SenseVoice-small-onnx部署实战:Ansible自动化部署ONNX语音识别服务集群

1. 引言

你有没有遇到过这样的场景?团队需要部署一个语音识别服务,支持中文、粤语、英语等多种语言,还要能自动检测说话人的情感。手动在一台服务器上部署已经够麻烦了,如果要部署到多台服务器组成集群,那工作量简直让人头疼。

今天我要分享的,就是如何用Ansible这个自动化工具,把SenseVoice-small-onnx语音识别服务一键部署到多台服务器上。SenseVoice-small是一个经过ONNX量化处理的多语言语音识别模型,只有230MB大小,但能力却很强——10秒的音频,推理时间只要70毫秒,还支持50多种语言的自动检测。

这篇文章不是那种只讲理论的教程,而是我实际部署过多次的实战经验总结。我会带你从零开始,一步步搭建起一个完整的语音识别服务集群。无论你是运维工程师、后端开发,还是对AI服务部署感兴趣的技术爱好者,都能跟着做下来。

2. 环境准备与规划

2.1 我们需要准备什么

在开始自动化部署之前,我们先要搞清楚整个架构是什么样的。简单来说,我们需要:

  1. 一台控制机:用来运行Ansible,控制所有的部署操作
  2. 多台目标服务器:实际运行语音识别服务的机器
  3. 模型文件:SenseVoice-small的ONNX量化模型
  4. 部署脚本:Ansible Playbook,告诉Ansible怎么部署

这里有个很重要的点:模型文件有230MB,如果每台服务器都从网上下载,既慢又浪费带宽。所以我们要用缓存机制,让第一台服务器下载后,其他服务器直接从内网拉取。

2.2 服务器配置建议

根据我的经验,下面这样的配置就够用了:

组件 最低要求 推荐配置
CPU 2核 4核或以上
内存 4GB 8GB
磁盘 20GB 50GB
系统 Ubuntu 20.04/22.04 Ubuntu 22.04
Python 3.8+ 3.9+

如果你只是测试,用2核4GB的服务器也能跑起来。但如果是生产环境,建议至少4核8GB,因为语音识别还是挺吃CPU的。

2.3 网络规划

网络这块要提前规划好:

  • 控制机到目标机:要能SSH免密登录
  • 目标机之间:如果要做模型缓存同步,需要内网互通
  • 服务端口:7860端口要对外开放(或者通过Nginx反向代理)

3. Ansible基础配置

3.1 安装Ansible

首先在控制机上安装Ansible。如果你用的是Ubuntu,很简单:

sudo apt update
sudo apt install -y ansible

安装完成后检查一下版本:

ansible --version

你应该能看到类似这样的输出:

ansible [core 2.15.0]

3.2 配置SSH免密登录

这是自动化部署的关键一步。Ansible需要通过SSH连接到目标服务器执行命令,如果每次都要输密码,那就不是自动化了。

第一步,生成SSH密钥(如果还没有的话):

ssh-keygen -t rsa -b 4096

直接按回车,使用默认设置就行。

第二步,把公钥复制到所有目标服务器

假设你的目标服务器IP是192.168.1.101到192.168.1.103,用户名都是ubuntu:

for ip in 192.168.1.{101..103}; do
    ssh-copy-id ubuntu@$ip
done

这会要求你输入每台服务器的密码,输一次就好,以后就不用了。

第三步,测试连接

ansible all -i "192.168.1.101,192.168.1.102,192.168.1.103," -m ping -u ubuntu

如果看到每台服务器都返回"ping": "pong",那就说明配置成功了。

3.3 创建Ansible项目结构

一个好的项目结构能让后续维护轻松很多。我建议这样组织:

