104 lines
3.8 KiB
Python
104 lines
3.8 KiB
Python
"""④ FastAPI orchestration — LLM → Higgsfield → Remotion wrapper.
|
|
|
|
POST /api/generate (multipart: photos[] + interview fields) → job_id 즉시 반환
|
|
백그라운드 워커: build_spec (OpenAI) → higgsfield.generate → remotion.render → mp4
|
|
GET /api/jobs/{id} 작업 상태 폴링 (queued | running | done | error)
|
|
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 import config as cfg, jobs
|
|
from app.schemas import GenerateRequest, JobStatus, ScriptResult
|
|
from app.pipeline import spec_builder
|
|
|
|
WEBAPP = cfg.WEBAPP_DIR
|
|
OUTPUTS = cfg.OUTPUTS_DIR
|
|
UPLOADS = cfg.UPLOADS_DIR
|
|
OUTPUTS.mkdir(parents=True, exist_ok=True)
|
|
|
|
app = FastAPI(title="ADO2 Higgsfield Shorts")
|
|
app.add_middleware(
|
|
CORSMiddleware, allow_origins=cfg.CORS_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블록 (OpenAI)."""
|
|
try:
|
|
return spec_builder.build_script(req)
|
|
except Exception as e:
|
|
raise HTTPException(502, f"자막 생성 실패: {e}") from e
|
|
|
|
|
|
@app.post("/api/generate", response_model=JobStatus, status_code=202)
|
|
def generate( # 논블로킹: 사진만 저장하고 작업을 큐에 넣은 뒤 job_id 반환
|
|
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장 이상이 필요합니다.")
|
|
|
|
# 업로드 사진을 디스크에 저장 (워커 스레드가 경로로 읽음).
|
|
# UploadFile 의 스트림은 요청 종료 후 닫히므로 반드시 여기서 영속화한다.
|
|
upload_id = uuid.uuid4().hex[:8]
|
|
job_dir = UPLOADS / upload_id
|
|
job_dir.mkdir(parents=True, exist_ok=True)
|
|
photo_paths: list[Path] = []
|
|
for i, up in enumerate(photos):
|
|
# 원본 파일명에 한글·유니코드 문자가 있으면 fal 업로드 클라이언트가 ASCII 에러를 냄.
|
|
# 확장자만 보존하고 나머지는 순수 숫자로 대체한다.
|
|
suffix = Path(up.filename or "img.jpg").suffix.lower() or ".jpg"
|
|
dest = job_dir / f"{i:02d}{suffix}"
|
|
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)
|
|
|
|
# 파이프라인은 백그라운드 워커에서 실행 → 즉시 queued 상태를 돌려준다.
|
|
job_id = jobs.submit(req, photo_paths, dry_run=dry_run)
|
|
status = jobs.get(job_id)
|
|
return JobStatus(**{k: status[k] for k in JobStatus.model_fields})
|
|
|
|
|
|
@app.get("/api/jobs/{job_id}", response_model=JobStatus)
|
|
def job_status(job_id: str):
|
|
"""작업 진행 상태 폴링. 프론트가 주기적으로 호출."""
|
|
status = jobs.get(job_id)
|
|
if status is None:
|
|
raise HTTPException(404, "작업을 찾을 수 없습니다(만료되었거나 잘못된 ID).")
|
|
return JobStatus(**{k: status[k] for k in JobStatus.model_fields})
|
|
|
|
|
|
# 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")
|