本节摘要:配音是音频层的第一站,也是整条流水线的"节奏之锚"。本节先说清为什么建议先配音后剪辑——改一句台词的代价比重做一个镜头小一个数量级;然后比较云端 API 与本地模型两种调用形态,给出一个可插拔的适配层写法示意;接着用一张音色映射表锁定"一个角色一个声音",用语速参数、停顿规则与多音字注音表控制播报质感;最后以"一句一文件"的方式组织产物,配一张听检清单收尾。所有代码为写法示意,接口细节以所用服务的官方文档为准。
新手流水线常把配音放在最后:"片子剪好了,再配个音"。这个顺序在纯手工剪辑里成立,在生成式流水线里恰恰应该反过来。理由有三:其一,台词的实测时长直接决定每个镜头需要多长的画面,先有音频,镜头生成的时长参数才有依据;其二,改一句台词只需要重跑一次 TTS(几秒、按字符计费),而改一个镜头要重跑一次视频生成(几十秒到几分钟、按秒计费),两者返工成本差一个数量级;其三,配音的情绪与节奏会反过来提示分镜该在哪里留白、哪里快切。
| 对比项 | 先画面后配音 | 先配音后画面 |
|---|---|---|
| 镜头时长来源 | 估拍,常偏长或偏短 | 台词实测时长加余量 |
| 改台词的代价 | 音画都可能重做 | 只重跑该句配音 |
| 节奏控制 | 后期硬剪,易突兀 | 音频定节奏,画面贴合 |
| 适合形态 | 音乐视觉片、无台词片 | 叙事短片、短剧 |
本章采用"先配音后剪辑"作为默认变体,第 6.3 节会把它落成可执行的脚本。
TTS 的接入形态只有两大类:云端 API 与本地模型。选型不是技术问题,是约束问题。
| 对比项 | 云端 API | 本地模型 |
|---|---|---|
| 音色数量与质量 | 通常更多、更稳定 | 取决于开源模型与音色包 |
| 成本 | 按字符计费,量大时显著 | 一次性算力投入,边际近零 |
| 延迟 | 网络往返,单句通常几秒 | 首次加载慢,之后本地推理 |
| 隐私与内容审查 | 文本出网,注意服务商条款 | 全程不出网 |
| 运维 | 无 | 需要显存与驱动环境 |
工程上建议把两者藏进同一个适配层,上层只认"文本进、音频文件出":
# tts_adapters.py —— 云端 API 与本地模型两种调用形态的适配层(写法示意) import json, os, subprocess, urllib.request API_URL = os.environ.get("TTS_API_URL", "") API_KEY = os.environ.get("TTS_API_KEY", "") def render_via_api(text, voice_id, out_path, speed=1.0): """云端形态:HTTP 提交文本与音色,响应即音频字节。以服务官方文档为准。""" payload = json.dumps({"text": text, "voice": voice_id, "speed": speed, "format": "wav"}).encode() req = urllib.request.Request(API_URL, data=payload, method="POST", headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=60) as resp, open(out_path, "wb") as f: f.write(resp.read()) def render_via_local(text, voice_id, out_path, speed=1.0): """本地形态:调用本地模型的命令行入口。命令行参数以所用模型文档为准。""" cmd = ["local-tts", "--text", text, "--voice", voice_id, "--speed", str(speed), "--out", out_path] subprocess.run(cmd, check=True)
密钥一律走环境变量,不写进代码与配置文件,这是第 11.2 节交付清单里的硬要求。
短剧至少两三个角色加一个旁白。最容易犯的错是"这句用 A 音色,下句忘了换",观众立刻出戏。解法是把"角色到音色"的映射固化成一张表,配音脚本只认角色名,不直接出现音色编号。
# voices.json —— 角色音色映射表(voice_id 以你的 TTS 服务音色列表为准) { "旁白": {"voice_id": "narrator_warm", "speed": 0.95}, "林小满": {"voice_id": "female_young_02", "speed": 1.0}, "陈师傅": {"voice_id": "male_mid_01", "speed": 0.9}, "电话音": {"voice_id": "telephone_band", "speed": 1.05} }
分配原则有三条:主要角色之间音色在性别、年龄、音高上拉开差距,避免相邻台词听感接近;旁白单独一类,不与角色混用;同一角色全片只用一个 voice_id,中途换声等于换演员。
TTS 默认输出的常见毛病是"太赶"和"没有呼吸感"。三个抓手可以解决大部分问题。
第一是语速参数。中文的自然语速大致在每秒四到五字(口径值,供估算用),叙事段落建议把 speed 放在 0.9 到 1.05 之间,情绪沉重的段落再慢一点。第二是停顿。句号与问号后建议留 300 到 500 毫秒,镜头切换处留 600 到 900 毫秒(均为口径值)。部分服务支持以标记语言在文本内标注停顿与读音,若所用服务支持,可把停顿写成参数而不是拼空字符串——拼一串空格的做法在不同引擎上表现不稳。第三是多音字与专名。中文 TTS 对"重""行""落"这类多音字,以及对剧中自造名词,都可能读错,需要一张注音表在送入 TTS 前做文本替换:
# pronunciation.json —— 多音字与专名注音替换表(写法示意) { "重新出发": " chóng 新出发", "银行门口": " 银 háng 门口", "雨夜便当": "雨夜便当(店名,读作 biàn dàng)" }
替换表要随听检结果滚动维护:每发现一处错读,就补一条规则,让同一类错不出现第二次。
不要把整集台词拼成一大段送给 TTS。一句一个文件,是后面一切环节的地基:重跑便宜、时长可测、对齐有锚点、字幕有依据。文件命名建议"镜头号_句号",例如 S01_L02.wav 表示第 01 镜头第 2 句。
# speak_lines.py —— 逐句生成配音并产出时长清单(演示模式用静音占位) import json, os, struct, wave LINES = json.load(open("build/lines_to_speak.json", encoding="utf-8")) OUT_DIR, RATE, CPS = "audio/voice", 16000, 4.3 def synth(text, out_path, speed=1.0, demo=True): """demo 模式按每秒 CPS 字生成静音,便于先跑通流水线再接真 TTS。""" if not demo: raise SystemExit("请在此接入 tts_adapters.py 中的真实渲染函数") dur = max(1.0, len(text) / CPS / speed) with wave.open(out_path, "wb") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(RATE) w.writeframes(struct.pack("h", 0) * int(dur * RATE)) return dur def main(): os.makedirs(OUT_DIR, exist_ok=True) durations = {} for item in LINES: name = f"{item['shot_id']}_{item['line_id']}.wav" path = os.path.join(OUT_DIR, name) dur = synth(item["text"], path, item.get("speed", 1.0)) durations[name] = round(dur, 3) print(f"合成 {name}:{dur:.2f}s,角色 {item['speaker']}") with open("build/voice_durations.json", "w", encoding="utf-8") as f: json.dump(durations, f, ensure_ascii=False, indent=2) print(f"共 {len(durations)} 句,时长表已写入 build/voice_durations.json") if __name__ == "__main__": main()
lines_to_speak.json 由第 02 章的剧本结构展开而来:每行包含 shot_id、line_id、speaker、text、speed。演示模式是刻意的工程习惯——没配好 TTS 服务之前,整条流水线的下游(混音、对齐、字幕)都拿静音占位先跑通。
自动检查测不出"读错字"和"情绪不对",人工听检不可省。清单如下:
| 检查项 | 听什么 | 处置 |
|---|---|---|
| 错读漏读 | 多音字、数字、专名是否读对 | 补注音表后重跑该句 |
| 情绪贴合 | 语速与角色状态是否匹配 | 调 speed 或换语气参数重跑 |
| 句间停顿 | 是否赶句、是否憋气感 | 调停顿参数或拆句 |
| 削波爆音 | 句首爆破音是否发劈 | 降音量参数或交 6.2 处理 |
| 拖长结尾 | 句尾是否机械拖音 | 换断句方式重跑 |
听检发现问题的处置单位永远是"句",不是"集"。这也是逐句生成的第二个理由:验收与返工的粒度一致。
配音文件就位后,时间线上还缺音乐与音效。下一节把台词、音效、BGM 三轨混成一条成片音轨,并让背景音乐在台词出现时自动退后半步。