Ajout CLAUDE.md : guide d'architecture pour Claude Code
This commit is contained in:
100
CLAUDE.md
Normal file
100
CLAUDE.md
Normal file
@@ -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/<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é.
|
||||
Reference in New Issue
Block a user