"""④ 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/ (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")