sensevoice-ansible/
├── inventories/
│   ├── production/
│   │   └── hosts
│   └── staging/
│       └── hosts
├── group_vars/
│   └── all.yml
├── roles/
│   └── sensevoice/
│       ├── tasks/
│       │   └── main.yml
│       ├── handlers/
│       │   └── main.yml
│       ├── templates/
│       │   └── app.py.j2
│       └── files/
│           └── requirements.txt
└── playbooks/
    └── deploy.yml

我来解释一下每个目录是干什么的:

  • inventories/:存放服务器列表,production是生产环境,staging是测试环境
  • group_vars/:存放所有服务器共用的变量
  • roles/sensevoice/:SenseVoice服务的具体部署逻辑
  • playbooks/:部署的入口文件

4. 编写Ansible部署脚本

4.1 定义服务器清单

先在inventories/production/hosts文件中定义你的服务器:

[sensevoice_servers]
server1 ansible_host=192.168.1.101 ansible_user=ubuntu
server2 ansible_host=192.168.1.102 ansible_user=ubuntu  
server3 ansible_host=192.168.1.103 ansible_user=ubuntu

[sensevoice_servers:vars]
ansible_python_interpreter=/usr/bin/python3
model_cache_server=192.168.1.101

这里我指定了192.168.1.101作为模型缓存服务器,其他服务器会从它那里拉取模型,而不是各自从网上下载。

4.2 配置全局变量

group_vars/all.yml中定义一些全局配置:

# SenseVoice服务配置
sensevoice_port: 7860
sensevoice_host: "0.0.0.0"
sensevoice_model_path: "/opt/sensevoice/models"
sensevoice_app_path: "/opt/sensevoice/app"

# 模型配置
sensevoice_model_repo: "danieldong/sensevoice-small-onnx-quant"
sensevoice_model_file: "model_quant.onnx"

# 系统配置
system_user: "sensevoice"
system_group: "sensevoice"

4.3 创建部署任务

这是最核心的部分,在roles/sensevoice/tasks/main.yml中定义所有部署步骤:

---
- name: 创建系统用户和组
  user:
    name: "{{ system_user }}"
    group: "{{ system_group }}"
    system: yes
    create_home: no

- name: 创建应用目录
  file:
    path: "{{ sensevoice_app_path }}"
    state: directory
    owner: "{{ system_user }}"
    group: "{{ system_group }}"
    mode: '0755'

- name: 创建模型目录
  file:
    path: "{{ sensevoice_model_path }}"
    state: directory
    owner: "{{ system_user }}"
    group: "{{ system_group }}"
    mode: '0755'

- name: 安装系统依赖
  apt:
    name:
      - python3-pip
      - python3-venv
      - ffmpeg
      - libsndfile1
    state: present
    update_cache: yes

- name: 复制requirements文件
  copy:
    src: files/requirements.txt
    dest: "{{ sensevoice_app_path }}/requirements.txt"
    owner: "{{ system_user }}"
    group: "{{ system_group }}"

- name: 创建Python虚拟环境
  pip:
    requirements: "{{ sensevoice_app_path }}/requirements.txt"
    virtualenv: "{{ sensevoice_app_path }}/venv"
    virtualenv_python: python3.9

- name: 部署模型文件(缓存服务器)
  block:
    - name: 下载模型文件
      shell: |
        cd {{ sensevoice_model_path }}
        huggingface-cli download {{ sensevoice_model_repo }} {{ sensevoice_model_file }} --local-dir .
      become: yes
      become_user: "{{ system_user }}"
      when: inventory_hostname == groups['sensevoice_servers'][0]
      
    - name: 创建模型同步服务
      template:
        src: templates/model-sync.service.j2
        dest: /etc/systemd/system/model-sync.service
      when: inventory_hostname == groups['sensevoice_servers'][0]
      
  when: inventory_hostname == groups['sensevoice_servers'][0]

- name: 从缓存服务器拉取模型(其他服务器)
  synchronize:
    src: "{{ sensevoice_model_path }}/"
    dest: "{{ sensevoice_model_path }}"
    mode: pull
    rsync_opts:
      - "--exclude=*.tmp"
  delegate_to: "{{ model_cache_server }}"
  when: inventory_hostname != groups['sensevoice_servers'][0]

