Traverba: Flutter-приложение с 3 ASR-моделями, локальным LLM и камерным OCR — всё на устройстве

Я создал Traverba — переводчик реального времени, который работает на 100% на вашем телефоне без каких-либо облачных API. Распознавание речи, перевод на 108 языков, камерный OCR на 92 письменностях, наложение перевода поверх экрана, транскрипция файлов и офлайн-групповой чат через Bluetooth для до 7 человек.

Всё приложение построено на Flutter с нативным платформенным кодом для ресурсоёмких задач. Вот что умеет приложение, почему Flutter оказался правильным выбором и с какими главными трудностями я столкнулся при выпуске на iOS и Android.

Что умеет приложение

Traverba включает пять основных функций, все работают офлайн:

Живой голосовой перевод — говорите на одном языке, слушайте и читайте перевод на другом. Три режима: внутри приложения, системный захват аудио через плавающее окно (переводит звук из Zoom/Teams/Meet) и плавающий микрофон. Автоматическое воспроизведение перевода на целевом языке.

Перевод по камере — наведите камеру на меню, вывеску, форму или документ. Два OCR-движка: режим Basic (ML Kit) для оперативного сканирования и режим Professional (PaddleOCR) для сложных письменностей — CJK, арабского, кириллицы и деванагари. Поддерживает 92 языка. Приложение заменяет иностранный текст переводом прямо на изображении.

Перевод экрана — плавающее наложение, которое переводит текст в любом приложении. Смотрите аниме, читаете корейские новости, играете в японскую игру или получили сообщение в WeChat? Нажмите кнопку наложения — текст переводится на месте, без переключения приложений.

Транскрипция файлов — импортируйте аудио или видеофайлы (mp3, m4a, wav, mp4) и получайте транскрипт с временными метками: исходный текст рядом с переводом.

Офлайн-групповой чат — до 7 человек подключаются через Bluetooth-сеть. Каждый говорит на своём языке. Каждое сообщение переводится на язык всех участников в реальном времени. Без интернета. Без сервера. Каждый телефон самостоятельно выполняет ASR и перевод.

Всё это работает локально на процессоре телефона. Никаких API-ключей, серверных расходов и передачи данных за пределы устройства.

Почему Flutter

В самом начале я оценивал Flutter, React Native и полностью нативный подход (отдельные кодовые базы на Swift и Kotlin).

Кроссплатформенность из единой кодовой базы была обязательным условием. Один разработчик, поддерживающий две нативные кодовые базы для столь сложного приложения — нереалистичный сценарий. UI-слой на Dart — настройки, чат-пузыри, экраны транскриптов, обучающий тур, листы загрузки, выбор языков — составляет примерно 60% всего кода. Писать это дважды означало удвоить сроки разработки.

Система платформенных каналов Flutter сделала нативную интеграцию практичной. Вся ресурсоёмкая работа (ASR-инференс, LLM-перевод, OCR, Bluetooth-сеть, плавающие наложения) выполняется в нативном Swift/Kotlin через платформенные и методные каналы. Flutter не пытается стать AI-рантаймом — он выступает UI-слоем и оркестрационным слоем, соединяющим нативные движки.

Производительность оказалась достаточной. Главное опасение при использовании Flutter для подобного приложения — накладные расходы: каждая миллисекунда на счету, когда вы выстраиваете цепочку ASR → перевод → TTS в разговоре. На практике Flutter-слой добавляет ничтожную задержку, поскольку тяжёлые вычисления происходят в нативном коде. Dart-слой управляет состоянием, маршрутизацией и обновлением UI — и справляется с этим хорошо.

Горячая перезагрузка кардинально ускорила итерации UI. При наличии пяти различных интерфейсных поверхностей (голос, камера, экран, файл, групповой чат), каждая с множеством состояний и режимов, возможность итерировать UI без пересборки стала существенным ускорителем разработки.

Архитектура: Flutter как оркестратор

Приложение следует чёткому разделению ответственности:

Flutter/Dart-слой отвечает за:

  • Весь UI (Material 3, адаптивные раскладки для телефонов и планшетов)
  • Управление состоянием и маршрутизацию функций
  • Выбор языка/локали и пользовательские настройки
  • Управление загрузкой дополнительных моделей
  • Монетную экономику и управление подпиской
  • Обучающий тур при онбординге

