PlanOpticon

Blame History Raw 200 lines
1
"""Multi-format output orchestration."""
2
3
import logging
4
from pathlib import Path
5
from typing import Optional
6
7
from tqdm import tqdm
8
9
from video_processor.models import VideoManifest
10
from video_processor.utils.rendering import render_mermaid, reproduce_chart
11
12
logger = logging.getLogger(__name__)
13
14
15
def generate_html_report(
16
manifest: VideoManifest,
17
output_dir: Path,
18
) -> Optional[Path]:
19
"""
20
Generate a self-contained HTML report with embedded diagrams.
21
22
Reads the markdown analysis and enriches it with rendered SVGs
23
and mermaid.js for any unrendered blocks.
24
"""
25
output_dir = Path(output_dir)
26
results_dir = output_dir / "results"
27
results_dir.mkdir(parents=True, exist_ok=True)
28
29
# Read markdown if available
30
md_content = ""
31
if manifest.analysis_md:
32
md_path = output_dir / manifest.analysis_md
33
if md_path.exists():
34
md_content = md_path.read_text()
35
36
# Convert markdown to HTML
37
try:
38
import markdown
39
40
html_body = markdown.markdown(
41
md_content,
42
extensions=["fenced_code", "tables", "toc"],
43
)
44
except ImportError:
45
logger.warning("markdown library not available, using raw text")
46
html_body = f"<pre>{md_content}</pre>"
47
48
# Build sections for key points, action items
49
sections = []
50
51
if manifest.key_points:
52
kp_html = "<h2>Key Points</h2><ul>"
53
for kp in manifest.key_points:
54
kp_html += f"<li><strong>{kp.point}</strong>"
55
if kp.details:
56
kp_html += f" - {kp.details}"
57
kp_html += "</li>"
58
kp_html += "</ul>"
59
sections.append(kp_html)
60
61
if manifest.action_items:
62
ai_html = "<h2>Action Items</h2><ul>"
63
for ai in manifest.action_items:
64
ai_html += f"<li><strong>{ai.action}</strong>"
65
if ai.assignee:
66
ai_html += f" (assigned to: {ai.assignee})"
67
if ai.deadline:
68
ai_html += f" — due: {ai.deadline}"
69
ai_html += "</li>"
70
ai_html += "</ul>"
71
sections.append(ai_html)
72
73
# Embed diagram SVGs
74
if manifest.diagrams:
75
diag_html = "<h2>Diagrams</h2>"
76
for i, d in enumerate(manifest.diagrams):
77
diag_html += f"<h3>Diagram {i + 1}: {d.description or d.diagram_type.value}</h3>"
78
svg_path = output_dir / d.svg_path if d.svg_path else None
79
if svg_path and svg_path.exists():
80
svg_content = svg_path.read_text()
81
diag_html += f'<div class="diagram">{svg_content}</div>'
82
elif d.image_path:
83
diag_html += (
84
f'<img src="{d.image_path}" alt="Diagram {i + 1}" style="max-width:100%">'
85
)
86
if d.mermaid:
87
diag_html += f'<pre class="mermaid">{d.mermaid}</pre>'
88
sections.append(diag_html)
89
90
title = manifest.video.title or "PlanOpticon Analysis"
91
full_html = f"""<!DOCTYPE html>
92
<html lang="en">
93
<head>
94
<meta charset="utf-8">
95
<title>{title}</title>
96
<style>
97
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
98
max-width: 960px; margin: 0 auto; padding: 20px; line-height: 1.6; color: #333; }}
99
h1 {{ color: #1a1a2e; border-bottom: 2px solid #e0e0e0; padding-bottom: 10px; }}
100
h2 {{ color: #16213e; margin-top: 2em; }}
101
h3 {{ color: #0f3460; }}
102
pre {{ background: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; }}
103
code {{ background: #f5f5f5; padding: 2px 6px; border-radius: 3px; }}
104
table {{ border-collapse: collapse; width: 100%; margin: 1em 0; }}
105
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
106
th {{ background: #f0f0f0; }}
107
.diagram {{ margin: 1em 0; text-align: center; }}
108
.diagram svg {{ max-width: 100%; height: auto; }}
109
img {{ max-width: 100%; height: auto; }}
110
ul {{ padding-left: 1.5em; }}
111
li {{ margin: 0.3em 0; }}
112
</style>
113
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
114
<script>mermaid.initialize({{startOnLoad: true}});</script>
115
</head>
116
<body>
117
<h1>{title}</h1>
118
{html_body}
119
{"".join(sections)}
120
</body>
121
</html>"""
122
123
html_path = results_dir / "analysis.html"
124
html_path.write_text(full_html)
125
logger.info(f"Generated HTML report: {html_path}")
126
return html_path
127
128
129
def generate_pdf_report(html_path: Path, output_path: Path) -> Optional[Path]:
130
"""
131
Convert HTML report to PDF using weasyprint.
132
133
Returns the PDF path or None if weasyprint is not available.
134
"""
135
try:
136
from weasyprint import HTML
137
138
HTML(filename=str(html_path)).write_pdf(str(output_path))
139
logger.info(f"Generated PDF report: {output_path}")
140
return output_path
141
except ImportError:
142
logger.info("weasyprint not installed, skipping PDF generation")
143
return None
144
except Exception as e:
145
logger.warning(f"PDF generation failed: {e}")
146
return None
147
148
149
def export_all_formats(
150
output_dir: str | Path,
151
manifest: VideoManifest,
152
) -> VideoManifest:
153
"""
154
Render all diagrams and generate HTML/PDF reports.
155
156
Updates manifest with output file paths and returns it.
157
"""
158
output_dir = Path(output_dir)
159
160
# Render mermaid diagrams to SVG/PNG
161
for i, diagram in enumerate(
162
tqdm(manifest.diagrams, desc="Rendering diagrams", unit="diag") if manifest.diagrams else []
163
):
164
if diagram.mermaid:
165
diagrams_dir = output_dir / "diagrams"
166
prefix = f"diagram_{i}"
167
paths = render_mermaid(diagram.mermaid, diagrams_dir, prefix)
168
if "svg" in paths:
169
diagram.svg_path = f"diagrams/{prefix}.svg"
170
if "png" in paths:
171
diagram.png_path = f"diagrams/{prefix}.png"
172
if "mermaid" in paths and not diagram.mermaid_path:
173
diagram.mermaid_path = f"diagrams/{prefix}.mermaid"
174
175
# Reproduce charts
176
if diagram.chart_data and diagram.diagram_type.value == "chart":
177
chart_paths = reproduce_chart(
178
diagram.chart_data,
179
output_dir / "diagrams",
180
f"diagram_{i}",
181
)
182
if "svg" in chart_paths:
183
diagram.svg_path = f"diagrams/diagram_{i}_chart.svg"
184
if "png" in chart_paths:
185
diagram.png_path = f"diagrams/diagram_{i}_chart.png"
186
187
# Generate HTML report
188
html_path = generate_html_report(manifest, output_dir)
189
if html_path:
190
manifest.analysis_html = str(html_path.relative_to(output_dir))
191
192
# Generate PDF from HTML
193
if html_path:
194
pdf_path = output_dir / "results" / "analysis.pdf"
195
result = generate_pdf_report(html_path, pdf_path)
196
if result:
197
manifest.analysis_pdf = str(pdf_path.relative_to(output_dir))
198
199
return manifest
200

Keyboard Shortcuts

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