Skip to content

Commit 08cdd5a

Browse files
zaidwhyclaude
andcommitted
voice endpoints: pre-validate audio with av.open, 400 on undecodable or silent input
Feeding non-audio bytes into whisper could hard-crash the worker (not a catchable exception), which surfaced to DreamOS as a dead request / 500. Both voice endpoints now decode-check first and return a friendly 400; empty transcriptions also 400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTdT6AcZMfnUfhiNA92T5e
1 parent 97c746a commit 08cdd5a

1 file changed

Lines changed: 31 additions & 2 deletions

File tree

  • src/personal_llm/interfaces

src/personal_llm/interfaces/api.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,41 @@ def integrations_sync_endpoint(req: IntegrationsSyncRequest) -> SyncResult:
125125
return sync_external_items(engine.store, engine.vectors, engine.router, req.items)
126126

127127

128+
def _transcribe_or_400(engine, tmp_path: str) -> str:
129+
"""Undecodable or silent audio must be a clear 400, never a crashed request.
130+
131+
The av.open pre-check matters: feeding non-audio bytes straight into whisper can
132+
hard-crash the worker process (not a catchable exception), so validate first.
133+
"""
134+
try:
135+
import av
136+
137+
with av.open(tmp_path) as container:
138+
if not container.streams.audio:
139+
raise ValueError("no audio stream")
140+
except Exception:
141+
raise HTTPException(
142+
status_code=400,
143+
detail="That did not decode as audio - record a bit longer and try again.",
144+
)
145+
try:
146+
text = engine.stt.transcribe(tmp_path)
147+
except Exception as exc:
148+
raise HTTPException(
149+
status_code=400,
150+
detail=f"Could not decode the audio - record a bit longer and try again. ({type(exc).__name__})",
151+
)
152+
if not text.strip():
153+
raise HTTPException(status_code=400, detail="I heard nothing - hold to talk, speak, then release.")
154+
return text
155+
156+
128157
@app.post("/voice/transcribe")
129158
async def voice_transcribe_endpoint(file: UploadFile = File(...)) -> dict:
130159
engine = build_engine()
131160
tmp_path = await _save_upload_to_temp(file)
132161
try:
133-
return {"text": engine.stt.transcribe(tmp_path)}
162+
return {"text": _transcribe_or_400(engine, tmp_path)}
134163
finally:
135164
Path(tmp_path).unlink(missing_ok=True)
136165

@@ -140,7 +169,7 @@ async def voice_ask_endpoint(file: UploadFile = File(...), verify: bool = False)
140169
engine = build_engine()
141170
tmp_path = await _save_upload_to_temp(file)
142171
try:
143-
question = engine.stt.transcribe(tmp_path)
172+
question = _transcribe_or_400(engine, tmp_path)
144173
finally:
145174
Path(tmp_path).unlink(missing_ok=True)
146175
try:

0 commit comments

Comments
 (0)