# /opt/trueconf_bot/photo_bot/ai_validator.py # Тестовый мод фото-бота: прямая проверка фото ИИ (llama.cpp). # Endpoint, модель и промпт — 1:1 из SibCemProfile/PhotoApp/photo_validator.py. # Фото никуда не отправляется: только в ИИ, ответ возвращается пользователю. import base64 import io import json import logging import urllib.request from PIL import Image import config.config as config logger = logging.getLogger(__name__) # Тег рассуждений thinking-моделей (конкатенация без литерала) THINK_CLOSE_TAG = '' def _build_prompt() -> str: """Промпт проверки фото (без подсказки MediaPipe — в тест-моде геометрия не считается).""" cv_hint = 'ℹ️ ПОДСКАЗКА ОТ ALGORITHM: Геометрическая проверка в тестовом режиме не выполняется, проверяй визуально.' return ('''Ты — строгий инспектор пропускной системы. Проверь фотографию сотрудника на соответствие регламенту. %s ЗАПРЕЩЕНО выводить внутренние рассуждения и теги размышлений. Сразу пиши финальный ответ. ПРЯМО СЕЙЧАС ВНИМАТЕЛЬНО РАССМОТРИ ФОТО И ПОСЛЕДОВАТЕЛЬНО ЗАФИКСИРУЙ: A) РЕАЛЬНОСТЬ: это живое фото, а не ИИ-генерация, рисунок, 3D, мультфильм, фильтр? B) ГРАФИКА: есть ли рамка, любые буквы/надписи/подписи (включая подпись с именем внизу), водяные знаки, декоративный фон с буквами, коллаж? C) МИМИКА: высунут ли язык? Скривлены ли губы? Есть ли гримаса, смешная рожа, щёлканье зубами, смех, скос глаз, закатанные глаза? D) РАКУРС: профиль, затылок, взгляд в сторону? E) МАСШТАБ: ниже линии груди видны ли живот, талия, подмышки? ПРАВИЛО ОТКАЗА: если по любому пункту A–E есть хоть малейшее отклонение — вердикт «Не прошел». Смех, ржа, рожи, высунутый язык, скривлённые губы — фото НЕ ПРОХОДИТ, даже если лицо анфас и хорошо освещено. Рамка или буквы/подпись на фоне или по краю кадра — фото НЕ ПРОХОДИТ. ИТОГОВЫЙ ВЕРДИКТ: - «Прошел» — только если все пункты A–E идеальны: нейтральное спокойное лицо, чистый кадр без рамки и букв. - «Не прошел» — любое отклонение, описанное выше. - «На модерацию» — только если фото реальное и лицо нейтральное, но масштаб или ракурс на грани нормы. ФОРМАТ ОТВЕТА — СТРОГО 4 строки, без других слов: Мимика: <нейтрально / высунут язык / гримаса / скривленная улыбка / смех — что именно видно> Графика: <нет / рамка / буквы / подпись / коллаж — что именно видно> Вердикт: [Прошел / Не прошел / На модерацию] Причина: [1–2 предложения с указанием конкретного пункта A–E] ''') % cv_hint def validate_photo_with_ai(file_bytes: bytes) -> dict: """ Прямая проверка фото ИИ (тестовый мод). Возвращает {'ok': bool, 'verdict': str, 'reason': str, 'error': str} """ try: # Сжатие как в photo_validator.py (PIL, без сторонних DLL) with Image.open(io.BytesIO(file_bytes)) as img: img = img.convert('RGB') img.thumbnail((768, 768)) buffer = io.BytesIO() img.save(buffer, format='JPEG', quality=85) b64_img = base64.b64encode(buffer.getvalue()).decode('utf-8') payload = { 'model': config.PHOTO_TEST_AI_MODEL, 'messages': [{ 'role': 'user', 'content': [ {'type': 'text', 'text': _build_prompt()}, {'type': 'image_url', 'image_url': {'url': 'data:image/jpeg;base64,' + b64_img}} ] }], 'temperature': 0.1, 'max_tokens': 1000 } req = urllib.request.Request( config.PHOTO_TEST_AI_URL, data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json'} ) with urllib.request.urlopen(req, timeout=config.PHOTO_TEST_AI_TIMEOUT) as resp: res = json.loads(resp.read().decode('utf-8')) full_content = (res['choices'][0]['message'].get('content') or '').strip() clean_ans = full_content.split(THINK_CLOSE_TAG)[-1].strip() if THINK_CLOSE_TAG in full_content else full_content.strip() text_lower = clean_ans.lower() if 'вердикт: прошел' in text_lower or ('прошел' in text_lower and 'не прошел' not in text_lower and 'модерац' not in text_lower): verdict = 'Прошел' elif 'на модерацию' in text_lower or 'модерац' in text_lower: verdict = 'На модерацию' else: verdict = 'Не прошел' reason = '' mimi = '' grafika = '' for line in clean_ans.splitlines(): ls = line.strip().lower() if ls.startswith('причина:'): reason = line.split(':', 1)[1].strip() elif ls.startswith('мимика:'): mimi = line.split(':', 1)[1].strip() elif ls.startswith('графика:'): grafika = line.split(':', 1)[1].strip() if not reason: reason = clean_ans.replace('\n', ' ').strip() logger.info(f"[TEST MODE] ИИ-вердикт: {verdict} | мимика={mimi} | графика={grafika} | {reason}") return {'ok': True, 'verdict': verdict, 'reason': reason, 'error': '', 'mimi': mimi, 'grafika': grafika} except Exception as e: logger.error(f"[TEST MODE] Ошибка обращения к ИИ: {e}") return {'ok': False, 'verdict': '', 'reason': '', 'error': str(e)}