Upyr
EN УК

Нотатки з поля · три дні роботи

Як я повертав до життя мертвий текст

Чиста реалізація найстарішого трюку Punto Switcher — спершу відтворена з поведінки й бенчмарків, а потім звірена з реальними бінарниками, щоб закрити недокументовані теги правил.

Rust · WASM EN ↔ UK знаковий n-грам точність 1.000
[ х
[ks,хліб
одна клавіша. два значення. увесь застосунок — це вибір правильного.

Проблема

Правильні клавіші, але не та розкладка

Ти сідаєш написати привіт, українська розкладка не ввімкнена — і клавіші виходять як ghbdsn. Або хочеш hello, а отримуєш руддщ. Якщо ти пишеш двома мовами, це трапляється сто разів на день, і щоразу треба зупинитися, стерти, перемкнути, набрати заново.

Punto Switcher — багаторічний інструмент Яндекса — зробив собі ім’я саме на тому, що виправляє це за тебе: стежить за клавішами, впізнає абракадабру, тихо перенабирає її в іншій розкладці. Я хотів те саме для української — але без інструмента, який іде в комплекті. Тож зробив Upyr: упир — це той, хто повернувся з мертвих, а це приблизно те, що застосунок робить із твоїм понівеченим текстом. Rust, чиста реалізація, жодного рядка спільного з Punto.

Поміняти літери — це легка частина: QWERTY та ЙЦУКЕН — це фіксовані позиційні карти, пошук на п’ятдесят рядків. Складне — зрозуміти, коли їх міняти. Кожне завершене слово — це «так» чи «ні»: лишити чи переписати? Перепишеш слово, яке людина справді мала на увазі, — і ти щойно спотворив її текст. Саме через це видаляють застосунки. Пропущене виправлення — це знизане плече; хибне виправлення — це зрада. Тому весь дизайн схиляється до одного правила: сумніваєшся — не роби нічого.

Як воно вийшло

Три дні: від першого релізу до підписаної v0.2.0

Уся історія — просто в логу комітів. Кожен вузол нижче — реальний коміт, а гранатові — це моменти, коли щось клацнуло, або коли бенчмарк упіймав мене на помилці.

День 1 · 16 лип.Ключова ідея приходить
  • Initial Upyr releaseзастосунок у треї, зміна розкладки, обв’язка доступності macOS
  • feat: train compact language n-gram modelз’являється статистичний оцінювач
  • fix: recognize mistyped Ukrainian bracket words
    момент, коли дизайн повертає: [ks, — це не пунктуація, це хліб
  • fix: honor Ukrainian physical punctuation keys
    оцінювання переходить у простір фізичних клавіш, незалежний від розкладки
День 2 · 17 лип.Загартовування й винесення ядра
  • fix: handle names and terminal punctuation
    зберегти кому в кінці: Jkmuf,Ольга,
  • test: simulate multilingual typing streamsпокриття послідовностей подій
  • feat: extract core and add wasm delivery foundation
    рушій стає переносним — та сама логіка рішень у браузері
  • release: harden Upyr 0.1.0 public previewперша тегована збірка, універсальний бандл macOS
День 3 · 18 лип.Вимірювання й ланцюг постачання
  • perf + trigger layer + recall benchmark
    висновки впроваджено: шар тригерів для сліпої зони коротких слів, правила політики для колізій, які знайшла відкладена вибірка
  • ci(security): scanners, SBOM, and Cosign signingCycloneDX SBOM, безключові підписи → v0.2.0

Дизайн, відтворений із поведінки

Рішення у два шари — бо один шар не буває водночас і безпечним, і повним

Punto ніде не каже, як воно вирішує, тож перша версія Upyr дивилася на поведінку — клавіші на вході, виправлення на виході — доки крізь це не проступила форма. Пізніше radare2 і Ghidra підтвердили глибший поділ: Mac-збірка Punto має бітові прапорці та рядкові перевірки, а Windows-збірка ходить скомпільованим автоматом правил. Upyr бере переносний висновок: коротка таблиця жорстких правил має перше слово й може скасувати будь-що; усе інше йде до статистичної моделі, яка радо каже «не знаю» й мовчить.

Про етику розбирання У Upyr не постачається ані код Punto, ані її дані правил. Бінарники розібрано, щоб зрозуміти формати файлів і семантику тегів, а корисну ідею потім окремо реалізовано в Rust. І тут є в’їдлива іронія: Punto Switcher постачає «щоденник» натискань, який записує набраний текст, — саме ту приватнісну проблему, яку середовище Upyr без жодної мережі й існує, щоб відкинути.
готове слово фізичні клавіші трекер клавіш незалежний від розкладки простір KeyboardEvent.code ШАР 1 triggers.dat 12 точних правил · детерм. скасовують модель ШАР 2 знаковий n-грам 173,964 записи · ~2.8 MiB мовчить, коли не певен Виправити → Лишити тригер може змусити в будь-який бік

Два шари роблять різну роботу. Модель обережна й не вгадуватиме. Правила — для тієї жменьки випадків, у яких я вже впевнений, зокрема для коротких частих слів, які модель навмисно пропускає.

Чим було розбирання — а чим ні

Що насправді всередині Punto Switcher

Слушне запитання до всього, що зветься «розбиранням»: із чого Punto зроблено і чим його розкрили? Відповідь вийшла змішаною: частина — публічна структура файлів, частина — робота з бінарниками.

Punto Switcher — багаторічний перемикач розкладки від Яндекса, з Windows- і macOS-збірками. Windows-виконуваний файл (punto.exe) обирає розкладку за словником у кілька мільйонів слів плюс правилом «неможливого поєднання» — російське слово не може починатися з певних літер, тож їхня низка викриває не ту розкладку — і перевіряє це на кожен пробіл, Enter чи Tab. Її стан живе в кількох двійкових, не-текстових файлах .dat:

ФайлРоль
Data/triggers.datправила-тригери перемикання — саме цей файл вихідник проєкту називає своєю моделлю
Data/ps.datосновний мовний словник (той список у кілька мільйонів слів)
Data/translit.datтаблиця транслітерації
User Data/replace.datавтозаміна / розгортання скорочень
User Data/user.dicкористувацький словник винятків
diary.datщоденник натискань

Публічна структура файлів збігається з тим, що завантажують бінарники. Прохід реверс-інжинірингу підтвердив ту саму родину даних на Mac і Windows: ps.dat, тригери, транслітерацію, автозаміни, винятки, фільтри шляхів/заголовків і сховище щоденника.

Файл, у який варто вдивитися Функція «Щоденник» (Дневник) Punto записує кожне натискання у diary.dat — чати в месенджерах, пошукові запити, паролі відкритим текстом — а програма тримає власне мережеве з’єднання; саме тому незалежні огляди вважають її кейлогером, а деякі антивіруси позначають збірку як PUA. Не побічний ризик: це поставлена, задокументована функція.

Що Ghidra закрила остаточно: тег A

Файли .dat досі не є частиною Upyr, але ps.dat вже не просто загадковий blob. Windows-завантажувач біля FUN_0041ea80 XOR-декодує його через 0xaa, зберігає декодовані рядки, а потім подає їх у trie-подібний автомат. Матчери FUN_004dfec0 і FUN_004e02f0 викликають малий предикат тегів, FUN_004b16a0.

Саме перша гілка цього предиката вирішальна: якщо тег дорівнює A, повернути true. У Windows A — живий wildcard для будь-якої позиції у слові. У macOS рядковий парсер виставляє біт A, але перевірник його ніколи не читає. Ті самі дані, різна поведінка.

ЗбіркаФорма рушіяРезультат тега A
macOSбітові прапорці плюс prefix/suffix/substring перевіркипарситься в 0x20, ніколи не тестується → мертвий
Windowsскомпільований trie/автомат із предикатом тегів на вузлахreturn true → живий wildcard для будь-якої позиції

Саме цей висновок Upyr забрав у код. Патерни тригерів тепер підтримують word, word*, *word і *word*. Вбудована таблиця лишається точною; wildcard доступний, коли правилу справді потрібна Windows-поведінка Punto «збіг будь-де».

Upyr усе одно відтворює форму, а не пропрієтарний вміст: triggers.dat стає малою таблицею правил, написаною з нуля; ps.dat стає знаковим n-грамом на 2.8 MiB плюс невеликим стоп-списком; diary.dat не стає нічим. Реверс-інжиніринг відповів на семантику. Рушій, що постачається, лишається незалежним.

Ідея, що перекроїла дизайн

Моделюй фізичні клавіші, а не символи

Мій перший оцінювач ставив очевидне запитання: «на що ghbdsn більше схоже — на англійське чи на українське?» Це вже неправильне запитання, і чотири символи показали мені чому.

# на українській розкладці ці фізичні клавіші — ЛІТЕРИ, а не пунктуація [ ] ; ' \ , . / х ї ж є ґ б ю . # тож клавіші [ k s , — це не «дужка, дві літери, кома» — [ks, хліб (bread)

Прочитай рядок [ks, символ за символом — і побачиш дужку-літеру-літеру-кому: засмічений пунктуацією мотлох, який ти б нізащо не чіпав. Але це звичайне слово. Тому я перестав оцінювати літери на екрані й почав оцінювати клавіші під ними: Upyr відстежує фізичні позиції (ті самі назви, що браузер дає в KeyboardEvent.code) і питає модель про слово, яке ці клавіші утворили б в іншій розкладці. Саме ця зміна дає змогу [ks,хліб спрацювати, тоді як справжнє Jkmuf,Ольга, зберігає свою кому.

Висновки

Що сказав навмисно суворий бенчмарк

За поведінкою стежать два бенчмарки, і в обох списки слів заморожені через SHA-256 — тож числа відтворювані, а не «на око». Головні цифри:

1.000
точність на межовому корпусі зі 191 випадку
0
хибних виправлень на 20 000 «чистих» слів
173,964
записів знакового n-грама · ~2.8 MiB
12
детермінованих тригерів, стартовий набір

Висновок — розрив у повноті це вибір, а не межа

Точність (precision) лише каже, що я не ламаю добрий текст. Про повноту (recall) — як часто я справді виправляю зламаний — вона мовчить. Тож другий бенчмарк набирає 1 200 українських і 1 000 англійських слів на неправильній розкладці й проганяє реальний застосунок по них. Ось повнота за замовчуванням, розбита за довжиною слова:

10066330 0.0% 93.1% 98.5% ≤ 3 симв.4–6 симв.7+ симв. профіль за замовч. · українське слово на англ. розкладці · повнота, %

Рівні 0% на коротких словах — і це задум. За min_word_length = 4 найчастіші слова мови (не, як, на, що) не чіпаються взагалі. Це найбільша діра в повноті — і вона навмисна: у трьох символах замало сигналу, щоб подолати планку, поставлену досить високо, аби бути безпечною.

Модель сильна; консервативність — це політика

Перемкни на агресивні пороги — без жодної зміни моделі — і повнота підскакує. Тож та чверть українських слів, яку пропускає режим за замовчуванням, — це не стеля, а рішення «точність важливіша за повноту», яке можна викрутити.

10066330 75.1 92.3 94.3 99.4 УК → АНГАНГ → УК повнота, %, виправлення цілого слова · та сама модель, дві політики

92% / 99% лежать напоготові — щойно я вирішу, що ризик того вартий. Слова, які я «пропускав», були не стіною, об яку я вдарився, а налаштуванням, яке я сам обрав. Ручка, яку можна крутнути, щоразу краща за модель, яку довелося б перетреновувати.

І той, що переважив інтуїцію: більше даних зробило гірше

На старті я припускав: «більший корпус → краща модель». Історія каже інакше. Виграш дало налаштування ємності та квантування, а не обсяг.

90898887 89 88 89 90 100K наївний 1M +сила обраний 1M точні межові виправлення з 90 · ревізії моделі, зліва направо

Кинути в неї у 10× більше тексту зробило її гіршою. Я б цього нізащо не впіймав без фіксованого бенчмарку, проти якого можна міняти по одній речі за раз — і саме тому я написав бенчмарк перед «очевидним» апгрейдом, а не після того, як він мене вкусив. Окрема відкладена вибірка з 20 000 слів Вікіпедії (навмисно інший тип тексту, ніж тренувальні дані) тоді повернула 0 хибних виправлень — і виявила чотири справжні українські слова, що зіштовхнулися з англійськими; тепер вони закріплені як регресійні тести, щоб не могли повернутися.

Висновки → впровадження

Кожен висновок змінив конкретний рядок продукту

Бенчмарк, який не змінює код, — це просто гарне число. Кожен із цих таки змінив — ось коміт для кожного.

Що виявив бенчмаркЩо змінилося і де
Символьна модель не читає фізичні клавіші пунктуації як літери ([ks, = хліб) Оцінювання перейшло в простір фізичних клавіш, незалежний від розкладки; оцінювач міряє задуманий кандидат.fix: recognize mistyped Ukrainian bracket words · fix: honor Ukrainian physical punctuation keys
Власні назви з комою в кінці спотворювалися (Jkmuf,) Спершу оцінюється кандидат зі збереженою пунктуацією, потім звичайне перетворення.fix: handle names and terminal punctuation
Повнота за замовч. — рівні 0% на коротких словах, найчастіших у мові Шар детермінованих тригерів дає коротким певним послідовностям шлях в обхід порога впевненості моделі.perf + trigger layer + recall benchmark
Чиста вибірка виявила чотири колізії з українськими словами (зокрема дупу проти фізичного lege) Пом’якшення пунктуації зробили одностороннім; неоднозначні пари тепер вимагають додаткового пів-запасу. Усі чотири закріплені як регресійні випадки.perf + trigger layer + recall benchmark
Наївний корпус у 10× дав гірший результат (88/90) Відбір налаштував ємність + силу квантування, а не обсяг — 1M / 1.75× / 32 дав 90/90 без жодної помилки на чистій вибірці.feat: train compact language n-gram model

Чому ця реалізація краща — і в чому саме

Не більше функцій. Більше того, що можна перевірити.

Скажу чесно: Punto — двадцятирічний продукт, який уміє куди більше за це, і він випустив цей трюк задовго до мене. Я роблю вузьке твердження: для завдання «англійська ↔ українська», за рядками нижче, моя версія сильніша. І кожну клітинку Upyr можна піти й перевірити самому — у цьому вся суть.

ВимірPunto SwitcherUpyr
МовиНасамперед російськаEN ↔ UK повноцінно, зокрема український ряд пунктуації [];'\,./
ПлатформиWindows і macOS-збірки; пропрієтарні рушії з різною семантикою правилmacOS основна; Windows / Linux у превʼю; і той самий рушій у браузері через WASM
Приватність під час роботиЗакрита; постачає «щоденник» натискань, що записує набране й вивантажує на ЯндексЖодної телеметрії, жодного HTTP-клієнта в застосунку — це стереже check_privacy.py у CI
Безпека памʼятіНативна, доба до RustRust наскрізь
Докази коректностіНе опублікованіТочність 1.000 на 191 межовому випадку; 0 / 20 000 на чистій вибірці; корпуси закріплені через SHA-256, відтворюються однією командою
Ланцюг постачанняПропрієтарний бінарникMIT; CycloneDX SBOM, безключові підписи Cosign, SHA256SUMS, атестації походження
МодельНепрозораЗнаковий n-грам на 173,964 записи, ~2.8 MiB, пошук двійковим поділом, повністю задокументований

Чесні межі: це «краще за цими осями для цієї пари мов», а не «кращий продукт». Широта функцій, охоплення мов і два десятиліття загартовування на крайніх випадках усе ще належать Punto.

Що це таке, а що ні

Межі — так, як їх формулює сам бенчмарк

  • Кожен результат — це детермінована вибірка з корпусу, а не твердження про реальну частоту. «0 хибних виправлень на 20 000 слів» — сильний сигнал безпеки, а не обіцянка щодо твого справжнього набору.
  • Межовий корпус оцінює рішення на одній межі слова (пробіл). Клавіші навігації, зміни розкладки посеред слова, момент виправлення й скидання трекера — це окремий, ще не збудований шар бенчмарку.
  • Повнота за замовчуванням навмисно неповна. Короткі часті слова пропускаються, доки не додано словник або менш консервативне значення за замовчуванням.
  • Поточне завантаження превʼю ще не нотаризоване Apple; бандл macOS підписаний ad-hoc, тож Gatekeeper може його заблокувати.
Що я з цього виніс Розбирати варто було не карту клавіатури — вона публічна й ніколи не змінюється. Варто було — судження: жорсткі правила для того, у чому певен, обережна модель для всього іншого, обидва працюють на фізичних клавішах, а не на літерах на екрані, і бенчмарк, суворий настільки, щоб двічі довести мою неправоту. Punto випустило все це двадцять років тому й ніколи не мусило пояснювати. Складаючи це назад руками, найбільше мене здивувало, скільки роботи — це навчити програму, коли тримати руки при собі.