PlanOpticon

Phase 5: Add rich output formats with mermaid rendering and PDF export Stories 5.1-5.5: Mermaid-to-SVG/PNG rendering, matplotlib chart reproduction from extracted data, self-contained HTML report with embedded diagrams and mermaid.js, PDF export via weasyprint, multi-format orchestration in export_all_formats(). Added mermaid-py, plotly, kaleido, weasyprint deps.

leo 2026-02-14 22:20 trunk
Commit cd8f2f9f153659bce7c550e85922b542eb52992d2a34aee4b78dd4d170a2dd9f
--- requirements.txt
+++ requirements.txt
@@ -22,10 +22,16 @@
2222
2323
# Markdown & visualization
2424
markdown>=3.4.0
2525
pymdown-extensions>=10.0.0
2626
python-markdown-math>=0.8.0
27
+mermaid-py>=0.5.0
28
+plotly>=5.18.0
29
+kaleido>=0.2.1
30
+
31
+# PDF generation (optional)
32
+weasyprint>=60.0
2733
2834
# Utilities
2935
python-dotenv>=1.0.0
3036
requests>=2.31.0
3137
aiohttp>=3.8.5
3238
3339
ADDED tests/test_rendering.py
3440
ADDED video_processor/utils/export.py
3541
ADDED video_processor/utils/rendering.py
--- requirements.txt
+++ requirements.txt
@@ -22,10 +22,16 @@
22
23 # Markdown & visualization
24 markdown>=3.4.0
25 pymdown-extensions>=10.0.0
26 python-markdown-math>=0.8.0
 
 
 
 
 
 
27
28 # Utilities
29 python-dotenv>=1.0.0
30 requests>=2.31.0
31 aiohttp>=3.8.5
32
33 DDED tests/test_rendering.py
34 DDED video_processor/utils/export.py
35 DDED video_processor/utils/rendering.py
--- requirements.txt
+++ requirements.txt
@@ -22,10 +22,16 @@
22
23 # Markdown & visualization
24 markdown>=3.4.0
25 pymdown-extensions>=10.0.0
26 python-markdown-math>=0.8.0
27 mermaid-py>=0.5.0
28 plotly>=5.18.0
29 kaleido>=0.2.1
30
31 # PDF generation (optional)
32 weasyprint>=60.0
33
34 # Utilities
35 python-dotenv>=1.0.0
36 requests>=2.31.0
37 aiohttp>=3.8.5
38
39 DDED tests/test_rendering.py
40 DDED video_processor/utils/export.py
41 DDED video_processor/utils/rendering.py
--- a/tests/test_rendering.py
+++ b/tests/test_rendering.py
@@ -0,0 +1,261 @@
1
+"""Tests for renderingg and export utilities."""
2
+
3
+from unittest.mock import patch
4
+
5
+from video_processor.models import (
6
+ ActionItem,
7
+ DiagramResult,
8
+ DiagramType,
9
+ KeyPoint,
10
+ ProcessingStats,
11
+ VideoManifest,
12
+ VideoMetadata,
13
+)
14
+from video_processor.utils.rendering import render_mermaid, reproduce_chart
15
+
16
+
17
+class TestRenderMermaid:
18
+ def test_writes_mermaid_source(self, tmp_path):
19
+ code = "graph LR\n A-->B"
20
+ result = render_mermaid(code, tmp_path, "test_diagram")
21
+ assert "mermaid" in result
22
+ assert result["mermaid"].exists()
23
+ assert result["mermaid"].read_text() == code
24
+
25
+ def test_source_file_named_correctly(self, tmp_path):
26
+ result = render_mermaid("graph TD\n X-->Y", tmp_path, "my_chart")
27
+ assert result["mermaid"].name == "my_chart.mermaid"
28
+
29
+ @patch("video_processor.utils.rendering.mmd", create=True)
30
+ def test_svg_png_on_import_error(self, mock_mmd, tmp_path):
31
+ """When mermaid-py is not installed, only source is written."""
32
+ # Simulate import error by using the real code path
33
+ # (mermaid-py may or may not be installed in test env)
34
+ result = render_mermaid("graph LR\n A-->B", tmp_path, "test")
35
+ # At minimum, mermaid source should always be written
36
+ assert "mermaid" in result
37
+ assert result["mermaid"].exists()
38
+
39
+ def test_creates_output_dir(self, tmp_path):
40
+ nested = tmp_path / "a" / "b"
41
+ result = render_mermaid("graph LR\n A-->B", nested, "test")
42
+ assert nested.exists()
43
+ assert result["mermaid"].exists()
44
+
45
+
46
+class TestReproduceChart:
47
+ def test_bar_chart(self, tmp_path):
48
+ data = {
49
+ "labels": ["A", "B", "C"],
50
+ "values": [10, 20, 30],
51
+ "chart_type": "bar",
52
+ }
53
+ result = reproduce_chart(data, tmp_path, "test")
54
+ assert "svg" in result
55
+ assert "png" in result
56
+ assert result["svg"].exists()
57
+ assert result["png"].exists()
58
+ assert result["svg"].suffix == ".svg"
59
+ assert result["png"].suffix == ".png"
60
+
61
+ def test_line_chart(self, tmp_path):
62
+ data = {
63
+ "labels": ["Jan", "Feb", "Mar"],
64
+ "values": [5, 15, 10],
65
+ "chart_type": "line",
66
+ }
67
+ result = reproduce_chart(data, tmp_path, "line_test")
68
+ assert "svg" in result
69
+ assert result["svg"].exists()
70
+
71
+ def test_pie_chart(self, tmp_path):
72
+ data = {
73
+ "labels": ["Dogs", "Cats"],
74
+ "values": [60, 40],
75
+ "chart_type": "pie",
76
+ }
77
+ result = reproduce_chart(data, tmp_path, "pie_test")
78
+ assert "svg" in result
79
+
80
+ def test_scatter_chart(self, tmp_path):
81
+ data = {
82
+ "labels": ["X1", "X2", "X3"],
83
+ "values": [1, 4, 9],
84
+ "chart_type": "scatter",
85
+ }
86
+ result = reproduce_chart(data, tmp_path, "scatter_test")
87
+ assert "svg" in result
88
+
89
+ def test_empty_data_returns_empty(self, tmp_path):
90
+ data = {"labels": [], "values": [], "chart_type": "bar"}
91
+ result = reproduce_chart(data, tmp_path, "empty")
92
+ assert result == {}
93
+
94
+ def test_missing_values_returns_empty(self, tmp_path):
95
+ data = {"labels": ["A", "B"]}
96
+ result = reproduce_chart(data, tmp_path, "no_vals")
97
+ assert result == {}
98
+
99
+ def test_creates_output_dir(self, tmp_path):
100
+ nested = tmp_path / "charts" / "output"
101
+ data = {"labels": ["A"], "values": [1], "chart_type": "bar"}
102
+ reproduce_chart(data, nested, "test")
103
+ assert nested.exists()
104
+
105
+
106
+class TestExportAllFormats:
107
+ def _make_manifest(self) -> VideoManifest:
108
+ return VideoManifest(
109
+ video=VideoMetadata(title="Test Video"),
110
+ stats=ProcessingStats(frames_extracted=5, diagrams_detected=1),
111
+ analysis_md="results/analysis.md",
112
+ key_points=[KeyPoint(point="Important finding")],
113
+ action_items=[ActionItem(action="Follow up", assignee="Alice")],
114
+ diagrams=[
115
+ DiagramResult(
116
+ frame_index=0,
117
+ diagram_type=DiagramType.flowchart,
118
+ confidence=0.9,
119
+ description="Login flow",
120
+ mermaid="graph LR\n Login-->Dashboard",
121
+ image_path="diagrams/diagram_0.jpg",
122
+ ),
123
+ ],
124
+ )
125
+
126
+ def test_export_renders_mermaid(self, tmp_path):
127
+ from video_processor.utils.export import export_all_formats
128
+
129
+ manifest = self._make_manifest()
130
+
131
+ # Create required dirs and files
132
+ (tmp_path / "results").mkdir()
133
+ (tmp_path / "results" / "analysis.md").write_text("# Test\nContent")
134
+ (tmp_path / "diagrams").mkdir()
135
+ (tmp_path / "diagrams" / "diagram_0.jpg").write_bytes(b"\xff\xd8\xff")
136
+
137
+ result = export_all_formats(tmp_path, manifest)
138
+
139
+ # Mermaid source should be written
140
+ assert (tmp_path / "diagrams" / "diagram_0.mermaid").exists()
141
+ # Manifest should be updated
142
+ assert result.diagrams[0].mermaid_path is not None
143
+
144
+ def test_export_generates_html(self, tmp_path):
145
+ from video_processor.utils.export import export_all_formats
146
+
147
+ manifest = self._make_manifest()
148
+ (tmp_path / "results").mkdir()
149
+ (tmp_path / "results" / "analysis.md").write_text("# Test")
150
+ (tmp_path / "diagrams").mkdir()
151
+
152
+ result = export_all_formats(tmp_path, manifest)
153
+ assert result.analysis_html is not None
154
+ html_path = tmp_path / result.analysis_html
155
+ assert html_path.exists()
156
+ html_content = html_path.read_text()
157
+ assert "Test Video" in html_content
158
+ assert "mermaid" in html_content.lower()
159
+
160
+ def test_export_with_chart_data(self, tmp_path):
161
+ from video_processor.utils.export import export_all_formats
162
+
163
+ manifest = VideoManifest(
164
+ video=VideoMetadata(title="Chart Test"),
165
+ diagrams=[
166
+ DiagramResult(
167
+ frame_index=0,
168
+ diagram_type=DiagramType.chart,
169
+ confidence=0.9,
170
+ chart_data={
171
+ "labels": ["Q1", "Q2", "Q3"],
172
+ "values": [100, 200, 150],
173
+ "chart_type": "bar",
174
+ },
175
+ ),
176
+ ],
177
+ )
178
+ (tmp_path / "results").mkdir()
179
+ (tmp_path / "diagrams").mkdir()
180
+
181
+ export_all_formats(tmp_path, manifest)
182
+ # Chart should be reproduced
183
+ chart_svg = tmp_path / "diagrams" / "diagram_0_chart.svg"
184
+ assert chart_svg.exists()
185
+
186
+
187
+class TestGenerateHtmlReport:
188
+ def test_html_contains_title(self, tmp_path):
189
+ from video_processor.utils.export import generate_html_report
190
+
191
+ manifest = VideoManifest(
192
+ video=VideoMetadata(title="My Meeting"),
193
+ analysis_md="results/analysis.md",
194
+ )
195
+ (tmp_path / "results").mkdir()
196
+ (tmp_path / "results" / "analysis.md").write_text("# My Meeting\nNotes here.")
197
+
198
+ path = generate_html_report(manifest, tmp_path)
199
+ assert path is not None
200
+ content = path.read_text()
201
+ assert "My Meeting" in content
202
+
203
+ def test_html_includes_key_points(self, tmp_path):
204
+ from video_processor.utils.export import generate_html_report
205
+
206
+ manifest = VideoManifest(
207
+ video=VideoMetadata(title="Test"),
208
+ key_points=[
209
+ KeyPoint(point="First point", details="Detail 1"),
210
+ KeyPoint(point="Second point"),
211
+ ],
212
+ )
213
+ (tmp_path / "results").mkdir()
214
+
215
+ path = generate_html_report(manifest, tmp_path)
216
+ content = path.read_text()
217
+ assert "First point" in content
218
+ assert "Detail 1" in content
219
+ assert "Second point" in content
220
+
221
+ def test_html_includes_action_items(self, tmp_path):
222
+ from video_processor.utils.export import generate_html_report
223
+
224
+ manifest = VideoManifest(
225
+ video=VideoMetadata(title="Test"),
226
+ action_items=[
227
+ ActionItem(action="Do the thing", assignee="Bob", deadline="Friday"),
228
+ ],
229
+ )
230
+ (tmp_path / "results").mkdir()
231
+
232
+ path = generate_html_report(manifest, tmp_path)
233
+ content = path.read_text()
234
+ assert "Do the thing" in content
235
+ assert "Bob" in content
236
+ assert "Friday" in content
237
+
238
+ def test_html_includes_mermaid_js(self, tmp_path):
239
+ from video_processor.utils.export import generate_html_report
240
+
241
+ manifest = VideoManifest(
242
+ video=VideoMetadata(title="Test"),
243
+ diagrams=[
244
+ DiagramResult(
245
+ frame_index=0,
246
+ mermaid="graph LR\n A-->B",
247
+ )
248
+ ],
249
+ )
250
+ (tmp_path / "results").mkdir()
251
+
252
+ path = generate_html_report(manifest, tmp_path)
253
+ content = path.read_text()
254
+ assert "mermaid" in content
255
+ assert "A-->B" in content
256
+MagicMock, patch
257
+
258
+import pytest
259
+
260
+from video_processor.models import (
261
+ Action
--- a/tests/test_rendering.py
+++ b/tests/test_rendering.py
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_rendering.py
+++ b/tests/test_rendering.py
@@ -0,0 +1,261 @@
1 """Tests for renderingg and export utilities."""
2
3 from unittest.mock import patch
4
5 from video_processor.models import (
6 ActionItem,
7 DiagramResult,
8 DiagramType,
9 KeyPoint,
10 ProcessingStats,
11 VideoManifest,
12 VideoMetadata,
13 )
14 from video_processor.utils.rendering import render_mermaid, reproduce_chart
15
16
17 class TestRenderMermaid:
18 def test_writes_mermaid_source(self, tmp_path):
19 code = "graph LR\n A-->B"
20 result = render_mermaid(code, tmp_path, "test_diagram")
21 assert "mermaid" in result
22 assert result["mermaid"].exists()
23 assert result["mermaid"].read_text() == code
24
25 def test_source_file_named_correctly(self, tmp_path):
26 result = render_mermaid("graph TD\n X-->Y", tmp_path, "my_chart")
27 assert result["mermaid"].name == "my_chart.mermaid"
28
29 @patch("video_processor.utils.rendering.mmd", create=True)
30 def test_svg_png_on_import_error(self, mock_mmd, tmp_path):
31 """When mermaid-py is not installed, only source is written."""
32 # Simulate import error by using the real code path
33 # (mermaid-py may or may not be installed in test env)
34 result = render_mermaid("graph LR\n A-->B", tmp_path, "test")
35 # At minimum, mermaid source should always be written
36 assert "mermaid" in result
37 assert result["mermaid"].exists()
38
39 def test_creates_output_dir(self, tmp_path):
40 nested = tmp_path / "a" / "b"
41 result = render_mermaid("graph LR\n A-->B", nested, "test")
42 assert nested.exists()
43 assert result["mermaid"].exists()
44
45
46 class TestReproduceChart:
47 def test_bar_chart(self, tmp_path):
48 data = {
49 "labels": ["A", "B", "C"],
50 "values": [10, 20, 30],
51 "chart_type": "bar",
52 }
53 result = reproduce_chart(data, tmp_path, "test")
54 assert "svg" in result
55 assert "png" in result
56 assert result["svg"].exists()
57 assert result["png"].exists()
58 assert result["svg"].suffix == ".svg"
59 assert result["png"].suffix == ".png"
60
61 def test_line_chart(self, tmp_path):
62 data = {
63 "labels": ["Jan", "Feb", "Mar"],
64 "values": [5, 15, 10],
65 "chart_type": "line",
66 }
67 result = reproduce_chart(data, tmp_path, "line_test")
68 assert "svg" in result
69 assert result["svg"].exists()
70
71 def test_pie_chart(self, tmp_path):
72 data = {
73 "labels": ["Dogs", "Cats"],
74 "values": [60, 40],
75 "chart_type": "pie",
76 }
77 result = reproduce_chart(data, tmp_path, "pie_test")
78 assert "svg" in result
79
80 def test_scatter_chart(self, tmp_path):
81 data = {
82 "labels": ["X1", "X2", "X3"],
83 "values": [1, 4, 9],
84 "chart_type": "scatter",
85 }
86 result = reproduce_chart(data, tmp_path, "scatter_test")
87 assert "svg" in result
88
89 def test_empty_data_returns_empty(self, tmp_path):
90 data = {"labels": [], "values": [], "chart_type": "bar"}
91 result = reproduce_chart(data, tmp_path, "empty")
92 assert result == {}
93
94 def test_missing_values_returns_empty(self, tmp_path):
95 data = {"labels": ["A", "B"]}
96 result = reproduce_chart(data, tmp_path, "no_vals")
97 assert result == {}
98
99 def test_creates_output_dir(self, tmp_path):
100 nested = tmp_path / "charts" / "output"
101 data = {"labels": ["A"], "values": [1], "chart_type": "bar"}
102 reproduce_chart(data, nested, "test")
103 assert nested.exists()
104
105
106 class TestExportAllFormats:
107 def _make_manifest(self) -> VideoManifest:
108 return VideoManifest(
109 video=VideoMetadata(title="Test Video"),
110 stats=ProcessingStats(frames_extracted=5, diagrams_detected=1),
111 analysis_md="results/analysis.md",
112 key_points=[KeyPoint(point="Important finding")],
113 action_items=[ActionItem(action="Follow up", assignee="Alice")],
114 diagrams=[
115 DiagramResult(
116 frame_index=0,
117 diagram_type=DiagramType.flowchart,
118 confidence=0.9,
119 description="Login flow",
120 mermaid="graph LR\n Login-->Dashboard",
121 image_path="diagrams/diagram_0.jpg",
122 ),
123 ],
124 )
125
126 def test_export_renders_mermaid(self, tmp_path):
127 from video_processor.utils.export import export_all_formats
128
129 manifest = self._make_manifest()
130
131 # Create required dirs and files
132 (tmp_path / "results").mkdir()
133 (tmp_path / "results" / "analysis.md").write_text("# Test\nContent")
134 (tmp_path / "diagrams").mkdir()
135 (tmp_path / "diagrams" / "diagram_0.jpg").write_bytes(b"\xff\xd8\xff")
136
137 result = export_all_formats(tmp_path, manifest)
138
139 # Mermaid source should be written
140 assert (tmp_path / "diagrams" / "diagram_0.mermaid").exists()
141 # Manifest should be updated
142 assert result.diagrams[0].mermaid_path is not None
143
144 def test_export_generates_html(self, tmp_path):
145 from video_processor.utils.export import export_all_formats
146
147 manifest = self._make_manifest()
148 (tmp_path / "results").mkdir()
149 (tmp_path / "results" / "analysis.md").write_text("# Test")
150 (tmp_path / "diagrams").mkdir()
151
152 result = export_all_formats(tmp_path, manifest)
153 assert result.analysis_html is not None
154 html_path = tmp_path / result.analysis_html
155 assert html_path.exists()
156 html_content = html_path.read_text()
157 assert "Test Video" in html_content
158 assert "mermaid" in html_content.lower()
159
160 def test_export_with_chart_data(self, tmp_path):
161 from video_processor.utils.export import export_all_formats
162
163 manifest = VideoManifest(
164 video=VideoMetadata(title="Chart Test"),
165 diagrams=[
166 DiagramResult(
167 frame_index=0,
168 diagram_type=DiagramType.chart,
169 confidence=0.9,
170 chart_data={
171 "labels": ["Q1", "Q2", "Q3"],
172 "values": [100, 200, 150],
173 "chart_type": "bar",
174 },
175 ),
176 ],
177 )
178 (tmp_path / "results").mkdir()
179 (tmp_path / "diagrams").mkdir()
180
181 export_all_formats(tmp_path, manifest)
182 # Chart should be reproduced
183 chart_svg = tmp_path / "diagrams" / "diagram_0_chart.svg"
184 assert chart_svg.exists()
185
186
187 class TestGenerateHtmlReport:
188 def test_html_contains_title(self, tmp_path):
189 from video_processor.utils.export import generate_html_report
190
191 manifest = VideoManifest(
192 video=VideoMetadata(title="My Meeting"),
193 analysis_md="results/analysis.md",
194 )
195 (tmp_path / "results").mkdir()
196 (tmp_path / "results" / "analysis.md").write_text("# My Meeting\nNotes here.")
197
198 path = generate_html_report(manifest, tmp_path)
199 assert path is not None
200 content = path.read_text()
201 assert "My Meeting" in content
202
203 def test_html_includes_key_points(self, tmp_path):
204 from video_processor.utils.export import generate_html_report
205
206 manifest = VideoManifest(
207 video=VideoMetadata(title="Test"),
208 key_points=[
209 KeyPoint(point="First point", details="Detail 1"),
210 KeyPoint(point="Second point"),
211 ],
212 )
213 (tmp_path / "results").mkdir()
214
215 path = generate_html_report(manifest, tmp_path)
216 content = path.read_text()
217 assert "First point" in content
218 assert "Detail 1" in content
219 assert "Second point" in content
220
221 def test_html_includes_action_items(self, tmp_path):
222 from video_processor.utils.export import generate_html_report
223
224 manifest = VideoManifest(
225 video=VideoMetadata(title="Test"),
226 action_items=[
227 ActionItem(action="Do the thing", assignee="Bob", deadline="Friday"),
228 ],
229 )
230 (tmp_path / "results").mkdir()
231
232 path = generate_html_report(manifest, tmp_path)
233 content = path.read_text()
234 assert "Do the thing" in content
235 assert "Bob" in content
236 assert "Friday" in content
237
238 def test_html_includes_mermaid_js(self, tmp_path):
239 from video_processor.utils.export import generate_html_report
240
241 manifest = VideoManifest(
242 video=VideoMetadata(title="Test"),
243 diagrams=[
244 DiagramResult(
245 frame_index=0,
246 mermaid="graph LR\n A-->B",
247 )
248 ],
249 )
250 (tmp_path / "results").mkdir()
251
252 path = generate_html_report(manifest, tmp_path)
253 content = path.read_text()
254 assert "mermaid" in content
255 assert "A-->B" in content
256 MagicMock, patch
257
258 import pytest
259
260 from video_processor.models import (
261 Action
--- a/video_processor/utils/export.py
+++ b/video_processor/utils/export.py
@@ -0,0 +1,97 @@
1
+"""Multi-format outputjsont output orchestration."""
2
+
3
+import logging
4
+from pathlib import Path
5
+from typing import Optional
6
+
7
+from vidDiagramResult,deo_processor.models import VideoManifest
8
+from video_processor.utils.rendering import render_mermaid, reproduce_chart
9
+
10
+logger = logging.getLogger(__name__)
11
+
12
+
13
+def generate_html_report(
14
+ manifest: VideoManifest,
15
+ output_dir: Path,
16
+) -> Optional[Path]:
17
+ """
18
+ Generate a self-contained HTML report with embedded diagrams.
19
+
20
+ Reads the markdown analysis and enriches it with rendered SVGs
21
+ and mermaid.js for any unrendered blocks.
22
+ """
23
+ output_dir = Path(output_dir)
24
+ results_dir = output_dir / "results"
25
+ results_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ # Read markdown if available
28
+ md_content = ""
29
+ if manifest.analysis_md:
30
+ md_path = output_dir / manifest.analysis_md
31
+ if h.read_text()
32
+
33
+ # Convert markdown to HTML
34
+ try:
35
+ import markdown
36
+
37
+ if d]
38
+ ):
39
+ if dimd_content,
40
+ extensions=["fenced_code", "tables", "toc"],
41
+ )
42
+ except ImportError:
43
+ logger.warning("markdown library not available, using raw text")
44
+ html_body = f"<pre>{md_content}</pre>"
45
+
46
+ # Build sections for key points, action items
47
+ sections = []
48
+
49
+ if manifest.key_points:
50
+ kp_html = "<h2>Key Points</h2><ul>"
51
+ for kp in manifest.key_points:
52
+ kp_html += f"<li><strong>{kp.point}</strong>"
53
+ if kp.details:
54
+ kp_html += f" - {kp.details}"
55
+ kp_html += "</li>"
56
+ kp_html += "</ul>"
57
+ sections.append(kp_html)
58
+
59
+ if manifest.action_items:
60
+ ai_html = "<h2>Action Items</h2><ul>"
61
+ for ai in manifest.action_items:
62
+ ai_html += f"<li><strongtqdm(manifest.diagrams, desc="Rendering diagrams", unit="diag") if manifest.diagrams else []ifest.diagrams else []
63
+ ):
64
+ if diagram.mermaid:
65
+ diagrams_dir = output_dir / "diagrams"
66
+ prefix = f"diagram_{i}"
67
+ paths = render_mermaid(diagram.mermaid, diagrams_dir, prefix)
68
+ if "svg" in paths:
69
+ diagram.svg_path = f"diagrams/{prefix}.svg"
70
+ if "png" in paths:
71
+ diagram.png_path = f"diagrams/{prefix}.png"
72
+ if "mermaid" in paths and not diagram.mermaid_path:
73
+ diagram.mermaid_path = f"diagrams/{prefix}.mermaid"
74
+
75
+ # Reproduce charts
76
+ if diagram.chart_data and diagram.diagram_type.value == "chart":
77
+ chart_paths = reproduce_chart(
78
+ diagram.chart_data,
79
+ output_dir / "diagrams",
80
+ f"diagram_{i}",
81
+ )
82
+ if "svg" in chart_paths:
83
+ diagram.svg_path = f"diagrams/diagram_{i}_chart.svg"
84
+ if "png" in chart_paths:
85
+ diagram.png_path = f"diagrams/diagram_{i}_chart.png"
86
+
87
+ # Generate HTML report
88
+ html_path = generate_html_report(manifest, output_dir)
89
+ if html_path:
90
+ manifest.analysis_html = str(html_path.relative_to(output_dir))
91
+
92
+ # Generate PDF from HTML
93
+ if html_path:
94
+ pdf_path = output_dir / "results" / "analysis.pdf"
95
+ result = generate_pdf_report(html_path, pdf_path)
96
+ if result:
97
+ manifest.analysis_pdf = str(pdf_path.relative_to(output_manifest.diagrams):s=["fenced_code", "ta
--- a/video_processor/utils/export.py
+++ b/video_processor/utils/export.py
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/video_processor/utils/export.py
+++ b/video_processor/utils/export.py
@@ -0,0 +1,97 @@
1 """Multi-format outputjsont output orchestration."""
2
3 import logging
4 from pathlib import Path
5 from typing import Optional
6
7 from vidDiagramResult,deo_processor.models import VideoManifest
8 from video_processor.utils.rendering import render_mermaid, reproduce_chart
9
10 logger = logging.getLogger(__name__)
11
12
13 def generate_html_report(
14 manifest: VideoManifest,
15 output_dir: Path,
16 ) -> Optional[Path]:
17 """
18 Generate a self-contained HTML report with embedded diagrams.
19
20 Reads the markdown analysis and enriches it with rendered SVGs
21 and mermaid.js for any unrendered blocks.
22 """
23 output_dir = Path(output_dir)
24 results_dir = output_dir / "results"
25 results_dir.mkdir(parents=True, exist_ok=True)
26
27 # Read markdown if available
28 md_content = ""
29 if manifest.analysis_md:
30 md_path = output_dir / manifest.analysis_md
31 if h.read_text()
32
33 # Convert markdown to HTML
34 try:
35 import markdown
36
37 if d]
38 ):
39 if dimd_content,
40 extensions=["fenced_code", "tables", "toc"],
41 )
42 except ImportError:
43 logger.warning("markdown library not available, using raw text")
44 html_body = f"<pre>{md_content}</pre>"
45
46 # Build sections for key points, action items
47 sections = []
48
49 if manifest.key_points:
50 kp_html = "<h2>Key Points</h2><ul>"
51 for kp in manifest.key_points:
52 kp_html += f"<li><strong>{kp.point}</strong>"
53 if kp.details:
54 kp_html += f" - {kp.details}"
55 kp_html += "</li>"
56 kp_html += "</ul>"
57 sections.append(kp_html)
58
59 if manifest.action_items:
60 ai_html = "<h2>Action Items</h2><ul>"
61 for ai in manifest.action_items:
62 ai_html += f"<li><strongtqdm(manifest.diagrams, desc="Rendering diagrams", unit="diag") if manifest.diagrams else []ifest.diagrams else []
63 ):
64 if diagram.mermaid:
65 diagrams_dir = output_dir / "diagrams"
66 prefix = f"diagram_{i}"
67 paths = render_mermaid(diagram.mermaid, diagrams_dir, prefix)
68 if "svg" in paths:
69 diagram.svg_path = f"diagrams/{prefix}.svg"
70 if "png" in paths:
71 diagram.png_path = f"diagrams/{prefix}.png"
72 if "mermaid" in paths and not diagram.mermaid_path:
73 diagram.mermaid_path = f"diagrams/{prefix}.mermaid"
74
75 # Reproduce charts
76 if diagram.chart_data and diagram.diagram_type.value == "chart":
77 chart_paths = reproduce_chart(
78 diagram.chart_data,
79 output_dir / "diagrams",
80 f"diagram_{i}",
81 )
82 if "svg" in chart_paths:
83 diagram.svg_path = f"diagrams/diagram_{i}_chart.svg"
84 if "png" in chart_paths:
85 diagram.png_path = f"diagrams/diagram_{i}_chart.png"
86
87 # Generate HTML report
88 html_path = generate_html_report(manifest, output_dir)
89 if html_path:
90 manifest.analysis_html = str(html_path.relative_to(output_dir))
91
92 # Generate PDF from HTML
93 if html_path:
94 pdf_path = output_dir / "results" / "analysis.pdf"
95 result = generate_pdf_report(html_path, pdf_path)
96 if result:
97 manifest.analysis_pdf = str(pdf_path.relative_to(output_manifest.diagrams):s=["fenced_code", "ta
--- a/video_processor/utils/rendering.py
+++ b/video_processor/utils/rendering.py
@@ -0,0 +1,134 @@
1
+"""Mermaid rendering and chart reproduction utilities."""
2
+
3
+import logging
4
+from pathlib import P, Optionalath
5
+from typing import Dict
6
+
7
+logger = logging.getLogger(__name__)
8
+
9
+
10
+def render_mermaid(mermaid_code: str, output_dir: str | Path, name: str) -> Dict[str, Path]:
11
+ """
12
+ Render mermaid code to SVG and PNG files.
13
+
14
+ Writes {name}.mermaid (source), {name}.svg, and {name}.png.
15
+ Uses mermaid-py if available, falls back gracefully.
16
+
17
+ Returns dict with keys: mermaid, svg, png (Paths to generated files).
18
+ """
19
+ output_dir = Path(output_dir)
20
+ output_dir.mkdir(parents=True, exist_ok=True)
21
+ result: Dict[str, Path] = {}
22
+
23
+ # Always write source
24
+ mermaid_path = output_dir / f"{name}.mermaid"
25
+ mermaid_path.write_text(mermaid_code)
26
+ result["mermaid"] = mermaid_path
27
+
28
+ try:
29
+ import mermaid as mmd
30
+ from mermaid.graph import Graph
31
+
32
+ graph = Graph("diagram", mermaid_code)
33
+ rendered = mmd.Mermaid(graph)
34
+
35
+ # SVG
36
+ svg_path = output_dir / f"{name}.svg"
37
+ svg_content = rendered.svg_response
38
+ if svg_content:
39
+ if isinstance(svg_content, bytes):
40
+ svg_path.write_bytes(svg_content)
41
+ else:
42
+ svg_path.write_text(svg_content)
43
+ result["svg"] = svg_path
44
+
45
+ # PNG
46
+ png_path = output_dir / f"{name}.png"
47
+ png_content = rendered.img_response
48
+ if png_content:
49
+ if isinstance(png_content, bytes):
50
+ png_path.write_bytes(png_content)
51
+ else:
52
+ bytes(
53
+ png_content.encode() if isinstance(png_con)
54
+ result["png"] = png_path
55
+
56
+ except ImportError:
57
+ logger.warning(ogger.warning(
58
+ " rendering)
59
+ except Exception as e:
60
+ logger.warning(f"Mermaid rendering failed for '{name}': {e}")
61
+
62
+ return result
63
+
64
+
65
+def reproduce_chart(
66
+ chart_data: dict,
67
+ output_dir: str | Path,
68
+ name: str,
69
+) -> Dict[str, Path]:
70
+ """
71
+ Reproduce a chart from extracted data using matplotlib.
72
+
73
+ chart_data should contain: labels, values, chart_type (bar/line/pie/scatter).
74
+ Returns dict with keys: svg, png (Paths to generated files).
75
+ """
76
+ output_dir = Path(output_dir)
77
+ output_dir.mkdir(parents=True, exist_ok=True)
78
+ result: Dict[str, Path] = {}
79
+
80
+ labels = chart_data.get("labels", [])
81
+ values = chart_data.get("values", [])
82
+ chart_type = chart_data.get("chart_type", "bar")
83
+
84
+ if not labels or not values:
85
+ logger.warning(f"Insufficient chart data for '{name}': missing labels or values")
86
+ return result
87
+
88
+ try:
89
+ import matplotlib
90
+
91
+ matplotlib.use("Agg") # Non-interactive backend
92
+ import matplotlib.pyplot as plt
93
+
94
+ fig, ax = plt.subplots(figsize=(10, 6))
95
+
96
+ if chart_type == "bar":
97
+ ax.bar(labels, values)
98
+ elif chart_type == "line":
99
+ ax.plot(labels, values, marker="o")
100
+ elif chart_type == "pie":
101
+ ax.pie(values, labels=labels, autopct="%1.1f%%")
102
+ elif chart_type == "scatter":
103
+ ax.scatter(range(len(values)), values)
104
+ if labels:
105
+ ax.set_xticks(range(len(labels)))
106
+ ax.set_xticklabels(labels, rotation=45, ha="right")
107
+ else:
108
+ ax.bar(labels, values)
109
+
110
+ if chart_type != "pie":
111
+ ax.set_xlabel("")
112
+ ax.set_ylabel("")
113
+ plt.xticks(rotation=45, ha="right")
114
+
115
+ plt.tight_layout()
116
+
117
+ # SVG
118
+ svg_path = output_dir / f"{name}_chart.svg"
119
+ fig.savefig(svg_path, format="svg")
120
+ result["svg"] = svg_path
121
+
122
+ # PNG
123
+ png_path = output_dir / f"{name}_chart.png"
124
+ fig.savefig(png_path, format="png", dpi=150)
125
+ result["png"] = png_path
126
+
127
+ plt.close(fig)
128
+
129
+ except ImportError:
130
+ logger.warning("matplotlib not installed, skipping chart reproduction")
131
+ except Exception as e:
132
+ logger.warning(f"Chart reproduction failed for '{name}': {e}")
133
+
134
+ return result
--- a/video_processor/utils/rendering.py
+++ b/video_processor/utils/rendering.py
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/video_processor/utils/rendering.py
+++ b/video_processor/utils/rendering.py
@@ -0,0 +1,134 @@
1 """Mermaid rendering and chart reproduction utilities."""
2
3 import logging
4 from pathlib import P, Optionalath
5 from typing import Dict
6
7 logger = logging.getLogger(__name__)
8
9
10 def render_mermaid(mermaid_code: str, output_dir: str | Path, name: str) -> Dict[str, Path]:
11 """
12 Render mermaid code to SVG and PNG files.
13
14 Writes {name}.mermaid (source), {name}.svg, and {name}.png.
15 Uses mermaid-py if available, falls back gracefully.
16
17 Returns dict with keys: mermaid, svg, png (Paths to generated files).
18 """
19 output_dir = Path(output_dir)
20 output_dir.mkdir(parents=True, exist_ok=True)
21 result: Dict[str, Path] = {}
22
23 # Always write source
24 mermaid_path = output_dir / f"{name}.mermaid"
25 mermaid_path.write_text(mermaid_code)
26 result["mermaid"] = mermaid_path
27
28 try:
29 import mermaid as mmd
30 from mermaid.graph import Graph
31
32 graph = Graph("diagram", mermaid_code)
33 rendered = mmd.Mermaid(graph)
34
35 # SVG
36 svg_path = output_dir / f"{name}.svg"
37 svg_content = rendered.svg_response
38 if svg_content:
39 if isinstance(svg_content, bytes):
40 svg_path.write_bytes(svg_content)
41 else:
42 svg_path.write_text(svg_content)
43 result["svg"] = svg_path
44
45 # PNG
46 png_path = output_dir / f"{name}.png"
47 png_content = rendered.img_response
48 if png_content:
49 if isinstance(png_content, bytes):
50 png_path.write_bytes(png_content)
51 else:
52 bytes(
53 png_content.encode() if isinstance(png_con)
54 result["png"] = png_path
55
56 except ImportError:
57 logger.warning(ogger.warning(
58 " rendering)
59 except Exception as e:
60 logger.warning(f"Mermaid rendering failed for '{name}': {e}")
61
62 return result
63
64
65 def reproduce_chart(
66 chart_data: dict,
67 output_dir: str | Path,
68 name: str,
69 ) -> Dict[str, Path]:
70 """
71 Reproduce a chart from extracted data using matplotlib.
72
73 chart_data should contain: labels, values, chart_type (bar/line/pie/scatter).
74 Returns dict with keys: svg, png (Paths to generated files).
75 """
76 output_dir = Path(output_dir)
77 output_dir.mkdir(parents=True, exist_ok=True)
78 result: Dict[str, Path] = {}
79
80 labels = chart_data.get("labels", [])
81 values = chart_data.get("values", [])
82 chart_type = chart_data.get("chart_type", "bar")
83
84 if not labels or not values:
85 logger.warning(f"Insufficient chart data for '{name}': missing labels or values")
86 return result
87
88 try:
89 import matplotlib
90
91 matplotlib.use("Agg") # Non-interactive backend
92 import matplotlib.pyplot as plt
93
94 fig, ax = plt.subplots(figsize=(10, 6))
95
96 if chart_type == "bar":
97 ax.bar(labels, values)
98 elif chart_type == "line":
99 ax.plot(labels, values, marker="o")
100 elif chart_type == "pie":
101 ax.pie(values, labels=labels, autopct="%1.1f%%")
102 elif chart_type == "scatter":
103 ax.scatter(range(len(values)), values)
104 if labels:
105 ax.set_xticks(range(len(labels)))
106 ax.set_xticklabels(labels, rotation=45, ha="right")
107 else:
108 ax.bar(labels, values)
109
110 if chart_type != "pie":
111 ax.set_xlabel("")
112 ax.set_ylabel("")
113 plt.xticks(rotation=45, ha="right")
114
115 plt.tight_layout()
116
117 # SVG
118 svg_path = output_dir / f"{name}_chart.svg"
119 fig.savefig(svg_path, format="svg")
120 result["svg"] = svg_path
121
122 # PNG
123 png_path = output_dir / f"{name}_chart.png"
124 fig.savefig(png_path, format="png", dpi=150)
125 result["png"] = png_path
126
127 plt.close(fig)
128
129 except ImportError:
130 logger.warning("matplotlib not installed, skipping chart reproduction")
131 except Exception as e:
132 logger.warning(f"Chart reproduction failed for '{name}': {e}")
133
134 return result

Keyboard Shortcuts

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