Последние пару месяцев у меня случилось много разных созвонов на английском. В целом, я обычно нормально все понимаю, но боюсь упустить что-то важное. Даже субтитры помогают лишь частично. То есть нужен костыль (или аугментация).

Поискал, что есть из того, что может пригодиться. Нашел статью на Хабре про расшифровку собеседований. Идея простая: записал аудио, прогнал через Whisper, скопировал текст в ChatGPT, получил анализ. В целом ок, но pipeline выглядел так:

  1. Запустить запись в OBS / аудасити / что-то ещё

  2. Дождаться конца звонка, сохранить файл

  3. Запустить скрипт с Whisper

  4. Открыть result.txt

  5. Скопировать в ChatGPT

  6. Прочитать ответ, куда-то сохранить

Шесть ручных шагов. И главное - в финальном тексте нет понимания, кто что сказал. Whisper отдаёт сплошной поток текста, в котором реплики всех собеседников перемешаны. Для собеса это ок (там обычно один собеседник), а для созвона уже на 3-4-х - бесполезно.

Мне захотелось автоматизировать (упростить для себя процесс): одна команда на старте, в результате размеченный транскрипт с именами спикеров и саммари в Obsidian.

Также думал, как результат усилий монетизировать :) Не придумал :) Так в итоге появился мой опенсорсный проектик tapeback

tapeback в Obsidian
скриншот итога из Obsidian

Что под капотом

tapeback - CLI-инструмент для Linux, который захватывает аудио на уровне ОС через PipeWire/PulseAudio, транскрибирует локально через faster-whisper, определяет спикеров и (опционально) генерирует саммари через LLM.
Весь результат - Markdown-файл в Obsidian-хранилище (обычная директория).

Никакого облака для транскрипции. Никакого Electron. Никакого Docker.

tapeback start          # начать запись
# ... созвон идёт ...
# Ctrl+C
# -> транскрипция -> диаризация -> саммари -> файл в Obsidian

Работает с любой платформой для созвонов: Google Meet, Zoom, Teams, Telegram, Discord.
Захват аудио происходит на уровне PulseAudio sink/source, а не через API конкретного сервиса.

Стерео-каналы как основное решение

Тут стоит объяснить подробнее, потому что это решение определило всю архитектуру.

Классический подход к определению спикеров - взять моно-запись и прогнать через модель диаризации (pyannote, например). Модель кластеризует фрагменты по голосовым эмбеддингам и присваивает каждому кластеру номер: Speaker 0, Speaker 1, Speaker 2…

Проблема: кластеризация голосов - это вероятностная задача. Модель может перепутать два похожих мужских голоса. Может разбить одного спикера на два, если он в начале звонка говорил тихо, а потом громче. И самое неприятное - вы никогда точно не знаете, какой кластер - это вы.

tapeback записывает стерео WAV: левый канал = микрофон (вы), правый = monitor source (все остальные). Это физическое разделение, а не статистическое. Ваш голос на левом канале - это факт, а не гипотеза.

┌─────────────┐     ┌──────────────┐     ┌────────────────────┐
│   Стерео    │     │   Whisper    │     │   Классификация    │
│  WAV-файл   │───▶│  (на каждый  │───▶│   по каналам:      │
│  L: mic     │     │   канал      │     │   L -> "You"       │
│  R: monitor │     │   отдельно)  │     │   R -> pyannote    │
└─────────────┘     └──────────────┘     │      для нумерации │
                                         │      среди "Others"│
                                         └────────────────────┘

Whisper работает на каждом канале независимо. Микрофонный канал = “You”. Если подключена диаризация, то сегменты классифицируются по RMS-энергии. pyannote применяется только к monitor-каналу, чтобы пронумеровать спикеров среди “Others” (Speaker 1, Speaker 2…). По возможности используется GPU, но при ошибках cuda или GPU-памяти переходит на CPU.

Ранняя версия мержила стерео в моно перед транскрипцией - и это была ошибка. Текущая архитектура сохраняет канальное разделение на всём протяжении pipeline.

Опциональные зависимости, или как не заставлять качать 2 ГБ PyTorch

pyannote-audio тащит за собой PyTorch, а это ~2 ГБ. Не каждому это нужно: если вы записываете один на один, вам хватит стерео-разделения без pyannote. LLM-саммари тоже нужны не всем - кто-то хочет только транскрипт.

