news 2026/4/15 10:17:58

Qwen3-ASR-0.6B与Vue.js前端集成:实时语音转写应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qwen3-ASR-0.6B与Vue.js前端集成:实时语音转写应用

Qwen3-ASR-0.6B与Vue.js前端集成:实时语音转写应用

你有没有想过,给你的网站或者应用加上一个“耳朵”,让它能听懂用户说的话?比如,做一个在线会议记录工具,或者一个语音输入的智能客服,甚至是一个能实时生成字幕的视频播放器。听起来很酷,但一想到要处理复杂的音频流、搭建语音识别服务,是不是就觉得头大?

别担心,今天我们就来聊聊怎么用Vue.js前端,结合最新的Qwen3-ASR-0.6B语音识别模型,快速搭建一个属于自己的实时语音转写应用。整个过程比你想象的要简单,而且效果相当不错。

1. 为什么选择Qwen3-ASR-0.6B?

在开始动手之前,我们先简单了解一下这次的主角——Qwen3-ASR-0.6B。这是一个最近开源的语音识别模型,别看它只有0.6B(约9亿)参数,但能力一点都不弱。

我实际测试下来,发现它有这几个特点特别适合我们做前端集成:

识别速度快:这是最让我惊喜的一点。模型支持流式推理,也就是说,用户一边说话,它就能一边识别,不用等整段话说完。这对于实时应用来说太重要了,延迟低,体验就好。

支持语言多:官方说支持52种语言和方言,包括22种中文方言。我试了普通话、粤语,甚至带点口音的英语,识别准确率都挺高的。这意味着你的应用可以面向更广泛的用户。

资源占用少:0.6B的模型大小,相比动辄几十亿参数的大模型,部署起来压力小很多。无论是自己搭服务,还是用云服务,成本都更容易控制。

有现成的API:如果你不想自己部署模型,阿里云百炼提供了开箱即用的API服务,按使用量计费,用多少付多少,特别适合项目初期或者流量不大的场景。

基于这些特点,我觉得把它和Vue.js前端结合起来,能做出不少有意思的应用。接下来,我就带你一步步实现。

2. 整体架构:前端怎么和后端“对话”

在开始写代码之前,我们先理清楚整个应用是怎么工作的。你可以把我们的应用想象成一个“翻译官”:前端负责“听”用户说话,后端负责“理解”并“翻译”成文字。

2.1 技术栈选择

前端(Vue.js 3 + TypeScript)

  • 用Vue 3的Composition API,代码组织更清晰
  • TypeScript提供类型安全,减少运行时错误
  • Pinia做状态管理,管理音频录制状态、识别结果等
  • Vite作为构建工具,开发体验好,打包速度快

音频处理(Web Audio API)

  • 这是浏览器自带的API,不需要额外安装
  • 负责从麦克风获取音频流
  • 对音频进行预处理,比如降噪、分块

通信(WebSocket + HTTP)

  • WebSocket用于流式传输,用户一边说话,一边发送音频数据到后端
  • HTTP用于一次性上传整段音频文件

后端服务

  • 你可以选择自己部署Qwen3-ASR-0.6B模型
  • 或者直接使用阿里云百炼的API服务
  • 我们这里会展示两种方式的对接方法

2.2 数据流示意图

整个应用的数据流大概是这样的:

用户说话 → 浏览器麦克风 → Web Audio API处理 → 分块编码 → WebSocket发送 ↓ 后端接收音频数据 ↓ Qwen3-ASR-0.6B识别 ↓ 返回识别文本结果 ↓ WebSocket接收 → 前端展示

听起来有点复杂?别担心,我们一步步来,每个部分我都会给出具体的代码示例。

3. 前端核心实现:让浏览器“听见”声音

前端部分我们要做三件事:获取麦克风权限、录制音频、处理并发送音频数据。这是整个应用的基础,也是和用户交互最直接的部分。

3.1 搭建基础的Vue组件

我们先创建一个基础的语音录制组件。这个组件会包含一个录音按钮、一个停止按钮,以及显示识别结果的区域。

<!-- SpeechRecognition.vue --> <template> <div class="speech-recognition"> <div class="controls"> <button @click="startRecording" :disabled="isRecording" class="record-btn" > {{ isRecording ? '录音中...' : '开始录音' }} </button> <button @click="stopRecording" :disabled="!isRecording" class="stop-btn" > 停止 </button> <button @click="clearText" class="clear-btn"> 清空文本 </button> </div> <div class="status"> <div v-if="isRecording" class="recording-indicator"> <span class="dot"></span> 正在录音... </div> <div v-if="error" class="error-message"> {{ error }} </div> </div> <div class="result"> <h3>识别结果:</h3> <div class="text-output"> {{ transcribedText }} </div> <div v-if="isProcessing" class="processing"> 处理中... </div> </div> <!-- 音频可视化,让界面更生动 --> <div class="visualizer"> <canvas ref="canvasRef" width="400" height="100"></canvas> </div> </div> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' // 定义组件状态 const isRecording = ref(false) const isProcessing = ref(false) const transcribedText = ref('') const error = ref('') const canvasRef = ref<HTMLCanvasElement>() // 这里先定义方法,具体实现后面会补充 const startRecording = async () => { // 开始录音 } const stopRecording = () => { // 停止录音 } const clearText = () => { transcribedText.value = '' } </script> <style scoped> .speech-recognition { max-width: 600px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .controls { display: flex; gap: 12px; margin-bottom: 20px; } button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.2s; } .record-btn { background-color: #4CAF50; color: white; } .record-btn:disabled { background-color: #cccccc; cursor: not-allowed; } .stop-btn { background-color: #f44336; color: white; } .stop-btn:disabled { background-color: #cccccc; cursor: not-allowed; } .clear-btn { background-color: #2196F3; color: white; } .status { margin-bottom: 20px; min-height: 24px; } .recording-indicator { display: flex; align-items: center; color: #f44336; font-weight: 500; } .dot { width: 10px; height: 10px; background-color: #f44336; border-radius: 50%; margin-right: 8px; animation: blink 1s infinite; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } .error-message { color: #f44336; padding: 10px; background-color: #ffebee; border-radius: 4px; } .result { margin-top: 30px; } .text-output { min-height: 150px; padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; background-color: #f9f9f9; white-space: pre-wrap; line-height: 1.6; } .processing { margin-top: 10px; color: #666; font-style: italic; } .visualizer { margin-top: 30px; border: 1px solid #e0e0e0; border-radius: 8px; padding: 10px; background-color: #f5f5f5; } </style>

这个组件已经有了基本的外观和状态管理。接下来我们要实现最核心的部分——音频录制。

3.2 使用Web Audio API录制音频

Web Audio API是浏览器处理音频的底层API,功能强大但稍微有点复杂。别担心,我会把关键步骤拆解清楚。

// 在SpeechRecognition.vue的script部分继续添加 import { ref, onMounted, onUnmounted } from 'vue' // 音频录制相关的状态和引用 const mediaRecorder = ref<MediaRecorder | null>(null) const audioChunks = ref<Blob[]>([]) const audioStream = ref<MediaStream | null>(null) const audioContext = ref<AudioContext | null>(null) const analyser = ref<AnalyserNode | null>(null) const animationFrameId = ref<number>() // 初始化音频上下文和分析器(用于可视化) const initAudioContext = () => { if (!audioContext.value) { audioContext.value = new (window.AudioContext || (window as any).webkitAudioContext)() analyser.value = audioContext.value.createAnalyser() analyser.value.fftSize = 256 } } // 开始录音 const startRecording = async () => { try { error.value = '' // 1. 请求麦克风权限 const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } }) audioStream.value = stream // 2. 初始化音频上下文 initAudioContext() // 3. 创建MediaRecorder const options = { mimeType: 'audio/webm;codecs=opus' } mediaRecorder.value = new MediaRecorder(stream, options) // 4. 设置数据可用时的处理 mediaRecorder.value.ondataavailable = (event) => { if (event.data.size > 0) { audioChunks.value.push(event.data) // 这里可以实时发送数据到后端 sendAudioChunk(event.data) } } // 5. 开始录制 mediaRecorder.value.start(1000) // 每1秒触发一次dataavailable isRecording.value = true // 6. 开始音频可视化 startVisualization(stream) console.log('录音开始') } catch (err) { error.value = `无法访问麦克风: ${err}` console.error('录音错误:', err) } } // 停止录音 const stopRecording = () => { if (mediaRecorder.value && isRecording.value) { mediaRecorder.value.stop() isRecording.value = false // 停止所有音轨 if (audioStream.value) { audioStream.value.getTracks().forEach(track => track.stop()) } // 停止可视化 stopVisualization() console.log('录音停止') // 处理最后的数据 processFinalAudio() } } // 发送音频数据块到后端 const sendAudioChunk = async (chunk: Blob) => { // 这里先留空,后面会实现WebSocket发送 console.log('发送音频块,大小:', chunk.size) } // 处理最终的完整音频 const processFinalAudio = async () => { if (audioChunks.value.length === 0) return isProcessing.value = true try { // 将多个音频块合并成一个Blob const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' }) // 这里可以调用一次性识别的API // await transcribeFullAudio(audioBlob) // 清空音频块 audioChunks.value = [] } catch (err) { error.value = `处理音频失败: ${err}` } finally { isProcessing.value = false } } // 音频可视化 const startVisualization = (stream: MediaStream) => { if (!audioContext.value || !analyser.value) return const source = audioContext.value.createMediaStreamSource(stream) source.connect(analyser.value) const draw = () => { if (!canvasRef.value || !analyser.value) return const canvas = canvasRef.value const ctx = canvas.getContext('2d') if (!ctx) return const bufferLength = analyser.value.frequencyBinCount const dataArray = new Uint8Array(bufferLength) analyser.value.getByteTimeDomainData(dataArray) ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.lineWidth = 2 ctx.strokeStyle = '#4CAF50' ctx.beginPath() const sliceWidth = canvas.width * 1.0 / bufferLength let x = 0 for (let i = 0; i < bufferLength; i++) { const v = dataArray[i] / 128.0 const y = v * canvas.height / 2 if (i === 0) { ctx.moveTo(x, y) } else { ctx.lineTo(x, y) } x += sliceWidth } ctx.lineTo(canvas.width, canvas.height / 2) ctx.stroke() animationFrameId.value = requestAnimationFrame(draw) } draw() } const stopVisualization = () => { if (animationFrameId.value) { cancelAnimationFrame(animationFrameId.value) } // 清空画布 if (canvasRef.value) { const ctx = canvasRef.value.getContext('2d') if (ctx) { ctx.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height) } } } // 组件卸载时清理资源 onUnmounted(() => { if (isRecording.value) { stopRecording() } stopVisualization() })

现在,我们的前端已经可以录制音频了,并且有一个简单的波形可视化。接下来我们要实现和后端的通信。

4. 连接后端:两种方案的选择

根据你的需求和资源,可以选择两种不同的后端方案。我会分别介绍,你可以根据自己的情况选择。

4.1 方案一:使用阿里云百炼API(快速上手)

如果你不想自己部署模型,或者项目刚开始想快速验证,使用阿里云百炼的API是最简单的选择。它提供了现成的语音识别服务,按使用量计费。

首先,我们需要在阿里云百炼开通服务并获取API密钥。然后在前端直接调用即可。

// 创建一个专门处理API调用的文件 // api/aliyun-asr.ts const ALIYUN_API_ENDPOINT = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/audio-transcription/transcriptions' const ALIYUN_API_KEY = '你的API密钥' // 注意:在实际项目中要从环境变量读取 export interface AliyunTranscriptionRequest { model: string audio_url?: string file?: Blob language?: string response_format?: 'json' | 'text' | 'srt' | 'vtt' } export interface AliyunTranscriptionResponse { output: { text: string sentences?: Array<{ text: string start_time: number end_time: number }> } usage: { audio_duration: number } request_id: string } // 通过URL识别音频 export async function transcribeByUrl(audioUrl: string, language?: string): Promise<AliyunTranscriptionResponse> { const requestBody: AliyunTranscriptionRequest = { model: 'qwen3-asr-flash-realtime', audio_url: audioUrl, language: language || 'auto', response_format: 'json' } const response = await fetch(ALIYUN_API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ALIYUN_API_KEY}` }, body: JSON.stringify(requestBody) }) if (!response.ok) { throw new Error(`API调用失败: ${response.statusText}`) } return await response.json() } // 通过文件识别音频 export async function transcribeByFile(audioFile: Blob, language?: string): Promise<AliyunTranscriptionResponse> { // 将Blob转换为Base64 const base64Audio = await blobToBase64(audioFile) const requestBody = { model: 'qwen3-asr-flash-realtime', file: base64Audio.split(',')[1], // 移除data:audio/...;base64,前缀 language: language || 'auto', response_format: 'json' } const response = await fetch(ALIYUN_API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ALIYUN_API_KEY}` }, body: JSON.stringify(requestBody) }) return await response.json() } // Blob转Base64的辅助函数 function blobToBase64(blob: Blob): Promise<string> { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onloadend = () => resolve(reader.result as string) reader.onerror = reject reader.readAsDataURL(blob) }) } // 流式识别(WebSocket方式) export class AliyunStreamingASR { private ws: WebSocket | null = null private messageCallback: ((text: string) => void) | null = null private errorCallback: ((error: Error) => void) | null = null constructor( private apiKey: string, private language: string = 'auto' ) {} // 开始流式识别 async startStreaming(): Promise<void> { // 阿里云百炼的流式API地址(示例,实际需要查看最新文档) const wsUrl = `wss://dashscope.aliyuncs.com/api/v1/services/aigc/audio-transcription/streaming?api_key=${this.apiKey}&language=${this.language}` this.ws = new WebSocket(wsUrl) return new Promise((resolve, reject) => { if (!this.ws) return reject(new Error('WebSocket初始化失败')) this.ws.onopen = () => { console.log('WebSocket连接已建立') resolve() } this.ws.onerror = (error) => { console.error('WebSocket错误:', error) reject(new Error('WebSocket连接失败')) } this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.output?.text && this.messageCallback) { this.messageCallback(data.output.text) } } catch (err) { console.error('解析消息失败:', err) } } }) } // 发送音频数据 sendAudioData(audioData: ArrayBuffer): void { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(audioData) } } // 停止流式识别 stopStreaming(): void { if (this.ws) { this.ws.close() this.ws = null } } // 设置消息回调 onMessage(callback: (text: string) => void): void { this.messageCallback = callback } // 设置错误回调 onError(callback: (error: Error) => void): void { this.errorCallback = callback } }

4.2 方案二:自建后端服务(更灵活控制)

如果你需要更多的控制权,或者有数据隐私的考虑,可以选择自己部署Qwen3-ASR-0.6B模型。这里我给出一个简单的FastAPI后端示例。

# backend/main.py from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware import torch from qwen_asr import Qwen3ASRModel import numpy as np import asyncio import json import logging # 配置日志 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="Qwen3-ASR实时语音识别服务") # 配置CORS,允许前端访问 app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], # 前端开发服务器地址 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 全局模型实例 asr_model = None @app.on_event("startup") async def startup_event(): """启动时加载模型""" global asr_model try: logger.info("正在加载Qwen3-ASR-0.6B模型...") # 加载模型,使用半精度浮点数减少内存占用 asr_model = Qwen3ASRModel.from_pretrained( "Qwen/Qwen3-ASR-0.6B", dtype=torch.bfloat16, device_map="cuda:0" if torch.cuda.is_available() else "cpu", max_inference_batch_size=32, max_new_tokens=256, ) logger.info("模型加载完成") except Exception as e: logger.error(f"模型加载失败: {e}") raise class ConnectionManager: """管理WebSocket连接""" def __init__(self): self.active_connections: list[WebSocket] = [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): if websocket in self.active_connections: self.active_connections.remove(websocket) async def send_text(self, websocket: WebSocket, text: str): await websocket.send_json({"type": "transcription", "text": text}) async def send_error(self, websocket: WebSocket, error: str): await websocket.send_json({"type": "error", "message": error}) manager = ConnectionManager() @app.websocket("/ws/transcribe") async def websocket_transcribe(websocket: WebSocket): """WebSocket端点,用于实时语音识别""" await manager.connect(websocket) # 存储音频数据的缓冲区 audio_buffer = [] try: while True: # 接收音频数据 data = await websocket.receive() if "bytes" in data: # 处理二进制音频数据 audio_chunk = data["bytes"] audio_buffer.append(audio_chunk) # 每收集一定量的数据就进行一次识别 if len(audio_buffer) >= 5: # 例如:每5个块识别一次 try: # 合并音频数据 combined_audio = b"".join(audio_buffer) # 这里需要将音频数据转换为模型需要的格式 # 注意:实际实现中需要根据音频格式进行解码 # 模拟识别过程 # result = asr_model.transcribe(audio=combined_audio) # transcription = result[0].text # 为了演示,这里返回模拟结果 transcription = "这是模拟的识别结果" # 发送识别结果 await manager.send_text(websocket, transcription) # 清空缓冲区(保留最后几个块用于上下文) audio_buffer = audio_buffer[-2:] if len(audio_buffer) > 2 else [] except Exception as e: logger.error(f"识别失败: {e}") await manager.send_error(websocket, f"识别失败: {str(e)}") elif "text" in data: # 处理文本消息(如控制命令) message = data["text"] if message == "stop": break except WebSocketDisconnect: logger.info("客户端断开连接") except Exception as e: logger.error(f"WebSocket错误: {e}") await manager.send_error(websocket, f"服务器错误: {str(e)}") finally: manager.disconnect(websocket) logger.info("连接已关闭") @app.post("/api/transcribe") async def transcribe_audio(): """一次性识别整个音频文件""" # 这里实现文件上传和识别的逻辑 return {"message": "音频识别接口"} @app.get("/health") async def health_check(): """健康检查端点""" return { "status": "healthy", "model_loaded": asr_model is not None, "gpu_available": torch.cuda.is_available() } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)

