116 lines
3.6 KiB
Python
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")
|