Back to report index

Step 5 linebitmapinference f800a27: AI3D-379 Align config module with fleet pattern

Miroslav Simko <ms@iolabs.ch> 2026-09-02T09:36:56+02:00

Commit #31 · 32 snippets

 README.md                                          |  23 +++-
 .../__init__.py                                    |  12 +-
 .../_config.py                                     | 150 +++++++--------------
 .../model_fetch.py                                 |  14 +-
 .../model_registry.py                              |  67 +++++++++
 tests/test_config.py                               |  84 +++++++-----
 6 files changed, 195 insertions(+), 155 deletions(-)
Importance #1: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -21,11 +22,12 @@
21logger = logging.getLogger(__name__)22logger = logging.getLogger(__name__)
2223
23_PACKAGE_NAME = "iolabs_image_analyzer_line_bitmap_inference"24_PACKAGE_NAME = "iolabs_image_analyzer_line_bitmap_inference"
24_DEFAULT_FILENAME = "line_bitmap_inference.default.json"25_DEFAULT_FILENAME = "line_bitmap_inference.default.json"
26_CONTEXT = "line bitmap inference config"
2527
2628
27class VectorizationConfig(config_loader.ConfigModel):29class LineBitmapInferenceVectorizationConfig(config_loader.ConfigModel):
28 """Polyline extraction parameters applied to the predicted mask."""30 """Polyline extraction parameters applied to the predicted mask."""
2931
30 min_component_pixels: int = 1632 min_component_pixels: int = 16
31 simplify_tolerance_px: float = 2.033 simplify_tolerance_px: float = 2.0
Importance #2: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -55,118 +59,54 @@
55 return {} if value is None else value59 return {} if value is None else value
5660
5761
58class LineBitmapInferenceConfigError(config_loader.ConfigError):62class LineBitmapInferenceConfigError(config_loader.ConfigError):
59 """Raised when the inference config contains unsupported keys or values."""63 """Raised when line bitmap inference config contains unsupported keys or values."""
6064
6165
62def _config_path_for_load(config_path: str | Path | None) -> str | Path | None:66def _load_model(
63 """Return *config_path*, or the sibling JSON when this module is loaded loose."""67 *,
68 overrides: Mapping[str, Any] | None = None,
69 config_path: str | Path | None = None,
70) -> LineBitmapInferenceConfig:
71 """Load packaged (or *config_path*) defaults, merge *overrides*, validate."""
72 if overrides:
73 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))
64 if config_path is not None:74 if config_path is not None:
65 return config_path75 logger.info("Config file applied: %s", config_path)
66 if __package__ in {None, ""}:76 return config_loader.load_config(
67 return Path(__file__).resolve().with_name(_DEFAULT_FILENAME)77 LineBitmapInferenceConfig,
68 return None78 package=_PACKAGE_NAME,
79 filename=_DEFAULT_FILENAME,
80 overrides=overrides,
81 config_path=config_path,
82 context=_CONTEXT,
83 error_cls=LineBitmapInferenceConfigError,
84 )
6985
7086
71def normalize_line_bitmap_inference_config(raw_config: dict[str, Any]) -> dict[str, Any]:87def normalize_line_bitmap_inference_config(
88 raw_config: Mapping[str, Any],
89) -> dict[str, Any]:
72 """Validate *raw_config* against the model, filling defaults; return a dict."""90 """Validate *raw_config* against the model, filling defaults; return a dict."""
73 config = config_loader.validate_config(91 return config_loader.validate_config(
74 LineBitmapInferenceConfig,92 LineBitmapInferenceConfig,
75 raw_config,93 raw_config,
76 context="inference config",94 context=_CONTEXT,
77 error_cls=LineBitmapInferenceConfigError,95 error_cls=LineBitmapInferenceConfigError,
78 ).model_dump()96 ).model_dump()
79 logger.debug("Normalized inference config")
80 return config
8197
8298
83def load_line_bitmap_inference_config(99def load_line_bitmap_inference_config(
84 config_path: str | Path | None = None,100 config_path: str | Path | None = None,
85) -> dict[str, Any]:101) -> dict[str, Any]:
86 """Load packaged (or *config_path*) defaults and validate them."""102 """Load the packaged defaults, or *config_path* in their place, and validate."""
87 return config_loader.load_config(103 return _load_model(config_path=config_path).model_dump()
88 LineBitmapInferenceConfig,
89 package=_PACKAGE_NAME,
90 filename=_DEFAULT_FILENAME,
91 config_path=_config_path_for_load(config_path),
92 context="inference config",
93 error_cls=LineBitmapInferenceConfigError,
94 ).model_dump()
95104
96105
97def build_line_bitmap_inference_config(106def build_line_bitmap_inference_config(
98 *,107 *,
99 overrides: dict[str, Any] | None = None,108 overrides: Mapping[str, Any] | None = None,
100 config_path: str | Path | None = None,109 config_path: str | Path | None = None,
101) -> dict[str, Any]:110) -> dict[str, Any]:
102 """Load defaults, deep-merge *overrides*, and validate."""111 """Load defaults (or *config_path* in their place), deep-merge *overrides*."""
103 return config_loader.load_config(112 return _load_model(overrides=overrides, config_path=config_path).model_dump()
104 LineBitmapInferenceConfig,
105 package=_PACKAGE_NAME,
106 filename=_DEFAULT_FILENAME,
107 overrides=overrides,
108 config_path=_config_path_for_load(config_path),
109 context="inference config",
110 error_cls=LineBitmapInferenceConfigError,
111 ).model_dump()
112
113
114# ---------------------------------------------------------------------------
115# Model artifact registry
116# ---------------------------------------------------------------------------
117# The runtime config above is orthogonal to the *model* itself. A trained model
118# is frozen into a portable bundle (model.ckpt + model.json) by ``export-model``
119# and hosted as a single ``.zip`` at a stable URL (an Azure Blob public/SAS
120# URL). ``model_fetch.resolve_model`` downloads + caches it on demand, verified
121# by sha256. URL / sha256 / cache are overridable per-environment without a code
122# change:
123# IOLABS_LINE_BITMAP_MODEL_URL
124# IOLABS_LINE_BITMAP_MODEL_SHA256
125# IOLABS_LINE_BITMAP_MODEL_CACHE
126
127DEFAULT_MODEL_NAME = "line_bitmap_v1"
128
129# NOTE: placeholder url/sha256. Upload the exported bundle .zip to Azure Blob,
130# then fill in the real values here (or set the env vars above). Until then,
131# auto-resolution raises a clear error and callers must pass an explicit
132# ``--bundle`` / ``--checkpoint``.
133MODEL_REGISTRY: dict[str, dict[str, str]] = {
134 DEFAULT_MODEL_NAME: {
135 "url": "https://REPLACE_ME.blob.core.windows.net/models/line-bitmap/line_bitmap_v1.zip",
136 "sha256": "",
137 "format": "bundle-zip",
138 },
139}
140
141_DEFAULT_MODEL_CACHE = Path.home() / ".cache" / "iolabs" / "line-bitmap-inference"
142
143
144def model_cache_dir() -> Path:
145 """Local cache root for downloaded model bundles (env-overridable)."""
146 override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_CACHE")
147 return Path(override).expanduser() if override else _DEFAULT_MODEL_CACHE
148
149
150def model_registry_entry(model_name: str | None = None) -> dict[str, str]:
151 """Return the registry entry for ``model_name`` (the default model if None).
152
153 ``IOLABS_LINE_BITMAP_MODEL_URL`` / ``IOLABS_LINE_BITMAP_MODEL_SHA256`` (when
154 set) override the registered url / sha256, so a deployment can point at a
155 model artifact without editing code.
156 """
157 name = model_name or DEFAULT_MODEL_NAME
158 try:
159 entry = dict(MODEL_REGISTRY[name])
160 except KeyError:
161 known = ", ".join(sorted(MODEL_REGISTRY)) or "<none>"
162 raise LineBitmapInferenceConfigError(
163 f"Unknown model name {name!r}. Known models: {known}"
164 ) from None
165
166 url_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_URL")
167 sha_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_SHA256")
168 if url_override:
169 entry["url"] = url_override
170 if sha_override:
171 entry["sha256"] = sha_override
172 return entry
Importance #3: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -1,18 +1,19 @@
1"""Inference runtime config: JSON defaults + overrides, unknown keys rejected.1"""Runtime inference config: tiling, device, output toggles, vectorization.
22
3Mirrors ``line_bitmap_inference.default.json`` with a pydantic3The schema is `LineBitmapInferenceConfig` (a `config_loader.ConfigModel`),
4``config_loader.ConfigModel`` tree. Unknown keys fail fast; ``--set``-style4mirroring `line_bitmap_inference.default.json` key for key.
5string overrides coerce through the fleet accepted-input matrix. This is the
6inference *runtime* config (tiling, device, output toggles, vectorization
7params). The *model* spec (architecture/encoder/classes) is a separate artifact
8read from the training YAML or a deployment bundle (see ``model_spec.py``).
95
10To add a config key, add the field to the model and the JSON default; nothing6Adding a config key means adding the field to the model and the same key to
11else.7`line_bitmap_inference.default.json` nothing else. Unknown keys are rejected.
8
9The entry points return a plain ``dict``. The *model* spec
10(architecture/encoder/classes) is a separate artifact read from the training
11YAML or a deployment bundle (see ``model_spec.py``); the model artifact
12registry lives in ``model_registry.py``.
12"""13"""
13import logging14import logging
14import os15from collections.abc import Mapping
15from pathlib import Path16from pathlib import Path
16from typing import Any17from typing import Any
1718
18import pydantic19import pydantic
Importance #4: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -45,9 +47,11 @@
45 write_vectors: bool = True47 write_vectors: bool = True
46 write_probabilities: bool = False48 write_probabilities: bool = False
47 write_overlay: bool = False49 write_overlay: bool = False
48 probabilities_dtype: str = "float16"50 probabilities_dtype: str = "float16"
49 vectorization: VectorizationConfig = VectorizationConfig()51 vectorization: LineBitmapInferenceVectorizationConfig = (
52 LineBitmapInferenceVectorizationConfig()
53 )
5054
51 @pydantic.field_validator("vectorization", mode="before")55 @pydantic.field_validator("vectorization", mode="before")
52 @classmethod56 @classmethod
53 def _none_section_is_default(cls, value: Any) -> Any:57 def _none_section_is_default(cls, value: Any) -> Any:
Importance #5: src/iolabs_image_analyzer_line_bitmap_inference/__init__.py @@ -13,9 +13,15 @@
13 __version__ = _pkg_version("iolabs-image-analyzer-line-bitmap-inference")13 __version__ = _pkg_version("iolabs-image-analyzer-line-bitmap-inference")
14except PackageNotFoundError: # running from a source checkout that isn't installed14except PackageNotFoundError: # running from a source checkout that isn't installed
15 __version__ = "0.0.0+local"15 __version__ = "0.0.0+local"
1616
17from ._config import build_line_bitmap_inference_config17from ._config import (
18 LineBitmapInferenceConfig,
19 LineBitmapInferenceConfigError,
20 build_line_bitmap_inference_config,
21 load_line_bitmap_inference_config,
22 normalize_line_bitmap_inference_config,
23)
18from .class_names import CLASS_NAMES24from .class_names import CLASS_NAMES
19from .model_fetch import resolve_model25from .model_fetch import resolve_model
20from .model_spec import (26from .model_spec import (
21 ModelSpec,27 ModelSpec,
Importance #6: src/iolabs_image_analyzer_line_bitmap_inference/__init__.py @@ -30,14 +36,18 @@
3036
31__all__ = [37__all__ = [
32 "__version__",38 "__version__",
33 "CLASS_NAMES",39 "CLASS_NAMES",
40 "LineBitmapInferenceConfig",
41 "LineBitmapInferenceConfigError",
34 "ModelSpec",42 "ModelSpec",
35 "FlipTTA",43 "FlipTTA",
36 "build_line_bitmap_inference_config",44 "build_line_bitmap_inference_config",
45 "load_line_bitmap_inference_config",
37 "load_model",46 "load_model",
38 "load_model_spec_from_bundle",47 "load_model_spec_from_bundle",
39 "load_model_spec_from_yaml",48 "load_model_spec_from_yaml",
49 "normalize_line_bitmap_inference_config",
40 "predict_tile",50 "predict_tile",
41 "resolve_model",51 "resolve_model",
42 "run_directory",52 "run_directory",
43 "run_tile",53 "run_tile",
Importance #7: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -4,9 +4,9 @@
4by ``export-model``). :func:`resolve_model` returns one of:4by ``export-model``). :func:`resolve_model` returns one of:
55
61. an explicit local path a bundle dir (or a ``.ckpt``) that exists is returned61. an explicit local path a bundle dir (or a ``.ckpt``) that exists is returned
7 unchanged, so an explicit ``--bundle`` / ``--checkpoint`` always wins;7 unchanged, so an explicit ``--bundle`` / ``--checkpoint`` always wins;
82. a named entry in :data:`._config.MODEL_REGISTRY` a zipped bundle hosted at a82. a named entry in :data:`.model_registry.MODEL_REGISTRY` a zipped bundle hosted at a
9 stable URL (e.g. an Azure Blob public/SAS URL), downloaded once into a9 stable URL (e.g. an Azure Blob public/SAS URL), downloaded once into a
10 content-addressed cache, verified by sha256, and reused thereafter.10 content-addressed cache, verified by sha256, and reused thereafter.
1111
12The download is plain stdlib ``urllib`` + ``hashlib`` a public/SAS blob URL is12The download is plain stdlib ``urllib`` + ``hashlib`` a public/SAS blob URL is
Importance #8: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -24,9 +24,9 @@
24import zipfile24import zipfile
25from pathlib import Path25from pathlib import Path
26from urllib.request import urlopen26from urllib.request import urlopen
2727
28from . import _config28from . import model_registry
2929
30__all__ = ["resolve_model", "ModelResolutionError"]30__all__ = ["resolve_model", "ModelResolutionError"]
3131
32logger = logging.getLogger(__name__)32logger = logging.getLogger(__name__)
Importance #9: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -55,16 +55,16 @@
55 if path.exists():55 if path.exists():
56 return path56 return path
57 raise ModelResolutionError(f"model path does not exist: {path}")57 raise ModelResolutionError(f"model path does not exist: {path}")
5858
59 entry = _config.model_registry_entry(model_name)59 entry = model_registry.model_registry_entry(model_name)
60 url = entry.get("url", "")60 url = entry.get("url", "")
61 sha256 = (entry.get("sha256") or "").lower()61 sha256 = (entry.get("sha256") or "").lower()
62 name = model_name or _config.DEFAULT_MODEL_NAME62 name = model_name or model_registry.DEFAULT_MODEL_NAME
6363
64 _require_configured(name, url, sha256)64 _require_configured(name, url, sha256)
6565
66 dest = _config.model_cache_dir() / name / sha256[:12]66 dest = model_registry.model_cache_dir() / name / sha256[:12]
67 if _is_valid_bundle(dest):67 if _is_valid_bundle(dest):
68 logger.debug("model %s already cached at %s", name, dest)68 logger.debug("model %s already cached at %s", name, dest)
69 return dest69 return dest
7070
Importance #10: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -80,9 +80,9 @@
80 if missing:80 if missing:
81 raise ModelResolutionError(81 raise ModelResolutionError(
82 f"model {name!r} is not configured ({', '.join(missing)} missing). "82 f"model {name!r} is not configured ({', '.join(missing)} missing). "
83 "Upload the exported bundle .zip and set its url + sha256 in "83 "Upload the exported bundle .zip and set its url + sha256 in "
84 "_config.MODEL_REGISTRY, or export IOLABS_LINE_BITMAP_MODEL_URL / "84 "model_registry.MODEL_REGISTRY, or export IOLABS_LINE_BITMAP_MODEL_URL / "
85 "IOLABS_LINE_BITMAP_MODEL_SHA256 — or pass an explicit "85 "IOLABS_LINE_BITMAP_MODEL_SHA256 — or pass an explicit "
86 "--bundle / --checkpoint."86 "--bundle / --checkpoint."
87 )87 )
8888
Importance #11: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -99,9 +99,9 @@
99 return checkpoint.is_file()99 return checkpoint.is_file()
100100
101101
102def _fetch_bundle(*, name: str, url: str, sha256: str, dest: Path) -> Path:102def _fetch_bundle(*, name: str, url: str, sha256: str, dest: Path) -> Path:
103 cache_root = _config.model_cache_dir()103 cache_root = model_registry.model_cache_dir()
104 cache_root.mkdir(parents=True, exist_ok=True)104 cache_root.mkdir(parents=True, exist_ok=True)
105 logger.info("downloading model %s from %s", name, url)105 logger.info("downloading model %s from %s", name, url)
106106
107 work = Path(tempfile.mkdtemp(prefix=".fetch-", dir=cache_root))107 work = Path(tempfile.mkdtemp(prefix=".fetch-", dir=cache_root))
Importance #12: src/iolabs_image_analyzer_line_bitmap_inference/model_registry.py @@ -0,0 +1,67 @@
1"""Registry of trained model artifacts and their local download cache.
2
3The runtime config (``_config.py``) is orthogonal to the *model* itself. A
4trained model is frozen into a portable bundle (model.ckpt + model.json) by
5``export-model`` and hosted as a single ``.zip`` at a stable URL (an Azure Blob
6public/SAS URL). ``model_fetch.resolve_model`` downloads + caches it on demand,
7verified by sha256. URL / sha256 / cache are overridable per-environment without
8a code change::
9
10 IOLABS_LINE_BITMAP_MODEL_URL
11 IOLABS_LINE_BITMAP_MODEL_SHA256
12 IOLABS_LINE_BITMAP_MODEL_CACHE
13"""
14import logging
15import os
16from pathlib import Path
17
18from ._config import LineBitmapInferenceConfigError
19
20logger = logging.getLogger(__name__)
21
22DEFAULT_MODEL_NAME = "line_bitmap_v1"
23
24# NOTE: placeholder url/sha256. Upload the exported bundle .zip to Azure Blob,
25# then fill in the real values here (or set the env vars above). Until then,
26# auto-resolution raises a clear error and callers must pass an explicit
27# ``--bundle`` / ``--checkpoint``.
28MODEL_REGISTRY: dict[str, dict[str, str]] = {
29 DEFAULT_MODEL_NAME: {
30 "url": "https://REPLACE_ME.blob.core.windows.net/models/line-bitmap/line_bitmap_v1.zip",
31 "sha256": "",
32 "format": "bundle-zip",
33 },
34}
35
36_DEFAULT_MODEL_CACHE = Path.home() / ".cache" / "iolabs" / "line-bitmap-inference"
37
38
39def model_cache_dir() -> Path:
40 """Local cache root for downloaded model bundles (env-overridable)."""
41 override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_CACHE")
42 return Path(override).expanduser() if override else _DEFAULT_MODEL_CACHE
43
44
45def model_registry_entry(model_name: str | None = None) -> dict[str, str]:
46 """Return the registry entry for ``model_name`` (the default model if None).
47
48 ``IOLABS_LINE_BITMAP_MODEL_URL`` / ``IOLABS_LINE_BITMAP_MODEL_SHA256`` (when
49 set) override the registered url / sha256, so a deployment can point at a
50 model artifact without editing code.
51 """
52 name = model_name or DEFAULT_MODEL_NAME
53 try:
54 entry = dict(MODEL_REGISTRY[name])
55 except KeyError:
56 known = ", ".join(sorted(MODEL_REGISTRY)) or "<none>"
57 raise LineBitmapInferenceConfigError(
58 f"Unknown model name {name!r}. Known models: {known}"
59 ) from None
60
61 url_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_URL")
62 sha_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_SHA256")
63 if url_override:
64 entry["url"] = url_override
65 if sha_override:
66 entry["sha256"] = sha_override
67 return entry
0
Importance #13: tests/test_config.py @@ -1,57 +1,59 @@
1"""Config loading/validation — loaded in isolation (no heavy package imports)."""1"""Config loading/validation for the line bitmap inference runtime config."""
2import importlib.util2import json
3from pathlib import Path3from importlib import resources
44
5import pytest5import pytest
6from iolabs.common import config_loader
7from iolabs_image_analyzer_line_bitmap_inference import _config
8
9
10def _packaged_defaults() -> dict:
11 text = (
12 resources.files("iolabs_image_analyzer_line_bitmap_inference")
13 .joinpath("line_bitmap_inference.default.json")
14 .read_text(encoding="utf-8")
15 )
16 return json.loads(text)
17
18
19def test_model_defaults_match_packaged_json() -> None:
20 assert _config.LineBitmapInferenceConfig().model_dump() == _packaged_defaults()
621
7MODULE_PATH = (22
8 Path(__file__).resolve().parents[1]23def test_load_line_bitmap_inference_config_returns_packaged_defaults() -> None:
9 / "src"24 assert _config.load_line_bitmap_inference_config() == _packaged_defaults()
10 / "iolabs_image_analyzer_line_bitmap_inference"25
11 / "_config.py"26
12)27def test_error_class_is_config_error() -> None:
1328 assert issubclass(_config.LineBitmapInferenceConfigError, config_loader.ConfigError)
14SPEC = importlib.util.spec_from_file_location("lbi_config", MODULE_PATH)29 assert issubclass(_config.LineBitmapInferenceConfigError, ValueError)
15assert SPEC is not None and SPEC.loader is not None
16MODULE = importlib.util.module_from_spec(SPEC)
17SPEC.loader.exec_module(MODULE)
18
19
20def test_load_defaults() -> None:
21 config = MODULE.load_line_bitmap_inference_config()
22 assert config["tile_size"] == 512
23 assert config["overlap"] == 128
24 assert config["tta"] is False
25 assert config["write_vectors"] is True
26 assert config["blend"] == "hann"
27 assert config["vectorization"]["min_component_pixels"] == 16
2830
2931
30def test_unknown_top_level_key_is_rejected() -> None:32def test_unknown_top_level_key_is_rejected() -> None:
31 with pytest.raises(MODULE.LineBitmapInferenceConfigError, match="random_seed"):33 with pytest.raises(_config.LineBitmapInferenceConfigError, match="random_seed"):
32 MODULE.build_line_bitmap_inference_config(overrides={"random_seed": 1})34 _config.build_line_bitmap_inference_config(overrides={"random_seed": 1})
3335
3436
35def test_unknown_vectorization_key_is_rejected() -> None:37def test_unknown_nested_key_is_rejected() -> None:
36 with pytest.raises(MODULE.LineBitmapInferenceConfigError, match="bogus"):38 with pytest.raises(_config.LineBitmapInferenceConfigError, match="bogus"):
37 MODULE.build_line_bitmap_inference_config(39 _config.build_line_bitmap_inference_config(
38 overrides={"vectorization": {"bogus": 1}}40 overrides={"vectorization": {"bogus": 1}}
39 )41 )
4042
4143
42def test_overrides_deep_merge_keeps_other_defaults() -> None:44def test_overrides_deep_merge_onto_defaults() -> None:
43 config = MODULE.build_line_bitmap_inference_config(45 config = _config.build_line_bitmap_inference_config(
44 overrides={"tile_size": 768, "vectorization": {"dash_max_gap_px": 10.0}}46 overrides={"tile_size": 768, "vectorization": {"dash_max_gap_px": 10.0}}
45 )47 )
46 assert config["tile_size"] == 76848 assert config["tile_size"] == 768
47 assert config["overlap"] == 128 # untouched default49 assert config["overlap"] == 128 # untouched default
48 assert config["vectorization"]["dash_max_gap_px"] == 10.050 assert config["vectorization"]["dash_max_gap_px"] == 10.0
49 assert config["vectorization"]["min_component_pixels"] == 16 # untouched default51 assert config["vectorization"]["min_component_pixels"] == 16 # untouched default
5052
5153
52def test_string_overrides_are_coerced() -> None:54def test_set_override_coercion_and_rejection() -> None:
53 config = MODULE.build_line_bitmap_inference_config(55 config = _config.build_line_bitmap_inference_config(
54 overrides={56 overrides={
55 "tile_size": "1e3",57 "tile_size": "1e3",
56 "tta": "on",58 "tta": "on",
57 "vectorization": {"dash_max_gap_px": "5"},59 "vectorization": {"dash_max_gap_px": "5"},
Importance #14: tests/test_config.py @@ -59,15 +61,25 @@
59 )61 )
60 assert config["tile_size"] == 100062 assert config["tile_size"] == 1000
61 assert config["tta"] is True63 assert config["tta"] is True
62 assert config["vectorization"]["dash_max_gap_px"] == 5.064 assert config["vectorization"]["dash_max_gap_px"] == 5.0
65 with pytest.raises(_config.LineBitmapInferenceConfigError, match="tta"):
66 _config.build_line_bitmap_inference_config(overrides={"tta": "flase"})
67
68
69def test_normalize_fills_defaults() -> None:
70 config = _config.normalize_line_bitmap_inference_config({"tile_size": 256})
71 assert config["tile_size"] == 256
72 assert config == {**_packaged_defaults(), "tile_size": 256}
6373
6474
65def test_bool_is_rejected_for_int_field() -> None:75def test_bool_is_rejected_for_int_field() -> None:
66 with pytest.raises(MODULE.LineBitmapInferenceConfigError, match="tile_size"):76 with pytest.raises(_config.LineBitmapInferenceConfigError, match="tile_size"):
67 MODULE.build_line_bitmap_inference_config(overrides={"tile_size": True})77 _config.build_line_bitmap_inference_config(overrides={"tile_size": True})
6878
6979
70def test_null_vectorization_section_falls_back_to_defaults() -> None:80def test_null_vectorization_section_falls_back_to_defaults() -> None:
71 config = MODULE.build_line_bitmap_inference_config(overrides={"vectorization": None})81 config = _config.build_line_bitmap_inference_config(
82 overrides={"vectorization": None}
83 )
72 assert config["vectorization"]["min_component_pixels"] == 1684 assert config["vectorization"]["min_component_pixels"] == 16
73 assert config["vectorization"]["dash_max_gap_px"] == 40.085 assert config["vectorization"]["dash_max_gap_px"] == 40.0
Importance #15: README.md @@ -58,9 +58,9 @@
5858
59### Model registry / fetch-on-demand59### Model registry / fetch-on-demand
6060
61When no explicit model is passed, the CLI (and `resolve_model()`) resolve the61When no explicit model is passed, the CLI (and `resolve_model()`) resolve the
62default model from `_config.MODEL_REGISTRY`: a zipped bundle hosted at a stable62default model from `model_registry.MODEL_REGISTRY`: a zipped bundle hosted at a stable
63URL (e.g. an Azure Blob public/SAS URL) is downloaded once into a63URL (e.g. an Azure Blob public/SAS URL) is downloaded once into a
64content-addressed cache (`~/.cache/iolabs/line-bitmap-inference/`), verified by64content-addressed cache (`~/.cache/iolabs/line-bitmap-inference/`), verified by
65sha256, and reused thereafter. The fetch is plain stdlib `urllib` — no65sha256, and reused thereafter. The fetch is plain stdlib `urllib` — no
66`azure-sdk`/`dvc` dependency; a blob URL is just a GET. Override per environment66`azure-sdk`/`dvc` dependency; a blob URL is just a GET. Override per environment
Importance #16: README.md @@ -96,12 +96,9 @@
9696
97### Key flags / config97### Key flags / config
9898
99Runtime config is layered: built-in defaults → `--config JSON` → individual CLI99Runtime config is layered: built-in defaults → `--config JSON` → individual CLI
100flags (highest precedence). Defaults live in100flags (highest precedence). See [Configuration](#configuration) for the schema.
101`line_bitmap_inference.default.json`, mirrored by the pydantic `ConfigModel`
102tree in `_config.py`. To add a config key, add the field to the model and the
103JSON default — nothing else (unknown keys are rejected automatically).
104101
105- `--tile-size` / `--overlap` — sliding-window size and overlap (back-stepped102- `--tile-size` / `--overlap` — sliding-window size and overlap (back-stepped
106 origins, Hann-blended).103 origins, Hann-blended).
107- `--batch-size`, `--device` (`auto|cpu|cuda`), `--precision`.104- `--batch-size`, `--device` (`auto|cpu|cuda`), `--precision`.
Importance #17: README.md @@ -126,8 +123,21 @@
126123
127`scripts/pipeline/run_infer.py` is a path-insert wrapper around the same CLI for124`scripts/pipeline/run_infer.py` is a path-insert wrapper around the same CLI for
128running without installing the package.125running without installing the package.
129126
127## Configuration
128
129Defaults live in
130`src/iolabs_image_analyzer_line_bitmap_inference/line_bitmap_inference.default.json`.
131The schema is `LineBitmapInferenceConfig` in `_config.py` (a
132`config_loader.ConfigModel`); nested JSON sections are nested models and unknown
133keys are rejected. **To add a config key: add the field (with its type, default
134and any `Field` range) to the model and the same key with the same default to
135the JSON — nothing else.** `load_line_bitmap_inference_config`,
136`build_line_bitmap_inference_config` and
137`normalize_line_bitmap_inference_config` return a plain `dict`. Runtime
138overrides come from repeatable `--set KEY=VALUE`, never repo-local JSON.
139
130## Logging140## Logging
131141
132Wired to `iolabs-logstash`. The CLI calls `configure_job_logging` +142Wired to `iolabs-logstash`. The CLI calls `configure_job_logging` +
133`install_context_filter` at entry; library modules log structured `props`143`install_context_filter` at entry; library modules log structured `props`
Importance #18: README.md @@ -143,9 +153,10 @@
143 tta.py # shared FlipTTA (SSOT)153 tta.py # shared FlipTTA (SSOT)
144 vectorize.py overlay.py # mask -> vectors / QA154 vectorize.py overlay.py # mask -> vectors / QA
145 model_spec.py bundle.py # model load + export155 model_spec.py bundle.py # model load + export
146 io.py runner.py cli.py # outputs, batch, CLI156 io.py runner.py cli.py # outputs, batch, CLI
147 _config.py _log_props.py line_bitmap_inference.default.json # config + logging157 _config.py model_registry.py line_bitmap_inference.default.json # config + models
158 _log_props.py # logging
148```159```
149160
150## Scope161## Scope
151162
Importance #19: src/iolabs_image_analyzer_line_bitmap_inference/__init__.py @@ -13,9 +13,15 @@
13 __version__ = _pkg_version("iolabs-image-analyzer-line-bitmap-inference")13 __version__ = _pkg_version("iolabs-image-analyzer-line-bitmap-inference")
14except PackageNotFoundError: # running from a source checkout that isn't installed14except PackageNotFoundError: # running from a source checkout that isn't installed
15 __version__ = "0.0.0+local"15 __version__ = "0.0.0+local"
1616
17from ._config import build_line_bitmap_inference_config17from ._config import (
18 LineBitmapInferenceConfig,
19 LineBitmapInferenceConfigError,
20 build_line_bitmap_inference_config,
21 load_line_bitmap_inference_config,
22 normalize_line_bitmap_inference_config,
23)
18from .class_names import CLASS_NAMES24from .class_names import CLASS_NAMES
19from .model_fetch import resolve_model25from .model_fetch import resolve_model
20from .model_spec import (26from .model_spec import (
21 ModelSpec,27 ModelSpec,
Importance #20: src/iolabs_image_analyzer_line_bitmap_inference/__init__.py @@ -30,14 +36,18 @@
3036
31__all__ = [37__all__ = [
32 "__version__",38 "__version__",
33 "CLASS_NAMES",39 "CLASS_NAMES",
40 "LineBitmapInferenceConfig",
41 "LineBitmapInferenceConfigError",
34 "ModelSpec",42 "ModelSpec",
35 "FlipTTA",43 "FlipTTA",
36 "build_line_bitmap_inference_config",44 "build_line_bitmap_inference_config",
45 "load_line_bitmap_inference_config",
37 "load_model",46 "load_model",
38 "load_model_spec_from_bundle",47 "load_model_spec_from_bundle",
39 "load_model_spec_from_yaml",48 "load_model_spec_from_yaml",
49 "normalize_line_bitmap_inference_config",
40 "predict_tile",50 "predict_tile",
41 "resolve_model",51 "resolve_model",
42 "run_directory",52 "run_directory",
43 "run_tile",53 "run_tile",
Importance #21: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -1,18 +1,19 @@
1"""Inference runtime config: JSON defaults + overrides, unknown keys rejected.1"""Runtime inference config: tiling, device, output toggles, vectorization.
22
3Mirrors ``line_bitmap_inference.default.json`` with a pydantic3The schema is `LineBitmapInferenceConfig` (a `config_loader.ConfigModel`),
4``config_loader.ConfigModel`` tree. Unknown keys fail fast; ``--set``-style4mirroring `line_bitmap_inference.default.json` key for key.
5string overrides coerce through the fleet accepted-input matrix. This is the
6inference *runtime* config (tiling, device, output toggles, vectorization
7params). The *model* spec (architecture/encoder/classes) is a separate artifact
8read from the training YAML or a deployment bundle (see ``model_spec.py``).
95
10To add a config key, add the field to the model and the JSON default; nothing6Adding a config key means adding the field to the model and the same key to
11else.7`line_bitmap_inference.default.json` nothing else. Unknown keys are rejected.
8
9The entry points return a plain ``dict``. The *model* spec
10(architecture/encoder/classes) is a separate artifact read from the training
11YAML or a deployment bundle (see ``model_spec.py``); the model artifact
12registry lives in ``model_registry.py``.
12"""13"""
13import logging14import logging
14import os15from collections.abc import Mapping
15from pathlib import Path16from pathlib import Path
16from typing import Any17from typing import Any
1718
18import pydantic19import pydantic
Importance #22: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -21,11 +22,12 @@
21logger = logging.getLogger(__name__)22logger = logging.getLogger(__name__)
2223
23_PACKAGE_NAME = "iolabs_image_analyzer_line_bitmap_inference"24_PACKAGE_NAME = "iolabs_image_analyzer_line_bitmap_inference"
24_DEFAULT_FILENAME = "line_bitmap_inference.default.json"25_DEFAULT_FILENAME = "line_bitmap_inference.default.json"
26_CONTEXT = "line bitmap inference config"
2527
2628
27class VectorizationConfig(config_loader.ConfigModel):29class LineBitmapInferenceVectorizationConfig(config_loader.ConfigModel):
28 """Polyline extraction parameters applied to the predicted mask."""30 """Polyline extraction parameters applied to the predicted mask."""
2931
30 min_component_pixels: int = 1632 min_component_pixels: int = 16
31 simplify_tolerance_px: float = 2.033 simplify_tolerance_px: float = 2.0
Importance #23: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -45,9 +47,11 @@
45 write_vectors: bool = True47 write_vectors: bool = True
46 write_probabilities: bool = False48 write_probabilities: bool = False
47 write_overlay: bool = False49 write_overlay: bool = False
48 probabilities_dtype: str = "float16"50 probabilities_dtype: str = "float16"
49 vectorization: VectorizationConfig = VectorizationConfig()51 vectorization: LineBitmapInferenceVectorizationConfig = (
52 LineBitmapInferenceVectorizationConfig()
53 )
5054
51 @pydantic.field_validator("vectorization", mode="before")55 @pydantic.field_validator("vectorization", mode="before")
52 @classmethod56 @classmethod
53 def _none_section_is_default(cls, value: Any) -> Any:57 def _none_section_is_default(cls, value: Any) -> Any:
Importance #24: src/iolabs_image_analyzer_line_bitmap_inference/_config.py @@ -55,118 +59,54 @@
55 return {} if value is None else value59 return {} if value is None else value
5660
5761
58class LineBitmapInferenceConfigError(config_loader.ConfigError):62class LineBitmapInferenceConfigError(config_loader.ConfigError):
59 """Raised when the inference config contains unsupported keys or values."""63 """Raised when line bitmap inference config contains unsupported keys or values."""
6064
6165
62def _config_path_for_load(config_path: str | Path | None) -> str | Path | None:66def _load_model(
63 """Return *config_path*, or the sibling JSON when this module is loaded loose."""67 *,
68 overrides: Mapping[str, Any] | None = None,
69 config_path: str | Path | None = None,
70) -> LineBitmapInferenceConfig:
71 """Load packaged (or *config_path*) defaults, merge *overrides*, validate."""
72 if overrides:
73 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))
64 if config_path is not None:74 if config_path is not None:
65 return config_path75 logger.info("Config file applied: %s", config_path)
66 if __package__ in {None, ""}:76 return config_loader.load_config(
67 return Path(__file__).resolve().with_name(_DEFAULT_FILENAME)77 LineBitmapInferenceConfig,
68 return None78 package=_PACKAGE_NAME,
79 filename=_DEFAULT_FILENAME,
80 overrides=overrides,
81 config_path=config_path,
82 context=_CONTEXT,
83 error_cls=LineBitmapInferenceConfigError,
84 )
6985
7086
71def normalize_line_bitmap_inference_config(raw_config: dict[str, Any]) -> dict[str, Any]:87def normalize_line_bitmap_inference_config(
88 raw_config: Mapping[str, Any],
89) -> dict[str, Any]:
72 """Validate *raw_config* against the model, filling defaults; return a dict."""90 """Validate *raw_config* against the model, filling defaults; return a dict."""
73 config = config_loader.validate_config(91 return config_loader.validate_config(
74 LineBitmapInferenceConfig,92 LineBitmapInferenceConfig,
75 raw_config,93 raw_config,
76 context="inference config",94 context=_CONTEXT,
77 error_cls=LineBitmapInferenceConfigError,95 error_cls=LineBitmapInferenceConfigError,
78 ).model_dump()96 ).model_dump()
79 logger.debug("Normalized inference config")
80 return config
8197
8298
83def load_line_bitmap_inference_config(99def load_line_bitmap_inference_config(
84 config_path: str | Path | None = None,100 config_path: str | Path | None = None,
85) -> dict[str, Any]:101) -> dict[str, Any]:
86 """Load packaged (or *config_path*) defaults and validate them."""102 """Load the packaged defaults, or *config_path* in their place, and validate."""
87 return config_loader.load_config(103 return _load_model(config_path=config_path).model_dump()
88 LineBitmapInferenceConfig,
89 package=_PACKAGE_NAME,
90 filename=_DEFAULT_FILENAME,
91 config_path=_config_path_for_load(config_path),
92 context="inference config",
93 error_cls=LineBitmapInferenceConfigError,
94 ).model_dump()
95104
96105
97def build_line_bitmap_inference_config(106def build_line_bitmap_inference_config(
98 *,107 *,
99 overrides: dict[str, Any] | None = None,108 overrides: Mapping[str, Any] | None = None,
100 config_path: str | Path | None = None,109 config_path: str | Path | None = None,
101) -> dict[str, Any]:110) -> dict[str, Any]:
102 """Load defaults, deep-merge *overrides*, and validate."""111 """Load defaults (or *config_path* in their place), deep-merge *overrides*."""
103 return config_loader.load_config(112 return _load_model(overrides=overrides, config_path=config_path).model_dump()
104 LineBitmapInferenceConfig,
105 package=_PACKAGE_NAME,
106 filename=_DEFAULT_FILENAME,
107 overrides=overrides,
108 config_path=_config_path_for_load(config_path),
109 context="inference config",
110 error_cls=LineBitmapInferenceConfigError,
111 ).model_dump()
112
113
114# ---------------------------------------------------------------------------
115# Model artifact registry
116# ---------------------------------------------------------------------------
117# The runtime config above is orthogonal to the *model* itself. A trained model
118# is frozen into a portable bundle (model.ckpt + model.json) by ``export-model``
119# and hosted as a single ``.zip`` at a stable URL (an Azure Blob public/SAS
120# URL). ``model_fetch.resolve_model`` downloads + caches it on demand, verified
121# by sha256. URL / sha256 / cache are overridable per-environment without a code
122# change:
123# IOLABS_LINE_BITMAP_MODEL_URL
124# IOLABS_LINE_BITMAP_MODEL_SHA256
125# IOLABS_LINE_BITMAP_MODEL_CACHE
126
127DEFAULT_MODEL_NAME = "line_bitmap_v1"
128
129# NOTE: placeholder url/sha256. Upload the exported bundle .zip to Azure Blob,
130# then fill in the real values here (or set the env vars above). Until then,
131# auto-resolution raises a clear error and callers must pass an explicit
132# ``--bundle`` / ``--checkpoint``.
133MODEL_REGISTRY: dict[str, dict[str, str]] = {
134 DEFAULT_MODEL_NAME: {
135 "url": "https://REPLACE_ME.blob.core.windows.net/models/line-bitmap/line_bitmap_v1.zip",
136 "sha256": "",
137 "format": "bundle-zip",
138 },
139}
140
141_DEFAULT_MODEL_CACHE = Path.home() / ".cache" / "iolabs" / "line-bitmap-inference"
142
143
144def model_cache_dir() -> Path:
145 """Local cache root for downloaded model bundles (env-overridable)."""
146 override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_CACHE")
147 return Path(override).expanduser() if override else _DEFAULT_MODEL_CACHE
148
149
150def model_registry_entry(model_name: str | None = None) -> dict[str, str]:
151 """Return the registry entry for ``model_name`` (the default model if None).
152
153 ``IOLABS_LINE_BITMAP_MODEL_URL`` / ``IOLABS_LINE_BITMAP_MODEL_SHA256`` (when
154 set) override the registered url / sha256, so a deployment can point at a
155 model artifact without editing code.
156 """
157 name = model_name or DEFAULT_MODEL_NAME
158 try:
159 entry = dict(MODEL_REGISTRY[name])
160 except KeyError:
161 known = ", ".join(sorted(MODEL_REGISTRY)) or "<none>"
162 raise LineBitmapInferenceConfigError(
163 f"Unknown model name {name!r}. Known models: {known}"
164 ) from None
165
166 url_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_URL")
167 sha_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_SHA256")
168 if url_override:
169 entry["url"] = url_override
170 if sha_override:
171 entry["sha256"] = sha_override
172 return entry
Importance #25: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -4,9 +4,9 @@
4by ``export-model``). :func:`resolve_model` returns one of:4by ``export-model``). :func:`resolve_model` returns one of:
55
61. an explicit local path a bundle dir (or a ``.ckpt``) that exists is returned61. an explicit local path a bundle dir (or a ``.ckpt``) that exists is returned
7 unchanged, so an explicit ``--bundle`` / ``--checkpoint`` always wins;7 unchanged, so an explicit ``--bundle`` / ``--checkpoint`` always wins;
82. a named entry in :data:`._config.MODEL_REGISTRY` a zipped bundle hosted at a82. a named entry in :data:`.model_registry.MODEL_REGISTRY` a zipped bundle hosted at a
9 stable URL (e.g. an Azure Blob public/SAS URL), downloaded once into a9 stable URL (e.g. an Azure Blob public/SAS URL), downloaded once into a
10 content-addressed cache, verified by sha256, and reused thereafter.10 content-addressed cache, verified by sha256, and reused thereafter.
1111
12The download is plain stdlib ``urllib`` + ``hashlib`` a public/SAS blob URL is12The download is plain stdlib ``urllib`` + ``hashlib`` a public/SAS blob URL is
Importance #26: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -24,9 +24,9 @@
24import zipfile24import zipfile
25from pathlib import Path25from pathlib import Path
26from urllib.request import urlopen26from urllib.request import urlopen
2727
28from . import _config28from . import model_registry
2929
30__all__ = ["resolve_model", "ModelResolutionError"]30__all__ = ["resolve_model", "ModelResolutionError"]
3131
32logger = logging.getLogger(__name__)32logger = logging.getLogger(__name__)
Importance #27: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -55,16 +55,16 @@
55 if path.exists():55 if path.exists():
56 return path56 return path
57 raise ModelResolutionError(f"model path does not exist: {path}")57 raise ModelResolutionError(f"model path does not exist: {path}")
5858
59 entry = _config.model_registry_entry(model_name)59 entry = model_registry.model_registry_entry(model_name)
60 url = entry.get("url", "")60 url = entry.get("url", "")
61 sha256 = (entry.get("sha256") or "").lower()61 sha256 = (entry.get("sha256") or "").lower()
62 name = model_name or _config.DEFAULT_MODEL_NAME62 name = model_name or model_registry.DEFAULT_MODEL_NAME
6363
64 _require_configured(name, url, sha256)64 _require_configured(name, url, sha256)
6565
66 dest = _config.model_cache_dir() / name / sha256[:12]66 dest = model_registry.model_cache_dir() / name / sha256[:12]
67 if _is_valid_bundle(dest):67 if _is_valid_bundle(dest):
68 logger.debug("model %s already cached at %s", name, dest)68 logger.debug("model %s already cached at %s", name, dest)
69 return dest69 return dest
7070
Importance #28: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -80,9 +80,9 @@
80 if missing:80 if missing:
81 raise ModelResolutionError(81 raise ModelResolutionError(
82 f"model {name!r} is not configured ({', '.join(missing)} missing). "82 f"model {name!r} is not configured ({', '.join(missing)} missing). "
83 "Upload the exported bundle .zip and set its url + sha256 in "83 "Upload the exported bundle .zip and set its url + sha256 in "
84 "_config.MODEL_REGISTRY, or export IOLABS_LINE_BITMAP_MODEL_URL / "84 "model_registry.MODEL_REGISTRY, or export IOLABS_LINE_BITMAP_MODEL_URL / "
85 "IOLABS_LINE_BITMAP_MODEL_SHA256 — or pass an explicit "85 "IOLABS_LINE_BITMAP_MODEL_SHA256 — or pass an explicit "
86 "--bundle / --checkpoint."86 "--bundle / --checkpoint."
87 )87 )
8888
Importance #29: src/iolabs_image_analyzer_line_bitmap_inference/model_fetch.py @@ -99,9 +99,9 @@
99 return checkpoint.is_file()99 return checkpoint.is_file()
100100
101101
102def _fetch_bundle(*, name: str, url: str, sha256: str, dest: Path) -> Path:102def _fetch_bundle(*, name: str, url: str, sha256: str, dest: Path) -> Path:
103 cache_root = _config.model_cache_dir()103 cache_root = model_registry.model_cache_dir()
104 cache_root.mkdir(parents=True, exist_ok=True)104 cache_root.mkdir(parents=True, exist_ok=True)
105 logger.info("downloading model %s from %s", name, url)105 logger.info("downloading model %s from %s", name, url)
106106
107 work = Path(tempfile.mkdtemp(prefix=".fetch-", dir=cache_root))107 work = Path(tempfile.mkdtemp(prefix=".fetch-", dir=cache_root))
Importance #30: src/iolabs_image_analyzer_line_bitmap_inference/model_registry.py @@ -0,0 +1,67 @@
1"""Registry of trained model artifacts and their local download cache.
2
3The runtime config (``_config.py``) is orthogonal to the *model* itself. A
4trained model is frozen into a portable bundle (model.ckpt + model.json) by
5``export-model`` and hosted as a single ``.zip`` at a stable URL (an Azure Blob
6public/SAS URL). ``model_fetch.resolve_model`` downloads + caches it on demand,
7verified by sha256. URL / sha256 / cache are overridable per-environment without
8a code change::
9
10 IOLABS_LINE_BITMAP_MODEL_URL
11 IOLABS_LINE_BITMAP_MODEL_SHA256
12 IOLABS_LINE_BITMAP_MODEL_CACHE
13"""
14import logging
15import os
16from pathlib import Path
17
18from ._config import LineBitmapInferenceConfigError
19
20logger = logging.getLogger(__name__)
21
22DEFAULT_MODEL_NAME = "line_bitmap_v1"
23
24# NOTE: placeholder url/sha256. Upload the exported bundle .zip to Azure Blob,
25# then fill in the real values here (or set the env vars above). Until then,
26# auto-resolution raises a clear error and callers must pass an explicit
27# ``--bundle`` / ``--checkpoint``.
28MODEL_REGISTRY: dict[str, dict[str, str]] = {
29 DEFAULT_MODEL_NAME: {
30 "url": "https://REPLACE_ME.blob.core.windows.net/models/line-bitmap/line_bitmap_v1.zip",
31 "sha256": "",
32 "format": "bundle-zip",
33 },
34}
35
36_DEFAULT_MODEL_CACHE = Path.home() / ".cache" / "iolabs" / "line-bitmap-inference"
37
38
39def model_cache_dir() -> Path:
40 """Local cache root for downloaded model bundles (env-overridable)."""
41 override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_CACHE")
42 return Path(override).expanduser() if override else _DEFAULT_MODEL_CACHE
43
44
45def model_registry_entry(model_name: str | None = None) -> dict[str, str]:
46 """Return the registry entry for ``model_name`` (the default model if None).
47
48 ``IOLABS_LINE_BITMAP_MODEL_URL`` / ``IOLABS_LINE_BITMAP_MODEL_SHA256`` (when
49 set) override the registered url / sha256, so a deployment can point at a
50 model artifact without editing code.
51 """
52 name = model_name or DEFAULT_MODEL_NAME
53 try:
54 entry = dict(MODEL_REGISTRY[name])
55 except KeyError:
56 known = ", ".join(sorted(MODEL_REGISTRY)) or "<none>"
57 raise LineBitmapInferenceConfigError(
58 f"Unknown model name {name!r}. Known models: {known}"
59 ) from None
60
61 url_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_URL")
62 sha_override = os.environ.get("IOLABS_LINE_BITMAP_MODEL_SHA256")
63 if url_override:
64 entry["url"] = url_override
65 if sha_override:
66 entry["sha256"] = sha_override
67 return entry
0
Importance #31: tests/test_config.py @@ -1,57 +1,59 @@
1"""Config loading/validation — loaded in isolation (no heavy package imports)."""1"""Config loading/validation for the line bitmap inference runtime config."""
2import importlib.util2import json
3from pathlib import Path3from importlib import resources
44
5import pytest5import pytest
6from iolabs.common import config_loader
7from iolabs_image_analyzer_line_bitmap_inference import _config
8
9
10def _packaged_defaults() -> dict:
11 text = (
12 resources.files("iolabs_image_analyzer_line_bitmap_inference")
13 .joinpath("line_bitmap_inference.default.json")
14 .read_text(encoding="utf-8")
15 )
16 return json.loads(text)
17
18
19def test_model_defaults_match_packaged_json() -> None:
20 assert _config.LineBitmapInferenceConfig().model_dump() == _packaged_defaults()
621
7MODULE_PATH = (22
8 Path(__file__).resolve().parents[1]23def test_load_line_bitmap_inference_config_returns_packaged_defaults() -> None:
9 / "src"24 assert _config.load_line_bitmap_inference_config() == _packaged_defaults()
10 / "iolabs_image_analyzer_line_bitmap_inference"25
11 / "_config.py"26
12)27def test_error_class_is_config_error() -> None:
1328 assert issubclass(_config.LineBitmapInferenceConfigError, config_loader.ConfigError)
14SPEC = importlib.util.spec_from_file_location("lbi_config", MODULE_PATH)29 assert issubclass(_config.LineBitmapInferenceConfigError, ValueError)
15assert SPEC is not None and SPEC.loader is not None
16MODULE = importlib.util.module_from_spec(SPEC)
17SPEC.loader.exec_module(MODULE)
18
19
20def test_load_defaults() -> None:
21 config = MODULE.load_line_bitmap_inference_config()
22 assert config["tile_size"] == 512
23 assert config["overlap"] == 128
24 assert config["tta"] is False
25 assert config["write_vectors"] is True
26 assert config["blend"] == "hann"
27 assert config["vectorization"]["min_component_pixels"] == 16
2830
2931
30def test_unknown_top_level_key_is_rejected() -> None:32def test_unknown_top_level_key_is_rejected() -> None:
31 with pytest.raises(MODULE.LineBitmapInferenceConfigError, match="random_seed"):33 with pytest.raises(_config.LineBitmapInferenceConfigError, match="random_seed"):
32 MODULE.build_line_bitmap_inference_config(overrides={"random_seed": 1})34 _config.build_line_bitmap_inference_config(overrides={"random_seed": 1})
3335
3436
35def test_unknown_vectorization_key_is_rejected() -> None:37def test_unknown_nested_key_is_rejected() -> None:
36 with pytest.raises(MODULE.LineBitmapInferenceConfigError, match="bogus"):38 with pytest.raises(_config.LineBitmapInferenceConfigError, match="bogus"):
37 MODULE.build_line_bitmap_inference_config(39 _config.build_line_bitmap_inference_config(
38 overrides={"vectorization": {"bogus": 1}}40 overrides={"vectorization": {"bogus": 1}}
39 )41 )
4042
4143
42def test_overrides_deep_merge_keeps_other_defaults() -> None:44def test_overrides_deep_merge_onto_defaults() -> None:
43 config = MODULE.build_line_bitmap_inference_config(45 config = _config.build_line_bitmap_inference_config(
44 overrides={"tile_size": 768, "vectorization": {"dash_max_gap_px": 10.0}}46 overrides={"tile_size": 768, "vectorization": {"dash_max_gap_px": 10.0}}
45 )47 )
46 assert config["tile_size"] == 76848 assert config["tile_size"] == 768
47 assert config["overlap"] == 128 # untouched default49 assert config["overlap"] == 128 # untouched default
48 assert config["vectorization"]["dash_max_gap_px"] == 10.050 assert config["vectorization"]["dash_max_gap_px"] == 10.0
49 assert config["vectorization"]["min_component_pixels"] == 16 # untouched default51 assert config["vectorization"]["min_component_pixels"] == 16 # untouched default
5052
5153
52def test_string_overrides_are_coerced() -> None:54def test_set_override_coercion_and_rejection() -> None:
53 config = MODULE.build_line_bitmap_inference_config(55 config = _config.build_line_bitmap_inference_config(
54 overrides={56 overrides={
55 "tile_size": "1e3",57 "tile_size": "1e3",
56 "tta": "on",58 "tta": "on",
57 "vectorization": {"dash_max_gap_px": "5"},59 "vectorization": {"dash_max_gap_px": "5"},
Importance #32: tests/test_config.py @@ -59,15 +61,25 @@
59 )61 )
60 assert config["tile_size"] == 100062 assert config["tile_size"] == 1000
61 assert config["tta"] is True63 assert config["tta"] is True
62 assert config["vectorization"]["dash_max_gap_px"] == 5.064 assert config["vectorization"]["dash_max_gap_px"] == 5.0
65 with pytest.raises(_config.LineBitmapInferenceConfigError, match="tta"):
66 _config.build_line_bitmap_inference_config(overrides={"tta": "flase"})
67
68
69def test_normalize_fills_defaults() -> None:
70 config = _config.normalize_line_bitmap_inference_config({"tile_size": 256})
71 assert config["tile_size"] == 256
72 assert config == {**_packaged_defaults(), "tile_size": 256}
6373
6474
65def test_bool_is_rejected_for_int_field() -> None:75def test_bool_is_rejected_for_int_field() -> None:
66 with pytest.raises(MODULE.LineBitmapInferenceConfigError, match="tile_size"):76 with pytest.raises(_config.LineBitmapInferenceConfigError, match="tile_size"):
67 MODULE.build_line_bitmap_inference_config(overrides={"tile_size": True})77 _config.build_line_bitmap_inference_config(overrides={"tile_size": True})
6878
6979
70def test_null_vectorization_section_falls_back_to_defaults() -> None:80def test_null_vectorization_section_falls_back_to_defaults() -> None:
71 config = MODULE.build_line_bitmap_inference_config(overrides={"vectorization": None})81 config = _config.build_line_bitmap_inference_config(
82 overrides={"vectorization": None}
83 )
72 assert config["vectorization"]["min_component_pixels"] == 1684 assert config["vectorization"]["min_component_pixels"] == 16
73 assert config["vectorization"]["dash_max_gap_px"] == 40.085 assert config["vectorization"]["dash_max_gap_px"] == 40.0