mattrguerra/transcription-pipelinemainPrivate on GitHub, published here in full

test_pipeline_inbox.py

Language Python6.1 KB170 lines

tests/test_pipeline_inbox.py

  1. """The pipeline end to end with stand-in model steps, and the watch-folder run."""
  2. import os
  3. import threading
  4. import time
  5. import pytest
  6. from transcription_pipeline import audio, cli, filing, inbox
  7. from transcription_pipeline.pipeline import Options, process
  8. from .helpers import fake_result, write_tone
  9. def fake_transcriber(wav, source, opts):
  10. assert wav.suffix == ".wav" and wav.exists()
  11. return fake_result(speakers=False)
  12. def fake_aligner(result, wav, opts):
  13. return result
  14. def fake_diarizer(result, wav, opts):
  15. return fake_result(speakers=True)
  16. def run(src, out, vault=None, **kw):
  17. return process(src, out, Options(**kw), fake_transcriber, fake_aligner, fake_diarizer,
  18. vault_inbox=vault)
  19. def test_process_writes_every_format(tone, tmp_path):
  20. res = run(tone, tmp_path / "out", formats=("txt", "md", "json", "srt", "vtt"))
  21. assert set(res.outputs) == {"txt", "md", "json", "srt", "vtt"}
  22. assert res.speakers == ["Speaker 1", "Speaker 2"] and res.sentences == 3
  23. assert abs(res.duration - 1.5) < 0.01
  24. assert res.outputs["txt"].read_text().startswith("[00:00:00] Speaker 1: Hi, thanks")
  25. assert 'duration: "0:00:02"' in res.outputs["md"].read_text()
  26. def test_no_diarize_skips_the_diarizer(tone, tmp_path):
  27. res = run(tone, tmp_path / "out", diarize=False)
  28. assert res.speakers == []
  29. assert "Speaker" not in res.outputs["txt"].read_text()
  30. def test_vault_copy_and_collisions(tone, tmp_path):
  31. vault = tmp_path / "vault" / "Inbox"
  32. a = run(tone, tmp_path / "out", vault=vault)
  33. b = run(tone, tmp_path / "out", vault=vault)
  34. assert a.outputs["vault"].name == "call.md" and b.outputs["vault"].name == "call (2).md"
  35. assert b.outputs["txt"].name == "call (2).txt"
  36. assert a.outputs["txt"].read_text() == b.outputs["txt"].read_text()
  37. def test_video_input_is_decoded(tmp_path):
  38. # Any container ffmpeg reads is fine: make an .m4a from the tone.
  39. wav = write_tone(tmp_path / "t.wav")
  40. m4a = tmp_path / "memo.m4a"
  41. audio._run(["-i", str(wav), "-c:a", "aac", str(m4a)])
  42. assert run(m4a, tmp_path / "out").duration > 1.0
  43. def test_unique_path(tmp_path):
  44. (tmp_path / "a.txt").write_text("x")
  45. (tmp_path / "a (2).txt").write_text("x")
  46. assert filing.unique_path(tmp_path, "a.txt").name == "a (3).txt"
  47. def handler(layout, vault=None):
  48. return lambda src: run(src, layout.transcripts, vault=vault)
  49. def test_inbox_good_and_corrupt(tmp_path):
  50. layout = inbox.Layout(tmp_path)
  51. layout.ensure()
  52. write_tone(layout.inbox / "good.wav")
  53. (layout.inbox / "broken.mp3").write_bytes(b"this is not audio" * 100)
  54. (layout.inbox / "notes.txt").write_text("ignored")
  55. (layout.inbox / ".hidden.wav").write_bytes(b"x")
  56. (layout.inbox / "big.mp3.part").write_bytes(b"x")
  57. vault = tmp_path / "vault"
  58. report = inbox.run(layout, handler(layout, vault), settle=0, log=lambda *_: None)
  59. assert report == {"done": ["good.wav"], "failed": ["broken.mp3"], "skipped": None}
  60. assert (layout.done / "good.wav").exists() and (layout.transcripts / "good.md").exists()
  61. assert (vault / "good.md").exists()
  62. assert (layout.failed / "broken.mp3").exists()
  63. err = (layout.failed / "broken.error.txt").read_text()
  64. assert err.startswith("AudioError: ffmpeg could not read this file")
  65. left = sorted(p.name for p in layout.inbox.iterdir())
  66. assert left == [".hidden.wav", "big.mp3.part", "notes.txt"]
  67. assert not layout.lock.exists()
  68. # A second run with nothing ready does nothing.
  69. again = inbox.run(layout, handler(layout), settle=0, log=lambda *_: None)
  70. assert again == {"done": [], "failed": [], "skipped": None}
  71. def test_inbox_waits_for_a_growing_file(tmp_path):
  72. layout = inbox.Layout(tmp_path)
  73. layout.ensure()
  74. growing = layout.inbox / "long.wav"
  75. growing.write_bytes(b"RIFF")
  76. stop = threading.Event()
  77. def grow():
  78. while not stop.is_set():
  79. with open(growing, "ab") as f:
  80. f.write(b"\0" * 1024)
  81. time.sleep(0.02)
  82. t = threading.Thread(target=grow)
  83. t.start()
  84. try:
  85. report = inbox.run(layout, handler(layout), settle=0.3, log=lambda *_: None)
  86. finally:
  87. stop.set()
  88. t.join()
  89. assert report["done"] == [] and report["failed"] == [] and growing.exists()
  90. def test_inbox_lock_held_by_live_process(tmp_path):
  91. layout = inbox.Layout(tmp_path)
  92. layout.ensure()
  93. layout.lock.write_text(str(os.getpid()))
  94. write_tone(layout.inbox / "a.wav")
  95. report = inbox.run(layout, handler(layout), settle=0, log=lambda *_: None)
  96. assert report["skipped"] == "locked" and (layout.inbox / "a.wav").exists()
  97. def test_inbox_takes_over_a_stale_lock(tmp_path):
  98. layout = inbox.Layout(tmp_path)
  99. layout.ensure()
  100. layout.lock.write_text("999999999")
  101. write_tone(layout.inbox / "a.wav")
  102. report = inbox.run(layout, handler(layout), settle=0, log=lambda *_: None)
  103. assert report["done"] == ["a.wav"] and not layout.lock.exists()
  104. def test_inbox_picks_up_files_that_arrive_mid_run(tmp_path):
  105. layout = inbox.Layout(tmp_path)
  106. layout.ensure()
  107. write_tone(layout.inbox / "first.wav")
  108. inner = handler(layout)
  109. def h(src):
  110. if src.name == "first.wav":
  111. write_tone(layout.inbox / "second.wav")
  112. return inner(src)
  113. report = inbox.run(layout, h, settle=0, log=lambda *_: None)
  114. assert report["done"] == ["first.wav", "second.wav"]
  115. def test_cli_options():
  116. args = cli.build_parser().parse_args(["x.m4a", "--speakers", "2", "--formats", "txt,.srt",
  117. "--no-diarize", "--engine", "deepinfra"])
  118. o = cli.options_from(args)
  119. assert (o.min_speakers, o.max_speakers, o.formats, o.diarize, o.engine) == \
  120. (2, 2, ("txt", "srt"), False, "deepinfra")
  121. with pytest.raises(SystemExit):
  122. cli.build_parser().parse_args(["x", "--formats", "docx"])
  123. def test_cli_inbox_needs_a_root(monkeypatch, capsys):
  124. monkeypatch.delenv("TP_ROOT", raising=False)
  125. assert cli.main(["inbox", "--no-diarize"]) == 2
  126. assert "TP_ROOT" in capsys.readouterr().err
  127. def test_cli_missing_file(capsys):
  128. assert cli.main(["/nonexistent/x.m4a", "--no-diarize"]) == 1