Spaces:
Running
CLAUDE.md — Picarones
Plateforme de benchmark OCR/HTR pour documents patrimoniaux. Repo : github.com/maribakulj/Picarones HuggingFace Space : huggingface.co/spaces/Ma-Ri-Ba-Ku/Picarones (Docker, port 7860)
Version courante : 0.9.0 (pré-1.0). Politique de versionning :
docs/explanation/versioning.md.
Architecture — 8 couches concentriques
Voir le manifeste complet dans docs/explanation/architecture.md.
domain → formats → evaluation → pipeline → adapters → app → reports → interfaces
Règle d'import stricte : les dépendances vont uniquement de l'extérieur
vers l'intérieur. Vérifié par tests/architecture/test_layer_dependencies.py
tests/architecture/test_no_legacy_imports_in_rewrite.py(LEGACY_PACKAGES = ()— plus aucun paquet legacy).
Tous les paquets legacy (picarones/{core,measurements,engines,modules, report,llm,pipelines,cli,web,extras}/ + adapters/legacy_engines/ +
adapters/legacy_pipelines/ + interfaces/{cli,web}/_legacy/) ont été
supprimés au cours des sprints A-H (mai 2026, clôture du rewrite
architectural — release 0.9.0).
Setup
pip install -e ".[dev,web]" # toujours inclure [web] pour les tests
pytest tests/ -q --tb=short # lancer les tests
picarones demo --output rapport.html # rapport démo sans moteur installé
picarones serve --port 8080 # interface web locale
Structure
Architecture canonique en 8 couches concentriques :
picarones/
├── domain/ Couche 1 — types purs (Pydantic + stdlib only)
│ ├── artifacts.py Artifact, ArtifactType (10 types)
│ ├── corpus.py CorpusSpec
│ ├── documents.py DocumentRef
│ ├── pipeline_spec.py PipelineSpec, PipelineStep
│ ├── module_protocol.py BaseModule
│ ├── facts.py Fact, FactType, FactImportance, DetectorRegistry
│ ├── evaluation_spec.py MetricSpec, EvaluationView, EvaluationSpec
│ ├── projection_spec.py ProjectionSpec
│ ├── provenance.py ProvenanceRecord
│ ├── run_manifest.py RunManifest
│ └── errors.py PicaronesError, AdapterStepError, ...
│
├── formats/ Couche 2 — parsing/sérialisation (lxml, defusedxml)
│ ├── alto/, pagexml/ ALTO 4, PAGE XML
│ ├── text/ normalisation, profils de comparaison
│ └── _xml_utils.py safe_parse_xml
│
├── evaluation/ Couche 3 — métriques et calcul
│ ├── metrics/ ~37 métriques (CER/WER, MUFI, philological, NER, …)
│ ├── statistics/ Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto, CDD
│ ├── views/, projectors/ EvaluationView, AltoToText, PageToText, ...
│ ├── registry/ MetricRegistry (DI)
│ ├── corpus.py Document, Corpus, GTLevel + payloads
│ ├── benchmark_result.py BenchmarkResult, EngineReport, DocumentResult
│ ├── metric_registry.py MetricSpec, register_metric, compute_at_junction
│ ├── metric_hooks.py register_document_metric, register_corpus_aggregator
│ ├── metric_result.py MetricsResult, aggregate_metrics
│ ├── synthetic.py generate_sample_benchmark (pour `picarones demo`)
│ └── _diff_utils.py compute_word_diff, compute_char_diff, diff_stats
│
├── pipeline/ Couche 4 — orchestration
│ ├── executor.py PipelineExecutor (instance-based)
│ ├── planner.py ExecutionPlan, StepInputBinding
│ ├── protocols.py StepExecutor Protocol
│ ├── runner.py CorpusRunner (backpressure + timeout + cancel)
│ ├── types.py RunContext, StepResult, PipelineResult
│ ├── llm_pipeline_builder.py make_ocr_llm_pipeline_spec (3 modes)
│ └── llm_pipeline_config.py OCRLLMPipelineConfig (container OCR+LLM)
│
├── adapters/ Couche 5 — adapters externes (libs autorisées)
│ ├── ocr/ Tesseract, Pero, Mistral OCR, Google Vision, Azure DI, Precomputed + factory
│ ├── llm/ OpenAI, Anthropic, Mistral, Ollama (BaseLLMAdapter)
│ ├── vlm/ Adapters VLM (zero-shot)
│ ├── corpus/ IIIF, Gallica, HTR-United, HuggingFace, eScriptorium
│ └── storage/ ArtifactStore, JobStore
│
├── app/ Couche 6 — services applicatifs
│ └── services/ BenchmarkService, RunOrchestrator, JobRunner,
│ benchmark_runner (entry point CLI/web), partial_store
│
├── reports/ Couche 7 — rendu HTML / JSON / CSV
│ ├── html/ ReportGenerator + ~37 renderers + 4 onglets XerOCR + 5 modules vues thématiques + templates Jinja2
│ ├── json/, csv/ exports tabulaires
│ ├── narrative/ moteur narratif (20 détecteurs)
│ ├── glossary/, i18n/ glossaire + i18n FR/EN
│ └── _helpers/ colors, render_helpers, assets
│
├── interfaces/ Couche 8 — entrées utilisateur
│ ├── cli/ Click CLI : run, diagnose, economics, edition,
│ │ compare, robustness, history, serve, metrics,
│ │ engines, info, demo, report, import (group)
│ └── web/ FastAPI : UI Jinja2 + SSE benchmark + ZIP upload
│
├── prompts/ 16 fichiers .txt (FR/EN/DE) — médiéval, imprimé ancien, presse XIXe
├── data/ Tables indicatives (pricing.yaml)
└── i18n.py Helper i18n (multi-langue rapports)
État des tests et bugs historiques
pytest tests/ → 5000+ tests collectés, 0 failed (mai 2026).
Les markers live (tests d'intégration contre vraie API/binaire) et
network (tests qui hit le réseau réel) sont opt-in en local via
pytest -m live ou pytest -m network. Le compteur exact dérive
de ±10 entre OS selon les binaires optionnels installés (tesseract,
pero-ocr) — c'est le badge CI qui porte le chiffre canonique, pas
la prose de ce fichier.
NB : utiliser python -m pytest tests/ plutôt que pytest tests/
directement — l'installation via uv tool install pytest masque
les deps Picarones et produit ~160 collection errors trompeurs.
Convention code_version dans les tests : la valeur "1.0.0"
utilisée dans ~50 fichiers de tests (RunContext,
ProvenanceRecord, ArtifactKey…) est un placeholder de
fixture, pas la version réelle du projet. Documentée dans
tests/_migration_helpers.py en tête
de module. Le garde-fou test_no_hardcoded_version la neutralise
explicitement.
Bugs documentés antérieurement — tous résolus
| Bug | Statut | Sprint de résolution |
|---|---|---|
Pipeline OCR+LLM sortie vide (tesseract → ministral-3b-latest) |
✅ Résolu | Sprint 15 — adapter Mistral logue finish_reason, completion_tokens, normalise les ContentChunk |
CI python-multipart manquant |
✅ Résolu | pyproject.toml expose python-multipart>=0.0.9 dans les extras dev ET web; ci.yml:71 installe .[dev,web] |
Tests fixtures post-Sprint 10 (counts moteurs, flag is_pipeline) |
✅ Résolu | Fixtures mises à jour |
Test Windows SQLite test_history_empty_db |
✅ Résolu | try/except OSError + gc.collect() avant unlink |
Test HuggingFace test_search_language_filter |
✅ Résolu | Assertion corrigée |
En cas de régression sur un de ces bugs, chercher les fichiers de test
correspondants (test_llm_pipeline_bugs.py, test_escriptorium_gallica.py,
test_web_interface.py) avant de ré-ouvrir une enquête.
Règles importantes — ne pas toucher
- Ne jamais retirer
python-multipartdes dépendances : FastAPI vérifie sa présence à l'import du module (décoration@app.postavecUploadFile), pas à l'exécution. Ça casse tous les tests web au setup. - Ne jamais mettre
except Exception: pass: remplacer parlogger.warning("[module] fonctionnalité dégradée : %s", e). - Toujours utiliser
logger.warningavec message explicite quand une fonctionnalité optionnelle échoue (confusion, taxonomy, structure, image_quality, etc.). - Avant tout push, lancer
make lint(ouruff check picarones/ tests/). La config est centralisée danspyproject.tomlsous[tool.ruff], donc CI, Makefile et invocation directe produisent le même résultat. Le joblintdu CI est bloquant — un F401 (import inutilisé) ou un E741 (variable ambiguë) fait échouer la PR, par design. - Les profils de normalisation sont dans
picarones/formats/text/normalization.py— l'endpoint/api/normalization/profilesdoit les lire dynamiquement depuis ce fichier, pas depuis une liste statique.
Variables d'environnement
# Clés API LLM (configurées dans HuggingFace Space Settings → Variables and secrets)
MISTRAL_API_KEY=...
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
# OCR cloud (optionnel)
GOOGLE_APPLICATION_CREDENTIALS=/path/to/creds.json
AZURE_DOC_INTEL_ENDPOINT=https://...
AZURE_DOC_INTEL_KEY=...
Pipelines OCR+LLM — modes
Trois modes canoniques (typés Literal[…] dans PipelineConfig.pipeline_mode
depuis Phase 2 du chantier post-rewrite — toute autre valeur est rejetée
en 422 par Pydantic, plus de fallback silencieux) :
| Mode (clé API) | Description |
|---|---|
zero_shot |
Le VLM reçoit l'image directement et transcrit sans OCR préalable (pas d'OCR amont). |
text_only |
OCR → texte brut → LLM corrige le texte sans voir l'image (modèles texte seul). |
text_and_image |
OCR → VLM reçoit image + texte brut pour correction multimodale. |
ministral-3b-latest = modèle texte pur → utiliser mode text_only
uniquement. L'UI envoie ces clés en anglais ; les libellés français
"Post-correction texte" / "Post-correction image+texte" sont des
labels d'affichage (i18n), pas des identifiants API.
CI/CD
- CI GitHub Actions :
.github/workflows/ci.yml— Python 3.11/3.12, Linux/macOS/Windows - Sync HuggingFace :
.github/workflows/sync_to_huggingface.yml— push auto sur main (nécessite secretHF_TOKENdans GitHub Settings → Secrets → Actions) - HuggingFace Space : Docker sur port 7860
Sprints réalisés
L'historique détaillé des 97+ sprints du projet (de la fondation
S1 jusqu'au rewrite ciblé S27-S46, l'audit institutionnel S47-S59,
puis le retrait complet du legacy A-H aboutissant à 0.9.0) est
dans le CHANGELOG.md à la racine.
Release 0.9.0 (mai 2026) : la migration vers l'architecture
8 couches canoniques est terminée. Plus aucun paquet legacy. Plus
aucun shim. Cette release portait en interne la dénomination « v2.0 »
jusqu'au repositionnement en SemVer pré-1.0 (voir
docs/explanation/versioning.md).
Chantier post-rewrite (mai 2026, branche claude/fix-module-rewiring-MHssX) :
réconciliation des chemins UI/API/runner après audit révélant des
options ignorées, moteurs annoncés sans backend, surfaces filesystem
ouvertes et round-trip JSON appauvri (cf. CHANGELOG.md). Cinq phases
exécutées :
- Phase 1 (sécurité P0) :
output_dirvalidé (importers HTR-United/HF),db_pathvalidé (/api/history/regressions), ZIP collision de basename + validation image extraite. - Phase 2 (méthodologie P0) :
pipeline_modestrict Literal,BenchmarkResult.from_json_object(round-trip JSON complet — taxonomy/NER/calibration/philological/searchability/hallucination préservés),partial_storefingerprint SHA-256 (engine_config + normalization + char_exclude + corpus mtime/size + code version). - Phase 3 (moteurs fantômes) : adapters
KrakenAdapteretCalamariAdapterimplémentés (lazy imports, extras[kraken]/[calamari]) + matrice CLI/Web/factory unifiée. - Phase 4 (code zombie) :
upload_purge_task(RGPD) branchée au lifespan + payload corpus dansJobStore.create_job,/api/benchmark/startunifié vers le worker v2,HTRUnitedCatalogue.from_remote(avec fallback demo + champis_demoexposé), endpoints config save/load branchés à l'UI, workflows CLIdiagnose/economics/editiongénèrent le HTML automatiquement. - Phase 5 (naming) :
CompetitorConfig→PipelineConfig,ocr_engine→engine_name(rupture API, le field accepte aussi des VLMs etcorpus). Le workerrun_benchmark_thread_v2propage les nouveaux champs (le suffixe_v2est un nom de fonction interne hérité, sans rapport avec la dénomination de version).
Règles d'architecture :
evaluation/whitelist externe :PIL, annotated_types, jiwer, numpy, pydantic, rapidfuzz, scipy, spacy, typing_extensions, yaml— paspytesseract,mistralai,azure,google,pero_ocr. Tout code qui importe ces libs externes va dansadapters/.evaluation/ne peut pas importer depuispipeline/: c'est le sens inverse de la dépendance. Un module qui pont les deux contrats vit danspipeline/.reports/consomme uniquementevaluation/metrics/.interfaces/(couche 8) consommeapp.services(couche 6) :app.services.benchmark_runnerest l'entry point unique pour CLI et web.test_file_budgets: si un fichier dépasse 400 LOC, ajouter une entrée avec budget = LOC actuel + ~15 %.
Moteur narratif
Le modèle de données (Fact, FactType, FactImportance,
DetectorRegistry) vit en couche 1 (domain) dans
picarones/domain/facts.py. Les détecteurs et
le rendu vivent en couche 7 (reports) :
picarones/reports/narrative/
├── __init__.py API publique + pipeline build_synthesis
├── arbiter.py Tri par importance, non-redondance, anti-contradiction
├── renderer.py Rendu templates YAML par str.format_map (déterministe)
├── registry.py Registre par défaut des détecteurs
├── templates/{fr,en}.yaml 20 templates × 2 langues
└── detectors/ 20 détecteurs en 6 familles
├── ranking.py 5 (global_leader, statistical_tie, significant_gap,
│ speed_winner, median_mean_gap_warning)
├── pareto.py 3 (pareto_alternative, cost_outlier,
│ pricing_staleness_warning)
├── stratum.py 3 (stratum_winner, stratum_collapse,
│ stratification_recommended)
├── quality.py 4 (error_profile_outlier, llm_hallucination_flag,
│ robustness_fragile, confidence_warning)
├── history.py 4 (engine_off_baseline, engine_unstable,
│ regression_in_history, importer_fallback_triggered)
└── ensemble.py 1 (ensemble_opportunity)
Principe anti-hallucination (formulation précise — audit F7) :
aucun LLM, aucune valeur aléatoire/fabriquée, rendu str.format_map
déterministe. Les noms d'entités sont repris verbatim du JSON
d'entrée ; les nombres sont soit verbatim, soit une fonction
déterministe et auditable de valeurs d'entrée (écart relatif,
accélération, largeur d'IC…). L'ancienne formulation « chaque nombre
provient du JSON » était trop forte (dérivations) et le test
historique test_narrative_engine.py était circulaire (il
validait les nombres rendus contre le payload du Fact, lui-même
rempli par le détecteur). La traçabilité à la source est désormais
vérifiée par tests/evaluation/test_scientific_audit_2026.py
(TestF7NarrativeTraceability) : reconstruction depuis le
BenchmarkResult d'origine + déterminisme bit-à-bit.
Règle anti-contradiction (arbitre) : si SIGNIFICANT_GAP (Wilcoxon
non corrigé) et STATISTICAL_TIE (Nemenyi corrigé) concernent les mêmes
moteurs, Nemenyi l'emporte.
Pipeline : build_synthesis(benchmark_data, lang, max_facts=5)
détecte, arbitre, rend.
Contexte développement
- Environnement : GitHub Codespaces, Python 3.11+
- Tests : voir « État des tests et bugs historiques » plus haut
(compteur synchronisé par
scripts/gen_readme_tables.py). - Manifeste architecture :
docs/explanation/architecture.md. - API publique stable :
docs/reference/api-stable.md.
Statut 0.9.0 — mai 2026
L'architecture 8 couches est complète. Tous les paquets legacy
top-level (core/, measurements/, engines/, modules/,
report/, llm/, pipelines/, cli/, web/, extras/) ainsi
que les sous-paquets transitoires (adapters/legacy_engines/,
adapters/legacy_pipelines/, interfaces/{cli,web}/_legacy/) ont
été supprimés.
| Phase / Sprint | Statut | Détails |
|---|---|---|
| Foundation S1-S26 | ✅ | domain, formats, evaluation, narrative engine |
| Rewrite ciblé S27-S46 | ✅ | pipeline, app.services, adapters/ocr canonique, reports |
| Audit S47-S59 | ✅ | confidences, sécurité web, registry typé, baselines |
| Plan A-H (mai 2026) | ✅ | Retrait complet du legacy : core/measurements/engines/modules/report/llm/pipelines/cli/web/extras supprimés ; interfaces/{cli,web}/_legacy promus au niveau canonique ; release 0.9.0 (cycle de rewrite clôturé) |
| Chantier UI + refonte rapport XerOCR | 🚧 | Branche claude/charming-ritchie-Z820A : importeurs IIIF/Gallica/eScriptorium côté web (S2-S3), vue Historique longitudinal SQLite (S4), refonte HTML 4 onglets XerOCR (S5-S12), compare 2 runs client-side (S12), audit institutionnel + 12 critiques corrigées (S13). Voir CHANGELOG.md pour le détail. |
Roadmap vers 1.0.0
La sortie de 1.0.0 est conditionnée à la livraison de :
- Surface UI complète — exposition de tous les champs
BenchmarkRunRequest, parité fonctionnelle avec la CLI (compare,robustness,history). - Parité importeurs corpus — IIIF, Gallica, eScriptorium accessibles en web.
- Refonte rapport HTML — IA 4 onglets (Overview / Engines / Documents / Crosses).
Releases intermédiaires 0.10.0, 0.11.0, … publient des jalons techniques.
Compare 2 runs — pattern client-side
Le rapport HTML expose désormais un bouton « ⇄ Comparer un run » dans
le footer qui ouvre un file picker JSON. Le second run est parsé
côté client uniquement (FileReader + JSON.parse, 0 appel réseau),
les deltas CER sont calculés par moteur (isinstance strict +
Number.isFinite), et un bandeau sticky non-intrusif affiche les
régressions (clay) / améliorations (fern). Plafond fichier 50 Mo.
La logique vit dans picarones/reports/html/templates/_compare.js ;
les CLI picarones compare a.json b.json -o diff.html reste
l'alternative server-side pour rapport autonome.
Convention badges A→E (renderers HTML)
Les badges moteurs (lettre + accent cyclique fern/slate/clay/butter/ink)
sont centralisés dans picarones/reports/_helpers/engine_badges.py.
Toute extension de la palette doit modifier ce module unique — les
renderers engines_table, documents_gallery et crosses consomment
les helpers engine_letter_color() et engine_accent_cssvar(idx, deep=...).