语音电子病历python_websocket实现

Python实现WebSocket音频处理完整方案

一、Python WebSocket框架对比

1.1 主流框架

框架 特点 性能 推荐指数 适用场景
FastAPI + WebSocket 现代化、类型提示、自动文档 ⭐⭐⭐⭐⭐ 推荐首选
websockets 纯异步、轻量级 极高 ⭐⭐⭐⭐ 简单WebSocket服务
Flask-SocketIO 易用、同步风格 ⭐⭐⭐ 小型项目
Django Channels Django集成、功能全 ⭐⭐⭐ 已有Django项目
Tornado 老牌异步框架 ⭐⭐ 遗留系统

二、方案1:FastAPI + WebSocket(推荐)

2.1 为什么选择FastAPI?

优势:

  • 现代化的异步框架(基于ASGI)
  • 自动API文档(Swagger UI)
  • 类型提示和验证(Pydantic)
  • 高性能(接近Go、Node.js)
  • 易于集成机器学习模型
  • 社区活跃,生态丰富

2.2 完整代码实现

安装依赖
bash 复制代码
# 安装FastAPI和相关库
pip install fastapi uvicorn[standard] python-multipart

# 音频处理库
pip install numpy scipy librosa soundfile

# ASR相关
pip install aliyun-python-sdk-core
pip install websocket-client  # 客户端测试用

# 可选:Redis(用于会话管理)
pip install redis aioredis
核心代码:main.py
python 复制代码
"""
医院语音病历系统 - WebSocket音频处理服务
基于FastAPI实现
"""

from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import Dict, Optional
import asyncio
import json
import logging
import uuid
import time
import numpy as np
from datetime import datetime

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

# 创建FastAPI应用
app = FastAPI(
    title="医院语音病历系统 - ASR服务",
    description="实时语音识别WebSocket API",
    version="1.0.0"
)

# 配置CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 生产环境应限制具体域名
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ==================== 连接管理器 ====================
class ConnectionManager:
    """
    WebSocket连接管理器
    管理所有活跃的WebSocket连接
    """
    def __init__(self):
        # 存储活跃连接:{session_id: WebSocket}
        self.active_connections: Dict[str, WebSocket] = {}
        
        # 会话信息:{session_id: session_info}
        self.sessions: Dict[str, dict] = {}
    
    async def connect(self, websocket: WebSocket, session_id: str):
        """接受新连接"""
        await websocket.accept()
        self.active_connections[session_id] = websocket
        
        # 初始化会话信息
        self.sessions[session_id] = {
            'session_id': session_id,
            'connected_at': datetime.now(),
            'audio_buffer': bytearray(),
            'total_bytes': 0,
            'last_heartbeat': time.time(),
        }
        
        logger.info(f"新连接建立: {session_id}, 当前连接数: {len(self.active_connections)}")
    
    def disconnect(self, session_id: str):
        """断开连接"""
        if session_id in self.active_connections:
            del self.active_connections[session_id]
        
        if session_id in self.sessions:
            session_info = self.sessions[session_id]
            logger.info(
                f"连接断开: {session_id}, "
                f"总接收字节: {session_info['total_bytes']}, "
                f"剩余连接数: {len(self.active_connections)}"
            )
            del self.sessions[session_id]
    
    async def send_message(self, session_id: str, message: dict):
        """发送消息到指定客户端"""
        if session_id in self.active_connections:
            websocket = self.active_connections[session_id]
            try:
                await websocket.send_json(message)
            except Exception as e:
                logger.error(f"发送消息失败 [{session_id}]: {e}")
    
    async def broadcast(self, message: dict):
        """广播消息到所有客户端"""
        for session_id in list(self.active_connections.keys()):
            await self.send_message(session_id, message)
    
    def get_session(self, session_id: str) -> Optional[dict]:
        """获取会话信息"""
        return self.sessions.get(session_id)
    
    def update_heartbeat(self, session_id: str):
        """更新心跳时间"""
        if session_id in self.sessions:
            self.sessions[session_id]['last_heartbeat'] = time.time()


# 创建全局连接管理器
manager = ConnectionManager()


