Add video generation API with job tracking and model loading
This commit is contained in:
+38
-16
@@ -1,19 +1,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
||||
from pydantic import ValidationError
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from app.config import settings
|
||||
from app.models import JobSubmitResponse, JobStatusResponse
|
||||
from app.models import GenerateForm, JobSubmitResponse, JobStatusResponse
|
||||
from app.service import get_service
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
JOB_ID_RE = re.compile(r"[a-f0-9]{12}")
|
||||
MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator:
|
||||
@@ -44,7 +49,7 @@ async def health() -> JSONResponse:
|
||||
|
||||
@app.post("/generate", status_code=201)
|
||||
async def generate(
|
||||
image: UploadFile = File(..., description="Input image (PNG, JPG, etc.)"),
|
||||
image: UploadFile = File(...),
|
||||
prompt: str | None = Form(None),
|
||||
width: int = Form(768),
|
||||
height: int = Form(512),
|
||||
@@ -54,10 +59,25 @@ async def generate(
|
||||
) -> JobSubmitResponse:
|
||||
from PIL import Image
|
||||
|
||||
img_bytes = await image.read()
|
||||
pil_image = Image.open(img_bytes).convert("RGB")
|
||||
if image.content_length and image.content_length > MAX_IMAGE_SIZE:
|
||||
raise HTTPException(status_code=400, detail="Image file must be under 10MB")
|
||||
|
||||
validate_frame_count(num_frames)
|
||||
try:
|
||||
GenerateForm(
|
||||
prompt=prompt,
|
||||
width=width,
|
||||
height=height,
|
||||
num_frames=num_frames,
|
||||
fps=fps,
|
||||
seed=seed,
|
||||
)
|
||||
except ValidationError as e:
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
|
||||
img_bytes = await image.read()
|
||||
if len(img_bytes) > MAX_IMAGE_SIZE:
|
||||
raise HTTPException(status_code=400, detail="Image file must be under 10MB")
|
||||
pil_image = Image.open(img_bytes).convert("RGB")
|
||||
|
||||
svc = await get_service()
|
||||
job = await svc.submit_job(pil_image, {
|
||||
@@ -71,8 +91,18 @@ async def generate(
|
||||
return JobSubmitResponse(job_id=job.id)
|
||||
|
||||
|
||||
def _resolve_job_id(job_id: str) -> str:
|
||||
if not JOB_ID_RE.fullmatch(job_id):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid job_id format. Expected 12 lowercase hex characters.",
|
||||
)
|
||||
return job_id
|
||||
|
||||
|
||||
@app.get("/jobs/{job_id}")
|
||||
async def get_job(job_id: str) -> JobStatusResponse:
|
||||
_resolve_job_id(job_id)
|
||||
svc = await get_service()
|
||||
job = await svc.get_job(job_id)
|
||||
if job is None:
|
||||
@@ -90,12 +120,13 @@ async def get_job(job_id: str) -> JobStatusResponse:
|
||||
|
||||
@app.get("/jobs/{job_id}/download")
|
||||
async def download_video(job_id: str) -> FileResponse:
|
||||
_resolve_job_id(job_id)
|
||||
svc = await get_service()
|
||||
job = await svc.get_job(job_id)
|
||||
if job is None:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
if job.status != "completed":
|
||||
raise HTTPException(status_code=404, detail="Video not ready")
|
||||
raise HTTPException(status_code=503, detail="Video not ready")
|
||||
|
||||
video_path = await svc.get_video_path(job_id)
|
||||
if video_path is None:
|
||||
@@ -105,6 +136,7 @@ async def download_video(job_id: str) -> FileResponse:
|
||||
|
||||
@app.delete("/jobs/{job_id}")
|
||||
async def delete_job(job_id: str) -> dict:
|
||||
_resolve_job_id(job_id)
|
||||
svc = await get_service()
|
||||
job = await svc.get_job(job_id)
|
||||
if job is None:
|
||||
@@ -115,16 +147,6 @@ async def delete_job(job_id: str) -> dict:
|
||||
return {"deleted": job_id}
|
||||
|
||||
|
||||
def validate_frame_count(num_frames: int) -> None:
|
||||
valid = {8 * n + 1 for n in range(1, 129)}
|
||||
if num_frames not in valid:
|
||||
candidates = sorted(f for f in valid if abs(f - num_frames) <= 8)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"num_frames must be 8n+1. Got {num_frames}. Closest valid: {candidates[:4]}",
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
|
||||
Reference in New Issue
Block a user