CVOICE 实战 - 语音识别服务选型与部署指南

0 阅读8分钟

语音识别是语音质检系统的核心引擎,选错方案轻则准确率惨不忍睹,重则成本爆炸。这篇聊聊我们在 CVOICE 项目中踩过的坑,以及最终落地的技术方案。

上一篇讲了 CVOICE 的整体架构,今天把镜头拉近,聚焦语音识别(ASR)这层。这是整个系统最"硬核"的部分——模型怎么选、服务怎么部署、性能怎么调优,每一个决策都直接影响质检效果。

我们团队在这块折腾了将近三个月,试过开源方案、也调研过云服务,最终形成了现在的混合部署架构。分享出来,希望能帮你少走点弯路。

CVOICE 电话录音界面 CVOICE 电话录音界面


先搞清楚你的需求

选型之前,必须先回答三个问题:

1. 你的录音是什么语言?

中文场景和英文场景完全是两条技术路线。Whisper 在英文上表现优异,但中文的准确率和速度都不如专门的中文模型。如果你的录音里夹杂着中英混杂(比如"Hello,请问您是张先生吗"),那选型就更复杂了。

2. 你的实时性要求是什么?

离线转写(文件上传后异步处理)和实时转写(边录音边出文字)对架构的要求天差地别。CVOICE 目前做的是离线质检,所以我们的方案是基于文件批处理的。如果你需要实时质检,这篇文章只能参考一半。

3. 你的成本预算是多少?

云服务 ASR 按量计费,1 小时录音大概 1-3 元。听起来不贵?但一天 1000 小时录音就是 1000-3000 元,一个月下来 3-9 万。自建 ASR 服务器一次性投入,但 GPU 服务器的月租也不便宜。

我们的业务量中等(日均 500-800 小时录音),最终选择了自建 + 云服务兜底的混合方案。


主流 ASR 方案对比

我们实际测试过的方案有四个:

| 方案 | 中文准确率 | 速度 | 成本 | 部署难度 | | --- | --- | --- | --- | --- | | 阿里云 ASR | 97%+ | 快 | 高(按量计费) | 低 | | FunASR (SenseVoice) | 95%+ | 快 | 低(自建) | 中 | | Whisper (OpenAI) | 85-90% | 中等 | 低(自建) | 低 | | 讯飞听见 | 96%+ | 快 | 极高 | 低 |

阿里云 ASR 准确率最高,但价格也最贵。我们用它来处理一些对准确率要求极高的场景(比如涉及法律纠纷的录音)。

FunASR 是阿里开源的,其中的 SenseVoiceSmall 模型在中文场景下表现非常接近云服务。500MB 的模型体积,4GB 显存就能跑起来,性价比极高。这是我们的主力方案。

Whisper 是 OpenAI 开源的,英文场景无敌,中文差点意思。我们把它作为备选,处理一些英文录音。

讯飞听见 准确率也不错,但价格劝退,我们只在小范围测试过,没上生产环境。


FunASR 部署实战

FunASR 是我们花精力最多的部分,详细讲讲部署过程。

环境准备

我们用的服务器配置:

  • • GPU 模式:NVIDIA RTX 4090(24GB 显存),CUDA 12.1

  • • CPU 模式:8 核 16G 内存,用于低峰期兜底

Docker 镜像是官方提供的,直接拉就行:

# GPU 版本
docker pull registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-gpu-0.2.0

# CPU 版本  
docker pull registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-cpu-0.4.6

启动服务

GPU 模式的启动脚本:

docker run --gpus all -p 8000:8000 \
  -v $(pwd)/models:/workspace/models \
  -v $(pwd)/audio:/workspace/audio \
  registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-gpu-0.2.0

注意 -v 挂载的两个目录:

  • • models:存放下载好的 SenseVoiceSmall 模型,避免每次启动都重新下载

  • • audio:临时存放待转写的音频文件

