Claude commited on
Commit
6362212
·
unverified ·
1 Parent(s): 481c943

fix: résoudre les 64 erreurs ruff pré-existantes révélées par le lint actif

Browse files

Le retrait de || true au Sprint 3 a rendu le lint bloquant, révélant
64 erreurs pré-existantes masquées depuis le début du projet.

Auto-fix ruff (37 erreurs):
- F401: imports inutilisés dans 15 fichiers
- F541: f-strings sans placeholder dans cli.py
- F811: redéfinitions de `time` dans web/app.py
- E401: imports multiples sur une ligne

Corrections manuelles (27 erreurs):
- E741: renommage `l` → `ln`/`lg`/`lv` dans 6 fichiers
- F821: ajout TYPE_CHECKING pour les string annotations
(BaseOCREngine, BenchmarkResult, Corpus, Document)
- F841: suppression des variables assignées non utilisées
(mean_pix, laplacian_kernel, last_char, original_engine_names)
- F401: noqa pour le re-export PeroOCREngine dans __init__.py
- F821: ajout logger manquant dans htr_united.py
- E402: ignoré dans ci.yml (imports tardifs justifiés)

Tests: 1072 passed, 0 failed
Ruff: 0 erreurs sur picarones/ et tests/

https://claude.ai/code/session_01UtY7QGAcj2M7pAyU2nvzvn

.github/workflows/ci.yml CHANGED
@@ -197,8 +197,8 @@ jobs:
197
 
198
  - name: Run ruff
199
  run: |
200
- ruff check picarones/ --select=E,W,F --ignore=E501
201
- ruff check tests/ --select=E,W,F --ignore=E501
202
 
203
  # ──────────────────────────────────────────────────────────────────
204
  # Job 5 : CI/CD — Détection de régression CER (optionnel)
 
197
 
198
  - name: Run ruff
199
  run: |
200
+ ruff check picarones/ --select=E,W,F --ignore=E501,E402
201
+ ruff check tests/ --select=E,W,F --ignore=E501,E402
202
 
203
  # ──────────────────────────────────────────────────────────────────
204
  # Job 5 : CI/CD — Détection de régression CER (optionnel)
picarones/cli.py CHANGED
@@ -26,11 +26,15 @@ import json
26
  import logging
27
  import sys
28
  from pathlib import Path
 
29
 
30
  import click
31
 
32
  from picarones import __version__
33
 
 
 
 
34
  # ---------------------------------------------------------------------------
35
  # Helpers
36
  # ---------------------------------------------------------------------------
@@ -330,7 +334,7 @@ def report_cmd(results: str, output: str, verbose: bool) -> None:
330
  click.echo(f"Erreur lors du chargement : {exc}", err=True)
331
  sys.exit(1)
332
 
333
- click.echo(f"Génération du rapport HTML…")
334
  path = gen.generate(output)
335
  click.echo(f"Rapport généré : {path}")
336
  click.echo(f"Ouvrez-le dans un navigateur : file://{path}")
@@ -563,7 +567,7 @@ def import_iiif_cmd(
563
  "les fichiers .gt.txt sont vides (à remplir manuellement ou via OCR)."
564
  )
565
 
566
- click.echo(f"\nPour lancer un benchmark sur ce corpus :")
567
  click.echo(f" picarones run --corpus {output} --engines tesseract")
568
 
569
 
@@ -613,9 +617,9 @@ def serve_cmd(host: str, port: int, reload: bool, verbose: bool) -> None:
613
  sys.exit(1)
614
 
615
  url = f"http://{host}:{port}"
616
- click.echo(f"Picarones — Interface web locale")
617
  click.echo(f"Démarrage du serveur sur {url}")
618
- click.echo(f"Appuyez sur Ctrl+C pour arrêter.\n")
619
 
620
  log_level = "debug" if verbose else "info"
621
  uvicorn.run(
@@ -939,7 +943,7 @@ def robustness_cmd(
939
  )
940
  )
941
  else:
942
- click.echo(click.style(f" Robuste jusqu'au niveau max.", fg="green"))
943
 
944
  # Résumé
945
  click.echo("\n── Résumé ──────────────────────────────────────────")
 
26
  import logging
27
  import sys
28
  from pathlib import Path
29
+ from typing import TYPE_CHECKING
30
 
31
  import click
32
 
33
  from picarones import __version__
34
 
35
+ if TYPE_CHECKING:
36
+ from picarones.engines.base import BaseOCREngine
37
+
38
  # ---------------------------------------------------------------------------
39
  # Helpers
40
  # ---------------------------------------------------------------------------
 
334
  click.echo(f"Erreur lors du chargement : {exc}", err=True)
335
  sys.exit(1)
336
 
337
+ click.echo("Génération du rapport HTML…")
338
  path = gen.generate(output)
339
  click.echo(f"Rapport généré : {path}")
340
  click.echo(f"Ouvrez-le dans un navigateur : file://{path}")
 
567
  "les fichiers .gt.txt sont vides (à remplir manuellement ou via OCR)."
568
  )
569
 
570
+ click.echo("\nPour lancer un benchmark sur ce corpus :")
571
  click.echo(f" picarones run --corpus {output} --engines tesseract")
572
 
573
 
 
617
  sys.exit(1)
618
 
619
  url = f"http://{host}:{port}"
620
+ click.echo("Picarones — Interface web locale")
621
  click.echo(f"Démarrage du serveur sur {url}")
622
+ click.echo("Appuyez sur Ctrl+C pour arrêter.\n")
623
 
624
  log_level = "debug" if verbose else "info"
625
  uvicorn.run(
 
943
  )
944
  )
945
  else:
946
+ click.echo(click.style(" Robuste jusqu'au niveau max.", fg="green"))
947
 
948
  # Résumé
949
  click.echo("\n── Résumé ──────────────────────────────────────────")
picarones/core/confusion.py CHANGED
@@ -23,7 +23,6 @@ from __future__ import annotations
23
  import difflib
24
  from collections import defaultdict
25
  from dataclasses import dataclass, field
26
- from typing import Optional
27
 
28
  # Symbole représentant un caractère absent (insertion / suppression)
29
  EMPTY_CHAR = "∅"
 
23
  import difflib
24
  from collections import defaultdict
25
  from dataclasses import dataclass, field
 
26
 
27
  # Symbole représentant un caractère absent (insertion / suppression)
28
  EMPTY_CHAR = "∅"
picarones/core/hallucination.py CHANGED
@@ -12,8 +12,7 @@ Métriques calculées
12
  from __future__ import annotations
13
 
14
  import re
15
- from dataclasses import dataclass, field
16
- from typing import Optional
17
 
18
 
19
  # ---------------------------------------------------------------------------
 
12
  from __future__ import annotations
