PlanOpticon

planopticon / video_processor / cli / output_formatter.py
Blame History Raw 253 lines
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

Keyboard Shortcuts

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