快速上手Fun-ASR语音识别:WebSocket流式接口开发与前端集成

1. 项目概述与核心价值

Fun-ASR-MLT-Nano-2512是阿里通义实验室推出的轻量级多语言语音识别模型,支持31种语言的高精度识别。这个800M参数的模型在消费级GPU上即可流畅运行,特别适合需要实时语音转写的应用场景。

传统语音识别方案通常需要上传完整音频文件,等待处理完成后才能获取结果。但在实际业务中,我们经常需要:

  • 实时会议记录:边说边转文字
  • 在线教育互动:即时反馈学生发言
  • 智能客服对话:实时理解用户需求

这些场景都要求流式识别能力——语音数据持续输入,识别结果实时输出。本文将带你从零构建一个基于WebSocket的流式语音识别接口,并完成前端集成。

2. 环境准备与模型部署

2.1 基础环境要求

确保你的开发环境满足以下条件:

  • 操作系统:Ubuntu 20.04或更高版本
  • Python:3.8及以上
  • GPU:NVIDIA显卡(可选但推荐)
  • 内存:至少8GB
  • 磁盘空间:5GB以上

2.2 快速部署模型

通过以下命令一键启动模型服务:

# 安装依赖
pip install -r requirements.txt
apt-get install -y ffmpeg

# 启动服务
cd /root/Fun-ASR-MLT-Nano-2512
nohup python app.py > /tmp/funasr_web.log 2>&1 &
echo $! > /tmp/funasr_web.pid

服务启动后,可以通过http://localhost:7860访问基础的Gradio界面。但我们的目标是构建更强大的流式接口。

3. WebSocket流式接口开发

3.1 项目结构调整

首先创建专门处理流式识别的目录结构:

Fun-ASR-MLT-Nano-2512/
├── model_loader.py     # 模型单例加载器
├── ws_server/
│   ├── __init__.py
│   ├── server.py       # WebSocket主服务
│   └── stream_processor.py  # 流式处理核心
└── app.py              # 原Gradio服务(保留)

3.2 核心流式处理器实现

stream_processor.py负责音频数据的实时处理:

import numpy as np
import torch
from model_loader import get_model

class ASRStreamProcessor:
    def __init__(self, device="cuda:0"):
        self.model = get_model(device)
        self.cache = {}  # 识别状态缓存
        self.audio_buffer = np.array([], dtype=np.float32)
        self.sampling_rate = 16000
    
    def append_audio_chunk(self, chunk_bytes: bytes) -> None:
        """追加音频数据块"""
        audio_np = np.frombuffer(chunk_bytes, dtype=np.int16).astype(np.float32)
        audio_np /= 32768.0  # 归一化
        self.audio_buffer = np.concatenate([self.audio_buffer, audio_np])
    
    def process_stream(self, language: str = "中文") -> Dict:
        """执行流式识别"""
        if len(self.audio_buffer) < self.sampling_rate * 0.2:  # 至少200ms音频
            return {"text": "", "is_final": False}
        
        waveform = torch.from_numpy(self.audio_buffer).unsqueeze(0)
        
        try:
            res = self.model.generate(
                input=waveform,
                cache=self.cache,
                batch_size=1,
                language=language,
                itn=True
            )
            
            if res and len(res) > 0:
                self.cache = res[0].get("cache", {})
            
            return {
                "text": res[0]["text"].strip() if res else "",
                "is_final": False,
                "confidence": res[0].get("confidence", 0.8) if res else 0.0
            }
        except Exception as e:
            print(f"处理错误: {e}")
            return {"text": "", "is_final": False}
    
    def reset(self) -> None:
        """重置会话状态"""
        self.audio_buffer = np.array([], dtype=np.float32)
        self.cache = {}

3.3 WebSocket服务实现

server.py提供WebSocket接口:

import asyncio
import json
import websockets
from stream_processor import ASRStreamProcessor

async def handle_client(websocket, path):
    processor = ASRStreamProcessor()
    
    try:
        async for message in websocket:
            data = json.loads(message)
            cmd = data.get("cmd")
            
            if cmd == "start":
                processor.reset()
                await websocket.send(json.dumps({
                    "status": "started",
                    "language": data.get("language", "中文")
                }))
            
            elif cmd == "audio":
                import base64
                audio_bytes = base64.b64decode(data["data"])
                processor.append_audio_chunk(audio_bytes)
                
                result = processor.process_stream(language=data.get("language", "中文"))
                await websocket.send(json.dumps(result))
            
            elif cmd == "stop":
                result = processor.process_stream()
                await websocket.send(json.dumps({
                    "text": result["text"],
                    "is_final": True,
                    "status": "finished"
                }))
    
    finally:
        print("客户端断开连接")

async def main():
    async with websockets.serve(handle_client, "0.0.0.0", 8765):
        print("WebSocket服务已启动: ws://localhost:8765")
        await asyncio.Future()

