Seedance 生产部署:异步队列、Webhook 与错误处理
视频生成的 API 调用和普通的文本 API 完全不同。一次调用 30-45 秒才返回结果,你的架构必须为此做出调整。
为什么不能用同步调用?
先看一个反面案例。假设你的后端是这样写的:
@app.post("/generate")
def generate_video(request):
result = fal_client.subscribe(
"fal-ai/bytedance/seedance/v1.5/pro/text-to-video",
arguments={"prompt": request.prompt, "duration": "5", "resolution": "720p"}
)
return {"video_url": result["video"]["url"]}
这段代码在本地跑没问题。但部署到生产环境后,你会遇到:
- 网关超时:Nginx 默认的
proxy_read_timeout是 60 秒,但如果加上冷启动,一次请求可能要 90 秒。用户看到的就是 502 Bad Gateway。 - 连接池耗尽:你的 Web 框架(Flask/FastAPI)每个 worker 都在阻塞等待 fal 返回,10 个并发请求就把 worker 全占满了,后面的请求全部排队。
- 前端超时:浏览器 fetch 请求通常 30 秒超时。用户点了按钮,等了 30 秒什么都没发生,关掉页面走了。
正确的异步架构
生产环境里应该用"提交-轮询"或"提交-回调"模式。
方案一:提交 + 前端轮询
用户点击 → 后端 submit → 立刻返回 task_id → 前端每 3 秒拿 task_id 查状态 → 完成后拿到 video_url
后端代码:
import fal_client
# 提交任务(非阻塞,立刻返回)
@app.post("/generate")
async def submit_task(request):
handle = fal_client.submit(
"fal-ai/bytedance/seedance/v1.5/pro/text-to-video",
arguments={
"prompt": request.prompt,
"duration": "5",
"resolution": "720p",
"generate_audio": True
}
)
# 把 request_id 存到数据库或 Redis
save_task(handle.request_id, status="processing")
return {"task_id": handle.request_id}
# 查询状态
@app.get("/status/{task_id}")
async def check_status(task_id: str):
task = get_task(task_id)
if task.status == "completed":
return {"status": "completed", "video_url": task.video_url}
elif task.status == "failed":
return {"status": "failed", "error": task.error_message}
else:
return {"status": "processing"}
前端代码(伪代码):
async function waitForVideo(taskId) {
while (true) {
const res = await fetch(`/status/${taskId}`);
const data = await res.json();
if (data.status === "completed") {
showVideo(data.video_url);
return;
}
if (data.status === "failed") {
showError(data.error);
return;
}
updateProgress("生成中...");
await sleep(3000); // 每 3 秒查一次
}
}
方案二:提交 + Webhook 回调
如果你不想让前端轮询,可以配置 Webhook。fal 在视频生成完成后,会主动 POST 到你指定的 URL。
handle = fal_client.submit(
"fal-ai/bytedance/seedance/v1.5/pro/text-to-video",
arguments={...},
webhook_url="https://your-server.com/webhook/fal"
)
然后在后端实现 Webhook 接收:
@app.post("/webhook/fal")
async def fal_webhook(request):
payload = await request.json()
task_id = payload["request_id"]
if payload.get("error"):
update_task(task_id, status="failed", error=str(payload["error"]))
else:
video_url = payload["data"]["video"]["url"]
update_task(task_id, status="completed", video_url=video_url)
# 通过 WebSocket 或 SSE 通知前端
notify_client(task_id)
return {"ok": True}
Webhook 方案更优雅,但要求你的服务器有公网可访问的 URL(本地开发时需要用 ngrok 之类的工具)。
错误处理:必须写重试逻辑
视频生成的失败率比文本 API 高得多。网络抖动、GPU 繁忙、Prompt 触发安全过滤,都可能导致失败。
一个最基本的重试逻辑:
import time
def generate_with_retry(prompt, max_retries=3):
for attempt in range(max_retries):
try:
result = fal_client.subscribe(
"fal-ai/bytedance/seedance/v1.5/pro/text-to-video",
arguments={
"prompt": prompt,
"duration": "5",
"resolution": "720p"
}
)
return result
except fal_client.exceptions.RateLimitError:
# 被限流了,等一会儿再试
wait_time = 2 ** attempt # 指数退避:1s, 2s, 4s
print(f"被限流,{wait_time}秒后重试...")
time.sleep(wait_time)
except fal_client.exceptions.ValidationError as e:
# Prompt 有问题,重试也没用
print(f"输入校验失败: {e}")
raise
except Exception as e:
# 未知错误,记录日志后重试
print(f"第{attempt+1}次尝试失败: {e}")
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
raise Exception("超过最大重试次数")
注意区分可重试错误和不可重试错误。RateLimitError(被限流)等一等就好了。ValidationError(输入有问题)重试一百次也是错。
安全检查器:别忽略它
Seedance 有个 enable_safety_checker 参数,默认是 true。
如果你的 Prompt 触发了安全过滤,API 不会返回视频,而是返回一个错误。但这个错误不是标准的 HTTP 4xx/5xx,而是在返回的 JSON 里以特定字段标识的。
建议:不要关掉安全检查器,除非你非常清楚自己在做什么。一是法律风险,二是如果你的产品允许用户自定义 Prompt,关掉安全检查器等于给自己埋了一个随时会爆的雷。
在产品层面,应该在用户提交 Prompt 之前做一次文本层面的预过滤(关键词黑名单 + LLM 审核),减少触发安全检查器的概率。触发一次安全检查器虽然不收费,但浪费了用户的等待时间。
上线前的检查清单
- 环境变量:API Key 不在代码里,通过环境变量注入
- 超时配置:Nginx/网关的超时设为 120 秒以上,或者用异步方案
- 错误监控:接入 Sentry 或类似工具,捕获 fal 返回的所有异常
- 限流:客户端限流(每用户每分钟 N 次)+ 全局限流(总并发不超过 M)
- 预算警报:在 fal.ai Dashboard 上设置每日/每月消费上限
- 视频存储:fal 返回的 video URL 只保留 24 小时,必须下载到自己的存储(S3/OSS)
- 进度反馈:前端必须有加载动画和预估时间提示,别让用户干等着
- 安全过滤:Prompt 预审 + Seedance 安全检查器双重保障
总结
视频生成 API 的部署和普通 REST API 差距很大。30-40 秒的响应时间意味着你不能用同步模式,必须上异步。24 小时的文件过期意味着你必须及时下载存储。不稳定的成功率意味着重试逻辑不是可选的,是必须的。
先把架构搭对,再去优化 Prompt。架构错了,Prompt 再好也没用。
浙公网安备 33010602011771号