一、引言
在现代的 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 地址和密钥信息,确保我们能够正确地与服务进行通信。