# ==================== ASR服务封装 ====================
class ASRService:
    """
    语音识别服务封装
    支持多种ASR提供商
    """
    def __init__(self, provider='mock'):
        """
        初始化ASR服务
        
        Args:
            provider: 'aliyun' | 'xunfei' | 'mock'
        """
        self.provider = provider
        self.sample_rate = 16000
        self.channels = 1
        
        if provider == 'aliyun':
            self._init_aliyun()
        elif provider == 'xunfei':
            self._init_xunfei()
        else:
            logger.info("使用Mock ASR服务(仅用于测试)")
    
    def _init_aliyun(self):
        """初始化阿里云ASR"""
        # 这里实现阿里云ASR初始化
        # from aliyunsdkcore.client import AcsClient
        pass
    
    def _init_xunfei(self):
        """初始化讯飞ASR"""
        # 这里实现讯飞ASR初始化
        pass
    
    async def recognize(self, audio_data: bytes, session_id: str) -> dict:
        """
        识别音频
        
        Args:
            audio_data: PCM音频数据
            session_id: 会话ID
        
        Returns:
            识别结果字典
        """
        if self.provider == 'mock':
            # Mock实现(仅用于测试)
            return await self._mock_recognize(audio_data, session_id)
        elif self.provider == 'aliyun':
            return await self._aliyun_recognize(audio_data, session_id)
        elif self.provider == 'xunfei':
            return await self._xunfei_recognize(audio_data, session_id)
    
    async def _mock_recognize(self, audio_data: bytes, session_id: str) -> dict:
        """Mock识别(测试用)"""
        # 模拟识别延迟
        await asyncio.sleep(0.3)
        
        # 计算音频时长
        audio_array = np.frombuffer(audio_data, dtype=np.int16)
        duration_seconds = len(audio_array) / self.sample_rate
        
        # 返回模拟结果
        mock_texts = [
            "我头疼",
            "有点发烧",
            "大概三天了",
            "最近血压有点高",
            "吃了药没什么效果",
        ]
        
        import random
        return {
            'text': random.choice(mock_texts),
            'isFinal': duration_seconds > 2.0,  # 超过2秒视为句子结束
            'confidence': 0.85 + random.random() * 0.1,
            'duration': duration_seconds,
        }
    
    async def _aliyun_recognize(self, audio_data: bytes, session_id: str) -> dict:
        """阿里云ASR识别"""
        # TODO: 实现真实的阿里云ASR调用
        # 参考前面提供的阿里云ASR代码
        pass
    
    async def _xunfei_recognize(self, audio_data: bytes, session_id: str) -> dict:
        """讯飞ASR识别"""
        # TODO: 实现讯飞ASR调用
        pass


# 创建ASR服务实例
asr_service = ASRService(provider='mock')


# ==================== 音频处理工具 ====================
class AudioProcessor:
    """
    音频处理工具类
    """
    @staticmethod
    def validate_audio(audio_data: bytes) -> bool:
        """验证音频数据"""
        if len(audio_data) < 100:  # 至少100字节
            return False
        return True
    
    @staticmethod
    def calculate_rms(audio_data: bytes) -> float:
        """计算音频RMS(响度)"""
        audio_array = np.frombuffer(audio_data, dtype=np.int16)
        rms = np.sqrt(np.mean(audio_array.astype(np.float32) ** 2))
        return float(rms)
    
    @staticmethod
    def is_silence(audio_data: bytes, threshold: float = 500.0) -> bool:
        """检测是否为静音"""
        rms = AudioProcessor.calculate_rms(audio_data)
        return rms < threshold
    
    @staticmethod
    def resample(audio_data: bytes, from_rate: int, to_rate: int) -> bytes:
        """重采样音频"""
        if from_rate == to_rate:
            return audio_data
        
        # 使用scipy进行重采样
        from scipy import signal
        
        audio_array = np.frombuffer(audio_data, dtype=np.int16)
        num_samples = int(len(audio_array) * to_rate / from_rate)
        resampled = signal.resample(audio_array, num_samples)
        
        return resampled.astype(np.int16).tobytes()


