maribakulj commited on
Commit
85f0958
·
unverified ·
1 Parent(s): c5915ce

Create CLAUDE.md with project guidelines and structure

Browse files

Added comprehensive project instructions, technical stack details, data model schemas, and API endpoints for Scriptorium AI.

Files changed (1) hide show
  1. CLAUDE.md +620 -0
CLAUDE.md ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Scriptorium AI — Instructions permanentes pour Claude Code
2
+
3
+ ## 1. Contexte du projet
4
+
5
+ Scriptorium AI est une **plateforme générique** de génération d'éditions savantes augmentées
6
+ pour documents patrimoniaux numérisés : manuscrits médiévaux, incunables, cartulaires,
7
+ archives, chartes, papyri — tout type de document, toute époque, toute langue.
8
+
9
+ Pipeline général :
10
+ images sources → ingestion → normalisation → analyse Google AI → JSON maître
11
+ → passes dérivées → ALTO / METS / Manifest IIIF → interface web → validation humaine
12
+
13
+ Le premier démonstrateur est le **Beatus de Saint-Sever** (manuscrit enluminé médiéval,
14
+ latin, BnF Latin 8878). Mais la plateforme n'est PAS un outil Beatus.
15
+ Le Beatus est un profil parmi d'autres.
16
+
17
+ ---
18
+
19
+ ## 2. Stack technique
20
+
21
+ | Composant | Technologie |
22
+ |-----------------|--------------------------------------------------|
23
+ | Backend | Python 3.11+, FastAPI, Uvicorn |
24
+ | Validation | Pydantic v2 (jamais v1) |
25
+ | Base de données | SQLite via SQLAlchemy 2.0 async |
26
+ | IA | Google AI API, modèle sélectionnable dynamiquement|
27
+ | SDK Google | google-generativeai >= 0.3 |
28
+ | XML | lxml |
29
+ | Images | Pillow |
30
+ | Tests | pytest, pytest-cov, pytest-asyncio |
31
+ | Frontend | React + Vite, TypeScript, Tailwind CSS (sprint 4+)|
32
+ | Hébergement | HuggingFace Spaces (Docker) + HF Datasets |
33
+
34
+ ---
35
+
36
+ ## 3. Arborescence du repo — structure canonique
37
+
38
+ ```
39
+ scriptorium-ai/
40
+
41
+ ├── CLAUDE.md ← CE FICHIER
42
+ ├── CONTEXT.md ← état courant du projet (tu ne le modifies pas)
43
+ ├── DECISIONS.md ← décisions figées (tu ne les remets pas en question)
44
+ ├── TODO.md ← tâches de la session courante
45
+
46
+ ├── backend/
47
+ │ ├── app/
48
+ │ │ ├── api/
49
+ │ │ │ └── v1/ ← tous les endpoints FastAPI
50
+ │ │ ├── models/ ← modèles SQLAlchemy (tables BDD)
51
+ │ │ ├── schemas/ ← modèles Pydantic (source canonique des types)
52
+ │ │ │ ├── __init__.py
53
+ │ │ │ ├── corpus_profile.py
54
+ │ │ │ ├── page_master.py
55
+ │ │ │ └── annotation.py
56
+ │ │ └── services/
57
+ │ │ ├── ingest/ ← ingestion corpus
58
+ │ │ ├── image/ ← normalisation + dérivés
59
+ │ │ ├── ai/ ← appels Google AI + parsing + validation
60
+ │ │ ├── export/ ← générateurs ALTO, METS, IIIF
61
+ │ │ └── search/ ← index recherche
62
+ │ ├── tests/
63
+ │ │ ├── __init__.py
64
+ │ │ ├── test_schemas.py
65
+ │ │ └── test_profiles.py
66
+ │ └── pyproject.toml
67
+
68
+ ├── prompts/
69
+ │ ├── medieval-illuminated/
70
+ │ │ ├── primary_v1.txt
71
+ │ │ ├── transcription_v1.txt
72
+ │ │ ├── translation_v1.txt
73
+ │ │ ├── commentary_v1.txt
74
+ │ │ └── iconography_v1.txt
75
+ │ ├── medieval-textual/
76
+ │ │ ├── primary_v1.txt
77
+ │ │ ├── translation_v1.txt
78
+ │ │ └── commentary_v1.txt
79
+ │ ├── early-modern-print/
80
+ │ │ └── primary_v1.txt
81
+ │ └── modern-handwritten/
82
+ │ └── primary_v1.txt
83
+
84
+ ├── profiles/
85
+ │ ├── medieval-illuminated.json
86
+ │ ├── medieval-textual.json
87
+ │ ├── early-modern-print.json
88
+ │ └── modern-handwritten.json
89
+
90
+ ├── data/ ← JAMAIS versionné (.gitignore)
91
+ │ └── corpora/
92
+ │ └── {corpus_slug}/
93
+ │ ├── masters/
94
+ │ ├── derivatives/
95
+ │ ├── iiif/
96
+ │ └── pages/
97
+ │ └── {folio}/
98
+ │ ├── master.json
99
+ │ ├── gemini_raw.json
100
+ │ ├── alto.xml
101
+ │ └── annotations.json
102
+
103
+ └── infra/
104
+ └── Dockerfile
105
+ ```
106
+
107
+ Ne jamais créer de fichiers en dehors de cette arborescence sans demande explicite.
108
+
109
+ ---
110
+
111
+ ## 4. Modèle de données — schémas canoniques
112
+
113
+ ### 4.1 CorpusProfile
114
+
115
+ Entité centrale. Tout le pipeline en est piloté.
116
+ Fichier : `backend/app/schemas/corpus_profile.py`
117
+
118
+ ```python
119
+ class LayerType(str, Enum):
120
+ IMAGE = "image"
121
+ OCR_DIPLOMATIC = "ocr_diplomatic"
122
+ OCR_NORMALIZED = "ocr_normalized"
123
+ TRANSLATION_FR = "translation_fr"
124
+ TRANSLATION_EN = "translation_en"
125
+ SUMMARY = "summary"
126
+ SCHOLARLY_COMMENTARY = "scholarly_commentary"
127
+ PUBLIC_COMMENTARY = "public_commentary"
128
+ ICONOGRAPHY_DETECTION = "iconography_detection"
129
+ MATERIAL_NOTES = "material_notes"
130
+ UNCERTAINTY = "uncertainty"
131
+
132
+ class ScriptType(str, Enum):
133
+ CAROLINE = "caroline"
134
+ GOTHIC = "gothic"
135
+ PRINT = "print"
136
+ CURSIVE = "cursive"
137
+ OTHER = "other"
138
+
139
+ class ExportConfig(BaseModel):
140
+ mets: bool = True
141
+ alto: bool = True
142
+ tei: bool = False
143
+
144
+ class UncertaintyConfig(BaseModel):
145
+ flag_below: float = Field(0.4, ge=0.0, le=1.0)
146
+ min_acceptable: float = Field(0.25, ge=0.0, le=1.0)
147
+
148
+ class CorpusProfile(BaseModel):
149
+ model_config = ConfigDict(frozen=True)
150
+
151
+ profile_id: str
152
+ label: str
153
+ language_hints: list[str]
154
+ script_type: ScriptType
155
+ active_layers: list[LayerType]
156
+ prompt_templates: dict[str, str] # {"primary": "path/v1.txt", ...}
157
+ uncertainty_config: UncertaintyConfig
158
+ export_config: ExportConfig
159
+ ```
160
+
161
+ ### 4.2 PageMaster
162
+
163
+ Source canonique de toute page. Toutes les sorties en dérivent.
164
+ Fichier : `backend/app/schemas/page_master.py`
165
+
166
+ ```python
167
+ class RegionType(str, Enum):
168
+ TEXT_BLOCK = "text_block"
169
+ MINIATURE = "miniature"
170
+ DECORATED_INITIAL = "decorated_initial"
171
+ MARGIN = "margin"
172
+ RUBRIC = "rubric"
173
+ OTHER = "other"
174
+
175
+ class Region(BaseModel):
176
+ id: str
177
+ type: RegionType
178
+ bbox: list[int] = Field(..., min_length=4, max_length=4)
179
+ confidence: float = Field(..., ge=0.0, le=1.0)
180
+ polygon: list[list[int]] | None = None
181
+ parent_region_id: str | None = None
182
+
183
+ @field_validator("bbox")
184
+ @classmethod
185
+ def bbox_must_be_positive(cls, v):
186
+ if any(x < 0 for x in v):
187
+ raise ValueError("bbox values must be >= 0")
188
+ if v[2] <= 0 or v[3] <= 0:
189
+ raise ValueError("bbox width and height must be > 0")
190
+ return v
191
+
192
+ class OCRResult(BaseModel):
193
+ diplomatic_text: str = ""
194
+ blocks: list[dict] = []
195
+ lines: list[dict] = []
196
+ language: str = "la"
197
+ confidence: float = Field(0.0, ge=0.0, le=1.0)
198
+ uncertain_segments: list[str] = []
199
+
200
+ class Translation(BaseModel):
201
+ fr: str = ""
202
+ en: str = ""
203
+
204
+ class CommentaryClaim(BaseModel):
205
+ claim: str
206
+ evidence_region_ids: list[str] = []
207
+ certainty: Literal["high", "medium", "low", "speculative"] = "medium"
208
+
209
+ class Commentary(BaseModel):
210
+ public: str = ""
211
+ scholarly: str = ""
212
+ claims: list[CommentaryClaim] = []
213
+
214
+ class ProcessingInfo(BaseModel):
215
+ model_id: str
216
+ model_display_name: str
217
+ prompt_version: str
218
+ raw_response_path: str
219
+ processed_at: datetime
220
+ cost_estimate_usd: float | None = None
221
+
222
+ class EditorialStatus(str, Enum):
223
+ MACHINE_DRAFT = "machine_draft"
224
+ NEEDS_REVIEW = "needs_review"
225
+ REVIEWED = "reviewed"
226
+ VALIDATED = "validated"
227
+ PUBLISHED = "published"
228
+
229
+ class EditorialInfo(BaseModel):
230
+ status: EditorialStatus = EditorialStatus.MACHINE_DRAFT
231
+ validated: bool = False
232
+ validated_by: str | None = None
233
+ version: int = 1
234
+ notes: list[str] = []
235
+
236
+ class PageMaster(BaseModel):
237
+ schema_version: str = "1.0"
238
+ page_id: str
239
+ corpus_profile: str # profile_id du CorpusProfile
240
+ manuscript_id: str
241
+ folio_label: str
242
+ sequence: int
243
+
244
+ image: dict # master, derivative_web, iiif_base, width, height
245
+ layout: dict # {"regions": [Region, ...]}
246
+ ocr: OCRResult | None = None
247
+ translation: Translation | None = None
248
+ summary: dict | None = None # {"short": str, "detailed": str}
249
+ commentary: Commentary | None = None
250
+ extensions: dict[str, Any] = {} # données spécifiques au profil
251
+
252
+ processing: ProcessingInfo | None = None
253
+ editorial: EditorialInfo = EditorialInfo()
254
+ ```
255
+
256
+ ### 4.3 AnnotationLayer
257
+
258
+ Fichier : `backend/app/schemas/annotation.py`
259
+
260
+ ```python
261
+ class LayerStatus(str, Enum):
262
+ PENDING = "pending"
263
+ RUNNING = "running"
264
+ DONE = "done"
265
+ FAILED = "failed"
266
+ NEEDS_REVIEW = "needs_review"
267
+ VALIDATED = "validated"
268
+
269
+ class AnnotationLayer(BaseModel):
270
+ id: str
271
+ page_id: str
272
+ layer_type: LayerType
273
+ status: LayerStatus = LayerStatus.PENDING
274
+ version: int = 1
275
+ source_model: str | None = None
276
+ prompt_version: str | None = None
277
+ created_at: datetime
278
+ ```
279
+
280
+ ---
281
+
282
+ ## 5. Règles absolues — NE JAMAIS ENFREINDRE
283
+
284
+ ### R01 — Aucune logique hardcodée par corpus
285
+ Jamais de condition du type `if corpus == "beatus"` ou `if profile == "medieval-illuminated"`.
286
+ Toute logique spécifique passe par le CorpusProfile. Le code est générique.
287
+
288
+ ### R02 — Le JSON maître est la source canonique
289
+ Toutes les sorties (IIIF, ALTO, METS, annotations) sont générées depuis le PageMaster JSON.
290
+ On ne génère jamais une sortie directement depuis la réponse brute de l'IA.
291
+
292
+ ### R03 — Convention bbox [x, y, width, height] UNIQUEMENT
293
+ Format : [x, y, largeur, hauteur] en pixels entiers dans l'image source.
294
+ - x, y = coin supérieur gauche
295
+ - width, height = dimensions
296
+ JAMAIS [x1, y1, x2, y2] (coins opposés).
297
+ JAMAIS de coordonnées relatives ou normalisées (0.0–1.0).
298
+ Le validator Pydantic doit rejeter toute bbox avec width ou height <= 0.
299
+
300
+ ### R04 — Prompts dans des fichiers, jamais dans le code
301
+ Les prompts vivent dans prompts/{profile_id}/{famille}_v{n}.txt
302
+ Le code charge le fichier, injecte les variables, envoie à l'API.
303
+ Jamais de f-string de prompt hardcodée dans un fichier .py.
304
+
305
+ ### R05 — Double stockage des réponses IA
306
+ Toujours écrire DEUX fichiers distincts :
307
+ - `gemini_raw.json` : réponse brute telle que retournée par l'API
308
+ - `master.json` : JSON parsé, validé par Pydantic, canonique
309
+ Un seul fichier = bug. Les deux sont obligatoires.
310
+
311
+ ### R06 — Clé API jamais dans le code
312
+ La clé API Google AI vit uniquement dans les variables d'environnement.
313
+ Jamais dans : le code, les logs, les fichiers versionnés, les exports, les JSON maîtres.
314
+ Variable d'environnement : GOOGLE_AI_API_KEY
315
+
316
+ ### R07 — Pydantic v2 exclusivement
317
+ Syntaxe v2 : `model_config = ConfigDict(...)` et non `class Config:`
318
+ `@field_validator` et non `@validator`
319
+ `model_validate()` et non `parse_obj()`
320
+ Imports : `from pydantic import BaseModel, ConfigDict, Field, field_validator`
321
+
322
+ ### R08 — Tests pour tout modèle de données
323
+ Aucun nouveau schéma Pydantic sans test correspondant.
324
+ Aucun profil JSON sans test de chargement et validation.
325
+ Les tests ne sont pas optionnels.
326
+
327
+ ### R09 — schema_version dans tout JSON maître
328
+ Le champ `schema_version: str = "1.0"` est obligatoire dans PageMaster.
329
+ Si le schéma change, la version change.
330
+
331
+ ### R10 — Endpoints préfixés /api/v1/
332
+ Tous les endpoints FastAPI sont sous /api/v1/.
333
+ Exemple : /api/v1/corpora, /api/v1/pages/{id}/master-json
334
+
335
+ ---
336
+
337
+ ## 6. Anti-patterns — ce qui est interdit
338
+
339
+ ```python
340
+ # ❌ INTERDIT — logique hardcodée par corpus
341
+ if profile_id == "medieval-illuminated":
342
+ process_iconography()
343
+
344
+ # ✅ CORRECT — piloté par le profil
345
+ if "iconography_detection" in corpus_profile.active_layers:
346
+ process_iconography()
347
+
348
+ # ❌ INTERDIT — prompt hardcodé dans le code
349
+ prompt = f"Tu analyses un manuscrit {profile.label}. Retourne ce JSON..."
350
+
351
+ # ✅ CORRECT — prompt chargé depuis fichier versionné
352
+ prompt_path = corpus_profile.prompt_templates["primary"]
353
+ prompt = load_and_render_prompt(prompt_path, context)
354
+
355
+ # ❌ INTERDIT — bbox en coordonnées de coins opposés
356
+ bbox = [x1, y1, x2, y2]
357
+
358
+ # ✅ CORRECT — bbox en [x, y, width, height]
359
+ bbox = [x, y, x2 - x1, y2 - y1]
360
+
361
+ # ❌ INTERDIT — pydantic v1
362
+ class MyModel(BaseModel):
363
+ class Config:
364
+ frozen = True
365
+
366
+ # ✅ CORRECT — pydantic v2
367
+ class MyModel(BaseModel):
368
+ model_config = ConfigDict(frozen=True)
369
+
370
+ # ❌ INTERDIT — réponse brute non conservée
371
+ master_json = parse_ai_response(response)
372
+ save(master_json)
373
+
374
+ # ✅ CORRECT — double stockage obligatoire
375
+ save_raw(response, path="gemini_raw.json")
376
+ master_json = parse_and_validate(response)
377
+ save_canonical(master_json, path="master.json")
378
+
379
+ # ❌ INTERDIT — clé API dans le code
380
+ client = genai.Client(api_key="AIza...")
381
+
382
+ # ✅ CORRECT — depuis l'environnement
383
+ client = genai.Client(api_key=os.environ["GOOGLE_AI_API_KEY"])
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 7. Conventions de code
389
+
390
+ ### Nommage
391
+ - Python : snake_case pour variables et fonctions, PascalCase pour classes
392
+ - TypeScript (sprint 4+) : camelCase pour variables, PascalCase pour composants
393
+ - Fichiers Python : snake_case.py
394
+ - Fichiers de prompts : {famille}_v{n}.txt (ex: primary_v1.txt, commentary_v2.txt)
395
+ - Profils JSON : {profile_id}.json (ex: medieval-illuminated.json)
396
+ - IDs de pages : {corpus_slug}-{folio_label} (ex: beatus-lat8878-0013r)
397
+
398
+ ### Structure d'un fichier Python
399
+ ```python
400
+ """
401
+ Module docstring courte (1–2 lignes max).
402
+ """
403
+ # 1. stdlib
404
+ import os
405
+ from datetime import datetime
406
+ from typing import Any
407
+
408
+ # 2. third-party
409
+ from pydantic import BaseModel, Field
410
+
411
+ # 3. local
412
+ from app.schemas.corpus_profile import CorpusProfile
413
+ ```
414
+
415
+ ### Gestion d'erreurs
416
+ ```python
417
+ # ✅ Exceptions explicites avec message utile
418
+ if not image_path.exists():
419
+ raise FileNotFoundError(f"Image not found: {image_path}")
420
+
421
+ # ✅ Logging structuré
422
+ import logging
423
+ logger = logging.getLogger(__name__)
424
+ logger.info("Processing page", extra={"page_id": page_id, "profile": profile_id})
425
+
426
+ # ❌ Jamais
427
+ try:
428
+ ...
429
+ except:
430
+ pass # silence total = bug silencieux
431
+ ```
432
+
433
+ ### Type hints
434
+ - Obligatoires sur toutes les signatures de fonctions
435
+ - `Any` accepté uniquement pour les extensions de profil
436
+ - Préférer `str | None` à `Optional[str]` (Python 3.10+ syntax)
437
+
438
+ ---
439
+
440
+ ## 8. Pipeline — étapes et responsabilités
441
+
442
+ ```
443
+ Étape 1 — Ingestion
444
+ Input : dossier local / ZIP / URLs IIIF / manifest IIIF
445
+ Output : enregistrements Corpus + Manuscript + Page en SQLite
446
+ Status : INGESTED
447
+ Règle : aucun appel IA, aucune image modifiée
448
+
449
+ Étape 2 — Préparation image
450
+ Input : image master (TIFF / JP2 / JPEG / PNG)
451
+ Output : dérivé JPEG 1500px max pour l'IA + thumbnail
452
+ Status : PREPARED
453
+ Règle : jamais envoyer le master brut à l'IA
454
+
455
+ Étape 3 — Analyse primaire IA (1 seul appel par page)
456
+ Input : dérivé JPEG + prompt primary_v1.txt rendu avec le profil
457
+ Output : gemini_raw.json (brut) + master.json partiel (layout + OCR)
458
+ Status : ANALYZED
459
+ Règle : 1 seule passe visuelle. Pas d'appels concurrents sur la même image.
460
+
461
+ Étape 4 — Passes dérivées (selon active_layers du profil)
462
+ Input : master.json de l'étape 3
463
+ Output : master.json enrichi (traduction, commentaire, iconographie)
464
+ Status : LAYERED
465
+ Règle : les passes dérivées sont textuelles. Pas de nouvelle passe visuelle
466
+ sauf pour l'iconographie (crops des régions uniquement).
467
+
468
+ Étape 5 — Génération documentaire
469
+ Input : master.json complet
470
+ Output : alto.xml + mets.xml + manifest.json + annotations IIIF
471
+ Status : EXPORTED
472
+ Règle : toujours régénérable depuis master.json. Ne jamais éditer les XML
473
+ manuellement — ils sont des sorties dérivées.
474
+
475
+ Étape 6 — Validation humaine
476
+ Input : master.json + interface
477
+ Output : master.json corrigé avec version incrémentée
478
+ Status : VALIDATED → PUBLISHED
479
+ ```
480
+
481
+ ---
482
+
483
+ ## 9. Modèle Google AI — sélection dynamique
484
+
485
+ Le modèle n'est jamais hardcodé. Flux :
486
+ 1. Utilisateur fournit sa clé API → `POST /api/v1/settings/api-key`
487
+ 2. Plateforme appelle Google AI List Models → filtre sur `generateContent` + vision
488
+ 3. Utilisateur sélectionne un modèle → `PUT /api/v1/corpora/{id}/model`
489
+ 4. Modèle stocké dans `ModelConfig` par corpus (pas dans CorpusProfile)
490
+ 5. Chaque appel IA journalise `model_id` + `model_display_name`
491
+
492
+ Entité `ModelConfig` (par corpus) :
493
+ ```python
494
+ class ModelConfig(BaseModel):
495
+ corpus_id: str
496
+ selected_model_id: str # ID technique Google AI
497
+ selected_model_display_name: str
498
+ supports_vision: bool
499
+ last_fetched_at: datetime
500
+ available_models: list[dict] # cache de la liste
501
+ ```
502
+
503
+ ---
504
+
505
+ ## 10. Statuts métier
506
+
507
+ ### Corpus / Page
508
+ ```
509
+ CREATED → INGESTING → INGESTED → PROCESSING → READY → ERROR
510
+ INGESTED → PREPARED → ANALYZED → LAYERED → EXPORTED → VALIDATED → ERROR
511
+ ```
512
+
513
+ ### Couche (AnnotationLayer)
514
+ ```
515
+ PENDING → RUNNING → DONE → FAILED → NEEDS_REVIEW → VALIDATED
516
+ ```
517
+
518
+ ### Éditorial (PageMaster.editorial.status)
519
+ ```
520
+ machine_draft → needs_review → reviewed → validated → published
521
+ ```
522
+
523
+ ---
524
+
525
+ ## 11. Endpoints API — liste complète
526
+
527
+ ```
528
+ # Configuration & modèles
529
+ POST /api/v1/settings/api-key
530
+ GET /api/v1/models
531
+ POST /api/v1/models/refresh
532
+ PUT /api/v1/corpora/{id}/model
533
+ GET /api/v1/corpora/{id}/model
534
+
535
+ # Profils
536
+ GET /api/v1/profiles
537
+ GET /api/v1/profiles/{id}
538
+
539
+ # Corpus
540
+ POST /api/v1/corpora
541
+ GET /api/v1/corpora
542
+ GET /api/v1/corpora/{id}
543
+ DELETE /api/v1/corpora/{id}
544
+
545
+ # Ingestion
546
+ POST /api/v1/corpora/{id}/ingest/files
547
+ POST /api/v1/corpora/{id}/ingest/iiif-manifest
548
+ POST /api/v1/corpora/{id}/ingest/iiif-images
549
+
550
+ # Jobs
551
+ POST /api/v1/corpora/{id}/run
552
+ POST /api/v1/pages/{id}/run
553
+ GET /api/v1/jobs/{job_id}
554
+ POST /api/v1/jobs/{job_id}/retry
555
+
556
+ # Pages
557
+ GET /api/v1/pages/{id}
558
+ GET /api/v1/pages/{id}/master-json
559
+ PUT /api/v1/pages/{id}/master-json
560
+ GET /api/v1/pages/{id}/layers
561
+ POST /api/v1/pages/{id}/layers/{layer_type}/regenerate
562
+
563
+ # Export
564
+ GET /api/v1/manuscripts/{id}/iiif-manifest
565
+ GET /api/v1/manuscripts/{id}/mets
566
+ GET /api/v1/pages/{id}/alto
567
+ GET /api/v1/manuscripts/{id}/export.zip
568
+
569
+ # Validation
570
+ POST /api/v1/pages/{id}/validate
571
+ POST /api/v1/pages/{id}/corrections
572
+ GET /api/v1/pages/{id}/history
573
+
574
+ # Recherche
575
+ GET /api/v1/search?q=
576
+ GET /api/v1/manuscripts/{id}/search?q=
577
+ ```
578
+
579
+ ---
580
+
581
+ ## 12. État du projet par sprint
582
+
583
+ ```
584
+ Sprint 1 — Fondations du modèle de données [ EN COURS ]
585
+ → Schémas Pydantic + tests pytest + profils JSON + templates prompts
586
+
587
+ Sprint 2 — Pipeline page unique [ À FAIRE ]
588
+ → Ingestion + appel Google AI + master.json
589
+
590
+ Sprint 3 — Exports documentaires [ À FAIRE ]
591
+ → ALTO + METS + Manifest IIIF
592
+
593
+ Sprint 4 — API FastAPI + interface de lecture [ À FAIRE ]
594
+ → Endpoints + visionneuse + 4 couches
595
+
596
+ Sprint 5 — Traitement en lot + HuggingFace [ À FAIRE ]
597
+ → Pipeline batch + déploiement public
598
+
599
+ Sprint 6 — Validation humaine + V1 complète [ À FAIRE ]
600
+ → Éditeur + versionnement + recherche
601
+ ```
602
+
603
+ **Règle :** ne jamais implémenter du code appartenant à un sprint ultérieur
604
+ au sprint en cours. Si une idée émerge pour un sprint futur, la noter
605
+ dans TODO.md section "Backlog" et ne pas la coder.
606
+
607
+ ---
608
+
609
+ ## 13. Ce que tu NE dois PAS faire sans demande explicite
610
+
611
+ - Modifier le schéma PageMaster (champs, types, noms, structure)
612
+ - Modifier la convention bbox
613
+ - Ajouter des dépendances non listées dans pyproject.toml
614
+ - Refactoriser du code existant si la session n'a pas ce but explicite
615
+ - Créer des fichiers hors de l'arborescence définie section 3
616
+ - Implémenter du code de sprint futur (voir section 12)
617
+ - Simplifier un schéma pour "faire plus propre" — les schémas sont figés
618
+ - Changer une règle listée en section 5 pour une raison de commodité
619
+ - Utiliser une librairie alternative à celles listées section 2
620
+ - Créer une logique spécifique à un corpus particulier