- BrainTools - https://www.braintools.ru -
Я учу английский по сериалам. Смотрю с английскими субтитрами, и каждое незнакомое слово — это пауза, alt‑tab, переводчик, обратно. К десятой паузе за серию хочется просто включить русские сабы и сдаться.
Есть Language Reactor и похожие сервисы, но у них жёсткий список площадок: Netflix, YouTube — и всё. Каждый стриминг рисует субтитры своим способом, и под каждый нужен отдельный парсер. А я смотрю где придётся.
Так что я сделал своё. Получилось расширение, которое работает почти на любом сайте с HTML5-плеером: наводишь на слово в субтитрах — тултип с переводом, кликаешь — слово уходит в личный словарь и подчёркивается во всех следующих сериях, пока не выучишь. Перевод крутится прямо в браузере, без серверов.

Главное открытие проекта: в Chrome 138+ есть встроенный Translator API — модель работает on‑device, бесплатно и офлайн. Проверка доступности выглядит так:
const availability = await Translator.availability({
sourceLanguage: 'en', targetLanguage: 'ru'
});
// 'available' | 'downloadable' | 'unavailable'
const translator = await Translator.create({ sourceLanguage: 'en', targetLanguage: 'ru' });
await translator.translate('stubborn'); // «упрямый»
Нюансы, которые не написаны в документации крупным шрифтом:
Первое скачивание языкового пакета Chrome разрешает только по пользовательскому жесту — по наведению мыши модель не скачать, нужен клик.
Translator.availability() умеет просто не резолвиться никогда, если сервис моделей не поднялся. При этом 'Translator' in window === true. Без таймаутов вокруг каждого вызова интерфейс залипает на «…» навсегда.
Первая версия переводила текст реплики как есть, и перевод был мусорным. Причина не в модели: субтитры режут предложение на куски, и переводчику достаётся огрызок.
|
Что уходит в переводчик |
Что получается |
|---|---|
|
|
«… оказывается, я» |
|
|
«Оказывается, я не тот, кем я думал.» |
Лечится склейкой: у дорожек <track> доступен весь список реплик с таймингами, так что предложение собирается по соседним репликам до ближайшей точки — и назад, и вперёд.
YouTube не использует <track> — субтитры это div’ы, которые он рисует сам. Причём контейнер держит два окна сразу (уходящее и текущее), а роллинг‑субтитры повторяют [1] в каждой следующей реплике хвост предыдущей. Наивное чтение контейнера даёт задвоенный текст, а наивная склейка — «я провёл месяцы я провёл месяцы я провёл месяцы» в переводе.
Решение: читать только последнее окно, повторы склеивать по перекрытию слов, как это делают в диффах.
Сканер видео на странице я дебаунсил через requestAnimationFrame. В фоновой вкладке rAF не вызывается вообще — флаг «скан запланирован» залипал в true, и сканер умирал навсегда, даже после возврата на вкладку. setTimeout и никаких украшательств.
В нативном fullscreen браузер рисует только document.fullscreenElement и его потомков. Если плеер уходит в fullscreen контейнером — оверлей просто перевешивается внутрь. Но часть сайтов открывает fullscreen на самом <video>, а у видеоэлемента не бывает детей — оверлей туда физически не подвесить. Выход: поймать момент, выйти из fullscreen и тут же запросить его для родителя, пока пользовательский жест ещё считается активным.
Самая поучительная история проекта, и мне про неё сообщили как про «на твиче лагает и пропадает меню громкости».
Для плееров без <track> и без известного селектора работает эвристика: полминуты смотрим на элементы внутри кадра и берём тот, чей текст сменился хотя бы трижды. Логика [2] простая — субтитры меняются, остальное стоит.
На твиче меняется всё. Качество, громкость, счётчик зрителей, время эфира. Эвристика находила счётчик зрителей, объявляла его субтитрами и ставила ему visibility: hidden — а потом перерисовывала «реплику» на каждое обновление счётчика. Отсюда и «пропадает меню», и «лагает»: причина была одна, и причиной был я, а не твич.
Признаков в итоге четыре, и элемент обязан пройти все:
не орган управления: сам не кнопка, кнопок внутри не содержит и внутри кнопки не лежит;
текст похож на предложение: минимум три слова из букв, либо два при длине от 14 символов. 12,4 тыс., 1080p60, Auto, 100%, 1:02:13 не проходят. Для письма без пробелов — японского, китайского, тайского — считаются не слова, а буквы: от пяти;
стоит по центру кадра: середина элемента не дальше 20% ширины кадра от его центра. Качество, громкость и счётчик живут по углам;
целиком внутри кадра и не является предком видео.
Первой версии этих защит не хватило, и узнал я это не глазами: собрал стенд, устроенный как твич — четыре тысячи узлов внутри плеера, живая лента чата, подписи, которые меняются каждые 400 мс. Стенд сразу показал, что счётчик зрителей всё ещё забирается: он не кнопка и внутри кнопки не лежит, поэтому первую проверку проходил. Признаки «предложение» и «по центру» появились из‑за него.
Мораль скучная, но настоящая: если эвристика решает, что на чужой странице считать своим, у неё должен быть стенд, а не «вроде работает».
Универсальный зажим в CSS выглядел безобидно:
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}
Начальное значение animation-delay он не трогает, а анимация с задержкой и fill: both держит стартовый кадр всю задержку целиком. То есть под «уменьшить движение» страница начинала двигаться больше обычного: элементы возникали по очереди из ниоткуда, без всякого движения между появлениями. Лечится одной строкой animation-delay: 0ms !important рядом, но найти это можно только специально включив флаг и посмотрев.
«This is some dirty fucking pool, Madelyn» встроенный переводчик переводит про бассейн. А «dirty pool» — это «нечестная игра». Маленькая on‑device модель идиом не знает, и промптами Translator API это не лечится — он переводчик, а не LLM.
Зато рядом лежит Prompt API — тот же on‑device Gemini Nano, но в режиме языковой модели. Идиомы он понимает. Ловушка: Chrome даёт этот API только расширениям, из userscript’а он недоступен — из‑за этого проект и переехал из Tampermonkey в MV3.
Дальше пришлось решать, где им переводить, а где нет. Модель отвечает секундами, а не мгновенно, и перевод реплики в тултипе срабатывает на каждое наведение мыши — туда её пускать нельзя. А вот сохранение фразы (Alt+A или протяжка мышью) — осознанное действие, там секунда ожидания уместна, и результат кэшируется навсегда. Так и сделано: два пути перевода фраз, быстрый и глубокий, с общим кэшем и разными ячейками в нём — иначе первое же наведение на слово закрыло бы дорогу хорошему переводу дословным.
Модель ещё и приходится проверять на выходе: она любит обрамить ответ кавычками, предложить второй вариант или вместо перевода написать абзац рассуждений. Всё, что втрое длиннее оригинала, отбраковывается — лучше дословный перевод, чем эссе.
DeepL остался как опция для тех, у кого модель не скачана: свой бесплатный ключ, 500 тыс. символов в месяц. И тут был отдельный урок — см. ниже.

