Spaces:
Running
feat(sprint-H.2.b): factory canonique ocr_adapter_from_name
Browse filesSprint H.2.b du plan v2.0 — première brique de la migration des
callers legacy_engines vers les adapters canoniques.
Pourquoi
--------
``picarones.adapters.legacy_engines.factory.engine_from_name``
retourne des ``BaseOCREngine`` (legacy, ``run(image_path)``)
qu'il faut wrapper via ``LegacyOCREngineExecutor`` avant de les
brancher au ``PipelineExecutor``. La factory canonique
retourne directement des ``BaseOCRAdapter`` qui implémentent le
protocole ``StepExecutor`` natif — pas de wrapping nécessaire.
Cette factory est le building block qui permettra la migration
progressive des callers (CLI/web ``_legacy/``,
``_legacy_runner_adapter``) sans toucher la classe ``BaseOCREngine``
elle-même (qui sera supprimée en H.2.d).
Modifications
-------------
- ``picarones/adapters/ocr/factory.py`` (nouveau, ~175 LOC) :
``ocr_adapter_from_name(name, **kwargs) → BaseOCRAdapter``.
Supporte les 6 adapters canoniques (tesseract, pero_ocr,
mistral_ocr, google_vision, azure_doc_intel, precomputed)
avec alias courts (``tess``, ``pero``, ``mistral``,
``google``/``gv``, ``azure``/``adi``). Insensible à la casse.
- ``picarones/adapters/ocr/__init__.py`` : exporte
``ocr_adapter_from_name``.
- ``picarones/adapters/legacy_engines/factory.py`` : docstring
mise à jour pour pointer vers la canonique et préciser le
calendrier de retrait (H.2.d avec ``BaseOCREngine``).
Tests
-----
- ``tests/adapters/ocr/test_factory.py`` (nouveau, 13 tests) :
- ``TestTesseract`` (6) : nom canonique, alias ``tess``,
casse normalisée, kwargs propagés, faute de frappe lève
``TypeError`` (pas masqué), psm invalide lève
``OCRAdapterError``.
- ``TestPrecomputed`` (1) : sans dep optionnelle.
- ``TestCloudAdapters`` (3) : mistral/google/azure
instanciables sans credentials (résolution paresseuse à
``execute()``).
- ``TestPeroOCR`` (1) : tolérant à l'absence de ``pero-ocr``
avec message d'erreur explicite.
- ``TestUnknownName`` (2) : nom inconnu liste les supportés
+ alias ; nom vide lève.
Comparaison avec la factory legacy
----------------------------------
| Aspect | legacy_engines.engine_from_name | ocr.ocr_adapter_from_name |
|--------|--------------------------------|---------------------------|
| Retour | ``BaseOCREngine`` (legacy) | ``BaseOCRAdapter`` (canonique) |
| Protocole | ``run(image_path) → EngineResult`` | ``execute(inputs, params, ctx)`` |
| Wrapping | ``LegacyOCREngineExecutor`` requis | ✅ direct |
| Adapters | tesseract, pero_ocr | tesseract, pero_ocr, mistral_ocr, google_vision, azure_doc_intel, precomputed |
| Alias | aucun | tess, pero, mistral, google/gv, azure/adi |
| kwargs | ``lang``, ``psm`` (positionnels) | tous les kwargs du constructeur |
Tests : 4663 passed, 9 skipped, 24 deselected.
Reste pour v2.0
---------------
- H.2.b suite : migrer les callers de ``engine_from_name`` (CLI
``_workflows.py``, ``_robustness.py`` ; web
``benchmark_utils.py``) à utiliser ``ocr_adapter_from_name``.
- H.2.c : suppression ``OCRLLMPipeline`` (callers basculent à
``make_ocr_llm_pipeline_spec`` directement).
- H.2.d : suppression ``BaseOCREngine`` + ``adapters/legacy_engines/``.
- H.4 : refonte interfaces/{cli,web}/_legacy/.
- H.6 : bump version + tag v2.0.0.
https://claude.ai/code/session_01NxyVKqg2SowXLZdM4H1ZDE
- CLAUDE.md +3 -3
- README.md +1 -1
- picarones/adapters/legacy_engines/factory.py +10 -15
- picarones/adapters/ocr/__init__.py +2 -0
- picarones/adapters/ocr/factory.py +184 -0
- tests/adapters/ocr/test_factory.py +125 -0
|
@@ -123,7 +123,7 @@ picarones/
|
|
| 123 |
|
| 124 |
## État des tests et bugs historiques
|
| 125 |
|
| 126 |
-
`pytest tests/` → **
|
| 127 |
(post-S59). Les deselected sont les markers `live` (5 tests d'intégration
|
| 128 |
contre vraie API/binaire) + `network` (3 tests qui hit le réseau réel),
|
| 129 |
opt-in en local via `pytest -m live` ou `pytest -m network`. Le
|
|
@@ -252,7 +252,7 @@ Résumé express :
|
|
| 252 |
|
| 253 |
1. `git branch --show-current` → `claude/repo-analysis-cukvm`.
|
| 254 |
2. `git status` → working tree clean.
|
| 255 |
-
3. `pytest tests/ -q --no-header --tb=line` →
|
| 256 |
4. `git log -1 --format=%B` → décrit la prochaine sub-phase.
|
| 257 |
|
| 258 |
**Règles d'architecture critiques** (apprises à la dure) :
|
|
@@ -340,7 +340,7 @@ détecte, arbitre, rend.
|
|
| 340 |
## Contexte développement
|
| 341 |
|
| 342 |
- **Environnement** : GitHub Codespaces, Python 3.11+
|
| 343 |
-
- **Tests** : `pytest tests/ -q` →
|
| 344 |
deselected, 0 failed (au moment de la pause de session).
|
| 345 |
- **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
|
| 346 |
- **Plan retrait du legacy (maître)** : [`docs/migration/legacy-retirement-plan.md`](docs/migration/legacy-retirement-plan.md).
|
|
|
|
| 123 |
|
| 124 |
## État des tests et bugs historiques
|
| 125 |
|
| 126 |
+
`pytest tests/` → **4690 passed, 12 skipped, 8 deselected, 0 failed**
|
| 127 |
(post-S59). Les deselected sont les markers `live` (5 tests d'intégration
|
| 128 |
contre vraie API/binaire) + `network` (3 tests qui hit le réseau réel),
|
| 129 |
opt-in en local via `pytest -m live` ou `pytest -m network`. Le
|
|
|
|
| 252 |
|
| 253 |
1. `git branch --show-current` → `claude/repo-analysis-cukvm`.
|
| 254 |
2. `git status` → working tree clean.
|
| 255 |
+
3. `pytest tests/ -q --no-header --tb=line` → 4690 passed.
|
| 256 |
4. `git log -1 --format=%B` → décrit la prochaine sub-phase.
|
| 257 |
|
| 258 |
**Règles d'architecture critiques** (apprises à la dure) :
|
|
|
|
| 340 |
## Contexte développement
|
| 341 |
|
| 342 |
- **Environnement** : GitHub Codespaces, Python 3.11+
|
| 343 |
+
- **Tests** : `pytest tests/ -q` → 4690 passed, 12 skipped, 24
|
| 344 |
deselected, 0 failed (au moment de la pause de session).
|
| 345 |
- **Plan d'évolution actif** : [`docs/roadmap/evolution-2026.md`](docs/roadmap/evolution-2026.md).
|
| 346 |
- **Plan retrait du legacy (maître)** : [`docs/migration/legacy-retirement-plan.md`](docs/migration/legacy-retirement-plan.md).
|
|
@@ -395,7 +395,7 @@ ruff check picarones/ tests/
|
|
| 395 |
python -m mypy picarones/core/
|
| 396 |
```
|
| 397 |
|
| 398 |
-
**Test suite**: ~
|
| 399 |
floor at 85% (currently ~87%). The `network` marker excludes tests
|
| 400 |
requiring live HTTP. A handful of tests depend on optional engines
|
| 401 |
(`pero-ocr`, `pytesseract`) and are skipped/fail gracefully when
|
|
|
|
| 395 |
python -m mypy picarones/core/
|
| 396 |
```
|
| 397 |
|
| 398 |
+
**Test suite**: ~4690 tests, ~3 min on a modern laptop. Coverage
|
| 399 |
floor at 85% (currently ~87%). The `network` marker excludes tests
|
| 400 |
requiring live HTTP. A handful of tests depend on optional engines
|
| 401 |
(`pero-ocr`, `pytesseract`) and are skipped/fail gracefully when
|
|
@@ -1,23 +1,18 @@
|
|
| 1 |
-
"""Factory : instancier un
|
| 2 |
|
| 3 |
Phase 7.A — module relocalisé depuis ``picarones.engines.factory``
|
| 4 |
-
vers ``picarones.adapters.legacy_engines.factory``.
|
| 5 |
-
reste disponible via un shim avec ``DeprecationWarning`` ;
|
| 6 |
-
suppression prévue en 2.0.
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
``
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
pas de FastAPI). Les erreurs sont signalées via ``ValueError``, le CLI
|
| 16 |
-
les retraduit en ``click.BadParameter`` et l'API web les convertit en
|
| 17 |
-
warning utilisateur.
|
| 18 |
|
| 19 |
Discipline : ne pas importer ``click`` ici, sous peine de remonter une
|
| 20 |
-
dépendance
|
| 21 |
"""
|
| 22 |
|
| 23 |
from __future__ import annotations
|
|
|
|
| 1 |
+
"""Factory legacy : instancier un ``BaseOCREngine`` à partir de son nom court.
|
| 2 |
|
| 3 |
Phase 7.A — module relocalisé depuis ``picarones.engines.factory``
|
| 4 |
+
vers ``picarones.adapters.legacy_engines.factory``.
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
Sprint H.2.b du plan v2.0 — équivalent canonique disponible :
|
| 7 |
+
``picarones.adapters.ocr.factory.ocr_adapter_from_name`` retourne
|
| 8 |
+
des ``BaseOCRAdapter`` (StepExecutor Protocol) directement
|
| 9 |
+
consommables par ``PipelineExecutor`` sans ``LegacyOCREngineExecutor``.
|
| 10 |
+
Les nouveaux callers doivent utiliser la factory canonique. Cette
|
| 11 |
+
factory ne sera supprimée qu'avec ``BaseOCREngine`` lui-même
|
| 12 |
+
(H.2.d).
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
Discipline : ne pas importer ``click`` ici, sous peine de remonter une
|
| 15 |
+
dépendance interfaces dans la couche adapters.
|
| 16 |
"""
|
| 17 |
|
| 18 |
from __future__ import annotations
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
| 21 |
|
| 22 |
from picarones.adapters.ocr.azure_doc_intel import AzureDocIntelAdapter
|
| 23 |
from picarones.adapters.ocr.base import BaseOCRAdapter, OCRAdapterError
|
|
|
|
| 24 |
from picarones.adapters.ocr.google_vision import GoogleVisionAdapter
|
| 25 |
from picarones.adapters.ocr.mistral_ocr import MistralOCRAdapter
|
| 26 |
from picarones.adapters.ocr.pero_ocr import PeroOCRAdapter
|
|
@@ -36,4 +37,5 @@ __all__ = [
|
|
| 36 |
"PeroOCRAdapter",
|
| 37 |
"PrecomputedTextAdapter",
|
| 38 |
"TesseractAdapter",
|
|
|
|
| 39 |
]
|
|
|
|
| 21 |
|
| 22 |
from picarones.adapters.ocr.azure_doc_intel import AzureDocIntelAdapter
|
| 23 |
from picarones.adapters.ocr.base import BaseOCRAdapter, OCRAdapterError
|
| 24 |
+
from picarones.adapters.ocr.factory import ocr_adapter_from_name
|
| 25 |
from picarones.adapters.ocr.google_vision import GoogleVisionAdapter
|
| 26 |
from picarones.adapters.ocr.mistral_ocr import MistralOCRAdapter
|
| 27 |
from picarones.adapters.ocr.pero_ocr import PeroOCRAdapter
|
|
|
|
| 37 |
"PeroOCRAdapter",
|
| 38 |
"PrecomputedTextAdapter",
|
| 39 |
"TesseractAdapter",
|
| 40 |
+
"ocr_adapter_from_name",
|
| 41 |
]
|
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Factory canonique : instancier un ``BaseOCRAdapter`` par nom court.
|
| 2 |
+
|
| 3 |
+
Sprint H.2.b du plan v2.0 — équivalent canonique de
|
| 4 |
+
``picarones.adapters.legacy_engines.factory.engine_from_name`` qui
|
| 5 |
+
retournait des ``BaseOCREngine`` (legacy, ``run(image_path) →
|
| 6 |
+
EngineResult``). Cette factory retourne des ``BaseOCRAdapter``
|
| 7 |
+
(rewrite, ``StepExecutor`` Protocol, ``execute(inputs, params,
|
| 8 |
+
context) → dict[ArtifactType, Artifact]``).
|
| 9 |
+
|
| 10 |
+
Pourquoi ici
|
| 11 |
+
------------
|
| 12 |
+
Vit en couche 5 (``picarones.adapters.ocr``) plutôt qu'en
|
| 13 |
+
``app/`` parce que c'est de la logique de catalogue OCR — la CLI
|
| 14 |
+
(couche 8) et la web API (couche 8) la consomment toutes les deux.
|
| 15 |
+
Cette factory ne dépend d'aucune brique de couche supérieure
|
| 16 |
+
(pas de ``click``, pas de FastAPI).
|
| 17 |
+
|
| 18 |
+
Migration depuis le legacy
|
| 19 |
+
--------------------------
|
| 20 |
+
Code legacy ::
|
| 21 |
+
|
| 22 |
+
from picarones.adapters.legacy_engines.factory import engine_from_name
|
| 23 |
+
engine = engine_from_name("tesseract", lang="fra", psm=6)
|
| 24 |
+
# engine est un BaseOCREngine, à wrapper via LegacyOCREngineExecutor
|
| 25 |
+
# avant de pouvoir être consommé par PipelineExecutor.
|
| 26 |
+
|
| 27 |
+
Code canonique équivalent ::
|
| 28 |
+
|
| 29 |
+
from picarones.adapters.ocr.factory import ocr_adapter_from_name
|
| 30 |
+
adapter = ocr_adapter_from_name("tesseract", lang="fra", psm=6)
|
| 31 |
+
# adapter est un BaseOCRAdapter — déjà un StepExecutor, peut
|
| 32 |
+
# être directement enregistré dans un adapter_resolver et
|
| 33 |
+
# consommé par PipelineExecutor sans wrapping.
|
| 34 |
+
|
| 35 |
+
Alias supportés
|
| 36 |
+
---------------
|
| 37 |
+
- ``tesseract`` / ``tess``
|
| 38 |
+
- ``pero_ocr`` / ``pero``
|
| 39 |
+
- ``mistral_ocr`` / ``mistral``
|
| 40 |
+
- ``google_vision`` / ``google`` / ``gv``
|
| 41 |
+
- ``azure_doc_intel`` / ``azure`` / ``adi``
|
| 42 |
+
- ``precomputed``
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
from __future__ import annotations
|
| 46 |
+
|
| 47 |
+
from typing import Any
|
| 48 |
+
|
| 49 |
+
from picarones.adapters.ocr.base import BaseOCRAdapter
|
| 50 |
+
|
| 51 |
+
#: Mapping ``alias → nom canonique`` pour les noms abrégés.
|
| 52 |
+
_ALIASES: dict[str, str] = {
|
| 53 |
+
"tess": "tesseract",
|
| 54 |
+
"pero": "pero_ocr",
|
| 55 |
+
"mistral": "mistral_ocr",
|
| 56 |
+
"google": "google_vision",
|
| 57 |
+
"gv": "google_vision",
|
| 58 |
+
"azure": "azure_doc_intel",
|
| 59 |
+
"adi": "azure_doc_intel",
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
#: Liste des noms canoniques supportés pour les messages d'erreur.
|
| 63 |
+
_SUPPORTED: tuple[str, ...] = (
|
| 64 |
+
"tesseract",
|
| 65 |
+
"pero_ocr",
|
| 66 |
+
"mistral_ocr",
|
| 67 |
+
"google_vision",
|
| 68 |
+
"azure_doc_intel",
|
| 69 |
+
"precomputed",
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def ocr_adapter_from_name(
|
| 74 |
+
name: str, **kwargs: Any,
|
| 75 |
+
) -> BaseOCRAdapter:
|
| 76 |
+
"""Instancie un ``BaseOCRAdapter`` canonique par son nom court.
|
| 77 |
+
|
| 78 |
+
Parameters
|
| 79 |
+
----------
|
| 80 |
+
name:
|
| 81 |
+
Identifiant court du moteur (cf. liste des alias dans le
|
| 82 |
+
docstring du module). Insensible à la casse.
|
| 83 |
+
**kwargs:
|
| 84 |
+
Arguments propagés au constructeur de l'adapter cible.
|
| 85 |
+
Les kwargs non reconnus par le constructeur lèveront un
|
| 86 |
+
``TypeError`` — c'est intentionnel, on ne masque pas les
|
| 87 |
+
fautes de frappe.
|
| 88 |
+
|
| 89 |
+
Returns
|
| 90 |
+
-------
|
| 91 |
+
BaseOCRAdapter
|
| 92 |
+
Instance prête à être enregistrée dans un
|
| 93 |
+
``adapter_resolver`` et consommée par ``PipelineExecutor``.
|
| 94 |
+
|
| 95 |
+
Raises
|
| 96 |
+
------
|
| 97 |
+
ValueError
|
| 98 |
+
Si ``name`` est inconnu, ou si l'adapter cible nécessite
|
| 99 |
+
une dépendance optionnelle non installée (ex : Pero OCR
|
| 100 |
+
sans ``pero-ocr``). Le message d'erreur inclut la liste
|
| 101 |
+
des moteurs effectivement supportés.
|
| 102 |
+
|
| 103 |
+
Examples
|
| 104 |
+
--------
|
| 105 |
+
>>> adapter = ocr_adapter_from_name("tesseract", lang="fra")
|
| 106 |
+
>>> adapter.name
|
| 107 |
+
'tesseract'
|
| 108 |
+
|
| 109 |
+
>>> adapter = ocr_adapter_from_name("tess") # alias
|
| 110 |
+
>>> adapter.name
|
| 111 |
+
'tesseract'
|
| 112 |
+
|
| 113 |
+
>>> adapter = ocr_adapter_from_name(
|
| 114 |
+
... "precomputed", source_label="bnf_jean_zay",
|
| 115 |
+
... )
|
| 116 |
+
>>> adapter.name
|
| 117 |
+
'precomputed:bnf_jean_zay'
|
| 118 |
+
"""
|
| 119 |
+
canonical = _ALIASES.get(name.lower(), name.lower())
|
| 120 |
+
|
| 121 |
+
if canonical == "tesseract":
|
| 122 |
+
from picarones.adapters.ocr.tesseract import TesseractAdapter
|
| 123 |
+
return TesseractAdapter(**kwargs)
|
| 124 |
+
|
| 125 |
+
if canonical == "pero_ocr":
|
| 126 |
+
try:
|
| 127 |
+
from picarones.adapters.ocr.pero_ocr import PeroOCRAdapter
|
| 128 |
+
except ImportError as exc:
|
| 129 |
+
raise ValueError(
|
| 130 |
+
f"Adapter 'pero_ocr' indisponible : {exc}. "
|
| 131 |
+
"Installer la dépendance optionnelle ``pero-ocr``."
|
| 132 |
+
) from exc
|
| 133 |
+
return PeroOCRAdapter(**kwargs)
|
| 134 |
+
|
| 135 |
+
if canonical == "mistral_ocr":
|
| 136 |
+
try:
|
| 137 |
+
from picarones.adapters.ocr.mistral_ocr import MistralOCRAdapter
|
| 138 |
+
except ImportError as exc:
|
| 139 |
+
raise ValueError(
|
| 140 |
+
f"Adapter 'mistral_ocr' indisponible : {exc}. "
|
| 141 |
+
"Installer la dépendance optionnelle ``mistralai``."
|
| 142 |
+
) from exc
|
| 143 |
+
return MistralOCRAdapter(**kwargs)
|
| 144 |
+
|
| 145 |
+
if canonical == "google_vision":
|
| 146 |
+
try:
|
| 147 |
+
from picarones.adapters.ocr.google_vision import (
|
| 148 |
+
GoogleVisionAdapter,
|
| 149 |
+
)
|
| 150 |
+
except ImportError as exc:
|
| 151 |
+
raise ValueError(
|
| 152 |
+
f"Adapter 'google_vision' indisponible : {exc}. "
|
| 153 |
+
"Installer la dépendance optionnelle "
|
| 154 |
+
"``google-cloud-vision``."
|
| 155 |
+
) from exc
|
| 156 |
+
return GoogleVisionAdapter(**kwargs)
|
| 157 |
+
|
| 158 |
+
if canonical == "azure_doc_intel":
|
| 159 |
+
try:
|
| 160 |
+
from picarones.adapters.ocr.azure_doc_intel import (
|
| 161 |
+
AzureDocIntelAdapter,
|
| 162 |
+
)
|
| 163 |
+
except ImportError as exc:
|
| 164 |
+
raise ValueError(
|
| 165 |
+
f"Adapter 'azure_doc_intel' indisponible : {exc}. "
|
| 166 |
+
"Installer la dépendance optionnelle "
|
| 167 |
+
"``azure-ai-formrecognizer``."
|
| 168 |
+
) from exc
|
| 169 |
+
return AzureDocIntelAdapter(**kwargs)
|
| 170 |
+
|
| 171 |
+
if canonical == "precomputed":
|
| 172 |
+
from picarones.adapters.ocr.precomputed import (
|
| 173 |
+
PrecomputedTextAdapter,
|
| 174 |
+
)
|
| 175 |
+
return PrecomputedTextAdapter(**kwargs)
|
| 176 |
+
|
| 177 |
+
raise ValueError(
|
| 178 |
+
f"Moteur OCR inconnu : {name!r}. Valeurs supportées : "
|
| 179 |
+
f"{', '.join(_SUPPORTED)} (alias : "
|
| 180 |
+
f"{', '.join(sorted(_ALIASES))}).",
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
__all__ = ["ocr_adapter_from_name"]
|
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sprint H.2.b — factory canonique ``ocr_adapter_from_name``.
|
| 2 |
+
|
| 3 |
+
Vérifie l'équivalent canonique de
|
| 4 |
+
``picarones.adapters.legacy_engines.factory.engine_from_name`` :
|
| 5 |
+
|
| 6 |
+
- Résolution des alias (``tess`` → ``tesseract``, etc.) ;
|
| 7 |
+
- Construction effective des 6 adapters supportés (1 sans deps,
|
| 8 |
+
4 cloud avec deps optionnelles, 1 precomputed) ;
|
| 9 |
+
- ``ValueError`` propre sur nom inconnu / dépendance absente,
|
| 10 |
+
avec message d'erreur listant les moteurs supportés ;
|
| 11 |
+
- Insensibilité à la casse du nom.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import pytest
|
| 17 |
+
|
| 18 |
+
from picarones.adapters.ocr import ocr_adapter_from_name
|
| 19 |
+
from picarones.adapters.ocr.base import BaseOCRAdapter
|
| 20 |
+
from picarones.adapters.ocr.tesseract import TesseractAdapter
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TestTesseract:
|
| 24 |
+
def test_canonical_name(self) -> None:
|
| 25 |
+
adapter = ocr_adapter_from_name("tesseract")
|
| 26 |
+
assert isinstance(adapter, TesseractAdapter)
|
| 27 |
+
assert adapter.name == "tesseract"
|
| 28 |
+
|
| 29 |
+
def test_alias_tess(self) -> None:
|
| 30 |
+
adapter = ocr_adapter_from_name("tess")
|
| 31 |
+
assert isinstance(adapter, TesseractAdapter)
|
| 32 |
+
# L'alias normalise vers le nom canonique "tesseract".
|
| 33 |
+
assert adapter.name == "tesseract"
|
| 34 |
+
|
| 35 |
+
def test_uppercase_name_normalized(self) -> None:
|
| 36 |
+
adapter = ocr_adapter_from_name("Tesseract")
|
| 37 |
+
assert isinstance(adapter, TesseractAdapter)
|
| 38 |
+
|
| 39 |
+
def test_kwargs_propagate(self) -> None:
|
| 40 |
+
adapter = ocr_adapter_from_name(
|
| 41 |
+
"tesseract", lang="eng", psm=3,
|
| 42 |
+
)
|
| 43 |
+
assert adapter.lang == "eng"
|
| 44 |
+
|
| 45 |
+
def test_invalid_kwarg_raises_typeerror(self) -> None:
|
| 46 |
+
# Pas de masquage des fautes de frappe.
|
| 47 |
+
with pytest.raises(TypeError):
|
| 48 |
+
ocr_adapter_from_name("tesseract", langg="fra")
|
| 49 |
+
|
| 50 |
+
def test_invalid_psm_raises_ocr_adapter_error(self) -> None:
|
| 51 |
+
from picarones.adapters.ocr.base import OCRAdapterError
|
| 52 |
+
with pytest.raises(OCRAdapterError, match="psm"):
|
| 53 |
+
ocr_adapter_from_name("tesseract", psm=99)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class TestPrecomputed:
|
| 57 |
+
"""``precomputed`` n'a pas de dépendance optionnelle — doit
|
| 58 |
+
toujours être instanciable."""
|
| 59 |
+
|
| 60 |
+
def test_canonical_name(self) -> None:
|
| 61 |
+
adapter = ocr_adapter_from_name(
|
| 62 |
+
"precomputed", source_label="bnf",
|
| 63 |
+
)
|
| 64 |
+
assert isinstance(adapter, BaseOCRAdapter)
|
| 65 |
+
assert "bnf" in adapter.name
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class TestCloudAdapters:
|
| 69 |
+
"""Les adapters cloud sont importables sans la dépendance
|
| 70 |
+
système (pas de credentials nécessaires à l'instanciation —
|
| 71 |
+
la lib client est résolue paresseusement à execute())."""
|
| 72 |
+
|
| 73 |
+
def test_mistral_ocr_via_alias(self) -> None:
|
| 74 |
+
adapter = ocr_adapter_from_name(
|
| 75 |
+
"mistral", model="mistral-ocr-latest", api_key="fake",
|
| 76 |
+
)
|
| 77 |
+
assert isinstance(adapter, BaseOCRAdapter)
|
| 78 |
+
assert adapter.name == "mistral_ocr"
|
| 79 |
+
|
| 80 |
+
def test_google_vision_via_alias(self) -> None:
|
| 81 |
+
adapter = ocr_adapter_from_name(
|
| 82 |
+
"google", api_key="fake",
|
| 83 |
+
)
|
| 84 |
+
assert isinstance(adapter, BaseOCRAdapter)
|
| 85 |
+
assert adapter.name == "google_vision"
|
| 86 |
+
|
| 87 |
+
def test_azure_doc_intel_via_alias(self) -> None:
|
| 88 |
+
adapter = ocr_adapter_from_name(
|
| 89 |
+
"azure", endpoint="https://x.com", api_key="fake",
|
| 90 |
+
)
|
| 91 |
+
assert isinstance(adapter, BaseOCRAdapter)
|
| 92 |
+
assert adapter.name == "azure_doc_intel"
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class TestPeroOCR:
|
| 96 |
+
"""Pero OCR a une dépendance optionnelle ``pero-ocr`` — peut
|
| 97 |
+
être absent dans l'environnement de test."""
|
| 98 |
+
|
| 99 |
+
def test_canonical_or_helpful_error(self, tmp_path) -> None:
|
| 100 |
+
cfg = tmp_path / "fake_pero.ini"
|
| 101 |
+
cfg.write_text("# fake config", encoding="utf-8")
|
| 102 |
+
try:
|
| 103 |
+
adapter = ocr_adapter_from_name(
|
| 104 |
+
"pero_ocr", config_path=str(cfg),
|
| 105 |
+
)
|
| 106 |
+
assert isinstance(adapter, BaseOCRAdapter)
|
| 107 |
+
except ValueError as exc:
|
| 108 |
+
# Si ``pero-ocr`` n'est pas installé, on attend un
|
| 109 |
+
# message d'erreur qui explique comment l'installer.
|
| 110 |
+
assert "pero-ocr" in str(exc).lower()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class TestUnknownName:
|
| 114 |
+
def test_unknown_raises_with_supported_list(self) -> None:
|
| 115 |
+
with pytest.raises(ValueError) as ctx:
|
| 116 |
+
ocr_adapter_from_name("not_a_real_engine")
|
| 117 |
+
msg = str(ctx.value)
|
| 118 |
+
# Le message liste les moteurs supportés et les alias —
|
| 119 |
+
# utile pour le diagnostic.
|
| 120 |
+
assert "tesseract" in msg
|
| 121 |
+
assert "alias" in msg.lower()
|
| 122 |
+
|
| 123 |
+
def test_empty_name_raises(self) -> None:
|
| 124 |
+
with pytest.raises(ValueError):
|
| 125 |
+
ocr_adapter_from_name("")
|