diff --git a/photo_bot/__pycache__/ai_validator.cpython-313.pyc b/photo_bot/__pycache__/ai_validator.cpython-313.pyc new file mode 100644 index 0000000..a1312d1 Binary files /dev/null and b/photo_bot/__pycache__/ai_validator.cpython-313.pyc differ diff --git a/photo_bot/__pycache__/handlers.cpython-313.pyc b/photo_bot/__pycache__/handlers.cpython-313.pyc index 688ef26..a80bad3 100644 Binary files a/photo_bot/__pycache__/handlers.cpython-313.pyc and b/photo_bot/__pycache__/handlers.cpython-313.pyc differ diff --git a/photo_bot/ai_validator.py b/photo_bot/ai_validator.py index 5a8096f..a3529c6 100644 --- a/photo_bot/ai_validator.py +++ b/photo_bot/ai_validator.py @@ -6,6 +6,7 @@ import base64 import io import json import logging +import time import urllib.request from PIL import Image @@ -55,8 +56,12 @@ def validate_photo_with_ai(file_bytes: bytes) -> dict: """ Прямая проверка фото ИИ (тестовый мод). - Возвращает {'ok': bool, 'verdict': str, 'reason': str, 'error': str} + Возвращает {'ok': bool, 'verdict': str, 'reason': str, 'error': str, + 'metrics': {'total_time', 'prompt_tokens', 'completion_tokens', + 'prompt_per_second', 'predicted_per_second'}, + 'error_time': float} """ + t0 = time.perf_counter() try: # Сжатие (PIL): разрешение настраивается в config (PHOTO_TEST_AI_MAX_SIDE) side = int(config.PHOTO_TEST_AI_MAX_SIDE) @@ -88,6 +93,18 @@ def validate_photo_with_ai(file_bytes: bytes) -> dict: with urllib.request.urlopen(req, timeout=config.PHOTO_TEST_AI_TIMEOUT) as resp: res = json.loads(resp.read().decode('utf-8')) + total_time = time.perf_counter() - t0 + + # llama.cpp в ответ кладёт usage (токены) и timings (скорости) + usage = res.get('usage') or {} + timings = res.get('timings') or {} + metrics = { + 'total_time': total_time, + 'prompt_tokens': usage.get('prompt_tokens'), + 'completion_tokens': usage.get('completion_tokens'), + 'prompt_per_second': timings.get('prompt_per_second'), + 'predicted_per_second': timings.get('predicted_per_second'), + } 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() @@ -114,10 +131,18 @@ def validate_photo_with_ai(file_bytes: bytes) -> dict: if not reason: reason = clean_ans.replace('\n', ' ').strip() - logger.info(f"[TEST MODE] ИИ-вердикт: {verdict} | мимика={mimi} | графика={grafika} | {reason}") + logger.info( + f"[TEST MODE] ИИ-вердикт: {verdict} | мимика={mimi} | графика={grafika} | {reason}" + f" | total={metrics['total_time']:.2f}s " + f"prompt={metrics['prompt_tokens']}tok/({metrics['prompt_per_second']:.1f} tps) " + f"gen={metrics['completion_tokens']}tok/({metrics['predicted_per_second']:.1f} tps)" + ) return {'ok': True, 'verdict': verdict, 'reason': reason, 'error': '', - 'mimi': mimi, 'grafika': grafika} + 'mimi': mimi, 'grafika': grafika, 'metrics': metrics} except Exception as e: - logger.error(f"[TEST MODE] Ошибка обращения к ИИ: {e}") - return {'ok': False, 'verdict': '', 'reason': '', 'error': str(e)} + err = f"{type(e).__name__}: {e}" + elapsed = time.perf_counter() - t0 + logger.error(f"[TEST MODE] Ошибка обращения к ИИ через {elapsed:.1f} сек: {err}") + return {'ok': False, 'verdict': '', 'reason': '', 'error': err, + 'error_time': elapsed} diff --git a/photo_bot/handlers.py b/photo_bot/handlers.py index e02371a..a44ddb4 100644 --- a/photo_bot/handlers.py +++ b/photo_bot/handlers.py @@ -323,7 +323,10 @@ async def photo_module_handler(msg: Message): if not ai_res['ok']: # Фото остаётся в pending — можно повторить проверку (кнопка 1) log_menu_stats(user_id, "Photo Bot (TEST)", "Ошибка обращения к ИИ") - await msg.answer(photo_test_ai_error(), parse_mode="html") + await msg.answer( + photo_test_ai_error(ai_res.get('error', ''), ai_res.get('error_time')), + parse_mode="html" + ) return verdict = ai_res['verdict'] @@ -333,7 +336,10 @@ async def photo_module_handler(msg: Message): log_menu_stats(user_id, "Photo Bot (TEST)", f"ИИ-вердикт: {verdict}") # Фото остаётся в pending — можно повторить проверку другим фото (кнопка 2) - await msg.answer(photo_test_result(verdict, reason, mimi, grafika), parse_mode="html") + await msg.answer( + photo_test_result(verdict, reason, mimi, grafika, ai_res.get('metrics')), + parse_mode="html" + ) return elif cmd == "2": diff --git a/utils/__pycache__/texts.cpython-313.pyc b/utils/__pycache__/texts.cpython-313.pyc index 82b9084..25ac25c 100644 Binary files a/utils/__pycache__/texts.cpython-313.pyc and b/utils/__pycache__/texts.cpython-313.pyc differ diff --git a/utils/texts.py b/utils/texts.py index 91c9a3e..387c710 100644 --- a/utils/texts.py +++ b/utils/texts.py @@ -309,7 +309,8 @@ def _photo_test_ai_checking(emojis: dict) -> str: def _photo_test_receiving(emojis: dict) -> str: return "🧪 Ищу лицо на фото (тестовый режим), пожалуйста, подождите..." -def _photo_test_result(verdict: str, reason: str, mimi: str = '', grafika: str = '', emojis: dict = None) -> str: +def _photo_test_result(verdict: str, reason: str, mimi: str = '', grafika: str = '', + ai_metrics: dict = None, emojis: dict = None) -> str: emojis = emojis or {} icons = {"Прошел": "✅", "Не прошел": "❌", "На модерацию": "⏳"} icon = icons.get(verdict, "ℹ️") @@ -322,19 +323,51 @@ def _photo_test_result(verdict: str, reason: str, mimi: str = '', grafika: str = detail = "\n".join(lines) + "\n\n" else: detail = "" + metrics_block = '' + if ai_metrics: + ml = ["📊 Технические показатели"] + tt = ai_metrics.get('total_time') + if tt is not None: + ml.append(f"⏱ Время ответа: {tt:.2f} сек") + pt = ai_metrics.get('prompt_tokens') + gps = ai_metrics.get('prompt_per_second') + if pt is not None: + s = f"⬇️ Приём (промпт): {pt} токенов" + if gps is not None: + s += f" — {gps:.1f} ток/сек" + ml.append(s) + ct = ai_metrics.get('completion_tokens') + ggs = ai_metrics.get('predicted_per_second') + if ct is not None: + s = f"⬆️ Отдача (генерация): {ct} токенов" + if ggs is not None: + s += f" — {ggs:.1f} ток/сек" + ml.append(s) + if len(ml) > 1: + metrics_block = "\n".join(ml) + "\n\n" return ( f"🧪 Тестовый режим: вердикт ИИ\n\n" f"{icon} Вердикт: {verdict}\n" f"{detail}" f"📝 Причина: {reason}\n\n" + f"{metrics_block}" f"⚠️ Тестовый режим: фото не публиковалось и никуда не отправлялось.\n\n" f"{emojis.get('2', '2')} — Загрузить другое фото\n" f"{emojis.get('0', '0')} — В главное меню" ) -def _photo_test_ai_error(emojis: dict) -> str: +def _photo_test_ai_error(emojis: dict, error: str = '', elapsed: float = None) -> str: + detail = '' + err = (error or '').strip() + if len(err) > 400: + err = err[:400] + '…' + if err: + detail += f"📋 Детали: {err}\n" + if elapsed is not None: + detail += f"⏱ Время до ошибки: {elapsed:.1f} сек\n" return ( f"❌ Ошибка обращения к ИИ.\n\n" + f"{detail}\n" f"Проверьте доступность ИИ-сервиса. Повторите попытку позже.\n\n" f"{emojis['1']} — Повторить проверку\n" f"{emojis['2']} — Загрузить другое фото\n" @@ -428,11 +461,12 @@ PHOTO_TEST_PREVIEW_TEXT = _photo_test_preview(EMOJI_DIGITS) PHOTO_TEST_AI_CHECKING_TEXT = _photo_test_ai_checking(EMOJI_DIGITS) PHOTO_TEST_RECEIVING_TEXT = _photo_test_receiving(EMOJI_DIGITS) -def photo_test_result(verdict: str, reason: str, mimi: str = '', grafika: str = '') -> str: - return _photo_test_result(verdict, reason, mimi, grafika, EMOJI_DIGITS) +def photo_test_result(verdict: str, reason: str, mimi: str = '', grafika: str = '', + ai_metrics: dict = None) -> str: + return _photo_test_result(verdict, reason, mimi, grafika, ai_metrics, EMOJI_DIGITS) -def photo_test_ai_error() -> str: - return _photo_test_ai_error(EMOJI_DIGITS) +def photo_test_ai_error(error: str = '', elapsed: float = None) -> str: + return _photo_test_ai_error(EMOJI_DIGITS, error, elapsed) # =========================================================