一条把选题变成动漫二创成片的命令行流水线(默认目标 2–4 分钟,可按选题体裁覆盖)。输入是一句选题,输出是可以直接上传的 mp4、封面图和标题候选。中间的写稿、配音、找素材、剪辑、质检由脚本和 coding agent 完成, 人类在 02.5(人审改稿)、03.5(配音顺听)、05(审时间码)与 09(发布)四处介入。
目录
文档索引:每份文档管什么 → docs/INDEX.md
一、项目介绍 —— 它做什么、人在哪里出现、核心设计取向、不做什么
data/ 放在哪numpy<2.5 的来由选题 → 写稿 + 稿件机检 → 本地 TTS 配音 → 语义检索排画面 → [人审时间码]
→ 渲染(切片 + 烧字幕 + 混 BGM)→ 11 项自动质检 → 封面候选 + 标题 → [人选并发布]
每一步都是一条独立命令,产物带编号落盘在 data/episodes/<本期>/(01-topic.md →
07-cover/)。看目录里有哪些文件,就知道进行到哪一步。
没有数据库、没有状态文件、没有任务队列,任何一步失败,修好之后从那一步接着跑。
| 步骤 | 谁做 | 时长 |
|---|---|---|
| 周批量选题(摊到每期) | 人 | 1.5 分钟 |
| 写稿、配音、排片 | Agent | 约 6 分钟机器时间 |
| 02.5 人审改稿 | 人 | 约 3–5 分钟 |
| 05 审时间码 | 人 | 5 分钟 |
| 渲染、质检、封面 | Agent + 机器 | 约 2.5 分钟机器时间 |
| 09 选封面、选标题、上传 | 人 | 3 分钟 |
设计目标是人类每期投入 ≤ 10 分钟(尚未端到端验证)。超预算不当作「这期多花点时间」, 而是当作流水线有 bug——改流水线或砍掉那个环节。
几乎所有难修的 bug 都有同一个特征:它不报错。ffmpeg 截取超出片尾时静默截断;
检索找不到画面照样返回 Top-K;索引只建了 6 集而片源有 41 集时成片照常渲染。所以项目的
重点不在功能,而在判据:每加一道检查,先问它测的是不是真实产物;每引入一个分数,
先问它能不能用来排序。完整证据见 docs/STANDARD.md。
| 项目 | 要求 | 不满足会怎样 |
|---|---|---|
| coding agent | 能跑 shell、能读图 | 第 02 步写稿没有对应脚本,缺了它这一步就是空的。详见 2.2 |
| 操作系统 | macOS | 只在 macOS 上跑过 |
| 处理器 | Apple Silicon(配音与 ASR 需要) | 装不上 mlx,配音与 ASR 兜底不可用;其余步骤能跑 |
| Python | 3.12 / 3.13 / 3.14 | 3.15 还在 beta,没验过。numpy<2.5 上界不能删,理由见 2.6 |
| ffmpeg | 必须带 libass | 烧不了字幕,渲染会在最后一步失败。brew install ffmpeg 的默认版本带 |
| 包管理 | uv | 也可以用别的,但依赖版本是按 uv 那一组钉住的 |
语音部分只跑 Apple Silicon 是硬限制。config/voice.json 留了 engine 字段作为接缝,
目前有两种 mlx 实现:IndexTTS-1.5(纯中文)与 Qwen3-TTS 1.7B(多语种,2026-08-15 起为默认)。
这个仓库的预期用法是 clone 下来交给成熟的 Coding Agent 驱动,不是一个人对着终端逐条手敲命令。 实战中默认推荐 Pi(配合 Gemini 3.8 / Kimi K3,已稳定生产并发布 15 期全网视频),亦完全兼容 Claude Code 等通用 Agent。
仓库里有两份东西专门写给 Agent 看:
| 文件 | 给 Agent 的内容 |
|---|---|
AGENTS.md / CLAUDE.md | 工程约定:六条红线、四阶段人工停机点、台词因果哈希、哪些判据不许绕过 |
skills/write-script/ | 写稿技能:五种题材骨架与判据、8–12 秒微单元标准、气口起伏量化 |
各步骤对 Agent 的依赖程度不同:02 写稿没有 Agent 不行(没有任何脚本会生成稿子); 08 挑封面候选和写标题可以没有 Agent,但会退化成机械铺开取样、不一定切题;其余各步都是 纯脚本。对 Agent 的能力要求有两条是实的:能跑 shell、能读图(第 08 步要看 6×5 的联系表认人)。不能读图的 Agent 仍能跑完整条流水线,只是封面挑选退化成机械取样。
模型全部落在 data/models/ 下,不落系统盘——由 pipeline/paths.py 在 import 时把
HF_HOME 钉过去。
| 模型 | 用途 | 大小 | 怎么来 |
|---|---|---|---|
BAAI/bge-base-zh-v1.5 | 字幕语义索引与检索 | 约 400M | 首次运行自动下载 |
mlx-community/whisper-large-v3-turbo | 回读质检、字幕对轴校验、无字幕时兜底转录 | 约 1.5G | 首次运行自动下载 |
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16 | 配音(多语种,中文旁白 + 英日歌名混读) | 约 4.3G | 改 config/voice.json 自动下载 |
deepghs/anime_face_detection | 视觉索引:人脸检测 | 约 42M | 首次运行自动下载 |
deepghs/ccip_onnx | 视觉索引:角色身份嵌入 | 约 143M | 首次运行自动下载 |
注意 import 顺序:任何会下载模型的模块,第一个 import 必须是 pipeline.paths,否则
模型会静默写进系统盘。paths.py 会检测并当场报错。
仓库不包含也不分发任何素材。
| 素材 | 要求 |
|---|---|
| 片源 | 一部番的完整正片,几十 G 量级 |
| 字幕 | 时间轴必须照你手上那个视频文件打的。 只允许三种来源:ffmpeg 提取的内封字幕;对该文件跑 ASR 转录的结果;与该文件同属一个发布、文件名逐一对应且通过对轴校验的外挂字幕。日语无字幕片源不支持(ASR 兜底仅中文,见 ADR-0007) |
| 参考干声 | 5–8 条,每条 8–10 秒,连续说话不要留大段静音。用来克隆口播音色 |
| BGM | 该番自己的 OST / OP / ED。整轨镜像加 cue 的碟需要先分轨 |
永远不要用第三方字幕文件配第三方视频文件。 跨版本差异是中间的插入与删除,不是恒定 偏移,加 offset 修不好。而帧级精确截取是渲染链路的前提,前提一塌视频照样渲染, 只是全程画面对不上话,没有任何报错。
data/ 放在哪仓库本身只有纯文本(代码、配置、文档、skills),全部进 git。片源、字幕、索引、模型、
每期产物都在 data/ 下,不进 git。两种放法都支持:
./pipeline/preflight.sh --init # 系统盘装得下,建实体目录
./pipeline/preflight.sh --init /Volumes/<你的盘>/anime-video-data # 外置盘 + 符号链接
脚本绝不自动创建 data/,不可达就立刻报错退出。 符号链接指向未挂载的卷时,mkdir -p
会把几十 G 静默写进系统盘且盘插上后看不见——建骨架必须是显式动作,preflight.sh --init
会打印结果落在哪块盘上。代码里一律用 ./data/... 相对路径,不硬编码卷路径。
brew install ffmpeg
uv venv --python 3.14 # 3.12 / 3.13 / 3.14 都可以
uv pip install -e ".[apple,dev]" # 非 Apple Silicon 去掉 apple
pytest # 纯函数测试,不碰片源/模型/外置盘
./pipeline/preflight.sh # 自检
cp CLAUDE.local.md.example CLAUDE.local.md # 填本机情况,不进 git
pytest 首次跑要一分钟左右,之后约 3 秒。preflight.sh 检查 data/ 可达、ffmpeg 在、
ffmpeg 真的带 ass 滤镜——有 ffmpeg 不等于能烧字幕,缺 libass 渲染会在最后一步才失败。
CLAUDE.md 是给 coding agent 读的工程约定,跑这个项目的人也建议读一遍。CLAUDE.local.md
放只对你这台机器成立的事实,不进 git。
numpy<2.52026-07-30 实测,3.12、3.13、3.14 三个版本都跑通,3.15 还在 beta,没验。
pyproject.toml 里那条 numpy>=2.0,<2.5 的上界不能删。依赖链
mlx-whisper → numba → llvmlite 里 numba 0.66 要求 numpy<2.5;numpy 2.5 一发布,
解析器就优先选它,把 numba 0.66 排除,回退到只支持 Python <3.10 的版本,转去源码构建
并失败。这个坑只咬新环境——已有 venv 一切正常不构成证据。判据得是真装一遍:
uv pip install --dry-run 只解析不构建,会「成功」地解出装不上的组合。
| 环节 | 用什么 | 为什么这么选 |
|---|---|---|
| 语义检索 | sentence-transformers + bge-base-zh-v1.5 | bge-small 实测命中率只有 75%,base 是 100% |
| 索引单元 | ASS 字幕滑窗 2 行,一集一个 .npy | 单行太碎,一句话常跨两行轴 |
| 配音 | mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16 跑在 mlx-audio 上(多语种,中文旁白夹英日歌名混读) | 云 API 把联网+账号+配额塞进主循环,还要外传参考干声 |
| ASR | mlx-whisper large-v3-turbo | 回读质检、对轴校验、兜底转录三处都用 |
| 视频与音频 | 全程 ffmpeg,无 NLE | 见 1.4 |
| 字幕烧录 | ass 滤镜(libass),折行自己算 | libass 靠空格找断点,中文整句没有空格就不折 |
| BGM | 侧链闪避,按实测响度归一到 -26 LUFS | 固定音量系数是错的控制量 |
| 镜头切分 | ffmpeg scdet,低阈值扫一遍再过滤 | 检索单元必须是镜头不是帧;改阈值不用重解码 |
| 角色在场 | 动漫人脸检测 → CCIP 嵌入 → 聚类 → 人工贴名 | booru tagger 词表大的认不准、认得准的词表小 |
| 封面筛选 | Pillow:清晰度/亮度/去重,可选角色在场过滤 | 只做剔除,不做排序 |
| 状态管理 | 文件系统 | 产物即状态。人机交接一律走文件 |
| 测试 | pytest,纯函数测试(条数见 CI / pytest --collect-only) | 不 mock ffmpeg 和模型,mock 检查的是 mock |
没有的东西也是选择:没有云 API、没有数据库、没有编排框架、没有 web 界面、没有一键脚本。
成本固定,摊到多期上才划算。一部番榨干再换番。 完整判据见 docs/WORKFLOW.md。
python -m pipeline.ingest intact data/library/raw/<番>/*/*.mkv
下载器显示 100% 不等于文件完整。 BT 没下完的文件是稀疏文件,ffprobe 照样读得出时长。
判据是 ffmpeg 全片解复用无报错(-c copy -f null -),约 0.9 秒一个文件。「实占块 ÷
逻辑大小」只当前置快筛:说「没下完」可信,说「下完了」不可信。
python -m pipeline.ingest phase0 data/library/raw/<番>/<该季目录>/*.mkv \
--anime <番> --season 1
一条命令做三件事:逐集对轴校验 → 过了才建索引 → 登记片源进 sources.json,三步不许拆开,
任何一集对轴不过就跳过并说明原因,不静默降级。对轴判据是 ASR 回读比对字符错误率——文件
名对应只能证明「打包时放在一起」,真正能证伪的只有拿视频自己的声音去对。需要字幕带日文轨,
只有纯中文字幕的集走 ASR 兜底或者放弃。
字幕索引答的是「谁说了什么」,画面里有谁它答不了。这一步补上(设计见 ADR-0003),机器时间约 2.4 小时:
python -m pipeline.shots calibrate <一集视频> # 定切分阈值,看抽检图再拍板
# 选定的数写进 config/project.json 的 visual.scene_threshold
python -m pipeline.shots build <视频> --anime <番> --season N --episode M # 逐集切分
python -m pipeline.shots frames <番> <集号> # 抽代表帧
python -m pipeline.faces detect <番> <集号>... # 人脸检测
python -m pipeline.faces cluster <番> && python -m pipeline.faces sheet <番> # 聚类出联系表
python -m pipeline.faces name <番> 0 八幡 # ← 人在这里:看图给每簇贴名(约 2 分钟)
python -m pipeline.faces presence <番> # 落索引
第 5 步的人工介入是设计的一部分,不是妥协:一部番只做一次,姐妹脸这类只有人能一眼 分开。不用现成 booru tagger 的原因:词表大的置信度太低,认得准的词表里连主角都没有。
python -m pipeline.vindex status --anime <番>
# 片源 / 字幕索引 / 笔记 / 镜头表 / 角色在场 / 画面语义
六条数字不相等就不许进每期循环。 索引或笔记悄悄缩水时,检索照常返回 Top-K、分数照常 在阈值以上、成片照常渲染,全程没有任何报错——判据不能是「检索得到东西吗」, 必须是「集数对得上吗」,前者永远为真。
for f in data/voice/reference/*.wav; do
python -m pipeline.tts probe "随便一句带长句和短判断的话" \
--ref "$f" --out "data/voice/probe/$(basename "$f")"
done
按重要性听三样:语速(不同参考音实测差 65%,直接决定成片长度)、稳不稳(电音、
忽快忽慢、句尾发飘)、音色(排最后,听的是「能不能撑 2–4 分钟口播」)。单句试音
不能预测整篇语速,估成片长度必须看 manifest.json 的总时长。选定后改
config/voice.json 的 ref_audio;script.cpm 要重测(同一音色不同文风下差过 24%)。
python -m pipeline.bgm scan <CD 目录> # 解 cue,列出 instrumental 轨
python -m pipeline.bgm extract <CD 目录> --anime <番> # 切成独立 flac
python -m pipeline.bgm measure data/library/bgm/<番>/*.flac # 量时长、响度、入声点
Phase 0 只攒候选池不 定死选曲(2026-08-08 起):具体每期用哪首,等这期配音(03)出来、
人耳听过之后填进当期 01-topic.md——BGM: 列表每行一首,@N秒 定起点(不写自动接上一首
结尾),支持任意首数拼接;老写法 BGM正文 / BGM中段(配 BGM中段切入点: N秒)/ BGM结尾
仍可用。扫曲库必须解 cue,不能只数文件(OP/ED 单曲碟是整轨镜像)。曲目表里没有实测
lufs 的曲子不许用(劇伴与 OP/ED 伴奏实测极差 11 dB)。乐评类期数还有试听型渲染
(pipeline/music):音乐段当前景、段落间降 BGM、结尾自然收尾。
01-topic.md 必须带齐四项:番、类型(人物志 / 剧情回顾 / 杂谈 / 盘点 / 共鸣)、锚点
(季集号),可选 封面集。杂谈还要带 模式(驳论 / 立论 / 吐槽)与 张力(驳论/立论)。
张力是整条流水线里唯一的编辑判断——必须在这一步定死并经人批准,不能留到写稿阶段由 agent
自行发明(那等于绕过唯一的人工关卡)。人物志/剧情回顾/共鸣不设张力:观点就是人物/故事本身。
写稿只校验、不更换。
调 skills/write-script:读上一期的稿子(为了不重样)→ 查证剧情 → 按 类型 读第 4 节对应骨架
(杂谈先校验 模式/张力)→ 按题材骨架写正文 → 切成 8–20 段,每段配一条 查询 和一条 备选。
五种题材五种骨架,全部从真实优秀内容里拆的(人物志=罗十五;剧情回顾/杂谈/盘点/共鸣=bangumi 长评与知乎长文)。骨架是「这类内容怎么走通」的范本,不是分步模板——写的时候像人说话, 写完再拿判据筛。
python -m pipeline.check_script data/episodes/<本期>/02-script.md
机检覆盖:字数、时长、单句上限、句长起伏、论文连接词、跨期套话、论据是否报菜名、剧情
锚点数、查询是否重复、查询是否误写成画面构图等。主观项机器判不了,脚本按
01-topic.md 的 类型 给出对应提示(杂谈看张力/公道话,人物志看有没有给角色安论点……)。机检全过不代表能交,自检两遍顺序不能反:先保真(台词
原文、说话人、集号、数字),再去味。查询 是机器接口,不是备注——它被原样送进
字幕索引检索,要写成台词语义:「角色承认自己一直在逃避」有效,「中景,逆光」必然落空,
字幕里没有构图信息。
python -m pipeline.tts data/episodes/<本期>
逐段合成,保留段落边界。每段时长用 ffprobe 复核后写进 03-audio/manifest.json——
这是下一步排画面的唯一依据,不采信模型自报。中断可直接重跑,已过的段落跳过;
改稿、换音色(engine/model/ref_audio/读音表)后再跑都只重做受影响的段。
按句合成不按段(语速与文本长度正相关)。
每段生成后用 Whisper 回读比对,抓漏读、重复、跑飞;不过就换种子重生成,三次仍不过
就整体退出——自回归 TTS 的典型失败不是报错,是静默地念错。回读比对前会归一化,而归一化
可能把缺陷一起抹掉,所以合成前用 tts.speakable 剥掉纯书面符号。成片交人前至少听一遍
开头和最长的那段。
python -m pipeline.clips data/episodes/<本期>
每段 查询 送进索引取 Top-24,然后全局贪心分派:所有 (段, 画面, 分数) 摊平按分数
降序占坑,同一处画面整期只用一次——不按段落顺序分,那是先到先得。每段按音频真实时长
填满,片段不短于 2.5 秒。顺序不可交换:字数估算与实际语速的偏差攒出几秒的渐进错位,
越到后面画面与口播差得越远,全程不报错。
检索落空走查询阶梯:查询 → 备选 → 该段 配音 原文,逐级重试,门槛 0.45 不动。
阶梯只救不比——上一级够格就到此为止,绝不因为下一级分数更高而换掉。三级都不及格就
写 "status": "no_match",渲染硬失败交人处理,不自动降级——自动塞空镜是把「画文不符」
藏起来。
分镜可以声明走哪条通道(ADR-0003),二选一,都不写就走台词检索:人物: 雪乃 = 台词检索
场景: 描述 = 画面语义检索,
当前不可用——门槛立不住,CLIP 余弦能排高低、答不了「这算不算命中」(ADR-0003 待实测
#4),写了会当场报错。两条铁律: 一段只走一个通道,两个字段都写会当场报错;分数只在自己的通道内排序,绝不 跨通道比较。分派画面时台词段先挑——它们要的是「那句话发生的那一刻」,具体且不可替换。
python -m pipeline.review data/episodes/<本期> # 出 04-review.html
open data/episodes/<本期>/04-review.html
python -m pipeline.review data/episodes/<本期> --approve
这是整条流水线唯一的人工关卡。 一页 HTML,每段并排口播、画面、字幕原文三样,每个片段
抽三帧(进点、中间、出点)。要抓的是台词对了但画面不对:那句话可能是画外音,机器判
不了,只有眼睛能抓。--approve 必须是显式动作——自动写 approved 是最省事的做法,
也正是这一关曾经形同虚设的原因。
python -m pipeline.render data/episodes/<本期>
切片 → 统一中间格式 → 拼接 → 混 BGM → 烧字幕 → 响度归一,一趟出片。40 个片段、3 分钟 成片实测 77 秒。
两道截取守卫强制执行,缺一不可: 切前校验 seek + duration <= 源时长,不足立即失败;
切后复核实际时长与请求值之差不超过 1 帧(按源片实际帧率现算,不许写死——番剧常见
23.976 / 24 / 29.97 三种帧率)。
字幕折行自己算,不指望 libass(它靠空格找断点,中文没有空格就不折):断点取标点, 贪心取放得下的最远那个;中文破折号是两个字符,必须等一对补全再断。
BGM 支持任意首数拼接:01-topic.md 的 BGM: 列表每行一首、@N秒 定起点,后一首
起点早于前一首自然结尾就等功率交叉(qsin,3 秒),晚了留停顿;老写法(正文/中段/结尾
槽位)仍可用,换曲点卡段落边界。每首按实测响度归一到 -26 LUFS,再以人声为触发做侧链
闪避。固定音量系数是错的控制量——换一首响度不同的曲子会凭空大 9.8 dB 压过口播,
且不报错。
python -m pipeline.qc data/episodes/<本期>
11 项,不达标非零退出,不进人工环节:时长落在目标带(默认 2–4 分钟,01-topic.md
写了 时长目标 就按那个带判,见 CLAUDE.md「内容参数」);音画误差 < 0.5s;响度 -16 LUFS、
true peak ≤ -1 dBTP;无 >0.5s 黑帧、>2s 静音;字幕无空段、不超宽、每卡一行、不超高。
查的是实际折行结果,不是「原文长度 ÷ 每行容量」——检查项一旦测的不是真实产物,
通过就毫无意义。主观项(节奏、观感)不进门禁,根在稿子和排片,回 02/05 改。
python -m pipeline.cover data/episodes/<本期> # 出池子 + 联系表
python -m pipeline.cover data/episodes/<本期> --pick 107,110,20 # 定 9 张
这一步不新增人工环节,选封面和选标题并进第 09 步。
候选帧从三处取: 实际用上的片段(每 0.5 秒一帧)、名场面时间码、01-topic.md 的
封面集 通篇(每 12 秒一帧,跳过 OP/ED)。第 3 路是被打脸打出来的:只用前两路时九张里
没有一张主角占主位——前两路的帧都来自字幕检索命中的时刻,而台词大多是别人说给主角听的,
镜头自然在说话人身上。
机器只做剔除,逐条有明确判据: 拉普拉斯方差 < 60 剔运动模糊;平均亮度落 40–215 之外
剔黑场过曝;同集相隔 < 6 秒或 dHash 相近算同一镜头只留一张;可选按 封面人物: 一色彩羽
(或 --character)硬过滤掉没有本期主角的帧(硬过滤为空则失败,与排片的软过滤相反);
2 个坑留给名场面,其余按段落号均分取样。候选之间没有排名,这是刻意的——拉普拉斯方差
测的是「画面里有多少细节」,不是「画面有多好」,两版打分都压掉了正确答案;清晰度是准入门槛,
不是排序依据。
分工是三层,不是两层: 机器剔黑场/过曝/糊帧、去重、编号 → agent 看联系表(6×5 一张、
带编号)挑出符合本期主题的(认人、占不占主位)→ 人从 9 张里定一张(审美)。agent 看完
报编号给 --pick;不加 --pick 时退到机械铺开取样,仍然出得了 9 张。
标题给 5 条候选,每条注明路子(下判断 / 反问 / 复述再打 / 剧情钩子 / 金句),原料取
稿件的第 2 节(亮判断)和第 8 节(结论)。判据是金句式而非论文式——这句话能不能脱离
视频单独发出去。07-titles.md 已填表就不再覆盖(标题和封面同一条命令出,重跑会冲掉)。
点一张封面、点一条标题,然后上传。上传手动做,不自动化。(2026-08-09 起本期耗时 由 agent 在会话内记录,不再落盘 meta.md。)
pytest # 纯函数测试,约 3 秒,不碰片源、模型、外置盘
测试只覆盖纯函数,测的是踩过的坑,不是 API 形状——每条断言对应一个具体错误。不为了 提覆盖率去 mock 掉 ffmpeg 或模型,涉及媒体的正确性交给截取守卫和质检门禁在真跑时兜。
写测试两条纪律:期望值先在实现上跑一遍、看懂为什么是这个值,再写进断言;写完做变异检验 ——把被测行为故意改坏,看测试红不红(一条永远绿的断言和没有断言效果一样)。
config/project.json 的 anime.defaultconfig/bgm.json 加一组曲目,每首必须带实测 lufsconfig/characters.json 加这部番的角色名表(中文名 ↔ 聚类贴的名字)clips 和 subindex search 都按番过滤换成真人影视素材时,视觉索引第 1 层要整块换掉(动漫人脸检测器和 CCIP 都是动漫专用的), 这是有意做成可插拔通道的原因。
skills/write-script/VOICE.md 是空模板,教你怎么从自己的既有文稿里提炼一份,填好放
VOICE.local.md(不进 git)。不填也能出稿,但会是通用 AI 腔。
MIT,见 LICENSE。
素材本身(片源、字幕、音乐)不属于本仓库,也不随仓库分发。 二创的版权风险由使用者
自负。config/bgm.json 里的曲目表只有路径和实测响度,不含任何音频。
100 commits
Python
99.3%
一条把选题变成动漫二创成片的命令行流水线(默认目标 2–4 分钟,可按选题体裁覆盖)。输入是一句选题,输出是可以直接上传的 mp4、封面图和标题候选。中间的写稿、配音、找素材、剪辑、质检由脚本和 coding agent 完成, 人类在 02.5(人审改稿)、03.5(配音顺听)、05(审时间码)与 09(发布)四处介入。
目录
文档索引:每份文档管什么 → docs/INDEX.md
一、项目介绍 —— 它做什么、人在哪里出现、核心设计取向、不做什么
data/ 放在哪numpy<2.5 的来由选题 → 写稿 + 稿件机检 → 本地 TTS 配音 → 语义检索排画面 → [人审时间码]
→ 渲染(切片 + 烧字幕 + 混 BGM)→ 11 项自动质检 → 封面候选 + 标题 → [人选并发布]
每一步都是一条独立命令,产物带编号落盘在 data/episodes/<本期>/(01-topic.md →
07-cover/)。看目录里有哪些文件,就知道进行到哪一步。
没有数据库、没有状态文件、没有任务队列,任何一步失败,修好之后从那一步接着跑。
| 步骤 | 谁做 | 时长 |
|---|---|---|
| 周批量选题(摊到每期) | 人 | 1.5 分钟 |
| 写稿、配音、排片 | Agent | 约 6 分钟机器时间 |
| 02.5 人审改稿 | 人 | 约 3–5 分钟 |
| 05 审时间码 | 人 | 5 分钟 |
| 渲染、质检、封面 | Agent + 机器 | 约 2.5 分钟机器时间 |
| 09 选封面、选标题、上传 | 人 | 3 分钟 |
设计目标是人类每期投入 ≤ 10 分钟(尚未端到端验证)。超预算不当作「这期多花点时间」, 而是当作流水线有 bug——改流水线或砍掉那个环节。
几乎所有难修的 bug 都有同一个特征:它不报错。ffmpeg 截取超出片尾时静默截断;
检索找不到画面照样返回 Top-K;索引只建了 6 集而片源有 41 集时成片照常渲染。所以项目的
重点不在功能,而在判据:每加一道检查,先问它测的是不是真实产物;每引入一个分数,
先问它能不能用来排序。完整证据见 docs/STANDARD.md。
| 项目 | 要求 | 不满足会怎样 |
|---|---|---|
| coding agent | 能跑 shell、能读图 | 第 02 步写稿没有对应脚本,缺了它这一步就是空的。详见 2.2 |
| 操作系统 | macOS | 只在 macOS 上跑过 |
| 处理器 | Apple Silicon(配音与 ASR 需要) | 装不上 mlx,配音与 ASR 兜底不可用;其余步骤能跑 |
| Python | 3.12 / 3.13 / 3.14 | 3.15 还在 beta,没验过。numpy<2.5 上界不能删,理由见 2.6 |
| ffmpeg | 必须带 libass | 烧不了字幕,渲染会在最后一步失败。brew install ffmpeg 的默认版本带 |
| 包管理 | uv | 也可以用别的,但依赖版本是按 uv 那一组钉住的 |
语音部分只跑 Apple Silicon 是硬限制。config/voice.json 留了 engine 字段作为接缝,
目前有两种 mlx 实现:IndexTTS-1.5(纯中文)与 Qwen3-TTS 1.7B(多语种,2026-08-15 起为默认)。
这个仓库的预期用法是 clone 下来交给成熟的 Coding Agent 驱动,不是一个人对着终端逐条手敲命令。 实战中默认推荐 Pi(配合 Gemini 3.8 / Kimi K3,已稳定生产并发布 15 期全网视频),亦完全兼容 Claude Code 等通用 Agent。
仓库里有两份东西专门写给 Agent 看:
| 文件 | 给 Agent 的内容 |
|---|---|
AGENTS.md / CLAUDE.md | 工程约定:六条红线、四阶段人工停机点、台词因果哈希、哪些判据不许绕过 |
skills/write-script/ | 写稿技能:五种题材骨架与判据、8–12 秒微单元标准、气口起伏量化 |
各步骤对 Agent 的依赖程度不同:02 写稿没有 Agent 不行(没有任何脚本会生成稿子); 08 挑封面候选和写标题可以没有 Agent,但会退化成机械铺开取样、不一定切题;其余各步都是 纯脚本。对 Agent 的能力要求有两条是实的:能跑 shell、能读图(第 08 步要看 6×5 的联系表认人)。不能读图的 Agent 仍能跑完整条流水线,只是封面挑选退化成机械取样。
模型全部落在 data/models/ 下,不落系统盘——由 pipeline/paths.py 在 import 时把
HF_HOME 钉过去。
| 模型 | 用途 | 大小 | 怎么来 |
|---|---|---|---|
BAAI/bge-base-zh-v1.5 | 字幕语义索引与检索 | 约 400M | 首次运行自动下载 |
mlx-community/whisper-large-v3-turbo | 回读质检、字幕对轴校验、无字幕时兜底转录 | 约 1.5G | 首次运行自动下载 |
mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16 | 配音(多语种,中文旁白 + 英日歌名混读) | 约 4.3G | 改 config/voice.json 自动下载 |
deepghs/anime_face_detection | 视觉索引:人脸检测 | 约 42M | 首次运行自动下载 |
deepghs/ccip_onnx | 视觉索引:角色身份嵌入 | 约 143M | 首次运行自动下载 |
注意 import 顺序:任何会下载模型的模块,第一个 import 必须是 pipeline.paths,否则
模型会静默写进系统盘。paths.py 会检测并当场报错。
仓库不包含也不分发任何素材。
| 素材 | 要求 |
|---|---|
| 片源 | 一部番的完整正片,几十 G 量级 |
| 字幕 | 时间轴必须照你手上那个视频文件打的。 只允许三种来源:ffmpeg 提取的内封字幕;对该文件跑 ASR 转录的结果;与该文件同属一个发布、文件名逐一对应且通过对轴校验的外挂字幕。日语无字幕片源不支持(ASR 兜底仅中文,见 ADR-0007) |
| 参考干声 | 5–8 条,每条 8–10 秒,连续说话不要留大段静音。用来克隆口播音色 |
| BGM | 该番自己的 OST / OP / ED。整轨镜像加 cue 的碟需要先分轨 |
永远不要用第三方字幕文件配第三方视频文件。 跨版本差异是中间的插入与删除,不是恒定 偏移,加 offset 修不好。而帧级精确截取是渲染链路的前提,前提一塌视频照样渲染, 只是全程画面对不上话,没有任何报错。
data/ 放在哪仓库本身只有纯文本(代码、配置、文档、skills),全部进 git。片源、字幕、索引、模型、
每期产物都在 data/ 下,不进 git。两种放法都支持:
./pipeline/preflight.sh --init # 系统盘装得下,建实体目录
./pipeline/preflight.sh --init /Volumes/<你的盘>/anime-video-data # 外置盘 + 符号链接
脚本绝不自动创建 data/,不可达就立刻报错退出。 符号链接指向未挂载的卷时,mkdir -p
会把几十 G 静默写进系统盘且盘插上后看不见——建骨架必须是显式动作,preflight.sh --init
会打印结果落在哪块盘上。代码里一律用 ./data/... 相对路径,不硬编码卷路径。
brew install ffmpeg
uv venv --python 3.14 # 3.12 / 3.13 / 3.14 都可以
uv pip install -e ".[apple,dev]" # 非 Apple Silicon 去掉 apple
pytest # 纯函数测试,不碰片源/模型/外置盘
./pipeline/preflight.sh # 自检
cp CLAUDE.local.md.example CLAUDE.local.md # 填本机情况,不进 git
pytest 首次跑要一分钟左右,之后约 3 秒。preflight.sh 检查 data/ 可达、ffmpeg 在、
ffmpeg 真的带 ass 滤镜——有 ffmpeg 不等于能烧字幕,缺 libass 渲染会在最后一步才失败。
CLAUDE.md 是给 coding agent 读的工程约定,跑这个项目的人也建议读一遍。CLAUDE.local.md
放只对你这台机器成立的事实,不进 git。
numpy<2.52026-07-30 实测,3.12、3.13、3.14 三个版本都跑通,3.15 还在 beta,没验。
pyproject.toml 里那条 numpy>=2.0,<2.5 的上界不能删。依赖链
mlx-whisper → numba → llvmlite 里 numba 0.66 要求 numpy<2.5;numpy 2.5 一发布,
解析器就优先选它,把 numba 0.66 排除,回退到只支持 Python <3.10 的版本,转去源码构建
并失败。这个坑只咬新环境——已有 venv 一切正常不构成证据。判据得是真装一遍:
uv pip install --dry-run 只解析不构建,会「成功」地解出装不上的组合。
| 环节 | 用什么 | 为什么这么选 |
|---|---|---|
| 语义检索 | sentence-transformers + bge-base-zh-v1.5 | bge-small 实测命中率只有 75%,base 是 100% |
| 索引单元 | ASS 字幕滑窗 2 行,一集一个 .npy | 单行太碎,一句话常跨两行轴 |
| 配音 | mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16 跑在 mlx-audio 上(多语种,中文旁白夹英日歌名混读) | 云 API 把联网+账号+配额塞进主循环,还要外传参考干声 |
| ASR | mlx-whisper large-v3-turbo | 回读质检、对轴校验、兜底转录三处都用 |
| 视频与音频 | 全程 ffmpeg,无 NLE | 见 1.4 |
| 字幕烧录 | ass 滤镜(libass),折行自己算 | libass 靠空格找断点,中文整句没有空格就不折 |
| BGM | 侧链闪避,按实测响度归一到 -26 LUFS | 固定音量系数是错的控制量 |
| 镜头切分 | ffmpeg scdet,低阈值扫一遍再过滤 | 检索单元必须是镜头不是帧;改阈值不用重解码 |
| 角色在场 | 动漫人脸检测 → CCIP 嵌入 → 聚类 → 人工贴名 | booru tagger 词表大的认不准、认得准的词表小 |
| 封面筛选 | Pillow:清晰度/亮度/去重,可选角色在场过滤 | 只做剔除,不做排序 |
| 状态管理 | 文件系统 | 产物即状态。人机交接一律走文件 |
| 测试 | pytest,纯函数测试(条数见 CI / pytest --collect-only) | 不 mock ffmpeg 和模型,mock 检查的是 mock |
没有的东西也是选择:没有云 API、没有数据库、没有编排框架、没有 web 界面、没有一键脚本。
成本固定,摊到多期上才划算。一部番榨干再换番。 完整判据见 docs/WORKFLOW.md。
python -m pipeline.ingest intact data/library/raw/<番>/*/*.mkv
下载器显示 100% 不等于文件完整。 BT 没下完的文件是稀疏文件,ffprobe 照样读得出时长。
判据是 ffmpeg 全片解复用无报错(-c copy -f null -),约 0.9 秒一个文件。「实占块 ÷
逻辑大小」只当前置快筛:说「没下完」可信,说「下完了」不可信。
python -m pipeline.ingest phase0 data/library/raw/<番>/<该季目录>/*.mkv \
--anime <番> --season 1
一条命令做三件事:逐集对轴校验 → 过了才建索引 → 登记片源进 sources.json,三步不许拆开,
任何一集对轴不过就跳过并说明原因,不静默降级。对轴判据是 ASR 回读比对字符错误率——文件
名对应只能证明「打包时放在一起」,真正能证伪的只有拿视频自己的声音去对。需要字幕带日文轨,
只有纯中文字幕的集走 ASR 兜底或者放弃。
字幕索引答的是「谁说了什么」,画面里有谁它答不了。这一步补上(设计见 ADR-0003),机器时间约 2.4 小时:
python -m pipeline.shots calibrate <一集视频> # 定切分阈值,看抽检图再拍板
# 选定的数写进 config/project.json 的 visual.scene_threshold
python -m pipeline.shots build <视频> --anime <番> --season N --episode M # 逐集切分
python -m pipeline.shots frames <番> <集号> # 抽代表帧
python -m pipeline.faces detect <番> <集号>... # 人脸检测
python -m pipeline.faces cluster <番> && python -m pipeline.faces sheet <番> # 聚类出联系表
python -m pipeline.faces name <番> 0 八幡 # ← 人在这里:看图给每簇贴名(约 2 分钟)
python -m pipeline.faces presence <番> # 落索引
第 5 步的人工介入是设计的一部分,不是妥协:一部番只做一次,姐妹脸这类只有人能一眼 分开。不用现成 booru tagger 的原因:词表大的置信度太低,认得准的词表里连主角都没有。
python -m pipeline.vindex status --anime <番>
# 片源 / 字幕索引 / 笔记 / 镜头表 / 角色在场 / 画面语义
六条数字不相等就不许进每期循环。 索引或笔记悄悄缩水时,检索照常返回 Top-K、分数照常 在阈值以上、成片照常渲染,全程没有任何报错——判据不能是「检索得到东西吗」, 必须是「集数对得上吗」,前者永远为真。
for f in data/voice/reference/*.wav; do
python -m pipeline.tts probe "随便一句带长句和短判断的话" \
--ref "$f" --out "data/voice/probe/$(basename "$f")"
done
按重要性听三样:语速(不同参考音实测差 65%,直接决定成片长度)、稳不稳(电音、
忽快忽慢、句尾发飘)、音色(排最后,听的是「能不能撑 2–4 分钟口播」)。单句试音
不能预测整篇语速,估成片长度必须看 manifest.json 的总时长。选定后改
config/voice.json 的 ref_audio;script.cpm 要重测(同一音色不同文风下差过 24%)。
python -m pipeline.bgm scan <CD 目录> # 解 cue,列出 instrumental 轨
python -m pipeline.bgm extract <CD 目录> --anime <番> # 切成独立 flac
python -m pipeline.bgm measure data/library/bgm/<番>/*.flac # 量时长、响度、入声点
Phase 0 只攒候选池不 定死选曲(2026-08-08 起):具体每期用哪首,等这期配音(03)出来、
人耳听过之后填进当期 01-topic.md——BGM: 列表每行一首,@N秒 定起点(不写自动接上一首
结尾),支持任意首数拼接;老写法 BGM正文 / BGM中段(配 BGM中段切入点: N秒)/ BGM结尾
仍可用。扫曲库必须解 cue,不能只数文件(OP/ED 单曲碟是整轨镜像)。曲目表里没有实测
lufs 的曲子不许用(劇伴与 OP/ED 伴奏实测极差 11 dB)。乐评类期数还有试听型渲染
(pipeline/music):音乐段当前景、段落间降 BGM、结尾自然收尾。
01-topic.md 必须带齐四项:番、类型(人物志 / 剧情回顾 / 杂谈 / 盘点 / 共鸣)、锚点
(季集号),可选 封面集。杂谈还要带 模式(驳论 / 立论 / 吐槽)与 张力(驳论/立论)。
张力是整条流水线里唯一的编辑判断——必须在这一步定死并经人批准,不能留到写稿阶段由 agent
自行发明(那等于绕过唯一的人工关卡)。人物志/剧情回顾/共鸣不设张力:观点就是人物/故事本身。
写稿只校验、不更换。
调 skills/write-script:读上一期的稿子(为了不重样)→ 查证剧情 → 按 类型 读第 4 节对应骨架
(杂谈先校验 模式/张力)→ 按题材骨架写正文 → 切成 8–20 段,每段配一条 查询 和一条 备选。
五种题材五种骨架,全部从真实优秀内容里拆的(人物志=罗十五;剧情回顾/杂谈/盘点/共鸣=bangumi 长评与知乎长文)。骨架是「这类内容怎么走通」的范本,不是分步模板——写的时候像人说话, 写完再拿判据筛。
python -m pipeline.check_script data/episodes/<本期>/02-script.md
机检覆盖:字数、时长、单句上限、句长起伏、论文连接词、跨期套话、论据是否报菜名、剧情
锚点数、查询是否重复、查询是否误写成画面构图等。主观项机器判不了,脚本按
01-topic.md 的 类型 给出对应提示(杂谈看张力/公道话,人物志看有没有给角色安论点……)。机检全过不代表能交,自检两遍顺序不能反:先保真(台词
原文、说话人、集号、数字),再去味。查询 是机器接口,不是备注——它被原样送进
字幕索引检索,要写成台词语义:「角色承认自己一直在逃避」有效,「中景,逆光」必然落空,
字幕里没有构图信息。
python -m pipeline.tts data/episodes/<本期>
逐段合成,保留段落边界。每段时长用 ffprobe 复核后写进 03-audio/manifest.json——
这是下一步排画面的唯一依据,不采信模型自报。中断可直接重跑,已过的段落跳过;
改稿、换音色(engine/model/ref_audio/读音表)后再跑都只重做受影响的段。
按句合成不按段(语速与文本长度正相关)。
每段生成后用 Whisper 回读比对,抓漏读、重复、跑飞;不过就换种子重生成,三次仍不过
就整体退出——自回归 TTS 的典型失败不是报错,是静默地念错。回读比对前会归一化,而归一化
可能把缺陷一起抹掉,所以合成前用 tts.speakable 剥掉纯书面符号。成片交人前至少听一遍
开头和最长的那段。
python -m pipeline.clips data/episodes/<本期>
每段 查询 送进索引取 Top-24,然后全局贪心分派:所有 (段, 画面, 分数) 摊平按分数
降序占坑,同一处画面整期只用一次——不按段落顺序分,那是先到先得。每段按音频真实时长
填满,片段不短于 2.5 秒。顺序不可交换:字数估算与实际语速的偏差攒出几秒的渐进错位,
越到后面画面与口播差得越远,全程不报错。
检索落空走查询阶梯:查询 → 备选 → 该段 配音 原文,逐级重试,门槛 0.45 不动。
阶梯只救不比——上一级够格就到此为止,绝不因为下一级分数更高而换掉。三级都不及格就
写 "status": "no_match",渲染硬失败交人处理,不自动降级——自动塞空镜是把「画文不符」
藏起来。
分镜可以声明走哪条通道(ADR-0003),二选一,都不写就走台词检索:人物: 雪乃 = 台词检索
场景: 描述 = 画面语义检索,
当前不可用——门槛立不住,CLIP 余弦能排高低、答不了「这算不算命中」(ADR-0003 待实测
#4),写了会当场报错。两条铁律: 一段只走一个通道,两个字段都写会当场报错;分数只在自己的通道内排序,绝不 跨通道比较。分派画面时台词段先挑——它们要的是「那句话发生的那一刻」,具体且不可替换。
python -m pipeline.review data/episodes/<本期> # 出 04-review.html
open data/episodes/<本期>/04-review.html
python -m pipeline.review data/episodes/<本期> --approve
这是整条流水线唯一的人工关卡。 一页 HTML,每段并排口播、画面、字幕原文三样,每个片段
抽三帧(进点、中间、出点)。要抓的是台词对了但画面不对:那句话可能是画外音,机器判
不了,只有眼睛能抓。--approve 必须是显式动作——自动写 approved 是最省事的做法,
也正是这一关曾经形同虚设的原因。
python -m pipeline.render data/episodes/<本期>
切片 → 统一中间格式 → 拼接 → 混 BGM → 烧字幕 → 响度归一,一趟出片。40 个片段、3 分钟 成片实测 77 秒。
两道截取守卫强制执行,缺一不可: 切前校验 seek + duration <= 源时长,不足立即失败;
切后复核实际时长与请求值之差不超过 1 帧(按源片实际帧率现算,不许写死——番剧常见
23.976 / 24 / 29.97 三种帧率)。
字幕折行自己算,不指望 libass(它靠空格找断点,中文没有空格就不折):断点取标点, 贪心取放得下的最远那个;中文破折号是两个字符,必须等一对补全再断。
BGM 支持任意首数拼接:01-topic.md 的 BGM: 列表每行一首、@N秒 定起点,后一首
起点早于前一首自然结尾就等功率交叉(qsin,3 秒),晚了留停顿;老写法(正文/中段/结尾
槽位)仍可用,换曲点卡段落边界。每首按实测响度归一到 -26 LUFS,再以人声为触发做侧链
闪避。固定音量系数是错的控制量——换一首响度不同的曲子会凭空大 9.8 dB 压过口播,
且不报错。
python -m pipeline.qc data/episodes/<本期>
11 项,不达标非零退出,不进人工环节:时长落在目标带(默认 2–4 分钟,01-topic.md
写了 时长目标 就按那个带判,见 CLAUDE.md「内容参数」);音画误差 < 0.5s;响度 -16 LUFS、
true peak ≤ -1 dBTP;无 >0.5s 黑帧、>2s 静音;字幕无空段、不超宽、每卡一行、不超高。
查的是实际折行结果,不是「原文长度 ÷ 每行容量」——检查项一旦测的不是真实产物,
通过就毫无意义。主观项(节奏、观感)不进门禁,根在稿子和排片,回 02/05 改。
python -m pipeline.cover data/episodes/<本期> # 出池子 + 联系表
python -m pipeline.cover data/episodes/<本期> --pick 107,110,20 # 定 9 张
这一步不新增人工环节,选封面和选标题并进第 09 步。
候选帧从三处取: 实际用上的片段(每 0.5 秒一帧)、名场面时间码、01-topic.md 的
封面集 通篇(每 12 秒一帧,跳过 OP/ED)。第 3 路是被打脸打出来的:只用前两路时九张里
没有一张主角占主位——前两路的帧都来自字幕检索命中的时刻,而台词大多是别人说给主角听的,
镜头自然在说话人身上。
机器只做剔除,逐条有明确判据: 拉普拉斯方差 < 60 剔运动模糊;平均亮度落 40–215 之外
剔黑场过曝;同集相隔 < 6 秒或 dHash 相近算同一镜头只留一张;可选按 封面人物: 一色彩羽
(或 --character)硬过滤掉没有本期主角的帧(硬过滤为空则失败,与排片的软过滤相反);
2 个坑留给名场面,其余按段落号均分取样。候选之间没有排名,这是刻意的——拉普拉斯方差
测的是「画面里有多少细节」,不是「画面有多好」,两版打分都压掉了正确答案;清晰度是准入门槛,
不是排序依据。
分工是三层,不是两层: 机器剔黑场/过曝/糊帧、去重、编号 → agent 看联系表(6×5 一张、
带编号)挑出符合本期主题的(认人、占不占主位)→ 人从 9 张里定一张(审美)。agent 看完
报编号给 --pick;不加 --pick 时退到机械铺开取样,仍然出得了 9 张。
标题给 5 条候选,每条注明路子(下判断 / 反问 / 复述再打 / 剧情钩子 / 金句),原料取
稿件的第 2 节(亮判断)和第 8 节(结论)。判据是金句式而非论文式——这句话能不能脱离
视频单独发出去。07-titles.md 已填表就不再覆盖(标题和封面同一条命令出,重跑会冲掉)。
点一张封面、点一条标题,然后上传。上传手动做,不自动化。(2026-08-09 起本期耗时 由 agent 在会话内记录,不再落盘 meta.md。)
pytest # 纯函数测试,约 3 秒,不碰片源、模型、外置盘
测试只覆盖纯函数,测的是踩过的坑,不是 API 形状——每条断言对应一个具体错误。不为了 提覆盖率去 mock 掉 ffmpeg 或模型,涉及媒体的正确性交给截取守卫和质检门禁在真跑时兜。
写测试两条纪律:期望值先在实现上跑一遍、看懂为什么是这个值,再写进断言;写完做变异检验 ——把被测行为故意改坏,看测试红不红(一条永远绿的断言和没有断言效果一样)。
config/project.json 的 anime.defaultconfig/bgm.json 加一组曲目,每首必须带实测 lufsconfig/characters.json 加这部番的角色名表(中文名 ↔ 聚类贴的名字)clips 和 subindex search 都按番过滤换成真人影视素材时,视觉索引第 1 层要整块换掉(动漫人脸检测器和 CCIP 都是动漫专用的), 这是有意做成可插拔通道的原因。
skills/write-script/VOICE.md 是空模板,教你怎么从自己的既有文稿里提炼一份,填好放
VOICE.local.md(不进 git)。不填也能出稿,但会是通用 AI 腔。
MIT,见 LICENSE。
素材本身(片源、字幕、音乐)不属于本仓库,也不随仓库分发。 二创的版权风险由使用者
自负。config/bgm.json 里的曲目表只有路径和实测响度,不含任何音频。
100 commits
Python
99.3%