#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ patch_render_engine_c5c.py — Chantier C5 · lot 3 (agenda, pyramid) ================================================================== Patch strict de render_engine_v2.py. Réintroduit les deux layouts v1 disparus à la refonte, restylés PR Editorial. Aucun prérequis sur les lots 1/2 (aucun helper chart utilisé). agenda (L40) — sommaire typographique aéré : badge numéroté (navy ; corail si actif: true), label (bold navy si actif), durée alignée à droite en muted, filet séparateur card_alt. Bornes : 2-8 sections (troncature tracée au-delà). pyramid (L41) — pyramide à degrés : rectangles empilés centrés, largeurs FIXES par étage (jamais recalculées) : 3 niveaux : 44 / 72 / 100 % 4 niveaux : 40 / 60 / 80 / 100 % Couleurs fixes du sommet à la base : navy, navy_light, slate, glacier (texte blanc, navy sur glacier). Descriptions optionnelles alignées à droite de chaque étage (la pyramide passe alors à 58 % de largeur) ; sans description, pyramide centrée sur 80 %. Usage (dossier du pipeline, single-line) : python3 patch_render_engine_c5c.py Vérifie chaque ancre, écrit .bak-c5c, compile, idempotent. """ import py_compile import shutil import sys from pathlib import Path TARGET = Path("render_engine_v2.py") COMPONENTS = ''' # ---------------- structure & concept (C5 lot 3) ---------------- AGENDA_MAX = 8 PYRAMID_WIDTHS = {3: (0.44, 0.72, 1.0), 4: (0.40, 0.60, 0.80, 1.0)} def _render_agenda(self, slide, d): """Sommaire : badges numérotés, section active en corail.""" self._title(slide, pick(d, "titre", "title")) sections = (d.get("sections") or [])[:self.AGENDA_MAX] if len(d.get("sections") or []) > self.AGENDA_MAX: print(f" ~ agenda : sections tronquées à {self.AGENDA_MAX}") if len(sections) < 2: self._text(slide, self.MX, self.CONT_Y, 12, 1.0, "[agenda : 2 sections minimum]", size=14, color=self.C["muted"]) return n = len(sections) gap = 0.5 row_h = min(1.9, (self.CONT_H - gap * (n - 1)) / n) tot = n * row_h + (n - 1) * gap y = self._cy(tot) bd = self.components["badge"]["sizes"]["m"] x0 = self.MX + 1.5 w = self.SLIDE_W - 2 * self.MX - 3.0 for i, s in enumerate(sections): if not isinstance(s, dict): s = {"label": str(s)} actif = bool(s.get("actif")) num = s.get("numero", i + 1) fill = self.C["coral"] if actif else self.C["navy"] self._badge(slide, x0 + bd / 2, y + row_h / 2, bd, num, fill=fill, font_size=20) self._text(slide, x0 + bd + 1.0, y, w - bd - 6.0, row_h, as_label(s, "label", "titre"), size=20 if actif else 19, bold=actif, color=self.C["navy"] if actif else self.C["body"], anchor=MSO_ANCHOR.MIDDLE) duree = pick(s, "duree", "duration") if duree: self._text(slide, x0 + w - 4.5, y, 4.5, row_h, duree, size=13, italic=True, color=self.C["muted"], align=PP_ALIGN.RIGHT, anchor=MSO_ANCHOR.MIDDLE, fit=False) if i < n - 1: self._rect(slide, x0 + bd + 1.0, y + row_h + gap / 2 - 0.015, w - bd - 1.0, 0.03, self.C["card"]) y += row_h + gap def _render_pyramid(self, slide, d): """Pyramide à degrés : largeurs fixes, couleurs fixes.""" self._title(slide, pick(d, "titre", "title")) niveaux = (d.get("niveaux") or d.get("levels") or [])[:4] if len(niveaux) < 3: self._text(slide, self.MX, self.CONT_Y, 12, 1.0, "[pyramid : 3 ou 4 niveaux]", size=14, color=self.C["muted"]) return n = len(niveaux) widths = self.PYRAMID_WIDTHS[n] colors = [self.C["navy"], self.C["navy2"], self.C["slate"], self.C["glacier"]] if n == 3: colors = [self.C["navy"], self.C["navy2"], self.C["glacier"]] has_desc = any(isinstance(nv, dict) and pick(nv, "description") for nv in niveaux) zone_w = self.SLIDE_W - 2 * self.MX pyr_w = zone_w * (0.58 if has_desc else 0.80) pyr_x = self.MX if has_desc else \ self.MX + (zone_w - pyr_w) / 2 cx = pyr_x + pyr_w / 2 gap = 0.15 avail = self.CONT_H - 0.4 stage_h = (avail - gap * (n - 1)) / n y = self._cy(avail) + 0.2 for i, nv in enumerate(niveaux): if not isinstance(nv, dict): nv = {"label": str(nv)} wt = widths[i] * pyr_w color = colors[i] txt_color = self.C["navy"] if color == self.C["glacier"] \ else self.C["white"] self._rect(slide, cx - wt / 2, y, wt, stage_h, color) self._text(slide, cx - wt / 2 + 0.3, y, wt - 0.6, stage_h, as_label(nv, "label", "titre"), size=16, bold=True, color=txt_color, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) desc = pick(nv, "description", "detail") if desc: dx = pyr_x + pyr_w + 1.2 self._text(slide, dx, y, self.SLIDE_W - self.MX - dx, stage_h, desc, size=13, color=self.C["body"], anchor=MSO_ANCHOR.MIDDLE) y += stage_h + gap ''' PATCHES = [ # REGISTRY ( " \"matrix_2x2\": \"_render_matrix_2x2\",\n", " \"matrix_2x2\": \"_render_matrix_2x2\",\n" " \"agenda\": \"_render_agenda\",\n" " \"pyramid\": \"_render_pyramid\",\n", ), ] MARKER = "_render_agenda" ANCHOR_ORCH = " # ---------------- orchestration ----------------" 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é (_render_agenda 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)) if content.count(ANCHOR_ORCH) != 1: fail("Ancre de la section orchestration introuvable ou non " "unique.") shutil.copy2(TARGET, str(TARGET) + ".bak-c5c") print(" + Sauvegarde : %s.bak-c5c" % TARGET) for old, new in PATCHES: content = content.replace(old, new) lines = content.split("\n") idx = next(i for i, l in enumerate(lines) if ANCHOR_ORCH in l) lines[idx:idx] = COMPONENTS.split("\n") content = "\n".join(lines) TARGET.write_text(content, encoding="utf-8") print(" + Renderers agenda + pyramid + REGISTRY appliqués.") try: py_compile.compile(str(TARGET), doraise=True) print(" + Compilation OK.") except py_compile.PyCompileError as e: shutil.copy2(str(TARGET) + ".bak-c5c", TARGET) fail("Erreur de compilation — fichier restauré :\n%s" % e) print("\n Suite : python3 patch_layouts_c5c.py puis propagation " "(prompt_injection_v2 + build_gallery).") if __name__ == "__main__": main()