Spaces:
Running
Running
Claude
refactor(measurements): promouvoir modules philologiques/acadรฉmiques/governance depuis extras/
7a072e2 unverified | """Marqueurs typographiques de l'imprimรฉ ancien (XVIแต-XVIIIแต). | |
| Sprint 58 โ รtape 3 / extension philologique du plan d'รฉvolution | |
| 2026. | |
| Pourquoi ce module | |
| ------------------ | |
| Les Sprints 56 (abrรฉviations Capelli) et 57 (couverture MUFI) sont | |
| orientรฉs **mรฉdiรฉval scribal**. Mais Picarones doit aussi servir | |
| les รฉditeurs d'**imprimรฉs anciens** (XVIแต-XVIIIแต siรจcles), pour | |
| qui les marqueurs caractรฉristiques ne sont pas scribaux mais | |
| **typographiques** : ligatures composรฉes (๏ฌ, ๏ฌ, ๏ฌ, ๏ฌ, ๏ฌ, ๏ฌ ), | |
| s long (ลฟ), i sans point (ฤฑ), esperluette (&), tildes nasaux | |
| indiquant une abrรฉviation (รฃ = an/am, รต = on/om). | |
| Distinction avec MUFI/abbreviations | |
| ------------------------------------ | |
| - ``mufi.py`` (Sprint 57) : caractรจres mรฉdiรฉvaux scribaux | |
| (Capelli + lettres รพ รฐ ฦฟ + PUA MUFI). | |
| - ``abbreviations.py`` (Sprint 56) : signes d'abrรฉviation latins | |
| scribaux mรฉdiรฉvaux (๊ ๊ โ + tildes scribaux). | |
| - ``early_modern_typography.py`` (ce module) : marqueurs | |
| **typographiques** de la composition imprimรฉe ancienne. | |
| Les ligatures ๏ฌ et ๏ฌ sont communes aux deux univers (mรฉdiรฉval et | |
| imprimรฉ ancien) ; le choix du module ร utiliser dรฉpend du **corpus** | |
| et de l'angle d'analyse รฉditoriale, pas du caractรจre pris isolรฉment. | |
| Catรฉgorisation | |
| -------------- | |
| Les marqueurs sont classรฉs en cinq catรฉgories pour permettre un | |
| breakdown รฉditorial : | |
| 1. ``ligatures`` : ๏ฌ ๏ฌ ๏ฌ ๏ฌ ๏ฌ ๏ฌ | |
| 2. ``long_s`` : ลฟ | |
| 3. ``dotless_i`` : ฤฑ | |
| 4. ``ampersand`` : & (esperluette typographique) | |
| 5. ``nasal_tildes`` : รฃ รต ลฉ รฑ ฤ ฤซ (abrรฉviation par tilde nasal) | |
| ``compute_early_modern_metrics`` retourne le taux de prรฉservation | |
| par catรฉgorie + global. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from difflib import SequenceMatcher | |
| from typing import Optional | |
| from picarones.core.metric_registry import register_metric | |
| from picarones.core.modules import ArtifactType | |
| logger = logging.getLogger(__name__) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Marqueurs typographiques imprimรฉ ancien | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Ligatures typographiques hรฉritรฉes de l'incunable (XVแต) et toujours | |
| # courantes jusqu'au XVIIIแต avant la normalisation typographique. | |
| LIGATURES: frozenset[str] = frozenset({ | |
| "๏ฌ", # U+FB00 ff | |
| "๏ฌ", # U+FB01 fi | |
| "๏ฌ", # U+FB02 fl | |
| "๏ฌ", # U+FB03 ffi | |
| "๏ฌ", # U+FB04 ffl | |
| "๏ฌ ", # U+FB05 long s + t | |
| "๏ฌ", # U+FB06 st | |
| }) | |
| # S long : Latin Extended-A. Caractรฉristique de la typographie | |
| # antรฉrieure ร 1800. | |
| LONG_S: frozenset[str] = frozenset({"ลฟ"}) # U+017F | |
| # i sans point : utilisรฉ en typographie ancienne, parfois confondu | |
| # avec un l ou un 1 par les OCR modernes. | |
| DOTLESS_I: frozenset[str] = frozenset({"ฤฑ"}) # U+0131 | |
| # Esperluette typographique : "&" remplace frรฉquemment "et" dans | |
| # les imprimรฉs ; sa prรฉservation discrimine un OCR diplomatique | |
| # d'un OCR modernisant. | |
| AMPERSAND: frozenset[str] = frozenset({"&"}) | |
| # Tildes nasaux : prรฉ-composรฉs (รฑ รฃ แบฝ ฤฉ รต ลฉ) ou sรฉquences | |
| # lettre + U+0303 combinant. En imprimรฉ ancien, รฃ = an/am abrรฉgรฉ, | |
| # รต = on/om, etc. Distinction avec les tildes scribaux mรฉdiรฉvaux | |
| # (Sprint 56) : ici on cible les **prรฉ-composรฉs** ou sรฉquences sur | |
| # des voyelles (le scribal mรฉdiรฉval cible plutรดt pฬ qฬ). | |
| NASAL_TILDE_PRECOMPOSED: frozenset[str] = frozenset({ | |
| "รฃ", "ร", # U+00E3 / U+00C3 | |
| "รฑ", "ร", # U+00F1 / U+00D1 | |
| "รต", "ร", # U+00F5 / U+00D5 | |
| "ลฉ", "ลจ", # U+0169 / U+0168 | |
| "แบฝ", "แบผ", # U+1EBD / U+1EBC | |
| "ฤฉ", "ฤจ", # U+0129 / U+0128 | |
| }) | |
| # Voyelles susceptibles de porter un tilde combinant pour former | |
| # un tilde nasal (couvre les รฉcritures NFD non prรฉ-composรฉes). | |
| _NASAL_TILDE_VOWELS: frozenset[str] = frozenset( | |
| "aeiouAEIOU" | |
| ) | |
| _COMBINING_TILDE = "ฬ" | |
| # Catรฉgorisation : nom โ set de caractรจres prรฉ-composรฉs ou sรฉquences. | |
| _CATEGORIES: dict[str, frozenset[str]] = { | |
| "ligatures": LIGATURES, | |
| "long_s": LONG_S, | |
| "dotless_i": DOTLESS_I, | |
| "ampersand": AMPERSAND, | |
| "nasal_tildes": NASAL_TILDE_PRECOMPOSED, | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Dรฉtection des marqueurs dans la GT | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _detect_markers(text: str) -> list[tuple[int, str, str]]: | |
| """Retourne les positions des marqueurs typographiques dans | |
| ``text``. | |
| Forme de sortie : ``[(index, marker, category), ...]`` dans | |
| l'ordre d'apparition. Pour les tildes nasaux non | |
| prรฉ-composรฉs, on dรฉtecte les sรฉquences ``voyelle + U+0303`` et | |
| on retourne l'index de la voyelle. | |
| """ | |
| if not text: | |
| return [] | |
| found: list[tuple[int, str, str]] = [] | |
| i = 0 | |
| while i < len(text): | |
| ch = text[i] | |
| # Cas 1 : marqueur prรฉ-composรฉ dans une catรฉgorie | |
| category = _category_of_char(ch) | |
| if category is not None: | |
| found.append((i, ch, category)) | |
| i += 1 | |
| continue | |
| # Cas 2 : voyelle + tilde combinant โ nasal_tildes | |
| if ( | |
| ch in _NASAL_TILDE_VOWELS | |
| and i + 1 < len(text) | |
| and text[i + 1] == _COMBINING_TILDE | |
| ): | |
| seq = ch + _COMBINING_TILDE | |
| found.append((i, seq, "nasal_tildes")) | |
| i += 2 | |
| continue | |
| i += 1 | |
| return found | |
| def _category_of_char(ch: str) -> Optional[str]: | |
| """Retourne la catรฉgorie d'un caractรจre typographique ou | |
| ``None`` s'il n'est pas reconnu.""" | |
| for cat, chars in _CATEGORIES.items(): | |
| if ch in chars: | |
| return cat | |
| return None | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Calcul de la prรฉservation par catรฉgorie | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def compute_early_modern_metrics( | |
| reference: Optional[str], | |
| hypothesis: Optional[str], | |
| ) -> dict: | |
| """Mesure la prรฉservation des marqueurs typographiques de | |
| l'imprimรฉ ancien dans l'OCR. | |
| Stratรฉgie d'alignement | |
| ---------------------- | |
| Pour chaque marqueur identifiรฉ dans la GT ร la position ``i``, | |
| on vรฉrifie si l'OCR l'a prรฉservรฉ en utilisant l'alignement | |
| caractรจre par caractรจre via ``difflib.SequenceMatcher`` (mรชme | |
| mรฉthode que les Sprints 55/57) : | |
| - Marqueur **mono-caractรจre** (๏ฌ, ลฟ, ฤฑ, &, รฃโฆ) : la position | |
| ``i`` est-elle dans un opcode ``equal`` ? | |
| - Marqueur **bi-caractรจre** (voyelle + U+0303) : les positions | |
| ``i`` et ``i+1`` sont-elles toutes deux dans un opcode | |
| ``equal`` ? | |
| Returns | |
| ------- | |
| dict | |
| ``{ | |
| "n_markers_reference": int, | |
| "n_markers_preserved": int, | |
| "global_preservation": float, # โ [0, 1] | |
| "per_category": { | |
| category: {"total", "preserved", "preservation"} | |
| }, | |
| "missed_markers": [{"index", "marker", "category"}, ...], | |
| }`` | |
| Cas dรฉgรฉnรฉrรฉs : GT vide ou sans marqueur โ tous compteurs ร 0, | |
| ``global_preservation = 0``. | |
| """ | |
| ref = reference or "" | |
| hyp = hypothesis or "" | |
| # Forme NFD pour reconnaรฎtre les tildes nasaux dรฉcomposรฉs (รฃ = | |
| # 'a' + U+0303) cรดtรฉ GT โ on conserve toutefois la forme passรฉe | |
| # pour les indices rapportรฉs dans missed_markers. | |
| markers = _detect_markers(ref) | |
| n_total = len(markers) | |
| if n_total == 0: | |
| return { | |
| "n_markers_reference": 0, | |
| "n_markers_preserved": 0, | |
| "global_preservation": 0.0, | |
| "per_category": {}, | |
| "missed_markers": [], | |
| } | |
| # Aligner GT/hyp et rรฉcupรฉrer le set des positions GT couvertes | |
| # par un opcode "equal". | |
| matcher = SequenceMatcher(a=ref, b=hyp, autojunk=False) | |
| correct_positions: set[int] = set() | |
| for op, i1, i2, _j1, _j2 in matcher.get_opcodes(): | |
| if op == "equal": | |
| correct_positions.update(range(i1, i2)) | |
| per_cat_total: dict[str, int] = {} | |
| per_cat_preserved: dict[str, int] = {} | |
| n_preserved = 0 | |
| missed: list[dict] = [] | |
| for index, marker, category in markers: | |
| per_cat_total[category] = per_cat_total.get(category, 0) + 1 | |
| # Marqueur prรฉservรฉ si toutes ses positions GT sont dans | |
| # un opcode "equal". | |
| marker_len = len(marker) | |
| positions_ok = all( | |
| (index + k) in correct_positions for k in range(marker_len) | |
| ) | |
| if positions_ok: | |
| per_cat_preserved[category] = ( | |
| per_cat_preserved.get(category, 0) + 1 | |
| ) | |
| n_preserved += 1 | |
| else: | |
| missed.append({ | |
| "index": index, | |
| "marker": marker, | |
| "category": category, | |
| }) | |
| per_category = { | |
| cat: { | |
| "total": per_cat_total[cat], | |
| "preserved": per_cat_preserved.get(cat, 0), | |
| "preservation": ( | |
| per_cat_preserved.get(cat, 0) / per_cat_total[cat] | |
| if per_cat_total[cat] > 0 | |
| else 0.0 | |
| ), | |
| } | |
| for cat in sorted(per_cat_total) | |
| } | |
| return { | |
| "n_markers_reference": n_total, | |
| "n_markers_preserved": n_preserved, | |
| "global_preservation": n_preserved / n_total, | |
| "per_category": per_category, | |
| "missed_markers": missed, | |
| } | |
| def early_modern_preservation( | |
| reference: Optional[str], hypothesis: Optional[str], | |
| ) -> float: | |
| """Raccourci : taux global de prรฉservation des marqueurs | |
| typographiques de l'imprimรฉ ancien.""" | |
| return compute_early_modern_metrics( | |
| reference, hypothesis, | |
| )["global_preservation"] | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Helpers exposรฉs | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def detect_markers(text: Optional[str]) -> list[tuple[int, str, str]]: | |
| """Wrapper public sur ``_detect_markers`` (acceptant ``None``).""" | |
| return _detect_markers(text or "") | |
| def get_category(char: str) -> Optional[str]: | |
| """Retourne la catรฉgorie typographique d'un caractรจre | |
| (``ligatures``, ``long_s``, ``dotless_i``, ``ampersand``, | |
| ``nasal_tildes``) ou ``None``. | |
| Pour un tilde combinant suivi d'une voyelle, l'utilisateur doit | |
| utiliser ``detect_markers`` qui gรจre les sรฉquences. | |
| """ | |
| return _category_of_char(char[0]) if char else None | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Enregistrement dans le registre typรฉ (Sprint 34) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _registered_early_modern(reference: str, hypothesis: str) -> float: | |
| return early_modern_preservation(reference, hypothesis) | |
| __all__ = [ | |
| "LIGATURES", | |
| "LONG_S", | |
| "DOTLESS_I", | |
| "AMPERSAND", | |
| "NASAL_TILDE_PRECOMPOSED", | |
| "detect_markers", | |
| "get_category", | |
| "compute_early_modern_metrics", | |
| "early_modern_preservation", | |
| ] | |