From c1ab6796868fdb5912d184bec40746a46ec30863 Mon Sep 17 00:00:00 2001 From: colgora Date: Sun, 21 Jun 2026 00:15:04 +0200 Subject: [PATCH] Ajout CLAUDE.md : guide d'architecture pour Claude Code --- CLAUDE.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..89a872f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# 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é.