if __name__ == "__main__":
    asyncio.run(main())

启动服务:

cd ws_server && python server.py

4. 前端集成实战

4.1 HTML页面结构

创建index.html实现录音和实时展示:

<!DOCTYPE html>
<html>
<head>
    <title>Fun-ASR 流式识别</title>
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 40px auto; }
        #output { background: #f5f5f5; padding: 15px; min-height: 100px; }
        button { padding: 10px 20px; margin: 5px; }
    </style>
</head>
<body>
    <h1>Fun-ASR流式识别演示</h1>
    <p>状态:<span id="status">未连接</span></p>
    
    <button onclick="startMic()">开始录音</button>
    <button onclick="stopMic()" disabled>停止</button>
    
    <div>
        <label>语言:</label>
        <select id="langSelect">
            <option value="中文">中文</option>
            <option value="English">English</option>
        </select>
    </div>
    
    <div id="output"></div>

    <script>
        let socket;
        let mediaRecorder;

        async function startMic() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                
                socket = new WebSocket('ws://localhost:8765');
                socket.onopen = () => {
                    document.getElementById('status').textContent = '已连接';
                    document.querySelector('button[onclick="stopMic()"]').disabled = false;
                    document.querySelector('button[onclick="startMic()"]').disabled = true;
                    
                    socket.send(JSON.stringify({
                        cmd: "start",
                        language: document.getElementById('langSelect').value
                    }));
                };

                socket.onmessage = (event) => {
                    const data = JSON.parse(event.data);
                    if (data.text) {
                        document.getElementById('output').innerHTML += 
                            `<div>${data.text}</div>`;
                    }
                };

                mediaRecorder = new MediaRecorder(stream);
                mediaRecorder.ondataavailable = (event) => {
                    if (socket.readyState === WebSocket.OPEN) {
                        const reader = new FileReader();
                        reader.onload = () => {
                            const arrayBuffer = reader.result;
                            const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
                            socket.send(JSON.stringify({
                                cmd: "audio",
                                data: base64,
                                language: document.getElementById('langSelect').value
                            }));
                        };
                        reader.readAsArrayBuffer(event.data);
                    }
                };

                mediaRecorder.start(200); // 每200ms发送一次数据
                
            } catch (err) {
                console.error('录音失败:', err);
                document.getElementById('status').textContent = '错误: ' + err.message;
            }
        }

        function stopMic() {
            if (mediaRecorder && mediaRecorder.state === 'recording') {
                mediaRecorder.stop();
                socket.send(JSON.stringify({ cmd: "stop" }));
                document.querySelector('button[onclick="startMic()"]').disabled = false;
                document.querySelector('button[onclick="stopMic()"]').disabled = true;
            }
        }
    </script>
</body>
</html>

4.2 关键实现细节

  1. 音频采集:使用浏览器MediaRecorder API获取麦克风输入
  2. 数据传输:将PCM音频数据转为base64编码通过WebSocket发送
  3. 实时展示:服务端返回的识别结果即时显示在页面上
  4. 语言切换:支持动态切换识别语言

5. 生产环境优化建议

5.1 服务稳定性增强

# 在stream_processor.py中添加资源监控
import gc

class ASRStreamProcessor:
    # ...原有代码...
    
    def process_stream(self, language: str = "中文") -> Dict:
        # 限制缓存大小
        if len(self.cache) > 50:
            keys_to_keep = list(self.cache.keys())[-10:]
            self.cache = {k: self.cache[k] for k in keys_to_keep}
            gc.collect()
        
        # ...原有处理逻辑...

5.2 前端自动重连机制

// 在前端代码中添加
let reconnectAttempts = 0;

socket.onclose = () => {
    if (reconnectAttempts < 3) {
        setTimeout(() => {
            startMic();
            reconnectAttempts++;
        }, 3000);
    }
};

socket.onopen = () => {
    reconnectAttempts = 0;
    // ...原有代码...
};

5.3 Docker容器化部署

创建Dockerfile.ws

FROM python:3.11-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    ffmpeg \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt && \
    pip install websockets

COPY . .

EXPOSE 7860 8765

CMD ["sh", "-c", "python app.py & python ws_server/server.py"]

构建并运行:

docker build -f Dockerfile.ws -t funasr-ws .
docker run -d -p 7860:7860 -p 8765:8765 --gpus all funasr-ws

6. 总结与下一步

通过本文,你已经完成了:

  1. Fun-ASR模型的WebSocket流式接口开发
  2. 前端实时语音采集与识别展示集成
  3. 生产环境部署优化方案

这套方案可以轻松集成到各种Web应用中,为你的产品添加实时语音识别能力。下一步可以考虑:

  • 实现自动语言检测
  • 添加标点恢复功能
  • 支持更多音频格式输入

获取更多AI镜像

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

更多推荐