一、引言
在現代的 Web 應用開發中,語音合成技術為用戶提供了更加便捷和人性化的交互體驗。訊飛語音合成(流式版)以其高效、穩定的性能,成為了眾多開發者的首選。本文將詳細介紹在 Home.vue 文件中實現訊飛語音合成(流式版)的開發邏輯、涉及的代碼以及相關的環境配置,幫助大家更好地掌握這一技術。
二、開發環境配置
在 Home.vue 文件中,我們需要引入并配置訊飛語音合成的相關參數。在代碼中,我們可以看到如下配置:
2-1, sparkCOnfig.js
// 訊飛星火大模型WebSocket配置
const getSparkConfig = () => {const config = {APPID: '6acb09d5',APISecret: 'MmNhN2VkY2JkMjQyODYyNzBhZDVhYjgz',APIKey: '36fb21a7095db0bb6ff2ac928e14a8e7',host: 'spark-api.xf-yun.com',path: '/v3.1/chat',};// 驗證配置參數的有效性const validateConfig = () => {const requiredFields = ['APPID', 'APISecret', 'APIKey', 'host', 'path'];for (const field of requiredFields) {if (!config[field]) {throw new Error(`缺少必要的配置參數: ${field}`);}}};// 獲取完整的WebSocket URLconst getWebSocketUrl = () => {validateConfig();return `wss://${config.host}${config.path}`;};return {...config,getWebSocketUrl,};
};export default getSparkConfig;
2-2, Home.vue
import TTSRecorder from '../utils/voice/onlineTTS'
const ttsConfig = {app_id: '6acb09d5',api_secret: 'MmNhN2VkY2Jk*',api_key: '36fb21a7095*'
};
TTSRecorder.init(ttsConfig);
這里的 app_id 、 api_secret 和 api_key 是我們在訊飛開放平臺申請的密鑰,用于身份驗證和訪問控制。通過調用 TTSRecorder.init(ttsConfig) 方法,我們將這些配置信息傳遞給 TTSRecorder 實例,以便后續使用。
三、開發邏輯分析
3.1 初始化階段
在 Home.vue 的 onMounted 生命周期鉤子中,我們首先調用 fetchWelcomeMessage 方法獲取歡迎消息。在獲取到消息后,我們初始化 3D 場景,并連接 WebSocket 服務。同時,我們還調用 ttsplaybtn 方法對歡迎消息進行語音播報:
onMounted(() => {fetchWelcomeMessage().then(() => {initGlbScene(container.value, objpath.value, objpath_create_time.value);animate();window.addEventListener('resize', handleResize);connectWebSocket();nextTick(() => {setTimeout(() => {ttsplaybtn(response.data.data.welcome, 0);}, 3000);});});scrollToBottom();
});
### 3.2 語音播放功能實現
ttsplaybtn 方法是實現語音播放的核心函數。在這個方法中,我們根據當前的播放狀態進行不同的處理:```vue
const ttsplaybtn = async (content, index) => {try {console.log('準備播放文本,當前狀態:', { content, index, isPlaying: isPlaying.value, currentPlayingIndex: currentPlayingIndex.value });if (isPlaying.value && currentPlayingIndex.value === index) {ttsRecorder.value.stop();stopAnimations();isPlaying.value = false;currentPlayingIndex.value = null;return;}if (ttsRecorder.value && isPlaying.value) {ttsRecorder.value.stop();stopAnimations();isPlaying.value = false;currentPlayingIndex.value = null;}ttsRecorder.value = new TTSRecorder({voiceName: 'xiaoyan',tte: 'UTF8',text: content,onEnd: () => {console.log('音頻播放結束');isPlaying.value = false;currentPlayingIndex.value = null;stopAnimations();}});currentPlayingIndex.value = index;isPlaying.value = true;console.log('開始播放文本:', content);startAnimations();await ttsRecorder.value.start();} catch (error) {console.error('語音播放出錯:', error);isPlaying.value = false;currentPlayingIndex.value = null;stopAnimations();ElMessage.error('語音播放失敗: ' + error.message);}
};
當用戶點擊播放按鈕時,我們首先檢查當前是否有其他消息正在播放。如果有,則停止當前播放的消息。然后,我們創建一個新的 TTSRecorder 實例,并傳入要播放的文本和相關配置。最后,我們調用 start 方法開始語音播放。sparkChat.js
// 訊飛星火大模型WebSocket通信模塊
import axios from ‘axios’
import getSparkConfig from ‘…/sparkConfig’
class SparkChatService {
constructor(callbacks) {
this.websocket = null
this.isReconnecting = false
this.reconnectAttempts = 0
this.MAX_RECONNECT_ATTEMPTS = 3
this.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()
}// 生成鑒權URL
async 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
}// 重連WebSocket
async 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
···
3.3 播放狀態管理
為了實現播放和暫停功能的切換,我們使用 isPlaying 和 currentPlayingIndex 兩個響應式變量來管理播放狀態。在 ttsplaybtn 方法中,我們根據這兩個變量的值來判斷當前的播放狀態,并進行相應的處理。同時,在模板中,我們根據 isPlaying 的值來顯示不同的圖標:
<svg class="input-icon tts-btn" viewBox="0 0 24 24" aria-label="播報語音圖標" @click="ttsplaybtn(message.content, index)"><path v-if="currentPlayingIndex === index && isPlaying" d="M6 4h4v16H6zM14 4h4v16h-4z" fill="currentColor"/><path v-else d="M12 3c-4.97 0-9 4.03-9 9v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-4v8h3c1.66 0 3-1.34 3-3v-7c0-4.97-4.03-9-9-9z" fill="currentColor"/>
</svg>
## 四、文件結構和代碼分析
### 4.1 文件結構
在項目中,與訊飛語音合成相關的文件主要包括:- Home.vue :主頁面文件,包含語音播放按鈕和相關邏輯。
- TTSRecorder.js :封裝了訊飛語音合成的核心功能,如初始化、開始播放、停止播放等。
- sparkConfig.js :配置文件,包含訊飛語音合成的 API 地址和密鑰信息。
### 4.2 代碼分析
- Home.vue :在這個文件中,我們主要處理用戶的交互事件,如點擊播放按鈕、停止播放等。同時,我們還管理播放狀態,并根據狀態更新界面。
- TTSRecorder.js :這個文件封裝了與訊飛語音合成服務的交互邏輯。它接收配置信息,并提供了 start 和 stop 方法來控制語音播放。
- sparkConfig.js :該文件存儲了訊飛語音合成的 API 地址和密鑰信息,確保我們能夠正確地與服務進行通信。