А если все-таки нужны, то в энвах (про них чуть ниже) надо прописать соответствующие токены. А для диаризации еще и зарегиться на huggingface.co и поставить галочки принятия в 3-х местах (подробнее в README.md проекта), потому что лучшая (из тех что нашел) моделька гейтится)

Поэтому tapeback разбит на опциональные extras:

Вариант

Что добавляет

Размер

tapeback

Запись + транскрипция

~320 МБ

tapeback[llm]

+ саммари (Anthropic, OpenAI, Gemini и др.)

+50 МБ

tapeback[diarize]

+ диаризация спикеров (pyannote + PyTorch)

+2 ГБ

tapeback[tray]

+ системный трей

+1 МБ

Важно, что tapeback[diarize] не ломает работу без себя: если pyannote не установлен, monitor-канал просто помечается как “Other” без нумерации. Graceful degradation, а не crash.

В коде это ленивые импорты: pyannote грузится внутри '__init__' диаризатора, а не на уровне модуля. Не пришлось разбивать diarizer.py на два файла или городить абстрактные классы.

LLM-саммари с fallback-цепочкой

После транскрипции tapeback может отправить текст в LLM для извлечения краткого содержания, экшн-айтемов и ключевых решений. Поддерживается 7 провайдеров: Anthropic, OpenAI, Groq, Gemini, DeepSeek, OpenRouter, Qwen.

Интересный момент: все провайдеры, кроме Anthropic, используют OpenAI-совместимый API. Поэтому вместо абстрактной иерархии классов достаточно простого if/else и словаря с base URL:

# Вместо ProviderFactory -> AbstractProvider -> AnthropicProvider -> ...
_OPENAI_COMPATIBLE_BASE_URLS = {
    "groq": "https://api.groq.com/openai/v1",
    "gemini": "https://generativelanguage.googleapis.com/v1beta/openai/",
    "deepseek": "https://api.deepseek.com",
    # ...
}

Если основной провайдер отвечает ошибкой - tapeback автоматически пробует следующий доступный из цепочки. Тестировал в основном на {“gemini”: “gemini-2.5-flash”}

Транскрипт сохраняется всегда, даже если все LLM-провайдеры легли. Результат - структурированный JSON, который вставляется в начало Markdown-файла:

## Summary
Brief overview of the meeting.
### Action Items
- [ ] **You:** Send the report by Friday
- [ ] **Speaker 1:** Review the PR
### Key decisions
- Use PostgreSQL instead of MongoDB

История с переименованиями

Первая версия называлась meetrec = meeting recorder. Тогда еще не думал, что буду проект регить как Pypi/AUR пакет. А когда подумал - то обнаружил что такой проект уже существует. Имя занято и у него даже сайт есть.

Переименовал в echo-vault. Переименовал всё: модули, CLI, конфиги, README. Запушил. Пошёл регистрировать на Pypi.
echo-vault is too similar to an existing project echovault.

Pypi нормализует имена: дефисы, подчёркивания и точки считаются одним и тем же символом. echo-vault, echo_vault, echovault - для Pypi это один пакет.
И echovault уже занято :)
pip install echo-vault и pip install echovault скачают один и тот же пакет.

Третья попытка: tapeback. Ещё раз переименовал всё - модули, CLI, пути к конфигам, temp-директории, AUR-пакеты.

В общем всегда выбирайте/проверяйте имя на Pypi/AUR заранее. И про нормализованное не забывайте. И не только там - везде проверяйте.

AUR: 4 пакета вместо одного

Для Arch Linux tapeback опубликован в AUR как 4 отдельных пакета:

  • tapeback - базовый (запись + транскрипция)

  • tapeback-tray - мета-пакет, добавляющий удобство работы через системный трей

  • tapeback-llm - мета-пакет, добавляющий LLM-зависимости

  • tapeback-diarize - мета-пакет, добавляющий PyTorch + pyannote

Причина та же: не заставлять пользователя качать 2 ГБ PyTorch ради транскрипции.
В AUR нельзя нормально упаковать pip-зависимости как нативные пакеты (PyTorch-колёса ~2 ГБ, это не ложится в PKGBUILD), поэтому tapeback-diarize, tapeback-tray и tapeback-llm - мета-пакеты, которые доустанавливают pip-зависимости в venv через install-хуки.

