Мне нужен был офлайновый голосовой ввод для Windows — push‑to‑talk, без облака, с хорошим распознаванием русского. Звучит просто? Я тоже так думал. За два месяца перепробовал три ASR‑движка, кучу оптимизаций, и большая часть идей оказалась тупиком. Но в итоге — 3.3% WER на CPU, в 2.4 раза лучше Whisper large‑v3-turbo на RTX 4090.

Зачем это вообще понадобилось

Голосовой ввод на русском в 2026 году — грустная история. Встроенный в Windows работает через облако и плохо понимает русскую речь. Google Cloud STT — платный и требует интернет.

Задача казалась простой: зажал горячую клавишу, сказал фразу, отпустил — текст вставился в позицию курсора. Как диктофон, только сразу в текст и без интернета.

Микрофон → VAD → ASR → Постобработка → Буфер обмена → Вставка (Ctrl+V)

Весь вопрос — чем заполнить блок «ASR».

Методология

Пять аудиофрагментов из русских аудиокниг, TTS‑генерация (воспроизводимо, но не совсем реальные условия):

  • short_1, short_2 (6–7с) — простая лексика

  • medium_1 (15с) — литературный текст

  • long_1, long_2 (25–30с) — редкие слова («обезматочивший», «Очумелов»)

Метрика — WER (jiwer), базовый уровень — Whisper base на CPU (33%). GPU — RTX 4090 Laptop 16 ГБ. Каждый замер ×3, среднее. Whisper через faster‑whisper + CTranslate2, int8.

Этап 1: Whisper — разочарование по нарастающей

Whisper — очевидный первый кандидат. OpenAI, 99 языков, хайп. Начал с base на CPU — единственная модель, которая укладывается в бюджет 1 секунды.

Модель

Устройство

WER

Задержка

Укладывается в 1с?

tiny

CPU

56.2%

0.30с

Да, но бесполезная

base

CPU

32.6%

0.45с

Да

small

CPU

23.4%

1.23с

Нет (+230мс)

medium

GPU

10.7%

1.75с

Нет

large‑v3

GPU

8.8%

2.30с

Нет

large‑v3-turbo

GPU

7.9%

0.44с

Да, но нужен GPU

На CPU реально использовать только base. Small не влезает в секунду, а дальше — только GPU.

Тупик 1: параметры декодинга

Ладно, может дело не в модели, а в настройках? Beam size 1/2/3, temperature 0.0/0.5/1.0 — WER одинаковый (32.6%). У Whisper base узкое место — энкодер, не декодер. Крутить ручки декодинга бесполезно.

Тупик 2: адаптивный фоллбэк (base → small)

Идея казалась красивой: если confidence у base низкий — переделать через small. На синтетике с шумом — 0.0% WER, я уже праздновал. На тестовом аудио — 34.6%, хуже чистого base. Классический случай, когда бенчмарк на синтетике врёт.

Тупик 3: агрессивная предобработка

noisereduce (2-pass), тяжёлая шумоподавка — стало хуже. Деноизинг разрушает форманты, Whisper путает слова ещё сильнее.

Этап 2: T5-коррекция — палка о двух концах

