Оффлайн телесуфлёр поверх Instagram и TikTok: распознавание речи на телефоне, выравнивание по словам

в 15:57, , рубрики: accessibility service, android, kotlin, open source, sherpa-onnx, zipformer, микрофон, офлайн, распознавание речи, телесуфлёр

Я снимаю рилсы с телефоном в руке и хотел суфлёр, который сам прокручивает текст, пока я говорю, причём поверх Instagram, TikTok или системной камеры, а не в собственном рекордере. Самым трудным оказалось не распознавание речи, а микрофон: во время записи видео Android отдаёт его камере.

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

Я когда сам снимаю рилсы и шортчы на фронтальную камеру и держу телефон в руке, говорю пару строк в объектив, смотрю в текст и продолжаю. Учить сценарий наизусть долго.

В суфлёре с прокруткой по таймеру скорость задаётся заранее. Текст то отстаёт от речи, то прокручивается раньше, чем нужно. Когда я сбиваюсь и начинаю фразу заново, прокрутка продолжается. Листать текст пальцем той же руки, в которой я держу телефон, неудобно.

Мне был нужен суфлёр, который прокручивает текст по голосу. Когда я повторяю фразу, текст должен вернуться к её началу. Если я пропускаю абзац, суфлёр должен сам перейти туда, где я продолжил, а пока я молчу, держать текст на месте. Работать всё это должно поверх приложения, в котором я снимаю. Большинство суфлёров с прокруткой по голосу, которые я нашёл, платные или записывают видео только своей камерой, и снятое видео потом нужно доставать из их приложения.

Я написал свой телесуфлёр для Android и назвал его Суфлёром. Он бесплатный, код открыт под лицензией GPL-3.0. Суфлёр показывает текст поверх приложения камеры и прокручивает его, пока я говорю. Речь он распознаёт на телефоне, разрешения на доступ в интернет у приложения нет.

Суфлёр: текст сразу под фронтальной камерой, запись идёт в обычном приложении камеры

Суфлёр: текст сразу под фронтальной камерой, запись идёт в обычном приложении камеры

Когда в инстаграме начинается запись видео, другое приложение, которое в этот момент тоже слушает микрофон, перестаёт получать звук.

Начиная с Android 10 система намеренно делит микрофон между приложениями по приоритетам. Когда камера начинает снимать видео, микрофон достаётся ей. Проверить, заглушено ли приложение, можно через AudioRecordingConfiguration.isClientSilenced(), но звук от этого не вернётся. Без обходного пути Суфлёр получал бы тишину всё время, пока идёт запись видео.

Нужное исключение есть в Android Compatibility Definition Document (CDD). В его разделе 5.4.5 про одновременный захват звука прописано правило: включённая служба специальных возможностей (accessibility service), которая берёт звук из источника VOICE_RECOGNITION, может слушать микрофон одновременно с другим приложением.

Я проверил это на своём Samsung Galaxy S24 FE (One UI 6.1.1, Android 14). инстаграм и основная камера пишут звук для видео с источником CAMCORDER. Пока служба включена, Суфлёр продолжает получать звук через источник VOICE_RECOGNITION. Звук в самом рилсе не меняется: Суфлёр только слушает микрофон параллельно с камерой и ничего у неё не отбирает.

Ради этого исключения Суфлёр просит включить службу специальных возможностей. Такие службы могут читать содержимое экрана и перехватывать нажатия, поэтому своей службе я оставил только то, что нужно Суфлёру.

Между сессиями Суфлёра служба ни на что не подписана, не получает нажатий кнопок и игнорирует события окон. Содержимое экрана она читать не может: в конфигурации стоит canRetrieveWindowContent="false". Единственное, что служба замечает, это название приложения на переднем плане. Его она записывает во внутренний журнал. Кроме того, что служба слушает микрофон, она показывает плавающее окно и принимает нажатия кнопок громкости и пульта. Кнопки можно выключить или переназначить в настройках.

<accessibility-service    android:accessibilityEventTypes="typeWindowStateChanged"    android:canRequestFilterKeyEvents="true"    android:canRetrieveWindowContent="false"    android:isAccessibilityTool="false"    ... />

Разрешение «Поверх других окон» нужно, только если служба выключена.

На Android 13 и новее у приложений, установленных из файла, переключатель службы сначала серый. Нужно открыть «Настройки → Приложения → Суфлёр → ⋮ → Разрешить ограниченные настройки» и включить службу ещё раз.

Распознавание речи на телефоне

Облачное распознавание я отбросил сразу, потому что не хотел давать приложению разрешение на доступ к сети. Мне нужна была модель, которая работает на телефоне и выдаёт слова, пока я ещё говорю. Распознавать она должна быстрее реального времени с запасом, чтобы текст не отставал от речи.

Для распознавания я взял sherpa‑onnx от команды k2-fsa. Это движок на ONNX Runtime, и у него есть готовая сборка под Android.