这个后端服务提供了WebSocket接口用于实时识别,也提供了HTTP接口用于一次性识别整个文件。部署时需要注意模型的大小和GPU内存需求。

5. 前端集成WebSocket:实现真正的实时转写

现在我们把前后端连接起来,实现真正的实时语音转写。这里我以自建后端为例,展示如何在前端集成WebSocket。

// 在SpeechRecognition.vue中添加WebSocket集成 import { ref, onMounted, onUnmounted } from 'vue' // WebSocket相关状态 const ws = ref<WebSocket | null>(null) const isConnected = ref(false) const reconnectAttempts = ref(0) const maxReconnectAttempts = 5 // 初始化WebSocket连接 const initWebSocket = () => { // 根据环境配置WebSocket地址 const wsUrl = import.meta.env.DEV ? 'ws://localhost:8000/ws/transcribe' : 'wss://你的生产环境地址/ws/transcribe' ws.value = new WebSocket(wsUrl) ws.value.onopen = () => { console.log('WebSocket连接成功') isConnected.value = true reconnectAttempts.value = 0 } ws.value.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.type === 'transcription') { // 追加识别结果 transcribedText.value += data.text + ' ' // 可以在这里添加一些动画效果 highlightNewText(data.text) } else if (data.type === 'error') { error.value = `识别错误: ${data.message}` } } catch (err) { console.error('解析WebSocket消息失败:', err) } } ws.value.onerror = (error) => { console.error('WebSocket错误:', error) error.value = '连接服务器失败' } ws.value.onclose = () => { console.log('WebSocket连接关闭') isConnected.value = false // 尝试重连 if (reconnectAttempts.value < maxReconnectAttempts) { reconnectAttempts.value++ console.log(`尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts})...`) setTimeout(() => { if (isRecording.value) { initWebSocket() } }, 2000 * reconnectAttempts.value) // 指数退避 } } } // 修改sendAudioChunk函数,通过WebSocket发送数据 const sendAudioChunk = async (chunk: Blob) => { if (!ws.value || ws.value.readyState !== WebSocket.OPEN) { console.warn('WebSocket未连接,无法发送音频数据') return } try { // 将Blob转换为ArrayBuffer const arrayBuffer = await chunk.arrayBuffer() // 发送二进制数据 ws.value.send(arrayBuffer) } catch (err) { console.error('发送音频数据失败:', err) error.value = '发送音频数据失败' } } // 高亮显示新识别的文本 const highlightNewText = (text: string) => { // 创建一个临时元素来显示高亮效果 const resultDiv = document.querySelector('.text-output') if (!resultDiv) return // 添加一个临时的高亮样式 resultDiv.classList.add('new-text') // 1秒后移除高亮 setTimeout(() => { resultDiv.classList.remove('new-text') }, 1000) } // 在开始录音时初始化WebSocket const startRecording = async () => { // ... 之前的代码不变 // 初始化WebSocket连接 if (!ws.value || ws.value.readyState !== WebSocket.OPEN) { initWebSocket() // 等待连接建立 await new Promise((resolve) => { const checkConnection = setInterval(() => { if (isConnected.value) { clearInterval(checkConnection) resolve(true) } }, 100) // 5秒超时 setTimeout(() => { clearInterval(checkConnection) resolve(false) }, 5000) }) } // ... 继续录音逻辑 } // 在停止录音时关闭WebSocket const stopRecording = () => { // ... 之前的代码不变 // 发送停止消息 if (ws.value && ws.value.readyState === WebSocket.OPEN) { ws.value.send('stop') } // 关闭WebSocket if (ws.value) { ws.value.close() ws.value = null } } // 添加新的CSS样式 <style scoped> /* ... 之前的样式不变 */ .text-output.new-text { background-color: #e8f5e9; transition: background-color 0.5s ease; } .connection-status { margin-bottom: 10px; font-size: 12px; color: #666; } .connected { color: #4CAF50; } .disconnected { color: #f44336; } </style> // 在模板中添加连接状态显示 <template> <!-- ... 之前的模板不变 --> <div class="connection-status"> 连接状态: <span :class="isConnected ? 'connected' : 'disconnected'"> {{ isConnected ? '已连接' : '未连接' }} </span> </div> <!-- ... 其他模板内容 --> </template>

