让 Agent 把视频做出来:DeepSeek Harness × DeepSeek-V4.1-Flash 视频生产流水线

引言

用大模型做视频,大多数人想到的是"生成":文生视频、图生视频、数字人口播。这条路的问题在于,产出的每一帧都由模型"想象"出来,因此不可控、不可复现,也承载不了真实信息------产品界面上的按钮、报表里的数字、游戏里的操作,它都没法准确画出来。

另一条路几乎没人讨论,但工程上更实在:让 Agent 写一套程序,把"已经存在的事实"搬进一条时间线 。画面来自真实运行的软件(浏览器的渲染帧、录屏文件),或者由代码逐帧绘制;时间轴由数据描述;最后由 ffmpeg 合成。模型在这里不像素,它做的是编排:写采集脚本、写时间轴、写合成命令、看渲染结果、改参数重跑。

本文讨论的就是这条路的完整技术栈:一个只有单一工具入口的 coding agent(DeepSeek Harness 驱动 DeepSeek-V4.1-Flash,1M 上下文、可开子代理、可起后台任务),如何从一句话需求走到一支可交付的成片;它在哪些环节必然翻车;什么样的题材适合交给它;以及一次成片的真实成本。


目录

  • 引言
  • [1. 为什么这条路现在成立:四个基础设施条件](#1. 为什么这条路现在成立:四个基础设施条件)
  • [2. 三条技术路线:画面从哪来,时间轴就长什么样](#2. 三条技术路线:画面从哪来,时间轴就长什么样)
  • [3. 一条通用流水线:六个阶段](#3. 一条通用流水线:六个阶段)
  • [4. 视听层:旁白、字幕、配乐](#4. 视听层:旁白、字幕、配乐)
  • [5. 并行:什么时候值得开子代理](#5. 并行:什么时候值得开子代理)
  • [6. 成本:钱花在哪,时间花在哪](#6. 成本:钱花在哪,时间花在哪)
  • [7. 适合做什么视频,不适合做什么](#7. 适合做什么视频,不适合做什么)
  • [8. 给想复刻这套流水线的工程师:10 条经验](#8. 给想复刻这套流水线的工程师:10 条经验)
  • [9. 局限与风险](#9. 局限与风险)
  • [10. 结语](#10. 结语)
  • [附录 A:口径与数据来源](#附录 A:口径与数据来源) | [附录 B:关键参数速查](#附录 B:关键参数速查)

1. 为什么这条路现在成立:四个基础设施条件

1.1 统一工具面:一个 run_code,把整个操作系统收进沙箱

视频制作是一条由几十个异构小动作拼成的流水线:找到浏览器、下载 ffmpeg、起静态服务、驱动鼠标键盘、抽帧、编码、量响度、烧字幕。如果每个动作都要声明成一个工具,工具面会迅速失控;如果只给一个 shell,模型又失去了用代码组织控制流和错误处理的能力。

run_code 的折中很关键:控制流用真正的编程语言写,副作用通过受控工具发生 。它接收一段 TypeScript 程序,程序里可以调 pwsh、读写文件、发 HTTP、起后台任务、拉子代理。于是"录制 20 个场景"不是 20 次工具调用,而是一个 for 循环 + 一个场景注册表。

ts 复制代码
const ff = "D:\\tools\\@ffmpeg-installer\\win32-x64\\ffmpeg.exe";
await tools.pwsh({
  description: "Extract QC frames from a clip",
  command: "& $ff -y -ss 5.2 -i out/clips/scene-03.mp4 -frames:v 1 -q:v 2 out/qc/03-5.2.jpg",
});
await tools.read_image({ file_path: "out/qc/03-5.2.jpg" });

最后一行是整件事的分水岭:编码结果不再只是一个成功/失败的返回值,而是可以被模型自己审视的画面。

1.2 长上下文 + 高缓存命中:让"记住整条流水线"变便宜

一支十几分钟的长片,制作过程中模型要同时持有:分镜表、几十段场景脚本、TTS 时间轴、字幕轨、编码参数,以及此前每一次报错的结论。实测中单条请求的输入规模可以爬到十几万 token(合成阶段上下文堆满时的典型值)。

这件事之所以不烧钱,是因为 prompt caching :Agent 负载的输出/输入比大约 1:115 ------读得多、写得少,天然是缓存最擅长的形状。实际生产里缓存命中可以稳定在 90% 以上,这是"让模型反复看同一份 20 万字上下文"在经济上可行的前提。

1.3 后台任务 + 子代理:把墙钟时间叠起来

视频生产里大量时间是在"等":等编码、等录屏、等渲染。所以运行时必须能起后台任务(job/registered → job/settled),并允许把可独立验收的工作拆给子代理并行做------配乐、卡片设计、事实核查、独立验收,都是典型的可并行块。

并行不是白来的:它把"一次长上下文读入"换成"N 路并行读入相加",省的是墙钟,花的是 token(后文有量化)。

1.4 确定性:同一份脚本必须能跑出同一支片子

最容易低估的一条。视频工程里,ffmpeg 参数、字体、响度、帧率、字幕时间轴必须可复现,否则"改一个地方"就变成"重做一支片子"。所以你会看到 Agent 为确定性付出各种代价:固定随机种子(Particles(78, seed=11))、要求音乐合成脚本"两次运行 sha256 一致"、把每段视频独立编码以便"失败可单场景重录"、把中间产物(timeline.json、marks.json)落盘而不是留在内存里。

小结 :Agent 做视频的可行性,不取决于某次生成的质量,而取决于能不能把视频变成一段可反复执行、可局部重跑、结果能被自己看见的程序。

1.5 运行时解剖:一次 step 到底发生了什么

把事件流摊开,一次 step 的生命周期非常清楚,而这些机制恰好都是做视频所依赖的:

事件 作用
assistant/message 一次模型响应结束,带 usage(输入/缓存/输出)与 tool-call 列表
tool/ptc-dispatch 代码沙箱内部实际派发的子工具调用(run_code 里调 pwsh、读图、子代理...)
job/registered → job/settled 后台任务(起服务、录制、编码),可被杀、可超时
llm/retry 传输层/超时重试,策略显式写在 policyKey 里
image/offload 把历史里的图片从上下文中卸载

三个细节值得单独说:

(1)重试策略是显式的。 运行时下发的策略长这样:

复制代码
["normal",5,["EMPTY_RESPONSE","RATE_LIMIT","SERVER","TIMEOUT","TRANSPORT"],500,10000,0.1]

含义是:普通模式、最多 5 次、只针对这几类失败、退避 500→10000 ms、抖动 0.1。长任务最常撞的其实不是模型能力,而是传输层------一场两小时的作业里出现几十次 TRANSPORT 重试并不罕见。

(2)图片是"有限资源",运行时会给它设闸门。 做视频必然要反复看图,而图片占用的上下文远高于文本。超限时请求会被拒绝:

复制代码
pi-ai request images exceed the configured image count or base64 payload bound;
3 more oldest occurrence(s) must be offloaded.
code: IMAGE_OFFLOAD_REQUIRED

然后运行时把最旧的图片卸载掉(image/offload 记录"卸载了哪几个事件的第几张图"),模型需要时再用 read_image 重读。这个机制解释了为什么"看图 → 改代码 → 再看图"能在长上下文里跑上百轮:图片被有策略地丢弃,而不是把上下文撑爆。它不是故障,是正常流控------别和重试混着看。

(3)图片的多寡直接决定"看得见多少"。 一次请求能带多少图是有上限的,所以抽帧自检必须"挑帧",不能"全看":一支 16 分钟的片子,全部帧数是万级,能回看的是几十张。选择看哪几帧,本身就是一个工程决策。


2. 三条技术路线:画面从哪来,时间轴就长什么样

决定流水线形态的不是模型,而是画面来源。三种来源对应三套完全不同的时间轴机制。

路线 画面来源 时间轴来源 典型用途 主要代价
A. CDP screencast 浏览器 Page.startScreencast 推来的 JPEG 帧 每帧自带的 CDP 时间戳 真实软件界面走查 帧率由画面活跃度决定(十几到三十几 fps),须重建恒定帧率
B. Playwright recordVideo 浏览器写出的 WebM 录制期打的 mark() 墙钟时间,事后反锚 网页/小游戏试玩、交互演示 要自造"墙钟 → 视频时间"映射,依赖容器元数据
C. 纯代码逐帧渲染 Pillow / Canvas 现算的位图 秒数直接换算成帧号 无界面可录的信息型内容、数据页、片头片尾 排版全靠手写坐标,没有布局引擎

2.1 路线 A:screencast ------ 用"推帧时间戳"换真实性

这条路走的是"真录":驱动一个无头浏览器,让页面自己渲染,把浏览器推出来的帧收下来。零依赖 CDP 客户端的启动参数里,塞满的都是"防抖"开关:

复制代码
--headless=new --disable-gpu --no-sandbox --no-first-run
--disable-background-timer-throttling --disable-renderer-backgrounding
--disable-backgrounding-occluded-windows --hide-scrollbars --mute-audio
--force-color-profile=srgb --font-render-hinting=none
--window-size=1920,1080 --force-device-scale-factor=1

录制核心只有几十行:

js 复制代码
await session.send('Page.startScreencast', {
  format: 'jpeg', quality: 92, maxWidth: 1920, maxHeight: 1080, everyNthFrame: 1,
});
// 收到帧 → 落盘 jpg + 记下 metadata.timestamp → 回 screencastFrameAck

核心矛盾 :screencast 是"内容变化才推帧",天然是可变帧率。实测一段纯数字比对的页面只有 15 fps ,一段多图表的页面能到 36 fps。而交付物必须是恒定帧率。

解法是:每帧落盘时记下时间戳,把"帧 + 它该持续多久"写成 concat demuxer 清单,交给 ffmpeg 补齐成 CFR:

复制代码
file 'frames/f000000.jpg'
duration 0.0666
file 'frames/f000001.jpg'
duration 0.0521
...
# 然后 -f concat -safe 0 -i list.txt -vsync cfr -r 30

首帧补上启动空档、末帧补上收尾空档------于是"页面静止时不出帧"不会让视频变短,只会让这一帧多停几秒。帧时长一般还会 clamp 到 [1/240, 20s] 之间,防止某个异常间隔把整条时间轴拉坏。

这条路还有一个容易被误读的细节:帧少不等于掉帧。画面越静,帧越少;所以"帧数/秒"是画面活跃度指标,不是性能指标。真正决定观感的是那根时间戳。

副作用也很实在:镜头没有呼吸感。页面不动就没帧,节奏只能靠"小幅放慢 + 尾帧补齐"微调;需要卡点剪辑的短片会很难看。

2.2 路线 B:recordVideo + mark 反锚 ------ 把"录制期的时间"翻译成"剪辑期的帧号"

Playwright 的 recordVideo 更省事:给一个 size,浏览器把整段过程写成 WebM(实测 VP9、25 fps)。

麻烦在于:录出来是一条连续时间流,而分镜需要的是"第 12.4 秒切下一个镜头"。recordVideo 不会告诉你"用户按下按钮"那一刻在视频的第几秒。

于是有了这套机制------页面里打点,事后反锚:

  1. 录制脚本在每个语义节点调用 mark('brush-start'),把 performance.now() 与墙钟一起写进 marks.json;
  2. 录制结束(关闭 context、await page.video().path() 拿到文件)后,用已知的关闭时刻 T_CLOSE 与视频总时长 D 反算:videoTime = D - (T_CLOSE - t);
  3. 所有剪辑点、字幕窗、转场全由 marks.json 算出,不再靠肉眼估。

坑在这里:ffprobe 有时读不出这类 WebM 的 duration 。于是基准 D 退化成 T_CLOSE - T0,一旦录制器有首帧延迟或尾帧丢弃,整条时间轴整体平移------这正是很多自动剪辑"字幕总差半秒"的根因。可行的修法是别用墙钟反锚:用 requestVideoFrameCallback 记录每帧的 mediaTime,把 mark 直接打在视频时间轴上;或者只录"动作片段",由合成器拼接。

这套机制之前,先有一轮"录制参数矩阵"实验。 一个常见的误解是"把 deviceScaleFactor 调高,录出来就更清晰"。实测把它拆穿了:

变体 viewport DPR recordVideo.size 实测输出 码率
A 360×640 3 1080×1920 vp9 1080×1920 @25fps 517 kb/s
B 540×960 2 1080×1920 vp9 1080×1920 @25fps 789 kb/s
C 1080×1920 1 1080×1920 vp9 1080×1920 @25fps 1280 kb/s
D 360×640 3 360×640 vp9 360×640 @25fps 470 kb/s

结论可以直接抄走:recordVideo 的输出像素只由 size 决定,与 deviceScaleFactor 无关------DPR 只影响页面内部位图(canvas)的绘制精度,不影响视频帧尺寸。四个变体除了码率(内容复杂度不同)以外规格完全一致。"调高 DPR 换清晰度"在视频录制里是无效操作。

同一轮对照还排除了两条路:

  • headed vs headless:产物没有可观测差异,因此默认 headless(也免去窗口被抢占带来的抖动);
  • CDP screencast :在一个移动端 canvas 页面上,1.5 秒只推出 6 帧 ,且帧 metadata 仍是小尺寸------淘汰 。同一个模型在另一类页面上却首选 screencast:选型依据是"页面活跃度"和"目标帧率",不是"哪种技术更先进"。

还有一个更根本的限制:canvas 类应用的 backing store 常常固定在设计尺寸(例如 750×1334),1080 视口只是 CSS 拉伸,拿不到原生高分辨率的绘制 。想要竖屏版式,只能用 CSS zoom 把版式撑起来,代价是媒体查询不跟着变------这个副作用如果页面里断点不多,可以侥幸不炸;断点一多,就得放弃这条路。

这不是"AI 试错",而是"AI 做受控实验":固定其余变量,只改一个自变量,产出一组可比较的产物,再用看图 / ffprobe 判定,最后把结论写回约束表。

2.3 路线 C:纯代码渲染 ------ 时间轴是"拍号",不是秒表

没有界面可录的题材(项目集、数据总结、概念片),整个采集层会被删掉,画面改由程序生成:

python 复制代码
W, H = 1920, 1080
SS = 2                      # supersample factor used when baking sprites

def render_frame(rig, t):
    ctx = {"fade": 0.0, "endfade": 0.0}
    frame = rig.bg.copy()
    ox, oy = -(t * 7.0) % 48, -(t * 4.0) % 48      # 缓慢漂移的经纬网
    blit(frame, rig.grid, ox - 48, oy - 48, alpha=0.85)
    rig.particles.draw_canvas(frame, t, alpha=0.85) # 固定 seed 的粒子
    rig.scene_at(t).draw(frame, t, ctx)             # 每个场景自己画自己
    blit(frame, rig.chrome, 0, 0, alpha=0.92)
    return frame

帧是 t 的纯函数(粒子种子固定),因此"任意时刻单独出图""只重渲某几秒""精确复现"全都成立。编码把原始帧直接管道喂给 ffmpeg,一次做完,不用 filter_complex:

python 复制代码
cmd = [FFMPEG, "-y", "-f", "rawvideo", "-pix_fmt", "rgb24",
       "-s", "%dx%d" % (W, H), "-r", str(FPS), "-i", "-",
       "-c:v", "libx264", "-preset", "medium", "-crf", "18",
       "-pix_fmt", "yuv420p", "-profile:v", "high", "-level", "4.2",
       "-g", str(FPS * 2), "-movflags", "+faststart"]

这条路最漂亮的一处设计,是时间轴用拍号而不是秒 :设 BPM=104,260 拍恰好 150.000 秒;画面切点写成 CUTS=[0,12,32,72,...](单位是拍),配乐也按同一张拍网格合成。于是"音画对齐"这个通常要逐帧对轴的活,变成了整数乘法------实测 9 个切点里 8 个零帧偏差。

代价同样明显:没有布局引擎 。元素坐标是手写常量(592 + dy、H - 176、y0 = 392),文本一长就压盖,只能靠人眼盯帧发现。这是它最大的一次性成本来源。

2.4 选型的判据

三条路线不是"先进程度"的排序,而是三组权衡:

你的情况 选
要展示的是真实软件行为,且页面变化频繁 A(screencast)
要展示的是真实软件行为,但需要精确的"某时刻发生了什么" B(recordVideo + mark)
没有界面可录,或画面本就是图表/文字/数据 C(代码渲染)
需要原生高分辨率 + 精确时序 A 或 C(B 受容器与反锚精度限制)

一句话:画面与时间轴必须解耦。画面负责"长什么样",时间轴负责"什么时候出现";只要这两件事能分开描述,Agent 就能像改代码一样改片子。

3. 一条通用流水线:六个阶段

把这三种路线抽象一层,会发现它们共享同一套阶段划分。这套划分本身就是方法论。

复制代码
[0] 环境侦察 ──→ [1] 约束化分镜 ──→ [2] 素材生产(卡片 / 片段)
                                        │
                                        ▼
[6] 合成与自检 ←── [5] 时间轴重建 ←── [4] 交互自动化 ←── [3] 页内注入
                                        │
                                        └─ 录制(三条路线任选)

自检不通过 ──→ 只回退受影响的阶段(2 素材 / 4 交互 / 5 时间轴),不重做全片

阶段 0 · 环境侦察:先问"这台机器能干什么"

Agent 不假设环境。第一个动作永远是并发探针:node -v、ffmpeg -version、Test-Path node_modules、浏览器缓存目录、磁盘空间、甚至 npm registry 连通性。

这一步的产出经常直接改写后续所有方案:

  • ffmpeg 可能是某个 IDE 或截图工具自带的旧构建,年份决定你能不能用 xfade、有没有 libass(字幕烧录)、有没有 libx264;
  • 系统 PATH 里可能根本没有 ffmpeg,但某个软件的安装目录里躺着一个;
  • Playwright 自带的 ffmpeg 常常只带 VP9/WebM,没有 h264/aac------这就是为什么录制端只能拿到 WebM,编码必须交给另一个 ffmpeg;
  • 有的构建压根没有 ffprobe,所有探测都得退化成 ffmpeg -i + 正则解析 stderr。

教训:Agent 的"能力"是环境的函数。同一个模型换一台机器,可能从"能做叠化转场"退化成"只能硬切"。环境侦察必须是流水线的第一等公民,而不是前置准备。

阶段 1 · 把创作目标翻译成可验收的约束

一句自然语言需求,必须先变成一张显式约束表,后面的每一段代码都要能对得上它。典型的一张:

约束 落地方式
形态 1920×1080 / 30fps / H.264 + AAC,中文配音 + 全程字幕,14---16 分钟
叙事结构 每一章先由静态卡点题,再由真实操作画面给证据
口径纪律 不出现第三方文档名称;数据一律称"示例数据"
分镜粒度 30 段 = 10 张卡片 + 20 段录制,每段独立录制、独立配音
时长口径 画面 targetSec 为目标,配音时长以 TTS 实测为准(允许 ±10%)

注意最后一条:它把"时长"这个最容易扯皮的量,定义成了一个有确定算法的量 ------先跑 TTS 拿到音频时长,再反过来决定画面多长。这是长片能自动化的关键:音频驱动时长,而不是时长驱动音频。

阶段 2 · 素材生产:卡片和片段是两种东西

素材永远是两类:

  • 静态卡片 (片头、章节页、片尾、数据页):PPT 光栅化、HTML + Playwright 截图、或者代码直接画。要高分辨率就用 deviceScaleFactor=2 截两倍图(例如 1080×1920 的卡片截成 2160×3840),缩回来更锐利;
  • 动态片段(真实操作过程):screencast 或 recordVideo 产出的视频文件。

两类的验收标准完全不同:卡片看"文字有没有压边、二维码静区够不够",片段看"操作是否完整、状态是否到达、控制台有没有报错"。

卡片侧还可以做自动像素自检(例如"最小净空 ≥ 51px"这类断言),把"看起来挤"变成可测的数字。

阶段 3 · 页面内注入:让看不见的操作变成看得见的手

真实 UI 里的点击是无声的。要让视频"讲清楚发生了什么",就得在页面里注入一层视觉提示:光标、点击涟漪、聚光灯(用一个超大外阴影把周围压暗一格)、气泡标注。

css 复制代码
#stage-root { position: fixed; inset: 0; z-index: 2147483000; pointer-events: none; }
#stage-cursor { position: absolute; width: 72px; height: 72px; border-radius: 50%;
  background: radial-gradient(circle at 50% 50%, rgba(58,47,38,.34) 0%, rgba(58,47,38,0) 76%);
  border: 3px solid rgba(253,243,220,.95); transition: transform .28s ease; }

关键细节是双轨同步:光标移动既驱动 CSS 动画,也用 CDP 分步派发真实鼠标事件------这样"看起来点了"和"真的点了"是同一件事,悬停态、聚焦态、按下态都会正确出现。

js 复制代码
await session.send('Input.dispatchMouseEvent', {
  type: 'mousePressed', x, y, button: 'left', clickCount: 1,
})

覆盖层用 Page.addScriptToEvaluateOnNewDocument 注入,因此 SPA 内部路由跳转后它仍然存在;hash 路由不重载页面时,脚本要主动重新注入一次。

还有一类场景需要合成事件 :某些 canvas 游戏只监听 pointer 事件,或者要求 MouseEvent 与 PointerEvent 成对出现。只发其中一种,会静默失败------状态机停在原地,而控制台什么错误都没有。稳妥做法是两条都发,并同时派发 pointermove 做插值,让拖拽轨迹平滑。

阶段 4 · 交互自动化:从"点坐标"升级到"读状态机"

最容易做砸的一步。如果只会 click(960, 540),换一个分辨率就全废。

可行做法是尽量从应用自身读状态:

  • 先读项目自带的 e2e 测试与采集脚本,学现成的选择器;
  • 把"等待"定义成条件而非固定睡眠:等两帧 rAF + 沉降时间(settle())、等某个 DOM 出现、等应用自己的 debug bridge 报告状态推进;
  • 遇到"点了没反应",去看页面实际状态(截图 + DOM 查询),而不是盲目加大等待。

极端一点的情况(完全没有 DOM、纯 canvas 绘制的小游戏),也能"看像素":截图 → 按网格采样 → 颜色量化 → 洪水填充,从画面里反推出可点击的目标。

阶段 5 · 时间轴重建:把"跑一遍"变成"可剪辑的素材"

技术含量最高的一环,也是三条路线分道扬镳的地方。共性抽象只有一条:

录制期只负责"忠实记录 + 打点",剪辑期只负责"按点重排"。

把这两件事混在一起的写法都会返工。一个典型事故:给静态 PNG 的叠加层加了 -loop 1,把一张图变成了无限帧流,overlay 链的 framesync 直接爆炸------4 次编码超时(累计十几分钟工具预算)外加一个 moov atom not found 的坏文件。修法三件事:去掉 -loop、用 enable='between(t,a,b)' 门控叠加时段、把叠加图预裁到最小包围盒。修完,同样一段 13.5 秒编完。

时间轴的落地形式,通常是这几个文件:

复制代码
timeline.json   # 场景 id / 起止 / 音频 / 片段 / cue 数
subtitles.ass   # 由 timeline 生成的字幕轨
segments.txt    # concat demuxer 清单(视频)
audios.txt      # concat demuxer 清单(音频)

阶段 6 · 合成与自检:能不能交付由检查项决定,不由模型自信决定

合成几乎全是 ffmpeg 的确定性工作,但质检环节花的时间往往超过合成本身。检查项大致四层:

层 检查什么 手段
规格 分辨率/帧率/时长/编码/体积 ffprobe 读 stream;-movflags +faststart
完整性 黑场、静音、帧数 blackdetect、silencedetect、帧数断言
观感 字幕压边、二维码可扫、淡出颜色 抽帧 → read_image → 模型用眼睛判断;二维码解码
内容 画面里的数字/状态是否真的对 逐帧比对、标准算例对拍、volumedetect 逐秒量化

第四层最值钱。有一类 bug 只有视频能暴露:抽帧核对时发现某页的"引擎值"整列恒红------为了把视频做对,先去把产品 bug 修了,然后重录那一段。这就是"视频会逼你先修产品"的来源。

第三层也不可省。合成后抽帧发现"淡出效果把画面淡到了黑色,而我的间隔帧是奶油色",两者不匹配------改淡出颜色再合成。没有"抽帧看图"这个动作,这个 bug 会一直留在成片里。


4. 视听层:旁白、字幕、配乐

视频"能看"是一半,"能听、能读"是另一半。这一层有几个反复出现的技术点。

4.1 无旁白时:配乐全本地合成

"不要旁白,可以有点背景音乐但不能抢戏"是最常见的需求。它其实简化了整条流水线:音画对齐的难点直接归零,剩下的只是"配乐够不够安静、够不够干净"。

而版权干净的最稳做法是不采样、纯合成------用代码算出一条 WAV,可复现、无素材检索、零版权风险。两条实现路径都很常见:

路径 实现 特点
Node 零依赖 手写 PCM 采样 + RIFF/WAV 头(writeUInt32LE 逐字段) 无第三方依赖,能控制到"两次运行 sha256 一致"
Python + numpy 加法合成波表 + ADSR 包络 + 简易混响 + BS.1770 响度归一 表达力强,适合做和声/包络/侧链

具体的合成配方(可直接照抄的思路):

  • 拨弦:多个谐波叠加 + 指数衰减包络 + 轻微揉弦;或者用 Karplus-Strong 波导,音色更像竹/琴;
  • 铺底:五度/八度长音 pad,音量压到拨弦的 1/5 以下;
  • 混响:Freeverb 式的 8 comb + 4 allpass,RT60 设到 2--3 秒;
  • 响度 :目标 -20 LUFS 左右、真峰 ≤ -3 dBTP,避免削波与"突兀静音";
  • 结构:按段落写和弦进行,用同一张拍表对齐画面切点。

有个细节很能说明"Agent 怎么听音乐":模型听不到声音,所以子代理会把频谱图放大来看,靠它揪出"振荡器相位错 → 泛音整体快了 64 倍"这种用耳朵都未必听得出的 bug。

4.2 有旁白时:TTS 驱动时长 + ASS 烧字幕

口播类视频的骨架是"音频先定、画面后跟":

bash 复制代码
python -m edge_tts --voice zh-CN-XiaoxiaoNeural --text "..." --write-media test.mp3
# 选定后:voice=zh-CN-XiaoxiaoNeural, rate=+4%

选型靠 A/B 试听:同一句话用不同 voice / rate 各生成一版,听感与时长都记下来,再定稿。

拿到音频后:

  1. 段长公式 :D = max(audioDur + 0.9, clipDur - 2.5)(长片),卡片段 D = max(4, audioDur + 1.2);
  2. 画面不够长就用 slow = min(1.12, D / clipDur) 放慢 + tpad=stop_mode=clone 补最后一帧;
  3. 每段音频前置留白 + 补静音到 D:adelay=450|450, apad,统一 48kHz / stereo / pcm_s16le;
  4. 所有段音频 concat 成一条完整音轨,最后与画面一次合流。

字幕没有词级时间戳,只能按字数比例切 cue:

js 复制代码
const d = Math.max(900, Math.round((cue.length / totalChars) * durationMs));
// 末句对齐音频尾;过长则按标点拆两行(ASS 里用 \N)

样式参数直接写进 ASS 头部,烧录时不用再调:

复制代码
Style: Sub,Microsoft YaHei,37,&H00FFFFFF,&H00FFFFFF,&H0012243C,&H9A000000,...
PlayResX: 1920, PlayResY: 1080, MarginV: 52

一条实战经验:字幕时刻别手工估 。手工估的版本总会差 0.5--1 秒,稳妥做法是把 cue 锚定到"事件锚点"(某段画面真正开始的时刻)再按字数分配。前面说的 marks.json 就是为这个服务的。

4.3 混音与响度

成片音频基本就三条命令:

复制代码
# 旁白 + 配乐:配乐压低 / 避让
[1:a]volume=-18dB,alimiter=0.85[bgm];[0:a][bgm]amix=inputs=2:duration=first[aout]
# 统一响度
[aout]loudnorm=I=-16:TP=-1.5:LRA=11[final]
# 收尾
-c:a aac -b:a 192k -ar 48000

验收靠 volumedetect / ebur128 逐段量化------"听起来差不多"不算数,得看 mean_volume 与 integrated LUFS。


5. 并行:什么时候值得开子代理

流水线里天然存在"可以并行做、且互不依赖"的块,最典型的是配乐 和片头/片尾卡。

做法是主线程建共享任务卡,write scope 显式互斥,然后并行拉起 fresh 子代理:

任务 write scope 子代理
合成 120s 垫乐 只写 .scratch/video/audio/ music
片头/片尾卡 + 字幕渲染器 只写 .scratch/video/cards/ cards

互斥是写进提示词的,不是靠文件锁:

只写 .scratch/video/audio/ 下的文件,不要动仓库源码、不要做 git 操作、不要碰其他 .scratch 目录。

实测时间账:spawn 发生在流程第 5 分钟;卡片子代理第 15.5 分钟交活、配乐子代理第 28.5 分钟交活,而主线程在 82.7 分钟才收尾。两个子代理都远早于终稿,主线程中途只做两次"只读观察"(读队友产出的 PNG 审卡),全程没有阻塞等待。

再进一步,可以把验收本身也拆成独立子代理:让它跑亮度统计、切点对齐测量、二维码可扫性检查。让做片子的人不给自己打分,是这条流水线里最"工程化"的一笔。

反过来,有些工作拆不开:

  • 录制与时间轴:所有镜头共享同一份时间戳/打点文件,拆开就要合并时间轴,合并成本高于收益;
  • 字幕时序:它依赖"每一段画面最后长什么样",必须在录制与裁剪定型后才能定稿。

判据可以浓缩成两句:

能并行 = 产物文件互斥 + 验收标准能写成句子。

不能并行 = 共享同一个时间轴 / 后一步的输入是前一步的像素结果。

代价也要说清:并行不是免费的,它把"一次长上下文读入"换成"N 路并行读入相加",子代理合计可以吃掉整项任务 30%--50% 的 token。


6. 成本:钱花在哪,时间花在哪

6.1 一次成片的量级

先说结论再看分解。下面是几类典型产出实测到的量级(同一套流水线、同一模型、同一种"从一句话到成片"的交付方式):

产出类型 成片时长 总 token 每秒成片消耗 token 缓存命中率
长片(演示/培训,带旁白) 约 16.5 分钟 约 1.10 亿 约 11.1 万 96.7%
中片(竖屏试玩/宣传) 103--150 秒 3100--3200 万 21--30 万 90%--96%
短片(60--90 秒) 约 84 秒 约 4500 万 约 54 万 91%

按官方价目(deepseek-flash,即 DeepSeek-V4.1-Flash-1017;单位:元 / 百万 tokens)折算:

计费项 空闲时段 高峰时段
输入(缓存命中) 0.02 0.04
输入(缓存未命中) 1.00 2.00
输出 4.00 8.00

高峰时段 = 北京时间周一至周五 9:00--12:00、14:00--18:00(不含法定节假日);其余时间(含周末与节假日全天)为空闲时段,价格为高峰的一半。

因为每个请求都带时间戳,可以按"这条消息落在哪个时段"逐条计价,而不是取平均值。结果:

产出类型 成片时长 单支成本 折合每分钟成片
长片(16 分钟) 约 16.5 分钟 约 ¥16.0 ¥0.97
中片(150 秒) 2.5 分钟 约 ¥4.6 ¥1.84
中片(103 秒) 1.7 分钟 约 ¥4.7 ¥2.73
短片(84 秒) 1.4 分钟 约 ¥6.2 ¥4.43

同一批数据,如果全部落在空闲时段是 ¥22.0 ,全部落在高峰时段是 ¥43.9 ,按真实时间戳逐条计价是 ¥31.5 ------时间点本身就是 2 倍的成本变量。这也解释了一个反直觉现象:长片折合每分钟只要 ¥0.97,短片却要 ¥4.43,固定开销(环境侦察、选型实验、卡片设计)摊不薄。

6.2 三个反直觉的结构关系

第一,负载几乎全是"重读上下文"。 输出 token 只占总量的 0.86% ,命中输入占 94.41% ,输出/输入比约 1:115。也就是说,每次请求的规模由上下文体积决定,而不是由模型"写多少"决定。同一件事如果在无缓存状态下重读十遍历史,token 量会差一个数量级。

第二,账单大头是"未命中的输入",不是输出。 按官方单价拆开:未命中输入占 43.2% 、输出占 36.3%、命中输入只占 20.5%。注意这里的关键比值------未命中输入是命中输入的 50 倍单价 ,输出是命中输入的 200 倍单价。所以真正该抠的是两件事:把钱优先花在提高缓存命中率 上(让 1033 万未命中输入继续往 2.06 亿命中里挪),以及压缩输出(187.7 万 token 的输出已经贡献了三分之一账单)。

第三,子代理可能比主线程更贵。 16 分钟长片那一次,主回合折合约 ¥5.7 ,它拉起的 5 个子代理合计约 ¥10.3 ------并行省的是墙钟,不是钱。原因是每个子代理都带一份自己的完整上下文,且它们的请求有一部分落在高峰时段。这也是"什么时候该开子代理"必须用"文件互斥 + 可书面验收"来约束的原因:拆得不好,就是花双倍 token 买一份重复理解。

6.3 时间花在哪:返工才是大头

一次 100 分钟的交付里,真正"录制"往往只占很小一块,其余是裁剪、重录、重编码。几条典型的返工线:

(1)自动找切点,踩了四版。 场景脚本都以登录起步,于是每段开头都带 3--5 秒登录页。解决办法是写一个帧相关性检测器(64×36 灰度 + 皮尔逊相关)自动找切点,但踩了四版:快照取错帧 → 深色帧过滤误杀某段 → 命中过渡帧 → 阈值从 0.97 调到 0.995。结果:全片因裁切点变化重编码 3 次,累计约 694 秒编码时间。

(2)合成期事故,占了 80% 的回合。 一次 82.7 分钟的交付里,约 66.5 分钟花在合成/自检/返工上------字幕时序两跳重排、-loop 1 导致的 4 次超时、坏文件回退。真正"录制"的时间反而不多。

(3)排版返工,全片重渲 4 版。 走程序化渲染路线的交付,4 版里 3 版是排版/质检问题触发的(数字错位、留白不均、文字压码、二维码静区不足),单帧渲染 610--870 ms,全片从 4.3 分钟涨到 6.3 分钟。

一句话 :成本不在"渲染",而在"判断哪里不对,然后重来一遍 "。降低成本的杠杆是把判断自动化(相关性检测、亮度统计、二维码解码、响度测量),而不是把模型换得更快。

6.4 优化清单

按性价比排序:

  1. 先修确定性,再谈优化。 中间产物落盘、每段独立编码、随机种子固定------返工成本会掉一个数量级,而返工是最大的隐性开销。
  2. 把验收做成断言。 响度、帧数、静音、二维码解码、切点偏差,全部脚本化。断言过的部分,模型不用再"看",也就省掉了反复读图的上下文。
  3. 盯住缓存命中率。 未命中输入是命中输入的 50 倍单价,账单里它是最大一项。稳定的系统提示、稳定的中间产物路径、把长上下文放在会话前部而不是反复改写,都能提高命中率。
  4. 控制输出。 输出单价是命中输入的 200 倍。让模型写"参数化脚本"而不是"逐次描述",输出量能压下来一半,账单能省掉一截。
  5. 注意时段。 同一批工作量,落在高峰时段是空闲时段的 2 倍价格。批量渲染、长时间重编码这类重活,可以排到 12:00--14:00 或 18:00 之后。
  6. 并行只用于互斥块。 配乐、卡片、事实核查、验收可以并行;录制与时间轴不要拆。
  7. 抽帧策略要省。 一次请求能带回的图片有限,优先抽"转场帧 + 关键动作 + 片尾",别平均分配。

7. 适合做什么视频,不适合做什么

7.1 结构性适合的四类

题材 为什么成立
产品/系统演示片(UI 走查、功能验收、售前演示) 证据都在屏幕上;路由与选择器可驱动;每个操作点都能用注入层点出来
网页/小游戏试玩片(竖屏为主) 交互语义明确;可读数据文件与 debug bridge 推进状态;无旁白时音画对齐难点归零
数据/项目集/年度总结类信息片 画面 = 几何 + 文字 + 图表,天然可程序化;版式重复度高、可模板化
带口播讲解的教学/培训片 TTS 时长驱动片段时长,字幕按字数比例切句、ASS 烧录,全链路确定性可重跑

7.2 结构性不适合的

题材 卡在哪
真人出镜 / 实拍素材 流水线里没有采集卡、没有相机、没有人脸处理;这条路上没有任何一次尝试
强叙事、需要表演的镜头语言 表现手段只有"卡片 + Ken Burns + 光标/涟漪/聚光灯/气泡";没有景别切换、没有多机位、没有运镜设计
需要精确口型的数字人 TTS 只产出音轨,不做口型;这类需求应当换成数字人方案
复杂 3D / 物理仿真 / 特效合成 代码渲染路线画的是"装饰性的球体",不是 3D 场景;没有 GPU 渲染管线
帧级时序敏感的内容(卡点踩鼓、逐帧对白) 路线 A 的帧率由内容驱动、路线 B 的基准会整体平移;现有容差是"秒级"(0.5--1 s)

7.3 一条更锋利的判据

如果只能记一句话:

画面能不能被"几何 + 文字 + 一次真实操作过程"描述完?能,就适合;不能,就别指望。

再加两条工程判据:

  • 证据在屏幕上吗? 在屏幕上(数字、状态、图表)→ 录制路线最优;只在心里(叙事、情绪)→ 这条路走不通。
  • 排版精度要求多高? 元素少、版式重复 → 可用;元素多、文本长度动态、要求像素级对齐 → 返工成本会指数上升(根因是没有布局引擎)。

8. 给想复刻这套流水线的工程师:10 条经验

  1. 先探环境,再定方案。 ffmpeg 的构建年份决定你能不能用 xfade、能不能烧 ASS 字幕、有没有 libx264。
  2. 把"画面"和"时间轴"在数据结构上分开。 前者是素材(图片/视频/渲染函数),后者是一张表(场景 id、起止、音频、cue)。
  3. 时长由音频驱动。 有旁白就"TTS 先跑、画面后定";无旁白就"拍号/网格先定、两边都从同一个整数算"。
  4. 每段独立编码,失败可单场景重录。 别做一条 16 分钟的大命令,那会让任何一次返工都变成全片重来。
  5. 静态图叠加永远不要 -loop 1。 用 enable='between(t,a,b)' 门控,并把叠加图预裁到最小包围盒。
  6. 抽帧自检要成为固定动作,不是应急手段。 每次合成后抽 5--6 帧(开头、转场、关键动作、片尾),读回来看。这一步抓到的 bug 密度最高。
  7. 把验收标准写成可执行断言。 "二维码能扫出目标 URL""响度在 −20...−23 LUFS""无 ≥1 s 静音""帧数对得上"------比"看起来不错"有用一百倍。
  8. 注入层用 pointer-events:none + 最高 z-index,并与真实输入事件双轨同步;canvas 应用要合成 MouseEvent + PointerEvent 两条通道。
  9. 并行的判据是文件互斥 + 可书面验收。 满足就拆,不满足就别拆;子代理会花掉 30%--50% 的 token。
  10. 给剪辑点留复核位。 相关性检测器可以自动找切点,但阈值一定需要被数据校准。

9. 局限与风险

诚实地列出这条路线暴露出来的问题:

  1. 时序精度是"秒级",不是"帧级"。 墙钟反锚会整体平移;screencast 帧率浮动;TTS 时长与画面裁剪互相让步。想做逐帧卡点的片子,需要换更硬的时序机制(把 mark 打在视频时间轴上,或用固定帧号渲染)。
  2. 排版没有布局引擎。 程序化渲染路线的坐标是手写常量,文本一长就压盖,只能靠人眼盯帧发现------这是它最大的一次性成本来源。
  3. 对外部数据源依赖很脆。 抓取类任务会撞 403 限流、SSRF 防护(域名解析到非公网地址直接被拒)、以及"页面结构已变"导致的正则全返回 0;最后往往要靠页面内嵌 JSON 才能拿到真值。
  4. 工具链版本决定能力上限。 没有 xfade 就只能硬切;没有 sidechaincompress 就做不了旁白避让;没有 libass 就烧不了字幕。这不是模型的问题,是机器的问题。
  5. 验收依赖"看图",而看图有盲区。 模型能从帧里看出"字压住了二维码",但听不出"这段配乐好不好听"------音频质量最终要靠频谱图、响度表、以及"放大频谱抓 bug"这种间接手段兜底。
  6. 失败会很好看地发生。 -loop 1 那次事故的产物是一个"3.6 秒的坏文件",如果不做时长/moov 检查,它会被当成成功交付。

10. 结语

如果只看结果,会以为这是"AI 生成视频"。拆开工程才知道,它真正做的是把视频工程变成可编程、可观测、可迭代的软件工程:

  • 用一个统一工具面把几十个异构工具收进一段程序;
  • 用 CDP screencast / Playwright recordVideo / 代码逐帧渲染三条路线解决"画面从哪来";
  • 用时间戳重建、打点反锚、拍号表三种机制解决"时间轴从哪来";
  • 用抽帧看图、二维码解码、响度测量、相关性检测解决"怎么知道做对了";
  • 用文件互斥 + 可书面验收的任务卡决定"什么能并行"。

它的成本结构也很诚实:token 的大头是重读上下文,钱的大头花在返工;而返工的根源,一半是环境的不确定性,一半是"排版与审美的判断还没被自动化"。

所以更准确的说法是:这套组合现在能稳定交付的,是"信息型视频"------把屏幕上已经存在的事实,用可复现的工程手段搬进一条时间线,并自己检查它有没有搬对。 至于"讲故事",那还是人的活。


附录 A:口径与数据来源

Token 口径 :逐条累加每次模型响应的 usage,分 inputTokens(未命中输入)、cacheReadTokens(命中输入)、outputTokens(输出)三项;三者之和应等于该次请求的 totalTokens(本文引用的数据全部通过这条一致性校验)。

耗时口径 :从首次 turn/start 到末次 turn/end 的墙钟差,含空档;讨论"实际干了多久"时用 turn 并集(去掉被中止的 turn)。

成本口径 :采用 DeepSeek 开放平台官方价目表(api-docs.deepseek.com/zh-cn/quick_start/pricing,查询于 2026-10-22),模型取表中 deepseek-flash(对应 DeepSeek-V4.1-Flash-1017)。单价为元 / 百万 tokens:

计费项 空闲时段 高峰时段
输入(缓存命中) 0.02 0.04
输入(缓存未命中) 1.00 2.00
输出 4.00 8.00

高峰时段按官方定义:北京时间周一至周五(不含中国法定节假日)9:00--12:00、14:00--18:00;其余时段为空闲,价格为高峰的一半。计费时逐条消息按其自身时间戳判定时段,因此得到的数字既不是"全按空闲",也不是"全按高峰"------同一批数据若全落空闲为 ¥22.0、全落高峰为 ¥43.9,逐条计价为 ¥31.5。

注:官方同时公布了并发限制(表中该档为 2,500)与网页搜索/抓取(Search / Fetch 各 5 元每 1,000 次成功调用)的价格;本文的成本只涉及 token,不含联网检索费用。

成片参数 :由 ffprobe(或等价的 MP4 box 解析:mvhd/mdhd/stsd/stts)读取,并与交付说明交叉核对;体积/码率沿用实测值,MiB 与十进制 MB 之间可能相差几个百分点。

未计入项 :标题生成、翻译插件等旁路模型调用不走主 usage 记账,未计入,对总量影响在千分之几量级。

关于"看图次数" :不要用 image/offload 事件数去估算"模型看了多少图"------大量读图发生在 run_code 内部的 read_image 调用里,不产生独立事件;要看实际读了哪些文件,得去翻工具调用的参数。

附录 B:关键参数速查

项 路线 A(screencast) 路线 B(recordVideo) 路线 C(代码渲染)
典型分辨率 1920×1080 1080×1920(竖屏,由 size 决定) 1920×1080(设计空间)
帧率 录制 15--36 fps → 重建为 CFR 30 录制 25 fps → 交付 30 fps(重采样) 30 fps 直接出帧
编码 libx264 crf17--18, veryfast→medium, yuv420p, +faststart libx264 crf17--18 + aac 192k rawvideo 管道 → libx264 -preset medium -crf 18 -profile high -level 4.2
音轨 edge-tts 旁白 + ASS 字幕 本地合成 BGM 本地合成 BGM(拍网格对齐)
转场 fade + concat fade + 硬切(旧版 ffmpeg 无 xfade) 场景内 sweep / 遮罩过渡
时间轴 帧时间戳 → concat demuxer → -vsync cfr marks.json → D - (T_CLOSE - t) 反锚 拍号表 CUTS(BPM 驱动)
自检 抽帧 + 像素自检 + 黑场/静音 + 响度 抽帧 + 二维码解码 + 帧数断言 亮度统计 + 切点偏差 + 二维码静区

文中的参数、命令与数值均为工程实测摘录;未验证的能力一律不做推断。价格取自 DeepSeek 官方价目页(2026-10-22),按逐条消息的时间戳区分高峰/空闲时段计价。

相关推荐
坤盾科技2 小时前
DeepSeek 开源模型矩阵深度拆解:不追“全能“,用架构差异重新定义能力边界
架构设计·moe·开源模型·deepseek·推理模型·engram
玫瑰互动GEO5 小时前
把GeoReady九维当工程项做:储能管理官网的AI就绪度改造笔记
人工智能·ai·ai搜索·seo优化·豆包·deepseek·geo优化
大模型真好玩21 小时前
初试短视频生成:豆包将我梦中的场景画出来了~
人工智能·agent·deepseek
ServBay1 天前
DeepSeek 又出王炸,用 DeepInfer 搭建自己的服务是不是很容易?
后端·ai编程·deepseek
comedate1 天前
DeepSeek API 接入教程:申请、Python 调用与常见坑
大模型·api·deepseek
Web3&Basketball1 天前
DeepSeek V4.1 Flash 实测:Token 砍半、Search API 跑通
数据库·人工智能·python·大模型·api调用·deepseek·推理优化
明月_清风1 天前
拆解 DeepSeek 最新开源推理框架 DeepInfer:万亿级大模型的高效落地实战
人工智能·后端·deepseek
浅安的邂逅1 天前
261020-DeepSeek 双线出击:V4.1-Flash 登顶 Agent Arena,推理框架 DeepInfer 全面开源
人工智能·开源·deepseek·ai日报·产品发布/更新
invicinble1 天前
dsh对比codex的机制
dsh