"""Generate a branded .pptx from a structured outline. The LLM only produces a PPTOutline JSON (title, subtitle, slides[]); rendering is deterministic via python-pptx so the deck is consistent and cheap. """ from __future__ import annotations import os from typing import Dict, Any from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN from .schemas import FactPack, Brief from .llm import LLMClient OUTLINE_SYSTEM = ( "You create a concise slide outline for a product/marketing update deck. " "Use ONLY the provided facts. Respond ONLY as JSON: {\"title\": str, " "\"subtitle\": str, \"slides\": [{\"title\": str, \"bullets\": [str]}]}. " "Aim for 4-6 slides, 2-4 bullets each." ) BRAND_COLOR = RGBColor(0x2E, 0x5B, 0xFF) DARK = RGBColor(0x1A, 0x1A, 0x2E) GRAY = RGBColor(0x55, 0x55, 0x66) def build_outline(fact_pack: FactPack, brief: Brief, llm: LLMClient) -> Dict[str, Any]: user = ( f"FACTS: {fact_pack.approved_facts}\nPROOF: {fact_pack.approved_proof}\n" f"UPDATE: {fact_pack.user_update}\nOBJECTIVE: {brief.objective}" ) return llm.generate_json("ppt_outline", OUTLINE_SYSTEM, user, context={"user_update": fact_pack.user_update}) def render_pptx(outline: Dict[str, Any], out_path: str) -> str: prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) blank = prs.slide_layouts[6] # --- Title slide --- slide = prs.slides.add_slide(blank) _band(slide, prs) tb = slide.shapes.add_textbox(Inches(0.8), Inches(2.6), Inches(11.7), Inches(2)) tf = tb.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = outline.get("title", "Product Update") p.font.size = Pt(46) p.font.bold = True p.font.color.rgb = DARK sub = tf.add_paragraph() sub.text = outline.get("subtitle", "") sub.font.size = Pt(22) sub.font.color.rgb = GRAY # --- Content slides --- for s in outline.get("slides", []): slide = prs.slides.add_slide(blank) _band(slide, prs, thin=True) head = slide.shapes.add_textbox(Inches(0.8), Inches(0.6), Inches(11.7), Inches(1)) hp = head.text_frame.paragraphs[0] hp.text = s.get("title", "") hp.font.size = Pt(32) hp.font.bold = True hp.font.color.rgb = BRAND_COLOR body = slide.shapes.add_textbox(Inches(1.0), Inches(1.8), Inches(11.3), Inches(5)) bf = body.text_frame bf.word_wrap = True for i, bullet in enumerate(s.get("bullets", [])): para = bf.paragraphs[0] if i == 0 else bf.add_paragraph() para.text = "• " + str(bullet) para.font.size = Pt(20) para.font.color.rgb = DARK para.space_after = Pt(12) os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True) prs.save(out_path) return out_path def _band(slide, prs, thin: bool = False): """Add a brand-colored accent band at the top of a slide.""" from pptx.enum.shapes import MSO_SHAPE height = Inches(0.25) if thin else Inches(0.6) shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, height) shape.fill.solid() shape.fill.fore_color.rgb = BRAND_COLOR shape.line.fill.background()