diff --git a/photo_bot/handlers.py b/photo_bot/handlers.py index eaa72b0..f69dd4b 100644 --- a/photo_bot/handlers.py +++ b/photo_bot/handlers.py @@ -1,3 +1,4 @@ +# /opt/trueconf_bot/photo_bot/handlers.py import os import asyncio import logging @@ -14,19 +15,11 @@ from trueconf.types import FSInputFile import config.config as config from utils import states from utils.menu import MENU_TEXT -from photo_bot.photo_ai_checker import verify_photo_with_ai -from utils.texts import ( - EMOJI_DIGITS, - PHOTO_MAIN_MENU_TEXT, - PHOTO_UNKNOWN_CMD_TEXT, - photo_error_handling, - PHOTO_UPLOADING_TEXT, - photo_success_sent, - photo_cancel_text, - PHOTO_PROCESSING_TEXT, - photo_preview_ready, - photo_error_processing, -) +from utils.texts import * # 👈 Импортируем все тексты и константы проекта + +# 🔌 Бизнес-логика обработки и ИИ-валидации +from photo_bot.photo_processor import prepare_ad_photo +from photo_bot.ai_validator import validate_photo_ai # 🔌 Импортируем централизованную функцию сбора статистики из main from utils.stats_logger import log_menu_stats @@ -47,7 +40,7 @@ def log_and_notify_photo_error(user_id: str, action: str, error_details: str): """Записывает системную ошибку в лог-файл и отправляет email-алерт администраторам""" global last_email_time - log_dir = "/opt/trueconf_bot/photo_bot/logs" + log_dir = os.path.dirname(os.path.abspath(__file__)) os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, "photo_errors.log") @@ -184,48 +177,76 @@ async def photo_module_handler(msg: Message): return else: - await msg.answer(PHOTO_UNKNOWN_CMD_TEXT, parse_mode="html") + await msg.answer(system_error_text(EMOJI_DIGITS), parse_mode="html") return - # --- ПРИЕМ НОВОГО ФОТО (ТЕСТОВЫЙ РЕЖИМ: ТОЛЬКО ИИ) --- + # --- ПРИЕМ НОВОГО ФОТО --- if is_attachment: file_id = getattr(msg.content, "file_id", None) - raw_data = None + ready_path = f"/tmp/{user_login}.bmp" + raw_path = f"/tmp/raw_{user_login}.webp" - await msg.answer("🤖 Отправка фото на проверку в ИИ (srvkem-ii-03:8083)...", parse_mode="html") + await msg.answer(PHOTO_PROCESSING_TEXT, parse_mode="html") try: - downloaded_file = await states.tc_bot.download_file_by_id(file_id) - raw_data = downloaded_file + # 1. Скачиваем байты файла из TrueConf + file_bytes = await states.tc_bot.download_file_by_id(file_id) - # Отправляем фото напрямую в ИИ Gemma 12B Vision - ai_result = await verify_photo_with_ai(raw_data) + # 2. Сохраняем байты во временный файл на диск + def save_raw_bytes(b_data, target_path): + with open(target_path, "wb") as f: + f.write(b_data) - # 🛡️ Защита от неожиданного формата (гарантирует отсутствие NameError) - if not isinstance(ai_result, dict): - ai_result = {"is_valid": False, "reason": "Некорректный формат ответа от ИИ."} + await asyncio.to_thread(save_raw_bytes, file_bytes, raw_path) - is_valid = ai_result.get("is_valid", False) - reason = ai_result.get("reason", "Нет ответа от модели.") + # 3. Техническая конвертация в BMP без геометрических проверок + def convert_to_bmp_only(src_path, dst_path): + with Image.open(src_path) as img: + img.convert("RGB").save(dst_path, format="BMP") + + await asyncio.to_thread(convert_to_bmp_only, raw_path, ready_path) + + # 4. ИИ-ПРОВЕРКА ЧЕРЕЗ GEMMA 4 VISION (srvkem-ii-03) + ai_res = await validate_photo_ai(raw_path) + + if ai_res["success"] and ai_res["approved"]: + # --- УСПЕШНАЯ ПРОВЕРКА ИИ --- + log_menu_stats(user_id, "Photo Bot", "Загрузка фото: Одобрено ИИ") + pending_photos[user_id] = ready_path + preview_path = f"/tmp/prev_{user_login}.jpg" + + try: + Image.open(ready_path).save(preview_path, format="JPEG") + p_main = FSInputFile(preview_path, filename="preview.jpg") + p_thumb = FSInputFile(preview_path, filename="preview_thumb.jpg") + await msg.answer_photo(p_main, p_thumb) + except Exception as lib_err: + await asyncio.to_thread(log_and_notify_photo_error, user_id, "Генерация превью", str(lib_err)) + finally: + if os.path.exists(preview_path): + os.remove(preview_path) + + await asyncio.sleep(0.5) + await msg.answer(photo_preview_ready(), parse_mode="html") - if not is_valid: - await msg.answer( - f"❌ [ИИ Отклонил]\n\nПричина: {reason}", - parse_mode="html" - ) else: + # --- ИИ ОТКЛОНИЛ ФОТО ИЛИ ПРОИЗОШЛА ОШИБКА --- + log_menu_stats(user_id, "Photo Bot", "Загрузка фото: Отклонено ИИ") + + reason_text = ai_res["text"] if ai_res["success"] else ai_res.get("error", "Не удалось проверить фото через ИИ.") + await msg.answer( - f"✅ [ИИ Одобрил]\n\nВердикт: Фотография полностью соответствует требованиям.", + f"❌ Фотография не прошла проверку\n\n{reason_text}", parse_mode="html" ) except Exception as e: - logger.exception(f"Ошибка при передаче фото в ИИ: {e}") await handle_photo_error(msg, user_id, "Скачивание и обработка файла", str(e)) finally: - if raw_data and isinstance(raw_data, str) and os.path.exists(raw_data): + if raw_path and os.path.exists(raw_path): try: - os.remove(raw_data) - except Exception: - pass + os.remove(raw_path) + except Exception as cleanup_err: + await asyncio.to_thread(log_and_notify_photo_error, user_id, "Очистка временных файлов", str(cleanup_err)) return +