На прошлой неделе мы писали про мобильный переводчик с изображения:
На этой же мы приоткроем занавес и расскажем подробнее о процессе создания этого прототипа. А именно Как сделать overlay-переводчик для Android на Flutter.
Мы рассмотрим в этой статье стадии решения поставленной задачи, применяемые технологии и осветим ключевые аспекты их использования.
В данной статье мы не даём полное представление кодовой базы приложения, в ней будут приведены лишь наиболее релевантные фрагменты кода, непосредственно связанные с описываемыми технологиями и подходами.
Задача
Требовалось разработать мобильное приложение для Android на Flutter, способное в реальном времени:
- Распознать текст на экране устройства.
- Перевести обнаруженный текст на целевой язык.
- Наложить переведённые текстовые блоки поверх исходных, сохраняя их расположение и структуру.
Этапы
Для создания нашего приложения мы использовали плагин для создания overlay (оверлея). Оверлей — приложение или части приложения (например, панель или виджеты), которые располагается поверх остальных.
Мы выделили в разработке решения следующие этапы:
- Создание оверлея, который будет отображаться поверх любых приложений, и научить его делать скриншоты экранов этих приложений.
- Распознавание скриншота и получение текста и метаданных, необходимых для отрисовки виджета с переводом.
- Перевод текста
- Создание виджета с расположением переведенного текста поверх исходного текста
Первый этап
Разбиваем этап на подзадачи:
- создать оверлей,
- реализовать обмен данными между оверлеем и приложением,
- реализовать захват экрана из оверлея.
Создание оверлея
Для создания оверлея мы использовали плагин flutter_overlay_window. Его функциональности хватает для создания оверлея, который будет показывать панель с кнопками управления или виджет с переводом в зависимости от состояния приложения. В оверлей передаётся виджет. В нашем случае один из двух виджетов: панель с кнопками, которая управляет переводчиком («Свернуть»/«Развернуть», «Закрыть», «Начать перевод») и виджет с переведённым текстом, который располагается в тех же областях экрана, что и исходный текст.
Обмен данными между оверлеем и приложением
Захват экрана должен выполняться при нажатии кнопки в оверлее. Но захват и обработка изображения экрана должны запускаться в основном приложении, а не в оверлее. То есть оверлей должен отправлять команду для запуска всей цепочки действий, в результате которых будут получены данные для отрисовки виджета с переводом. Было принято решение запускать оверлей в отдельном изоляте (Isolate) и управлять им с помощью команд.
В функцию sendMessage передаётся источник сообщения (приложение или оверлей) и команда. В соответствии с источником сообщения выбирается порт для отправки сообщения.
Реализация захвата экрана для перевода
Когда оверлей инициирует процесс перевода, первым шагом необходимо получить изображение экрана. Однако здесь мы сталкиваемся с существенными платформенными ограничениями из-за безопасности:
1. Ограничения стандартных решений. Существующие Flutter-плагины для скриншотов работают исключительно в рамках собственного приложения и не могут захватывать содержимое других приложений.
2. Решение через MediaProjection API. Для системного захвата экрана требуется использование Android MediaProjection API - специального системного интерфейса, который позволяет:
- захватывать контент всего экрана;
- получать скриншоты сторонних приложений;
- записывать видео (в нашем случае не используется).
3. Особенности реализации. В использовании MediaProjection есть важные нюансы:
- требует явного подтверждения пользователя через системный диалог;
- отображает постоянную иконку записи в строке состояния;
- ограничено политиками безопасности Android (начиная с версии 10+).
Так как MediaProjection API является частью Android SDK и не поддерживается напрямую из Flutter, то для того чтобы воспользоваться возможностями MediaProjection API из Flutter, нужно сделать несколько шагов.
1. Добавить разрешения в android/app/src/main/AndroidManifest.xml.
2. Создать сервис захвата: android/app/src/main/kotlin/com/example/multilingual_app/ScreenshotService.kt.
3. Реализовать вызов MediaProjection в Kotlin.
4. Связь с Flutter (MethodChannel).
5. Сделать обработку полученного изображения.
Второй этап
Распознавание текста и извлечение метаданных из скриншота
После получения скриншота мы приступили к распознаванию текста и его структуры. Для этого мы использовали пакет google_mlkit_text_recognition, предоставляющий мощные инструменты для обработки изображений с помощью ML-моделей Google.
Преобразование входных данных
На вход алгоритма поступает массив байтов (Uint8List), однако метод распознавания текста требует объект типа InputImage.
При попытке использовать InputImage.fromBytes() мы столкнулись с проблемой:
- Метод требует обязательные метаданные (формат, ориентации др.),
- При ручном задании параметров возникали ошибки, приводящие к некорректному распознаванию.
Решение:
Был применен альтернативный подход – сохранение массива байтов во временный файл и создание InputImage через InputImage.fromFile().
Результат распознавания
На выходе получаем:
- распознанный текст (строка),
- список TextBlock — структурные блоки текста.
TextBlock содержит:
- текст и его координаты на изображении,
- cornerPoints — массив точек описывающих границы блока (полигон, в котором расположен текст).
Эти данные критически важны для четвёртого этапа – наложения переведённого текста поверх исходного сохранением позиции и размера.
Координаты cornerPoints будут использоваться для:
- позиционирования виджета с переводом,
- маскировки исходного текста (если требуется),
- адаптации под изменения масштаба/ориентации экрана.
Таким образом, на этом этапе не только извлекается текст, но и подготавливаются данные для точного визуального сопоставления с оригиналом.
Третий этап
Перевод текста и обновление текстовых блоков
После успешного распознавания текста переходим к переводу с сохранением структуры исходных блоков. Для этого мы использовали пакет google_mlkit_translation, обеспечивающий машинный перевод на устройстве.
Примечание: качество перевода оставляет желать лучшего, но целью нашего приложения была попробовать сам подход к распознаванию с экрана, этот момент мы ещё проработаем и приручим какую-нибудь хорошую нейрушку для перевода.
Инициализация переводчика
1. Определение языков:
- sourceLanguage – язык исходного текста (автоопределение или заданный);
- targetLanguage – язык перевода (например, русский).
Коды языков должны быть преобразованы из формата BCP-47 (например,"en","ru") в тип TranslateLanguage:
Создание транслятора
Процесс перевода
Для каждого TextBlock из распознанного текста:
1. извлекается исходный текст,
2. выполняется перевод,
3. сохраняется результат с сохранением структуры блока.
Формирование списка переведенных блоков
Результат — список обновленных TextBlock, где:
- исходный текст заменён на переведенный,
- все метаданные (координаты, размеры и т.д.) остаются неизменными.
Реализация:
Итоговые данные
На выходе получаем:
- translatedTextList – список переведенных строк (для логирования или проверки),
- translatedBlocks – модифицированные TextBlock с переведенным текстом, но исходными координатами и структурой.
Зачем это нужно?
1. Точное наложение перевода – координаты из cornerPoints позволяют разместить переведенный текст поверх оригинального с пиксельной точностью.
2. Сохранение контекста – структурные элементы (разбивка на строки, блоки) остаются неизменными, что важно для читаемости.
3. Гибкость – можно дополнительно обрабатывать отдельные блоки (например, игнорировать знаки или числа).
Этот этап завершает подготовку данных для финального рендеринга — отрисовки перевода поверх интерфейса приложения.
Четвёртый этап
Реализация CustomPainter для отрисовки переведённого текста
Для визуализации переведённого текста с сохранением исходного позиционирования был создан специализированный CustomPainter – класс TranslatedTextPainter. Его задача — корректно отобразить текст поверх оригинального контента с учётом масштабирования экрана.
Ключевые особенности
1. Масштабирование координат.
Параметр ratio преобразует физические пиксели (из cornerPoints) в логические:
2. Метод paint().
Отрисовывает каждый TextBlock как:
- фоновую подложку текста,
- выделение границ блока,
- переведённый текст.
3. Оптимизация перерисовки.
shouldRepaint перерисовывает виджет только при изменении данных или масштаба.
Интеграция с оверлеем
Для встраивания в интерфейс:
- Создаём CustomPaint с нашим TranslatedTextPainter:
- Разворачиваем на весь экран через _resizeOverlay:
Визуальный результат
- Текст отображается поверх оригинального контента,
- Блоки повторяют форму и положение исходных элементов,
- Поддержка динамического масштабирования (например, при повороте экрана).
Таким образом, система обеспечивает бесшовную интеграцию перевода в реальном времени.
Проект полностью можно посмотреть на гитхабе: https://github.com/Grovety/Android_translater
Возможно, вам также будет интересно: