diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5fb62d --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Обязательные параметры +TG_BOT_TOKEN=your_telegram_bot_token_here + +# Модель faster-whisper: tiny, base, small, medium, large-v3, turbo +WHISPER_MODEL=turbo + +# Устройство: cpu, cuda, auto +WHISPER_DEVICE=cpu + +# Тип вычислений: int8 (экономит RAM на CPU), float32, float16 (GPU) +COMPUTE_TYPE=int8 + +# Количество параллельных обработчиков +WORKER_COUNT=2 + +# Максимальный размер очереди задач +QUEUE_MAXSIZE=200 + +# Сохранять ли аудиофайлы после обработки +SAVE_VOICES=False + +# Директория для сохранённых файлов +VOICES_DIR=voices + +# Уровень логирования: DEBUG, INFO, WARNING, ERROR, CRITICAL, NONE +LOG_LEVEL=INFO diff --git a/README.md b/README.md index 9a39c67..f317798 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,109 @@ # WhisperBot — Telegram Bot для распознавания речи -Этот Telegram бот использует модель Whisper от OpenAI для преобразования голосовых сообщений и видео-кружочков в текст. Бот поддерживает очередь задач и обрабатывает сообщения параллельно с использованием нескольких рабочих потоков. +Telegram бот для преобразования голосовых сообщений и видео-кружочков в текст с использованием [faster-whisper](https://github.com/SYSTRAN/faster-whisper). Поддерживает очередь задач и параллельную обработку. ## Возможности -* Преобразование голосовых сообщений в текст. -* Поддержка обработки видео-кружочков (видео-сообщений). -* Очередь задач для обработки голосовых сообщений. -* Поддержка использования GPU для обработки с использованием CUDA. -* Возможность сохранять аудио-файлы, если требуется. -* Информирование пользователя о статусе очереди и обработке. +* Преобразование голосовых сообщений в текст +* Поддержка видео-кружочков (video note) +* Очередь задач с параллельной обработкой (настраиваемое число воркеров) +* VAD-фильтрация (пропуск тишины для ускорения) +* Поддержка CPU (int8 квантизация) и GPU (CUDA) +* Сохранение аудиофайлов (опционально) ## Установка -1. **Клонируйте репозиторий:** +### 1. Клонируйте репозиторий - ```bash - git clone https://github.com/AlexMelanFromRingo/WhisperBot.git - cd WhisperBot - ``` +```bash +git clone https://github.com/AlexMelanFromRingo/WhisperBot.git +cd WhisperBot +``` -2. **Создайте виртуальное окружение и активируйте его:** +### 2. Создайте виртуальное окружение - ```bash - python -m venv venv - source venv/bin/activate # для Linux/Mac - .\venv\Scripts\activate # для Windows - ``` +```bash +python3 -m venv venv +source venv/bin/activate # Linux/Mac +# .\venv\Scripts\activate # Windows +``` -3. **Установите зависимости:** +### 3. Установите зависимости - ```bash - pip install -r requirements.txt - ``` +```bash +pip install -r requirements.txt +``` -4. **Создайте файл `.env` и настройте переменные окружения:** +### 4. Настройте конфигурацию - Пример содержимого `.env`: +```bash +cp .env.example .env +``` - ```env - TG_BOT_TOKEN=your_telegram_bot_token - WHISPER_MODEL=base # Возможные значения: base, small, medium, large, turbo, etc. Подробнее см. в документации OpenAI Whisper - WHISPER_DEVICE=cpu # Используйте "cuda" для GPU, "cpu" для использования процессора или "auto" для автоматического выбора - WORKER_COUNT=2 # Количество рабочих потоков - QUEUE_MAXSIZE=200 # Максимальный размер очереди - SAVE_VOICES=True # Устанавливайте False, если не хотите сохранять аудио файлы - VOICES_DIR=voices # Директория для хранения голосовых файлов - LOG_LEVEL=INFO # Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL) - ``` +Отредактируйте `.env` — обязательно укажите `TG_BOT_TOKEN`. -5. **Запустите бота:** +Параметры `.env`: - ```bash - python telegram_whisper_bot.py - ``` +| Параметр | По умолчанию | Описание | +|---|---|---| +| `TG_BOT_TOKEN` | — | Токен Telegram бота (обязательно) | +| `WHISPER_MODEL` | `turbo` | Модель: `tiny`, `base`, `small`, `medium`, `large-v3`, `turbo` | +| `WHISPER_DEVICE` | `cpu` | Устройство: `cpu`, `cuda`, `auto` | +| `COMPUTE_TYPE` | `int8` | Тип вычислений: `int8`, `float32` (CPU); `float16`, `int8` (GPU) | +| `WORKER_COUNT` | `2` | Количество параллельных обработчиков | +| `QUEUE_MAXSIZE` | `200` | Максимальный размер очереди | +| `SAVE_VOICES` | `True` | Сохранять аудиофайлы после обработки | +| `VOICES_DIR` | `voices` | Директория для сохранённых файлов | +| `LOG_LEVEL` | `INFO` | Уровень логирования | -## Использование +### 5. Запустите бота -### Команды бота +```bash +OMP_NUM_THREADS=4 python telegram_whisper_bot.py +``` -* `/start` — Приветственное сообщение с информацией о сервере и возможностях бота. -* `/status` — Отображает статус очереди и состояние обработки сообщений для пользователя. +`OMP_NUM_THREADS` задаёт количество потоков для faster-whisper. Рекомендуется ставить по числу ядер CPU. -### Как это работает? +## Рекомендуемая конфигурация для CPU-сервера (4 Core, 24GB RAM) -1. Отправьте боту голосовое сообщение или видео-кружочек (video note). -2. Бот начнёт процесс транскрипции с использованием модели Whisper. -3. После завершения обработки бот отправит текст обратно в чат. +```env +WHISPER_MODEL=turbo +WHISPER_DEVICE=cpu +COMPUTE_TYPE=int8 +WORKER_COUNT=2 +``` -Если очередь переполнена, бот сообщит об этом и предложит попробовать позже. +- `int8` снижает потребление RAM примерно вдвое по сравнению с `float32` +- `turbo` — оптимальный баланс скорости и качества +- `WORKER_COUNT=2` при 4 ядрах — каждый воркер использует ~2 ядра -### Структура проекта +## Команды бота -* **`bot.py`** — Основной файл с логикой работы бота. -* **`config.py`** — Конфигурационные настройки для работы с ботом. -* **`requirements.txt`** — Все необходимые библиотеки. -* **`.env`** — Файл с переменными окружения. -* **`voices/`** — Директория для хранения голосовых файлов (если `SAVE_VOICES=True`). -* **`logs/`** — Логи работы бота (если логирование активно). +* `/start` — информация о боте и сервере +* `/status` — статус очереди и задач пользователя -## Логирование +## Как это работает -Бот поддерживает логирование для отслеживания ошибок и состояния системы. Уровень логирования можно настроить в файле `.env` через переменную `LOG_LEVEL`. +1. Отправьте боту голосовое сообщение или видео-кружочек +2. Сообщение встаёт в очередь обработки +3. Свободный воркер берёт задачу, транскрибирует аудио через faster-whisper +4. Результат отправляется в ответ на исходное сообщение -* Уровни логирования: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`. +Один пользователь может отправить несколько голосовых подряд — все встанут в общую очередь. -## Примечания +## Структура проекта -* Если очередь обработки переполнена, бот уведомит пользователя о том, что сервер перегружен, и предложит повторить попытку позже. -* Для корректной работы с GPU, необходимо установить соответствующие зависимости для CUDA. -* Если модель Whisper не доступна для вашей системы, бот будет использовать CPU для обработки. +* **`telegram_whisper_bot.py`** — основной файл бота +* **`requirements.txt`** — зависимости +* **`.env.example`** — пример конфигурации +* **`voices/`** — директория для аудиофайлов (при `SAVE_VOICES=True`) ## Зависимости -В проекте используются следующие библиотеки: - -* `whisper` — Модель для распознавания речи от OpenAI. -* `python-telegram-bot` — Библиотека для работы с Telegram API. -* `dotenv` — Для загрузки переменных окружения из файла `.env`. -* `torch` — Для работы с моделью Whisper на GPU (если доступно). - -Установите их с помощью команды: - -```bash -pip install -r requirements.txt -``` +* [faster-whisper](https://github.com/SYSTRAN/faster-whisper) — быстрая реализация Whisper на CTranslate2 +* [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) — Telegram Bot API +* [python-dotenv](https://github.com/theskumar/python-dotenv) — загрузка `.env` ## Автор -Этот проект был разработан Alex Melan и является открытым исходным кодом. Вы можете использовать его по своему усмотрению и модифицировать в соответствии с вашими потребностями. +Этот проект был разработан Alex Melan и является открытым исходным кодом. diff --git a/requirements.txt b/requirements.txt index 58ac905..cc66cce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,4 @@ python-telegram-bot==20.7 -openai-whisper==20231117 +faster-whisper>=1.1.0 python-dotenv==1.0.0 -torch==2.1.0+cpu -torchaudio==2.1.0+cpu -numpy==1.24.3 -ffmpeg-python==0.2.0 +numpy>=1.24 diff --git a/telegram_whisper_bot.py b/telegram_whisper_bot.py index 780c405..4d01d2c 100644 --- a/telegram_whisper_bot.py +++ b/telegram_whisper_bot.py @@ -5,12 +5,12 @@ import threading import time from pathlib import Path -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List import tempfile import hashlib from datetime import datetime -import whisper +from faster_whisper import WhisperModel from telegram import Update, Message from telegram.ext import Application, MessageHandler, filters, ContextTypes from dotenv import load_dotenv @@ -22,18 +22,19 @@ class Config: def __init__(self): self.TG_BOT_TOKEN = os.getenv('TG_BOT_TOKEN') - self.WHISPER_MODEL = os.getenv('WHISPER_MODEL', 'base') + self.WHISPER_MODEL = os.getenv('WHISPER_MODEL', 'turbo') self.WHISPER_DEVICE = os.getenv('WHISPER_DEVICE', 'cpu') # cpu/cuda/auto + self.COMPUTE_TYPE = os.getenv('COMPUTE_TYPE', 'int8') # int8/float32 для CPU; float16/int8 для CUDA self.WORKER_COUNT = int(os.getenv('WORKER_COUNT', '2')) self.QUEUE_MAXSIZE = int(os.getenv('QUEUE_MAXSIZE', '200')) self.SAVE_VOICES = os.getenv('SAVE_VOICES', 'True').lower() == 'true' self.VOICES_DIR = os.getenv('VOICES_DIR', 'voices') self.LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO') - + # Валидация обязательных параметров if not self.TG_BOT_TOKEN: raise ValueError("TG_BOT_TOKEN не установлен в .env файле") - + # Создаем директорию для голосовых, если нужно сохранять if self.SAVE_VOICES: Path(self.VOICES_DIR).mkdir(exist_ok=True) @@ -45,7 +46,7 @@ def setup_logging(): if config.LOG_LEVEL.upper() == 'NONE': logging.disable(logging.CRITICAL) return - + level = getattr(logging, config.LOG_LEVEL.upper(), logging.INFO) logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -56,63 +57,64 @@ def setup_logging(): logger = logging.getLogger(__name__) class VoiceProcessor: - """Обработчик голосовых сообщений с использованием Whisper""" - + """Обработчик голосовых сообщений с использованием faster-whisper""" + def __init__(self): - # Определяем устройство для обработки self.device = self._get_device() - logger.info(f"Загружаем модель Whisper: {config.WHISPER_MODEL} на устройство: {self.device}") - - # Загружаем модель с явным указанием устройства - self.model = whisper.load_model(config.WHISPER_MODEL, device=self.device) - logger.info("Модель Whisper загружена успешно") - + logger.info(f"Загружаем модель faster-whisper: {config.WHISPER_MODEL} на устройство: {self.device}, compute_type: {config.COMPUTE_TYPE}") + + self.model = WhisperModel( + config.WHISPER_MODEL, + device=self.device, + compute_type=config.COMPUTE_TYPE + ) + logger.info("Модель faster-whisper загружена успешно") + def _get_device(self): """Определяет устройство для обработки""" - import torch - if config.WHISPER_DEVICE.lower() == 'auto': - if torch.cuda.is_available(): - device = 'cuda' - logger.info("CUDA доступна, используем GPU") - else: - device = 'cpu' - logger.info("CUDA недоступна, используем CPU") + try: + import torch + if torch.cuda.is_available(): + logger.info("CUDA доступна, используем GPU") + return 'cuda' + except ImportError: + pass + logger.info("CUDA недоступна, используем CPU") + return 'cpu' elif config.WHISPER_DEVICE.lower() == 'cuda': - if torch.cuda.is_available(): - device = 'cuda' - logger.info("Принудительно используем CUDA/GPU") - else: - logger.warning("CUDA запрошена, но недоступна. Используем CPU") - device = 'cpu' - else: # cpu или любое другое значение - device = 'cpu' + logger.info("Принудительно используем CUDA/GPU") + return 'cuda' + else: logger.info("Принудительно используем CPU") - - return device - + return 'cpu' + def transcribe_audio(self, audio_path: str) -> Dict[str, Any]: """Транскрибирует аудио файл""" try: logger.info(f"Начинаем транскрипцию: {audio_path}") start_time = time.time() - - result = self.model.transcribe( + + segments, info = self.model.transcribe( audio_path, - # language='ru', # Можно сделать автоопределение, убрав этот параметр - task='transcribe' + beam_size=5, + vad_filter=True, + vad_parameters=dict(min_silence_duration_ms=500), ) - + + # faster-whisper возвращает генератор — собираем текст + text = " ".join(segment.text.strip() for segment in segments) + processing_time = time.time() - start_time - logger.info(f"Транскрипция завершена за {processing_time:.2f} сек") - + logger.info(f"Транскрипция завершена за {processing_time:.2f} сек, язык: {info.language}") + return { 'success': True, - 'text': result['text'].strip(), - 'language': result.get('language', 'unknown'), + 'text': text.strip(), + 'language': info.language, 'processing_time': processing_time } - + except Exception as e: logger.error(f"Ошибка при транскрипции: {e}") return { @@ -122,54 +124,62 @@ def transcribe_audio(self, audio_path: str) -> Dict[str, Any]: class QueueManager: """Менеджер очереди обработки голосовых сообщений""" - + def __init__(self): self.processing_queue = queue.Queue(maxsize=config.QUEUE_MAXSIZE) - self.result_queue = queue.Queue() # Очередь для результатов - self.active_tasks = {} # chat_id -> task_info + self.active_tasks: Dict[int, List[Dict[str, Any]]] = {} # chat_id -> список задач + self.lock = threading.Lock() self.processor = VoiceProcessor() self.workers = [] self.running = True - self.event_loop = None # Ссылка на основной event loop - + self.event_loop = None + # Запускаем worker'ов for i in range(config.WORKER_COUNT): worker = threading.Thread(target=self._worker, args=(i,), daemon=True) worker.start() self.workers.append(worker) logger.info(f"Запущен worker #{i}") - + def add_task(self, chat_id: int, message: Message, audio_path: str) -> bool: """Добавляет задачу в очередь""" try: + task_id = f"{chat_id}_{message.message_id}" task = { + 'task_id': task_id, 'chat_id': chat_id, 'message': message, 'audio_path': audio_path, 'timestamp': datetime.now() } - + self.processing_queue.put_nowait(task) - self.active_tasks[chat_id] = { - 'status': 'queued', - 'position': self.processing_queue.qsize() - } - + + with self.lock: + if chat_id not in self.active_tasks: + self.active_tasks[chat_id] = [] + self.active_tasks[chat_id].append({ + 'task_id': task_id, + 'status': 'queued', + 'position': self.processing_queue.qsize() + }) + logger.info(f"Задача добавлена в очередь для chat_id={chat_id}, позиция: {self.processing_queue.qsize()}") return True - + except queue.Full: logger.warning(f"Очередь переполнена, отклоняем задачу для chat_id={chat_id}") return False - - def get_queue_status(self, chat_id: int) -> Optional[Dict[str, Any]]: - """Возвращает статус задачи в очереди""" - return self.active_tasks.get(chat_id) - + + def get_user_tasks_count(self, chat_id: int) -> int: + """Возвращает количество активных задач пользователя""" + with self.lock: + return len(self.active_tasks.get(chat_id, [])) + def set_event_loop(self, loop): """Устанавливает ссылку на основной event loop""" self.event_loop = loop - + def _schedule_coroutine(self, coro): """Планирует выполнение корутины в основном event loop""" if self.event_loop and not self.event_loop.is_closed(): @@ -179,46 +189,49 @@ def _schedule_coroutine(self, coro): logger.error(f"Ошибка при планировании корутины: {e}") else: logger.error("Event loop недоступен для планирования корутины") - + def _worker(self, worker_id: int): """Worker для обработки задач из очереди""" logger.info(f"Worker #{worker_id} запущен") - + while self.running: try: - # Получаем задачу из очереди task = self.processing_queue.get(timeout=1) - + chat_id = task['chat_id'] + task_id = task['task_id'] message = task['message'] audio_path = task['audio_path'] - - logger.info(f"Worker #{worker_id} обрабатывает задачу для chat_id={chat_id}") - + + logger.info(f"Worker #{worker_id} обрабатывает задачу {task_id}") + # Обновляем статус - self.active_tasks[chat_id] = {'status': 'processing'} - + with self.lock: + tasks = self.active_tasks.get(chat_id, []) + for t in tasks: + if t['task_id'] == task_id: + t['status'] = 'processing' + break + try: - # Обрабатываем аудио result = self.processor.transcribe_audio(audio_path) - - # Планируем отправку результата в основном event loop self._schedule_coroutine(self._send_result(message, result, audio_path)) - except Exception as e: logger.error(f"Ошибка при обработке задачи: {e}") self._schedule_coroutine(self._send_error(message, str(e), audio_path)) - finally: - # Удаляем из активных задач - self.active_tasks.pop(chat_id, None) + with self.lock: + tasks = self.active_tasks.get(chat_id, []) + self.active_tasks[chat_id] = [t for t in tasks if t['task_id'] != task_id] + if not self.active_tasks[chat_id]: + del self.active_tasks[chat_id] self.processing_queue.task_done() - + except queue.Empty: continue except Exception as e: logger.error(f"Критическая ошибка в worker #{worker_id}: {e}") - + async def _send_result(self, original_message: Message, result: Dict[str, Any], audio_path: str): """Отправляет результат транскрипции""" try: @@ -229,27 +242,26 @@ async def _send_result(self, original_message: Message, result: Dict[str, Any], else: processing_time = result.get('processing_time', 0) response = f"📝 *Текст голосового сообщения:*\n\n{text}\n\n_Обработано за {processing_time:.1f} сек_" - + await original_message.reply_text( - response, + response, parse_mode='Markdown', reply_to_message_id=original_message.message_id ) else: await self._send_error(original_message, result.get('error', 'Неизвестная ошибка'), audio_path) - + except Exception as e: logger.error(f"Ошибка при отправке результата: {e}") - + finally: - # Удаляем временный файл, если не нужно сохранять if not config.SAVE_VOICES and os.path.exists(audio_path): try: os.remove(audio_path) logger.info(f"Временный файл удален: {audio_path}") except Exception as e: logger.error(f"Не удалось удалить временный файл: {e}") - + async def _send_error(self, original_message: Message, error: str, audio_path: str): """Отправляет сообщение об ошибке""" try: @@ -259,22 +271,20 @@ async def _send_error(self, original_message: Message, error: str, audio_path: s ) except Exception as e: logger.error(f"Ошибка при отправке сообщения об ошибке: {e}") - + finally: - # Удаляем файл при ошибке в любом случае if os.path.exists(audio_path): try: os.remove(audio_path) logger.info(f"Файл удален после ошибки: {audio_path}") except Exception as e: logger.error(f"Не удалось удалить файл после ошибки: {e}") - + def shutdown(self): """Корректное завершение работы""" logger.info("Завершаем работу QueueManager...") self.running = False - - # Ждем завершения всех workers + for worker in self.workers: worker.join(timeout=5) @@ -285,50 +295,37 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик голосовых сообщений""" chat_id = update.effective_chat.id message = update.message - - # Проверяем, есть ли уже активная задача для этого чата - if queue_manager.get_queue_status(chat_id): - await message.reply_text( - "⏳ У вас уже есть голосовое сообщение в обработке. Пожалуйста, дождитесь завершения.", - reply_to_message_id=message.message_id - ) - return - + try: - # Получаем файл голосового сообщения voice = message.voice file = await context.bot.get_file(voice.file_id) - - # Генерируем имя файла + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_hash = hashlib.md5(f"{chat_id}_{voice.file_id}".encode()).hexdigest()[:8] - + if config.SAVE_VOICES: - # Сохраняем в постоянную директорию filename = f"voice_{timestamp}_{file_hash}.ogg" audio_path = os.path.join(config.VOICES_DIR, filename) else: - # Используем временный файл temp_file = tempfile.NamedTemporaryFile( - suffix='.ogg', + suffix='.ogg', delete=False, prefix=f'voice_{timestamp}_{file_hash}_' ) audio_path = temp_file.name temp_file.close() - - # Скачиваем файл + await file.download_to_drive(audio_path) logger.info(f"Голосовое сообщение сохранено: {audio_path}") - - # Добавляем в очередь + if queue_manager.add_task(chat_id, message, audio_path): queue_position = queue_manager.processing_queue.qsize() + user_tasks = queue_manager.get_user_tasks_count(chat_id) if queue_position > 1: await message.reply_text( f"📥 Голосовое сообщение добавлено в очередь на обработку.\n" f"Позиция в очереди: {queue_position}\n" - f"⏱ Примерное время ожидания: {queue_position * 10-30} сек", + f"Ваших задач в очереди: {user_tasks}", reply_to_message_id=message.message_id ) else: @@ -337,17 +334,14 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): reply_to_message_id=message.message_id ) else: - # Очередь переполнена await message.reply_text( "😔 Извините, сервер перегружен. Попробуйте позже.\n" f"Максимальная длина очереди: {config.QUEUE_MAXSIZE}", reply_to_message_id=message.message_id ) - - # Удаляем файл, если очередь переполнена if os.path.exists(audio_path): os.remove(audio_path) - + except Exception as e: logger.error(f"Ошибка при обработке голосового сообщения: {e}") await message.reply_text( @@ -357,39 +351,31 @@ async def handle_voice(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_video_note(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обработчик кружочков (video note)""" - # Аналогично голосовым сообщениям, но для видео-кружочков chat_id = update.effective_chat.id message = update.message - - if queue_manager.get_queue_status(chat_id): - await message.reply_text( - "⏳ У вас уже есть сообщение в обработке. Пожалуйста, дождитесь завершения.", - reply_to_message_id=message.message_id - ) - return - + try: video_note = message.video_note file = await context.bot.get_file(video_note.file_id) - + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_hash = hashlib.md5(f"{chat_id}_{video_note.file_id}".encode()).hexdigest()[:8] - + if config.SAVE_VOICES: filename = f"videonote_{timestamp}_{file_hash}.mp4" audio_path = os.path.join(config.VOICES_DIR, filename) else: temp_file = tempfile.NamedTemporaryFile( - suffix='.mp4', + suffix='.mp4', delete=False, prefix=f'videonote_{timestamp}_{file_hash}_' ) audio_path = temp_file.name temp_file.close() - + await file.download_to_drive(audio_path) logger.info(f"Видео-кружочек сохранен: {audio_path}") - + if queue_manager.add_task(chat_id, message, audio_path): await message.reply_text( "🔄 Начинаю извлечение аудио из видео-кружочка...", @@ -402,7 +388,7 @@ async def handle_video_note(update: Update, context: ContextTypes.DEFAULT_TYPE): ) if os.path.exists(audio_path): os.remove(audio_path) - + except Exception as e: logger.error(f"Ошибка при обработке видео-кружочка: {e}") await message.reply_text( @@ -415,69 +401,62 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): welcome_text = ( "🎤 *Привет! Я бот для распознавания речи*\n\n" "Отправь мне голосовое сообщение или видео-кружочек, " - "и я преобразую речь в текст с помощью OpenAI Whisper.\n\n" + "и я преобразую речь в текст с помощью faster-whisper.\n\n" "📊 *Информация о сервере:*\n" - f"• Модель Whisper: `{config.WHISPER_MODEL}`\n" - f"• Устройство обработки: `{config.WHISPER_DEVICE}`\n" - f"• Количество обработчиков: {config.WORKER_COUNT}\n" - f"• Максимальный размер очереди: {config.QUEUE_MAXSIZE}\n" + f"• Модель: `{config.WHISPER_MODEL}`\n" + f"• Устройство: `{config.WHISPER_DEVICE}`\n" + f"• Тип вычислений: `{config.COMPUTE_TYPE}`\n" + f"• Обработчиков: {config.WORKER_COUNT}\n" + f"• Макс. очередь: {config.QUEUE_MAXSIZE}\n" f"• Сохранение файлов: {'✅' if config.SAVE_VOICES else '❌'}\n\n" "Просто отправь голосовое сообщение! 🚀" ) - + await update.message.reply_text(welcome_text, parse_mode='Markdown') async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Команда /status - показывает статус очереди""" chat_id = update.effective_chat.id queue_size = queue_manager.processing_queue.qsize() - + status_text = f"📊 *Статус обработки:*\n\n" status_text += f"• Задач в очереди: {queue_size}\n" status_text += f"• Активных обработчиков: {config.WORKER_COUNT}\n" - - user_status = queue_manager.get_queue_status(chat_id) - if user_status: - if user_status['status'] == 'queued': - status_text += f"• Ваша позиция в очереди: {user_status['position']}\n" - elif user_status['status'] == 'processing': - status_text += "• Ваше сообщение обрабатывается прямо сейчас ⚡\n" + + user_tasks_count = queue_manager.get_user_tasks_count(chat_id) + if user_tasks_count > 0: + status_text += f"• Ваших задач в обработке: {user_tasks_count}\n" else: status_text += "• У вас нет активных задач\n" - + await update.message.reply_text(status_text, parse_mode='Markdown') def main(): """Главная функция""" logger.info("Запускаем Telegram бота...") - - # Создаем приложение + application = Application.builder().token(config.TG_BOT_TOKEN).build() - - # Регистрируем обработчики + application.add_handler(MessageHandler(filters.VOICE, handle_voice)) application.add_handler(MessageHandler(filters.VIDEO_NOTE, handle_video_note)) application.add_handler(MessageHandler(filters.COMMAND & filters.Regex(r'^/start'), start_command)) application.add_handler(MessageHandler(filters.COMMAND & filters.Regex(r'^/status'), status_command)) - - # Устанавливаем event loop после инициализации приложения + async def post_init(application): loop = asyncio.get_running_loop() queue_manager.set_event_loop(loop) logger.info("Event loop установлен для QueueManager") - + application.post_init = post_init - + try: logger.info("Бот запущен и готов к работе!") - # Запускаем бота (это создаст и запустит свой event loop) application.run_polling(allowed_updates=Update.ALL_TYPES) except KeyboardInterrupt: logger.info("Получен сигнал завершения...") finally: - # Корректное завершение logger.info("Завершаем работу...") queue_manager.shutdown() if __name__ == '__main__': - main() \ No newline at end of file + main()