Три ловушки в VOSK: моя программа удаляла корректные модели после скачивания

Срез: vosk 0.3.45 (
libvosk.dllпод Windows x64), реестр моделейalphacephei.com/vosk/modelsна 26 сентября 2026. Имена моделей и их состав меняются — если читаете это позже, сверяйтесь со страницей VOSK.
Зачем вообще офлайн
Мне нужно было приложение, которое распознаёт речь и никуда не отправляет аудио. Не «не отправляет по умолчанию» и не «у нас есть офлайн‑режим» — а физически не может, потому что сети нет в процессе.
Когда я начал искать, выяснилось, что выбор небольшой:
Облачные сервисы (Google, Яндекс, Whisper через API) — качество отличное, но чужой сервер слышит голос. Для диктовок и встреч это неприемлемо.
Whisper локально — качество выше, но даже самые компактные модели весят сотни мегабайт, а тяжёлые — гигабайты, и тянется вместе с ними PyTorch. Развернуть это на голом CPU оказалось дороже, чем я рассчитывал.
VOSK — модели от 50 МБ до 1.8 ГБ, работает на CPU, распознаёт в реальном времени. Качество скромнее, но для диктовки, заметок и субтитров достаточно.
Сравнение тут не про качество распознавания, а про стоимость запуска на обычной машине без GPU. Против облака у VOSK выигрыш очевиден, против Whisper — вопрос спорный, и решала простота установки.
Я выбрал VOSK и написал вокруг него программу: VoxSpica — распознавание с микрофона в реальном времени и из файлов, история в SQLite, 33 языка распознавания, 9 языков интерфейса, Windows.
Дальше — три вещи, на которые я потерял больше времени, чем на весь остальной код.
Реестр языков собирается руками
Первый вопрос: откуда взять список моделей. На странице VOSK они перечислены, но шаблона нет:
vosk-model-small-ru-0.22
vosk-model-ru-0.42
vosk-model-small-en-us-0.15
vosk-model-en-us-0.22
vosk-model-pt-fb-v0.1.1-20220516_2113
Пятый пример — реальное имя португальской модели. Здесь нет ни версии, ни шаблона: fb, точка, дата, время. Любой генератор regex здесь ломается.
Поэтому реестр — это плоский словарь, собранный руками:
# VOSK model registry: language -> {size: archive_name | None}.
# Names taken from the page https://alphacephei.com/vosk/models.
# Each language has its own versions — a fixed template will not fit them.
MODELS: dict[str, dict[str, Optional[str]]] = {
"ru": {"small": "vosk-model-small-ru-0.22", "large": "vosk-model-ru-0.42"},
"pt": {
"small": "vosk-model-small-pt-0.3",
"large": "vosk-model-pt-fb-v0.1.1-20220516_2113",
},
# ...
}
None означает «в реестре VOSK такого размера нет». И таких языков 13 из 33: четыре только с большой моделью (ar, br, el-gr, tl-ph), девять только с маленькой (ca, cs, eo, ko, pl, sv, te, tr, uz). Остальные 20 имеют обе.
Это не теория, это баг, который у меня был: интерфейс показывал кнопку «Скачать (small)» для арабского, где маленькой модели не существует. Клик — и ошибка. Теперь размеры, которых нет, просто не предлагаются:
def available_sizes(lang: str) -> list[str]:
"""Which sizes actually exist for a language in the official VOSK registry."""
entry = MODELS.get((lang or "").lower()) or {}
return [size for size in SIZES if entry.get(size)]
Четыре языка, на которых всё и сломалось
Полная таблица на 33 языка в конце статьи. Здесь — только те, где валидатор врал, потому что каждый закрыл отдельный регресс:
Модель | Размер | Раскладка | Что проверка утверждала |
|---|---|---|---|
| 1,8 ГБ | современная, | не хватает |
| ~1,8 ГБ | современная, | то же |
| ~50 МБ | плоская, | не хватает всех пяти |
| ~50 МБ | плоская | то же |
Плюс vosk-model-br-0.8 — тоже плоская, и тоже только с большой моделью.
Ловушка 1: имя файла графа
Самая дорогая из трёх. 26 сентября 2026, коммит c54b6be.
После скачивания архива программа проверяла, что модель распаковалась целиком. Список обязательных файлов был таким:
REQUIRED_MODEL_FILES = (
"am/final.mdl",
"conf/mfcc.conf",
"conf/model.conf",
"graph/Gr.fst",
"graph/HCLr.fst",
"graph/phones/word_boundary.int",
)
Так выглядит логика проверки в исходном виде — после распаковки:
model = model_dir / archive_name
missing = validate_model_dir(model)
if missing:
shutil.rmtree(model, ignore_errors=True)
raise SystemExit("Модель распаковалась не полностью, недостают: ...")
Проблема: Gr.fst и HCLr.fst — имена старых моделей (small-ru-0.22, small-uk-v3-nano). У новых один файл:
ru-0.42, en-us-0.22, de-0.21, uk-v3, cn, ja, el-gr → graph/HCLG.fst
Итог: я нажимал «Скачать большую модель» для русского. Программа скачивала 1.8 ГБ, распаковывала, объявляла модель неполной и удаляла её. Пользователь получал «недостают graph/Gr.fst, graph/HCLr.fst» — и оставался без модели. Повторить можно сколько угодно раз, результат тот же.
Приём, которым я это нашёл, пригодился ещё раз, поэтому опишу: качать модели, чтобы проверить, я не стал. Достаточно одного Range‑запроса — сервер возвращает в Content-Range полный размер архива, а по HEAD видно доступность:
HEAD https://alphacephei.com/vosk/models/vosk-model-ru-0.42.zip
→ 200, Content-Length: 1837429010
GET (тот же URL), Range: bytes=0-0
→ 206, Content-Range: bytes 0-0/1837429010
Второй запрос отдаёт одну полосу данных и длину всего архива. Умножать это на все модели реестра и качать по гигабайту ради проверки не нужно.
Так я перебрал весь официальный список архивов и выяснил, какие раскладки в реестре вообще встречаются. Оказалось — две (об этом следующая ловушка).
Фикс — проверять граф как «любой .fst» в каталоге графа:
REQUIRED_MODEL_GLOBS = ("graph/*.fst",)
Заодно из обязательных ушёл conf/model.conf — это необязательные параметры декодирования, их нет в части моделей вообще.
Регресс закрыт параметризованным тестом:
@pytest.mark.parametrize("graph", ["Gr.fst", "HCLr.fst", "HCLG.fst"])
def test_validate_accepts_any_graph_layout(self, tmp_path, graph):
"""Имя файла графа у моделей разное — проверка не должна его знать.
Регресс: требовались `Gr.fst` и `HCLr.fst`, и нормальная большая
модель ru-0.42 (в ней один `HCLG.fst`) объявлялась неполной —
скачанные 1,8 ГБ удалялись, модель оставалась недоступной.
"""
model = self._make_model(tmp_path, "vosk-model-x", graph)
assert models.validate_model_dir(model) == []
И второй тест, чтобы проверка не ослабла до нуля — без графа модель нерабочая и должна удаляться по‑прежнему.
Ловушка 2: две раскладки
На следующий день, 27 сентября, коммит b6e63e7. Выяснилось, что у VOSK две раскладки, и VOSK грузит обе:
современная:
am/final.mdl
conf/mfcc.conf
graph/phones/word_boundary.int
graph/HCLG.fst
старая плоская — те же файлы прямо в корне модели:
final.mdl
mfcc.conf
word_boundary.int
Gr.fst + HCLr.fst
Плоские — это vosk-model-small-pt-0.3, vosk-model-small-tr-0.3, vosk-model-br-0.8. То есть португальский маленький, турецкий маленький и бразильский. Установка португальской выглядела так:
Модель распаковалась не полностью, недостают: am/final.mdl, conf/mfcc.conf, graph/phones/word_boundary.int, graph/Gr.fst, graph/HCLr.fst
Все пять пунктов отсутствовали — потому что проверка знала ровно одну раскладку. И модель удалялась. Тут я впервые понял, что строгая проверка вреднее никакой: она не защищает, а выкидывает рабочее.
Фикс — для каждой части два кандидата, и годен любой:
REQUIRED_MODEL_PARTS: tuple[tuple[str, ...], ...] = (
("am/final.mdl", "final.mdl"),
("conf/mfcc.conf", "mfcc.conf"),
("graph/phones/word_boundary.int", "word_boundary.int"),
("graph/*.fst", "*.fst"),
)
Обратите внимание на две вещи. Первое: *.fst вместо конкретного имени — это исправление ловушки 1 внутри структуры, а не рядом с ней. Второе: то, что каталог вообще является моделью, тоже определяется «где угодно»:
#: What makes a directory a model: the acoustic model, in either layout.
MODEL_MARKERS = ("am/final.mdl", "final.mdl")
Без этого плоские модели были ещё и не видны в списке, и не удалялись.
Чего требовать нельзя
Третий случай я поймал не через падение, а при разборе того, почему падали остальные: graph/words.txt отсутствует в части официальных архивов. Если включить его в обязательные, модель удаляется, ссылаясь на файл, которого нет и не должно быть.
Так же и conf/model.conf, которого нет в плоских моделях.
Оба просто не входят в REQUIRED_MODEL_PARTS. И тут я должен быть честен относительно своего же кода: теста на words.txt у меня нет. Две другие ловушки закрыты тестами, эта — только комментарием и записью в README. Это не «забыл написать», это «защищено словом, а не кодом», и я понимаю, что слово — слабее.
Чего проверка не делает
Скачивание идёт во временный .part, три попытки, сверка размера, проверка zip через testzip(), потом проверка модели и rmtree при неудаче — чтобы битый файл никогда не остался под финальным именем и не выглядел как установленная модель.
Но официальных хешей VOSK не публикует — ни на странице моделей, ни в отдельном файле. Поэтому сверка размера и CRC внутри архива — это максимум, доступный без побочных источников, и это не защита от подмены. Если сервер VOSK отдаст другой файл того же размера, программа его примет.
Условия, при которых проверка становится настоящей, я вижу такие:
хеши ведутся в самом реестре, рядом с именем архива — тогда сверка ложится в ту же строку данных и стоит ноль усилий;
для зеркала, которому доверяешь, — сверять со своим
SHA256SUMS.txt, как это уже сделано для моих сборок.
Отдельно про кеш: если модель уже скачана ранее и лежит на диске, повторная сверка хеша её не покрывает — файл мог быть повреждён уже после установки.
Скачивание целиком идёт во временный .part, три попытки, сверка размера, проверка zip через testzip(), потом проверка модели и rmtree при неудаче — чтобы битый файл никогда не остался под финальным именем и не выглядел как установленная модель.
Что стоит знать про загрузку модели
Раз уж зашла речь. vosk.Model(...) — блокирующий вызов C, прервать его нельзя. Для большой модели это 60–90 секунд молчания. Таймаут поставить нельзя, а «программа зависла» — точное впечатление, которое получит пользователь. Поэтому поток, который раз в секунду сообщает, что процесс жив, и печатает прогресс каждые 10 секунд.
В GUI модель грузится фоном сразу после показа окна, с задержкой 300 мс. И с auto_download=False намеренно:
auto_download=False for the warm-up: a model that is not installed must
NOT silently download 1.8 GB when the application starts.
Отдельная мелочь, стоившая времени: libvosk.dll в сборке 0.3.45 под Windows не открывает файлы по пути с не‑ASCII символами. Проект мой лежит в папке C:\work\диктофон, и путь с кириллицей приводил к ошибке загрузки модели.
Уточню: это наблюдение про конкретную сборку под Windows, а не документированное свойство VOSK вообще — проверить на другой платформе я не мог. И это обход, а не решение: junction в ASCII‑каталог лечит симптом, оставляя причину внутри библиотеки. Если у вас путь ASCII — проблемы не будет.
Про запятые — честно про границы
VOSK возвращает текст без знаков препинания. Я восстанавливаю эвристикой капитализацию, точку на границе сегмента и точку в конце. Запятые не восстанавливаются вообще — без синтаксического разбора любое правило даёт «Как, дела».
Точка расширения готова:
class PostProcessor(Protocol):
"""Post-processing interface. Implement it — and plug it in via set_backend."""
def apply(self, text: str, segments: Sequence[str] = ()) -> str: ...
Нейросетевая модель пунктуации (vosk-recasepunc, 1.6 ГБ) туда подключается, но её нет — не хватило времени, а не архитектуры.
Что получилось
small | large | |
|---|---|---|
Размер | ~50 МБ | ~1.8 ГБ |
RAM | ~0.3 ГБ | несколько ГБ |
Загрузка | секунды | 60–90 с |
Русский | хороший | заметно лучше |
Что из этого стоит забрать
Три правила, которые я бы написал себе за месяц до всех этих ловушек:
Имя файла не проверяйте — проверяйте класс файла. Имя графа у VOSK менялось, и будет меняться. Спрашивайте «есть ли
*.fst», а не «есть лиHCLG.fst».Каждая часть — список кандидатов, годен любой. Если формат допускает две раскладки, валидатор обязан знать обе, иначе он удаляет то, что работает.
Перед тем как требовать файл, убедитесь, что он есть в реестре. Часть официальных архивов упакована иначе, и файл, которого там нет, — законное отсутствие, а не недокачанная модель.
И общее: если проверка удаляет то, что стоило часов скачивания, — сначала спросите, откуда взялся ваш список обязательных файлов, а не ужесточайте проверку. Обе мои ловушки выросли из ответа на этот вопрос.
Что дальше
Подпись кода — до неё Windows SmartScreen показывает красный экран при первом запуске. Это единственное, что мешает воспринимать программу всерьёз.
Если хотите обсудить — https://github.com/alex37529/voxspica
Приложение: реестр моделей на дату среза
Раскладка проверена на четырёх моделях, помеченных ниже. Для остальных она предполагается по версии — если в реестре встретится новая плоская раскладка, валидатор надо расширить.
Код | small | large |
|---|---|---|
| — |
|
| — |
|
|
| — |
|
|
|
|
| — |
|
|
|
| — |
|
|
|
|
|
|
|
|
| — |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| — |
|
|
|
|
|
|
|
|
|
|
| — |
|
|
|
|
|
|
|
| — |
|
| — |
|
|
|
| — |
|
|
| — |
|
|
|
|
| — |
|
|
|
33 языка, 53 архива. Только large: ar, br, el-gr, tl-ph. Только small: ca, cs, eo, ko, pl, sv, te, tr, uz. Остальные 20 имеют обе.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.