- name: 复制应用代码
  template:
    src: templates/app.py.j2
    dest: "{{ sensevoice_app_path }}/app.py"
    owner: "{{ system_user }}"
    group: "{{ system_group }}"
    mode: '0644'

- name: 创建systemd服务文件
  template:
    src: templates/sensevoice.service.j2
    dest: /etc/systemd/system/sensevoice.service

- name: 重载systemd配置
  systemd:
    daemon_reload: yes

- name: 启用并启动服务
  systemd:
    name: sensevoice
    state: started
    enabled: yes
    daemon_reload: yes

- name: 检查服务状态
  shell: systemctl status sensevoice --no-pager
  register: service_status
  changed_when: false

- name: 显示服务状态
  debug:
    msg: "{{ service_status.stdout_lines }}"

这个脚本做了很多事情,我挑几个重点说说:

  1. 创建专用用户:为了安全,不用root用户运行服务
  2. 安装依赖:包括Python包和系统库(ffmpeg处理音频必须)
  3. 智能模型部署:第一台服务器从Hugging Face下载,其他服务器从第一台拉取
  4. 创建系统服务:用systemd管理,服务崩溃了会自动重启

4.4 创建应用模板

我们需要一个FastAPI应用来提供REST API。在roles/sensevoice/templates/app.py.j2中:

from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.responses import JSONResponse
import uvicorn
import soundfile as sf
import numpy as np
import tempfile
import os
from funasr_onnx import SenseVoiceSmall
import logging

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="SenseVoice语音识别服务")

# 初始化模型
MODEL_PATH = "{{ sensevoice_model_path }}"
model = None

@app.on_event("startup")
async def startup_event():
    """服务启动时加载模型"""
    global model
    try:
        logger.info(f"正在加载模型,路径: {MODEL_PATH}")
        model = SenseVoiceSmall(
            MODEL_PATH,
            batch_size=10,
            quantize=True
        )
        logger.info("模型加载成功")
    except Exception as e:
        logger.error(f"模型加载失败: {e}")
        raise

@app.get("/health")
async def health_check():
    """健康检查接口"""
    return {
        "status": "healthy",
        "model_loaded": model is not None,
        "service": "sensevoice-onnx"
    }

@app.post("/api/transcribe")
async def transcribe_audio(
    file: UploadFile = File(...),
    language: str = "auto",
    use_itn: bool = True
):
    """
    语音转写接口
    
    Args:
        file: 音频文件
        language: 语言代码 (auto, zh, en, yue, ja, ko)
        use_itn: 是否使用逆文本正则化
    """
    if model is None:
        raise HTTPException(status_code=503, detail="模型未加载")
    
    # 检查文件类型
    allowed_extensions = ['.wav', '.mp3', '.m4a', '.flac']
    file_ext = os.path.splitext(file.filename)[1].lower()
    
    if file_ext not in allowed_extensions:
        raise HTTPException(
            status_code=400,
            detail=f"不支持的文件格式。支持: {', '.join(allowed_extensions)}"
        )
    
    try:
        # 保存上传的文件
        with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
            content = await file.read()
            tmp_file.write(content)
            tmp_path = tmp_file.name
        
        # 读取音频文件
        audio, sample_rate = sf.read(tmp_path)
        
        # 如果是单声道,确保是1D数组
        if len(audio.shape) > 1:
            audio = audio.mean(axis=1)
        
        # 执行语音识别
        result = model([tmp_path], language=language, use_itn=use_itn)
        
        # 清理临时文件
        os.unlink(tmp_path)
        
        return JSONResponse({
            "text": result[0],
            "language": language,
            "success": True,
            "audio_duration": len(audio) / sample_rate if sample_rate else 0
        })
        
    except Exception as e:
        logger.error(f"语音识别失败: {e}")
        raise HTTPException(status_code=500, detail=f"处理失败: {str(e)}")

