109 lines
3.3 KiB
Python
109 lines
3.3 KiB
Python
"""
|
|
Source browser for detection pipeline.
|
|
|
|
Lists available media sources from blob storage (MinIO).
|
|
|
|
GET /detect/sources — list chunk jobs
|
|
GET /detect/sources/{job_id}/chunks — list chunks for a job
|
|
GET /detect/sources/{job_id}/chunks/{name}/url — presigned preview URL
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/detect", tags=["detect"])
|
|
|
|
|
|
class ChunkInfoResponse(BaseModel):
|
|
filename: str
|
|
key: str
|
|
size_bytes: int
|
|
|
|
|
|
class SourceInfoResponse(BaseModel):
|
|
job_id: str
|
|
source_type: str = "chunk_job"
|
|
chunk_count: int
|
|
total_bytes: int = 0
|
|
|
|
|
|
def _list_sources() -> list[SourceInfoResponse]:
|
|
"""List chunk jobs from blob storage."""
|
|
from core.storage.blob import get_store
|
|
|
|
store = get_store("out")
|
|
try:
|
|
objects = store.list(prefix="chunks/")
|
|
except Exception as e:
|
|
logger.warning("Failed to list blob sources: %s", e)
|
|
return []
|
|
|
|
jobs: dict[str, int] = {}
|
|
job_bytes: dict[str, int] = {}
|
|
for obj in objects:
|
|
rel_key = obj.key.removeprefix(store.prefix)
|
|
parts = rel_key.split("/")
|
|
if len(parts) >= 3 and parts[0] == "chunks":
|
|
job_id = parts[1]
|
|
jobs[job_id] = jobs.get(job_id, 0) + 1
|
|
job_bytes[job_id] = job_bytes.get(job_id, 0) + obj.size_bytes
|
|
|
|
sources = []
|
|
for job_id, count in sorted(jobs.items()):
|
|
source = SourceInfoResponse(
|
|
job_id=job_id,
|
|
source_type="chunk_job",
|
|
chunk_count=count,
|
|
total_bytes=job_bytes.get(job_id, 0),
|
|
)
|
|
sources.append(source)
|
|
return sources
|
|
|
|
|
|
@router.get("/sources", response_model=list[SourceInfoResponse])
|
|
def list_sources():
|
|
"""List available chunk jobs from blob storage."""
|
|
return _list_sources()
|
|
|
|
|
|
@router.get("/sources/{source_job_id}/chunks", response_model=list[ChunkInfoResponse])
|
|
def list_chunks(source_job_id: str):
|
|
"""List chunks for a specific source job."""
|
|
from core.storage.blob import get_store
|
|
|
|
store = get_store("out")
|
|
try:
|
|
objects = store.list(prefix=f"chunks/{source_job_id}/", extensions={".mp4"})
|
|
except Exception as e:
|
|
logger.warning("Failed to list chunks for %s: %s", source_job_id, e)
|
|
raise HTTPException(status_code=503, detail=f"Blob storage unavailable: {e}")
|
|
|
|
if not objects:
|
|
raise HTTPException(status_code=404, detail=f"Source not found: {source_job_id}")
|
|
|
|
chunks = []
|
|
for obj in objects:
|
|
info = ChunkInfoResponse(filename=obj.filename, key=obj.key, size_bytes=obj.size_bytes)
|
|
chunks.append(info)
|
|
return sorted(chunks, key=lambda c: c.filename)
|
|
|
|
|
|
@router.get("/sources/{source_job_id}/chunks/{filename}/url")
|
|
def get_chunk_url(source_job_id: str, filename: str):
|
|
"""Return a presigned URL for previewing a chunk in the browser."""
|
|
from core.storage.blob import get_store
|
|
|
|
store = get_store("out")
|
|
key = f"chunks/{source_job_id}/{filename}"
|
|
try:
|
|
url = store.get_url(key, expires=3600)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=503, detail=f"Could not generate URL: {e}")
|
|
return {"url": url}
|