АвтоTTS Irina с подсветкой слов v2.1

Чтение статей, книг и больших текстов с подсветкой слов, плавной прокруткой и голосом Irina

// ==UserScript==
// @name         АвтоTTS Irina с подсветкой слов v2.1
// @namespace    https://gf.qytechs.cn/ru/users/You
// @version      2.1
// @description  Чтение статей, книг и больших текстов с подсветкой слов, плавной прокруткой и голосом Irina
// @author       You
// @license      MIT
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let rate = 2.5;
    let paragraphs = [];
    let isReading = false;
    let currentUtter = null;
    let selectedVoice = null;
    let highlightedSpan = null;

    // ====== Кнопка активации ======
    const activateBtn = document.createElement("button");
    activateBtn.textContent = "Загрузка голоса...";
    Object.assign(activateBtn.style, {
        position: "fixed", top: "10px", left: "10px",
        zIndex: "2147483647", padding: "5px 10px",
        backgroundColor: "#007bff", color: "#fff",
        border: "none", borderRadius: "5px",
        cursor: "not-allowed", fontFamily: "Arial, sans-serif"
    });
    document.body.appendChild(activateBtn);

    // ====== Панель управления ======
    const indicator = document.createElement("div");
    Object.assign(indicator.style, {
        position: "fixed", bottom: "10px", right: "10px",
        padding: "5px 10px", backgroundColor: "rgba(0,0,0,0.7)",
        color: "#fff", fontSize: "14px",
        borderRadius: "5px", zIndex: "2147483647",
        fontFamily: "Arial, sans-serif", display: "flex",
        alignItems: "center", gap: "5px", cursor: "move"
    });
    const rateText = document.createElement("span");
    rateText.textContent = `Скорость: ${rate.toFixed(1)}`;
    indicator.appendChild(rateText);
    const btnUp = document.createElement("button"); btnUp.textContent = "▲"; indicator.appendChild(btnUp);
    const btnDown = document.createElement("button"); btnDown.textContent = "▼"; indicator.appendChild(btnDown);
    const btnPause = document.createElement("button"); btnPause.textContent = "⏸"; indicator.appendChild(btnPause);
    const btnStop = document.createElement("button"); btnStop.textContent = "⏹"; indicator.appendChild(btnStop);
    document.body.appendChild(indicator);

    // ====== Перетаскивание ======
    function makeDraggable(el) {
        let drag = false, ox, oy;
        el.addEventListener("mousedown", e => { drag = true; ox = e.clientX - el.offsetLeft; oy = e.clientY - el.offsetTop; });
        document.addEventListener("mousemove", e => { if (drag) { el.style.left = (e.clientX - ox) + "px"; el.style.top = (e.clientY - oy) + "px"; } });
        document.addEventListener("mouseup", () => drag = false);
    }
    makeDraggable(activateBtn);
    makeDraggable(indicator);

    // ====== Голос Irina ======
    function waitForVoice(callback) {
        const check = setInterval(() => {
            const voices = speechSynthesis.getVoices();
            selectedVoice = voices.find(v => /irina/i.test(v.name) && /ru/i.test(v.lang))
                           || voices.find(v => v.lang.toLowerCase().startsWith("ru"));
            if (selectedVoice) {
                clearInterval(check);
                callback();
            }
        }, 200);
    }

    // ====== Фильтры ======
    function isAdBlock(text) {
        const lower = text.toLowerCase();
        return /реклама|подписк|контак|меню|ссылка|поделиться|рекомендуем|©|инн|телефон|cookie|политика/.test(lower);
    }

    function findWidestBlock() {
        const all = Array.from(document.body.querySelectorAll("div, section"));
        let maxW = 0, widest = null;
        all.forEach(el => {
            const rect = el.getBoundingClientRect();
            if (rect.width > maxW && el.querySelectorAll("p").length > 0) {
                maxW = rect.width;
                widest = el;
            }
        });
        return widest;
    }

    // ====== Инициализация параграфов ======
    function initParagraphs() {
        const container = document.querySelector("article")
                        || document.querySelector("main")
                        || document.querySelector(".intro")
                        || document.querySelector(".lead")
                        || findWidestBlock()
                        || document.body;

        let allPs = Array.from(container.querySelectorAll("p"));

        paragraphs = allPs.filter((p, idx) => {
            const text = p.textContent.trim();
            if (idx === 0 && text.length > 0) return true; // первый абзац всегда включаем
            return text.length > 30 && !isAdBlock(text);
        });

        paragraphs.forEach(p => {
            const words = p.textContent.trim().split(/\s+/);
            p.innerHTML = words.map(w => `<span>${w}</span>`).join(" ");
        });
    }

    // ====== Подсветка ======
    function highlightSpan(span) {
        if (highlightedSpan) highlightedSpan.style.backgroundColor = "";
        highlightedSpan = span;
        if (span) {
            span.style.backgroundColor = "orange";
            span.scrollIntoView({behavior:"smooth", block:"center"});
        }
    }

    // ====== Чтение ======
    function readParagraph(index) {
        if (!isReading || index >= paragraphs.length) return;
        const p = paragraphs[index];
        const utter = new SpeechSynthesisUtterance(p.textContent);
        utter.voice = selectedVoice;
        utter.rate = rate;
        utter.lang = "ru-RU";

        const words = Array.from(p.querySelectorAll("span"));
        utter.onboundary = function(event) {
            if (event.name === "word" || event.name === "sentence" || event.charIndex !== undefined) {
                const charIndex = event.charIndex;
                let cumulative = 0;
                for (let i = 0; i < words.length; i++) {
                    const len = words[i].textContent.length + 1;
                    if (cumulative + len >= charIndex) {
                        highlightSpan(words[i]);
                        break;
                    }
                    cumulative += len;
                }
            }
        };

        utter.onend = () => {
            highlightSpan(null);
            readParagraph(index + 1);
        };

        currentUtter = utter;
        speechSynthesis.speak(utter);
    }

    function startReading() {
        if (paragraphs.length === 0) initParagraphs();
        if (paragraphs.length === 0) return;
        isReading = true;
        activateBtn.textContent = "Стоп TTS";
        readParagraph(0);
    }

    function stopReading() {
        isReading = false;
        speechSynthesis.cancel();
        activateBtn.textContent = "Активировать TTS";
        if (highlightedSpan) highlightedSpan.style.backgroundColor = "";
    }

    // ====== Кнопки ======
    waitForVoice(() => {
        activateBtn.textContent = "Активировать TTS";
        activateBtn.style.cursor = "pointer";
        activateBtn.addEventListener("click", () => isReading ? stopReading() : startReading());
    });

    btnUp.addEventListener("click", () => { rate += 0.1; rateText.textContent = `Скорость: ${rate.toFixed(1)}`; });
    btnDown.addEventListener("click", () => { rate = Math.max(0.5, rate - 0.1); rateText.textContent = `Скорость: ${rate.toFixed(1)}`; });
    btnPause.addEventListener("click", () => { if (!currentUtter) return; speechSynthesis.paused ? speechSynthesis.resume() : speechSynthesis.pause(); });
    btnStop.addEventListener("click", stopReading);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址