227 lines
6.6 KiB
Python
227 lines
6.6 KiB
Python
"""
|
|
Timeline + Job management endpoints.
|
|
|
|
POST /detect/timeline — create timeline from chunk selection
|
|
GET /detect/timeline — list timelines
|
|
GET /detect/timeline/{id} — timeline detail
|
|
DELETE /detect/timeline/{id}/cache — clear frame cache
|
|
|
|
GET /detect/jobs — list jobs (optionally by timeline)
|
|
GET /detect/jobs/{id} — job detail + checkpoints + stage outputs
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/detect", tags=["detect"])
|
|
|
|
|
|
# --- Request/Response models ---
|
|
|
|
class CreateTimelineRequest(BaseModel):
|
|
chunk_paths: list[str]
|
|
profile_name: str = "soccer_broadcast"
|
|
name: str = ""
|
|
source_asset_id: str = ""
|
|
fps: float = 2.0
|
|
|
|
|
|
class TimelineResponse(BaseModel):
|
|
id: str
|
|
name: str
|
|
chunk_paths: list[str]
|
|
profile_name: str
|
|
status: str
|
|
fps: float
|
|
frame_count: int
|
|
source_ephemeral: bool
|
|
created_at: str | None = None
|
|
|
|
|
|
class JobResponse(BaseModel):
|
|
id: str
|
|
timeline_id: str | None
|
|
source_asset_id: str
|
|
video_path: str
|
|
profile_name: str
|
|
run_type: str
|
|
status: str
|
|
current_stage: str | None
|
|
config_overrides: dict
|
|
error_message: str | None
|
|
created_at: str | None
|
|
started_at: str | None
|
|
completed_at: str | None
|
|
|
|
|
|
class JobDetailResponse(JobResponse):
|
|
checkpoints: list[dict]
|
|
stage_outputs: dict[str, dict]
|
|
|
|
|
|
# --- Timeline endpoints ---
|
|
|
|
@router.post("/timeline", response_model=TimelineResponse)
|
|
def create_timeline_endpoint(req: CreateTimelineRequest):
|
|
"""Create a timeline from a chunk selection."""
|
|
from uuid import UUID
|
|
from core.detect.checkpoint.storage import create_timeline
|
|
|
|
source_asset_id = UUID(req.source_asset_id) if req.source_asset_id else None
|
|
tid = create_timeline(
|
|
chunk_paths=req.chunk_paths,
|
|
profile_name=req.profile_name,
|
|
name=req.name,
|
|
source_asset_id=source_asset_id,
|
|
fps=req.fps,
|
|
)
|
|
|
|
from core.detect.checkpoint.storage import get_timeline
|
|
tl = get_timeline(tid)
|
|
return TimelineResponse(
|
|
id=tl["id"],
|
|
name=tl["name"],
|
|
chunk_paths=tl["chunk_paths"],
|
|
profile_name=tl["profile_name"],
|
|
status=tl["status"],
|
|
fps=tl["fps"],
|
|
frame_count=0,
|
|
source_ephemeral=False,
|
|
created_at=tl["created_at"],
|
|
)
|
|
|
|
|
|
@router.get("/timeline", response_model=list[TimelineResponse])
|
|
def list_timelines():
|
|
"""List all timelines."""
|
|
from sqlmodel import select
|
|
from core.db.models import Timeline
|
|
from core.db.connection import get_session
|
|
|
|
with get_session() as session:
|
|
stmt = select(Timeline).order_by(Timeline.created_at.desc())
|
|
timelines = session.exec(stmt).all()
|
|
|
|
return [
|
|
TimelineResponse(
|
|
id=str(t.id),
|
|
name=t.name,
|
|
chunk_paths=t.chunk_paths or [],
|
|
profile_name=t.profile_name,
|
|
status=t.status,
|
|
fps=t.fps,
|
|
frame_count=t.frame_count,
|
|
source_ephemeral=t.source_ephemeral,
|
|
created_at=str(t.created_at) if t.created_at else None,
|
|
)
|
|
for t in timelines
|
|
]
|
|
|
|
|
|
@router.get("/timeline/{timeline_id}", response_model=TimelineResponse)
|
|
def get_timeline_endpoint(timeline_id: str):
|
|
"""Get timeline detail."""
|
|
from core.detect.checkpoint.storage import get_timeline
|
|
try:
|
|
tl = get_timeline(timeline_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=404, detail=f"Timeline not found: {timeline_id}")
|
|
|
|
from core.detect.checkpoint.frames import cache_exists
|
|
from uuid import UUID
|
|
from core.db.models import Timeline
|
|
from core.db.connection import get_session
|
|
|
|
with get_session() as session:
|
|
timeline = session.get(Timeline, UUID(timeline_id))
|
|
|
|
return TimelineResponse(
|
|
id=tl["id"],
|
|
name=tl["name"],
|
|
chunk_paths=tl["chunk_paths"],
|
|
profile_name=tl["profile_name"],
|
|
status=tl["status"],
|
|
fps=tl["fps"],
|
|
frame_count=timeline.frame_count if timeline else 0,
|
|
source_ephemeral=timeline.source_ephemeral if timeline else False,
|
|
created_at=tl["created_at"],
|
|
)
|
|
|
|
|
|
@router.delete("/timeline/{timeline_id}/cache")
|
|
def clear_timeline_cache(timeline_id: str):
|
|
"""Clear the frame cache for a timeline."""
|
|
from core.detect.checkpoint.frames import clear_cache
|
|
from core.detect.checkpoint.storage import update_timeline_status
|
|
|
|
clear_cache(timeline_id)
|
|
update_timeline_status(timeline_id, "created")
|
|
return {"status": "cleared", "timeline_id": timeline_id}
|
|
|
|
|
|
# --- Job endpoints ---
|
|
|
|
def _job_to_response(job) -> JobResponse:
|
|
return JobResponse(
|
|
id=str(job.id),
|
|
timeline_id=str(job.timeline_id) if job.timeline_id else None,
|
|
source_asset_id=str(job.source_asset_id),
|
|
video_path=job.video_path,
|
|
profile_name=job.profile_name,
|
|
run_type=job.run_type,
|
|
status=job.status,
|
|
current_stage=job.current_stage,
|
|
config_overrides=job.config_overrides or {},
|
|
error_message=job.error_message,
|
|
created_at=str(job.created_at) if job.created_at else None,
|
|
started_at=str(job.started_at) if job.started_at else None,
|
|
completed_at=str(job.completed_at) if job.completed_at else None,
|
|
)
|
|
|
|
|
|
@router.get("/jobs", response_model=list[JobResponse])
|
|
def list_jobs_endpoint(timeline_id: str | None = Query(None)):
|
|
"""List jobs, optionally filtered by timeline."""
|
|
from uuid import UUID
|
|
from core.db.connection import get_session
|
|
from core.db.job import list_jobs
|
|
|
|
tid = UUID(timeline_id) if timeline_id else None
|
|
with get_session() as session:
|
|
jobs = list_jobs(session, timeline_id=tid)
|
|
|
|
return [_job_to_response(j) for j in jobs]
|
|
|
|
|
|
@router.get("/jobs/{job_id}", response_model=JobDetailResponse)
|
|
def get_job_endpoint(job_id: str):
|
|
"""Get job detail with checkpoints and stage outputs."""
|
|
from uuid import UUID
|
|
from core.db.connection import get_session
|
|
from core.db.job import get_job
|
|
from core.detect.checkpoint.storage import (
|
|
get_checkpoints_for_job,
|
|
load_stage_outputs_for_job,
|
|
)
|
|
|
|
with get_session() as session:
|
|
job = get_job(session, UUID(job_id))
|
|
if not job:
|
|
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
|
|
|
|
checkpoints = get_checkpoints_for_job(job_id)
|
|
stage_outputs = load_stage_outputs_for_job(job_id)
|
|
|
|
base = _job_to_response(job)
|
|
return JobDetailResponse(
|
|
**base.model_dump(),
|
|
checkpoints=checkpoints,
|
|
stage_outputs=stage_outputs,
|
|
)
|