Друг вписал ключ DeepL, получил ошибку [3] 451 — DeepL не обслуживает его страну — и перевод фраз у него сломался полностью. Потому что код был такой:
const key = await getDeeplKey();
if (key) return deeplTranslate(text, src, tgt, key); // и всё
Сохранённый нерабочий ключ оказался хуже, чем никакого. Причём падений тут четыре разных класса: 451 (страна), 456 (кончился месячный лимит), 401/403 (ключ отозван), обрыв сети. Ни один из них не должен уносить с собой основную функцию.
Сейчас отказ DeepL просто уводит фразу на встроенную модель, плюс пауза по этому ключу — час на отказы, которые сами не пройдут, две минуты на сетевые, — чтобы каждая следующая фраза не платила пятнадцатисекундным таймаутом. Пауза привязана к самому ключу: вставили другой — она сразу перестаёт действовать.
Общее правило, которое я вынес: опциональная интеграция обязана иметь запасной путь на каждом своём отказе. Иначе это не опция, а скрытая зависимость.
Наведение — перевод слова и всей реплики; клик — слово в словарь вместе с фразой‑примером; протяжка по словам — целая фраза.
Сохранённые слова подчёркиваются в субтитрах во всех следующих видео.
Alt+E — объяснение реплики on‑device моделью: идиомы, сленг, фразовые глаголы.
Тренажёр с интервальным повторением, статистика, выгрузка в CSV, Excel, Anki и PDF.
Учить можно в любую сторону: не только английский по русскому переводу, но и русский по английскому. 39 языков — всё, что Chrome переводит on‑device.
Язык субтитров и отступ строки запоминаются отдельно для каждого сайта: на одном субтитры вшиты в разметку и язык приходится ставить руками, на другом он есть в дорожке.
Выключатель на конкретном сайте: скрипт там засыпает целиком — ни поиска видео, ни наблюдателей, ни таймеров.
Всё on‑device: без аккаунтов, без телеметрии, субтитры никуда не уходят (кроме опционального DeepL по твоему собственному ключу).
Селекторы под Netflix, Twitch, video.js, JW Player, Plyr и Shaka написаны по их разметке и покрыты синтетическими стендами. Живьём я их не проверял — нет подписки и нет эфира. Если у вас работает или не работает — напишите, это ровно та обратная связь, которой мне не хватает.
Требуется Chrome 138+ на компьютере. На мобильных встроенных моделей нет.
Языковую пару без английского Chrome тянет через английский: два языковых пакета и потеря качества.
Расширение бесплатное: https://chromewebstore.google.com/detail/subtitle‑translator‑learn/floomboamhlmkalghefpabefhciknllf [4]
Если найдёте сайт, где субтитры есть, а расширение их не видит — напишите, адаптеры под новые плееры добавляются быстро.
Автор: noffybtw
Источник [5]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/36103
URLs in this post:
[1] повторяют: http://www.braintools.ru/article/4012
[2] Логика: http://www.braintools.ru/article/7640
[3] ошибку: http://www.braintools.ru/article/4192
[4] https://chromewebstore.google.com/detail/subtitle‑translator‑learn/floomboamhlmkalghefpabefhciknllf: https://chromewebstore.google.com/detail/subtitle-translator-learn/floomboamhlmkalghefpabefhciknllf
[5] Источник: https://habr.com/ru/articles/1087092/?utm_campaign=1087092&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.