"""Ingest tests (spec M1, lane A). Pure unit tests for the path/fps helpers, plus ffmpeg-gated tests that probe a real rendered clip, extract audio, and run the full idempotent ``run_ingest``. """ from __future__ import annotations import shutil import numpy as np import pytest from festival4d import config, db, ingest, synthetic # --------------------------------------------------------------------------- # pure helpers # --------------------------------------------------------------------------- def test_audio_wav_path_convention(): assert ingest.audio_wav_path(7) == config.AUDIO_DIR / "7.wav" def test_parse_fps(): assert ingest._parse_fps("30/1") == 30.0 assert abs(ingest._parse_fps("30000/1001") - 29.97) < 0.01 assert ingest._parse_fps("25") == 25.0 assert ingest._parse_fps(None) is None assert ingest._parse_fps("0/0") is None assert ingest._parse_fps("N/A") is None def test_run_ingest_no_files_is_empty(tmp_path, monkeypatch): """An empty raw dir yields an empty summary, not a crash.""" monkeypatch.setattr(config, "RAW_DIR", tmp_path / "empty_raw") (tmp_path / "empty_raw").mkdir() db.init_engine() db.reset_db() assert ingest.run_ingest() == [] # --------------------------------------------------------------------------- # ffmpeg-gated # --------------------------------------------------------------------------- def _render_clip(tmp_path, duration_s=2.0, cam_index=0): sr = config.AUDIO_SAMPLE_RATE master, _ = synthetic.synth_master_audio(sr) clip = synthetic.slice_for_offset(master, sr, 0.0, duration_s) wav = tmp_path / "src.wav" synthetic.write_wav(wav, clip, sr) mp4 = tmp_path / f"cam{cam_index}.mp4" synthetic.render_video(mp4, wav, cam_index, 640, 360, 30, duration_s) return mp4 @pytest.mark.skipif(shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None, reason="ffmpeg/ffprobe required") def test_probe_video(tmp_path): mp4 = _render_clip(tmp_path, duration_s=2.0) info = ingest.probe_video(mp4) assert info["width"] == 640 and info["height"] == 360 assert abs(info["fps"] - 30.0) < 1.0 assert 1.5 < info["duration_s"] < 2.6 @pytest.mark.skipif(shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None, reason="ffmpeg/ffprobe required") def test_extract_audio(tmp_path): mp4 = _render_clip(tmp_path, duration_s=2.0) out = ingest.extract_audio(mp4, tmp_path / "out.wav", sample_rate=16_000) assert out.exists() samples, sr = synthetic.read_wav(out) assert sr == 16_000 assert samples.ndim == 1 # mono assert abs(len(samples) / sr - 2.0) < 0.3 # ~2 s @pytest.mark.skipif(shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None, reason="ffmpeg/ffprobe required") def test_run_ingest_registers_and_is_idempotent(): synthetic.build(duration_s=2.0, run_ffmpeg=True) # into the temp data dir (conftest) summary = ingest.run_ingest() assert len(summary) == 3 for s in summary: assert ingest.audio_wav_path(s["video_id"]).exists() samples, sr = synthetic.read_wav(ingest.audio_wav_path(s["video_id"])) assert sr == 16_000 and samples.ndim == 1 # Second run reuses the existing rows (no unique-constraint crash) and re-extracts. again = ingest.run_ingest() assert len(again) == 3 assert all(s["reused_db_row"] for s in again) assert len(db.get_videos()) == 3 # no duplicate rows