PlanOpticon

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

Keyboard Shortcuts

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