Переводчик экрана: наложение перевода поверх текста на экране
build / build (push) Failing after 10s
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:
@@ -0,0 +1,86 @@
|
||||
<#
|
||||
Эталонный кадр со связной прозой: абзацы разной длины, заголовки, список, подпись.
|
||||
|
||||
Проверяет главное требование к переводу — что абзац остаётся ЦЕЛЫМ блоком и переводится
|
||||
связно, а не рассыпается на отдельные строки. Признаки разрыва блоков (граница, фон,
|
||||
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 абзаца целыми блоками, заголовки и пункты списка отдельно."
|
||||
Reference in New Issue
Block a user