长文本合成时,整段喂和按句切分的差别通常不在模型本身,而在切分边界由谁决定:整段输入时,分句、停顿和语气延续交给服务端一次处理;按句切分时,切在哪里、句间留多少静音、每句用什么参数,都由你自己的脚本控制。有声书场景里,整段输入更省事但长文容易触发超时或长度上限,切句后拼接更可控但接缝处的停顿和语气衔接要单独检查。
做法建议:先用同一份带标点的长文本分别跑整段输入和按句切分两条链路,记录耗时、输出位置与接缝现象,再决定走哪条。判断依据是文本类型而非模型能力——叙述类长句多,倾向整段或按段落切;对白类跨句情绪强,接缝处最容易听出断裂;列表类条目之间本来就有停顿,按句切反而更自然。拼接后处理往往需要重采样或淡入淡出,具体参数要结合你的采样率和播放器行为确认。
准备一段带标点的长文本做测试样本
两种调用方式必须跑在同一份输入上,否则差别来自文本而不是切分策略。样本建议覆盖你实际有声书的文体:叙述段、对白段、带编号的列表段各留几段,总长度可以先取一两千字,确保它明显超过你配置里的单次文本上限,这样整段输入到底是被截断、报错还是分块处理,能一眼看出来。
标点分布要刻意铺开:句号、问号、感叹号、分号、逗号、顿号、冒号、引号、省略号、破折号、换行,再故意放几个小数点数字(如 3.14)和英文缩写,用来验证切句规则会不会误切。保存为 UTF-8 无 BOM 的纯文本,路径按项目约定放,例如 data/long_sample.txt,同时用脚本统计字数和各标点出现次数,把结果留档,后面比对两条链路时用得上。
python3 - <<'PY'
import collections, pathlib
txt = pathlib.Path("data/long_sample.txt").read_text(encoding="utf-8")
punct = "。!?;,、:…—\"\"''\n"
c = collections.Counter(ch for ch in txt if ch in punct)
print("chars", len(txt), "lines", txt.count("\n"))
print(dict(c))
PY
整段传入跑一次并记录耗时与输出
整段链路的调用方式就是把文件内容一次性放进请求的文本字段,或者用 CLI 直接传文件路径。接口细节按你手头的 SDK 替换,下面的骨架只表达“一次调用、一个输出文件”这件事:
# 占位接口,按实际 SDK 或 HTTP 服务替换字段名
text = open("data/long_sample.txt", encoding="utf-8").read()
resp = tts.synthesize(text=text, voice=VOICE_ID, out="out/whole.wav")
print(resp.status, resp.error_msg)
输出统一放在 out/whole.wav,日志按次写一行,字段至少包含:调用时间、输入字数、wall_time(从发请求到落盘)、audio_duration(生成音频时长)、首字节延迟、返回状态码或错误信息、是否被截断。如果服务端支持分块返回,把分块数和每块字数也记下来,这样“整段喂会不会断”就有可查的证据,而不是靠听感猜。
按标点切句后逐句合成再拼接
切句链路的切分规则建议保守一点:句末标点(。!?;…)后切一刀,逗号默认不切,只有当某一小段超过你设的阈值时才在逗号或顿号处二次切;引号内的内容尽量不打断,省略号、破折号归到前一句。小数点、英文缩写、时间格式这类要显式排除,否则会在数字中间断开。
import re, json, pathlib
txt = pathlib.Path("data/long_sample.txt").read_text(encoding="utf-8")
parts = re.split(r"(?<=[。!?;…])", txt)
segs, MAX = [], 60
for p in parts:
p = p.strip()
if not p:
continue
if len(p) > MAX:
segs += [s for s in re.split(r"(?<=[,、])", p) if s.strip()]
else:
segs.append(p)
pathlib.Path("work").mkdir(exist_ok=True)
manifest = []
for i, s in enumerate(segs, 1):
f = f"work/seg_{i:04d}.txt"
pathlib.Path(f).write_text(s, encoding="utf-8")
manifest.append({"id": i, "file": f, "text": s})
pathlib.Path("work/manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print("segments", len(segs))
逐句合成时中间文件按序号补零命名,例如 work/seg_0001.wav,保证排序不出错;合成的顺序、每句用的参数都写进 manifest,方便回查。拼接前先生成 concat 列表,再执行 ffmpeg:
for f in work/seg_*.wav; do echo "file '$PWD/$f'"; done > work/list.txt
ffmpeg -f concat -safe 0 -i work/list.txt -c copy out/split_joined.wav
# 采样率或声道不一致时 -c copy 会报错,改用重编码:
# ffmpeg -f concat -safe 0 -i work/list.txt -ar 44100 -ac 1 out/split_joined.wav
逐段听接缝处确认是否连贯
这一步只看现象,不下“音质好坏”的判断。按接缝序号逐个定位:前一句最后一个字、后一句第一个字、接缝处静音长度、前后语速是否有变化、音高是否跳、语气是否在中途断掉、有没有咔哒声或削波。静音长度可以用静音检测粗略量化,再配合人工听确认:
ffmpeg -i out/split_joined.wav -af silencedetect=noise=-40dB:d=0.15 -f null -
把每处接缝记成一行:接缝编号、对应句号范围、静音时长、语速变化方向、音高变化位置、是否有爆音。同一份样本再用整段链路输出跑一遍,对比同一句话位置是否有停顿——如果整段输出在该处没有停顿而切句拼接有,那这个停顿就是你切句引入的,属于脚本层要调的东西。
按文本类型给出选择依据
- 叙述类:句子偏长、跨句语气连续,整段输入通常更稳;确需切分时优先按段落切,而不是每个句号切一刀,减少人为接缝数量。判断依据是段落末尾是否本身就是自然停顿点。
- 对白类:说话人切换处是天然切点,按引号和说话人切比按句号切更合理;但同一人连续的几句话情绪往往连贯,切碎了容易在接缝处听到语气重置,建议以“一个说话人的一段话”为单位,而不是一句一合。
- 列表类:条目之间本来就有停顿,按条目或按句切再拼接,听感上通常更接近朗读习惯;这类文本对切分的容忍度最高,也最适合用切句链路规避长度上限。
实际操作可以先按上面的样本跑两条链路,把耗时字段和接缝记录放在一起比对,再针对你主要的文本类型固定一套切分参数。整段输入如果稳定且没有长度问题,就没有必要为了切分而切分;只有当输入长度触发上限、超时或截断时,切句拼接才是必要的兜底手段。