Spaces:
Running
sprint80: sur-normalisation lexicale (A.I.7 couche calcul + HTML)
Browse filesLe dรฉtecteur llm_hallucination_flag (Sprint 19) signale via un
score agrรฉgรฉ mais ne dit pas QUOI corriger dans le prompt. Ce
sprint produit une table de frรฉquences dรฉtaillรฉe par token GT.
- Nouveau module picarones/core/lexical_modernization.py :
- compute_lexical_modernization(reference, hypothesis, stop_list,
case_sensitive) aligne mot-ร -mot via difflib et accumule par
token GT {n_total, n_modernized, rate_modernized, variants}.
- aggregate_lexical_modernization corpus-wide.
- top_modernized_tokens(n=20, min_total=1) tri descendant rate
avec filtre anecdotiques.
- Stop-list paramรฉtrable (par dรฉfaut vide).
- Suppression GT โ variant โ
.
- Nouveau module picarones/report/lexical_modernization_render.py :
- build_lexical_modernization_html : tableau 4 colonnes (forme
GT, variantes top-3, n GT, % gradient blancโorange).
- Adaptive : "" si data None ou aucun modernisรฉ.
- +6 clรฉs i18n FR/EN.
- +20 tests dans test_sprint80_lexical_modernization.py.
Verrou levรฉ : ยซ maistre โ maรฎtre modernisรฉ dans 100 % des cas ยป
permet d'ajuster le prompt โ info exploitable au lieu d'un score
agrรฉgรฉ abstrait.
Tests : 2699 passed, 2 skipped, 0 failed.
https://claude.ai/code/session_01RusTQYcSfXqTsbFNvwmCV7
- CHANGELOG.md +43 -0
- CLAUDE.md +2 -1
- picarones/core/lexical_modernization.py +263 -0
- picarones/report/i18n/en.json +7 -1
- picarones/report/i18n/fr.json +7 -1
- picarones/report/lexical_modernization_render.py +119 -0
- tests/test_sprint80_lexical_modernization.py +242 -0
|
@@ -16,6 +16,49 @@ La numรฉrotation de version suit [Semantic Versioning](https://semver.org/lang/f
|
|
| 16 |
|
| 17 |
### Ajoutรฉ
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
- **Sprint 79 โ A.I.6 : projection de coรปt en volume cible
|
| 20 |
(couche de calcul).** La vue Pareto (Sprint 20) trace CER vs
|
| 21 |
coรปt mais le coรปt est par unitรฉ (1 000 pages). Pour dรฉcider
|
|
|
|
| 16 |
|
| 17 |
### Ajoutรฉ
|
| 18 |
|
| 19 |
+
- **Sprint 80 โ A.I.7 : sur-normalisation lexicale en vue
|
| 20 |
+
analytique dรฉdiรฉe (couche calcul + table HTML).** Le dรฉtecteur
|
| 21 |
+
``llm_hallucination_flag`` (Sprint 19) signale qu'un moteur
|
| 22 |
+
sur-normalise via un score agrรฉgรฉ. Mais ce score ne dit rien
|
| 23 |
+
sur **quoi** corriger dans le prompt. Ce sprint produit une
|
| 24 |
+
**table de frรฉquences dรฉtaillรฉe** par token GT.
|
| 25 |
+
- Nouveau module `picarones/core/lexical_modernization.py` :
|
| 26 |
+
- ``compute_lexical_modernization(reference, hypothesis,
|
| 27 |
+
stop_list, case_sensitive)`` aligne mot-ร -mot via
|
| 28 |
+
``difflib.SequenceMatcher`` et accumule par token GT :
|
| 29 |
+
``{n_total, n_modernized, rate_modernized, variants}``.
|
| 30 |
+
- ``aggregate_lexical_modernization(per_doc_results)`` somme
|
| 31 |
+
les compteurs corpus-wide.
|
| 32 |
+
- ``top_modernized_tokens(data, n=20, min_total=1)`` retourne
|
| 33 |
+
les N tokens GT les plus modernisรฉs (tri dรฉcroissant par
|
| 34 |
+
taux, tie-break par n_total). Filtre les anecdotiques
|
| 35 |
+
via ``min_total``.
|
| 36 |
+
- Stop-list paramรฉtrable (tokens GT ร ignorer mรชme s'ils
|
| 37 |
+
sont modifiรฉs) โ par dรฉfaut vide, le module ne devine pas
|
| 38 |
+
ce qui est ยซ moderne ยป.
|
| 39 |
+
- Cas particuliers : token GT supprimรฉ โ variant ``โ
``.
|
| 40 |
+
- Nouveau module `picarones/report/lexical_modernization_render.py` :
|
| 41 |
+
- ``build_lexical_modernization_html(data, labels, top_n,
|
| 42 |
+
min_total)`` produit un tableau HTML 4 colonnes (forme
|
| 43 |
+
historique GT, variantes OCR, n GT, % modernisรฉ).
|
| 44 |
+
- Cellule ``% modernisรฉ`` colorรฉe en gradient blanc โ orange.
|
| 45 |
+
- Compactage des variants : top 3 affichรฉs + ``+N`` pour le
|
| 46 |
+
reste.
|
| 47 |
+
- Adaptive : ``""`` si ``data is None`` ou aucun token
|
| 48 |
+
modernisรฉ.
|
| 49 |
+
- +6 clรฉs i18n FR/EN (``lexmod_*``).
|
| 50 |
+
- +20 tests dans `test_sprint80_lexical_modernization.py` :
|
| 51 |
+
couche calcul (9 cas โ systรฉmatique, prรฉservรฉ, partiel,
|
| 52 |
+
multi-variants, stop-list, casse, suppression, vide, None) ;
|
| 53 |
+
agrรฉgation (2 cas) ; top (2 cas โ tri, min_total) ; rendu
|
| 54 |
+
(5 cas โ None, no_modernization, table, %, anti-injection) ;
|
| 55 |
+
complรฉtude i18n FR + EN.
|
| 56 |
+
- **Verrou levรฉ** : le chercheur peut dรฉsormais lire ยซ maistre
|
| 57 |
+
โ maรฎtre modernisรฉ dans 100 % des cas ยป et ajuster son prompt
|
| 58 |
+
en consรฉquence pour prรฉserver l'orthographe historique.
|
| 59 |
+
L'information est exploitable au lieu d'un score agrรฉgรฉ
|
| 60 |
+
abstrait.
|
| 61 |
+
|
| 62 |
- **Sprint 79 โ A.I.6 : projection de coรปt en volume cible
|
| 63 |
(couche de calcul).** La vue Pareto (Sprint 20) trace CER vs
|
| 64 |
coรปt mais le coรปt est par unitรฉ (1 000 pages). Pour dรฉcider
|
|
@@ -207,6 +207,7 @@ AZURE_DOC_INTEL_KEY=...
|
|
| 207 |
| 33 | **Sprint 2 du plan d'รฉvolution 2026 โ Phase 0.2 : interface module gรฉnรฉrique**. Nouveau module `picarones/core/modules.py` avec l'enum `ArtifactType` (IMAGE, TEXT, ALTO, PAGE, ENTITIES, READING_ORDER) et la classe abstraite `BaseModule` qui dรฉclare `input_types`/`output_types`, `execution_mode` (`"io"`/`"cpu"`), une mรฉthode `process(dict[ArtifactType, Any]) โ dict[ArtifactType, Any]`, et des helpers `validate_inputs`/`validate_outputs`. `BaseOCREngine` (`picarones/engines/base.py`) hรฉrite dรฉsormais de `BaseModule` avec `input_types=(IMAGE,)` et `output_types=(TEXT,)` ; sa nouvelle mรฉthode `process` wrappe l'API historique `run()`. Aucun adaptateur OCR existant n'est touchรฉ โ `test_engines.py` passe ร 20/20 sans modification. +23 tests dans `test_sprint33_module_interface.py` (contrat, validation, MockModule TEXTโALTO dรฉmonstratif comme demandรฉ par le plan, dรฉlรฉgation `BaseOCREngine.process โ run`, cohรฉrence ArtifactType/GTLevel). **Verrou levรฉ** : un mรชme runner peut maintenant exรฉcuter un OCR (imageโtexte), un mappeur VLMโALTO, un rewriter ALTOโALTO, un module NER (texteโentitรฉs), etc. โ fondation directe pour l'axe B du plan. |
|
| 208 |
| 34 | **Sprint 3 du plan d'รฉvolution 2026 โ Phase 0.3 : registre typรฉ de mรฉtriques (clรดture Phase 0)**. Nouveaux modules `picarones/core/metric_registry.py` (`MetricSpec`, `@register_metric`, `select_metrics`, `compute_at_junction`) et `picarones/core/builtin_metrics.py` qui enregistre `cer`, `wer`, `mer`, `wil` sur `(TEXT, TEXT)` plus un stub `text_preservation_after_reconstruction` sur `(TEXT, ALTO)` comme preuve de concept de jonction hรฉtรฉrogรจne. **Approche strictement additive** : ni `metrics.py` ni `compute_metrics` ne sont modifiรฉs, le rapport HTML reste identique octet par octet. La sรฉlection par signature de types est exacte (pas de coercion). +21 tests dans `test_sprint34_metric_registry.py`, dont une paritรฉ numรฉrique CER/WER/MER/WIL avec `compute_metrics` legacy ร 1e-9 prรจs sur 4 paires de textes. **Verrou levรฉ** : le runner d'une pipeline composรฉe peut maintenant calculer automatiquement la mรฉtrique adรฉquate ร chaque jonction de son DAG selon les types d'artefacts produits/attendus โ fondation directe pour la mรฉtrique d'absorption d'erreur (acte B.3) et toutes les mรฉtriques structurelles ร venir (Layout F1, reading order F1, NER). |
|
| 209 |
| 35 | **Sprint 4 du plan d'รฉvolution 2026 โ รtape 2 / axe A : mรฉtriques inter-moteurs (couche de calcul)**. Nouveau module `picarones/core/inter_engine.py` qui rรฉpond ร deux questions distinctes mais liรฉes : *(a) ร quel point les moteurs font-ils des erreurs de natures diffรฉrentes ?* via `kl_divergence`, `jensen_shannon_divergence` (symรฉtrique, bornรฉe `[0, 1]`), et `taxonomy_divergence_matrix` qui construit la matrice triangulaire inter-moteurs ; *(b) quel CER serait atteignable si on combinait les moteurs ?* via `oracle_token_recall` (proxy bag-of-words, borne supรฉrieure du recall atteignable), `complementarity_gap` (oracle vs meilleur moteur seul, gap absolu/relatif), et `pairwise_disagreement_rate`. Fonctions pures, sans I/O ni intรฉgration runner โ la couche de calcul est livrรฉe indรฉpendamment, le cรขblage narratif (`ENSEMBLE_OPPORTUNITY`) et HTML (matrice de divergence, badge oracle) suit au Sprint 36. +27 tests couvrant les invariants mathรฉmatiques (KL โฅ 0, KL(p,p) = 0, JS symรฉtrique et bornรฉe, oracle โฅ best_single, multiplicitรฉ respectรฉe), les cas concrets (deux moteurs spรฉcialisรฉs sortent comme candidats ensemble, complรฉmentaritรฉ parfaite atteint oracle = 1), et les garde-fous (rรฉfรฉrence vide, hypothรจses vides, mรฉtrique inconnue). |
|
|
|
|
| 210 |
| 79 | **Sprint 48 du plan d'รฉvolution 2026 โ A.I.6 : projection de coรปt en volume cible (couche de calcul)**. La vue Pareto (Sprint 20) trace CER vs coรปt mais le coรปt est par unitรฉ (1 000 pages) ; payer 50 โฌ de plus sur 50 pages est trivial, sur 5 millions รงa change tout. Nouveau module `picarones/core/cost_projection.py` : `ProjectedCost(engine_key, target_pages, cost_total_eur, co2_total_g, cost_per_1k_pages_eur, co2_per_1k_pages_g, type)`, `project_cost_total/co2_total` linรฉaire en pages avec `None` si donnรฉes insuffisantes ou target<0, `project_engine` retourne le ProjectedCost complet, `project_all_engines(engine_costs, target_pages)` projette N moteurs (ValueError si target<0, moteurs sans donnรฉes conservรฉs avec cost_total=None), `cost_gap_table(projections, baseline)` retourne `{engine: {total, delta_abs, delta_rel}}` vs baseline (KeyError si baseline inconnue, delta_rel=None si baseline=0). +17 tests (calcul 5 cas, COโ 2 cas, engine 2 cas, all_engines 3 cas, gap_table 4 cas, **cas rรฉaliste BnF 80 000 pages BMS** Tesseract=3.20โฌ/Pero=0โฌ/Mistral=280โฌ/GPT-4o=600โฌ). **Verrou levรฉ** : couche calcul prรชte pour cรขbler le panneau ยซ Avancรฉ ยป avec champ ยซ Volume cible ยป qui recalcule Pareto et table coรปt en valeur totale projetรฉe. UX HTML suivra. |
|
| 211 |
| 78 | **Sprint 47 du plan d'รฉvolution 2026 โ A.I.5 : รฉquivalences diplomatiques en curseur fin (couche de calcul)**. Les profils `DIPLOMATIC_*` de `normalization.py` appliquent un bloc entier ; un รฉditeur peut vouloir nuancer (ยซ je tolรจre ลฟโs mais pas uโv ยป). Nouveau module `picarones/core/equivalence_profile.py` : dataclass `EquivalenceRule(name, source, target, description, profile_tag)`, catalogue `BUILTIN_EQUIVALENCES` dรฉrivรฉ automatiquement des 4 profils intรฉgrรฉs avec 15 rรจgles canoniques nommรฉes (`longs_s`, `u_eq_v`, `i_eq_j`, `ae_ligature`, `thorn_th`, `vv_eq_w`, etc.), `list_equivalences_by_profile`, `apply_selected_equivalences(text, selected_names)` (rรจgles inconnues ignorรฉes + warning, texte vide โ ""), `compute_cer_with_equivalences(reference, hypothesis, selected_names)` qui applique les deux cรดtรฉs puis renvoie CER. Aucune modification de normalization.py โ purement additif. +17 tests (catalogue 4 cas, liste 3 cas, apply 6 cas dont sรฉlectif/exclu/multi/inconnue, compute_cer 4 cas dont application bilatรฉrale). **Verrou levรฉ** : la couche calcul est prรชte pour cรขbler le panneau ยซ Avancรฉ ยป du rapport avec cases ร cocher granulaires et recalcul JS client. UX (URL state + debounce) suivra dans un sprint dรฉdiรฉ. |
|
| 212 |
| 77 | **Sprint 46 du plan d'รฉvolution 2026 โ A.I.4 chantier 3 : taxonomie comparative cรดte-ร -cรดte (clรดture A.I.4)**. Troisiรจme chantier d'A.I.4. Rรฉpond ร ยซ deux moteurs ont le mรชme CER global, mais lequel fait des erreurs plus rรฉcupรฉrables ? ยป. Nouveau module `picarones/core/taxonomy_comparison.py` : `compare_taxonomies(engine_a, counts_a, engine_b, counts_b)` normalise en proportions, calcule deltas signรฉs, agrรจge par niveau de **rรฉcupรฉrabilitรฉ รฉditoriale** (recoverable: case/ligature/abbreviation ; difficult: diacritic/visual/hapax ; irrecoverable: lacuna/oov/segmentation). Constante `RECOVERABILITY` exportรฉe. Retourne None si vide. Nouveau module `picarones/report/taxonomy_comparison_render.py` : `build_taxonomy_comparison_html` produit titre + note + diagramme miroir SVG + tableau rรฉsumรฉ par catรฉgorie. `_build_mirror_chart_svg` server-side : ligne par classe, barres horizontales A ร gauche / B ร droite, รฉtiquette au centre, %, couleur selon rรฉcupรฉrabilitรฉ (vert/orange/rouge), รฉchelle normalisรฉe. `_build_recoverability_summary_html` : tableau 3ร2 avec pastilles colorรฉes. Adaptive : "" si None ou pas de classes. +6 clรฉs i18n FR/EN. +18 tests (calcul 7 cas dont sanitรฉ RECOVERABILITY couvre ERROR_CLASSES, rendu 7 cas, anti-injection, i18n). **Choix รฉditorial assumรฉ** : classification recoverable/difficult/irrecoverable est un guide pragmatique, pas un verdict โ note explicative dit ยซ ร CER รฉgal, un moteur dont les erreurs sont majoritairement vertes est prรฉfรฉrable pour une รฉdition critique ยป. **A.I.4 livrรฉ bout-en-bout** (Sprints 75-77). |
|
|
@@ -297,7 +298,7 @@ au template `_narrative_summary.html` (placรฉ entre `_header.html` et `_critical
|
|
| 297 |
## Contexte dรฉveloppement
|
| 298 |
|
| 299 |
- **Environnement** : GitHub Codespaces (`/workspaces/Picarones`), Python 3.12
|
| 300 |
-
- **Tests** :
|
| 301 |
- **Plan d'รฉvolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md)
|
| 302 |
- **Branche active** : `claude/analyze-project-evolution-KOA56`
|
| 303 |
- **Transcript de la conversation de dรฉveloppement** :
|
|
|
|
| 207 |
| 33 | **Sprint 2 du plan d'รฉvolution 2026 โ Phase 0.2 : interface module gรฉnรฉrique**. Nouveau module `picarones/core/modules.py` avec l'enum `ArtifactType` (IMAGE, TEXT, ALTO, PAGE, ENTITIES, READING_ORDER) et la classe abstraite `BaseModule` qui dรฉclare `input_types`/`output_types`, `execution_mode` (`"io"`/`"cpu"`), une mรฉthode `process(dict[ArtifactType, Any]) โ dict[ArtifactType, Any]`, et des helpers `validate_inputs`/`validate_outputs`. `BaseOCREngine` (`picarones/engines/base.py`) hรฉrite dรฉsormais de `BaseModule` avec `input_types=(IMAGE,)` et `output_types=(TEXT,)` ; sa nouvelle mรฉthode `process` wrappe l'API historique `run()`. Aucun adaptateur OCR existant n'est touchรฉ โ `test_engines.py` passe ร 20/20 sans modification. +23 tests dans `test_sprint33_module_interface.py` (contrat, validation, MockModule TEXTโALTO dรฉmonstratif comme demandรฉ par le plan, dรฉlรฉgation `BaseOCREngine.process โ run`, cohรฉrence ArtifactType/GTLevel). **Verrou levรฉ** : un mรชme runner peut maintenant exรฉcuter un OCR (imageโtexte), un mappeur VLMโALTO, un rewriter ALTOโALTO, un module NER (texteโentitรฉs), etc. โ fondation directe pour l'axe B du plan. |
|
| 208 |
| 34 | **Sprint 3 du plan d'รฉvolution 2026 โ Phase 0.3 : registre typรฉ de mรฉtriques (clรดture Phase 0)**. Nouveaux modules `picarones/core/metric_registry.py` (`MetricSpec`, `@register_metric`, `select_metrics`, `compute_at_junction`) et `picarones/core/builtin_metrics.py` qui enregistre `cer`, `wer`, `mer`, `wil` sur `(TEXT, TEXT)` plus un stub `text_preservation_after_reconstruction` sur `(TEXT, ALTO)` comme preuve de concept de jonction hรฉtรฉrogรจne. **Approche strictement additive** : ni `metrics.py` ni `compute_metrics` ne sont modifiรฉs, le rapport HTML reste identique octet par octet. La sรฉlection par signature de types est exacte (pas de coercion). +21 tests dans `test_sprint34_metric_registry.py`, dont une paritรฉ numรฉrique CER/WER/MER/WIL avec `compute_metrics` legacy ร 1e-9 prรจs sur 4 paires de textes. **Verrou levรฉ** : le runner d'une pipeline composรฉe peut maintenant calculer automatiquement la mรฉtrique adรฉquate ร chaque jonction de son DAG selon les types d'artefacts produits/attendus โ fondation directe pour la mรฉtrique d'absorption d'erreur (acte B.3) et toutes les mรฉtriques structurelles ร venir (Layout F1, reading order F1, NER). |
|
| 209 |
| 35 | **Sprint 4 du plan d'รฉvolution 2026 โ รtape 2 / axe A : mรฉtriques inter-moteurs (couche de calcul)**. Nouveau module `picarones/core/inter_engine.py` qui rรฉpond ร deux questions distinctes mais liรฉes : *(a) ร quel point les moteurs font-ils des erreurs de natures diffรฉrentes ?* via `kl_divergence`, `jensen_shannon_divergence` (symรฉtrique, bornรฉe `[0, 1]`), et `taxonomy_divergence_matrix` qui construit la matrice triangulaire inter-moteurs ; *(b) quel CER serait atteignable si on combinait les moteurs ?* via `oracle_token_recall` (proxy bag-of-words, borne supรฉrieure du recall atteignable), `complementarity_gap` (oracle vs meilleur moteur seul, gap absolu/relatif), et `pairwise_disagreement_rate`. Fonctions pures, sans I/O ni intรฉgration runner โ la couche de calcul est livrรฉe indรฉpendamment, le cรขblage narratif (`ENSEMBLE_OPPORTUNITY`) et HTML (matrice de divergence, badge oracle) suit au Sprint 36. +27 tests couvrant les invariants mathรฉmatiques (KL โฅ 0, KL(p,p) = 0, JS symรฉtrique et bornรฉe, oracle โฅ best_single, multiplicitรฉ respectรฉe), les cas concrets (deux moteurs spรฉcialisรฉs sortent comme candidats ensemble, complรฉmentaritรฉ parfaite atteint oracle = 1), et les garde-fous (rรฉfรฉrence vide, hypothรจses vides, mรฉtrique inconnue). |
|
| 210 |
+
| 80 | **Sprint 49 du plan d'รฉvolution 2026 โ A.I.7 : sur-normalisation lexicale (couche calcul + table HTML)**. Le dรฉtecteur `llm_hallucination_flag` (Sprint 19) signale via un score agrรฉgรฉ mais ne dit pas **quoi** corriger dans le prompt. Nouveau module `picarones/core/lexical_modernization.py` : `compute_lexical_modernization(reference, hypothesis, stop_list, case_sensitive)` aligne mot-ร -mot via `difflib.SequenceMatcher` et accumule par token GT `{n_total, n_modernized, rate_modernized, variants}` ; `aggregate_lexical_modernization` somme corpus-wide ; `top_modernized_tokens(data, n=20, min_total=1)` retourne les N tokens GT les plus modernisรฉs (tri dรฉcroissant par taux, tie-break par n_total, filtre anecdotiques via min_total). Stop-list paramรฉtrable (par dรฉfaut vide). Suppression GT โ variant โ
. Nouveau module `picarones/report/lexical_modernization_render.py` : `build_lexical_modernization_html(data, labels, top_n, min_total)` tableau 4 colonnes (forme GT, variantes OCR top-3, n GT, % modernisรฉ gradient blancโorange). Adaptive : "" si data None ou aucun modernisรฉ. +6 clรฉs i18n FR/EN. +20 tests (calcul 9 cas dont systรฉmatique/prรฉservรฉ/partiel/multi-variants/stop-list/casse/suppression/vide, agrรฉgation 2 cas, top 2 cas, rendu 5 cas dont anti-injection, complรฉtude i18n). **Verrou levรฉ** : le chercheur lit ยซ maistre โ maรฎtre modernisรฉ dans 100 % des cas ยป et ajuste son prompt โ info exploitable au lieu d'un score agrรฉgรฉ. |
|
| 211 |
| 79 | **Sprint 48 du plan d'รฉvolution 2026 โ A.I.6 : projection de coรปt en volume cible (couche de calcul)**. La vue Pareto (Sprint 20) trace CER vs coรปt mais le coรปt est par unitรฉ (1 000 pages) ; payer 50 โฌ de plus sur 50 pages est trivial, sur 5 millions รงa change tout. Nouveau module `picarones/core/cost_projection.py` : `ProjectedCost(engine_key, target_pages, cost_total_eur, co2_total_g, cost_per_1k_pages_eur, co2_per_1k_pages_g, type)`, `project_cost_total/co2_total` linรฉaire en pages avec `None` si donnรฉes insuffisantes ou target<0, `project_engine` retourne le ProjectedCost complet, `project_all_engines(engine_costs, target_pages)` projette N moteurs (ValueError si target<0, moteurs sans donnรฉes conservรฉs avec cost_total=None), `cost_gap_table(projections, baseline)` retourne `{engine: {total, delta_abs, delta_rel}}` vs baseline (KeyError si baseline inconnue, delta_rel=None si baseline=0). +17 tests (calcul 5 cas, COโ 2 cas, engine 2 cas, all_engines 3 cas, gap_table 4 cas, **cas rรฉaliste BnF 80 000 pages BMS** Tesseract=3.20โฌ/Pero=0โฌ/Mistral=280โฌ/GPT-4o=600โฌ). **Verrou levรฉ** : couche calcul prรชte pour cรขbler le panneau ยซ Avancรฉ ยป avec champ ยซ Volume cible ยป qui recalcule Pareto et table coรปt en valeur totale projetรฉe. UX HTML suivra. |
|
| 212 |
| 78 | **Sprint 47 du plan d'รฉvolution 2026 โ A.I.5 : รฉquivalences diplomatiques en curseur fin (couche de calcul)**. Les profils `DIPLOMATIC_*` de `normalization.py` appliquent un bloc entier ; un รฉditeur peut vouloir nuancer (ยซ je tolรจre ลฟโs mais pas uโv ยป). Nouveau module `picarones/core/equivalence_profile.py` : dataclass `EquivalenceRule(name, source, target, description, profile_tag)`, catalogue `BUILTIN_EQUIVALENCES` dรฉrivรฉ automatiquement des 4 profils intรฉgrรฉs avec 15 rรจgles canoniques nommรฉes (`longs_s`, `u_eq_v`, `i_eq_j`, `ae_ligature`, `thorn_th`, `vv_eq_w`, etc.), `list_equivalences_by_profile`, `apply_selected_equivalences(text, selected_names)` (rรจgles inconnues ignorรฉes + warning, texte vide โ ""), `compute_cer_with_equivalences(reference, hypothesis, selected_names)` qui applique les deux cรดtรฉs puis renvoie CER. Aucune modification de normalization.py โ purement additif. +17 tests (catalogue 4 cas, liste 3 cas, apply 6 cas dont sรฉlectif/exclu/multi/inconnue, compute_cer 4 cas dont application bilatรฉrale). **Verrou levรฉ** : la couche calcul est prรชte pour cรขbler le panneau ยซ Avancรฉ ยป du rapport avec cases ร cocher granulaires et recalcul JS client. UX (URL state + debounce) suivra dans un sprint dรฉdiรฉ. |
|
| 213 |
| 77 | **Sprint 46 du plan d'รฉvolution 2026 โ A.I.4 chantier 3 : taxonomie comparative cรดte-ร -cรดte (clรดture A.I.4)**. Troisiรจme chantier d'A.I.4. Rรฉpond ร ยซ deux moteurs ont le mรชme CER global, mais lequel fait des erreurs plus rรฉcupรฉrables ? ยป. Nouveau module `picarones/core/taxonomy_comparison.py` : `compare_taxonomies(engine_a, counts_a, engine_b, counts_b)` normalise en proportions, calcule deltas signรฉs, agrรจge par niveau de **rรฉcupรฉrabilitรฉ รฉditoriale** (recoverable: case/ligature/abbreviation ; difficult: diacritic/visual/hapax ; irrecoverable: lacuna/oov/segmentation). Constante `RECOVERABILITY` exportรฉe. Retourne None si vide. Nouveau module `picarones/report/taxonomy_comparison_render.py` : `build_taxonomy_comparison_html` produit titre + note + diagramme miroir SVG + tableau rรฉsumรฉ par catรฉgorie. `_build_mirror_chart_svg` server-side : ligne par classe, barres horizontales A ร gauche / B ร droite, รฉtiquette au centre, %, couleur selon rรฉcupรฉrabilitรฉ (vert/orange/rouge), รฉchelle normalisรฉe. `_build_recoverability_summary_html` : tableau 3ร2 avec pastilles colorรฉes. Adaptive : "" si None ou pas de classes. +6 clรฉs i18n FR/EN. +18 tests (calcul 7 cas dont sanitรฉ RECOVERABILITY couvre ERROR_CLASSES, rendu 7 cas, anti-injection, i18n). **Choix รฉditorial assumรฉ** : classification recoverable/difficult/irrecoverable est un guide pragmatique, pas un verdict โ note explicative dit ยซ ร CER รฉgal, un moteur dont les erreurs sont majoritairement vertes est prรฉfรฉrable pour une รฉdition critique ยป. **A.I.4 livrรฉ bout-en-bout** (Sprints 75-77). |
|
|
|
|
| 298 |
## Contexte dรฉveloppement
|
| 299 |
|
| 300 |
- **Environnement** : GitHub Codespaces (`/workspaces/Picarones`), Python 3.12
|
| 301 |
+
- **Tests** : 2699 passed, 2 skipped (Sprints 32-34 = Phase 0 close ; Sprints 35-37 = inter-moteurs livrรฉs bout-en-bout ; Sprints 38+40+41 = NER livrรฉ bout-en-bout ; Sprints 39+42+43 = calibration livrรฉe bout-en-bout cรดtรฉ rapport ; Sprint 44 = mรฉdiane par dรฉfaut ; Sprints 45+46 = stratification A.III livrรฉe bout-en-bout ; Sprints 47-51 = les 5 adapters OCR exposent leurs confidences natives ; **รtape 2 close** ; Sprints 52-54 = axe A.II.2 (mรฉtriques structurelles) couches de calcul intรฉgralement livrรฉes ; Sprints 55-62 = extension philologique livrรฉe bout-en-bout sur trois pรฉriodes + numรฉraux romains transversaux + cรขblage runner adaptive + vue HTML ยซ Profil philologique ยป ; Sprints 63-70 = axe B livrรฉ bout-en-bout ; Sprints 71-72 = A.I.1 livrรฉ bout-en-bout ; Sprints 73-74 = A.I.3 livrรฉ bout-en-bout ; Sprints 75-77 = A.I.4 livrรฉ bout-en-bout ; Sprint 78 = A.I.5 couche calcul ; Sprint 79 = A.I.6 couche calcul ; **Sprint 80 = A.I.7 โ sur-normalisation lexicale couche calcul + table HTML**)
|
| 302 |
- **Plan d'รฉvolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md)
|
| 303 |
- **Branche active** : `claude/analyze-project-evolution-KOA56`
|
| 304 |
- **Transcript de la conversation de dรฉveloppement** :
|
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dรฉtection de la sur-normalisation lexicale par les LLM/VLM โ
|
| 2 |
+
Sprint 80 (A.I.7).
|
| 3 |
+
|
| 4 |
+
Sprint 80 โ A.I.7 du plan d'รฉvolution 2026.
|
| 5 |
+
|
| 6 |
+
Pourquoi ce module
|
| 7 |
+
------------------
|
| 8 |
+
Le dรฉtecteur ``llm_hallucination_flag`` (Sprint 19) signale qu'un
|
| 9 |
+
moteur sur-normalise (ยซ 0,05 % ยป). Mais ce score agrรฉgรฉ ne dit
|
| 10 |
+
rien sur **quoi** corriger dans le prompt. Ce module produit
|
| 11 |
+
une **table de frรฉquences dรฉtaillรฉe** :
|
| 12 |
+
|
| 13 |
+
+----------------------+--------------------+------+----------+
|
| 14 |
+
| Forme historique GT | Forme modernisรฉe | n GT | % modern |
|
| 15 |
+
+======================+====================+======+==========+
|
| 16 |
+
| maistre | maรฎtre | 47 | 85 % |
|
| 17 |
+
| nostre | nostre | 92 | 8 % |
|
| 18 |
+
| veoir | voir | 23 | 100 % |
|
| 19 |
+
+----------------------+--------------------+------+----------+
|
| 20 |
+
|
| 21 |
+
Lecture immรฉdiate : *ยซ le LLM modernise systรฉmatiquement
|
| 22 |
+
maistre โ maรฎtre ; pour prรฉserver l'orthographe historique, ajouter
|
| 23 |
+
au prompt "ne pas moderniser maistre, nostre, veoir" ยป*.
|
| 24 |
+
|
| 25 |
+
Mรฉthode
|
| 26 |
+
-------
|
| 27 |
+
Alignement mot-ร -mot via ``difflib.SequenceMatcher``. Chaque
|
| 28 |
+
``replace`` ou ``equal`` produit une paire ``(gt_token,
|
| 29 |
+
hyp_token)``. On accumule pour chaque ``gt_token`` :
|
| 30 |
+
|
| 31 |
+
- ``n_total`` : nombre d'occurrences du token dans la GT
|
| 32 |
+
- ``n_modernized`` : nombre d'occurrences oรน ``hyp_token != gt_token``
|
| 33 |
+
- ``variants`` : dict des hyp_tokens observรฉs avec leur count
|
| 34 |
+
|
| 35 |
+
Stop-list
|
| 36 |
+
---------
|
| 37 |
+
L'utilisateur peut passer ``stop_list`` (ensemble de tokens GT ร
|
| 38 |
+
ignorer). Par dรฉfaut, vide โ le module ne tente pas de deviner ce
|
| 39 |
+
qui est ยซ moderne ยป ou ยซ historique ยป, c'est au chercheur de
|
| 40 |
+
fournir le filtre adaptรฉ ร son corpus.
|
| 41 |
+
|
| 42 |
+
Sortie
|
| 43 |
+
------
|
| 44 |
+
``compute_lexical_modernization`` retourne une structure adaptรฉe
|
| 45 |
+
au rendu HTML. ``aggregate_lexical_modernization`` agrรจge
|
| 46 |
+
plusieurs documents.
|
| 47 |
+
|
| 48 |
+
Limites documentรฉes
|
| 49 |
+
-------------------
|
| 50 |
+
- Tokenisation au niveau mot (split sur espace) โ cohรฉrent avec
|
| 51 |
+
``taxonomy.py`` et autres modules. Pas de stemming ni de
|
| 52 |
+
lemmatisation.
|
| 53 |
+
- La mรฉtrique mesure la **rรฉรฉcriture lexicale** ; elle n'attrape
|
| 54 |
+
pas les modernisations infra-mot (perte du s long ลฟ qui se
|
| 55 |
+
fond dans la mรชme forme). Pour รงa, voir ``early_modern_typography``
|
| 56 |
+
(Sprint 58) et ``equivalence_profile`` (Sprint 78).
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
from __future__ import annotations
|
| 60 |
+
|
| 61 |
+
import difflib
|
| 62 |
+
import logging
|
| 63 |
+
from typing import Iterable, Optional
|
| 64 |
+
|
| 65 |
+
logger = logging.getLogger(__name__)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _split_words(text: Optional[str]) -> list[str]:
|
| 69 |
+
"""Tokenisation simple par split sur whitespace."""
|
| 70 |
+
if not text:
|
| 71 |
+
return []
|
| 72 |
+
return text.split()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def compute_lexical_modernization(
|
| 76 |
+
reference: Optional[str],
|
| 77 |
+
hypothesis: Optional[str],
|
| 78 |
+
*,
|
| 79 |
+
stop_list: Optional[Iterable[str]] = None,
|
| 80 |
+
case_sensitive: bool = False,
|
| 81 |
+
) -> dict:
|
| 82 |
+
"""Calcule le tableau de modernisation lexicale pour un document.
|
| 83 |
+
|
| 84 |
+
Returns
|
| 85 |
+
-------
|
| 86 |
+
dict
|
| 87 |
+
``{
|
| 88 |
+
"n_gt_tokens": int,
|
| 89 |
+
"tokens": {
|
| 90 |
+
gt_token: {
|
| 91 |
+
"n_total": int,
|
| 92 |
+
"n_modernized": int,
|
| 93 |
+
"rate_modernized": float, # โ [0, 1]
|
| 94 |
+
"variants": {hyp_token: count, ...},
|
| 95 |
+
},
|
| 96 |
+
...
|
| 97 |
+
},
|
| 98 |
+
}``
|
| 99 |
+
Si ``reference`` est vide โ ``tokens == {}``.
|
| 100 |
+
"""
|
| 101 |
+
ref_tokens = _split_words(reference)
|
| 102 |
+
hyp_tokens = _split_words(hypothesis)
|
| 103 |
+
if not ref_tokens:
|
| 104 |
+
return {"n_gt_tokens": 0, "tokens": {}}
|
| 105 |
+
|
| 106 |
+
if not case_sensitive:
|
| 107 |
+
ref_for_match = [t.lower() for t in ref_tokens]
|
| 108 |
+
hyp_for_match = [t.lower() for t in hyp_tokens]
|
| 109 |
+
else:
|
| 110 |
+
ref_for_match = ref_tokens
|
| 111 |
+
hyp_for_match = hyp_tokens
|
| 112 |
+
|
| 113 |
+
stop = frozenset(
|
| 114 |
+
(t.lower() if not case_sensitive else t)
|
| 115 |
+
for t in (stop_list or [])
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# On accumule par gt_token (forme display = forme originale,
|
| 119 |
+
# match key = forme casรฉe selon ``case_sensitive``).
|
| 120 |
+
tokens_data: dict[str, dict] = {}
|
| 121 |
+
|
| 122 |
+
matcher = difflib.SequenceMatcher(
|
| 123 |
+
None, ref_for_match, hyp_for_match, autojunk=False,
|
| 124 |
+
)
|
| 125 |
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
| 126 |
+
if tag == "equal":
|
| 127 |
+
for k in range(i2 - i1):
|
| 128 |
+
gt_orig = ref_tokens[i1 + k]
|
| 129 |
+
gt_match = ref_for_match[i1 + k]
|
| 130 |
+
if gt_match in stop:
|
| 131 |
+
continue
|
| 132 |
+
slot = tokens_data.setdefault(
|
| 133 |
+
gt_orig,
|
| 134 |
+
{"n_total": 0, "n_modernized": 0, "variants": {}},
|
| 135 |
+
)
|
| 136 |
+
slot["n_total"] += 1
|
| 137 |
+
elif tag == "replace":
|
| 138 |
+
# Apparier 1-ร -1 quand possible
|
| 139 |
+
paired = min(i2 - i1, j2 - j1)
|
| 140 |
+
for k in range(paired):
|
| 141 |
+
gt_orig = ref_tokens[i1 + k]
|
| 142 |
+
gt_match = ref_for_match[i1 + k]
|
| 143 |
+
if gt_match in stop:
|
| 144 |
+
continue
|
| 145 |
+
hyp_orig = hyp_tokens[j1 + k]
|
| 146 |
+
slot = tokens_data.setdefault(
|
| 147 |
+
gt_orig,
|
| 148 |
+
{"n_total": 0, "n_modernized": 0, "variants": {}},
|
| 149 |
+
)
|
| 150 |
+
slot["n_total"] += 1
|
| 151 |
+
slot["n_modernized"] += 1
|
| 152 |
+
slot["variants"][hyp_orig] = slot["variants"].get(hyp_orig, 0) + 1
|
| 153 |
+
# Si plus de gt que de hyp, le reste des gt_tokens est
|
| 154 |
+
# ยซ perdu ยป โ on les compte comme totaux mais pas comme
|
| 155 |
+
# modernisรฉs (on ne sait pas en quoi).
|
| 156 |
+
for k in range(paired, i2 - i1):
|
| 157 |
+
gt_orig = ref_tokens[i1 + k]
|
| 158 |
+
gt_match = ref_for_match[i1 + k]
|
| 159 |
+
if gt_match in stop:
|
| 160 |
+
continue
|
| 161 |
+
slot = tokens_data.setdefault(
|
| 162 |
+
gt_orig,
|
| 163 |
+
{"n_total": 0, "n_modernized": 0, "variants": {}},
|
| 164 |
+
)
|
| 165 |
+
slot["n_total"] += 1
|
| 166 |
+
slot["n_modernized"] += 1
|
| 167 |
+
slot["variants"]["โ
"] = slot["variants"].get("โ
", 0) + 1
|
| 168 |
+
elif tag == "delete":
|
| 169 |
+
# gt prรฉsent, pas en hyp โ modernisation par
|
| 170 |
+
# suppression (ou perte pure)
|
| 171 |
+
for k in range(i2 - i1):
|
| 172 |
+
gt_orig = ref_tokens[i1 + k]
|
| 173 |
+
gt_match = ref_for_match[i1 + k]
|
| 174 |
+
if gt_match in stop:
|
| 175 |
+
continue
|
| 176 |
+
slot = tokens_data.setdefault(
|
| 177 |
+
gt_orig,
|
| 178 |
+
{"n_total": 0, "n_modernized": 0, "variants": {}},
|
| 179 |
+
)
|
| 180 |
+
slot["n_total"] += 1
|
| 181 |
+
slot["n_modernized"] += 1
|
| 182 |
+
slot["variants"]["โ
"] = slot["variants"].get("โ
", 0) + 1
|
| 183 |
+
|
| 184 |
+
# Calcul du taux par token
|
| 185 |
+
for slot in tokens_data.values():
|
| 186 |
+
total = slot["n_total"]
|
| 187 |
+
slot["rate_modernized"] = (
|
| 188 |
+
slot["n_modernized"] / total if total > 0 else 0.0
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"n_gt_tokens": len(ref_tokens),
|
| 193 |
+
"tokens": tokens_data,
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def aggregate_lexical_modernization(
|
| 198 |
+
per_doc_results: Iterable[dict],
|
| 199 |
+
) -> dict:
|
| 200 |
+
"""Agrรจge des ``compute_lexical_modernization`` per-doc.
|
| 201 |
+
|
| 202 |
+
Renvoie la structure agrรฉgรฉe corpus-wide avec la mรชme forme
|
| 203 |
+
que ``compute_lexical_modernization``.
|
| 204 |
+
"""
|
| 205 |
+
agg_tokens: dict[str, dict] = {}
|
| 206 |
+
n_gt_total = 0
|
| 207 |
+
for doc_result in per_doc_results:
|
| 208 |
+
if not doc_result:
|
| 209 |
+
continue
|
| 210 |
+
n_gt_total += doc_result.get("n_gt_tokens", 0)
|
| 211 |
+
for gt, data in (doc_result.get("tokens") or {}).items():
|
| 212 |
+
slot = agg_tokens.setdefault(
|
| 213 |
+
gt, {"n_total": 0, "n_modernized": 0, "variants": {}},
|
| 214 |
+
)
|
| 215 |
+
slot["n_total"] += data.get("n_total", 0)
|
| 216 |
+
slot["n_modernized"] += data.get("n_modernized", 0)
|
| 217 |
+
for hyp_t, count in (data.get("variants") or {}).items():
|
| 218 |
+
slot["variants"][hyp_t] = slot["variants"].get(hyp_t, 0) + count
|
| 219 |
+
|
| 220 |
+
for slot in agg_tokens.values():
|
| 221 |
+
total = slot["n_total"]
|
| 222 |
+
slot["rate_modernized"] = (
|
| 223 |
+
slot["n_modernized"] / total if total > 0 else 0.0
|
| 224 |
+
)
|
| 225 |
+
return {
|
| 226 |
+
"n_gt_tokens": n_gt_total,
|
| 227 |
+
"tokens": agg_tokens,
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def top_modernized_tokens(
|
| 232 |
+
data: dict,
|
| 233 |
+
*,
|
| 234 |
+
n: int = 20,
|
| 235 |
+
min_total: int = 1,
|
| 236 |
+
) -> list[tuple[str, dict]]:
|
| 237 |
+
"""Top-N tokens GT par taux de modernisation.
|
| 238 |
+
|
| 239 |
+
Filtre les tokens dont ``n_total < min_total`` (anecdotiques).
|
| 240 |
+
Tri par ``rate_modernized`` dรฉcroissant, tie-break par
|
| 241 |
+
``n_total`` dรฉcroissant.
|
| 242 |
+
"""
|
| 243 |
+
tokens = data.get("tokens") or {}
|
| 244 |
+
candidates = [
|
| 245 |
+
(gt, slot) for gt, slot in tokens.items()
|
| 246 |
+
if slot.get("n_total", 0) >= min_total
|
| 247 |
+
and slot.get("n_modernized", 0) > 0
|
| 248 |
+
]
|
| 249 |
+
candidates.sort(
|
| 250 |
+
key=lambda pair: (
|
| 251 |
+
-pair[1].get("rate_modernized", 0.0),
|
| 252 |
+
-pair[1].get("n_total", 0),
|
| 253 |
+
pair[0],
|
| 254 |
+
),
|
| 255 |
+
)
|
| 256 |
+
return candidates[:n]
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
__all__ = [
|
| 260 |
+
"compute_lexical_modernization",
|
| 261 |
+
"aggregate_lexical_modernization",
|
| 262 |
+
"top_modernized_tokens",
|
| 263 |
+
]
|
|
@@ -256,5 +256,11 @@
|
|
| 256 |
"taxocomp_level_label": "Category",
|
| 257 |
"taxocomp_recoverable": "Recoverable",
|
| 258 |
"taxocomp_difficult": "Difficult",
|
| 259 |
-
"taxocomp_irrecoverable": "Irrecoverable"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
}
|
|
|
|
| 256 |
"taxocomp_level_label": "Category",
|
| 257 |
"taxocomp_recoverable": "Recoverable",
|
| 258 |
"taxocomp_difficult": "Difficult",
|
| 259 |
+
"taxocomp_irrecoverable": "Irrecoverable",
|
| 260 |
+
"lexmod_title": "Lexical modernization (top tokens)",
|
| 261 |
+
"lexmod_note": "GT tokens that the engine rewrites most often. Reading: ยซ maistre โ maรฎtre modernized in 85 % of cases ยป tells you what to fix in the prompt to preserve historical spelling.",
|
| 262 |
+
"lexmod_gt_label": "Historical GT form",
|
| 263 |
+
"lexmod_hyp_label": "OCR variants",
|
| 264 |
+
"lexmod_n_label": "n GT",
|
| 265 |
+
"lexmod_rate_label": "% modernized"
|
| 266 |
}
|
|
@@ -256,5 +256,11 @@
|
|
| 256 |
"taxocomp_level_label": "Catรฉgorie",
|
| 257 |
"taxocomp_recoverable": "Rรฉcupรฉrable",
|
| 258 |
"taxocomp_difficult": "Difficile",
|
| 259 |
-
"taxocomp_irrecoverable": "Irrรฉcupรฉrable"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
}
|
|
|
|
| 256 |
"taxocomp_level_label": "Catรฉgorie",
|
| 257 |
"taxocomp_recoverable": "Rรฉcupรฉrable",
|
| 258 |
"taxocomp_difficult": "Difficile",
|
| 259 |
+
"taxocomp_irrecoverable": "Irrรฉcupรฉrable",
|
| 260 |
+
"lexmod_title": "Modernisation lexicale (top tokens)",
|
| 261 |
+
"lexmod_note": "Tokens GT que le moteur rรฉรฉcrit le plus souvent. Lecture : ยซ maistre โ maรฎtre modernisรฉ dans 85 % des cas ยป indique de quoi corriger dans le prompt pour prรฉserver l'orthographe historique.",
|
| 262 |
+
"lexmod_gt_label": "Forme historique GT",
|
| 263 |
+
"lexmod_hyp_label": "Variantes OCR",
|
| 264 |
+
"lexmod_n_label": "n GT",
|
| 265 |
+
"lexmod_rate_label": "% modernisรฉ"
|
| 266 |
}
|
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rendu HTML de la vue ยซ Modernisation lexicale ยป โ Sprint 80.
|
| 2 |
+
|
| 3 |
+
A.I.7 du plan d'รฉvolution 2026.
|
| 4 |
+
|
| 5 |
+
Suite directe ``picarones/core/lexical_modernization.py``.
|
| 6 |
+
Pattern identique aux autres rendus (Sprints 41/43/62/67/72/74/75/76/77) :
|
| 7 |
+
**server-side**, pas de JavaScript, anti-injection systรฉmatique.
|
| 8 |
+
|
| 9 |
+
Vue
|
| 10 |
+
---
|
| 11 |
+
Tableau triรฉ par taux de modernisation dรฉcroissant : forme
|
| 12 |
+
historique GT โ forme(s) modernisรฉe(s), occurrences GT, %.
|
| 13 |
+
Couleur de cellule pour le %.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
from html import escape as _e
|
| 19 |
+
from typing import Optional
|
| 20 |
+
|
| 21 |
+
from picarones.core.lexical_modernization import top_modernized_tokens
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _color_for_rate(rate: float) -> str:
|
| 25 |
+
"""Gradient blanc โ orange profond pour rate โ [0, 1]."""
|
| 26 |
+
f = max(0.0, min(1.0, rate))
|
| 27 |
+
r = int(255 + (194 - 255) * f)
|
| 28 |
+
g = int(255 + (65 - 255) * f)
|
| 29 |
+
b = int(255 + (12 - 255) * f)
|
| 30 |
+
return f"#{r:02x}{g:02x}{b:02x}"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _format_variants(variants: dict, max_show: int = 3) -> str:
|
| 34 |
+
"""Liste compacte des variants modernisรฉs."""
|
| 35 |
+
items = sorted(variants.items(), key=lambda kv: -kv[1])
|
| 36 |
+
shown = items[:max_show]
|
| 37 |
+
rest = len(items) - max_show
|
| 38 |
+
parts = [
|
| 39 |
+
f"{_e(form)} ({count})"
|
| 40 |
+
for form, count in shown
|
| 41 |
+
]
|
| 42 |
+
if rest > 0:
|
| 43 |
+
parts.append(f"+{rest}")
|
| 44 |
+
return ", ".join(parts)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def build_lexical_modernization_html(
|
| 48 |
+
data: Optional[dict],
|
| 49 |
+
labels: Optional[dict[str, str]] = None,
|
| 50 |
+
*,
|
| 51 |
+
top_n: int = 20,
|
| 52 |
+
min_total: int = 1,
|
| 53 |
+
) -> str:
|
| 54 |
+
"""Construit la table HTML de modernisation lexicale.
|
| 55 |
+
|
| 56 |
+
Retourne ``""`` si ``data is None`` ou si aucun token modernisรฉ.
|
| 57 |
+
"""
|
| 58 |
+
if not data:
|
| 59 |
+
return ""
|
| 60 |
+
rows = top_modernized_tokens(data, n=top_n, min_total=min_total)
|
| 61 |
+
if not rows:
|
| 62 |
+
return ""
|
| 63 |
+
labels = labels or {}
|
| 64 |
+
title = labels.get(
|
| 65 |
+
"lexmod_title", "Modernisation lexicale (top tokens)",
|
| 66 |
+
)
|
| 67 |
+
note = labels.get(
|
| 68 |
+
"lexmod_note",
|
| 69 |
+
"Tokens GT que le moteur rรฉรฉcrit le plus souvent. "
|
| 70 |
+
"Lecture : ยซ maistre โ maรฎtre modernisรฉ dans 85 % des cas ยป "
|
| 71 |
+
"indique de quoi corriger dans le prompt pour prรฉserver "
|
| 72 |
+
"l'orthographe historique.",
|
| 73 |
+
)
|
| 74 |
+
gt_label = labels.get("lexmod_gt_label", "Forme historique GT")
|
| 75 |
+
hyp_label = labels.get("lexmod_hyp_label", "Variantes OCR")
|
| 76 |
+
n_label = labels.get("lexmod_n_label", "n GT")
|
| 77 |
+
rate_label = labels.get("lexmod_rate_label", "% modernisรฉ")
|
| 78 |
+
|
| 79 |
+
parts = [
|
| 80 |
+
'<div class="lexmod" style="margin:1rem 0">',
|
| 81 |
+
f'<div style="font-weight:600;margin-bottom:.4rem">{_e(title)}</div>',
|
| 82 |
+
f'<div style="font-size:.85rem;opacity:.75;margin-bottom:.5rem">'
|
| 83 |
+
f'{_e(note)}</div>',
|
| 84 |
+
'<table style="border-collapse:collapse;width:100%;'
|
| 85 |
+
'font-size:.85rem">',
|
| 86 |
+
'<thead><tr>',
|
| 87 |
+
]
|
| 88 |
+
for col in (gt_label, hyp_label, n_label, rate_label):
|
| 89 |
+
parts.append(
|
| 90 |
+
f'<th style="padding:.3rem .5rem;text-align:left;'
|
| 91 |
+
f'border-bottom:1px solid #ccc;font-weight:600">'
|
| 92 |
+
f'{_e(col)}</th>'
|
| 93 |
+
)
|
| 94 |
+
parts.append("</tr></thead><tbody>")
|
| 95 |
+
for gt_token, slot in rows:
|
| 96 |
+
rate = slot.get("rate_modernized", 0.0)
|
| 97 |
+
n_total = slot.get("n_total", 0)
|
| 98 |
+
variants_str = _format_variants(slot.get("variants") or {})
|
| 99 |
+
rate_color = _color_for_rate(rate)
|
| 100 |
+
parts.append(
|
| 101 |
+
f'<tr>'
|
| 102 |
+
f'<td style="padding:.3rem .5rem;font-family:monospace">'
|
| 103 |
+
f'{_e(gt_token)}</td>'
|
| 104 |
+
f'<td style="padding:.3rem .5rem;font-size:.85rem">'
|
| 105 |
+
f'{variants_str}</td>'
|
| 106 |
+
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 107 |
+
f'font-family:monospace">{n_total}</td>'
|
| 108 |
+
f'<td style="padding:.3rem .5rem;text-align:right;'
|
| 109 |
+
f'background:{rate_color};font-family:monospace">'
|
| 110 |
+
f'{rate * 100:.0f}%</td>'
|
| 111 |
+
f'</tr>'
|
| 112 |
+
)
|
| 113 |
+
parts.append("</tbody></table></div>")
|
| 114 |
+
return "".join(parts)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
__all__ = [
|
| 118 |
+
"build_lexical_modernization_html",
|
| 119 |
+
]
|
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests Sprint 80 โ A.I.7 : sur-normalisation lexicale.
|
| 2 |
+
|
| 3 |
+
Couvre :
|
| 4 |
+
|
| 5 |
+
1. ``compute_lexical_modernization`` :
|
| 6 |
+
- Token GT modernisรฉ systรฉmatiquement โ 100 %
|
| 7 |
+
- Token GT prรฉservรฉ โ 0 %
|
| 8 |
+
- Plusieurs variantes hyp pour un mรชme gt
|
| 9 |
+
- Stop-list filtre les tokens
|
| 10 |
+
- Casse insensible par dรฉfaut
|
| 11 |
+
- Token GT supprimรฉ (lacuna) โ modernisรฉ vers โ
|
| 12 |
+
- GT vide โ tokens vide
|
| 13 |
+
2. ``aggregate_lexical_modernization`` :
|
| 14 |
+
- Somme correcte sur N docs
|
| 15 |
+
3. ``top_modernized_tokens`` :
|
| 16 |
+
- Tri dรฉcroissant par rate
|
| 17 |
+
- ``min_total`` filtre les anecdotiques
|
| 18 |
+
- Tokens ร 0 % exclus
|
| 19 |
+
4. Rendu HTML :
|
| 20 |
+
- Tableau, ``""`` si data None ou aucun modernisรฉ
|
| 21 |
+
- Anti-injection
|
| 22 |
+
5. Complรฉtude i18n FR/EN.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import json
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
|
| 30 |
+
from picarones.core.lexical_modernization import (
|
| 31 |
+
aggregate_lexical_modernization,
|
| 32 |
+
compute_lexical_modernization,
|
| 33 |
+
top_modernized_tokens,
|
| 34 |
+
)
|
| 35 |
+
from picarones.report.lexical_modernization_render import (
|
| 36 |
+
build_lexical_modernization_html,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 41 |
+
# 1. compute_lexical_modernization
|
| 42 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class TestCompute:
|
| 46 |
+
def test_systematic_modernization(self) -> None:
|
| 47 |
+
gt = "maistre maistre maistre"
|
| 48 |
+
hyp = "maรฎtre maรฎtre maรฎtre"
|
| 49 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 50 |
+
slot = result["tokens"]["maistre"]
|
| 51 |
+
assert slot["n_total"] == 3
|
| 52 |
+
assert slot["n_modernized"] == 3
|
| 53 |
+
assert slot["rate_modernized"] == 1.0
|
| 54 |
+
assert slot["variants"] == {"maรฎtre": 3}
|
| 55 |
+
|
| 56 |
+
def test_preserved_token(self) -> None:
|
| 57 |
+
gt = "nostre nostre"
|
| 58 |
+
hyp = "nostre nostre"
|
| 59 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 60 |
+
slot = result["tokens"]["nostre"]
|
| 61 |
+
assert slot["n_total"] == 2
|
| 62 |
+
assert slot["n_modernized"] == 0
|
| 63 |
+
assert slot["rate_modernized"] == 0.0
|
| 64 |
+
|
| 65 |
+
def test_partial_modernization(self) -> None:
|
| 66 |
+
gt = "maistre maistre maistre maistre"
|
| 67 |
+
hyp = "maรฎtre maistre maรฎtre maรฎtre"
|
| 68 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 69 |
+
slot = result["tokens"]["maistre"]
|
| 70 |
+
assert slot["n_total"] == 4
|
| 71 |
+
assert slot["n_modernized"] == 3
|
| 72 |
+
assert slot["rate_modernized"] == 0.75
|
| 73 |
+
|
| 74 |
+
def test_multiple_variants(self) -> None:
|
| 75 |
+
gt = "veoir veoir veoir"
|
| 76 |
+
hyp = "voir voyr voir"
|
| 77 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 78 |
+
slot = result["tokens"]["veoir"]
|
| 79 |
+
assert slot["n_total"] == 3
|
| 80 |
+
assert slot["n_modernized"] == 3
|
| 81 |
+
assert slot["variants"] == {"voir": 2, "voyr": 1}
|
| 82 |
+
|
| 83 |
+
def test_stop_list_filter(self) -> None:
|
| 84 |
+
gt = "maistre le veoir"
|
| 85 |
+
hyp = "maรฎtre la voir"
|
| 86 |
+
result = compute_lexical_modernization(
|
| 87 |
+
gt, hyp, stop_list=["le"],
|
| 88 |
+
)
|
| 89 |
+
# ยซ le ยป filtrรฉ, mais maistre et veoir prรฉsents
|
| 90 |
+
assert "le" not in result["tokens"]
|
| 91 |
+
assert "maistre" in result["tokens"]
|
| 92 |
+
assert "veoir" in result["tokens"]
|
| 93 |
+
|
| 94 |
+
def test_case_insensitive_default(self) -> None:
|
| 95 |
+
gt = "Maistre maistre"
|
| 96 |
+
hyp = "Maรฎtre maรฎtre"
|
| 97 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 98 |
+
# Les deux formes sont distinctes en sortie display mais
|
| 99 |
+
# appariรฉes correctement en match
|
| 100 |
+
assert result["tokens"]["Maistre"]["n_modernized"] == 1
|
| 101 |
+
assert result["tokens"]["maistre"]["n_modernized"] == 1
|
| 102 |
+
|
| 103 |
+
def test_deletion_counted_as_modernized(self) -> None:
|
| 104 |
+
gt = "maistre veoir"
|
| 105 |
+
hyp = "maรฎtre" # veoir manque
|
| 106 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 107 |
+
# veoir โ โ
comptรฉ comme modernisรฉ
|
| 108 |
+
slot = result["tokens"]["veoir"]
|
| 109 |
+
assert slot["n_modernized"] == 1
|
| 110 |
+
assert "โ
" in slot["variants"]
|
| 111 |
+
|
| 112 |
+
def test_empty_gt(self) -> None:
|
| 113 |
+
result = compute_lexical_modernization("", "anything")
|
| 114 |
+
assert result["tokens"] == {}
|
| 115 |
+
assert result["n_gt_tokens"] == 0
|
| 116 |
+
|
| 117 |
+
def test_none_inputs(self) -> None:
|
| 118 |
+
result = compute_lexical_modernization(None, None)
|
| 119 |
+
assert result["tokens"] == {}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 123 |
+
# 2. aggregate
|
| 124 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class TestAggregate:
|
| 128 |
+
def test_sum_across_docs(self) -> None:
|
| 129 |
+
d1 = compute_lexical_modernization(
|
| 130 |
+
"maistre maistre", "maรฎtre maรฎtre",
|
| 131 |
+
)
|
| 132 |
+
d2 = compute_lexical_modernization(
|
| 133 |
+
"maistre", "maรฎtre",
|
| 134 |
+
)
|
| 135 |
+
agg = aggregate_lexical_modernization([d1, d2])
|
| 136 |
+
assert agg["tokens"]["maistre"]["n_total"] == 3
|
| 137 |
+
assert agg["tokens"]["maistre"]["n_modernized"] == 3
|
| 138 |
+
assert agg["tokens"]["maistre"]["rate_modernized"] == 1.0
|
| 139 |
+
|
| 140 |
+
def test_empty_iterable(self) -> None:
|
| 141 |
+
agg = aggregate_lexical_modernization([])
|
| 142 |
+
assert agg["tokens"] == {}
|
| 143 |
+
assert agg["n_gt_tokens"] == 0
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 147 |
+
# 3. top_modernized_tokens
|
| 148 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class TestTop:
|
| 152 |
+
def test_sorted_by_rate_desc(self) -> None:
|
| 153 |
+
gt = "a a b b c c d d"
|
| 154 |
+
hyp = "x x y b z c d d"
|
| 155 |
+
# a: 100% (2/2 modernisรฉ), b: 50%, c: 50%, d: 0%
|
| 156 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 157 |
+
top = top_modernized_tokens(result, n=10)
|
| 158 |
+
# a en premier
|
| 159 |
+
assert top[0][0] == "a"
|
| 160 |
+
# d exclu (0%)
|
| 161 |
+
names = [t[0] for t in top]
|
| 162 |
+
assert "d" not in names
|
| 163 |
+
|
| 164 |
+
def test_min_total_filter(self) -> None:
|
| 165 |
+
gt = "rare maistre maistre maistre"
|
| 166 |
+
hyp = "moderne maรฎtre maรฎtre maรฎtre"
|
| 167 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 168 |
+
# Avec min_total=2 : rare (1) exclu, maistre (3) conservรฉ
|
| 169 |
+
top = top_modernized_tokens(result, min_total=2)
|
| 170 |
+
names = [t[0] for t in top]
|
| 171 |
+
assert "rare" not in names
|
| 172 |
+
assert "maistre" in names
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 176 |
+
# 4. Rendu HTML
|
| 177 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class TestRender:
|
| 181 |
+
def test_returns_empty_when_none(self) -> None:
|
| 182 |
+
assert build_lexical_modernization_html(None) == ""
|
| 183 |
+
|
| 184 |
+
def test_returns_empty_when_no_modernizations(self) -> None:
|
| 185 |
+
result = compute_lexical_modernization("a b c", "a b c")
|
| 186 |
+
# Aucun modernisรฉ
|
| 187 |
+
assert build_lexical_modernization_html(result) == ""
|
| 188 |
+
|
| 189 |
+
def test_renders_table(self) -> None:
|
| 190 |
+
result = compute_lexical_modernization(
|
| 191 |
+
"maistre veoir", "maรฎtre voir",
|
| 192 |
+
)
|
| 193 |
+
html = build_lexical_modernization_html(result)
|
| 194 |
+
assert "<table" in html
|
| 195 |
+
assert "maistre" in html
|
| 196 |
+
assert "maรฎtre" in html
|
| 197 |
+
|
| 198 |
+
def test_rate_displayed_as_percent(self) -> None:
|
| 199 |
+
result = compute_lexical_modernization(
|
| 200 |
+
"maistre maistre maistre maistre",
|
| 201 |
+
"maรฎtre maistre maรฎtre maรฎtre",
|
| 202 |
+
)
|
| 203 |
+
html = build_lexical_modernization_html(result)
|
| 204 |
+
# 75% prรฉsent
|
| 205 |
+
assert "75%" in html
|
| 206 |
+
|
| 207 |
+
def test_anti_injection_token(self) -> None:
|
| 208 |
+
gt = "<script>alert(1)</script> normal"
|
| 209 |
+
hyp = "MODERNIZED normal"
|
| 210 |
+
result = compute_lexical_modernization(gt, hyp)
|
| 211 |
+
html = build_lexical_modernization_html(result)
|
| 212 |
+
assert "<script>alert" not in html
|
| 213 |
+
assert "<script>" in html
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 217 |
+
# 5. Complรฉtude i18n
|
| 218 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class TestI18nCompleteness:
|
| 222 |
+
def _load(self, lang: str) -> dict:
|
| 223 |
+
path = (
|
| 224 |
+
Path(__file__).parent.parent
|
| 225 |
+
/ "picarones" / "report" / "i18n" / f"{lang}.json"
|
| 226 |
+
)
|
| 227 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 228 |
+
|
| 229 |
+
def test_all_keys_fr(self) -> None:
|
| 230 |
+
d = self._load("fr")
|
| 231 |
+
for key in (
|
| 232 |
+
"lexmod_title", "lexmod_note", "lexmod_gt_label",
|
| 233 |
+
"lexmod_hyp_label", "lexmod_n_label", "lexmod_rate_label",
|
| 234 |
+
):
|
| 235 |
+
assert key in d, f"manque clรฉ FR : {key}"
|
| 236 |
+
|
| 237 |
+
def test_all_keys_en(self) -> None:
|
| 238 |
+
d_fr = self._load("fr")
|
| 239 |
+
d_en = self._load("en")
|
| 240 |
+
for key in d_fr:
|
| 241 |
+
if key.startswith("lexmod_"):
|
| 242 |
+
assert key in d_en, f"manque clรฉ EN : {key}"
|