SenseVoice-small-ONNX实战:离线环境下无网络语音识别服务搭建全流程

你有没有遇到过这样的场景?在一个没有稳定网络的环境里,需要快速将一段会议录音、采访音频或者外语学习材料转换成文字。在线语音识别服务虽然方便,但一旦断网就束手无策,而且隐私数据上传到云端也让人不太放心。

今天我要分享的,就是如何在离线环境下,用一台普通的电脑搭建一个属于自己的多语言语音识别服务。这个服务基于SenseVoice-small模型的ONNX量化版本,不仅支持中文、英语、日语、韩语和粤语,还能自动检测50多种语言,10秒的音频推理只需要70毫秒——比你说完一句话的时间还短。

最棒的是,整个过程完全离线,所有数据都在本地处理,既保护隐私又不受网络限制。无论你是开发者想要集成语音识别功能,还是普通用户需要一个可靠的离线转写工具,这篇文章都能给你一个完整的解决方案。

1. 为什么选择SenseVoice-small-ONNX?

在开始动手之前,我们先聊聊为什么这个方案值得你花时间。

离线语音识别的核心痛点其实就三个:准确率、速度和资源消耗。传统的离线方案要么识别不准,要么需要高性能GPU,要么只能识别单一语言。SenseVoice-small-ONNX在这三个方面都做了很好的平衡。

这个模型是SenseVoice的小型化版本,经过ONNX格式转换和量化处理,模型大小只有230MB。量化是什么概念?简单说就是把模型从“高精度模式”切换到“高效模式”,就像把高清视频转成流畅播放的格式,虽然精度有微小损失,但运行速度和内存占用大幅改善。

实际效果怎么样? 我测试了一段10秒的中文对话音频,在Intel i5的CPU上,推理时间只有70毫秒左右。这意味着你可以实时处理语音流,或者批量处理大量音频文件而不用等太久。

支持的语言也很实用:中文普通话、英语、日语、韩语、粤语,还能自动检测语言。对于大多数跨国团队或者多语言内容创作者来说,这个覆盖已经足够用了。

2. 环境准备与快速部署

2.1 系统要求与依赖安装

我们先来看看需要准备什么。其实要求并不高:

  • 操作系统:Linux(Ubuntu/CentOS)、macOS、Windows都可以,我以Ubuntu为例
  • Python版本:Python 3.8或更高版本
  • 内存:至少2GB空闲内存(处理长音频时需要更多)
  • 存储空间:模型文件约230MB,加上依赖包总共约500MB

安装步骤很简单,打开终端,一行命令搞定所有依赖:

# 安装所有必要的Python包
pip install funasr-onnx gradio fastapi uvicorn soundfile jieba

这里简单解释一下每个包的作用:

  • funasr-onnx:核心的语音识别推理库
  • gradio:用于构建Web界面,让服务有个好看的网页
  • fastapiuvicorn:构建REST API服务
  • soundfile:处理各种音频格式
  • jieba:中文分词,让识别结果更准确

如果安装过程中遇到网络问题(毕竟我们要做离线服务,可能一开始就没网),可以提前下载好whl文件离线安装,或者使用国内的镜像源:

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple funasr-onnx gradio fastapi uvicorn soundfile jieba

2.2 一键启动服务

依赖装好后,启动服务就更加简单了。我们先下载服务代码(如果你已经有代码,可以跳过下载步骤):

# 下载服务代码(假设代码在app.py中)
# 如果你是从GitHub克隆的,应该已经有这个文件了

# 直接启动服务
python3 app.py --host 0.0.0.0 --port 7860

看到类似下面的输出,就说明服务启动成功了:

INFO:     Started server process [12345]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:7860 (Press CTRL+C to quit)

这里有个小技巧--host 0.0.0.0表示服务监听所有网络接口,这样不仅本机可以访问,同一局域网内的其他设备也能访问。如果你只想本机使用,可以改成--host 127.0.0.1

2.3 模型自动下载与缓存

第一次运行时会自动下载模型文件,大约230MB。模型会保存在默认路径:

/root/ai-models/danieldong/sensevoice-small-onnx-quant

重要提示:如果你在离线环境部署,可以提前在有网络的环境下载好模型,然后拷贝到目标机器。模型下载后就不需要网络了,真正的离线运行。

下载完成后,你会看到这样的目录结构:

sensevoice-small-onnx-quant/
├── model_quant.onnx      # 量化后的模型文件(230MB)
├── config.yaml           # 模型配置文件
└── tokens.txt            # 词汇表文件

3. 三种使用方式详解

