| """Shared audio-input bounds + decode for the ASR endpoint handlers.
|
|
|
| Byte-identical across the endpoint*/ dirs that decode audio to a numpy array
|
| (MMS / badrex / seamless / whisper). Each endpoint dir is deployed standalone
|
| (pushed to its own HF model repo), so it cannot import from src/ at runtime —
|
| hence one copy per dir. Keep the copies in sync; a CI hash-diff guards it
|
| (see docs/endpoint-adversarial-review-2026-06-20.md, Phase 2 smoke test).
|
|
|
| All bounds are enforced BEFORE b64decode / torchaudio.load / model inference, so
|
| an oversized or malformed payload is rejected up front rather than exhausting
|
| memory/CPU/GPU. Bounds are env-overridable so a bigger box can raise them without
|
| a code change.
|
| """
|
| from __future__ import annotations
|
|
|
| import base64
|
| import io
|
| import os
|
|
|
| import numpy as np
|
|
|
| SAMPLE_RATE = 16000
|
|
|
|
|
|
|
| MAX_ENCODED_BYTES = int(os.environ.get("ASR_MAX_ENCODED_BYTES", str(100 * 1024 * 1024)))
|
| MAX_DECODED_SAMPLES = int(os.environ.get("ASR_MAX_DECODED_SAMPLES", str(SAMPLE_RATE * 60 * 90)))
|
| MAX_LIST_LEN = int(os.environ.get("ASR_MAX_LIST_LEN", str(MAX_DECODED_SAMPLES * 2)))
|
|
|
|
|
| class AudioRequestError(ValueError):
|
| """A request that fails a pre-decode bound. Handlers map this to a
|
| {"error": ..., "status": "bad_request"} response so the runner can tell a
|
| client error apart from a model failure."""
|
|
|
|
|
| def decode_to_raw(audio_input):
|
| """Return raw bytes for the str/bytes path, or None for the list path
|
| (the caller routes None to array_from_list). Enforces the encoded-size +
|
| base64-validity bounds on the way."""
|
| if isinstance(audio_input, str):
|
|
|
| if len(audio_input) > MAX_ENCODED_BYTES * 4 // 3 + 4:
|
| raise AudioRequestError("encoded payload exceeds size limit")
|
| try:
|
| raw = base64.b64decode(audio_input, validate=True)
|
| except Exception as exc:
|
| raise AudioRequestError(f"invalid base64: {exc}") from exc
|
| if len(raw) > MAX_ENCODED_BYTES:
|
| raise AudioRequestError("decoded payload exceeds size limit")
|
| return raw
|
| if isinstance(audio_input, list):
|
| return None
|
| raw = bytes(audio_input)
|
| if len(raw) > MAX_ENCODED_BYTES:
|
| raise AudioRequestError("payload exceeds size limit")
|
| return raw
|
|
|
|
|
| def array_from_list(audio_input):
|
| """float32 mono array from a JSON list, with shape + finiteness bounds."""
|
| if len(audio_input) > MAX_LIST_LEN:
|
| raise AudioRequestError("list input exceeds sample limit")
|
| arr = np.asarray(audio_input, dtype=np.float32)
|
| if not np.isfinite(arr).all():
|
| raise AudioRequestError("list input contains NaN/Inf")
|
| if arr.ndim > 1:
|
| arr = arr.mean(axis=0)
|
| return arr
|
|
|
|
|
| def check_samples(arr):
|
| """Post-decode duration bound (covers compressed payloads that expand past
|
| the encoded cap once decoded)."""
|
| if arr.shape[0] > MAX_DECODED_SAMPLES:
|
| raise AudioRequestError("decoded audio exceeds duration limit")
|
| return arr
|
|
|
|
|
| def load_and_validate_audio(audio_input):
|
| """Decode any accepted input (base64 str / raw bytes / float list) to a
|
| bounded float32 mono 16 kHz numpy array, raising AudioRequestError for any
|
| input that fails a bound. torchaudio decodes m4a/webm/opus/mp4 via ffmpeg, so
|
| no client-side conversion is needed."""
|
| raw = decode_to_raw(audio_input)
|
| if raw is None:
|
| return check_samples(array_from_list(audio_input))
|
|
|
| import torchaudio
|
|
|
| waveform, sr = torchaudio.load(io.BytesIO(raw))
|
| if sr != SAMPLE_RATE:
|
| waveform = torchaudio.transforms.Resample(orig_freq=sr, new_freq=SAMPLE_RATE)(waveform)
|
| arr = waveform.numpy()
|
| if arr.ndim > 1:
|
| arr = arr.mean(axis=0)
|
| return check_samples(arr.astype(np.float32))
|
|
|