照片电影导演(Windows 版)

SKILL前端设计
前端设计 未标注权限 许可证不明确
安装这个技能

你硬盘里那些照片,正在过期。 拍了二十年,几万张躺在硬盘里,除了自己没人看过。不是拍得不好——是从「照片」到「作品」隔着一道工序:写解说、录旁白、配乐、字幕、卡节奏,每一步都得学一个新软件。于是它们就一直躺着。 这个技能把这道工序整个接管了。 想给父母做一部纪念视频、把老照片做成电子相册、或者给一次旅行留一部纪录片,都不需要你会剪辑。 如果你不太会剪片 —— 给一个照片文件夹,再说一句话:这片子讲什么。它自己选片、自己写解说、自己配音、自己上字幕、自己配乐混音,最后交出一支能直接发出去的成片。配音是 AI 合成的,普通话、粤语、东北话随你挑,男女声几十种音色,不用你录一句。第一支片子就拿得出手,而且你一个剪辑软件都不用装。 如果你自己会剪片 —— 这正是我最想给你的。会剪的人从来不是被剪辑难住,是被重复劳动拖死的:几百张照片手动排运镜、字幕逐句对时、混音电平反复试、封面还得单独抽帧……它把这些全包了。你省下的那一两天,留给真正需要判断力的地方——选片、结构、表达。 它做的也不只是「照片变视频」:选片闸门(跑题的、糊掉的、重复的自动挡在门外)、逐镜运镜方案、三轨混音定标到行业标准、封面与发布文案,还有一道合规与事实核查——省得你辛苦做完,被平台一条理由打回。 而且它是**跑通过**的:一档 **13 集**的相册电影节目,就是照这套流程一集一集做出来、并在各平台持续更新的。你拿到的不是一份说明书,是一条验证过的产线。 会拍照的人已经很多了。能把拍过的照片变成作品的人,还很少。 你负责拍,剩下的交给它。 本版为 Windows 版(需 Python 3.10+ 与 ffmpeg,安装步骤见技能内说明);macOS 用户请搜「照片电影导演(Mac 版)」。 (关键词:照片电影、相册视频、电子相册、相册制作、纪念视频、照片转视频、照片做成视频、老照片做成视频、老照片纪录片、家庭影像、Ken Burns 运镜、AI 配音、自动字幕、照片配乐)

一句话选型结论

按资料字段整理

照片电影导演(Windows 版)是前端设计类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类前端设计 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_0ce0d907/photo-film-director-win
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

前端设计
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_0ce0d907仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791693777
正文体量333.6 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
出页面与组件

按描述生成页面结构和组件草稿。

调整样式与布局

改间距、配色与响应式布局。

生成设计说明

把设计稿整理成开发能接的说明。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在前端设计场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
照片电影导演(Windows 版)当前 未标注权限 未标注 未标注 未标注 有
前端设计 只读 GitHub 仓库 882.3 万 Codex、Claude、Cursor 有
Vercel 组合模式 联网访问 GitHub 仓库 385.9 万 Codex、Claude、Cursor 有
Vercel React native skills 前端 只读 GitHub 仓库 246.9 万 Codex、Claude、Cursor 有
adapt 搜索 浏览器控制 GitHub 仓库 202.7 万 Codex、Claude、Cursor 有
使用我的浏览器 浏览器控制 GitHub 仓库 184.8 万 Codex、Claude、Cursor 有

SKILL.md 全文

333.6 KB
模型实际读到的说明书原文,来自技能仓库

照片电影导演(photo film)

把「一堆照片」变成可发布成片。配音默认走 AI 合成 —— 使用者不需要自己录音,
文字稿进去,配音和字幕一起出来。

三条入口:

入口 使用者给什么 走哪条产线
① 标准(默认) 照片 + 拍摄地点 AI 写文案 → 合规/事实核查 → AI 合成配音 → 出片(§3.0.6)
② 自带录音 照片 + 现成录音 从 §3.1 剪接(跳过合成)
③ 高级 · 真人配音 照片 + 想用真人声音 §3.0 —— 属定制服务,需另行安排录制

> ★ 关于入口 ③:真人配音是可选的增值服务(真人的嗓音、语气、地域口音,
> AI 合成替代不了)。技能自带的完整闭环是入口 ①;想走 ③ 的,按 §3.0 的规范
> 出录音稿后交真人录制。默认不要主动把使用者往 ③ 引导 —— 先用 ① 跑通出片。

> ★ 合规前置:走 AI 配音就必须按平台规定声明 AI 生成(§3.0.6 末节有红线清单)。
> 不得声称「真人原声」。这条不是建议,是发布前提。

通用章节(事实核查 / 响度口径 / 封面 / 交付三件套)已内联到
references/common-standards.md —— 本技能自包含,无需外部技能。

环境准备(Windows)

# 需要什么 怎么装
1 Python 3.10+ python.org 下载,安装时务必勾上 Add Python to PATH
2 ffmpeg winget install Gyan.FFmpeg;或下载压缩包解压后,把其中的 bin 目录加进系统 PATH
3 Python 库 pip install pillow numpy opencv-python scipy edge-tts
4 中文字体 系统自带微软雅黑/中易宋体,脚本会自动挑一个可用的
# 一次性装齐(Windows PowerShell)
winget install Gyan.FFmpeg
pip install pillow numpy opencv-python scipy edge-tts

# 自检:能列出音色就说明配音链路通了
python scripts\tts_narrate.py --list

★ 三个 Windows 专属注意点:

  1. 路径尽量用纯英文、不带空格。 中文路径在部分 ffmpeg 调用上会出问题
    (本工具链内部已用引号包裹,但外部传入的路径仍建议英文)。
  2. 控制台中文乱码:先执行 chcp 65001 切成 UTF-8;
    或直接用 Windows Terminal(默认就是 UTF-8)。
  3. 首次运行会自动下载模型(人脸检测约 200 KB、语音识别约 1.5 GB),
    需要联网,之后走本地缓存。

★ 字体自检:若输出里出现方框/豆腐块,跑一次
python scripts/_fonts.py 看各字族命中了哪个文件。

0. 铁律

  1. 不要停下来等确认。 只有三种情况需要用户介入:额外付费、覆盖原片、对外发布。
    用户说过「过程别问我确认不确认,始终确认的」——那就一路做到出片。
  2. 照片必须每一张都动。 静止镜 = 观众划走。全部镜次带运镜,且相邻镜不得同运镜。
  3. 视觉结论必须肉眼过。 封面、文字层、调色,「数字上成立」≠「视觉上成立」。
    渲染完出对比图并真的打开看,不合格就重做 —— 别拿数值糊弄。
    ★ 调色/片头/转场另有专节 §5.5「影片风格六组件」(2026-10-03 建立)——
    核心一句:风格 = 六件组件(片头/字幕/运动/转场/调色/尾板),不是六个滤镜;
    调色只占六分之一。
    调色管线、四数验收、片头质检、转场闸门红线全在 §5.5。
  4. ★ 合规审查 + 事实核查,必须在「剪辑之前」完成 —— 不能等出片后再审。
    内容问题在文案阶段是「改一行字」,到成片阶段就是「重渲染 + 重烧字幕 + 重合成」,
    修改成本差两个数量级。先出一份 内容合规审查报告.md(结论 / 风险分级 /
    发布注意事项 / 被平台退回时的备选处置),通过后才开始制作。

审什么:政治敏感与主权表述、血腥暴力与不良内容(博物馆 / 历史 / 刑具题材重点)、
封建迷信、价值导向、史实准确性、极限词、版权署名、路人入镜的个人隐私。

事实核查细节见 references/common-standards.md:照片题材多为地方史,
数字/年份/专名/因果/方向性表述逐条查出处;查不到的剪,绝不用模糊词覆盖。
5. ★ 剪重复句要用「电平谷」定位,不是 silencedetect。
现场录音底噪常在 −50 dB 以上、不存在真正的数字静音,
silencedetect 默认阈值会误杀低电平语音(实测把「惊堂木一拍」整句切掉)。
6. ★ 响度 −14.0 LUFS / 真峰值 ≤ −1.5 dBTP,且 TP 判定必须以落盘文件为准(详 §7)。
★★★ 交付音轨必须是 stereo(channels=2) —— 素材全是立体声却被 -ac 1 拍平过(东湖 EP01 返工根因,详 §7.7)。
★★ 连续背景床(水声/风声/街噪)用 --duck-preset bed,不要用给一次性音效的 event(详 §7.8)。
7. 交付必须含封面,且用无字幕原片做底——不能从成片截帧(会带上已烧的字幕和遮幕)。
★ 封面 + 发布文案是「交付件」,不是「下一步」。
实测教训:凤凰这一期把封面写进了「下一步」清单就交了成片,用户直接反问
「封面为何没有完成设计制作?」—— 成片 ≠ 交付完成。
→ 收尾时对着 §10 交付件清单逐项打勾,凡是清单里的,当场做完再报;
只有清单外的(如长图文发布)才写成「下一步」。
8. ★ 配音一律用真人原声,不要用 AI 合成声音替代 —— 已定案,不要再推合成音方案(详 §3.0)。
用户 2026-09-21 明确否决(AI 标识强制,对老照片叙事类内容不划算)。
若用户提「你写稿 + 用我的声音合成,我就不录了」,直接答「标识绕不开,不划算」,
然后把话题转到分工:AI 写文案 + 出录音稿,用户只念一遍。
9. ★ 外部素材(公版贴片 / 配乐 / 音效):下完 ≠ 下好了,必须过完整性判据。
音频是静默失败的重灾区 —— 实测一个库 13 首里 8 首是坏的,其中
MemoriesOfStone.mp3 7.42 MB,在访达里完全正常,真内容只有 59%。
挑中它 = 成片里半句断,而那时已烧进时间轴。
- 唯一可靠判据:ffmpeg -c copy 重封装进临时 .mkv,量长度,
与声明时长比 < 0.97 即截断。工具:_shared/音频素材管理/verify_media.py。
0 字节或判不过的,一律当「未完成」,不许当已归档。
- ★ 图片也要验,判据是 PIL.Image.open(p).load()(不是 verify()、不是「能打开」)。
open() 是惰性的,只读文件头 —— 截断的文件照样 open 成功、.size 也照样正确。
实测 B01_姑苏繁华图_全卷.jpg 只差 28 字节,访达里能开、能看缩略图,
只是下半截显示不出来 → 不逐张程序化验,永远发现不了。
verify_media.py 已支持图片,加 --min-side 1000 还能顺带查「够不够 1080p」。
- ★★★ 图片另有一条静默陷阱:「下载成功」≠「下到了原图」。
实测 A08_欧阳修_像 本地 300×395,而 Commons 原图本来就是 1158×1020 ——
早先那次下载悄悄取了缩略图,没有任何报错。
→ 凡要「落位后缓推」的图,下完必须回头对一遍 API 报的尺寸。
又:显示尺寸不得超过原生像素,原生不够就「不推」(推=放大=糊)。
2026-09-26 寒山寺案:6 张公版画像里 4 张短边只有 338–579 px,
改成「落位定格」比换素材更划算(贴片本就带纸纹+软投影,不推反而更像实物)。
- ★★★ 图片的第四条静默陷阱:古画/文物的「年代与国别」,凭画面风格猜必错。
实测 2026-09-26:手上那张 MET《丰干寒山拾得图》,我按画风写成了「传为南宋」——
它实际是十五世纪上半叶 · 日本 · 传灵彩。画风像宋画 ≠ 它是宋画。
→ 凡要上屏标注朝代/国别,必须回头对一遍馆藏著录
(Commons 的 extmetadata,或博物馆官网条目),不许凭风格猜、不许凭「看起来像」。
标错国别/年代 = 史实错误,与「署名错 = 授权违规」同一性质 ——
公版素材不需要授权,但照样会因为标错而毁掉整支片的可信度。
- ★★★ 第三条静默陷阱,也是最阴的一条:服务器返回拦截页/错误页时,
文件照样在长,大小看着完全像正常进度。
实测 B04_虎丘_徐扬.jpg 长到 6.55 MB,我据此报告「下载成功」——是错的:
它的头 12 字节是 3c 21 44 4f 43 54 59 50 45 20 68 74 = &lt;!DOCTYPE ht,
整份是一张 HTML。
→ 判据:落回「这到底是不是那类文件」,不要用「涨了多少 MB」当结论。
python def looks_like_html(p): with open(p, &quot;rb&quot;) as f: h = f.read(512).lstrip() return (h[:1] == b&quot;&lt;&quot; or h[:9].lower().startswith(b&quot;&lt;!doctype&quot;) or h[:5].lower().startswith(b&quot;&lt;html&quot;))
★ 由此立一条通则:「有进展」和「进展是对的」是两件事。
长字节只证明连接活着,不证明拿到的是想要的东西——
任何自动化下载,收尾都必须有一道「内容类型」验证。
- 铁律:半截文件永不删,只 -C - 续传(曾把一份下了 5 分钟的 2.7 MB 残件清零重来)。
★ 但续传前必须先确证服务器认 Range:
python def range_cap(url): # 三态,不许把「判不出来」当成「不认」 code = &lt;curl -r 0-0 -w &#x27;%{http_code}&#x27;&gt; return True if code==&quot;206&quot; else (False if code==&quot;200&quot; else None)
206 → 原地 -C -;200 → 绝不能原地续,改另存 .new 从 0 下、
验完整才替换(实测 B01 被 -C - 从 1.92 MB 清回 0.39 MB,是真数据丢失);
None(线抖,没拿到响应)→ 不许猜,本轮跳过、不计次数,
否则线一抖就从 0 重下,白扔一遍。
★★ 还有一条反向陷阱:已完整的文件不要再碰。
对一个已下满的文件带 -C -,服务器会收到一个超出末尾的 Range,
回 404/416 → 被当成「URL 永久失效」判死。
实测 MemoriesOfStone.mp3(其实早就 330.4s/330.3s 完整)中招。
→ 每轮动手前先 validate() 一遍现有文件,好了就直接跳过。
- ★★★ 别对这一类站点开并行 —— 会被 429,而且 429 伪装成「网络慢」。
实测对 upload.wikimedia.org 开 4 路并行 → HTTP 429 Too Many Requests;
而 429 的响应不带 Accept-Ranges → -C - 报
(33) doesn&#x27;t support byte ranges. Cannot resume. → 重排队 → 再撞 429 →
死循环,永远不可能成功。日志里只显示「零增长 45s」,看着像网差,其实是被限流。
→ 按 host 定策略:Wikimedia 类串行 + 15 s 间隔,429 单独识别并长退避
(且不计入尝试次数);CDN 类(如 Scott Buckley)不受限,仍可并行。
--retry 0 —— curl 默认的 --retry 会在 429 上自己死磕,把窗口糊满、看不到真实状态码。
「零增长」先问「是不是我请求太密」,一条带 -w &#x27;%{http_code}&#x27; 的单发 curl 一分钟出真相。
- 授权只认 PD / CC0,排掉 CC BY-SA(「相同方式共享」有传染性)。
没有可靠画像的人物,用「他的著作/他的碑」代替(见 §2 与寒山寺案)。
但手上没有真拓片,就别把排版的铭文叫「拓片」——名实不符是硬伤。
- 下载器必须多轮循环 + 只砍真死连接(这些站点是脉冲式,同一分钟 25→108 KB/s,
设高阈值会把自己的正常连接砍掉);nohup … &amp; 在本机活不下来,要用托管后台。
- ★★★ 链路可能是「间歇性」的 —— 「慢」和「断」要用两套判据。
实测同一分钟:baidu 200/0.2 s、scottbuckley 200/5.7 s(通)、
upload.wikimedia.org 000(断);而 26 分钟里 8 个文件 0 成功。
→ 先做「探活」再下,且探活的判据是「有没有响应」,不是「响应好不好」
(403/404/301 都算线路通,只是这个 URL 有问题)。
python def probe(url): # 只要 1 字节 code = &lt;curl -r 0-0 -w &#x27;%{http_code}&#x27;&gt; return code in {&quot;200&quot;,&quot;206&quot;,&quot;301&quot;,&quot;302&quot;,&quot;303&quot;,&quot;307&quot;,&quot;308&quot;, &quot;403&quot;,&quot;404&quot;,&quot;410&quot;}
★★ 关键:一个字节都没下到 = 线路问题,不要算作一次「尝试」。
旧版把 rc=28 当普通失败,14 次用光就永久判死 —— 等于「等窗口」的能力为零。
正确形态:线不通就长退避(90 s)只等不撞、不计次数,真正的刹车是总时长预算。
另加 宽限期:连接刚建好后的零增长是正常的,40–45 s 就判死会误杀(给到 120 s)。
- 判定「素材够不够」时,别把竖版按「不可用」扣掉。
竖版 3744×5616 裁一条 16:9 带 = 3744×2106,仍是 1080p 的 1.95 倍,
带内做纵向摇移是照片电影的常规手法。先算裁切后的有效分辨率,再下结论——
2026-09-26 寒山寺案曾据此错判「缺 51 张」,其实 204 张够 187 镜。
- 本机 shell 的 grep 不可靠(实测:对确实存在的字符串静默返回空)。
核查文件内容一律用检索工具,不要用 shell grep —— 曾因此差点把真问题当已排除。
- 一次性 HTTP 请求(解析直链、查 API)别用 urllib:它的 timeout 是
每次 socket 读的超时,走代理时对端慢速滴数据 → 永不超时,进程挂死(实测踩过 4 分钟)。
改用 curl(--max-time 是硬性墙钟上限),并把解析挪进 worker,别让一条挂住堵整轮。
- 完整踩坑记录(含六个错误完整性判据、429 死循环)见
~/.workbuddy/memory/工具手册_卡顿排查与记忆瘦身.md §十、§十一、§十二。

1. 工程目录

&lt;project&gt;/
  01_source/     # ★ 照片**软链** c001.jpg…(软链到素材盘原件,绝不实拷贝!见 §11.43)
                 #   短名避免中文长路径 + narration_raw.mp3 备份
  work/          # 脚本与中间产物
    w/           #   ★ 预缩放缓存(提速关键,见 §2)
    txt/         #   文字 PNG
    edl.py kb.py make_text.py audio.py cover.py
    cut_audio.py cut_audit.py   # 剪接表 + ★ 逐刀核对(见 §3.3)
  03_render/     # 逐镜渲染 s001.mp4 …
  02_work/       # picture.mp4 → picture_text.mp4
  05_record/     # ★ 录音稿.md(人念的)+ 字幕_全片.srt(机器对齐用)
  04_out/        # 成片 + 封面 + 验收记录.md + 作品说明与发布文案.md
                 # + 内容合规审查报告.md + 旁白工作流_写稿到录音.md

流程顺序(不可颠倒):写文案 → 合规审查 + 事实核查 → 用户审文案 →
出录音稿 → 【等录音窗口:跑题批审 + 选片链前置,见 §2.1】→ 用户录音 → 剪接 →
镜头表 → 渲染 → 字幕 → 混音 → 成片 → 封面 → 发布文案。

★★★ 2026-10-03 新增 §2.1 那条顺序是硬规定:跑题批审必须排在「镜头表」之前。
排在之后 = 镜表必然返工(EP05 为此刻了 4 版、烧掉 42 分钟)。

2. 素材准备(预缩放缓存 —— 必做)

照片原始分辨率(如 4032×3024)直接参与 Ken Burns 会极慢。
先并行缩到工作分辨率,渲染从 9 s/镜降到 1.13 s/镜。

  • 横构图缩到宽 2560,竖构图缩到宽 2400(LANCZOS,q95)
  • 8 进程并行(293 张约 31 s)
  • 渲染时优先读 work/w/,缺失才回落到原图

2.1 ★★★★ 跑题批审 —— 必须排在镜头表之前(2026-10-03 EP05 定案)

病灶:42 分钟的镜表返工,是为「审图太晚」付的利息

EP05 镜表迭代 4 版 42 分钟(22:47→23:29):

迭代 镜数 素材改动 真实动因
v1→v2 129→109 107/109 镜 三联版式全部否掉
v2→v3 109→106 105 镜 整体平移(重新分层)
v3→v4 106→97 91 镜 ★ 放大逐张审之后,发现 11-19 整批跑题

根因:现有两个审图工具的输入都是「已定版的镜表」——
make_review_sheet / make_cand_sheet 都读 镜表_EP05_v4.csv
⇒ 流程被迫变成 排镜表 → 审图 → 发现跑题 → 重排 → 再审 → …

附带一处纯浪费:_pool/ 的图已经是 2400×1600(够看清是塔是殿是饭桌),
但 make_cand_sheet_ep5.py 把它 fit 到 256×144 再看,
而它自己的文档写着「256 px 仍然不够 …… ⇒ 必须看 plate 尺寸」。

正解:工具_跑题批审图.py

按拍摄连续段切批(相邻间隔 > 20 分钟)→ 每批等间隔抽 24 张 →
拼 4×6、每格 640×360 的接触表(从 2400 px 缩,清晰够判)→ 人勾批号 → --apply 出黑名单。

# 1) 出批审图(★ 不需要镜表、不需要旁白 ⇒ 可整段放进等录音窗口)
$PY 07_strategy/工具_跑题批审图.py --pool &lt;集&gt;/07_work/照片池_&lt;集&gt;.csv \
        --out &lt;集&gt;/02_work/_批审 --gap 20 --per-batch 24 --cell 640
# 2) 看图,把跑题批号写进 02_work/_批审/_剔除批.txt
#      b17                 ← 整批剔除
#      b15 -c0933          ← 剔除 b15,但保留 c0933(该批里这一张是正常的)
# 3) 生成黑名单(★ 落到该批**全量**,不止抽样到的那 24 张)
$PY 07_strategy/工具_跑题批审图.py --pool &lt;集&gt;/07_work/照片池_&lt;集&gt;.csv \
        --out &lt;集&gt;/02_work/_批审 --apply

EP05 实测:22 批,精确切出全部 3 类跑题

工具切出的批 档案里事后才逐张审出来的跑题
b15 潭柘寺 11-12 14:43–14:57(7 张) ★ 14:45–14:57 出差饭桌照(茶具/炒菜/点心)
b17 潭柘寺 11-12 18:45–18:51(9 张) ★ 18:46–18:50 逛街照|含西什库教堂夜景
b18–b21 潭柘寺 11-19(101 张) ★ 11-19 整批现代城市街景/明十三陵大红门

真打开看过 b15:7 张里第 1 张 c0933 是正常山景(=镜表 s0001 用的图),
其余 6 张全是饭桌(白瓷茶壶/蓝白格桌布/红烧肉/炒鸡蛋/玉米饼)——一眼可判。

★ 再次印证「detail 闸门筛不出跑题」:菜品照 detail = 0.183~0.193,不算低。

三条实测坑(写进来,别重踩)

  1. 「整批剔」会误伤 —— b15 的 c0933 是正常的 ⇒ 必须支持 b15 -c0933(剔该批但保留指定编号)
  2. 剔除要落「该批全量」 —— 不能只剔抽样到的那 24 张(b16 有 115 张只抽 24 ⇒ 漏剔 91 张)
  3. 小批别单独成页 —— 出现过「1 张占一页」⇒ 需要向邻批合并

★★★ 2026-10-04 盘库模式三处修复(用 --folder 直扫素材夹时必看)

缘起:EP06 立项前的候选比较 —— 第一次不走「做片子」流程(手上没有照片池 CSV),
改成 --folder 直扫素材夹 ⇒ 一次暴露三个 bug,其中两个是静默出错。

# 症状 根因 已修
1 整页「缺图」 cache = dirname(pool) = out 目录,而 pid 形如 照片/IMG_0001 ⇒ 拼出的路径必然不存在 池表有「路径」列就优先用绝对路径(C_PATH),cache 只作回落
2 扫描 X → **0 张** ExifTags 压根没 import,NameError 被裸 except Exception: continue 吞掉 补 import;把吞错改成逐条打印(静默失败比失败更贵)
3 切段全错 im.getexif() 只返回 IFD0,而 DateTimeOriginal 在 Exif SubIFD(0x8769) 里 ⇒ 取不到 ⇒ 静默回落到文件 mtime 新增 shoot_time():先 ex.get_ifd(0x8769);回落时计数并报警,>20% 直接判「切段不可信」

实测(南京高淳_2014,540 张):

段落 日期
索引 / 真实 EXIF 4 批 2014-09-20(355)+ 09-21(185)
修复前(落到 mtime) 3 批 全部挤在 2014-09-22 ❌

> ★★★ 第 3 条为什么最贵:切段是镜表和一切下游的地基。
> mtime 是「拷盘时间」不是「拍摄时间」⇒ 段落全错 ⇒ 后面排的镜表、算的时长全跟着错,
> 而且不会报错 —— 高淳那次的输出看着完全正常(日期整齐、批号连续、图也出得出来)。
> ⇒ 凡「读不到就换一个来源顶上」的写法,必须三件齐备:
> ① 回报一个「是否用了回落」的标志 ② 调用方计数 ③ 超阈值判不可信。

★ 用法(盘库模式,2026-10-04 起可直接用):

$PY 07_strategy/工具_跑题批审图.py \
    --folder &quot;/Volumes/…/10_景点/南京高淳_2014&quot; \
    --out &lt;项目&gt;/_EP06_候选/南京高淳_2014/_批审 --gap 20 --per-batch 24 --cell 640

回归验证:盘库模式与「索引池表 + --pool」两路结果逐批一致(批数/日期/时段/张数全同)。

★ 还能更省 —— 素材库已有全盘 EXIF 索引,零 IO 建池表:

# 源:_shared/素材库管理/exif_索引_全盘_20261003.csv
# 按「路径」列筛出目标夹 → 建池表(编号/来源/拍摄时间/可用=Y/dup/宽/高/路径)
# ⇒ 不必为几百张 24 MP 的 JPEG 反复读外置盘(USB 机械盘随机读只有 50–100 IOPS)

★ Python 选择:图像脚本必须用带 PIL 的解释器(本机 /opt/homebrew/bin/python3)。
托管 runtime 3.13.12 没有 PIL ⇒ ModuleNotFoundError: No module named &#x27;PIL&#x27;。

★ 顺手一条选材判据(EP06 高淳实证):
「单文件夹 ≠ 单景点」 —— 高淳 540 张实为「一趟出行两个地方」
(09-20 高淳老街 355 张 + 09-21 另一处建筑群 185 张)。
⇒ 立项前必须跑批审图,否则会把两个地方的画面当同一个地方排进镜表。

★ 本工具已收入技能:scripts/batch_topic_audit.py(与项目内 07_strategy/工具_跑题批审图.py 同源)。

★★★★★ 借壳文件夹:边界必须逐帧看图,不许用时间阈值(2026-10-05 EP08 立规)

病灶:EP08 云锦所那批素材藏在 10_景点/**大屠杀纪念馆_2014**/照片(553 张)——
同一个夹,前半是云锦所,后半才是大屠杀纪念馆。我按时间窗截,右端没卡住,
两次都用时间阈值(≥17:57)推断边界 ⇒ 把 IMG_8247(人行道街景)与
IMG_8248(遇难者名单墙)留在了池子里 ——
用户看接触表截图举报:「南京大屠杀纪念馆的混进来了」。

实读定界(唯一正确的做法):

帧 时间 内容(实读) 判定
IMG_8245 17:52:58 「中國南京雲錦博物館/人类非物」金属格栅幕墙 ✅ 留用段末帧
IMG_8246 17:54:50 铁栅栏 +「禁止停车」牌 ⛔ 离馆途中
IMG_8247 17:56:07 人行道 + 禁停标志 + 行人 ⛔
IMG_8248 17:56:38 密集中文人名刻字墙(遇难者名单墙) ⛔🔴
IMG_8249 17:57:08 同上(纪念馆园区) ⛔

⛔⛔ 铁律

  1. 借壳文件夹的边界必须逐帧看图定界 —— 不许用文件夹边界、不许用"按分钟取整"推断、
    不许用时间阈值。(时间阈值 = 推断;逐帧看图 = 实读。)
  2. 锚点写成「文件名常量」,不是时间 —— 不同批次时间字段格式可能不同(2014 批只有 17:15:28,
    按 [11:16] 切片会静默变空串),而且时间阈值可以"差一点点",文件名不会。
  3. 脚本必须自检锚点,落地脚本做两道闸门:
    ① 两个锚点文件必须真实存在于池中(否则 sys.exit);
    ② 边界连续性 —— 边界之后不许出现留用帧、边界之前不许出现跑题帧。
  4. 报数口径必须分清:跑题段「总帧」≠「从净池剔掉的张数」。
    EP08 实测:跑题段 61 帧(含 9 张已被画质闸淘汰的 + 1 张 dup),但从净池只剔 51 张。
  5. 别把「跑题」记成「肖像」 —— EP08 旧记「肖像剔 4 张」,其中 3 张(IMG_8277/8278/8279)
    拍摄时间在 18:10,本来就在跑题段内。同一帧只能归一个闸门,否则口径互相打架。

★ 旁证手段(零成本、高信号):留用段若时间连续无断层(EP08:180 帧连续 37 分钟、零 ≥3 分钟空档),
基本可判「一次连续拍摄、同一地点」,不必再全量翻图。
但边界附近 6–8 帧仍须实读 —— 越靠近边界越可疑(EP08 正是这样才找到 IMG_8246 也是跑题的)。

参照实现:EP08 07_work/trim_pool_yunjin2014.py(边界=文件名常量 + 两道自检)。

★★★★ 用户发的截图 = 局部裁剪 —— 不能拿整帧去比对(2026-10-05 EP08 立规)

实测:用户截了一张图指认「这张就是江宁织造整体所处地含院子」。
我用 dHash + 直方图把 51 张整帧逐一比对 —— 最高相似度只有 dHash 61/144、直方图相关 0.15,
全不匹配 ⇒ 我据此判定「不是你拍的这批」。

错在哪:用户是从聊天窗口截的画面中央那片院子(局部),不是整帧。
拿局部去比整帧,必然不匹配 —— 而且失败得「很像真的」:有数字、有排序、有 top12 列表。
开了原图一看,IMG_4000 就是他要指认的那张。

★★★★ 四条正确做法

  1. 截图先别上算法,先人眼开原图核。 用户既然能指认,说明他一眼认得 —— 人眼比哈希可靠。
  2. 真要比,必须把候选区域也裁出来(多尺度滑窗),不能整帧比整帧。
  3. 哈希/直方图只用于「同一张图的不同副本」(EXIF 同秒、批量导出、裁切版),
    不用于「同题材的不同构图」 —— 构图一变,哈希就废。
  4. ★ 缩略图分辨率就是判据上限。 那轮 51 张总览每格只有 470 px,
    而江宁织造博物馆的关键特征是白墙围合 + 水池 + 假山(细部)——
    470 px 下看不见 ⇒ 我把博物馆看成了「普通城市俯瞰」。
    ⇒ 要判题材,格子必须够大;宁可分多页,不许一页塞满。

> ★ 一句话:比对之前先问「我比的这两样东西,是不是同一种东西」。

为什么它同时是「提效」

它不吃旁白、不吃镜表 ⇒ 可以整段搬进「等录音」的空窗。
EP05 的空窗是 3h58m,机器实际只干了 11 分钟。
⇒ 把这 3h58m 用来跑批审 + 选片链,镜表就能一次成型。

2.1b ★★★ 池表构建链 —— 每集只改路径常量,不从零写(2026-10-04 EP06 定稿)

四步(脚本从上一集复制 → 改 EP / POOL / CACHE 三处路径即可):

步 脚本 做什么 输出
1 build_pool_epN.py ① 零 IO 建清单(读全盘 EXIF 索引)② 建 2400 px 缓存 ③ 算 detail/sharp/spread/chroma/dHash ④ 四道闸 + 去重 07_work/照片池_EPN_&lt;地名&gt;.csv
2 detect_faces_epN.py OpenCV YuNet(模型 scripts/models/face_detection_yunet_2023mar.onnx)正+镜像双跑、网格去重 写回池表:faces/face_max/face_lv/face_box
3 重跑第 1 步 合并人脸列(load_faces() 会读回自己写过的池表)<br>★ 池表已存 dhash 列 ⇒ 重跑时全程不读图、不重算,只重判闸门 池表完整
4 plan_sheet_epN.py 排镜表(读 黑名单_跑题.csv) 镜表

四道闸(口径五集未变,别改):

闸 阈值 后果
内容闸 chroma &lt; 1.5 灰阶(翻拍/证件照)整组不用
质量闸 detail &lt; 0.10 不进片
肖像闸 face_lv == 3(人脸 > 12% 短边) 可辨认面部,整张不用
去重闸 dHash 汉明 ≤ 8 同组保留 detail 最高的

两级人脸分级(不是只有「用/不用」):
0 无人脸 → 自由用 | 1 ≤ 3.5% 短边 → 远景可用 | 2 3.5–12% → 不进主位 | 3 > 12% → 整张不用

★★★ 三个坑(EP06 实测,都会静默或白跑)

  1. Python 选错会白试两轮 —— 图像/数值脚本一律用 /usr/bin/python3
    (3.9.6 + numpy 2.0.2 + PIL 11.3.0 + cv2 4.13.0)。
    /opt/homebrew/bin/python3 有 PIL 无 numpy;托管 3.13.12 两个都没有。
  2. 全盘 EXIF 索引 exif_索引_全盘_*.csv 里有 NUL 字节 ⇒ csv.DictReader 直接抛
    _csv.Error: line contains NUL(整个文件读不进来)。
    修法:open(..., errors=&#x27;replace&#x27;) → .replace(&#x27;\x00&#x27;, &#x27;&#x27;) → splitlines() 再交给 DictReader。
  3. 跑题批次要直接写进池表(可用 = N + 排除原因),不要只留在批审目录 ——
    否则选片时还得回去翻批审图。

★ 提速:只给「会进片」的批次建缓存(EP06:540 张里只有 355 张要建,省掉 185 张)。
池表仍列全部张数,跑题的带原因留档(这样池表本身就是一份完整的判定记录)。

★ EP06 实测数(可作下一集的期望值):
540 张入表 → 355 张评分 → 净可用 339 张;detail 中位 0.258、&lt; 0.10 仅 8 张;
6′12″ 的片需 ≈135 镜 ⇒ 余量 2.5 倍,够用。

2.1c ★★★★★ 选片链:题材台账 → 选片表(唯一权威) → 隔离清单(2026-10-06 EP08 定案)

为什么必须有这一节 —— 用户原话点破的病根:

> 「题材表按大区间取图,前面的组没取尽就轮不到后面。
> 改成句级区间+兜底。这个问题可以在选题之后专门做一次选片,按优先级排序,
> 选好之后,不在项目内的隔离一下。」

EP08 实测的病:GROUPS 写成 (&#x27;c0226&#x27;–&#x27;c0245&#x27;) = 云锦织物陈列,
而那一区间实际画面是城市高楼天际线 —— 错得毫无提示,成片直接拿城市照配「云锦」旁白。

三张表 / 一条链

池表(501 张)  →  题材台账.csv      初判题材(按**文件名编号**划段,RANGES)
              →  题材覆盖_EPN.csv  ★ 精修覆盖:`原题材 → 新题材 + 理由`(**只覆盖,不改台账**)
              →  选片表.csv        ★★★ 唯一权威 —— 后续脚本只认这张
              →  隔离清单.csv      池内未入选(状态「备用·本片未入选」)

> ★★ 为什么"精修"要单独一张表,而不是直接改台账(2026-10-06 定案):
> 直接改 RANGES 会把错误埋进初判 —— 下次没人看得出"这里改过什么、为什么改"。
> 拆成 初判(脚本里的 RANGES)+ 精修(覆盖表 CSV) 后,覆盖表可 diff、可追溯,
> 每条都带修改理由;replan_epN.py 启动时加载它,覆盖表缺失即 sys.exit(不许静默跳过)。
> 另有一个实用副作用:踢出池也是一条覆盖,分两类 ——
> SHOP(文创商店:不是展厅,也不是任何叙事要的内容)/
> OFF(画面里的文字会与旁白打架:EP08 的「曹府戏苑」四字匾,既对不上句13「写着三个字:萱瑞堂」,
> 又对不上句48「匾上写着江宁织造」—— 它在本片叙事里没有位置)。
> 两类都不参与任何分配,但留在覆盖表与隔离清单里(用户在"哪些没用、为什么没用"里看得见)。

表 生成 记什么
题材台账_EPN.csv build_tags_epN.py 编号/批/题材/来源夹/拍摄时间/文件名/宽高/detail/face_lv/路径/题材说明
题材覆盖_EPN.csv refine_tags_epN.py 编号/文件名/原题材/新题材/理由(只列被改的,152/501)
选片表_EPN.csv replan_epN.py 镜号/段/句序/时长/运镜/题材/版式/图数/编号/文件名/拍摄时间/宽高/路径
隔离清单_EPN.csv replan_epN.py 未入选的编号(给用户一个「哪些没用、为什么没用」的可见答案)

★★★ 四条硬规矩

  1. 题材表绝不按大区间。RANGES 按文件名编号划段(不是 c/y 流水号 ——
    流水号会随池变动而漂)。未归类即 sys.exit(EP08 靠它抓出漏标的 IMG_4083/4087/4088)。
  2. 判据三条:拍摄时间 / 画面内容(联系表肉眼过)/ 不采信「来源夹」
    (来源夹 只是人工内容分类,非物理目录;EP08 有 IMG_4466–4468 标「园区」实为别处)。
  3. 句级 NEED + FALLBACK,取图顺序:横图单图优先(cover,不留白)→ 竖图三联兜底;
    定向三联的镜反过来。NEED[sent] 是列表,第 0 项是主题材、其后是兜底链。
  4. 隔离清单不是可选项 —— 它是「选片这件事做完没有」的凭证,也是用户下次改片时的第一手材料。

★★★ 六个机制(都不是锦上添花,缺了会出结构性问题)

机制 作用 EP08 实参
CITY_MAX 按句锁死城市镜配额 {6:1, 7:1, 67:1} ⇒ 全片城市 3 镜 = 1.5%(原 31 镜)
TRIPLE 定向三联配额(不只是补救留白):一整面墙的云锦/一排展柜/两人配合这类复数内容,三张并排信息量本来就更大。安排在该句最后 k 镜(先铺陈,再并束) 三联 14 → 22 镜
PRIM_CAP 主题材逐句限量,防止前面几句吃干后面的核心素材 {53:2,54:2,55:1,56:1,57:2}
PREFER 题材类内优先序。类分对了,类内仍是编号序 ⇒ 取到的是"该类里编号最小的几张",而不是"最贴这句的几张"。给"必须精准匹配"的类加显式优先表(不在此表的按编号序排后) [&#x27;TANG&#x27;] 把"画面能辨认出萱瑞堂三字"的排前面;[&#x27;GPURE&#x27;] 把亭/水池/假山/廊排前面
RESERVE 循环前先占位:收尾金句/关键句必须拿到的东西,先占好再跑主循环 句68「城西那一半,还在织」→ 题材型 LOOM
RESERVE(素材型) 上一条的升级:PREFER 只解决"类内排序",解决不了"先到先得被前面的句抢走" ⇒ 只有循环前预占才拿得到。EP08 扩展为直接指定素材,并支持三联逐镜对应(第 3 项写成空格分隔的三张) {68:&#x27;LOOM&#x27;, 35:[&#x27;IMG_4178&#x27;,&#x27;IMG_4179&#x27;,&#x27;IMG_4114 IMG_4283 IMG_4213&#x27;]}

> ★ PREFER 与 RESERVE 的分工:PREFER 管「同一个类里先取哪张」,RESERVE 管
> 「这一句必须拿到的那几张,别被前面的句抢走」。两者都要有 —— EP08 只加 PREFER
> 时,句35 最贴的「水池+假山」三张仍被段2/段3 的园林句先取走,加 RESERVE 才拿到。

🔴🔴🔴 七个「静默」坑(全部实测踩过,都不报错)

  1. scan() 不许用 cursor 当扫描起点。 首版 i = cursor[tag],而 cursor 是 commit 次数;
    commit 只增不减,但被取走的编号在组内是分散的(scan 会跳过 used 与方向不符的条目)
    ⇒ 游标跑到还没取过的条目前面,把它们整个跳过。
    表现:「主组明明还有货,却静默退兜底」。
    ⇒ 改全表扫、不用游标(每组 ≤114 条 × 185 镜,开销可忽略)+ 加「幽灵降级」闸门。
  2. PRIM_CAP 的生效前置是 len(base) &gt; 1。 NEED 若写成单元素 [&#x27;LOOM&#x27;],
    限量规则整段被跳过 ⇒ 句53/54/55/57 照旧各吃 3 张、LOOM 取空硬失败。
    ⇒ 教训:限量规则与它依赖的结构要一起写([&#x27;LOOM&#x27;,&#x27;YHALL&#x27;])。
  3. 帧对齐读旧表、写新表。 render_epN.py 的 plan() 原写 src = 镜表_v1.csv,
    而写回目标是 SHEET = 镜表_v3.csv ⇒ 每跑一次渲染,就把 v1 的「素材/张数/版式」整列灌回权威表。
    ⇒ 修:源表改成本表自身 + 加闸门「只许改 入/出/时长/帧数 四列」
    (除这四列外逐格比对,不一致即 sys.exit)。
  4. 钉死镜(FIXED)的配额要在主循环之前先占。 EP08 的 s0019 排在句7 第 10 位,
    轮到它时 city[7] 已被前 3 镜用满 ⇒ 城市悄悄变 4 镜。
  5. 闸门③「幽灵降级」会误报 —— 把「连镜去闷」(主动换题材)也判成幽灵。
    ⇒ 引入 turned(连镜超阈值且主组就是它)/capped(主题材已达上限)两个变量,
    主动降级进白名单(EP08 从 38 处误报降到 0)。
  6. plate 与 clip 都是「存在即跳过」,双双静默(2026-10-06 实测,一晚上踩两次):
    - make_plates_epN.py:if not force and os.path.exists(out) and getsize(out) &gt; 30000: return &#x27;skip&#x27;
    - render_epN.py shots:clip 已存在就跳过(--force 才强制)
    ⇒ 只重渲 --only s0036 而不先重建 plate,会拿旧图渲一遍;
    先把 clip 渲回来了、再改选片表重跑,则 shots 报「新 0/185」正常退出——
    画面根本没变,却一个字都不报。
    ⇒ 规(两条都要):
    ① 重渲前先把目标镜的 clip mv 走(别指望脚本自己发现);
    ② 跑完必看「片段:新 N/185」的 N 是否等于预期改动镜数(EP08:没移走时 N=0;移走后 N=11 ✓)。
  7. 改题材分类会引发「连锁位移」,不止改一张。
    IMG_4190 从 TANG 移到 GATE 后,GATE 类内取图顺序整体前移一位
    ⇒ 实测11 个镜换了图(不是 1 个)。
    ⇒ 改完题材必须 diff 旧/新选片表,按 diff 决定重建范围;
    不要凭"我只改了一张图"下判断(会漏渲,且不报错)。

★★★★★ 认字闸门:旁白点名了具体文字时必须过 OCR,不许看缩略图(2026-10-06 EP08 血本)

病:旁白「你抬头看,院子里挂着一块匾,写着三个字:萱瑞堂」。
我靠人眼看联系表缩略图认匾额,把 IMG_4190 判成「厅堂正面(白墙+门+上方匾)」放进 TANG。
成片后用户报「1:37 画面没对上」—— 放大一看,那块匾写的是「曹府戏苑」四个字。
旁白说三个字、画面是另一块四字匾 ⇒ 比上一版更糟。

根因:联系表缩略图里匾额只有几十像素,认字必错。
(同类坑:_pool/&lt;编号&gt;.jpg 缩略图与选片表编号不同源 —— 实测 _pool/c0182.jpg 是织物,
而选片表 c0182 是 IMG_4190。用缩略图核内容 = 双重不可靠。)

正解 —— 三步,全程不靠眼睛认字:

  1. 先建 OCR 索引(一集一次,EP08 是 2055 条):07_work/ocr_epN.py → ocr_epN.json
    = {原图路径: [识别文本, …]}
  2. 搜关键词,且要搜「单字」 —— 竖排匾额 OCR 会从右往左读出:
    「萱瑞堂」→ 堂端萱 / 堂瑞萱;只有横排才读成 萱瑞堂。
    ⇒ 搜 萱 一个字,两种读法一网打尽(EP08 命中 10 张:3799/4119/4120/4121/4122/4123/4125/4274/4275/4276)。
  3. 命中后仍要放大复核(OCR 也会错)—— 从原图裁匾额区域看,不看缩略图。

★ 判据升级(比"找到对素材"更值钱):
旁白点名了具体文字(匾额/招牌/站名/题字)时,该句「每一镜」都要过这道闸,不能只核一次。
EP08 句13 三镜里 s0034/s0035 是对的、只有 s0036 错,
而用户报的 1:37=97.0 s 恰好落在 s0036(该镜 96.97–99.77 s)。
⇒ 用户说「某一秒不对」时,先按秒抽帧定位到具体镜号,再单独查那一镜 ——
不要笼统地"整句重看",那既看不出问题、又容易把本来没错的镜改坏。
(用户的时间点是播放器绝对秒,含片头;本片片头 3.5 s。)

★ 还有一条更隐蔽的(第一版修完立刻又踩):
换掉一张错误素材时,不能只看它"原本对不上"的那一句 —— 还要看它换到新位置后,
画面里的文字会不会跟新位置的旁白打架
。
EP08 把「曹府戏苑」从 TANG 改归 GATE 后,它自动排到句47/48 交界,
而紧接着的句48 旁白正是「匾上写的也是那四个字:江宁织造」
⇒ 同一个"匾不对字"的错误当场犯第二次。
⇒ 正解:直接踢出池(OFF 类),而不是"换个类塞进去"。
> 通用判据:带文字的素材是"有归属"的 —— 它的文字要么与某句对应,要么就该排除;
> 不存在"随便放哪都行"的带字素材。

★★ 版式硬规矩(与 §5.5 配套)

  • 单图必横(cover,不留白);竖图一律三联并置。VERT = 高 &gt; 宽 × 1.05(近方形不算竖)。
  • blur_fill() 这类「竖图两侧补模糊」的做法整段删除 —— 用户明确否掉:「必须竖版三联屏,
    不给画面两边留白,否则观感不好」。
  • make_plates 里 one() 不许静默降级:返回 &#x27;VERT_SINGLE&#x27; / &#x27;HORZ_IN_TRIPLE&#x27; 让主流程 sys.exit。

★ 关于「素材不够」的正确反应

v2 有 36 镜用了池外素材(IMG_4465–4550,当天 17:00 后别处拍的)
—— 根因不是"池里没有",而是分配逻辑按大区间走、把馆内素材跳过了。
⇒ 先查分配逻辑,再谈扩池。 EP08 重排后池只用了 229/501 张,一张都不缺。

2.2 ★★ 「一材多片」必须算重合度,不能靠感觉(2026-10-03)

前提已核实:素材不是瓶颈。
- 素材盘 10_景点 = 345 个文件夹 / 89,100 张,其中 ≥100 张的 212 个
- EP05 单集池 1194 张:可用 875、已用 110、剩 765 张 = 现有用图量的 7.0 倍
(戒台寺剩 561、潭柘寺剩 204),且已过同一门槛,不是废片
⇒ 同一批素材可以切成多条不同选题,不必等新素材。

但有两个必须量化的风险,靠感觉判一定翻车:

  1. 画面撞车 —— 百家号把「批量发布重复或无意义内容」明确列为流量作弊 ⇒ 两条片子的用图重合度必须 < 30%
  2. 体量不够 —— 某个「角度」看着成立,实际只有 30 张

工具:工具_素材池切面.py(用池内客观字段,不读图、毫秒级)

$PY 07_strategy/工具_素材池切面.py --pool &lt;集&gt;/07_work/照片池_&lt;集&gt;.csv \
        --used &lt;集&gt;/01_source/_exif_上屏.csv --per-film 110 --overlap-max 0.30

★★★ 它最值钱的输出 =「必然撞车对」:体量够、名字也不同,但同一套选片规则下取图几乎相同。
EP05 命中:戒台寺·人物·午 × 戒台寺·2011:07:29 = 85%。
⇒ 把「以为换了选题,其实换了文案没换画面」变成一张可见的表。

★ 铁律:切面只是素材侧的可能性,不是选题。
「换角度」是换立意(换知识线),不是换措辞。 逐条仍须过事实核查 + 合规审查 + 去重闸门。

2.3 ★★★ 库级属性必须缓存 —— 别每集重算(2026-10-03 发现)

病灶:analyze_tracks.py 模块头原文:

> 对 _shared/配乐曲库_CCBY 全部曲目做同一套量测

这是库级属性,跟「哪一集」毫无关系,却被复制到每集、每集重算一遍。

铁证(md5 完全相同):

ep04_苏州陆巷/07_work/起音点.json   md5=5fac16ef5ce4  (225,445 B)
ep05_北京双寺/07_work/起音点.json   md5=5fac16ef5ce4  (225,445 B)   ← 同一份文件,各算一遍

为什么现在就要改:
- 现在曲库仅 13 首 / 145 MB ⇒ 只是小浪费
- 将来用户手上有 6,400 首 BGM 素材库,且相册电影要提产 ⇒ 选曲面必然扩大
⇒ 这个重复计算会线性膨胀成真瓶颈

修法:建 _shared/曲库索引/,只算一次,各集只读:

_shared/曲库索引/
    配乐量测.json      ← analyze_tracks.py 产出(含 envelope)
    起音点.json        ← detect_onsets.py 产出
    _manifest.json     ← 曲库指纹(文件名+大小+mtime 的哈希)

脚本改 --lib 指向曲库、--out 指向 _shared/曲库索引;每集只做 _manifest 指纹比对,
一致就直接复用,不一致才重算。

★ 这同时是「元原则」的一例:任何「第二次重复做的事」必须当场变成缓存或闸门。
⚠️ 本项尚未落地(2026-10-03 只完成诊断)。

3. 音频:剪重复句与口误

3.0 ★ 配音来源:真人原声(第一条产线 —— 第一人称真人叙事专用)

> ### ⚠️ 先看这一行:本技能有两条配音产线,规则不同,不要混用
>
> | 场景 | 走哪条 | 一句话 |
> |---|---|---|
> | 本人 IP/第一人称叙事(观众信的是"这个人") | 本节(§3.0)真人原声 | 换合成音 = 抽掉地基 |
> | 代客户制作/客户不便或不愿出声 | §3.0.6 · AI 配音产线 | 客户要的是"片子",不是"他的嗓子" |
>
> 两条产线的差别不止"用什么声音" —— 更在声明与署名怎么写。
> 最典型的混用事故:走了 AI 产线,却照抄了真人产线的交付声明
> (「旁白按原录音剪辑未用合成声音替换」)⇒ 等于自己出具了一份反证。
> 这条已单独拆出成节:§3.0.7「AI 版的画面落地:显式标识 + 尾板署名」,
> 走 AI 产线的必须在出片前读完那一节。

2026-09-21 用户定案,原话:
> 「既然 AI 标识是强制的,那我选择放弃了,还是自己念旁白吧!
> 那么就是以后可以由你来帮我写好合适的文案,我来录好音频再把音频给你」

→ 以后不要主动向用户提声音克隆 / TTS 合成方案。 那是被否决过的路线,
重提只会消耗信任。用户若自己再问起,按下表如实回答(不要说「绕不开」就完了)。

★★★ 2026-10-03 重大更正:旧结论「挂 AI 标识 = 自毁」被实测数据打掉,按下表说。

用户当天的质疑原话:「为何现在平台上大量的……这方式的作品都拿到了很好的收益???原因是??」
—— 这个质疑是对的,必须正面回答,不能再用旧论证搪塞。

# 约束 实测 / 规则要点 旧结论
1 标注是强制的 《人工智能生成合成内容标识办法》2025-09-01 施行;2026-04 起升级为发布必经环节(6 类必选标签含「含有AI生成内容」,不选不能发);未合规标注不予分发;存量短视频分批回溯补标,逾期限流/下架 ✅ 成立
2 ★ 但「标注 = 限流」是错的 20 条对照实测:标了 AI 的播放量没掉,平均还比没标的稳;不标的对照组第 3 天起从 2,800 掉到 600,其中 3 条被平台补标、数据更凉。TikTok 侧同向:官方从未把标签当分发信号,已标注内容无触达惩罚;「不标被检测」才是重罚(约 −60% 触达/30 天,另一来源称 −73%/48h,且是账号级处置)。抖音 AI 检测能自动抓 35~45% ❌ 此条作废
3 掉量的真因不是标签 「If your reach is down and you're blaming an AI tag, the tag is probably not the cause. Retention, hook quality and originality almost certainly are.」 ⚠️ 认知更正
4 ★ 平台差异才是真边界 百家号:「即便声明了是 AI 生成的,内容要是有问题也是一样从严处理」(与后台实读一致)⇒ 百家号是唯一必须排除的平台;抖音 / 视频号 / 小红书:如实标注后可正常分发与变现(合规前提下不影响变现) ✅ 这条才是硬边界
5 内容/品牌代价(非算法) 第一人称叙事挂「内容由AI生成」,观众自然会问「哪部分是 AI?」—— 这是信任感代价,不是流量惩罚。护城河仍是真人 IP,但这是取舍,不是「自毁」 ⚠️ 降级为权衡项
6 安全 最短 3~5 秒样本即可克隆,普通人识别合成语音准确率 <50%;样本外流 = 骗子用用户的声音打给用户的粉丝(《民法典》声音保护参照肖像权) ✅ 成立

★ 为什么会「感觉」标了 AI 就掉量? —— 幸存者偏差 + 归因错位:
掉量的片子通常是留存/钩子/原创度出了问题,标签只是恰好同时在。先比完播率与近期基线,再怪标签。

★ 那些「AI 配音相册视频赚到钱」的案例,多数不是同一种生意(归因要分清):
- 一大类是接单代做(帮中老年人做 AI 语音相册/修复老照片,月入 3,000~12,000)—— B2B 服务收入,不是平台流量分成
- 另一类是信息型/故事型/修复展示型内容,观众要的是内容本身,不依赖"真人在场"
- 而照片电影是第一人称真人叙事,核心资产是「本人」⇒ 能不能用,取决于你自己愿不愿意让渡这份信任感

3.0.6 ★★★ AI 配音(无录音条件时的另一条产线)—— 2026-10-10 新增

先说边界,别把两条产线的规矩搞混:

用哪条产线 为什么
第一人称真人叙事(本人出镜/本人讲述,个人 IP 号) 必须真人原声(§3.0 定案) 核心资产是「本人」,观众信的是这个人;换合成音等于抽掉地基
代客户制作 / 客户不愿或不便出声 可以用 AI 配音(本节) 客户要的是「片子」,不是「他的嗓音」;这是 B2B 代做,不是流量分成

> 行业实况(§3.0 已载):「帮中老年人做 AI 语音相册」是被验证能赚钱的一类生意,
> 月入 3,000~12,000,属服务收入。客户群体的痛点本就是「会拍不会写、会写不会说」,
> 强制要求他录旁白 = 把最不会说话的那批人挡在门外。

用法

# 先看有哪些音色
python scripts/tts_narrate.py --list

# 不确定语速时先校准(实测反推满足 2.75 字/秒的 rate)
python scripts/tts_narrate.py --calibrate --script 录音稿.md --voice zh-CN-YunjianNeural

# 正式合成(★ --rate=-40% 要连写等号,否则 -40% 会被当成选项名)
python scripts/tts_narrate.py \
  --script 05_record/录音稿.md --out 04_audio/ \
  --voice zh-CN-YunjianNeural --rate=-40% \
  --gap 0.35 --para-gap 0.70 --jobs 8

产出三件套(直接接进后续流程,不用另做对齐):

文件 用途
narration.mp3 整条旁白 → 进 §3.1 之后的剪接/混音链(无需再剪口误,本来就没有)
narration.srt 字幕 → 已按规范去标点,可直接进 §6 烧录
sentences.csv 每句起止秒 → 直接喂 §4 镜头表,跳掉 whisper 对齐那一步

音色怎么选(实测建议,2026-10-10)

音色 性别 实测语速 @-40% 适合
zh-CN-YunjianNeural 云健 男 2.72 字/秒 ✅ 最接近 2.75 纪实、历史、怀旧——本系列默认
zh-CN-YunyangNeural 云扬 男 2.88 解说、播报腔
zh-CN-XiaoxiaoNeural 晓晓 女 2.69 通用、温柔讲述
zh-CN-YunxiNeural 云希 男 2.91 轻松、年轻题材
zh-CN-liaoning-XiaobeiNeural 女 — 辽宁方言(地域题材加分)
zh-CN-shaanxi-XiaoniNeural 女 — 陕西方言

★★★ 本节的三个坑(都踩过)

  1. 逐句合成必须裁首尾留白。 TTS 每句自带 0.3~0.5 s 留白,几十句累积就是几十秒
    「白给的停顿」,语速会莫名掉下去。脚本已默认裁(--no-trim 可关)。
    ★ 裁法只能「反转→裁→反转」 —— silenceremove 的 stop_periods 不是
    裁尾部静音,而是「检测到第一个静音就截断到结尾」,实测把 530.8 s 正文一刀
    砍成 87.5 s。别用它裁尾。
  2. 音色换了必须重跑校准。 各音色基准语速不同(云扬比云健快 6%),
    拿一个 rate 通吃会让片长漂。
  3. --rate 后面要跟等号(--rate=-40%),否则 argparse 把 -40% 当选项名报错。

★★★★★ 合规:这一节最容易出事的不是技术,是声明

> AI 合成配音属于「AI 生成内容」,必须按规定标注。
> 三条不能碰:
> 1. 不得声称「真人原声」 —— 那是虚假陈述,比不标更严重。
> 2. 百家号要格外小心 —— 其口径是「即便声明了是 AI 生成,内容有问题一样从严」,
> 是所有平台里最严的(§3.0 表格第 4 条)。代客户做片投百家号前,先跟客户讲清。
> 3. 平台要求勾选/声明的,一个都不能漏(6 类必选标签含「含有AI生成内容」,
> 不选不能发;存量内容会分批回溯补标,逾期限流/下架)。
>
> 脚本跑完会打印一次合规提醒 —— 别当成客套话划过去,那是交付前的最后一道闸。

3.0.7 ★★★★★ AI 版的画面落地:两处署名 + 显式标识(2026-10-10 新增)

> 走 AI 产线的,出片前必须读完本节。 §3.0.6 讲的是「怎么合成」,本节讲的是
> 「合成出来的片子,画面上该怎么写」 —— 这一步做错,前面全白干。

一、★★★★★ 最容易漏的一条:AI 版有「两处」署名,不是一个

位置 性质 判断标准
① 起始画面(片头里) 显式标识 合规强标:字号越大越好,但字高不得低于画面短边的 5%
② 尾板 署名 如实:说清哪一部分是 AI

这两处的判断标准完全不同,所以不能"改了一处就以为完事"。

🚨 只改 ① 而让尾板沿用真人版,比"完全不标"更糟。
真实事故:尾板上原封不动留着真人产线的两句 ——
旁白:我自己念的,按原录音剪辑,未用合成声音 / 右下角 本片无 AI 生成画面。
于是片头说「本片含 AI 生成」、尾板说「没用到 AI」,同一支片子里两句话互相打架 ——
观众抓住任何一句,另一句就变成了谎言。

★ 那一句 本片无 AI 生成画面 单看并不假(画面确实全是本人实拍照片)。
问题在于:它把观众最该知道的那一半藏起来了。
⇒ 判断口径:别问「这句话单看是不是真的」,问「合起来会不会让人形成错误认知」。
正解是一句同时讲清两个维度:

字段 ❌ 照抄真人版 ✅ AI 版改成
旁白行 我自己念的,按原录音剪辑,未用合成声音 AI 自动生成
右下角 本片无 AI 生成画面 旁白为 AI 生成 · 画面为真人实拍

二、显式标识(那处 ①)的三条硬指标

  1. 必须落在「起始画面」 —— 不是结尾、不是中段。发布后平台与公众第一时间要能看到。
  2. 字高 ≥ 画面最短边 × 5%。1920×1080 ⇒ ≥ 54 px。
    ⚠️ 这一条要按实测字号算,不能凭"看着挺大" —— 不同字体、不同 index,
    同一个 size= 参数出来的实际字高能差 20%。渲染完量一遍再放行。
  3. ★ 字号会被「责任转移」悄悄拉低 —— 这是最容易翻车的细节:
    标识牌原本是「大标题 + 小说明」两行(大标题 72 px 负责过线、小说明 40 px 不用管)。
    一旦按审阅意见去掉大标题、只留小说明,那行 40 px 就顶不住 54 px 的线了。
    ⇒ 删掉大字的同时,必须把剩下的字同步放大到过线(实测 40 px → 60 px 才够)。
    任何一次「去掉某块文字」的修改,都要回头重算剩下的字还够不够。

> 不要用平铺水印代替标识 —— 全帧铺满半透明文字既难看、又影响观感,
> 且平铺件往往字号极小,同样过不了 5% 那条线。标识是一块专门的排版,不是滤镜。

三、尾板必须换,而且真人版原件一个字节都不能碰

AI 版与真人版会共用同一套尾板生成脚本(排版逻辑一行不用改),差异只有文案。
⇒ 不要覆盖原配置,用环境变量开新一路:

# 真人版(默认,不传环境变量时的行为 = 与改动前逐位相同)
python scripts/credits.py build --bg-photo 01_source/photo/&lt;底图&gt;.jpg

# AI 版(新开一路,另存一份文案档)
CREDITS_JSON=work/credits_ai.json \
CREDITS_MP4=04_out/尾板_署名_AI.mp4 \
  python scripts/credits.py clip --bg-photo 01_source/photo/&lt;底图&gt;.jpg
项 真人版(不要动) AI 版(新增)
文案档 work/credits.json work/credits_ai.json
尾板 png / mp4 04_out/尾板_署名.png / .mp4 …_署名_AI.png / .mp4

★ 为什么必须分开、不能覆盖:真人版那套文件是已发布成片的署名依据。
覆盖了,以后就无法回溯「那天发出去的到底是哪一版」—— 而这恰恰是合规自查时
唯一能自证的东西。验证办法:改完比对两份原件的 sha1 与 mtime,必须没变。

★ 更一般的原则:凡是"AI 版要替换的件",一律走环境变量/参数,绝不写死覆盖真人的那一份。
片头图、尾板、文案档都是同一类东西。

四、混音前先做一件事:把 AI 旁白降到与真人相同的目标 LUFS

这一条是实测逼出来的,不是洁癖:

  • AI 语音的 LUFS 比真人高约 2.2 dB,但总 RMS 只高约 0.26 dB
    (响度计 K 加权的低频搁架对不同声源加权不同)。
  • 而混音链是按 LUFS 设计的 ⇒ AI 旁白一进来就"过响",
    limiter 会替你做完响度归一 —— 那不是你想要的,动态被压扁了。
  • 硬把响度抬回目标值,真峰会顶穿(实测会到 ≈ −1.2 dBTP,违反交付硬指标)。

⇒ 正解:先给 AI 旁白一个固定衰减(实测 --volume=-2.2dB),把它送到与真人相同的
目标 LUFS,后面所有参数自动回归真人版取值,不必逐个重解。

★ 动手前先各跑一次 ebur128 把两个值量出来再定衰减量,不要照抄本节的数字 ——
不同 TTS 音色、不同语速,这个差值会变。

五、出片后的三步验收(缺一步等于没验)

# 动作 判据
1 抽 t=0 首帧打开看 起始画面确实含标识 —— 不是"我加过了",是"抽帧看过了"
2 抽尾板帧打开看 新文案确实进了成片(不是只改了配置文件)
3 出改前/改后对比图 见下

★ 对比图的正确做法:只放大"真正变了的那几处",不要交整幅四宫格。
两版尾板逐像素往往只差 0.3%(就是那两句),整幅缩到 960 宽后文案只剩 8–10 px 高,
等于没给看 ⇒ 裁原尺寸再放大 2 倍。

★ 排版类修改,sha1 变了不算验证,命令回显 ✓更不算 —— 工具会打印
✓ 尾板.mp4 6.0s | 180 帧 而内容其实没变(改的是配置文件,脚本读的是缓存 PNG)。
⇒ 判据只有一条:打开图看。

六、一键重出脚本里,重出尾板要是第 ⓪ 步

画面是拼出来的,改片头/尾板必须从拼接起全链重走
(拼接 → 烧字幕 → 终混;拼接会丢字幕,别指望字幕还在)。

但音频不必重算 —— 只要尾板帧数不变,总时长、配音补白、总长参数全都不变。
(实测:换尾板后落盘响度与改前逐位相同。)

⇒ 脚本开头无条件先重出尾板(成本几秒),并把旧成片自动留档到 work/_prev/,
改前改后两版都在,才拿得出对比图。

七、平台边界(承 §3.0 表格第 4 条,仍然成立)

  • 百家号是唯一必须排除的平台 —— 其口径是「即便声明了是 AI 生成的,内容有问题一样从严」,
    不因为标了就安全。代客户做片要投百家号前,先跟客户讲清这一条。
  • 抖音 / 视频号 / 小红书:如实标注后可正常分发与变现。

3.0.8 ★★★ 成片竖版(9:16 / 1080×1920)交付:三个变量都要重算(2026-10-10 新增)

> 起因而非洁癖:一条横版成片要同发抖音竖版,第一反应是「裁一刀」—— 那是错的。
> 竖版不是裁出来的,是重新排一次版。
> ⚠️ 本节讲成片本身;封面规格(1080×1440 等)在 §9.1–§9.4,两回事别混。

一、🚨 整幅缩放,不要裁切

横版成片的字幕是烘死在画面里的(字幕带固定在某个 y 区间,不是外挂字幕)。
裁切 ⇒ 把字幕裁掉或裁一半,成片直接废。

⇒ 正解是 scale=1080:-2(等比缩放、高取偶),整幅缩进去,字幕跟着缩。
判据:缩完抽一帧打开看 —— 字幕完整、没被切边。

二、★★★ 下区留白是「故意」的,不是没排满

1920×1080 缩到 1080 宽后,画面只占 608 px 高;剩下的高度里,我们要主动留出下部空白:

平台 底部会被自己 UI 压住的区域
抖音 作品描述 / 按钮 / 音乐条 —— 约 25%

⇒ 别看到留白就想填东西。 那片空白是保护内容不被平台 UI 遮挡的。
(实测:下区留 656 px,画面居中偏上,正好避开。)

三、★★★★ 合规字号必须留余量,不能压线过

竖版的短边是 1080(不是 1920)⇒ 5% 强标线 = ≥ 54 px。

实测翻车过程:

字号参数 实测字高 判定
58 55 px ❌ 只高线 1 px —— 等于没有余量
66 64 px ✅ 余量 10 px

> ⚠️ 与 §3.0.7 第二条第 3 点是同一条病:字号参数 ≠ 实际字高,不同字体/不同 index 能差 20%。
> 竖版尤其容易翻车 —— 短边变小、线跟着抬高,横版刚好的字号挪到竖版就不够了。

⇒ 规则:凡改画幅/改字号/删文字块,回头实测一遍字高再放行。

四、竖版落地清单(六步,缺一步等于没验)

  1. scale=1080:-2 整幅缩放,不裁切
  2. 画面居中偏上,下区留白 ≥ 600 px 避平台 UI
  3. 角标/标识重排 —— 横版的位置不能照搬(短边变了,相对位置全变)
  4. 实测字高 ≥ 54 px,且留余量
  5. 抽帧看:字幕完整 + 标识在 + 没被切边
  6. ★ 响度当独立成片重新核 —— 竖版是另一支片子,TP 按硬指标重测
    (实测:loudnorm TP=-1.5 落盘 −1.4 超标;改 TP=-2.2 才落到 −2.0)

3.0.5 ★ 产能(用户 2026-10-03 提出「每天 5 条」)—— 先算账,别先换工具

> ★ 2026-10-04 更正编号:本节原误编为 3.0.3,与「录音稿规范」(§3.0.3)撞号,
> 且正文顺序上夹在 §3.0 与 §3.0.1 之间。⇒ 改号 3.0.5(位置不动,避免大段搬动引入新错)。
> ⚠️ 本节位置与编号不一致是已知的:读到 3.0 → 3.0.5 → 3.0.1 不要以为漏页。

环节 谁 5 条/天的真实成本
写文案 + 合规审查 + 事实核查 AI 并行,不占用户时间
出录音稿 AI 并行
录音 用户 一条 500~900 字 ≈ 念 3~5 分钟,含 NG 约 10~15 分钟 ⇒ 5 条 50~75 分钟
后期全部(剪接→镜表→运镜→字幕→混音→成片→封面→发布文案) AI 流水线批处理

⇒ 5 条/天 ≈ 用户每天 1 小时录音,真人路线本来就撑得住,不必为产能牺牲真人 IP。

真正会先崩的三样(按先后顺序):
1. ~~素材储备 —— 每条 50~200 张 ⇒ 5 条要 250~1,000 张/天~~
→ ★★★ 2026-10-03 实测更正:这条是错的。素材不是瓶颈,见下。
2. 选题储备 —— 相册电影的文案是从立意推出来的,不是「有照片就能写」
(★ 且「一材多片」之后这条更重要了:一个素材池要想 3~5 个互不重复的立意)
3. 平台反批量 —— 百家号明确把「批量发布重复或无意义内容」列为流量作弊;同质化会触发全站降权
⇒ 可量化的硬判据:两条片子的用图重合度必须 < 30%(见 §2.2 工具)

★★★ 素材储备为什么不是瓶颈(2026-10-03 实读):
- 素材盘 10_景点 = 345 个文件夹 / 89,100 张,其中 ≥100 张的景点 212 个
- EP05 单集池 1194 张:可用 875、已用 110、剩 765 张 = 现有用图量的 7.0 倍
- 且这 765 张已过同一门槛(detail≥0.10、无肖像权特写、dHash 不重复)⇒ 不是废片
- ⇒ 「一材多片」不需要新素材(用户 2026-10-03 的判断是对的,我的旧结论作废)

⇒ 提效正解 =「批量前置(一次写 N 条稿)+ 集中录音 + 并行后期」,不是更换声音来源。

★ 复核本技能的「成品系数」:纯念稿 → 成片旁白 ≈ 2.72~2.81 字/秒(见 §3.0.1 标定表),
5 条按 700 字/条估 ≈ 每条 33 分钟成片——选片与渲染才是 CPU 侧的真瓶颈。

本机能力备查(万一将来技术或法规有变):edge-tts 只有预置音色、不能克隆,
中文方言音色只有东北话与陕西话;无 torch、无本地克隆模型。
⚠️ 用预置音色做的「对比音频」不是声音克隆,只证明「机器念稿的观感」——
做这类演示必须当场标明(这个错犯过,被用户当场指正)。

★ 标准分工 —— 以后的固定流程:

环节 谁做 说明
① 写文案 AI 用户给照片 + 拍摄地点 + 他知道的掌故,AI 出完整旁白文案
①′ 合规审查 + 事实核查 AI 8 个维度逐条过,数字/年份/专名/因果逐条查出处。制作之前完成
② 出录音稿 AI 带断句、停顿标记、重点提示、生僻字拼音 —— 用户照着念(详 §3.0.3)
③ 录音 用户 只剩这一件事,分段录
④ 后期全部 AI 剪接 → 镜头表 → 配图 → 运镜 → 字幕 → 混音 → 成片 → 封面 → 发布文案

为什么这样分工:用户真正不想干的是「从零想句子」和「反复录错重来」,
不是录音本身。实测:即兴口述 8 分钟要剪掉 51 秒(全是重复与口误,因为边想边说);
念稿录制基本不再产生 → 用户的实际耗时反而更短。

交付话术(写进文档):你想句子那几个小时我全接,你只剩照稿念一遍。

3.0.1 ★ 文案阶段:立意优先 + 字数时长必须脚本数

① 写文案前必须回读选题方案。 用户为本条纠正过两次:
> 「做这个视频做选题的时候就有立意的呀?」/「这个节目是根据……开始立意来做的!跑题了吗?」

文案是从「立意」推出来的,不是把查到的资料排成队。 落笔前先回答:
- 全片那句落点是什么?(一句话,能背下来)
- 每一段在为那句落点做什么?答不出来的段落就是跑题,删掉或重写。
- 🚨 钩子必须和立意接得住——把引子抽掉后读一遍,如果变成"一篇没有结论的考据",就是跑题了。

② 旁白稿格式约定:正文用 &lt;!--BODY_START--&gt; / &lt;!--BODY_END--&gt; 包住;
&gt; 开头的引用块放「〔画面〕+ 核查注」(不念),脚本会自动剔除。

> 🚨 2026-09-27 事故:《苏州博物馆》EP1 把引文塞进了引用块,字数少算 172 字、时长少算 76 秒。
> 引用块在本约定里是「不念」的容器(画面提示、核查注放这里是对的)。
> 但凡要念的字加了 &gt; 前缀,脚本就会按约定把它剔掉——而且不报错、不报警。
> 本片 4 段引文(张欣录音、贝聿铭原话 ×2、普利兹克评语)共 172 字全部要念,
> 却被当成"不念",页首因此写着「1,014 字 ≈ 6′23″」——
> 真值 1,186 字 ≈ 7′28″,口语化后录音稿 1,262 字 ≈ 8′53″,与「6 分钟」差了近 3 分钟。
>
> ★ 铁律:要念的引文,一律写在正文里(用 「」 包裹即可),绝不给它加 &gt; 前缀。
> ★ 自检动作(核完字数必做):把脚本剔掉的引用块逐条念一遍问「这句要不要念」;
> 只要有一条要念,口径就已经错了 —— 把它移回正文,重算。
>
> ★ 定性要说准:这次不是脚本错,是我把输入放错了容器。
> 脚本忠实执行了约定 —— 所以「验证工具」不够,还要验证「输入是否符合约定」。
> (对比:《寒山寺》文案 v4 的引文写在正文里,同一个脚本算得准 —— 证明脚本没问题。)

③ 字数与时长一律脚本数,绝不手数:

~/.workbuddy/binaries/python/envs/default/bin/python \
  ~/.workbuddy/skills/photo-film-director/scripts/count_narration.py &lt;旁白稿.md&gt;

# ★★★ 要往录音稿里贴「分段表」时,加 --md —— 直接产出可粘贴的 Markdown 表
~/.workbuddy/binaries/python/envs/default/bin/python \
  ~/.workbuddy/skills/photo-film-director/scripts/count_narration.py &lt;旁白稿.md&gt; --md

> ### ★★★ 2026-10-04(EP06):新增 --md —— 禁止再手抄脚本输出
>
> 病根:本技能早就写着「字数只允许有一个来源 —— 脚本产出的那张表」(见下方 ④ 前的粗体条),
> 但没有工具能产出「那张表」 ⇒ 于是 EP04/EP05/EP06 连着三集都把 stdout 手抄进录音稿。
> 手抄必错(抄错数、抄漏段),而且改了稿下次不会同步(表里是死的)。
>
> 修法:count_narration.py 加 --md,末尾多打印一张分段表(段/字数/纯念/停顿/合计 + 合计行)。
> ⇒ 录音稿里那张表一律粘贴生成,永不手打。
> 回归:不加 --md 时 stdout 逐字节与旧版一致(只多一个 flag 分支,不改口径)。
>
> ★ 配套放置位置:这张表必须放在 &lt;!--BODY_START--&gt; 之前。
> 放在块内会被算进正文(表格行虽被 skip_line() 剔掉,但块内出现 ## 会把分段逻辑搅乱);
> 放在块外则整段不进 re.search 的捕获组,零风险。
> 自检一行:python3 -c &quot;raw=open(p).read(); print(raw.index(&#x27;| 段 | 字数&#x27;) &lt; raw.index(&#x27;&lt;!--BODY_START--&gt;&#x27;))&quot; → True。
>
> ★ 口径提醒必须跟着表一起给用户(两张表会让人当真去掐秒):
> ① 2.75 是四集成片反推的均值(极差 3.1%),实际 ±5% 正常,不许为凑表上的秒数去赶;
> ② 表不许用来判「超时,要砍内容」 —— 真值以录音实测为准。
>
> ★ 顺带固化:count_narration.py 只用 stdlib(re + sys),
> 故任何 python 都能跑(/usr/bin/python3 亦可)—— 它不吃 PIL/numpy,
> 别因为「托管 3.13 没装 PIL」就以为它跑不了。

口径 值 备注
语速基准 2.75 字/秒(稿内 0 个 〔停 时用) 🚨 2026-10-03 重标,旧值 2.65 已作废 —— 四点实测(见下)。不要用 250 字/分
语速基准(稿内有 〔停) 仍是 2.75 + 显式累加停顿 🚨 2026-10-04(EP06)更正:此前本行写「2.98 字/秒」,是错的。 代码里从来没有 2.98,那是写文档时留下的一句设想 ⇒ 与实现不符。停顿只做「另加」一次;若再把纯念语速也调高,等于同一件事算两遍。脚本自己的文件头已声明此条(count_narration.py L42–47)
显式停顿 脚本累加 〔停 N 秒〕 优先口径。稿里标了停顿就一定能算出来,别再用 ×1.09 估
段间停顿(无标注的旧稿) ×1.09 只在稿子没有任何 〔停 N 秒〕 时用
镜头需求 时长 ÷ 2.75 s/镜 写稿时就知道要选多少片

> ## 🚨🚨 2026-10-03:语速系数 2.65 → 2.75(推翻 2026-09-26 的旧标定)
>
> 四点独立实测,极差 3.1%,均值 2.753:
>
> | 源 | 纯念稿字数 | 成片旁白音频 | 系数 |
> |---|---:|---:|---:|
> | 凤凰古城 | 725 | 266.581 s(04_audio/旁白_凤凰_成片.wav) | 2.720 |
> | EP01 东湖 | 1024 | 365.124 s(02_work/旁白_剪辑版_v2_48k16mono.wav) | 2.805 |
> | EP02 东关街 | 1127 | 412.610 s(02_work/narration/旁白_定版_v3.wav) | 2.731 |
> | EP05 北京双寺 | 956 | 346.600 s(04_audio/旁白_定版_48k.wav) | 2.758 |
>
> 四点预测误差均 ≤ 2.4%(均值 1.0%)⇒ 取 2.75。
>
> ★ 旧值 2.65 为什么错 —— 是"取错了分母",不是数值微调:
> 旧标定写「727 字 ÷ 272.901 s = 2.66」,但那 272.901 s 是 05_record/ 里的原始录音
> (含念错、重念、卡壳),而 725 字来自剪干净之后的纯念稿。
> ⇒ 拿干净稿去配带废片的录音,等于把废片时间也算成朗读时间,系数被系统性压低。
> 改用同一支的成片旁白 旁白_凤凰_成片.wav 266.581 s ⇒ 2.720 ✔ 立刻归队。
>
> ★★ 两条口径铁律(违反就错):
> 1. 分子与分母必须同为「成片状态」 —— 干净稿 ↔ 成片旁白音频(绝不配 05_record/ 的原始录音)
> 2. 分母只取 04_audio/旁白_定版*.wav —— 成片 ffprobe 的总长含尾板,不是旁白长度
> (EP04 案:354.000 = 正片 346.593 + 尾板 7.400)
>
> ⚠️ 待查:EP03《安昌》835 ÷ 349.147 = 2.392,与四点差 13%,原因未查明 ⇒ 引其时长结论前先查清。
> ✗ 不可用:EP01 的文案(无 BODY 标记,字数含说明章节)⇒ 标定须改用其录音稿(1024 字)。
>
> ★ 影响:旧值 2.65 会高估时长约 3.5%(保守方向,不会超时,但会让人误以为还能多写)。

> ### ★★ 2026-10-03 相册电影线补三条(EP04《苏州 · 陆巷》案)
>
> ① 「文案 md 必须包 &lt;!--BODY_START--&gt; / &lt;!--BODY_END--&gt;」这条,相册电影线一样适用。
> EP04 文案最初没写这两个标记,脚本遂退回全文件核算(有 stderr 警告),
> 把「★ 点细节配额」「🚫 三条主动不写」「⚠️ 四处不断言」等说明性章节全算进了旁白 ⇒
> 报出 1,050 字;加上标记后立刻回落到 1,218 字 ✅。
> ⇒ 凡本脚本要核的稿子,先问一句「BODY 标记写了没」。
>
> ② 「各拍标题里不许写字数」—— 我又犯了一次。
> EP04 首稿每拍标题都写着 `54 字 · ≈ 20 s`。即便是脚本回量的值也不许写:
> 读者无法分辨哪个是脚本产出、哪个是手写,下次改动就会只改一处。
> ⇒ 字数只允许出现在「脚本产出的那张表」里。
>
> ③ ~~2.65 字/秒 拿到第二次独立标定,系数可以放心用了。~~
> ⚠️ 这一条已于 2026-10-03 当天被推翻(见上方红框):
>
> | 错在哪 | 内容 |
> |---|---|
> | 分子错了 | 写的 941 字,实为 956 字(EP05 文案当时没加 BODY 标记,脚本退回全文件;加上标记后是 956) |
> | 分母错了 | 写的 354.000 s,那是成片容器总长(正片 346.593 + 尾板 7.400),不是旁白长度 |
> | 正确值 | 956 ÷ 346.600 = 2.758 |
>
> ★ 教训:一个"看起来验过了"的标定,可以同时错在分子和分母上。
> ⇒ 标定三问:① 分子是脚本产出的吗?② 稿子有 BODY 标记吗?③ 分母是旁白音频还是成片容器?
> ★ 反向教训仍成立:EP05 文案页首当时自己写着「1,192 字」——人算出来的数,连自己的稿子都骗得过。
>
> ④ 别自造统计脚本(第四次同一类错)。
> 本轮我为核算 EP04 又写了一个 tool_count_script.py,而本技能第三节标题就写着
> 「凡本技能有现成脚本,一律用现成的」。该脚本已删除,EP04 数字改用本脚本复核并完全一致(1,218)。
> ⇒ 动脚本之前先翻技能;自造 = 制造第二套口径。

脚本剔除口径(2026-09-24 补,2026-09-26 修正):&lt;!--BODY_START/END--&gt; 之外一律不算;
▸ 贴片位标记不算(🆕 2026-09-26:原先漏了,把 19 行贴片标记 + 11 个段名当成了正文,
《寒山寺》一度算出 1845 字,真值 1409 —— 配 2.65 字/秒高估 2 分 44 秒);
# 标题/段名、&gt; 引用块、| 表格行、--- 都不算;
〔...〕 方括号提示不算(那是记号,不是要念的字);〔停 N 秒〕 不算字、但要单独累加进"含停顿"时长。
→ 所以录音稿可以直接拿这个脚本核,不必另写一个。

★ 语速基准的标定 —— 现行值见上方红框(2.75)。以下是旧值的错因留档,别再用:

口径 字数 分母 判定
旧:不含标点 727 272.901 s(原始录音,含废片) ❌ 2.66 —— 分母取错,已废
旧:含标点 839 272.901 s ❌ 3.07,本来就偏高(口径错)
新:不含标点 725 266.581 s(成片旁白) ✅ 2.720 —— 归队

> ★ 关键教训:旧标定看上去"验过"(回归误差 0.4%),却是错的。
> 因为它分子分母不同状态 —— 干净纯念稿 vs 带废片的原始录音。
> ⇒ 对表对得再准,也可能是在对一个错的靶子。(此即「自证<EXIF<平台裁定」里的"自证"陷阱)
> ⇒ 标定前先问:分子分母是不是同一支、同一状态、同一口径?

> ### 🚨🚨 2026-10-01 东湖案:旧系数会虚高约 10%,别拿它判"超时"
>
> 标定源:东湖 录音稿_东湖_纯念版_v1.md(1,024 字)↔ 实测 旁白_剪辑版_v2_48k16mono.wav(365.124 s)
> ⇒ 1,024 ÷ 365.124 = 2.805 ✔ (与本表四点吻合,是"旧值偏低"的又一个独立证据)
>
> 为什么旧值差这么多:凤凰那条稿一句 〔停 N 秒〕 都没有(自然停顿已含在系数里);
> 而稿子一旦标了大量停顿,同一系数就会二次计入,越标越高估。
>
> ⇒ 判据(已按 2.75 更新;★ 2026-10-04 EP06 二次更正):
> - 稿子里 〔停 出现 0 次 → 用 2.75(含自然呼吸)
> - 稿子里 〔停 出现多次 → 仍然用 2.75,再显式累加 Σ 停顿秒
> (~~2.98 那一支已删~~ —— 代码里从来没有 2.98,见 count_narration.py L42–47)。
> 本集实测:EP06 1,032 字 ÷ 2.75 = 375 s(6′15″)+ 9×2 s = 393 s(6′33″)✅
> - 两种算法都不许用来判"超时,要砍内容" —— 误差 10% 在 6 分钟的片子里就是 40 秒,
> 足以把一个合适的选题误判成"太长"。实测录音回来再定。

> ★★ 这一条比它看起来重要:文件头曾写「来源:凤凰实测 717 字/272.9 s」,
> 而脚本数出的是 725–727 字 —— 差 10 字,量级上没暴露问题,口径却整体错了 3.5%(且此前自认"验过")。
> 凡工具的产出数字,先拿一个有实测答案的案例验证过再信它 —— 而且要验"分子分母是否同状态"。
> (本轮差点把《寒山寺》9′57″ 报成 12′41″ 交给用户。)

🚨 各拍标题里不许写字数。 手写的字数与脚本实测必然打架
(本轮实测:手写「约 340 字」的那拍实际 216 字)。
字数只允许有一个来源 —— 脚本产出的那张表。

🚨🚨 ④ 别自写统计脚本 + 注音必须用 〔〕(2026-10-01 东湖案,连错两次)

事故:核算《绍兴东湖》录音稿,我没用本技能自带的 count_narration.py,自己现写了一个,
从 ## 正文 一刀切到文件末尾 —— 把后面的《录音环境清单》和《念法提醒》两块说明文字算进了旁白,
报出「1,337 字 ⇒ 8 分半」,还据此对用户宣布「估差 30%」并启动了「要不要砍内容」的讨论。
真值 1,025 字 ⇒ 6′46″。教训的分量在于:同一个错,技能里早就记过一次
(寒山寺「▸ 贴片标记被当成正文,虚高 435 字」,见上)——我等于把已交过的学费又交了一遍。

★ 铁律一:凡本技能有现成脚本,一律用现成的。
它的口径是拿凤凰实测 272.901 s 标定过的(回归误差 0.4%),自写脚本没有这个背书。

★ 铁律二:录音稿必须写 &lt;!--BODY_START--&gt; / &lt;!--BODY_END--&gt;。
不写的话脚本只能退回全文核算(会 stderr 警告),说明性章节就有被算进去的风险。
本片加上区块标记后,脚本逐段输出与人工逐段相加完全一致(1,025) —— 这就是正确姿势的交叉验证。

🚨 ⑤ 注音/拼音标注必须用 〔〕,绝不能用 ()

本技能约定里 〔...〕 是唯一的「不念」容器,count_narration.py 的 strip_cues() 会自动剔除它。
而 () 没有任何特殊地位 —— 脚本的 spoken() 会把里面的拼音当成要念的拉丁词计进字数:
〔dàng〕 剔除 ✓,(dàng) → 算 1 个词 ✗。

实测:《东湖》录音稿在正文里标了 16 处 (pīn yīn),
脚本因此多算 43 字(1,068 → 改成 〔〕 后回到 1,025)。

✅ 石宕〔dàng〕 遗址        ← 脚本自动剔除,不影响字数
❌ 石宕(dàng) 遗址        ← 被算成 1 个口播词

配套:在稿子的「记号表」里写明 「方括号里是拼音提示,不要念出来」。
★ 只标真生僻易错的,不标常见字 —— 标多了正文全是括号,反而碍读
(《东湖》第一版标了 20 处,剔除 郭沫若/隋/堤/峭壁 后留 16 处)。
{{释义|生僻易错的标准}}:① 多音字在本词里读的不是常用音(书院山长 zhǎng、砌 qì);
② 形近致误(冶 yě 不是「治」、镂 lòu 不是「lóu」);③ 专业名物(石宕 dàng、箬篑 ruò kuì)。

3.0.2 🚨 平台发布标题:上限 + 搜索词(2026-09-25 补,视频号上限写错过一次)

平台 上限 备注
视频号 16 字 🚨 硬上限。我一度按 22 字做,整池视频号候选(17–20 字)全部超限作废
百家号(列表页) 30 字 30 字内完整显示,信息量可以给足
公众号 20 字 超了在会话列表里截断
  • 上限只写在 scripts/title_len.py 的 LIMITS 字典里,别处不许硬编码。
    上限写错,脚本再准也白搭 —— 改平台参数先改这里。
  • 标题必须有「读者会敲进搜索框的实体名」,且放句首。
    全是代词(「它」「这座山」)= 平台不知道主题、读者搜不到 = 搜索流量为 0。
    推荐流吃钩子,搜索流吃实体名,是两笔账 —— 只写推荐流那半张卷子等于放弃长尾。
  • 实体名前置 + 一个具体年份/数字/动作 = 本号点击率最高的结构。
    只有搜索词=平淡词条;只有数字=看不懂的悬念。
  • 16 字的词账:两个实体名(「张家界」3 +「哈利路亚山」5)= 9 字,只剩 7 字给动作
    → 全给「主体/动作/结局」,连「的」都要省。形容词在视频号标题里是奢侈品。
  • 宁多 2 字也别留歧义:省掉限定词(如「石柱」)会被读成另一个主体(张家界市改名),
    标题歧义 → 点进来发现不对 → 完播率崩。
  • 写完标题必须回扫交付清单:验收_*.md、合规审查_*.md、上传即用_*.md、发布文案_*.md
    四处常残留旧标题 —— 交付文档里的标题是最容易变馊的东西。

④ 写超长了,别急着砍内容 —— 给三个选项写进文件(本轮验证有效):

选项 要写什么
A 照发 说明长在哪、为什么值这个时长
B 压缩 逐条列「删哪句/在哪/省几字」,并标明哪条强烈建议别删
C 拆两支 说明拆了会失去什么(弧线断在哪、落点会掉在哪一支)

用户拍板,不替他决定砍料。 删除顺序:先删「画面能代替的句子」→ 再删「节奏性短句」→
最后才删骨架(建立对照关系的那些事实)。

3.0.3 ★ 录音稿规范(本技能的核心交付物之一)

必须在用户录音之前交付,格式定死,别让用户猜。 记号约定:

记号 含义
〔〕 里的文字 不要念 —— 是提示,念时眼睛跳过
〔停 N 秒〕 真的停够 N 秒
空行 自然段的呼吸(短停)
行尾标点 有意的停顿,照着停,不要一路念到底
〔放慢〕〔加重〕〔压低〕〔语气松下来〕 语气提示
生僻地名 括注拼音,如「盱眙〔xū yí〕」—— 给用户看的,让他知道念什么就行
方言读法/要念出来的音 必须换成同音汉字,不能只写拼音字母(写 gǎi,人会念成「g-a-i」)。<br>写法:《张家界》「这个字不念「界」,念「改」」+ 旁注〔按当地口音念〕
阿拉伯数字 一律改汉字念法(1994→一九九四年、179→一百七十九个、243→二百四十三座)→ 照字面念就不会念错
【第 N 段】 一段一个文件,段界卡在章节卡上

> ### 🚨🚨 2026-10-04(EP06)修两处自相矛盾的旧条文(曾连害三集)
>
> ① 本节原写「每段结构:【段号】章名 / 字数 / 预计时长 / 朗读提示 / 正文」
> —— 这与 §3.0.1 的粗体铁律 「各拍标题里不许写字数,字数只允许有一个来源 —— 脚本产出的那张表」
> 直接打架。后果实测:EP04/EP05/EP06 连着三集都在段标题上手写字数,每集都要回头擦掉。
> ⇒ 已删。正确做法:段标题只有 【第 N 段】章名;
> 字数/时长一律由 count_narration.py --md 产出一张独立的表,放在 &lt;!--BODY_START--&gt; 之前。
> (该 flag 于 2026-10-04 新增 —— 因为「要产出那张表」以前没有工具,见 §3.0.1 ③ 的红框。)
>
> ② 拼音示例原写「盱眙(xū yí)」—— 用了 ()
> —— 与 §3.0.1 ⑤ 「注音必须用 〔〕,绝不能用 ()」 打架。
> () 里的拼音会被 spoken() 当拉丁词计进字数(东湖案实测多算 43 字)。
> ⇒ 已改为 〔xū yí〕。 范例是会被照抄的 —— 错的范例比没有范例更坏。
>
> ★ 教训归纳:本技能的「铁律」与「示例/格式说明」是分两次写的,
> 后写的铁律没有回头清理先写的示例 ⇒ 示例成了唯一被真正执行的那个。
> 凡新增一条铁律,必须 grep 全文找与之矛盾的老示例,一并改掉。

分段录制法(真人同样按章分段,理由和 TTS 一样充分):
全片旁白通常 1200 字上下,按章切 8 段,每段 130~200 字。
- 一段一个文件 → 某段念砸了只重录那一段,不用全念
- 段界正好落在章节卡上,语气自然跟着章节走
- 用户每段开头先停 2 秒再开口、结尾停 3 秒再按停,给剪接留余量

★ 目标时长取自原录音该章实测(t_out − t_in),不是按字数估算 ——
用户照着念时能自己对着秒表找节奏。

★ 标点不能省:字幕和录音稿是两份文本。字幕不加标点(画面干净);
录音稿必须加 —— 标点决定停顿与语调升降,没标点就念成一串没起伏的流水。
(这一条原本是为 TTS 总结的,真人念稿同样成立。)

录音操作提示(跟着录音稿一起交给用户):
关窗 / 关空调(低频嗡嗡是录音头号杀手)、手机开飞行模式、
衣柜前或挂条被子录最安静、手机离嘴 15~20 cm 偏 45° 防喷麦、
手机自带「语音备忘录」就够不需要买麦。

★ 念错的处理(这条最省用户的事,必须写进稿子):
不要回退去删、不要重录整段、不要自己剪 ——
停下来,把这一句从头再念一遍,然后继续往下。
AI 后期会在每个接口逐刀核对,把错的、重的全部剪掉。

★ 从用户即兴录音整理成稿时,必须附「改动说明」:
逐条列(原话 → 改动后 → 为什么)。这是诚实问题 ——
不能让用户以为念的还是他原来的话。同理,成片字幕不得擅自改写(见 references/common-standards.md)。

★ 从书面文案改成口语录音稿,同样要附「改动说明」+ 报净增字数。
口语化必然增字(加连接词、拆句)。《张家界》实测:文案 1,471 → 录音稿 1,558,净增 87 字,
全是连接词
(「我先说一件更早的」这类)——这个数要主动报出来、说明来源,
否则用户会以为你背着他改内容。表里逐条写「原句 → 改成 → 为什么」,并声明"事实、数字、判断一个没动"。

★★ 录音稿必须口语化(2026-09-23 用户明确要求:「全文口语化一些吧」) ——
书面旁白稿 ≠ 录音稿。用户要念的是一段话,不是一篇文章。 三条手法,实测有效:

  1. 开场换悬念,别用新闻开头。新闻导语式开场(「X 月 X 日,某省宣布……」)一念出来就是宣传片,
    与「不做新闻宣传式」的调性自相矛盾。先抛两个具体的东西制造反差,把新闻压到第 3 句之后。
    > 实例《两座金山》:①「扬州湖心一座小山/镇江江边一座山,名字只差一个字」→
    > ②「更怪的是——扬州那一座,它不是山,是人堆出来的」→ ③ 新闻才出场。
  2. 加口语连接,每拍 2–4 处:你算算/什么意思/你先别急着笑/换句话说/你看。
    密度高了一定油,它是盐不是菜。
  3. 允许单句成段:「不高。」「赢了。」「但名字肯。」——
    念的时候自然就慢下来了,比写「〔停 1 秒〕」更管用(二者可叠加)。

★ 时长听用户的,不擅自砍内容(2026-09-23 原话「只要吸睛,不怕长!」):
篇幅由内容定。要压就把「删哪句 / 在哪 / 省几字」列成表放稿尾并换算成秒,
再附一句"我的意见是别删"+ 理由,让他自己选。不要先斩后奏。

★ 第一人称纪实里「体感」不代写(2026-09-24《张家界》):
凡是「那天站在那儿,什么感觉」这类句子,只留一个 〔★ 现场感受位〕 空位让用户自己说。
AI 替他感受 = 自毁第一人称纪实(他的原话是「我不替你写」被用户接受为规矩)。
但空位要给足提示:"说一句就够/说完停 1 秒接着往下念/想不出可跳过,不影响逻辑"——
别让他对着空位卡住。留空位 ≠ 甩包袱。

★ 录音稿尾部四件套(全部标注「不念」):
① 可删清单(删哪句 / 在哪 / 省几字 ≈ 多少秒)② 合规自检表(评价政策?/为景区说好话?/宗教/站外导流,逐条打勾)
③ 录音环境 + 念错处理(见上文)④ 录完之后我做什么(1→N 步,让他知道稿子交给谁、下一步等什么)。
> 第 ① 项的顺序:从省得多的往下排,并单独标出「哪一条强烈建议别删」。
> ⚠️ 删减表若从别处(如文案 §六)抄来,必须回查到录音稿里确有此句,且对仗句/骨架句不许进表
> (《张家界》就遇到:旧表列到「一边…一边…」那两句骨架,已在录音稿里纠正)。

交付方式:01_source/录音稿_&lt;片名&gt;_纯念版_vN.md(与文案同目录,方便逐句对照)+ 字幕另出。
别只留一份"最终版" —— 文案留着做核查底稿,录音稿稿首写清「这份是念的,那份是核的」。

★★ 贴片位(资料卡 / 公版图)必须标进录音稿(2026-09-26《寒山寺》新增):
片子若有贴片位表(《寒山寺》19 处),录音稿要在对应句子的前一行加
▸ 贴片 #N|内容(停留 Xs)。
- ▸ 行不念 —— 写进稿首约定表,否则用户会把它念出来。
- 一律标在「触发句之前」(贴片在念到下面那一句时出现)。
⚠️ 实测过我一份稿里写了两套规矩(13 处放前、6 处放后)——同一份稿只许一套。
- 为什么必须标:位表锚在句子上,录音稿是唯一能对上「用户实际念到哪」的文本。
不标 = 后期只能靠听,19 个落点全成手工活。

★★ 录音稿交出去之前,必须跑脚本复核(不能肉眼扫):

查什么 判据
段号连续 1..N 无缺无重
贴片位齐全 编号排序 == 1..N
贴片锚点位置 每个 ▸ 之后的第一句,必须命中位表里的触发句(脚本核)
文案关键句覆盖 抽 80–100 个特征词,命中率应 ≥ 98%(不中先怀疑探针写错,再怀疑漏句)
字数 / 时长 / 镜数 见下两条
旧数字 / 内部术语残留 干净(见下条)

★★ 交付稿里不许出现只有我们自己懂的术语(2026-09-26):
「钉子」「落点」「锚句」这类内部叫法,念的人看不懂。
→ 写进 〔〕 提示时一律换成人话:
「全片第一个钉子」→「六个字放慢,一个字一个字说」;
「〔一个钉子〕」→「这句要落稳」。
★ 判据:交付稿的每一行,都要能假设「用户完全不知道我们的工作流程」还读得懂。

★ 提示与停顿要分行写(2026-09-26):
念法提示和停顿时长混在一个 〔〕 里(如 〔&quot;X&quot;这句别赶;念完停 1.5 秒〕),
录音时视线找不到「停几秒」。→ 拆两行:〔念法提示〕 + 〔停 1.5 秒〕。
副产品:停顿数从"估算"变成可脚本统计的准确值(《寒山寺》实测 42 处/65 s)。

★★ 字数一律脚本核,不手估(2026-09-26 血的教训):
《寒山寺》文案页首自称「1,348 字」是估的,脚本实测 1,396 字,偏小 48 字。
→ 凡「字数/时长/镜数」,写进任何文档之前先跑一遍脚本。
口径统一用「不含标点、只数发得出音的字符」(标点不发音,但停顿单独算秒)。
语速基准 2.75 字/秒(2026-10-03 重标,四点实测;旧值 2.65 已作废 —— 见本文件上方红框)。

★★ 录音稿一出来,必须重算素材账(2026-09-26 新增):
录音稿必然比书面稿长(加了停顿),镜数会涨 —— 但贴片位不是实拍镜,
它占的时间不消耗照片。
《寒山寺》实测:折 217 镜 vs 素材 204 张,看着不够;
扣掉 19 处贴片(≈68 s ≈ 25 镜)后 实拍 ≈192 ≤ 204,够。
这个账不主动算,到选镜时才发现,就会被逼着复用或临时找素材。

3.0.4 ★★★ 旁白结构:钩子框架的长视频变体(2026-10-01 新增)

> 来源:~/.workbuddy/skills/content-hook-design/SKILL.md(「山海墓欲」九元素的本项目适配)
> 这是旁白「写什么」的骨架;§3.0.1 管「立意」,本节管「结构」。

★ 第一判据(最硬的一条):画面给「看得到的」,旁白给「看不到的」。
旁白不许描述画面 ——「这是一张全家福」= 白扔一镜。
画面已经给观众看过了,旁白再说一遍就是零信息。

六段骨架(长视频变体)

段 做什么 字数 硬要求
① 立缺口 抛一件没说完的事 30–50 不用抛反常识(图文的玩法),照片电影要的是"这后面有故事"。禁新闻式开场(既有规则)
② 铺共识 观众以为的版本 100–150 不铺,第③段的翻转就没有落差
③ 转折 「直到……」 100–150 必须带具体的人 / 时间 / 动作,不许抽象
④ 拆解 ×2–3 按非时间逻辑层层深入 每层 150–300 段末留半句;小标题式的话不写进旁白
⑤ 落具体 点出照片里的一格细节 100–200 「具体」的唯一兑现处
⑥ 收束 回扣①那个物件 / 那句话 30–60 不喊口号

★★★ 相册电影里,四个元素的唯一合法形态(其余形态都是假的,别硬凹)

元素 相册电影里的落点 说明
山(对抗) 只有「时间」 —— 人 vs 衰老、离别、消失 照片是事后的、温和的,没有人和环境的对抗可用。硬造就是假的
墓(越界) ★ 照片边框外的那一格 —— 拍这张时发生了什么、谁在按快门、前一秒/后一秒 这是照片电影独有的资源:照片只给一帧,旁白能还给你那一帧之外的现场。比任何"揭秘"都值钱
欲(悬念) 前 20% 埋一个物件或半句话,后 20% 回收 相册电影的悬念不是"结局",是"这张脸后来怎么样了"
海(层层深入) 见下 §4.8 第 5 条:主结构必须不是时间线

三条频率判据

  • 每 4–6 镜(≈每 30–45 s)必须有一个「可是 / 后来才知道」
  • 一集里至少 3 处「点细节」(手边的搪瓷缸、背景里的缝纫机、墙上的日历)—— 这是「具体」的兑现,也是观众"这是我"的来源
  • 换一版旁白 = 全链重锚:④⑤⑥ 若锚在秒数上,必须重算(既有铁律)

★★ 红线(照片电影特有):照片背景故事一个字都不许编。
「这是最后一张合影」「这卷胶卷放了二十年」这类代价句最有杀伤力,也最容易编 ——
只能用 EXIF / 当事人确认得来。核查连「像常识的数字」一起核(既有铁律)。


3.1 ★★ 第一步不是找剪点,是「逐块转录」(2026-09-24 定案)

为什么:全局 ASR 的句子时间戳不可信 —— whisper 的词级时间戳是段内线性插值,
-ml 切段后误差可达 ±2 s。拿它去定剪点 = 慢性翻车。

本次实测:用全局时间戳做的一版 EDL,13 刀里 3 刀切错 ——
① 把该留的「同治年间」整块切掉(「江沙淤积」的真实位置比 ASR 报的早 0.9 s);
② 把「才跟南岸连成一片」从中间切断;
③ 「江沙淤积」他其实念了三遍,ASR 只报了两次。

正确做法:按静音把录音切成「语音块」,每块单独送模型。

# 1) -50 dB 阈值 + 合并间隔 &lt; 0.30 s → 语音块(块边界即静音,天然适合下刀)
# 2) 逐块导出 wav(尾部补 0.25 s 静音,防模型吃掉尾音)
# 3) 8 线程并行 whisper-cli,每块一次调用 → 得到「每块到底说了什么」
whisper-cli -m &lt;模型&gt; -f blocks/b0007.wav -l zh -t 2 -np -nt -otxt -of blocks/b0007

收益:块时间戳是实测边界而非插值,重录段一眼可见。
本次 234 块 / 8 线程 ≈ 1 分钟跑完全片。之后 EDL 直接用「块号」表达,不再写秒数。

两个必配的招:

  1. 可疑块必须无 prompt 再转一次 —— prompt 会把幻觉"喂"进输出
    (本次多块在纯噪声上吐出 作词:李宗华、小红桥,小红桥,石狮子...=prompt 回吐)。
  2. 用相对电平判噪声块 —— 全片块 RMS 取中位,低于中位 9 dB 以上 = 噪声块
    (本次 5 块 −31~−35 dB,全是幻觉)。
    ⚠️ 边界:−6~−9 dB 的弱块是真的轻声说话(本次"不高。""个子远""才搬上来"都在这一档),别删。

  3. ★★ 低电平只是"嫌疑",不是"判决"(2026-09-24《张家界》三条差点误删的命案):

块 相对中位 ASR 说 真相 怎么查出来
62 +4.9 dB(电平完全正常) 「嵩梁山」 「飘过」("云海从脚下飘过") prompt 里有"嵩梁山"→ 幻觉回吐;无 prompt 重转 = "飘过"
93 −15.1 dB(像噪声) 「雲啊」 「赢了」(轻声说的,全片关键词) 带/不带 prompt 两套独立转录都是"赢了"才认
107 +5.8 dB 「三。」 「山」("山从来没动过"的头一个字) 与下一块「我从来没动过」连读才通

判据:噪声块必须「两套独立转录 + 位置语境」双证;只看电平必然出错。

  1. ★★ 大块(> 20 s)必须二级细分再转 —— 底噪偏高的停顿会把好几句话并成一个
    20–50 秒的大块(本片块 82 = 49.9 s),模型转长块会漏句
    (块 82 就漏了「同一年,他还替这一片地貌,挣回了一个自己的名字」整句)。
    做法:块内取中位、阈值 = 中位 − 10 dB,重切子块逐块转(sub.py block &lt;块号&gt;)。
    漏一句就可能让剪辑漏掉一整句旁白,而且验收时看不出来。

  2. 要删的句子落在块内部(块内切点)时,别猜静音点 —— 用「试切 + 无 prompt 转录」逼近
    (sub.py asr &lt;起&gt; &lt;止&gt; / sub.py probe &lt;起&gt; &lt;止&gt;):
    从候选位置取一段转出来,看开头第一个字对不对,逐步挪到正确边界。本次三处落点:
    段标题「1994年,他换掉了自己的名字」→ 切 190.70;「那年冬天。我到了那儿。」→ 切 344.95;
    重复句「可他本来是一个村的名字」→ 删 100.74–106.26。
    > 别在低电平处硬切:底噪高的录音里"最安静的点"也可能只有 −48 dB。
    > 切在字与字之间、再加 15 ms 淡入淡出,比追求"切在纯静音里"更实际。

  3. ⚠️ 本机环境三个坑(2026-09-24 实测,都会让脚本静默中断):
    - 不要用 librosa —— 它收尾时会清理自己的 util/__pycache__/tmpXXXX,
    被安全门禁 SAFE_DELETE_BULK_CONFIRM_REQUIRED 拦下 → 整个脚本中断。
    音频读写统一用 soundfile;RMS 包络手算(frame_length=400, hop_length=160, center=True
    与 librosa 同口径)。
    - 不要 shutil.rmtree —— 同理被拦(blocks/ 100+ 文件就过阈值 50)。
    改 os.makedirs(OUT, exist_ok=True) 覆盖写。
    - 模型文件先看体积:ggml-small.bin 只有 304 MB 就是没下完(正常 466 MB)→
    进程段错误崩掉,而 capture_output=True 把错误吞了,表现成所有块都「«失败»」。

3.2 判定「哪一块是念错的」

主判据:同一段文本在 15–60 s 内出现 ≥2 次 → 前是残句、后是重念,删前留后。

三条反例必须查:

  • 可能念了三遍甚至四遍(本次「江沙淤积」×3、「苏轼诗句」×4)→
    按块列出来数一遍块数,不要只信"重复检测报了 2 处"。
  • 脚本本身的合法重复不是重录(本次"一对狮子""一盘棋""何惜乎小"各出现两次)→
    必须拿录音稿逐条对照,不能只看重复次数。
  • 块级转录仍会误听专名("两米六长"→"两米六场"、"十船算一纲"→"石船算一钢")→
    这类不必改,但要在听审清单里标出来让作者亲耳确认。

输出物:剪切表_*.md(块号 / 原始区间 / 类型 / 内容)
+ 听审清单.md(必须亲耳确认的 3–8 处,带新时间轴时间)。

间隙处理:原始录音里作者会在段落间停很久(本次 8–13 s)。
按录音稿的〔停 N 秒〕重给:句间默认 1.2 s,剧本标了停顿的 2–3 s,段间 1.5–1.8 s。
每块前后各留 40 ms 气口 + 15 ms 淡入淡出。

爆音验收:剪后与原片的「二阶差分尖峰」应当完全一致(本次两者 max|Δ²| 都是 2.2119)。不一致就是接缝有咔哒。

3.3 ★ 剪后必须验证(逐刀核对,不能靠通读)

> 2026-09-24 补充:本节下面的「时间映射」法建立在全局 ASR 时间戳上,
> 已被 §3.1 的块级 EDL 取代 —— 用块号表达删除量时不需要换算,也就不存在换算误差。
> 但本节的核心纪律(逐刀核对、不许通读)依然成立,且新增一条硬验收 ↓

★ 剪后硬验收(两关,都要过):

  1. 残留重复数必须归零 —— 剪后重转,再跑一遍重复检测
    (≥4 字 / 间隔 3–40 s)。只剩录音稿本身的合法重复才算过;
    本次剪后剩 3 处,逐条对稿确认是"一对狮子/一盘棋/何惜乎小"的合法复现 → 才算过。
  2. 剪后全文与录音稿逐句对读 —— 用「段间隔 > 1.3 s 视为断句」聚合成句再读,
    确认没有缺成分、没有串词、顺序与稿子一致。

  3. ★★ 剪后验收有两类假警报,必须先排除再动刀(2026-09-24《张家界》):
    - 块尾回吐:块级转录在块尾(补的那 0.25 s 静音处)会"猜"下一块的开头 →
    验收时会看到「是一片|是一片世界自然遗产」这种根本不存在的重复。
    - 拼接点幻觉:ASR 会在刀口处把两侧文字合并读出来(本次「天桥布阱天桥布阵」),
    看着像重录残留,回原音复核只有一次。
    > 判据:块级/剪后转录在接缝处的"重复",一律回原音切同一区间用无 prompt 复转一次再定。
    > 代价对比:不回原音就改刀 = 白剪一遍 + 时轴平移 + 重渲染;回原音复核 = 一条命令 5 秒。

错误做法(v1 踩过):用 ASR 重转剪后音频,逐句读一遍看是否连贯。
系统性缺陷:只能发现「读不通」的剪法,发现不了「读得过去但缺了成分」的剪法。

本次实测代价:第 6 刀 (219.45, 222.93) 偏右 1.23 s,把该保留的「就能看懂」
一起切了 → 成片成了「要漫步其间,古代官府是怎么来运作的」,缺谓语。
通读时这句看上去通顺,就这么漏过去了;直到核对刀口才发现。

正确做法:在剪后音频的每个刀口位置,看它的前后文。

# 剪后位置 = 刀口 a − 前面所有刀的删除量之和
acc = 0.0
for (a, b) in CUT:
    pos = a - acc
    acc += (b - a)
    # 取剪后 ASR 中 pos 前后各一段文本,逐刀打印「前文 ‖ 后文」

判据:前后两段接起来必须是完整、能读通的语义单元;
后段若缺主语/谓语/宾语,就是刀口切进了保留侧。

原录音侧同样要核 —— 确认「打算删的那句」与「实际删掉的那段」时间对齐:

ffmpeg -ss A -to B -i 原录音 -af &quot;highpass=f=85,volume=7dB,loudnorm=I=-18&quot; \
       -ar 16000 -ac 1 seg.wav
whisper-cli -m &lt;模型&gt; -f seg.wav -l zh -otxt -of seg --print-progress false

注意事项:

  • whisper 句级时间戳有 ±0.3 s 粒度,机械比对(判断刀口是否落在句内)
    只能提示嫌疑、不能定性 —— 每一刀都必须切出来实测。
  • 「重说」类刀口的正确形态:左边界落在前一句尾部的停顿里,
    右边界落在重说句的句首之前。右边界尤其容易切多(本次就是)。
  • 一旦发现一处偏差,其余每一刀都要重新过一遍 —— 不能假设它们是好的。

改一刀的代价链:重剪 → 时轴整体平移 → 重渲染受影响镜头 → 重烧字幕 → 重合成。
本次(+0.93 s)约 20 分钟。所以宁可在音频阶段多花 5 分钟核对。

★ 时轴平移不要逐行改数字 —— 上百个时间点手改必出错。做法:
在字幕/点题卡定义之后加一次性换算,可追溯、可回退。

_SHIFT_AT, _SHIFT_DT = 198.28, 0.93     # 198.28s 之后整体后移 0.93s
def _s(t):
    return round(t + _SHIFT_DT, 2) if t &gt;= _SHIFT_AT - 0.01 else t
SUBS  = [(_s(a), _s(b), x) for a, b, x in SUBS]
CARDS = [(_s(a), _s(b), k, t, m) for a, b, k, t, m in CARDS]

3.4 ★★★ 2026-10-01 东湖 EP01 定案:整条转写 + 逐语音段打标签 + N 刀剪切

> 这一节是 §3.1/§3.2 的升级版。 §3.1 的「按静音切块 → 每块单独转」思路是对的,
> 但那一版要手工挑块;本节把它做成全自动、可复核、能一次剪 N 刀的稳定链路。
> 触发场景:用户已经把旁白录好了,里面有若干处念错 / 重念,衔接得很近,要细致剪干净。

为什么旧的「块级转录」还不够:

问题 表现 后果
whisper 段级时间戳会漂移 同一段音频,-ot 155000 -d 8000 与 -d 12000 对同一条给出 159.82 与 161.44 差 1.6 s。用户说"重念衔接很近",1.6 s 足够切错
手工挑块容易漏 一块 0.4–5 s,全片 150 段量级 漏一段 = 残句留在片子里

> ### 🚨 铁律:whisper 只用来「认字」,绝不用来「定时」,更不用来「判重复」。
> 所有切点一律取 prosody.py 的能量语音段边界。
> 语音段边界来自音频能量,是几何量;段间是真静音,在静音里下刀,
> 接缝逐样本跳变=0(东湖实测 9 处全部 0.00000),不可能有爆音。

> ### 🚨🚨 但这条铁律有个致命的边界 —— 东湖 v1 就是栽在这上面
>
> v1 靠「整条重转写 + find_flubs.py 差异块」验收,报了「7 处全部剪除、验收通过」,
> 实际漏剪 2 处,全是用户亲耳听出来的:

>
> | 漏剪 | 原始区间 | 为什么骗过了转写 |
> |---|---|---|
> | 「天下第一」重复 | 35.09–36.07 | whisper 会把重复内容抹平:32.31–39.71 有三段独立语音(「天下第一山水,」/「天下第一」/「天下第一山水大盆景」),任何窗口长度、带不带 prompt 都只读一遍 |
> | 「我当时」前多的「我当」 | 49.625–49.960 | 0.85 s 片段的尾部被语言模型吃掉:整段读成「这是个字啊」(5 字),把尾巴上的「我当」两个字盖掉 |
>
> 两条对策,缺一不可:
>
> 对策 A(判重):筛 prosody 语音段表里所有 0.4–1.3 s 的短段,看后面是否紧跟一个明显更长的完整句
> —— 这是假开头(false start)的指纹。命中了哪怕转写读得很通顺也要剪。
> python &gt; # 对 prosody[&#x27;speech&#x27;] 逐段:短段(&lt;1.2s) + 后隙(&lt;1.0s) + 后段更长 ⇒ 疑似假开头 &gt;
> 同时在全片转写里检索关键串,确认出现次数符合预期
> (东湖 v2:「天下第一」×1 ✓ /「我当」只出现在「我当时」里 ✓)。
>
> 对策 B(判字):短片段读不准时,去录音里找同一内容的真参照做声学对比,
> 别数音节(count_syllables.py 已知 9 字检出 13、已知 14 字检出 26,误差 1.44–1.86 倍)。
> 用「对数梅尔谱 DTW + 谱心/高频比/过零率」,东湖实测与真参照拉开 11 倍差距(详见 narration-qc §二·五 铁律 B)。
>
> 对策 C(最便宜的一条):交付前自己整条听一遍(≈6 分钟)。
> 「转写对得上」≠「片子对了」。 返工一次 ≈65 分钟,等于听 10 遍。

四步链路:

KIT=scripts
PY=~/.workbuddy/binaries/python/envs/default/bin/python

# ① 时长 + 韵律(150 个语音段,0 削顶)
$PY $KIT/prosody.py 旁白.mp3 --json 韵律.json

# ② whisper.cpp 字级时间戳(402 s 全片 41 秒,M2 Metal)→ 喂 align.py 排镜表
#    ★ 两个参数是 positional:&lt;音频&gt; &lt;输出.json&gt;
$PY $KIT/whisper_cli_words.py 旁白_16k.wav 字级.json --prompt &quot;专名…&quot;

# ③ ★★ 逐语音段打标签(4 分钟)→ 地面真值映射表「第 k 段(t0~t1)=这几个字」
$PY $KIT/label_segments.py --wav 旁白_16k.wav --prosody 韵律.json \
      --out 语音段文本表.json --tmp _segs

#    用 find_flubs.py 出「稿子 vs ASR」的差异块清单(insert 多念 / delete 漏念 / replace 念错)
$PY $KIT/find_flubs.py --script 录音稿.md --words 字级.json --prosody 韵律.json --min 2

#    可疑区精查(--zone 是 t0 t1 两个值)
$PY $KIT/zone_probe.py --words 字级.json --prosody 韵律.json --zone 348 360

# ④ ★★ N 刀剪切(一次剪完,内置四项自检)
$PY $KIT/splice_multi.py --src 旁白.mp3 --cuts cuts.json \
      --out 旁白_剪辑版_v1_48k16mono.wav --meta 剪切清单.json

# ⑤ ★★★ 两项硬验收(v1 漏剪后必加,见上方「铁律的边界」)
#    5a. 从原始按 cuts 重切一遍 → 逐字节比对(证明「除这些区间外一个采样没动」)
$PY $KIT/splice_multi.py --src 旁白.mp3 --cuts cuts.json --out _verify.wav --sr 48000
cmp _verify.wav 旁白_剪辑版_v1_48k16mono.wav &amp;&amp; echo &quot;✅ 完全一致&quot;

#    5b. 回 prosody 扫「假开头」+ 全片检索关键串出现次数
#        (短段 &lt;1.2s + 后隙 &lt;1.0s + 后段更长 ⇒ 疑似;逐条对稿子判定)

# ⑥ 全链重锚(换了旁白必须重跑;不做这一步 = 后面全错)
$PY $KIT/whisper_cli_words.py 旁白_剪辑版_v1_16k.wav 字级_剪辑版.json
$PY $KIT/prosody.py 旁白_剪辑版_v1_16k.wav --json 韵律_剪辑版.json
$PY $KIT/make_sent_table.py --script 录音稿.md --words 字级_剪辑版.json \
      --prosody 韵律_剪辑版.json --out 句界表.csv --word-out 句界表_字级.csv

#    最后:出听审 mp3 交用户,并**自己先整条听一遍**
ffmpeg -i 旁白_剪辑版_v1_48k16mono.wav -c:a libmp3lame -b:a 192k 听审.mp3

> ★ 参数形式别记错:whisper_cli_words.py &lt;音频&gt; &lt;输出&gt; 是 positional;
> label_segments.py 用 --wav(不是 positional);zone_probe.py 用 --zone t0 t1。
> 写脚本前先 grep add_argument 看一眼,比事后 debug 快。

label_segments.py 为什么一定对:把 prosody 的每个语音段(0.3–5 s,段间是真静音)
单独切出来喂 whisper,得到「第 k 段 = 这几个字」的一字不差映射。
对稿子一看就知道哪几段多余,而且是整段取舍,不存在"切一半"的问题。

splice_multi.py 内置四项自检(跑完直接看输出):
① 切点电平自检(应为 −43 ~ −72 dB,真静音)② 时长校验(原→新,删除占比)
③ 峰值/削顶复核 ④ 接缝逐样本跳变自检 + 首尾 8 ms 淡入淡出。

> ★ 切点不落在真静音里也能用,但要量化:东湖第 3 刀(49.625,切在「这四个字」与「我当」之间)
> 切点电平只有 −36.6 dB。8 ms 淡入淡出兜住了爆音(跳变仍 0.000000),代价实测:
> 8 ms 淡出窗未淡出 RMS −41.3 dBFS → 淡出后 −44.5 dBFS(能量少 3.2 dB),
> 该窗落在「字」尾的自然衰减段(前后 25 ms 包络已从 −28.6 降到 −47.2 dB),
> 接缝后紧接 0.38 s 句间停顿(RMS −64.9 dBFS)⇒ 低于可闻阈。
> 以后遇到必须在连续语音里下刀的情况,照这套量一遍再放行。

东湖实测成绩(v2 定版):402.469 s → 365.124 s(−9.3%),9 刀全对;
峰值 −0.49 dBFS、0 削顶、9 处接缝跳变全部 0.00000(全片中位 0.000366);
底噪 −70.97 → −70.94 dB、语音电平 −25.71 → −25.59 dB,均无跳变;
从原始按 9 刀重切的结果与定版 cmp 逐字节一致;v1→v2 逐样本差异只有两处新接缝的 8 ms 淡入淡出窗
(max |Δ| = 0.0102 / 1,083 样本 / 0.0062%)。

> ★ 别忘了「疑似」与「判决」的分界:本片排除了多处 whisper 误报,未动刀。
> 口语里被 whisper 读成同音字的地方(「凿→钊」「宕→荡」「濬宣→瑞轩」「园冶→原野」)都不是念错。

> ★ 口语里自然添加的虚词不要删(「年代栏里面写着」「这个对联刻在这个霞川桥」)——
> 删了反而不像人说话。只有「整句重复/半截卡住/念错字」三类才剪。


4. 镜头表(EDL)★ —— 决定「工整」的唯一环节

成片「旁白与画面对不对得上」几乎全由这一节决定,跟运镜、调色、字幕都无关。
第一版常在这里做错,且错得不明显 —— 成片「像那么回事」,但观众总觉得画面跟不上话。

4.1 ★ 组边界 = 句子开口时刻(不要按章权重均分)

错误做法(v1 实测踩过):先划章边界,再在章内按权重把照片均分。
后果:章边界本身对得上语义段,但章内每个切点与句读完全无关 ——
每句字幕跨 2~3 个镜头,且切点随机落在句子中间。

正确做法:以字幕句为原子单元。

# b = 每句的开口时刻;第 i 组区间 = [b[i], b[i+1])
b = [SUBS[i].t_in for i in range(N - 1)] + [TAIL_START, TOTAL]
span = b[i + 1] - b[i]
n = len(POOL[i])                       # 该句的镜数(池长即镜数)
d = round(span / n * FPS) / FPS        # 组内均分 + 1/30 s 帧对齐
# 末镜吸收帧对齐误差,保证整片总长精确

三条要点:

  1. 句与句之间本来就有停顿,把它吸收进上一组(当作呼吸)——
    不要让画面停在某句话中间。这样切点 100% 落在句界。
  2. 画面在旁白说下一句的「同时」切换,观感上就是「话到画到」。
  3. 目标镜长 2.5 s:n = round(span/2.5);若 n==1 且 span&gt;3.6 则拆成 2 镜
    (避免单镜拖到 4 s 以上)。末尾 1~2 s 的残句并入前一句,别单独成组。

4.2 ★ 逐句绑定语义照片(观众一眼看得出对不上)

v1 是「章内照片按顺序播」→ 讲到「签筒」时画面可能还是别的照片。
v2 给每句一个专属照片池,池长 = 该句的目标镜数,顺序人工排列。

配图前必须先建照片索引(这一步不能省):

# 带编号的接触表:编号画在左上角(黑底金字,缩略图下也看得清)
# 5 列 × 5 行 = 25 张/页,单格 700×525 → 293 张共 12 页
# 然后逐页肉眼过,记录「cXXX = 什么」

本次正是靠这一步才发现了几处实质性配错:

旁白 想当然的配图 实际应为
「正面刻着三个字:公生明」 任意石刻局部 c030 —— 「公生明 廉生威」官箴碑,旁附包公像
「背景是海水朝日图」 大致的大堂内景 c063 —— 大堂背景墙就是海水朝日图
「签筒分为红绿签」 公案全景 c078 —— 公案特写,红绿签筒清晰

⚠️ 配不上就要如实说,不要假装配上了。
本次相册里根本没拍到戒石坊背面的十六字(那张是进士题名碑),
只能用相邻石刻镜托底 —— 交付文档里必须写明这一点。

★★ 4.2.1 接触表只印一个 ID + 绑完必须按镜号复核(2026-09-22 凤凰案)

事故:接触表格子里同时印了「全局序号 + 照片编号」(如 154 c657 01-04 09:17)。
挑图时按序号定位、抄写时抄成了照片编号 —— 想把序号 154(拱桥+亭)写进镜表,
写成 c154,而 c154 是一位苗族老人的特写肖像;那一镜的台词是「凤凰古城评上了国家 5A 级景区」。
如果没做复核,它会一路渲染进成片。

两条铁律:
1. 接触表格子里只印「照片编号 + 拍摄时刻」,不要印全局序号 —— 两个 ID 并存 = 必然抄错。
2. 绑完图必须再生成一张「按镜号排序」的复核表(07_work/preview_shotlist.py),
每格标「镜号 / 段号 / 照片 / 时长 / 运镜」,逐格核对「画面 vs 该镜台词」。
★ 凤凰这一步抓出 4 处配图错(抄号 1 处 + 凭印象记错画面 3 处),成本只是读 4 张接触表。
别指望「挑图时的记忆」:97 张图看过一遍之后,编号和画面必然对不上 —— 这是常态,不是疏忽。

顺带:pick_candidates.py(程序化出候选池:dHash 去重 + 按时段配额)→ 接触表逐张挑 →
preview_shotlist.py 复核 → 生成镜表。三段闭环,缺第三段就等于没验收。

★★★ 4.2.2 从缩略图读出的事实,必须回 CSV 原文核对(2026-10-02 安昌案,代价 = 剪错了一句正确的旁白)

事故:读 6 页全量接触表(14 列 × 7 行、格 300 px、每格下方一行 26 px 小字)时,
把 c0345 的标签 09:29:15 读成了 11:09,据此判定
「文案说『九点半在酱园』,但照片证明酱园是 11 点拍的 ⇒ 文案编造 ⇒ 剪掉」。
实际 c0345 = 09:29:15,酱园正是九点半 —— 旁白本来就对,是我剪错了。

同一批还把小图内容读反了三项:

编号 真实内容 我读成
c0346 墙上石匾「中国银行」 「酱园」
c0350 石拱桥 「酱缸」
c0355 石阶码头 「斗笠盖的酱缸」

⇒ 段 6/7/14 的选片整体错位。而真正的酱缸在 c0366–c0371(09:34–09:35)、
真正的酱园店面是 c0224(09:05:53)—— 全是后来「定向放大」才认出来的。

根因:① 格 300 px、14 列,标签与缩略图视觉距离近,相邻格容易串行;
② 300 px 不足以判定「这是什么物体」 —— 石阶码头与「一排盖着斗笠的缸」在这个尺寸下几乎一样。

四条铁律:
1. 任何「看图得到的编号 / 时刻 / 机型 / 内容」在写进档案或据此决策之前,必须回 CSV
用程序打印核对
(编号 → 拍摄时间 / 可用 / 内容)。缩略图标签只用来「定位」,不用来「取证」。
2. 接触表分级使用:全量接触表(300 px、~100 张/页)= 只做区块级判断(「这一段是街景」);
任何要写进镜表的图都必须过「定向放大」(480–640 px、≤20 张/页)确认内容。
点名镜(旁白点出的实体:酱缸 / 鳊鱼 / 乌篷船 / 牌坊 / 招牌)必须逐张放大确认。
3. 「像某物」≠「是某物」。判物体看形状细节(缸是圆的、有口的;码头是斜的、有阶的),
拿不准就放大 —— 放大成本 = 读一张图,读错成本 = 整段画面错位。
4. 闸门挡下的照片,先问一句「它是不是这段唯一可用 / 最优的那张」。
本案 c0367 含「仁昌酱园」招牌,却被 face_lv=3 排除(人脸占短边 33%,face_box=0.031,-0.034,0.218,0.334)。
→ 处置:查 face_box 评估能否裁(人脸在左上、占 22% 宽 × 33% 短边,裁掉会毁构图)⇒ 放弃,改用 c0224 的店面正面。
判据:裁切后主体区域需保留 ≥ 70% 画面,否则换图。

★ 正面的收获(同一动作的收益):这一轮「定向放大」救回了四组关键素材 ——
仁昌酱园店面 c0224、成排酱缸 c0366–c0371、鳊鱼 c0256、
「做手工的人 + 一排陶罐」c0140(正是「靠一门手艺吃饭」那一镜的正解)。
它们在 300 px 全量接触表上只是几格毫无辨识度的小图。
⇒ 结论:定向放大不是「复核」,是「发现」。它既防错,也是把片子做准的唯一手段。

4.3 红线体检(每次改完 EDL 都跑一遍)

项 合格线
切点落在句界 全部(容差 1 帧,因为做过帧对齐)
相邻同运镜 0 处
相邻同照片 0 处
过短镜 &lt; 1.8 s → 0 处
过长镜 &gt; 4.3 s → 0 处
运镜族分布 相对均衡(in / pan / tilt 都要有)

自动化做,不要手工数。 改完配图池立刻重算,冲突项在脚本里定位到镜号再改。
按句子重建后实测:切点 71/71 落在句界、相邻同运镜 0、相邻同照片 0。

4.4 章节卡 / 片尾板的时间也必须卡在句界

  • 章节卡起点 = 该章第一句的 t_in,否则卡片会「切在话中间」
    (本次把第二章从 126.52 修正到 127.04、第三章 198.25→198.28 等)。
  • 片尾板起点必须晚于末条字幕结束 —— 重叠是高频错误,定完时间轴交叉检查一遍。
  • 片尾板的内容版式按发布方要求定;若发布方要求去掉片内音乐署名,
    把 CC BY 署名移到发布文案/简介里即可(署名在适当媒介给出仍满足该许可)。

4.5 ★★ 多图版式(九宫格 / 百叶窗 / 照片墙)—— 2026-09-27 新增

> 用途:片长不变,提高画面信息量。用户定调:「所有的排列方式都取决于节目进展的
> 节奏和能带动观众情绪的要求,灵活选择」→ 所以这不是"可选装饰",是节奏的表情。
> 工具:scripts/grid.py(通用,含 still/anim/demo)。
> 完整判据:博物馆_v1/07_strategy/画面版式谱_多图拼版_20260927.md。

★★★ 先记住三条实测结论(都反直觉)

  1. 裁切不是问题:源 3:2(1.5) 与格位 16:9(1.778) 接近,cover 只损失 15–18%,
    六种版式全部保留 80–82%。「横拍可合理裁剪」成立且代价极小。
  2. 总量守恒:画幅 207 万像素固定。九宫格每格 0.21 MP(单张的 11%),
    但一屏总量 1.89 MP ≈ 单张 1.91 MP → 多图不是加法,是重新分配:买广度,付深度。
  3. ★★★ 百叶窗不是"增加信息量",是"止损":
    竖图单放 16:9 只露 37.5% 高度(实测一张折扇被切成横杠,扇形完全消失);
    3 联露 90.2% | 4 联 67.4% | 5 联 53.6%。
    横屏里"高"稀缺、"宽"不稀缺 —— 竖图最经不起交高度,并排只交宽度。
    → 竖图集中的题材(扇面/立轴/碑刻/造像/匾额/廊道/夜景)一律成排,不要单放。

★★ 三步选择法(顺序不能跳)

① 先看这句旁白多长 → ② 再看要什么情绪 → ③ 才挑版式。
因为镜长由句子决定,不由版式决定(本技能按句界切镜):

句长 情绪目标 版式 每格手机 pt
<2.0 s 停顿/判断/落点 单张 379×208
2.0–2.8 s 平行陈述、对比、因果 双联 / 3 联 188×208 / 125×208
2.8–3.6 s 铺陈、琳琅满目 静物九宫格(3×3 或 3×2) 125×68
3.6–4.5 s 聚焦 + 环境 hero 一主五辅(主格 2×2) 252×138
4.5 s+ 震撼、"量" 照片墙(30×28 等超密格) 12.6×7.3

★★ 节奏铁律(比版式本身更值钱)

  • 开场 0–8 s 禁多图 —— 前 3 秒跳出率 >75% 的视频收益低 57%。开场多图 = 观众不知在看什么。
  • 情绪高点与收尾必须单张 + 长缓推 —— 多图会稀释。收束要让观众盯住一样东西。
  • 两个多图镜之间必须插单张 —— 连续多图 = 观众一直在"扫",累且不看内容。
  • 多图镜占比 15–25%(约 20–35 镜/139 镜)。超 1/3,完播率与情绪同时掉。
  • 单张永远是主力:照片电影的力量来自"凝视",多图是重音,不是节拍器。

★★ 三条纪律(用错会毁片)

  1. 同类才拼。9 件玉器 ✅;玉器+建筑+街景 ❌ 大杂烩。
    实测:混拼像"相册乱翻",改同类立刻变成展柜墙 —— 差别只在这一条。
    > 展品照天生适合拼版(单件居中、背景统一),风景照拼版必然乱。
  2. 概括句才配多图,特指句绝对不行。
    判据:旁白里出现「这/那/它」(特指)就不能多图 —— 观众找不着说的是哪一件。
    ✅「这个馆里有三万件藏品」/❌「这只碗上画着缠枝莲」。
  3. 有字的图不许裁:碑刻/匾额/书画/展板 —— cover 会正好切掉落款、年代、署名。
    只能单张不裁(contain + 底)或并排(并排裁的是宽,字通常在中间)。

★ 工程实现:零改造

拼版在素材预处理阶段做完 → 输出一张普通照片丢进 work/w/ → Ken Burns 照跑。
kb.py / render.py 一行都不用动。 镜表加两字段:

{&quot;n&quot;: 88, &quot;seg&quot;: 4, &quot;ph&quot;: &quot;g0088&quot;,
 &quot;grid&quot;: &quot;3x3&quot;, &quot;plist&quot;: [&quot;c0405&quot;,&quot;c0410&quot;,&quot;...&quot;],
 &quot;mv&quot;: &quot;breath&quot;, &quot;spec&quot;: true}
  • mv 只能是静止族(breath / wsHold / vsStatic)。
    ★ 九宫格严禁 pan/tilt/大幅推拉 —— 整块一动,边上格子先跑出画面,像"没对齐"。
  • 必须 spec: true —— 否则段首/段末全景政策会把它改成大幅运镜(寒山寺踩过同类坑)。
  • 动效(grid.py anim):逐格错峰入场(间隔 0.11 s、每格 0.42 s 由 1.18× 缩到 1.0 + 淡入),
    收尾接整块 4.5% 极缓推 —— 九宫格就不呆,且与后一镜无缝衔接。

★ 素材侧的先手:找「天然同类组」

拼版成败一半在选图。开工时先在池清单里按内容分组(玉器/青花/扇面/造像/夜景…),
分组结果直接决定哪些句子能上多图。没有同类组,就别硬拼。

> 🚨 落笔素材编号前必须回查 拍摄时间 列 —— 不许凭缩略图的记忆写。
> 实测踩过:文档里写「金鸡湖夜景 6 张(c0769 c0788 c0834 c0835 c0838 c0840)」,
> 六个里错了四个 —— c0769/c0788 是平江路(16:57/17:08),
> c0834–c0840 是次日 07-10 上午 09:43,根本不在本片这一天。
> 正确是 c0801–c0831(19:36–20:53,共 31 张)。

4.6 ★★ 跨批素材的「拍摄日期」怎么处理(2026-09-27 系列定案)

问题:一集里混了不同批次拍的素材(EP1 苏州博物馆:主体 2011-07-09 + 拙政园 2011-12-03)。

定案:画面内一律不标日期;日期全部收到片尾「拍摄」行。

理由(第一条是实测数字,不是审美):

  1. 「标了=没标;想真标就得毁画面」—— 中间没有可用档位。
    双联每格 940 px,视频号手机满宽 390 pt ⇒ 每格 195 pt,系数 0.2074。
    30 px 角标落到手机上只剩 6.2 pt(可读下限 11–12 pt);
    要手机可读需 ≥ 55 px —— 那是主标题的字号,日期反成画面里最响的东西。
    ⇒ 复算方法:scripts/grid.py demo4(含全幅与手机两版对比图)。
  2. 放角标是主动指出接缝,不是缝上接缝。
    旁白里没说"同一天",观众本来不问;两个日期一并排,观众第一反应是
    「为什么是两天的?」——注意力被引到一个我们不打算回答的问题上。
  3. 合规落点本来就在片尾。 尾板「拍摄」行是合规三件套之一。

⇒ 片尾写法:逐日列出,不写区间。

情形 片尾「拍摄」行
同一天 2011 年 7 月 9 日
两次出行(同年) 2011 年 7 月 9 日 · 12 月 3 日
跨年 2011 年 7 月 9 日 · 2012 年 12 月 1 日

🚨 不许写「7 月 9 日 – 12 月 3 日」:破折号读起来是连续区间,
而实际是两次出行 —— 区间号是更强的声明,且是错的。

通用件:scripts/shoot_dates.py
(--selftest 自测 6 条格式规则;--edit/--photo 读镜表 EXIF 出文案)。
它对寒山寺回归验证通过(读到 2012:12:01 → 「2012 年 12 月 1 日」)。
★ 两个老坑它都堵了:只认 36867/36868,绝不回退 306(导出时间);
读不到显式报「EXIF 缺日期」,不静默用错的日期冒充对的。

4.6.1 ★★★ 手写 EXIF 读法的三条铁律(2026-10-05 认证材料案,又自己重踩了一遍)

同一晚的新脚本里,我绕开 shoot_dates.py 手写了一段 EXIF 读取,把 §4.6 早就堵过的坑
原封不动踩了回去。
⇒ 只要要读 EXIF,先看 scripts/shoot_dates.py 能不能用;确实不能用
(比如要展示原始字段值)再手写,且下面三条一条都不能省:

铁律 错法 后果 正解
① 必须读 Exif 子 IFD dict(img.getexif()) 拿不到 DateTimeOriginal,静默回退到主 IFD 的 DateTime(=改图/导出时间) ex.get_ifd(0x8769) 再合进来
② 子 IFD 优先、主 IFD 兜底 直接 d.get(&#x27;DateTime&#x27;) IMG_9807 实测:DateTime=2008-09-01 17:17:31(Photoshop 改图)vs DateTimeOriginal=2008-08-17 11:32:38(真拍摄)—— 差 15 天 DateTimeOriginal → DateTimeDigitized → 报错
③ 展示用数值必须 float() 转 isinstance(v,(int,float)) 判 ExposureTime PIL 给的是 IFDRational(既非 float 也非 tuple)⇒ 两个分支全落空 ⇒ 屏上印出 0.004 float(v) 后再决定 1/250 s
def exif_all(path):                       # ★ 主 IFD + Exif 子 IFD 合并
    from PIL import Image, ExifTags
    out = {}
    ex = Image.open(path).getexif()
    for k, v in ex.items():
        out[ExifTags.TAGS.get(k, k)] = v
    for k, v in ex.get_ifd(0x8769).items():     # ← 少了这 2 行就是错的
        out.setdefault(ExifTags.TAGS.get(k, k), v)
    return out

连带发现(对外证明材料必读):老照片文件里常有
Software: Adobe Photoshop CS Windows / Artist: Picasa 2.7 这类字段。
⇒ 凡是拿原片对外举证(认证材料 / 版权说明),措辞不能说「相机原始文件 / RAW 直出」
—— 应写成「本人相机拍摄的文件(未使用网络素材、无 AI 生成画面)」,这是可核对的说法。
字段本身不删不改(改 EXIF = 毁证据),只是选择要展示哪几项(只列与创作相关的:
型号 / 拍摄时间 / 尺寸 / 镜头参数)。

另一个静默杀手:_plate/_ledger.csv 表头带 BOM(&#x27;\ufeff镜号&#x27;)。
csv.DictReader 下 r[&#x27;镜号&#x27;] 恒为空,于是「逐版生成」变成静默产出 0 帧、脚本还打印成功。
⇒ 一律 open(..., encoding=&#x27;utf-8-sig&#x27;),并在读完后 assert 行数。

4.7 ★ 照片墙/素材池必须过滤到「旁白声称的那一天」

旁白的硬数字(「我在苏州待了九个半小时」)是判据:
07-09 首末两张 EXIF 实测 11:23:56 → 20:53:13 = 9 小时 29 分 —— 正好对上。
池里混进的 07-08 晚 7 张、07-10 早 9 张打进墙里,这句话就变假。
⇒ 先按日期过滤(824 张),再打墙。

4.8 ★★★ 钩子框架在镜表上的五条判据(2026-10-01 新增)

> 骨架与红线见 §3.0.3。这里只管落到 EDL 上怎么判。

① 一镜必有一「缝」 —— EDL 每一行都要能回答「这一镜让观众多了什么?」
回答不出来的镜,要么并进邻镜,要么删。风景好看 ≠ 有缝。

② 开场镜不许是「最美的那张」 —— 必须立缺口:不完整的、有疑问的、只有局部的。
第一镜是全片唯一的"第一次印象",用它换一张漂亮的空镜是亏的。

③ 每 4–6 镜一个回收点 —— 镜数 ≈ 时长 ÷ 2.75 s。
90 s ≈ 33 镜 ⇒ 全片至少要 5 个转折点。 少于 5 个,中段必塌。

④ 细节镜配额 ≥ 3 —— 全片至少 3 个只拍局部的镜(手 / 物件 / 一角)。
这是「具体」的落地处,也是 §3.0.3「墓」的入口(照片边框外的现场只能从细节镜切进去)。

⑤ ★★★ 主结构必须不是时间线 ——
如果整个 EDL 只能按拍摄日期排,说明还没找到结构。
时间线可以做底层,但必须叠一层认知顺序:

可叠的层 例
远 → 近 全景 → 门 → 门槛 → 脚
物 → 人 缝纫机 → 她的手 → 她
外 → 内 街 → 院 → 屋 → 照片墙
已知 → 未知 大家都知道的 → 只有家里人知道的

⇒ 这一条是「工整」和「好看」的分水岭(§4 标题已定:镜表决定工整)。

★ 与既有铁律不冲突:组边界仍卡句子开口时刻(§4.1)、逐句绑定语义照片(§4.2)、
红线体检照跑(§4.3)—— 本节加的是「选哪些、按什么顺序」的上层判据,不替代下层规则。

★ 与 §9.2 衔接(别搞反):钩子元素在素材里不存在时不许硬凑 ——
让文字承担钩子、画面承担主题(寒山寺 v1「两只橘猫」案例)。
本节判据只在素材里真有钩子素材时才适用;没有就退回 §9.2 的处置。

5. Ken Burns 引擎

5.1 ★ 用 cv2 亚像素采样,不要用 ffmpeg zoompan

zoompan 的坐标是整数像素量化 —— 慢速推移时每几帧跳 1 像素,
单镜看不出来,长片里累积成明显的顿挫。

M = np.float32([[1, 0, -x0], [0, 1, -y0]])          # 亚像素平移
fr = cv2.warpAffine(src, M, (W, H), flags=cv2.INTER_LINEAR)

提速技巧:先对采样窗做整数 floor/ceil 裁剪,再对这个小图做亚像素 warpAffine ——
输入面积减半,速度翻倍,画质不变。

5.1b ★ 例外:「图卡说明视频」这种短卡直接上 zoompan 就行(2026-10-05 立)

§5.1 的前提是「一镜几秒 × 几百镜的长片」——量化误差能累积。
但另一类产出完全不适用这个前提:静止图卡拼出来的说明视频
(认证材料 / 项目说明 / 流程演示:每张卡 9–14 s、缩放只有 1.00→1.07、
总长 2 分钟上下)。这种场景用 zoompan 是最优解:

# ★ 四个必须显式给的东西,少一个时长就会漂
ffmpeg -loop 1 -framerate 30 -t &lt;D&gt; -i card.png \
  -vf &quot;scale=3840:2160,zoompan=z=&#x27;min(zoom+&lt;Z&gt;,&lt;ZEND&gt;)&#x27;:d=1:\
x=&#x27;iw/2-(iw/zoom/2)&#x27;:y=&#x27;ih/2-(ih/zoom/2)&#x27;:s=1920x1080:fps=30,setsar=1,format=yuv420p&quot; \
  -c:v libx264 -preset medium -crf 19 -an seg.mp4
#  &lt;Z&gt; = (ZEND-1)/(D*30)
要点 为什么
-framerate 30 -t D 必须显式给 不给就按 25 fps 生成,d=1 下一帧只得一帧 ⇒ 时长漂掉
d=1 + zoom 自累加 每输入帧出 1 帧;zoom 保存上一帧值,zoom+Z 才能逐帧推近
先 scale 到 2× 画布再 zoompan 量化步长落在 3840 上,缩回 1920 后降到亚像素级,顿挫看不出来
setsar=1,format=yuv420p 与后续 xfade / concat 段参数一致才能拼

★ 烟测法:拿 2 s 输入跑一遍,ffprobe 必须正好 60 帧 / 2.000 s,
数字对上才批量跑(2026-10-05 实测通过)。

与 scripts/xfade_concat.py 的关系:那个脚本吃 work/edit.json + 03_render/,
offset 用「转场骑在切点上」模型(offset_k = Σ_{i≤k} d_i)。
如果各段已经渲染成 mp4、时长已知(图卡视频就是这种),直接从时长算更省事,
两者数学等价:

offset_k = Σ_{i≤k} D_i − k·T        # D 是各段成品时长,末段不含 T
总时长   = Σ D_i − (n−1)·T

校验手段:拼完 ffprobe 的 duration 必须等于这个公式值(2026-10-05 实测 126.1 s ✓)。

5.2 速度曲线

  • pan / tilt / diag 用梯形曲线(加速→匀速→减速),更像真实云台:
    python def ease_ramp(u, k=0.32): # k = 加速段占比 if u &lt; k: return (u*u)/(2*k) / (1-k) if u &gt; 1-k: u = 1-u; return 1 - (u*u)/(2*k)/(1-k) return (u - k/2) / (1-k)
  • 推拉用 smoothstep:u*u*(3-2*u)

全程用 smoothstep 会「软塌塌」,全程线性会「生硬」。

5.3 运镜库(至少 14 种,含方向变体)

in / in2 / out / out2 / panL / panR / tiltU / tiltD / diagLR / diagRL / breath / inL / inR / inU / inD / inTL / inBR / outTL / outBR

方向变体的存在意义:让相邻镜次能不同。首版若只用 in/out/panL/panR,
178 镜里会出现 25 处「相邻同运镜」;加方向变体后降到 0。

渲染完跑一次体检:相邻同运镜数 / 过短(<1.8s) / 过长(>4.2s),全部要为 0。

5.4 竖构图照片

竖片(3:4)在 16:9 画布上要用 tilt 类运镜充分发挥竖向空间,
不要用平移(会露出空白)。先扫一遍挑出所有竖片。

5.5 ★★★★ 调色基调(影片风格六组件之一 —— 2026-10-03 建立)

> 方针全文见 锦绣山河_v1/07_strategy/节目方针_照片电影_素材边界与风格_20261003.md
> 工具:scripts/grade.py(--verify 自动验收 / --by-shots 逐镜选档)

★ 先记一句最贵的:风格 = 六个组件,不是六个滤镜。

① 片头  ② 字幕版式  ③ 照片运动语言  ④ 转场语法  ⑤ 调色基调  ⑥ 尾板

调色只占六分之一。 单看调色,主基调 film 相对原片只深了一点黑,谁都认不出来;
观众记住的是六件一起、每集同样地出现。⇒ 别把预算全押在调色上。

5.5.1 ★★★ 做在「光」上,不做在「色」上

v1 曾把"电影感"做成染旧色(sepia 化) ⇒ 目视推翻:历史感上来了,但风景被拍丑了
(树的绿、天的蓝被压成米黄),而节目同时要「历史深度」和「让人想去」。

v2 正解:不选一个风格,选「一个主基调 + 两个叙事层变奏」—— 用切换代替折中。

层 风格 用在哪
主基调 film 全片默认。真实色 + 电影光比(深黑位/高光滚降/局部对比)
历史层变奏 archive 讲义/族谱/老照片/明代/民国那几拍。靠掉饱和显旧,不靠提灰
风景层变奏 vivid 大全景/水乡,为"想去"服务。不染旧

观众最后记住的是「这个节目会按时间层换色调」——比"整片一个滤镜"识别度高得多。
实现:镜表加一列 调色(留空=film)。

5.5.2 管线顺序(固定,别乱调)

局部对比 → 去饱和 → split toning → lift/gamma/gain → S 曲线 → 高光滚降 →〔颗粒 → 暗角〕

> 「先定层次,再定色相,最后加质感」。反过来做,颜色会被对比度吃掉。

5.5.3 ★★★ 两段式(工程上的关键决定)

段 算子 何时做 为什么
照片段 局部对比/去饱和/split/lgg/S曲线/高光滚降 入版式之前,逐张 每张内容不同,局部对比要相对自身算
plate 段 颗粒/暗角 版式拼完之后,整幅一次 颗粒与暗角是镜头整体属性。三联版式下若按照片各来一次,一帧里会出现三个暗角,一眼假

5.5.4 ★★★ 铁律:不做真 LUT

EP02 反面教材:导出稿「灰蓝低对比」的根因是
把 DJI D-Log M to Rec.709 LUT 套在 2011–2013 年佳能 5D Mark II 的 JPEG 上
(本来就是成品色,不是 log),再叠剪映滤镜 + 调整层 ⇒ 提灰 + 褪色 + 压平对比。
⇒ grade.py 全是参数化数学运算,没有 .cube —— 结构性防呆,不靠人记住。

5.5.5 ★★★ 验收:看四个数,不看"感觉高级了"

grade.py --verify 自动打印并判定:

风格 对比(std) 饱和 黑场 P1
film 必须 ↑ 略 ↓ ↓(更深)
archive 略 ↓ ↓↓ ↑(提灰,故意)
vivid ↑↑ 基本不变 ↓

★ 最贵的一条:film 让黑场 P1 19 → 4。原片 P1=19 是发灰的暗部(数码照片通病),
压到 4 才是"有黑"。凡让对比下降、却自称"电影感"的 —— 那就是 EP02 的病,退回。

5.5.6 ★★ 分界:滤镜能修什么、修不了什么

问题 在哪一阶段解决 靠调色能救吗
清晰度(拍虚/糊/噪点/构图废) 选片阶段淘汰 ❌ 救不了。糊就是糊,加对比只会让糊更刺眼
影调(发灰/高光顶死/暗部没黑/平) 调色阶段 ✅ 这正是调色要干的

> ⇒ 铁律:清晰度 → 淘汰;影调 → 调色。
> 不许因为"反正能调色"而放宽选片红线(detail &lt; 0.10 不进片)。

5.5.7 片头(六组件①,2026-10-03 建立|同日定案)

工具 scripts/title_card.py。

> ### ★★★ 定案:--variant AS = A 的版式 + 一枚朱印(已是 CLI 默认)
> 三版候选:A 题签式·居中(与尾板最呼应)/B 竖排·右侧(辨识度最高但易落"国学号"套路)/
> C 左下角·左对齐(最克制、也最易撞)。
>
> | 件 | 定值 | 为什么 |
> |---|---|---|
> | 版式 | A(居中宋体大字+细金横线+小字副题) | 与尾板同一套语言 ⇒ 第 0 秒与第 7 分钟看到同一种东西,三集就记死 |
> | 固定符号 | B 的那一枚朱印(「卷 N」方章) | 版式可以模仿,一枚形制固定+位置固定+带我们集号的印,抄不走 |
> | 印的位置 | 落款(日期行)右侧,与落款合成一组后整体居中 | 中式"印随款"。★ 别放"主标右下方" —— 主标 118px 居中、金线紧贴其下,印插进去会切断「大字→金线→副题」的节奏 |
> | 印边长 | 58 px(DEFAULT_SEAL_SIZE) | 72 ⇒ 全帧唯一饱和色块、抢主标;48 ⇒ 缩到 720p 糊成红点 |
> | 印内字号/demo | 0.32 比例、宋体 Bold 白字、非篆书 | 0.39 ⇒「卷四」撑满方框像"色块里塞了字";印章的识别特征是朱多白少 |
>
> ★ 明确不取:❌ 竖排整版 ❌ 假篆书 ❌ 仿宣纸纹理 ❌ 满屏古风边饰(只取一枚印)。
> ★ 一句话:A 是"我们和尾板是一家的";朱印是"这一家是我们"。
>
> ★★★★ 比参数更重要的一条方法论(本轮栽过):
> 第一版 72px 在 1080 母版上看"没问题",缩到观众实际看的 1280×720 才暴露"红块抢主标"。
> ⇒ 片头这类"一眼定生死"的组件,必须在交付尺寸下看,不能在母版尺寸下看。
> ⇒ 定案留档:锦绣山河_v1/07_strategy/_style/_tmp/定案_印尺寸对比.jpg(48/58/72+无印基准)、
> 定案_终检_全幅与720p.jpg(全幅缩略 + 720p 实尺寸 1:1 双判据)。

设计原则:片头 = 尾板的镜像,首尾呼应。 沿用尾板同一套语言(宋体大字+细金线+实拍照片压暗做底)。

★ 两条质检判据,量在两个不同地方(第一版搞混过):

判据 量在哪 阈值
白字可读 字区 255 − P90 ≥ 75
照片还在 全幅 中位 ∈ [24, 44](标定自尾板整幅实测 P10 21 / 中位 35 / P90 76)

> 踩坑记录:第一版 k=0.30 + 字区压暗 95/255 ⇒ 主标区中位只有 12,
> 照片基本看不见了,片头退化成一张字卡,白瞎了实拍素材。
> ⇒ 「首尾呼应」必须可测,不能靠感觉像;k=0.36 ⇒ 全幅中位 31 ✓

渲染规格:底图 3.5 s 缓推 z 1.00→1.04;字 0.4–1.0 s 淡入、2.9–3.5 s 淡出后硬切入第一镜。

5.5.8 转场语法(六组件④)+ 边界跳变闸门

语法:全片硬切(零叠化/零白场/零推拉),靠运镜方向接力保持连贯。

★ 隐含前提(以前没人量过):相邻两镜的亮度与色温不能跳 —— 一跳,硬切就"闪"一下。

工具 scripts/gate_cuts.py。红线来自 EP01(已出片)实测 127 处边界:

中位 P90(=红线) 最大
亮度 ΔL 17.4 40.2 → 红线 40 81.1
色温 Δ(R−B) 6.7 19.8 → 红线 20 64.1

EP01 回溯:超线 24/127(19%)。 ⚠️ 不是"EP01 做错了",是"从今天起才有这道闸"。

超线处置(★ 不加转场):加叠化会破坏"全片硬切"的风格 —— 那是用工具掩盖判断错误。
正确处置在选片/排序阶段:(a) 换掉其中一镜 (b) 调换两镜顺序 (c) 把其中一镜的调色档切到 archive/vivid 拉近色温。

5.5.9 ★ 反向判据(一眼"不像我们"—— 出现任一条即破)

  • 出现第三个字体 / 第三种金色
  • 出现叠化转场
  • 出现程序纹理底(仿宣纸、噪点、粒子)—— 有被判 AI 的实测先例
  • 某集改了调色基调却没改片头/尾板 → 六件必须同批更新

5.5.10 ★★ 素材边界(写稿/选片前先知道手上有什么)

级 来源 地位
P0 本人实拍照片(含自拍视频) 基本素材,必须占主体
P1 本人其它时期的旧素材 需核 EXIF
P2 公版图片(PD / CC0 / CC BY) 历史场景首选
P3 AI 生成图 仅补视觉空隙,有上限(≤10% 且 ≤8 张/集;不得进片头/封面/尾板)

> ★★★ 最容易被搞错的一条优先级:
> 历史场景(放足会、民国旧影)拍不到 ⇒ 优先用「公版老照片」,不是 AI 图。
> AI 图只能填「视觉空隙」,不能填「证据空缺」。
> ★ 附带一条硬约束:AI 图不许有可见 AI 标记,且必须过同一套调色 + 同尺寸 + 同编码,
> 不能一眼看出是另一套来源。
>
> ★★★ AI 图上限纪律(6 条,2026-10-03 定案)
> ① 单集 ≤10% 且 ≤8 张 ② 单镜画面里 AI 图 ≤1/3 面积(三联版式 ≤1 个竖条)
> ③ 每用一张记 07_docs/素材缺口台账_&lt;地点&gt;.md(镜号/缺什么/为什么缺/替代方案/试过什么)
> ④ 必答:删掉这张 AI 图,这一镜的论证还成立吗? 成立=可有可无 ⇒ 不用
> ⑤ ★★★「8 张是天花板,不是预算」 —— 默认值 0 张;不许"额度还有,不用白不用"。
> 上限类纪律的失效从不是"超标",而是把上限当配额
> ⑥ 声明层:尾板不主动写"含 AI 生成图 N 张"(主体是实拍,主动加=把整片打成 AI 片,
> 且破坏尾板的档案署名功能);但发布时各平台的「AI 生成内容」开关按当次规则如实填;
> 台账必须能逐张说清来路 —— 这才是"如实"的真正兑现方式
>
> ★ 用户立场(2026-10-03 原话):「不能被所谓的 AI 审核束缚住手脚」—— 创作的胆子不自己先收。
> 但与"胆子"无关的三件已知事实要摆着:视频号曾把程序纹理判为「可能含 AI 水印」;
> 百家号「即使声明 AI 生成仍从严打击」(本系列唯一不可逆的雷);平台判的是视觉特征,不是文件名。
> ⇒ 不要以"让平台识别不出"为目标,而以"AI 图占比低到不影响声明"为目标 ——
> 前者一旦被判规避识别,损失的是「本人实地拍摄」这个唯一不可替代的资产。

6. 文字层:不依赖 ffmpeg 的 libass/drawtext

本技能的字幕与所有画面文字,一律走 PIL 逐帧 alpha 合成,不依赖 ffmpeg 的
libass / drawtext / ass 三种文字方案。

⇒ 你的 ffmpeg 有没有这三种滤镜,都不影响出片,也不需要为此重装或重新编译 ffmpeg。

> ★ 为什么特意这么设计:文字方案是 ffmpeg 各构建之间差异最大的地方 ——
> 部分构建(某些精简版 / 过期版本 / 某些包管理器装出来的)会同时缺这三种,
> 于是使用者在「字幕怎么都烧不上去」这一步白耗半天。
> 把文字层挪到 PIL 之后,这一整类环境差异被挡在门外,与 ffmpeg 装成什么样无关。

# 合成只在文字非零 alpha 的 bbox 内做,全片 14107 帧约 3 分钟
al = im[:, :, 3]; ys, xs = np.nonzero(al)
bb = (xs.min(), ys.min(), xs.max()+1, ys.max()+1)
a = (png[y0:y1, x0:x1, 3:4] / 255.0) * fade     # 局部 alpha
frame[y0:y1, x0:x1] = frame[y0:y1, x0:x1] * (1-a) + png[y0:y1, x0:x1, :3] * a

要点:
- 点题卡遮幕不要太重:实测 120 + 46*u 会把照片压死,92 + 34*u**1.4 才合适
- 字幕(黑体 48~52px + 4px 描边 + 阴影)配底部柔化暗带(中心 alpha ~132,边缘羽化)
- 片尾与末条字幕不得重叠 —— 定完时间轴要交叉检查一遍
- 输出 -fps_mode cfr,避免 VFR 造成音画漂移

片尾板内容按发布方要求定,不要自行堆条款。
曾有发布方明确要求只留「素材说明 + 账号引导」两行,删掉「未用合成声音替换」和曲目署名——
此时照办(★ 合规强标不在此列 —— 涉 AI 的显式标识与 AI 署名见 §3.0.7,那条不能以「精简」为由删掉),
但要把作者声明与 CC BY 署名移到 作品说明与发布文案.md,
让发布文案里带上(署名在适当媒介给出仍满足 CC BY 4.0)。

> ★ 署名必须核对实际曲目文件,别凭记忆写。用
> ffprobe -show_entries format_tags=title,artist,album 读 ID3。
> 署名错 = CC BY 4.0 授权违规(该许可唯一强制条件就是署名准确)。

6.1 ★★★ 画面地理标注(箭头 + 地名标签)—— 2026-10-06 立

什么时候要:某一镜是地理实拍(俯瞰/远景/地标),但观众认不出这是哪儿——
典型是「这片园子就是 XX 遗址」。加一支箭头 + 地名标签,观众立刻对上号,
后文再提这个地名时就有了画面锚点。用户原话:「应该在画面上做个地理箭头标注……给观众有期待」。

四条铁律(漏一条就返工)

① 绝不改写正本底图。
_plate/&lt;镜号&gt;.jpg 是台账 plate_sha1 的取证对象,覆盖它 = 素材链断掉、台账作废。
标注版另存 &lt;镜号&gt;_anno.jpg(同尺寸、同 quality),渲染时「标注版优先」,
删掉 _anno 即回正本 ⇒ 可撤销。改完复算正本 sha1 与台账逐位一致才算过。

② ★★★ 先算「全程可见矩形」,再定标注坐标。
运镜的本质是「在放大图上开一个窗口平移」,而这个窗口会被 clip 到图边界 ——
名义振幅 ≠ 实际行程,标在框外的元素会随镜头摇出画面,而且全程不报错。

> 实测(EP08 s0019,底图 2400×1350,zoompan z=1.12 + y 平移 ±110 px):
> 窗口 2142.86×1205.36,居中时 y=72.32,±110 ⇒ 名义 [−37.7, 182.3],
> 但被 clip 到 [0, 144.64] ⇒ 实际行程只有 144.64,不是 220(少了 34%)。
> ⇒ 全程可见 = 各帧窗口的交集 = 图内 x 0.054–0.946 / y 0.107–0.893。

def visible_box(W, H, z, amp):
    &quot;&quot;&quot;运镜「全程可见矩形」(返回图内分数 x0,y0,x1,y1)。
    窗口 = (W/z)×(H/z),y 起点居中后 ±amp,两端都要 clamp 到 [0, H-H/z]。&quot;&quot;&quot;
    cw, ch = W / z, H / z
    x0 = (W - cw) / 2                    # 水平居中:不动
    yc = (H - ch) / 2
    lo = min(max(yc + amp, 0.0), H - ch)     # on=0 的窗口顶(clamp 后)
    hi = min(max(yc - amp, 0.0), H - ch)     # on=N 的窗口顶(clamp 后)
    return (x0 / W, lo / H, (x0 + cw) / W, (hi + ch) / H)

生成前必须断言:把「箭头 bbox」与「文字 bbox」都换算成图内分数,逐个 assert 在
visible_box 里,越界直接退出。别靠眼睛估,也别信"应该差不多"。
(左右摇镜同理,把 x 那一维换成同式即可;斜向/推拉则取全程窗口交集,
推近时 z 会变 ⇒ 逐帧算再取交。)

③ 三层描边:深灰 12 → 白 6 → 主色 0(同一锚点、anchor=&#x27;mm&#x27;,逐层收窄)。
真实航拍画面里白墙/灰屋顶/绿树并存,单层描边必在其中一块背景上糊掉;
三层 = 任何背景上都有一道分离带。箭头同样画三遍(线宽/头宽/头长逐层收)。
主色取朱红(中式地理标注/朱批调性),字号 ≈ 0.040 × W(比字幕大 60% 才够醒目)。

④ 箭头要「钉在底图上」,不是钉在屏幕上。
标注烧进底图 ⇒ 随画面一起移动,观感是「贴在地上」;烧在输出帧上 ⇒ 摇镜时箭头会乱指。
屏幕固定只适合完全静止的镜头。

⑤ 改了画面就必须重跑终混。 合成是 -c:v copy —— 只换片段不重跑,
母版里的画面还是旧的。完整链:
shots --only &lt;镜号&gt; → assemble(核帧数对账)→ 烧字幕 → finalize → 复测三数。
音频链路一字未动 ⇒ 响度三数应与改前逐项相同(这本身就是「标注不影响音频」的凭证)。

视觉验收:首版很容易「指向感弱」(箭头太短、箭尖没压进目标内部)。
必须抽首/中/末帧真打开看,并出前后并排对比图——「数字上在框内」≠「视觉上指得准」。

参考实现:锦绣山河_v1/ep08_江宁织造/07_work/annotate_ep8.py。

7. 混音与响度定标

7.0 ★★ 用「显式增益 + 实测闭环」定标,别指望 loudnorm 的 offset(2026-09-22 凤凰案)

现象:loudnorm=I=-14:TP=-1.5:measured_I=-18.72:...:offset=2.37:linear=true 落 −13.3 LUFS;
只把 offset 改成 1.67 再跑 → −13.4,几乎没动。

根因:前置 acompressor 把语音的 LUFS−TP 从 18.5 dB 压到 9 dB →
「线性提 4.72 dB」必然超 TP → loudnorm 自动降级成 normalization_type=dynamic,
而 dynamic 模式下 offset 不参与计算(它自己追 target,追不到就 overshoot 0.7 dB)。

正确做法(三步,全部可复核):
1. 跑 CHAIN(highpass + compressor + limiter,先不加 loudnorm)→ 实测得基线 X LUFS
2. 目标 T(−14.0)→ 增益 = T − X,写成链尾 volume=&lt;增益&gt;dB(线性,不动动态)
3. 重跑 → 再实测一次闭环
凤凰实测:基线 −12.0 / TP −1.4 → volume=-2.0dB → −14.0 / TP −3.4 ✅

顺带:highpass 会刷 Channel 0 clipping N times —— 那是滤波器内部过冲,
下游有 limiter 就无害,不要因此去改 highpass 增益。

7.0.0 ★★★ 先定义「有几轨」:影视五轨 ↔ 本系列映射(2026-10-02 EP03 定案)

别再每次重查。 影视工业的标准分层是 5 条音轨(好莱坞/《八佰》声音指导富康的公开分类一致):

# 英文 是什么 本系列对应 EP03 实况
1 BACKGROUND 背景音 / 环境音(Atmos 床) 环境声轨 ✅ v3
2 VOICE OVER 旁白 旁白_定版 ✅ 真人原声
3 FOLEY 与动作匹配的拟音 点音效轨 ⚠️ 只有「笔/纸/翻页」,不是与画面动作匹配
4 SOUND EFFECT 特殊 / 合成音效 点音效轨 + 混响 IR ✅
5 WALLA 人群声 / 群杂(街市、茶馆、集市的人声底噪) —— ❌ 全系列零

音乐在他(小鹿Lawrence)的分组里单独一类,与我们的 配乐轨 一致。

★ 铁律(他视频里 410 赞评论的题眼,也是全片含金量最高的一句):
> 好音效的标准是「观众说不出哪里好」。如果观众的注意力被音效吸引了,这个音效就做突兀了。

⇒ 落点选「句间空档」、电平压在 −30 ~ −34 dBFS(听得见但不抢),不要为了炫技抬高。


🚨 WALLA 是本系列的结构性缺口(EP03 实测)

EP03 是古镇市集题材,而且旁白自己就写了人群:

> s0011「第二天早上七点半进的街,街上还没有人。」→ s0015「到八点多,人就上来了。」

旁白把「人来了」当时间推移的标志,音频里却一个人声都没有 —— 一个「有人但没声音」的市集。
这不是清单打勾问题,是声画不匹配。


🚨🚨 剪映音效库不能用(2026-10-02 逐条查协议原文后的硬结论)

剪映确实有人群声素材,但它的《素材使用许可协议》把路堵死了:

条款 原文要点 对我们的后果
5.2(3) 商用声音类素材(含【音效】模块下的声音)——包含它的视频只能发到剪映、抖音("许可平台") 我们发百家号/视频号/头条 ⇒ 越界
5.3 任何情况下不得对单独的剪映素材下载、分发;在剪映以外的平台编辑/使用,剪映不为其出具任何著作权证明 导出 wav 进 FFmpeg 链路正是这条
5.1(1) 非商用素材限「个人分享」,不含企业/商事主体注册认证的账号 我们的号有分成=营利 ⇒ 越界

> ★ 网上大量测评说「剪映素材已获商用授权、随便用」——是软文,与官方协议原文相反。
> 判据照旧:看协议原文,不看二手转述。

✅ 正解:拍摄期自录环境声(EP04 起列为必做工序)

每集都是用户实地拍的。拍完照片,原地用手机录 30–60 秒环境声(含现场人声、市集嘈杂、水声):

  • 自有版权,零授权风险,不碰任何平台协议
  • 与画面同源同地,契合度高于任何素材库
  • 成本≈0(顺手录),在影视工业里这叫 room tone / ambience 采集,是标准工序

拍摄清单新增一项:

□ 环境声:原地录 30–60 s(不要说话、不要走动、手机别贴嘴)
          → 存 01_source/ambience/ ,与照片同一天

混入方式:接在 环境声轨 里作叠加层,只在句间空档抬起来(复用 make_ambience_ep3.py 的 SILENT_LIFT_DB 机制),
电平压在旁白下方 −20 dB 以上,别撞坏「三条线」。

若某集没录到(补救顺序):① 已核验 CC0 库里的 crowd/market ambiance(逐条核授权)
② 用器物声暗示人群(稀疏的碗碟、门轴、脚步点音效),让观众脑补 —— 省略法,不引入人声。

7.0.1 ★ 三轨电平先算再混(不要边听边调)

轨 电平 依据
旁白 −14.0 LUFS 主指标;平台只往下压不往上抬
配乐(基准) −26 LUFS 句间停顿时应为「旁白下方 10–12 LU」→ −14−12 = −26
配乐(说话时) 侧链自动降 ≈ −34 sidechaincompress=threshold=0.05:ratio=6:attack=20:release=800 → 压约 10 dB = 下方 20 LU
环境音 −46 LUFS 音乐在场时的设计值(−45~−49);音乐缺席才提到 −33~−38

验收判据:三轨叠加后总响度应几乎不变(−14.0 → −14.2 以内)。
功率验算:旁白 −14 → 0.0398,配乐 −26 → 0.0025(合 +0.06 dB),环境音 −46 可忽略 → 实测 −14.2,与算得吻合 ✅
若叠加后偏了 1 dB 以上 → 某一轨电平定错了,回去查,不要靠总响度硬拉。

7.1 ★ ffmpeg 三个默认值是陷阱

滤镜 默认值 后果 正确写法
acompressor detection=rms 对瞬态峰值迟钝,压缩器碰不到峰。实测 makeup 的 +7.96 dB 被整段拾走,峰值 −0.96 → +7.00 detection=peak
acompressor makeup=1(零补偿) 压缩只降不升 → 响度掉 5~8 dB,还得再补增益,峰值更糟 显式给 makeup(线性倍数)
alimiter attack=5ms 对削顶瞬态来不及,穿透严重 attack=0.1 + level=0

7.2 旁白链定稿(实测 −14.00 LUFS / TP −3.60 落盘)

highpass=f=80,
acompressor=threshold=-16dB:ratio=4:attack=3:release=180:makeup=4:detection=peak,
alimiter=limit=0.995:attack=0.1:release=10:level=0

limit 要在落盘文件上扫出拐点:这里的限幅呈「全有或全无」——
0.995 削 4.1 dB、0.94 削 4.6 dB(仅差 0.5 dB),而不限幅则峰值超线 1.4 dB。
→ 取最松的可用值(削得最少 = 最保真)。

增益要两遍算:先渲染整形链量出实际响度,再据此算 volume 定标。
写死增益会差几个 dB(limiter 会改变响度)。

7.2.1 ★★ 手机录音:先治病,再走定稿链(2026-09-26 寒山寺案)

§7.2 那条链是给正常录音的。手机录的必须先做频谱修复,否则修完还是闷的。

第一步永远是「逐频段量一遍」,拿这张表对:

# 逐频段 mean 电平(★ 上限绝不用 22050 —— 见下方坑 1)
for r in 20:60 60:120 120:250 250:1000 1000:3000 3000:6000 6000:10000; do
  ffmpeg -i 旁白.mp3 -af &quot;highpass=f=${r%%:*},lowpass=f=${r##*:},volumedetect&quot; -f null -
done
频段 正常语音(相对 250–1000Hz) 手机录音实测 说明
20–60 −15 以下 −9.9 手持/桌面传导的隆隆 → 高通 80–90
120–250 0 ~ −3 +1.0 ★ 比主体还响 = 闷、浑浊 → EQ 挖
250–1000 基准 基准 语音主体
1000–3000 −2 ~ −4 −8.9 ★ 可懂度塌陷(「含在嘴里」) → EQ 提
3000–6000 −8 ~ −12 −21.0 ★ 辅音/齿音几乎没有 → EQ 提
10000+ — 空(−70 以下) 不硬提,提了只放大 MP3 编码噪声

修复链(在 §7.2 链的 highpass 与 acompressor 之间插三段 EQ):

pan=mono|c0=0.5*c0+0.5*c1,                    # 手机多是伪立体声,先单声道化
highpass=f=80,
equalizer=f=180:width_type=q:width=1.0:g=-4.5,     # ★ 挖浊(量见下)
equalizer=f=1600:width_type=q:width=1.2:g=+3.5,    # 可懂度
equalizer=f=3400:width_type=q:width=1.4:g=+3.0,    # 辅音
acompressor=threshold=-16dB:ratio=4:attack=3:release=180:makeup=4:detection=peak,
alimiter=limit=&lt;扫&gt;:attack=0.1:release=10:level=0

🚨 这里有一个必须知道的反直觉现象:压缩器会把 EQ 修好的低频抬回来

受控实验(逐步加器件,测 120–250Hz 相对 250–1000Hz 的变化):

加了什么 变化
+EQ 180Hz −2.5dB −1.10 dB ← EQ 确实生效
+EQ 1600Hz +3dB −0.00 dB ← 选择性好,不伤低频
+acompressor +0.90 dB ← ★ 又抬回去了

净效果只剩 −0.2 dB。 第一版照直觉给 −2.5dB,实测只降 0.3dB,卡在这里查了两轮。

为什么:压缩是宽带的。语音主体(250–1000Hz)最常超过 −16dB 阈值 → 被压得最多 →
低频和高频相对回升。

> ★ 判据:EQ 的衰减量必须把压缩的这一份补偿进去。
> 所以上面写的是 −4.5dB 而不是 −2.5dB。别照直觉填,要闭环实测。

闭环标定(照 §7.0「显式增益+实测闭环」做,别猜):
调完 EQ 后再量一遍频段,看 120–250 是否落到 −1 ~ −1.5、1–3k 是否落到 −4 ~ −5。
不达就改 EQ 参数重测,2–3 轮收敛。

> ★ 脚本:scripts/voice_fix.py —— 上面这一整套(体检 → 三档 → limiter 扫描 → 响度定标 → 试听样本)
> 已经封好了,别手搓:
> bash &gt; # 只体检(不动文件)——先看有没有病 &gt; python voice_fix.py 旁白.mp3 --probe --tail 554.5 &gt; # 出三档 + 试听对比样本 &gt; python voice_fix.py 旁白.mp3 --outdir ./处理后 --tail 554.5 --demo 413.32:16.5 &gt;
> --probe 会直接把三个关键频段判成 ✅/⚠️,不用自己对表。
> 尾巴 --tail 用 voice_check.py check 报的语音结束时刻(切掉忘按停的空白)。

两个工具坑(都实测踩过)

  1. lowpass 的截止频率永远不要设在 Nyquist。
    44.1kHz 素材上写 lowpass=f=22050,滤波器数值退化自激,
    测出「16–22kHz = −0.4 dB」(比全文件峰值还高)这种不可能的数据。
    → 测频段时上限最高写到 20000;真实值(重测)是 mean −80.3 / max −39.2 dB。
    ★ 凡是量出「比峰值还高的分频能量」,先怀疑滤波器,别怀疑素材。
  2. volumedetect 把 max_volume 放在 histogram 块的「开头」,不是末尾。
    用 tail -6 去取,拿到的是 histogram 尾行 —— 会连续几次读错还以为读到了。
    → 读它一律用文本检索,不用 tail。

顺带两条体检项(voice_check.py check 已含)

  • 「真削顶」看 astats 的 Flat factor / Peak count,不看 max_volume。
    峰值 −0.24dBFS 但 Flat factor = 0 = 尖峰不是平顶,没削顶。
  • 忘按停会留下超长尾部空白,而 whisper 会在长静音上产生幻觉
    (寒山寺案:尾部 97.57s 空白 → whisper 吐了 80 个幻觉片段反复重复同一句)。
    → 先把转写按 silencedetect 的语音结束时刻裁掉,再做对齐,否则幻觉会污染对齐。

7.3 ★ 测量口径(本技能最容易翻车的一条)

TP 判定一律以「落盘文件」为准,不能用 filter 链上 -f null 的读数。

同一条链:单声道域测 TP +12.30,立体声落盘测 TP +3.20
—— I 完全相同,TP 差 9.1 dB。

原因:单声道源写盘时 -ac 2 转立体声,ffmpeg 用等功率 pan law(每声道 ×0.7071),
LUFS 守恒(能量不变),但每声道峰值降 3.01 dB。

推论:未压缩语音的 LUFS − TP ≈ 18 dB 是固有特性(K 加权积分 vs 瞬态峰值),
不是「孤立尖峰失真」。要落到 −14/−1.5(差 12.5 dB)必须做真实动态压缩;
削峰量尽量交给 limiter(削的是样本级尖峰,LRA 几乎不动),
别用压缩器压平整体动态(会把语音压闷)。

7.4 三轨混音

  • 配乐铺底:比旁白低 10~12 dB(如旁白 −14、配乐 −25)
  • 侧链压缩:旁白一开口,配乐深让
    sidechaincompress=threshold=0.022:ratio=6:attack=12:release=420
  • 配乐不要简单循环 —— 多段不同入点错开接缝,9 s 叠化;末段用原曲自带收尾衰减落在谢幕
  • ffmpeg filter 输出标签只能连一次 → 需要在两处用同一路音频时必须 asplit
  • 末端两遍 loudnorm(先 print_format=json 量,再 linear=true + measured_* 回填)

7.5 ★★ 自造音轨层(音效 / 母题层)的九条判据(2026-09-26 寒山寺 L3 案,含用户听感反馈后的修正)

自己用 numpy 合成一整条音效轨时(不是 ffmpeg 滤镜链),下面九条都会静默出错 ——
不报错,只是听感不对:

① 电平旋钮绝不能放在归一化之前。
先乘旋钮、再把整段 norm(-3 dBFS) → 旋钮被归一化完全抵消
(实测旋钮转 14 dB,输出一动不动,且不报错)。
→ 素材先归一到工作电平,再乘旋钮,之后不再归一化。

② 补「处理造成的电平损失」必须用 LUFS,不能用 RMS。
低通 8k→1.2k:RMS 视角只掉 1.79 dB,LUFS 视角掉 2.70 dB —— 差 1.4 dB。
因为 K-weighting 在 2~4 kHz 是加权重的,而被低通削掉的正是那一段。
→ 用 RMS 补,段间关系会随滤波器漂移,那就不是「你指定的」了。

③「时长 15 s」≠「能响 15 s」。
一条 15.86 s 的钟声素材,实测包络 ▇█▇▅▅▃▃▁▁▁▁ + 11 s 静默(尾部 −65 dBFS)。
按 14 s 间隔铺 → 每 14 秒只有 4.5 秒有声音 → 量出的段电平完全取决于窗口含几记起振
(段间差 3~5 dB,数字根本不可比)。
→ 定参数前先打印素材包络(0.5 s 一格,相对峰值打条形图),别信文件名里的时长。

④ 连续声场的段间电平:直接指定「绝对目标 LUFS」,不要「补偿损失 + 设计值」。
~~gain = 该段LUFS损失 + 设计段间值,段中心点插值~~ ← 这条 2026-09-26 被证伪:
控制点放段中点、再线性插值 → 增益在段界之前就开始爬升,各段测量窗口互相污染。
实测后果:「保持失真段」反而比「在场段」响 1.4 dB,与「被夺走=存在感变弱」方向相反,
且不报错。
→ 改成:TARGETS = [(&quot;在场&quot;, A, T_D0, -33.5), (&quot;保持失真&quot;, T_DIST, T_BACK, -35.0), …],
迭代 2–3 次反算增益(增益→测量→修正),每段收敛到 ±0.3 dB 内。
→ 并保证各段都高于配乐被避让后的电平(−37),否则听不出它是内容轨。

⑤ ★★ 要改音高,只能作用在「音源」上,不能作用在「正在响的信号」上。
对整段变速:一记钟声若横跨变速点,它的余韵会前半降调、后半被拉回原调 ——
一个音自己往上滑 8.6%(实测,用 Hilbert 瞬时频率可测)。
听感就是「变形失真」,而且不随段落消失(只要那一记落在附近就一路带着滑音)。
→ 改成每记独立定调:每记在它开始的那一刻取一个固定音高,记内恒定。
听感从「一个音在滑」变成「钟越来越沉」——那才是对的。
★ 物理上也讲不通:余韵不会因为当下发生的事而改变音高。

⑥ 时变滤波不要用「多个固定截止版本混合」。
两路信号过了不同截止的滤波器之后相位已经不同,在任何混合比下相加都会抵消(梳状)。
过渡那几秒听感就是「发毛 / 变形」,而波形上看不出错。
→ 用 STFT 逐帧改幅度(scipy.signal.stft → 乘频域响应 → istft),
帧间靠窗重叠平滑,不存在两路相加。
→ 时变过渡别做太陡:1.5 s 内跨 2.2 个八度会有「开关感」,4 s 就自然了。

⑦ ★★ 定滤波器参数前,先量素材的有效带宽。
按常识把低通起点设在 8 kHz,而素材实测「4 kHz 以上能量 0.0%」(能量在 200 Hz–2 kHz)→
「8k→2k」这一段完全空转,扫频的大部分时间听不出变化。
→ 「钟声应该到 10 kHz」是想象,不是事实。 先 fft 分频带量占比,再定截止。

⑧ ★★ 击发点必须锚在「设计的关键句」上,不能拿算术级数铺。
arange(P0, P1, 13) 铺出来的击发点,会让「钟正在被夺走」那 7 s
恰好落在两记之间 —— 实测该窗口只有 −48.1 LUFS,没有钟可扫,整段设计空转,
而脚本不报任何错。
→ 击发点写成「关键句时刻的列表」(HITS_ABS = [A, …, T_D0, …, T_BACK, …, T_CUT-2.5]),
时刻全部由变量推出来,不写字面秒数(换一版旁白时才会跟着走)。

⑨ ★★ 标定窗与探针窗必须逐字一致。
音效是稀疏事件,「窗口平均 LUFS」会被起振点主导。
标定用 [98.13,123.94]、探针用 [99.5,123.4] —— 差的 1.4 s 正好是第一记的起振,
读数就差 2.9 dB:同一件事量出两个数。
→ 用窗口均值当判据时,窗必须逐字一致;否则改用「事件峰值」当判据。

★ 判据(贯穿九条):听感判据不是「它多响」,是「同一窗口里它比旁白低多少 LU」。
逐落点量「旁白 / 音效」同窗口 LUFS,直接看差值 —— 绝对值(−20 LUFS 之类)没意义,
因为旁白本身在不同的句子间就有 ±2 LU 起伏。

★ 九条的共同点:都是「看不见的东西」在作怪。
归一化把旋钮吃掉 · K-weighting 把损失吃掉 · 素材尾音把时长吃掉 ·
相位抵消把音色吃掉 · 无效频段把扫频吃掉 · 算术级数把落点吃掉 ·
不一致的窗口把读数吃掉 · 段中点插值把段界吃掉。
数字看起来都对,听感上不对。
→ 凡「参数调了没反应」或「量出来的数字与听感不符」,先怀疑是不是被某处的静默/加权/抵消吃掉了。

★ 修完必须出「修复前 / 修复后」对比(用户硬规矩)。
音频的对比 = ① A/B 连听(先新后旧,避免先入为主)② 找到能钉死机制的
可控实验(本例:220 Hz 纯音 → 量瞬时频率轨迹 → v1 爬 8.6% / v2 恒 0.0%)。
③ 别用谱通量当「变形」的判据 —— 起振本身就是高谱通量,
实测新版反而更高(因为更稀疏、起振更清楚),会得出反向结论。

> 共同点:这四条都是「看不见的静默」造成的 —— 归一化把旋钮吃掉、
> K-weighting 把损失吃掉、素材尾音把时长吃掉。
> 凡是「参数调了没反应」或「量出的数字与听感不符」,先怀疑被某处的静默/加权吃掉了。

7.6 ★★★★ 落盘响度的完整判据(2026-10-01 东湖 EP01 定案 —— 推翻旧注释)

> 旧注释写「本链能走 Linear 两遍法」是错的。 东湖一集落盘一度是 −16.1 LUFS
> (比目标低 2.1,比 EP2 的 −14.3 还闷),追了 6 版才定位。以下四条是代价换来的。

① Linear 模式的两条判据(缺一不可)

|target_i − measured_I| ≤ 1        且        measured_TP + offset ≤ target_tp ≤ 0
  • 本链第一道 alimiter(auto level)把混音峰钉在 0 dBFS;要到 −14 LUFS 需 +3.5 dB
    ⇒ 线性后 TP 恒 +3.5 dBTP > 0 ⇒ 被 ffmpeg 直接拒绝(--target-tp 6 报 Result too large,
    target_tp 的合法上限就是 0)。
  • ⇒ 凡是「混音峰已被 limiter 顶到 0」的链,Dynamic 是唯一可能的模式。 别再试 Linear。

② pass1 自报的 Target Offset 是脏值 —— 不能信

条件 Input Integrated 自报 Target Offset
pre-gain 0 −17.4 +2.5
pre-gain +3.5(已抬到 −13.9 = target) −13.9 +2.5(没变)

与电平无关 ⇒ 它不是 target − measured_I。
→ 按定义现算 off = target_i − measured_I,并在 |off| &gt; 1 时告警「建议 --pre-gain」。

③ Dynamic 在高峰均比素材上低估约 2 LU ⇒ 用静态落盘校正,别反复试 target-i

finalize.py 两个旋钮(target-i 微调对落盘 I 非单调,那条老路不要走):

  • --pre-gain:进 loudnorm 之前的垫脚增益(把 Input Integrated 抬到目标附近)
  • --post-gain:loudnorm 之后、192k 限幅之前的确定性校正增益

控 TP 唯一正解仍是末级双闸:192k → alimiter(0.73) → 48k → alimiter(0.76)
(192k 管真峰,48k 管降采样振铃;alimiter attack=0.1、level=0 必须显式给)。

④ 削顶代价可接受(已量化,别再当缺陷)

post-gain 贴顶(≤0.6 dB)总时长 占 374 s 0.1 s 峰均比
0 186.6 ms 0.050% 10.6 dB
+3.2 229.6 ms 0.061% 10.4 dB
+5.5(定案) 271.7 ms 0.073% 10.2 dB

⑤ 两行日志不许再忽略:pass2 后打印的 Normalization Type: Dynamic|Linear
(这行被忽略 = 「每集都静默走 Dynamic」的根因);ebur128 汇总解析只取 Summary: 之后
(否则会取到中间值,东湖曾读出 I = -70.0)。

⑥ 出片后必须等 +faststart 写完 moov(最长 13 s)再 ffprobe/ebur128,不等则测值全废。

⑦ 环境声(第四路)的「可闻性」判据 = 真静默窗中位 ≥ −30 dBFS。
东湖首版 13/18 个静默窗低于 −30(中位 −31.5)=等于没铺。修复两招:
只抬不压的慢包络(1.5 s 窗、累积和 O(n)、最多 +6 dB)+ 归一按真静默窗中位
(不要用 mask&lt;0.02 的全集 RMS)⇒ 中位 −29.4 ✓。
★ 环境音库要先验完整性:共享 CC0 库 9 条里 6 条有问题(3 截断 + 3 个 153 B 占位),
先用 verify_media.py 实读,别按目录名单就用。

7.7 ★★★★ 交付音轨必须是 stereo(2026-10-01 东湖 EP01 返工定案)

> 触发:用户听完 v1 母版的反馈 ——
> 「背景的水声太响了……配乐也不够流畅,整体融合感差,带人沉浸的感觉没做出来,反而显得杂乱,让人听不下去!」
> 追下去发现:「沉浸感没做出来」不是调参问题,是结构性缺陷 —— 交付母版音轨是 mono。

① 根因:素材源全部是真立体声(MemoriesOfStone.mp3 44.1k/2ch、510829_small river.mp3 48k/2ch),
全链却 -ac 1 把它们下混到正中央一个点 ⇒ 声场塌成一条线。
实测母版:旧版 L/R 相关 1.000 / S/M −240 dB(零侧声道) → 新版 旁白段 0.983 / 静默窗 0.335。

② 这是系列级回归,不是风格选项 —— 博物馆_v1 各集成片(苏州博物馆 EP1、上博 EP4)音轨都是 stereo,
只有《锦绣山河》EP01 是 mono。修复后响度/动态逐位对齐系列基准(苏博 EP1 I −14.3/LRA 5.5/TP −1.7
→ 东湖 v3a I −14.5/LRA 5.4/TP −1.6;LRA 由 2.6 → 5.4,正是「动态被压平」被治好)。

③ 交付前必核:ffprobe -select_streams a:0 -show_entries stream=channels 必须 = 2。
旁白(单声道)在混音时用 aformat=channel_layouts=stereo 居中,背景保留素材声像,不加人工加宽。

④ 立体声下末级限幅要多干 3 dB 的活(同峰值下 stereo 比 mono 响约 3 dB),
⇒ --post-gain 会饱和失效:实测降 3.2 dB post-gain 只换 0.6 dB 响度,真峰反而升高(非单调)。
控响度改走 target-i,post-gain 只当微调。
也不要试图抬末级限幅天花板换响度:0.769/0.800 实测 TP 冲到 −0.8 dBTP(超红线),已回退。

⑤ 🚨 别把次声隆隆当成声场 —— 追查「写盘前后 S/M 从 −0.3 掉到 −7.5」时一度误判写盘有问题,
真因是 highpass=f=60。侧声道能量的频段分布:

频带 占侧声道总能量
0–20 Hz 75.76%
20–40 Hz 13.31%
40–60 Hz 2.10%
60–100 Hz 0.58%

60 Hz 以上侧声道比中置低 4~15 dB。⇒ 高通是净化,不是变窄。

7.8 ★★★ 连续背景床 vs 一次性音效:两套 duck 预设

症状:水声/风声这类连续背景床用 event 预设的深快 duck(threshold 0.10 / ratio 2.5 / atk 120 / rel 2600 的加强版)
⇒ 说话段被压 6.8 dB、句间又弹回,背景在旁白面前反复进出 = 听感上的「融合感差/杂乱」。

finalize.py 已加 --duck-preset:

预设 用途 参数 说话段自差(实测)
event(默认) 一次性音效(钟声、橹声、鸟叫) 深压快落 —
bed 连续背景床(水声、风、街噪) threshold=0.10 : ratio=2.5 : attack=120 : release=2600(浅压慢落) 6.8 → 2.2 dB

配套:--channels {1,2}、--limit-192k(默认 0.73)、--limit-48k(默认 0.76)三个 CLI 参数。
默认行为全部不变,不影响既有各集。

配乐包络也要跟着改:v1 用 41 个手调锚点 ⇒ 正文增益来回摆 −7.0…0.0、短时波动 26.3 dB。
返工:锚点砍到 12 个、正文压到 −2.0…0.4(波动 →19.0 dB)、
换曲由 0.25 s 线性插值(中心功率 −3.01 dB 塌陷)改 4.5 s 等功率 S 曲线(中心 −0.00 dB)。

频谱避让要加宽加深:配乐 1.8k±1200 −6 dB + 700±300 −2.5 dB;环境声 2.6k±1900 −10 dB。
判据用 「清晰带/体腔」= 1800–3500 Hz ÷ 300–1000 Hz(越低=越让出辅音带)⇒ 环境声 −1.1 → −8.3 dB。
别用「谱重心」判亮度 —— 去掉了 60 Hz 以下的隆隆之后分母变小,谱重心反而会升高,是假信号。

7.9 ★★ 背景电平:唯一该看的量是「背景露出」

「背景露出」= 空档段 RMS − 说话段 RMS(同一路 stem,越小=背景在旁白面前越不抢)。
东湖 v1 = +0.5 dB(旁白一停背景反而更响,这正是「喧宾夺主」的量化形态)⇒ 返工后 −10.0 dB。
环境声说话段从 −34.0 → −48.0 dBFS(比旁白低 13.5 dB)。

但别把背景杀干净 —— 「可闻性」判据(真静默窗中位 ≥ −30 dBFS)在交付母版上复核:
v1 −12.3 → v3a −28.4 dBFS ✓(比旧版低 15.8 dB,仍达标)。

7.10 ★★ 五个度量陷阱(全在 audio_diff_ep1.py 里踩过)

陷阱 症状 正解
在「布尔抽取」的样本上做 FFT x[mask] 把全片说话段首尾相接成时间不连续序列,求的是不存在信号的谱 —— 同一路环境声脚本报谱重心 3888 Hz,逐窗实测 856→580 Hz,差 5 倍 改连续窗(46 个 3 s 说话窗)取中位
拿成品轨包络最低点当「换曲塌陷」 得「v3 9.7 dB > v1 8.9 dB」的荒谬结论(两版取到了不同乐句) 比较斜坡函数本身:线性 (1−u)²+u² 中心 −3.01 dB;等功率 cos²+sin² ≡ 1 中心 0 dB
mono/stereo 电平横比 差 3.01 dB 用总功率口径 (a**2).sum(axis=1).mean()(ffmpeg 上混是功率保持的,每声道 ×1/√2)
ebur128 的 Summary 是 info 级 加 -v error 会吞掉整段 ⇒ 表格全 ?;去掉后又不能全文正则(开头逐帧 I −70 / LRA 0 会先命中) out[out.rfind(&#x27;Summary:&#x27;):]
★★★★ 在「母版」上量单路指标 母版级「真静默窗中位」量的是背景合成(音乐占主导)⇒ 环境声让路从 6.9 dB 修到 21.6 dB,母版读数只从 −28.0 动到 −28.9(0.9 dB,方向还相反)。会把「已修好」读成「没修好」,或反向也是 验收判据一律回到 stem 分路(见 §7.11);母版只用于最终响度/真峰复测

★★★ 换母版 = 档案所有读数作废。 交付件一旦重出(哪怕只换一条音轨),
制作档案里响度、真峰、LRA、静默窗电平、立体声相关全部必须在新文件上重跑一遍再落笔 ——
东湖 v1.2 收尾时发现档案三行仍是上一版(v1.1)读数(LRA 5.4/TP −1.6/静默窗 −28.4),
与实交付(LRA 5.5/TP −1.9/静默窗 −28.9)不符。先测、后写,别把旧数抄过去。

★★ 慢包络别在全采样率上卷:np.convolve 卷 9 s Hann 窗 ≈ 43.2 万抽头 × 1790 万样本 = 7.7e12(跑 4 分钟无结果)。
改粗网格再插值:配乐曲线 100 Hz(残差 < 0.01 dB)、duck 包络 500 Hz(递推非线性、时间常数随上下行切换,无法向量化:
全采样率 1790 万次 Python 循环 60–90 s → 粗网格 3.6 万次 < 1 s)。

★ 旁白也可能变:duck_envelope 与可闻性掩码都要按实际解码长度重建
(AAC 解码长度与素材不一致:mono 多 464 帧、stereo 少 560 帧);空档掩码要加 body[:int(BODY*SR)] = True
把尾板排除(尾板是配乐署名段,会把空档电平抬高)。

7.11 ★★★★★ 背景电平的唯一正确判据:分频带 × 相对对白 × 两路先合成(2026-10-01 东湖二次返工定案)

> 触发:同一集连续两次被用户退回 —— 第一次「背景的水声太响了…喧宾夺主…沉浸感没做出来」,
> 第二次「水声太大了呀!搞得背景音都糊了」,并直接批评:
> 「拿作品在试参数,试试就改一下试试就改一下,这样不好吧」。
> 第二次返工追出来的原因不是参数没调够,而是我用错了判据。

① 我错在哪:v1.1 我报「环境声说话段比旁白低 21 dB,已修复」——
那是总 RMS 差,不是听觉相关量。按频带逐带重测:

轨 关键带最低让路 判据 ≥20 dB
环境声 v1 0.1 dB ✗
环境声 v1.1(我交付的) 7.2 dB ✗
环境声 v1.2 23.0 dB ✓

总 RMS 差 21 dB,分带看只有 7.2 dB —— 水声的能量集中在旁白最关键的带上,
而总量被两端的死区(<200 Hz / >6 kHz)拉平了。

② 判据(三个,缺一不可)

① 分频带掩蔽:**两路背景合成**后,逐带「低于旁白 ≥ 20 dB」
② 空档可闻:背景合成在句间空档,相对对白 **−12 … −2 dB**
③ 让路动态:**环境声这一路** 空档 − 说话 ≥ 10 dB
  • 必须「合成」:掩蔽是能量可加的。实测 v1.1 单看环境声 7.2、单看配乐 19.4(都像还行),
    合成后只有 6.9 dB —— 这才是「糊」的客观形态。先做加法,再做判据。
  • ③ 只看环境声这一路:配乐是连续底,动态就该小(大动态 = 「反复进出」);
    让路的责任在环境声。合成动态只有 ~1 dB 是正常且正确的,别去「修」它。
  • ② 用相对量不用绝对 dBFS:行业给的 −30~−24 dBFS 是电影工作站的表头口径,
    与本项目「说话段 RMS(总功率口径、含句内停顿)」不同尺,直接比会误判。
    换算:行业对白均值 ≈ −18 dBFS / ambience −30~−24 ⇒ 比对白低 6~12 dB;
    电影课「低于 −30 dB 就听不见」≈ 比对白低 ≥12 dB 即失效。

③ 工具(已固化,下集直接复用)

$PY 07_work/band_masking_ep1.py \
  --track &quot;环境声 v4:0.0:环境声轨_EP1_v4.wav&quot; --track &quot;配乐 v4:-11.6:配乐轨_EP1_v4.wav&quot;
$PY 07_work/bed_level_ep1.py --bed &quot;配乐:-11.6:配乐轨.wav&quot; --bed &quot;环境声:0:环境声轨.wav&quot;

(增益参数必须写混音链里实际给的那个,否则判据不对。)

④ 定点修什么 —— 每一处都由判据反推,不猜

症状 实测根因 修法
分带让路只有 7.2 dB 避让坑位置错了:放在 2600 Hz,而水声最强带是 800–1500(占全带 −4.3 dB,最高) 主坑移到 1400 ± 1900,−13 dB
让路动态只有 4.5 dB 说话/空档差太小 说话段再降 16 dB,动态做到 15.1 dB
配乐 200–400 只让 19.4 两刀(1800±1200/700±300)中间留了缝,200–400 恰好落在缝里 补第三刀 300±400, −7 dB

> ★★★ 两条通用教训
> 1. 挖频之前必须先量「素材在语音带里到底哪儿最响」 —— 不能按「语音清晰度带」
> 这个通用概念想当然。(水声最强在 800–1500,不是 2600。)
> 2. 避让带之间要有重叠,别留缝;校验必须逐带过,不能只看总电平。

⑤ 找现成工具时的经验:查 SkillHub / Vercel Skills / ClawHub,
唯一对口的 sonilo-ai/skills@audio-ducking 是付费云 API 包装
(360 s 上限、需 API key、不含方法论)⇒ 不适用。
「有没有技能」不等于「有没有方法」——先看技能里有没有判据。

8. 成片合成与体检

# 合成
ffmpeg -i 02_work/picture_text.mp4 -i work/mix_final.wav \
  -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -b:a 192k -ar 48000 -ac 2 \
  -movflags +faststart -shortest out.mp4

# ★ 色标补齐(无需重编码,14107 帧 1.18 秒)
ffmpeg -i out.mp4 -c copy \
  -bsf:v h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1 \
  -movflags +faststart out_final.mp4

出片必检:

项 命令 合格线
响度(成片实测) ffmpeg -i out.mp4 -af ebur128=peak=true -f null - I ≈ −14.0 / Peak ≤ −1.5
声道数 ffprobe -select_streams a:0 -show_entries stream=channels -of csv=p=0 out.mp4 必须 = 2(见 §7.7;-ac 1 会静默拍平声场)
CFR python3 scripts/check_cfr.py out.mp4 最长间隔 = 帧间隔
帧数/时长 ffprobe -show_entries stream=nb_frames -show_entries format=duration 与设计一致(音频容器时长含 AAC priming,比视频长约 100 ms 属正常;核解码后长度)
色标 ffprobe -select_streams v:0 -show_entries stream=color_primaries,color_transfer,color_space 三标记齐全

> ★ AAC 会抬真峰值 1.1 dB(实测母版 −3.20 → 成片 −2.1),
> 比通常说的 0.2~0.5 大得多 —— 混音时就要预留 ≥1.5 dB。

9. 封面

  • 横版 1920×1080(公众号/视频号)、竖版 1080×1440(抖音)
  • 底图必须用无字幕原片,不能从成片截帧
  • 从候选里挑:主标压在画面留白区(天空/地面),不要盖在主体建筑上
  • 主标暖金大字(#FFD65A 系)比白字在信息流里更跳;配 8~10px 黑描边
  • 副标(宋体)+ 钩子行(黑体)
  • ★ 一定出对比图并肉眼过:实测第一版把标题压在牌坊顶部、竖版裁碎了牌匾,
    数值上「尺寸对」,视觉上完全不能用
  • ★ 底图用原图,不要用缓存缩略图(缓存 1600px 放大到 1920 宽必糊)。
    脚本里把「原图路径表」读进来,按编号取原图,缓存只作兜底。
  • ★ 底图硬拦 w_blur/:证件照/肖像照那批不能当封面底图(脚本里 raise SystemExit,
    不要靠自觉)。西塘与凤凰都出过「差点把糊化肖像当底图」的苗头。
  • ★ 一批素材的「同一机位」比想象中多:凤凰 834 张里 c017 与 c022 是前后 5 分钟、同机位,
    两张分别当选后封面看起来像重复投放。→ 选完底图必须并排看一遍,
    先按「拍摄时刻 + 机型焦段」把疑似同机位圈出来。
  • ★ 冬季阴天素材:白天片做封面普遍吃亏(发灰、发闷、与夜片风格割裂)。
    实测凤凰连废 3 张白天候选(晾衣吊脚楼 / 大水车 / 石桥),最后四张横版全部统一到夜/暮色,风格才整齐。
    → 同一组封面要有统一的光线基调,别一张白天三张夜景。
  • 竖版裁窗只占横构图源宽约 50%:横跨式主体(拱桥、长廊)竖裁后会只剩桥墩,识别度崩。
    → 竖版底图优先选纵向构图的原片,不要硬裁横构图。
  • 出厂前必做「340px 预演」:把横版缩到 340px 宽拼一条,主标题全部可读才算过。

9.1 ★★ 三平台视频封面规格(2026-09 寒山寺 v1 实测)

平台 封面规格 平台明文红线
百家号(视频) 16:9(≥660×370) 明文「比例一定是 16:9」;禁水印;禁黑框;禁广告性质信息
视频号 横屏 1080×608;竖版 1080×1440(3:4) 封面会被「搜一搜」抓作缩略图 → 文字面积 ≤30%,过大判「营销内容」
今日头条 16:9 —
抖音/B站 9:16 / B站 16:10(比例与视频不同,需单独做) —

⇒ 主版做 1920×1080(16:9)一张通吃百家号/视频号横屏/头条,3:4 只在视频号要求竖封面时用。

★ 渐变遮罩 ≠ 黑框。 平台禁「黑框」,但硬边黑条视觉上就是黑框。
压暗文字区用一侧到另一侧的渐变(画面的一部分),不要用实心黑条。

9.2 ★★ 封面不许出现的两样东西(比好看更重要)

  1. 新闻截图 —— 版权。热点选题时这条最要命。
  2. 钩子元素在素材里不存在时,不许硬凑 ——
    让文字承担钩子、画面承担主题。
    实例(寒山寺 v1):片头钩子是「两只橘猫刷屏」,但全片一张猫的实拍都没有
    (旁白原话「一张猫都没拍到」),素材纪律又禁新闻截图。
    → 封面画的是那口钟(全片母题),文字说的是猫。图与文错位本身就是全片的转折:
    「满院子人在拍猫,没人看那口钟」。钩子最强 ≠ 画面上最该出现。

9.3 ★★★ 文字面积占比 —— 用「同底做差」量,别估算

视频号拿封面当缩略图,文字面积过大会被判「营销内容」(线 30%)。
量法只有一种干净:

> 同一底图出两版 —— 叠字版 与 无字版 —— 逐像素做差,差值区=文字+描边+阴影。

我连错两版,都栽在同一个坑上:
| 错法 | 结果 | 为什么会错 |
|---|---|---|
| 「高对比边缘像素」 | 0.66%(偏小) | 只数了笔画边缘,不是面积 |
| 「文字包围盒」 | 94.3%(荒谬) | 底图自身纹理也满足判据 —— 钟身铭文、院墙红白对比全是高对比边缘 |

★ 通用教训:凡「估算」型判据,先问一句「底图/对照组自己会不会满足它」。
这与本项目已有的做法同源(贴片自检的「对照区」、真空段「成片 vs 底层做差」)。
正确量法实测:文字面积 4.8%(16:9)/10.1%(3:4),远低于线。

9.4 选型判据:选「一眼等于主题」的那张,不是最好看的那张

寒山寺 v1 三版草稿一次看完后定的 A 版:
- A(铜钟特写)✅ —— 底图一眼就是「钟」,正是全片母题
- B(诗碑)❌ —— 文字压在碑座上,把碑上那二十八个字挡住了,
而那二十八个字恰是全片落点(封面挡掉了自己片子的核心资产)
- C(庭院秋景)❌ —— 画面最美,但画里没有钟,钩子弱

9.4.1 ★★★ 封面主标是长句时:分行,不要缩字号(2026-09 寒山寺 v1 实测)

字号自适应有个陷阱:为了把长句塞进一行,它会一路缩字号。
实测同一底图、变量只有文字:

版 主标 字号 占幅宽 380px 缩略
单行 14 字 寒山寺猫打架火了,这正常吗? 123 89.7% 挤成"一条",副标全糊
拆两行 寒山寺猫打架火了/这正常吗? 131 54.6% ✅ 第二行成视觉重心

字号更大 + 行宽更窄 —— 两头都赢。 单行硬排横贯整幅,
缩略图里没有呼吸、且压小了字。封面是"一瞥",要的是透气,不是信息量。

⇒ 实现:cover() 支持 \n 分行(拆行后逐行绘、统一字号=各行都能放下时的最大号)。
⇒ 另有一条:字数多的那行做第一行,短的那行做第二行 ——
短行天然成为视觉焦点(「这正常吗?」5 字 vs 首行 8 字)。
⇒ 副标别写主标里已有的信息(主标已有「寒山寺」,副标别再写「苏州寒山寺」):
砍字数能把副标字号提上去(56 → 62 号,380px 下 11.1 → 12.3 px)。

9.4.2 ★★ 主标用陈述句还是反问句

同一底图实测(寒山寺 v1,2026-09-26 从 A 版改判 D2):

  • 陈述句(「满院子人在拍猫」)—— 讲完就完了,闭合。
  • 反问句(「这正常吗?」)—— 把新闻变成议题,制造未闭合感,逼读者表态。
    ★ 与内容线「观点型 60%」一致;且反问答得上来就引评论、答不上来就引点击。

但反问有一条硬约束:见 §9.6。

★ 副标的分工:主标给态度时,副标必须给事实。
主标已是态度、副标再给态度 = 整张封面零信息。
实例:A 版副标「没人看那口钟」是从主标"人在拍猫"直接可推的,等于没给东西;
D2 副标「那口钟早就不在了」是能拿住的事实 + 悬念(为什么?现在那口是哪个?)。
⇒ 判据:副标能否从主标推出来?能推出来的,就是废话,换掉。

9.5 分发标题(三平台)

标题上限(以列表页实际显示为准):

平台 上限 吃什么
百家号 30 字 搜索流吃实体名且前置;推荐流吃钩子 —— 两笔账
视频号 16 字 社交转发:短 + 有梗。超 16 字的标题在视频号里直接废掉钩子
今日头条 30 字 算法流:信息量 + 悬念 + 具体数字
  • ★★★ 字数一律脚本数,绝不手数(含标点、不含标点两个口径都算,以含标点为准)。
  • ★★★ 同一句文案,用在「会被读」的地方是资产,用在「只被瞥」的地方是负担。
    ⇒ 判断一句文案好不好之前,先问「这块地方读者是读还是瞥」:
落点 读者行为 长句/双问句 为什么
列表页标题(30 字场) 读 ✅ 能用,而且好 两个问句=两个进入理由
视频号标题(16 字场) 扫 ⚠️ 只取半句 整条塞不进;16 字是硬约束
封面主标 瞥(0.3 s) ❌ 不能用 两个问句=零事实,读者拿不住任何东西

实例:用户提的双问句「苏州寒山寺里的猫打架火了这正常吗?寒山寺里究竟什么最值得看的?」
—— 31 字超 30 线,但字数只是小事:它当百家号标题很好(压到 26 字)、
上封面必须拆(拆成「寒山寺猫打架火了/这正常吗?」,下半句移到置顶评论,见 §9.6)。
- ★ 「攻略型问句」不要放进封面/标题。
实例:「寒山寺里究竟什么最值得看的」会把读者预期带成旅游攻略,
而片子不是攻略(讲历史观点)→ 预期错位 → 点进来没得到答案 → 完播率掉。
⇒ 它该待的地方是评论区(问法还要改,见 §9.6)。
- ★★ 三平台标题各不相同是故意的,不要图省事统一 —— 16 字的场子塞 25 字的标题 = 废。
- ★ 热点的时效表述必须「发布日 − 事发日」实算。
实例:新闻在 2026-08,成片 09-26 发 → 不许写「近日」「刚刚」(时效性不实
= 触平台标题党判定,且与自己的史实核查是两套标准)。统一写「刷屏」「八月」。
- ★ 热点事实只写查得到的,一个数字都不编。
查不到权威传播量就不写「X 亿人看过」;网传说法若出自 AI 生成稿 → 不采信。
- ★ 优先用「话题原话」和「现成梗」:
实例:话题原名 #寒山寺猫咪吵架回头全是人# 可蹭搜索;
游客戏改的 「姑苏城外寒山寺,夜半喵声到客船」 = 15 字,正好卡进视频号 16 字线,
且改编梗在社交场最易转发。
- ★ 封面文字与标题可同一句 → 重复强化记忆。
(实例定稿:「寒山寺猫打架火了,这正常吗?」同时用作封面主标与视频号标题。)
- 发布后 48–72 h 一个字都别改(改标题会重置推荐)。

9.6 ★★★ 抛出「问题」的封面/标题,必须配套一个「接住」动作

这条是必做,不是可选。 封面/标题一旦用了反问句,
就必须在发布时置顶一条评论把答案先给掉 —— 否则评论区会滑向两种结果,两种都伤:

风险 后果
被读成在 diss 谁 实例:「这正常吗?」可能被读成「拍猫的人不正常」→ 对撕
被读成标题党 问了不答 → 读者觉得被骗

置顶评论三段结构(寒山寺 v1 定稿文案见 06_deliver/发布文案_三平台标题.md §5):

  1. 先自我表态(「先说我的答案:正常。猫可爱,人人举手机」)
    → 卸掉敌意,堵死"你在 diss 游客"这个解读。
  2. 转话题到片子主体(那口钟、张继)
    → 给读者一个可讨论的实体,把话题从热点拽回片子。
  3. 引评论的问题要「能回忆、不用评判」:
    ✅ 「你去寒山寺,会先看什么?」—— 人人答得上
    ❌ 「什么最值得看?」—— 让人做评判,门槛高、容易冷场
    ★ 这条与口播 CTA 的规矩同源:抛问题引评论,别抛教学口号。

⚠️ 置顶评论同样不放站外链接、不写关注引导(各平台评论区共同红线)。
★ 通用:标题/封面提出的问题,必须有人接住。
「双问句的下半句属于评论区,不属于封面。」

10. 交付件

04_out/ 下:
- 成片 mp4
- 封面横版 + 竖版(规格与判据见 §9.1–§9.4)
- 发布文案_三平台标题.md —— 三平台各 5 条候选 + 简介 + 发布顺序(判据见 §9.5)
- 验收记录.md —— 成片参数 / 色标 / 响度实测 / 运镜统计 / 章节表 /
剪点表 / 事实核查表 / 交付诚信声明 / 遗留事项
- 内容合规审查报告.md —— 开工前就要有(见 §0 铁律 4)
- 作品说明与发布文案.md —— 作品简介 / CC BY 署名(强制) /
多平台文案(抖音短版 + 公众号长版 + 朋友圈一句话)/ 发布前检查清单
- 旁白工作流_写稿到录音.md —— 分工、流程图、录音实操建议(首次建,后续项目沿用)

05_record/ 下(先于成片交付):
- 录音稿.md —— 用户照着念的稿(〔〕 提示 + 分段 + 目标时长 + 朗读提示)
- 字幕_全片.srt —— 机器对齐用的字幕

交付诚信声明 —— 🚨 措辞必须按配音产线分叉,抄错就是虚假陈述。

# 项目 真人原声产线(§3.0) AI 配音产线(§3.0.6/§3.0.7)
① 旁白 按原录音剪辑,未用合成声音替换 旁白由 AI 合成生成
② 图像 未使用 AI 生成图像 未使用 AI 生成图像<br>★ 例外:若画面里用了 AI 生成图/AI 修复老照片/AI 扩图,本条必须改成如实列出
③ 字幕 按剪后录音逐句转写、未做顺句改写(两条产线相同) ← 同左

> ⚠️⚠️ 走 AI 产线却照抄真人那句,比「不声明」更严重。
> 那是自己出具反证:一边放合成语音、一边白纸黑字写「未用合成声音替换」,
> 同一支片子里两句话互相打架 ⇒ 两句都不再可信。
>
> ★ 同理适用于第 ② 条:AI 修复老照片是本技能的常见用法,
> 修过的图还写「未使用 AI 生成图像」,是同一类错的第二副面孔。
> 判断口径:别问「这句话单看是不是真的」,问「合起来会不会让人形成错误认知」。
> 完整事故复盘与落地做法 → §3.0.7。

10.1 ★★★ 尾板「用图 N 张」=屏上实际张数,不是镜数(2026-10-01 东湖案)

东湖尾板曾写「用图 128 张」,实际屏上是 128 主图 + 46 三联辅图 = 174 张。

  • 根因:计数脚本只读镜表「素材」列,而三联/九宫格/画中画的辅图只登记在 plate 台账里。
  • 修法:photo_keys(shots_csv, ledger_csv=None) 增加台账参数,只并台账「辅图」列。
    🚨 绝不并台账「素材」列 —— 它与镜表素材列是同一批主图,两边都读会把主图算两遍(实测 174 → 302)。
  • 连带:尾板取 EXIF 需要屏上全部原图在 01_source/photo/&lt;键&gt;.jpg(东湖原只放 128 张,补了 47 张)。
    规则:凡上屏的图,原图都要在 01_source/photo。
  • 底图反解技巧:_photo_bg() 是确定性变换(cover → GaussianBlur 14 → ×0.32 → 暗角),
    可在无字区(如 x∈[1480,1902], y∈[120,780])逐候选比差,反解出尾板用的是哪张
    (东湖反解 c0121,差 0.122,次优 5.893 —— 判据很干净)。重出后逐像素比对验证只改了该改的地方。

10.1b ★★★★ 素材命名口径 —— cXXXX 定律不通用(2026-10-04 EP07 实测)

credits.py::photo_keys() 的 _KEY_RE = [a-z][a-z0-9]{4,5} 只认 cXXXX/x… 这类编号。
EP07 的素材名是 IMG_7152/大栅栏_2008_10_18_2609 ⇒ 一张都认不出,
尾板直接印出 「用图 0 张」「拍摄(EXIF 缺日期)」(不报错、不告警,静默出图)。

正解:只换口径,不复制排版函数 —— 新建 make_credits_epN.py,
_photo_bg / _wrap / _font / _tokens / music_lines 全部 importlib 直引 KIT。
新口径 = 「去掉扩展名后能在 01_source/photo/ 里找到同名文件」即认(比正则稳,且不依赖命名习惯)。
★ 仍然只并台账「辅图」列,绝不并「素材」列(见 §10.1)。

★★★★ 由此上升为一条通用规矩:

> 跨集复用脚本前,先核「口径假设」,再核「参数值」。
> 口径假设 = 命名规则 / 编号体系 / 常量 / 单位。参数值抄错会报错,口径假设错了会静默出错图。

EP07 一次抓到 4 处同形态问题:

位置 过期的东西 后果
credits.py 正则 只认 cXXXX 尾板印「用图 0 张」
band_masking_epN.py 注释 留着上一集的 −12.4 终混实际 −15.8,判据算错
score_epN.py 注释「句NN」 停在句界表改版前 27 条全错(最大偏 +9)
render_score_ep*.py _rms(0)+… 是 −240 dB 固定偏移 真峰 −3.01 印成 −243.03,误判「渲染失败」

⇒ 尾板生成后必须人眼过一遍成品(数字不对时要一眼看出,别等发布)。

10.1c ★★★★★ 交付前必做「用图级肖像复核」(2026-10-04 EP07 立规)

病灶:立项阶段的人脸闸只扫素材池,而合规声明里写的是「其余可辨认面部者已在选片阶段剔除」——
这是从立项口径推断出来的,没有用图级证据。

正解:两个口径都要跑,且跑 plate 不跑源图。

口径 为什么
plate(屏上实际显示)★ 正确判据 源图裁切/三联拼版后,屏上暴露的面部与源图不同(EP07:源图大脸 21 / plate 大脸 26)
源图 只作交叉参考
  • 池表:02_work/_补漏/_池_plate90.csv(编号=镜号,路径=_plate/s####.jpg)
  • 工具:02_work/工具_人脸闸.py --pool … --out-csv …
  • ★ 人眼终审不可省:人脸闸只检正脸/侧脸,闭眼/侧躺/背影会漏检 ⇒「无人脸」≠「无人」;
    反过来也会误检(佛像/木匾/书法/食物)。
    出密排(contain 不裁切、编号从 csv 实读 —— §2.1 两条铁律),只看占比最高的 12 张即可定性。

★★ 关键产出不是「数字」,是「分批复核授权」:
EP07 用图分两批拍、授权状态不同 —— 2008-08-17(37 张)用户已明确授权;
2008-10-18(34 张)含多张可辨识正面人像特写,授权未确认(立项授权只覆盖前者)。
⇒ 交付前必须按「批」列出授权状态,不能整片一句「已剔除」带过。

> ★ 定规:凡合规声明里出现「其余已剔除/已避免」这类话,必须有用图级复核留痕支撑。

10.2 ★★ 换尾板的最小代价重封(不重跑音频链)

finalize.py 对视频流是 -c:v copy(只重写元数据),所以:

# 1. 重出尾板 png + mp4(--level 必须与正片一致)
CREDITS_DUR=9.28 python scripts/credits.py build --ep &quot;$PWD&quot; --bg-photo 01_source/photo/&lt;键&gt;.jpg
CREDITS_DUR=9.28 python scripts/credits.py clip  --ep &quot;$PWD&quot; --level 4.1 --fade 0.9
# 2. concat(-c copy;帧数必须与旧版一致)
ffmpeg -f concat -safe 0 -i list.txt -c copy -movflags +faststart 02_work/画面_含尾板_带字幕.mp4
# 3. 只换视频、沿用已验收的音频流(零风险;色标 bsf 必须补回)
ffmpeg -i 02_work/画面_含尾板_带字幕.mp4 -i 旧终版.mp4 -map 0:v:0 -map 1:a:0 \
  -c:v copy -bsf:v h264_metadata=colour_primaries=1:transfer_characteristics=1:matrix_coefficients=1 \
  -c:a copy -movflags +faststart -video_track_timescale 15360 新终版.mp4

东湖实测:帧数 11223 不变、响度复测 I −14.2 / LRA 2.6 / TP −2.0 逐位不变。

★★★★ --level 4.1 的对齐对象是「烘字幕后的正片」,不是原始片段(2026-10-02 EP2 踩到)

阶段 profile / pix_fmt / level / range
原始片段(render.py --stage shots) High / yuvj420p / 40 / pc
_video.mp4(concat -c copy) 同上,原样保持 40 / pc
_sub.mp4(burn_subs.py 重编码) High / yuv420p / 41 / unknown
_tail.mp4(credits.py clip) 必须 41 ← 它 concat 的对象是上面那一行

burn_subs.py 的 --level 默认就是 4.1(重编码时 x264 按内容判定,1080p30 会写 4.1)。
尾板要对齐的是 _sub.mp4(41),不是原始片段(40) ——
「对齐正片」这句话有歧义,对齐对象搞错,concat 出来的流参数就不一致。

同理 --total 必须用实测尾板时长:9.280 s × 30 fps 取整成 278 帧 = 9.266667 s,
写常量会与视频流差 0.013 s。一律 ffprobe 现测。

★ 另外:_sub.mp4 重编码后 duration 会精确等于镜表值(帧数 ÷ 30),
而 _video.mp4(concat copy)的 format duration 会因容器时间戳取整偏小约 0.02 s。
核帧数才作准(ffprobe -count_frames)。

10.3 ★★★ plate 必须按「镜号」命名,不是素材编号

render.py stage_shots 取图路径是 plate/(镜号 + &#x27;.jpg&#x27;)。东湖第一版按 cXXXX.jpg 命名 ⇒ 全链找不到图。
配套落 _ledger.csv 台账(镜号/素材/辅图/版式图/源路径/源 sha1 + plate sha1)——
「哪个镜用了哪张图、某张图上屏几次」事后可核,§10.1 的尾板纠错就靠它。

竖图不再 blur-fill,改「三联版式」(对齐系列下限 EP2):中主图 contain(980 宽)+ 左右辅图
cover(各 690 宽),底 (16,16,18)、gap 10。辅图三闸:face_lv ∈ {0,1} + detail ≥ 0.10
+ ±150 s 同场景两档排序;另留人工黑名单(东湖 c0605「公园草地鸡群」曾挂上「石宕」段落)。

★ 灰阶素材要靠程序判定、不能靠人工名单:东湖书面名单只列 8 张,
实测 24 张连续帧(10:54:25–11:00:02)。
判据:chroma = mean(max(RGB) − min(RGB)) &lt; 1.5 判灰阶,整组不用。

10.4 ★★★ 素材合规复核:OCR 不是判决器,且复核面要盖住整个候选池

2026-10-01 EP2 东关街实测出来的两条硬结论。下片直接照办,别重复踩。

一、OCR 只能当「线索生成器」,判决必须靠人眼

实测反例(同一批素材):

图 画面实际是什么 macOS Vision OCR 读出
c0478 「功德箱」黄底黑字大招牌 只有「財」一个字
c0475 财神殿匾额/对联 有降龍伏席手設龜蛇云乎数(乱码)

⇒ 漏检率极高,ocr_hits.txt 为空不等于干净。规矩:

  • 主判据 = 人眼画面复核(拼图铺满一屏,一张图只读一次);
  • OCR 只用来挑出可疑项,用来「多发现一条线索」,不用来「排除」;
  • 敏感词表必须简繁双写(财神|財神、功德箱|功德箱、殿|殿)——
    繁体招牌用简体词表匹配不上;
  • 词表分三类:政治(含「国家/政府/主席」这类会误伤官方文保碑的词 → 必须人工裁定)/
    宗教(功德箱 最高优先,其次 神像/香炉/开光/殿)/医疗功效(广告法)。

二、复核面 = 整个候选池,不是「已进片的 N 张」

踩的坑:只扫了已进片的 227 张,而 pick_aux 会从未扫过的剩余竖图池里取图 ——

  • s0155(全片最后一镜)连换三轮全是没扫过的新图:泥塑兔 → 招财猫霓虹灯 → 兔子玩具;
  • 而 c0478「功德箱」就躺在那个池子里,随时会被捡进来。

⇒ 规矩改成:凡有资格进片的,一律先过闸。 实现上就是
make_plates 的候选池(可用=Y + 纵横比匹配 + detail ≥ 0.10)整池过一遍,
而不是只过 _ledger.csv 里已登记的那些。

三、face_lv=2 这条闸:本池的绝对刻度不可信,要逐张看检测框

实测全池分布:lv0 = 0.0000(无检出)/lv1 = 0.0050–0.0350/lv2 = 0.0352–0.1200。
lv1 上界与 lv2 下界只差 0.0002,且全池没有任何高置信人脸(真人脸特写会到 0.9+)。
⇒ 这条线是在检测器噪声带里划的。

做法:zoom_facebox.py —— 以 face_box 为中心裁 4 倍框宽放大,并带一张已知误报做对照
(EP2 用 c0106 = 武当行宫殿内的神像,max 0.0519,证明同分数带 ≠ 人脸)。
实测判定:c0058(max 0.0525)框内 = 纯青砖墙纹理;c0044(0.0975)框内 = 木门铺首衔环
(门环本身就是兽面造型)⇒ 两张均为误报。
豁免要落在代码里、写清证据(bind_ep02.py 的 FACE_WAIVER),不要口头放行。

四、黑名单 AUX_BLOCK 治不了「整段同质」→ 需要 AUX_PIN

s0155 附近那个时段拍的全是商铺工艺品。拉黑一张,pick_aux 按时间邻近补上的是
同质的另一张 —— 连换三轮都是。
⇒ make_plates 要有辅图显式指定 AUX_PIN = {镜号: [左, 右]}(被指定的仍进 taken,
不破坏跨镜不重复)。收尾镜/情绪落点镜优先手工指定。

五、注释会写错,主图内容一律以看大图为凭

EP2 的 bind_ep02.py 里 c0319 写着「黑色木门 + 石阶」,实际是
「东关街街景 + 木质导览指示牌」—— 按错误注释选的主图,与旁白「门楼气派」不贴。
而真正的长乐客栈门楼是 c0329(OCR 实读匾额 =「長樂客棧」)。
⇒ 主图复核要单看 plate 成品,不能只看候选表缩略图,也不能只信注释。

★★ 同一坑 2026-10-02 又犯一次、且更贵:c0145 的注释写「姜铺柜台」,
而旁白逐字是「有姜铺的柜台」 —— 注释与台词严丝合缝,于是一路骗过选片、迁移、
连第一轮 plate 肉眼复核都没抓住。直到把 plate 放大到原尺寸才现形:
它是彩色虎头布艺玩偶特写。
⇒ 加强版规矩:凡「注释说的内容」正好等于「旁白说的内容」的位置,必须看原尺寸 ——
那是注释最可能被「按台词倒推」写出来的地方。

六、★★★★ 辅图必须与主图同等人眼复核(本轮最贵的一条)

EP2 第一轮只人眼复核了主图,辅图只过了 flv ∈ {0,1} 机器闸。实测后果:
抓出 4 张必须换的 —— c0553(中景真人,脸清楚)、c0618(店内顾客,真人背对)、
c0272(泥塑人物)、c0460(玩偶堆)。

为什么机器闸必漏:那 4 张的 faces=1、face_max 只有 0.032–0.033 ——
脸小 ⇒ 闸全绿。而 pick_aux 的闸只有 flv 这一道,没有内容闸。
更根本的原因:辅图是机器自动挑的,选片时人工根本没看过它们。

做法:写一个只拼辅图的放大页(EP2 是 aux_sheet_ep2.py:12 列 × 6 行,
按辅图真实宽高比 690:1330,每格 300 宽 ≈ 43% 原尺寸),一次读完 72 张。
⇒ 铁律:三联/九宫/画中画的辅图,必须与主图走同一套人眼复核,不得只靠 flv 闸。

七、★★★★ 黑名单必须是单一来源,且写盘前必须有硬闸

EP2 的黑名单一度存在两份:make_plates_ep2.py 里 18 条、
show_aux_pool.py 里只有 16 条(缺 c0475/c0476/c0478/c0499)。
更致命的是它只活在 plate 脚本里 ——
只有辅图挑选看得到它,而补位脚本(pick_add_*.py → migrate_bind_*.py)
从不知道有这张表,于是把已封死的 c0105(武当行宫宗教供奉)、
c0499(粉衣老人背影近景)又灌回主图。

做法:
1. 抽成独立模块 07_work/blacklist_&lt;ep&gt;.py,理由随图走({id: 为什么拉黑}),
不只是一份 id 列表 —— 复审时才知道该不该摘。
2. 所有写盘脚本在落盘前调 assert_clean(ids, where):踩到就报错停下,绝不静默放行。
3. 用 sys.path.insert 直接 import,不要正则从别的脚本里抠名单(名单一搬走就崩)。

八、★★★★ 「换一张图」这个动作本身,要重跑全部闸门

EP2 因「注释撒谎」把 c0145 换成 c0321(「姜」字大招牌 + 柜台 + 货架,
内容与旁白完美贴合)—— 但换图时只核了「内容对不对」,没有重新过肖像闸。
成片关键帧复核时才发现:c0321 画面里有两位店员,其中一位面部轮廓可辨。

补救不是口号,是把全片的量化判据一次性拉出来(比逐张看更快、更硬、还能直接写进声明):

判据(源:照片池 CSV 的 face_box 列) EP2 实测
全片最大人脸框宽占画面比 0.0340 ⇒ 65 px @1920
框宽 ≥ 0.10(192 px,业界「可辨识」量级) 0 张
框宽 ≥ 0.05(96 px) 0 张
face_lv = 2 0 张

⇒ 结论「未作人物特写」成立、该图保留。

★ 但尾板措辞仍据实收紧:把「动作承诺」改成「状态描述」

措辞
原 片中游客均为远景或背影自然入镜 · 未作人物特写;可辨认面部者已在选片阶段剔除
新 片中游客均为远景或背影自然入镜 · 未作人物面部特写 · 无可辨识的个人肖像

理由:原句是绝对的动作承诺(「已剔除」),而画面里事实上仍有 65 px 的远景小脸;
新句陈述的是可核验的状态,与上表判据一一对应 —— 被质询时拿得出数字。
⇒ 规矩:尾板/文案里的合规声明,优先写「可核验的状态」,不写「我已做过的动作」。

九、★★★ _pool/ 里的图不是原图(EXIF 已被剥);补图只能回源盘

EP2 补 14 张缺的上屏原图时,第一反应是从 07_work/_pool/cXXXX.jpg 复制 —— 错的。
_pool 里是 2400×1600 的处理副本、EXIF 已被剥掉
(实测 _pool/c0001.jpg 的 DateTimeOriginal = None,而 01_source/photo/c0001.jpg
是 5616×3744 原图、EXIF 完整)。而尾板的拍摄日期是 credits.py
从 EXIF 现算的 ⇒ 拿 _pool 补进去,尾板会打印「(EXIF 缺日期)」或只算得出部分拍摄日
⇒ 尾板上的日期是假的,而尾板是发布件。

正确做法(见 ep02_扬州东关街/07_work/fill_photo_dir.py):
1. 从照片池 CSV 的「路径」列取源盘原图(5616×3744,EXIF 完整);
2. 复制前后都复核 EXIF(复制后 DateTimeOriginal 必须与池记录逐字相同);
3. 逐张打印对照,不一致就停下 —— 不静默放行。

★ 元教训(比这条坑本身更重要):同一个坑「技能里早就写了」却还是重犯。
本脚本第一版又把回退链写成 ex.get(36867) or ex.get(306),把 c0001 读成
2011:01:08(真值 2011:01:06,差两天)。
⇒ 写任何读 EXIF / 读元数据的脚本之前,先在本技能里搜一次关键词
(DateTimeOriginal、level、music-gain、duck…)。
技能是 2700+ 行,靠记忆必漏 —— 这与用户那句「不会每次都从头撞一圈吧」是同一件事:
先查,再写。

10.5 ★★★★ 锚「不变量」,不锚「派生量」(2026-10-02 EP2 重排,三处同犯)

EP2 的镜表从 v1(155 镜/438.767 s,按估稿时长排的)改成 v3(149 镜/412.600 s,
按真人录音实测)—— 因为估值 ≈2.65 字/秒,实录音短了 26.157 s,镜数必须重排。

合并「1-2」这一下,让所有组号整体位移 1–3 位。 于是三处写死的「组号」全部指错:

脚本 写死的量 后果
bind_ep02.py 按组号索引的绑定表 绑定与镜表错位
migrate_bind_v3.py 同上 补位补到别的镜上
make_music_ep2.py HANDOVER_GROUP = 53 + 12 个锚点组号 换曲点指到别的句子上

根因:组号是「派生量」(=镜表里 (段, 句序) 变化的位置编号),句序才是「不变量」。

改法(一次性根治):
- 配乐脚本的 sheet_info() 同时返回 g_first(组号→秒)与 g_by_sent(句序→秒);
锚点一律锚句序,并在解析时对未知句序报错停下(不静默取默认)。
- 换曲点常量从 HANDOVER_GROUP = 53 改为 HANDOVER_SENT = &#x27;54&#x27;。
- ⇒ 镜表今后再改(合并、增删镜、换集),配乐脚本一个字都不用动。

推广判据:写任何脚本前先问一句 ——
「我锚的这个数,会不会因为上游数据增删而改变?」
会 → 它是派生量,去找它背后的不变量(句序/素材 id/sha1),锚那个。

11. 坑清单(都是实测踩出来的)

  1. 批量 os.remove 清理缓存目录会触发安全确认中断脚本 → 改成同名覆盖、不清空。
  2. mkdir(exist_ok=True) 在沙箱下对已存在目录可能抛 PermissionError → 先判 os.path.exists()。
  3. 脚本若用 argv[1] 单选动作,传多个参数只会跑第一个 → 分次调用或用 &amp;&amp; 串联。
  4. 剪点若落在语音上,听感上是「吞字」而不是「静音」—— 剪完必须重转验证。
  5. 点题卡遮幕过重会把照片压死(见 §6)。
  6. 片尾与末条字幕时间重叠是高频错误 —— 交叉检查。
  7. ★ 剪接刀口切进保留侧,通读发现不了 —— 「要漫步其间,古代官府是怎么来
    运作的」读得过去,却缺了谓语。必须逐刀看刀口处的前后文(见 §3.3)。
  8. ★ 字幕与录音稿必须是两份文本 —— 字幕不加标点(画面干净),
    录音稿必须加标点(标点决定停顿与语调升降,没标点就念成一串没起伏的流水)。
    录音稿还要带 〔〕 提示、〔停 N 秒〕、语气标记(见 §3.0.1)。
  9. 改一刀 ≠ 只改音频 —— 时轴平移会连锁到镜头表、字幕、章节卡、片尾、
    混音、成片。定刀口前先把这一串代价想清楚(见 §3.3 末)。
  10. ★ 用户的决定不要反复推回去 —— 用预置音色的对比音频去论证克隆音色
    的效果,是无效论证,会被用户的实际体验直接推翻。声音克隆这条路用户已经
    否决过一次,不要再提
    (详 §3.0)。做任何演示必须当场标明用的是哪一种技术。
  11. ★ 纯白水印在浅色背景上等于没有 —— 长文配图的水印必须
    白色填充 + 深灰描边(或反之),否则在天空、白墙、浅色砖上完全看不见。
    第一版就栽在这,见 §12。
  12. ★ 正文/片尾里的时间必须拿素材 EXIF 核一遍,不要写「去年/最近」这类相对表述。
    本次开头「去年冬天」与文末「摄于 2019 年 12 月」矛盾六年,
    且文章已经发到平台上了才发现(见 §12.4.1)。
    > ★★★ EXIF 的「拍摄时间」只认 DateTimeOriginal(36867) / CreateDate(36868)。
    > DateTime(306) 是「文件修改/从相机导出的时间」,不是快门时刻。
    > 实测同一张照片:DateTimeOriginal = 2012:12:01 15:03:07,
    > DateTime = 2012:12:10 14:32:29 —— 差 9 天,且整批 163 张全都不同。
    > 回退链写成 ex.get(36867) or ex.get(306) 会「静默印成导出时间」,
    > 不报错、不看日志发现不了 ⇒ 判据:「读不到就退到另一个字段」= 用错的冒充对的。

    > 读不到就返回 None,让上层显式报「EXIF 缺日期」。
    >
    > ★★ 片尾板的「拍摄」日期也走同一条路 —— 不许手写。
    > 踩过的坑:板上写「2026 年 9 月」,那是做片子的月份(项目所有文件时间戳都是当月),
    > 照着「今天」写就错;片头那句新闻由头式的「今年八月」同样不等于拍摄事实。
    > 正解:读镜表实际用到的照片的 DateTimeOriginal 现算(寒山寺:163 张全部
    > 2012:12:01,14:40–15:51 一次连拍 ⇒ 板印「2012 年 12 月 1 日」)。
    >
    > ★ 怎么判「这真的是快门时刻」而不是相机时钟复位:
    > 时间与文件编号同序递增(IMG_7290@14:40 → IMG_7499@15:51)= 真实连拍;
    > 时钟复位的默认值产生不出与编号同增的时间序列。
    >
    > ★ 本机 PIL 的 _getexif() 返回裸 dict(不是 Image.Exif),get_ifd() 会抛
    > AttributeError;但 36867 已被拍平在顶层,ex.get(36867) 直接可用。
    > 两条路径都试,谁有用谁。

    >
    > ★ 定位原始素材用 mdfind -name &quot;&lt;文件名&gt;&quot;(Spotlight,秒回),
    > 别用 find 遍历六个盘。同名文件可能遍布多个年份的文件夹
    > (实测 IMG_7347 有 10 个互不相同的版本,横跨 2009–2018),
    > 必须「夹 + 文件名 + 时间窗」三重限定才敢认定是同一张。
  13. ★ 图表/按钮的位置说明不要只给名字 —— 没有文字标签的纯图标按钮,
    必须给「图标长什么样 + 相对位置 + 相邻元素」,并裁剪放大用户截图确认
    (见 §12.4(2))。给错位置会让用户白找两轮。
  14. ★ 一稿多用要真的出第二套文件,不要只在文档里写「记得改标题」——
    标题/开头/水印/版权段四处一起改,独立目录,并写清「两套图别混用」(见 §12.4.1)。
  15. ★★ ffmpeg -filter_complex 里多一个逗号 = 整个任务报废。
    fc.append(f&quot;{prev},&quot; + &quot;,&quot;.join(fx) + &quot;[vout]&quot;) 会拼出 [v3],fade=t=in:... ——
    逗号后面被当成一个空滤镜,直接 No such filter: &#x27;&#x27;。正确写法是 f&quot;{prev}&quot; + &quot;,&quot;.join(fx)。
    为什么难发现:这条分支只在「挂片头淡入/片尾淡出」时进入。
    写预飞自检时图省事把 fade_in/fade_out 填成 0 → 正好跳过唯一有 bug 的分支 → 自检全绿、正式跑必挂。
    > 铁律:预飞自检的参数取值必须是正式跑的真实值。 图省事填 0/空/默认,等于没自检。
    > 自检要故意走遍「有/无」两条路。
  16. ★★ mp4 的 nb_frames 是「声明值」,不能拿来判文件完整性。
    -movflags +faststart 把 moov 挪到文件头 ⇒ 被砍掉一半的 mp4 照样读出完整声明帧数
    (实测:271 MB 的批文件砍到 40% 字节,ffprobe -show_entries stream=nb_frames 仍报 3230,-count_frames 真解只有 1251)。
    ffmpeg -sseof -2 -f null - 尾段解码同样骗得过(干净退出、无报错)。
    → 长任务一律「批处理 + 台账制」断点续跑:
    ① 按批切,每批输出独立文件;② 每批编码完立刻全解码验一次(本来就要打印 ✅,顺手记账);
    ③ 记账进 _chunks_manifest.json(want / size / frames);④ 重跑只复用台账对得上的批;
    ⑤ 被 kill 最多损失当前一批。复用核 文件大小 —— 大小变了就是一票否决。
    > 省的是几分钟,赌的不能是成片。
  17. ★★ 复核口播/旁白「念的是 A 还是 B」,必须两套独立 ASR,不认孤证。
    本机 whisper-cli(/opt/homebrew/bin/)Metal 路径必崩,large-v3-turbo/small
    即便 -ng 走 CPU 也初始化失败 —— 只有 ggml-base(148 MB)+ -ng 能跑:
    bash B=~/.cache/whisper.cpp/ggml-base.bin ffmpeg -v error -y -ss &lt;起点秒&gt; -t &lt;时长&gt; -i 录音.wav -ac 1 -ar 16000 /tmp/w.wav /opt/homebrew/bin/whisper-cli -m &quot;$B&quot; -f /tmp/w.wav -l zh -ng -np 2&gt;/dev/null
    base 在带方言/古语的口播上会明显写错字(「壶园」→「湖源」、「咸丰同治」→「闲风从至」)
    → 绝不能整句照抄,它的用法是只取「二选一」那一个词。
    判据:① 先把 ASR 容易错的地方逐条列出;② 若全数是 ASR 听错、稿件没错
    → 说明本录音的 ASR 不可信,孤证不足以推翻稿件;③ 只有两套独立 ASR 在同一处指向同一个词,
    才认账 —— 此时是人念错了。
    处置:字幕是录音的转写,不是稿件的复印件。 证据够了就必须改字幕跟音频走
    (观众「听到的」和「读到的」对不上,比写错一个字更刺眼)。
    改法写进代码里的差异表(如 make_subtitle.py 的 AUDIO_FIX,可复现、可 diff),
    不是手工改成品;改完必须 diff 一次,确认只有预期的那几行变了。
  18. ★★ 编号只允许程序生成。 手写的「时间 → 编号」对照表必须回候选表对账
    (本次整表错了一格:拱门标成 Y241、小红桥标成 Y242,真值是 Y242=拱门/Y243=小红桥)。
    引用具体画面优先用文件名(IMG_1734.jpg)而不是编号 —— 本片成片没被污染纯属侥幸,
    因为镜表引用拱门用的正是文件名。发现错格后必须回查所有已出产物(封面/片尾/镜表/字板)。
  19. ★ 出片流水线要做成一条幂等命令(见本项目 02_work/build_final.sh):
    拼接 → 烧字幕/引文板 → 贴片尾 → 挂音轨 → 补色彩标记 → 体检。
    每步都可重跑,上游有台账就直接跳过。别手工一步步敲 —— 中断一次就得从记忆里重建整条链。
  20. ★★★ 「中间产物正确」≠「成品正确」—— 叠加类工序必须回读落盘成品来验。
    真实事故(2026-09-24《两座金山》):成片参数七项体检全绿
    (514.9 s / 1920×1080 / 30 fps / 三色彩标记齐 / −14.2 LUFS / TP −2.0 dBTP),
    但画面里一条字幕都没有,只有引文板在。
    • 根因:loaded.sort(key=lambda x: (x[2] != &quot;P&quot;, x[0])) 把 7 张板排在 113 条字幕前面,
      而逐帧循环里写了 if t &lt; a - 0.02: break —— 遍历到第 2 张板就跳出整个循环,
      排在板后面的字幕永远轮不到。实测 t<449 s 的 110 条字幕全丢。
      > 铁律:列表按「复合键」排序时,任何「提前 break」的优化都不成立(它默认只按单键升序)。
      > 要么改成 continue,要么另建一个只按起点排序的索引。
    • 为什么没被发现:体检全是容器/流参数层面的,没有一项看画面里有没有字;
      而我先前验的是字幕 PNG 本身(张张完美)和引文板(板是好的)—— 验的全是中间产物。
    • 修法(两道,缺一不可):
      ① 加叠加统计:字幕帧数/板帧数各记一份,任一类为 0 直接 raise;
      ② 加回读成片自证:抽 8 条字幕,在每条中点分别从成片与源片读同一帧,
      数「字幕带」里的近白像素(字幕是白字+黑描边,落上去必然显著增多),
      要求 成片 &gt; 源 + 2000,任何一条不合格就报错。
    • ★ 检查写完必须做反向测试:拿刚做出来的坏文件喂给它,必须报错
      (本例 8/8 全部报错 ✅)。 没做过反向测试的检查,等于没有检查。
  21. ★★ 图文配图的「图号」有两处都要写,两边必须程序化对齐。
    真实事故(2026-09-24《两座金山》图文):build_article.py 的 PLACE 表按叙事顺序
    写了 20 条图注,picks.csv 按另一套顺序列了 20 张图 —— 两边不同序,
    图注就整段错位(「临水通廊」的说明挂到「殿前庭院」的图上),而每一处单看都合理,
    通读发现不了。同源错误还有:① 映射表整表错一格(§11.18)② 手数段落号一改字就错位。
    • 铁律:图注表不手抄。 用 make_caption_table.py 从 PLACE + picks 程序化拼,
      并断言 len(PLACE) == len(picks),不等就报错。
    • 插图位置用「锚点句」不用「第几段」:(&#x27;堆着堆着,堆成了一座岛&#x27;, 4, &#x27;…&#x27;);
      脚本按子串找段,命中数 ≠ 1 直接退出(找不到 / 命中多段都算失败,绝不静默插错位置)。
    • 发版正文一个字都不许手打:拼完做逐字 diff(去掉图行/标题/尾板后与定稿比对),
      不一致就报警退出。手抄会悄悄改字(实测把「那」抄成「这」、半角引号被换成「」)。
    • 一条命令固化全链:06_article_bjh/build_all.sh(导出配图 → 拼 MD → HTML → docx 两版 → 自检
      「20 图 / 4 产物 / 无占位符」)。同 §11.19,别手工敲。
    • ★★ 换图必须连「图注」一起复核 —— 换了图、没看注文 = 换了个新的错。
      真实事故(2026-09-25 张家界图 7):原图因有可辨游客被换掉,换进来的新图是
      大晴天通透峰林,而注文仍写着「石峰一片一片地立在雾里」—— 图与注文互相打架,
      直到下次缩略图核对才暴露(中间隔了一整轮)。
      动作:改 picks.csv 的 c 编号时,同一轮必须把 build_article.py 的图注、
      以及正文里所有描述这张画面的句子一起过一遍。只改编号 = 半截修复。
    • ★ 挑图时「低对比度 ≠ 雾」。 用 contrast 低当雾的代理指标,会把猴子特写、
      溶洞红灯、无叶枯枝
      全吸进来(实测前 16 名里 10 张是这三类)。
      可靠做法:从视频已选镜(edit.json 的 207 镜)里找 —— 那批是按内容精挑过的,
      再按 contrast 排序取前 20,一次就命中真雾景(work/diag/fog_from_edit.py)。
    • ★ all_meta.json 的键是「批次__IMG_编号」,不是 cXXX.jpg。
      拿 c 编号直接 meta.get(ph) 会全部返回 None ⇒ 候选为空 ⇒ Image.new 高度为 0 ⇒
      保存时报 cannot write empty image,看着像盘坏了,其实是查表键写错。
      查表必须先过 ph_map.json(cXXX.jpg → 批次__IMG_编号),而 edit.json/picks.csv 里存的都是 c 编号。
    • ★★ 换代产物必须显式标注作废,只在新文档里说「以新版为准」没用。
      真实情况(2026-09-25):同一次交付里同时躺着旧版 6 张配图 + 1400 字正文(27 字标题)
      与最终版 11 张配图 + 1168 字正文(29 字标题),两个目录都叫「配图」。
      做法:在旧产物的入口文档顶部加作废横幅 + 新旧对照表(标题字数/正文字数/图数/水印/生成方式),
      并在验收清单里把旧项直接标成「已作废,勿用」。用户会从旧文档开始看。
    • ★ 交付图文时同一轮出两件:① 预览_图文_*.html(图按顺序插回原位 —— MD 里图片只是
      【图 N · 注文】 一行占位,对不对位肉眼看不出来);② 剔除插图标记的纯文本正文
      (免得把方括号标记一起粘进平台编辑器)。两件都由 build_preview.py 从 MD + picks 程序化生成。
    • ★★ 交付图文必须给「能导入的文档」,不能只给正文+一堆图。
      用户原话(2026-09-25 张家界):「得做成导入文档式的呀!否则文插图不得搞死?」
      —— 只给 正文.txt + img/ 11 张,用户就得在编辑器里一张张手动插图。
      平台「导入文档」能力(2026-09-25 核实):
      百家号 = 发布 → 图文 → 导入文档,支持 docx/pdf/txt,上限 10MB;
      头条 = 2MB。→ 两版都出(高清 1600px + 轻量 860px),按更严的 2MB 卡线。
      一条命令:bash 06_article_bjh/build_all.sh(配图 → MD → HTML → docx 两版 → 对位预览 → 自检)。
      导入后必查三件事:① 标题框是否空(导入只解析正文,标题要另填)
      ② 正文首行有没有把标题重复一遍 ③ 图是不是 11 张都在(尤其尾板那张)。
    • ★ docx「生成成功」≠「能用」,必须逐项拆包核。 三项计数要一致:
      word/media/ 图片文件数 = document.xml 的 &lt;w:drawing&gt; 数 =
      r:embed 引用经 document.xml.rels 能落到 media 的条数(11 → 11 → 11 无断链)。
      再加一条:系统 textutil -convert html 能解析成功(证明是合法 docx)。
      手写 OOXML 最容易断在 rels 上,而断链的 docx 打开只显示红叉、代码不报错。
    • ★★ 改标题必须回扫「脚本里写死的那一份」。 标题在交付里至少 6 处出现:
      定稿标题表/爆款标题/发布文案/上传即用/验收清单/build_article.py 的 TITLE 常量。
      实测漏掉的正是脚本里那份 —— 所有文档都显示新标题,唯独脚本拼出来的 MD 首行还挂着旧标题,
      而 MD 是唯一会被复制进平台的正文。改标题时用 grep 把旧标题全文捞一遍,为 0 才算改完。
  22. ★★ 大批量复制(rsync)跑完,不要拿 exit=0 当验收。
    退出码只说明「它认为传完了」;半截文件、被排除规则漏掉的、写坏的分片,它都不报。
    真实案例(2026-09-24 音频搬运 108.67 GB / 7,271 文件):两边各 walk 一遍、
    按相对路径建 {路径: 字节} 对账,才发现目标比源多 887 个 / 31.22 GB ——
    那批是「第一批已搬、源那边后来清掉」的,不是漏搬;若只看 exit=0 就删源,
    等于把「其实没核对过」当成「核对过」。
    • 三条一起给:文件数 + 总字节 + 相对路径清单,缺一不算过。
    • 口径必须两边一致(本例都排除 macOS 的 ._* / .DS_Store),否则差值全是噪声。
    • 脚本:_shared/素材库管理/verify_audio_move.py(含 {路径: 字节} 对账与人类可读汇总)。
  23. ★★★ 缓存编号(cXXX.jpg)是「间接引用」,扩池后必然整体漂移,且旧文件还在。
    管线的真实链条是两跳,任何一环失配都不报错:

    镜表说的素材(P 编号)──pick_id*.json──▶ src 键 ──ph_map.json──▶ 缓存编号 cXXX.jpg ──▶ 实际画面

    真实事故(2026-09-25 张家界):cXXX 的编号规则是「key 在镜表首次出现的顺序」。
    扩池(122 → 184 张不同素材)后,编号→素材的映射整体偏移,
    但 work/w/ 里的旧文件一个没删 ⇒ 「镜表说第 1 镜是 A,c001.jpg 里是 B」,
    静默出错,肉眼在成片出来前发现不了。
    - 判据:缓存编号永远不能当"身份"用。 当身份用的必须是真实路径 src。
    - 修法(三重,缺一不可):① mk_edit.py 建缓存改全量重建,并在结束时
    json.dump 落盘 ph_map.json +抽查 12 个编号的 mtime 自证;
    ② 重建前先比对新旧映射,不一致自动转 FORCE;
    ③ 凡是引用缓存编号的地方(封面脚本、配图脚本、抽帧脚本)每次重跑后都要回来重核编号。
    - 封面/配图的编号注释必须写清「核对链三步」(P 编号 → src 键 → cXXX),
    只写一个编号,下次一定重踩。
    24. ★★★ 渲染指纹必须挂「真实路径 + 映射内容哈希」,挂编号 = 一条都失效。
    指纹原为 f&quot;{ph}|{mv}|{dur}&quot;,其中 ph 是缓存编号。扩池后编号没变、映射变了
    ⇒ 指纹一条都没变 ⇒ 210 镜全部 skip ⇒ 拿着新镜表合成的成片里装的
    还是旧画面,而时长、帧数、镜头数全对得上(这是最毒的地方:所有体检项都绿)。
    - 正确指纹:f&quot;{ph_map_md5}|{src}|{ph}|{mv}|{dur:.3f}&quot;,
    其中 ph_map_md5 = md5(ph_map.json)[:10],映射文件一变 → 全部指纹失效 → 强制重渲。
    - 必须打印 skip/new 构成,并在「全是 skip」时告警。
    判读口诀:「一条都没新渲」在改过镜表的轮次里 = 一定有鬼,不是"缓存命中率高"。
    25. ★★★ 复核分四个粒度,但只有最后一个验的是交付物。
    | 轮次 | 复核对象 | 建议格尺寸 | 验的是什么 |
    |---|---|---|---|
    | ① | 按镜号复核表 | 470×264/格 | 镜表 |
    | ② | 主力池复核条 | 168×112/格 | 池 |
    | ③ | N 池大格表 | ≥375px/格 | 池 |
    | ④ | 成片全片抽帧(每 10 s 一帧) | 330×205 | ✅ 交付物本身 |

    ①②③ 全在间接引用的上游,任一跳失配,前面验得再细,成片里也是旧画面。
    本轮两次静默失败,都是第 ④ 轮才抓到的。
    - 抽帧脚本:work/film_check.py [步长秒] [每页帧数],直接从成片抽,不经过任何中间映射。
    - ★ 小目标漏检率由缩略图尺寸决定(实测):480px 原图缩到 168px 后,
    30px 的猴子只剩 ~10px 认不出;缩到 375px 仍为 ~23px,轮廓可辨。
    结论:找人物/动物/现代设施,格子不能小于 375px。
    - ★ 小目标的程序判据当前无可用方案(三种实测全败):
    暖色小连通域(65 张误报全是砂岩色调,真凶排不进前 30)/
    近水平长直线(屋顶/护栏判据,真凶得 0 分)/人脸占比(猴子不是人,远景小剪影也漏)。
    唯一可靠办法就是把格子放大到 375px 以上让人眼看。
    26. ★★ 「拍」的语义边界划错,配图再好看也是错的。
    真实事故:旁白三句讲石峰数量与形态(「三千多座/超过一千米高的有二百四十三座/
    它们从雾里冒出来」),却挂在 07_冬天雾凇(主池含 H:branch 雾凇枝特写)下
    ⇒ 成片出现雾凇枝特写配「243 座千米石峰」。
    - 判据:一个「拍」= 一句话的语义单元,不是"感觉差不多的一堆图"。
    数字、数量、形态类句子必须单独开拍,池里只放能承担这个数字的画面。
    - 查法:work/av_check.py —— 字幕 × 画面时间轴对齐接触表,
    逐镜取中点字幕句拼成几页,一眼看出「这句话配的是哪张」。
    27. ★★ 审计自己写的报告时,凡"具体名词"(曲名、文件名、行内文字、授权类型)必须回源头核。
    真实事故(2026-09-25 张家界合规报告):凭记忆写了三处失实 ——
    ① 配乐写成 Music of Silence 一首,实际是 Memories Of Stone + Echoes Of Home 两首;
    ② 把底部字幕当成尾板文字(尾板实为「素材…张家界实拍」);
    ③ 环境音来源写「freesound 548373」且称「尾板已印」,尾板根本没这一行。
    - 核验链:EDL json(实际用了哪些文件)→ 生成脚本文本定义(make_text.py 的 TAIL)
    → 成片实抽帧放大逐字读。三者一致才算核过。
    - 曲名/署名类错误 = 授权违规(历史红线),不是笔误。
    - 报告里"未闭合项"要敢留白:真源查不到就写"查不到 + 已做的否定性检验 + 为什么不阻塞发布",
    不要编一个看起来对的答案填上。

  24. ★★★ 剪旁白:「旁白删 N 秒」必须「画面也删 N 秒」,且两边都要落在整帧上。
    只删旁白 = 其后旁白整体提前 N 秒而画面不动 → 从刀口起旁白讲在画面前面,越到后面错越多。

    • 算法:N 秒 × FPS 必须是整数帧。本次 7.000 s × 30 = 210 帧
      → 删 3 镜 × 70 帧/镜(s176+s177+s178)恰好 210 帧,音画各删 210 帧,差 0 帧。
    • 落点:切点必须选在实测数字静音或 −55 dB 以下(atrim + 两端 20 ms afade)。
      先跑 10 ms 分辨率电平表找到静音窗,再定刀口 —— 不要按 ASR 的时间戳切
      (ASR 会把前后静音算进时长,本次差点据此切掉 4.34 s,实测重复句只有 1.55 s)。
    • 留气口:切完要保证「上一句收尾 → 下一句起音」仍有 ≈0.5 s 以上呼吸,否则听感是硬接。
    • 脚本要自证:abs((B-A) - 秒数/FPS) &lt;= 1e-6 不过就拒绝执行(见本例 work/apply_v4.py)。
    • 回退:动手前把 成片 / 旁白 / EDL json / srt 四件归拢到 _废弃/对比_改前/(用 mv,不用 rm)。
  25. ★★★ 做电平/内容扫描,绝不能带关键字过滤 —— 滤掉的正是你要找的数据。
    真实事故:查「464.7–468.8 s 之间到底有没有人声」,脚本里写了「只打印含『乾坤柱』的帧」。
    语音帧频谱均衡、不集中在任何单频,被关键字条件整片滤掉 → 读数像"这段几乎没人声"。

    • 重跑:不设任何过滤,只打原始 dB 值 → 立刻看到「乾坤柱」在 464.1–464.8、「南天一柱」在 465.3–466.15。
    • 规矩:扫描 = 只收敛输出格式,不收敛数据本身。 要筛,也在拿到全量读数之后另写一步筛。
    • 同类:判断素材是否存在用 os.listdir + 三重限定(夹+文件名+时间窗),别靠文件名索引。
  26. ★★ 改字幕必须整串精确匹配(txt == 目标),子串匹配会连坐。
    真实事故:any(k in txt for k in (&quot;乾坤柱&quot;,)) 本意只想改第 120 条,
    结果同时命中「那座山现在叫乾坤柱也叫南天一柱」和「本地人叫它乾坤柱」
    → 三条全被改成 464.12 → 466.72,其中两条变成 2.6 s 的非法条目。

    • 同理:"防重叠"的守卫要判真重叠(t0 &lt; prev_end - 0.001),
      不能写成 t0 = max(t0, prev_end + 0.03) —— 后者会把全表每条都推后 0.05 s。
    • 改完必须 diff:条数不变、只有预期那几条动、末条仍在片长内、无负时长。
  27. ★★ 改引擎参数必须 bump 版本串,否则指纹不变 → 全镜 skip → 成片里装的是旧画面。
    KB_VER(如 mv3-wide-20260925 → mv4-chain-20260925)在指纹串首。
    数字变了版本不变 = 一条指纹都不变 = 渲染全 skip,而时长、帧数、所有体检全绿。

    • 这是「间接引用」类静默失败的第三次(前两次见 23 / 24)。
    • 规矩:凡改动会进指纹的东西(运镜表、基准 zoom、映射表、配对表),当次就 bump。
  28. ★★★ 函数式运镜逃出了「名字指纹」—— 间接引用第四次咬人。
    为解决「相邻两镜同图 → 运镜弹回」(14 处 / 76.3 s / 13.6%),后一镜的运镜必须
    从上一镜的终点出发,这无法用一个固定名字表达 → render() 改为接受 callable。

    • 但指纹原先是 move 名字:连镜后仍报 &quot;panL&quot; → 改了配对数指纹不变 → 静默复用旧文件。
    • 修法:指纹里加连镜标记 #c&lt;上游镜号&gt;
      → f&quot;{KB_VER}|{maphash}|{src}|{ph}|{fp}|{move_of(s)}|#c{ch}|{dur:.3f}&quot;。
    • 连镜的两条实测教训:
      (a) 方向要比较「A 自己的首末帧 cy」,不能按「cy 落在上半还是下半」判断 ——
      后者会让 s142 把 s141 倒着扫回来(实测 0.701→0.188),牌子上的字刚读完又倒放一遍;
      (b) 竖图扫到边就停(余量 < 0.05 才掉头),横图顺势拉回全景收(顺带补环境交代)。
    • 验收方式:写成像素事实,不是看参数 —— 逐接缝解码(320×180 灰度、裁掉 5% 边和下方 25% 字幕带)
      算平均帧差:改前 51.71 → 改后 0.45,可见跳变 14/14 → 0/14(最坏 s180→s181:131.25 → 0.40)。
    • ⚠️ 对比"改前"要用旧片本身的时轴(本次旧片长 7 s,读改前帧要 +7.000 s 偏移)。
  29. ★★ 字体路径必须先验存在;「降级成功」比直接报错更坏。
    真实事故:对比图脚本写 PingFang.ttc,本机根本没有这个文件
    → ImageFont.load_default() 静默兜底成点阵字体,一个汉字都渲染不出
    → 整批对比图标签全是豆腐块,而脚本打印的是"成功"。

    • 修法:① 换成实测存在的 STHeiti Medium.ttc;
      ② 字体加载函数找不到就 raise,禁止 any fallback。
    • 通用规矩:凡"找不到就降级"的写法,都要问一句「降级后的产物还能用吗」。
      不能用的,就该抛错。(同一类:素材脚本用错 python → Pillow 不在 → 不报错、只是 EXIF 全空。)
    • 第二个实例:try/except 吞掉解析异常 → 报出一个"全部为 0"的假结论。
      对账 03_render/ 时用 -of default=nw=1 读帧数,它打的是 nb_read_frames=116 而不是 116,
      int() 抛 ValueError 被 except 吞成 n=0
      → 输出「210 个文件全部解不出帧」(全部!——全 0 这种整齐的读数本身就是异常信号)。
      修法:正则抠 nb_read_frames=(\d+),抠不到就 raise。
      > 识别特征:当一批数据的读数"整齐地全军覆没"时,先怀疑测量工具,不要怀疑数据。
    • 第三个实例(同一处的镜像坑):换成 -of default=nw=1:nk=1 后,
      输出只剩裸数字 60、不带 nb_read_frames= 前缀,
      抠前缀的正则又抠不到。两个 -of 变体的输出格式不一样,别混用同一套正则:
      nw=1 → key=value;nw=1:nk=1 → 只有 value。
  30. ★★ alimiter 是「采样峰值」限制器,不做真峰值 —— 别拿它的 dBFS 当 dBTP。
    真实事故:设 limit=0.841(−1.5 dBFS),实测真峰值 −1.3 dBTP —— 超线了。

    • 修法:按实测 TP 反推,目标定 −1.8 dBTP(给 AAC 抬峰留余量)。
      本次链:母版 −1.80 dBTP → AAC 抬 0.3 dB → 容器 −1.5 dBTP。
    • 必须以落盘文件实测为准(loudnorm 与 ebur128 两个口径都读一遍),
      混音台上的数字不算数。
    • ⚠️ TP 达标 ≠ 有余量:本次正好压在 −1.5、余量 0;此时响度想从 −14.5 抬到 −14.0
      就必须额外削峰
      ,不能直接加增益。
    • 另两条遗留口径:平台只往下压不往上抬 → 响度要正好 −14.0;
      acompressor 必须 detection=peak + 显式 makeup,alimiter 的 attack 必须 0.1
      (默认 5 ms 会削顶穿透)。
  31. ★★★ 判据改了,必须【全量重跑】,不能只作用于增量。(本项目最贵的一条)
    真实事故:定「不选索道」的判据时,白名单注释里逐条列出了排除的编号
    (c002 c003 c047 c059 c067 c170 c173)—— 但那条判据只用在新换进来的图上,
    没有回溯已经躺在片子里面的镜头。结果这 6 个编号里的 6 镜
    (s002 吊椅开场、s063 吊椅上坐两个人、s072 缆索横穿、s198 索道立柱、s199/s202 吊厢)
    从第一版就在片里,四轮复核一次都没被这条判据碰过。

    • 代价:一条「有可辨识路人」的镜头(s063)带进了成片,而它旁边就是肖像权红线。
    • 更坏的是它的形态:新增的图干干净净,检查新增的每一项都过 ——
      判据生效的证据充分,所以没人怀疑存量。
    • 规矩:① 判据落成可执行脚本(如 fix_cable.py),每次改判据对全片跑一遍;
      ② 判据写进白名单注释不算落地,注释不会自己执行;
      ③ 交付前肉眼过一遍全片联系表(storyboard.py → 6 张 320px 表,逐格看)。
      > 识别特征:一条判据的"通过率"是 100% 时,先问它是不是根本没被应用到存量上。
  32. ★★★ 真峰值必须按【最终容器】反推,不能按 WAV 达标就收工。
    第 34 条说过「AAC 会抬峰」,但把余量定成「WAV ≤ −1.8 dBTP」就停了
    → 本片实测 WAV −1.80 → 容器 −1.40 dBTP,超线 0.1 dB(抬了 0.4 不是 0.3)。

    • 标定方法(work/tune_peak.py):对候选的 (增益, alimiter limit) 组合,
      各做一次真机 AAC 编码再量,别用经验值猜余量。
    • 本次标定:AAC 对积分响度抬 0(−14.5 → −14.5),只抬真峰值 +0.4 dB。
      → 所以「补增益」和「削峰」是两个独立旋钮:
      想提响度只能靠限峰,不能靠加增益。
    • ⚠️ 第 34 条里「AAC 抬 0.3 dB」这个数不要复用 —— 抬峰量随节目素材变,
      每次都要用本片的素材真机标定。
  33. ★ zsh 里 $VAR: 会被当成路径修饰符展开 —— 参数里紧跟冒号必须写 ${VAR}。
    真实事故:循环写 -af &quot;alimiter=limit=$L:attack=0.1...&quot;,
    zsh 把 $L:a 解读成 :a 修饰符(转绝对路径)
    → 参数变成 limit=/Users/.../0.794ttack=0.1 → ffmpeg 报错。

    • 修法:一律 ${L};凡是变量后面紧跟 : 的,全加花括号。
    • 同批踩到的另一个 shell 坑:for 里用 set -- $w 拆两个变量会坏掉,
      量测循环一律用 Python 写,不写 shell 循环。
  34. ★★ 量「竖向运镜」必须按【画面高度占比】,不能按源图坐标比例。
    真实事故:我报「竖扫族 5.7%、11 镜 / 31.3 s」→ 换口径重算是 14.1%、29 镜 / 77.9 s,四个数全错。
    根因:kb.MOVES 里的 Δcy 是源图比例;而竖图放进 16:9 时,
    窗口只占源高约 33%(横图占 ~55%)→ 同样的 Δcy,竖图的屏幕位移是横图的 1.7 倍。
    观众感觉到的是屏幕上的位移。

    • 正确公式:位移占屏高比 = |Δcy| × 工作图高 ÷ (1080 / zoom);
      工作分辨率横 1920、竖 3200(见 kb.py 开头)。
    • 已固化成脚本:scripts/vertical_audit.py(逐镜读真实像素判横竖、按屏高比重算、输出分段表)
      → 以后一律跑它,不要手算、不要按运镜名字分类。
    • ⚠️ 按运镜名字分类会漏:diagLR/diagRL 名字像对角,其实 Δcy=0.12(带竖向分量)。
  35. ★★ 重跑 mk_edit.py 会按 shotlist 覆盖 mv —— 上一轮改过的运镜会静默回退。
    真实事故:上一轮报告「竖图改大部分静止、竖扫族降到 6.5%」,
    实际现在的片子里 29 镜竖图仍用 tiltU/tiltD/inU/inD,屏幕位移 0.46–0.54 个画面高。
    快照对比证明:第 2 轮把竖图 83 → 31 镜(那是换图,不是换运镜),
    后来重生成时 mv 被 shotlist 覆盖回去了。

    • 判据:凡是"重生成 edit.json"之后,必须重跑 vertical_audit.py 等体检脚本,
      不许继承上一轮的结论。
      时长、帧数、镜数全对得上 —— 这类回退不报错。
  36. ★★ 任何"降档/预览/轻量"版本,音轨必须 -c:a copy,且要单独测真峰值。
    真实事故:720p 预览版为压体积把音频重编 128k AAC →
    容器真峰值 −1.7 → +1.1 dBTP(抬 2.8 dB,冲破 0)。
    改回 -c:a copy 后与母版逐比特一致(−1.7 dBTP)。

    • 判据:交付物不分"母版/预览",每一件都要单独测 TP。
      (与第 33/34 条同源:AAC 抬峰、TP 以落盘文件为准。)
    • 代价参考:720p CRF26 视频 ~78 MiB + copy 音轨 ~17 MiB = 95.7 MiB(母版 486 MiB 的 1/5.1)。
  37. ★★ 客户端"报错提示"先去源码里搜原文,按 i18n 键定位,比猜快十倍。
    真实场景:用户报「该视频编码格式浏览器不支持在线预览」。
    在 WorkBuddy.app/Contents/Resources/app.asar 里搜这句话 →
    命中 mediaPreview.videoDecodeError,由 &lt;video&gt; 的 onError 触发。
    兄弟键文案都不同,可据此排除整条路:

    • videoLoadTimeout="加载超时"(15 s 元数据超时)→ 用户没看到 → 不是超时
    • unsupportedVideoFormat="{ext} 不支持"→ .mp4 在白名单 CHROMIUM_PLAYABLE_VIDEO_EXTS 里 → 不是格式
    • fileTooLarge="文件过大" → 文案对不上 → 我上一轮猜的"体积说"当场被否
    • 结论:文件本身没问题;失败在"把文件喂给 &lt;video&gt;"那一步
      (源码只有两条路:URL.createObjectURL(file) 内存 Blob / 直接喂 URI)。
    • 可复用做法:python 读 asar → re.finditer 找字节偏移 → 打印前后各 2–3 KB 上下文。
  38. ★ 验证"这个视频浏览器能不能播",要用真实内核+支持 Range 的服务器。

    • python -m http.server 不支持 Range → 会让 seek 失败,把服务器问题误判成编码问题。
      改用 scripts/range_server.py(60 行,带 206 Partial Content)。
    • 用 scripts/preview_probe.js(playwright + channel: &quot;chrome&quot; 走真 Chrome 内核)读
      video.error.code/message、readyState、videoWidth,并且真的 play() 一下。
    • 判据:readyState === 4 且 video.error === null = 文件没问题。
      (实测:486 MB 与 595 MB 的片子都是 4/null → 体积与编码都不是原因。)
  39. ★★★ 01_source/photo/ 一律放「软链」,绝不放实拷贝。(2026-09-25 实测纠正)
    早期项目(凤凰、西塘)把素材盘原图整目录拷进项目 → 01_source/photo/ 吃 8.1 G + 1.6 G;
    后期项目(镇江 02_work/build_workcache.py)才是正解:
    ① 01_source/photo/&lt;编号&gt;.jpg = 软链到素材盘原件;② 02_work/w/&lt;编号&gt;.jpg = 工作尺寸缓存,
    之后渲染只读内置盘 → 素材盘每个文件只读一次。

    • 为什么必须软链而不是实拷贝:省的空间完全一样(软链 0 字节),
      但重跑时立即可用(render.py 按 01_source/photo/&lt;编号&gt;.jpg 找图,软链自动落到原件);
      实拷贝则是白白 duplicate 一份 10 G —— 成片已出、备份已在素材盘时,实拷贝没有任何价值。
    • 转换前必须三项全过(缺一不可):
      ① 映射清单在(07_work/照片池清单.csv:编号/拍摄时间/原文件名/来源/源路径);
      ② 按原文件名在素材盘当前目录全量命中(本次凤凰 834/834、西塘 251/251);
      ③ 字节数全量一致 + MD5 抽检相同(不能只看大小)。
    • 执行姿势:实体文件 shutil.move 进废纸篓(可还原)→ os.symlink → 双向校验
      (islink 且 exists 且 size 一致);任一异常回滚。
      本次 1117 项 / 10.10 GB,成功 1068 / 失败 0,转换后死链 0,cv2.imread 实测可读。
    • ⚠️ 软链的代价:素材盘未挂载时是死链。但源照片本来就在盘上,
      盘不插整个流程都跑不了 → 不额外增加风险。转换后务必测一次 cv2.imread,
      别只看 ls 显示正常。
    • ★ 照片池清单.csv 里的「源路径」会过期 —— 素材盘加数字前缀后旧路径全失效
      (景点/ → 10_景点/、西塘古镇挑选 → 西塘古镇挑选_2010)。
      定位一律按「原文件名 + 当前源目录」,不要信清单里存的路径。
    • ★ 这类清单 CSV 是 UTF-8 带 BOM:csv.DictReader 必须 encoding=&quot;utf-8-sig&quot;,
      否则首列名变成 \ufeff编号 → KeyError: &#x27;编号&#x27;(本次连踩两次)。
  40. ★★★ 实拍有缺口时,用「贴片」补,绝不用别的实拍顶替。(2026-09-26 寒山寺案)

    • 贴片 = 公版古画/老照片/自制资料卡(引文/日期/铭文),按「动态 PPT」式插入:
      宣纸底、自画面一角浮起带投影、缓推 2.5–4 s、人名条左下角小字、淡出叠化 0.4 s。
    • 三条素材纪律:① 只认 PD / CC0——CC BY-SA 有传染性(用了就得整片按 CC BY-SA 发布,
      对要收益的账号不可接受),一律跳过;② 不用 AI 生图(中文必乱码,本机实测传统题材失败率高);
      ③ 人名条用 PIL 后期叠,不许 AI 生成带字图片。
    • ★ 红线:拍不到的东西,不许拿相近的实拍冒充。 例:本片没拍到枫桥,
      只能用山塘街/虎丘的照片顶上——不做,那叫用画面撒谎。宁缺勿假。
  41. ★★★ 拍不到的,用「记录」代替「画面」——这往往比画面更硬。

    • 寒山寺元末焚寺案:没有火的画面。改用「记载六个字(《百城烟水》『元季,寺、塔俱毁』)
      + 人证两首诗(高启《赋得寒山寺送别》『塔中灯照远僧归』/《归吴至枫桥》『不见青山旧塔微』,
      自注『旧有塔,今废』)」。
    • 妙处在:诗人自己从没说过「寺被烧了」,他照常写景。是两首诗排在一起才说出了那场火。
      → 与全片论点同构(实物没了、文字还在)。这不是退让,是把限制变成方法。
    • ★ 具体手法(可复用):两张诗卡并置,中间裂开一道黑缝——那道缝就是那场火。
      旁白说「火就在这两首诗中间」的同一秒裂开。文字/画面/声音三件事咬合在同一秒。
    • ★ 声音:钟声 持续 → 失真(低通缓慢扫频 8k→1.2k)→ 回升 → 硬切(0.05 s 急停,不是渐弱)
      → 全静。观众听见钟声「断了」,比看见火烧更有力。 火只以「远处一声」出现一次。
    • ★ 全片只此一处结构性断裂/硬切,别处不许重复,否则失分量。
  42. ★★ 没有可靠画像的历史人物,用「他的著作/他的碑/他的拓本」代替——不要用一张想象出来的脸。

    • 判据:明清人凭想象画的"某某像"=造假。张继(约 715–779)没有可靠的传世肖像。
    • 替换:张继 → 《枫桥夜泊》诗碑;范成大(考证「定夜钟」)→ 《吴郡志》考点卡;王珪 → 考点卡。
    • ★ 顺带的好处:若结尾有回指(本片:「你是去见那二十八个字」),
      提前让观众见过那件实物,结尾这句话才有回响 —— 首尾闭环多收一层。
  43. ★ 取公版素材(Wikimedia)不用 API 也能下 —— 见
    ~/.workbuddy/memory/工具手册_卡顿排查与记忆瘦身.md §十。
    要点:API 全被墙时,原图路径是纯函数(/commons/&lt;md5[:1]&gt;/&lt;md5[:2]&gt;/&lt;文件名&gt;),
    知道文件名就能直连;并发会触发 429,要单线程+长退避+Range 断点续传。


11.1 无旁白片(纯配乐相册)复用本链 —— 2026-10-01 闲鱼样片实测新增

做「商品样片 / 客户交付的相册视频」时没有旁白,但本链是旁白驱动的。三条硬骨头与解法:

  1. ★★★ render.py --stage shots 是按「镜号」找 plate 的,不是按素材名。
    它执行的是 -i _plate/s001.jpg。若你把 plate 命名成 01_w0001.jpg,
    21 个镜头会全挂,报 Error opening input file _plate/s001.jpg,
    而 _render.log 是空的(日志里没写步骤名),容易误判成 ffmpeg 坏了。
    ⇒ 铁律:plate 一律命名 s%03d.jpg(与镜号一致),「素材」列只用来记来源。

  2. ★★★ cache_photos.py 产出的缓存图「不保留 EXIF」(实测 exif keys = [],
    即使原图有完整的 SubIFD)。想拿拍摄时间必须回原图读,且用 get_ifd(0x8769)。
    另外 scripts/quality.py 需要照片池 CSV 里必须有「拍摄时间」列,
    否则 KeyError: &#x27;拍摄时间&#x27; 直接崩。

  3. ★★★ 无旁白片的正解 = 喂一条「等长静音 wav」当 --voice,不改通用脚本。
    render.py 的 assemble 里 inputs = [&#x27;-i&#x27;, vis, &#x27;-i&#x27;, a.voice] 没有条件判断,
    --voice 空会崩。但静音走旁白链后仍是静音 ⇒ sidechain 不触发 ⇒
    配乐原样进 amix ⇒ loudnorm 照样归一化。
    ⇒ 这样零风险,不会波及博物馆系列。
    bash ffmpeg -f lavfi -i anullsrc=r=48000:cl=stereo -t &lt;片长&gt; -c:a pcm_s16le voice_silence.wav
    ★ 静音时长必须 = 视频时长(amix=duration=first 以它为准)。

  4. ★★★ 出片音频务必走 finalize.py,不要只用 render.py 的单遍 loudnorm。
    同一条片实测对照:

    走法 综合响度 I 真峰 TP
    render.py assemble 内建 loudnorm(单遍) −13.2 LUFS(偏 0.8) −1.4 dBFS(超红线 ≤−1.5)
    finalize.py(两遍 loudnorm + 192k/48k 双闸) −14.0 LUFS ✅ −2.6 dBFS ✅

    ⇒ 单遍 loudnorm 出来「看着差不多」,但 TP 会顶破红线。
    finalize.py 的用法(无音效时):
    bash ffmpeg -i 成片.mp4 -c:v copy -an 画面_无音轨.mp4 # 先剥音轨 $PY $KIT/finalize.py --video 画面_无音轨.mp4 \ --voice voice_silence.wav --music 配乐.wav \ --music-gain -6 --total &lt;片长&gt; --out 成片_v2.mp4
    ★ --music 给了就必须显式给 --music-gain(脚本会直接报错拦下)。
    ★ 出片后等 +faststart 写完 moov 再复测,否则测值全废。

  5. ★★ 「目录名不可信」再添一例(第 N 次) —— 一批叫
    笑笑结婚 沈阳_2019 的 599 张,抽 24 帧看是沈阳故宫/博物馆/街景,不是婚礼。
    ⇒ 任何目录在动手前先 make_contact_sheet.py 抽帧,30 秒的事。

  6. ★★ 沙箱会拦「inline 命令里带外部盘中文路径」的 python ——
    报 sandbox-center cmd decisionRecord missing actual resource subject(不是权限提示,像内部错误)。
    ⇒ 一律写成 .py 脚本文件再跑,别用 python -c &quot;...&quot; 拼。

  7. ★ 选片「按拍摄时间均匀抽」比「按分数取 topN」更适合叙事类。
    婚礼/寿宴这类素材,整批的拍摄时间本身就是叙事顺序(仪式→敬酒)。
    实测:358 张 → 过 detail ≥ 0.10 闸 338 张 → 按时间均匀抽 30 张做联系表 →
    肉眼定 20 张,覆盖度远好于按分数取前 20。

11.2 「人头出现一半」的根治 —— 2026-10-01 婚礼样片返工实测新增

> 背景:v1 出片后我自评「画面验收通过 ✅ 无裁头」(抽 16 帧,每 6 秒 1 帧),
> 被用户在 1:18 一句话推翻。以下是根治方案,全部实测过。
> 工具落在 闲鱼样片_20261001/02_work/cropkit.py(人脸感知裁切核心件,可直接复用)。

  1. ★★★ 切头验收必须交给程序,肉眼抽样必漏。
    6 秒抽 1 帧漏掉了持续 4 秒的切头镜 —— 抽样再密也是抽样。
    正解:全片 0.5 秒粒度扫人脸,判「主体脸」是否压画面边缘。
    实测 96 s / 192 帧只要 40 秒跑完。
    bash $PY cutface_check2.py 成片.mp4 0.5 # 全片 0.5s 粒度;区分上下左右四条边 $PY audit_sheets.py 成片.mp4 2 6 # 肉眼联络表只做构图/色调复核
    ★ 必须区分「哪条边」:脸压上边 = 真切头(严重);
    压左/右下边 = 身体出画、头是完整的(远景路人属正常,别当缺陷改)。

  2. ★★★ Ken Burns 运镜会二次裁切 —— 只修裁切不改运镜 = 白修。
    render.py 的 in/out 最高 z=1.25 ⇒ 只显示底图中央 80%,四边各吃 10%;
    left/right 是 z=1.12(吃 5.4%)再叠 ±110 px 位移(≈4.6%)。
    ⇒ 底图裁得再好,运镜一动又切。
    正解:按运镜最大裁切反推边距,人脸占窗超限就退成 static。
    改在自己的 make_plates.py 里,不要动 scripts/:
    python ZOOM_LIMIT = 0.78 # z=1.25 → 四边各吃 10% ⇒ 占窗 ≤78% 才留得住 PAN_LIMIT = 0.66 # z=1.12 吃 5.4% + 位移 ±110px ≈ 4.6% def pick_move(move, span): if move in (&#x27;left&#x27;, &#x27;right&#x27;) and span &gt; PAN_LIMIT: return &#x27;static&#x27; if move not in (&#x27;static&#x27;,) and span &gt; ZOOM_LIMIT: return &#x27;static&#x27; return move

  3. ★★★ 取窗必须「居中」,不能把人脸顶到窗口一边。
    旧写法 want = lo - pad 把人脸顶在窗口起始处、只留 5% 边距,余量全浪费在另一侧 ——
    运镜一放大就切。正解:
    python want = (lo + hi) / 2.0 - span / 2.0 # 人脸区间居中 → 两侧等余量 want = max(0.0, min(want, total - span)) # 夹紧

  4. ★★★ 竖拍照片改 duo 双联,是本片最大的观感提升。
    竖构图进 16:9 只能留 37.5% 高度,下边缘必然切人(358 张里 59 张竖拍);
    两张竖拍并排拼 2400×1350 ⇒ 每张只损失 25% 高度,一根头发都不切,
    版式还自带「相册感」。婚礼/寿宴的竖拍人像(头纱、相拥、队列)优先走 duo。
    python cropkit.duo([&#x27;w0086.jpg&#x27;, &#x27;w0107.jpg&#x27;], dst, W=2400, H=1350, gap=14)
    另备 cropkit.blur():单张竖拍全幅居中 + 高斯模糊铺底,完全不裁。

  5. ★★ 「主体脸」过滤 —— 不加会误伤,加了要防误滤。
    两类假脸会把占窗撑爆,逼着镜头降级:请柬/相框上印的人像(w0012 被判占窗 83%)、
    满堂宾客的 31 张小脸(w0286 被撑到 78%)。
    门槛取「3% 图高」与「最大脸的 45%」两者较大值:
    python thr = max(0.030 * img_h, 0.45 * max_face_h)
    ★ 为什么必须带相对项:远景里新人脸也小,纯绝对阈值会把他们滤掉 —— 那才是真主体。

  6. ★★ YuNet 在白紫色花束/烛台上会稳定误报人脸。
    实测 3 处(脸高 6.9% / 10% / 18% / 21.6%,全是花艺镜)。
    ⇒ 看到「脸高 >15% 却出现在花艺镜」先怀疑假阳性,必须放大看图定案,别直接改片。

  7. ★★ 人脸检测件怎么拿(cv2 5.x 已删掉 Haar 级联 xml)
    bash mkdir -p &lt;工作目录&gt;/models &amp;&amp; cd $_ curl -sL -o yu.onnx https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx
    python det = cv2.FaceDetectorYN.create(MODEL, &#x27;&#x27;, (320, 320), 0.55, 0.3, 5000) det.setInputSize((ww, hh)); _, faces = det.detect(im2) # faces[:, :4] = x,y,w,h
    ★ 人脸框只覆盖眉到下巴,不含头发 ⇒ 必须外扩(上 45% / 左右 18%),否则仍会切头顶。

  8. ★ 选片用「实裁预览表」而不是原始缩略图。
    把每张照片按最终裁切结果缩成 16:9 拼表,看到的就是最终画面 ——
    避免「选的时候好看、裁完才发现切头」。build_previews.py 直接出。

  9. ★ 镜序要照顾「运镜降级」:插入一个可自由运镜的镜隔开。
    若相邻两镜都因人脸占窗过大而只能 static,会撞出相邻同运镜。
    实测:把「花艺暖光」(占窗 0.00,可任意运镜)插在
    「请柬桌花」(0.83) 与「宴会厅」(0.94) 之间即解。

11.3 寿宴样片实测新增(2026-10-01)

  1. ★★★ 增量渲染必须比 mtime,只判「文件在不在」= 最危险的一类 bug。
    render.py --stage shots 原本的跳过条件是「片段存在且 >20KB」。
    后果:改了 plate 再跑会静默跳过去(打印「待渲染 0」),最后拿旧片段拼成片
    —— 不报错、不崩溃,但画面是旧的。你以为重渲了,其实没有。
    python def _stale(clip_path, plate_path): if not os.path.exists(clip_path) or os.path.getsize(clip_path) &lt;= 20000: return True if plate_path and os.path.exists(plate_path): return os.path.getmtime(plate_path) &gt; os.path.getmtime(clip_path) return False
    ★ 通用原则:任何「产物 ← 输入」的缓存判断,都要比输入 mtime,不能只看产物在不在。

  2. ★★ 切头放大图必须按「危险度」排序,不能按时间取前 N。
    cutface_check2.py 的 zoom_sheet 原本 items[:12]。
    后果:一镜(九人合影)连报 12 帧,就把后面所有报警全挤掉了
    —— t=84/85s 的两处从头到尾没被放大过 = 等于没检查。
    python items = sorted(items, key=lambda r: (-r[2][3], r[2][0]))[:12] # 脸高降序,再按边距升序
    ★ 凡是「只看前 N 个」的输出,都要问一句:前 N 个是按什么排的?

  3. ★★★ 「运镜只会把脸放大,不可能放大出更小的脸」—— 鉴别假阳性的铁律。
    成片 t=84/85s 报警脸高 13.0% / 10.5%,而该镜 plate 里最小的脸也有 15.0%。
    in/out 是 z 从 1.00 推到 1.14,脸只能变大。⇒ 那两张「脸」必然是 YuNet 误检。
    再用局部重检 + 极限放大确认:该区实际唯一的脸距右边 12.6%,根本不贴边。
    ⇒ 拿「plate 的脸高分布」当参照系,比放大图更快定案。

  4. ★★★ 切脸先辨「裁切造成」还是「原图就切」—— 三处对照法。
    分别在成片帧 / plate / 原图上量同一张脸的边距:
    | 结果 | 结论 | 修法 |
    |---|---|---|
    | 三处一致 | 原图拍摄时就贴边 | 改裁切没用,只能换素材 |
    | 原图够、plate 紧 | 裁切环节没留够 | 改 fit_window 边距/取窗 |
    | plate 够、成片紧 | Ken Burns 二次裁切 | 降运镜或退 static |
    实测(b0186):L 2.3% / 2.3% / 2.2% ⇒ 三处一致 ⇒ 换素材。
    ★ 横拍 4:3 → 16:9 只裁上下,横向根本没裁,横向贴边一定是原图的问题。

  5. ★★ 连拍组里换镜:按「四周留白」重扫全组,别只试相邻编号。
    九人合影 b0170–b0200 是人群移动中的连拍,边距天差地别:
    b0182 的 L=8.4% 而 b0194 的 R=−0.9%(已出画)、b0188 的 L=0.4%。
    python L = min(b[0] for b in heads); T = min(b[1] for b in heads) R = min(1 - b[0] - b[2] for b in heads); B = min(1 - b[1] - b[3] for b in heads)
    判据:四边都要 > 该片运镜限值(scripts/render.py 6.2%;z=1.25 的片要 10%)。

  6. ★★ 异色场景段(KTV 紫灯 / 舞台彩灯)用「饱和度 + 色相占比」定位边界。
    不要靠肉眼一张张看。实测寿宴尾段:
    python hsv = cv2.cvtColor(cv2.resize(img, (120, 90)), cv2.COLOR_BGR2HSV) v = hsv[:, :, 2].mean() # 亮度 pur = ((hue &gt; 125) &amp; (hue &lt; 165) &amp; (hsv[:, :, 1] &gt; 90)).mean() * 100 # 紫/品红占比
    分界比亮度清晰得多:KTV 段饱和 218–238 / 紫占比 84–93%;正常包间 饱和 74–89 / 紫 ≈0。
    ★ 异色镜剪进去会「色调跳变」,样片和商品案例一律换掉(不要试图调色救)。

  7. ★★★ --music-gain 用错默认值会逼 loudnorm 退化成 Dynamic 模式。
    现象:finalize pass1 落 −39.7 LUFS(正常应为 −19.7),
    差 25.7 dB ⇒ loudnorm 无法用 linear 纯增益,报 Normalization Type: Dynamic
    —— 那是动态压缩,不是纯增益,音质被改。
    ★ 本系列正确值 = -6(初混落 −19.7 LUFS)。
    验证法(最省事):跑完 finalize 看 pass1 实测 那行,落 {&#x27;I&#x27;: &#x27;-19.7&#x27;} 即正确;
    落到 −39.x 就是增益用错了,立刻重做,别接受结果。
    ★ 修法是改初混增益,不是让它去补大增益。

  8. ★ 镜表备注必须核对画面 —— 写错的 note 会误导后面的判断。
    本片 note 把 b0133 记成「深情拥抱」、b0203 记成「拥抱」,
    实际前者是两人搭肩合影、后者是女士举花束。
    后果:选尾声镜时差点因「已有两个拥抱镜」而排除掉真正合适的 b0236。
    ⇒ 定镜时把 note 写成「画面里实际在发生什么」,不要写成「我想表达什么」。

  9. ★★★ 别用 ffmpeg -ac 1 下混,它带增益会内削顶。(2026-10-01 东湖实测)
    源 mp3 峰值 −0.27 dBFS,ffmpeg -i in.mp3 -ac 1 ... 出来 +2.52 dBFS,
    2,826 个样本内削顶 —— ffmpeg 的 downmix 矩阵不是简单平均,带增益。
    python # ❌ 错:会内削顶,且后续所有电平判据全废 # ffmpeg -i in.mp3 -ac 1 -ar 48000 -f f32le - # ✅ 对:先 -ac 2 取回各声道,再 numpy 求平均 raw = subprocess.run([&#x27;ffmpeg&#x27;,&#x27;-hide_banner&#x27;,&#x27;-v&#x27;,&#x27;error&#x27;,&#x27;-i&#x27;,path, &#x27;-ac&#x27;,&#x27;2&#x27;,&#x27;-ar&#x27;,str(sr),&#x27;-f&#x27;,&#x27;f32le&#x27;,&#x27;-&#x27;], capture_output=True, check=True).stdout y = np.frombuffer(raw, dtype=np.float32).reshape(-1, 2) mono = y.mean(axis=1).astype(np.float32) # 峰值回到 −0.49 dBFS、0 削顶
    为什么危险:削顶是不可逆的,且会让 prosody.py 报"有削顶",
    然后你会去怀疑源文件 —— 实际是自己引入的。

  10. ★★ 本机 ASR 链路已换代:~/.cache/whisper/ 的 .pt 模型已不存在。
    Python 版 openai-whisper 无模型可用;改走 whisper.cpp:

    • whisper-cli(brew,/opt/homebrew/bin/whisper-cli)
    • 模型 ~/.cache/whisper-models/ggml-large-v3-turbo.bin(1.6 GB)★主力
    • M2 Metal 加速,402 s 全片转写只用 41 秒(Python 版要 9 分钟量级,快 13 倍)

    两个必踩的坑:
    - 多文件必须用 positional:whisper-cli -m $M -l zh -np a.wav b.wav c.wav ✅;
    -f a.wav b.wav 会把 b.wav 丢掉(只跑第一个)❌
    - 多文件输出无文件名标记,靠「空行分块」对应回文件 ⇒ 分批跑 + 每批校验块数 == 文件数
    - -ojf 的 JSON 对中文按 BPE 拆成字节碎片(如 &quot;\xe6\x9e&quot;),
    整个文件是非法 UTF-8,json.load 会 UnicodeDecodeError
    ⇒ 用 open(p,&#x27;rb&#x27;).read().decode(&#x27;utf-8&#x27;,&#x27;surrogateescape&#x27;) 读,再按字节拼回字符。

  11. ★★★ shutil.rmtree 撞安全删除守卫会 SIGTERM 整个脚本(exit 137)。
    报错形如 [safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {&quot;count&quot;:145,&quot;threshold&quot;:50},
    阈值 50 个文件。(§3.1 坑 6 已有 blocks/ 100+ 的先例,本节是 145 文件复现。)
    ⇒ 不删目录,同名文件直接覆盖(os.makedirs(tmp, exist_ok=True) + -y)。
    残留的旧文件不影响结果,不要为了"干净"去 rmtree。

  12. ★★ 「整条重转写」比「只验编辑段」更省事也更保险。
    367 s 全片重转写 只要 37 秒(whisper.cpp)。
    只验编辑段的问题:切点找偏了会把邻句咬掉半截,编辑段内看不出来。
    ⇒ 验收一律整条重转写,逐刀核对接缝前后一整句。

  13. ★ 尾部静音会 hallucinate —— 东湖片尾出现
    「请不吝点赞 订阅 转发 打赏支持明镜与点点栏目」。
    判读时直接忽略尾条,不要当成"多念了一句"去剪。

  14. ★★★ whisper 会把「重复内容」抹平 —— 判重复不能看转写。
    东湖原始 32.31–39.71 有三段独立语音(「天下第一山水,」/「天下第一」/「天下第一山水大盆景」),
    但任何窗口长度(5.68 s / 3.11 s)、带不带 prompt,都只读一遍「天下第一山水大盆景」。
    转写看着干净,片子里「天下第一」实际念了两遍,v1 因此漏剪、被用户听出来。
    ⇒ 判「有没有重复」只能看 prosody.py 的能量语音段表:
    短段(0.4–1.3 s)紧接一个明显更长的完整句 = 假开头,命中了哪怕转写通顺也要剪。
    配套:在整条转写里检索关键串,确认出现次数(「天下第一」×1、「我当」只出现在「我当时」里)。

  15. ★★★ 0.85 s 片段的尾部会被语言模型吃掉 —— 短片段转写不可单独下结论。
    东湖 49.11–49.96(0.85 s)整段被读成「这是个字啊」(5 字),把尾部的「我当」两个字盖掉,
    导致我把整段当正句保留 = 漏剪。
    ⇒ 判「这段到底说了哪几个字」的正解:去录音里找同一内容的真参照做声学对比。
    用对数梅尔谱 + DTW 与谱心 / 高频比 / 过零率(东湖实测与真参照拉开 11 倍距离)。
    ❌ 别用 count_syllables.py(已知 9 字检出 13、已知 14 字检出 26,误差 1.44–1.86 倍),
    ❌ 别用谱通量起音检测(0.85 s 只检出 3 个音;已知「我当」只检出 1 个)—— 都只作旁证。

  16. ★★★ 「转写对得上」≠「片子对了」—— 交付前必须自己整条听一遍。
    v1 报告写「7 处全部剪除、验收通过」,实际漏剪 2 处,用户听审才发现。
    上述四项自检(时长/削顶/接缝跳变/整条重转写)全部通过,片子还是错的。
    ⇒ 硬性要求:交付前自己带耳机过一遍(至少每个接缝两侧 ±2 s)。 成本 ≈6 分钟,
    而返工一次 ≈65 分钟(复查 25 + 声学对比 8 + 重剪 6 + 重锚 5 + 沉淀 20)。
    ⇒ 另一条随手的硬验收:拿原始源文件按 cuts.json 重切一遍,与定版 cmp 逐字节比 ——
    一致 ⇒「除这些区间外一个采样都没动」;改版时再与上一版逐样本比,差异应只在新增接缝的 8 ms 淡入淡出窗内。

  17. ★★★★ 「沉浸感做不出来」的根因往往是「音轨被 -ac 1 拍平了」,不是调参不努力。
    东湖 EP01 v1 交付母版实测 L/R 相关 1.000 / S/M −240 dB(零侧声道) —— 素材源(两条 mp3、两条 CC0 wav)
    全是真立体声,全链 -ac 1 把它们下混到正中央一个点。
    ⇒ 交付前必核 ffprobe -select_streams a:0 -show_entries stream=channels 必须 = 2。
    判据:静默窗 L/R 相关应在 0.2–0.5 之间(背景铺开),旁白段应接近 1(人声居中)。
    修完东湖:旁白段 0.983 / 静默窗 0.335,且 LRA 从 2.6 → 5.4(动态被压平同时被治好)。
    ★ 同系列横向对照是最快的自证:博物馆_v1 各集都是 stereo,只有《锦绣山河》EP01 是 mono ⇒ 是回归,不是风格。

  18. ★★★ 连续背景床与一次性音效要分开的 duck 预设 —— 搞混了就是「融合感差」。
    水声/风声用深快 duck ⇒ 说话段被压 6.8 dB、句间又弹回,背景在旁白面前反复进出。
    bash # 连续背景床(水/风/街噪) $PY $KIT/finalize.py ... --duck-preset bed --channels 2 --music-gain -11.6
    bed = sidechaincompress=threshold=0.10:ratio=2.5:attack=120:release=2600 ⇒ 自差 6.8 → 2.2 dB。
    判据看「背景露出」= 空档段 RMS − 说话段 RMS:东湖 v1 = +0.5 dB(旁白一停背景更响 = 喧宾夺主)
    ⇒ 返工后 −10.0 dB。但别杀干净:可闻性(真静默窗中位 ≥ −30 dBFS)在交付母版上复核 −28.4 ✓。

  19. ★★★ 慢包络不要在全采样率上做卷积 —— 先算复杂度。
    np.convolve 用 9 s Hann 窗卷 1790 万样本 = 43.2 万抽头 × 1790 万 = 7.7e12(跑 4 分钟无结果、看着像死机)。
    ⇒ 粗时间网格再插值:配乐曲线 100 Hz(残差 < 0.01 dB)、duck 包络 500 Hz
    (递推非线性、时间常数随上下行切换,无法向量化:全采样率 1790 万次 Python 循环 60–90 s → 3.6 万次 < 1 s)。

  20. ★★★ 三个会让「量化对比」得出反向结论的度量陷阱。

    • 在布尔抽取的样本上做 FFT ⇒ x[mask] 把全片说话段首尾相接成时间不连续序列,求的是不存在信号的谱。
      实测同一路环境声脚本报谱重心 3888 Hz,逐窗实测 856→580 Hz,差 5 倍 ⇒ 改连续窗取中位。
    • 拿成品轨包络最低点当「换曲塌陷」 ⇒ 两版取到了不同乐句,得「新版 9.7 dB > 旧版 8.9 dB」的荒谬结论。
      ⇒ 比较斜坡函数本身:线性 (1−u)²+u² 中心 −3.01 dB;等功率 cos²+sin² ≡ 1 中心 0 dB。
    • mono/stereo 横比电平差 3.01 dB ⇒ 用总功率口径 (a**2).sum(axis=1).mean()
      (ffmpeg 上混是功率保持的,每声道 ×1/√2)。
    • 另:ebur128 的 Summary 是 info 级 ⇒ 加 -v error 会吞掉整段(表格全 ?),
      去掉后又不能全文正则(开头逐帧 I −70 / LRA 0 会先命中)⇒ out[out.rfind(&#x27;Summary:&#x27;):]。
    • 另:highpass=60 之后 S/M 会「看起来变窄」,但侧声道 75.76% 的能量在 0–20 Hz(次声隆隆)
      ⇒ 别把次声隆隆当成声场,高通是净化。
  21. ★★★★ 在「母版」上量单路指标 —— 量不出责任,还会给出反向结论。
    母版级「真静默窗中位」量的是背景合成、音乐占主导 ⇒ 东湖环境声让路从 6.9 dB 修到 21.6 dB,
    母版读数只从 −28.0 动到 −28.9(0.9 dB,方向还相反)。⇒ 背景判据一律回到 stem 分路,
    母版只用于最终响度/真峰复测(判据见 §7.11)。

  22. ★★★★ 换母版 = 档案里所有读数作废,必须在新文件上重测再落笔。
    东湖 v1.2 收尾时发现制作档案「实测数据」三行仍是上一版读数
    (LRA 5.4/TP −1.6/静默窗 −28.4),与实交付(5.5/−1.9/−28.9)不符 ——
    因为只换了音轨、没重跑核验就顺手抄了旧数。
    ⇒ 交付件一旦重出(哪怕只换一条音轨),响度/真峰/LRA/静默窗/立体声相关全部重跑一遍。

  23. ★★★★ 用户报「喷麦/爆音出来了,我录完听是没有的」—— 先证明是不是人声链放大的,大概率不是。
    EP02 实测(检出 717 处候选;判据:分帧 FFT 后 LF 20–200 Hz 与 VOX 200–4 kHz 的功率比,
    高出局部本底 ≥7 dB 且 LF 绝对电平高出本底 ≥5 dB):

    量 结果
    VC 对 LF 的影响 +10.72 dB
    VC 对 VOX 的影响 +11.31 dB
    ⇒ 低频净提升 −0.59 dB(低频反而略被压)
    最响 25 处事件的突出度(LF 高出本底) 原始 21.1 → VC 后 17.9 dB(−3.2 dB)

    ⇒ 人声链没有放大喷麦,反而略降。 病根是整体电平:最小二乘解 成片 ≈ a·旁白
    (音乐只进残差,比直接比 RMS 干净),5 个窗口解得 a 均值 +6.3 dB ——
    原录音日常听感在 −24…−30 dBFS,归一交付到 −14.7 LUFS 后,
    原本沉在听阈以下的唇爆破自然浮出来。
    ★ 结论:这不是「处理坏了」,是「交付电平的必然代价」。
    ⇒ 要消只能专门去喷麦(band-split:低频段单独压缩;或把 highpass 从 70 抬到 90–100 Hz),
    绝不能靠改 acompressor 的 ratio/makeup —— 那会连人声主体一起改,得不偿失。
    ★ 探针脚本:&lt;EP&gt;/07_work/plosive_probe_ep2.py(纯只读,不改任何文件)。

  24. ★★★★ 「片子到底有没有旁白」用互相关判 —— 别用耳朵、更别用总 RMS。
    起因:用户看片后问「没有旁白声音?」。三步定位:
    ① 存在性判据:同一时间窗取「旁白原轨」与「成片」,做归一化互相关(增益不变)。
    成片 ↔ 旁白原轨 +0.80(时延 0 ms)⇒ 在;旧样片 +0.02 ⇒ 不在。
    全片抽 9 点(20/60/150/200/260/300/340/370/400 s)全在 +0.67…+0.81 ⇒ 全程无断。
    ② 总 RMS 不能当判据:有配乐时,「没旁白」的样片比成片只低约 8 dB,听感/数值都分不出来。
    ③ 先怀疑「用户打开的是哪个文件」:同项目 03_render/*_无旁白*.mp4(438.7 s / 282 MB)
    与成片(421.9 s / 292 MB)体积仅差 3%、文件名挨着 ⇒ 必被误开。
    ⇒ 中间产物与成片体积相近时,落地当场改名或移进带警示的子目录(_旧样片_无旁白_请勿打开/),别等出事。
    ④ 另记:_video.mp4 / _sub.mp4 / 画面_含尾板_带字幕.mp4 全都没有音轨(烧字幕前的中间件)。
    ★ 探针脚本:&lt;EP&gt;/07_work/audio_presence_ep2.py。
    ★ 新集复用时:把这两个脚本拷进新集的 07_work/,只改顶部 EP / 输入路径常量即可。

  25. ★★★★ 调色:平台二次转码不会吃掉「清晰度」——别凭直觉劝用户降参数(实测打脸过两次)。
    起因:用户在剪映给 EP02 加 DJI Air 3S D-Log M→Rec.709 LUT 强度25 / 光感15 / 锐化5 / 清晰度80 / 伤感故事滤镜。
    我两次预设都被实测推翻,记下来别再犯:

    • 预设①「log LUT 套非 log 素材会褪色发灰」→ 错。实测饱和度 17.3%→29.7%(+72%)、对比 std 66.7→74.5、天空灰蓝→青蓝。
      该 LUT 是「还原+风格化」二合一,不是纯 log 还原,套普通素材反而是加色。
    • 预设②「清晰度80 加出的高频,平台低码率重编码会丢光,只剩压黑的副作用」→ 错。
      实测(07_work/recode_loss_ep2.py,母版 ↔ 导出稿各 6 点,h264 1080p 2.5 Mbps 重编码):
    P1 黑场 对比 std 饱和 高频能量(Laplacian var)
    母版 A 6.8 65.5 15.7% 738
    调色 B 3.2 75.6 21.9% 1390
    A 转码后 8.0 64.7 15.4% 647
    B 转码后 3.6 74.9 22.1% 1199

    ⇒ 高频增益保留率 97%,压黑也几乎全留。 原因:照片电影几乎无运动,帧间冗余极大,2.5 Mbps 对 1080p 静态画面绰绰有余。
    ⇒ 重编码本身只掉约 13% 高频,而调色增益 +88%,净赚。
    ★ 但对「有运动 / 密集纹理」的集(水乡瓦片、树叶、人流)不成立 ——
    Unsharp-Mask 类清晰度在强边缘两侧出白/黑光晕(halo),静止看不出,一动就闪。
    ⇒ 静态为主:清晰度 ≤80 OK;有明显运动或密集纹理:降到 40–50。
    ★ 三条数值判据(用这个定案,不要用「看着顺眼」):
    ① 黑场 P1 ≥ 6(EP02 压到 3.2、个别帧 2.7 —— 已在暗部并块边缘,下集别再压)
    ⇒ 要加对比,优先加「对比度」少吃暗部,别再加「清晰度」;
    ② 高频增益 ≤ +100%(EP02 +88%,安全);
    ③ 饱和度 +5…+15 pp(EP02 +6.2 pp 正好;> +20 pp 显廉价)。
    ★ 脚本:&lt;EP&gt;/07_work/recode_loss_ep2.py(只读抽帧+重编码,不改交付件)。

  26. ★★★★ 用户红绿色弱 ⇒ 调色一律以「专业标准 + 数值判据」定案,不以他的偏好为准。
    用户原话(2026-10-02 本人授权):
    > 「因为我红绿色弱,所以我看到的不是标准色,所以不能以我的偏好来看片子的调整,
    > 我就是瞎调到我看的舒服些而已,应该以你专业的电影调色为准!」
    ⇒ 他说「我看舒服了」不构成判据,只能当线索(比如他说「天空发灰」,翻译成「饱和度不足」)。
    ⇒ 他说「清晰度/对比度我调了」时,也要复核数值(见第 88 条三条判据)——
    他看不到色偏,但压黑和过锐是他调完也发现不了的。
    ★ 「平台自动审核先看清晰度」这个理解方向对、位置错,要如实纠正:

    • 机器「过审不过审」看的是:分辨率门槛/违规画面/重复度原创度/水印二维码 —— 清晰度不决定过审;
    • 清晰度影响的是「给多少量」:低清晰度被判低质 → 压推荐池分配。
      ⇒ 所以把清晰度做好是加分项,不是保命项;保命的是原创度与合规(见 §1)。
  27. ★★★★★ 「剔除类清单」必须带「状态」列 —— 判定是给某一版镜表做的,镜表一改就过期。
    真实事故(2026-10-05 EP07 大栅栏):黑名单_跑题.csv 里 122 条把整整一批
    2012-11-23 的照片判成「文玩店铺/室内/器物跑题」,依据写着「逐页实看」「批审图人工判定」。
    我照单全收,据此得出「这批没素材可用」。用户直接推翻:
    > 「大栅栏那组图里不是有很多店铺的照片吗?门脸,内联升的布鞋特写那一组」

    实看:那批正是老字号门脸与器物特写(金蟾斋/全聚德/瑞蚨祥/祥義號/王麻子/大碗茶/
    毛笔店/布鞋特写/铁艺牌坊)—— 正是片头段需要的画面。
    那条判定的真实语境是给旧镜表做的(旧镜表里这段要用街景,店铺门脸算跑题)。
    - 根因:判据是有「适用范围」的,而清单把适用范围丢掉了。
    清单只记「编号 → 跑题」,不记「谁判的、为什么、对哪一版有效」⇒ 换了叙事重点后,
    它仍然生效,而且看起来像是权威证据。
    - 修法:黑名单/剔除清单一律加两列,且作废只改状态、不删行:
    编号, 跑题依据, 状态(生效/作废), 作废依据
    读取端 缺 状态 列直接 SystemExit —— 没有状态列就无法区分
    「仍生效」与「已作废」,必然误杀。
    - 判据:任何「这批不能用」的结论,落笔前必须回看那条判定的原始依据是什么、
    它是为哪一版镜表做的。
    我这次是从没回看依据,只信了标签。

  28. ★★★★★ 改「主图资格」必须同步回看它对「辅图池」的连锁影响。
    接上条,同一份黑名单同时卡主图与辅图(EP07 里 make_plates 只在辅图那一处用它)。
    解封前连锁后果:2012 批 108 张被黑名单挡在辅图池外 ⇒ 辅图池只剩 4 张 ⇒
    该批 9 个竖幅三联镜没辅图可用 ⇒ 同一对辅图(IMG_7128+IMG_7129)被 6 个镜复用。

    • 判据:「一图只进一镜」是铁律,复用会被观众一眼看出(「左右那两条怎么又是它」)。
    • 漏斗要按批算,不能只看总数:池 128 → −跑题 108 → −禁列 → −已作主图 → 18。
      「总数够」不代表「这一批够」。
    • 改任何上游资格闸门(黑名单/清晰度阈值/人脸阈值)后,必做一次
      「逐批辅图池余量 vs 该批三联镜需求(镜数×2)」的对账。
  29. ★★★★★ 唯一性断言必须对「全表」下,不能只对「本批」—— 这个坑我踩了第二次。
    EP07 换 23 个镜的素材时,脚本只 assert「这 23 个新素材彼此不重复」,
    漏了「与表里其余 67 个镜不重复」⇒ 结果:

    s0017 ← IMG_9937,而 s0058 早已是 IMG_9937 ⇒ 一图进两镜 s0061 ← IMG_0057,而 s0023 早已是 IMG_0057 ⇒ 一图进两镜

    • 铁律:换素材时最容易撞的是「老素材」,不是「同批新素材」。
    • 修法:不手改 CSV,写一个应用替换映射的工具,5 条硬断言(缺一不可):
      ① 原素材 与镜表现值逐条核对一致(防张冠李戴)
      ② 新素材 必须在候选池里
      ③ 新素材 不在禁列名单里
      ④ 应用后全表唯一性(含未替换的旧行)—— 见上面的写法
      ⑤ 按源图长宽比重算版式(见第 93 条)
      写盘前自动备份原镜表。

    python after = [r for r in rows if r[&#x27;镜号&#x27;] not in {m[&#x27;镜号&#x27;].strip() for m in mp}] after_mat = collections.Counter([r[&#x27;素材&#x27;] for r in after] + newmats) dup = {k: v for k, v in after_mat.items() if v &gt; 1} if dup: # 报出「哪个素材在哪几个镜」才算合格诊断 where = [f&quot;{r[&#x27;镜号&#x27;]}(原留)&quot; for r in after if r[&#x27;素材&#x27;] == k] where += [m[&#x27;镜号&#x27;] + &#x27;(本次新填)&#x27; for m in mp if m[&#x27;新素材&#x27;].strip() == k] raise SystemExit(&#x27;✗✗✗ 替换后全表出现同一素材进多镜 —— 硬停:\n&#x27; + ...)

  30. ★★★★ 镜表的「版式」列是由源图长宽比推导的,换素材后必须重算。
    plan_sheet.py 里是 &#x27;版式&#x27;: &#x27;竖&#x27; if (p.get(&#x27;h&#x27;) or 0) &gt; (p.get(&#x27;w&#x27;) or 0) else &#x27;横&#x27;
    —— 它不是「这一镜打算用横构图还是竖构图」的作者意图,而是源图属性的快照。
    换素材后没重算 ⇒ 实测 7 个镜写着「横」而实际是竖幅
    (s0004/s0008/s0011/s0013/s0015/s0020/s0056)。

    • 为什么它必须正确:渲染端不消费此列(三联判定看原图长宽比),
      但它进档案、是核对判据 ⇒ 下游任何人拿它做判断都会错。
    • 修法:替换工具内建重算;核对表额外打印一行
      「镜表版式 vs 原图长宽比 错配 N 镜」,要求 N=0。
  31. ★★★★★ 人脸闸写死「半分辨率」= 尺度盲区,大图的正脸/侧脸会漏检。
    EP07 的 工具_人脸闸.py 旧实现只有 cv2.imread(fp, cv2.IMREAD_REDUCED_COLOR_2)。
    主图多为 3888×2592 / 5616×3744,缩到半分辨率后正脸/侧脸掉进尺度盲区。
    实证两例,都判「无脸」而人眼一眼可见:

    素材 人眼所见 旧判定
    大栅栏_2008_10_18_2923 老太太侧面 无脸 ✗
    IMG_9848 低头老者大脸 无脸 ✗
    • 修法:多尺度取并集 —— 半分辨率必跑 + 全分辨率补,
      取「最大脸占比最大、同值取人脸数最多」的一次。
      ★ 人脸占比是比值(fw·fh / w·h),与分辨率无关,两次结果可直接比较。
      ★ 清晰度仍固定按半分辨率算 —— 全项目阈值(AUX_MIN_SHARP=120 等)
      都在该口径上标定过,换分辨率会让全部历史数据不可比。
    • 改工具 = 口径分界,必须写清旧数据怎么办(否则下一个人拿旧 CSV 当真):
      旧口径只会漏报(有脸判成无脸),不会凭空造脸 ⇒
      以旧数据支撑「这批没人脸」的结论须用新口径复跑;
      旧数据报「有大脸」的仍成立。工具 docstring 与结论文档两处都要写。
    • ★ 仍未覆盖:闭眼/侧躺/背影/极端侧脸 ⇒ 「无人脸」≠「无人」,永远要人眼终审。
  32. ★★★★★ 「印一行 ⚠️ 就过」的降级分支 = 静默劣化,该硬停的必须硬停。
    EP07 辅图分配原本在找不到足够辅图时走了个「放宽:允许复用」的分支,
    只在脚本末尾印一行 ⚠️ ⇒ 同一对辅图被 6 个镜共用而没人看见。

    • 判据:区分「警告级」与「硬停级」的唯一标准 = 违反了铁律没有。
      违反铁律的没有降级选项,只能:
      ① 如实留缺口(宁可缺,不偷偷降级);② 在落盘之后下硬断言。
    • 「落盘后断言」而不是「写盘前断言」:断言的对象应当是
      实际写出去的台账(ledger),而不是内存里的中间态 ——
      两者会因为排序、去重、二次覆盖而分叉。
    • 断言要一次性把四类都查了,缺一类就是留一个后门:
      不足(short) / 多镜复用(over) / 与主图撞图(clash) / 同镜两条辅图相同(same)
      报错时附上池现状(逐批余量)与需求算式(竖三联 N 镜 ⇒ 需 2N 个位),
      否则下一个人只知道「失败了」,不知道「该怎么补料」。
  33. ★★★★ 「交付物体积」不是判据,sha1 才是 —— 同一份输入的 mp4 体积会抖。
    EP07 实测:同一条 finalize.py 命令、同一份输入,三次跑出
    209,417,841 / 209,450,153 / 209,441,939 B —— 差 ~32 KB(≈0.015%,x264 多线程
    非确定性),而帧数、时长、I/LRA/TP 三数完全相同。

    • 危害:档案里记的是其中一次的体积 ⇒ 下次拿 ls -l 对照会得出
      「交付物被换过」的错误结论,然后去查一个不存在的 bug。
    • 规:档案留档一律 sha1 + 体积两条都写,但判「换没换」只比 sha1;
      文档里对体积要加一句口径说明,否则下一个人一定会拿它当指纹。
    • ★ 连带的一条:凡「A 产出 → B 消费」分两次跑的链路(如
      「出尾板」与「终混成片」),验的是 A 的产物有没有真的进到 B ——
      从 B 抽帧肉眼读,不能只看 A 自己对不对。
      EP07 就是这么发现「母版里到底装的是新版尾板还是旧版尾板」的。
  34. ★★★★★ 改尾板文案后,必须比对 build 打印的四个数 —— 否则「只改了字」是假的。
    尾板是固定画布(1920×1080)+ 固定 DUR、字号走 SCALES 自适应 ⇒
    改文案不改时轴(EP07 实测尾板恒为 278 帧,--total 可沿用)。
    但文案变长会挤垮字号档:EP07 新文案多 3 个字,档位 0.91 → 0.87(小字明显变小);
    日志「试排 scale=0.91:正文需 786 > 可容 770」⇒ 只差 16 px 就掉档。

    • 判据:build 打印的 scale / 正文区 y[a,b] / 高 / 余 —— 四个数
      与改前逐字一致,才算「只改了字」。
      任何一个变了,就是版式也变了,要重看观感。
    • 字太长就压文案,不要接受掉档(掉档是「静默的视觉降级」,没人会报错)。
    • 尾板前后都要留 PNG + mp4 双 sha1,旧版另存目录,便于回退比对。
  35. ★★★★★ 用户拍板改了「用 / 不用」,第一件事是回扫所有对外文字里关于「用/不用」的表述。
    EP07 真实事故:用户拍板保留 s0053/s0054 两镜,而尾板肖像行写着
    「2008-10-18 街头人像连拍整组不用」—— 那两张正是这批、且确实在片子里。

    • 性质:这不是「文案不严谨」,是对外声明与事实不符(本项目属合规红线)。
      比「素材要不要换」严重 —— 素材换不换是审美/风险取舍,声明不实是硬伤。
    • 回扫四处(缺一不可):尾板 credits.json 的 portrait_note/
      发布文案/置顶评论/制作档案。这四处互相独立,改一处不会带动其他。
    • 口诀:片子改了、话没改 = 片子说谎。
    • 连带:credits.json 一动 ⇒ 尾板 PNG → mp4 → 拼接 → 烧字幕 → 终混 → 预览
      整条尾段链重跑
      (EP07 ≈13 min)。改之前先把这个成本算进去。
  36. ★★★★★ 过滤子进程输出,绝不能用「行首字符」当判据 —— 会把正主连同噪声一起滤光。
    EP07 判老盘 147 条视频「音轨有没有被配商业歌曲」时写的 ASR 脚本:

    ```python

    本意:滤掉 [Music] 这类标记行

    txt = " ".join(ln for ln in out.splitlines()
    if ln.strip() and not ln.strip().startswith("["))
    ```

    而 whisper 每一条转写行的行首就是时间戳([00:00:00.000 --&gt; 00:00:04.000] 你來一個)
    ⇒ 全部转写被自己滤光,14 条一律显示「(空)」。
    差一点据此写下「老盘音轨全部不可用」的错误结论 —— 而对照组
    (已知音轨是《两只蝴蝶》)也显示「(空)」,本该立刻暴露是工具坏了。

    • 规:过滤必须匹配该行的确切格式(这里用
      ^\[\d\d:\d\d:\d\d\.\d{3}\s*--&gt;\s*\d\d:\d\d:\d\d\.\d{3}\]\s*(.*)$ 取值),
      而不是靠「首字符像不像噪声」。
    • ★ 更重要的自检习惯:批量任务里放一个「已知答案的对照组」。
      本次的对照组是那条已用别的方法证实过音轨内容的视频 ——
      它一显示「(空)」,就能立刻区分「是素材不行」还是「是工具不行」。
      没有对照组的批量结论,一律不可信。
    • 修好之后同一批数据立刻可用:老盘确实有现场人声(「…到大使那裡,現在都得剪炸…」),
      即 EP07 已用的 s0035/s0036 同源。是工具错了,不是素材废了。
  37. ★★★★★ 换「素材」前,先核它的 EXIF 相机型号与拍摄日 —— 尾板文字是现算的。
    尾板的「拍摄(日期)」与「相机型号 ×N」是从用图 ledger 现算的,
    所以换一张图就可能改掉尾板上的数字,进而触发 §99 那条整段链重跑。

    • EP07 换 s0051(IMG_0045 → IMG_0124)时先核了这一项:
      两张同为 Canon EOS 40D、同为 2008-08-17 ⇒ 相机分布 40D ×45 与
      7 个拍摄日都不变、用图仍 132 张 ⇒ 尾板一个字都不用改。
      核这一项只花 10 秒,不核则可能白跑 13 分钟。
    • 核法:DateTimeOriginal(36867)(不是 DateTime(306) ——
      老盘这批 306 普遍比 36867 晚半个月,是导入/改图日期)+ Model。
    • 口诀:换画面 ≠ 只换画面。凡是「现算」的字幕/尾板字段,都要先回看它依赖哪些元数据。
  38. ★★★★ 中间产物也能用 sha1 做「只该变的变了」自检 —— 比重跑后肉眼看靠谱。
    返工「重建全部 N 个中间产物」时(EP07 重建 90 张 plate),
    真正要证明的不是「新的对」,而是「除目标那一个外,其余一个都没动」。

    • 做法:重建前dump shasum -a 1 清单 → 重建 → 逐条比对 →
      断言只有目标文件变化。EP07 结果:90 张里恰好只有 s0051.jpg 变
      (dd31c41a… → 448fd0db…),其余 89 张逐字节一致 ✓
      ⇒ 一次性证明了「重建链是确定性的」+「改动精确命中」。
    • 反例价值:若比对出意料之外的变化(比如别的镜也变了),
      说明有未被察觉的耦合(工具被改过、池子变了、排序不稳),这是真正的隐患。
    • 适用面:plate/片段/字幕/尾板等任何可重建的中间产物。
  39. ★★★★★ alimiter 的 level 默认 true(auto level)—— 它不是"一律关掉",
    而是分用途;判据是:这道 limiter 的职责是「限幅」还是「归一」?
    bash ...alimiter=limit=0.89:level=false... # 职责=限幅:只防削顶,绝不改电平 ...alimiter=limit=0.917... # 职责=归一:故意用默认 auto level
    两种事故都真实发生过(同一天内):

    • ① 该关没关 —— 做「垫入量对比」试听件时挂了默认值:
      垫入层无论设多少增益都被拉回同一响度(±4 dB 与 ±11 dB 全白算,
      六版测出一律 −16.4/−16.5,我还误读成"口径统一");
      挂在链尾那个还把「不垫任何东西的对照版」抬高 1.0 dB ⇒ 对照组当场失效。
      ⇒ 做定量的 A/B 试听件时,限幅器一律 level=false。
    • ② 该项目特意不关 —— KIT/finalize.py 的第一道 limiter 不能加 level=0:
      旁白链(acompressor + makeup=4)把信号推到 +3.4 dBFS(结构性过载,
      不是偶发),靠 auto level 归一化到 0 dBFS 峰才能收进合法区;
      若显式 level=0,+9.7 dBTP 原样传下去,会逼 loudnorm 静默退回 Dynamic 模式。
      而同一文件末级控峰的 limiter 又必须显式 level=0
      (否则 auto level 偷偷归一化 ⇒ 控峰彻底失效;连带 attack 必须 0.1 ms,
      默认 5 ms 会让瞬态峰直接穿透)。
    • ⇒ 同一工程里这两道 limiter 的写法是相反的、且都是对的。
      动 limiter 前先问自己「我要它干哪件事」,别照抄上一次的写法。
  40. ★★★★ 「垫入量」必须按「实际取用那一段」实测反推,不能按素材全长算。
    一条 150 s 的素材,实际只取其中 20 s;用全长 mean 推增益,
    则各版之间的响度关系是乱的 ⇒ 试听对比失去意义(用户听到的差异里混着电平差)。
    python src_mean = mean_db(src, ss, dur) # ★ 先切、再测 gain = TARGET_MEAN_DB - src_mean # 全部版本落到同一目标电平

    • 目标怎么定:垫入层比旁白窗实测 mean 低 6–8 dB(EP07 取 −25.0 dB vs 旁白 −18.3 dB)。
    • 验算:线性能量相加 10^(m1/10)+10^(m2/10) ⇒ 10log10(…)
      (EP07 验算 −17.46 dB,实测 −17.6 dB ✓)。算得出来才对得上,否则有隐藏增益/衰减。
    • 提增益前先看素材 mean:要提 >10 dB 的素材(EP07 街声 −36.4 → +11.4),
      提的是它自己的底噪 —— 必须提醒用户「这条可能是在放底噪」,别让他以为只是变响了。
  41. ★★★★★ 「取素材哪一段」和「垫多少」是两个独立变量 —— 先扫段落,再定增益。
    EP07 实例:用户反馈「北京市场那条我没听到」⇒ 复查发现我把切片起点填了 0,
    而该素材 0–20 s 恰是全曲最空段(−29.3 dB),60–80 s 才最饱满(−20.5 dB),差 8.8 dB。

    • 用户听不到不是耳朵问题,是我切了哑的一段 —— 而且我当时还先怀疑"垫入量太保守",
      差点去调音量掩盖真因。先查选段,再查电平。
    • 扫法(每 20 s 一段测 mean,取最响、且剩余时长 ≥ 需要时长的起点):
      bash for t in $(seq 0 20 $dur); do ffmpeg -nostdin -hide_banner -ss $t -t 20 -i 素材 -af volumedetect -f null - 2&gt;&amp;1 \ | grep mean_volume; done
      剩余时长是硬约束:43 s 的车站广播只能取到 20 s 起(40 s 起只剩 3 s)。
    • 脚本必须加防呆断言:素材时长 − 起点 &lt; 需要时长 直接抛错,别默默出残切片。
    • ★★ 同一素材的切片定义只能写一处:EP07 单版 demo 与串烧各写了一份 PLAN
      ⇒ 修了单版忘改串烧,用户听到的与说明不符。正解是串烧 import 单版脚本的定义。
    • 同源错误:切片起点变动 ⇒ 该版全部旧数据作废,要连同串烧一起重出并归档旧版。
  42. ★★★★ 批量抽帧(-ss X -t Y … -vf fps=…)的输出帧,不能用来定位时间点。
    与「密排联系的标签不能用来定位编号」是同一类错误:
    批量的坐标不可信,必须回原始数据实读。

    • EP07 实例:-ss 200 -t 50 -vf fps=1/16 出的第 3 帧按算应为 232 s,
      画面字幕却对应 SRT 的 3:42(223 s)—— 差 9 秒。
      一度怀疑「SRT 与成片有偏移」,差点去改字幕。
    • 复核:改用逐帧精确 seek -i 文件 -ss 时间 -frames:v 1,
      223.5 / 232.0 两帧字幕与 SRT 逐字吻合 ⇒ SRT 没问题,是批量抽帧的坐标错了。
    • 规:凡「第 N 秒是什么画面 / 什么字幕 / 哪一镜」,一律逐帧 seek 实读,
      绝不用缩略图、联系表、fps 抽帧的推算值。
  43. ★★★★★ 「某个声音在哪个频段」用「前后逐频段增幅比对」定位,一次定死。
    做法:取该声音出现前与出现后等长的两段,逐频段测 mean,算 Δ
    ⇒ Δ 最大的频段就是它。
    EP07 实例(用户指认「笛子就是 0:04 进来的那一段」后的复核):
    | 频段 | 4 s 前 | 4 s 后 | Δ |
    |---|---|---|---|
    | 150–300 Hz | −37.6 | −30.7 | +6.9 |
    | 600–1200 Hz | −48.2 | −40.8 | +7.4 |
    | 1200–2500 Hz | −59.1 | −49.1 | +10.0 |
    | 2500–5000 Hz | −71.9 | −60.4 | +11.5 |
    | 5000–10000 Hz | −84.3 | −71.3 | +13.0 |
    ⇒ 频率越高增幅越大 = 基频在低频、谐波一路延伸到 10 kHz 的吹管乐器(笛类)✓

    • ★ 反面教训(这条最贵):我先按「笛子=1.5–4 kHz」这个想当然的频段假设扫了全片,
      还据此向用户报「只有两个候选」——假设错了,结论就全错,
      两个候选里都没有正确答案,最后要用户自己指认。
    • ⇒ 别用"某乐器应该在哪段频率"的先验假设;用 Δ 实测。
    • 采样要点:前后两段都要取够长(2–3 s 起),单帧测不准;
      环境声层本身有起伏,Δ 要能做进噪声以上。
  44. ★★★★ 粗粒度扫描(每 10 s)不能用来定位事件 —— 短事件会被平均掉。
    同一实例:我逐 10 s 扫配乐中高频,0–10 s 报 −25.3(看着"中高频很弱"),
    把「1 s 音乐进来、4 s 主奏进来」这两次进入整个抹平了。

    • 分工:定位事件用细粒度(1–2 s 窗);看整体分布/弧线才用粗粒度(10 s 窗)。
    • 与「密排联系表的标签不能定位编号」「批量抽帧的坐标不可信」同源:
      分辨率不够,坐标就不可信。
    • 推论:凡是"某秒/某张是什么"的判断,必须能回到原始实读;
      凡是给出"候选"之前,先问自己这个扫描粒度能不能看见我声称的那个东西。
  45. ★★★★ bandpass 的 w 默认按 Q(品质因数)解,不是 Hz —— 数字离谱时先怀疑单位。
    bash # ✗ w=2500 被当成 Q=2500 ⇒ 带宽窄到近乎全阻,mean 掉到 −87 dB -af &quot;bandpass=f=2750:w=2500&quot; # ✓ 要 Hz 带宽必须显式 width_type;或干脆用无歧义的两级级联 -af &quot;bandpass=f=2750:w=2500:width_type=h&quot; -af &quot;highpass=f=1500,lowpass=f=4000&quot; # ← 推荐,没有单位歧义

    • 症状:测出来 −87 dB 这种物理上不可能的数(正常应在 −30 ~ −45)。
    • ⇒ 规:看到离谱的数字,先查参数单位与默认值,再查素材。
      同一类:alimiter 的 level(默认 true)、ebur128 被 -v error 吃掉 Summary。
  46. ★★★★★ 判据的窗长必须匹配事件的尺度 —— 一天之内栽了四次,全部同源。
    实例(2026-10-05,EP07 查「用户说的笛子」):
    | 写法 | 后果 |
    |---|---|
    | 每 10 s 粒度扫 | 把「1 s 音乐进、4 s 主奏进」整个抹平(=108/107 条) |
    | STFT 帧级 crest(10.7 ms 窗) | 全片 0 命中,而人工点算 33.25s 明显合格 |
    | STFT 帧级 5–12k 占比(10.7 ms 窗) | 271 处,连 32.01–34.65 整 2.6 秒都判成「高频 100%」;长窗才知只有 0.1 秒是真的 |
    | 帧级 SFM(10.7 ms 窗) | 33.25s 算出 0.21;长窗(0.12 s / 4096 点)才是 0.0001,差 2000 倍 |
    | 用 0.25 s 窗测压制效果 | 报「只降 6 dB」;换事件等长窗(0.12 s)才看到真实的 −34.5 dB |

    • ⇒ 规一:定位事件用细窗;判性质、测效果一律用「与事件等长的窗」。
    • ⇒ 规二:能不用 STFT 帧就不用 —— 要「比值」类判据改用
      带通滤波 + 短时能量包络(cumsum 实现),窗长显式写在参数里。
    • ⇒ 规三:STFT 的绝对幅度谱是未归一化的(−45 dB 量级),与时域峰值不是一回事;
      跨文件比绝对电平必须用同一算法,否则会得出「配乐比旁白响 40 dB」这种荒唐结论。
  47. ★★★★★ 判「某声音在成片里是不是主导」,必须比「分轨绝对电平 vs 成片绝对电平」。
    同一实例:配乐轨 1663 Hz(G#6 长音)显著度 +13~+39 dB,看着像元凶;
    但它在成片里被 --music-gain -15.8 压到 −82 ~ −87,
    而成片同窄带在旁白说话时是 −24.6(10.05s)——
    分轨上做 18 dB 的 EQ,成片层只动 +0.4~+1.7 dB,白做。

    • ⇒ 只看分轨的 Δ 会误判「压了却没效果」;要看绝对电平才知道压的是不是主角。
    • ⇒ 分轨的 Δ 不受恒定增益影响 ⇒ Δ 可跨轨比大小;绝对电平必须换算增益后再比。
  48. ★★★★ ffmpeg 的 deesser 对「窄带高频爆发」完全无效 —— 用的是 equalizer + enable。

    • 实测:目标段 peak 0.1 → 0.2 dBFS(纹丝不动);deesser 是为宽带齿音设计的。
    • 有效写法(事件核心 5k 以上 −14.0 → −48.5 dB,降 34.5 dB):
      bash equalizer=f=6500:width_type=o:width=1:g=-30, equalizer=f=9000:width_type=o:width=1:g=-30
    • 定点修复:equalizer 支持 timeline ⇒ 加 enable=&#x27;between(t,0.66,0.80)&#x27;(t 为该滤镜链内的时间)。
    • lowpass=f=4000 只有 12 dB/倍频程滚降,压不动 5–8k(实测只降 5.1 dB);要陡必须级联。
  49. ★★★★★ 工具疑似失效时,先用「纯音自证」再怀疑工具 —— 这招省一小时的弯路。
    bash ffmpeg -f lavfi -i &quot;sine=frequency=7000:duration=0.4:sample_rate=48000&quot; \ -af &quot;equalizer=f=7000:width_type=o:width=1:g=-16,volumedetect&quot; -f null -
    实测:7 kHz 过 f=7000:g=-16 → 降 16.0 dB(全效);过 f=5500 → 降 10.1 dB;
    过 200 Hz 同参数 → 降 0 dB(不伤低频)。⇒ 工具正常,问题在测法。

    • 反面:我曾因此怀疑「equalizer 根本不工作 / 整条链路都是空的」,
      实际只是我用了 0.25 s 窗测 0.12 s 的事件(=第 109 条)。
  50. ★★★★ 「用户描述的音色」是真信息,别用先验频段覆盖它。
    用户说「笛子」「突然蹦出来」——两处都精确成立:
    目标段 5–12k 占 99.1%、谱重心 8064 Hz、主峰 7.0/7.1/7.2 kHz 间隔 93.5 Hz
    (=说话基频的 75–77 次谐波),谱平坦度 0.0001(纯音性、离散谱线)。
    ⇒ 擦音听起来就是一声尖笛。先信描述,再用实测去对;不要先按「笛子=1.5–4 kHz」扫,
    那两个候选一个都不含正确答案。

  51. ★★★★★ 「某个特征符合我的描述」≠「它就是用户指认的那个」—— 先让用户指认,再深挖。
    反面实例(2026-10-05,就在第 113 条之后立刻踩到):
    我把旁白 33.26–33.38s 的 5–12k 爆发查得极细(112 处、主峰间隔 93.5 Hz、SFM 0.0001…),
    但那只是我自己认定的对象。用户听了对照件后一句:「两遍都没有笛子呀」。

    • ⇒ 规:先把「能听的对照件」交出去让用户点头,再投入深处。
      「物理特征吻合」是必要不充分条件;用户的耳朵才是判据。
    • ⇒ 同一条也适用于「用户说某个声音突兀」:先确认对象,再定手法。
  52. ★★★★★ 对照件的源文件,必须是「用户实际听到的那个版本」。
    同一实例:我拿 成片_终版.mp4(v1,根本不含垫入层)做对照 ⇒ 自然「两遍都没有」。
    而用户听到笛子的是 v2(含垫入层)。 拿错源文件,等于把答案摆在眼前却看不见。

    • ⇒ 规:做对照前先确认「这个文件里有没有那个东西」——
      用实测(如特征频段显著度)核一遍,不要凭版本号想当然。
  53. ★★★★★ 垫入层/环境层的素材,入库前先扫「有没有乐器长音」。
    真凶实例:垫入素材 139951_北京市场_卖乐器的人旁边 里面真的有乐器在响——
    1663 Hz(G#6)及 7.0/7.1/7.2 kHz 谐波列,绝对电平 −21~−32 dBFS、显著度 +42~+53 dB,
    比配乐在同一窄带高 30 dB。旁证:不含垫入的环境 raw 轨在同频段是 −240 dB(全静音)
    ⇒ 成片该处能量 100% 由垫入引进;成片 v1 vs v2 在 1663 Hz 差 19.7 dB。
    bash # 扫法:窄带稳定分量(乐器/长音的特征),不是看整体电平 python 工具_哨音检测.py --in 素材 --lo 800 --hi 6000 --prom 15 --min-dur 0.4 # 「未检出窄带稳定分量」 ⇒ 干净可用 # 追踪指定频率全片时间线(怀疑某音时) python 工具_哨音检测.py --in 素材 --trace 1663 --trace-bw 150 --trace-step 2

    • ⇒ 素材自身电平均匀也重要:本次新素材各 20 s 段 −20.3~−23.1(只差 2.8 dB),没有"空段"。
    • ⇒ 时长要够:段 1 要 109 s ⇒ 只有 225.9 s 的那条够用(104641 只有 28.3 s,不够)。
  54. ★★★★ 生成「垫入层/环境层」后,必须实测落点并按偏差重生成 —— 别用算出来的增益当结果。
    工具_建垫入层.py 已加自动校准:生成 → 实测落点 → 按偏差重跑一遍(链路无压缩,一次即收敛)。

    • ★★ 校准口径必须是 gen_target(=落盘目标 − sfx_gain),不是 target:
      该脚本生成的是「终混前」的层,终混还要再乘一次 sfx_gain。口径搞反会差 1 dB 且不自知。
    • 实例:换素材后初算增益的落点比目标低 3.4 dB(已排除淡入淡出),原因未定位
      ⇒ 不猜,直接实测反推;修正 +3.40 / +3.80 dB 后落点 −24.0 ✓ → 经 sfx-gain 后 −25.0 达标。
  55. ★★★★★ 换单张图之前,「已用素材」集合必须从 _plate/_ledger.csv 重建 —— 镜表「素材」列 ≠ 实际用图。
    镜表只记主图;竖图主位走三联时,脚本会另挑 2 张辅图并写进 ledger 的「辅图」列。

    • 实例:某「未用候选池」只排除了镜表 90 张主图,漏掉 42 张辅图 ⇒ 27 张候选里
      10 张其实早已进片;按它挑图会撞车。
    • ★ 顺带:同一批的「未用」也可能被旧口径人脸闸虚增。
      本项目 2026-10-05 修过「尺度盲区」(旧实现只跑半分辨率,只漏报不误报)
      ⇒ 旧闸报的「无脸」不可信(实证:同一张图旧报 0 脸、新口径 3 脸/最大占比 0.528%)。
    • 规:换图前 已用 = ledger.素材 ∪ ledger.辅图;候选池的人脸闸必须与当前口径同版。
  56. ★★★★★ 换掉主图 ≠ 那张图走了 —— 它会从「辅图位」回来。
    素材脱离镜表后不再是「已用」,于是立刻落进辅图池,被三联自动挑中。

    • 实例:把 s0025 主图换成竖图后重跑,它的右辅条被自动挑成刚撤下的那张(等于没换干净)。
    • 跑题黑名单只过滤辅图,挡不住将来有人把它塞回主图 ⇒ 必须双闸:
      ① 跑题黑名单(黑名单_跑题.csv,带「状态」列)② 用户禁列硬闸门(脚本 BANS,入口断言硬停)。
    • 规:因「质量差/用户否掉」而撤下的素材,当场写进 _黑名单_禁列_*.csv 并接进 BANS。
  57. ★★★ 三联的辅图是「全局零复用」按顺序分配的 —— 插入 / 改变一个竖图镜,会顺移后面所有竖图镜的辅条。
    改一处先算影响范围,再决定要不要重渲:看 ledger diff,别猜。

    • 实例:s0025 改竖图后,s0027/s0032/s0033/s0035/s0036 的辅条各顺移一位(主图全不变)。
    • ★★ 其中 s0033/s0035/s0036 是「视频插入镜」(画面走 2008 拍客视频,render.py 不读其 plate)
      ⇒ 它们的 ledger 虽然变了,成片不受影响 ⇒ 只需重渲真正用到 plate 的那几镜。
    • ★ 重渲是增量的:render.py --stage shots 的 todo 只收「不存在或 <20 KB」的镜
      ⇒ 必须先把要重渲的 &lt;镜号&gt;.mp4 移走(mv,不要 rm),否则它会跳过。
    • ★ 尾板「用图 N 张」是从 ledger 现算的 ⇒ 图数一变就要重出尾板(make_credits_ep7.py build|clip),
      但尾板是固定画布 + 固定 DUR,改文案不改时轴 ⇒ --total 可沿用,只需确认帧数仍为 278。
  58. ★★★★★ 尾板时长不是自由变量 —— 它被「配乐末段可取的 tail 上限」锁死;改时长必须同步重出配乐轨。
    (2026-10-06 EP08 实测,症状全静默、不报错)
    ① 约束怎么来的:末乐章用某首曲子,M9 对齐点钉在曲内某一秒(艺术决定,不动),
    该曲实长 − 对齐点 = 对齐点后可用 tail 上限(EP08:EchoesOfHome 上限 62.63 s)。
    tail 由片长反解(CUES[-1][&#x27;tail&#x27;] = FILM_DUR − hit_film,EP08 hit_film=461.72)
    ⇒ 片长有上限 ⇒ 尾板有上限(EP08:≤ 461.72+62.63−517.9333 = 6.4167 s,取 6.0)。
    ★ 超限的表现是谱曲环节硬失败(这是好事,会拦住你):
    EchoesOfHome: 取到 3006166 样本 &lt; 需要 3082080。
    ② 踩的坑:make_credits_epN.py 的 DUR 默认值被写成 8.0
    (理由挺像回事:EP07 是 9.28 s,本集 6 曲+肖像说明内容多约 50%),
    重建尾板时没显式传 CREDITS_DUR ⇒ 拿到 240 帧尾板、成片 15778 帧,
    而配乐轨/音效轨仍按 6.0 s 生成的 523.933 s(没重出) ——
    "改了尾板"与"配乐还按旧片长"两条线各自都自洽,合起来才有病。
    ③ 症状(三个都要会查):
    · 音频流短于视频流:524.014 s < 525.933 s ⇒ 尾板末 1.97 s 连一个音频样本都没有;
    · 配乐 RIDE 末点(EP08 (523.92, −60))本是为 6.0 s 收尾设计的
    ⇒ 8.0 s 下变成「尾板第 6.0 秒处配乐硬切断」+ 后 2 秒静音,不是渐隐收尾;
    · 尾板文字"读得完"是假象 —— 时长变了排版并不会自动跟着变,
    SCALES 只按内容行数缩放字号,不按时长。
    ④ 怎么查(30 秒,不要靠耳朵):
    bash ffprobe -v error -select_streams v:0 -show_entries stream=duration -of csv=p=0 成片.mp4 ffprobe -v error -select_streams a:0 -show_entries stream=duration -of csv=p=0 成片.mp4
    两者必须相等;AAC 尾帧 padding 允许音频略长(≤0.1 s),绝不能短。
    再逐 0.5 s 扫尾部 RMS(-ss &lt;末-8&gt; -t 8 -f s16le + 按窗算)确认不是"提前切断再静音"。
    ⑤ 规(四条):

    • 改尾板时长之前,先打开谱曲脚本读 FILM_DUR 段 —— 那里写着尾板上限与推导;
    • 重建尾板前先确认脚本默认 DUR,并显式传值:CREDITS_DUR=6.0 … make_credits_epN.py clip;
    • 尾板时长 ∈ 「改时轴」(区别于"改文案")⇒ 必须同步重出配乐轨/音效轨,
      全链重走「尾板 → 拼接 → 烧字幕 → 终混」,--total 必须同步改;
    • 内容装不下就缩字号/减行数,或把长内容(史实来源、URL 清单)移入发布文案与置顶评论
      —— 不要靠拉长尾板解决。
    • ★ 两个脚本要互加交叉指针:谱曲脚本写清"尾板上限 = 多少、为什么",
      尾板脚本写清"默认值 = 上限内的定案、改它要连带重出配乐轨"。
      (EP08 已双向补齐:score_ep08.py ↔ make_credits_ep8.py。)
    • 与 §97 的分界:§97「改文案不改时轴(EP07 尾板恒 278 帧)」讲的是字变、时长不变;
      本条讲时长真的变了。两句都要记住,别用错一把:改字 → 时轴不动;改时长 → 音频链全重走。

12. 平台运营与素材复用 → 见 references/

本节内容已拆到 references/platform-and-reuse.md(原 §12 素材复用 + §13 平台运营复盘)。

什么时候去读:
- 任务涉及「这条内容发哪个平台」「多平台怎么分工」
- 账号被限流 / 被处罚 / 权益被取消,要判断怎么恢复
- 要算收益、读平台后台数据表、判断成长等级与门槛
- 一批照片要再做一篇图文长文

⚠️ 该文件开头有一条前提更正,读之前先看 ——
「照片是素材,不是独立内容线」(用户 2026-09-21 亲自更正)。

做视频本身(§0–§11)不需要读这份。

常见问题

5 问
答案由本站依据条目字段整理
照片电影导演(Windows 版)怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_0ce0d907/photo-film-director-win。安装方式为未标注。装完刷新技能列表或重开客户端。

照片电影导演(Windows 版)支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

照片电影导演(Windows 版)权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

照片电影导演(Windows 版)收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

照片电影导演(Windows 版)用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。