fix: voice pipeline hardening — 7 bug fixes with tests

1. Anthropic + ElevenLabs TTS silence: forward full response to TTS
   callback for non-streaming providers (choices first, then native
   content blocks fallback).

2. Subprocess timeout kill: play_audio_file now kills the process on
   TimeoutExpired instead of leaving zombie processes.

3. Discord disconnect cleanup: leave all voice channels before closing
   the client to prevent leaked state.

4. Audio stream leak: close InputStream if stream.start() fails.

5. Race condition: read/write _on_silence_stop under lock in audio
   callback thread.

6. _vprint force=True: show API error, retry, and truncation messages
   even during streaming TTS.

7. _refresh_level lock: read _voice_recording under _voice_lock.
This commit is contained in:
0xbyt4 2026-03-14 13:06:49 +03:00
parent 7a24168080
commit eb34c0b09a
8 changed files with 317 additions and 10 deletions

View file

@ -1194,3 +1194,40 @@ class TestVoiceStopAndTranscribeReal:
cli = _make_voice_cli(_voice_recording=True, _voice_recorder=recorder)
cli._voice_stop_and_transcribe()
mock_tr.assert_called_once_with("/tmp/test.wav", model="whisper-large-v3")
# ---------------------------------------------------------------------------
# Bugfix: _refresh_level must read _voice_recording under lock
# ---------------------------------------------------------------------------
class TestRefreshLevelLock:
"""Bug: _refresh_level thread read _voice_recording without lock."""
def test_refresh_stops_when_recording_false(self):
import threading, time
lock = threading.Lock()
recording = True
iterations = 0
def refresh_level():
nonlocal iterations
while True:
with lock:
still = recording
if not still:
break
iterations += 1
time.sleep(0.01)
t = threading.Thread(target=refresh_level, daemon=True)
t.start()
time.sleep(0.05)
with lock:
recording = False
t.join(timeout=1)
assert not t.is_alive(), "Refresh thread did not stop"
assert iterations > 0, "Refresh thread never ran"