У меня от трёх до пяти созвонов в день, почти все в браузере: Meet, Яндекс Телемост, ktalk, реже всего Zoom. Детали встреч мне нужны в тексте, иначе договорённости расползаются. Писал я звонки через OBS с захватом экрана, и на самих звонках началось веселье: звук временами жёстко квакал, всё чуть-чуть подвисало. Причину до конца не выяснял, моё предположение - процессор зажирался захватом и перебивал сам разговор; ktalk и браузер тут ни при чём. А экран, как потом дошло, мне вообще был не нужен.

Дальше вторая серия. Расшифровку я гонял своими Python-скриптами: конвертация → транскрибация → диаризация (разметка, кто когда говорил). На встрече с пятью участниками диаризация расставляла голоса крайне плохо, половину фраз всё равно восстанавливал по памяти.

Пошёл искать вариант, чтобы запись не мешала звонку, результат был приличный и всё крутилось локально: не хотелось ни отдавать записи в облако, ни платить подписку за минуты. Нашёл платный Audio Hijack, он подкупил лёгкостью и простотой настройки. Потом узнал, что на M-чипе быстрее всего у меня отрабатывает mlx-whisper на MLX: веса large-v3-turbo с Hugging Face, инференс на GPU Mac. Это архитектура Whisper, но рантайм — пакет mlx-whisper и бинарь mlx_whisper, не pip install openai-whisper и не репозиторий openai/whisper. Так собрался стек: Hijack пишет один mp3, mlx_whisper делает VTT, pyannote раскладывает по SPEAKER_XX. На 56-минутном звонке транскрипция заняла около пяти минут. Платный только Hijack — разовая лицензия, без подписки (цену смотрите на сайте Rogue Amoeba). Скрипты выложил в mac-call-transcribe под MIT. Ниже сборка, замеры с двух реальных звонков и грабли; повторяется на любом Mac с M-чипом.

Запись: Audio Hijack вместо OBS

Возвращаться к OBS и копать, что именно там зажирало процессор, я не стал: квакающий звук на живом звонке дороже любой бесплатности. Облачные транскрибаторы вроде Otter снимают возню с расшифровкой, только запись уезжает на чужой сервер, а подписка капает каждый месяц. И есть третий пункт, о котором вспоминаешь позже всех: для диаризации нужен один файл, где все голоса вместе. Два раздельных трека «микрофон» и «система» pyannote не переварит.

Audio Hijack закрывает всё разом. Сессия собирается один раз, дальше просто копируется. Три блока: Google Chrome → Recorder, микрофон → тот же Recorder, и Google Chrome → Output Device, чтобы слышать собеседника в наушниках во время записи. Automatic Connectors выключить обязательно. Микрофон на Output не вешать, иначе будете слушать сами себя. У Rogue Amoeba принцип сформулирован прямо: если звук слышно на Mac, Audio Hijack может его записать.

Сессия Audio Hijack: Chrome на Output (слышимость), Chrome и микрофон в один Recorder MP3

Дальше рутина. Перед звонком жму Run, после Stop переношу mp3 в рабочую папку. Один файл, все голоса внутри. Работает с любым созвоном, который идёт в Chrome.

Бесплатный путь записи тоже существует: виртуальное аудиоустройство BlackHole плюс любая записывалка. Сам не пробовал: сводить микрофон и звук системы в один файл там нужно руками, а Hijack ровно эту возню и убирает.

Расшифровка: один вызов вместо цепочки руками

Первую версию я гонял в два шага: transcribe-mlx (внутри — CLI mlx_whisper) и diarize-vtt.py поверх VTT. Работало, но имена и чистку мусора приходилось собирать руками. Сейчас вход один: transcribe-meeting.py из репозитория. Внутри STT — тот же mlx_whisper через stt.py (subprocess), не import whisper из openai/whisper. Скрипт выкидывает типовые галлюцинации на тишине (строки про «Субтитры», зацикленное слово), склеивает куски, если Hijack разбил запись на несколько файлов, и пишет meeting.json.

