diff --git a/hook/useTTSPlayer.js b/hook/useTTSPlayer.js index 525318d..2864987 100644 --- a/hook/useTTSPlayer.js +++ b/hook/useTTSPlayer.js @@ -8,7 +8,6 @@ import { onHide, onUnload } from '@dcloudio/uni-app' -import WavDecoder from '@/lib/wav-decoder@1.3.0.js' export function useTTSPlayer(httpUrl) { const isSpeaking = ref(false) @@ -23,22 +22,55 @@ export function useTTSPlayer(httpUrl) { // #ifdef MP-WEIXIN const audioContext = null // 微信小程序不支持 AudioContext + let innerAudioContext = null // 微信小程序音频上下文 // #endif let currentAudioBuffer = null let currentSource = null let playTimeOffset = 0 - const speak = async (text) => { - if (!audioContext) { - console.warn('⚠️ TTS not supported in current environment'); - return; + // 初始化微信小程序音频上下文 + // #ifdef MP-WEIXIN + const initInnerAudioContext = () => { + if (!innerAudioContext) { + innerAudioContext = uni.createInnerAudioContext() + innerAudioContext.autoplay = false + innerAudioContext.onPlay(() => { + console.log('🎵 微信小程序音频播放开始') + isSpeaking.value = true + isPaused.value = false + }) + innerAudioContext.onPause(() => { + console.log('⏸️ 微信小程序音频播放暂停') + isPaused.value = true + }) + innerAudioContext.onStop(() => { + console.log('⏹️ 微信小程序音频播放停止') + isSpeaking.value = false + isComplete.value = true + }) + innerAudioContext.onEnded(() => { + console.log('🎵 微信小程序音频播放结束') + isSpeaking.value = false + isComplete.value = true + }) + innerAudioContext.onError((res) => { + console.error('❌ 微信小程序音频播放错误:', res.errMsg) + isSpeaking.value = false + isComplete.value = false + }) + innerAudioContext.onCanplay(() => { + console.log('🎵 微信小程序音频可以播放了') + // 只有在需要播放且未播放状态下才调用play + if (isSpeaking.value && !isPaused.value) { + innerAudioContext.play() + } + }) } - - console.log('🎤 TTS speak function called'); - console.log('📝 Text to synthesize:', text ? text.substring(0, 100) + '...' : 'No text'); - console.log('🔗 HTTP URL:', httpUrl); - + } + // #endif + + const speak = async (text) => { // 停止当前播放 stop() @@ -51,22 +83,46 @@ export function useTTSPlayer(httpUrl) { const url = `${httpUrl}?text=${encodeURIComponent(speechText)}` console.log('🔗 Final GET URL:', url); - // 发送GET请求获取语音数据 - const response = await fetch(url) - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) + // #ifdef MP-WEIXIN + // 微信小程序环境,使用微信音频API + initInnerAudioContext() + console.log('🎵 微信小程序:设置音频 src 为:', url); + // 重置音频状态 + isSpeaking.value = true + isPaused.value = false + isComplete.value = false + // 设置src,等待onCanplay事件触发后再播放 + innerAudioContext.src = url + // #endif + + // #ifdef H5 + // H5环境,使用 AudioContext + if (audioContext) { + // 发送GET请求获取语音数据 + const response = await fetch(url) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + // 获取二进制数据 + const arrayBuffer = await response.arrayBuffer() + console.log('✅ Received audio data, size:', arrayBuffer.byteLength + ' bytes'); + + try { + // 直接使用 audioContext.decodeAudioData 解码,不依赖外部库 + const decoded = await audioContext.decodeAudioData(arrayBuffer) + console.log('✅ Audio decoded, sampleRate:', decoded.sampleRate, 'channels:', decoded.numberOfChannels); + + // 播放音频 + playDecodedAudio(decoded) + } catch (decodeError) { + console.error('❌ AudioContext decodeAudioData failed:', decodeError); + // 降级处理:创建一个简单的音频缓冲区 + createFallbackAudio(arrayBuffer) + } } + // #endif - // 获取二进制数据 - const arrayBuffer = await response.arrayBuffer() - console.log('✅ Received audio data, size:', arrayBuffer.byteLength + ' bytes'); - - // 解码音频数据 - const decoded = await WavDecoder.decode(arrayBuffer) - console.log('✅ Audio decoded, sampleRate:', decoded.sampleRate, 'channels:', decoded.channelData.length); - - // 播放音频 - playDecodedAudio(decoded) } catch (error) { console.error('❌ TTS synthesis failed:', error); isSpeaking.value = false @@ -74,18 +130,13 @@ export function useTTSPlayer(httpUrl) { } } + // #ifdef H5 const playDecodedAudio = (decoded) => { if (!audioContext) return; - // 使用第一个声道的数据 - const audioBuffer = audioContext.createBuffer(1, decoded.channelData[0].length, decoded.sampleRate) - audioBuffer.copyToChannel(decoded.channelData[0], 0) - - currentAudioBuffer = audioBuffer - // 创建音频源 currentSource = audioContext.createBufferSource() - currentSource.buffer = audioBuffer + currentSource.buffer = decoded currentSource.connect(audioContext.destination) // 监听播放结束 @@ -100,11 +151,42 @@ export function useTTSPlayer(httpUrl) { isSpeaking.value = true isPaused.value = false isComplete.value = false - console.log('� Audio playback started'); + console.log('🎵 Audio playback started'); } + // 降级处理:创建一个简单的音频缓冲区 + const createFallbackAudio = (arrayBuffer) => { + console.log('🔄 使用降级方案创建音频'); + + // 创建一个简单的音频缓冲区,生成提示音 + const sampleRate = 44100 + const duration = 1 // 1秒 + const frameCount = sampleRate * duration + + const audioBuffer = audioContext.createBuffer(1, frameCount, sampleRate) + const channelData = audioBuffer.getChannelData(0) + + // 生成一个简单的提示音(正弦波) + for (let i = 0; i < frameCount; i++) { + const t = i / sampleRate + channelData[i] = Math.sin(2 * Math.PI * 440 * t) * 0.1 // 440Hz正弦波,音量0.1 + } + + playDecodedAudio(audioBuffer) + } + // #endif + const pause = () => { - if (!audioContext || !isSpeaking.value || isPaused.value) { + // #ifdef MP-WEIXIN + if (innerAudioContext && isSpeaking.value && !isPaused.value) { + console.log('⏸️ 微信小程序音频暂停'); + innerAudioContext.pause() + return + } + // #endif + + // #ifdef H5 + if (audioContext && !isSpeaking.value || isPaused.value) { console.warn('⚠️ Cannot pause TTS playback'); return; } @@ -118,10 +200,20 @@ export function useTTSPlayer(httpUrl) { playTimeOffset = audioContext.currentTime console.log('✅ Audio paused successfully'); } + // #endif } const resume = () => { - if (!audioContext || !isSpeaking.value || !isPaused.value) { + // #ifdef MP-WEIXIN + if (innerAudioContext && isSpeaking.value && isPaused.value) { + console.log('▶️ 微信小程序音频恢复播放'); + innerAudioContext.play() + return + } + // #endif + + // #ifdef H5 + if (audioContext && !isSpeaking.value || !isPaused.value) { console.warn('⚠️ Cannot resume TTS playback'); return; } @@ -133,6 +225,7 @@ export function useTTSPlayer(httpUrl) { isPaused.value = false console.log('✅ Audio resumed successfully'); } + // #endif } const cancelAudio = () => { @@ -142,6 +235,18 @@ export function useTTSPlayer(httpUrl) { const stop = () => { console.log('⏹️ TTS stop called'); + // #ifdef MP-WEIXIN + if (innerAudioContext) { + try { + innerAudioContext.stop() + console.log('✅ 微信小程序音频停止'); + } catch (e) { + console.error('❌ 微信小程序音频停止错误:', e); + } + } + // #endif + + // #ifdef H5 if (currentSource) { try { currentSource.stop() @@ -159,6 +264,7 @@ export function useTTSPlayer(httpUrl) { console.error('❌ Error suspending audio context:', e); } } + // #endif isSpeaking.value = false isPaused.value = false diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index e67e54b..ffeb33f 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -283,7 +283,7 @@ import { getCurrentInstance, } from 'vue'; import { storeToRefs } from 'pinia'; -// import config from '@/config.js'; +// 移除重复导入,使用从globalFunction注入的config import useChatGroupDBStore from '@/stores/userChatGroupStore'; import MdRender from '@/components/md-render/md-render.vue'; import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue'; diff --git a/utils/streamRequest.js b/utils/streamRequest.js index 0b5c529..19a517c 100644 --- a/utils/streamRequest.js +++ b/utils/streamRequest.js @@ -51,101 +51,101 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom } }); - // UTF-8解码函数,用于微信小程序真机环境 - function utf8Decode(uint8Array) { - let result = ''; - let i = 0; - const len = uint8Array.length; - - while (i < len) { - const byte1 = uint8Array[i]; - - // 1字节字符 (0xxxxxxx) - if (byte1 < 0x80) { - result += String.fromCharCode(byte1); - i++; - } - // 2字节字符 (110xxxxx 10xxxxxx) - else if (byte1 >= 0xC0 && byte1 < 0xE0) { - if (i + 1 < len) { - const byte2 = uint8Array[i + 1]; - if (byte2 >= 0x80 && byte2 < 0xC0) { - const codePoint = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F); - result += String.fromCharCode(codePoint); - i += 2; - continue; - } - } - // 无效的UTF-8序列,跳过 - result += '�'; - i++; - } - // 3字节字符 (1110xxxx 10xxxxxx 10xxxxxx) - else if (byte1 >= 0xE0 && byte1 < 0xF0) { - if (i + 2 < len) { - const byte2 = uint8Array[i + 1]; - const byte3 = uint8Array[i + 2]; - if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0)) { - const codePoint = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F); - result += String.fromCharCode(codePoint); - i += 3; - continue; - } - } - // 无效的UTF-8序列,跳过 - result += '�'; - i++; - } - // 4字节字符 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) - else if (byte1 >= 0xF0 && byte1 < 0xF8) { - if (i + 3 < len) { - const byte2 = uint8Array[i + 1]; - const byte3 = uint8Array[i + 2]; - const byte4 = uint8Array[i + 3]; - if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0) && (byte4 >= 0x80 && byte4 < 0xC0)) { - let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F); - // 处理UTF-16代理对 - if (codePoint >= 0x10000) { - codePoint -= 0x10000; - const highSurrogate = (codePoint >> 10) + 0xD800; - const lowSurrogate = (codePoint & 0x3FF) + 0xDC00; - result += String.fromCharCode(highSurrogate, lowSurrogate); - } else { - result += String.fromCharCode(codePoint); - } - i += 4; - continue; - } - } - // 无效的UTF-8序列,跳过 - result += '�'; - i++; - } - // 无效的UTF-8序列,跳过 - else { - result += '�'; - i++; - } - } - - return result; - } - - // 监听分块数据 - requestTask.onChunkReceived((res) => { - try { - // 微信小程序兼容处理:微信小程序不支持TextDecoder,使用自定义UTF-8解码 - let chunk = ''; - if (typeof TextDecoder !== 'undefined') { - // 支持TextDecoder的环境(如开发者工具) - const decoder = new TextDecoder('utf-8'); - chunk = decoder.decode(new Uint8Array(res.data)); - } else { - // 微信小程序真机环境,使用自定义UTF-8解码函数 - const uint8Array = new Uint8Array(res.data); - chunk = utf8Decode(uint8Array); - } - console.log('📦 收到分块数据:', chunk); + // UTF-8解码函数,用于微信小程序真机环境 + function utf8Decode(uint8Array) { + let result = ''; + let i = 0; + const len = uint8Array.length; + + while (i < len) { + const byte1 = uint8Array[i]; + + // 1字节字符 (0xxxxxxx) + if (byte1 < 0x80) { + result += String.fromCharCode(byte1); + i++; + } + // 2字节字符 (110xxxxx 10xxxxxx) + else if (byte1 >= 0xC0 && byte1 < 0xE0) { + if (i + 1 < len) { + const byte2 = uint8Array[i + 1]; + if (byte2 >= 0x80 && byte2 < 0xC0) { + const codePoint = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F); + result += String.fromCharCode(codePoint); + i += 2; + continue; + } + } + // 无效的UTF-8序列,跳过 + result += '�'; + i++; + } + // 3字节字符 (1110xxxx 10xxxxxx 10xxxxxx) + else if (byte1 >= 0xE0 && byte1 < 0xF0) { + if (i + 2 < len) { + const byte2 = uint8Array[i + 1]; + const byte3 = uint8Array[i + 2]; + if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0)) { + const codePoint = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F); + result += String.fromCharCode(codePoint); + i += 3; + continue; + } + } + // 无效的UTF-8序列,跳过 + result += '�'; + i++; + } + // 4字节字符 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx) + else if (byte1 >= 0xF0 && byte1 < 0xF8) { + if (i + 3 < len) { + const byte2 = uint8Array[i + 1]; + const byte3 = uint8Array[i + 2]; + const byte4 = uint8Array[i + 3]; + if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0) && (byte4 >= 0x80 && byte4 < 0xC0)) { + let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F); + // 处理UTF-16代理对 + if (codePoint >= 0x10000) { + codePoint -= 0x10000; + const highSurrogate = (codePoint >> 10) + 0xD800; + const lowSurrogate = (codePoint & 0x3FF) + 0xDC00; + result += String.fromCharCode(highSurrogate, lowSurrogate); + } else { + result += String.fromCharCode(codePoint); + } + i += 4; + continue; + } + } + // 无效的UTF-8序列,跳过 + result += '�'; + i++; + } + // 无效的UTF-8序列,跳过 + else { + result += '�'; + i++; + } + } + + return result; + } + + // 监听分块数据 + requestTask.onChunkReceived((res) => { + try { + // 微信小程序兼容处理:微信小程序不支持TextDecoder,使用自定义UTF-8解码 + let chunk = ''; + if (typeof TextDecoder !== 'undefined') { + // 支持TextDecoder的环境(如开发者工具) + const decoder = new TextDecoder('utf-8'); + chunk = decoder.decode(new Uint8Array(res.data)); + } else { + // 微信小程序真机环境,使用自定义UTF-8解码函数 + const uint8Array = new Uint8Array(res.data); + chunk = utf8Decode(uint8Array); + } + console.log('📦 收到分块数据:', chunk); buffer += chunk; let lines = buffer.split("\n");