news 2026/4/18 0:21:45

Qwen3-ASR与Node.js集成:构建实时语音转写服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qwen3-ASR与Node.js集成:构建实时语音转写服务

Qwen3-ASR与Node.js集成:构建实时语音转写服务

想象一下,你正在开发一个在线会议应用,或者一个智能客服系统。用户对着麦克风说话,屏幕上几乎同步地出现他们说的文字。这种实时语音转写的体验,不仅能让沟通更高效,还能为后续的搜索、分析和存档提供极大便利。

过去,要实现这样的功能,要么得自己搭建复杂的语音识别模型,要么得依赖昂贵且延迟较高的云端服务。现在,有了开源的Qwen3-ASR模型,特别是其支持实时流式识别的版本,再结合Node.js强大的异步和网络编程能力,我们自己就能搭建一个高性能、低延迟的实时语音转写服务。

这篇文章,我就带你一步步用Node.js和Qwen3-ASR,从零开始构建这样一个服务。我们会用到WebSocket来实现双向实时通信,确保文字转写能紧跟语音输入。

1. 为什么选择Qwen3-ASR和Node.js?

在动手之前,我们先看看手里的“牌”为什么好。

Qwen3-ASR是阿里开源的语音识别模型家族。对于我们构建实时服务来说,它的几个特点特别吸引人:

  • 高精度与强鲁棒性:官方评测显示,其1.7B版本在中文、英文等场景下达到了开源SOTA水平,即使在复杂噪声或快速说唱环境下也能稳定输出。这意味着我们服务的转写结果会更可靠。
  • 支持实时流式识别:模型提供了专门的qwen3-asr-flash-realtime版本,专为流式音频设计。它能够一边接收音频流,一边实时返回识别出的文字片段,完美契合“实时”的需求。
  • 多语言与方言支持:原生支持多达52种语言和方言的识别。如果你的应用有国际化或多方言用户的需求,这一个模型就能搞定。
  • 开源免费:模型权重和推理代码完全开源,可以免费商用。这为我们控制成本、自主部署提供了最大的灵活性。

Node.js则是实现这个服务的绝佳“舞台”:

  • 事件驱动与非阻塞I/O:天生适合处理像WebSocket连接、音频流传输这类高并发、实时性要求高的场景。
  • 丰富的生态:有成熟稳定的ws库来处理WebSocket服务端和客户端,axios用于HTTP请求,生态完善,工具链齐全。
  • 开发效率高:JavaScript语言上手快,前后端统一,对于全栈开发者非常友好。

把这两者结合起来,我们就能构建一个部署灵活、性能出色、且完全自主可控的实时语音转写后端服务。

2. 项目环境搭建与准备

让我们先从创建一个干净的Node.js项目开始。

首先,打开你的终端,创建一个新的项目目录并初始化:

mkdir qwen-asr-realtime-service cd qwen-asr-realtime-service npm init -y

接下来,安装我们需要的核心依赖包:

npm install ws axios dotenv npm install --save-dev nodemon

这里简单说明一下:

  • ws:一个简单易用、性能强大的WebSocket库,我们将用它来创建WebSocket服务器和处理客户端连接。
  • axios:用于向Qwen3-ASR的API服务端(或我们自己封装的HTTP接口)发送HTTP请求。
  • dotenv:用来管理环境变量,比如我们的API Key等敏感信息就不会硬编码在代码里。
  • nodemon:开发工具,监听文件变化自动重启服务,提升开发体验。

然后,在项目根目录创建一个.env文件,用来存放配置。切记,这个文件要添加到.gitignore中,不要提交到代码仓库。

# .env DASHSCOPE_API_KEY=你的API_Key_在这里 QWEN_ASR_MODEL=qwen3-asr-flash-realtime # 注意地域,以下为北京地域endpoint,新加坡或美国地域需更换 DASHSCOPE_WS_ENDPOINT=wss://dashscope.aliyuncs.com/api-ws/v1/realtime SERVER_PORT=8080

关于DASHSCOPE_API_KEY:你需要前往阿里云百炼平台申请。开通服务后,在控制台就能创建和管理API Key。这是调用Qwen3-ASR服务的凭证。

3. 核心架构:如何连接音频流与识别模型

我们的服务核心是建立一个“桥梁”。桥的一边是客户端(比如网页、移动App)通过WebSocket发送过来的实时音频数据流;桥的另一边是Qwen3-ASR的实时识别API。我们的Node.js服务就是这个桥梁,负责协议转换、会话管理和结果转发。

整个流程可以概括为以下几个步骤:

  1. 客户端连接:前端通过WebSocket连接到我们的Node.js服务。
  2. 建立ASR会话:Node.js服务使用客户端的唯一连接标识,向Qwen3-ASR的WebSocket端点发起另一个连接,并初始化一个语音识别会话。
  3. 音频流中转:前端将采集到的音频数据(通常是PCM格式)通过自己的WebSocket连接发送过来。Node.js服务立即将这些数据“搬运”到与ASR服务的那个连接上。
  4. 文字流返回:ASR服务实时处理音频流,并返回片段化的识别文字。Node.js服务再将这些文字通过对应的WebSocket连接发回给前端。
  5. 会话结束:当客户端停止发送音频(如前端检测到静音并主动结束),或连接断开时,Node.js服务需要清理两端连接。

这个架构的优势在于,Node.js服务作为中间层,可以处理鉴权、负载均衡、会话映射、错误处理等通用逻辑,让前端和ASR模型都能更专注于自己的核心任务。

4. 分步实现Node.js WebSocket服务

理论清楚了,我们开始写代码。我会把核心代码拆解开来,方便你理解。

4.1 创建WebSocket服务器与客户端管理

首先,我们创建一个server.js文件,作为服务的入口。

// server.js const WebSocket = require('ws'); const axios = require('axios'); require('dotenv').config(); // 从环境变量读取配置 const SERVER_PORT = process.env.SERVER_PORT || 8080; const DASHSCOPE_API_KEY = process.env.DASHSCOPE_API_KEY; const QWEN_ASR_MODEL = process.env.QWEN_ASR_MODEL; const DASHSCOPE_WS_ENDPOINT = process.env.DASHSCOPE_WS_ENDPOINT; // 存储活跃的客户端连接和对应的ASR会话 const clients = new Map(); // key: clientWs, value: { asrClient, sessionId } // 创建WebSocket服务器 const wss = new WebSocket.Server({ port: SERVER_PORT }); console.log(`实时语音转写服务启动在 ws://localhost:${SERVER_PORT}`); wss.on('connection', (clientWs, request) => { console.log('新的客户端连接'); const clientId = Date.now().toString(); // 简单生成一个客户端ID // 为这个客户端创建到ASR服务的连接 const asrWsUrl = `${DASHSCOPE_WS_ENDPOINT}?model=${QWEN_ASR_MODEL}`; const asrWs = new WebSocket(asrWsUrl, { headers: { 'Authorization': `Bearer ${DASHSCOPE_API_KEY}`, 'OpenAI-Beta': 'realtime=v1' } }); // 存储关联关系 clients.set(clientWs, { asrClient: asrWs, clientId }); // ... 后续代码处理消息和事件 });

这段代码初始化了服务,并为每一个新连接的客户端,都创建了一个对应的、连接到真实Qwen3-ASR服务的WebSocket连接。我们用clients这个Map来管理这种一对一的映射关系。

4.2 处理ASR服务连接与音频转发

接下来,我们需要处理ASR服务连接成功后的初始化,以及最重要的音频数据转发。

在上面的connection事件回调里,我们继续补充对asrWs的事件监听和clientWs的消息处理:

// 监听ASR服务WebSocket的事件 asrWs.on('open', () => { console.log(`[${clientId}] 已连接到ASR服务`); // 发送会话更新事件,配置识别参数 const sessionUpdateEvent = { event_id: `session_update_${clientId}`, type: 'session.update', session: { modalities: ['text'], // 我们只关心文本输出 input_audio_format: 'pcm', // 假设前端发送的是PCM音频 sample_rate: 16000, // 16kHz采样率,这是常见配置 input_audio_transcription: { language: 'zh' // 指定中文识别,可选。不指定则自动检测 }, turn_detection: { // 开启服务端VAD(语音活动检测),自动断句 type: 'server_vad', threshold: 0.0, silence_duration_ms: 400 // 静音400毫秒后认为一句话结束 } } }; asrWs.send(JSON.stringify(sessionUpdateEvent)); }); asrWs.on('message', (data) => { // 收到来自ASR服务的消息 try { const event = JSON.parse(data); // console.log(`[${clientId}] 收到ASR事件:`, event.type); if (event.type === 'session.finished') { // 最终转写结果 const finalTranscript = event.transcript; console.log(`[${clientId}] 最终转写结果:`, finalTranscript); // 可以发送给客户端,或存入数据库等 if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ type: 'final_transcript', data: finalTranscript })); } } else if (event.type === 'transcript' && event.transcript) { // 实时增量转写结果(流式输出) // console.log(`[${clientId}] 实时转写:`, event.transcript); if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ type: 'partial_transcript', data: event.transcript })); } } } catch (error) { console.error(`[${clientId}] 解析ASR消息失败:`, error); } }); asrWs.on('error', (error) => { console.error(`[${clientId}] ASR连接错误:`, error); }); asrWs.on('close', () => { console.log(`[${clientId}] ASR连接关闭`); // 清理资源 cleanupClient(clientWs); }); // 监听客户端发来的消息 clientWs.on('message', (message) => { const clientSession = clients.get(clientWs); if (!clientSession) return; try { const msg = JSON.parse(message); switch (msg.type) { case 'audio_data': // 前端发送来的音频数据块(Base64编码的PCM数据) if (clientSession.asrClient.readyState === WebSocket.OPEN) { const audioEvent = { event_id: `audio_${Date.now()}`, type: 'input_audio_buffer.append', audio: msg.data // 这里应该是Base64字符串 }; clientSession.asrClient.send(JSON.stringify(audioEvent)); } break; case 'audio_end': // 前端通知音频发送完毕 if (clientSession.asrClient.readyState === WebSocket.OPEN) { const finishEvent = { event_id: `finish_${clientId}`, type: 'session.finish' }; clientSession.asrClient.send(JSON.stringify(finishEvent)); } break; default: console.log(`[${clientId}] 未知的客户端消息类型:`, msg.type); } } catch (error) { console.error(`[${clientId}] 处理客户端消息失败:`, error); } }); // 客户端断开连接 clientWs.on('close', () => { console.log(`[${clientId}] 客户端断开连接`); cleanupClient(clientWs); }); clientWs.on('error', (error) => { console.error(`[${clientId}] 客户端连接错误:`, error); cleanupClient(clientWs); });

这段代码是核心中的核心。它做了以下几件关键事:

  1. 配置ASR会话:在asrWs.on('open')中,我们发送session.update事件,告诉ASR服务我们需要的音频格式(PCM, 16kHz)、语言,并开启了服务端的VAD功能,让它能自动检测一句话的开始和结束。
  2. 转发音频数据:在clientWs.on('message')中,我们监听前端发来的audio_data消息,将其重新包装成ASR服务能识别的input_audio_buffer.append事件,然后转发过去。
  3. 返回识别结果:在asrWs.on('message')中,我们监听ASR服务返回的事件。如果是transcript类型,就是实时的增量识别结果,我们立刻通过clientWs发回给前端。如果是session.finished,就是最终完整的转写结果。
  4. 处理会话结束:无论是前端发来audio_end,还是连接断开,我们都通过cleanupClient函数来优雅地关闭ASR连接并清理资源。

4.3 实现资源清理与错误处理

我们还需要实现上面用到的cleanupClient函数,以及服务器的基本错误处理。

// 清理客户端资源的函数 function cleanupClient(clientWs) { const session = clients.get(clientWs); if (session) { if (session.asrClient && session.asrClient.readyState === WebSocket.OPEN) { session.asrClient.close(); } clients.delete(clientWs); console.log(`[${session.clientId}] 资源已清理`); } } // 服务器全局错误处理 wss.on('error', (error) => { console.error('WebSocket服务器错误:', error); }); // 优雅关闭 process.on('SIGINT', () => { console.log('正在关闭服务...'); wss.close(() => { console.log('WebSocket服务器已关闭'); process.exit(0); }); });

至此,我们Node.js端的WebSocket中转服务就完成了。你可以运行node server.js来启动它。

5. 前端示例:采集音频并连接服务

服务端准备好了,我们还需要一个简单的前端来测试。这里提供一个使用浏览器Web Audio API和WebSocket的简单示例。

创建一个index.html文件:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>实时语音转写测试</title> <style> body { font-family: sans-serif; padding: 20px; } button { padding: 10px 20px; margin: 5px; font-size: 16px; } #status { margin: 10px 0; padding: 10px; background: #eee; } #transcript { border: 1px solid #ccc; padding: 15px; min-height: 100px; margin-top: 20px; white-space: pre-wrap; } </style> </head> <body> <h1>Qwen3-ASR 实时转写测试</h1> <button id="startBtn">开始录音</button> <button id="stopBtn" disabled>停止录音</button> <div id="status">状态:就绪</div> <div> <h3>实时转写结果:</h3> <div id="transcript"></div> </div> <script> const serverWsUrl = 'ws://localhost:8080'; // 你的Node.js服务地址 let mediaRecorder; let audioChunks = []; let ws; let isRecording = false; const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); const statusDiv = document.getElementById('status'); const transcriptDiv = document.getElementById('transcript'); // 初始化WebSocket连接 function initWebSocket() { ws = new WebSocket(serverWsUrl); ws.onopen = () => { statusDiv.textContent = '状态:已连接到转写服务'; console.log('WebSocket连接已打开'); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'partial_transcript') { // 实时增量结果,可以高亮显示最后一句 transcriptDiv.innerHTML += `<span style="color:blue">${data.data}</span> `; } else if (data.type === 'final_transcript') { // 一句话结束的最终结果 transcriptDiv.innerHTML += `<strong>${data.data}</strong><br>`; } }; ws.onerror = (error) => { statusDiv.textContent = '状态:连接错误'; console.error('WebSocket错误:', error); }; ws.onclose = () => { statusDiv.textContent = '状态:连接断开'; console.log('WebSocket连接关闭'); }; } // 开始录音 startBtn.onclick = async () => { try { statusDiv.textContent = '状态:请求麦克风权限...'; const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); statusDiv.textContent = '状态:初始化音频处理器...'; const audioContext = new AudioContext({ sampleRate: 16000 }); const source = audioContext.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor(4096, 1, 1); // 连接:麦克风 -> 处理器 -> 目的地(静音) source.connect(processor); processor.connect(audioContext.destination); // 初始化WebSocket if (!ws || ws.readyState !== WebSocket.OPEN) { initWebSocket(); } processor.onaudioprocess = (e) => { if (!ws || ws.readyState !== WebSocket.OPEN) return; // 获取PCM数据(Float32) const inputData = e.inputBuffer.getChannelData(0); // 转换为16位整数PCM (Int16Array) const pcm16 = new Int16Array(inputData.length); for (let i = 0; i < inputData.length; i++) { pcm16[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768)); } // 转换为Base64 const base64String = btoa(String.fromCharCode(...new Uint8Array(pcm16.buffer))); // 通过WebSocket发送音频数据块 ws.send(JSON.stringify({ type: 'audio_data', data: base64String })); }; isRecording = true; startBtn.disabled = true; stopBtn.disabled = false; statusDiv.textContent = '状态:正在录音和转写...'; transcriptDiv.innerHTML = ''; } catch (err) { statusDiv.textContent = `状态:错误 - ${err.message}`; console.error('启动录音失败:', err); } }; // 停止录音 stopBtn.onclick = () => { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'audio_end' })); } if (mediaRecorder && isRecording) { mediaRecorder.stop(); } // 关闭所有音频流和处理器 // ... (实际项目中需要更细致的资源释放) isRecording = false; startBtn.disabled = false; stopBtn.disabled = true; statusDiv.textContent = '状态:已停止'; }; // 页面加载时初始化 window.onload = () => { initWebSocket(); }; </script> </body> </html>

这个前端页面做了以下几件事:

  1. 提供了开始/停止录音的按钮。
  2. 使用getUserMedia获取麦克风权限。
  3. 使用AudioContextScriptProcessorNode实时处理音频流,将其转换为16kHz、16位的PCM格式,并分块进行Base64编码。
  4. 通过WebSocket将编码后的音频数据块发送给我们刚搭建的Node.js服务。
  5. 接收并显示从服务端返回的partial_transcript(实时增量文字)和final_transcript(一句话的最终结果)。

注意:这个前端示例为了简洁,省略了详细的错误处理和资源释放代码。在生产环境中,你需要更健壮地处理音频流的开启、关闭以及WebSocket的重连逻辑。

6. 部署与优化建议

