rohitsar567 Claude Opus 4.7 (1M context) commited on
Commit
a777198
·
1 Parent(s): f2aff19

fix(stt): transcode browser webm/opus → wav before Sarvam (D-024)

Browse files

Root cause: Browser MediaRecorder emits audio/webm;codecs=opus by default.
Sarvam STT only accepts WAV/MP3/FLAC/OGG/M4A. The /api/transcribe endpoint
silently coerced webm→wav in the Content-Type but uploaded the raw webm
bytes, so Sarvam returned HTTP 400 Bad Request:

STT failed: HTTPStatusError: Client error '400 Bad Request' for url
'https://api.sarvam.ai/speech-to-text'

Fix in three layers:

1) sarvam_stt.py — _transcode_to_wav() helper using pydub (ffmpeg-backed)
converts any non-native container to 16 kHz mono WAV before upload.
Also short-audio guard: <1 KB bytes return empty STTResult instead of
500ing on Sarvam's silence rejection.

2) main.py — extend the format whitelist to include webm/opus/mp4 so the
real extension is passed through to the provider (provider then decides
to transcode).

3) Dockerfile — add ffmpeg apt package (pydub's runtime dependency).
requirements.txt — add pydub==0.25.1.

This is the first of three live-product issues surfaced today:
Issue 3 (this commit): STT 400 errors
Issue 2 + 4 (next): fact-find recording STT garbage as profile fields
AND character-splitting health_conditions list
Issue 1 (last): full-duplex barge-in / interrupt-while-speaking

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Dockerfile CHANGED
@@ -25,12 +25,15 @@ RUN npm run build
25
  FROM python:3.11-slim
26
  WORKDIR /app
27
 
28
- # System deps for pdfplumber + torch CPU + sentence-transformers
 
 
29
  RUN apt-get update && apt-get install -y --no-install-recommends \
30
  build-essential \
31
  libpoppler-cpp-dev \
32
  pkg-config \
33
  poppler-utils \
 
34
  && rm -rf /var/lib/apt/lists/*
35
 
36
  # Install Python deps
 
25
  FROM python:3.11-slim
26
  WORKDIR /app
27
 
28
+ # System deps:
29
+ # pdfplumber + torch CPU + sentence-transformers → build-essential, libpoppler
30
+ # pydub (webm→wav transcode for Sarvam STT) → ffmpeg
31
  RUN apt-get update && apt-get install -y --no-install-recommends \
32
  build-essential \
33
  libpoppler-cpp-dev \
34
  pkg-config \
35
  poppler-utils \
36
+ ffmpeg \
37
  && rm -rf /var/lib/apt/lists/*
38
 
39
  # Install Python deps
backend/main.py CHANGED
@@ -233,10 +233,12 @@ async def transcribe(
233
  t0 = time.time()
234
  audio_bytes = await file.read()
235
  ext = (file.filename or "audio.wav").rsplit(".", 1)[-1].lower()
 
 
236
  try:
237
  result = await get_stt().transcribe(
238
  audio_bytes=audio_bytes,
239
- audio_format=ext if ext in ("wav", "mp3", "flac", "ogg", "m4a") else "wav",
240
  language_code=language_code,
241
  )
242
  except Exception as e:
 
233
  t0 = time.time()
234
  audio_bytes = await file.read()
235
  ext = (file.filename or "audio.wav").rsplit(".", 1)[-1].lower()
236
+ # Pass the real extension through; sarvam_stt.py transcodes non-native
237
+ # containers (webm/opus from browser MediaRecorder) to WAV before upload.
238
  try:
239
  result = await get_stt().transcribe(
240
  audio_bytes=audio_bytes,
241
+ audio_format=ext if ext in ("wav", "mp3", "flac", "ogg", "m4a", "webm", "opus", "mp4") else "wav",
242
  language_code=language_code,
243
  )
244
  except Exception as e:
backend/providers/sarvam_stt.py CHANGED
@@ -32,15 +32,55 @@ class SarvamSTT(STTProvider):
32
  if not self.api_key:
33
  raise RuntimeError("SARVAM_API_KEY not set in .env")
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  async def transcribe(
36
  self,
37
  audio_bytes: bytes,
38
  audio_format: str = "wav",
39
  language_code: Optional[str] = None,
40
  ) -> STTResult:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  url = f"{settings.SARVAM_BASE_URL}{settings.SARVAM_STT_PATH}"
42
  files = {
43
- "file": (f"audio.{audio_format}", io.BytesIO(audio_bytes), f"audio/{audio_format}"),
44
  }
45
  data = {"model": self.model}
46
  if language_code:
 
32
  if not self.api_key:
33
  raise RuntimeError("SARVAM_API_KEY not set in .env")
34
 
35
+ # Sarvam STT accepts these container formats per their API. webm is NOT
36
+ # in this list — browser MediaRecorder defaults to webm/opus, so we
37
+ # transcode webm → wav (16kHz mono) on the fly via pydub before calling
38
+ # Sarvam. This is the fix for the live "400 Bad Request" we hit when
39
+ # the browser's webm bytes were uploaded as if they were wav.
40
+ _SARVAM_NATIVE_FORMATS = {"wav", "mp3", "flac", "ogg", "m4a"}
41
+
42
+ @staticmethod
43
+ def _transcode_to_wav(audio_bytes: bytes, src_format: str) -> bytes:
44
+ """Convert any pydub-readable container to 16 kHz mono WAV.
45
+
46
+ Sarvam's recommended sampling rate is 16 kHz mono — what Saarika
47
+ was trained on. Down-mixing + resampling at the gateway also
48
+ prevents Sarvam from doing it server-side, which keeps latency tight.
49
+ """
50
+ from pydub import AudioSegment
51
+ audio = AudioSegment.from_file(io.BytesIO(audio_bytes), format=src_format)
52
+ audio = audio.set_frame_rate(16000).set_channels(1).set_sample_width(2)
53
+ buf = io.BytesIO()
54
+ audio.export(buf, format="wav")
55
+ return buf.getvalue()
56
+
57
  async def transcribe(
58
  self,
59
  audio_bytes: bytes,
60
  audio_format: str = "wav",
61
  language_code: Optional[str] = None,
62
  ) -> STTResult:
63
+ if not audio_bytes or len(audio_bytes) < 1024:
64
+ # < 1 KB audio is almost certainly silence or a record-and-immediately-stop;
65
+ # Sarvam 400s on these. Surface a clean empty result instead of a 500.
66
+ return STTResult(text="", language_code=language_code, confidence=0.0, raw={"reason": "audio_too_short"})
67
+
68
+ fmt = (audio_format or "wav").lower().lstrip(".")
69
+ # Browser MediaRecorder uses webm/opus by default; Sarvam rejects it.
70
+ # Transcode in-process when we get a non-native format.
71
+ if fmt not in self._SARVAM_NATIVE_FORMATS:
72
+ try:
73
+ audio_bytes = self._transcode_to_wav(audio_bytes, fmt)
74
+ fmt = "wav"
75
+ except Exception as e:
76
+ # If pydub/ffmpeg fails (e.g., truly corrupt audio), let Sarvam
77
+ # see the original bytes and return its own error rather than
78
+ # silently swallowing.
79
+ pass
80
+
81
  url = f"{settings.SARVAM_BASE_URL}{settings.SARVAM_STT_PATH}"
82
  files = {
83
+ "file": (f"audio.{fmt}", io.BytesIO(audio_bytes), f"audio/{fmt}"),
84
  }
85
  data = {"model": self.model}
86
  if language_code:
requirements.txt CHANGED
@@ -26,6 +26,11 @@ duckdb==1.1.3
26
  sentence-transformers==5.1.2
27
  torch==2.11.0 # CPU-only fine for our usage
28
 
 
 
 
 
 
29
  # Utility
30
  numpy==1.26.4
31
  tqdm==4.66.6
 
26
  sentence-transformers==5.1.2
27
  torch==2.11.0 # CPU-only fine for our usage
28
 
29
+ # Audio transcoding — browser MediaRecorder emits webm/opus by default;
30
+ # Sarvam STT accepts WAV/MP3/FLAC/OGG/M4A but NOT webm. pydub (ffmpeg-backed)
31
+ # converts in-process. The Dockerfile installs the ffmpeg apt package.
32
+ pydub==0.25.1
33
+
34
  # Utility
35
  numpy==1.26.4
36
  tqdm==4.66.6