# ==================== WebSocket端点 ====================
@app.websocket("/ws/asr")
async def websocket_asr_endpoint(websocket: WebSocket):
    """
    实时语音识别WebSocket端点
    
    URL: ws://localhost:8000/ws/asr
    
    通信协议:
    1. 客户端发送二进制音频数据
    2. 服务端返回JSON格式识别结果
    3. 支持心跳检测(客户端发送 {"type": "ping"})
    """
    # 生成会话ID
    session_id = str(uuid.uuid4())
    
    try:
        # 接受连接
        await manager.connect(websocket, session_id)
        
        # 发送欢迎消息
        await manager.send_message(session_id, {
            'type': 'connected',
            'session_id': session_id,
            'timestamp': time.time(),
            'message': '连接成功,开始语音识别'
        })
        
        # 主循环:接收和处理消息
        while True:
            try:
                # 接收消息(支持文本和二进制)
                message = await websocket.receive()
                
                # 处理二进制音频数据
                if 'bytes' in message:
                    await handle_audio_data(session_id, message['bytes'])
                
                # 处理文本控制消息
                elif 'text' in message:
                    await handle_control_message(session_id, message['text'])
            
            except WebSocketDisconnect:
                logger.info(f"客户端主动断开: {session_id}")
                break
            
            except Exception as e:
                logger.error(f"处理消息异常 [{session_id}]: {e}")
                await manager.send_message(session_id, {
                    'type': 'error',
                    'message': str(e)
                })
    
    except Exception as e:
        logger.error(f"WebSocket连接异常 [{session_id}]: {e}")
    
    finally:
        # 清理连接
        manager.disconnect(session_id)


async def handle_audio_data(session_id: str, audio_data: bytes):
    """
    处理音频数据
    
    Args:
        session_id: 会话ID
        audio_data: 音频数据(PCM格式,16kHz,16bit,单声道)
    """
    session = manager.get_session(session_id)
    if not session:
        return
    
    # 更新统计
    session['total_bytes'] += len(audio_data)
    
    # 验证音频
    if not AudioProcessor.validate_audio(audio_data):
        logger.warning(f"无效音频数据 [{session_id}]")
        return
    
    # 检测静音
    if AudioProcessor.is_silence(audio_data):
        logger.debug(f"检测到静音 [{session_id}]")
        return
    
    # 累积音频到缓冲区
    session['audio_buffer'].extend(audio_data)
    
    # 当累积到一定量时进行识别(例如:3200字节 = 100ms@16kHz)
    min_buffer_size = 3200 * 3  # 300ms
    
    if len(session['audio_buffer']) >= min_buffer_size:
        # 提取音频进行识别
        audio_to_recognize = bytes(session['audio_buffer'])
        
        # 清空缓冲区(或保留overlap)
        # 这里简单清空,实际可以保留最后一部分作为上下文
        session['audio_buffer'].clear()
        
        # 异步识别(不阻塞接收)
        asyncio.create_task(
            recognize_and_send(session_id, audio_to_recognize)
        )


async def recognize_and_send(session_id: str, audio_data: bytes):
    """
    识别音频并发送结果
    
    Args:
        session_id: 会话ID
        audio_data: 音频数据
    """
    try:
        # 调用ASR服务识别
        result = await asr_service.recognize(audio_data, session_id)
        
        # 添加时间戳
        result['timestamp'] = time.time()
        result['type'] = 'transcript'
        
        # 发送结果
        await manager.send_message(session_id, result)
        
        logger.info(
            f"识别结果 [{session_id}]: {result['text']} "
            f"(置信度: {result['confidence']:.2f})"
        )
    
    except Exception as e:
        logger.error(f"识别异常 [{session_id}]: {e}")
        await manager.send_message(session_id, {
            'type': 'error',
            'message': f'识别失败: {str(e)}'
        })


