This commit is contained in:
2026-03-30 09:53:10 -03:00
parent 4220b0418e
commit aac27b8504
32 changed files with 1068 additions and 329 deletions

View File

@@ -1,7 +1,7 @@
"""
Pipeline run endpoints.
POST /detect/run — launch pipeline on selected source
POST /detect/run — launch pipeline on a timeline
POST /detect/stop/{job_id} — cancel a running pipeline
POST /detect/pause/{job_id} — pause after current stage
POST /detect/resume/{job_id} — resume a paused pipeline
@@ -30,20 +30,20 @@ _cancelled_jobs: set[str] = set()
class RunRequest(BaseModel):
video_path: str # storage key
timeline_id: str
profile_name: str = "soccer_broadcast"
source_asset_id: str = ""
checkpoint: bool = True
skip_vlm: bool = False
skip_cloud: bool = False
log_level: str = "INFO" # INFO | DEBUG
pause_after_stage: bool = False
config_overrides: dict | None = None
class RunResponse(BaseModel):
status: str
job_id: str
video_path: str
timeline_id: str
def _resolve_video_path(video_path: str) -> str:
@@ -59,13 +59,41 @@ def _resolve_video_path(video_path: str) -> str:
@router.post("/run", response_model=RunResponse)
def run_pipeline(req: RunRequest):
"""Launch a detection pipeline run on a source chunk."""
"""Launch a detection pipeline run on a timeline."""
from core.detect import emit
from core.detect.graph import get_pipeline
from core.detect.state import DetectState
from core.detect.checkpoint.storage import get_timeline
from core.db.connection import get_session
from core.db.job import create_job, update_job_status
local_path = _resolve_video_path(req.video_path)
job_id = str(uuid.uuid4())
# Load timeline
try:
timeline = get_timeline(req.timeline_id)
except ValueError:
raise HTTPException(status_code=404, detail=f"Timeline not found: {req.timeline_id}")
chunk_paths = timeline["chunk_paths"]
if not chunk_paths:
raise HTTPException(status_code=400, detail="Timeline has no chunk paths")
# Resolve first chunk to local path for the pipeline
local_path = _resolve_video_path(chunk_paths[0])
# Create job in DB
source_asset_id_str = timeline.get("source_asset_id", "")
with get_session() as session:
from uuid import UUID as _UUID
source_asset_id = _UUID(source_asset_id_str) if source_asset_id_str else uuid.uuid4()
job = create_job(
session,
source_asset_id=source_asset_id,
video_path=chunk_paths[0],
timeline_id=_UUID(req.timeline_id),
profile_name=req.profile_name,
config_overrides=req.config_overrides,
)
job_id = str(job.id)
if req.skip_vlm:
os.environ["SKIP_VLM"] = "1"
@@ -77,7 +105,7 @@ def run_pipeline(req: RunRequest):
elif "SKIP_CLOUD" in os.environ:
del os.environ["SKIP_CLOUD"]
# Clear any stale events from a previous run with same job_id
# Clear any stale events
from core.events import _get_redis
from core.detect.events import DETECT_EVENTS_PREFIX
r = _get_redis()
@@ -94,7 +122,9 @@ def run_pipeline(req: RunRequest):
video_path=local_path,
job_id=job_id,
profile_name=req.profile_name,
source_asset_id=req.source_asset_id,
source_asset_id=source_asset_id_str or str(source_asset_id),
timeline_id=req.timeline_id,
config_overrides=req.config_overrides or {},
)
from core.detect.graph import (
@@ -105,18 +135,29 @@ def run_pipeline(req: RunRequest):
set_cancel_check(job_id, lambda: job_id in _cancelled_jobs)
init_pause(job_id, pause_after_stage=req.pause_after_stage)
def _update_job(status, stage=None, error=None):
from core.db.connection import get_session
from core.db.job import update_job_status
with get_session() as session:
update_job_status(session, _UUID(job_id), status,
current_stage=stage, error_message=error)
def _run():
try:
_update_job("running")
emit.log(job_id, "Pipeline", "INFO",
f"Starting pipeline: {req.video_path} (profile={req.profile_name})")
f"Starting pipeline: {chunk_paths[0]} (profile={req.profile_name})")
pipeline.invoke(initial_state)
_update_job("completed")
emit.log(job_id, "Pipeline", "INFO", "Pipeline completed successfully")
emit.job_complete(job_id, {"status": "completed"})
except PipelineCancelled:
_update_job("cancelled")
emit.log(job_id, "Pipeline", "INFO", "Pipeline cancelled")
emit.job_complete(job_id, {"status": "cancelled"})
except Exception as e:
logger.exception("Pipeline run %s failed: %s", job_id, e)
_update_job("failed", error=str(e))
from core.detect.graph import _node_states, NODES
if job_id in _node_states:
states = _node_states[job_id]
@@ -134,12 +175,14 @@ def run_pipeline(req: RunRequest):
clear_cancel_check(job_id)
clear_pause(job_id)
emit.clear_run_context()
from core.detect.checkpoint.runner_bridge import reset_checkpoint_state
reset_checkpoint_state(job_id)
thread = threading.Thread(target=_run, daemon=True, name=f"pipeline-{job_id}")
_running_jobs[job_id] = thread
thread.start()
return RunResponse(status="started", job_id=job_id, video_path=req.video_path)
return RunResponse(status="started", job_id=job_id, timeline_id=req.timeline_id)
@router.post("/stop/{job_id}")
@@ -224,18 +267,6 @@ def pipeline_status(job_id: str):
return {"status": status, "job_id": job_id}
@router.get("/timeline/{job_id}")
def get_timeline_for_job(job_id: str):
"""Get the timeline_id for a running or completed job."""
from core.detect.checkpoint.runner_bridge import get_timeline_id
tid = get_timeline_id(job_id)
if tid is None:
raise HTTPException(status_code=404, detail=f"No timeline for job: {job_id}")
return {"timeline_id": tid, "job_id": job_id}
@router.post("/clear/{job_id}")
def clear_pipeline(job_id: str):
"""Clear events for a job from Redis."""