Русская и английская модели относятся к потоковым Zipformer‑трансдьюсерам. Энкодер и joiner в обеих квантованы в int8.

  • Русская модель vosk‑model‑small‑streaming‑ru от Alpha Cephei весит около 29 МБ.

  • Английская модель из проекта icefall обучена на LibriSpeech (экспорт 2023–06-26) и весит около 73 МБ.

Обе модели лежат внутри APK и скачать их после установки оно не может. Из‑за этого APK весит около 130 МБ.

Замеры я делал на том же S24 FE. Записи тестовой речи я подавал в распознаватель файлом, без микрофона.

  • Английский распознаётся в 15 раз быстрее реального времени (RTF 0,067), русский в 28 раз быстрее (RTF 0,035).

  • Первые слова появляются меньше чем через секунду после начала речи.

  • Модель загружается примерно за секунду.

Текст сценария известен заранее, и Суфлёр передаёт его слова распознавателю как подсказки. Они нужны, чтобы модель чаще распознавала редкие слова из текста, например фамилии и термины, и в sherpa‑onnx работают с декодированием modified beam search. Насколько они помогают, я пока толком не измерил. На английском тестовом файле распознанный текст с подсказками и без них отличался мало. В настройках подсказки можно выключить.

Язык речи Суфлёр определяет по буквам сценария или берёт из настроек. Если открыть сценарий на другом языке, Суфлёр переключает модель.

Как трекер находит место в тексте

Распознаватель выдаёт поток слов с ошибками распознавания, повторами, «э‑э», пропусками и фразами не по тексту, например «так, ещё раз». По этому потоку трекер определяет, какое слово сценария я сейчас произношу.

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

На каждом обновлении распознавателя трекер берёт восемь последних распознанных слов и выравнивает их по всему сценарию. Для этого он использует вариант алгоритма Смита‑Уотермана. Этим алгоритмом локального выравнивания в биоинформатике ищут похожие участки последовательностей. Слова трекер сравнивает нечётко: пара «дрона» и «дрон» или слово с неточно распознанным окончанием даёт частичное совпадение. Пропущенное слово сценария или лишнее услышанное слово трекер штрафует, но путь на этом не обрывается.

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

У слов разный вес. Слова «и» и «что» встречаются в тексте десятки раз, и по их совпадению место не определить. Длинное слово, которое встречается в сценарии один раз, почти однозначно указывает место. Вес слова равен базовому весу, умноженному на множитель редкости. Базовый вес у служебных и коротких слов маленький, у длинных большой.

вес = базовый_вес × (0,35 + 0,65 / n)

Здесь n равно числу вхождений слова в этот сценарий. Чтобы формы одного слова считались вместе, в русском трекер сравнивает первые пять букв. У словоформ одного слова они обычно совпадают. Способ грубый, но для этой задачи его хватает.

В пути подряд может быть пропущено не больше двух слов. Без этого ограничения путь мог бы пройти через длинный разрыв. Вес старых правильных совпадений прибавился бы к далёкому совпадению какого‑нибудь частого слова, и текст перескочил бы на место, которое я не читал.

Последнее слово распознаватель часто выдаёт недоговорённым, например «взгл» от слова «взгляни». Совпадение по началу слова трекер тоже засчитывает, но с меньшим весом.

Выравнивание выдаёт кандидатов. У каждого кандидата есть место в сценарии и оценка того, насколько надёжен путь к нему. Требования к переходу зависят от расстояния между кандидатом и текущей позицией:

Куда переходит текст

Что нужно трекеру

До трёх слов вперёд при обычном чтении

Одно совпадение с достаточным весом

На одну‑две строки выше или ниже

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

На дальнее место, если я пропустил блок или вернулся больше чем на две строки назад

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

Паузы и разговоры не по тексту позицию не меняют, потому что слова, которых нет в сценарии, не добавляют вес ни одному месту.

Допустим, на экране такой текст:

Сегодня разберём,
как выбрать первый дрон
для съёмки путешествий
и не переплатить.

Я дочитал до «для съёмки», сбился, сказал «так, ещё раз» и начал вторую строку заново. Последние распознанные слова в этот момент такие:

… дрон для съёмки так ещё раз как выбрать

Слов «так», «ещё» и «раз» в сценарии нет, и вес ни к одному месту они не добавляют. Путь, который заканчивается на последнем услышанном слове «выбрать», совпадает с началом второй строки «как выбрать». Для соседней строки этого достаточно, и текст возвращается к ней. Трекер, который ищет только вперёд от текущего места, ждал бы слово «путешествий».

Окно поверх камеры

Текст в окне Суфлёра начинается сразу под фронтальной камерой, чтобы взгляд оставался у объектива, а кнопки я вынес вниз. Прочитанные строки становятся тусклее. Пометки в квадратных скобках, например «[пауза, улыбка]», видны на экране, но вслух их читать не нужно.

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

