test_pipeline_inbox.py
Language Python6.1 KB170 lines
"""The pipeline end to end with stand-in model steps, and the watch-folder run."""import osimport threadingimport timeimport pytestfrom transcription_pipeline import audio, cli, filing, inboxfrom transcription_pipeline.pipeline import Options, processfrom .helpers import fake_result, write_tonedef fake_transcriber(wav, source, opts):assert wav.suffix == ".wav" and wav.exists()return fake_result(speakers=False)def fake_aligner(result, wav, opts):return resultdef fake_diarizer(result, wav, opts):return fake_result(speakers=True)def run(src, out, vault=None, **kw):return process(src, out, Options(**kw), fake_transcriber, fake_aligner, fake_diarizer,vault_inbox=vault)def test_process_writes_every_format(tone, tmp_path):res = run(tone, tmp_path / "out", formats=("txt", "md", "json", "srt", "vtt"))assert set(res.outputs) == {"txt", "md", "json", "srt", "vtt"}assert res.speakers == ["Speaker 1", "Speaker 2"] and res.sentences == 3assert abs(res.duration - 1.5) < 0.01assert res.outputs["txt"].read_text().startswith("[00:00:00] Speaker 1: Hi, thanks")assert 'duration: "0:00:02"' in res.outputs["md"].read_text()def test_no_diarize_skips_the_diarizer(tone, tmp_path):res = run(tone, tmp_path / "out", diarize=False)assert res.speakers == []assert "Speaker" not in res.outputs["txt"].read_text()def test_vault_copy_and_collisions(tone, tmp_path):vault = tmp_path / "vault" / "Inbox"a = run(tone, tmp_path / "out", vault=vault)b = run(tone, tmp_path / "out", vault=vault)assert a.outputs["vault"].name == "call.md" and b.outputs["vault"].name == "call (2).md"assert b.outputs["txt"].name == "call (2).txt"assert a.outputs["txt"].read_text() == b.outputs["txt"].read_text()def test_video_input_is_decoded(tmp_path):# Any container ffmpeg reads is fine: make an .m4a from the tone.wav = write_tone(tmp_path / "t.wav")m4a = tmp_path / "memo.m4a"audio._run(["-i", str(wav), "-c:a", "aac", str(m4a)])assert run(m4a, tmp_path / "out").duration > 1.0def test_unique_path(tmp_path):(tmp_path / "a.txt").write_text("x")(tmp_path / "a (2).txt").write_text("x")assert filing.unique_path(tmp_path, "a.txt").name == "a (3).txt"def handler(layout, vault=None):return lambda src: run(src, layout.transcripts, vault=vault)def test_inbox_good_and_corrupt(tmp_path):layout = inbox.Layout(tmp_path)layout.ensure()write_tone(layout.inbox / "good.wav")(layout.inbox / "broken.mp3").write_bytes(b"this is not audio" * 100)(layout.inbox / "notes.txt").write_text("ignored")(layout.inbox / ".hidden.wav").write_bytes(b"x")(layout.inbox / "big.mp3.part").write_bytes(b"x")vault = tmp_path / "vault"report = inbox.run(layout, handler(layout, vault), settle=0, log=lambda *_: None)assert report == {"done": ["good.wav"], "failed": ["broken.mp3"], "skipped": None}assert (layout.done / "good.wav").exists() and (layout.transcripts / "good.md").exists()assert (vault / "good.md").exists()assert (layout.failed / "broken.mp3").exists()err = (layout.failed / "broken.error.txt").read_text()assert err.startswith("AudioError: ffmpeg could not read this file")left = sorted(p.name for p in layout.inbox.iterdir())assert left == [".hidden.wav", "big.mp3.part", "notes.txt"]assert not layout.lock.exists()# A second run with nothing ready does nothing.again = inbox.run(layout, handler(layout), settle=0, log=lambda *_: None)assert again == {"done": [], "failed": [], "skipped": None}def test_inbox_waits_for_a_growing_file(tmp_path):layout = inbox.Layout(tmp_path)layout.ensure()growing = layout.inbox / "long.wav"growing.write_bytes(b"RIFF")stop = threading.Event()def grow():while not stop.is_set():with open(growing, "ab") as f:f.write(b"\0" * 1024)time.sleep(0.02)t = threading.Thread(target=grow)t.start()try:report = inbox.run(layout, handler(layout), settle=0.3, log=lambda *_: None)finally:stop.set()t.join()assert report["done"] == [] and report["failed"] == [] and growing.exists()def test_inbox_lock_held_by_live_process(tmp_path):layout = inbox.Layout(tmp_path)layout.ensure()layout.lock.write_text(str(os.getpid()))write_tone(layout.inbox / "a.wav")report = inbox.run(layout, handler(layout), settle=0, log=lambda *_: None)assert report["skipped"] == "locked" and (layout.inbox / "a.wav").exists()def test_inbox_takes_over_a_stale_lock(tmp_path):layout = inbox.Layout(tmp_path)layout.ensure()layout.lock.write_text("999999999")write_tone(layout.inbox / "a.wav")report = inbox.run(layout, handler(layout), settle=0, log=lambda *_: None)assert report["done"] == ["a.wav"] and not layout.lock.exists()def test_inbox_picks_up_files_that_arrive_mid_run(tmp_path):layout = inbox.Layout(tmp_path)layout.ensure()write_tone(layout.inbox / "first.wav")inner = handler(layout)def h(src):if src.name == "first.wav":write_tone(layout.inbox / "second.wav")return inner(src)report = inbox.run(layout, h, settle=0, log=lambda *_: None)assert report["done"] == ["first.wav", "second.wav"]def test_cli_options():args = cli.build_parser().parse_args(["x.m4a", "--speakers", "2", "--formats", "txt,.srt","--no-diarize", "--engine", "deepinfra"])o = cli.options_from(args)assert (o.min_speakers, o.max_speakers, o.formats, o.diarize, o.engine) == \(2, 2, ("txt", "srt"), False, "deepinfra")with pytest.raises(SystemExit):cli.build_parser().parse_args(["x", "--formats", "docx"])def test_cli_inbox_needs_a_root(monkeypatch, capsys):monkeypatch.delenv("TP_ROOT", raising=False)assert cli.main(["inbox", "--no-diarize"]) == 2assert "TP_ROOT" in capsys.readouterr().errdef test_cli_missing_file(capsys):assert cli.main(["/nonexistent/x.m4a", "--no-diarize"]) == 1