Files
InkFlow/backend/inkflow/analysis/pronunciation.py
colgora ba1813c583 Voicebank : vraies voix françaises (CML-TTS) + pool anonyme + garde-fou Qwen3
Remplace la voicebank générée par Kokoro (timbre anglais sur français phonémisé
-> accent que Qwen3 clonait) par 41 vraies voix FR issues de CML-TTS (livres
audio studio) : 1 narrateur dédié, 18F/14M nommées, 4F/4M anonymes réservées.

- scripts/import_voices.py : import multi-shards parquet, 1 clip/locuteur (le
  plus propre via levenshtein), genre estimé par F0 (YIN, anti-octave), filtre
  débit de parole (ref_text aligné sur l'audio).
- VoiceEntry.anonymous + assign_voices : les figurants « anonyme (...) » tirent
  dans un pool réservé, jamais mélangé avec les voix nommées ; narrateur dédié
  (fr_narrator remplace fr_f_siwis).
- dedup._anon_attrs : genre/âge déduits du nom anonyme (bon genre de voix).
- tts/qwen3.py : garde-fou anti-dérive (rejette/réessaie les sorties en boucle
  ou coupées en estimant la durée plausible du chunk).

Limite connue : Qwen3 ne sait pas synthétiser les fragments d'1-2 mots (incises,
titres) -> trous ; à traiter (repli Kokoro ou fusion des incises).

Inclut aussi du travail en cours antérieur (refacto backend LLM pluggable
mlx/lmstudio, benchmark, ajustements frontend/API).

Claude-Session: https://claude.ai/code/session_01XSVvcy1mfb4k1xDgib9vVU
2026-06-21 21:32:31 +02:00

60 lines
2.2 KiB
Python

"""Dictionnaire de prononciation : application + proposition de candidats.
L'application est une simple reecriture de surface du texte (graphie guidee)
avant synthese. Les candidats (noms propres, termes SF) peuvent etre proposes
par Gemma puis valides par l'utilisateur dans l'UI.
"""
from __future__ import annotations
import re
from typing import Iterable
from ..models import Pronunciation, PronunciationEntry
from ..settings import get_settings
from .llm.client import LLM
def apply_pronunciation(text: str, pron: Pronunciation) -> str:
"""Remplace chaque terme actif par sa graphie phonetique (mot entier)."""
for entry in pron.entries:
if not entry.enabled or not entry.term:
continue
pattern = re.compile(rf"\b{re.escape(entry.term)}\b")
text = pattern.sub(entry.replacement, text)
return text
# Le prompt systeme est editable dans les reglages (settings.prompt_pronunciation).
def propose_pronunciations(text: str, gemma: LLM, *, max_chars: int = 16000) -> list[PronunciationEntry]:
"""Propose des candidats de prononciation a valider."""
sample = text[:max_chars]
prompt = (
"Repere dans cet extrait les mots a risque de mauvaise prononciation par "
"une voix de synthese francaise. Pour chacun, propose une graphie "
"phonetique francaise (replacement) qui guide la prononciation.\n\n"
f"EXTRAIT:\n{sample}\n\n"
'Reponds par un tableau JSON: '
'[{"term":"Tiamat","replacement":"Tia-matt","note":"nom propre"}]'
)
result = gemma.generate_json(prompt, system=get_settings().prompt_pronunciation)
entries: list[PronunciationEntry] = []
for item in result:
if isinstance(item, dict) and item.get("term") and item.get("replacement"):
entries.append(PronunciationEntry(
term=str(item["term"]).strip(),
replacement=str(item["replacement"]).strip(),
note=item.get("note"),
))
return entries
def merge_pronunciations(
existing: Pronunciation, new: Iterable[PronunciationEntry]
) -> Pronunciation:
by_term = {e.term.lower(): e for e in existing.entries}
for e in new:
by_term.setdefault(e.term.lower(), e)
return Pronunciation(entries=list(by_term.values()))