当你本地测试通过后,就可以考虑部署到生产环境了。这里有一些建议:

  1. 服务器选择:选择一台网络状况良好、CPU性能足够的云服务器。Qwen3-ASR的推理负载主要在阿里云侧,你的Node.js服务主要是网络中转,所以对CPU要求不高,但网络延迟和稳定性很重要。
  2. 环境变量管理:在生产环境,使用更安全的方式管理DASHSCOPE_API_KEY,比如云服务商提供的密钥管理服务(如AWS KMS, 阿里云KMS)。
  3. 进程管理:使用pm2systemd来管理Node.js进程,确保服务崩溃后能自动重启。
    npm install -g pm2 pm2 start server.js --name "qwen-asr-service"
  4. 安全性
    • 为你的WebSocket服务(ws://)添加WSS(wss://)支持,即WebSocket over TLS,确保通信加密。这通常可以通过在Node.js服务前配置Nginx反向代理来实现。
    • 考虑添加简单的认证机制,比如连接时验证Token,防止服务被滥用。
  5. 可扩展性:如果并发用户量很大,单个Node.js实例可能成为瓶颈。你可以考虑:
    • 使用Node.js的集群模式(cluster模块)利用多核CPU。
    • 将客户端-ASR会话的映射关系存储到Redis等外部缓存中,实现无状态的服务水平扩展。
  6. 监控与日志:接入监控系统,记录服务的连接数、音频处理时长、错误率等指标。将日志输出到文件或日志服务,方便排查问题。

7. 总结

走完这一趟,我们从零搭建了一个基于Qwen3-ASR和Node.js的实时语音转写服务。这个方案把开源模型的能力与Node.js的实时特性结合了起来,给了我们一个成本可控、自主性高的选择。

实际用下来,你会发现核心的中转逻辑并不复杂,但带来的可能性却很多。你可以把这个服务集成到在线教育平台里,实时生成课堂字幕;可以放到视频会议工具里,自动生成会议纪要;甚至可以结合大语言模型,做一个能实时听懂并回答问题的语音助手。

当然,现在这个版本还是一个起点。你可能需要根据实际业务,添加更多的功能,比如识别结果的后处理、多说话人分离、或者更复杂的会话状态管理。但有了这个基础框架,后续的扩展都会容易很多。

希望这篇文章能帮你打开思路。语音交互正在变得越来越普遍,拥有一个自己能够掌控的实时转写引擎,无疑会在很多项目中成为亮点。


获取更多AI镜像

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 18:05:33

Z-Image-Turbo_Sugar脸部Lora实战案例:短视频封面甜妹形象统一化生成

Z-Image-Turbo_Sugar脸部Lora实战案例&#xff1a;短视频封面甜妹形象统一化生成 1. 项目背景与价值 在短视频内容创作领域&#xff0c;封面图片的质量和风格统一性直接影响点击率和用户留存。传统人工绘制封面存在效率低、风格不一致等问题。Z-Image-Turbo_Sugar脸部Lora模型…

作者头像 李华
网站建设 2026/4/18 3:52:32

轻松掌控博德之门3模组:BG3 Mod Manager完整指南

轻松掌控博德之门3模组&#xff1a;BG3 Mod Manager完整指南 【免费下载链接】BG3ModManager A mod manager for Baldurs Gate 3. 项目地址: https://gitcode.com/gh_mirrors/bg/BG3ModManager 在《博德之门3》的冒险旅程中&#xff0c;模组是扩展游戏体验的关键。但杂乱…

作者头像 李华
网站建设 2026/4/18 3:53:07

多模态搜索:GLM-Image构建视觉搜索引擎

多模态搜索&#xff1a;GLM-Image构建视觉搜索引擎 1. 为什么需要“以图搜图→生成相似图”的新范式 传统图像搜索大多停留在关键词匹配层面——你输入“红色跑车”&#xff0c;系统返回一堆带“红色”和“跑车”标签的图片。但现实中的需求远比这复杂&#xff1a;设计师看到…

作者头像 李华
网站建设 2026/4/18 3:39:48

圣女司幼幽-造相Z-Turbo一文详解:Z-Image-Turbo基座+LoRA定制技术原理

圣女司幼幽-造相Z-Turbo一文详解&#xff1a;Z-Image-Turbo基座LoRA定制技术原理 1. 模型简介与核心价值 圣女司幼幽-造相Z-Turbo是基于Z-Image-Turbo基座模型&#xff0c;通过LoRA技术微调定制的文生图模型。该模型专门针对《牧神记》中圣女司幼幽这一角色进行优化&#xff…

作者头像 李华
网站建设 2026/4/18 3:51:45

Cosmos-Reason1-7B模型监控与日志分析实战

Cosmos-Reason1-7B模型监控与日志分析实战 想让你的大模型服务跑得又稳又好&#xff0c;光部署上线可不够。模型跑起来之后&#xff0c;怎么知道它是不是在“健康工作”&#xff1f;响应慢了、内存快爆了、突然报错了&#xff0c;这些情况你总不能等用户投诉了才发现吧。 这就…

作者头像 李华