from __future__ import annotations import argparse import json import os from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Dict, Any, Optional from pptx import Presentation # ============================================================ # PATHS / CONFIG (flexible via env vars and CLI) # ============================================================ def _env_path(name: str, default: Optional[str] = None) -> Optional[Path]: v = os.getenv(name, default) return Path(v).expanduser() if v else None OPENCLAW_WORKDIR = _env_path("OPENCLAW_WORKDIR", ".") # default: current directory PPTX_TEMPLATE_PATH = _env_path( "PPTX_TEMPLATE_PATH", str(OPENCLAW_WORKDIR / "template_openclaw_oci_clean.pptx"), ) PPTX_OUTPUT_PATH = _env_path( "PPTX_OUTPUT_PATH", str(OPENCLAW_WORKDIR / "openclaw_oci_presentation.pptx"), ) # Prefer OCI_CONTENT_FILE (policy name) but accept PPTX_CONTENT_PATH too PPTX_CONTENT_PATH = _env_path( "OCI_CONTENT_FILE", os.getenv("PPTX_CONTENT_PATH", str(OPENCLAW_WORKDIR / "content.json")), ) DEFAULT_LINK = "https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm" DEFAULT_PRESENTER = os.getenv("PPTX_PRESENTER", "Cristiano Hoshikawa") DEFAULT_COVER_SUBTITLE = os.getenv("PPTX_COVER_SUBTITLE", "Architecture") # ============================================================ # TEMPLATE ENGINE # ============================================================ class RedwoodSafePPT: """ Loads a PPTX template, wipes all existing slides safely, and builds a new deck using named layouts from the template. """ LAYOUT_COVER = "Cover 1 - Full Image" LAYOUT_CONTENT = "Full Page - Light" def __init__(self, template_path: Path): template_path = Path(template_path).expanduser() if not template_path.exists(): raise FileNotFoundError(f"Template not found: {template_path}") self.prs = Presentation(str(template_path)) # Remove ALL slides while len(self.prs.slides) > 0: rId = self.prs.slides._sldIdLst[0].rId self.prs.part.drop_rel(rId) del self.prs.slides._sldIdLst[0] self.layouts = {layout.name: layout for layout in self.prs.slide_layouts} def _layout(self, name: str): if name not in self.layouts: available = ", ".join(sorted(self.layouts.keys())) raise ValueError(f"Layout '{name}' not found in template. Available: {available}") return self.layouts[name] def add_content(self, title: str, subhead: str, body: str): slide = self.prs.slides.add_slide(self._layout(self.LAYOUT_CONTENT)) text_placeholders = [ph for ph in slide.placeholders if getattr(ph, "has_text_frame", False)] if len(text_placeholders) < 2: raise RuntimeError("Content layout must have at least 2 text placeholders.") text_placeholders[0].text = title text_placeholders[1].text = f"{subhead}\n\n{body}" def add_cover(self, title: str, subtitle: str, presenter: str): slide = self.prs.slides.add_slide(self._layout(self.LAYOUT_COVER)) text_placeholders = [ph for ph in slide.placeholders if getattr(ph, "has_text_frame", False)] if len(text_placeholders) < 2: raise RuntimeError("Cover layout must have at least 2 text placeholders.") text_placeholders[0].text = title text_placeholders[1].text = subtitle # Optional placeholders by name for ph in text_placeholders: name = (getattr(ph, "name", "") or "").lower() if "date" in name: ph.text = datetime.now().strftime("%d %b %Y") if "presenter" in name: ph.text = presenter def save(self, output_path: Path): output_path = Path(output_path).expanduser() output_path.parent.mkdir(parents=True, exist_ok=True) if output_path.exists(): output_path.unlink() self.prs.save(str(output_path)) # ============================================================ # DECK (fixed 7 slides) # ============================================================ class OCIStrategicArchitectDeck: def __init__(self, template_path: Path): self.ppt = RedwoodSafePPT(template_path) def _format_section(self, section: Dict[str, Any]) -> str: bullets = section.get("bullets", []) or [] evidence = section.get("evidence", []) or [] keywords = section.get("keywords", []) or [] lines = [] for b in bullets: lines.append(f"• {str(b).strip()}") # if evidence: # lines.append("") # lines.append("Evidence:") # for e in evidence[:2]: # lines.append(f"- {str(e).strip()}") # # if keywords: # lines.append("") # lines.append("Keywords: " + ", ".join([str(k).strip() for k in keywords[:8]])) return "\n".join(lines).strip() def build(self, material_link: str, content: Dict[str, Any], presenter: str, cover_subtitle: str): # 1) Cover self.ppt.add_cover( title=str(content["cover_title"]).strip(), subtitle=cover_subtitle, presenter=presenter, ) # 2) Intro self.ppt.add_content( title="Intro", subhead="Context and Motivation", body=self._format_section(content["introduction"]), ) # 3) Technologies self.ppt.add_content( title="Technologies", subhead="Stack OCI", body=self._format_section(content["technologies"]), ) # 4) Architecture self.ppt.add_content( title="Architecture", subhead="Architecture Flow", body=self._format_section(content["architecture"]), ) # 5) Problems self.ppt.add_content( title="Problems", subhead="Technical Challenges", body=self._format_section(content["problems"]), ) # 6) Demo self.ppt.add_content( title="Demo", subhead="Materials", body=f"{material_link}\n\n{self._format_section(content['demo'])}", ) # 7) Conclusion self.ppt.add_content( title="Conclusion", subhead="Strategies", body=self._format_section(content["conclusion"]), ) if len(self.ppt.prs.slides) != 7: raise RuntimeError("Deck must contain exactly 7 slides.") def save(self, output_path: Path): self.ppt.save(output_path) # ============================================================ # CLI # ============================================================ def _load_json(path: Path) -> Dict[str, Any]: path = Path(path).expanduser() if not path.exists(): raise FileNotFoundError(f"content.json not found: {path}") with open(path, "r", encoding="utf-8") as f: return json.load(f) def main(): parser = argparse.ArgumentParser(description="Generate a 7-slide OCI strategic PPTX from a template + content.json.") parser.add_argument("--template", default=str(PPTX_TEMPLATE_PATH), help="Path to the PPTX template file.") parser.add_argument("--output", default=str(PPTX_OUTPUT_PATH), help="Path to the output PPTX to be written.") parser.add_argument("--content", default=str(PPTX_CONTENT_PATH), help="Path to content.json.") parser.add_argument("--link", default=os.getenv("OCI_LINK_DEMO", DEFAULT_LINK), help="Source link shown on Demo slide.") parser.add_argument("--presenter", default=DEFAULT_PRESENTER, help="Presenter name on cover (if placeholder exists).") parser.add_argument("--cover-subtitle", default=DEFAULT_COVER_SUBTITLE, help="Cover subtitle.") args = parser.parse_args() content = _load_json(Path(args.content)) deck = OCIStrategicArchitectDeck(Path(args.template)) deck.build(args.link, content, presenter=args.presenter, cover_subtitle=args.cover_subtitle) deck.save(Path(args.output)) print("✅ PPT generated:", Path(args.output).expanduser().resolve()) if __name__ == "__main__": main()