把整篇文稿一次性丢进 SparkAudio 1.0 合成,常见两类表现:一是音频在中途停住,后面的文字没有念出来;二是勉强生成完整,但段与段接缝处有明显停顿、爆音或吞字。这两类问题多半不在模型本身,而在三件事没有固定下来:单次请求的文本长度、每段输出参数是否一致、拼接时是否统一插入静音。先把长文按标点切成可控小段,再固定采样率与容器格式,最后用同一套参数拼接并逐段核对时长,接缝问题就能退回到具体某一步,而不是继续盲目重试。
长文本批量合成建议先固定三件事:单次请求字数上限、每段输出格式一致、拼接时统一段间静音。切分按句末标点→逗号→字数硬切三级回退,句末标点保留在段尾;段间静音固定为 200ms 左右即可,先用 ffprobe 核对每段时长,再逐段试听。接口字段以 SparkAudio 1.0 实际文档为准,参数名可能不同,先跑通再逐项加。
把一篇长文稿按标点切成可控小段
切分的目标不是切得越碎越好,而是让每一段都落在接口能稳定返回的长度内,同时保证断点落在语义自然的位置。建议做三级回退:先按句号、问号、感叹号、分号切句;如果单句仍然超过上限,再按逗号、顿号切;如果连逗号都没有(比如大段引用或没有标点的口语稿),才按最大字数硬切。硬切是兜底,用多了语调会断,所以上限不要设得太小。
切分时有两个细节容易被忽略:句末标点要留在段尾,不要把标点单独留在下一段开头,否则模型收尾语调会变平;段落之间不要做 trim 之外的清洗,换行和数字格式保持原样,避免字数统计和实际发音字数不一致。
import re
MAX_CHARS = 120 # 单次请求字数上限,先按接口限制和试听感受调
SENT_END = "。!?;!?;"
COMMA = ",、,::"
def split_long(text, max_chars=MAX_CHARS):
text = text.strip()
# 一级:按句末标点切,标点保留在前一段
parts = re.split(r'(?<=[%s])' % re.escape(SENT_END), text)
segs = []
for p in parts:
p = p.strip()
if not p:
continue
if len(p) <= max_chars:
segs.append(p)
continue
# 二级:按逗号类标点切
subs = re.split(r'(?<=[%s])' % re.escape(COMMA), p)
buf = ""
for s in subs:
if len(buf) + len(s) <= max_chars:
buf += s
else:
if buf:
segs.append(buf)
# 三级:仍超长则按字数硬切
while len(s) > max_chars:
segs.append(s[:max_chars])
s = s[max_chars:]
buf = s
if buf:
segs.append(buf)
return segs
for i, seg in enumerate(split_long(open("script.txt", encoding="utf-8").read()), 1):
print(f"{i:03d}\t{len(seg)}\t{seg[:24]}")
打印出来的就是分段清单,序号、字数、开头若干字三项足够核对。清单示例:
- 001 86 字 “各位好,今天这次分享主要围绕……”
- 002 104 字 “第一件事是把长文切成可控小段……”
- 003 61 字 “这里补充一个例外情况……”
如果发现某一项字数明显高于其他段,说明它的句内没有可切标点,建议回头在文稿里补一个逗号,而不是让硬切规则去处理。
在每段请求里确认文本与音频参数
分段之后,每段请求的输出必须完全一致,否则拼接阶段会先出问题。采样率、位深、声道数、输出容器这四项要显式写进请求或配置文件,不要依赖服务端默认值,因为默认值可能随接入方式变化。下面是一个通用请求骨架,字段名以 SparkAudio 1.0 的实际文档为准,鉴权与地址用占位符。
POST {BASE_URL}/tts
Authorization: Bearer {TOKEN}
Content-Type: application/json
{
"text": "第 002 段正文……",
"voice": "{VOICE_ID}",
"speed": 1.0,
"format": "wav",
"sample_rate": 24000,
"bit_depth": 16,
"channels": 1
}
三个关键参数建议统一记录在一个地方,而不是散落在脚本里:采样率、位深、容器格式写进 config 文件(或环境变量),请求体从配置读取;同时在日志里把本次实际使用的参数打一行出来,出问题时能和落盘文件对上。如果响应里带回请求 id 或时长元信息,一并写进同名 sidecar 文件。
落盘命名建议带序号和参数,便于排序和排查:out/seg_001_24000_16_wav.wav,同时写 out/seg_001.json 记录该段原文、字数、请求参数。序号补零到三位,保证 sorted() 顺序和文稿顺序一致,避免第 10 段排在第 2 段前面这种低级错位。
把分段音频按顺序拼接并插入固定静音
接缝处的断裂感,多数来自段与段之间直接首尾相接,缺少一点停顿;而爆音和杂音,多数来自各段参数不一致,被强行拼到一起。所以顺序是:先确认所有段落采样率、声道、编码一致,再插入统一长度的静音。
先生成一个与素材同参数的静音文件,再用 ffmpeg 的 concat 分离器按列表拼接:
# 生成 200ms 静音,参数必须与分段音频一致
ffmpeg -f lavfi -i anullsrc=r=24000:cl=mono -t 0.2 -c:a pcm_s16le silence_200ms.wav
# list.txt,段与段之间插入静音文件
file 'seg_001.wav'
file 'silence_200ms.wav'
file 'seg_002.wav'
file 'silence_200ms.wav'
file 'seg_003.wav'
ffmpeg -f concat -safe 0 -i list.txt -c copy out_full.wav
用 -c copy 的前提是各段编码、采样率、声道完全一致;只要有一项不同,就改成重新编码(例如 -ar 24000 -ac 1 -c:a pcm_s16le),否则容易出现忽快忽慢或杂音。用 pydub 拼接则更直观:
from pydub import AudioSegment
GAP_MS = 200
paths = [f"out/seg_{i:03d}.wav" for i in range(1, 4)]
segs = [AudioSegment.from_file(p) for p in paths]
out = AudioSegment.empty()
for i, s in enumerate(segs):
out += s
if i != len(segs) - 1:
out += AudioSegment.silent(duration=GAP_MS, frame_rate=segs[0].frame_rate)
out.export("out_full.wav", format="wav")
静音时长建议先取 150–300ms 试听,陈述性文稿偏短一点,段落之间有语义转折时可略长;不要用静音去填补某段被截断的空缺,那是掩盖问题。拼接后核对顺序:用 ffprobe 逐段打印时长,拼接总时长应接近各段时长之和加上 (段数−1) × 静音时长,偏差明显就说明有段落没进去或参数不一致。
for f in out/seg_*.wav out_full.wav; do
printf "%s\t" "$f"
ffprobe -v error -show_entries format=duration -of csv=p=0 "$f"
done
逐段试听定位接缝处的爆音或断字
出问题时不要整篇反复重合成,先逐段听。每段的检查点有三个:开头第一个字是否完整、结尾最后一个字是否收住、时长是否与字数大致匹配。中文播报通常在每秒三到五字之间,语速参数不同会有偏移,但同一篇文稿里各段的“秒/字”比值应当接近,明显偏离的就是异常段。
from pydub import AudioSegment
for i, p in enumerate(sorted(__import__("glob").glob("out/seg_*.wav")), 1):
a = AudioSegment.from_file(p)
print(p, round(a.duration_seconds, 2), a.frame_rate, a.channels, round(a.dBFS, 1))
判断依据可以按现象对应:某段时长明显偏短、末尾文字没念完,多半是单次请求文本过长或超时导致返回不完整,回到第一步把 MAX_CHARS 调小;每段单独播放都正常、拼接后段首出现爆音,多半是采样率、声道或静音文件参数不一致,回到第二步统一参数并重新生成静音;接缝处停顿突兀或语调断裂,多半是切分点落在句子中间,回到第一步改切分规则,让断点尽量落在句末标点。
波形检查也可以辅助定位:把拼接结果放大到接缝附近,看是否有超出正常范围的尖峰或突变的电平跳变,或者段首极短的一段静音被硬接上去。这类现象对应的处理动作仍然是统一参数、重拼,而不是在成品上做降噪补救。切分的代价也要一并考虑:切得太碎会增加请求次数、语调连贯性变差;静音拉得太长会听起来像卡顿。先用一篇文章跑通全流程、逐段听过,再决定是否批量处理,是比较稳妥的推进方式。