flat: 暂存
This commit is contained in:
157
hook/useSystemPlayer.js
Normal file
157
hook/useSystemPlayer.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
ref,
|
||||
onUnmounted,
|
||||
readonly
|
||||
} from 'vue';
|
||||
|
||||
const defaultExtractSpeechText = (text) => text;
|
||||
|
||||
|
||||
export function useTTSPlayer() {
|
||||
const synth = window.speechSynthesis;
|
||||
const isSpeaking = ref(false);
|
||||
const isPaused = ref(false);
|
||||
const utteranceRef = ref(null);
|
||||
|
||||
const cleanup = () => {
|
||||
isSpeaking.value = false;
|
||||
isPaused.value = false;
|
||||
utteranceRef.value = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} text - The text to be spoken.
|
||||
* @param {object} [options] - Optional settings for the speech.
|
||||
* @param {string} [options.lang] - Language (e.g., 'en-US', 'es-ES').
|
||||
* @param {number} [options.rate] - Speed (0.1 to 10, default 1).
|
||||
* @param {number} [options.pitch] - Pitch (0 to 2, default 1).
|
||||
* @param {SpeechSynthesisVoice} [options.voice] - A specific voice object.
|
||||
* @param {function(string): string} [options.extractSpeechText] - A function to filter/clean the text before speaking.
|
||||
*/
|
||||
const speak = (text, options = {}) => {
|
||||
if (!synth) {
|
||||
console.error('SpeechSynthesis API is not supported in this browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSpeaking.value) {
|
||||
synth.cancel();
|
||||
}
|
||||
|
||||
const filteredText = extractSpeechText(text);
|
||||
|
||||
if (!filteredText || typeof filteredText !== 'string' || filteredText.trim() === '') {
|
||||
console.warn('Text to speak is empty after filtering.');
|
||||
cleanup(); // Ensure state is clean
|
||||
return;
|
||||
}
|
||||
|
||||
const newUtterance = new SpeechSynthesisUtterance(filteredText); // Use filtered text
|
||||
utteranceRef.value = newUtterance;
|
||||
|
||||
newUtterance.rate = options.rate || 1;
|
||||
newUtterance.pitch = options.pitch || 1;
|
||||
if (options.voice) {
|
||||
newUtterance.voice = options.voice;
|
||||
}
|
||||
|
||||
newUtterance.onstart = () => {
|
||||
isSpeaking.value = true;
|
||||
isPaused.value = false;
|
||||
};
|
||||
|
||||
newUtterance.onpause = () => {
|
||||
isPaused.value = true;
|
||||
};
|
||||
newUtterance.onresume = () => {
|
||||
isPaused.value = false;
|
||||
};
|
||||
newUtterance.onend = () => {
|
||||
cleanup();
|
||||
};
|
||||
newUtterance.onerror = (event) => {
|
||||
console.error('SpeechSynthesis Error:', event.error);
|
||||
cleanup();
|
||||
};
|
||||
|
||||
synth.speak(newUtterance);
|
||||
};
|
||||
|
||||
const pause = () => {
|
||||
if (synth && isSpeaking.value && !isPaused.value) {
|
||||
synth.pause();
|
||||
}
|
||||
};
|
||||
|
||||
const resume = () => {
|
||||
if (synth && isPaused.value) {
|
||||
synth.resume();
|
||||
}
|
||||
};
|
||||
|
||||
const cancelAudio = () => {
|
||||
if (synth) {
|
||||
synth.cancel();
|
||||
}
|
||||
cleanup();
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAudio();
|
||||
});
|
||||
|
||||
return {
|
||||
speak,
|
||||
pause,
|
||||
resume,
|
||||
cancelAudio,
|
||||
isSpeaking: readonly(isSpeaking),
|
||||
isPaused: readonly(isPaused),
|
||||
};
|
||||
}
|
||||
|
||||
function extractSpeechText(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;
|
||||
} catch (e) {
|
||||
console.warn('JSON 解析失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 提取引导语(第一个 job-json 之前的文字)
|
||||
const guideText = firstJobStartIndex > 0 ?
|
||||
markdown.slice(0, firstJobStartIndex).trim() :
|
||||
'';
|
||||
|
||||
// 提取结束语(最后一个 job-json 之后的文字)
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user