ocr: lang= is a real parameter now, and the default is auto-recognition (v0.3.23)

/v1/ocr has accepted `lang` since the beginning, with the comment "reserved": it
was parsed and dropped. A client asking for Italian got whatever the install was
configured for and no way to tell.

It is honoured now, on /v1/ocr, /v1/ocr/batch and the pipeline step, with
precedence request > ocr.<engine>_lang > ocr.lang > auto. The request beats the
engine-specific setting deliberately: that setting is an install's default, while
a caller naming a language knows something about the document in hand. Asking for
`auto` explicitly is NOT the same as sending nothing, so an install configured
for Italian can still be asked to work it out.

The default is auto-recognition rather than any particular language, because a
server that has not been told what it is reading should not assume one. For
surya-2, olmOCR and docTR that is simply what they already do: they are
multilingual and have NO language input to constrain -- surya-2's entire prompt is
"OCR this block image to HTML." and its own module warns the wording is the
training-time contract, so there is nowhere to put a language and nothing to gain.
Rather than let `lang` look effective everywhere, every response now carries what
was applied AND whether the engine that served could act on it
("lang": "it", "lang_applied": false), and /v1/ocr/engines reports the same per
engine so a client can choose before sending.

PaddleOCR is the exception that shapes the rest. It loads a language-specific
recognition model and its options are sent once at load, so an instance is built
for one language and cannot be re-languaged. Hence: `auto` becomes
ocr.paddle_auto_lang (default `latin`, a SCRIPT family rather than detection --
PaddleOCR's own default is `ch`, a worse guess for most documents than no guess),
and paddle gets one pool per language, keyed on the model it is BUILT with so
`auto` and `latin` share a pool instead of loading it twice. The multilingual
engines keep a single pool whatever is asked. That is not tidiness: a
language-keyed pool is a second set of loaded instances holding VRAM, and a
production install runs 24 surya instances. Both pool-iteration sites already use
.values(), and _Pool keeps the plain engine name, so eviction and the VRAM
releaser handle the new keys unchanged.

ocr.lang's shipped default moves "it" -> "auto"; installs that set it explicitly
(Digesta production does) are unaffected. ocr.surya_langs stays as the legacy
setting it is -- it reaches only the <=0.17 recognition API and does nothing on
the pod path.

21 tests. The 8 failures in test_broker_config / test_studio_composed_surfaces /
test_whisper_server_local_models are pre-existing, verified at HEAD.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PxYHWbCAjjjD7A1rhFKtaW
parent dc86733f
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.3.22" __version__ = "0.3.23"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -4120,6 +4120,7 @@ def build_settings_dict(c, gpu_cards): ...@@ -4120,6 +4120,7 @@ def build_settings_dict(c, gpu_cards):
"surya_dpi": c.ocr.surya_dpi, "surya_dpi": c.ocr.surya_dpi,
"max_concurrency": c.ocr.max_concurrency, "max_concurrency": c.ocr.max_concurrency,
"lang": c.ocr.lang, "lang": c.ocr.lang,
"paddle_auto_lang": c.ocr.paddle_auto_lang,
"paddle_enabled": c.ocr.paddle_enabled, "paddle_enabled": c.ocr.paddle_enabled,
"paddle_use_gpu": c.ocr.paddle_use_gpu, "paddle_use_gpu": c.ocr.paddle_use_gpu,
"paddle_instances": c.ocr.paddle_instances, "paddle_instances": c.ocr.paddle_instances,
...@@ -4917,7 +4918,8 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad ...@@ -4917,7 +4918,8 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
if "dpi" in d: o.dpi = _i("dpi", o.dpi, 36) if "dpi" in d: o.dpi = _i("dpi", o.dpi, 36)
if "surya_dpi" in d: o.surya_dpi = _i("surya_dpi", o.surya_dpi, 0) if "surya_dpi" in d: o.surya_dpi = _i("surya_dpi", o.surya_dpi, 0)
if "max_concurrency" in d: o.max_concurrency = _i("max_concurrency", o.max_concurrency, 1) if "max_concurrency" in d: o.max_concurrency = _i("max_concurrency", o.max_concurrency, 1)
if "lang" in d: o.lang = (d.get("lang") or "it").strip() if "lang" in d: o.lang = (d.get("lang") or "auto").strip()
if "paddle_auto_lang" in d: o.paddle_auto_lang = (d.get("paddle_auto_lang") or "latin").strip()
# PaddleOCR # PaddleOCR
if "paddle_enabled" in d: o.paddle_enabled = bool(d["paddle_enabled"]) if "paddle_enabled" in d: o.paddle_enabled = bool(d["paddle_enabled"])
if "paddle_use_gpu" in d: o.paddle_use_gpu = bool(d["paddle_use_gpu"]) if "paddle_use_gpu" in d: o.paddle_use_gpu = bool(d["paddle_use_gpu"])
......
...@@ -1115,9 +1115,22 @@ ...@@ -1115,9 +1115,22 @@
<label class="form-label">Max concurrency</label> <label class="form-label">Max concurrency</label>
<input type="number" id="s-ocr-max-concurrency" class="form-input" min="1" placeholder="4"> <input type="number" id="s-ocr-max-concurrency" class="form-input" min="1" placeholder="4">
</div> </div>
<div class="form-group" style="flex:1;min-width:90px"> <div class="form-group" style="flex:1;min-width:140px">
<label class="form-label">Language</label> <label class="form-label">Language</label>
<input type="text" id="s-ocr-lang" class="form-input" placeholder="it"> <input type="text" id="s-ocr-lang" class="form-input" placeholder="auto">
<span class="form-hint">Default when a request sends no <code>lang</code>.
<b>auto</b> lets each engine recognise what it finds. Surya-2, olmOCR and docTR
are multilingual and have no language input at all, so <code>lang</code> never
steers them; only PaddleOCR is built for one language.</span>
</div>
<div class="form-group" style="flex:1;min-width:140px">
<label class="form-label">Paddle "auto" language</label>
<input type="text" id="s-ocr-paddle-auto-lang" class="form-input" placeholder="latin">
<span class="form-hint">PaddleOCR has no auto mode, so <b>auto</b> becomes this.
A script family, not detection: <code>latin</code> covers Italian, French,
Spanish, German and the rest of the Latin alphabet. Another script needs naming
(<code>arabic</code>, <code>japan</code>, <code>cyrillic</code>,
<code>devanagari</code>) or one of the multilingual engines.</span>
</div> </div>
<div class="form-group" style="flex:1;min-width:130px"> <div class="form-group" style="flex:1;min-width:130px">
<label class="form-label">Retry cooldown (s)</label> <label class="form-label">Retry cooldown (s)</label>
...@@ -2122,7 +2135,8 @@ async function loadSettings(){ ...@@ -2122,7 +2135,8 @@ async function loadSettings(){
document.getElementById('s-ocr-dpi').value = ocr.dpi ?? 200; document.getElementById('s-ocr-dpi').value = ocr.dpi ?? 200;
document.getElementById('s-ocr-surya-dpi').value = ocr.surya_dpi ?? 250; document.getElementById('s-ocr-surya-dpi').value = ocr.surya_dpi ?? 250;
document.getElementById('s-ocr-max-concurrency').value = ocr.max_concurrency ?? 4; document.getElementById('s-ocr-max-concurrency').value = ocr.max_concurrency ?? 4;
document.getElementById('s-ocr-lang').value = ocr.lang ?? 'it'; document.getElementById('s-ocr-lang').value = ocr.lang ?? 'auto';
document.getElementById('s-ocr-paddle-auto-lang').value = ocr.paddle_auto_lang ?? 'latin';
document.getElementById('s-ocr-retry-cooldown').value = ocr.build_retry_cooldown_s ?? 60; document.getElementById('s-ocr-retry-cooldown').value = ocr.build_retry_cooldown_s ?? 60;
document.getElementById('s-ocr-vlm-gmu').value = ocr.vlm_gpu_memory_utilization ?? 0; document.getElementById('s-ocr-vlm-gmu').value = ocr.vlm_gpu_memory_utilization ?? 0;
document.getElementById('s-ocr-fallback').value = ocr.fallback_engines ?? 'auto'; document.getElementById('s-ocr-fallback').value = ocr.fallback_engines ?? 'auto';
...@@ -2377,7 +2391,8 @@ async function saveSettings(silent){ ...@@ -2377,7 +2391,8 @@ async function saveSettings(silent){
dpi: parseInt(document.getElementById('s-ocr-dpi').value) || 200, dpi: parseInt(document.getElementById('s-ocr-dpi').value) || 200,
surya_dpi: parseInt(document.getElementById('s-ocr-surya-dpi').value) || 0, surya_dpi: parseInt(document.getElementById('s-ocr-surya-dpi').value) || 0,
max_concurrency: parseInt(document.getElementById('s-ocr-max-concurrency').value) || 4, max_concurrency: parseInt(document.getElementById('s-ocr-max-concurrency').value) || 4,
lang: document.getElementById('s-ocr-lang').value.trim() || 'it', lang: document.getElementById('s-ocr-lang').value.trim() || 'auto',
paddle_auto_lang: document.getElementById('s-ocr-paddle-auto-lang').value.trim() || 'latin',
build_retry_cooldown_s: parseFloat(document.getElementById('s-ocr-retry-cooldown').value) || 0, build_retry_cooldown_s: parseFloat(document.getElementById('s-ocr-retry-cooldown').value) || 0,
vlm_gpu_memory_utilization: parseFloat(document.getElementById('s-ocr-vlm-gmu').value) || 0, vlm_gpu_memory_utilization: parseFloat(document.getElementById('s-ocr-vlm-gmu').value) || 0,
fallback_engines: document.getElementById('s-ocr-fallback').value.trim() || 'auto', fallback_engines: document.getElementById('s-ocr-fallback').value.trim() || 'auto',
......
...@@ -63,7 +63,7 @@ async def create_ocr( ...@@ -63,7 +63,7 @@ async def create_ocr(
file: UploadFile = File(...), file: UploadFile = File(...),
engine: Optional[str] = Form(None), # paddle|doctr|surya|olmocr (default from config) engine: Optional[str] = Form(None), # paddle|doctr|surya|olmocr (default from config)
dpi: Optional[int] = Form(None), # PDF rasterisation DPI dpi: Optional[int] = Form(None), # PDF rasterisation DPI
lang: Optional[str] = Form(None), # reserved (per-engine language override) lang: Optional[str] = Form(None), # document language; omit or "auto" to let the engine decide
structured: Optional[bool] = Form(False), # structured JSON extraction via text model structured: Optional[bool] = Form(False), # structured JSON extraction via text model
schema_: Optional[str] = Form(None, alias="schema"), # override extraction schema (JSON string) schema_: Optional[str] = Form(None, alias="schema"), # override extraction schema (JSON string)
detect: Optional[str] = Form(None), # off|layout|detector|both detect: Optional[str] = Form(None), # off|layout|detector|both
...@@ -89,6 +89,7 @@ async def create_ocr( ...@@ -89,6 +89,7 @@ async def create_ocr(
detect=detect, detect=detect,
structured=bool(structured), structured=bool(structured),
schema=schema_, schema=schema_,
lang=lang,
) )
except OcrError as e: except OcrError as e:
raise HTTPException(status_code=e.status, detail=str(e)) raise HTTPException(status_code=e.status, detail=str(e))
...@@ -101,6 +102,7 @@ async def create_ocr_batch( ...@@ -101,6 +102,7 @@ async def create_ocr_batch(
files: List[UploadFile] = File(...), files: List[UploadFile] = File(...),
engine: Optional[str] = Form(None), engine: Optional[str] = Form(None),
dpi: Optional[int] = Form(None), dpi: Optional[int] = Form(None),
lang: Optional[str] = Form(None),
structured: Optional[bool] = Form(False), structured: Optional[bool] = Form(False),
schema_: Optional[str] = Form(None, alias="schema"), schema_: Optional[str] = Form(None, alias="schema"),
detect: Optional[str] = Form(None), detect: Optional[str] = Form(None),
...@@ -125,7 +127,7 @@ async def create_ocr_batch( ...@@ -125,7 +127,7 @@ async def create_ocr_batch(
try: try:
res = await ocr_manager.ocr_document( res = await ocr_manager.ocr_document(
data, filename=name, content_type=ctype, engine=engine, dpi=dpi, data, filename=name, content_type=ctype, engine=engine, dpi=dpi,
detect=detect, structured=bool(structured), schema=schema_, detect=detect, structured=bool(structured), schema=schema_, lang=lang,
) )
res["filename"] = name res["filename"] = name
return res return res
...@@ -226,6 +228,7 @@ class OcrStepRequest(BaseModel): ...@@ -226,6 +228,7 @@ class OcrStepRequest(BaseModel):
image: str = "" # data: URI, base64, or {{stepN.url}} reference image: str = "" # data: URI, base64, or {{stepN.url}} reference
engine: Optional[str] = None engine: Optional[str] = None
dpi: Optional[int] = None dpi: Optional[int] = None
lang: Optional[str] = None
detect: Optional[str] = None detect: Optional[str] = None
structured: Optional[bool] = False structured: Optional[bool] = False
schema_: Optional[str] = Field(default=None, alias="schema") schema_: Optional[str] = Field(default=None, alias="schema")
...@@ -264,6 +267,7 @@ async def run_ocr_step(request: OcrStepRequest, http_request=None): ...@@ -264,6 +267,7 @@ async def run_ocr_step(request: OcrStepRequest, http_request=None):
return await ocr_manager.ocr_document( return await ocr_manager.ocr_document(
data, filename=filename, engine=request.engine, dpi=request.dpi, data, filename=filename, engine=request.engine, dpi=request.dpi,
detect=request.detect, structured=bool(request.structured), schema=schema, detect=request.detect, structured=bool(request.structured), schema=schema,
lang=getattr(request, "lang", None),
) )
except OcrError as e: except OcrError as e:
raise HTTPException(status_code=e.status, detail=str(e)) raise HTTPException(status_code=e.status, detail=str(e))
...@@ -911,7 +911,17 @@ class OcrConfig: ...@@ -911,7 +911,17 @@ class OcrConfig:
# rasterisation time and memory for no extra pixels at the model. # rasterisation time and memory for no extra pixels at the model.
surya_dpi: int = 250 surya_dpi: int = 250
max_concurrency: int = 4 # global cap on in-flight OCR pages max_concurrency: int = 4 # global cap on in-flight OCR pages
lang: str = "it" # default document language # Default document language when a request names none. "auto" (the shipped
# default) means let the engine recognise what it finds: surya-2, olmOCR and
# docTR are multilingual and have no language input at all, and PaddleOCR --
# the one engine built for a single language -- gets `paddle_auto_lang`.
lang: str = "auto"
# What "auto" becomes for PaddleOCR, which has no auto mode. A SCRIPT family,
# not detection: `latin` covers Italian, French, Spanish, German, Portuguese
# and the rest of the Latin alphabet. PaddleOCR's own default is `ch`, a worse
# guess for most documents than no guess. Another script needs naming
# (lang=arabic|japan|cyrillic|devanagari) or one of the multilingual engines.
paddle_auto_lang: str = "latin"
# After an engine fails to come up, refuse that engine FAST for this long instead of # After an engine fails to come up, refuse that engine FAST for this long instead of
# re-attempting the load on every request (a vLLM-backed engine burns ~35s per failed # re-attempting the load on every request (a vLLM-backed engine burns ~35s per failed
# boot; Surya-2 once did that 338 times in a row on a card that was 11 GB short). # boot; Surya-2 once did that 338 times in a row on a card that was 11 GB short).
......
This diff is collapsed.
...@@ -33,7 +33,10 @@ class PaddleEngine(SubprocessOcrEngine): ...@@ -33,7 +33,10 @@ class PaddleEngine(SubprocessOcrEngine):
def _worker_opts(self) -> dict: def _worker_opts(self) -> dict:
return { return {
"lang": self.cfg.paddle_lang or self.cfg.lang or "it", # Already resolved by the manager (request > paddle_lang > lang >
# auto), and `auto` already translated to a script family -- an engine
# is built for ONE language and cannot be re-languaged afterwards.
"lang": self.cfg.paddle_lang or self.cfg.lang or "latin",
"use_gpu": bool(self.cfg.paddle_use_gpu), "use_gpu": bool(self.cfg.paddle_use_gpu),
"structure": bool(self.cfg.paddle_structure), "structure": bool(self.cfg.paddle_structure),
"det_model_dir": self.cfg.paddle_det_model_dir or "", "det_model_dir": self.cfg.paddle_det_model_dir or "",
......
...@@ -266,7 +266,7 @@ back. ...@@ -266,7 +266,7 @@ back.
## Enable & configure ## Enable & configure
Settings → **OCR** card, or `config.json` `"ocr"`. Key fields: `enabled`, Settings → **OCR** card, or `config.json` `"ocr"`. Key fields: `enabled`,
`default_engine`, `dpi`, `surya_dpi`, `max_concurrency`, `lang`, `build_retry_cooldown_s`, `default_engine`, `dpi`, `surya_dpi`, `max_concurrency`, `lang`, `paddle_auto_lang`, `build_retry_cooldown_s`,
`vlm_gpu_memory_utilization`, `vlm_max_num_batched_tokens`, `vlm_max_num_seqs`, `vlm_gpu_memory_utilization`, `vlm_max_num_batched_tokens`, `vlm_max_num_seqs`,
`vlm_max_model_len`, `fallback_engines`, `evict_drain_timeout_s`; `vlm_max_model_len`, `fallback_engines`, `evict_drain_timeout_s`;
per-engine `*_enabled` / per-engine `*_enabled` /
...@@ -295,7 +295,7 @@ aggregate throughput without vLLM (a continuous-batching path is a future option ...@@ -295,7 +295,7 @@ aggregate throughput without vLLM (a continuous-batching path is a future option
## Endpoints ## Endpoints
``` ```
POST /v1/ocr multipart: file=@doc.pdf [engine=] [dpi=] [detect=] [structured=] [schema=] POST /v1/ocr multipart: file=@doc.pdf [engine=] [lang=] [dpi=] [detect=] [structured=] [schema=]
POST /v1/ocr/batch multipart: files=@a.pdf files=@b.png [...same fields...] POST /v1/ocr/batch multipart: files=@a.pdf files=@b.png [...same fields...]
GET /v1/ocr/engines what this install offers, and what each engine returns GET /v1/ocr/engines what this install offers, and what each engine returns
GET/POST/DELETE /v1/ocr/schemas[/{name}] manage extraction schemas GET/POST/DELETE /v1/ocr/schemas[/{name}] manage extraction schemas
...@@ -320,6 +320,58 @@ page carries `lines[]` (text + bbox + conf), `regions[]` (layout), `tables[]`, a ...@@ -320,6 +320,58 @@ page carries `lines[]` (text + bbox + conf), `regions[]` (layout), `tables[]`, a
`meta` when the engine reports page metadata (olmOCR does; the others do not). The `meta` when the engine reports page metadata (olmOCR does; the others do not). The
`ocr` **pipeline step type** chains OCR with `text_gen` in custom pipelines. `ocr` **pipeline step type** chains OCR with `text_gen` in custom pipelines.
### `lang=` — and why the default is auto-recognition
A client names the document's language; omitting it (or sending `lang=auto`) means
**let the engine recognise what it finds**. The default is deliberately not a
language: a server that has not been told what it is reading should not assume one.
The parameter does not mean the same thing to every engine, and the response says
which it was:
| engine | `lang=` | why |
|---|---|---|
| `paddle` | **steers it** | loads a language-specific recognition model |
| `doctr` | accepted, no effect | recognition is Latin-script-wide |
| `surya` | accepted, no effect | VLM; its whole prompt is `"OCR this block image to HTML."` |
| `olmocr` | accepted, no effect | VLM; fixed prompt |
So every response carries both the language applied and whether the engine that
served could act on it — silently ignoring `lang` would leave a caller believing it
steered something:
```json
{ "engine": "surya", "lang": "it", "lang_applied": false, ... }
```
`/v1/ocr/engines` reports the same per engine (`language_aware`), so a client can
choose before sending.
**Precedence** is request > `ocr.<engine>_lang` > `ocr.lang` > auto. The request
beats the engine-specific setting on purpose: that setting is an install's default,
while a caller naming a language knows something about the document in hand. Asking
for `auto` explicitly is *not* the same as sending nothing — an install configured
for Italian can still be asked to work it out.
**PaddleOCR is the exception that shapes the design.** It has no auto mode and its
options are sent once at load (`{"cmd": "load", "opts": ...}`), so an instance is
built for one language and cannot be re-languaged. Two consequences:
* `auto` becomes `ocr.paddle_auto_lang` (default `latin`). That is a *script family*,
not detection — it covers Italian, French, Spanish, German, Portuguese and the rest
of the Latin alphabet, and most ISO codes PaddleOCR accepts resolve to it anyway.
PaddleOCR's own default is `ch`, a worse guess for most documents than no guess.
Another script needs naming: `lang=arabic|japan|cyrillic|devanagari`.
* paddle gets **one pool per language**, keyed on the model it is built with (so
`auto` and `latin` share a pool rather than loading it twice). The multilingual
engines keep a single pool whatever is asked, which is not tidiness: a
language-keyed pool is a second set of loaded instances holding VRAM, and a
production install runs 24 surya instances.
Note that `ocr.surya_langs` is a **legacy** setting. It reaches only the ≤0.17
recognition API (`rec(images, langs, det_predictor)`); surya-2 has no language input,
so on the pod path it does nothing.
### Where `conf` comes from on a served engine ### Where `conf` comes from on a served engine
A VLM pipeline has no detector score to report, so surya asks the model server for A VLM pipeline has no detector score to report, so surya asks the model server for
......
"""`lang=` was accepted on /v1/ocr and marked "reserved" -- it did nothing.
Now a client names the document's language and we respect it, and when it names
none the default is AUTO-RECOGNITION rather than any particular language. That
asymmetry is the whole design:
* surya-2, olmOCR and docTR are multilingual and have NO language input. Surya-2's
entire prompt is "OCR this block image to HTML." and its own module warns the
wording is the training-time contract, so there is nowhere to put a language and
nothing to gain. For them auto is simply what they already do.
* PaddleOCR loads a language-specific recognition model, and the options are sent
once at load -- an instance cannot be re-languaged. So it is the only engine
that needs a pool per language, and the only one with something to translate
`auto` into.
Keeping that set small is not tidiness: a language-keyed pool is a SECOND set of
loaded instances holding VRAM, and a production install runs 24 surya instances.
"""
import pathlib
import sys
ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from codai.ocr import manager as om # noqa: E402
class _Cfg:
def __init__(self, **kw):
self.lang = "auto"
self.paddle_lang = ""
self.paddle_auto_lang = ""
for k, v in kw.items():
setattr(self, k, v)
# ------------------------------------------------------------ what auto means #
def test_nothing_specified_means_auto():
assert om.resolve_lang(_Cfg(), "surya", None) == "auto"
assert om.DEFAULT_LANG == "auto"
def test_the_ways_of_saying_auto_all_mean_auto():
for word in ("", "auto", "AUTO", " any ", "multi", "multilingual", "detect"):
assert om.normalize_lang(word) == "auto", word
def test_a_named_language_is_normalised_not_mangled():
assert om.normalize_lang(" EN ") == "en"
assert om.normalize_lang("pt_BR") == "pt-br"
# ------------------------------------------------------------- precedence #
def test_the_request_wins_over_every_configured_language():
"""A caller naming a language knows something about the document in hand;
a configured one is only an install's default."""
cfg = _Cfg(lang="it", paddle_lang="french")
assert om.resolve_lang(cfg, "paddle", "japan") == "japan"
def test_the_engine_specific_setting_beats_the_global_one():
cfg = _Cfg(lang="it", paddle_lang="french")
assert om.resolve_lang(cfg, "paddle", None) == "french"
def test_the_global_setting_is_used_when_there_is_nothing_else():
assert om.resolve_lang(_Cfg(lang="it"), "surya", None) == "it"
def test_asking_for_auto_explicitly_is_not_the_same_as_asking_for_nothing():
"""An install configured for Italian must still be able to serve a request
that says "work it out yourself"."""
cfg = _Cfg(lang="it")
assert om.resolve_lang(cfg, "surya", None) == "it"
assert om.resolve_lang(cfg, "surya", "auto") == "auto"
# --------------------------------------------- which engines can act on it #
def test_only_paddle_reads_by_language():
assert om.engine_uses_language("paddle")
for name in ("surya", "olmocr", "doctr"):
assert not om.engine_uses_language(name), name
def test_auto_becomes_a_script_family_for_paddle_which_has_no_auto_mode():
assert om.engine_lang(_Cfg(), "paddle", "auto") == "latin"
assert om.AUTO_PADDLE_LANG == "latin"
def test_the_paddle_auto_language_is_configurable():
assert om.engine_lang(_Cfg(paddle_auto_lang="arabic"), "paddle", "auto") == "arabic"
def test_a_named_language_is_passed_to_paddle_untouched():
assert om.engine_lang(_Cfg(), "paddle", "japan") == "japan"
def test_auto_stays_auto_for_an_engine_that_needs_no_translation():
assert om.engine_lang(_Cfg(), "surya", "auto") == "auto"
# ------------------------------------------------------------ pool identity #
def _pools_for(monkeypatch, cfg, asks):
"""Resolve each (engine, lang) ask against a fresh manager; return the pool
objects and the keys they landed under."""
import asyncio
class _StubPool:
def __init__(self, name, cfg, size):
self.name, self.cfg, self.size = name, cfg, size
monkeypatch.setattr(om, "_Pool", _StubPool)
monkeypatch.setattr(om, "_engine_instances", lambda c, n: 1)
m = om.OcrManager()
m._require_cfg = lambda: cfg
m._pools = {}
m._lock = asyncio.Lock()
async def _run():
return [await m._get_pool(n, lg) for (n, lg) in asks]
got = asyncio.run(_run())
return got, set(m._pools)
def test_two_languages_are_two_paddle_pools(monkeypatch):
got, keys = _pools_for(monkeypatch, _Cfg(),
[("paddle", "it"), ("paddle", "japan")])
assert got[0] is not got[1]
assert keys == {"paddle:it", "paddle:japan"}
def test_auto_and_the_language_it_maps_to_share_one_pool(monkeypatch):
"""They are the same loaded model; two pools would be two copies in VRAM."""
got, keys = _pools_for(monkeypatch, _Cfg(),
[("paddle", "auto"), ("paddle", "latin")])
assert got[0] is got[1]
assert keys == {"paddle:latin"}
def test_a_multilingual_engine_keeps_one_pool_whatever_is_asked(monkeypatch):
"""24 surya instances must not become 24 per language for a parameter the
engine cannot use."""
got, keys = _pools_for(monkeypatch, _Cfg(),
[("surya", "it"), ("surya", "japan")])
assert got[0] is got[1]
assert keys == {"surya"}
def test_a_paddle_pool_is_built_with_the_language_resolved_into_its_config(monkeypatch):
from codai.config import OcrConfig
got, _ = _pools_for(monkeypatch, OcrConfig(), [("paddle", "japan")])
assert got[0].cfg.lang == "japan"
# ------------------------------------------- the config handed to an engine #
def test_the_resolved_language_is_written_into_the_config_the_engine_reads():
"""The pool builds engines with factory(cfg) and each engine reads its own
language out of the config, so resolving it here means no engine has to know
a per-request language exists."""
import dataclasses
from codai.config import OcrConfig
cfg = OcrConfig(lang="it", paddle_lang="french")
out = om.OcrManager._cfg_for(cfg, "paddle", "japan")
assert out.lang == "japan"
# the request has to beat paddle_lang too, or the override would be ignored
assert out.paddle_lang == "japan"
assert dataclasses.is_dataclass(out)
# and the original is untouched -- other pools are still serving from it
assert cfg.lang == "it" and cfg.paddle_lang == "french"
def test_no_language_leaves_the_config_exactly_as_it_was():
from codai.config import OcrConfig
cfg = OcrConfig(lang="it")
assert om.OcrManager._cfg_for(cfg, "surya", "") is cfg
def test_a_config_that_is_not_a_dataclass_does_not_crash_the_request():
assert om.OcrManager._cfg_for(_Cfg(), "paddle", "japan") is not None
# ------------------------------------------------------------- what we report #
def test_the_catalogue_says_which_engines_language_actually_steers():
from codai.config import OcrConfig
m = om.OcrManager()
m._require_cfg = lambda: OcrConfig(paddle_enabled=True, surya_enabled=True,
surya_accept_license=True)
rows = {r["id"]: r for r in m.engine_catalogue()["data"]}
assert rows["paddle"]["language_aware"] is True
assert rows["surya"]["language_aware"] is False
def test_the_shipped_default_is_auto_not_a_language():
from codai.config import OcrConfig
assert OcrConfig().lang == "auto"
assert OcrConfig().paddle_auto_lang == "latin"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment