Qwen3-ASR-0.6B与Node.js集成实时语音转文字服务想象一下你正在开发一个在线会议应用或者一个智能客服系统用户对着麦克风说话屏幕上几乎实时地就出现了他说的话。这种体验是不是很酷以前要实现这样的功能要么得用昂贵的商业API要么就得自己折腾复杂的语音识别模型部署和维护都是大问题。现在情况不一样了。阿里开源的Qwen3-ASR-0.6B模型让这件事变得简单多了。这个模型只有6亿参数但能力一点都不弱支持52种语言和方言识别准确率很高关键是速度特别快。官方数据显示在128个并发请求的情况下它每秒能处理2000秒的音频相当于10秒钟就能把5个小时的录音转成文字。更棒的是它原生支持流式推理。这意味着你不用等用户说完一整段话可以一边录音一边识别实现真正的实时字幕效果。今天我就来分享怎么用Node.js把这个强大的语音识别模型变成一个可用的微服务让你在自己的项目里也能轻松用上这个能力。1. 为什么选择Qwen3-ASR-0.6B在开始动手之前我们先看看为什么Qwen3-ASR-0.6B是个不错的选择。市面上语音识别方案不少有开源的Whisper也有各种商业API但Qwen3-ASR-0.6B有几个明显的优势。首先是性能表现。虽然它只有0.6B参数是个轻量级模型但在多项测试中表现都很不错。特别是在中文识别上它的准确率比很多更大的模型还要好。对于国内开发者来说这点特别重要毕竟我们的应用大部分用户都是说中文的。其次是效率。这个模型的设计目标就是在保证准确率的前提下尽可能提高推理速度。它支持vLLM推理框架这意味着你可以用更少的GPU资源服务更多的并发请求。对于中小型项目来说这个特性能帮你省下不少硬件成本。再就是功能全面。它不仅能识别普通话还支持22种中文方言比如广东话、四川话、东北话等等。如果你的用户来自不同地区这个功能就很有用了。另外它还能识别唱歌音频甚至带背景音乐的歌曲这在一些娱乐类应用里会是个亮点。最后是开源和易用性。模型完全开源你可以自己部署不用担心API调用次数限制或者费用问题。而且官方提供了完整的推理框架支持流式推理、批量推理、时间戳预测等多种功能集成起来比较方便。2. 环境准备与模型部署要搭建这个服务我们需要准备两方面的环境模型推理服务和Node.js后端服务。模型推理服务负责实际的语音识别计算Node.js服务则负责接收音频流、调用模型、返回结果。2.1 安装Node.js和必要工具首先确保你的系统已经安装了Node.js。我建议用Node.js 18或更高版本因为我们需要用到一些比较新的特性。你可以用下面的命令检查版本node --version如果还没安装可以去Node.js官网下载安装包或者用nvm这样的版本管理工具来安装。接下来创建一个项目目录并初始化Node.js项目mkdir qwen3-asr-service cd qwen3-asr-service npm init -y然后安装我们需要的依赖包npm install express ws multer axios npm install --save-dev nodemon这里简单说明一下这些包的作用express用来创建Web服务器wsWebSocket库实现实时通信multer处理文件上传axios发送HTTP请求nodemon开发时自动重启服务2.2 部署Qwen3-ASR推理服务模型推理部分官方推荐用vLLM来部署这样能获得最好的性能。如果你有GPU环境可以按照下面的步骤来部署。首先安装vLLM和相关的依赖# 创建Python虚拟环境 python -m venv venv source venv/bin/activate # Linux/Mac # 或者 venv\Scripts\activate # Windows # 安装vLLM需要CUDA环境 pip install vllm pip install vllm[audio]然后启动vLLM服务vllm serve Qwen/Qwen3-ASR-0.6B \ --gpu-memory-utilization 0.8 \ --host 0.0.0.0 \ --port 8000这个命令会启动一个推理服务监听8000端口。--gpu-memory-utilization 0.8表示使用80%的GPU显存你可以根据实际情况调整。如果你没有GPU或者想先快速体验一下也可以用CPU模式不过速度会慢很多vllm serve Qwen/Qwen3-ASR-0.6B \ --device cpu \ --host 0.0.0.0 \ --port 8000服务启动后你可以用curl测试一下curl http://localhost:8000/v1/models如果返回模型信息说明服务启动成功了。3. 构建Node.js WebSocket服务现在模型推理服务已经跑起来了接下来我们要构建一个Node.js服务它负责接收客户端的音频数据然后调用模型服务进行识别再把结果实时返回给客户端。3.1 创建基础WebSocket服务我们先创建一个简单的WebSocket服务器让客户端可以连接上来// server.js const WebSocket require(ws); const express require(express); const http require(http); const app express(); const server http.createServer(app); const wss new WebSocket.Server({ server }); // 存储所有连接的客户端 const clients new Map(); wss.on(connection, (ws, req) { const clientId Date.now().toString(); clients.set(clientId, ws); console.log(新客户端连接: ${clientId}); ws.on(message, async (message) { try { // 这里处理音频数据 console.log(收到来自 ${clientId} 的消息); } catch (error) { console.error(处理消息时出错:, error); ws.send(JSON.stringify({ error: error.message })); } }); ws.on(close, () { console.log(客户端断开连接: ${clientId}); clients.delete(clientId); }); ws.on(error, (error) { console.error(客户端 ${clientId} 错误:, error); clients.delete(clientId); }); // 发送欢迎消息 ws.send(JSON.stringify({ type: connected, clientId, message: 连接成功可以开始发送音频数据 })); }); const PORT 3000; server.listen(PORT, () { console.log(WebSocket服务运行在 ws://localhost:${PORT}); console.log(HTTP服务运行在 http://localhost:${PORT}); });这个服务做了几件事创建了一个WebSocket服务器监听3000端口当客户端连接时给它分配一个唯一的ID存储所有连接的客户端方便管理设置了消息处理、连接关闭和错误处理的逻辑3.2 实现音频数据处理逻辑接下来我们要处理客户端发送的音频数据。客户端可能会发送两种格式的数据原始的音频二进制数据或者Base64编码的字符串。我们需要根据实际情况来处理。// 在server.js中添加音频处理函数 const axios require(axios); // 配置模型服务地址 const MODEL_API_URL http://localhost:8000/v1/audio/transcriptions; async function transcribeAudio(audioData, language null) { try { const formData new FormData(); // 将音频数据转换为Buffer const audioBuffer Buffer.from(audioData); // 创建Blob对象在Node.js中模拟 const blob { arrayBuffer: () Promise.resolve(audioBuffer), size: audioBuffer.length, type: audio/wav }; // 构建请求参数 const requestData { model: Qwen/Qwen3-ASR-0.6B, file: blob, response_format: json }; if (language) { requestData.language language; } // 发送请求到模型服务 const response await axios.post(MODEL_API_URL, requestData, { headers: { Content-Type: multipart/form-data }, timeout: 30000 // 30秒超时 }); return response.data; } catch (error) { console.error(语音识别失败:, error.message); throw error; } }这个函数负责调用模型服务进行语音识别。它接收音频数据和可选的语言参数然后发送到我们之前启动的vLLM服务。3.3 完善WebSocket消息处理现在我们把音频处理函数集成到WebSocket消息处理中// 修改ws.on(message)部分 ws.on(message, async (message) { try { const data JSON.parse(message); if (data.type audio) { // 处理音频数据 const { audio, language, requestId } data; // 发送处理中的状态 ws.send(JSON.stringify({ type: processing, requestId, message: 正在处理音频... })); // 进行语音识别 const result await transcribeAudio(audio, language); // 发送识别结果 ws.send(JSON.stringify({ type: transcription, requestId, text: result.text, language: result.language, duration: result.duration })); } else if (data.type ping) { // 心跳检测 ws.send(JSON.stringify({ type: pong })); } else { ws.send(JSON.stringify({ type: error, message: 不支持的消息类型 })); } } catch (error) { console.error(处理消息时出错:, error); ws.send(JSON.stringify({ type: error, message: error.message })); } });这样当客户端发送音频数据时服务端就会调用模型进行识别然后把结果返回给客户端。4. 实现流式语音识别前面的实现是每次接收完整的音频数据然后识别但真正的实时应用需要流式识别也就是一边录音一边识别。Qwen3-ASR支持流式推理我们可以在服务端实现这个功能。4.1 客户端流式发送音频首先客户端需要以流式的方式发送音频数据。我们可以让客户端每采集到一小段音频就发送一次// 客户端示例代码前端 class AudioStreamer { constructor(wsUrl) { this.ws new WebSocket(wsUrl); this.audioContext null; this.mediaStream null; this.processor null; this.isRecording false; this.requestId Date.now().toString(); this.setupWebSocket(); } setupWebSocket() { this.ws.onopen () { console.log(WebSocket连接已建立); }; this.ws.onmessage (event) { const data JSON.parse(event.data); this.handleServerMessage(data); }; this.ws.onerror (error) { console.error(WebSocket错误:, error); }; } async startRecording() { try { // 获取麦克风权限 this.mediaStream await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true } }); this.audioContext new AudioContext({ sampleRate: 16000 }); const source this.audioContext.createMediaStreamSource(this.mediaStream); // 创建音频处理器 this.processor this.audioContext.createScriptProcessor(4096, 1, 1); source.connect(this.processor); this.processor.connect(this.audioContext.destination); this.processor.onaudioprocess (event) { if (!this.isRecording) return; const audioData event.inputBuffer.getChannelData(0); const int16Array this.floatTo16BitPCM(audioData); // 发送音频数据到服务器 this.ws.send(JSON.stringify({ type: audio_chunk, requestId: this.requestId, audio: Array.from(int16Array), isFinal: false })); }; this.isRecording true; console.log(开始录音); } catch (error) { console.error(启动录音失败:, error); } } stopRecording() { this.isRecording false; if (this.processor) { this.processor.disconnect(); this.processor null; } if (this.audioContext) { this.audioContext.close(); this.audioContext null; } if (this.mediaStream) { this.mediaStream.getTracks().forEach(track track.stop()); this.mediaStream null; } // 发送结束标记 this.ws.send(JSON.stringify({ type: audio_chunk, requestId: this.requestId, audio: [], isFinal: true })); console.log(停止录音); } floatTo16BitPCM(float32Array) { const int16Array new Int16Array(float32Array.length); for (let i 0; i float32Array.length; i) { const s Math.max(-1, Math.min(1, float32Array[i])); int16Array[i] s 0 ? s * 0x8000 : s * 0x7FFF; } return int16Array; } handleServerMessage(data) { switch (data.type) { case transcription_chunk: console.log(识别结果:, data.text); // 更新UI显示 break; case transcription_final: console.log(最终结果:, data.text); break; case error: console.error(服务器错误:, data.message); break; } } }4.2 服务端流式处理服务端需要累积音频数据然后定期发送给模型进行识别// 在server.js中添加流式处理逻辑 const audioBuffers new Map(); // 修改消息处理逻辑 ws.on(message, async (message) { try { const data JSON.parse(message); if (data.type audio_chunk) { const { requestId, audio, isFinal, language } data; // 初始化或获取音频缓冲区 if (!audioBuffers.has(requestId)) { audioBuffers.set(requestId, { buffers: [], startTime: Date.now(), language: language || null }); } const bufferInfo audioBuffers.get(requestId); // 添加音频数据到缓冲区 if (audio.length 0) { bufferInfo.buffers.push(...audio); } // 如果缓冲区有足够的数据或者收到结束标记就进行识别 const shouldProcess bufferInfo.buffers.length 16000 * 2 || isFinal; if (shouldProcess bufferInfo.buffers.length 0) { // 提取要处理的数据 const processData bufferInfo.buffers.slice(0, 16000 * 2); // 1秒数据 bufferInfo.buffers bufferInfo.buffers.slice(processData.length); // 转换为Int16Array const int16Array Int16Array.from(processData); // 转换为WAV格式 const wavBuffer this.createWavBuffer(int16Array, 16000); // 发送到模型服务 const result await transcribeAudio(wavBuffer, bufferInfo.language); // 发送识别结果 ws.send(JSON.stringify({ type: transcription_chunk, requestId, text: result.text, isPartial: !isFinal })); } // 如果是最终块清理缓冲区 if (isFinal) { audioBuffers.delete(requestId); ws.send(JSON.stringify({ type: transcription_final, requestId, message: 识别完成 })); } } else if (data.type ping) { ws.send(JSON.stringify({ type: pong })); } } catch (error) { console.error(处理消息时出错:, error); ws.send(JSON.stringify({ type: error, message: error.message })); } }); // 创建WAV格式的音频缓冲区 function createWavBuffer(audioData, sampleRate) { const buffer new ArrayBuffer(44 audioData.length * 2); const view new DataView(buffer); // WAV头部 writeString(view, 0, RIFF); view.setUint32(4, 36 audioData.length * 2, true); writeString(view, 8, WAVE); writeString(view, 12, fmt ); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); writeString(view, 36, data); view.setUint32(40, audioData.length * 2, true); // 音频数据 let offset 44; for (let i 0; i audioData.length; i, offset 2) { view.setInt16(offset, audioData[i], true); } return buffer; } function writeString(view, offset, string) { for (let i 0; i string.length; i) { view.setUint8(offset i, string.charCodeAt(i)); } }这样我们就实现了一个基本的流式语音识别服务。客户端一边录音一边发送数据服务端累积数据并定期识别然后把结果实时返回。5. 添加HTTP API接口除了WebSocket接口我们还可以提供HTTP API方便一些不需要实时性的场景使用比如上传完整的音频文件进行识别。5.1 创建文件上传接口// 在server.js中添加HTTP路由 const multer require(multer); const upload multer({ storage: multer.memoryStorage() }); // 文件上传接口 app.post(/api/transcribe, upload.single(audio), async (req, res) { try { if (!req.file) { return res.status(400).json({ error: 请上传音频文件 }); } const { language } req.body; const audioBuffer req.file.buffer; console.log(收到文件上传: ${req.file.originalname}, 大小: ${audioBuffer.length}字节); // 发送识别请求 const result await transcribeAudio(audioBuffer, language); res.json({ success: true, text: result.text, language: result.language, duration: result.duration }); } catch (error) { console.error(文件识别失败:, error); res.status(500).json({ success: false, error: error.message }); } }); // 健康检查接口 app.get(/api/health, (req, res) { res.json({ status: healthy, timestamp: new Date().toISOString(), model: Qwen3-ASR-0.6B }); }); // 获取支持的语言列表 app.get(/api/languages, (req, res) { res.json({ languages: [ { code: zh, name: 中文 }, { code: en, name: 英文 }, { code: yue, name: 粤语 }, { code: ja, name: 日语 }, { code: ko, name: 韩语 }, // ... 其他支持的语言 ] }); });5.2 添加批量处理接口对于需要处理大量音频文件的场景我们可以提供批量处理接口// 批量处理接口 app.post(/api/transcribe/batch, upload.array(audio, 10), async (req, res) { try { if (!req.files || req.files.length 0) { return res.status(400).json({ error: 请上传音频文件 }); } const { language } req.body; const results []; console.log(收到批量处理请求: ${req.files.length}个文件); // 并行处理所有文件 const promises req.files.map(async (file, index) { try { const result await transcribeAudio(file.buffer, language); return { filename: file.originalname, success: true, text: result.text, language: result.language }; } catch (error) { return { filename: file.originalname, success: false, error: error.message }; } }); const batchResults await Promise.all(promises); res.json({ success: true, total: req.files.length, processed: batchResults.filter(r r.success).length, failed: batchResults.filter(r !r.success).length, results: batchResults }); } catch (error) { console.error(批量处理失败:, error); res.status(500).json({ success: false, error: error.message }); } });6. 性能优化和错误处理在实际使用中我们还需要考虑性能优化和错误处理确保服务的稳定性和可靠性。6.1 添加请求限流为了防止服务被滥用我们可以添加请求限流const rateLimit require(express-rate-limit); // WebSocket连接限流 const wsConnections new Map(); // HTTP API限流 const apiLimiter rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 每个IP最多100次请求 message: 请求过于频繁请稍后再试 }); app.use(/api/, apiLimiter); // WebSocket连接数限制 wss.on(connection, (ws, req) { const clientIp req.socket.remoteAddress; const connections wsConnections.get(clientIp) || 0; if (connections 5) { ws.close(1008, 连接数超过限制); return; } wsConnections.set(clientIp, connections 1); ws.on(close, () { const current wsConnections.get(clientIp) || 0; if (current 0) { wsConnections.set(clientIp, current - 1); } }); // ... 原有的连接处理逻辑 });6.2 添加重试机制网络请求可能会失败我们可以添加重试机制async function transcribeAudioWithRetry(audioData, language null, maxRetries 3) { let lastError; for (let attempt 1; attempt maxRetries; attempt) { try { return await transcribeAudio(audioData, language); } catch (error) { lastError error; console.log(识别失败第${attempt}次重试:, error.message); if (attempt maxRetries) { // 等待一段时间再重试 await new Promise(resolve setTimeout(resolve, 1000 * attempt)); } } } throw lastError; }6.3 添加监控和日志为了更好地监控服务运行状态我们可以添加详细的日志// 请求日志中间件 app.use((req, res, next) { const startTime Date.now(); res.on(finish, () { const duration Date.now() - startTime; console.log(${new Date().toISOString()} ${req.method} ${req.url} ${res.statusCode} ${duration}ms); }); next(); }); // WebSocket消息日志 function logWebSocketMessage(clientId, type, data) { console.log(${new Date().toISOString()} [WS:${clientId}] ${type}:, typeof data object ? JSON.stringify(data).substring(0, 100) : data); } // 在消息处理中添加日志 ws.on(message, async (message) { logWebSocketMessage(clientId, received, message); // ... 处理逻辑 });7. 部署和配置建议最后我们来谈谈如何部署这个服务以及一些配置建议。7.1 Docker部署为了方便部署我们可以创建Docker镜像# Dockerfile FROM node:18-alpine WORKDIR /app # 复制package文件 COPY package*.json ./ # 安装依赖 RUN npm install --production # 复制源代码 COPY . . # 创建非root用户 RUN addgroup -g 1001 -S nodejs \ adduser -S nodejs -u 1001 USER nodejs # 暴露端口 EXPOSE 3000 # 启动命令 CMD [node, server.js]然后创建docker-compose.yml文件把Node.js服务和模型服务一起部署version: 3.8 services: asr-model: image: qwen3-asr:latest build: context: ./model-service ports: - 8000:8000 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] environment: - CUDA_VISIBLE_DEVICES0 asr-api: build: . ports: - 3000:3000 depends_on: - asr-model environment: - MODEL_API_URLhttp://asr-model:8000/v1/audio/transcriptions - NODE_ENVproduction - PORT30007.2 性能调优建议根据实际使用情况你可能需要调整一些参数音频分块大小流式识别时分块大小影响延迟和准确率的平衡。太小会增加请求次数太大会增加延迟。一般1-2秒比较合适。并发连接数根据服务器配置调整最大并发连接数。Qwen3-ASR-0.6B在128并发下性能最好但你的Node.js服务可能需要在更早的时候限流。缓冲区管理及时清理不再使用的音频缓冲区避免内存泄漏。GPU内存使用调整vLLM的--gpu-memory-utilization参数根据你的GPU显存大小来设置。7.3 安全建议启用HTTPS生产环境一定要用HTTPS保护音频数据的安全。身份验证添加API密钥或JWT token验证。输入验证验证客户端发送的音频数据格式和大小。错误信息不要向客户端返回详细的内部错误信息。8. 总结整体用下来Qwen3-ASR-0.6B确实是个不错的语音识别选择特别是对于中文场景识别准确率让人满意。结合Node.js来构建服务整个过程还算顺畅没有遇到特别棘手的问题。这套方案在实际项目里应该能解决不少问题。比如在线教育平台的实时字幕、会议系统的语音转写、客服系统的对话记录等等。性能方面只要硬件配置跟得上处理一般的并发量应该没问题。当然也有些需要注意的地方。模型服务对GPU有要求如果没有合适的显卡用CPU模式速度会慢很多。另外虽然Qwen3-ASR支持很多语言但对于一些小语种或者特别专业的领域术语可能还需要针对性地优化。如果你打算在实际项目中使用建议先小规模测试看看在具体场景下的表现如何。可以尝试调整音频采样率、分块大小这些参数找到最适合你需求的配置。另外关注一下模型的更新开源社区经常会有优化和改进。总的来说用开源模型自建语音识别服务相比依赖商业API给了我们更多的控制权和灵活性。虽然需要自己维护但长期来看可能更经济也更符合数据隐私的要求。希望这个实践分享对你有帮助如果有具体问题欢迎继续交流。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
Qwen3-ASR-0.6B与Node.js集成:实时语音转文字服务
发布时间:2026/5/25 4:16:29
Qwen3-ASR-0.6B与Node.js集成实时语音转文字服务想象一下你正在开发一个在线会议应用或者一个智能客服系统用户对着麦克风说话屏幕上几乎实时地就出现了他说的话。这种体验是不是很酷以前要实现这样的功能要么得用昂贵的商业API要么就得自己折腾复杂的语音识别模型部署和维护都是大问题。现在情况不一样了。阿里开源的Qwen3-ASR-0.6B模型让这件事变得简单多了。这个模型只有6亿参数但能力一点都不弱支持52种语言和方言识别准确率很高关键是速度特别快。官方数据显示在128个并发请求的情况下它每秒能处理2000秒的音频相当于10秒钟就能把5个小时的录音转成文字。更棒的是它原生支持流式推理。这意味着你不用等用户说完一整段话可以一边录音一边识别实现真正的实时字幕效果。今天我就来分享怎么用Node.js把这个强大的语音识别模型变成一个可用的微服务让你在自己的项目里也能轻松用上这个能力。1. 为什么选择Qwen3-ASR-0.6B在开始动手之前我们先看看为什么Qwen3-ASR-0.6B是个不错的选择。市面上语音识别方案不少有开源的Whisper也有各种商业API但Qwen3-ASR-0.6B有几个明显的优势。首先是性能表现。虽然它只有0.6B参数是个轻量级模型但在多项测试中表现都很不错。特别是在中文识别上它的准确率比很多更大的模型还要好。对于国内开发者来说这点特别重要毕竟我们的应用大部分用户都是说中文的。其次是效率。这个模型的设计目标就是在保证准确率的前提下尽可能提高推理速度。它支持vLLM推理框架这意味着你可以用更少的GPU资源服务更多的并发请求。对于中小型项目来说这个特性能帮你省下不少硬件成本。再就是功能全面。它不仅能识别普通话还支持22种中文方言比如广东话、四川话、东北话等等。如果你的用户来自不同地区这个功能就很有用了。另外它还能识别唱歌音频甚至带背景音乐的歌曲这在一些娱乐类应用里会是个亮点。最后是开源和易用性。模型完全开源你可以自己部署不用担心API调用次数限制或者费用问题。而且官方提供了完整的推理框架支持流式推理、批量推理、时间戳预测等多种功能集成起来比较方便。2. 环境准备与模型部署要搭建这个服务我们需要准备两方面的环境模型推理服务和Node.js后端服务。模型推理服务负责实际的语音识别计算Node.js服务则负责接收音频流、调用模型、返回结果。2.1 安装Node.js和必要工具首先确保你的系统已经安装了Node.js。我建议用Node.js 18或更高版本因为我们需要用到一些比较新的特性。你可以用下面的命令检查版本node --version如果还没安装可以去Node.js官网下载安装包或者用nvm这样的版本管理工具来安装。接下来创建一个项目目录并初始化Node.js项目mkdir qwen3-asr-service cd qwen3-asr-service npm init -y然后安装我们需要的依赖包npm install express ws multer axios npm install --save-dev nodemon这里简单说明一下这些包的作用express用来创建Web服务器wsWebSocket库实现实时通信multer处理文件上传axios发送HTTP请求nodemon开发时自动重启服务2.2 部署Qwen3-ASR推理服务模型推理部分官方推荐用vLLM来部署这样能获得最好的性能。如果你有GPU环境可以按照下面的步骤来部署。首先安装vLLM和相关的依赖# 创建Python虚拟环境 python -m venv venv source venv/bin/activate # Linux/Mac # 或者 venv\Scripts\activate # Windows # 安装vLLM需要CUDA环境 pip install vllm pip install vllm[audio]然后启动vLLM服务vllm serve Qwen/Qwen3-ASR-0.6B \ --gpu-memory-utilization 0.8 \ --host 0.0.0.0 \ --port 8000这个命令会启动一个推理服务监听8000端口。--gpu-memory-utilization 0.8表示使用80%的GPU显存你可以根据实际情况调整。如果你没有GPU或者想先快速体验一下也可以用CPU模式不过速度会慢很多vllm serve Qwen/Qwen3-ASR-0.6B \ --device cpu \ --host 0.0.0.0 \ --port 8000服务启动后你可以用curl测试一下curl http://localhost:8000/v1/models如果返回模型信息说明服务启动成功了。3. 构建Node.js WebSocket服务现在模型推理服务已经跑起来了接下来我们要构建一个Node.js服务它负责接收客户端的音频数据然后调用模型服务进行识别再把结果实时返回给客户端。3.1 创建基础WebSocket服务我们先创建一个简单的WebSocket服务器让客户端可以连接上来// server.js const WebSocket require(ws); const express require(express); const http require(http); const app express(); const server http.createServer(app); const wss new WebSocket.Server({ server }); // 存储所有连接的客户端 const clients new Map(); wss.on(connection, (ws, req) { const clientId Date.now().toString(); clients.set(clientId, ws); console.log(新客户端连接: ${clientId}); ws.on(message, async (message) { try { // 这里处理音频数据 console.log(收到来自 ${clientId} 的消息); } catch (error) { console.error(处理消息时出错:, error); ws.send(JSON.stringify({ error: error.message })); } }); ws.on(close, () { console.log(客户端断开连接: ${clientId}); clients.delete(clientId); }); ws.on(error, (error) { console.error(客户端 ${clientId} 错误:, error); clients.delete(clientId); }); // 发送欢迎消息 ws.send(JSON.stringify({ type: connected, clientId, message: 连接成功可以开始发送音频数据 })); }); const PORT 3000; server.listen(PORT, () { console.log(WebSocket服务运行在 ws://localhost:${PORT}); console.log(HTTP服务运行在 http://localhost:${PORT}); });这个服务做了几件事创建了一个WebSocket服务器监听3000端口当客户端连接时给它分配一个唯一的ID存储所有连接的客户端方便管理设置了消息处理、连接关闭和错误处理的逻辑3.2 实现音频数据处理逻辑接下来我们要处理客户端发送的音频数据。客户端可能会发送两种格式的数据原始的音频二进制数据或者Base64编码的字符串。我们需要根据实际情况来处理。// 在server.js中添加音频处理函数 const axios require(axios); // 配置模型服务地址 const MODEL_API_URL http://localhost:8000/v1/audio/transcriptions; async function transcribeAudio(audioData, language null) { try { const formData new FormData(); // 将音频数据转换为Buffer const audioBuffer Buffer.from(audioData); // 创建Blob对象在Node.js中模拟 const blob { arrayBuffer: () Promise.resolve(audioBuffer), size: audioBuffer.length, type: audio/wav }; // 构建请求参数 const requestData { model: Qwen/Qwen3-ASR-0.6B, file: blob, response_format: json }; if (language) { requestData.language language; } // 发送请求到模型服务 const response await axios.post(MODEL_API_URL, requestData, { headers: { Content-Type: multipart/form-data }, timeout: 30000 // 30秒超时 }); return response.data; } catch (error) { console.error(语音识别失败:, error.message); throw error; } }这个函数负责调用模型服务进行语音识别。它接收音频数据和可选的语言参数然后发送到我们之前启动的vLLM服务。3.3 完善WebSocket消息处理现在我们把音频处理函数集成到WebSocket消息处理中// 修改ws.on(message)部分 ws.on(message, async (message) { try { const data JSON.parse(message); if (data.type audio) { // 处理音频数据 const { audio, language, requestId } data; // 发送处理中的状态 ws.send(JSON.stringify({ type: processing, requestId, message: 正在处理音频... })); // 进行语音识别 const result await transcribeAudio(audio, language); // 发送识别结果 ws.send(JSON.stringify({ type: transcription, requestId, text: result.text, language: result.language, duration: result.duration })); } else if (data.type ping) { // 心跳检测 ws.send(JSON.stringify({ type: pong })); } else { ws.send(JSON.stringify({ type: error, message: 不支持的消息类型 })); } } catch (error) { console.error(处理消息时出错:, error); ws.send(JSON.stringify({ type: error, message: error.message })); } });这样当客户端发送音频数据时服务端就会调用模型进行识别然后把结果返回给客户端。4. 实现流式语音识别前面的实现是每次接收完整的音频数据然后识别但真正的实时应用需要流式识别也就是一边录音一边识别。Qwen3-ASR支持流式推理我们可以在服务端实现这个功能。4.1 客户端流式发送音频首先客户端需要以流式的方式发送音频数据。我们可以让客户端每采集到一小段音频就发送一次// 客户端示例代码前端 class AudioStreamer { constructor(wsUrl) { this.ws new WebSocket(wsUrl); this.audioContext null; this.mediaStream null; this.processor null; this.isRecording false; this.requestId Date.now().toString(); this.setupWebSocket(); } setupWebSocket() { this.ws.onopen () { console.log(WebSocket连接已建立); }; this.ws.onmessage (event) { const data JSON.parse(event.data); this.handleServerMessage(data); }; this.ws.onerror (error) { console.error(WebSocket错误:, error); }; } async startRecording() { try { // 获取麦克风权限 this.mediaStream await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, sampleRate: 16000, echoCancellation: true, noiseSuppression: true } }); this.audioContext new AudioContext({ sampleRate: 16000 }); const source this.audioContext.createMediaStreamSource(this.mediaStream); // 创建音频处理器 this.processor this.audioContext.createScriptProcessor(4096, 1, 1); source.connect(this.processor); this.processor.connect(this.audioContext.destination); this.processor.onaudioprocess (event) { if (!this.isRecording) return; const audioData event.inputBuffer.getChannelData(0); const int16Array this.floatTo16BitPCM(audioData); // 发送音频数据到服务器 this.ws.send(JSON.stringify({ type: audio_chunk, requestId: this.requestId, audio: Array.from(int16Array), isFinal: false })); }; this.isRecording true; console.log(开始录音); } catch (error) { console.error(启动录音失败:, error); } } stopRecording() { this.isRecording false; if (this.processor) { this.processor.disconnect(); this.processor null; } if (this.audioContext) { this.audioContext.close(); this.audioContext null; } if (this.mediaStream) { this.mediaStream.getTracks().forEach(track track.stop()); this.mediaStream null; } // 发送结束标记 this.ws.send(JSON.stringify({ type: audio_chunk, requestId: this.requestId, audio: [], isFinal: true })); console.log(停止录音); } floatTo16BitPCM(float32Array) { const int16Array new Int16Array(float32Array.length); for (let i 0; i float32Array.length; i) { const s Math.max(-1, Math.min(1, float32Array[i])); int16Array[i] s 0 ? s * 0x8000 : s * 0x7FFF; } return int16Array; } handleServerMessage(data) { switch (data.type) { case transcription_chunk: console.log(识别结果:, data.text); // 更新UI显示 break; case transcription_final: console.log(最终结果:, data.text); break; case error: console.error(服务器错误:, data.message); break; } } }4.2 服务端流式处理服务端需要累积音频数据然后定期发送给模型进行识别// 在server.js中添加流式处理逻辑 const audioBuffers new Map(); // 修改消息处理逻辑 ws.on(message, async (message) { try { const data JSON.parse(message); if (data.type audio_chunk) { const { requestId, audio, isFinal, language } data; // 初始化或获取音频缓冲区 if (!audioBuffers.has(requestId)) { audioBuffers.set(requestId, { buffers: [], startTime: Date.now(), language: language || null }); } const bufferInfo audioBuffers.get(requestId); // 添加音频数据到缓冲区 if (audio.length 0) { bufferInfo.buffers.push(...audio); } // 如果缓冲区有足够的数据或者收到结束标记就进行识别 const shouldProcess bufferInfo.buffers.length 16000 * 2 || isFinal; if (shouldProcess bufferInfo.buffers.length 0) { // 提取要处理的数据 const processData bufferInfo.buffers.slice(0, 16000 * 2); // 1秒数据 bufferInfo.buffers bufferInfo.buffers.slice(processData.length); // 转换为Int16Array const int16Array Int16Array.from(processData); // 转换为WAV格式 const wavBuffer this.createWavBuffer(int16Array, 16000); // 发送到模型服务 const result await transcribeAudio(wavBuffer, bufferInfo.language); // 发送识别结果 ws.send(JSON.stringify({ type: transcription_chunk, requestId, text: result.text, isPartial: !isFinal })); } // 如果是最终块清理缓冲区 if (isFinal) { audioBuffers.delete(requestId); ws.send(JSON.stringify({ type: transcription_final, requestId, message: 识别完成 })); } } else if (data.type ping) { ws.send(JSON.stringify({ type: pong })); } } catch (error) { console.error(处理消息时出错:, error); ws.send(JSON.stringify({ type: error, message: error.message })); } }); // 创建WAV格式的音频缓冲区 function createWavBuffer(audioData, sampleRate) { const buffer new ArrayBuffer(44 audioData.length * 2); const view new DataView(buffer); // WAV头部 writeString(view, 0, RIFF); view.setUint32(4, 36 audioData.length * 2, true); writeString(view, 8, WAVE); writeString(view, 12, fmt ); view.setUint32(16, 16, true); view.setUint16(20, 1, true); view.setUint16(22, 1, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 2, true); view.setUint16(32, 2, true); view.setUint16(34, 16, true); writeString(view, 36, data); view.setUint32(40, audioData.length * 2, true); // 音频数据 let offset 44; for (let i 0; i audioData.length; i, offset 2) { view.setInt16(offset, audioData[i], true); } return buffer; } function writeString(view, offset, string) { for (let i 0; i string.length; i) { view.setUint8(offset i, string.charCodeAt(i)); } }这样我们就实现了一个基本的流式语音识别服务。客户端一边录音一边发送数据服务端累积数据并定期识别然后把结果实时返回。5. 添加HTTP API接口除了WebSocket接口我们还可以提供HTTP API方便一些不需要实时性的场景使用比如上传完整的音频文件进行识别。5.1 创建文件上传接口// 在server.js中添加HTTP路由 const multer require(multer); const upload multer({ storage: multer.memoryStorage() }); // 文件上传接口 app.post(/api/transcribe, upload.single(audio), async (req, res) { try { if (!req.file) { return res.status(400).json({ error: 请上传音频文件 }); } const { language } req.body; const audioBuffer req.file.buffer; console.log(收到文件上传: ${req.file.originalname}, 大小: ${audioBuffer.length}字节); // 发送识别请求 const result await transcribeAudio(audioBuffer, language); res.json({ success: true, text: result.text, language: result.language, duration: result.duration }); } catch (error) { console.error(文件识别失败:, error); res.status(500).json({ success: false, error: error.message }); } }); // 健康检查接口 app.get(/api/health, (req, res) { res.json({ status: healthy, timestamp: new Date().toISOString(), model: Qwen3-ASR-0.6B }); }); // 获取支持的语言列表 app.get(/api/languages, (req, res) { res.json({ languages: [ { code: zh, name: 中文 }, { code: en, name: 英文 }, { code: yue, name: 粤语 }, { code: ja, name: 日语 }, { code: ko, name: 韩语 }, // ... 其他支持的语言 ] }); });5.2 添加批量处理接口对于需要处理大量音频文件的场景我们可以提供批量处理接口// 批量处理接口 app.post(/api/transcribe/batch, upload.array(audio, 10), async (req, res) { try { if (!req.files || req.files.length 0) { return res.status(400).json({ error: 请上传音频文件 }); } const { language } req.body; const results []; console.log(收到批量处理请求: ${req.files.length}个文件); // 并行处理所有文件 const promises req.files.map(async (file, index) { try { const result await transcribeAudio(file.buffer, language); return { filename: file.originalname, success: true, text: result.text, language: result.language }; } catch (error) { return { filename: file.originalname, success: false, error: error.message }; } }); const batchResults await Promise.all(promises); res.json({ success: true, total: req.files.length, processed: batchResults.filter(r r.success).length, failed: batchResults.filter(r !r.success).length, results: batchResults }); } catch (error) { console.error(批量处理失败:, error); res.status(500).json({ success: false, error: error.message }); } });6. 性能优化和错误处理在实际使用中我们还需要考虑性能优化和错误处理确保服务的稳定性和可靠性。6.1 添加请求限流为了防止服务被滥用我们可以添加请求限流const rateLimit require(express-rate-limit); // WebSocket连接限流 const wsConnections new Map(); // HTTP API限流 const apiLimiter rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 每个IP最多100次请求 message: 请求过于频繁请稍后再试 }); app.use(/api/, apiLimiter); // WebSocket连接数限制 wss.on(connection, (ws, req) { const clientIp req.socket.remoteAddress; const connections wsConnections.get(clientIp) || 0; if (connections 5) { ws.close(1008, 连接数超过限制); return; } wsConnections.set(clientIp, connections 1); ws.on(close, () { const current wsConnections.get(clientIp) || 0; if (current 0) { wsConnections.set(clientIp, current - 1); } }); // ... 原有的连接处理逻辑 });6.2 添加重试机制网络请求可能会失败我们可以添加重试机制async function transcribeAudioWithRetry(audioData, language null, maxRetries 3) { let lastError; for (let attempt 1; attempt maxRetries; attempt) { try { return await transcribeAudio(audioData, language); } catch (error) { lastError error; console.log(识别失败第${attempt}次重试:, error.message); if (attempt maxRetries) { // 等待一段时间再重试 await new Promise(resolve setTimeout(resolve, 1000 * attempt)); } } } throw lastError; }6.3 添加监控和日志为了更好地监控服务运行状态我们可以添加详细的日志// 请求日志中间件 app.use((req, res, next) { const startTime Date.now(); res.on(finish, () { const duration Date.now() - startTime; console.log(${new Date().toISOString()} ${req.method} ${req.url} ${res.statusCode} ${duration}ms); }); next(); }); // WebSocket消息日志 function logWebSocketMessage(clientId, type, data) { console.log(${new Date().toISOString()} [WS:${clientId}] ${type}:, typeof data object ? JSON.stringify(data).substring(0, 100) : data); } // 在消息处理中添加日志 ws.on(message, async (message) { logWebSocketMessage(clientId, received, message); // ... 处理逻辑 });7. 部署和配置建议最后我们来谈谈如何部署这个服务以及一些配置建议。7.1 Docker部署为了方便部署我们可以创建Docker镜像# Dockerfile FROM node:18-alpine WORKDIR /app # 复制package文件 COPY package*.json ./ # 安装依赖 RUN npm install --production # 复制源代码 COPY . . # 创建非root用户 RUN addgroup -g 1001 -S nodejs \ adduser -S nodejs -u 1001 USER nodejs # 暴露端口 EXPOSE 3000 # 启动命令 CMD [node, server.js]然后创建docker-compose.yml文件把Node.js服务和模型服务一起部署version: 3.8 services: asr-model: image: qwen3-asr:latest build: context: ./model-service ports: - 8000:8000 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] environment: - CUDA_VISIBLE_DEVICES0 asr-api: build: . ports: - 3000:3000 depends_on: - asr-model environment: - MODEL_API_URLhttp://asr-model:8000/v1/audio/transcriptions - NODE_ENVproduction - PORT30007.2 性能调优建议根据实际使用情况你可能需要调整一些参数音频分块大小流式识别时分块大小影响延迟和准确率的平衡。太小会增加请求次数太大会增加延迟。一般1-2秒比较合适。并发连接数根据服务器配置调整最大并发连接数。Qwen3-ASR-0.6B在128并发下性能最好但你的Node.js服务可能需要在更早的时候限流。缓冲区管理及时清理不再使用的音频缓冲区避免内存泄漏。GPU内存使用调整vLLM的--gpu-memory-utilization参数根据你的GPU显存大小来设置。7.3 安全建议启用HTTPS生产环境一定要用HTTPS保护音频数据的安全。身份验证添加API密钥或JWT token验证。输入验证验证客户端发送的音频数据格式和大小。错误信息不要向客户端返回详细的内部错误信息。8. 总结整体用下来Qwen3-ASR-0.6B确实是个不错的语音识别选择特别是对于中文场景识别准确率让人满意。结合Node.js来构建服务整个过程还算顺畅没有遇到特别棘手的问题。这套方案在实际项目里应该能解决不少问题。比如在线教育平台的实时字幕、会议系统的语音转写、客服系统的对话记录等等。性能方面只要硬件配置跟得上处理一般的并发量应该没问题。当然也有些需要注意的地方。模型服务对GPU有要求如果没有合适的显卡用CPU模式速度会慢很多。另外虽然Qwen3-ASR支持很多语言但对于一些小语种或者特别专业的领域术语可能还需要针对性地优化。如果你打算在实际项目中使用建议先小规模测试看看在具体场景下的表现如何。可以尝试调整音频采样率、分块大小这些参数找到最适合你需求的配置。另外关注一下模型的更新开源社区经常会有优化和改进。总的来说用开源模型自建语音识别服务相比依赖商业API给了我们更多的控制权和灵活性。虽然需要自己维护但长期来看可能更经济也更符合数据隐私的要求。希望这个实践分享对你有帮助如果有具体问题欢迎继续交流。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。