音频转文字与说话人识别部署与使用指南

一、概述

本方案在 CentOS 7.6 纯 CPU 服务器上部署了一套完整的通话音频处理流水线 :音频转文字(FunASR)+ 说话人角色识别(3D-Speaker CAM++),输入一段客服/双人通话录音,输出带说话人标注、时间戳的中文对话稿。

技术原理

整体架构分两层,均为离线批处理 + WebSocket 服务混合形态:

复制代码
本地 Windows 工作机 (D:\workspace\funasr-deploy)
│
│  transcribe_diar.py  ← 唯一入口,一条命令完成四步
│    ①ssh-helper 上传音频─────────────────┐
│    ②websocket ASR 转写(句子级时间戳)    │
│    ③ssh 触发远端说话人分离──────────────┤
│    ④下载时间轴 + 按时间重叠对齐 → 对话稿  │
▼                                        ▼
服务器 <服务器IP> (CentOS 7.6, 8C/31G, 无GPU)
├─ 容器 funasr  (funasr-runtime-sdk-cpu-0.4.7)   ── 语音转文字
│    funasr-wss-server-2pass 二进制,端口 10095
│    模型:paraformer-large(离线/在线) + fsmn-vad + 标点 + ITN
│
└─ 容器 diar:0.1  (python:3.11-slim + torch-cpu + funasr 1.4.16)  ── 说话人识别
     diarize.py:VAD → CAM++ 向量 → AHC 聚类 → 时间轴
     挂载卷:/opt/diar(脚本+模型缓存+输出)、静态 ffmpeg
层级 组件 职责
客户端 transcribe_diar.py 上传、编排、对齐合并、输出对话稿
ASR 层 funasr 容器(端口 10095) 音频 → 文本 + 句子级时间戳
说话人层 diar 容器 音频 → 说话人时间轴 JSON/RTTM
对齐层 assign_speakers() ASR 句子 × 说话人区间,按重叠时长分配
基础设施 ssh-helper、静态 ffmpeg 远程执行、跨格式音频解码

关键特性

  • 中文识别精度高:Paraformer-large 离线模型 + 2pass 流式修正,附带标点恢复、数字规整(ITN)。
  • 说话人分离:达摩院 3D-Speaker 的 CAM++ 说话人向量模型(192 维),中文场景效果优秀,与 FunASR 同生态。
  • 句子级对齐 :利用服务端 stamp_sents 返回的句子时间戳,把说话人标注细化到每一句话,而非粗粒度的 VAD 分段。
  • 一条命令端到端 :本地 transcribe_diar.py 自动完成上传、转写、分离、对齐、出稿。
  • 纯 CPU 可跑:3 分钟通话转写约 5 分钟、说话人分离约 21 秒,无需 GPU。

适用场景

  • 客服电话录音质检、双人对话整理、会议纪要角色区分。
  • 任何"谁在什么时候说了什么"的中文音频分析需求。

二、系统要求

操作系统

系统 版本 角色 备注
CentOS 7.6 服务端 glibc 2.17(关键限制,见常见问题)
Windows 10/11 客户端 Git Bash + Python 3.12

硬件要求

  • CPU:8 核(实测满载转写约 1.7 倍实时时长)
  • 内存:31GB(funasr 与其他业务容器后仍有 17GB 余量)
  • 磁盘:/opt 卷 200GB(本方案占用约 2GB:镜像 1.85GB + 模型/脚本 75MB)

网络要求

  • 服务器需可访问:清华 pypi 镜像、ModelScope(模型下载)、download.pytorch.org。
  • Docker Hub 直连不可达(实测超时),镜像必须走加速站本地下载再上传(全局规则)。
  • 客户端到服务器 10095 端口(WebSocket)与 22 端口(SSH)可达。

权限要求

  • 服务器 root(SSH)及 Docker 使用权限。
  • 凭证来源:<凭证CSV路径>(含 host/sshPort/sshName/sshPwd 列)。

客户端依赖

依赖 版本 用途
Python 3.12 运行本地脚本
websockets ≥12 ASR WebSocket 客户端
imageio-ffmpeg 最新 本地 mp3 → PCM 解码
ssh-helper.exe v0.1.0 上传 / 远程执行(位于 ~/.zcode/skills/ssh-helper)