6. 优化与进阶功能

基础功能实现后,我们可以考虑添加一些优化和进阶功能,让应用更加完善和实用。

6.1 音频预处理优化

原始音频数据可能包含噪音,或者音量不合适。我们可以添加一些预处理步骤来提升识别准确率。

// audio-processor.ts export class AudioProcessor { private audioContext: AudioContext private source: MediaStreamAudioSourceNode | null = null private processor: ScriptProcessorNode | null = null constructor() { this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() } // 处理音频流,添加降噪和增益控制 async processStream(stream: MediaStream, onProcessed: (audioData: Float32Array) => void): Promise<void> { this.source = this.audioContext.createMediaStreamSource(stream) // 创建音频处理节点 const gainNode = this.audioContext.createGain() gainNode.gain.value = 2.0 // 增加音量 // 创建高通滤波器(减少低频噪音) const highPassFilter = this.audioContext.createBiquadFilter() highPassFilter.type = 'highpass' highPassFilter.frequency.value = 80 // 80Hz以下过滤 // 创建低通滤波器(减少高频噪音) const lowPassFilter = this.audioContext.createBiquadFilter() lowPassFilter.type = 'lowpass' lowPassFilter.frequency.value = 8000 // 8kHz以上过滤 // 连接处理链 this.source .connect(highPassFilter) .connect(lowPassFilter) .connect(gainNode) // 创建ScriptProcessorNode处理音频数据 this.processor = this.audioContext.createScriptProcessor(4096, 1, 1) this.processor.onaudioprocess = (event) => { const inputBuffer = event.inputBuffer const channelData = inputBuffer.getChannelData(0) // 应用简单的噪声门 const processedData = this.applyNoiseGate(channelData) // 回调处理后的数据 onProcessed(processedData) } gainNode.connect(this.processor) this.processor.connect(this.audioContext.destination) } // 简单的噪声门实现 private applyNoiseGate(audioData: Float32Array): Float32Array { const threshold = 0.01 // 阈值,低于此值视为噪音 const result = new Float32Array(audioData.length) for (let i = 0; i < audioData.length; i++) { if (Math.abs(audioData[i]) > threshold) { result[i] = audioData[i] } else { result[i] = 0 } } return result } // 停止处理 stop(): void { if (this.processor) { this.processor.disconnect() this.processor = null } if (this.source) { this.source.disconnect() this.source = null } } }

6.2 添加语音命令识别

我们可以扩展应用,让它不仅能转写语音,还能识别特定的语音命令。