模型下载

第一次启动会自动下载模型,但速度很慢。建议提前手动下载:

from funasr import AutoModel

# 下载 SenseVoiceSmall 模型
model = AutoModel(
    model="iic/SenseVoiceSmall",
    device="cuda",
    disable_update=True
)

# 下载标点恢复模型
punc_model = AutoModel(
    model="ct-punc",
    device="cuda",
    disable_update=True
)

下载好的模型默认在 ~/.cache/modelscope/hub/iic/ 目录,把它拷到挂载的 models 目录就行。


转写服务封装

FunASR 提供的是基础能力,我们需要封装一层 HTTP API,让 CVOICE 的 Agent 能方便调用。

API 设计

from fastapi import FastAPI, File, UploadFile
from funasr import AutoModel
from zhconv import convert
import re

app = FastAPI()

# 加载模型(启动时执行)
model = AutoModel(
    model="iic/SenseVoiceSmall",
    device="cuda",
    ncpu=4,
    disable_update=True
)

punc_model = AutoModel(
    model="ct-punc",
    device="cuda",
    disable_update=True
)

@app.post("/transcribe")
async def transcribe(audio: UploadFile = File(...)):
    """音频转写接口"""
    # 保存上传的文件
    audio_path = f"/tmp/{audio.filename}"
    with open(audio_path, "wb") as f:
        f.write(await audio.read())
    
    try:
        # 1. ASR 转写
        result = model.generate(
            input=audio_path,
            language="auto",
            use_itn=True
        )
        
        text = result[0]["text"]
        language = result[0].get("language""zh")
        duration = result[0].get("duration", 0)
        
        # 2. 繁体转简体
        text = convert(text, 'zh-cn')
        
        # 3. 清理标记
        text = re.sub(r'<[^>]*>''', text)
        text = re.sub(r'[。!?!?,,;;]{2,}''。', text)
        text = re.sub(r'\s+'' ', text).strip()
        
        # 4. 标点恢复
        if punc_model and text:
            punc_result = punc_model.generate(input=text)
            text = punc_result[0]["text"]
        
        return {
            "code": 0,
            "text": text,
            "language": language,
            "duration": duration,
            "confidence": result[0].get("confidence", 0.95)
        }
    
    except Exception as e:
        return {"code": -1, "error": str(e)}
    
    finally:
        # 清理临时文件
        if os.path.exists(audio_path):
            os.remove(audio_path)

关键处理点

繁简转换:台湾、香港客户的录音经常有繁体字,如果不转换,敏感词匹配会失效。用 zhconv 库一行代码搞定。

标记清理:SenseVoice 输出会带语言标记(如 <|zh|><|NEUTRAL|><|Speech|>),需要正则清理掉。

标点恢复:ASR 输出通常没标点,用 ct-punc 模型恢复后,质检员阅读起来舒服很多。


性能调优

GPU 并发配置

RTX 4090 24GB 显存,我们测试出的最佳并发数是 4:

# 服务配置
SERVICE_CONFIG = {
    "funasr-gpu-01": {
        "url""http://gpu-01:8000/transcribe",
        "concurrency": 4,      # 最大并发
        "gpu": True,
        "timeout": 300         # 5分钟超时
    }
}

并发太高会导致显存 OOM,太低又浪费算力。建议从 2 开始逐步增加,观察显存占用和响应时间。

音频预处理

不是所有录音都能直接丢给 ASR,需要预处理:

import ffmpeg

