SparkAudio 1.0 批量长文本转语音,先解决分段与拼接的接缝

文章导读
把整篇文稿一次性丢进 SparkAudio 1.0 合成,常见两类表现:一是音频在中途停住,后面的文字没有念出来;二是勉强生成完整,但段与段接缝处有明显停顿、爆音或吞字。这两类问题多半不在模型本身,而在三件事没有固定下来:单次请求的文本长度、每段输出参数是否一致、拼接时是否统一插入静音。先把长文按标点切成可控小段,再固定采样率与容器格式,最后用同一套参数拼接并逐段核对时长,接缝问题就能退回到具体某
📋 目录
  1. 一 把一篇长文稿按标点切成可控小段
  2. 二 在每段请求里确认文本与音频参数
  3. 三 把分段音频按顺序拼接并插入固定静音
  4. 四 逐段试听定位接缝处的爆音或断字
A A

把整篇文稿一次性丢进 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 批量长文本转语音,先解决分段与拼接的接缝

在每段请求里确认文本与音频参数

分段之后,每段请求的输出必须完全一致,否则拼接阶段会先出问题。采样率、位深、声道数、输出容器这四项要显式写进请求或配置文件,不要依赖服务端默认值,因为默认值可能随接入方式变化。下面是一个通用请求骨架,字段名以 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 段前面这种低级错位。

SparkAudio 1.0 批量长文本转语音,先解决分段与拼接的接缝

把分段音频按顺序拼接并插入固定静音

接缝处的断裂感,多数来自段与段之间直接首尾相接,缺少一点停顿;而爆音和杂音,多数来自各段参数不一致,被强行拼到一起。所以顺序是:先确认所有段落采样率、声道、编码一致,再插入统一长度的静音。

先生成一个与素材同参数的静音文件,再用 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) × 静音时长,偏差明显就说明有段落没进去或参数不一致。

SparkAudio 1.0 批量长文本转语音,先解决分段与拼接的接缝
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 调小;每段单独播放都正常、拼接后段首出现爆音,多半是采样率、声道或静音文件参数不一致,回到第二步统一参数并重新生成静音;接缝处停顿突兀或语调断裂,多半是切分点落在句子中间,回到第一步改切分规则,让断点尽量落在句末标点。

波形检查也可以辅助定位:把拼接结果放大到接缝附近,看是否有超出正常范围的尖峰或突变的电平跳变,或者段首极短的一段静音被硬接上去。这类现象对应的处理动作仍然是统一参数、重拼,而不是在成品上做降噪补救。切分的代价也要一并考虑:切得太碎会增加请求次数、语调连贯性变差;静音拉得太长会听起来像卡顿。先用一篇文章跑通全流程、逐段听过,再决定是否批量处理,是比较稳妥的推进方式。