// speech-commands.ts export class SpeechCommandRecognizer { private commands: Map<string, () => void> = new Map() private isListening = false // 注册命令 registerCommand(pattern: RegExp | string, handler: () => void): void { const regex = typeof pattern === 'string' ? new RegExp(`^${pattern}$`, 'i') : pattern this.commands.set(regex.source, handler) } // 检查文本是否匹配命令 checkCommand(text: string): boolean { for (const [pattern, handler] of this.commands) { const regex = new RegExp(pattern, 'i') if (regex.test(text.trim())) { handler() return true } } return false } // 开始监听命令 startListening(): void { this.isListening = true } // 停止监听命令 stopListening(): void { this.isListening = false } // 处理识别到的文本 processText(text: string): void { if (this.isListening) { this.checkCommand(text) } } } // 在Vue组件中使用 const commandRecognizer = new SpeechCommandRecognizer() // 注册一些常用命令 commandRecognizer.registerCommand('清空屏幕', () => { transcribedText.value = '' }) commandRecognizer.registerCommand('停止录音', () => { if (isRecording.value) { stopRecording() } }) commandRecognizer.registerCommand('开始录音', () => { if (!isRecording.value) { startRecording() } }) // 在收到识别结果时检查命令 ws.value.onmessage = (event) => { try { const data = JSON.parse(event.data) if (data.type === 'transcription') { const text = data.text // 先检查是否是命令 if (!commandRecognizer.processText(text)) { // 如果不是命令,则显示为普通文本 transcribedText.value += text + ' ' highlightNewText(text) } } } catch (err) { console.error('解析消息失败:', err) } }

6.3 添加多语言支持

Qwen3-ASR支持多种语言,我们可以让用户选择识别语言。

<!-- 在模板中添加语言选择 --> <template> <div class="language-selection"> <label for="language">识别语言:</label> <select id="language" v-model="selectedLanguage" :disabled="isRecording" > <option value="auto">自动检测</option> <option value="zh">中文</option> <option value="en">英语</option> <option value="yue">粤语</option> <option value="ja">日语</option> <option value="ko">韩语</option> <!-- 可以添加更多语言 --> </select> <div class="language-hint" v-if="selectedLanguage === 'auto'"> 自动检测模式可能会稍微降低识别速度 </div> </div> </template> <script setup lang="ts"> const selectedLanguage = ref('auto') // 在发送音频数据时带上语言参数 const sendAudioChunk = async (chunk: Blob) => { if (!ws.value || ws.value.readyState !== WebSocket.OPEN) { console.warn('WebSocket未连接,无法发送音频数据') return } try { const arrayBuffer = await chunk.arrayBuffer() // 创建包含语言信息的数据包 const packet = { type: 'audio_chunk', language: selectedLanguage.value, data: arrayBuffer, timestamp: Date.now() } // 转换为JSON字符串发送 ws.value.send(JSON.stringify(packet)) } catch (err) { console.error('发送音频数据失败:', err) error.value = '发送音频数据失败' } } </script> <style scoped> .language-selection { margin-bottom: 20px; } .language-selection select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; margin-left: 10px; } .language-hint { margin-top: 5px; font-size: 12px; color: #666; font-style: italic; } </style>

7. 部署与性能优化建议

当应用开发完成后,我们需要考虑如何部署和优化性能。这里给出一些实用的建议。

7.1 前端部署优化

  1. 代码分割:使用Vite的动态导入功能,将音频处理等较重的代码拆分成单独的chunk。
// 动态导入音频处理器 const loadAudioProcessor = async () => { const { AudioProcessor } = await import('./audio-processor') return new AudioProcessor() }
  1. 压缩资源:确保音频相关的资源被正确压缩。
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], build: { rollupOptions: { output: { manualChunks: { 'audio-lib': ['./src/audio-processor.ts', './src/speech-commands.ts'] } } } } })
  1. Service Worker缓存:对于频繁使用的音频处理库,可以使用Service Worker进行缓存。

7.2 后端部署建议

  1. 使用Docker容器化:确保环境一致性。
# Dockerfile FROM pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime WORKDIR /app # 安装依赖 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制代码 COPY . . # 下载模型(可以在构建时下载,减少启动时间) RUN python -c "from qwen_asr import Qwen3ASRModel; Qwen3ASRModel.from_pretrained('Qwen/Qwen3-ASR-0.6B', device_map='cpu')" EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
  1. 使用GPU加速:如果使用自建服务,确保有合适的GPU。

  2. 设置合理的并发限制:根据服务器配置调整最大并发数。

