rag / voice_toolkit /voice_toolkit.js
ikun520's picture
Upload 26 files
2909463 verified
//// init.js
function sendMessageToStreamlitClient(type, data) {
const outData = Object.assign({
isStreamlitMessage: true, type: type,
}, data);
window.parent.postMessage(outData, "*");
}
function init() {
sendMessageToStreamlitClient("streamlit:componentReady", {apiVersion: 1});
}
function setFrameHeight(height) {
sendMessageToStreamlitClient("streamlit:setFrameHeight", {height: height});
}
// The `data` argument can be any JSON-serializable value.
function sendDataToPython(data) {
sendMessageToStreamlitClient("streamlit:setComponentValue", data);
}
init()
//// parent.js
// 获取可选语音
window.speechSynthesis.onvoiceschanged = () => {
let voices = window.speechSynthesis.getVoices();
window.parent.voices = voices
let defaultLanguage;
let browserLanguage = navigator.language;
// 参考自 https://chrome.google.com/webstore/detail/voice-control-for-chatgpt/eollffkcakegifhacjnlnegohfdlidhn
const languageAll = [["普通话 (中国大陆)", "zh-CN"], ["中文 (中国台灣)", "zh-TW"], ["粵語 (中国香港)", "zh-HK"], ["English (US)", "en-US"], ["English (UK)", "en-GB"], ["English (IN)", "en-IN"], ["Afrikaans", "af-ZA"], ["Bahasa Indonesia", "id-ID"], ["Bahasa Melayu", "ms-MY"], ["Català", "ca-ES"], ["Čeština", "cs-CZ"], ["Dansk", "da-DK"], ["Deutsch", "de-DE"], ["Español (ES)", "es-ES"], ["Français", "fr-FR"], ["Galego", "gl-ES"], ["Hrvatski", "hr_HR"], ["IsiZulu", "zu-ZA"], ["Íslenska", "is-IS"], ["Italiano", "it-IT"], ["Magyar", "hu-HU"], ["Nederlands", "nl-NL"], ["Norsk bokmål", "nb-NO"], ["Polski", "pl-PL"], ["Português (PT)", "pt-PT"], ["Română", "ro-RO"], ["Slovenčina", "sk-SK"], ["Suomi", "fi-FI"], ["Svenska", "sv-SE"], ["Türkçe", "tr-TR"], ["български", "bg-BG"], ["日本語", "ja-JP"], ["한국어", "ko-KR"], ["Pусский", "ru-RU"], ["Српски", "sr-RS"]];
let languageArray = [];
languageAll.forEach((n => {
if (voices.some(e => e.lang === n[1])) languageArray.push(n)
}))
for (let i = 0; i < voices.length; i++) {
if (voices[i].lang === browserLanguage) {
defaultLanguage = voices[i].lang
break
}
}
if (!defaultLanguage) {
defaultLanguage = languageArray[0][1];
}
let selectedLanguage = localStorage.getItem("selectedLanguage") || defaultLanguage;
let voiceSelectElement = document.getElementById("voice-select");
let languageSelectElement = document.getElementById("language-select");
for (let i = 0; i < languageArray.length; i++) {
let option = document.createElement("option");
option.text = languageArray[i][0];
option.value = languageArray[i][1];
if (option.value === selectedLanguage) {
option.selected = true;
}
languageSelectElement.add(option);
}
for (let i = 0; i < voices.length; i++) {
if (voices[i].lang.includes(selectedLanguage)) {
let option = document.createElement("option");
option.text = voices[i].name;
option.value = voices[i].name;
if (!option.selected) {
if (option.value === localStorage.getItem(selectedLanguage + "selectedVoice")) {
option.selected = true;
}
}
voiceSelectElement.add(option);
}
}
let localStorageVoice = localStorage.getItem(selectedLanguage + "selectedVoice")
if (!localStorageVoice) {
localStorageVoice = "Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)";
localStorage.setItem(selectedLanguage + "selectedVoice", localStorageVoice);
}
for (let option of voiceSelectElement.options) {
if (option.value === localStorageVoice) {
option.selected = true;
break
} else if (!localStorageVoice && option.value.includes("Nature")) {
option.selected = true;
break
}
}
if (voiceSelectElement.options.length === 0) {
voiceSelectElement.style.display = "none";
}
window.parent.selectedVoiceName = voiceSelectElement.value
languageSelectElement.addEventListener("change", function () {
selectedLanguage = languageSelectElement.value;
localStorage.setItem("selectedLanguage", selectedLanguage);
voiceSelectElement.innerHTML = "";
let selectedVoices = voices.filter(
voice => voice.lang === selectedLanguage
);
for (let i = 0; i < selectedVoices.length; i++) {
let option = document.createElement("option");
option.text = selectedVoices[i].name;
option.value = selectedVoices[i].name;
if (option.value === localStorage.getItem(selectedLanguage + "selectedVoice")) {
option.selected = true;
}
voiceSelectElement.add(option);
}
if (voiceSelectElement.options.length > 0) {
voiceSelectElement.style.display = "initial";
} else {
voiceSelectElement.style.display = "none";
}
window.parent.selectedVoiceName = voiceSelectElement.value
});
voiceSelectElement.addEventListener("change", function () {
window.parent.selectedVoiceName = voiceSelectElement.value
localStorage.setItem(selectedLanguage + "selectedVoice", voiceSelectElement.value);
});
}
//// record.js
let recordBtn = document.getElementById('record-btn');
const toggleBtn = document.getElementById("toggle-btn");
const speedSelect = document.getElementById('speed');
// 语音识别对象
let recognition = new webkitSpeechRecognition;
recognition.continuous = true; // 设置为连续模式
recognition.interimResults = true; // 获取中间结果
let voiceTranscriptFinal = '';
let isRecording = false;
ifAutoPlay = localStorage.getItem("autoPlay")
if (ifAutoPlay) {
window.parent.autoPlay = ifAutoPlay === 'yes';
} else {
window.parent.autoPlay = true;
}
let btnClassList = toggleBtn.querySelector('i').classList
if (window.parent.autoPlay) {
btnClassList.add('fa-volume-high');
btnClassList.remove('fa-volume-xmark');
} else {
btnClassList.remove('fa-volume-high');
btnClassList.add('fa-volume-xmark');
}
speed = localStorage.getItem("speed")
if (speed) {
speedSelect.value = speed
}
speedSelect.addEventListener("change", function () {
localStorage.setItem("speed", speedSelect.value)
})
function toggleRecording() {
isRecording = !isRecording;
recordBtn.classList.toggle('recording', isRecording);
if (recordBtn.classList.contains('recording')) {
if (window.parent.selectedVoiceName) {
recognition.lang = window.parent.voices.find(function (v) {
return v.name === window.parent.selectedVoiceName;
}).lang;
}
let zhLang = [["zh-CN", "cmn-Hans-CN"], ["zh-TW", "cmn-Hant-TW"], ["zh-HK", "yue-Hant-HK"]]
for (let i = 0; i < zhLang.length; i++) {
if (recognition.lang === zhLang[i][0]) {
recognition.lang = zhLang[i][1]
break
}
}
recognition.start();
} else {
recognition.stop();
voiceTranscriptFinal = ''
}
}
recordBtn.addEventListener('click', toggleRecording);
recognition.onresult = function (event) {
let voiceTranscript = '';
let flag = 'interim'
for (let i = event.resultIndex; i < event.results.length; i++) {
let transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
voiceTranscriptFinal += transcript;
flag = 'final'
} else {
voiceTranscript += transcript;
}
}
voiceTranscript = voiceTranscriptFinal + voiceTranscript
if (flag === 'interim') {
sendDataToPython({
"value": {'voice_result': {'value': voiceTranscript, 'flag': flag}}
})
} else {
sendDataToPython({
"value": {'voice_result': {'value': voiceTranscriptFinal, 'flag': flag}}
})
}
}
// 保证未点击结束时录音处于开启状态
recognition.onend = function () {
if (recordBtn.classList.contains('recording')) {
recognition.start();
}
}
// 自动播放按钮
toggleBtn.addEventListener('click', function () {
let btnClassList = toggleBtn.querySelector('i').classList
if (btnClassList.contains('fa-volume-high')) {
synth.cancel()
clearTimeout(TIMEOUT_KEEP_SYNTHESIS_WORKING)
btnClassList.add('fa-volume-xmark');
btnClassList.remove('fa-volume-high');
window.parent.autoPlay = false;
localStorage.setItem("autoPlay", "no");
} else {
btnClassList.add('fa-volume-high');
btnClassList.remove('fa-volume-xmark');
window.parent.autoPlay = true;
localStorage.setItem("autoPlay", "yes");
}
})
//// autoPlay.js 参考自开源项目https://github.com/C-Nedelcu/talk-to-chatgpt
// 语音合成对象
const synth = window.parent.speechSynthesis;
synth.cancel()
let TIMEOUT_KEEP_SYNTHESIS_WORKING = null;
let KEEP_CheckNewMessages = null;
let IGNORE_COMMAS = false;
let preResultElementCount
let preHrElementCount
let IGNORE_CODE_BLOCKS = true;
let NEW_ELEMENT = null;
let CURRENT_MESSAGE_SENTENCES;
let CURRENT_MESSAGE_SENTENCES_NEXT_READ;
function KeepSpeechSynthesisActive() {
synth.pause();
synth.resume();
TIMEOUT_KEEP_SYNTHESIS_WORKING = setTimeout(KeepSpeechSynthesisActive, 5000);
}
// 播报函数
function SayOutLoud(text) {
const utterance = new SpeechSynthesisUtterance();
utterance.text = text;
utterance.rate = speedSelect.value;
utterance.pitch = 1;
if (window.parent.selectedVoiceName) {
utterance.voice = window.parent.voices.find(function (v) {
return v.name === window.parent.selectedVoiceName;
});
utterance.lang = utterance.voice.lang
}
utterance.onstart = () => {
clearTimeout(TIMEOUT_KEEP_SYNTHESIS_WORKING);
TIMEOUT_KEEP_SYNTHESIS_WORKING = setTimeout(KeepSpeechSynthesisActive, 5000);
// 播放时关闭录音
if (recordBtn.classList.contains('recording')) {
recordBtn.click()
}
};
utterance.onend = () => {
clearTimeout(TIMEOUT_KEEP_SYNTHESIS_WORKING);
}
synth.speak(utterance);
}
async function SayOut() {
try {
const response = await fetch('https://zklx.xtu.vip.cpolar.top/api-dev/qa/get_text', {
method: 'POST',
});
console.log("Response status:", response.status);
console.log("Response headers:", response.headers);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
console.log("Received text:", text);
SayOutLoud(text);
} catch (error) {
console.error("Error in SayOut:", error);
SayOutLoud("语音请求失败。");
}
}
SayOut();
// 分割文段
function SplitIntoSentences(text) {
const sentences = [];
let currentSentence = "";
for (let i = 0; i < text.length; i++) {
const currentChar = text[i];
// Add character to current sentence
currentSentence += currentChar;
// is the current character a delimiter? if so, add current part to array and clear
if (// Latin punctuation
currentChar === (IGNORE_COMMAS ? '.' : ',')
|| currentChar === (IGNORE_COMMAS ? '.' : ':')
|| currentChar === '.'
|| currentChar === '!'
|| currentChar === '?'
|| currentChar === (IGNORE_COMMAS ? '.' : ';')
|| currentChar === '…'
// Chinese/japanese punctuation
|| currentChar === (IGNORE_COMMAS ? '.' : '、')
|| currentChar === (IGNORE_COMMAS ? '.' : ',')
|| currentChar === '。' || currentChar === '.'
|| currentChar === '!' || currentChar === '?'
|| currentChar === (IGNORE_COMMAS ? '.' : ';')
|| currentChar === (IGNORE_COMMAS ? '.' : ':')) {
if (currentSentence.trim() !== "") sentences.push(currentSentence.trim());
currentSentence = "";
}
}
return sentences;
}
function skipCode(divElement, excludeSelector) {
let excludeElements = Array.from(divElement.querySelectorAll(excludeSelector));
return Array.from(divElement.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '' || node.nodeType === Node.ELEMENT_NODE && !excludeElements.includes(node))
.map(node => node.textContent)
.join('')
}
function CheckNewMessages() {
let ResultElements = window.parent.document.querySelectorAll("div.content-div.assistant")
if (ResultElements.length === preResultElementCount + 1) {
showPlayBtn()
preResultElementCount += 1;
NEW_ELEMENT = ResultElements[ResultElements.length - 1]
CURRENT_MESSAGE_SENTENCES = [];
CURRENT_MESSAGE_SENTENCES_NEXT_READ = 0;
}
let currentText = ''
let sayOutText = '';
if (NEW_ELEMENT) {
currentText = NEW_ELEMENT.textContent;
if (IGNORE_CODE_BLOCKS) {
sayOutText = skipCode(NEW_ELEMENT, 'div.stCodeBlock')
} else {
sayOutText = currentText
}
const newSentences = SplitIntoSentences(sayOutText);
if (newSentences.length !== CURRENT_MESSAGE_SENTENCES.length) {
const nextRead = CURRENT_MESSAGE_SENTENCES_NEXT_READ;
for (let i = nextRead; i < newSentences.length; i++) {
CURRENT_MESSAGE_SENTENCES_NEXT_READ = i + 1;
const lastPart = newSentences[i];
SayOutLoud(lastPart);
}
CURRENT_MESSAGE_SENTENCES = newSentences;
}
}
let HrElementCount = window.parent.document.querySelectorAll("section.main hr").length
let errorElement = window.parent.document.querySelector("div[data-baseweb='notification']")
// hr元素未出现并且没有报错
if ((preHrElementCount !== HrElementCount - 1) && !errorElement) {
KEEP_CheckNewMessages = setTimeout(CheckNewMessages, 500);
} else if (!errorElement && ((ResultElements.length === preResultElementCount + 1) || !NEW_ELEMENT || (NEW_ELEMENT.textContent !== currentText))) {
KEEP_CheckNewMessages = setTimeout(CheckNewMessages, 500);
}
}
//// 监听主页事件
// 监听主页提交按钮,触发监控
function checkFormSubmit() {
let stFormSubmit = window.parent.document.querySelector('button[kind="secondaryFormSubmit"]');
if (stFormSubmit) {
stFormSubmit.addEventListener('click', function () {
if (recordBtn.classList.contains('recording')) {
recordBtn.click()
}
const textInput = window.parent.document.querySelector("textarea[aria-label='**输入:**']");
if (window.parent.autoPlay && textInput.textContent !== '') {
preResultElementCount = window.parent.document.querySelectorAll("div.content-div.assistant").length;
preHrElementCount = window.parent.document.querySelectorAll("section.main hr").length
CheckNewMessages()
}
})
} else {
setTimeout(checkFormSubmit, 500);
}
}
checkFormSubmit()
function showPlayBtn() {
synth.cancel();
if (window.parent.textSound) {
window.parent.textSound.forEach(value => {
{
value.querySelector('#play-btn').style.display = "inline-block";
value.querySelector('#toggle-btn').style.display = "none";
value.querySelector('#stop-btn').style.display = "none";
}
})
}
}
// 监听页面切换chat
function checkChatRadioBlock() {
const stChatRadioBlock = window.parent.document.querySelector('section[data-testid="stSidebar"] div[data-testid="stVerticalBlock"] div[data-testid="stVerticalBlock"]:has(div[role="radiogroup"])');
if (stChatRadioBlock) {
const config = {
attributes: true,
subtree: true
};
// 创建MutationObserver实例
const observer = new MutationObserver((mutationsList, observer) => {
// 监控到变化时的回调函数
for (let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'tabindex') {
showPlayBtn()
clearTimeout(KEEP_CheckNewMessages)
clearTimeout(TIMEOUT_KEEP_SYNTHESIS_WORKING);
}
}
});
// 启动MutationObserver
observer.observe(stChatRadioBlock, config);
} else {
setTimeout(checkChatRadioBlock, 500);
}
}
checkChatRadioBlock()
// 监控页面增删chat
function checkChatButton() {
let stChatButtonAll = window.parent.document.querySelectorAll('section[data-testid="stSidebar"] button[kind="secondary"]');
if (stChatButtonAll.length === 2) {
stChatButtonAll.forEach(stChatButton => {
stChatButton.addEventListener('click', function () {
showPlayBtn()
clearTimeout(KEEP_CheckNewMessages)
clearTimeout(TIMEOUT_KEEP_SYNTHESIS_WORKING);
})
})
} else {
setTimeout(checkChatButton, 500);
}
}
checkChatButton()
//// 设置组件高度
window.addEventListener("DOMContentLoaded", function () {
setFrameHeight(120)
});
// 监控双击事件
document.body.addEventListener("dblclick", function () {
window.parent.document.dispatchEvent(new Event('dblclick'));
});