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"]}

这段代码在本地跑没问题。但部署到生产环境后,你会遇到:

  1. 网关超时:Nginx 默认的 proxy_read_timeout 是 60 秒,但如果加上冷启动,一次请求可能要 90 秒。用户看到的就是 502 Bad Gateway。
  2. 连接池耗尽:你的 Web 框架(Flask/FastAPI)每个 worker 都在阻塞等待 fal 返回,10 个并发请求就把 worker 全占满了,后面的请求全部排队。
  3. 前端超时:浏览器 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 审核),减少触发安全检查器的概率。触发一次安全检查器虽然不收费,但浪费了用户的等待时间。

上线前的检查清单

  1. 环境变量:API Key 不在代码里,通过环境变量注入
  2. 超时配置:Nginx/网关的超时设为 120 秒以上,或者用异步方案
  3. 错误监控:接入 Sentry 或类似工具,捕获 fal 返回的所有异常
  4. 限流:客户端限流(每用户每分钟 N 次)+ 全局限流(总并发不超过 M)
  5. 预算警报:在 fal.ai Dashboard 上设置每日/每月消费上限
  6. 视频存储:fal 返回的 video URL 只保留 24 小时,必须下载到自己的存储(S3/OSS)
  7. 进度反馈:前端必须有加载动画和预估时间提示,别让用户干等着
  8. 安全过滤:Prompt 预审 + Seedance 安全检查器双重保障

总结

视频生成 API 的部署和普通 REST API 差距很大。30-40 秒的响应时间意味着你不能用同步模式,必须上异步。24 小时的文件过期意味着你必须及时下载存储。不稳定的成功率意味着重试逻辑不是可选的,是必须的。

先把架构搭对,再去优化 Prompt。架构错了,Prompt 再好也没用。

posted @ 2026-02-09 16:30  147API  阅读(3)  评论(0)    收藏  举报