先切音频再对齐文本——Xiaomi-CocktailASR-1 做说话人转写

文章导读
纠结“先分离还是先识别”,通常是因为不清楚 Xiaomi-CocktailASR-1 的入口吃什么、吐什么。判断顺序的关键在于示例入口的形态:如果它按短片段输入、输出里带 speaker 字段或分离后的多路音频,那么先切音频、再让模型判说话人、最后把片段文本对齐回原始时间轴,是一条每步都能验证、出错能定位的路径;如果入口只吃整段长音频、分离在模型内部完成,先手工切分反而可能破坏模型依赖的上下文。所
📋 目录
  1. Ⅰ 固定一段双人对话音频并统一采样率
  2. Ⅱ 按静音或说话人变化切出候选片段
  3. Ⅲ 对候选片段跑 Xiaomi-CocktailASR-1 并保存 spea
  4. Ⅳ 把片段级标签映射回原始时间轴
  5. Ⅴ 人工抽听开头、重叠处和结尾三条边界片段
A A

纠结“先分离还是先识别”,通常是因为不清楚 Xiaomi-CocktailASR-1 的入口吃什么、吐什么。判断顺序的关键在于示例入口的形态:如果它按短片段输入、输出里带 speaker 字段或分离后的多路音频,那么先切音频、再让模型判说话人、最后把片段文本对齐回原始时间轴,是一条每步都能验证、出错能定位的路径;如果入口只吃整段长音频、分离在模型内部完成,先手工切分反而可能破坏模型依赖的上下文。所以先确认入口的输入输出,再决定切分粒度。

把说话人转写拆成切分、识别、回对齐三段推进:切分产出带起止时间的片段清单,识别产出片段级 speaker 与 text,回对齐把片段起点累加还原成全局时间轴。适用场景是模型按短片段工作且输出含说话人归属;验证方式是人工抽听开头、重叠处、结尾三处并核对时间轴偏移;边界在于切分粒度、说话人判属和重叠语音都可能引入误差,任一步异常都应回退到对应步骤,而不是在最终表上手工改。

固定一段双人对话音频并统一采样率

后续所有片段和对齐都以同一份基准音频为准,先把格式和参数固定下来,避免中途换源导致时间戳对不上。用 ffmpeg 统一导出单声道 16k wav,再用 ffprobe 记录时长、声道和采样率;这三个值建议写进产物说明,回对齐时用来核对总时长是否吻合。

ffmpeg -i input.m4a -vn -ac 1 -ar 16000 -c:a pcm_s16le base.wav
ffprobe -v error -show_entries stream=sample_rate,channels:format=duration -of default=noprint_wrappers=1 base.wav

需要结合环境确认的点:如果原始录音是多声道且左右声道分别对应不同说话人,保留多声道信息量更大,此时不要急着 -ac 1,可以按声道分别导出再分别切分。记录的时长也用于后续核对,所有片段时长之和应与整段时长接近,差得太多说明切分漏段或片段重叠。

按静音或说话人变化切出候选片段

切分的目的是把长音频变成模型能吃下的短片段。最省事的是按静音边界切,用 ffmpeg 的 silencedetect 先探测静音区间,再在边界处切断。噪声门限和最短静音时长需要结合录音环境确认,安静录音可以收紧,嘈杂录音要放宽,否则容易把词尾停顿误判成边界。

ffmpeg -i base.wav -af silencedetect=noise=-35dB:d=0.4 -f null - 2>&1 | grep silence

拿到静音边界后切出片段,同时设置最小片段长度,例如短于半秒的片段可与相邻片段合并,具体阈值结合语速和模型输入上限调整,并输出片段清单:

seg_id,start,end,dur,wav_path
seg_0001,0.00,3.42,3.42,segs/seg_0001.wav
seg_0002,3.80,7.15,3.35,segs/seg_0002.wav

检查点:清单里不应出现零长度或超过模型输入上限的片段;片段之间的间隙要留在清单里,回对齐时可据此判断是否需要补切。若录音存在明显重叠语音,纯静音切分会把两个说话人切进同一片段,这恰好是后面靠模型判属解决的部分。

对候选片段跑 Xiaomi-CocktailASR-1 并保存 spea

按仓库示例入口逐个片段推理。参数名、是否支持批量、是否要 manifest,都以仓库示例为准,下面是可替换的通用骨架:

python infer.py `--audio` segs/seg_0001.wav `--output` out/seg_0001.json

for f in segs/*.wav; do
  b=$(basename "$f" .wav)
  python infer.py `--audio` "$f" `--output` "out/$b.json"
done

输出通常要包含片段号、说话人标签和文本,字段名按实际返回调整,示例结构如下:

先切音频再对齐文本——Xiaomi-CocktailASR-1 做说话人转写
{"seg_id": "seg_0001",
 "speaker": "spk1",
 "text": "……",
 "start": 0.00,
 "end": 3.42}

关键约束:文件名是片段与结果的唯一纽带,务必保留 seg_id、wav 路径和 speaker 的对应关系,不要只存文本。若模型返回的是对话轮次而不是逐片段标签,先把它落成片段号到 speaker 的映射表再进入下一步。同一说话人的编号是否跨片段一致需要确认,不一致时要在回对齐阶段统一一次。

把片段级标签映射回原始时间轴

切分后片段内的 start、end 是相对该片段的局部时间,必须加上片段在原始音频中的起点才是全局时间。写脚本累加片段起点,把每个片段的识别结果拼成一张全局表:

import csv, json

rows = []
with open("segments.csv") as f:
    for seg in csv.DictReader(f):
        offset = float(seg["start"])
        path = "out/" + seg["seg_id"] + ".json"
        with open(path) as g:
            r = json.load(g)
        rows.append({
            "start": round(offset + float(r.get("start", 0)), 3),
            "end": round(offset + float(r.get("end", seg["dur"])), 3),
            "speaker": r.get("speaker"),
            "text": r.get("text", ""),
        })

with open("transcript.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=["start", "end", "speaker", "text"])
    w.writeheader()
    w.writerows(rows)

如果识别结果没返回局部 end,就用片段自身的 dur 兜底,避免出现空时间戳。合并后要抽查对齐:从 transcript.csv 里取若干条,跳到对应 start 附近收听,确认听到的内容与 text 一致、speaker 与预期一致。整体偏移、末尾时间超过原始音频总时长,都是回对齐出错的信号。

人工抽听开头、重叠处和结尾三条边界片段

不必整段复核,但边界位置必须人工听。建议至少取三条:开头第一条判断首句说话人标签有没有反,明显重叠处判断模型是否把两人混成一人的文本,最后一条判断结尾是否被切掉或时间戳越界。每条记录三件事:标签是否正确、文本是否完整、时间戳是否落在对应语音上。

出错时按现象回退,而不是直接在最终表上改:

  • 整段文本正确但说话人标签互换或错标,多半是判属问题,回退到识别步骤,检查是否需要在回对齐阶段做一次全局说话人统一。
  • 文本缺字、被截断或开头少半句,通常是切分边界切进了词中间,回退到切分步骤,调整静音门限或最小片段长度后重新生成片段清单。
  • 文本和标签都对但全局时间戳整体偏移或末尾越界,回退到对齐步骤,核对片段起点累加逻辑和原始音频总时长。

判断这条顺序是否可用,不是看跑通一次,而是三条边界片段听下来标签和文本都能对上;对不上就先定位属于上面哪一类,再决定重切、重跑还是重对齐。