# 会写分镜还会出片的 AI Agent:基于任务轮询的多步视频工作流 "一句话生成视频"演示起来很好看,但真要做一条 30 秒、有起承转合的短片,单次调用通常不够。比较可行的做法是:让 LLM 先写分镜,再把每个镜头拆成独立的视频生成任务,并发提交、轮询收结果,最后拼接成片。 本文用 Python 实现一个这样的 Agent。底层视频接口使用 [RelayDance](https://relaydance.com),它以 OpenAI 兼容协议提供 ByteDance Seedance 2.0 / 1.5 Pro / 2.5、HappyHorse、Grok Imagine 等视频与图像模型,以及 Grok 文本模型。同一个 API Key 可以完成"写分镜 → 出定妆图 → 出镜头"的全部步骤。 --- ## 一、整体架构 用户需求 │ ▼ [1] Planner(文本模型)──► 结构化分镜 JSON(N 个镜头) │ ▼ [2] 定妆图(gpt-image-2)──► 角色/场景参考图,保证跨镜头一致 │ ▼ [3] 并发提交视频任务 POST /v1/video/generations │ ▼ [4] 轮询 GET /v1/video/generations/{task_id}(或 Webhook 回调) │ └─ failed → 改写 prompt 后重试 ▼ [5] 下载各镜头 → ffmpeg 拼接 → 成片 各步骤的设计考虑如下: - **视频生成是异步长任务**。一个镜头要跑几十秒到几分钟,HTTP 同步等待不现实,因此接口采用"提交任务 → 拿 task_id → 查状态"的模式。 - **镜头之间相互独立**,可以并发提交,总耗时约等于最慢那个镜头的耗时。 - **失败重试的成本低**。据 relaydance.com 官方文档,「失败或报错的请求一律不计费」,所以 Agent 可以对失败镜头自动改写 prompt 再试,不用担心为废片付费。 --- ## 二、接入:只改 base_url 据 relaydance.com/docs 官方文档,「将 base_url 改为 https://relaydance.com/v1 并保留 OpenAI SDK 即可调用」。文本与图像部分直接用 OpenAI SDK。视频任务端点不在 OpenAI SDK 的内置方法里,用 `httpx` 发请求即可,鉴权方式相同:`Authorization: Bearer YOUR_API_KEY`。 python import os, json, time, asyncio import httpx from openai import OpenAI API_KEY = os.environ["RELAYDANCE_API_KEY"] # 在 https://relaydance.com/console 创建 BASE_URL = "https://relaydance.com/v1" client = OpenAI(api_key=API_KEY, base_url=BASE_URL) HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} TEXT_MODEL = "YOUR_GROK_TEXT_MODEL" # 以 /models 页面列出的名称为准 VIDEO_MODEL = "doubao-seedance-2-5-1080p" # 2026-10-10 新增的 Seedance 2.5 1080p IMAGE_MODEL = "gpt-image-2" > 模型名与实时单价以 [relaydance.com/models](https://relaydance.com/models) 为准,请求字段细节以 [relaydance.com/docs](https://relaydance.com/docs) 为准。 --- ## 三、步骤 1:让 LLM 写结构化分镜 Planner 的输出必须是机器可读的 JSON,后续步骤才能自动处理。约束要写进 system prompt:单镜头不超过 15 秒(接口单条上限),提示词中用 `@image1` 引用参考图。 python PLANNER_SYSTEM = """你是短片分镜师。把用户需求拆成 4-8 个镜头,输出 JSON: {"title": str, "characters": [{"name": str, "look": str}], "shots": [{"id": int, "seconds": int, "ratio": "16:9", "prompt": str, "camera": str, "audio": bool}]} 规则: - 每个镜头 seconds 在 4-15 之间; - prompt 中用 @image1 指代主角定妆图,保持外观一致; - 写清景别、运镜、光线、动作,一个镜头只做一件事; - 只输出 JSON。""" def plan_storyboard(brief: str) -> dict: resp = client.chat.completions.create( model=TEXT_MODEL, messages=[{"role": "system", "content": PLANNER_SYSTEM}, {"role": "user", "content": brief}], response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content) board = plan_storyboard("一只橘猫宇航员在月球上找到一条小鱼干,30 秒,温馨幽默") --- ## 四、步骤 2:用 gpt-image-2 出定妆图 多镜头短片最常见的问题是角色"每个镜头长得不一样"。解决办法是先生成一张定妆图,再在每个镜头请求的 `metadata.content[]` 中带上它,并在 prompt 里写 `@image1`。 成本方面,RelayDance 的 gpt-image-2 **图像输出免费,只按输入计费**,图生图约 ¥0.035 起,**4K 与 1K 出图同价**(来源:[relaydance.com/models](https://relaydance.com/models))。所以定妆图可以直接出高分辨率,也可以多出几版供挑选。 python def make_character_sheet(board: dict) -> str: c = board["characters"][0] img = client.images.generate( model=IMAGE_MODEL, prompt=f"角色定妆图,正面全身,纯色背景:{c['name']},{c['look']}", size="1024x1024", ) return img.data[0].url # 若返回 b64_json,可先上传到自己的对象存储再取 URL ref_url = make_character_sheet(board) 单个请求最多可带 **9 张参考图、3 段参考视频、3 条音轨**,在 prompt 中依次以 `@image1` … `@imageN` 引用。场景图、道具图也可以一并放进去。 --- ## 五、步骤 3:并发提交视频任务 每个镜头对应一次 `POST /v1/video/generations`,核心字段为 `model`、`prompt`、`seconds`,其余放进 `metadata`:`ratio`、`resolution`、`generate_audio`、`callback_url`、`content[]`。 python async def submit_shot(http: httpx.AsyncClient, shot: dict, ref_url: str) -> str: payload = { "model": VIDEO_MODEL, "prompt": f"{shot['prompt']}。镜头:{shot['camera']}", "seconds": shot["seconds"], "metadata": { "ratio": shot.get("ratio", "16:9"), "resolution": "1080p", "generate_audio": shot.get("audio", False), # content[] 的元素结构以 /docs 为准,这里示意一张参考图 "content": [{"type": "image_url", "image_url": {"url": ref_url}}], }, } r = await http.post(f"{BASE_URL}/video/generations", headers=HEADERS, json=payload) r.raise_for_status() data = r.json() return data.get("task_id") or data.get("id") --- ## 六、步骤 4:轮询直到 succeeded / failed 轮询逻辑本身简单,需要注意三点: 1. **指数退避**。前几秒查得勤一些,之后逐步放慢,避免无意义的请求。 2. **设置总超时**。防止某个任务卡住,导致整条流水线一直挂起。 3. **只认两个终态**:`succeeded` 和 `failed`。其余状态都视为"还在跑"。 python def extract_video_url(task: dict) -> str | None: # 结果中包含视频 url,具体层级以 /docs 为准,这里做宽松提取 for key in ("url", "video_url"): if task.get(key): return task[key] res = task.get("result") or task.get("output") or {} if isinstance(res, dict): return res.get("url") or res.get("video_url") if isinstance(res, list) and res: return res[0].get("url") return None async def poll(http: httpx.AsyncClient, task_id: str, timeout=900) -> dict: delay, start = 3, time.monotonic() while time.monotonic() - start < timeout: r = await http.get(f"{BASE_URL}/video/generations/{task_id}", headers=HEADERS) r.raise_for_status() task = r.json() if task.get("status") in ("succeeded", "failed"): return task await asyncio.sleep(delay) delay = min(delay * 1.5, 20) raise TimeoutError(task_id) ### 带自愈的镜头执行器 这是体现 Agent 能力的环节:任务失败时,不是直接报错,而是把失败信息交给 LLM 改写 prompt(例如去掉可能触发审核的描述、降低动作复杂度),然后重新提交。失败请求不计费,因此最多重试 2 次是一个合理的默认值。 python def repair_prompt(shot: dict, error: str) -> dict: resp = client.chat.completions.create( model=TEXT_MODEL, messages=[{"role": "user", "content": f"这个视频镜头生成失败,错误:{error}\n原镜头:{json.dumps(shot, ensure_ascii=False)}\n" "请在保留叙事意图的前提下改写 prompt,降低动作复杂度、去掉敏感描述,只输出同结构 JSON。"}], response_format={"type": "json_object"}, ) return json.loads(resp.choices[0].message.content) async def run_shot(http, shot, ref_url, max_retry=2): for attempt in range(max_retry + 1): task_id = await submit_shot(http, shot, ref_url) task = await poll(http, task_id) if task["status"] == "succeeded": return shot["id"], extract_video_url(task) err = json.dumps(task.get("error", task), ensure_ascii=False)[:500] shot = repair_prompt(shot, err) return shot["id"], None async def render_all(board, ref_url, concurrency=4): sem = asyncio.Semaphore(concurrency) async with httpx.AsyncClient(timeout=60) as http: async def guarded(s): async with sem: return await run_shot(http, s, ref_url) results = await asyncio.gather(*(guarded(s) for s in board["shots"])) return dict(sorted(results)) clips = asyncio.run(render_all(board, ref_url)) ### 轮询的替代方案:Webhook 如果 Agent 部署在服务端,可以在 `metadata.callback_url` 中填自己的接收地址,任务进入终态后结果会 POST 到该地址。这样就不需要维护轮询循环,更适合队列化、任务量大的场景。比较稳妥的做法是两者结合:**以 Webhook 为主,并定时轮询兜底**,防止回调丢失。 --- ## 七、步骤 5:拼接成片 python import subprocess, urllib.request, pathlib def stitch(clips: dict, out="final.mp4"): d = pathlib.Path("clips"); d.mkdir(exist_ok=True) lines = [] for sid, url in clips.items(): if not url: continue p = d / f"{sid:02d}.mp4" urllib.request.urlretrieve(url, p) lines.append(f"file '{p.resolve()}'") (d / "list.txt").write_text("\n".join(lines)) subprocess.run(["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", str(d / "list.txt"), "-c", "copy", out], check=True) stitch(clips) 如果各镜头的编码参数不一致,把 `-c copy` 换成重新编码,例如 `-c:v libx264 -c:a aac`。 --- ## 八、成本估算:一条 30 秒短片要花多少? 以 6 个镜头 × 5 秒 = 30 秒为例,按 [relaydance.com/models](https://relaydance.com/models) 公布的单价计算(均为约数): | 方案 | 单价 | 30 秒成片成本 | |---|---|---| | Seedance Fast | 约 $0.152 / 秒 | 约 $4.56 | | Seedance 2.0 720p | 约 $0.190 / 秒 | 约 $5.70 | | Seedance 2.0 1080p | 约 $0.470 / 秒 | 约 $14.10 | | Seedance 2.5 1080p(`doubao-seedance-2-5-1080p`) | 约 $0.711 / 秒 | 约 $21.33 | | Seedance 原生 4K | 约 $4.90 / 5 秒条 | 约 $29.40 | 定妆图走 gpt-image-2,输出免费、只按输入计费,在总成本中几乎可以忽略。 **推荐的分层策略:** 1. **草稿轮用 Fast 或 720p**。先确认分镜节奏、运镜和角色一致性,这一轮的成本大约是终稿的 1/4。 2. **定稿轮再切到 2.5 1080p 或 4K**。只重渲已经确认的镜头。 3. **失败重试不额外花钱**。Agent 的自愈循环只会让成片质量更稳定,不会让账单失控。 计费方式为按生成的视频量后付费,支持 USDT 与 Stripe 银行卡,无需 BytePlus 企业账号或 KYC。 --- ## 九、工程上的几点经验 - **一个镜头只做一件事**。在同一个镜头里要求"转身 + 跑步 + 跳跃",失败率和画面崩坏率都会明显上升。这条规则应该写进 Planner 的 system prompt。 - **镜头时长控制在 4 到 15 秒**。15 秒是接口单条上限,更长的段落应拆成多个镜头。 - **持久化 task_id**。把 `task_id` 与镜头 id 的对应关系写入数据库,进程崩溃后可以直接恢复轮询,不用重新提交。 - **控制并发**。用 Semaphore 限制同时在跑的任务数,避免触发限流。 - **在 prompt 中统一风格词**。光线、色调、画幅这些描述放进每个镜头的 prompt 后缀,与参考图配合使用,跨镜头的一致性会更好。 --- ## 结语 这个 Agent 的价值不在于某一次生成的效果,而在于它把"写分镜 → 定妆 → 出片 → 失败自愈 → 拼接"串成了一条无人值守的流水线。任务轮询(或 Webhook)解决了长任务的异步问题,失败不计费让自动重试可以放心使用,OpenAI 兼容协议则让现有的 LLM 工具链几乎不用改动。 完整接口说明见 [relaydance.com/docs](https://relaydance.com/docs),各模型实时单价见 [relaydance.com/models](https://relaydance.com/models)。
据 relaydance.com 官方文档:「失败或报错的请求一律不计费」
据 relaydance.com/docs 官方文档:「将 base_url 改为 https://relaydance.com/v1 并保留 OpenAI SDK 即可调用」

关键事实与数据

项目数值来源
Seedance 2.0 720p 价格约 $0.190 / 秒relaydance.com/models
Seedance 2.0 1080p 价格约 $0.470 / 秒relaydance.com/models
Seedance 2.5 1080p 价格约 $0.711 / 秒,2026-10-10 新增,模型 doubao-seedance-2-5-1080prelaydance.com/models
Seedance Fast 价格约 $0.152 / 秒relaydance.com/models
Seedance 原生 4K 价格约 $4.90 / 5 秒条relaydance.com/models
gpt-image-2 出图计费图像输出免费,只按输入计费;图生图约 ¥0.035 起,4K 与 1K 出图同价relaydance.com/models
接口协议OpenAI 兼容,base_url 改为 https://relaydance.com/v1 即可relaydance.com/docs
失败计费失败或报错的请求一律不计费relaydance.com/docs

数据更新于 2026-06-29,实时价格以官方 /models 页为准。


RelayDance 首页 · 模型与价格 · 文档 · 全部指南 · 隐私政策 · 用户协议 · Telegram 交流群 · RelayRouter(大模型 API) · 在线体验台