镜像自地址
https://github.com/binary-husky/gpt_academic.git
已同步 2025-12-06 22:46:48 +00:00
351 行
12 KiB
JavaScript
351 行
12 KiB
JavaScript
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
// TTS语音生成函数
|
|
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
audio_debug = false;
|
|
class AudioPlayer {
|
|
constructor() {
|
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
this.queue = [];
|
|
this.isPlaying = false;
|
|
this.currentSource = null; // 添加属性来保存当前播放的源
|
|
}
|
|
|
|
// Base64 编码的字符串转换为 ArrayBuffer
|
|
base64ToArrayBuffer(base64) {
|
|
const binaryString = window.atob(base64);
|
|
const len = binaryString.length;
|
|
const bytes = new Uint8Array(len);
|
|
for (let i = 0; i < len; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return bytes.buffer;
|
|
}
|
|
|
|
// 检查音频播放队列并播放音频
|
|
checkQueue() {
|
|
if (!this.isPlaying && this.queue.length > 0) {
|
|
this.isPlaying = true;
|
|
const nextAudio = this.queue.shift();
|
|
this.play_wave(nextAudio);
|
|
}
|
|
}
|
|
|
|
// 将音频添加到播放队列
|
|
enqueueAudio(audio_buf_wave) {
|
|
if (allow_auto_read_tts_flag) {
|
|
this.queue.push(audio_buf_wave);
|
|
this.checkQueue();
|
|
}
|
|
}
|
|
|
|
// 播放音频
|
|
async play_wave(encodedAudio) {
|
|
//const audioData = this.base64ToArrayBuffer(encodedAudio);
|
|
const audioData = encodedAudio;
|
|
try {
|
|
const buffer = await this.audioCtx.decodeAudioData(audioData);
|
|
const source = this.audioCtx.createBufferSource();
|
|
source.buffer = buffer;
|
|
source.connect(this.audioCtx.destination);
|
|
source.onended = () => {
|
|
if (allow_auto_read_tts_flag) {
|
|
this.isPlaying = false;
|
|
this.currentSource = null; // 播放结束后清空当前源
|
|
this.checkQueue();
|
|
}
|
|
};
|
|
this.currentSource = source; // 保存当前播放的源
|
|
source.start();
|
|
} catch (e) {
|
|
console.log("Audio error!", e);
|
|
this.isPlaying = false;
|
|
this.currentSource = null; // 出错时也应清空当前源
|
|
this.checkQueue();
|
|
}
|
|
}
|
|
|
|
// 新增:立即停止播放音频的方法
|
|
stop() {
|
|
if (this.currentSource) {
|
|
this.queue = []; // 清空队列
|
|
this.currentSource.stop(); // 停止当前源
|
|
this.currentSource = null; // 清空当前源
|
|
this.isPlaying = false; // 更新播放状态
|
|
// 关闭音频上下文可能会导致无法再次播放音频,因此仅停止当前源
|
|
// this.audioCtx.close(); // 可选:如果需要可以关闭音频上下文
|
|
}
|
|
}
|
|
}
|
|
|
|
const audioPlayer = new AudioPlayer();
|
|
|
|
class FIFOLock {
|
|
constructor() {
|
|
this.queue = [];
|
|
this.currentTaskExecuting = false;
|
|
}
|
|
|
|
lock() {
|
|
let resolveLock;
|
|
const lock = new Promise(resolve => {
|
|
resolveLock = resolve;
|
|
});
|
|
|
|
this.queue.push(resolveLock);
|
|
|
|
if (!this.currentTaskExecuting) {
|
|
this._dequeueNext();
|
|
}
|
|
|
|
return lock;
|
|
}
|
|
|
|
_dequeueNext() {
|
|
if (this.queue.length === 0) {
|
|
this.currentTaskExecuting = false;
|
|
return;
|
|
}
|
|
this.currentTaskExecuting = true;
|
|
const resolveLock = this.queue.shift();
|
|
resolveLock();
|
|
}
|
|
|
|
unlock() {
|
|
this.currentTaskExecuting = false;
|
|
this._dequeueNext();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Define the trigger function with delay parameter T in milliseconds
|
|
function trigger(T, fire) {
|
|
// Variable to keep track of the timer ID
|
|
let timeoutID = null;
|
|
// Variable to store the latest arguments
|
|
let lastArgs = null;
|
|
|
|
return function (...args) {
|
|
// Update lastArgs with the latest arguments
|
|
lastArgs = args;
|
|
// Clear the existing timer if the function is called again
|
|
if (timeoutID !== null) {
|
|
clearTimeout(timeoutID);
|
|
}
|
|
// Set a new timer that calls the `fire` function with the latest arguments after T milliseconds
|
|
timeoutID = setTimeout(() => {
|
|
fire(...lastArgs);
|
|
}, T);
|
|
};
|
|
}
|
|
|
|
|
|
prev_text = ""; // previous text, this is used to check chat changes
|
|
prev_text_already_pushed = ""; // previous text already pushed to audio, this is used to check where we should continue to play audio
|
|
prev_chatbot_index = -1;
|
|
const delay_live_text_update = trigger(3000, on_live_stream_terminate);
|
|
|
|
function on_live_stream_terminate(latest_text) {
|
|
// remove `prev_text_already_pushed` from `latest_text`
|
|
if (audio_debug) console.log("on_live_stream_terminate", latest_text);
|
|
remaining_text = latest_text.slice(prev_text_already_pushed.length);
|
|
if ((!isEmptyOrWhitespaceOnly(remaining_text)) && remaining_text.length != 0) {
|
|
prev_text_already_pushed = latest_text;
|
|
push_text_to_audio(remaining_text);
|
|
}
|
|
}
|
|
function is_continue_from_prev(text, prev_text) {
|
|
abl = 5
|
|
if (text.length < prev_text.length - abl) {
|
|
return false;
|
|
}
|
|
if (prev_text.length > 10) {
|
|
return text.startsWith(prev_text.slice(0, Math.min(prev_text.length - abl, 100)));
|
|
} else {
|
|
return text.startsWith(prev_text);
|
|
}
|
|
}
|
|
function isEmptyOrWhitespaceOnly(remaining_text) {
|
|
// Replace \n and 。 with empty strings
|
|
let textWithoutSpecifiedCharacters = remaining_text.replace(/[\n。]/g, '');
|
|
// Check if the remaining string is empty
|
|
return textWithoutSpecifiedCharacters.trim().length === 0;
|
|
}
|
|
function process_increased_text(remaining_text) {
|
|
// console.log('[is continue], remaining_text: ', remaining_text)
|
|
// remaining_text starts with \n or 。, then move these chars into prev_text_already_pushed
|
|
while (remaining_text.startsWith('\n') || remaining_text.startsWith('。')) {
|
|
prev_text_already_pushed = prev_text_already_pushed + remaining_text[0];
|
|
remaining_text = remaining_text.slice(1);
|
|
}
|
|
if (remaining_text.includes('\n') || remaining_text.includes('。')) { // determine remaining_text contain \n or 。
|
|
// new message begin!
|
|
index_of_last_sep = Math.max(remaining_text.lastIndexOf('\n'), remaining_text.lastIndexOf('。'));
|
|
// break the text into two parts
|
|
tobe_pushed = remaining_text.slice(0, index_of_last_sep + 1);
|
|
prev_text_already_pushed = prev_text_already_pushed + tobe_pushed;
|
|
// console.log('[is continue], push: ', tobe_pushed)
|
|
// console.log('[is continue], update prev_text_already_pushed: ', prev_text_already_pushed)
|
|
if (!isEmptyOrWhitespaceOnly(tobe_pushed)) {
|
|
// console.log('[is continue], remaining_text is empty')
|
|
push_text_to_audio(tobe_pushed);
|
|
}
|
|
}
|
|
}
|
|
function process_latest_text_output(text, chatbot_index) {
|
|
if (text.length == 0) {
|
|
prev_text = text;
|
|
prev_text_mask = text;
|
|
// console.log('empty text')
|
|
return;
|
|
}
|
|
if (text == prev_text) {
|
|
// console.log('[nothing changed]')
|
|
return;
|
|
}
|
|
|
|
var is_continue = is_continue_from_prev(text, prev_text_already_pushed);
|
|
if (chatbot_index == prev_chatbot_index && is_continue) {
|
|
// on_text_continue_grow
|
|
remaining_text = text.slice(prev_text_already_pushed.length);
|
|
process_increased_text(remaining_text);
|
|
delay_live_text_update(text); // in case of no \n or 。 in the text, this timer will finally commit
|
|
}
|
|
else if (chatbot_index == prev_chatbot_index && !is_continue) {
|
|
if (audio_debug) console.log('---------------------');
|
|
if (audio_debug) console.log('text twisting!');
|
|
if (audio_debug) console.log('[new message begin]', 'text', text, 'prev_text_already_pushed', prev_text_already_pushed);
|
|
if (audio_debug) console.log('---------------------');
|
|
prev_text_already_pushed = "";
|
|
delay_live_text_update(text); // in case of no \n or 。 in the text, this timer will finally commit
|
|
}
|
|
else {
|
|
// on_new_message_begin, we have to clear `prev_text_already_pushed`
|
|
if (audio_debug) console.log('---------------------');
|
|
if (audio_debug) console.log('new message begin!');
|
|
if (audio_debug) console.log('[new message begin]', 'text', text, 'prev_text_already_pushed', prev_text_already_pushed);
|
|
if (audio_debug) console.log('---------------------');
|
|
prev_text_already_pushed = "";
|
|
process_increased_text(text);
|
|
delay_live_text_update(text); // in case of no \n or 。 in the text, this timer will finally commit
|
|
}
|
|
prev_text = text;
|
|
prev_chatbot_index = chatbot_index;
|
|
}
|
|
|
|
const audio_push_lock = new FIFOLock();
|
|
async function push_text_to_audio(text) {
|
|
if (!allow_auto_read_tts_flag) {
|
|
return;
|
|
}
|
|
await audio_push_lock.lock();
|
|
var lines = text.split(/[\n。]/);
|
|
for (const audio_buf_text of lines) {
|
|
if (audio_buf_text) {
|
|
// Append '/vits' to the current URL to form the target endpoint
|
|
const url = `${window.location.href}vits`;
|
|
// Define the payload to be sent in the POST request
|
|
const payload = {
|
|
text: audio_buf_text, // Ensure 'audio_buf_text' is defined with valid data
|
|
text_language: "zh"
|
|
};
|
|
// Call the async postData function and log the response
|
|
post_text(url, payload, send_index);
|
|
send_index = send_index + 1;
|
|
if (audio_debug) console.log(send_index, audio_buf_text);
|
|
// sleep 2 seconds
|
|
if (allow_auto_read_tts_flag) {
|
|
await delay(3000);
|
|
}
|
|
}
|
|
}
|
|
audio_push_lock.unlock();
|
|
}
|
|
|
|
|
|
send_index = 0;
|
|
recv_index = 0;
|
|
to_be_processed = [];
|
|
async function UpdatePlayQueue(cnt, audio_buf_wave) {
|
|
if (cnt != recv_index) {
|
|
to_be_processed.push([cnt, audio_buf_wave]);
|
|
if (audio_debug) console.log('cache', cnt);
|
|
}
|
|
else {
|
|
if (audio_debug) console.log('processing', cnt);
|
|
recv_index = recv_index + 1;
|
|
if (audio_buf_wave) {
|
|
audioPlayer.enqueueAudio(audio_buf_wave);
|
|
}
|
|
// deal with other cached audio
|
|
while (true) {
|
|
find_any = false;
|
|
for (i = to_be_processed.length - 1; i >= 0; i--) {
|
|
if (to_be_processed[i][0] == recv_index) {
|
|
if (audio_debug) console.log('processing cached', recv_index);
|
|
if (to_be_processed[i][1]) {
|
|
audioPlayer.enqueueAudio(to_be_processed[i][1]);
|
|
}
|
|
to_be_processed.pop(i);
|
|
find_any = true;
|
|
recv_index = recv_index + 1;
|
|
}
|
|
}
|
|
if (!find_any) { break; }
|
|
}
|
|
}
|
|
}
|
|
|
|
function post_text(url, payload, cnt) {
|
|
if (allow_auto_read_tts_flag) {
|
|
postData(url, payload, cnt)
|
|
.then(data => {
|
|
UpdatePlayQueue(cnt, data);
|
|
return;
|
|
});
|
|
} else {
|
|
UpdatePlayQueue(cnt, null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
notify_user_error = false
|
|
// Create an async function to perform the POST request
|
|
async function postData(url = '', data = {}) {
|
|
try {
|
|
// Use the Fetch API with await
|
|
const response = await fetch(url, {
|
|
method: 'POST', // Specify the request method
|
|
body: JSON.stringify(data), // Convert the JavaScript object to a JSON string
|
|
});
|
|
// Check if the response is ok (status in the range 200-299)
|
|
if (!response.ok) {
|
|
// If not OK, throw an error
|
|
console.info('There was a problem during audio generation requests:', response.status);
|
|
// if (!notify_user_error){
|
|
// notify_user_error = true;
|
|
// alert('There was a problem during audio generation requests:', response.status);
|
|
// }
|
|
return null;
|
|
}
|
|
// If OK, parse and return the JSON response
|
|
return await response.arrayBuffer();
|
|
} catch (error) {
|
|
// Log any errors that occur during the fetch operation
|
|
console.info('There was a problem during audio generation requests:', error);
|
|
// if (!notify_user_error){
|
|
// notify_user_error = true;
|
|
// alert('There was a problem during audio generation requests:', error);
|
|
// }
|
|
return null;
|
|
}
|
|
} |