├── LICENSE ├── whisper_transcribe_setup.sh ├── README.md ├── whisper_transcribe.py ├── whisperx_diarization_setup.sh └── whisperx_diarization.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mikhail Shardin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /whisper_transcribe_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 🛠️ Скрипт для настройки окружения OpenAI Whisper на Ubuntu 🛠️ 4 | # 5 | # Этот Shell-скрипт автоматизирует полную установку и настройку программного 6 | # окружения, необходимого для работы системы распознавания речи OpenAI Whisper 7 | # с использованием GPU от NVIDIA. 8 | # 9 | # Напоминание: Скрипт ориентирован на Ubuntu и может потребовать адаптации 10 | # для других дистрибутивов Linux. 11 | # 12 | # Основные задачи: 13 | # - Обновление системы и установка базовых утилит (python3-venv, ffmpeg). 14 | # - Проверка и установка драйверов NVIDIA. 15 | # - Проверка и установка CUDA Toolkit для вычислений на GPU. 16 | # - Создание изолированного Python-окружения (.venv) для избежания конфликтов. 17 | # - Установка PyTorch с учетом архитектуры GPU (стабильная или nightly версия). 18 | # - Установка библиотеки openai-whisper и других зависимостей. 19 | # - Запуск финального теста для проверки совместимости PyTorch и GPU. 20 | # 21 | # Порядок использования: 22 | # 1. Сделайте скрипт исполняемым: chmod +x setup_whisper.sh 23 | # 2. Запустите его: ./setup_whisper.sh 24 | # 3. В случае установки драйверов NVIDIA может потребоваться перезагрузка. 25 | 26 | # Следить за состоянием GPU: $ watch -n 5 nvidia-smi 27 | # 28 | # Автор: Михаил Шардин https://shardin.name/ 29 | # Дата создания: 29.08.2025 30 | # Версия: 1.1 31 | # 32 | # Актуальная версия скрипта всегда здесь: https://github.com/empenoso/offline-audio-transcriber 33 | # 34 | 35 | echo "🚀 Установка окружения для OpenAI Whisper" 36 | echo "=========================================" 37 | 38 | # Проверка Ubuntu версии 39 | echo "📋 Информация о системе:" 40 | lsb_release -a 41 | echo "" 42 | 43 | # Обновление системы 44 | echo "🔄 Обновление пакетов..." 45 | sudo apt update && sudo apt upgrade -y 46 | 47 | # Установка Python и pip 48 | echo "🐍 Установка Python и зависимостей..." 49 | sudo apt install -y python3 python3-pip python3-venv python3-dev 50 | 51 | # Установка системных зависимостей для аудио 52 | echo "🎵 Установка библиотек для работы с аудио..." 53 | sudo apt install -y ffmpeg libsndfile1 portaudio19-dev 54 | 55 | # Проверка NVIDIA драйверов 56 | echo "🎮 Проверка NVIDIA драйверов..." 57 | if nvidia-smi &> /dev/null; then 58 | echo "✅ NVIDIA драйверы установлены" 59 | nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits 60 | else 61 | echo "⚠️ NVIDIA драйверы не найдены. Установка..." 62 | sudo apt install -y nvidia-driver-575 nvidia-dkms-575 63 | echo "🔄 После установки драйверов требуется перезагрузка!" 64 | echo "Запустите: sudo reboot" 65 | fi 66 | 67 | # Установка CUDA toolkit (если нужно) 68 | echo "🔧 Проверка CUDA..." 69 | if nvcc --version &> /dev/null; then 70 | echo "✅ CUDA toolkit уже установлен" 71 | nvcc --version 72 | else 73 | echo "📦 Установка CUDA toolkit..." 74 | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb 75 | sudo dpkg -i cuda-keyring_1.1-1_all.deb 76 | sudo apt update 77 | sudo apt-get -y install cuda-toolkit-13-0 78 | fi 79 | 80 | # Создание виртуального окружения 81 | echo "🏠 Создание виртуального окружения..." 82 | python3 -m venv .venv 83 | source .venv/bin/activate 84 | 85 | # Обновление pip 86 | echo "⬆️ Обновление pip..." 87 | pip install --upgrade pip 88 | 89 | # Определение архитектуры GPU для выбора совместимой версии PyTorch 90 | echo "🔥 Установка PyTorch с поддержкой RTX 5060 Ti..." 91 | 92 | # Проверяем архитектуру GPU 93 | if nvidia-smi &> /dev/null; then 94 | GPU_INFO=$(nvidia-smi --query-gpu=name --format=csv,noheader,nounits) 95 | echo "🎮 Обнаружен GPU: $GPU_INFO" 96 | 97 | # Для RTX 5060 Ti (Ada Lovelace) нужна nightly версия PyTorch 98 | if echo "$GPU_INFO" | grep -q "RTX 5060 Ti\|RTX 40\|RTX 50"; then 99 | echo "🚀 Установка PyTorch nightly для поддержки новых GPU..." 100 | pip install --pre torch torchvision torchaudio --index-url https://download.pytorch.org/whl/nightly/cu129 101 | else 102 | echo "📦 Установка стабильной версии PyTorch..." 103 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 104 | fi 105 | else 106 | echo "📦 GPU не обнаружен, установка CPU версии PyTorch..." 107 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu 108 | fi 109 | 110 | # Установка OpenAI Whisper 111 | echo "🎙️ Установка OpenAI Whisper..." 112 | pip install openai-whisper 113 | 114 | # Дополнительные полезные библиотеки 115 | echo "📚 Установка дополнительных библиотек..." 116 | pip install numpy scipy librosa soundfile pydub 117 | 118 | # Тест установки с проверкой совместимости GPU 119 | echo "🧪 Тестирование установки..." 120 | python3 -c " 121 | import torch 122 | import whisper 123 | print(f'PyTorch версия: {torch.__version__}') 124 | print(f'CUDA доступна: {torch.cuda.is_available()}') 125 | 126 | if torch.cuda.is_available(): 127 | try: 128 | gpu_name = torch.cuda.get_device_name(0) 129 | print(f'GPU: {gpu_name}') 130 | print(f'CUDA версия: {torch.version.cuda}') 131 | print(f'GPU устройств: {torch.cuda.device_count()}') 132 | 133 | # Тест совместимости 134 | test_tensor = torch.zeros(10, 10).cuda() 135 | result = test_tensor + 1 136 | print('✅ GPU совместим с PyTorch') 137 | 138 | except Exception as e: 139 | print(f'⚠️ GPU несовместим: {e}') 140 | print('🔄 Будет использоваться CPU режим') 141 | else: 142 | print('💻 Будет использоваться CPU') 143 | 144 | print('✅ Whisper импортирован успешно') 145 | " 146 | 147 | echo "" 148 | echo "🎉 Установка завершена!" 149 | echo "=========================================" 150 | echo "Для активации окружения используйте:" 151 | echo "source .venv/bin/activate" 152 | echo "" 153 | echo "Для запуска скрипта:" 154 | echo "python3 whisper_transcribe.py [директория] [модель] [выходная_папка]" 155 | echo "" 156 | echo "Примеры:" 157 | echo "python3 whisper_transcribe.py ./audio" 158 | echo "python3 whisper_transcribe.py ./audio large ./results" 159 | echo "" 160 | echo "Доступные модели (от быстрой к точной):" 161 | echo "tiny, base, small, medium, large" 162 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Локальная система распознавания и диаризации речи (Whisper & WhisperX) 2 | 3 | Этот репозиторий содержит набор скриптов для создания полностью автономной системы расшифровки аудиозаписей на вашем компьютере. Решение позволяет не только превращать речь в текст, но и автоматически разделять его по говорящим (диаризация), что идеально подходит для анализа лекций, интервью и совещаний. 4 | 5 | В основе лежат мощные open-source модели **OpenAI Whisper** и **WhisperX**, что гарантирует высокую точность и полную приватность ваших данных. 6 | 7 | ## ✨ Ключевые возможности 8 | 9 | * **Два режима работы:** 10 | 1. **Простая транскрибация (Whisper):** быстрое получение сплошного текста из аудио. Идеально для монологов, лекций. 11 | 2. **Транскрибация с диаризацией (WhisperX):** получение структурированного диалога с разметкой по спикерам (`SPEAKER_01`, `SPEAKER_02`). Отлично подходит для совещаний, интервью. 12 | * **Приватность:** все ваши аудиозаписи и текстовые расшифровки остаются на вашем компьютере. Никакие данные не передаются третьим лицам. 13 | * **Бесплатно и Open Source:** проект построен на бесплатных инструментах. Вы не платите за подписку или поминутное распознавание. 14 | * **Высокая точность:** используется модель `large-v3` от OpenAI, которая является одной из самых точных для распознавания речи, включая русский язык. 15 | * **Ускорение на GPU:** решение оптимизировано для работы с видеокартами NVIDIA, что многократно ускоряет обработку. 16 | * **Простая установка:** для каждого режима предусмотрены bash-скрипты, которые автоматизируют настройку окружения. 17 | 18 | ## 🤔 Какой метод выбрать? 19 | 20 | | Критерий | Метод 1: Простая транскрибация (Whisper) | Метод 2: Диа-ри-за-ци-я (WhisperX + Docker) | 21 | | :--- | :--- | :--- | 22 | | **Задача** | Получить сплошной текст из аудио (лекция, доклад) | Разделить речь на реплики разных спикеров (совещание) | 23 | | **Результат** | `...текст одного спикера текст другого...` | `[время] SPEAKER_01: текст...`
`[время] SPEAKER_02: текст...` | 24 | | **Технологии** | Python, PyTorch, Whisper | Docker, NVIDIA Container Toolkit, WhisperX | 25 | | **Сложность** | Простая настройка в виртуальном окружении Python | Требует Docker и токен Hugging Face. Установка автоматизирована. | 26 | | **Когда использовать** | Для быстрой расшифровки монологов, лекций, личных заметок. | Для анализа диалогов, интервью, совещаний, создания протоколов. | 27 | 28 | --- 29 | 30 | ## 🚀 Метод 1: простая транскрибация (Whisper) 31 | 32 | Этот метод идеально подходит для быстрой расшифровки аудиозаписей без необходимости разделять речь по спикерам. 33 | 34 | ### Шаг 1: Подготовка окружения 35 | 36 | Для автоматической установки всех необходимых компонентов (Python, FFmpeg, NVIDIA Drivers, CUDA, PyTorch) используется специальный bash-скрипт. 37 | 38 | 1. **Клонируйте репозиторий:** 39 | ```bash 40 | git clone https://github.com/empenoso/offline-audio-transcriber 41 | cd offline-audio-transcriber 42 | ``` 43 | 44 | 2. **Сделайте скрипт установки исполняемым:** 45 | ```bash 46 | chmod +x whisper_transcribe_setup.sh 47 | ``` 48 | 49 | 3. **Запустите скрипт:** 50 | ```bash 51 | ./whisper_transcribe_setup.sh 52 | ``` 53 | Скрипт обновит систему, установит драйверы NVIDIA (если необходимо), CUDA, создаст виртуальное окружение `.venv` и установит все Python-библиотеки. 54 | 55 | > **Примечание:** Если скрипт установит драйверы NVIDIA, может потребоваться перезагрузка компьютера. 56 | 57 | ### Шаг 2: Запуск распознавания 58 | 59 | 1. **Поместите ваши аудиофайлы** (поддерживаются `.mp3`, `.wav`, `.m4a`) в любую папку, например, `audio/`. 60 | 61 | 2. **Активируйте виртуальное окружение:** 62 | ```bash 63 | source .venv/bin/activate 64 | ``` 65 | 66 | 3. **Запустите скрипт `whisper_transcribe.py`:** 67 | 68 | * **Простой запуск** (поиск аудио в текущей папке): 69 | ```bash 70 | python3 whisper_transcribe.py 71 | ``` 72 | 73 | * **Указание параметров вручную** (папка с аудио, модель, папка для результатов): 74 | ```bash 75 | python3 whisper_transcribe.py ./audio large ./results 76 | ``` 77 | 78 | ### Шаг 3: Анализ результатов 79 | 80 | После выполнения скрипта в выходной папке вы найдете: 81 | * **`.txt` файл** для каждой аудиозаписи с полным текстом. 82 | * **`.srt` файл** для каждой аудиозаписи с субтитрами и таймкодами. 83 | * **`all_transcripts.txt`** — один файл, содержащий все расшифровки подряд. 84 | 85 | --- 86 | 87 | ## 🚀 Метод 2: транскрибация с диаризацией (WhisperX + Docker) 88 | 89 | Этот метод использует Docker для решения проблем с зависимостями и позволяет не только транскрибировать аудио, но и определять, кто из спикеров говорил. 90 | 91 | ### Подготовка 92 | 93 | 1. **Docker и NVIDIA GPU:** для работы требуется установленный Docker и видеокарта NVIDIA с актуальными драйверами. 94 | 2. **Токен Hugging Face:** для диаризации необходим токен доступа. 95 | * Получите его в [настройках профиля Hugging Face](https://huggingface.co/settings/tokens). 96 | * Примите условия использования моделей [pyannote/speaker-diarization-3.1](https://huggingface.co/pyannote/speaker-diarization-3.1) и [pyannote/segmentation-3.0](https://huggingface.co/pyannote/segmentation-3.0). 97 | 98 | ### Шаг 1: Установка и настройка 99 | 100 | Специальный скрипт автоматизирует установку Docker, NVIDIA Container Toolkit и настройку рабочего пространства. 101 | 102 | 1. **Сделайте скрипт исполняемым:** 103 | ```bash 104 | chmod +x whisperx_diarization_setup.sh 105 | ``` 106 | 107 | 2. **Запустите установщик:** 108 | ```bash 109 | ./whisperx_diarization_setup.sh 110 | ``` 111 | Скрипт установит и настроит Docker, скачает необходимый образ WhisperX и создаст структуру папок (`audio`, `results`) и файл конфигурации `config.env`. 112 | 113 | 3. **Настройте `config.env`:** 114 | Откройте файл `config.env` и вставьте ваш токен Hugging Face: 115 | ```ini 116 | HF_TOKEN=your_token_here 117 | ``` 118 | 119 | 4. **Перезагрузка:** После установки может потребоваться перезагрузка, чтобы применились права для группы `docker`. 120 | ```bash 121 | sudo reboot 122 | ``` 123 | 124 | ### Шаг 2: Запуск диаризации 125 | 126 | 1. **Поместите аудиофайлы** в папку `audio/`. 127 | 128 | 2. **Запустите скрипт-оркестратор:** 129 | * Для проверки системы: 130 | ```bash 131 | python3 whisperx_diarization.py --check 132 | ``` 133 | * Для обработки всех файлов в папке `audio/`: 134 | ```bash 135 | python3 whisperx_diarization.py 136 | ``` 137 | 138 | ### Шаг 3: Анализ результатов 139 | 140 | Результаты для каждого аудиофайла будут сохранены в отдельной подпапке внутри `results/`. Вы получите структурированный протокол встречи. 141 | 142 | **Было (обычный Whisper):** 143 | > ...да, я согласен с этим подходом но нужно учесть риски которые мы не обсудили например финансовую сторону вопроса и как это повлияет на сроки я думаю нам стоит вернуться к этому на следующей неделе... 144 | 145 | **Стало (WhisperX с диаризацией):** 146 | 147 | > [00:01:15.520 --> 00:01:19.880] SPEAKER_01: Да, я согласен с этим подходом, но нужно учесть риски, которые мы не обсудили. 148 | 149 | > [00:01:20.100 --> 00:01:22.740] SPEAKER_02: Например, финансовую сторону вопроса и как это повлияет на сроки? 150 | 151 | > [00:01:23.020 --> 00:01:25.900] SPEAKER_01: Именно. Я думаю, нам стоит вернуться к этому на следующей неделе. 152 | 153 | ## ❓ Где получить помощь? 154 | 155 | Если у вас возникли проблемы с установкой, запуском или есть предложения по улучшению, пожалуйста, создайте `Issue` в этом репозитории. 156 | 157 | ## 👤 Автор и поддержка 158 | 159 | **Автор:** Михаил Шардин 160 | 161 | **Онлайн-визитка:** [shardin.name](https://shardin.name/?utm_source=github) 162 | 163 | **Telegram-канал:** ["Умный Дом Инвестора"](https://t.me/+asaEcPax8o41MjQy) 164 | -------------------------------------------------------------------------------- /whisper_transcribe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 🎙️ Массовое распознавание аудио с помощью OpenAI Whisper 🎙️ 6 | 7 | Этот Python-скрипт предназначен для пакетной обработки аудиофайлов (mp3, wav, m4a) 8 | в указанной директории, используя модель OpenAI Whisper для транскрибации речи. 9 | Скрипт оптимизирован для работы на GPU NVIDIA для значительного ускорения. 10 | 11 | Напоминание: Для максимальной производительности убедитесь, что у вас установлены 12 | совместимые драйверы NVIDIA, CUDA и PyTorch с поддержкой CUDA. 13 | 14 | Основные задачи: 15 | - Автоматическое определение и использование GPU, если он доступен. 16 | - Поиск всех поддерживаемых аудиофайлов в заданной директории. 17 | - Последовательная обработка каждого файла с отображением прогресса. 18 | - Сохранение результатов в нескольких форматах для удобства: 19 | - .txt: чистый текст для каждого файла. 20 | - .srt: файл субтитров с временными метками. 21 | - all_transcripts.txt: общий файл со всеми текстами. 22 | - Вывод итоговой статистики по окончании работы. 23 | 24 | Порядок использования: 25 | 1. Активируйте виртуальное окружение: source .venv/bin/activate 26 | 2. Запустите скрипт, указав параметры в командной строке: 27 | python whisper_transcribe.py <путь_к_аудио> <модель> <папка_результатов> 28 | 3. Если параметры не указаны, будут использованы значения по умолчанию. 29 | 30 | Автор: Михаил Шардин https://shardin.name/ 31 | Дата создания: 29.08.2025 32 | Версия: 1.0 33 | 34 | Актуальная версия скрипта всегда здесь: https://github.com/empenoso/offline-audio-transcriber 35 | 36 | """ 37 | 38 | import os 39 | import sys 40 | import glob 41 | import json 42 | import time 43 | from pathlib import Path 44 | import whisper 45 | import torch 46 | 47 | def check_gpu(): 48 | """Проверка доступности CUDA и GPU с тестированием совместимости""" 49 | if not torch.cuda.is_available(): 50 | print("❌ CUDA недоступна. Будет использоваться CPU") 51 | return False 52 | 53 | try: 54 | gpu_name = torch.cuda.get_device_name(0) 55 | memory_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3 56 | print(f"🎮 Найден GPU: {gpu_name}") 57 | print(f"💾 Память GPU: {memory_gb:.1f} GB") 58 | 59 | # Тест совместимости GPU - создаем небольшой тензор 60 | test_tensor = torch.zeros(10, 10).cuda() 61 | _ = test_tensor + 1 # Простая операция 62 | test_tensor = test_tensor.cpu() # Освобождаем память 63 | del test_tensor 64 | torch.cuda.empty_cache() 65 | 66 | print("✅ GPU совместим с PyTorch") 67 | return True 68 | 69 | except Exception as e: 70 | print(f"⚠️ GPU найден, но несовместим с текущим PyTorch: {str(e)}") 71 | print("🔄 Переключение на CPU режим") 72 | return False 73 | 74 | def load_whisper_model(model_size="medium", use_gpu=True): 75 | """Загрузка модели Whisper с обработкой ошибок GPU""" 76 | print(f"🔄 Загрузка модели Whisper ({model_size})...") 77 | 78 | device = "cuda" if use_gpu and torch.cuda.is_available() else "cpu" 79 | 80 | try: 81 | model = whisper.load_model(model_size, device=device) 82 | print(f"✅ Модель загружена на {device}") 83 | return model, device 84 | except Exception as e: 85 | if device == "cuda": 86 | print(f"⚠️ Ошибка загрузки на GPU: {str(e)}") 87 | print("🔄 Переключение на CPU...") 88 | model = whisper.load_model(model_size, device="cpu") 89 | print(f"✅ Модель загружена на CPU") 90 | return model, "cpu" 91 | else: 92 | raise e 93 | 94 | def get_audio_files(directory): 95 | """Поиск аудиофайлов в директории""" 96 | audio_extensions = ['*.wav', '*.mp3', '*.m4a', '*.WAV', '*.MP3', '*.M4A'] 97 | files = [] 98 | 99 | for ext in audio_extensions: 100 | files.extend(glob.glob(os.path.join(directory, ext))) 101 | 102 | return sorted(files) 103 | 104 | def transcribe_audio(model, file_path, device="cpu", language="ru"): 105 | """Распознавание одного аудиофайла""" 106 | print(f"🎵 Обрабатываю: {os.path.basename(file_path)}") 107 | 108 | try: 109 | # Засекаем время обработки 110 | start_time = time.time() 111 | 112 | # Распознавание с указанием языка 113 | result = model.transcribe( 114 | file_path, 115 | language=language, 116 | verbose=False, 117 | fp16=device == "cuda" # Используем fp16 только для GPU 118 | ) 119 | 120 | processing_time = time.time() - start_time 121 | 122 | # Извлекаем текст и сегменты 123 | text = result["text"].strip() 124 | segments = result.get("segments", []) 125 | 126 | print(f"✅ Готово за {processing_time:.1f}с") 127 | 128 | return { 129 | "file": file_path, 130 | "text": text, 131 | "segments": segments, 132 | "language": result.get("language", language), 133 | "processing_time": processing_time 134 | } 135 | 136 | except Exception as e: 137 | print(f"❌ Ошибка при обработке {file_path}: {e}") 138 | return None 139 | 140 | def save_single_result(result, output_dir): 141 | """Сохранение результата одного файла сразу после обработки""" 142 | if not result: 143 | return 144 | 145 | os.makedirs(output_dir, exist_ok=True) 146 | 147 | base_name = os.path.splitext(os.path.basename(result['file']))[0] 148 | 149 | # Текстовый файл 150 | individual_txt = os.path.join(output_dir, f"{base_name}.txt") 151 | with open(individual_txt, 'w', encoding='utf-8') as f: 152 | f.write(result['text']) 153 | 154 | # SRT субтитры (если есть сегменты) 155 | if result['segments']: 156 | srt_path = os.path.join(output_dir, f"{base_name}.srt") 157 | with open(srt_path, 'w', encoding='utf-8') as f: 158 | for i, segment in enumerate(result['segments'], 1): 159 | start = format_timestamp(segment['start']) 160 | end = format_timestamp(segment['end']) 161 | text = segment['text'].strip() 162 | f.write(f"{i}\n{start} --> {end}\n{text}\n\n") 163 | 164 | # Добавляем в общий файл 165 | all_txt_path = os.path.join(output_dir, "all_transcripts.txt") 166 | with open(all_txt_path, 'a', encoding='utf-8') as f: 167 | f.write(f"=== {os.path.basename(result['file'])} ===\n") 168 | f.write(f"{result['text']}\n\n") 169 | 170 | print(f"💾 Файл сохранен: {base_name}.txt, {base_name}.srt") 171 | 172 | def save_final_json(results, output_dir): 173 | """Сохранение финального JSON файла со всеми результатами""" 174 | os.makedirs(output_dir, exist_ok=True) 175 | 176 | # Сохранение JSON с детальной информацией 177 | json_path = os.path.join(output_dir, "transcripts_detailed.json") 178 | with open(json_path, 'w', encoding='utf-8') as f: 179 | json.dump(results, f, ensure_ascii=False, indent=2) 180 | 181 | def format_timestamp(seconds): 182 | """Форматирование времени для SRT""" 183 | hours = int(seconds // 3600) 184 | minutes = int((seconds % 3600) // 60) 185 | secs = int(seconds % 60) 186 | millis = int((seconds % 1) * 1000) 187 | return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}" 188 | 189 | def print_statistics(results): 190 | """Вывод статистики обработки""" 191 | successful = [r for r in results if r is not None] 192 | failed = len(results) - len(successful) 193 | 194 | if successful: 195 | total_time = sum(r['processing_time'] for r in successful) 196 | avg_time = total_time / len(successful) 197 | total_text = sum(len(r['text']) for r in successful) 198 | 199 | print(f"\n📊 Статистика:") 200 | print(f"✅ Успешно обработано: {len(successful)} файлов") 201 | print(f"❌ Ошибок: {failed}") 202 | print(f"⏱️ Общее время: {total_time:.1f}с") 203 | print(f"⚡ Среднее время на файл: {avg_time:.1f}с") 204 | print(f"📝 Всего символов распознано: {total_text}") 205 | 206 | def main(): 207 | """Основная функция""" 208 | print("🎙️ Скрипт распознавания русской речи с OpenAI Whisper\n") 209 | 210 | # Параметры (можно изменить) 211 | input_directory = "." # Текущая директория 212 | output_directory = "transcripts" 213 | model_size = "large" # tiny, base, small, medium, large 214 | language = "ru" # Русский язык 215 | 216 | # Получение параметров из аргументов командной строки 217 | if len(sys.argv) > 1: 218 | input_directory = sys.argv[1] 219 | if len(sys.argv) > 2: 220 | model_size = sys.argv[2] 221 | if len(sys.argv) > 3: 222 | output_directory = sys.argv[3] 223 | 224 | print(f"📁 Директория с аудио: {input_directory}") 225 | print(f"🎯 Модель: {model_size}") 226 | print(f"💾 Выходная директория: {output_directory}") 227 | print(f"🌍 Язык: {language}\n") 228 | 229 | # Проверка GPU 230 | use_gpu = check_gpu() 231 | print() 232 | 233 | # Поиск аудиофайлов 234 | audio_files = get_audio_files(input_directory) 235 | 236 | if not audio_files: 237 | print(f"❌ Аудиофайлы не найдены в {input_directory}") 238 | print("Поддерживаемые форматы: wav, mp3, m4a") 239 | return 240 | 241 | print(f"🎵 Найдено {len(audio_files)} аудиофайлов:") 242 | for file in audio_files: 243 | size_mb = os.path.getsize(file) / (1024 * 1024) 244 | print(f" - {os.path.basename(file)} ({size_mb:.1f} MB)") 245 | print() 246 | 247 | # Загрузка модели 248 | model, actual_device = load_whisper_model(model_size, use_gpu) 249 | print() 250 | 251 | # Создаем выходную директорию и очищаем общий файл 252 | os.makedirs(output_directory, exist_ok=True) 253 | 254 | # Очищаем общий файл в начале 255 | all_txt_path = os.path.join(output_directory, "all_transcripts.txt") 256 | with open(all_txt_path, 'w', encoding='utf-8') as f: 257 | f.write("") # Очищаем файл 258 | 259 | # Обработка файлов с немедленным сохранением 260 | results = [] 261 | total_files = len(audio_files) 262 | 263 | print(f"🚀 Начинаю обработку {total_files} файлов на {actual_device.upper()}...\n") 264 | 265 | for i, file_path in enumerate(audio_files, 1): 266 | print(f"[{i}/{total_files}] ", end="") 267 | result = transcribe_audio(model, file_path, actual_device, language) 268 | 269 | if result: 270 | results.append(result) 271 | # Сохраняем результат сразу после обработки 272 | save_single_result(result, output_directory) 273 | 274 | # Показываем превью текста 275 | if result['text']: 276 | preview = result['text'][:100] + "..." if len(result['text']) > 100 else result['text'] 277 | print(f"📝 Превью: {preview}") 278 | else: 279 | results.append(None) 280 | print() 281 | 282 | # Сохранение финального JSON файла 283 | print("💾 Сохраняю итоговый JSON...") 284 | save_final_json(results, output_directory) 285 | 286 | # Статистика 287 | print_statistics(results) 288 | 289 | print(f"\n🎉 Готово! Результаты сохранены в {output_directory}/") 290 | print(f"📄 Файлы:") 291 | print(f" - all_transcripts.txt (весь текст)") 292 | print(f" - transcripts_detailed.json (JSON с деталями)") 293 | print(f" - [имя_файла].txt (отдельные текстовые файлы)") 294 | print(f" - [имя_файла].srt (субтитры)") 295 | 296 | if __name__ == "__main__": 297 | # Справка по использованию 298 | if len(sys.argv) > 1 and sys.argv[1] in ['-h', '--help']: 299 | print("Использование:") 300 | print(" python whisper_transcribe.py [директория] [модель] [выходная_папка]") 301 | print("\nПримеры:") 302 | print(" python whisper_transcribe.py") 303 | print(" python whisper_transcribe.py ./audio") 304 | print(" python whisper_transcribe.py ./audio large ./results") 305 | print("\nМодели: tiny, base, small, medium, large") 306 | print("Чем больше модель, тем точнее, но медленнее") 307 | sys.exit(0) 308 | 309 | try: 310 | main() 311 | except KeyboardInterrupt: 312 | print("\n\n❌ Прервано пользователем") 313 | except Exception as e: 314 | print(f"\n❌ Критическая ошибка: {e}") 315 | sys.exit(1) -------------------------------------------------------------------------------- /whisperx_diarization_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 🛠️ Скрипт установки WhisperX с диаризацией (Docker + NVIDIA) 🛠️ 4 | # 5 | # Этот Shell-скрипт полностью автоматизирует подготовку системы Ubuntu 6 | # (20.04/22.04/24.04) для работы с WhisperX через Docker с ускорением на GPU 7 | # от NVIDIA. Он устанавливает все компоненты, настраивает их и создает 8 | # готовое к работе окружение. 9 | # 10 | # Напоминание: скрипт следует официальным инструкциям NVIDIA и Docker 11 | # для обеспечения максимальной надежности и совместимости. 12 | # 13 | # Основные задачи: 14 | # - Проверка системы: Определяет дистрибутив и наличие драйверов NVIDIA. 15 | # - Установка Docker: Устанавливает Docker Engine и добавляет пользователя 16 | # в нужную группу для работы без `sudo`. 17 | # - Установка NVIDIA Container Toolkit: Позволяет Docker-контейнерам 18 | # напрямую использовать ресурсы GPU. 19 | # - Тестирование GPU в Docker: Запускает тестовый контейнер для проверки 20 | # корректности настройки. 21 | # - Загрузка образа WhisperX: Скачивает готовый Docker-образ со всеми 22 | # зависимостями. 23 | # - Создание рабочего пространства: 24 | # - Локальные папки `audio/` и `results/`. 25 | # - Глобальный кеш для моделей в `~/whisperx/` для экономии места. 26 | # - Файл конфигурации `config.env` с настройками по умолчанию. 27 | # - Управление правами: Назначает корректные права на папки, чтобы избежать 28 | # конфликтов доступа у Docker-контейнера. 29 | # 30 | # Порядок использования: 31 | # 1. Сделайте скрипт исполняемым: chmod +x whisperx_diarization_setup.sh 32 | # 2. Запустите его: ./whisperx_diarization_setup.sh 33 | # 3. После завершения может потребоваться перезагрузка системы. 34 | 35 | # Следить за состоянием GPU: $ watch -n 5 nvidia-smi 36 | # 37 | # Автор: Михаил Шардин https://shardin.name/ 38 | # Дата создания: 14.09.2025 39 | # Версия: 2.2 40 | # 41 | # Актуальная версия скрипта всегда здесь: https://github.com/empenoso/offline-audio-transcriber 42 | # 43 | # =================================================================== 44 | 45 | ## Строгий режим для bash. Прерывает выполнение при любой ошибке. 46 | set -euo pipefail 47 | 48 | # Цвета для вывода 49 | RED='\033[0;31m' 50 | GREEN='\033[0;32m' 51 | YELLOW='\033[1;33m' 52 | BLUE='\033[0;34m' 53 | NC='\033[0m' # No Color 54 | 55 | # Функции логирования (используем printf для большей надежности) 56 | log() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; } 57 | success() { printf "${GREEN}[SUCCESS]${NC} %s\n" "$1"; } 58 | warning() { printf "${YELLOW}[WARNING]${NC} %s\n" "$1"; } 59 | error() { printf "${RED}[ERROR]${NC} %s\n" "$1" >&2; } # Ошибки выводим в stderr 60 | 61 | # --- Функции проверки системы --- 62 | 63 | check_distro() { 64 | if ! [ -f /etc/os-release ]; then 65 | error "Не удалось определить операционную систему." 66 | exit 1 67 | fi 68 | . /etc/os-release 69 | if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then 70 | error "Этот скрипт предназначен для Ubuntu/Debian. Обнаружено: $PRETTY_NAME" 71 | exit 1 72 | fi 73 | success "Обнаружена совместимая система: $PRETTY_NAME" 74 | } 75 | 76 | check_gpu() { 77 | log "Проверка наличия NVIDIA GPU и драйверов..." 78 | if ! command -v nvidia-smi &> /dev/null; then 79 | error "Команда 'nvidia-smi' не найдена. Установите драйверы NVIDIA." 80 | printf "Рекомендуемые команды:\n" 81 | printf " sudo ubuntu-drivers autoinstall\n" 82 | printf " sudo reboot\n" 83 | exit 1 84 | fi 85 | if ! nvidia-smi &> /dev/null; then 86 | error "'nvidia-smi' не отвечает. Возможно, требуется перезагрузка после установки драйверов." 87 | exit 1 88 | fi 89 | GPU_INFO=$(nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits) 90 | DRIVER_VERSION=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits) 91 | success "Найден GPU: $GPU_INFO" 92 | log "Версия драйвера: $DRIVER_VERSION" 93 | } 94 | 95 | # --- Функции установки компонентов --- 96 | 97 | install_docker() { 98 | if command -v docker &> /dev/null && docker --version &> /dev/null; then 99 | success "Docker уже установлен: $(docker --version)" 100 | else 101 | log "Установка Docker Engine..." 102 | sudo apt-get update 103 | sudo apt-get install -y ca-certificates curl 104 | sudo install -m 0755 -d /etc/apt/keyrings 105 | sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc 106 | sudo chmod a+r /etc/apt/keyrings/docker.asc 107 | 108 | echo \ 109 | "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ 110 | $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ 111 | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 112 | 113 | sudo apt-get update 114 | sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 115 | success "Docker успешно установлен." 116 | fi 117 | 118 | # Добавление пользователя в группу docker, если еще не там 119 | if ! groups "$USER" | grep -q '\bdocker\b'; then 120 | log "Добавление пользователя $USER в группу docker..." 121 | sudo usermod -aG docker "$USER" 122 | warning "Для применения изменений группы docker требуется перезагрузка или перелогин." 123 | log "Вы можете выполнить 'sudo reboot' после завершения установки." 124 | fi 125 | } 126 | 127 | install_nvidia_toolkit() { 128 | log "Установка NVIDIA Container Toolkit..." 129 | 130 | if command -v nvidia-ctk &> /dev/null; then 131 | success "NVIDIA Container Toolkit уже установлен." 132 | else 133 | log "Настройка репозитория NVIDIA..." 134 | # Этот метод автоматически определяет версию дистрибутива (ubuntu22.04, ubuntu24.04 и т.д.) 135 | curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \ 136 | && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ 137 | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ 138 | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null 139 | 140 | log "Обновление списка пакетов и установка..." 141 | sudo apt-get update 142 | sudo apt-get install -y nvidia-container-toolkit 143 | success "NVIDIA Container Toolkit успешно установлен." 144 | fi 145 | 146 | log "Конфигурирование Docker для работы с NVIDIA GPU..." 147 | sudo nvidia-ctk runtime configure --runtime=docker 148 | 149 | log "Перезапуск Docker daemon для применения конфигурации..." 150 | sudo systemctl restart docker 151 | sleep 3 # Даем демону время на перезапуск 152 | success "Docker настроен для работы с NVIDIA GPU." 153 | } 154 | 155 | test_docker_gpu() { 156 | log "Тестирование Docker с поддержкой GPU..." 157 | if ! sudo docker run --rm hello-world > /dev/null 2>&1; then 158 | error "Базовый Docker не работает. Проверьте 'systemctl status docker'" 159 | exit 1 160 | fi 161 | success "Базовый тест Docker пройден." 162 | 163 | log "Проверка доступа к GPU из контейнера..." 164 | local cuda_image="nvidia/cuda:12.4.1-base-ubuntu22.04" # Используем актуальный образ 165 | log "Используем тестовый образ: $cuda_image" 166 | 167 | if ! sudo docker pull "$cuda_image" > /dev/null; then 168 | warning "Не удалось загрузить тестовый образ $cuda_image. Пропускаем тест GPU." 169 | return 1 170 | fi 171 | 172 | # Пытаемся выполнить nvidia-smi внутри контейнера 173 | local gpu_name_in_container 174 | gpu_name_in_container=$(sudo docker run --rm --gpus all "$cuda_image" nvidia-smi --query-gpu=name --format=csv,noheader) 175 | 176 | if [[ -n "$gpu_name_in_container" ]]; then 177 | success "🎉 GPU успешно обнаружен в Docker контейнере: $gpu_name_in_container" 178 | return 0 # Успех 179 | else 180 | error "Не удалось получить доступ к GPU из Docker контейнера." 181 | warning "WhisperX будет работать на CPU (значительно медленнее)." 182 | log "Возможные причины:" 183 | log " - Конфликт версий драйвера, toolkit или docker." 184 | log " - Необходимо перезагрузить систему: 'sudo reboot'" 185 | return 1 # Неудача 186 | fi 187 | } 188 | 189 | pull_whisperx_image() { 190 | log "Загрузка Docker образа WhisperX..." 191 | local whisperx_image="ghcr.io/jim60105/whisperx:latest" 192 | 193 | if sudo docker pull "$whisperx_image"; then 194 | success "Образ $whisperx_image загружен успешно." 195 | local image_size_bytes 196 | image_size_bytes=$(sudo docker image inspect "$whisperx_image" --format='{{.Size}}') 197 | local image_size_gb 198 | image_size_gb=$(awk "BEGIN {printf \"%.2f\", $image_size_bytes/1024/1024/1024}") 199 | log "Размер образа: ~${image_size_gb} GB" 200 | else 201 | error "Не удалось загрузить образ WhisperX: $whisperx_image" 202 | exit 1 203 | fi 204 | } 205 | 206 | setup_workspace() { 207 | log "Создание рабочих директорий и конфигурации..." 208 | local base_dir="." 209 | local cache_dir="$HOME/whisperx" 210 | 211 | mkdir -p "$base_dir"/{audio,results} 212 | mkdir -p "$cache_dir" 213 | 214 | log "Установка прав 777 на папки..." 215 | chmod -R 777 "$base_dir"/audio "$base_dir"/results "$cache_dir" 216 | 217 | success "Созданы директории:" 218 | printf " 📂 %s/audio - для входных аудиофайлов\n" "$(pwd)" 219 | printf " 📂 %s/results - для результатов\n" "$(pwd)" 220 | printf " 🧠 %s - для кеширования моделей\n" "$cache_dir" 221 | 222 | local config_file="$base_dir/config.env" 223 | if [ -f "$config_file" ]; then 224 | # ИЗМЕНЕНИЕ: Исправлена переменная $config.env на $config_file 225 | success "Конфигурационный файл $config_file уже существует. Пропускаем создание." 226 | else 227 | log "Создание конфигурационного файла: $config_file" 228 | cat > "$config_file" << 'EOF' 229 | # Конфигурация WhisperX 230 | # HuggingFace токен для диаризации (получите на https://huggingface.co/settings/tokens) 231 | # ВАЖНО: Примите лицензии на: 232 | # https://huggingface.co/pyannote/speaker-diarization-3.1 233 | # https://huggingface.co/pyannote/segmentation-3.0 234 | HF_TOKEN=your_token_here 235 | 236 | # Модель Whisper (tiny, base, small, medium, large-v1, large-v2, large-v3) 237 | WHISPER_MODEL=large-v3 238 | 239 | # Язык аудио (ru, en, auto для автоопределения) 240 | LANGUAGE=ru 241 | 242 | # Размер батча (чем больше - тем быстрее, но больше памяти GPU) 243 | BATCH_SIZE=16 244 | 245 | # Устройство для вычислений (cuda или cpu) 246 | DEVICE=cuda 247 | 248 | # Включить диаризацию (разделение по спикерам) 249 | ENABLE_DIARIZATION=true 250 | 251 | # Минимальное количество спикеров (оставить пустым для автоопределения) 252 | MIN_SPEAKERS= 253 | 254 | # Максимальное количество спикеров (оставить пустым для автоопределения) 255 | MAX_SPEAKERS= 256 | 257 | # Тип вычислений (float16, float32, int8) 258 | COMPUTE_TYPE=float16 259 | 260 | # Метод VAD для обнаружения речи (pyannote, silero) 261 | VAD_METHOD=pyannote 262 | 263 | # Размер чанков в секундах 264 | CHUNK_SIZE=30 265 | EOF 266 | success "Конфигурационный файл создан: $config_file" 267 | fi 268 | } 269 | 270 | final_check() { 271 | log "Выполнение финальной проверки установки..." 272 | 273 | if ! command -v docker &>/dev/null; then error "Docker не найден!"; exit 1; fi 274 | if ! sudo docker image inspect "ghcr.io/jim60105/whisperx:latest" &>/dev/null; then error "Образ WhisperX не найден!"; exit 1; fi 275 | if ! [ -d "./audio" ]; then error "Рабочая директория не найдена!"; exit 1; fi 276 | if ! [ -d "$HOME/whisperx" ]; then error "Директория кеша моделей не найдена!"; exit 1; fi 277 | 278 | success "Все компоненты установлены и готовы к работе!" 279 | } 280 | 281 | show_usage() { 282 | printf "\n=====================================================================\n" 283 | printf "🎉 УСТАНОВКА ЗАВЕРШЕНА УСПЕШНО!\n" 284 | printf "=====================================================================\n\n" 285 | 286 | printf "🔥 ВАЖНЫЕ СЛЕДУЮЩИЕ ШАГИ:\n\n" 287 | 288 | printf "1. 🔑 ${YELLOW}Отредактируйте токен Hugging Face${NC} для диаризации:\n" 289 | printf " - Откройте файл: nano ./config.env\n" 290 | printf " - Замените 'your_token_here' на ваш токен с https://huggingface.co/settings/tokens\n\n" 291 | 292 | printf "2. 🔄 ${YELLOW}Перезагрузите систему${NC}, если вы не были в группе docker:\n" 293 | printf " sudo reboot\n\n" 294 | 295 | printf "После перезагрузки:\n" 296 | printf "3. 📁 Скопируйте ваши аудиофайлы в ./audio/\n" 297 | printf "4. 🚀 Запустите обработку: python3 whisperx_diarization.py\n\n" 298 | 299 | printf "Рабочие директории:\n" 300 | printf " 📂 ./audio - Входные файлы (*.wav, *.mp3, *.m4a)\n" 301 | printf " 📂 ./results - Результаты распознавания\n" 302 | printf " 🧠 ~/whisperx/ - Кеш моделей (общий для всех проектов)\n" 303 | printf " ⚙️ ./config.env - Настройки\n\n" 304 | 305 | printf "=====================================================================\n" 306 | } 307 | 308 | # --- Основная функция --- 309 | main() { 310 | printf "=====================================================================\n" 311 | printf "🎙️ УСТАНОВКА WHISPERX ДЛЯ ДИАРИЗАЦИИ РЕЧИ (DOCKER + NVIDIA)\n" 312 | printf "=====================================================================\n\n" 313 | 314 | check_distro 315 | check_gpu 316 | install_docker 317 | install_nvidia_toolkit 318 | 319 | if test_docker_gpu; then 320 | log "Тест GPU пройден. WhisperX будет использовать видеокарту." 321 | else 322 | warning "Тест GPU не пройден. Проверьте настройки в './config.env' и установите DEVICE=cpu, если GPU не заработает." 323 | fi 324 | 325 | pull_whisperx_image 326 | setup_workspace 327 | final_check 328 | show_usage 329 | } 330 | 331 | # Запуск основной функции 332 | main 333 | -------------------------------------------------------------------------------- /whisperx_diarization.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | 🎙️ Распознавание речи с диаризацией через WhisperX (Docker) 🎙️ 6 | 7 | Этот Python-скрипт является оркестратором для пакетной обработки аудиофайлов 8 | с использованием WhisperX. Он запускает транскрибацию и диаризацию (разделение 9 | по спикерам) в изолированном Docker-контейнере, что решает проблемы 10 | совместимости и обеспечивает стабильную работу на системах с GPU NVIDIA. 11 | 12 | Напоминание: для работы диаризации требуется токен Hugging Face (HF_TOKEN) 13 | и принятие лицензий для моделей pyannote. 14 | 15 | Основные задачи: 16 | - Изоляция зависимостей: Использует готовый Docker-образ, избавляя от ручной 17 | установки PyTorch, CUDA и других сложных компонентов. 18 | - Поддержка GPU и CPU: Автоматически задействует GPU NVIDIA для максимального 19 | ускорения и может работать в режиме CPU. 20 | - Пакетная обработка: Обрабатывает как отдельные файлы, так и все аудио 21 | в указанной директории (mp3, wav, m4a и др.). 22 | - Централизованный кеш: Сохраняет скачанные модели в общей папке `~/.whisperx/`, 23 | экономя дисковое пространство и время при повторных запусках. 24 | - Гибкая конфигурация: Управляет параметрами (модель, язык, токен) через 25 | внешний файл `config.env`. 26 | - Информативный вывод: Отображает детальный прогресс и итоговую статистику, 27 | включая скорость обработки относительно реального времени. 28 | - Встроенная проверка системы: Команда `--check` позволяет быстро убедиться, 29 | что Docker, GPU и права доступа настроены корректно. 30 | 31 | Порядок использования: 32 | 1. Отредактируйте файл `config.env`, указав ваш HF_TOKEN. 33 | 2. Поместите аудиофайлы в папку `audio/`. 34 | 3. Запустите скрипт: 35 | python3 whisperx_diarization.py 36 | 4. Результаты (txt, srt, json) появятся в папке `results/`. 37 | 38 | Автор: Михаил Шардин https://shardin.name/ 39 | Дата создания: 13.09.2025 40 | Версия: 2.1 41 | 42 | Актуальная версия скрипта всегда здесь: https://github.com/empenoso/offline-audio-transcriber 43 | """ 44 | 45 | import os 46 | import sys 47 | import subprocess 48 | import argparse 49 | import logging 50 | from pathlib import Path 51 | from typing import List, Dict, Optional 52 | import time 53 | from datetime import datetime 54 | import shutil 55 | import itertools 56 | 57 | # Определяем базовую директорию относительно местоположения скрипта 58 | SCRIPT_DIR = Path(__file__).parent.resolve() 59 | 60 | # Определяем глобальную директорию для кеша моделей в домашней папке пользователя 61 | USER_CACHE_DIR = Path.home() / 'whisperx' 62 | 63 | # Настройка логирования 64 | logging.basicConfig( 65 | level=logging.INFO, 66 | format='%(asctime)s - %(levelname)s - %(message)s', 67 | handlers=[ 68 | logging.FileHandler(SCRIPT_DIR / 'whisperx_diarization.log', encoding='utf-8'), 69 | logging.StreamHandler(sys.stdout) 70 | ] 71 | ) 72 | logger = logging.getLogger(__name__) 73 | 74 | class Colors: 75 | """ANSI цвета для консольного вывода""" 76 | RED = '\033[0;31m' 77 | GREEN = '\033[0;32m' 78 | YELLOW = '\033[1;33m' 79 | CYAN = '\033[0;36m' 80 | WHITE = '\033[1;37m' 81 | NC = '\033[0m' 82 | 83 | class WhisperXDocker: 84 | """Класс для работы с WhisperX через Docker""" 85 | 86 | def __init__(self, config_path: str = "config.env"): 87 | self.work_dir = SCRIPT_DIR 88 | # ИЗМЕНЕНИЕ: Ссылка на глобальный кеш 89 | self.cache_dir = USER_CACHE_DIR 90 | self.config_path = self.work_dir / config_path 91 | self.config = self._load_config() 92 | self.image_name = "ghcr.io/jim60105/whisperx:latest" 93 | self.use_gpu = self.config.get('DEVICE') == 'cuda' 94 | self._ensure_directories() 95 | 96 | def _load_config(self) -> Dict[str, str]: 97 | """Загружает конфигурацию из файла .env""" 98 | config = { 99 | 'HF_TOKEN': '', 'WHISPER_MODEL': 'large-v3', 'LANGUAGE': 'ru', 100 | 'BATCH_SIZE': '16', 'DEVICE': 'cuda', 'ENABLE_DIARIZATION': 'true', 101 | 'MIN_SPEAKERS': '', 'MAX_SPEAKERS': '', 'COMPUTE_TYPE': 'float16', 102 | 'VAD_METHOD': 'pyannote', 'CHUNK_SIZE': '30' 103 | } 104 | if self.config_path.exists(): 105 | try: 106 | with open(self.config_path, 'r', encoding='utf-8') as f: 107 | for line in f: 108 | line = line.strip() 109 | if line and not line.startswith('#') and '=' in line: 110 | key, value = line.split('=', 1) 111 | config[key.strip()] = value.strip().strip('"\'') 112 | except Exception as e: 113 | logger.warning(f"Ошибка загрузки конфигурации: {e}") 114 | logger.info("Создание файла конфигурации по умолчанию...") 115 | self._create_default_config() 116 | else: 117 | logger.info("Файл конфигурации не найден. Создание по умолчанию...") 118 | self._create_default_config() 119 | return config 120 | 121 | def _create_default_config(self): 122 | """Создает файл конфигурации по умолчанию""" 123 | self.config_path.parent.mkdir(parents=True, exist_ok=True) 124 | default_config = """# Конфигурация WhisperX 125 | # HuggingFace токен для диаризации (получите на https://huggingface.co/settings/tokens) 126 | # ВАЖНО: Примите лицензии на: 127 | # https://huggingface.co/pyannote/speaker-diarization-3.1 128 | # https://huggingface.co/pyannote/segmentation-3.0 129 | HF_TOKEN=your_token_here 130 | 131 | # Модель Whisper (tiny, base, small, medium, large-v1, large-v2, large-v3) 132 | WHISPER_MODEL=large-v3 133 | 134 | # Язык аудио (ru, en, auto для автоопределения) 135 | LANGUAGE=ru 136 | 137 | # Размер батча (чем больше - тем быстрее, но больше памяти GPU) 138 | BATCH_SIZE=16 139 | 140 | # Устройство для вычислений (cuda или cpu) 141 | DEVICE=cuda 142 | 143 | # Включить диаризацию (разделение по спикерам) 144 | ENABLE_DIARIZATION=true 145 | 146 | # Минимальное количество спикеров (оставить пустым для автоопределения) 147 | MIN_SPEAKERS= 148 | 149 | # Максимальное количество спикеров (оставить пустым для автоопределения) 150 | MAX_SPEAKERS= 151 | 152 | # Тип вычислений (float16, float32, int8) 153 | COMPUTE_TYPE=float16 154 | 155 | # Метод VAD для обнаружения речи (pyannote, silero) 156 | VAD_METHOD=pyannote 157 | 158 | # Размер чанков в секундах 159 | CHUNK_SIZE=30 160 | """ 161 | with open(self.config_path, 'w', encoding='utf-8') as f: 162 | f.write(default_config) 163 | logger.info(f"Создан файл конфигурации: {self.config_path}") 164 | 165 | def _ensure_directories(self): 166 | """Создает необходимые рабочие директории""" 167 | # Создаем локальные папки audio и results 168 | for dir_name in ['audio', 'results']: 169 | p = self.work_dir / dir_name 170 | p.mkdir(parents=True, exist_ok=True) 171 | 172 | # Создаем глобальную папку для кеша моделей, если ее нет 173 | self.cache_dir.mkdir(parents=True, exist_ok=True) 174 | logger.info(f"Кеш моделей будет сохраняться в: {self.cache_dir}") 175 | 176 | 177 | def _run_command(self, cmd: List[str], timeout: int = 45) -> Optional[subprocess.CompletedProcess]: 178 | """Унифицированная функция для запуска внешних команд""" 179 | try: 180 | return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=True, encoding='utf-8') 181 | except FileNotFoundError: 182 | logger.error(f"Команда '{cmd[0]}' не найдена.") 183 | except subprocess.TimeoutExpired: 184 | logger.error(f"Команда '{' '.join(cmd)}' заняла слишком много времени.") 185 | except subprocess.CalledProcessError as e: 186 | logger.error(f"Ошибка выполнения команды. Код: {e.returncode}") 187 | if e.stderr: 188 | logger.error(f"Stderr: {e.stderr.strip()}") 189 | return None 190 | 191 | def _check_gpu(self) -> bool: 192 | """Проверяет доступность GPU через Docker""" 193 | logger.info("Проверка доступа к GPU из Docker...") 194 | cmd = [ 195 | 'sudo', 'docker', 'run', '--rm', '--gpus', 'all', 196 | 'nvidia/cuda:12.4.1-base-ubuntu22.04', 197 | 'nvidia-smi', '--query-gpu=name', '--format=csv,noheader' 198 | ] 199 | result = self._run_command(cmd) 200 | if result and result.stdout.strip(): 201 | logger.info(f"✅ GPU успешно обнаружен: {result.stdout.strip()}") 202 | return True 203 | return False 204 | 205 | def _format_time(self, seconds: float) -> str: 206 | """Форматирует секунды в читаемый вид (ч:м:с)""" 207 | if seconds < 0: return "0.0с" 208 | mins, secs = divmod(seconds, 60) 209 | hours, mins = divmod(mins, 60) 210 | if hours > 0: 211 | return f"{int(hours)}ч {int(mins)}м {int(secs)}с" 212 | elif mins > 0: 213 | return f"{int(mins)}м {int(secs)}с" 214 | else: 215 | return f"{secs:.1f}с" 216 | 217 | def _get_audio_duration(self, file_path: Path) -> Optional[float]: 218 | """Получает длительность аудиофайла через ffprobe""" 219 | if not shutil.which('ffprobe'): 220 | return None 221 | cmd = [ 222 | 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', 223 | '-of', 'default=noprint_wrappers=1:nokey=1', str(file_path) 224 | ] 225 | result = self._run_command(cmd, timeout=15) 226 | try: 227 | return float(result.stdout.strip()) if result and result.stdout.strip() else None 228 | except (ValueError, AttributeError): 229 | return None 230 | 231 | def list_audio_files(self, directory: Optional[Path] = None) -> List[Path]: 232 | """Находит все поддерживаемые аудиофайлы в директории""" 233 | directory = directory or self.work_dir / "audio" 234 | extensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg', '.aac', '.wma', '.mp4', '.mkv', '.avi'] 235 | return sorted([p for p in directory.rglob('*') if p.suffix.lower() in extensions and p.is_file()]) 236 | 237 | def process_file(self, audio_file: Path, output_dir: Optional[Path] = None) -> bool: 238 | """Обрабатывает один аудиофайл с помощью WhisperX в Docker""" 239 | output_dir = output_dir or self.work_dir / "results" 240 | file_output_dir = output_dir / audio_file.stem 241 | file_output_dir.mkdir(exist_ok=True) 242 | 243 | cmd = ['sudo', 'docker', 'run', '--rm', '--user', f"{os.getuid()}:{os.getgid()}"] 244 | 245 | if self.use_gpu: 246 | cmd.extend(['--gpus', 'all']) 247 | 248 | cmd.extend([ 249 | '-v', f"{audio_file.parent.resolve()}:/audio:ro", 250 | '-v', f"{file_output_dir.resolve()}:/results", 251 | # ИЗМЕНЕНИЕ: Монтируем глобальную директорию кеша в /models внутри контейнера 252 | '-v', f"{self.cache_dir.resolve()}:/models", 253 | '--workdir', '/app', 254 | # ИЗМЕНЕНИЕ: Все пути кеша внутри контейнера теперь указывают на смонтированный том /models 255 | '-e', 'HOME=/models', 256 | '-e', 'HF_HOME=/models/.cache/huggingface', 257 | '-e', 'XDG_CACHE_HOME=/models/.cache', 258 | '-e', 'TORCH_HOME=/models/.cache/torch', 259 | ]) 260 | 261 | hf_token = self.config.get('HF_TOKEN', '').strip() 262 | if hf_token and hf_token != 'your_token_here': 263 | cmd.extend(['-e', f"HF_TOKEN={hf_token}"]) 264 | logger.info("✅ HF_TOKEN передан в контейнер") 265 | else: 266 | logger.warning(f"{Colors.YELLOW}⚠️ HF_TOKEN не настроен! Диализация может не работать.{Colors.NC}") 267 | 268 | cmd.extend([self.image_name, 'whisperx']) 269 | whisper_args = [ 270 | '--output_dir', "/results", 271 | '--model', self.config.get('WHISPER_MODEL', 'large-v3'), 272 | '--language', self.config.get('LANGUAGE', 'ru'), 273 | '--batch_size', self.config.get('BATCH_SIZE', '16'), 274 | '--device', 'cuda' if self.use_gpu else 'cpu', 275 | '--compute_type', self.config.get('COMPUTE_TYPE', 'float16'), 276 | '--output_format', 'all', 277 | '--verbose', 'False' 278 | ] 279 | 280 | if (self.config.get('ENABLE_DIARIZATION', 'true').lower() == 'true' and hf_token and hf_token != 'your_token_here'): 281 | whisper_args.extend(['--diarize', '--hf_token', hf_token]) 282 | for key, name in [('MIN_SPEAKERS', '--min_speakers'), ('MAX_SPEAKERS', '--max_speakers')]: 283 | value = self.config.get(key) 284 | if value and value.isdigit() and int(value) > 0: 285 | whisper_args.extend([name, value]) 286 | elif self.config.get('ENABLE_DIARIZATION', 'true').lower() == 'true': 287 | logger.warning("⚠️ Диаризация отключена - нет HF_TOKEN") 288 | 289 | whisper_args.append(f"/audio/{audio_file.name}") 290 | cmd.extend(whisper_args) 291 | 292 | duration = self._get_audio_duration(audio_file) 293 | logger.info(f"{Colors.CYAN}🎵 Обрабатываем: {audio_file.name}{Colors.NC}") 294 | logger.info(f" 📊 Размер: {audio_file.stat().st_size / (1024*1024):.1f} МБ") 295 | if duration: 296 | logger.info(f" ⏱️ Длительность: {self._format_time(duration)}") 297 | logger.info(f" 📁 Результаты: {file_output_dir}") 298 | 299 | start_time = time.time() 300 | logger.info(f"{Colors.YELLOW}🚀 Запуск WhisperX...{Colors.NC}") 301 | 302 | try: 303 | process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 304 | text=True, encoding='utf-8', bufsize=1, universal_newlines=True) 305 | 306 | spinner = itertools.cycle(['⠇', '⠏', '⠋', '⠙', '⠸', '⠴', '⠦', '⠇']) 307 | stderr_lines = [] 308 | 309 | current_status = "Инициализация..." 310 | sys.stdout.write(f" [PROGRESS] {next(spinner)} {current_status}\r") 311 | 312 | while process.poll() is None: 313 | line = process.stderr.readline() 314 | if line: 315 | stderr_lines.append(line.strip()) 316 | if "Performing VAD" in line or "voice activity detection" in line: 317 | current_status = "1/4 Обнаружение речи (VAD)..." 318 | elif "Performing transcription" in line: 319 | current_status = "2/4 Транскрибация текста..." 320 | elif "Performing alignment" in line: 321 | current_status = "3/4 Выравнивание временных меток..." 322 | elif "Performing diarization" in line: 323 | current_status = f"4/4 Диарізація (може зайняти багато часу)..." 324 | 325 | sys.stdout.write(f" [PROGRESS] {next(spinner)} {current_status}\r") 326 | sys.stdout.flush() 327 | time.sleep(0.1) 328 | 329 | sys.stdout.write(" " * (len(current_status) + 20) + "\r") 330 | sys.stdout.flush() 331 | 332 | process.wait() 333 | 334 | if process.returncode == 0: 335 | processing_time = time.time() - start_time 336 | logger.info(f"{Colors.GREEN}✅ Обработка завершена успешно!{Colors.NC}") 337 | logger.info(f" ⏱️ Время обработки: {self._format_time(processing_time)}") 338 | if duration and processing_time > 0: 339 | speed_factor = duration / processing_time 340 | logger.info(f" 🚀 Скорость: {speed_factor:.1f}x от реального времени") 341 | 342 | result_files = list(file_output_dir.glob('*')) 343 | if result_files: 344 | logger.info(f" 📄 Создано файлов: {len(result_files)}") 345 | for rf in sorted(result_files): 346 | logger.info(f" • {rf.name}") 347 | return True 348 | else: 349 | logger.error(f"{Colors.RED}❌ Ошибка обработки файла {audio_file.name}{Colors.NC}") 350 | logger.error(f" Код возврата Docker: {process.returncode}") 351 | if stderr_lines: 352 | logger.error(" Последние сообщения из лога контейнера:") 353 | for line in stderr_lines[-10:]: 354 | if line.strip(): 355 | logger.error(f" [Docker ERR]: {line}") 356 | return False 357 | 358 | except Exception as e: 359 | logger.error(f"❌ Критическая ошибка при запуске Docker: {e}") 360 | return False 361 | 362 | def process_directory(self, input_dir: Optional[Path] = None): 363 | """Обрабатывает все аудиофайлы в директории""" 364 | audio_files = self.list_audio_files(input_dir) 365 | if not audio_files: 366 | logger.warning(f"В директории {input_dir or self.work_dir / 'audio'} не найдено аудиофайлов.") 367 | return 368 | 369 | logger.info(f"{Colors.CYAN}📁 Найдено {len(audio_files)} аудиофайлов{Colors.NC}") 370 | stats = {"total": len(audio_files), "success": 0, "failed": 0} 371 | start_time = time.time() 372 | 373 | for i, audio_file in enumerate(audio_files, 1): 374 | logger.info(f"\n{Colors.WHITE}═══ Файл {i}/{stats['total']} ═══{Colors.NC}") 375 | if self.process_file(audio_file): 376 | stats["success"] += 1 377 | else: 378 | stats["failed"] += 1 379 | 380 | logger.info(f"\n{Colors.WHITE}{'═'*35}{Colors.NC}") 381 | logger.info(f"{Colors.GREEN}🎯 ИТОГИ ОБРАБОТКИ{Colors.NC}") 382 | logger.info(f"{Colors.WHITE}{'═'*35}{Colors.NC}") 383 | logger.info(f"📊 Всего файлов: {stats['total']}") 384 | logger.info(f"✅ Успешно: {stats['success']}") 385 | logger.info(f"❌ С ошибками: {stats['failed']}") 386 | total_time = time.time() - start_time 387 | logger.info(f"⏱️ Общее время: {self._format_time(total_time)}") 388 | if stats['total'] > 0: 389 | logger.info(f"📈 Среднее время на файл: {self._format_time(total_time / stats['total'])}") 390 | 391 | def check_system(self) -> bool: 392 | logger.info(f"{Colors.CYAN}🔍 Проверка системы...{Colors.NC}") 393 | 394 | if not self._run_command(['docker', '--version']): 395 | logger.error("❌ Docker не найден. Установите Docker."); return False 396 | logger.info("✅ Docker найден") 397 | 398 | if self.use_gpu: 399 | if not self._check_gpu(): 400 | logger.warning("⚠️ GPU недоступен через Docker, переключаемся на CPU.") 401 | self.use_gpu = False; self.config['DEVICE'] = 'cpu' 402 | else: logger.info("✅ GPU-ускорение активно") 403 | else: logger.info("⚙️ Режим CPU активен (согласно config.env)") 404 | 405 | if not self._run_command(['sudo', 'docker', 'image', 'inspect', self.image_name]): 406 | logger.error(f"❌ Образ WhisperX не найден. Выполните: sudo docker pull {self.image_name}"); return False 407 | logger.info("✅ Образ WhisperX найден") 408 | 409 | hf_token = self.config.get('HF_TOKEN', '').strip() 410 | if self.config.get('ENABLE_DIARIZATION', 'true').lower() == 'true': 411 | if not hf_token or hf_token == 'your_token_here': 412 | logger.error(f"❌ HF_TOKEN не настроен в {self.config_path}") 413 | logger.info("💡 Получите токен на https://huggingface.co/settings/tokens и примите лицензии."); return False 414 | else: logger.info("✅ HF_TOKEN настроен") 415 | 416 | if not shutil.which('ffprobe'): 417 | logger.warning("⚠️ ffprobe не найден. Длительность аудио не будет отображаться. (sudo apt install ffmpeg)") 418 | 419 | #Проверяем права на запись в глобальный кеш, а не в локальную папку 420 | try: 421 | test_file = self.cache_dir / 'test_write.tmp' 422 | test_file.touch(); test_file.unlink() 423 | logger.info(f"✅ Есть права на запись в кеш {self.cache_dir}") 424 | except Exception as e: 425 | logger.error(f"❌ Нет прав на запись в директорию кеша {self.cache_dir}: {e}"); return False 426 | 427 | logger.info(f"{Colors.GREEN}✅ Система готова к работе{Colors.NC}") 428 | return True 429 | 430 | 431 | def main(): 432 | """Главная функция-обработчик CLI""" 433 | parser = argparse.ArgumentParser(description='🎙️ Распознавание речи с диаризацией через WhisperX (DOCKER)') 434 | parser.add_argument('-f', '--file', type=str, help='Путь к конкретному аудиофайлу для обработки') 435 | parser.add_argument('-d', '--directory', type=str, help='Путь к директории с аудиофайлами') 436 | parser.add_argument('--check', action='store_true', help='Проверить готовность системы к работе') 437 | parser.add_argument('--config', type=str, default="config.env", help='Путь к файлу конфигурации относительно скрипта') 438 | parser.add_argument('--debug', action='store_true', help='Включить отладочный режим') 439 | args = parser.parse_args() 440 | 441 | if args.debug: 442 | logging.getLogger().setLevel(logging.DEBUG) 443 | 444 | print(f"{Colors.CYAN}{'═'*70}\n🎙️ WHISPERX ДИАРИЗАЦИЯ РЕЧИ (DOCKER)\n{'═'*70}{Colors.NC}") 445 | print(f"Автор скрипта: Михаил Шардин | https://shardin.name/\nДата: {datetime.now().strftime('%d.%m.%Y %H:%M')}\n") 446 | 447 | try: 448 | whisperx = WhisperXDocker(config_path=args.config) 449 | 450 | if args.check: 451 | whisperx.check_system(); return 452 | 453 | if not whisperx.check_system(): 454 | logger.error("Система не готова. Исправьте ошибки и повторите."); sys.exit(1) 455 | 456 | if args.file: 457 | file_path = Path(args.file).expanduser().resolve() 458 | if not file_path.exists(): 459 | logger.error(f"Файл не найден: {file_path}"); sys.exit(1) 460 | whisperx.process_file(file_path) 461 | else: 462 | input_dir = Path(args.directory).expanduser() if args.directory else None 463 | whisperx.process_directory(input_dir=input_dir) 464 | 465 | except KeyboardInterrupt: 466 | logger.info(f"\n{Colors.YELLOW}⏹️ Работа прервана пользователем{Colors.NC}"); sys.exit(130) 467 | except Exception as e: 468 | logger.error(f"Критическая непредвиденная ошибка: {e}", exc_info=True); sys.exit(1) 469 | 470 | if __name__ == "__main__": 471 | main() --------------------------------------------------------------------------------