Voix : Qwen gardé, nombres en lettres reconnus, Gemini plus court

Vu en production avec Qwen sur GPU : la plupart des phrases basculaient sur
Gemini puis Edge (voix différente d'un sous-titre à l'autre).
- Vérification de lecture : les nombres en lettres (« trente », « vingt-cinq
  pour cent ») et leur signe (« plus », « moins ») sont comparés comme les
  chiffres écrits par Whisper, au lieu de passer pour des mots sautés.
- Prises emballées (14 s pour 34 caractères) écartées d'office ; côté
  qwen-tts, max_new_tokens borne la durée à « caractères / 6 + 3 » s.
- Le moteur choisi est gardé tant qu'il lit au moins 70 % du texte
  (avec avertissement), au lieu de 85 % ; 3 tentatives Qwen.
- Gemini : délai d'attente 45 s au lieu de 120 s, erreurs journalisées en repr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
This commit is contained in:
Claude committed 2026-09-29 12:01:42 +00:00
1 parent 90a9b6be4d
commit 338546662f
7 files changed
+93 -17

No files matched your search

+17 -5
View File
@@ -89,9 +89,16 @@ async def synthesize(
return VoiceTrack(processed, duration, words, used_engine, used_voice, warnings)
# En dessous, une lecture imparfaite est écartée au profit du moteur suivant :
# mieux vaut une autre voix qu'une phrase aux mots sautés ou inventés.
KEEP_IMPERFECT_COVERAGE = 0.85
# En dessous, une lecture imparfaite est écartée au profit du moteur suivant.
# Au-dessus, on garde la voix choisie (avec un avertissement) : changer de moteur
# pour un mot douteux donnait une voix différente d'une phrase à l'autre.
KEEP_IMPERFECT_COVERAGE = 0.7
def is_runaway(duration: float, text: str) -> bool:
"""Prise emballée : le modèle a continué à produire du son bien après la fin
du texte (vu avec Qwen : 14 s pour une phrase de 34 caractères)."""
return duration > max(3.0, len(text) / 6) + 1.5
async def _checked(
@@ -112,12 +119,17 @@ async def _checked(
try:
raw, engine_voice = await generate(attempt)
except Exception as error: # noqa: BLE001 — nouvelle tentative, puis moteur suivant
log.warning("%s TTS en échec (tentative %d) : %s", name, attempt + 1, error)
log.warning("%s TTS en échec (tentative %d) : %r", name, attempt + 1, error)
last_error = error
continue
processed = workdir / f"voice_{name.lower()}_{attempt}.wav"
await audio.process_voice(raw, processed)
duration = (await proc.probe(processed)).duration
if is_runaway(duration, text):
log.warning("%s : prise emballée (%.1f s pour %d car.), écartée", name, duration, len(text))
last_error = RuntimeError(f"voix de {duration:.0f} s pour {len(text)} caractères")
continue
spoken = await align.transcribe(processed)
check = quality.check_reading(text, spoken)
log.info("Lecture %s (tentative %d) : %s", name, attempt + 1, check.describe())
@@ -128,7 +140,7 @@ async def _checked(
break
if best is None:
warnings.append(f"{name} indisponible ({last_error}) : voix de secours utilisée.")
warnings.append(f"{name} : aucune voix utilisable ({last_error!r}) : voix de secours utilisée.")
return None
check, processed, spoken, engine_voice = best