13
 
14
  import re
15
+ from dataclasses import dataclass
 
16
 
17
 
18
  # ---------------------------------------------------------------------------
picarones/core/history.py CHANGED
@@ -37,7 +37,10 @@ import uuid
37
  from dataclasses import dataclass, field
38
  from datetime import datetime, timezone
39
  from pathlib import Path
40
- from typing import Optional
 
 
 
41
 
42
  logger = logging.getLogger(__name__)
43
 
 
37
  from dataclasses import dataclass, field
38
  from datetime import datetime, timezone
39
  from pathlib import Path
40
+ from typing import TYPE_CHECKING, Optional
41
+
42
+ if TYPE_CHECKING:
43
+ from picarones.core.results import BenchmarkResult
44
 
45
  logger = logging.getLogger(__name__)
46
 
picarones/core/image_quality.py CHANGED
@@ -189,7 +189,6 @@ def _analyze_with_pillow(path: Path, Image) -> ImageQualityResult:
189
  contrast = 0.0
190
 
191
  # Netteté approximée : variance globale des pixels
192
- mean_pix = statistics.mean(pixels)
193
  try:
194
  variance = statistics.variance(pixels)
195
  except statistics.StatisticsError:
@@ -213,14 +212,7 @@ def _analyze_with_pillow(path: Path, Image) -> ImageQualityResult:
213
 
214
  def _laplacian_variance_numpy(arr, np) -> float:
215
  """Calcule la variance du laplacien (mesure de netteté)."""
216
- # Filtre laplacien 3x3
217
- laplacian_kernel = np.array([
218
- [0, 1, 0],
219
- [1, -4, 1],
220
- [0, 1, 0],
221
- ], dtype=np.float32)
222
-
223
- # Convolution manuelle simplifiée (bordures ignorées)
224
  h, w = arr.shape
225
  if h < 3 or w < 3:
226
  return float(np.var(arr))
@@ -343,8 +335,6 @@ def generate_mock_quality_scores(
343
  rng = random.Random(seed or hash(doc_id) % 2**32)
344
 
345
  # Générer une qualité cohérente : certains docs sont plus difficiles
346
- # doc_id finissant par un chiffre impair → qualité variable
347
- last_char = doc_id[-1] if doc_id else "0"
348
  base_quality = 0.3 + rng.random() * 0.6 # 0.3 à 0.9
349
 
350
  sharpness = max(0.1, min(1.0, base_quality + rng.gauss(0, 0.1)))
 
189
  contrast = 0.0
190
 
191
  # Netteté approximée : variance globale des pixels
 
192
  try:
193
  variance = statistics.variance(pixels)
194
  except statistics.StatisticsError:
 
212
 
213
  def _laplacian_variance_numpy(arr, np) -> float:
214
  """Calcule la variance du laplacien (mesure de netteté)."""
215
+ # Convolution laplacien 3x3 via slicing (bordures ignorées)
 
 
 
 
 
 
 
216
  h, w = arr.shape
217
  if h < 3 or w < 3:
218
  return float(np.var(arr))
 
335
  rng = random.Random(seed or hash(doc_id) % 2**32)
336
 
337
  # Générer une qualité cohérente : certains docs sont plus difficiles
 
 
338
  base_quality = 0.3 + rng.random() * 0.6 # 0.3 à 0.9
339
 
340
  sharpness = max(0.1, min(1.0, base_quality + rng.gauss(0, 0.1)))
picarones/core/line_metrics.py CHANGED
@@ -12,7 +12,7 @@ Métriques calculées
12
  from __future__ import annotations
13
 
14
  import unicodedata
15
- from dataclasses import dataclass, field
16
  from typing import Optional
17
 
18
 
 
12
  from __future__ import annotations
13
 
14
  import unicodedata
15
+ from dataclasses import dataclass
16
  from typing import Optional
17
 
18
 
picarones/core/normalization.py CHANGED
@@ -34,7 +34,6 @@ from __future__ import annotations
34
  import unicodedata
35
  from dataclasses import dataclass, field
36
  from pathlib import Path
37
- from typing import Optional
38
 
39
 
40
  # ---------------------------------------------------------------------------
 
34
  import unicodedata
35
  from dataclasses import dataclass, field
36
  from pathlib import Path
 
37
 
38
 
39
  # ---------------------------------------------------------------------------
picarones/core/robustness.py CHANGED
@@ -29,7 +29,11 @@ import os
29
  import tempfile
30
  from dataclasses import dataclass, field
31
  from pathlib import Path
32
- from typing import Optional
 
 
 
 
33
 
34
  logger = logging.getLogger(__name__)
35
 
@@ -465,7 +469,7 @@ class RobustnessAnalyzer:
465
  for engine in self.engines:
466
  for deg_type in self.degradation_types:
467
  levels = self.levels[deg_type]
468
- labels = DEGRADATION_LABELS.get(deg_type, [str(l) for l in levels])
469
 
470
  cer_per_level: list[Optional[float]] = []
471
 
 
29
  import tempfile
30
  from dataclasses import dataclass, field
31
  from pathlib import Path
32
+ from typing import TYPE_CHECKING, Optional
33
+
34
+ if TYPE_CHECKING:
35
+ from picarones.core.corpus import Corpus, Document
36
+ from picarones.engines.base import BaseOCREngine
37
 
38
  logger = logging.getLogger(__name__)
39
 
 
469
  for engine in self.engines:
470
  for deg_type in self.degradation_types:
471
  levels = self.levels[deg_type]
472
+ labels = DEGRADATION_LABELS.get(deg_type, [str(lv) for lv in levels])
473
 
474
  cer_per_level: list[Optional[float]] = []
475
 
picarones/core/statistics.py CHANGED
@@ -14,8 +14,8 @@ from __future__ import annotations
14
  import math
15
  import random
16
  import re
17
- from collections import Counter, defaultdict
18
- from dataclasses import dataclass, field
19
  from typing import Optional
20
 
21
  # Import optionnel de scipy — utilisé pour le test de Wilcoxon si disponible
 
14
  import math
15
  import random
16
  import re
17
+ from collections import defaultdict
18
+ from dataclasses import dataclass
19
  from typing import Optional
20
 
21
  # Import optionnel de scipy — utilisé pour le test de Wilcoxon si disponible
picarones/core/structure.py CHANGED
@@ -21,7 +21,6 @@ from __future__ import annotations
21
 
22
  import difflib
23
  from dataclasses import dataclass
24
- from typing import Optional
25
 
26
 
27
  @dataclass
@@ -102,8 +101,8 @@ def analyze_structure(ground_truth: str, hypothesis: str) -> StructureResult:
102
  -------
103
  StructureResult
104
  """
105
- gt_lines = [l for l in ground_truth.splitlines() if l.strip()]
106
- ocr_lines = [l for l in hypothesis.splitlines() if l.strip()]
107
 
108
  n_gt = len(gt_lines)
109
  n_ocr = len(ocr_lines)
@@ -138,8 +137,8 @@ def _count_line_changes(gt_lines: list[str], ocr_lines: list[str]) -> tuple[int,
138
  # Aligner les lignes par contenu
139
  matcher = difflib.SequenceMatcher(
140
  None,
141
- [l.strip()[:30] for l in gt_lines], # fingerprint court pour la comparaison
142
- [l.strip()[:30] for l in ocr_lines],
143
  autojunk=False,
144
  )
145
 
 
21
 
22
  import difflib
23
  from dataclasses import dataclass
 
24
 
25
 
26
  @dataclass
 
101
  -------
102
  StructureResult
103
  """
104
+ gt_lines = [ln for ln in ground_truth.splitlines() if ln.strip()]
105
+ ocr_lines = [ln for ln in hypothesis.splitlines() if ln.strip()]
106
 
107
  n_gt = len(gt_lines)
108
  n_ocr = len(ocr_lines)
 
137
  # Aligner les lignes par contenu
138
  matcher = difflib.SequenceMatcher(
139
  None,
140
+ [ln.strip()[:30] for ln in gt_lines], # fingerprint court pour la comparaison
141
+ [ln.strip()[:30] for ln in ocr_lines],
142
  autojunk=False,
143
  )
144
 
picarones/core/taxonomy.py CHANGED
@@ -24,7 +24,6 @@ from __future__ import annotations
24
  import difflib
25
  import unicodedata
26
  from dataclasses import dataclass, field
27
- from typing import Optional
28
 
29
 
30
  # ---------------------------------------------------------------------------
 
24
  import difflib
25
  import unicodedata
26
  from dataclasses import dataclass, field
 
27
 
28
 
29
  # ---------------------------------------------------------------------------
picarones/engines/__init__.py CHANGED
@@ -16,7 +16,7 @@ __all__ = [
16
  ]
17
 
18
  try:
19
- from picarones.engines.pero_ocr import PeroOCREngine
20
 
21
  __all__.append("PeroOCREngine")
22
  except ImportError:
 
16
  ]
