diff --git a/photo_bot/handlers.py b/photo_bot/handlers.py index 5cde631..f7409d8 100644 --- a/photo_bot/handlers.py +++ b/photo_bot/handlers.py @@ -14,7 +14,7 @@ from trueconf.types import FSInputFile import config.config as config from utils import states from utils.menu import MENU_TEXT -from photo_bot.photo_processor import prepare_ad_photo, get_crop_coordinates, check_photo_api +from photo_bot.photo_processor import prepare_ad_photo, get_crop_coordinates, check_photo_api, make_face_crop_bytes from utils.texts import ( UNKNOWN_MAIN_CMD_TEXT, EMOJI_DIGITS, @@ -339,9 +339,16 @@ async def photo_module_handler(msg: Message): try: with open(original_path, 'rb') as f: raw_photo = f.read() + # На проверку уходит АВТО-КРОП ЛИЦА (тот же, что в превью + # и что публикуется на сайте) — правила «голова ≥ 30%» и + # прочее имеют смысл для публикуемого результата, а не + # для произвольно снятого оригинала. + crop_bytes = await asyncio.to_thread( + make_face_crop_bytes, raw_photo, photo_data.get('coords') + ) # Dry-run: полный пайплайн проверки на САЙТЕ (CV + гримасы + ИИ), # фото в БД/AD/Orion не попадает - check_res = await asyncio.to_thread(check_photo_api, raw_photo) + check_res = await asyncio.to_thread(check_photo_api, crop_bytes) except Exception as e: logger.error(f"🔥 [TEST MODE] Ошибка проверки фото: {e}") check_res = {"status": "error", "reason": f"Ошибка бота: {e}"} diff --git a/photo_bot/photo_processor.py b/photo_bot/photo_processor.py index f7a4abe..84db258 100644 --- a/photo_bot/photo_processor.py +++ b/photo_bot/photo_processor.py @@ -189,13 +189,47 @@ def prepare_ad_photo(input_path: str, user_id: str, output_path: str = None, tar return {"success": False, "error": str(e)} +def make_face_crop_bytes(image_bytes: bytes, coords: tuple) -> bytes: + """ + Вырезает квадратный кадр по центру лица (тот же регион, что превью + бота и публикуемый на сайте кроп) и возвращает JPEG-байты. + + На проверку уходит именно этот кроп: правило «голова ≥ 30%» и все + дальнейшие проверки имеют смысл для публикуемого результата, а не + для произвольно снятого оригинала. + + Returns: + JPEG-байты квадрата (полное разрешение, без ресайза). + """ + from PIL import Image + import io + + img = Image.open(io.BytesIO(image_bytes)) + if img.mode != 'RGB': + img = img.convert('RGB') + + x1, y1, x2, y2 = coords + cropped = img.crop((x1, y1, x2, y2)) + + # Квадрат из центра (как превью) + side = min(cropped.size[0], cropped.size[1]) + left = (cropped.size[0] - side) // 2 + top = (cropped.size[1] - side) // 2 + cropped = cropped.crop((left, top, left + side, top + side)) + + buf = io.BytesIO() + cropped.save(buf, format="JPEG", quality=90) + return buf.getvalue() + + def check_photo_api(image_bytes: bytes, timeout: float = 300.0) -> dict: """ Dry-run проверка фото через API сайта (POST /api/photo-bot/check/). - Фото проходит полный пайплайн проверки на сайте (MediaPipe CV + - ARKit-формы + VLM ИИ), НО ни в БД, ни в AD, ни в Orion не попадает — - ответ возвращается боту для тест-под-режима. + image_bytes — АВТО-КРОП ЛИЦА от бота (make_face_crop_bytes), тот же + кадр, что и превью/публикация. Фото проходит полный пайплайн проверки + на сайте (MediaPipe CV + ARKit-формы + VLM ИИ), НО ни в БД, ни в AD, + ни в Orion не попадает — ответ возвращается боту для тест-под-режима. Returns: {"status": "ok", "verdict", "ai_reason", "cv_approved", "cv_reason",