Files
agentcut/backend/main.py
T
calderbuild 8c95f3da99 feat: complete AgentCut multi-agent video production pipeline
6 specialized AI agents collaborate through a structured SOP pipeline:
Director (planning) -> Script (writing) -> Visual+Voice+Music (parallel generation) -> Editor (ffmpeg compositing)

Backend: FastAPI with SSE streaming, MiniMax API integration (Hailuo video, Speech-2.6-HD TTS, Music-2.0, M1 LLM)
Frontend: single-page app with real-time pipeline visualization
2026-02-19 15:50:17 +08:00

167 lines
4.9 KiB
Python

"""FastAPI server for AgentCut - AI Director Video Production Pipeline."""
import os
import json
import uuid
import asyncio
from typing import AsyncGenerator
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from backend.pipeline import run_pipeline, PipelineEvent
from backend.config import OUTPUT_DIR
app = FastAPI(title="AgentCut", description="AI Director - Multi-Agent Video Production")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# In-memory job storage
jobs: dict = {}
class CreateVideoRequest(BaseModel):
prompt: str
duration: int = 30
num_shots: int = 3
include_music: bool = True
class JobStatus(BaseModel):
job_id: str
status: str # "pending", "running", "completed", "failed"
progress: float
events: list
output_path: str = None
error: str = None
@app.post("/api/create")
async def create_video(req: CreateVideoRequest):
"""Start a new video production job."""
job_id = str(uuid.uuid4())[:8]
jobs[job_id] = {
"status": "pending",
"progress": 0.0,
"events": [],
"output_path": None,
"error": None,
}
# Run pipeline in background
asyncio.get_event_loop().run_in_executor(
None, _run_job, job_id, req.prompt, req.duration, req.num_shots, req.include_music,
)
return {"job_id": job_id}
def _run_job(job_id: str, prompt: str, duration: int, num_shots: int, include_music: bool):
"""Execute the pipeline in a background thread."""
jobs[job_id]["status"] = "running"
def on_event(event: PipelineEvent):
jobs[job_id]["progress"] = event.progress
jobs[job_id]["events"].append({
"stage": event.stage,
"message": event.message,
"progress": event.progress,
"data": event.data,
"timestamp": event.timestamp,
})
try:
result = run_pipeline(
prompt, duration, num_shots, include_music, on_event=on_event,
)
jobs[job_id]["status"] = "completed"
jobs[job_id]["progress"] = 1.0
jobs[job_id]["output_path"] = result["output_path"]
except Exception as e:
jobs[job_id]["status"] = "failed"
jobs[job_id]["error"] = str(e)
@app.get("/api/status/{job_id}")
async def get_status(job_id: str):
"""Get current job status and progress."""
if job_id not in jobs:
raise HTTPException(status_code=404, detail="Job not found")
return jobs[job_id]
@app.get("/api/stream/{job_id}")
async def stream_events(job_id: str):
"""SSE stream for real-time progress updates."""
if job_id not in jobs:
raise HTTPException(status_code=404, detail="Job not found")
async def event_generator() -> AsyncGenerator[str, None]:
last_idx = 0
while True:
job = jobs[job_id]
events = job["events"]
# Send new events
while last_idx < len(events):
event = events[last_idx]
yield f"data: {json.dumps(event)}\n\n"
last_idx += 1
if job["status"] in ("completed", "failed"):
final = {
"stage": "done",
"message": "Pipeline finished" if job["status"] == "completed" else f"Error: {job['error']}",
"progress": job["progress"],
"data": {"status": job["status"], "output_path": job.get("output_path")},
}
yield f"data: {json.dumps(final)}\n\n"
break
await asyncio.sleep(1)
return StreamingResponse(
event_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
)
@app.get("/api/download/{job_id}")
async def download_video(job_id: str):
"""Download the final video."""
if job_id not in jobs:
raise HTTPException(status_code=404, detail="Job not found")
job = jobs[job_id]
if job["status"] != "completed" or not job["output_path"]:
raise HTTPException(status_code=400, detail="Video not ready")
return FileResponse(
job["output_path"],
media_type="video/mp4",
filename="agentcut_output.mp4",
)
@app.get("/api/health")
async def health():
return {"status": "ok", "service": "AgentCut"}
# Serve frontend static files (must be last - catches all unmatched routes)
_frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend")
if os.path.isdir(_frontend_dir):
app.mount("/", StaticFiles(directory=_frontend_dir, html=True), name="frontend")
if __name__ == "__main__":
import uvicorn
uvicorn.run("backend.main:app", host="0.0.0.0", port=8000, reload=True)