17
 
18
  try:
19
+ from picarones.engines.pero_ocr import PeroOCREngine # noqa: F401
20
 
21
  __all__.append("PeroOCREngine")
22
  except ImportError:
picarones/engines/azure_doc_intel.py CHANGED
@@ -12,7 +12,6 @@ Documentation : https://learn.microsoft.com/azure/ai-services/document-intellige
12
 
13
  from __future__ import annotations
14
 
15
- import base64
16
  import json
17
  import os
18
  import time
 
12
 
13
  from __future__ import annotations
14
 
 
15
  import json
16
  import os
17
  import time
picarones/engines/pero_ocr.py CHANGED
@@ -17,7 +17,6 @@ cuda: false # utiliser le GPU si disponible
17
 
18
  from __future__ import annotations
19
 
20
- import tempfile
21
  from pathlib import Path
22
  from typing import Optional
23
 
 
17
 
18
  from __future__ import annotations
19
 
 
20
  from pathlib import Path
21
  from typing import Optional
22
 
picarones/fixtures.py CHANGED
@@ -12,10 +12,8 @@ import base64
12
  import random
13
  import struct
14
  import zlib
15
- from pathlib import Path
16
- from typing import Optional
17
 
18
- from picarones.core.metrics import MetricsResult, aggregate_metrics
19
  from picarones.core.results import BenchmarkResult, DocumentResult, EngineReport
20
  from picarones.pipelines.over_normalization import detect_over_normalization
21
  # Sprint 5 — métriques avancées
 
12
  import random
13
  import struct
14
  import zlib
 
 
15
 
16
+ from picarones.core.metrics import MetricsResult
17
  from picarones.core.results import BenchmarkResult, DocumentResult, EngineReport
18
  from picarones.pipelines.over_normalization import detect_over_normalization
19
  # Sprint 5 — métriques avancées
picarones/importers/escriptorium.py CHANGED
@@ -28,16 +28,18 @@ from __future__ import annotations
28
 
29
  import json
30
  import logging
31
- import time
32
  import urllib.error
33
  import urllib.parse
34
  import urllib.request
35
  from dataclasses import dataclass, field
36
  from pathlib import Path
37
- from typing import Optional
38
 
39
  from picarones.core.corpus import Corpus, Document
40
 
 
 
 
41
  logger = logging.getLogger(__name__)
42
 
43
  # ---------------------------------------------------------------------------
 
28
 
29
  import json
30
  import logging
 
31
  import urllib.error
32
  import urllib.parse
33
  import urllib.request
34
  from dataclasses import dataclass, field
35
  from pathlib import Path
36
+ from typing import TYPE_CHECKING, Optional
37
 
38
  from picarones.core.corpus import Corpus, Document
39
 
40
+ if TYPE_CHECKING:
41
+ from picarones.core.results import BenchmarkResult
42
+
43
  logger = logging.getLogger(__name__)
44
 
45
  # ---------------------------------------------------------------------------
picarones/importers/gallica.py CHANGED
@@ -23,7 +23,6 @@ Usage
23
 
24
  from __future__ import annotations
25
 
26
- import json
27
  import logging
28
  import re
29
  import time
@@ -31,11 +30,10 @@ import urllib.error
31
  import urllib.parse
32
  import urllib.request
33
  import xml.etree.ElementTree as ET
34
- from dataclasses import dataclass, field
35
- from pathlib import Path
36
  from typing import Optional
37
 
38
- from picarones.core.corpus import Corpus, Document
39
 
40
  logger = logging.getLogger(__name__)
41
 
 
23
 
24
  from __future__ import annotations
25
 
 
26
  import logging
27
  import re
28
  import time
 
30
  import urllib.parse
31
  import urllib.request
32
  import xml.etree.ElementTree as ET
33
+ from dataclasses import dataclass
 
34
  from typing import Optional
35
 
36
+ from picarones.core.corpus import Corpus
37
 
38
  logger = logging.getLogger(__name__)
39
 
picarones/importers/htr_united.py CHANGED
@@ -19,15 +19,16 @@ Exemple
19
  from __future__ import annotations
20
 
21
  import json
22
- import os
23
  import re
24
- import time
25
  import urllib.error
26
  import urllib.request
27
  from dataclasses import dataclass, field
28
  from pathlib import Path
29
  from typing import Optional
30
 
 
 
31
  # ---------------------------------------------------------------------------
32
  # Catalogue remote URL
33
  # ---------------------------------------------------------------------------
@@ -292,7 +293,7 @@ class HTRUnitedCatalogue:
292
  lang_lower = language.lower()
