PAYATHON 2026

coqui-tts 的 VITS 导出为 ONNX 后,如何支持动态输入和音频长度?

AI 小顾

结论

不要通过增大 dummy_input_length 来解决音频长度问题。示例输入用于导出模型,不应作为所有文本的固定音频长度配置。

代码已经为输入和输出声明了动态维度,但模型内部的长度计算仍可能被固化。例如,推理时将 Tensor 中的长度转换成 Python 数值,或使用固定长度构造 mask、对齐路径和切片,都可能让导出的计算图保留示例输入对应的长度。

“修改示例输入长度后,输出音频长度也变化”提示可以优先检查这类问题。不过,现有材料没有提供 self.inference() 及其调用函数的实现,无法确定具体是哪一行导致的,也不能断言尾部噪声只有这一个原因。

先确认限制出现在什么位置

需要区分三个长度:

  • 文本 token 数:input.shape[1]。
  • 模型预测的声学帧数:通常由时长预测和对齐计算产生。
  • 输出波形的采样点数:决定音频文件的播放时长。

输入能接受不同数量的 token,仍需分别验证声学帧数和波形采样点数是否可变。

先打印 ONNX Runtime 看到的输入和输出:

import onnxruntime as ort

session = ort.InferenceSession("model.onnx")

for item in session.get_inputs():
    print("input:", item.name, item.shape, item.type)

for item in session.get_outputs():
    print("output:", item.name, item.shape, item.type)

如果文本维度显示为 phonemes 等符号,说明导出文件声明了动态输入。还要用不同长度的文本实际运行,观察输出数组的形状,不要只凭 WAV 的听感判断。

下面的函数可以替换测试代码中的输入组装与推理部分。ids 应当由训练时相同的 tokenizer 生成,sample_rate 应取自模型配置:

import numpy as np


def run_case(session, ids, sample_rate, speaker_id=0, language_id=0):
    if not ids:
        raise ValueError("Token sequence must not be empty")

    candidates = {
        "input": np.asarray([ids], dtype=np.int64),
        "input_lengths": np.asarray([len(ids)], dtype=np.int64),
        "scales": np.asarray([0.667, 1.0, 0.8], dtype=np.float32),
        "sid": np.asarray([speaker_id], dtype=np.int64),
        "langid": np.asarray([language_id], dtype=np.int64),
    }

    required_names = {item.name for item in session.get_inputs()}
    missing = required_names - candidates.keys()
    if missing:
        raise ValueError(f"Unsupported model inputs: {sorted(missing)}")

    feeds = {name: candidates[name] for name in required_names}
    output = session.run(None, feeds)[0]

    print("tokens:", len(ids), "output shape:", output.shape)

    # Assumes the verified output layout is [batch, channel, samples].
    if output.ndim != 3 or output.shape[:2] != (1, 1):
        raise ValueError(f"Check output layout before extracting audio: {output.shape}")

    audio = output[0, 0].astype(np.float32)
    if not np.isfinite(audio).all():
        raise ValueError("Model output contains NaN or Inf")

    print("samples:", audio.size, "seconds:", audio.size / sample_rate)
    return audio

分别用短句、中等长度句子和长句测试。自然语音的时长不必与 token 数严格成比例,但不应仅由导出时的示例长度决定。

检查模型内部是否把长度固化了

当前设置了 dynamo=False。排查时查看导出日志中的 TracerWarning,特别留意 Tensor 转换为 Python 数值或布尔值的提示。

从 self.inference() 向下检查:

  • 根据 text_lengths 生成文本 mask 的函数。
  • 时长预测之后计算声学帧数的代码。
  • 构造对齐路径、输出 mask 和中间张量的函数。
  • 解码前后的切片,以及实现中可能存在的输出长度上限。

以下写法值得检查,但出现这些写法并不一定有错。要看其结果是否用于本应随输入变化的计算:

length = int(length_tensor)
length = length_tensor.item()
length = length_tensor.tolist()

for i in range(length):
    ...

例如,下面的模式可能让 tracing 记录示例输入对应的长度:

max_len = int(lengths.max().item())
positions = torch.arange(max_len, device=lengths.device)
mask = positions.unsqueeze(0) < lengths.unsqueeze(1)

可以尝试让长度继续参与 Tensor 运算:

max_len = lengths.max()
positions = torch.arange(max_len, device=lengths.device)
mask = positions.unsqueeze(0) < lengths.unsqueeze(1)

