Переводчик экрана: наложение перевода поверх текста на экране
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,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace ScreenTranslator
|
||||
{
|
||||
/// <summary>Прогон всего конвейера без окна: захват -> OCR -> блоки -> перевод -> отрисовка в PNG.
|
||||
/// Нужен, чтобы проверять качество распознавания и вёрстки плашек, не гоняя оверлей поверх
|
||||
/// рабочего стола и не кликая по нему.
|
||||
///
|
||||
/// RenderTest.exe [--in кадр.png] [--out каталог] [--scale 2.0] [--no-translate]
|
||||
/// </summary>
|
||||
static class RenderTest
|
||||
{
|
||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||
static extern int GetConsoleProcessList(int[] buffer, int count);
|
||||
|
||||
/// <summary>Запущено двойным кликом (своя консоль), а не из терминала — тогда после
|
||||
/// работы надо дождаться пользователя, иначе окно захлопнется вместе с выводом.</summary>
|
||||
static bool OwnsConsole()
|
||||
{
|
||||
try { return GetConsoleProcessList(new int[4], 4) <= 1; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
static int Main(string[] args)
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
bool pause = OwnsConsole();
|
||||
try { return Run(args); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("\nОшибка: " + ex.Message);
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pause)
|
||||
{
|
||||
Console.WriteLine("\nНажмите Enter, чтобы закрыть окно…");
|
||||
Console.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Проверка биграммной модели на реальных примерах из прошлых прогонов:
|
||||
/// слева то, что должно проходить, справа — то, что модель обязана отбраковывать.
|
||||
/// Порог берётся из этих чисел, а не из общих соображений.</summary>
|
||||
static int SelfTest()
|
||||
{
|
||||
string[] good = {
|
||||
"Profile", "Cancel", "Settings", "Search", "Mining", "Tailoring", "Engineering",
|
||||
"Leatherworking", "Consumable", "Trade Goods", "Not equippable", "All Subclasses",
|
||||
"Have Materials", "Network configuration", "Wireless adapter", "Save changes",
|
||||
"Connection established", "Restore defaults", "First Aid", "Storage almost full"
|
||||
};
|
||||
string[] bad = {
|
||||
"npocbwlb", "C03AaTb", "Pa3Mep", "npocM0TpeTb", "COPTMPOBaTb", "Bxoaguvqe",
|
||||
"KOMnb}0Tep", "Meccetqxep", "KonnpoeaTb", "nepenaeH0BaTb", "Bblpe3aTb",
|
||||
"3TOT KOMnb0Tep", "6b1CTPb1i AOCTyn", "npoeKTb1 Crpeneu", "Pa60ti cron"
|
||||
};
|
||||
|
||||
Console.WriteLine("--- должно проходить ---");
|
||||
double gMin = double.MaxValue;
|
||||
foreach (var s in good)
|
||||
{
|
||||
double v = EnglishBigrams.TextScore(s);
|
||||
Console.WriteLine(" {0,8:F3} {1}", v, s);
|
||||
if (!double.IsNaN(v) && v < gMin) gMin = v;
|
||||
}
|
||||
Console.WriteLine("--- должно отбраковываться ---");
|
||||
double bMax = double.MinValue;
|
||||
foreach (var s in bad)
|
||||
{
|
||||
double v = EnglishBigrams.TextScore(s);
|
||||
Console.WriteLine(" {0,8:F3} {1}", v, s);
|
||||
if (!double.IsNaN(v) && v > bMax) bMax = v;
|
||||
}
|
||||
Console.WriteLine("\nхудший «хороший»: {0:F3} лучший «плохой»: {1:F3} зазор: {2:F3}",
|
||||
gMin, bMax, gMin - bMax);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int Run(string[] args)
|
||||
{
|
||||
if (args.Contains("--selftest")) return SelfTest();
|
||||
|
||||
string inPath = Arg(args, "--in", null);
|
||||
string outDir = Arg(args, "--out", Path.Combine(Path.GetTempPath(), "screen-translator-test"));
|
||||
double scale = double.Parse(Arg(args, "--scale", "2.0"), System.Globalization.CultureInfo.InvariantCulture);
|
||||
bool translate = !args.Contains("--no-translate");
|
||||
Directory.CreateDirectory(outDir);
|
||||
|
||||
string err;
|
||||
var cfg = AppConfig.Load(out err);
|
||||
cfg.OcrScale = scale;
|
||||
if (err != null) Console.WriteLine("config: " + err);
|
||||
|
||||
var ocr = OcrService.Create(cfg.OcrLanguage);
|
||||
if (ocr == null) { Console.WriteLine("OCR недоступен"); return 2; }
|
||||
ocr.Grayscale = args.Contains("--gray");
|
||||
ocr.Invert = Arg(args, "--invert", "never");
|
||||
Console.WriteLine("OCR: {0} масштаб: {1} серый: {2} инверсия: {3}",
|
||||
ocr.LanguageTag, scale, ocr.Grayscale ? "да" : "нет", ocr.Invert);
|
||||
|
||||
CapturedFrame frame = inPath != null ? FromFile(inPath) : ScreenCapture.Capture(Native.MonitorUnderCursor());
|
||||
Console.WriteLine("кадр: {0}x{1}", frame.Width, frame.Height);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var lines = ocr.RecognizeAsync(frame, cfg.OcrScale).GetAwaiter().GetResult();
|
||||
long tOcr = sw.ElapsedMilliseconds;
|
||||
|
||||
var sampler = new FrameSampler(frame);
|
||||
int minWords = int.Parse(Arg(args, "--min-words", cfg.MinWords.ToString()));
|
||||
var blocks = Blocks.Group(lines, cfg.MinChars, cfg.SkipGarbage, sampler, minWords);
|
||||
Console.WriteLine("OCR {0} мс: строк {1} -> блоков {2}", tOcr, lines.Count, blocks.Count);
|
||||
|
||||
if (cfg.DetectForeign)
|
||||
{
|
||||
var other = OcrService.CreateOther(ocr.LanguageTag);
|
||||
if (other == null) Console.WriteLine("второй модели OCR нет — проверка языка пропущена");
|
||||
else
|
||||
{
|
||||
sw.Restart();
|
||||
var otherLines = other.RecognizeAsync(frame, cfg.OcrScale).GetAwaiter().GetResult();
|
||||
var before = new List<TextBlock>(blocks);
|
||||
Blocks.DropForeign(blocks, otherLines, Blocks.IsCyrillicLanguage(cfg.SourceLang));
|
||||
Console.WriteLine("проверка моделью «{0}» {1} мс: отброшено {2}, осталось {3}",
|
||||
other.LanguageTag, sw.ElapsedMilliseconds, before.Count - blocks.Count, blocks.Count);
|
||||
foreach (var dropped in before.Where(b => !blocks.Contains(b)))
|
||||
Console.WriteLine(" отброшено: " + Cut(dropped.Source, 70));
|
||||
}
|
||||
}
|
||||
|
||||
Blocks.ComputeGrowLimits(blocks, lines, new Size(frame.Width, frame.Height));
|
||||
|
||||
foreach (var b in blocks)
|
||||
{
|
||||
Color bg, fg;
|
||||
sampler.Sample(b.Rect, out bg, out fg);
|
||||
b.Bg = bg; b.Fg = fg;
|
||||
}
|
||||
|
||||
long tTr = 0;
|
||||
if (translate && blocks.Count > 0)
|
||||
{
|
||||
var cache = cfg.Cache ? new TranslationCache(cfg) : null;
|
||||
var chain = new TranslationChain(cfg, cache);
|
||||
sw.Restart();
|
||||
var res = chain.TranslateAsync(blocks.Select(b => b.Source).ToArray(), CancellationToken.None)
|
||||
.GetAwaiter().GetResult();
|
||||
tTr = sw.ElapsedMilliseconds;
|
||||
for (int i = 0; i < blocks.Count; i++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(res[i])) { blocks[i].Failed = true; continue; }
|
||||
blocks[i].Translated = res[i];
|
||||
}
|
||||
if (cache != null) cache.Save();
|
||||
Console.WriteLine("перевод {0} мс движком «{1}»{2}", tTr, chain.LastUsed ?? "—",
|
||||
chain.LastError != null ? " (ошибки: " + chain.LastError + ")" : "");
|
||||
}
|
||||
else foreach (var b in blocks) b.Translated = b.Source;
|
||||
|
||||
// 1) как видит OCR
|
||||
using (var dbg = Clone(frame))
|
||||
using (var g = Graphics.FromImage(dbg))
|
||||
{
|
||||
foreach (var b in blocks)
|
||||
using (var p = new Pen(Color.FromArgb(200, 255, 60, 60)))
|
||||
g.DrawRectangle(p, b.Rect);
|
||||
Save(dbg, Path.Combine(outDir, "1-blocks.png"));
|
||||
}
|
||||
|
||||
// 2) итог: перевод поверх оригинала
|
||||
using (var res = Clone(frame))
|
||||
using (var g = Graphics.FromImage(res))
|
||||
{
|
||||
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
||||
var fonts = new Dictionary<TextBlock, Font>();
|
||||
var layout = new Dictionary<TextBlock, Rectangle>();
|
||||
foreach (var b in blocks)
|
||||
{
|
||||
if (b.Translated == null) continue;
|
||||
PlateRenderer.Draw(g, b, cfg, new Size(frame.Width, frame.Height), fonts, layout);
|
||||
}
|
||||
foreach (var f in fonts.Values) f.Dispose();
|
||||
Save(res, Path.Combine(outDir, "2-translated.png"));
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
foreach (var b in blocks.OrderBy(b => b.Rect.Top).Take(15))
|
||||
Console.WriteLine(" [{0,4},{1,4} {2,3}x{3,-3} h={4:0.0}] {5}\n -> {6}",
|
||||
b.Rect.X, b.Rect.Y, b.Rect.Width, b.Rect.Height, b.LineHeight,
|
||||
Cut(b.Source, 70), Cut(b.Translated ?? "—", 70));
|
||||
|
||||
// Замер качества распознавания: сколько заранее известных строк реально нашлось.
|
||||
// Без такой метрики любые правки препроцессинга оцениваются на глаз, а на глаз
|
||||
// одинаково убедительно выглядят и улучшение, и ухудшение.
|
||||
string expect = Arg(args, "--expect", null);
|
||||
if (expect != null)
|
||||
{
|
||||
var want = File.ReadAllLines(expect)
|
||||
.Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
|
||||
var all = string.Join(" | ", blocks.Select(b => b.Source));
|
||||
int hit = 0;
|
||||
var missed = new List<string>();
|
||||
foreach (var w in want)
|
||||
{
|
||||
if (all.IndexOf(w, StringComparison.OrdinalIgnoreCase) >= 0) hit++;
|
||||
else missed.Add(w);
|
||||
}
|
||||
Console.WriteLine("\nРАСПОЗНАНО: {0} из {1} ({2:P0})", hit, want.Count, (double)hit / want.Count);
|
||||
if (missed.Count > 0) Console.WriteLine("не найдено: " + string.Join(", ", missed));
|
||||
}
|
||||
|
||||
Console.WriteLine("\nкартинки: " + outDir);
|
||||
frame.Image.Dispose();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static string Cut(string s, int n) { return s.Length <= n ? s : s.Substring(0, n - 1) + "…"; }
|
||||
|
||||
static string Arg(string[] a, string name, string def)
|
||||
{
|
||||
int i = Array.IndexOf(a, name);
|
||||
return i >= 0 && i + 1 < a.Length ? a[i + 1] : def;
|
||||
}
|
||||
|
||||
static Bitmap Clone(CapturedFrame f)
|
||||
{
|
||||
var b = new Bitmap(f.Width, f.Height, PixelFormat.Format32bppArgb);
|
||||
using (var g = Graphics.FromImage(b)) g.DrawImageUnscaled(f.Image, 0, 0);
|
||||
return b;
|
||||
}
|
||||
|
||||
static CapturedFrame FromFile(string path)
|
||||
{
|
||||
using (var src = new Bitmap(path))
|
||||
{
|
||||
var bmp = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb);
|
||||
using (var g = Graphics.FromImage(bmp)) g.DrawImageUnscaled(src, 0, 0);
|
||||
var tmp = new CapturedFrame
|
||||
{
|
||||
Image = bmp,
|
||||
Width = bmp.Width,
|
||||
Height = bmp.Height,
|
||||
Area = new Rectangle(0, 0, bmp.Width, bmp.Height)
|
||||
};
|
||||
// тот же путь извлечения пикселей, что и у экранного захвата
|
||||
tmp.Pixels = ScreenCapture.Crop(tmp, new Rectangle(0, 0, bmp.Width, bmp.Height)).Pixels;
|
||||
return tmp;
|
||||
}
|
||||
}
|
||||
|
||||
static void Save(Bitmap b, string path)
|
||||
{
|
||||
b.Save(path, ImageFormat.Png);
|
||||
Console.WriteLine(" сохранено: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
TH 116997844
|
||||
HE 100689263
|
||||
IN 87674002
|
||||
ER 77134382
|
||||
AN 69775179
|
||||
RE 60923600
|
||||
ES 57070453
|
||||
ON 56915252
|
||||
ST 54018399
|
||||
NT 50701084
|
||||
EN 48991276
|
||||
AT 48274564
|
||||
ED 46647960
|
||||
ND 46194306
|
||||
TO 46115188
|
||||
OR 45725191
|
||||
EA 43329810
|
||||
TI 42888666
|
||||
AR 42353262
|
||||
TE 42295813
|
||||
NG 38567365
|
||||
AL 38211584
|
||||
IT 37938534
|
||||
AS 37773878
|
||||
IS 37349981
|
||||
HA 35971841
|
||||
ET 32872552
|
||||
SE 31532272
|
||||
OU 31112284
|
||||
OF 30540904
|
||||
LE 30383262
|
||||
SA 30080131
|
||||
VE 29320973
|
||||
RO 29230770
|
||||
RA 28645577
|
||||
RI 27634643
|
||||
HI 27495342
|
||||
NE 27331675
|
||||
ME 27237733
|
||||
DE 27029835
|
||||
CO 26737101
|
||||
TA 26147593
|
||||
EC 25775798
|
||||
SI 25758841
|
||||
LL 24636875
|
||||
SO 23903631
|
||||
NA 23547524
|
||||
LI 23291169
|
||||
LA 23178317
|
||||
EL 23092248
|
||||
MA 21828378
|
||||
DI 21673998
|
||||
IC 21468412
|
||||
RT 21456059
|
||||
NS 21306421
|
||||
RS 21237259
|
||||
IO 21210160
|
||||
OM 21066156
|
||||
CH 20132750
|
||||
OT 20088048
|
||||
CA 19930754
|
||||
CE 19803619
|
||||
HO 19729026
|
||||
BE 19468489
|
||||
TT 19367472
|
||||
FO 18923772
|
||||
TS 18922522
|
||||
SS 18915696
|
||||
NO 18894111
|
||||
EE 18497942
|
||||
EM 18145294
|
||||
AC 17904683
|
||||
IL 17877600
|
||||
DA 17584055
|
||||
NI 17452104
|
||||
UR 17341717
|
||||
WA 16838794
|
||||
SH 16773127
|
||||
EI 16026915
|
||||
AM 15975981
|
||||
TR 15821226
|
||||
DT 15759673
|
||||
US 15699353
|
||||
LO 15596310
|
||||
PE 15573318
|
||||
UN 15237699
|
||||
NC 15214623
|
||||
WI 15213018
|
||||
UT 15137169
|
||||
AD 14877234
|
||||
EW 14776406
|
||||
OW 14610429
|
||||
GE 14425023
|
||||
EP 14024377
|
||||
AI 13974919
|
||||
LY 13742031
|
||||
OL 13726491
|
||||
FT 13696078
|
||||
OS 13596265
|
||||
EO 13524186
|
||||
EF 13252227
|
||||
PR 13191182
|
||||
WE 13185116
|
||||
DO 13120322
|
||||
MO 12950768
|
||||
ID 12896787
|
||||
IE 12505546
|
||||
MI 12168944
|
||||
PA 12068709
|
||||
FI 11993833
|
||||
PO 11917535
|
||||
CT 11888752
|
||||
WH 11852909
|
||||
IR 11681353
|
||||
AY 11523416
|
||||
GA 11239788
|
||||
SC 10800636
|
||||
KE 10650670
|
||||
EV 10574011
|
||||
SP 10570626
|
||||
IM 10544422
|
||||
OP 10459455
|
||||
DS 10429887
|
||||
LD 10245579
|
||||
UL 10173468
|
||||
OO 10168856
|
||||
SU 10031005
|
||||
IA 10002012
|
||||
GH 9880399
|
||||
PL 9812226
|
||||
EB 9738798
|
||||
IG 9530574
|
||||
VI 9380037
|
||||
IV 9129232
|
||||
WO 9106647
|
||||
YO 9088497
|
||||
RD 9025637
|
||||
TW 8910254
|
||||
BA 8867461
|
||||
AG 8809266
|
||||
RY 8788539
|
||||
AB 8775582
|
||||
LS 8675452
|
||||
SW 8673234
|
||||
AP 8553911
|
||||
FE 8529289
|
||||
TU 8477495
|
||||
CI 8446084
|
||||
FA 8357929
|
||||
HT 8351551
|
||||
FR 8339376
|
||||
AV 8288885
|
||||
EG 8286463
|
||||
GO 8188708
|
||||
BO 8172395
|
||||
BU 8113271
|
||||
TY 8008918
|
||||
MP 7835172
|
||||
OC 7646952
|
||||
OD 7610214
|
||||
EH 7559141
|
||||
YS 7539621
|
||||
EY 7528342
|
||||
RM 7377989
|
||||
OV 7350014
|
||||
GT 7347990
|
||||
YA 7239548
|
||||
CK 7205091
|
||||
GI 7103140
|
||||
RN 7064635
|
||||
GR 6989963
|
||||
RC 6974063
|
||||
BL 6941044
|
||||
LT 6817273
|
||||
YT 6714151
|
||||
OA 6554221
|
||||
YE 6499305
|
||||
OB 6212512
|
||||
DB 6106719
|
||||
FF 6085519
|
||||
SF 6073995
|
||||
RR 5896212
|
||||
DU 5861311
|
||||
KI 5814357
|
||||
UC 5742385
|
||||
IF 5740414
|
||||
AF 5702567
|
||||
DR 5701879
|
||||
CL 5683204
|
||||
EX 5649363
|
||||
SM 5580755
|
||||
PI 5559210
|
||||
SB 5553684
|
||||
CR 5514347
|
||||
TL 5403137
|
||||
OI 5336616
|
||||
RU 5330557
|
||||
UP 5306948
|
||||
BY 5232074
|
||||
TC 5196817
|
||||
NN 5180899
|
||||
AK 5137311
|
||||
SL 4965012
|
||||
NF 4950333
|
||||
UE 4927837
|
||||
DW 4906814
|
||||
AU 4884168
|
||||
PP 4873393
|
||||
UG 4832325
|
||||
RL 4803246
|
||||
RG 4645938
|
||||
BR 4621080
|
||||
CU 4604045
|
||||
UA 4589997
|
||||
DH 4585765
|
||||
RK 4491400
|
||||
YI 4461214
|
||||
LU 4402940
|
||||
UM 4389720
|
||||
BI 4356462
|
||||
NY 4343290
|
||||
NW 4215967
|
||||
QU 4169424
|
||||
OG 4163126
|
||||
SN 4157990
|
||||
MB 4121764
|
||||
VA 4111375
|
||||
DF 4033878
|
||||
DD 4001275
|
||||
MS 3922855
|
||||
GS 3920675
|
||||
AW 3918960
|
||||
NH 3915410
|
||||
PU 3858148
|
||||
HR 3843001
|
||||
SD 3842250
|
||||
TB 3815459
|
||||
PT 3812475
|
||||
NM 3796928
|
||||
DC 3782481
|
||||
GU 3768430
|
||||
TM 3759861
|
||||
MU 3755834
|
||||
NU 3732602
|
||||
MM 3730508
|
||||
NL 3692985
|
||||
EU 3674130
|
||||
WN 3649615
|
||||
NB 3602692
|
||||
RP 3588188
|
||||
DM 3544905
|
||||
SR 3513808
|
||||
UD 3499535
|
||||
UI 3481482
|
||||
RF 3436232
|
||||
OK 3397570
|
||||
YW 3379064
|
||||
TF 3368452
|
||||
IP 3348621
|
||||
RW 3348005
|
||||
RB 3346212
|
||||
OH 3254659
|
||||
KS 3227333
|
||||
DP 3145043
|
||||
FU 3138900
|
||||
YC 3128053
|
||||
TP 3070427
|
||||
MT 3055946
|
||||
DL 3050945
|
||||
NK 3043200
|
||||
CC 3026492
|
||||
UB 2990868
|
||||
RH 2968706
|
||||
NP 2968126
|
||||
JU 2924815
|
||||
FL 2890839
|
||||
DN 2840522
|
||||
KA 2833038
|
||||
PH 2825344
|
||||
HU 2771830
|
||||
JO 2721345
|
||||
LF 2702522
|
||||
YB 2696786
|
||||
RV 2692445
|
||||
OE 2616308
|
||||
IB 2598444
|
||||
IK 2585124
|
||||
YP 2581863
|
||||
GL 2576787
|
||||
LP 2543957
|
||||
YM 2516273
|
||||
LB 2463693
|
||||
HS 2462026
|
||||
DG 2442139
|
||||
GN 2426429
|
||||
EK 2411639
|
||||
NR 2393580
|
||||
PS 2377036
|
||||
TD 2346516
|
||||
LC 2328063
|
||||
SK 2321888
|
||||
YF 2305244
|
||||
YH 2291273
|
||||
VO 2253292
|
||||
AH 2225270
|
||||
DY 2218040
|
||||
LM 2216514
|
||||
SY 2214270
|
||||
NV 2194534
|
||||
YD 2122337
|
||||
FS 2047416
|
||||
SG 2043770
|
||||
YR 2021939
|
||||
YL 2013939
|
||||
WS 1988727
|
||||
MY 1949129
|
||||
OY 1932892
|
||||
KN 1903836
|
||||
IZ 1865802
|
||||
XP 1840696
|
||||
LW 1836811
|
||||
TN 1782119
|
||||
KO 1758001
|
||||
AA 1721143
|
||||
JA 1712763
|
||||
ZE 1709871
|
||||
FC 1570791
|
||||
GW 1567991
|
||||
TG 1530045
|
||||
XT 1509969
|
||||
FH 1507604
|
||||
LR 1505092
|
||||
JE 1487348
|
||||
YN 1485655
|
||||
GG 1468286
|
||||
GF 1465290
|
||||
EQ 1461436
|
||||
HY 1446451
|
||||
KT 1443985
|
||||
HC 1441057
|
||||
BS 1409672
|
||||
HW 1403223
|
||||
HN 1383958
|
||||
CS 1381608
|
||||
HM 1353001
|
||||
NJ 1342735
|
||||
HH 1329998
|
||||
WT 1301293
|
||||
GC 1299541
|
||||
LH 1274048
|
||||
EJ 1256993
|
||||
FM 1251312
|
||||
DV 1238565
|
||||
LV 1238287
|
||||
WR 1226755
|
||||
GP 1215204
|
||||
FP 1199845
|
||||
GB 1184377
|
||||
GM 1178511
|
||||
HL 1169468
|
||||
LK 1164186
|
||||
CY 1145316
|
||||
MC 1101727
|
||||
YG 1049082
|
||||
XI 1024736
|
||||
HB 1014004
|
||||
FW 1005903
|
||||
GY 979804
|
||||
HP 978649
|
||||
MW 937621
|
||||
PM 931225
|
||||
ZA 929119
|
||||
LG 926472
|
||||
IW 922059
|
||||
XA 904148
|
||||
FB 888155
|
||||
SV 882083
|
||||
GD 879792
|
||||
IX 879360
|
||||
AJ 870262
|
||||
KL 846309
|
||||
HF 834284
|
||||
HD 828755
|
||||
AE 815963
|
||||
SQ 800346
|
||||
DJ 799366
|
||||
FY 789961
|
||||
AZ 768359
|
||||
LN 752316
|
||||
AO 749566
|
||||
FD 748027
|
||||
KW 719633
|
||||
MF 715087
|
||||
MH 710864
|
||||
SJ 704442
|
||||
UF 701892
|
||||
TV 698150
|
||||
XC 697995
|
||||
YU 695512
|
||||
BB 689158
|
||||
WW 674610
|
||||
OJ 661082
|
||||
AX 660826
|
||||
MR 660619
|
||||
WL 657782
|
||||
XE 653947
|
||||
KH 650095
|
||||
OX 650078
|
||||
UO 649906
|
||||
ZI 644035
|
||||
FG 637758
|
||||
IH 610683
|
||||
TK 610333
|
||||
II 607124
|
||||
IU 576683
|
||||
TJ 559473
|
||||
MN 558397
|
||||
WY 553647
|
||||
KY 553296
|
||||
KF 537342
|
||||
FN 534362
|
||||
UY 531960
|
||||
PW 530411
|
||||
DK 525744
|
||||
RJ 518157
|
||||
UK 514873
|
||||
KR 507020
|
||||
KU 506618
|
||||
WM 505687
|
||||
KM 485617
|
||||
MD 481126
|
||||
ML 478528
|
||||
EZ 465466
|
||||
KB 457860
|
||||
WC 448394
|
||||
WD 432646
|
||||
HG 429607
|
||||
BT 428276
|
||||
ZO 424016
|
||||
KC 420017
|
||||
PF 418168
|
||||
YV 411487
|
||||
PC 400308
|
||||
PY 396147
|
||||
WB 394820
|
||||
YK 391953
|
||||
CP 382923
|
||||
YJ 378679
|
||||
KP 375653
|
||||
PB 369336
|
||||
CD 358435
|
||||
JI 357577
|
||||
UW 352732
|
||||
UH 339341
|
||||
WF 336213
|
||||
YY 332973
|
||||
WP 321746
|
||||
BC 320380
|
||||
AQ 315068
|
||||
CB 298053
|
||||
IQ 291635
|
||||
CM 285942
|
||||
MG 285133
|
||||
DQ 283314
|
||||
BJ 282608
|
||||
TZ 280007
|
||||
KD 277982
|
||||
PD 273162
|
||||
FJ 269865
|
||||
CF 267630
|
||||
NZ 266461
|
||||
CW 257253
|
||||
FV 244685
|
||||
VY 233082
|
||||
FK 228905
|
||||
OZ 228556
|
||||
ZZ 221275
|
||||
IJ 219128
|
||||
LJ 218362
|
||||
NQ 217422
|
||||
UV 212051
|
||||
XO 211173
|
||||
PG 211133
|
||||
HK 210385
|
||||
KG 209266
|
||||
VS 204093
|
||||
HV 197539
|
||||
BM 191807
|
||||
HJ 189906
|
||||
CN 188046
|
||||
GV 186777
|
||||
CG 181590
|
||||
WU 180884
|
||||
GJ 176947
|
||||
XH 166599
|
||||
GK 163830
|
||||
TQ 159111
|
||||
CQ 157546
|
||||
RQ 156933
|
||||
BH 154489
|
||||
XS 154347
|
||||
UZ 153736
|
||||
WK 148964
|
||||
XU 147533
|
||||
UX 144814
|
||||
BD 141752
|
||||
BW 140189
|
||||
WG 139890
|
||||
MV 136314
|
||||
MJ 134263
|
||||
PN 131645
|
||||
XM 127492
|
||||
OQ 122677
|
||||
BV 120081
|
||||
XW 119322
|
||||
KK 118811
|
||||
BP 115161
|
||||
ZU 113538
|
||||
RZ 113432
|
||||
XF 113031
|
||||
MK 111041
|
||||
ZH 107639
|
||||
BN 106125
|
||||
ZY 105871
|
||||
HQ 101241
|
||||
WJ 99435
|
||||
IY 98361
|
||||
DZ 98038
|
||||
VR 96416
|
||||
ZS 94993
|
||||
XY 94329
|
||||
CV 94224
|
||||
XB 94041
|
||||
XR 90046
|
||||
UJ 88168
|
||||
YQ 87953
|
||||
VD 85611
|
||||
PK 83017
|
||||
VU 82830
|
||||
JR 80471
|
||||
ZL 80039
|
||||
SZ 79840
|
||||
YZ 78281
|
||||
LQ 77148
|
||||
KJ 76816
|
||||
BF 75352
|
||||
NX 74844
|
||||
QA 73527
|
||||
QI 73387
|
||||
KV 73184
|
||||
ZW 68865
|
||||
WV 63930
|
||||
UU 63043
|
||||
VT 62912
|
||||
VP 62577
|
||||
XD 60101
|
||||
GQ 59750
|
||||
XL 59585
|
||||
VC 59024
|
||||
CZ 57914
|
||||
LZ 57314
|
||||
ZT 56955
|
||||
WZ 52836
|
||||
SX 50975
|
||||
ZB 50652
|
||||
VL 49032
|
||||
PV 48105
|
||||
FQ 47504
|
||||
PJ 47043
|
||||
ZM 46034
|
||||
VW 45608
|
||||
CJ 41526
|
||||
ZC 41037
|
||||
BG 40516
|
||||
JS 39326
|
||||
XG 39289
|
||||
RX 38654
|
||||
HZ 37066
|
||||
XX 35052
|
||||
VM 35024
|
||||
XN 34734
|
||||
QW 34669
|
||||
JP 34520
|
||||
VN 33082
|
||||
ZD 32906
|
||||
ZR 32685
|
||||
FZ 31186
|
||||
XV 31117
|
||||
ZP 30389
|
||||
VH 30203
|
||||
VB 29192
|
||||
ZF 28658
|
||||
GZ 28514
|
||||
TX 28156
|
||||
VF 28090
|
||||
DX 27413
|
||||
QB 27307
|
||||
BK 26993
|
||||
ZG 26369
|
||||
VG 25585
|
||||
JC 24770
|
||||
ZK 24262
|
||||
ZN 24241
|
||||
UQ 23386
|
||||
JM 22338
|
||||
VV 22329
|
||||
JD 21903
|
||||
MQ 21358
|
||||
JH 20960
|
||||
QS 20847
|
||||
JT 20408
|
||||
JB 19380
|
||||
FX 19313
|
||||
PQ 18607
|
||||
MZ 18271
|
||||
YX 16945
|
||||
QT 16914
|
||||
WQ 16245
|
||||
JJ 16085
|
||||
JW 16083
|
||||
LX 15467
|
||||
GX 14778
|
||||
JN 14452
|
||||
ZV 14339
|
||||
MX 14250
|
||||
JK 13967
|
||||
KQ 13905
|
||||
XK 13651
|
||||
JF 12640
|
||||
QM 12315
|
||||
QH 12273
|
||||
JL 12149
|
||||
JG 12023
|
||||
VK 11469
|
||||
VJ 11432
|
||||
KZ 11192
|
||||
QC 10667
|
||||
XJ 10629
|
||||
PZ 9697
|
||||
QL 9603
|
||||
QO 9394
|
||||
JV 8925
|
||||
QF 8778
|
||||
QD 8678
|
||||
BZ 8132
|
||||
HX 7526
|
||||
ZJ 7167
|
||||
PX 6814
|
||||
QP 6062
|
||||
QE 6020
|
||||
QR 5975
|
||||
ZQ 5773
|
||||
JY 5723
|
||||
BQ 5513
|
||||
XQ 5416
|
||||
CX 5300
|
||||
KX 5083
|
||||
WX 4678
|
||||
QY 4557
|
||||
QV 4212
|
||||
QN 3808
|
||||
VX 3192
|
||||
BX 3021
|
||||
JZ 2859
|
||||
VZ 2633
|
||||
QG 2567
|
||||
QQ 2499
|
||||
ZX 2463
|
||||
XZ 2082
|
||||
QK 2023
|
||||
VQ 1488
|
||||
QJ 1342
|
||||
QX 765
|
||||
JX 747
|
||||
JQ 722
|
||||
QZ 280
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Mining
|
||||
Tailoring
|
||||
Cooking
|
||||
Engineering
|
||||
First Aid
|
||||
Leatherworking
|
||||
Search
|
||||
Has Skill Up
|
||||
Have Materials
|
||||
Trade Goods
|
||||
Not equippable
|
||||
All Subclasses
|
||||
All Slots
|
||||
Consumable
|
||||
@@ -0,0 +1,123 @@
|
||||
<#
|
||||
Превращает частотную таблицу английских биграмм в компактный C#-файл.
|
||||
|
||||
Каждая из 676 биграмм кодируется одним печатным ASCII-символом (квантованный log10
|
||||
вероятности), поэтому вся модель — одна строка примерно в 700 байт вместо мегабайтных
|
||||
словарей и без единой внешней зависимости.
|
||||
|
||||
Источник частот: practicalcryptography.com (корпус английских текстов).
|
||||
#>
|
||||
param(
|
||||
[string]$In = "$PSScriptRoot\english_bigrams.txt",
|
||||
[string]$Out = "$PSScriptRoot\..\src\EnglishBigrams.cs"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$counts = @{}
|
||||
[double]$total = 0
|
||||
foreach ($line in Get-Content $In) {
|
||||
if ($line -notmatch '^\s*([A-Za-z]{2})\s+(\d+)\s*$') { continue }
|
||||
$bg = $Matches[1].ToUpperInvariant()
|
||||
$n = [double]$Matches[2]
|
||||
$counts[$bg] = $n
|
||||
$total += $n
|
||||
}
|
||||
Write-Host "биграмм: $($counts.Count), суммарная частота: $total"
|
||||
|
||||
# Квантование: log10(p) укладываем в печатный ASCII с шагом 1/12 порядка величины.
|
||||
$LO = -9.0; $STEP = 12.0
|
||||
$sb = New-Object Text.StringBuilder
|
||||
$minLog = 0.0
|
||||
foreach ($i in 0..25) {
|
||||
foreach ($j in 0..25) {
|
||||
$bg = [char](65 + $i) + [string][char](65 + $j)
|
||||
$n = if ($counts.ContainsKey($bg)) { $counts[$bg] } else { 0 }
|
||||
# ненаблюдаемой биграмме даём вероятность заметно ниже самой редкой наблюдаемой
|
||||
$p = if ($n -gt 0) { $n / $total } else { 1e-9 }
|
||||
$lg = [Math]::Log10($p)
|
||||
if ($lg -lt $minLog) { $minLog = $lg }
|
||||
$q = [int][Math]::Round(($lg - $LO) * $STEP)
|
||||
if ($q -lt 0) { $q = 0 }
|
||||
if ($q -gt 88) { $q = 88 }
|
||||
[void]$sb.Append([char](34 + $q)) # с 34, чтобы не попасть на кавычку и обратный слэш
|
||||
}
|
||||
}
|
||||
Write-Host "минимальный log10(p): $minLog"
|
||||
|
||||
$data = $sb.ToString()
|
||||
$chunks = @()
|
||||
for ($i = 0; $i -lt $data.Length; $i += 130) {
|
||||
$len = [Math]::Min(130, $data.Length - $i)
|
||||
$chunks += ' "' + $data.Substring($i, $len).Replace('\', '\\') + '"'
|
||||
}
|
||||
$joined = $chunks -join " +`r`n"
|
||||
|
||||
$code = @"
|
||||
using System;
|
||||
|
||||
namespace ScreenTranslator
|
||||
{
|
||||
/// <summary>Модель правдоподобия английского текста по частотам буквенных пар.
|
||||
///
|
||||
/// Решает задачу, с которой не справляются ни словарь, ни алфавит: английская модель OCR,
|
||||
/// прочитав русскую надпись, выдаёт правдоподобное на вид латинское слово («Профиль» →
|
||||
/// «npocbwlb»). Но пары букв выдают подделку сразу — сочетаний «wl», «cb», «bw» в английском
|
||||
/// практически не бывает, тогда как «th», «he», «in» составляют его основу.
|
||||
///
|
||||
/// Таблица — 676 квантованных log10-вероятностей, по символу на пару, около 700 байт.
|
||||
/// Сгенерирована tools/make-bigrams.ps1 из частот корпуса английских текстов
|
||||
/// (practicalcryptography.com). Правится только генератором, не руками.</summary>
|
||||
internal static class EnglishBigrams
|
||||
{
|
||||
const double Lo = $LO, Step = $STEP;
|
||||
|
||||
const string Table =
|
||||
$joined;
|
||||
|
||||
/// <summary>Средний log10 вероятности пар букв в слове. Около -2,5 у обычного английского
|
||||
/// слова, ниже -4 у случайного набора букв. NaN, если букв слишком мало для оценки.</summary>
|
||||
public static double WordScore(string word)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word)) return double.NaN;
|
||||
double sum = 0; int n = 0;
|
||||
for (int i = 0; i + 1 < word.Length; i++)
|
||||
{
|
||||
int a = Index(word[i]), b = Index(word[i + 1]);
|
||||
if (a < 0 || b < 0) continue;
|
||||
sum += Lo + (Table[a * 26 + b] - 34) / Step;
|
||||
n++;
|
||||
}
|
||||
return n == 0 ? double.NaN : sum / n;
|
||||
}
|
||||
|
||||
static int Index(char c)
|
||||
{
|
||||
if (c >= 'a' && c <= 'z') return c - 'a';
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Оценка текста целиком: усреднение по словам длиной от четырёх букв —
|
||||
/// на более коротких пары букв ничего не доказывают. NaN, если оценивать нечего.</summary>
|
||||
public static double TextScore(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return double.NaN;
|
||||
double sum = 0; int n = 0;
|
||||
foreach (var w in text.Split(new[] { ' ', '\t', '\n', '\r', '.', ',', ':', ';', '!', '?',
|
||||
'(', ')', '[', ']', '"', '\'', '/', '\\', '-' },
|
||||
StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (w.Length < 4) continue;
|
||||
double s = WordScore(w);
|
||||
if (double.IsNaN(s)) continue;
|
||||
sum += s; n++;
|
||||
}
|
||||
return n == 0 ? double.NaN : sum / n;
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$code | Set-Content $Out -Encoding UTF8
|
||||
Write-Host "записано: $Out ($($data.Length) символов таблицы)"
|
||||
@@ -0,0 +1,85 @@
|
||||
<#
|
||||
Эталонный кадр с РУССКИМ интерфейсом.
|
||||
|
||||
Нужен как негативный тест: английская модель OCR читает кириллицу как бессмыслицу,
|
||||
и эту бессмыслицу нельзя ни переводить, ни закрывать плашками. На таком кадре
|
||||
правильный результат — ноль переведённых блоков.
|
||||
#>
|
||||
param([string]$Out = "$PSScriptRoot\..\dist\sample-ru.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 32 34 42), 0, 0, $W, $H)
|
||||
$g.FillRectangle((Brush 24 26 32), 0, 0, 260, $H)
|
||||
|
||||
$white = Brush 236 238 243
|
||||
$grey = Brush 150 155 168
|
||||
$body = Brush 205 210 220
|
||||
|
||||
# боковая панель — короткие подписи, на них и сыпался фильтр
|
||||
Text 'Быстрый доступ' (Fnt 12) $grey 20 18
|
||||
Text 'Рабочий стол' (Fnt 14) $white 20 46
|
||||
Text 'Загрузки' (Fnt 14) $white 20 76
|
||||
Text 'Документы' (Fnt 14) $white 20 106
|
||||
Text 'Изображения' (Fnt 14) $white 20 136
|
||||
Text 'Этот компьютер' (Fnt 14) $white 20 176
|
||||
Text 'Проекты' (Fnt 14) $white 20 206
|
||||
Text 'Стрелец' (Fnt 14) $white 20 236
|
||||
|
||||
# панель инструментов
|
||||
Text 'Создать' (Fnt 13) $white 290 20
|
||||
Text 'Вырезать' (Fnt 13) $white 370 20
|
||||
Text 'Копировать' (Fnt 13) $white 460 20
|
||||
Text 'Переименовать' (Fnt 13) $white 570 20
|
||||
Text 'Сортировать' (Fnt 13) $white 700 20
|
||||
Text 'Просмотреть' (Fnt 13) $white 810 20
|
||||
|
||||
# заголовки таблицы
|
||||
Text 'Имя' (Fnt 13) $grey 290 70
|
||||
Text 'Дата изменения' (Fnt 13) $grey 600 70
|
||||
Text 'Тип' (Fnt 13) $grey 820 70
|
||||
Text 'Размер' (Fnt 13) $grey 980 70
|
||||
|
||||
# строки таблицы: имена файлов латиницей рядом с русскими типами
|
||||
$rows = @(
|
||||
@('config.json', '27.07.2026 13:05', 'Файл "JSON"', '3 КБ'),
|
||||
@('cache.json', '27.07.2026 13:17', 'Файл "JSON"', '18 КБ'),
|
||||
@('sample.png', '27.07.2026 13:05', 'Файл "PNG"', '41 КБ'),
|
||||
@('ScreenTranslator.exe', '27.07.2026 13:17', 'Приложение', '81 КБ')
|
||||
)
|
||||
$y = 104
|
||||
foreach ($r in $rows) {
|
||||
Text $r[0] (Fnt 14) $body 290 $y
|
||||
Text $r[1] (Fnt 13) $body 600 $y
|
||||
Text $r[2] (Fnt 13) $body 820 $y
|
||||
Text $r[3] (Fnt 13) $body 980 $y
|
||||
$y += 30
|
||||
}
|
||||
|
||||
# связный русский абзац
|
||||
$para = @(
|
||||
'Выберите файлы, которые нужно переместить в архив. После подтверждения',
|
||||
'операция будет выполнена в фоновом режиме, а исходные файлы останутся',
|
||||
'на месте до завершения проверки контрольных сумм.'
|
||||
)
|
||||
$y = 300
|
||||
foreach ($line in $para) { Text $line (Fnt 15) $body 290 $y; $y += 26 }
|
||||
|
||||
Text 'Элементов: 6' (Fnt 12) $grey 290 420
|
||||
Text 'Выбран 1 элемент: 73,5 КБ' (Fnt 12) $grey 420 420
|
||||
|
||||
$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"
|
||||
@@ -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 абзаца целыми блоками, заголовки и пункты списка отдельно."
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user