PlanOpticon

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

Keyboard Shortcuts

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