- "engine/presets/sign.json" - "engine/presets/patch.json" - "engine/presets/stencil.json" - "engine/presets/detailed.json" - "engine/presets/custom.json" - "engine/presets/loader.py" - "engine/api/routes.py" - "engine/tests/test_presets.py" GSD-Task: S03/T01
117 lines
3.7 KiB
Python
117 lines
3.7 KiB
Python
"""Preset loader — reads JSON config files from the presets directory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_PRESETS_DIR = Path(__file__).parent
|
|
_VALID_SECTIONS = {"preprocessing", "vectorization", "postprocessing"}
|
|
|
|
# In-memory cache — loaded once per process, cleared on reload().
|
|
_cache: dict[str, dict[str, Any]] = {}
|
|
|
|
|
|
def _load_all() -> dict[str, dict[str, Any]]:
|
|
"""Scan the presets directory and load every .json file."""
|
|
presets: dict[str, dict[str, Any]] = {}
|
|
for p in sorted(_PRESETS_DIR.glob("*.json")):
|
|
try:
|
|
data = json.loads(p.read_text())
|
|
name = data.get("name", p.stem)
|
|
presets[name] = data
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
logger.warning("Skipping invalid preset file %s: %s", p, exc)
|
|
return presets
|
|
|
|
|
|
def all_presets() -> dict[str, dict[str, Any]]:
|
|
"""Return all presets, loading from disk on first call.
|
|
|
|
Returns:
|
|
Dict mapping preset name → full preset config dict.
|
|
"""
|
|
if not _cache:
|
|
_cache.update(_load_all())
|
|
return dict(_cache)
|
|
|
|
|
|
def get_preset(name: str) -> dict[str, Any] | None:
|
|
"""Return a single preset by name, or None if not found."""
|
|
presets = all_presets()
|
|
return presets.get(name)
|
|
|
|
|
|
def preset_names() -> list[str]:
|
|
"""Return sorted list of available preset names."""
|
|
return sorted(all_presets().keys())
|
|
|
|
|
|
def resolve_params(
|
|
preset_name: str,
|
|
user_params: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Resolve effective parameters by merging preset defaults with user overrides.
|
|
|
|
The preset provides three sections: preprocessing, vectorization, postprocessing.
|
|
User params are applied as a flat overlay — keys in user_params override matching
|
|
keys from the preset at any level.
|
|
|
|
Special keys:
|
|
- ``mode``: if present in user_params, overrides ``vectorization.mode``.
|
|
- ``epsilon``: if present in user_params, overrides ``postprocessing.epsilon``.
|
|
|
|
Returns:
|
|
Merged param dict with top-level keys for preprocessing, vectorization mode,
|
|
vectorizer-specific params, and postprocessing epsilon.
|
|
"""
|
|
user = user_params or {}
|
|
preset = get_preset(preset_name)
|
|
|
|
if preset is None:
|
|
# No preset found — fall back to raw user params.
|
|
return {
|
|
"preprocessing": {},
|
|
"vectorization_mode": user.get("mode", "potrace"),
|
|
"vectorizer_params": {k: v for k, v in user.items() if k != "mode"},
|
|
"postprocessing": {"epsilon": float(user.get("epsilon", 1.0))},
|
|
}
|
|
|
|
# -- Preprocessing: preset defaults + user overrides --
|
|
pre = dict(preset.get("preprocessing", {}))
|
|
for key in list(pre.keys()):
|
|
if key in user:
|
|
pre[key] = user[key]
|
|
|
|
# -- Vectorization mode --
|
|
vec_cfg = preset.get("vectorization", {})
|
|
mode = user.get("mode", vec_cfg.get("mode", "potrace"))
|
|
|
|
# -- Vectorizer-specific params: preset defaults + user overrides --
|
|
vec_params = dict(vec_cfg.get(mode, vec_cfg.get("potrace", {})))
|
|
# Apply any user overrides that match vectorizer param names.
|
|
for key in list(vec_params.keys()):
|
|
if key in user:
|
|
vec_params[key] = user[key]
|
|
|
|
# -- Postprocessing --
|
|
post = dict(preset.get("postprocessing", {}))
|
|
if "epsilon" in user:
|
|
post["epsilon"] = float(user["epsilon"])
|
|
|
|
return {
|
|
"preprocessing": pre,
|
|
"vectorization_mode": mode,
|
|
"vectorizer_params": vec_params,
|
|
"postprocessing": post,
|
|
}
|
|
|
|
|
|
def reload() -> None:
|
|
"""Clear the preset cache and reload from disk."""
|
|
_cache.clear()
|
|
_cache.update(_load_all())
|