[SNOLAB] I Heard Telegram Speaking

[SNOLAB] Speak latest telegram message With TTS technology just in your browser. 1. Speak latest message your received 2. Speak what you just send. 3. Send what you saying

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @deprecated         Use my new script [SNOLAB] [Mulango] Telegram Translator
// @name               [SNOLAB] I Heard Telegram Speaking
// @namespace          [email protected]
// @author             [email protected]
// @version            0.3.1
// @description        [SNOLAB] Speak latest telegram message With TTS technology just in your browser. 1. Speak latest message your received 2. Speak what you just send. 3. Send what you saying
// @match              https://*.telegram.org/z/
// @grant              none
// @run-at             document-start
// @license            GPL-3.0+
// @supportURL         https://github.com/snomiao/userscript.js/issues
// @contributionURL    https://snomiao.com/donate
// ==/UserScript==
/**
 * 1. Speak latest message your received
 * 2. Speak what you just send.
 * 3. Send what you saying
 */
main();

async function main() {
    if (!globalThis.speechSynthesis)
        return alert(
            "unable to access speechSynthesis service, please update your browser"
        );
    // polyfill and error detect
    globalThis.SpeechRecognition =
        globalThis.SpeechRecognition ||
        globalThis.webkitSpeechRecognition ||
        globalThis.mozillaSpeechRecognition;
    if (!globalThis.SpeechRecognition)
        return alert(
            "unable to access speechSynthesis service, please update your browser"
        );
    listeningLooper().then();
    speakingLooper().then();
    speakingMySelfLooper().then();
}
async function listeningLooper() {
    while (1)
        await tgSend(
            await messageSendingConfirmed(await heard(await userLangGet()))
        );
}
async function speakingLooper() {
    const changed = edger("");
    while (1) {
        await speak(changed(latestMessage()));
        await delay1s();
    }
}
async function speakingMySelfLooper() {
    const changed = edger("");
    while (1) {
        await speak2nd(changed(latestMyMessage()));
        await delay1s();
    }
}
// async function titleLooper(){
//     const titleGet = ()=>document.querySelector('.selected h3').innerHTML
//     const titleUpdate = (t)=>t&&
//     while (1) {await titleUpdate(changed(titleGet())); await delay1s()}
// }

async function heard(lang = "en-US") {
    globalThis.SpeechRecognition =
        globalThis.SpeechRecognition ||
        globalThis.webkitSpeechRecognition ||
        globalThis.mozillaSpeechRecognition;
    console.log("listening");
    const result = await new Promise((resolve, reject) => {
        const sr = new globalThis.SpeechRecognition();
        sr.continuous = true;
        sr.lang = lang;
        sr.onresult = (e) => {
            const result = e?.results?.[0]?.[0]?.transcript || "";
            resolve(result);
        };
        sr.onerror = () => resolve("");
        sr.start();
    });
    console.log("heard", result);
    if (tgMessageEmptyQ()) await tgMessageInput(result + " //heard");
    await new Promise((r) => setTimeout(r, 64));
    return result;
}

async function messageSendingConfirmed(s = "") {
    const r = s.match(/^(话说|話說|你看|呼叫|CQ |もし)(?:\1)(?<msg>.*)$/i)
        ?.groups?.msg;
    if (!r) return;
    await speak("发送 " + r);
    return r;
}

async function speak(s) {
    if (!s) return; // console.error('say empty msg')

    console.log("saying " + s);
    await waitFor(voicesAvailiable);

    const utter = new SpeechSynthesisUtterance(s);
    utter.voice = await userVoiceGet();
    utter.rate = Math.min(Math.max(1, s.length / 60), 4);
    if (speechSynthesis.speaking) speechSynthesis.cancel();
    speechSynthesis.speak(utter);
}

async function speak2nd(s) {
    if (!s) return; // console.error('say empty msg')
    if (speechSynthesis.speaking) return; // low priority
    console.log("saying " + s);
    await waitFor(voicesAvailiable);

    const utter = new SpeechSynthesisUtterance(s);
    utter.voice = await userVoiceGet2nd();
    utter.rate = Math.min(Math.max(1, s.length / 60), 4);
    if (speechSynthesis.speaking) speechSynthesis.cancel();
    speechSynthesis.speak(utter);
}

function voicesAvailiable() {
    return globalThis.speechSynthesis.getVoices().length !== 0;
}
function latestMessage() {
    return [...document.querySelectorAll(".Message:not(.own) .text-content")]
        .map((e) => e.textContent)
        .reverse()[0];
}
function latestMyMessage() {
    return [...document.querySelectorAll(".Message.own .text-content")]
        .map((e) => e.textContent)
        .reverse()[0];
}

function edger(init) {
    return (e) => (e !== init ? (init = e) : undefined);
}

async function tgSend(s) {
    if (!s) return;
    await tgMessageInput(s);
    await new Promise((r) => setTimeout(r, 1e3));
    document.querySelector("#editable-message-text").dispatchEvent(
        new KeyboardEvent("keydown", {
            bubbles: true,
            cancellable: true,
            composed: true,
            key: "Enter",
            code: "Enter",
        })
    );
}

function tgMessageEmptyQ() {
    const content = document.querySelector("#editable-message-text").innerHTML;
    return content === "" || content.endsWith(" //heard");
}

async function tgMessageInput(s) {
    document.querySelector("#editable-message-text").innerHTML = s;
    await new Promise((r) => setTimeout(r, 1e3));
    document.querySelector("#editable-message-text").dispatchEvent(
        new InputEvent("input", {
            bubbles: true,
            cancellable: false,
            inputType: "insertText",
            composed: true,
        })
    );
}

async function waitFor(cond) {
    while (!cond()) await delay1s();
}

async function delay1s() {
    await new Promise((r) => setTimeout(r, 1e3));
}

async function userLangGet() {
    return (await userVoiceGet()).lang;
}

async function userVoiceGet() {
    return (await voicesForUserLang())[0];
}

async function userVoiceGet2nd() {
    const ls = await voicesForUserLang();
    return ls[1] || ls[0];
}
async function voicesForUserLang() {
    await waitFor(voicesAvailiable);
    return navigator.languages
        .map((nlang) =>
            globalThis.speechSynthesis
                .getVoices()
                .filter(({ lang }) => lang.startsWith(nlang))
                .reverse()
        )
        .flat();
    // return globalThis.speechSynthesis.getVoices().filter(({ lang }) => navigator.languages.find(nlang => lang.startsWith(nlang))).reverse();
}