diff --git a/render_engine_v2.py b/render_engine_v2.py index f8467c4..7c09e20 100644 --- a/render_engine_v2.py +++ b/render_engine_v2.py @@ -38,8 +38,14 @@ def hex_to_rgb(h: str) -> RGBColor: return RGBColor(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) -def pick(d: dict, *keys, default=""): - """Accès tolérant : retourne la première clé non vide trouvée.""" +def pick(d, *keys, default=""): + """Accès tolérant : première clé non vide trouvée. + Si d est une chaîne (l'agent a produit 'Texte' au lieu de {label:'Texte'}), + on la retourne directement — évite les crashs 'str has no attribute get'.""" + if isinstance(d, str): + return d + if not isinstance(d, dict): + return default for k in keys: v = d.get(k) if v not in (None, ""): @@ -47,6 +53,16 @@ def pick(d: dict, *keys, default=""): return default +def as_label(item, *keys, default=""): + """Normalise un élément de liste en texte : 'Texte' ou {label:'Texte'}.""" + if isinstance(item, str): + return item + if isinstance(item, dict): + return pick(item, *(keys or ("label", "texte", "titre", "title")), + default=default) + return default + + def estimate_text_height(text: str, size_pt: int, width_cm: float) -> float: """Hauteur estimée d'un texte (cm) pour une largeur donnée.""" if not text: @@ -475,6 +491,8 @@ class RenderEngineV2: y = self._cy(tot) bd = self.components["badge"]["sizes"]["l"] for i, st in enumerate(steps): + if isinstance(st, str): + st = {"titre": st} self._card(slide, self.MX, y, self.SLIDE_W - 2 * self.MX, rh) self._badge(slide, self.MX + 1.02 + bd / 2, y + rh / 2, bd, st.get("numero", i + 1), font_size=28) @@ -665,7 +683,8 @@ class RenderEngineV2: HDR_H = 0.80 WS_H = 0.68 TASK_H = 0.78 - n_tasks = sum(len(ws.get("tasks", [])) for ws in workstreams) + n_tasks = sum(len(ws.get("tasks", [])) for ws in workstreams + if isinstance(ws, dict)) total_h = HDR_H + len(workstreams)*WS_H + n_tasks*TASK_H y0 = self._cy(total_h) gx = self.MX + LBL_W + GAP_COL # grid x origin @@ -687,6 +706,8 @@ class RenderEngineV2: cur_y = y0 + HDR_H for ws_idx, ws in enumerate(workstreams): + if not isinstance(ws, dict): + continue ws_col = self.cycle[ws_idx % len(self.cycle)] # Workstream row self._rect(slide, self.MX, cur_y, self.SLIDE_W - 2*self.MX, WS_H, "#EEECEA") @@ -696,6 +717,8 @@ class RenderEngineV2: color=self.C["navy"], anchor=MSO_ANCHOR.MIDDLE) cur_y += WS_H for task in ws.get("tasks", []): + if not isinstance(task, dict): + continue start = task.get("start", 0) end = task.get("end", start + 1) bg = self.C["white"] @@ -837,6 +860,8 @@ class RenderEngineV2: # Rows for i, task in enumerate(tasks): + if not isinstance(task, dict): + continue ry = y + HDR_H + i * ROW_H bg = self.C["card"] if i % 2 == 0 else self.C["white"] self._rect(slide, self.MX, ry, self.SLIDE_W - 2*self.MX, ROW_H, bg) @@ -907,12 +932,25 @@ class RenderEngineV2: # ── org_chart ───────────────────────────────────────────────────────────── def _render_org_chart(self, slide, d): self._title(slide, pick(d, "titre", "title")) + + def _node(n): + """Normalise un nœud : 'Texte' → {label:'Texte'}, dict inchangé.""" + if isinstance(n, str): + return {"label": n, "children": []} + if isinstance(n, dict): + return {"label": pick(n, "label", "titre", "title", "nom"), + "children": n.get("children", []) or []} + return {"label": str(n), "children": []} + root = d.get("root", {}) + if isinstance(root, str): + root = {"label": root, "children": []} if not root: return + root = _node(root) BOX_W, BOX_H = 3.81, 1.0 GAP_V, GAP_H = 1.27, 0.64 - children = root.get("children", []) + children = [_node(c) for c in root.get("children", [])] n_children = len(children) # grandchildren count per child (max) max_gd = max((len(c.get("children", [])) for c in children), default=0) @@ -971,7 +1009,7 @@ class RenderEngineV2: ln3.line.width = Pt(1.0) # Grandchildren — sizing dynamique pour tenir dans la colonne - grandchildren = child.get("children", []) + grandchildren = [_node(g) for g in child.get("children", [])] if grandchildren: gd_y = child_y + BOX_H + GAP_V n_gd = len(grandchildren) @@ -1077,6 +1115,8 @@ class RenderEngineV2: # Items as numbered circles for i, item in enumerate(items): + if not isinstance(item, dict): + continue ix = item.get("x", 50) # 0-100 iy = item.get("y", 50) # 0-100 px = mx0 + (ix / 100) * MAT_W @@ -1099,7 +1139,7 @@ class RenderEngineV2: size=9, bold=True, color=self.C["white"], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) self._text(slide, lx + 0.6, leg_y - 0.35, 2.8, 0.6, - item.get("label", ""), size=10, color=self.C["body"]) + as_label(item), size=10, color=self.C["body"])