phase 12
This commit is contained in:
88
core/api/detect_config.py
Normal file
88
core/api/detect_config.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
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 StageConfigInfo(BaseModel):
|
||||
name: str
|
||||
label: str
|
||||
description: str
|
||||
category: str
|
||||
config_fields: list[dict]
|
||||
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/stages", response_model=list[StageConfigInfo])
|
||||
def list_stage_configs():
|
||||
"""Return the stage palette with config field metadata for the editor."""
|
||||
from detect.stages import list_stages
|
||||
|
||||
result = []
|
||||
for stage in list_stages():
|
||||
info = 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
|
||||
],
|
||||
reads=stage.io.reads,
|
||||
writes=stage.io.writes,
|
||||
)
|
||||
result.append(info)
|
||||
return result
|
||||
@@ -26,6 +26,7 @@ from strawberry.fastapi import GraphQLRouter
|
||||
from core.api.chunker_sse import router as chunker_router
|
||||
from core.api.detect_sse import router as detect_router
|
||||
from core.api.detect_replay import router as detect_replay_router
|
||||
from core.api.detect_config import router as detect_config_router
|
||||
from core.api.graphql import schema as graphql_schema
|
||||
|
||||
CALLBACK_API_KEY = os.environ.get("CALLBACK_API_KEY", "")
|
||||
@@ -60,6 +61,9 @@ app.include_router(detect_router)
|
||||
# Detection replay/retry
|
||||
app.include_router(detect_replay_router)
|
||||
|
||||
# Detection config
|
||||
app.include_router(detect_config_router)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
"target": "typescript",
|
||||
"output": "ui/detection-app/src/types/sse-contract.ts",
|
||||
"include": ["detect_views"]
|
||||
},
|
||||
{
|
||||
"target": "typescript",
|
||||
"output": "ui/detection-app/src/types/store-state.ts",
|
||||
"include": ["ui_state_views"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ from .detect_jobs import (
|
||||
from .media import AssetStatus, MediaAsset
|
||||
from .presets import BUILTIN_PRESETS, TranscodePreset
|
||||
from .detect import DETECT_VIEWS # noqa: F401 — discovered by modelgen generic loader
|
||||
from .ui_state import UI_STATE_VIEWS # noqa: F401 — UI store state types
|
||||
from .views import ChunkEvent, ChunkOutputFile, PipelineStats, WorkerEvent
|
||||
|
||||
# Core domain models - generates Django, Pydantic, TypeScript
|
||||
|
||||
139
core/schema/models/ui_state.py
Normal file
139
core/schema/models/ui_state.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
UI application state models.
|
||||
|
||||
Source of truth for all frontend store state shapes.
|
||||
Generates TypeScript types via modelgen.
|
||||
The store implementation (Pinia, etc.) is just the reactive container.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pipeline store
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class NodeState:
|
||||
"""A pipeline node's current status."""
|
||||
id: str
|
||||
status: str = "pending" # pending | running | done | error
|
||||
has_checkpoint: bool = False
|
||||
has_region_editor: bool = False # stage works with visual regions
|
||||
has_config_editor: bool = True # all stages have config
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineState:
|
||||
"""Full pipeline run state."""
|
||||
job_id: str = ""
|
||||
status: str = "idle" # idle | running | paused | completed | error
|
||||
layout_mode: str = "normal" # normal | bbox_editor | stage_editor
|
||||
editor_stage: Optional[str] = None # which stage's editor is open
|
||||
nodes: List[NodeState] = field(default_factory=list)
|
||||
current_stage: Optional[str] = None
|
||||
run_id: Optional[str] = None
|
||||
parent_job_id: Optional[str] = None
|
||||
run_type: str = "initial" # initial | replay | retry
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config store
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class DetectionConfigOverrides:
|
||||
"""Tunable detection stage config."""
|
||||
model_name: Optional[str] = None
|
||||
confidence_threshold: Optional[float] = None
|
||||
target_classes: Optional[List[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OCRConfigOverrides:
|
||||
"""Tunable OCR stage config."""
|
||||
languages: Optional[List[str]] = None
|
||||
min_confidence: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResolverConfigOverrides:
|
||||
"""Tunable brand resolver config."""
|
||||
fuzzy_threshold: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EscalationConfigOverrides:
|
||||
"""Tunable escalation config."""
|
||||
vlm_min_confidence: Optional[float] = None
|
||||
cloud_min_confidence: Optional[float] = None
|
||||
cloud_provider: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PreprocessingConfigOverrides:
|
||||
"""Tunable preprocessing config."""
|
||||
binarize: Optional[bool] = None
|
||||
deskew: Optional[bool] = None
|
||||
contrast: Optional[bool] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigOverrides:
|
||||
"""Aggregated config overrides from all panels."""
|
||||
detection: Optional[DetectionConfigOverrides] = None
|
||||
ocr: Optional[OCRConfigOverrides] = None
|
||||
resolver: Optional[ResolverConfigOverrides] = None
|
||||
escalation: Optional[EscalationConfigOverrides] = None
|
||||
preprocessing: Optional[PreprocessingConfigOverrides] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigState:
|
||||
"""Config store state."""
|
||||
current: ConfigOverrides = field(default_factory=ConfigOverrides)
|
||||
pending: ConfigOverrides = field(default_factory=ConfigOverrides)
|
||||
dirty: bool = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Selection store
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class BboxRegion:
|
||||
"""A user-drawn bounding box region."""
|
||||
x: int
|
||||
y: int
|
||||
w: int
|
||||
h: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SelectionState:
|
||||
"""Cross-panel selection state."""
|
||||
selected_frame: Optional[int] = None
|
||||
selected_brand: Optional[str] = None
|
||||
hovered_timestamp: Optional[float] = None
|
||||
bbox_region: Optional[BboxRegion] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Export for modelgen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
UI_STATE_VIEWS = [
|
||||
NodeState,
|
||||
PipelineState,
|
||||
DetectionConfigOverrides,
|
||||
OCRConfigOverrides,
|
||||
ResolverConfigOverrides,
|
||||
EscalationConfigOverrides,
|
||||
PreprocessingConfigOverrides,
|
||||
ConfigOverrides,
|
||||
ConfigState,
|
||||
BboxRegion,
|
||||
SelectionState,
|
||||
]
|
||||
Reference in New Issue
Block a user