From 7a7aa3312888a3271c7d8df30906f9744904e290 Mon Sep 17 00:00:00 2001 From: francis_fh <13935151924@163.com> Date: Sat, 24 Jan 2026 17:57:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=97=B6=E6=92=AD=E6=94=BE=E8=81=8A?= =?UTF-8?q?=E6=9D=A1=E9=9F=B3=E9=A2=91=E7=9A=84=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hook/useTTSPlayer.js | 742 +++++++++++++++------------- pages/chat/components/ai-paging.vue | 23 +- 2 files changed, 400 insertions(+), 365 deletions(-) diff --git a/hook/useTTSPlayer.js b/hook/useTTSPlayer.js index a7a6d1b..b8f4b99 100644 --- a/hook/useTTSPlayer.js +++ b/hook/useTTSPlayer.js @@ -9,77 +9,79 @@ import { onUnload } from '@dcloudio/uni-app' -export function useTTSPlayer(httpUrl) { - const isSpeaking = ref(false) - const isPaused = ref(false) - const isComplete = ref(false) +// 创建单例实例 +let ttsInstance = null - // #ifdef H5 - // H5环境,使用 AudioContext - // 初始化时不立即创建,而是在需要时创建,确保在用户交互后创建 - let audioContext = null - // #endif +// 创建音频播放器类 +class TTSPlayer { + constructor(httpUrl) { + this.httpUrl = httpUrl + this.isSpeaking = ref(false) + this.isPaused = ref(false) + this.isComplete = ref(false) + + // #ifdef H5 + this.audioContext = null + this.htmlAudioElement = null + this.abortController = null + // #endif + + // #ifdef MP-WEIXIN + this.backgroundAudioManager = null + this.innerAudioContext = null + // #endif + + this.currentAudioBuffer = null + this.currentSource = null + this.playTimeOffset = 0 + this.isProcessingRequest = false + } - // #ifdef MP-WEIXIN - const audioContext = null // 微信小程序不支持 AudioContext - let innerAudioContext = null // 微信小程序音频上下文 - let backgroundAudioManager = null // 微信小程序背景音频管理器 - // #endif - - let currentAudioBuffer = null - let currentSource = null - let playTimeOffset = 0 - // 初始化微信小程序音频上下文 - // #ifdef MP-WEIXIN - const initAudioManager = () => { + initAudioManager() { try { console.log('📱 微信小程序:创建背景音频管理器') - backgroundAudioManager = uni.getBackgroundAudioManager() + this.backgroundAudioManager = uni.getBackgroundAudioManager() // 设置默认配置 - backgroundAudioManager.title = 'AI语音播报' - backgroundAudioManager.singer = 'KS AI' - backgroundAudioManager.coverImgUrl = '/static/icon/logo.png' - backgroundAudioManager.volume = 1.0 + this.backgroundAudioManager.title = 'AI语音播报' + this.backgroundAudioManager.singer = 'KS AI' + this.backgroundAudioManager.coverImgUrl = '/static/icon/logo.png' + this.backgroundAudioManager.volume = 1.0 - backgroundAudioManager.onPlay(() => { + this.backgroundAudioManager.onPlay(() => { console.log('🎵 微信小程序背景音频播放开始') - isSpeaking.value = true - isPaused.value = false + this.isSpeaking.value = true + this.isPaused.value = false }) - backgroundAudioManager.onPause(() => { + this.backgroundAudioManager.onPause(() => { console.log('⏸️ 微信小程序背景音频播放暂停') - isPaused.value = true + this.isPaused.value = true }) - backgroundAudioManager.onStop(() => { + this.backgroundAudioManager.onStop(() => { console.log('⏹️ 微信小程序背景音频播放停止') - isSpeaking.value = false - isComplete.value = true + this.isSpeaking.value = false + this.isComplete.value = true }) - backgroundAudioManager.onEnded(() => { + this.backgroundAudioManager.onEnded(() => { console.log('🎵 微信小程序背景音频播放结束') - isSpeaking.value = false - isComplete.value = true + this.isSpeaking.value = false + this.isComplete.value = true }) - backgroundAudioManager.onError((res) => { + this.backgroundAudioManager.onError((res) => { console.error('❌ 微信小程序背景音频播放错误:', res.errMsg, '错误码:', res.errCode) - isSpeaking.value = false - isComplete.value = false + this.isSpeaking.value = false + this.isComplete.value = false }) - backgroundAudioManager.onCanplay(() => { + this.backgroundAudioManager.onCanplay(() => { console.log('🎵 微信小程序背景音频可以播放了') }) - backgroundAudioManager.onWaiting(() => { - console.log('⏳ 微信小程序背景音频加载中...') - }) - console.log('✅ 微信小程序背景音频管理器初始化成功') return true } catch (e) { @@ -87,137 +89,361 @@ export function useTTSPlayer(httpUrl) { // 降级使用InnerAudioContext console.log('🔄 微信小程序:背景音频不可用,降级使用InnerAudioContext') - if (!innerAudioContext) { - innerAudioContext = uni.createInnerAudioContext() - innerAudioContext.autoplay = false - innerAudioContext.obeyMuteSwitch = false + if (!this.innerAudioContext) { + this.innerAudioContext = uni.createInnerAudioContext() + this.innerAudioContext.autoplay = false + this.innerAudioContext.obeyMuteSwitch = false - innerAudioContext.onPlay(() => { + this.innerAudioContext.onPlay(() => { console.log('🎵 微信小程序InnerAudioContext播放开始') - isSpeaking.value = true - isPaused.value = false + this.isSpeaking.value = true + this.isPaused.value = false }) - innerAudioContext.onPause(() => { + this.innerAudioContext.onPause(() => { console.log('⏸️ 微信小程序InnerAudioContext播放暂停') - isPaused.value = true + this.isPaused.value = true }) - innerAudioContext.onStop(() => { + this.innerAudioContext.onStop(() => { console.log('⏹️ 微信小程序InnerAudioContext播放停止') - isSpeaking.value = false - isComplete.value = true + this.isSpeaking.value = false + this.isComplete.value = true }) - innerAudioContext.onEnded(() => { + this.innerAudioContext.onEnded(() => { console.log('🎵 微信小程序InnerAudioContext播放结束') - isSpeaking.value = false - isComplete.value = true + this.isSpeaking.value = false + this.isComplete.value = true }) - innerAudioContext.onError((res) => { + this.innerAudioContext.onError((res) => { console.error('❌ 微信小程序InnerAudioContext错误:', res.errMsg, '错误码:', res.errCode) - isSpeaking.value = false - isComplete.value = false + this.isSpeaking.value = false + this.isComplete.value = false }) - innerAudioContext.onCanplay(() => { + this.innerAudioContext.onCanplay(() => { console.log('🎵 微信小程序InnerAudioContext可以播放了') - if (isSpeaking.value && !isPaused.value) { - innerAudioContext.play() + if (this.isSpeaking.value && !this.isPaused.value) { + this.innerAudioContext.play() } }) } return false } } - // #endif - - const speak = async (text) => { - // 停止当前播放 - stop() + + // H5环境播放解码后的音频 + playDecodedAudio(decoded) { + // #ifdef H5 + if (!this.audioContext) return; + + // 创建音频源 + this.currentSource = this.audioContext.createBufferSource() + this.currentSource.buffer = decoded + this.currentSource.connect(this.audioContext.destination) + + // 监听播放结束 + this.currentSource.onended = () => { + console.log('🎵 Audio playback completed'); + this.isSpeaking.value = false + this.isComplete.value = true + } + + // 开始播放 + this.currentSource.start() + this.isSpeaking.value = true + this.isPaused.value = false + this.isComplete.value = false + console.log('🎵 Audio playback started'); + // #endif + } + + // 降级处理:创建一个简单的音频缓冲区 + createFallbackAudio(arrayBuffer) { + // #ifdef H5 + console.log('🔄 使用降级方案创建音频'); + + // 创建一个简单的音频缓冲区,生成提示音 + const sampleRate = 44100 + const duration = 1 // 1秒 + const frameCount = sampleRate * duration + + const audioBuffer = this.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 + } + + this.playDecodedAudio(audioBuffer) + // #endif + } + + // 暂停播放 + pause() { + console.log('⏸️ TTS pause called'); + + // #ifdef MP-WEIXIN + // 优先使用背景音频管理器 + if (this.backgroundAudioManager) { + try { + this.backgroundAudioManager.pause() + console.log('⏸️ 微信小程序背景音频暂停'); + return + } catch (e) { + console.error('❌ 微信小程序背景音频暂停失败:', e); + } + } + + // 降级使用InnerAudioContext + if (this.innerAudioContext && this.isSpeaking.value && !this.isPaused.value) { + try { + this.innerAudioContext.pause() + console.log('⏸️ 微信小程序InnerAudioContext暂停'); + return + } catch (e) { + console.error('❌ 微信小程序InnerAudioContext暂停失败:', e); + } + } + // #endif + + // #ifdef H5 + if (this.audioContext && !this.isSpeaking.value || this.isPaused.value) { + console.warn('⚠️ Cannot pause TTS playback'); + return; + } + + if (this.audioContext.state === 'running') { + this.audioContext.suspend() + this.isPaused.value = true + // 保存当前播放位置 + this.playTimeOffset = this.audioContext.currentTime + console.log('✅ H5 Audio paused successfully'); + } + // #endif + } + + // 恢复播放 + resume() { + console.log('▶️ TTS resume called'); + + // #ifdef MP-WEIXIN + // 优先使用背景音频管理器 + if (this.backgroundAudioManager) { + try { + this.backgroundAudioManager.play() + console.log('▶️ 微信小程序背景音频恢复播放'); + return + } catch (e) { + console.error('❌ 微信小程序背景音频恢复失败:', e); + } + } + + // 降级使用InnerAudioContext + if (this.innerAudioContext && this.isSpeaking.value && this.isPaused.value) { + try { + this.innerAudioContext.play() + console.log('▶️ 微信小程序InnerAudioContext恢复播放'); + return + } catch (e) { + console.error('❌ 微信小程序InnerAudioContext恢复失败:', e); + } + } + // #endif + + // #ifdef H5 + if (this.audioContext && !this.isSpeaking.value || !this.isPaused.value) { + console.warn('⚠️ Cannot resume TTS playback'); + return; + } + + if (this.audioContext.state === 'suspended') { + this.audioContext.resume() + this.isPaused.value = false + console.log('✅ H5 Audio resumed successfully'); + } + // #endif + } + + // 停止播放 + stop() { + console.log('⏹️ TTS stop called'); + + // #ifdef MP-WEIXIN + // 优先使用背景音频管理器 + if (this.backgroundAudioManager) { + try { + this.backgroundAudioManager.stop() + console.log('✅ 微信小程序背景音频停止'); + } catch (e) { + console.error('❌ 微信小程序背景音频停止失败:', e); + } + } + + // 降级使用InnerAudioContext + if (this.innerAudioContext) { + try { + this.innerAudioContext.stop() + console.log('✅ 微信小程序InnerAudioContext停止'); + this.innerAudioContext.destroy() + this.innerAudioContext = null + } catch (e) { + console.error('❌ 微信小程序InnerAudioContext停止错误:', e); + } + } + // #endif + + // #ifdef H5 + // 取消正在进行的fetch请求 + if (this.abortController) { + try { + this.abortController.abort(); + this.abortController = null; + console.log('✅ H5 fetch request aborted'); + } catch (e) { + console.error('❌ Error aborting H5 fetch request:', e); + } + } + + if (this.currentSource) { + try { + this.currentSource.stop() + this.currentSource.disconnect() + } catch (e) { + console.error('❌ Error stopping H5 audio source:', e); + } + this.currentSource = null + } + + // 停止并清理HTML5 Audio元素 + if (this.htmlAudioElement) { + try { + this.htmlAudioElement.pause(); + this.htmlAudioElement.src = ''; + this.htmlAudioElement = null; + console.log('✅ H5 HTML5 Audio element stopped and cleaned up'); + } catch (e) { + console.error('❌ Error stopping H5 HTML5 Audio element:', e); + } + } + + if (this.audioContext && this.audioContext.state === 'running') { + try { + this.audioContext.suspend() + } catch (e) { + console.error('❌ Error suspending H5 audio context:', e); + } + } + // #endif + + this.isSpeaking.value = false + this.isPaused.value = false + this.isComplete.value = false + this.currentAudioBuffer = null + this.playTimeOffset = 0 + this.isProcessingRequest = false + + console.log('✅ TTS playback stopped'); + } + + // 取消音频播放 + cancelAudio() { + this.stop() + } + + // 合成并播放语音 + async speak(text) { + // 停止当前播放和处理中的请求 + this.stop() try { + // 标记开始处理请求 + this.isProcessingRequest = true + // 提取要合成的文本 const speechText = extractSpeechText(text) console.log('📤 Sending text to TTS server via GET:', speechText.substring(0, 100) + '...'); // 构建GET请求URL - const url = `${httpUrl}?text=${encodeURIComponent(speechText)}` + const url = `${this.httpUrl}?text=${encodeURIComponent(speechText)}` console.log('🔗 Final GET URL:', url); // #ifdef MP-WEIXIN // 微信小程序环境,使用背景音频管理器 - const isBackgroundAudioAvailable = initAudioManager() + const isBackgroundAudioAvailable = this.initAudioManager() // 重置音频状态 - isSpeaking.value = true - isPaused.value = false - isComplete.value = false + this.isSpeaking.value = true + this.isPaused.value = false + this.isComplete.value = false - if (isBackgroundAudioAvailable && backgroundAudioManager) { + if (isBackgroundAudioAvailable && this.backgroundAudioManager) { console.log('🎵 微信小程序:使用背景音频管理器播放,URL:', url); // 设置背景音频参数 - backgroundAudioManager.title = 'AI语音播报' - backgroundAudioManager.singer = 'KS AI' - backgroundAudioManager.coverImgUrl = '/static/icon/logo.png' + this.backgroundAudioManager.title = 'AI语音播报' + this.backgroundAudioManager.singer = 'KS AI' + this.backgroundAudioManager.coverImgUrl = '/static/icon/logo.png' // 直接设置src并播放 - backgroundAudioManager.src = url + this.backgroundAudioManager.src = url console.log('🎵 微信小程序背景音频开始播放'); } else { // 降级方案:使用InnerAudioContext console.log('🔄 微信小程序:背景音频不可用,降级使用InnerAudioContext'); // 如果已有音频上下文,先销毁再重新创建 - if (innerAudioContext) { - innerAudioContext.destroy() - innerAudioContext = null + if (this.innerAudioContext) { + this.innerAudioContext.destroy() + this.innerAudioContext = null } - innerAudioContext = uni.createInnerAudioContext() - innerAudioContext.autoplay = false - innerAudioContext.obeyMuteSwitch = false - innerAudioContext.volume = 1.0 + this.innerAudioContext = uni.createInnerAudioContext() + this.innerAudioContext.autoplay = false + this.innerAudioContext.obeyMuteSwitch = false + this.innerAudioContext.volume = 1.0 - innerAudioContext.onPlay(() => { + this.innerAudioContext.onPlay(() => { console.log('🎵 微信小程序InnerAudioContext播放开始') - isSpeaking.value = true - isPaused.value = false + this.isSpeaking.value = true + this.isPaused.value = false }) - innerAudioContext.onPause(() => { + this.innerAudioContext.onPause(() => { console.log('⏸️ 微信小程序InnerAudioContext播放暂停') - isPaused.value = true + this.isPaused.value = true }) - innerAudioContext.onStop(() => { + this.innerAudioContext.onStop(() => { console.log('⏹️ 微信小程序InnerAudioContext播放停止') - isSpeaking.value = false - isComplete.value = true + this.isSpeaking.value = false + this.isComplete.value = true }) - innerAudioContext.onEnded(() => { + this.innerAudioContext.onEnded(() => { console.log('🎵 微信小程序InnerAudioContext播放结束') - isSpeaking.value = false - isComplete.value = true + this.isSpeaking.value = false + this.isComplete.value = true }) - innerAudioContext.onError((res) => { + this.innerAudioContext.onError((res) => { console.error('❌ 微信小程序InnerAudioContext错误:', res.errMsg, '错误码:', res.errCode) - isSpeaking.value = false - isComplete.value = false + this.isSpeaking.value = false + this.isComplete.value = false }) - innerAudioContext.onCanplay(() => { + this.innerAudioContext.onCanplay(() => { console.log('🎵 微信小程序InnerAudioContext可以播放了') - if (isSpeaking.value && !isPaused.value) { - innerAudioContext.play() + if (this.isSpeaking.value && !this.isPaused.value) { + this.innerAudioContext.play() } }) - innerAudioContext.src = url + this.innerAudioContext.src = url console.log('🎵 微信小程序InnerAudioContext开始播放'); } // #endif @@ -225,21 +451,25 @@ export function useTTSPlayer(httpUrl) { // #ifdef H5 // H5环境,使用 AudioContext try { + // 创建新的AbortController,用于取消当前请求 + this.abortController = new AbortController(); + const signal = this.abortController.signal; + // 确保AudioContext已创建 - if (!audioContext) { + if (!this.audioContext) { console.log('🎵 H5: 创建新的AudioContext'); - audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } // 检查并恢复AudioContext状态(浏览器安全策略要求用户交互后才能播放音频) - if (audioContext.state === 'suspended') { + if (this.audioContext.state === 'suspended') { console.log('🎵 H5: 恢复挂起的AudioContext'); - await audioContext.resume(); - console.log('✅ H5: AudioContext已恢复,状态:', audioContext.state); + await this.audioContext.resume(); + console.log('✅ H5: AudioContext已恢复,状态:', this.audioContext.state); } - // 发送GET请求获取语音数据 - const response = await fetch(url) + // 发送GET请求获取语音数据,添加signal支持取消 + const response = await fetch(url, { signal }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } @@ -250,277 +480,101 @@ export function useTTSPlayer(httpUrl) { try { // 直接使用 audioContext.decodeAudioData 解码,不依赖外部库 - const decoded = await audioContext.decodeAudioData(arrayBuffer) + const decoded = await this.audioContext.decodeAudioData(arrayBuffer) console.log('✅ H5: Audio decoded, sampleRate:', decoded.sampleRate, 'channels:', decoded.numberOfChannels); // 播放音频 - playDecodedAudio(decoded) + this.playDecodedAudio(decoded) } catch (decodeError) { console.error('❌ H5: AudioContext decodeAudioData failed:', decodeError); // 降级处理:创建一个简单的音频缓冲区 - createFallbackAudio(arrayBuffer) + this.createFallbackAudio(arrayBuffer) } } catch (h5Error) { + // 检查是否是取消请求导致的错误 + if (h5Error.name === 'AbortError') { + console.log('✅ H5: Fetch request aborted as requested'); + this.isSpeaking.value = false; + this.isComplete.value = false; + return; + } + console.error('❌ H5: Audio playback failed:', h5Error); // 尝试使用HTML5 Audio元素作为最终降级方案 try { console.log('🔄 H5: 尝试使用HTML5 Audio元素播放'); + + // 如果已有Audio元素,先停止并销毁 + if (this.htmlAudioElement) { + this.htmlAudioElement.pause(); + this.htmlAudioElement.src = ''; + this.htmlAudioElement = null; + } + const audio = new Audio(url); + this.htmlAudioElement = audio; audio.play(); console.log('✅ H5: HTML5 Audio元素开始播放'); // 设置音频状态 - isSpeaking.value = true; - isPaused.value = false; - isComplete.value = false; + this.isSpeaking.value = true; + this.isPaused.value = false; + this.isComplete.value = false; // 监听播放结束 audio.onended = () => { console.log('🎵 H5: HTML5 Audio播放结束'); - isSpeaking.value = false; - isComplete.value = true; + this.isSpeaking.value = false; + this.isComplete.value = true; + this.htmlAudioElement = null; }; // 监听播放错误 audio.onerror = (error) => { console.error('❌ H5: HTML5 Audio播放错误:', error); - isSpeaking.value = false; - isComplete.value = false; + this.isSpeaking.value = false; + this.isComplete.value = false; + this.htmlAudioElement = null; }; } catch (audioError) { console.error('❌ H5: HTML5 Audio播放也失败了:', audioError); - isSpeaking.value = false; - isComplete.value = false; + this.isSpeaking.value = false; + this.isComplete.value = false; + this.htmlAudioElement = null; } + } finally { + // 清除AbortController,因为请求已经完成(无论成功还是失败) + this.abortController = null; } // #endif } catch (error) { console.error('❌ TTS synthesis failed:', error); - isSpeaking.value = false - isComplete.value = false + this.isSpeaking.value = false + this.isComplete.value = false + } finally { + // 标记请求处理完成 + this.isProcessingRequest = false } } +} - // #ifdef H5 - const playDecodedAudio = (decoded) => { - if (!audioContext) return; - - // 创建音频源 - currentSource = audioContext.createBufferSource() - currentSource.buffer = decoded - currentSource.connect(audioContext.destination) - - // 监听播放结束 - currentSource.onended = () => { - console.log('🎵 Audio playback completed'); - isSpeaking.value = false - isComplete.value = true - } - - // 开始播放 - currentSource.start() - isSpeaking.value = true - isPaused.value = false - isComplete.value = false - console.log('🎵 Audio playback started'); +// 导出单例hook +export function useTTSPlayer(httpUrl) { + // 如果已经有实例,直接返回 + if (!ttsInstance) { + ttsInstance = new TTSPlayer(httpUrl) } - - // 降级处理:创建一个简单的音频缓冲区 - 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 = () => { - console.log('⏸️ TTS pause called'); - - // #ifdef MP-WEIXIN - // 优先使用背景音频管理器 - if (backgroundAudioManager) { - try { - backgroundAudioManager.pause() - console.log('⏸️ 微信小程序背景音频暂停'); - return - } catch (e) { - console.error('❌ 微信小程序背景音频暂停失败:', e); - } - } - - // 降级使用InnerAudioContext - if (innerAudioContext && isSpeaking.value && !isPaused.value) { - try { - innerAudioContext.pause() - console.log('⏸️ 微信小程序InnerAudioContext暂停'); - return - } catch (e) { - console.error('❌ 微信小程序InnerAudioContext暂停失败:', e); - } - } - // #endif - - // #ifdef H5 - if (audioContext && !isSpeaking.value || isPaused.value) { - console.warn('⚠️ Cannot pause TTS playback'); - return; - } - - if (audioContext.state === 'running') { - audioContext.suspend() - isPaused.value = true - // 保存当前播放位置 - playTimeOffset = audioContext.currentTime - console.log('✅ H5 Audio paused successfully'); - } - // #endif - } - - const resume = () => { - console.log('▶️ TTS resume called'); - - // #ifdef MP-WEIXIN - // 优先使用背景音频管理器 - if (backgroundAudioManager) { - try { - backgroundAudioManager.play() - console.log('▶️ 微信小程序背景音频恢复播放'); - return - } catch (e) { - console.error('❌ 微信小程序背景音频恢复失败:', e); - } - } - - // 降级使用InnerAudioContext - if (innerAudioContext && isSpeaking.value && isPaused.value) { - try { - innerAudioContext.play() - console.log('▶️ 微信小程序InnerAudioContext恢复播放'); - return - } catch (e) { - console.error('❌ 微信小程序InnerAudioContext恢复失败:', e); - } - } - // #endif - - // #ifdef H5 - if (audioContext && !isSpeaking.value || !isPaused.value) { - console.warn('⚠️ Cannot resume TTS playback'); - return; - } - - if (audioContext.state === 'suspended') { - audioContext.resume() - isPaused.value = false - console.log('✅ H5 Audio resumed successfully'); - } - // #endif - } - - const cancelAudio = () => { - stop() - } - - const stop = () => { - console.log('⏹️ TTS stop called'); - - // #ifdef MP-WEIXIN - // 优先使用背景音频管理器 - if (backgroundAudioManager) { - try { - backgroundAudioManager.stop() - console.log('✅ 微信小程序背景音频停止'); - } catch (e) { - console.error('❌ 微信小程序背景音频停止失败:', e); - } - } - - // 降级使用InnerAudioContext - if (innerAudioContext) { - try { - innerAudioContext.stop() - console.log('✅ 微信小程序InnerAudioContext停止'); - innerAudioContext.destroy() - innerAudioContext = null - } catch (e) { - console.error('❌ 微信小程序InnerAudioContext停止错误:', e); - } - } - // #endif - - // #ifdef H5 - if (currentSource) { - try { - currentSource.stop() - currentSource.disconnect() - } catch (e) { - console.error('❌ Error stopping H5 audio source:', e); - } - currentSource = null - } - - if (audioContext && audioContext.state === 'running') { - try { - audioContext.suspend() - } catch (e) { - console.error('❌ Error suspending H5 audio context:', e); - } - } - // #endif - - isSpeaking.value = false - isPaused.value = false - isComplete.value = false - currentAudioBuffer = null - playTimeOffset = 0 - - console.log('✅ TTS playback stopped'); - } - - onUnmounted(() => { - stop() - }) - - // 页面刷新/关闭时 - onMounted(() => { - if (typeof window !== 'undefined') { - window.addEventListener('beforeunload', cancelAudio) - } - }) - - onBeforeUnmount(() => { - cancelAudio() - if (typeof window !== 'undefined') { - window.removeEventListener('beforeunload', cancelAudio) - } - }) - - onHide(cancelAudio) - onUnload(cancelAudio) - + + // 返回实例的方法和状态 return { - speak, - pause, - resume, - cancelAudio, - isSpeaking, - isPaused, - isComplete + speak: ttsInstance.speak.bind(ttsInstance), + pause: ttsInstance.pause.bind(ttsInstance), + resume: ttsInstance.resume.bind(ttsInstance), + cancelAudio: ttsInstance.cancelAudio.bind(ttsInstance), + isSpeaking: ttsInstance.isSpeaking, + isPaused: ttsInstance.isPaused, + isComplete: ttsInstance.isComplete } } diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index 3206870..af01b9a 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -794,29 +794,10 @@ function readMarkdown(value, index) { clearTimeout(ttsDebounceTimer); } - // 如果当前正在播放其他消息,先停止 - if (speechIndex.value !== index && speechIndex.value !== 0) { - console.log('🛑 Stopping current speech and starting new one'); - speechIndex.value = index; - speak(value); - return; - } - + // 总是先停止当前播放,无论是不是同一消息 + console.log('🛑 Always stopping current speech before starting new one'); speechIndex.value = index; - // 如果当前正在播放且暂停了,直接恢复 - if (isPaused.value && isSpeaking.value) { - console.log('▶️ Resuming paused speech'); - resume(); - return; - } - - // 如果当前正在播放且没有暂停,不需要重新开始 - if (isSpeaking.value && !isPaused.value) { - console.log('🔊 Already speaking, no need to restart'); - return; - } - // 使用防抖,避免频繁调用TTS ttsDebounceTimer = setTimeout(() => { console.log('🎵 Starting new speech');