kerf-engine/engine/presets/loader.py
jlightner 32eb02ccb6 fix: Implemented 5 preset configs (sign, patch, stencil, detailed, cust…
- "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
2026-03-26 04:45:52 +00:00

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())