Claude Skill

transcribe-audio-local

Локальная транскрибация аудиофайлов без отправки в облако. Используй когда пользователь просит транскрибировать запись, расшифровать аудио, сделать конспект встречи, преобразовать речь в текст. Только для аудио (m4a/mp3/wav/ogg/flac/aac/wma/opus). Движок: faster-whisper CUDA + оп

LLM Mart · 0 points · 12 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download Desko77-claude-code-skills-1c-skills_transcribe-audio-local-eb281b4.zip · 23 KB
Part of desko77/claude-code-skills-1c — 48 skills

Install

skills CLI npx skills add https://github.com/Desko77/claude-code-skills-1c/tree/main/skills/transcribe-audio-local
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install desko77-claude-code-skills-1c@llmmart
Git git clone https://github.com/Desko77/claude-code-skills-1c.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole desko77/claude-code-skills-1c collection as a plugin from our marketplace. Git is the plain clone.

README

transcribe-audio-local

Локальная транскрибация аудио + опциональная диаризация. Без облака, без API-ключей.

  • Транскрипция: faster-whisper (CUDA, large-v3-turbo)
  • Диаризация: sherpa-onnx GPU (pyannote-segmentation-3.0 + 3D-Speaker eres2net)
  • Производительность на RTX 5070 Ti Laptop: ~7 мин на 30 мин аудио с диаризацией (RTF ~0.24)

Системные требования

  • Python 3.10+ (рекомендуется 3.12)
  • NVIDIA GPU с CUDA 12 и cuDNN 9 (для GPU режима)
  • ffmpeg - либо в PATH, либо setup поставит static-ffmpeg (pip-пакет) в venv-whisper автоматически. Системные права не нужны.
  • Windows x64 или Linux x64
  • ~5 ГБ свободного места (venv-whisper ~2 ГБ, venv-sherpa ~3 ГБ, модели ~91 МБ)

CPU-режим работает, но в 10+ раз медленнее.

Установка

1. Скопируйте папку скила

В Claude Code/Cursor скилы лежат в ~/.claude/skills/. Скопируйте папку transcribe-audio-local/ туда либо в любое другое место (если запускаете скрипты напрямую).

2. Запустите установщик

python ~/.claude/skills/transcribe-audio-local/scripts/setup.py

Что произойдет:

  1. Проверка Python и ffmpeg.
  2. Создание venv-whisper/ рядом со скилом и установка faster-whisper + CUDA-стека.
  3. Создание venv-sherpa/ и установка sherpa-onnx (GPU сборка) + onnxruntime-gpu.
  4. Скачивание моделей в models/:
    • sherpa-onnx-pyannote-segmentation-3-0.tar.bz2 (~7 МБ, распакуется)
    • 3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx (~40 МБ)

Время установки: 5-15 минут (зависит от скорости интернета).

Флаги установщика

Флаг Когда нужен
--skip-whisper venv-whisper уже создан, повторное создание не нужно
--skip-sherpa Диаризация не нужна, пропустить установку sherpa
--skip-models Модели уже скачаны/положены вручную
--allow-cpu Разрешить установку на машине без NVIDIA GPU (CPU-режим, в 10+ раз медленнее)

Проверка установки

python scripts/verify.py             # быстрая проверка (5 сек)
python scripts/verify.py --full      # с реальным прогоном на silent WAV (~1-2 мин)

Что проверяется: venv-whisper и venv-sherpa с импортами, наличие моделей, ffmpeg в PATH. С --full дополнительно запускает транскрипцию tiny-модели на сгенерированном WAV.

Для AI-агентов

Установка длинная (~15-25 минут с загрузкой моделей). Перед запуском setup.py через subprocess/Bash увеличьте таймаут до 25-30 минут или используйте фоновый режим. См. раздел "Для агента" в SKILL.md.

Использование

Без диаризации

python ~/.claude/skills/transcribe-audio-local/scripts/transcribe.py "путь/к/audio.mp3"

Создает в путь/к/Транскрипция/audio/:

  • audio - транскрипция.md (markdown с таймкодами)
  • audio - транскрипция.txt (plain text)

С диаризацией (разделение по спикерам)

python ~/.claude/skills/transcribe-audio-local/scripts/transcribe.py "путь/к/audio.mp3" --diarize

Дополнительно создает:

  • audio - со спикерами.md (реплики с [SPEAKER_XX, MM:SS])

Все опции

positional:
  AudioPath              Путь к аудиофайлу (m4a/mp3/wav/ogg/flac/aac/wma/opus)

options:
  --output-dir DIR       Каталог вывода (default: <каталог входа>/Транскрипция/<имя>/)
  --diarize              Включить диаризацию (параллельно с транскрипцией)
  --num-speakers N       Точное число спикеров (без авто-кластеризации)
  --threshold T          Порог кластеризации sherpa (default 0.5)
  --language LANG        Язык транскрипции (default ru)
  --model NAME           Модель faster-whisper (default mobiuslabsgmbh/faster-whisper-large-v3-turbo)
  --device DEV           cuda или cpu (default cuda)
  --compute-type CT      Точность (default float16)
  --keep-intermediate    Не удалять промежуточные JSON

Поддерживаемые форматы

Только аудио: mp3, wav, ogg, m4a, flac, aac, wma, opus.

Видео не поддерживается. Если у вас видео - извлеките аудиодорожку через ffmpeg:

ffmpeg -i video.mp4 -vn -acodec libmp3lame -ab 192k audio.mp3

Архитектура

скил/
├── SKILL.md                # описание для Claude Code
├── README.md               # эта инструкция
├── scripts/
│   ├── transcribe.py       # orchestrator (CLI)
│   ├── diarize_sherpa.py   # worker диаризации
│   └── setup.py            # установщик зависимостей
├── venv-whisper/           # создается setup.py: faster-whisper + CUDA
├── venv-sherpa/            # создается setup.py: sherpa-onnx + onnxruntime-gpu
└── models/                 # скачивается setup.py
    ├── sherpa-onnx-pyannote-segmentation-3-0/model.onnx
    └── 3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx

Два venv нужны из-за конфликта CUDA DLL: ctranslate2 (через faster-whisper) и onnxruntime-gpu (через sherpa-onnx) грузят несовместимые версии cuDNN. Изоляция через subprocess.

Транскрипция и диаризация запускаются параллельно (если --diarize).

Troubleshooting

ffmpeg не найден в PATH

Windows: скачайте с https://www.gyan.dev/ffmpeg/builds/, распакуйте, добавьте bin/ в PATH. Linux: sudo apt install ffmpeg.

CUDA out of memory

Большая модель large-v3-turbo требует ~3 ГБ VRAM. Если не хватает - используйте меньшую:

python scripts/transcribe.py audio.mp3 --model openai/whisper-small --compute-type int8_float16

Или --device cpu (в 10+ раз медленнее).

sherpa_onnx не устанавливается с CUDA

Установщик пробует pip install sherpa-onnx -f https://k2-fsa.github.io/sherpa/onnx/cuda.html. Если не вышло:

  1. Скачайте wheel вручную с https://huggingface.co/csukuangfj2/sherpa-onnx-wheels (например, sherpa_onnx-1.13.0+cuda12.cudnn9-cp312-cp312-win_amd64.whl для Python 3.12 Windows).
  2. Установите: <skill>/venv-sherpa/Scripts/pip.exe install <путь-к-wheel.whl>.

CPU-режим тоже работает: pip install sherpa-onnx без extra-index.

pyannote/speaker-diarization gated model

Этот скил НЕ использует gated модели pyannote. Использует только открытые ONNX:

  • pyannote-segmentation-3.0 (выложен k2-fsa в открытом доступе)
  • 3D-Speaker eres2net (Apache 2.0)

HF_TOKEN не нужен.

Модели не скачиваются

URL из setup.py:

Если они недоступны - скачайте вручную, положите в models/, повторите setup.py --skip-models (распаковка архива все равно произойдет).

Транскрипция падает с dll not found

CUDA DLL не подхватываются. Проверьте:

  1. Установлен ли CUDA 12 Toolkit?
  2. В venv-whisper/Lib/site-packages/nvidia/ есть пакеты cublas, cudnn, cuda_runtime?
  3. Перезапустите терминал после установки CUDA.

Лицензии

  • faster-whisper - MIT
  • sherpa-onnx - Apache 2.0
  • pyannote-segmentation-3.0 (ONNX) - MIT (k2-fsa)
  • 3D-Speaker eres2net - Apache 2.0

Происхождение

