Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

GigaSTT — книга рецептов

Сценарные рецепты для gigastt — локального сервера русской речи-в-текст на базе GigaAM v3. Каждая глава устроена одинаково: сценарий → предпосылки → рецепт → проверка результата → частые ошибки → ссылки.

Это поваренная книга, а не справочник. Канонические справочники остаются в docs/ — книга ссылается на них, а не дублирует.

Документировано против gigastt 2.14.x. В install-скриптах предпочитайте резолв latest-тега (gh api …/releases/latest), а не устаревшие миноры.

Я хочу…

ЦельВремяГлава
Первый транскрипт на этой машине~5–15 минНачало работы
Пакет папки / watch / async jobs~15–30 минCLI и пакетная обработка
АТС / Opus / raw telephony; стерео-спикеры~20 минТелефония и VoIP
Live-субтитры или voice-bot по WebSocket~30 минСтриминг по WebSocket
macOS / Electron / mobile приложение~30–60 минДесктоп и встраивание
Прод с метриками, апгрейдами, hot-reload модели~45 минРазвёртывание и эксплуатация
Выбрать голову / INT8 / GPU / pool / punct / hotwords~20 минМодели и бэкенды
Метки спикеров на mono-встречах(в гл. 3)Телефония — диаризация
Расшифровать HTTP/WS код ошибки~2 минПриложение A — Коды ошибок
Air-gapped / offline~30 минПриложение B — Offline-чеклист
Установка на Windows~10 минНачало работы — Windows

Главы

  1. Начало работы — установка (macOS/Linux/Windows/Docker/air-gap), первая транскрибация. Новичок · ~5–15 мин
  2. CLI и пакетная обработка — CLI, batch и watch. Новичок · ~15–30 мин
  3. Телефония и VoIP — G.711/G.722/Opus, АТС, stereo split и диаризация. Средний · ~20 мин
  4. Стриминг по WebSocket — partials, VAD-эндпоинтинг, session caps. Средний · ~30 мин
  5. Десктоп и встраивание — Swift/SPM, sidecar, Electron, UniFFI. Средний · ~30–60 мин
  6. Развёртывание и эксплуатация — прод, мониторинг, апгрейды, admin reload. Ops · ~45 мин
  7. Модели и бэкенды — головы, квантование, EP, пунктуация/ITN, hotwords. Средний · ~20 мин

Приложения

Английская версия — каноническая; эта книга зеркалирует её глава в главу.

Карта документации

Полный инвентарь документации репозитория: что в каждом файле и где он живёт.

Справочники (канонические — в книге не дублируются)

ФайлСодержимоеСудьба
docs/api.mdСправочник HTTP / WebSocket / SSE APIостаётся
docs/asyncapi.yamlAsyncAPI-схема WS-протоколаостаётся
docs/openapi.yamlOpenAPI-схема REST APIостаётся
docs/cli.mdСправочник CLI (serve, download, transcribe, …)остаётся
docs/architecture.mdОбзор архитектурыостаётся
docs/benchmarks.mdИзмерения WER / RTFостаётся
docs/privacy.mdПриватность и потоки данныхостаётся
docs/troubleshooting.mdТаблица «симптом → причина → решение»остаётся
docs/observability/Алерты Prometheus и дашборд Grafanaостаётся

Гайды (актуальные)

ФайлСодержимоеСудьба
docs/deployment.mdReverse proxy, TLS, systemd, Dockerостаётся
docs/quickstarts.mdКвикстарты по встраиванию (FFI-биндинги)остаётся
docs/runbook.mdРанбук оператора для productionостаётся
docs/self-hosted-runner.mdSelf-hosted CI-раннеры для бенчмарковостаётся
docs/embedding-packaging.mdЛинковка и упаковка onnxruntimeостаётся
docs/verifying-releases.mdПроверка релизных артефактовостаётся
docs/ane-backend.mdЗаметка о бэкенде ANE (Core ML) — живой код --features aneостаётся
docs/candle-backend.mdЗаметка о бэкенде Candle/Metal — живой код --features candleостаётся
sdks/go/README.mdGo SDK для WebSocket-клиентаостаётся
sdks/js/README.mdTypeScript SDK для WebSocket-клиентаостаётся

Исторические (в архиве)

Завершённые дизайн-документы и планы, сохранённые для истории в docs/archive/:

ФайлСодержимоеСудьба
docs/archive/candle-metal-backend-plan.mdПлан реализации бэкенда Candle/Metal (завершён)в архиве
docs/archive/candle-metal-backend-design.mdДизайн бэкенда Candle/Metal (замещён поставленным бэкендом)в архиве

Правила для контрибьюторов

  • Книга содержит рецепты; docs/api.md, docs/cli.md и схемы AsyncAPI/OpenAPI остаются каноническими справочниками. Ссылайтесь на них — не копируйте содержимое.
  • Каждая команда и пример в главе должны быть проверены перед мерджем.
  • Внутри книги (глава ↔ глава, глава ↔ intro) — только относительные ссылки на .md, они работают и на GitHub, и в собранной книге. Ссылки из книги на файлы репозитория (docs/, crates/, …) — только абсолютные GitHub-URL, относительные на опубликованном сайте ведут в 404. Никакой mdBook-специфичной шаблонизации.
  • Новые главы следуют структуре _template.md.
  • Английская версия — каноническая. Русская книга (docs/workbook/ru/) зеркалирует её с идентичными именами файлов; обе версии правятся в одном PR.
  • Когда фича меняет документируемую поверхность (CLI-флаги, коды ошибок, аудиоформаты), обновляйте главу, оглавление книги SUMMARY.md и канонические справочники в том же PR — и держите docs-drift gate зелёным: python3 scripts/check-docs-drift.py (пока advisory в CI; сверяет CLI-флаги, коды ошибок WS, аудиоформаты, оглавления mdBook, паритет EN/RU и относительные ссылки с кодом).

Начало работы

Сценарий

Вы никогда не запускали gigastt и хотите получить рабочую локальную транскрибацию примерно за пять минут: установить бинарник, скачать модель GigaAM v3, транскрибировать первый аудиофайл — на macOS, Linux или в Docker. Эта глава покрывает весь путь; другие документы для этого не понадобятся.

Предпосылки

  • Диск: ~1,5 ГБ свободно (модель + инструменты). «Лёгкий» путь с --prequantized требует при установке всего ~250 МБ.
  • RAM: ~800 МБ свободно при дефолтном --pool-size 2 (~400 МБ на сессию).
  • Сеть (если вы не идёте по рецепту для замкнутого контура): доступ к huggingface.co (полная модель) или github.com (предквантизованный бандл).
  • Аудиофайл для транскрибации — WAV, M4A, MP3, OGG или FLAC. Подойдёт любая короткая запись русской речи.
  • Только для cargo install (сборка из исходников): Rust 1.88+ и protoc в PATH (brew install protobuf / apt install protobuf-compiler).

Выберите один рецепт ниже — macOS, Linux, Windows, Docker или замкнутый контур, — затем один раз прочитайте Выбор головы распознавания и Дорогой первый запуск.

Рецепт: macOS (Homebrew)

Homebrew — самый быстрый путь на Apple Silicon (tap содержит бинарник с CoreML). На Intel Mac используйте cargo install gigastt — см. требование про protoc в рецепте для Linux.

brew tap ekhodzitsky/gigastt https://github.com/ekhodzitsky/gigastt
brew install gigastt

# Скачать модель (~850 МБ FP32 с HuggingFace, затем разовая ~2-минутная
# INT8-квантизация — см. «Дорогой первый запуск» ниже):
gigastt download

# Транскрибировать первый файл:
gigastt transcribe recording.wav

Проверка: последняя команда печатает распознанный текст в stdout, например:

$ gigastt transcribe recording.wav
Привет, как дела?

а ls ~/.gigastt/models/ показывает v3_rnnt_encoder_int8.onnx, v3_rnnt_decoder.onnx, v3_rnnt_joint.onnx и v3_vocab.txt.

Рецепт: Linux (готовый бинарник или cargo)

Вариант A — готовый бинарник (без Rust-инструментария и protoc). Каждый релиз публикует tarball’ы для x86_64-unknown-linux-gnu и aarch64-unknown-linux-gnu:

