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)
# =========================================================