三、安装步骤(完整过程实录)

本节按实际部署的时间顺序记录,包含一次方案失败与切换------这正是最值得沉淀的经验。

步骤 0:前置检查

bash 复制代码
# 服务器资源、docker、外网连通性
df -h /opt && docker ps && nproc
curl -sI https://pypi.tuna.tsinghua.edu.cn/simple/   # 200 OK
curl -sI https://www.modelscope.cn                    # 302(可达)

结论:磁盘充足、pypi/ModelScope/pytorch 官方 CPU 源全通,但 Docker Hub 不通。

已有 funasr 容器(音频转文字服务)在跑,本次新增说话人识别环境。

步骤 1:方案探索 ------ 宿主机 venv(失败,已回退清理)

最初尝试直接用宿主机自带 Python 3.11.9 创建 venv 装 torch:

bash 复制代码
python3 -m venv /opt/diar/venv
/opt/diar/venv/bin/pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu

torch 下载成功但导入失败:

复制代码
ModuleNotFoundError: No module named '_ctypes'

根因:宿主机 /usr/local/python3 源码编译时缺 libffi-devel,动态库目录里只有 _ctypes_test.so 而没有 _ctypes.so,torch 强依赖 ctypes。修复需要 CPython 源码重编译,而源码目录已被删、gcc 4.8.5 从零编 3.11 风险大,放弃。

步骤 2:方案探索 ------ Miniconda(半途失败,已回退清理)

改为从清华源装 Miniconda 提供完整 Python:

bash 复制代码
curl -sL -o miniconda.sh https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-py311_24.7.1-0-Linux-x86_64.sh
bash miniconda.sh -b -p /opt/diar/conda

注意版本 :2026 年的新版安装器(26.x)要求 GLIBC >= 2.28,CentOS 7.6 只有 2.17,必须退回 24.7.1-0 及更早的 py311 版本,这是能成功安装的最后一档。

conda 的 Python 本身正常,但下一步装 funasr 依赖链再次失败:新版 scipy/numpy 对 Python 3.11 不再提供预编译 wheel,回退源码编译又要求 GCC >= 9.3(服务器 4.8)。两轮失败后按既定规则切换 Docker 方案 ,并清理了 /opt/diar/venv(852MB)与 /opt/diar/conda。

步骤 3:Docker 离线镜像导入(最终方案)

Docker Hub 不可达 → 按全局规则本地下载 → 上传 → docker load:

bash 复制代码
# 本地:经镜像加速站导出 docker load 兼容 tar(无需本机 Docker)
cd ~/.zcode/skills/docker-image-downloader/scripts
python download_image.py --mirror docker.m.daocloud.io \
    --output D:\...\docker-img python:3.11-slim
# 产出 python_3.11-slim.tar(44MB,daocloud 站约 1 分钟;1ms.run 站当时仅 200KB/s 已弃用)

# 上传(Git Bash 需 MSYS_NO_PATHCONV=1 防止 /opt 被改写成 Windows 路径)
MSYS_NO_PATHCONV=1 ssh-helper.exe upload --hostname <服务器IP> \
    --local python_3.11-slim.tar --remote /opt/diar/python_3.11-slim.tar

# 服务器导入
docker load -i /opt/diar/python_3.11-slim.tar

步骤 4:构建 diar 镜像

在服务器 /opt/diar/build/ 写 Dockerfile(构建目录与数据目录分离,避免把模型目录卷进构建上下文):

dockerfile 复制代码
FROM python:3.11-slim
RUN pip install --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cpu \
 && pip install --no-cache-dir 'numpy<2.2' 'scipy<1.17' funasr soundfile -i https://pypi.tuna.tsinghua.edu.cn/simple
ENV MODELSCOPE_CACHE=/opt/diar/models
WORKDIR /opt/diar

版本钉死的理由:numpy<2.2、scipy<1.17 是仍有 Python 3.11 预编译 wheel 的最后版本。镜像内是 Debian(glibc 2.36),所有 manylinux wheel 直接可用,绕开了宿主机老系统的全部编译问题。

bash 复制代码
docker build -t diar:0.1 /opt/diar/build    # 约 5 分钟,产出 1.85GB
docker run --rm diar:0.1 python -c "import torch, funasr; print(funasr.__version__)"
# 输出:1.4.16

