Spaces:
Running
Architecture Picarones — manifeste
Audience : développeurs et mainteneurs. Ce document explique pourquoi le code est organisé comme il l'est, pas seulement où sont les fichiers. Pour la liste exhaustive des modules, lire directement le code — il est typé et documenté.
Statut 0.9.0 — une seule arborescence canonique
À la release 0.9.0 (mai 2026, clôture du rewrite architectural),
Picarones a une seule arborescence. Tous les paquets
pré-rewrite ainsi que leurs sous-paquets transitoires ont été
supprimés au cours des sprints A-H. Pour le détail historique,
voir le CHANGELOG section 0.9.0 et
docs/archive/2026-migration/.
Toute documentation, tout commentaire qui mentionne « deux
arborescences » ou « legacy en cours de retrait » est obsolète.
La seule cohabitation acceptable depuis 0.9.0 est celle entre
modules canoniques : par exemple evaluation/metric_registry
(module-level, side-effect d'import) et evaluation/registry/registry
(instance-based) — deux patterns volontairement coexistants pour
deux usages distincts (auto-discovery vs DI explicite).
Le test tests/architecture/test_no_legacy_imports_in_rewrite.py
verrouille cet invariant via LEGACY_PACKAGES = ().
Architecture — 8 couches concentriques
domain → formats → evaluation → pipeline → adapters → app → reports → interfaces
Règle de dépendance stricte : les flèches d'import vont uniquement
de l'extérieur vers l'intérieur (couche N peut importer 1..N-1, pas
N+1..8). Vérifié par
tests/architecture/test_layer_dependencies.py.
| # | Couche | Rôle |
|---|---|---|
| 1 | domain/ |
Types purs (Pydantic + stdlib) |
| 2 | formats/ |
Parsers ALTO, PAGE XML, normalisation texte |
| 3 | evaluation/ |
Métriques, statistiques, vues d'évaluation |
| 4 | pipeline/ |
DAG d'étapes, cache, runner corpus-wide |
| 5 | adapters/ |
OCR, LLM, VLM, corpus importers, storage |
| 6 | app/ |
Services applicatifs (orchestration) |
| 7 | reports/ |
Rendu HTML / JSON / CSV |
| 8 | interfaces/ |
CLI Click + Web FastAPI |
picarones/domain/ — types purs
Couche 1 (la plus interne). Aucune dépendance d'exécution, aucun I/O, aucun framework. Pydantic et stdlib uniquement.
| Module | Contenu |
|---|---|
artifacts.py |
Artifact, ArtifactType (10 types : IMAGE, RAW_TEXT, ALTO_XML, PAGE_XML, ENTITIES, READING_ORDER, ALIGNMENT, CORRECTED_TEXT, CANONICAL_DOCUMENT, CONFIDENCES) |
artifact_key.py |
ArtifactKey — clé canonique multi-paramètres pour la reprise par hash |
corpus.py |
CorpusSpec, métadonnées de corpus |
documents.py |
DocumentRef, GroundTruthRef |
evaluation_spec.py |
MetricSpec, EvaluationView, EvaluationSpec |
pipeline_spec.py |
PipelineSpec, PipelineStep, INITIAL_STEP_ID |
projection_spec.py |
ProjectionSpec (transformation candidate avant évaluation) |
provenance.py |
ProvenanceRecord |
run_manifest.py |
RunManifest — empreinte immuable d'un run, sérialisée en run_manifest.json |
errors.py |
Hiérarchie d'exceptions (PicaronesError, AdapterStepError, ArtifactValidationError, …) |
picarones/formats/ — parsers et sérialiseurs
Lecture/écriture des formats externes : ALTO XML, PAGE XML, texte normalisé. Dépend du domain ; aucune logique d'évaluation.
Le parser XML interne (_xml_utils.safe_parse_xml) délègue à
defusedxml avec forbid_dtd=True, bloquant XXE, Billion Laughs
et déclarations <!DOCTYPE>. Les défenses sont verrouillées par
tests/security/test_xxe_attack.py.
picarones/evaluation/ — moteurs d'évaluation
| Sous-package | Rôle |
|---|---|
metrics/ |
~37 métriques (CER/WER, philologiques, calibration, NER, layout…). Enregistrées via @register_metric au registre typé |
projectors/ |
Projections inter-types (ALTO → texte, canonical → texte) avec ProjectionReport |
views/ |
Vues d'évaluation : TextView, AltoView, SearchView. L'EvaluationViewExecutor aligne candidate + GT, applique normalisation + projection, calcule les métriques |
evaluation_engine.py |
Moteur central qui exécute une EvaluationView |
projection_engine.py |
Moteur de projection |
registry/ |
MetricRegistry — découverte typée par signature (input_type, output_type) |
statistics/ |
Wilcoxon, Friedman/Nemenyi, bootstrap, Pareto, CDD |
synthetic.py |
generate_sample_benchmark (utilisé par picarones demo) |
Whitelist d'imports externes : PIL, annotated_types, jiwer, numpy, pydantic, rapidfuzz, scipy, spacy, typing_extensions, yaml. Pas pytesseract, mistralai, azure, google, pero_ocr — ceux-là vivent en couche 5 (adapters/).
picarones/pipeline/ — DAG d'étapes
Orchestration mono-document d'une pipeline composée :
| Module | Rôle |
|---|---|
executor.py |
PipelineExecutor — exécute un PipelineSpec step par step, capture StepResult, filtre outputs sur step.output_types |
planner.py |
PipelinePlanner — résout les inputs_from, valide la spec, calcule les métriques aux jonctions |
validation.py |
Validation statique d'une PipelineSpec (types s'enchaînent, pas de cycle) |
runner.py |
CorpusRunner — orchestration corpus-wide (ThreadPool unique), backpressure, timeout, cancellation |
cache.py, cache_helpers.py, cache_protocol.py |
Reprise par hash via ArtifactCachePort |
yaml_io.py |
Sérialisation YAML déterministe d'une PipelineSpec |
llm_pipeline_builder.py |
make_ocr_llm_pipeline_spec (3 modes : text_only, text_and_image, zero_shot) |
llm_pipeline_config.py |
OCRLLMPipelineConfig (container OCR+LLM) |
picarones/adapters/ — implémentations concrètes
C'est ici que vivent les dépendances externes (pytesseract, pero, mistralai, openai, anthropic, google-cloud-vision, …).
| Sous-package | Adapters |
|---|---|
ocr/ |
TesseractAdapter, PeroOCRAdapter, MistralOCRAdapter, GoogleVisionAdapter, AzureDocIntelAdapter, PrecomputedTextAdapter + factory ocr_adapter_from_name |
llm/ |
AnthropicLLMAdapter, OpenAILLMAdapter, MistralLLMAdapter, OllamaLLMAdapter |
vlm/ |
AnthropicVLMAdapter, OpenAIVLMAdapter, MistralVLMAdapter, OllamaVLMAdapter (héritage multiple BaseVLMAdapter + BaseLLMAdapter, MRO guard) |
corpus/ |
local folder, IIIF, Gallica, HTR-United, HuggingFace Datasets, eScriptorium |
storage/ |
InMemoryArtifactStore, FilesystemArtifactStore, JobStore (SQLite avec schema versioning) |
output_paths.py |
Helper partagé resolve_output_path (workspace-aware, read-only-mount-safe) |
_retry.py |
Helper partagé call_with_retry (3 retries, backoff 2/4/8s, sur 429+5xx+timeout réseau) |
Règle : un adapter peut importer le domain et ses libs externes.
Il ne doit jamais importer app/ ou interfaces/. Il n'a aucune
logique d'évaluation (un OCR adapter ne calcule pas le CER — il
produit un artefact texte que evaluation/ consommera).
Anti-SSRF : corpus/_http.py:validate_http_url refuse
loopback, lien-local, RFC 1918, métadonnées cloud (AWS
169.254.169.254, GCP metadata.google.internal). Verrouillé par
tests/security/test_ssrf_attack.py.
picarones/app/ — services applicatifs
Orchestration entre adapters et evaluation.
| Module | Rôle |
|---|---|
services/run_orchestrator.py |
RunOrchestrator.execute(RunSpec) — point d'entrée d'un run complet |
services/benchmark_service.py |
BenchmarkService.run — exécute pipelines × vues × corpus, produit RunResult |
services/benchmark_runner.py |
Façade run_benchmark_via_service consommée par CLI/web |
services/job_runner.py |
JobRunner — soumission asynchrone (thread daemon) avec persistance JobStore |
services/corpus_service.py |
Loading + sandboxing + extraction ZIP avec ZIP slip protection |
services/dependencies.py |
capture_dependencies_lock() via importlib.metadata pour le RunManifest |
services/path_security.py |
WorkspaceManager — sandboxe par session |
services/registry_service.py |
Découverte des adapters et vues canoniques |
services/partial_store.py |
Persistance NDJSON des résultats partiels (reprise sur interruption) |
schemas/run_spec.py |
RunSpec, StepSpec — modèles YAML user-facing |
results.py |
RunResult, RunDocumentResult, ReportRenderer (alias type unique) |
Anti ZIP slip : corpus_service._extract_safely rejette les
chemins absolus, .., octets nuls, symlinks ZIP entries
(mode UNIX 0xA000), avec garde-fou final target.resolve().relative_to(extract_dir).
Verrouillé par tests/security/test_zip_slip_attack.py.
picarones/reports/ — rendu déterministe
| Sous-package | Rôle |
|---|---|
csv/render.py |
CsvReportRenderer — un CSV plat (run_id, doc, pipeline, view, metric, value, status) |
json/render.py |
JsonReportRenderer — manifest + documents en JSON déterministe |
html/render.py |
HtmlReportRenderer — rapport autonome (TextView, AltoView, SearchView) — minimaliste |
html/generator.py |
ReportGenerator — rapport interactif riche (~37 renderers + 4 onglets XerOCR + 5 modules de vues thématiques internes) consommé par CLI/web |
narrative/ |
Moteur narratif (20 détecteurs) — synthèse factuelle déterministe |
glossary/, i18n/ |
Glossaire + i18n FR/EN |
Le rendu est strict : pas de JS dynamique côté serveur, pas d'I/O hors écriture finale, déterministe bit-for-bit à entrée constante. Permet à un relecteur 5 ans plus tard de hasher un rapport et de le citer.
Anti-XSS : html/generator.py utilise
autoescape=select_autoescape(['html', 'j2', 'xml']) (Jinja2) +
helper _safe_json_for_script_tag qui encode <>& en
<>& pour le JSON injecté dans
<script type="application/json">. Verrouillé par
tests/security/test_xss_in_reports.py.
picarones/interfaces/ — points d'entrée user-facing
| Sous-package | Rôle |
|---|---|
cli/ |
Click — 16+ commandes : run, diagnose, economics, edition, compare, robustness, history, serve, metrics, engines, info, demo, report, import (group) |
web/ |
FastAPI — UI Jinja2 + SSE benchmark + ZIP upload + 11 routers (corpus, benchmark, jobs, reports, history, engines, normalization, importers, synthesis, system, home) |
Anti-CSRF : middleware csrf_middleware actif si
PICARONES_CSRF_REQUIRED=1. Pattern double-submit cookie + HMAC
signature. Verrouillé par tests/security/test_csrf_required.py.
Principes architecturaux
Pas de shim hors deprecation period
Un module a un seul emplacement canonique. Quand un module migre, on choisit explicitement entre :
- Suppression dure (pour la dette interne, pas de caller externe).
- Shim avec
DeprecationWarning(pour la stabilité d'API publique). Le shim a une date de retrait inscrite dans le CHANGELOG.
À 0.9.0 il reste un seul shim documenté :
picarones/pipeline/spec.py (réexporte picarones.domain.pipeline_spec),
dont la deprecation period expire à la prochaine minor 0.10.0.
Pas d'except Exception: pass
Toute fonctionnalité optionnelle qui échoue émet un
logger.warning("[module] feature dégradée : %s", exc) avec contexte.
Vérifié par tests/architecture/test_no_side_effect_imports.py.
Tests architecturaux comme garde-fous
Plusieurs tests verrouillent des invariants structurels que la revue de code humaine raterait :
test_layer_dependencies.py— couches strictement orientéestest_no_legacy_imports_in_rewrite.py—LEGACY_PACKAGES = ()test_file_budgets.py— pas de god-modulestest_doc_paths.py— chemins cités dans la doc existenttest_output_paths_uniformity.py— tous les adapters passent parresolve_output_pathtest_storage_keys_filesystem_safe.py— clés du store filesystem-safe (Windows)test_manifest_reproducibility.py—RunManifestcapture tout pour rejouertest_module_coverage.py— chaque module a un test associé
Tests de sécurité comme verrous de défense
63 tests d'attaque verrouillent les défenses revendiquées :
tests/security/test_xss_in_reports.py(5) — autoescape Jinja2 + escape JSON.tests/security/test_xxe_attack.py(9) — XXE / Billion Laughs / DTD.tests/security/test_zip_slip_attack.py(9) — ZIP slip + symlinks.tests/security/test_ssrf_attack.py(26) — loopback, RFC 1918, métadonnées cloud.tests/security/test_csrf_required.py(14) — double-submit + HMAC.
Reproductibilité bit-for-bit
Le RunManifest capture systématiquement : code_version,
pipeline_specs complets, adapter_kwargs, dependencies_lock
(via importlib.metadata), view_specs, timestamps. La
sérialisation est déterministe (Pydantic ordered fields, JSON
sorted keys). Le hash du manifest peut être cité dans une
publication scientifique.
Évolution
L'évolution de l'architecture est documentée :
- Backlog vivant :
docs/roadmap/backlog.md - Plans archivés (migration legacy → rewrite, terminée à
0.9.0) :docs/archive/2026-migration/ - Roadmap historique pré-rewrite :
docs/archive/2026-roadmap/ - Audits institutionnels :
docs/archive/2026-audits/ - Politique d'API publique :
docs/reference/api-stable.md