mirror of
https://github.com/R0m1k3/LiveFlow.git
synced 2026-10-11 17:27:19 +02:00
Qualité de transcription : audio propre pour le moteur (corrige les erreurs)
L'AGC sur-amplifiait (jusqu'à x60) l'audio envoyé au moteur -> signal déformé/bruité -> mauvaise reconnaissance. Désormais : - Le VAD détecte sur l'audio amplifié, mais le segment stocke l'audio BRUT - Le segment est normalisé proprement (mise à l'échelle linéaire unique vers crête ~22000, sans saturation) juste avant le moteur - Segment max ramené de 25 s à 15 s pour un retour plus rapide Validé bout en bout : micro faible -> détecté -> audio propre -> transcrit. https://claude.ai/code/session_01YHMp3EKzr4s6o8w1ygxuUe
This commit is contained in:
2 files changed
+25
-14
No files matched your search
+15
-6
@@ -189,18 +189,27 @@ def pcm_to_wav(pcm: bytes) -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
QUIET_PEAK = 9000 # niveau crête en dessous duquel on amplifie (échelle int16)
|
||||
NORM_TARGET_PEAK = 22000 # niveau crête visé (≈0,67 pleine échelle), sans saturer
|
||||
NORM_MAX_GAIN = 25.0 # amplification maximale
|
||||
NORM_NOISE_FLOOR = 80 # en dessous : silence, on ne touche pas
|
||||
|
||||
|
||||
def boost_quiet_audio(pcm: bytes) -> tuple[bytes, int]:
|
||||
"""Amplifie les segments trop faibles (micros Bluetooth mains-libres...).
|
||||
"""Normalise proprement le niveau du segment pour le moteur ASR.
|
||||
|
||||
Retourne (audio, niveau crête d'origine).
|
||||
Mise à l'échelle linéaire unique du segment entier vers un niveau cible
|
||||
confortable, SANS saturation (gain = cible / crête), bien meilleure pour
|
||||
la reconnaissance qu'une amplification par à-coups. Renvoie (audio, crête).
|
||||
"""
|
||||
peak = audioop.max(pcm, 2)
|
||||
if 0 < peak < QUIET_PEAK:
|
||||
factor = min(6.0, 26000 / peak)
|
||||
return audioop.mul(pcm, 2, factor), peak
|
||||
if peak < NORM_NOISE_FLOOR:
|
||||
return pcm, peak
|
||||
gain = min(NORM_MAX_GAIN, NORM_TARGET_PEAK / peak)
|
||||
if gain <= 1.05:
|
||||
return pcm, peak
|
||||
try:
|
||||
return audioop.mul(pcm, 2, gain), peak
|
||||
except Exception:
|
||||
return pcm, peak
|
||||
|
||||
|
||||
|
||||
+10
-8
@@ -22,7 +22,7 @@ PREROLL_FRAMES = 10 # 300 ms conservées avant le déclenchement
|
||||
TRIGGER_RATIO = 0.6 # part de trames "parole" du pré-roll pour démarrer
|
||||
SILENCE_END_MS = 700 # silence qui clôt un segment
|
||||
MIN_SPEECH_MS = 300 # en dessous, le segment est ignoré (bruit)
|
||||
MAX_SEGMENT_S = 25 # coupe forcée pour garder une latence raisonnable
|
||||
MAX_SEGMENT_S = 15 # coupe forcée pour garder une latence raisonnable
|
||||
|
||||
# Contrôle de gain automatique appliqué AVANT la détection de parole : sans lui,
|
||||
# un micro faible (casque...) reste sous le seuil du VAD et aucune phrase n'est
|
||||
@@ -83,10 +83,12 @@ class SpeechSegmenter:
|
||||
self._pending.extend(data)
|
||||
segments = []
|
||||
while len(self._pending) >= FRAME_BYTES:
|
||||
frame = bytes(self._pending[:FRAME_BYTES])
|
||||
raw = bytes(self._pending[:FRAME_BYTES])
|
||||
del self._pending[:FRAME_BYTES]
|
||||
frame = self._agc.process(frame) # amplifie avant le VAD
|
||||
seg = self._process_frame(frame)
|
||||
# Le VAD travaille sur l'audio amplifié (détection fiable même micro
|
||||
# faible) ; le segment stocke l'audio BRUT (transcription propre).
|
||||
amplified = self._agc.process(raw)
|
||||
seg = self._process_frame(raw, amplified)
|
||||
if seg is not None:
|
||||
segments.append(seg)
|
||||
return segments
|
||||
@@ -98,12 +100,12 @@ class SpeechSegmenter:
|
||||
self._pending.clear()
|
||||
return seg
|
||||
|
||||
def _process_frame(self, frame: bytes) -> Segment | None:
|
||||
is_speech = self._vad.is_speech(frame, SAMPLE_RATE)
|
||||
def _process_frame(self, raw: bytes, amplified: bytes) -> Segment | None:
|
||||
is_speech = self._vad.is_speech(amplified, SAMPLE_RATE)
|
||||
self._frame_index += 1
|
||||
|
||||
if not self._triggered:
|
||||
self._ring.append((frame, is_speech))
|
||||
self._ring.append((raw, is_speech))
|
||||
voiced = sum(1 for _, s in self._ring if s)
|
||||
if len(self._ring) == self._ring.maxlen and voiced >= TRIGGER_RATIO * self._ring.maxlen:
|
||||
self._triggered = True
|
||||
@@ -114,7 +116,7 @@ class SpeechSegmenter:
|
||||
self._ring.clear()
|
||||
return None
|
||||
|
||||
self._segment.extend(frame)
|
||||
self._segment.extend(raw)
|
||||
if is_speech:
|
||||
self._speech_frames += 1
|
||||
self._silence_frames = 0
|
||||
|
||||
Reference in new issue
Block a user