# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. InkFlow transforme un **EPUB** en **livre audio** (1 dossier/livre, 1 MP3/chapitre, tags ID3 + cover), 100 % en local sur Mac Apple Silicon via **MLX**. Analyse de texte par **Gemma** (`mlx-lm`), synthèse vocale par backend pluggable **Kokoro** (rapide, previews/mono-narrateur) ou **Qwen3-TTS** (qualité + clonage, rendu final multi-voix). Optimisé pour le **français**. ## Commandes ```bash # Installation (Python >= 3.11, macOS arm64) brew install ffmpeg espeak-ng # prérequis système python3.13 -m venv .venv && source .venv/bin/activate pip install -e backend # installe le package `inkflow` python backend/scripts/setup_models.py # vérifie l'env + télécharge les modèles MLX # Pipeline CLI (point d'entrée : `inkflow`, défini dans backend/inkflow/cli.py) inkflow parse "samples/livre.epub" # EPUB -> data//{book.json, chapters/} inkflow analyze --chapter 5 # un chapitre ; sans --chapter = tous inkflow analyze --force # ré-analyse même si l'artefact existe inkflow cast --dedup # voicebank + auto-assignation des voix inkflow pronounce # dictionnaire de prononciation (Gemma) inkflow render 5 --backend kokoro # rendu rapide mono-narrateur inkflow render 5 --backend qwen3 --no-mono # rendu final multi-voix inkflow render 5 --max-paragraphs 10 # test rapide (tronque) inkflow info # structure d'un livre parsé inkflow serve # API + UI sur http://127.0.0.1:8000 # Sans `pip install -e`, lancer depuis backend/ : python -m inkflow.cli ... # Tests (pytest ; lancer depuis backend/ pour résoudre l'import `inkflow`) cd backend && python -m pytest # toute la suite cd backend && python -m pytest tests/test_incises.py # un fichier cd backend && python -m pytest tests/test_incises.py::test_inversion_au_milieu # un test # Frontend (React + Vite + Tailwind) cd frontend && npm install && npm run build # build -> dist/ (servi par `inkflow serve`) cd frontend && npm run dev # dev sur :5173, proxy /api+/ws vers :8000 ``` Le développement frontend nécessite `inkflow serve` (backend :8000) **et** `npm run dev` (UI :5173) en parallèle. En production, `inkflow serve` sert `frontend/dist/` sur le même port que l'API. ## Architecture ### Pipeline orienté artefacts (reprenable) Chaque étape lit l'artefact JSON de la précédente et écrit le sien dans `data//`. Le contrat entre étapes = les modèles pydantic v2 de `backend/inkflow/models.py` (sérialisés tels quels). C'est **ce qui rend le pipeline reprenable** : l'existence d'un fichier signale qu'une étape est faite. ``` parse → book.json + chapters/chNN.json (epub/parser.py) analyze → analysis/chNN.json + cast.json (analysis/segmenter.py) cast → cast.json enrichi (voix) (casting/assign.py + voicebank.py) pronounce → pronunciation.json (analysis/pronunciation.py) render → output//NN-....mp3 (pipeline/render.py + audio/postprocess.py) ``` `store/artifacts.py` centralise toute lecture/écriture de ces artefacts (`load_*`/`save_*`). Ne pas lire/écrire ces JSON ailleurs. ### Deux niveaux de configuration (distinction importante) - **`config.py`** : constantes figées lues à l'import, surchargeables **uniquement** par variables d'environnement au démarrage (`INKFLOW_*`). Chemins, IDs de modèles MLX par défaut, params audio. - **`settings.py`** : objet `Settings` **persisté** dans `data/settings.json`, éditable au runtime depuis l'UI. Contient aussi les **prompts système Gemma** (éditables). Le pipeline consulte `get_settings()` au moment de l'exécution. `save_settings()` invalide les caches modèles (`get_backend.cache_clear()`, `analysis.llm.factory.reset_llm_cache()`) pour appliquer les changements **sans redémarrage**. ### Orchestration (UI temps réel) `pipeline/orchestrator.py` expose un singleton `orchestrator` avec un **unique worker thread** (un Mac = une charge MLX à la fois) : les jobs sont enfilés et rendent la main immédiatement à l'API. L'état (`ProjectState`) est persisté dans `data//state.json` et diffusé par WebSocket à chaque changement via un `broadcaster` injecté par la couche API (l'orchestrateur reste indépendant de FastAPI). `load_state()` appelle `_reconcile()` : il **réaligne l'état sur les artefacts présents sur disque**, donc le travail fait en CLI (ou après redémarrage) est reflété dans l'UI sans le rejouer. Quand on ajoute une étape, penser à étendre `_reconcile()`. `api/app.py` : routes lourdes (analyze/cast/pronounce/render) → `orchestrator.run_*` (enfilées, renvoient `{queued: True}`) ; routes rapides (preview de voix) → threadpool ; lecture/écriture directe pour cast/pronunciation/settings. WebSocket sur `/ws/{slug}`. ### Détection d'incises — déterministe et cast-aware (cœur de l'attribution) `analysis/segmenter.py` est le module le plus subtil. La segmentation narration/dialogue est déterministe (un paragraphe commençant par cadratin `—` est une réplique). L'attribution du locuteur est **hybride** : 1. **Détection d'incises déterministe** (`detect_incises`) : deux passes regex — inversion verbe-pronom (`dit-il`) et nominale consciente du casting (`compatit Holden`, `informa le soldat`). Les incises sont des **bornes** (offsets) annotées sur la réplique persistée entière, **non destructif** ; le découpage en voix narrateur/personnage se fait au rendu (`iter_incise_pieces`). 2. **Seeding** : une incise nominale qui nomme un personnage **fixe le locuteur avant l'appel LLM** (corrige les ratés du petit modèle). 3. **Attribution LLM** (`attribute_speakers`) : Gemma résout les répliques restantes par chunks, avec contexte narratif avant/après et confidence. 4. **Passe rétroactive** (`_refine_unknown_speakers`) : re-résout les répliques `inconnu`/`low` via l'alternance des tours. Coût nul s'il ne reste aucun doute. Cette logique est **pure et testée** (`tests/test_incises.py`, 30+ cas sans Gemma). **Toute modification des regexes/verbes/rôles doit garder ces tests verts.** Préférer rater une incise qu'en inventer une (les listes `_SPEECH_VERBS`/`_ROLE_NOUNS` sont curées en ce sens). ### TTS pluggable `tts/base.py` définit `TTSBackend.synthesize(text, VoiceSpec) -> (audio mono float32, sample_rate)` et `VoiceSpec` (preset pour Kokoro, ref_audio/ref_text pour le clonage Qwen3). `tts/factory.get_backend(name)` est `lru_cache` par **nom** (pas par id de modèle — d'où l'invalidation explicite dans settings). `pipeline/render.py` construit des `RenderUnit` (mono ou multi-voix), où `glued_to_prev` réduit le silence pour les incises rattachées à la réplique précédente. ### LLM d'analyse pluggable `analysis/llm/` suit le **même pattern que le TTS**. La façade `client.LLM` (anciennement `Gemma`) expose `generate`/`generate_json` consommés par tout le pipeline ; elle porte toute la logique agnostique du moteur (calcul des params depuis `Settings`, retrait de la pensée des modèles à raisonnement, extraction JSON tolérante — helpers dans `_text.py`, testés purs dans `tests/test_gemma_reasoning.py`). Sous elle, `base.LLMBackend.complete(messages, ...) -> str` (texte brut) a deux implémentations : **`mlx_backend`** (mlx-lm, défaut) et **`lmstudio_backend`** (API OpenAI locale de LM Studio, sert GGUF *et* MLX). Sélection par nom via `factory.get_llm_backend(backend, model_ref)` (`lru_cache`, `reset_llm_cache()` pour invalider). Backend choisi dans `settings.gemma_backend` (`mlx`/`lmstudio`), surchargeable par `--backend`/`--model` sur les commandes CLI `analyze`/`pronounce`/`cast`/`benchmark`. LM Studio doit tourner avec son serveur local actif (onglet Developer). **Qui possède la config de génération ?** Les réglages `gemma_temperature`/`gemma_max_tokens`/`gemma_reasoning*` pilotent le backend **MLX** (seule source de config pour `mlx-lm`). Pour **LM Studio**, c'est la config du modèle **dans LM Studio** qui prime : par défaut (`settings.lmstudio_defer_config=True`) le backend **n'impose ni `temperature` ni `max_tokens`** dans la requête — imposer `max_tokens` tronquait la réponse des modèles à raisonnement (pensée non terminée → « aucun JSON »). Le **contexte** se règle aussi côté LM Studio (au chargement : `lms load --context-length N` ou l'UI) — InkFlow ne peut pas le porter, d'où l'erreur « context length » si le modèle est JIT-chargé avec un contexte trop court pour un chapitre. Mettre `lmstudio_defer_config=False` pour réimposer les réglages InkFlow (benchmarks reproductibles). LM Studio sépare déjà la pensée (`reasoning_content`) de la réponse (`content`) : le backend ne renvoie que `content`. ## Fichiers de référence (vérité terrain pour l'attribution) `data//reference/chNN.json` contient des **versions corrigées à la main** de la sortie d'analyse `analysis/chNN.json` — **même schéma `ChapterAnalysis`** (`index`, `title`, `segments` avec `type`/`text`/`speaker`/`incises`). Ce sont des **fixtures de vérité terrain** servant à juger la qualité de la segmentation, des incises et de l'attribution des locuteurs : on compare la sortie réelle du pipeline à la référence. - Fixture canonique : `data/la-colere-de-tiamat/reference/ch05.json` (PROLOGUE - HOLDEN), plus `ch06.json`. - **Aucun code ne les charge** : ce sont des références manuelles, distinctes des tests unitaires purs de `tests/test_incises.py`. Elles vérifient le comportement **bout-en-bout avec Gemma**, que les tests purs ne couvrent pas. - Sous `data/` → **git-ignored** : présents localement, non versionnés. Les régénérer/corriger à la main quand on retravaille la logique d'attribution, puis comparer manuellement à `analyze --force`. ## Pièges d'environnement - **espeak-ng** : Kokoro en français en dépend via phonemizer. `config.setup_espeak()` localise automatiquement `libespeak-ng.dylib` (homebrew) ; sinon exporter `PHONEMIZER_ESPEAK_LIBRARY`. - **Modèle Gemma = goulot d'étranglement** : `gemma-3-4b-it-4bit` par défaut. C'est le petit modèle qui rate des attributions — d'où le seeding déterministe et la dedup heuristique-first. - **`dedup_use_gemma = False` par défaut** : la dedup du casting est heuristique (sûre) ; la passe Gemma (opt-in `--llm`) rattache les variantes non évidentes mais produit des fusions erronées avec un petit modèle local. - **Encodage MP3 via ffmpeg CLI** (pas pydub pour l'encodage final) ; tags/cover via mutagen. - Les répertoires `data/`, `output/`, `samples/`, `node_modules/` sont git-ignored. `voicebank/` (clips de référence) est versionné.