Нотатки з поля · три дні роботи
Як я повертав до життя мертвий текст
Чиста реалізація найстарішого трюку Punto Switcher — спершу відтворена з поведінки й бенчмарків, а потім звірена з реальними бінарниками, щоб закрити недокументовані теги правил.
Проблема
Правильні клавіші, але не та розкладка
Ти сідаєш написати привіт, українська розкладка не ввімкнена — і клавіші виходять як ghbdsn. Або хочеш hello, а отримуєш руддщ. Якщо ти пишеш двома мовами, це трапляється сто разів на день, і щоразу треба зупинитися, стерти, перемкнути, набрати заново.
Punto Switcher — багаторічний інструмент Яндекса — зробив собі ім’я саме на тому, що виправляє це за тебе: стежить за клавішами, впізнає абракадабру, тихо перенабирає її в іншій розкладці. Я хотів те саме для української — але без інструмента, який іде в комплекті. Тож зробив Upyr: упир — це той, хто повернувся з мертвих, а це приблизно те, що застосунок робить із твоїм понівеченим текстом. Rust, чиста реалізація, жодного рядка спільного з Punto.
Поміняти літери — це легка частина: QWERTY та ЙЦУКЕН — це фіксовані позиційні карти, пошук на п’ятдесят рядків. Складне — зрозуміти, коли їх міняти. Кожне завершене слово — це «так» чи «ні»: лишити чи переписати? Перепишеш слово, яке людина справді мала на увазі, — і ти щойно спотворив її текст. Саме через це видаляють застосунки. Пропущене виправлення — це знизане плече; хибне виправлення — це зрада. Тому весь дизайн схиляється до одного правила: сумніваєшся — не роби нічого.
Як воно вийшло
Три дні: від першого релізу до підписаної v0.2.0
Уся історія — просто в логу комітів. Кожен вузол нижче — реальний коміт, а гранатові — це моменти, коли щось клацнуло, або коли бенчмарк упіймав мене на помилці.
- Initial Upyr release — застосунок у треї, зміна розкладки, обв’язка доступності macOS
- feat: train compact language n-gram model — з’являється статистичний оцінювач
- fix: recognize mistyped Ukrainian bracket words
момент, коли дизайн повертає: [ks, — це не пунктуація, це хліб - fix: honor Ukrainian physical punctuation keys
оцінювання переходить у простір фізичних клавіш, незалежний від розкладки
- 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
- perf + trigger layer + recall benchmark
висновки впроваджено: шар тригерів для сліпої зони коротких слів, правила політики для колізій, які знайшла відкладена вибірка - ci(security): scanners, SBOM, and Cosign signing — CycloneDX SBOM, безключові підписи → v0.2.0
Дизайн, відтворений із поведінки
Рішення у два шари — бо один шар не буває водночас і безпечним, і повним
Punto ніде не каже, як воно вирішує, тож перша версія Upyr дивилася на поведінку — клавіші на вході, виправлення на виході — доки крізь це не проступила форма. Пізніше radare2 і Ghidra підтвердили глибший поділ: Mac-збірка Punto має бітові прапорці та рядкові перевірки, а Windows-збірка ходить скомпільованим автоматом правил. Upyr бере переносний висновок: коротка таблиця жорстких правил має перше слово й може скасувати будь-що; усе інше йде до статистичної моделі, яка радо каже «не знаю» й мовчить.
Два шари роблять різну роботу. Модель обережна й не вгадуватиме. Правила — для тієї жменьки випадків, у яких я вже впевнений, зокрема для коротких частих слів, які модель навмисно пропускає.
Чим було розбирання — а чим ні
Що насправді всередині 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, тригери, транслітерацію, автозаміни, винятки, фільтри шляхів/заголовків і сховище щоденника.
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 більше схоже — на англійське чи на українське?» Це вже неправильне запитання, і чотири символи показали мені чому.
Прочитай рядок [ks, символ за символом — і побачиш дужку-літеру-літеру-кому: засмічений пунктуацією мотлох, який ти б нізащо не чіпав. Але це звичайне слово. Тому я перестав оцінювати літери на екрані й почав оцінювати клавіші під ними: Upyr відстежує фізичні позиції (ті самі назви, що браузер дає в KeyboardEvent.code) і питає модель про слово, яке ці клавіші утворили б в іншій розкладці. Саме ця зміна дає змогу [ks, → хліб спрацювати, тоді як справжнє Jkmuf, → Ольга, зберігає свою кому.
Висновки
Що сказав навмисно суворий бенчмарк
За поведінкою стежать два бенчмарки, і в обох списки слів заморожені через SHA-256 — тож числа відтворювані, а не «на око». Головні цифри:
Висновок — розрив у повноті це вибір, а не межа
Точність (precision) лише каже, що я не ламаю добрий текст. Про повноту (recall) — як часто я справді виправляю зламаний — вона мовчить. Тож другий бенчмарк набирає 1 200 українських і 1 000 англійських слів на неправильній розкладці й проганяє реальний застосунок по них. Ось повнота за замовчуванням, розбита за довжиною слова:
Рівні 0% на коротких словах — і це задум. За min_word_length = 4 найчастіші слова мови (не, як, на, що) не чіпаються взагалі. Це найбільша діра в повноті — і вона навмисна: у трьох символах замало сигналу, щоб подолати планку, поставлену досить високо, аби бути безпечною.
Модель сильна; консервативність — це політика
Перемкни на агресивні пороги — без жодної зміни моделі — і повнота підскакує. Тож та чверть українських слів, яку пропускає режим за замовчуванням, — це не стеля, а рішення «точність важливіша за повноту», яке можна викрутити.
92% / 99% лежать напоготові — щойно я вирішу, що ризик того вартий. Слова, які я «пропускав», були не стіною, об яку я вдарився, а налаштуванням, яке я сам обрав. Ручка, яку можна крутнути, щоразу краща за модель, яку довелося б перетреновувати.
І той, що переважив інтуїцію: більше даних зробило гірше
На старті я припускав: «більший корпус → краща модель». Історія каже інакше. Виграш дало налаштування ємності та квантування, а не обсяг.
Кинути в неї у 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 Switcher | Upyr |
|---|---|---|
| Мови | Насамперед російська | EN ↔ UK повноцінно, зокрема український ряд пунктуації [];'\,./ |
| Платформи | Windows і macOS-збірки; пропрієтарні рушії з різною семантикою правил | macOS основна; Windows / Linux у превʼю; і той самий рушій у браузері через WASM |
| Приватність під час роботи | Закрита; постачає «щоденник» натискань, що записує набране й вивантажує на Яндекс | Жодної телеметрії, жодного HTTP-клієнта в застосунку — це стереже check_privacy.py у CI |
| Безпека памʼяті | Нативна, доба до Rust | Rust наскрізь |
| Докази коректності | Не опубліковані | Точність 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 може його заблокувати.