// ==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);
})();