Spaces:
Build error
Build error
maribakulj commited on
Create CLAUDE.md with project guidelines and structure
Browse filesAdded comprehensive project instructions, technical stack details, data model schemas, and API endpoints for Scriptorium AI.
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
|