# Определить тег последнего релиза (или задайте TAG=v2.14.1 вручную):
TAG=$(curl -fsSL https://api.github.com/repos/ekhodzitsky/gigastt/releases/latest \
      | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')
VER=${TAG#v}

curl -fLO "https://github.com/ekhodzitsky/gigastt/releases/download/${TAG}/gigastt-${VER}-x86_64-unknown-linux-gnu.tar.gz"
curl -fLO "https://github.com/ekhodzitsky/gigastt/releases/download/${TAG}/gigastt-${VER}-x86_64-unknown-linux-gnu.tar.gz.sha256"
sha256sum -c "gigastt-${VER}-x86_64-unknown-linux-gnu.tar.gz.sha256"

tar xf "gigastt-${VER}-x86_64-unknown-linux-gnu.tar.gz"
sudo install -m 0755 gigastt /usr/local/bin/gigastt

(На ARM64 замените x86_64-unknown-linux-gnu на aarch64-unknown-linux-gnu. Homebrew на Linux x86_64 — brew install gigastt после tap из рецепта для macOS — тоже работает.)

Вариант B — cargo (любая платформа, нужны Rust 1.88+ и protoc):

sudo apt install protobuf-compiler   # Debian/Ubuntu; пропустите, если protoc есть
cargo install gigastt

Затем скачайте модель «лёгким» способом — предквантизованный INT8-бандл ~225 МБ из закреплённого GitHub Release (без ~850 МБ FP32-загрузки и без ~2-минутной квантизации на устройстве; удобно и тогда, когда HuggingFace недоступен, а GitHub — нет):

gigastt download --prequantized
gigastt transcribe recording.wav

Проверка: gigastt transcribe recording.wav печатает распознанный текст в stdout, а ls ~/.gigastt/models/ показывает файлы модели v3_rnnt_*.

Рецепт: Windows (готовый бинарь)

Каждый релиз публикует tarball x86_64-pc-windows-msvc (CPU). PowerShell 5.1+ / Windows 10+ уже содержат tar и curl:

$rel = Invoke-RestMethod https://api.github.com/repos/ekhodzitsky/gigastt/releases/latest
$TAG = $rel.tag_name          # например v2.14.1
$VER = $TAG.TrimStart('v')
$asset = "gigastt-$VER-x86_64-pc-windows-msvc.tar.gz"
$base = "https://github.com/ekhodzitsky/gigastt/releases/download/$TAG"

Invoke-WebRequest "$base/$asset" -OutFile $asset
Invoke-WebRequest "$base/$asset.sha256" -OutFile "$asset.sha256"
# Опциональная проверка (файл хеша: "HASH  filename"):
$expected = (Get-Content "$asset.sha256").Split()[0]
$actual = (Get-FileHash $asset -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $expected.ToLower()) { throw "SHA-256 mismatch" }

tar xf $asset
# Положите gigastt.exe в PATH или вызывайте по полному пути:
.\gigastt.exe download --prequantized
.\gigastt.exe transcribe recording.wav

Каталог моделей по умолчанию — %USERPROFILE%\.gigastt\models\. Первый serve слушает только loopback (127.0.0.1:9876), пока не передадите --bind-all.

Проверка: .\gigastt.exe transcribe recording.wav печатает текст; после .\gigastt.exe serve отвечает http://127.0.0.1:9876/health.

Рецепт: Docker

Готовые мультиархитектурные образы (amd64 + arm64) публикуются в GHCR для каждого релиза; теги -cuda содержат CUDA-вариант:

docker pull ghcr.io/ekhodzitsky/gigastt:latest   # в продакшене зафиксируйте :<version>

docker run -d --name gigastt \
  -p 127.0.0.1:9876:9876 \
  -v gigastt-models:/home/gigastt/.gigastt/models \
  ghcr.io/ekhodzitsky/gigastt:latest

Именованный volume сохраняет модель между перезапусками контейнера; без него контейнер будет заново скачивать ~850 МБ при каждом пересоздании. При первом старте контейнер скачивает модель и квантизует её — порт поднимается сразу, но инференс доступен, только когда /ready становится «зелёным»:

# Дождаться загрузки модели (503, пока идёт инициализация):
until curl -sf http://127.0.0.1:9876/ready > /dev/null; do sleep 5; done

curl http://127.0.0.1:9876/health

Затем транскрибируйте файл с хоста (путь к файлу — на хосте: его читает curl, а не контейнер):

curl -F file=@recording.wav http://127.0.0.1:9876/v1/transcribe

Проверка: /health возвращает

{"status":"ok","model":"gigaam-v3-rnnt","variant":"rnnt","version":"2.14.1","punctuation":true,"itn":true}

(поле version отражает скачанный образ), а POST возвращает JSON с транскриптом:

{"text":"Привет, как дела?","words":[{"word":"привет","start":0.0,"end":0.4,"confidence":0.99}],"duration":1.2}

Рецепт: замкнутый контур (offline bundle)

Для машин без доступа в интернет каждый релиз публикует самодостаточный офлайн-бандл для каждой Linux-цели — бинарник + предквантизованная INT8-модель rnnt + модель пунктуации + systemd unit + установщик, — а также два Debian-пакета с тем же содержимым. Скачайте их на подключённой машине, перенесите и установите.

Поток с tarball’ом (любой дистрибутив):

# На подключённой машине (TAG/VER — см. рецепт для Linux):
curl -fLO "https://github.com/ekhodzitsky/gigastt/releases/download/${TAG}/gigastt-${VER}-offline-x86_64-unknown-linux-gnu.tar.gz"
curl -fLO "https://github.com/ekhodzitsky/gigastt/releases/download/${TAG}/gigastt-${VER}-offline-x86_64-unknown-linux-gnu.tar.gz.sha256"
sha256sum -c "gigastt-${VER}-offline-x86_64-unknown-linux-gnu.tar.gz.sha256"

# На целевой машине:
tar xf "gigastt-${VER}-offline-x86_64-unknown-linux-gnu.tar.gz"
cd "gigastt-${VER}-offline-x86_64-unknown-linux-gnu"
sudo ./install.sh                      # проверяет SHA256SUMS, ставит бинарник + модель + unit
sudo systemctl enable --now gigastt

Поток с Debian-пакетами: установите gigastt_<ver>_amd64.deb (бинарник + unit) вместе с gigastt-model-int8_<ver>_all.deb (тот же набор моделей), затем sudo systemctl enable --now gigastt.

В бандл намеренно не входят опциональные части — диаризация спикеров и головы e2e_rnnt / ml_ctc. Установленный unit работает с GIGASTT_OFFLINE=1, поэтому отсутствующая опциональная модель — это быстрая, понятная ошибка с точным путём, куда положить файл (скачайте его на подключённой машине командой gigastt download и скопируйте), а не сетевой таймаут. Полный список содержимого и проверка подписей — в packaging/offline/README-OFFLINE.md.

Проверка: curl http://127.0.0.1:9876/health возвращает {"status":"ok",...} с "model":"gigaam-v3-rnnt", а gigastt transcribe sample.wav --model-dir /usr/share/gigastt/models печатает текст (флаг нужен только при запуске без установки — systemd unit уже указывает на установленную модель).

Выбор головы распознавания

gigastt поставляется с четырьмя головами распознавания; --model-variant выбирает одну из них при download / serve / transcribe. Если флаг не указан, существующая директория модели используется как есть (автоопределение), а свежая установка по умолчанию получает rnnt.

ГоловаЯзыкиСтиль выводаКогда выбирать
rnnt (по умолчанию)русский«Голый» lowercase из акустической модели; регистр + пунктуация восстанавливаются автоскачиваемым проходом RuPunct, цифры — ITNПо умолчанию: минимальный WER на русской речи
e2e_rnntрусскийПунктуация / регистр / ITN «зашиты» в акустическую модельНужна одна самодостаточная модель без постобработки
ml_ctcru/en/kk/ky/uz«Голый» lowercase, без восстановленияСмешанная русско-английская (или kk/ky/uz) речь; лёгкий энкодер 220M
ml_ctc_largeru/en/kk/ky/uz«Голый» lowercase, без восстановленияМультиязычная речь, где точность важнее размера (энкодер 600M)

Головы ml_ctc* скачиваются сразу в предквантизованном INT8, поэтому шага квантизации у них нет. Смена головы после установки:

gigastt download --model-variant e2e_rnnt   # скачать другую голову
gigastt serve --model-variant e2e_rnnt      # и явно подать её

Цифры WER/RTF по каждой голове — в docs/benchmarks.md; более глубокий обзор моделей и бэкендов — в главе Модели и бэкенды.

Дорогой первый запуск

Самый первый gigastt download (или первый gigastt serve, который автоматически скачивает отсутствующую модель) делает две разовые вещи:

  1. Скачивает ~850 МБ FP32 ONNX-файлов с HuggingFace (с проверкой SHA-256, через промежуточный .partial и атомарное переименование).
  2. Квантизует энкодер в INT8 (~2 минуты, один раз), создавая энкодер ~225 МБ, который движок реально загружает. Следующие запуски используют его повторно.

Три рычага меняют цену:

  • gigastt download --prequantized — рекомендуемый короткий путь: скачать предквантизованный INT8-бандл ~225 МБ из закреплённого GitHub Release. Без FP32-загрузки, без локальной квантизации, без protoc. Обратите внимание: файлы берутся с github.com, а не с huggingface.co — полезно, когда один из двух хостов заблокирован.
  • gigastt download --skip-quantize (или GIGASTT_SKIP_QUANTIZE=1 у serve) — оставить FP32-энкодер и пропустить квантизацию. Движок тогда загружает FP32: инференс медленнее, а модель занимает в ~4 раза больше RAM. Только для отладки.
  • Ничего — просто позволить первому serve сделать всё самому. Порт поднимается сразу; /health отвечает 200 с "model":"loading", а /ready возвращает 503 {"reason":"initializing"}, пока модель не готова, поэтому клиенты должны ждать /ready, а не сам факт запущенного процесса.

Проверка результата

Сквозной чек-лист, работающий после любого из рецептов выше:

# 1. Файлы модели на месте:
ls ~/.gigastt/models/
#   v3_rnnt_encoder_int8.onnx  v3_rnnt_decoder.onnx  v3_rnnt_joint.onnx  v3_vocab.txt  ...

# 2. Офлайн-транскрибация работает (сервер не нужен):
gigastt transcribe recording.wav
#   → печатает распознанный текст в stdout

# 3. Сервер поднимается и сообщает загруженную голову:
gigastt serve &                      # Ctrl-C для остановки; по умолчанию http://127.0.0.1:9876
curl http://127.0.0.1:9876/ready     # 200, когда модель загружена
curl http://127.0.0.1:9876/health
#   {"status":"ok","model":"gigaam-v3-rnnt","variant":"rnnt","version":"...","punctuation":true,"itn":true}

# 4. REST-транскрибация работает:
curl -F file=@recording.wav http://127.0.0.1:9876/v1/transcribe
#   → {"text":"...","words":[...],"duration":N}

Частые ошибки

  • protoc не найден при cargo install или сборке из исходников — установите компилятор Protocol Buffers (brew install protobuf / apt install protobuf-compiler) или вовсе обойдитесь без инструментария, взяв готовый бинарник / Homebrew.
  • Первый serve «висит» несколько минут — это разовая загрузка модели + INT8-квантизация, а не зависание: /health в это время возвращает {"model":"loading"}. Подготовьте модель заранее командой gigastt download --prequantized и настройте клиентов на ожидание /ready.
  • Address already in use на порту 9876 — найдите, кто держит порт: lsof -nP -tiTCP:9876 -sTCP:LISTEN; убедитесь, что это gigastt (ps -p <pid> -o command=), затем kill <pid> (SIGTERM корректно завершает сессии) или запустите на другом порту с --port.
  • Скачивание модели падает или виснет (прокси, файрвол, HuggingFace недоступен) — повторите gigastt download; промежуточный .partial-файл делает повтор безопасным, а коды выхода различают причины (65 = контрольная сумма, 69 = сеть, 74 = диск). Если huggingface.co заблокирован, а github.com — нет, используйте gigastt download --prequantized; в полностью замкнутом контуре — офлайн-бандл. При ошибках диска проверьте права на ~/.gigastt/models/.
  • OOM или активный swap при старте — каждая сессия пула загружает свою копию энкодера (~400 МБ резидентно с INT8); дефолтный --pool-size 2 достигает ~790 МБ. На слабых машинах запускайте с --pool-size 1.

Полная таблица «симптом → причина → исправление» — в docs/troubleshooting.md.

Ссылки

CLI и пакетная обработка

Превращаем папку с записями в транскрипты: разовые прогоны через transcribe-batch, постоянно наблюдаемая папка-сброс через watch и асинхронная очередь через jobs API. Каждый рецепт можно скопировать и запустить как есть, и каждый заканчивается проверкой результата.

Сценарий

У вас есть каталог с аудио — записи колл-центра, выпуски подкастов, архив голосовых заметок — и на выходе нужны текстовые файлы. Иногда это разовая конвертация архива; иногда записи продолжают поступать, и конвейер должен работать без присмотра, повторять неудачные попытки и не спотыкаться о недокопированные файлы.

Предварительные требования

  • Установленный gigastt и скачанная модель (gigastt download) — см. Начало работы.
  • Папка с аудиофайлами: WAV, MP3, M4A, OGG, FLAC (вложенные папки сканируются рекурсивно).
  • Больше ничего: transcribe, transcribe-batch и watch — офлайн-команды, без сервера и сети.

Что важно знать до написания скриптов: каждый запуск CLI загружает модель (~1–2 с в тёплом состоянии). transcribe-batch амортизирует эту стоимость на всю папку, поэтому предпочитайте его shell-циклу for вокруг одиночных вызовов transcribe.

Рецепт: разовый прогон папки — transcribe-batch

Основной рабочий инструмент. Укажите входной и выходной каталоги:

gigastt transcribe-batch calls/ transcripts/

Команда рекурсивно сканирует calls/, транскрибирует каждый поддерживаемый аудиофайл в --pool-size воркеров (по умолчанию 2) и пишет transcripts/<имя>.txt и transcripts/<имя>.json на каждый входной файл (по умолчанию --format txt,json).

Прогон «как в продакшене» — больше форматов, больше воркеров и политика исходников:

gigastt transcribe-batch calls/ transcripts/ \
  --format txt,json,srt \
  --pool-size 4 \
  --retries 2 \
  --move-to calls/done/
  • --format — список через запятую из txt,json,md,srt,vtt; по одному выходному файлу на формат на входной файл.
  • --pool-size — параллельные воркеры; каждый стоит ~0,4 ГБ RAM (INT8- энкодер), поэтому масштабируйтесь по памяти, а не только по ядрам (см. рецепт про производительность ниже).
  • --retries — дополнительные попытки на файл с коротким бэкоффом (200 мс, 400 мс, …). По умолчанию 0 для batch и 2 для watch.
  • --move-to — перемещать каждый успешно транскрибированный исходник в указанный каталог. Файлы с ошибкой всегда остаются на месте. Каталог move-to исключается из сканирования, поэтому размещение его внутри входной папки (calls/done/) безопасно и является рекомендуемой раскладкой.
  • --delete-source — альтернатива --move-to: удалять исходники после успеха. Несовместимо с --move-to.

Как читать отчёт о прогоне. Каждый файл оставляет строку в логе, а прогон завершается сводкой:

INFO gigastt::batch: done /calls/alpha.wav processed=1 failed=0
WARN gigastt::batch: failed /calls/broken.mp3 error=invalid audio: Unsupported audio format: ...
INFO gigastt: batch finished processed=12 failed=1 skipped=0

Коды выхода (скриптуйте по ним, а не по тексту лога):

КодЗначение
0все файлы транскрибированы
1хотя бы один файл завершился ошибкой после всех попыток
130прервано Ctrl-C — файлы в работе завершаются, остальные пропускаются (skipped=N в сводке)

Пустая входная папка — не ошибка: логируется no audio files found, код выхода 0.

Проверка результата

gigastt transcribe-batch calls/ transcripts/ --move-to calls/done/
echo "exit code: $?"        # 0 = чистый прогон, 1 = были ошибки
ls transcripts/             # по <имя>.txt + <имя>.json на каждый исходник
ls calls/done/              # успешно обработанные исходники
ls calls/*.wav 2>/dev/null  # всё, что осталось, завершилось ошибкой — см. строки WARN

Рецепт: живая папка — watch

watch опрашивает каталог и транскрибирует файлы по мере их появления:

gigastt watch inbox/ transcripts/ --format txt,json --move-to inbox/done/

Чем отличается от transcribe-batch:

  • Бэклог пропускается. Файлы, уже лежащие в папке на момент запуска, регистрируются, но не транскрибируются (watching /inbox backlog=3 poll_ms=1000). Существующую гору сначала разгребите transcribe-batch (см. рецепт с обёрткой), а watch оставьте для новых поступлений.
  • Settle-защита. Файл ставится в работу только после того, как его размер + mtime не менялись на протяжении --settle-polls подряд опросов (по умолчанию 2) с интервалом --poll-interval-ms (по умолчанию 1000 мс). Запись, которую ещё копируют или пишут, никогда не будет подхвачена наполовину. Медленные сетевые шары → увеличьте оба параметра.
  • Изменения подхватываются. Перезапись файла сбрасывает settle-счётчик, и новая версия транскрибируется (изменение посреди транскрибации ставит файл в очередь повторно после завершения текущего прогона).
  • Ошибки «липкие». Файл, исчерпавший попытки (по умолчанию 2 для watch), помечается failed и не трогается, пока не изменится его содержимое.
  • Мягкая остановка. Ctrl-C прекращает постановку новых файлов, ждёт завершения текущих, печатает watch stopped processed=N failed=M и выходит с кодом 0 (1, если были ошибки).

Проверка результата

# терминал 1
gigastt watch inbox/ transcripts/ --move-to inbox/done/

# терминал 2
cp ~/recordings/sample.wav inbox/
# подождите settle-polls x poll-interval плюс время транскрибации, затем:
ls transcripts/sample.txt        # появился
ls inbox/done/sample.wav         # исходник заархивирован
# в терминале 1: Ctrl-C → "watch stopped processed=1 failed=0"

Рецепт: конвейер-обёртка для inbox (shell)

Стандартный сервис «папка-сброс»: аудио падает в inbox/, наружу выходят транскрипты, успехи архивируются в done/, ошибки собираются в failed/ и автоматически повторяются при следующем прогоне. Сохраните как transcribe-inbox.sh:

#!/usr/bin/env bash
# Usage: transcribe-inbox.sh [INBOX] [OUT]
set -uo pipefail

INBOX="${1:-inbox}"
OUT="${2:-transcripts}"
DONE="$INBOX/done"
FAILED="$INBOX/failed"
mkdir -p "$OUT" "$DONE" "$FAILED"

# Requeue previous failures for another attempt.
find "$FAILED" -maxdepth 1 -type f \
  \( -name '*.wav' -o -name '*.mp3' -o -name '*.m4a' -o -name '*.ogg' -o -name '*.flac' \) \
  -exec mv -n {} "$INBOX/" \;

gigastt transcribe-batch "$INBOX" "$OUT" --format txt,json --move-to "$DONE"
rc=$?

# Successes were moved to done/; whatever audio remains at the inbox top
# level failed all retries — collect it for inspection and future requeue.
if [ "$rc" -eq 1 ]; then
  find "$INBOX" -maxdepth 1 -type f \
    \( -name '*.wav' -o -name '*.mp3' -o -name '*.m4a' -o -name '*.ogg' -o -name '*.flac' \) \
    -exec mv -n {} "$FAILED/" \;
  echo "some files failed — collected in $FAILED" >&2
fi
exit "$rc"

Запуск по расписанию. Для большинства инбоксов достаточно cron:

*/15 * * * * /usr/local/bin/transcribe-inbox.sh /srv/stt/inbox /srv/stt/transcripts >> /var/log/stt-batch.log 2>&1

Вариант с systemd timer + service-юнитом вместо cron — см. Развёртывание и эксплуатация.

Watch + догоняющий batch. Обе команды складываются в постоянно работающий конвейер: watch обрабатывает поступающие файлы с малой задержкой, а периодический transcribe-batch разгребает стартовый бэклог и всё, что наблюдатель пометил failed. Обе команды учитывают одно и то же исключение --move-to, поэтому бэклог не обрабатывается дважды. Одна оговорка: догоняющий прогон, запущенный пока наблюдатель работает, может подхватить файл, который наблюдатель только что поставил в работу, но ещё не переместил — планируйте прогоны на тихие часы или примите, что файл изредка будет транскрибирован дважды (его выходные файлы просто перезапишутся).

# один раз и далее периодически (тихие часы): разобрать бэклог + повторить ошибки
./transcribe-inbox.sh /srv/stt/inbox /srv/stt/transcripts

# постоянно: новые поступления
gigastt watch /srv/stt/inbox /srv/stt/transcripts \
  --format txt,json --move-to /srv/stt/inbox/done/

Проверка результата

chmod +x transcribe-inbox.sh
cp ~/recordings/*.wav inbox/ && printf 'junk' > inbox/broken.mp3
./transcribe-inbox.sh inbox transcripts; echo "exit: $?"   # 1 — broken.mp3 failed
ls transcripts/   # транскрипты для хороших файлов
ls inbox/done/    # хорошие исходники заархивированы
ls inbox/failed/  # broken.mp3 собран здесь
./transcribe-inbox.sh inbox transcripts   # повторяет ошибочный, снова выходит с 1

Рецепт: конвейер с очередью — jobs API

watch покрывает одну машину с общей папкой. Переходите на jobs API, когда производители на других машинах, когда файлы настолько длинные, что держать синхронный HTTP-запрос неудобно, или когда нужны прогресс и отмена. Это тот же движок за in-memory FIFO-очередью внутри gigastt serve.

Jobs по умолчанию выключены. Включите их (и зарезервируйте инференс-слоты, чтобы очередь не душила WebSocket/REST-стриминг):

gigastt serve --enable-jobs --batch-pool-size 1

Отправка → опрос → получение:

# submit (принимает те же query-параметры, что и /v1/transcribe, напр. ?format=srt)
curl -s -X POST http://127.0.0.1:9876/v1/jobs \
  --data-binary @episode.wav
# {"job_id":"019f858a-...","status":"queued","created_at":1784651881.9}

# poll status
curl -s http://127.0.0.1:9876/v1/jobs/019f858a-...
# {"job_id":"...","status":"processing","processed_seconds":12.5,"percent":42}

# fetch the result once status is "done"
curl -s http://127.0.0.1:9876/v1/jobs/019f858a-.../result
# {"text":"...","words":[...],"duration":3512.4}

status проходит путь queuedprocessingdone | failed | cancelled. Запрос /result до done возвращает 409 job_not_finished. Другие эндпоинты: DELETE /v1/jobs/{id} отменяет queued/processing-задачу (204), а GET /v1/jobs/{id}/events стримит SSE-прогресс (data: {"type":"progress","percent":42,...}, затем done/failed).

Минимальный скрипт-драйвер:

#!/usr/bin/env bash
# submit-and-wait.sh AUDIO_FILE — submit a job and print its transcript.
set -euo pipefail
BASE="${GIGASTT_BASE:-http://127.0.0.1:9876}"

job=$(curl -sf -X POST "$BASE/v1/jobs" --data-binary "@$1" \
      | python3 -c 'import json,sys; print(json.load(sys.stdin)["job_id"])')
echo "job: $job" >&2

while true; do
  status=$(curl -sf "$BASE/v1/jobs/$job" \
           | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')
  case "$status" in
    done)               break ;;
    failed|cancelled)   echo "job $status" >&2; exit 1 ;;
  esac
  sleep 2
done

curl -sf "$BASE/v1/jobs/$job/result" | python3 -c 'import json,sys; print(json.load(sys.stdin)["text"])'

Поведение очереди, которое нужно учитывать:

  • --jobs-retry (по умолчанию 3) — повторяет только временные сбои: таймауты инференса и паники воркеров. Файл, который не декодируется, падает сразу, без ретраев.
  • --jobs-max (по умолчанию 100) — когда хранилище полно, submit возвращает 429 queue_full с Retry-After. Отступите и отправьте повторно.
  • --jobs-ttl-secs (по умолчанию 3600) — завершённые/упавшие/отменённые задачи вытесняются после TTL. Забирайте и сохраняйте результаты сразу — хранилище в памяти, поэтому перезапуск сервера теряет и очередь, и незабранные результаты.

Проверка результата

curl -s http://127.0.0.1:9876/ready     # {"status":"ready",...} перед отправкой
./submit-and-wait.sh episode.wav        # печатает текст транскрипта
# проверка выключенного API: без --enable-jobs любой вызов /v1/jobs возвращает 404

Рецепт: выбор формата выхода

Пять форматов, по файлу на формат на входной файл. Выбирайте по потребителю:

ФорматДля чегоПримечания
txtлюди, grep, текстовые пайплайнытолько текст транскрипта
jsonмашины{"text", "words": [{"word","start","end","confidence"}], "duration"} — пословные тайминги и confidence
srtвидеоредакторы, загрузка на YouTubeкью SubRip, сгруппированные из пословных таймингов
vttвеб-плеерыWebVTT-вариант тех же кью
mdзаметки, архивыYAML-шапка (duration, language, speakers) + транскрипт
gigastt transcribe-batch episodes/ out/ --format txt,json        # default pair
gigastt transcribe recording.wav -f srt -o recording.srt         # single file

Настройка субтитров (SRT/VTT): --max-chars-per-line (по умолчанию 80) и --max-words-per-line (по умолчанию 14) управляют группировкой кью; 0 отключает ограничение. Для эфирных титров обычно нужны строки короче:

gigastt transcribe recording.wav -f vtt --max-chars-per-line 42 -o recording.vtt

Дополнения Markdown: --word-timestamps добавляет пословную таблицу с таймингами и confidence — удобно для ручной вычитки, шумно для архивов.

Тонкость для скриптов с одиночным transcribe: на уровне info логи идут в stdout вперемешку с транскриптом. Используйте -o для записи транскрипта в файл или глушите логи глобальным флагом, который ставится перед подкомандой:

gigastt --log-level error transcribe recording.wav          # stdout = transcript only

Извлечение текста из папки JSON-результатов:

jq -r '.text' transcripts/*.json

Проверка результата

gigastt transcribe recording.wav -f srt -o /tmp/check.srt
head -4 /tmp/check.srt
# 1
# 00:00:00,480 --> 00:00:02,160
# Привет, как дела?
jq -r '.duration' transcripts/episode.json    # JSON parses and has fields

Рецепт: необычные входы — телефонные WAV, Opus, raw-потоки

G.711 / G.722 внутри WAV — работает само. A-law/μ-law (телефонные экспорты 8 кГц) и G.722 ADPCM (Asterisk/Cisco/Teams, теги формата 0x0064/0x028F) декодируются автоматически; batch-обходчик подхватывает их как любой другой .wav.

OGG/Opus и .opus (голосовые Telegram, браузерный MediaRecorder). Контейнер определяется по содержимому, поэтому одиночная транскрибация работает как есть:

gigastt transcribe voice.opus

Но обходчики batch/watch сканируют по расширению (wav,mp3,m4a,ogg,flac) и не подхватывают .opus-файлы. Переименуйте их в .ogg перед прогоном — содержимое уже является OGG-контейнером, поэтому достаточно простого переименования:

for f in inbox/*.opus; do mv "$f" "${f%.opus}.ogg"; done
gigastt transcribe-batch inbox/ transcripts/

Raw-потоки без заголовка (дампы RTP, Asterisk Monitor raw) не несут контейнера для определения — объявите кодек и частоту явно:

gigastt transcribe call.ulaw --codec pcmu --sample-rate 8000
gigastt transcribe call.alaw --codec pcma --sample-rate 8000
gigastt transcribe call.g722 --codec g722 --sample-rate 8000   # 16000 also accepted

--codec принимает pcmu (алиас ulaw), pcma (алиас alaw), g722 и требует --sample-rate. Всё остальное — WebM, AMR, видео MP4, битый файл — падает с invalid audio: Unsupported audio format: ... (REST: 422 invalid_audio).

Проверка результата

file recording.wav                 # confirms the container type
gigastt --log-level error transcribe call.ulaw --codec pcmu --sample-rate 8000
echo "exit: $?"                    # 0 = decoded and transcribed
gigastt transcribe call.ulaw --codec pcmu 2>&1 | head -2
# error: the following required arguments were not provided: --sample-rate

Рецепт: производительность и память

Голова rnnt на INT8 работает с RTF ≈ 0,10 на M1 CPU — один воркер переваривает час аудио примерно за 6 минут. Архив на 100 часов при --pool-size 4 закончится примерно за 100 ч × 0,10 / 4 ≈ 2,5 ч астрономического времени. Полные измерения, другое железо и цифры WER: docs/benchmarks.md.

Прежде чем поднимать --pool-size, прикиньте память:

  • Каждый воркер загружает свою копию энкодера: ~0,4 ГБ resident с INT8-энкодером по умолчанию, ~1,7 ГБ с FP32. Пул по умолчанию из 2 ≈ 790 МБ RSS.
  • Движок не даёт пулу съесть больше половины всей RAM: завышенный --pool-size урезается с предупреждением при загрузке, так что проверяйте лог, а не предполагайте, что получили заказанный параллелизм.
  • Оставайтесь на INT8 (по умолчанию после авто-квантизации при первом запуске): энкодер ужимается с 844 МБ до 215 МБ на диске с деградацией WER ~0%, а FP32 учетверяет память на воркера без выигрыша в скорости пакетной обработки.
  • На CPU-сборках --encoder-intra-threads по умолчанию равен числу логических CPU, поделённому на размер пула, — правильное значение для выделенной batch-машины; крутите только для совместно используемых.

Проверка результата

# предупреждение об урезании, если есть, появляется при загрузке:
gigastt transcribe-batch calls/ transcripts/ --pool-size 8 2>&1 | grep -i "pool" | head -3
# per-file throughput in the log: "transcribe complete audio_s=... wall_s=... rtf=0.129"
time gigastt transcribe-batch calls/ transcripts/ --pool-size 4 --move-to calls/done/

Частые ошибки

  • Недокопированные файлы. transcribe-batch транскрибирует то, что лежит в папке сейчас, включая файл, который ещё копируется, — получите ошибку декодирования или обрезанный транскрипт. Производители должны писать во временное имя и делать mv в inbox (переименование атомарно в пределах одной файловой системы). watch защищается settle-опросами; batch рассчитывает на тихую папку.
  • Случайная повторная обработка. Без --move-to/--delete-source каждый повторный прогон переделывает всю папку. Для регулярных прогонов всегда задавайте политику исходников. Кроме того: --move-to схлопывает вложенные папки — a/week1/call.wav и a/week2/call.wav столкнутся в одном done/call.wav (а про транскрипты прогон предупредит duplicate output ... inputs with equal file stems overwrite each other). Держите имена исходников уникальными.
  • Ожидание параллелизма, которого не получили. --pool-size 16 на машине с 8 ГБ RAM молча урезается при загрузке (предупреждение в логе). Проверяйте стартовый лог и помните, что FP32 учетверяет память на воркера.
  • invalid audio / 422 на неподдерживаемом контейнере. WebM, AMR, видео MP4 или битая выгрузка не декодируются. Сначала сконвертируйте (ffmpeg -i in.webm -ar 16000 -ac 1 out.wav) или, для raw-телефонии, объявите --codec + --sample-rate. Файл .opus декодируем, но невидим для batch/watch — переименуйте в .ogg.
  • Watch «забывает» ошибки. Файл, исчерпавший попытки, не повторяется, пока не изменится его содержимое, а перезапуск наблюдателя регистрирует его как бэклог (никогда не обрабатывается). Исправьте или замените файл либо направьте на него transcribe-batch — для этого и нужен догоняющий прогон из рецепта с обёрткой.
  • Результаты задач испаряются. Хранилище задач в памяти с TTL 1 ч на терминальные задачи: перезапуск теряет очередь, а незабранные результаты вытесняются. Сохраняйте результаты на клиенте; воспринимайте 429 queue_full как backpressure, а не как ошибку, достойную алерта.

Ссылки

Телефония и VoIP: G.711, G.722, Opus и записи PBX

Сценарий

Вы обслуживаете колл-центр или интегрируете АТС (Asterisk, FreeSWITCH, Cisco, Teams), и записи звонков — ваш основной источник аудио. Перед вами папка, в которой намешаны wav49-файлы, WAV с G.711/G.722, сырые .ulaw-дампы, снятые с RTP, и пара голосовых из Telegram — и нужны транскрипты без ручной конвертации каждого файла.

gigastt декодирует большинство телефонных форматов сам: G.711 A-law/μ-law в WAV, G.722 в WAV (оба зарегистрированных тега формата), OGG/Opus, а также сырые потоки без заголовков — через явную подсказку кодека. Эта глава ведёт от «папки со странными файлами» к рабочим транскриптам, включая разделение спикеров по каналам для стереозаписей.

Требования

  • Установленный gigastt и скачанная модель — см. Начало работы.
  • Запущенный сервер для REST-рецептов (gigastt serve, по умолчанию http://127.0.0.1:9876). CLI-рецепты работают офлайн, сервер не нужен.
  • ffprobe/ffmpeg — только для определения формата и двух запасных вариантов с конвертацией (wav49, G.729). Для поддерживаемых форматов не нужны.

Рецепт

Шаг 0 — определяем, что за файл перед нами

Выгрузки АТС редко говорят, что они такое. Две команды снимают вопрос:

file recording.wav
ffprobe -v error -show_entries stream=codec_name,codec_tag_string,sample_rate,channels \
  -of default=noprint_wrappers=1 recording.wav

Сверьте вывод с таблицей:

ffprobe / file говоритЧто этоЧто делать
codec_name=pcm_alaw или pcm_mulaw, 8000 HzG.711 в WAVотправлять как есть
codec_name=adpcm_g722, тег [0x0064] или [0x028f]G.722 в WAVотправлять как есть
codec_name=gsm_mswav49 (GSM 06.10 в WAV)сначала конвертировать — см. Asterisk ниже
codec_name=opus в контейнере OggOpus (Telegram, MediaRecorder)отправлять как есть
ffprobe падает с Invalid data found when processing input, file говорит dataсырой поток без заголовковобъявить кодек — см. RTP-дамп ниже

Оба тега G.722 — один и тот же кодек от разных писалок: 0x0064 приходит из выгрузок в стиле SBC/Asterisk, 0x028F — из инструментов на базе ffmpeg. gigastt принимает оба.

Записи Asterisk Monitor (wav49, G.722 WAV, сырые потоки)

Что пишут Monitor()/MixMonitor, зависит от настроенного формата:

  • wav — обычный PCM16 WAV. Отправлять как есть.

  • wav49 — GSM 06.10 в WAV-контейнере (codec_name=gsm_ms). gigastt не декодирует GSM, поэтому один раз конвертируем в PCM16 WAV:

    ffmpeg -y -i call.wav -ar 16000 -ac 1 -c:a pcm_s16le call_16k.wav
    

    Проверка: ffprobe call_16k.wav показывает codec_name=pcm_s16le, sample_rate=16000. Дальше транскрибируем call_16k.wav как обычный WAV.

  • ulaw / alaw / g722 — сырые потоки без заголовков (контейнеру нечего «понюхать»). Кодек объявляем явно:

    # REST — алиасы кодеков: pcmu=ulaw, pcma=alaw
    curl -X POST "http://127.0.0.1:9876/v1/transcribe?codec=pcmu&sample_rate=8000" \
      -H "Content-Type: application/octet-stream" --data-binary @call.ulaw
    
    # CLI
    gigastt transcribe call.ulaw --codec pcmu --sample-rate 8000
    

    Проверка: HTTP 200 и транскрипт, совпадающий со звонком. Если перепутать компандирование (A-law вместо μ-law), запрос всё равно вернёт 200, но текст будет мусором — поменяйте имя кодека и повторите.

Выгрузки Cisco и Teams (G.722 WAV)

Телефония Cisco и инструменты вокруг Teams обычно отдают G.722 в WAV-контейнере — codec_name=adpcm_g722, тег 0x0064 или 0x028F. Контейнер сам объявляет кодек, поэтому флаги не нужны:

curl -X POST http://127.0.0.1:9876/v1/transcribe \
  -H "Content-Type: application/octet-stream" --data-binary @call_g722.wav

gigastt transcribe call_g722.wav

Проверка: HTTP 200 с JSON-текстом, либо транскрипт в stdout для CLI.

Типичный сбой здесь — 422: сервер не смог декодировать загрузку. Это значит, что файл не тот, за кого себя выдаёт (внутри G.729 или проприетарная обёртка вендора), либо файл обрезан. Вернитесь к шагу 0 и посмотрите, что на самом деле говорит ffprobe.

Голосовые Telegram и WhatsApp (Opus)

Голосовые Telegram (Bot API отдаёт voice.oga), голосовые WhatsApp (.ogg) и записи браузерного MediaRecorder (.opus) — всё это Opus в контейнере Ogg. Отправляйте как есть: сервер определяет контейнер по байтам, поэтому расширение файла роли не играет:

curl -X POST http://127.0.0.1:9876/v1/transcribe \
  -H "Content-Type: application/octet-stream" --data-binary @voice.ogg

gigastt transcribe voice.opus

Проверка: HTTP 200 с транскриптом. Opus декодируется в свои родные 48 кГц и ресемплируется внутри; поддерживаются моно и стерео (стерео миксуется в моно, если не включить разделение каналов ниже), multistream (>2 каналов) OGG/Opus отклоняется.

Стерео-звонок → два спикера (channels=split)

Многие АТС пишут каждого участника в свой канал: левый — один спикер, правый — другой. Разделение каналов транскрибирует каждый канал как отдельного спикера вместо микширования в моно: канал 0 (левый) становится speaker_0, канал 1 (правый) — speaker_1.

# REST
curl -X POST "http://127.0.0.1:9876/v1/transcribe?channels=split" \
  -H "Content-Type: application/octet-stream" --data-binary @call.wav

# CLI — SRT с метками спикеров [SPEAKER_0] / [SPEAKER_1] в репликах
gigastt transcribe call.wav --stereo-speakers -f srt -o call.srt

В JSON каждое слово получает поле speaker, слова упорядочены по времени начала:

{
  "text": "…",
  "words": [
    {"word": "покажи", "start": 0.08, "end": 0.48, "confidence": 0.95, "speaker": 1},
    {"word": "шестьдесят", "start": 0.52, "end": 1.08, "confidence": 0.96, "speaker": 0}
  ],
  "duration": 3.43
}

Для отчётов группируйте слова по speaker — получите текст и время речи каждой стороны (агент vs клиент). Какая сторона в каком канале — конвенция вашей АТС; откалибруйтесь один раз на звонке с известными спикерами.

Проверка: присутствуют обе метки —

curl -s -X POST "http://127.0.0.1:9876/v1/transcribe?channels=split" \
  -H "Content-Type: application/octet-stream" --data-binary @call.wav \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(sorted({w.get('speaker') for w in d['words']}))"
# [0, 1]

Откаты, о которых стоит знать: если файл моно, каналов больше двух или запись dual-mono (каналы почти идентичны — некоторые АТС пишут микс звонка в оба канала), gigastt откатывается на обычный моно-транскрипт без полей speaker и пишет в лог предупреждение falling back to mono transcription. Разделение каналов несовместимо с диаризацией: channels=split&diarization=true возвращает 400 conflicting_modes.

Моно-запись встречи → спикеры через диаризацию

Если есть один моно-микс (или dual-mono, который откатывается выше), а метки говорящих всё равно нужны — используйте ML-диаризацию, а не split каналов:

РежимВходКак ставятся меткиКогда брать
channels=split / --stereo-speakersНастоящее стерео, сторона = спикерИндекс канала → speaker_0 / speaker_1Стереозаписи АТС
?diarization=true / WS configureМоно (или смешанное)WeSpeaker + кластеризация polyvoiceВстречи, интервью, mono-микс
# Speaker-модель качается обычным download
# (отказ: gigastt download --skip-diarization)
gigastt serve   # diarization в default features

# Opt-in на запрос — plain /v1/transcribe метки speaker не ставит
curl -s -X POST "http://127.0.0.1:9876/v1/transcribe?diarization=true" \
  -H "Content-Type: application/octet-stream" --data-binary @meeting.wav \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(sorted({w.get('speaker') for w in d.get('words',[]) if 'speaker' in w}))"
# например [0, 1], если разделили двух говорящих

# Live: после ready, до первого audio-фрейма
# {"type":"configure","diarization":true}

Проверка: GET /v1/models (или WS ready) показывает "diarization": true только если загружен wespeaker_resnet34.onnx. Без файла запрос успешен, но слов без speaker. Offline-диаризация сопоставляет mid слова с turn; streaming ставит последний turn на новые слова (грубее). Контракт: docs/api.md.

RTP-дамп без контейнера

RTP-захват, очищенный до полезной нагрузки, не имеет заголовка для сниффинга, поэтому кодек нужно объявить. Экспортируйте только payload из вашего инструмента (в Wireshark: Telephony → RTP → Stream Analysis → сохранение payload; у SBC есть похожий экспорт) — 12-байтовых RTP-заголовков в файле быть не должно.

# Захват G.711 A-law на 8 кГц
curl -X POST "http://127.0.0.1:9876/v1/transcribe?codec=pcma&sample_rate=8000" \
  -H "Content-Type: application/octet-stream" --data-binary @dump.alaw

# Захват G.722 — см. замечание про clock-rate в SDP ниже
curl -X POST "http://127.0.0.1:9876/v1/transcribe?codec=g722&sample_rate=8000" \
  -H "Content-Type: application/octet-stream" --data-binary @dump.g722

# Эквивалент для CLI
gigastt transcribe dump.g722 --codec g722 --sample-rate 8000

Особенность G.722 в SDP: по историческим причинам SDP/RTP анонсирует G.722 с clock-rate 8000 Гц, хотя поток на самом деле декодируется в 16 кГц. gigastt принимает для g722 и 8000, и 16000 и всегда декодирует в 16 кГц, так что подойдёт любое значение. Для сырого G.711 (pcmu/pcma) принимается любая частота в диапазоне 8000–48000 Гц, с ресемплингом.

Проверка: HTTP 200 и осмысленный текст. Ошибки параметров срабатывают сразу, до инференса: codec без sample_rate400 invalid_sample_rate («sample_rate is required when codec is set»); неизвестный кодек → 400 unsupported_codec («Unsupported codec. Supported: pcmu (ulaw), pcma (alaw), g722»).

Потери в захвате: поток декодируется как есть. Дыры от потерянных пакетов и переупорядоченное аудио не восстанавливаются — при дырах в транскрипте подавите джиттер или переснимите захват.

Папка записей (пакетная обработка)

gigastt transcribe-batch сканирует файлы с расширениями wav, mp3, m4a, ogg и flac — это покрывает G.711/G.722 WAV и OGG/Opus (.ogg) из коробки:

gigastt transcribe-batch recordings/ out/ --format txt,json

Два вида телефонных файлов не сканируются:

  • файлы .opus — переименуйте их в .ogg. Внутри контейнер Ogg, он определяется по байтам, поэтому переименование безопасно:

    for f in recordings/*.opus; do mv "$f" "${f%.opus}.ogg"; done
    
  • сырые потоки .ulaw / .alaw / .g722 — сначала оберните в WAV (у batch нет флага --codec), затем запустите batch на обёрнутых файлах:

    mkdir -p wav
    for f in recordings/*.ulaw; do
      ffmpeg -y -v error -f mulaw -ar 8000 -ac 1 -i "$f" \
        -ar 16000 -ac 1 -c:a pcm_s16le "wav/$(basename "${f%.ulaw}").wav"
    done
    gigastt transcribe-batch wav/ out/ --format txt,json
    

    Для A-law используйте -f alaw, для G.722 — -f g722 (G.722 не нужен входной -ar).

Проверка: в out/ по одному .txt/.json на каждый исходный файл, команда завершается с кодом 0. Если во входящую папку постоянно падают новые записи, то же самое в непрерывном режиме делает gigastt watch — подробности о batch/watch и длинных записях в главе CLI и пакетная обработка.

Шпаргалка по форматам

ВходОпределяется по контейнеруНужен ?codec= / --codecЗамечания и ограничения
WAV PCM (8–32 бита, IEEE float)данетстерео автоматически миксуется в моно
WAV с G.711 A-law / μ-lawданетобычно 8 кГц, ресемплинг в 16 кГц
WAV с G.722 ADPCM (теги 0x0064, 0x028F)данетдекодируется в родные 16 кГц
OGG/Opus, .opusданеттолько моно/стерео; >2 каналов отклоняется
сырой .ulaw / .alawнет (без заголовков)да — pcmu / pcmasample_rate 8000–48000
сырой .g722нет (без заголовков)да — g722sample_rate 8000 (конвенция SDP) или 16000; декодируется в 16 кГц
wav49 (GSM 06.10 в WAV)дан/дне декодируется — сначала конвертировать в PCM16 WAV
G.729 (любая обёртка)дан/дне поддерживается — сначала конвертировать в PCM16 WAV

Общее для всех путей: загрузка ограничена 30 минутами декодированного аудио, а ?codec= / ?sample_rate= одинаково работают на /v1/transcribe, /v1/transcribe/stream и /v1/jobs. Deepgram-совместимый эндпоинт /v1/listen, принимающий те же телефонные форматы, в работе.

Проверка результата

В репозитории лежат телефонные фикстуры, которые используют его собственные e2e-тесты, — 4 секунды русской речи («шестьдесят тысяч тенге сколько будет стоить»), перекодированные во все поддерживаемые телефонные форматы. Нужны скачанная модель и запущенный сервер:

# сырой путь
curl -s -X POST "http://127.0.0.1:9876/v1/transcribe?codec=pcmu&sample_rate=8000" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @crates/gigastt/tests/fixtures/telephony/speech.ulaw

# контейнерный путь — без параметров
curl -s -X POST http://127.0.0.1:9876/v1/transcribe \
  -H "Content-Type: application/octet-stream" \
  --data-binary @crates/gigastt/tests/fixtures/telephony/speech_g722.wav

Оба запроса возвращают HTTP 200 с транскриптом, где есть «тенге» и «стоить», — при включённых на сервере пунктуации и ITN текст выглядит как «60000 тенге, сколько будет стоить?». Если ваш файл падает, а эти фикстуры проходят, проблема в файле, а не в сервере — возвращайтесь к шагу 0.

Частые ошибки

  • 422 — «Check audio format» (invalid_audio / transcription_error). Байты были проверены как контейнер, и декодирование не удалось. Обычные причины: сырой поток отправлен без codec=; файл wav49 (GSM) или G.729; обрезанная/битая загрузка. Определите файл шагом 0.

  • G.729 не поддерживается. Сырая загрузка с ?codec=g729 возвращает 400 unsupported_codec; G.729-в-WAV падает с 422. Конвертируйте ffmpeg и отправляйте результат:

    ffmpeg -y -i call_g729.wav -ar 16000 -ac 1 -c:a pcm_s16le call_16k.wav
    
  • ?codec= на файле-контейнере. Параметр полностью переопределяет сниффинг: WAV, отправленный с ?codec=pcmu, декодирует WAV-заголовок как μ-law-шум и возвращает 200 с мусором. Используйте codec= только для потоков без заголовков.

  • RTP-дамп с заголовками или джиттером. ?codec= ждёт только байты payload. Оставшиеся 12-байтовые RTP-заголовки декодируются как периодические щелчки, а дыры от потерь и переупорядочивания превращаются в провалы транскрипта — на сервере ничего не восстанавливается. Экспортируйте только payload и подавите джиттер до загрузки.

  • «8-битный WAV» — это не поломка. G.711 в WAV законно показывает bits_per_sample=8 (компандированные сэмплы) — отправляйте как есть. Настоящая ловушка обратная: при собственной конвертации всегда пишите 16-битный PCM (pcm_s16le); 8-битный линейный PCM (pcm_u8) убивает точность распознавания.

  • Путаница моно/стерео. channels=split на моно-файле, файле с >2 каналами или dual-mono стерео (некоторые АТС пишут микс звонка в оба канала) молча откатывается на обычный моно-транскрипт — без полей speaker, лишь с предупреждением falling back to mono transcription в логе. Если АТС пишет сведённое моно, никакой флаг не разделит спикеров задним числом; пишите стерео или используйте diarization=true (несовместимо с channels=split).

  • «Audio file too long». Одна загрузка ограничена 30 минутами декодированного аудио («Maximum supported: 1800s»). Делите длинные записи — например, по плечам звонка — перед загрузкой.

  • Перепутаны A-law/μ-law. Оба закона декодируются «успешно», поэтому неверный выбор возвращает 200 с мусором вместо ошибки. Если сырой поток транскрибируется в шум, повторите с другим именем кодека.

Ссылки

Стриминг по WebSocket

Живая транскрипция с промежуточными результатами: микрофон, телефонная линия или захват звука в браузере поступают как сырой PCM16, а текст появляется примерно через секунду после речи. Эта глава — книга рецептов для такой интеграции. Пофилдовый справочник протокола остаётся в docs/api.md, машиночитаемая схема — в docs/asyncapi.yaml; мы ссылаемся на них, а не повторяем.

Сценарий

Вы строите real-time интеграцию — живые субтитры для встреч, голосового бота на телефонной линии или «диктофон с мгновенным текстом». Аудио идёт непрерывно; пользователи ждут, что транскрипт растёт прямо во время речи, каждая фраза финализируется чисто, а хвостовые слова не теряются при завершении потока. Некоторые сессии длятся часами, в некоторых конфигурациях захватываются два источника сразу (микрофон + системный звук), а клиент обязан переживать насыщение пула и сетевые обрывы без ручного вмешательства.

Требования

  • Запущенный сервер с моделью — по главе Начало работы: gigastt serve (первый запуск скачивает ~850 МБ и квантизирует — ждите готовности, не убивайте процесс):

    until curl -sf http://127.0.0.1:9876/ready; do sleep 2; done
    # {"status":"ready","pool_available":2,"pool_total":2}
    
  • WebSocket-стек для вашего языка: pip install websockets для рецептов на Python, Node.js ≥ 22 (глобальный WebSocket, без зависимостей) для JavaScript, Go 1.23+ для SDK.

  • Источник PCM16 mono. Для копипастных проверок ниже в репозитории есть 4-секундная фикстура русской речи ровно в нужном формате (16 кГц mono Int16): crates/gigastt/tests/fixtures/golos_00.wav. Для реального микрофона ffmpeg превращает любое устройство захвата в сырой PCM16 на stdout.

Рецепт

Форма сессии, на которую опираются все рецепты, — подключение, чтение ready, опциональный configure, поток бинарных PCM16, stop для финализации:

Client                            Server
  |-------- connect --------------> |
  | <------- ready ----------------- |  лимиты и supported rates (читайте их!)
  |------- configure (optional) --> |  до первого аудиофрейма
  |-------- binary PCM16 ---------> |
  | <------- partial / final ------ |
  |--------- stop ----------------> |
  | <------- final ----------------- |  хвостовые слова сброшены, затем close

Рецепт 1: подключаемся, согласуем параметры и стримим с микрофона

  1. Подключитесь и сначала прочитайте ready. Первое сообщение сервера — всегда ready. В нём всё, что нельзя хардкодить: version протокола, допустимые supported_rates и лимиты сессии (max_session_secs, idle_timeout_secs), вокруг которых строится рецепт 4:

    {
      "type": "ready",
      "model": "gigaam-v3-rnnt",
      "sample_rate": 48000,
      "version": "1.0",
      "supported_rates": [8000, 16000, 24000, 44100, 48000],
      "max_session_secs": 3600,
      "idle_timeout_secs": 300
    }
    

    sample_rate (по умолчанию 48000) — это то, что сервер ждёт, если вы не пришлёте configure. Одна деталь, которую стоит знать до отладки «молчащего подключения»: при насыщенном пуле сервер отвечает сообщением error вместо ready — см. рецепт 5.

  2. Отправьте configure до первого аудиофрейма. Выберите частоту, которую реально выдаёт ваш конвейер захвата, — она обязана быть из ready.supported_rates. 16 кГц — оптимум, когда источник под вашим контролем (модель внутри работает на 16 кГц, ресемплинг не нужен); браузерный захват на 48 кГц тоже можно слать как есть. Неподдерживаемая частота не фатальна: вы получите ошибку invalid_sample_rate, а сессия продолжится на прежней частоте. configure, пришедший после первого аудиофрейма, отклоняется с configure_too_late, настройки сохраняются — поэтому шлите его сразу после ready:

    {"type": "configure", "sample_rate": 16000}
    
  3. Шлите бинарные фреймы PCM16 (signed 16-bit little-endian, mono) на согласованной частоте. Разумная нарезка: 100–500 мс на фрейм (3 200–16 000 байт при 16 кГц). Партилы формируются со шагом декодирования примерно 0.8 с нового аудио, поэтому субсекундные чанки сохраняют «живость» превью без лишних накладных расходов. Жёсткий потолок — --ws-frame-max-bytes (по умолчанию 512 КиБ): больший фрейм закрывает сокет с кодом 1009. Нечётная длина допустима (последний байт переносится в следующий фрейм), случайные пустые фреймы терпимы. Стереоисточники сначала микшируйте в mono (-ac 1 в ffmpeg).

  4. Собираем всё вместе — микрофон в живой текст. Конвейер гонит микрофон через ffmpeg в минимальный Python-клиент:

    # macOS (-f avfoundation); Linux: -f alsa -i default; Windows: -f dshow -i audio="..."
    ffmpeg -hide_banner -loglevel error -f avfoundation -i ":default" \
      -ac 1 -ar 16000 -f s16le - | python3 mic_stream.py
    

    mic_stream.py — полный эталонный цикл, который использует эта глава:

    #!/usr/bin/env python3
    """Stream raw PCM16 from stdin to gigastt and print live transcripts.
    
    Usage: ffmpeg ... -f s16le - | python3 mic_stream.py [label] [server]
    """
    import asyncio
    import json
    import sys
    
    import websockets
    
    LABEL = sys.argv[1] if len(sys.argv) > 1 else "mic"
    SERVER = sys.argv[2] if len(sys.argv) > 2 else "ws://127.0.0.1:9876/v1/ws"
    RATE = 16000
    CHUNK = RATE * 2 // 5  # 400 ms of PCM16
    
    
    async def main() -> None:
        async with websockets.connect(SERVER) as ws:
            ready = json.loads(await ws.recv())
            assert ready["type"] == "ready", ready
            assert RATE in ready.get("supported_rates", [ready["sample_rate"]])
            await ws.send(json.dumps({"type": "configure", "sample_rate": RATE}))
            print(f"{LABEL}: connected to {ready['model']}", file=sys.stderr)
    
            async def receive() -> None:
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg["type"] == "partial":
                        print(f"\r{LABEL} ... {msg['text']}   ", end="", flush=True)
                    elif msg["type"] == "final":
                        conf = msg.get("confidence")
                        suffix = f" ({conf:.2f})" if conf is not None else ""
                        print(f"\r{LABEL} >>> {msg['text']}{suffix}   ")
                    elif msg["type"] == "error":
                        print(f"\n{LABEL} ERR {msg['code']}: {msg['message']}")
    
            receiver = asyncio.create_task(receive())
            try:
                while data := await asyncio.to_thread(sys.stdin.buffer.read, CHUNK):
                    await ws.send(data)
                # Capture ended: finalize — never close before the trailing final.
                await ws.send(json.dumps({"type": "stop"}))
                await receiver
            except websockets.ConnectionClosed:
                pass  # server closed after the trailing final (or an error we printed)
    
    
    asyncio.run(main())
    

    Блокирующее чтение stdin выполняется в потоке (asyncio.to_thread), чтобы задача-приёмник продолжала печатать, пока вы говорите.

Проверка: во время речи строки ... обновляются примерно раз в секунду; пауза около полусекунды даёт >>> final. Закрытие ffmpeg (Ctrl+C) вызывает последний final и чистое закрытие — ничего не обрезается.

Рецепт 2: partial показываем вживую, final — в транскрипт

Оба типа сообщений несут одинаковую полезную нагрузку, но играют разные роли в UI — относитесь к ним по-разному.

  • partial — это превью. Всегда сырая гипотеза декодера: нижний регистр, без пунктуации, и она может измениться с приходом нового аудио. Новый partial появляется примерно каждые 0.8 с речи (шаг декодирования):

    {
      "type": "partial",
      "text": "привет как",
      "timestamp": 1712700000.123,
      "is_final": false,
      "confidence": 0.93,
      "words": [
        {"word": "привет", "start": 0.0, "end": 0.4, "confidence": 0.97},
        {"word": "как", "start": 0.5, "end": 0.7, "confidence": 0.89}
      ]
    }
    
  • final — это зафиксированная строка. Обогащается на границе финализации: инверсная нормализация текста (числительные → цифры), затем восстановление пунктуации и регистра. Голове rnnt нужна подключённая пунктуационная модель (серверный --punctuation auto по умолчанию; проверка: GET /health"punctuation":true); голова e2e_rnnt пунктуирует сама — см. Модели и бэкенды. words[] в обоих типах сообщений всегда хранят сырой вывод декодера; переписывается только склеенный text:

    {
      "type": "final",
      "text": "Привет, как дела?",
      "timestamp": 1712700001.456,
      "is_final": true,
      "confidence": 0.95,
      "words": [
        {"word": "привет", "start": 0.0, "end": 0.4, "confidence": 0.97},
        {"word": "как", "start": 0.5, "end": 0.7, "confidence": 0.93},
        {"word": "дела", "start": 0.8, "end": 1.1, "confidence": 0.95}
      ]
    }
    

Правило для UI: последний partial рисуем в стиле «превью» (серым/курсивом, заменяемым), а когда приходит final для этой фразы — заменяем превью на final.text и дописываем в зафиксированный транскрипт. Текст partial никогда не сохраняем.

final срабатывает при завершении фразы. Кто владеет эндпоинтингом (с 2.14.1):

Флаги сервераКто завершает фразуКноб
по умолчанию (без --vad)blank-run эвристика декодера (~0.6 с тишины)не настраивается
auto + --vadтолько Silero VAD — blank-run игнорируется--vad-min-silence-ms (дефолт 500)
assistant + --vadтолько VAD (голосовые команды)--vad-min-silence-ms (900–1500)
manualтолько клиентский stop

Окно энкодера ~2.5 с не закрывает реплику: коммитит стабильный префикс и шлёт partial (раньше final с cap ломал Ирину).

С VAD можно поднять порог тишины, чтобы естественные паузы не рвали фразы посередине (до 2.14.1 blank-run всё равно срабатывал ~на 600 мс даже при --vad). Окно ~2.5 с только сдвигает контекст и шлёт partial, не final. final — по VAD/blank/stop (рецепт 3) и перед закрытием сервером (рецепт 4).

# Голосовой ассистент (Ирина)
gigastt serve --vad --vad-min-silence-ms 1200 --endpoint-mode assistant

# Дольше ждать тишину перед final — удобнее для диктовки
gigastt serve --vad --vad-min-silence-ms 900

Сессионные переопределения комбинируются тем же configure (финалы; partial сырые). punctuation: true без модели — no-op:

{"type":"configure","sample_rate":16000,"endpoint_mode":"assistant","min_silence_ms":1200,"punctuation":false,"itn":false}

Проверка: скажите «привет как дела» с короткими паузами. Превью показывает привет как дела в нижнем регистре; зафиксированная строка приходит как Привет, как дела? (когда пунктуационная модель подключена — проверьте curl -s http://127.0.0.1:9876/health с "punctuation":true). При --vad --vad-min-silence-ms 900 пауза ~0.7 с между словами не должна форсировать ранний final.

Рецепт 3: корректное завершение — stop вместо drain

Единственно правильный паттерн конца потока:

  1. Отправьте {"type": "stop"}.
  2. Дождитесь final — сервер декодирует всё, что ещё буферизовано с последнего partial (хвостовые слова не теряются), и выдаёт последний final, возможно с пустым text, если ничего не оставалось.
  3. Только потом закрывайте сокет (или позвольте серверу закрыть его — он завершает сессию сразу после этого final).

Не закрывайте сокет сразу после последнего аудиофрейма (хвост короче шага декодирования будет потерян) и не вставляйте фиксированный sleep для drain — final после stop и есть явный, безпотерный маркер конца. Цикл mic_stream.py из рецепта 1 уже реализует этот паттерн.

Keepalive не требует кода с вашей стороны: сервер шлёт ping каждые 30 с и закрывает соединение после двух подряд ping без входящих фреймов между ними (≈ 90 с на обнаружение полуоткрытого пира). Любой входящий фрейм — pong, бинарное аудио или текст — сбрасывает счётчик, а стандартные WebSocket-клиенты отвечают на ping автоматически. Отвечать на ping вручную нужно только в реализациях на голых сокетах.

Проверка: скажите одно слово и немедленно отправьте stop, до прихода любого partial. Слово всё равно появится в хвостовом final. Затем посмотрите на закрытие: оно происходит после этого final, а не до.

Рецепт 4: держим длинные сессии (idle-таймаут и потолок сессии)

Каждую сессию ограничивают два серверных таймера; оба объявляются в ready, чтобы вы планировали заранее, а не узнавали о них из close-фрейма.

  • Idle-таймаут (idle_timeout_secs, по умолчанию 300): любой фрейм — аудио, pong или текст — сбрасывает его. Паузы в речи считаются простоем, только если клиент перестаёт слать; поток тихого PCM держит сессию живой (тишина — тоже аудио). При срабатывании: ошибка idle_timeout и close 1001. Если ваше приложение глушит захват на паузах, шлите фреймы тишины вместо ничего.
  • Потолок сессии (max_session_secs, по умолчанию 3600, 0 — отключён): настенные часы от момента подключения. При срабатывании сервер шлёт ошибку max_session_duration_exceeded, сначала сбрасывает final, затем закрывает с 1008 — всё уже распознанное сохранено, поэтому переподключение безопасно.

Для записей длиннее часа есть два варианта, и их можно комбинировать:

  1. Сторона оператора: поднять или отключить потолок — gigastt serve --max-session-secs 0 (каждый лимит — CLI-флаг; см. docs/cli.md).

  2. Сторона клиента: ротация по расписанию. Прочитайте потолок из ready и на ~90 % от него финализируйте stop и откройте свежую сессию — плановый реконнект, а не обрыв:

    # rotate_sessions.py — keep transcription running across the server cap.
    import asyncio
    import json
    
    import websockets
    
    SERVER = "ws://127.0.0.1:9876/v1/ws"
    
    
    def handle_message(msg: dict) -> None:
        if msg["type"] == "final":
            print(">>>", msg["text"])
        elif msg["type"] == "error":
            print("ERR", msg["code"], msg["message"])
    
    
    async def run_session(ws, stream_audio, rotate_at: float | None) -> None:
        async def rotate() -> None:
            await asyncio.sleep(rotate_at)
            await ws.send(json.dumps({"type": "stop"}))  # flush now, reconnect fresh
    
        tasks = [asyncio.create_task(stream_audio(ws))]
        if rotate_at:
            tasks.append(asyncio.create_task(rotate()))
        try:
            async for raw in ws:  # ends when the server closes after the final
                handle_message(json.loads(raw))
        finally:
            for task in tasks:
                task.cancel()
    
    
    async def run_shift(stream_audio) -> None:
        """Rotate before the cap; back off on transient drops."""
        backoff = 0.25
        while True:
            try:
                async with websockets.connect(SERVER) as ws:
                    ready = json.loads(await ws.recv())
                    cap = ready["max_session_secs"]  # always sent; 0 = no cap
                    await run_session(ws, stream_audio, rotate_at=cap * 0.9 or None)
                    backoff = 0.25  # clean stop → final → close: rotate at once
            except (OSError, websockets.ConnectionClosed):
                # 1006 drop, a proxy-injected 1011, the 1008 cap close, network...
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 30)
    

    stream_audio(ws) — ваш цикл захвата из рецепта 1. Обратите внимание, чего этот паттерн не делает: он никогда не ретраит фатальные ошибки «чините-клиента» (unsupported_protocol_version, частота вне supported_rates) — те падают сразу на рукопожатии и должны чиниться, а не ретраиться.

Проверка: запустите сервер с крошечным потолком, чтобы увидеть весь жизненный цикл за минуту: gigastt serve --max-session-secs 30 --idle-timeout-secs 10. С идущим аудио вы увидите ошибку max_session_duration_exceeded, сброшенный final, close 1008 и переподключение ротатора без потери текста. Полностью прекратите слать фреймы — через 10 с сработает ошибка idle_timeout с close 1001.

Рецепт 5: пороги confidence и обратное давление

Confidence. Каждый сегмент транскрипта несёт опциональный confidence — взвешенное по длительности среднее его words[].confidence (у слова это средний softmax по его BPE-токенам). Это среднее softmax-оценок, не калиброванная вероятность, и оно опускается, когда в сегменте нет слов. Стартовые пороги для подстройки на ваших данных: подсвечивайте для человеческой проверки слова ниже ~0.7, помечайте сегменты ниже ~0.8:

const unsure = (msg.words ?? []).filter((w) => w.confidence < 0.7);
if (unsure.length) console.log("review:", unsure.map((w) => w.word).join(" "));

Обратное давление. Слоты инференса берутся из пула (--pool-size, по умолчанию 2), и WebSocket-сессия держит свой слот всё своё время жизни. Когда все слоты заняты, новое подключение ждёт до 30 с — и если слот не освобождается, сервер отвечает вместо ready ошибкой timeout с retry_after_ms, затем закрывает. Точно соблюдайте подсказку вместо угадывания задержки:

{"type": "error", "message": "Server busy, try again later", "code": "timeout", "retry_after_ms": 30000}
first = json.loads(await ws.recv())          # may be an error, not ready
if first["type"] == "error" and first["code"] == "timeout":
    await asyncio.sleep(first["retry_after_ms"] / 1000)  # then reconnect

Для всех остальных неожиданных закрытий сокета — обрыв 1006, код вроде 1011, подставленный прокси, сетевой сбой — переподключайтесь с экспоненциальной задержкой (старт ~250 мс, удвоение до нескольких секунд, потолок попыток), как в run_shift из рецепта 4. Фатальные ошибки рукопожатия (unsupported_protocol_version) не ретрайте. Оба официальных SDK реализуют ровно эту политику — configure-first рукопожатие, retry_after_ms при насыщении пула, экспоненциальная задержка в остальных случаях — поэтому предпочитайте их самодельным велосипедам: sdks/go, sdks/js.

Проверка: запустите с --pool-size 1 и подключите два клиента одновременно. Второй ждёт ~30 с, затем получает {"code":"timeout","retry_after_ms":30000}; выспавшись положенное, он подключается после завершения первой сессии. Для confidence: прогоните чистый речевой файл и убедитесь, что финалы несут confidence около 1.0, затем попробуйте тихое/шумное аудио и понаблюдайте, как слова проваливаются ниже 0.7.

Рецепт 6: два канала — микрофон + системный звук

Паттерн ассистента встреч (ваш микрофон + системный звук собеседника, каждый со своей меткой) раскладывается на две независимые WebSocket-сессии — сервер не помечает источники, поэтому метки ставятся на клиенте. Ограничение, вокруг которого надо проектировать, — пул: каждая сессия держит один инференс-слот всё своё время жизни, поэтому дефолтный --pool-size 2 вмещает ровно два канала и больше ничего. Дайте серверу запас, если подключаются и другие клиенты, — каждый дополнительный слот стоит примерно 0.4 ГБ RAM с INT8-энкодером (сервер ограничивает пул по доступной памяти при загрузке):

gigastt serve --pool-size 4

Затем запустите по конвейеру захвата на источник, каждый со своей меткой (mic_stream.py из рецепта 1 принимает метку первым аргументом):

# terminal 1 — your microphone (macOS example; Linux: -f alsa -i default)
ffmpeg -hide_banner -loglevel error -f avfoundation -i ":default" \
  -ac 1 -ar 16000 -f s16le - | python3 mic_stream.py mic

# terminal 2 — system audio (Linux Pulse/PipeWire monitor source;
# macOS: a virtual device such as BlackHole selected via avfoundation)
ffmpeg -hide_banner -loglevel error -f pulse -i default.monitor \
  -ac 1 -ar 16000 -f s16le - | python3 mic_stream.py system

Две более лёгкие альтернативы, когда помеченные каналы не нужны: смикшируйте оба источника в один поток на клиенте (один слот пула, одна сессия) или возьмите пословные метки говорящих из диаризации — сервер, собранный с --features diarization, объявляет "diarization": true в ready, и {"type":"configure","diarization":true} добавляет поле speaker к каждому слову (см. справочник в docs/api.md).

Проверка: оба терминала печатают со своими метками mic / system; пока оба работают, curl -s http://127.0.0.1:9876/ready показывает "pool_available":0,"pool_total":2 (с дефолтным пулом), а третий клиент получает обработку timeout + retry_after_ms из рецепта 5.

Рецепт 7: клиентские скелеты (Python, Node.js, Go)

Минимальные, но полные циклы для старта — каждый делает полный оборот ready → configure → stream → stop → wait-for-final → close с обработкой ошибок. Больше клиентов (Bun, Kotlin, Rust) лежит в examples/.

Python — стримит WAV 16 кГц mono PCM16, соблюдает retry_after_ms при насыщении пула:

#!/usr/bin/env python3
"""Stream a 16 kHz mono PCM16 WAV to gigastt, honoring server backpressure.

Usage: python3 stream_wav.py <audio.wav> [ws://host:port]
"""
import asyncio
import json
import sys
import wave

import websockets


class PoolBusy(Exception):
    """Pool saturated: the server sent retry_after_ms instead of ready."""


async def session(path: str, server: str) -> None:
    async with websockets.connect(server) as ws:
        first = json.loads(await ws.recv())
        if first["type"] == "error":  # pool busy: error + close instead of ready
            raise PoolBusy(first.get("retry_after_ms", 30000))
        assert first["type"] == "ready", first
        await ws.send(json.dumps({"type": "configure", "sample_rate": 16000}))

        with wave.open(path, "rb") as wav:
            assert wav.getnchannels() == 1 and wav.getsampwidth() == 2, "mono PCM16 WAV"
            pcm = wav.readframes(wav.getnframes())

        async def receive() -> None:
            async for raw in ws:
                msg = json.loads(raw)
                if msg["type"] == "partial":
                    print(f"\r... {msg['text']}   ", end="", flush=True)
                elif msg["type"] == "final":
                    print(f"\r>>> {msg['text']}   ")
                elif msg["type"] == "error":
                    print(f"\nERR {msg['code']}: {msg['message']}")

        receiver = asyncio.create_task(receive())
        try:
            for off in range(0, len(pcm), 16000):  # 0.5 s of PCM16 at 16 kHz
                await ws.send(pcm[off : off + 16000])
                await asyncio.sleep(0.1)  # pace it like a live feed
            await ws.send(json.dumps({"type": "stop"}))
            await receiver  # trailing final, then the server closes
        except websockets.ConnectionClosed:
            pass


async def main() -> None:
    server = sys.argv[2] if len(sys.argv) > 2 else "ws://127.0.0.1:9876/v1/ws"
    while True:
        try:
            await session(sys.argv[1], server)
            return
        except PoolBusy as busy:
            wait = busy.args[0] / 1000
            print(f"pool busy, retrying in {wait:.0f}s")
            await asyncio.sleep(wait)  # honor retry_after_ms exactly


asyncio.run(main())

Node.js — без зависимостей (глобальный WebSocket, Node ≥ 22), тот же контракт WAV-файла:

// stream_wav.mjs — stream a 16 kHz mono PCM16 WAV to gigastt (Node.js ≥ 22).
import { readFile } from "node:fs/promises";

const [wavPath, server = "ws://127.0.0.1:9876/v1/ws"] = process.argv.slice(2);
if (!wavPath) {
  console.error("usage: node stream_wav.mjs <audio.wav> [ws://host:port]");
  process.exit(1);
}

const pcm = (await readFile(wavPath)).subarray(44); // skip the WAV header
const ws = new WebSocket(server);
ws.binaryType = "arraybuffer";

let stopped = false;
const done = new Promise((resolve, reject) => {
  ws.onmessage = async (event) => {
    const msg = JSON.parse(event.data);
    if (msg.type === "ready") {
      ws.send(JSON.stringify({ type: "configure", sample_rate: 16000 }));
      for (let off = 0; off < pcm.byteLength; off += 16000) { // 0.5 s at 16 kHz
        ws.send(pcm.subarray(off, off + 16000));
        await new Promise((r) => setTimeout(r, 100)); // pace like a live feed
      }
      stopped = true;
      ws.send(JSON.stringify({ type: "stop" })); // finalize — do NOT close yet
    } else if (msg.type === "partial") {
      process.stdout.write(`\r... ${msg.text}   `);
    } else if (msg.type === "final") {
      console.log(`\r>>> ${msg.text}   `);
      if (stopped) { ws.close(); resolve(); } // trailing final: safe to close
    } else if (msg.type === "error") {
      const hint = msg.retry_after_ms ? ` (retry in ${msg.retry_after_ms} ms)` : "";
      reject(new Error(`${msg.code}: ${msg.message}${hint}`));
    }
  };
  ws.onerror = () => reject(new Error("websocket transport error"));
});
await done;

Для всего серьёзнее скелета типизированный SDK из коробки добавляет политику реконнекта из рецепта 5: npm install @gigastt/client — см. sdks/js.

Go — на официальном SDK (go get github.com/ekhodzitsky/gigastt/sdks/go@latest), который пинит версию протокола, шлёт configure первым и ретраит обратное давление с серверным retry_after_ms:

// stream_wav.go — stream a 16 kHz mono PCM16 WAV to gigastt via the Go SDK.
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	gigastt "github.com/ekhodzitsky/gigastt/sdks/go"
)

func main() {
	if len(os.Args) < 2 {
		log.Fatal("usage: go run stream_wav.go <audio.wav>")
	}
	done := make(chan struct{})

	client, err := gigastt.Dial(context.Background(), gigastt.DefaultURL,
		gigastt.WithSampleRate(16000), // must be in the server's supported_rates
		gigastt.WithReconnect(250*time.Millisecond, 5*time.Second, 10),
		gigastt.WithHandlers(gigastt.Handlers{
			OnPartial: func(t gigastt.Transcript) { fmt.Printf("\r... %s   ", t.Text) },
			OnFinal:   func(t gigastt.Transcript) { fmt.Printf("\r>>> %s   \n", t.Text) },
			OnError:   func(e *gigastt.ServerError) { log.Printf("server error: %v", e) },
			OnClose: func(err error) {
				if err != nil {
					log.Printf("connection closed: %v", err)
				}
				close(done)
			},
		}),
	)
	if err != nil {
		log.Fatal(err) // e.g. *gigastt.ServerError unsupported_protocol_version
	}
	defer client.Close()

	wav, err := os.ReadFile(os.Args[1])
	if err != nil {
		log.Fatal(err)
	}
	for off := 44; off < len(wav); off += 16000 { // skip WAV header; 0.5 s chunks
		if err := client.SendPCM(wav[off:min(off+16000, len(wav))]); err != nil {
			log.Fatal(err) // ErrReconnecting: drop or retry the frame yourself
		}
		time.Sleep(100 * time.Millisecond) // pace like a live feed
	}
	if err := client.Stop(); err != nil { // finalize; server closes after the final
		log.Fatal(err)
	}
	<-done
}

Проверка: направьте любой из трёх на фикстуру из репозитория — python3 stream_wav.py crates/gigastt/tests/fixtures/golos_00.wav — и получите несколько ... partial, за которыми следуют >>> final русской речи, а затем чистый выход после хвостового final.

Проверка результата

Сквозная проверка в двух терминалах:

# terminal 1
gigastt serve

# terminal 2 — wait for readiness, then stream the bundled speech fixture
until curl -sf http://127.0.0.1:9876/ready; do sleep 2; done
python3 stream_wav.py crates/gigastt/tests/fixtures/golos_00.wav

Интеграция стриминга здорова, когда выполняется всё нижеперечисленное:

  • Первое сообщение клиента — ready с version: "1.0", а выбранная вами частота входит в его supported_rates.
  • ... partial появляются примерно раз в секунду во время аудио и идут в нижнем регистре/сырыми; >>> final коммитят обогащённый текст после каждой паузы и несут words[] с таймингами и (обычно) confidence.
  • Отправка stop даёт ровно ещё один final (возможно, пустой), и сокет закрывается только после него — остановка на середине слова всё равно распознаёт это слово.
  • ready.max_session_secs / ready.idle_timeout_secs совпадают со значениями, переданными в gigastt serve, и ваша логика ротации/backoff (рецепты 4/5) переживает принудительный потолок: gigastt serve --max-session-secs 30.

Типичные ошибки

Симптом → причина с указателем на исправление — jump table в Приложении A — Коды ошибок; полная таблица оператора — в docs/troubleshooting.md, а таблицы полей — в docs/api.md.

СимптомПричинаКуда смотреть
Подключение висит ~30 с, затем {"code":"timeout","retry_after_ms":30000}Насыщение пула — checkout происходит до ready, поэтому ошибка заменяет егоРецепт 5; api.md error codes
Сокет закрывается 1008 ровно на часовой отметкеПотолок --max-session-secs; final сбрасывается первым, так что просто переподключайтесьРецепт 4; troubleshooting
Сокет закрывается 1001 после ~5 мин тишиныIdle-таймаут — ни одного фрейма; шлите тихий PCM, чтобы остаться живымиРецепт 4; troubleshooting
Сокет закрывается 1009Фрейм превысил --ws-frame-max-bytes (по умолчанию 512 КиБ) — режьте мельчеРецепт 1; api.md limits
Upgrade отклонён с HTTP 503 {"code":"initializing"}Модель ещё скачивается/квантизируется — опрашивайте /ready, не перезапускайтеtroubleshooting
Браузерное приложение с другого origin не подключаетсяAllowlist origin — по умолчанию только loopback; добавьте --allow-origindocs/cli.md
Финалы приходят в голом нижнем регистре, без пунктуацииПунктуационная модель не подключена или политика выключена; e2e_rnnt пунктуирует самРецепт 2; troubleshooting
configure не действуетОтправлен после первого аудиофрейма (configure_too_late) — шлите сразу после readyРецепт 1
Транскрипта нет вообщеТри независимых домена отказа: готовность сервера, захват аудио, язык/головатриаж troubleshooting

Deepgram-совместимый режим WebSocket (drop-in эндпоинт для клиентов Deepgram) в работе; эта глава покрывает только нативный протокол.

Ссылки

Десктоп и встраивание: Swift/SPM, sidecar, Electron, UniFFI

Сценарий

Вы пишете десктопное или мобильное приложение с локальным распознаванием русской речи: диктофон на Swift под macOS, записывающее приложение для встреч на Electron или инструмент на Kotlin / Python. Модель должна работать локально (без облака), и движок можно поставить двумя способами — встроить в процесс через нативный биндинг или запустить gigastt serve как sidecar-подпроцесс и общаться с ним по loopback HTTP/WS API. Эта глава помогает выбрать путь и проводит по каждому до первого транскрипта.

Какой путь выбрать: embedded или sidecar

Embedded (в процессе)Sidecar (gigastt serve + клиент)
КакДвижок слинкован с приложением: SwiftPM GigaSTT, npm gigastt, PyPI gigastt, UniFFI-биндингиДвижок работает дочерним процессом; приложение общается по WS/REST через loopback-порт
ИнтерфейсНативные вызовы; типизированные ошибки (throws / исключения / rejected promises)Сетевой протокол (/v1/ws, REST /v1/transcribe, SSE)
ДеплойОдна установка пакета; без супервизии процессовПоиск бинаря на машине пользователя, spawn, супервизия, выбор порта
ПамятьМодель и пул живут внутри вашего приложения (~350–400 МБ RSS на сессию пула)Модель живёт в отдельном серверном процессе
КонкурентностьОдин движок/пул на процесс приложенияОдин сервер на несколько приложений/клиентов
Изоляция паденийПадение движка роняет приложение (и наоборот)Падение сервера изолировано; приложение выживает и может его перезапустить
ОбновленияПередеплой приложения с новым движкомБинарь сервера обновляется независимо от клиентов
ВерсионированиеПриложение и движок — один артефактПриложение должно гейтоваться на версию найденного сервера (/healthversion)

Правила выбора:

  • Embedded, когда приложение владеет всем аудио-конвейером (диктофон, рекордер) и является единственным потребителем. На iOS это единственный вариант — приложения не могут запускать подпроцессы.
  • Sidecar, когда один движок делят несколько клиентов, когда приложение должно переживать падение движка или когда движок нужно обновлять без передеплоя приложения. Оба подтверждённых внешних десктоп-интегратора используют именно этот паттерн.

Предпосылки

  • Директория с моделью — все рецепты ниже её предполагают. Один раз скачайте преквантизованный INT8-бандл (~215 МБ, без скачивания FP32 и без квантизации на устройстве):

    gigastt download --prequantized          # -> ~/.gigastt/models
    
  • Embedded: тулчейн вашего биндинга — Xcode 15+ (Swift), Node.js (npm-пакет), Python 3 (wheel) или Android SDK/NDK (Kotlin).

  • Sidecar: бинарь gigastt — забандленный в ресурсы приложения или установленный (brew tap ekhodzitsky/gigastt https://github.com/ekhodzitsky/gigastt && brew install gigastt, тарбол релиза или cargo install gigastt) — плюс curl для проб.

Рецепт — Swift/SPM (iOS + macOS)

Swift-пакет GigaSTT оборачивает C ABI в безопасный Swift-интерфейс. Нативный код поставляется готовым GigasttFFI.xcframework (iOS device arm64, симулятор arm64/x86_64, macOS arm64) со статически слинкованным ONNX Runtime — отдельный рантайм бандлить не нужно. Требования: iOS 15 / macOS 13 (только Apple Silicon — слайса для Intel macOS нет) и Xcode 15+.

  1. Добавьте пакет. Xcode → File → Add Package Dependencies… → введите URL зеркала https://github.com/ekhodzitsky/gigastt-swift и добавьте продукт GigaSTT к вашему таргету. Зеркало — каноничный удалённый источник (SwiftPM требует Package.swift в корне репозитория, поэтому поддиректорию монорепо packaging/swift нельзя подключить по URL — используйте её только как локальную path-зависимость для разработки).

  2. Забандлите модель. Скопируйте ~/.gigastt/models в таргет приложения как folder reference (синяя папка — сохраняет структуру директорий; жёлтая «group» плющит файлы, и движок их не найдёт). Альтернатива — скачать модель при первом запуске и закешировать.

  3. Загрузите движок и транскрибируйте:

    import GigaSTT
    
    guard let modelDir = Bundle.main.url(
        forResource: "models", withExtension: nil
    )?.path else {
        fatalError("bundle the model directory as a folder reference")
    }
    
    // poolSize: 1 keeps RAM around ~350 MB, recommended on device.
    let engine = try Engine(modelDir: modelDir, poolSize: 1)
    
    // Path is relative to the current working directory; absolute paths and
    // ".." are rejected by the engine.
    let text = try engine.transcribeFile(path: "audio.wav")
    print(text)
    
  4. Стриминг — чанки little-endian mono PCM16 на частоте захвата (внутри ресемплируется в 16 кГц):

    let stream = try Stream(engine: engine)
    
    // pcm16: Data of little-endian Int16 mono samples at 48 kHz.
    for segment in try stream.processChunk(pcm16, sampleRate: 48000) {
        print(segment.text, segment.isFinal)
    }
    
    // Drain the tail at end-of-stream.
    for segment in try stream.flush() {
        print(segment.text)
    }
    

    processChunk и flush возвращают [TranscriptSegment]; каждый сегмент несёт text, words (по каждому слову word/start/end/confidence и опциональный speaker), isFinal и timestamp.

  5. Обрабатывайте ошибки. Обёртка бросает GigasttError: engineLoadFailed(modelDir:) (директория модели отсутствует/нечитаема), streamCreationFailed (не удалось занять сессию пула), inferenceFailed и decodingFailed(underlying:). C ABI сигналит об ошибке NULL-возвратом, поэтому кейс говорит, где произошёл сбой, а не несёт сообщение движка.

Проверка: запустите приложение, транскрибируйте заведомо известный WAV и убедитесь, что напечатан ожидаемый текст. Если движок бросает engineLoadFailed при старте — директории модели нет там, куда указывает Bundle.main.url(forResource: "models", withExtension: nil); см. «Частые ошибки».

Рецепт — sidecar-сервер (macOS / Electron)

Запускаем gigastt serve как управляемый дочерний процесс. Полный жизненный цикл: найти бинарь, предустановить модель, запустить, дождаться готовности, транскрибировать, корректно остановить.

  1. Найдите бинарь в порядке приоритета: env-override (например, MYAPP_GIGASTT_BIN) → копия в ресурсах приложения → /opt/homebrew/bin/gigastt/usr/local/bin/gigasttPATH. Залогируйте, какой вариант выбран.

  2. Предустановите модель при установке или первом запуске, с машиночитаемым прогрессом для UI:

    gigastt download --prequantized --progress json
    

    stdout несёт по одному NDJSON-событию на строку ({"phase":"download","file":...,"bytes_done":N,"bytes_total":M}, затем verify, затем done), а коды выхода различают сетевые/дисковые/контрольные ошибки — см. docs/cli.md. --prequantized пропускает ~2-минутный проход INT8-квантизации на устройстве, поэтому первый serve стартует за секунды, а не минуты.

  3. Выберите порт. Сегодня: фиксированный высокий порт (например, 49876). Автовыбор эфемерного порта (--port 0 с машиночитаемой строкой LISTENING), а также --die-with-parent и --log-file — планируемые дополнения сервера: они требуют версии gigastt, где эти флаги есть, поэтому пока не полагайтесь на их синтаксис; используйте фиксированный порт и жизненный цикл ниже.

  4. Запустите и собирайте логи. Никогда не отправляйте вывод sidecar’я в /dev/null — перенаправьте stdout/stderr в лог-файл (если подключаете пайпы вместо файла, непрерывно их читайте: полный буфер пайпа может заблокировать дочерний процесс). Пример для главного процесса Electron / Node:

    import { spawn } from 'node:child_process';
    import fs from 'node:fs';
    
    const PORT = 49876;
    const BASE = `http://127.0.0.1:${PORT}`;
    
    const log = fs.createWriteStream('gigastt-sidecar.log', { flags: 'a' });
    const server = spawn(gigasttBin, [
      'serve',
      '--port', String(PORT),
      '--pool-size', '1',
      '--model-dir', modelDir,
    ], { stdio: ['ignore', log, log] });
    
    async function waitForReady(timeoutMs = 120_000) {
      const deadline = Date.now() + timeoutMs;
      for (;;) {
        try {
          const res = await fetch(`${BASE}/ready`);
          // 200 {"status":"ready","pool_available":N,"pool_total":M}
          if (res.ok) return;
          // 503 {"status":"not_ready","reason":"initializing"} — keep waiting.
        } catch {
          // Connection refused: the listener is not up (yet) — or the process
          // is gone. Distinguish by the child exit code, not by killing it.
          if (server.exitCode !== null) {
            throw new Error(`gigastt exited with code ${server.exitCode}`);
          }
        }
        if (Date.now() > deadline) throw new Error('readiness timeout');
        await new Promise((resolve) => setTimeout(resolve, 500));
      }
    }
    
  5. Гейт по /ready, а не по TCP-connect. Сервер биндит порт сразу и отвечает на пробы из bootstrap-ответчика, пока модель грузится, поэтому «порт слушается» не значит «готов». Поллите GET /ready до 200; тело 503 несёт {"status":"not_ready","reason":"initializing"|"pool_exhausted"|"shutting_down"}. Connection-refused означает, что процесс мёртв или ещё не запущен, — а не что он завис.

  6. Версионное рукопожатие по HTTP. GET /health возвращает 200 в обеих фазах — {"status":"ok","model":"loading","version":"2.14.1"} во время bootstrap, затем {"status":"ok","model":"gigaam-v3-rnnt","variant":"rnnt","version":"2.14.1","punctuation":true,"itn":true}. Гейт минимальной версии движка делайте по полю version, а не запуском gigastt --version подпроцессом.

  7. Транскрибация. Для live-частичных результатов откройте WebSocket-сессию на /v1/ws (паттерны — в главе Стриминг по WebSocket); для целых файлов — POST на /v1/transcribe (рецепты — в главе CLI и пакетная обработка). При насыщении пула сервер отвечает 503 + Retry-After (REST) или ошибкой с retry_after_ms (WS) — соблюдайте подсказку вместо выдумывания своего backoff’а. Перед закрытием WS-сессии отправьте {"type":"stop"}, чтобы сервер дописал хвост в финальный сегмент.

  8. Корректная остановка. Пошлите SIGTERM: сервер дожидается завершения активных сессий до --shutdown-drain-secs (по умолчанию 10), шлёт final и закрывает WS-клиентов кодом 1001. Эскалируйте до SIGKILL только после окна drain. Отслеживайте pid дочернего процесса и завершайте его при выходе приложения — осиротевший sidecar держит ~1 ГБ RSS и порт.

Проверка:

gigastt serve --port 49876 --pool-size 1 &
curl -s http://127.0.0.1:49876/ready    # -> {"status":"ready","pool_available":1,"pool_total":1}
curl -s http://127.0.0.1:49876/health   # -> {"status":"ok","model":"gigaam-v3-rnnt",...,"version":"..."}
kill %1                                  # SIGTERM — process exits within the drain window

Рецепт — Node/Electron в процессе

npm-пакет gigastt (napi-rs) встраивает движок внутрь вашего Node/Electron процесса — без sidecar’я, порта и версионного гейта. Инференс идёт на рабочем потоке libuv, поэтому вызовы возвращают Promise и не блокируют event loop; onnxruntime слинкован статически, поэтому .node-аддон самодостаточен.

  1. Установка: npm install gigastt. postinstall скачивает ровно один готовый бинарь (gigastt.<platform>.node, ~47 МБ) под платформу установки из GitHub-релиза. Готовые платформы: darwin-arm64, linux-x64-gnu, linux-arm64-gnu, win32-x64-msvc (Intel macOS нет).

  2. Загрузите модель — она не забандлена: gigastt download --prequantized~/.gigastt/models, либо задайте GIGASTT_MODEL_DIR.

  3. Использование:

    const { Engine, Stream } = require('gigastt');
    
    const engine = new Engine('/path/to/gigastt/models'); // new Engine(modelDir, poolSize?)
    const t = await engine.transcribeFile('recording.wav');
    console.log(t.text, t.durationS);
    for (const w of t.words) console.log(w.text, w.startS, w.endS, w.confidence);
    
    // streaming — await each chunk before sending the next to keep order
    const s = new Stream(engine);
    for (const seg of await s.processChunk(pcm16, 16000)) console.log(seg.text);
    console.log((await s.flush()).map((seg) => seg.text));
    
    // errors are thrown JS Errors whose message starts with a stable code
    try { new Engine('/no/such/dir'); } catch (e) { /* e.message starts "ModelNotFound:" */ }
    
  4. Схема для Electron: создавайте Engine в главном процессе (никогда в renderer’е) и держите по одному Stream на аудиоканал (например, mic + system). Каждый Stream держит одну сессию пула всё своё время жизни, поэтому poolSize должен покрывать число живых стримов — третий Stream на пуле из 2 бросит PoolExhausted. Полный двухканальный паттерн с IPC-обработчиками — в examples/electron_main.mjs.

  5. Размер thread-пула. Инференс идёт на рабочем пуле libuv (по умолчанию 4 потока, общие с fs/crypto). Для N одновременно транскрибирующих каналов запускайте с UV_THREADPOOL_SIZE >= N (например, UV_THREADPOOL_SIZE=4 electron .) и соответствующим poolSize.

  6. Упаковка (asar): держите директорию модели и нативный .node-аддон вне asar-архива — нативный код мапит веса в память и делает dlopen аддона, а оба этих действия невозможны из виртуальной ФС asar. В electron-builder используйте asarUnpack для **/*.node и поставляйте модель через extraResources.

Проверка: в чекауте репозитория GIGASTT_MODEL_DIR=~/.gigastt/models npm run smoke (из crates/gigastt-node) транскрибирует тестовую фикстуру; в вашем приложении — дождитесь engine.transcribeFile на заведомо известном WAV и проверьте текст.

Рецепт — Kotlin и Python (UniFFI)

crates/gigastt-uniffi генерирует идиоматичные биндинги для Swift, Kotlin и Python из одного Rust-источника через UniFFI. Они оборачивают синхронное урезанное ядро (модели подгружаются с диска, без tokio-рантайма), дают типизированные ошибки GigasttFfiError (ModelNotFound, InvalidAudio, PoolExhausted, Inference, InvalidArgument) и управляют объектами по счётчику ссылок.

Общая поверхность (регистр имён следует идиомам языка):

  • Engine(model_dir) / Engine.new_with_pool_size(model_dir, pool_size) · transcribe_file(path) -> Transcript { text, words, duration_s }
  • Stream(engine) · process_chunk(pcm16, sample_rate) -> [TranscriptSegment] · flush() -> [TranscriptSegment]
  • TranscriptSegment { text, words, is_final } · Word { text, start_s, end_s, confidence, speaker }

Python — опубликован. pip install gigastt ставит самодостаточный готовый wheel (py3-none-<platform>; onnxruntime слинкован статически):

import gigastt_uniffi as g

engine = g.Engine("/path/to/gigastt/models")        # side-loaded model dir
t = engine.transcribe_file("recording.wav")
print(t.text)
for w in t.words:
    print(w.text, w.start_s, w.end_s, w.confidence)

# streaming
s = g.Stream(engine)
for seg in s.process_chunk(pcm16_bytes, 16000):
    print(seg.text)
print([seg.text for seg in s.flush()])

# typed errors
try:
    g.Engine("/no/such/dir")
except g.GigasttFfiError.ModelNotFound as e:
    ...

Kotlin — экспериментальный AAR. Кросс-сборка Rust доказана (CI кросс-компилирует libgigastt_uniffi.so под каждый ABI через cargo-ndk), но сборка и публикация Gradle/Maven AAR ещё не проверены end-to-end. Локальная сборка:

# native libs per ABI
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 \
  -o packaging/android/gigastt/src/main/jniLibs build --release -p gigastt-uniffi
# Kotlin bindings (from a host build of the cdylib; metadata is arch-independent)
cargo build --release -p gigastt-uniffi
cargo run --release -p gigastt-uniffi --bin uniffi-bindgen -- generate \
  --library target/release/libgigastt_uniffi.* --language kotlin \
  --out-dir packaging/android/gigastt/src/main/kotlin
# assemble
cd packaging/android && gradle :gigastt:assembleRelease

Swift (UniFFI): биндинги генерируются (--language swift), но поставляемый SwiftPM-пакет использует рукописную обёртку над C ABI из Swift-рецепта выше — для разработки приложений берите её.

Перегенерация любого биндинга из чекаута:

cargo build -p gigastt-uniffi
LIB=target/debug/libgigastt_uniffi.dylib   # .so on Linux

cargo run -p gigastt-uniffi --bin uniffi-bindgen -- generate --library "$LIB" --language python --out-dir bindings/python
cargo run -p gigastt-uniffi --bin uniffi-bindgen -- generate --library "$LIB" --language swift  --out-dir bindings/swift
cargo run -p gigastt-uniffi --bin uniffi-bindgen -- generate --library "$LIB" --language kotlin --out-dir bindings/kotlin

Сгенерированные биндинги — артефакты сборки (bindings/ в git-игноре). Статус и детали упаковки: crates/gigastt-uniffi/README.md, packaging/android/README.md.

Проверка: Python-сниппет выше печатает транскрипт заведомо известного файла; для Kotlin — соберите AAR и транскрибируйте из instrumented-теста или отладочного экрана.

Проверка результата

Сквозной чек-лист для любого из путей:

  • Первый транскрипт: приложение печатает ожидаемый текст для известного WAV (любой файл с русской речью, например сэмпл Golos).
  • Модель на диске: ls ~/.gigastt/models показывает файлы v3_rnnt_* (бандл --prequantized включает v3_rnnt_encoder_int8.onnx).
  • Память: RSS в ожидаемых пределах — примерно 350–400 МБ на сессию пула; poolSize/pool-size 1 — правильный дефолт для устройства.
  • Здоровье sidecar’я: curl -s http://127.0.0.1:<port>/ready возвращает {"status":"ready",...}, а /health показывает version, на которую вы гейтованы; SIGTERM завершает процесс в пределах окна drain с чистым хвостом лога.
  • Порядок в стриминге: частичные сегменты (isFinal == false) могут пересматриваться; сохраняйте только финальные, а перед завершением сессии вызывайте flush() (или шлите {"type":"stop"} по WS), чтобы не потерять хвост.

Частые ошибки

  • Модель забандлена не как folder reference (Swift). Добавленная жёлтой «group», Xcode плющит директорию, и движок падает при старте с engineLoadFailed. Исправление: добавьте models как folder reference (синяя папка) и в отладочном запуске проверьте, что Bundle.main.url(forResource: "models", withExtension: nil) не nil.
  • Неопределённые символы std::__1::* при линковке (Swift). ONNX Runtime — это C++; актуальный пакет уже объявляет .linkedLibrary("c++") — такая ошибка означает, что вы используете старый или самосборный xcframework. Берите релизный пакет из зеркала (или актуальный packaging/swift).
  • Блокирующий вызов в UI-потоке. Вызовы Engine/Stream синхронны, а хендлы не thread-safe. Выполняйте всю работу движка на выделенной фоновой очереди/акторе (Swift) и сериализуйте доступ; никогда не транскрибируйте на главном потоке. В Node вызовы уже идут на пуле libuv — просто делайте await.
  • Таймауты готовности при первом запуске (sidecar). Обычный gigastt download оставляет ~2-минутный проход INT8-квантизации на первый serve, а модель пунктуации подгружается лениво при первом старте — клиент с таймаутом загрузки 10–30 с сдаётся слишком рано. Исправление: предустановка через gigastt download --prequantized, щедрый таймаут готовности и ветвление по reason из /ready вместо убийства процесса.
  • Убийство сервера на 503. Пока модель грузится, порт занят bootstrap-ответчиком и каждый API отвечает 503 initializing — это прогресс, а не зависание. Признак сбоя — только connection-refused вместе с мёртвым дочерним процессом.
  • Коллизия захардкоженного порта. Занятый фиксированный порт (часто — осиротевшим gigastt от прошлого запуска) проявляется как молчаливый таймаут. Проверяйте /healthversion, чтобы понять, ваш ли это сервер, и завершайте дочерний процесс при выходе приложения (флаг --die-with-parent в планах).
  • Логи sidecar’я в /dev/null. Они понадобятся в первый же раз, когда директория модели окажется битой. Перенаправляйте stdout/stderr в файл — а если используете пайпы, непрерывно их читайте во избежание дедлока на буфере пайпа.
  • require('gigastt') падает после npm install --ignore-scripts. postinstall, скачивающий нативный бинарь, был пропущен; запустите node install.js внутри пакета.
  • Модель или аддон внутри asar (Electron). Нативный код мапит веса в память и делает dlopen для .node — обоим нужны реальные файлы. asarUnpack для аддона и поставка модели через extraResources.
  • Абсолютный путь в transcribeFile (Swift). C ABI принимает только относительные пути внутри рабочей директории — сначала направьте рабочую директорию на расположение файла (или скопируйте файл туда).

Ссылки

В этой книге:

Справочные материалы (не дублируем — читайте здесь):

Заметка по дистрибуции macOS. Нотаризация .app / Developer ID — вне этой книги: используйте tooling Apple для своего signed-приложения + sidecar или embedded framework. Движок из GitHub Releases — unsigned OSS-бинарь; версию гейть через /healthversion, а не через code signature.

Развёртывание и эксплуатация

Сценарий

Вы — администратор, который запускает gigastt на сервере, а не на ноутбуке. Маршрут этой главы: установить → ограничить → наблюдать → обновлять — один управляемый сервис (systemd или Docker), метрики в Prometheus/Grafana, алерты на важные режимы отказа и процедура обновления, не обрывающая живые сессии транскрибации.

Каждый рецепт заканчивается шагом «Проверить». Флаги сверены с gigastt serve --help; полный справочник флагов живёт в docs/cli.md и здесь не повторяется.

Предпосылки

  • gigastt установлен (бинарник, пакет или образ) — см. Начало работы.
  • Linux-хост с 4+ ГБ RAM: при --pool-size 2 по умолчанию с INT8-энкодером RSS составляет ~790 МиБ; оставьте запас под ОС и пики запросов.
  • Для пути с systemd: systemd 241 или новее (любой современный дистрибутив, включая Astra Linux, RED OS, ALT) и root-доступ.
  • Для пути с Docker: Docker 20.10+; NVIDIA Container Toolkit — только для CUDA-варианта.
  • Модель либо скачивается один раз (~850 МБ FP32, автоматически квантуется в INT8 при первом запуске), либо предустановлена из офлайн-бандла / deb с моделью.

Рецепт

Docker

Каждый тегированный релиз публикует мультиархитектурные образы в GHCR — предпочтительнее тянуть готовое, а не собирать:

docker pull ghcr.io/ekhodzitsky/gigastt:2.14.1        # CPU, linux/amd64 + linux/arm64
docker pull ghcr.io/ekhodzitsky/gigastt:2.14.1-cuda   # CUDA, linux/amd64

Закрепляйте конкретный тег для воспроизводимых развёртываний; :latest / :cuda — плавающие.

Запускайте с именованным томом, чтобы модель ~850 МБ (и автоматически сгенерированный INT8-энкодер) переживали замену контейнера:

docker run -d --name gigastt \
  -p 127.0.0.1:9876:9876 \
  -v gigastt-models:/home/gigastt/.gigastt/models \
  ghcr.io/ekhodzitsky/gigastt:2.14.1

Примечания:

  • Команда образа по умолчанию — serve --port 9876 --host 0.0.0.0 --bind-all (это нужно контейнерной сети); -p 127.0.0.1:9876:9876 оставляет доступ с хоста только на loopback. TLS-прокси ставится впереди точно так же, как при установке без контейнера.
  • Контейнер работает под непривилегированным пользователем gigastt; каталог моделей внутри — /home/gigastt/.gigastt/models, именно он монтируется томом.
  • В образ встроен HEALTHCHECK на /health, так что docker ps покажет healthy, как только порт начнёт отвечать. Во время первичного скачивания модели и INT8-квантования (~2 мин) /health отвечает 200 с model:"loading", а /ready503 {"status":"not_ready","reason":"initializing"} — пропускайте трафик по /ready, а не по /health.
  • Baked-образ (нулевой холодный старт, +~850 МБ): соберите локально с моделью внутри — docker build --build-arg GIGASTT_BAKE_MODEL=1 -t gigastt:baked .
  • CUDA: docker run --gpus all -p 127.0.0.1:9876:9876 ghcr.io/ekhodzitsky/gigastt:2.14.1-cuda (требуется NVIDIA Container Toolkit; при отсутствии GPU бинарник откатывается на CPU).

Проверить:

curl -s http://127.0.0.1:9876/ready
# {"status":"ready","pool_available":2,"pool_total":2}
curl -s http://127.0.0.1:9876/health
# {"status":"ok","model":"gigaam-v3-rnnt","variant":"rnnt","version":"2.14.1","punctuation":true,"itn":true}

Установка без сети (замкнутый контур)

Для хостов без доступа в интернет каждый релиз публикует самодостаточный tarball под каждую Linux-цель — бинарник + предквантованная INT8-модель rnnt + модель пунктуации + systemd-юнит + установщик — и два Debian-пакета (gigastt_<ver>_<arch>.deb + gigastt-model-int8_<ver>_all.deb). Полный состав бандла приведён в README-OFFLINE.md и здесь не повторяется.

На машине с сетью скачайте файлы и проверьте их до переноса в контур (зачем и от каких угроз: docs/verifying-releases.md):

gh release download v2.14.1 -R ekhodzitsky/gigastt \
    -p 'gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz' \
    -p 'gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz.sha256' \
    -p 'gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz.minisig'
sha256sum -c gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz.sha256
minisign -Vm gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz -p gigastt.pub
gh attestation verify gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz \
    --repo ekhodzitsky/gigastt

На целевом хосте:

tar xf gigastt-2.14.1-offline-x86_64-unknown-linux-gnu.tar.gz
cd gigastt-2.14.1-offline
sudo ./install.sh    # verifies SHA256SUMS.txt, then installs binary + models + unit
sudo systemctl enable --now gigastt

Альтернатива для Debian-семейства:

sudo dpkg -i gigastt_2.14.1_amd64.deb gigastt-model-int8_2.14.1_all.deb
sudo systemctl enable --now gigastt

Модель уже в INT8 — ни скачивания, ни квантования, ни сети. Установленный юнит выставляет GIGASTT_OFFLINE=1 через /etc/gigastt/gigastt.env, поэтому любой путь кода, который попытался бы скачать модель (включение --vad, диаризация, альтернативная голова распознавания), падает быстро с ошибкой, называющей нужный файл, вместо зависания на connect timeout. Чтобы добавить опциональные модели позже, выполните gigastt download на машине с сетью и скопируйте файлы в /usr/share/gigastt/models/.

Типовые ошибки:

  • install.sh прерывается с sha256sum: WARNING: 1 computed checksum did NOT match — tarball повреждён при переносе в замкнутый контур. Проверьте внешний .sha256, скопируйте заново и повторите; частичной установки не происходит.
  • Ошибка офлайн-режима, называющая отсутствующий файл (например, модель VAD после включения --vad) — этой модели нет в бандле; скачайте её на машине с сетью и скопируйте.

Проверить:

systemctl is-active gigastt
# active
curl -s http://127.0.0.1:9876/health
# {"status":"ok",...} — served immediately, the model is pre-installed

systemd-сервис

Усиленный юнит лежит в packaging/systemd/ и устанавливается как deb-пакетом, так и офлайн-бандлом. Ключевые свойства (сам юнит короткий и с комментариями — полный список читайте в нём):

  • Запуск под непривилегированным пользователем gigastt; модели в /usr/share/gigastt/models доступны ему только на чтение.
  • Прослушивание только loopback (127.0.0.1:9876); наружу API выставляется через reverse proxy.
  • Restart=on-failure, RestartSec=5 — падение перезапускается, чистый systemctl stop — нет.
  • Набор харденинга, совместимый с systemd 241 (ProtectSystem=strict, NoNewPrivileges, PrivateTmp, …), поэтому юнит работает без изменений на Astra Linux, RED OS и ALT.
  • Переопределения живут в /etc/gigastt/gigastt.env (переменные GIGASTT_*, RUST_LOG) и подхватываются через EnvironmentFile.

Логи идут в журнал:

journalctl -u gigastt -f          # follow
journalctl -u gigastt -n 100      # recent

Уровень логирования меняется правкой /etc/gigastt/gigastt.env (RUST_LOG=gigastt=debug), затем sudo systemctl restart gigastt.

Флаги меняйте через drop-in — никогда не правьте поставляемый юнит (обновление пакета его перезапишет). ExecStart нужно сначала очистить, потом задать заново:

# sudo systemctl edit gigastt
[Service]
ExecStart=
ExecStart=/usr/bin/gigastt serve --model-dir /usr/share/gigastt/models --punct-model-dir /usr/share/gigastt/models/punct --metrics

systemctl restart gigastt шлёт SIGTERM; сервер дренирует живые WebSocket/SSE-сессии — каждый клиент получает кадр Final + Close(1001 Going Away) — в течение --shutdown-drain-secs (по умолчанию 10 с), что с запасом укладывается в стандартный стоп-таймаут systemd 90 с. Как использовать это при обновлении версий: Обновление и откат ниже.

Проверить:

systemctl status gigastt --no-pager
curl -s http://127.0.0.1:9876/health

Наблюдаемость

Метрики включаются опционально и отдаются на отдельном слушателе — никогда на порту API, поэтому они находятся вне CORS-allowlist и per-IP rate-лимитера:

gigastt serve --metrics                                  # http://127.0.0.1:9090/metrics
gigastt serve --metrics --metrics-listen 127.0.0.1:9100  # custom port

Держите слушатель на loopback, если только ваш Prometheus не на другом хосте — и даже тогда привязывайте его к доверенному интерфейсу, никогда к публичному.

Минимальная проводка Prometheus (prometheus.yml):

scrape_configs:
  - job_name: gigastt
    static_configs:
      - targets: ["127.0.0.1:9090"]

rule_files:
  - /etc/prometheus/rules/gigastt-alerts.yml   # copy of docs/observability/alerts.yml

Метрики, которые важны (все с префиксом gigastt_):

МетрикаЗначение
gigastt_http_requests_totalЗапросы по path/method/status — доля 5xx, 503
gigastt_http_request_duration_secondsГистограмма HTTP-латентности (p50/p95/p99)
gigastt_pool_available / gigastt_pool_waitersСвободные триплеты инференса против ожидающих вызовов — сигнал насыщения
gigastt_pool_timeouts_totalТаймауты checkout → клиенты получили 503 + Retry-After
gigastt_inference_timeouts_totalЗапуски, прерванные --inference-timeout-secs
gigastt_inference_duration_secondsГистограмма латентности инференса
gigastt_ws_active_connectionsЖивые WebSocket-сессии
gigastt_rate_limit_rejections_totalОтветы 429 от per-IP лимитера
gigastt_batch_pool_available / gigastt_batch_pool_waitersТе же метрики пула для раздела --batch-pool-size

Готовые артефакты — импортируйте, не изобретайте:

  • docs/observability/alerts.yml — правила Prometheus: 5xx выше 5%, gigastt_pool_available == 0 в течение 1 мин, p95 выше 10 с, устойчивые таймауты пула, падение health-пробы (blackbox exporter).
  • docs/observability/dashboard.json — дашборд Grafana (Dashboards → Import): частота запросов, латентность, 5xx, доступность пула, активные WebSocket, длительность инференса, отказы rate-лимитера.

Что алертить на практике: насыщение пула (gigastt_pool_available == 0 устойчиво — клиенты получают 503), долю 5xx и RAM на уровне узла (gigastt не экспортирует метрику собственного RSS; используйте node_exporter или cAdvisor).

Логи: env-фильтр tracing через RUST_LOG (по умолчанию gigastt=info; gigastt=debug для разбора). Логи содержат метаданные запросов — длительности, число слов — но никогда текст транскриптов (docs/privacy.md).

Проверить:

curl -s http://127.0.0.1:9876/ready > /dev/null   # samples the pool gauges once
curl -s http://127.0.0.1:9090/metrics | grep '^gigastt_pool_available'
# gigastt_pool_available 2

Безопасность по умолчанию

Значения по умолчанию — это уже усиленная конфигурация, поэтому рецепт в основном о том, как её не ослабить:

  • Привязка к loopback. serve отказывается слушать не-loopback адреса, пока не задан --bind-all / GIGASTT_ALLOW_BIND_ANY=1. Удалённый доступ = TLS-терминирующий reverse proxy на том же хосте (конфиги Caddy/nginx: docs/deployment.md).
  • Origin-allowlist. Loopback-источники разрешены всегда; любой другой Origin нужно перечислить через --allow-origin (повторяемый флаг, точное совпадение). --cors-allow-any — только для разработки. Неразрешённые источники получают 403.
  • Лимиты запросов. --body-limit-bytes (по умолчанию 50 МиБ), --ws-frame-max-bytes (512 КиБ), --idle-timeout-secs (300), --max-session-secs (3600), --inference-timeout-secs (600), --pool-checkout-timeout-secs (30) — при насыщении пула 503 + Retry-After.
  • Rate-лимитинг (опционально): --rate-limit-per-minute N с --rate-limit-burst429 + Retry-After. За прокси он работает по-клиентски, только если прокси перезаписывает X-Forwarded-For и задан --trust-proxy — копируйте сниппеты прокси из docs/deployment.md дословно.
  • Целостность модели. Скачивания проверяются по SHA-256 и атомарно переименовываются (.partial → финальный); повреждённый файл никогда не попадает в каталог моделей.
  • Верификация релизов. У каждого артефакта релиза есть .sha256-сайдкар + SHA256SUMS.txt, подпись minisign, CycloneDX SBOM и SLSA-провенанс сборки. Проверяйте перед установкой — docs/verifying-releases.md. Минимальный ритуал:
minisign -Vm gigastt-2.14.1-x86_64-unknown-linux-gnu.tar.gz -p gigastt.pub
gh attestation verify gigastt-2.14.1-x86_64-unknown-linux-gnu.tar.gz \
    --repo ekhodzitsky/gigastt
  • Приватность. Нет телеметрии, нет исходящих соединений после разового скачивания модели, транскрипты не логируются (docs/privacy.md).

Проверить:

ss -ltnp | grep 9876
# tcp LISTEN 0 ... 127.0.0.1:9876 ...   (loopback only)
curl -s -o /dev/null -w '%{http_code}\n' \
    -H 'Origin: https://attacker.example' http://127.0.0.1:9876/v1/models
# 403

Горячая перезагрузка модели без рестарта

Когда вы заменили файлы модели на диске (новый INT8-энкодер, другая голова, обновлённая punct-модель), движок можно пересобрать на месте, не останавливая serve:

# Только с loopback — не-loopback клиенты получают 403 loopback_only
# даже при --bind-all.
curl -s -X POST http://127.0.0.1:9876/v1/admin/reload
# {"reloaded":true,"variant":"rnnt","encoder":"int8"}

Сервер пересобирает engine по boot-рецепту (каталог моделей, размер пула, punct / ITN / VAD / hotwords), греет новый engine и атомарно подменяет его. Запросы в полёте дорабатывают на старом. Ошибка сборки оставляет прежнюю модель (503 reload_failed). Параллельные reload → 409 reload_in_progress. Полный контракт: docs/api.md — Admin reload.

Проверка:

curl -s -X POST http://127.0.0.1:9876/v1/admin/reload | tee /tmp/reload.json
python3 -c "import json; d=json.load(open('/tmp/reload.json')); assert d['reloaded'] is True"
# From a non-loopback bind (only if you deliberately opened one): expect 403.

Обновление и откат

Закрепляйте то, что разворачиваете (тег образа, версию deb), чтобы обновление было осознанным и обратимым шагом. Каталог моделей — это состояние: он переживает обновления, движок сам определяет установленную голову распознавания, и никакого молчаливого перекачивания при смене бинарника не происходит. В скриптах установки предпочтите резолв latest-тега:

TAG=$(gh api repos/ekhodzitsky/gigastt/releases/latest -q .tag_name)   # e.g. v2.14.1
VER=${TAG#v}

Docker (обновление до выбранного пина — здесь 2.14.1):

docker pull ghcr.io/ekhodzitsky/gigastt:2.14.1
docker stop --time 15 gigastt && docker rm gigastt
docker run -d --name gigastt \
  -p 127.0.0.1:9876:9876 \
  -v gigastt-models:/home/gigastt/.gigastt/models \
  ghcr.io/ekhodzitsky/gigastt:2.14.1

docker stop шлёт SIGTERM; --time 15 даёт окну дренажа (--shutdown-drain-secs, по умолчанию 10 с) завершиться до SIGKILL — стандартные 10 с Docker соревнуются с дренажом. Клиенты получают Final + Close(1001) и переподключаются; короткие REST-загрузки в полёте, возможно, придётся повторить.

systemd / deb:

sudo dpkg -i gigastt_2.14.1_amd64.deb
sudo systemctl restart gigastt
journalctl -u gigastt -f    # expect a clean drain, no "Drain window expired"

В Kubernetes то же правило действует со стороны оркестратора: terminationGracePeriodSecondsshutdown_drain_secs + 5 (полный манифест: docs/deployment.md).

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

docker run -d --name gigastt \
  -p 127.0.0.1:9876:9876 \
  -v gigastt-models:/home/gigastt/.gigastt/models \
  ghcr.io/ekhodzitsky/gigastt:2.14.0
# or: sudo dpkg -i gigastt_2.14.0_amd64.deb && sudo systemctl restart gigastt

Если регрессия дренажа ломает ваших WebSocket-клиентов после обновления, аварийный выход — --shutdown-drain-secs 0 (прижимается к 1 с); полная таблица симптомов — docs/runbook.md.

Проверить (после каждого обновления или отката):

curl -s http://127.0.0.1:9876/health
# "version" is the release you deployed; "model"/"variant" are unchanged
curl -s http://127.0.0.1:9876/ready

Проверка результата

Сквозной смоук после любого рецепта выше:

systemctl is-active gigastt || docker ps --filter name=gigastt --format '{{.Status}}'
curl -s http://127.0.0.1:9876/health     # status ok, expected version
curl -s http://127.0.0.1:9876/ready      # ready, pool_available >= 1
curl -s http://127.0.0.1:9090/metrics | grep '^gigastt_pool_available'

Затем транскрибируйте один короткий файл через тот API, который реально выставлен (REST-рецепты: CLI и пакетная обработка; проверка через CLI: Начало работы).

Частые ошибки

  • OOM — контейнер или сервис убит. RSS растёт вместе с --pool-size: INT8-энкодер — ~400 МиБ на триплет, ~790 МиБ при пуле 2 по умолчанию; FP32-энкодер примерно в 4 раза больше (никогда не передавайте --skip-quantize в production). На машине с 4 ГБ держите --pool-size в пределах 1–2; --pool-min-size 1 позволяет серверу подняться на деградированном пуле вместо падения при нехватке памяти. Если Kubernetes сообщает OOMKilled, уменьшите пул или поднимите лимит пода — подробности в docs/runbook.md.
  • 503 timeout под нагрузкой. Все триплеты заняты, и вызывающий дождался конца --pool-checkout-timeout-secs (30 с): REST получает 503 + Retry-After, WebSocket — ошибку с retry_after_ms. Это противодавление, а не баг — поднимите --pool-size, отделите пакетную работу через --batch-pool-size и следите за gigastt_pool_waiters / gigastt_pool_timeouts_total.
  • /metrics недоступен с хоста Prometheus. Так задумано: слушатель по умолчанию — 127.0.0.1:9090. Направьте скрейпер на сам хост gigastt или осознанно перепривяжите порт через --metrics-listen на доверенном интерфейсе — никогда на публичном. Скрейпер, по-прежнему смотрящий на :9876/metrics, получит 404: метрики убраны с порта API.
  • Флапающие readiness-пробы при первом запуске. Первый запуск скачивает ~850 МБ и квантует (~2 мин). Всё это время /health возвращает 200 с model:"loading", но /ready возвращает 503 initializing. Если ваш балансировщик маршрутизирует по /health, ранние клиенты получат 503 — пробуйте /ready или предустановите / запеките модель, чтобы окно исчезло.
  • Rate-лимитер штрафует всех за прокси. Без --trust-proxy — и без прокси, перезаписывающего, а не дописывающего X-Forwarded-For — все клиенты делят один бакет, ключованный адресом прокси. Симптомы и точная конфигурация прокси: docs/deployment.md.

Ссылки

Модели и бэкенды

Сценарий

gigastt уже работает с головой rnnt по умолчанию на CPU-бэкенде, и теперь нужно осознанно что-то поменять: другую голову распознавания (пунктуация «из коробки» или языки помимо русского), более лёгкую загрузку модели, более быстрый execution provider под ваше железо или больший пул сессий. Эта глава отвечает на четыре вопроса проверяемыми рецептами: какую голову, INT8 или FP32, какой бэкенд и сколько RAM нужно пулу.

Цифры WER и RTF здесь не дублируются — канонические таблицы с доверительными интервалами живут в docs/benchmarks.md; глава лишь ссылается на них. Флаги сверены с gigastt <command> --help; полный справочник флагов — docs/cli.md.

Предпосылки

  • Установленный gigastt (бинарь, пакет или образ) — см. Начало работы.
  • Диск: ~1,1 ГБ свободно для стандартного пути с FP32-загрузкой (FP32-набор плюс сгенерированный INT8-энкодер) или ~250 МБ для лёгкого pre-quantized пути.
  • Для сборки нестандартного бэкенда из исходников: Rust 1.88+ и protoc в PATH (требования сборки: docs/architecture.md).

Рецепт

Выбор головы распознавания

Голова выбирается флагом --model-variant (env GIGASTT_MODEL_VARIANT) у команд serve / download / transcribe. Все головы используют общий mel-фронтенд и контракт входа 16 кГц моно; различаются ONNX-файлами, словарём и декодированием.

ГоловаРазмер на дискеЯзыкиТекст на выходеТочностьКогда брать
rnnt (по умолчанию)энкодер 844 МБ FP32 → ~215 МБ INT8 (авто-квантизация) + decoder/joiner/vocab (несколько МБ)русский«Голый» lowercase; дополняйте --punctuation / --itn (включены по умолчанию в режиме auto)Лучший русский WER из четырёх — таблицаРусскоязычные нагрузки; дефолт не случаен
e2e_rnntТот же класс размера, что у rnnt (~850 МБ FP32 → INT8 генерируется локально)русскийПунктуация / регистр / ITN встроены, один проходWER выше, чем у rnnt, но лучший F1 пунктуации/регистра — сравнениеНужен читаемый русский текст за один проход, без шага восстановления
ml_ctc~225 МБ pre-quantized INT8, только энкодер (без decoder/joiner)ru/en/kk/ky/uz«Голый» lowercaseМультиязычные таблицыМультиязычное аудио при малом футпринте
ml_ctc_large~592 МБ pre-quantized INT8, только энкодерru/en/kk/ky/uz«Голый» lowercaseЛучшая мультиязычная точность; на чистом русском чтении приближается к rnntтаблицаСмешанные языки или английский/казахский/кыргызский/узбекский как таковые

Два жёстких ограничения, следующих из таблицы:

  • У rnnt / e2e_rnnt кириллический словарь — английский они не могут транскрибировать в принципе. Для английского (или kk/ky/uz) подходят только Multilingual-головы.
  • Multilingual-головы — это encoder-only CTC: они всегда поставляются и работают как pre-quantized INT8 — ни FP32-загрузки, ни шага квантизации на устройстве для них не существует.

Загрузка и запуск другой головы:

gigastt download --model-variant e2e_rnnt
gigastt serve --model-variant e2e_rnnt

Правила автоопределения (из crates/gigastt-core/src/model/mod.rs):

  • Без --model-variant движок определяет установленную голову по файлам в каталоге модели и использует полный комплект как есть (вообще без сетевых запросов).
  • Явный --model-variant, отличный от установленной головы, → запрошенный набор скачивается рядом; головы никогда не смешиваются в одном инференсе, в лог пишется предупреждение. Если файлы нескольких голов сосуществуют, а вариант не задан, автоопределение предпочитает rnnt.
  • Пустой каталог модели + отсутствие флага → скачивается дефолтный rnnt.

Проверка:

curl -s http://127.0.0.1:9876/health
# {"status":"ok","model":"gigaam-v3-e2e-rnnt","variant":"e2e_rnnt",...}
curl -s http://127.0.0.1:9876/v1/models
# .id / .name отражают загруженную голову; .encoder показывает int8 или fp32

Читаемый текст: пунктуация, ITN и hotwords

Дефолтная голова rnnt выдаёт голый lowercase. Читаемый русский — это пост-проход (или голова e2e_rnnt, где пунктуация/регистр/ITN уже внутри).

КнобДефолт (auto)Принудительно onПринудительно off
--punctuation / GIGASTT_PUNCTUATIONon для rnnt, если есть RuPuncton (докачает модель)off
--itn / GIGASTT_ITNon для rnntonoff
REST / WSполитика сервера?punctuation=true / configure?punctuation=false
# Явный читаемый пайплайн на дефолтной голове
gigastt serve --punctuation on --itn on
curl -s http://127.0.0.1:9876/health
# "punctuation":true,"itn":true

# Переопределение на запрос (409 punctuation_not_available, если модели нет и force on)
curl -s -X POST "http://127.0.0.1:9876/v1/transcribe?punctuation=true&itn=true" \
  -H "Content-Type: application/octet-stream" --data-binary @clip.wav

Hotword bias поднимает бренды и доменные термины, которые модель иначе корежит. Одна фраза на строку; опционально встроенный русский lexicon брендов:

cat > /tmp/hotwords.txt <<'EOF'
гигачат
сбер
еком
EOF

gigastt serve --hotwords-file /tmp/hotwords.txt --hotwords-default \
  --hotwords-boost 5.0
# Те же флаги работают на offline `transcribe` / `transcribe-batch` / `watch`.

Проверка: клип с брендом из списка — без файла гипотеза часто ломает токен, с hotwords написание совпадает. Слишком большой boost может «выдумывать» бренды из шума — начните с 5.0. Флаги: docs/cli.md.

INT8 или FP32

Короткий ответ: всегда INT8, если только вы не отлаживаете саму модель. INT8-энкодер работает как настоящие целочисленные вычисления (ядра DynamicQuantizeLinear + MatMulInteger/ConvInteger), сжимает энкодер 844 МБ → 215 МБ (~3,9×) и даёт ~0% деградации WER — цифры и методология: docs/benchmarks.md.

Что происходит на каждом пути (RNN-T-головы):

  • По умолчаниюgigastt download (или первый serve / transcribe) скачивает FP32-набор с HuggingFace (istupakov/gigaam-v3-onnx), затем однократно (~2 мин) выполняет квантизацию нативным Rust-кодом и пишет v3_rnnt_encoder_int8.onnx рядом. Движок предпочитает INT8-энкодер, когда файл присутствует.
  • Лёгкий путьgigastt download --prequantized скачивает готовый INT8-бандл (INT8-энкодер + decoder + joiner + vocab) из закреплённого GitHub Release: ни ~844 МБ FP32-загрузки, ни ~2-минутной квантизации. Это также запасной вариант, когда HuggingFace недоступен, а GitHub — нет.
  • Multilingualml_ctc / ml_ctc_large скачивают pre-quantized INT8-энкодер istupakov’а напрямую с HuggingFace; --prequantized для них — холостое уточнение (отдельного бандла нет).
  • Вручнуюgigastt quantize [--force] повторно запускает квантизацию для головы, определённой в --model-dir (например, после подмены FP32- энкодера своим файнтюном).
  • Отказ--skip-quantize (env GIGASTT_SKIP_QUANTIZE=1) пропускает шаг квантизации; движок тогда загружает FP32-энкодер с ~4× расходом RAM на слот пула и более медленными CPU-ядрами. Оставьте это только для отладки.

Проверка:

ls ~/.gigastt/models/
# v3_rnnt_encoder_int8.onnx присутствует рядом с decoder/joint/vocab
gigastt transcribe sample.wav 2>&1 | grep 'transcribe complete'
# ... encoder=int8/cpu ... rtf=0.1xx

Поле encoder=int8/<backend> в строке лога о завершении — истина о том, какой файл энкодера был загружен.

Бэкенд под ваше железо

Бэкенд — это compile-time Cargo-фича, а не runtime-флаг: провайдер зашит в бинарь, который вы устанавливаете или собираете:

Ваше железоФичаПровайдерПримечания
Любое (по умолчанию)CPU (ONNX Runtime)Эталонная сборка; RTF заметно ниже 1.0 с INT8-энкодером
macOS ARM64 (M1–M4)--features coremlCoreML + Neural EngineГотовые релизные бинари macOS уже собраны с этой фичей; ~3× энкодер на коротких клипах, ~5,6× на длинных файлах против CPU
Linux x86_64 + NVIDIA--features cudaCUDA 12+Готового tarball нет — берите Docker-образ -cuda или Dockerfile.cuda; при отсутствии GPU в рантайме откатывается на CPU
Android / ARM64--features nnapiNNAPI (NPU/DSP)Не является взаимоисключающей с остальными
macOS ARM64, экспериментально--features aneНативный Apple Neural Engine (Core ML .mlpackage)Только голова rnnt, ускорение файлового режима; см. ниже
Apple Silicon, экспериментально--features candleЧистый Rust Candle на MetalТолько голова rnnt, FP32; см. ниже

Соберите подходящий вариант:

cargo build --release                      # CPU, любая платформа
cargo build --release --features coreml    # macOS ARM64
cargo build --release --features cuda      # Linux x86_64 + NVIDIA (CUDA 12+)
cargo build --release --features nnapi     # Android / ARM64

Исключительность проверяется на этапе компиляции: coreml и cuda взаимно исключают друг друга; ane конфликтует с coreml/cuda/nnapi/candle; candle конфликтует с coreml/cuda. На плохой комбинации срабатывает compile_error!.

Поведение в рантайме, о котором стоит знать:

  • Откат на CPU — намеренный, никогда не падение. CoreML-сборка при старте выполняет ~1-секундную warmup-проверку; при неудаче пишет в лог falling back to CPU execution provider и пересобирает сессии на CPU. CUDA-бинарь аналогично работает на CPU, когда GPU не виден (например, контейнер запущен без --gpus all).
  • Упаковка CUDA. Готового CUDA-tarball нет — релизная матрица собирает CPU-бинари для Linux (x86_64, aarch64), Windows и CoreML-сборку для macOS ARM64. Для GPU берите опубликованный образ ghcr.io/ekhodzitsky/gigastt:<ver>-cuda (рецепты: Развёртывание и эксплуатация) или собирайте через Dockerfile.cuda.
  • Нативный ANE-бэкенд (--features ane, macOS ARM64): выполняет энкодер rnnt на Neural Engine через пакеты Core ML с фиксированными формами по бакетам, ~10× тёплого end-to-end ускорения против CPU-сборки (упор в декодирование). Пакеты скачиваются командой gigastt download --ane~/.gigastt/models/ane/); модель e2e_rnnt прозрачно откатывается на ort-энкодер, а стриминговые окна всегда идут по CPU-пути — ANE является ускорителем файлового режима. Полный дизайн и честные цифры: docs/ane-backend.md.
  • Candle-бэкенд (--features candle, экспериментально): чисто-Rust инференс на Metal GPU, побайтовая идентичность с ort, только голова rnnt, FP32-веса, однократно конвертируемые скриптом scripts/convert_gigaam_candle.py. Подробности: docs/candle-backend.md.

Проверка:

gigastt transcribe sample.wav 2>&1 | grep 'transcribe complete'
# encoder=int8/coreml  → активна CoreML-сборка (int8/cuda, int8/cpu, ...)
# encoder=int8/ane     → энкодер обработал нативный ANE-бэкенд

На CoreML-сборке также смотрите стартовый лог: отсутствие строки falling back to CPU execution provider означает, что warmup-проверка прошла.

Файлы модели в замкнутом контуре

Всё, что нужно движку, живёт в одном каталоге — ~/.gigastt/models/ по умолчанию, переопределяется флагом --model-dir у serve / download / transcribe / quantize. Поэтому набор моделей — это обычный файловый артефакт, который можно подготовить и переносить:

# На машине с сетью:
gigastt download --prequantized --model-dir /srv/gigastt-models

# Любым способом скопируйте на целевой хост (rsync, USB, хранилище артефактов):
rsync -a /srv/gigastt-models/ offline-host:/srv/gigastt-models/

# На офлайн-хосте — запретить любые сетевые обращения, падать сразу, а не висеть:
gigastt --offline serve --model-dir /srv/gigastt-models

Свойства, которые делают это безопасным:

  • Целостность проверяет бинарь, а не вы. Каждая загрузка проверяется по SHA-256 против контрольных сумм, закреплённых в коде, складывается в .partial и атомарно переименовывается на место; повреждённый файл удаляется, а не принимается. Несовпадение контрольной суммы завершает процесс с кодом 65 (сеть 69, диск 74, Ctrl-C 130) — пригодно для скриптов.
  • Полный набор модели = ноль сети. При наличии полного комплекта головы (энкодер — INT8 или FP32 — плюс decoder/joiner/vocab для RNN-T-голов или INT8-энкодер + vocab для Multilingual) запуск не выполняет ни одного сетевого запроса, даже без --offline. --offline / GIGASTT_OFFLINE=1 превращает любую недостающую опциональную модель (пунктуация, VAD, диаризация) в немедленную ошибку с именем файла, который нужно предоставить.
  • Голова автоопределяется по файлам — скопированному каталогу не нужен флаг --model-variant на целевом хосте.
  • При копировании включайте *_int8.onnx-энкодер (или скопируйте также FP32- энкодер и выполните gigastt quantize --model-dir … на целевом хосте), vocab и — для rnnt/e2e_rnnt — decoder и joiner.

Для полностью упакованной офлайн-установки (tarball с бинарём + INT8-моделью + моделью пунктуации + systemd-юнит или вариант из двух deb-пакетов) берите релизный offline-бандл — рецепт и шаги проверки в Развёртывание и эксплуатация; состав — README-OFFLINE.md.

Проверка:

gigastt --offline transcribe sample.wav --model-dir /srv/gigastt-models
# транскрибирует без сети; недостающий файл — немедленная ошибка с именем файла

Память под пул сессий

Каждый слот пула десериализует свою копию энкодера, поэтому RSS растёт линейно с --pool-size (по умолчанию 2). Движок закладывает на слот примерно 2 × размер-файла-энкодера резидентной памяти (измерено ~1,9× на INT8- энкодере rnnt, CPU-провайдер, release-сборка):

Голова (как загружена)Файл энкодера≈ RAM на слот пулаДефолтный пул 2
rnnt / e2e_rnnt INT8~215 МБ~0,4 ГБ~790 МБ суммарного RSS
rnnt / e2e_rnnt FP32 (--skip-quantize)844 МБ~1,6 ГБ~3,3 ГБ — никогда в проде
ml_ctc INT8~225 МБ~0,45 ГБ~0,9 ГБ
ml_ctc_large INT8~592 МБ~1,2 ГБ~2,4 ГБ

Встроены две защиты:

  • Авто-ограничение по RAM. При загрузке запрошенный пул урезается так, чтобы энкодеры пула оставались в пределах половины общей RAM — при срабатывании пишется предупреждение Capping pool size N -> M. Ограничение никогда не повышает ваш запрос и никогда не опускается ниже 1.
  • Деградированный запуск. --pool-min-size 1 (по умолчанию) позволяет серверу стартовать на частично загруженном пуле вместо падения, когда память заканчивается посреди загрузки.

Эмпирическое правило: RAM ≥ pool_size × расход-на-слот + ~1 ГБ на ОС и пики запросов. На машине с 4 ГБ это означает --pool-size 1–2 с INT8-энкодером — тот же вывод, что и в пункте про OOM в Развёртывание и эксплуатация.

Проверка:

# нет предупреждения "Capping pool size" при старте, и:
curl -s http://127.0.0.1:9876/ready
# {"status":"ready","pool_available":2,"pool_total":2}

Проверка результата

Сквозной смоук после любого изменения из этой главы:

ls ~/.gigastt/models/                  # полный набор файлов головы, включая *_int8.onnx + vocab
gigastt transcribe sample.wav 2>&1 | grep 'transcribe complete'
# encoder=<int8|fp32>/<cpu|coreml|cuda|ane|candle>, rtf заметно ниже 1.0
curl -s http://127.0.0.1:9876/health   # "model"/"variant" соответствуют выбранной голове
curl -s http://127.0.0.1:9876/ready    # ready, pool_available >= 1

Затем сверьте ожидания по точности с docs/benchmarks.md, а не с кустарными замерами — харнесс, манифесты и нормализация значат больше, чем секундомер.

Частые ошибки

  • Английское аудио превращается в мусор с головой по умолчанию. Это не баг: у rnnt / e2e_rnnt кириллический словарь, и латиницу они выдавать не могут в принципе. Переключитесь на --model-variant ml_ctc (или ml_ctc_large).
  • --model-variant словно игнорируется после первой загрузки. Движок автоопределяет голову по каталогу модели; явный вариант, отличный от установленного, инициирует вторую загрузку рядом (с предупреждением variants are never mixed), а следующий запуск без флага снова предпочтёт rnnt. Удалите файлы неиспользуемой головы, чтобы каталог был однозначным, и верните место на диске.
  • Первый serve выглядит зависшим. Это одноразовая загрузка ~850 МБ FP32
    • ~2 мин квантизации; /health отвечает 200 с model:"loading", пока /ready остаётся 503 initializing. Уберите это окно командой gigastt download --prequantized и стробируйте клиентов по /ready, никогда по /health.
  • OOM после переключения на ml_ctc_large. Каждый слот теперь стоит ~1,2 ГБ. Снизьте --pool-size, держите --pool-min-size 1, чтобы тесный хост загружался деградированно, и следите за предупреждением Capping pool size при старте.
  • CoreML-сборка не быстрее CPU. Ищите в стартовом логе falling back to CPU execution provider — warmup-проверка не прошла, и движок (намеренно) работает на CPU. Поле encoder=int8/cpu в логе завершения это подтверждает.
  • CUDA-контейнер работает на скорости CPU. GPU не виден: контейнеру нужны NVIDIA Container Toolkit и --gpus all. Бинарь молча откатывается на CPU — проверяйте encoder=int8/cuda в логе завершения.
  • error: ane and coreml are mutually exclusive (или похожая) при сборке. Бэкенд-фичи конфликтуют по дизайну; собирайте ровно одну из coreml / cuda / ane / candle (nnapi — исключение).
  • SHA-256 mismatch во время gigastt download. Стейджинговая загрузка повреждена или подменена; она удаляется, а не принимается, CLI завершается с кодом 65. Просто перезапустите команду — не переименовывайте .partial вручную на место.

Ссылки

Приложение A — Коды ошибок и close-коды

Таблица симптом → код → что делать. Полные контракты полей — в docs/api.md и docs/troubleshooting.md; эта страница — оглавление для поваренной книги.

Сначала пробы

ПроверкаОжиданиеГлава
GET /health200, model"loading" или имя головы01
GET /ready200 до первого audio / job04, 02
version в /healthсовпадает с развёрнутым бинарём/образом06

Liveness/health, readiness/ready; не гадайте по «порт открыт».

REST / SSE / jobs

HTTPCodeТипичная причинаЧто делать
400empty_bodyПустое тело POSTОтправьте байты аудио
400invalid_formatПлохой ?format=json / txt / srt / vtt / md02
400unsupported_codecПлохой ?codec=pcmu / pcma / g72203
400invalid_sample_rateНет/неверный rate с raw codecsample_rate=8000 или 1600003
400conflicting_modeschannels=split и diarization=trueВыберите одно — 03
403loopback_onlyНе-loopback POST /v1/admin/reloadВызов с 127.0.0.106
404(без body) / jobs_disabledJobs API выключен--enable-jobs02
404job_not_foundНеизвестный или истёкший jobСохраняйте результаты на клиенте — 02
409job_not_finishedResult слишком раноЖдите done / поллите status
409job_not_cancellableCancel на terminal jobИгнорируйте / считайте done
409reload_in_progressПараллельные admin reloadПодождите и повторите — 06
409punctuation_not_availableForce punctuation=true без моделиУстановите punct / auto07
413payload_too_largeТело > --body-limit-bytesПоднимите лимит или jobs — 02
422invalid_audioБитый/неподдерживаемый контейнерТаблица форматов — 03
422transcription_errorDecode ok, inference failedЛоги; INT8 на месте — 07
429rate_limitedBucket по IP пустRetry-After; лимиты / --trust-proxy06
429queue_fullJob store полонЗабирайте results / --jobs-max02
503timeoutПул насыщенBackoff; --pool-size07
503pool_closedShutdownПереподключение после деплоя — 06
503initializingМодель ещё грузитсяПоллите /ready01
503reload_failed / reload_unsupportedHot-reload не удалсяПочините файлы модели — 06
504inference_timeoutПрогон > --inference-timeout-secsПоднимите timeout — 02

WebSocket error codes

CodeСессияСмыслЧто делать
timeoutне открыласьCheckout пулаЖдите retry_after_ms04
pool_closedendsDrainReconnect после апгрейда — 06
idle_timeoutends (1001)Нет фреймовШлите PCM (тишина ок) — 04
max_session_duration_exceededends (1008)--max-session-secsReconnect; final уже сброшен — 04
policy_violationends (1008)Спам пустыми фреймамиНе шлите empty binaries
inference_timeoutendsИнференс чанка слишком долгийКороче аудио / timeout
inference_errorcontinuesПлохой chunkПочините клиент; сессия жива
inference_paniccontinues, state resetPanic изолированНовая фраза; старые final валидны
configure_too_latecontinuesconfigure после audioConfigure первым — 04
invalid_sample_ratecontinuesRate не из supported_ratesRate из ready
unsupported_protocol_versionendsНесовпадение протокола1.004

WebSocket close codes

CodeКогдаДействие клиента
1001 Going AwaySIGTERM drain, idle, ping timeoutReconnect; на shutdown ждите final
1008 Policy Violationmax session / empty spamCaps / поведение клиента
1009 Message Too BigФрейм > --ws-frame-max-bytesМеньше PCM-фреймы
1006 Abnormal ClosureНет close frameПроверьте процесс; сервер этот код не шлёт

Ссылки

Приложение B — Чеклист offline / air-gapped

Когда хост не должен ходить в HuggingFace или GitHub в рантайме. Подробные install-рецепты — в Начало работы и Развёртывание; здесь — операторский чеклист.

На машине с сетью

  • Тег релиза: TAG=$(gh api repos/ekhodzitsky/gigastt/releases/latest -q .tag_name) (или пин v2.14.1)
  • Скачать offline bundle под arch
    (gigastt-${VER}-offline-x86_64-unknown-linux-gnu.tar.gz или aarch64)
    или deb-пару: gigastt_…_amd64.deb + gigastt-model-int8_…_all.deb
  • Скачать .sha256 (+ .minisig при проверке minisign)
  • Проверить checksums (sha256sum -c) и при желании docs/verifying-releases.md
  • Если нужна диаризация, отдельно заберите wespeaker_resnet34.onnx (не всегда в lean offline-бандле) и скопируйте в model dir — либо смиритесь с mono без speaker
  • Если нужен Silero VAD offline — так же заранее заберите VAD-модель (--vad иначе полезет в сеть)

На air-gapped хосте

  • Установить бинарь + модель (installer, dpkg -i, или распаковка)
  • Файлы в ~/.gigastt/models/ (или ваш --model-dir)
  • Offline-гард: GIGASTT_OFFLINE=1 / --offline — missing file падает сразу, а не висит на DNS
  • Smoke: gigastt transcribe sample.wav (без сети)
  • Сервер: gigastt servecurl http://127.0.0.1:9876/ready
  • systemd: unit из бандла; bind остаётся loopback, пока явно не --bind-all / GIGASTT_ALLOW_BIND_ANY=1

Что остаётся offline в рантайме

ДействиеСеть?
transcribe / batch / watch при моделях на дискеНет
serve после наличия моделейНет
POST /v1/admin/reload после замены файловНет
Первый download / нет punct / VAD / speakerДа (режет --offline)

После копирования новых файлов модели

# Рестарт не обязателен, если serve уже поднят:
curl -s -X POST http://127.0.0.1:9876/v1/admin/reload
# {"reloaded":true,"variant":"rnnt","encoder":"int8"}

См. Горячая перезагрузка.

Ссылки