一、概述
本方案在 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
脚本内部依次完成:
- 上传 :中文文件名自动转 hash 安全名(规避 SSH 通道中文编码问题),上传到
/opt/diar/audio/; - 转写 :本地 websocket 连
<服务器IP>:10095,offline 模式整文件发送(--fast可加速发送),收集stamp_sents拆出的句子级分段; - 说话人分离 :ssh 触发远端容器跑
diarize.py(VAD→CAM++→聚类),下载时间轴 JSON 到音频同目录; - 对齐出稿 :时间重叠分配说话人 → 连续同说话人合并轮次 → 写
素材/<音频名>_对话稿.txt; - 清理 :默认删除服务器上的音频副本(
--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 继承此配置)。
十、最佳实践
性能优化
- 离线模式优先:整文件 offline 转写比 2pass 少一轮在线推理往返,且一次拿到全文件句子级时间戳,对齐更干净。
- 只跑一次 ASR :
transcribe_diar.py把转写结果和时间轴都落盘,复核对话稿时直接读文件,不要重复跑全流程。 - 模型放卷不放镜像:重建镜像秒级(层缓存),模型缓存独立于镜像生命周期。
- 音频先转 16k 单声道:上传前本地 ffmpeg 预转可减少服务端解码开销(当前管线已自动处理,无需手工)。
准确率优化
- 显式指定说话人数:双人通话传 2;这是窄带电话音频上最有效的一项设置。
- 对齐用句子级时间戳(本方案默认):比 VAD 分段级对齐明显减少跨段错标。
- 容忍结尾误标:结束语快速互叠是窄带 diarization 的固有弱区,重要质检场景建议人工复核最后 10 秒。
- 语音纯净、时长较长的会议录音可尝试
--num-speakers 0自动估计。
运维建议
- 迁移新服务器四步 :拷
python_3.11-slim.tar→docker load→docker build -t diar:0.1 /opt/diar/build→ 拷/opt/diar(脚本+模型)并确保有静态 ffmpeg(chmod +x)。 - 全局资源规则:服务器下载不到(镜像/包/模型)一律本地下载再上传,不在服务器上反复重试(已写入用户级 AGENTS.md)。
- 磁盘巡检 :
/opt/diar/audio默认自动清理,但手动调试跑的文件需留意;产出目录按需归档。 - 凭证安全:文档与脚本中不硬编码密码,运行时从 <凭证CSV> 读取。
调试技巧
- 协议抓包 :临时客户端打印每条消息的
mode/keys/is_final,一次即可确认服务器行为(本方案的 stamp_sents 发现就是这么来的)。 - 聚类诊断:打印窗口向量两两距离的百分位分布 + 扫阈值看簇数变化,可判断"无双峰→自动估计不可靠"。
- 时间轴复核 :
*_diar.rttm可导入标注工具可视化,比读 JSON 快。 - 服务器端命令输出经 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),确认电话音频必须显式指定人数 |
相关资源
- FunASR 官方仓库(GitHub)
- 3D-Speaker 官方仓库(GitHub)
- CAM++ 模型页(ModelScope)