o2o-ado2-short-form/server/app/main.py

116 lines
3.6 KiB
Python

"""④ FastAPI orchestration — LLM → Higgsfield → Remotion wrapper.
POST /api/generate (multipart: photos[] + interview fields)
→ build_spec (Claude) → higgsfield.generate → remotion.render → mp4
GET /api/health
Static: /outputs/<file> (final videos), / (the web app)
"""
from __future__ import annotations
import shutil
import uuid
from pathlib import Path
from fastapi import FastAPI, File, Form, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from app.schemas import GenerateRequest, GenerateResult, ScriptResult
from app.pipeline import spec_builder, higgsfield_client, remotion_render
ENGINE = Path(__file__).resolve().parents[2] # engine/higgsfield_shorts
WEBAPP = ENGINE / "webapp"
OUTPUTS = ENGINE / "server" / "outputs"
UPLOADS = ENGINE / "server" / ".uploads"
OUTPUTS.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="ADO2 Higgsfield Shorts")
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)
@app.get("/api/health")
def health():
return {"ok": True}
@app.post("/api/captions", response_model=ScriptResult)
def captions(req: GenerateRequest):
"""인터뷰 답변 → 인트로·셀링포인트·감성스토리·CTA 4블록 (Claude)."""
try:
return spec_builder.build_script(req)
except Exception as e:
raise HTTPException(502, f"자막 생성 실패: {e}") from e
@app.post("/api/generate", response_model=GenerateResult)
async def generate(
photos: list[UploadFile] = File(...),
kind: str = Form(...),
biz_name: str = Form(...),
selling: str = Form(...),
addr: str | None = Form(None),
price: str | None = Form(None),
dry_run: bool = Form(False),
):
if len(photos) < 4:
raise HTTPException(400, "사진 4장 이상이 필요합니다.")
# 1. Persist uploaded photos
job = uuid.uuid4().hex[:8]
job_dir = UPLOADS / job
job_dir.mkdir(parents=True, exist_ok=True)
photo_paths: list[Path] = []
for i, up in enumerate(photos):
dest = job_dir / f"{i:02d}_{Path(up.filename or 'img').name}"
with dest.open("wb") as f:
shutil.copyfileobj(up.file, f)
photo_paths.append(dest)
req = GenerateRequest(kind=kind, biz_name=biz_name, addr=addr, price=price, selling=selling)
# 2. LLM → spec
try:
spec = spec_builder.build_spec(req)
except Exception as e: # surface as a clean 502, don't swallow
raise HTTPException(502, f"spec 생성 실패: {e}") from e
# 3. Higgsfield → base video
try:
base_video, credits = higgsfield_client.generate(
spec.higgsfield_prompt, photo_paths, dry_run=dry_run
)
except Exception as e:
raise HTTPException(502, f"Higgsfield 생성 실패: {e}") from e
# 4. Remotion → final with overlays
try:
final = remotion_render.render(spec, base_video)
except Exception as e:
raise HTTPException(502, f"Remotion 합성 실패: {e}") from e
served = OUTPUTS / f"{job}.mp4"
shutil.copy(final, served)
return GenerateResult(
video_url=f"/outputs/{job}.mp4",
caption=spec.caption,
profile=spec.profile,
cost_credits=credits,
job_id=job,
)
# Static mounts (after API routes)
app.mount("/outputs", StaticFiles(directory=str(OUTPUTS)), name="outputs")
@app.get("/")
def index():
return FileResponse(str(WEBAPP / "index.html"))
app.mount("/", StaticFiles(directory=str(WEBAPP)), name="webapp")