// ==UserScript==
// @name АвтоTTS Irina с подсветкой слов v2.0
// @namespace https://example.com/
// @version 2.0
// @description Чтение статей, книг и больших текстов с подсветкой слов, плавной прокруткой и естественной речью Irina
// @author You
// @license MIT
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
let rate = 2.5;
let paragraphs = [];
let currentIndex = 0;
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();
}
}, 100);
}
// ====== Фильтры ======
function isAdBlock(text) {
const lower = text.toLowerCase();
return /реклама|подписк|контак|меню|ссылка|поделиться|рекомендуем|©|инн|телефон|cookie|политика/.test(lower) || text.length>500;
}
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") || findWidestBlock() || document.body;
paragraphs = Array.from(container.querySelectorAll("p"))
.filter(p => p.textContent.trim().length>30 && !isAdBlock(p.textContent));
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";
let words = Array.from(p.querySelectorAll("span"));
utter.onboundary = function(event) {
if (event.name==="word") {
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;
currentIndex = 0;
activateBtn.textContent = "Стоп TTS";
readParagraph(currentIndex);
}
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);
})();