home/src/utils/speech.js
2024-12-14 23:20:37 +08:00

222 lines
6.3 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.

let currentAudio = null;
let audioQueue = [];
let isPlaying = false;
let controller = null;
let timeoutId = null;
/**
* Speech
* Made by NanoRocky
* 使用指定参数生成语音并播放音频。
* 该功能原为 Azure 设计,理应兼容大部分使用 post 传参的 api 。请自行根据要求修改!如果也使用 Azure ,您可直接使用 https://github.com/NanoRocky/AzureSpeechAPI-by-PHP 完成 API 部署
* https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/speech-synthesis-markup-voice
*
* @param {string} text - 朗读的文本
* @param {string} [voice="zh-CN-YunxiaNeural"] - 音色默认为“zh-CN-YunxiaNeural”
* @param {string} [style="cheerful"] - 声音特定的讲话风格默认为“cheerful”
* @param {string} [role="Boy"] - 讲话角色扮演默认为“Boy”
* @param {string} [rate="1"] - 语速默认为“1”
* @param {string} [volume="100"] - 音量默认为“100”
* @param {number} [delay=300] - 等待时间【毫秒】后发出请求防止频繁点击产生请求洪水默认为等待300毫秒
* @returns {Promise<void>} - 一个 Promise在语音播放完成时解析或出现错误时拒绝
*/
export function Speech(
text,
voice = "zh-CN-YunxiaNeural",
style = "cheerful",
role = "Boy",
rate = "1",
volume = "100",
delay = 300,
) {
return new Promise(async (resolve, reject) => {
// 如果有现有的等待,取消之前的 timeout
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
};
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
};
// 创建新的 AbortController 实例,并中断旧请求
if (controller) {
controller.abort();
};
controller = new AbortController();
const { signal } = controller;
const formData = new FormData();
formData.append("text", text);
formData.append("voice", voice);
formData.append("style", style);
formData.append("role", role);
formData.append("rate", rate);
formData.append("volume", volume);
// 在指定的 delay 后开始请求
timeoutId = setTimeout(async () => {
try {
const speechapi = import.meta.env.VITE_TTS_API;
const response = await fetch(speechapi, {
method: "POST",
body: formData,
signal, // 传递 AbortSignal
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error);
};
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
// 将新的音频对象添加到队列
audioQueue.push(audioUrl);
if (!isPlaying) {
playNext();
};
function playNext() {
if (audioQueue.length === 0) {
isPlaying = false;
return;
};
isPlaying = true;
const nextAudioUrl = audioQueue.shift();
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
};
const audio = new Audio();
audio.src = nextAudioUrl;
audio.play();
// 在音频播放结束时解析 Promise
audio.onended = () => {
resolve();
playNext();
};
// 如果发生错误,拒绝 Promise
audio.onerror = (error) => {
reject(error);
playNext();
};
// 将当前播放的语音赋值给全局变量
currentAudio = audio;
};
} catch (error) {
if (error.name === "AbortError") {
console.log("Request canceled");
} else {
console.error("Error:", error.message);
reject(error);
};
};
}, delay);
});
}
/**
* 停止当前播放的语音,并清空播放队列。
*/
export function stopSpeech() {
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
};
audioQueue = [];
isPlaying = false;
if (controller) {
controller.abort();
controller = null;
};
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
};
};
/**
* SpeechLocal
* Made by NanoRocky
* 播放本地预生成的语音音频。
* 考虑到生成延迟,所以加了这个,仅必要模块调用 api 实时生成,其它模块使用预先生成好的音频。记得根据需求更换自己的音频文件哇!
*
* @param {string} fileName - 音频文件名 + 文件拓展名(请将文件放在指定路径)
* @param {number} [delay=0] - 等待时间【毫秒】后发出请求,防止频繁点击产生请求洪水(默认提前生成的不等待)
* @returns {Promise<void>} - 一个 Promise在语音播放完成时解析或出现错误时拒绝
*/
export function SpeechLocal(fileName, delay = 0) {
return new Promise((resolve, reject) => {
if (!fileName) {
reject(new Error("No file name provided"));
return;
}
const audioUrl = `https://file.nanorocky.top/home/speechlocal/${fileName}`;
// 如果有现有的等待,取消之前的 timeout
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
};
// 清除之前的音频
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
timeoutId = setTimeout(async () => {
// 停止当前正在播放的语音
audioQueue = [];
isPlaying = false;
if (controller) {
controller.abort();
controller = null;
}
// 添加新音频到队列并播放
audioQueue.push(audioUrl);
if (!isPlaying) {
playNext();
}
function playNext() {
if (audioQueue.length === 0) {
isPlaying = false;
return;
}
isPlaying = true;
const nextAudioUrl = audioQueue.shift();
const audio = new Audio();
audio.src = nextAudioUrl;
// 确保新的音频对象没有被中途替换
audio.oncanplaythrough = () => {
currentAudio = audio;
currentAudio.play();
};
// 在音频播放结束时解析 Promise
audio.onended = () => {
resolve();
playNext();
};
// 如果发生错误,拒绝 Promise
audio.onerror = (error) => {
reject(error);
playNext();
};
};
}, delay);
});
};