Skip to content
Back to skills

Local Transcription

ASecurity

Локально транскрибировать аудио/видео запись в markdown - offline ASR с диаризацией. Брать для "транскрибируй запись" и когда надо поднять сам движок на машине. Облако - `mymeet-snapshot`.

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 29, 2026
businesspythonswiftshellbashgit

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add dewil/claude-toolkit --skill local-transcription --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Local Transcription?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Local Transcription
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/dewil-local-transcription/badge)](https://www.skillsdirectory.com/skills/dewil-local-transcription)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: local-transcription
description: Локально транскрибировать аудио/видео запись в markdown - offline ASR с диаризацией. Брать для "транскрибируй запись" и когда надо поднять сам движок на машине. Облако - `mymeet-snapshot`.
---

# local-transcription

Локальная транскрипция записи встречи в markdown - offline и приватно, как дополнение к облачным транскрипт-сервисам (mymeet, KTalk и т.п.). Оборачивается в один CLI, который берет любой аудио/видео и выдает `.md` со спикерами и таймкодами.

## Когда НЕ применять

- **Нужны имена участников** - локальный движок дает обезличенных "Спикер N": имена берутся из метаданных звонка, а их знает только облачный сервис (mymeet, KTalk). Есть раздельные дорожки - имена появляются (см. "Сначала проверь дорожки"); нет - проставляются вручную.
- **Запись публичная и облако устраивает** - там же и summary, и имена, и скорость; локальная транскрипция стоит времени машины.
- **Нужен перевод или неподдержанный язык** - скилл про распознавание речи, а не про перевод; движок специализирован под язык записи.
- **Нет самого файла** - живая встреча не транскрибируется задним числом; сначала запись.

## Пайплайн и выбор движка

Схема: **специализированный локальный ASR** (с пословными таймкодами) + **token-free диаризатор** + склейка "слово -> спикер по перекрытию таймкодов" -> markdown.

- **ASR: бери ASR, специализированный под язык записи, а не универсальный Whisper.** Проверено на русском: GigaAM v3 (SberDevices, MIT) заметно точнее локального Whisper (large-v3-turbo гарблит слова, теряет пунктуацию и регистр) и держит паритет с облачным сервисом по тексту. Для другого языка - аналогичный language-specialized открытый ASR. Whisper - дефолт только если специализированного под язык нет.
- **Диаризация: sherpa-onnx** (pyannote-segmentation ONNX + speaker-embedding ONNX). Модели свободно качаются из релизов sherpa-onnx, БЕЗ gated HF-токена (в отличие от чистого pyannote.audio). Число участников, если известно, задавай явно - точнее.
- **Apple SpeechAnalyzer (macOS 26+):** премиальный `SpeechTranscriber` поддерживает узкий список локалей и многие неанглийские языки НЕ покрывает (напр., русского нет); их тянет только `DictationTranscriber` (движок диктовки - без пунктуации, слабее). Перед ставкой на Apple для неанглийского - проверь `supportedLocales` ОБОИХ транскрайберов через мини Swift-CLI (Speech.framework есть в CLT-SDK, полный Xcode не нужен).

## Грабли ASR-библиотек (проверено на GigaAM)

- **Штатный longform может тянуть gated-модель.** У GigaAM `model.transcribe_longform()` подтягивает `pyannote.audio` с моделью под HF-токеном - то есть упирается в ту же gated-стену, от которой уводит выбор sherpa-onnx для диаризации. Вывод: **резать на куски самому**, а не штатным longform. Достаточно фиксированной нарезки под потолок ASR (так сделано в эталонной реализации) - gated-стена обходится уже этим. Плата: граница куска может пройти по середине слова, и оно теряется с обеих сторон. Улучшение, если это мешает, - нарезка по речи своим VAD (silero из релизов sherpa-onnx, токенов не требует): режет по паузам и не гоняет тишину через ASR. В эталонной реализации VAD не сделан.
- **У ASR бывает жесткий потолок длины куска.** GigaAM отвечает `ValueError: Too long wav file` на аудио длиннее 25 секунд - резать надо с запасом (24 с). Без этого пайплайн падает на первом же длинном сегменте речи.
- **Скрытые зависимости.** `torchaudio` не значится в зависимостях gigaam, но импортируется в `preprocess.py` - без него пакет не грузится вообще. Ставится отдельно, с индекса CPU-колес.

## Потолок диаризации (важное и неочевидное)

Из ОДНОЙ смешанной дорожки звонка аудио-only диаризация надежно разделяет **~2-3 голоса**. 4+ похожих голосов (особенно однополых на сжатом миксе) сливаются - и это НЕ лечится числом кластеров / порогом / более сильным эмбеддингом: упирается в различимость эмбеддингов, а не в настройку. Облачные сервисы дают чистые N спикеров с именами, потому что берут разметку из метаданных платформы звонка (active-speaker events), а не из аудио. Практический вывод: приблизительная обезличенная разметка - свойство **смешанной дорожки**, а не локальной транскрипции как таковой. За точное именованное N-way разделение отвечают раздельные дорожки участников (см. следующий раздел), не тюнинг диаризатора; нет дорожек - спикеры выходят обезличенные и приблизительные, имена проставляются вручную.

## Сначала проверь дорожки

До запуска диаризации выясни, не разложены ли голоса по дорожкам: пофайловый экспорт по участникам (умеют многие рекордеры и телефония - раздельные стороны звонка) или каналы внутри одного файла. Число каналов не угадывай - проверь: `ffprobe -v error -show_entries stream=channels -of csv=p=0 <файл>`; телефонная запись часто оказывается двухканальной со сторонами по каналам. Если дорожки есть - диаризацию не запускай вовсе: спикер известен по построению, разметка точная и именованная, и ASR чище - на изолированной дорожке голос не перекрыт чужим.

```bash
transcribe-meeting ivan.wav petr.wav --track-names "Иван,Петр"          # файл = участник
transcribe-meeting call.wav --split-channels --track-names "Иван,Петр"  # канал = сторона звонка
```

`--speakers` при этом не передавай - он про диаризацию смешанной дорожки. Дорожки из общего микрофона не изолированы: чужой голос протекает в соседнюю тише и с искаженным текстом; такие двойники движок отсекает сам (отключается `--no-bleed-filter`).

## Три фронта над одним CLI

Ядро - один CLI (напр. `transcribe-meeting <файл...> [--speakers N] [--no-diar] [--track-names "А,Б"] [--split-channels] [-o out.md] [--title]`), а поверх:

1. **Терминал** - прямой вызов.
2. **Скилл в проекте** - агент находит локальный файл, гонит CLI, кладет md по конвенциям встреч рядом с облачными транскриптами, цепляет summary и сверку имен.
3. **macOS Быстрое действие** (Finder, правый клик). Грабли:
   - Quick Action стартует с **урезанным PATH** - обертка ОБЯЗАНА `export PATH` с brew-путем, иначе ffmpeg не найден.
   - Shell-скрипт в трее Automator всегда показывает **"0%"** (он не читает прогресс) - гони прогресс через уведомления по фазам, длинный шаг (диаризацию) подписывай явно ("долгий шаг"), иначе выглядит как зависание.
   - Руками положенный `.workflow` регистрируется в подменю **"Службы"**, а НЕ в секции "Быстрые действия" - функционально то же, но чтобы поднять именно в "Быстрые действия", пересохрани бандл в Automator (Cmd+S).

## Один движок на машину

Движок (venv, веса ASR, ONNX-модели диаризации) - **машинный ресурс, не проектный**: ставится один раз глобально и в проектах не дублируется. CLI кладется в `~/.local/bin` с shebang на venv - тогда он зовется из любой папки и не тянет за собой знание о том, где venv.

- Краткая справка "что уже стоит и как звать" - в **пользовательский** `~/.claude/CLAUDE.md` (он читается во всех проектах устройства). Машинный runbook с точными путями, установкой и бенчмарками - в проекте, где движок поднимали.
- Нужна проектная специализация (свои конвенции путей, шапка документа) - делать тонкую обертку поверх общего CLI, а не второй движок.

Повод для правила: параллельная сессия из другого каталога развернула на той же машине **второй** экземпляр - свой venv на 4,9 ГБ, свой клон репозитория ASR, свои копии моделей. Справку в пользовательском файле она либо не увидела, либо стартовала раньше. Поэтому первым шагом любой работы с движком - проверить, не стоит ли он уже (`which <cli>`), а не начинать с установки.

## Очередь прогонов и защита машины

Прогоны на машине идут **по очереди**, их разводит файловый лок; уникальные имена файлов для этого недостаточны. `mkdtemp` (см. ниже) снимает коллизии имен, но не конкуренцию за ресурсы: один процесс держит ~1,5 ГБ RSS, а с `--threads 0` или `--fast` ставит `num_threads` по числу ядер, поэтому два таких прогона на 4-ядерной машине дерутся за CPU и рискуют упереться в память.

Четыре предохранителя, каждый закрывает свой сценарий - по отдельности ни один не достаточен:

1. **Лок с человеческим содержимым.** Эксклюзивный `flock` на общий файл берется **до тяжелых импортов** (`import torch` и прочего). Иначе смысл теряется: ждущий процесс уже загрузил модель в память. Взял лок первым - грузишь модель; ждешь - висишь на голом интерпретаторе (~20 МБ), и очередь любой длины стоит по памяти как один прогон. В сам файл лока пиши, **кто занял**: проект, файл, время старта, pid - ждущий печатает это словами ("занято проектом X, файл Y, идет 6 мин, pid 1746675"), и непонятное ожидание превращается в понятное, а заодно видно, что снимать, если прогон завис. Ожидание ограничивай таймаутом (`--wait-timeout`, разумный дефолт - часы): иначе один зависший держатель подвешивает всех навсегда. Флаг `--no-wait` - выйти сразу вместо ожидания (для расписаний и фоновых вызовов).
2. **Проверка свободной памяти до загрузки модели.** Лок может быть свободен, а память уже съедена браузером и агентами. Тогда ждать, а не лезть (`--min-free-mb`).
3. **Лимит памяти на прогон (cgroup).** Главное против "положил машину": без лимита превышение обрабатывает **глобальный** OOM-killer и выбирает жертву по всей системе - падает база, докер, что угодно. Под своим scope (`systemd-run --user --scope -p MemoryMax=... -p MemorySwapMax=0`) OOM локальный: умирает только транскрибация. Своп режем намеренно - уход инференса в своп по последствиям не лучше нехватки памяти. Симптом упора в лимит: процесс молча "Killed" - лечится повышением лимита. Должно отключаться флагом; нет systemd-user (macOS) - предупредить и работать без лимита.
4. **Фоновый режим по умолчанию: прогон уступает машину.** Лок разводит прогоны между собой, но не защищает остальных жильцов машины (сессии, браузеры, боты, cron): три длинных записи подряд со всеми ядрами подняли нагрузку до 32 на 6-ядерном сервере. Поэтому по умолчанию потоков половина доступных ядер, не больше 4 (`max(1, min(4, cpu // 2))`), и процесс идет с `nice 19`: при простое прогон берет свое, при конкуренции уступает. Это и есть защита. Scope дополнительно получает `CPUWeight=20` и `IOWeight=20`, но они действуют, только если пользовательскому systemd делегированы контроллеры `cpu` и `io` - на части машин их нет (проверено 25.09.2026 на сервере: `io` не делегирован вовсе), и полагаться на них нельзя. Строка "Режим:" при старте печатает фактический `nice`, прочитанный у процесса; не удалось понизить приоритет - строка говорит об этом прямо. Цена - время: на двух потоках RTF около 0,7 против 0,35 на всех ядрах (замер проекта), то есть примерно вдвое дольше. Режим печатается строкой при старте, иначе медленный прогон выглядит зависшим. `--fast` - для свободной машины: все ядра, обычный приоритет; лимит памяти он не снимает (это `--no-limit`). `--threads 0` - все ядра при фоновом приоритете, явный `--threads N` сильнее `--fast`.

Отдельная причина, почему очередь, а не просто "переживем конкуренцию": докачка весов у некоторых ASR-библиотек **неатомарна** - файл пишется прямо в целевой путь, и готовым считается любой существующий. Два одновременных первых запуска на новую модель оставляют битый кэш, который потом молча грузится.

**Лок помогает только тем, кто ходит через общий CLI.** Второй движок, поставленный мимо (свой venv, свой скрипт), про лок не знает - поэтому "один движок на машину" выше не гигиена, а условие работоспособности очереди.

## Устойчивость CLI

- Все промежуточные файлы - в СВОЕМ temp-каталоге на прогон (`mkdtemp`), не по фиксированным именам: иначе **параллельные запуски конфликтуют** на одинаковых temp-именах.
- md писать **атомарно** (во временный файл, потом `os.replace`) - прерванный прогон не оставит полу-файла.
- Обработчики **SIGINT/SIGTERM + atexit** - при остановке чистить temp-каталог и недописанные файлы.

## Формат выхода

md: шапка (файл, длительность, дата, движок, число спикеров) + транскрипт блоками `**[чч:мм:сс] Спикер N:** текст`. При диаризации смешанной дорожки в шапке пометка "диаризация приблизительная - сверить имена вручную", подписи обезличенные, имена сопоставляются вручную после. При многодорожечном входе спикеры именованы по построению (`**[чч:мм:сс] Иван:**`) - сверять нечего, в шапке отметка о дорожках вместо пометки о приблизительности. По этому md проект дальше строит свое summary (смысл и задачи выводим сами, не из движка).

## Установка движка (машинно, разово)

**Эталонная реализация - `transcribe_meeting.py` рядом с этим файлом.** Это не скрипт для раскатки по проектам: он ставится на машину **один раз** (см. "Один движок на машину") и дальше зовется из любой папки. Порядок: положить его в `~/.local/share/giga-transcribe/`, создать venv с ASR и зависимостями, положить в `~/.local/bin/<cli>` трехстрочный launcher, который экспортирует `GIGA_TRANSCRIBE_HOME` и запускает движок питоном из venv. В нем реализованы решения этого скилла: своя нарезка под потолок куска 24 с (без gated longform), лок с метаданными и таймаутом, проверка памяти, cgroup-лимит, `mkdtemp` на прогон, атомарная запись md, многодорожечный вход с фильтром протечки. Чего в нем нет - нарезки по речи (VAD) и защиты от вырожденных случаев на очень длинных записях (полная матрица расстояний в автодиаризации растет квадратично от числа сегментов). Правки метода вносите в него, а не в форк в проекте.

Ниже - то, что придется решать при установке независимо от реализации:

- Ставь специализированный ASR из **исходника (git)**, если на PyPI устаревшая версия (частый случай у молодых проектов).
- На python.org-сборке Python выставь `SSL_CERT_FILE` через `certifi` - иначе скачивание весов сырым `urllib` падает на `CERTIFICATE_VERIFY_FAILED`.
- Диаризационные ONNX-модели (segmentation + speaker-embedding) - из релизов sherpa-onnx.
- Инференс на CPU у таких моделей **однопоточный** (в мониторе ~1 ядро, выглядит как простой - но не висит): на длинных файлах закладывай время, при потоке выноси на GPU-хост.
- Ориентир по скорости - всегда с указанием железа и того, что именно замерено, иначе он вводит в заблуждение. Замерено на x86 CPU, 4 ядра, GigaAM v3_e2e_rnnt: **полный прогон 42-минутной записи - ~15 минут на всех ядрах** (`--fast`; RTF ≈ 0,35 по всему пайплайну; в фоновом режиме по умолчанию на двух потоках - RTF около 0,7, примерно вдвое дольше; чистый ASR-шаг заметно быстрее, диаризация поверх него - секунды). Считай по полному прогону: ffmpeg, нарезка и склейка тоже входят в ожидание пользователя.

## Coupled tooling

Скилл - драйвер локального CLI, который ставится на машину (пути/venv свои). Здесь - переносимый метод, решения, грабли и эталонная реализация; машинно-специфичное (точные пути на этом устройстве, команды установки под его ОС, бенчмарки своего железа) держи в runbook того проекта, где движок поднимали, и краткую справку "что стоит и как звать" - в пользовательском `~/.claude/CLAUDE.md`.

Files in this skill

  • SKILL.md23.8 KB
  • transcribe_meeting.py46.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.