1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
"""Tests for transcribe-worker: the queue drain on the transcription host.
ffmpeg, whisper-cli and the diarizer are replaced by small fake executables (the
process boundary). The queue handling, the merge and the loop guard run for real.
"""
import importlib.machinery
import importlib.util
import json
import os
import stat
import sys
from pathlib import Path
import pytest
SRC = Path(__file__).resolve().parent.parent / "src"
sys.path.insert(0, str(SRC))
def _load_worker():
loader = importlib.machinery.SourceFileLoader("transcribe_worker", str(SRC / "transcribe-worker"))
spec = importlib.util.spec_from_loader("transcribe_worker", loader)
assert spec is not None
module = importlib.util.module_from_spec(spec)
sys.modules["transcribe_worker"] = module # dataclasses looks the module up by name
loader.exec_module(module)
return module
worker = _load_worker()
FAKE_FFMPEG = """#!/usr/bin/env bash
# last argument is the output; the one after -i is the input
while [[ $# -gt 1 ]]; do [[ "$1" == "-i" ]] && in="$2"; shift; done
cp "$in" "$1"
exit "${FAKE_FFMPEG_EXIT:-0}"
"""
FAKE_WHISPER = """#!/usr/bin/env bash
printf '%s\\n' "$*" >> "$FAKE_LOG"
while [[ $# -gt 0 ]]; do [[ "$1" == "-of" ]] && prefix="$2"; shift; done
[[ "${FAKE_WHISPER_EXIT:-0}" == "0" ]] && cp "$FAKE_WHISPER_JSON" "$prefix.json"
exit "${FAKE_WHISPER_EXIT:-0}"
"""
FAKE_DIARIZE = """#!/usr/bin/env bash
printf 'diarize %s\\n' "$*" >> "$FAKE_LOG"
[[ "${FAKE_DIARIZE_EXIT:-0}" == "0" ]] && cp "$FAKE_TURNS_JSON" "$2"
echo "fake diarizer says hello" >&2
exit "${FAKE_DIARIZE_EXIT:-0}"
"""
def whisper_json(*words, step_ms=400):
return {"transcription": [
{"offsets": {"from": i * step_ms, "to": i * step_ms + 300}, "text": f" {w}"}
for i, w in enumerate(words)
]}
class Rig:
def __init__(self, tmp_path, monkeypatch):
self.home = tmp_path / "state"
self.bin = tmp_path / "bin"
self.bin.mkdir()
for name, body in (("ffmpeg", FAKE_FFMPEG), ("whisper-cli", FAKE_WHISPER), ("fake-diarize", FAKE_DIARIZE)):
path = self.bin / name
path.write_text(body)
path.chmod(path.stat().st_mode | stat.S_IXUSR)
self.log = tmp_path / "calls.log"
self.whisper_json = tmp_path / "whisper.json"
self.turns_json = tmp_path / "turns.json"
self.set_whisper(whisper_json("Good", "morning."))
self.turns_json.write_text(json.dumps([{"start": 0.0, "end": 60.0, "speaker": "SPEAKER_00"}]))
monkeypatch.setenv("PATH", f"{self.bin}:{os.environ['PATH']}")
monkeypatch.setenv("FAKE_LOG", str(self.log))
monkeypatch.setenv("FAKE_WHISPER_JSON", str(self.whisper_json))
monkeypatch.setenv("FAKE_TURNS_JSON", str(self.turns_json))
self.monkeypatch = monkeypatch
self.config = worker.Config(
home=self.home,
whisper_model=tmp_path / "model.bin",
diarize_cmd=[str(self.bin / "fake-diarize")],
threads=2,
)
def set_whisper(self, data):
self.whisper_json.write_text(json.dumps(data))
def submit(self, job_id, job=None, audio=b"audio", where="incoming"):
folder = self.home / where / job_id
folder.mkdir(parents=True)
(folder / "audio.m4a").write_bytes(audio)
if job is not False:
(folder / "job.json").write_text(json.dumps(job if job is not None else {"language": "en"}))
return folder
def calls(self):
return self.log.read_text().splitlines() if self.log.exists() else []
@pytest.fixture
def rig(tmp_path, monkeypatch):
return Rig(tmp_path, monkeypatch)
class TestDrainNormal:
def test_transcribe_worker_drain_one_job_writes_transcript_to_done(self, rig):
"""Normal: a job in incoming/ ends as done/<id>.txt and leaves nothing behind."""
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 1, "failed": 0}
assert (rig.home / "done" / "job1.txt").read_text() == "00:00:00 Speaker A: Good morning.\n"
assert not (rig.home / "incoming" / "job1").exists()
assert not (rig.home / "work" / "job1").exists()
assert not (rig.home / "failed" / "job1.log").exists()
def test_transcribe_worker_drain_writes_metadata_beside_the_transcript(self, rig):
"""Normal: done/<id>.json records what ran, for the client and for debugging."""
rig.submit("job1", {"language": "en", "speakers": 3, "original_name": "standup.mkv"})
worker.drain(rig.config)
meta = json.loads((rig.home / "done" / "job1.json").read_text())
assert meta["original_name"] == "standup.mkv"
assert meta["speakers_found"] == 1
assert meta["lines"] == 1
assert meta["seconds"] >= 0
def test_transcribe_worker_drain_passes_language_and_speaker_count_through(self, rig):
"""Normal: job options reach whisper and the diarizer."""
rig.submit("job1", {"language": "es", "speakers": 3})
worker.drain(rig.config)
whisper_call = next(c for c in rig.calls() if not c.startswith("diarize"))
diarize_call = next(c for c in rig.calls() if c.startswith("diarize"))
assert "-l es" in whisper_call
assert "-mc 0" in whisper_call and "-ojf" in whisper_call
assert diarize_call.endswith("--speakers 3")
def test_transcribe_worker_drain_min_and_max_speakers_are_forwarded(self, rig):
"""Normal: a range instead of an exact count."""
rig.submit("job1", {"min_speakers": 2, "max_speakers": 5})
worker.drain(rig.config)
diarize_call = next(c for c in rig.calls() if c.startswith("diarize"))
assert "--min-speakers 2" in diarize_call and "--max-speakers 5" in diarize_call
def test_transcribe_worker_drain_processes_every_job_oldest_first(self, rig):
"""Normal: the queue drains completely, in arrival order."""
first = rig.submit("b-first")
rig.submit("a-second")
os.utime(first, (1, 1))
assert worker.drain(rig.config) == {"done": 2, "failed": 0}
diarize_calls = [c for c in rig.calls() if c.startswith("diarize")]
assert "b-first" in diarize_calls[0] and "a-second" in diarize_calls[1]
class TestDrainBoundary:
def test_transcribe_worker_drain_empty_queue_is_a_no_op(self, rig):
"""Boundary: nothing to do, and the folders get created."""
assert worker.drain(rig.config) == {"done": 0, "failed": 0}
assert (rig.home / "incoming").is_dir() and (rig.home / "done").is_dir()
def test_transcribe_worker_drain_ignores_uploads_still_in_flight(self, rig):
"""Boundary: a dot-prefixed folder is left alone (second line of defence behind uploading/)."""
rig.submit(".tmp-job9")
assert worker.drain(rig.config) == {"done": 0, "failed": 0}
assert (rig.home / "incoming" / ".tmp-job9").exists()
def test_transcribe_worker_drain_job_already_done_is_not_rerun(self, rig):
"""Boundary: resubmitting finished work costs nothing."""
(rig.home / "done").mkdir(parents=True)
(rig.home / "done" / "job1.txt").write_text("already here\n")
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 0}
assert (rig.home / "done" / "job1.txt").read_text() == "already here\n"
assert not (rig.home / "incoming" / "job1").exists()
assert rig.calls() == []
def test_transcribe_worker_drain_missing_job_file_uses_defaults(self, rig):
"""Boundary: audio with no job.json still transcribes, in English, count estimated."""
rig.submit("job1", job=False)
assert worker.drain(rig.config) == {"done": 1, "failed": 0}
def test_transcribe_worker_drain_retries_a_job_left_in_work_by_a_crash(self, rig):
"""Boundary: a job stranded in work/ is picked up again."""
rig.submit("job1", where="work")
assert worker.drain(rig.config) == {"done": 1, "failed": 0}
def test_transcribe_worker_drain_gives_up_on_a_job_that_keeps_crashing(self, rig):
"""Boundary: the second stranding is a failure, not a loop."""
rig.submit("job1", {"language": "en", "attempts": 2}, where="work")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert "attempt" in (rig.home / "failed" / "job1.log").read_text()
class TestDrainError:
def test_transcribe_worker_drain_whisper_failure_is_logged_with_its_stage(self, rig):
"""Error: whisper exits non-zero."""
rig.monkeypatch.setenv("FAKE_WHISPER_EXIT", "3")
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
log = (rig.home / "failed" / "job1.log").read_text()
assert "whisper" in log
assert not (rig.home / "done" / "job1.txt").exists()
assert not (rig.home / "work" / "job1").exists()
def test_transcribe_worker_drain_diarizer_failure_keeps_its_stderr(self, rig):
"""Error: the diarizer fails; its own words land in the log."""
rig.monkeypatch.setenv("FAKE_DIARIZE_EXIT", "1")
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
log = (rig.home / "failed" / "job1.log").read_text()
assert "diarize" in log and "fake diarizer says hello" in log
def test_transcribe_worker_drain_looping_transcription_fails(self, rig):
"""Error: whisper's repetition loop is a failed job, never a transcript."""
rig.set_whisper(whisper_json(*("we do not know where we are going".split() * 5), step_ms=1000)) # 40 s
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert "looped" in (rig.home / "failed" / "job1.log").read_text()
assert not (rig.home / "done" / "job1.txt").exists()
def test_transcribe_worker_drain_short_loop_is_collapsed_and_recorded(self, rig):
"""Normal: a stutter of a few seconds is collapsed to one copy and noted in the metadata."""
rig.set_whisper(whisper_json("Okay,", *("fair, it's not going to be".split() * 4), "easy."))
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 1, "failed": 0}
assert (rig.home / "done" / "job1.txt").read_text() == "00:00:00 Speaker A: Okay, fair, it's not going to be easy.\n"
meta = json.loads((rig.home / "done" / "job1.json").read_text())
assert meta["loops_collapsed"] == [["fair, it's not going to be", 4]]
def test_transcribe_worker_drain_malformed_job_file_fails_that_job_only(self, rig):
"""Error: one bad job does not stop the queue."""
bad = rig.submit("bad")
(bad / "job.json").write_text("{not json")
os.utime(bad, (1, 1))
rig.submit("good")
assert worker.drain(rig.config) == {"done": 1, "failed": 1}
assert (rig.home / "done" / "good.txt").exists()
assert "job.json" in (rig.home / "failed" / "bad.log").read_text()
def test_transcribe_worker_drain_job_without_audio_fails(self, rig):
"""Error: a job folder holding no audio file."""
folder = rig.submit("job1")
(folder / "audio.m4a").unlink()
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert "audio" in (rig.home / "failed" / "job1.log").read_text()
@pytest.mark.parametrize("bad", [{"speakers": 0}, {"speakers": "three"}, {"language": "en; rm -rf"}])
def test_transcribe_worker_drain_rejects_bad_option_values(self, rig, bad):
"""Error: options are validated before they reach a command line."""
rig.submit("job1", bad)
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert rig.calls() == []
class TestDrainSilenceHallucinations:
def _silent_stretch(self, rig):
spoken = [{"offsets": {"from": 0, "to": 900}, "text": " Good morning."}]
invented = [
{"offsets": {"from": 60_000 + i * 5_000, "to": 64_000 + i * 5_000}, "text": " Thank you."}
for i in range(12)
]
rig.set_whisper({"transcription": spoken + invented})
rig.turns_json.write_text(json.dumps([{"start": 0.0, "end": 2.0, "speaker": "SPEAKER_00"}]))
def test_transcribe_worker_drain_words_invented_in_silence_are_dropped_not_failed(self, rig):
"""Normal: a quiet meeting transcribes; the invented run never reaches the transcript."""
self._silent_stretch(rig)
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 1, "failed": 0}
assert (rig.home / "done" / "job1.txt").read_text() == "00:00:00 Speaker A: Good morning.\n"
def test_transcribe_worker_drain_metadata_counts_the_dropped_words(self, rig):
"""Normal: the count is on record, so a transcript that lost a lot is visible."""
self._silent_stretch(rig)
rig.submit("job1")
worker.drain(rig.config)
assert json.loads((rig.home / "done" / "job1.json").read_text())["dropped_outside_speech"] == 12
def test_transcribe_worker_drain_recording_with_no_speech_at_all_fails_cleanly(self, rig):
"""Error: everything whisper produced sits in silence."""
rig.set_whisper({"transcription": [{"offsets": {"from": 60_000, "to": 61_000}, "text": " Thank you."}]})
rig.turns_json.write_text(json.dumps([{"start": 0.0, "end": 2.0, "speaker": "SPEAKER_00"}]))
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert "no speech" in (rig.home / "failed" / "job1.log").read_text()
class TestRunStdin:
def test_transcribe_worker_run_gives_tools_no_stdin(self, tmp_path):
"""Boundary: ffmpeg reads stdin when it can, which would eat a calling loop's input."""
out = tmp_path / "stdin-target"
read_end, write_end = os.pipe()
saved = os.dup(0)
os.dup2(read_end, 0)
try:
worker._run("probe", ["bash", "-c", f"readlink /proc/self/fd/0 > {out}"])
finally:
os.dup2(saved, 0)
for fd in (saved, read_end, write_end):
os.close(fd)
assert out.read_text().strip() == "/dev/null"
class TestDrainResilience:
def test_transcribe_worker_drain_null_attempts_counts_as_a_first_try(self, rig):
"""Error: a job.json with "attempts": null must not take the worker down; it is a first try."""
odd = rig.submit("odd", {"language": "en", "attempts": None})
os.utime(odd, (1, 1))
rig.submit("good")
assert worker.drain(rig.config) == {"done": 2, "failed": 0}
assert (rig.home / "done" / "odd.txt").exists()
assert (rig.home / "done" / "good.txt").exists()
def test_transcribe_worker_drain_unexpected_error_in_one_job_is_logged_and_the_queue_goes_on(self, rig):
"""Error: an exception the pipeline never anticipated lands in failed/ with its type, not on the run."""
first = rig.submit("first")
os.utime(first, (1, 1))
rig.submit("second")
def explode(job: dict) -> list[str]:
raise RuntimeError("boom")
rig.monkeypatch.setattr(worker, "_diarize_options", explode)
assert worker.drain(rig.config) == {"done": 0, "failed": 2} # both reached, neither crashed the run
for job_id in ("first", "second"):
log = (rig.home / "failed" / f"{job_id}.log").read_text()
assert "RuntimeError" in log and "boom" in log
assert not (rig.home / "work" / job_id).exists()
def test_transcribe_worker_drain_unwritable_done_dir_is_a_failed_job_not_a_crash(self, rig):
"""Error: an OSError while writing the transcript lands in failed/, and the run survives."""
if os.geteuid() == 0:
pytest.skip("root ignores directory permissions")
(rig.home / "done").mkdir(parents=True)
(rig.home / "done").chmod(0o500)
try:
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert "job1" in "".join(p.name for p in (rig.home / "failed").iterdir())
finally:
(rig.home / "done").chmod(0o700)
def test_transcribe_worker_drain_ffmpeg_failure_is_logged_with_its_stage(self, rig):
"""Error: ffmpeg exits non-zero."""
rig.monkeypatch.setenv("FAKE_FFMPEG_EXIT", "2")
rig.submit("job1")
assert worker.drain(rig.config) == {"done": 0, "failed": 1}
assert "ffmpeg" in (rig.home / "failed" / "job1.log").read_text()
def test_transcribe_worker_drain_returns_at_once_when_another_worker_holds_the_lock(self, rig):
"""Boundary: a second drain does not touch the queue while the first holds the lock."""
import fcntl
rig.home.mkdir(parents=True, exist_ok=True)
rig.submit("job1")
with open(rig.home / "lock", "w", encoding="utf-8") as held:
fcntl.flock(held, fcntl.LOCK_EX | fcntl.LOCK_NB)
assert worker.drain(rig.config) == {"done": 0, "failed": 0}
assert (rig.home / "incoming" / "job1").exists()
assert rig.calls() == []
|