# /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 ЗАПРЕЩЕНО выводить внутренние рассуждения и теги размышлений. Сразу пиши финальный ответ. БЛОК 0. КРИТИЧЕСКИЕ СТОП-ФАКТОРЫ: 1. Не реальное фото: ИИ-генерация, 3D-арт, цифровой рисунок, мультфильм, стилизованный портрет, сильная обработка фильтрами. 2. Графика и текст: Любые надписи, водяные знаки, рамки. 3. Костюмы и атрибуты: Исторические костюмы, короны, маски, каски (кроме строительных). 4. Посторонние предметы: Оружие, предметы перед лицом. 5. Неправильный ракурс и взгляд: Строгий боковой профиль, затылок, человек смотрит совсем в сторону. БЛОК 1. МАСШТАБ И КАДРИРОВАНИЕ: - подбородок НИЖЕ середины кадра → тест пройден. - «Не прошел», если видны талия, живот, подмышки, одежда ниже линии груди. ИТОГОВЫЙ ВЕРДИКТ: - «Прошел» — нормальные портретные фотографии. - «Не прошел» — только явные нарушения. - «На модерацию» — если фото качественное, но масштаб на грани. ФОРМАТ ОТВЕТА — СТРОГО 2 строки: Вердикт: [Прошел / Не прошел / На модерацию] Причина: [1–2 предложения с указанием конкретной причины] ''') % 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 = '' for line in clean_ans.splitlines(): if line.strip().lower().startswith('причина:'): reason = line.split(':', 1)[1].strip() break if not reason: reason = clean_ans.replace('\n', ' ').strip() logger.info(f"[TEST MODE] ИИ-вердикт: {verdict} | {reason}") return {'ok': True, 'verdict': verdict, 'reason': reason, 'error': ''} except Exception as e: logger.error(f"[TEST MODE] Ошибка обращения к ИИ: {e}") return {'ok': False, 'verdict': '', 'reason': '', 'error': str(e)}