@app.get("/docs", include_in_schema=False)
async def custom_docs():
    """自定义API文档重定向"""
    from fastapi.responses import RedirectResponse
    return RedirectResponse(url="/docs")

if __name__ == "__main__":
    uvicorn.run(
        app,
        host="{{ sensevoice_host }}",
        port={{ sensevoice_port }},
        log_level="info"
    )

这个应用提供了两个主要接口:

  • /health:健康检查,监控服务状态
  • /api/transcribe:核心的语音转写接口

4.5 创建系统服务文件

roles/sensevoice/templates/sensevoice.service.j2中:

[Unit]
Description=SenseVoice ONNX语音识别服务
After=network.target

[Service]
Type=simple
User={{ system_user }}
Group={{ system_group }}
WorkingDirectory={{ sensevoice_app_path }}
Environment="PATH={{ sensevoice_app_path }}/venv/bin"
ExecStart={{ sensevoice_app_path }}/venv/bin/python app.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

4.6 创建依赖文件

roles/sensevoice/files/requirements.txt中:

funasr-onnx==0.0.1
gradio==4.19.0
fastapi==0.104.1
uvicorn[standard]==0.24.0
soundfile==0.12.1
jieba==0.42.1
numpy==1.24.3
python-multipart==0.0.6

5. 执行部署与验证

5.1 创建部署Playbook

playbooks/deploy.yml中:

---
- name: 部署SenseVoice语音识别集群
  hosts: sensevoice_servers
  become: yes
  gather_facts: yes
  
  roles:
    - sensevoice
  
  handlers:
    - name: restart sensevoice
      systemd:
        name: sensevoice
        state: restarted
        daemon_reload: yes

5.2 执行部署

现在一切准备就绪,执行部署命令:

cd sensevoice-ansible
ansible-playbook -i inventories/production/hosts playbooks/deploy.yml

你会看到Ansible开始执行任务,输出类似这样:

PLAY [部署SenseVoice语音识别集群] **********************************************

TASK [Gathering Facts] *********************************************************
ok: [server1]
ok: [server2]
ok: [server3]

TASK [sensevoice : 创建系统用户和组] *******************************************
changed: [server1]
changed: [server2]
changed: [server3]

TASK [sensevoice : 创建应用目录] ***********************************************
changed: [server1]
changed: [server2]
changed: [server3]

...(更多任务输出)...

PLAY RECAP *********************************************************************
server1  : ok=15 changed=12 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
server2  : ok=14 changed=11 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0  
server3  : ok=14 changed=11 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0

整个过程大概需要5-10分钟,主要时间花在下载模型和安装Python包上。

5.3 验证部署结果

部署完成后,我们来验证一下服务是否正常。

首先检查服务状态

# 在控制机上执行
ansible sensevoice_servers -i inventories/production/hosts -m shell -a "systemctl status sensevoice --no-pager"

应该看到所有服务器的服务都是active (running)状态。

测试健康检查接口

# 测试第一台服务器
curl http://192.168.1.101:7860/health

应该返回:

{
  "status": "healthy",
  "model_loaded": true,
  "service": "sensevoice-onnx"
}

测试语音识别接口

先准备一个测试音频文件(可以用手机录一段话保存为test.wav),然后:

curl -X POST "http://192.168.1.101:7860/api/transcribe" \
  -F "file=@test.wav" \
  -F "language=auto" \
  -F "use_itn=true"

如果一切正常,你会得到转写结果:

{
  "text": "这是一段测试语音,用于验证语音识别服务是否正常工作。",
  "language": "zh",
  "success": true,
  "audio_duration": 3.5
}

5.4 测试多语言支持

SenseVoice支持多种语言,我们可以测试一下:

# 测试英语
curl -X POST "http://192.168.1.101:7860/api/transcribe" \
  -F "file=@english.wav" \
  -F "language=en"

