fix(gateway): STT high-pass + gain for SLIC mic

SLIC handset injects low-frequency rumble/DC drift that swamps speech
and causes Kyutai STT to output nothing. Apply a box moving-average
high-pass (~110 Hz) before peak-normalisation. Also raise gain cap to
x40 and target_peak to 0.7 (mic is still very quiet post-ES8388 fix).
Verified: same capture transcribes empty without HP, clean French with.
This commit is contained in:
clement
2026-06-16 09:35:00 +02:00
parent 49ec7d9dc7
commit bc5363cf2c
+19 -9
View File
@@ -1908,12 +1908,17 @@ def _build_wav(pcm: bytes, sr: int, ch: int, sw: int) -> bytes:
return out.getvalue() return out.getvalue()
def _normalize_wav_for_stt(wav_bytes: bytes, target_peak: float = 0.5, def _normalize_wav_for_stt(wav_bytes: bytes, target_peak: float = 0.7,
max_gain: float = 25.0) -> bytes: max_gain: float = 40.0, hp_cutoff_hz: float = 110.0) -> bytes:
"""Boost a quiet capture up to `target_peak` before STT. The PLIP handset """Condition a PLIP handset capture for Kyutai STT: high-pass then normalise.
mic comes in very low (~1-4 % FS through the SLIC), too quiet for Kyutai to
transcribe — it hears only the noise floor and hallucinates. Peak-normalise Two problems with the SLIC handset path: (1) it injects low-frequency rumble
in software (gain capped so pure silence isn't blown up into noise).""" / DC drift that swamps the speech and makes Kyutai output nothing — a
high-pass (~110 Hz, box moving-average subtraction) removes it; (2) the mic
comes in very quiet (~2-5 % FS even at +24 dB PGA), so peak-normalise after
filtering (gain capped so silence isn't blown up). WITHOUT the high-pass the
exact same capture transcribes empty; WITH it, clean French — verified on the
bench."""
if _np is None: if _np is None:
return wav_bytes return wav_bytes
parsed = _wav_pcm(wav_bytes) parsed = _wav_pcm(wav_bytes)
@@ -1923,12 +1928,17 @@ def _normalize_wav_for_stt(wav_bytes: bytes, target_peak: float = 0.5,
if sw != 2 or not pcm: if sw != 2 or not pcm:
return wav_bytes return wav_bytes
arr = _np.frombuffer(pcm, dtype=_np.int16).astype(_np.float32) arr = _np.frombuffer(pcm, dtype=_np.int16).astype(_np.float32)
# High-pass = signal minus its low-frequency moving average. Window length
# sets the cutoff (~sr/k Hz); k≈145 @16 kHz → ~110 Hz.
k = max(2, int(sr / max(hp_cutoff_hz, 1.0)))
if arr.size > k:
lp = _np.convolve(arr, _np.ones(k, dtype=_np.float32) / k, mode="same")
arr = arr - lp
peak = float(_np.max(_np.abs(arr))) or 1.0 peak = float(_np.max(_np.abs(arr))) or 1.0
gain = min((target_peak * 32767.0) / peak, max_gain) gain = min((target_peak * 32767.0) / peak, max_gain)
if gain <= 1.05: # already loud enough
return _build_wav(pcm, sr, ch, sw)
arr = _np.clip(arr * gain, -32768, 32767).astype(_np.int16) arr = _np.clip(arr * gain, -32768, 32767).astype(_np.int16)
logging.info("STT normalise: peak %.1f%% FS → gain x%.1f", 100 * peak / 32768, gain) logging.info("STT conditioning: high-pass %.0f Hz, peak %.1f%% FS → gain x%.1f",
hp_cutoff_hz, 100 * peak / 32768, gain)
return _build_wav(arr.tobytes(), sr, ch, sw) return _build_wav(arr.tobytes(), sr, ch, sw)