Нативный слой отвечает за (через платформенные каналы):

  • ASR-инференс (sherpa-onnx / whisper.cpp)
  • LLM-перевод (Gemma через нативный рантайм на устройстве)
  • Классический ML-перевод (NMT-модели в формате ONNX)
  • Камерный OCR (ML Kit + PaddleOCR)
  • Синтез речи
  • Bluetooth-сеть для группового чата
  • Плавающие наложения (перевод экрана + захват системного аудио)
  • Управление памятью и загрузкой/выгрузкой моделей

Мост между этими слоями — набор платформенных каналов с чётко определёнными контрактами. Dart-сторона отправляет команды («запустить ASR для кантонского», «перевести этот текст с японского на английский», «захватить экран и выполнить OCR»). Нативная сторона возвращает результаты асинхронно.

Три ASR-модели, автоматическая маршрутизация

Ни одна модель распознавания речи не справляется одинаково хорошо со всеми языками. Вместо того чтобы мириться с низким качеством для части языков, приложение включает три специализированные модели:

  • Parakeet — оптимизирована для английского. Поставляется с приложением для мгновенного ASR на английском без загрузки.
  • Whisper.cpp — широкое многоязычное покрытие на 30+ языках.
  • Qwen3-ASR — оптимизирована для CJK с отдельной поддержкой кантонского.

Когда пользователь выбирает исходный язык, маршрутизационный слой на Dart выбирает лучшую модель и сообщает нативной стороне, какой движок активировать. Пользователь не видит и не управляет выбором модели.

Сложность — в управлении памятью. Эти модели занимают от 500 МБ до 1,9 ГБ в ОЗУ. Одновременная загрузка всех трёх привела бы к краху большинства телефонов. Планировщик памяти (написан на Dart, запрашивает нативную статистику памяти) отслеживает загруженные модели и вытесняет наименее недавно использованную при необходимости загрузить новую. Хранилище предпочтений бэкенда запоминает, какой вычислительный бэкенд (NPU, GPU, CPU) сработал на конкретном устройстве, чтобы при последующих запусках пропускать неподходящие бэкенды.

108 языков через два уровня перевода

Перевод осуществляется по двум путям:

Уровень 1 — классические NMT-модели (61 язык): быстрые, малопотребляющие, высокое качество для хорошо обеспеченных языковых пар. Модели ONNX, работающие через sherpa-onnx на обеих платформах.

Уровень 2 — локальный LLM Gemma (47 дополнительных языков): для языков без выделенных NMT-моделей (йоруба, амхарский, лаосский, бирманский и другие) локально работает квантизованная модель Gemma 2B. Медленнее первого уровня, но расширяет охват на языки, которые традиционные модели обслуживают плохо.

Dart-слой управляет выбором уровня в зависимости от языковой пары. Пользователь видит единый интерфейс.

Камерный OCR: двойной движок, 92 языка

Перевод через камеру использует два движка по выбору пользователя:

Режим Basic использует нативное распознавание текста Google ML Kit. Быстрый, хорошо справляется с латиницей, кириллицей и CJK. Подходит для использования в режиме видоискателя реального времени.

Режим Professional использует PaddleOCR на устройстве. Более высокая точность для сложных раскладок, смешанных письменностей, изогнутого текста и низкого контраста. Требует монет (5 монет за снимок), так как более вычислительно затратен.

Один урок, усвоенный на горьком опыте: iOS по умолчанию поставляется только с моделью латинского OCR-скрипта. Для CJK, деванагари и арабского необходимо явно добавить соответствующие pods в Podfile. Сбой при этом полностью беззвучный — ML Kit возвращает пустые результаты для неподдерживаемых письменностей без какой-либо ошибки. Если вы разрабатываете многоязычный OCR на iOS с ML Kit — проверьте свой Podfile.

Плавающее наложение: нативное, не Flutter

Функции перевода экрана и системного аудио работают как нативные наложения за пределами рендерной поверхности Flutter. На Android это системный оверлей-сервис, написанный на Kotlin. На iOS используются API захвата экрана на Swift.

Эти наложения взаимодействуют с Flutter-движком перевода через платформенные каналы. Архитектурная сложность — в управлении жизненным циклом: наложение должно оставаться активным, пока основной Flutter-активити может быть свёрнут.

