Spaces:
Sleeping
Sleeping
| """ | |
| Unit tests for Node 4: Clip Signal Extractor | |
| (src/envs/subenv2/node4_clip_extractor.py) | |
| All heavy dependencies β OpenCV VideoCapture, InsightFace, and MediaPipe | |
| FaceMesh β are fully mocked so no real video files or GPU/model weights | |
| are required. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import sys | |
| import types | |
| from pathlib import Path | |
| from unittest.mock import MagicMock, patch | |
| import numpy as np | |
| import pytest | |
| from src.schemas.subenv2 import ClipSignalObservation | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| _RNG = np.random.default_rng(42) | |
| def _random_frame(height: int = 72, width: int = 128) -> np.ndarray: | |
| """Return a random uint8 BGR frame.""" | |
| return _RNG.integers(0, 256, (height, width, 3), dtype=np.uint8) | |
| # --------------------------------------------------------------------------- | |
| # Fixture: mock_capture | |
| # --------------------------------------------------------------------------- | |
| def mock_capture(frame_count: int, height: int = 72, width: int = 128) -> MagicMock: | |
| """Return a MagicMock that behaves like cv2.VideoCapture. | |
| * ``get(cv2.CAP_PROP_FRAME_COUNT)`` β *frame_count* | |
| * ``get(cv2.CAP_PROP_FPS)`` β 24.0 | |
| * ``read()`` cycles through *frame_count* random numpy frames then | |
| returns ``(False, None)`` | |
| * ``isOpened()`` β True | |
| * ``release()`` β no-op | |
| """ | |
| import cv2 # noqa: PLC0415 | |
| frames = [_random_frame(height, width) for _ in range(frame_count)] | |
| read_returns = [(True, f) for f in frames] + [(False, None)] | |
| read_iter = iter(read_returns) | |
| cap = MagicMock() | |
| cap.isOpened.return_value = True | |
| def _get(prop_id): | |
| if prop_id == cv2.CAP_PROP_FRAME_COUNT: | |
| return float(frame_count) | |
| if prop_id == cv2.CAP_PROP_FPS: | |
| return 24.0 | |
| return 0.0 | |
| cap.get.side_effect = _get | |
| cap.read.side_effect = lambda: next(read_iter) | |
| cap.release.return_value = None | |
| return cap | |
| # --------------------------------------------------------------------------- | |
| # Shared MediaPipe / InsightFace mock builders | |
| # --------------------------------------------------------------------------- | |
| def _make_face_mesh_mock() -> MagicMock: | |
| """Return a MagicMock mimicking mediapipe FaceMesh. | |
| Each call to ``process()`` returns a result with a single detected face | |
| carrying 478 stable landmarks (x=0.5, y=0.5, z=0.0). | |
| """ | |
| lm = MagicMock() | |
| lm.x = 0.5 | |
| lm.y = 0.5 | |
| lm.z = 0.0 | |
| face_landmark = MagicMock() | |
| face_landmark.landmark = [lm] * 478 | |
| mp_result = MagicMock() | |
| mp_result.multi_face_landmarks = [face_landmark] | |
| face_mesh = MagicMock() | |
| face_mesh.process.return_value = mp_result | |
| face_mesh.close.return_value = None | |
| return face_mesh | |
| def _make_mediapipe_mock(face_mesh_instance: MagicMock) -> MagicMock: | |
| """Build a fake ``mp`` module alias as imported in node4_clip_extractor.""" | |
| mp_mock = MagicMock() | |
| mp_mock.solutions.face_mesh.FaceMesh.return_value = face_mesh_instance | |
| return mp_mock | |
| def _make_insightface_modules() -> dict: | |
| """Return a sys.modules patch dict for insightface. | |
| ``FaceAnalysis.get()`` returns a single face with a unit 512-D embedding. | |
| """ | |
| embedding = np.ones(512, dtype=np.float32) | |
| face = MagicMock() | |
| face.normed_embedding = embedding | |
| fa_instance = MagicMock() | |
| fa_instance.get.return_value = [face] | |
| fa_instance.prepare.return_value = None | |
| FaceAnalysis = MagicMock(return_value=fa_instance) | |
| insightface_mod = types.ModuleType("insightface") | |
| app_mod = types.ModuleType("insightface.app") | |
| app_mod.FaceAnalysis = FaceAnalysis | |
| insightface_mod.app = app_mod | |
| return { | |
| "insightface": insightface_mod, | |
| "insightface.app": app_mod, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Common dataset context skeleton | |
| # --------------------------------------------------------------------------- | |
| _EMPTY_CTX: dict = { | |
| "current_phoneme_coverage": {}, | |
| "current_pose_distribution": {}, | |
| "clips_audited_so_far": 0, | |
| "similar_clips_accepted": 0, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Context manager: full env patch | |
| # --------------------------------------------------------------------------- | |
| def _full_patch(cap_mock: MagicMock, mp_mock: MagicMock): | |
| """Return a combined context manager that patches cv2, insightface, and mp.""" | |
| from contextlib import ExitStack | |
| stack = ExitStack() | |
| stack.enter_context(patch("cv2.VideoCapture", return_value=cap_mock)) | |
| stack.enter_context(patch.dict("sys.modules", _make_insightface_modules())) | |
| stack.enter_context( | |
| patch("src.envs.subenv2.node4_clip_extractor.mp", mp_mock) | |
| ) | |
| return stack | |
| # --------------------------------------------------------------------------- | |
| # Test 1 β short clip raises ValueError mentioning the minimum frame count | |
| # --------------------------------------------------------------------------- | |
| def test_raises_on_short_clip(tmp_path): | |
| """Clips with fewer than 24 frames must raise ValueError containing '24'.""" | |
| dummy = tmp_path / "dummy.mp4" | |
| dummy.touch() | |
| cap = mock_capture(10) | |
| with patch("cv2.VideoCapture", return_value=cap): | |
| with pytest.raises(ValueError, match="24"): | |
| from src.envs.subenv2.node4_clip_extractor import extract_clip_signals | |
| extract_clip_signals(dummy, {}) | |
| # --------------------------------------------------------------------------- | |
| # Test 2 β blur_score is in [0, 1] and result is ClipSignalObservation | |
| # --------------------------------------------------------------------------- | |
| def test_blur_score_in_range(tmp_path): | |
| """blur_score must be in [0.0, 1.0] and return type must be ClipSignalObservation.""" | |
| dummy = tmp_path / "dummy.mp4" | |
| dummy.touch() | |
| cap30 = mock_capture(30) | |
| mp_mock = _make_mediapipe_mock(_make_face_mesh_mock()) | |
| with _full_patch(cap30, mp_mock): | |
| from src.envs.subenv2.node4_clip_extractor import extract_clip_signals | |
| result = extract_clip_signals(dummy, _EMPTY_CTX) | |
| assert isinstance(result, ClipSignalObservation) | |
| assert 0.0 <= result.blur_score <= 1.0 | |
| # --------------------------------------------------------------------------- | |
| # Test 3 β phoneme_coverage_new == 1.0 when dataset phoneme coverage is empty | |
| # --------------------------------------------------------------------------- | |
| def test_phoneme_coverage_new_empty_dataset(tmp_path): | |
| """All phonemes in the clip are new when the dataset coverage is empty.""" | |
| dummy = tmp_path / "dummy.mp4" | |
| dummy.touch() | |
| aligner_data = {"phonemes": ["AH", "EE", "OW"]} | |
| aligner_json = tmp_path / "align.json" | |
| aligner_json.write_text(json.dumps(aligner_data)) | |
| cap30 = mock_capture(30) | |
| mp_mock = _make_mediapipe_mock(_make_face_mesh_mock()) | |
| ctx = {**_EMPTY_CTX, "current_phoneme_coverage": {}} | |
| with _full_patch(cap30, mp_mock): | |
| from src.envs.subenv2.node4_clip_extractor import extract_clip_signals | |
| result = extract_clip_signals( | |
| dummy, | |
| ctx, | |
| aligner_output=json.loads(aligner_json.read_text()), | |
| ) | |
| assert result.phoneme_coverage_new == 1.0 | |
| # --------------------------------------------------------------------------- | |
| # Test 4 β phoneme_coverage_new β 2/3 when one phoneme already covered | |
| # --------------------------------------------------------------------------- | |
| def test_phoneme_coverage_new_partial(tmp_path): | |
| """When one of three unique phonemes is already covered, new coverage = 2/3.""" | |
| dummy = tmp_path / "dummy.mp4" | |
| dummy.touch() | |
| aligner_data = {"phonemes": ["AH", "EE", "OW"]} | |
| aligner_json = tmp_path / "align.json" | |
| aligner_json.write_text(json.dumps(aligner_data)) | |
| cap30 = mock_capture(30) | |
| mp_mock = _make_mediapipe_mock(_make_face_mesh_mock()) | |
| ctx = {**_EMPTY_CTX, "current_phoneme_coverage": {"AH": 3}} | |
| with _full_patch(cap30, mp_mock): | |
| from src.envs.subenv2.node4_clip_extractor import extract_clip_signals | |
| result = extract_clip_signals( | |
| dummy, | |
| ctx, | |
| aligner_output=json.loads(aligner_json.read_text()), | |
| ) | |
| assert abs(result.phoneme_coverage_new - 2 / 3) < 1e-6 | |
| # --------------------------------------------------------------------------- | |
| # Test 5 β no forced-align path β empty phoneme sequence and zero lip sync | |
| # --------------------------------------------------------------------------- | |
| def test_no_forced_align_path(tmp_path): | |
| """When aligner_output is None, phoneme_sequence=[] and lip_sync_confidence=0.0.""" | |
| dummy = tmp_path / "dummy.mp4" | |
| dummy.touch() | |
| cap30 = mock_capture(30) | |
| mp_mock = _make_mediapipe_mock(_make_face_mesh_mock()) | |
| with _full_patch(cap30, mp_mock): | |
| from src.envs.subenv2.node4_clip_extractor import extract_clip_signals | |
| result = extract_clip_signals(dummy, _EMPTY_CTX, aligner_output=None) | |
| assert result.phoneme_sequence == [] | |
| assert result.lip_sync_confidence == 0.0 | |