Spaces:
Sleeping
docs: mettre à jour CLAUDE.md, api-stable.md, supprimer architecture-cercles.md
Browse files- ``CLAUDE.md`` : section Architecture réécrite pour refléter la
structure 3 cercles. Ancien tableau "chantiers post-Sprint 97"
remplacé par un manifeste concis qui pointe vers
``docs/architecture.md``. Section "Moteur narratif" mise à jour
pour refléter la nouvelle location (``measurements/narrative/`` +
``core/facts.py``). Compteurs de tests actualisés.
- ``docs/api-stable.md`` : noms des modules mis à jour pour refléter
les nouveaux chemins (``picarones.measurements.metrics``,
``picarones.measurements.runner``, ``picarones.core.pipeline``,
``picarones.measurements.pipeline_benchmark``,
``picarones.measurements.pipeline_comparison``,
``picarones.measurements.pipeline_spec_loader``,
``picarones.measurements.builtin_metrics``,
``picarones.measurements.alto_metrics``,
``picarones.web.jobs``, ``picarones.core.facts``).
- ``docs/architecture-cercles.md`` supprimé : remplacé par
``docs/architecture.md`` (plus à jour, plus précis).
- ``picarones/extras/__init__.py`` et
``picarones/measurements/__init__.py`` : référence à
``architecture.md`` au lieu de l'ancien ``architecture-cercles.md``.
- ``tests/test_phaseC_migration.py`` supprimé (validait la migration
intermédiaire).
https://claude.ai/code/session_01Hsd7kL8yeCbXn1mA7GQK9L
- CLAUDE.md +100 -138
- docs/api-stable.md +17 -17
- docs/architecture-cercles.md +0 -229
- picarones/extras/__init__.py +2 -2
- picarones/measurements/__init__.py +1 -1
- tests/test_phaseC_migration.py +0 -229
|
@@ -6,121 +6,89 @@ HuggingFace Space : huggingface.co/spaces/Ma-Ri-Ba-Ku/Picarones (Docker, port 78
|
|
| 6 |
|
| 7 |
---
|
| 8 |
|
| 9 |
-
##
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
section `[post-Sprint 97]` pour le détail.
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
[`picarones/core/narrative/detectors/`](picarones/core/narrative/detectors/).
|
| 27 |
|
| 28 |
---
|
| 29 |
|
| 30 |
## Setup
|
| 31 |
|
| 32 |
```bash
|
| 33 |
-
pip install -e ".[dev,web]" #
|
| 34 |
pytest tests/ -q --tb=short # lancer les tests
|
| 35 |
picarones demo --output rapport.html # rapport démo sans moteur installé
|
| 36 |
picarones serve --port 8080 # interface web locale
|
| 37 |
```
|
| 38 |
|
| 39 |
-
Mise à jour Codespace complète :
|
| 40 |
-
```bash
|
| 41 |
-
git pull && pip install -e ".[dev,web]" && picarones demo --output rapport_demo.html && picarones serve --port 8080
|
| 42 |
-
```
|
| 43 |
-
|
| 44 |
---
|
| 45 |
|
| 46 |
-
##
|
| 47 |
|
| 48 |
```
|
| 49 |
picarones/
|
| 50 |
-
├──
|
| 51 |
-
│ ├──
|
| 52 |
-
│ ├──
|
| 53 |
-
│ ├──
|
| 54 |
-
│ ├──
|
| 55 |
-
├──
|
| 56 |
-
├──
|
| 57 |
-
│ └──
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
│ ├──
|
| 61 |
-
│ ├──
|
| 62 |
-
│
|
| 63 |
-
│ ├──
|
| 64 |
-
│ ├──
|
| 65 |
-
│ ├──
|
| 66 |
-
│
|
| 67 |
-
│
|
| 68 |
-
│
|
| 69 |
-
│
|
| 70 |
-
|
| 71 |
-
│ ├──
|
| 72 |
-
│ ├──
|
| 73 |
-
│ ├──
|
| 74 |
-
│
|
| 75 |
-
|
| 76 |
-
│
|
| 77 |
-
│
|
| 78 |
-
│
|
| 79 |
-
├──
|
| 80 |
-
│ ├── base.py
|
| 81 |
-
│
|
| 82 |
-
|
| 83 |
-
│
|
| 84 |
-
│
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
│
|
| 88 |
-
|
| 89 |
-
│ ├──
|
| 90 |
-
│ ├──
|
| 91 |
-
│
|
| 92 |
-
|
| 93 |
-
│
|
| 94 |
-
|
| 95 |
-
├──
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
│ ├── early_modern_french_zero_shot.txt
|
| 100 |
-
│ ├── medieval_english.txt
|
| 101 |
-
│ ├── early_modern_english.txt
|
| 102 |
-
│ ├── medieval_latin.txt
|
| 103 |
-
│ └── zero_shot.txt
|
| 104 |
-
├── report/
|
| 105 |
-
│ ├── generator.py # Orchestration Jinja2 (617 lignes depuis Sprint 17)
|
| 106 |
-
│ ├── diff_utils.py
|
| 107 |
-
│ ├── templates/ # Templates Jinja2 (Sprint 17)
|
| 108 |
-
│ │ ├── base.html.j2 # assemble tout via {% include %}
|
| 109 |
-
│ │ ├── _header.html, _footer.html, _styles.css, _app.js
|
| 110 |
-
│ │ └── view_ranking.html, view_gallery.html, view_document.html,
|
| 111 |
-
│ │ view_analyses.html, view_characters.html
|
| 112 |
-
│ ├── i18n/ # Traductions FR/EN (Sprint 17 — extraites de i18n.py)
|
| 113 |
-
│ │ ├── fr.json
|
| 114 |
-
│ │ └── en.json
|
| 115 |
-
│ └── vendor/ # Chart.js vendorisé
|
| 116 |
-
├── web/
|
| 117 |
-
│ └── app.py # FastAPI, SSE, upload corpus ZIP, endpoints modèles dynamiques
|
| 118 |
-
└── importers/
|
| 119 |
-
├── iiif.py
|
| 120 |
-
├── htr_united.py
|
| 121 |
-
├── huggingface.py
|
| 122 |
-
├── gallica.py
|
| 123 |
-
└── escriptorium.py
|
| 124 |
```
|
| 125 |
|
| 126 |
---
|
|
@@ -160,7 +128,7 @@ correspondants (`test_sprint15_llm_pipeline_bugs.py`, `test_sprint8_escriptorium
|
|
| 160 |
CI, Makefile et invocation directe produisent le même résultat. Le job
|
| 161 |
`lint` du CI est bloquant — un F401 (import inutilisé) ou un E741
|
| 162 |
(variable ambiguë) fait échouer la PR, par design.
|
| 163 |
-
- **Les profils de normalisation** sont dans `picarones/
|
| 164 |
`/api/normalization/profiles` doit les lire dynamiquement depuis ce fichier, pas depuis une
|
| 165 |
liste statique.
|
| 166 |
|
|
@@ -299,58 +267,52 @@ AZURE_DOC_INTEL_KEY=...
|
|
| 299 |
|
| 300 |
---
|
| 301 |
|
| 302 |
-
## Moteur narratif
|
| 303 |
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
```
|
| 307 |
-
|
| 308 |
-
├── __init__.py
|
| 309 |
-
├──
|
| 310 |
-
├──
|
| 311 |
-
├──
|
| 312 |
-
├──
|
| 313 |
-
└──
|
| 314 |
-
├──
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
```
|
| 317 |
|
| 318 |
-
**Principe anti-hallucination** : chaque valeur numérique ou nom d'entité
|
| 319 |
-
`payload` d'un `Fact`
|
| 320 |
-
parse la synthèse rendue et vérifie
|
| 321 |
-
|
| 322 |
-
de template (`95`, `100`).
|
| 323 |
-
|
| 324 |
-
**Détecteurs activés dans le registre par défaut (Sprint 20)** — les 12 sont opérationnels :
|
| 325 |
-
- Sprint 3 : `statistical_tie`
|
| 326 |
-
- Sprint 4 : `global_leader_cer`, `significant_gap`, `stratum_winner`, `stratum_collapse`,
|
| 327 |
-
`error_profile_outlier`, `llm_hallucination_flag`, `robustness_fragile`,
|
| 328 |
-
`speed_winner`, `confidence_warning`
|
| 329 |
-
- Sprint 5 : `pareto_alternative`, `cost_outlier`
|
| 330 |
|
| 331 |
-
**Règle anti-contradiction** (arbitre) : si `SIGNIFICANT_GAP` (Wilcoxon
|
| 332 |
-
et `STATISTICAL_TIE` (Nemenyi corrigé) concernent les mêmes
|
| 333 |
-
l'emporte
|
| 334 |
-
"A et B sont indiscernables".
|
| 335 |
|
| 336 |
-
**Pipeline** : `build_synthesis(benchmark_data, lang, max_facts=5)`
|
| 337 |
-
arbitre, rend.
|
| 338 |
-
au template `_narrative_summary.html` (placé entre `_header.html` et `_critical_difference.html`).
|
| 339 |
|
| 340 |
---
|
| 341 |
|
| 342 |
## Contexte développement
|
| 343 |
|
| 344 |
-
- **Environnement** : GitHub Codespaces
|
| 345 |
-
- **Tests** :
|
| 346 |
-
pour les nouveaux tests des chantiers 1-5 qui ajoutent ~1500 lignes
|
| 347 |
-
de validation : `test_alto_baseline.py`, `test_metric_hooks.py`,
|
| 348 |
-
`test_views.py`, `test_chantier4.py`, `test_chantier5.py`).
|
| 349 |
- **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
|
| 350 |
-
- **
|
| 351 |
-
|
| 352 |
-
[`docs/views.md`](docs/views.md).
|
| 353 |
- **Branche active** : `claude/code-quality-audit-ACnhK`.
|
| 354 |
-
- **Détecteurs narratifs** : 18 (et non 12 comme indiqué historiquement),
|
| 355 |
-
organisés en 6 familles dans
|
| 356 |
-
[`picarones/core/narrative/detectors/`](picarones/core/narrative/detectors/).
|
|
|
|
| 6 |
|
| 7 |
---
|
| 8 |
|
| 9 |
+
## Architecture en 3 cercles
|
| 10 |
|
| 11 |
+
Voir le manifeste complet dans [`docs/architecture.md`](docs/architecture.md).
|
| 12 |
|
| 13 |
+
```
|
| 14 |
+
Cercle 3 (extras, report, cli, web)
|
| 15 |
+
│
|
| 16 |
+
▼
|
| 17 |
+
Cercle 2 (measurements, engines, llm, pipelines, modules)
|
| 18 |
+
│
|
| 19 |
+
▼
|
| 20 |
+
Cercle 1 (core)
|
| 21 |
+
```
|
|
|
|
| 22 |
|
| 23 |
+
Règle de dépendance stricte : les imports vont uniquement de l'extérieur
|
| 24 |
+
vers l'intérieur. **Aucun shim** — un module a un seul emplacement.
|
|
|
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
## Setup
|
| 29 |
|
| 30 |
```bash
|
| 31 |
+
pip install -e ".[dev,web]" # toujours inclure [web] pour les tests
|
| 32 |
pytest tests/ -q --tb=short # lancer les tests
|
| 33 |
picarones demo --output rapport.html # rapport démo sans moteur installé
|
| 34 |
picarones serve --port 8080 # interface web locale
|
| 35 |
```
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
---
|
| 38 |
|
| 39 |
+
## Structure
|
| 40 |
|
| 41 |
```
|
| 42 |
picarones/
|
| 43 |
+
├── core/ Cercle 1 — abstractions pures (7 modules)
|
| 44 |
+
│ ├── modules.py BaseModule, ArtifactType
|
| 45 |
+
│ ├── corpus.py Document, Corpus, GTLevel, payloads typés
|
| 46 |
+
│ ├── results.py DocumentResult, EngineReport, BenchmarkResult
|
| 47 |
+
│ ├── metric_registry.py MetricSpec, register_metric, compute_at_junction
|
| 48 |
+
│ ├── metric_hooks.py register_document_metric, register_corpus_aggregator
|
| 49 |
+
│ ├── pipeline.py PipelineRunner, PipelineSpec, PipelineStep
|
| 50 |
+
│ └── facts.py Fact, FactType, FactImportance, DetectorRegistry
|
| 51 |
+
│
|
| 52 |
+
├── measurements/ Cercle 2 — métriques officielles (~55 modules)
|
| 53 |
+
│ ├── runner.py run_benchmark (orchestration)
|
| 54 |
+
│ ├── metrics.py / statistics.py / normalization.py / builtin_hooks.py
|
| 55 |
+
│ ├── confusion.py / taxonomy.py / calibration.py / line_metrics.py / ...
|
| 56 |
+
│ ├── readability.py / reliability.py / searchability.py / ner.py / ...
|
| 57 |
+
│ ├── mufi.py / abbreviations.py / unicode_blocks.py / roman_numerals.py
|
| 58 |
+
│ ├── pipeline_benchmark.py / pipeline_comparison.py / pipeline_spec_loader.py
|
| 59 |
+
│ └── narrative/ moteur narratif (arbiter, renderer, registry,
|
| 60 |
+
│ 18 détecteurs en 6 familles : ranking, pareto,
|
| 61 |
+
│ stratum, quality, history, ensemble)
|
| 62 |
+
│
|
| 63 |
+
├── engines/ Cercle 2 — adapters OCR (5)
|
| 64 |
+
│ ├── base.py BaseOCREngine (hérite de BaseModule)
|
| 65 |
+
│ ├── tesseract.py / pero_ocr.py
|
| 66 |
+
│ ├── mistral_ocr.py / google_vision.py / azure_doc_intel.py
|
| 67 |
+
│
|
| 68 |
+
├── llm/ Cercle 2 — adapters LLM (4)
|
| 69 |
+
│ ├── base.py / mistral_adapter.py / openai_adapter.py
|
| 70 |
+
│ └── anthropic_adapter.py / ollama_adapter.py
|
| 71 |
+
│
|
| 72 |
+
├── pipelines/ Cercle 2 — pipelines OCR+LLM intégrés
|
| 73 |
+
│ ├── base.py (OCRLLMPipeline) / over_normalization.py
|
| 74 |
+
│
|
| 75 |
+
├── modules/ Cercle 2 — modules BaseModule officiels
|
| 76 |
+
│ └── alto_text_to_mono_region.py
|
| 77 |
+
│
|
| 78 |
+
├── extras/ Cercle 3 — plugins / extensions
|
| 79 |
+
│ └── importers/ IIIF, Gallica, HTR-United, HuggingFace, eScriptorium
|
| 80 |
+
│
|
| 81 |
+
├── report/ Cercle 3 — rendu HTML
|
| 82 |
+
│ ├── generator.py / colors.py / diff_utils.py
|
| 83 |
+
│ ├── views/ 5 vues thématiques
|
| 84 |
+
│ ├── templates/ / i18n/ / glossary/ / vendor/
|
| 85 |
+
│ └── *_render.py ~22 renderers (calibration, NER, Pareto, etc.)
|
| 86 |
+
│
|
| 87 |
+
├── cli/ Cercle 3 — Click (7 fichiers)
|
| 88 |
+
├── web/ Cercle 3 — FastAPI (app.py, jobs.py)
|
| 89 |
+
├── prompts/ 8 fichiers .txt FR+EN
|
| 90 |
+
├── data/ Tables indicatives (pricing.yaml)
|
| 91 |
+
└── fixtures.py Corpus de test fictifs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
```
|
| 93 |
|
| 94 |
---
|
|
|
|
| 128 |
CI, Makefile et invocation directe produisent le même résultat. Le job
|
| 129 |
`lint` du CI est bloquant — un F401 (import inutilisé) ou un E741
|
| 130 |
(variable ambiguë) fait échouer la PR, par design.
|
| 131 |
+
- **Les profils de normalisation** sont dans `picarones/measurements/normalization.py` — l'endpoint
|
| 132 |
`/api/normalization/profiles` doit les lire dynamiquement depuis ce fichier, pas depuis une
|
| 133 |
liste statique.
|
| 134 |
|
|
|
|
| 267 |
|
| 268 |
---
|
| 269 |
|
| 270 |
+
## Moteur narratif
|
| 271 |
|
| 272 |
+
Le modèle de données (`Fact`, `FactType`, `FactImportance`,
|
| 273 |
+
`DetectorRegistry`) vit en cercle 1 dans
|
| 274 |
+
[`picarones/core/facts.py`](picarones/core/facts.py). Les détecteurs et
|
| 275 |
+
le rendu vivent en cercle 2 :
|
| 276 |
|
| 277 |
```
|
| 278 |
+
picarones/measurements/narrative/
|
| 279 |
+
├── __init__.py API publique + pipeline build_synthesis
|
| 280 |
+
├── arbiter.py Tri par importance, non-redondance, anti-contradiction
|
| 281 |
+
├── renderer.py Rendu templates YAML par str.format_map (déterministe)
|
| 282 |
+
├── registry.py Registre par défaut des détecteurs
|
| 283 |
+
├── templates/{fr,en}.yaml 18 templates × 2 langues
|
| 284 |
+
└── detectors/ 18 détecteurs en 6 familles
|
| 285 |
+
├── ranking.py 5 (global_leader, statistical_tie, significant_gap,
|
| 286 |
+
│ speed_winner, median_mean_gap_warning)
|
| 287 |
+
├── pareto.py 2 (pareto_alternative, cost_outlier)
|
| 288 |
+
├── stratum.py 3 (stratum_winner, stratum_collapse,
|
| 289 |
+
│ stratification_recommended)
|
| 290 |
+
├── quality.py 4 (error_profile_outlier, llm_hallucination_flag,
|
| 291 |
+
│ robustness_fragile, confidence_warning)
|
| 292 |
+
├── history.py 3 (engine_off_baseline, engine_unstable,
|
| 293 |
+
│ regression_in_history)
|
| 294 |
+
└── ensemble.py 1 (ensemble_opportunity)
|
| 295 |
```
|
| 296 |
|
| 297 |
+
**Principe anti-hallucination** : chaque valeur numérique ou nom d'entité
|
| 298 |
+
dans le `payload` d'un `Fact` provient du JSON d'entrée. Le test
|
| 299 |
+
`test_sprint19_narrative_engine.py` parse la synthèse rendue et vérifie
|
| 300 |
+
la traçabilité.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
+
**Règle anti-contradiction** (arbitre) : si `SIGNIFICANT_GAP` (Wilcoxon
|
| 303 |
+
non corrigé) et `STATISTICAL_TIE` (Nemenyi corrigé) concernent les mêmes
|
| 304 |
+
moteurs, Nemenyi l'emporte.
|
|
|
|
| 305 |
|
| 306 |
+
**Pipeline** : `build_synthesis(benchmark_data, lang, max_facts=5)`
|
| 307 |
+
détecte, arbitre, rend.
|
|
|
|
| 308 |
|
| 309 |
---
|
| 310 |
|
| 311 |
## Contexte développement
|
| 312 |
|
| 313 |
+
- **Environnement** : GitHub Codespaces, Python 3.11+
|
| 314 |
+
- **Tests** : `pytest tests/ -q` → ~3354 passed, 2 skipped, 0 failed.
|
|
|
|
|
|
|
|
|
|
| 315 |
- **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
|
| 316 |
+
- **Manifeste architecture** : [`docs/architecture.md`](docs/architecture.md).
|
| 317 |
+
- **API publique stable** : [`docs/api-stable.md`](docs/api-stable.md).
|
|
|
|
| 318 |
- **Branche active** : `claude/code-quality-audit-ACnhK`.
|
|
|
|
|
|
|
|
|
|
@@ -79,7 +79,7 @@ class EngineReport: # agrégat moteur sur tout le corpus
|
|
| 79 |
class BenchmarkResult: # résultat global multi-moteurs
|
| 80 |
```
|
| 81 |
|
| 82 |
-
### `picarones.
|
| 83 |
|
| 84 |
```python
|
| 85 |
class MetricsResult: # CER, WER, MER, WIL + variantes diplomatique/caseless
|
|
@@ -87,7 +87,7 @@ def compute_metrics(reference, hypothesis, char_exclude=None) -> MetricsResult
|
|
| 87 |
def aggregate_metrics(results: list) -> dict
|
| 88 |
```
|
| 89 |
|
| 90 |
-
### `picarones.
|
| 91 |
|
| 92 |
```python
|
| 93 |
def run_benchmark(
|
|
@@ -105,7 +105,7 @@ def run_benchmark(
|
|
| 105 |
) -> BenchmarkResult
|
| 106 |
```
|
| 107 |
|
| 108 |
-
### `picarones.core.
|
| 109 |
|
| 110 |
```python
|
| 111 |
class PipelineStep:
|
|
@@ -115,7 +115,7 @@ class PipelineResult:
|
|
| 115 |
class PipelineRunner:
|
| 116 |
```
|
| 117 |
|
| 118 |
-
### `picarones.
|
| 119 |
|
| 120 |
```python
|
| 121 |
class StepAggregate:
|
|
@@ -125,7 +125,7 @@ def default_initial_inputs(doc) -> dict
|
|
| 125 |
def run_pipeline_benchmark(spec, corpus, factory=...) -> PipelineBenchmarkResult
|
| 126 |
```
|
| 127 |
|
| 128 |
-
### `picarones.
|
| 129 |
|
| 130 |
```python
|
| 131 |
class PipelineComparisonResult:
|
|
@@ -133,7 +133,7 @@ class PipelineComparisonResult:
|
|
| 133 |
def compare_pipelines(specs, corpus, factories=None) -> PipelineComparisonResult
|
| 134 |
```
|
| 135 |
|
| 136 |
-
### `picarones.
|
| 137 |
|
| 138 |
```python
|
| 139 |
class PipelineSpecLoadError(ValueError):
|
|
@@ -183,7 +183,7 @@ def run_document_hooks(profile, *, ground_truth, hypothesis, image_path, corpus_
|
|
| 183 |
def run_corpus_aggregators(profile, document_results) -> dict
|
| 184 |
```
|
| 185 |
|
| 186 |
-
### `picarones.
|
| 187 |
|
| 188 |
Métriques scalaires natives, enregistrées dans le registre typé :
|
| 189 |
|
|
@@ -197,7 +197,7 @@ def wil(reference, hypothesis) -> float
|
|
| 197 |
def text_preservation_after_reconstruction(reference_text, hypothesis_alto) -> float
|
| 198 |
```
|
| 199 |
|
| 200 |
-
### `picarones.
|
| 201 |
|
| 202 |
Métriques (ALTO, ALTO) + helper :
|
| 203 |
|
|
@@ -210,7 +210,7 @@ def alto_text_mer(reference_alto, hypothesis_alto) -> float
|
|
| 210 |
def alto_text_wil(reference_alto, hypothesis_alto) -> float
|
| 211 |
```
|
| 212 |
|
| 213 |
-
### `picarones.
|
| 214 |
|
| 215 |
Persistance des jobs benchmark (utilisé par l'interface web) :
|
| 216 |
|
|
@@ -241,7 +241,7 @@ def reset_default_store(...)
|
|
| 241 |
reflètent ces changements.
|
| 242 |
- **Modules `picarones.extras/`** : statut variable selon le
|
| 243 |
sous-package (academic / governance / historical / importers).
|
| 244 |
-
Voir `docs/architecture
|
| 245 |
- **Comportement des renderers HTML** : la structure des fichiers HTML
|
| 246 |
peut évoluer entre versions mineures. Nous gardons les noms des
|
| 247 |
vues principales.
|
|
@@ -268,15 +268,15 @@ version mineure si une RFC le justifie.
|
|
| 268 |
|
| 269 |
```python
|
| 270 |
# Mesures (déplacées vers picarones.measurements/)
|
| 271 |
-
from picarones.
|
| 272 |
-
from picarones.
|
| 273 |
-
from picarones.
|
| 274 |
# ... ~40 modules métriques ...
|
| 275 |
|
| 276 |
# Moteur narratif (déplacé vers picarones.measurements.narrative/)
|
| 277 |
-
from picarones.
|
| 278 |
-
from picarones.core.
|
| 279 |
-
from picarones.
|
| 280 |
|
| 281 |
# Plugins (déplacés vers picarones.extras/)
|
| 282 |
from picarones.core.taxonomy_intra_doc import compute_taxonomy_position_heatmap
|
|
@@ -296,7 +296,7 @@ Pour les **nouvelles** intégrations, préférer les chemins canoniques :
|
|
| 296 |
|
| 297 |
## Voir aussi
|
| 298 |
|
| 299 |
-
- [`docs/architecture
|
| 300 |
des 3 cercles + critères d'assignation.
|
| 301 |
- [`docs/architecture.md`](architecture.md) — vue d'ensemble post-chantiers.
|
| 302 |
- [`tests/test_public_api.py`](../tests/test_public_api.py) — test
|
|
|
|
| 79 |
class BenchmarkResult: # résultat global multi-moteurs
|
| 80 |
```
|
| 81 |
|
| 82 |
+
### `picarones.measurements.metrics`
|
| 83 |
|
| 84 |
```python
|
| 85 |
class MetricsResult: # CER, WER, MER, WIL + variantes diplomatique/caseless
|
|
|
|
| 87 |
def aggregate_metrics(results: list) -> dict
|
| 88 |
```
|
| 89 |
|
| 90 |
+
### `picarones.measurements.runner`
|
| 91 |
|
| 92 |
```python
|
| 93 |
def run_benchmark(
|
|
|
|
| 105 |
) -> BenchmarkResult
|
| 106 |
```
|
| 107 |
|
| 108 |
+
### `picarones.core.pipeline`
|
| 109 |
|
| 110 |
```python
|
| 111 |
class PipelineStep:
|
|
|
|
| 115 |
class PipelineRunner:
|
| 116 |
```
|
| 117 |
|
| 118 |
+
### `picarones.measurements.pipeline_benchmark`
|
| 119 |
|
| 120 |
```python
|
| 121 |
class StepAggregate:
|
|
|
|
| 125 |
def run_pipeline_benchmark(spec, corpus, factory=...) -> PipelineBenchmarkResult
|
| 126 |
```
|
| 127 |
|
| 128 |
+
### `picarones.measurements.pipeline_comparison`
|
| 129 |
|
| 130 |
```python
|
| 131 |
class PipelineComparisonResult:
|
|
|
|
| 133 |
def compare_pipelines(specs, corpus, factories=None) -> PipelineComparisonResult
|
| 134 |
```
|
| 135 |
|
| 136 |
+
### `picarones.measurements.pipeline_spec_loader`
|
| 137 |
|
| 138 |
```python
|
| 139 |
class PipelineSpecLoadError(ValueError):
|
|
|
|
| 183 |
def run_corpus_aggregators(profile, document_results) -> dict
|
| 184 |
```
|
| 185 |
|
| 186 |
+
### `picarones.measurements.builtin_metrics`
|
| 187 |
|
| 188 |
Métriques scalaires natives, enregistrées dans le registre typé :
|
| 189 |
|
|
|
|
| 197 |
def text_preservation_after_reconstruction(reference_text, hypothesis_alto) -> float
|
| 198 |
```
|
| 199 |
|
| 200 |
+
### `picarones.measurements.alto_metrics`
|
| 201 |
|
| 202 |
Métriques (ALTO, ALTO) + helper :
|
| 203 |
|
|
|
|
| 210 |
def alto_text_wil(reference_alto, hypothesis_alto) -> float
|
| 211 |
```
|
| 212 |
|
| 213 |
+
### `picarones.web.jobs`
|
| 214 |
|
| 215 |
Persistance des jobs benchmark (utilisé par l'interface web) :
|
| 216 |
|
|
|
|
| 241 |
reflètent ces changements.
|
| 242 |
- **Modules `picarones.extras/`** : statut variable selon le
|
| 243 |
sous-package (academic / governance / historical / importers).
|
| 244 |
+
Voir `docs/architecture.md`.
|
| 245 |
- **Comportement des renderers HTML** : la structure des fichiers HTML
|
| 246 |
peut évoluer entre versions mineures. Nous gardons les noms des
|
| 247 |
vues principales.
|
|
|
|
| 268 |
|
| 269 |
```python
|
| 270 |
# Mesures (déplacées vers picarones.measurements/)
|
| 271 |
+
from picarones.measurements.confusion import build_confusion_matrix
|
| 272 |
+
from picarones.measurements.taxonomy import classify_errors
|
| 273 |
+
from picarones.measurements.calibration import compute_calibration_metrics
|
| 274 |
# ... ~40 modules métriques ...
|
| 275 |
|
| 276 |
# Moteur narratif (déplacé vers picarones.measurements.narrative/)
|
| 277 |
+
from picarones.measurements.narrative import build_synthesis
|
| 278 |
+
from picarones.core.facts import Fact, FactType, FactImportance
|
| 279 |
+
from picarones.measurements.narrative.detectors import detect_global_leader_cer
|
| 280 |
|
| 281 |
# Plugins (déplacés vers picarones.extras/)
|
| 282 |
from picarones.core.taxonomy_intra_doc import compute_taxonomy_position_heatmap
|
|
|
|
| 296 |
|
| 297 |
## Voir aussi
|
| 298 |
|
| 299 |
+
- [`docs/architecture.md`](architecture.md) — cartographie
|
| 300 |
des 3 cercles + critères d'assignation.
|
| 301 |
- [`docs/architecture.md`](architecture.md) — vue d'ensemble post-chantiers.
|
| 302 |
- [`tests/test_public_api.py`](../tests/test_public_api.py) — test
|
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
# Architecture en 3 cercles — chantier de refonte post-chantier 6
|
| 2 |
-
|
| 3 |
-
Ce document **fige la cartographie** de chaque module Picarones dans son
|
| 4 |
-
cercle d'appartenance. Il sert de référence stable pour les
|
| 5 |
-
contributions futures : avant d'ajouter un module, consulter ce
|
| 6 |
-
document pour identifier dans quel cercle il doit aller.
|
| 7 |
-
|
| 8 |
-
## Principe — 3 cercles concentriques
|
| 9 |
-
|
| 10 |
-
```
|
| 11 |
-
┌─────────────────────────────────────────────────────────────┐
|
| 12 |
-
│ Cercle 3 — Plugins (extras/) │
|
| 13 |
-
│ ┌─────────────────────────────────────────────────────┐ │
|
| 14 |
-
│ │ Cercle 2 — Modules officiels │ │
|
| 15 |
-
│ │ ┌──────────────────────────────────────────┐ │ │
|
| 16 |
-
│ │ │ Cercle 1 — Noyau invariant (core/) │ │ │
|
| 17 |
-
│ │ │ API publique stable, ~15 modules │ │ │
|
| 18 |
-
│ │ └──────────────────────────────────────────┘ │ │
|
| 19 |
-
│ │ Adapters, mesures, rapport, CLI, web │ │
|
| 20 |
-
│ │ ~30 modules métriques + ~15 adapters/UI │ │
|
| 21 |
-
│ └─────────────────────────────────────────────────────┘ │
|
| 22 |
-
│ Modules niche, gouvernance préventive, importers exotiques │
|
| 23 |
-
│ Distribués via extras pip ou packages séparés à terme │
|
| 24 |
-
└─────────────────────────────────────────────────────────────┘
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
Plus on s'éloigne du cœur, plus c'est optionnel et plus c'est facile
|
| 28 |
-
à supprimer/remplacer/externaliser.
|
| 29 |
-
|
| 30 |
-
## Cercle 1 — Noyau invariant
|
| 31 |
-
|
| 32 |
-
**Critères** : ce qui définit *ce qu'est* Picarones. API publique
|
| 33 |
-
stable. Ne casse pas entre versions mineures.
|
| 34 |
-
|
| 35 |
-
**Localisation** : `picarones/core/` (après phase E) — strictement
|
| 36 |
-
~15 modules.
|
| 37 |
-
|
| 38 |
-
**Contenu** :
|
| 39 |
-
|
| 40 |
-
| Module | Rôle |
|
| 41 |
-
|---|---|
|
| 42 |
-
| `corpus.py` | Document, Corpus, GTLevel multi-niveaux |
|
| 43 |
-
| `modules.py` | BaseModule, ArtifactType (contrat unique pour modules tiers) |
|
| 44 |
-
| `results.py` | BenchmarkResult, EngineReport, DocumentResult |
|
| 45 |
-
| `metrics.py` | CER/WER/MER/WIL via jiwer (métriques de base) |
|
| 46 |
-
| `runner.py` | Orchestrateur (parallélisation, reprise, timeout) |
|
| 47 |
-
| `pipeline_runner.py` | Banc d'essai mono-doc des pipelines composées |
|
| 48 |
-
| `pipeline_benchmark.py` | Orchestration corpus-wide |
|
| 49 |
-
| `pipeline_comparison.py` | Comparaison de N pipelines |
|
| 50 |
-
| `pipeline_spec_loader.py` | Chargement YAML déclaratif |
|
| 51 |
-
| `metric_registry.py` | Registre typé `(input_type, output_type) → metric` |
|
| 52 |
-
| `metric_hooks.py` | Profils + registre de hooks document/corpus |
|
| 53 |
-
| `builtin_metrics.py` | CER/WER/MER/WIL enregistrés sur registre typé |
|
| 54 |
-
| `alto_metrics.py` | Métriques `(ALTO, ALTO)` (chantier 1) |
|
| 55 |
-
|
| 56 |
-
**Discipline** :
|
| 57 |
-
- Toute modification non rétrocompatible exige une **RFC** et bump majeur.
|
| 58 |
-
- Test `test_public_api.py` (à créer en phase D) qui échoue si un nom disparaît.
|
| 59 |
-
- Aucun import direct depuis `extras/` ou de modules optionnels.
|
| 60 |
-
|
| 61 |
-
## Cercle 2 — Modules officiels
|
| 62 |
-
|
| 63 |
-
**Critères** : maintenu par les mainteneurs Picarones, livré par
|
| 64 |
-
défaut, mais peut techniquement vivre ailleurs (un fork peut le
|
| 65 |
-
remplacer par un équivalent).
|
| 66 |
-
|
| 67 |
-
**Localisation** :
|
| 68 |
-
- `picarones/measurements/` (après phase E) — métriques au-delà du CER de base.
|
| 69 |
-
- `picarones/engines/` — adapters OCR.
|
| 70 |
-
- `picarones/llm/` — adapters LLM.
|
| 71 |
-
- `picarones/modules/` — modules `BaseModule` de référence (chantier 1).
|
| 72 |
-
- `picarones/report/` — génération HTML.
|
| 73 |
-
- `picarones/cli/` — interface CLI.
|
| 74 |
-
- `picarones/web/` — interface web FastAPI.
|
| 75 |
-
- `picarones/pipelines/` — pipelines OCR+LLM legacy (à statuer en phase D).
|
| 76 |
-
|
| 77 |
-
**Métriques officielles** (futur `picarones/measurements/`) :
|
| 78 |
-
|
| 79 |
-
| Catégorie | Modules |
|
| 80 |
-
|---|---|
|
| 81 |
-
| Texte | `confusion`, `char_scores`, `taxonomy`, `structure`, `taxonomy_comparison` |
|
| 82 |
-
| Lignes | `line_metrics`, `hallucination` |
|
| 83 |
-
| Fiabilité | `calibration`, `reliability`, `robustness`, `robustness_projection` |
|
| 84 |
-
| Structure ALTO/PAGE | `reading_order`, `layout`, `error_absorption` |
|
| 85 |
-
| Recherche | `searchability`, `numerical_sequences`, `rare_tokens` |
|
| 86 |
-
| Lisibilité | `readability` (Flesch), `specialization` |
|
| 87 |
-
| Inter-moteurs | `inter_engine`, `worst_lines` |
|
| 88 |
-
| Économie | `throughput`, `cost_projection`, `marginal_cost`, `pricing` |
|
| 89 |
-
| Comparaison | `incremental_comparison` |
|
| 90 |
-
| Narrative | `narrative/` (engine + 6 familles de détecteurs) |
|
| 91 |
-
| Hooks | `builtin_hooks` |
|
| 92 |
-
| Contexte corpus | `history`, `difficulty`, `image_quality`, `normalization` |
|
| 93 |
-
| Statistiques | `statistics` |
|
| 94 |
-
| Levers | `levers` |
|
| 95 |
-
|
| 96 |
-
**Discipline** :
|
| 97 |
-
- Modification libre sans RFC.
|
| 98 |
-
- Nouveau module doit s'enregistrer via `@register_metric` ou
|
| 99 |
-
`@register_document_metric` plutôt qu'imports directs depuis `runner.py`.
|
| 100 |
-
- Couvre les 4 axes du produit : viabilité prod, hallucinations VLM,
|
| 101 |
-
pipelines composées, projection coût/vitesse.
|
| 102 |
-
|
| 103 |
-
## Cercle 3 — Plugins
|
| 104 |
-
|
| 105 |
-
**Critères** : ne sert pas tout le monde, peut être désactivé sans
|
| 106 |
-
amputer le produit principal.
|
| 107 |
-
|
| 108 |
-
**Localisation** : `picarones/extras/` (sous-package interne pour
|
| 109 |
-
l'instant ; packages PyPI séparés possibles à terme).
|
| 110 |
-
|
| 111 |
-
**Sous-packages** :
|
| 112 |
-
|
| 113 |
-
### `extras/academic/` — modules techniques sans cas d'usage prod
|
| 114 |
-
|
| 115 |
-
| Module | Pourquoi en plugin |
|
| 116 |
-
|---|---|
|
| 117 |
-
| `taxonomy_intra_doc.py` | Heatmap classe×position. Question rare, peu actionnable |
|
| 118 |
-
| `taxonomy_cooccurrence.py` | Jaccard inter-classes. Académique, info rare |
|
| 119 |
-
| `image_predictive.py` | Score combiné avec poids éditoriaux arbitraires |
|
| 120 |
-
|
| 121 |
-
### `extras/governance/` — gouvernance préventive
|
| 122 |
-
|
| 123 |
-
| Module | Pourquoi en plugin |
|
| 124 |
-
|---|---|
|
| 125 |
-
| `module_policy.py` | Manifest + audit pour modules contribués externes. Inutile tant qu'il n'y a pas 5+ modules tiers réels |
|
| 126 |
-
|
| 127 |
-
### `extras/historical/` — métriques philologiques (phase B)
|
| 128 |
-
|
| 129 |
-
| Module | Public spécifique |
|
| 130 |
-
|---|---|
|
| 131 |
-
| `unicode_blocks.py` | Tous périodes |
|
| 132 |
-
| `abbreviations.py` | Médiéval (Capelli) |
|
| 133 |
-
| `mufi.py` | Médiéval (PUA) |
|
| 134 |
-
| `early_modern_typography.py` | XVIᵉ-XVIIIᵉ siècles |
|
| 135 |
-
| `modern_archives.py` | XIXᵉ-XXᵉ siècles |
|
| 136 |
-
| `roman_numerals.py` | Toutes périodes |
|
| 137 |
-
| `lexical_modernization.py` | Édition critique |
|
| 138 |
-
| `philological_runner.py` | Orchestration des 6 modules ci-dessus |
|
| 139 |
-
|
| 140 |
-
### `extras/importers/` — imports externes (phase C)
|
| 141 |
-
|
| 142 |
-
| Module | Statut |
|
| 143 |
-
|---|---|
|
| 144 |
-
| `_http.py` | Helpers HTTP partagés (chantier 4) |
|
| 145 |
-
| `iiif.py` | Maintenu |
|
| 146 |
-
| `htr_united.py` | Maintenu |
|
| 147 |
-
| `gallica.py` | Maintenu |
|
| 148 |
-
| `huggingface.py` | Expérimental (à finir ou marqué unstable) |
|
| 149 |
-
| `escriptorium.py` | Expérimental (à finir ou marqué unstable) |
|
| 150 |
-
|
| 151 |
-
### `extras/render/` — renderers correspondants
|
| 152 |
-
|
| 153 |
-
Renderers atomiques pour les modules `extras/`. Importés
|
| 154 |
-
conditionnellement par les vues thématiques du chantier 3 (qui sont
|
| 155 |
-
elles-mêmes dans `report/views/`, donc Cercle 2).
|
| 156 |
-
|
| 157 |
-
## Distinguer un module Cercle 1 vs Cercle 2
|
| 158 |
-
|
| 159 |
-
Critère **corrigé** (alignement architecture hexagonale / DDD) :
|
| 160 |
-
|
| 161 |
-
> **Cercle 1 = abstractions et logique métier du domaine,
|
| 162 |
-
> indépendantes de l'interface utilisateur. Stables entre versions
|
| 163 |
-
> mineures.**
|
| 164 |
-
>
|
| 165 |
-
> **Cercle 2 = adapters concrets (engines, LLM, modules de référence),
|
| 166 |
-
> couches d'interface (report, cli, web), et mesures au-delà du noyau
|
| 167 |
-
> (measurements). Maintenus mais peuvent évoluer.**
|
| 168 |
-
|
| 169 |
-
Le critère « si on supprime ce module, le produit reste viable »
|
| 170 |
-
mélange deux questions distinctes (« est-ce indispensable ? » et
|
| 171 |
-
« est-ce une abstraction stable ? »). On préfère le critère DDD :
|
| 172 |
-
|
| 173 |
-
- **Cercle 1** : abstractions et orchestration qui définissent ce
|
| 174 |
-
que Picarones *est* logiquement (corpus, BaseModule, registres,
|
| 175 |
-
runner). Indépendant de l'interface utilisateur.
|
| 176 |
-
- **Cercle 2** : ce qui rend le domaine utilisable concrètement
|
| 177 |
-
(adapters, mesures, présentation HTML, CLI).
|
| 178 |
-
|
| 179 |
-
Exemple :
|
| 180 |
-
- `corpus.py` → Cercle 1 (abstraction du domaine).
|
| 181 |
-
- `runner.py` → Cercle 1 (orchestration du domaine).
|
| 182 |
-
- `confusion.py` → Cercle 2 (mesure au-delà du noyau, dans
|
| 183 |
-
``measurements/``).
|
| 184 |
-
- `report/generator.py` → Cercle 2 (couche de présentation, même si
|
| 185 |
-
essentielle à l'usage pratique).
|
| 186 |
-
- `engines/tesseract.py` → Cercle 2 (adapter concret).
|
| 187 |
-
|
| 188 |
-
> Note : la convention « `base.py` dans le dossier du concept »
|
| 189 |
-
> (`engines/base.py`, `llm/base.py`) reste dans son dossier d'origine.
|
| 190 |
-
> Ces contrats sont logiquement Cercle 1 (API publique stable) mais
|
| 191 |
-
> physiquement co-localisés avec leurs implémentations, comme dans
|
| 192 |
-
> Django, SQLAlchemy, FastAPI. Convention universelle Python.
|
| 193 |
-
- Sans `taxonomy_intra_doc.py` : on a toujours un bench complet et
|
| 194 |
-
utile → Cercle 3.
|
| 195 |
-
|
| 196 |
-
## Distinguer un module Cercle 2 vs Cercle 3
|
| 197 |
-
|
| 198 |
-
Test concret : ce module sert-il à répondre à la question
|
| 199 |
-
*« peut-on déployer ce moteur en prod sur ce corpus dans nos
|
| 200 |
-
contraintes ? »* — soit en mesurant un risque (hallucinations,
|
| 201 |
-
stabilité), soit en projetant un coût (throughput, pricing), soit
|
| 202 |
-
en évaluant la qualité (CER, calibration, structure) ?
|
| 203 |
-
|
| 204 |
-
- **Oui** → Cercle 2.
|
| 205 |
-
- **Non** → Cercle 3.
|
| 206 |
-
|
| 207 |
-
Exemple :
|
| 208 |
-
- `hallucination.py` : mesure un risque pour la prod VLM → Cercle 2.
|
| 209 |
-
- `throughput.py` : projette un coût opérationnel → Cercle 2.
|
| 210 |
-
- `taxonomy_intra_doc.py` : décrit une distribution sans implication
|
| 211 |
-
de décision → Cercle 3.
|
| 212 |
-
|
| 213 |
-
## Disclaimer
|
| 214 |
-
|
| 215 |
-
Cette cartographie est **une décision produit**, pas une vérité
|
| 216 |
-
absolue. Elle peut évoluer si les usages réels d'institutions
|
| 217 |
-
révèlent qu'un module Cercle 3 est en fait essentiel, ou
|
| 218 |
-
inversement.
|
| 219 |
-
|
| 220 |
-
Toute remise en cause doit passer par une RFC documentée, pas par
|
| 221 |
-
une PR silencieuse.
|
| 222 |
-
|
| 223 |
-
## Voir aussi
|
| 224 |
-
|
| 225 |
-
- [`docs/architecture.md`](architecture.md) — vue d'ensemble post-chantiers 1-6.
|
| 226 |
-
- [`docs/profiles.md`](profiles.md) — profils de calcul (chantier 2).
|
| 227 |
-
- [`docs/views.md`](views.md) — vues HTML du rapport.
|
| 228 |
-
- [`docs/cli-workflows.md`](cli-workflows.md) — commandes CLI.
|
| 229 |
-
- `docs/api-stable.md` — *à créer en phase D* — engagement API publique du Cercle 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -15,9 +15,9 @@ Convention de rétrocompat
|
|
| 15 |
Pour chaque module déplacé depuis ``picarones/core/`` ou
|
| 16 |
``picarones/report/`` vers ``picarones/extras/``, un fichier-shim est
|
| 17 |
laissé à l'ancien emplacement qui réexporte les noms publics. Les
|
| 18 |
-
imports historiques (``from picarones.
|
| 19 |
...``) continuent à fonctionner sans modification.
|
| 20 |
|
| 21 |
-
Voir :doc:`docs/architecture
|
| 22 |
et les critères d'assignation au Cercle 3.
|
| 23 |
"""
|
|
|
|
| 15 |
Pour chaque module déplacé depuis ``picarones/core/`` ou
|
| 16 |
``picarones/report/`` vers ``picarones/extras/``, un fichier-shim est
|
| 17 |
laissé à l'ancien emplacement qui réexporte les noms publics. Les
|
| 18 |
+
imports historiques (``from picarones.measurements.taxonomy_intra_doc import
|
| 19 |
...``) continuent à fonctionner sans modification.
|
| 20 |
|
| 21 |
+
Voir :doc:`docs/architecture.md` pour la cartographie complète
|
| 22 |
et les critères d'assignation au Cercle 3.
|
| 23 |
"""
|
|
@@ -82,6 +82,6 @@ Tous les modules historiquement dans ``picarones.core.X`` restent
|
|
| 82 |
accessibles via des fichiers-shims qui les redirigent vers le nouvel
|
| 83 |
emplacement. Aucun import existant ne casse.
|
| 84 |
|
| 85 |
-
Voir :doc:`docs/architecture
|
| 86 |
refonte.
|
| 87 |
"""
|
|
|
|
| 82 |
accessibles via des fichiers-shims qui les redirigent vers le nouvel
|
| 83 |
emplacement. Aucun import existant ne casse.
|
| 84 |
|
| 85 |
+
Voir :doc:`docs/architecture.md` et la phase E du plan de
|
| 86 |
refonte.
|
| 87 |
"""
|
|
@@ -1,229 +0,0 @@
|
|
| 1 |
-
"""Tests de la phase C — extras/importers/ (importers vers Cercle 3).
|
| 2 |
-
|
| 3 |
-
Couvre :
|
| 4 |
-
|
| 5 |
-
- 6 importers (``_http``, ``iiif``, ``htr_united``, ``gallica``,
|
| 6 |
-
``huggingface``, ``escriptorium``) déplacés vers
|
| 7 |
-
``picarones/extras/importers/``.
|
| 8 |
-
- Identité préservée à travers les shims.
|
| 9 |
-
- ``huggingface`` et ``escriptorium`` émettent un ``UserWarning``
|
| 10 |
-
``experimental`` à l'import.
|
| 11 |
-
- ``picarones.importers/__init__.py`` continue à réexporter les
|
| 12 |
-
noms historiques.
|
| 13 |
-
- ``cli/_imports.py`` continue à fonctionner.
|
| 14 |
-
- pyproject.toml déclare ``[importers]``.
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
from __future__ import annotations
|
| 18 |
-
|
| 19 |
-
import importlib
|
| 20 |
-
import sys
|
| 21 |
-
import warnings
|
| 22 |
-
from pathlib import Path
|
| 23 |
-
|
| 24 |
-
import pytest
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 28 |
-
# 1. Imports historiques rétrocompat via shims
|
| 29 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
class TestImportersRetrocompat:
|
| 33 |
-
@pytest.mark.parametrize("module_path, attribute", [
|
| 34 |
-
("picarones.importers.iiif", "IIIFImporter"),
|
| 35 |
-
("picarones.importers.iiif", "import_iiif_manifest"),
|
| 36 |
-
("picarones.importers.htr_united", "HTRUnitedEntry"),
|
| 37 |
-
("picarones.importers.htr_united", "HTRUnitedCatalogue"),
|
| 38 |
-
("picarones.importers.htr_united", "import_htr_united_corpus"),
|
| 39 |
-
("picarones.importers.gallica", "GallicaClient"),
|
| 40 |
-
("picarones.importers.gallica", "GallicaRecord"),
|
| 41 |
-
("picarones.importers.gallica", "search_gallica"),
|
| 42 |
-
("picarones.importers.gallica", "import_gallica_document"),
|
| 43 |
-
("picarones.importers._http", "validate_http_url"),
|
| 44 |
-
("picarones.importers._http", "download_url"),
|
| 45 |
-
])
|
| 46 |
-
def test_legacy_path_works(self, module_path: str, attribute: str):
|
| 47 |
-
with warnings.catch_warnings():
|
| 48 |
-
warnings.simplefilter("ignore")
|
| 49 |
-
mod = importlib.import_module(module_path)
|
| 50 |
-
assert hasattr(mod, attribute)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 54 |
-
# 2. Imports via le nouveau chemin extras/importers/
|
| 55 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
class TestExtrasImportersPath:
|
| 59 |
-
@pytest.mark.parametrize("new_path, attribute", [
|
| 60 |
-
("picarones.extras.importers._http", "validate_http_url"),
|
| 61 |
-
("picarones.extras.importers._http", "download_url"),
|
| 62 |
-
("picarones.extras.importers.iiif", "IIIFImporter"),
|
| 63 |
-
("picarones.extras.importers.iiif", "import_iiif_manifest"),
|
| 64 |
-
("picarones.extras.importers.htr_united", "HTRUnitedCatalogue"),
|
| 65 |
-
("picarones.extras.importers.gallica", "GallicaClient"),
|
| 66 |
-
("picarones.extras.importers.huggingface", "HuggingFaceImporter"),
|
| 67 |
-
("picarones.extras.importers.escriptorium", "EScriptoriumClient"),
|
| 68 |
-
])
|
| 69 |
-
def test_extras_path_works(self, new_path: str, attribute: str):
|
| 70 |
-
with warnings.catch_warnings():
|
| 71 |
-
warnings.simplefilter("ignore")
|
| 72 |
-
mod = importlib.import_module(new_path)
|
| 73 |
-
assert hasattr(mod, attribute)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 77 |
-
# 3. Identité préservée
|
| 78 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
class TestIdentityThroughShim:
|
| 82 |
-
def test_iiif_identity(self):
|
| 83 |
-
with warnings.catch_warnings():
|
| 84 |
-
warnings.simplefilter("ignore")
|
| 85 |
-
from picarones.extras.importers.iiif import IIIFImporter as via_new
|
| 86 |
-
from picarones.importers.iiif import IIIFImporter as via_old
|
| 87 |
-
assert via_old is via_new
|
| 88 |
-
|
| 89 |
-
def test_gallica_identity(self):
|
| 90 |
-
with warnings.catch_warnings():
|
| 91 |
-
warnings.simplefilter("ignore")
|
| 92 |
-
from picarones.extras.importers.gallica import GallicaClient as via_new
|
| 93 |
-
from picarones.importers.gallica import GallicaClient as via_old
|
| 94 |
-
assert via_old is via_new
|
| 95 |
-
|
| 96 |
-
def test_http_helpers_identity(self):
|
| 97 |
-
with warnings.catch_warnings():
|
| 98 |
-
warnings.simplefilter("ignore")
|
| 99 |
-
from picarones.extras.importers._http import (
|
| 100 |
-
validate_http_url as via_new,
|
| 101 |
-
)
|
| 102 |
-
from picarones.importers._http import (
|
| 103 |
-
validate_http_url as via_old,
|
| 104 |
-
)
|
| 105 |
-
assert via_old is via_new
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 109 |
-
# 4. Modules expérimentaux : UserWarning à l'import
|
| 110 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def _force_reimport(module_name_substring: str) -> None:
|
| 114 |
-
"""Vide le cache d'import pour pouvoir capturer le UserWarning."""
|
| 115 |
-
for name in list(sys.modules.keys()):
|
| 116 |
-
if module_name_substring in name:
|
| 117 |
-
del sys.modules[name]
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
class TestExperimentalImporters:
|
| 121 |
-
def test_huggingface_emits_userwarning(self):
|
| 122 |
-
_force_reimport("huggingface")
|
| 123 |
-
with warnings.catch_warnings(record=True) as w:
|
| 124 |
-
warnings.simplefilter("always")
|
| 125 |
-
import picarones.extras.importers.huggingface # noqa: F401
|
| 126 |
-
msgs = [str(x.message) for x in w if issubclass(x.category, UserWarning)]
|
| 127 |
-
assert any("experimental" in m for m in msgs), (
|
| 128 |
-
f"huggingface n'a pas émis de UserWarning experimental — "
|
| 129 |
-
f"warnings reçus : {[str(x.message) for x in w]}"
|
| 130 |
-
)
|
| 131 |
-
|
| 132 |
-
def test_escriptorium_emits_userwarning(self):
|
| 133 |
-
_force_reimport("escriptorium")
|
| 134 |
-
with warnings.catch_warnings(record=True) as w:
|
| 135 |
-
warnings.simplefilter("always")
|
| 136 |
-
import picarones.extras.importers.escriptorium # noqa: F401
|
| 137 |
-
msgs = [str(x.message) for x in w if issubclass(x.category, UserWarning)]
|
| 138 |
-
assert any("experimental" in m for m in msgs)
|
| 139 |
-
|
| 140 |
-
def test_iiif_does_not_emit_warning(self):
|
| 141 |
-
"""Les importers maintenus ne doivent PAS émettre de warning."""
|
| 142 |
-
_force_reimport("iiif")
|
| 143 |
-
with warnings.catch_warnings(record=True) as w:
|
| 144 |
-
warnings.simplefilter("always")
|
| 145 |
-
import picarones.extras.importers.iiif # noqa: F401
|
| 146 |
-
msgs = [str(x.message) for x in w if issubclass(x.category, UserWarning)]
|
| 147 |
-
# Il peut y avoir d'autres warnings (deprecation Python, etc.)
|
| 148 |
-
# mais pas de "experimental" sur iiif
|
| 149 |
-
assert not any(
|
| 150 |
-
"iiif" in m and "experimental" in m for m in msgs
|
| 151 |
-
), "iiif ne doit pas être marqué experimental"
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 155 |
-
# 5. picarones.importers/__init__.py — réexports historiques
|
| 156 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
class TestImportersInitReexports:
|
| 160 |
-
def test_reexports_work(self):
|
| 161 |
-
"""Le ``__init__`` réexporte des symboles via les shims, eux-mêmes
|
| 162 |
-
chargeant depuis extras."""
|
| 163 |
-
with warnings.catch_warnings():
|
| 164 |
-
warnings.simplefilter("ignore")
|
| 165 |
-
from picarones.importers import (
|
| 166 |
-
EScriptoriumClient,
|
| 167 |
-
GallicaClient,
|
| 168 |
-
IIIFImporter,
|
| 169 |
-
)
|
| 170 |
-
assert IIIFImporter is not None
|
| 171 |
-
assert GallicaClient is not None
|
| 172 |
-
assert EScriptoriumClient is not None
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 176 |
-
# 6. cli/_imports.py — toujours fonctionnel
|
| 177 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
class TestCliImportsCommand:
|
| 181 |
-
def test_cli_imports_module_loads(self):
|
| 182 |
-
"""``picarones.cli._imports`` importe IIIFImporter depuis
|
| 183 |
-
``picarones.importers.iiif`` — doit fonctionner via shim."""
|
| 184 |
-
try:
|
| 185 |
-
with warnings.catch_warnings():
|
| 186 |
-
warnings.simplefilter("ignore")
|
| 187 |
-
import picarones.cli._imports # noqa: F401
|
| 188 |
-
except ImportError as exc:
|
| 189 |
-
if "click" in str(exc):
|
| 190 |
-
pytest.skip("click absent")
|
| 191 |
-
raise
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 195 |
-
# 7. pyproject.toml — extra [importers]
|
| 196 |
-
# ─────────────────────────────────────────────────────���────────────────────
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
class TestPyprojectExtra:
|
| 200 |
-
def test_importers_extra_declared(self):
|
| 201 |
-
path = Path(__file__).parent.parent / "pyproject.toml"
|
| 202 |
-
content = path.read_text(encoding="utf-8")
|
| 203 |
-
assert "importers = []" in content or 'importers = [' in content
|
| 204 |
-
assert "extras/importers" in content
|
| 205 |
-
assert "Cercle 3" in content
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 209 |
-
# 8. Originaux sont des shims minces
|
| 210 |
-
# ──────────────────────────────────────────────────────────────────────────
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
class TestOriginalsAreShims:
|
| 214 |
-
@pytest.mark.parametrize("path", [
|
| 215 |
-
"picarones/importers/_http.py",
|
| 216 |
-
"picarones/importers/iiif.py",
|
| 217 |
-
"picarones/importers/htr_united.py",
|
| 218 |
-
"picarones/importers/gallica.py",
|
| 219 |
-
"picarones/importers/huggingface.py",
|
| 220 |
-
"picarones/importers/escriptorium.py",
|
| 221 |
-
])
|
| 222 |
-
def test_is_thin_shim(self, path):
|
| 223 |
-
repo_root = Path(__file__).parent.parent
|
| 224 |
-
content = (repo_root / path).read_text(encoding="utf-8")
|
| 225 |
-
n_lines = len([line for line in content.splitlines() if line.strip()])
|
| 226 |
-
assert n_lines < 30, (
|
| 227 |
-
f"{path} fait {n_lines} lignes — devrait être un shim mince"
|
| 228 |
-
)
|
| 229 |
-
assert "déplacé" in content or "extras" in content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|