Критический урок: на iOS нативные экземпляры мостов должны быть строго удержаны (статические или хранимые синглтоном). Если мост является переменной экземпляра, которую ничто не удерживает, ARC соберёт его как мусор и все замыкания обработчиков беззвучно станут no-op. Я потерял несколько дней из-за индикатора прогресса загрузки, застрявшего на 0%, — просто потому что мост собирался сборщиком мусора. Ни краша, ни ошибки — только тихий отказ.

Bluetooth-групповой чат

Офлайн-групповой чат использует протокол Bluetooth-сети. Каждый телефон выступает одновременно получателем и ретранслятором. Сообщения транслируются на все подключённые устройства. Каждое устройство самостоятельно выполняет ASR и перевод — нет ни «хоста», ни «серверного» телефона.

Flutter управляет UI чата, состоянием сообщений и участниками. Bluetooth-стек полностью нативный (CoreBluetooth на iOS, Android Bluetooth API) с мостом через платформенные каналы.

Общая задержка конвейера (голос → ASR → перевод → трансляция → отображение) составляет около 1 секунды. Dart UI использует оптимистичное отображение (сразу показывает «переводится...» при отправке), поэтому разговор ощущается отзывчивым.

Цифры

  • Языков: 108 всего (61 ML + 47 AI)
  • Языков ASR: 30 (распознавание речи на устройстве)
  • Языков OCR: 92 (распознавание текста камерой)
  • Базовый размер приложения: ~150 МБ + загружаемые модели
  • Платформы: iOS + Android из единой кодовой базы Flutter
  • Код Dart/Flutter: ~60% от всей кодовой базы (UI, состояние, маршрутизация, бизнес-логика)
  • Нативный код: ~40% (ASR, LLM, OCR, Bluetooth, наложения, управление памятью)
  • Размер команды: 1

Что Flutter сделал правильно для этого проекта

Единая кодовая база для сложного UI. Пять интерфейсных поверхностей, несколько режимов в каждой, листы загрузки, обучающий тур, адаптивные раскладки — написать это один раз вместо двух стало разницей между «выпустил» и «не выпустил» для одиночного разработчика.

Платформенные каналы хорошо спроектированы. Мост между Dart и нативным кодом — чистый, хорошо задокументированный и надёжный. Для приложения, где тяжёлые вычисления нативные, а пользовательский опыт — Flutter, это именно правильная архитектура.

Горячая перезагрузка для быстрых итераций. Настройка раскладок, тестирование состояний, итерации по UX-потокам — горячая перезагрузка сократила время итераций с минут до секунд на сотнях изменений UI.

Экосистема зрелая. Управление состоянием (Riverpod), навигация, локализация (117 локалей), адаптивный дизайн — экосистема Flutter предлагает production-quality решения для всего этого.

Что было сложным

Память — слепое пятно Flutter. Flutter не предоставляет детального контроля над памятью. Когда вы одновременно загружаете несколько нативных ML-моделей вместе с Flutter-движком, необходимо управлять памятью на нативном уровне и передавать эту информацию в Dart. Не существует встроенного Flutter API для «сколько ОЗУ использует моё приложение» или «каково давление системной памяти».

Жизненный цикл нативных наложений. Плавающие наложения, работающие за пределами Flutter-поверхности, архитектурно сложны. Flutter-активити может быть свёрнут, пока наложение активно. Поддержание моста в рабочем состоянии, синхронизация состояний и обработка платформо-специфичных событий жизненного цикла потребовали значительного объёма нативного кода, который Flutter не может абстрагировать.

Сложность iOS-сборки. CocoaPods с pods ML Kit-скриптов, entitlements (increased-memory-limit для совместной загрузки ASR + LLM), тайминг App Tracking Transparency, подпись кода — iOS-конвейер сборки имеет множество движущихся частей, которые Flutter-инструментарий полностью не охватывает.

Попробуйте

Traverba бесплатна на обеих платформах. Голосовой перевод, перевод камерой, перевод экрана и текстовый перевод — всё бесплатно и без ограничений. Premium ($1.99/месяц) открывает групповой чат, транскрипцию файлов и резюмирование встреч.

Доступно в Google Play и App Store. Подробнее на traverba.com.

Готов ответить на вопросы об архитектуре, паттернах интеграции Flutter с нативным кодом или об ИИ на устройстве в целом.