Merge pull request #2042 from debpalash/fix/transcribe-m4a

fix(asr): PyTorch Whisper transcribes M4A through the ffmpeg fallback
This commit is contained in:
Palash Debnath
2026-09-10 15:51:53 -07:00
committed by GitHub
3 changed files with 100 additions and 1 deletions
+1
View File
@@ -26,6 +26,7 @@ the frozen-backend fallback mirror it for their toolchains.
- Stopping a process on macOS no longer fails with "Operation not permitted" when it was already exiting (#2032)
- A YouTube link blocked by its "not a bot" check now says how to attach signed-in cookies in Dub, instead of quoting yt-dlp's command-line flags (#2036, #2034)
- An engine that fails to start now says whether it timed out, crashed (with its exit code and last output) or answered wrongly, instead of "did not signal ready: None" (#2037, #2026)
- Transcribing an M4A file with PyTorch Whisper works, instead of failing with "Format not recognised" (#2042, #2039)
### CI
+9 -1
View File
@@ -1401,7 +1401,15 @@ class PyTorchWhisperBackend(ASRBackend):
import soundfile as sf
import torch
self._ensure_pipe()
audio_np, sr = sf.read(audio_path, dtype="float32")
# #2039: libsndfile cannot open MP4/M4A (AAC), which /transcribe and
# the MCP tool both accept. Those decode through the validated ffmpeg
# path, which resamples to 16 kHz properly. Anything soundfile can
# read keeps its native rate, so the pipeline's band-limited
# resampler does the conversion rather than a linear interpolation.
try:
audio_np, sr = sf.read(audio_path, dtype="float32")
except Exception:
audio_np, sr = _decode_audio_16k_mono(audio_path), 16000
if audio_np.ndim > 1:
audio_np = audio_np.mean(axis=1)
bs = 16 if torch.cuda.is_available() else 2
+90
View File
@@ -0,0 +1,90 @@
"""#2039 — PyTorch Whisper read uploads with soundfile, which cannot open
MP4/M4A (AAC). /transcribe and the MCP tool both accept .m4a, so every such
upload failed with "Format not recognised" before ASR ran."""
import sys
import numpy as np
import pytest
M4A_BYTES = b"\x00\x00\x00\x20ftypM4A " + b"\x00" * 64 # soundfile refuses this
def _recording_pipe(seen):
def fake_pipe(inputs, **_kwargs):
seen.update(inputs)
return {"text": "hello", "chunks": []}
return fake_pipe
def test_an_m4a_upload_reaches_the_pipeline_through_the_ffmpeg_fallback(tmp_path, monkeypatch):
from services import asr_backend as ab
clip = tmp_path / "memo.m4a"
clip.write_bytes(M4A_BYTES)
decoded = np.zeros(16000, dtype=np.float32)
decoded_paths = []
def fake_ffmpeg_decode(path):
decoded_paths.append(path)
return decoded
monkeypatch.setattr(ab, "_decode_audio_16k_mono", fake_ffmpeg_decode)
seen = {}
result = ab.PyTorchWhisperBackend(asr_pipe=_recording_pipe(seen)).transcribe(str(clip))
assert result["text"] == "hello"
assert decoded_paths == [str(clip)]
assert seen["sampling_rate"] == 16000
assert np.array_equal(seen["array"], decoded)
def test_readable_audio_keeps_its_native_rate(tmp_path, monkeypatch):
"""No linear resample for WAV/FLAC: the pipeline's own resampler is
band-limited, a plain interpolation from 48 kHz would alias."""
import soundfile as sf
from services import asr_backend as ab
clip = tmp_path / "take.wav"
stereo = np.zeros((4800, 2), dtype=np.float32)
sf.write(str(clip), stereo, 48000)
def must_not_decode(_path):
raise AssertionError("soundfile-readable audio must not go through ffmpeg")
monkeypatch.setattr(ab, "_decode_audio_16k_mono", must_not_decode)
seen = {}
ab.PyTorchWhisperBackend(asr_pipe=_recording_pipe(seen)).transcribe(str(clip))
assert seen["sampling_rate"] == 48000
assert seen["array"].ndim == 1 and len(seen["array"]) == 4800
@pytest.mark.usefixtures("asr_model_installed")
def test_the_transcribe_route_accepts_an_m4a_upload(monkeypatch):
from fastapi import FastAPI
from fastapi.testclient import TestClient
from api.routers import capture
ab = sys.modules["services.asr_backend"] # the module the route imports from
monkeypatch.setattr(ab, "asr_model_missing_error", lambda **_kw: None)
monkeypatch.setattr(ab, "_decode_audio_16k_mono", lambda _path: np.zeros(16000, dtype=np.float32))
seen = {}
backend = ab.PyTorchWhisperBackend(asr_pipe=_recording_pipe(seen))
monkeypatch.setattr(ab, "get_capture_asr_backend", lambda *a, **k: backend)
app = FastAPI()
app.include_router(capture.router)
r = TestClient(app).post(
"/transcribe", files={"audio": ("memo.m4a", M4A_BYTES, "audio/mp4")}
)
assert r.status_code == 200, r.text
assert seen["sampling_rate"] == 16000
def test_the_mcp_tool_names_m4a_bytes_as_m4a():
import mcp_server
assert mcp_server._sniff_audio_ext(M4A_BYTES) == ".m4a"