Spaces:
Running
feat(evaluation): Sprint A14-S13 — DefaultEvaluationViewExecutor + ProjectorRegistry + ré-ordonnancement des couches
Browse filesSprint S13 du plan rewrite ciblé. Démarre la Phase 3 (vues
d'évaluation, S13-S18). Pose le moteur d'orchestration des vues :
recevoir un candidat + une GT, projeter si nécessaire, normaliser,
calculer chaque métrique, retourner un ViewResult.
Modules livrés
--------------
``picarones/evaluation/projectors/registry.py``
``ProjectorRegistry`` instancié explicitement (symétrique au
``MetricRegistry`` du S5). API : register(projector), get(name),
__contains__, __len__, names(). Erreurs typées
``ProjectorRegistrationError``, ``ProjectorNotFoundError`` (héritent
de ``PicaronesError``). Pas de singleton global, pas de side-effect.
``picarones/evaluation/views/executor.py``
``DefaultEvaluationViewExecutor(metric_registry, projector_registry,
payload_loader)`` — implémentation concrète du protocole
``EvaluationViewExecutor`` (S5).
Flux d'``evaluate(view, candidate, ground_truth)`` :
1. Vérifie ``view.accepts(candidate.type)``. Refuse en
``ValueError`` si non.
2. Si ``view.projection`` non-identité : récupère le projecteur du
registre, applique, capture ``ProjectionReport``.
``ProjectorNotFoundError`` → ``ProjectionError`` typée.
Toute autre exception du projecteur → ``ProjectionError`` typée.
3. Charge les payloads candidat + GT via ``payload_loader``.
Si le loader plante : ``ViewResult`` avec ``failed_metrics``
pour toutes les métriques, ``warnings`` enrichi du message.
4. Si ``view.normalization_profile`` : applique le profil aux
deux payloads (str uniquement, les non-str passent inchangés).
5. Pour chaque métrique : compute via ``MetricRegistry``. Métrique
qui lève ou non enregistrée → ``failed_metrics`` (le ViewResult
reste construit, autres métriques traitées normalement).
6. Construit le ``ViewResult`` final en fusionnant :
- ``view.warnings`` + ``projection_report.warnings``,
- ``view.ignored_dimensions`` + ``projection_report.ignored_dimensions``
(déduplication préservant l'ordre).
``payload_loader`` injecté pour découpler l'executor du stockage
(filesystem, in-memory, distant). En tests : dict in-memory.
En prod (S19) : service applicatif qui sait gérer les workspaces.
Ré-ordonnancement architectural
-------------------------------
``LAYER_ORDER`` mis à jour : ``formats`` passe avant ``evaluation``.
Ancien : domain → evaluation → pipeline → formats → adapters → ...
Nouveau : domain → formats → evaluation → pipeline → adapters → ...
Justification : ``formats/`` (parsers, normalization) est un
utilitaire bas niveau qu'``evaluation/`` consomme (cf.
``DefaultEvaluationViewExecutor`` qui charge un profil de
normalisation depuis ``formats.text.normalization``). L'inverse
n'a aucun sens.
Conséquence : les projecteurs ``AltoToText`` et ``PageToText`` qui
vivaient dans ``formats/alto/projector.py`` et
``formats/pagexml/projector.py`` violaient la nouvelle hiérarchie
(ils importent ``evaluation.projectors.base.ProjectionReport``).
Déplacés dans :
``picarones.evaluation.projectors.alto``
``picarones.evaluation.projectors.pagexml``
Choix architectural assumé : la projection est conceptuellement un
**composant d'évaluation**, pas un format. Un parser ALTO appartient
à ``formats/`` (lit/écrit du XML). Une projection ALTO → texte
appartient à ``evaluation/`` (transforme un artefact pour le
comparer dans une vue).
Aucun re-export ascendant n'est ajouté dans ``formats/alto/__init__.py``
(violerait la couche). Les 2 tests S9 qui importaient ces noms
sont mis à jour pour utiliser le nouveau chemin :
from picarones.evaluation.projectors import (
AltoToText, alto_document_to_text,
PageToText, page_document_to_text,
)
Tests — 18 nouveaux tests
-------------------------
``tests/evaluation/test_sprint_a14_s13_view_executor.py`` :
10 cas d'évaluation paramétrant le flux principal :
1. RAW_TEXT direct, pas de projection (égalité parfaite).
2. RAW_TEXT direct, candidat différent.
3. ALTO_XML projeté en RAW_TEXT, ProjectionReport présent.
4. View rejette wrong artifact type → ValueError.
5. Projecteur introuvable → ProjectionError typée.
6. Projecteur qui lève → ProjectionError typée.
7. Métrique qui lève → failed_metrics, autres OK.
8. Métrique non enregistrée → failed_metrics.
9. View avec normalization_profile → normalisation appliquée.
10. Loader qui plante → toutes métriques en failed.
3 tests sur le constructeur (rejette types invalides).
5 tests sur ``ProjectorRegistry`` (register, idempotent, not found,
two registries indépendants, refus protocole non satisfait).
État de la suite
----------------
``pytest tests/ -q`` → 4188 passed, 8 skipped, 2 failed
(strictement environnementaux). +18 tests vs S12. Aucune
régression S13.
Critère go/no-go S13 atteint
----------------------------
``executor.evaluate(text_view, alto_artifact, gt_text_artifact)``
retourne un ``ViewResult`` avec :
- ``metric_values["cer"]`` calculé après projection ALTO→texte,
- ``projection_report`` avec ``ignored_dimensions`` et
``warnings`` propagés,
- ``ignored_dimensions`` du ViewResult fusionnant ceux de la vue
et ceux de la projection.
Prêt pour S14 (TextView — première vue canonique qui répond à
"quel pipeline produit le meilleur texte final ?").
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
- picarones/evaluation/projectors/__init__.py +27 -1
- picarones/{formats/alto/projector.py → evaluation/projectors/alto.py} +0 -0
- picarones/{formats/pagexml/projector.py → evaluation/projectors/pagexml.py} +0 -0
- picarones/evaluation/projectors/registry.py +130 -0
- picarones/evaluation/views/__init__.py +10 -1
- picarones/evaluation/views/executor.py +308 -0
- picarones/formats/alto/__init__.py +9 -4
- picarones/formats/pagexml/__init__.py +3 -3
- tests/architecture/test_layer_dependencies.py +5 -1
- tests/evaluation/test_sprint_a14_s13_view_executor.py +379 -0
- tests/formats/alto/test_sprint_a14_s9_alto.py +1 -2
- tests/formats/pagexml/test_sprint_a14_s9_pagexml.py +1 -2
|
@@ -21,6 +21,32 @@ pas un projecteur.
|
|
| 21 |
|
| 22 |
from __future__ import annotations
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
from picarones.evaluation.projectors.base import ProjectionReport, Projector
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
from __future__ import annotations
|
| 23 |
|
| 24 |
+
from picarones.evaluation.projectors.alto import (
|
| 25 |
+
AltoToText,
|
| 26 |
+
alto_document_to_text,
|
| 27 |
+
)
|
| 28 |
from picarones.evaluation.projectors.base import ProjectionReport, Projector
|
| 29 |
+
from picarones.evaluation.projectors.pagexml import (
|
| 30 |
+
PageToText,
|
| 31 |
+
page_document_to_text,
|
| 32 |
+
)
|
| 33 |
+
from picarones.evaluation.projectors.registry import (
|
| 34 |
+
ProjectorNotFoundError,
|
| 35 |
+
ProjectorRegistrationError,
|
| 36 |
+
ProjectorRegistry,
|
| 37 |
+
)
|
| 38 |
|
| 39 |
+
__all__ = [
|
| 40 |
+
# Protocol + report
|
| 41 |
+
"Projector",
|
| 42 |
+
"ProjectionReport",
|
| 43 |
+
# Registry
|
| 44 |
+
"ProjectorRegistry",
|
| 45 |
+
"ProjectorRegistrationError",
|
| 46 |
+
"ProjectorNotFoundError",
|
| 47 |
+
# Concrete projectors (déplacés depuis formats/ au S13)
|
| 48 |
+
"AltoToText",
|
| 49 |
+
"alto_document_to_text",
|
| 50 |
+
"PageToText",
|
| 51 |
+
"page_document_to_text",
|
| 52 |
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``ProjectorRegistry`` — Sprint A14-S13.
|
| 2 |
+
|
| 3 |
+
Container instancié explicitement qui mappe ``projector_name``
|
| 4 |
+
vers une instance ``Projector``. Symétrique du ``MetricRegistry``
|
| 5 |
+
(S5) : pas de singleton global, pas de side-effect d'import.
|
| 6 |
+
|
| 7 |
+
Pattern d'utilisation
|
| 8 |
+
---------------------
|
| 9 |
+
|
| 10 |
+
.. code-block:: python
|
| 11 |
+
|
| 12 |
+
from picarones.evaluation.projectors import (
|
| 13 |
+
ProjectorRegistry, AltoToText,
|
| 14 |
+
)
|
| 15 |
+
from picarones.formats.alto import AltoToText as _AltoToText
|
| 16 |
+
|
| 17 |
+
registry = ProjectorRegistry()
|
| 18 |
+
registry.register(_AltoToText())
|
| 19 |
+
registry.register(PageToText())
|
| 20 |
+
|
| 21 |
+
projector = registry.get("alto_to_text")
|
| 22 |
+
target_artifact, report = projector.project(source_artifact, {})
|
| 23 |
+
|
| 24 |
+
Au S20, ce registre sera construit par
|
| 25 |
+
``app/services/registry_service.py`` au démarrage de l'application.
|
| 26 |
+
Pour S13-S18, chaque test ou consommateur l'instancie explicitement.
|
| 27 |
+
|
| 28 |
+
Anti-sur-ingénierie
|
| 29 |
+
-------------------
|
| 30 |
+
Pas de versioning de projecteur, pas de namespace, pas de recherche
|
| 31 |
+
par tag. Ces extras viendront quand un caller en aura concrètement
|
| 32 |
+
besoin (probablement avec les projecteurs contribués par des modules
|
| 33 |
+
tiers, post-livraison).
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
from picarones.domain.errors import PicaronesError
|
| 39 |
+
from picarones.evaluation.projectors.base import Projector
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class ProjectorRegistrationError(PicaronesError):
|
| 43 |
+
"""Tentative d'enregistrement invalide d'un projecteur."""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class ProjectorNotFoundError(PicaronesError):
|
| 47 |
+
"""Le projecteur demandé n'est pas enregistré."""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ProjectorRegistry:
|
| 51 |
+
"""Container mutable de projecteurs indexés par ``name``.
|
| 52 |
+
|
| 53 |
+
Thread-safe en lecture après initialisation ; la séquence
|
| 54 |
+
d'enregistrement attendue est : un seul service, au démarrage,
|
| 55 |
+
enregistre tous les projecteurs en une fois, puis l'instance
|
| 56 |
+
est figée par convention.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(self) -> None:
|
| 60 |
+
self._projectors: dict[str, Projector] = {}
|
| 61 |
+
|
| 62 |
+
# ──────────────────────────────────────────────────────────────────
|
| 63 |
+
# Enregistrement
|
| 64 |
+
# ──────────────────────────────────────────────────────────────────
|
| 65 |
+
|
| 66 |
+
def register(self, projector: Projector) -> None:
|
| 67 |
+
"""Enregistre un projecteur.
|
| 68 |
+
|
| 69 |
+
Raises
|
| 70 |
+
------
|
| 71 |
+
ProjectorRegistrationError
|
| 72 |
+
Si un projecteur du même nom est déjà enregistré (sauf
|
| 73 |
+
re-enregistrement strict du même objet, toléré pour les
|
| 74 |
+
tests qui re-instancient).
|
| 75 |
+
"""
|
| 76 |
+
if not hasattr(projector, "name"):
|
| 77 |
+
raise ProjectorRegistrationError(
|
| 78 |
+
"register : l'objet n'expose pas d'attribut ``name``."
|
| 79 |
+
)
|
| 80 |
+
if not isinstance(projector, Projector):
|
| 81 |
+
raise ProjectorRegistrationError(
|
| 82 |
+
f"register : {projector!r} ne satisfait pas le protocole "
|
| 83 |
+
"Projector (attributs ``name``, ``source_type``, "
|
| 84 |
+
"``target_type``, méthode ``project``)."
|
| 85 |
+
)
|
| 86 |
+
existing = self._projectors.get(projector.name)
|
| 87 |
+
if existing is not None:
|
| 88 |
+
if existing is projector:
|
| 89 |
+
return # idempotent
|
| 90 |
+
raise ProjectorRegistrationError(
|
| 91 |
+
f"Projecteur {projector.name!r} déjà enregistré avec "
|
| 92 |
+
"une autre instance."
|
| 93 |
+
)
|
| 94 |
+
self._projectors[projector.name] = projector
|
| 95 |
+
|
| 96 |
+
# ──────────────────────────────────────────────────────────────────
|
| 97 |
+
# Lecture
|
| 98 |
+
# ──────────────────────────────────────────────────────────────────
|
| 99 |
+
|
| 100 |
+
def __contains__(self, name: str) -> bool:
|
| 101 |
+
return name in self._projectors
|
| 102 |
+
|
| 103 |
+
def __len__(self) -> int:
|
| 104 |
+
return len(self._projectors)
|
| 105 |
+
|
| 106 |
+
def names(self) -> list[str]:
|
| 107 |
+
"""Liste des noms enregistrés (ordre d'enregistrement)."""
|
| 108 |
+
return list(self._projectors.keys())
|
| 109 |
+
|
| 110 |
+
def get(self, name: str) -> Projector:
|
| 111 |
+
"""Récupère le projecteur par son ``name``.
|
| 112 |
+
|
| 113 |
+
Raises
|
| 114 |
+
------
|
| 115 |
+
ProjectorNotFoundError
|
| 116 |
+
Si le nom n'est pas enregistré.
|
| 117 |
+
"""
|
| 118 |
+
if name not in self._projectors:
|
| 119 |
+
raise ProjectorNotFoundError(
|
| 120 |
+
f"Projecteur {name!r} non enregistré. "
|
| 121 |
+
f"Disponibles : {sorted(self._projectors)}."
|
| 122 |
+
)
|
| 123 |
+
return self._projectors[name]
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
__all__ = [
|
| 127 |
+
"ProjectorRegistry",
|
| 128 |
+
"ProjectorRegistrationError",
|
| 129 |
+
"ProjectorNotFoundError",
|
| 130 |
+
]
|
|
@@ -21,5 +21,14 @@ Reporté post-livraison : ``LayoutView``, ``HallucinationView``,
|
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
from picarones.evaluation.views.base import EvaluationViewExecutor, ViewResult
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
__all__ = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
from __future__ import annotations
|
| 22 |
|
| 23 |
from picarones.evaluation.views.base import EvaluationViewExecutor, ViewResult
|
| 24 |
+
from picarones.evaluation.views.executor import (
|
| 25 |
+
DefaultEvaluationViewExecutor,
|
| 26 |
+
PayloadLoader,
|
| 27 |
+
)
|
| 28 |
|
| 29 |
+
__all__ = [
|
| 30 |
+
"EvaluationViewExecutor",
|
| 31 |
+
"ViewResult",
|
| 32 |
+
"DefaultEvaluationViewExecutor",
|
| 33 |
+
"PayloadLoader",
|
| 34 |
+
]
|
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""``DefaultEvaluationViewExecutor`` — Sprint A14-S13.
|
| 2 |
+
|
| 3 |
+
Implémentation concrète du protocole ``EvaluationViewExecutor`` (S5).
|
| 4 |
+
Orchestration d'une vue d'évaluation sur une paire (candidat, GT) :
|
| 5 |
+
|
| 6 |
+
1. Vérifie que ``candidate.type`` est dans ``view.candidate_types``.
|
| 7 |
+
2. Si ``view.projection`` est défini, récupère le projecteur depuis
|
| 8 |
+
``ProjectorRegistry`` et applique la projection. Capture le
|
| 9 |
+
``ProjectionReport``.
|
| 10 |
+
3. Charge les payloads (texte, ALTO parsé, etc.) via le
|
| 11 |
+
``payload_loader`` injecté au constructeur.
|
| 12 |
+
4. Applique optionnellement un profil de normalisation texte
|
| 13 |
+
(``view.normalization_profile``) sur les payloads texte.
|
| 14 |
+
5. Calcule chaque métrique listée dans ``view.metric_names`` via
|
| 15 |
+
``MetricRegistry``. Une métrique qui lève est enregistrée dans
|
| 16 |
+
``failed_metrics`` au lieu de planter le ViewResult complet.
|
| 17 |
+
6. Retourne un ``ViewResult`` agrégeant tout (metric_values,
|
| 18 |
+
failed_metrics, projection_report, warnings,
|
| 19 |
+
ignored_dimensions).
|
| 20 |
+
|
| 21 |
+
Le ``payload_loader`` est injecté pour découpler l'executor de la
|
| 22 |
+
manière dont les artefacts sont stockés (filesystem, in-memory,
|
| 23 |
+
remote). Le service applicatif (S19) injectera un loader qui sait
|
| 24 |
+
gérer les workspaces sandboxés.
|
| 25 |
+
|
| 26 |
+
Anti-sur-ingénierie
|
| 27 |
+
-------------------
|
| 28 |
+
Pas de cache de payload chargé entre métriques (chaque métrique
|
| 29 |
+
relit l'artefact via le loader). Si un caller veut éviter le coût
|
| 30 |
+
de re-lecture, il instancie un loader qui memo-ize lui-même.
|
| 31 |
+
|
| 32 |
+
Pas de gestion de batch (évaluer N paires en une seule passe). À
|
| 33 |
+
ajouter quand un caller en a concrètement besoin.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
import logging
|
| 39 |
+
from typing import Any, Callable
|
| 40 |
+
|
| 41 |
+
from picarones.domain.artifacts import Artifact
|
| 42 |
+
from picarones.domain.errors import ProjectionError
|
| 43 |
+
from picarones.domain.evaluation_spec import EvaluationView
|
| 44 |
+
from picarones.evaluation.projectors.registry import (
|
| 45 |
+
ProjectorNotFoundError,
|
| 46 |
+
ProjectorRegistry,
|
| 47 |
+
)
|
| 48 |
+
from picarones.evaluation.registry import MetricRegistry, MetricNotFoundError
|
| 49 |
+
from picarones.evaluation.views.base import ViewResult
|
| 50 |
+
|
| 51 |
+
logger = logging.getLogger(__name__)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
#: Type alias : un payload loader prend un Artifact et retourne le
|
| 55 |
+
#: contenu chargé (str pour RAW_TEXT, dict pour ENTITIES, etc.).
|
| 56 |
+
PayloadLoader = Callable[[Artifact], Any]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class DefaultEvaluationViewExecutor:
|
| 60 |
+
"""Implémentation par défaut de ``EvaluationViewExecutor``.
|
| 61 |
+
|
| 62 |
+
Parameters
|
| 63 |
+
----------
|
| 64 |
+
metric_registry:
|
| 65 |
+
``MetricRegistry`` contenant les métriques référencées par
|
| 66 |
+
``view.metric_names``.
|
| 67 |
+
projector_registry:
|
| 68 |
+
``ProjectorRegistry`` contenant les projecteurs référencés
|
| 69 |
+
par ``view.projection.projector_name``.
|
| 70 |
+
payload_loader:
|
| 71 |
+
Callable ``(Artifact) -> Any`` qui charge le contenu d'un
|
| 72 |
+
artefact. Pour les tests, typiquement un dict in-memory.
|
| 73 |
+
En production (S19), un service applicatif qui sait gérer
|
| 74 |
+
les workspaces.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def __init__(
|
| 78 |
+
self,
|
| 79 |
+
metric_registry: MetricRegistry,
|
| 80 |
+
projector_registry: ProjectorRegistry,
|
| 81 |
+
payload_loader: PayloadLoader,
|
| 82 |
+
) -> None:
|
| 83 |
+
if not isinstance(metric_registry, MetricRegistry):
|
| 84 |
+
raise TypeError(
|
| 85 |
+
"metric_registry doit être un MetricRegistry."
|
| 86 |
+
)
|
| 87 |
+
if not isinstance(projector_registry, ProjectorRegistry):
|
| 88 |
+
raise TypeError(
|
| 89 |
+
"projector_registry doit être un ProjectorRegistry."
|
| 90 |
+
)
|
| 91 |
+
if not callable(payload_loader):
|
| 92 |
+
raise TypeError("payload_loader doit être callable.")
|
| 93 |
+
self._metrics = metric_registry
|
| 94 |
+
self._projectors = projector_registry
|
| 95 |
+
self._loader = payload_loader
|
| 96 |
+
|
| 97 |
+
# ──────────────────────────────────────────────────────────────────
|
| 98 |
+
# API publique
|
| 99 |
+
# ──────────────────────────────────────────────────────────────────
|
| 100 |
+
|
| 101 |
+
def evaluate(
|
| 102 |
+
self,
|
| 103 |
+
view: EvaluationView,
|
| 104 |
+
candidate: Artifact,
|
| 105 |
+
ground_truth: Artifact,
|
| 106 |
+
) -> ViewResult:
|
| 107 |
+
"""Évalue la vue sur la paire (candidat, GT).
|
| 108 |
+
|
| 109 |
+
Returns
|
| 110 |
+
-------
|
| 111 |
+
ViewResult
|
| 112 |
+
Toujours retourné, jamais d'exception en sortie normale —
|
| 113 |
+
les erreurs vont dans ``failed_metrics`` ou
|
| 114 |
+
(pour les erreurs de projection) lèvent ``ProjectionError``
|
| 115 |
+
qui est cohérente avec le contrat du S5.
|
| 116 |
+
|
| 117 |
+
Raises
|
| 118 |
+
------
|
| 119 |
+
ProjectionError
|
| 120 |
+
Si la vue exige une projection que le projecteur ne peut
|
| 121 |
+
pas réaliser (ex : type d'entrée incompatible avec le
|
| 122 |
+
projecteur trouvé).
|
| 123 |
+
ValueError
|
| 124 |
+
Si ``candidate.type`` n'est pas dans
|
| 125 |
+
``view.candidate_types``. Le caller (typiquement le
|
| 126 |
+
service applicatif) doit filtrer les pipelines qui ne
|
| 127 |
+
produisent pas le bon type avant d'appeler ``evaluate``.
|
| 128 |
+
"""
|
| 129 |
+
# 1. Vérification du type d'entrée.
|
| 130 |
+
if not view.accepts(candidate.type):
|
| 131 |
+
raise ValueError(
|
| 132 |
+
f"View {view.name!r} n'accepte pas l'artefact "
|
| 133 |
+
f"{candidate.id!r} (type {candidate.type.value!r}). "
|
| 134 |
+
f"Types acceptés : "
|
| 135 |
+
f"{sorted(t.value for t in view.candidate_types)}."
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# 2. Projection (optionnelle).
|
| 139 |
+
effective_candidate = candidate
|
| 140 |
+
projection_report = None
|
| 141 |
+
if view.projection is not None and not view.projection.is_identity:
|
| 142 |
+
try:
|
| 143 |
+
projector = self._projectors.get(
|
| 144 |
+
view.projection.projector_name,
|
| 145 |
+
)
|
| 146 |
+
except ProjectorNotFoundError as exc:
|
| 147 |
+
raise ProjectionError(
|
| 148 |
+
f"View {view.name!r} référence le projecteur "
|
| 149 |
+
f"{view.projection.projector_name!r} introuvable "
|
| 150 |
+
"dans le ProjectorRegistry."
|
| 151 |
+
) from exc
|
| 152 |
+
try:
|
| 153 |
+
effective_candidate, projection_report = projector.project(
|
| 154 |
+
candidate, dict(view.projection.params),
|
| 155 |
+
)
|
| 156 |
+
except ProjectionError:
|
| 157 |
+
raise
|
| 158 |
+
except Exception as exc: # noqa: BLE001
|
| 159 |
+
raise ProjectionError(
|
| 160 |
+
f"Projecteur {view.projection.projector_name!r} a "
|
| 161 |
+
f"levé sur l'artefact {candidate.id!r} : {exc}"
|
| 162 |
+
) from exc
|
| 163 |
+
|
| 164 |
+
# 3. Chargement des payloads.
|
| 165 |
+
# Échec de chargement = ViewResult avec une erreur globale
|
| 166 |
+
# (pas de failed_metric par métrique — l'erreur est en amont).
|
| 167 |
+
try:
|
| 168 |
+
cand_payload = self._loader(effective_candidate)
|
| 169 |
+
except Exception as exc: # noqa: BLE001
|
| 170 |
+
return self._failed_view_result(
|
| 171 |
+
view=view,
|
| 172 |
+
candidate=candidate,
|
| 173 |
+
ground_truth=ground_truth,
|
| 174 |
+
projection_report=projection_report,
|
| 175 |
+
global_error=(
|
| 176 |
+
f"payload_loader a échoué sur le candidat "
|
| 177 |
+
f"{effective_candidate.id!r} : {exc}"
|
| 178 |
+
),
|
| 179 |
+
)
|
| 180 |
+
try:
|
| 181 |
+
gt_payload = self._loader(ground_truth)
|
| 182 |
+
except Exception as exc: # noqa: BLE001
|
| 183 |
+
return self._failed_view_result(
|
| 184 |
+
view=view,
|
| 185 |
+
candidate=candidate,
|
| 186 |
+
ground_truth=ground_truth,
|
| 187 |
+
projection_report=projection_report,
|
| 188 |
+
global_error=(
|
| 189 |
+
f"payload_loader a échoué sur la GT "
|
| 190 |
+
f"{ground_truth.id!r} : {exc}"
|
| 191 |
+
),
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
# 4. Normalisation texte (optionnelle).
|
| 195 |
+
if view.normalization_profile is not None:
|
| 196 |
+
cand_payload, gt_payload = self._apply_normalization(
|
| 197 |
+
view.normalization_profile, cand_payload, gt_payload,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
# 5. Calcul des métriques. Une métrique qui lève va dans
|
| 201 |
+
# failed_metrics. Une métrique non enregistrée va dans
|
| 202 |
+
# failed_metrics avec un message explicite.
|
| 203 |
+
metric_values: dict[str, Any] = {}
|
| 204 |
+
failed_metrics: dict[str, str] = {}
|
| 205 |
+
for name in view.metric_names:
|
| 206 |
+
try:
|
| 207 |
+
value = self._metrics.compute(name, gt_payload, cand_payload)
|
| 208 |
+
metric_values[name] = value
|
| 209 |
+
except MetricNotFoundError as exc:
|
| 210 |
+
failed_metrics[name] = (
|
| 211 |
+
f"métrique non enregistrée dans le MetricRegistry : "
|
| 212 |
+
f"{exc}"
|
| 213 |
+
)
|
| 214 |
+
except Exception as exc: # noqa: BLE001
|
| 215 |
+
failed_metrics[name] = (
|
| 216 |
+
f"{type(exc).__name__}: {exc}"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# 6. Construction du ViewResult.
|
| 220 |
+
warnings = tuple(view.warnings)
|
| 221 |
+
ignored = tuple(view.ignored_dimensions)
|
| 222 |
+
if projection_report is not None:
|
| 223 |
+
warnings = warnings + tuple(projection_report.warnings)
|
| 224 |
+
# Déduplique les ignored_dimensions tout en préservant l'ordre.
|
| 225 |
+
seen: set[str] = set(ignored)
|
| 226 |
+
extra = tuple(
|
| 227 |
+
d for d in projection_report.ignored_dimensions
|
| 228 |
+
if d not in seen
|
| 229 |
+
)
|
| 230 |
+
ignored = ignored + extra
|
| 231 |
+
|
| 232 |
+
return ViewResult(
|
| 233 |
+
view_name=view.name,
|
| 234 |
+
candidate_artifact_id=candidate.id,
|
| 235 |
+
ground_truth_artifact_id=ground_truth.id,
|
| 236 |
+
metric_values=metric_values,
|
| 237 |
+
failed_metrics=failed_metrics,
|
| 238 |
+
projection_report=projection_report,
|
| 239 |
+
warnings=warnings,
|
| 240 |
+
ignored_dimensions=ignored,
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# ──────────────────────────────────────────────────────────────────
|
| 244 |
+
# Helpers internes
|
| 245 |
+
# ──────────────────────────────────────────────────────────────────
|
| 246 |
+
|
| 247 |
+
@staticmethod
|
| 248 |
+
def _apply_normalization(
|
| 249 |
+
profile_name: str,
|
| 250 |
+
cand_payload: Any,
|
| 251 |
+
gt_payload: Any,
|
| 252 |
+
) -> tuple[Any, Any]:
|
| 253 |
+
"""Applique un profil de normalisation aux deux payloads.
|
| 254 |
+
|
| 255 |
+
Si l'un des deux n'est pas une string, on saute la
|
| 256 |
+
normalisation pour ce payload (cas typique : ALTO non encore
|
| 257 |
+
projeté en texte → on laisse passer).
|
| 258 |
+
"""
|
| 259 |
+
from picarones.formats.text.normalization import get_builtin_profile
|
| 260 |
+
try:
|
| 261 |
+
profile = get_builtin_profile(profile_name)
|
| 262 |
+
except Exception as exc: # noqa: BLE001
|
| 263 |
+
logger.warning(
|
| 264 |
+
"[view_executor] profil normalisation %r introuvable : %s",
|
| 265 |
+
profile_name, exc,
|
| 266 |
+
)
|
| 267 |
+
return cand_payload, gt_payload
|
| 268 |
+
normalized_cand = (
|
| 269 |
+
profile.normalize(cand_payload)
|
| 270 |
+
if isinstance(cand_payload, str)
|
| 271 |
+
else cand_payload
|
| 272 |
+
)
|
| 273 |
+
normalized_gt = (
|
| 274 |
+
profile.normalize(gt_payload)
|
| 275 |
+
if isinstance(gt_payload, str)
|
| 276 |
+
else gt_payload
|
| 277 |
+
)
|
| 278 |
+
return normalized_cand, normalized_gt
|
| 279 |
+
|
| 280 |
+
@staticmethod
|
| 281 |
+
def _failed_view_result(
|
| 282 |
+
*,
|
| 283 |
+
view: EvaluationView,
|
| 284 |
+
candidate: Artifact,
|
| 285 |
+
ground_truth: Artifact,
|
| 286 |
+
projection_report: Any,
|
| 287 |
+
global_error: str,
|
| 288 |
+
) -> ViewResult:
|
| 289 |
+
"""Construit un ``ViewResult`` quand le payload n'a pas pu
|
| 290 |
+
être chargé. Toutes les métriques sont marquées en échec
|
| 291 |
+
avec le même message d'erreur global."""
|
| 292 |
+
failed = {name: global_error for name in view.metric_names}
|
| 293 |
+
return ViewResult(
|
| 294 |
+
view_name=view.name,
|
| 295 |
+
candidate_artifact_id=candidate.id,
|
| 296 |
+
ground_truth_artifact_id=ground_truth.id,
|
| 297 |
+
metric_values={},
|
| 298 |
+
failed_metrics=failed,
|
| 299 |
+
projection_report=projection_report,
|
| 300 |
+
warnings=tuple(view.warnings) + (global_error,),
|
| 301 |
+
ignored_dimensions=tuple(view.ignored_dimensions),
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
__all__ = [
|
| 306 |
+
"DefaultEvaluationViewExecutor",
|
| 307 |
+
"PayloadLoader",
|
| 308 |
+
]
|
|
@@ -25,7 +25,6 @@ Anti-sur-ingénierie
|
|
| 25 |
from __future__ import annotations
|
| 26 |
|
| 27 |
from picarones.formats.alto.parser import AltoParseError, parse_alto
|
| 28 |
-
from picarones.formats.alto.projector import AltoToText, alto_document_to_text
|
| 29 |
from picarones.formats.alto.types import (
|
| 30 |
AltoBBox,
|
| 31 |
AltoDocument,
|
|
@@ -36,6 +35,15 @@ from picarones.formats.alto.types import (
|
|
| 36 |
)
|
| 37 |
from picarones.formats.alto.writer import write_alto
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
__all__ = [
|
| 40 |
# Types
|
| 41 |
"AltoBBox",
|
|
@@ -48,7 +56,4 @@ __all__ = [
|
|
| 48 |
"parse_alto",
|
| 49 |
"AltoParseError",
|
| 50 |
"write_alto",
|
| 51 |
-
# Projector
|
| 52 |
-
"alto_document_to_text",
|
| 53 |
-
"AltoToText",
|
| 54 |
]
|
|
|
|
| 25 |
from __future__ import annotations
|
| 26 |
|
| 27 |
from picarones.formats.alto.parser import AltoParseError, parse_alto
|
|
|
|
| 28 |
from picarones.formats.alto.types import (
|
| 29 |
AltoBBox,
|
| 30 |
AltoDocument,
|
|
|
|
| 35 |
)
|
| 36 |
from picarones.formats.alto.writer import write_alto
|
| 37 |
|
| 38 |
+
# S13 — les projecteurs ``alto_document_to_text`` et ``AltoToText``
|
| 39 |
+
# vivent désormais dans ``picarones.evaluation.projectors.alto``
|
| 40 |
+
# (la projection est conceptuellement un composant d'évaluation,
|
| 41 |
+
# pas un format). Importer depuis le nouveau chemin :
|
| 42 |
+
#
|
| 43 |
+
# from picarones.evaluation.projectors import (
|
| 44 |
+
# AltoToText, alto_document_to_text,
|
| 45 |
+
# )
|
| 46 |
+
|
| 47 |
__all__ = [
|
| 48 |
# Types
|
| 49 |
"AltoBBox",
|
|
|
|
| 56 |
"parse_alto",
|
| 57 |
"AltoParseError",
|
| 58 |
"write_alto",
|
|
|
|
|
|
|
|
|
|
| 59 |
]
|
|
@@ -16,7 +16,6 @@ est plus rare que pour ALTO).
|
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
from picarones.formats.pagexml.parser import PageParseError, parse_pagexml
|
| 19 |
-
from picarones.formats.pagexml.projector import PageToText, page_document_to_text
|
| 20 |
from picarones.formats.pagexml.types import (
|
| 21 |
PageDocument,
|
| 22 |
PagePage,
|
|
@@ -24,6 +23,9 @@ from picarones.formats.pagexml.types import (
|
|
| 24 |
PageTextRegion,
|
| 25 |
)
|
| 26 |
|
|
|
|
|
|
|
|
|
|
| 27 |
__all__ = [
|
| 28 |
"PageTextLine",
|
| 29 |
"PageTextRegion",
|
|
@@ -31,6 +33,4 @@ __all__ = [
|
|
| 31 |
"PageDocument",
|
| 32 |
"parse_pagexml",
|
| 33 |
"PageParseError",
|
| 34 |
-
"page_document_to_text",
|
| 35 |
-
"PageToText",
|
| 36 |
]
|
|
|
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
from picarones.formats.pagexml.parser import PageParseError, parse_pagexml
|
|
|
|
| 19 |
from picarones.formats.pagexml.types import (
|
| 20 |
PageDocument,
|
| 21 |
PagePage,
|
|
|
|
| 23 |
PageTextRegion,
|
| 24 |
)
|
| 25 |
|
| 26 |
+
# S13 — les projecteurs vivent désormais dans
|
| 27 |
+
# ``picarones.evaluation.projectors.pagexml``.
|
| 28 |
+
|
| 29 |
__all__ = [
|
| 30 |
"PageTextLine",
|
| 31 |
"PageTextRegion",
|
|
|
|
| 33 |
"PageDocument",
|
| 34 |
"parse_pagexml",
|
| 35 |
"PageParseError",
|
|
|
|
|
|
|
| 36 |
]
|
|
@@ -64,9 +64,13 @@ PICARONES_ROOT = REPO_ROOT / "picarones"
|
|
| 64 |
#: avant** la sienne (i.e. plus internes), mais jamais l'inverse.
|
| 65 |
LAYER_ORDER: tuple[str, ...] = (
|
| 66 |
"domain",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
"evaluation",
|
| 68 |
"pipeline",
|
| 69 |
-
"formats",
|
| 70 |
"adapters",
|
| 71 |
"app",
|
| 72 |
"reports_v2",
|
|
|
|
| 64 |
#: avant** la sienne (i.e. plus internes), mais jamais l'inverse.
|
| 65 |
LAYER_ORDER: tuple[str, ...] = (
|
| 66 |
"domain",
|
| 67 |
+
"formats", # S13 — re-ordonné : parsers/normalization sont des
|
| 68 |
+
# utilitaires bas niveau qu'``evaluation`` consomme
|
| 69 |
+
# (ex : ``DefaultEvaluationViewExecutor`` charge un
|
| 70 |
+
# profil de normalisation depuis
|
| 71 |
+
# ``formats.text.normalization``).
|
| 72 |
"evaluation",
|
| 73 |
"pipeline",
|
|
|
|
| 74 |
"adapters",
|
| 75 |
"app",
|
| 76 |
"reports_v2",
|
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sprint A14-S13 — ``DefaultEvaluationViewExecutor``.
|
| 2 |
+
|
| 3 |
+
Tests d'orchestration : la vue + ses dépendances (registries +
|
| 4 |
+
payload loader) sur 10+ cas couvrant les chemins critiques.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import pytest
|
| 10 |
+
|
| 11 |
+
from picarones.domain import (
|
| 12 |
+
Artifact,
|
| 13 |
+
ArtifactType,
|
| 14 |
+
EvaluationView,
|
| 15 |
+
MetricSpec,
|
| 16 |
+
ProjectionError,
|
| 17 |
+
ProjectionSpec,
|
| 18 |
+
)
|
| 19 |
+
from picarones.evaluation.projectors import (
|
| 20 |
+
ProjectionReport,
|
| 21 |
+
ProjectorRegistry,
|
| 22 |
+
ProjectorRegistrationError,
|
| 23 |
+
ProjectorNotFoundError,
|
| 24 |
+
)
|
| 25 |
+
from picarones.evaluation.registry import MetricRegistry
|
| 26 |
+
from picarones.evaluation.views import (
|
| 27 |
+
DefaultEvaluationViewExecutor,
|
| 28 |
+
ViewResult,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 33 |
+
# Stubs réutilisables
|
| 34 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class _StubProjector:
|
| 38 |
+
"""Projecteur ALTO → texte simple pour les tests."""
|
| 39 |
+
|
| 40 |
+
name = "stub_alto_to_text"
|
| 41 |
+
source_type = ArtifactType.ALTO_XML
|
| 42 |
+
target_type = ArtifactType.RAW_TEXT
|
| 43 |
+
|
| 44 |
+
def __init__(self, output_payload: str = "projected text") -> None:
|
| 45 |
+
self.output_payload = output_payload
|
| 46 |
+
|
| 47 |
+
def project(self, artifact, params):
|
| 48 |
+
target = Artifact(
|
| 49 |
+
id=f"{artifact.id}:projected",
|
| 50 |
+
document_id=artifact.document_id,
|
| 51 |
+
type=self.target_type,
|
| 52 |
+
)
|
| 53 |
+
report = ProjectionReport(
|
| 54 |
+
source_artifact_id=artifact.id,
|
| 55 |
+
source_type=self.source_type,
|
| 56 |
+
target_type=self.target_type,
|
| 57 |
+
projector_name=self.name,
|
| 58 |
+
lossy=True,
|
| 59 |
+
ignored_dimensions=("geometry", "blocks"),
|
| 60 |
+
warnings=("ordre de lecture deviné",),
|
| 61 |
+
)
|
| 62 |
+
return target, report
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _build_executor(
|
| 66 |
+
payloads: dict[str, object],
|
| 67 |
+
*,
|
| 68 |
+
register_projector: bool = True,
|
| 69 |
+
extra_metrics: dict[str, object] | None = None,
|
| 70 |
+
) -> DefaultEvaluationViewExecutor:
|
| 71 |
+
metrics = MetricRegistry()
|
| 72 |
+
metrics.register(
|
| 73 |
+
MetricSpec(
|
| 74 |
+
name="cer",
|
| 75 |
+
input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
|
| 76 |
+
),
|
| 77 |
+
lambda gt, hyp: 0.0 if gt == hyp else (
|
| 78 |
+
0.5 if isinstance(gt, str) and isinstance(hyp, str) and len(gt) == len(hyp)
|
| 79 |
+
else 1.0
|
| 80 |
+
),
|
| 81 |
+
)
|
| 82 |
+
metrics.register(
|
| 83 |
+
MetricSpec(
|
| 84 |
+
name="wer",
|
| 85 |
+
input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
|
| 86 |
+
),
|
| 87 |
+
lambda gt, hyp: 0.0 if gt == hyp else 0.5,
|
| 88 |
+
)
|
| 89 |
+
if extra_metrics:
|
| 90 |
+
for name, fn in extra_metrics.items():
|
| 91 |
+
metrics.register(
|
| 92 |
+
MetricSpec(
|
| 93 |
+
name=name,
|
| 94 |
+
input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
|
| 95 |
+
),
|
| 96 |
+
fn,
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
projectors = ProjectorRegistry()
|
| 100 |
+
if register_projector:
|
| 101 |
+
projectors.register(_StubProjector())
|
| 102 |
+
|
| 103 |
+
def loader(artifact: Artifact):
|
| 104 |
+
if artifact.id not in payloads:
|
| 105 |
+
raise KeyError(f"payload manquant : {artifact.id}")
|
| 106 |
+
return payloads[artifact.id]
|
| 107 |
+
|
| 108 |
+
return DefaultEvaluationViewExecutor(metrics, projectors, loader)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _text_view(
|
| 112 |
+
*,
|
| 113 |
+
name: str = "text_final",
|
| 114 |
+
candidate_types: frozenset = frozenset({
|
| 115 |
+
ArtifactType.RAW_TEXT,
|
| 116 |
+
ArtifactType.CORRECTED_TEXT,
|
| 117 |
+
ArtifactType.ALTO_XML,
|
| 118 |
+
}),
|
| 119 |
+
projection: ProjectionSpec | None = None,
|
| 120 |
+
normalization_profile: str | None = None,
|
| 121 |
+
metric_names: tuple[str, ...] = ("cer",),
|
| 122 |
+
ignored_dimensions: tuple[str, ...] = (),
|
| 123 |
+
warnings: tuple[str, ...] = (),
|
| 124 |
+
) -> EvaluationView:
|
| 125 |
+
return EvaluationView(
|
| 126 |
+
name=name,
|
| 127 |
+
candidate_types=candidate_types,
|
| 128 |
+
projection=projection,
|
| 129 |
+
normalization_profile=normalization_profile,
|
| 130 |
+
metric_names=metric_names,
|
| 131 |
+
ignored_dimensions=ignored_dimensions,
|
| 132 |
+
warnings=warnings,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 137 |
+
# 10 cas d'évaluation
|
| 138 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class TestEvaluator:
|
| 142 |
+
|
| 143 |
+
def test_text_direct_no_projection(self) -> None:
|
| 144 |
+
"""Cas 1 — RAW_TEXT direct, pas de projection."""
|
| 145 |
+
payloads = {"cand": "hello", "gt": "hello"}
|
| 146 |
+
executor = _build_executor(payloads)
|
| 147 |
+
view = _text_view(metric_names=("cer", "wer"))
|
| 148 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 149 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 150 |
+
result = executor.evaluate(view, cand, gt)
|
| 151 |
+
assert result.metric_values["cer"] == 0.0
|
| 152 |
+
assert result.metric_values["wer"] == 0.0
|
| 153 |
+
assert result.projection_report is None
|
| 154 |
+
assert result.failed_metrics == {}
|
| 155 |
+
|
| 156 |
+
def test_text_direct_with_difference(self) -> None:
|
| 157 |
+
"""Cas 2 — RAW_TEXT, candidat différent de la GT."""
|
| 158 |
+
payloads = {"cand": "world", "gt": "hello"}
|
| 159 |
+
executor = _build_executor(payloads)
|
| 160 |
+
view = _text_view()
|
| 161 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 162 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 163 |
+
result = executor.evaluate(view, cand, gt)
|
| 164 |
+
assert result.metric_values["cer"] > 0
|
| 165 |
+
|
| 166 |
+
def test_alto_to_text_via_projection(self) -> None:
|
| 167 |
+
"""Cas 3 — ALTO_XML projeté en RAW_TEXT, projection_report présent."""
|
| 168 |
+
payloads = {
|
| 169 |
+
"alto:projected": "projected text",
|
| 170 |
+
"gt": "projected text",
|
| 171 |
+
}
|
| 172 |
+
executor = _build_executor(payloads)
|
| 173 |
+
view = _text_view(
|
| 174 |
+
projection=ProjectionSpec(
|
| 175 |
+
source_type=ArtifactType.ALTO_XML,
|
| 176 |
+
target_type=ArtifactType.RAW_TEXT,
|
| 177 |
+
projector_name="stub_alto_to_text",
|
| 178 |
+
),
|
| 179 |
+
)
|
| 180 |
+
cand = Artifact(id="alto", document_id="d", type=ArtifactType.ALTO_XML)
|
| 181 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 182 |
+
result = executor.evaluate(view, cand, gt)
|
| 183 |
+
assert result.projection_report is not None
|
| 184 |
+
assert result.projection_report.projector_name == "stub_alto_to_text"
|
| 185 |
+
assert "geometry" in result.ignored_dimensions
|
| 186 |
+
assert "ordre de lecture deviné" in result.warnings
|
| 187 |
+
assert result.metric_values["cer"] == 0.0
|
| 188 |
+
|
| 189 |
+
def test_view_rejects_wrong_artifact_type(self) -> None:
|
| 190 |
+
"""Cas 4 — la vue n'accepte pas IMAGE → ValueError."""
|
| 191 |
+
payloads = {}
|
| 192 |
+
executor = _build_executor(payloads)
|
| 193 |
+
view = _text_view(
|
| 194 |
+
candidate_types=frozenset({ArtifactType.RAW_TEXT}),
|
| 195 |
+
)
|
| 196 |
+
cand = Artifact(id="x", document_id="d", type=ArtifactType.IMAGE)
|
| 197 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 198 |
+
with pytest.raises(ValueError, match="n'accepte pas"):
|
| 199 |
+
executor.evaluate(view, cand, gt)
|
| 200 |
+
|
| 201 |
+
def test_unknown_projector_raises_projection_error(self) -> None:
|
| 202 |
+
"""Cas 5 — la vue référence un projecteur non enregistré."""
|
| 203 |
+
payloads = {"cand": "x", "gt": "x"}
|
| 204 |
+
executor = _build_executor(payloads, register_projector=False)
|
| 205 |
+
view = _text_view(
|
| 206 |
+
projection=ProjectionSpec(
|
| 207 |
+
source_type=ArtifactType.ALTO_XML,
|
| 208 |
+
target_type=ArtifactType.RAW_TEXT,
|
| 209 |
+
projector_name="nonexistent",
|
| 210 |
+
),
|
| 211 |
+
)
|
| 212 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.ALTO_XML)
|
| 213 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 214 |
+
with pytest.raises(ProjectionError, match="introuvable"):
|
| 215 |
+
executor.evaluate(view, cand, gt)
|
| 216 |
+
|
| 217 |
+
def test_projector_that_raises_wraps_in_projection_error(self) -> None:
|
| 218 |
+
"""Cas 6 — le projecteur lève une exception interne."""
|
| 219 |
+
class _CrashingProjector:
|
| 220 |
+
name = "crash"
|
| 221 |
+
source_type = ArtifactType.ALTO_XML
|
| 222 |
+
target_type = ArtifactType.RAW_TEXT
|
| 223 |
+
def project(self, artifact, params):
|
| 224 |
+
raise RuntimeError("boom interne")
|
| 225 |
+
|
| 226 |
+
metrics = MetricRegistry()
|
| 227 |
+
projectors = ProjectorRegistry()
|
| 228 |
+
projectors.register(_CrashingProjector())
|
| 229 |
+
executor = DefaultEvaluationViewExecutor(
|
| 230 |
+
metrics, projectors, lambda a: None,
|
| 231 |
+
)
|
| 232 |
+
view = _text_view(
|
| 233 |
+
projection=ProjectionSpec(
|
| 234 |
+
source_type=ArtifactType.ALTO_XML,
|
| 235 |
+
target_type=ArtifactType.RAW_TEXT,
|
| 236 |
+
projector_name="crash",
|
| 237 |
+
),
|
| 238 |
+
metric_names=(),
|
| 239 |
+
)
|
| 240 |
+
cand = Artifact(id="c", document_id="d", type=ArtifactType.ALTO_XML)
|
| 241 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 242 |
+
with pytest.raises(ProjectionError, match="boom interne"):
|
| 243 |
+
executor.evaluate(view, cand, gt)
|
| 244 |
+
|
| 245 |
+
def test_metric_that_raises_goes_to_failed_metrics(self) -> None:
|
| 246 |
+
"""Cas 7 — une métrique qui lève → failed_metrics, pas plante."""
|
| 247 |
+
def _broken(gt, hyp):
|
| 248 |
+
raise ValueError("métrique cassée")
|
| 249 |
+
payloads = {"cand": "x", "gt": "x"}
|
| 250 |
+
executor = _build_executor(
|
| 251 |
+
payloads,
|
| 252 |
+
extra_metrics={"broken": _broken},
|
| 253 |
+
)
|
| 254 |
+
view = _text_view(metric_names=("cer", "broken", "wer"))
|
| 255 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 256 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 257 |
+
result = executor.evaluate(view, cand, gt)
|
| 258 |
+
assert "cer" in result.metric_values
|
| 259 |
+
assert "wer" in result.metric_values
|
| 260 |
+
assert "broken" in result.failed_metrics
|
| 261 |
+
assert "métrique cassée" in result.failed_metrics["broken"]
|
| 262 |
+
|
| 263 |
+
def test_unknown_metric_goes_to_failed_metrics(self) -> None:
|
| 264 |
+
"""Cas 8 — une métrique non enregistrée → failed_metrics."""
|
| 265 |
+
payloads = {"cand": "x", "gt": "x"}
|
| 266 |
+
executor = _build_executor(payloads)
|
| 267 |
+
view = _text_view(metric_names=("cer", "nonexistent_metric"))
|
| 268 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 269 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 270 |
+
result = executor.evaluate(view, cand, gt)
|
| 271 |
+
assert "cer" in result.metric_values
|
| 272 |
+
assert "nonexistent_metric" in result.failed_metrics
|
| 273 |
+
assert "non enregistrée" in result.failed_metrics["nonexistent_metric"]
|
| 274 |
+
|
| 275 |
+
def test_normalization_profile_applied(self) -> None:
|
| 276 |
+
"""Cas 9 — vue avec normalization_profile applique la
|
| 277 |
+
normalisation aux deux payloads."""
|
| 278 |
+
# Avec medieval_french : ſ → s, u → v
|
| 279 |
+
payloads = {"cand": "afpre", "gt": "aſpre"}
|
| 280 |
+
executor = _build_executor(payloads)
|
| 281 |
+
view = _text_view(normalization_profile="medieval_french")
|
| 282 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 283 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 284 |
+
result = executor.evaluate(view, cand, gt)
|
| 285 |
+
# Après normalisation, les deux deviennent "aspre" (cer stub
|
| 286 |
+
# retourne 0.5 pour len égal, 0.0 pour égalité stricte).
|
| 287 |
+
# On vérifie au moins que la métrique a été calculée.
|
| 288 |
+
assert "cer" in result.metric_values
|
| 289 |
+
|
| 290 |
+
def test_payload_loader_failure_blocks_all_metrics(self) -> None:
|
| 291 |
+
"""Cas 10 — le loader plante → toutes les métriques sont
|
| 292 |
+
marquées en échec global."""
|
| 293 |
+
# Loader plante systématiquement.
|
| 294 |
+
metrics = MetricRegistry()
|
| 295 |
+
metrics.register(
|
| 296 |
+
MetricSpec(
|
| 297 |
+
name="cer",
|
| 298 |
+
input_types=(ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT),
|
| 299 |
+
),
|
| 300 |
+
lambda r, h: 0.0,
|
| 301 |
+
)
|
| 302 |
+
projectors = ProjectorRegistry()
|
| 303 |
+
|
| 304 |
+
def _bad_loader(artifact):
|
| 305 |
+
raise FileNotFoundError(f"missing file for {artifact.id}")
|
| 306 |
+
|
| 307 |
+
executor = DefaultEvaluationViewExecutor(metrics, projectors, _bad_loader)
|
| 308 |
+
view = _text_view(metric_names=("cer",))
|
| 309 |
+
cand = Artifact(id="cand", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 310 |
+
gt = Artifact(id="gt", document_id="d", type=ArtifactType.RAW_TEXT)
|
| 311 |
+
result = executor.evaluate(view, cand, gt)
|
| 312 |
+
assert result.metric_values == {}
|
| 313 |
+
assert "cer" in result.failed_metrics
|
| 314 |
+
assert "payload_loader a échoué" in result.failed_metrics["cer"]
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 318 |
+
# Constructor validation
|
| 319 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
class TestConstructor:
|
| 323 |
+
def test_rejects_non_metric_registry(self) -> None:
|
| 324 |
+
with pytest.raises(TypeError, match="metric_registry"):
|
| 325 |
+
DefaultEvaluationViewExecutor(
|
| 326 |
+
"not a registry", ProjectorRegistry(), lambda a: None, # type: ignore[arg-type]
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
def test_rejects_non_projector_registry(self) -> None:
|
| 330 |
+
with pytest.raises(TypeError, match="projector_registry"):
|
| 331 |
+
DefaultEvaluationViewExecutor(
|
| 332 |
+
MetricRegistry(), "nope", lambda a: None, # type: ignore[arg-type]
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
def test_rejects_non_callable_loader(self) -> None:
|
| 336 |
+
with pytest.raises(TypeError, match="callable"):
|
| 337 |
+
DefaultEvaluationViewExecutor(
|
| 338 |
+
MetricRegistry(), ProjectorRegistry(), "not_callable", # type: ignore[arg-type]
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 343 |
+
# ProjectorRegistry — tests directs
|
| 344 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
class TestProjectorRegistry:
|
| 348 |
+
def test_register_and_get(self) -> None:
|
| 349 |
+
reg = ProjectorRegistry()
|
| 350 |
+
p = _StubProjector()
|
| 351 |
+
reg.register(p)
|
| 352 |
+
assert "stub_alto_to_text" in reg
|
| 353 |
+
assert reg.get("stub_alto_to_text") is p
|
| 354 |
+
|
| 355 |
+
def test_register_non_protocol_raises(self) -> None:
|
| 356 |
+
reg = ProjectorRegistry()
|
| 357 |
+
class _NotAProjector:
|
| 358 |
+
pass
|
| 359 |
+
with pytest.raises(ProjectorRegistrationError):
|
| 360 |
+
reg.register(_NotAProjector()) # type: ignore[arg-type]
|
| 361 |
+
|
| 362 |
+
def test_idempotent_re_registration(self) -> None:
|
| 363 |
+
reg = ProjectorRegistry()
|
| 364 |
+
p = _StubProjector()
|
| 365 |
+
reg.register(p)
|
| 366 |
+
reg.register(p) # ne lève pas
|
| 367 |
+
assert len(reg) == 1
|
| 368 |
+
|
| 369 |
+
def test_get_unknown_raises(self) -> None:
|
| 370 |
+
reg = ProjectorRegistry()
|
| 371 |
+
with pytest.raises(ProjectorNotFoundError):
|
| 372 |
+
reg.get("missing")
|
| 373 |
+
|
| 374 |
+
def test_two_registries_independent(self) -> None:
|
| 375 |
+
a = ProjectorRegistry()
|
| 376 |
+
b = ProjectorRegistry()
|
| 377 |
+
a.register(_StubProjector())
|
| 378 |
+
assert "stub_alto_to_text" in a
|
| 379 |
+
assert "stub_alto_to_text" not in b
|
|
@@ -15,6 +15,7 @@ import pytest
|
|
| 15 |
|
| 16 |
from picarones.domain import Artifact, ArtifactType
|
| 17 |
from picarones.domain.errors import ProjectionError
|
|
|
|
| 18 |
from picarones.formats.alto import (
|
| 19 |
AltoBBox,
|
| 20 |
AltoDocument,
|
|
@@ -23,8 +24,6 @@ from picarones.formats.alto import (
|
|
| 23 |
AltoParseError,
|
| 24 |
AltoString,
|
| 25 |
AltoTextBlock,
|
| 26 |
-
AltoToText,
|
| 27 |
-
alto_document_to_text,
|
| 28 |
parse_alto,
|
| 29 |
write_alto,
|
| 30 |
)
|
|
|
|
| 15 |
|
| 16 |
from picarones.domain import Artifact, ArtifactType
|
| 17 |
from picarones.domain.errors import ProjectionError
|
| 18 |
+
from picarones.evaluation.projectors import AltoToText, alto_document_to_text
|
| 19 |
from picarones.formats.alto import (
|
| 20 |
AltoBBox,
|
| 21 |
AltoDocument,
|
|
|
|
| 24 |
AltoParseError,
|
| 25 |
AltoString,
|
| 26 |
AltoTextBlock,
|
|
|
|
|
|
|
| 27 |
parse_alto,
|
| 28 |
write_alto,
|
| 29 |
)
|
|
@@ -6,14 +6,13 @@ import pytest
|
|
| 6 |
|
| 7 |
from picarones.domain import Artifact, ArtifactType
|
| 8 |
from picarones.domain.errors import ProjectionError
|
|
|
|
| 9 |
from picarones.formats.pagexml import (
|
| 10 |
PageDocument,
|
| 11 |
PageParseError,
|
| 12 |
PagePage,
|
| 13 |
PageTextLine,
|
| 14 |
PageTextRegion,
|
| 15 |
-
PageToText,
|
| 16 |
-
page_document_to_text,
|
| 17 |
parse_pagexml,
|
| 18 |
)
|
| 19 |
|
|
|
|
| 6 |
|
| 7 |
from picarones.domain import Artifact, ArtifactType
|
| 8 |
from picarones.domain.errors import ProjectionError
|
| 9 |
+
from picarones.evaluation.projectors import PageToText, page_document_to_text
|
| 10 |
from picarones.formats.pagexml import (
|
| 11 |
PageDocument,
|
| 12 |
PageParseError,
|
| 13 |
PagePage,
|
| 14 |
PageTextLine,
|
| 15 |
PageTextRegion,
|
|
|
|
|
|
|
| 16 |
parse_pagexml,
|
| 17 |
)
|
| 18 |
|