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>
87 lines
3.9 KiB
PowerShell
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 абзаца целыми блоками, заголовки и пункты списка отдельно."
|