158 lines
4.7 KiB
JavaScript
158 lines
4.7 KiB
JavaScript
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.lang = 'zh-CN';
|
|
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');
|
|
} |