PlanOpticon

planopticon / video_processor / exporters / pptx_export.py
Source Blame History 202 lines
54d5d79… noreply 1 """Generate PPTX slide decks from knowledge graph data.
54d5d79… noreply 2
54d5d79… noreply 3 Uses python-pptx for slide generation. Falls back gracefully if not installed.
54d5d79… noreply 4 No LLM required — pure template-based generation from KG data.
54d5d79… noreply 5 """
54d5d79… noreply 6
54d5d79… noreply 7 import logging
54d5d79… noreply 8 from datetime import datetime
54d5d79… noreply 9 from pathlib import Path
54d5d79… noreply 10 from typing import Dict, List, Optional
54d5d79… noreply 11
54d5d79… noreply 12 logger = logging.getLogger(__name__)
54d5d79… noreply 13
54d5d79… noreply 14
54d5d79… noreply 15 def _add_title_slide(prs, title: str, subtitle: str):
54d5d79… noreply 16 """Add a title slide."""
54d5d79… noreply 17 from pptx.util import Pt
54d5d79… noreply 18
54d5d79… noreply 19 slide = prs.slides.add_slide(prs.slide_layouts[0])
54d5d79… noreply 20 slide.shapes.title.text = title
54d5d79… noreply 21 body = slide.placeholders[1]
54d5d79… noreply 22 body.text = subtitle
54d5d79… noreply 23 for paragraph in body.text_frame.paragraphs:
54d5d79… noreply 24 for run in paragraph.runs:
54d5d79… noreply 25 run.font.size = Pt(14)
54d5d79… noreply 26
54d5d79… noreply 27
54d5d79… noreply 28 def _add_content_slide(prs, title: str, bullets: List[str]):
54d5d79… noreply 29 """Add a slide with bullet points."""
54d5d79… noreply 30 from pptx.util import Pt
54d5d79… noreply 31
54d5d79… noreply 32 slide = prs.slides.add_slide(prs.slide_layouts[1])
54d5d79… noreply 33 slide.shapes.title.text = title
54d5d79… noreply 34 body = slide.placeholders[1]
54d5d79… noreply 35 tf = body.text_frame
54d5d79… noreply 36 tf.clear()
54d5d79… noreply 37
54d5d79… noreply 38 for i, bullet in enumerate(bullets):
54d5d79… noreply 39 if i == 0:
54d5d79… noreply 40 tf.paragraphs[0].text = bullet
54d5d79… noreply 41 for run in tf.paragraphs[0].runs:
54d5d79… noreply 42 run.font.size = Pt(14)
54d5d79… noreply 43 else:
54d5d79… noreply 44 p = tf.add_paragraph()
54d5d79… noreply 45 p.text = bullet
54d5d79… noreply 46 for run in p.runs:
54d5d79… noreply 47 run.font.size = Pt(14)
54d5d79… noreply 48
54d5d79… noreply 49
54d5d79… noreply 50 def _add_table_slide(prs, title: str, headers: List[str], rows: List[List[str]]):
54d5d79… noreply 51 """Add a slide with a table."""
54d5d79… noreply 52 from pptx.util import Emu, Inches, Pt
54d5d79… noreply 53
54d5d79… noreply 54 slide = prs.slides.add_slide(prs.slide_layouts[5]) # Blank layout
54d5d79… noreply 55 slide.shapes.title.text = title
54d5d79… noreply 56
54d5d79… noreply 57 num_rows = len(rows) + 1
54d5d79… noreply 58 num_cols = len(headers)
54d5d79… noreply 59
54d5d79… noreply 60 left = Inches(0.5)
54d5d79… noreply 61 top = Inches(1.5)
54d5d79… noreply 62 width = Inches(9.0)
54d5d79… noreply 63 row_height = Emu(int(Inches(0.35)))
54d5d79… noreply 64 height = row_height * num_rows
54d5d79… noreply 65
54d5d79… noreply 66 table_shape = slide.shapes.add_table(num_rows, num_cols, left, top, width, height)
54d5d79… noreply 67 table = table_shape.table
54d5d79… noreply 68
54d5d79… noreply 69 for i, header in enumerate(headers):
54d5d79… noreply 70 cell = table.cell(0, i)
54d5d79… noreply 71 cell.text = header
54d5d79… noreply 72 for paragraph in cell.text_frame.paragraphs:
54d5d79… noreply 73 paragraph.font.size = Pt(11)
54d5d79… noreply 74 paragraph.font.bold = True
54d5d79… noreply 75
54d5d79… noreply 76 for r_idx, row in enumerate(rows):
54d5d79… noreply 77 for c_idx, val in enumerate(row):
54d5d79… noreply 78 cell = table.cell(r_idx + 1, c_idx)
54d5d79… noreply 79 cell.text = str(val)
54d5d79… noreply 80 for paragraph in cell.text_frame.paragraphs:
54d5d79… noreply 81 paragraph.font.size = Pt(10)
54d5d79… noreply 82
54d5d79… noreply 83
54d5d79… noreply 84 def _add_image_slide(prs, title: str, image_path: Path):
54d5d79… noreply 85 """Add a slide with an embedded image."""
54d5d79… noreply 86 from pptx.util import Inches
54d5d79… noreply 87
54d5d79… noreply 88 slide = prs.slides.add_slide(prs.slide_layouts[5]) # Blank
54d5d79… noreply 89 slide.shapes.title.text = title
54d5d79… noreply 90
54d5d79… noreply 91 left = Inches(1.0)
54d5d79… noreply 92 top = Inches(1.5)
54d5d79… noreply 93 width = Inches(8.0)
54d5d79… noreply 94 slide.shapes.add_picture(str(image_path), left, top, width=width)
54d5d79… noreply 95
54d5d79… noreply 96
54d5d79… noreply 97 def generate_pptx(
54d5d79… noreply 98 kg_data: dict,
54d5d79… noreply 99 output_path: Path,
54d5d79… noreply 100 title: Optional[str] = None,
54d5d79… noreply 101 diagrams_dir: Optional[Path] = None,
54d5d79… noreply 102 ) -> Path:
54d5d79… noreply 103 """Generate a PPTX slide deck from knowledge graph data.
54d5d79… noreply 104
54d5d79… noreply 105 Args:
54d5d79… noreply 106 kg_data: Knowledge graph dict with 'nodes' and 'relationships'.
54d5d79… noreply 107 output_path: Path to write the PPTX file.
54d5d79… noreply 108 title: Optional presentation title.
54d5d79… noreply 109 diagrams_dir: Optional directory containing diagram images to embed.
54d5d79… noreply 110
54d5d79… noreply 111 Returns:
54d5d79… noreply 112 Path to the generated PPTX.
54d5d79… noreply 113
54d5d79… noreply 114 Raises:
54d5d79… noreply 115 ImportError: If python-pptx is not installed.
54d5d79… noreply 116 """
54d5d79… noreply 117 from pptx import Presentation
54d5d79… noreply 118
54d5d79… noreply 119 output_path = Path(output_path)
54d5d79… noreply 120 output_path.parent.mkdir(parents=True, exist_ok=True)
54d5d79… noreply 121
54d5d79… noreply 122 prs = Presentation()
54d5d79… noreply 123 nodes = kg_data.get("nodes", [])
54d5d79… noreply 124 rels = kg_data.get("relationships", [])
54d5d79… noreply 125
54d5d79… noreply 126 report_title = title or "Knowledge Graph"
54d5d79… noreply 127 now = datetime.now().strftime("%Y-%m-%d %H:%M")
54d5d79… noreply 128
54d5d79… noreply 129 # Title slide
54d5d79… noreply 130 _add_title_slide(
54d5d79… noreply 131 prs,
54d5d79… noreply 132 report_title,
54d5d79… noreply 133 f"Generated {now}\n{len(nodes)} entities \u2022 {len(rels)} relationships",
54d5d79… noreply 134 )
54d5d79… noreply 135
54d5d79… noreply 136 # Overview slide
54d5d79… noreply 137 by_type: Dict[str, list] = {}
54d5d79… noreply 138 for n in nodes:
54d5d79… noreply 139 t = n.get("type", "concept")
54d5d79… noreply 140 by_type.setdefault(t, []).append(n)
54d5d79… noreply 141
54d5d79… noreply 142 overview_bullets = [f"{len(nodes)} entities across {len(by_type)} types"]
54d5d79… noreply 143 for etype, elist in sorted(by_type.items(), key=lambda x: -len(x[1])):
54d5d79… noreply 144 examples = ", ".join(e.get("name", "") for e in elist[:3])
54d5d79… noreply 145 overview_bullets.append(f"{etype.title()} ({len(elist)}): {examples}")
54d5d79… noreply 146 _add_content_slide(prs, "Overview", overview_bullets)
54d5d79… noreply 147
54d5d79… noreply 148 # Key entities slide
54d5d79… noreply 149 degree: Dict[str, int] = {}
54d5d79… noreply 150 for r in rels:
54d5d79… noreply 151 degree[r.get("source", "")] = degree.get(r.get("source", ""), 0) + 1
54d5d79… noreply 152 degree[r.get("target", "")] = degree.get(r.get("target", ""), 0) + 1
54d5d79… noreply 153
54d5d79… noreply 154 top = sorted(degree.items(), key=lambda x: -x[1])[:10]
54d5d79… noreply 155 if top:
54d5d79… noreply 156 _add_table_slide(
54d5d79… noreply 157 prs,
54d5d79… noreply 158 "Key Entities",
54d5d79… noreply 159 ["Entity", "Connections"],
54d5d79… noreply 160 [[name, str(deg)] for name, deg in top],
54d5d79… noreply 161 )
54d5d79… noreply 162
54d5d79… noreply 163 # Diagram slides
54d5d79… noreply 164 if diagrams_dir and diagrams_dir.exists():
54d5d79… noreply 165 pngs = sorted(diagrams_dir.glob("*.png"))
54d5d79… noreply 166 for i, png in enumerate(pngs):
54d5d79… noreply 167 _add_image_slide(prs, f"Diagram {i + 1}", png)
54d5d79… noreply 168
54d5d79… noreply 169 # Relationship types slide
54d5d79… noreply 170 rel_types: Dict[str, int] = {}
54d5d79… noreply 171 for r in rels:
54d5d79… noreply 172 rt = r.get("type", "related_to")
54d5d79… noreply 173 rel_types[rt] = rel_types.get(rt, 0) + 1
54d5d79… noreply 174
54d5d79… noreply 175 if rel_types:
54d5d79… noreply 176 _add_table_slide(
54d5d79… noreply 177 prs,
54d5d79… noreply 178 "Relationship Types",
54d5d79… noreply 179 ["Type", "Count"],
54d5d79… noreply 180 [[rt, str(c)] for rt, c in sorted(rel_types.items(), key=lambda x: -x[1])],
54d5d79… noreply 181 )
54d5d79… noreply 182
54d5d79… noreply 183 # Entity detail slides (batched, max 12 per slide)
54d5d79… noreply 184 batch_size = 12
54d5d79… noreply 185 for batch_start in range(0, len(nodes), batch_size):
54d5d79… noreply 186 batch = nodes[batch_start : batch_start + batch_size]
54d5d79… noreply 187 bullets = []
54d5d79… noreply 188 for node in batch:
54d5d79… noreply 189 name = node.get("name", "")
54d5d79… noreply 190 etype = node.get("type", "concept")
54d5d79… noreply 191 descs = node.get("descriptions", [])
54d5d79… noreply 192 desc = descs[0][:80] if descs else ""
54d5d79… noreply 193 bullets.append(f"{name} ({etype}): {desc}")
54d5d79… noreply 194
54d5d79… noreply 195 slide_num = batch_start // batch_size + 1
54d5d79… noreply 196 total_pages = (len(nodes) + batch_size - 1) // batch_size
54d5d79… noreply 197 page_label = f" ({slide_num}/{total_pages})" if total_pages > 1 else ""
54d5d79… noreply 198 _add_content_slide(prs, f"Entities{page_label}", bullets)
54d5d79… noreply 199
54d5d79… noreply 200 prs.save(str(output_path))
54d5d79… noreply 201 logger.info(f"Generated PPTX: {output_path}")
54d5d79… noreply 202 return output_path

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button