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:
119
backend/inkflow/analysis/llm/client.py
Normal file
119
backend/inkflow/analysis/llm/client.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Facade LLM pour l'analyse de texte (anciennement `Gemma`).
|
||||
|
||||
Charge un backend pluggable (mlx par defaut, ou LM Studio) selon les reglages et
|
||||
expose `generate` / `generate_json` consommes par tout le pipeline. Toute la
|
||||
logique agnostique du moteur vit ici : calcul des parametres depuis les Settings,
|
||||
retrait de la chaine de pensee (modeles a raisonnement) et `generate_json`
|
||||
tolerant qui extrait le premier objet/array JSON valide de la sortie du modele.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from ...settings import Settings, get_settings
|
||||
from ._text import _extract_json, _strip_reasoning
|
||||
from .factory import get_llm_backend
|
||||
|
||||
# Hook de streaming optionnel. Si defini, `generate()` diffuse chaque morceau de
|
||||
# texte AU FIL de la generation (pensee comprise, avant tout nettoyage) en
|
||||
# appelant ce callback. Utilise par `inkflow benchmark --stream` pour voir les
|
||||
# tokens en temps reel. None -> generation par lot classique (plus rapide).
|
||||
_TOKEN_SINK: Optional[Any] = None
|
||||
|
||||
|
||||
def set_token_sink(callback) -> None:
|
||||
"""Definit (ou retire avec None) le callback de streaming des tokens."""
|
||||
global _TOKEN_SINK
|
||||
_TOKEN_SINK = callback
|
||||
|
||||
|
||||
def _model_ref_for(backend: str, settings: Settings) -> str:
|
||||
"""Reference de modele par defaut pour un backend donne."""
|
||||
if backend == "lmstudio":
|
||||
return settings.lmstudio_model
|
||||
return settings.gemma_model
|
||||
|
||||
|
||||
class LLM:
|
||||
"""Petite facade multi-backend pour piloter le LLM d'analyse."""
|
||||
|
||||
def __init__(self, model_id: Optional[str] = None, backend: Optional[str] = None):
|
||||
settings = get_settings()
|
||||
self.backend_name = backend or settings.gemma_backend
|
||||
self.model_ref = model_id or _model_ref_for(self.backend_name, settings)
|
||||
self._backend = None
|
||||
|
||||
def _ensure_loaded(self) -> None:
|
||||
if self._backend is None:
|
||||
self._backend = get_llm_backend(self.backend_name, self.model_ref)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
system: Optional[str] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
) -> str:
|
||||
"""Genere une reponse texte a partir d'un prompt (template de chat).
|
||||
|
||||
`max_tokens`/`temperature` non fournis -> valeurs des reglages courants.
|
||||
"""
|
||||
self._ensure_loaded()
|
||||
settings = get_settings()
|
||||
if max_tokens is None:
|
||||
max_tokens = settings.gemma_max_tokens
|
||||
# En mode raisonnement, plafond dedie (garde-fou anti-boucle) ; la
|
||||
# generation s'arrete de toute facon des que le JSON post-pensee est
|
||||
# complet (cf. arret anticipe des backends).
|
||||
if settings.gemma_reasoning:
|
||||
max_tokens = max(max_tokens, settings.gemma_reasoning_max_tokens)
|
||||
if temperature is None:
|
||||
temperature = settings.gemma_temperature
|
||||
# Decodage glouton (temp 0) + raisonnement = boucles de pensee sans fin.
|
||||
# On force un echantillonnage minimal en mode raisonnement.
|
||||
if settings.gemma_reasoning and temperature == 0.0:
|
||||
temperature = settings.gemma_reasoning_temperature
|
||||
|
||||
messages = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
raw = self._backend.complete(
|
||||
messages,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
reasoning=settings.gemma_reasoning,
|
||||
token_sink=_TOKEN_SINK,
|
||||
)
|
||||
# Retire la chaine de pensee des modeles a raisonnement (sinon des
|
||||
# fragments de la "pensee" parasitent l'extraction JSON en aval).
|
||||
if settings.gemma_reasoning:
|
||||
return _strip_reasoning(raw)
|
||||
return raw
|
||||
|
||||
def generate_json(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
system: Optional[str] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
temperature: Optional[float] = None,
|
||||
retries: int = 1,
|
||||
) -> Any:
|
||||
"""Genere puis parse un JSON. Reessaie en cas d'echec de parsing.
|
||||
|
||||
`max_tokens`/`temperature` non fournis -> valeurs des reglages courants.
|
||||
"""
|
||||
last_err: Optional[Exception] = None
|
||||
for attempt in range(retries + 1):
|
||||
raw = self.generate(
|
||||
prompt, system=system, max_tokens=max_tokens,
|
||||
temperature=temperature if attempt == 0 else 0.0,
|
||||
)
|
||||
try:
|
||||
return _extract_json(raw)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
last_err = exc
|
||||
raise ValueError(f"Reponse JSON invalide apres {retries + 1} essais: {last_err}")
|
||||
Reference in New Issue
Block a user