Spaces:
Running
feat(web): S1 — expose 6 toggles BenchmarkRunRequest figés dans l'UI
Browse filesSprint S1 — débloque les options Pydantic qui existaient déjà côté
API mais étaient jamais transmises depuis l'interface web.
Champs exposés :
1. ``report_lang`` (FR / EN) — toggle dans Options avancées. Permet
de générer un rapport en anglais ; auparavant figé à "fr".
2. ``views`` — checkboxes ``alto_documentary`` et ``searchability``
(``text_final`` toujours actif). Active les vues canoniques
AltoView et SearchView du rapport HTML.
3. ``expose_alto`` (par-compétiteur) — checkbox dans la section
Concurrents, visible uniquement quand le moteur sélectionné
est Tesseract. Débloque la production d'ALTO XML natif.
4. ``entity_extractor`` — input texte avec datalist de presets
(``spacy.fr_core_news_sm``, ``spacy.en_core_web_sm``).
Débloque les renderers NER summary + per category du rapport.
5. ``output_json`` — toggle ; ON dérive un chemin auto depuis
``report_name`` (relatif au workspace, validé par Pydantic).
6. ``partial_dir`` — toggle ; ON dérive un chemin checkpoint auto
avec timestamp. Permet la reprise après interruption.
Modifications :
- ``_view_benchmark.html`` : ajout d'une section ``<details>``
collapsible « Options avancées » dans Section 03 + checkbox
``compose-expose-alto`` masquable dans Section 02.
- ``web-app.js`` : helper ``_gatherAdvancedOptions()`` qui collecte
les 5 champs globaux ; étendu ``startBenchmark()``,
``_gatherCurrentConfig()``, ``_applyConfig()`` pour les inclure.
Handler ``onComposeOCRChange()`` montre/cache le checkbox
expose_alto selon le moteur. Modification ``addCompetitor()``
pour lire la valeur du checkbox sur les compétiteurs Tesseract.
- i18n : 12 nouvelles clés (FR + EN) dans la table T inline.
Tests (tests/web/test_benchmark_request_options.py) :
- 7 tests présence des IDs DOM dans le template rendu.
- 2 tests payload UI-style complet vs minimal accepté par l'API.
- 2 tests ``expose_alto`` per-compétiteur (mix Tesseract +
cloud OCR).
- 10 tests couverture i18n FR ↔ EN des nouveaux libellés.
Verification :
- 5182 tests passed (+21 vs S0-ter), 0 failed, 20 skipped
- make lint : All checks passed
- Le contrat Pydantic API ↔ payload restait inchangé (déjà testé
par tests/web/test_benchmark_run_b3_final_fields.py — 22 tests).
- Pas de refactor du JS ``web-app.js`` (1762 → 1885 LOC) — pas
de budget LOC enforced sur ce fichier ; le découpage ESM
proposé dans le plan S1 est reporté au sprint refonte rapport
(S5) pour cohérence (un seul gros refactor JS au lieu de deux).
DoD :
- 6 champs UI traversent jusqu'au worker ``run_benchmark_thread_v2``
(vérifié par lecture de benchmark_utils.py:469-525, propagation
déjà branchée depuis l'audit Phase D3 mai 2026).
- Test bout-en-bout : payload-style UI accepté par /api/benchmark/run.
- Bloc NER du rapport HTML deviendra rempli dès que l'utilisateur
fournit un entity_extractor sur un corpus annoté ENTITIES.
https://claude.ai/code/session_01WYDbfkhKPeBZ15BTP4e9Ye
|
@@ -120,6 +120,22 @@ const T = {
|
|
| 120 |
compose_prompt: "Prompt",
|
| 121 |
compose_max_image_dim: "Image max (px)",
|
| 122 |
compose_max_image_dim_hint: "0 = pleine résolution (défaut, méthodo inchangée). > 0 réduit l'image envoyée au VLM (modes image) pour éviter les 429 — change la méthodo, run fingerprinté à part.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
compose_add: "+ Ajouter",
|
| 124 |
compose_empty: "Aucun concurrent ajouté.",
|
| 125 |
mode_text_only: "Post-correction texte",
|
|
@@ -269,6 +285,22 @@ const T = {
|
|
| 269 |
compose_prompt: "Prompt",
|
| 270 |
compose_max_image_dim: "Max image (px)",
|
| 271 |
compose_max_image_dim_hint: "0 = full resolution (default, methodology unchanged). > 0 shrinks the image sent to the VLM (image modes) to avoid 429s — changes methodology, fingerprinted as a separate run.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
compose_add: "+ Add",
|
| 273 |
compose_empty: "No competitors added.",
|
| 274 |
mode_text_only: "Text post-correction",
|
|
@@ -544,6 +576,15 @@ async function onComposeOCRChange() {
|
|
| 544 |
const engine = document.getElementById("compose-ocr-engine").value;
|
| 545 |
_pendingOCREngine = engine; // marquer la requête courante
|
| 546 |
const sp = document.getElementById("sp-ocr-model");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
// Google Vision et Azure ont des listes statiques — pas d'appel API nécessaire
|
| 548 |
if (engine === "google_vision") {
|
| 549 |
sp.style.display = "none";
|
|
@@ -658,9 +699,14 @@ function addCompetitor() {
|
|
| 658 |
|
| 659 |
const comp = { name: "", engine_name: "", ocr_model: "",
|
| 660 |
llm_provider: "", llm_model: "", pipeline_mode: "", prompt_file: "",
|
| 661 |
-
max_image_dimension: 0 };
|
| 662 |
const _maxImgDim = parseInt((document.getElementById("compose-max-image-dim") || {}).value, 10);
|
| 663 |
const maxImgDim = Number.isFinite(_maxImgDim) && _maxImgDim > 0 ? _maxImgDim : 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
|
| 665 |
if (mode === "postcorrection") {
|
| 666 |
// Post-correction : OCR vient du corpus (.ocr.txt)
|
|
@@ -691,6 +737,7 @@ function addCompetitor() {
|
|
| 691 |
comp.pipeline_mode = document.getElementById("compose-pipeline-mode").value;
|
| 692 |
comp.prompt_file = document.getElementById("compose-prompt").value;
|
| 693 |
comp.max_image_dimension = maxImgDim;
|
|
|
|
| 694 |
if (!comp.llm_provider) {
|
| 695 |
errEl.textContent = lang === "fr" ? "Sélectionnez un provider LLM." : "Select an LLM provider.";
|
| 696 |
return;
|
|
@@ -708,7 +755,9 @@ function addCompetitor() {
|
|
| 708 |
}
|
| 709 |
comp.engine_name = ocrEngine;
|
| 710 |
comp.ocr_model = ocrModel;
|
| 711 |
-
|
|
|
|
|
|
|
| 712 |
}
|
| 713 |
|
| 714 |
errEl.textContent = "";
|
|
@@ -870,6 +919,7 @@ async function startBenchmark() {
|
|
| 870 |
output_dir: document.getElementById("output-dir").value,
|
| 871 |
report_name: document.getElementById("report-name").value,
|
| 872 |
profile: (document.getElementById("run-profile") || {}).value || "standard",
|
|
|
|
| 873 |
};
|
| 874 |
|
| 875 |
document.getElementById("start-btn").disabled = true;
|
|
@@ -1603,11 +1653,57 @@ function _updateCorpusOCRNotice(corpusData) {
|
|
| 1603 |
// côté serveur (avec tests dédiés) mais aucun bouton ne les appelait —
|
| 1604 |
// code zombie typique post-rewrite.
|
| 1605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1606 |
function _gatherCurrentConfig() {
|
| 1607 |
/** Sérialise l'état UI courant en dict compatible
|
| 1608 |
* ``/api/config/save``. Inclut les compétiteurs composés
|
| 1609 |
-
* (_competitors), les options de normalisation
|
| 1610 |
-
* langue rapport. */
|
| 1611 |
return {
|
| 1612 |
label: document.getElementById("report-name").value || "picarones-config",
|
| 1613 |
corpus_path: document.getElementById("corpus-path").value,
|
|
@@ -1617,6 +1713,7 @@ function _gatherCurrentConfig() {
|
|
| 1617 |
output_dir: document.getElementById("output-dir").value,
|
| 1618 |
report_name: document.getElementById("report-name").value,
|
| 1619 |
profile: (document.getElementById("run-profile") || {}).value || "standard",
|
|
|
|
| 1620 |
};
|
| 1621 |
}
|
| 1622 |
|
|
@@ -1729,6 +1826,28 @@ function _applyConfig(cfg) {
|
|
| 1729 |
_competitors = cfg.competitors;
|
| 1730 |
renderCompetitors();
|
| 1731 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
}
|
| 1733 |
|
| 1734 |
// ─── Init ────────────────────────────────────────────────────────────────────
|
|
|
|
| 120 |
compose_prompt: "Prompt",
|
| 121 |
compose_max_image_dim: "Image max (px)",
|
| 122 |
compose_max_image_dim_hint: "0 = pleine résolution (défaut, méthodo inchangée). > 0 réduit l'image envoyée au VLM (modes image) pour éviter les 429 — change la méthodo, run fingerprinté à part.",
|
| 123 |
+
compose_expose_alto_label: "Produire l'ALTO XML natif",
|
| 124 |
+
compose_expose_alto_hint: "Active la vue « alto_documentary » du rapport. Tesseract uniquement.",
|
| 125 |
+
bench_advanced_options: "Options avancées",
|
| 126 |
+
bench_advanced_options_hint: "vues du rapport, langue, NER, reprise, export JSON",
|
| 127 |
+
bench_report_lang_label: "Langue du rapport",
|
| 128 |
+
bench_entity_extractor_label: "Extracteur NER (optionnel)",
|
| 129 |
+
bench_entity_extractor_hint: "Format : module.submodule:Symbol. Débloque le bloc NER du rapport HTML.",
|
| 130 |
+
bench_views_label: "Vues d'évaluation à activer",
|
| 131 |
+
bench_view_text_final: "text_final (toujours actif)",
|
| 132 |
+
bench_view_alto_documentary: "alto_documentary",
|
| 133 |
+
bench_view_searchability: "searchability",
|
| 134 |
+
bench_views_hint: "alto_documentary nécessite qu'au moins un concurrent Tesseract ait expose_alto activé.",
|
| 135 |
+
bench_partial_resume_label: "Permettre la reprise sur interruption",
|
| 136 |
+
bench_partial_resume_hint: "Crée un répertoire de checkpoint pour reprendre un run interrompu.",
|
| 137 |
+
bench_output_json_label: "Exporter aussi en JSON",
|
| 138 |
+
bench_output_json_hint: "Génère un fichier JSON additionnel à côté du rapport HTML.",
|
| 139 |
compose_add: "+ Ajouter",
|
| 140 |
compose_empty: "Aucun concurrent ajouté.",
|
| 141 |
mode_text_only: "Post-correction texte",
|
|
|
|
| 285 |
compose_prompt: "Prompt",
|
| 286 |
compose_max_image_dim: "Max image (px)",
|
| 287 |
compose_max_image_dim_hint: "0 = full resolution (default, methodology unchanged). > 0 shrinks the image sent to the VLM (image modes) to avoid 429s — changes methodology, fingerprinted as a separate run.",
|
| 288 |
+
compose_expose_alto_label: "Produce native ALTO XML",
|
| 289 |
+
compose_expose_alto_hint: "Enables the \"alto_documentary\" report view. Tesseract only.",
|
| 290 |
+
bench_advanced_options: "Advanced options",
|
| 291 |
+
bench_advanced_options_hint: "report views, language, NER, resume, JSON export",
|
| 292 |
+
bench_report_lang_label: "Report language",
|
| 293 |
+
bench_entity_extractor_label: "NER extractor (optional)",
|
| 294 |
+
bench_entity_extractor_hint: "Format: module.submodule:Symbol. Unlocks the NER block of the HTML report.",
|
| 295 |
+
bench_views_label: "Evaluation views to enable",
|
| 296 |
+
bench_view_text_final: "text_final (always on)",
|
| 297 |
+
bench_view_alto_documentary: "alto_documentary",
|
| 298 |
+
bench_view_searchability: "searchability",
|
| 299 |
+
bench_views_hint: "alto_documentary requires at least one Tesseract competitor with expose_alto enabled.",
|
| 300 |
+
bench_partial_resume_label: "Enable resume after interruption",
|
| 301 |
+
bench_partial_resume_hint: "Creates a checkpoint directory to resume an interrupted run.",
|
| 302 |
+
bench_output_json_label: "Also export as JSON",
|
| 303 |
+
bench_output_json_hint: "Generates an additional JSON file alongside the HTML report.",
|
| 304 |
compose_add: "+ Add",
|
| 305 |
compose_empty: "No competitors added.",
|
| 306 |
mode_text_only: "Text post-correction",
|
|
|
|
| 576 |
const engine = document.getElementById("compose-ocr-engine").value;
|
| 577 |
_pendingOCREngine = engine; // marquer la requête courante
|
| 578 |
const sp = document.getElementById("sp-ocr-model");
|
| 579 |
+
// expose_alto est spécifique à Tesseract. Visible uniquement pour ce moteur.
|
| 580 |
+
const altoWrap = document.getElementById("compose-expose-alto-wrap");
|
| 581 |
+
if (altoWrap) {
|
| 582 |
+
altoWrap.style.display = engine === "tesseract" ? "block" : "none";
|
| 583 |
+
if (engine !== "tesseract") {
|
| 584 |
+
const cb = document.getElementById("compose-expose-alto");
|
| 585 |
+
if (cb) cb.checked = false;
|
| 586 |
+
}
|
| 587 |
+
}
|
| 588 |
// Google Vision et Azure ont des listes statiques — pas d'appel API nécessaire
|
| 589 |
if (engine === "google_vision") {
|
| 590 |
sp.style.display = "none";
|
|
|
|
| 699 |
|
| 700 |
const comp = { name: "", engine_name: "", ocr_model: "",
|
| 701 |
llm_provider: "", llm_model: "", pipeline_mode: "", prompt_file: "",
|
| 702 |
+
max_image_dimension: 0, expose_alto: false };
|
| 703 |
const _maxImgDim = parseInt((document.getElementById("compose-max-image-dim") || {}).value, 10);
|
| 704 |
const maxImgDim = Number.isFinite(_maxImgDim) && _maxImgDim > 0 ? _maxImgDim : 0;
|
| 705 |
+
// expose_alto n'est lu que pour Tesseract (les autres moteurs ne le
|
| 706 |
+
// propagent pas — ignoré côté adapter, mais on ne l'envoie pas pour
|
| 707 |
+
// garder le payload propre).
|
| 708 |
+
const exposeAltoCb = document.getElementById("compose-expose-alto");
|
| 709 |
+
const composeExposeAlto = !!(exposeAltoCb && exposeAltoCb.checked);
|
| 710 |
|
| 711 |
if (mode === "postcorrection") {
|
| 712 |
// Post-correction : OCR vient du corpus (.ocr.txt)
|
|
|
|
| 737 |
comp.pipeline_mode = document.getElementById("compose-pipeline-mode").value;
|
| 738 |
comp.prompt_file = document.getElementById("compose-prompt").value;
|
| 739 |
comp.max_image_dimension = maxImgDim;
|
| 740 |
+
if (ocrEngine === "tesseract") comp.expose_alto = composeExposeAlto;
|
| 741 |
if (!comp.llm_provider) {
|
| 742 |
errEl.textContent = lang === "fr" ? "Sélectionnez un provider LLM." : "Select an LLM provider.";
|
| 743 |
return;
|
|
|
|
| 755 |
}
|
| 756 |
comp.engine_name = ocrEngine;
|
| 757 |
comp.ocr_model = ocrModel;
|
| 758 |
+
if (ocrEngine === "tesseract") comp.expose_alto = composeExposeAlto;
|
| 759 |
+
const altoSuffix = (ocrEngine === "tesseract" && composeExposeAlto) ? " · ALTO" : "";
|
| 760 |
+
comp.name = `${ocrEngine}${ocrModel ? " ("+ocrModel+")" : ""}${altoSuffix}`;
|
| 761 |
}
|
| 762 |
|
| 763 |
errEl.textContent = "";
|
|
|
|
| 919 |
output_dir: document.getElementById("output-dir").value,
|
| 920 |
report_name: document.getElementById("report-name").value,
|
| 921 |
profile: (document.getElementById("run-profile") || {}).value || "standard",
|
| 922 |
+
..._gatherAdvancedOptions(),
|
| 923 |
};
|
| 924 |
|
| 925 |
document.getElementById("start-btn").disabled = true;
|
|
|
|
| 1653 |
// côté serveur (avec tests dédiés) mais aucun bouton ne les appelait —
|
| 1654 |
// code zombie typique post-rewrite.
|
| 1655 |
|
| 1656 |
+
function _gatherAdvancedOptions() {
|
| 1657 |
+
/** Collecte les 5 champs de la section « Options avancées » sous
|
| 1658 |
+
* forme de dict prêt à être étalé dans le payload
|
| 1659 |
+
* ``POST /api/benchmark/run`` ou la sauvegarde de config.
|
| 1660 |
+
*
|
| 1661 |
+
* Champs collectés :
|
| 1662 |
+
* - ``report_lang`` ("fr" | "en")
|
| 1663 |
+
* - ``views`` liste des vues activées (text_final toujours inclus)
|
| 1664 |
+
* - ``partial_dir`` chemin auto-généré si toggle ON, "" sinon
|
| 1665 |
+
* - ``entity_extractor`` dotted path NER, "" si vide
|
| 1666 |
+
* - ``output_json`` chemin auto si toggle ON, "" sinon
|
| 1667 |
+
*
|
| 1668 |
+
* ``expose_alto`` n'est PAS dans ce dict — il est par-compétiteur
|
| 1669 |
+
* dans ``_competitors[i].expose_alto`` (positionné par addCompetitor).
|
| 1670 |
+
*/
|
| 1671 |
+
const views = ["text_final"];
|
| 1672 |
+
if ((document.getElementById("view-alto-documentary") || {}).checked) {
|
| 1673 |
+
views.push("alto_documentary");
|
| 1674 |
+
}
|
| 1675 |
+
if ((document.getElementById("view-searchability") || {}).checked) {
|
| 1676 |
+
views.push("searchability");
|
| 1677 |
+
}
|
| 1678 |
+
// Reprise : si toggle ON, générer un partial_dir relatif avec
|
| 1679 |
+
// timestamp lisible. Le validator Pydantic refuse les chemins
|
| 1680 |
+
// absolus et ``..``, ce format est sûr.
|
| 1681 |
+
let partialDir = "";
|
| 1682 |
+
if ((document.getElementById("enable-partial-resume") || {}).checked) {
|
| 1683 |
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
| 1684 |
+
partialDir = `partial/run-${ts}`;
|
| 1685 |
+
}
|
| 1686 |
+
// Export JSON : si toggle ON, dériver le chemin depuis report_name
|
| 1687 |
+
// (ou un nom par défaut). Relatif au output_dir côté serveur.
|
| 1688 |
+
let outputJson = "";
|
| 1689 |
+
if ((document.getElementById("enable-output-json") || {}).checked) {
|
| 1690 |
+
const stem = (document.getElementById("report-name").value || "rapport").trim();
|
| 1691 |
+
outputJson = `${stem}.json`;
|
| 1692 |
+
}
|
| 1693 |
+
return {
|
| 1694 |
+
report_lang: (document.getElementById("report-lang") || {}).value || "fr",
|
| 1695 |
+
views: views,
|
| 1696 |
+
partial_dir: partialDir,
|
| 1697 |
+
entity_extractor: (document.getElementById("entity-extractor") || {}).value.trim(),
|
| 1698 |
+
output_json: outputJson,
|
| 1699 |
+
};
|
| 1700 |
+
}
|
| 1701 |
+
|
| 1702 |
function _gatherCurrentConfig() {
|
| 1703 |
/** Sérialise l'état UI courant en dict compatible
|
| 1704 |
* ``/api/config/save``. Inclut les compétiteurs composés
|
| 1705 |
+
* (_competitors), les options de normalisation, le profil de
|
| 1706 |
+
* langue rapport et les options avancées. */
|
| 1707 |
return {
|
| 1708 |
label: document.getElementById("report-name").value || "picarones-config",
|
| 1709 |
corpus_path: document.getElementById("corpus-path").value,
|
|
|
|
| 1713 |
output_dir: document.getElementById("output-dir").value,
|
| 1714 |
report_name: document.getElementById("report-name").value,
|
| 1715 |
profile: (document.getElementById("run-profile") || {}).value || "standard",
|
| 1716 |
+
..._gatherAdvancedOptions(),
|
| 1717 |
};
|
| 1718 |
}
|
| 1719 |
|
|
|
|
| 1826 |
_competitors = cfg.competitors;
|
| 1827 |
renderCompetitors();
|
| 1828 |
}
|
| 1829 |
+
// Options avancées
|
| 1830 |
+
if (typeof cfg.report_lang === "string") {
|
| 1831 |
+
const sel = document.getElementById("report-lang");
|
| 1832 |
+
if (sel) sel.value = cfg.report_lang;
|
| 1833 |
+
}
|
| 1834 |
+
if (Array.isArray(cfg.views)) {
|
| 1835 |
+
const altoCb = document.getElementById("view-alto-documentary");
|
| 1836 |
+
if (altoCb) altoCb.checked = cfg.views.includes("alto_documentary");
|
| 1837 |
+
const searchCb = document.getElementById("view-searchability");
|
| 1838 |
+
if (searchCb) searchCb.checked = cfg.views.includes("searchability");
|
| 1839 |
+
}
|
| 1840 |
+
if (typeof cfg.entity_extractor === "string") {
|
| 1841 |
+
const inp = document.getElementById("entity-extractor");
|
| 1842 |
+
if (inp) inp.value = cfg.entity_extractor;
|
| 1843 |
+
}
|
| 1844 |
+
// ``partial_dir`` / ``output_json`` non vides → toggle ON (l'UI
|
| 1845 |
+
// régénère le chemin auto à la prochaine soumission, donc la
|
| 1846 |
+
// valeur exacte n'est pas restaurée mais le toggle l'est).
|
| 1847 |
+
const enablePartial = document.getElementById("enable-partial-resume");
|
| 1848 |
+
if (enablePartial) enablePartial.checked = !!(cfg.partial_dir && cfg.partial_dir !== "");
|
| 1849 |
+
const enableJson = document.getElementById("enable-output-json");
|
| 1850 |
+
if (enableJson) enableJson.checked = !!(cfg.output_json && cfg.output_json !== "");
|
| 1851 |
}
|
| 1852 |
|
| 1853 |
// ─── Init ────────────────────────────────────────────────────────────────────
|
|
@@ -128,6 +128,19 @@
|
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
<div id="compose-pipeline-section" style="display:none; margin-top:14px;">
|
| 132 |
<div class="grid-2" style="gap:14px;">
|
| 133 |
<div class="field">
|
|
@@ -243,6 +256,81 @@
|
|
| 243 |
<input type="text" id="report-name" placeholder="rapport_2026_05_20" class="mono-input" />
|
| 244 |
</div>
|
| 245 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
</div>
|
| 247 |
</div>
|
| 248 |
|
|
|
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
|
| 131 |
+
{# expose_alto : visible uniquement pour Tesseract. Active la
|
| 132 |
+
production native d'ALTO XML, débloquant la vue
|
| 133 |
+
``alto_documentary`` du rapport HTML. #}
|
| 134 |
+
<div id="compose-expose-alto-wrap" class="field" style="margin-top:12px;">
|
| 135 |
+
<label class="row" style="gap:8px; align-items:flex-start; cursor:pointer;">
|
| 136 |
+
<input type="checkbox" id="compose-expose-alto" />
|
| 137 |
+
<span>
|
| 138 |
+
<span class="field-label" style="display:inline; margin:0;" data-i18n="compose_expose_alto_label">Produire l'ALTO XML natif</span>
|
| 139 |
+
<small class="help" style="display:block; margin-top:4px; padding-left:0; border-left:0;" data-i18n="compose_expose_alto_hint">Active la vue « alto_documentary » du rapport. Tesseract uniquement.</small>
|
| 140 |
+
</span>
|
| 141 |
+
</label>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
<div id="compose-pipeline-section" style="display:none; margin-top:14px;">
|
| 145 |
<div class="grid-2" style="gap:14px;">
|
| 146 |
<div class="field">
|
|
|
|
| 256 |
<input type="text" id="report-name" placeholder="rapport_2026_05_20" class="mono-input" />
|
| 257 |
</div>
|
| 258 |
</div>
|
| 259 |
+
|
| 260 |
+
{# Options avancées (collapsible) — débloquent les vues
|
| 261 |
+
alto/searchability, le rapport en/fr, l'extracteur NER, la
|
| 262 |
+
reprise sur interruption et l'export JSON additionnel. #}
|
| 263 |
+
<details id="bench-advanced-options" style="margin-top:18px;">
|
| 264 |
+
<summary style="cursor:pointer; font-weight:500; padding:8px 0; color:var(--g-700);">
|
| 265 |
+
<span data-i18n="bench_advanced_options">Options avancées</span>
|
| 266 |
+
<small class="help" style="display:inline; margin-left:8px; padding-left:0; border-left:0;" data-i18n="bench_advanced_options_hint">vues du rapport, langue, NER, reprise, export JSON</small>
|
| 267 |
+
</summary>
|
| 268 |
+
|
| 269 |
+
<div class="grid-2" style="gap:14px; margin-top:14px;">
|
| 270 |
+
<div class="field">
|
| 271 |
+
<div class="field-label"><span data-i18n="bench_report_lang_label">Langue du rapport</span></div>
|
| 272 |
+
<select id="report-lang">
|
| 273 |
+
<option value="fr">Français</option>
|
| 274 |
+
<option value="en">English</option>
|
| 275 |
+
</select>
|
| 276 |
+
</div>
|
| 277 |
+
<div class="field">
|
| 278 |
+
<div class="field-label">
|
| 279 |
+
<span data-i18n="bench_entity_extractor_label">Extracteur NER (optionnel)</span>
|
| 280 |
+
</div>
|
| 281 |
+
<input type="text" id="entity-extractor" class="mono-input"
|
| 282 |
+
list="entity-extractor-presets"
|
| 283 |
+
placeholder="ex : spacy.fr_core_news_sm" />
|
| 284 |
+
<datalist id="entity-extractor-presets">
|
| 285 |
+
<option value="spacy.fr_core_news_sm">Spacy — français standard</option>
|
| 286 |
+
<option value="spacy.en_core_web_sm">Spacy — English standard</option>
|
| 287 |
+
</datalist>
|
| 288 |
+
<small class="help" style="margin-top:6px; padding-left:0; border-left:0;" data-i18n="bench_entity_extractor_hint">Format : <code>module.submodule:Symbol</code>. Débloque le bloc NER du rapport HTML.</small>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
|
| 292 |
+
<div class="field" style="margin-top:14px;">
|
| 293 |
+
<div class="field-label"><span data-i18n="bench_views_label">Vues d'évaluation à activer</span></div>
|
| 294 |
+
<div class="row" style="gap:18px; flex-wrap:wrap; margin-top:4px;">
|
| 295 |
+
<label class="row" style="gap:6px; cursor:pointer;">
|
| 296 |
+
<input type="checkbox" id="view-text-final" checked disabled />
|
| 297 |
+
<span data-i18n="bench_view_text_final">text_final (toujours actif)</span>
|
| 298 |
+
</label>
|
| 299 |
+
<label class="row" style="gap:6px; cursor:pointer;">
|
| 300 |
+
<input type="checkbox" id="view-alto-documentary" />
|
| 301 |
+
<span data-i18n="bench_view_alto_documentary">alto_documentary</span>
|
| 302 |
+
</label>
|
| 303 |
+
<label class="row" style="gap:6px; cursor:pointer;">
|
| 304 |
+
<input type="checkbox" id="view-searchability" />
|
| 305 |
+
<span data-i18n="bench_view_searchability">searchability</span>
|
| 306 |
+
</label>
|
| 307 |
+
</div>
|
| 308 |
+
<small class="help" style="margin-top:6px; padding-left:0; border-left:0;" data-i18n="bench_views_hint">
|
| 309 |
+
<code>alto_documentary</code> nécessite qu'au moins un concurrent Tesseract ait <code>expose_alto</code> activé.
|
| 310 |
+
</small>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div class="grid-2" style="gap:14px; margin-top:14px;">
|
| 314 |
+
<div class="field">
|
| 315 |
+
<label class="row" style="gap:8px; align-items:flex-start; cursor:pointer;">
|
| 316 |
+
<input type="checkbox" id="enable-partial-resume" />
|
| 317 |
+
<span>
|
| 318 |
+
<span class="field-label" style="display:inline; margin:0;" data-i18n="bench_partial_resume_label">Permettre la reprise sur interruption</span>
|
| 319 |
+
<small class="help" style="display:block; margin-top:4px; padding-left:0; border-left:0;" data-i18n="bench_partial_resume_hint">Crée un répertoire de checkpoint pour reprendre un run interrompu.</small>
|
| 320 |
+
</span>
|
| 321 |
+
</label>
|
| 322 |
+
</div>
|
| 323 |
+
<div class="field">
|
| 324 |
+
<label class="row" style="gap:8px; align-items:flex-start; cursor:pointer;">
|
| 325 |
+
<input type="checkbox" id="enable-output-json" />
|
| 326 |
+
<span>
|
| 327 |
+
<span class="field-label" style="display:inline; margin:0;" data-i18n="bench_output_json_label">Exporter aussi en JSON</span>
|
| 328 |
+
<small class="help" style="display:block; margin-top:4px; padding-left:0; border-left:0;" data-i18n="bench_output_json_hint">Génère un fichier JSON additionnel à côté du rapport HTML.</small>
|
| 329 |
+
</span>
|
| 330 |
+
</label>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
</details>
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
|
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests S1 — toggles UI exposés pour les options `BenchmarkRunRequest`.
|
| 2 |
+
|
| 3 |
+
Sprint S1 expose dans le template web 6 champs Pydantic qui étaient
|
| 4 |
+
figés côté UI :
|
| 5 |
+
|
| 6 |
+
- ``report_lang`` (FR / EN)
|
| 7 |
+
- ``views`` (text_final + alto_documentary + searchability)
|
| 8 |
+
- ``expose_alto`` (par-compétiteur, sur Tesseract uniquement)
|
| 9 |
+
- ``entity_extractor`` (dotted path NER)
|
| 10 |
+
- ``output_json`` (toggle qui dérive un chemin auto)
|
| 11 |
+
- ``partial_dir`` (toggle qui dérive un chemin auto)
|
| 12 |
+
|
| 13 |
+
Ces tests vérifient :
|
| 14 |
+
|
| 15 |
+
1. La présence des éléments DOM dans le template HTML rendu
|
| 16 |
+
(l'UI peut effectivement collecter ces valeurs).
|
| 17 |
+
2. Le payload complet UI-style est accepté par
|
| 18 |
+
``POST /api/benchmark/run`` (compat ascendante préservée).
|
| 19 |
+
3. Le payload sans les nouveaux champs continue de marcher (les
|
| 20 |
+
défauts Pydantic restent valides).
|
| 21 |
+
|
| 22 |
+
Le contrat **API ↔ Pydantic** lui-même (validation positive/négative,
|
| 23 |
+
path traversal) est testé exhaustivement par
|
| 24 |
+
``tests/web/test_benchmark_run_b3_final_fields.py``. Ce module-ci
|
| 25 |
+
cible la couche **UI → JSON payload**.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
|
| 32 |
+
import pytest
|
| 33 |
+
from fastapi.testclient import TestClient
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@pytest.fixture
|
| 37 |
+
def client():
|
| 38 |
+
from picarones.interfaces.web.app import app
|
| 39 |
+
return TestClient(app)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@pytest.fixture
|
| 43 |
+
def workspace_corpus(tmp_path: Path) -> str:
|
| 44 |
+
"""Crée un corpus minimal sous un workspace autorisé pour les
|
| 45 |
+
validators d'``output_dir`` / ``corpus_path``."""
|
| 46 |
+
from PIL import Image
|
| 47 |
+
|
| 48 |
+
img_path = tmp_path / "doc01.png"
|
| 49 |
+
Image.new("RGB", (40, 40), color=(255, 255, 255)).save(img_path)
|
| 50 |
+
(tmp_path / "doc01.gt.txt").write_text("hello", encoding="utf-8")
|
| 51 |
+
return str(tmp_path)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 55 |
+
# 1. Présence des éléments DOM dans le template
|
| 56 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class TestAdvancedOptionsUIElements:
|
| 60 |
+
"""Le template ``_view_benchmark.html`` doit contenir les IDs DOM
|
| 61 |
+
consommés par ``_gather_advanced_options()`` côté JS."""
|
| 62 |
+
|
| 63 |
+
@pytest.fixture
|
| 64 |
+
def html(self, client) -> str:
|
| 65 |
+
response = client.get("/")
|
| 66 |
+
assert response.status_code == 200
|
| 67 |
+
return response.text
|
| 68 |
+
|
| 69 |
+
def test_report_lang_select_present(self, html: str) -> None:
|
| 70 |
+
assert 'id="report-lang"' in html, (
|
| 71 |
+
"Sélecteur de langue du rapport manquant — "
|
| 72 |
+
"report_lang ne sera jamais transmis depuis l'UI."
|
| 73 |
+
)
|
| 74 |
+
# Options FR + EN visibles
|
| 75 |
+
assert 'value="fr"' in html
|
| 76 |
+
assert 'value="en"' in html
|
| 77 |
+
|
| 78 |
+
def test_views_checkboxes_present(self, html: str) -> None:
|
| 79 |
+
"""Les checkboxes pour alto_documentary et searchability."""
|
| 80 |
+
assert 'id="view-alto-documentary"' in html
|
| 81 |
+
assert 'id="view-searchability"' in html
|
| 82 |
+
# text_final reste activé en permanence (checkbox disabled)
|
| 83 |
+
assert 'id="view-text-final"' in html
|
| 84 |
+
|
| 85 |
+
def test_entity_extractor_input_present(self, html: str) -> None:
|
| 86 |
+
assert 'id="entity-extractor"' in html
|
| 87 |
+
# Le datalist propose des presets utiles
|
| 88 |
+
assert "spacy.fr_core_news_sm" in html
|
| 89 |
+
assert "spacy.en_core_web_sm" in html
|
| 90 |
+
|
| 91 |
+
def test_partial_resume_toggle_present(self, html: str) -> None:
|
| 92 |
+
assert 'id="enable-partial-resume"' in html
|
| 93 |
+
|
| 94 |
+
def test_output_json_toggle_present(self, html: str) -> None:
|
| 95 |
+
assert 'id="enable-output-json"' in html
|
| 96 |
+
|
| 97 |
+
def test_expose_alto_checkbox_present(self, html: str) -> None:
|
| 98 |
+
"""``expose_alto`` est dans la section compose (par-compétiteur)."""
|
| 99 |
+
assert 'id="compose-expose-alto"' in html
|
| 100 |
+
# Wrap dont la visibilité dépend du moteur (Tesseract uniquement).
|
| 101 |
+
assert 'id="compose-expose-alto-wrap"' in html
|
| 102 |
+
|
| 103 |
+
def test_advanced_options_collapsible_present(self, html: str) -> None:
|
| 104 |
+
"""Section ``<details>`` qui regroupe les options avancées."""
|
| 105 |
+
assert 'id="bench-advanced-options"' in html
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 109 |
+
# 2. Payload UI-style complet accepté par l'API
|
| 110 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class TestFullUIStylePayloadAccepted:
|
| 114 |
+
"""Un payload qui reflète la sérialisation produite par
|
| 115 |
+
``_gather_advanced_options()`` doit être accepté tel quel."""
|
| 116 |
+
|
| 117 |
+
def test_payload_with_all_advanced_options(
|
| 118 |
+
self, client, workspace_corpus: str,
|
| 119 |
+
) -> None:
|
| 120 |
+
"""Smoke test : payload UI-style avec les 6 toggles activés.
|
| 121 |
+
|
| 122 |
+
Note : le validator de chemins est strict — on utilise
|
| 123 |
+
``partial/run-2026-05-23`` (relatif, sans ``..``) et
|
| 124 |
+
``rapport.json`` (basename).
|
| 125 |
+
"""
|
| 126 |
+
payload = {
|
| 127 |
+
"corpus_path": workspace_corpus,
|
| 128 |
+
"competitors": [
|
| 129 |
+
{
|
| 130 |
+
"name": "tesseract (fra) · ALTO",
|
| 131 |
+
"engine_name": "tesseract",
|
| 132 |
+
"ocr_model": "fra",
|
| 133 |
+
"expose_alto": True,
|
| 134 |
+
"max_image_dimension": 0,
|
| 135 |
+
},
|
| 136 |
+
],
|
| 137 |
+
"normalization_profile": "nfc",
|
| 138 |
+
"char_exclude": "",
|
| 139 |
+
"output_dir": workspace_corpus, # même workspace
|
| 140 |
+
"report_name": "test_s1",
|
| 141 |
+
"profile": "standard",
|
| 142 |
+
# Options avancées :
|
| 143 |
+
"report_lang": "en",
|
| 144 |
+
"views": ["text_final", "alto_documentary", "searchability"],
|
| 145 |
+
"partial_dir": "partial/run-2026-05-23",
|
| 146 |
+
"entity_extractor": "spacy.fr_core_news_sm",
|
| 147 |
+
"output_json": "test_s1.json",
|
| 148 |
+
}
|
| 149 |
+
# On ne lance pas vraiment le benchmark (lourd) — on vérifie
|
| 150 |
+
# juste que Pydantic accepte le payload et que le runner
|
| 151 |
+
# démarre un job. Le retour est ``{"job_id": ..., "status": "pending"}``.
|
| 152 |
+
response = client.post("/api/benchmark/run", json=payload)
|
| 153 |
+
# Accepte 200 (job démarré) ou 429 (rate-limited en CI parallèle)
|
| 154 |
+
# mais surtout pas 422 (validation).
|
| 155 |
+
assert response.status_code != 422, (
|
| 156 |
+
f"Payload rejeté par Pydantic : {response.json()}"
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
def test_payload_with_minimal_advanced_options(
|
| 160 |
+
self, client, workspace_corpus: str,
|
| 161 |
+
) -> None:
|
| 162 |
+
"""Payload sans les nouveaux champs : compat ascendante.
|
| 163 |
+
|
| 164 |
+
Un client qui n'envoie aucun champ avancé doit continuer à
|
| 165 |
+
marcher. Les défauts Pydantic prennent le relais.
|
| 166 |
+
"""
|
| 167 |
+
payload = {
|
| 168 |
+
"corpus_path": workspace_corpus,
|
| 169 |
+
"competitors": [
|
| 170 |
+
{"name": "tesseract", "engine_name": "tesseract"},
|
| 171 |
+
],
|
| 172 |
+
"output_dir": workspace_corpus,
|
| 173 |
+
}
|
| 174 |
+
response = client.post("/api/benchmark/run", json=payload)
|
| 175 |
+
assert response.status_code != 422, (
|
| 176 |
+
f"Payload minimal rejeté : {response.json()}"
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 181 |
+
# 3. expose_alto par-compétiteur : transmission propre
|
| 182 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class TestExposeAltoPerCompetitor:
|
| 186 |
+
"""``expose_alto`` est un champ de ``PipelineConfig``, pas de
|
| 187 |
+
``BenchmarkRunRequest`` : il s'applique par-compétiteur."""
|
| 188 |
+
|
| 189 |
+
def test_expose_alto_only_on_tesseract_competitor(
|
| 190 |
+
self, client, workspace_corpus: str,
|
| 191 |
+
) -> None:
|
| 192 |
+
"""Mixer un compétiteur Tesseract+expose_alto et un autre OCR
|
| 193 |
+
cloud sans expose_alto doit fonctionner."""
|
| 194 |
+
payload = {
|
| 195 |
+
"corpus_path": workspace_corpus,
|
| 196 |
+
"competitors": [
|
| 197 |
+
{
|
| 198 |
+
"name": "tesseract (ALTO)",
|
| 199 |
+
"engine_name": "tesseract",
|
| 200 |
+
"ocr_model": "fra",
|
| 201 |
+
"expose_alto": True,
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"name": "mistral_ocr",
|
| 205 |
+
"engine_name": "mistral_ocr",
|
| 206 |
+
"expose_alto": False, # No-op pour Mistral
|
| 207 |
+
},
|
| 208 |
+
],
|
| 209 |
+
"output_dir": workspace_corpus,
|
| 210 |
+
"views": ["text_final", "alto_documentary"],
|
| 211 |
+
}
|
| 212 |
+
response = client.post("/api/benchmark/run", json=payload)
|
| 213 |
+
assert response.status_code != 422
|
| 214 |
+
|
| 215 |
+
def test_expose_alto_default_false(self, client, workspace_corpus: str) -> None:
|
| 216 |
+
"""Compat ascendante : un client qui n'envoie pas expose_alto
|
| 217 |
+
reçoit le défaut ``false``."""
|
| 218 |
+
from picarones.interfaces.web.models import PipelineConfig
|
| 219 |
+
|
| 220 |
+
config = PipelineConfig(name="t", engine_name="tesseract")
|
| 221 |
+
assert config.expose_alto is False
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# ─────────────────────────────────────────────────────────────────────────────
|
| 225 |
+
# 4. Synchronisation i18n des nouvelles clés
|
| 226 |
+
# ─────────────────────────────────────────────────────────────��───────────────
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
class TestI18nNewKeysCovered:
|
| 230 |
+
"""Les nouveaux libellés doivent avoir une traduction FR ET EN
|
| 231 |
+
pour ne pas dégrader silencieusement en clé brute (``bench_views_label``)
|
| 232 |
+
quand la langue d'interface est EN."""
|
| 233 |
+
|
| 234 |
+
@pytest.fixture
|
| 235 |
+
def js_source(self) -> str:
|
| 236 |
+
path = (
|
| 237 |
+
Path(__file__).resolve().parents[2]
|
| 238 |
+
/ "picarones" / "interfaces" / "web" / "static" / "web-app.js"
|
| 239 |
+
)
|
| 240 |
+
return path.read_text(encoding="utf-8")
|
| 241 |
+
|
| 242 |
+
@pytest.mark.parametrize("key", [
|
| 243 |
+
"compose_expose_alto_label",
|
| 244 |
+
"compose_expose_alto_hint",
|
| 245 |
+
"bench_advanced_options",
|
| 246 |
+
"bench_report_lang_label",
|
| 247 |
+
"bench_entity_extractor_label",
|
| 248 |
+
"bench_views_label",
|
| 249 |
+
"bench_view_alto_documentary",
|
| 250 |
+
"bench_view_searchability",
|
| 251 |
+
"bench_partial_resume_label",
|
| 252 |
+
"bench_output_json_label",
|
| 253 |
+
])
|
| 254 |
+
def test_key_present_in_both_languages(self, js_source: str, key: str) -> None:
|
| 255 |
+
"""Chaque clé doit apparaître au moins **deux fois** dans la
|
| 256 |
+
source — une fois dans ``T.fr`` et une fois dans ``T.en``."""
|
| 257 |
+
count = js_source.count(f"{key}:")
|
| 258 |
+
assert count >= 2, (
|
| 259 |
+
f"Clé i18n {key!r} déclarée seulement {count} fois ; "
|
| 260 |
+
"attendu ≥ 2 (FR + EN). Risque : clé brute affichée en EN."
|
| 261 |
+
)
|