Merge listen-robustness: transcription retries + missing-mic message

This commit is contained in:
Ryan Malloy 2026-07-04 13:22:45 -06:00
commit 177bf9f7cc
2 changed files with 66 additions and 32 deletions

View File

@ -217,6 +217,27 @@ async def _force_pwplay_volume_100() -> None:
return return
async def _terminate_quietly(proc) -> None:
"""Stop a recorder subprocess that may have ALREADY exited.
When the mic is disconnected, pw-record exits instantly a later
proc.terminate() then raises ProcessLookupError, which otherwise masks the
friendly "no audio captured / is a mic connected?" error. Swallow that.
"""
try:
proc.terminate()
except ProcessLookupError:
return
try:
await asyncio.wait_for(proc.wait(), timeout=2.0)
except asyncio.TimeoutError:
try:
proc.kill()
except ProcessLookupError:
return
await proc.wait()
async def record_audio_until_silence( async def record_audio_until_silence(
out_path: Path, out_path: Path,
silence_threshold_ms: int = 1500, silence_threshold_ms: int = 1500,
@ -322,12 +343,7 @@ async def record_audio_until_silence(
# The inner read returned no data — break outer loop # The inner read returned no data — break outer loop
break break
finally: finally:
proc.terminate() await _terminate_quietly(proc)
try:
await asyncio.wait_for(proc.wait(), timeout=2.0)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
if not pcm_buf: if not pcm_buf:
raise PlaybackError( raise PlaybackError(
@ -414,14 +430,12 @@ async def record_audio(
# If we get here, pw-record exited early (mic gone, permission, etc.) # If we get here, pw-record exited early (mic gone, permission, etc.)
except asyncio.TimeoutError: except asyncio.TimeoutError:
# Expected path — stop recording cleanly. # Expected path — stop recording cleanly.
proc.terminate() await _terminate_quietly(proc)
try:
await asyncio.wait_for(proc.wait(), timeout=2.0)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
except asyncio.CancelledError: except asyncio.CancelledError:
proc.kill() try:
proc.kill()
except ProcessLookupError:
pass
await proc.wait() await proc.wait()
raise raise
@ -482,14 +496,12 @@ async def _record_fixed_stream(
try: try:
await asyncio.wait_for(_drain(), timeout=duration_seconds) await asyncio.wait_for(_drain(), timeout=duration_seconds)
except asyncio.TimeoutError: except asyncio.TimeoutError:
proc.terminate() await _terminate_quietly(proc)
try:
await asyncio.wait_for(proc.wait(), timeout=2.0)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
except asyncio.CancelledError: except asyncio.CancelledError:
proc.kill() try:
proc.kill()
except ProcessLookupError:
pass
await proc.wait() await proc.wait()
raise raise

View File

@ -43,6 +43,10 @@ OUTPUT_DIR_CONTAINER = Path("/output")
# the discipline _resolve_output_path uses for writes. # the discipline _resolve_output_path uses for writes.
_TRANSCRIBE_INPUT_DIRS = (Path("/output"), Path("/tmp/mcspeak")) _TRANSCRIBE_INPUT_DIRS = (Path("/output"), Path("/tmp/mcspeak"))
# listen() retries transcription this many times (the Parakeet gateway
# intermittently times out; the recording is saved, so retrying is free).
_LISTEN_TRANSCRIBE_ATTEMPTS = 3
def _validate_readable_audio_path(audio_path: str) -> Path: def _validate_readable_audio_path(audio_path: str) -> Path:
"""Resolve audio_path against the allowed input dirs and check existence. """Resolve audio_path against the allowed input dirs and check existence.
@ -893,19 +897,37 @@ async def listen(
await ctx.info(f"Recording saved failed (non-fatal): {e}") await ctx.info(f"Recording saved failed (non-fatal): {e}")
await ctx.info(f"Transcribing {rec_path.name}...") await ctx.info(f"Transcribing {rec_path.name}...")
try: # The Parakeet gateway serializes inference on one slot and intermittently
result = await transcribe_audio( # times out when the backend is busy/cold. The recording is already saved,
rec_path, # so retry a couple of times before giving up rather than making the caller
response_format=response_format, # re-run transcribe() by hand.
timestamp_granularities=timestamp_granularities, result = None
diarize=diarize, last_err: TranscriptionError | None = None
num_speakers=num_speakers, for attempt in range(_LISTEN_TRANSCRIBE_ATTEMPTS):
punctuation=punctuation, try:
min_confidence=min_confidence, result = await transcribe_audio(
) rec_path,
except TranscriptionError as e: response_format=response_format,
timestamp_granularities=timestamp_granularities,
diarize=diarize,
num_speakers=num_speakers,
punctuation=punctuation,
min_confidence=min_confidence,
)
break
except TranscriptionError as e:
last_err = e
if attempt < _LISTEN_TRANSCRIBE_ATTEMPTS - 1:
await ctx.info(
f"Transcription attempt {attempt + 1} failed ({e}); retrying..."
)
await asyncio.sleep(0.75)
if result is None:
return { return {
"error": str(e), "error": (
f"Transcription failed after {_LISTEN_TRANSCRIBE_ATTEMPTS} attempts: "
f"{last_err}"
),
"recorded": str(rec_path), "recorded": str(rec_path),
"saved_to": saved_to, "saved_to": saved_to,
} }