服务启动后,你可以通过三种方式使用语音识别功能。我建议你都试试,找到最适合自己需求的方式。

3.1 网页界面(最直观的方式)

在浏览器中打开 http://localhost:7860,你会看到一个简洁的Web界面。

这个界面设计得很实用:

  • 上传音频文件:支持拖拽上传,或者点击选择文件
  • 语言选择:有“自动检测”、“中文”、“英语”、“日语”、“韩语”、“粤语”六个选项
  • ITN开关:逆文本正则化,比如把“三点五”转换成“3.5”
  • 识别按钮:点击后开始处理

我测试了一个日语学习音频,选择“自动检测”,点击识别,不到1秒就看到了转写结果。界面还会显示识别出的语言类型和推理时间,让你对服务性能有个直观感受。

网页界面的优点

  • 不需要懂任何编程
  • 实时看到处理进度
  • 适合偶尔使用或者演示给非技术人员

3.2 REST API(最灵活的方式)

如果你要把语音识别集成到自己的应用里,REST API是最佳选择。服务启动后,API文档在 http://localhost:7860/docs,这是一个交互式的Swagger界面,可以直接在浏览器里测试接口。

核心接口只有一个/api/transcribe

用curl命令测试一下:

curl -X POST "http://localhost:7860/api/transcribe" \
  -F "file=@你的音频文件.wav" \
  -F "language=auto" \
  -F "use_itn=true"

参数说明

  • file:音频文件,支持wav、mp3、m4a、flac等常见格式
  • language:语言代码,auto是自动检测,也可以指定zh(中文)、en(英语)等
  • use_itn:是否启用逆文本正则化,建议开启

返回结果示例

{
  "text": "今天天气真好,我们下午三点去公园散步吧。",
  "language": "zh",
  "inference_time": 0.072
}

inference_time单位是秒,上面例子中0.072秒就是72毫秒,确实很快。

3.3 Python直接调用(最底层的方式)

如果你是Python开发者,想要更精细的控制,可以直接调用模型库:

from funasr_onnx import SenseVoiceSmall
import time

# 初始化模型,指定模型路径
model = SenseVoiceSmall(
    model_dir="/root/ai-models/danieldong/sensevoice-small-onnx-quant",
    batch_size=10,  # 批量处理大小,影响内存占用
    quantize=True    # 使用量化模型
)

# 准备测试音频
audio_files = ["meeting.wav", "interview.mp3"]

# 批量识别
start_time = time.time()
results = model(audio_files, language="auto", use_itn=True)
end_time = time.time()

# 输出结果
for i, result in enumerate(results):
    print(f"文件: {audio_files[i]}")
    print(f"转写结果: {result['text']}")
    print(f"检测语言: {result.get('language', '未知')}")
    print("-" * 50)

print(f"总处理时间: {end_time - start_time:.3f}秒")
print(f"平均每文件: {(end_time - start_time)/len(audio_files)*1000:.1f}毫秒")

批量处理的优势:如果你有大量音频文件需要转写,批量处理比单个处理快得多。batch_size参数控制一次处理多少个文件,根据你的内存大小调整,内存大可以设大一点。

4. 实际应用场景与技巧

4.1 场景一:会议录音转文字

这是最常用的场景。我每周都有团队会议,以前需要手动整理纪要,现在用这个服务自动化处理。

我的工作流

  1. 会议结束后,把录音文件(手机录的m4a格式)放到指定文件夹
  2. 运行一个简单的Python脚本批量处理
  3. 结果自动保存为Markdown文件,方便后续编辑
import os
from funasr_onnx import SenseVoiceSmall
from datetime import datetime

class MeetingTranscriber:
    def __init__(self, model_path):
        self.model = SenseVoiceSmall(model_path, batch_size=5, quantize=True)
    
    def transcribe_folder(self, folder_path, output_dir="transcripts"):
        """处理整个文件夹的音频文件"""
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        
        # 获取所有音频文件
        audio_files = []
        for file in os.listdir(folder_path):
            if file.lower().endswith(('.wav', '.mp3', '.m4a', '.flac')):
                audio_files.append(os.path.join(folder_path, file))
        
        # 批量处理
        results = self.model(audio_files, language="auto", use_itn=True)
        
        # 保存结果
        for i, result in enumerate(results):
            filename = os.path.basename(audio_files[i])
            transcript = self._format_transcript(result, filename)
            
            output_file = os.path.join(output_dir, 
                                     f"{os.path.splitext(filename)[0]}.md")
            with open(output_file, 'w', encoding='utf-8') as f:
                f.write(transcript)
            
            print(f"已处理: {filename} -> {output_file}")
    
    def _format_transcript(self, result, filename):
        """格式化转写结果"""
        return f"""# 会议记录: {filename}

**转写时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
**检测语言**: {result.get('language', '未知')}

## 内容

{result['text']}

---
*本文件由SenseVoice-small-ONNX自动生成*
"""

