Переводчик экрана: наложение перевода поверх текста на экране
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
+126
View File
@@ -0,0 +1,126 @@
<#
Сборка «Переводчика экрана».
.NET SDK не нужен: используется Roslyn из VS Build Tools и сборки .NET Framework 4.8,
которые уже есть в Windows. На выходе — один самодостаточный exe.
Использование: .\build.ps1 собрать в .\dist
.\build.ps1 -Run собрать и запустить
#>
[CmdletBinding()]
param(
[switch]$Run,
[switch]$Test, # дополнительно собрать RenderTest.exe — прогон конвейера без окна, результат в PNG
[string]$OutDir = "$PSScriptRoot\dist"
)
$ErrorActionPreference = 'Stop'
function Find-Csc {
$candidates = @(
"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\Roslyn\csc.exe",
"C:\Program Files\Microsoft Visual Studio\2022\*\MSBuild\Current\Bin\Roslyn\csc.exe",
"C:\Program Files (x86)\Microsoft Visual Studio\2022\*\MSBuild\Current\Bin\Roslyn\csc.exe",
"C:\Program Files (x86)\MSBuild\14.0\Bin\csc.exe"
)
foreach ($c in $candidates) {
$hit = Get-Item $c -ErrorAction SilentlyContinue | Select-Object -First 1
if ($hit) { return $hit.FullName }
}
# запасной вариант — компилятор из самой Windows (старый C#, но соберёт)
$fw = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe"
if (Test-Path $fw) { return $fw }
throw "Не найден компилятор C#. Поставьте Visual Studio Build Tools 2022."
}
function Find-WinMd {
$root = "C:\Program Files (x86)\Windows Kits\10\UnionMetadata"
if (-not (Test-Path $root)) { throw "Не найден Windows SDK (UnionMetadata) — без него нет доступа к Windows OCR." }
# рядом лежит папка Facade с заглушкой — берём только версионные каталоги (10.0.x)
$ver = Get-ChildItem $root -Directory |
Where-Object { $_.Name -match '^\d+\.' -and (Test-Path (Join-Path $_.FullName 'Windows.winmd')) } |
Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1
if (-not $ver) { throw "В Windows SDK нет версионного Windows.winmd." }
return (Join-Path $ver.FullName 'Windows.winmd')
}
$csc = Find-Csc
$winmd = Find-WinMd
$fw = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319"
$gac = "C:\Windows\Microsoft.NET\assembly\GAC_MSIL"
function Gac($name) {
$hit = Get-ChildItem (Join-Path $gac $name) -Recurse -Filter "$name.dll" -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $hit) { throw "Нет сборки $name в GAC." }
return $hit.FullName
}
$refs = @(
"$fw\mscorlib.dll"
"$fw\System.dll"
"$fw\System.Core.dll"
"$fw\System.Drawing.dll"
"$fw\System.Windows.Forms.dll"
"$fw\System.Runtime.dll"
"$fw\System.Web.dll"
(Gac 'System.Web.Extensions')
(Gac 'System.Runtime.WindowsRuntime')
$winmd
)
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$exe = Join-Path $OutDir 'ScreenTranslator.exe'
# запущенная копия держит файл, и линковка падает с невнятной ошибкой
$running = Get-Process ScreenTranslator -ErrorAction SilentlyContinue
if ($running) {
Write-Host "Останавливаю запущенный ScreenTranslator (PID $($running.Id))…"
$running | Stop-Process -Force
Start-Sleep -Milliseconds 400
}
$sources = Get-ChildItem "$PSScriptRoot\src" -Filter *.cs | ForEach-Object { $_.FullName }
Write-Host "csc : $csc"
Write-Host "winmd : $winmd"
Write-Host "исходники: $($sources.Count) файлов"
Write-Host "выход : $exe"
Write-Host ''
$cscArgs = @(
'/nologo'
'/target:winexe'
'/platform:x64'
'/optimize+'
'/langversion:latest'
"/out:$exe"
"/win32manifest:$PSScriptRoot\app.manifest"
) + ($refs | ForEach-Object { "/r:$_" }) + $sources
& $csc @cscArgs
if ($LASTEXITCODE -ne 0) { throw "Сборка не удалась (код $LASTEXITCODE)." }
Write-Host ("`nГотово: {0} ({1:N0} КБ)" -f $exe, ((Get-Item $exe).Length / 1KB))
if ($Test) {
$testExe = Join-Path $OutDir 'RenderTest.exe'
# тот же код, но с консольной точкой входа вместо трея
$testSources = ($sources | Where-Object { $_ -notlike '*\Program.cs' }) + "$PSScriptRoot\tools\RenderTest.cs"
$testArgs = @(
'/nologo', '/target:exe', '/platform:x64', '/optimize+', '/langversion:latest'
"/out:$testExe"
"/win32manifest:$PSScriptRoot\app.manifest" # без него захват придёт в логических пикселях
) + ($refs | ForEach-Object { "/r:$_" }) + $testSources
& $csc @testArgs
if ($LASTEXITCODE -ne 0) { throw "Сборка теста не удалась (код $LASTEXITCODE)." }
Write-Host ("Готово: {0} ({1:N0} КБ)" -f $testExe, ((Get-Item $testExe).Length / 1KB))
}
if ($Run) {
Get-Process ScreenTranslator -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Milliseconds 300
Start-Process $exe
Write-Host "Запущен. Значок в трее."
}