import { ref, onUnmounted } from 'vue' import { onHide, onUnload } from '@dcloudio/uni-app' import { isY9MachineType } from '../common/globalFunction'; /** * 封装 hh.call 通用调用 */ const callBridge = (params) => { return new Promise((resolve) => { if (typeof window !== 'undefined' && window.hh && window.hh.call) { // 打印日志方便调试 console.log('[TTS Bridge Send]:', params) window.hh.call("ampeHHCommunication", params, (res) => { console.log('[TTS Bridge Res]:', res) resolve(res) }) } else { console.warn('当前环境不支持 hh.call,模拟成功') resolve({ success: true }) } }) } /** * 生成随机 TaskId */ const generateTaskId = () => { return 'task_' + Date.now() + '_' + Math.floor(Math.random() * 1000) } /** * 直接播放文本 (静态方法,无需实例化 Hook) * @param {string} text - 需要朗读的文本 */ export async function playTextDirectly(text) { if (!text) return if (!isY9MachineType()) return const processedText = extractSpeechText(text) if (!processedText) return try { // 构造播放参数 const params = { "action": "speech", "event": "open", "taskId": generateTaskId(), "params": { "text": processedText, "pitch": 1.0, "rate": 1.0, "speechQueue": 1 // 1表示打断当前播放,立即播放新的 } } // 直接调用 await callBridge(params) } catch (e) { console.error('Direct Play Error:', e) } } /** * 一体机 TTS 播放器 Hook (修复版) */ export function useTTSPlayer() { // UI 状态 const isSpeaking = ref(false) // 是否正在交互 const isPaused = ref(false) // 是否处于暂停状态 const isLoading = ref(false) // 是否正在加载 // 记录最后一次播放的文本 const lastText = ref('') /** * 停止 (中断) - 内部使用 */ const executeStop = async () => { const params = { "action": "speechStop", "event": "open", "taskId": generateTaskId() } await callBridge(params) } /** * 核心朗读方法 */ const speak = async (text) => { if (!text) return const processedText = extractSpeechText(text) if (!processedText) return // 1. 立即更新记忆文本 (确保即使后续失败,resume也能读到新的) lastText.value = processedText isLoading.value = true try { // 2.【关键修复】先强制停止上一次播报 // 虽然 speechQueue:1 理论上可以打断,但显式停止更稳健 await executeStop() // 3. 构造播放参数 const params = { "action": "speech", "event": "open", "taskId": generateTaskId(), "params": { "text": processedText, "pitch": 1.0, "rate": 1.0, "speechQueue": 1 // 1表示打断 } } // 4. 发送播放指令 await callBridge(params) // 5. 更新状态 isSpeaking.value = true isPaused.value = false } catch (e) { console.error('TTS Speak Error:', e) resetState() } finally { isLoading.value = false } } /** * 暂停 */ const pause = async () => { if (!isSpeaking.value || isPaused.value) return try { await executeStop() isPaused.value = true // 注意:不要设置 isSpeaking = false,因为逻辑上只是暂停 } catch (e) { console.error("Pause failed:", e) } } /** * 恢复 (由于硬件限制,实际上是从头播放当前文本) */ const resume = async () => { if (!isPaused.value) return // 如果有缓存的文本,重新触发 speak if (lastText.value) { console.log('[TTS Resume] Re-speaking text:', lastText.value.substring(0, 20) + '...') // 调用 speak 会自动处理 stop 和状态更新 await speak(lastText.value) } else { isPaused.value = false } } /** * 切换 播放/暂停 * 注意:如果是切换了新文章,请直接调用 speak(newText),不要调用 togglePlay */ const togglePlay = () => { // 如果当前是暂停状态,则恢复 if (isPaused.value) { resume() } // 如果当前正在播放,则暂停 else if (isSpeaking.value) { pause() } // 如果既没播放也没暂停(比如刚进页面),需要业务层调用 speak 启动,这里无法自动推断文本 } /** * 停止并重置 */ const stop = async () => { await executeStop() resetState() } const resetState = () => { isSpeaking.value = false isPaused.value = false isLoading.value = false } // === 生命周期管理 === onUnmounted(() => stop()) if (typeof onHide === 'function') onHide(() => stop()) if (typeof onUnload === 'function') onUnload(() => stop()) return { speak, pause, resume, togglePlay, stop, cancelAudio: stop, isSpeaking, isPaused, isLoading } } /** * 文本提取工具函数 (保持不变) */ function extractSpeechText(markdown) { if (!markdown || typeof markdown !== 'string') return ''; // 增加类型安全检查 if (markdown.indexOf('job-json') === -1) { return markdown; } const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g; const jobs = []; let match; let lastJobEndIndex = 0; let firstJobStartIndex = -1; 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; } catch (e) { console.warn('JSON 解析失败', e); } } const guideText = firstJobStartIndex > 0 ? markdown.slice(0, firstJobStartIndex).trim() : ''; const endingText = lastJobEndIndex < markdown.length ? markdown.slice(lastJobEndIndex).trim() : ''; 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); return finalTextParts.join('\n'); } // 使用方法 1 // import { useTTSPlayer } from '@/hook/useTTSPlayer-all-in-one' // const { speak, stop, isSpeaking } = useTTSPlayer() // // 调用 // speak('你好,这是一段测试文本') // 使用方法 2 // import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one' // // 直接调用即可,无需实例化 // playTextDirectly('直接朗读这段话,不需要处理暂停和UI状态')