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:
@@ -20,7 +20,7 @@ from pydantic import BaseModel
|
||||
|
||||
from ..config import DATA_DIR, book_data_dir, book_output_dir, ensure_dirs
|
||||
from ..epub.parser import load_book, load_chapter_text, parse_epub
|
||||
from ..models import Cast, ChapterAnalysis, Pronunciation
|
||||
from ..models import Cast, ChapterAnalysis, Character, Pronunciation
|
||||
from ..pipeline.orchestrator import load_state, orchestrator
|
||||
from ..settings import Settings, get_settings, save_settings
|
||||
from ..store import artifacts
|
||||
@@ -196,6 +196,43 @@ def put_cast(slug: str, cast: Cast) -> dict:
|
||||
return {"saved": True}
|
||||
|
||||
|
||||
@app.get("/api/books/{slug}/cast/unresolved")
|
||||
def get_unresolved_speakers(slug: str) -> dict:
|
||||
"""Locuteurs apparaissant dans l'analyse mais rattaches a aucun personnage.
|
||||
|
||||
Surface les surfaces que la canonicalisation deterministe a refuse de
|
||||
trancher, pour que l'utilisateur les aliase/fusionne a la main. Predicat =
|
||||
rattachement a un Character (par nom/alias exact ou heuristique), independant
|
||||
de l'assignation de voix."""
|
||||
from ..casting.dedup import heuristic_match
|
||||
from ..epub.parser import load_book
|
||||
_require(slug)
|
||||
cast = artifacts.load_cast(slug)
|
||||
|
||||
def resolves(spk: str) -> bool:
|
||||
low = spk.lower()
|
||||
for ch in cast.characters:
|
||||
if ch.name.lower() == low or low in (a.lower() for a in ch.aliases):
|
||||
return True
|
||||
return isinstance(heuristic_match(spk, cast.characters), Character)
|
||||
|
||||
agg: dict[str, dict] = {}
|
||||
for ch in load_book(slug).chapters:
|
||||
if not artifacts.analysis_path(slug, ch.index).exists():
|
||||
continue
|
||||
for seg in artifacts.load_analysis(slug, ch.index).segments:
|
||||
spk = (seg.speaker or "").strip()
|
||||
if not spk or spk.lower() in {"narrateur", "inconnu", "?"}:
|
||||
continue
|
||||
if resolves(spk):
|
||||
continue
|
||||
row = agg.setdefault(spk, {"speaker": spk, "count": 0, "chapters": []})
|
||||
row["count"] += 1
|
||||
if ch.index not in row["chapters"]:
|
||||
row["chapters"].append(ch.index)
|
||||
return {"unresolved": sorted(agg.values(), key=lambda r: -r["count"])}
|
||||
|
||||
|
||||
@app.get("/api/books/{slug}/pronunciation")
|
||||
def get_pron(slug: str) -> dict:
|
||||
_require(slug)
|
||||
@@ -222,6 +259,16 @@ def write_settings(settings: Settings) -> dict:
|
||||
return {"saved": True}
|
||||
|
||||
|
||||
@app.get("/api/lmstudio/models")
|
||||
def list_lmstudio_models() -> dict:
|
||||
"""Modeles telecharges dans LM Studio (pour peupler le selecteur de l'UI)."""
|
||||
from ..analysis.llm.lmstudio_backend import list_models
|
||||
try:
|
||||
return {"models": list_models(get_settings().lmstudio_base_url)}
|
||||
except Exception as exc: # noqa: BLE001 — serveur down / injoignable
|
||||
raise HTTPException(503, f"LM Studio injoignable: {exc}")
|
||||
|
||||
|
||||
# --- Voicebank + preview -----------------------------------------------------
|
||||
|
||||
@app.get("/api/voicebank")
|
||||
|
||||
Reference in New Issue
Block a user