mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 00:55:14 -07:00
fix(transcription): transcode uploads to WAV before STT (#957)
The /transcribe endpoint passed the raw uploaded file straight to the STT backend (mlx_audio.stt -> miniaudio), which only decodes WAV/FLAC/MP3/Vorbis. Browser recordings arrive as WebM/Opus (Chrome/Firefox MediaRecorder), so web-mode dictation failed with 500 "unsupported file format". The Tauri app was unaffected because WebKit produces MP4. librosa already fully decodes the upload to compute duration (falling back to audioread/ffmpeg for exotic containers), so re-encode that PCM to a temp WAV and hand it to Whisper. WAV inputs pass through unchanged; the temp file is cleaned up in the finally block. Co-authored-by: Claude Opus 4.8 <[email protected]>
This commit is contained in:
committed by
Jamie Pine
co-authored by
Claude Opus 4.8
parent
eab3d45192
commit
e70e639838
@@ -15,6 +15,10 @@ router = APIRouter()
|
|||||||
|
|
||||||
UPLOAD_CHUNK_SIZE = 1024 * 1024 # 1MB
|
UPLOAD_CHUNK_SIZE = 1024 * 1024 # 1MB
|
||||||
|
|
||||||
|
# Same set profiles.py accepts for voice samples. librosa picks its decoder from the
|
||||||
|
# file extension, so the temp file has to keep the uploaded one.
|
||||||
|
ALLOWED_AUDIO_EXTS = {".wav", ".mp3", ".m4a", ".ogg", ".flac", ".aac", ".webm", ".opus"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/transcribe", response_model=models.TranscriptionResponse)
|
@router.post("/transcribe", response_model=models.TranscriptionResponse)
|
||||||
async def transcribe_audio(
|
async def transcribe_audio(
|
||||||
@@ -23,18 +27,33 @@ async def transcribe_audio(
|
|||||||
model: str | None = Form(None),
|
model: str | None = Form(None),
|
||||||
):
|
):
|
||||||
"""Transcribe audio file to text."""
|
"""Transcribe audio file to text."""
|
||||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
|
uploaded_ext = Path(file.filename or "").suffix.lower()
|
||||||
|
file_suffix = uploaded_ext if uploaded_ext in ALLOWED_AUDIO_EXTS else ".wav"
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=file_suffix, delete=False) as tmp:
|
||||||
while chunk := await file.read(UPLOAD_CHUNK_SIZE):
|
while chunk := await file.read(UPLOAD_CHUNK_SIZE):
|
||||||
tmp.write(chunk)
|
tmp.write(chunk)
|
||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
stt_path = tmp_path
|
||||||
try:
|
try:
|
||||||
|
from ..utils.audio import load_audio, save_audio
|
||||||
from ..backends import WHISPER_HF_REPOS
|
from ..backends import WHISPER_HF_REPOS
|
||||||
from ..utils.audio import load_audio
|
|
||||||
|
|
||||||
audio, sr = await asyncio.to_thread(load_audio, tmp_path)
|
audio, sr = await asyncio.to_thread(load_audio, tmp_path)
|
||||||
duration = len(audio) / sr
|
duration = len(audio) / sr
|
||||||
|
|
||||||
|
# The STT backend (mlx_audio.stt -> miniaudio) only decodes
|
||||||
|
# WAV/FLAC/MP3/Vorbis, so browser recordings uploaded as WebM/Opus
|
||||||
|
# fail with "unsupported file format" (issue: web-mode dictation).
|
||||||
|
# librosa already decoded the file above (it falls back to
|
||||||
|
# audioread/ffmpeg for exotic containers), so re-encode that PCM to a
|
||||||
|
# temp WAV and hand *that* to Whisper. WAV inputs pass through
|
||||||
|
# unchanged.
|
||||||
|
if file_suffix != ".wav":
|
||||||
|
stt_path = f"{tmp_path}.stt.wav"
|
||||||
|
await asyncio.to_thread(save_audio, audio, stt_path, sr)
|
||||||
|
|
||||||
whisper_model = transcribe.get_whisper_model()
|
whisper_model = transcribe.get_whisper_model()
|
||||||
model_size = model if model else whisper_model.model_size
|
model_size = model if model else whisper_model.model_size
|
||||||
|
|
||||||
@@ -69,7 +88,7 @@ async def transcribe_audio(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
text = await whisper_model.transcribe(tmp_path, language, model_size)
|
text = await whisper_model.transcribe(stt_path, language, model_size)
|
||||||
|
|
||||||
return models.TranscriptionResponse(
|
return models.TranscriptionResponse(
|
||||||
text=text,
|
text=text,
|
||||||
@@ -82,3 +101,5 @@ async def transcribe_audio(
|
|||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
finally:
|
finally:
|
||||||
Path(tmp_path).unlink(missing_ok=True)
|
Path(tmp_path).unlink(missing_ok=True)
|
||||||
|
if stt_path != tmp_path:
|
||||||
|
Path(stt_path).unlink(missing_ok=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user