Предыстория
На Android у меня живёт Govorun Lite - офлайн-диктовка на русском. Нажал кнопку, сказал, текст вставился. Никаких облаков, никакой отправки голоса на серверы. Работает через GigaAM v2 от Сбера.
Проблема одна: на ПК такого нет. Встроенная Windows-диктовка - онлайн. Whisper — либо медленный, либо требует видеокарту. Сторонние сервисы - снова облако.
Я решил портировать Govorun на Windows, и для ускорения взял Claude как пару-программиста. Что из этого вышло - в этой статье.
Стек
|
Компонент |
Библиотека |
Лицензия |
|---|---|---|
|
Распознавание речи |
GigaAM v2 (NeMo CTC) |
MIT |
|
ONNX-рантайм |
sherpa-onnx |
Apache 2.0 |
|
Захват звука |
sounddevice |
MIT |
|
Глобальные хоткеи |
keyboard |
MIT |
|
Буфер обмена |
pyperclip |
BSD |
|
Пунктуация |
punctuators (xlm-roberta) |
Apache 2.0 |
|
Системный трей |
pystray |
LGPL |
|
Иконки |
Pillow |
HPND |
Всё офлайн. После первоначальной загрузки моделей (~380 МБ суммарно) интернет не нужен.
Как это устроено внутри
Программа делает четыре простых вещи последовательно:
1. Слушает нажатие клавиш
Фоновый хук отслеживает Alt+X во всей системе - в любом окне, в любом приложении.
2. Пишет микрофон
Первое нажатие Alt+X - открывается поток с микрофона (16 кГц, моно). Звук накапливается в памяти кусками. Второе нажатие - поток закрывается, все куски склеиваются в один массив чисел.
3. Распознаёт речь
Массив уходит в GigaAM v2 через sherpa-onnx. Модель работает офлайн прямо на процессоре, возвращает строку без знаков препинания - это особенность CTC-архитектуры.
4. Восстанавливает пунктуацию и вставляет
Строка проходит через вторую модель (xlm-roberta ONNX) - она расставляет запятые, точки и заглавные буквы. Готовый текст копируется в буфер обмена и вставляется через Ctrl+V в то окно, где стоял курсор.
Alt+X → 🎤 Запись → Alt+X → 🧠 GigaAM v2 → ✏️ xlm-roberta → 📋 Ctrl+V в активное окно
Шаги 3 и 4 выполняются в отдельном потоке - пока идёт распознавание, хоткей не блокируется и программа не зависает.
Что докрутили по дороге
Изначально в плане были только распознавание и вставка. Но по ходу добавили ещё несколько вещей.
Системный трей
Приложение работает в фоне - терминал держать открытым не нужно. Иконка в трее показывает состояние: серая в покое, красная при записи. Реализовано через pystray + Pillow - иконка рисуется программно, без внешних файлов.
Ключевые куски кода
def _make_icon(recording: bool) -> Image.Image:
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
bg = (210, 40, 40, 255) if recording else (60, 60, 60, 255)
draw.ellipse([2, 2, 62, 62], fill=bg)
draw.rounded_rectangle([24, 10, 40, 36], radius=8, fill="white")
# ... микрофон
if recording:
draw.ellipse([46, 4, 60, 18], fill=(255, 80, 80, 255)) # красная точка
return img
Индикатор записи
Когда идёт запись - в трее красный бейдж с точкой, а тултип меняется на 🎤 Запись идёт.... Сразу видно что приложение активно.
GUI настроек
Правый клик на иконке → Настройки - открывается окно на tkinter. Там можно:
-
Сменить горячую клавишу (применяется без перезапуска)
-
Выбрать микрофон из выпадающего списка
-
Включить/выключить пунктуацию
class SettingsWindow:
def _apply(self) -> None:
# Горячая клавиша — снимаем старый хук, вешаем новый
keyboard.remove_hotkey(HOTKEY)
keyboard.add_hotkey(new_hotkey, self.ctrl.toggle, suppress=True)
# Микрофон — просто меняем глобал, следующая запись возьмёт новый
INPUT_DEVICE = new_device_id
Запись звука
class Recorder:
def _callback(self, indata, frames, time_info, status):
if status:
print(f"[!] sounddevice: {status}")
with self._lock:
if self.recording:
self.frames.append(indata.copy())
def start(self) -> None:
self.stream = sd.InputStream(
samplerate=16_000, channels=1, dtype="float32",
device=INPUT_DEVICE, callback=self._callback,
)
self.stream.start()
GigaAM ожидает моно, 16 кГц, float32 - именно это мы и пишем.
Распознавание
def load_recognizer() -> sherpa_onnx.OfflineRecognizer:
return sherpa_onnx.OfflineRecognizer.from_nemo_ctc(
model="models/model.onnx",
tokens="models/tokens.txt",
num_threads=4,
sample_rate=16_000,
feature_dim=80,
decoding_method="greedy_search",
)
Вставка в активное окно
def paste_text(text: str) -> None:
pyperclip.copy(text)
time.sleep(0.05)
keyboard.send("ctrl+v")
Грабли, которые мы собрали
1. Неправильный микрофон
sounddevice без параметра device берёт системный дефолт - но на Windows это может оказаться виртуальным устройством или стереомикшером. Добавили вывод списка устройств при старте и флаг --device N.
def print_devices():
devices = sd.query_devices()
default_in = sd.default.device[0]
for i, d in enumerate(devices):
if d["max_input_channels"] > 0:
marker = " ◄ дефолт" if i == default_in else ""
print(f"[{i}] {d['name']}{marker}")
И флаг --device N для выбора нужного микрофона.
2. Нет пунктуации
GigaAM CTC - это «сырая» модель: она выдаёт слова строчными буквами без запятых и точек. Это ограничение архитектуры CTC, а не конкретной модели.
Решение - постобработка через отдельную модель. Сначала попробовали deepmultilingualpunctuation (PyTorch), но она потянула 1.5 ГБ зависимостей. Перешли на punctuators - ONNX-based xlm-roberta, ~50 МБ, поддерживает русский:
from punctuators.models import PunctCapSegModelONNX
model = PunctCapSegModelONNX.from_pretrained(
"1-800-BAD-CODE/xlm-roberta_punctuation_fullstop_truecase"
)
result = model.infer(["привет как дела это тест"])
# → ["Привет, как дела? Это тест."]
3. WinError 6714 — транзакция NTFS
При первом запуске с deepmultilingualpunctuation PyTorch попытался писать временные файлы прямо в C:Govorun. Windows заблокировала директорию через TxF (Transactional NTFS) и не отпустила — все последующие Path.exists() в той же папке падали с OSError: [WinError 6714].
Лечится перезагрузкой ПК. Но чтобы не повторялось — нужно заранее перенаправить кэши:
_CACHE = Path.home() / ".cache" / "govorun"
os.environ.setdefault("HF_HOME", str(_CACHE / "huggingface"))
os.environ.setdefault("TRANSFORMERS_CACHE", str(_CACHE / "huggingface" / "hub"))
os.environ.setdefault("TORCH_HOME", str(_CACHE / "torch"))
Важно: эти строки должны стоять до любых import torch / import transformers.
4. pyautogui не вставлял текст
Первоначально вставка делалась через pyautogui.hotkey("ctrl", "v"). Из фонового потока это иногда не срабатывало - фокус окна успевал уйти пока шло распознавание.
Заменили на keyboard.send("ctrl+v") из той же библиотеки, что уже используется для хоткеев. Работает надёжнее, потому что эмулирует нажатие на уровне драйвера, а не через UI Automation.
Что получилось
📋 Доступные устройства ввода: [0] Микрофон (Realtek HD Audio) ◄ дефолт
🎙 Используется устройство [0]: Микрофон (Realtek HD Audio)
⏳ Загружаю модель пунктуации (ONNX)… ✅ Пунктуация готова.
⏳ Загружаю GigaAM… ✅ Модель готова.
🎯 [ALT+X] — старт/стоп записи. Настройки и выход — иконка в системном трее.
🎤 Запись… (ALT+X — остановить) ⏹ Записано 4.2с, уровень: 0.0318, распознаю… 📝 Привет, это тестовая запись. Всё работает отлично. ✅ Скопировано и вставлено
Текст появляется в том поле, где стоял курсор - в браузере, Telegram, Word, IDE, где угодно
Установка
Вариант 1 — готовый exe (не нужен Python)
-
Скачайте GovorunPC.exe (38 МБ)
-
Положите рядом папку
models/с файлами модели -
Запустите
GovorunPC.exe
При первом запуске punctuators скачает xlm-roberta (~50 МБ) в ~/.cache/govorun/. Интернет больше не понадобится.
Модели скачиваются отдельно (~330 МБ, один раз):
pip install requests tqdm
python download_models.py
Вариант 2 — из исходников
# Скачать исходники
# Распаковать архив со страницы релиза
pip install -r requirements.txt
python download_models.py # ~330 МБ, один раз
python govorun_pc.py # или двойной клик на govorun.vbs
При первом запуске punctuators скачает xlm-roberta (~50 МБ) в ~/.cache/govorun/. После этого интернет больше не нужен.
Итог
На всё ушло меньше вечера. Начали с простого скрипта - распознавание + вставка. По дороге добавили трей, индикатор записи, GUI настроек и собрали готовый .exe.
Основная часть времени - не написание кода, а отладка специфических Windows-проблем: WinError 6714, выбор микрофона, ненадёжная вставка через pyautogui. Claude предлагал решения, я проверял на живой машине, правили вместе.
Стек получился полностью офлайн, без PyTorch в рантайме (только ONNX), и работает на обычном процессоре без GPU.
Буду рад вопросам в комментариях.
Благодарности
Отдельное спасибо amidexe - автору оригинального Govorun Lite для Android. Именно его работа стала отправной точкой: готовая идея, проверенный стек (GigaAM + sherpa-onnx), понятная концепция «нажал - сказал - вставилось».
Без его приложения я бы не знал, с чего начать, и вряд ли вообще взялся за эту задачу.
Автор: size222