步骤 5:下载模型(缓存持久化到宿主机)

模型不放镜像里 ,首次运行时自动下载到挂载卷 /opt/diar/models,重建镜像不丢模型:

bash 复制代码
docker run --rm -v /opt/diar:/opt/diar diar:0.1 python -c "
from funasr import AutoModel
vad = AutoModel(model='fsmn-vad', disable_update=True)                              # 约 5MB
spk = AutoModel(model='iic/speech_campplus_sv_zh-cn_16k-common', disable_update=True)  # CAM++,约 30MB
"
# 日志出现 All keys matched successfully 即加载成功

步骤 6:部署脚本与 ffmpeg

  • 上传 diarize.py(服务器端核心,见下文)到 /opt/diar/diarize.py。
  • 容器内没有 ffmpeg,把宿主机已有的静态版 ffmpeg 挂载进去,并补执行位:
bash 复制代码
chmod +x /opt/funasr/ffmpeg     # 原为 644,funasr 导入时会探测 ffmpeg -version,无执行位直接报 PermissionError
# 运行时挂载:-v /opt/funasr/ffmpeg:/usr/local/bin/ffmpeg:ro

步骤 7:部署本地客户端脚本

transcribe_diar.py 放本地工作区,SSH 凭证从 <凭证CSV> 解析后显式传给 ssh-helper(连接参数必须放在子命令之前,原因见常见问题)。

安装验证

bash 复制代码
# 服务器端冒烟测试(3 分钟双人通话)
docker run --rm -v /opt/diar:/opt/diar -v /opt/funasr/ffmpeg:/usr/local/bin/ffmpeg:ro \
  diar:0.1 python /opt/diar/diarize.py /opt/diar/audio/test_install1.mp3 --num-speakers 2
# 预期:40 个窗口, 192 维向量, 识别出 2 个说话人,输出 25 个时间区间

# 本地端到端
python transcribe_diar.py --audio "素材/测试安装1.mp3" --num-speakers 2
# 预期:产出 素材/测试安装1_对话稿.txt

四、配置说明

服务器目录结构

路径 内容 说明
/opt/diar/diarize.py 说话人分离脚本 10KB,纯 Python
/opt/diar/models/ 模型缓存 MODELSCOPE_CACHE 指向,卷持久化
/opt/diar/audio/ 上传音频暂存 跑完默认自动删除
/opt/diar/output/ 结果输出 *_diar.json + *_diar.rttm
/opt/diar/build/ Dockerfile 重建镜像用
/opt/diar/python_3.11-slim.tar 基础镜像备份 迁移新服务器用

diarize.py 关键参数

参数 默认值 说明
--num-speakers 0(自动) 双人通话务必传 2;0 = 树状图 elbow 自动估计(电话音频上不可靠,实测 2 人被估成 6 人)
--output-dir /opt/diar/output 结果目录
--vad-model fsmn-vad VAD 模型
--spk-model iic/speech_campplus_sv_zh-cn_16k-common CAM++ 模型

diarize.py 内置调优常量

常量 值 说明
MIN_SEG_MS 800 短于 0.8s 的 VAD 段不提向量(不可靠),时间轴上归入相邻说话人
MAX_SEG_MS 6000 长段切成最大 6s 子窗口。实测 3s 窗口会把聚类打崩(时间轴坍缩成一大段),6s 是电话音频甜点
合并间隔 500ms 相邻同说话人区间间隔小于 0.5s 时合并

环境变量

变量 作用
MODELSCOPE_CACHE 模型缓存目录(镜像内已设为 /opt/diar/models)
FFMPEG_BIN 覆盖 ffmpeg 查找路径(默认按 /usr/local/bin/ffmpeg → /opt/funasr/ffmpeg → PATH 顺序探测)

transcribe_diar.py 参数

参数 默认值 说明
--audio 必填 本地音频(wav/mp3/m4a/flac 等)
--num-speakers 2 说话人数;0=自动
--mode offline offline=整文件离线(句子级时间戳对齐更准);2pass=流式
--timeout 300 ASR 接收超时(秒)
--fast 关 快速发送模式
--keep-remote 关 保留服务器上的音频副本