这只是一个示例,需要结合实际函数修改,并不是当前模型的完整补丁。修改后仍需确认导出器支持相关算子,并验证短文本和长文本的运行结果。不要机械地替换模型代码中所有的 .item()。

如果长度来自 Tensor 的形状,也应尽量保留形状运算与输入之间的依赖,避免通过 Python 容器或常量重新构造。

动态轴应对应真实的张量布局

你目前将输出的第 1、2 维都命名成时间维:

"output": {0: "batch_size", 1: "time1", 2: "time2"}

先打印 PyTorch 的原始输出形状。如果确认波形布局是 [B, 1, T],第 1 维是单声道维度,第 2 维才是采样点维度,可以将原导出调用的 dynamic_axes 改为:

dynamic_axes={
    "input": {0: "batch_size", 1: "phonemes"},
    "input_lengths": {0: "batch_size"},
    "output": {0: "batch_size", 2: "samples"},
},

如果实际使用多说话人或多语言输入,且模型实现支持动态 batch,可以按需为 sid、langid 的第 0 维补充动态轴。仅添加这些声明,无法让原本不支持动态 batch 的内部逻辑自动支持它。

上述调整能让维度含义更准确,但不能修复已经固化在计算图里的长度。opset_version=18 也不能单独保证动态长度正确。

用相同输入对比 PyTorch 和 ONNX

使用同一组 token、input_lengths、说话人、语言和 scales,分别运行 PyTorch 与 ONNX:

观察结果优先检查的位置
PyTorch 输出长度变化,ONNX 基本固定导出过程中的常量化、固定形状操作或算子转换
两者都出现长度限制原始推理逻辑、输出长度上限或输入处理
输出采样点数变化,但播放时长明显异常WAV 写入时使用的采样率
两者都有尾部噪声tokenizer、语言和说话人配置、模型本身的生成结果

VITS 推理可能包含随机采样,因此不宜要求两次输出的每个采样点完全相同。先比较形状、时长和是否存在明显截断,再进一步检查数值差异。

核对文本处理、时长参数与采样率

使用训练时相同的 tokenizer

当前 text_to_ids() 按字符查表,并在每个字符后追加 <BLNK>。这是否正确,取决于模型训练时的文本处理配置。

需要核对是否使用 phonemizer、文本清洗规则、blank 插入位置、BOS/EOS 以及 token ID 映射。塞尔维亚语文本与训练语言一致,也不能证明逐字符编码与训练输入一致。

原代码遇到未知字符时会跳过。排查阶段建议先报错,避免文本被删减后继续推理:

if ch not in token_map:
    raise ValueError(f"Unknown character: {ch!r}")

先把 length_scale 设为 1.0

按照导出包装函数中的索引,测试参数:

scales = np.array([0.667, 3.0, 0.8], dtype=np.float32)

对应的 length_scale 是 3.0。按该参数的通常语义,它会拉长预测时长。建议先用 1.0 建立对照,避免干扰对长度问题的判断。

同时检查 self.inference() 是否直接使用这些 Tensor 参数。如果内部又将其转换成 Python 数值,参数也可能在导出过程中被固化。仅在函数入口给 self.length_scale 赋值,不能证明它在 ONNX 推理时一定生效。

WAV 采样率取自模型配置

原代码固定使用:

sf.write("output.wav", audio, 22050, format='WAV')

只有模型的输出采样率确实为 22050 时,这样写才正确。音频时长为:

duration_seconds = number_of_samples / sample_rate

采样率写错会改变播放速度和时长,但不会改变 ONNX 返回的采样点数,因此应与计算图长度问题分别检查。

排查期间也建议先检查 NaN/Inf 和原始幅值,再决定是否执行 np.nan_to_num() 或归一化。这些后处理无法修复错误的长度计算,还可能掩盖异常输出。

修复完成后的验证标准

重新导出后,至少要确认不同 token 长度的有效文本都能运行,输出采样点数会随内容和预测时长变化,并且相同文本不会仅因更换 dummy_input_length 就出现固定的截断或多余尾部。

如果问题仍然复现,需要进一步提供 coqui-tts、PyTorch、ONNX Runtime 版本,以及 self.inference() 中时长预测、mask 和对齐路径的相关实现。现有代码足以确定排查方向,但不足以给出某个内部函数的确定补丁。

备注:内容仅供参考。