有声书制作正从录音棚走向合成引擎。TTS 技术不再只是导航和语音助手的专属,它在长篇内容领域展现出越来越强的可用性。一部几十万字的长篇小说,传统录制需要播音员花费数周甚至数月,还要面对补录、剪辑、混音等大量后期工作。TTS 则能在保证基础听感的前提下,把制作周期压缩到小时级别,并且可以根据角色、旁白、情绪标记灵活调整声音表现。不过,长篇小说的语音合成并不是简单地把文本扔给接口,它需要一整套面向长文本的工程化处理。

第一个核心问题是文本边界的划分。整本小说动辄数十万字,主流 TTS 接口都有单次请求长度限制,而且超长文本也容易触发超时、内存上涨和韵律不稳定。按章节切分是自然的选择,但章节内部仍然会有大量段落和直接引语,需要进一步拆成适合合成的句子单元。
一、长篇小说文本预处理与分章策略
长篇小说的文本源通常来自排版文件或网络文本,里面包含大量空行、缩进、特殊符号和章节标题。预处理的第一步是统一换行符和空行,删除无意义的装饰符号,但必须保留省略号、破折号和引号,因为这些直接影响语气停顿。
章节标题是天然的切分锚点。中文小说常见“第一章”“第二章”或“第1章”,可以用正则表达式识别。切分之后建议给每个章节生成独立文本文件和元数据,包括章节编号、标题、预计字数、主要角色列表。后续合成时,不同章节任务可以独立重试,不会因为某一章失败导致整本书重新处理。
import re
from pathlib import Path
def split_chapter(text):
# 识别中文数字或阿拉伯数字章节标题
chapters = re.split(r'第[一二三四五六七八九十百千0-9]+章', text)
result = []
for idx, ch in enumerate(chapters):
ch = ch.strip()
if not ch:
continue
# 将多个空白符压缩为单个空格,保留句末标点
ch = re.sub(r'\s+', ' ', ch)
result.append(ch)
return result
raw_text = Path('novel.txt').read_text(encoding='utf-8')
chapter_list = split_chapter(raw_text)
for i, chapter in enumerate(chapter_list):
Path(f'chapter_{i:04d}.txt').write_text(chapter, encoding='utf-8')
上面的脚本只做基础切分,实际工程中还要处理目录页、作者简介和版权页。可以事先建立章节标题白名单或使用目录锚点,避免把“第三章”三个字误当成新章节。对于每章内部,建议按句号、问号、感叹号、省略号进行句子切分,并在句末保留标点,这样合成端能够获得明确的韵律边界。
二、SSML标记与角色音色控制
普通文本调用 TTS 只能获得平淡的朗读效果,而小说需要旁白、对白、内心独白等不同层次。SSML(语音合成标记语言)是解决这一问题的关键。通过 <speak> 根元素,可以在文本中嵌入停顿、语速、音调和发音修正。例如 <break> 标签可以插入时长不等的停顿,<prosody> 标签可以调整局部语速和音高。
角色音色控制一般依赖 TTS 服务提供的多音色能力,通过 <voice> 标签切换不同发音人。旁白可以选择偏中性的声音,年轻女性角色选择明亮音色,年长男性角色选择低沉音色。需要注意的是,同一角色必须固定使用同一个音色 ID,否则听众会明显察觉角色声音前后不一致。
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="zh-CN">
<voice name="zh-CN-XiaoxiaoNeural">
她推开那扇门,<break time="150ms"/>屋里安静得能听见自己的心跳。
</voice>
<voice name="zh-CN-YunxiNeural">
<prosody rate="+8%">你终于回来了。</prosody>
</voice>
</speak>
SSML 还能处理多音字和特殊读法。比如“重来”的“重”在不同语境中读音不同,可以用 <sub> 标签指定替换文本,用 <say-as> 标签声明数字或日期类型。情感强度方面,不同服务商提供的参数不同,有些支持风格强度,有些只能通过调整语速和音高来模拟紧张、平静、激动等情绪。
三、长时合成稳定性与批处理架构
一本小说可能被拆成数千个短句,如果串行请求,耗时会非常长,而且任意一次网络抖动都会中断流程。工程上需要把句子任务放入队列,用异步或并发方式调用 TTS 接口,同时控制每秒请求数,避免触发服务限流。每个任务至少保存两个状态:待合成、已完成,最好增加重试中。
断点续传是长时任务的基本要求。每次请求成功后,不仅保存音频文件,还要把句子哈希、任务状态和文件路径写入本地数据库或 JSON 文件。进程重启后可以先读取已完成句子,跳过重复请求,只处理未完成任务。
import time
import requests
def synthesize_sentence(text, voice, output_path):
url = "https://api.ipipp.com/tts"
payload = {
"text": text,
"voice": voice,
"format": "mp3",
"sample_rate": 24000
}
headers = {"Authorization": "Bearer YOUR_TOKEN"}
for attempt in range(3):
try:
resp = requests.post(url, json=payload, headers=headers, timeout=30)
resp.raise_for_status()
with open(output_path, "wb") as f:
f.write(resp.content)
return True
except Exception as exc:
print(f"attempt {attempt} failed: {exc}")
time.sleep(2 ** attempt)
return False
音频格式也会影响存储和分发。有声书通常使用 MP3 或 AAC 格式,采样率 24kHz 或 44.1kHz,码率 64kbps 到 128kbps 已经足够语音场景。若使用默认的云服务参数,可以统一转码,避免不同句子的响度和采样率不一致。转码和音量归一化可以用 ffmpeg 批量完成,命令中注意输出路径的反斜杠要保留,例如 Windows 下可使用 C:\output\chapter_0001.mp3。
四、质量评估与后处理
合成完成后不能直接上架,必须经过质量抽检。客观层面可以统计每个章节的音频时长、静音占比、文件大小异常值,并通过语音识别反向比对文本,找出漏读、增读和错读。主观层面则建议建立人工抽听机制,重点检查角色对白、情绪冲突段落以及带数字、英文缩写、成语的句子。
多音字误读是长篇小说最常见的问题。可以建立一本小说专属的发音映射表,把容易出错的词提前替换或标记。比如“他得了一等奖”中的“得”读二声,“他得走了”中的“得”读三声,这类单靠模型很难完全正确。使用 <sub> 标签强制替换为同音字或拼音,可以明显降低抽检返工率。
后处理还包括段落间静音统一。章节内句子之间的停顿建议控制在 250ms 到 500ms,段落之间可以插入 700ms 到 1000ms 的静音,这样听起来更像真人朗读。片头可以加入 1 秒左右的淡入,片尾做淡出,避免生硬的开始和结束。完成这些处理后,一本长篇小说的语音合成才算从能听走向耐听。