Luna-TTS 批量合成多段文本 / 先统一每段的长度和文件命名

文章导读
一次要合成几十段文本时,真正拖慢进度的通常不是合成速度,而是前面没把「每段文本是什么、对应哪个文件、用了什么参数」记清楚。手动一段段提交,漏段往往到全部跑完才发现;脚本能解决重复劳动,但脚本本身也需要先把切分粒度、序号规则和输出目录定下来。可以先按「切分 → 命名 → 循环 → 逐段校验 → 断点重跑」这五步走,每一步都能用命令或日志验证,再决定要不要接进更完整的流程。
📋 目录
  1. 一 先把待合成文本切成等长段落并编号
  2. 二 统一参数与文件命名,让输出能对应回原文
  3. 三 写一段循环骨架把每段依次交给合成入口
  4. 四 每跑完一段就校验文件数、时长和文本对应关系
  5. 五 处理中断与重跑,只补失败的段落
A A

一次要合成几十段文本时,真正拖慢进度的通常不是合成速度,而是前面没把「每段文本是什么、对应哪个文件、用了什么参数」记清楚。手动一段段提交,漏段往往到全部跑完才发现;脚本能解决重复劳动,但脚本本身也需要先把切分粒度、序号规则和输出目录定下来。可以先按「切分 → 命名 → 循环 → 逐段校验 → 断点重跑」这五步走,每一步都能用命令或日志验证,再决定要不要接进更完整的流程。

适合场景:几十到几百段文本、需要可核对和可补跑的批量合成任务。操作方向是先把文本切成带唯一序号的段落文件,再用「批次号+序号+段首文字」命名输出,脚本里用一个占位入口函数逐段提交,并记录成功清单。验证方式是数文件数、抽查时长、比对 done 清单与序号全集。边界:具体的合成入口、参数名和并发能力取决于你实际使用的 Luna-TTS 客户端或配置,下面的骨架只是通用写法,需要按环境替换。

先把待合成文本切成等长段落并编号

合成入口通常对单次输入长度有倾向性,太长容易在末尾截断或语速漂移,太短又会让拼接痕迹变多。可以先按标点或换行做一次粗切,再把过长的句子按逗号、分号二次切开。按经验,一段控制在几十字到一两百字之间比较顺手,具体上限需要结合你所用客户端的输入限制确认。

切分粒度建议:优先保留语义完整的句子,避免在数字、英文缩写或引号中间断开;如果原文本身是带空行的段落,直接以空行和换行为主切分,比按字符数硬切更好核对。

编号规则建议用零填充的固定宽度,例如三位数的 001、002,或者带批次号的 b01_001、b01_002。零填充的好处是字典序和数字序一致,用 ls 或 sort 列目录时不会出现 10 排在 2 前面的情况。切好后落到一个纯文本文件里,一行一段,行号即序号。

切完做一次抽查,别直接进合成:

grep -c . segments.txt          # 总段数,和预期条数比
sed -n '1p;2p' segments.txt     # 看首两段文字是否完整
tail -n 2 segments.txt          # 看最后两段,确认结尾没被吃掉
awk '{print length}' segments.txt | sort -n | tail -3   # 看最长几段有多长

重点看两处:总数是否等于原文段数,首段和末段文字是否与原文一致。首尾对得上,中间通常不会出现整段丢失;发现最长段明显超出预期,就回到切分规则再拆一次。

统一参数与文件命名,让输出能对应回原文

命名要能一眼看出「第几段、说的是什么」。可以用「批次号_序号_段首若干字」作为模板,段首文字取前 8 到 12 个字符并去掉标点和路径分隔符,避免文件名里出现斜杠或冒号:

Luna-TTS 批量合成多段文本 / 先统一每段的长度和文件命名
b01_007_今天先讲三个要点.wav
b01_008_第二点是校验时长.wav

