|
287a3bb…
|
leo
|
1 |
"""Output formatting for PlanOpticon analysis results.""" |
|
287a3bb…
|
leo
|
2 |
|
|
287a3bb…
|
leo
|
3 |
import html |
|
287a3bb…
|
leo
|
4 |
import logging |
|
287a3bb…
|
leo
|
5 |
import shutil |
|
287a3bb…
|
leo
|
6 |
from pathlib import Path |
|
287a3bb…
|
leo
|
7 |
from typing import Dict, List, Optional, Union |
|
287a3bb…
|
leo
|
8 |
|
|
287a3bb…
|
leo
|
9 |
logger = logging.getLogger(__name__) |
|
287a3bb…
|
leo
|
10 |
|
|
829e24a…
|
leo
|
11 |
|
|
287a3bb…
|
leo
|
12 |
class OutputFormatter: |
|
287a3bb…
|
leo
|
13 |
"""Formats and organizes output from video analysis.""" |
|
829e24a…
|
leo
|
14 |
|
|
287a3bb…
|
leo
|
15 |
def __init__(self, output_dir: Union[str, Path]): |
|
287a3bb…
|
leo
|
16 |
""" |
|
287a3bb…
|
leo
|
17 |
Initialize output formatter. |
|
829e24a…
|
leo
|
18 |
|
|
287a3bb…
|
leo
|
19 |
Parameters |
|
287a3bb…
|
leo
|
20 |
---------- |
|
287a3bb…
|
leo
|
21 |
output_dir : str or Path |
|
287a3bb…
|
leo
|
22 |
Output directory for formatted content |
|
287a3bb…
|
leo
|
23 |
""" |
|
287a3bb…
|
leo
|
24 |
self.output_dir = Path(output_dir) |
|
287a3bb…
|
leo
|
25 |
self.output_dir.mkdir(parents=True, exist_ok=True) |
|
829e24a…
|
leo
|
26 |
|
|
287a3bb…
|
leo
|
27 |
def organize_outputs( |
|
287a3bb…
|
leo
|
28 |
self, |
|
287a3bb…
|
leo
|
29 |
markdown_path: Union[str, Path], |
|
287a3bb…
|
leo
|
30 |
knowledge_graph_path: Union[str, Path], |
|
287a3bb…
|
leo
|
31 |
diagrams: List[Dict], |
|
287a3bb…
|
leo
|
32 |
frames_dir: Optional[Union[str, Path]] = None, |
|
829e24a…
|
leo
|
33 |
transcript_path: Optional[Union[str, Path]] = None, |
|
287a3bb…
|
leo
|
34 |
) -> Dict: |
|
287a3bb…
|
leo
|
35 |
""" |
|
287a3bb…
|
leo
|
36 |
Organize outputs into a consistent structure. |
|
829e24a…
|
leo
|
37 |
|
|
287a3bb…
|
leo
|
38 |
Parameters |
|
287a3bb…
|
leo
|
39 |
---------- |
|
287a3bb…
|
leo
|
40 |
markdown_path : str or Path |
|
287a3bb…
|
leo
|
41 |
Path to markdown analysis |
|
287a3bb…
|
leo
|
42 |
knowledge_graph_path : str or Path |
|
287a3bb…
|
leo
|
43 |
Path to knowledge graph JSON |
|
287a3bb…
|
leo
|
44 |
diagrams : list |
|
287a3bb…
|
leo
|
45 |
List of diagram analysis results |
|
287a3bb…
|
leo
|
46 |
frames_dir : str or Path, optional |
|
287a3bb…
|
leo
|
47 |
Directory with extracted frames |
|
287a3bb…
|
leo
|
48 |
transcript_path : str or Path, optional |
|
287a3bb…
|
leo
|
49 |
Path to transcript file |
|
829e24a…
|
leo
|
50 |
|
|
287a3bb…
|
leo
|
51 |
Returns |
|
287a3bb…
|
leo
|
52 |
------- |
|
287a3bb…
|
leo
|
53 |
dict |
|
287a3bb…
|
leo
|
54 |
Dictionary with organized output paths |
|
287a3bb…
|
leo
|
55 |
""" |
|
287a3bb…
|
leo
|
56 |
# Create output structure |
|
287a3bb…
|
leo
|
57 |
md_dir = self.output_dir / "markdown" |
|
287a3bb…
|
leo
|
58 |
diagrams_dir = self.output_dir / "diagrams" |
|
287a3bb…
|
leo
|
59 |
data_dir = self.output_dir / "data" |
|
829e24a…
|
leo
|
60 |
|
|
287a3bb…
|
leo
|
61 |
md_dir.mkdir(exist_ok=True) |
|
287a3bb…
|
leo
|
62 |
diagrams_dir.mkdir(exist_ok=True) |
|
287a3bb…
|
leo
|
63 |
data_dir.mkdir(exist_ok=True) |
|
829e24a…
|
leo
|
64 |
|
|
287a3bb…
|
leo
|
65 |
# Copy markdown file |
|
287a3bb…
|
leo
|
66 |
markdown_path = Path(markdown_path) |
|
287a3bb…
|
leo
|
67 |
md_output = md_dir / markdown_path.name |
|
287a3bb…
|
leo
|
68 |
shutil.copy2(markdown_path, md_output) |
|
829e24a…
|
leo
|
69 |
|
|
287a3bb…
|
leo
|
70 |
# Copy knowledge graph |
|
287a3bb…
|
leo
|
71 |
kg_path = Path(knowledge_graph_path) |
|
287a3bb…
|
leo
|
72 |
kg_output = data_dir / kg_path.name |
|
287a3bb…
|
leo
|
73 |
shutil.copy2(kg_path, kg_output) |
|
829e24a…
|
leo
|
74 |
|
|
287a3bb…
|
leo
|
75 |
# Copy diagram images if available |
|
287a3bb…
|
leo
|
76 |
diagram_images = [] |
|
287a3bb…
|
leo
|
77 |
for diagram in diagrams: |
|
287a3bb…
|
leo
|
78 |
if "image_path" in diagram and diagram["image_path"]: |
|
287a3bb…
|
leo
|
79 |
img_path = Path(diagram["image_path"]) |
|
287a3bb…
|
leo
|
80 |
if img_path.exists(): |
|
287a3bb…
|
leo
|
81 |
img_output = diagrams_dir / img_path.name |
|
287a3bb…
|
leo
|
82 |
shutil.copy2(img_path, img_output) |
|
287a3bb…
|
leo
|
83 |
diagram_images.append(str(img_output)) |
|
829e24a…
|
leo
|
84 |
|
|
287a3bb…
|
leo
|
85 |
# Copy transcript if provided |
|
287a3bb…
|
leo
|
86 |
transcript_output = None |
|
287a3bb…
|
leo
|
87 |
if transcript_path: |
|
287a3bb…
|
leo
|
88 |
transcript_path = Path(transcript_path) |
|
287a3bb…
|
leo
|
89 |
if transcript_path.exists(): |
|
287a3bb…
|
leo
|
90 |
transcript_output = data_dir / transcript_path.name |
|
287a3bb…
|
leo
|
91 |
shutil.copy2(transcript_path, transcript_output) |
|
829e24a…
|
leo
|
92 |
|
|
287a3bb…
|
leo
|
93 |
# Copy selected frames if provided |
|
287a3bb…
|
leo
|
94 |
frame_outputs = [] |
|
287a3bb…
|
leo
|
95 |
if frames_dir: |
|
287a3bb…
|
leo
|
96 |
frames_dir = Path(frames_dir) |
|
287a3bb…
|
leo
|
97 |
if frames_dir.exists(): |
|
287a3bb…
|
leo
|
98 |
frames_output_dir = self.output_dir / "frames" |
|
287a3bb…
|
leo
|
99 |
frames_output_dir.mkdir(exist_ok=True) |
|
829e24a…
|
leo
|
100 |
|
|
287a3bb…
|
leo
|
101 |
# Copy a limited number of representative frames |
|
287a3bb…
|
leo
|
102 |
frame_files = sorted(list(frames_dir.glob("*.jpg"))) |
|
287a3bb…
|
leo
|
103 |
max_frames = min(10, len(frame_files)) |
|
287a3bb…
|
leo
|
104 |
step = max(1, len(frame_files) // max_frames) |
|
829e24a…
|
leo
|
105 |
|
|
287a3bb…
|
leo
|
106 |
for i in range(0, len(frame_files), step): |
|
287a3bb…
|
leo
|
107 |
if len(frame_outputs) >= max_frames: |
|
287a3bb…
|
leo
|
108 |
break |
|
829e24a…
|
leo
|
109 |
|
|
287a3bb…
|
leo
|
110 |
frame = frame_files[i] |
|
287a3bb…
|
leo
|
111 |
frame_output = frames_output_dir / frame.name |
|
287a3bb…
|
leo
|
112 |
shutil.copy2(frame, frame_output) |
|
287a3bb…
|
leo
|
113 |
frame_outputs.append(str(frame_output)) |
|
829e24a…
|
leo
|
114 |
|
|
287a3bb…
|
leo
|
115 |
# Return organized paths |
|
287a3bb…
|
leo
|
116 |
return { |
|
287a3bb…
|
leo
|
117 |
"markdown": str(md_output), |
|
287a3bb…
|
leo
|
118 |
"knowledge_graph": str(kg_output), |
|
287a3bb…
|
leo
|
119 |
"diagram_images": diagram_images, |
|
287a3bb…
|
leo
|
120 |
"frames": frame_outputs, |
|
829e24a…
|
leo
|
121 |
"transcript": str(transcript_output) if transcript_output else None, |
|
287a3bb…
|
leo
|
122 |
} |
|
829e24a…
|
leo
|
123 |
|
|
287a3bb…
|
leo
|
124 |
def create_html_index(self, outputs: Dict) -> Path: |
|
287a3bb…
|
leo
|
125 |
""" |
|
287a3bb…
|
leo
|
126 |
Create HTML index page for outputs. |
|
287a3bb…
|
leo
|
127 |
|
|
287a3bb…
|
leo
|
128 |
Parameters |
|
287a3bb…
|
leo
|
129 |
---------- |
|
287a3bb…
|
leo
|
130 |
outputs : dict |
|
287a3bb…
|
leo
|
131 |
Dictionary with organized output paths |
|
287a3bb…
|
leo
|
132 |
|
|
287a3bb…
|
leo
|
133 |
Returns |
|
287a3bb…
|
leo
|
134 |
------- |
|
287a3bb…
|
leo
|
135 |
Path |
|
287a3bb…
|
leo
|
136 |
Path to HTML index |
|
287a3bb…
|
leo
|
137 |
""" |
|
287a3bb…
|
leo
|
138 |
esc = html.escape |
|
287a3bb…
|
leo
|
139 |
|
|
287a3bb…
|
leo
|
140 |
# Simple HTML index template |
|
287a3bb…
|
leo
|
141 |
lines = [ |
|
287a3bb…
|
leo
|
142 |
"<!DOCTYPE html>", |
|
287a3bb…
|
leo
|
143 |
"<html>", |
|
287a3bb…
|
leo
|
144 |
"<head>", |
|
287a3bb…
|
leo
|
145 |
" <title>PlanOpticon Analysis Results</title>", |
|
287a3bb…
|
leo
|
146 |
" <style>", |
|
829e24a…
|
leo
|
147 |
" body { font-family: Arial, sans-serif;" |
|
829e24a…
|
leo
|
148 |
" margin: 0; padding: 20px; line-height: 1.6; }", |
|
287a3bb…
|
leo
|
149 |
" .container { max-width: 1200px; margin: 0 auto; }", |
|
287a3bb…
|
leo
|
150 |
" h1 { color: #333; }", |
|
287a3bb…
|
leo
|
151 |
" h2 { color: #555; margin-top: 30px; }", |
|
287a3bb…
|
leo
|
152 |
" .section { margin-bottom: 30px; }", |
|
287a3bb…
|
leo
|
153 |
" .files { display: flex; flex-wrap: wrap; }", |
|
287a3bb…
|
leo
|
154 |
" .file-item { margin: 10px; text-align: center; }", |
|
287a3bb…
|
leo
|
155 |
" .file-item img { max-width: 200px; max-height: 150px; object-fit: contain; }", |
|
287a3bb…
|
leo
|
156 |
" .file-name { margin-top: 5px; font-size: 0.9em; }", |
|
287a3bb…
|
leo
|
157 |
" a { color: #0066cc; text-decoration: none; }", |
|
287a3bb…
|
leo
|
158 |
" a:hover { text-decoration: underline; }", |
|
287a3bb…
|
leo
|
159 |
" </style>", |
|
287a3bb…
|
leo
|
160 |
"</head>", |
|
287a3bb…
|
leo
|
161 |
"<body>", |
|
287a3bb…
|
leo
|
162 |
"<div class='container'>", |
|
287a3bb…
|
leo
|
163 |
" <h1>PlanOpticon Analysis Results</h1>", |
|
829e24a…
|
leo
|
164 |
"", |
|
287a3bb…
|
leo
|
165 |
] |
|
287a3bb…
|
leo
|
166 |
|
|
287a3bb…
|
leo
|
167 |
# Add markdown section |
|
287a3bb…
|
leo
|
168 |
if outputs.get("markdown"): |
|
287a3bb…
|
leo
|
169 |
md_path = Path(outputs["markdown"]) |
|
287a3bb…
|
leo
|
170 |
md_rel = esc(str(md_path.relative_to(self.output_dir))) |
|
287a3bb…
|
leo
|
171 |
|
|
287a3bb…
|
leo
|
172 |
lines.append(" <div class='section'>") |
|
287a3bb…
|
leo
|
173 |
lines.append(" <h2>Analysis Report</h2>") |
|
287a3bb…
|
leo
|
174 |
lines.append(f" <p><a href='{md_rel}' target='_blank'>View Analysis</a></p>") |
|
287a3bb…
|
leo
|
175 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
176 |
|
|
287a3bb…
|
leo
|
177 |
# Add diagrams section |
|
287a3bb…
|
leo
|
178 |
if outputs.get("diagram_images") and len(outputs["diagram_images"]) > 0: |
|
287a3bb…
|
leo
|
179 |
lines.append(" <div class='section'>") |
|
287a3bb…
|
leo
|
180 |
lines.append(" <h2>Diagrams</h2>") |
|
287a3bb…
|
leo
|
181 |
lines.append(" <div class='files'>") |
|
287a3bb…
|
leo
|
182 |
|
|
287a3bb…
|
leo
|
183 |
for img_path in outputs["diagram_images"]: |
|
287a3bb…
|
leo
|
184 |
img_path = Path(img_path) |
|
287a3bb…
|
leo
|
185 |
img_rel = esc(str(img_path.relative_to(self.output_dir))) |
|
287a3bb…
|
leo
|
186 |
img_name = esc(img_path.name) |
|
287a3bb…
|
leo
|
187 |
|
|
287a3bb…
|
leo
|
188 |
lines.append(" <div class='file-item'>") |
|
287a3bb…
|
leo
|
189 |
lines.append(f" <a href='{img_rel}' target='_blank'>") |
|
287a3bb…
|
leo
|
190 |
lines.append(f" <img src='{img_rel}' alt='Diagram'>") |
|
287a3bb…
|
leo
|
191 |
lines.append(" </a>") |
|
287a3bb…
|
leo
|
192 |
lines.append(f" <div class='file-name'>{img_name}</div>") |
|
287a3bb…
|
leo
|
193 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
194 |
|
|
287a3bb…
|
leo
|
195 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
196 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
197 |
|
|
287a3bb…
|
leo
|
198 |
# Add frames section |
|
287a3bb…
|
leo
|
199 |
if outputs.get("frames") and len(outputs["frames"]) > 0: |
|
287a3bb…
|
leo
|
200 |
lines.append(" <div class='section'>") |
|
287a3bb…
|
leo
|
201 |
lines.append(" <h2>Key Frames</h2>") |
|
287a3bb…
|
leo
|
202 |
lines.append(" <div class='files'>") |
|
287a3bb…
|
leo
|
203 |
|
|
287a3bb…
|
leo
|
204 |
for frame_path in outputs["frames"]: |
|
287a3bb…
|
leo
|
205 |
frame_path = Path(frame_path) |
|
287a3bb…
|
leo
|
206 |
frame_rel = esc(str(frame_path.relative_to(self.output_dir))) |
|
287a3bb…
|
leo
|
207 |
frame_name = esc(frame_path.name) |
|
287a3bb…
|
leo
|
208 |
|
|
287a3bb…
|
leo
|
209 |
lines.append(" <div class='file-item'>") |
|
287a3bb…
|
leo
|
210 |
lines.append(f" <a href='{frame_rel}' target='_blank'>") |
|
287a3bb…
|
leo
|
211 |
lines.append(f" <img src='{frame_rel}' alt='Frame'>") |
|
287a3bb…
|
leo
|
212 |
lines.append(" </a>") |
|
287a3bb…
|
leo
|
213 |
lines.append(f" <div class='file-name'>{frame_name}</div>") |
|
287a3bb…
|
leo
|
214 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
215 |
|
|
287a3bb…
|
leo
|
216 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
217 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
218 |
|
|
287a3bb…
|
leo
|
219 |
# Add data files section |
|
287a3bb…
|
leo
|
220 |
data_files = [] |
|
287a3bb…
|
leo
|
221 |
if outputs.get("knowledge_graph"): |
|
287a3bb…
|
leo
|
222 |
data_files.append(Path(outputs["knowledge_graph"])) |
|
287a3bb…
|
leo
|
223 |
if outputs.get("transcript"): |
|
287a3bb…
|
leo
|
224 |
data_files.append(Path(outputs["transcript"])) |
|
287a3bb…
|
leo
|
225 |
|
|
287a3bb…
|
leo
|
226 |
if data_files: |
|
287a3bb…
|
leo
|
227 |
lines.append(" <div class='section'>") |
|
287a3bb…
|
leo
|
228 |
lines.append(" <h2>Data Files</h2>") |
|
287a3bb…
|
leo
|
229 |
lines.append(" <ul>") |
|
287a3bb…
|
leo
|
230 |
|
|
287a3bb…
|
leo
|
231 |
for data_path in data_files: |
|
287a3bb…
|
leo
|
232 |
data_rel = esc(str(data_path.relative_to(self.output_dir))) |
|
287a3bb…
|
leo
|
233 |
data_name = esc(data_path.name) |
|
829e24a…
|
leo
|
234 |
lines.append( |
|
829e24a…
|
leo
|
235 |
f" <li><a href='{data_rel}' target='_blank'>{data_name}</a></li>" |
|
829e24a…
|
leo
|
236 |
) |
|
287a3bb…
|
leo
|
237 |
|
|
287a3bb…
|
leo
|
238 |
lines.append(" </ul>") |
|
287a3bb…
|
leo
|
239 |
lines.append(" </div>") |
|
287a3bb…
|
leo
|
240 |
|
|
287a3bb…
|
leo
|
241 |
# Close HTML |
|
287a3bb…
|
leo
|
242 |
lines.append("</div>") |
|
287a3bb…
|
leo
|
243 |
lines.append("</body>") |
|
287a3bb…
|
leo
|
244 |
lines.append("</html>") |
|
287a3bb…
|
leo
|
245 |
|
|
287a3bb…
|
leo
|
246 |
# Write HTML file |
|
287a3bb…
|
leo
|
247 |
index_path = self.output_dir / "index.html" |
|
287a3bb…
|
leo
|
248 |
with open(index_path, "w") as f: |
|
287a3bb…
|
leo
|
249 |
f.write("\n".join(lines)) |
|
287a3bb…
|
leo
|
250 |
|
|
287a3bb…
|
leo
|
251 |
logger.info(f"Created HTML index at {index_path}") |
|
287a3bb…
|
leo
|
252 |
return index_path |