Управлять Суфлёром можно кнопками громкости, Bluetooth пультами для селфи, кольцами, кликерами и клавиатурами. Каждую кнопку можно переназначить. Плитка в шторке запускает Суфлёр поверх того приложения, которое сейчас открыто. Если голос не нужен, есть прокрутка с заданной скоростью и отсчётом 3-2-1. Перед съёмкой текст можно прочитать в режиме репетиции.

Суфлёр открывает TXT в кодировках UTF-8, UTF-16, Windows-1251 и KOI8-R, Markdown и заметки Obsidian, DOCX, ODT, RTF, HTML и PDF. Текст из PDF извлекает системный PdfRenderer, поэтому для PDF нужен Android 15 или Android 12–14 со свежими системными обновлениями Google Play. Сценарий можно передать из другого приложения через «Поделиться» или «Открыть с помощью», вставить из буфера обмена или написать в редакторе с акцентами, # заголовками и [пометками]. Длинные абзацы Суфлёр делит на короткие фразы, и каждую можно прочитать без паузы на вдох. Импортёры защищены от zip‑бомб, атак через XML‑сущности и слишком больших файлов.

Библиотека сценариев, сценарий, редактор и настройки

Библиотека сценариев, сценарий, редактор и настройки

Английский язык

В версии 0.4 в интерфейсе и распознавании появился английский язык. Трекеру для него понадобились отдельные правила.

В сценарии стоит don’t с типографским апострофом, а распознаватель пишет DON’T с прямым. Трекер считает их одним словом. Так же он обрабатывает it’s и остальные слова с апострофом.

В тексте написано «2025», а произносят twenty twenty five. Числа, записанные в сценарии цифрами, трекер не учитывает. Если такие числа в сценарии есть, трекер отбрасывает числительные из распознанного текста, кроме тех, которые встречаются в самом сценарии словами.

Mr. произносят mister, Dr. произносят doctor, и трекер сопоставляет сокращения с этими словами. Точку после Mr. и e.g. Суфлёр не считает концом предложения и не режет по ней текст на фразы.

Аббревиатуры распознаватель выдаёт по буквам: AI в виде «A I», CEO в виде «C E O». Если такое слово есть в сценарии, трекер собирает буквы обратно в слово. Слова um и uh трекер отбрасывает.

В русском окончания одного слова меняются, и сравнение слов там нечёткое. В английском одна буква отличает одно слово от другого, например the и then или want и went, и сравнение строже. Ещё трекер принимал недоговорённое the за начало слова then. Теперь служебное слово не может считаться началом более длинного.

Приватность и проверяемые сборки

У Суфлёра нет разрешения на доступ в интернет, и Android не даёт ему подключаться к сети. Проверить это можно в настройках приложения, в полном списке разрешений «Разрешения → ⋮ → Все разрешения». Если это разрешение когда‑нибудь появится, сборка релиза на GitHub Actions завершится с ошибкой.

Микрофон Суфлёр использует только во время сессии. Звук он не записывает, не хранит и никуда не отправляет. На Android 12 и новее система вдобавок показывает свой индикатор микрофона. Аналитики, рекламы, аккаунтов и трекеров слежки в приложении нет. Сценарии хранятся в закрытой папке приложения и в облачную резервную копию не попадают.

APK собирает GitHub Actions из исходников с тегом версии. Все релизы подписаны одним ключом, и к каждому приложен SHA256SUMS.txt. Отпечаток сертификата SHA-256:

77:F3:94:32:F5:05:FB:B7:8B:85:E7:37:CD:00:21:0A:44:C0:7A:13:2C:AC:FF:81:FE:9D:9D:79:CD:E9:A9:06

Сборка воспроизводимая. APK на GitHub побайтно совпадает с тем, который я собрал локально из того же коммита. Без моего ключа это можно проверить, если перенести подпись с релизного APK на свою сборку, например утилитой apksigcopier. Библиотек распознавания и моделей в git нет. Их скачивает Gradle‑задача из закреплённых ревизий и сверяет каждый файл по SHA-256.

Как писался код

Значительную часть кода написал Claude от Anthropic. Идея и решения мои, результат я проверял на своём телефоне.

Планы

Я планирую сделать пакеты для других языков, у которых есть компактные потоковые модели, в том числе для испанского, немецкого, итальянского, польского и вьетнамского. Пакет будет отдельным файлом, который открывается в Суфлёре.

Ссылки

Суфлёру нужен Android 10 или новее на 64-битном ARM‑телефоне. С 2027 года Google вводит проверку разработчиков, и на телефонах с сервисами Google может понадобиться одноразовая настройка. Инструкция есть в README.

Мне нужны отчёты с других телефонов, в том числе о том, что всё работает. Напишите в комментариях модель телефона, версию Android и прокручивается ли текст во время записи видео. Если что‑то не работает или текст перескакивает не на то место, приложите журнал из «Настройки → Журнал → Поделиться». Если вы снимаете по сценарию, напишите, как справляетесь с текстом сейчас. Если видите случай, на котором трекер ошибётся, тоже напишите.

Автор: olerast

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js