# 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()`, `gemma._load.cache_clear()`) 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. ## 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é.