The Jerusalem PostFormer hostage Romi Gonen reveals footage from moments she was taken to GazaPunchArmy confirms killing of ISWAP leader in OndoESPN'If we bunt, we win': How the Rays developed a secret weaponBollywood HungamaSanjay Leela Bhansali and Alia Bhatt planning to revive Inshallah after Love And War: REPORTUN NewsOne day behind bars: Students and incarcerated women break prison stigma in ThailandInquirerEl Niño to bring drought to all provinces by March 2027 – DOSTSky TG24Baby Gang resta in carcere, gip respinge domiciliari: "È in rete criminale internazionale"Observador DesportoMoedas: "As lutas ideológicas nunca deram casa a ninguém"Il Fatto Quotidiano“Dal ‘ti amo’ all’addio in una settimana, questo è lui”: Elon Musk lascia Shivon Zilis all’improvviso e lei pubblica le loro chat privateRTL BoulevardKeizer Naruhito woont openingsceremonie Japans parlement bijOnetDonald Tusk zapytany o list żelazny dla posła PiS. Premier wskazał, co było błędem7sur7“Malgré la maladie, il a continué à créer”: le rappeur français Prince Waly est mort à 34 ans
The Daily Newsstand · Free, Always
Monday, October 5, 2026

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

Translate

Срез: 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 языка в конце статьи. Здесь — только те, где валидатор врал, потому что каждый закрыл отдельный регресс:

Модель

Размер

Раскладка

Что проверка утверждала

vosk-model-ru-0.42

1,8 ГБ

современная, graph/HCLG.fst

не хватает Gr.fst, HCLr.fst

vosk-model-en-us-0.22

~1,8 ГБ

современная, graph/HCLG.fst

то же

vosk-model-small-pt-0.3

~50 МБ

плоская, Gr.fst в корне

не хватает всех пяти

vosk-model-small-tr-0.3

~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 с

Русский

хороший

заметно лучше

Что из этого стоит забрать

Три правила, которые я бы написал себе за месяц до всех этих ловушек:

  1. Имя файла не проверяйте — проверяйте класс файла. Имя графа у VOSK менялось, и будет меняться. Спрашивайте «есть ли *.fst», а не «есть ли HCLG.fst».

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

  3. Перед тем как требовать файл, убедитесь, что он есть в реестре. Часть официальных архивов упакована иначе, и файл, которого там нет, — законное отсутствие, а не недокачанная модель.

И общее: если проверка удаляет то, что стоило часов скачивания, — сначала спросите, откуда взялся ваш список обязательных файлов, а не ужесточайте проверку. Обе мои ловушки выросли из ответа на этот вопрос.

Что дальше

Подпись кода — до неё Windows SmartScreen показывает красный экран при первом запуске. Это единственное, что мешает воспринимать программу всерьёз.

Если хотите обсудить — https://github.com/alex37529/voxspica

Приложение: реестр моделей на дату среза

Раскладка проверена на четырёх моделях, помеченных ниже. Для остальных она предполагается по версии — если в реестре встретится новая плоская раскладка, валидатор надо расширить.

Код

small

large

ar

—

vosk-model-ar-mgb2-0.4

br

—

vosk-model-br-0.8 — плоская

ca

vosk-model-small-ca-0.4

—

cn

vosk-model-small-cn-0.22

vosk-model-cn-0.22

cs

vosk-model-small-cs-0.4-rhasspy

—

de

vosk-model-small-de-0.15

vosk-model-de-0.21

el-gr

—

vosk-model-el-gr-0.7

en-in

vosk-model-small-en-in-0.4

vosk-model-en-in-0.5

en-us

vosk-model-small-en-us-0.15

vosk-model-en-us-0.22

eo

vosk-model-small-eo-0.42

—

es

vosk-model-small-es-0.42

vosk-model-es-0.42

fa

vosk-model-small-fa-0.42

vosk-model-fa-0.42

fr

vosk-model-small-fr-0.22

vosk-model-fr-0.22

gu

vosk-model-small-gu-0.42

vosk-model-gu-0.42

hi

vosk-model-small-hi-0.22

vosk-model-hi-0.22

it

vosk-model-small-it-0.22

vosk-model-it-0.22

ja

vosk-model-small-ja-0.22

vosk-model-ja-0.22

ka

vosk-model-small-ka-0.42

vosk-model-ka-0.42

ko

vosk-model-small-ko-0.22

—

ky

vosk-model-small-ky-0.42

vosk-model-ky-0.42

kz

vosk-model-small-kz-0.42

vosk-model-kz-0.42

nl

vosk-model-small-nl-0.22

vosk-model-nl-spraakherkenning-0.6

pl

vosk-model-small-pl-0.22

—

pt

vosk-model-small-pt-0.3 — плоская

vosk-model-pt-fb-v0.1.1-20220516_2113

ru

vosk-model-small-ru-0.22

vosk-model-ru-0.42

sv

vosk-model-small-sv-rhasspy-0.15

—

te

vosk-model-small-te-0.42

—

tg

vosk-model-small-tg-0.22

vosk-model-tg-0.22

tl-ph

—

vosk-model-tl-ph-generic-0.6

tr

vosk-model-small-tr-0.3 — плоская

—

uk

vosk-model-small-uk-v3-nano

vosk-model-uk-v3

uz

vosk-model-small-uz-0.22

—

vn

vosk-model-small-vn-0.4

vosk-model-vn-0.4

33 языка, 53 архива. Только large: ar, br, el-gr, tl-ph. Только small: ca, cs, eo, ko, pl, sv, te, tr, uz. Остальные 20 имеют обе.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.