async def handle_control_message(session_id: str, message_text: str):
    """
    处理控制消息
    
    Args:
        session_id: 会话ID
        message_text: 消息文本(JSON)
    """
    try:
        message = json.loads(message_text)
        msg_type = message.get('type')
        
        # 心跳检测
        if msg_type == 'ping':
            manager.update_heartbeat(session_id)
            await manager.send_message(session_id, {
                'type': 'pong',
                'timestamp': time.time()
            })
        
        # 重置会话
        elif msg_type == 'reset':
            session = manager.get_session(session_id)
            if session:
                session['audio_buffer'].clear()
                session['total_bytes'] = 0
            
            await manager.send_message(session_id, {
                'type': 'reset_ok',
                'message': '会话已重置'
            })
        
        # 其他控制消息...
        else:
            logger.warning(f"未知消息类型 [{session_id}]: {msg_type}")
    
    except json.JSONDecodeError:
        logger.error(f"无效JSON [{session_id}]: {message_text}")
    except Exception as e:
        logger.error(f"处理控制消息异常 [{session_id}]: {e}")


# ==================== HTTP端点(用于监控和管理)====================
@app.get("/")
async def root():
    """根路径"""
    return {
        "service": "医院语音病历系统 - ASR服务",
        "version": "1.0.0",
        "status": "running",
        "websocket_endpoint": "/ws/asr"
    }


@app.get("/health")
async def health_check():
    """健康检查"""
    return {
        "status": "healthy",
        "active_connections": len(manager.active_connections),
        "timestamp": time.time()
    }


@app.get("/stats")
async def get_stats():
    """获取统计信息"""
    total_bytes = sum(
        session.get('total_bytes', 0) 
        for session in manager.sessions.values()
    )
    
    return {
        "active_connections": len(manager.active_connections),
        "total_sessions": len(manager.sessions),
        "total_bytes_received": total_bytes,
        "sessions": [
            {
                "session_id": sid,
                "connected_at": session['connected_at'].isoformat(),
                "total_bytes": session['total_bytes'],
                "buffer_size": len(session['audio_buffer']),
            }
            for sid, session in manager.sessions.items()
        ]
    }


# ==================== 后台任务:心跳检测 ====================
@app.on_event("startup")
async def startup_event():
    """应用启动时执行"""
    logger.info("ASR服务启动")
    
    # 启动心跳检测任务
    asyncio.create_task(heartbeat_monitor())


async def heartbeat_monitor():
    """
    心跳监控任务
    定期检查所有连接的心跳,超时则断开
    """
    while True:
        await asyncio.sleep(60)  # 每60秒检查一次
        
        current_time = time.time()
        timeout = 120  # 120秒超时
        
        # 检查所有会话
        timeout_sessions = []
        for session_id, session in manager.sessions.items():
            last_heartbeat = session.get('last_heartbeat', 0)
            if current_time - last_heartbeat > timeout:
                timeout_sessions.append(session_id)
        
        # 断开超时连接
        for session_id in timeout_sessions:
            logger.warning(f"会话心跳超时,断开连接: {session_id}")
            if session_id in manager.active_connections:
                websocket = manager.active_connections[session_id]
                try:
                    await websocket.close(code=1000, reason="心跳超时")
                except:
                    pass
                manager.disconnect(session_id)


@app.on_event("shutdown")
async def shutdown_event():
    """应用关闭时执行"""
    logger.info("ASR服务关闭")
    
    # 关闭所有连接
    for session_id in list(manager.active_connections.keys()):
        websocket = manager.active_connections[session_id]
        try:
            await websocket.close(code=1000, reason="服务关闭")
        except:
            pass


# ==================== 启动服务 ====================
if __name__ == "__main__":
    import uvicorn
    
    # 启动服务
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        reload=True,  # 开发模式自动重载
        log_level="info",
        
        # WebSocket配置
        ws_ping_interval=30,  # WebSocket ping间隔(秒)
        ws_ping_timeout=10,   # ping超时(秒)
    )

2.3 客户端测试代码

Python测试客户端
python 复制代码
"""
WebSocket客户端测试脚本
"""

import asyncio
import websockets
import json
import numpy as np