Раз Whisper ошибается — может, подчистить за ним языковой моделью? Взял [UrukHan/t5-russian‑spell](https://huggingface.co/UrukHan/t5-russian‑spell), ускорил через CTranslate2.

Проблема: нефильтрованная T5 ухудшает WER. Модель «креативит» — заменяет слова на семантически похожие, удаляет куски текста.

Решение: пословная фильтрация (Smart T5)

Сравниваем каждое слово до и после коррекции через difflib.SequenceMatcher. Принимаем исправление, только если слово изменилось незначительно:

def smart_correct(self, text: str) → str:

  corrected = self._correct(text) # прогоняем через T5
  original_words = text.split()
  corrected_words = corrected.split()
  result_words = []
  
  for orig, corr in zip(original_words, corrected_words):
    ratio = SequenceMatcher(None, orig, corr).ratio()
    threshold = 0.80 if len(orig.split()) == 1 else 0.85
    result_words.append(corr if ratio >= threshold else orig)

  return ’ “.join(result_words)”

ASR выдал «привед», T5 исправил на «привет» — ratio ≈ 0.83, принимаем. T5 заменил «кошка» на «собака» — ratio ≈ 0.33, отклоняем.

Тупики 4–5: перебор порогов и confidence‑based T5

Пороги 0.60–0.85 дают одинаковый WER (32.6%), выше 0.90 — хуже (35.4%). Применение T5 только к низко/высококонфидентным словам — тоже без эффекта. T5 лишь удерживает Whisper base от совсем позорных результатов (без неё — 41%). Потолок 33%, до цели далеко. Нужен другой движок.

Этап 3: Vosk — быстрый, но ограниченный

Vosk — офлайн ASR на базе Kaldi. Модель 50 МБ, и у меня были на него большие надежды.

Длина аудио

WER

Задержка

6–7 сек

0–6%

0.71–0.80с

25–30 сек

20–27%

Среднее

13.0%

0.75с

На коротких фразах — прилично. Но стоит подкинуть что‑то подлиннее с редкой лексикой — Vosk сыпется. «Обезматочивший» и «Очумелов» ему неизвестны, словарь слишком мал. 60% снижения — неплохо, но до цели (80%) не дотягивает.

Тупик 6: чанкование длинного аудио

Разбить длинное аудио на куски, распознать по частям — WER стал хуже. И у Vosk, и позже у GigaAM. Модели используют внутренний контекст, и чем короче сегмент — тем больше ошибок. Контринтуитивно, но целое аудио распознаётся лучше, чем сумма его частей.

Этап 4: GigaAM — и тут всё изменилось

К этому моменту я почти смирился, что 80% снижения на CPU — утопия. И тут наткнулся на GigaAM — ASR от SberDevices, обученную на 700 000 часов русской речи (MIT). Whisper обучен на ~680K часов всех языков, русский — лишь часть.

Интеграция через onnx‑asr:

import onnx_asr

model = onnx_asr.load_model(“gigaam‑v3-e2e‑rnnt”)
text = model.recognize(audio_float32, sample_rate=16000)

Результаты

Модель

Тип

WER

Снижение

Задержка (CPU)

gigaam‑v3-e2e‑rnnt

RNNT

3.3%

90.0%

0.66с

gigaam‑v3-rnnt

RNNT

3.3%

90.0%

0.82с

gigaam‑v3-e2e‑ctc

CTC

4.2%

87.2%

1.08с

gigaam‑v2-ctc

CTC

4.7%

85.7%

1.18с

Я перепроверил трижды. Лучший Whisper на RTX 4090 — 7.9%. GigaAM на CPU — 3.3%. В 2.4 раза лучше, без видеокарты.

RNNT vs CTC

На сложных текстах разница огромная: RNNT 2.6% vs CTC 13.2% WER. RNNT‑декодер учитывает контекст ранее сгенерированных токенов, CTC — нет. Для литературного русского это критично.

Подводные камни

Float32 — обязательно. Стоило мне часа отладки. Передал int16 — на выходе пустая строка, ни ошибки, ни предупреждения. GigaAM молча глотает неправильный формат. Нужен строго numpy.float32.

Warmup. Первые 2 вызова ONNX Runtime в 3–5 раз медленнее. Прогреваю тихим аудио при запуске.

Пунктуация из коробки. Модель v3-e2e-rnnt расставляет точки и запятые сама.

T5 не нужна. Ирония: при 3.3% WER та самая T5, которую я так долго допиливал, только мешает — «исправляет» правильный текст.

Что НЕ сработало с GigaAM

  • Предобработка аудио (pre‑emphasis, нормализация) — WER не меняется, модель и так робастная.

  • Ансамблевое голосование — модели ошибаются в одних и тех же местах. Голосование трёх согласных дурачков не даёт одного умного ответа.

  • Английский — пустой вывод или мусор. GigaAM — только русский.

Мультиязычность: гибридная схема

Для английского — Whisper large‑v3-turbo на GPU:

Конфигурация

WER (рус.)

WER (англ.)

Среднее

Снижение

GigaAM + Whisper GPU

3.3%

7.3%

5.6%

82.8%

Whisper auto‑detect

7.9%

7.3%

7.5%

76.9%

Режим auto: GigaAM для русского, Whisper для английского, язык определяется автоматически.

Итоговая архитектура

Микрофон

→ Silero VAD (ONNX)

→ GigaAM (рус.) / Whisper (мульт.) / Vosk (лёгкий)

→ Smart T5 (если не GigaAM)

→ Ctrl+V

asr_backend: «gigaam» # или «whisper», «vosk», «auto»

gigaam_model: «v3-e2e‑rnnt» # лучшее качество + скорость

use_t5_correction: false # не нужна для GigaAM

Выводы

  1. Специализация бьёт универсальность. 700K часов русской речи vs 680K часов всех языков — результат: 3.3% vs 7.9%, без GPU.

  2. Параметры декодинга не спасут слабый энкодер. Beam search и temperature для Whisper base — строго 0% улучшения.

  3. Языковая коррекция — осторожно. Нефильтрованная T5 ухудшает результат. Пословная фильтрация помогает слабым ASR, но бесполезна для сильных.

  4. Чанкование вредит. Целое аудио распознаётся лучше суммы частей — модели теряют контекст.

  5. RNNT > CTC. На сложных текстах — пятикратная разница (2.6% vs 13.2%).

  6. Ансамбли однотипных моделей бесполезны. Для ансамбля нужны разные ошибки.

Ограничения

  • Тестовый датасет — TTS, не живая запись. На реальном аудио с шумами WER будет выше.

  • Пять фрагментов — мало для академической строгости, но достаточно для выбора движка.

  • GigaAM — только русский. Для мультиязычных задач нужен гибрид.

Скрытый текст

Код проекта на GitHub (MIT). Если воспроизведёте бенчмарки на живом аудио — буду признателен.