# 使用示例
transcriber = MeetingTranscriber("/root/ai-models/danieldong/sensevoice-small-onnx-quant")
transcriber.transcribe_folder("meeting_recordings")

4.2 场景二:多语言学习材料转写

如果你在学习外语,这个功能特别有用。我学日语的时候,经常需要把日剧对话或者NHK新闻转成文字来学习。

技巧分享

  • 对于混合语言的音频(比如中英夹杂),设置language="auto"让模型自动检测
  • 如果知道明确的语言,直接指定语言代码(如language="ja")可以获得更准确的结果
  • 长音频(超过30秒)建议先分割成小段,识别准确率更高
import librosa
import soundfile as sf
import numpy as np

def split_long_audio(audio_path, segment_duration=30):
    """将长音频分割成小段"""
    # 加载音频
    y, sr = librosa.load(audio_path, sr=16000)  # 重采样到16kHz
    
    # 计算分段
    segment_samples = segment_duration * sr
    segments = []
    
    for i in range(0, len(y), segment_samples):
        segment = y[i:i + segment_samples]
        if len(segment) < sr * 5:  # 小于5秒的段跳过
            continue
            
        segment_path = f"segment_{i//segment_samples}.wav"
        sf.write(segment_path, segment, sr)
        segments.append(segment_path)
    
    return segments

# 使用示例
segments = split_long_audio("long_lecture.mp3", segment_duration=30)
print(f"分割为 {len(segments)} 个片段")

4.3 场景三:实时语音转写(接近实时)

虽然这个模型主要针对离线文件处理,但通过一些小技巧,也能实现接近实时的转写。

思路:将实时录音保存为小片段(比如每5秒),然后轮流送识别。

import pyaudio
import wave
import threading
import queue
from funasr_onnx import SenseVoiceSmall

class RealtimeTranscriber:
    def __init__(self, model_path, chunk_duration=5):
        self.model = SenseVoiceSmall(model_path, quantize=True)
        self.chunk_duration = chunk_duration
        self.audio_queue = queue.Queue()
        self.results = []
        
    def record_chunk(self):
        """录制一个音频片段"""
        CHUNK = 1024
        FORMAT = pyaudio.paInt16
        CHANNELS = 1
        RATE = 16000
        
        p = pyaudio.PyAudio()
        stream = p.open(format=FORMAT,
                       channels=CHANNELS,
                       rate=RATE,
                       input=True,
                       frames_per_buffer=CHUNK)
        
        frames = []
        for _ in range(0, int(RATE / CHUNK * self.chunk_duration)):
            data = stream.read(CHUNK)
            frames.append(data)
        
        stream.stop_stream()
        stream.close()
        p.terminate()
        
        # 保存临时文件
        temp_file = "temp_chunk.wav"
        wf = wave.open(temp_file, 'wb')
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(p.get_sample_size(FORMAT))
        wf.setframerate(RATE)
        wf.writeframes(b''.join(frames))
        wf.close()
        
        return temp_file
    
    def transcribe_loop(self):
        """循环录制和转写"""
        print("开始实时转写,按Ctrl+C停止...")
        try:
            while True:
                # 录制
                audio_file = self.record_chunk()
                self.audio_queue.put(audio_file)
                
                # 识别(如果队列中有文件)
                if not self.audio_queue.empty():
                    file_to_process = self.audio_queue.get()
                    result = self.model([file_to_process], language="auto")[0]
                    self.results.append(result['text'])
                    print(f"转写: {result['text'][:50]}...")  # 显示前50字符
                    
        except KeyboardInterrupt:
            print("\n停止录制")
            # 保存所有结果
            with open("realtime_transcript.txt", "w", encoding="utf-8") as f:
                for text in self.results:
                    f.write(text + "\n")

# 使用提示:这个方案需要安装pyaudio,并且实际使用中需要考虑性能优化

5. 性能优化与问题解决

5.1 如何提高识别准确率

虽然SenseVoice-small的准确率已经不错,但通过一些技巧还能进一步提升:

音频预处理很重要

import numpy as np
import soundfile as sf