~/tools/mlx-whisper-env/bin/python ~/bin/transcribe-meeting.py run meeting.mp3 \
  --out-dir ./transcripts --name 2026-07-20__meeting --diarize auto

(Скрипты из репозитория кладу в ~/bin/; путь к transcribe-meeting.py подставьте свой.)

JSON здесь главный файл, субтитры VTT и простой текст TXT собираются из него. VTT удобно читать глазами, но как протокол встречи он слабый: некуда положить список удалённого мусора и спорные стыки спикеров. В meeting.json лежат сегменты с таймкодами, поле removed с отфильтрованным и метка SPEAKER_UNKNOWN там, где pyannote не уверен. Имена пересобираются без повторного прогона mlx_whisper. На групповом разборе один кластер иногда цепляет двух людей, тогда правлю по времени в JSON, а не весь SPEAKER_06 махом. Старый двухшаговый путь (transcribe-mlx + diarize-vtt.py) в репозитории оставил для точечных правок уже готового VTT.

Двоим на звонке хватает --speakers 2. На групповом созвоне ставлю --diarize auto и не форсирую число: семь голосов, зажатые в два кластера, путаются ещё хуже.

Пара слов про формат. Модель speaker-diarization-3.1 обучена на телефонном звуке: один канал, 16 кГц. Стерео с разнесёнными каналами ломает её предположения о наложении голосов, поэтому один mp3 от Hijack, где все участники в одном канале, подходит идеально.

Минимальный STT без моего пайплайна — CLI из пакета mlx-whisper (именно так зовёт stt.py):

~/tools/mlx-whisper-env/bin/mlx_whisper meeting.mp3 \
  --model ~/models/mlx-whisper-large-v3-turbo \
  --language ru --output-format vtt \
  --output-dir ./out --output-name meeting

В карточке mlx-community есть и Python-API mlx_whisper.transcribe(...). В репозитории я сознательно гоняю CLI mlx_whisper: проще кэшировать части multipart и не тащить openai-whisper.

Скорость на M4 по двум звонкам той же недели:

Звонок

Длина аудио

Время транскрипции

SEO-созвон

~14 мин

1–2 мин

Урок по Cursor

~56 мин

~5 мин

Короткий SEO-созвон я разобрал сразу после Stop: договорённости, задачи на неделю. Урок по Cursor шёл почти час, там контекст плотнее и много пауз ученика. После прогона открываю начало VTT, смотрю, кто как представился, и подставляю имена руками. pyannote имена не угадывает, отдаёт только SPEAKER_00, SPEAKER_01 и дальше по списку.

Ставится на Mac M1–M4: pip install mlx-whisper (даёт бинарь mlx_whisper), pip install pyannote.audio, venv на ARM64 Python. Веса large-v3-turbo (1,5 ГБ) я держу в `/models/mlx-whisper-large-v3-turbo/`; суммарно под модели — 5–8 ГБ на диске.

Грабли первого вечера

Больше всего крови выпил Python. У меня стоял Homebrew ещё с Intel-времён, в /usr/local, и интерпретатор оттуда x86. MLX на нём не заводится, Rosetta нормального пути к Metal не даёт. Проверка простая: python3 -c "import platform; print(platform.machine())" должен вернуть arm64. Видите x86_64 - переставляйте Python на ARM64-native: Homebrew для M-чипов живёт в /opt/homebrew, оттуда brew install python. Мне это закрыло половину «почему ничего не работает».

Потом завис Hugging Face. Загрузка mlx-community/whisper-large-v3-turbo из скрипта стояла без прогресса. Подождал, плюнул, скачал веса прямым curl в ~/models, указал путь в обёртке. Заработало с первого раза. Если progress bar молчит дольше пары минут, час ждать смысла нет.

