// ==UserScript==
// @name Voice Control for ChatGPT
// @name-CN ChatGPT 语音助手
// @namespace http://tampermonkey.net/
// @version 0.1.0
// @description Expands ChatGPT with voice control and read aloud. fork from https://chrome.google.com/webstore/detail/eollffkcakegifhacjnlnegohfdlidhn
// @author You
// @match https://chat.openai.com/*
// @match https://chat-shared1.zhile.io/c/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com
// @grant none
// @run-at document-idle
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
// Your code here...
const e = document.createElement("style");
var n;
e.innerHTML = '\n#sai-root {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-top: 10px;\n}\n\n#sai-input-wrapper {\n position: relative;\n cursor: pointer;\n background-color: #e02d2d;\n animation-name: red-pulsating-color;\n animation-duration: 2s;\n animation-iteration-count: infinite;\n max-width: 75%;\n}\n\n#sai-input-wrapper:hover {\n opacity: 0.7;\n}\n\n#sai-input-wrapper div.w-full {\n padding-right: 35px;\n}\n\n#sai-input-wrapper div {\n display: block;\n min-height: 24px;\n color: #fff;\n}\n\n#sai-input-wrapper.is-idle {\n background-color: #9a8e81;\n animation: none;\n}\n\n/*.light #sai-input-wrapper.is-idle {\n background-color: #7f7a89;\n}*/\n\n#sai-input-wrapper.is-idle #sai-speech-button {\n right: 50%;\n margin-right: -13px;\n width: 24px;\n height: 24px;\n top: 12px;\n}\n\n#sai-input-wrapper.is-idle #sai-speech-button svg {\n width: 24px;\n height: 24px;\n}\n\n#sai-speech-button {\n position: absolute;\n top: 10px;\n right: 12px;\n width: 18px;\n transition: 0.5s;\n right: 10px;\n user-select: none;\n}\n\n#sai-speech-button svg {\n width: 18px;\n height: 18px;\n}\n\n#sai-input-wrapper.is-idle #sai-cancel-msg {\n visibility: hidden;\n opacity: 0;\n}\n\n#sai-button-wrapper {\n display: flex;\n justify-content: space-between;\n flex: 1;\n padding: 10px 15px;\n background: #eeeeee;\n margin-left: 15px;\n border-radius: 5px;\n z-index: 10;\n}\n\n.dark #sai-button-wrapper {\n background: #eeeeee4a;\n}\n\n#sai-cancel-msg {\n font-size: 8px;\n color: #fff;\n position: absolute;\n bottom: -7px;\n right: 12px;\n transition: 0.2s;\n user-select: none;\n visibility: visible;\n opacity: 1;\n}\n\n#sai-speech-button path {\n fill: #fff;\n}\n\n#sai-lang-selector-wrapper {\n display: flex;\n align-items: center;\n}\n\n#sai-no-voices {\n font-size: 12px;\n cursor: pointer;\n min-width: 75px;\n text-decoration: underline;\n color: #1abc9c;\n}\n\n#sai-no-voices:hover {\n opacity: 0.5;\n}\n\n#sai-lang-selector {\n font-size: 12px;\n height: 25px;\n padding: 0 10px;\n user-select: none;\n height: 30px;\n}\n\n#sai-lang-selector.sai-hide {\n display: none;\n}\n\n.dark #sai-lang-selector {\n color: #000 !important;\n}\n\n#sai-settings-button {\n background-color: #1a82bc;\n padding: 3px 4px;\n border-radius: 5px;\n}\n\n#sai-settings-button svg {\n width: 24px;\n height: 22px;\n margin-top: 1px;\n}\n\n#sai-skip-read-aloud.sai-active:hover,\n#sai-disable-read-aloud:hover,\n#sai-settings-button:hover {\n opacity: 0.8;\n cursor: pointer;\n}\n\n\n#sai-disable-read-aloud {\n background-color: #1abc9c;\n padding: 3px 4px;\n border-radius: 5px;\n margin-left: 10px;\n margin-right: 10px;\n position: relative;\n}\n\n#sai-disable-read-aloud.disabled {\n background-color: #cb4b4b;\n}\n\n#sai-disable-read-aloud.disabled:before {\n content: "";\n width: 2px;\n height: 25px;\n background-color: #fff;\n position: absolute;\n transform: rotate(45deg);\n left: 13px;\n}\n\n#sai-disable-read-aloud svg {\n fill: rgba(0,0,0,0.0);\n width: 24px;\n}\n\n#sai-skip-read-aloud {\n background-color: #969696;\n padding: 3px 4px;\n border-radius: 5px;\n margin-left: 10px;\n position: relative;\n}\n\n#sai-skip-read-aloud.sai-active {\n animation-name: yellow-pulsating-color;\n animation-duration: 2s;\n animation-iteration-count: infinite;\n background-color: #daa266;\n}\n\n#sai-skip-read-aloud svg {\n fill: #fff;\n height: 16px;\n width: 24px;\n margin-top: 6px;\n}\n\n@media only screen and (max-width:450px) {\n #sai-skip-read-aloud {\n display: none;\n }\n\n .sai-compact #sai-skip-read-aloud {\n display: block;\n }\n}\n\n@keyframes red-pulsating-color {\n 0% {\n background-color: #e02d2d;\n }\n 50% {\n background-color: #ef8585;\n }\n 100 {\n background-color: #e02d2d;\n }\n}\n\n@keyframes yellow-pulsating-color {\n 0% {\n background-color: #daa266;\n }\n 50% {\n background-color: #c78d4f;\n }\n 100 {\n background-color: #daa266;\n }\n}\n\ndiv.px-3.pt-2.pb-3.text-center.text-xs {\n padding: 6px;\n font-size: 0.6rem;\n}\n\n#sai-error-message {\n position: fixed;\n top: 0;\n right: 0;\n width: 200px;\n min-height: 100px;\n background-color: #cb4b4b;\n padding: 15px;\n box-shadow: rgb(0 0 0 / 21%) 0px 0px 10px 2px;\n color: #fff;\n font-weight: bold;\n font-size: 12px;\n}\n\n\n/* ==== SETTINGS ====== */\n\n#sai-settings-view {\n position: fixed;\n right: 0;\n top: 0;\n width: 100%;\n background-color: rgb(30 30 30 / 90%);\n height: 100vh;\n padding: 25px;\n z-index: 100000;\n}\n\n#sai-settings-view.sai-hide {\n display: none;\n}\n\n#sai-settings-view-inner {\n max-width: 700px;\n margin: 0 auto;\n display: flex;\n justify-content: space-between;\n}\n\n.sai-settings-col {\n width: 45%;\n}\n\n#sai-settings-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n max-width: 700px;\n margin: 0 auto;\n border-bottom: 1px solid #777;\n margin-bottom: 25px;\n padding-bottom: 10px;\n}\n\n#sai-settings-view a {\n color: #1abc9c;\n text-decoration: none;\n font-weight: bold;\n}\n\n.sai-button {\n all: unset;\n background-color: #1abc9c;\n color: #fff;\n padding: 10px 15px;\n font-weight: bold;\n border-radius: 5px;\n font-size: 14px;\n color: #fff !important;\n cursor: pointer;\n line-height: 1.6;\n}\n\n.sai-button:hover {\n opacity: 0.8;\n}\n\n\n#sai-settings-view h3,\n#sai-settings-view h4,\n#sai-settings-view p {\n color: #fff;\n margin-bottom: 25px;\n}\n\n#sai-settings-view li {\n color: #fff;\n}\n\n#sai-settings-view h3 {\n font-size: 20px;\n}\n\n#sai-settings-view h4 {\n font-size: 17px;\n font-weight:bold;\n margin-bottom: 15px;\n}\n\n\n.sai-settings-section {\n margin-top: 35px;\n padding-top: 25px;\n border-top: 1px solid #777;\n}\n\n#sai-settings-view li strong {\n color: #ffca92;\n}\n\n#sai-settings-view ul {\n padding-left: 0;\n margin: 0;\n list-style: none;\n}\n\n#sai-settings-view li {\n margin-top: 10px;\n}\n\n#sai-settings-read-aloud-header {\n\n}\n\n#sai-settings-voice-link {\n display: inline-block;\n margin-top: 7px;\n font-size: 12px;\n}\n\n.sai-slidecontainer {\n width: 100%;\n}\n\n.sai-slider {\n -webkit-appearance: none;\n width: 100%;\n height: 15px;\n border-radius: 5px;\n background: #d3d3d3;\n outline: none;\n opacity: 0.7;\n -webkit-transition: 0.2s;\n transition: opacity 0.2s;\n}\n\n.sai-slider:hover {\n opacity: 1;\n}\n\n.sai-slider::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 25px;\n height: 25px;\n border-radius: 50%;\n background: #1abc9c;\n cursor: pointer;\n}\n\n.sai-link-talkio {\n color: #ac99ff !important;\n}\n\n@media only screen and (max-height: 720px) {\n #sai-settings-header {\n margin-bottom: 15px;\n padding-bottom: 0;\n }\n\n #sai-settings-view {\n font-size: 12px;\n overflow-y: auto;\n }\n\n #sai-settings-view h4 {\n font-size: 16px;\n }\n\n .sai-settings-section {\n margin-top: 20px;\n padding-top: 10px;\n }\n}\n\n/* ======== REPEAT BUTTON ======= */\n.sai-repeat-button {\n border-radius: 5px;\n width: 22px;\n height: 22px;\n cursor: pointer;\n position: relative;\n}\n\n.sai-repeat-button.sai-disabled {\n display: none;\n}\n\n.sai-repeat-button svg {\n width: 18px;\n height: 18px;\n margin-top: 2px;\n margin-left: 2px;\n}\n\n.sai-repeat-button path {\n fill: #acacbe !important;\n}\n\n.sai-repeat-button:hover {\n background: #ececf1;\n}\n\n.sai-repeat-button:hover path {\n fill: #40414f !important;\n}\n\n.dark .sai-repeat-button:hover {\n background: #40414f;\n}\n\n.dark .sai-repeat-button:hover path {\n fill: #fff !important;\n}\n\n\n/* ======== HIDE SAI ======= */\n.sai-hidden #sai-input-wrapper,\n.sai-hidden #sai-lang-selector-wrapper,\n.sai-hidden #sai-skip-read-aloud,\n.sai-hidden #sai-disable-read-aloud {\n display: none;\n}\n\n.sai-hidden #sai-button-wrapper {\n background: transparent;\n padding: 0;\n}\n\n.sai-hidden #sai-settings-button {\n border-radius: 5px;\n position: fixed;\n top: 7px;\n right: 45px;\n z-index: 10000;\n}\n\n@media only screen and (min-width: 768px) {\n .sai-hidden #sai-settings-button {\n top: 20px;\n right: 20px\n }\n}\n\n@media only screen and (max-width: 768px) {\n form > div.relative.flex.h-full {\n flex-direction: column;\n }\n\n #sai-input-wrapper {\n height: 50px;\n }\n}\n\n/* ======== SAI COMPACT ======= */\n.sai-compact #sai-root {\n height: 0;\n margin: 0;\n position: relative;\n}\n\n.sai-compact #sai-input-wrapper{\n position: absolute;\n width: 30px;\n height: 30px;\n right: 10px;\n top: 8px;\n border: none;\n z-index: 10;\n}\n\n.sai-compact #sai-input-wrapper.is-idle {\n background: none;\n border: none;\n box-shadow: none;\n opacity: 0.5;\n}\n\n.sai-compact .sai-input {\n display: none !important;\n}\n\n.sai-compact #sai-speech-button {\n width: 20px !important;\n height: 20px !important;\n top: 4px !important;\n right: 0 !important;\n margin-right: 4px !important;\n}\n\n.sai-compact #sai-speech-button svg {\n width: 20px !important;\n height: 20px !important;\n}\n\n.sai-compact #sai-input-wrapper.is-idle #sai-speech-button svg path {\n fill: #999;\n}\n\n.sai-compact #sai-cancel-msg {\n display: none;\n}\n\n.sai-compact #sai-button-wrapper {\n position: absolute;\n bottom: 15px;\n right: 0;\n padding: 5px 7px;\n}\n\n.sai-compact #sai-lang-selector {\n font-size: 10px !important;\n height: 25px;\n}\n\n.sai-compact #sai-settings-button svg,\n.sai-compact #sai-disable-read-aloud svg{\n width: 20px !important;\n height: 20px !important;\n margin-top: 0px !important;\n}\n\n.sai-compact #sai-skip-read-aloud svg {\n width: 20px !important;\n height: 13px !important;\n margin-top: 5px !important;\n}\n\n.sai-compact #sai-disable-read-aloud.disabled:before {\n left: 11px;\n bottom: 1px;\n}\n\n.sai-compact textarea {\n padding-right: 4rem !important;\n}\n\n.sai-compact textarea + button {\n margin-right: 35px;\n}\n\n@media only screen and (max-width: 900px) {\n .sai-compact .flex.ml-1.gap-0.justify-center{\n position: static;\n justify-content: flex-start !important;\n }\n}\n\n@media only screen and (max-width: 768px) {\n .sai-compact .w-full.h-32.flex-shrink-0 {\n margin-top: 25px;\n }\n\n .sai-compact .flex.ml-1.gap-0.justify-center{\n position: absolute;\n bottom: 62px;\n height: 30px;\n }\n}\n\n@media only screen and (min-width: 768px) {\n .sai-compact #sai-input-wrapper {\n top: 12px;\n }\n\n .sai-compact #sai-button-wrapper {\n bottom: 10px;\n }\n\n .sai-compact .flex.ml-1.gap-0.justify-center{\n position: absolute;\n top: -46px;\n max-height: 36px;\n }\n}\n\n',
document.body.appendChild(e),
function (e) {
e.info = "info",
e.warning = "warning",
e.error = "error",
e.verbose = "verbose",
e.success = "success"
}(n || (n = {}));
class t {
constructor(e = !0) {
this.logToConsole = e,
window.addEventListener("sai-print-logs", (() => {
console.log("All logs:"),
console.log(t.allLogs)
}))
}
static info(e, t) {
this.instance.write(e, n.info, t)
}
static success(e, t) {
this.instance.write(e, n.success, t)
}
static warn(e, t) {
this.instance.write(e, n.warning, t)
}
static error(e, t) {
this.instance.write(e, n.error, t)
}
static verbose(e, i) {
this.instance.logToConsole && t.allLogs.push([Date.now(), n.verbose, e, i])
}
static setup() {
if (!t.instance) {
const e = "true" === window.localStorage.getItem("sai-log");
this.instance = new t(e)
}
return t.instance
}
write(e, n, i) {
if (this.logToConsole) {
const s = `color: ${this.getConsoleColor(n)}`;
i ? console.log(`%c[${n}] ${e}`, s, i) : console.log(`%c[${n}] ${e}`, s),
t.allLogs.push([Date.now(), n, e, i])
}
}
getConsoleColor(e) {
return e === n.info ? "#2e99d9" : e === n.warning ? "#ffbb00" : e === n.success ? "#1abc9c" : "#b91e1e"
}
}
t.allLogs = [];
class i {
constructor(e) {
this.element = e,
this.isVisible = !1
}
write(e, n = 3e3) {
this.isVisible && clearTimeout(this.timer),
this.element.innerHTML = e,
this.setVisible(!0),
this.timer = setTimeout((() => {
this.setVisible(!1),
this.element.innerHTML = ""
}), n)
}
setVisible(e) {
this.element.style.display = e ? "block" : "none",
this.isVisible = e
}
}
function s(e, n) {
return e === n || ("zh-CN" === e && "cmn-Hans-CN" === n || ("zh-TW" === e && "cmn-Hant-TW" === n || "zh-HK" === e && "yue-Hant-HK" === n))
}
const a = [
["English (US)", "en-US"],
["English (UK)", "en-GB"],
["English (AU)", "en-AU"],
["English (CA)", "en-CA"],
["English (IN)", "en-IN"],
["English (NZ)", "en-NZ"],
["普通话 (中国大陆)", "cmn-Hans-CN"],
["中文 (台灣)", "cmn-Hant-TW"],
["粵語 (香港)", "yue-Hant-HK"],
["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"],
["Español (MX)", "es-MX"],
["Español (AR)", "es-AR"],
["Español (CO)", "es-CO"],
["Español (PE)", "es-PE"],
["Español (VE)", "es-VE"],
["Euskara", "eu-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"],
["Português (BR)", "pt-BR"],
["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 o = [];
async function r() {
if (o.length > 0)
return o;
const e = await new Promise((e => {
window.speechSynthesis.onvoiceschanged = () => {
const n = window.speechSynthesis.getVoices();
e(n)
}
}));
return a.forEach((n => {
e.some((e => s(e.lang, n[1]))) ? o.push(n) : t.warn(`${n[0]} not supported. Removed from selector.`)
})),
o
}
class l {
constructor(e, n) {
this.selectionCb = e,
this.selected = n,
this.storageKey = "sai-language",
this.setDefaultFromStorage(),
this.element = document.createElement("div"),
this.selector = document.createElement("select"),
this.element.id = "sai-lang-selector-wrapper",
this.selector.id = "sai-lang-selector",
r().then((e => {
if (0 === e.length) {
this.selector.classList.add("sai-hide");
const e = document.createElement("div");
return e.id = "sai-no-voices",
e.innerHTML = "Install voices",
void this.element.appendChild(e)
}
e.forEach((([e, n]) => {
const t = document.createElement("option");
t.innerText = e,
t.value = n,
n === this.selected && (t.selected = !0),
this.selector.appendChild(t)
})),
this.element.appendChild(this.selector),
this.selector.onchange = e => {
const n = e.target;
this.selectLanguage(n.value)
}
}))
}
selectLanguage(e) {
window.localStorage.setItem(this.storageKey, e),
this.selectionCb(e)
}
setDefaultFromStorage() {
let e = window.localStorage.getItem(this.storageKey);
e && (this.selected = e,
this.selectLanguage(e))
}
}
class c {
constructor(e, n, i) {
this.lang = e,
this.waitForContent = i,
this.lastText = "",
this.lastRead = Date.now(),
this.lastUtter = Date.now(),
this.lastUtterCharCount = 0,
this.lastTimeout = 0,
this.lastTimeSinceLastUtter = 0,
this.synth = window.speechSynthesis,
this.queue = [],
this.enabled = !0,
this.storageKey = "sai-read-aloud",
this.queueIdle = !0,
this.disableButton = document.createElement("div"),
this.disableButton.innerHTML = '\n\n\n',
this.disableButton.id = "sai-disable-read-aloud",
this.disableButton.title = "Toggle read aloud",
this.skipButton = document.createElement("div"),
this.skipButton.innerHTML = '\n\n',
this.skipButton.id = "sai-skip-read-aloud",
this.skipButton.title = "Skip read aloud",
window.speechSynthesis.cancel(),
this.disableButton.addEventListener("click", (() => {
this.enabled ? this.disableReadAloud() : this.enableReadAloud()
})),
this.skipButton.onclick = () => {
this.skipReading()
},
this.setReadAloudFromStorage(),
t.info(`reInit ${n}, lastTextLength: ${this.lastText.length}`),
n && this.reset()
}
async runQueue() {
if (t.info(`Queue is idle: ${this.queueIdle}`),
this.queue.length > 0 && this.queueIdle) {
this.skipButton.classList.add("sai-active"),
this.queueIdle = !1;
const e = this.queue.shift();
await this.readAloud(e),
this.queueIdle = !0,
this.skipButton.classList.remove("sai-active"),
this.queue.length > 0 && this.runQueue()
}
}
observerCallback(e) {
const n = this.getText();
if (0 === n.length)
t.info("No text, reset"),
this.reset();
else if (this.waitForContent)
return t.info("Wait for content"),
this.lastText = n,
void(this.waitForContent = !1);
const i = n.replace(this.lastText.trim(), "").trim(),
s = i[i.length - 1],
a = this.lastRead + 1e4 < Date.now();
if (i.length > 0 && ("." === s || "?" === s || "!" === s || ":" === s || "。" === s || a)) {
a && (t.warn(`Long time since last read. Queue length: ${this.queue.length}`),
this.queueIdle = !0),
t.info(`Push to queue: ${i}`);
i.split(".").filter((e => e.length > 0)).forEach((e => {
this.queue.push(e)
})),
this.runQueue(),
this.lastRead = Date.now(),
this.lastText = n
}
}
setLang(e) {
this.lang = e
}
skipReading() {
this.synth.cancel(),
this.queue = [],
this.queueIdle = !0;
const e = document.querySelectorAll(".text-base");
for (var n = 0; n < e.length; n++)
e[n]?.classList.add("sai-skip")
}
repeat(e) {
this.synth.cancel(),
this.queue = [],
this.queueIdle = !0;
const n = this.getText(e);
t.info(`Repeat: ${n}`),
this.queue.push(n),
this.runQueue()
}
readAloud(e) {
return new Promise(((n, i) => {
if (!this.enabled)
return t.info("Read aloud disabled"),
void n(void 0);
if (!e)
return t.info("No text to read"),
void n(void 0);
if (!document.getElementById("sai-root"))
return void n(void 0);
let a = e.replace(/([0-9]+)\.(?=[0-9]+(?!\.))/g, "$1,");
this.synth = window.speechSynthesis;
const o = new SpeechSynthesisUtterance(a),
r = this.synth.getVoices().reverse().filter((e => s(e.lang, this.lang))),
l = window.localStorage.getItem("sai-voice-preference" + this.lang),
c = r.find((e => e.voiceURI === l)) ?? r[0];
if (!c)
throw new Error(`unknown voice: ${c} lang: ${this.lang}`);
o.volume = 1,
o.voice = c;
const d = window.localStorage.getItem("sai-voice-speed-v2");
d || t.error("No speed stored in storage");
const p = function (e) {
switch (e) {
case "1":
return .1;
case "2":
return .2;
case "3":
return .3;
case "4":
return .4;
case "5":
return .5;
case "6":
return .6;
case "7":
return .7;
case "8":
return .8;
case "9":
return .9;
case "10":
default:
return 1;
case "11":
return 1.1;
case "12":
return 1.13;
case "13":
return 1.15;
case "14":
return 1.17;
case "15":
return 1.2;
case "16":
return 1.25;
case "17":
return 1.3;
case "18":
return 1.35;
case "19":
return 1.4;
case "20":
return 1.45
}
}(d);
let h;
o.rate = p,
t.success(`Voice name: ${c.name}, lang: ${c.lang}, rate: ${p}: ${a}`);
const u = () => {
const [e, n] = function (e, n, i, s, a, o) {
const r = Date.now() - i,
l = function (e, n, t) {
let i = 100;
return "zh-CN" !== e && "zh-TW" !== e && "zh-HK" !== e || (i = 240),
"zh-TW" === e && (i = 300),
"ja-JP" === e && (i = 260),
"ko-KR" === e && (i = 240),
7e3 + n * i * (1 / t)
}(e, a, n);
if (t.warn(`[resumeInfinity] Time since last utter: ${r.toFixed(1)}. Timeout: ${l.toFixed(1)}. Last char count: ${a}`),
window.navigator.userAgent.search("Mac") > -1 && 0 === r && o > 0) {
const e = s - o,
n = e / s * 100;
t.warn(`Last timeout safety gap: ${e.toFixed(1)}ms. ${n.toFixed(1)}%`),
n < 25 && t.error(`________Safety gap ${n.toFixed(1)}% too low!________`)
}
return r > l ? (t.error(`No utter timeout ${l.toFixed(1)} - cancel.`),
window.speechSynthesis.cancel(),
setTimeout((() => {
window.speechSynthesis.resume()
}), 50),
[0, 0]) : [l, r]
}(c.lang, p, this.lastUtter, this.lastTimeout, this.lastUtterCharCount, this.lastTimeSinceLastUtter);
this.lastTimeout = e,
this.lastTimeSinceLastUtter = n,
window.speechSynthesis.pause(),
window.speechSynthesis.resume(),
h = setTimeout(u, 7e3)
};
o.addEventListener("error", (e => {
t.error(`Read aloud error ${e.error}`, e),
n(void 0),
clearTimeout(h)
})),
o.addEventListener("start", (() => {
t.info(`Speech has started. Volume: ${o.volume}`),
this.lastUtter = Date.now(),
u()
})),
o.addEventListener("end", (function (e) {
t.info("Speech has ended"),
n(void 0),
clearTimeout(h)
})),
o.addEventListener("pause", (function (e) {
t.verbose("Speech has paused", e)
})),
o.addEventListener("resume", (function (e) {
t.verbose("Speech has resumed", e)
})),
o.addEventListener("boundary", (function (e) {
t.verbose(`Speech reached boundary. CharIndex: ${e.charIndex}`, e)
})),
o.addEventListener("mark", (function (e) {
t.info("Speech reached mark", e)
})),
this.synth.speak(o),
this.lastUtterCharCount = a.length
}))
}
enableReadAloud() {
this.enabled = !0,
this.disableButton.classList.remove("disabled"),
this.updateStorage(),
document.querySelectorAll(".sai-repeat-button").forEach((e => {
e.classList.remove("sai-disabled")
}))
}
disableReadAloud() {
this.queue = [],
this.queueIdle = !0,
this.synth.cancel(),
this.disableButton.classList.add("disabled"),
this.enabled = !1,
this.updateStorage(),
document.querySelectorAll(".sai-repeat-button").forEach((e => {
e.classList.add("sai-disabled")
}))
}
updateStorage() {
window.localStorage.setItem(this.storageKey, this.enabled.toString())
}
setReadAloudFromStorage() {
const e = window.localStorage.getItem(this.storageKey);
e && (this.enabled = "true" === e,
this.enabled ? this.enableReadAloud() : this.disableReadAloud())
}
getText(e) {
const n = document.querySelectorAll(".text-base:not(.sai-skip) .markdown"),
t = (e || n[n.length - 1])?.children ?? [];
let i = "";
for (const e of t)
"PRE" !== e.nodeName && (i += e.textContent);
return i = i.replace(/`/g, "").replace(/\*/g, "").replace(/\"/g, "").replace(/\\n/g, "").replace(/\\t/g, "").replace(/\\b/g, "").replace(/(/g, " (").replace(/)/g, ") ").replace(/?/g, "? ").replace(/:/g, ": ").replace(/!/g, "! ").replace(/。/g, ". "),
i
}
reset() {
t.warn("RESET read aloud queue"),
this.queue = [],
this.lastRead = Date.now()
}
}
class d {
constructor(e, n, i) {
this.lang = e,
this.errorMessage = n,
this.transcript = "",
this.recognition = new webkitSpeechRecognition,
this.isRecording = !1,
this.recognition.continuous = !0,
this.recognition.interimResults = !0,
this.recognition.onstart = () => {},
this.recognition.onresult = e => {
let n = "";
for (let t = e.resultIndex; t < e.results.length; ++t)
e.results[t].isFinal ? this.isRecording && (this.transcript += e.results[t][0].transcript,
i(this.transcript)) : n += e.results[t][0].transcript;
this.isRecording && i(this.transcript + n)
},
this.recognition.onerror = e => {
let n = e.error;
"not-allowed" === e.error && (n = "The webpage is not allowed to access your microphone"),
"no-speech" === e.error && (n = "No sound from the microphone");
let i = `\n \n Error from Voice Control:\n
\n ${n}\n
\n \n See voicecontrol.chat/support for help\n \n \n `;
this.errorMessage.write(i, 8e3),
t.error(`recognition.onerror ${e.error}`)
},
this.recognition.onend = () => {
t.info("Ended"),
this.endCallback?.()
}
}
start(e) {
t.info("Start"),
this.endCallback = e,
this.recognition.lang = this.lang,
this.recognition.start(),
this.isRecording = !0
}
stop() {
t.info(`Stop: ${this.transcript}`),
this.isRecording = !1,
this.recognition.stop(),
this.endCallback = void 0
}
reset() {
this.isRecording = !1,
this.transcript = ""
}
setLang(e) {
this.lang = e
}
}
class p {
constructor(e) {
this.readAloud = e,
this.showCompactUi = "true" === window.localStorage.getItem("sai-compact-ui"),
this.appIsHidden = "true" === window.localStorage.getItem('"sai-hidden"'),
this.settingsView = document.createElement("div"),
this.settingsView.innerHTML = `\n
\n If you have trouble loading voices or need help troubleshooting please\n \n see the FAQ.\n \n
\n\n\n If you have suggestions on how to improve the extension please share your ideas\n \n here.\n \n
\nUpgrade your language learning experience with Talkio AI,\n the premium version of this extension designed specifically for language learners.
\n\n The extension is created by Theis Frøhlich\n
\n Please leave a review\n if you like this extension.\n