Адаптировано из скила transcribe (https://github.com/Desko77/claude-code-skills-1c) с урезанием поддержки видео и облачного Gemini-движка для self-contained передачи.

Skill manifest

/transcribe-audio-local - Локальная транскрибация аудио

Локальный движок: faster-whisper (CUDA) + опц. диаризация sherpa-onnx GPU (CUDA) с моделями pyannote-segmentation-3.0 + 3D-Speaker eres2net. Нет затрат, ничего не уходит наружу.

На RTX 5070 Ti Laptop: ~6-7 мин на 30 мин аудио (RTF ~0.24). Только аудио, видео не поддерживается.

Установка (один раз)

python scripts/setup.py

Подробнее в README.md. После установки venv-whisper и venv-sherpa создаются рядом со скилом, модели качаются в models/.

Для агента (Claude Code и подобные)

Workflow первого использования:

  1. Проверь, установлен ли скил - есть ли файл ~/.claude/skills/transcribe-audio-local/venv-whisper/Scripts/python.exe (на Linux: venv-whisper/bin/python). Если есть - переходи к шагу 4.

  2. Если не установлен - запусти setup. Установка занимает 10-20 минут (faster-whisper + ctranslate2-CUDA + sherpa-onnx + скачивание моделей). Дефолтный Bash-таймаут 2 минуты не подходит:

    • В Claude Code Bash tool используй run_in_background: true или timeout: 1500000 (25 мин).
    • В других runtime - аналогичный длинный таймаут.
    python ~/.claude/skills/transcribe-audio-local/scripts/setup.py
    

    Что setup сделает сам:

    • Создаст venv-whisper и venv-sherpa
    • Если ffmpeg нет в PATH - поставит static-ffmpeg (pip-пакет с бинарниками) в venv-whisper. Без прав админа.
    • Скачает модели диаризации
    • Проверит версию драйверов NVIDIA и предупредит, если они старее 525.x

    Что setup НЕ может сделать сам (нужны действия пользователя):

    • Установить системные драйверы NVIDIA / CUDA Toolkit (нужны админ-права)
    • Поставить NVIDIA GPU в машину 😉

    Если у машины нет GPU - setup упадет с FAIL. Уточни у пользователя, согласен ли на CPU-режим (в 10+ раз медленнее), и перезапусти с --allow-cpu.

  3. Проверь установку smoke-тестом (1-2 минуты, с реальной транскрипцией):

    python ~/.claude/skills/transcribe-audio-local/scripts/verify.py --full
    

    Без --full (5 сек) - только проверка наличия venv'ов и моделей, без реального прогона.

  4. Транскрибируй файл (см. секцию "Инструкция" ниже). На CPU добавляй --device cpu --compute-type int8.

Режимы

Без диаризации (default)

Выходные файлы:

  • <имя> - транскрипция.md - таймкоды + текст
  • <имя> - транскрипция.txt - plain text

С диаризацией (--diarize)

Дополнительно:

  • <имя> - со спикерами.md - реплики с метками [SPEAKER_XX, MM:SS]

Аргументы

Параметр Обязательный По умолчанию Описание
AudioPath да - Путь к аудиофайлу
--output-dir нет <каталог>/Транскрипция/<имя>/ Каталог результатов
--diarize нет выкл Разделение по спикерам (sherpa-onnx)
--num-speakers N нет автодетект Точное число спикеров
--threshold нет 0.5 Порог кластеризации (меньше -> больше кластеров)
--language нет ru Язык транскрипции
--model нет mobiuslabsgmbh/faster-whisper-large-v3-turbo Модель faster-whisper
--device нет cuda cuda или cpu
--compute-type нет float16 Точность вычислений

Поддерживаемые форматы

mp3, wav, ogg, m4a, flac, aac, wma, opus.

Зависимости

  • venv-whisper (рядом со скилом): faster-whisper, ctranslate2-CUDA, av, nvidia-cublas/cudnn/cuda-runtime
  • venv-sherpa (рядом со скилом, для --diarize): sherpa_onnx (GPU CUDA сборка), onnxruntime-gpu, soundfile
  • ffmpeg в PATH
  • NVIDIA GPU + CUDA 12 + cuDNN 9 (для GPU режима, рекомендуется)

Инструкция

  1. Получи AudioPath от пользователя. Проверь что расширение - аудио из поддерживаемого списка.

  2. Запусти транскрипцию:

PYTHONUNBUFFERED=1 PYTHONIOENCODING=utf-8 \
  python ~/.claude/skills/transcribe-audio-local/scripts/transcribe.py \
  "<AudioPath>" [--output-dir "<OutputDir>"] [--diarize] [--num-speakers N]

Время работы:

  • Без диаризации: ~1.5-2 мин на 27-мин файл (RTF ~0.07)
  • С диаризацией: ~10 мин на 27-мин файл (RTF ~0.4, параллельно)
  • Часовое аудио с диаризацией: ~25 мин
  1. После завершения покажи пользователю пути к файлам и прочитай начало транскрипции.

ВАЖНО: PYTHONUNBUFFERED=1 обязательно для прогресса.

Ограничения

  • Только аудио, видео не поддерживается. Если нужно из видео - извлеките аудиодорожку через ffmpeg отдельно.
  • Требуется NVIDIA GPU с CUDA 12+ (CPU-режим работает, но в 10+ раз медленнее).
  • Точность таймкодов +/- несколько секунд.
  • Кириллические имена файлов обрабатываются скриптом.

Установка диагностика

Если транскрипция или диаризация падает - см. README.md секция "Troubleshooting".

Files (claude-code-skills-1c)
  • scripts
    • diarize_sherpa.py 7.6 KB
      """
      Worker для sherpa-onnx диаризации (CUDA через onnxruntime-gpu).
      
      Запускается как subprocess из transcribe.py orchestrator. Работает в отдельном venv:
          <skill_root>/venv-sherpa
      
      Использует:
          - pyannote-segmentation-3.0 в ONNX
          - 3D-Speaker eres2net 200k (multilingual) embedding extractor
          - Спектральная кластеризация (FastClustering) для группировки эмбеддингов
      
      Аргументы:
          --input <audio>     путь к аудио (любой формат, конвертируется через ffmpeg в 16kHz mono WAV)
          --out-json <path>   путь сохранения turns JSON
          --num-speakers N    точное число спикеров (отключает кластеризацию по threshold)
          --threshold T       порог кластеризации (default 0.5, чем меньше - тем больше кластеров)
          --provider cuda|cpu (default cuda)
      
      Вывод JSON: list[{"start": float, "end": float, "speaker": "SPEAKER_XX"}].
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import subprocess
      import sys
      import tempfile
      import time
      from pathlib import Path
      
      
      SKILL_ROOT = Path(__file__).resolve().parent.parent
      MODELS_DIR = SKILL_ROOT / "models"
      SEG_MODEL = MODELS_DIR / "sherpa-onnx-pyannote-segmentation-3-0" / "model.onnx"
      EMB_MODEL = MODELS_DIR / "3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx"
      
      
      def setup_nvidia_dll_path() -> None:
          """Зарегистрировать bin-директории nvidia.* пакетов (CUDA 12 для onnxruntime-gpu)."""
          venv_root = Path(sys.executable).parent.parent
          nvidia_root = venv_root / "Lib" / "site-packages" / "nvidia"
          if not nvidia_root.exists():
              return
          for sub in nvidia_root.iterdir():
              bin_dir = sub / "bin"
              if bin_dir.is_dir():
                  if hasattr(os, "add_dll_directory"):
                      try:
                          os.add_dll_directory(str(bin_dir))
                      except OSError:
                          pass
                  os.environ["PATH"] = str(bin_dir) + os.pathsep + os.environ.get("PATH", "")
      
      
      setup_nvidia_dll_path()
      
      import numpy as np  # noqa: E402
      import soundfile as sf  # noqa: E402
      import sherpa_onnx  # noqa: E402
      
      # Вывод содержит кириллицу. Без явного переключения печать падает с UnicodeEncodeError
      # везде, где консоль не в UTF-8: сборочный агент, чужая локаль.
      sys.stdout.reconfigure(encoding='utf-8')
      sys.stderr.reconfigure(encoding='utf-8')
      
      
      def ffmpeg_to_wav16k(input_path: Path, out_wav: Path) -> None:
          """Конвертировать любое аудио в 16kHz mono PCM_S16LE WAV через ffmpeg."""
          cmd = [
              "ffmpeg", "-y", "-i", str(input_path),
              "-vn", "-ac", "1", "-ar", "16000", "-acodec", "pcm_s16le",
              str(out_wav),
          ]
          subprocess.run(cmd, check=True, capture_output=True)
      
      
      def diarize(args) -> int:
          if not SEG_MODEL.exists():
              print(f"[D] Не найдена модель сегментации: {SEG_MODEL}", file=sys.stderr)
              print(f"[D] Запустите установку: python scripts/setup.py", file=sys.stderr)
              return 1
          if not EMB_MODEL.exists():
              print(f"[D] Не найдена модель эмбеддингов: {EMB_MODEL}", file=sys.stderr)
              print(f"[D] Запустите установку: python scripts/setup.py", file=sys.stderr)
              return 1
      
          input_path = Path(args.input)
          print(f"[D] Файл: {input_path.name}", flush=True)
          print(f"[D] Provider: {args.provider}", flush=True)
          print(f"[D] Сегментация: {SEG_MODEL.name}", flush=True)
          print(f"[D] Эмбеддинги: {EMB_MODEL.name}", flush=True)
      
          with tempfile.TemporaryDirectory() as tmp:
              wav_path = Path(tmp) / "audio_16k.wav"
              print(f"[D] ffmpeg -> {wav_path.name}...", flush=True)
              t0 = time.time()
              ffmpeg_to_wav16k(input_path, wav_path)
              print(f"[D]   ffmpeg готов за {time.time() - t0:.1f}с", flush=True)
      
              samples, sr = sf.read(str(wav_path), dtype="float32")
              if samples.ndim > 1:
                  samples = samples.mean(axis=1)
              print(f"[D] Sample rate: {sr}, длительность: {len(samples) / sr:.1f}с", flush=True)
      
              config = sherpa_onnx.OfflineSpeakerDiarizationConfig(
                  segmentation=sherpa_onnx.OfflineSpeakerSegmentationModelConfig(
                      pyannote=sherpa_onnx.OfflineSpeakerSegmentationPyannoteModelConfig(
                          model=str(SEG_MODEL),
                      ),
                      provider=args.provider,
                      num_threads=1,
                      debug=False,
                  ),
                  embedding=sherpa_onnx.SpeakerEmbeddingExtractorConfig(
                      model=str(EMB_MODEL),
                      provider=args.provider,
                      num_threads=1,
                      debug=False,
                  ),
                  clustering=sherpa_onnx.FastClusteringConfig(
                      num_clusters=args.num_speakers if args.num_speakers else -1,
                      threshold=args.threshold,
                  ),
                  min_duration_on=0.3,
                  min_duration_off=0.5,
              )
      
              if not config.validate():
                  print("[D] Конфигурация невалидна", file=sys.stderr)
                  return 1
      
              print("[D] Загрузка моделей...", flush=True)
              t0 = time.time()
              sd = sherpa_onnx.OfflineSpeakerDiarization(config)
              print(f"[D]   Готово за {time.time() - t0:.1f}с", flush=True)
      
              print("[D] Диаризация...", flush=True)
              t0 = time.time()
              last_pct = -1
      
              def progress(num_processed: int, num_total: int) -> int:
                  nonlocal last_pct
                  pct = int(100 * num_processed / max(num_total, 1))
                  if pct >= last_pct + 5:
                      last_pct = pct
                      print(f"[D]   [{pct:3d}%]", flush=True)
                  return 0
      
              result = sd.process(samples, callback=progress).sort_by_start_time()
              elapsed = time.time() - t0
      
              turns: list[dict] = []
              for r in result:
                  turns.append({
                      "start": float(r.start),
                      "end": float(r.end),
                      "speaker": f"SPEAKER_{int(r.speaker):02d}",
                  })
      
              speakers_set = sorted({t["speaker"] for t in turns})
              rtf = elapsed / (len(samples) / sr) if len(samples) > 0 else 0
              print(f"[D] Готово за {elapsed:.1f}с (RTF {rtf:.3f}, {len(turns)} turns, {len(speakers_set)} спикеров)", flush=True)
      
              Path(args.out_json).write_text(json.dumps(turns, ensure_ascii=False), encoding="utf-8")
              print(f"[D]   -> {args.out_json}", flush=True)
      
          sys.stdout.flush()
          sys.stderr.flush()
          os._exit(0)
      
      
      def main() -> int:
          ap = argparse.ArgumentParser()
          ap.add_argument("--input", required=True)
          ap.add_argument("--out-json", required=True)
          ap.add_argument("--num-speakers", type=int, default=None)
          ap.add_argument("--threshold", type=float, default=0.5)
          ap.add_argument("--provider", default="cuda", choices=["cuda", "cpu"])
          args = ap.parse_args()
          return diarize(args)
      
      
      if __name__ == "__main__":
          import traceback
          try:
              rc = main()
          except SystemExit:
              raise
          except BaseException:
              log_path = SKILL_ROOT / "diarize_sherpa.crash.log"
              with log_path.open("a", encoding="utf-8") as f:
                  f.write("=" * 80 + "\n")
                  f.write(f"argv: {sys.argv}\n")
                  f.write(traceback.format_exc())
              traceback.print_exc()
              rc = 2
          sys.exit(rc)
      
    • setup.py 14.1 KB
      """
      Установка зависимостей скила transcribe-audio-local.
      
      Что делает:
      1. Проверяет системные требования (Python, ffmpeg).
      2. Создает venv-whisper и ставит faster-whisper + ctranslate2 (CUDA) + av.
      3. Создает venv-sherpa и ставит sherpa_onnx (GPU CUDA) + onnxruntime-gpu + soundfile.
      4. Скачивает модели диаризации в models/:
         - sherpa-onnx-pyannote-segmentation-3-0 (~7 MB)
         - 3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx (~40 MB)
      
      Запуск:
          python scripts/setup.py
          python scripts/setup.py --skip-models       # только venv'ы
          python scripts/setup.py --skip-sherpa       # без диаризации
          python scripts/setup.py --skip-whisper      # пропустить пересоздание venv-whisper
      
      Требования:
          - Python 3.10+ (рекомендуется 3.12)
          - NVIDIA GPU + CUDA 12 + cuDNN 9 (для GPU режима)
          - ffmpeg в PATH
          - Windows x64 или Linux x64
      """
      
      from __future__ import annotations
      
      import argparse
      import os
      import shutil
      import subprocess
      import sys
      import tarfile
      import urllib.request
      from pathlib import Path
      
      # Вывод содержит кириллицу. Без явного переключения печать падает с UnicodeEncodeError
      # везде, где консоль не в UTF-8: сборочный агент, чужая локаль.
      sys.stdout.reconfigure(encoding='utf-8')
      sys.stderr.reconfigure(encoding='utf-8')
      
      SKILL_ROOT = Path(__file__).resolve().parent.parent
      VENV_WHISPER = SKILL_ROOT / "venv-whisper"
      VENV_SHERPA = SKILL_ROOT / "venv-sherpa"
      MODELS_DIR = SKILL_ROOT / "models"
      
      IS_WIN = os.name == "nt"
      VENV_BIN = "Scripts" if IS_WIN else "bin"
      PY_EXE = "python.exe" if IS_WIN else "python"
      
      # URL моделей (k2-fsa официальные релизы)
      MODEL_URLS = {
          "sherpa-onnx-pyannote-segmentation-3-0.tar.bz2":
              "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/"
              "sherpa-onnx-pyannote-segmentation-3-0.tar.bz2",
          "3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx":
              "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/"
              "3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx",
      }
      
      # Пакеты для venv-whisper (faster-whisper + GPU stack)
      WHISPER_PACKAGES = [
          "faster-whisper>=1.0",
          "ctranslate2>=4.5",
          "av>=11",
          "huggingface-hub",
          "numpy<3",
          "nvidia-cublas-cu12",
          "nvidia-cudnn-cu12",
          "nvidia-cuda-runtime-cu12",
          "nvidia-cuda-nvrtc-cu12",
      ]
      
      # Пакеты для venv-sherpa
      # sherpa_onnx CUDA wheel ставится отдельно по индексу k2-fsa (см. install_sherpa_gpu)
      SHERPA_PACKAGES = [
          "onnxruntime-gpu>=1.18",
          "soundfile>=0.12",
          "numpy<2",
          "nvidia-cublas-cu12",
          "nvidia-cudnn-cu12",
          "nvidia-cuda-runtime-cu12",
          "nvidia-cuda-nvrtc-cu12",
          "nvidia-cufft-cu12",
          "nvidia-nvjitlink-cu12",
      ]
      
      
      def step(msg: str) -> None:
          print(f"\n{'=' * 70}\n{msg}\n{'=' * 70}", flush=True)
      
      
      def info(msg: str) -> None:
          print(f"  {msg}", flush=True)
      
      
      def run(cmd: list[str], **kwargs) -> int:
          print(f"  $ {' '.join(str(c) for c in cmd)}", flush=True)
          return subprocess.run(cmd, **kwargs).returncode
      
      
      REQUIRED_GB = 5.0
      MIN_DRIVER_VERSION = (525, 0)  # минимум для CUDA 12.0+
      
      
      def check_gpu() -> tuple[bool, bool]:
          """Probe NVIDIA GPU + версия драйвера. Возвращает (gpu_found, driver_ok).
      
          Печатает info-строки. Если драйвер старый - выдает инструкцию обновить.
          """
          if not shutil.which("nvidia-smi"):
              return False, False
          try:
              result = subprocess.run(
                  ["nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader"],
                  capture_output=True, text=True, timeout=10,
              )
              if result.returncode != 0 or not result.stdout.strip():
                  return False, False
          except (subprocess.SubprocessError, OSError):
              return False, False
      
          driver_ok = True
          for line in result.stdout.strip().splitlines():
              parts = [p.strip() for p in line.split(",")]
              if len(parts) < 3:
                  continue
              name, mem, driver_ver = parts[0], parts[1], parts[2]
              info(f"OK:   GPU: {name} ({mem})")
              info(f"OK:   Драйвер NVIDIA: {driver_ver}")
              try:
                  major_minor = tuple(int(x) for x in driver_ver.split(".")[:2])
                  if major_minor < MIN_DRIVER_VERSION:
                      driver_ok = False
                      info(f"WARN: Драйвер {driver_ver} ниже минимального {MIN_DRIVER_VERSION[0]}.x для CUDA 12")
                      info("      Обновите драйверы NVIDIA:")
                      info("      - Windows: https://www.nvidia.com/Download/index.aspx или GeForce Experience")
                      info("      - Linux: см. инструкцию вашего дистрибутива (apt install nvidia-driver-550 и т.п.)")
                      info("      Без обновления faster-whisper / sherpa-onnx могут падать с CUDA errors.")
              except ValueError:
                  info(f"WARN: Не удалось распарсить версию драйвера '{driver_ver}'")
      
          return True, driver_ok
      
      
      def check_requirements(allow_cpu: bool = False) -> tuple[bool, bool]:
          """Возвращает (ok, need_static_ffmpeg)."""
          step("Проверка требований")
          ok = True
          need_static_ffmpeg = False
      
          py_version = sys.version_info
          if py_version < (3, 10):
              info(f"FAIL: Python {py_version.major}.{py_version.minor} < 3.10")
              ok = False
          else:
              info(f"OK:   Python {py_version.major}.{py_version.minor}.{py_version.micro}")
      
          if not shutil.which("ffmpeg"):
              info("INFO: ffmpeg не найден в PATH - будет установлен как pip-пакет static-ffmpeg в venv-whisper")
              need_static_ffmpeg = True
          else:
              info(f"OK:   ffmpeg в {shutil.which('ffmpeg')}")
      
          gpu_found, _driver_ok = check_gpu()
          if not gpu_found:
              if allow_cpu:
                  info("WARN: NVIDIA GPU не найден (nvidia-smi). Установка продолжится для CPU-режима.")
                  info("      Транскрипция будет работать, но в 10+ раз медленнее. Запуск с --device cpu.")
              else:
                  info("FAIL: NVIDIA GPU не найден (nvidia-smi). Скил настроен для CUDA GPU.")
                  info("      Если у вас нет GPU - перезапустите setup с флагом --allow-cpu")
                  info("      Если GPU есть, но nvidia-smi не работает - установите драйверы NVIDIA + CUDA 12.")
                  ok = False
      
          try:
              free_gb = shutil.disk_usage(SKILL_ROOT).free / (1024 ** 3)
              if free_gb < REQUIRED_GB:
                  info(f"FAIL: Свободного места {free_gb:.1f} GB < требуется {REQUIRED_GB} GB")
                  ok = False
              else:
                  info(f"OK:   Свободно на диске: {free_gb:.1f} GB")
          except OSError as e:
              info(f"WARN: Не удалось проверить свободное место: {e}")
      
          info(f"Платформа: {'Windows' if IS_WIN else sys.platform}")
          return ok, need_static_ffmpeg
      
      
      def create_venv(venv_path: Path) -> Path:
          """Создать venv если его нет, вернуть путь к python внутри."""
          py = venv_path / VENV_BIN / PY_EXE
          if py.exists():
              info(f"venv уже существует: {venv_path}")
              return py
          info(f"Создание venv: {venv_path}")
          rc = run([sys.executable, "-m", "venv", str(venv_path)])
          if rc != 0:
              raise RuntimeError(f"Не удалось создать venv {venv_path}")
          rc = run([str(py), "-m", "pip", "install", "--upgrade", "pip", "setuptools", "wheel"])
          if rc != 0:
              raise RuntimeError("Не удалось обновить pip")
          return py
      
      
      def pip_install(py: Path, packages: list[str], extra_args: list[str] | None = None) -> None:
          cmd = [str(py), "-m", "pip", "install"]
          if extra_args:
              cmd += extra_args
          cmd += packages
          rc = run(cmd)
          if rc != 0:
              raise RuntimeError(f"pip install упал на: {packages}")
      
      
      def install_whisper(skip: bool, install_static_ffmpeg: bool) -> None:
          step("venv-whisper: faster-whisper + CUDA stack")
          if skip:
              info("Пропускаем по флагу --skip-whisper")
              return
          py = create_venv(VENV_WHISPER)
          pip_install(py, WHISPER_PACKAGES)
          if install_static_ffmpeg:
              info("Установка static-ffmpeg (ffmpeg+ffprobe бинарники как pip-пакет)")
              pip_install(py, ["static-ffmpeg"])
          info("OK")
      
      
      def install_sherpa_gpu(py: Path) -> None:
          """Установить sherpa_onnx с GPU поддержкой.
      
          Сначала пробуем с PyPI с extra-index-url от k2-fsa (содержит CUDA wheels).
          Если не вышло - инструкция пользователю.
          """
          info("Установка sherpa_onnx (GPU/CUDA)...")
          extra_index = "https://k2-fsa.github.io/sherpa/onnx/cuda.html"
          try:
              cmd = [str(py), "-m", "pip", "install", "sherpa-onnx",
                     "-f", extra_index]
              rc = run(cmd)
              if rc == 0:
                  info("sherpa_onnx установлен (попробуйте проверить наличие CUDA: см. README)")
                  return
          except Exception as e:
              info(f"WARN: {e}")
      
          info("ВНИМАНИЕ: автоматическая установка sherpa_onnx с CUDA не удалась.")
          info("Установите вручную:")
          info(f"  {py} -m pip install sherpa-onnx")
          info("Или скачайте GPU wheel с https://huggingface.co/csukuangfj2/sherpa-onnx-wheels")
          info("и установите:")
          info(f"  {py} -m pip install <путь-к-wheel.whl>")
      
      
      def install_sherpa(skip: bool) -> None:
          step("venv-sherpa: sherpa_onnx + onnxruntime-gpu")
          if skip:
              info("Пропускаем по флагу --skip-sherpa")
              return
          py = create_venv(VENV_SHERPA)
          pip_install(py, SHERPA_PACKAGES)
          install_sherpa_gpu(py)
          info("OK")
      
      
      def download_file(url: str, dest: Path) -> None:
          if dest.exists():
              info(f"Уже скачан: {dest.name} ({dest.stat().st_size / 1e6:.1f} MB)")
              return
          info(f"Скачивание {dest.name}")
          info(f"  url: {url}")
          tmp = dest.with_suffix(dest.suffix + ".tmp")
          try:
              with urllib.request.urlopen(url) as resp, open(tmp, "wb") as f:
                  total = int(resp.headers.get("Content-Length", 0))
                  downloaded = 0
                  chunk = 1 << 16
                  last_pct = -1
                  while True:
                      data = resp.read(chunk)
                      if not data:
                          break
                      f.write(data)
                      downloaded += len(data)
                      if total:
                          pct = int(100 * downloaded / total)
                          if pct >= last_pct + 10:
                              last_pct = pct
                              info(f"  [{pct:3d}%] {downloaded / 1e6:.1f} / {total / 1e6:.1f} MB")
              tmp.rename(dest)
          except Exception as e:
              if tmp.exists():
                  tmp.unlink()
              raise RuntimeError(f"Не удалось скачать {url}: {e}") from e
      
      
      def extract_segmentation_archive() -> None:
          """Распаковать sherpa-onnx-pyannote-segmentation-3-0.tar.bz2 в models/."""
          archive = MODELS_DIR / "sherpa-onnx-pyannote-segmentation-3-0.tar.bz2"
          target_dir = MODELS_DIR / "sherpa-onnx-pyannote-segmentation-3-0"
          if (target_dir / "model.onnx").exists():
              info(f"Уже распакован: {target_dir}")
              return
          info(f"Распаковка {archive.name}")
          with tarfile.open(archive, "r:bz2") as tf:
              tf.extractall(MODELS_DIR)
          if not (target_dir / "model.onnx").exists():
              raise RuntimeError(f"После распаковки нет {target_dir / 'model.onnx'}")
      
      
      def download_models(skip: bool) -> None:
          step("Модели диаризации")
          if skip:
              info("Пропускаем по флагу --skip-models")
              return
          MODELS_DIR.mkdir(exist_ok=True)
          for fname, url in MODEL_URLS.items():
              download_file(url, MODELS_DIR / fname)
          extract_segmentation_archive()
          info("OK")
      
      
      def main() -> int:
          ap = argparse.ArgumentParser(description="Установка скила transcribe-audio-local")
          ap.add_argument("--skip-whisper", action="store_true", help="Не пересоздавать venv-whisper")
          ap.add_argument("--skip-sherpa", action="store_true", help="Не ставить sherpa (без диаризации)")
          ap.add_argument("--skip-models", action="store_true", help="Не скачивать модели")
          ap.add_argument("--allow-cpu", action="store_true", help="Разрешить установку без GPU (CPU-режим, в 10+ раз медленнее)")
          args = ap.parse_args()
      
          ok, need_static_ffmpeg = check_requirements(allow_cpu=args.allow_cpu)
          if not ok:
              print("\nПроверка требований не пройдена. Исправьте и повторите.", file=sys.stderr)
              return 1
      
          try:
              install_whisper(args.skip_whisper, install_static_ffmpeg=need_static_ffmpeg)
              install_sherpa(args.skip_sherpa)
              download_models(args.skip_models)
          except Exception as e:
              print(f"\nОшибка установки: {e}", file=sys.stderr)
              return 1
      
          step("Готово")
          info(f"Скил: {SKILL_ROOT}")
          info(f"venv-whisper: {VENV_WHISPER}")
          info(f"venv-sherpa:  {VENV_SHERPA}")
          info(f"models:       {MODELS_DIR}")
          info("")
          info("Проверка:")
          info(f"  python {SKILL_ROOT / 'scripts' / 'transcribe.py'} <audio.mp3>")
          info(f"  python {SKILL_ROOT / 'scripts' / 'transcribe.py'} <audio.mp3> --diarize")
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • transcribe.py 18.1 KB
      """
      Локальная транскрипция аудио через faster-whisper (CUDA) + опц. диаризация (sherpa-onnx GPU).
      
      Архитектура: orchestrator + 2 subprocess (изоляция CUDA-DLL ctranslate2 vs onnxruntime).
      При --diarize транскрипция и диаризация запускаются параллельно.
      
      Использование:
          python transcribe.py <input_path> [--output-dir DIR] [--model MODEL]
                               [--language ru] [--diarize]
                               [--num-speakers N] [--threshold T]
      
      Зависимости:
          venv-whisper (рядом со скилом): faster-whisper, ctranslate2-CUDA, av, huggingface-hub.
          venv-sherpa (рядом со скилом, для --diarize): sherpa_onnx CUDA, onnxruntime-gpu, soundfile.
          ffmpeg в PATH.
      
      Файлы вывода:
          <base> - транскрипция.md       MD c таймкодами по сегментам (без спикеров)
          <base> - транскрипция.txt      Plain text
          <base> - со спикерами.md       MD c [Спикер, MM:SS] (только при --diarize)
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import subprocess
      import sys
      import time
      
      # Подключаем ffmpeg из static-ffmpeg если он установлен (когда системного ffmpeg нет в PATH).
      # subprocess'ы наследуют PATH, поэтому worker'ам ffmpeg тоже будет доступен.
      try:
          from static_ffmpeg import add_paths as _sff_add_paths
          _sff_add_paths()
      except ImportError:
          pass
      import traceback
      from pathlib import Path
      
      # Вывод содержит кириллицу. Без явного переключения печать падает с UnicodeEncodeError
      # везде, где консоль не в UTF-8: сборочный агент, чужая локаль.
      sys.stdout.reconfigure(encoding='utf-8')
      sys.stderr.reconfigure(encoding='utf-8')
      
      
      SKILL_ROOT = Path(__file__).resolve().parent.parent
      WHISPER_VENV_PYTHON = SKILL_ROOT / "venv-whisper" / ("Scripts" if os.name == "nt" else "bin") / ("python.exe" if os.name == "nt" else "python")
      SHERPA_VENV_PYTHON = SKILL_ROOT / "venv-sherpa" / ("Scripts" if os.name == "nt" else "bin") / ("python.exe" if os.name == "nt" else "python")
      SHERPA_WORKER = Path(__file__).resolve().parent / "diarize_sherpa.py"
      
      AUDIO_EXTS = {".m4a", ".mp3", ".wav", ".ogg", ".flac", ".aac", ".wma", ".opus"}
      
      
      def _setup_cuda_dll_path() -> None:
          """Добавить bin-директории nvidia.* пакетов в PATH и DLL search (Windows, CUDA 12)."""
          for name in ("nvidia.cublas", "nvidia.cudnn", "nvidia.cuda_runtime", "nvidia.cuda_nvrtc"):
              try:
                  mod = __import__(name, fromlist=[""])
                  bin_dir = os.path.join(mod.__path__[0], "bin")
                  if os.path.isdir(bin_dir):
                      if hasattr(os, "add_dll_directory"):
                          os.add_dll_directory(bin_dir)
                      os.environ["PATH"] = bin_dir + os.pathsep + os.environ.get("PATH", "")
              except ImportError:
                  pass
      
      
      def format_ts(seconds: float) -> str:
          s = int(seconds)
          return f"{s // 60:02d}:{s % 60:02d}"
      
      
      # =====================================================================
      # WORKER: транскрипция (запускается как subprocess в venv-whisper)
      # =====================================================================
      
      def worker_transcribe(args) -> int:
          _setup_cuda_dll_path()
          from faster_whisper import WhisperModel  # noqa
      
          print(f"[T] Модель: {args.model} ({args.device}/{args.compute_type})", flush=True)
          t0 = time.time()
          model = WhisperModel(args.model, device=args.device, compute_type=args.compute_type)
          print(f"[T] Модель загружена за {time.time() - t0:.1f}с", flush=True)
      
          print(f"[T] Транскрипция (word_timestamps={args.need_words})...", flush=True)
          t0 = time.time()
          segments_iter, info = model.transcribe(
              args.input,
              language=args.language,
              beam_size=5,
              vad_filter=True,
              vad_parameters={"min_silence_duration_ms": 500},
              condition_on_previous_text=True,
              word_timestamps=args.need_words,
          )
          print(f"[T] Язык: {info.language} (p={info.language_probability:.2f}) длительность {info.duration:.1f}с", flush=True)
      
          segments: list[dict] = []
          last_pct = -1
          for seg in segments_iter:
              words = []
              if args.need_words and seg.words:
                  for w in seg.words:
                      words.append({"start": float(w.start), "end": float(w.end), "text": w.word})
              segments.append({
                  "start": float(seg.start),
                  "end": float(seg.end),
                  "text": seg.text.strip(),
                  "words": words,
              })
              pct = int(100 * seg.end / max(info.duration, 1))
              if pct >= last_pct + 5:
                  last_pct = pct
                  print(f"[T] [{pct:3d}%] {format_ts(seg.start)}: {seg.text.strip()[:80]}", flush=True)
      
          info_dict = {
              "language": info.language,
              "language_probability": float(info.language_probability),
              "duration": float(info.duration),
          }
      
          out = {"segments": segments, "info": info_dict}
          Path(args.out_json).write_text(json.dumps(out, ensure_ascii=False), encoding="utf-8")
          print(f"[T] Готово за {time.time() - t0:.1f}с ({len(segments)} сегментов) -> {args.out_json}", flush=True)
          sys.stdout.flush()
          sys.stderr.flush()
          os._exit(0)
      
      
      # =====================================================================
      # MERGE / OUTPUT
      # =====================================================================
      
      def assign_speaker(midpoint: float, turns: list[dict]) -> str:
          for t in turns:
              if t["start"] <= midpoint <= t["end"]:
                  return t["speaker"]
          if not turns:
              return "UNKNOWN"
          nearest = min(turns, key=lambda t: min(abs(t["start"] - midpoint), abs(t["end"] - midpoint)))
          return nearest["speaker"]
      
      
      def smooth_word_speakers(words: list[dict], min_run: float = 1.2) -> None:
          """Сгладить короткие пробивки спикера в словах: если короткий run X между двумя длинными run Y,
          переаттрибутировать слова run X на спикера Y. Изменяет words на месте.
          """
          if not words:
              return
          runs: list[tuple[int, int, str, float]] = []
          i = 0
          n = len(words)
          while i < n:
              j = i
              spk = words[i]["speaker"]
              while j + 1 < n and words[j + 1]["speaker"] == spk:
                  j += 1
              duration = words[j]["end"] - words[i]["start"]
              runs.append((i, j, spk, duration))
              i = j + 1
      
          changed = True
          while changed:
              changed = False
              for k in range(1, len(runs) - 1):
                  ri, rj, rspk, rdur = runs[k]
                  li, lj, lspk, ldur = runs[k - 1]
                  ni, nj, nspk, ndur = runs[k + 1]
                  if rdur < min_run and lspk == nspk and lspk != rspk:
                      for x in range(ri, rj + 1):
                          words[x]["speaker"] = lspk
                      runs[k - 1] = (li, rj, lspk, words[rj]["end"] - words[li]["start"])
                      runs.pop(k)
                      if k < len(runs):
                          nri, nrj, nrspk, _ = runs[k]
                          if nrspk == lspk:
                              runs[k - 1] = (li, nrj, lspk, words[nrj]["end"] - words[li]["start"])
                              runs.pop(k)
                      changed = True
                      break
      
      
      def merge_with_speakers(segments: list[dict], turns: list[dict]) -> list[dict]:
          all_words: list[dict] = []
          word_segments_no_words: list[dict] = []
          for seg in segments:
              words = seg.get("words") or []
              if not words:
                  mid = (seg["start"] + seg["end"]) / 2
                  spk = assign_speaker(mid, turns)
                  word_segments_no_words.append({
                      "start": seg["start"], "end": seg["end"], "speaker": spk, "text": seg["text"],
                  })
                  continue
              for w in words:
                  mid = (w["start"] + w["end"]) / 2
                  spk = assign_speaker(mid, turns)
                  all_words.append({"start": w["start"], "end": w["end"], "text": w["text"], "speaker": spk})
      
          smooth_word_speakers(all_words, min_run=1.2)
      
          utterances: list[dict] = []
          current: dict | None = None
          for w in all_words:
              if current and current["speaker"] == w["speaker"]:
                  current["end"] = w["end"]
                  current["text"] += w["text"]
              else:
                  if current:
                      utterances.append(current)
                  current = {"start": w["start"], "end": w["end"], "speaker": w["speaker"], "text": w["text"]}
          if current:
              utterances.append(current)
      
          utterances.extend(word_segments_no_words)
          utterances.sort(key=lambda u: u["start"])
      
          merged: list[dict] = []
          for u in utterances:
              u["text"] = u["text"].strip()
              if not u["text"]:
                  continue
              if merged and merged[-1]["speaker"] == u["speaker"] and u["start"] - merged[-1]["end"] < 1.5:
                  merged[-1]["end"] = u["end"]
                  merged[-1]["text"] += " " + u["text"]
              else:
                  merged.append(u)
          return merged
      
      
      def write_basic_md(path: Path, input_name: str, info: dict, segments: list[dict], model_name: str) -> None:
          lines = [
              f"# Транскрипция: {input_name}",
              "",
              f"- Модель: `{model_name}`",
              f"- Язык: {info['language']} (p={info['language_probability']:.2f})",
              f"- Длительность: {format_ts(info['duration'])} ({info['duration']:.1f}с)",
              "",
              "---",
              "",
          ]
          for seg in segments:
              lines.append(f"**[{format_ts(seg['start'])}]** {seg['text']}")
              lines.append("")
          path.write_text("\n".join(lines), encoding="utf-8")
      
      
      def write_plain(path: Path, segments: list[dict]) -> None:
          path.write_text(" ".join(s["text"] for s in segments), encoding="utf-8")
      
      
      def write_speakers_md(path: Path, input_name: str, info: dict, utterances: list[dict], model_name: str) -> None:
          speakers_set = sorted({u["speaker"] for u in utterances})
          lines = [
              f"# Транскрипция со спикерами: {input_name}",
              "",
              f"- Модель: `{model_name}`",
              f"- Диаризация: `sherpa-onnx (pyannote-segmentation-3.0 + 3D-Speaker)`",
              f"- Длительность: {format_ts(info['duration'])} ({info['duration']:.1f}с)",
              f"- Найдено спикеров: {len(speakers_set)} ({', '.join(speakers_set)})",
              "",
              "---",
              "",
          ]
          for u in utterances:
              lines.append(f"**[{u['speaker']}, {format_ts(u['start'])}]** {u['text']}")
              lines.append("")
          path.write_text("\n".join(lines), encoding="utf-8")
      
      
      # =====================================================================
      # ORCHESTRATOR
      # =====================================================================
      
      def spawn_worker(mode: str, env_extra: dict[str, str], cli: list[str], python_exe: Path) -> subprocess.Popen:
          cmd = [str(python_exe), __file__, "--worker", mode] + cli
          env = os.environ.copy()
          env["PYTHONUNBUFFERED"] = "1"
          env["PYTHONIOENCODING"] = "utf-8"
          env.update(env_extra)
          return subprocess.Popen(cmd, env=env)
      
      
      def spawn_sherpa_diarize(env_extra: dict[str, str], cli: list[str]) -> subprocess.Popen:
          """Запустить sherpa-onnx диаризацию из отдельного venv-sherpa."""
          if not SHERPA_VENV_PYTHON.exists():
              raise RuntimeError(
                  f"venv-sherpa не найден: {SHERPA_VENV_PYTHON}. "
                  "Запустите установку: python scripts/setup.py"
              )
          cmd = [str(SHERPA_VENV_PYTHON), str(SHERPA_WORKER)] + cli
          env = os.environ.copy()
          env["PYTHONUNBUFFERED"] = "1"
          env["PYTHONIOENCODING"] = "utf-8"
          env.update(env_extra)
          return subprocess.Popen(cmd, env=env)
      
      
      def orchestrate(args) -> int:
          input_path = Path(args.input)
          if not input_path.exists():
              print(f"Файл не найден: {input_path}", file=sys.stderr)
              return 1
      
          ext = input_path.suffix.lower()
          if ext not in AUDIO_EXTS:
              print(f"Расширение {ext} не поддерживается. Допустимы: {', '.join(sorted(AUDIO_EXTS))}", file=sys.stderr)
              return 1
      
          if not WHISPER_VENV_PYTHON.exists():
              print(f"venv-whisper не найден: {WHISPER_VENV_PYTHON}", file=sys.stderr)
              print("Запустите установку: python scripts/setup.py", file=sys.stderr)
              return 1
      
          if args.output_dir:
              output_dir = Path(args.output_dir)
          else:
              output_dir = input_path.parent / "Транскрипция" / input_path.stem
          output_dir.mkdir(parents=True, exist_ok=True)
      
          base = input_path.stem
          transcribe_json = output_dir / f"{base}.transcribe.json"
          turns_json = output_dir / f"{base}.turns.json"
      
          print(f"Файл: {input_path.name}", flush=True)
          print(f"Каталог: {output_dir}", flush=True)
      
          transcribe_cli = [
              "--input", str(input_path),
              "--out-json", str(transcribe_json),
              "--model", args.model,
              "--language", args.language,
              "--device", args.device,
              "--compute-type", args.compute_type,
          ]
          if args.diarize:
              transcribe_cli.append("--need-words")
      
          transcribe_proc = spawn_worker("transcribe", {}, transcribe_cli, WHISPER_VENV_PYTHON)
      
          diarize_proc = None
          if args.diarize:
              sherpa_cli = [
                  "--input", str(input_path),
                  "--out-json", str(turns_json),
                  "--provider", args.device,
                  "--threshold", str(args.threshold),
              ]
              if args.num_speakers is not None:
                  sherpa_cli += ["--num-speakers", str(args.num_speakers)]
              print(f"Диаризация: sherpa-onnx (pyannote-segmentation-3.0 + 3D-Speaker)", flush=True)
              diarize_proc = spawn_sherpa_diarize({}, sherpa_cli)
      
          rc_t = transcribe_proc.wait()
          if rc_t != 0:
              if diarize_proc is not None:
                  diarize_proc.terminate()
              print(f"Транскрипция упала с кодом {rc_t}", file=sys.stderr)
              return rc_t
      
          payload = json.loads(transcribe_json.read_text(encoding="utf-8"))
          segments = payload["segments"]
          info = payload["info"]
      
          transcript_md = output_dir / f"{base} - транскрипция.md"
          plain_txt = output_dir / f"{base} - транскрипция.txt"
          write_basic_md(transcript_md, input_path.name, info, segments, args.model)
          write_plain(plain_txt, segments)
          print(f"  MD:    {transcript_md}", flush=True)
          print(f"  Plain: {plain_txt}", flush=True)
      
          if diarize_proc is not None:
              rc_d = diarize_proc.wait()
              if rc_d != 0:
                  print(f"Диаризация упала с кодом {rc_d} (транскрипция сохранена)", file=sys.stderr)
                  return rc_d
              turns = json.loads(turns_json.read_text(encoding="utf-8"))
              utterances = merge_with_speakers(segments, turns)
              speakers_md = output_dir / f"{base} - со спикерами.md"
              write_speakers_md(speakers_md, input_path.name, info, utterances, args.model)
              print(f"  Spk:   {speakers_md}", flush=True)
      
          if not args.keep_intermediate:
              for f in (transcribe_json, turns_json):
                  try:
                      if f.exists():
                          f.unlink()
                  except OSError:
                      pass
      
          return 0
      
      
      # =====================================================================
      # MAIN
      # =====================================================================
      
      def main() -> int:
          if len(sys.argv) >= 3 and sys.argv[1] == "--worker":
              mode = sys.argv[2]
              worker_args = sys.argv[3:]
              ap = argparse.ArgumentParser()
              if mode == "transcribe":
                  ap.add_argument("--input", required=True)
                  ap.add_argument("--out-json", required=True)
                  ap.add_argument("--model", default="mobiuslabsgmbh/faster-whisper-large-v3-turbo")
                  ap.add_argument("--language", default="ru")
                  ap.add_argument("--device", default="cuda")
                  ap.add_argument("--compute-type", default="float16")
                  ap.add_argument("--need-words", action="store_true")
                  return worker_transcribe(ap.parse_args(worker_args))
              print(f"Неизвестный worker mode: {mode}", file=sys.stderr)
              return 1
      
          ap = argparse.ArgumentParser(description="Локальная транскрипция аудио через faster-whisper + опц. диаризация sherpa-onnx")
          ap.add_argument("input", help="Путь к аудиофайлу (m4a/mp3/wav/ogg/flac/aac/wma/opus)")
          ap.add_argument("--output-dir", default=None, help="Каталог вывода (default: <каталог входа>/Транскрипция/<имя>/)")
          ap.add_argument("--model", default="mobiuslabsgmbh/faster-whisper-large-v3-turbo")
          ap.add_argument("--language", default="ru")
          ap.add_argument("--device", default="cuda", choices=["cuda", "cpu"])
          ap.add_argument("--compute-type", default="float16")
          ap.add_argument("--diarize", action="store_true", help="Включить диаризацию (sherpa-onnx, параллельно)")
          ap.add_argument("--num-speakers", type=int, default=None, help="Точное число спикеров (без авто-кластеризации)")
          ap.add_argument("--threshold", type=float, default=0.5, help="Порог кластеризации sherpa-onnx (default 0.5)")
          ap.add_argument("--keep-intermediate", action="store_true", help="Не удалять промежуточные JSON")
          args = ap.parse_args()
          return orchestrate(args)
      
      
      if __name__ == "__main__":
          try:
              rc = main()
          except SystemExit:
              raise
          except BaseException:
              log_path = SKILL_ROOT / "transcribe.crash.log"
              with log_path.open("a", encoding="utf-8") as f:
                  f.write("=" * 80 + "\n")
                  f.write(f"argv: {sys.argv}\n")
                  f.write(traceback.format_exc())
                  f.write("\n")
              print(f"\nКраш записан в {log_path}", file=sys.stderr)
              traceback.print_exc()
              rc = 2
          sys.exit(rc)
      
    • verify.py 7.1 KB
      """
      Smoke-тест после установки скила transcribe-audio-local.
      
      Что проверяет:
      1. venv-whisper существует и в нем установлен faster-whisper
      2. venv-sherpa существует и в нем установлены sherpa_onnx + onnxruntime
      3. Модели sherpa-onnx и 3D-Speaker лежат в models/
      4. ffmpeg доступен
      5. (опционально) тестовый прогон на коротком сгенерированном WAV
      
      Запуск:
          python scripts/verify.py             # все проверки кроме прогона
          python scripts/verify.py --full      # + тестовый прогон (создает 3-сек тон и транскрибирует)
      
      Выход: 0 если все проверки прошли, 1 если есть FAIL.
      """
      
      from __future__ import annotations
      
      import argparse
      import os
      import shutil
      import subprocess
      import sys
      import tempfile
      import wave
      from pathlib import Path
      
      # Вывод содержит кириллицу. Без явного переключения печать падает с UnicodeEncodeError
      # везде, где консоль не в UTF-8: сборочный агент, чужая локаль.
      sys.stdout.reconfigure(encoding='utf-8')
      sys.stderr.reconfigure(encoding='utf-8')
      
      
      SKILL_ROOT = Path(__file__).resolve().parent.parent
      VENV_BIN = "Scripts" if os.name == "nt" else "bin"
      PY_EXE = "python.exe" if os.name == "nt" else "python"
      
      VENV_WHISPER_PY = SKILL_ROOT / "venv-whisper" / VENV_BIN / PY_EXE
      VENV_SHERPA_PY = SKILL_ROOT / "venv-sherpa" / VENV_BIN / PY_EXE
      MODELS_DIR = SKILL_ROOT / "models"
      SEG_MODEL = MODELS_DIR / "sherpa-onnx-pyannote-segmentation-3-0" / "model.onnx"
      EMB_MODEL = MODELS_DIR / "3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx"
      
      results: list[tuple[str, bool, str]] = []
      
      
      def check(name: str, condition: bool, detail: str = "") -> None:
          results.append((name, condition, detail))
          status = "OK  " if condition else "FAIL"
          print(f"  [{status}] {name}" + (f": {detail}" if detail else ""), flush=True)
      
      
      def check_venv(name: str, py_exe: Path, test_import: str) -> bool:
          if not py_exe.exists():
              check(f"venv-{name}", False, f"не найден: {py_exe}")
              return False
          check(f"venv-{name}", True, str(py_exe))
          try:
              result = subprocess.run(
                  [str(py_exe), "-c", test_import],
                  capture_output=True, text=True, timeout=60,
              )
              if result.returncode == 0:
                  check(f"venv-{name} imports", True, result.stdout.strip())
                  return True
              check(f"venv-{name} imports", False, result.stderr.strip().splitlines()[-1] if result.stderr else "ошибка импорта")
              return False
          except (subprocess.SubprocessError, OSError) as e:
              check(f"venv-{name} imports", False, str(e))
              return False
      
      
      def create_silent_wav(path: Path, duration_sec: float = 3.0, sample_rate: int = 16000) -> None:
          n_samples = int(duration_sec * sample_rate)
          silence = b"\x00\x00" * n_samples
          with wave.open(str(path), "wb") as w:
              w.setnchannels(1)
              w.setsampwidth(2)
              w.setframerate(sample_rate)
              w.writeframes(silence)
      
      
      def run_transcribe_test() -> bool:
          print("\n  Тестовый прогон транскрипции на 3-сек silent WAV...", flush=True)
          with tempfile.TemporaryDirectory() as tmp:
              wav_path = Path(tmp) / "test.wav"
              create_silent_wav(wav_path)
              out_dir = Path(tmp) / "out"
              out_dir.mkdir()
              try:
                  result = subprocess.run(
                      [str(VENV_WHISPER_PY),
                       str(SKILL_ROOT / "scripts" / "transcribe.py"),
                       str(wav_path),
                       "--output-dir", str(out_dir),
                       "--model", "openai/whisper-tiny",
                       "--compute-type", "int8"],
                      capture_output=True, text=True, timeout=180,
                  )
                  if result.returncode == 0:
                      check("smoke-test транскрипция", True, "транскрибация silent WAV прошла")
                      return True
                  check("smoke-test транскрипция", False, f"rc={result.returncode}: {result.stderr[-200:]}")
                  return False
              except subprocess.TimeoutExpired:
                  check("smoke-test транскрипция", False, "таймаут (>3 мин)")
                  return False
              except OSError as e:
                  check("smoke-test транскрипция", False, str(e))
                  return False
      
      
      def main() -> int:
          ap = argparse.ArgumentParser(description="Smoke-тест скила transcribe-audio-local")
          ap.add_argument("--full", action="store_true", help="Включить тестовый прогон транскрипции (~1-2 мин)")
          args = ap.parse_args()
      
          print(f"Скил: {SKILL_ROOT}\n", flush=True)
          print("Проверка установки:", flush=True)
      
          ffmpeg_path = shutil.which("ffmpeg")
          if ffmpeg_path:
              check("ffmpeg", True, f"в PATH: {ffmpeg_path}")
          else:
              # Может быть установлен через static-ffmpeg в venv-whisper
              try:
                  r = subprocess.run(
                      [str(VENV_WHISPER_PY), "-c",
                       "from static_ffmpeg import add_paths; add_paths(); "
                       "import shutil; print(shutil.which('ffmpeg') or 'NOT_FOUND')"],
                      capture_output=True, text=True, timeout=30,
                  )
                  if r.returncode == 0 and "NOT_FOUND" not in r.stdout:
                      check("ffmpeg", True, f"через static-ffmpeg в venv-whisper: {r.stdout.strip()}")
                  else:
                      check("ffmpeg", False, "не найден ни в PATH, ни через static-ffmpeg")
              except (subprocess.SubprocessError, OSError) as e:
                  check("ffmpeg", False, f"не найден в PATH, static-ffmpeg тоже недоступен: {e}")
      
          check_venv("whisper", VENV_WHISPER_PY,
                     "import faster_whisper; print('faster-whisper', faster_whisper.__version__)")
      
          check_venv("sherpa", VENV_SHERPA_PY,
                     "import sherpa_onnx, onnxruntime; print('sherpa_onnx', sherpa_onnx.__version__, '| onnxruntime', onnxruntime.__version__)")
      
          check("модель сегментации (pyannote-3.0)", SEG_MODEL.exists(),
                f"{SEG_MODEL.stat().st_size / 1e6:.1f} MB" if SEG_MODEL.exists() else "не найдена")
      
          check("модель эмбеддингов (3D-Speaker)", EMB_MODEL.exists(),
                f"{EMB_MODEL.stat().st_size / 1e6:.1f} MB" if EMB_MODEL.exists() else "не найдена")
      
          if args.full and VENV_WHISPER_PY.exists():
              run_transcribe_test()
      
          print("", flush=True)
          failed = [name for name, ok, _ in results if not ok]
          if failed:
              print(f"FAIL: {len(failed)} проверок не прошли: {', '.join(failed)}", flush=True)
              print("Запустите 'python scripts/setup.py' для (пере)установки.", flush=True)
              return 1
          print(f"OK: все {len(results)} проверок прошли. Скил готов к работе.", flush=True)
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
  • .gitignore 66 B · in bundle
  • README.md 9.3 KB
    # transcribe-audio-local
    
    Локальная транскрибация аудио + опциональная диаризация. Без облака, без API-ключей.
    
    - **Транскрипция**: faster-whisper (CUDA, large-v3-turbo)
    - **Диаризация**: sherpa-onnx GPU (pyannote-segmentation-3.0 + 3D-Speaker eres2net)
    - **Производительность** на RTX 5070 Ti Laptop: ~7 мин на 30 мин аудио с диаризацией (RTF ~0.24)
    
    ## Системные требования
    
    - **Python 3.10+** (рекомендуется 3.12)
    - **NVIDIA GPU** с CUDA 12 и cuDNN 9 (для GPU режима)
    - **ffmpeg** - либо в PATH, либо setup поставит `static-ffmpeg` (pip-пакет) в venv-whisper автоматически. Системные права не нужны.
    - **Windows x64** или **Linux x64**
    - **~5 ГБ свободного места** (venv-whisper ~2 ГБ, venv-sherpa ~3 ГБ, модели ~91 МБ)
    
    CPU-режим работает, но в 10+ раз медленнее.
    
    ## Установка
    
    ### 1. Скопируйте папку скила
    
    В Claude Code/Cursor скилы лежат в `~/.claude/skills/`. Скопируйте папку `transcribe-audio-local/` туда либо в любое другое место (если запускаете скрипты напрямую).
    
    ### 2. Запустите установщик
    
    ```bash
    python ~/.claude/skills/transcribe-audio-local/scripts/setup.py
    ```
    
    Что произойдет:
    1. Проверка Python и ffmpeg.
    2. Создание `venv-whisper/` рядом со скилом и установка `faster-whisper` + CUDA-стека.
    3. Создание `venv-sherpa/` и установка `sherpa-onnx` (GPU сборка) + `onnxruntime-gpu`.
    4. Скачивание моделей в `models/`:
       - `sherpa-onnx-pyannote-segmentation-3-0.tar.bz2` (~7 МБ, распакуется)
       - `3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx` (~40 МБ)
    
    Время установки: 5-15 минут (зависит от скорости интернета).
    
    ### Флаги установщика
    
    | Флаг | Когда нужен |
    |---|---|
    | `--skip-whisper` | venv-whisper уже создан, повторное создание не нужно |
    | `--skip-sherpa` | Диаризация не нужна, пропустить установку sherpa |
    | `--skip-models` | Модели уже скачаны/положены вручную |
    | `--allow-cpu` | Разрешить установку на машине без NVIDIA GPU (CPU-режим, в 10+ раз медленнее) |
    
    ### Проверка установки
    
    ```bash
    python scripts/verify.py             # быстрая проверка (5 сек)
    python scripts/verify.py --full      # с реальным прогоном на silent WAV (~1-2 мин)
    ```
    
    Что проверяется: venv-whisper и venv-sherpa с импортами, наличие моделей, ffmpeg в PATH. С `--full` дополнительно запускает транскрипцию tiny-модели на сгенерированном WAV.
    
    ### Для AI-агентов
    
    Установка длинная (~15-25 минут с загрузкой моделей). Перед запуском `setup.py` через subprocess/Bash увеличьте таймаут до 25-30 минут или используйте фоновый режим. См. раздел "Для агента" в `SKILL.md`.
    
    ## Использование
    
    ### Без диаризации
    
    ```bash
    python ~/.claude/skills/transcribe-audio-local/scripts/transcribe.py "путь/к/audio.mp3"
    ```
    
    Создает в `путь/к/Транскрипция/audio/`:
    - `audio - транскрипция.md` (markdown с таймкодами)
    - `audio - транскрипция.txt` (plain text)
    
    ### С диаризацией (разделение по спикерам)
    
    ```bash
    python ~/.claude/skills/transcribe-audio-local/scripts/transcribe.py "путь/к/audio.mp3" --diarize
    ```
    
    Дополнительно создает:
    - `audio - со спикерами.md` (реплики с `[SPEAKER_XX, MM:SS]`)
    
    ### Все опции
    
    ```
    positional:
      AudioPath              Путь к аудиофайлу (m4a/mp3/wav/ogg/flac/aac/wma/opus)
    
    options:
      --output-dir DIR       Каталог вывода (default: <каталог входа>/Транскрипция/<имя>/)
      --diarize              Включить диаризацию (параллельно с транскрипцией)
      --num-speakers N       Точное число спикеров (без авто-кластеризации)
      --threshold T          Порог кластеризации sherpa (default 0.5)
      --language LANG        Язык транскрипции (default ru)
      --model NAME           Модель faster-whisper (default mobiuslabsgmbh/faster-whisper-large-v3-turbo)
      --device DEV           cuda или cpu (default cuda)
      --compute-type CT      Точность (default float16)
      --keep-intermediate    Не удалять промежуточные JSON
    ```
    
    ## Поддерживаемые форматы
    
    Только аудио: `mp3`, `wav`, `ogg`, `m4a`, `flac`, `aac`, `wma`, `opus`.
    
    Видео не поддерживается. Если у вас видео - извлеките аудиодорожку через ffmpeg:
    
    ```bash
    ffmpeg -i video.mp4 -vn -acodec libmp3lame -ab 192k audio.mp3
    ```
    
    ## Архитектура
    
    ```
    скил/
    ├── SKILL.md                # описание для Claude Code
    ├── README.md               # эта инструкция
    ├── scripts/
    │   ├── transcribe.py       # orchestrator (CLI)
    │   ├── diarize_sherpa.py   # worker диаризации
    │   └── setup.py            # установщик зависимостей
    ├── venv-whisper/           # создается setup.py: faster-whisper + CUDA
    ├── venv-sherpa/            # создается setup.py: sherpa-onnx + onnxruntime-gpu
    └── models/                 # скачивается setup.py
        ├── sherpa-onnx-pyannote-segmentation-3-0/model.onnx
        └── 3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx
    ```
    
    Два venv нужны из-за конфликта CUDA DLL: `ctranslate2` (через faster-whisper) и `onnxruntime-gpu` (через sherpa-onnx) грузят несовместимые версии cuDNN. Изоляция через subprocess.
    
    Транскрипция и диаризация запускаются параллельно (если `--diarize`).
    
    ## Troubleshooting
    
    ### `ffmpeg не найден в PATH`
    
    Windows: скачайте с https://www.gyan.dev/ffmpeg/builds/, распакуйте, добавьте `bin/` в PATH.
    Linux: `sudo apt install ffmpeg`.
    
    ### `CUDA out of memory`
    
    Большая модель `large-v3-turbo` требует ~3 ГБ VRAM. Если не хватает - используйте меньшую:
    
    ```bash
    python scripts/transcribe.py audio.mp3 --model openai/whisper-small --compute-type int8_float16
    ```
    
    Или `--device cpu` (в 10+ раз медленнее).
    
    ### `sherpa_onnx не устанавливается с CUDA`
    
    Установщик пробует `pip install sherpa-onnx -f https://k2-fsa.github.io/sherpa/onnx/cuda.html`. Если не вышло:
    
    1. Скачайте wheel вручную с https://huggingface.co/csukuangfj2/sherpa-onnx-wheels (например, `sherpa_onnx-1.13.0+cuda12.cudnn9-cp312-cp312-win_amd64.whl` для Python 3.12 Windows).
    2. Установите: `<skill>/venv-sherpa/Scripts/pip.exe install <путь-к-wheel.whl>`.
    
    CPU-режим тоже работает: `pip install sherpa-onnx` без extra-index.
    
    ### `pyannote/speaker-diarization gated model`
    
    Этот скил **НЕ** использует gated модели pyannote. Использует только открытые ONNX:
    - pyannote-segmentation-3.0 (выложен k2-fsa в открытом доступе)
    - 3D-Speaker eres2net (Apache 2.0)
    
    HF_TOKEN не нужен.
    
    ### Модели не скачиваются
    
    URL из `setup.py`:
    - https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2
    - https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_base_200k_sv_zh-cn_16k-common.onnx
    
    Если они недоступны - скачайте вручную, положите в `models/`, повторите `setup.py --skip-models` (распаковка архива все равно произойдет).
    
    ### Транскрипция падает с `dll not found`
    
    CUDA DLL не подхватываются. Проверьте:
    1. Установлен ли CUDA 12 Toolkit?
    2. В `venv-whisper/Lib/site-packages/nvidia/` есть пакеты `cublas`, `cudnn`, `cuda_runtime`?
    3. Перезапустите терминал после установки CUDA.
    
    ## Лицензии
    
    - faster-whisper - MIT
    - sherpa-onnx - Apache 2.0
    - pyannote-segmentation-3.0 (ONNX) - MIT (k2-fsa)
    - 3D-Speaker eres2net - Apache 2.0
    
    ## Происхождение
    
    Адаптировано из скила `transcribe` (https://github.com/Desko77/claude-code-skills-1c) с урезанием поддержки видео и облачного Gemini-движка для self-contained передачи.
    
  • SKILL.md 7.2 KB
    ---
    name: transcribe-audio-local
    description: "Локальная транскрибация аудиофайлов без отправки в облако. Используй когда пользователь просит транскрибировать запись, расшифровать аудио, сделать конспект встречи, преобразовать речь в текст. Только для аудио (m4a/mp3/wav/ogg/flac/aac/wma/opus). Движок: faster-whisper CUDA + опц. диаризация sherpa-onnx GPU (CUDA, RTF ~0.24). Поддерживает разделение по спикерам."
    argument-hint: "<AudioPath> [--output-dir DIR] [--diarize] [--num-speakers N] [--language ru]"
    allowed-tools:
      - Bash
      - Read
      - Glob
    ---
    
    # /transcribe-audio-local - Локальная транскрибация аудио
    
    Локальный движок: `faster-whisper` (CUDA) + опц. диаризация `sherpa-onnx GPU` (CUDA) с моделями pyannote-segmentation-3.0 + 3D-Speaker eres2net. Нет затрат, ничего не уходит наружу.
    
    На RTX 5070 Ti Laptop: ~6-7 мин на 30 мин аудио (RTF ~0.24). Только аудио, видео не поддерживается.
    
    ## Установка (один раз)
    
    ```bash
    python scripts/setup.py
    ```
    
    Подробнее в `README.md`. После установки venv-whisper и venv-sherpa создаются рядом со скилом, модели качаются в `models/`.
    
    ## Для агента (Claude Code и подобные)
    
    **Workflow первого использования:**
    
    1. **Проверь, установлен ли скил** - есть ли файл `~/.claude/skills/transcribe-audio-local/venv-whisper/Scripts/python.exe` (на Linux: `venv-whisper/bin/python`). Если есть - переходи к шагу 4.
    
    2. **Если не установлен - запусти setup**. Установка занимает 10-20 минут (faster-whisper + ctranslate2-CUDA + sherpa-onnx + скачивание моделей). Дефолтный Bash-таймаут 2 минуты не подходит:
    
       - В Claude Code Bash tool используй `run_in_background: true` или `timeout: 1500000` (25 мин).
       - В других runtime - аналогичный длинный таймаут.
    
       ```bash
       python ~/.claude/skills/transcribe-audio-local/scripts/setup.py
       ```
    
       Что setup сделает сам:
       - Создаст venv-whisper и venv-sherpa
       - Если `ffmpeg` нет в PATH - поставит `static-ffmpeg` (pip-пакет с бинарниками) в venv-whisper. Без прав админа.
       - Скачает модели диаризации
       - Проверит версию драйверов NVIDIA и предупредит, если они старее 525.x
    
       Что setup НЕ может сделать сам (нужны действия пользователя):
       - Установить системные драйверы NVIDIA / CUDA Toolkit (нужны админ-права)
       - Поставить NVIDIA GPU в машину 😉
    
       Если у машины нет GPU - setup упадет с FAIL. Уточни у пользователя, согласен ли на CPU-режим (в 10+ раз медленнее), и перезапусти с `--allow-cpu`.
    
    3. **Проверь установку** smoke-тестом (1-2 минуты, с реальной транскрипцией):
    
       ```bash
       python ~/.claude/skills/transcribe-audio-local/scripts/verify.py --full
       ```
    
       Без `--full` (5 сек) - только проверка наличия venv'ов и моделей, без реального прогона.
    
    4. **Транскрибируй файл** (см. секцию "Инструкция" ниже). На CPU добавляй `--device cpu --compute-type int8`.
    
    ## Режимы
    
    ### Без диаризации (default)
    
    Выходные файлы:
    - `<имя> - транскрипция.md` - таймкоды + текст
    - `<имя> - транскрипция.txt` - plain text
    
    ### С диаризацией (`--diarize`)
    
    Дополнительно:
    - `<имя> - со спикерами.md` - реплики с метками `[SPEAKER_XX, MM:SS]`
    
    ## Аргументы
    
    | Параметр | Обязательный | По умолчанию | Описание |
    |----------|:---:|---|---|
    | AudioPath | да | - | Путь к аудиофайлу |
    | --output-dir | нет | `<каталог>/Транскрипция/<имя>/` | Каталог результатов |
    | --diarize | нет | выкл | Разделение по спикерам (sherpa-onnx) |
    | --num-speakers N | нет | автодетект | Точное число спикеров |
    | --threshold | нет | 0.5 | Порог кластеризации (меньше -> больше кластеров) |
    | --language | нет | ru | Язык транскрипции |
    | --model | нет | mobiuslabsgmbh/faster-whisper-large-v3-turbo | Модель faster-whisper |
    | --device | нет | cuda | cuda или cpu |
    | --compute-type | нет | float16 | Точность вычислений |
    
    ## Поддерживаемые форматы
    
    mp3, wav, ogg, m4a, flac, aac, wma, opus.
    
    ## Зависимости
    
    - **venv-whisper** (рядом со скилом): `faster-whisper`, `ctranslate2-CUDA`, `av`, nvidia-cublas/cudnn/cuda-runtime
    - **venv-sherpa** (рядом со скилом, для `--diarize`): `sherpa_onnx` (GPU CUDA сборка), `onnxruntime-gpu`, `soundfile`
    - `ffmpeg` в PATH
    - NVIDIA GPU + CUDA 12 + cuDNN 9 (для GPU режима, рекомендуется)
    
    ## Инструкция
    
    1. Получи `AudioPath` от пользователя. Проверь что расширение - аудио из поддерживаемого списка.
    
    2. Запусти транскрипцию:
    
    ```bash
    PYTHONUNBUFFERED=1 PYTHONIOENCODING=utf-8 \
      python ~/.claude/skills/transcribe-audio-local/scripts/transcribe.py \
      "<AudioPath>" [--output-dir "<OutputDir>"] [--diarize] [--num-speakers N]
    ```
    
    Время работы:
    - Без диаризации: ~1.5-2 мин на 27-мин файл (RTF ~0.07)
    - С диаризацией: ~10 мин на 27-мин файл (RTF ~0.4, параллельно)
    - Часовое аудио с диаризацией: ~25 мин
    
    3. После завершения покажи пользователю пути к файлам и прочитай начало транскрипции.
    
    **ВАЖНО:** `PYTHONUNBUFFERED=1` обязательно для прогресса.
    
    ## Ограничения
    
    - Только аудио, видео не поддерживается. Если нужно из видео - извлеките аудиодорожку через ffmpeg отдельно.
    - Требуется NVIDIA GPU с CUDA 12+ (CPU-режим работает, но в 10+ раз медленнее).
    - Точность таймкодов +/- несколько секунд.
    - Кириллические имена файлов обрабатываются скриптом.
    
    ## Установка диагностика
    
    Если транскрипция или диаризация падает - см. `README.md` секция "Troubleshooting".
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related