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
106 lines
4.0 KiB
Python
106 lines
4.0 KiB
Python
"""Configuration centrale d'InkFlow.
|
|
|
|
Toutes les constantes (chemins, identifiants de modeles MLX, parametres par
|
|
defaut) sont regroupees ici pour rester facilement surchargeables via variables
|
|
d'environnement.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
# --- Racines du projet -------------------------------------------------------
|
|
# config.py est dans backend/inkflow/, la racine projet est donc deux niveaux
|
|
# au-dessus de backend/.
|
|
BACKEND_DIR = Path(__file__).resolve().parents[1]
|
|
PROJECT_ROOT = BACKEND_DIR.parent
|
|
|
|
|
|
def _env_path(var: str, default: Path) -> Path:
|
|
return Path(os.environ.get(var, default)).expanduser().resolve()
|
|
|
|
|
|
# Donnees de travail (etat par livre : json, db, wav intermediaires)
|
|
DATA_DIR = _env_path("INKFLOW_DATA_DIR", PROJECT_ROOT / "data")
|
|
# Sortie finale (1 dossier par livre, 1 mp3 par chapitre)
|
|
OUTPUT_DIR = _env_path("INKFLOW_OUTPUT_DIR", PROJECT_ROOT / "output")
|
|
# Banque de voix de reference (clips + metadata.json)
|
|
VOICEBANK_DIR = _env_path("INKFLOW_VOICEBANK_DIR", PROJECT_ROOT / "voicebank")
|
|
# Echantillons fournis
|
|
SAMPLES_DIR = PROJECT_ROOT / "samples"
|
|
|
|
# --- Moteur LLM d'analyse ----------------------------------------------------
|
|
# Backend par defaut : "mlx" (mlx-lm, Apple Silicon) ou "lmstudio" (API OpenAI
|
|
# locale de LM Studio, sert GGUF *et* MLX charges via sa GUI).
|
|
GEMMA_BACKEND = os.environ.get("INKFLOW_GEMMA_BACKEND", "mlx")
|
|
# Endpoint OpenAI-compatible de LM Studio (onglet Developer > Start Server).
|
|
LMSTUDIO_BASE_URL = os.environ.get(
|
|
"INKFLOW_LMSTUDIO_BASE_URL", "http://127.0.0.1:1234/v1"
|
|
)
|
|
|
|
# --- Modeles MLX (HuggingFace mlx-community) ---------------------------------
|
|
# Analyse de texte : Gemma via mlx-lm (backend "mlx").
|
|
GEMMA_MODEL = os.environ.get(
|
|
"INKFLOW_GEMMA_MODEL", "mlx-community/gemma-3-4b-it-4bit"
|
|
)
|
|
|
|
# TTS : Qwen3-TTS (rendu final, clonage) et Kokoro (preview rapide).
|
|
QWEN3_TTS_MODEL = os.environ.get(
|
|
"INKFLOW_QWEN3_MODEL", "mlx-community/Qwen3-TTS-12Hz-1.7B-Base-8bit"
|
|
)
|
|
KOKORO_MODEL = os.environ.get(
|
|
"INKFLOW_KOKORO_MODEL", "mlx-community/Kokoro-82M-bf16"
|
|
)
|
|
|
|
# --- Parametres TTS ----------------------------------------------------------
|
|
DEFAULT_LANGUAGE = os.environ.get("INKFLOW_LANGUAGE", "French")
|
|
# Code langue Kokoro (misaki) : 'f' = francais.
|
|
KOKORO_LANG_CODE = os.environ.get("INKFLOW_KOKORO_LANG", "f")
|
|
# Voix Kokoro par defaut pour les previews / mono-narrateur rapide.
|
|
KOKORO_DEFAULT_VOICE = os.environ.get("INKFLOW_KOKORO_VOICE", "ff_siwis")
|
|
# Voix Qwen3 par defaut (narrateur) si aucun clip de reference fourni.
|
|
QWEN3_DEFAULT_VOICE = os.environ.get("INKFLOW_QWEN3_VOICE", "Chelsie")
|
|
|
|
# Frequence d'echantillonnage cible pour la concatenation (Hz). Les backends
|
|
# renvoient leur propre sr ; postprocess reechantillonne au besoin.
|
|
TARGET_SAMPLE_RATE = int(os.environ.get("INKFLOW_SAMPLE_RATE", "24000"))
|
|
|
|
# Encodage mp3 final.
|
|
MP3_BITRATE = os.environ.get("INKFLOW_MP3_BITRATE", "128k")
|
|
# Cible de normalisation loudness (LUFS approx via pydub gain).
|
|
TARGET_DBFS = float(os.environ.get("INKFLOW_TARGET_DBFS", "-18.0"))
|
|
|
|
|
|
def book_data_dir(book_slug: str) -> Path:
|
|
"""Dossier de travail pour un livre (artefacts intermediaires)."""
|
|
return DATA_DIR / book_slug
|
|
|
|
|
|
def book_output_dir(book_title: str) -> Path:
|
|
"""Dossier de sortie final pour un livre (mp3 par chapitre)."""
|
|
return OUTPUT_DIR / book_title
|
|
|
|
|
|
def ensure_dirs() -> None:
|
|
for d in (DATA_DIR, OUTPUT_DIR, VOICEBANK_DIR):
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def setup_espeak() -> None:
|
|
"""Localise libespeak-ng pour phonemizer (requis par Kokoro non-anglais).
|
|
|
|
phonemizer ne trouve pas toujours la lib installee via brew ; on pointe
|
|
explicitement PHONEMIZER_ESPEAK_LIBRARY si la variable n'est pas deja fixee.
|
|
"""
|
|
if os.environ.get("PHONEMIZER_ESPEAK_LIBRARY"):
|
|
return
|
|
candidates = [
|
|
"/opt/homebrew/lib/libespeak-ng.dylib",
|
|
"/usr/local/lib/libespeak-ng.dylib",
|
|
"/opt/homebrew/lib/libespeak-ng.1.dylib",
|
|
]
|
|
for path in candidates:
|
|
if os.path.exists(path):
|
|
os.environ["PHONEMIZER_ESPEAK_LIBRARY"] = path
|
|
return
|