Files
LiveFlow/app/segmenter.py
T
Claude a8517c3597 Implémentation complète de LiveFlow (proposition 2, moteur Qwen3-ASR)
- docker-compose : Caddy (HTTPS local) + app FastAPI + Qwen3-ASR via vLLM (GPU)
- Backend : WebSocket audio, segmentation VAD (WebRTC), appel ASR compatible
  OpenAI, stockage SQLite, exports TXT/MD/SRT/JSON
- Frontend : capture micro (AudioWorklet, rééchantillonnage 16 kHz),
  transcription live, historique des réunions, copie et téléchargement
- README : démarrage, prérequis GPU, changement de moteur ASR

https://claude.ai/code/session_01YHMp3EKzr4s6o8w1ygxuUe
2026-06-11 16:28:17 +00:00

101 lines
3.6 KiB
Python

"""Découpage du flux micro en segments de parole via WebRTC VAD.
Le client envoie du PCM 16 bits mono 16 kHz. On analyse des trames de 30 ms :
un segment démarre quand la parole domine la fenêtre récente (avec un
pré-roll pour ne pas couper le début de phrase) et se termine après un
silence prolongé ou une durée maximale.
"""
from collections import deque
from dataclasses import dataclass
import webrtcvad
SAMPLE_RATE = 16000
FRAME_MS = 30
FRAME_BYTES = SAMPLE_RATE * FRAME_MS // 1000 * 2 # 960 octets
VAD_AGGRESSIVENESS = 2
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
@dataclass
class Segment:
pcm: bytes
t0: float # secondes depuis le début de la réunion
t1: float
class SpeechSegmenter:
def __init__(self):
self._vad = webrtcvad.Vad(VAD_AGGRESSIVENESS)
self._pending = bytearray()
self._ring: deque[tuple[bytes, bool]] = deque(maxlen=PREROLL_FRAMES)
self._frame_index = 0
self._triggered = False
self._segment = bytearray()
self._segment_start_frame = 0
self._silence_frames = 0
self._speech_frames = 0
def feed(self, data: bytes) -> list[Segment]:
"""Ajoute de l'audio brut et renvoie les segments terminés."""
self._pending.extend(data)
segments = []
while len(self._pending) >= FRAME_BYTES:
frame = bytes(self._pending[:FRAME_BYTES])
del self._pending[:FRAME_BYTES]
seg = self._process_frame(frame)
if seg is not None:
segments.append(seg)
return segments
def flush(self) -> Segment | None:
"""Clôt le segment en cours (fin d'enregistrement)."""
seg = self._finish_segment() if self._triggered else None
self._ring.clear()
self._pending.clear()
return seg
def _process_frame(self, frame: bytes) -> Segment | None:
is_speech = self._vad.is_speech(frame, SAMPLE_RATE)
self._frame_index += 1
if not self._triggered:
self._ring.append((frame, 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
self._segment_start_frame = self._frame_index - len(self._ring)
self._segment = bytearray(b"".join(f for f, _ in self._ring))
self._speech_frames = voiced
self._silence_frames = 0
self._ring.clear()
return None
self._segment.extend(frame)
if is_speech:
self._speech_frames += 1
self._silence_frames = 0
else:
self._silence_frames += 1
too_long = len(self._segment) >= MAX_SEGMENT_S * SAMPLE_RATE * 2
ended = self._silence_frames * FRAME_MS >= SILENCE_END_MS
if ended or too_long:
return self._finish_segment()
return None
def _finish_segment(self) -> Segment | None:
self._triggered = False
seg, self._segment = self._segment, bytearray()
if self._speech_frames * FRAME_MS < MIN_SPEECH_MS:
return None
t0 = self._segment_start_frame * FRAME_MS / 1000
t1 = self._frame_index * FRAME_MS / 1000
return Segment(pcm=bytes(seg), t0=t0, t1=t1)