Spaces:
Sleeping
Sleeping
Claude
feat(evaluation): Sprint A14-S15 — AltoView (vue canonique 2) + 3 métriques structurelles + pattern d'omission
99ad1af unverified | """Métriques structurelles ALTO — Sprint A14-S15. | |
| Métriques typées ``(ALTO_XML, ALTO_XML)`` qui mesurent la fidélité | |
| **documentaire** d'un ALTO produit par un pipeline (par exemple un | |
| reconstructeur post-correction LLM ou un VLM avec module | |
| ALTO_reconstruction) face à la GT ALTO du corpus. | |
| Distinct de ``picarones/measurements/alto_metrics.py`` (legacy) | |
| qui calcule CER/WER sur le **texte extrait** des deux ALTO. Ici | |
| on mesure la **structure** : nombre de lignes, présence de bbox, | |
| ordre de lecture cohérent. | |
| Métriques livrées au S15 | |
| ------------------------ | |
| - ``compute_alto_validity(ref, hyp)`` — 1.0 si l'hypothèse a une | |
| structure cohérente (≥ 1 page, ≥ 1 bloc, ≥ 1 ligne). Détecte | |
| les ALTO vides ou tronqués. | |
| - ``compute_line_count_ratio(ref, hyp)`` — ``min(n_hyp, n_ref) / | |
| max(n_hyp, n_ref)`` ∈ [0, 1]. 1.0 = même nombre de lignes. | |
| - ``compute_word_box_coverage(ref, hyp)`` — fraction des | |
| ``AltoString`` de l'hypothèse qui ont une ``bbox``. 1.0 = tous | |
| les mots ont une boîte (cas idéal pour un reconstructeur ALTO). | |
| Reportées à des sprints suivants (post-livraison) | |
| ------------------------------------------------- | |
| - ``textline_alignment`` (IoU des bbox de lignes) — exige un | |
| algorithme d'alignement bipartite par bbox. | |
| - ``reading_order_consistency`` (Kendall tau sur les IDs de | |
| lignes) — exige un mapping ID → position. | |
| - ``layout_f1`` (ICDAR 2015) — déjà implémenté dans | |
| ``evaluation/metrics/layout.py`` (migré au S10) sur des | |
| ``Region`` génériques ; un wrapper ALTO peut être ajouté plus | |
| tard. | |
| Convention de signature | |
| ----------------------- | |
| Les payloads attendus sont des ``AltoDocument`` parsés (par le | |
| ``payload_loader`` du service applicatif). Si le caller passe | |
| des bytes XML brut, il doit appeler ``parse_alto`` lui-même | |
| en amont. | |
| higher_is_better | |
| ---------------- | |
| Toutes les métriques de ce module ∈ [0, 1] avec ``higher_is_better=True`` | |
| (1.0 = parfait, 0.0 = pire). Cohérent avec le schéma ICDAR pour | |
| les métriques de fidélité documentaire. | |
| """ | |
| from __future__ import annotations | |
| from picarones.formats.alto.types import AltoDocument | |
| def _count_lines(doc: AltoDocument) -> int: | |
| """Compte le nombre total de ``AltoLine`` dans un document.""" | |
| return sum( | |
| len(block.lines) | |
| for page in doc.pages | |
| for block in page.blocks | |
| ) | |
| def _count_strings(doc: AltoDocument) -> int: | |
| """Compte le nombre total de ``AltoString`` dans un document.""" | |
| return sum( | |
| len(line.strings) | |
| for page in doc.pages | |
| for block in page.blocks | |
| for line in block.lines | |
| ) | |
| def compute_alto_validity( | |
| reference: AltoDocument, | |
| hypothesis: AltoDocument, | |
| ) -> float: | |
| """Vérifie que l'hypothèse a une structure ALTO cohérente. | |
| Cohérence = au moins 1 page ET au moins 1 bloc ET au moins | |
| 1 ligne dans l'hypothèse. Détecte les ALTO vides, tronqués, | |
| ou produits par un reconstructeur défaillant. | |
| Returns | |
| ------- | |
| float | |
| 1.0 si l'hypothèse est structurellement cohérente, | |
| 0.0 sinon. | |
| Notes | |
| ----- | |
| On ne compare PAS la cohérence à la référence ici — la | |
| référence est juste passée pour homogénéité d'API avec les | |
| autres métriques. Un ALTO de référence vide (cas dégénéré) | |
| n'invalide pas l'hypothèse. | |
| """ | |
| if not hypothesis.pages: | |
| return 0.0 | |
| has_block = any(page.blocks for page in hypothesis.pages) | |
| if not has_block: | |
| return 0.0 | |
| has_line = any( | |
| block.lines | |
| for page in hypothesis.pages | |
| for block in page.blocks | |
| ) | |
| if not has_line: | |
| return 0.0 | |
| return 1.0 | |
| def compute_line_count_ratio( | |
| reference: AltoDocument, | |
| hypothesis: AltoDocument, | |
| ) -> float: | |
| """Ratio min/max du nombre de lignes des deux ALTO. | |
| Returns | |
| ------- | |
| float | |
| ``min(n_hyp, n_ref) / max(n_hyp, n_ref)`` ∈ [0, 1]. | |
| 1.0 = même nombre de lignes. 0.0 si l'un des deux n'a | |
| aucune ligne (cas dégénéré). | |
| Permet de détecter un reconstructeur qui invente ou perd des | |
| lignes vs la GT. Ne dit RIEN sur l'alignement spatial — | |
| c'est ``textline_alignment`` (post-livraison) qui mesurera | |
| cette dimension. | |
| """ | |
| n_ref = _count_lines(reference) | |
| n_hyp = _count_lines(hypothesis) | |
| if n_ref == 0 and n_hyp == 0: | |
| return 1.0 # convention : deux vides identiques | |
| if n_ref == 0 or n_hyp == 0: | |
| return 0.0 | |
| return min(n_ref, n_hyp) / max(n_ref, n_hyp) | |
| def compute_word_box_coverage( | |
| reference: AltoDocument, | |
| hypothesis: AltoDocument, | |
| ) -> float: | |
| """Fraction des ``AltoString`` de l'hypothèse qui ont une ``bbox``. | |
| Returns | |
| ------- | |
| float | |
| ``n_strings_with_bbox / n_strings_total`` ∈ [0, 1]. | |
| 1.0 = tous les mots ont une boîte (cas idéal pour un | |
| reconstructeur ALTO). 0.0 si l'hypothèse n'a aucun mot. | |
| La référence n'est pas utilisée dans le calcul, mais elle est | |
| passée pour homogénéité d'API. Un caller qui veut comparer | |
| "candidat a-t-il autant de bbox que la GT" peut mesurer | |
| ``compute_word_box_coverage(gt, hyp) / compute_word_box_coverage(hyp, gt)`` | |
| ou utiliser un calcul dédié. | |
| """ | |
| total = _count_strings(hypothesis) | |
| if total == 0: | |
| return 0.0 | |
| with_bbox = sum( | |
| 1 | |
| for page in hypothesis.pages | |
| for block in page.blocks | |
| for line in block.lines | |
| for s in line.strings | |
| if s.bbox is not None | |
| ) | |
| return with_bbox / total | |
| __all__ = [ | |
| "compute_alto_validity", | |
| "compute_line_count_ratio", | |
| "compute_word_box_coverage", | |
| ] | |