Spaces:
Sleeping
Sleeping
Claude
feat(evaluation): Sprint A14-S14 — TextView (vue canonique 1) + projection multi-types
32d3f14 unverified | """``MetricSpec``, ``EvaluationView``, ``EvaluationSpec`` — Sprint A14-S5. | |
| Cœur de la valeur ajoutée du rewrite : **comparer librement des | |
| pipelines hétérogènes en projetant leurs sorties vers une vue | |
| d'évaluation explicite**. L'utilisateur ne compare jamais | |
| directement un OCR brut et une sortie ALTO reconstruite ; il | |
| compare leur projection dans une vue commune (texte, ALTO, | |
| recherchabilité, ...) et le rapport explicite ce que la vue | |
| ignore. | |
| Trois couches de contrat : | |
| - ``MetricSpec`` — déclare une métrique (nom + signature de types). | |
| - ``EvaluationView`` — déclare une vue (sélecteur de candidats + | |
| projection optionnelle + liste de métriques + dimensions | |
| ignorées). | |
| - ``EvaluationSpec`` — container de N vues qu'un benchmark applique. | |
| Différence avec l'existant ``core/metric_registry.py:MetricSpec`` | |
| ----------------------------------------------------------------- | |
| L'ancien ``MetricSpec`` (Sprint 34) porte un ``func: Callable``, | |
| un singleton global ``_METRIC_REGISTRY``, et un décorateur | |
| ``@register_metric`` qui s'exécute par effet de bord d'import. | |
| C'est exactement l'anti-pattern que le rewrite cherche à bannir | |
| (cf. ``BACKLOG_POST_LIVRAISON.md`` §2.4 + tests d'architecture du | |
| S3). | |
| Le nouveau ``MetricSpec`` est purement **déclaratif** : pas de | |
| callable. L'association ``MetricSpec ↔ Callable`` se fait | |
| explicitement dans ``picarones.evaluation.registry.MetricRegistry`` | |
| qu'un service applicatif construit au démarrage (S20). | |
| Anti-sur-ingénierie | |
| ------------------- | |
| Pas de validation cross-références à l'instanciation d'un | |
| ``EvaluationView`` (par exemple, on ne vérifie pas que les | |
| ``metric_names`` existent dans un registre). Cette validation | |
| est faite au moment de l'exécution par ``EvaluationViewExecutor`` | |
| (S13), avec un message d'erreur explicite si une métrique | |
| référencée n'est pas enregistrée. Raison : un ``EvaluationView`` | |
| est un objet déclaratif qu'on peut sérialiser dans un YAML sans | |
| avoir besoin du registre runtime. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import BaseModel, ConfigDict, Field | |
| from picarones.domain.artifacts import ArtifactType | |
| from picarones.domain.projection_spec import ProjectionSpec | |
| class MetricSpec(BaseModel): | |
| """Description déclarative d'une métrique enregistrable. | |
| Attributs | |
| --------- | |
| name: | |
| Identifiant unique dans un ``MetricRegistry``. | |
| input_types: | |
| Tuple ``(reference_type, hypothesis_type)`` indiquant la | |
| signature attendue par la métrique. Le registre sélectionne | |
| les métriques applicables à une jonction par cette signature. | |
| description: | |
| Phrase courte affichée dans le rapport et le glossaire. | |
| higher_is_better: | |
| ``True`` pour les métriques de qualité (F1, recall, accuracy), | |
| ``False`` pour les métriques d'erreur (CER, WER). Utilisé | |
| par les vues pour orienter la coloration et le tri. | |
| tags: | |
| Étiquettes libres pour grouper les métriques (``"text"``, | |
| ``"structure"``, ``"icdar"``, ``"philological"``, ...). | |
| Contrairement à l'ancien ``core.metric_registry.MetricSpec``, | |
| aucun ``func: Callable`` n'est porté ici — un ``MetricSpec`` | |
| est purement déclaratif et peut être chargé depuis un YAML. | |
| L'association nom → fonction est faite par ``MetricRegistry``. | |
| """ | |
| model_config = ConfigDict(frozen=True, extra="forbid") | |
| name: str = Field(min_length=1, max_length=128) | |
| input_types: tuple[ArtifactType, ArtifactType] | |
| description: str = "" | |
| higher_is_better: bool = False | |
| tags: frozenset[str] = Field(default_factory=frozenset) | |
| class EvaluationView(BaseModel): | |
| """Une vue d'évaluation = une "lentille" pour comparer des pipelines. | |
| Une vue répond à une question précise : "lequel des pipelines | |
| disponibles produit la meilleure sortie sous cet angle ?" | |
| Trois exemples canoniques (à implémenter S14-S16) : | |
| - ``TextView`` (text_final) — accepte RAW_TEXT, CORRECTED_TEXT, | |
| ALTO_XML, PAGE_XML, projette tout vers RAW_TEXT, mesure CER/WER. | |
| Ignore : géométrie, blocs, ordre spatial, validité ALTO. | |
| - ``AltoView`` (alto_documentary) — exige ALTO_XML, mesure | |
| validité, alignement lignes/mots, ordre de lecture. Ignore : | |
| qualité linguistique pure. | |
| - ``SearchView`` (searchability) — projette tout vers RAW_TEXT, | |
| mesure recall fuzzy, séquences numériques préservées, noms | |
| propres retrouvés. | |
| Attributs | |
| --------- | |
| name: | |
| Identifiant lisible (``"text_final"``, ``"alto_documentary"``). | |
| description: | |
| Phrase d'introduction affichée dans le rapport. | |
| candidate_types: | |
| Set des ``ArtifactType`` qu'on accepte en entrée. Un pipeline | |
| ne produisant aucun artefact dans ce set est **omis | |
| explicitement** de la vue (pas de score factice). | |
| projection: | |
| Spec optionnelle de projection à appliquer aux candidats avant | |
| évaluation. ``None`` = pas de projection (l'artefact est | |
| comparé tel quel au GT). | |
| normalization_profile: | |
| Nom d'un profil de normalisation texte | |
| (cf. ``picarones.formats.text.normalization``). ``None`` = | |
| pas de normalisation (NFC implicite). | |
| metric_names: | |
| Liste ordonnée des métriques à calculer. Validées par | |
| l'executor au runtime (le registre doit contenir chaque nom). | |
| ignored_dimensions: | |
| Liste de dimensions explicitement ignorées par cette vue. | |
| Affiché dans le rapport pour signaler ce que la comparaison | |
| ne dit PAS. Ex : ``("geometry", "block_structure", | |
| "reading_order")`` pour TextView. | |
| warnings: | |
| Avertissement(s) méthodologique(s) à afficher en tête du | |
| bloc de la vue dans le rapport. Ex : "Cette vue ignore la | |
| qualité spatiale et documentaire." | |
| """ | |
| model_config = ConfigDict(frozen=True, extra="forbid") | |
| name: str = Field(min_length=1, max_length=128) | |
| description: str = "" | |
| candidate_types: frozenset[ArtifactType] = Field(...) | |
| projection: ProjectionSpec | None = None | |
| """Projection unique appliquée à TOUS les candidats avant | |
| évaluation. ``None`` = pas de projection (artefact comparé | |
| tel quel). Si ``projections_by_source_type`` est aussi | |
| renseigné, ce champ sert de fallback pour les types non listés.""" | |
| projections_by_source_type: dict[ArtifactType, ProjectionSpec] = Field( | |
| default_factory=dict, | |
| ) | |
| """S14 — projection conditionnelle par type d'artefact source. | |
| Permet à une vue qui accepte plusieurs types (ex : ``TextView`` | |
| qui accepte RAW_TEXT, ALTO_XML, PAGE_XML) d'utiliser un | |
| projecteur différent par type sans avoir à dupliquer la vue. | |
| Convention de résolution dans ``DefaultEvaluationViewExecutor`` : | |
| 1. Si ``projections_by_source_type[candidate.type]`` existe : | |
| utiliser cette projection. | |
| 2. Sinon, si ``projection`` est défini ET son ``source_type`` | |
| matche ``candidate.type`` : utiliser cette projection. | |
| 3. Sinon : pas de projection (artefact comparé tel quel). | |
| Toutes les projections référencées doivent exister dans le | |
| ``ProjectorRegistry`` au moment de l'exécution (validé runtime). | |
| """ | |
| normalization_profile: str | None = Field(default=None, max_length=128) | |
| metric_names: tuple[str, ...] = Field(default_factory=tuple) | |
| ignored_dimensions: tuple[str, ...] = Field(default_factory=tuple) | |
| warnings: tuple[str, ...] = Field(default_factory=tuple) | |
| def accepts(self, artifact_type: ArtifactType) -> bool: | |
| """Vrai si cette vue peut consommer un artefact du type donné.""" | |
| return artifact_type in self.candidate_types | |
| def projection_for( | |
| self, source_type: ArtifactType, | |
| ) -> ProjectionSpec | None: | |
| """Retourne la projection à appliquer pour un artefact source | |
| de type ``source_type``, ou ``None`` si aucune projection n'est | |
| applicable (artefact comparé tel quel). | |
| Convention de résolution : | |
| 1. ``projections_by_source_type[source_type]`` si présent. | |
| 2. ``projection`` si son ``source_type`` matche. | |
| 3. ``None``. | |
| """ | |
| if source_type in self.projections_by_source_type: | |
| return self.projections_by_source_type[source_type] | |
| if ( | |
| self.projection is not None | |
| and self.projection.source_type == source_type | |
| ): | |
| return self.projection | |
| return None | |
| class EvaluationSpec(BaseModel): | |
| """Container de N ``EvaluationView`` qu'un benchmark applique. | |
| Un ``EvaluationSpec`` est versionné dans un YAML ; un service | |
| applicatif (S19) le résout en runtime contre un ``MetricRegistry`` | |
| instancié, et le ``EvaluationViewExecutor`` (S13) l'applique aux | |
| artefacts produits par le pipeline executor. | |
| """ | |
| model_config = ConfigDict(frozen=True, extra="forbid") | |
| views: tuple[EvaluationView, ...] = Field(default_factory=tuple) | |
| def view_by_name(self, name: str) -> EvaluationView | None: | |
| """Retourne la vue de nom ``name`` ou ``None``.""" | |
| for v in self.views: | |
| if v.name == name: | |
| return v | |
| return None | |
| __all__ = ["MetricSpec", "EvaluationView", "EvaluationSpec"] | |