Whistle 单次最多 30 秒。直接截断会丢字,正确做法是在安静处切——本文给出切点算法、可运行脚本与词时间戳回移方法。
输入录音总时长,看看会切成几段(按 24 秒标记推算,实际切点会落在窗口内最安静的帧上)。
秒Whistle 的编码器是非因果的:一个 3 秒处的词可以参考 12 秒处的信息。这意味着它必须一次看到整段音频,没法边听边出字。音频前端按 10 ms 步长生成帧,卷积 stem 三次减半后,30 秒音频压成 375 帧(每帧 80 ms)——这就是 30 秒上限的来源。长音频不是模型偷懒,是架构决定的。
① 自己切成 ≤30 秒的片段再逐段转录(本文方案,最可控);② 用上层框架的流式接口,由框架内部切片与拼接;③ 换用支持长音频的模型。若你要的是会议转写,Whistle 本身就不合适——它在 AMI 会议集上词错率 26.07%,约每四个词错一个,换模型比优化切分更划算。
关键参数:在每 24 秒标记附近搜索 ±3 秒的窗口,取窗口内能量最低的 20 ms 帧作为切点。为什么是 24 秒而不是 30 秒:留出余量,避免片段尾部被截断。为什么找安静帧:人说话的词与词之间有自然停顿,在停顿处切不会把任何一个词劈成两半。
import wave, numpy as np
def frame_rms(x, sr, frame_ms=20, hop_ms=10):
f, h = int(sr*frame_ms/1000), int(sr*hop_ms/1000)
win = np.lib.stride_tricks.sliding_window_view(x, f)[::h]
return np.sqrt((win.astype(np.float64)**2).mean(axis=1)), np.arange(len(win))*h
def find_cut_points(x, sr, mark_s=24.0, search_s=3.0, max_s=30.0):
maxlen = int(sr*max_s)
if len(x) <= maxlen:
return [0, len(x)]
mark, window = int(sr*mark_s), int(sr*search_s)
cuts, pos = [0], 0
while len(x) - pos > maxlen:
center = pos + mark
lo = max(pos+1, center-window)
hi = min(center+window, pos+maxlen-1)
rms, offs = frame_rms(x[lo-int(sr*0.02):hi], sr)
pos = lo + int(offs[int(np.argmin(rms))]) if len(rms) else pos+maxlen
cuts.append(pos)
cuts.append(len(x))
return cuts
切完之后每段单独送进 Whistle,再把各段的词时间戳加上该段在原录音里的偏移量,拼回完整时间轴。
import needle
from core.splitter import read_wav_mono, find_cut_points, shift_words
x, sr = read_wav_mono("long.wav") # 必须是 16 kHz mono 16-bit PCM
cuts = find_cut_points(x, sr)
segments, language = [], None
for i in range(len(cuts)-1):
piece = x[cuts[i]:cuts[i+1]]
out = needle.transcribe(piece, language=language, word_timestamps=True)
language = language or out["language"] # 首片检测到的语言,后续锁定
segments.append({"offset_s": cuts[i]/sr, "words": out.get("words", [])})
print(out["text"])
words = shift_words(segments) # 回移到整段录音的时间轴
① 静音短路:引擎在解码前先测响度,低于阈值直接返回空文本,不会进入解码。整段静音的片段会得到空结果,这是设计如此,不是 bug。② 空片段:长录音里的静音区会被切成独立片段,产出空文本,拼接时要过滤。③ token 上限:单片段转写上限 320 个 token,语速极快的段落可能被截断。④ 单次请求上限约 1 小时,更长的录音需自行分批。
有用户在 Hacker News 上报告:把 Whistle 跑在一集电视剧上,它有时卡住、连续 60 秒输出「Thank you.」。官方的静音短路能处理纯静音,但超出 30 秒窗口的嘈杂长音频仍可能触发填充词。这是 16.9 MB 小模型的已知边界,遇到时优先怀疑音频是否超出了设计场景,而不是调参。
Whistle 自己只能读 WAV(各种位深)与裸 PCM16。MP3、M4A、OGG、FLAC 等需要先转:ffmpeg -i in.mp3 -ar 16000 -ac 1 out.wav
在安静帧处切不会。但 30 秒边界附近如果有连续语音,找不到安静点,算法会退化为硬切——此时仍可能丢字。实测建议先跑一遍看切点的帧能量:接近 0 说明切在了静音上。
Whisper 内部也是 30 秒窗口,只是它把补齐与滑动窗口封装好了。Whistle 把这些暴露给了调用方,代价是要自己实现,好处是你可以控制切点。