译文时断时续,先不要急着怀疑模型或网络,第一步应该确认喂进流式接口的音频是否和模型期望的输入格式一致。Hy-SimulMT 这类流式同传,输入侧通常隐含两个假设:固定采样率的单声道波形,以及按固定采样点切分的音频块。如果麦克风是 44.1kHz 或 48kHz、模型按 16kHz 解释,或者双声道数据被当成单声道读取,模型拿到的就是被拉伸或声道交错错位的波形,表现出来往往是短句被切碎、译文卡顿、时间戳缓慢漂移。处理顺序建议是:先测量真实格式,再统一转换,然后检查分块边界,最后用带停顿的对话音频确认连续性。
采样率与声道数是流式同传最先要对齐的两项输入设置。建议先用 ffprobe 读出音频源的真实采样率和声道数,在送入模型前统一重采样到模型声明的采样率并转为单声道,再按固定采样点而不是按字节数或浮点毫秒切块。验证方式是重跑一段带停顿的对话音频,观察译文是否只在说话人停顿处断开,并对比格式转换前后的时间戳差值。转换带来的时间开销需要结合机器性能和音频长度自行确认。
用 ffprobe 确认输入音频的采样率和声道数
先拿到音频源的真实参数,不要凭录音软件界面或设备标称值判断。对已经落盘的音频文件,可以直接用 ffprobe 读取第一个音频流的关键字段:
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels,channel_layout,sample_fmt,duration \
-of default=noprint_wrappers=1 input.wav输出里几个字段的含义需要看清楚:sample_rate=44100 表示每秒 44100 个采样点,如果模型声明的是 16000,就必须重采样;channels=2 配合 channel_layout=stereo 说明是双声道,直接按单声道读会把左右声道交错成两倍长度的错误波形;sample_fmt 是 s16、s32 还是 flt,决定了解码时要不要做整数到浮点的归一化。如果输入是麦克风或网络流而不是文件,可以先用同样的参数探测源地址,或者用 ffmpeg -i 源 -t 3 -f null - 打印一段输入信息,三秒足够看到格式行。
这一步的产出是一份明确的“输入画像”:采样率、声道数、采样格式。后续所有转换都以这份画像为基准,而不是以“应该是 16k 吧”的猜测为基准。
在模型输入前做重采样和声道转换
拿到真实格式后,在进入模型之前统一成模型可接受的格式。下面是一个可替换参数的骨架,用于离线文件或整段音频的前置处理:
import numpy as np, soundfile as sf
from scipy.signal import resample_poly
from math import gcd
TARGET_SR = 16000 # 替换为模型声明的采样率
TARGET_CH = 1 # 替换为模型期望的声道数
def to_model_format(path):
audio, sr = sf.read(path, dtype="float32", always_2d=True) # (frames, ch)
if TARGET_CH == 1 and audio.shape[1] > 1:
audio = audio.mean(axis=1, keepdims=True) # 多声道下混
if sr != TARGET_SR:
g = gcd(sr, TARGET_SR)
audio = resample_poly(audio, TARGET_SR // g, sr // g, axis=0)
return np.ascontiguousarray(audio[:, 0]), TARGET_SR需要替换的就是 TARGET_SR 和 TARGET_CH 两项,分别对应模型的采样率约定和声道约定。对真正的流式场景,整段 resample_poly 并不合适,因为它每次调用都在块边界重新起算,容易在拼接处产生轻微不连续;更稳妥的做法是用带内部状态的流式重采样器,或者干脆用外部命令一次性转好管道:
ffmpeg -i input.wav -ar 16000 -ac 1 -f s16le -acodec pcm_s16le -把重采样和声道转换放在模型输入之前而不是模型内部,好处是格式问题会在这一层暴露成可观测的日志,而不是变成译文层面的断续现象。
检查流式分块是否按固定采样点切分
格式对齐之后仍然断续,常见原因是分块边界算错。按字节数切、按浮点毫秒数累加切、或者对双声道数据按样本交错切,都会让某一块的起点落在采样点中间或半个声道上,听感就是每隔一段出现一次卡顿或噪声。建议统一改成按采样点整数计数切块:
CHUNK = 1600 # 替换为模型期望的块长,例如 100ms @16kHz
buf = np.empty(0, dtype=np.float32)
while True:
new_samples = read_from_source() # 已转成 TARGET_SR/单声道
buf = np.concatenate([buf, new_samples])
while buf.shape[0] >= CHUNK: # 边界判断只看采样点数
chunk, buf = buf[:CHUNK], buf[CHUNK:]
send_to_model(chunk) # 长度恒为 CHUNK
if flush_on_end and buf.shape[0] > 0:
send_to_model(buf) # 尾块单独标记,不要补齐成整块边界判断的核心是 buf.shape[0] >= CHUNK 这种整数比较,而不是 int(elapsed_ms * sr / 1000) 之类的浮点换算,浮点累加会在一段时间后产生一个采样点的偏差,正好让块首尾错位。另外需要确认尾块的处理方式:补零成整块会让模型多算一段静音,直接丢弃会吃掉句尾,通常的做法是单独标记尾块并告知模型这是最后一块。
用带停顿的对话音频验证译文连续性
格式和分块都改完之后,用一段自己构造的音频做回归验证,而不是随手录一句话。建议的特征是:30 到 60 秒、两个人交替说话、句与句之间留 0.5 到 1.5 秒静音、中间插入一处 3 秒以上的长停顿、至少包含一个短于两秒的快速应答。这样的结构能把“模型正常断句”和“格式错误导致的断裂”区分开。
观察点集中在四处:译文的分句位置是否只落在静音区间;同一句话里是否出现重复输出的半句;长静音段是否返回空结果而不是持续吐字;译文的起止时间是否单调递增、没有回跳。如果断裂从“每句中间都断一次”变成了“只在长停顿后重新起句”,通常说明格式已经对齐,剩下的属于模型分段策略问题,不应再回到采样率上排查。
记录格式转换前后的时间戳差值
重采样本身会引入少量处理开销,需要确认它没有把端到端延迟推高到影响同传的程度。做法是在每块音频发出前,同时记录两个时间基准:原始音频中的采样点位置除以原始采样率,以及转换后已发送的采样点数除以目标采样率,两者相减即为该块的时间偏移。
把结果记成一张简单的表,跑一段几分钟的音频就能看出是常数偏移还是累积漂移:
| 块序号 | 原始时间戳(样本/原始SR) | 转换后时间戳(样本/目标SR) | 差值(ms) | 累计漂移(ms) |
|---|---|---|---|---|
| 1 | 待记录 | 待记录 | 待记录 | 待记录 |
| 2 | 待记录 | 待记录 | 待记录 | 待记录 |
| N | 待记录 | 待记录 | 待记录 | 待记录 |
判断方式很直接:差值基本持平,说明重采样只是引入固定的小偏移,属于可接受范围;累计漂移随块序号持续增长,通常意味着采样率换算用了近似整数比,或者流式重采样器没有正确保留状态,需要回到第二、第三节重新检查参数。这页里不给具体数值,因为不同机器、不同音频长度下的开销差别很大,需要在目标环境里按上表自行跑一遍确认。