需要固定记录的参数项至少包括:发音人 / voice、语速、采样率或输出格式、切分规则版本、批次号、总段数、实际使用的入口或配置名。这些不用写得很正式,一个同目录下的 params.txt 或 manifest.tsv 就够,关键是下次补跑时能照着复现,而不是靠回忆。

容易踩的坑是把参数写进脚本却不出现在输出里。同一批文件如果有两套参数混在一起,后面根本分不清哪段是哪个版本生成的,所以批次号和参数要绑定记录。

写一段循环骨架把每段依次交给合成入口

下面是一个不依赖具体接口的骨架,入口函数用占位符 synth_one 表示,实际调用方式以你所用客户端或配置为准。骨架里包含失败跳过、日志输出和结果目录三件事:

BATCH = 'b01'
SRC   = 'segments.txt'
OUT   = 'out/'          # 结果目录,先确保存在
LOG   = 'run.log'
DONE  = 'done.txt'

params = {              # 需要与实际入口支持的参数对齐
    'voice':  '<换成实际值>',
    'speed':  1.0,
    'format': 'wav',
}

for seq, text in enumerate(read_lines(SRC), start=1):
    name = f'{BATCH}_{seq:03d}_{slug(text[:12])}.wav'
    path = join(OUT, name)
    if already_done(DONE, seq):
        log_line(LOG, seq, 'skip', path)
        continue
    try:
        synth_one(text, path, **params)   # 占位入口,按实际客户端替换
        append(DONE, seq)
        log_line(LOG, seq, 'ok', path)
    except Exception as e:
        log_line(LOG, seq, 'fail', path, err=str(e))
        continue                          # 单段失败不阻塞后面的段落

run.log 每行建议带上时间、序号、状态、输出路径和错误摘要;done.txt 只写已完成序号,越简单越好,方便后面做集合比对。先在小批量(比如十几段)上跑通再加量,能省掉很多返工。

每跑完一段就校验文件数、时长和文本对应关系

不要等全部跑完再检查。每段合成后顺手数一次文件数,成本很低:

Luna-TTS 批量合成多段文本 / 先统一每段的长度和文件命名
ls out/*.wav | wc -l      # 当前文件数
ls -l out/*.wav | tail -5 # 看最近几个文件大小是否为 0

时长抽查可以挑首段、中间段、末段各一个,用 ffprobe 读一下:

ffprobe -v error -show_entries format=duration -of csv=p=0 out/b01_001_xxx.wav

时长明显偏短或接近 0,通常意味着文本为空、被截断或写入失败,先看对应那段原文是否正常。文本对应关系则用文件名里的段首文字和 segments.txt 里同序号的行比对,抽三段即可。

发现缺段的定位步骤:先用 done.txt 与序号全集求差,找出没被记录的序号;再到 run.log 里搜这个序号,看是 fail 还是压根没有日志行。没有日志行一般是循环提前中断或输入行数与预期不符,有 fail 行则先解决该段的报错原因,再单独重跑。

comm -13 <(sort -u done.txt) <(seq -f '%03g' 1 60 | sort)   # 缺哪些序号

处理中断与重跑,只补失败的段落

中断不可避免,所以成功清单要在每段成功后就追加写入,而不是全部跑完再统一写。重跑时先读 done.txt 成集合,循环开头判断序号是否已在集合中,在就 skip,不在才调用入口函数,这就是上面骨架里 already_done 的作用。

需要注意两点:一是文件已存在但清单没记录的情况,可能是上次写到一半被打断,建议以清单为准重跑该段,让它覆盖;二是修改了切分规则或参数后,序号含义会变,应该换一个新批次号,不要在原批次上续跑,否则新旧内容会混在同一目录。

日志里建议保留的字段:序号、段首若干字(便于人眼核对)、输出路径、状态、开始与结束时间、错误信息。有这几个字段,基本能回答「哪段没成、为什么没成、重跑要跑哪几段」,不需要重新翻原文。