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
60 lines
2.2 KiB
Python
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()))
|