diff --git a/whisperx_diarization.py b/whisperx_diarization.py index 7dc554a..7e0ff64 100644 --- a/whisperx_diarization.py +++ b/whisperx_diarization.py @@ -2,106 +2,470 @@ # -*- coding: utf-8 -*- """ -🎙️ [ПЛАН] Распознавание с диаризацией (WhisperX через Docker) 🎙️ +🎙️ Распознавание речи с диаризацией через WhisperX (Docker) 🎙️ -Этот файл — заготовка для будущего скрипта, который будет выполнять диаризацию -(разделение речи по спикерам) с помощью WhisperX. +Этот Python-скрипт является оркестратором для пакетной обработки аудиофайлов +с использованием WhisperX. Он запускает транскрибацию и диаризацию (разделение +по спикерам) в изолированном Docker-контейнере, что решает проблемы +совместимости и обеспечивает стабильную работу на системах с GPU NVIDIA. -ВНИМАНИЕ: Прямая установка WhisperX в локальное Python-окружение вызывает -конфликты зависимостей, особенно с новыми видеокартами NVIDIA. -Поэтому реализация будет основана на **Docker-контейнерах**, что является -более надёжным и воспроизводимым решением. +Напоминание: для работы диаризации требуется токен Hugging Face (HF_TOKEN) +и принятие лицензий для моделей pyannote. ------------------------------------------------------------------------ - ПОЧЕМУ DOCKER? ------------------------------------------------------------------------ +Основные задачи: +- Изоляция зависимостей: Использует готовый Docker-образ, избавляя от ручной + установки PyTorch, CUDA и других сложных компонентов. +- Поддержка GPU и CPU: Автоматически задействует GPU NVIDIA для максимального + ускорения и может работать в режиме CPU. +- Пакетная обработка: Обрабатывает как отдельные файлы, так и все аудио + в указанной директории (mp3, wav, m4a и др.). +- Централизованный кеш: Сохраняет скачанные модели в общей папке `~/.whisperx/`, + экономя дисковое пространство и время при повторных запусках. +- Гибкая конфигурация: Управляет параметрами (модель, язык, токен) через + внешний файл `config.env`. +- Информативный вывод: Отображает детальный прогресс и итоговую статистику, + включая скорость обработки относительно реального времени. +- Встроенная проверка системы: Команда `--check` позволяет быстро убедиться, + что Docker, GPU и права доступа настроены корректно. -Проблема: -WhisperX требует определённых версий библиотек (например, PyTorch, torchaudio), -которые могут конфликтовать с последними драйверами NVIDIA или другими -пакетами в вашей системе. Это классическая "dependency hell". - -Решение: -NVIDIA предоставляет готовые Docker-контейнеры (через NGC и сообщество), -в которых уже настроено всё необходимое: CUDA, PyTorch и нужные библиотеки. -Это избавляет от ручной настройки и гарантирует, что окружение будет -работать "из коробки". Мы будем использовать готовый образ с WhisperX. - ------------------------------------------------------------------------ - ПЛАНИРУЕМЫЙ ФУНКЦИОНАЛ ------------------------------------------------------------------------ - -- Распознавание речи с помощью оптимизированной модели Whisper. -- Выравнивание временных меток на уровне слов для высокой точности. -- **Диаризация спикеров** для определения, кто и когда говорит. -- Сохранение результатов в форматах .txt, .srt, .json с разметкой спикеров - (например, "[SPEAKER_01]: Здравствуйте!"). - ------------------------------------------------------------------------ - ПОРЯДОК ИСПОЛЬЗОВАНИЯ (ПЛАН) ------------------------------------------------------------------------ - -Процесс будет включать следующие шаги: - -1. **Установка Docker и NVIDIA Container Toolkit.** - Это позволит Docker-контейнерам использовать вашу GPU. - -2. **Загрузка готового Docker-образа с WhisperX:** - ```bash - docker pull ghcr.io/jim60105/whisperx - ``` - -3. **Запуск контейнера для обработки аудио:** - Этот скрипт (`whisperx_diarization.py`) в будущем станет обёрткой, - автоматизирующей запуск Docker-контейнера для обработки ваших файлов. - Пример команды, которая будет выполняться "под капотом": - - ```bash - # Токен Hugging Face нужен для скачивания моделей диаризации - export HF_TOKEN=ваш_токен_доступа - - docker run --gpus all --rm \ - -e HF_TOKEN=$HF_TOKEN \ - -v /путь/к/вашим/аудио:/app/audio \ - -v /путь/к/результатам:/app/results \ - ghcr.io/jim60105/whisperx \ - --audio /app/audio/meeting.mp3 \ - --output_dir /app/results \ - --diarize \ - --model large-v3 - ``` +Порядок использования: +1. Отредактируйте файл `config.env`, указав ваш HF_TOKEN. +2. Поместите аудиофайлы в папку `audio/`. +3. Запустите скрипт: + python3 whisperx_diarization.py +4. Результаты (txt, srt, json) появятся в папке `results/`. Автор: Михаил Шардин https://shardin.name/ -Дата создания: 30.08.2025 -Версия: 0.2-alpha (План реализации через Docker) +Дата создания: 13.09.2025 +Версия: 2.1 Актуальная версия скрипта всегда здесь: https://github.com/empenoso/offline-audio-transcriber """ -# -# В будущей версии здесь будет код, который формирует и выполняет -# команду `docker run` на основе переданных аргументов (путь к файлам, модель и т.д.). -# -# import subprocess -# import sys -# ... -# +import os +import sys +import subprocess +import argparse +import logging +from pathlib import Path +from typing import List, Dict, Optional +import time +from datetime import datetime +import shutil +import itertools + +# Определяем базовую директорию относительно местоположения скрипта +SCRIPT_DIR = Path(__file__).parent.resolve() + +# Определяем глобальную директорию для кеша моделей в домашней папке пользователя +USER_CACHE_DIR = Path.home() / 'whisperx' + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(SCRIPT_DIR / 'whisperx_diarization.log', encoding='utf-8'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +class Colors: + """ANSI цвета для консольного вывода""" + RED = '\033[0;31m' + GREEN = '\033[0;32m' + YELLOW = '\033[1;33m' + CYAN = '\033[0;36m' + WHITE = '\033[1;37m' + NC = '\033[0m' + +class WhisperXDocker: + """Класс для работы с WhisperX через Docker""" + + def __init__(self, config_path: str = "config.env"): + self.work_dir = SCRIPT_DIR + # ИЗМЕНЕНИЕ: Ссылка на глобальный кеш + self.cache_dir = USER_CACHE_DIR + self.config_path = self.work_dir / config_path + self.config = self._load_config() + self.image_name = "ghcr.io/jim60105/whisperx:latest" + self.use_gpu = self.config.get('DEVICE') == 'cuda' + self._ensure_directories() + + def _load_config(self) -> Dict[str, str]: + """Загружает конфигурацию из файла .env""" + config = { + 'HF_TOKEN': '', 'WHISPER_MODEL': 'large-v3', 'LANGUAGE': 'ru', + 'BATCH_SIZE': '16', 'DEVICE': 'cuda', 'ENABLE_DIARIZATION': 'true', + 'MIN_SPEAKERS': '', 'MAX_SPEAKERS': '', 'COMPUTE_TYPE': 'float16', + 'VAD_METHOD': 'pyannote', 'CHUNK_SIZE': '30' + } + if self.config_path.exists(): + try: + with open(self.config_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + config[key.strip()] = value.strip().strip('"\'') + except Exception as e: + logger.warning(f"Ошибка загрузки конфигурации: {e}") + logger.info("Создание файла конфигурации по умолчанию...") + self._create_default_config() + else: + logger.info("Файл конфигурации не найден. Создание по умолчанию...") + self._create_default_config() + return config + + def _create_default_config(self): + """Создает файл конфигурации по умолчанию""" + self.config_path.parent.mkdir(parents=True, exist_ok=True) + default_config = """# Конфигурация WhisperX +# HuggingFace токен для диаризации (получите на https://huggingface.co/settings/tokens) +# ВАЖНО: Примите лицензии на: +# https://huggingface.co/pyannote/speaker-diarization-3.1 +# https://huggingface.co/pyannote/segmentation-3.0 +HF_TOKEN=your_token_here + +# Модель Whisper (tiny, base, small, medium, large-v1, large-v2, large-v3) +WHISPER_MODEL=large-v3 + +# Язык аудио (ru, en, auto для автоопределения) +LANGUAGE=ru + +# Размер батча (чем больше - тем быстрее, но больше памяти GPU) +BATCH_SIZE=16 + +# Устройство для вычислений (cuda или cpu) +DEVICE=cuda + +# Включить диаризацию (разделение по спикерам) +ENABLE_DIARIZATION=true + +# Минимальное количество спикеров (оставить пустым для автоопределения) +MIN_SPEAKERS= + +# Максимальное количество спикеров (оставить пустым для автоопределения) +MAX_SPEAKERS= + +# Тип вычислений (float16, float32, int8) +COMPUTE_TYPE=float16 + +# Метод VAD для обнаружения речи (pyannote, silero) +VAD_METHOD=pyannote + +# Размер чанков в секундах +CHUNK_SIZE=30 +""" + with open(self.config_path, 'w', encoding='utf-8') as f: + f.write(default_config) + logger.info(f"Создан файл конфигурации: {self.config_path}") + + def _ensure_directories(self): + """Создает необходимые рабочие директории""" + # Создаем локальные папки audio и results + for dir_name in ['audio', 'results']: + p = self.work_dir / dir_name + p.mkdir(parents=True, exist_ok=True) + + # Создаем глобальную папку для кеша моделей, если ее нет + self.cache_dir.mkdir(parents=True, exist_ok=True) + logger.info(f"Кеш моделей будет сохраняться в: {self.cache_dir}") + + + def _run_command(self, cmd: List[str], timeout: int = 45) -> Optional[subprocess.CompletedProcess]: + """Унифицированная функция для запуска внешних команд""" + try: + return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=True, encoding='utf-8') + except FileNotFoundError: + logger.error(f"Команда '{cmd[0]}' не найдена.") + except subprocess.TimeoutExpired: + logger.error(f"Команда '{' '.join(cmd)}' заняла слишком много времени.") + except subprocess.CalledProcessError as e: + logger.error(f"Ошибка выполнения команды. Код: {e.returncode}") + if e.stderr: + logger.error(f"Stderr: {e.stderr.strip()}") + return None + + def _check_gpu(self) -> bool: + """Проверяет доступность GPU через Docker""" + logger.info("Проверка доступа к GPU из Docker...") + cmd = [ + 'sudo', 'docker', 'run', '--rm', '--gpus', 'all', + 'nvidia/cuda:12.4.1-base-ubuntu22.04', + 'nvidia-smi', '--query-gpu=name', '--format=csv,noheader' + ] + result = self._run_command(cmd) + if result and result.stdout.strip(): + logger.info(f"✅ GPU успешно обнаружен: {result.stdout.strip()}") + return True + return False + + def _format_time(self, seconds: float) -> str: + """Форматирует секунды в читаемый вид (ч:м:с)""" + if seconds < 0: return "0.0с" + mins, secs = divmod(seconds, 60) + hours, mins = divmod(mins, 60) + if hours > 0: + return f"{int(hours)}ч {int(mins)}м {int(secs)}с" + elif mins > 0: + return f"{int(mins)}м {int(secs)}с" + else: + return f"{secs:.1f}с" + + def _get_audio_duration(self, file_path: Path) -> Optional[float]: + """Получает длительность аудиофайла через ffprobe""" + if not shutil.which('ffprobe'): + return None + cmd = [ + 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', str(file_path) + ] + result = self._run_command(cmd, timeout=15) + try: + return float(result.stdout.strip()) if result and result.stdout.strip() else None + except (ValueError, AttributeError): + return None + + def list_audio_files(self, directory: Optional[Path] = None) -> List[Path]: + """Находит все поддерживаемые аудиофайлы в директории""" + directory = directory or self.work_dir / "audio" + extensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg', '.aac', '.wma', '.mp4', '.mkv', '.avi'] + return sorted([p for p in directory.rglob('*') if p.suffix.lower() in extensions and p.is_file()]) + + def process_file(self, audio_file: Path, output_dir: Optional[Path] = None) -> bool: + """Обрабатывает один аудиофайл с помощью WhisperX в Docker""" + output_dir = output_dir or self.work_dir / "results" + file_output_dir = output_dir / audio_file.stem + file_output_dir.mkdir(exist_ok=True) + + cmd = ['sudo', 'docker', 'run', '--rm', '--user', f"{os.getuid()}:{os.getgid()}"] + + if self.use_gpu: + cmd.extend(['--gpus', 'all']) + + cmd.extend([ + '-v', f"{audio_file.parent.resolve()}:/audio:ro", + '-v', f"{file_output_dir.resolve()}:/results", + # ИЗМЕНЕНИЕ: Монтируем глобальную директорию кеша в /models внутри контейнера + '-v', f"{self.cache_dir.resolve()}:/models", + '--workdir', '/app', + # ИЗМЕНЕНИЕ: Все пути кеша внутри контейнера теперь указывают на смонтированный том /models + '-e', 'HOME=/models', + '-e', 'HF_HOME=/models/.cache/huggingface', + '-e', 'XDG_CACHE_HOME=/models/.cache', + '-e', 'TORCH_HOME=/models/.cache/torch', + ]) + + hf_token = self.config.get('HF_TOKEN', '').strip() + if hf_token and hf_token != 'your_token_here': + cmd.extend(['-e', f"HF_TOKEN={hf_token}"]) + logger.info("✅ HF_TOKEN передан в контейнер") + else: + logger.warning(f"{Colors.YELLOW}⚠️ HF_TOKEN не настроен! Диализация может не работать.{Colors.NC}") + + cmd.extend([self.image_name, 'whisperx']) + whisper_args = [ + '--output_dir', "/results", + '--model', self.config.get('WHISPER_MODEL', 'large-v3'), + '--language', self.config.get('LANGUAGE', 'ru'), + '--batch_size', self.config.get('BATCH_SIZE', '16'), + '--device', 'cuda' if self.use_gpu else 'cpu', + '--compute_type', self.config.get('COMPUTE_TYPE', 'float16'), + '--output_format', 'all', + '--verbose', 'False' + ] + + if (self.config.get('ENABLE_DIARIZATION', 'true').lower() == 'true' and hf_token and hf_token != 'your_token_here'): + whisper_args.extend(['--diarize', '--hf_token', hf_token]) + for key, name in [('MIN_SPEAKERS', '--min_speakers'), ('MAX_SPEAKERS', '--max_speakers')]: + value = self.config.get(key) + if value and value.isdigit() and int(value) > 0: + whisper_args.extend([name, value]) + elif self.config.get('ENABLE_DIARIZATION', 'true').lower() == 'true': + logger.warning("⚠️ Диаризация отключена - нет HF_TOKEN") + + whisper_args.append(f"/audio/{audio_file.name}") + cmd.extend(whisper_args) + + duration = self._get_audio_duration(audio_file) + logger.info(f"{Colors.CYAN}🎵 Обрабатываем: {audio_file.name}{Colors.NC}") + logger.info(f" 📊 Размер: {audio_file.stat().st_size / (1024*1024):.1f} МБ") + if duration: + logger.info(f" ⏱️ Длительность: {self._format_time(duration)}") + logger.info(f" 📁 Результаты: {file_output_dir}") + + start_time = time.time() + logger.info(f"{Colors.YELLOW}🚀 Запуск WhisperX...{Colors.NC}") + + try: + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, encoding='utf-8', bufsize=1, universal_newlines=True) + + spinner = itertools.cycle(['⠇', '⠏', '⠋', '⠙', '⠸', '⠴', '⠦', '⠇']) + stderr_lines = [] + + current_status = "Инициализация..." + sys.stdout.write(f" [PROGRESS] {next(spinner)} {current_status}\r") + + while process.poll() is None: + line = process.stderr.readline() + if line: + stderr_lines.append(line.strip()) + if "Performing VAD" in line or "voice activity detection" in line: + current_status = "1/4 Обнаружение речи (VAD)..." + elif "Performing transcription" in line: + current_status = "2/4 Транскрибация текста..." + elif "Performing alignment" in line: + current_status = "3/4 Выравнивание временных меток..." + elif "Performing diarization" in line: + current_status = f"4/4 Диарізація (може зайняти багато часу)..." + + sys.stdout.write(f" [PROGRESS] {next(spinner)} {current_status}\r") + sys.stdout.flush() + time.sleep(0.1) + + sys.stdout.write(" " * (len(current_status) + 20) + "\r") + sys.stdout.flush() + + process.wait() + + if process.returncode == 0: + processing_time = time.time() - start_time + logger.info(f"{Colors.GREEN}✅ Обработка завершена успешно!{Colors.NC}") + logger.info(f" ⏱️ Время обработки: {self._format_time(processing_time)}") + if duration and processing_time > 0: + speed_factor = duration / processing_time + logger.info(f" 🚀 Скорость: {speed_factor:.1f}x от реального времени") + + result_files = list(file_output_dir.glob('*')) + if result_files: + logger.info(f" 📄 Создано файлов: {len(result_files)}") + for rf in sorted(result_files): + logger.info(f" • {rf.name}") + return True + else: + logger.error(f"{Colors.RED}❌ Ошибка обработки файла {audio_file.name}{Colors.NC}") + logger.error(f" Код возврата Docker: {process.returncode}") + if stderr_lines: + logger.error(" Последние сообщения из лога контейнера:") + for line in stderr_lines[-10:]: + if line.strip(): + logger.error(f" [Docker ERR]: {line}") + return False + + except Exception as e: + logger.error(f"❌ Критическая ошибка при запуске Docker: {e}") + return False + + def process_directory(self, input_dir: Optional[Path] = None): + """Обрабатывает все аудиофайлы в директории""" + audio_files = self.list_audio_files(input_dir) + if not audio_files: + logger.warning(f"В директории {input_dir or self.work_dir / 'audio'} не найдено аудиофайлов.") + return + + logger.info(f"{Colors.CYAN}📁 Найдено {len(audio_files)} аудиофайлов{Colors.NC}") + stats = {"total": len(audio_files), "success": 0, "failed": 0} + start_time = time.time() + + for i, audio_file in enumerate(audio_files, 1): + logger.info(f"\n{Colors.WHITE}═══ Файл {i}/{stats['total']} ═══{Colors.NC}") + if self.process_file(audio_file): + stats["success"] += 1 + else: + stats["failed"] += 1 + + logger.info(f"\n{Colors.WHITE}{'═'*35}{Colors.NC}") + logger.info(f"{Colors.GREEN}🎯 ИТОГИ ОБРАБОТКИ{Colors.NC}") + logger.info(f"{Colors.WHITE}{'═'*35}{Colors.NC}") + logger.info(f"📊 Всего файлов: {stats['total']}") + logger.info(f"✅ Успешно: {stats['success']}") + logger.info(f"❌ С ошибками: {stats['failed']}") + total_time = time.time() - start_time + logger.info(f"⏱️ Общее время: {self._format_time(total_time)}") + if stats['total'] > 0: + logger.info(f"📈 Среднее время на файл: {self._format_time(total_time / stats['total'])}") + + def check_system(self) -> bool: + logger.info(f"{Colors.CYAN}🔍 Проверка системы...{Colors.NC}") + + if not self._run_command(['docker', '--version']): + logger.error("❌ Docker не найден. Установите Docker."); return False + logger.info("✅ Docker найден") + + if self.use_gpu: + if not self._check_gpu(): + logger.warning("⚠️ GPU недоступен через Docker, переключаемся на CPU.") + self.use_gpu = False; self.config['DEVICE'] = 'cpu' + else: logger.info("✅ GPU-ускорение активно") + else: logger.info("⚙️ Режим CPU активен (согласно config.env)") + + if not self._run_command(['sudo', 'docker', 'image', 'inspect', self.image_name]): + logger.error(f"❌ Образ WhisperX не найден. Выполните: sudo docker pull {self.image_name}"); return False + logger.info("✅ Образ WhisperX найден") + + hf_token = self.config.get('HF_TOKEN', '').strip() + if self.config.get('ENABLE_DIARIZATION', 'true').lower() == 'true': + if not hf_token or hf_token == 'your_token_here': + logger.error(f"❌ HF_TOKEN не настроен в {self.config_path}") + logger.info("💡 Получите токен на https://huggingface.co/settings/tokens и примите лицензии."); return False + else: logger.info("✅ HF_TOKEN настроен") + + if not shutil.which('ffprobe'): + logger.warning("⚠️ ffprobe не найден. Длительность аудио не будет отображаться. (sudo apt install ffmpeg)") + + #Проверяем права на запись в глобальный кеш, а не в локальную папку + try: + test_file = self.cache_dir / 'test_write.tmp' + test_file.touch(); test_file.unlink() + logger.info(f"✅ Есть права на запись в кеш {self.cache_dir}") + except Exception as e: + logger.error(f"❌ Нет прав на запись в директорию кеша {self.cache_dir}: {e}"); return False + + logger.info(f"{Colors.GREEN}✅ Система готова к работе{Colors.NC}") + return True + def main(): - """Информационное сообщение о статусе скрипта.""" - print("======================================================================") - print("🎙️ Скрипт для распознавания с диаризацией (План: WhisperX + Docker)") - print("======================================================================") - print("\n⚠️ ВНИМАНИЕ: Этот скрипт является заготовкой для будущей реализации.") - print("\nТекущий план — использовать Docker для решения проблем с зависимостями,") - print("что обеспечит стабильную работу на системах с GPU NVIDIA.") - print("\nПроцесс будет выглядеть так:") - print(" 1. Вы запускаете этот скрипт с указанием папки аудио.") - print(" 2. Скрипт автоматически запускает Docker-контейнер с WhisperX.") - print(" 3. Результаты с разметкой по спикерам сохраняются в указанную папку.") - print("\nСледите за обновлениями в репозитории!") - print("[ссылка на ваш GitHub репозиторий]") - print() + """Главная функция-обработчик CLI""" + parser = argparse.ArgumentParser(description='🎙️ Распознавание речи с диаризацией через WhisperX (DOCKER)') + parser.add_argument('-f', '--file', type=str, help='Путь к конкретному аудиофайлу для обработки') + parser.add_argument('-d', '--directory', type=str, help='Путь к директории с аудиофайлами') + parser.add_argument('--check', action='store_true', help='Проверить готовность системы к работе') + parser.add_argument('--config', type=str, default="config.env", help='Путь к файлу конфигурации относительно скрипта') + parser.add_argument('--debug', action='store_true', help='Включить отладочный режим') + args = parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + print(f"{Colors.CYAN}{'═'*70}\n🎙️ WHISPERX ДИАРИЗАЦИЯ РЕЧИ (DOCKER)\n{'═'*70}{Colors.NC}") + print(f"Автор скрипта: Михаил Шардин | https://shardin.name/\nДата: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n") + + try: + whisperx = WhisperXDocker(config_path=args.config) + + if args.check: + whisperx.check_system(); return + + if not whisperx.check_system(): + logger.error("Система не готова. Исправьте ошибки и повторите."); sys.exit(1) + + if args.file: + file_path = Path(args.file).expanduser().resolve() + if not file_path.exists(): + logger.error(f"Файл не найден: {file_path}"); sys.exit(1) + whisperx.process_file(file_path) + else: + input_dir = Path(args.directory).expanduser() if args.directory else None + whisperx.process_directory(input_dir=input_dir) + + except KeyboardInterrupt: + logger.info(f"\n{Colors.YELLOW}⏹️ Работа прервана пользователем{Colors.NC}"); sys.exit(130) + except Exception as e: + logger.error(f"Критическая непредвиденная ошибка: {e}", exc_info=True); sys.exit(1) if __name__ == "__main__": main() \ No newline at end of file diff --git a/whisperx_diarization_setup.sh b/whisperx_diarization_setup.sh index e69de29..56de1d7 100644 --- a/whisperx_diarization_setup.sh +++ b/whisperx_diarization_setup.sh @@ -0,0 +1,332 @@ +#!/usr/bin/env bash + +# 🛠️ Скрипт установки WhisperX с диаризацией (Docker + NVIDIA) 🛠️ +# +# Этот Shell-скрипт полностью автоматизирует подготовку системы Ubuntu +# (20.04/22.04/24.04) для работы с WhisperX через Docker с ускорением на GPU +# от NVIDIA. Он устанавливает все компоненты, настраивает их и создает +# готовое к работе окружение. +# +# Напоминание: скрипт следует официальным инструкциям NVIDIA и Docker +# для обеспечения максимальной надежности и совместимости. +# +# Основные задачи: +# - Проверка системы: Определяет дистрибутив и наличие драйверов NVIDIA. +# - Установка Docker: Устанавливает Docker Engine и добавляет пользователя +# в нужную группу для работы без `sudo`. +# - Установка NVIDIA Container Toolkit: Позволяет Docker-контейнерам +# напрямую использовать ресурсы GPU. +# - Тестирование GPU в Docker: Запускает тестовый контейнер для проверки +# корректности настройки. +# - Загрузка образа WhisperX: Скачивает готовый Docker-образ со всеми +# зависимостями. +# - Создание рабочего пространства: +# - Локальные папки `audio/` и `results/`. +# - Глобальный кеш для моделей в `~/whisperx/` для экономии места. +# - Файл конфигурации `config.env` с настройками по умолчанию. +# - Управление правами: Назначает корректные права на папки, чтобы избежать +# конфликтов доступа у Docker-контейнера. +# +# Порядок использования: +# 1. Сделайте скрипт исполняемым: chmod +x whisperx_diarization_setup.sh +# 2. Запустите его: ./whisperx_diarization_setup.sh +# 3. После завершения может потребоваться перезагрузка системы. + +# Следить за состоянием GPU: $ watch -n 5 nvidia-smi +# +# Автор: Михаил Шардин https://shardin.name/ +# Дата создания: 14.09.2025 +# Версия: 2.2 +# +# Актуальная версия скрипта всегда здесь: https://github.com/empenoso/offline-audio-transcriber +# +# =================================================================== + +## Строгий режим для bash. Прерывает выполнение при любой ошибке. +set -euo pipefail + +# Цвета для вывода +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Функции логирования (используем printf для большей надежности) +log() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } +success() { printf "${GREEN}[SUCCESS]${NC} %s\n" "$1"; } +warning() { printf "${YELLOW}[WARNING]${NC} %s\n" "$1"; } +error() { printf "${RED}[ERROR]${NC} %s\n" "$1" >&2; } # Ошибки выводим в stderr + +# --- Функции проверки системы --- + +check_distro() { + if ! [ -f /etc/os-release ]; then + error "Не удалось определить операционную систему." + exit 1 + fi + . /etc/os-release + if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then + error "Этот скрипт предназначен для Ubuntu/Debian. Обнаружено: $PRETTY_NAME" + exit 1 + fi + success "Обнаружена совместимая система: $PRETTY_NAME" +} + +check_gpu() { + log "Проверка наличия NVIDIA GPU и драйверов..." + if ! command -v nvidia-smi &> /dev/null; then + error "Команда 'nvidia-smi' не найдена. Установите драйверы NVIDIA." + printf "Рекомендуемые команды:\n" + printf " sudo ubuntu-drivers autoinstall\n" + printf " sudo reboot\n" + exit 1 + fi + if ! nvidia-smi &> /dev/null; then + error "'nvidia-smi' не отвечает. Возможно, требуется перезагрузка после установки драйверов." + exit 1 + fi + GPU_INFO=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits) + DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits) + success "Найден GPU: $GPU_INFO" + log "Версия драйвера: $DRIVER_VERSION" +} + +# --- Функции установки компонентов --- + +install_docker() { + if command -v docker &> /dev/null && docker --version &> /dev/null; then + success "Docker уже установлен: $(docker --version)" + else + log "Установка Docker Engine..." + sudo apt-get update + sudo apt-get install -y ca-certificates curl + sudo install -m 0755 -d /etc/apt/keyrings + sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc + sudo chmod a+r /etc/apt/keyrings/docker.asc + + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + + sudo apt-get update + sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + success "Docker успешно установлен." + fi + + # Добавление пользователя в группу docker, если еще не там + if ! groups "$USER" | grep -q '\bdocker\b'; then + log "Добавление пользователя $USER в группу docker..." + sudo usmod -aG docker "$USER" + warning "Для применения изменений группы docker требуется перезагрузка или перелогин." + log "Вы можете выполнить 'sudo reboot' после завершения установки." + fi +} + +install_nvidia_toolkit() { + log "Установка NVIDIA Container Toolkit..." + + if command -v nvidia-ctk &> /dev/null; then + success "NVIDIA Container Toolkit уже установлен." + else + log "Настройка репозитория NVIDIA..." + # Этот метод автоматически определяет версию дистрибутива (ubuntu22.04, ubuntu24.04 и т.д.) + curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ + && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ + sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ + sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null + + log "Обновление списка пакетов и установка..." + sudo apt-get update + sudo apt-get install -y nvidia-container-toolkit + success "NVIDIA Container Toolkit успешно установлен." + fi + + log "Конфигурирование Docker для работы с NVIDIA GPU..." + sudo nvidia-ctk runtime configure --runtime=docker + + log "Перезапуск Docker daemon для применения конфигурации..." + sudo systemctl restart docker + sleep 3 # Даем демону время на перезапуск + success "Docker настроен для работы с NVIDIA GPU." +} + +test_docker_gpu() { + log "Тестирование Docker с поддержкой GPU..." + if ! sudo docker run --rm hello-world > /dev/null 2>&1; then + error "Базовый Docker не работает. Проверьте 'systemctl status docker'" + exit 1 + fi + success "Базовый тест Docker пройден." + + log "Проверка доступа к GPU из контейнера..." + local cuda_image="nvidia/cuda:12.4.1-base-ubuntu22.04" # Используем актуальный образ + log "Используем тестовый образ: $cuda_image" + + if ! sudo docker pull "$cuda_image" > /dev/null; then + warning "Не удалось загрузить тестовый образ $cuda_image. Пропускаем тест GPU." + return 1 + fi + + # Пытаемся выполнить nvidia-smi внутри контейнера + local gpu_name_in_container + gpu_name_in_container=$(sudo docker run --rm --gpus all "$cuda_image" nvidia-smi --query-gpu=name --format=csv,noheader) + + if [[ -n "$gpu_name_in_container" ]]; then + success "🎉 GPU успешно обнаружен в Docker контейнере: $gpu_name_in_container" + return 0 # Успех + else + error "Не удалось получить доступ к GPU из Docker контейнера." + warning "WhisperX будет работать на CPU (значительно медленнее)." + log "Возможные причины:" + log " - Конфликт версий драйвера, toolkit или docker." + log " - Необходимо перезагрузить систему: 'sudo reboot'" + return 1 # Неудача + fi +} + +pull_whisperx_image() { + log "Загрузка Docker образа WhisperX..." + local whisperx_image="ghcr.io/jim60105/whisperx:latest" + + if sudo docker pull "$whisperx_image"; then + success "Образ $whisperx_image загружен успешно." + local image_size_bytes + image_size_bytes=$(sudo docker image inspect "$whisperx_image" --format='{{.Size}}') + local image_size_gb + image_size_gb=$(awk "BEGIN {printf \"%.2f\", $image_size_bytes/1024/1024/1024}") + log "Размер образа: ~${image_size_gb} GB" + else + error "Не удалось загрузить образ WhisperX: $whisperx_image" + exit 1 + fi +} + +setup_workspace() { + log "Создание рабочих директорий и конфигурации..." + local base_dir="." + local cache_dir="$HOME/whisperx" + + mkdir -p "$base_dir"/{audio,results} + mkdir -p "$cache_dir" + + log "Установка прав 777 на папки..." + chmod -R 777 "$base_dir"/audio "$base_dir"/results "$cache_dir" + + success "Созданы директории:" + printf " 📂 %s/audio - для входных аудиофайлов\n" "$(pwd)" + printf " 📂 %s/results - для результатов\n" "$(pwd)" + printf " 🧠 %s - для кеширования моделей\n" "$cache_dir" + + local config_file="$base_dir/config.env" + if [ -f "$config_file" ]; then + # ИЗМЕНЕНИЕ: Исправлена переменная $config.env на $config_file + success "Конфигурационный файл $config_file уже существует. Пропускаем создание." + else + log "Создание конфигурационного файла: $config_file" + cat > "$config_file" << 'EOF' +# Конфигурация WhisperX +# HuggingFace токен для диаризации (получите на https://huggingface.co/settings/tokens) +# ВАЖНО: Примите лицензии на: +# https://huggingface.co/pyannote/speaker-diarization-3.1 +# https://huggingface.co/pyannote/segmentation-3.0 +HF_TOKEN=your_token_here + +# Модель Whisper (tiny, base, small, medium, large-v1, large-v2, large-v3) +WHISPER_MODEL=large-v3 + +# Язык аудио (ru, en, auto для автоопределения) +LANGUAGE=ru + +# Размер батча (чем больше - тем быстрее, но больше памяти GPU) +BATCH_SIZE=16 + +# Устройство для вычислений (cuda или cpu) +DEVICE=cuda + +# Включить диаризацию (разделение по спикерам) +ENABLE_DIARIZATION=true + +# Минимальное количество спикеров (оставить пустым для автоопределения) +MIN_SPEAKERS= + +# Максимальное количество спикеров (оставить пустым для автоопределения) +MAX_SPEAKERS= + +# Тип вычислений (float16, float32, int8) +COMPUTE_TYPE=float16 + +# Метод VAD для обнаружения речи (pyannote, silero) +VAD_METHOD=pyannote + +# Размер чанков в секундах +CHUNK_SIZE=30 +EOF + success "Конфигурационный файл создан: $config_file" + fi +} + +final_check() { + log "Выполнение финальной проверки установки..." + + if ! command -v docker &>/dev/null; then error "Docker не найден!"; exit 1; fi + if ! sudo docker image inspect "ghcr.io/jim60105/whisperx:latest" &>/dev/null; then error "Образ WhisperX не найден!"; exit 1; fi + if ! [ -d "./audio" ]; then error "Рабочая директория не найдена!"; exit 1; fi + if ! [ -d "$HOME/whisperx" ]; then error "Директория кеша моделей не найдена!"; exit 1; fi + + success "Все компоненты установлены и готовы к работе!" +} + +show_usage() { + printf "\n=====================================================================\n" + printf "🎉 УСТАНОВКА ЗАВЕРШЕНА УСПЕШНО!\n" + printf "=====================================================================\n\n" + + printf "🔥 ВАЖНЫЕ СЛЕДУЮЩИЕ ШАГИ:\n\n" + + printf "1. 🔑 ${YELLOW}Отредактируйте токен Hugging Face${NC} для диаризации:\n" + printf " - Откройте файл: nano ./config.env\n" + printf " - Замените 'your_token_here' на ваш токен с https://huggingface.co/settings/tokens\n\n" + + printf "2. 🔄 ${YELLOW}Перезагрузите систему${NC}, если вы не были в группе docker:\n" + printf " sudo reboot\n\n" + + printf "После перезагрузки:\n" + printf "3. 📁 Скопируйте ваши аудиофайлы в ./audio/\n" + printf "4. 🚀 Запустите обработку: python3 whisperx_diarization.py\n\n" + + printf "Рабочие директории:\n" + printf " 📂 ./audio - Входные файлы (*.wav, *.mp3, *.m4a)\n" + printf " 📂 ./results - Результаты распознавания\n" + printf " 🧠 ~/whisperx/ - Кеш моделей (общий для всех проектов)\n" + printf " ⚙️ ./config.env - Настройки\n\n" + + printf "=====================================================================\n" +} + +# --- Основная функция --- +main() { + printf "=====================================================================\n" + printf "🎙️ УСТАНОВКА WHISPERX ДЛЯ ДИАРИЗАЦИИ РЕЧИ (DOCKER + NVIDIA)\n" + printf "=====================================================================\n\n" + + check_distro + check_gpu + install_docker + install_nvidia_toolkit + + if test_docker_gpu; then + log "Тест GPU пройден. WhisperX будет использовать видеокарту." + else + warning "Тест GPU не пройден. Проверьте настройки в './config.env' и установите DEVICE=cpu, если GPU не заработает." + fi + + pull_whisperx_image + setup_workspace + final_check + show_usage +} + +# Запуск основной функции +main \ No newline at end of file