patristic-be / src /lib /audiobook /encode.py
Mario33333's picture
deploy: audiobook subsystem + encode int16 fix + R2 page fast-path + costs breakdown + query-run flow
1e55b98 verified
Raw
History Blame Contribute Delete
3.92 kB
"""Audio encoding helper: float/int PCM waveform β†’ compact streamable bytes.
A provider that renders raw audio (MMS β†’ 16 kHz float waveform) calls
:func:`encode_waveform` to get ``(audio_bytes, mime)`` ready to store + serve.
The pipeline never special-cases a codec β€” it just stores the bytes + mime.
Preferred output is **Opus in an Ogg container** (``audio/ogg``) β€” ~24-32 kbps
mono speech β‰ˆ 3-4 KB/s, matching the contract's "keep clips small" note and the
FE's ``Accept: audio/ogg`` preference. If the libsndfile build lacks the Opus
subtype we fall back to Ogg/Vorbis (still ``audio/ogg``). A plain-WAV fallback
exists only as a last resort so a render is never lost to an encoder gap.
Opus mandates a 48 kHz sample rate; libsndfile resamples on write, but we also
pre-handle the rate so duration math stays exact regardless of subtype.
"""
from __future__ import annotations
import io
from functools import lru_cache
# Opus only accepts these container sample rates; 48000 is the canonical one.
_OPUS_RATE = 48000
@lru_cache(maxsize=1)
def _ogg_subtype() -> str | None:
"""Best Ogg subtype this libsndfile supports: 'OPUS' > 'VORBIS' > None."""
try:
import soundfile as sf
subs = sf.available_subtypes("OGG")
if "OPUS" in subs:
return "OPUS"
if "VORBIS" in subs:
return "VORBIS"
except Exception: # noqa: BLE001
return None
return None
def duration_ms_for(num_frames: int, sample_rate: int) -> int:
"""Clip length in ms from frame count + sample rate (pre-encode, exact)."""
if sample_rate <= 0:
return 0
return int(round(num_frames * 1000.0 / sample_rate))
def encode_waveform(samples, sample_rate: int) -> tuple[bytes, str, int]:
"""Encode a 1-D PCM waveform to compact bytes.
``samples`` is a 1-D numpy float array (mono, typically in [-1, 1]) or any
array soundfile accepts. Returns ``(audio_bytes, mime, duration_ms)``.
Tries Opus/Ogg, then Vorbis/Ogg (both ``audio/ogg``), then WAV
(``audio/wav`` β€” last resort so a clip is never lost).
"""
import numpy as np
import soundfile as sf
# Accept either a float waveform already in [-1, 1] (MMS) OR integer PCM
# (Gemini / Google-Cloud TTS return 16-bit PCM). Integer input MUST be
# normalized to float: casting int16 straight to float leaves values at
# Β±32768, which soundfile then clips to full-scale garbage β€” the "random
# noise instead of speech" failure. Float input passes through untouched.
arr = np.asarray(samples)
if np.issubdtype(arr.dtype, np.integer):
info = np.iinfo(arr.dtype)
max_mag = float(max(abs(int(info.min)), int(info.max)))
arr = (arr.astype("float32") / max_mag).reshape(-1)
else:
arr = arr.astype("float32").reshape(-1)
duration_ms = duration_ms_for(arr.shape[0], sample_rate)
subtype = _ogg_subtype()
if subtype is not None:
buf = io.BytesIO()
try:
# libsndfile writes Opus at 48 kHz; passing the true input rate lets
# it resample correctly. Vorbis accepts the native rate directly.
sf.write(buf, arr, sample_rate, format="OGG", subtype=subtype)
return buf.getvalue(), "audio/ogg", duration_ms
except Exception: # noqa: BLE001 β€” fall through to WAV
pass
# Last-resort uncompressed fallback (still playable by the FE <audio>).
buf = io.BytesIO()
sf.write(buf, arr, sample_rate, format="WAV", subtype="PCM_16")
return buf.getvalue(), "audio/wav", duration_ms
def ext_for_mime(mime: str) -> str:
"""File extension for a clip mime β€” used to build the storage key."""
return {
"audio/ogg": "ogg",
"audio/mpeg": "mp3",
"audio/wav": "wav",
}.get(mime, "bin")