async def test_asr_websocket():
    """测试ASR WebSocket服务"""
    uri = "ws://localhost:8000/ws/asr"
    
    async with websockets.connect(uri) as websocket:
        print("已连接到ASR服务")
        
        # 接收欢迎消息
        welcome = await websocket.recv()
        print(f"欢迎消息: {welcome}")
        
        # 发送心跳
        await websocket.send(json.dumps({"type": "ping"}))
        pong = await websocket.recv()
        print(f"心跳响应: {pong}")
        
        # 模拟发送音频数据
        for i in range(10):
            # 生成模拟音频(1秒,16kHz,16bit)
            duration = 1.0
            sample_rate = 16000
            samples = int(duration * sample_rate)
            
            # 生成正弦波(模拟语音)
            frequency = 440  # A4音符
            audio = np.sin(2 * np.pi * frequency * np.arange(samples) / sample_rate)
            audio = (audio * 32767).astype(np.int16)
            
            # 发送音频数据
            await websocket.send(audio.tobytes())
            print(f"已发送音频包 {i+1}/10")
            
            # 接收识别结果
            try:
                result = await asyncio.wait_for(websocket.recv(), timeout=2.0)
                result_data = json.loads(result)
                print(f"识别结果: {result_data.get('text')} "
                      f"(置信度: {result_data.get('confidence', 0):.2f})")
            except asyncio.TimeoutError:
                print("等待结果超时")
            
            await asyncio.sleep(0.5)
        
        print("测试完成")


if __name__ == "__main__":
    asyncio.run(test_asr_websocket())
JavaScript客户端(浏览器)
html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>ASR WebSocket测试</title>
</head>
<body>
    <h1>语音识别测试</h1>
    <button id="startBtn">开始录音</button>
    <button id="stopBtn" disabled>停止录音</button>
    <div id="status">未连接</div>
    <div id="transcript"></div>

    <script>
        let ws = null;
        let mediaStream = null;
        let audioContext = null;
        let scriptProcessor = null;

        // 连接WebSocket
        function connect() {
            ws = new WebSocket('ws://localhost:8000/ws/asr');
            ws.binaryType = 'arraybuffer';

            ws.onopen = () => {
                console.log('WebSocket已连接');
                document.getElementById('status').textContent = '已连接';
            };

            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                console.log('收到消息:', data);

                if (data.type === 'transcript') {
                    const div = document.getElementById('transcript');
                    div.innerHTML += `<p>${data.text} (${(data.confidence * 100).toFixed(0)}%)</p>`;
                }
            };

            ws.onerror = (error) => {
                console.error('WebSocket错误:', error);
                document.getElementById('status').textContent = '连接错误';
            };

            ws.onclose = () => {
                console.log('WebSocket已关闭');
                document.getElementById('status').textContent = '已断开';
            };
        }

        // 开始录音
        async function startRecording() {
            connect();

            mediaStream = await navigator.mediaDevices.getUserMedia({
                audio: {
                    sampleRate: 16000,
                    channelCount: 1,
                    echoCancellation: true,
                    noiseSuppression: true,
                }
            });

            audioContext = new AudioContext({ sampleRate: 16000 });
            const source = audioContext.createMediaStreamSource(mediaStream);
            scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);

            scriptProcessor.onaudioprocess = (e) => {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    const float32Data = e.inputBuffer.getChannelData(0);
                    const int16Data = new Int16Array(float32Data.length);
                    
                    for (let i = 0; i < float32Data.length; i++) {
                        const s = Math.max(-1, Math.min(1, float32Data[i]));
                        int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
                    }

                    ws.send(int16Data.buffer);
                }
            };

            source.connect(scriptProcessor);
            scriptProcessor.connect(audioContext.destination);

            document.getElementById('startBtn').disabled = true;
            document.getElementById('stopBtn').disabled = false;
            document.getElementById('status').textContent = '录音中...';
        }

        // 停止录音
        function stopRecording() {
            if (scriptProcessor) {
                scriptProcessor.disconnect();
                scriptProcessor = null;
            }

            if (audioContext) {
                audioContext.close();
                audioContext = null;
            }

            if (mediaStream) {
                mediaStream.getTracks().forEach(track => track.stop());
                mediaStream = null;
            }

            if (ws) {
                ws.close();
                ws = null;
            }

            document.getElementById('startBtn').disabled = false;
            document.getElementById('stopBtn').disabled = true;
            document.getElementById('status').textContent = '已停止';
        }

        document.getElementById('startBtn').onclick = startRecording;
        document.getElementById('stopBtn').onclick = stopRecording;
    </script>