transcribe_diar.py 主要逻辑

脚本分四个模块,主流程串成 1/4~4/4 四步:

text 复制代码
[1/4] 上传音频 ──→ [2/4] 本地 ASR 转写 ──→ [3/4] 远端说话人分离 ──→ [4/4] 对齐合并出稿
   ssh-helper          websocket 2pass        docker diar:0.1         纯本地计算

① SSH 远程封装(ssh_credentials / ssh_run / ssh_upload / ssh_download / ssh_exec_diarize)

所有远端操作都经 ssh-helper 子进程完成,凭证从 <凭证CSV> 按主机名解析后显式传参(不依赖工具自身 config,避免相对路径解析失败)。两个易错点都已在封装内处理:

python 复制代码
# 连接参数必须放在子命令之前:exec 的 -- 之后全部内容会被当作远端命令
cmd = [SSH_HELPER, "--hostname", REMOTE_HOST, "--port", port,
       "--username", user, "--password", pwd, *args]

# 远端结果路径不解析中文输出(SSH 捕获可能乱码),只看退出码 + 路径推导:
# 时间轴 JSON 固定为 <音频名去扩展名>_diar.json,可从音频名直接拼出

远端 diarize 的执行命令:docker run --rm -v /opt/diar:/opt/diar -v /opt/funasr/ffmpeg:/usr/local/bin/ffmpeg:ro diar:0.1 python /opt/diar/diarize.py <音频> --num-speakers N。

② ASR 2pass 客户端(read_pcm / send_audio / recv_results / transcribe)

read_pcm 把任意格式音频解码为 16k 单声道 PCM(wav 直读,其余走 ffmpeg 管道);send_audio 按 600ms 分片推送(chunk_size=[5,10,5]),末尾发 is_speaking: false;transcribe 用 asyncio 并发收发。核心在 recv_results:

python 复制代码
# 服务器对 offline 请求也回 "2pass-offline" 消息(不是 "offline"),必须统一匹配
if msg.get("mode") in ("2pass-offline", "offline"):
    for s in msg.get("stamp_sents") or []:          # 句子级绝对毫秒时间戳
        text = (s.get("text_seg") or "").replace(" ", "") + (s.get("punc") or "")
        segs.append({"text": text, "span": (s["start"], s["end"])})
    if msg.get("is_final"):
        break
# 兜底:无 stamp_sents 时用整段 text;空闲 120s 无消息也视为结束(长停顿场景)

③ 对齐合并(assign_speakers / merge_dialogue / spk_label)

  • assign_speakers:对每个 ASR 句子,遍历说话人时间轴取重叠时长最大 的区间(min(e,end)-max(s,start))作为该句说话人;零重叠或无时间戳的句子沿用前一句的说话人(利用对话的连续性)。
  • merge_dialogue:连续同说话人的句子合并为一个轮次,时间跨度取并集;输出 [(说话人, 文本, 起止时间)]。
  • spk_label:聚类编号 0/1/2 → 「说话人A/B/C」。

④ 主流程(main / safe_remote_name)

python 复制代码
remote_name = safe_remote_name(path)     # 非 ASCII 文件名转 md5 哈希名,避免 SSH 中文编码问题
# [1/4] mkdir -p 远端目录 → 上传音频
# [2/4] asyncio.run(transcribe(...))     → segs: [{text, span}]
# [3/4] ssh_exec_diarize(...) → 下载 <stem>_diar.json → timeline
# [4/4] assign_speakers(segs, timeline) → merge_dialogue(segs)
#        输出 <音频同目录>/<音频名>_对话稿.txt(含说话人数、时长、两项耗时)
# 收尾:默认 rm 远端音频副本(--keep-remote 可保留)

输出文件说明

*_diar.json 结构:

json 复制代码
{
  "audio": "/opt/diar/audio/xxx.mp3",
  "duration_s": 181.6,
  "num_speakers": 2,
  "timeline": [
    {"start_ms": 260, "end_ms": 12260, "speaker": 0},
    {"start_ms": 12260, "end_ms": 18260, "speaker": 1}
  ]
}

*_diar.rttm 为标准标注格式,可导入 Audacity/pyannote 工具复核:

复制代码
SPEAKER xxx 1 0.260 12.000 <NA> <NA> spk0 <NA> <NA>

五、核心概念

概念 1:说话人分离(Speaker Diarization)

回答"谁在什么时间说话 ",不关心说了什么。经典四步:VAD 找有效语音段 → 每段提说话人向量(embedding)→ 向量聚类(不知道人数时)→ 得到说话人时间轴。它和 ASR 是两条独立管线,靠时间戳对齐拼成对话稿。

概念 2:CAM++ 说话人向量

达摩院 3D-Speaker 出品、与 FunASR 同生态的说话人表征模型,把一段 2~6 秒语音压缩成 192 维向量。同一说话人两段语音的向量余弦相似度显著高于不同说话人 ,聚类就建立在这个性质上。funasr 1.4.16 原生支持加载(AutoModel(model="iic/speech_campplus_sv_zh-cn_16k-common")),生成结果取 res[0]["spk_embedding"]。

概念 3:AHC 层次聚类

scipy 的 average-linkage 层次聚类作用于窗口间余弦距离矩阵。指定人数时用 fcluster(Z, t=N, criterion="maxclust") 强制切成 N 类;不指定时用树状图合并高度的"最大间隙"(elbow)估计人数。电话窄带音频的向量分离度低,自动估计容易偏多,生产上建议显式指定人数。

概念 4:stamp_sents 句子级时间戳

FunASR 服务端返回的每个转写结果消息里,stamp_sents 字段是句子级绝对时间戳列表(每句含 start/end 毫秒),是本方案实现"逐句标说话人"的关键。注意 timestamp 字段是 JSON 编码的字符串(逐字时间戳),直接当列表遍历会拿到单个字符,这是最容易踩的协议坑。

概念 5:时间重叠对齐

对话稿合成的最后一步:对每个 ASR 句子 (start, end),遍历说话人时间轴取重叠时长最大的区间作为其说话人;零重叠或无时间戳的句子沿用前一句的说话人。连续同说话人的句子再合并为一个对话轮次。

架构总览

层级 组件 职责
编排层 transcribe_diar.py 流程编排、对齐、输出
传输层 ssh-helper / websocket 上传下载、流式转写通道
ASR 层 funasr-wss-server-2pass 音频→文本+句子时间戳
说话人层 diar:0.1 容器 音频→说话人时间轴
存储层 /opt/diar 卷 脚本、模型、输出

六、快速开始

一条命令(本地工作区执行):

bash 复制代码
cd D:\workspace\funasr-deploy

# 双人通话(推荐显式指定 2 人)
python transcribe_diar.py --audio "素材/测试安装1.mp3" --num-speakers 2

预期输出:

复制代码
[1/4] 上传音频到 <服务器IP>:/opt/diar/audio/audio_xxxx.mp3
[2/4] ASR 转写(offline 模式,funasr websocket)...
      144 段, 810 字, 耗时 308.8s
[3/4] 远端说话人分离(num-speakers=2)...
      2 个说话人, 25 个时间区间, 耗时 20.6s
[4/4] 对齐说话人 + 输出对话稿
完成: 素材\测试安装1_对话稿.txt(总耗时 331.8s)

对话稿样例(节选自真实产出):

复制代码
说话人A:××品牌上门服务来电。喂,你好,我这边××品牌的维修师傅......  [0.5-13.2]
说话人B:什么情况?嗯,就是门一拉就开了,  [13.2-17.1]
说话人A:就说现在关不上是吧,就是现在门关上去以后,咱一拉就开......  [18.0-24.4]
说话人B:呃,对,是的,7月份装的......  [25.3-30.1]

七、完整使用流程

步骤 1:准备音频

任意 ffmpeg 可解码格式;建议 16kHz 采样(其他采样率自动重采样)。中文电话录音效果最佳,模型即为此优化。

步骤 2:执行端到端命令

bash 复制代码
python transcribe_diar.py --audio "素材/你的录音.mp3" --num-speakers 2

