Fix speaker-track readiness race: skip mixed-file wait when tracks exist
This commit is contained in:
@@ -287,7 +287,16 @@ async def process_recording(
|
||||
)
|
||||
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"
|
||||
await _save_retry_state(guild_id, file_path, fallback_channel_id, reason)
|
||||
await safe_send_chunked(
|
||||
|
||||
@@ -111,6 +111,9 @@ class MeetingSink(voice_recv.AudioSink):
|
||||
return track
|
||||
|
||||
def write(self, user, data) -> None:
|
||||
if self._cleaned_up:
|
||||
return
|
||||
|
||||
pcm = getattr(data, "pcm", b"") or b""
|
||||
|
||||
# Mixed archive always gets every frame (including silence) so the
|
||||
|
||||
@@ -200,3 +200,56 @@ def test_transcribe_recording_falls_back_to_single_file_for_old_recordings(tmp_p
|
||||
|
||||
assert result == "single transcript"
|
||||
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")]
|
||||
|
||||
@@ -92,3 +92,25 @@ def test_meeting_sink_ignores_silence_only_and_unknown_users(tmp_path):
|
||||
assert manifest["tracks"] == []
|
||||
# Mixed archive still finalized and valid.
|
||||
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
|
||||
Reference in New Issue
Block a user