# 测试粤语
curl -X POST "http://192.168.1.102:7860/api/transcribe" \
  -F "file=@cantonese.wav" \
  -F "language=yue"

# 让模型自动检测语言
curl -X POST "http://192.168.1.103:7860/api/transcribe" \
  -F "file=@japanese.wav" \
  -F "language=auto"

6. 生产环境优化建议

6.1 负载均衡配置

现在我们有3台服务器都运行着语音识别服务,可以通过Nginx做负载均衡。创建一个Nginx配置:

upstream sensevoice_backend {
    server 192.168.1.101:7860;
    server 192.168.1.102:7860;
    server 192.168.1.103:7860;
    
    # 最少连接数负载均衡
    least_conn;
}

server {
    listen 80;
    server_name voice-api.yourdomain.com;
    
    location / {
        proxy_pass http://sensevoice_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # 超时设置
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # 健康检查
    location /health {
        proxy_pass http://sensevoice_backend;
    }
}

6.2 监控与告警

生产环境必须要有监控。我建议用Prometheus + Grafana:

  1. 在每台服务器上部署Node Exporter,监控系统资源
  2. 添加自定义指标,比如:
    • 语音识别请求数
    • 平均处理时间
    • 错误率
    • 模型加载状态

可以在app.py中添加Prometheus指标:

from prometheus_client import Counter, Histogram, generate_latest

# 定义指标
REQUEST_COUNT = Counter('sensevoice_requests_total', 'Total requests')
REQUEST_LATENCY = Histogram('sensevoice_request_latency_seconds', 'Request latency')
ERROR_COUNT = Counter('sensevoice_errors_total', 'Total errors')

@app.post("/api/transcribe")
async def transcribe_audio(...):
    REQUEST_COUNT.inc()
    start_time = time.time()
    
    try:
        # ... 处理逻辑 ...
        duration = time.time() - start_time
        REQUEST_LATENCY.observe(duration)
        return result
    except Exception as e:
        ERROR_COUNT.inc()
        raise

@app.get("/metrics")
async def metrics():
    return Response(generate_latest(), media_type="text/plain")

6.3 日志收集

用ELK Stack(Elasticsearch, Logstash, Kibana)收集和分析日志:

  1. 在每台服务器上安装Filebeat
  2. 配置收集/var/log/syslog和journalctl日志
  3. 在Kibana中创建仪表板,监控错误日志和性能指标

6.4 自动扩缩容

如果流量波动比较大,可以考虑自动扩缩容。用Kubernetes的话很简单,但如果是纯虚拟机,可以用Ansible + 监控脚本:

#!/bin/bash
# auto-scale.sh

# 获取当前CPU使用率
CPU_USAGE=$(ssh user@server1 "top -bn1 | grep 'Cpu(s)' | awk '{print \$2}'")

# 如果CPU使用率超过80%,增加一台服务器
if (( $(echo "$CPU_USAGE > 80" | bc -l) )); then
    echo "CPU使用率过高 ($CPU_USAGE%),开始扩容..."
    
    # 调用云服务商API创建新服务器
    # 这里以AWS为例
    NEW_INSTANCE_ID=$(aws ec2 run-instances \
        --image-id ami-xxxxxxxx \
        --instance-type t3.medium \
        --key-name my-key \
        --security-group-ids sg-xxxxxxxx \
        --subnet-id subnet-xxxxxxxx \
        --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=sensevoice-auto-scale}]' \
        --query 'Instances[0].InstanceId' \
        --output text)
    
    # 等待实例启动
    aws ec2 wait instance-running --instance-ids $NEW_INSTANCE_ID
    
    # 获取新实例IP
    NEW_IP=$(aws ec2 describe-instances \
        --instance-ids $NEW_INSTANCE_ID \
        --query 'Reservations[0].Instances[0].PublicIpAddress' \
        --output text)
    
    # 添加到Ansible inventory
    echo "server_new ansible_host=$NEW_IP ansible_user=ubuntu" >> inventories/production/hosts
    
    # 部署服务
    ansible-playbook -i inventories/production/hosts playbooks/deploy.yml --limit server_new
    
    # 更新负载均衡配置
    # ... 更新Nginx配置并重载 ...
    
    echo "扩容完成,新增服务器: $NEW_IP"
