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