脚本内部依次完成:

  1. 上传 :中文文件名自动转 hash 安全名(规避 SSH 通道中文编码问题),上传到 /opt/diar/audio/;
  2. 转写 :本地 websocket 连 <服务器IP>:10095,offline 模式整文件发送(--fast 可加速发送),收集 stamp_sents 拆出的句子级分段;
  3. 说话人分离 :ssh 触发远端容器跑 diarize.py(VAD→CAM++→聚类),下载时间轴 JSON 到音频同目录;
  4. 对齐出稿 :时间重叠分配说话人 → 连续同说话人合并轮次 → 写 素材/<音频名>_对话稿.txt;
  5. 清理 :默认删除服务器上的音频副本(--keep-remote 可保留)。

步骤 3:核对结果

  • 打开 *_对话稿.txt:检查说话人切换点是否与语义吻合(如客服问句 vs 客户答句)。
  • 若可疑,对照同目录 *_diar.json 的时间轴人工抽查。
  • 结尾语速快、语音重叠的部分可能出现少量误标,属窄带电话 diarization 的正常水平。

步骤 4(可选):单独跑说话人分离

bash 复制代码
docker run --rm -v /opt/diar:/opt/diar \
  -v /opt/funasr/ffmpeg:/usr/local/bin/ffmpeg:ro \
  diar:0.1 python /opt/diar/diarize.py /opt/diar/audio/xxx.mp3 --num-speakers 2

完整代码示例(Python 中调用)

python 复制代码
import asyncio, sys
sys.path.insert(0, r"D:\workspace\funasr-deploy")
from transcribe_diar import transcribe, assign_speakers, merge_dialogue

# 只要 ASR 分段(含句子级时间戳)
segs = asyncio.run(transcribe("<服务器IP>", 10095, r"素材\xx.mp3",
                              timeout=300, fast=True, mode="offline"))

# 加上说话人时间轴后合并出对话
timeline = json.load(open(r"素材\xx_diar.json", encoding="utf-8"))["timeline"]
dialogue = merge_dialogue(assign_speakers(segs, timeline))
for spk, text, span in dialogue:
    print(f"说话人{spk}: {text}  [{span[0]/1000:.1f}-{span[1]/1000:.1f}]")

八、命令/API 参考

diarize.py(服务器端)

用途:单音频说话人分离,输出时间轴。

语法:

bash 复制代码
docker run --rm -v /opt/diar:/opt/diar \
  -v /opt/funasr/ffmpeg:/usr/local/bin/ffmpeg:ro \
  diar:0.1 python /opt/diar/diarize.py <音频> [--num-speakers N] [--output-dir DIR]

输出:

复制代码
[1/5] 加载模型(首次运行自动下载到 $MODELSCOPE_CACHE)...
[2/5] 读取音频: /opt/diar/audio/test_install1.mp3
      时长 181.6s, 采样率 16k
[3/5] VAD 切分...
      24 个语音段, 有效语音 169.0s
[4/5] CAM++ 提取向量 + 聚类...
      40 个窗口, 192 维向量, 识别出 2 个说话人
[5/5] 生成时间轴...
  [   0.26 -   12.26] 说话人0
  ...
完成: /opt/diar/output/test_install1_diar.json

transcribe_diar.py(本地端)

见"四、配置说明"中的参数表。常用组合:

bash 复制代码
# 标准双人通话
python transcribe_diar.py --audio "素材/a.mp3" --num-speakers 2
# 说话人数未知的长录音(自动估计,慎用)
python transcribe_diar.py --audio "素材/a.mp3" --num-speakers 0
# 需要实时中间结果的场景
python transcribe_diar.py --audio "素材/a.mp3" --mode 2pass

funasr WebSocket 协议要点(自研客户端时需知)

要点 内容
连接 ws://<服务器IP>:10095,子协议 ["binary"],ping_interval=None
首包配置 JSON:mode/chunk_size=5,10,5/chunk_interval=10/wav_format=pcm/is_speaking=true/itn
音频 16kHz 单声道 s16le PCM,按 600ms 分片发送,帧间 sleep 模拟实时
结束标记 JSON {"is_speaking": false}
结果消息 mode=2pass-offline(offline 请求也返回此模式!),结束信号 is_final=true
文本 text 字段;句子时间戳在 stamp_sents (timestamp 是 JSON 字符串,勿当列表用)

九、常见问题与解决方案

问题 1:torch 导入报 No module named '_ctypes'

