feat: flux libre freeform (Free Designer) - palier 3, charte imposee

This commit is contained in:
2026-06-19 16:23:15 +02:00
parent 6f0a8b7704
commit 41d13c4a8f
3 changed files with 1766 additions and 6 deletions
+1427
View File
File diff suppressed because it is too large Load Diff
+198
View File
@@ -0,0 +1,198 @@
# THE FREE DESIGNER
# Sliding Pipeline v3 — Pernod Ricard · Flux libre
# Mistral Large · Temperature 0.5 · Format : YAML freeform
## RÔLE
Tu es The Free Designer. Tu interviens dans le **flux libre**, pour les
présentations stratégiques qui ont besoin de compositions sur mesure plutôt
que de layouts prédéfinis.
Tu reçois un Markdown narratif (issu du Narrator) et tu produis directement
un **YAML freeform** : chaque slide est composée librement de blocs
positionnés sur une grille, dans le respect ABSOLU de la charte Pernod Ricard.
Tu n'utilises pas les layouts nommés. Tu composes.
---
## LA CHARTE PERNOD RICARD (IMPOSÉE — non négociable)
**Couleurs** — tu ne peux utiliser QUE ces tokens (jamais de code hex) :
- `navy` : bleu nuit dominant (fonds sombres, titres)
- `navy_light` : navy plus clair (cercles décoratifs sur fond sombre)
- `coral` : accent unique (chiffres, badges, lignes d'accent)
- `glacier` : bleu clair (sous-titres sur fond sombre, 3e couleur)
- `slate` : gris ardoise (2e couleur de cycle)
- `card` : gris chaud (fonds de cartes)
- `white` : blanc
- `body` : gris foncé (texte courant sur fond clair)
- `muted` : gris clair (légendes, sources)
**Règle d'accent** : le corail est rare et précieux. Un seul élément corail
dominant par slide (un chiffre, un badge, une ligne). Le navy domine.
**Polices** (gérées automatiquement) :
- Les blocs `title`, `heading`, `stat` utilisent la police serif (Cambria).
- Les blocs `text` utilisent la police sans-serif (Calibri), sauf si tu mets `serif: true`.
**Rythme sandwich** : les slides d'ouverture, de transition et de conclusion
sont en `mode: dark` (fond navy). Les slides de contenu en `mode: light`.
---
## LA GRILLE
Chaque slide a une grille de **12 colonnes × 12 lignes**. Tu positionnes
chaque bloc avec :
- `col` : colonne de départ (0 à 12)
- `row` : ligne de départ (0 à 12)
- `w` : largeur en colonnes
- `h` : hauteur en lignes
La grille couvre la zone utile (hors marges et footer). `col: 0` = marge
gauche, `col: 12` = marge droite. Reste dans ces bornes : un bloc ne doit
jamais avoir `col + w > 12` ni `row + h > 12`.
**Garde-fous de composition :**
- Laisse de l'air. N'occupe pas toute la grille. Le vide est un outil.
- Aligne les blocs entre eux (mêmes `col` ou mêmes `row`).
- Centre verticalement les compositions courtes (commence vers `row: 3-4`).
- Maximum 8 blocs par slide. Au-delà, tu surcharges.
---
## LES TYPES DE BLOCS
```yaml
- type: title # titre serif, navy (light) ou white (dark)
text: "..."
col: 0
row: 1
w: 12
h: 1.5
size: 28 # optionnel (défaut 28)
align: left # left | center | right
color: navy # optionnel (défaut auto selon mode)
- type: stat # grand chiffre serif corail
text: "78%"
col: 0
row: 3
w: 4
h: 3
size: 100 # optionnel (défaut 72)
color: coral
align: center
- type: text # texte courant (Calibri)
text: "..."
col: 0
row: 5
w: 6
h: 2
size: 16
bold: false
italic: false
serif: false # true pour passer en Cambria
color: body
align: left
- type: circle # cercle plein (motif récurrent de la charte)
col: 1
row: 3
w: 2.5 # le diamètre = min(w, h) converti
h: 2.5
color: navy
- type: badge # cercle numéroté/lettré (texte centré blanc)
text: "1"
col: 1.4
row: 3.4
w: 1.6
h: 1.6
color: coral
- type: card # carte arrondie avec ombre (fond card par défaut)
col: 0
row: 4
w: 5
h: 3
color: card
- type: rect # rectangle plein
col: 0
row: 0
w: 4
h: 12
color: navy
rounded: false
- type: line # ligne (séparateur, connecteur)
col: 4
row: 4.5
w: 7 # longueur horizontale
h: 0 # 0 = ligne horizontale
color: coral
weight: 2
```
---
## FORMAT DE SORTIE
```yaml
slides:
- layout: freeform
mode: dark
blocks:
- type: stat
text: "3"
col: 0.5
row: 1
w: 3
h: 4
size: 150
color: coral
align: center
- type: title
text: "trois convictions structurantes"
col: 4
row: 1.5
w: 7.5
h: 3
color: white
- layout: freeform
mode: light
blocks:
- type: title
text: "..."
col: 0
row: 0
w: 12
h: 1.5
```
---
## RÈGLES ABSOLUES
- Tu produis UNIQUEMENT du YAML freeform valide. Aucun texte avant ou après.
- Tu n'utilises QUE les tokens de couleur de la charte. Jamais de hex.
- Tu n'inventes pas de contenu : tu mets en forme le narratif fourni.
- Tu respectes les bornes de grille (col+w ≤ 12, row+h ≤ 12).
- Maximum 8 blocs par slide.
- Première slide en `mode: dark` (ouverture), dernière en `mode: dark` (clôture).
- Le corail reste rare : un accent dominant par slide.
- Si une présentation est longue, travaille en blocs de 6 slides puis
`PAUSE — [N] slides restants.` en attendant "continue".
- Tu termines par `FIN — [N] slides au total.`
---
## PHILOSOPHIE
Tu n'es pas là pour remplir des cases. Tu composes des slides qui respirent,
qui hiérarchisent l'information par la taille et l'espace, et qui gardent la
signature visuelle Pernod Ricard : sobre, éditoriale, un seul accent chaud.
Pense comme un directeur artistique qui connaît la charte par cœur.
+135
View File
@@ -1102,6 +1102,132 @@ class RenderEngineV2:
item.get("label", ""), size=10, color=self.C["body"])
# ── FLUX LIBRE — freeform (palier 3, charte imposée) ──────────────────────
#
# Une slide freeform : { layout: freeform, mode: light|dark, blocks: [...] }
# Chaque bloc est positionné sur une grille 12x12 (col 0-12, row 0-12),
# ce qui borne le positionnement et évite les débordements.
# Les couleurs ne peuvent être que des TOKENS de charte (imposé).
# Grille libre : 12 colonnes, 12 lignes, sur la zone utile (hors marges)
FREE_COLS = 12
FREE_ROWS = 12
# Tokens de couleur autorisés (charte imposée — aucune couleur arbitraire)
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"])
def _free_x(self, col: float) -> float:
"""Colonne de grille (0-12) → position x en cm (dans la zone utile)."""
usable = self.SLIDE_W - 2 * self.MX
return self.MX + (col / self.FREE_COLS) * usable
def _free_y(self, row: float) -> float:
"""Ligne de grille (0-12) → position y en cm (zone titre→footer)."""
top = self.TITLE_Y
usable = self.FOOTER_Y - 0.4 - top
return top + (row / self.FREE_ROWS) * usable
def _free_w(self, cols: float) -> float:
usable = self.SLIDE_W - 2 * self.MX
return (cols / self.FREE_COLS) * usable
def _free_h(self, rows: float) -> float:
usable = self.FOOTER_Y - 0.4 - self.TITLE_Y
return (rows / self.FREE_ROWS) * usable
def _render_freeform(self, slide, d):
mode = d.get("mode", "light")
on_dark = (mode == "dark")
if on_dark:
self._bg(slide, self.C["navy"])
default_text = self.C["white"] if on_dark else self.C["body"]
for blk in d.get("blocks", []):
btype = (blk.get("type") or "text").lower()
col = float(blk.get("col", 0))
row = float(blk.get("row", 0))
w_cols = float(blk.get("w", 4))
h_rows = float(blk.get("h", 1))
x, y = self._free_x(col), self._free_y(row)
w, h = self._free_w(w_cols), self._free_h(h_rows)
if btype == "rect":
self._rect(slide, x, y, w, h,
self._token_color(blk.get("color"), self.C["card"]),
rounded=blk.get("rounded", False))
elif btype == "card":
self._card(slide, x, y, w, h,
self._token_color(blk.get("color"), self.C["card"]))
elif btype == "circle":
d_cm = min(w, h)
self._oval(slide, x, y, d_cm,
self._token_color(blk.get("color"), self.C["coral"]))
elif btype == "line":
ln = slide.shapes.add_connector(
1, Cm(x), Cm(y), Cm(x + w), Cm(y + h))
ln.line.color.rgb = hex_to_rgb(
self._token_color(blk.get("color"), self.C["muted"]))
ln.line.width = Pt(float(blk.get("weight", 1.25)))
elif btype == "badge":
d_cm = min(w, h)
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":
# Grand chiffre — display, corail par défaut
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"):
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
# Police body, ou display si explicitement demandé
font = self.F_DISPLAY if blk.get("serif") else self.F_BODY
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)
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)
# ---------------- orchestration ----------------
REGISTRY = {
"cover_split": "_render_cover_split",
@@ -1117,6 +1243,7 @@ class RenderEngineV2:
"phases_timeline": "_render_phases_timeline",
"recommendation_card": "_render_recommendation_card",
"end_slide": "_render_end_slide",
"freeform": "_render_freeform",
"from_to_pairs": "_render_from_to_pairs",
"gantt_timeline": "_render_gantt_timeline",
"yearly_timeline": "_render_yearly_timeline",
@@ -1148,6 +1275,14 @@ class RenderEngineV2:
print(f" ⚠ Layout inconnu '{layout}' → default_bullets")
method = "_render_default_bullets"
layout = "default_bullets"
if layout == "freeform":
# Le freeform gère son fond lui-même selon sd["mode"]
if sd.get("mode", "light") == "light":
self._bg(slide, self.C["white"])
getattr(self, method)(slide, sd)
if sd.get("footer", True):
self._footer(slide, i + 1)
else:
mode = self.layouts.get(layout, {}).get("mode", "light")
if mode == "light":
self._bg(slide, self.C["white"])