語音交互大模型的功能越來越受到重視。訊飛語音聽寫(流式版)為開發者提供了一種高效、準確的語音識別解決方案。本文將基于 Home.vue
、iat_xfyun.js
和 sparkChat.js
這三個文檔,詳細闡述訊飛語音聽寫(流式版)的開發邏輯、涉及的代碼,并提供開發說明、文件結構和分析。
開發說明
頁面示例
功能概述
訊飛語音聽寫(流式版)允許用戶通過麥克風輸入語音,實時將語音轉換為文字。在本項目中,該功能主要應用于聊天界面,用戶可以通過語音輸入問題,系統將語音轉換為文字后發送給后端進行處理。
環境配置
- 開發環境:Vue.js 3.x、Vite
- 依賴庫:
@muguilin/xf-voice-dictation
用于實現訊飛語音聽寫功能
配置步驟
- 安裝依賴:在項目根目錄下執行以下命令安裝
@muguilin/xf-voice-dictation
。
npm install @muguilin/xf-voice-dictation
- 配置訊飛 API 信息:在
iat_xfyun.js
文件中,配置訊飛語音聽寫的 API 信息,包括APPID
、APIKey
和APISecret
。
// 訊飛語音識別配置
const xfConfig = {APPID: '6acb09d5',APIKey: '36fb21a7095db0bb***',APISecret: 'MmNhN2VkY2JkMj****',host: 'iat-api.xfyun.cn',path: '/v2/iat'
}
文件結構
主要文件
Home.vue
:聊天界面組件,包含語音輸入按鈕和語音識別結果顯示區域。iat_xfyun.js
:封裝訊飛語音聽寫功能的工具文件,提供創建語音識別實例的工廠函數。sparkChat.js
:與后端進行 WebSocket 通信的工具文件,負責將語音識別結果發送給后端。
文件關系
Home.vue
組件引入 iat_xfyun.js
中創建的語音識別實例,當用戶點擊語音輸入按鈕時,調用語音識別實例的 start
方法開始錄音。識別結果通過 onTextChange
回調函數返回,將結果顯示在界面上,并通過 sparkChat.js
發送給后端。
開發邏輯
1. 創建語音識別實例
在 iat_xfyun.js
文件中,創建一個工廠函數 createVoiceInstance
,用于創建語音識別實例。該函數接受一個回調對象作為參數,包含 onStatusChange
、onTextChange
和 onError
三個回調函數。
文件 iat_xfyun.js
import { XfVoiceDictation } from '@muguilin/xf-voice-dictation'// 訊飛語音識別配置
const xfConfig = {APPID: '6acb09d5',APIKey: '36fb21a7095db0bb***',APISecret: 'MmNhN2VkY2JkMj****',host: 'iat-api.xfyun.cn',path: '/v2/iat'
}// 創建語音識別實例的工廠函數
export const createVoiceInstance = (callbacks) => {let instance = new XfVoiceDictation({...xfConfig, onWillStatusChange: (oldStatus, newStatus) => {console.log('語音識別狀態變更:', { oldStatus, newStatus }) },onTextChange: (text) => { console.log('語音識別結果:', {text: text,textLength: text ? text.length : 0 })callbacks.onTextChange?.(text)},onError: (error) => {console.error('語音識別錯誤:', error)callbacks.onError?.(error)}})return instance
}
2. 在 Home.vue
中使用語音識別實例
在 Home.vue
組件中,引入 createVoiceInstance
函數,創建語音識別實例,并綁定到語音輸入按鈕的點擊事件上。
<template><!-- ... 其他代碼 ... --><div class="input-bar"><input v-model="inputMessage" type="text" class="apple-input" placeholder="輸入咨詢內容(如:居住證續簽)"@keypress.enter="sendInputMessage(false)" autocapitalize="none" autocomplete="off" spellcheck="false"><svg class="input-icon" :class="{ 'recording': isRecording }" viewBox="0 0 24 24" aria-label="語音麥圖標"@mousedown="checkLoginBeforeAction(startRecording)" @mouseup="checkLoginBeforeAction(stopBtnRecording)"><pathd="M12 15c1.65 0 3-1.35 3-3V6c0-1.65-1.35-3-3-3S9 4.35 9 6v6c0 1.65 1.35 3 3 3zm5.91-4.56c.08.33.13.67.13 1.01 0 2.49-2.01 4.5-4.5 4.5H13v2.5h2c.55 0 1 .45 1 1s-.45 1-1 1H8c-.55 0-1-.45-1-1s.45-1 1-1h2V16H9.59c-2.49 0-4.5-2.01-4.5-4.5 0-.34.05-.68.13-1.01A2.999 2.999 0 0 1 3 9c0-1.66 1.34-3 3-3h3V4c0-.55.45-1 1-1s1 .45 1 1v2h3c1.66 0 3 1.34 3 3 0 1.28-.81 2.36-1.9 2.73l.01-.17z" /></svg><div v-show="isRecording" class="recording-tip">正在錄音...<span class="recording-dots"></span></div><svg class="input-icon sendTxtMsg" viewBox="0 0 24 24" aria-label="發送圖標" @click="sendInputMessage(true)"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg></div><!-- ... 其他代碼 ... -->
</template><script setup>
import { ref, onBeforeUnmount } from 'vue'
import { createVoiceInstance } from '../utils/voice/iat_xfyun'
import { ElMessage } from 'element-plus'const isRecording = ref(false)
const recognizedText = ref('')
let times = null// 創建語音識別實例
const xfVoice = createVoiceInstance({onTextChange: (text) => {if (text && text.length > 0) {const currentTime = Date.now();// 防止操作過于頻繁if (currentTime - lastCallTime.value < 3000) {//只要無這段代碼message.value.push()會加入一條除了本身語言以外只多了問號或一個點的消息//如:你好 //message數組內會有兩條消息://1.你好//2.你好?//如果有這段代碼,message數組內只會有一條消息:console.log('操作過于頻繁,請等待10秒后再試'); return;}lastCallTime.value = currentTime;clearTimeout(times); xfVoice.stop();// 發送識別結果到服務器inputMessage.value = text;user_message.value = text; tmpMsgArr.value = [];sendInputMessage(true);}},onError: (error) => {if (error.includes('WebSocket')) {ElMessage.error('語音識別連接失敗,請檢查網絡');} else if (error.includes('authorization')) {ElMessage.error('語音識別授權失敗,請檢查配置');} else {ElMessage.error('語音識別發生錯誤:' + error);}isRecording.value = false;}
});const startRecording = () => {if (!isRecording.value) {recognizedText.value = '';xfVoice.start();isRecording.value = true;}
}const stopBtnRecording = () => {isRecording.value = false;xfVoice.stop();
}onBeforeUnmount(() => {clearTimeout(times);if (isRecording.value) {xfVoice.stop();}
});
</script>
3. 處理語音識別結果
在 onTextChange
回調函數中,處理語音識別結果。當識別到有效文本時,停止錄音,并將識別結果發送給后端。
4. 錯誤處理
在 onError
回調函數中,處理語音識別過程中可能出現的錯誤,如網絡連接失敗、授權失敗等,并通過 ElMessage
提示用戶。
代碼分析
iat_xfyun.js
- 優點:將訊飛語音聽寫功能封裝在一個獨立的文件中,提高了代碼的可維護性和可復用性。
- 缺點:配置信息硬編碼在文件中,不利于配置的修改和管理。可以考慮將配置信息提取到環境變量中。
Home.vue
- 優點:在組件中使用語音識別實例,實現了語音輸入功能的集成。通過回調函數處理識別結果和錯誤,代碼結構清晰。
- 缺點:語音輸入按鈕的樣式和交互邏輯可以進一步優化,提高用戶體驗。
sparkChat.js
// 訊飛星火大模型WebSocket通信模塊
import axios from 'axios'
import getSparkConfig from '../sparkConfig'class SparkChatService {constructor(callbacks) {this.websocket = nullthis.isReconnecting = falsethis.reconnectAttempts = 0this.MAX_RECONNECT_ATTEMPTS = 3this.RECONNECT_INTERVAL = 2000// 獲取配置const sparkConfig = getSparkConfig()this.APPID = sparkConfig.APPIDthis.APISecret = sparkConfig.APISecretthis.APIKey = sparkConfig.APIKeythis.host = sparkConfig.hostthis.path = sparkConfig.paththis.sparkBaseUrl = sparkConfig.getWebSocketUrl()// 回調函數this.callbacks = callbacks || {}}// 生成鑒權URL所需的日期getAuthorizationDate() {return new Date().toUTCString()}// 生成鑒權URLasync getAuthUrl() {const date = this.getAuthorizationDate()const tmp = `host: ${this.host}\ndate: ${date}\nGET ${this.path} HTTP/1.1`const encoder = new TextEncoder()const key = await window.crypto.subtle.importKey('raw',encoder.encode(this.APISecret),{ name: 'HMAC', hash: 'SHA-256' },false,['sign'])const signature = await window.crypto.subtle.sign('HMAC',key,encoder.encode(tmp))const signatureBase64 = btoa(String.fromCharCode(...new Uint8Array(signature)))const authorization_origin = `api_key="${this.APIKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureBase64}"`const authorization = btoa(authorization_origin)return `${this.sparkBaseUrl}?authorization=${encodeURIComponent(authorization)}&date=${encodeURIComponent(date)}&host=${encodeURIComponent(this.host)}`}// 檢查WebSocket連接狀態checkWebSocketConnection() {return this.websocket && this.websocket.readyState === WebSocket.OPEN}// 重連WebSocketasync reconnectWebSocket() {if (this.isReconnecting || this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) returnthis.isReconnecting = truethis.reconnectAttempts++console.log(`嘗試重新連接WebSocket (第${this.reconnectAttempts}次)...`)try {await this.connect()this.isReconnecting = falsethis.reconnectAttempts = 0console.log('WebSocket重連成功')} catch (error) {console.error('WebSocket重連失敗:', error)this.isReconnecting = falseif (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {setTimeout(() => this.reconnectWebSocket(), this.RECONNECT_INTERVAL)} else {console.error('WebSocket重連次數達到上限')this.callbacks.onError?.('網絡連接異常,請刷新頁面重試')}}}// 建立WebSocket連接async connect() {try {const url = await this.getAuthUrl()this.websocket = new WebSocket(url)this.websocket.onopen = () => {console.log('WebSocket連接已建立')this.isReconnecting = falsethis.reconnectAttempts = 0this.callbacks.onOpen?.()}this.websocket.onmessage = (event) => {const response = JSON.parse(event.data)if (response.header.code === 0) {if (response.payload.choices.text[0].content) {const content = response.payload.choices.text[0].content.replace(/\r?\n/g, '')this.callbacks.onMessage?.(content)}if (response.header.status === 2) {this.callbacks.onComplete?.()}} else {this.callbacks.onError?.(`抱歉,發生錯誤:${response.header.message}`)}}this.websocket.onerror = (error) => {console.error('WebSocket錯誤:', error)if (!this.isReconnecting) {this.reconnectWebSocket()}this.callbacks.onError?.(error)}this.websocket.onclose = () => {console.log('WebSocket連接已關閉')if (!this.isReconnecting) {this.reconnectWebSocket()}this.callbacks.onClose?.()}} catch (error) {console.error('連接WebSocket失敗:', error)throw error}}// 發送消息async sendMessage(message) {if (!this.checkWebSocketConnection()) {try {await this.reconnectWebSocket()} catch (error) {console.error('重連失敗,無法發送消息')throw new Error('網絡連接異常,請稍后重試')}}const requestData = {header: {app_id: this.APPID,uid: 'user1'},parameter: {chat: {domain: 'generalv3',temperature: 0.5,max_tokens: 4096}},payload: {message: {text: [{ role: 'user', content: message }]}}}try {this.websocket.send(JSON.stringify(requestData))} catch (error) {console.error('發送消息失敗:', error)throw new Error('發送消息失敗,請重試')}}// 關閉連接close() {if (this.websocket) {this.websocket.close()}}
}export default SparkChatService
雖然該文件主要負責與后端進行 WebSocket 通信,但在語音聽寫功能中起到了將識別結果發送給后端的重要作用。可以考慮對該文件進行進一步的封裝,提高代碼的可維護性。
AIChat
語音聽寫(流式版)WebAPI 文檔幫助文檔:
https://www.xfyun.cn/doc/asr/voicedictation/API.html#%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
缺點:
語音聽寫流式接口,用于
1分鐘
內的即時語音轉文字技術,支持實時返回識別結果,達到一邊上傳音頻一邊獲得識別文本的效果。
整個會話時長最多持續60s,或者超過10s
未發送數據,服務端會主動斷開連接