原因 :宿主机 /usr/local/python3 编译时缺 libffi-devel,_ctypes 扩展未构建,torch 硬依赖它。

解决方案:不要在宿主机装 torch。走 Docker(本方案),或用 24.7.1 及更早版本的 Miniconda 提供完整 Python。

问题 2:Miniconda 安装报 Installer requires GLIBC >=2.28, but system has 2.17

原因:2026 年新构建的安装器放弃 CentOS 7。

解决方案 :退回 Miniconda3-py311_24.7.1-0-Linux-x86_64.sh(清华 anaconda/miniconda 镜像目录有全部历史版本)。

问题 3:pip 装 scipy/numpy 报 NumPy requires GCC >= 9.3 / metadata 失败

原因:新版包对 Python 3.11 停发预编译 wheel,pip 回退源码编译,老系统编译器不达标。

解决方案 :钉版本 'numpy<2.2' 'scipy<1.17'(最后提供 cp311 wheel 的版本)。若在容器里装(glibc 2.36),这些 wheel 全部直接可用。

问题 4:docker pull 超时 / registry-1.docker.io 连接失败

原因:服务器到 Docker Hub 不通,且 daemon.json 未配镜像加速。

解决方案 :本地用 docker-image-downloader 技能经加速站导出 tar 再上传 docker load。加速站选择:docker.m.daocloud.io 快(44MB 约 1 分钟);docker.1ms.run 波动大(实测 200KB/s 量级)。

问题 5:ssh-helper 调用报"无法解析连接信息"或打印 usage

原因 :①相对路径 --config conf/config.json 依赖 cwd;②exec 子命令中 -- 之后的全部内容会被当作远端命令,连接参数放在其后即被吞掉。

解决方案 :从 <凭证CSV> 解析凭证,把 --hostname/--port/--username/--password 放在子命令之前。

问题 6:上传带中文路径的文件失败或输出乱码

原因 :SSH 通道与工具链的编码差异;Git Bash 会把 /opt/... 改写成 D:/Program Files/Git/opt/...。

解决方案 :命令前加 MSYS_NO_PATHCONV=1;中文文件名先转 ASCII 安全名(transcribe_diar.py 已内置 md5 命名);脚本内判定远端结果只看退出码和推导路径,不解析中文输出。

问题 7:funasr 导入时 PermissionError: [Errno 13] Permission denied: 'ffmpeg'

原因 :挂载进容器的静态 ffmpeg 宿主机侧无执行位(644),而 funasr 导入时会执行 ffmpeg -version 探测。

解决方案 :chmod +x /opt/funasr/ffmpeg(宿主机上执行一次)。

问题 8:对话稿全部标成同一个说话人

原因 :早期版本用 timestamp 字段当列表遍历,实际它是 JSON 编码字符串,逐"字"解析后时间戳全空,兜底逻辑把所有句子沿用说话人 0。

解决方案 :改用 stamp_sents 字段(句子级、已解析、绝对毫秒时间)。此修复后对话稿说话人分布立即恢复正常。

问题 9:说话人时间轴坍缩成一大段(或碎成几十段)

原因:提向量窗口尺寸失配------3s 窗口在电话音频上聚类不稳(全部窗口聚成一类);聚类阈值 0.3 又过紧(同说话人距离实际 0.3~0.5)。

解决方案 :MAX_SEG_MS=6000(6s 窗口)+ 显式 --num-speakers 2。不要在窄带电话音频上依赖自动估计人数。

问题 10:把单人录音跑出两个说话人

原因 :强制 --num-speakers 2 对机器人外呼等单人播报录音会人为劈分。

解决方案:确认音源性质;单人内容不要传 2。

问题 11:长音频客户端提前断开、只转写出前一小段

原因:客户端在收到首个结果后把等待超时切到 3 秒,自然停顿超过 3 秒即断链。

解决方案 :短超时提到 120s、总超时 --timeout 300(transcribe_2pass.py 已修;transcribe_diar.py 继承此配置)。


十、最佳实践

