Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

Или история о том, как я собрал AI-арбитра для совещаний, потерял три дня на аудиодрайверах и подружил Python с RTX 5080.

Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

Привет! Меня зовут Сергей, и последние пару месяцев я проектировал и писал систему, которая умеет слушать рабочие встречи, превращать их в текст и — самое главное — находить доказательства в спорах за секунды. Не «находить похожие слова», а реально понимать, что «расходы на серверы» и «бюджет на ЦОД» — это одно и то же.

Если вы хоть раз участвовали в совещании, где через неделю никто не помнит, о чём договорились — добро пожаловать под кат. Здесь будут схемы на Graphviz, куски Python-кода и пара историй о том, как я наступил на грабли с CUDA, аудиодрайверами и PyTorch.

О чём этот лонгрид

  • Почему я не взял готовое решение (и вам не советую, если работаете с русским языком)
  • Архитектура, которая не ломается при смене модели распознавания
  • Как подружить микрофон, системный звук, очередь и Whisper на GPU
  • Почему WASAPI — это боль, но Stereo Mix — ещё больнее
  • Что делать, если PyTorch говорит «Key already registered with the same priority: C10» и молча падает
  • RAG-поиск на 21 000+ фразах и почему RTX 5080 для эмбеддингов — пока бесполезна
  • Что получилось в итоге: цифры, метрики и планы

О чём этот лонгрид

  • Почему я не взял готовое решение (и вам не советую, если работаете с русским языком)
  • Архитектура, которая не ломается при смене модели распознавания
  • Как подружить микрофон, системный звук, очередь и Whisper на GPU
  • Почему WASAPI — это боль, но Stereo Mix — ещё больнее
  • Что делать, если PyTorch говорит «Key already registered with the same priority: C10» и молча падает
  • RAG-поиск на 21 000+ фразах и почему RTX 5080 для эмбеддингов — пока бесполезна
  • Что получилось в итоге: цифры, метрики и планы
class ASRAdapter(ABC): """Интерфейс для любого сервиса распознавания речи. Как PCIe-слот — вставляй что угодно.""" @abstractmethod async def transcribe(self, audio_chunk: bytes) -> ASRResult: ... # Реализация для faster-whisper (локальный GPU) class WhisperAdapter(ASRAdapter): async def transcribe(self, audio_chunk: bytes) -> ASRResult: segments, _ = self._model.transcribe(audio_array, language="ru") return ASRResult(text=" ".join(s.text for s in segments)) # Заглушка для тестов — вообще не требует GPU class MockASR(ASRAdapter): async def transcribe(self, audio_chunk: bytes) -> ASRResult: return ASRResult(text="Тестовая фраза")

То же самое с источниками звука:

class AudioSource(ABC): """Любой источник звука: микрофон, WASAPI, бот в Zoom.""" @abstractmethod async def start(self, audio_queue: asyncio.Queue, seconds_per_chunk: float = 2.0): ... @abstractmethod def stop(self): ...

AudioConsumer — сердце системы — работает только с этими абстракциями. Ему всё равно, откуда взялся звук и кто его распознаёт. Это как розетка: можно воткнуть утюг или дрель, главное — чтобы вилка подходила.

Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

Мораль: потратьте час на проектирование интерфейсов в начале — сэкономите дни на переделках потом.

Конвейер: как аудио превращается в текст (и не только)

Центральный конвейер — это классический Producer-Consumer с асинхронной очередью:

  1. Producer (микрофон или WASAPI) нарезает непрерывный аудиопоток на 2-секундные чанки и складывает в asyncio.Queue
  2. Буфер (очередь) разделяет быстрый источник (микрофон выдаёт данные равномерно) и медленный обработчик (GPU думает 0.3 секунды)
  3. Consumer забирает чанки, прогоняет через VAD-фильтр (отсев тишины), отправляет в Whisper и сохраняет результат
Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

Отдельного упоминания заслуживает VAD-фильтр (Voice Activity Detection). Без него Whisper на тишине начинает галлюцинировать: «Продолжение следует...», «Субтитры сделал DimaTorzok». Я отсекаю тишину по среднеквадратичной громкости (RMS < 0.01). Подобрал порог экспериментально: записал тишину, измерил RMS ≈ 0.005, речь даёт 0.1–0.5. Порог 0.01 — золотая середина. Это экономит ~95% ресурсов GPU.

Грабли №1: Stereo Mix vs WASAPI vs USB-наушники

Первая неожиданность: системный звук из браузера или Zoom не захватывается.

Что я сделал сначала: Stereo Mix — классический loopback в Windows. Работает? Да. Но он слушает аналоговый выход материнской платы. А у меня USB-наушники — Windows определяет их как отдельное аудиоустройство. Stereo Mix их просто не видит.

Попытка №2: VB-Cable — виртуальный аудиокабель. Настраивается, но требует, чтобы пользователь вручную перенаправлял звук. Неудобно для тиражирования.

Решение: Перешёл на WASAPI Loopback через библиотеку soundcard. Эта технология умеет захватывать звук конкретного устройства вывода напрямую, без аналоговых петель. В диспетчере устройств нашел свои USB-наушники (они определяются как «Динамики (3- USB Audio Device)») и направил WASAPI на них:

class WASAPILoopbackSource(AudioSource): async def start(self, audio_queue, seconds_per_chunk=2.0): # Ищем USB-наушники self._device = sc.default_speaker() # или по имени self._mic = sc.get_microphone(id=self._device.id, include_loopback=True) with self._mic.recorder(samplerate=16000, channels=1) as rec: while self._is_running: data = rec.record(numframes=frames_per_chunk) int16_data = (data.flatten() * 32767).astype(np.int16) chunk = AudioChunk(audio=int16_data.tobytes(), speaker_label="others") await audio_queue.put(chunk)

Теперь «Советник» захватывает звук из любого приложения: Chrome, Zoom, Discord — без единой настройки со стороны пользователя.

Грабли №2: PyTorch, CUDA 13 и загадочная ошибка C10

Самая болезненная проблема ждала меня при установке pyannote.audio для Voice ID (диаризации спикеров).

У меня стоит CUDA 13.2 (последняя на момент установки). faster-whisper через ctranslate2 ищет cublas64_12.dll. А у меня в папке CUDA\v13.1\bin\x64 лежит только cublas64_13.dll. Whisper падает с ошибкой «Library not found».

Решение: Копируем cublas64_13.dll → переименовываем в cublas64_12.dll. Грязный хак, но работает. Не делайте так в продакшене, но для локальной разработки — почему нет?

Дальше — больше. При установке pyannote.audio PyTorch молча обновился с 2.5.1 до 2.12.0. И всё. Процесс Python начал падать при импорте с загадочным:

Key already registered with the same priority: C10

Ни трейсбека, ни объяснения. Просто молча умирает. Я потратил несколько часов: переустанавливал PyTorch раз пять, чистил кеш pip, пересоздавал venv. Оказалось:

  1. PyTorch 2.12.0 (CPU) конфликтует с остатками CUDA-версии в кеше
  2. Windows блокирует DLL при удалении ([WinError 5] Отказано в доступе) — приходится убивать процессы через taskkill
  3. pyannote.audio 4.x требует PyTorch 2.8+, который не поддерживает RTX 5080 (архитектура Blackwell, sm_120)

Финальное решение: Пересоздал venv с Python 3.11 (системный был 3.8 — тоже сюрприз!), установил PyTorch 2.5.1+cu121 первым, затем всё остальное. Voice ID временно отложил до выхода стабильного PyTorch с поддержкой RTX 5080. Код для Voice ID готов, лежит в репозитории, ждёт своего часа.

Мораль: python --version — самая важная команда перед созданием venv. И pip freeze > requirements.txt — сразу после установки всего.

Хранение: почему одной базы данных категорически мало

Я использую три разных хранилища. Не потому что «модно», а потому что каждое решает свою задачу:

Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

Отдельная история про WAV-файлы. Первая версия сохраняла сырые PCM-байты. Ни один плеер не мог их открыть — он просто не знал частоту дискретизации и битность. Пришлось добавить метод _add_wav_header(), который формирует правильный 44-байтный RIFF-заголовок. Теперь файлы открываются в любом плеере.

Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

RAG-поиск: когда «найти по слову» — это провал

Обычный полнотекстовый поиск по ключевым словам не работает для разговорной речи. Человек может сказать «расходы на серверы» вместо «бюджет», и LIKE '%бюджет%' ничего не найдёт.

Я использую семантический поиск: каждая фраза превращается в 1024-мерный вектор (эмбеддинг) через модель intfloat/multilingual-e5-large. При поиске запрос тоже векторизуется, и Qdrant находит ближайшие векторы по косинусному сходству.

def search(self, query: str, limit: int = 5): query_vector = self.embed(query) # текст → 1024 числа results = self._client.query_points( collection_name=self._collection, query=query_vector, limit=limit ).points return [{"text": r.payload["text"], "score": r.score} for r in results]

Результат: запрос «кластер» находит «Начальное состояние кластера» с релевантностью 91%. Запрос «отказоустойчивость» — «Тест наш заключался в том, что у нас проверяется отказоустойчивость кластера» с релевантностью 89%.

Но есть нюанс. RTX 5080 не поддерживается текущей версией PyTorch для эмбеддингов (архитектура Blackwell, sm_120). Поэтому эмбеддинги считаются на CPU. Это медленнее, но работает. Ждём обновления PyTorch.

Что получилось в итоге

Система работает полностью локально на Windows 11 с RTX 5080 (16 ГБ VRAM), 128 ГБ RAM:

Как я перестал верить в «мы договаривались» и написал систему с Whisper, которая помнит всё

Что бы я сделал по-другому

  1. Сразу взял WASAPI, а не Stereo Mix. Потерял пару дней на отладку loopback, который принципиально не работает с USB-наушниками.
  2. Закрепил версию PyTorch в requirements.txt с первой минуты. pyannote.audio молча обновил torch, и я потратил часы на восстановление.
  3. Сделал веб-дашборд с первого дня. Первую неделю управлял системой через командную строку. Веб-интерфейс с кнопками «Старт/Стоп» и поиском сделал жизнь в разы удобнее.
  4. Не хранил бы модель Whisper в Git. 3 ГБ бинарных данных в репозитории — сомнительное удовольствие. Для личного GitLab сойдёт, но для командной работы лучше вынести в отдельное хранилище.

Что дальше

В планах — Voice ID (диаризация спикеров), «Арбитр» с LLM для автоматического разрешения споров и автоматический протокол встреч. Код для Voice ID уже написан и ждёт обновления PyTorch.

А пока система работает, записывает совещания и честно ищет доказательства. Если вы тоже устали от «мы это не обсуждали» — добро пожаловать в issues.

Теги (хабы): python, machine learning, ASR, Whisper, RAG, архитектура, разработка под Windows, open source

2