Files
web-page-backend/video-lab/app/main.py
gahusb c9a094969d feat(video-lab): main.py — FastAPI + redis client + 2 endpoint (SP-8)
POST /api/video/generate (provider validation + Redis push + task_id 반환).
GET /api/video/tasks/{id} (DB 조회).
GET /api/video/providers (4 provider 메타).
SUPPORTED_PROVIDERS = sora/veo/kling/seedance.
Plan-B-Video Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:30:21 +09:00

118 lines
3.9 KiB
Python

"""FastAPI entrypoint for video-lab.
POST /api/video/generate — provider + params → Redis push → task_id 반환
GET /api/video/tasks/{id} — DB 조회
"""
from __future__ import annotations
import json
import logging
import os
import uuid
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
import redis.asyncio as aioredis
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from . import db
from .internal_router import router as internal_router
logger = logging.getLogger(__name__)
CORS_ALLOW_ORIGINS = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080")
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379")
redis_client = aioredis.from_url(REDIS_URL, decode_responses=False)
SUPPORTED_PROVIDERS = {"sora", "veo", "kling", "seedance"}
app = FastAPI()
app.include_router(internal_router)
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in CORS_ALLOW_ORIGINS.split(",")],
allow_credentials=False,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["Content-Type"],
)
@app.on_event("startup")
def on_startup():
db.init_db()
@app.get("/health")
def health():
return {"ok": True, "service": "video-lab"}
@app.get("/api/video/providers")
def list_providers():
"""4 provider 항상 노출 (key 누락은 worker가 failed 보고)."""
return {"providers": [
{"id": "sora", "name": "Sora 2", "models": ["sora-2", "sora-2-pro"],
"durations": [8, 16, 20], "sizes": ["1280x720", "1920x1080", "1080x1920", "848x480", "480x848"]},
{"id": "veo", "name": "Veo 3.1", "models": ["veo-3.1-generate-001", "veo-3.1-fast-generate-001"],
"durations": [4, 6, 8], "aspect_ratios": ["16:9", "9:16"]},
{"id": "kling", "name": "Kling", "models": ["1.5", "1.6", "2.1", "2.1-master", "2.5", "2.6"],
"durations": [5, 10], "aspect_ratios": ["16:9", "9:16", "1:1"]},
{"id": "seedance", "name": "Seedance 2.0", "models": ["seedance-2.0"],
"durations": [4, 5, 6, 8, 10, 12, 15], "aspect_ratios": ["16:9", "9:16", "1:1", "4:3"]},
]}
class GenerateRequest(BaseModel):
provider: str = Field(..., description="sora|veo|kling|seedance")
model: Optional[str] = None
prompt: str
# Optional 공통
duration: Optional[int] = None
aspect_ratio: Optional[str] = None
image_url: Optional[str] = None
negative_prompt: Optional[str] = None
# Provider 별 추가 키는 extra 허용
extra: Optional[Dict[str, Any]] = None
class Config:
extra = "allow"
async def _push_render_job(task_id: str, job_type: str, params: dict) -> None:
"""Redis queue:video-render에 push."""
kst = timezone(timedelta(hours=9))
payload = {
"task_id": task_id,
"kind": "video",
"job_type": job_type,
"params": params,
"submitted_at": datetime.now(kst).isoformat(),
}
await redis_client.rpush("queue:video-render", json.dumps(payload))
@app.post("/api/video/generate")
async def generate_video(req: GenerateRequest):
"""영상 생성 — Redis 큐로 Windows video-render에 위임."""
if req.provider not in SUPPORTED_PROVIDERS:
raise HTTPException(400, f"지원하지 않는 provider: {req.provider} (supported: {sorted(SUPPORTED_PROVIDERS)})")
task_id = str(uuid.uuid4())
params = req.model_dump(exclude_none=True)
db.create_task(task_id, req.provider, params)
job_type = f"{req.provider}_generation" # sora_generation, veo_generation, kling_generation, seedance_generation
await _push_render_job(task_id, job_type, params)
return {"task_id": task_id, "provider": req.provider}
@app.get("/api/video/tasks/{task_id}")
def get_task_status(task_id: str):
t = db.get_task(task_id)
if not t:
raise HTTPException(404, "task not found")
return t