Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bcb5e6d191 | |||
| f17b1696d5 | |||
| dc92171f82 |
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
check_goldens.py — C0 · goldens de non-régression du moteur
|
||||
===========================================================
|
||||
Rend chaque golden_*.yaml présent et compare sa STRUCTURE (nb de
|
||||
slides, nb de shapes par slide) au manifeste goldens_manifest.json.
|
||||
Le PPTX contient des timestamps : un diff binaire est impossible — la
|
||||
structure, elle, est déterministe et attrape les régressions de rendu
|
||||
(shape manquante, layout cassé, exception).
|
||||
|
||||
python3 check_goldens.py → vérifie (code retour 1 si écart)
|
||||
python3 check_goldens.py --update → (re)génère le manifeste
|
||||
|
||||
À lancer avant tout push moteur (ou en tâche DSM hebdo).
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MANIFEST = Path("goldens_manifest.json")
|
||||
|
||||
|
||||
def structure(pptx_path):
|
||||
from pptx import Presentation
|
||||
prs = Presentation(pptx_path)
|
||||
return [len(list(s.shapes)) for s in prs.slides]
|
||||
|
||||
|
||||
def render_all():
|
||||
out = {}
|
||||
for y in sorted(glob.glob("golden_*.yaml")):
|
||||
pptx = "_check_%s.pptx" % Path(y).stem
|
||||
r = subprocess.run(
|
||||
["python3", "render_engine_v2.py", y, pptx,
|
||||
"--assets", "assets"],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
print(" ! %s : le rendu ÉCHOUE\n%s" % (y, r.stdout[-400:]))
|
||||
sys.exit(1)
|
||||
out[y] = structure(pptx)
|
||||
Path(pptx).unlink(missing_ok=True)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
got = render_all()
|
||||
if "--update" in sys.argv:
|
||||
MANIFEST.write_text(json.dumps(got, indent=2))
|
||||
print(" + Manifeste écrit : %d goldens, %d slides au total."
|
||||
% (len(got), sum(len(v) for v in got.values())))
|
||||
return
|
||||
if not MANIFEST.exists():
|
||||
print(" ! Pas de manifeste — lancer d'abord : "
|
||||
"python3 check_goldens.py --update")
|
||||
sys.exit(1)
|
||||
want = json.loads(MANIFEST.read_text())
|
||||
ok = True
|
||||
for y, shapes in got.items():
|
||||
ref = want.get(y)
|
||||
if ref is None:
|
||||
print(" ~ %s : nouveau golden (absent du manifeste)" % y)
|
||||
continue
|
||||
if shapes != ref:
|
||||
ok = False
|
||||
print(" ! %s : ÉCART — slides/shapes %s ≠ attendu %s"
|
||||
% (y, shapes, ref))
|
||||
for y in want:
|
||||
if y not in got:
|
||||
ok = False
|
||||
print(" ! %s : golden du manifeste INTROUVABLE" % y)
|
||||
print(" %s" % ("✓ goldens conformes (%d fichiers)" % len(got)
|
||||
if ok else "✗ régression détectée"))
|
||||
sys.exit(0 if ok else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+115
-15
@@ -47,7 +47,11 @@ SYSTEM_PROMPT = (
|
||||
"d'important ; condenser légèrement si nécessaire.\n"
|
||||
"- Texte brut uniquement : aucun balisage Markdown (pas de **, __, #, `, "
|
||||
"ni tirets de liste dans les valeurs).\n"
|
||||
"- Français, ton affirmatif, guillemets typographiques évités.\n"
|
||||
"- LANGUE : conserve strictement la langue du contenu fourni (un plan "
|
||||
"anglais donne des champs 100 % anglais). Ces instructions sont en "
|
||||
"français mais ce n'est JAMAIS la langue du livrable. Ne traduis "
|
||||
"rien.\n"
|
||||
"- Ton affirmatif, guillemets typographiques évités.\n"
|
||||
"- Champs optionnels sans contenu correspondant : null.\n"
|
||||
"- Les nombres des champs numériques (start, end, x, y, numero) sont des "
|
||||
"entiers, pas des chaînes.\n"
|
||||
@@ -58,7 +62,22 @@ SYSTEM_PROMPT = (
|
||||
"ignore-les."
|
||||
)
|
||||
|
||||
# "SLIDE 12 — layout_name" (tiret cadratin, demi-cadratin ou simple)
|
||||
# FORMAT RÉEL du Designer (prompt_the_designer_v3) : le Markdown du
|
||||
# Narrator repris intégralement, avec une ligne "@layout: nom" ajoutée
|
||||
# sous chaque titre de slide, et des "@note:" facultatives.
|
||||
# ### 3. The CEO's 3 priorities
|
||||
# @layout: two_cols_text
|
||||
# @note: ...
|
||||
# <contenu>
|
||||
# On découpe donc sur les lignes @layout, et on déduit la position du
|
||||
# titre qui précède (### 3. …) ou, à défaut, de l'ordre d'apparition.
|
||||
LAYOUT_RE = re.compile(r"^[ \t]*@layout[ \t]*:[ \t]*([a-z][a-z0-9_]*)[ \t]*$",
|
||||
re.MULTILINE)
|
||||
# Titre de slide : "### 12. Titre" ou "### [OPT-EXEC] 5. Titre"
|
||||
TITLE_NUM_RE = re.compile(
|
||||
r"^[ \t]*#{1,6}[ \t]*(?:\[[^\]]+\][ \t]*)?(\d+)[.)][ \t]*",
|
||||
re.MULTILINE)
|
||||
# Compat ascendante : ancien format "SLIDE 12 — layout_name"
|
||||
SLIDE_RE = re.compile(
|
||||
r"^\s*SLIDE\s+(\d+)\s*[—–\-]+\s*([a-z][a-z0-9_]*)\s*$",
|
||||
re.MULTILINE)
|
||||
@@ -72,22 +91,60 @@ MD_PATTERNS = [
|
||||
|
||||
# ── Découpage du plan ────────────────────────────────────────────────────────
|
||||
|
||||
def _position_before(plan_md, idx, fallback):
|
||||
"""Numéro de la dernière ligne de titre '### N.' avant idx."""
|
||||
last = None
|
||||
for m in TITLE_NUM_RE.finditer(plan_md, 0, idx):
|
||||
last = m
|
||||
if last:
|
||||
try:
|
||||
return int(last.group(1))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return fallback
|
||||
|
||||
|
||||
def split_plan(plan_md):
|
||||
"""Découpe le plan Designer en slides.
|
||||
"""Découpe le plan annoté par le Designer en slides.
|
||||
Format principal : lignes '@layout: nom'. Compat : 'SLIDE N — nom'.
|
||||
Retourne (segments, errors) ; segment = {position, layout, content}."""
|
||||
matches = list(SLIDE_RE.finditer(plan_md))
|
||||
segments, errors = [], []
|
||||
if not matches:
|
||||
return [], ["Aucune ligne 'SLIDE N — layout' détectée dans le plan."]
|
||||
valid = set(known_layouts())
|
||||
|
||||
matches = list(LAYOUT_RE.finditer(plan_md))
|
||||
if matches:
|
||||
for i, m in enumerate(matches):
|
||||
start = m.start()
|
||||
end = (matches[i + 1].start() if i + 1 < len(matches)
|
||||
else len(plan_md))
|
||||
layout = m.group(1)
|
||||
pos = _position_before(plan_md, start, i + 1)
|
||||
seg = {"position": pos, "layout": layout,
|
||||
"content": plan_md[start:end].strip()}
|
||||
if layout not in valid:
|
||||
errors.append("Slide %d : layout '%s' sans schéma (connus"
|
||||
" : %s)" % (pos, layout,
|
||||
", ".join(sorted(valid))))
|
||||
continue
|
||||
segments.append(seg)
|
||||
# Positions dupliquées (titre absent) → renumérotation d'ordre
|
||||
seen = [s["position"] for s in segments]
|
||||
if len(set(seen)) != len(seen):
|
||||
for i, s in enumerate(segments, start=1):
|
||||
s["position"] = i
|
||||
return segments, errors
|
||||
|
||||
# Fallback : ancien format explicite
|
||||
matches = list(SLIDE_RE.finditer(plan_md))
|
||||
if not matches:
|
||||
return [], ["Aucune annotation '@layout: nom' (ni 'SLIDE N — "
|
||||
"layout') détectée dans le plan du Designer."]
|
||||
for i, m in enumerate(matches):
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(plan_md)
|
||||
end = (matches[i + 1].start() if i + 1 < len(matches)
|
||||
else len(plan_md))
|
||||
layout = m.group(2)
|
||||
seg = {
|
||||
"position": int(m.group(1)),
|
||||
"layout": layout,
|
||||
"content": plan_md[m.start():end].strip(),
|
||||
}
|
||||
seg = {"position": int(m.group(1)), "layout": layout,
|
||||
"content": plan_md[m.start():end].strip()}
|
||||
if layout not in valid:
|
||||
errors.append("Slide %d : layout '%s' sans schéma (connus : %s)"
|
||||
% (seg["position"], layout,
|
||||
@@ -121,7 +178,7 @@ def _post_with_retry(payload, api_key):
|
||||
% (MAX_RETRY, last_err))
|
||||
|
||||
|
||||
def encode_slide(content, layout, api_key, model=None):
|
||||
def encode_slide(content, layout, api_key, model=None, lang=None):
|
||||
"""Encode UNE slide. Retourne (dict_contenu, usage_dict).
|
||||
Lève RuntimeError/ValueError en cas d'échec (isolé par l'appelant)."""
|
||||
schema = get_schema(layout)
|
||||
@@ -131,7 +188,9 @@ def encode_slide(content, layout, api_key, model=None):
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content":
|
||||
"Layout : %s\n\nContenu de la slide :\n%s"
|
||||
("LANGUE DU DECK : %s — tous les champs texte restent "
|
||||
"dans cette langue.\n" % lang if lang else "")
|
||||
+ "Layout : %s\n\nContenu de la slide :\n%s"
|
||||
% (layout, content)},
|
||||
],
|
||||
"response_format": {
|
||||
@@ -181,6 +240,41 @@ def _accumulate(total, usage):
|
||||
return total
|
||||
|
||||
|
||||
def detect_lang(text):
|
||||
"""Heuristique stopwords : 'anglais' / 'français' / None (mixte)."""
|
||||
t = " %s " % re.sub(r"\s+", " ", text.lower())
|
||||
en = sum(t.count(w) for w in (" the ", " and ", " of ", " to ",
|
||||
" is ", " for ", " with "))
|
||||
fr = sum(t.count(w) for w in (" le ", " la ", " les ", " des ",
|
||||
" est ", " pour ", " avec ", " une "))
|
||||
if en > fr * 1.5:
|
||||
return "anglais"
|
||||
if fr > en * 1.5:
|
||||
return "français"
|
||||
return None
|
||||
|
||||
|
||||
TITLE_PREFIX_RE = re.compile(r"^\s*(?:\[[^\]]+\]\s*)?\d+[.)]\s*")
|
||||
|
||||
|
||||
def strip_title_number(content):
|
||||
"""Retire la numérotation Markdown résiduelle du titre ('13. X' → 'X')."""
|
||||
t = content.get("titre")
|
||||
if isinstance(t, str):
|
||||
content["titre"] = TITLE_PREFIX_RE.sub("", t)
|
||||
return content
|
||||
|
||||
|
||||
def trim_trailing_meta(segments):
|
||||
"""Le DERNIER segment court jusqu'à la fin du texte : il peut embarquer
|
||||
les commentaires de conclusion du Designer (après un '---'). On coupe."""
|
||||
if segments:
|
||||
parts = segments[-1]["content"].split("\n---")
|
||||
if parts[0].count("\n") >= 1:
|
||||
segments[-1]["content"] = parts[0].rstrip()
|
||||
return segments
|
||||
|
||||
|
||||
# ── Orchestration ────────────────────────────────────────────────────────────
|
||||
|
||||
def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
@@ -190,6 +284,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
progress : callable(str) pour l'affichage (info du facilitator).
|
||||
Retourne (data, usage, errors) :
|
||||
data = {"titre_presentation": str|None, "slides": [...]}
|
||||
La langue du plan est détectée et imposée à chaque slide (anti-dérive).
|
||||
usage = tokens cumulés {"prompt_tokens","completion_tokens","total_tokens"}
|
||||
errors = liste de messages (slides en échec — absentes de data).
|
||||
"""
|
||||
@@ -198,6 +293,10 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
progress(msg)
|
||||
|
||||
segments, errors = split_plan(plan_md)
|
||||
segments = trim_trailing_meta(segments)
|
||||
lang = detect_lang(plan_md)
|
||||
if lang:
|
||||
say("Langue du deck : %s (imposée à chaque slide)" % lang)
|
||||
if only_positions is not None:
|
||||
wanted = set(int(p) for p in only_positions)
|
||||
segments = [s for s in segments if s["position"] in wanted]
|
||||
@@ -207,7 +306,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
say("Slide %d (%s)..." % (seg["position"], seg["layout"]))
|
||||
try:
|
||||
content, u = encode_slide(seg["content"], seg["layout"],
|
||||
api_key, model)
|
||||
api_key, model, lang=lang)
|
||||
except (RuntimeError, ValueError, KeyError,
|
||||
json.JSONDecodeError) as e:
|
||||
errors.append("Slide %d (%s) : %s"
|
||||
@@ -215,6 +314,7 @@ def encode_plan(plan_md, api_key, model=None, only_positions=None,
|
||||
continue
|
||||
_accumulate(usage, u)
|
||||
content = strip_md(drop_nulls(content))
|
||||
content = strip_title_number(content)
|
||||
if seg["layout"] == "cover_split" and titre_presentation is None:
|
||||
titre_presentation = content.get("titre")
|
||||
slide = {"position": seg["position"], "layout": seg["layout"]}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
patch_prompt_designer_meta.py — Le Designer ne conclut plus.
|
||||
Bug : ses commentaires de synthèse (« Points clés de la proposition… »)
|
||||
après la dernière slide fuyaient dans le deck (slide 27 du deck Sisley).
|
||||
Défense en profondeur : encoder_schema v3 tronque déjà après '---' ;
|
||||
cette règle prompt tarit la source.
|
||||
Usage : python3 patch_prompt_designer_meta.py
|
||||
puis prompt_injection_v2.py + recoller le Designer dans Mistral Studio.
|
||||
"""
|
||||
import shutil, sys
|
||||
from pathlib import Path
|
||||
|
||||
BLOC = """- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
|
||||
slide. AUCUN commentaire, bilan, synthèse ou justification après —
|
||||
tout texte postérieur serait transcrit dans le deck comme du contenu.
|
||||
"""
|
||||
p = Path("prompt_the_designer_v3.md")
|
||||
if not p.exists():
|
||||
print(" ! prompt_the_designer_v3.md introuvable"); sys.exit(1)
|
||||
c = p.read_text(encoding="utf-8")
|
||||
if "se termine à la DERNIÈRE ligne" in c:
|
||||
print(" = déjà à jour"); sys.exit(0)
|
||||
anchor = "- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`."
|
||||
if c.count(anchor) != 1:
|
||||
print(" ! ancre introuvable/non unique"); sys.exit(1)
|
||||
shutil.copy2(p, str(p) + ".bak-meta")
|
||||
p.write_text(c.replace(anchor, anchor + "\n" + BLOC), encoding="utf-8")
|
||||
print(" + règle anti-conclusion insérée — propager avec prompt_injection_v2.py")
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
patch_render_engine_c5fin.py — Finitions C5 (2 micro-fixes visuels)
|
||||
===================================================================
|
||||
F1. bar_chart horizontal : PowerPoint affiche les catégories de BAS
|
||||
en HAUT en BAR_CLUSTERED — la première catégorie du YAML se
|
||||
retrouvait en bas. On inverse catégories ET valeurs quand
|
||||
horizontal, pour un ordre de lecture naturel (haut → bas).
|
||||
F2. waterfall : les libellés de catégories longs (Décommissionnements…)
|
||||
wrappaient en orphelin. Réduction automatique à 9 pt au-delà de
|
||||
14 caractères.
|
||||
|
||||
Usage (dossier du pipeline) : python3 patches/patch_render_engine_c5fin.py
|
||||
Vérifie les ancres, écrit .bak-c5fin, compile, idempotent.
|
||||
"""
|
||||
import py_compile
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TARGET = Path("render_engine_v2.py")
|
||||
|
||||
P1_OLD = """ cd = CategoryChartData()
|
||||
cd.categories = cats
|
||||
for s in series:
|
||||
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
|
||||
vals += [0.0] * (len(cats) - len(vals))
|
||||
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
|
||||
x, y, w, h = self._chart_zone(d)
|
||||
horiz = bool(d.get("horizontal"))
|
||||
"""
|
||||
P1_NEW = """ horiz = bool(d.get("horizontal"))
|
||||
if horiz:
|
||||
# BAR_CLUSTERED trace de bas en haut : on inverse pour un
|
||||
# ordre de lecture naturel (1re catégorie du YAML en haut).
|
||||
cats = cats[::-1]
|
||||
cd = CategoryChartData()
|
||||
cd.categories = cats
|
||||
for s in series:
|
||||
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
|
||||
vals += [0.0] * (len(cats) - len(vals))
|
||||
if horiz:
|
||||
vals = vals[::-1]
|
||||
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
|
||||
x, y, w, h = self._chart_zone(d)
|
||||
"""
|
||||
|
||||
P2_OLD = """ def cat_label(i, label):
|
||||
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
|
||||
slot - 0.1, 1.05, label, size=10,
|
||||
color=self.C["slate"], align=PP_ALIGN.CENTER)
|
||||
"""
|
||||
P2_NEW = """ def cat_label(i, label):
|
||||
sz = 9 if len(str(label)) > 14 else 10
|
||||
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
|
||||
slot - 0.1, 1.05, label, size=sz,
|
||||
color=self.C["slate"], align=PP_ALIGN.CENTER)
|
||||
"""
|
||||
|
||||
|
||||
def fail(m):
|
||||
print(" ! %s" % m)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if not TARGET.exists():
|
||||
fail("%s introuvable." % TARGET)
|
||||
c = TARGET.read_text(encoding="utf-8")
|
||||
if "cats = cats[::-1]" in c:
|
||||
fail("Déjà patché — rien à faire.")
|
||||
for i, old in enumerate((P1_OLD, P2_OLD), 1):
|
||||
if c.count(old) != 1:
|
||||
fail("Ancre F%d introuvable ou non unique." % i)
|
||||
shutil.copy2(TARGET, str(TARGET) + ".bak-c5fin")
|
||||
c = c.replace(P1_OLD, P1_NEW).replace(P2_OLD, P2_NEW)
|
||||
TARGET.write_text(c, encoding="utf-8")
|
||||
try:
|
||||
py_compile.compile(str(TARGET), doraise=True)
|
||||
except py_compile.PyCompileError as e:
|
||||
shutil.copy2(str(TARGET) + ".bak-c5fin", TARGET)
|
||||
fail("Compilation : %s" % e)
|
||||
print(" + F1 (ordre bar horizontal) + F2 (labels waterfall) OK.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -338,11 +338,27 @@ annoté complet mis à jour, pas seulement la partie modifiée.
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue.
|
||||
- Tu reprends le Markdown INTÉGRALEMENT — aucune perte de contenu.
|
||||
- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`.
|
||||
- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
|
||||
slide. AUCUN commentaire, bilan, synthèse ou justification après —
|
||||
tout texte postérieur serait transcrit dans le deck comme du contenu.
|
||||
|
||||
- Tu ne produis ni YAML, ni plan séparé, ni JSON.
|
||||
- `cover_split` en premier, `end_slide` en dernier, toujours.
|
||||
|
||||
|
||||
@@ -101,11 +101,27 @@ annoté complet mis à jour, pas seulement la partie modifiée.
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue.
|
||||
- Tu reprends le Markdown INTÉGRALEMENT — aucune perte de contenu.
|
||||
- Tu ajoutes UNIQUEMENT les lignes `@layout:` et `@note:`.
|
||||
- Ta réponse se termine à la DERNIÈRE ligne de contenu de la dernière
|
||||
slide. AUCUN commentaire, bilan, synthèse ou justification après —
|
||||
tout texte postérieur serait transcrit dans le deck comme du contenu.
|
||||
|
||||
- Tu ne produis ni YAML, ni plan séparé, ni JSON.
|
||||
- `cover_split` en premier, `end_slide` en dernier, toujours.
|
||||
|
||||
|
||||
@@ -451,13 +451,6 @@ bullets:
|
||||
|
||||
---
|
||||
|
||||
### Notes du présentateur
|
||||
|
||||
Si une slide du plan contient une ligne « Notes : ... », transcris son
|
||||
contenu dans un champ `notes` de la slide YAML (chaîne simple). Les
|
||||
lignes de justification du Designer (commençant par →) ne sont NI du
|
||||
contenu NI des notes : ignore-les.
|
||||
|
||||
## GESTION DE LA LONGUEUR
|
||||
|
||||
Si la présentation dépasse 8 slides, tu travailles en blocs :
|
||||
@@ -469,6 +462,18 @@ Le YAML de chaque bloc doit être **syntaxiquement valide indépendamment** —
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue — aucun autre nom n'existe
|
||||
|
||||
@@ -81,6 +81,18 @@ Le YAML de chaque bloc doit être **syntaxiquement valide indépendamment** —
|
||||
|
||||
---
|
||||
|
||||
## LANGUE DU DECK (RÈGLE ABSOLUE)
|
||||
|
||||
Le contenu que tu reçois peut être dans n'importe quelle langue
|
||||
(français, anglais, autre). **Tu conserves TOUJOURS la langue du plan
|
||||
reçu**, mot pour mot, y compris pour les titres, les bullets, les
|
||||
labels, les légendes et les notes.
|
||||
|
||||
Ces instructions sont en français — c'est la langue de TON pilotage,
|
||||
jamais celle du livrable. Un plan en anglais produit un deck 100 %
|
||||
anglais. Ne traduis JAMAIS, ne mélange JAMAIS deux langues dans un
|
||||
même deck.
|
||||
|
||||
## RÈGLES ABSOLUES
|
||||
|
||||
- Tu utilises UNIQUEMENT les 21 layouts du catalogue — aucun autre nom n'existe
|
||||
|
||||
+9
-2
@@ -1711,14 +1711,20 @@ class RenderEngineV2:
|
||||
if len(series) > 3:
|
||||
print(f" ~ bar_chart : {len(series)} séries → 3 (borne)")
|
||||
series = series[:3]
|
||||
horiz = bool(d.get("horizontal"))
|
||||
if horiz:
|
||||
# BAR_CLUSTERED trace de bas en haut : on inverse pour un
|
||||
# ordre de lecture naturel (1re catégorie du YAML en haut).
|
||||
cats = cats[::-1]
|
||||
cd = CategoryChartData()
|
||||
cd.categories = cats
|
||||
for s in series:
|
||||
vals = [self._num(v) for v in (s.get("values") or [])][:len(cats)]
|
||||
vals += [0.0] * (len(cats) - len(vals))
|
||||
if horiz:
|
||||
vals = vals[::-1]
|
||||
cd.add_series(pick(s, "label", default="Série"), tuple(vals))
|
||||
x, y, w, h = self._chart_zone(d)
|
||||
horiz = bool(d.get("horizontal"))
|
||||
ctype = (XL_CHART_TYPE.BAR_CLUSTERED if horiz
|
||||
else XL_CHART_TYPE.COLUMN_CLUSTERED)
|
||||
ch = self._chart_base(slide, x, y, w, h, ctype, cd)
|
||||
@@ -1929,8 +1935,9 @@ class RenderEngineV2:
|
||||
align=PP_ALIGN.CENTER, fit=False)
|
||||
|
||||
def cat_label(i, label):
|
||||
sz = 9 if len(str(label)) > 14 else 10
|
||||
self._text(slide, zx + i * slot + 0.05, chart_b + 0.15,
|
||||
slot - 0.1, 1.05, label, size=10,
|
||||
slot - 0.1, 1.05, label, size=sz,
|
||||
color=self.C["slate"], align=PP_ALIGN.CENTER)
|
||||
|
||||
def connector(x1, x2, level):
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# resilience_boot.sh — Résilience C2 · à planifier en tâche DSM
|
||||
# « Au démarrage », utilisateur ROOT (recrée la règle sudoers).
|
||||
# Rejouable sans risque (idempotent). Journalise dans resilience.log.
|
||||
|
||||
BASE=/volume1/homes/Master/App/Sliding/python-pptx
|
||||
LOG="$BASE/resilience.log"
|
||||
TS=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 1. Règle sudoers de la preview (sautée par les MAJ DSM majeures)
|
||||
SUDOERS=/etc/sudoers.d/sliding-preview
|
||||
if [ ! -f "$SUDOERS" ]; then
|
||||
printf 'Master ALL=(root) NOPASSWD: %s/preview.sh\n' "$BASE" > "$SUDOERS"
|
||||
chmod 440 "$SUDOERS"
|
||||
echo "$TS [sudoers] regle sliding-preview RECREEE" >> "$LOG"
|
||||
fi
|
||||
|
||||
# 2. Purge des previews de plus de 30 jours (le disque du DS218 est petit)
|
||||
N=$(find "$BASE/projets" -type d -name "*_previews" -mtime +30 2>/dev/null | wc -l)
|
||||
if [ "$N" -gt 0 ]; then
|
||||
find "$BASE/projets" -type d -name "*_previews" -mtime +30 -exec rm -rf {} + 2>/dev/null
|
||||
echo "$TS [purge] $N dossier(s) de previews > 30j supprimes" >> "$LOG"
|
||||
fi
|
||||
|
||||
# 3. L'image docker de la preview a-t-elle survécu ?
|
||||
DOCKER=/volume1/@appstore/ContainerManager/usr/bin/docker
|
||||
if [ -x "$DOCKER" ]; then
|
||||
if ! "$DOCKER" image inspect sliding-preview >/dev/null 2>&1; then
|
||||
echo "$TS [docker] IMAGE sliding-preview ABSENTE — rebuild requis (voir fiche systeme Trilium)" >> "$LOG"
|
||||
fi
|
||||
else
|
||||
echo "$TS [docker] demon/binaire docker indisponible" >> "$LOG"
|
||||
fi
|
||||
@@ -60,7 +60,7 @@
|
||||
{% if previews %}
|
||||
<div class="gallery">
|
||||
{% for n in previews %}
|
||||
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}" loading="lazy">
|
||||
<figure><img src="/p/{{ p.slug }}/previews/{{ n }}?v={{ cache_v }}" loading="lazy">
|
||||
<figcaption>{{ n }}</figcaption></figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
+68
-31
@@ -191,15 +191,16 @@ async def project_page(request):
|
||||
entry = NARRATORS.get(slug)
|
||||
chat = entry["chat"] if entry else []
|
||||
# previews existantes ?
|
||||
previews = []
|
||||
pptx = info.get("dernier_pptx")
|
||||
previews, cache_v = [], 0
|
||||
pptx = _pptx_path(slug)
|
||||
if pptx:
|
||||
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
|
||||
prev_dir = pptx.parent / f"{pptx.stem}_previews"
|
||||
cache_v = int(pptx.stat().st_mtime)
|
||||
if prev_dir.is_dir():
|
||||
previews = sorted(p.name for p in
|
||||
prev_dir.glob("slide-*.png"))
|
||||
return render("project.html", p=info, chat_html=_chat_html(chat),
|
||||
previews=previews)
|
||||
previews=previews, cache_v=cache_v)
|
||||
|
||||
|
||||
# ── routes : chat Narrator ───────────────────────────────────────────────────
|
||||
@@ -285,28 +286,45 @@ async def formalise(request):
|
||||
entry = _narrator(slug)
|
||||
if not entry["started"]:
|
||||
return HTMLResponse('<p class="err">Aucune conversation '
|
||||
'Narrator — travaille le plan d\'abord.'
|
||||
'</p>')
|
||||
'Narrator — travaille le plan d\'abord, '
|
||||
'puis sauvegarde-le.</p>')
|
||||
proj = fac.Project(slug)
|
||||
try:
|
||||
md = entry["agent"].send(
|
||||
"formalise — produis la formalisation complète slide par "
|
||||
"slide (SLIDE N — titre, contenus, Notes éventuelles), "
|
||||
"sans commentaire autour.")
|
||||
entry["chat"].append(("system", "Formalisation demandée…"))
|
||||
# Message IDENTIQUE à /formalise du facilitator (le Narrator
|
||||
# produit un Markdown structuré #/##/### — PAS des "SLIDE N —
|
||||
# layout" : ce sont le Designer puis l'Encoder qui les créent).
|
||||
md = entry["agent"].feedback(
|
||||
"Formalise maintenant le plan compact validé en Markdown "
|
||||
"structuré complet pour le Designer (format #, ##, ###), "
|
||||
"en développant le contenu de chaque slide. Inclus les "
|
||||
"slides tiroir en les marquant [OPT-XXX].")
|
||||
entry["chat"].append(("system", "Formalisation demandée."))
|
||||
entry["chat"].append(("narrator", md))
|
||||
# Garde : le Narrator a-t-il vraiment produit un plan ?
|
||||
if md.count("#") < 3 or len(md) < 300:
|
||||
return HTMLResponse(
|
||||
'<p class="err">Le Narrator n\'a pas renvoyé un plan '
|
||||
'formalisé exploitable (réponse trop courte ou non '
|
||||
'structurée). Vérifie le plan dans le chat, ajuste, '
|
||||
'puis relance.</p>')
|
||||
proj.mark_formalised(md)
|
||||
r = api.formalise(slug, md)
|
||||
except Exception as e:
|
||||
return HTMLResponse(f'<p class="err">Échec : '
|
||||
f'{html.escape(str(e))}</p>')
|
||||
if r.get("error"):
|
||||
return HTMLResponse(f'<p class="err">'
|
||||
f'{html.escape(str(r))}</p>')
|
||||
return HTMLResponse(
|
||||
f'<p class="err">{html.escape(str(r.get("error")))}'
|
||||
f'<br><span class="hint">Le Designer/Encoder n\'a pas su '
|
||||
f'transcrire ce plan. Regarde la formalisation dans le '
|
||||
f'chat : elle doit lister les slides une à une.</span>'
|
||||
f'</p>')
|
||||
return HTMLResponse(
|
||||
f'<p class="ok">Deck généré : {r["slides"]} slides — '
|
||||
f'{html.escape(r["validation"])}<br>'
|
||||
f'<a href="/p/{slug}/download">Télécharger le PPTX</a> · '
|
||||
f'<button hx-post="/p/{slug}/preview" '
|
||||
f'hx-target="#gallery" hx-indicator="#busy">Générer les '
|
||||
f'aperçus</button></p>')
|
||||
f'<a class="btn" href="/p/{slug}/download">Télécharger le '
|
||||
f'PPTX</a> <span class="hint">puis « Générer les aperçus » '
|
||||
f'ci-dessus pour les voir.</span></p>')
|
||||
|
||||
|
||||
async def preview(request):
|
||||
@@ -325,6 +343,8 @@ async def preview(request):
|
||||
f'</figcaption></figure>'
|
||||
for i, n in enumerate(r["slides"]))
|
||||
return HTMLResponse(
|
||||
f'<p class="ok">{len(r["slides"])} aperçus générés. Coche des '
|
||||
f'slides pour les réviser.</p>'
|
||||
f'<form hx-post="/p/{slug}/revise" hx-target="#pipeline-out" '
|
||||
f'hx-indicator="#busy"><div class="gallery">{cells}</div>'
|
||||
f'<input name="instruction" placeholder="Instruction de '
|
||||
@@ -349,33 +369,45 @@ async def revise(request):
|
||||
f'{html.escape(str(r))}</p>')
|
||||
return HTMLResponse(
|
||||
f'<p class="ok">Slides {r["slides_revisees"]} révisées, deck '
|
||||
f'complet régénéré.<br><a href="/p/{slug}/download">'
|
||||
f'Télécharger</a> · <button hx-post="/p/{slug}/preview" '
|
||||
f'hx-target="#gallery" hx-indicator="#busy">Re-générer les '
|
||||
f'aperçus</button></p>')
|
||||
f'complet régénéré.<br><a class="btn" href="/p/{slug}/'
|
||||
f'download">Télécharger</a> <span class="hint">puis '
|
||||
f'« Générer les aperçus » pour revoir le deck.</span></p>')
|
||||
|
||||
|
||||
# ── routes : fichiers ────────────────────────────────────────────────────────
|
||||
|
||||
def _pptx_path(slug):
|
||||
"""Chemin ABSOLU du dernier PPTX. project_state.json stocke un
|
||||
chemin relatif à la racine du pipeline — la webapp peut avoir un
|
||||
cwd différent, on résout donc depuis BASE."""
|
||||
proj = fac.Project(slug)
|
||||
raw = (proj.load_state().get("dernier_pptx") or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
p = Path(raw)
|
||||
if not p.is_absolute():
|
||||
p = BASE / p
|
||||
return p if p.exists() else None
|
||||
|
||||
|
||||
async def preview_png(request):
|
||||
guard = require_login(request)
|
||||
if guard:
|
||||
return guard
|
||||
slug = request.path_params["slug"]
|
||||
name = request.path_params["name"]
|
||||
proj = fac.Project(slug)
|
||||
state = proj.load_state()
|
||||
pptx = state.get("dernier_pptx")
|
||||
pptx = _pptx_path(slug)
|
||||
if not pptx:
|
||||
return Response(status_code=404)
|
||||
prev_dir = Path(pptx).parent / f"{Path(pptx).stem}_previews"
|
||||
prev_dir = pptx.parent / f"{pptx.stem}_previews"
|
||||
try:
|
||||
png = _safe_child(prev_dir, name)
|
||||
except ValueError:
|
||||
return Response(status_code=403)
|
||||
if not png.exists():
|
||||
return Response(status_code=404)
|
||||
return FileResponse(str(png), media_type="image/png")
|
||||
return FileResponse(str(png), media_type="image/png",
|
||||
headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
async def download(request):
|
||||
@@ -383,11 +415,16 @@ async def download(request):
|
||||
if guard:
|
||||
return guard
|
||||
slug = request.path_params["slug"]
|
||||
proj = fac.Project(slug)
|
||||
pptx = proj.load_state().get("dernier_pptx")
|
||||
if not pptx or not Path(pptx).exists():
|
||||
return Response(status_code=404)
|
||||
return FileResponse(pptx, filename=Path(pptx).name)
|
||||
pptx = _pptx_path(slug)
|
||||
if not pptx:
|
||||
return HTMLResponse(
|
||||
'<p class="err">Aucun PPTX disponible pour ce projet — '
|
||||
'lance « Formaliser le deck » d\'abord.</p>',
|
||||
status_code=404)
|
||||
return FileResponse(
|
||||
str(pptx), filename=pptx.name,
|
||||
media_type="application/vnd.openxmlformats-officedocument."
|
||||
"presentationml.presentation")
|
||||
|
||||
|
||||
async def health(request):
|
||||
|
||||
Reference in New Issue
Block a user