/
tari.gpt
/
ServitorBot
Обзор
Документация
Войти
/
tari.gpt
/
ServitorBot
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
voice_processor.py
342 строки
18 KB
tarrri
fix ошибок
26 ноя 2025, 12:24
26 ноя 2025, 12:24
f0c002a
Код
Авторство
О чём код?
# Voice Message Processor import os import json import logging from datetime import datetime, timedelta from collections import deque from groq import Groq from openai import OpenAI logger = logging.getLogger(__name__) class RateLimiter: """ Rate limiter для соблюдения ограничений API Groq Ограничения (с запасом): - RPM (Requests per minute): до 15 (вместо 20) - RPD (Requests per day): до 1800 (вместо 2000) - ASH (Audio seconds per hour): до 6000 (вместо 7200) - ASD (Audio seconds per day): до 25000 (вместо 28800) """ def __init__(self, max_rpm=15, max_rpd=1800, max_ash=6000, max_asd=25000): self.requests_per_minute = deque() self.requests_per_day = deque() self.audio_seconds_per_hour = deque() self.audio_seconds_per_day = deque() # Лимиты (настраиваемые через параметры конструктора) self.MAX_RPM = max_rpm self.MAX_RPD = max_rpd self.MAX_ASH = max_ash self.MAX_ASD = max_asd def can_process(self, audio_duration_seconds): """ Проверяет, можно ли обработать аудио с учётом лимитов Args: audio_duration_seconds: длительность аудио в секундах Returns: tuple: (bool: можно ли обработать, str: причина отказа если нельзя) """ now = datetime.now() # Очищаем старые записи self._cleanup_old_records(now) # Проверяем лимиты if len(self.requests_per_minute) >= self.MAX_RPM: return False, "Достигнут лимит запросов в минуту. Попробуйте через минуту." if len(self.requests_per_day) >= self.MAX_RPD: return False, "Достигнут дневной лимит запросов. Попробуйте завтра." current_ash = sum(duration for _, duration in self.audio_seconds_per_hour) if current_ash + audio_duration_seconds > self.MAX_ASH: return False, f"Достигнут лимит секунд аудио в час. Попробуйте через час." current_asd = sum(duration for _, duration in self.audio_seconds_per_day) if current_asd + audio_duration_seconds > self.MAX_ASD: return False, f"Достигнут дневной лимит секунд аудио. Попробуйте завтра." return True, "" def record_request(self, audio_duration_seconds): """Записывает выполненный запрос""" now = datetime.now() self.requests_per_minute.append(now) self.requests_per_day.append(now) self.audio_seconds_per_hour.append((now, audio_duration_seconds)) self.audio_seconds_per_day.append((now, audio_duration_seconds)) def _cleanup_old_records(self, now): """Удаляет старые записи из очередей""" # Очищаем записи старше минуты while self.requests_per_minute and (now - self.requests_per_minute[0]).total_seconds() > 60: self.requests_per_minute.popleft() # Очищаем записи старше дня while self.requests_per_day and (now - self.requests_per_day[0]).total_seconds() > 86400: self.requests_per_day.popleft() # Очищаем аудио секунды старше часа while self.audio_seconds_per_hour and (now - self.audio_seconds_per_hour[0][0]).total_seconds() > 3600: self.audio_seconds_per_hour.popleft() # Очищаем аудио секунды старше дня while self.audio_seconds_per_day and (now - self.audio_seconds_per_day[0][0]).total_seconds() > 86400: self.audio_seconds_per_day.popleft() # Глобальный rate limiter (для соблюдения лимитов Groq API) global_rate_limiter = RateLimiter() # Per-user rate limiters (защита от злоупотреблений отдельными пользователями) user_rate_limiters = {} def get_user_rate_limiter(user_id): """ Получает или создаёт rate limiter для конкретного пользователя Args: user_id: ID пользователя Telegram Returns: RateLimiter: Rate limiter для данного пользователя """ if user_id not in user_rate_limiters: # Создаём per-user rate limiter с более щедрыми лимитами user_rate_limiters[user_id] = RateLimiter( max_rpm=10, # 10 запросов в минуту на пользователя max_rpd=100, # 100 запросов в день на пользователя max_ash=2000, # 2000 секунд аудио в час (~33 минуты) max_asd=10000 # 10000 секунд аудио в день (~166 минут) ) return user_rate_limiters[user_id] def transcribe_audio(audio_file_path, groq_api_key, user_id=None): """ Транскрибирует аудиофайл через Groq API (whisper-large-v3) Args: audio_file_path: путь к аудиофайлу groq_api_key: API ключ для Groq user_id: ID пользователя Telegram (для per-user rate limiting) Returns: tuple: (bool: успешно ли, str: текст транскрипции или сообщение об ошибке) """ try: # Проверяем существование файла if not os.path.exists(audio_file_path): return False, "Аудиофайл не найден" # Получаем длительность аудио (приблизительно по размеру файла) # OGG обычно ~12-16 kbps для голоса, используем 14 kbps как среднее file_size_bytes = os.path.getsize(audio_file_path) estimated_duration = file_size_bytes / (14 * 1024 / 8) # в секундах # Проверяем глобальные лимиты (Groq API) can_process, error_message = global_rate_limiter.can_process(estimated_duration) if not can_process: return False, f"❌ Глобальный лимит API: {error_message}" # Проверяем per-user лимиты (если указан user_id) if user_id: user_limiter = get_user_rate_limiter(user_id) can_process, error_message = user_limiter.can_process(estimated_duration) if not can_process: return False, f"❌ {error_message}" # Инициализируем клиент Groq client = Groq(api_key=groq_api_key) # Транскрибируем with open(audio_file_path, "rb") as audio_file: transcription = client.audio.transcriptions.create( file=audio_file, model="whisper-large-v3", language="ru", # указываем русский язык для лучшей точности response_format="text" ) # Записываем успешный запрос в оба лимитера global_rate_limiter.record_request(estimated_duration) if user_id: user_limiter = get_user_rate_limiter(user_id) user_limiter.record_request(estimated_duration) logger.info(f"✅ Транскрипция успешна (user_id={user_id}). Длина текста: {len(transcription)} символов") return True, transcription except Exception as e: logger.error(f"Ошибка при транскрипции аудио: {e}") return False, f"Ошибка транскрипции: {str(e)}" def extract_task_data(transcription_text, openrouter_api_key): """ Извлекает данные задачи из текста транскрипции через OpenRouter API Args: transcription_text: текст транскрипции openrouter_api_key: API ключ для OpenRouter Returns: tuple: (bool: успешно ли, dict/str: данные задачи в формате JSON или сообщение об ошибке) """ try: # Инициализируем клиент OpenRouter (использует OpenAI-совместимый API) client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key=openrouter_api_key ) # Получаем текущую дату для контекста current_date = datetime.now().strftime("%d.%m.%Y (%A)") # Создаём оптимизированный промпт для извлечения данных system_prompt = f"""Extract task data from voice message text. Return ONLY valid JSON, no explanations. Output format: {{"client_name":"name or null","task":"action or null","deadline":"date/description or null","deadline_time":"time or null","contact_info":"contact or null","recurrence_pattern":"pattern or null"}} Rules: - client_name: person FOR whom task (if "мне"/"себе"/"я" → "✨ Для себя") - task: FULL description with ALL details - what to do, where, how, context, specifications. Keep important info: location, platform, method, materials, format, etc. - deadline: when (as is: "завтра"/"21 число"/"пятница"/"25 декабря"/"10 января"). IMPORTANT: Keep month names EXACTLY as heard (декабря, января, февраля, марта, апреля, мая, июня, июля, августа, сентября, октября, ноября) - deadline_time: time if mentioned (HH:MM format: "14:30"/"09:00"), else null - contact_info: phone/email/telegram if mentioned, else null - recurrence_pattern: if task repeats - keep EXACTLY as heard ("каждый день"/"ежедневно"/"каждый понедельник"/"каждую неделю"/"каждое 15 число"/"каждый месяц"/"каждые 3 дня"), else null Current date: {current_date} Examples: "Вася настроить бота на сервере до пятницы" → {{"client_name":"Вася","task":"настроить бота на сервере","deadline":"пятница","deadline_time":null,"contact_info":null,"recurrence_pattern":null}} "мне снять уроки по курсу в студии на 21 число к 14:30" → {{"client_name":"✨ Для себя","task":"снять уроки по курсу в студии","deadline":"21 число","deadline_time":"14:30","contact_info":null,"recurrence_pattern":null}} "себе звонить маме каждую неделю" → {{"client_name":"✨ Для себя","task":"звонить маме","deadline":null,"deadline_time":null,"contact_info":null,"recurrence_pattern":"каждую неделю"}} "Петя отчёт в Excel каждый понедельник в 9 утра" → {{"client_name":"Петя","task":"отчёт в Excel","deadline":null,"deadline_time":"09:00","contact_info":null,"recurrence_pattern":"каждый понедельник"}} "себе купить молоко в магазине у дома сегодня к 18" → {{"client_name":"✨ Для себя","task":"купить молоко в магазине у дома","deadline":"сегодня","deadline_time":"18:00","contact_info":null,"recurrence_pattern":null}} "Мария провести встречу 15 января в 10:00" → {{"client_name":"Мария","task":"провести встречу","deadline":"15 января","deadline_time":"10:00","contact_info":null,"recurrence_pattern":null}} "мне тренировка каждую среду и пятницу в 18:00" → {{"client_name":"✨ Для себя","task":"тренировка","deadline":null,"deadline_time":"18:00","contact_info":null,"recurrence_pattern":"каждую среду и пятницу"}} "Иван проверка отчётов каждое 1 число в 10:00" → {{"client_name":"Иван","task":"проверка отчётов","deadline":null,"deadline_time":"10:00","contact_info":null,"recurrence_pattern":"каждое 1 число"}} "себе принимать витамины ежедневно в течение месяца" → {{"client_name":"✨ Для себя","task":"принимать витамины","deadline":null,"deadline_time":null,"contact_info":null,"recurrence_pattern":"ежедневно в течение месяца"}}""" user_prompt = f"Parse: {transcription_text}" # Делаем запрос к API (используем Claude 3.5 Haiku) response = client.chat.completions.create( model="anthropic/claude-3.5-haiku", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.1, # низкая температура для стабильности max_tokens=400 # увеличен лимит для более детальных описаний задач ) # Получаем ответ result_text = response.choices[0].message.content.strip() # Парсим JSON # Иногда модель может обернуть JSON в ```json ... ```, удаляем это if result_text.startswith("```"): # Ищем JSON между ``` и ``` start = result_text.find("{") end = result_text.rfind("}") + 1 if start != -1 and end > start: result_text = result_text[start:end] task_data = json.loads(result_text) logger.info(f"✅ Данные задачи успешно извлечены: {task_data}") return True, task_data except json.JSONDecodeError as e: logger.error(f"Ошибка парсинга JSON: {e}. Текст ответа: {result_text}") return False, "Ошибка обработки данных. Попробуйте переформулировать задачу." except Exception as e: logger.error(f"Ошибка при извлечении данных задачи: {e}") return False, f"Ошибка обработки: {str(e)}" def process_voice_message(audio_file_path, groq_api_key, openrouter_api_key, user_id=None): """ Полный цикл обработки голосового сообщения: 1. Транскрипция через Groq 2. Извлечение данных через OpenRouter Args: audio_file_path: путь к аудиофайлу groq_api_key: API ключ для Groq openrouter_api_key: API ключ для OpenRouter user_id: ID пользователя Telegram (для per-user rate limiting) Returns: tuple: (bool: успешно ли, dict/str: данные задачи или сообщение об ошибке) """ # Шаг 1: Транскрипция success, result = transcribe_audio(audio_file_path, groq_api_key, user_id) if not success: return False, result transcription_text = result # Проверяем минимальную длину транскрипции if len(transcription_text.strip()) < 10: return False, "Голосовое сообщение слишком короткое или не распознано. Попробуйте ещё раз." # Шаг 2: Извлечение данных success, task_data = extract_task_data(transcription_text, openrouter_api_key) if not success: return False, task_data # Добавляем оригинальную транскрипцию для справки task_data['_transcription'] = transcription_text return True, task_data def parse_task_from_text(transcription_text, openrouter_api_key, user_id=None): """ Извлекает данные задачи из уже распознанного текста (без транскрипции аудио). Используется когда транскрипция уже есть и нужно только извлечь структурированные данные. Args: transcription_text: текст транскрипции openrouter_api_key: API ключ для OpenRouter user_id: ID пользователя (опционально, для логирования) Returns: tuple: (bool: успешно ли, dict/str: данные задачи или сообщение об ошибке) """ try: # Проверяем минимальную длину текста if len(transcription_text.strip()) < 5: return False, "Текст слишком короткий для извлечения задачи." # Извлекаем данные через AI success, task_data = extract_task_data(transcription_text, openrouter_api_key) if not success: return False, task_data # Добавляем транскрипцию для справки task_data['_transcription'] = transcription_text logger.info(f"✅ Данные задачи успешно извлечены из текста для user_id={user_id}") return True, task_data except Exception as e: logger.error(f"Ошибка при парсинге задачи из текста: {e}") return False, f"Ошибка обработки: {str(e)}"