Переводчик экрана: наложение перевода поверх текста на экране
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
+101
View File
@@ -0,0 +1,101 @@
<#
Рисует эталонный кадр с англоязычным интерфейсом: мелкое меню, заголовки, абзац,
кнопки, светлая и тёмная зоны. Нужен, чтобы прогонять конвейер на предсказуемом входе.
#>
param([string]$Out = "$PSScriptRoot\..\dist\sample.png")
Add-Type -AssemblyName System.Drawing
$W, $H = 1600, 900
$bmp = New-Object Drawing.Bitmap($W, $H, [Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = 'AntiAlias'
$g.TextRenderingHint = 'ClearTypeGridFit'
function Brush($r, $gg, $b) { New-Object Drawing.SolidBrush ([Drawing.Color]::FromArgb($r, $gg, $b)) }
function Fnt($size, $style = 'Regular') { New-Object Drawing.Font('Segoe UI', $size, [Drawing.FontStyle]::$style, [Drawing.GraphicsUnit]::Pixel) }
function Text($s, $font, $brush, $x, $y) { $g.DrawString($s, $font, $brush, [single]$x, [single]$y) }
# фон приложения
$g.FillRectangle((Brush 250 250 252), 0, 0, $W, $H)
# сайдбар
$g.FillRectangle((Brush 32 34 42), 0, 0, 240, $H)
# верхняя панель
$g.FillRectangle((Brush 255 255 255), 240, 0, $W - 240, 56)
$g.DrawLine((New-Object Drawing.Pen(([Drawing.Color]::FromArgb(226, 228, 233)))), 240, 56, $W, 56)
$white = Brush 236 238 243
$grey = Brush 150 155 168
$dark = Brush 28 30 36
$body = Brush 70 74 84
$blue = Brush 46 108 220
# --- сайдбар: мелкий текст на тёмном ---
Text 'Workspace' (Fnt 12) $grey 20 18
Text 'Dashboard' (Fnt 14) $white 20 46
Text 'Recent files' (Fnt 14) $white 20 76
Text 'Shared with me' (Fnt 14) $white 20 106
Text 'Notifications' (Fnt 14) $white 20 136
Text 'Trash' (Fnt 14) $white 20 166
Text 'Storage almost full' (Fnt 12) $grey 20 210
Text 'Upgrade your plan to continue' (Fnt 12) $grey 20 230
# --- верхняя панель ---
Text 'Project settings' (Fnt 15 'Bold') $dark 264 18
Text 'Save changes' (Fnt 13) $blue 1360 20
Text 'Cancel' (Fnt 13) $body 1480 20
# --- заголовок и абзац ---
Text 'Network configuration' (Fnt 30 'Bold') $dark 264 100
Text 'Choose how this device connects to the internet.' (Fnt 16) $body 264 148
$para = @(
'When automatic configuration is enabled, the application selects the fastest available',
'connection and switches between them without interrupting downloads. Manual mode gives',
'you full control over the proxy address, port and authentication method, but requires',
'the settings to be updated whenever the server changes.'
)
$y = 200
foreach ($line in $para) { Text $line (Fnt 15) $body 264 $y; $y += 26 }
# --- карточка со списком ---
$g.FillRectangle((Brush 255 255 255), 264, 330, 700, 250)
$g.DrawRectangle((New-Object Drawing.Pen(([Drawing.Color]::FromArgb(224, 226, 232)))), 264, 330, 700, 250)
Text 'Connection status' (Fnt 17 'Bold') $dark 288 350
Text 'Interface' (Fnt 13) $grey 288 392
Text 'Wireless adapter' (Fnt 14) $dark 288 412
Text 'IP address' (Fnt 13) $grey 288 448
Text 'Assigned automatically' (Fnt 14) $dark 288 468
Text 'Last error' (Fnt 13) $grey 288 504
Text 'The remote server did not respond in time' (Fnt 14) $dark 288 524
# --- кнопки ---
$g.FillRectangle((Brush 46 108 220), 264, 620, 170, 40)
Text 'Apply and restart' (Fnt 14) (Brush 255 255 255) 282 631
$g.FillRectangle((Brush 238 240 244), 450, 620, 130, 40)
Text 'Restore defaults' (Fnt 14) $body 464 631
# --- тёмная плашка с предупреждением ---
$g.FillRectangle((Brush 58 30 30), 264, 700, 700, 90)
Text 'Warning' (Fnt 15 'Bold') (Brush 255 180 170) 288 716
Text 'Changing these values may disconnect active sessions.' (Fnt 14) (Brush 240 200 195) 288 742
Text 'Make sure no downloads are running before you continue.' (Fnt 14) (Brush 240 200 195) 288 764
# --- правая колонка, мелкий текст ---
Text 'Activity' (Fnt 15 'Bold') $dark 1020 100
$log = @(
'Connection established',
'Certificate verified',
'Profile synchronised',
'Background sync paused',
'Waiting for network'
)
$y = 140
foreach ($line in $log) { Text $line (Fnt 12) $body 1020 $y; $y += 22 }
$g.Dispose()
$dir = Split-Path $Out -Parent
New-Item -ItemType Directory -Force -Path $dir | Out-Null
$bmp.Save($Out, [Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
Write-Host "Эталонный кадр: $Out"