В апреле выпустил в RuStore Android-приложение «Не пиши голосовое!». Наговорил в микрофон, получил расшифровку. Всё прямо на телефоне: ни облака, ни аккаунта, ни интернета. Голосовые часто содержат чувствительные вещи, и отправлять их на чужие серверы — так себе идея.
Первую версию собрал за два дня на Expo SDK 54 и React Native 0.81. Прожила месяц. За это время подкрутил чанкинг, поймал пару утечек памяти на долгих записях, повоевал с JS-мостом и таймерами. Стало понятно: дальше пилить надстройку поверх RN значит каждый раз воевать с прокладкой между приложением и микрофоном.
Перевёл всё на Kotlin. Главный довод — прямой доступ к AudioRecord и foreground-сервису без RN-моста. Поток PCM-байтов идёт от микрофона сразу в sherpa-onnx, без сериализации через JS. Меньше слоёв, меньше мест, где течёт память. Заодно проще тестировать: Robolectric поднимает Android-окружение без эмулятора, тесты бегут на CI в считаные минуты. И инструменты: Android Studio с профилировщиком честнее показывает, на что уходит время и память, чем Flipper с RN.
Стек: Kotlin 2.2.21, Jetpack Compose с Material 3, Hilt для внедрения зависимостей, Room для базы заметок, Coroutines и StateFlow для состояния. Запись идёт через нативный AudioRecord, 16 кГц mono PCM16. Навигация на Navigation Compose. Аналитика на AppMetrica 8.2.0, события собираю анонимно: запуск записи, скачивание модели, ошибки распознавания. Поддерживает Android 7 и выше, тестировал в основном на Xiaomi Poco M5S.
Сердце то же: sherpa-onnx 1.13.2 и GigaAM v3 e2e CTC (int8). Модель Сбера весит около 320 МБ, качается один раз, дальше работает офлайн. На русском она примерно в 2,5 раза точнее Whisper-large-v3. Длинные записи режу на чанки по 22–25 секунд и склеиваю результат. Поверх движка повесил VAD, чтобы не гонять распознавание по тишине. Базу со старыми записями подтягиваю из RN-версии при первом запуске — пользователю не нужно ничего экспортировать руками.
Что прокачалось по сравнению с RN-версией. Тестов было около шести на Jest, стало 295 на JUnit, Robolectric и Compose UI. Настройки переехали с AsyncStorage на Preferences DataStore: теперь ключи типизированы на этапе компиляции, а не подбираются строкой в рантайме. Темизацию Light, Dark и System собрал на CompositionLocal и Material 3 одновременно, чтобы и моя палитра, и системные диалоги выглядели одинаково. Foreground-сервис теперь явный, со своим каналом уведомлений и обработкой отмены. Часовая запись больше не падает, когда телефон уходит в спящий режим.
Интерфейс остался прежним: список заметок, плавающая кнопка записи, live-волнограмма из 44 баров в такт громкости, свайп влево удаляет. Свайпы и пульсация теперь сделаны нативно через Compose-анимации вместо Reanimated. Темы переключаются прямо в настройках, включая режим System: приложение слушает системную тему и подхватывает изменение без перезапуска. Лендинг negolosom.ru не трогал — одностраничник с кнопками в RuStore и к контактам, он и так делал работу.
Технологии: Kotlin 2.2.21, Jetpack Compose, Material 3, Hilt, Room, Kotlin Coroutines, sherpa-onnx 1.13.2, GigaAM v3 (NeMo CTC, INT8), AppMetrica 8.2.0
negolosom.ru