</body>
</html>

2.4 部署配置

requirements.txt
txt 复制代码
# Web框架
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6

# WebSocket
websockets==12.0

# 异步支持
aiofiles==23.2.1
aioredis==2.0.1

# 音频处理
numpy==1.24.3
scipy==1.11.3
librosa==0.10.1
soundfile==0.12.1

# ASR SDK(按需选择)
aliyun-python-sdk-core==2.14.0
# 或者讯飞SDK
# xfyun-asr==1.0.0

# 工具库
python-dotenv==1.0.0
pydantic==2.5.0
Dockerfile
dockerfile 复制代码
FROM python:3.11-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y \
    gcc \
    libsndfile1 \
    && rm -rf /var/lib/apt/lists/*

# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码
COPY . .

# 暴露端口
EXPOSE 8000

# 启动服务
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml
yaml 复制代码
version: '3.8'

services:
  asr-service:
    build: .
    ports:
      - "8000:8000"
    environment:
      - ENV=production
      - LOG_LEVEL=info
    volumes:
      - ./logs:/app/logs
    restart: unless-stopped
    
  # 可选:Redis(用于会话管理)
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

三、方案2:websockets库(轻量级)

3.1 纯异步实现

python 复制代码
"""
使用websockets库的简单实现
适合纯WebSocket服务,不需要HTTP API
"""

import asyncio
import websockets
import json
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# 连接管理
connections = set()

async def handle_client(websocket, path):
    """处理客户端连接"""
    # 注册连接
    connections.add(websocket)
    logger.info(f"新连接: {websocket.remote_address}, 当前连接数: {len(connections)}")
    
    try:
        async for message in websocket:
            # 处理二进制音频数据
            if isinstance(message, bytes):
                logger.info(f"收到音频数据: {len(message)} 字节")
                
                # 模拟识别
                await asyncio.sleep(0.3)
                result = {
                    'type': 'transcript',
                    'text': '识别结果',
                    'confidence': 0.85
                }
                await websocket.send(json.dumps(result))
            
            # 处理文本消息
            else:
                data = json.loads(message)
                if data.get('type') == 'ping':
                    await websocket.send(json.dumps({'type': 'pong'}))
    
    except websockets.exceptions.ConnectionClosed:
        logger.info(f"连接关闭: {websocket.remote_address}")
    
    finally:
        # 移除连接
        connections.remove(websocket)
        logger.info(f"剩余连接数: {len(connections)}")


async def main():
    """启动服务器"""
    async with websockets.serve(
        handle_client,
        "0.0.0.0",
        8000,
        ping_interval=30,
        ping_timeout=10,
        max_size=10**7  # 10MB
    ):
        logger.info("WebSocket服务器启动: ws://0.0.0.0:8000")
        await asyncio.Future()  # 永久运行


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

四、性能优化

4.1 多进程部署

python 复制代码
"""
使用Gunicorn + Uvicorn实现多进程
"""

# gunicorn_config.py
import multiprocessing

# 绑定地址
bind = "0.0.0.0:8000"

# Worker进程数(CPU核心数 × 2 + 1)
workers = multiprocessing.cpu_count() * 2 + 1

# Worker类(使用Uvicorn)
worker_class = "uvicorn.workers.UvicornWorker"

# 日志
accesslog = "/var/log/asr-service/access.log"
errorlog = "/var/log/asr-service/error.log"
loglevel = "info"

# 超时
timeout = 120
keepalive = 5

# 启动命令
# gunicorn -c gunicorn_config.py main:app

4.2 性能测试

python 复制代码
"""
性能测试脚本
测试并发连接和吞吐量
"""

import asyncio
import websockets
import time
import numpy as np

async def test_single_connection():
    """测试单个连接"""
    uri = "ws://localhost:8000/ws/asr"
    
    async with websockets.connect(uri) as ws:
        # 发送100个音频包
        start_time = time.time()
        
        for i in range(100):
            # 生成100ms音频
            audio = np.random.randint(-32768, 32767, 1600, dtype=np.int16)
            await ws.send(audio.tobytes())
            
            # 接收结果
            result = await ws.recv()
        
        elapsed = time.time() - start_time
        print(f"单连接性能: {100/elapsed:.2f} 包/秒")

async def test_concurrent_connections(num_connections=100):
    """测试并发连接"""
    tasks = [test_single_connection() for _ in range(num_connections)]
    
    start_time = time.time()
    await asyncio.gather(*tasks)
    elapsed = time.time() - start_time
    
    print(f"并发测试: {num_connections}个连接, 耗时{elapsed:.2f}秒")

if __name__ == "__main__":
    # 单连接测试
    asyncio.run(test_single_connection())
    
    # 并发测试
    asyncio.run(test_concurrent_connections(100))

五、生产环境部署

5.1 Nginx反向代理

nginx 复制代码
# /etc/nginx/sites-available/asr-service

upstream asr_backend {
    # 多个worker实例
    server 127.0.0.1:8000 weight=1;
    server 127.0.0.1:8001 weight=1;
    server 127.0.0.1:8002 weight=1;
}

server {
    listen 443 ssl http2;
    server_name asr.hospital.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    # WebSocket路径
    location /ws/ {
        proxy_pass http://asr_backend;
        
        # WebSocket必需配置
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # 超时配置
        proxy_connect_timeout 7d;
        proxy_send_timeout 7d;
        proxy_read_timeout 7d;
        
        # 其他header
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # HTTP API路径
    location / {
        proxy_pass http://asr_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

5.2 Systemd服务

ini 复制代码
# /etc/systemd/system/asr-service.service

[Unit]
Description=ASR WebSocket Service
After=network.target

[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/opt/asr-service
Environment="PATH=/opt/asr-service/venv/bin"
ExecStart=/opt/asr-service/venv/bin/gunicorn -c gunicorn_config.py main:app
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

六、总结

推荐方案

场景 推荐框架 理由
全功能Web应用 FastAPI ⭐⭐⭐⭐⭐ 现代化、高性能、易用
纯WebSocket服务 websockets ⭐⭐⭐⭐ 轻量、简单
已有Flask项目 Flask-SocketIO ⭐⭐⭐ 集成方便
已有Django项目 Django Channels ⭐⭐⭐ Django生态

性能指标

复制代码
FastAPI + Uvicorn:
- 并发连接: 10,000+ (单进程)
- 延迟: <50ms (不含ASR)
- 吞吐量: 5000+ 消息/秒

适合医院场景:
- 100个医生同时录音 ✅
- 实时响应 ✅
- 易于维护 ✅

Python实现WebSocket完全可行,而且非常成熟! 🎉

相关推荐
JavaEdge.1 小时前
07-LangChain Toolkit 实战:从工具函数到 Python Agent,再到 SQL Agent
python·sql·langchain
listhi5201 小时前
基于NSGA-II的多目标整数规划MATLAB实现
开发语言·matlab
2401_853087881 小时前
Confluence 替代落地复盘:存量数据迁移、权限重构、信创适配踩坑总结
开发语言·重构·c#
无聊的老谢1 小时前
编译期即正义:利用 Java Lambda 构建类型安全的 SQL 表达式引擎
java·开发语言
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_64:从 object 到 iframe 的嵌入技术全面解析
开发语言·前端·javascript·ui·html·音视频
小小de风呀1 小时前
de风——【从零开始学C++】(八):string的模拟实现
开发语言·c++
运维行者_1 小时前
ITOps自动化:全面解析
java·服务器·开发语言·网络·云计算
Chase_______1 小时前
【Java杂项】为什么 b += 1 可以,但 b = b + 1 会报错?类型提升与复合赋值详解
java·开发语言·python
Wiktok1 小时前
【Wit智慧引擎】亲测可用国内pytorch镜像
人工智能·pytorch·python