yay -S tapeback                  # ~320 МБ, базовая функциональность
yay -S tapeback-tray             # + system tray icon
yay -S tapeback-diarize          # + ~2 ГБ PyTorch
yay -S tapeback-llm              # + LLM SDK

Релизный процесс довольно автоматизирован: scripts/release.sh бампает версию во всех файлах, CI публикует на PyPI через Trusted Publisher (OIDC, без токенов в секретах), scripts/aur-publish.sh обновляет 4 AUR-репозитория. Actions запинены на SHA коммитов - паранойя по поводу supply-chain атак через перезапись тегов.

Процесс разработки

От первого коммита до кое-как первой работающей локально версии прошло примерно 3 дня.
До версии на момент написания v0.8.9 прошло еще 2 недели - пока допиливал все баги и тестил пакеты.
Подход — spec-driven: сначала через веб-проект Антропика готовлю детальную спеку (ресерчи, обсуждения и тп.) на каждую фазу (capture, diarization, summarization).
Спеки — .md-файлы с секциями «разрешённые действия», «критерии готовности», и тп.
Почти весь код по этим спекам писал Claude Code.
Я - в основном как ревьювер и QA. Ну и в гит я еще не готов агентам разрешать писать :)
Ну еще немного играл роль злого техлида со строгим подходом к качеству кода и архитектурными прошлыми травмами.

Про монетизацию :)

Когда проект начал обретать форму, я задумался: а можно ли это продавать? Локальная транскрипция, диаризация, интеграция с Obsidian - звучит как SaaS за $10/мес.

Но чем дольше думал, тем яснее становилось, что это не тот продукт.
Целевая аудитория - линуксоиды, которые записывают созвоны из терминала. Это не массовый рынок.
Платить за CLI-тул, который требует ffmpeg, parecord и опционально 2 ГБ PyTorch? Вряд ли.
А упаковывать это в Electron с кнопками “Start” и “Stop” и продавать как дешёвую копию коммерческих транскриберов - значит убить то, что мне самому в нём нравится.

В итоге просто выложил в open source. Если кому-то пригодится - отлично. Если нет - у меня всё равно есть инструмент, который решает мою задачу.

Что дальше

tapeback пока что делает уже достаточно для меня: записал созвон - получил транскрипт с разделением по голосам и саммари в Obsidian.

Но есть вещи, которые может быть соберусь позже доделать:

  • Улучшение диаризации: pyannote иногда разбивает одного спикера на двух. Уже есть spectral similarity merging (сравнение спектральных профилей голосов), но пороги и алгоритмы надо тюнить

  • Профили спикеров: запоминаем имена спикеров из прошлых созвонов

  • Транскрибация в реальном времени: может даже выводить текст в отдельном окне

  • Поддержка macOS/Windows: заменить parecord на CoreAudio / WASAPI loopback захват. Pipeline после записи платформонезависимый.

  • Поддержка других языков кроме английского: пока не знаю, зачем мне это нужно :) Whisper и так работает со 100+ языками, но тестировал только на английском - за остальное не ручаюсь

Попробовать

# Минимальная установка — запись + транскрипция
uv tool install tapeback
# Со всем
uv tool install "tapeback[tray,diarize,llm]"
# Arch Linux
yay -S tapeback tapeback-diarize tapeback-llm tapeback-tray

Энвы с путем и токенами хранятся .env или в ~/.config/tapeback/.env

# Прописать путь, куда сохранять аудио и транскрибацию. Это не обязательно обсидиан, подойдет любая директория
# Если не прописать энв, то по дефолту будет ~/tapeback
mkdir -p ~/.config/tapeback
echo 'TAPEBACK_VAULT_PATH=~/Documents/obsidian/vault' > ~/.config/tapeback/.env
# Естественно, что перед созвоном всегда спрашивайте у собеседников,
# не против ли они записи
tapeback start
# ... созвон ...
# -> press Ctrl+C to stop and transcribe now
tapeback tray
# или можно запускать запись через иконку в систем трей

Исходники: github.com/yastcher/tapeback
Если проект показался полезным - поставьте звездочку на GitHub.
Если остались вопросы - пишите в комментарии или открывайте issue.