Spaces:
Sleeping
Sleeping
Claude
docs(sprint-H.8): cleanup obsolete legacy/shim language in production docstrings
e407ec0 unverified | """``Artifact`` et ``ArtifactType``. | |
| Toute sortie d'une étape de pipeline est un **artefact traçable** : | |
| identifiant stable, type explicite, hash du contenu, provenance. | |
| L'enum ``ArtifactType`` a 9 valeurs canoniques + 3 aliases courts | |
| pour les types texte/ALTO/PAGE. Distinctions clés pour les vues | |
| d'évaluation : | |
| - **``RAW_TEXT`` vs ``CORRECTED_TEXT``** — un OCR brut et un texte | |
| corrigé par un LLM ont la même structure (string) mais des contrats | |
| différents : seul le second peut être projeté vers ``ALTO_XML`` | |
| via reconstruction. Cette distinction permet à ``TextView`` de | |
| comparer honnêtement les deux types dans la même vue tout en | |
| signalant à l'utilisateur que la projection a un sens différent. | |
| - **``ALTO_XML`` vs ``PAGE_XML`` vs ``CANONICAL_DOCUMENT``** — les | |
| trois formats spatiaux sont conceptuellement distincts ; un | |
| ``CANONICAL_DOCUMENT`` (markdown ou JSON canonique produit par un | |
| VLM) n'a pas de coordonnées et ne peut pas être projeté vers | |
| ``ALTO_XML`` sans étape de reconstruction. | |
| Anti-sur-ingénierie | |
| ------------------- | |
| ``Artifact`` ne porte que les champs nécessaires aux vues actuelles. | |
| Champs reportés (à ajouter quand un caller en a concrètement besoin) : | |
| ``media_type``, ``cost``, ``latency``, ``warnings``, ``model_version``, | |
| ``parent_artifact_ids`` (DAG d'origine). | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import re | |
| from enum import Enum | |
| from pydantic import BaseModel, ConfigDict, Field, field_validator | |
| class ArtifactType(str, Enum): | |
| """Type d'un artefact produit ou consommé par une étape de pipeline. | |
| Volontairement extensible : si une nouvelle vue (post-livraison) | |
| nécessite un type supplémentaire (ex : ``LAYOUT_HEATMAP``), on | |
| l'ajoute ici avec un commentaire indiquant la vue qui le | |
| consomme. | |
| Convention de nommage : ``UPPER_SNAKE_CASE`` pour le nom Python, | |
| ``lower_snake_case`` pour la valeur string sérialisée (utilisée | |
| dans les YAML de pipeline et dans les exports JSON). | |
| """ | |
| #: Image source (PNG, TIFF, JPEG). Entrée typique d'un OCR. | |
| IMAGE = "image" | |
| #: Texte brut produit par un OCR (avant correction LLM). | |
| RAW_TEXT = "raw_text" | |
| #: Texte corrigé par un LLM ou un module de post-correction. | |
| #: Distinct de ``RAW_TEXT`` parce que les vues d'évaluation | |
| #: doivent pouvoir signaler "ce texte a été modifié par un | |
| #: modèle après l'OCR" (impact sur over-normalisation, | |
| #: hallucination, fidélité philologique). | |
| CORRECTED_TEXT = "corrected_text" | |
| #: ALTO XML 4.x avec lignes, mots, coordonnées, ordre de lecture. | |
| ALTO_XML = "alto_xml" | |
| #: PAGE XML (PRIMA / Transkribus). | |
| PAGE_XML = "page_xml" | |
| #: Représentation canonique structurée sans coordonnées. | |
| #: Typique d'une sortie VLM (markdown, JSON canonique). Peut | |
| #: être reconstruit en ALTO via un module dédié, mais n'a pas | |
| #: nativement les coordonnées spatiales. | |
| CANONICAL_DOCUMENT = "canonical_document" | |
| #: Liste d'entités nommées (PER, LOC, ORG, DATE, MISC...). | |
| ENTITIES = "entities" | |
| #: Liste ordonnée d'IDs de régions documentaires définissant | |
| #: l'ordre de lecture (essentiel pour les manuscrits glosés et | |
| #: les journaux multi-colonnes). | |
| READING_ORDER = "reading_order" | |
| #: Alignement entre deux artefacts (typiquement ``RAW_TEXT`` → | |
| #: ``CORRECTED_TEXT`` produit par un module de post-correction | |
| #: ou de remapping ALTO). Utilisé par ``HallucinationView`` et | |
| #: ``error_absorption``. | |
| ALIGNMENT = "alignment" | |
| #: Confidences OCR au niveau token. Sidecar JSON produit par les | |
| #: adapters OCR qui exposent des scores natifs (Tesseract | |
| #: image_to_data, Pero transcription_confidence, Mistral OCR API | |
| #: confidences, Google Vision Word.confidence, Azure DI | |
| #: Word.confidence). | |
| #: | |
| #: Schéma JSON : ``{"tokens": [{"text": str, "confidence": | |
| #: float ∈ [0, 1]}], "extractor": str, "model_version": str | | |
| #: null}``. Consommé par les vues de calibration (ECE/MCE, | |
| #: reliability diagram). | |
| CONFIDENCES = "confidences" | |
| #: Aliases courts pour les types texte/ALTO/PAGE. Le mécanisme | |
| #: natif d'Enum Python rend ces noms équivalents aux canoniques : | |
| #: | |
| #: >>> ArtifactType.TEXT is ArtifactType.RAW_TEXT | |
| #: True | |
| #: | |
| #: Utilisés par les ``@register_metric(...)`` qui déclarent leurs | |
| #: signatures de manière concise. | |
| TEXT = "raw_text" | |
| ALTO = "alto_xml" | |
| PAGE = "page_xml" | |
| def _missing_(cls, value: object) -> "ArtifactType | None": | |
| """Accepte les chaînes courtes (``"text"``, ``"alto"``, | |
| ``"page"``) en plus des valeurs canoniques. | |
| Permet aux specs YAML d'utiliser indifféremment l'un ou | |
| l'autre nom. | |
| """ | |
| short_map: dict[str, "ArtifactType"] = { | |
| "text": cls.RAW_TEXT, | |
| "alto": cls.ALTO_XML, | |
| "page": cls.PAGE_XML, | |
| } | |
| if not isinstance(value, str): | |
| return None | |
| return short_map.get(value) | |
| #: Map valeur canonique → valeur string courte. Permet aux dicts | |
| #: indexés par ``ArtifactType.value`` de présenter les **deux** clés : | |
| #: un caller qui cherche ``["raw_text"]`` et un caller qui cherche | |
| #: ``["text"]`` voient le même résultat. | |
| LEGACY_VALUE_ALIASES: dict[str, str] = { | |
| "raw_text": "text", | |
| "alto_xml": "alto", | |
| "page_xml": "page", | |
| } | |
| def expand_legacy_keys(d: dict) -> dict: | |
| """Pour chaque clé canonique de ``d`` qui a un alias legacy | |
| (cf. :data:`LEGACY_VALUE_ALIASES`), copie la valeur sous l'alias. | |
| Mute le dict en place ET le retourne (chainable). Idempotent : | |
| si la clé legacy existe déjà avec une valeur différente, on ne | |
| l'écrase pas (un override explicite gagne). | |
| """ | |
| for canonical, legacy in LEGACY_VALUE_ALIASES.items(): | |
| if canonical in d and legacy not in d: | |
| d[legacy] = d[canonical] | |
| return d | |
| def compute_content_hash(payload: bytes) -> str: | |
| """SHA-256 hex (64 chars) d'un payload binaire. | |
| Helper exposé au domain pour que les adapters puissent calculer | |
| un hash compatible avec ``Artifact.content_hash`` sans dépendre | |
| d'un détail d'implémentation. | |
| """ | |
| return hashlib.sha256(payload).hexdigest() | |
| # Validation des identifiants. On veut un ``id`` stable et | |
| # filesystem-safe (utilisable comme nom de fichier dans | |
| # ``ArtifactStore``) sans imposer un format trop restrictif. | |
| _ID_RE = re.compile(r"^[A-Za-z0-9_.\-:/]+$") | |
| class Artifact(BaseModel): | |
| """Une sortie traçable d'une étape de pipeline. | |
| Immuable (``frozen=True``) : un artefact ne change pas après | |
| création. Pour produire un artefact "modifié", une étape produit | |
| un nouvel ``Artifact`` distinct. | |
| Sérialisation déterministe : ``model_dump_json()`` produit les | |
| mêmes octets pour le même contenu (champs Pydantic ordonnés). | |
| Indispensable pour le cache d'artefacts. | |
| Attributs | |
| --------- | |
| id: | |
| Identifiant unique de l'artefact dans le contexte d'un run. | |
| Convention : ``"<doc_id>:<step_name>:<artifact_type>"``, | |
| mais le caller est libre du format tant que c'est unique | |
| et que ``_ID_RE`` matche. | |
| document_id: | |
| ``DocumentRef.id`` du document auquel cet artefact appartient. | |
| type: | |
| Type de l'artefact (cf. ``ArtifactType``). | |
| uri: | |
| Chemin filesystem ou URI distant vers le contenu. ``None`` | |
| si l'artefact est stocké inline (cas des petits artefacts | |
| comme un texte court produit en mémoire). Le caller (typiquement | |
| ``ArtifactStore``, S7) est responsable de la résolution. | |
| content_hash: | |
| SHA-256 hex (64 chars) du contenu. ``None`` autorisé seulement | |
| pour les artefacts initiaux fournis par l'utilisateur (image, | |
| GT) qui n'ont pas encore été lus. Une fois calculé, immuable. | |
| produced_by_step: | |
| Nom de l'étape de pipeline qui a produit l'artefact. ``None`` | |
| pour les artefacts initiaux (entrées du pipeline, GT). | |
| provenance: | |
| ``ProvenanceRecord`` portant ``code_version`` et | |
| ``parameters_hash``. ``None`` pour les artefacts initiaux. | |
| """ | |
| model_config = ConfigDict(frozen=True, extra="forbid") | |
| id: str = Field(min_length=1, max_length=512) | |
| document_id: str = Field(min_length=1, max_length=256) | |
| type: ArtifactType | |
| uri: str | None = Field(default=None, max_length=2048) | |
| content_hash: str | None = Field(default=None, min_length=64, max_length=64) | |
| produced_by_step: str | None = Field(default=None, max_length=256) | |
| # ``provenance`` typé en str pour éviter import croisé pydantic | |
| # avec ProvenanceRecord ; remplacé par le vrai type via __init__ | |
| # plus bas. | |
| provenance: "ProvenanceRecord | None" = Field(default=None) | |
| def _validate_filesystem_safe_id(cls, v: str) -> str: | |
| if not _ID_RE.match(v): | |
| from picarones.domain.errors import ArtifactValidationError | |
| raise ArtifactValidationError( | |
| f"id invalide : {v!r}. " | |
| f"Doit matcher {_ID_RE.pattern!r} (alphanum + ``_.-:/``)." | |
| ) | |
| return v | |
| def _validate_hex_hash(cls, v: str | None) -> str | None: | |
| if v is None: | |
| return v | |
| try: | |
| int(v, 16) | |
| except ValueError: | |
| from picarones.domain.errors import ArtifactValidationError | |
| raise ArtifactValidationError( | |
| f"content_hash doit être hex SHA-256 64 chars : {v!r}" | |
| ) | |
| return v.lower() | |
| # Forward reference pour ``provenance``. | |
| from picarones.domain.provenance import ProvenanceRecord # noqa: E402 | |
| Artifact.model_rebuild() | |
| __all__ = [ | |
| "Artifact", | |
| "ArtifactType", | |
| "compute_content_hash", | |
| ] | |