Синтез речи на форуме

òbi

-Wan Kenobi
Administrator
Звезда Отваги
Участник Новогоднего Фонда 2023
Победитель в номинации 2023
Почетный знак Модератора (II степени)
Почетный знак Модератора (I степени)
На страже закона
Стрелок
Орден Почета
Мастер реакций
Знаток письма
Неукротимое пламя
Медаль Благодарности
Победитель в номинации 2022
Победитель в номинации 2021
Участник Новогоднего Фонда 2021
Часть системы
Стальной Визионер
Web разработчик
Любитель реакций
Куратор Данных
Старожил I степени
Медаль за активность на Форуме
За заслуги перед форумом
За веру и верность форуму
Победитель в номинации 2020
Победитель в номинации 2019
Сообщения
985
Розыгрыши
8
Решения
15
Репутация
1 882
Реакции
961
Баллы
1 828
Да да, синтез речи на форуме. То есть озвучиваем текстовые сообщения на форуме по средствам встроенных возможностей браузера (Web Speech API)

Вот скрипт:
JavaScript:
const url = window.location.href
if (url.startsWith('https://mmo-dev.info/threads/')) {
    window.addEventListener("load", () => {
        class TTS {
            #_speechSynthesis = window.speechSynthesis || null;
            #_speechSynthesisUtterance = new SpeechSynthesisUtterance();
           
            voiceId = -1;
           
            constructor() {
                this.#_speechSynthesisUtterance.volume = localStorage.getItem('tts-volume') || 1;
                this.#_speechSynthesisUtterance.pitch = localStorage.getItem('tts-pitch') || 1;
                this.#_speechSynthesisUtterance.rate = localStorage.getItem('tts-rate') || 1;
            }
           
            loadVoicesAsync() {
                return new Promise((resolve, reject) => {
                    if (this.#_speechSynthesis) {
                        let timerId, attempts = 0;
                        timerId = setInterval(() => {
                            if (++attempts > 20) {
                                reject();
                                clearInterval(timerId);
                            } else {
                                if (this.#_speechSynthesis.getVoices().length !== 0) {
                                    resolve();
                                    clearInterval(timerId);
                                }
                            }
                        }, 100);
                    } else {
                        reject('SpeechSynthesis is not supported!');
                    }
                });
            }
           
            getSpeechSynthesis() {
                return this.#_speechSynthesis;
            }
           
            getSpeechSynthesisUtterance() {
                return this.#_speechSynthesisUtterance;
            }
           
            setVolume(val) {
                localStorage.setItem('tts-volume', this.#_speechSynthesisUtterance.volume = val);
            }
           
            setPitch(val) {
                localStorage.setItem('tts-pitch', this.#_speechSynthesisUtterance.pitch = val);
            }
           
            setRate(val) {
                localStorage.setItem('tts-rate', this.#_speechSynthesisUtterance.rate = val);
            }
           
            setVoice(val) {
                this.#_speechSynthesisUtterance.voice = val;
            }
           
            setLang(val) {
                this.#_speechSynthesisUtterance.lang = val;
            }
        }

        const tts = new TTS();

        tts.loadVoicesAsync().then(() => {  
            const ss = tts.getSpeechSynthesis();
            ss.cancel();
            //ss.onvoiceschanged = () => {};
           
            const ssu = tts.getSpeechSynthesisUtterance();
            const voices = ss.getVoices()/*.filter(v => {
                return v.lang == document.documentElement.lang
            })*/;
           
            if (voices.length > 0) {
                tts.setVoice(voices[0]);
                tts.setLang(voices[0].lang);
            }

            try {
                let messages = document.querySelectorAll('article.message-body.js-selectToQuote > div > div.bbWrapper');

                messages.forEach((message, index) => {
                    let panel = document.createElement('div');
                    panel.className = 'tts-panel';
                    panel.style = 'width: 500px; margin-left: auto;';
                   
                    // Voice ----------------------
                    let fieldsetVoice = document.createElement('fieldset');
                    let legendVoice = document.createElement('legend');          
                    legendVoice.innerText = 'Voice';
                   
                    fieldsetVoice.appendChild(legendVoice);
                   
                    let selectVoice = document.createElement('select');
                    selectVoice.id = selectVoice.name = 'tts-voice-' + index;
                    selectVoice.className = 'tts-voice';
                    voices.forEach(voice => {
                        const option = document.createElement("option");
                        option.value = voice.voiceURI;
                        option.innerText = voice.name;
                       
                        selectVoice.appendChild(option);
                    });
                    selectVoice.onchange = (e) => {
                        voices.forEach(voice => {
                            const value = e.target.options[e.target.selectedIndex].value;
                            if (voice.voiceURI === value) {
                                tts.setVoice(voice);
                                tts.setLang(voice.lang);
                            }
                        });
                    };
                    fieldsetVoice.appendChild(selectVoice);
                   
                    let btnStop = document.createElement('button');
                    btnStop.id = 'tts-stop-' + index;
                    btnStop.innerHTML = 'stop';
                    btnStop.disabled = true;
                    btnStop.onclick = (e) => {
                        btnStop.disabled = true;
                        ss.cancel();
                    };
                   
                    let btnPlayResume = document.createElement('button');
                    btnPlayResume.style = 'margin: 0 5px;';
                    btnPlayResume.id = 'tts-play-pause-' + index;
                    btnPlayResume.innerText = 'play';
                    btnPlayResume.onclick = (e) => {
                        if (tts.voiceId !== index) {
                            ss.cancel();
                        }
                       
                        setTimeout(() => {
                            //console.info('ss', ss);
                            //console.info('ssu', ssu);
                           
                            /*if (ss.pending) {
                                return;
                            }*/
                           
                            if (ss.speaking && ss.paused) {
                                ss.resume();
                            } else if (ss.speaking) {
                                ss.pause();
                            } else {
                                tts.voiceId = index;
                               
                                ssu.text = message.innerText.trim().replace(/\s+/g, ' ');
                                //ssu.onboundary = () => {};
                                //ssu.onmark = () => {};
                                ssu.onstart = (e) => {
                                    btnStop.disabled = false;
                                    btnPlayResume.innerText = 'pause';
                                };
                                ssu.onpause = (e) => {
                                    btnPlayResume.innerText = 'resume';
                                };
                                ssu.onresume = (e) => {
                                    btnPlayResume.innerText = 'pause';
                                };
                                ssu.onend = (e) => {
                                    btnStop.disabled = true;
                                    btnPlayResume.innerText = 'play';
                                };
                                ssu.onerror = (e) => {
                                    console.info('SpeechSynthesisUtterance', e)
                                }
                               
                                ss.speak(ssu);
                            }
                        }, 100);
                    }
                   
                    fieldsetVoice.appendChild(btnPlayResume);
                    fieldsetVoice.appendChild(btnStop);
                   
                    panel.appendChild(fieldsetVoice);
                    // ------------------------

                    // Volume ------------------
                    let fieldsetVolume = document.createElement('fieldset');
                    let legendVolume = document.createElement('legend');          
                    legendVolume.innerText = 'Volume (' + parseInt( ssu.volume * 100 ) + '%)';
                   
                    fieldsetVolume.appendChild(legendVolume);
                   
                    let inputVolume = document.createElement('input');
                    inputVolume.id = inputVolume.name = 'tts-volume-' + index;
                    inputVolume.className = 'tts-volume';
                    inputVolume.style = 'width:100%';
                    inputVolume.type = 'range';
                    inputVolume.min = 0;
                    inputVolume.max = 1;
                    inputVolume.step = 0.1;
                    inputVolume.value = ssu.volume;
                    inputVolume.onchange = (e) => {
                        legendVolume.innerText = 'Volume (' + parseInt( e.target.value * 100 ) + '%)';
                        tts.setVolume(e.target.value);
                    };
                    fieldsetVolume.appendChild(inputVolume);          
                    panel.appendChild(fieldsetVolume);  
                    // ----------------
                   
                    // Pitch ------------------
                    let fieldsetPitch = document.createElement('fieldset');
                    let legendPitch = document.createElement('legend');          
                    legendPitch.innerText = 'Pitch (' + ssu.pitch + ')';
                   
                    fieldsetPitch.appendChild(legendPitch);
                   
                    let inputPitch = document.createElement('input');
                    inputPitch.id = inputPitch.name = 'tts-pitch-' + index;
                    inputPitch.className = 'tts-pitch';
                    inputPitch.style = 'width:100%';
                    inputPitch.type = 'range';
                    inputPitch.min = 0;
                    inputPitch.max = 2;
                    inputPitch.step = 0.5;
                    inputPitch.value = ssu.pitch;
                    inputPitch.onchange = (e) => {
                        legendPitch.innerText = 'Pitch (' + e.target.value + ')';
                        tts.setPitch(e.target.value);
                    };
                    fieldsetPitch.appendChild(inputPitch);          
                    panel.appendChild(fieldsetPitch);  
                    // ----------------
                   
                    // Rate ------------------
                    let fieldsetRate = document.createElement('fieldset');
                    let legendRate = document.createElement('legend');          
                    legendRate.innerText = 'Rate (' + ssu.rate + ')';
                   
                    fieldsetRate.appendChild(legendRate);
                   
                    let inputRate = document.createElement('input');
                    inputRate.id = inputRate.name = 'tts-rate-' + index;
                    inputRate.className = 'tts-rate';
                    inputRate.style = 'width:100%';
                    inputRate.type = 'range';
                    inputRate.min = 0;
                    inputRate.max = 10;
                    inputRate.step = 1;
                    inputRate.value = ssu.rate;
                    inputRate.onchange = (e) => {
                        legendRate.innerText = 'Rate (' + e.target.value + ')';
                        tts.setRate(e.target.value);
                    };
                    fieldsetRate.appendChild(inputRate);          
                    panel.appendChild(fieldsetRate);  
                    // ---------------------      
                   
                    message.before(panel);
                })
            } catch (e) {
                console.info(e)
            }
        }).catch((e) => {
            console.info('TTS:', e)
        })
    });
}

Для установки скрипта на firefox я использовал расширение . После установки расширения достаточно в нем выбрать "Создать пользовательский скрипт", добавить наш код и сохранить.

Как-то так это все безобразие выглядит :Wahaha:

Посмотреть вложение Новый проект2.mp4
Посмотреть вложение Новый проект.mp4

Вся необходимая информация по Web Speech API была взята с этого сайта
 
Последнее редактирование:

òbi, А синтез речи можно будет для чата ММО отключить?
 
òbi, я думаю лучше остановиться на сообщениях форума.
 
Чисто дунди и психа слушать)
В продолжение серии "дело было вечером, делать было нечего" :)

Скрипт для воспроизведения вновь появившихся сообщений в чата:
JavaScript:
const url = window.location.href;
const urls = [ 'https://mmo-dev.info/', 'https://mmo-dev.info/chat/' ];
if (urls.includes(url)) {
    window.addEventListener("load", () => {
        setTimeout(() => {
            const tts = new TTS();
            tts.loadVoicesAsync().then(() => {
                const ss = tts.getSpeechSynthesis();
                ss.cancel();

                const ssu = tts.getSpeechSynthesisUtterance();
                const voices = ss.getVoices();

                if (voices.length > 0) {
                    tts.setVoice(voices[0]);
                    tts.setLang(voices[0].lang);
                }

                let chat = document.querySelector('.messages.block-body > .content.block-row');
                new MutationObserver((mutations) => {
                    for (let mutation of mutations) {
                        if (mutation.type === 'childList') {
                            mutation.addedNodes.forEach(node => {
                                if (node !== undefined && node.className?.includes('message')) {
                                    let childrenCollection = Array.from(node.children);
                                    let messageText = childrenCollection.find(el => el.className === 'message-text');
                                    let username = childrenCollection.find(el => el.className === 'username');

                                    if (username && messageText) {
                                        ssu.text = username.innerText + ' ' + messageText.innerText;
                                        ss.speak(ssu);
                                    }
                                }
                            });
                        }
                    }
                }).observe(chat, { childList: true });
            }).catch(e => {
                console.info('TTS:', e)
            });
        }, 2000);
    });
}

class TTS {
    #_speechSynthesis = window.speechSynthesis || null;
    #_speechSynthesisUtterance = new SpeechSynthesisUtterance();

    #_voiceId = -1;

    constructor() {
        this.#_speechSynthesisUtterance.volume = localStorage.getItem('tts-volume') || 1;
        this.#_speechSynthesisUtterance.pitch = localStorage.getItem('tts-pitch') || 1;
        this.#_speechSynthesisUtterance.rate = localStorage.getItem('tts-rate') || 1;
    }

    loadVoicesAsync() {
        return new Promise((resolve, reject) => {
            if (this.#_speechSynthesis) {
                let timerId, attempts = 0;
                timerId = setInterval(() => {
                    if (++attempts > 20) {
                        reject();
                        clearInterval(timerId);
                    } else {
                        if (this.#_speechSynthesis.getVoices().length !== 0) {
                            resolve();
                            clearInterval(timerId);
                        }
                    }
                }, 100);
            } else {
                reject('SpeechSynthesis is not supported!');
            }
        });
    }

    getVoiceId() {
        return this.#_voiceId;
    }

    setVoiceId(val) {
        this.#_voiceId = val;
    }

    getSpeechSynthesis() {
        return this.#_speechSynthesis;
    }

    getSpeechSynthesisUtterance() {
        return this.#_speechSynthesisUtterance;
    }

    setVolume(val) {
        localStorage.setItem('tts-volume', this.#_speechSynthesisUtterance.volume = val);
    }

    setPitch(val) {
        localStorage.setItem('tts-pitch', this.#_speechSynthesisUtterance.pitch = val);
    }

    setRate(val) {
        localStorage.setItem('tts-rate', this.#_speechSynthesisUtterance.rate = val);
    }

    setVoice(val) {
        this.#_speechSynthesisUtterance.voice = val;
    }

    setLang(val) {
        this.#_speechSynthesisUtterance.lang = val;
    }
}

демо:
Посмотреть вложение Новый проект3.mp4
 
Назад
Сверху Снизу