Голосовой ввод для Windows через Vosk своими руками
Привет! Меня зовут Иван Володин, я разработчик DD Planet, и я задался целью сделать для себя максимально удобный скрипт для набора текста речью.

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

Я пытался найти в Windows похожий встроенный инструмент или готовое решение, но все они либо брали на себя слишком много неактуального для меня функционала, так как задумывались для людей с ограниченными возможностями, либо были платными, либо были недоступны для русского языка.
Продукта который позволял бы, как в андроиде, делать это одной кнопкой (+ через локальную ллм) — нет.

Лучшим выходом из моей ситуации было создать свое минималистичное решение, и вот как это было:
Определимся с целями
- Мы хотим использовать свой voice2text (real-time перевод аудио в текст) в самых разных приложениях, во всех, где можно вводить что угодно с клавиатуры. Поэтому ставим себе требование — распознанные слова должны сами печататься в активное текстовое поле любого вида.
- Поток с микрофона мы будем отправлять в нейросеть, запущенную локально. Изначально был план использовать API от OpenAI, но self-host дает нам больше преимуществ, ведь так наш voice2text сможет работать без интернета, без проблем с конфиденциальностью и бесплатно.
- Интерфейса у нас не будет, скрипт будет работать в headless режиме и запускаться автоматически при запуске ПК.
- Для схожести с оригиналом включать/выключать микрофон будем по дефолту на самую верхнюю правую кнопку TKL клавиатуры — Pause. Благо, она редко используется в других приложениях.
Выбор нейронки
Основных предложений для локального запуска на обычном домашнем железе лидера два: Vosk и Whisper. Vosk более легковесный, запускается на CPU и из коробки поддерживает стриминг потока из микрофона. Whisper поддерживает куда больше языков и имеет заметно большее разнообразие моделей.
Для наших целей лучше подойдет Vosk, так как нас нас интересует быстрая потоковая обработка речи, а не постанализ аудио на любом языке. Немаловажный аргумент — при распознавании русской речи лучше всего себя показал Vosk.
Первые шаги
ТЗ определен, переходим к коду. Пример работы Vosk от нейросети:

Простейший демонстрационный пример работы Vosk показывает первый подводный камень: модель читает поток с микрофона блоками (по blocksize). Пока блок читается с микрофона, она успевает несколько раз его распознать, а значит может на ходу передумать и отредактировать распознанный ранее текст. Воспроизвести это проще всего повторяя похожие слова, в моем случае это «они» и «а не». Выглядеть это будет следующим образом:

Чтобы избежать этого эффекта при вводе текста, нужно учесть один момент:
если новая строка с распознаванием того же блока не является дополнением предыдущей, мы находим общий префикс, стираем текст до него и дописываем новую, более точную строку.
Теперь можно переходить к реализации этой логики.
Для полной симуляции ввода с клавиатуры будем использовать WinAPI. Такой подход позволит нам отправлять события нажатия клавиш напрямую в операционную систему, что освобождает нас от проблем поиска активных текстовых полей вручную и отправки данных в них.

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

По аналогии с примером, загружаем Vosk и распознанный им текст передаем в apply_text

Теперь, когда все готово, остается реализовать только главный цикл приложения. В нем мы будем следить за состоянием клавиши Pause:
- При нажатии — менять флаг is_listening,
- Если запись активна, останавливать поток audio_thread с функцией audio_raw_input_stream,
- Если запись выключена — наоборот, запускать audio_thread.
Работа с audio_raw_input_stream в отдельном потоке необходима по двум причинам:
- Чтобы поток с микрофона не читал данные непрерывно, когда запись не ведется,
- И чтобы при отключении микрофона не «падал» главный цикл приложения.
В решении с отдельным потоком достаточно лишний раз нажать Pause, чтобы звук начал читаться из любого другого подключенного микрофона.

Скрипт готов! Первую итерацию можно запускать и использовать. Чтобы запускать скрипт фоном, используем .pyw (или ярлык, в котором прямо укажем открытие через pythonw.exe). Чтобы скрипт запускался автоматически при старте ПК, переместим его в Автозагрузку: C:\Users\BathDuck\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
Определить, активно ли сейчас распознавание речи, можно по иконке микрофона в трее — она отображает текущее состояние.

Первые проблемы
Первые проблемы, с которыми мы сталкиваемся, используя свое решение — vosk-small распознает русский текст недостаточно точно (20-25% ошибок, это слишком неудобно). Особенно, если речь не дикторская и микрофон не стоит близко к говорящему.
Пример:
Распознано vosk-small: скажи который час девять нас без пяти только генеральский что генерал честно не в можешь тебе такие как давай снимать а ты закурить лена меда я русскую ты мне котлы
Оригинал: дядь скажи который час девятнадцать без пяти у тебя котлы-то генеральские чтоль так я же генерал да ну че не веришь честное слово дядь не в масть тебе такие котлы давай снимай а ты мне закурить мена мена я тебе папироску ты мне котлы
Решим проблему самым простым способом — возьмем модель потяжелее: vosk-model-ru-0.42. Работает она в разы точнее, но запускается несколько минут.
Реализация, при которой после запуска ПК необходимо будет ждать несколько минут, пока тяжелая модель запустится и сможет работать — не очень user-friendly, поэтому решим эту проблему следующим образом: сначала запустим small модель и распознавать текст будем ей, в этот же момент в отдельном потоке поставим грузиться тяжелую. Как только она загрузится, поменяем их местами. Так мы сможем и распознавать текст сразу с момента запуска ПК, и повысить точность распознавания настолько быстро, насколько это возможно.
Вторая итерация
Изменим инициализацию моделей под новую логику

И в основном потоке запустим отдельный поток, загружающий тяжёлую модель.

Проблемы второй версии
После долгого использования второй версии на практике все больше начинает смущать тот факт, что наш скрипт после инициализации тяжелой модели занимает 5-6 ГБ ОЗУ. В повседневной работе это не большая проблема, но при запуске других требовательных к ОЗУ приложений, наш фоновый скрипт может им мешать.

Вместо того, чтобы каждый раз убивать наше приложение в Диспетчере задач, изменим наш скрипт так, чтобы при нажатии на другую клавишу (Ctrl+Pause) тяжелая модель выгружалась из памяти, а распознавала текст снова small версия.
Заодно, так как наш скрипт не будет работать корректно, если мы запустим несколько его инстансов, мы запретим запуск более одного экземпляра нашей программы с помощью WinAPI мьютексов.
Третья итерация
Добавим логику удаления тяжёлой модели из памяти.

И используем её в главном потоке.

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

На будущее есть планы добавить пунктуацию (через vosk-recasepunc-ru-0.22), поддержку интеграции пользовательских слов в словарь Vosk и переключение на онлайн-режим распознавания.