Files
sliding-automation/patches/patch_render_engine_c6.py

366 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
patch_render_engine_c6.py — Chantier C6 (Freeform palier 4)
===========================================================
« Charte minimale, vocabulaire maximal. » Patch strict de
render_engine_v2.py. PRÉREQUIS : C5 lot 2 appliqué (_blend, MSO_LINE).
P1. _token_color v2 — dérivés paramétrés : 'navy@15' = navy éclairci
de 15 % vers blanc. L'espace couleur reste ENTIÈREMENT engendré
par la charte (aucun hex, jamais), mais passe de 9 couleurs à un
continuum de tons de marque. Regex stricte, inconnu → défaut.
P2. _render_freeform v2 :
- background: <token[@NN]> au niveau slide (remplace/complète
mode light|dark ; la couleur du texte par défaut s'adapte).
- full_bleed: true par bloc — la grille 12×12 mappe alors la
slide ENTIÈRE (aplats bord à bord, colonnes pleine hauteur).
- Nouveau type shape : chevron, arrow, triangle, pill, donut,
bracket_left/right, oval, diamond, hexagon, parallelogram,
moon (douzaine curée MSO_SHAPE) + texte centré optionnel.
- Propriétés transverses sur tout bloc : alpha (0-100),
rotation (arrondie au pas de 15°), border {color, weight},
radius (0-0.5, rects arrondis).
- line : diagonales natives (w ET h non nuls) + dash: true.
- Plafond moteur 15 blocs (troncature tracée — le validateur
du facilitator bloque avant en usage normal).
Rien n'est ouvert sur : les polices, les hex, le footer/logo.
Usage (dossier du pipeline, single-line) :
python3 patches/patch_render_engine_c6.py
(ou depuis la racine si le script y est copié)
Vérifie chaque ancre, écrit .bak-c6, compile, idempotent.
"""
import py_compile
import shutil
import sys
from pathlib import Path
TARGET = Path("render_engine_v2.py")
OLD_TOKEN_COLOR = ''' def _token_color(self, token: str, default: str = None) -> str:
mapping = {
"navy": self.C["navy"], "navy_light": self.C["navy2"],
"coral": self.C["coral"], "glacier": self.C["glacier"],
"slate": self.C["slate"], "card": self.C["card"],
"white": self.C["white"], "body": self.C["body"],
"muted": self.C["muted"],
}
return mapping.get((token or "").strip().lower(),
default or self.C["navy"])
'''
NEW_TOKEN_COLOR = ''' _TOKEN_RE = re.compile(r"^([a-z_]+)(?:@(\\d{1,3}))?$")
def _token_color(self, token: str, default: str = None) -> str:
"""Tokens de la charte + dérivés paramétrés (C6, palier 4) :
'navy@15' = navy éclairci de 15 % vers blanc. L'espace couleur
reste engendré par la charte — jamais de hex."""
mapping = {
"navy": self.C["navy"], "navy_light": self.C["navy2"],
"coral": self.C["coral"], "glacier": self.C["glacier"],
"slate": self.C["slate"], "card": self.C["card"],
"card_alt": self.C["card_alt"],
"white": self.C["white"], "body": self.C["body"],
"muted": self.C["muted"],
}
m = self._TOKEN_RE.match((token or "").strip().lower())
if not m:
return default or self.C["navy"]
base = mapping.get(m.group(1))
if base is None:
return default or self.C["navy"]
if m.group(2) is not None:
pct = min(100, int(m.group(2)))
return _blend(base, "#FFFFFF", pct / 100.0)
return base
'''
OLD_FREEFORM_START = " def _render_freeform(self, slide, d):\n"
OLD_FREEFORM_END = (' '
'PP_ALIGN.LEFT)\n')
NEW_FREEFORM = ''' FREE_SHAPES = {
"chevron": MSO_SHAPE.CHEVRON,
"arrow": MSO_SHAPE.RIGHT_ARROW,
"triangle": MSO_SHAPE.ISOSCELES_TRIANGLE,
"pill": MSO_SHAPE.ROUNDED_RECTANGLE,
"donut": MSO_SHAPE.DONUT,
"bracket_left": MSO_SHAPE.LEFT_BRACKET,
"bracket_right": MSO_SHAPE.RIGHT_BRACKET,
"oval": MSO_SHAPE.OVAL,
"diamond": MSO_SHAPE.DIAMOND,
"hexagon": MSO_SHAPE.HEXAGON,
"parallelogram": MSO_SHAPE.PARALLELOGRAM,
"moon": MSO_SHAPE.MOON,
}
_DARK_BASES = {"navy", "navy_light", "slate", "body"}
FREE_MAX_BLOCKS = 15
def _set_fill_alpha(self, sh, alpha_pct):
"""Transparence du remplissage (voile) — silencieux si le
shape n'a pas de solidFill (textbox…)."""
try:
srgb = sh._element.spPr.find(qn("a:solidFill")).find(
qn("a:srgbClr"))
a = srgb.makeelement(
qn("a:alpha"),
{"val": str(int(max(0.0, min(100.0,
float(alpha_pct))) * 1000))})
srgb.append(a)
except Exception:
pass
def _free_geom(self, blk, full_bleed):
"""Grille 12×12 : zone utile par défaut, slide entière en
full_bleed."""
col = float(blk.get("col", 0))
row = float(blk.get("row", 0))
wc = float(blk.get("w", 4))
hr = float(blk.get("h", 1))
if full_bleed:
return ((col / self.FREE_COLS) * self.SLIDE_W,
(row / self.FREE_ROWS) * self.SLIDE_H,
(wc / self.FREE_COLS) * self.SLIDE_W,
(hr / self.FREE_ROWS) * self.SLIDE_H)
return (self._free_x(col), self._free_y(row),
self._free_w(wc), self._free_h(hr))
def _free_decorate(self, sh, blk):
"""Propriétés transverses du palier 4 : alpha, rotation (pas
de 15°), border {color, weight}, radius. Silencieux quand une
propriété ne s'applique pas au type de shape."""
if sh is None:
return
if blk.get("alpha") is not None:
self._set_fill_alpha(sh, blk["alpha"])
rot = blk.get("rotation")
if rot:
try:
sh.rotation = (round(float(rot) / 15.0) * 15) % 360
except Exception:
pass
border = blk.get("border")
if isinstance(border, dict):
try:
sh.line.color.rgb = hex_to_rgb(self._token_color(
border.get("color"), self.C["navy"]))
sh.line.width = Pt(float(border.get("weight", 1.0)))
except Exception:
pass
if blk.get("radius") is not None:
try:
sh.adjustments[0] = max(0.0, min(
0.5, float(blk["radius"])))
except Exception:
pass
def _render_freeform(self, slide, d):
mode = d.get("mode", "light")
bg = d.get("background")
on_dark = (mode == "dark")
if bg:
self._bg(slide, self._token_color(bg, self.C["white"]))
m = self._TOKEN_RE.match(str(bg).strip().lower())
if m and m.group(1) in self._DARK_BASES:
pct = int(m.group(2)) if m.group(2) is not None else 0
on_dark = pct < 45
else:
on_dark = False
elif on_dark:
self._bg(slide, self.C["navy"])
default_text = self.C["white"] if on_dark else self.C["body"]
blocks = d.get("blocks", [])
if len(blocks) > self.FREE_MAX_BLOCKS:
print(f" ~ freeform : {len(blocks)} blocs → "
f"{self.FREE_MAX_BLOCKS} (plafond moteur)")
blocks = blocks[:self.FREE_MAX_BLOCKS]
for blk in blocks:
btype = (blk.get("type") or "text").lower()
fb = bool(blk.get("full_bleed"))
x, y, w, h = self._free_geom(blk, fb)
sh = None
if btype == "rect":
sh = self._rect(slide, x, y, w, h,
self._token_color(blk.get("color"),
self.C["card"]),
rounded=blk.get("rounded", False))
elif btype == "card":
sh = self._card(slide, x, y, w, h,
self._token_color(blk.get("color"),
self.C["card"]))
elif btype == "circle":
d_cm = min(w, h)
sh = self._oval(slide, x, y, d_cm,
self._token_color(blk.get("color"),
self.C["coral"]))
elif btype == "shape":
kind = self.FREE_SHAPES.get(
str(blk.get("shape") or "oval").lower())
if kind is None:
print(f" ~ freeform : shape "
f"'{blk.get('shape')}' inconnue → oval")
kind = MSO_SHAPE.OVAL
sh = slide.shapes.add_shape(
kind, Cm(x), Cm(y), Cm(w), Cm(h))
sh.fill.solid()
sh.fill.fore_color.rgb = hex_to_rgb(
self._token_color(blk.get("color"),
self.C["coral"]))
sh.line.fill.background()
sh.shadow.inherit = False
if blk.get("text"):
tf = sh.text_frame
tf.word_wrap = True
tf.text = str(blk["text"])
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.CENTER
for r in p.runs:
r.font.name = self.F_BODY
r.font.size = Pt(int(blk.get("size", 14)))
r.font.bold = True
r.font.color.rgb = hex_to_rgb(
self._token_color(blk.get("text_color"),
self.C["white"]))
elif btype == "line":
sh = slide.shapes.add_connector(
1, Cm(x), Cm(y), Cm(x + w), Cm(y + h))
sh.line.color.rgb = hex_to_rgb(
self._token_color(blk.get("color"),
self.C["muted"]))
sh.line.width = Pt(float(blk.get("weight", 1.25)))
if blk.get("dash"):
sh.line.dash_style = MSO_LINE.DASH
sh.shadow.inherit = False
sh = None # pas de décoration fill sur ligne
elif btype == "image":
sh = self._image(slide, x, y, w, h,
blk.get("image") or blk.get("src", ""),
fit=str(blk.get("fit") or "cover"))
elif btype == "badge":
d_cm = min(w, h)
sh = self._badge(slide, x + d_cm / 2, y + d_cm / 2,
d_cm, blk.get("text", ""),
fill=self._token_color(
blk.get("color"), self.C["navy"]))
elif btype == "stat":
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
font=self.F_DISPLAY,
size=int(blk.get("size", 72)),
bold=True,
color=self._token_color(
blk.get("color"), self.C["coral"]),
align=self._free_align(
blk.get("align", "left")),
anchor=MSO_ANCHOR.MIDDLE)
elif btype in ("title", "heading"):
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
font=self.F_DISPLAY,
size=int(blk.get("size", 28)),
bold=True,
color=self._token_color(
blk.get("color"),
self.C["white"] if on_dark
else self.C["navy"]),
align=self._free_align(
blk.get("align", "left")),
anchor=MSO_ANCHOR.MIDDLE)
else: # text
font = self.F_DISPLAY if blk.get("serif") \
else self.F_BODY
sh = self._text(slide, x, y, w, h, blk.get("text", ""),
font=font,
size=int(blk.get("size", 16)),
bold=blk.get("bold", False),
italic=blk.get("italic", False),
color=self._token_color(
blk.get("color"), default_text),
align=self._free_align(
blk.get("align", "left")),
anchor=MSO_ANCHOR.TOP)
self._free_decorate(sh, blk)
def _free_align(self, a: str):
return {"left": PP_ALIGN.LEFT, "center": PP_ALIGN.CENTER,
"right": PP_ALIGN.RIGHT}.get((a or "left").lower(),
PP_ALIGN.LEFT)
'''
MARKER = "_free_decorate"
PREREQ = "_blend"
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)
content = TARGET.read_text(encoding="utf-8")
if MARKER in content:
fail("Déjà patché (_free_decorate présent) — rien à faire.")
if ("def " + PREREQ) not in content:
fail("Prérequis manquant : C5 lot 2 (_blend) doit être appliqué "
"avant C6.")
# P1 — _token_color
if content.count(OLD_TOKEN_COLOR) != 1:
fail("Ancre _token_color introuvable ou non unique — moteur "
"inattendu.")
# P2 — bloc freeform complet (de def _render_freeform à la fin de
# _free_align)
start = content.find(OLD_FREEFORM_START)
if start == -1:
fail("Ancre _render_freeform introuvable.")
end = content.find(OLD_FREEFORM_END, start)
if end == -1:
fail("Fin du bloc freeform (_free_align) introuvable — moteur "
"inattendu.")
end += len(OLD_FREEFORM_END)
shutil.copy2(TARGET, str(TARGET) + ".bak-c6")
print(" + Sauvegarde : %s.bak-c6" % TARGET)
content = content.replace(OLD_TOKEN_COLOR, NEW_TOKEN_COLOR)
# recalcul des offsets après P1
start = content.find(OLD_FREEFORM_START)
end = content.find(OLD_FREEFORM_END, start) + len(OLD_FREEFORM_END)
content = content[:start] + NEW_FREEFORM + content[end:]
TARGET.write_text(content, encoding="utf-8")
print(" + _token_color v2 (dérivés @NN) + freeform palier 4 "
"appliqués.")
try:
py_compile.compile(str(TARGET), doraise=True)
print(" + Compilation OK.")
except py_compile.PyCompileError as e:
shutil.copy2(str(TARGET) + ".bak-c6", TARGET)
fail("Erreur de compilation — fichier restauré :\n%s" % e)
print("\n Suite C6 : python3 patches/patch_facilitator_c6.py "
"(validateur 2 niveaux),")
print(" puis remplacer le prompt Free Designer dans Mistral Studio"
" (v2, temp 0.7).")
if __name__ == "__main__":
main()