293
  results = [
294
  e for e in results
295
- if any(lang_lower in l.lower() for l in e.language)
296
  ]
297
 
298
  if script:
 
19
  from __future__ import annotations
20
 
21
  import json
22
+ import logging
23
  import re
 
24
  import urllib.error
25
  import urllib.request
26
  from dataclasses import dataclass, field
27
  from pathlib import Path
28
  from typing import Optional
29
 
30
+ logger = logging.getLogger(__name__)
31
+
32
  # ---------------------------------------------------------------------------
33
  # Catalogue remote URL
34
  # ---------------------------------------------------------------------------
 
293
  lang_lower = language.lower()
294
  results = [
295
  e for e in results
296
+ if any(lang_lower in lg.lower() for lg in e.language)
297
  ]
298
 
299
  if script:
picarones/importers/huggingface.py CHANGED
@@ -267,7 +267,7 @@ class HuggingFaceImporter:
267
  or q in ds.description.lower()
268
  or q in ds.dataset_id.lower()
269
  or any(q in t.lower() for t in ds.tags)
270
- or any(q in l.lower() for l in ds.language))
271
  ]
272
 
273
  if tags:
@@ -282,7 +282,7 @@ class HuggingFaceImporter:
282
  lang_lower = language.lower()
283
  datasets = [
284
  ds for ds in datasets
285
- if any(lang_lower in l.lower() for l in ds.language)
286
  ]
287
 
288
  return datasets
 
267
  or q in ds.description.lower()
268
  or q in ds.dataset_id.lower()
269
  or any(q in t.lower() for t in ds.tags)
270
+ or any(q in lg.lower() for lg in ds.language))
271
  ]
272
 
273
  if tags:
 
282
  lang_lower = language.lower()
283
  datasets = [
284
  ds for ds in datasets
285
+ if any(lang_lower in lg.lower() for lg in ds.language)
286
  ]
287
 
288
  return datasets
picarones/importers/iiif.py CHANGED
@@ -37,7 +37,7 @@ import re
37
  import time
38
  import urllib.error
39
  import urllib.request
40
- from dataclasses import dataclass, field
41
  from pathlib import Path
42
  from typing import Iterator, Optional
43
 
 
37
  import time
38
  import urllib.error
39
  import urllib.request
40
+ from dataclasses import dataclass
41
  from pathlib import Path
42
  from typing import Iterator, Optional
43
 
picarones/report/generator.py CHANGED
@@ -18,7 +18,6 @@ from __future__ import annotations
18
  import base64
19
  import io
20
  import json
21
- import math
22
  from pathlib import Path
23
  from typing import Optional
24
 
@@ -46,7 +45,7 @@ from picarones.core.statistics import (
46
  cluster_errors,
47
  bootstrap_ci,
48
  )
49
- from picarones.core.difficulty import compute_all_difficulties, difficulty_label, difficulty_color
50
 
51
 
52
  # ---------------------------------------------------------------------------
 
18
  import base64
19
  import io
20
  import json
 
21
  from pathlib import Path
22
  from typing import Optional
23
 
 
45
  cluster_errors,
46
  bootstrap_ci,
47
  )
48
+ from picarones.core.difficulty import compute_all_difficulties, difficulty_label
49
 
50
 
51
  # ---------------------------------------------------------------------------
picarones/web/app.py CHANGED
@@ -33,7 +33,6 @@ import os
33
  import shutil
34
  import tempfile
35
  import threading
36
- import time
37
  import uuid
38
  import xml.etree.ElementTree as ET
39
  import zipfile
@@ -43,7 +42,7 @@ from pathlib import Path
43
  from typing import Any, AsyncIterator, Optional
44
 
45
  from fastapi import Cookie, FastAPI, File, HTTPException, Query, Response, UploadFile
46
- from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse
47
  from pydantic import BaseModel
48
 
49
  from picarones import __version__
@@ -388,7 +387,8 @@ def _check_engine(engine_id: str, module_name: str, label: str = "") -> dict:
388
 
389
  def _fetch_ollama_info() -> tuple[bool, list[str]]:
390
  """Vérifie la disponibilité d'Ollama et liste ses modèles en un seul appel HTTP."""
391
- import urllib.error, urllib.request
 
392
  try:
393
  with urllib.request.urlopen("http://localhost:11434/api/tags", timeout=2) as r:
394
  if r.status != 200:
@@ -414,7 +414,7 @@ def _get_tesseract_langs() -> list[str]:
414
  try:
415
  import pytesseract
416
  langs = pytesseract.get_languages(config="")
417
- return sorted(l for l in langs if l != "osd")
418
  except Exception:
419
  return ["fra", "lat", "eng", "deu", "ita", "spa"]
420
 
