Initial commit: InkFlow — EPUB vers livre audio local (MLX/Kokoro)
This commit is contained in:
170
backend/inkflow/settings.py
Normal file
170
backend/inkflow/settings.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Reglages techniques editables au runtime (globaux a l'app).
|
||||
|
||||
Contrairement a `config.py` (constantes figees lues a l'import, surchargeables
|
||||
seulement par variables d'environnement au demarrage), ce module expose un objet
|
||||
`Settings` *persiste* dans `data/settings.json` et modifiable depuis l'UI.
|
||||
|
||||
Les valeurs par defaut reprennent celles de `config.py`. Le code du pipeline
|
||||
consulte `get_settings()` au moment de l'execution ; une sauvegarde invalide les
|
||||
caches de modeles (backends TTS, chargement Gemma) pour que les nouveaux
|
||||
identifiants/parametres prennent effet sans redemarrage.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from . import config
|
||||
|
||||
# --- Prompts systeme par defaut (source canonique) ---------------------------
|
||||
# Ces chaines pilotent les trois taches Gemma. L'utilisateur peut les editer.
|
||||
DEFAULT_PROMPT_SPEAKERS = (
|
||||
"Tu es un assistant d'analyse litteraire. Tu identifies QUI prononce chaque "
|
||||
"replique de dialogue dans un extrait de roman en francais. Une liste des "
|
||||
"personnages du chapitre t'est fournie : choisis le locuteur dans cette "
|
||||
"liste en recopiant son nom EXACTEMENT. Appuie-toi sur la narration qui "
|
||||
"PRECEDE et qui SUIT chaque replique (incise d'attribution type 'dit "
|
||||
"Marie'), sur les vocatifs (le personnage a qui l'on s'adresse) et sur "
|
||||
"l'alternance des tours de parole. Mets 'inconnu' si tu n'es pas sur. Tu "
|
||||
"reponds UNIQUEMENT en JSON valide, sans texte autour."
|
||||
)
|
||||
DEFAULT_PROMPT_SPEAKERS_REFINE = (
|
||||
"Tu es un assistant d'analyse litteraire. On te donne des repliques dont le "
|
||||
"locuteur est reste indetermine, avec le locuteur DEJA identifie des "
|
||||
"repliques voisines. Deduis qui parle en exploitant l'alternance des tours "
|
||||
"de parole et le contexte narratif autour. Choisis le nom dans la liste des "
|
||||
"personnages fournie, en le recopiant exactement, ou 'inconnu' si vraiment "
|
||||
"indeterminable. Tu reponds UNIQUEMENT en JSON valide, sans texte autour."
|
||||
)
|
||||
DEFAULT_PROMPT_CHARACTERS = (
|
||||
"Tu es un assistant d'analyse litteraire. Tu extrais la liste des "
|
||||
"personnages d'un extrait de roman et leurs attributs vocaux. Tu reponds "
|
||||
"UNIQUEMENT en JSON valide."
|
||||
)
|
||||
DEFAULT_PROMPT_PRONUNCIATION = (
|
||||
"Tu es un assistant de preparation de livre audio en francais. Tu reperes "
|
||||
"les mots dont la prononciation par un synthetiseur vocal francais risque "
|
||||
"d'etre incorrecte (noms propres etrangers, termes de science-fiction, "
|
||||
"acronymes). Tu reponds UNIQUEMENT en JSON valide."
|
||||
)
|
||||
DEFAULT_PROMPT_INCISES = (
|
||||
"Tu es un assistant d'analyse litteraire. Tu reperes les INCISES de "
|
||||
"narration inserees dans une replique de dialogue (ex: 'dit Mamie', "
|
||||
"'repondit le capitaine'). Tu reponds UNIQUEMENT en JSON valide, sans "
|
||||
"texte autour."
|
||||
)
|
||||
DEFAULT_PROMPT_DEDUP = (
|
||||
"Tu es un assistant d'analyse litteraire. Tu rapproches les differentes "
|
||||
"facons de nommer un meme personnage (nom complet, prenom, surnom, "
|
||||
"diminutif) pour eviter les doublons dans le casting d'un livre audio. Tu "
|
||||
"ne fusionnes deux noms que si c'est, avec certitude, la meme personne. Tu "
|
||||
"reponds UNIQUEMENT en JSON valide, sans texte autour."
|
||||
)
|
||||
|
||||
|
||||
class Settings(BaseModel):
|
||||
"""Reglages techniques globaux, persistes dans data/settings.json."""
|
||||
|
||||
# --- Modeles MLX (identifiants HuggingFace) ---
|
||||
gemma_model: str = config.GEMMA_MODEL
|
||||
qwen3_model: str = config.QWEN3_TTS_MODEL
|
||||
kokoro_model: str = config.KOKORO_MODEL
|
||||
|
||||
# --- Generation Gemma ---
|
||||
gemma_temperature: float = Field(0.1, ge=0.0, le=2.0)
|
||||
gemma_max_tokens: int = Field(2048, ge=64, le=8192)
|
||||
|
||||
# --- Prompts systeme (analyse) ---
|
||||
prompt_speakers: str = DEFAULT_PROMPT_SPEAKERS
|
||||
prompt_speakers_refine: str = DEFAULT_PROMPT_SPEAKERS_REFINE
|
||||
prompt_characters: str = DEFAULT_PROMPT_CHARACTERS
|
||||
prompt_pronunciation: str = DEFAULT_PROMPT_PRONUNCIATION
|
||||
prompt_incises: str = DEFAULT_PROMPT_INCISES # DEPRECIE (detection deterministe)
|
||||
prompt_dedup: str = DEFAULT_PROMPT_DEDUP
|
||||
|
||||
# --- Incises ---
|
||||
# DEPRECIE : la detection d'incises est desormais deterministe et conscience
|
||||
# du casting (analysis.segmenter.detect_incises), sans fallback Gemma. Champ
|
||||
# conserve pour charger les settings.json existants sans erreur.
|
||||
split_incises_use_gemma: bool = True
|
||||
|
||||
# --- Attribution retroactive (2e passe sur les repliques indeterminees) ---
|
||||
# Apres la 1re passe, une 2e passe ciblee re-resout les repliques restees
|
||||
# 'inconnu' (ou peu sures) en s'appuyant sur les voisins deja identifies.
|
||||
# Declenchee seulement s'il reste des doutes -> cout nul sinon.
|
||||
retro_pass_use_gemma: bool = True
|
||||
|
||||
# --- Deduplication du casting ---
|
||||
# Heuristique (sure, deterministe) par defaut. La passe Gemma rattache en
|
||||
# plus les variantes non evidentes (diminutifs, titres) mais, avec un petit
|
||||
# modele local, produit des fusions erronees -> opt-in.
|
||||
dedup_use_gemma: bool = False
|
||||
|
||||
# --- TTS ---
|
||||
default_backend: str = "kokoro"
|
||||
language: str = config.DEFAULT_LANGUAGE
|
||||
kokoro_lang_code: str = config.KOKORO_LANG_CODE
|
||||
kokoro_default_voice: str = config.KOKORO_DEFAULT_VOICE
|
||||
qwen3_default_voice: str = config.QWEN3_DEFAULT_VOICE
|
||||
|
||||
# --- Audio (encodage final) ---
|
||||
target_sample_rate: int = Field(config.TARGET_SAMPLE_RATE, ge=8000, le=48000)
|
||||
mp3_bitrate: str = config.MP3_BITRATE
|
||||
target_dbfs: float = Field(config.TARGET_DBFS, ge=-40.0, le=0.0)
|
||||
|
||||
|
||||
_LOCK = threading.Lock()
|
||||
_cache: Optional[Settings] = None
|
||||
|
||||
|
||||
def settings_path():
|
||||
return config.DATA_DIR / "settings.json"
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
"""Renvoie les reglages courants (charges depuis le disque une seule fois)."""
|
||||
global _cache
|
||||
with _LOCK:
|
||||
if _cache is None:
|
||||
path = settings_path()
|
||||
if path.exists():
|
||||
try:
|
||||
_cache = Settings.model_validate_json(
|
||||
path.read_text(encoding="utf-8"))
|
||||
except Exception: # noqa: BLE001 — fichier corrompu -> defauts
|
||||
_cache = Settings()
|
||||
else:
|
||||
_cache = Settings()
|
||||
return _cache
|
||||
|
||||
|
||||
def save_settings(settings: Settings) -> Settings:
|
||||
"""Persiste les reglages et invalide les caches de modeles."""
|
||||
global _cache
|
||||
with _LOCK:
|
||||
_cache = settings
|
||||
path = settings_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(settings.model_dump_json(indent=2), encoding="utf-8")
|
||||
_invalidate_model_caches()
|
||||
return settings
|
||||
|
||||
|
||||
def _invalidate_model_caches() -> None:
|
||||
"""Force le rechargement des modeles apres un changement d'identifiant/param.
|
||||
|
||||
`get_backend` est cache par *nom* de backend, pas par id de modele ; sans
|
||||
purge, un changement d'id serait ignore. Idem pour le chargement Gemma.
|
||||
"""
|
||||
try:
|
||||
from .tts.factory import get_backend
|
||||
get_backend.cache_clear()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
try:
|
||||
from .analysis.gemma import _load
|
||||
_load.cache_clear()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
Reference in New Issue
Block a user