Я сделал RightLayout потому что все корректоры раскладки на macOS, которые я пробовал, ломались на именах, коде и опечатках. Ставка была небольшая: обучить CoreML-модель с нуля, три раскладки, на устройстве. Сработало. А потом пришёл счёт за поддержку, и я открыл исходники.
1. Что не так со словарными punto-switcher
Если вы печатаете на двух или трёх языках на Mac, вы это переживали. Начали предложение по-английски, раскладка всё ещё на русском, экран наполняется кириллической бессмыслицей. Лекарство в теории есть. Существуют утилиты, которые следят за вводом и переключают раскладку, когда слово «выглядит неправильно».
Классическая версия такой утилиты словарная. Она проверяет каждое слово по фиксированному словарю и исправляет, если слова там нет. Для простых случаев это работает. И ломается в ту же секунду, как живой человек начинает печатать живой текст.
Имена ломают. Код ломает. Аббревиатуры ломают. URL ломают. Слова kubectl нет ни в одном русском словаре, но это и не неправильно набранное английское слово. Опечатки вроде helo тоже нет в словаре, и утилита заботливо превращает её в руды. А в смешанных по языку абзацах словарь вообще не понимает, к чему привязываться.
Коммерческую версию этой утилиты под Mac выпускает команда, которую я уважаю. Кажется, около десяти человек. Продукт крепкий, шлифованный, и он всё ещё ловит тот же класс ложных срабатываний, потому что под капотом у него нет понимания того, что вы печатаете. Только проверка по словарю.
Мне нужно было что-то, что читает контекст.
2. Ставка
Ставка была достаточно маленькая, чтобы попробовать за несколько уикендов. Обучить крошечную character-level модель, которая берёт короткое окно недавнего ввода и предсказывает один из девяти классов. Девять классов это три родные раскладки (EN, RU, HE) плюс шесть кросс-раскладочных промахов: en_from_ru, ru_from_en, en_from_he, he_from_en, ru_from_he, he_from_ru. Этот набор классов и есть весь трюк. Как только модель говорит «это похоже на русский, набранный на английской раскладке», детерминированный mapper делает фактическую подстановку символов.
Тренировочный пайплайн лежит в репо и выглядит обыденно. Корпуса Wikipedia и субтитров для трёх языков, генерация чистых пар и пар с кросс-раскладочными опечатками, character-level токенизация, аугментация под опечатки и шум регистра, mixup, label smoothing. Сама модель это ансамбль из небольшого мульти-масштабного CNN и четырёхслойного character-Transformer, оба пулятся в одну линейную голову. Работает на фиксированном окне в 20 символов. Экспорт идёт через PyTorch в CoreML.
# из Tools/CoreMLTrainer/train.py
CLASSES = [
'ru', 'en', 'he',
'ru_from_en', 'he_from_en',
'en_from_ru', 'en_from_he',
'he_from_ru', 'ru_from_he'
]CoreML-модель, которая едет внутри .pkg, весит около 14 МБ. Работает полностью на устройстве. Инференс по окну достаточно быстрый, чтобы вся логика коррекции укладывалась в бюджет 50 мс, который я задал для всего пайплайна (от event tap до замены текста). Достаточно маленькая, чтобы лежать в бандле и не зависеть от облака.
В первый раз, когда она правильно превратила ghbdtn в «привет» в середине предложения с куском кода, я понял, что это сработает. Словарная утилита, которой я пользовался годами, съела бы кусок кода.
3. Где модель реально выигрывает
Три места, где она чисто бьёт словарь.
Опечатки. Короткое слово с одним пропущенным или продублированным символом всё ещё узнаётся как нужный язык на character-level модели. Словарные утилиты либо тихо пропускают слово, либо, что хуже, «исправляют» его в бессмыслицу.
Имена и код. Модель видела достаточно смешанного по языку и скриптам текста на тренировке, чтобы английский фрагмент внутри русского предложения не запускал переключение. Словарный аналог этого, вручную поддерживаемый whitelist, который растёт бесконечно.
Иврит, который реально сложный. RTL-текст плюс набор символов без пересечения с латиницей и кириллицей плюс раскладка, которая мапит еврейские буквы на английские клавиши, означают, что словарный подход должен поддерживать три попарных таблицы и контекстную эвристику сверху. Модель просто выучила, что akuo это «שלום», набранное на английской раскладке, и едет дальше.
Три-четыре месяца я каждый день пользовался собственным инструментом. Это был первый случай, когда корректор был достаточно невидимым, чтобы про него забыть.
4. Почему я открыл исходники, а не масштабировал
Потом пришёл счёт за поддержку.
У бесплатной macOS-утилиты с обученной моделью есть длинный хвост неромантичной работы. Event tap через accessibility-API должен продолжать работать на новых версиях macOS. Apple обожает молча менять семантику пермишенов между релизами. CoreML runtime дрейфует. У модели нет тестовой инфраструктуры под «реальные пользователи печатают реальный текст», потому что это по определению нет в трейне. Цикл обучения по undo-ratio, где утилита смотрит на отмены коррекций и адаптируется, тяжело сделать безопасным и ещё тяжелее провалидировать без телеметрии, которую я отказываюсь собирать.
Для платного продукта с десятью инженерами эти издержки впитываемы. Для бесплатного инструмента, который поддерживает один человек с дневной работой, они складываются. Каждый мажорный релиз macOS становился неделей вечернего дебага. Каждый bump CoreML был маленьким риском. Каждое issue в GitHub было развилкой: становлюсь ли я Mac-системным инженером в свободное время, или я даю проекту тихо гнить, делая вид, что он всё ещё поддерживается.
Я выбрал третий вариант. Пометил проект как community-maintained, написал честный баннер на странице и в README, оставил модель в бандле, чтобы установка по-прежнему работала, и переключил внимание на Bernstein. Репо публичное. Тренировочный пайплайн публичный. Pull-реквесты ревьюятся. Никаких gatekeeper'ов. Шлёшь хорошие PR, получаешь commit access.
Это более честная позиция, чем «v2 скоро, следите за обновлениями».
5. Что вы можете забрать
Хотите инструмент. .pkg лежит на странице релизов. macOS 13 или новее, разрешение Accessibility, бесплатно. Модель внутри бандла.
Хотите код. репо небольшое, архитектурный документ в .sdd/, тренировочный пайплайн в Tools/CoreMLTrainer/. Добавить четвёртый язык это упражнение на несколько часов: расширить class enum, добавить layout map, переобучить, выкатить.
Хотите урок. Он короткий. Соло-разработчик может обогнать по доставке маленькую команду на сфокусированном продукте, потому что команда несёт координационные накладные расходы, которых у соло нет. Тот же соло-разработчик не может пересидеть маленькую команду по поддержке, потому что поддержка это координационная задача и шорткатов в ней нет. Выбирайте соответственно.
Я искренне рад, что он на воле. Берите, чините, выпускайте.