Govorun PC: переносим офлайн-диктовку с Android на Windows за один вечер (с Claude)

от автора

Предыстория

На 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

Всё офлайн. После первоначальной загрузки моделей (~380 МБ суммарно) интернет не нужен.

Как это устроено внутри

Программа делает четыре простых вещи последовательно:

1. Слушает нажатие клавиш
Фоновый хук отслеживает Alt+X во всей системе — в любом окне, в любом приложении.

2. Пишет микрофон
Первое нажатие Alt+X — открывается поток с микрофона (16 кГц, моно). Звук накапливается в памяти кусками. Второе нажатие — поток закрывается, все куски склеиваются в один массив чисел.

3. Распознаёт речь
Массив уходит в GigaAM v2 через sherpa-onnx. Модель работает офлайн прямо на процессоре, возвращает строку без знаков препинания — это особенность CTC-архитектуры.

4. Восстанавливает пунктуацию и вставляет
Строка проходит через вторую модель (xlm-roberta ONNX) — она расставляет запятые, точки и заглавные буквы. Готовый текст копируется в буфер обмена и вставляется через Ctrl+V в то окно, где стоял курсор.

Ключевые куски кода

Запись звука

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:        with self._lock:            self.frames = []            self.recording = True        self.stream = sd.InputStream(            samplerate=16_000,            channels=1,            dtype="float32",            device=INPUT_DEVICE,   # None = системный дефолт            callback=self._callback,        )        self.stream.start()    def stop(self) -> np.ndarray:        with self._lock:            self.recording = False        self.stream.stop(); self.stream.close()        return np.concatenate(self.frames).flatten().astype(np.float32)

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,          # GigaAM v2 — 80-мерные мел-фичи        decoding_method="greedy_search",    )def recognize(rec, audio: np.ndarray) -> str:    stream = rec.create_stream()    stream.accept_waveform(16_000, audio)    rec.decode_stream(stream)    return stream.result.text.strip()

Вставка в активное окно

def paste_text(text: str) -> None:    pyperclip.copy(text)    time.sleep(0.05)    keyboard.send("ctrl+v")   # работает в любом окне

Грабли, которые мы собрали

1. Неправильный микрофон

sounddevice без параметра device берёт системный дефолт — но на Windows это может оказаться виртуальным устройством или стереомикшером. Добавили диагностику:

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 PunctCapSegModelONNXmodel = 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) ◄ дефолт [1] Стерео микшер (Realtek HD Audio)

🎙 Используется устройство [0]: Микрофон (Realtek HD Audio)

⏳ Загружаю модель пунктуации (ONNX)… ✅ Пунктуация готова.

⏳ Загружаю GigaAM… ✅ Модель готова.

🎯 [ALT+X] — старт/стоп записи. Ctrl+C в этом окне — выход.

🎤 Запись… (нажмите ALT+X ещё раз чтобы остановить) ⏹ Записано 4.2с, уровень: 0.0318, распознаю… 📝 Привет, это тестовая запись. Всё работает отлично. ✅ Скопировано и вставлено

Текст появляется в том поле, где стоял курсор — в браузере, Telegram, Word, IDE, где угодно

Установка

git clone <репозиторий>cd Govorunpip install -r requirements.txtpython download_models.py   # скачивает GigaAM v2, ~330 МБpython govorun_pc.py

При первом запуске punctuators скачает xlm-roberta (~50 МБ) в ~/.cache/govorun/. Интернет больше не понадобится.

Что можно докрутить

  • VAD — Silero VAD для автостопа по паузе, как в оригинальном Android-приложении

  • Системный трей — pystray, чтобы не держать терминал открытым

  • Стриминг — sherpa-onnx умеет онлайн-режим (streaming-zipformer модели), текст будет появляться по мере речи

  • GUI для настроек — смена хоткея без правки кода, выбор микрофона из списка.

Итог

На всё ушло меньше вечера. Основная часть времени — не написание кода, а отладка специфических Windows-проблем (WinError 6714, выбор микрофона). Claude предлагал решения, я проверял на живой машине, правили вместе.

Стек получился полностью офлайн, без PyTorch в рантайме (только ONNX), и работает на обычном процессоре без GPU.


Буду рад вопросам в комментариях.

Благодарности

Отдельное спасибо amidexe — автору оригинального Govorun Lite для Android. Именно его работа стала отправной точкой: готовая идея, проверенный стек (GigaAM + sherpa-onnx), понятная концепция «нажал — сказал — вставилось».

Без его приложения я бы не знал, с чего начать, и вряд ли вообще взялся за эту задачу.

ссылка на оригинал статьи https://habr.com/ru/articles/1031240/