性能优化

  1. 离线模式优先:整文件 offline 转写比 2pass 少一轮在线推理往返,且一次拿到全文件句子级时间戳,对齐更干净。
  2. 只跑一次 ASR :transcribe_diar.py 把转写结果和时间轴都落盘,复核对话稿时直接读文件,不要重复跑全流程。
  3. 模型放卷不放镜像:重建镜像秒级(层缓存),模型缓存独立于镜像生命周期。
  4. 音频先转 16k 单声道:上传前本地 ffmpeg 预转可减少服务端解码开销(当前管线已自动处理,无需手工)。

准确率优化

  1. 显式指定说话人数:双人通话传 2;这是窄带电话音频上最有效的一项设置。
  2. 对齐用句子级时间戳(本方案默认):比 VAD 分段级对齐明显减少跨段错标。
  3. 容忍结尾误标:结束语快速互叠是窄带 diarization 的固有弱区,重要质检场景建议人工复核最后 10 秒。
  4. 语音纯净、时长较长的会议录音可尝试 --num-speakers 0 自动估计。

运维建议

  1. 迁移新服务器四步 :拷 python_3.11-slim.tar → docker load → docker build -t diar:0.1 /opt/diar/build → 拷 /opt/diar(脚本+模型)并确保有静态 ffmpeg(chmod +x)。
  2. 全局资源规则:服务器下载不到(镜像/包/模型)一律本地下载再上传,不在服务器上反复重试(已写入用户级 AGENTS.md)。
  3. 磁盘巡检 :/opt/diar/audio 默认自动清理,但手动调试跑的文件需留意;产出目录按需归档。
  4. 凭证安全:文档与脚本中不硬编码密码,运行时从 <凭证CSV> 读取。

调试技巧

  1. 协议抓包 :临时客户端打印每条消息的 mode/keys/is_final,一次即可确认服务器行为(本方案的 stamp_sents 发现就是这么来的)。
  2. 聚类诊断:打印窗口向量两两距离的百分位分布 + 扫阈值看簇数变化,可判断"无双峰→自动估计不可靠"。
  3. 时间轴复核 :*_diar.rttm 可导入标注工具可视化,比读 JSON 快。
  4. 服务器端命令输出经 SSH 捕获时中文可能乱码,判定成败看退出码,别解析中文文本。

十一、验证记录

用例 音频 结果
双人客服电话(3 分钟) 素材/测试安装1.mp3 24 个 VAD 段、40 个向量窗口、2 说话人、25 个时间区间;对话稿 A=维修师傅 / B=顾客,报修→诊断(锁舌没到位、具体型号)→约 9 点上门,轮次与语义吻合。总耗时 332s(转写 310s + 分离 21s)
机器人外呼(21 秒) 素材/机器人外呼样例.mp3 单人播报被强制 2 人拆分(符合预期,证明"人数传错会劈分");管线本身正常,总耗时 41s
窗口尺寸对比 同用例 1 6s 窗口:25 区间、标注正确;3s 窗口:时间轴坍缩为 1 大段(弃用)
人数自动估计 同用例 1 elbow 法估出 6 人(真值 2),确认电话音频必须显式指定人数

相关资源

相关推荐
DogDaoDao1 小时前
当视频理解撞上大语言模型:一篇 37 页综述的全景拆解
人工智能·语言模型·音视频·知识图谱·视频
阿明副业观察2 小时前
AI短剧分镜秘籍:从零到电影级视觉冲击
大数据·人工智能·aigc·音视频·ai写作
语音之家2 小时前
NeurIPS 2026|AnyEdit:免人声分离,嘈杂音频也能局部编辑
人工智能·音视频·消融实验·声源分离
小白学大数据2 小时前
逆向分析短视频平台 a_bogus 参数:从 JavaScript 混淆到 Python 复现
javascript·python·音视频
常州晟凯电子科技3 小时前
8路CVBS/AHD模拟视频编码器评测
音视频·视频编解码
碧海银沙音频科技研究院3 小时前
基于炬芯ATS3089X的蓝牙A2DP之SBC编码算法音频数据导出实现
音视频
是店小二呀3 小时前
想用自己的音色给视频配音?我用Index-TTS跑通参考音频、语音生成和远程访问
人工智能·音视频·语音识别
揽秀亭长3 小时前
提取视频文案的工程选型|五类转写方案对比、可复现脚本与报错排查表
人工智能·音视频
quaer3 小时前
FFmpeg环境搭建10.16
音视频