mirror of
https://github.com/empenoso/offline-audio-transcriber.git
synced 2026-04-19 00:20:29 +05:00
Обновление. Распознаёт отлично
This commit is contained in:
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user