Я не пишу код каждый день уже много лет, последний продакшен на PHP отгрузил году в 2009. Но за последние годы инструменты дошли до состояния, когда сольный pet‑проект с распознаванием речи на устройстве собирается силами одного человека. Эта статья про то, как я сделал голосовой дневник мыслей для когнитивно‑поведенческой терапии (КПТ), почему распознавание речи у меня крутится прямо на телефоне, и какие на этом пути были технические развилки. Кода почти не будет, будет архитектура и обоснование решений.
Сразу дисклеймер про мотивацию, потому что без него непонятно, зачем вообще городить on‑device. Я сам прошёл через тревожные расстройства, панические атаки и несколько депрессивных периодов. Из всего, что мне помогало, переломной стала КПТ, и у неё есть домашняя часть, дневник мыслей, который нужно вести между сессиями. Вести его текстом в момент тревоги у меня не получалось годами, и в какой‑то момент я понял, что хочу диктовать его голосом. Так появился проект, который я тут и разбираю.
Почему текстовый дневник мыслей — это трение
Дневник мыслей, или thought record, это короткая структурированная запись: ситуация, автоматическая мысль, эмоция и её сила, реакция, альтернативная мысль. Пять полей. На бумаге или в заметке это выглядит безобидно, но заполнять пять текстовых полей нужно в тот момент, когда вас накрывает, в очереди, в транспорте, ночью. Именно тогда, когда меньше всего хочется доставать клавиатуру и формулировать.
С точки зрения разработчика это классическая задача про снижение трения во вводе. Любая форма из пяти полей с клавиатурным вводом в стрессовом состоянии имеет конверсию близкую к нулю. Я это проверил на себе, заводил таблицы и заметки, и почти не заполнял их. А ценность КПТ‑дневника прямо пропорциональна тому, насколько регулярно вы его ведёте, так что пустой дневник обнуляет половину пользы терапии.
Очевидный способ убрать трение во вводе это голос. Проговорить ситуацию и мысль вслух занимает секунд двадцать и не требует собранности. Дальше нужно только распознать речь и разложить её по полям дневника. Вот тут и начинается интересное.
Почему распознавание должно быть на устройстве
Самый простой путь это взять облачное API распознавания речи. Отправил аудио на сервер, получил текст, дёшево и быстро. Для большинства приложений это правильный выбор. Для дневника мыслей в КПТ, на мой взгляд, нет.
Записи в таком дневнике это самое уязвимое, что у человека есть: панические мысли, страхи, то, что он не скажет вслух никому. Отправлять это на чужие серверы означает создавать базу интимных аудиозаписей где‑то в облаке, с логами, бэкапами и теоретической возможностью утечки или запроса. Я не хотел брать на себя такую ответственность, и как пользователь сам бы такому приложению не доверял.
Отсюда требование, которое определило весь стек: распознавание речи целиком работает на телефоне, аудио никуда не уходит, текст хранится в локальной базе на устройстве, облачной синхронизации нет. Приватность тут не маркетинговый буллит, а архитектурное ограничение, из которого растёт всё остальное.
Стек
Базовый выбор получился такой:
— Flutter и Dart как кросс-платформенный UI-слой, один код на iOS и Android.
— Whisper.cpp для распознавания речи прямо на устройстве, без сети.
— Локальная SQLite-база для хранения записей дневника.
Flutter я взял по банальной причине: один человек не потянет две нативные кодовые базы, а мне нужны были и iOS, и Android. Dart после многолетнего перерыва в разработке зашёл неожиданно легко, а горячая перезагрузка для сольной работы экономит огромное количество времени. UI дневника, графики динамики тревоги, экраны записи, всё это живёт во Flutter.
Состояние и зависимости в приложении держит Riverpod, а локальное хранилище это SQLite через пакет sqflite. Самая интересная часть это распознавание. Whisper.cpp это порт модели OpenAI Whisper на чистый C и C++, который запускается локально на телефоне, десктопе или сервере без Python-зависимостей. Для мобильного приложения это идеальный кандидат: компактный, без рантайма, работает офлайн. К Flutter он подключается через нативный слой, и сама речь распознаётся на устройстве, не покидая телефон.
Хранение я сделал на локальной SQLite-базе. Каждая запись дневника, оценки тревоги до и после, разбор по полям, всё лежит в одном файле на устройстве. Облачной синхронизации нет, и это сознательное ограничение по дизайну, я не считаю отсутствие синка недоработкой. Экспорт для терапевта реализован отдельно: отчёт за выбранный период собирается на лету и шифруется AES-256-GCM перед отправкой по почте, так что данные защищены и в этом единственном сценарии, когда они вообще покидают телефон.
Какую модель Whisper выбрать на телефон
Вот тут пришлось искать баланс между размером, скоростью и качеством. У Whisper есть линейка моделей от tiny до large, и в формате ggml их размеры заметно разнятся: tiny около 75 МБ, base около 142 МБ, small около 466 МБ в полном виде. Для телефона полная small великовата, а base по качеству на сложной речи уже спотыкается.
Спасает квантизация. Whisper.cpp поддерживает квантованные веса (Q5_0, Q5_1, Q8_0 и другие), которые сильно уменьшают размер модели и потребление памяти при небольшой потере качества. Small в квантизации Q5_1 весит около 182 МБ против исходных 466 МБ, и качество для распознавания спокойной диктовки остаётся достаточным. Такой вариант я и взял в работу, конкретный файл ggml‑small‑q5_1.bin примерно на 181 МБ.
Качается модель отдельной загрузкой на онбординге, через экран ModelDownloadScreen, без зашивания в установщик. Класть 181 МБ в бандл приложения означает раздувать вес из стора и заставлять каждого пользователя тащить модель, даже если он передумает на онбординге. После загрузки файл лежит в служебном каталоге приложения, в Library на iOS и в Application Support на Android, и дальше телефону сеть для распознавания уже не нужна никогда.
Как аудио доезжает до модели
Whisper ожидает на входе моно‑аудио в 16 кГц, и чтобы не возиться с ресемплингом постфактум, я пишу звук сразу в нужном формате: WAV, 16 кГц, моно, PCM 16 бит. На iOS запись идёт через пакет record, на Android через нативный рекордер на базе AudioRecord, подключённый отдельным Platform Channel native_audio_recorder. На выходе всегда один готовый файл в правильном формате, никакой конвертации перед распознаванием не требуется.
Мост между Flutter и нативным распознаванием сделан на Platform Channels, без FFI. Через MethodChannel whisper_method_channel идут команды initializeModel, transcribeSample и isModelLoaded, а через EventChannel whisper_events возвращаются события и результат. Нативная реализация под платформы разная: на iOS это Swift с ускорением на Metal GPU, whisper собран в whisper.xcframework из исходников whisper.cpp; на Android это Kotlin и JNI поверх libwhisper.so из AAR‑зависимости плюс своя libwhisper‑params.so для параметров. Сам WhisperService инициализируется один раз в фоне на старте приложения и дальше просто ждёт запросов.
Распознавание идёт по сохранённому файлу, а живой поток с микрофона я не использую. Сначала пользователь одной непрерывной записью наговаривает ответы на все пять вопросов дневника, и только потом этот WAV уходит в Whisper. Конфигурация распознавания лежит в whisper.yaml: beam_size 5, temperature 0 для предсказуемости, четыре потока, и главное, включён word_timestamps, отметки времени по словам.
Качество на разных языках и разбор по полям
Whisper мультиязычен из коробки, и это закрыло сразу большую задачу. Приложение поддерживает семь языков: английский, русский, испанский, французский, португальский, немецкий и итальянский. Одна и та же модель распознаёт все семь, отдельные движки под каждый язык не нужны, что для сольного проекта принципиально.
После распознавания одну непрерывную запись нужно разложить по пяти полям дневника, и вот тут самая интересная инженерная часть. Человек наговаривает всё подряд, ситуацию, мысль, эмоцию, реакцию, альтернативу, а на выходе должно получиться пять аккуратных фрагментов. Я решил это через те самые word_timestamps от Whisper: модель отдаёт не просто текст, а каждое слово с отметкой времени, и отдельный сервис нарезки сопоставляет границы ответов с этими отметками, раскладывая один WAV на пять кусков по вопросам. Никакой угадайки по смыслу, привязка идёт к таймингам слов, что на порядок надёжнее.
Поверх готового текста работает разбор когнитивных искажений. Он опирается на ключевые слова и распознаёт десять типов искажений, катастрофизацию, чтение мыслей, чёрно‑белое мышление и другие. Поскольку разбор работает по уже распознанному тексту, он одинаково ложится на все семь языков, без привязки к язык‑специфичным аудиомоделям.
Производительность ожидаемо упирается в железо телефона: на свежих устройствах распознавание ощущается почти мгновенным, на старых заметна задержка в несколько секунд. Записи дневника короткие, так что даже на бюджетных телефонах это остаётся в рамках терпимого ожидания. Будь это распознавание получасового монолога целиком, on‑device был бы спорным выбором, но для коротких записей он отлично ложится.
Что получилось
В сумме вышло приложение, где вы нажимаете запись, проговариваете шаги дневника мыслей вслух, а текст распознаётся и раскладывается по полям прямо на телефоне, без единого обращения к серверу. Получился голосовой КПТ‑дневник Mentalium, который снимает то самое трение во вводе, из‑за которого я годами забрасывал текстовый дневник.
Главный технический вывод для меня такой: on‑device‑распознавание перестало быть экзотикой, доступной только большим командам. Квантованная Whisper‑модель на пару сотен мегабайт, нативный мост во Flutter и локальная база дают полностью офлайновый продукт, который один человек собирает и поддерживает. А для чувствительных данных вроде дневника мыслей это не просто приятный бонус, а единственно честная архитектура.
И отдельно, не как разработчик, а как человек, который через это прошёл: тревога и депрессия лечатся, большинство людей возвращаются к нормальной жизни, а КПТ один из самых изученных способов туда вернуться. Инструмент, который помогает вести её домашнюю часть регулярно, делает терапию сильнее, и ради этого стоило повозиться с квантизацией.
Дисклеймер
Я не врач. Mentalium это инструмент самопомощи, у него нет статуса медицинского изделия, он не предназначен для диагностики или лечения каких‑либо состояний и не заменяет работу со специалистом. Если вы подозреваете у себя тревожное расстройство или депрессию, запишитесь к психиатру или к врачу‑психотерапевту.
ссылка на оригинал статьи https://habr.com/ru/articles/1043432/