import { ref, onUnmounted, onBeforeUnmount, onMounted } from 'vue' import { onHide, onUnload } from '@dcloudio/uni-app' // 创建单例实例 let ttsInstance = null // 创建音频播放器类 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 } // 初始化微信小程序音频上下文 initAudioManager() { try { console.log('📱 微信小程序:创建背景音频管理器') this.backgroundAudioManager = uni.getBackgroundAudioManager() // 设置默认配置 this.backgroundAudioManager.title = 'AI语音播报' this.backgroundAudioManager.singer = 'KS AI' this.backgroundAudioManager.coverImgUrl = '/static/icon/logo.png' this.backgroundAudioManager.volume = 1.0 this.backgroundAudioManager.onPlay(() => { console.log('🎵 微信小程序背景音频播放开始') this.isSpeaking.value = true this.isPaused.value = false }) this.backgroundAudioManager.onPause(() => { console.log('⏸️ 微信小程序背景音频播放暂停') this.isPaused.value = true }) this.backgroundAudioManager.onStop(() => { console.log('⏹️ 微信小程序背景音频播放停止') this.isSpeaking.value = false this.isComplete.value = true }) this.backgroundAudioManager.onEnded(() => { console.log('🎵 微信小程序背景音频播放结束') this.isSpeaking.value = false this.isComplete.value = true }) this.backgroundAudioManager.onError((res) => { console.error('❌ 微信小程序背景音频播放错误:', res.errMsg, '错误码:', res.errCode) this.isSpeaking.value = false this.isComplete.value = false }) this.backgroundAudioManager.onCanplay(() => { console.log('🎵 微信小程序背景音频可以播放了') }) console.log('✅ 微信小程序背景音频管理器初始化成功') return true } catch (e) { console.error('❌ 微信小程序背景音频管理器初始化失败:', e) // 降级使用InnerAudioContext console.log('🔄 微信小程序:背景音频不可用,降级使用InnerAudioContext') if (!this.innerAudioContext) { this.innerAudioContext = uni.createInnerAudioContext() this.innerAudioContext.autoplay = false this.innerAudioContext.obeyMuteSwitch = false this.innerAudioContext.onPlay(() => { console.log('🎵 微信小程序InnerAudioContext播放开始') this.isSpeaking.value = true this.isPaused.value = false }) this.innerAudioContext.onPause(() => { console.log('⏸️ 微信小程序InnerAudioContext播放暂停') this.isPaused.value = true }) this.innerAudioContext.onStop(() => { console.log('⏹️ 微信小程序InnerAudioContext播放停止') this.isSpeaking.value = false this.isComplete.value = true }) this.innerAudioContext.onEnded(() => { console.log('🎵 微信小程序InnerAudioContext播放结束') this.isSpeaking.value = false this.isComplete.value = true }) this.innerAudioContext.onError((res) => { console.error('❌ 微信小程序InnerAudioContext错误:', res.errMsg, '错误码:', res.errCode) this.isSpeaking.value = false this.isComplete.value = false }) this.innerAudioContext.onCanplay(() => { console.log('🎵 微信小程序InnerAudioContext可以播放了') if (this.isSpeaking.value && !this.isPaused.value) { this.innerAudioContext.play() } }) } return false } } // 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 = `${this.httpUrl}?text=${encodeURIComponent(speechText)}` console.log('🔗 Final GET URL:', url); // #ifdef MP-WEIXIN // 微信小程序环境,使用背景音频管理器 const isBackgroundAudioAvailable = this.initAudioManager() // 重置音频状态,但不立即设置为正在播放,等待实际播放时再设置 this.isPaused.value = false this.isComplete.value = false if (isBackgroundAudioAvailable && this.backgroundAudioManager) { console.log('🎵 微信小程序:使用背景音频管理器播放,URL:', url); // 设置背景音频参数 this.backgroundAudioManager.title = 'AI语音播报' this.backgroundAudioManager.singer = 'KS AI' this.backgroundAudioManager.coverImgUrl = '/static/icon/logo.png' // 直接设置src并播放 this.backgroundAudioManager.src = url console.log('🎵 微信小程序背景音频开始加载'); } else { // 降级方案:使用InnerAudioContext console.log('🔄 微信小程序:背景音频不可用,降级使用InnerAudioContext'); // 如果已有音频上下文,先销毁再重新创建 if (this.innerAudioContext) { this.innerAudioContext.destroy() this.innerAudioContext = null } this.innerAudioContext = uni.createInnerAudioContext() this.innerAudioContext.autoplay = true // 设置自动播放,等音频加载完成后自动开始 this.innerAudioContext.obeyMuteSwitch = false this.innerAudioContext.volume = 1.0 this.innerAudioContext.onPlay(() => { console.log('🎵 微信小程序InnerAudioContext播放开始') this.isSpeaking.value = true this.isPaused.value = false }) this.innerAudioContext.onPause(() => { console.log('⏸️ 微信小程序InnerAudioContext播放暂停') this.isPaused.value = true }) this.innerAudioContext.onStop(() => { console.log('⏹️ 微信小程序InnerAudioContext播放停止') this.isSpeaking.value = false this.isComplete.value = true }) this.innerAudioContext.onEnded(() => { console.log('🎵 微信小程序InnerAudioContext播放结束') this.isSpeaking.value = false this.isComplete.value = true }) this.innerAudioContext.onError((res) => { console.error('❌ 微信小程序InnerAudioContext错误:', res.errMsg, '错误码:', res.errCode) this.isSpeaking.value = false this.isComplete.value = false }) this.innerAudioContext.onCanplay(() => { console.log('🎵 微信小程序InnerAudioContext可以播放了') // 不需要手动调用play,因为已经设置了autoplay }) this.innerAudioContext.src = url console.log('🎵 微信小程序InnerAudioContext开始加载'); } // #endif // #ifdef H5 // H5环境,使用 AudioContext try { // 创建新的AbortController,用于取消当前请求 this.abortController = new AbortController(); const signal = this.abortController.signal; // 确保AudioContext已创建 if (!this.audioContext) { console.log('🎵 H5: 创建新的AudioContext'); this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); } // 检查并恢复AudioContext状态(浏览器安全策略要求用户交互后才能播放音频) if (this.audioContext.state === 'suspended') { console.log('🎵 H5: 恢复挂起的AudioContext'); await this.audioContext.resume(); console.log('✅ H5: AudioContext已恢复,状态:', this.audioContext.state); } // 发送GET请求获取语音数据,添加signal支持取消 const response = await fetch(url, { signal }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } // 获取二进制数据 const arrayBuffer = await response.arrayBuffer() console.log('✅ H5: Received audio data, size:', arrayBuffer.byteLength + ' bytes'); try { // 直接使用 audioContext.decodeAudioData 解码,不依赖外部库 const decoded = await this.audioContext.decodeAudioData(arrayBuffer) console.log('✅ H5: Audio decoded, sampleRate:', decoded.sampleRate, 'channels:', decoded.numberOfChannels); // 播放音频 this.playDecodedAudio(decoded) } catch (decodeError) { console.error('❌ H5: AudioContext decodeAudioData failed:', decodeError); // 降级处理:创建一个简单的音频缓冲区 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元素开始播放'); // 设置音频状态 this.isSpeaking.value = true; this.isPaused.value = false; this.isComplete.value = false; // 监听播放结束 audio.onended = () => { console.log('🎵 H5: HTML5 Audio播放结束'); this.isSpeaking.value = false; this.isComplete.value = true; this.htmlAudioElement = null; }; // 监听播放错误 audio.onerror = (error) => { console.error('❌ H5: HTML5 Audio播放错误:', error); this.isSpeaking.value = false; this.isComplete.value = false; this.htmlAudioElement = null; }; } catch (audioError) { console.error('❌ H5: HTML5 Audio播放也失败了:', audioError); 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); this.isSpeaking.value = false this.isComplete.value = false } finally { // 标记请求处理完成 this.isProcessingRequest = false } } } // 导出单例hook export function useTTSPlayer(httpUrl) { // 如果已经有实例,直接返回 if (!ttsInstance) { ttsInstance = new TTSPlayer(httpUrl) } // 返回实例的方法和状态 return { 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 } } function extractSpeechText(markdown) { console.log('🔍 extractSpeechText called'); console.log('📝 Input markdown length:', markdown ? markdown.length : 0); console.log('📝 Input markdown preview:', markdown ? markdown.substring(0, 200) + '...' : 'No markdown'); const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g; const jobs = []; let match; let lastJobEndIndex = 0; let firstJobStartIndex = -1; // 提取岗位 json 数据及前后位置 while ((match = jobRegex.exec(markdown)) !== null) { const jobStr = match[1]; try { const job = JSON.parse(jobStr); jobs.push(job); if (firstJobStartIndex === -1) { firstJobStartIndex = match.index; } lastJobEndIndex = jobRegex.lastIndex; console.log('✅ Found job:', job.jobTitle); } catch (e) { console.warn('JSON 解析失败', e); } } console.log('📊 Jobs found:', jobs.length); console.log('📍 First job start index:', firstJobStartIndex); console.log('📍 Last job end index:', lastJobEndIndex); // 提取引导语(第一个 job-json 之前的文字) const guideText = firstJobStartIndex > 0 ? markdown.slice(0, firstJobStartIndex).trim() : ''; // 提取结束语(最后一个 job-json 之后的文字) const endingText = lastJobEndIndex < markdown.length ? markdown.slice(lastJobEndIndex).trim() : ''; console.log('📝 Guide text:', guideText); console.log('📝 Ending text:', endingText); // 岗位信息格式化为语音文本 const jobTexts = jobs.map((job, index) => { return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}。`; }); // 拼接总语音内容 const finalTextParts = []; if (guideText) finalTextParts.push(guideText); finalTextParts.push(...jobTexts); if (endingText) finalTextParts.push(endingText); const finalText = finalTextParts.join('\n'); console.log('🎤 Final TTS text length:', finalText.length); console.log('🎤 Final TTS text preview:', finalText.substring(0, 200) + '...'); console.log('🎤 Final TTS text parts count:', finalTextParts.length); return finalText; }