Files
Project_Velocity/backend/routers/scenes.py
2026-04-12 02:02:58 +05:30

103 lines
3.4 KiB
Python

"""
backend/routers/scenes.py - Video scene map ingestion.
"""
from __future__ import annotations
import csv
import io
import asyncpg
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from backend.auth.dependencies import UserPrincipal, require_role
from backend.db.pool import get_pool
router = APIRouter()
@router.post("/upload", summary="Upload a scene CSV for a marketing video")
async def upload_scene_map(
video_asset_id: str,
file: UploadFile = File(...),
pool: asyncpg.Pool = Depends(get_pool),
user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
) -> dict[str, int | str]:
del user
if not file.filename or not file.filename.lower().endswith(".csv"):
raise HTTPException(status_code=400, detail="Scene upload must be a CSV file.")
raw_bytes = await file.read()
try:
text = raw_bytes.decode("utf-8-sig")
except UnicodeDecodeError as exc:
raise HTTPException(status_code=400, detail="CSV must be UTF-8 encoded.") from exc
reader = csv.DictReader(io.StringIO(text))
required = {"scene_no", "start_ms", "end_ms", "room_type"}
if not reader.fieldnames or not required.issubset(set(reader.fieldnames)):
raise HTTPException(
status_code=400,
detail="CSV must contain scene_no,start_ms,end_ms,room_type columns.",
)
rows: list[tuple[str, int, int, int, str, str | None]] = []
for row in reader:
try:
rows.append(
(
video_asset_id,
int(row["scene_no"]),
int(row["start_ms"]),
int(row["end_ms"]),
row["room_type"].strip(),
(row.get("description") or "").strip() or None,
)
)
except (TypeError, ValueError, KeyError) as exc:
raise HTTPException(status_code=400, detail=f"Invalid scene row: {row}") from exc
if not rows:
raise HTTPException(status_code=400, detail="CSV contains no scene rows.")
async with pool.acquire() as conn:
async with conn.transaction():
await conn.execute(
"DELETE FROM video_scene_maps WHERE video_asset_id = $1",
video_asset_id,
)
await conn.executemany(
"""
INSERT INTO video_scene_maps
(video_asset_id, scene_no, start_ms, end_ms, room_type, description)
VALUES ($1, $2, $3, $4, $5, $6)
""",
rows,
)
return {"status": "uploaded", "video_asset_id": video_asset_id, "row_count": len(rows)}
@router.get("/{video_asset_id}", summary="List the uploaded scene map for a marketing video")
async def get_scene_map(
video_asset_id: str,
pool: asyncpg.Pool = Depends(get_pool),
user: UserPrincipal = Depends(require_role("SENIOR_BROKER")),
) -> dict[str, object]:
del user
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT scene_no, start_ms, end_ms, room_type, description
FROM video_scene_maps
WHERE video_asset_id = $1
ORDER BY scene_no ASC
""",
video_asset_id,
)
return {
"video_asset_id": video_asset_id,
"row_count": len(rows),
"scenes": [dict(row) for row in rows],
}