Переводчик экрана: наложение перевода поверх текста на экране
build / build (push) Failing after 10s

Программа накладывает русский перевод прямо на английские надписи —
как переводчик по фото, только для монитора. Работает с играми,
интерфейсами и документами: распознаёт кадр, собирает текст в связные
блоки, переводит и рисует плашки под цвет фона.

Распознавание — встроенный Windows.Media.Ocr, офлайн. Перевод — цепочка
движков с автопереключением (google, lingva, mymemory), потому что до
разных сервисов из разных сетей достучаться получается по-разному.

Собирается без .NET SDK: Roslyn из Build Tools плюс сборки Framework 4.8,
которые уже есть в системе. На выходе один exe без установщика.

Главная сложность оказалась не в переводе, а в том, чтобы понять, где на
экране связный текст, а где отдельные надписи. Классические алгоритмы
сегментации решают это по межстрочному зазору, но в плотном интерфейсе он
одинаков у строк абзаца и у соседних контролов. Поэтому решение принимает
ансамбль признаков: фон, граница между строками, заполненность строки,
кегль. Надписи на чужом языке отсеиваются тремя уровнями — форма слова,
вторая модель распознавания и частоты буквенных пар.

Чтобы правки эвристик не оценивались на глаз, в tools лежат эталонные
кадры и RenderTest: прогон конвейера без окна с подсчётом попаданий.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 15:54:02 +03:00
commit 7c6b182402
26 changed files with 5161 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
# Переводчик экрана
Наводит перевод поверх английского текста прямо на экране — как Яндекс.Переводчик по фото,
только для монитора. Нажал горячую клавишу → английские надписи закрылись плашками
под цвет фона, на них русский текст. Остальная картинка остаётся на месте.
Работает с любой программой: игры, PDF, видео, интерфейсы, картинки — всё, что видно на экране.
* Распознавание — встроенный в Windows OCR: офлайн, бесплатно, ничего ставить не надо.
* Перевод — несколько движков с автоматическим переключением, если какой-то не отвечает.
* Один файл `ScreenTranslator.exe` (81 КБ), без установщика и без .NET SDK.
## Как пользоваться
| Клавиши | Действие |
|---|---|
| `Ctrl+Alt+Z` | перевести монитор, на котором сейчас курсор |
| `Ctrl+Alt+X` | выделить область рамкой и перевести её |
| `Esc` или повторное нажатие | закрыть перевод |
| Пробел (удерживать) | подсмотреть оригинал под плашками |
| Наведение мышью | показать оригинал одного фрагмента |
| Клик по плашке | скопировать её перевод |
| `Ctrl+C` | скопировать весь перевод |
Значок в трее — те же команды плюс «Проверить переводчики» и правка настроек.
Пока идёт распознавание и перевод, экран уже заморожен, а фрагменты подсвечены — видно,
что программа работает. Полный цикл на мониторе 3440×1440 — около трёх секунд
(0,6 с распознавание + ~2 с перевод), повтор того же текста — мгновенно из кэша.
## Установка
Скопировать папку `dist` куда угодно и запустить `ScreenTranslator.exe`. Всё.
Если Windows покажет «SmartScreen защитил ваш компьютер» — «Подробнее» → «Выполнить в любом случае»
(файл без цифровой подписи).
Автозапуск: `Win+R``shell:startup` → положить туда ярлык на exe.
### Требование одно: модель распознавания
Нужен установленный в Windows языковой компонент «Распознавание текста» для английского.
Проверить: значок в трее → «Проверить переводчики» покажет ошибку, если моделей нет.
Ставится так: **Параметры → Время и язык → Язык и регион** → у английского языка `···`
**Компоненты языка** → отметить **Распознавание текста**. На большинстве систем уже стоит.
## Настройка
Рядом с exe лежит `config.json` (создаётся при первом запуске). После правки —
«Перечитать настройки» в трее, перезапуск не нужен.
Самое полезное:
```jsonc
"hotkeyTranslate": "Ctrl+Alt+Z", // своя комбинация, если эта занята
"ocrLanguage": "en-US", // язык оригинала на экране
"targetLang": "ru", // на какой переводить
"overlayMode": "frozen", // frozen — стоп-кадр; live — плашки поверх живого экрана
"captureScope": "monitor", // monitor — экран под курсором; all — все мониторы сразу
"ocrScale": 2.0, // 1.0 быстрее, 2.0 точнее на мелком шрифте
"detectForeign": true, // не трогать надписи на языке системы (см. ниже)
"markTranslated": true, // подчёркивать переведённые места
"chain": ["google", "lingva", "mymemory"]
```
### Смешанный экран
Английская модель OCR, встретив русскую надпись, читает её как бессмыслицу вроде `C03AaTb`
или `Pa3Mep` — и без защиты программа переводила бы эту кашу, залепляя плашками весь
русский интерфейс.
Защита двухступенчатая:
1. `skipGarbage` — отсев по форме слов: цифра посреди слова, заглавная после строчной,
символы-артефакты, имена файлов.
2. `detectForeign` — кадр дополнительно распознаётся моделью второго языка. Если она видит
на этом месте кириллицу, надпись изначально русская и её не трогают. Это ещё примерно
столько же времени на распознавание; выключается, если весь экран и так на одном языке.
3. Список ходовых английских слов ([EnglishWords.cs](src/EnglishWords.cs)) — арбитр в спорных
случаях. Русская модель читает латинское «Cancel» как «Сапсеl», то есть по алфавиту оно
неотличимо от русского «Профиль» → `npocbwlb`. Если основная модель выдала узнаваемое
английское слово, верим ей.
На эталонах это даёт 22 распознанных блока из 22 на английском кадре (ни одной потери)
и 2 оставшихся из 20 на русском.
Совсем идеально не будет: очень мелкие надписи (вкладки браузера, 8 px) обе модели читают
плохо, и часть их может проскочить. **Если на экране мешанина языков — надёжнее `Ctrl+Alt+X`
и рамка вокруг нужного куска.**
### Чего программа не умеет и не сможет
Рабочий стол, панель задач, списки установленных программ — не её сценарий. Подписи там это
названия приложений, а отличить название от обычного слова невозможно: «Steam», «Discord»,
«Wand» одновременно и то, и другое. Перевод получится буквальным и бессмысленным.
Помогает `"minWords": 2` — тогда переводятся только фразы от двух слов, а одиночные подписи
остаются как есть. Но за это приходится платить: на интерфейсе игры теряется половина надписей
(«Mining», «Search», «Consumable» — тоже одиночные слова), поэтому по умолчанию стоит `1`.
### Про переводчики и Россию
`chain` — это порядок попыток. Первый ответивший движок используется, упавший временно
исключается и пробуется следующий. Ничего перенастраивать при переезде в другую сеть не нужно —
если Google недоступен, программа сама уйдёт на запасной.
Что стоит знать:
* **google** — бесплатный публичный эндпоинт, без ключа, лучшее соотношение качества и скорости.
`translate.googleapis.com` в России не заблокирован (замедляли YouTube, а не API), но
у отдельных провайдеров ТСПУ рвёт к нему TLS. Отсюда и нужна цепочка.
* **lingva** — открытый шлюз к тому же Google на других адресах. Ровно тот случай, когда
напрямую не пускает, а через шлюз проходит. Медленнее (2–4 с).
* **mymemory** — последний рубеж, без ключа, лимит около 5000 слов в сутки на IP.
* **yandex** — самый устойчивый канал внутри РФ, но нужен платный Yandex Cloud
(`folderId` + API-ключ).
* **deepl** — лучшее качество en→ru, есть бесплатный тариф с ключом.
* **llm** — перевод целым экраном через Claude API: лучше держит контекст интерфейса,
но платный по токенам и медленнее.
* **libretranslate** / **relay** — свой сервер, ничего не уходит наружу.
Проверить, что доступно из конкретной сети: трей → **Проверить переводчики** — покажет
время ответа или ошибку по каждому.
Через прокси (например, свой sing-box в режиме mixed):
```jsonc
"proxy": "http://192.168.5.42:2080"
```
### Свой релей
Если публичные сервисы недоступны, а свой сервер есть — поднимите эндпоинт, принимающий
`POST {"texts": [...], "from": "en", "to": "ru"}` и возвращающий `{"texts": [...]}`
той же длины, затем:
```jsonc
"chain": ["relay", "google"],
"engines": { "relay": { "url": "https://ваш-домен/translate" } }
```
Внутри релей может ходить хоть в Google, хоть в локальную модель — клиенту всё равно.
## Приватность
Распознавание идёт локально, наружу уходит **только распознанный текст** — картинка экрана
никогда не покидает компьютер. Но текст с экрана всё же уходит в выбранный сервис перевода,
поэтому для чувствительного содержимого используйте `libretranslate` или `relay` на своём сервере.
Кэш переводов лежит рядом в `cache.json` — его можно удалять в любой момент.
## Сборка из исходников
.NET SDK не нужен — используется Roslyn из Visual Studio Build Tools и сборки .NET Framework 4.8,
которые уже есть в Windows.
```powershell
.\build.ps1 # собрать в .\dist
.\build.ps1 -Run # собрать и запустить
.\build.ps1 -Test # плюс RenderTest.exe
```
`RenderTest.exe` — не сама программа, а диагностика: прогоняет весь конвейер без окна и
сохраняет две картинки (как OCR разметил блоки и как лёг перевод). Запускать лучше из терминала;
при двойном клике он всё равно отработает и подождёт Enter, а картинки положит
в `%TEMP%\screen-translator-test`.
```powershell
.\dist\RenderTest.exe --in кадр.png --out .\dist\test
.\dist\RenderTest.exe --scale 1.0 --no-translate
```
Два эталонных кадра для прогонов рисуются скриптами:
* `tools\make-sample.ps1` — англоязычный интерфейс. Проверка «всё ли переводится»:
правильный результат — 22 блока.
* `tools\make-sample-ru.ps1` — русский интерфейс. Негативная проверка: английская модель OCR
читает кириллицу как кашу, и её нельзя ни переводить, ни закрывать плашками.
Правильный результат — около 5 блоков (мелкие обрывки вроде «Tn»), было 20 до фильтра.
Оба стоит прогонять после правок в `Blocks.cs`: ужесточение фильтра легко начинает резать
короткие английские кнопки, и английский кадр это сразу показывает.
Флаг `--expect файл.txt` считает, сколько заранее известных строк реально распозналось —
без такой метрики улучшение и ухудшение на глаз выглядят одинаково убедительно.
`--selftest` проверяет биграммную модель на примерах настоящих надписей и каши.
```powershell
.\dist\RenderTest.exe --in wow.jpg --expect tools\expect-wow.txt --no-translate
.\dist\RenderTest.exe --selftest
```
Текущие показатели (после каждой правки должны держаться):
| Кадр | Метрика | Значение |
|---|---|---|
| Скриншот игры | распознано известных надписей | 14 из 14 |
| Английский эталон | ложно отброшено | 0 |
| Русский эталон | осталось непойманной каши | 2 из 20 |
## Что внутри
| Файл | Зачем |
|---|---|
| `src/Program.cs` | трей, горячие клавиши, порядок работы |
| `src/ScreenCapture.cs` | снимок экрана и отдельная копия пикселей для фоновой обработки |
| `src/Ocr.cs` | Windows.Media.Ocr, увеличение кадра ради мелкого шрифта |
| `src/Blocks.cs` | склейка строк в абзацы, отсев каши, подбор цветов плашки |
| `src/Translators.cs` | движки перевода, цепочка фоллбеков, кэш |
| `src/PlateRenderer.cs` | подбор кегля и отрисовка плашки |
| `src/OverlayForm.cs` | окно поверх экрана |
| `src/RegionSelector.cs` | выделение области рамкой |
| `src/EnglishBigrams.cs` | частоты буквенных пар: отличает слово от каши распознавания |
| `src/EnglishWords.cs` | список ходовых английских слов — арбитр в спорных случаях |
Откуда взялись решения (короткий разбор чужого опыта):
* Классические алгоритмы сегментации — RLSA, Docstrum, XY-cut, Voronoi — принимают решение
«склеивать или нет» по одному сигналу, межстрочному зазору. В плотном интерфейсе он
одинаков у строк абзаца и у соседних контролов, поэтому ни один из них эту задачу не решает.
Работает только ансамбль независимых признаков — как в Tesseract (`paragraphs.h`, геометрия
плюс пунктуация) и UIED, где графику детектируют отдельно от текста.
* Отсюда четыре признака разрыва: разный фон, граница между строками, «строка не дотянула
до правого края» (в абзаце короткой бывает только последняя) и несовпадение кегля.
* Определение чужого языка — это не задача language identification: готовые детекторы
(cld3, fastText) всегда выбирают какой-то язык из списка и уверенно зовут кашу английским.
Нужна одноклассовая модель «похоже на настоящий английский», отсюда биграммы.
* Проверено и отвергнуто: перевод в оттенки серого и инверсия тёмного фона перед OCR.
Обещали заметный прирост, на замере не дали ничего (14 из 20 во всех вариантах),
поэтому по умолчанию выключены, хотя в конфиге остались.
Тонкие места, если будете править:
* Приложение помечено `PerMonitorV2` — все координаты физические, иначе разметка OCR
разъезжается с экраном при масштабе интерфейса больше 100 %.
* OCR обводит только тело букв, без выносных элементов, поэтому плашка расширяется
по вертикали — иначе сверху и снизу торчат хвосты оригинала.
* Строки склеиваются в абзац по межстрочному интервалу и одинаковому кеглю: рваные строки
переводятся заметно хуже целого абзаца.