- pipeline edge transforms: stages can declare accepted_transforms, edges carry a transform dict, runner injects per-stage and nodes apply (e.g. invert_mask before edge detection); editable from UI via PUT /config/edge-transform - rename mpr-ui-framework -> soleprint-ui (now an external package synced via .spr from /home/mariano/wdir/spr); add @vue-flow/core and uplot to detection-app so linked package resolves them - Tiltfile guards kubectl context, k8s commands pin --context kind-mpr - kind-config: gateway on hostPort 30080 (Caddy fronts mpr.local.ar) - modelgen: pyproject.toml, .spr marker, dict default_factory support
204 lines
5.8 KiB
Python
204 lines
5.8 KiB
Python
"""
|
|
Runtime config endpoint for the detection pipeline.
|
|
|
|
GET /detect/config — read current config
|
|
PUT /detect/config — update config (takes effect on next run)
|
|
GET /detect/config/stages — list stage palette with config fields
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/detect", tags=["detect"])
|
|
|
|
# In-memory config — persists until server restart.
|
|
# Phase 12+ moves this to DB.
|
|
_runtime_config: dict = {}
|
|
|
|
|
|
class ConfigUpdate(BaseModel):
|
|
detection: dict | None = None
|
|
ocr: dict | None = None
|
|
resolver: dict | None = None
|
|
escalation: dict | None = None
|
|
preprocessing: dict | None = None
|
|
|
|
|
|
class StageOutputHintInfo(BaseModel):
|
|
key: str
|
|
type: str
|
|
label: str = ""
|
|
default_opacity: float = 0.5
|
|
src_format: str = "png"
|
|
|
|
|
|
class TransformOptionInfo(BaseModel):
|
|
key: str
|
|
type: str
|
|
default: object = False
|
|
label: str = ""
|
|
description: str = ""
|
|
|
|
|
|
class StageConfigInfo(BaseModel):
|
|
name: str
|
|
label: str
|
|
description: str
|
|
category: str
|
|
config_fields: list[dict]
|
|
output_hints: list[StageOutputHintInfo] = []
|
|
accepted_transforms: list[TransformOptionInfo] = []
|
|
reads: list[str]
|
|
writes: list[str]
|
|
|
|
|
|
@router.get("/config")
|
|
def read_config():
|
|
return _runtime_config
|
|
|
|
|
|
@router.put("/config")
|
|
def write_config(update: ConfigUpdate):
|
|
changes = update.model_dump(exclude_none=True)
|
|
for section, values in changes.items():
|
|
if section not in _runtime_config:
|
|
_runtime_config[section] = {}
|
|
_runtime_config[section].update(values)
|
|
|
|
logger.info("Config updated: %s", list(changes.keys()))
|
|
return _runtime_config
|
|
|
|
|
|
@router.get("/config/profiles")
|
|
def get_profiles():
|
|
"""List available detection profiles."""
|
|
from core.detect.profile import list_profiles as _list
|
|
return [{"name": name} for name in _list()]
|
|
|
|
|
|
@router.get("/config/profiles/{profile_name}/pipeline")
|
|
def get_pipeline_config(profile_name: str):
|
|
"""Return the pipeline composition for a profile."""
|
|
from core.detect.profile import get_profile
|
|
from fastapi import HTTPException
|
|
|
|
try:
|
|
profile = get_profile(profile_name)
|
|
except ValueError:
|
|
raise HTTPException(status_code=404, detail=f"Unknown profile: {profile_name}")
|
|
|
|
return profile["pipeline"]
|
|
|
|
|
|
class UpdateEdgeTransformRequest(BaseModel):
|
|
profile_name: str = "soccer_broadcast"
|
|
source_stage: str
|
|
target_stage: str
|
|
transform: dict
|
|
|
|
|
|
@router.put("/config/edge-transform")
|
|
def update_edge_transform(req: UpdateEdgeTransformRequest):
|
|
"""Update the transform on an edge in a profile's pipeline config."""
|
|
from uuid import UUID
|
|
from core.db.models import Profile
|
|
from core.db.connection import get_session
|
|
from sqlmodel import select
|
|
from fastapi import HTTPException
|
|
|
|
with get_session() as session:
|
|
stmt = select(Profile).where(Profile.name == req.profile_name)
|
|
profile = session.exec(stmt).first()
|
|
if not profile:
|
|
raise HTTPException(status_code=404, detail=f"Profile not found: {req.profile_name}")
|
|
|
|
pipeline = dict(profile.pipeline)
|
|
edges = pipeline.get("edges", [])
|
|
|
|
found = False
|
|
for edge in edges:
|
|
if edge.get("source") == req.source_stage and edge.get("target") == req.target_stage:
|
|
edge["transform"] = req.transform
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Edge not found: {req.source_stage} → {req.target_stage}",
|
|
)
|
|
|
|
pipeline["edges"] = edges
|
|
profile.pipeline = pipeline
|
|
session.commit()
|
|
|
|
return {"status": "updated", "edge": f"{req.source_stage} → {req.target_stage}", "transform": req.transform}
|
|
|
|
|
|
@router.get("/config/stages", response_model=list[StageConfigInfo])
|
|
def list_stage_configs():
|
|
"""Return the stage palette with config field metadata for the editor."""
|
|
from core.detect.stages import list_stages
|
|
|
|
result = []
|
|
for stage in list_stages():
|
|
info = _stage_to_info(stage)
|
|
result.append(info)
|
|
return result
|
|
|
|
|
|
@router.get("/config/stages/{stage_name}", response_model=StageConfigInfo)
|
|
def get_stage_config(stage_name: str):
|
|
"""Return config field metadata for a single stage."""
|
|
from core.detect.stages import get_stage
|
|
|
|
try:
|
|
stage = get_stage(stage_name)
|
|
except KeyError:
|
|
from fastapi import HTTPException
|
|
raise HTTPException(status_code=404, detail=f"Unknown stage: {stage_name}")
|
|
return _stage_to_info(stage)
|
|
|
|
|
|
def _stage_to_info(stage) -> StageConfigInfo:
|
|
return StageConfigInfo(
|
|
name=stage.name,
|
|
label=stage.label,
|
|
description=stage.description,
|
|
category=stage.category,
|
|
config_fields=[
|
|
{
|
|
"name": f.name,
|
|
"type": f.type,
|
|
"default": f.default,
|
|
"description": f.description,
|
|
"min": f.min,
|
|
"max": f.max,
|
|
"options": f.options,
|
|
}
|
|
for f in stage.config_fields
|
|
],
|
|
output_hints=[
|
|
StageOutputHintInfo(
|
|
key=h.key, type=h.type, label=h.label,
|
|
default_opacity=h.default_opacity, src_format=h.src_format,
|
|
)
|
|
for h in getattr(stage, "output_hints", [])
|
|
],
|
|
accepted_transforms=[
|
|
TransformOptionInfo(
|
|
key=t.key, type=t.type, default=t.default,
|
|
label=t.label, description=t.description,
|
|
)
|
|
for t in getattr(stage, "accepted_transforms", [])
|
|
],
|
|
reads=stage.io.reads,
|
|
writes=stage.io.writes,
|
|
)
|