This commit is contained in:
2026-03-26 06:10:19 -03:00
parent 731964ca10
commit e27cb5bcc3
41 changed files with 2079 additions and 95 deletions

88
core/api/detect_config.py Normal file
View 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

View File

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

View File

@@ -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"]
}
]
}

View File

@@ -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

View 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,
]