Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import Any | |
| from tiny_trigger.models import ActionEvent, Detection | |
| from tiny_trigger.video import _create_browser_mp4_writer, process_video, render_automation_video | |
| class FakeDetector: | |
| class_names = ["cat"] | |
| def detect( | |
| self, | |
| frame: Any, | |
| *, | |
| frame_index: int, | |
| timestamp_sec: float, | |
| confidence: float, | |
| image_size: int | None = None, | |
| max_detections: int | None = None, | |
| ) -> list[Detection]: | |
| assert image_size == 960 | |
| assert max_detections == 20 | |
| return [ | |
| Detection( | |
| frame_index=frame_index, | |
| timestamp_sec=timestamp_sec, | |
| label="cat", | |
| confidence=0.99, | |
| bbox_xyxy=(2.0, 2.0, 12.0, 12.0), | |
| bbox_xyxy_norm=(0.1, 0.1, 0.6, 0.6), | |
| ) | |
| ] | |
| class TrackedDetector: | |
| class_names = ["cat"] | |
| def detect( | |
| self, | |
| frame: Any, | |
| *, | |
| frame_index: int, | |
| timestamp_sec: float, | |
| confidence: float, | |
| image_size: int | None = None, | |
| max_detections: int | None = None, | |
| ) -> list[Detection]: | |
| offset = frame_index * 0.01 | |
| return [ | |
| Detection( | |
| frame_index=frame_index, | |
| timestamp_sec=timestamp_sec, | |
| label="cat", | |
| confidence=0.99, | |
| bbox_xyxy=(2.0 + frame_index, 2.0, 12.0 + frame_index, 12.0), | |
| bbox_xyxy_norm=(0.1 + offset, 0.1, 0.2 + offset, 0.2), | |
| track_id=7, | |
| ) | |
| ] | |
| class DuplicateDetector: | |
| class_names = ["cat"] | |
| def detect( | |
| self, | |
| frame: Any, | |
| *, | |
| frame_index: int, | |
| timestamp_sec: float, | |
| confidence: float, | |
| image_size: int | None = None, | |
| max_detections: int | None = None, | |
| ) -> list[Detection]: | |
| return [ | |
| Detection( | |
| frame_index=frame_index, | |
| timestamp_sec=timestamp_sec, | |
| label="cat", | |
| confidence=0.62, | |
| bbox_xyxy=(2.0, 2.0, 16.0, 16.0), | |
| bbox_xyxy_norm=(0.1, 0.1, 0.5, 0.5), | |
| ), | |
| Detection( | |
| frame_index=frame_index, | |
| timestamp_sec=timestamp_sec, | |
| label="cat", | |
| confidence=0.91, | |
| bbox_xyxy=(3.0, 3.0, 17.0, 17.0), | |
| bbox_xyxy_norm=(0.11, 0.11, 0.51, 0.51), | |
| ), | |
| ] | |
| def test_process_video_with_fake_detector(tmp_path: Path) -> None: | |
| cv2 = __import__("cv2") | |
| video_path = _make_video(tmp_path) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=2, | |
| max_frames=2, | |
| image_size=960, | |
| max_detections=20, | |
| detector=FakeDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| assert Path(result.output_video_path).exists() | |
| assert result.processed_frames == 2 | |
| assert [item.frame_index for item in result.detections] == [0, 2] | |
| assert result.frame_stride == 2 | |
| assert result.sample_interval_sec is None | |
| capture = cv2.VideoCapture(result.output_video_path) | |
| try: | |
| assert capture.get(cv2.CAP_PROP_FRAME_COUNT) == 4 | |
| assert capture.get(cv2.CAP_PROP_FPS) == 10.0 | |
| finally: | |
| capture.release() | |
| def test_process_video_does_not_synthesize_track_ids(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=10.0, frames=4) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=1, | |
| max_frames=3, | |
| detector=FakeDetector(), | |
| image_size=960, | |
| max_detections=20, | |
| output_dir=str(tmp_path), | |
| ) | |
| assert [item.track_id for item in result.detections] == [None, None, None] | |
| def test_process_video_preserves_detector_track_ids(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=10.0, frames=4) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=1, | |
| max_frames=3, | |
| detector=TrackedDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| assert [item.track_id for item in result.detections] == [7, 7, 7] | |
| def test_process_video_suppresses_duplicate_same_label_boxes(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=10.0, frames=2) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=1, | |
| max_frames=1, | |
| detector=DuplicateDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| assert len(result.detections) == 1 | |
| assert result.detections[0].confidence == 0.91 | |
| def test_process_video_samples_once_per_second(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=30.0, frames=95) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=2, | |
| sample_interval_sec=1.0, | |
| max_frames=3, | |
| image_size=960, | |
| max_detections=20, | |
| detector=FakeDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| assert result.frame_stride == 30 | |
| assert result.sample_interval_sec == 1.0 | |
| assert [item.frame_index for item in result.detections] == [0, 30, 60] | |
| def test_process_video_samples_half_second_intervals(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=10.0, frames=16) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| sample_interval_sec=0.5, | |
| max_frames=3, | |
| image_size=960, | |
| max_detections=20, | |
| detector=FakeDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| assert result.frame_stride == 5 | |
| assert [item.frame_index for item in result.detections] == [0, 5, 10] | |
| def test_render_automation_video_with_fired_event(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path) | |
| detections = [ | |
| Detection( | |
| frame_index=0, | |
| timestamp_sec=0.0, | |
| label="cat", | |
| confidence=0.99, | |
| bbox_xyxy=(2.0, 2.0, 12.0, 12.0), | |
| bbox_xyxy_norm=(0.1, 0.1, 0.6, 0.6), | |
| ) | |
| ] | |
| events = [ | |
| ActionEvent( | |
| rule="cat-rule", | |
| action="feed cat", | |
| type="simulate", | |
| frame_index=0, | |
| timestamp_sec=0.0, | |
| status="simulated", | |
| ) | |
| ] | |
| output_path = render_automation_video( | |
| source_video_path=str(video_path), | |
| detections=detections, | |
| events=events, | |
| frame_stride=2, | |
| max_frames=2, | |
| output_dir=str(tmp_path), | |
| ) | |
| assert Path(output_path).exists() | |
| assert output_path.endswith("-automated.mp4") | |
| def test_render_automation_video_uses_computed_stride(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=30.0, frames=95) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| sample_interval_sec=1.0, | |
| max_frames=3, | |
| image_size=960, | |
| max_detections=20, | |
| detector=FakeDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| output_path = render_automation_video( | |
| source_video_path=str(video_path), | |
| detections=result.detections, | |
| events=[], | |
| frame_stride=result.frame_stride, | |
| max_frames=3, | |
| output_dir=str(tmp_path), | |
| ) | |
| assert Path(output_path).exists() | |
| def test_process_video_writes_full_motion_clip_with_sampled_overlays(tmp_path: Path) -> None: | |
| cv2 = __import__("cv2") | |
| video_path = _make_video(tmp_path, fps=5.0, frames=15) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=5, | |
| max_frames=3, | |
| image_size=960, | |
| max_detections=20, | |
| detector=FakeDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| capture = cv2.VideoCapture(result.output_video_path) | |
| try: | |
| assert capture.get(cv2.CAP_PROP_FPS) == 5.0 | |
| assert capture.get(cv2.CAP_PROP_FRAME_COUNT) == 15 | |
| finally: | |
| capture.release() | |
| def test_process_video_writes_faststart_mp4(tmp_path: Path) -> None: | |
| video_path = _make_video(tmp_path, fps=5.0, frames=6) | |
| result = process_video( | |
| video_path=str(video_path), | |
| class_prompt="cat", | |
| frame_stride=2, | |
| max_frames=2, | |
| image_size=960, | |
| max_detections=20, | |
| detector=FakeDetector(), | |
| output_dir=str(tmp_path), | |
| ) | |
| data = Path(result.output_video_path).read_bytes() | |
| assert data.find(b"moov") < data.find(b"mdat") | |
| def test_browser_mp4_writer_uses_mp4v_only(monkeypatch, tmp_path: Path) -> None: | |
| calls: list[str] = [] | |
| class Writer: | |
| def isOpened(self) -> bool: | |
| return True | |
| def release(self) -> None: | |
| return None | |
| class CV2: | |
| def VideoWriter_fourcc(*codec): | |
| calls.append("".join(codec)) | |
| return 1234 | |
| def VideoWriter(path, fourcc, fps, frame_size): | |
| return Writer() | |
| monkeypatch.setitem(__import__("sys").modules, "cv2", CV2) | |
| writer = _create_browser_mp4_writer(tmp_path / "out.mp4", 8.0, (32, 32)) | |
| assert writer is not None | |
| assert calls == ["mp4v"] | |
| def _make_video(tmp_path: Path, *, fps: float = 10.0, frames: int = 4) -> Path: | |
| cv2 = __import__("cv2") | |
| video_path = tmp_path / "input.mp4" | |
| writer = cv2.VideoWriter(str(video_path), cv2.VideoWriter_fourcc(*"mp4v"), fps, (32, 32)) | |
| for index in range(frames): | |
| frame = __import__("numpy").zeros((32, 32, 3), dtype="uint8") | |
| frame[:] = (index * 20) % 256 | |
| writer.write(frame) | |
| writer.release() | |
| return video_path | |