|
ccf32cc…
|
leo
|
1 |
"""Tests for the core video processing pipeline.""" |
|
ccf32cc…
|
leo
|
2 |
|
|
ccf32cc…
|
leo
|
3 |
import json |
|
0981a08…
|
noreply
|
4 |
from pathlib import Path |
|
0981a08…
|
noreply
|
5 |
from unittest.mock import MagicMock, patch |
|
ccf32cc…
|
leo
|
6 |
|
|
0981a08…
|
noreply
|
7 |
import pytest |
|
0981a08…
|
noreply
|
8 |
|
|
0981a08…
|
noreply
|
9 |
from video_processor.pipeline import ( |
|
0981a08…
|
noreply
|
10 |
_extract_action_items, |
|
0981a08…
|
noreply
|
11 |
_extract_key_points, |
|
0981a08…
|
noreply
|
12 |
_format_srt_time, |
|
0981a08…
|
noreply
|
13 |
process_single_video, |
|
0981a08…
|
noreply
|
14 |
) |
|
ccf32cc…
|
leo
|
15 |
|
|
ccf32cc…
|
leo
|
16 |
|
|
ccf32cc…
|
leo
|
17 |
class TestFormatSrtTime: |
|
ccf32cc…
|
leo
|
18 |
def test_zero(self): |
|
ccf32cc…
|
leo
|
19 |
assert _format_srt_time(0) == "00:00:00,000" |
|
ccf32cc…
|
leo
|
20 |
|
|
ccf32cc…
|
leo
|
21 |
def test_seconds(self): |
|
ccf32cc…
|
leo
|
22 |
assert _format_srt_time(5.5) == "00:00:05,500" |
|
ccf32cc…
|
leo
|
23 |
|
|
ccf32cc…
|
leo
|
24 |
def test_minutes(self): |
|
ccf32cc…
|
leo
|
25 |
assert _format_srt_time(90.0) == "00:01:30,000" |
|
ccf32cc…
|
leo
|
26 |
|
|
ccf32cc…
|
leo
|
27 |
def test_hours(self): |
|
ccf32cc…
|
leo
|
28 |
assert _format_srt_time(3661.123) == "01:01:01,123" |
|
ccf32cc…
|
leo
|
29 |
|
|
ccf32cc…
|
leo
|
30 |
def test_large_value(self): |
|
ccf32cc…
|
leo
|
31 |
result = _format_srt_time(7200.0) |
|
ccf32cc…
|
leo
|
32 |
assert result == "02:00:00,000" |
|
ccf32cc…
|
leo
|
33 |
|
|
ccf32cc…
|
leo
|
34 |
|
|
ccf32cc…
|
leo
|
35 |
class TestExtractKeyPoints: |
|
ccf32cc…
|
leo
|
36 |
def test_parses_valid_response(self): |
|
ccf32cc…
|
leo
|
37 |
pm = MagicMock() |
|
829e24a…
|
leo
|
38 |
pm.chat.return_value = json.dumps( |
|
829e24a…
|
leo
|
39 |
[ |
|
829e24a…
|
leo
|
40 |
{"point": "Main point", "topic": "Architecture", "details": "Some details"}, |
|
829e24a…
|
leo
|
41 |
{"point": "Second point", "topic": None, "details": None}, |
|
829e24a…
|
leo
|
42 |
] |
|
829e24a…
|
leo
|
43 |
) |
|
ccf32cc…
|
leo
|
44 |
result = _extract_key_points(pm, "Some transcript text here") |
|
ccf32cc…
|
leo
|
45 |
assert len(result) == 2 |
|
ccf32cc…
|
leo
|
46 |
assert result[0].point == "Main point" |
|
ccf32cc…
|
leo
|
47 |
assert result[0].topic == "Architecture" |
|
ccf32cc…
|
leo
|
48 |
assert result[1].point == "Second point" |
|
ccf32cc…
|
leo
|
49 |
|
|
ccf32cc…
|
leo
|
50 |
def test_skips_invalid_items(self): |
|
ccf32cc…
|
leo
|
51 |
pm = MagicMock() |
|
829e24a…
|
leo
|
52 |
pm.chat.return_value = json.dumps( |
|
829e24a…
|
leo
|
53 |
[ |
|
829e24a…
|
leo
|
54 |
{"point": "Valid", "topic": None}, |
|
829e24a…
|
leo
|
55 |
{"topic": "No point field"}, |
|
829e24a…
|
leo
|
56 |
{"point": "", "topic": "Empty point"}, |
|
829e24a…
|
leo
|
57 |
] |
|
829e24a…
|
leo
|
58 |
) |
|
ccf32cc…
|
leo
|
59 |
result = _extract_key_points(pm, "text") |
|
ccf32cc…
|
leo
|
60 |
assert len(result) == 1 |
|
ccf32cc…
|
leo
|
61 |
assert result[0].point == "Valid" |
|
ccf32cc…
|
leo
|
62 |
|
|
ccf32cc…
|
leo
|
63 |
def test_handles_error(self): |
|
ccf32cc…
|
leo
|
64 |
pm = MagicMock() |
|
ccf32cc…
|
leo
|
65 |
pm.chat.side_effect = Exception("API error") |
|
ccf32cc…
|
leo
|
66 |
result = _extract_key_points(pm, "text") |
|
ccf32cc…
|
leo
|
67 |
assert result == [] |
|
ccf32cc…
|
leo
|
68 |
|
|
ccf32cc…
|
leo
|
69 |
def test_handles_non_list_response(self): |
|
ccf32cc…
|
leo
|
70 |
pm = MagicMock() |
|
ccf32cc…
|
leo
|
71 |
pm.chat.return_value = '{"not": "a list"}' |
|
ccf32cc…
|
leo
|
72 |
result = _extract_key_points(pm, "text") |
|
ccf32cc…
|
leo
|
73 |
assert result == [] |
|
ccf32cc…
|
leo
|
74 |
|
|
ccf32cc…
|
leo
|
75 |
|
|
ccf32cc…
|
leo
|
76 |
class TestExtractActionItems: |
|
ccf32cc…
|
leo
|
77 |
def test_parses_valid_response(self): |
|
ccf32cc…
|
leo
|
78 |
pm = MagicMock() |
|
829e24a…
|
leo
|
79 |
pm.chat.return_value = json.dumps( |
|
829e24a…
|
leo
|
80 |
[ |
|
829e24a…
|
leo
|
81 |
{ |
|
829e24a…
|
leo
|
82 |
"action": "Deploy fix", |
|
829e24a…
|
leo
|
83 |
"assignee": "Bob", |
|
829e24a…
|
leo
|
84 |
"deadline": "Friday", |
|
829e24a…
|
leo
|
85 |
"priority": "high", |
|
829e24a…
|
leo
|
86 |
"context": "Production", |
|
829e24a…
|
leo
|
87 |
}, |
|
829e24a…
|
leo
|
88 |
] |
|
829e24a…
|
leo
|
89 |
) |
|
ccf32cc…
|
leo
|
90 |
result = _extract_action_items(pm, "Some transcript text") |
|
ccf32cc…
|
leo
|
91 |
assert len(result) == 1 |
|
ccf32cc…
|
leo
|
92 |
assert result[0].action == "Deploy fix" |
|
ccf32cc…
|
leo
|
93 |
assert result[0].assignee == "Bob" |
|
ccf32cc…
|
leo
|
94 |
|
|
ccf32cc…
|
leo
|
95 |
def test_skips_invalid_items(self): |
|
ccf32cc…
|
leo
|
96 |
pm = MagicMock() |
|
829e24a…
|
leo
|
97 |
pm.chat.return_value = json.dumps( |
|
829e24a…
|
leo
|
98 |
[ |
|
829e24a…
|
leo
|
99 |
{"action": "Valid action"}, |
|
829e24a…
|
leo
|
100 |
{"assignee": "No action field"}, |
|
829e24a…
|
leo
|
101 |
{"action": ""}, |
|
829e24a…
|
leo
|
102 |
] |
|
829e24a…
|
leo
|
103 |
) |
|
ccf32cc…
|
leo
|
104 |
result = _extract_action_items(pm, "text") |
|
ccf32cc…
|
leo
|
105 |
assert len(result) == 1 |
|
ccf32cc…
|
leo
|
106 |
|
|
ccf32cc…
|
leo
|
107 |
def test_handles_error(self): |
|
ccf32cc…
|
leo
|
108 |
pm = MagicMock() |
|
ccf32cc…
|
leo
|
109 |
pm.chat.side_effect = Exception("API down") |
|
ccf32cc…
|
leo
|
110 |
result = _extract_action_items(pm, "text") |
|
ccf32cc…
|
leo
|
111 |
assert result == [] |
|
0981a08…
|
noreply
|
112 |
|
|
0981a08…
|
noreply
|
113 |
|
|
0981a08…
|
noreply
|
114 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
115 |
# process_single_video tests (heavily mocked) |
|
0981a08…
|
noreply
|
116 |
# --------------------------------------------------------------------------- |
|
0981a08…
|
noreply
|
117 |
|
|
0981a08…
|
noreply
|
118 |
|
|
0981a08…
|
noreply
|
119 |
def _make_mock_pm(): |
|
0981a08…
|
noreply
|
120 |
"""Build a mock ProviderManager with usage tracker and predictable responses.""" |
|
0981a08…
|
noreply
|
121 |
pm = MagicMock() |
|
0981a08…
|
noreply
|
122 |
|
|
0981a08…
|
noreply
|
123 |
# Usage tracker stub |
|
0981a08…
|
noreply
|
124 |
pm.usage = MagicMock() |
|
0981a08…
|
noreply
|
125 |
pm.usage.start_step = MagicMock() |
|
0981a08…
|
noreply
|
126 |
pm.usage.end_step = MagicMock() |
|
0981a08…
|
noreply
|
127 |
|
|
0981a08…
|
noreply
|
128 |
# transcribe_audio returns a simple transcript |
|
0981a08…
|
noreply
|
129 |
pm.transcribe_audio.return_value = { |
|
0981a08…
|
noreply
|
130 |
"text": "Alice discussed the Python deployment strategy with Bob.", |
|
0981a08…
|
noreply
|
131 |
"segments": [ |
|
0981a08…
|
noreply
|
132 |
{"start": 0.0, "end": 5.0, "text": "Alice discussed the Python deployment strategy."}, |
|
0981a08…
|
noreply
|
133 |
{"start": 5.0, "end": 10.0, "text": "Bob agreed on the timeline."}, |
|
0981a08…
|
noreply
|
134 |
], |
|
0981a08…
|
noreply
|
135 |
"duration": 10.0, |
|
0981a08…
|
noreply
|
136 |
"language": "en", |
|
0981a08…
|
noreply
|
137 |
"provider": "mock", |
|
0981a08…
|
noreply
|
138 |
"model": "mock-whisper", |
|
0981a08…
|
noreply
|
139 |
} |
|
0981a08…
|
noreply
|
140 |
|
|
0981a08…
|
noreply
|
141 |
# chat returns predictable JSON depending on the call |
|
0981a08…
|
noreply
|
142 |
def _chat_side_effect(messages, **kwargs): |
|
0981a08…
|
noreply
|
143 |
content = messages[0]["content"] if messages else "" |
|
0981a08…
|
noreply
|
144 |
if "key points" in content.lower(): |
|
0981a08…
|
noreply
|
145 |
return json.dumps( |
|
0981a08…
|
noreply
|
146 |
[{"point": "Deployment strategy discussed", "topic": "DevOps", "details": "Python"}] |
|
0981a08…
|
noreply
|
147 |
) |
|
0981a08…
|
noreply
|
148 |
if "action items" in content.lower(): |
|
0981a08…
|
noreply
|
149 |
return json.dumps( |
|
0981a08…
|
noreply
|
150 |
[{"action": "Deploy to production", "assignee": "Bob", "priority": "high"}] |
|
0981a08…
|
noreply
|
151 |
) |
|
0981a08…
|
noreply
|
152 |
# Default: entity extraction for knowledge graph |
|
0981a08…
|
noreply
|
153 |
return json.dumps( |
|
0981a08…
|
noreply
|
154 |
{ |
|
0981a08…
|
noreply
|
155 |
"entities": [ |
|
0981a08…
|
noreply
|
156 |
{"name": "Python", "type": "technology", "description": "Programming language"}, |
|
0981a08…
|
noreply
|
157 |
{"name": "Alice", "type": "person", "description": "Engineer"}, |
|
0981a08…
|
noreply
|
158 |
], |
|
0981a08…
|
noreply
|
159 |
"relationships": [ |
|
0981a08…
|
noreply
|
160 |
{"source": "Alice", "target": "Python", "type": "uses"}, |
|
0981a08…
|
noreply
|
161 |
], |
|
0981a08…
|
noreply
|
162 |
} |
|
0981a08…
|
noreply
|
163 |
) |
|
0981a08…
|
noreply
|
164 |
|
|
0981a08…
|
noreply
|
165 |
pm.chat.side_effect = _chat_side_effect |
|
0981a08…
|
noreply
|
166 |
pm.get_models_used.return_value = {"chat": "mock-gpt", "transcription": "mock-whisper"} |
|
0981a08…
|
noreply
|
167 |
return pm |
|
0981a08…
|
noreply
|
168 |
|
|
0981a08…
|
noreply
|
169 |
|
|
0981a08…
|
noreply
|
170 |
def _make_tqdm_passthrough(mock_tqdm): |
|
0981a08…
|
noreply
|
171 |
"""Configure mock tqdm to pass through iterables while supporting .set_description() etc.""" |
|
0981a08…
|
noreply
|
172 |
|
|
0981a08…
|
noreply
|
173 |
def _tqdm_side_effect(iterable, **kw): |
|
0981a08…
|
noreply
|
174 |
wrapper = MagicMock() |
|
0981a08…
|
noreply
|
175 |
wrapper.__iter__ = lambda self: iter(iterable) |
|
0981a08…
|
noreply
|
176 |
return wrapper |
|
0981a08…
|
noreply
|
177 |
|
|
0981a08…
|
noreply
|
178 |
mock_tqdm.side_effect = _tqdm_side_effect |
|
0981a08…
|
noreply
|
179 |
|
|
0981a08…
|
noreply
|
180 |
|
|
0981a08…
|
noreply
|
181 |
def _create_fake_video(path: Path) -> Path: |
|
0981a08…
|
noreply
|
182 |
"""Create a tiny file that stands in for a video (all extractors are mocked).""" |
|
0981a08…
|
noreply
|
183 |
path.parent.mkdir(parents=True, exist_ok=True) |
|
0981a08…
|
noreply
|
184 |
path.write_bytes(b"\x00" * 64) |
|
0981a08…
|
noreply
|
185 |
return path |
|
0981a08…
|
noreply
|
186 |
|
|
0981a08…
|
noreply
|
187 |
|
|
0981a08…
|
noreply
|
188 |
class TestProcessSingleVideo: |
|
0981a08…
|
noreply
|
189 |
"""Integration-level tests for process_single_video with heavy mocking.""" |
|
0981a08…
|
noreply
|
190 |
|
|
0981a08…
|
noreply
|
191 |
@pytest.fixture |
|
0981a08…
|
noreply
|
192 |
def setup(self, tmp_path): |
|
0981a08…
|
noreply
|
193 |
"""Create fake video, output dir, and mock PM.""" |
|
0981a08…
|
noreply
|
194 |
video_path = _create_fake_video(tmp_path / "input" / "meeting.mp4") |
|
0981a08…
|
noreply
|
195 |
output_dir = tmp_path / "output" |
|
0981a08…
|
noreply
|
196 |
pm = _make_mock_pm() |
|
0981a08…
|
noreply
|
197 |
return video_path, output_dir, pm |
|
0981a08…
|
noreply
|
198 |
|
|
0981a08…
|
noreply
|
199 |
@patch("video_processor.pipeline.export_all_formats") |
|
0981a08…
|
noreply
|
200 |
@patch("video_processor.pipeline.PlanGenerator") |
|
0981a08…
|
noreply
|
201 |
@patch("video_processor.pipeline.DiagramAnalyzer") |
|
0981a08…
|
noreply
|
202 |
@patch("video_processor.pipeline.AudioExtractor") |
|
0981a08…
|
noreply
|
203 |
@patch("video_processor.pipeline.filter_people_frames") |
|
0981a08…
|
noreply
|
204 |
@patch("video_processor.pipeline.save_frames") |
|
0981a08…
|
noreply
|
205 |
@patch("video_processor.pipeline.extract_frames") |
|
0981a08…
|
noreply
|
206 |
@patch("video_processor.pipeline.tqdm") |
|
0981a08…
|
noreply
|
207 |
def test_returns_manifest( |
|
0981a08…
|
noreply
|
208 |
self, |
|
0981a08…
|
noreply
|
209 |
mock_tqdm, |
|
0981a08…
|
noreply
|
210 |
mock_extract_frames, |
|
0981a08…
|
noreply
|
211 |
mock_save_frames, |
|
0981a08…
|
noreply
|
212 |
mock_filter_people, |
|
0981a08…
|
noreply
|
213 |
mock_audio_extractor_cls, |
|
0981a08…
|
noreply
|
214 |
mock_diagram_analyzer_cls, |
|
0981a08…
|
noreply
|
215 |
mock_plan_gen_cls, |
|
0981a08…
|
noreply
|
216 |
mock_export, |
|
0981a08…
|
noreply
|
217 |
setup, |
|
0981a08…
|
noreply
|
218 |
): |
|
0981a08…
|
noreply
|
219 |
video_path, output_dir, pm = setup |
|
0981a08…
|
noreply
|
220 |
|
|
0981a08…
|
noreply
|
221 |
# tqdm pass-through |
|
0981a08…
|
noreply
|
222 |
_make_tqdm_passthrough(mock_tqdm) |
|
0981a08…
|
noreply
|
223 |
|
|
0981a08…
|
noreply
|
224 |
# Frame extraction mocks |
|
0981a08…
|
noreply
|
225 |
mock_extract_frames.return_value = [b"fake_frame_1", b"fake_frame_2"] |
|
0981a08…
|
noreply
|
226 |
mock_filter_people.return_value = ([b"fake_frame_1", b"fake_frame_2"], 0) |
|
0981a08…
|
noreply
|
227 |
|
|
0981a08…
|
noreply
|
228 |
frames_dir = output_dir / "frames" |
|
0981a08…
|
noreply
|
229 |
frames_dir.mkdir(parents=True, exist_ok=True) |
|
0981a08…
|
noreply
|
230 |
frame_paths = [] |
|
0981a08…
|
noreply
|
231 |
for i in range(2): |
|
0981a08…
|
noreply
|
232 |
fp = frames_dir / f"frame_{i:04d}.jpg" |
|
0981a08…
|
noreply
|
233 |
fp.write_bytes(b"\xff") |
|
0981a08…
|
noreply
|
234 |
frame_paths.append(fp) |
|
0981a08…
|
noreply
|
235 |
mock_save_frames.return_value = frame_paths |
|
0981a08…
|
noreply
|
236 |
|
|
0981a08…
|
noreply
|
237 |
# Audio extractor mock |
|
0981a08…
|
noreply
|
238 |
audio_ext = MagicMock() |
|
0981a08…
|
noreply
|
239 |
audio_ext.extract_audio.return_value = output_dir / "audio" / "meeting.wav" |
|
0981a08…
|
noreply
|
240 |
audio_ext.get_audio_properties.return_value = {"duration": 10.0} |
|
0981a08…
|
noreply
|
241 |
mock_audio_extractor_cls.return_value = audio_ext |
|
0981a08…
|
noreply
|
242 |
|
|
0981a08…
|
noreply
|
243 |
# Diagram analyzer mock |
|
0981a08…
|
noreply
|
244 |
diag_analyzer = MagicMock() |
|
0981a08…
|
noreply
|
245 |
diag_analyzer.process_frames.return_value = ([], []) |
|
0981a08…
|
noreply
|
246 |
mock_diagram_analyzer_cls.return_value = diag_analyzer |
|
0981a08…
|
noreply
|
247 |
|
|
0981a08…
|
noreply
|
248 |
# Plan generator mock |
|
0981a08…
|
noreply
|
249 |
plan_gen = MagicMock() |
|
0981a08…
|
noreply
|
250 |
mock_plan_gen_cls.return_value = plan_gen |
|
0981a08…
|
noreply
|
251 |
|
|
0981a08…
|
noreply
|
252 |
# export_all_formats returns the manifest it receives |
|
0981a08…
|
noreply
|
253 |
mock_export.side_effect = lambda out_dir, manifest: manifest |
|
0981a08…
|
noreply
|
254 |
|
|
0981a08…
|
noreply
|
255 |
manifest = process_single_video( |
|
0981a08…
|
noreply
|
256 |
input_path=video_path, |
|
0981a08…
|
noreply
|
257 |
output_dir=output_dir, |
|
0981a08…
|
noreply
|
258 |
provider_manager=pm, |
|
0981a08…
|
noreply
|
259 |
depth="standard", |
|
0981a08…
|
noreply
|
260 |
) |
|
0981a08…
|
noreply
|
261 |
|
|
0981a08…
|
noreply
|
262 |
from video_processor.models import VideoManifest |
|
0981a08…
|
noreply
|
263 |
|
|
0981a08…
|
noreply
|
264 |
assert isinstance(manifest, VideoManifest) |
|
0981a08…
|
noreply
|
265 |
assert manifest.video.title == "Analysis of meeting" |
|
0981a08…
|
noreply
|
266 |
assert manifest.stats.frames_extracted == 2 |
|
0981a08…
|
noreply
|
267 |
assert manifest.transcript_json == "transcript/transcript.json" |
|
0981a08…
|
noreply
|
268 |
assert manifest.knowledge_graph_json == "results/knowledge_graph.json" |
|
0981a08…
|
noreply
|
269 |
|
|
0981a08…
|
noreply
|
270 |
@patch("video_processor.pipeline.export_all_formats") |
|
0981a08…
|
noreply
|
271 |
@patch("video_processor.pipeline.PlanGenerator") |
|
0981a08…
|
noreply
|
272 |
@patch("video_processor.pipeline.DiagramAnalyzer") |
|
0981a08…
|
noreply
|
273 |
@patch("video_processor.pipeline.AudioExtractor") |
|
0981a08…
|
noreply
|
274 |
@patch("video_processor.pipeline.filter_people_frames") |
|
0981a08…
|
noreply
|
275 |
@patch("video_processor.pipeline.save_frames") |
|
0981a08…
|
noreply
|
276 |
@patch("video_processor.pipeline.extract_frames") |
|
0981a08…
|
noreply
|
277 |
@patch("video_processor.pipeline.tqdm") |
|
0981a08…
|
noreply
|
278 |
def test_creates_output_directories( |
|
0981a08…
|
noreply
|
279 |
self, |
|
0981a08…
|
noreply
|
280 |
mock_tqdm, |
|
0981a08…
|
noreply
|
281 |
mock_extract_frames, |
|
0981a08…
|
noreply
|
282 |
mock_save_frames, |
|
0981a08…
|
noreply
|
283 |
mock_filter_people, |
|
0981a08…
|
noreply
|
284 |
mock_audio_extractor_cls, |
|
0981a08…
|
noreply
|
285 |
mock_diagram_analyzer_cls, |
|
0981a08…
|
noreply
|
286 |
mock_plan_gen_cls, |
|
0981a08…
|
noreply
|
287 |
mock_export, |
|
0981a08…
|
noreply
|
288 |
setup, |
|
0981a08…
|
noreply
|
289 |
): |
|
0981a08…
|
noreply
|
290 |
video_path, output_dir, pm = setup |
|
0981a08…
|
noreply
|
291 |
|
|
0981a08…
|
noreply
|
292 |
_make_tqdm_passthrough(mock_tqdm) |
|
0981a08…
|
noreply
|
293 |
mock_extract_frames.return_value = [] |
|
0981a08…
|
noreply
|
294 |
mock_filter_people.return_value = ([], 0) |
|
0981a08…
|
noreply
|
295 |
mock_save_frames.return_value = [] |
|
0981a08…
|
noreply
|
296 |
|
|
0981a08…
|
noreply
|
297 |
audio_ext = MagicMock() |
|
0981a08…
|
noreply
|
298 |
audio_ext.extract_audio.return_value = output_dir / "audio" / "meeting.wav" |
|
0981a08…
|
noreply
|
299 |
audio_ext.get_audio_properties.return_value = {"duration": 5.0} |
|
0981a08…
|
noreply
|
300 |
mock_audio_extractor_cls.return_value = audio_ext |
|
0981a08…
|
noreply
|
301 |
|
|
0981a08…
|
noreply
|
302 |
diag_analyzer = MagicMock() |
|
0981a08…
|
noreply
|
303 |
diag_analyzer.process_frames.return_value = ([], []) |
|
0981a08…
|
noreply
|
304 |
mock_diagram_analyzer_cls.return_value = diag_analyzer |
|
0981a08…
|
noreply
|
305 |
|
|
0981a08…
|
noreply
|
306 |
plan_gen = MagicMock() |
|
0981a08…
|
noreply
|
307 |
mock_plan_gen_cls.return_value = plan_gen |
|
0981a08…
|
noreply
|
308 |
|
|
0981a08…
|
noreply
|
309 |
mock_export.side_effect = lambda out_dir, manifest: manifest |
|
0981a08…
|
noreply
|
310 |
|
|
0981a08…
|
noreply
|
311 |
process_single_video( |
|
0981a08…
|
noreply
|
312 |
input_path=video_path, |
|
0981a08…
|
noreply
|
313 |
output_dir=output_dir, |
|
0981a08…
|
noreply
|
314 |
provider_manager=pm, |
|
0981a08…
|
noreply
|
315 |
) |
|
0981a08…
|
noreply
|
316 |
|
|
0981a08…
|
noreply
|
317 |
# Verify standard output directories were created |
|
0981a08…
|
noreply
|
318 |
assert (output_dir / "transcript").is_dir() |
|
0981a08…
|
noreply
|
319 |
assert (output_dir / "frames").is_dir() |
|
0981a08…
|
noreply
|
320 |
assert (output_dir / "results").is_dir() |
|
0981a08…
|
noreply
|
321 |
|
|
0981a08…
|
noreply
|
322 |
@patch("video_processor.pipeline.export_all_formats") |
|
0981a08…
|
noreply
|
323 |
@patch("video_processor.pipeline.PlanGenerator") |
|
0981a08…
|
noreply
|
324 |
@patch("video_processor.pipeline.DiagramAnalyzer") |
|
0981a08…
|
noreply
|
325 |
@patch("video_processor.pipeline.AudioExtractor") |
|
0981a08…
|
noreply
|
326 |
@patch("video_processor.pipeline.filter_people_frames") |
|
0981a08…
|
noreply
|
327 |
@patch("video_processor.pipeline.save_frames") |
|
0981a08…
|
noreply
|
328 |
@patch("video_processor.pipeline.extract_frames") |
|
0981a08…
|
noreply
|
329 |
@patch("video_processor.pipeline.tqdm") |
|
0981a08…
|
noreply
|
330 |
def test_resume_existing_frames( |
|
0981a08…
|
noreply
|
331 |
self, |
|
0981a08…
|
noreply
|
332 |
mock_tqdm, |
|
0981a08…
|
noreply
|
333 |
mock_extract_frames, |
|
0981a08…
|
noreply
|
334 |
mock_save_frames, |
|
0981a08…
|
noreply
|
335 |
mock_filter_people, |
|
0981a08…
|
noreply
|
336 |
mock_audio_extractor_cls, |
|
0981a08…
|
noreply
|
337 |
mock_diagram_analyzer_cls, |
|
0981a08…
|
noreply
|
338 |
mock_plan_gen_cls, |
|
0981a08…
|
noreply
|
339 |
mock_export, |
|
0981a08…
|
noreply
|
340 |
setup, |
|
0981a08…
|
noreply
|
341 |
): |
|
0981a08…
|
noreply
|
342 |
"""When frames already exist on disk, extraction should be skipped.""" |
|
0981a08…
|
noreply
|
343 |
video_path, output_dir, pm = setup |
|
0981a08…
|
noreply
|
344 |
|
|
0981a08…
|
noreply
|
345 |
_make_tqdm_passthrough(mock_tqdm) |
|
0981a08…
|
noreply
|
346 |
|
|
0981a08…
|
noreply
|
347 |
# Pre-create frames directory with existing frames |
|
0981a08…
|
noreply
|
348 |
frames_dir = output_dir / "frames" |
|
0981a08…
|
noreply
|
349 |
frames_dir.mkdir(parents=True, exist_ok=True) |
|
0981a08…
|
noreply
|
350 |
for i in range(3): |
|
0981a08…
|
noreply
|
351 |
(frames_dir / f"frame_{i:04d}.jpg").write_bytes(b"\xff") |
|
0981a08…
|
noreply
|
352 |
|
|
0981a08…
|
noreply
|
353 |
audio_ext = MagicMock() |
|
0981a08…
|
noreply
|
354 |
audio_ext.extract_audio.return_value = output_dir / "audio" / "meeting.wav" |
|
0981a08…
|
noreply
|
355 |
audio_ext.get_audio_properties.return_value = {"duration": 10.0} |
|
0981a08…
|
noreply
|
356 |
mock_audio_extractor_cls.return_value = audio_ext |
|
0981a08…
|
noreply
|
357 |
|
|
0981a08…
|
noreply
|
358 |
diag_analyzer = MagicMock() |
|
0981a08…
|
noreply
|
359 |
diag_analyzer.process_frames.return_value = ([], []) |
|
0981a08…
|
noreply
|
360 |
mock_diagram_analyzer_cls.return_value = diag_analyzer |
|
0981a08…
|
noreply
|
361 |
|
|
0981a08…
|
noreply
|
362 |
plan_gen = MagicMock() |
|
0981a08…
|
noreply
|
363 |
mock_plan_gen_cls.return_value = plan_gen |
|
0981a08…
|
noreply
|
364 |
mock_export.side_effect = lambda out_dir, manifest: manifest |
|
0981a08…
|
noreply
|
365 |
|
|
0981a08…
|
noreply
|
366 |
manifest = process_single_video( |
|
0981a08…
|
noreply
|
367 |
input_path=video_path, |
|
0981a08…
|
noreply
|
368 |
output_dir=output_dir, |
|
0981a08…
|
noreply
|
369 |
provider_manager=pm, |
|
0981a08…
|
noreply
|
370 |
) |
|
0981a08…
|
noreply
|
371 |
|
|
0981a08…
|
noreply
|
372 |
# extract_frames should NOT have been called (resume path) |
|
0981a08…
|
noreply
|
373 |
mock_extract_frames.assert_not_called() |
|
0981a08…
|
noreply
|
374 |
assert manifest.stats.frames_extracted == 3 |
|
0981a08…
|
noreply
|
375 |
|
|
0981a08…
|
noreply
|
376 |
@patch("video_processor.pipeline.export_all_formats") |
|
0981a08…
|
noreply
|
377 |
@patch("video_processor.pipeline.PlanGenerator") |
|
0981a08…
|
noreply
|
378 |
@patch("video_processor.pipeline.DiagramAnalyzer") |
|
0981a08…
|
noreply
|
379 |
@patch("video_processor.pipeline.AudioExtractor") |
|
0981a08…
|
noreply
|
380 |
@patch("video_processor.pipeline.filter_people_frames") |
|
0981a08…
|
noreply
|
381 |
@patch("video_processor.pipeline.save_frames") |
|
0981a08…
|
noreply
|
382 |
@patch("video_processor.pipeline.extract_frames") |
|
0981a08…
|
noreply
|
383 |
@patch("video_processor.pipeline.tqdm") |
|
0981a08…
|
noreply
|
384 |
def test_resume_existing_transcript( |
|
0981a08…
|
noreply
|
385 |
self, |
|
0981a08…
|
noreply
|
386 |
mock_tqdm, |
|
0981a08…
|
noreply
|
387 |
mock_extract_frames, |
|
0981a08…
|
noreply
|
388 |
mock_save_frames, |
|
0981a08…
|
noreply
|
389 |
mock_filter_people, |
|
0981a08…
|
noreply
|
390 |
mock_audio_extractor_cls, |
|
0981a08…
|
noreply
|
391 |
mock_diagram_analyzer_cls, |
|
0981a08…
|
noreply
|
392 |
mock_plan_gen_cls, |
|
0981a08…
|
noreply
|
393 |
mock_export, |
|
0981a08…
|
noreply
|
394 |
setup, |
|
0981a08…
|
noreply
|
395 |
): |
|
0981a08…
|
noreply
|
396 |
"""When transcript exists on disk, transcription should be skipped.""" |
|
0981a08…
|
noreply
|
397 |
video_path, output_dir, pm = setup |
|
0981a08…
|
noreply
|
398 |
|
|
0981a08…
|
noreply
|
399 |
_make_tqdm_passthrough(mock_tqdm) |
|
0981a08…
|
noreply
|
400 |
mock_extract_frames.return_value = [] |
|
0981a08…
|
noreply
|
401 |
mock_filter_people.return_value = ([], 0) |
|
0981a08…
|
noreply
|
402 |
mock_save_frames.return_value = [] |
|
0981a08…
|
noreply
|
403 |
|
|
0981a08…
|
noreply
|
404 |
audio_ext = MagicMock() |
|
0981a08…
|
noreply
|
405 |
audio_ext.extract_audio.return_value = output_dir / "audio" / "meeting.wav" |
|
0981a08…
|
noreply
|
406 |
audio_ext.get_audio_properties.return_value = {"duration": 10.0} |
|
0981a08…
|
noreply
|
407 |
mock_audio_extractor_cls.return_value = audio_ext |
|
0981a08…
|
noreply
|
408 |
|
|
0981a08…
|
noreply
|
409 |
# Pre-create transcript file |
|
0981a08…
|
noreply
|
410 |
transcript_dir = output_dir / "transcript" |
|
0981a08…
|
noreply
|
411 |
transcript_dir.mkdir(parents=True, exist_ok=True) |
|
0981a08…
|
noreply
|
412 |
transcript_data = { |
|
0981a08…
|
noreply
|
413 |
"text": "Pre-existing transcript text.", |
|
0981a08…
|
noreply
|
414 |
"segments": [{"start": 0.0, "end": 5.0, "text": "Pre-existing transcript text."}], |
|
0981a08…
|
noreply
|
415 |
"duration": 5.0, |
|
0981a08…
|
noreply
|
416 |
} |
|
0981a08…
|
noreply
|
417 |
(transcript_dir / "transcript.json").write_text(json.dumps(transcript_data)) |
|
0981a08…
|
noreply
|
418 |
|
|
0981a08…
|
noreply
|
419 |
diag_analyzer = MagicMock() |
|
0981a08…
|
noreply
|
420 |
diag_analyzer.process_frames.return_value = ([], []) |
|
0981a08…
|
noreply
|
421 |
mock_diagram_analyzer_cls.return_value = diag_analyzer |
|
0981a08…
|
noreply
|
422 |
|
|
0981a08…
|
noreply
|
423 |
plan_gen = MagicMock() |
|
0981a08…
|
noreply
|
424 |
mock_plan_gen_cls.return_value = plan_gen |
|
0981a08…
|
noreply
|
425 |
mock_export.side_effect = lambda out_dir, manifest: manifest |
|
0981a08…
|
noreply
|
426 |
|
|
0981a08…
|
noreply
|
427 |
process_single_video( |
|
0981a08…
|
noreply
|
428 |
input_path=video_path, |
|
0981a08…
|
noreply
|
429 |
output_dir=output_dir, |
|
0981a08…
|
noreply
|
430 |
provider_manager=pm, |
|
0981a08…
|
noreply
|
431 |
) |
|
0981a08…
|
noreply
|
432 |
|
|
0981a08…
|
noreply
|
433 |
# transcribe_audio should NOT have been called (resume path) |
|
0981a08…
|
noreply
|
434 |
pm.transcribe_audio.assert_not_called() |
|
0981a08…
|
noreply
|
435 |
|
|
0981a08…
|
noreply
|
436 |
@patch("video_processor.pipeline.export_all_formats") |
|
0981a08…
|
noreply
|
437 |
@patch("video_processor.pipeline.PlanGenerator") |
|
0981a08…
|
noreply
|
438 |
@patch("video_processor.pipeline.DiagramAnalyzer") |
|
0981a08…
|
noreply
|
439 |
@patch("video_processor.pipeline.AudioExtractor") |
|
0981a08…
|
noreply
|
440 |
@patch("video_processor.pipeline.filter_people_frames") |
|
0981a08…
|
noreply
|
441 |
@patch("video_processor.pipeline.save_frames") |
|
0981a08…
|
noreply
|
442 |
@patch("video_processor.pipeline.extract_frames") |
|
0981a08…
|
noreply
|
443 |
@patch("video_processor.pipeline.tqdm") |
|
0981a08…
|
noreply
|
444 |
def test_custom_title( |
|
0981a08…
|
noreply
|
445 |
self, |
|
0981a08…
|
noreply
|
446 |
mock_tqdm, |
|
0981a08…
|
noreply
|
447 |
mock_extract_frames, |
|
0981a08…
|
noreply
|
448 |
mock_save_frames, |
|
0981a08…
|
noreply
|
449 |
mock_filter_people, |
|
0981a08…
|
noreply
|
450 |
mock_audio_extractor_cls, |
|
0981a08…
|
noreply
|
451 |
mock_diagram_analyzer_cls, |
|
0981a08…
|
noreply
|
452 |
mock_plan_gen_cls, |
|
0981a08…
|
noreply
|
453 |
mock_export, |
|
0981a08…
|
noreply
|
454 |
setup, |
|
0981a08…
|
noreply
|
455 |
): |
|
0981a08…
|
noreply
|
456 |
video_path, output_dir, pm = setup |
|
0981a08…
|
noreply
|
457 |
|
|
0981a08…
|
noreply
|
458 |
_make_tqdm_passthrough(mock_tqdm) |
|
0981a08…
|
noreply
|
459 |
mock_extract_frames.return_value = [] |
|
0981a08…
|
noreply
|
460 |
mock_filter_people.return_value = ([], 0) |
|
0981a08…
|
noreply
|
461 |
mock_save_frames.return_value = [] |
|
0981a08…
|
noreply
|
462 |
|
|
0981a08…
|
noreply
|
463 |
audio_ext = MagicMock() |
|
0981a08…
|
noreply
|
464 |
audio_ext.extract_audio.return_value = output_dir / "audio" / "meeting.wav" |
|
0981a08…
|
noreply
|
465 |
audio_ext.get_audio_properties.return_value = {"duration": 5.0} |
|
0981a08…
|
noreply
|
466 |
mock_audio_extractor_cls.return_value = audio_ext |
|
0981a08…
|
noreply
|
467 |
|
|
0981a08…
|
noreply
|
468 |
diag_analyzer = MagicMock() |
|
0981a08…
|
noreply
|
469 |
diag_analyzer.process_frames.return_value = ([], []) |
|
0981a08…
|
noreply
|
470 |
mock_diagram_analyzer_cls.return_value = diag_analyzer |
|
0981a08…
|
noreply
|
471 |
|
|
0981a08…
|
noreply
|
472 |
plan_gen = MagicMock() |
|
0981a08…
|
noreply
|
473 |
mock_plan_gen_cls.return_value = plan_gen |
|
0981a08…
|
noreply
|
474 |
mock_export.side_effect = lambda out_dir, manifest: manifest |
|
0981a08…
|
noreply
|
475 |
|
|
0981a08…
|
noreply
|
476 |
manifest = process_single_video( |
|
0981a08…
|
noreply
|
477 |
input_path=video_path, |
|
0981a08…
|
noreply
|
478 |
output_dir=output_dir, |
|
0981a08…
|
noreply
|
479 |
provider_manager=pm, |
|
0981a08…
|
noreply
|
480 |
title="My Custom Title", |
|
0981a08…
|
noreply
|
481 |
) |
|
0981a08…
|
noreply
|
482 |
|
|
0981a08…
|
noreply
|
483 |
assert manifest.video.title == "My Custom Title" |
|
0981a08…
|
noreply
|
484 |
|
|
0981a08…
|
noreply
|
485 |
@patch("video_processor.pipeline.export_all_formats") |
|
0981a08…
|
noreply
|
486 |
@patch("video_processor.pipeline.PlanGenerator") |
|
0981a08…
|
noreply
|
487 |
@patch("video_processor.pipeline.DiagramAnalyzer") |
|
0981a08…
|
noreply
|
488 |
@patch("video_processor.pipeline.AudioExtractor") |
|
0981a08…
|
noreply
|
489 |
@patch("video_processor.pipeline.filter_people_frames") |
|
0981a08…
|
noreply
|
490 |
@patch("video_processor.pipeline.save_frames") |
|
0981a08…
|
noreply
|
491 |
@patch("video_processor.pipeline.extract_frames") |
|
0981a08…
|
noreply
|
492 |
@patch("video_processor.pipeline.tqdm") |
|
0981a08…
|
noreply
|
493 |
def test_key_points_and_action_items_extracted( |
|
0981a08…
|
noreply
|
494 |
self, |
|
0981a08…
|
noreply
|
495 |
mock_tqdm, |
|
0981a08…
|
noreply
|
496 |
mock_extract_frames, |
|
0981a08…
|
noreply
|
497 |
mock_save_frames, |
|
0981a08…
|
noreply
|
498 |
mock_filter_people, |
|
0981a08…
|
noreply
|
499 |
mock_audio_extractor_cls, |
|
0981a08…
|
noreply
|
500 |
mock_diagram_analyzer_cls, |
|
0981a08…
|
noreply
|
501 |
mock_plan_gen_cls, |
|
0981a08…
|
noreply
|
502 |
mock_export, |
|
0981a08…
|
noreply
|
503 |
setup, |
|
0981a08…
|
noreply
|
504 |
): |
|
0981a08…
|
noreply
|
505 |
video_path, output_dir, pm = setup |
|
0981a08…
|
noreply
|
506 |
|
|
0981a08…
|
noreply
|
507 |
_make_tqdm_passthrough(mock_tqdm) |
|
0981a08…
|
noreply
|
508 |
mock_extract_frames.return_value = [] |
|
0981a08…
|
noreply
|
509 |
mock_filter_people.return_value = ([], 0) |
|
0981a08…
|
noreply
|
510 |
mock_save_frames.return_value = [] |
|
0981a08…
|
noreply
|
511 |
|
|
0981a08…
|
noreply
|
512 |
audio_ext = MagicMock() |
|
0981a08…
|
noreply
|
513 |
audio_ext.extract_audio.return_value = output_dir / "audio" / "meeting.wav" |
|
0981a08…
|
noreply
|
514 |
audio_ext.get_audio_properties.return_value = {"duration": 10.0} |
|
0981a08…
|
noreply
|
515 |
mock_audio_extractor_cls.return_value = audio_ext |
|
0981a08…
|
noreply
|
516 |
|
|
0981a08…
|
noreply
|
517 |
diag_analyzer = MagicMock() |
|
0981a08…
|
noreply
|
518 |
diag_analyzer.process_frames.return_value = ([], []) |
|
0981a08…
|
noreply
|
519 |
mock_diagram_analyzer_cls.return_value = diag_analyzer |
|
0981a08…
|
noreply
|
520 |
|
|
0981a08…
|
noreply
|
521 |
plan_gen = MagicMock() |
|
0981a08…
|
noreply
|
522 |
mock_plan_gen_cls.return_value = plan_gen |
|
0981a08…
|
noreply
|
523 |
mock_export.side_effect = lambda out_dir, manifest: manifest |
|
0981a08…
|
noreply
|
524 |
|
|
0981a08…
|
noreply
|
525 |
manifest = process_single_video( |
|
0981a08…
|
noreply
|
526 |
input_path=video_path, |
|
0981a08…
|
noreply
|
527 |
output_dir=output_dir, |
|
0981a08…
|
noreply
|
528 |
provider_manager=pm, |
|
0981a08…
|
noreply
|
529 |
) |
|
0981a08…
|
noreply
|
530 |
|
|
0981a08…
|
noreply
|
531 |
assert len(manifest.key_points) == 1 |
|
0981a08…
|
noreply
|
532 |
assert manifest.key_points[0].point == "Deployment strategy discussed" |
|
0981a08…
|
noreply
|
533 |
assert len(manifest.action_items) == 1 |
|
0981a08…
|
noreply
|
534 |
assert manifest.action_items[0].action == "Deploy to production" |