def preprocess_audio(input_path: str, output_path: str):
    """音频预处理:统一转为 16kHz 单声道 WAV"""
    try:
        (
            ffmpeg
            .input(input_path)
            .output(output_path, 
                   ar=16000,      # 采样率 16kHz
                   ac=1,          # 单声道
                   acodec='pcm_s16le'# 16bit PCM
            .run(quiet=True)
        )
        return True
    except ffmpeg.Error as e:
        logger.error(f"音频预处理失败: {e}")
        return False

统一转为 16kHz 单声道 WAV,能显著提升识别准确率。FunASR 虽然支持多种格式,但标准输入效果更好。

批量处理优化

对于大量录音,别串行处理,用线程池:

from concurrent.futures import ThreadPoolExecutor, as_completed

def batch_transcribe(audio_files: list, max_workers: int = 4):
    """批量转写"""
    results = {}
    
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        # 提交所有任务
        future_to_file = {
            executor.submit(transcribe_single, file): file 
            for file in audio_files
        }
        
        # 收集结果
        for future in as_completed(future_to_file):
            file = future_to_file[future]
            try:
                results[file] = future.result()
            except Exception as e:
                results[file] = {"error": str(e)}
    
    return results

混合调度策略

我们最终的架构是GPU 主力 + CPU 兜底 + 云服务保底的三层结构:

# 转写服务调度器
class TranscriptionScheduler:
    def __init__(self):
        self.services = [
            {"name""gpu-01""url""http://gpu-01:8000""type""gpu""load": 0},
            {"name""gpu-02""url""http://gpu-02:8000""type""gpu""load": 0},
            {"name""cpu-01""url""http://cpu-01:8000""type""cpu""load": 0},
        ]
        self.aliyun_client = AliyunASRClient()  # 云服务客户端
    
    def schedule(self, audio_file: str, priority: str = "normal") -> dict:
        """调度转写请求"""
        
        # 高优先级任务直接走云服务
        if priority == "high":
            return self.aliyun_client.transcribe(audio_file)
        
        # 普通任务:优先 GPU,满了转 CPU
        gpu_services = [s for s in self.services if s["type"] == "gpu" and s["load"] < 4]
        if gpu_services:
            service = min(gpu_services, key=lambda x: x["load"])
            return self._call_service(service, audio_file)
        
        # GPU 满了,看 CPU
        cpu_services = [s for s in self.services if s["type"] == "cpu" and s["load"] < 2]
        if cpu_services:
            service = cpu_services[0]
            return self._call_service(service, audio_file)
        
        # 都满了, fallback 到云服务
        return self.aliyun_client.transcribe(audio_file)

调度逻辑很简单:

  • • 普通录音走自建服务(GPU 优先,满了转 CPU)

  • • 高优先级录音(如涉及投诉、法律风险)直接走阿里云 ASR

  • • 高峰期自建服务满载时,自动降级到云服务

这样既保证了成本可控,又能在关键时刻不掉链子。


常见问题排查

显存 OOM

现象:服务突然崩溃,日志显示 CUDA out of memory

解决:降低并发数,或者改用 CPU 模式处理部分任务。也可以尝试用 torch.cuda.empty_cache() 定期清理显存。

识别结果为空

现象:返回的 text 是空字符串。

排查:检查音频格式是否正确,FunASR 对采样率、声道数敏感。用 ffprobe 查看音频信息,必要时用 ffmpeg 转换。

语速太快识别率低

现象:客服说话像机关枪,识别结果乱码。

解决:这是 ASR 的通病。可以尝试在预处理阶段调整音频速度(atempo=0.8),或者换用专门针对快语速优化的模型。

中英混杂识别差

现象:录音里中英文夹杂,识别结果中英文都错乱。

解决:SenseVoice 支持语言自动检测,但中英混杂场景确实容易出问题。建议按段落切分,分别指定语言参数处理。


写在最后

语音识别是语音质检系统的底座,底座不稳,上面的敏感词匹配、话术评分都是空中楼阁。

我们在 CVOICE 项目中的经验是:

下一篇我们聊聊敏感词匹配引擎的实现,包括 Trie 树优化、语义匹配、以及如何处理"变体词"(比如把"诈骗"写成"诈.骗"或"诈 骗")。

CVOICE 质检规则配置 CVOICE 质检规则配置


全文约 3400 字