From 0bf399cb79f0856c18d38d4f2fcf7a9377bf3b62 Mon Sep 17 00:00:00 2001 From: alf Date: Tue, 23 Jun 2026 23:30:45 +0200 Subject: [PATCH] =?UTF-8?q?fix(voice):=20=C2=A77=20=E2=80=94=20coupe-silen?= =?UTF-8?q?ce=20+=20troncature=20phrase=20(anti=20mot=20parasite)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le cap dur à 16s coupait la réf en plein mot ; l'auto-ASR transcrivait le bout tronqué (ex. 'présenter') et OmniVoice le recrachait en tête de chaque génération. - prepare_wav : couper sur une frontière de silence (librosa.effects.split), jamais à cap brut → clip finit en silence, mots entiers. - enroll : tronquer ref_text à la dernière phrase complète (. ! ?). Validé sur le consentement réel de richard : 24.4s→15.7s, ref_text finit '…des oiseaux.' (plus de mot parasite). NB : voix enrôlées avec l'ancien cap = à ré-enrôler. Co-Authored-By: Claude Opus 4.8 (1M context) --- kazeia_central/voice/enroll.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/kazeia_central/voice/enroll.py b/kazeia_central/voice/enroll.py index 4a68014..23458aa 100644 --- a/kazeia_central/voice/enroll.py +++ b/kazeia_central/voice/enroll.py @@ -27,12 +27,24 @@ class EnrollmentError(RuntimeError): def prepare_wav(in_path: str, out_path: str, target_s: float = OV_TARGET_S) -> dict: import librosa, soundfile as sf + cap = min(target_s, _HARD_MAX_S) y, _ = librosa.load(in_path, sr=_PREP_SR, mono=True) orig_s = len(y) / _PREP_SR yt, _ = librosa.effects.trim(y, top_db=30) if yt.size == 0: yt = y - yt = yt[: int(min(target_s, _HARD_MAX_S) * _PREP_SR)] + # §7 FIX : couper sur une FRONTIÈRE DE SILENCE, jamais à `cap` brut (= en plein mot, + # ce qui faisait recracher le dernier mot tronqué en tête de chaque génération). + # Garder les segments non-silencieux jusqu'à ~cap, finir à la fin du dernier segment. + end = 0 + for _s, e in librosa.effects.split(yt, top_db=30): + if e / _PREP_SR <= cap: + end = e + else: + break + if end == 0: # un seul long segment → repli au cap dur + end = min(len(yt), int(cap * _PREP_SR)) + yt = yt[:end] final_s = len(yt) / _PREP_SR sf.write(out_path, yt, _PREP_SR, subtype="PCM_16") return {"orig_s": round(orig_s, 1), "final_s": round(final_s, 1), "too_short": final_s < _MIN_WARN_S} @@ -81,6 +93,12 @@ def enroll(wav_path: str, text: str, out_path: str) -> dict: if C != 8: raise EnrollmentError(f"attendu 8 codebooks, eu {C}") ref_text = (getattr(vcp, "ref_text", "") or text or "").strip() + # §7 FIX (ceinture+bretelles) : tronquer à la dernière phrase complète (. ! ?), pour + # qu'un éventuel mot tronqué de fin de réf ne soit jamais recraché en tête de synthèse. + import re + _ends = list(re.finditer(r"[.!?]", ref_text)) + if _ends: + ref_text = ref_text[: _ends[-1].end()].strip() txt = ref_text.encode("utf-8") os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True) with open(out_path, "wb") as f: