Fix speaker-track readiness race: skip mixed-file wait when tracks exist

This commit is contained in:
2026-06-15 12:06:17 +00:00
parent 8fd5e5b464
commit 12f35ccdc3
4 changed files with 88 additions and 1 deletions
+10 -1
View File
@@ -287,7 +287,16 @@ async def process_recording(
) )
return return
if not await wait_for_file_ready(file_path): # Speaker tracks are written and finalized independently in MeetingSink
# cleanup — they always have valid WAV headers if they exist. Check
# them first so we can skip the mixed-file "ready" wait (which is prone
# to timing races with the SilenceGenerator shutdown path).
if _load_speaker_tracks(file_path):
logger.info(
"Speaker tracks present for guild_id=%s; skipping mixed-file readiness check",
guild_id,
)
elif not await wait_for_file_ready(file_path):
reason = "recording file was not finalized in time" reason = "recording file was not finalized in time"
await _save_retry_state(guild_id, file_path, fallback_channel_id, reason) await _save_retry_state(guild_id, file_path, fallback_channel_id, reason)
await safe_send_chunked( await safe_send_chunked(
+3
View File
@@ -111,6 +111,9 @@ class MeetingSink(voice_recv.AudioSink):
return track return track
def write(self, user, data) -> None: def write(self, user, data) -> None:
if self._cleaned_up:
return
pcm = getattr(data, "pcm", b"") or b"" pcm = getattr(data, "pcm", b"") or b""
# Mixed archive always gets every frame (including silence) so the # Mixed archive always gets every frame (including silence) so the
+53
View File
@@ -200,3 +200,56 @@ def test_transcribe_recording_falls_back_to_single_file_for_old_recordings(tmp_p
assert result == "single transcript" assert result == "single transcript"
assert calls == [("single", str(meeting))] assert calls == [("single", str(meeting))]
def test_process_recording_skips_ready_wait_when_tracks_exist(tmp_path, monkeypatch):
"""When speaker tracks exist, process_recording should not call wait_for_file_ready."""
meeting = tmp_path / "meeting.wav"
meeting.write_bytes(b"RIFFdemo")
tracks_dir = tmp_path / "tracks"
tracks_dir.mkdir()
alice = tracks_dir / "track-111.wav"
alice.write_bytes(b"alice audio")
(tracks_dir / "tracks.json").write_text(
'{"tracks":[{"display_name":"Alice","path":"' + str(alice) + '"}]}'
)
calls = []
async def fake_transcribe_recording(path, guild_id):
calls.append(("transcribe", path))
return "[Alice]\nhello"
async def fake_summarize(transcript):
calls.append(("summarize", transcript))
return "Summary text."
async def fake_get_output_channel(guild_id):
return 999
async def fake_clear_retry_state(_guild_id):
pass
async def fake_resolve_text_channel(_channel_id):
return SimpleNamespace(id=999, send=lambda t: None)
async def fake_safe_send_chunked(_channel, text, **kw):
calls.append(("delivered", text[:30]))
return True
monkeypatch.setattr(bot, "_transcribe_recording", fake_transcribe_recording)
monkeypatch.setattr(bot, "summarize", fake_summarize)
monkeypatch.setattr(bot, "wait_for_file_ready", lambda *a, **kw: calls.append(("ready",)) or True)
monkeypatch.setattr(bot, "safe_send_chunked", fake_safe_send_chunked)
monkeypatch.setattr(bot, "config", SimpleNamespace(
get_output_channel=fake_get_output_channel,
clear_retry_state=fake_clear_retry_state,
set_retry_state=lambda g, p, c, r: None,
))
monkeypatch.setattr(bot, "resolve_text_channel", fake_resolve_text_channel)
asyncio.run(bot.process_recording(str(meeting), 123, 456, None))
# wait_for_file_ready should NOT have been called
assert ("ready",) not in calls, "wait_for_file_ready was called despite tracks existing"
assert calls[:2] == [("transcribe", str(meeting)),
("summarize", "[Alice]\nhello")]
+22
View File
@@ -92,3 +92,25 @@ def test_meeting_sink_ignores_silence_only_and_unknown_users(tmp_path):
assert manifest["tracks"] == [] assert manifest["tracks"] == []
# Mixed archive still finalized and valid. # Mixed archive still finalized and valid.
assert _read_wav_frames(mixed) == SILENCE * 2 assert _read_wav_frames(mixed) == SILENCE * 2
def test_meeting_sink_ignores_writes_after_cleanup(tmp_path):
"""Verify that writes after cleanup() are silently dropped."""
mixed = tmp_path / "meeting.wav"
tracks_dir = tmp_path / "tracks"
sink = recording.MeetingSink(str(mixed), str(tracks_dir))
alice = FakeUser(111, display_name="Alice")
sink.write(alice, FakeData(SPEECH))
sink.cleanup()
# These writes should be dropped, not cause crashes or corrupt the files.
sink.write(alice, FakeData(SPEECH))
sink.write(None, FakeData(SILENCE))
# Mixed file should contain only the pre-cleanup frame.
assert _read_wav_frames(mixed) == SPEECH
assert _read_wav_frames(tracks_dir / "track-111.wav") == SPEECH
# Manifest should still reference Alice.
manifest = json.loads((tracks_dir / "tracks.json").read_text())
assert len(manifest["tracks"]) == 1