# 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/<slug>/{book.json, chapters/}
inkflow analyze <slug> --chapter 5          # un chapitre ; sans --chapter = tous
inkflow analyze <slug> --force              # ré-analyse même si l'artefact existe
inkflow cast <slug> --dedup                 # voicebank + auto-assignation des voix
inkflow pronounce <slug>                    # dictionnaire de prononciation (Gemma)
inkflow render <slug> 5 --backend kokoro            # rendu rapide mono-narrateur
inkflow render <slug> 5 --backend qwen3 --no-mono   # rendu final multi-voix
inkflow render <slug> 5 --max-paragraphs 10         # test rapide (tronque)
inkflow info <slug>                          # 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/<slug>/`. 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/<titre>/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/<slug>/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/<slug>/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é.
