6.1 TTS 配音


6.1 TTS 配音

本节摘要:配音是音频层的第一站,也是整条流水线的"节奏之锚"。本节先说清为什么建议先配音后剪辑——改一句台词的代价比重做一个镜头小一个数量级;然后比较云端 API 与本地模型两种调用形态,给出一个可插拔的适配层写法示意;接着用一张音色映射表锁定"一个角色一个声音",用语速参数、停顿规则与多音字注音表控制播报质感;最后以"一句一文件"的方式组织产物,配一张听检清单收尾。所有代码为写法示意,接口细节以所用服务的官方文档为准。

学习目标

  • 说清先配音后剪辑与先画面后配音两种顺序的返工成本差异。
  • 写出 API 与本地两类 TTS 的适配层,并能切换与离线演示。
  • 建立角色到音色的映射表,保证全片音色一致。
  • 用语速、停顿与多音字注音控制配音的节奏与正确读音。
  • 以"一句一文件"组织配音产物并完成人工听检。

一、为什么先谈配音:音频是节奏之锚

新手流水线常把配音放在最后:"片子剪好了,再配个音"。这个顺序在纯手工剪辑里成立,在生成式流水线里恰恰应该反过来。理由有三:其一,台词的实测时长直接决定每个镜头需要多长的画面,先有音频,镜头生成的时长参数才有依据;其二,改一句台词只需要重跑一次 TTS(几秒、按字符计费),而改一个镜头要重跑一次视频生成(几十秒到几分钟、按秒计费),两者返工成本差一个数量级;其三,配音的情绪与节奏会反过来提示分镜该在哪里留白、哪里快切。

对比项 先画面后配音 先配音后画面
镜头时长来源 估拍,常偏长或偏短 台词实测时长加余量
改台词的代价 音画都可能重做 只重跑该句配音
节奏控制 后期硬剪,易突兀 音频定节奏,画面贴合
适合形态 音乐视觉片、无台词片 叙事短片、短剧

本章采用"先配音后剪辑"作为默认变体,第 6.3 节会把它落成可执行的脚本。

二、两种调用形态:API 与本地

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 服务之前,整条流水线的下游(混音、对齐、字幕)都拿静音占位先跑通。

六、听检:TTS 也需要验收

自动检查测不出"读错字"和"情绪不对",人工听检不可省。清单如下:

检查项 听什么 处置
错读漏读 多音字、数字、专名是否读对 补注音表后重跑该句
情绪贴合 语速与角色状态是否匹配 调 speed 或换语气参数重跑
句间停顿 是否赶句、是否憋气感 调停顿参数或拆句
削波爆音 句首爆破音是否发劈 降音量参数或交 6.2 处理
拖长结尾 句尾是否机械拖音 换断句方式重跑

听检发现问题的处置单位永远是"句",不是"集"。这也是逐句生成的第二个理由:验收与返工的粒度一致。

本节要点回顾

  • 先配音后剪辑:台词时长是镜头时长的依据,改台词远比改画面便宜。
  • API 与本地两种形态藏进同一个适配层,密钥走环境变量。
  • 角色音色映射表锁死"一个角色一个声音",旁白不与角色混用。
  • 语速、停顿、多音字注音三件套决定"像不像人话",注音表滚动维护。
  • 一句一文件是流水线的地基:重跑便宜、可测时长、可做对齐锚点。

配音文件就位后,时间线上还缺音乐与音效。下一节把台词、音效、BGM 三轨混成一条成片音轨,并让背景音乐在台词出现时自动退后半步。

作者与出处
原作者:灏天文库
整理人: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U