Spaces:
Sleeping
Sleeping
| """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() | |