С pyannote отдельная история. Модели закрытые (gated): по README нужно принять условия на двух страницах, segmentation-3.0 и сама speaker-diarization-3.1, плюс токен доступа через hf auth login. Пропустили segmentation или не залогинились - на диаризации ждёт ошибка 403, доступ запрещён. Я в тот вечер накликал три Accept, потому что попробовал ещё speaker-diarization-community-1 для offline-режима. Для базового 3.1 третья страница не нужна.

А voxscriber так и не завёлся. Хотел готовую сборку из коробки, но связка torchcodec с Intel ffmpeg развалилась, и я написал свой консольный скрипт поверх VTT. Если хватает mlx_whisper + pyannote без красивого интерфейса, туда можно не ходить.

Что осталось кривым

На стыках коротких реплик pyannote путает SPEAKER_00 и SPEAKER_01. На групповых встречах один кластер иногда смешивает двух людей: проверяйте первые минуты и строки вида «Саша: Саш, привет». Это ограничение модели, настройками оно не лечится. Точные имена без ручной правки я никому не обещаю.

Whisper-модель на тишине выдумывает текст. Встроенная чистка в transcribe-meeting.py срезает типовой мусор; полный список удалённого — в meeting.jsonremoved (в двухшаговом пути ещё *.qc.json у diarize-vtt.py). Спорные стыки помечаются SPEAKER_UNKNOWN, а не приписываются случайному голосу.

Ещё есть флаг --extract-commitments, он вытаскивает черновик цифр и договорённостей. Это эвристика, ручной разбор она не заменяет. Итоговый текст я читаю как протокол: что решили, кто взял задачу, какие цифры прозвучали. По таймкодам удобно вернуться к спорному месту записи.

Чеклист перед первым прогоном

Если хотите попробовать мой вариант, проверьте у себя:

  1. platform.machine() возвращает arm64; Python из ARM64 Homebrew, не из /usr/local.

  2. Веса mlx-community/whisper-large-v3-turbo на диске (1,5 ГБ в `/models/…); если загрузка с Hugging Face зависает — curl` по ссылкам из карточки модели.

  3. Оба Accept на Hugging Face + hf auth login; токен с доступом к закрытым моделям.

  4. Audio Hijack: Chrome и микрофон в один Recorder; Automatic Connectors = Off; Chrome → Output, чтобы слышать собеседника.

  5. Один mp3 на звонок; для группы --diarize auto, не --speakers 2.

  6. После прогона: первые реплики в VTT → имена руками; пробежать removed в meeting.json.

Остался вопрос, который я сам себе задаю: нужна ли диаризация вообще, если тот же текст можно скормить языковой модели и попросить разметить спикеров по контексту? Пока оставляю pyannote: таймкоды и SPEAKER_XX дают опору до всякой нейросети, и правится это по времени, без повторного прогона часа аудио. Для диалога двоих иногда хватает Whisper и ручной разметки по первым минутам. Если соберёте свой вариант, киньте ссылку на репо в комменты.

Источники

Комментарии (9)


  1. Nelenara
    23.07.2026 05:45

    Самое забавное, что проблема обычно всплывает не на коротких звонках, а когда встреча идет час-полтора)


    1. andreyilin Автор
      23.07.2026 05:45

      Верно. На двоих вообще не проблема. А вот когда 4–6 и больше — уже да.

      Вчера доработал скрипт под большее число спикеров. На встрече были два Саши, девочка и мальчик. Всё получилось, но лишние 30 минут на это убил.


  1. Anyothernick
    23.07.2026 05:45

    To record audio only in OBS Studio, navigate to Settings > Output, set Mode to Advanced, and go to the Recording tab. Select Custom Output (FFmpeg) from the dropdown. Under Container Format, choose mp3 or wav, and set the Video Encoder to Disable to skip video processing completely

    А так- поздравляю с открытием mlx-whisper