// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // 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; } }