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 |
Главы
- Начало работы — установка (macOS/Linux/Windows/Docker/air-gap), первая транскрибация. Новичок · ~5–15 мин
- CLI и пакетная обработка — CLI, batch и watch. Новичок · ~15–30 мин
- Телефония и VoIP — G.711/G.722/Opus, АТС, stereo split и диаризация. Средний · ~20 мин
- Стриминг по WebSocket — partials, VAD-эндпоинтинг, session caps. Средний · ~30 мин
- Десктоп и встраивание — Swift/SPM, sidecar, Electron, UniFFI. Средний · ~30–60 мин
- Развёртывание и эксплуатация — прод, мониторинг, апгрейды, admin reload. Ops · ~45 мин
- Модели и бэкенды — головы, квантование, EP, пунктуация/ITN, hotwords. Средний · ~20 мин
Приложения
- A — Коды ошибок — jump table REST/WS/close
- B — Offline-чеклист — air-gapped операторский список
Английская версия — каноническая; эта книга зеркалирует её глава в главу.
Карта документации
Полный инвентарь документации репозитория: что в каждом файле и где он живёт.
Справочники (канонические — в книге не дублируются)
| Файл | Содержимое | Судьба |
|---|---|---|
| docs/api.md | Справочник HTTP / WebSocket / SSE API | остаётся |
| docs/asyncapi.yaml | AsyncAPI-схема WS-протокола | остаётся |
| docs/openapi.yaml | OpenAPI-схема 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.md | Reverse proxy, TLS, systemd, Docker | остаётся |
| docs/quickstarts.md | Квикстарты по встраиванию (FFI-биндинги) | остаётся |
| docs/runbook.md | Ранбук оператора для production | остаётся |
| docs/self-hosted-runner.md | Self-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.md | Go SDK для WebSocket-клиента | остаётся |
| sdks/js/README.md | TypeScript 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_ctc | ru/en/kk/ky/uz | «Голый» lowercase, без восстановления | Смешанная русско-английская (или kk/ky/uz) речь; лёгкий энкодер 220M |
ml_ctc_large | ru/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, который
автоматически скачивает отсутствующую модель) делает две разовые вещи:
- Скачивает ~850 МБ FP32 ONNX-файлов с HuggingFace (с проверкой SHA-256,
через промежуточный
.partialи атомарное переименование). - Квантизует энкодер в 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.
Ссылки
- docs/cli.md — канонический справочник CLI (все флаги и переменные окружения)
- docs/api.md — справочник REST / SSE / WebSocket API
- docs/benchmarks.md — цифры WER / RTF по каждой голове
- docs/troubleshooting.md — симптом → причина → исправление
- docs/deployment.md — детали Docker, reverse proxy, systemd, офлайн-установка
- docs/verifying-releases.md — контрольные суммы, minisign, SLSA provenance для артефактов релизов
- packaging/offline/README-OFFLINE.md — состав офлайн-бандла и опции установщика
- CLI и пакетная обработка — следующая глава: пакетная обработка, режим watch, форматы экспорта
- Модели и бэкенды — головы, квантизация, execution providers в деталях
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 проходит путь queued → processing → done | 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, а не как ошибку, достойную алерта.
Ссылки
- docs/cli.md —
полный справочник флагов
transcribe,transcribe-batch,watch - docs/api.md — эндпоинты jobs API, query-параметры, коды ошибок
- docs/benchmarks.md — измерения RTF, памяти и WER
- docs/troubleshooting.md — каталог ошибок декодирования и форматов
- Начало работы — установка и скачивание модели
- Развёртывание и эксплуатация — systemd-юниты и таймеры для постоянно работающих конвейеров
Телефония и 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 Hz | G.711 в WAV | отправлять как есть |
codec_name=adpcm_g722, тег [0x0064] или [0x028f] | G.722 в WAV | отправлять как есть |
codec_name=gsm_ms | wav49 (GSM 06.10 в WAV) | сначала конвертировать — см. Asterisk ниже |
codec_name=opus в контейнере Ogg | Opus (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_rate → 400 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 / pcma | sample_rate 8000–48000 |
сырой .g722 | нет (без заголовков) | да — g722 | sample_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 с мусором вместо ошибки. Если сырой поток транскрибируется в шум, повторите с другим именем кодека.
Ссылки
- CLI и пакетная обработка — пакетный и watch-режимы, форматы экспорта, длинные записи.
- Начало работы — установка и скачивание модели.
- Введение — карта документации.
- docs/api.md — Audio formats and telephony codecs — каноническая таблица форматов и все query-параметры.
- docs/cli.md —
справочник флагов
transcribe,transcribe-batchиwatch. - crates/gigastt-core/src/inference/audio.rs — внутреннее устройство декодеров (сниффинг G.722, путь Opus, детект dual-mono).
- Телефонные и Opus-фикстуры с их генераторами generate_telephony_fixtures.sh и generate_opus_fixtures.sh.
Стриминг по 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: подключаемся, согласуем параметры и стримим с микрофона
-
Подключитесь и сначала прочитайте
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. -
Отправьте
configureдо первого аудиофрейма. Выберите частоту, которую реально выдаёт ваш конвейер захвата, — она обязана быть изready.supported_rates. 16 кГц — оптимум, когда источник под вашим контролем (модель внутри работает на 16 кГц, ресемплинг не нужен); браузерный захват на 48 кГц тоже можно слать как есть. Неподдерживаемая частота не фатальна: вы получите ошибкуinvalid_sample_rate, а сессия продолжится на прежней частоте.configure, пришедший после первого аудиофрейма, отклоняется сconfigure_too_late, настройки сохраняются — поэтому шлите его сразу послеready:{"type": "configure", "sample_rate": 16000} -
Шлите бинарные фреймы 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). -
Собираем всё вместе — микрофон в живой текст. Конвейер гонит микрофон через 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.pymic_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
Единственно правильный паттерн конца потока:
- Отправьте
{"type": "stop"}. - Дождитесь
final— сервер декодирует всё, что ещё буферизовано с последнего partial (хвостовые слова не теряются), и выдаёт последнийfinal, возможно с пустымtext, если ничего не оставалось. - Только потом закрывайте сокет (или позвольте серверу закрыть его — он завершает сессию сразу после этого 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 — всё уже распознанное сохранено, поэтому переподключение безопасно.
Для записей длиннее часа есть два варианта, и их можно комбинировать:
-
Сторона оператора: поднять или отключить потолок —
gigastt serve --max-session-secs 0(каждый лимит — CLI-флаг; см. docs/cli.md). -
Сторона клиента: ротация по расписанию. Прочитайте потолок из
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-origin | docs/cli.md |
| Финалы приходят в голом нижнем регистре, без пунктуации | Пунктуационная модель не подключена или политика выключена; e2e_rnnt пунктуирует сам | Рецепт 2; troubleshooting |
configure не действует | Отправлен после первого аудиофрейма (configure_too_late) — шлите сразу после ready | Рецепт 1 |
| Транскрипта нет вообще | Три независимых домена отказа: готовность сервера, захват аудио, язык/голова | триаж troubleshooting |
Deepgram-совместимый режим WebSocket (drop-in эндпоинт для клиентов Deepgram) в работе; эта глава покрывает только нативный протокол.
Ссылки
- Справочники (канонические, здесь не дублируются): docs/api.md — протокол WebSocket, docs/asyncapi.yaml, docs/troubleshooting.md, docs/cli.md
- Клиентский код: examples/ (Python, Bun/TypeScript, Go, Kotlin, Rust), sdks/go, sdks/js
- В этой книге: Начало работы — установка и первый
запуск, Модели и бэкенды — головы и поведение
пунктуации/ITN, CLI и пакетная обработка —
альтернативы живому стримингу на REST/jobs,
Развёртывание и эксплуатация — запуск
serveв продакшене, Приложение A — Коды ошибок — jump table REST/WS/close.
Десктоп и встраивание: 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 на сессию пула) | Модель живёт в отдельном серверном процессе |
| Конкурентность | Один движок/пул на процесс приложения | Один сервер на несколько приложений/клиентов |
| Изоляция падений | Падение движка роняет приложение (и наоборот) | Падение сервера изолировано; приложение выживает и может его перезапустить |
| Обновления | Передеплой приложения с новым движком | Бинарь сервера обновляется независимо от клиентов |
| Версионирование | Приложение и движок — один артефакт | Приложение должно гейтоваться на версию найденного сервера (/health → version) |
Правила выбора:
- 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+.
-
Добавьте пакет. Xcode → File → Add Package Dependencies… → введите URL зеркала
https://github.com/ekhodzitsky/gigastt-swiftи добавьте продуктGigaSTTк вашему таргету. Зеркало — каноничный удалённый источник (SwiftPM требуетPackage.swiftв корне репозитория, поэтому поддиректорию монорепоpackaging/swiftнельзя подключить по URL — используйте её только как локальную path-зависимость для разработки). -
Забандлите модель. Скопируйте
~/.gigastt/modelsв таргет приложения как folder reference (синяя папка — сохраняет структуру директорий; жёлтая «group» плющит файлы, и движок их не найдёт). Альтернатива — скачать модель при первом запуске и закешировать. -
Загрузите движок и транскрибируйте:
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) -
Стриминг — чанки 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. -
Обрабатывайте ошибки. Обёртка бросает
GigasttError:engineLoadFailed(modelDir:)(директория модели отсутствует/нечитаема),streamCreationFailed(не удалось занять сессию пула),inferenceFailedиdecodingFailed(underlying:). C ABI сигналит об ошибкеNULL-возвратом, поэтому кейс говорит, где произошёл сбой, а не несёт сообщение движка.
Проверка: запустите приложение, транскрибируйте заведомо известный WAV и
убедитесь, что напечатан ожидаемый текст. Если движок бросает
engineLoadFailed при старте — директории модели нет там, куда указывает
Bundle.main.url(forResource: "models", withExtension: nil); см. «Частые
ошибки».
Рецепт — sidecar-сервер (macOS / Electron)
Запускаем gigastt serve как управляемый дочерний процесс. Полный жизненный
цикл: найти бинарь, предустановить модель, запустить, дождаться готовности,
транскрибировать, корректно остановить.
-
Найдите бинарь в порядке приоритета: env-override (например,
MYAPP_GIGASTT_BIN) → копия в ресурсах приложения →/opt/homebrew/bin/gigastt→/usr/local/bin/gigastt→PATH. Залогируйте, какой вариант выбран. -
Предустановите модель при установке или первом запуске, с машиночитаемым прогрессом для UI:
gigastt download --prequantized --progress jsonstdout несёт по одному NDJSON-событию на строку (
{"phase":"download","file":...,"bytes_done":N,"bytes_total":M}, затемverify, затемdone), а коды выхода различают сетевые/дисковые/контрольные ошибки — см. docs/cli.md.--prequantizedпропускает ~2-минутный проход INT8-квантизации на устройстве, поэтому первыйserveстартует за секунды, а не минуты. -
Выберите порт. Сегодня: фиксированный высокий порт (например,
49876). Автовыбор эфемерного порта (--port 0с машиночитаемой строкойLISTENING), а также--die-with-parentи--log-file— планируемые дополнения сервера: они требуют версии gigastt, где эти флаги есть, поэтому пока не полагайтесь на их синтаксис; используйте фиксированный порт и жизненный цикл ниже. -
Запустите и собирайте логи. Никогда не отправляйте вывод 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)); } } -
Гейт по
/ready, а не по TCP-connect. Сервер биндит порт сразу и отвечает на пробы из bootstrap-ответчика, пока модель грузится, поэтому «порт слушается» не значит «готов». ПоллитеGET /readyдо 200; тело 503 несёт{"status":"not_ready","reason":"initializing"|"pool_exhausted"|"shutting_down"}. Connection-refused означает, что процесс мёртв или ещё не запущен, — а не что он завис. -
Версионное рукопожатие по 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подпроцессом. -
Транскрибация. Для live-частичных результатов откройте WebSocket-сессию на
/v1/ws(паттерны — в главе Стриминг по WebSocket); для целых файлов — POST на/v1/transcribe(рецепты — в главе CLI и пакетная обработка). При насыщении пула сервер отвечает 503 +Retry-After(REST) или ошибкой сretry_after_ms(WS) — соблюдайте подсказку вместо выдумывания своего backoff’а. Перед закрытием WS-сессии отправьте{"type":"stop"}, чтобы сервер дописал хвост в финальный сегмент. -
Корректная остановка. Пошлите 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-аддон самодостаточен.
-
Установка:
npm install gigastt. postinstall скачивает ровно один готовый бинарь (gigastt.<platform>.node, ~47 МБ) под платформу установки из GitHub-релиза. Готовые платформы:darwin-arm64,linux-x64-gnu,linux-arm64-gnu,win32-x64-msvc(Intel macOS нет). -
Загрузите модель — она не забандлена:
gigastt download --prequantized→~/.gigastt/models, либо задайтеGIGASTT_MODEL_DIR. -
Использование:
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:" */ } -
Схема для Electron: создавайте
Engineв главном процессе (никогда в renderer’е) и держите по одномуStreamна аудиоканал (например, mic + system). КаждыйStreamдержит одну сессию пула всё своё время жизни, поэтомуpoolSizeдолжен покрывать число живых стримов — третийStreamна пуле из 2 броситPoolExhausted. Полный двухканальный паттерн с IPC-обработчиками — в examples/electron_main.mjs. -
Размер thread-пула. Инференс идёт на рабочем пуле libuv (по умолчанию 4 потока, общие с
fs/crypto). ДляNодновременно транскрибирующих каналов запускайте сUV_THREADPOOL_SIZE >= N(например,UV_THREADPOOL_SIZE=4 electron .) и соответствующимpoolSize. -
Упаковка (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-size1 — правильный дефолт для устройства. - Здоровье 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 от прошлого запуска) проявляется как молчаливый таймаут.
Проверяйте
/health→version, чтобы понять, ваш ли это сервер, и завершайте дочерний процесс при выходе приложения (флаг--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 принимает только относительные пути внутри рабочей директории — сначала направьте рабочую директорию на расположение файла (или скопируйте файл туда).
Ссылки
В этой книге:
- Начало работы — установка и скачивание модели
- Стриминг по WebSocket — паттерны WebSocket-протокола для sidecar’я
- CLI и пакетная обработка — рецепты REST / SSE / jobs
Справочные материалы (не дублируем — читайте здесь):
- packaging/swift/README.md — справочник API Swift-пакета
- crates/gigastt-node/README.md — API npm-пакета + trade-offs in-process vs sidecar
- crates/gigastt-uniffi/README.md — UniFFI-биндинги и генерация
- packaging/android/README.md — статус сборки Kotlin/AAR
- examples/electron_main.mjs — полный паттерн главного процесса Electron
- docs/quickstarts.md — квикстарты по биндингам и таблица доступности
- docs/embedding-packaging.md — статическая vs динамическая линковка onnxruntime
- docs/api.md — справочник
/health,/ready, REST/WS/SSE - docs/cli.md — флаги
serveиdownload(использованные выше ручки жизненного цикла) - Приложение A — Коды ошибок — HTTP/WS, когда sidecar сбоит
Заметка по дистрибуции macOS. Нотаризация .app / Developer ID — вне
этой книги: используйте tooling Apple для своего signed-приложения + sidecar
или embedded framework. Движок из GitHub Releases — unsigned OSS-бинарь;
версию гейть через /health → version, а не через 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", а/ready—503 {"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-burst→429+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 то же правило действует со стороны оркестратора:
terminationGracePeriodSeconds ≥ shutdown_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.
Ссылки
- Начало работы — установка и первая транскрибация
- CLI и пакетная обработка — рецепты REST / SSE / jobs
- Стриминг по WebSocket — паттерны WebSocket-протокола
- Приложение A — Коды ошибок — jump table HTTP/WS/close
- Приложение B — Offline-чеклист — air-gapped список
- docs/deployment.md — reverse proxy, TLS, манифесты Kubernetes
- docs/runbook.md — симптом → причина → аварийный выход
- docs/cli.md — полный справочник флагов
serve - docs/observability/alerts.yml — правила алертинга Prometheus
- docs/observability/dashboard.json — дашборд Grafana
- docs/verifying-releases.md — minisign, SBOM, SLSA-провенанс
- docs/privacy.md — какие данные куда движутся
- packaging/systemd/ — юнит + env-файл
- packaging/offline/README-OFFLINE.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_PUNCTUATION | on для rnnt, если есть RuPunct | on (докачает модель) | off |
--itn / GIGASTT_ITN | on для rnnt | on | off |
| 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 — нет. - Multilingual —
ml_ctc/ml_ctc_largeскачивают pre-quantized INT8-энкодер istupakov’а напрямую с HuggingFace;--prequantizedдля них — холостое уточнение (отдельного бандла нет). - Вручную —
gigastt quantize [--force]повторно запускает квантизацию для головы, определённой в--model-dir(например, после подмены FP32- энкодера своим файнтюном). - Отказ —
--skip-quantize(envGIGASTT_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 coreml | CoreML + Neural Engine | Готовые релизные бинари macOS уже собраны с этой фичей; ~3× энкодер на коротких клипах, ~5,6× на длинных файлах против CPU |
| Linux x86_64 + NVIDIA | --features cuda | CUDA 12+ | Готового tarball нет — берите Docker-образ -cuda или Dockerfile.cuda; при отсутствии GPU в рантайме откатывается на CPU |
| Android / ARM64 | --features nnapi | NNAPI (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-C130) — пригодно для скриптов. - Полный набор модели = ноль сети. При наличии полного комплекта головы
(энкодер — 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.
- ~2 мин квантизации;
- 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вручную на место.
Ссылки
- Начало работы — установка и первая транскрипция
- CLI и пакетная обработка — рецепт по пропускной способности и памяти для офлайн-CLI
- Развёртывание и эксплуатация — offline-бандл, systemd, пункты ранбука про OOM
- docs/benchmarks.md — канонические таблицы WER / RTF / футпринта
- docs/architecture.md — пайплайн, провайдеры, внутренности квантизации
- docs/cli.md — полный справочник флагов
- docs/ane-backend.md — нативный бэкенд Apple Neural Engine
- docs/candle-backend.md — бэкенд Candle/Metal
- docs/verifying-releases.md — проверка релизных артефактов
- packaging/offline/README-OFFLINE.md — состав offline-бандла
Приложение A — Коды ошибок и close-коды
Таблица симптом → код → что делать. Полные контракты полей — в docs/api.md и docs/troubleshooting.md; эта страница — оглавление для поваренной книги.
Сначала пробы
| Проверка | Ожидание | Глава |
|---|---|---|
GET /health | 200, model — "loading" или имя головы | 01 |
GET /ready | 200 до первого audio / job | 04, 02 |
version в /health | совпадает с развёрнутым бинарём/образом | 06 |
Liveness — /health, readiness — /ready; не гадайте по «порт открыт».
REST / SSE / jobs
| HTTP | Code | Типичная причина | Что делать |
|---|---|---|---|
| 400 | empty_body | Пустое тело POST | Отправьте байты аудио |
| 400 | invalid_format | Плохой ?format= | json / txt / srt / vtt / md — 02 |
| 400 | unsupported_codec | Плохой ?codec= | pcmu / pcma / g722 — 03 |
| 400 | invalid_sample_rate | Нет/неверный rate с raw codec | sample_rate=8000 или 16000 — 03 |
| 400 | conflicting_modes | channels=split и diarization=true | Выберите одно — 03 |
| 403 | loopback_only | Не-loopback POST /v1/admin/reload | Вызов с 127.0.0.1 — 06 |
| 404 | (без body) / jobs_disabled | Jobs API выключен | --enable-jobs — 02 |
| 404 | job_not_found | Неизвестный или истёкший job | Сохраняйте результаты на клиенте — 02 |
| 409 | job_not_finished | Result слишком рано | Ждите done / поллите status |
| 409 | job_not_cancellable | Cancel на terminal job | Игнорируйте / считайте done |
| 409 | reload_in_progress | Параллельные admin reload | Подождите и повторите — 06 |
| 409 | punctuation_not_available | Force punctuation=true без модели | Установите punct / auto — 07 |
| 413 | payload_too_large | Тело > --body-limit-bytes | Поднимите лимит или jobs — 02 |
| 422 | invalid_audio | Битый/неподдерживаемый контейнер | Таблица форматов — 03 |
| 422 | transcription_error | Decode ok, inference failed | Логи; INT8 на месте — 07 |
| 429 | rate_limited | Bucket по IP пуст | Retry-After; лимиты / --trust-proxy — 06 |
| 429 | queue_full | Job store полон | Забирайте results / --jobs-max — 02 |
| 503 | timeout | Пул насыщен | Backoff; --pool-size — 07 |
| 503 | pool_closed | Shutdown | Переподключение после деплоя — 06 |
| 503 | initializing | Модель ещё грузится | Поллите /ready — 01 |
| 503 | reload_failed / reload_unsupported | Hot-reload не удался | Почините файлы модели — 06 |
| 504 | inference_timeout | Прогон > --inference-timeout-secs | Поднимите timeout — 02 |
WebSocket error codes
| Code | Сессия | Смысл | Что делать |
|---|---|---|---|
timeout | не открылась | Checkout пула | Ждите retry_after_ms — 04 |
pool_closed | ends | Drain | Reconnect после апгрейда — 06 |
idle_timeout | ends (1001) | Нет фреймов | Шлите PCM (тишина ок) — 04 |
max_session_duration_exceeded | ends (1008) | --max-session-secs | Reconnect; final уже сброшен — 04 |
policy_violation | ends (1008) | Спам пустыми фреймами | Не шлите empty binaries |
inference_timeout | ends | Инференс чанка слишком долгий | Короче аудио / timeout |
inference_error | continues | Плохой chunk | Почините клиент; сессия жива |
inference_panic | continues, state reset | Panic изолирован | Новая фраза; старые final валидны |
configure_too_late | continues | configure после audio | Configure первым — 04 |
invalid_sample_rate | continues | Rate не из supported_rates | Rate из ready |
unsupported_protocol_version | ends | Несовпадение протокола | 1.0 — 04 |
WebSocket close codes
| Code | Когда | Действие клиента |
|---|---|---|
| 1001 Going Away | SIGTERM drain, idle, ping timeout | Reconnect; на shutdown ждите final |
| 1008 Policy Violation | max session / empty spam | Caps / поведение клиента |
| 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 serve→curl 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"}
См. Горячая перезагрузка.