major refactor
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user