fi

然后把这个脚本加到crontab里,每分钟执行一次。

7. 常见问题与解决方案

7.1 模型下载失败

问题:部署时模型下载很慢或者失败。

解决方案

  1. 提前下载模型到本地,然后通过内网分发
  2. 使用国内镜像源

修改部署脚本,使用本地模型文件:

- name: 从本地复制模型文件
  copy:
    src: "/local/path/to/model_quant.onnx"
    dest: "{{ sensevoice_model_path }}/model_quant.onnx"
    owner: "{{ system_user }}"
    group: "{{ system_group }}"
  when: inventory_hostname == groups['sensevoice_servers'][0]

7.2 内存不足

问题:语音识别时内存占用过高。

解决方案

  1. 调整batch_size参数,减少批处理大小
  2. 增加swap空间
  3. 使用内存限制

在app.py中调整:

model = SenseVoiceSmall(
    MODEL_PATH,
    batch_size=5,  # 从10减少到5
    quantize=True
)

7.3 并发性能问题

问题:高并发时响应变慢。

解决方案

  1. 增加服务器数量
  2. 使用异步处理
  3. 添加请求队列

使用FastAPI的异步特性:

from concurrent.futures import ThreadPoolExecutor
import asyncio

executor = ThreadPoolExecutor(max_workers=10)

@app.post("/api/transcribe")
async def transcribe_audio(...):
    # 使用线程池处理CPU密集型任务
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(
        executor,
        lambda: model([tmp_path], language=language, use_itn=use_itn)
    )
    return result

7.4 音频格式不支持

问题:上传的音频文件格式不被支持。

解决方案

  1. 使用ffmpeg进行格式转换
  2. 添加更详细的错误提示

在代码中添加格式转换:

import subprocess

def convert_audio(input_path, output_path):
    """转换音频格式为wav"""
    cmd = [
        'ffmpeg', '-i', input_path,
        '-acodec', 'pcm_s16le',
        '-ar', '16000',
        '-ac', '1',
        output_path,
        '-y'
    ]
    subprocess.run(cmd, check=True, capture_output=True)

8. 总结

通过这篇文章,我们完成了一个完整的SenseVoice-small-onnx语音识别服务集群的自动化部署。让我简单回顾一下关键点:

第一,Ansible让部署变得简单。我们只需要写一次部署脚本,就可以在任何数量的服务器上重复执行。今天部署3台,明天部署30台,工作量几乎是一样的。

第二,模型缓存机制很重要。230MB的模型文件,如果每台服务器都从网上下载,既慢又浪费。我们让第一台服务器下载,其他服务器从内网拉取,速度提升了几十倍。

第三,生产环境要考虑周全。负载均衡、监控告警、日志收集、自动扩缩容,这些都不是可有可无的。特别是语音识别这种CPU密集型的服务,监控系统资源使用情况特别重要。

第四,SenseVoice-small真的很实用。支持50多种语言自动检测,10秒音频只要70毫秒就能识别,还有情感识别功能。无论是做客服系统、会议记录,还是内容审核,都能用得上。

我建议你根据自己的实际需求调整这个方案。如果只是内部使用,可能不需要那么复杂的监控和扩缩容。如果是面向公众的服务,那么安全性、稳定性和性能就都要考虑进去。

最后,自动化部署不是一劳永逸的。随着业务发展,你可能需要调整配置、更新模型、优化性能。但有了Ansible这个基础,这些后续的维护工作也会轻松很多。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