Локальное десктопное приложение для работы с речью и текстом:
- распознавание речи (ASR) — аудио/видео в текст с таймкодами на уровне слов;
- LLM-постобработка — YouTube-описания, таймкоды, посты для Telegram, статьи, свободные вопросы к транскрипту (стриминг ответов в реальном времени);
- озвучка текста (TTS) — превращение
.txtв аудиокнигу (mp3/m4b) одним из пяти движков.
Всё работает локально — модели скачиваются в кеш при первом использовании. Облако нужно только для облачных TTS (Яндекс/Сбер) и LLM. Расшифровки и озвучки сохраняются между сеансами.
Распознавание речи (ASR) — backend/asr/
- GigaAM v3 CTC + KenLM (T-one) — высокое качество для русского языка (по умолчанию).
- faster-whisper — мультиязычная модель (en, ru, es, de, fr, …), грузится лениво при первом запуске.
- Таймкоды на уровне слов с точностью до ~40 мс.
- Диаризация — разделение записи по говорящим (необязательно, выключено по умолчанию). Работает с обоими движками: привязка идёт по таймкодам слов. Подписи попадают в UI, в TXT/SRT/VTT и в промпты LLM.
LLM-постобработка — backend/llm/
- 4 встроенных пресета (YouTube-описание, YouTube-таймкоды, Telegram-пост, Статья) + свободные вопросы к транскрипту.
- SSE-стриминг ответов по словам.
- Несколько профилей с fallback-цепочкой: порядок профилей = порядок попыток, порядок
models[]внутри профиля = приоритет моделей. - Провайдеры:
openai(любой OpenAI-совместимый endpoint — Azure, OpenRouter, Qwen, DeepSeek, Ollama, LM Studio) иanthropic. - Редактируемые промпты с разделением на
systemиuser_template.
Озвучка текста в аудиокнигу (TTS) — backend/tts/
Пять движков через общий интерфейс (initialize / get_status / list_speakers / synth):
- Silero (по умолчанию) — офлайн на CPU, модель
v4_ru, 5 голосов (aidar, baya, kseniya, xenia, eugene); понимает+-разметку ударений. - Piper — офлайн, выше качество, голоса
rhasspy/piper-voices(напр.ru_RU-ruslan-medium), регулятор темпа (length_scale). - edge-tts (рекомендуемый «из коробки») — нейронные голоса Microsoft (
ru-RU-DmitryNeural/ru-RU-SvetlanaNeural, 24 кГц), без ключа и регистрации. - Яндекс SpeechKit — облако, нужны
api_key+folder_id. - Сбер SaluteSpeech — облако, нужны
client_id/client_secret(обмениваются на access-token в рантайме).
Конвейер озвучки: загрузка .txt → разбиение на главы → нормализация чисел/дат/аббревиатур (rule-based через num2words или LLM с дисковым кэшем и защитой от «отсебятины») → расстановка ударений (ruaccent) → синтез → паузы между предложениями/абзацами/главами → сборка в mp3 / m4b (с главами) / m4a / wav. Прогресс стримится по SSE. Каждая озвучка сохраняется в историю.
Сбой облачного движка бросает
CloudTtsErrorи прерывает книгу — pipeline не подменяет тишину молча.edge-ttsходит на неофициальный endpoint и иногда транзиентно отдаёт «No audio» → ретрай с backoff.
Экспорт и хранение
- Расшифровки → TXT / SRT / VTT.
- Озвучки → mp3 / m4b / m4a / wav.
- Расшифровки и озвучки хранятся локально и доступны между сеансами (
backend/transcripts.py,backend/tts_runs.py).
Zapis/
├── main.py # Desktop entry point (pywebview)
├── backend/
│ ├── main.py # FastAPI: маршруты ASR / LLM / TTS / расшифровки / экспорт
│ ├── config.py # settings.json: чтение, валидация, кеш
│ ├── schema.py # Pydantic-модели (app / asr / llm / prompts / tts)
│ ├── formats.py # SRT / VTT / TXT
│ ├── transcripts.py # сохранённые расшифровки (persistence)
│ ├── tts_runs.py # история озвучек (persistence)
│ ├── asr/
│ │ ├── base.py # Transcriber Protocol
│ │ ├── factory.py # фабрика и переключение движков
│ │ ├── gigaam_engine.py # GigaAM v3 CTC + KenLM
│ │ ├── whisper_engine.py # faster-whisper (ленивая загрузка)
│ │ └── diarize.py # диаризация: кто говорит (sherpa-onnx)
│ ├── llm/
│ │ ├── client.py # AsyncOpenAI / AsyncAnthropic + fallback + SSE
│ │ └── prompts.py # дефолты пресетов + сборка messages
│ └── tts/
│ ├── factory.py # выбор движка по имени
│ ├── engine.py # Silero (по умолчанию, офлайн)
│ ├── engine_piper.py # Piper (офлайн, качество)
│ ├── engine_cloud_base.py # общий базис облачных движков
│ ├── engine_yandex.py # Яндекс SpeechKit
│ ├── engine_sber.py # Сбер SaluteSpeech
│ ├── engine_edge.py # Microsoft edge-tts (без ключа)
│ ├── pipeline.py # сборка аудиокниги (главы, паузы, экспорт)
│ ├── reader.py # .txt → текст с разметкой глав
│ ├── chapters.py # разбиение по главам
│ ├── chunker.py # нарезка на фрагменты для синтеза
│ ├── normalize.py # rule-based нормализация (num2words)
│ ├── normalize_cache.py # кэш LLM-нормализации на диске
│ ├── stress.py # ударения (ruaccent) перед синтезом
│ ├── assemble.py # склейка PCM
│ ├── export.py # mp3 / m4b / m4a / wav
│ ├── spool.py # буферизация выходных файлов
│ └── errors.py # CloudTtsError
├── frontend/
│ ├── index.html # двухколоночный UI с табами
│ └── static/
│ ├── style.css # тёмная/светлая тема через CSS-переменные
│ ├── app.js # основной поток UI
│ ├── stream.js # SSE через fetch + ReadableStream
│ ├── settings.js # модалка настроек
│ ├── tts.js / tts.css # вкладка «Озвучка»
├── settings.json # пользовательские настройки
├── requirements.txt
├── build.ps1 # сборка exe (Windows)
├── build.sh # сборка (Linux / macOS)
└── README.md
backend/asr/gigaam_engine.py — обёртка над GigaAM v3 CTC и алгоритм Longform-склейки. Логика синхронизирована с отдельным сервисом gigaam_tone (transcribe.py), чьё ASR-ядро является source-of-truth.
Процедура синхронизации при изменениях в gigaam_tone:
- Обновите
gigaam_tone/transcribe.py(новая модель / правка алгоритма). - Перенесите изменения в классы
GigaAMCTC,LongformCTC,CTCDecoderWithLM,merge_ctc_log_probs_by_blank_sep,chunk_audioи т.п. вbackend/asr/gigaam_engine.py. - Прогоните smoke-тест транскрипции (раздел «Верификация»).
cd C:\Projects\Zapis
pip install -r requirements.txtДополнительно нужен ffmpeg в PATH — он используется для декодирования произвольных аудио/видео в 16 кГц mono.
PyPI-версия пакета gigaam поддерживает только до v2. Для v3 пакет ставится прямо из репозитория Salute Developers — эта строка уже включена в requirements.txt (пин на коммит):
pip install --force-reinstall git+https://github.com/salute-developers/GigaAM.gitЕсли после pip install -r requirements.txt приложение пишет Model 'v3_ctc' not found — значит осталась старая PyPI-версия, выполните команду выше вручную с --force-reinstall.
Движки считают на разных стеках, поэтому и устройство у них задаётся отдельно — asr.gigaam.device и asr.whisper.device. Не заданное значение наследуется из общего asr.device.
| Движок | Считает на | GPU в готовом дистрибутиве |
|---|---|---|
| Whisper | CTranslate2 | возможен — нужен cuDNN 9 на машине |
| GigaAM | torch | нет: torch в сборке без CUDA, движок сам откатится на CPU |
| Диаризация | sherpa-onnx | нет: колесо с PyPI собрано без CUDA, всегда CPU |
Задаётся в Настройки → ASR → Вычислительное устройство (общее плюс отдельные значения для каждого движка) или прямо в settings.json — только NVIDIA, CTranslate2 умеет исключительно CUDA:
"asr": { "device": "cpu", "whisper": { "device": "cuda" } }Дополнительно нужны драйвер NVIDIA с поддержкой CUDA 12 и cuDNN 9. Сам cuDNN в exe не входит — колесо ctranslate2 везёт только диспетчер на 266 КБ, а движковые библиотеки (nvidia-cudnn-cu12 + nvidia-cublas-cu12) добавили бы к дистрибутиву больше гигабайта. Поставьте cuDNN 9 в систему либо положите его DLL рядом с Zapis.exe.
При "device": "auto" наличие видеокарты у Whisper определяется через ctranslate2.get_cuda_device_count(), а не через torch: torch в дистрибутиве собран без CUDA, и опрос через него всегда давал бы CPU.
Смена устройства из UI применяется сразу: движки пересоздаются, и следующая транскрибация уже пойдёт на новом устройстве (загруженная модель при этом теряется — но только когда устройство действительно поменялось, сохранение других настроек её не трогает). А вот правка settings.json руками, мимо приложения, по-прежнему требует перезапуска: файл перечитывается, но фабрика движков об этом не узнаёт.
Веса моделей не входят в дистрибутив и качаются при первом использовании с нескольких независимых хостов — доступность одного ничего не говорит о другом:
| Что | Откуда | Куда кладётся |
|---|---|---|
| GigaAM v3 | cdn.chatwm.opensmodel.sberdevices.ru |
~/.cache/gigaam |
| KenLM (T-one), Whisper, ruaccent, голоса Piper | huggingface.co + *.xethub.hf.co |
~/.cache/huggingface |
| Silero (TTS) | github.com (torch.hub) |
~/.cache/torch/hub |
| Модели диаризации (~34 МБ) | github.com (релизы sherpa-onnx) |
cache/diarization рядом с приложением |
huggingface.co: huggingface_hub 1.x отдаёт их через Xet (cas-server.xethub.hf.co, transfer.xethub.hf.co). Если файрвол пропускает huggingface.co, но режет xethub.hf.co, скачивание молча зависает — метаданные приходят, а файл остаётся нулевого размера. Обходится переменной окружения:
$env:HF_HUB_DISABLE_XET = "1" # качать напрямую по HTTPS с huggingface.coЕсли модель не скачивается, приложение назовёт в ошибке конкретный хост и причину (блокировка, таймаут, перехват TLS). Полный traceback — в zapis.log рядом с исполняемым файлом, он пишется всегда на уровне DEBUG.
На машинах с корпоративным прокси, подменяющим сертификаты, проверка идёт через системное хранилище Windows/macOS (пакет truststore) — отдельно прописывать корневой сертификат не нужно.
settings.json (поля можно править из UI: Настройки → ASR / LLM-профили / Промпты / Озвучка / Вид):
{
"app": { "title": "Записная книжка", "port": 8001, "theme": "dark" },
"asr": {
"engine": "gigaam",
"language": "ru",
"gigaam": { "version": "v3" },
"whisper": { "model": "small", "cpu_threads": 0 },
"diarization": {
"enabled": false,
"num_speakers": 0,
"threshold": 0.5,
"embedding_model": "wespeaker_en_voxceleb_resnet34_LM.onnx",
"num_threads": 0,
"window_shift_ratio": 0.3
}
},
"llm": {
"temperature": 0.3,
"max_tokens": 4096,
"profiles": [
{
"name": "openai",
"api_provider": "openai",
"base_url": "https://api.openai.com/v1",
"api_key": "sk-…",
"models": ["gpt-4o", "gpt-4o-mini"]
}
]
},
"prompts": {
"youtube_description": { "system": "", "user_template": "" },
"youtube_timecodes": { "system": "", "user_template": "" },
"telegram_post": { "system": "", "user_template": "" },
"article": { "system": "", "user_template": "" },
"custom_system": "",
"tts_normalize": { "system": "", "user_template": "" }
},
"tts": {
"engine": "silero",
"language": "ru",
"silero": { "speaker": "baya", "sample_rate": 48000 },
"piper": { "speaker": "ru_RU-ruslan-medium", "length_scale": 1.0 },
"edge": { "voice": "ru-RU-DmitryNeural" },
"yandex": { "api_key": "", "folder_id": "", "voice": "alena" },
"sber": { "client_id": "", "client_secret": "", "voice": "Nazar" },
"pauses": { "sentence": 300, "paragraph": 700, "chapter": 1500 },
"normalize": { "use_llm": false },
"accent": { "enabled": true, "model_size": "tiny" },
"export": { "format": "mp3", "split_chapters": true, "bitrate": 128000 }
}
}Пустые поля в prompts.* означают «использовать встроенный шаблон». Секреты облачных TTS и LLM хранятся plaintext — приложение локальное и однопользовательское.
asr.whisper.cpu_threads — число потоков CPU для faster-whisper. 0 (по умолчанию) означает «по числу ядер»; собственный дефолт CTranslate2 — 4 потока независимо от железа, из-за чего на многоядерных машинах транскрибация шла втрое дольше, чем могла бы.
Необязательная функция, по умолчанию выключена (asr.diarization.enabled). Включается галочкой «Определять говорящих» на панели транскрипции или в Настройки → ASR.
Стек. Ровно тот же, что у pyannote/speaker-diarization-3.1: сегментация pyannote-segmentation-3.0 + эмбеддинги WeSpeaker ResNet34-LM. Отличие в источнике: берутся ONNX-зеркала из релизов sherpa-onnx, а не оригиналы с HuggingFace. Оригиналы лежат за гейтом — нужен аккаунт, принятие условий и токен, то есть на машине пользователя они бы просто не скачались. Лицензии позволяют: модель MIT, sherpa-onnx — Apache-2.0.
Считает на onnxruntime внутри sherpa-onnx: torch не участвует, всегда CPU. Модели (~34 МБ) качаются один раз в cache/diarization рядом с приложением — кнопкой «Скачать модели» в настройках или автоматически при первом запуске.
Как склеивается с расшифровкой. Движки ASR про диаризацию ничего не знают: они, как и раньше, отдают слова с таймкодами. Разметка накладывается сверху (backend/formats.py: apply_speakers) — каждое слово получает говорящего по максимальному перекрытию с репликой, сегменты режутся не только по паузам, но и по смене говорящего. Слова, попавшие в паузу между репликами, наследуют ближайшего по времени соседа. Поэтому диаризация одинаково работает и с GigaAM, и с Whisper.
Настройки:
| Поле | Смысл |
|---|---|
num_speakers |
0 — определить самому по порогу. Если число участников известно, лучше указать явно. |
threshold |
Порог кластеризации при num_speakers: 0. Меньше — больше говорящих. |
embedding_model |
Имя файла модели эмбеддингов из релизов sherpa-onnx (список — в backend/asr/diarize.py). |
num_threads |
0 — по числу ядер (собственный дефолт sherpa-onnx — один поток). |
window_shift_ratio |
Шаг окна сегментации: главный регулятор «скорость против точности». |
Замеры (4 vCPU Xeon 2.3 ГГц, синтетический диалог из двух голосов, 29 с):
| Шаг окна | Время | Скорость | Результат |
|---|---|---|---|
| 0.1 (как в pyannote) | 20 с | 1.4× реального времени | 2 говорящих, без ошибок |
| 0.3 (по умолчанию) | 7 с | 4.3× | 2 говорящих, без ошибок |
| 0.5 | 5 с | 6.1× | кластеризация развалилась в одного говорящего |
Отсюда умолчание 0.3: втрое быстрее при той же разметке. Порог 0.5 тоже проверен развёрткой — при 0.6 и выше два голоса схлопываются в один.
Если диаризация не сложилась (нет сети, битая модель, пакет не установлен), расшифровка не теряется: текст возвращается без подписей, а причина показывается предупреждением над транскриптом. Терять минуты работы ASR из-за необязательной надстройки нельзя.
Пакет sherpa-onnx необязателен: без него функция просто не показывается в UI, всё остальное работает как раньше.
- Порядок профилей в массиве = порядок попыток. Если первый профиль возвращает ошибку до первого чанка ответа, движок переходит к следующему.
- Внутри профиля порядок
models[]— тоже приоритет (для одного URL пробуются разные модели по очереди).
python main.pyОткроется окно pywebview. Модель GigaAM подгружается лениво при первой транскрибации (при первом запуске может занять несколько минут — скачивается с HuggingFace). Whisper и TTS-модели (Silero, ruaccent, голоса Piper) тоже грузятся при первом использовании соответствующей функции.
Расшифровка:
- Выберите движок (GigaAM для русского, Whisper — для прочих языков).
- Перетащите файл в зону загрузки.
- Нажмите «Транскрибировать», дождитесь результата (вкладка «Транскрипт»).
- Экспортируйте в TXT/SRT/VTT.
ИИ-обработка: перейдите на вкладку «ИИ-обработка», нажмите пресет или задайте свой вопрос — ответ стримится по словам.
Озвучка:
- Перейдите на вкладку «Озвучка», загрузите
.txt(книгу/статью/расшифровку). - Выберите движок и голос (для пробы без настройки —
edge-tts). - При необходимости включите LLM-нормализацию чисел/аббревиатур и расстановку ударений.
- Нажмите «Озвучить» — прогресс стримится по главам; результат сохранится в
audiobooks/и в истории озвучек.
| Платформа | Команда | Результат |
|---|---|---|
| Windows | .\build.ps1 |
dist\Zapis.exe |
| Linux | bash build.sh |
dist/Zapis |
| macOS (Apple Silicon) | bash build.sh |
dist/Zapis.app |
Скрипт ставит зависимости в локальный venv, ставит pyctcdecode без конфликтующих deps и собирает PyInstaller-бинарь. Дефолтный settings.json кладётся рядом только при первом билде — пользовательский не затирается.
Модели (GigaAM, KenLM, Whisper, Silero, ruaccent, голоса Piper) НЕ пакуются в бинарь — они скачиваются в кеш при первом использовании. Это держит размер дистрибутива в разумных пределах.
KenLM имеет готовые wheels для Linux/macOS. На Windows это C++-расширение: компилируется через MSVC (CI ставит
microsoft/setup-msbuild) либо, без MSVC, движок откатывается на greedy-декодер.В собранном приложении автодоустановка kenlm не выполняется: pip в дистрибутиве нет, а
sys.executableтам — самZapis.exe, так что попытка установки запустила бы вторую копию приложения. Если модуль не импортировался (не собран или на машине нет Visual C++ Runtime), декодирование идёт без языковой модели, с записью вzapis.log.
.github/workflows/build.yml собирает все платформы при пуше в main и при пуше тега v*:
- Windows / Linux / macOS Apple Silicon — основные job'ы, определяют зелёный статус.
- macOS Intel (
macos-13) —continue-on-error: собирается «по возможности», не блокирует статус и релиз (Intel Mac снят с продаж в 2020, сборка медленная и нестабильная). - При пуше тега
v*артефакты автоматически аттачатся к GitHub Release (черезsoftprops/action-gh-release). macOS-бандл.appзапаковывается в zip (релиз принимает только файлы).
- GigaAM v3 — взять русскоязычное
.mp3(1–3 мин), запустить транскрипцию, убедиться что текст корректный, экспортировать SRT, открыть в плеере. - faster-whisper — переключить движок, выбрать язык
en, загрузить английский.mp4. Первый запуск качает модель (small≈ 500 MB). - LLM-стриминг — настроить рабочий профиль, нажать «YouTube таймкоды»: текст должен появляться по чанкам.
- Fallback LLM — поставить первым профиль с заведомо нерабочим ключом, вторым — рабочий. Запрос должен пройти со второго.
- Custom-чат — задать «Сделай 5 ключевых тезисов» — ответ стримится.
- TTS (edge-tts) — на вкладке «Озвучка» выбрать
edge-tts, загрузить.txt, «Озвучить» → проверитьmp3/m4bвaudiobooks/. - TTS-нормализация — включить LLM-нормализацию на тексте с числами/датами, перегенерировать — числа должны произноситься словами.
- Диаризация — взять запись с двумя-тремя собеседниками, включить «Определять говорящих» (первый запуск качает ~34 МБ), проверить, что подписи в транскрипте меняются по репликам и попадают в TXT/SRT.
Этот репозиторий использован в статье на Инфостарт