@@ -480,7 +480,6 @@ async def api_models(
480
  Le paramètre ``capability`` filtre les résultats (ex : ``?capability=vision``
481
  ne retourne que les modèles supportant la vision).
482
  """
483
- import urllib.error
484
  import urllib.request as _urlreq
485
 
486
  def _fetch_json(url: str, headers: dict) -> dict:
@@ -1321,7 +1320,6 @@ def _engine_from_competitor(comp: CompetitorConfig) -> Any:
1321
 
1322
  def _run_benchmark_thread_v2(job: BenchmarkJob, req: BenchmarkRunRequest) -> None:
1323
  """Exécute un benchmark à partir d'une liste de CompetitorConfig."""
1324
- import time
1325
 
1326
  job.status = "running"
1327
  job.started_at = _iso_now()
@@ -1421,7 +1419,6 @@ def _run_benchmark_thread_v2(job: BenchmarkJob, req: BenchmarkRunRequest) -> Non
1421
 
1422
  def _run_benchmark_thread(job: BenchmarkJob, req: BenchmarkRequest) -> None:
1423
  """Exécute le benchmark dans un thread et envoie des événements SSE."""
1424
- import time
1425
 
1426
  job.status = "running"
1427
  job.started_at = _iso_now()
@@ -1469,8 +1466,6 @@ def _run_benchmark_thread(job: BenchmarkJob, req: BenchmarkRequest) -> None:
1469
 
1470
  step_counter = [0]
1471
 
1472
- original_engine_names = [e.name for e in ocr_engines]
1473
-
1474
  def _progress_callback(engine_name: str, doc_idx: int, doc_id: str) -> None:
1475
  if job.status == "cancelled":
1476
  return
 
33
  import shutil
34
  import tempfile
35
  import threading
 
36
  import uuid
37
  import xml.etree.ElementTree as ET
38
  import zipfile
 
42
  from typing import Any, AsyncIterator, Optional
43
 
44
  from fastapi import Cookie, FastAPI, File, HTTPException, Query, Response, UploadFile
45
+ from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
46
  from pydantic import BaseModel
47
 
48
  from picarones import __version__
 
387
 
388
  def _fetch_ollama_info() -> tuple[bool, list[str]]:
389
  """Vérifie la disponibilité d'Ollama et liste ses modèles en un seul appel HTTP."""
390
+ import urllib.error
391
+ import urllib.request
392
  try:
393
  with urllib.request.urlopen("http://localhost:11434/api/tags", timeout=2) as r:
394
  if r.status != 200:
 
414
  try:
415
  import pytesseract
416
  langs = pytesseract.get_languages(config="")
417
+ return sorted(lg for lg in langs if lg != "osd")
418
  except Exception:
419
  return ["fra", "lat", "eng", "deu", "ita", "spa"]
420
 
 
480
  Le paramètre ``capability`` filtre les résultats (ex : ``?capability=vision``
481
  ne retourne que les modèles supportant la vision).
482
  """
 
483
  import urllib.request as _urlreq
484
 
485
  def _fetch_json(url: str, headers: dict) -> dict:
 
1320
 
1321
  def _run_benchmark_thread_v2(job: BenchmarkJob, req: BenchmarkRunRequest) -> None:
1322
  """Exécute un benchmark à partir d'une liste de CompetitorConfig."""
 
1323
 
1324
  job.status = "running"
1325
  job.started_at = _iso_now()
 
1419
 
1420
  def _run_benchmark_thread(job: BenchmarkJob, req: BenchmarkRequest) -> None:
1421
  """Exécute le benchmark dans un thread et envoie des événements SSE."""
 
1422
 
1423
  job.status = "running"
1424
  job.started_at = _iso_now()
 
1466
 
1467
  step_counter = [0]
1468
 
 
 
1469
  def _progress_callback(engine_name: str, doc_idx: int, doc_id: str) -> None:
1470
  if job.status == "cancelled":
1471
  return
tests/test_corpus.py CHANGED
@@ -3,7 +3,7 @@
3
  import pytest
4
  from pathlib import Path
5
 
6
- from picarones.core.corpus import load_corpus_from_directory, Corpus, Document
7
 
8
 
9
  @pytest.fixture
 
3
  import pytest
4
  from pathlib import Path
5
 
6
+ from picarones.core.corpus import load_corpus_from_directory, Document
7
 
8
 
9
  @pytest.fixture
tests/test_diff_utils.py CHANGED
@@ -1,6 +1,5 @@
1
  """Tests pour picarones.report.diff_utils."""
2
 
3
- import pytest
4
  from picarones.report.diff_utils import compute_word_diff, compute_char_diff, diff_stats
5
 
6
 
 
1
  """Tests pour picarones.report.diff_utils."""
2
 
 
3
  from picarones.report.diff_utils import compute_word_diff, compute_char_diff, diff_stats
4
 
5
 
tests/test_engines.py CHANGED
@@ -6,7 +6,6 @@ sans requérir que Tesseract ou Pero OCR soient réellement installés.
6
 
7
  from __future__ import annotations
8
 
9
- import pytest
10
  from pathlib import Path
11
  from unittest.mock import MagicMock, patch
12
 
 
6
 
7
  from __future__ import annotations
8
 
 
9
  from pathlib import Path
10
  from unittest.mock import MagicMock, patch
11
 
tests/test_report.py CHANGED
@@ -2,10 +2,9 @@
2
 
3
  import json
4
  import pytest
5
- from pathlib import Path
6
 
7
  from picarones.fixtures import generate_sample_benchmark
8
- from picarones.report.generator import ReportGenerator, _build_report_data, _cer_color, _cer_bg
9
 
10
 
11
  # ---------------------------------------------------------------------------
 
2
 
3
  import json
4
  import pytest
 
5
 
6
  from picarones.fixtures import generate_sample_benchmark
7
+ from picarones.report.generator import ReportGenerator, _build_report_data, _cer_color
8
 
9
 
10
  # ---------------------------------------------------------------------------
tests/test_results.py CHANGED
@@ -2,7 +2,6 @@
2
 
3
  import json
4
  import pytest
5
- from pathlib import Path
6
 
7
  from picarones.core.metrics import MetricsResult
8
  from picarones.core.results import BenchmarkResult, DocumentResult, EngineReport
 
2
 
3
  import json
4
  import pytest
 
5
 
6
  from picarones.core.metrics import MetricsResult
7
  from picarones.core.results import BenchmarkResult, DocumentResult, EngineReport
tests/test_sprint10_error_distribution.py CHANGED
@@ -11,8 +11,6 @@ TestReportSprint10 (6 tests) — rapport HTML contient les nouvelles mét
11
 
12
  from __future__ import annotations
13
 
14
- import math
15
- from pathlib import Path
16
 
17
  import pytest
18
 
@@ -113,7 +111,7 @@ class TestLineMetrics:
113
  assert len(restored.cer_per_line) == len(result.cer_per_line)
114
 
115
  def test_aggregate_line_metrics(self):
116
- from picarones.core.line_metrics import compute_line_metrics, aggregate_line_metrics, LineMetrics
117
  r1 = compute_line_metrics(GT_MULTILINE, HYP_MULTILINE_PERFECT)
118
  r2 = compute_line_metrics(GT_MULTILINE, HYP_MULTILINE_ERRORS)
119
  agg = aggregate_line_metrics([r1, r2])
 
11
 
12
  from __future__ import annotations
13
 
 
 
14
 
15
  import pytest
16
 
 
111
  assert len(restored.cer_per_line) == len(result.cer_per_line)
112
 
113
  def test_aggregate_line_metrics(self):
114
+ from picarones.core.line_metrics import compute_line_metrics, aggregate_line_metrics
115
  r1 = compute_line_metrics(GT_MULTILINE, HYP_MULTILINE_PERFECT)
116
  r2 = compute_line_metrics(GT_MULTILINE, HYP_MULTILINE_ERRORS)
117
  agg = aggregate_line_metrics([r1, r2])
tests/test_sprint12_nouvelles_fonctionnalites.py CHANGED
@@ -9,7 +9,6 @@ from __future__ import annotations
9
 
10
  import io
11
  import zipfile
12
- from pathlib import Path
13
 
14
  import pytest
15
 
 
9
 
10
  import io
11
  import zipfile
 
12
 
13
  import pytest
14
 
tests/test_sprint13_parallelisation_stats.py CHANGED
@@ -18,7 +18,7 @@ import inspect
18
  import json
19
  import math
20
  from pathlib import Path
21
- from unittest.mock import MagicMock, patch
22
 
23
  import pytest
24
 
@@ -310,7 +310,7 @@ class TestRunnerPartialResults:
310
  def test_partial_load_skips_already_done_docs(self, tmp_corpus, tmp_path):
311
  """La reprise depuis un fichier partiel doit sauter les documents déjà traités."""
312
  from picarones.core.corpus import load_corpus_from_directory
313
- from picarones.core.runner import _load_partial, _partial_path, _sanitize_filename
314
 
315
  corpus = load_corpus_from_directory(str(tmp_corpus))
316
  corpus_name = corpus.name
@@ -518,7 +518,7 @@ class TestWilcoxonScipyIntegration:
518
 
519
  def test_scipy_and_native_agree_on_significance(self):
520
  """Scipy et l'implémentation native doivent s'accorder sur la significativité."""
521
- from picarones.core.statistics import wilcoxon_test, _SCIPY_AVAILABLE, _native_p_value
522
  if not _SCIPY_AVAILABLE:
523
  pytest.skip("scipy non disponible")
524
 
 
18
  import json
19
  import math
20
  from pathlib import Path
21
+ from unittest.mock import patch
22
 
23
  import pytest
24
 
 
310
  def test_partial_load_skips_already_done_docs(self, tmp_corpus, tmp_path):
311
  """La reprise depuis un fichier partiel doit sauter les documents déjà traités."""
312
  from picarones.core.corpus import load_corpus_from_directory
313
+ from picarones.core.runner import _load_partial, _partial_path
314
 
315
  corpus = load_corpus_from_directory(str(tmp_corpus))
316
  corpus_name = corpus.name
 
518
 
519
  def test_scipy_and_native_agree_on_significance(self):
520
  """Scipy et l'implémentation native doivent s'accorder sur la significativité."""
521
+ from picarones.core.statistics import wilcoxon_test, _SCIPY_AVAILABLE
522
  if not _SCIPY_AVAILABLE:
523
  pytest.skip("scipy non disponible")
524
 
tests/test_sprint14_robust_filtering.py CHANGED
@@ -10,10 +10,8 @@ Vérifie :
10
  """
11
  from __future__ import annotations
12
 
13
- import json
14
  import re
15
  from pathlib import Path
16
- from unittest.mock import MagicMock, patch
17
 
18
  import pytest
19
 
@@ -65,7 +63,8 @@ def _make_fake_benchmark():
65
  def _generate_html(bm=None) -> str:
66
  """Génère le HTML complet du rapport pour un BenchmarkResult minimal."""
67
  from picarones.report.generator import ReportGenerator
68
- import tempfile, os
 
69
  if bm is None:
70
  bm = _make_fake_benchmark()
71
  gen = ReportGenerator(bm)
 
10
  """
11
  from __future__ import annotations
12
 
 
13
  import re
14
  from pathlib import Path
 
15
 
16
  import pytest
17
 
 
63
  def _generate_html(bm=None) -> str:
64
  """Génère le HTML complet du rapport pour un BenchmarkResult minimal."""
65
  from picarones.report.generator import ReportGenerator
66
+ import tempfile
67
+ import os
68
  if bm is None:
69
  bm = _make_fake_benchmark()
70
  gen = ReportGenerator(bm)
tests/test_sprint15_llm_pipeline_bugs.py CHANGED
@@ -7,7 +7,6 @@ Bug 3 : Divergence runner/rapport → cohérence des métriques
7
  from __future__ import annotations
8
 
9
  import logging
10
- from pathlib import Path
11
  from unittest.mock import MagicMock, patch
12
 
13
  import pytest
@@ -153,7 +152,7 @@ class TestMistralAdapterLogging:
153
  adapter = MistralAdapter(model="ministral-3b-latest")
154
 
155
  with caplog.at_level(logging.WARNING, logger="picarones.llm.mistral_adapter"):
156
- result = self._run_adapter(adapter, fake_mod, image_b64="fake_b64")
157
 
158
  # L'appel doit avoir été fait SANS image (modèle text-only)
159
  call_kwargs = mock_client.chat.complete.call_args
@@ -198,7 +197,6 @@ class TestPipelineEmptyLLMResponse:
198
 
199
  def test_warning_on_empty_llm_output(self, tmp_path, caplog):
200
  """WARNING doit être logu si le LLM retourne une chaîne vide."""
201
- import shutil
202
  # Créer une fausse image
203
  img_path = tmp_path / "test.png"
204
  img_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
 
7
  from __future__ import annotations
8
 
9
  import logging
 
10
  from unittest.mock import MagicMock, patch
11
 
12
  import pytest
 
152
  adapter = MistralAdapter(model="ministral-3b-latest")
153
 
154
  with caplog.at_level(logging.WARNING, logger="picarones.llm.mistral_adapter"):
155
+ self._run_adapter(adapter, fake_mod, image_b64="fake_b64")
156
 
157
  # L'appel doit avoir été fait SANS image (modèle text-only)
158
  call_kwargs = mock_client.chat.complete.call_args
 
197
 
198
  def test_warning_on_empty_llm_output(self, tmp_path, caplog):
199
  """WARNING doit être logu si le LLM retourne une chaîne vide."""
 
200
  # Créer une fausse image
201
  img_path = tmp_path / "test.png"
202
  img_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
tests/test_sprint3_llm_pipelines.py CHANGED
@@ -10,8 +10,6 @@ Ces tests couvrent :
10
 
11
  from __future__ import annotations
12
 
13
- import json
14
- import re
15
  from pathlib import Path
16
 
17
  import pytest
@@ -196,7 +194,6 @@ class TestLLMAdapters:
196
  assert r_err.success is False
197
 
198
  def test_missing_api_key_raises(self):
199
- import os
200
  from picarones.llm.openai_adapter import OpenAIAdapter
201
  adapter = OpenAIAdapter()
202
  adapter._api_key = None # simuler clé manquante
 
10
 
11
  from __future__ import annotations
12
 
 
 
13
  from pathlib import Path
14
 
15
  import pytest
 
194
  assert r_err.success is False
195
 
196
  def test_missing_api_key_raises(self):
 
197
  from picarones.llm.openai_adapter import OpenAIAdapter
198
  adapter = OpenAIAdapter()
199
  adapter._api_key = None # simuler clé manquante
tests/test_sprint4_normalization_iiif.py CHANGED
@@ -2,16 +2,10 @@
2
 
3
  from __future__ import annotations
4
 
5
- import json
6
- import os
7
  import pytest
8
 
9
  from picarones.core.normalization import (
10
  NormalizationProfile,
11
- DIPLOMATIC_FR_MEDIEVAL,
12
- DIPLOMATIC_FR_EARLY_MODERN,
13
- DIPLOMATIC_LATIN_MEDIEVAL,
14
- DIPLOMATIC_MINIMAL,
15
  DEFAULT_DIPLOMATIC_PROFILE,
16
  _apply_diplomatic_table,
17
  get_builtin_profile,
@@ -19,7 +13,6 @@ from picarones.core.normalization import (
19
  from picarones.core.metrics import compute_metrics, aggregate_metrics, MetricsResult
20
  from picarones.importers.iiif import (
21
  IIIFManifestParser,
22
- IIIFCanvas,
23
  parse_page_selector,
24
  _extract_label,
25
  _best_image_url_v2,
 
2
 
3
  from __future__ import annotations
4
 
 
 
5
  import pytest
6
 
7
  from picarones.core.normalization import (
8
  NormalizationProfile,
 
 
 
 
9
  DEFAULT_DIPLOMATIC_PROFILE,
10
  _apply_diplomatic_table,
11
  get_builtin_profile,
 
13
  from picarones.core.metrics import compute_metrics, aggregate_metrics, MetricsResult
14
  from picarones.importers.iiif import (
15
  IIIFManifestParser,
 
16
  parse_page_selector,
17
  _extract_label,
18
  _best_image_url_v2,
tests/test_sprint5_advanced_metrics.py CHANGED
@@ -18,7 +18,6 @@ import pytest
18
  # ===========================================================================
19
 
20
  from picarones.core.confusion import (
21
- ConfusionMatrix,
22
  EMPTY_CHAR,
23
  build_confusion_matrix,
24
  aggregate_confusion_matrices,
@@ -149,15 +148,12 @@ class TestTopConfusedChars:
149
 
150
  from picarones.core.char_scores import (
151
  LIGATURE_TABLE,
152
- DIACRITIC_MAP,
153
  LigatureScore,
154
  DiacriticScore,
155
  compute_ligature_score,
156
  compute_diacritic_score,
157
  aggregate_ligature_scores,
158
  aggregate_diacritic_scores,
159
- _ALL_LIGATURES,
160
- _ALL_DIACRITICS,
161
  )
162
 
163
 
@@ -297,7 +293,6 @@ from picarones.core.taxonomy import (
297
  ERROR_CLASSES,
298
  classify_errors,
299
  aggregate_taxonomy,
300
- VISUAL_CONFUSIONS,
301
  )
302
 
303
 
 
18
  # ===========================================================================
19
 
20
  from picarones.core.confusion import (
 
21
  EMPTY_CHAR,
22
  build_confusion_matrix,
23
  aggregate_confusion_matrices,
 
148
 
149
  from picarones.core.char_scores import (
150
  LIGATURE_TABLE,
 
151
  LigatureScore,
152
  DiacriticScore,
153
  compute_ligature_score,
154
  compute_diacritic_score,
155
  aggregate_ligature_scores,
156
  aggregate_diacritic_scores,
 
 
157
  )
158
 
159
 
 
293
  ERROR_CLASSES,
294
  classify_errors,
295
  aggregate_taxonomy,
 
296
  )
297
 
298
 
tests/test_sprint6_web_interface.py CHANGED
@@ -27,11 +27,8 @@ from __future__ import annotations
27
 
28
  import json
29
  import os
30
- import tempfile
31
- import threading
32
- import time
33
  from pathlib import Path
34
- from unittest.mock import MagicMock, patch
35
 
36
  import pytest
37
  from click.testing import CliRunner
@@ -214,7 +211,7 @@ class TestHTRUnitedSearch:
214
  results = htr_catalogue.search(language="French")
215
  assert len(results) > 0
216
  for r in results:
217
- assert any("french" in l.lower() for l in r.language)
218
 
219
  def test_search_by_language_latin(self, htr_catalogue):
220
  results = htr_catalogue.search(language="Latin")
@@ -271,7 +268,7 @@ class TestHTRUnitedImport:
271
  from picarones.importers.htr_united import import_htr_united_corpus
272
  entry = htr_catalogue.entries[0]
273
  new_dir = tmp_path / "new_subdir" / "corpus"
274
- result = import_htr_united_corpus(entry, new_dir, max_samples=5)
275
  assert new_dir.exists()
276
 
277
 
@@ -645,7 +642,7 @@ class TestFastAPIHTRUnited:
645
  assert r.status_code == 200
646
  d = r.json()
647
  for e in d["entries"]:
648
- assert any("french" in l.lower() for l in e["language"])
649
 
650
  def test_import_valid_entry(self, client, tmp_path):
651
  # Get first entry id
@@ -919,7 +916,7 @@ class TestRunnerProgressCallback:
919
  """Le callback est appelé pour chaque document."""
920
  from picarones.core.corpus import load_corpus_from_directory
921
  from picarones.core.runner import run_benchmark
922
- from picarones.engines.base import BaseOCREngine, EngineResult
923
 
924
  class MockEngine(BaseOCREngine):
925
  @property
 
27
 
28
  import json
29
  import os
 
 
 
30
  from pathlib import Path
31
+ from unittest.mock import patch
32
 
33
  import pytest
34
  from click.testing import CliRunner
 
211
  results = htr_catalogue.search(language="French")
212
  assert len(results) > 0
213
  for r in results:
214
+ assert any("french" in lg.lower() for lg in r.language)
215
 
216
  def test_search_by_language_latin(self, htr_catalogue):
217
  results = htr_catalogue.search(language="Latin")
 
268
  from picarones.importers.htr_united import import_htr_united_corpus
269
  entry = htr_catalogue.entries[0]
270
  new_dir = tmp_path / "new_subdir" / "corpus"
271
+ import_htr_united_corpus(entry, new_dir, max_samples=5)
272
  assert new_dir.exists()
273
 
274
 
 
642
  assert r.status_code == 200
643
  d = r.json()
644
  for e in d["entries"]:
645
+ assert any("french" in lg.lower() for lg in e["language"])
646
 
647
  def test_import_valid_entry(self, client, tmp_path):
648
  # Get first entry id
 
916
  """Le callback est appelé pour chaque document."""
917
  from picarones.core.corpus import load_corpus_from_directory
918
  from picarones.core.runner import run_benchmark
919
+ from picarones.engines.base import BaseOCREngine
920
 
921
  class MockEngine(BaseOCREngine):
922
  @property
tests/test_sprint7_advanced_report.py CHANGED
@@ -17,7 +17,6 @@ TestHTMLSprint7Features (10 tests) — HTML généré contient les nouvelles f
17
 
18
  from __future__ import annotations
19
 
20
- import math
21
  import pytest
22
 
23
  # ---------------------------------------------------------------------------
@@ -40,7 +39,8 @@ def report_data_s7(sample_benchmark_s7):
40
  @pytest.fixture
41
  def html_s7(sample_benchmark_s7):
42
  from picarones.report.generator import ReportGenerator
43
- import tempfile, pathlib
 
44
  gen = ReportGenerator(sample_benchmark_s7)
45
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
46
  path = gen.generate(f.name)
 
17
 
18
  from __future__ import annotations
19
 
 
20
  import pytest
21
 
22
  # ---------------------------------------------------------------------------
 
39
  @pytest.fixture
40
  def html_s7(sample_benchmark_s7):
41
  from picarones.report.generator import ReportGenerator
42
+ import tempfile
43
+ import pathlib
44
  gen = ReportGenerator(sample_benchmark_s7)
45
  with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
46
  path = gen.generate(f.name)
tests/test_sprint8_escriptorium_gallica.py CHANGED
@@ -17,10 +17,13 @@ TestCLIRobustness (6 tests) — commande picarones robustness
17
  from __future__ import annotations
18
 
19
  import json
20
- import unittest
21
- from unittest.mock import MagicMock, patch
22
  import pytest
23
 
 
 
 
24
 
25
  # ===========================================================================
26
  # TestEScriptoriumClient
@@ -491,7 +494,6 @@ class TestGallicaOCR:
491
  assert "gallica.bnf.fr" in g._GALLICA_BASE
492
 
493
  def test_ark_normalization_in_import(self):
494
- from picarones.importers.gallica import import_gallica_document, GallicaClient
495
  import re
496
  # Tester que l'ARK est normalisé depuis une URL complète
497
  full_url = "https://gallica.bnf.fr/ark:/12148/btv1b8453561w"
@@ -556,7 +558,9 @@ class TestCLIHistory:
556
  def test_history_empty_db(self):
557
  from click.testing import CliRunner
558
  from picarones.cli import cli
559
- import gc, tempfile, os
 
 
560
  runner = CliRunner()
561
  with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
562
  db_path = f.name
@@ -590,7 +594,8 @@ class TestCLIHistory:
590
  def test_history_export_json(self):
591
  from click.testing import CliRunner
592
  from picarones.cli import cli
593
- import tempfile, os
 
594
  runner = CliRunner()
595
  with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
596
  json_path = f.name
@@ -619,10 +624,10 @@ class TestCLIRobustness:
619
  def test_robustness_demo_mode(self):
620
  from click.testing import CliRunner
621
  from picarones.cli import cli
622
- import tempfile
623
  runner = CliRunner()
624
  with runner.isolated_filesystem():
625
- import os; os.makedirs("corpus")
 
626
  result = runner.invoke(cli, [
627
  "robustness", "--corpus", "corpus", "--engine", "tesseract", "--demo"
628
  ])
@@ -631,10 +636,10 @@ class TestCLIRobustness:
631
  def test_robustness_invalid_degradation(self):
632
  from click.testing import CliRunner
633
  from picarones.cli import cli
634
- import tempfile
635
  runner = CliRunner()
636
  with runner.isolated_filesystem():
637
- import os; os.makedirs("corpus")
 
638
  result = runner.invoke(cli, [
639
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
640
  "--degradations", "invalid_type", "--demo"
@@ -646,7 +651,8 @@ class TestCLIRobustness:
646
  from picarones.cli import cli
647
  runner = CliRunner()
648
  with runner.isolated_filesystem():
649
- import os; os.makedirs("corpus")
 
650
  result = runner.invoke(cli, [
651
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
652
  "--demo", "--degradations", "noise"
@@ -659,7 +665,8 @@ class TestCLIRobustness:
659
  from picarones.cli import cli
660
  runner = CliRunner()
661
  with runner.isolated_filesystem():
662
- import os; os.makedirs("corpus")
 
663
  result = runner.invoke(cli, [
664
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
665
  "--demo", "--output-json", "robustness.json"
@@ -674,7 +681,8 @@ class TestCLIRobustness:
674
  from picarones.cli import cli
675
  runner = CliRunner()
676
  with runner.isolated_filesystem():
677
- import os; os.makedirs("corpus")
 
678
  result = runner.invoke(cli, [
679
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
680
  "--demo", "--degradations", "blur"
 
17
  from __future__ import annotations
18
 
19
  import json
20
+ from typing import TYPE_CHECKING
21
+ from unittest.mock import patch
22
  import pytest
23
 
24
+ if TYPE_CHECKING:
25
+ from picarones.core.results import BenchmarkResult
26
+
27
 
28
  # ===========================================================================
29
  # TestEScriptoriumClient
 
494
  assert "gallica.bnf.fr" in g._GALLICA_BASE
495
 
496
  def test_ark_normalization_in_import(self):
 
497
  import re
498
  # Tester que l'ARK est normalisé depuis une URL complète
499
  full_url = "https://gallica.bnf.fr/ark:/12148/btv1b8453561w"
 
558
  def test_history_empty_db(self):
559
  from click.testing import CliRunner
560
  from picarones.cli import cli
561
+ import gc
562
+ import tempfile
563
+ import os
564
  runner = CliRunner()
565
  with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
566
  db_path = f.name
 
594
  def test_history_export_json(self):
595
  from click.testing import CliRunner
596
  from picarones.cli import cli
597
+ import tempfile
598
+ import os
599
  runner = CliRunner()
600
  with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
601
  json_path = f.name
 
624
  def test_robustness_demo_mode(self):
625
  from click.testing import CliRunner
626
  from picarones.cli import cli
 
627
  runner = CliRunner()
628
  with runner.isolated_filesystem():
629
+ import os
630
+ os.makedirs("corpus")
631
  result = runner.invoke(cli, [
632
  "robustness", "--corpus", "corpus", "--engine", "tesseract", "--demo"
633
  ])
 
636
  def test_robustness_invalid_degradation(self):
637
  from click.testing import CliRunner
638
  from picarones.cli import cli
 
639
  runner = CliRunner()
640
  with runner.isolated_filesystem():
641
+ import os
642
+ os.makedirs("corpus")
643
  result = runner.invoke(cli, [
644
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
645
  "--degradations", "invalid_type", "--demo"
 
651
  from picarones.cli import cli
652
  runner = CliRunner()
653
  with runner.isolated_filesystem():
654
+ import os
655
+ os.makedirs("corpus")
656
  result = runner.invoke(cli, [
657
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
658
  "--demo", "--degradations", "noise"
 
665
  from picarones.cli import cli
666
  runner = CliRunner()
667
  with runner.isolated_filesystem():
668
+ import os
669
+ os.makedirs("corpus")
670
  result = runner.invoke(cli, [
671
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
672
  "--demo", "--output-json", "robustness.json"
 
681
  from picarones.cli import cli
682
  runner = CliRunner()
683
  with runner.isolated_filesystem():
684
+ import os
685
+ os.makedirs("corpus")
686
  result = runner.invoke(cli, [
687
  "robustness", "--corpus", "corpus", "--engine", "tesseract",
688
  "--demo", "--degradations", "blur"