From 309b1318a7b14e12c6ae8d388d43fb6ef3a4cbe1 Mon Sep 17 00:00:00 2001 From: liqupan Date: Sat, 6 Dec 2025 22:42:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TEST_MODE_GUIDE.md | 269 +++++++++++++++++ src/components/ChatBox.vue | 487 +++++++++++++++++++++++++++++- src/static/output.pcm | Bin 0 -> 101674 bytes src/utils/config.js | 33 +- src/utils/voiceStreamWebSocket.js | 310 +++++++++++++++++++ 如何准备测试音频数据.md | 262 ++++++++++++++++ 测试数据快速生成命令.md | 168 +++++++++++ 7 files changed, 1523 insertions(+), 6 deletions(-) create mode 100644 TEST_MODE_GUIDE.md create mode 100644 src/static/output.pcm create mode 100644 src/utils/voiceStreamWebSocket.js create mode 100644 如何准备测试音频数据.md create mode 100644 测试数据快速生成命令.md diff --git a/TEST_MODE_GUIDE.md b/TEST_MODE_GUIDE.md new file mode 100644 index 0000000..2d01d6e --- /dev/null +++ b/TEST_MODE_GUIDE.md @@ -0,0 +1,269 @@ +# 🧪 WebSocket语音流测试模式使用指南 + +## 概述 + +测试模式允许你在不使用实际录音功能的情况下测试WebSocket语音流式对话功能。这对于在不支持录音的环境(如某些微信小程序开发环境)或需要使用特定测试数据的场景非常有用。 + +## 启用测试模式 + +在 `webUI/src/utils/config.js` 中配置: + +```javascript +TEST_MODE: { + enabled: true, // 设置为 true 启用测试模式,false 使用正常录音 + audioDuration: 3, // 测试音频时长(秒) + sampleRate: 16000, // 采样率(Hz) + channels: 1, // 声道数(1=单声道,2=立体声) + bitDepth: 16 // 位深度(8或16) +} +``` + +## 测试模式特性 + +### 启用时(enabled: true) +- ✅ 显示 "🧪 发送测试音频" 按钮 +- ✅ 禁用自动录音功能 +- ✅ 点击按钮发送假数据到WebSocket +- ✅ 页面提示 "测试模式:点击发送假数据" + +### 禁用时(enabled: false) +- ✅ 隐藏测试按钮 +- ✅ 启用正常录音功能 +- ✅ 自动模式下自动开始录音 + +## 使用步骤 + +1. **启用测试模式** + ```javascript + // 在 config.js 中 + TEST_MODE: { enabled: true } + ``` + +2. **进入语音模式** + - 点击输入框右侧的 🎧 图标切换到语音模式 + +3. **发送测试数据** + - 点击 "🧪 发送测试音频" 按钮 + - 系统会自动生成假PCM数据并发送到WebSocket服务器 + +4. **观察结果** + - STT识别结果会显示在聊天界面 + - AI回复会实时流式显示 + - TTS音频会实时播放 + +## PCM音频数据格式说明 + +### 当前生成的测试数据格式 + +代码会自动生成符合以下规格的PCM数据: + +```javascript +{ + 采样率: 16000 Hz, // 16kHz,语音常用采样率 + 位深度: 16 bit, // 16位有符号整数 + 声道数: 1, // 单声道 + 字节序: Little Endian, // 小端序 + 时长: 3秒, + 数据大小: 96000 bytes // = 16000 * 3 * 2 +} +``` + +### 测试数据生成原理 + +代码生成的是一个模拟人声的音频信号: +- **基频**: 200Hz(人声范围) +- **泛音**: 包含2次和3次谐波,让声音更丰富 +- **噪声**: 添加轻微随机噪声,模拟呼吸音 +- **包络**: 淡入淡出效果,让声音更自然 + +### 如果需要使用真实音频文件 + +如果你想使用真实的音频文件进行测试,需要先将音频转换为PCM格式: + +#### 1. 使用FFmpeg转换 + +```bash +# 从MP3/WAV转换为PCM +ffmpeg -i input.mp3 -f s16le -acodec pcm_s16le -ar 16000 -ac 1 output.pcm + +# 参数说明: +# -f s16le: 输出格式为16位小端序PCM +# -acodec pcm_s16le: 使用16位PCM编码 +# -ar 16000: 采样率16kHz +# -ac 1: 单声道 +``` + +#### 2. 在代码中使用真实PCM文件 + +修改 `ChatBox.vue` 中的 `generateTestPCMAudio()` 函数: + +```javascript +// 方案A: 直接读取PCM文件 +const generateTestPCMAudio = async () => { + // #ifdef MP-WEIXIN + const fs = uni.getFileSystemManager(); + return new Promise((resolve, reject) => { + fs.readFile({ + filePath: '/static/test_audio.pcm', // 你的PCM文件路径 + success: (res) => { + resolve(res.data); // res.data 是 ArrayBuffer + }, + fail: reject + }); + }); + // #endif +}; + +// 方案B: 从base64字符串加载 +const TEST_PCM_BASE64 = "你的base64编码的PCM数据..."; +const generateTestPCMAudio = () => { + return uni.base64ToArrayBuffer(TEST_PCM_BASE64); +}; +``` + +#### 3. 获取音频的base64编码 + +```bash +# Linux/Mac +base64 output.pcm > output_base64.txt + +# Windows PowerShell +[Convert]::ToBase64String([IO.File]::ReadAllBytes("output.pcm")) > output_base64.txt +``` + +然后将base64字符串复制到代码中。 + +## PCM数据格式要求 + +### 必须满足的要求 + +✅ **格式**: 原始PCM数据(无文件头) +✅ **采样率**: 16000 Hz(后端STT服务要求) +✅ **位深度**: 16 bit +✅ **声道数**: 1(单声道) +✅ **字节序**: Little Endian(小端序) +✅ **编码**: 有符号整数(Signed Integer) + +### 数据大小计算 + +``` +数据大小(字节)= 采样率 × 时长(秒)× 声道数 × (位深度/8) + +示例:3秒的音频 += 16000 × 3 × 1 × 2 += 96000 bytes += 93.75 KB +``` + +## 注意事项 + +⚠️ **测试数据限制** +- 生成的测试数据是简单的合成音,不包含真实语音内容 +- STT识别结果可能为空或乱码(取决于STT服务对合成音的处理) +- 如需测试真实识别,请使用真实录音的PCM数据 + +⚠️ **WebSocket模式要求** +- 测试模式需要配合WebSocket模式使用 +- 确保在 `ChatBox.vue` 中设置 `isWebSocketMode.value = true` +- 确保WebSocket服务器正在运行 + +⚠️ **性能考虑** +- 较长的音频文件会占用更多内存 +- 建议测试音频时长不超过5秒 +- 如需长时间测试,可以循环发送短音频 + +## 调试技巧 + +### 查看浏览器控制台 + +测试模式会输出详细日志: + +``` +[TestMode] 生成测试PCM音频: { + sampleRate: 16000, + duration: "3秒", + channels: 1, + bitDepth: "16位", + dataSize: "96000 bytes", + frequency: "200 Hz" +} +[TestMode] 开始发送测试音频数据 +[VoiceMode] 测试音频已通过WebSocket发送 +``` + +### 验证数据格式 + +在浏览器控制台检查: + +```javascript +// 检查生成的数据 +const data = generateTestPCMAudio(); +console.log('数据类型:', data instanceof ArrayBuffer); +console.log('数据大小:', data.byteLength, 'bytes'); +console.log('预期大小:', 16000 * 3 * 2, 'bytes'); +``` + +## 故障排除 + +### 问题1: 点击按钮无反应 +- 检查是否已进入语音模式(点击🎧图标) +- 检查是否启用了WebSocket模式 +- 查看浏览器控制台是否有错误 + +### 问题2: STT识别结果为空 +- 这是正常的,合成音不包含真实语音内容 +- 使用真实PCM录音文件进行测试 + +### 问题3: WebSocket连接失败 +- 检查后端服务是否运行 +- 检查 `config.js` 中的 `WS_BASE_URL` 配置 +- 检查网络连接和防火墙设置 + +### 问题4: 音频无法播放 +- 检查返回的TTS音频格式 +- 查看浏览器控制台的音频播放日志 +- 确认播放权限已授予 + +## 示例代码 + +### 完整的测试流程示例 + +```javascript +// 1. 配置测试模式 +// config.js +export const API_CONFIG = { + TEST_MODE: { + enabled: true, + audioDuration: 3, + sampleRate: 16000, + channels: 1, + bitDepth: 16 + } +}; + +// 2. 进入语音模式 +toggleVoiceMode(); // 点击🎧图标 + +// 3. 发送测试数据 +sendTestAudio(); // 点击测试按钮 + +// 4. 观察回调 +voiceStreamWs.on('sttResult', (text) => { + console.log('识别结果:', text); +}); + +voiceStreamWs.on('sentence', (sentence) => { + console.log('完整句子:', sentence); +}); + +voiceStreamWs.on('audioChunk', (audioData) => { + console.log('收到音频:', audioData.byteLength, 'bytes'); +}); +``` + +## 总结 + +测试模式为你提供了一个便捷的方式来测试WebSocket语音流功能,无需实际录音设备。通过生成符合格式的假PCM数据,你可以验证整个语音对话流程的正确性。 + +如有任何问题,请检查浏览器控制台的日志输出,那里会有详细的调试信息。 + diff --git a/src/components/ChatBox.vue b/src/components/ChatBox.vue index 1488e0c..f59d649 100644 --- a/src/components/ChatBox.vue +++ b/src/components/ChatBox.vue @@ -71,6 +71,14 @@ + + + + 测试模式:点击发送假数据 + + @@ -261,6 +269,12 @@ import { getUnreadMessages, clearUnreadMessages } from '@/utils/unreadMessages.js'; +// 🎙️ 新增:导入WebSocket语音流模块 +import VoiceStreamWebSocket from '@/utils/voiceStreamWebSocket.js'; +import { getWsUrl, API_CONFIG } from '@/utils/config.js'; + +// 🧪 测试模式配置 +const isTestMode = ref(API_CONFIG.TEST_MODE.enabled); // 获取组件实例 const instance = getCurrentInstance(); @@ -371,6 +385,15 @@ const vadConfig = ref({ silenceDuration: 1500 // 静音判定时间 (ms) }); +// 🎙️ WebSocket 语音流相关 +const voiceStreamWs = ref(null); +const isWebSocketMode = ref(true); // 是否使用WebSocket模式(默认false使用HTTP) +const audioPlayQueue = ref([]); // 音频播放队列 +const isPlayingAudio = ref(false); // 是否正在播放音频 +const currentSentence = ref(''); // 当前正在显示的句子 +const currentChatSessionId = ref(null); // 对话会话ID(用于历史记录) +const currentVoiceTemplateId = ref(null); // 语音模式使用的模板ID + // AI配置 const availableTemplates = ref([]); const currentModelId = ref(null); @@ -397,6 +420,17 @@ const voiceStateText = computed(() => { return stateMap[voiceState.value] || '准备就绪'; }); +// 🧪 测试按钮文本 +const testButtonText = computed(() => { + const stateMap = { + idle: '🧪 发送测试音频', + listening: '⏺️ 录音中...', + thinking: '⏳ 处理中...', + speaking: '🔊 回复中...' + }; + return stateMap[voiceState.value] || '🧪 发送测试音频'; +}); + // 根据音量动态生成声纹高度 const voiceWaveStyles = computed(() => { const barCount = 40; @@ -602,6 +636,21 @@ onBeforeUnmount(() => { voiceState.value = 'idle'; currentVolume.value = 0; + // 6. 关闭WebSocket连接 + if (voiceStreamWs.value) { + try { + voiceStreamWs.value.close(); + voiceStreamWs.value = null; + console.log('🔌 已关闭WebSocket连接'); + } catch (error) { + console.warn('关闭WebSocket失败:', error); + } + } + + // 7. 清空音频播放队列 + audioPlayQueue.value = []; + isPlayingAudio.value = false; + console.log('✅ 资源清理完成'); }); @@ -1490,7 +1539,7 @@ const clearContext = async () => { }; // 🎧 切换语音模式 -const toggleVoiceMode = () => { +const toggleVoiceMode = async () => { isVoiceMode.value = !isVoiceMode.value; if (isVoiceMode.value) { @@ -1498,14 +1547,116 @@ const toggleVoiceMode = () => { voiceState.value = 'idle'; isVoiceModeInterrupted.value = false; // 重置中断标志 + // 🎙️ 建立WebSocket连接(如果启用WebSocket模式) + if (isWebSocketMode.value) { + try { + const wsUrl = getWsUrl(API_CONFIG.WS_ENDPOINTS.VOICE_STREAM); + voiceStreamWs.value = new VoiceStreamWebSocket(wsUrl); + + // 设置事件监听器 + voiceStreamWs.value.on('connected', () => { + console.log('[VoiceMode] WebSocket已连接'); + }); + + voiceStreamWs.value.on('sttResult', (text) => { + console.log('[VoiceMode] STT结果:', text); + addMessage('user', text); + currentSentence.value = ''; + }); + + voiceStreamWs.value.on('llmToken', (token) => { + // 可选:显示LLM输出 + console.log('[VoiceMode] LLM Token:', token); + }); + + voiceStreamWs.value.on('sentence', (sentence) => { + console.log('[VoiceMode] 完整句子:', sentence); + currentSentence.value = sentence; + addMessage('ai', sentence); + }); + + voiceStreamWs.value.on('audioChunk', (audioData) => { + // 将音频数据加入播放队列 + audioPlayQueue.value.push(audioData); + processAudioQueue(); + }); + + voiceStreamWs.value.on('complete', () => { + console.log('[VoiceMode] 对话完成,等待播放队列清空'); + + // 等待播放队列清空后再设置idle + const checkQueueEmpty = () => { + if (audioPlayQueue.value.length === 0 && !isPlayingAudio.value) { + console.log('[VoiceMode] 播放队列已清空,切换到idle状态'); + voiceState.value = 'idle'; + + // 如果是自动模式,重新开始录音 + if (isAutoVoiceMode.value && isVoiceMode.value) { + console.log('↺ 自动模式:重新开始监听'); + startVoiceRecording(); + } + } else { + console.log('[VoiceMode] 队列还有数据或正在播放,100ms后再检查'); + setTimeout(checkQueueEmpty, 100); + } + }; + + checkQueueEmpty(); + }); + + voiceStreamWs.value.on('error', (error) => { + console.error('[VoiceMode] 错误:', error); + uni.showToast({ + title: '语音处理失败: ' + error, + icon: 'none', + duration: 2000 + }); + voiceState.value = 'idle'; + }); + + voiceStreamWs.value.on('disconnected', () => { + console.log('[VoiceMode] WebSocket已断开'); + }); + + // 复用文字对话的 conversationId,保持历史记录一致 + // 如果还没有 conversationId,会在initializeConversation中生成 + if (!conversationId.value) { + await initializeConversation(); + } + + // 使用当前角色的templateId(如果有的话) + currentVoiceTemplateId.value = currentCharacter.value.templateId || currentCharacter.value.roleId || null; + + console.log('[VoiceMode] 连接参数 - SessionId:', conversationId.value, 'TemplateId:', currentVoiceTemplateId.value); + + // 连接WebSocket(传递与文字对话相同的sessionId和templateId) + await voiceStreamWs.value.connect( + conversationId.value, + currentVoiceTemplateId.value, + userStore.token, + userStore.userId + ); + + } catch (error) { + console.error('[VoiceMode] WebSocket连接失败:', error); + uni.showToast({ + title: 'WebSocket连接失败', + icon: 'none', + duration: 2000 + }); + isVoiceMode.value = false; + return; + } + } + uni.showToast({ - title: '已切换到语音模式', + title: '已切换到语音模式' + (isWebSocketMode.value ? '(实时流式)' : '') + (isTestMode.value ? '(测试)' : ''), icon: 'none', duration: 1500 }); - // 如果是自动模式,直接开始监听 - if (isAutoVoiceMode.value) { + // 如果是自动模式且非测试模式,直接开始监听 + if (isAutoVoiceMode.value && !isTestMode.value) { startVoiceRecording(); } } else { @@ -1548,6 +1699,22 @@ const toggleVoiceMode = () => { voiceState.value = 'idle'; currentVolume.value = 0; + // 6. 关闭WebSocket连接 + if (voiceStreamWs.value) { + try { + voiceStreamWs.value.close(); + voiceStreamWs.value = null; + console.log('🔌 已关闭WebSocket连接'); + } catch (error) { + console.warn('关闭WebSocket失败:', error); + } + } + + // 7. 清空音频播放队列 + audioPlayQueue.value = []; + isPlayingAudio.value = false; + currentSentence.value = ''; + uni.showToast({ title: '已切换到文本模式', icon: 'none', @@ -1763,7 +1930,38 @@ const handleVoiceModeMessage = async (filePath) => { try { console.log('🎧 语音模式:开始处理录音文件', filePath); - // 调用后端voice-chat接口 + // 🎙️ WebSocket模式:通过WebSocket发送音频 + if (isWebSocketMode.value && voiceStreamWs.value && voiceStreamWs.value.isConnected) { + // 读取音频文件并通过WebSocket发送 + const fs = uni.getFileSystemManager(); + console.log(wx.env.USER_DATA_PATH) + fs.readFile({ + filePath: filePath, + success: (res) => { + console.log('[VoiceMode] 读取音频文件成功,大小:', res.data.byteLength); + + // 发送音频数据 + const success = voiceStreamWs.value.sendAudio(res.data); + if (!success) { + throw new Error('发送音频数据失败'); + } + + console.log('[VoiceMode] 音频已通过WebSocket发送'); + }, + fail: (err) => { + console.error('[VoiceMode] 读取音频文件失败:', err); + uni.showToast({ + title: '读取音频文件失败', + icon: 'none', + duration: 2000 + }); + voiceState.value = 'idle'; + } + }); + return; + } + + // HTTP模式:调用后端voice-chat接口 const result = await voiceAPI.voiceChat(filePath, { sessionId: conversationId.value, // 使用当前会话ID保持上下文 modelId: currentCharacter.value.modelId || 10, @@ -1851,6 +2049,247 @@ const handleVoiceModeMessage = async (filePath) => { } }; +// 🧪 加载测试PCM音频数据 +const generateTestPCMAudio = () => { + return new Promise((resolve, reject) => { + const config = API_CONFIG.TEST_MODE; + + // 方式1: 优先使用base64数据 + if (config.testAudioBase64 && config.testAudioBase64.trim() !== '') { + try { + console.log('[TestMode] 从base64加载测试音频数据'); + const arrayBuffer = uni.base64ToArrayBuffer(config.testAudioBase64); + console.log('[TestMode] 测试音频数据已加载:', { + dataSize: arrayBuffer.byteLength + ' bytes', + expectedFormat: '16000Hz, 16bit, 单声道, Little Endian' + }); + resolve(arrayBuffer); + } catch (error) { + console.error('[TestMode] base64解码失败:', error); + reject(new Error('base64数据解码失败')); + } + return; + } + + // 方式2: 从文件路径读取 + if (config.testAudioPath && config.testAudioPath.trim() !== '') { + // #ifdef MP-WEIXIN + const fs = uni.getFileSystemManager(); + const testAudioPath = wx.env.USER_DATA_PATH + "/output.pcm"; + fs.readFile({ + filePath: testAudioPath, + success: (res) => { + console.log('[TestMode] 从文件加载测试音频数据:', config.testAudioPath); + console.log('[TestMode] 文件大小:', res.data.byteLength + ' bytes'); + resolve(res.data); + }, + fail: (err) => { + console.error('[TestMode] 读取音频文件失败:', err); + reject(new Error('读取音频文件失败: ' + JSON.stringify(err))); + } + }); + // #endif + + // #ifndef MP-WEIXIN + reject(new Error('文件读取仅支持微信小程序')); + // #endif + return; + } + + // 如果两个都没有配置,提示错误 + reject(new Error('请在config.js中配置 testAudioBase64 或 testAudioPath')); + }); +}; + +// 🧪 发送测试音频数据 +const sendTestAudio = async () => { + if (!isVoiceMode.value) { + uni.showToast({ + title: '请先进入语音模式', + icon: 'none', + duration: 2000 + }); + return; + } + + if (voiceState.value === 'thinking' || voiceState.value === 'speaking') { + uni.showToast({ + title: '正在处理中,请稍候', + icon: 'none', + duration: 1500 + }); + return; + } + + try { + console.log('[TestMode] 开始发送测试音频数据'); + voiceState.value = 'listening'; + + // 加载测试PCM数据 + const testAudioData = await generateTestPCMAudio(); + + // 模拟VAD检测(等待一段时间模拟说话) + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 切换到思考状态 + voiceState.value = 'thinking'; + + // WebSocket模式:直接发送ArrayBuffer + if (isWebSocketMode.value && voiceStreamWs.value && voiceStreamWs.value.isConnected) { + const success = voiceStreamWs.value.sendAudio(testAudioData); + if (!success) { + throw new Error('发送音频数据失败'); + } + console.log('[TestMode] 测试音频已通过WebSocket发送'); + } else { + // HTTP模式:需要先保存为文件,然后调用接口 + // #ifdef MP-WEIXIN + const fs = uni.getFileSystemManager(); + const filePath = `${wx.env.USER_DATA_PATH}/test_audio_${Date.now()}.pcm`; + + // 将ArrayBuffer转为base64 + const base64 = uni.arrayBufferToBase64(testAudioData); + + await new Promise((resolve, reject) => { + fs.writeFile({ + filePath: filePath, + data: base64, + encoding: 'base64', + success: () => { + console.log('[TestMode] 测试音频文件已保存:', filePath); + // 调用现有的处理逻辑 + handleVoiceModeMessage(filePath); + resolve(); + }, + fail: (err) => { + console.error('[TestMode] 保存测试音频文件失败:', err); + reject(err); + } + }); + }); + // #endif + + // #ifndef MP-WEIXIN + uni.showToast({ + title: 'HTTP模式仅支持微信小程序', + icon: 'none', + duration: 2000 + }); + voiceState.value = 'idle'; + // #endif + } + + } catch (error) { + console.error('[TestMode] 发送测试音频失败:', error); + uni.showToast({ + title: '发送测试音频失败', + icon: 'none', + duration: 2000 + }); + voiceState.value = 'idle'; + } +}; + +// 🎙️ 处理音频播放队列(WebSocket流式音频) +const processAudioQueue = async () => { + if (isPlayingAudio.value || audioPlayQueue.value.length === 0) { + return; + } + + isPlayingAudio.value = true; + voiceState.value = 'speaking'; + + // #ifdef MP-WEIXIN + try { + // 开始播放队列中的音频 + while (audioPlayQueue.value.length > 0 && isVoiceMode.value) { + const audioData = audioPlayQueue.value.shift(); + + // 将ArrayBuffer转换为临时文件(MP3格式) + const fs = uni.getFileSystemManager(); + const filePath = `${wx.env.USER_DATA_PATH}/stream_audio_${Date.now()}.mp3`; + + await new Promise((resolve, reject) => { + // 将ArrayBuffer转为base64 + const base64 = uni.arrayBufferToBase64(audioData); + + fs.writeFile({ + filePath: filePath, + data: base64, + encoding: 'base64', + success: () => { + console.log('[AudioQueue] MP3文件写入成功:', filePath); + + // 播放音频 + if (audioContext.value) { + audioContext.value.destroy(); + } + + uni.setInnerAudioOption({ + obeyMuteSwitch: false, + speakerOn: true + }); + + audioContext.value = uni.createInnerAudioContext(); + audioContext.value.autoplay = false; + audioContext.value.obeyMuteSwitch = false; + audioContext.value.volume = 1; + audioContext.value.loop = false; + audioContext.value.src = filePath; + + audioContext.value.onCanplay(() => { + console.log('[AudioQueue] 音频可以播放,开始播放'); + if (audioContext.value?.paused) { + audioContext.value.play(); + } + }); + + audioContext.value.onPlay(() => { + console.log('[AudioQueue] 开始播放音频块'); + startVolumeSimulation(); + }); + + audioContext.value.onEnded(() => { + console.log('[AudioQueue] 音频块播放完成'); + stopVolumeSimulation(); + audioContext.value?.destroy(); + audioContext.value = null; + resolve(); + }); + + audioContext.value.onError((err) => { + console.error('[AudioQueue] 音频播放错误:', err); + stopVolumeSimulation(); + audioContext.value?.destroy(); + audioContext.value = null; + resolve(); // 继续播放下一个 + }); + + // 尝试立即播放(某些情况下不会触发onCanplay) + audioContext.value.play(); + }, + fail: (err) => { + console.error('[AudioQueue] 写入文件失败:', err); + reject(err); + } + }); + }); + } + } catch (error) { + console.error('[AudioQueue] 播放队列处理失败:', error); + } finally { + isPlayingAudio.value = false; + + // 检查队列是否还有数据 + if (audioPlayQueue.value.length > 0 && isVoiceMode.value) { + processAudioQueue(); + } + // 不要在队列为空时设置idle,应该由complete事件来控制状态 + // 因为队列为空不代表对话完成,后端可能还在合成下一句音频 + } + // #endif +}; + // 🎧 播放Base64编码的音频 const playVoiceFromBase64 = async (audioBase64, text = '') => { return new Promise((resolve, reject) => { @@ -2823,4 +3262,42 @@ page { background: linear-gradient(180deg, #f9e076 0%, #f5d042 100%); } +/* 🧪 测试模式样式 */ +.test-controls { + margin-top: 80rpx; + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; +} + +.test-btn { + padding: 24rpx 60rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 50rpx; + font-size: 32rpx; + font-weight: 500; + border: none; + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4); + transition: all 0.3s ease; +} + +.test-btn:active { + transform: scale(0.95); + box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3); +} + +.test-btn[disabled] { + background: linear-gradient(135deg, #bbb 0%, #999 100%); + box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); + opacity: 0.6; +} + +.test-hint { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.6); + text-align: center; +} + diff --git a/src/static/output.pcm b/src/static/output.pcm new file mode 100644 index 0000000000000000000000000000000000000000..d14a8c442365277770093f5985231f313aa0aded GIT binary patch literal 101674 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 IdYpg%AM8u5fdBvi literal 0 HcmV?d00001 diff --git a/src/utils/config.js b/src/utils/config.js index c800673..59d32f5 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -1,11 +1,32 @@ // API配置统一管理 export const API_CONFIG = { // 基础API地址 - BASE_URL: 'https://api.aixsy.com.cn', + BASE_URL: 'http://192.168.3.13:8091', + + // WebSocket地址 + WS_BASE_URL: 'ws://192.168.3.13:8091', // 其他服务地址(如果需要) WEB_URL: 'https://www.aixsy.com.cn', + // 🧪 测试模式配置 + TEST_MODE: { + // 是否启用测试模式(true: 显示测试按钮,禁用录音; false: 正常录音模式) + enabled: false, + + // 📝 测试音频数据(base64编码的PCM数据) + // 格式要求: + // - 采样率: 16000 Hz + // - 位深度: 16 bit (有符号整数) + // - 声道数: 1 (单声道) + // - 字节序: Little Endian (小端序) + // - 无文件头,纯PCM数据 + testAudioBase64: '', // 👈 在这里填入你的base64编码的PCM数据 + + // 或者使用文件路径(优先使用base64) + testAudioPath: 'src/static/output.pcm', // 例如: '/static/test_audio.pcm' + }, + // API端点 ENDPOINTS: { // 登录相关 @@ -44,6 +65,11 @@ export const API_CONFIG = { CONFIG_TTS: '/app/config/tts' }, + // WebSocket端点 + WS_ENDPOINTS: { + VOICE_STREAM: '/ws/voice-stream' + }, + // 请求超时时间(毫秒) TIMEOUT: 30000, @@ -61,3 +87,8 @@ export const getWebUrl = (endpoint) => { return API_CONFIG.WEB_URL + endpoint; }; +// 导出WebSocket URL构建函数 +export const getWsUrl = (endpoint) => { + return API_CONFIG.WS_BASE_URL + endpoint; +}; + diff --git a/src/utils/voiceStreamWebSocket.js b/src/utils/voiceStreamWebSocket.js new file mode 100644 index 0000000..aaced1e --- /dev/null +++ b/src/utils/voiceStreamWebSocket.js @@ -0,0 +1,310 @@ +/** + * 语音流式对话 WebSocket 管理模块 + */ + +class VoiceStreamWebSocket { + constructor(url) { + this.url = url + this.ws = null + this.isConnected = false + this.reconnectAttempts = 0 + this.maxReconnectAttempts = 5 + this.reconnectDelay = 2000 + this.manualClose = false // 标记是否为主动关闭 + + // 事件监听器 + this.listeners = { + onConnected: null, + onDisconnected: null, + onSttResult: null, + onLlmToken: null, + onSentence: null, + onAudioChunk: null, + onComplete: null, + onError: null + } + } + + /** + * 建立连接 + * @param {String} sessionId - 聊天会话ID(用于历史记录查询和保存,与文字对话的sessionId保持一致) + * @param {Number} templateId - 模板ID + * @param {String} token - 认证令牌(可选) + * @param {String} userId - 用户ID(可选) + */ + connect(sessionId = null, templateId = null, token = null, userId = null) { + return new Promise((resolve, reject) => { + try { + // 重置主动关闭标志 + this.manualClose = false + + // 构建连接URL + let wsUrl = this.url + const params = [] + if (sessionId) params.push(`sessionId=${sessionId}`) + if (templateId) params.push(`templateId=${templateId}`) + if (token) params.push(`token=${token}`) + if (userId) params.push(`userId=${userId}`) + if (params.length > 0) { + wsUrl += '?' + params.join('&') + } + + console.log('[VoiceStreamWS] 正在连接:', wsUrl) + + // 创建WebSocket连接 + this.ws = uni.connectSocket({ + url: wsUrl, + success: () => { + console.log('[VoiceStreamWS] WebSocket创建成功') + }, + fail: (err) => { + console.error('[VoiceStreamWS] WebSocket创建失败:', err) + reject(err) + } + }) + + // 连接打开 + this.ws.onOpen(() => { + console.log('[VoiceStreamWS] 连接已建立') + this.isConnected = true + this.reconnectAttempts = 0 + if (this.listeners.onConnected) { + this.listeners.onConnected() + } + resolve() + }) + + // 接收消息 + this.ws.onMessage((res) => { + this.handleMessage(res) + }) + + // 连接错误 + this.ws.onError((err) => { + console.error('[VoiceStreamWS] 连接错误:', err) + this.isConnected = false + if (this.listeners.onError) { + this.listeners.onError('WebSocket连接错误') + } + }) + + // 连接关闭 + this.ws.onClose(() => { + console.log('[VoiceStreamWS] 连接已关闭') + this.isConnected = false + if (this.listeners.onDisconnected) { + this.listeners.onDisconnected() + } + + // 只有非主动关闭才尝试重连 + if (!this.manualClose) { + console.log('[VoiceStreamWS] 检测到异常断开,将尝试自动重连') + this.tryReconnect() + } else { + console.log('[VoiceStreamWS] 主动关闭连接,不进行重连') + } + }) + + } catch (err) { + console.error('[VoiceStreamWS] 连接异常:', err) + reject(err) + } + }) + } + + /** + * 处理接收到的消息 + */ + handleMessage(res) { + try { + // 二进制消息(音频数据) + if (res.data instanceof ArrayBuffer) { + console.log('[VoiceStreamWS] 收到音频数据:', res.data.byteLength, 'bytes') + if (this.listeners.onAudioChunk) { + this.listeners.onAudioChunk(res.data) + } + return + } + + // 文本消息(JSON格式) + const message = JSON.parse(res.data) + console.log('[VoiceStreamWS] 收到消息:', message.type) + + switch (message.type) { + case 'connected': + console.log('[VoiceStreamWS] 服务器确认连接') + break + + case 'stt_result': + // STT识别结果 + if (this.listeners.onSttResult) { + this.listeners.onSttResult(message.message) + } + break + + case 'llm_token': + // LLM输出token + if (this.listeners.onLlmToken) { + this.listeners.onLlmToken(message.message) + } + break + + case 'sentence': + // 完整句子 + if (this.listeners.onSentence) { + this.listeners.onSentence(message.message) + } + break + + case 'complete': + // 对话完成 + if (this.listeners.onComplete) { + this.listeners.onComplete() + } + break + + case 'error': + // 错误 + console.error('[VoiceStreamWS] 服务器错误:', message.message) + if (this.listeners.onError) { + this.listeners.onError(message.message) + } + break + + case 'pong': + // 心跳响应 + console.log('[VoiceStreamWS] 心跳响应') + break + + default: + console.warn('[VoiceStreamWS] 未知消息类型:', message.type) + } + } catch (err) { + console.error('[VoiceStreamWS] 处理消息失败:', err) + } + } + + /** + * 发送音频数据 + */ + sendAudio(audioData) { + if (!this.isConnected || !this.ws) { + console.error('[VoiceStreamWS] 未连接,无法发送音频') + return false + } + + try { + this.ws.send({ + data: audioData, + success: () => { + console.log('[VoiceStreamWS] 音频数据发送成功:', audioData.byteLength, 'bytes') + }, + fail: (err) => { + console.error('[VoiceStreamWS] 音频数据发送失败:', err) + if (this.listeners.onError) { + this.listeners.onError('发送音频失败') + } + } + }) + return true + } catch (err) { + console.error('[VoiceStreamWS] 发送音频异常:', err) + return false + } + } + + /** + * 发送文本消息 + */ + sendMessage(type, data = null) { + if (!this.isConnected || !this.ws) { + console.error('[VoiceStreamWS] 未连接,无法发送消息') + return false + } + + try { + const message = { + type, + timestamp: Date.now() + } + if (data) { + message.data = data + } + + this.ws.send({ + data: JSON.stringify(message), + success: () => { + console.log('[VoiceStreamWS] 消息发送成功:', type) + }, + fail: (err) => { + console.error('[VoiceStreamWS] 消息发送失败:', err) + } + }) + return true + } catch (err) { + console.error('[VoiceStreamWS] 发送消息异常:', err) + return false + } + } + + /** + * 取消当前对话(打断) + */ + cancel() { + return this.sendMessage('cancel') + } + + /** + * 发送心跳 + */ + ping() { + return this.sendMessage('ping') + } + + /** + * 尝试重连 + */ + tryReconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + console.log('[VoiceStreamWS] 达到最大重连次数,停止重连') + return + } + + this.reconnectAttempts++ + console.log(`[VoiceStreamWS] 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`) + + setTimeout(() => { + if (!this.isConnected) { + this.connect().catch(err => { + console.error('[VoiceStreamWS] 重连失败:', err) + }) + } + }, this.reconnectDelay) + } + + /** + * 关闭连接 + */ + close() { + if (this.ws) { + console.log('[VoiceStreamWS] 主动关闭连接') + this.manualClose = true // 标记为主动关闭 + this.isConnected = false + this.reconnectAttempts = 0 // 重置重连计数 + this.ws.close() + this.ws = null + } + } + + /** + * 设置事件监听器 + */ + on(event, callback) { + if (this.listeners.hasOwnProperty(`on${event.charAt(0).toUpperCase()}${event.slice(1)}`)) { + this.listeners[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`] = callback + } + } +} + +export default VoiceStreamWebSocket + diff --git a/如何准备测试音频数据.md b/如何准备测试音频数据.md new file mode 100644 index 0000000..0d446f9 --- /dev/null +++ b/如何准备测试音频数据.md @@ -0,0 +1,262 @@ +# 📝 如何准备测试音频数据 + +## 你需要提供的数据格式 + +### PCM格式要求(重要!) + +``` +✅ 采样率: 16000 Hz +✅ 位深度: 16 bit (有符号整数,Signed Integer) +✅ 声道数: 1 (单声道,Mono) +✅ 字节序: Little Endian (小端序) +✅ 格式: 纯PCM数据(无任何文件头,不是WAV) +``` + +### 数据大小计算 + +``` +数据大小(bytes)= 采样率 × 时长(秒)× 2 + +示例: +- 1秒音频 = 16000 × 1 × 2 = 32,000 bytes ≈ 31.25 KB +- 3秒音频 = 16000 × 3 × 2 = 96,000 bytes ≈ 93.75 KB +- 5秒音频 = 16000 × 5 × 2 = 160,000 bytes ≈ 156.25 KB +``` + +## 方式一:使用 FFmpeg 转换(推荐) + +### 1. 从MP3/WAV/M4A等格式转换 + +```bash +ffmpeg -i input.mp3 -f s16le -acodec pcm_s16le -ar 16000 -ac 1 output.pcm +``` + +**参数说明:** +- `-i input.mp3`: 输入文件(可以是任何音频格式) +- `-f s16le`: 输出格式为16位小端序PCM +- `-acodec pcm_s16le`: 使用16位PCM编码 +- `-ar 16000`: 采样率16000 Hz +- `-ac 1`: 单声道 + +### 2. 验证生成的PCM文件 + +```bash +# 查看文件大小 +ls -lh output.pcm + +# 计算时长(秒)= 文件大小(bytes)/ 32000 +# 例如:96000 bytes / 32000 = 3 秒 +``` + +### 3. 转换为base64 + +```bash +# Linux/Mac +base64 output.pcm > output_base64.txt + +# 或者一行输出(适合小文件) +base64 output.pcm | tr -d '\n' > output_base64.txt + +# Windows PowerShell +[Convert]::ToBase64String([IO.File]::ReadAllBytes("output.pcm")) > output_base64.txt +``` + +## 方式二:在线录音并导出 + +### 1. 使用Audacity(免费开源) + +1. 打开Audacity +2. 点击红色按钮录音 +3. 录制3-5秒的测试语音(说点什么都可以) +4. 点击停止 +5. **设置项目采样率**:左下角设置为 `16000 Hz` +6. **转换为单声道**:轨道 → 混音 → 混音立体声为单声道 +7. **导出**: + - 文件 → 导出 → 导出音频 + - 文件类型选择:`其他未压缩文件` + - 头部:`RAW (header-less)` + - 编码:`Signed 16-bit PCM` + - 保存为 `test.pcm` + +### 2. 使用Python脚本录音 + +```python +import pyaudio +import wave +import struct + +# 配置 +RATE = 16000 +CHANNELS = 1 +FORMAT = pyaudio.paInt16 +RECORD_SECONDS = 3 + +# 录音 +audio = pyaudio.PyAudio() +stream = audio.open(format=FORMAT, channels=CHANNELS, + rate=RATE, input=True, + frames_per_buffer=1024) + +print("录音中... (3秒)") +frames = [] +for i in range(0, int(RATE / 1024 * RECORD_SECONDS)): + data = stream.read(1024) + frames.append(data) + +print("录音完成") +stream.stop_stream() +stream.close() +audio.terminate() + +# 保存为PCM +with open('output.pcm', 'wb') as f: + f.write(b''.join(frames)) + +print("已保存为 output.pcm") +``` + +## 如何使用生成的数据 + +### 选项A:使用base64(推荐,适合小文件) + +1. **转换为base64**(见上面的命令) + +2. **打开** `webUI/src/utils/config.js` + +3. **填入base64数据**: + +```javascript +TEST_MODE: { + enabled: true, + testAudioBase64: 'AAEAAgADAAQABQAG...', // 👈 粘贴你的base64字符串 + testAudioPath: '', +} +``` + +**完整示例:** +```javascript +TEST_MODE: { + enabled: true, + testAudioBase64: 'AAEAAgADAAQABQAGAAcACA...(很长的base64字符串)...==', + testAudioPath: '', +} +``` + +### 选项B:使用文件路径(适合大文件) + +1. **将PCM文件放入项目**: + ``` + webUI/src/static/test_audio.pcm + ``` + +2. **配置路径**: +```javascript +TEST_MODE: { + enabled: true, + testAudioBase64: '', // 留空 + testAudioPath: '/static/test_audio.pcm', // 👈 文件路径 +} +``` + +## 快速测试数据示例 + +### 生成一个简单的测试文件(Python) + +```python +import struct + +# 生成3秒的简单正弦波(200Hz) +sample_rate = 16000 +duration = 3 +frequency = 200 + +with open('test.pcm', 'wb') as f: + for i in range(sample_rate * duration): + t = i / sample_rate + # 正弦波,振幅8000 + sample = int(8000 * (2 * 3.14159 * frequency * t) ** 0.5) + sample = max(-32768, min(32767, sample)) + f.write(struct.pack(' test_base64.txt + +# 复制 test_base64.txt 的内容,粘贴到 config.js 的 testAudioBase64 +``` + +### 方式2: 快速录音(Mac) + +```bash +# 录制3秒音频并自动转换 +rec -r 16000 -c 1 -b 16 test.pcm trim 0 3 + +# 转为base64 +base64 test.pcm | tr -d '\n' > test_base64.txt +``` + +### 方式3: 生成测试音频(Python) + +```python +# 保存为 generate_test.py +import struct +import base64 + +sample_rate = 16000 +duration = 3 + +data = bytearray() +for i in range(sample_rate * duration): + # 简单正弦波 + import math + sample = int(8000 * math.sin(2 * math.pi * 200 * i / sample_rate)) + data.extend(struct.pack(' test_base64.txt + +# 4. 复制内容 +cat test_base64.txt | pbcopy # Mac +# 或手动打开 test_base64.txt 复制 + +# 5. 粘贴到 config.js +# testAudioBase64: '刚才复制的内容' + +# 完成! +``` + +## Windows 用户 + +### 使用 PowerShell + +```powershell +# 转base64 +$bytes = [System.IO.File]::ReadAllBytes("test.pcm") +$base64 = [System.Convert]::ToBase64String($bytes) +$base64 | Out-File -Encoding ascii test_base64.txt + +# 打开查看 +notepad test_base64.txt +``` + +## 快速测试内容建议 + +录制这些内容(任选一个,3秒即可): + +- "你好" +- "今天天气怎么样" +- "给我讲个笑话" +- "帮我查询一下" +- 随便说点什么 + +--- + +**准备好后**,在项目中点击 🎧 进入语音模式,然后点击 "🧪 发送测试音频" 按钮测试! +