声音与配音 - ZLHad/OpenVideoHarness GitHub Wiki
English · 中文
agent 听不见声音,所以这里的声音都做成可以计算、可以测量的。旁白和配乐先做,实测出来的时间决定画面。每条命令都读写项目 audio/ 目录里的普通文件;Python 依赖第一次用时由 uv 下载到它的缓存里,不装进全局环境。完整说明见 playbook/04-audio.md。
bin/vh tts projects/<p> qwen Serena zh # audio/script.txt → voiceover.zh.wav + timeline.zh.json
bin/vh captions projects/<p> zh # captions.zh / en / bi .srt + captions.json
bin/vh music --example > projects/<p>/audio/score.json # 起步谱(--example zh:中国乐器;--example list:全部起步谱)
bin/vh music projects/<p>/audio/score.json projects/<p>/audio/music.wav # 同时写出 music.beats.json
bin/vh sfx lib projects/<p>/audio/sfx # 把 21 个内置音效(plain 版)和 sfx-lib.json 写进项目
bin/vh mix projects/<p>/audio/mix.wav profile=short voice=projects/<p>/audio/voiceover.zh.wav music=projects/<p>/audio/music.wav \
events=projects/<p>/audio/events.json lib=projects/<p>/audio/sfx timeline=projects/<p>/audio/timeline.zh.json stems=projects/<p>/audio/stems
bin/vh qa projects/<p>/audio/mix.wav projects/<p>/audio/music.beats.json projects/<p>/audio/events.json --stems projects/<p>/audio/stems
bin/vh mux projects/<p>/out/final.mp4 projects/<p>/audio/mix.wav projects/<p>/out/final-av.mp4 \
projects/<p>/audio/captions.zh.srt projects/<p>/audio/captions.en.srt # 软字幕轨showcase 00–03 每支片子都有一个 tools/build_audio.sh,从提交进仓库的源文件把整条链路跑一遍,每次得到同样的字节;02(中文旁白)和 03(英文旁白)正好是旁白(带词级时间)、字幕和混音 profile 配在一起的现成样板。
| provider | 是什么 | 需要 | 什么时候用 | 实测 |
|---|---|---|---|---|
qwen(默认) |
本地开源 Qwen3-TTS,经 mlx-audio 运行。中文:Serena、Vivian、Uncle_Fu、Dylan(京腔)、Eric(川话);英文:Aiden(默认)、Ryan | Apple Silicon;首次使用下载约 2 GB | 在 Mac 上离线、免费地配旁白 | 已测,中英 |
say |
macOS 自带的音色 | macOS | 快速打草稿 | 已测 |
edge |
微软 Edge 在线音色,走非官方的 edge-tts
|
联网 | 不在 Mac 上又想免费配音;随时可能失效 | 已测英文 |
dashscope |
阿里云百炼上的 Qwen3-TTS | DASHSCOPE_API_KEY |
需要国内的云端音色 | 未测 |
elevenlabs |
ElevenLabs 多语种音色 |
ELEVENLABS_API_KEY、voice id |
你本来就在用 ElevenLabs | 未测 |
gemini、gemini-lite
|
Google Gemini 3.8 Flash TTS 和 Flash-Lite TTS;30 个 studio 音色(默认 Kore),都能说中文 |
GEMINI_API_KEY;有免费档和付费档 |
要表演力的旁白、逐句导演、双人对话;gemini-lite 适合批量的单人旁白 |
gemini 已测,中英 |
已知问题:默认的本地 Qwen3-TTS 0.6B 模型念英文短句时,偶尔念完还停不下来,拖出几秒含糊的声音。所以每条配音都要查:加 --align gemini(见下文),或者至少看一眼 timeline.<lang>.json 里的时长。被标出来的句子重跑,或者换 1.7B 模型(QWEN_TTS_MODEL),或者改用 gemini。英文默认音色从 Ryan 换成 Aiden,也是因为 Ryan 出错多得多。每个 provider 在句首句尾留的静音都会被裁掉(低于 −50 dBFS 的部分,人声前留 30 毫秒、后留 80 毫秒),所以句间距离就是你设的 --gap,卡拍的句子也真的在拍上开口。
Gemini 合成的音频都带听不出来的 SynthID 水印:用了它的片子,在 NOTES.md 里写明旁白是 AI 合成的,发布时按平台要求标注。免费档的内容可能被 Google 用来改进产品。价格和限额见 playbook/04。
audio/script.txt 一行一句,一句对应一条字幕:
@hook [像在跟朋友分享一个惊人的发现,语速偏快,尾音上扬成问句] 一句话,能做出一支片子吗? || One sentence. Can it make a film?
@yes:downbeat [干脆利落地揭晓答案,短促有力] 能! || It can.
-
中文 || English两种语言写在一行。--lang决定念哪一边,两边都会进时间表,供字幕使用。整份稿子没有中文时,会用英文念。 -
@id给一句起名字。[指示]只导演这一句,不会进字幕;整体语气写在bin/vh tts的第 5 个参数(--instruct)里。 -
<short pause>、<breath>、<laugh>这类标签只有gemini会演出来,其他 provider 和字幕里都会自动去掉。 - 谁听得懂指示:
gemini最好;本地qwen要换成 1.7B 的 instruct 模型才行;say和edge只会照念。 - 写念出来的样子,不写屏幕上的样子:单位、缩写和符号按口语写给配音("2 G赫兹",不写"2 GHz")。字幕必须不一样时,这一句按口语写法合成,字幕保留原文,口语写法在
script.txt里记一笔。合成后要去听数字和单位,转写的第一遍在这些地方不可靠。 - 一段时间放得下多少字:知识短视频句内约每秒 4.5–5.5 个中文字,讲解和论文约 3.5–4.5,算稿子预算时再给停顿扣掉 15%(45 秒的短视频大约 170–210 个字)。
bin/vh readcheck --budget <秒>会给出一段时间里放得下的字数:上屏的、做字幕的、念出来的。各类视频的默认语气列在 playbook/04 里。
动效类、发布片、MV、梗视频,旁白要骑在音乐上:
bin/vh tts projects/<p> gemini Kore zh --beats projects/<p>/audio/music.beats.json --snap beat每句都从下一个拍点开始(--snap beat|half|downbeat 选网格,--lead 设第一句最早从哪开始);@id:downbeat 可以单独指定一句,比如让答案落在 drop 上。做法是:先写配乐骨架,旁白对拍,在时间表里看每句落在哪一拍,回头调配乐的段落长度,最后画面按时间表来做。实测 120 BPM 下,几句的起点正好落在 0.50、4.00、5.00、10.00 秒。
任何 provider 都能用。每句合成后交给 gemini-3.5-transcribe 转写,拿到带时间戳的词,再和稿子逐字比对。相似度低于 --min-sim(默认 0.85),或者第一个词之前、最后一个词之后多出 1 秒以上的声音,这句就会被标出来。Qwen 停不下来、漏词,都能这样抓到。时间戳的步长是 0.1 秒。Gemini 的 Tier 1 每分钟只能转写 10 次、每天约 100 次:遇到 429,工具会按接口要求的时间等待再重试;要求等 90 秒以上的是日配额用完了,会直接报错。
转写失败也不会丢掉已经合成的配音:配音和时间表在合成后就先写好,没能核对完的句子保留实测时长,命令以退出码 1 结束并说明原因。原因排除后,用同一条命令加 --resume 接着跑:只补缺的部分,已经成功的句子不会再花一次钱。
@speakers A=Kore B=Puck
@q1 A: [好奇] 你猜这支片子手写了几行代码? || Guess how many lines of this film were written by hand?
@a1 B: [压低声音,卖个关子] 一行都没有。 |嗯?| 全是它自己写的。 || Not a single one. |huh?| It wrote all of them.
只有 @speakers 里声明过的标签才算说话人。|嗯?| 是对方的插话,只在 Gemini 对话里出声。Gemini 对话默认按段(空行之间的几句)一次请求,最多 2 个说话人,而且要用音色库里的音色。旁白也可以用 --join block|all 整段合成:句与句之间语气更连贯,代价是段内的句子不能单独重跑,也不能单独卡拍。
bin/vh captions <项目> [zh|en] [zh-max] 生成 captions.zh.srt、captions.en.srt、双行的 captions.bi.srt,以及给引擎用的 captions.json。竖屏把 zh-max 设成 11(每行最多 11 个中文字)。有词级时间时,captions.json 里还带每个词的时间,另外会写一份 captions.<lang>.lines.srt:一条字幕折成几行,每行在它的第一个字念出来时才出现。字幕可以烧进画面(引擎读 captions.json 来画,同样必须是 t 的纯函数),也可以用 bin/vh mux 封装成能开关的字幕轨。画面里已经烧了字幕时,给 bin/vh mux 加 --subs-off,播放器就不会自己再打开一套字幕叠上去。
短于 1.8 秒的字幕,会往后面的空白里延长,最多到 1.8 秒、下一条字幕开始前,或者画面结束(--media-end S,否则依次取项目的 index.html、media/final.mp4 或时间表);开始时间不动,也不会和下一条重叠。重复旁白内容的烧录字幕按字幕来查:标上 data-read="subtitle",bin/vh readcheck 就按字幕的阅读速度判,而不是按更慢的上屏文字规则。
bin/vh music score.json music.wav 把一份 JSON 格式的乐谱渲成音乐:速度、调、调式;各段的和弦、声部(kick、clap、hats、bass、pad、arp、lead,以及中国乐器编钟、古筝、竹笛、大鼓)和能量;段落效果 riser、impact、fill、bend;meters 用来改拍号。同一份谱永远生成同一段音乐,music.beats.json 记下精确的段落、拍点、小节头和冲击点,画面拿它来卡点。速度选一拍正好是整数帧的(30 fps 下:90、100、112.5、120、150、180 BPM)。
乐器声部。 layers 只有一套合成音色,只用它写的配乐听起来都差不多。乐谱还可以加 parts:91 种用代码合成、不用采样的乐器(键盘、敲击旋律乐器、拨弦、拉弦和管乐、合成器、各种鼓组,打击乐里有一套锣鼓经,还有黑胶、雨声这类底噪),用步进网格、带和弦相对音高的音符表或现成的伴奏型(走路贝斯、oompah、华尔兹、扫弦、琶音)来演奏,带立体声、混响发送,还可以加 lo-fi 和磁带。bin/vh music --instruments 列出全部乐器(连同 layers 的 11 种音色共 102 种),--example list 列出起步谱。只写 layers 的旧谱子渲出来和以前逐字节相同;某个声部没有音符,或者在最终文件里低于 −40 dBFS,会中止渲染并指出是哪个声部。
物理建模的拨弦。 另有 14 个音色可选:现有音色的物理模型版(guqin_pm、pipa_pm、harp_pm、nylon_pm、ukulele_pm、upright_pm、balalaika_pm、cimbalom_pm)和新乐器(guitar、koto、shamisen、banjo、kalimba、musicbox_pm)。弦是数字波导,所以一个音里音高可以滑动、弯音、颤动,重拨时音头略高再回落;卡林巴和八音盒是悬臂梁的模态合成。把一个声部换成它们,只要改 inst 这一个词,对比很容易;换完要重新看一下 gain_db,高音区的音量和旧音色不一样。它们的检查都是测量:三个八度内音高偏差约一个音分,音量和同音高的拨弦对齐,每次渲染字节相同。
动机和动态。 一个乐句在 motifs 里写一次,到处调用:移位、倒影、倒放、拉长、取片段,或做成模进。段落内部,dyn 是一条推子曲线,cresc 和 dim 让声部在一段里渐强或渐弱,段落的 stop 只留你指定的声部继续演奏(屏息),写成负数的拍位是弱起。还没有的:速度图(渐慢、渐快)。怎么用这些写出篇章、能哼出来的主题和起伏,见 playbook/11。
按音符驱动画面的音符表。 声部上写 "note_map": true,节拍表里就会多一个 notes,记下这些声部每个音的起止、音高、力度和声部名。琴键可以随着音按下去,角色可以踩着旋律走,时间都从表里读,不手写进场景。
把音乐变成图。 agent 听不到谱子,所以有两个参数让它看得见。--roll 给每个声部写一张谱面图,带响度,另有整体响度和乐谱 energy 的对照;--length S|auto 让文件正好在 S 秒结束(auto:最后一小节结束处),在一个拍点上起淡出,而不是留一段尾巴再让 mux 去切。
来源的优先顺序:你自己的或有授权的曲子(bin/vh beats 会分析出节拍、重音和鼓点);本机确认能跑的音乐生成模型;代码作曲。不要下载来路不明的 BGM,也不要扒参考视频的音乐。带人声的歌曲在 Suno 这类服务上生成后导入;ElevenLabs Music 和本地歌曲模型只留了接口。
audio/events.json 写成 {t, sfx, gain_db, pan, dist} 的列表,每个声音正好落在动作发生的那一帧。21 个内置音效都是用代码合成的原创音效:
- 转场:whoosh、swish_rev、whip、swoosh_tonal、air、paper、tape、shimmer;
- 小动作:click、tick、pop、typing、shutter、glitch;
- 铺垫和命中:riser、impact、boom;
- 提示音:ding、success、error、toggle。
转场按风格挑一个家族(手绘和拼贴用 paper,安静、纪录片式的用 air,卡通和快切用 whip,科幻和界面用 swoosh_tonal,复古用 tape)。除了提示音,内置音效的每个事件都有自己的 variant:同一个音效在一支片子里按时间顺序走一条有界的路,相邻两下一定不同,又不会差得太远;想留住某一下就写 "variant": n。事件还可以按动作塑形:dur、pitch 或 center、dir、bright、tone,以及 pan_from / pan_to。bin/vh sfx audition <名字|all> 把一个音效的几个 variant 排进一个文件,每个附一行测量,可以加频谱图;同一个声音连着三次以上一模一样时,bin/vh qa 会提醒。用 profile 混音时,混音器自己把事件摆好(events=… lib=…);bin/vh sfx place 用来单独渲一条音效轨。pan 从画面上算:pan = 2·x / 画面宽度 − 1,再乘 0.7–0.8 收一点。有授权的录音素材优先,来源和许可记进 NOTES.md。
为了分电平,每个事件都归在一类:hero(一次撞击、一声钟:就是那一下)、detail(点击、脚步)、ambience(风声、房间声)或 signal(读数、带信息的滴答)。类先看事件的 role,没写就按声音名字里的整词判断。名字会误导时就写 role:手绘短片里的一阵风本身就是包袱,所以它是 detail,不是 ambience。
bin/vh mix 把人声、配乐、音效混成一条立体声,响度 −14 LUFS。按视频类型选一个 profile,所有层都相对一个锚点放:有旁白时锚点是旁白各句响度的中位数,没有旁白时是音乐。
| profile | 用于 | 音乐在旁白下面(目标,正常范围,单位 LU) |
|---|---|---|
explainer |
01 原理讲解、06 论文 | 13(11–18) |
short |
02 知识短视频 | 11.5(10–16) |
promo |
03 发布片、05 数据故事、08 快剪 | 10(8–16) |
cartoon |
07 手绘短片 | 10(8–16) |
mv |
04 MV | 6(4–12):音乐是主角 |
swatch |
风格样片 | 没有旁白,音乐是锚点 |
这个数字叫 VMR:人声比音乐高多少响度单位。混音器按这个顺序做:
- 旁白各句往中位数拉平;
- 音乐逐句、带前瞻地让到每句的目标 VMR;1–4 kHz 的挖让(中文旁白再加 250 Hz–1 kHz)只挖词需要的深度;
- 每个音效向它那一类的范围走一半,低频的重击从不抬高;
- 所有音效共用一个短房间;
- 母带是一个按 BS.1770 量的整体增益加一个真峰值限幅器,所以电影感配乐的起伏不会被压扁。
A=projects/<p>/audio
bin/vh mix $A/mix.wav profile=explainer voice=$A/voiceover.en.wav music=$A/music.wav \
events=$A/events.json lib=$A/sfx timeline=$A/timeline.en.json music_db=-5 stems=$A/stems
bin/vh mix $A/mix.wav profile=cartoon music=$A/music.wav events=$A/events.json lib=$A/sfx dur=12 fade=0.04 stems=$A/stems # 没有旁白:音乐是锚点stems= 把各条总线按最终增益写出来,给 bin/vh qa 用。不写 profile= 时走原来的 ffmpeg 链:
- 默认
duck=voice:人声出现时音乐让位。没有人声总线时默认duck=off。 - 默认
duck_ratio=1.6。一次听感测试里,句间停顿 0.25 秒的旁白配上音乐,6 和 3 都会让音乐在句间掉下去,1.6 没有这个问题,听起来也最顺。 - 动效类片子靠音乐带节奏,只让它轻轻让位(
music_db=-5 duck=voice,duck_ratio取 1.5–2),或者干脆不让位。 -
duck=on会让音效也压音乐,这时 ratio 要降到 2–3,否则每个音效都会让音乐抽一下。
量编码之后的文件。 profile 混音的 WAV 真峰值留在 −1.65 dBTP,但 AAC 编码会抬高真峰值,抬多少看内容和码率(showcase 的片子在 192k 下是 −0.1 到 +0.1 dB,密一点的样片在 128k 下最多约 +1.4 dB)。要量编码后的文件(ffmpeg -i final.mp4 -af ebur128=peak=true -f null -),高于 −1.5 dBTP 就用更低的 tp= 重混。样片渲染器会自己做这一步。
bin/vh mux 把音轨合到画面上:它把索引写在文件开头(faststart),浏览器不用等整个文件下完就能开始播;音轨比画面长要截断时,最后 40 毫秒做淡出。
bin/vh qa <混音> [节拍表] [音效事件表] [--stems 目录] 在最终混音上跑,不合格时退出码为 1:
- 数字静音:片中任何一处所有声道都低于 −60 dBFS 且持续 20 毫秒以上;
- 掉音:0.1 秒窗口比所在段落的中位数低 12 dB 以上;
-
抽吸:凹下超过 4 dB、持续 60 毫秒以上(加
--stems,或者--voice voiceover.wav,旁白下面设计好的让位和配乐自己的起伏就不会被算进去); - 爆音:只报警告,按严重程度列出前 10 处,要用耳朵复听;
- 重复:只报警告,同一个声音连着三次以上一模一样时提醒(几个 take 轮着用不算);
-
卡点检查:节拍表里的每个冲击点、每个音效事件,都要在 1 帧以内找到对应的起音。每个卡点都会打印 margin,勉强找到的标成
OK~。起音检测漏掉的近乎纯音的音效(tick、ding),会用它自己的声音再对一次来确认。AAC 文件的容差多 12 毫秒;整个编码都偏了(说明封装错位)会直接判失败。
加了 --stems 还会打印混音报告(单独看报告用 bin/vh qa mix <stems 目录>):每一句的 VMR、1–4 kHz 里有被盖住风险的词、每个停顿里音乐的响度、每个音效相对锚点和它那一类的位置、各条总线的纵深。出现这些情况会判失败:有一句低于 profile 的下限,说话时 hero 音效比人声还响,有风险的词太多,某一类的中位数离范围超过 3 LU,或者一个卡点对上了、却被标成 BURIED(它在那里,但听不见)。每个标记怎么读、怎么改,见 playbook/04 的"混音"。
这份报告是往中间拉的;它判断不了哪个音效重要,低频的重击要加一层 1–4 kHz 的起音,不能靠加增益。戏剧性的停顿要做成屏息:保留一层低音垫底,把滤波收下来,撤掉鼓,再用一个渐强拉进下一个重拍。
API key 只放在环境变量里:GEMINI_API_KEY、ELEVENLABS_API_KEY、DASHSCOPE_API_KEY,生成式视频则是 FAL_KEY 之类。仓库里不写任何 key,LOCAL.md 只记变量名,不记值。缺 key 时,工具会告诉你缺的是哪一个。