Files
screen-translator/tools/make-sample-text.ps1
gusadmin 7c6b182402
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>
2026-07-27 15:54:02 +03:00

87 lines
3.9 KiB
PowerShell

<#
Эталонный кадр со связной прозой: абзацы разной длины, заголовки, список, подпись.
Проверяет главное требование к переводу — что абзац остаётся ЦЕЛЫМ блоком и переводится
связно, а не рассыпается на отдельные строки. Признаки разрыва блоков (граница, фон,
fill-ratio) настраивались на плотных интерфейсах, и этот кадр страхует от перекоса
в другую сторону.
#>
param([string]$Out = "$PSScriptRoot\..\dist\sample-text.png")
Add-Type -AssemblyName System.Drawing
$W, $H = 1400, 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('Georgia', $size, [Drawing.FontStyle]::$style, [Drawing.GraphicsUnit]::Pixel) }
$g.FillRectangle((Brush 252 251 248), 0, 0, $W, $H)
$dark = Brush 26 26 28
$body = Brush 52 54 58
$grey = Brush 130 134 140
$y = 60
$g.DrawString('The Lighthouse at the End of the Road', (Fnt 34 'Bold'), $dark, 90, $y)
$y += 56
$g.DrawString('A short account of the winter crossing, 1897', (Fnt 17 'Italic'), $grey, 90, $y)
$y += 54
# абзац 1 — длинный, шесть строк
$p1 = @(
'The keeper had lived alone on the rock for eleven years, and in that time he had',
'learned to read the weather the way other men read a newspaper. He knew that a',
'certain thinness in the light before noon meant the wind would turn by evening,',
'and that gulls settling early on the western ledge were worth more than any',
'barometer. When the supply boat failed to arrive in the first week of December,',
'he did not worry. When it failed again in the second week, he began to count.'
)
foreach ($line in $p1) { $g.DrawString($line, (Fnt 19), $body, 90, $y); $y += 30 }
$y += 26
# абзац 2 — средний, три строки
$p2 = @(
'There were four barrels of oil remaining, and rather less flour than he would have',
'liked. He wrote the numbers in the margin of the log, where the inspector would not',
'think to look for them, and then he climbed the stairs to light the lamp.'
)
foreach ($line in $p2) { $g.DrawString($line, (Fnt 19), $body, 90, $y); $y += 30 }
$y += 26
# подзаголовок
$g.DrawString('What the log recorded', (Fnt 22 'Bold'), $dark, 90, $y)
$y += 40
# список — короткие пункты, НЕ должны склеиться в один абзац
$items = @(
'Fog signal sounded for nine hours without interruption.',
'The eastern glazing cracked but did not give way.',
'No vessel sighted between the fourth and the nineteenth.'
)
foreach ($line in $items) {
$g.FillEllipse((Brush 150 150 155), 92, ($y + 9), 5, 5)
$g.DrawString($line, (Fnt 18), $body, 112, $y)
$y += 32
}
$y += 24
# абзац 3 — короткий, две строки
$p3 = @(
'By the time the boat came, on the second morning of the new year, the lamp had',
'not failed once. This, he felt, was the only fact that deserved recording.'
)
foreach ($line in $p3) { $g.DrawString($line, (Fnt 19), $body, 90, $y); $y += 30 }
$y += 30
$g.DrawString('Transcribed from the original manuscript', (Fnt 15 'Italic'), $grey, 90, $y)
$g.Dispose()
New-Item -ItemType Directory -Force -Path (Split-Path $Out -Parent) | Out-Null
$bmp.Save($Out, [Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
Write-Host "Эталон со связным текстом: $Out"
Write-Host "Ожидание: 3 абзаца целыми блоками, заголовки и пункты списка отдельно."