chore: regroupement des scripts de patch dans patches/
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
patch_render_engine_c3.py — Chantier C3 (Moteur durci)
|
||||
======================================================
|
||||
Patch strict de render_engine_v2.py :
|
||||
P1. Import optionnel de measure.py (mesure PIL réelle).
|
||||
P2. estimate_text_height() délègue à la mesure réelle quand elle est
|
||||
disponible (fallback : heuristique v2 inchangée) + ajoute
|
||||
strip_markdown_tree() (nettoyage récursif du Markdown résiduel).
|
||||
P3. Fitter anti-débordement dans _text() : si le texte mesuré dépasse
|
||||
la zone, réduction par pas de 1 pt (plancher 60 % du nominal) puis
|
||||
troncature avec « … » — chaque ajustement est tracé en console.
|
||||
Désactivable par appel (fit=False) ou globalement (FIT_TEXT=0).
|
||||
P4. Nettoyage Markdown appliqué à TOUTE donnée entrante de render().
|
||||
P5. Speaker notes : champ notes: par slide → zone notes PowerPoint.
|
||||
|
||||
Usage (dossier du pipeline, single-line) :
|
||||
python3 patch_render_engine_c3.py
|
||||
Prérequis : measure.py à côté (sinon P1/P3 restent inertes, sans casse).
|
||||
Vérifie chaque ancre, écrit render_engine_v2.py.bak-c3, compile,
|
||||
idempotent.
|
||||
"""
|
||||
|
||||
import py_compile
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TARGET = Path("render_engine_v2.py")
|
||||
|
||||
PATCHES = [
|
||||
# P1 — import measure (optionnel)
|
||||
(
|
||||
"from pptx.util import Cm, Emu, Pt\n",
|
||||
"from pptx.util import Cm, Emu, Pt\n"
|
||||
"\n"
|
||||
"try:\n"
|
||||
" import measure\n"
|
||||
" HAS_MEASURE = True\n"
|
||||
"except ImportError:\n"
|
||||
" HAS_MEASURE = False\n"
|
||||
"FIT_TEXT = os.getenv(\"FIT_TEXT\", \"1\") != \"0\" # C3\n",
|
||||
),
|
||||
# P2 — mesure réelle + strip_markdown_tree
|
||||
(
|
||||
"def estimate_text_height(text: str, size_pt: int, width_cm: float)"
|
||||
" -> float:\n"
|
||||
" \"\"\"Hauteur estimée d'un texte (cm) pour une largeur donnée."
|
||||
"\"\"\"\n"
|
||||
" if not text:\n"
|
||||
" return 0.0\n"
|
||||
" char_w_cm = size_pt * 0.0185 # largeur moyenne d'un "
|
||||
"caractère\n"
|
||||
" chars_per_line = max(1, int(width_cm / char_w_cm))\n"
|
||||
" lines = 0\n"
|
||||
" for para in str(text).split(\"\\n\"):\n"
|
||||
" lines += max(1, -(-len(para) // chars_per_line))\n"
|
||||
" return lines * size_pt * 0.0455 # hauteur de ligne ≈ 1.3"
|
||||
" em\n",
|
||||
"def estimate_text_height(text: str, size_pt: int, width_cm: float,"
|
||||
"\n"
|
||||
" font_name: str = \"Calibri\",\n"
|
||||
" bold: bool = False) -> float:\n"
|
||||
" \"\"\"Hauteur d'un texte (cm). Mesure réelle PIL si disponible"
|
||||
" (C3),\n"
|
||||
" sinon heuristique v2 inchangée.\"\"\"\n"
|
||||
" if not text:\n"
|
||||
" return 0.0\n"
|
||||
" if HAS_MEASURE:\n"
|
||||
" try:\n"
|
||||
" return measure.text_height_cm(str(text), font_name,\n"
|
||||
" size_pt, width_cm, bold)"
|
||||
"\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" char_w_cm = size_pt * 0.0185 # largeur moyenne d'un "
|
||||
"caractère\n"
|
||||
" chars_per_line = max(1, int(width_cm / char_w_cm))\n"
|
||||
" lines = 0\n"
|
||||
" for para in str(text).split(\"\\n\"):\n"
|
||||
" lines += max(1, -(-len(para) // chars_per_line))\n"
|
||||
" return lines * size_pt * 0.0455 # hauteur de ligne ≈ 1.3"
|
||||
" em\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"_MD_RES = [\n"
|
||||
" (re.compile(r\"\\*\\*(.+?)\\*\\*\"), r\"\\1\"),\n"
|
||||
" (re.compile(r\"__(.+?)__\"), r\"\\1\"),\n"
|
||||
" (re.compile(r\"`([^`]+)`\"), r\"\\1\"),\n"
|
||||
" (re.compile(r\"^#{1,4}\\s+\"), \"\"),\n"
|
||||
" (re.compile(r\"^[-•]\\s+\"), \"\"),\n"
|
||||
"]\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def strip_markdown_tree(node):\n"
|
||||
" \"\"\"Nettoyage récursif du Markdown résiduel (C3) : gras,\n"
|
||||
" italique, code inline, titres et puces en tête de valeur.\"\"\""
|
||||
"\n"
|
||||
" if isinstance(node, dict):\n"
|
||||
" return {k: strip_markdown_tree(v) for k, v in node.items()}"
|
||||
"\n"
|
||||
" if isinstance(node, list):\n"
|
||||
" return [strip_markdown_tree(v) for v in node]\n"
|
||||
" if isinstance(node, str):\n"
|
||||
" s = node\n"
|
||||
" for rx, rep in _MD_RES:\n"
|
||||
" s = rx.sub(rep, s)\n"
|
||||
" return s\n"
|
||||
" return node\n",
|
||||
),
|
||||
# P3 — fitter dans _text
|
||||
(
|
||||
" def _text(self, slide, x, y, w, h, txt, *, font=None, size=14,"
|
||||
"\n"
|
||||
" bold=False, italic=False, color=None, align=PP_ALIGN."
|
||||
"LEFT,\n"
|
||||
" anchor=MSO_ANCHOR.TOP, spacing=None, char_spacing="
|
||||
"None):\n"
|
||||
" tb = slide.shapes.add_textbox(Cm(x), Cm(y), Cm(w), Cm(h))\n",
|
||||
" def _text(self, slide, x, y, w, h, txt, *, font=None, size=14,"
|
||||
"\n"
|
||||
" bold=False, italic=False, color=None, align=PP_ALIGN."
|
||||
"LEFT,\n"
|
||||
" anchor=MSO_ANCHOR.TOP, spacing=None, char_spacing="
|
||||
"None,\n"
|
||||
" fit=True):\n"
|
||||
" if (fit and FIT_TEXT and HAS_MEASURE and txt\n"
|
||||
" and h and h > 0.3 and w and w > 0.5):\n"
|
||||
" _ratio = (spacing / size) if spacing else None\n"
|
||||
" _s, _t, _tr = measure.fit_text(\n"
|
||||
" str(txt), font or self.F_BODY, size, w, h,\n"
|
||||
" bold=bold, line_ratio=_ratio)\n"
|
||||
" if _s != size or _tr:\n"
|
||||
" print(f\" ~ fit slide \"\n"
|
||||
" f\"{getattr(self, '_slide_num', '?')} : \"\n"
|
||||
" f\"{size}→{_s} pt\"\n"
|
||||
" + (\" +troncature\" if _tr else \"\"))\n"
|
||||
" size, txt = _s, _t\n"
|
||||
" tb = slide.shapes.add_textbox(Cm(x), Cm(y), Cm(w), Cm(h))\n",
|
||||
),
|
||||
# P4 — strip à l'entrée de render()
|
||||
(
|
||||
" if isinstance(data, str):\n"
|
||||
" data = yaml.safe_load(data)\n"
|
||||
" slides = data.get(\"slides\", data) if isinstance(data, "
|
||||
"dict) else data\n",
|
||||
" if isinstance(data, str):\n"
|
||||
" data = yaml.safe_load(data)\n"
|
||||
" data = strip_markdown_tree(data)\n"
|
||||
" slides = data.get(\"slides\", data) if isinstance(data, "
|
||||
"dict) else data\n",
|
||||
),
|
||||
# P5 — speaker notes
|
||||
(
|
||||
" if layout not in excluded and layout != "
|
||||
"\"recommendation_card\":\n"
|
||||
" self._footer(slide, i + 1)\n"
|
||||
"\n"
|
||||
" prs.save(output_path)\n",
|
||||
" if layout not in excluded and layout != "
|
||||
"\"recommendation_card\":\n"
|
||||
" self._footer(slide, i + 1)\n"
|
||||
" notes = sd.get(\"notes\") if isinstance(sd, dict) else"
|
||||
" None\n"
|
||||
" if notes:\n"
|
||||
" slide.notes_slide.notes_text_frame.text = "
|
||||
"str(notes)\n"
|
||||
"\n"
|
||||
" prs.save(output_path)\n",
|
||||
),
|
||||
]
|
||||
|
||||
MARKER = "strip_markdown_tree"
|
||||
|
||||
|
||||
def fail(msg):
|
||||
print(" ! %s" % msg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if not TARGET.exists():
|
||||
fail("%s introuvable — lancer depuis le dossier du pipeline."
|
||||
% TARGET)
|
||||
if not Path("measure.py").exists():
|
||||
print(" ~ measure.py absent : le moteur restera sur l'heuristique"
|
||||
" tant qu'il n'est pas déposé (patch appliqué quand même).")
|
||||
content = TARGET.read_text(encoding="utf-8")
|
||||
if MARKER in content:
|
||||
fail("Déjà patché (strip_markdown_tree présent) — rien à faire.")
|
||||
|
||||
for i, (old, _) in enumerate(PATCHES, 1):
|
||||
n = content.count(old)
|
||||
if n == 0:
|
||||
fail("Ancre du patch %d introuvable — moteur inattendu." % i)
|
||||
if n > 1:
|
||||
fail("Ancre du patch %d non unique (%d occurrences)." % (i, n))
|
||||
|
||||
shutil.copy2(TARGET, str(TARGET) + ".bak-c3")
|
||||
print(" + Sauvegarde : %s.bak-c3" % TARGET)
|
||||
|
||||
for old, new in PATCHES:
|
||||
content = content.replace(old, new)
|
||||
|
||||
TARGET.write_text(content, encoding="utf-8")
|
||||
print(" + 5 patchs appliqués.")
|
||||
try:
|
||||
py_compile.compile(str(TARGET), doraise=True)
|
||||
print(" + Compilation OK.")
|
||||
except py_compile.PyCompileError as e:
|
||||
shutil.copy2(str(TARGET) + ".bak-c3", TARGET)
|
||||
fail("Erreur de compilation — fichier restauré :\n%s" % e)
|
||||
print("\n Prérequis mesure réelle : pip3 install Pillow (venv) et TTF"
|
||||
" dans assets/fonts/")
|
||||
print(" Diagnostic : python3 measure.py")
|
||||
print(" Désactivation d'urgence du fitter : FIT_TEXT=0 dans .env")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user