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
This commit is contained in:
@@ -53,14 +53,16 @@ def analyze(
|
||||
chapter: Optional[int] = typer.Option(None, help="Index de chapitre unique (def: tous)."),
|
||||
limit: Optional[int] = typer.Option(None, help="Limiter au N premiers chapitres rendus."),
|
||||
force: bool = typer.Option(False, help="Re-analyser meme si un artefact existe."),
|
||||
backend: Optional[str] = typer.Option(None, help="Moteur LLM: mlx ou lmstudio (def: reglages)."),
|
||||
model: Optional[str] = typer.Option(None, help="Identifiant de modele (def: reglages)."),
|
||||
):
|
||||
"""Analyse Gemma : segments narration/dialogue + locuteurs + casting."""
|
||||
from .analysis.gemma import Gemma
|
||||
from .analysis.llm.client import LLM
|
||||
from .analysis.segmenter import analyze_chapter
|
||||
from .settings import get_settings
|
||||
|
||||
book = load_book(slug)
|
||||
gemma = Gemma()
|
||||
gemma = LLM(model_id=model, backend=backend)
|
||||
dedup_gemma = gemma if get_settings().dedup_use_gemma else None
|
||||
cast = artifacts.load_cast(slug)
|
||||
chars = list(cast.characters)
|
||||
@@ -100,6 +102,8 @@ def benchmark(
|
||||
slug: str,
|
||||
models: Optional[str] = typer.Option(
|
||||
None, help="Modeles a comparer, separes par des virgules (def: modele courant)."),
|
||||
backend: Optional[str] = typer.Option(
|
||||
None, help="Moteur LLM: mlx ou lmstudio (def: reglages)."),
|
||||
chapter: Optional[int] = typer.Option(
|
||||
None, help="Restreindre a un chapitre (def: tous ceux avec reference)."),
|
||||
temperature: Optional[float] = typer.Option(
|
||||
@@ -115,12 +119,16 @@ def benchmark(
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from .analysis import gemma as _gemma
|
||||
from .analysis.llm import client as _llm
|
||||
from .analysis.benchmark import run_benchmark
|
||||
from .settings import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
backend_name = backend or settings.gemma_backend
|
||||
default_model = (settings.lmstudio_model if backend_name == "lmstudio"
|
||||
else settings.gemma_model)
|
||||
model_ids = ([m.strip() for m in models.split(",") if m.strip()]
|
||||
if models else [get_settings().gemma_model])
|
||||
if models else [default_model])
|
||||
chapters = [chapter] if chapter is not None else None
|
||||
|
||||
label = "artefacts en cache" if use_cached else f"{len(model_ids)} modele(s)"
|
||||
@@ -137,17 +145,17 @@ def benchmark(
|
||||
def _sink(piece: str) -> None:
|
||||
sys.stdout.write(piece)
|
||||
sys.stdout.flush()
|
||||
_gemma.set_token_sink(_sink)
|
||||
_llm.set_token_sink(_sink)
|
||||
try:
|
||||
report = run_benchmark(
|
||||
slug, model_ids, chapters=chapters,
|
||||
slug, model_ids, backend=backend_name, chapters=chapters,
|
||||
temperature=temperature,
|
||||
reasoning=reasoning if reasoning else None,
|
||||
use_cached=use_cached,
|
||||
progress=_progress)
|
||||
finally:
|
||||
if stream:
|
||||
_gemma.set_token_sink(None)
|
||||
_llm.set_token_sink(None)
|
||||
report.generated_at = datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
# Table comparative : une ligne par modele (agregat micro-moyenne).
|
||||
@@ -197,9 +205,11 @@ def benchmark(
|
||||
def pronounce(
|
||||
slug: str,
|
||||
chapter: Optional[int] = typer.Option(None, help="Index de chapitre (def: 1er rendu)."),
|
||||
backend: Optional[str] = typer.Option(None, help="Moteur LLM: mlx ou lmstudio (def: reglages)."),
|
||||
model: Optional[str] = typer.Option(None, help="Identifiant de modele (def: reglages)."),
|
||||
):
|
||||
"""Propose des candidats de prononciation (Gemma) -> pronunciation.json."""
|
||||
from .analysis.gemma import Gemma
|
||||
from .analysis.llm.client import LLM
|
||||
from .analysis.pronunciation import merge_pronunciations, propose_pronunciations
|
||||
|
||||
book = load_book(slug)
|
||||
@@ -209,7 +219,7 @@ def pronounce(
|
||||
console.print("[red]Chapitre introuvable.[/]"); raise typer.Exit(1)
|
||||
|
||||
ct = load_chapter_text(slug, ch)
|
||||
gemma = Gemma()
|
||||
gemma = LLM(model_id=model, backend=backend)
|
||||
with console.status("Recherche des mots a risque…"):
|
||||
new = propose_pronunciations("\n".join(ct.paragraphs), gemma)
|
||||
pron = merge_pronunciations(artifacts.load_pronunciation(slug), new)
|
||||
@@ -228,6 +238,8 @@ def cast(
|
||||
rebuild_voicebank: bool = typer.Option(False, help="Regenere les clips de la voicebank."),
|
||||
dedup: bool = typer.Option(False, help="Deduplique d'abord les variantes de noms (heuristique)."),
|
||||
llm: bool = typer.Option(False, "--llm", help="Ajoute la passe Gemma a la dedup (moins sur)."),
|
||||
backend: Optional[str] = typer.Option(None, help="Moteur LLM pour --llm: mlx ou lmstudio (def: reglages)."),
|
||||
model: Optional[str] = typer.Option(None, help="Identifiant de modele pour --llm (def: reglages)."),
|
||||
):
|
||||
"""Construit la voicebank (si besoin) et auto-assigne les voix au casting."""
|
||||
from .casting.assign import assign_voices
|
||||
@@ -243,8 +255,8 @@ def cast(
|
||||
from .models import Cast
|
||||
gemma = None
|
||||
if llm:
|
||||
from .analysis.gemma import Gemma
|
||||
gemma = Gemma()
|
||||
from .analysis.llm.client import LLM
|
||||
gemma = LLM(model_id=model, backend=backend)
|
||||
before = len(cast.characters)
|
||||
with console.status("Deduplication du casting…"):
|
||||
chars = dedup_cast(cast.characters, gemma)
|
||||
|
||||
Reference in New Issue
Block a user