Files
qingdao-employment-service/hook/useTTSPlayer-all-in-one.js

285 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 || 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) => {
// 处理薪资格式:将 "数字-数字" 中的 '-' 替换为 '到'
let salaryText = job.salary;
if (salaryText) {
// 匹配 "数字-数字" 的格式15000-18000元/月)
salaryText = salaryText.replace(/(\d+)-(\d+)/g, '$1到$2');
}
return `${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${salaryText},地点:${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状态')