7.3 监控与日志

  1. 前端错误监控:使用Sentry或类似工具监控前端错误。

  2. 性能监控:监控识别延迟、准确率等关键指标。

  3. 使用日志服务:记录重要的操作和错误信息。

8. 实际应用场景

这个技术组合可以应用在很多实际场景中,我举几个例子:

在线会议记录:在视频会议中实时生成字幕和会议纪要,支持多语言翻译。

语音笔记应用:用户说话就能自动生成文字笔记,支持语音命令整理和分类。

客服系统:自动记录客户通话内容,分析客户情绪和需求。

教育工具:语言学习应用,实时纠正发音,生成学习报告。

无障碍访问:为听障人士提供实时字幕服务。

每个场景都有一些特殊的考虑点,比如会议记录需要特别关注多人对话的区分,客服系统需要关注敏感信息过滤等。你可以根据具体需求进行调整和扩展。

9. 总结

从头到尾走了一遍,你会发现用Vue.js前端集成Qwen3-ASR-0.6B做实时语音转写,并没有想象中那么难。关键是把整个流程拆解清楚:前端用Web Audio API获取和处理音频,通过WebSocket实时传输,后端用高效的语音识别模型处理,再把结果实时返回。

实际做的时候,我建议先从简单的开始,比如先用阿里云百炼的API快速验证想法,等需求明确了再考虑自建服务。在开发过程中,多关注用户体验,比如添加音频可视化、实时反馈这些细节,能让应用显得更专业。

性能方面,前端的音频预处理、后端的模型优化都很重要。特别是对于实时应用,延迟是影响体验的关键因素,需要不断测试和优化。

最后,这个技术组合的想象空间很大。除了基本的语音转文字,你还可以结合其他AI能力,比如情绪分析、内容总结、多语言翻译等,做出更有价值的产品。如果你在实现过程中遇到问题,或者有更好的想法,欢迎一起交流讨论。


获取更多AI镜像

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

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

在免费的 T4 GPU 上优化小型语言模型

原文&#xff1a;towardsdatascience.com/optimizing-small-language-models-on-a-free-t4-gpu-008c37700d57 https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/89c20ff6b5fa89c36d5f78bb9d4cea28.png 由 Donald Wu 在 Unsplash 拍摄的照片…

作者头像 李华
网站建设 2026/4/14 5:24:36

pdd csr_risk_token/anti_content

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01;部分python代码anti_content_cp execj…

作者头像 李华
网站建设 2026/4/12 3:22:31

解构UEFI固件:UEFITool深度分析与实战指南

解构UEFI固件&#xff1a;UEFITool深度分析与实战指南 【免费下载链接】UEFITool UEFI firmware image viewer and editor 项目地址: https://gitcode.com/gh_mirrors/ue/UEFITool 引言&#xff1a;固件分析的破局者 在现代计算机系统中&#xff0c;UEFI固件扮演着至关…

作者头像 李华
网站建设 2026/4/12 22:32:07

如何让老旧Mac焕发新生:OpenCore工具实现macOS系统兼容的技术探索

如何让老旧Mac焕发新生&#xff1a;OpenCore工具实现macOS系统兼容的技术探索 【免费下载链接】OpenCore-Legacy-Patcher 体验与之前一样的macOS 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 随着苹果系统的不断迭代&#xff0c;许多早期…

作者头像 李华
网站建设 2026/4/12 2:16:51

YaeAchievement:原神成就数据提取与多平台导出工具技术指南

YaeAchievement&#xff1a;原神成就数据提取与多平台导出工具技术指南 【免费下载链接】YaeAchievement 更快、更准的原神成就导出工具 项目地址: https://gitcode.com/gh_mirrors/ya/YaeAchievement YaeAchievement作为一款开源的原神成就管理工具&#xff0c;通过高效…

作者头像 李华
网站建设 2026/3/28 3:03:59

使用GLM-4.7-Flash进行Python入门教学辅助系统开发

使用GLM-4.7-Flash进行Python入门教学辅助系统开发 教Python入门这件事&#xff0c;我做了好几年。最头疼的就是学生问的那些问题&#xff1a;“老师&#xff0c;这个循环怎么写&#xff1f;”“这个错误是什么意思&#xff1f;”“接下来该学什么&#xff1f;”每个问题都要重…

作者头像 李华