照片电影导演(photo film)
把「一堆照片」变成可发布成片。配音默认走 AI 合成 —— 使用者不需要自己录音,
文字稿进去,配音和字幕一起出来。
三条入口:
| 入口 | 使用者给什么 | 走哪条产线 |
|---|---|---|
| ① 标准(默认) | 照片 + 拍摄地点 | AI 写文案 → 合规/事实核查 → AI 合成配音 → 出片(§3.0.6) |
| ② 自带录音 | 照片 + 现成录音 | 从 §3.1 剪接(跳过合成) |
| ③ 高级 · 真人配音 | 照片 + 想用真人声音 | §3.0 —— 属定制服务,需另行安排录制 |
> ★ 关于入口 ③:真人配音是可选的增值服务(真人的嗓音、语气、地域口音,
> AI 合成替代不了)。技能自带的完整闭环是入口 ①;想走 ③ 的,按 §3.0 的规范
> 出录音稿后交真人录制。默认不要主动把使用者往 ③ 引导 —— 先用 ① 跑通出片。
> ★ 合规前置:走 AI 配音就必须按平台规定声明 AI 生成(§3.0.6 末节有红线清单)。
> 不得声称「真人原声」。这条不是建议,是发布前提。
通用章节(事实核查 / 响度口径 / 封面 / 交付三件套)已内联到
references/common-standards.md —— 本技能自包含,无需外部技能。
环境准备(macOS)
| # | 需要什么 | 怎么装 |
|---|---|---|
| 1 | Python 3.10+ | 系统自带;不够新就 brew install python |
| 2 | ffmpeg | brew install ffmpeg |
| 3 | Python 库 | pip install pillow numpy opencv-python scipy edge-tts |
| 4 | 中文字体 | 系统自带,无需处理(宋体/黑体/苹方已足够) |
# 一次性装齐(macOS)
brew install ffmpeg
pip install pillow numpy opencv-python scipy edge-tts
# 自检:能列出音色就说明配音链路通了
python scripts/tts_narrate.py --list
★ 首次运行会自动下载模型(人脸检测约 200 KB、语音识别约 1.5 GB),
需要联网,之后走本地缓存。不跑人脸/语音相关环节就不会触发。
0. 铁律
- 不要停下来等确认。 只有三种情况需要用户介入:额外付费、覆盖原片、对外发布。
用户说过「过程别问我确认不确认,始终确认的」——那就一路做到出片。 - 照片必须每一张都动。 静止镜 = 观众划走。全部镜次带运镜,且相邻镜不得同运镜。
- 视觉结论必须肉眼过。 封面、文字层、调色,「数字上成立」≠「视觉上成立」。
渲染完出对比图并真的打开看,不合格就重做 —— 别拿数值糊弄。
★ 调色/片头/转场另有专节 §5.5「影片风格六组件」(2026-10-03 建立)——
核心一句:风格 = 六件组件(片头/字幕/运动/转场/调色/尾板),不是六个滤镜;
调色只占六分之一。 调色管线、四数验收、片头质检、转场闸门红线全在 §5.5。 - ★ 合规审查 + 事实核查,必须在「剪辑之前」完成 —— 不能等出片后再审。
内容问题在文案阶段是「改一行字」,到成片阶段就是「重渲染 + 重烧字幕 + 重合成」,
修改成本差两个数量级。先出一份内容合规审查报告.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 = <!DOCTYPE ht,
整份是一张 HTML。
→ 判据:落回「这到底是不是那类文件」,不要用「涨了多少 MB」当结论。
python
def looks_like_html(p):
with open(p, "rb") as f:
h = f.read(512).lstrip()
return (h[:1] == b"<" or h[:9].lower().startswith(b"<!doctype")
or h[:5].lower().startswith(b"<html"))
★ 由此立一条通则:「有进展」和「进展是对的」是两件事。
长字节只证明连接活着,不证明拿到的是想要的东西——
任何自动化下载,收尾都必须有一道「内容类型」验证。
- 铁律:半截文件永不删,只 -C - 续传(曾把一份下了 5 分钟的 2.7 MB 残件清零重来)。
★ 但续传前必须先确证服务器认 Range:
python
def range_cap(url): # 三态,不许把「判不出来」当成「不认」
code = <curl -r 0-0 -w '%{http_code}'>
return True if code=="206" else (False if code=="200" 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't support byte ranges. Cannot resume. → 重排队 → 再撞 429 →
死循环,永远不可能成功。日志里只显示「零增长 45s」,看着像网差,其实是被限流。
→ 按 host 定策略:Wikimedia 类串行 + 15 s 间隔,429 单独识别并长退避
(且不计入尝试次数);CDN 类(如 Scott Buckley)不受限,仍可并行。
--retry 0 —— curl 默认的 --retry 会在 429 上自己死磕,把窗口糊满、看不到真实状态码。
「零增长」先问「是不是我请求太密」,一条带 -w '%{http_code}' 的单发 curl 一分钟出真相。
- 授权只认 PD / CC0,排掉 CC BY-SA(「相同方式共享」有传染性)。
没有可靠画像的人物,用「他的著作/他的碑」代替(见 §2 与寒山寺案)。
但手上没有真拓片,就别把排版的铭文叫「拓片」——名实不符是硬伤。
- 下载器必须多轮循环 + 只砍真死连接(这些站点是脉冲式,同一分钟 25→108 KB/s,
设高阈值会把自己的正常连接砍掉);nohup … & 在本机活不下来,要用托管后台。
- ★★★ 链路可能是「间歇性」的 —— 「慢」和「断」要用两套判据。
实测同一分钟: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 = <curl -r 0-0 -w '%{http_code}'>
return code in {"200","206","301","302","303","307","308",
"403","404","410"}
★★ 关键:一个字节都没下到 = 线路问题,不要算作一次「尝试」。
旧版把 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. 工程目录
<project>/
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 <集>/07_work/照片池_<集>.csv \
--out <集>/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 <集>/07_work/照片池_<集>.csv \
--out <集>/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,不算低。
三条实测坑(写进来,别重踩)
- 「整批剔」会误伤 —— b15 的
c0933是正常的 ⇒ 必须支持b15 -c0933(剔该批但保留指定编号) - 剔除要落「该批全量」 —— 不能只剔抽样到的那 24 张(b16 有 115 张只抽 24 ⇒ 漏剔 91 张)
- 小批别单独成页 —— 出现过「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 "/Volumes/…/10_景点/南京高淳_2014" \
--out <项目>/_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 'PIL'。
★ 顺手一条选材判据(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 | 同上(纪念馆园区) | ⛔ |
⛔⛔ 铁律
- 借壳文件夹的边界必须逐帧看图定界 —— 不许用文件夹边界、不许用"按分钟取整"推断、
不许用时间阈值。(时间阈值 = 推断;逐帧看图 = 实读。) - 锚点写成「文件名常量」,不是时间 —— 不同批次时间字段格式可能不同(2014 批只有
17:15:28,
按[11:16]切片会静默变空串),而且时间阈值可以"差一点点",文件名不会。 - 脚本必须自检锚点,落地脚本做两道闸门:
① 两个锚点文件必须真实存在于池中(否则sys.exit);
② 边界连续性 —— 边界之后不许出现留用帧、边界之前不许出现跑题帧。 - 报数口径必须分清:跑题段「总帧」≠「从净池剔掉的张数」。
EP08 实测:跑题段 61 帧(含 9 张已被画质闸淘汰的 + 1 张 dup),但从净池只剔 51 张。 - 别把「跑题」记成「肖像」 —— 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 就是他要指认的那张。
★★★★ 四条正确做法
- 截图先别上算法,先人眼开原图核。 用户既然能指认,说明他一眼认得 —— 人眼比哈希可靠。
- 真要比,必须把候选区域也裁出来(多尺度滑窗),不能整帧比整帧。
- 哈希/直方图只用于「同一张图的不同副本」(EXIF 同秒、批量导出、裁切版),
不用于「同题材的不同构图」 —— 构图一变,哈希就废。 - ★ 缩略图分辨率就是判据上限。 那轮 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_<地名>.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 < 1.5 |
灰阶(翻拍/证件照)整组不用 |
| 质量闸 | detail < 0.10 |
不进片 |
| 肖像闸 | face_lv == 3(人脸 > 12% 短边) |
可辨认面部,整张不用 |
| 去重闸 | dHash 汉明 ≤ 8 | 同组保留 detail 最高的 |
两级人脸分级(不是只有「用/不用」):
0 无人脸 → 自由用 | 1 ≤ 3.5% 短边 → 远景可用 | 2 3.5–12% → 不进主位 | 3 > 12% → 整张不用
★★★ 三个坑(EP06 实测,都会静默或白跑)
- 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两个都没有。 - 全盘 EXIF 索引
exif_索引_全盘_*.csv里有 NUL 字节 ⇒csv.DictReader直接抛
_csv.Error: line contains NUL(整个文件读不进来)。
修法:open(..., errors='replace')→.replace('\x00', '')→splitlines()再交给 DictReader。 - 跑题批次要直接写进池表(
可用 = N+ 排除原因),不要只留在批审目录 ——
否则选片时还得回去翻批审图。
★ 提速:只给「会进片」的批次建缓存(EP06:540 张里只有 355 张要建,省掉 185 张)。
池表仍列全部张数,跑题的带原因留档(这样池表本身就是一份完整的判定记录)。
★ EP06 实测数(可作下一集的期望值):
540 张入表 → 355 张评分 → 净可用 339 张;detail 中位 0.258、< 0.10 仅 8 张;
6′12″ 的片需 ≈135 镜 ⇒ 余量 2.5 倍,够用。
2.1c ★★★★★ 选片链:题材台账 → 选片表(唯一权威) → 隔离清单(2026-10-06 EP08 定案)
为什么必须有这一节 —— 用户原话点破的病根:
> 「题材表按大区间取图,前面的组没取尽就轮不到后面。
> 改成句级区间+兜底。这个问题可以在选题之后专门做一次选片,按优先级排序,
> 选好之后,不在项目内的隔离一下。」
EP08 实测的病:GROUPS 写成 ('c0226'–'c0245') = 云锦织物陈列,
而那一区间实际画面是城市高楼天际线 —— 错得毫无提示,成片直接拿城市照配「云锦」旁白。
三张表 / 一条链
池表(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 |
未入选的编号(给用户一个「哪些没用、为什么没用」的可见答案) |
★★★ 四条硬规矩
- 题材表绝不按大区间。
RANGES按文件名编号划段(不是 c/y 流水号 ——
流水号会随池变动而漂)。未归类即sys.exit(EP08 靠它抓出漏标的IMG_4083/4087/4088)。 - 判据三条:拍摄时间 / 画面内容(联系表肉眼过)/ 不采信「来源夹」
(来源夹只是人工内容分类,非物理目录;EP08 有IMG_4466–4468标「园区」实为别处)。 - 句级
NEED+FALLBACK,取图顺序:横图单图优先(cover,不留白)→ 竖图三联兜底;
定向三联的镜反过来。NEED[sent]是列表,第 0 项是主题材、其后是兜底链。 - 隔离清单不是可选项 —— 它是「选片这件事做完没有」的凭证,也是用户下次改片时的第一手材料。
★★★ 六个机制(都不是锦上添花,缺了会出结构性问题)
| 机制 | 作用 | 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 |
题材类内优先序。类分对了,类内仍是编号序 ⇒ 取到的是"该类里编号最小的几张",而不是"最贴这句的几张"。给"必须精准匹配"的类加显式优先表(不在此表的按编号序排后) | ['TANG'] 把"画面能辨认出萱瑞堂三字"的排前面;['GPURE'] 把亭/水池/假山/廊排前面 |
RESERVE |
循环前先占位:收尾金句/关键句必须拿到的东西,先占好再跑主循环 | 句68「城西那一半,还在织」→ 题材型 LOOM |
RESERVE(素材型) |
上一条的升级:PREFER 只解决"类内排序",解决不了"先到先得被前面的句抢走" ⇒ 只有循环前预占才拿得到。EP08 扩展为直接指定素材,并支持三联逐镜对应(第 3 项写成空格分隔的三张) |
{68:'LOOM', 35:['IMG_4178','IMG_4179','IMG_4114 IMG_4283 IMG_4213']} |
> ★ PREFER 与 RESERVE 的分工:PREFER 管「同一个类里先取哪张」,RESERVE 管
> 「这一句必须拿到的那几张,别被前面的句抢走」。两者都要有 —— EP08 只加 PREFER
> 时,句35 最贴的「水池+假山」三张仍被段2/段3 的园林句先取走,加 RESERVE 才拿到。
🔴🔴🔴 七个「静默」坑(全部实测踩过,都不报错)
scan()不许用 cursor 当扫描起点。 首版i = cursor[tag],而cursor是 commit 次数;
commit 只增不减,但被取走的编号在组内是分散的(scan 会跳过 used 与方向不符的条目)
⇒ 游标跑到还没取过的条目前面,把它们整个跳过。
表现:「主组明明还有货,却静默退兜底」。
⇒ 改全表扫、不用游标(每组 ≤114 条 × 185 镜,开销可忽略)+ 加「幽灵降级」闸门。PRIM_CAP的生效前置是len(base) > 1。NEED若写成单元素['LOOM'],
限量规则整段被跳过 ⇒ 句53/54/55/57 照旧各吃 3 张、LOOM取空硬失败。
⇒ 教训:限量规则与它依赖的结构要一起写(['LOOM','YHALL'])。- 帧对齐读旧表、写新表。
render_epN.py的plan()原写src = 镜表_v1.csv,
而写回目标是SHEET = 镜表_v3.csv⇒ 每跑一次渲染,就把 v1 的「素材/张数/版式」整列灌回权威表。
⇒ 修:源表改成本表自身 + 加闸门「只许改 入/出/时长/帧数 四列」
(除这四列外逐格比对,不一致即sys.exit)。 - 钉死镜(
FIXED)的配额要在主循环之前先占。 EP08 的s0019排在句7 第 10 位,
轮到它时city[7]已被前 3 镜用满 ⇒ 城市悄悄变 4 镜。 - 闸门③「幽灵降级」会误报 —— 把「连镜去闷」(主动换题材)也判成幽灵。
⇒ 引入turned(连镜超阈值且主组就是它)/capped(主题材已达上限)两个变量,
主动降级进白名单(EP08 从 38 处误报降到 0)。 plate与clip都是「存在即跳过」,双双静默(2026-10-06 实测,一晚上踩两次):
-make_plates_epN.py:if not force and os.path.exists(out) and getsize(out) > 30000: return 'skip'
-render_epN.py shots:clip 已存在就跳过(--force才强制)
⇒ 只重渲--only s0036而不先重建 plate,会拿旧图渲一遍;
先把 clip 渲回来了、再改选片表重跑,则shots报「新 0/185」正常退出——
画面根本没变,却一个字都不报。
⇒ 规(两条都要):
① 重渲前先把目标镜的 clipmv走(别指望脚本自己发现);
② 跑完必看「片段:新 N/185」的 N 是否等于预期改动镜数(EP08:没移走时 N=0;移走后 N=11 ✓)。- 改题材分类会引发「连锁位移」,不止改一张。
IMG_4190从TANG移到GATE后,GATE类内取图顺序整体前移一位
⇒ 实测11 个镜换了图(不是 1 个)。
⇒ 改完题材必须diff旧/新选片表,按 diff 决定重建范围;
不要凭"我只改了一张图"下判断(会漏渲,且不报错)。
★★★★★ 认字闸门:旁白点名了具体文字时必须过 OCR,不许看缩略图(2026-10-06 EP08 血本)
病:旁白「你抬头看,院子里挂着一块匾,写着三个字:萱瑞堂」。
我靠人眼看联系表缩略图认匾额,把 IMG_4190 判成「厅堂正面(白墙+门+上方匾)」放进 TANG。
成片后用户报「1:37 画面没对上」—— 放大一看,那块匾写的是「曹府戏苑」四个字。
旁白说三个字、画面是另一块四字匾 ⇒ 比上一版更糟。
根因:联系表缩略图里匾额只有几十像素,认字必错。
(同类坑:_pool/<编号>.jpg 缩略图与选片表编号不同源 —— 实测 _pool/c0182.jpg 是织物,
而选片表 c0182 是 IMG_4190。用缩略图核内容 = 双重不可靠。)
正解 —— 三步,全程不靠眼睛认字:
- 先建 OCR 索引(一集一次,EP08 是 2055 条):
07_work/ocr_epN.py→ocr_epN.json
={原图路径: [识别文本, …]} - 搜关键词,且要搜「单字」 —— 竖排匾额 OCR 会从右往左读出:
「萱瑞堂」→堂端萱/堂瑞萱;只有横排才读成萱瑞堂。
⇒ 搜萱一个字,两种读法一网打尽(EP08 命中 10 张:3799/4119/4120/4121/4122/4123/4125/4274/4275/4276)。 - 命中后仍要放大复核(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 = 高 > 宽 × 1.05(近方形不算竖)。 blur_fill()这类「竖图两侧补模糊」的做法整段删除 —— 用户明确否掉:「必须竖版三联屏,
不给画面两边留白,否则观感不好」。make_plates里one()不许静默降级:返回'VERT_SINGLE'/'HORZ_IN_TRIPLE'让主流程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),且已过同一门槛,不是废片
⇒ 同一批素材可以切成多条不同选题,不必等新素材。
但有两个必须量化的风险,靠感觉判一定翻车:
- 画面撞车 —— 百家号把「批量发布重复或无意义内容」明确列为流量作弊 ⇒ 两条片子的用图重合度必须 < 30%
- 体量不够 —— 某个「角度」看着成立,实际只有 30 张
工具:工具_素材池切面.py(用池内客观字段,不读图、毫秒级)
$PY 07_strategy/工具_素材池切面.py --pool <集>/07_work/照片池_<集>.csv \
--used <集>/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 |
女 | — | 陕西方言 |
★★★ 本节的三个坑(都踩过)
- 逐句合成必须裁首尾留白。 TTS 每句自带 0.3~0.5 s 留白,几十句累积就是几十秒
「白给的停顿」,语速会莫名掉下去。脚本已默认裁(--no-trim可关)。
★ 裁法只能「反转→裁→反转」 ——silenceremove的stop_periods不是
裁尾部静音,而是「检测到第一个静音就截断到结尾」,实测把 530.8 s 正文一刀
砍成 87.5 s。别用它裁尾。 - 音色换了必须重跑校准。 各音色基准语速不同(云扬比云健快 6%),
拿一个 rate 通吃会让片长漂。 --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 生成 · 画面为真人实拍 |
二、显式标识(那处 ①)的三条硬指标
- 必须落在「起始画面」 —— 不是结尾、不是中段。发布后平台与公众第一时间要能看到。
- 字高 ≥ 画面最短边 × 5%。1920×1080 ⇒ ≥ 54 px。
⚠️ 这一条要按实测字号算,不能凭"看着挺大" —— 不同字体、不同 index,
同一个size=参数出来的实际字高能差 20%。渲染完量一遍再放行。 - ★ 字号会被「责任转移」悄悄拉低 —— 这是最容易翻车的细节:
标识牌原本是「大标题 + 小说明」两行(大标题 72 px 负责过线、小说明 40 px 不用管)。
一旦按审阅意见去掉大标题、只留小说明,那行 40 px 就顶不住 54 px 的线了。
⇒ 删掉大字的同时,必须把剩下的字同步放大到过线(实测 40 px → 60 px 才够)。
任何一次「去掉某块文字」的修改,都要回头重算剩下的字还够不够。
> 不要用平铺水印代替标识 —— 全帧铺满半透明文字既难看、又影响观感,
> 且平铺件往往字号极小,同样过不了 5% 那条线。标识是一块专门的排版,不是滤镜。
三、尾板必须换,而且真人版原件一个字节都不能碰
AI 版与真人版会共用同一套尾板生成脚本(排版逻辑一行不用改),差异只有文案。
⇒ 不要覆盖原配置,用环境变量开新一路:
# 真人版(默认,不传环境变量时的行为 = 与改动前逐位相同)
python scripts/credits.py build --bg-photo 01_source/photo/<底图>.jpg
# AI 版(新开一路,另存一份文案档)
CREDITS_JSON=work/credits_ai.json \
CREDITS_MP4=04_out/尾板_署名_AI.mp4 \
python scripts/credits.py clip --bg-photo 01_source/photo/<底图>.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%。
> 竖版尤其容易翻车 —— 短边变小、线跟着抬高,横版刚好的字号挪到竖版就不够了。
⇒ 规则:凡改画幅/改字号/删文字块,回头实测一遍字高再放行。
四、竖版落地清单(六步,缺一步等于没验)
scale=1080:-2整幅缩放,不裁切- 画面居中偏上,下区留白 ≥ 600 px 避平台 UI
- 角标/标识重排 —— 横版的位置不能照搬(短边变了,相对位置全变)
- 实测字高 ≥ 54 px,且留余量
- 抽帧看:字幕完整 + 标识在 + 没被切边
- ★ 响度当独立成片重新核 —— 竖版是另一支片子,
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 ★ 文案阶段:立意优先 + 字数时长必须脚本数
① 写文案前必须回读选题方案。 用户为本条纠正过两次:
> 「做这个视频做选题的时候就有立意的呀?」/「这个节目是根据……开始立意来做的!跑题了吗?」
文案是从「立意」推出来的,不是把查到的资料排成队。 落笔前先回答:
- 全片那句落点是什么?(一句话,能背下来)
- 每一段在为那句落点做什么?答不出来的段落就是跑题,删掉或重写。
- 🚨 钩子必须和立意接得住——把引子抽掉后读一遍,如果变成"一篇没有结论的考据",就是跑题了。
② 旁白稿格式约定:正文用 <!--BODY_START--> / <!--BODY_END--> 包住;
> 开头的引用块放「〔画面〕+ 核查注」(不念),脚本会自动剔除。
> 🚨 2026-09-27 事故:《苏州博物馆》EP1 把引文塞进了引用块,字数少算 172 字、时长少算 76 秒。
> 引用块在本约定里是「不念」的容器(画面提示、核查注放这里是对的)。
> 但凡要念的字加了 > 前缀,脚本就会按约定把它剔掉——而且不报错、不报警。
> 本片 4 段引文(张欣录音、贝聿铭原话 ×2、普利兹克评语)共 172 字全部要念,
> 却被当成"不念",页首因此写着「1,014 字 ≈ 6′23″」——
> 真值 1,186 字 ≈ 7′28″,口语化后录音稿 1,262 字 ≈ 8′53″,与「6 分钟」差了近 3 分钟。
>
> ★ 铁律:要念的引文,一律写在正文里(用 「」 包裹即可),绝不给它加 > 前缀。
> ★ 自检动作(核完字数必做):把脚本剔掉的引用块逐条念一遍问「这句要不要念」;
> 只要有一条要念,口径就已经错了 —— 把它移回正文,重算。
>
> ★ 定性要说准:这次不是脚本错,是我把输入放错了容器。
> 脚本忠实执行了约定 —— 所以「验证工具」不够,还要验证「输入是否符合约定」。
> (对比:《寒山寺》文案 v4 的引文写在正文里,同一个脚本算得准 —— 证明脚本没问题。)
③ 字数与时长一律脚本数,绝不手数:
~/.workbuddy/binaries/python/envs/default/bin/python \
~/.workbuddy/skills/photo-film-director/scripts/count_narration.py <旁白稿.md>
# ★★★ 要往录音稿里贴「分段表」时,加 --md —— 直接产出可粘贴的 Markdown 表
~/.workbuddy/binaries/python/envs/default/bin/python \
~/.workbuddy/skills/photo-film-director/scripts/count_narration.py <旁白稿.md> --md
> ### ★★★ 2026-10-04(EP06):新增 --md —— 禁止再手抄脚本输出
>
> 病根:本技能早就写着「字数只允许有一个来源 —— 脚本产出的那张表」(见下方 ④ 前的粗体条),
> 但没有工具能产出「那张表」 ⇒ 于是 EP04/EP05/EP06 连着三集都把 stdout 手抄进录音稿。
> 手抄必错(抄错数、抄漏段),而且改了稿下次不会同步(表里是死的)。
>
> 修法:count_narration.py 加 --md,末尾多打印一张分段表(段/字数/纯念/停顿/合计 + 合计行)。
> ⇒ 录音稿里那张表一律粘贴生成,永不手打。
> 回归:不加 --md 时 stdout 逐字节与旧版一致(只多一个 flag 分支,不改口径)。
>
> ★ 配套放置位置:这张表必须放在 <!--BODY_START--> 之前。
> 放在块内会被算进正文(表格行虽被 skip_line() 剔掉,但块内出现 ## 会把分段逻辑搅乱);
> 放在块外则整段不进 re.search 的捕获组,零风险。
> 自检一行:python3 -c "raw=open(p).read(); print(raw.index('| 段 | 字数') < raw.index('<!--BODY_START-->'))" → 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 必须包 <!--BODY_START--> / <!--BODY_END-->」这条,相册电影线一样适用。
> 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 修正):<!--BODY_START/END--> 之外一律不算;
▸ 贴片位标记不算(🆕 2026-09-26:原先漏了,把 19 行贴片标记 + 11 个段名当成了正文,
《寒山寺》一度算出 1845 字,真值 1409 —— 配 2.65 字/秒高估 2 分 44 秒);
# 标题/段名、> 引用块、| 表格行、--- 都不算;
〔...〕 方括号提示不算(那是记号,不是要念的字);〔停 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%),自写脚本没有这个背书。
★ 铁律二:录音稿必须写 <!--BODY_START--> / <!--BODY_END-->。
不写的话脚本只能退回全文核算(会 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 产出一张独立的表,放在 <!--BODY_START--> 之前。
> (该 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 用户明确要求:「全文口语化一些吧」) ——
书面旁白稿 ≠ 录音稿。用户要念的是一段话,不是一篇文章。 三条手法,实测有效:
- 开场换悬念,别用新闻开头。新闻导语式开场(「X 月 X 日,某省宣布……」)一念出来就是宣传片,
与「不做新闻宣传式」的调性自相矛盾。先抛两个具体的东西制造反差,把新闻压到第 3 句之后。
> 实例《两座金山》:①「扬州湖心一座小山/镇江江边一座山,名字只差一个字」→
> ②「更怪的是——扬州那一座,它不是山,是人堆出来的」→ ③ 新闻才出场。 - 加口语连接,每拍 2–4 处:你算算/什么意思/你先别急着笑/换句话说/你看。
密度高了一定油,它是盐不是菜。 - 允许单句成段:「不高。」「赢了。」「但名字肯。」——
念的时候自然就慢下来了,比写「〔停 1 秒〕」更管用(二者可叠加)。
★ 时长听用户的,不擅自砍内容(2026-09-23 原话「只要吸睛,不怕长!」):
篇幅由内容定。要压就把「删哪句 / 在哪 / 省几字」列成表放稿尾并换算成秒,
再附一句"我的意见是别删"+ 理由,让他自己选。不要先斩后奏。
★ 第一人称纪实里「体感」不代写(2026-09-24《张家界》):
凡是「那天站在那儿,什么感觉」这类句子,只留一个 〔★ 现场感受位〕 空位让用户自己说。
AI 替他感受 = 自毁第一人称纪实(他的原话是「我不替你写」被用户接受为规矩)。
但空位要给足提示:"说一句就够/说完停 1 秒接着往下念/想不出可跳过,不影响逻辑"——
别让他对着空位卡住。留空位 ≠ 甩包袱。
★ 录音稿尾部四件套(全部标注「不念」):
① 可删清单(删哪句 / 在哪 / 省几字 ≈ 多少秒)② 合规自检表(评价政策?/为景区说好话?/宗教/站外导流,逐条打勾)
③ 录音环境 + 念错处理(见上文)④ 录完之后我做什么(1→N 步,让他知道稿子交给谁、下一步等什么)。
> 第 ① 项的顺序:从省得多的往下排,并单独标出「哪一条强烈建议别删」。
> ⚠️ 删减表若从别处(如文案 §六)抄来,必须回查到录音稿里确有此句,且对仗句/骨架句不许进表
> (《张家界》就遇到:旧表列到「一边…一边…」那两句骨架,已在录音稿里纠正)。
交付方式:01_source/录音稿_<片名>_纯念版_vN.md(与文案同目录,方便逐句对照)+ 字幕另出。
别只留一份"最终版" —— 文案留着做核查底稿,录音稿稿首写清「这份是念的,那份是核的」。
★★ 贴片位(资料卡 / 公版图)必须标进录音稿(2026-09-26《寒山寺》新增):
片子若有贴片位表(《寒山寺》19 处),录音稿要在对应句子的前一行加
▸ 贴片 #N|内容(停留 Xs)。
- ▸ 行不念 —— 写进稿首约定表,否则用户会把它念出来。
- 一律标在「触发句之前」(贴片在念到下面那一句时出现)。
⚠️ 实测过我一份稿里写了两套规矩(13 处放前、6 处放后)——同一份稿只许一套。
- 为什么必须标:位表锚在句子上,录音稿是唯一能对上「用户实际念到哪」的文本。
不标 = 后期只能靠听,19 个落点全成手工活。
★★ 录音稿交出去之前,必须跑脚本复核(不能肉眼扫):
| 查什么 | 判据 |
|---|---|
| 段号连续 | 1..N 无缺无重 |
| 贴片位齐全 | 编号排序 == 1..N |
| 贴片锚点位置 | 每个 ▸ 之后的第一句,必须命中位表里的触发句(脚本核) |
| 文案关键句覆盖 | 抽 80–100 个特征词,命中率应 ≥ 98%(不中先怀疑探针写错,再怀疑漏句) |
| 字数 / 时长 / 镜数 | 见下两条 |
| 旧数字 / 内部术语残留 | 干净(见下条) |
★★ 交付稿里不许出现只有我们自己懂的术语(2026-09-26):
「钉子」「落点」「锚句」这类内部叫法,念的人看不懂。
→ 写进 〔〕 提示时一律换成人话:
「全片第一个钉子」→「六个字放慢,一个字一个字说」;
「〔一个钉子〕」→「这句要落稳」。
★ 判据:交付稿的每一行,都要能假设「用户完全不知道我们的工作流程」还读得懂。
★ 提示与停顿要分行写(2026-09-26):
念法提示和停顿时长混在一个 〔〕 里(如 〔"X"这句别赶;念完停 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 阈值 + 合并间隔 < 0.30 s → 语音块(块边界即静音,天然适合下刀)
# 2) 逐块导出 wav(尾部补 0.25 s 静音,防模型吃掉尾音)
# 3) 8 线程并行 whisper-cli,每块一次调用 → 得到「每块到底说了什么」
whisper-cli -m <模型> -f blocks/b0007.wav -l zh -t 2 -np -nt -otxt -of blocks/b0007
收益:块时间戳是实测边界而非插值,重录段一眼可见。
本次 234 块 / 8 线程 ≈ 1 分钟跑完全片。之后 EDL 直接用「块号」表达,不再写秒数。
两个必配的招:
- 可疑块必须无 prompt 再转一次 —— prompt 会把幻觉"喂"进输出
(本次多块在纯噪声上吐出作词:李宗华、小红桥,小红桥,石狮子...=prompt 回吐)。 -
用相对电平判噪声块 —— 全片块 RMS 取中位,低于中位 9 dB 以上 = 噪声块
(本次 5 块 −31~−35 dB,全是幻觉)。
⚠️ 边界:−6~−9 dB 的弱块是真的轻声说话(本次"不高。""个子远""才搬上来"都在这一档),别删。 -
★★ 低电平只是"嫌疑",不是"判决"(2026-09-24《张家界》三条差点误删的命案):
| 块 | 相对中位 | ASR 说 | 真相 | 怎么查出来 |
|---|---|---|---|---|
| 62 | +4.9 dB(电平完全正常) | 「嵩梁山」 | 「飘过」("云海从脚下飘过") | prompt 里有"嵩梁山"→ 幻觉回吐;无 prompt 重转 = "飘过" |
| 93 | −15.1 dB(像噪声) | 「雲啊」 | 「赢了」(轻声说的,全片关键词) | 带/不带 prompt 两套独立转录都是"赢了"才认 |
| 107 | +5.8 dB | 「三。」 | 「山」("山从来没动过"的头一个字) | 与下一块「我从来没动过」连读才通 |
判据:噪声块必须「两套独立转录 + 位置语境」双证;只看电平必然出错。
-
★★ 大块(> 20 s)必须二级细分再转 —— 底噪偏高的停顿会把好几句话并成一个
20–50 秒的大块(本片块 82 = 49.9 s),模型转长块会漏句
(块 82 就漏了「同一年,他还替这一片地貌,挣回了一个自己的名字」整句)。
做法:块内取中位、阈值 = 中位 − 10 dB,重切子块逐块转(sub.py block <块号>)。
漏一句就可能让剪辑漏掉一整句旁白,而且验收时看不出来。 -
要删的句子落在块内部(块内切点)时,别猜静音点 —— 用「试切 + 无 prompt 转录」逼近
(sub.py asr <起> <止>/sub.py probe <起> <止>):
从候选位置取一段转出来,看开头第一个字对不对,逐步挪到正确边界。本次三处落点:
段标题「1994年,他换掉了自己的名字」→ 切 190.70;「那年冬天。我到了那儿。」→ 切 344.95;
重复句「可他本来是一个村的名字」→ 删 100.74–106.26。
> 别在低电平处硬切:底噪高的录音里"最安静的点"也可能只有 −48 dB。
> 切在字与字之间、再加 15 ms 淡入淡出,比追求"切在纯静音里"更实际。 -
⚠️ 本机环境三个坑(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 取代 —— 用块号表达删除量时不需要换算,也就不存在换算误差。
> 但本节的核心纪律(逐刀核对、不许通读)依然成立,且新增一条硬验收 ↓
★ 剪后硬验收(两关,都要过):
- 残留重复数必须归零 —— 剪后重转,再跑一遍重复检测
(≥4 字 / 间隔 3–40 s)。只剩录音稿本身的合法重复才算过;
本次剪后剩 3 处,逐条对稿确认是"一对狮子/一盘棋/何惜乎小"的合法复现 → 才算过。 -
剪后全文与录音稿逐句对读 —— 用「段间隔 > 1.3 s 视为断句」聚合成句再读,
确认没有缺成分、没有串词、顺序与稿子一致。 -
★★ 剪后验收有两类假警报,必须先排除再动刀(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 "highpass=f=85,volume=7dB,loudnorm=I=-18" \
-ar 16000 -ac 1 seg.wav
whisper-cli -m <模型> -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 >= _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
> # 对 prosody['speech'] 逐段:短段(<1.2s) + 后隙(<1.0s) + 后段更长 ⇒ 疑似假开头
>
> 同时在全片转写里检索关键串,确认出现次数符合预期
> (东湖 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:<音频> <输出.json>
$PY $KIT/whisper_cli_words.py 旁白_16k.wav 字级.json --prompt "专名…"
# ③ ★★ 逐语音段打标签(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 && echo "✅ 完全一致"
# 5b. 回 prosody 扫「假开头」+ 全片检索关键串出现次数
# (短段 <1.2s + 后隙 <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 <音频> <输出> 是 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 帧对齐
# 末镜吸收帧对齐误差,保证整片总长精确
三条要点:
- 句与句之间本来就有停顿,把它吸收进上一组(当作呼吸)——
不要让画面停在某句话中间。这样切点 100% 落在句界。 - 画面在旁白说下一句的「同时」切换,观感上就是「话到画到」。
- 目标镜长 2.5 s:
n = round(span/2.5);若n==1 且 span>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 处 |
| 过短镜 | < 1.8 s → 0 处 |
| 过长镜 | > 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。
★★★ 先记住三条实测结论(都反直觉)
- 裁切不是问题:源 3:2(1.5) 与格位 16:9(1.778) 接近,cover 只损失 15–18%,
六种版式全部保留 80–82%。「横拍可合理裁剪」成立且代价极小。 - 总量守恒:画幅 207 万像素固定。九宫格每格 0.21 MP(单张的 11%),
但一屏总量 1.89 MP ≈ 单张 1.91 MP → 多图不是加法,是重新分配:买广度,付深度。 - ★★★ 百叶窗不是"增加信息量",是"止损":
竖图单放 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,完播率与情绪同时掉。
- 单张永远是主力:照片电影的力量来自"凝视",多图是重音,不是节拍器。
★★ 三条纪律(用错会毁片)
- 同类才拼。9 件玉器 ✅;玉器+建筑+街景 ❌ 大杂烩。
实测:混拼像"相册乱翻",改同类立刻变成展柜墙 —— 差别只在这一条。
> 展品照天生适合拼版(单件居中、背景统一),风景照拼版必然乱。 - 概括句才配多图,特指句绝对不行。
判据:旁白里出现「这/那/它」(特指)就不能多图 —— 观众找不着说的是哪一件。
✅「这个馆里有三万件藏品」/❌「这只碗上画着缠枝莲」。 - 有字的图不许裁:碑刻/匾额/书画/展板 —— cover 会正好切掉落款、年代、署名。
只能单张不裁(contain + 底)或并排(并排裁的是宽,字通常在中间)。
★ 工程实现:零改造
拼版在素材预处理阶段做完 → 输出一张普通照片丢进 work/w/ → Ken Burns 照跑。
kb.py / render.py 一行都不用动。 镜表加两字段:
{"n": 88, "seg": 4, "ph": "g0088",
"grid": "3x3", "plist": ["c0405","c0410","..."],
"mv": "breath", "spec": 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)。
定案:画面内一律不标日期;日期全部收到片尾「拍摄」行。
理由(第一条是实测数字,不是审美):
- 「标了=没标;想真标就得毁画面」—— 中间没有可用档位。
双联每格 940 px,视频号手机满宽 390 pt ⇒ 每格 195 pt,系数 0.2074。
30 px 角标落到手机上只剩 6.2 pt(可读下限 11–12 pt);
要手机可读需 ≥ 55 px —— 那是主标题的字号,日期反成画面里最响的东西。
⇒ 复算方法:scripts/grid.py demo4(含全幅与手机两版对比图)。 - 放角标是主动指出接缝,不是缝上接缝。
旁白里没说"同一天",观众本来不问;两个日期一并排,观众第一反应是
「为什么是两天的?」——注意力被引到一个我们不打算回答的问题上。 - 合规落点本来就在片尾。 尾板「拍摄」行是合规三件套之一。
⇒ 片尾写法:逐日列出,不写区间。
| 情形 | 片尾「拍摄」行 |
|---|---|
| 同一天 | 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('DateTime') |
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('\ufeff镜号')。
csv.DictReader 下 r['镜号'] 恒为空,于是「逐版生成」变成静默产出 0 帧、脚本还打印成功。
⇒ 一律 open(..., encoding='utf-8-sig'),并在读完后 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 <D> -i card.png \
-vf "scale=3840:2160,zoompan=z='min(zoom+<Z>,<ZEND>)':d=1:\
x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1920x1080:fps=30,setsar=1,format=yuv420p" \
-c:v libx264 -preset medium -crf 19 -an seg.mp4
# <Z> = (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 < k: return (u*u)/(2*k) / (1-k) if u > 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 < 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/素材缺口台账_<地点>.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/<镜号>.jpg 是台账 plate_sha1 的取证对象,覆盖它 = 素材链断掉、台账作废。
标注版另存 <镜号>_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):
"""运镜「全程可见矩形」(返回图内分数 x0,y0,x1,y1)。
窗口 = (W/z)×(H/z),y 起点居中后 ±amp,两端都要 clamp 到 [0, H-H/z]。"""
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='mm',逐层收窄)。
真实航拍画面里白墙/灰屋顶/绿树并存,单层描边必在其中一块背景上糊掉;
三层 = 任何背景上都有一道分离带。箭头同样画三遍(线宽/头宽/头长逐层收)。
主色取朱红(中式地理标注/朱批调性),字号 ≈ 0.040 × W(比字幕大 60% 才够醒目)。
④ 箭头要「钉在底图上」,不是钉在屏幕上。
标注烧进底图 ⇒ 随画面一起移动,观感是「贴在地上」;烧在输出帧上 ⇒ 摇镜时箭头会乱指。
屏幕固定只适合完全静止的镜头。
⑤ 改了画面就必须重跑终混。 合成是 -c:v copy —— 只换片段不重跑,
母版里的画面还是旧的。完整链:
shots --only <镜号> → 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=<增益>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 "highpass=f=${r%%:*},lowpass=f=${r##*:},volumedetect" -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=<扫>: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
> # 只体检(不动文件)——先看有没有病
> python voice_fix.py 旁白.mp3 --probe --tail 554.5
> # 出三档 + 试听对比样本
> python voice_fix.py 旁白.mp3 --outdir ./处理后 --tail 554.5 --demo 413.32:16.5
>
> --probe 会直接把三个关键频段判成 ✅/⚠️,不用自己对表。
> 尾巴 --tail 用 voice_check.py check 报的语音结束时刻(切掉忘按停的空白)。
两个工具坑(都实测踩过)
lowpass的截止频率永远不要设在 Nyquist。
44.1kHz 素材上写lowpass=f=22050,滤波器数值退化自激,
测出「16–22kHz = −0.4 dB」(比全文件峰值还高)这种不可能的数据。
→ 测频段时上限最高写到 20000;真实值(重测)是 mean −80.3 / max −39.2 dB。
★ 凡是量出「比峰值还高的分频能量」,先怀疑滤波器,别怀疑素材。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 = [("在场", A, T_D0, -33.5), ("保持失真", 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| > 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<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('Summary:'):] |
| ★★★★ 在「母版」上量单路指标 | 母版级「真静默窗中位」量的是背景合成(音乐占主导)⇒ 环境声让路从 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 "环境声 v4:0.0:环境声轨_EP1_v4.wav" --track "配乐 v4:-11.6:配乐轨_EP1_v4.wav"
$PY 07_work/bed_level_ep1.py --bed "配乐:-11.6:配乐轨.wav" --bed "环境声:0:环境声轨.wav"
(增益参数必须写混音链里实际给的那个,否则判据不对。)
④ 定点修什么 —— 每一处都由判据反推,不猜
| 症状 | 实测根因 | 修法 |
|---|---|---|
| 分带让路只有 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 ★★ 封面不许出现的两样东西(比好看更重要)
- 新闻截图 —— 版权。热点选题时这条最要命。
- 钩子元素在素材里不存在时,不许硬凑 ——
让文字承担钩子、画面承担主题。
实例(寒山寺 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):
- 先自我表态(「先说我的答案:正常。猫可爱,人人举手机」)
→ 卸掉敌意,堵死"你在 diss 游客"这个解读。 - 转话题到片子主体(那口钟、张继)
→ 给读者一个可讨论的实体,把话题从热点拽回片子。 - 引评论的问题要「能回忆、不用评判」:
✅ 「你去寒山寺,会先看什么?」—— 人人答得上
❌ 「什么最值得看?」—— 让人做评判,门槛高、容易冷场
★ 这条与口播 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/<键>.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 "$PWD" --bg-photo 01_source/photo/<键>.jpg
CREDITS_DUR=9.28 python scripts/credits.py clip --ep "$PWD" --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/(镜号 + '.jpg')。东湖第一版按 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)) < 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_<ep>.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 = '54'。
- ⇒ 镜表今后再改(合并、增删镜、换集),配乐脚本一个字都不用动。
推广判据:写任何脚本前先问一句 ——
「我锚的这个数,会不会因为上游数据增删而改变?」
会 → 它是派生量,去找它背后的不变量(句序/素材 id/sha1),锚那个。
11. 坑清单(都是实测踩出来的)
- 批量
os.remove清理缓存目录会触发安全确认中断脚本 → 改成同名覆盖、不清空。 mkdir(exist_ok=True)在沙箱下对已存在目录可能抛 PermissionError → 先判os.path.exists()。- 脚本若用
argv[1]单选动作,传多个参数只会跑第一个 → 分次调用或用&&串联。 - 剪点若落在语音上,听感上是「吞字」而不是「静音」—— 剪完必须重转验证。
- 点题卡遮幕过重会把照片压死(见 §6)。
- 片尾与末条字幕时间重叠是高频错误 —— 交叉检查。
- ★ 剪接刀口切进保留侧,通读发现不了 —— 「要漫步其间,古代官府是怎么来
运作的」读得过去,却缺了谓语。必须逐刀看刀口处的前后文(见 §3.3)。 - ★ 字幕与录音稿必须是两份文本 —— 字幕不加标点(画面干净),
录音稿必须加标点(标点决定停顿与语调升降,没标点就念成一串没起伏的流水)。
录音稿还要带〔〕提示、〔停 N 秒〕、语气标记(见 §3.0.1)。 - 改一刀 ≠ 只改音频 —— 时轴平移会连锁到镜头表、字幕、章节卡、片尾、
混音、成片。定刀口前先把这一串代价想清楚(见 §3.3 末)。 - ★ 用户的决定不要反复推回去 —— 用预置音色的对比音频去论证克隆音色
的效果,是无效论证,会被用户的实际体验直接推翻。声音克隆这条路用户已经
否决过一次,不要再提(详 §3.0)。做任何演示必须当场标明用的是哪一种技术。 - ★ 纯白水印在浅色背景上等于没有 —— 长文配图的水印必须
白色填充 + 深灰描边(或反之),否则在天空、白墙、浅色砖上完全看不见。
第一版就栽在这,见 §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 "<文件名>"(Spotlight,秒回),
> 别用find遍历六个盘。同名文件可能遍布多个年份的文件夹
> (实测IMG_7347有 10 个互不相同的版本,横跨 2009–2018),
> 必须「夹 + 文件名 + 时间窗」三重限定才敢认定是同一张。 - ★ 图表/按钮的位置说明不要只给名字 —— 没有文字标签的纯图标按钮,
必须给「图标长什么样 + 相对位置 + 相邻元素」,并裁剪放大用户截图确认
(见 §12.4(2))。给错位置会让用户白找两轮。 - ★ 一稿多用要真的出第二套文件,不要只在文档里写「记得改标题」——
标题/开头/水印/版权段四处一起改,独立目录,并写清「两套图别混用」(见 §12.4.1)。 - ★★
ffmpeg -filter_complex里多一个逗号 = 整个任务报废。
fc.append(f"{prev}," + ",".join(fx) + "[vout]")会拼出[v3],fade=t=in:...——
逗号后面被当成一个空滤镜,直接No such filter: ''。正确写法是f"{prev}" + ",".join(fx)。
为什么难发现:这条分支只在「挂片头淡入/片尾淡出」时进入。
写预飞自检时图省事把fade_in/fade_out填成 0 → 正好跳过唯一有 bug 的分支 → 自检全绿、正式跑必挂。
> 铁律:预飞自检的参数取值必须是正式跑的真实值。 图省事填 0/空/默认,等于没自检。
> 自检要故意走遍「有/无」两条路。 - ★★ 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 最多损失当前一批。复用核 文件大小 —— 大小变了就是一票否决。
> 省的是几分钟,赌的不能是成片。 - ★★ 复核口播/旁白「念的是 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 <起点秒> -t <时长> -i 录音.wav -ac 1 -ar 16000 /tmp/w.wav /opt/homebrew/bin/whisper-cli -m "$B" -f /tmp/w.wav -l zh -ng -np 2>/dev/null
base 在带方言/古语的口播上会明显写错字(「壶园」→「湖源」、「咸丰同治」→「闲风从至」)
→ 绝不能整句照抄,它的用法是只取「二选一」那一个词。
判据:① 先把 ASR 容易错的地方逐条列出;② 若全数是 ASR 听错、稿件没错
→ 说明本录音的 ASR 不可信,孤证不足以推翻稿件;③ 只有两套独立 ASR 在同一处指向同一个词,
才认账 —— 此时是人念错了。
处置:字幕是录音的转写,不是稿件的复印件。 证据够了就必须改字幕跟音频走
(观众「听到的」和「读到的」对不上,比写错一个字更刺眼)。
改法写进代码里的差异表(如make_subtitle.py的AUDIO_FIX,可复现、可 diff),
不是手工改成品;改完必须 diff 一次,确认只有预期的那几行变了。 - ★★ 编号只允许程序生成。 手写的「时间 → 编号」对照表必须回候选表对账
(本次整表错了一格:拱门标成 Y241、小红桥标成 Y242,真值是 Y242=拱门/Y243=小红桥)。
引用具体画面优先用文件名(IMG_1734.jpg)而不是编号 —— 本片成片没被污染纯属侥幸,
因为镜表引用拱门用的正是文件名。发现错格后必须回查所有已出产物(封面/片尾/镜表/字板)。 - ★ 出片流水线要做成一条幂等命令(见本项目
02_work/build_final.sh):
拼接 → 烧字幕/引文板 → 贴片尾 → 挂音轨 → 补色彩标记 → 体检。
每步都可重跑,上游有台账就直接跳过。别手工一步步敲 —— 中断一次就得从记忆里重建整条链。 - ★★★ 「中间产物正确」≠「成品正确」—— 叠加类工序必须回读落盘成品来验。
真实事故(2026-09-24《两座金山》):成片参数七项体检全绿
(514.9 s / 1920×1080 / 30 fps / 三色彩标记齐 / −14.2 LUFS / TP −2.0 dBTP),
但画面里一条字幕都没有,只有引文板在。- 根因:
loaded.sort(key=lambda x: (x[2] != "P", x[0]))把 7 张板排在 113 条字幕前面,
而逐帧循环里写了if t < a - 0.02: break—— 遍历到第 2 张板就跳出整个循环,
排在板后面的字幕永远轮不到。实测 t<449 s 的 110 条字幕全丢。
> 铁律:列表按「复合键」排序时,任何「提前 break」的优化都不成立(它默认只按单键升序)。
> 要么改成continue,要么另建一个只按起点排序的索引。 - 为什么没被发现:体检全是容器/流参数层面的,没有一项看画面里有没有字;
而我先前验的是字幕 PNG 本身(张张完美)和引文板(板是好的)—— 验的全是中间产物。 - 修法(两道,缺一不可):
① 加叠加统计:字幕帧数/板帧数各记一份,任一类为 0 直接 raise;
② 加回读成片自证:抽 8 条字幕,在每条中点分别从成片与源片读同一帧,
数「字幕带」里的近白像素(字幕是白字+黑描边,落上去必然显著增多),
要求成片 > 源 + 2000,任何一条不合格就报错。 - ★ 检查写完必须做反向测试:拿刚做出来的坏文件喂给它,必须报错
(本例 8/8 全部报错 ✅)。 没做过反向测试的检查,等于没有检查。
- 根因:
- ★★ 图文配图的「图号」有两处都要写,两边必须程序化对齐。
真实事故(2026-09-24《两座金山》图文):build_article.py的 PLACE 表按叙事顺序
写了 20 条图注,picks.csv按另一套顺序列了 20 张图 —— 两边不同序,
图注就整段错位(「临水通廊」的说明挂到「殿前庭院」的图上),而每一处单看都合理,
通读发现不了。同源错误还有:① 映射表整表错一格(§11.18)② 手数段落号一改字就错位。- 铁律:图注表不手抄。 用
make_caption_table.py从 PLACE + picks 程序化拼,
并断言len(PLACE) == len(picks),不等就报错。 - 插图位置用「锚点句」不用「第几段」:
('堆着堆着,堆成了一座岛', 4, '…');
脚本按子串找段,命中数 ≠ 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的<w:drawing>数 =
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 才算改完。
- 铁律:图注表不手抄。 用
- ★★ 大批量复制(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(含{路径: 字节}对账与人类可读汇总)。
-
★★★ 缓存编号(
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"{ph}|{mv}|{dur}",其中ph是缓存编号。扩池后编号没变、映射变了
⇒ 指纹一条都没变 ⇒ 210 镜全部 skip ⇒ 拿着新镜表合成的成片里装的
还是旧画面,而时长、帧数、镜头数全对得上(这是最毒的地方:所有体检项都绿)。
- 正确指纹:f"{ph_map_md5}|{src}|{ph}|{mv}|{dur:.3f}",
其中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)
→ 成片实抽帧放大逐字读。三者一致才算核过。
- 曲名/署名类错误 = 授权违规(历史红线),不是笔误。
- 报告里"未闭合项"要敢留白:真源查不到就写"查不到 + 已做的否定性检验 + 为什么不阻塞发布",
不要编一个看起来对的答案填上。 -
★★★ 剪旁白:「旁白删 N 秒」必须「画面也删 N 秒」,且两边都要落在整帧上。
只删旁白 = 其后旁白整体提前 N 秒而画面不动 → 从刀口起旁白讲在画面前面,越到后面错越多。- 算法:
N 秒 × FPS必须是整数帧。本次7.000 s × 30 = 210 帧
→ 删 3 镜 × 70 帧/镜(s176+s177+s178)恰好 210 帧,音画各删 210 帧,差 0 帧。 - 落点:切点必须选在实测数字静音或 −55 dB 以下(
atrim+ 两端 20 msafade)。
先跑 10 ms 分辨率电平表找到静音窗,再定刀口 —— 不要按 ASR 的时间戳切
(ASR 会把前后静音算进时长,本次差点据此切掉 4.34 s,实测重复句只有 1.55 s)。 - 留气口:切完要保证「上一句收尾 → 下一句起音」仍有 ≈0.5 s 以上呼吸,否则听感是硬接。
- 脚本要自证:
abs((B-A) - 秒数/FPS) <= 1e-6不过就拒绝执行(见本例work/apply_v4.py)。 - 回退:动手前把 成片 / 旁白 / EDL json / srt 四件归拢到
_废弃/对比_改前/(用mv,不用rm)。
- 算法:
-
★★★ 做电平/内容扫描,绝不能带关键字过滤 —— 滤掉的正是你要找的数据。
真实事故:查「464.7–468.8 s 之间到底有没有人声」,脚本里写了「只打印含『乾坤柱』的帧」。
语音帧频谱均衡、不集中在任何单频,被关键字条件整片滤掉 → 读数像"这段几乎没人声"。- 重跑:不设任何过滤,只打原始 dB 值 → 立刻看到「乾坤柱」在 464.1–464.8、「南天一柱」在 465.3–466.15。
- 规矩:扫描 = 只收敛输出格式,不收敛数据本身。 要筛,也在拿到全量读数之后另写一步筛。
- 同类:判断素材是否存在用
os.listdir+ 三重限定(夹+文件名+时间窗),别靠文件名索引。
-
★★ 改字幕必须整串精确匹配(
txt == 目标),子串匹配会连坐。
真实事故:any(k in txt for k in ("乾坤柱",))本意只想改第 120 条,
结果同时命中「那座山现在叫乾坤柱也叫南天一柱」和「本地人叫它乾坤柱」
→ 三条全被改成464.12 → 466.72,其中两条变成 2.6 s 的非法条目。- 同理:"防重叠"的守卫要判真重叠(
t0 < prev_end - 0.001),
不能写成t0 = max(t0, prev_end + 0.03)—— 后者会把全表每条都推后 0.05 s。 - 改完必须 diff:条数不变、只有预期那几条动、末条仍在片长内、无负时长。
- 同理:"防重叠"的守卫要判真重叠(
-
★★ 改引擎参数必须 bump 版本串,否则指纹不变 → 全镜 skip → 成片里装的是旧画面。
KB_VER(如mv3-wide-20260925→mv4-chain-20260925)在指纹串首。
数字变了版本不变 = 一条指纹都不变 = 渲染全 skip,而时长、帧数、所有体检全绿。- 这是「间接引用」类静默失败的第三次(前两次见 23 / 24)。
- 规矩:凡改动会进指纹的东西(运镜表、基准 zoom、映射表、配对表),当次就 bump。
-
★★★ 函数式运镜逃出了「名字指纹」—— 间接引用第四次咬人。
为解决「相邻两镜同图 → 运镜弹回」(14 处 / 76.3 s / 13.6%),后一镜的运镜必须
从上一镜的终点出发,这无法用一个固定名字表达 →render()改为接受callable。- 但指纹原先是
move 名字:连镜后仍报"panL"→ 改了配对数指纹不变 → 静默复用旧文件。 - 修法:指纹里加连镜标记
#c<上游镜号>
→f"{KB_VER}|{maphash}|{src}|{ph}|{fp}|{move_of(s)}|#c{ch}|{dur:.3f}"。 - 连镜的两条实测教训:
(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 偏移)。
- 但指纹原先是
-
★★ 字体路径必须先验存在;「降级成功」比直接报错更坏。
真实事故:对比图脚本写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。
- 修法:① 换成实测存在的
-
★★
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 会削顶穿透)。
- 修法:按实测 TP 反推,目标定 −1.8 dBTP(给 AAC 抬峰留余量)。
-
★★★ 判据改了,必须【全量重跑】,不能只作用于增量。(本项目最贵的一条)
真实事故:定「不选索道」的判据时,白名单注释里逐条列出了排除的编号
(c002 c003 c047 c059 c067 c170 c173)—— 但那条判据只用在新换进来的图上,
没有回溯已经躺在片子里面的镜头。结果这 6 个编号里的 6 镜
(s002 吊椅开场、s063 吊椅上坐两个人、s072 缆索横穿、s198 索道立柱、s199/s202 吊厢)
从第一版就在片里,四轮复核一次都没被这条判据碰过。- 代价:一条「有可辨识路人」的镜头(s063)带进了成片,而它旁边就是肖像权红线。
- 更坏的是它的形态:新增的图干干净净,检查新增的每一项都过 ——
判据生效的证据充分,所以没人怀疑存量。 - 规矩:① 判据落成可执行脚本(如
fix_cable.py),每次改判据对全片跑一遍;
② 判据写进白名单注释不算落地,注释不会自己执行;
③ 交付前肉眼过一遍全片联系表(storyboard.py→ 6 张 320px 表,逐格看)。
> 识别特征:一条判据的"通过率"是 100% 时,先问它是不是根本没被应用到存量上。
-
★★★ 真峰值必须按【最终容器】反推,不能按 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」这个数不要复用 —— 抬峰量随节目素材变,
每次都要用本片的素材真机标定。
- 标定方法(
-
★ zsh 里
$VAR:会被当成路径修饰符展开 —— 参数里紧跟冒号必须写${VAR}。
真实事故:循环写-af "alimiter=limit=$L:attack=0.1...",
zsh 把$L:a解读成:a修饰符(转绝对路径)
→ 参数变成limit=/Users/.../0.794ttack=0.1→ ffmpeg 报错。- 修法:一律
${L};凡是变量后面紧跟:的,全加花括号。 - 同批踩到的另一个 shell 坑:
for里用set -- $w拆两个变量会坏掉,
量测循环一律用 Python 写,不写 shell 循环。
- 修法:一律
-
★★ 量「竖向运镜」必须按【画面高度占比】,不能按源图坐标比例。
真实事故:我报「竖扫族 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(带竖向分量)。
- 正确公式:
-
★★ 重跑
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等体检脚本,
不许继承上一轮的结论。 时长、帧数、镜数全对得上 —— 这类回退不报错。
- 判据:凡是"重生成 edit.json"之后,必须重跑
-
★★ 任何"降档/预览/轻量"版本,音轨必须
-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)。
- 判据:交付物不分"母版/预览",每一件都要单独测 TP。
-
★★ 客户端"报错提示"先去源码里搜原文,按 i18n 键定位,比猜快十倍。
真实场景:用户报「该视频编码格式浏览器不支持在线预览」。
在WorkBuddy.app/Contents/Resources/app.asar里搜这句话 →
命中mediaPreview.videoDecodeError,由<video>的 onError 触发。
兄弟键文案都不同,可据此排除整条路:videoLoadTimeout="加载超时"(15 s 元数据超时)→ 用户没看到 → 不是超时unsupportedVideoFormat="{ext}不支持"→.mp4在白名单CHROMIUM_PLAYABLE_VIDEO_EXTS里 → 不是格式fileTooLarge="文件过大" → 文案对不上 → 我上一轮猜的"体积说"当场被否- 结论:文件本身没问题;失败在"把文件喂给
<video>"那一步
(源码只有两条路:URL.createObjectURL(file)内存 Blob / 直接喂 URI)。 - 可复用做法:
python读 asar →re.finditer找字节偏移 → 打印前后各 2–3 KB 上下文。
-
★ 验证"这个视频浏览器能不能播",要用真实内核+支持 Range 的服务器。
python -m http.server不支持 Range → 会让 seek 失败,把服务器问题误判成编码问题。
改用scripts/range_server.py(60 行,带 206 Partial Content)。- 用
scripts/preview_probe.js(playwright +channel: "chrome"走真 Chrome 内核)读
video.error.code/message、readyState、videoWidth,并且真的play()一下。 - 判据:
readyState === 4且video.error === null= 文件没问题。
(实测:486 MB 与 595 MB 的片子都是 4/null → 体积与编码都不是原因。)
-
★★★
01_source/photo/一律放「软链」,绝不放实拷贝。(2026-09-25 实测纠正)
早期项目(凤凰、西塘)把素材盘原图整目录拷进项目 →01_source/photo/吃 8.1 G + 1.6 G;
后期项目(镇江02_work/build_workcache.py)才是正解:
①01_source/photo/<编号>.jpg= 软链到素材盘原件;②02_work/w/<编号>.jpg= 工作尺寸缓存,
之后渲染只读内置盘 → 素材盘每个文件只读一次。- 为什么必须软链而不是实拷贝:省的空间完全一样(软链 0 字节),
但重跑时立即可用(render.py按01_source/photo/<编号>.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="utf-8-sig",
否则首列名变成\ufeff编号→KeyError: '编号'(本次连踩两次)。
- 为什么必须软链而不是实拷贝:省的空间完全一样(软链 0 字节),
-
★★★ 实拍有缺口时,用「贴片」补,绝不用别的实拍顶替。(2026-09-26 寒山寺案)
- 贴片 = 公版古画/老照片/自制资料卡(引文/日期/铭文),按「动态 PPT」式插入:
宣纸底、自画面一角浮起带投影、缓推 2.5–4 s、人名条左下角小字、淡出叠化 0.4 s。 - 三条素材纪律:① 只认 PD / CC0——
CC BY-SA有传染性(用了就得整片按 CC BY-SA 发布,
对要收益的账号不可接受),一律跳过;② 不用 AI 生图(中文必乱码,本机实测传统题材失败率高);
③ 人名条用 PIL 后期叠,不许 AI 生成带字图片。 - ★ 红线:拍不到的东西,不许拿相近的实拍冒充。 例:本片没拍到枫桥,
只能用山塘街/虎丘的照片顶上——不做,那叫用画面撒谎。宁缺勿假。
- 贴片 = 公版古画/老照片/自制资料卡(引文/日期/铭文),按「动态 PPT」式插入:
-
★★★ 拍不到的,用「记录」代替「画面」——这往往比画面更硬。
- 寒山寺元末焚寺案:没有火的画面。改用「记载六个字(《百城烟水》『元季,寺、塔俱毁』)
+ 人证两首诗(高启《赋得寒山寺送别》『塔中灯照远僧归』/《归吴至枫桥》『不见青山旧塔微』,
自注『旧有塔,今废』)」。 - 妙处在:诗人自己从没说过「寺被烧了」,他照常写景。是两首诗排在一起才说出了那场火。
→ 与全片论点同构(实物没了、文字还在)。这不是退让,是把限制变成方法。 - ★ 具体手法(可复用):两张诗卡并置,中间裂开一道黑缝——那道缝就是那场火。
旁白说「火就在这两首诗中间」的同一秒裂开。文字/画面/声音三件事咬合在同一秒。 - ★ 声音:钟声 持续 → 失真(低通缓慢扫频 8k→1.2k)→ 回升 → 硬切(0.05 s 急停,不是渐弱)
→ 全静。观众听见钟声「断了」,比看见火烧更有力。 火只以「远处一声」出现一次。 - ★ 全片只此一处结构性断裂/硬切,别处不许重复,否则失分量。
- 寒山寺元末焚寺案:没有火的画面。改用「记载六个字(《百城烟水》『元季,寺、塔俱毁』)
-
★★ 没有可靠画像的历史人物,用「他的著作/他的碑/他的拓本」代替——不要用一张想象出来的脸。
- 判据:明清人凭想象画的"某某像"=造假。张继(约 715–779)没有可靠的传世肖像。
- 替换:张继 → 《枫桥夜泊》诗碑;范成大(考证「定夜钟」)→ 《吴郡志》考点卡;王珪 → 考点卡。
- ★ 顺带的好处:若结尾有回指(本片:「你是去见那二十八个字」),
提前让观众见过那件实物,结尾这句话才有回响 —— 首尾闭环多收一层。
-
★ 取公版素材(Wikimedia)不用 API 也能下 —— 见
~/.workbuddy/memory/工具手册_卡顿排查与记忆瘦身.md§十。
要点:API 全被墙时,原图路径是纯函数(/commons/<md5[:1]>/<md5[:2]>/<文件名>),
知道文件名就能直连;并发会触发 429,要单线程+长退避+Range 断点续传。
11.1 无旁白片(纯配乐相册)复用本链 —— 2026-10-01 闲鱼样片实测新增
做「商品样片 / 客户交付的相册视频」时没有旁白,但本链是旁白驱动的。三条硬骨头与解法:
-
★★★
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(与镜号一致),「素材」列只用来记来源。 -
★★★
cache_photos.py产出的缓存图「不保留 EXIF」(实测exif keys = [],
即使原图有完整的 SubIFD)。想拿拍摄时间必须回原图读,且用get_ifd(0x8769)。
另外scripts/quality.py需要照片池 CSV 里必须有「拍摄时间」列,
否则KeyError: '拍摄时间'直接崩。 -
★★★ 无旁白片的正解 = 喂一条「等长静音 wav」当
--voice,不改通用脚本。
render.py的 assemble 里inputs = ['-i', vis, '-i', a.voice]没有条件判断,
--voice空会崩。但静音走旁白链后仍是静音 ⇒ sidechain 不触发 ⇒
配乐原样进amix⇒loudnorm照样归一化。
⇒ 这样零风险,不会波及博物馆系列。
bash ffmpeg -f lavfi -i anullsrc=r=48000:cl=stereo -t <片长> -c:a pcm_s16le voice_silence.wav
★ 静音时长必须 = 视频时长(amix=duration=first以它为准)。 -
★★★ 出片音频务必走
finalize.py,不要只用render.py的单遍 loudnorm。
同一条片实测对照:走法 综合响度 I 真峰 TP render.pyassemble 内建 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 <片长> --out 成片_v2.mp4
★--music给了就必须显式给--music-gain(脚本会直接报错拦下)。
★ 出片后等+faststart写完 moov 再复测,否则测值全废。 -
★★ 「目录名不可信」再添一例(第 N 次) —— 一批叫
笑笑结婚 沈阳_2019的 599 张,抽 24 帧看是沈阳故宫/博物馆/街景,不是婚礼。
⇒ 任何目录在动手前先make_contact_sheet.py抽帧,30 秒的事。 -
★★ 沙箱会拦「inline 命令里带外部盘中文路径」的 python ——
报sandbox-center cmd decisionRecord missing actual resource subject(不是权限提示,像内部错误)。
⇒ 一律写成.py脚本文件再跑,别用python -c "..."拼。 -
★ 选片「按拍摄时间均匀抽」比「按分数取 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(人脸感知裁切核心件,可直接复用)。
-
★★★ 切头验收必须交给程序,肉眼抽样必漏。
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 # 肉眼联络表只做构图/色调复核
★ 必须区分「哪条边」:脸压上边 = 真切头(严重);
压左/右下边 = 身体出画、头是完整的(远景路人属正常,别当缺陷改)。 -
★★★ 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 ('left', 'right') and span > PAN_LIMIT: return 'static' if move not in ('static',) and span > ZOOM_LIMIT: return 'static' return move -
★★★ 取窗必须「居中」,不能把人脸顶到窗口一边。
旧写法want = lo - pad把人脸顶在窗口起始处、只留 5% 边距,余量全浪费在另一侧 ——
运镜一放大就切。正解:
python want = (lo + hi) / 2.0 - span / 2.0 # 人脸区间居中 → 两侧等余量 want = max(0.0, min(want, total - span)) # 夹紧 -
★★★ 竖拍照片改
duo双联,是本片最大的观感提升。
竖构图进 16:9 只能留 37.5% 高度,下边缘必然切人(358 张里 59 张竖拍);
两张竖拍并排拼 2400×1350 ⇒ 每张只损失 25% 高度,一根头发都不切,
版式还自带「相册感」。婚礼/寿宴的竖拍人像(头纱、相拥、队列)优先走 duo。
python cropkit.duo(['w0086.jpg', 'w0107.jpg'], dst, W=2400, H=1350, gap=14)
另备cropkit.blur():单张竖拍全幅居中 + 高斯模糊铺底,完全不裁。 -
★★ 「主体脸」过滤 —— 不加会误伤,加了要防误滤。
两类假脸会把占窗撑爆,逼着镜头降级:请柬/相框上印的人像(w0012被判占窗 83%)、
满堂宾客的 31 张小脸(w0286被撑到 78%)。
门槛取「3% 图高」与「最大脸的 45%」两者较大值:
python thr = max(0.030 * img_h, 0.45 * max_face_h)
★ 为什么必须带相对项:远景里新人脸也小,纯绝对阈值会把他们滤掉 —— 那才是真主体。 -
★★ YuNet 在白紫色花束/烛台上会稳定误报人脸。
实测 3 处(脸高 6.9% / 10% / 18% / 21.6%,全是花艺镜)。
⇒ 看到「脸高 >15% 却出现在花艺镜」先怀疑假阳性,必须放大看图定案,别直接改片。 -
★★ 人脸检测件怎么拿(cv2 5.x 已删掉 Haar 级联 xml)
bash mkdir -p <工作目录>/models && 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, '', (320, 320), 0.55, 0.3, 5000) det.setInputSize((ww, hh)); _, faces = det.detect(im2) # faces[:, :4] = x,y,w,h
★ 人脸框只覆盖眉到下巴,不含头发 ⇒ 必须外扩(上 45% / 左右 18%),否则仍会切头顶。 -
★ 选片用「实裁预览表」而不是原始缩略图。
把每张照片按最终裁切结果缩成 16:9 拼表,看到的就是最终画面 ——
避免「选的时候好看、裁完才发现切头」。build_previews.py直接出。 -
★ 镜序要照顾「运镜降级」:插入一个可自由运镜的镜隔开。
若相邻两镜都因人脸占窗过大而只能 static,会撞出相邻同运镜。
实测:把「花艺暖光」(占窗 0.00,可任意运镜)插在
「请柬桌花」(0.83) 与「宴会厅」(0.94) 之间即解。
11.3 寿宴样片实测新增(2026-10-01)
-
★★★ 增量渲染必须比 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) <= 20000: return True if plate_path and os.path.exists(plate_path): return os.path.getmtime(plate_path) > os.path.getmtime(clip_path) return False
★ 通用原则:任何「产物 ← 输入」的缓存判断,都要比输入 mtime,不能只看产物在不在。 -
★★ 切头放大图必须按「危险度」排序,不能按时间取前 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 个是按什么排的? -
★★★ 「运镜只会把脸放大,不可能放大出更小的脸」—— 鉴别假阳性的铁律。
成片 t=84/85s 报警脸高 13.0% / 10.5%,而该镜 plate 里最小的脸也有 15.0%。
in/out是 z 从 1.00 推到 1.14,脸只能变大。⇒ 那两张「脸」必然是 YuNet 误检。
再用局部重检 + 极限放大确认:该区实际唯一的脸距右边 12.6%,根本不贴边。
⇒ 拿「plate 的脸高分布」当参照系,比放大图更快定案。 -
★★★ 切脸先辨「裁切造成」还是「原图就切」—— 三处对照法。
分别在成片帧 / plate / 原图上量同一张脸的边距:
| 结果 | 结论 | 修法 |
|---|---|---|
| 三处一致 | 原图拍摄时就贴边 | 改裁切没用,只能换素材 |
| 原图够、plate 紧 | 裁切环节没留够 | 改fit_window边距/取窗 |
| plate 够、成片紧 | Ken Burns 二次裁切 | 降运镜或退 static |
实测(b0186):L 2.3% / 2.3% / 2.2% ⇒ 三处一致 ⇒ 换素材。
★ 横拍 4:3 → 16:9 只裁上下,横向根本没裁,横向贴边一定是原图的问题。 -
★★ 连拍组里换镜:按「四周留白」重扫全组,别只试相邻编号。
九人合影 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.py6.2%;z=1.25 的片要 10%)。 -
★★ 异色场景段(KTV 紫灯 / 舞台彩灯)用「饱和度 + 色相占比」定位边界。
不要靠肉眼一张张看。实测寿宴尾段:
python hsv = cv2.cvtColor(cv2.resize(img, (120, 90)), cv2.COLOR_BGR2HSV) v = hsv[:, :, 2].mean() # 亮度 pur = ((hue > 125) & (hue < 165) & (hsv[:, :, 1] > 90)).mean() * 100 # 紫/品红占比
分界比亮度清晰得多:KTV 段饱和 218–238 / 紫占比 84–93%;正常包间 饱和 74–89 / 紫 ≈0。
★ 异色镜剪进去会「色调跳变」,样片和商品案例一律换掉(不要试图调色救)。 -
★★★
--music-gain用错默认值会逼 loudnorm 退化成 Dynamic 模式。
现象:finalizepass1 落 −39.7 LUFS(正常应为 −19.7),
差 25.7 dB ⇒ loudnorm 无法用 linear 纯增益,报Normalization Type: Dynamic
—— 那是动态压缩,不是纯增益,音质被改。
★ 本系列正确值 =-6(初混落 −19.7 LUFS)。
验证法(最省事):跑完 finalize 看pass1 实测那行,落{'I': '-19.7'}即正确;
落到 −39.x 就是增益用错了,立刻重做,别接受结果。
★ 修法是改初混增益,不是让它去补大增益。 -
★ 镜表备注必须核对画面 —— 写错的 note 会误导后面的判断。
本片 note 把b0133记成「深情拥抱」、b0203记成「拥抱」,
实际前者是两人搭肩合影、后者是女士举花束。
后果:选尾声镜时差点因「已有两个拥抱镜」而排除掉真正合适的b0236。
⇒ 定镜时把 note 写成「画面里实际在发生什么」,不要写成「我想表达什么」。 -
★★★ 别用
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(['ffmpeg','-hide_banner','-v','error','-i',path, '-ac','2','-ar',str(sr),'-f','f32le','-'], 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报"有削顶",
然后你会去怀疑源文件 —— 实际是自己引入的。 -
★★ 本机 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 拆成字节碎片(如"\xe6\x9e"),
整个文件是非法 UTF-8,json.load会UnicodeDecodeError
⇒ 用open(p,'rb').read().decode('utf-8','surrogateescape')读,再按字节拼回字符。 -
★★★
shutil.rmtree撞安全删除守卫会 SIGTERM 整个脚本(exit 137)。
报错形如[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {"count":145,"threshold":50},
阈值 50 个文件。(§3.1 坑 6 已有blocks/100+ 的先例,本节是 145 文件复现。)
⇒ 不删目录,同名文件直接覆盖(os.makedirs(tmp, exist_ok=True)+-y)。
残留的旧文件不影响结果,不要为了"干净"去 rmtree。 -
★★ 「整条重转写」比「只验编辑段」更省事也更保险。
367 s 全片重转写 只要 37 秒(whisper.cpp)。
只验编辑段的问题:切点找偏了会把邻句咬掉半截,编辑段内看不出来。
⇒ 验收一律整条重转写,逐刀核对接缝前后一整句。 -
★ 尾部静音会 hallucinate —— 东湖片尾出现
「请不吝点赞 订阅 转发 打赏支持明镜与点点栏目」。
判读时直接忽略尾条,不要当成"多念了一句"去剪。 -
★★★ whisper 会把「重复内容」抹平 —— 判重复不能看转写。
东湖原始 32.31–39.71 有三段独立语音(「天下第一山水,」/「天下第一」/「天下第一山水大盆景」),
但任何窗口长度(5.68 s / 3.11 s)、带不带 prompt,都只读一遍「天下第一山水大盆景」。
转写看着干净,片子里「天下第一」实际念了两遍,v1 因此漏剪、被用户听出来。
⇒ 判「有没有重复」只能看prosody.py的能量语音段表:
短段(0.4–1.3 s)紧接一个明显更长的完整句 = 假开头,命中了哪怕转写通顺也要剪。
配套:在整条转写里检索关键串,确认出现次数(「天下第一」×1、「我当」只出现在「我当时」里)。 -
★★★ 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 个)—— 都只作旁证。 -
★★★ 「转写对得上」≠「片子对了」—— 交付前必须自己整条听一遍。
v1 报告写「7 处全部剪除、验收通过」,实际漏剪 2 处,用户听审才发现。
上述四项自检(时长/削顶/接缝跳变/整条重转写)全部通过,片子还是错的。
⇒ 硬性要求:交付前自己带耳机过一遍(至少每个接缝两侧 ±2 s)。 成本 ≈6 分钟,
而返工一次 ≈65 分钟(复查 25 + 声学对比 8 + 重剪 6 + 重锚 5 + 沉淀 20)。
⇒ 另一条随手的硬验收:拿原始源文件按cuts.json重切一遍,与定版cmp逐字节比 ——
一致 ⇒「除这些区间外一个采样都没动」;改版时再与上一版逐样本比,差异应只在新增接缝的 8 ms 淡入淡出窗内。 -
★★★★ 「沉浸感做不出来」的根因往往是「音轨被
-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 ⇒ 是回归,不是风格。 -
★★★ 连续背景床与一次性音效要分开的 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 ✓。 -
★★★ 慢包络不要在全采样率上做卷积 —— 先算复杂度。
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)。 -
★★★ 三个会让「量化对比」得出反向结论的度量陷阱。
- 在布尔抽取的样本上做 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('Summary:'):]。 - 另:
highpass=60之后 S/M 会「看起来变窄」,但侧声道 75.76% 的能量在 0–20 Hz(次声隆隆)
⇒ 别把次声隆隆当成声场,高通是净化。
- 在布尔抽取的样本上做 FFT ⇒
-
★★★★ 在「母版」上量单路指标 —— 量不出责任,还会给出反向结论。
母版级「真静默窗中位」量的是背景合成、音乐占主导 ⇒ 东湖环境声让路从 6.9 dB 修到 21.6 dB,
母版读数只从 −28.0 动到 −28.9(0.9 dB,方向还相反)。⇒ 背景判据一律回到 stem 分路,
母版只用于最终响度/真峰复测(判据见 §7.11)。 -
★★★★ 换母版 = 档案里所有读数作废,必须在新文件上重测再落笔。
东湖 v1.2 收尾时发现制作档案「实测数据」三行仍是上一版读数
(LRA 5.4/TP −1.6/静默窗 −28.4),与实交付(5.5/−1.9/−28.9)不符 ——
因为只换了音轨、没重跑核验就顺手抄了旧数。
⇒ 交付件一旦重出(哪怕只换一条音轨),响度/真峰/LRA/静默窗/立体声相关全部重跑一遍。 -
★★★★ 用户报「喷麦/爆音出来了,我录完听是没有的」—— 先证明是不是人声链放大的,大概率不是。
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 —— 那会连人声主体一起改,得不偿失。
★ 探针脚本:<EP>/07_work/plosive_probe_ep2.py(纯只读,不改任何文件)。 -
★★★★ 「片子到底有没有旁白」用互相关判 —— 别用耳朵、更别用总 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全都没有音轨(烧字幕前的中间件)。
★ 探针脚本:<EP>/07_work/audio_presence_ep2.py。
★ 新集复用时:把这两个脚本拷进新集的07_work/,只改顶部EP/ 输入路径常量即可。 -
★★★★ 调色:平台二次转码不会吃掉「清晰度」——别凭直觉劝用户降参数(实测打脸过两次)。
起因:用户在剪映给 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 显廉价)。
★ 脚本:<EP>/07_work/recode_loss_ep2.py(只读抽帧+重编码,不改交付件)。 - 预设①「log LUT 套非 log 素材会褪色发灰」→ 错。实测饱和度 17.3%→29.7%(+72%)、对比 std 66.7→74.5、天空灰蓝→青蓝。
-
★★★★ 用户红绿色弱 ⇒ 调色一律以「专业标准 + 数值判据」定案,不以他的偏好为准。
用户原话(2026-10-02 本人授权):
> 「因为我红绿色弱,所以我看到的不是标准色,所以不能以我的偏好来看片子的调整,
> 我就是瞎调到我看的舒服些而已,应该以你专业的电影调色为准!」
⇒ 他说「我看舒服了」不构成判据,只能当线索(比如他说「天空发灰」,翻译成「饱和度不足」)。
⇒ 他说「清晰度/对比度我调了」时,也要复核数值(见第 88 条三条判据)——
他看不到色偏,但压黑和过锐是他调完也发现不了的。
★ 「平台自动审核先看清晰度」这个理解方向对、位置错,要如实纠正:- 机器「过审不过审」看的是:分辨率门槛/违规画面/重复度原创度/水印二维码 —— 清晰度不决定过审;
- 清晰度影响的是「给多少量」:低清晰度被判低质 → 压推荐池分配。
⇒ 所以把清晰度做好是加分项,不是保命项;保命的是原创度与合规(见 §1)。
-
★★★★★ 「剔除类清单」必须带「状态」列 —— 判定是给某一版镜表做的,镜表一改就过期。
真实事故(2026-10-05 EP07 大栅栏):黑名单_跑题.csv里 122 条把整整一批
2012-11-23 的照片判成「文玩店铺/室内/器物跑题」,依据写着「逐页实看」「批审图人工判定」。
我照单全收,据此得出「这批没素材可用」。用户直接推翻:
> 「大栅栏那组图里不是有很多店铺的照片吗?门脸,内联升的布鞋特写那一组」实看:那批正是老字号门脸与器物特写(金蟾斋/全聚德/瑞蚨祥/祥義號/王麻子/大碗茶/
毛笔店/布鞋特写/铁艺牌坊)—— 正是片头段需要的画面。
那条判定的真实语境是给旧镜表做的(旧镜表里这段要用街景,店铺门脸算跑题)。
- 根因:判据是有「适用范围」的,而清单把适用范围丢掉了。
清单只记「编号 → 跑题」,不记「谁判的、为什么、对哪一版有效」⇒ 换了叙事重点后,
它仍然生效,而且看起来像是权威证据。
- 修法:黑名单/剔除清单一律加两列,且作废只改状态、不删行:
编号, 跑题依据, 状态(生效/作废), 作废依据
读取端 缺状态列直接SystemExit—— 没有状态列就无法区分
「仍生效」与「已作废」,必然误杀。
- 判据:任何「这批不能用」的结论,落笔前必须回看那条判定的原始依据是什么、
它是为哪一版镜表做的。 我这次是从没回看依据,只信了标签。 -
★★★★★ 改「主图资格」必须同步回看它对「辅图池」的连锁影响。
接上条,同一份黑名单同时卡主图与辅图(EP07 里make_plates只在辅图那一处用它)。
解封前连锁后果:2012 批 108 张被黑名单挡在辅图池外 ⇒ 辅图池只剩 4 张 ⇒
该批 9 个竖幅三联镜没辅图可用 ⇒ 同一对辅图(IMG_7128+IMG_7129)被 6 个镜复用。- 判据:「一图只进一镜」是铁律,复用会被观众一眼看出(「左右那两条怎么又是它」)。
- 漏斗要按批算,不能只看总数:池 128 → −跑题 108 → −禁列 → −已作主图 → 18。
「总数够」不代表「这一批够」。 - 改任何上游资格闸门(黑名单/清晰度阈值/人脸阈值)后,必做一次
「逐批辅图池余量 vs 该批三联镜需求(镜数×2)」的对账。
-
★★★★★ 唯一性断言必须对「全表」下,不能只对「本批」—— 这个坑我踩了第二次。
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['镜号'] not in {m['镜号'].strip() for m in mp}] after_mat = collections.Counter([r['素材'] for r in after] + newmats) dup = {k: v for k, v in after_mat.items() if v > 1} if dup: # 报出「哪个素材在哪几个镜」才算合格诊断 where = [f"{r['镜号']}(原留)" for r in after if r['素材'] == k] where += [m['镜号'] + '(本次新填)' for m in mp if m['新素材'].strip() == k] raise SystemExit('✗✗✗ 替换后全表出现同一素材进多镜 —— 硬停:\n' + ...) -
★★★★ 镜表的「版式」列是由源图长宽比推导的,换素材后必须重算。
plan_sheet.py里是'版式': '竖' if (p.get('h') or 0) > (p.get('w') or 0) else '横'
—— 它不是「这一镜打算用横构图还是竖构图」的作者意图,而是源图属性的快照。
换素材后没重算 ⇒ 实测 7 个镜写着「横」而实际是竖幅
(s0004/s0008/s0011/s0013/s0015/s0020/s0056)。- 为什么它必须正确:渲染端不消费此列(三联判定看原图长宽比),
但它进档案、是核对判据 ⇒ 下游任何人拿它做判断都会错。 - 修法:替换工具内建重算;核对表额外打印一行
「镜表版式 vs 原图长宽比 错配 N 镜」,要求 N=0。
- 为什么它必须正确:渲染端不消费此列(三联判定看原图长宽比),
-
★★★★★ 人脸闸写死「半分辨率」= 尺度盲区,大图的正脸/侧脸会漏检。
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 与结论文档两处都要写。 - ★ 仍未覆盖:闭眼/侧躺/背影/极端侧脸 ⇒ 「无人脸」≠「无人」,永远要人眼终审。
- 修法:多尺度取并集 —— 半分辨率必跑 + 全分辨率补,
-
★★★★★ 「印一行 ⚠️ 就过」的降级分支 = 静默劣化,该硬停的必须硬停。
EP07 辅图分配原本在找不到足够辅图时走了个「放宽:允许复用」的分支,
只在脚本末尾印一行⚠️⇒ 同一对辅图被 6 个镜共用而没人看见。- 判据:区分「警告级」与「硬停级」的唯一标准 = 违反了铁律没有。
违反铁律的没有降级选项,只能:
① 如实留缺口(宁可缺,不偷偷降级);② 在落盘之后下硬断言。 - 「落盘后断言」而不是「写盘前断言」:断言的对象应当是
实际写出去的台账(ledger),而不是内存里的中间态 ——
两者会因为排序、去重、二次覆盖而分叉。 - 断言要一次性把四类都查了,缺一类就是留一个后门:
不足(short) / 多镜复用(over) / 与主图撞图(clash) / 同镜两条辅图相同(same)
报错时附上池现状(逐批余量)与需求算式(竖三联 N 镜 ⇒ 需 2N 个位),
否则下一个人只知道「失败了」,不知道「该怎么补料」。
- 判据:区分「警告级」与「硬停级」的唯一标准 = 违反了铁律没有。
-
★★★★ 「交付物体积」不是判据,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 就是这么发现「母版里到底装的是新版尾板还是旧版尾板」的。
- 危害:档案里记的是其中一次的体积 ⇒ 下次拿
-
★★★★★ 改尾板文案后,必须比对
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,旧版另存目录,便于回退比对。
- 判据:
-
★★★★★ 用户拍板改了「用 / 不用」,第一件事是回扫所有对外文字里关于「用/不用」的表述。
EP07 真实事故:用户拍板保留 s0053/s0054 两镜,而尾板肖像行写着
「2008-10-18 街头人像连拍整组不用」—— 那两张正是这批、且确实在片子里。- 性质:这不是「文案不严谨」,是对外声明与事实不符(本项目属合规红线)。
比「素材要不要换」严重 —— 素材换不换是审美/风险取舍,声明不实是硬伤。 - 回扫四处(缺一不可):尾板
credits.json的portrait_note/
发布文案/置顶评论/制作档案。这四处互相独立,改一处不会带动其他。 - 口诀:片子改了、话没改 = 片子说谎。
- 连带:
credits.json一动 ⇒ 尾板 PNG → mp4 → 拼接 → 烧字幕 → 终混 → 预览
整条尾段链重跑(EP07 ≈13 min)。改之前先把这个成本算进去。
- 性质:这不是「文案不严谨」,是对外声明与事实不符(本项目属合规红线)。
-
★★★★★ 过滤子进程输出,绝不能用「行首字符」当判据 —— 会把正主连同噪声一起滤光。
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 --> 00:00:04.000] 你來一個)
⇒ 全部转写被自己滤光,14 条一律显示「(空)」。
差一点据此写下「老盘音轨全部不可用」的错误结论 —— 而对照组
(已知音轨是《两只蝴蝶》)也显示「(空)」,本该立刻暴露是工具坏了。- 规:过滤必须匹配该行的确切格式(这里用
^\[\d\d:\d\d:\d\d\.\d{3}\s*-->\s*\d\d:\d\d:\d\d\.\d{3}\]\s*(.*)$取值),
而不是靠「首字符像不像噪声」。 - ★ 更重要的自检习惯:批量任务里放一个「已知答案的对照组」。
本次的对照组是那条已用别的方法证实过音轨内容的视频 ——
它一显示「(空)」,就能立刻区分「是素材不行」还是「是工具不行」。
没有对照组的批量结论,一律不可信。 - 修好之后同一批数据立刻可用:老盘确实有现场人声(「…到大使那裡,現在都得剪炸…」),
即 EP07 已用的 s0035/s0036 同源。是工具错了,不是素材废了。
- 规:过滤必须匹配该行的确切格式(这里用
-
★★★★★ 换「素材」前,先核它的 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。 - 口诀:换画面 ≠ 只换画面。凡是「现算」的字幕/尾板字段,都要先回看它依赖哪些元数据。
- EP07 换 s0051(
-
★★★★ 中间产物也能用 sha1 做「只该变的变了」自检 —— 比重跑后肉眼看靠谱。
返工「重建全部 N 个中间产物」时(EP07 重建 90 张 plate),
真正要证明的不是「新的对」,而是「除目标那一个外,其余一个都没动」。- 做法:重建前dump
shasum -a 1清单 → 重建 → 逐条比对 →
断言只有目标文件变化。EP07 结果:90 张里恰好只有s0051.jpg变
(dd31c41a…→448fd0db…),其余 89 张逐字节一致 ✓
⇒ 一次性证明了「重建链是确定性的」+「改动精确命中」。 - 反例价值:若比对出意料之外的变化(比如别的镜也变了),
说明有未被察觉的耦合(工具被改过、池子变了、排序不稳),这是真正的隐患。 - 适用面:plate/片段/字幕/尾板等任何可重建的中间产物。
- 做法:重建前dump
-
★★★★★
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 前先问自己「我要它干哪件事」,别照抄上一次的写法。
- ① 该关没关 —— 做「垫入量对比」试听件时挂了默认值:
-
★★★★ 「垫入量」必须按「实际取用那一段」实测反推,不能按素材全长算。
一条 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),
提的是它自己的底噪 —— 必须提醒用户「这条可能是在放底噪」,别让他以为只是变响了。
-
★★★★★ 「取素材哪一段」和「垫多少」是两个独立变量 —— 先扫段落,再定增益。
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>&1 \ | grep mean_volume; done
剩余时长是硬约束:43 s 的车站广播只能取到 20 s 起(40 s 起只剩 3 s)。 - 脚本必须加防呆断言:
素材时长 − 起点 < 需要时长直接抛错,别默默出残切片。 - ★★ 同一素材的切片定义只能写一处:EP07 单版 demo 与串烧各写了一份 PLAN
⇒ 修了单版忘改串烧,用户听到的与说明不符。正解是串烧import单版脚本的定义。 - 同源错误:切片起点变动 ⇒ 该版全部旧数据作废,要连同串烧一起重出并归档旧版。
- 用户听不到不是耳朵问题,是我切了哑的一段 —— 而且我当时还先怀疑"垫入量太保守",
-
★★★★ 批量抽帧(
-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 抽帧的推算值。
- EP07 实例:
-
★★★★★ 「某个声音在哪个频段」用「前后逐频段增幅比对」定位,一次定死。
做法:取该声音出现前与出现后等长的两段,逐频段测 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 起),单帧测不准;
环境声层本身有起伏,Δ 要能做进噪声以上。
- ★ 反面教训(这条最贵):我先按「笛子=1.5–4 kHz」这个想当然的频段假设扫了全片,
-
★★★★ 粗粒度扫描(每 10 s)不能用来定位事件 —— 短事件会被平均掉。
同一实例:我逐 10 s 扫配乐中高频,0–10 s报 −25.3(看着"中高频很弱"),
把「1 s 音乐进来、4 s 主奏进来」这两次进入整个抹平了。- 分工:定位事件用细粒度(1–2 s 窗);看整体分布/弧线才用粗粒度(10 s 窗)。
- 与「密排联系表的标签不能定位编号」「批量抽帧的坐标不可信」同源:
分辨率不够,坐标就不可信。 - 推论:凡是"某秒/某张是什么"的判断,必须能回到原始实读;
凡是给出"候选"之前,先问自己这个扫描粒度能不能看见我声称的那个东西。
-
★★★★
bandpass的w默认按Q(品质因数)解,不是 Hz —— 数字离谱时先怀疑单位。
bash # ✗ w=2500 被当成 Q=2500 ⇒ 带宽窄到近乎全阻,mean 掉到 −87 dB -af "bandpass=f=2750:w=2500" # ✓ 要 Hz 带宽必须显式 width_type;或干脆用无歧义的两级级联 -af "bandpass=f=2750:w=2500:width_type=h" -af "highpass=f=1500,lowpass=f=4000" # ← 推荐,没有单位歧义- 症状:测出来 −87 dB 这种物理上不可能的数(正常应在 −30 ~ −45)。
- ⇒ 规:看到离谱的数字,先查参数单位与默认值,再查素材。
同一类:alimiter的level(默认 true)、ebur128被-v error吃掉 Summary。
-
★★★★★ 判据的窗长必须匹配事件的尺度 —— 一天之内栽了四次,全部同源。
实例(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」这种荒唐结论。
-
★★★★★ 判「某声音在成片里是不是主导」,必须比「分轨绝对电平 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,白做。- ⇒ 只看分轨的 Δ 会误判「压了却没效果」;要看绝对电平才知道压的是不是主角。
- ⇒ 分轨的 Δ 不受恒定增益影响 ⇒ Δ 可跨轨比大小;绝对电平必须换算增益后再比。
-
★★★★
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='between(t,0.66,0.80)'(t 为该滤镜链内的时间)。 lowpass=f=4000只有 12 dB/倍频程滚降,压不动 5–8k(实测只降 5.1 dB);要陡必须级联。
- 实测:目标段 peak 0.1 → 0.2 dBFS(纹丝不动);
-
★★★★★ 工具疑似失效时,先用「纯音自证」再怀疑工具 —— 这招省一小时的弯路。
bash ffmpeg -f lavfi -i "sine=frequency=7000:duration=0.4:sample_rate=48000" \ -af "equalizer=f=7000:width_type=o:width=1:g=-16,volumedetect" -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 条)。
- 反面:我曾因此怀疑「
-
★★★★ 「用户描述的音色」是真信息,别用先验频段覆盖它。
用户说「笛子」「突然蹦出来」——两处都精确成立:
目标段 5–12k 占 99.1%、谱重心 8064 Hz、主峰 7.0/7.1/7.2 kHz 间隔 93.5 Hz
(=说话基频的 75–77 次谐波),谱平坦度 0.0001(纯音性、离散谱线)。
⇒ 擦音听起来就是一声尖笛。先信描述,再用实测去对;不要先按「笛子=1.5–4 kHz」扫,
那两个候选一个都不含正确答案。 -
★★★★★ 「某个特征符合我的描述」≠「它就是用户指认的那个」—— 先让用户指认,再深挖。
反面实例(2026-10-05,就在第 113 条之后立刻踩到):
我把旁白 33.26–33.38s 的 5–12k 爆发查得极细(112 处、主峰间隔 93.5 Hz、SFM 0.0001…),
但那只是我自己认定的对象。用户听了对照件后一句:「两遍都没有笛子呀」。- ⇒ 规:先把「能听的对照件」交出去让用户点头,再投入深处。
「物理特征吻合」是必要不充分条件;用户的耳朵才是判据。 - ⇒ 同一条也适用于「用户说某个声音突兀」:先确认对象,再定手法。
- ⇒ 规:先把「能听的对照件」交出去让用户点头,再投入深处。
-
★★★★★ 对照件的源文件,必须是「用户实际听到的那个版本」。
同一实例:我拿成片_终版.mp4(v1,根本不含垫入层)做对照 ⇒ 自然「两遍都没有」。
而用户听到笛子的是 v2(含垫入层)。 拿错源文件,等于把答案摆在眼前却看不见。- ⇒ 规:做对照前先确认「这个文件里有没有那个东西」——
用实测(如特征频段显著度)核一遍,不要凭版本号想当然。
- ⇒ 规:做对照前先确认「这个文件里有没有那个东西」——
-
★★★★★ 垫入层/环境层的素材,入库前先扫「有没有乐器长音」。
真凶实例:垫入素材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,不够)。
-
★★★★ 生成「垫入层/环境层」后,必须实测落点并按偏差重生成 —— 别用算出来的增益当结果。
工具_建垫入层.py已加自动校准:生成 → 实测落点 → 按偏差重跑一遍(链路无压缩,一次即收敛)。- ★★ 校准口径必须是
gen_target(=落盘目标 − sfx_gain),不是target:
该脚本生成的是「终混前」的层,终混还要再乘一次 sfx_gain。口径搞反会差 1 dB 且不自知。 - 实例:换素材后初算增益的落点比目标低 3.4 dB(已排除淡入淡出),原因未定位
⇒ 不猜,直接实测反推;修正 +3.40 / +3.80 dB 后落点 −24.0 ✓ → 经 sfx-gain 后 −25.0 达标。
- ★★ 校准口径必须是
-
★★★★★ 换单张图之前,「已用素材」集合必须从
_plate/_ledger.csv重建 —— 镜表「素材」列 ≠ 实际用图。
镜表只记主图;竖图主位走三联时,脚本会另挑 2 张辅图并写进 ledger 的「辅图」列。- 实例:某「未用候选池」只排除了镜表 90 张主图,漏掉 42 张辅图 ⇒ 27 张候选里
10 张其实早已进片;按它挑图会撞车。 - ★ 顺带:同一批的「未用」也可能被旧口径人脸闸虚增。
本项目 2026-10-05 修过「尺度盲区」(旧实现只跑半分辨率,只漏报不误报)
⇒ 旧闸报的「无脸」不可信(实证:同一张图旧报 0 脸、新口径 3 脸/最大占比 0.528%)。 - 规:换图前
已用 = ledger.素材 ∪ ledger.辅图;候选池的人脸闸必须与当前口径同版。
- 实例:某「未用候选池」只排除了镜表 90 张主图,漏掉 42 张辅图 ⇒ 27 张候选里
-
★★★★★ 换掉主图 ≠ 那张图走了 —— 它会从「辅图位」回来。
素材脱离镜表后不再是「已用」,于是立刻落进辅图池,被三联自动挑中。- 实例:把 s0025 主图换成竖图后重跑,它的右辅条被自动挑成刚撤下的那张(等于没换干净)。
- 跑题黑名单只过滤辅图,挡不住将来有人把它塞回主图 ⇒ 必须双闸:
① 跑题黑名单(黑名单_跑题.csv,带「状态」列)② 用户禁列硬闸门(脚本BANS,入口断言硬停)。 - 规:因「质量差/用户否掉」而撤下的素材,当场写进
_黑名单_禁列_*.csv并接进BANS。
-
★★★ 三联的辅图是「全局零复用」按顺序分配的 —— 插入 / 改变一个竖图镜,会顺移后面所有竖图镜的辅条。
改一处先算影响范围,再决定要不要重渲:看 ledger diff,别猜。- 实例:s0025 改竖图后,
s0027/s0032/s0033/s0035/s0036的辅条各顺移一位(主图全不变)。 - ★★ 其中
s0033/s0035/s0036是「视频插入镜」(画面走 2008 拍客视频,render.py不读其 plate)
⇒ 它们的 ledger 虽然变了,成片不受影响 ⇒ 只需重渲真正用到 plate 的那几镜。 - ★ 重渲是增量的:
render.py --stage shots的todo只收「不存在或 <20 KB」的镜
⇒ 必须先把要重渲的<镜号>.mp4移走(mv,不要rm),否则它会跳过。 - ★ 尾板「用图 N 张」是从 ledger 现算的 ⇒ 图数一变就要重出尾板(
make_credits_ep7.py build|clip),
但尾板是固定画布 + 固定 DUR,改文案不改时轴 ⇒--total可沿用,只需确认帧数仍为 278。
- 实例:s0025 改竖图后,
-
★★★★★ 尾板时长不是自由变量 —— 它被「配乐末段可取的 tail 上限」锁死;改时长必须同步重出配乐轨。
(2026-10-06 EP08 实测,症状全静默、不报错)
① 约束怎么来的:末乐章用某首曲子,M9 对齐点钉在曲内某一秒(艺术决定,不动),
该曲实长 − 对齐点 = 对齐点后可用 tail 上限(EP08:EchoesOfHome上限 62.63 s)。
tail由片长反解(CUES[-1]['tail'] = FILM_DUR − hit_film,EP08 hit_film=461.72)
⇒ 片长有上限 ⇒ 尾板有上限(EP08:≤ 461.72+62.63−517.9333 = 6.4167 s,取 6.0)。
★ 超限的表现是谱曲环节硬失败(这是好事,会拦住你):
EchoesOfHome: 取到 3006166 样本 < 需要 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 <末-8> -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)不需要读这份。