feat: flux libre freeform (Free Designer) - palier 3, charte imposee
This commit is contained in:
+1427
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||||
+141
-6
@@ -1102,6 +1102,132 @@ class RenderEngineV2:
|
|||||||
item.get("label", ""), size=10, color=self.C["body"])
|
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 ----------------
|
# ---------------- orchestration ----------------
|
||||||
REGISTRY = {
|
REGISTRY = {
|
||||||
"cover_split": "_render_cover_split",
|
"cover_split": "_render_cover_split",
|
||||||
@@ -1117,6 +1243,7 @@ class RenderEngineV2:
|
|||||||
"phases_timeline": "_render_phases_timeline",
|
"phases_timeline": "_render_phases_timeline",
|
||||||
"recommendation_card": "_render_recommendation_card",
|
"recommendation_card": "_render_recommendation_card",
|
||||||
"end_slide": "_render_end_slide",
|
"end_slide": "_render_end_slide",
|
||||||
|
"freeform": "_render_freeform",
|
||||||
"from_to_pairs": "_render_from_to_pairs",
|
"from_to_pairs": "_render_from_to_pairs",
|
||||||
"gantt_timeline": "_render_gantt_timeline",
|
"gantt_timeline": "_render_gantt_timeline",
|
||||||
"yearly_timeline": "_render_yearly_timeline",
|
"yearly_timeline": "_render_yearly_timeline",
|
||||||
@@ -1148,12 +1275,20 @@ class RenderEngineV2:
|
|||||||
print(f" ⚠ Layout inconnu '{layout}' → default_bullets")
|
print(f" ⚠ Layout inconnu '{layout}' → default_bullets")
|
||||||
method = "_render_default_bullets"
|
method = "_render_default_bullets"
|
||||||
layout = "default_bullets"
|
layout = "default_bullets"
|
||||||
mode = self.layouts.get(layout, {}).get("mode", "light")
|
if layout == "freeform":
|
||||||
if mode == "light":
|
# Le freeform gère son fond lui-même selon sd["mode"]
|
||||||
self._bg(slide, self.C["white"])
|
if sd.get("mode", "light") == "light":
|
||||||
getattr(self, method)(slide, sd)
|
self._bg(slide, self.C["white"])
|
||||||
if layout not in excluded and layout != "recommendation_card":
|
getattr(self, method)(slide, sd)
|
||||||
self._footer(slide, i + 1)
|
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"])
|
||||||
|
getattr(self, method)(slide, sd)
|
||||||
|
if layout not in excluded and layout != "recommendation_card":
|
||||||
|
self._footer(slide, i + 1)
|
||||||
|
|
||||||
prs.save(output_path)
|
prs.save(output_path)
|
||||||
print(f"✓ PPTX généré : {output_path} ({len(slides)} slides)")
|
print(f"✓ PPTX généré : {output_path} ({len(slides)} slides)")
|
||||||
|
|||||||
Reference in New Issue
Block a user