major refactor

This commit is contained in:
2026-03-27 06:02:58 -03:00
parent bcf6f3dc71
commit 51ce14a812
18 changed files with 351 additions and 523 deletions

View File

@@ -1,7 +1,7 @@
"""
API endpoints for checkpoint inspection, replay, retry, and GPU proxy.
GET /detect/checkpoints/{job_id} — list available checkpoints
GET /detect/checkpoints/{timeline_id} — list available checkpoints
POST /detect/replay — replay from a stage with config overrides
POST /detect/retry — queue async retry with different provider
POST /detect/replay-stage — replay single stage (fast path)
@@ -31,7 +31,7 @@ class CheckpointInfo(BaseModel):
class ScenarioInfo(BaseModel):
job_id: str
timeline_id: str
stage: str
scenario_label: str
profile_name: str
@@ -41,21 +41,21 @@ class ScenarioInfo(BaseModel):
class ReplayRequest(BaseModel):
job_id: str
timeline_id: str
start_stage: str
config_overrides: dict | None = None
class ReplayResponse(BaseModel):
status: str
job_id: str
timeline_id: str
start_stage: str
detections: int = 0
brands_found: int = 0
class RetryRequest(BaseModel):
job_id: str
timeline_id: str
config_overrides: dict | None = None
start_stage: str = "escalate_vlm"
schedule_seconds: float | None = None # delay before execution (off-peak)
@@ -64,11 +64,11 @@ class RetryRequest(BaseModel):
class RetryResponse(BaseModel):
status: str
task_id: str
job_id: str
timeline_id: str
class ReplaySingleStageRequest(BaseModel):
job_id: str
timeline_id: str
stage: str
frame_refs: list[int] | None = None
config_overrides: dict | None = None
@@ -102,15 +102,15 @@ class ReplaySingleStageResponse(BaseModel):
# --- Endpoints ---
@router.get("/checkpoints/{job_id}")
def list_checkpoints(job_id: str) -> list[CheckpointInfo]:
@router.get("/checkpoints/{timeline_id}")
def list_checkpoints(timeline_id: str) -> list[CheckpointInfo]:
"""List available checkpoint stages for a job."""
from detect.checkpoint import list_checkpoints as _list
try:
stages = _list(job_id)
stages = _list(timeline_id)
except Exception as e:
raise HTTPException(status_code=404, detail=f"No checkpoints for job {job_id}: {e}")
raise HTTPException(status_code=404, detail=f"No checkpoints for job {timeline_id}: {e}")
result = [CheckpointInfo(stage=s) for s in stages]
return result
@@ -123,7 +123,7 @@ class CheckpointFrameInfo(BaseModel):
class CheckpointData(BaseModel):
job_id: str
timeline_id: str
stage: str
profile_name: str
video_path: str
@@ -135,26 +135,32 @@ class CheckpointData(BaseModel):
stage_output_key: str = ""
@router.get("/checkpoints/{job_id}/{stage}", response_model=CheckpointData)
def get_checkpoint_data(job_id: str, stage: str):
@router.get("/checkpoints/{timeline_id}/{stage}", response_model=CheckpointData)
def get_checkpoint_data(timeline_id: str, stage: str):
"""Load checkpoint frames + metadata for the editor UI."""
from core.db.detect import get_stage_checkpoint
from uuid import UUID
from core.db.tables import Timeline, Checkpoint
from core.db.connection import get_session
from core.db.checkpoint import list_checkpoints
from detect.checkpoint.frames import load_frames_b64
checkpoint = get_stage_checkpoint(job_id, stage)
if not checkpoint:
raise HTTPException(status_code=404, detail=f"No checkpoint for {job_id}/{stage}")
with get_session() as session:
timeline = session.get(Timeline, UUID(timeline_id))
if not timeline:
raise HTTPException(status_code=404, detail=f"Timeline not found: {timeline_id}")
raw_manifest = checkpoint.frames_manifest or {}
manifest = {int(k): v for k, v in raw_manifest.items()}
frame_metadata = checkpoint.frames_meta or []
checkpoints = list_checkpoints(session, UUID(timeline_id))
if not checkpoints:
raise HTTPException(status_code=404, detail=f"No checkpoints for timeline {timeline_id}")
# Prefer a checkpoint that has this stage's output; fall back to latest
checkpoint = next(
(c for c in reversed(checkpoints) if stage in (c.stage_outputs or {})),
checkpoints[-1],
)
# Only load filtered frames if available, otherwise all
filtered = set(checkpoint.filtered_frame_sequences or [])
if filtered:
manifest = {k: v for k, v in manifest.items() if k in filtered}
frames_b64 = load_frames_b64(manifest, frame_metadata)
raw_manifest = timeline.frames_manifest or {}
manifest = {int(k): v for k, v in raw_manifest.items()}
frames_b64 = load_frames_b64(manifest, timeline.frames_meta or [])
frame_list = [
CheckpointFrameInfo(seq=f["seq"], timestamp=f["timestamp"], jpeg_b64=f["jpeg_b64"])
@@ -162,38 +168,44 @@ def get_checkpoint_data(job_id: str, stage: str):
]
return CheckpointData(
job_id=str(checkpoint.job_id),
stage=checkpoint.stage,
profile_name=checkpoint.profile_name,
video_path=checkpoint.video_path,
timeline_id=timeline_id,
stage=stage,
profile_name=timeline.profile_name,
video_path=timeline.source_video,
is_scenario=checkpoint.is_scenario,
scenario_label=checkpoint.scenario_label,
frames=frame_list,
stats=checkpoint.stats or {},
config_snapshot=checkpoint.config_snapshot or {},
stage_output_key=checkpoint.stage_output_key or "",
config_snapshot=checkpoint.config_overrides or {},
stage_output_key=stage,
)
@router.get("/scenarios", response_model=list[ScenarioInfo])
def list_scenarios_endpoint():
"""List all available scenarios (bookmarked checkpoints)."""
from core.db.detect import list_scenarios
from core.db.tables import Timeline
from core.db.connection import get_session
from core.db.checkpoint import list_scenarios
scenarios = list_scenarios()
result = []
for s in scenarios:
manifest = s.frames_manifest or {}
info = ScenarioInfo(
job_id=str(s.job_id),
stage=s.stage,
scenario_label=s.scenario_label,
profile_name=s.profile_name,
video_path=s.video_path,
frame_count=len(manifest),
created_at=str(s.created_at) if s.created_at else "",
)
result.append(info)
with get_session() as session:
scenarios = list_scenarios(session)
result = []
for s in scenarios:
timeline = session.get(Timeline, s.timeline_id)
if not timeline:
continue
last_stage = next(reversed(s.stage_outputs), "") if s.stage_outputs else ""
info = ScenarioInfo(
timeline_id=str(s.timeline_id),
stage=last_stage,
scenario_label=s.scenario_label,
profile_name=timeline.profile_name,
video_path=timeline.source_video,
frame_count=len(timeline.frames_manifest or {}),
created_at=str(s.created_at) if s.created_at else "",
)
result.append(info)
return result
@@ -204,7 +216,7 @@ def replay(req: ReplayRequest):
try:
result = replay_from(
job_id=req.job_id,
timeline_id=req.timeline_id,
start_stage=req.start_stage,
config_overrides=req.config_overrides,
)
@@ -219,7 +231,7 @@ def replay(req: ReplayRequest):
response = ReplayResponse(
status="completed",
job_id=req.job_id,
timeline_id=req.timeline_id,
start_stage=req.start_stage,
detections=len(detections),
brands_found=brands_found,
@@ -233,7 +245,7 @@ def retry(req: RetryRequest):
from detect.checkpoint.tasks import retry_candidates
kwargs = {
"job_id": req.job_id,
"timeline_id": req.timeline_id,
"config_overrides": req.config_overrides,
"start_stage": req.start_stage,
}
@@ -246,7 +258,7 @@ def retry(req: RetryRequest):
response = RetryResponse(
status="queued",
task_id=task.id,
job_id=req.job_id,
timeline_id=req.timeline_id,
)
return response
@@ -258,7 +270,7 @@ def replay_single_stage(req: ReplaySingleStageRequest):
try:
result = _replay(
job_id=req.job_id,
timeline_id=req.timeline_id,
stage=req.stage,
frame_refs=req.frame_refs,
config_overrides=req.config_overrides,