讯飞语音合成(流式版)语音专业版高质量的分析
一、引言
在现代的 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 URL
const 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.APPID
this.APISecret = sparkConfig.APISecret
this.APIKey = sparkConfig.APIKey
this.host = sparkConfig.host
this.path = sparkConfig.path
this.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) return
this.isReconnecting = true
this.reconnectAttempts++
console.log(`尝试重新连接WebSocket (第${this.reconnectAttempts}次)...`)
try {
await this.connect()
this.isReconnecting = false
this.reconnectAttempts = 0
console.log('WebSocket重连成功')
} catch (error) {
console.error('WebSocket重连失败:', error)
this.isReconnecting = false
if (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 = false
this.reconnectAttempts = 0
this.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 地址和密钥信息,确保我们能够正确地与服务进行通信。