""" 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, )