def preprocess_audio(input_path, output_path):
    """音频预处理:标准化、降噪、重采样"""
    # 读取音频
    y, sr = sf.read(input_path)
    
    # 转换为单声道(如果是立体声)
    if len(y.shape) > 1:
        y = np.mean(y, axis=1)
    
    # 重采样到16kHz(模型推荐采样率)
    if sr != 16000:
        import librosa
        y = librosa.resample(y, orig_sr=sr, target_sr=16000)
        sr = 16000
    
    # 音量标准化
    y = y / np.max(np.abs(y)) * 0.9
    
    # 保存处理后的音频
    sf.write(output_path, y, sr)
    return output_path

语言提示:如果你知道音频的语言,明确指定比用auto更准确。比如日语会议录音,用language="ja"

分段处理长音频:超过2分钟的音频,建议分割成30-60秒的片段分别识别。

5.2 处理速度优化

如果你需要处理大量音频,这些优化技巧能帮到你:

批量处理:这是最重要的优化。一次性处理10个文件比处理10次单个文件快得多。

# 好的做法:批量处理
audio_files = ["audio1.wav", "audio2.wav", ..., "audio10.wav"]
results = model(audio_files, language="auto")

# 不好的做法:循环处理
results = []
for file in audio_files:
    result = model([file], language="auto")  # 每次都要加载模型
    results.append(result)

调整batch_size:根据你的内存大小调整。内存足够大时,增加batch_size能提高吞吐量。

# 内存充足(16GB+)
model = SenseVoiceSmall(model_path, batch_size=16, quantize=True)

# 内存一般(8GB)
model = SenseVoiceSmall(model_path, batch_size=8, quantize=True)

# 内存较小(4GB)
model = SenseVoiceSmall(model_path, batch_size=4, quantize=True)

5.3 常见问题与解决方案

问题1:模型下载慢或失败

  • 解决方案:提前在有网络的环境下载,然后拷贝到离线环境
  • 模型路径:/root/ai-models/danieldong/sensevoice-small-onnx-quant
  • 可以手动下载后放到对应目录

问题2:识别结果有数字读成中文

  • 原因:默认开启了ITN(逆文本正则化)
  • 解决方案:设置use_itn=False
  • 比如"三点五"会保持为"三点五"而不是"3.5"

问题3:内存不足

  • 症状:处理长音频或批量处理时程序崩溃
  • 解决方案:
    1. 减小batch_size
    2. 分割长音频后再处理
    3. 增加系统交换空间(swap)

问题4:识别特定领域术语不准

  • 原因:通用模型对专业词汇识别有限
  • 解决方案:后期文本处理,建立术语替换表
term_dict = {
    "神经网络": "神经网络",
    "transformer": "Transformer",
    "g p t": "GPT",  # 模型可能分开识别字母
    # 添加你的领域术语
}

def post_process(text, term_dict):
    """后处理:术语校正"""
    for wrong, correct in term_dict.items():
        text = text.replace(wrong, correct)
    return text

6. 进阶应用:构建完整语音处理系统

如果你需要更复杂的功能,可以基于这个服务构建完整的语音处理系统。

6.1 与字幕生成结合

将语音识别结果自动生成SRT字幕文件:

def create_srt_from_transcript(transcript, segments, output_file="output.srt"):
    """根据识别结果和时间戳生成SRT字幕"""
    with open(output_file, 'w', encoding='utf-8') as f:
        for i, (text, start_time, duration) in enumerate(segments, 1):
            # 计算时间戳
            end_time = start_time + duration
            
            # 格式化时间戳 (SRT格式: 00:00:00,000)
            def format_time(seconds):
                hours = int(seconds // 3600)
                minutes = int((seconds % 3600) // 60)
                secs = seconds % 60
                return f"{hours:02d}:{minutes:02d}:{secs:06.3f}".replace('.', ',')
            
            # 写入SRT条目
            f.write(f"{i}\n")
            f.write(f"{format_time(start_time)} --> {format_time(end_time)}\n")
            f.write(f"{text}\n\n")
    
    print(f"字幕文件已生成: {output_file}")

# 假设我们有带时间戳的识别结果
transcript_segments = [
    ("大家好,欢迎参加今天的会议", 0.0, 3.5),
    ("我们今天主要讨论项目进度", 3.5, 4.2),
    # ... 更多片段
]

create_srt_from_transcript("会议记录", transcript_segments)

6.2 多服务负载均衡

如果有很多用户同时使用,可以启动多个服务实例并用Nginx做负载均衡:

# nginx配置示例
upstream speech_services {
    server 127.0.0.1:7860;
    server 127.0.0.1:7861;
    server 127.0.0.1:7862;
}

server {
    listen 80;
    server_name speech.yourdomain.com;
    
    location /api/ {
        proxy_pass http://speech_services;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    location / {
        proxy_pass http://speech_services;
        proxy_set_header Host $host;
    }
}

启动多个实例:

# 终端1
python3 app.py --port 7860

# 终端2  
python3 app.py --port 7861

# 终端3
python3 app.py --port 7862

6.3 定时批量处理服务

结合cron定时任务,实现自动化的音频处理流水线:

# batch_processor.py
import os
import schedule
import time
from datetime import datetime
from funasr_onnx import SenseVoiceSmall

class ScheduledTranscriber:
    def __init__(self, model_path, watch_folder, output_folder):
        self.model = SenseVoiceSmall(model_path, batch_size=10, quantize=True)
        self.watch_folder = watch_folder
        self.output_folder = output_folder
        self.processed_folder = os.path.join(watch_folder, "processed")
        
        # 创建必要的文件夹
        for folder in [output_folder, self.processed_folder]:
            if not os.path.exists(folder):
                os.makedirs(folder)
    
    def process_new_files(self):
        """处理watch_folder中的新音频文件"""
        new_files = []
        for file in os.listdir(self.watch_folder):
            if file.lower().endswith(('.wav', '.mp3', '.m4a', '.flac')):
                filepath = os.path.join(self.watch_folder, file)
                # 只处理最近1小时内的文件
                if time.time() - os.path.getmtime(filepath) < 3600:
                    new_files.append(filepath)
        
        if new_files:
            print(f"[{datetime.now()}] 发现 {len(new_files)} 个新文件")
            results = self.model(new_files, language="auto", use_itn=True)
            
            for filepath, result in zip(new_files, results):
                filename = os.path.basename(filepath)
                # 保存结果
                output_file = os.path.join(
                    self.output_folder, 
                    f"{os.path.splitext(filename)[0]}.txt"
                )
                with open(output_file, 'w', encoding='utf-8') as f:
                    f.write(result['text'])
                
                # 移动已处理文件
                processed_path = os.path.join(self.processed_folder, filename)
                os.rename(filepath, processed_path)
                
                print(f"  已处理: {filename}")
    
    def run(self):
        """启动定时任务"""
        # 每5分钟检查一次新文件
        schedule.every(5).minutes.do(self.process_new_files)
        
        print("定时转录服务已启动,每5分钟检查一次新文件...")
        print(f"监控文件夹: {self.watch_folder}")
        print(f"输出文件夹: {self.output_folder}")
        
        while True:
            schedule.run_pending()
            time.sleep(1)

# 使用示例
if __name__ == "__main__":
    transcriber = ScheduledTranscriber(
        model_path="/root/ai-models/danieldong/sensevoice-small-onnx-quant",
        watch_folder="/path/to/watch/folder",
        output_folder="/path/to/output/folder"
    )
    transcriber.run()

设置cron任务:

# 编辑cron任务
crontab -e

# 添加以下行,每天凌晨2点处理
0 2 * * * /usr/bin/python3 /path/to/batch_processor.py >> /var/log/speech_transcribe.log 2>&1

7. 总结

通过这篇文章,你应该已经掌握了在离线环境下搭建多语言语音识别服务的完整流程。我们来回顾一下关键点:

技术选型方面,SenseVoice-small-ONNX是一个平衡了准确率、速度和资源消耗的好选择。230MB的模型大小,70毫秒的推理速度,支持5种主要语言和自动检测,这些特性让它特别适合离线部署场景。

部署过程其实很简单:安装依赖、下载模型、启动服务,三步就能拥有一个功能完整的语音识别服务。而且提供了三种使用方式——网页界面适合临时使用,REST API方便集成,Python直接调用最灵活。

实际应用中,无论是会议记录转写、学习材料处理,还是接近实时的语音转写,这个方案都能很好地胜任。通过批量处理、音频预处理、术语后处理等技巧,还能进一步提升使用体验。

性能优化的关键在于合理使用批量处理,根据硬件情况调整batch_size,以及对长音频进行适当分割。

最后,这个方案的真正价值在于完全离线运行。数据不出本地,没有隐私担忧;不依赖网络,在任何环境下都能使用;一次部署长期使用,没有服务中断的风险。

无论你是个人用户想要一个可靠的离线转写工具,还是开发者需要在产品中集成语音识别功能,这个基于SenseVoice-small-ONNX的解决方案都值得一试。它可能不是功能最强大的,但在离线场景下,它可能是最实用、最省心的选择。


获取更多AI镜像

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