From a0a059887ac03c1e60ec1a11b5f1ba13407b7ecc Mon Sep 17 00:00:00 2001 From: root Date: Sat, 25 Apr 2026 22:44:40 -0300 Subject: [PATCH] Deck generator: honor output.render_standard_sections, fix principles overlap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs flagged on the MySQL HeatWave HA example (2026-04-25 round 5): 1. ``render_standard_sections: false`` under the ``output:`` block was silently ignored — the generator only checked the top-level key. Result: every spec that put the flag where the docs suggest got the full standard deck PLUS the custom slide, producing a duplicated "Architecture Overview" + dedicated diagram slide. Now the generator honors both locations. 2. Architecture Principles slide allocated only 0.4" per bullet — exactly one 11pt line. Wrapping principle summaries (e.g. D-03 Use-Case Fit's 125-char text) overflowed into the next bullet's slot, visually overlapping D-02. Diego: "D-03 esta superpuesto a D-02, lo mismo pasa en otras columnas". Bumped per-item height to 0.95" — fits 2-3 wrapped lines with breathing room. 3. PPTX rasterizer (oci_pptx_render.py) gained word-wrap so the local PNG preview matches what real PowerPoint draws. Without it, long text rendered on a single line off the column and the principles fix looked broken in preview when it was actually fine in PowerPoint. Persistent: all three fixes apply to every deck the skill generates. Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/oci_deck_gen.py | 27 +++++++++++++++++++++++---- tools/oci_pptx_render.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/tools/oci_deck_gen.py b/tools/oci_deck_gen.py index 7d47809..8b773ce 100644 --- a/tools/oci_deck_gen.py +++ b/tools/oci_deck_gen.py @@ -1516,6 +1516,17 @@ class OCIDeckGenerator(OraclePresBase): col_x = self.MARGIN col_width = Inches(4) + # Per-item height was Inches(0.4) — fits exactly one 11pt line. + # ECAL principle summaries routinely wrap to 2-3 lines (e.g. + # "D-03 Use-Case Fit — Map workloads into applicable use-cases. + # Architectural simplicity and operational success come from + # re-use") which then overflowed into the next bullet's slot. + # Diego flagged 2026-04-25: "D-03 esta superpuesto a D-02". + # Bumped to 0.95" per item — fits 2-3 wrapped lines with + # breathing room. With 3 items per category there's ample + # vertical room (3 * 0.95" = 2.85" out of ~5.5" working area). + ITEM_H = Inches(0.95) + for category in ["design", "deployment", "service"]: items = principles.get(category, []) if not items: @@ -1528,7 +1539,7 @@ class OCIDeckGenerator(OraclePresBase): color=Colors.TEAL, ) - item_y = y + Inches(0.4) + item_y = y + Inches(0.45) for item in items: pid = item.get("id", "") name = item.get("name", "") @@ -1542,10 +1553,10 @@ class OCIDeckGenerator(OraclePresBase): label += f" — {summary}" self._add_textbox( slide, col_x + Inches(0.1), item_y, - col_width - Inches(0.1), Inches(0.4), + col_width - Inches(0.1), ITEM_H, text=f"• {label}", font_size=11, ) - item_y += Inches(0.4) + item_y += ITEM_H col_x += Inches(4.2) @@ -2297,7 +2308,15 @@ class OCIDeckGenerator(OraclePresBase): title=pick(s, "title", default="Engagement Summary"), ) - render_standard_sections = spec.get("render_standard_sections", True) + # The flag is documented in two places (top-level OR under + # `output:`) — earlier specs put it under `output:` to live + # alongside the format selector. Honor both locations so a + # spec written either way works. + output_block = spec.get("output") or {} + render_standard_sections = spec.get( + "render_standard_sections", + output_block.get("render_standard_sections", True), + ) def render_custom_slide(item: dict): slide_type = item.get("type") diff --git a/tools/oci_pptx_render.py b/tools/oci_pptx_render.py index ca8bad1..0428dbc 100644 --- a/tools/oci_pptx_render.py +++ b/tools/oci_pptx_render.py @@ -539,11 +539,38 @@ class PPTXSlideRenderer: text_right = rect[2] - inset_r text_bottom = rect[3] - inset_b box_w = max(text_right - text_left, 1) + # Word-wrap to box_w. PowerPoint does this natively when + # text_frame.word_wrap=true (the default for added textboxes); + # without it the rasterizer preview shows long bullets running + # off the column and visually overlapping siblings — Diego + # flagged 2026-04-25 on the Architecture Principles slide. + def _wrap(text: str, font: ImageFont.ImageFont, max_w: int) -> str: + wrapped_lines: list[str] = [] + for raw_line in text.split("\n"): + if not raw_line: + wrapped_lines.append("") + continue + words = raw_line.split(" ") + current = "" + for word in words: + candidate = (current + " " + word).strip() + cand_w = self.draw.textbbox((0, 0), candidate, font=font)[2] + if cand_w <= max_w or not current: + current = candidate + else: + wrapped_lines.append(current) + current = word + if current: + wrapped_lines.append(current) + return "\n".join(wrapped_lines) if wrapped_lines else text + layouts = [] total_h = 0 for paragraph in paragraphs: font = _font(paragraph["size_px"], paragraph["bold"]) - bbox = self.draw.multiline_textbbox((0, 0), paragraph["text"], font=font, spacing=2, align="left") + wrapped_text = _wrap(paragraph["text"], font, box_w) + paragraph["text"] = wrapped_text + bbox = self.draw.multiline_textbbox((0, 0), wrapped_text, font=font, spacing=2, align="left") width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] layouts.append((paragraph, font, width, height))