From 78dc7f4810359188969fbf7421effbbd7583c597 Mon Sep 17 00:00:00 2001
From: itexpert228 <67105314+fdaser1337@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:47:56 +0300
Subject: [PATCH 01/17] pisi i sisi
---
games/nude.py | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++
main.py | 3 ++
2 files changed, 87 insertions(+)
create mode 100644 games/nude.py
diff --git a/games/nude.py b/games/nude.py
new file mode 100644
index 0000000..5dfe692
--- /dev/null
+++ b/games/nude.py
@@ -0,0 +1,84 @@
+import aiohttp
+from aiogram import types
+import config
+import logging
+import random
+import json
+
+logger = logging.getLogger(__name__)
+
+CAPTIONS = [
+ "Hot stuff 🔥",
+ "Sexy content 😏"
+]
+
+async def handle_nude_cmd(message: types.Message):
+ args = message.text.split(maxsplit=1)
+
+ if len(args) > 1 and args[1].strip() == "-h":
+ help_msg = (
+ "🔥 Справка по команде /nude:\n\n"
+ "Получает случайные изображения взрослых женщин из открытых источников.\n"
+ "• Использует Pornhub API для поиска контента\n"
+ "• По умолчанию ищет популярные видео с высоким рейтингом\n"
+ "• Отправляет превью видео как изображение\n\n"
+ "Пример: /nude\n"
+ "По умолчанию: Популярный контент для взрослых"
+ )
+ await message.reply(help_msg, parse_mode="HTML")
+ return
+
+ # Используем Pornhub API для поиска видео
+ url = "https://www.pornhub.com/webmasters/search"
+
+ params = {
+ "category": "straight",
+ "ordering": "mostviewed",
+ "period": "week",
+ "thumbsize": "medium"
+ }
+
+ headers = {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+ }
+
+ try:
+ async with aiohttp.ClientSession() as session:
+ async with session.get(url, params=params, headers=headers) as resp:
+ if resp.status != 200:
+ logger.error(f"Pornhub API Error: {resp.status}")
+ await message.reply("Не удалось получить контент от сервера :(")
+ return
+
+ # Pornhub API возвращает HTML, нужно парсить
+ text = await resp.text()
+
+ # Ищем URL превью в HTML
+ import re
+ thumb_pattern = r'
]+src="([^"]+)"[^>]+class="thumb"'
+ matches = re.findall(thumb_pattern, text)
+
+ if not matches:
+ await message.reply("Не удалось найти контент :(")
+ return
+
+ # Берем случайное превью
+ thumb_url = random.choice(matches)
+
+ # Убедимся что URL полный
+ if not thumb_url.startswith('http'):
+ thumb_url = f"https://thumb.pornhub.com{thumb_url}"
+
+ caption = random.choice(CAPTIONS)
+
+ # Отправляем как фото
+ try:
+ input_file = types.URLInputFile(thumb_url)
+ await message.answer_photo(photo=input_file, caption=caption)
+ except Exception as e:
+ logger.error(f"Error sending photo: {e}")
+ await message.answer(f"{caption}\n{thumb_url}")
+
+ except Exception as e:
+ logger.exception("Error in nude command")
+ await message.reply("Произошла ошибка при обращении к API :(")
diff --git a/main.py b/main.py
index 7ee496c..7891bde 100644
--- a/main.py
+++ b/main.py
@@ -23,6 +23,7 @@ from AI.talk_handler import handle_talk, generate_autoreply, push_message
from games.casino import play_casino
from games.fortune import generate_fortune
from games.uwu import handle_uwu_cmd
+from games.nude import handle_nude_cmd
from games.betting import (
clear_user_sport_context,
debug_sports,
@@ -1417,6 +1418,7 @@ def main():
dp.message.register(handle_sports_debug_cmd, Command("sports_debug"))
dp.message.register(handle_gen_mem, Command("gen_mem"))
dp.message.register(handle_uwu_cmd, Command("uwu"))
+ dp.message.register(handle_nude_cmd, Command("nude"))
dp.message.register(handle_keywords, F.text)
async def on_startup(bot: Bot):
@@ -1446,6 +1448,7 @@ def main():
BotCommand(command="sports_debug", description="диагностика API матчей"),
BotCommand(command="svodka", description="СВО: итоги"),
BotCommand(command="uwu", description="Случайная картинка с e621"),
+ BotCommand(command="nude", description="Голые женщины из открытых источников"),
]
scopes = (
BotCommandScopeDefault(),
From 6433d9c11e4ec5f007e03d7150662aa95a4ddb0b Mon Sep 17 00:00:00 2001
From: itexpert228 <67105314+fdaser1337@users.noreply.github.com>
Date: Fri, 10 Apr 2026 10:56:19 +0300
Subject: [PATCH 02/17] suki
---
games/nude.py | 104 +++++++++++++++++++++++++++++++-------------------
1 file changed, 64 insertions(+), 40 deletions(-)
diff --git a/games/nude.py b/games/nude.py
index 5dfe692..280bbf4 100644
--- a/games/nude.py
+++ b/games/nude.py
@@ -3,7 +3,7 @@ from aiogram import types
import config
import logging
import random
-import json
+import re
logger = logging.getLogger(__name__)
@@ -19,66 +19,90 @@ async def handle_nude_cmd(message: types.Message):
help_msg = (
"🔥 Справка по команде /nude:\n\n"
"Получает случайные изображения взрослых женщин из открытых источников.\n"
- "• Использует Pornhub API для поиска контента\n"
- "• По умолчанию ищет популярные видео с высоким рейтингом\n"
- "• Отправляет превью видео как изображение\n\n"
+ "• Использует прямое сканирование сайтов для поиска контента\n"
+ "• По умолчанию ищет популярные изображения\n"
+ "• Отправляет изображения напрямую\n\n"
"Пример: /nude\n"
- "По умолчанию: Популярный контент для взрослых"
+ "По умолчанию: Случайные изображения с популярных сайтов"
)
await message.reply(help_msg, parse_mode="HTML")
return
- # Используем Pornhub API для поиска видео
- url = "https://www.pornhub.com/webmasters/search"
+ # Список сайтов для сканирования
+ sites = [
+ "https://www.imagefap.com/gallery.php?pg=random",
+ "https://xhamster.com/photos?sort=random",
+ "https://spankbang.com/random"
+ ]
- params = {
- "category": "straight",
- "ordering": "mostviewed",
- "period": "week",
- "thumbsize": "medium"
- }
+ random_site = random.choice(sites)
headers = {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Language": "en-US,en;q=0.5",
+ "Accept-Encoding": "gzip, deflate",
+ "Connection": "keep-alive",
+ "Upgrade-Insecure-Requests": "1"
}
try:
async with aiohttp.ClientSession() as session:
- async with session.get(url, params=params, headers=headers) as resp:
+ async with session.get(random_site, headers=headers) as resp:
if resp.status != 200:
- logger.error(f"Pornhub API Error: {resp.status}")
- await message.reply("Не удалось получить контент от сервера :(")
+ logger.error(f"Site access error: {resp.status}")
+ await message.reply("Не удалось получить доступ к сайту :(")
return
- # Pornhub API возвращает HTML, нужно парсить
- text = await resp.text()
+ html = await resp.text()
- # Ищем URL превью в HTML
- import re
- thumb_pattern = r'
]+src="([^"]+)"[^>]+class="thumb"'
- matches = re.findall(thumb_pattern, text)
+ # Ищем URL изображений в HTML
+ img_patterns = [
+ r'
]+src="([^"]+)"',
+ r']+href="([^"]+\.(jpg|jpeg|png|gif|webp))"',
+ r'data-src="([^"]+)"'
+ ]
- if not matches:
- await message.reply("Не удалось найти контент :(")
+ img_urls = []
+ for pattern in img_patterns:
+ matches = re.findall(pattern, html, re.IGNORECASE)
+ img_urls.extend(matches)
+
+ if not img_urls:
+ await message.reply("Изображения не найдены на странице :(")
return
- # Берем случайное превью
- thumb_url = random.choice(matches)
+ # Фильтруем и выбираем случайное изображение
+ valid_imgs = []
+ for img_url in img_urls:
+ if img_url.startswith('//'):
+ img_url = 'https:' + img_url
+ elif img_url.startswith('/'):
+ if 'imagefap.com' in random_site:
+ img_url = 'https://www.imagefap.com' + img_url
+ elif 'xhamster.com' in random_site:
+ img_url = 'https://xhamster.com' + img_url
+ elif 'spankbang.com' in random_site:
+ img_url = 'https://spankbang.com' + img_url
+
+ if any(ext in img_url.lower() for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
+ valid_imgs.append(img_url)
- # Убедимся что URL полный
- if not thumb_url.startswith('http'):
- thumb_url = f"https://thumb.pornhub.com{thumb_url}"
+ if not valid_imgs:
+ await message.reply("Подходящие изображения не найдены :(")
+ return
- caption = random.choice(CAPTIONS)
-
- # Отправляем как фото
- try:
- input_file = types.URLInputFile(thumb_url)
- await message.answer_photo(photo=input_file, caption=caption)
- except Exception as e:
- logger.error(f"Error sending photo: {e}")
- await message.answer(f"{caption}\n{thumb_url}")
+ final_img_url = random.choice(valid_imgs)
+ caption = random.choice(CAPTIONS)
+
+ # Отправляем изображение
+ try:
+ input_file = types.URLInputFile(final_img_url)
+ await message.answer_photo(photo=input_file, caption=caption)
+ except Exception as e:
+ logger.error(f"Error sending photo: {e}")
+ await message.answer(f"{caption}\n{final_img_url}")
except Exception as e:
logger.exception("Error in nude command")
- await message.reply("Произошла ошибка при обращении к API :(")
+ await message.reply("Произошла ошибка при обработке запроса :(")
From 7afd8ca21644fc36de8de6f641c25e12b5fa17d7 Mon Sep 17 00:00:00 2001
From: q
Date: Fri, 10 Apr 2026 11:17:45 +0300
Subject: [PATCH 03/17] some fix for nude
---
games/nude.py | 227 +++++++++++++++++++++++++++++++-------------------
1 file changed, 139 insertions(+), 88 deletions(-)
diff --git a/games/nude.py b/games/nude.py
index 280bbf4..71ac438 100644
--- a/games/nude.py
+++ b/games/nude.py
@@ -1,108 +1,159 @@
-import aiohttp
-from aiogram import types
-import config
import logging
import random
-import re
+from pathlib import PurePosixPath
+from urllib.parse import urlparse
+
+import aiohttp
+from aiogram import types
+
logger = logging.getLogger(__name__)
CAPTIONS = [
"Hot stuff 🔥",
- "Sexy content 😏"
+ "Sexy content 😏",
]
+LESAINT_POSTS_API = "https://lesaintdesseins.fr/wp-json/wp/v2/posts"
+LESAINT_REFERER = "https://lesaintdesseins.fr/"
+LATEST_POSTS_LIMIT = 30
+REQUEST_HEADERS = {
+ "User-Agent": (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/124.0.0.0 Safari/537.36"
+ ),
+ "Accept": "application/json,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Language": "en-US,en;q=0.9",
+ "Referer": LESAINT_REFERER,
+}
+
+
+def _extract_photo_candidates(posts: list[dict]) -> list[dict]:
+ candidates = []
+
+ for post in posts:
+ embedded = post.get("_embedded") or {}
+ media_items = embedded.get("wp:featuredmedia") or []
+
+ for media in media_items:
+ if media.get("media_type") != "image":
+ continue
+
+ media_details = media.get("media_details") or {}
+ sizes = media_details.get("sizes") or {}
+ image_url = (
+ (sizes.get("full") or {}).get("source_url")
+ or media.get("source_url")
+ )
+
+ if not image_url:
+ continue
+
+ candidates.append(
+ {
+ "image_url": image_url,
+ "post_url": post.get("link", LESAINT_REFERER),
+ "title": (post.get("title") or {}).get("rendered", "").strip(),
+ }
+ )
+ break
+
+ return candidates
+
+
+def _guess_filename(image_url: str) -> str:
+ parsed = urlparse(image_url)
+ filename = PurePosixPath(parsed.path).name or "nude.jpg"
+
+ if "." not in filename:
+ filename = f"{filename}.jpg"
+
+ return filename
+
+
+async def _fetch_latest_photo_candidates(session: aiohttp.ClientSession) -> list[dict]:
+ params = {
+ "per_page": str(LATEST_POSTS_LIMIT),
+ "orderby": "date",
+ "order": "desc",
+ "_embed": "wp:featuredmedia",
+ }
+
+ async with session.get(LESAINT_POSTS_API, params=params, headers=REQUEST_HEADERS) as resp:
+ if resp.status != 200:
+ body = await resp.text()
+ raise RuntimeError(f"API returned {resp.status}: {body[:300]}")
+
+ posts = await resp.json(content_type=None)
+
+ if not isinstance(posts, list):
+ raise RuntimeError("Unexpected API response format")
+
+ return _extract_photo_candidates(posts)
+
+
+async def _download_photo(
+ session: aiohttp.ClientSession, image_url: str
+) -> tuple[bytes | None, str]:
+ async with session.get(image_url, headers=REQUEST_HEADERS) as resp:
+ if resp.status != 200:
+ logger.warning("Image download failed for %s: %s", image_url, resp.status)
+ return None, _guess_filename(image_url)
+
+ content_type = resp.headers.get("Content-Type", "")
+ if "image" not in content_type.lower():
+ logger.warning(
+ "Unexpected content type for %s: %s", image_url, content_type
+ )
+ return None, _guess_filename(image_url)
+
+ return await resp.read(), _guess_filename(image_url)
+
+
async def handle_nude_cmd(message: types.Message):
args = message.text.split(maxsplit=1)
-
+
if len(args) > 1 and args[1].strip() == "-h":
help_msg = (
"🔥 Справка по команде /nude:\n\n"
- "Получает случайные изображения взрослых женщин из открытых источников.\n"
- "• Использует прямое сканирование сайтов для поиска контента\n"
- "• По умолчанию ищет популярные изображения\n"
- "• Отправляет изображения напрямую\n\n"
- "Пример: /nude\n"
- "По умолчанию: Случайные изображения с популярных сайтов"
+ "Берет случайную фотку из последних постов сайта le saint des seins.\n"
+ "• Источник: https://lesaintdesseins.fr/\n"
+ "• Достает последние посты через WordPress REST API\n"
+ "• Выбирает случайную картинку и отправляет ее прямо в Telegram\n\n"
+ "Пример: /nude"
)
await message.reply(help_msg, parse_mode="HTML")
return
- # Список сайтов для сканирования
- sites = [
- "https://www.imagefap.com/gallery.php?pg=random",
- "https://xhamster.com/photos?sort=random",
- "https://spankbang.com/random"
- ]
-
- random_site = random.choice(sites)
-
- headers = {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
- "Accept-Language": "en-US,en;q=0.5",
- "Accept-Encoding": "gzip, deflate",
- "Connection": "keep-alive",
- "Upgrade-Insecure-Requests": "1"
- }
-
+ timeout = aiohttp.ClientTimeout(total=30)
+
try:
- async with aiohttp.ClientSession() as session:
- async with session.get(random_site, headers=headers) as resp:
- if resp.status != 200:
- logger.error(f"Site access error: {resp.status}")
- await message.reply("Не удалось получить доступ к сайту :(")
- return
-
- html = await resp.text()
-
- # Ищем URL изображений в HTML
- img_patterns = [
- r'
]+src="([^"]+)"',
- r']+href="([^"]+\.(jpg|jpeg|png|gif|webp))"',
- r'data-src="([^"]+)"'
- ]
-
- img_urls = []
- for pattern in img_patterns:
- matches = re.findall(pattern, html, re.IGNORECASE)
- img_urls.extend(matches)
-
- if not img_urls:
- await message.reply("Изображения не найдены на странице :(")
- return
-
- # Фильтруем и выбираем случайное изображение
- valid_imgs = []
- for img_url in img_urls:
- if img_url.startswith('//'):
- img_url = 'https:' + img_url
- elif img_url.startswith('/'):
- if 'imagefap.com' in random_site:
- img_url = 'https://www.imagefap.com' + img_url
- elif 'xhamster.com' in random_site:
- img_url = 'https://xhamster.com' + img_url
- elif 'spankbang.com' in random_site:
- img_url = 'https://spankbang.com' + img_url
-
- if any(ext in img_url.lower() for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
- valid_imgs.append(img_url)
-
- if not valid_imgs:
- await message.reply("Подходящие изображения не найдены :(")
- return
-
- final_img_url = random.choice(valid_imgs)
- caption = random.choice(CAPTIONS)
-
- # Отправляем изображение
- try:
- input_file = types.URLInputFile(final_img_url)
- await message.answer_photo(photo=input_file, caption=caption)
- except Exception as e:
- logger.error(f"Error sending photo: {e}")
- await message.answer(f"{caption}\n{final_img_url}")
-
- except Exception as e:
+ async with aiohttp.ClientSession(timeout=timeout) as session:
+ candidates = await _fetch_latest_photo_candidates(session)
+
+ if not candidates:
+ await message.reply("Не удалось найти свежие фотки :(")
+ return
+
+ selected = random.choice(candidates)
+ caption = random.choice(CAPTIONS)
+ image_bytes, filename = await _download_photo(session, selected["image_url"])
+
+ if image_bytes:
+ photo = types.BufferedInputFile(image_bytes, filename=filename)
+ await message.answer_photo(photo=photo, caption=caption)
+ return
+
+ try:
+ photo = types.URLInputFile(selected["image_url"])
+ await message.answer_photo(photo=photo, caption=caption)
+ except Exception:
+ logger.exception("Failed to send photo by URL")
+ await message.answer(
+ f"{caption}\n{selected['image_url']}\n{selected['post_url']}"
+ )
+
+ except Exception:
logger.exception("Error in nude command")
- await message.reply("Произошла ошибка при обработке запроса :(")
+ await message.reply("Произошла ошибка при получении фото :(")
From 53b6fa72ae9242d124ae73eb419228ba1b3df04c Mon Sep 17 00:00:00 2001
From: q
Date: Fri, 10 Apr 2026 11:20:14 +0300
Subject: [PATCH 04/17] some fix for nude change word
---
games/nude.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/games/nude.py b/games/nude.py
index 71ac438..0e0a0fd 100644
--- a/games/nude.py
+++ b/games/nude.py
@@ -10,8 +10,9 @@ from aiogram import types
logger = logging.getLogger(__name__)
CAPTIONS = [
- "Hot stuff 🔥",
- "Sexy content 😏",
+ "Шо ты блять псюнчик решил свой подергать?",
+ "Решил подрочить сука?",
+ "гнойный писюн у тебя кнч, да",
]
LESAINT_POSTS_API = "https://lesaintdesseins.fr/wp-json/wp/v2/posts"
From eeed9af5451f6de0e00a8559a5f7bf42082e04ee Mon Sep 17 00:00:00 2001
From: q
Date: Fri, 10 Apr 2026 12:01:20 +0300
Subject: [PATCH 05/17] some fix for nude save db with history with pic
---
games/nude.py | 145 ++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 136 insertions(+), 9 deletions(-)
diff --git a/games/nude.py b/games/nude.py
index 0e0a0fd..4c37767 100644
--- a/games/nude.py
+++ b/games/nude.py
@@ -1,3 +1,5 @@
+import asyncio
+import json
import logging
import random
from pathlib import PurePosixPath
@@ -6,6 +8,8 @@ from urllib.parse import urlparse
import aiohttp
from aiogram import types
+import config
+
logger = logging.getLogger(__name__)
@@ -17,7 +21,6 @@ CAPTIONS = [
LESAINT_POSTS_API = "https://lesaintdesseins.fr/wp-json/wp/v2/posts"
LESAINT_REFERER = "https://lesaintdesseins.fr/"
-LATEST_POSTS_LIMIT = 30
REQUEST_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
@@ -29,9 +32,12 @@ REQUEST_HEADERS = {
"Referer": LESAINT_REFERER,
}
+_NUDE_HISTORY_LOCK = asyncio.Lock()
+
def _extract_photo_candidates(posts: list[dict]) -> list[dict]:
candidates = []
+ seen_urls = set()
for post in posts:
embedded = post.get("_embedded") or {}
@@ -48,9 +54,10 @@ def _extract_photo_candidates(posts: list[dict]) -> list[dict]:
or media.get("source_url")
)
- if not image_url:
+ if not image_url or image_url in seen_urls:
continue
+ seen_urls.add(image_url)
candidates.append(
{
"image_url": image_url,
@@ -73,25 +80,130 @@ def _guess_filename(image_url: str) -> str:
return filename
-async def _fetch_latest_photo_candidates(session: aiohttp.ClientSession) -> list[dict]:
+def _load_nude_history() -> dict:
+ path = config.NUDE_HISTORY_PATH
+ if not path.exists():
+ return {"sent_images": []}
+
+ try:
+ with path.open("r", encoding="utf-8") as fh:
+ data = json.load(fh)
+ except (OSError, json.JSONDecodeError):
+ logger.exception("Failed to load nude history")
+ return {"sent_images": []}
+
+ if not isinstance(data, dict):
+ return {"sent_images": []}
+
+ sent_images = data.get("sent_images")
+ if not isinstance(sent_images, list):
+ sent_images = []
+
+ cleaned = [item for item in sent_images if isinstance(item, str) and item.strip()]
+ return {"sent_images": cleaned[-config.NUDE_HISTORY_MAX_SIZE :]}
+
+
+def _save_nude_history(data: dict) -> None:
+ path = config.NUDE_HISTORY_PATH
+ path.parent.mkdir(parents=True, exist_ok=True)
+ tmp_path = path.with_name(f"{path.name}.tmp")
+ with tmp_path.open("w", encoding="utf-8") as fh:
+ json.dump(data, fh, ensure_ascii=False, indent=2)
+ tmp_path.replace(path)
+
+
+def _remember_sent_image(history: dict, image_url: str) -> None:
+ sent_images = history.get("sent_images")
+ if not isinstance(sent_images, list):
+ sent_images = []
+
+ sent_images = [item for item in sent_images if item != image_url]
+ sent_images.append(image_url)
+ history["sent_images"] = sent_images[-config.NUDE_HISTORY_MAX_SIZE :]
+
+
+async def _fetch_posts_page(
+ session: aiohttp.ClientSession, page: int, per_page: int
+) -> tuple[list[dict], int | None]:
params = {
- "per_page": str(LATEST_POSTS_LIMIT),
+ "page": str(page),
+ "per_page": str(per_page),
"orderby": "date",
"order": "desc",
"_embed": "wp:featuredmedia",
}
async with session.get(LESAINT_POSTS_API, params=params, headers=REQUEST_HEADERS) as resp:
+ if resp.status == 400 and page > 1:
+ body = await resp.text()
+ if "rest_post_invalid_page_number" in body:
+ return [], None
+ raise RuntimeError(f"API returned 400: {body[:300]}")
+
if resp.status != 200:
body = await resp.text()
raise RuntimeError(f"API returned {resp.status}: {body[:300]}")
+ total_pages_header = resp.headers.get("X-WP-TotalPages")
+ total_pages = (
+ int(total_pages_header)
+ if total_pages_header and total_pages_header.isdigit()
+ else None
+ )
posts = await resp.json(content_type=None)
if not isinstance(posts, list):
raise RuntimeError("Unexpected API response format")
- return _extract_photo_candidates(posts)
+ return posts, total_pages
+
+
+async def _fetch_latest_photo_candidates(
+ session: aiohttp.ClientSession, sent_urls: set[str]
+) -> list[dict]:
+ per_page = max(1, min(config.NUDE_POSTS_PER_PAGE, 100))
+ max_scan = max(config.NUDE_POSTS_POOL_TARGET, config.NUDE_MAX_POSTS_TO_SCAN)
+ max_pages = max(1, (max_scan + per_page - 1) // per_page)
+
+ candidates = []
+ seen_urls = set()
+ unseen_count = 0
+ total_pages = None
+
+ for page in range(1, max_pages + 1):
+ if total_pages is not None and page > total_pages:
+ break
+
+ posts, reported_total_pages = await _fetch_posts_page(session, page, per_page)
+ if reported_total_pages is not None:
+ total_pages = reported_total_pages
+ if not posts:
+ break
+
+ for candidate in _extract_photo_candidates(posts):
+ image_url = candidate["image_url"]
+ if image_url in seen_urls:
+ continue
+
+ seen_urls.add(image_url)
+ candidates.append(candidate)
+
+ if image_url not in sent_urls:
+ unseen_count += 1
+
+ if len(candidates) >= config.NUDE_POSTS_POOL_TARGET and unseen_count >= config.NUDE_MIN_UNSEEN_POOL:
+ break
+ if len(candidates) >= max_scan:
+ break
+
+ return candidates
+
+
+def _pick_unsent_candidate(candidates: list[dict], sent_urls: set[str]) -> dict | None:
+ unseen = [candidate for candidate in candidates if candidate["image_url"] not in sent_urls]
+ if not unseen:
+ return None
+ return random.choice(unseen)
async def _download_photo(
@@ -120,24 +232,39 @@ async def handle_nude_cmd(message: types.Message):
"🔥 Справка по команде /nude:\n\n"
"Берет случайную фотку из последних постов сайта le saint des seins.\n"
"• Источник: https://lesaintdesseins.fr/\n"
- "• Достает последние посты через WordPress REST API\n"
+ "• Пагинацией тянет 1000+ последних постов через WordPress REST API\n"
+ "• Хранит историю отправок и старается не повторять фотки\n"
"• Выбирает случайную картинку и отправляет ее прямо в Telegram\n\n"
"Пример: /nude"
)
await message.reply(help_msg, parse_mode="HTML")
return
- timeout = aiohttp.ClientTimeout(total=30)
+ timeout = aiohttp.ClientTimeout(total=60)
+ history_snapshot = _load_nude_history()
+ sent_urls_snapshot = set(history_snapshot["sent_images"])
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
- candidates = await _fetch_latest_photo_candidates(session)
+ candidates = await _fetch_latest_photo_candidates(session, sent_urls_snapshot)
if not candidates:
await message.reply("Не удалось найти свежие фотки :(")
return
- selected = random.choice(candidates)
+ async with _NUDE_HISTORY_LOCK:
+ history = _load_nude_history()
+ sent_urls = set(history["sent_images"])
+ selected = _pick_unsent_candidate(candidates, sent_urls)
+
+ if selected is None:
+ logger.info("Nude history exhausted current pool, resetting history")
+ history = {"sent_images": []}
+ selected = random.choice(candidates)
+
+ _remember_sent_image(history, selected["image_url"])
+ _save_nude_history(history)
+
caption = random.choice(CAPTIONS)
image_bytes, filename = await _download_photo(session, selected["image_url"])
From 9f8f767ae5f43dc9734f6549cea3d65be6072c8a Mon Sep 17 00:00:00 2001
From: q
Date: Fri, 10 Apr 2026 12:14:20 +0300
Subject: [PATCH 06/17] some fix for nude save db with
---
games/nude.py | 48 ++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 40 insertions(+), 8 deletions(-)
diff --git a/games/nude.py b/games/nude.py
index 4c37767..46e4c36 100644
--- a/games/nude.py
+++ b/games/nude.py
@@ -1,8 +1,9 @@
import asyncio
import json
import logging
+import os
import random
-from pathlib import PurePosixPath
+from pathlib import Path, PurePosixPath
from urllib.parse import urlparse
import aiohttp
@@ -35,6 +36,37 @@ REQUEST_HEADERS = {
_NUDE_HISTORY_LOCK = asyncio.Lock()
+def _resolve_int_setting(name: str, default: int) -> int:
+ value = getattr(config, name, None)
+ if value is None:
+ value = os.getenv(name, str(default))
+
+ try:
+ return int(value)
+ except (TypeError, ValueError):
+ logger.warning("Invalid %s=%r, using default %s", name, value, default)
+ return default
+
+
+def _resolve_path_setting(name: str, default: str) -> Path:
+ value = getattr(config, name, None)
+ if value is None:
+ value = os.getenv(name, default)
+
+ if isinstance(value, Path):
+ return value
+
+ return Path(str(value))
+
+
+NUDE_HISTORY_PATH = _resolve_path_setting("NUDE_HISTORY_PATH", "/db/nude_history.json")
+NUDE_POSTS_PER_PAGE = _resolve_int_setting("NUDE_POSTS_PER_PAGE", 100)
+NUDE_POSTS_POOL_TARGET = _resolve_int_setting("NUDE_POSTS_POOL_TARGET", 1200)
+NUDE_MAX_POSTS_TO_SCAN = _resolve_int_setting("NUDE_MAX_POSTS_TO_SCAN", 2000)
+NUDE_MIN_UNSEEN_POOL = _resolve_int_setting("NUDE_MIN_UNSEEN_POOL", 100)
+NUDE_HISTORY_MAX_SIZE = _resolve_int_setting("NUDE_HISTORY_MAX_SIZE", 5000)
+
+
def _extract_photo_candidates(posts: list[dict]) -> list[dict]:
candidates = []
seen_urls = set()
@@ -81,7 +113,7 @@ def _guess_filename(image_url: str) -> str:
def _load_nude_history() -> dict:
- path = config.NUDE_HISTORY_PATH
+ path = NUDE_HISTORY_PATH
if not path.exists():
return {"sent_images": []}
@@ -100,11 +132,11 @@ def _load_nude_history() -> dict:
sent_images = []
cleaned = [item for item in sent_images if isinstance(item, str) and item.strip()]
- return {"sent_images": cleaned[-config.NUDE_HISTORY_MAX_SIZE :]}
+ return {"sent_images": cleaned[-NUDE_HISTORY_MAX_SIZE :]}
def _save_nude_history(data: dict) -> None:
- path = config.NUDE_HISTORY_PATH
+ path = NUDE_HISTORY_PATH
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_name(f"{path.name}.tmp")
with tmp_path.open("w", encoding="utf-8") as fh:
@@ -119,7 +151,7 @@ def _remember_sent_image(history: dict, image_url: str) -> None:
sent_images = [item for item in sent_images if item != image_url]
sent_images.append(image_url)
- history["sent_images"] = sent_images[-config.NUDE_HISTORY_MAX_SIZE :]
+ history["sent_images"] = sent_images[-NUDE_HISTORY_MAX_SIZE :]
async def _fetch_posts_page(
@@ -161,8 +193,8 @@ async def _fetch_posts_page(
async def _fetch_latest_photo_candidates(
session: aiohttp.ClientSession, sent_urls: set[str]
) -> list[dict]:
- per_page = max(1, min(config.NUDE_POSTS_PER_PAGE, 100))
- max_scan = max(config.NUDE_POSTS_POOL_TARGET, config.NUDE_MAX_POSTS_TO_SCAN)
+ per_page = max(1, min(NUDE_POSTS_PER_PAGE, 100))
+ max_scan = max(NUDE_POSTS_POOL_TARGET, NUDE_MAX_POSTS_TO_SCAN)
max_pages = max(1, (max_scan + per_page - 1) // per_page)
candidates = []
@@ -191,7 +223,7 @@ async def _fetch_latest_photo_candidates(
if image_url not in sent_urls:
unseen_count += 1
- if len(candidates) >= config.NUDE_POSTS_POOL_TARGET and unseen_count >= config.NUDE_MIN_UNSEEN_POOL:
+ if len(candidates) >= NUDE_POSTS_POOL_TARGET and unseen_count >= NUDE_MIN_UNSEEN_POOL:
break
if len(candidates) >= max_scan:
break
From 4d0a3841b7ed03ad558be44e377c7b7a5ab87155 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 16:15:14 +0300
Subject: [PATCH 07/17] update ai dolboeba
---
AI/talk_handler.py | 710 +++++++++++++++++++++++++++++++++------------
README.md | 1 +
config.py | 8 +
main.py | 24 +-
4 files changed, 543 insertions(+), 200 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 2758c97..6139bd7 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -1,7 +1,12 @@
+from __future__ import annotations
+
import asyncio
-import os
import logging
+import os
+import random
+import re
import sqlite3
+import time
from pathlib import Path
import aiohttp
@@ -9,236 +14,585 @@ from aiogram.types import Message
logger = logging.getLogger(__name__)
-# Адрес OpenAI-совместимого API у llama.cpp сервера.
+# Основные настройки поведения и стиля бота редактируются здесь.
LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-ai/")
-# ПЖ НЕ ДЕЛАЙТЕ ДУДОС
-# Системный промпт задаёт стиль и "характер" ответа.
-SYSTEM_PROMPT = (
- "Ты — дерзкий, мемный, гиперэмоциональный пацан с района. Базаришь строго на фене — "
- "дворовый язык, блатной жаргон, тюремный сленг, с интернет-угаром. "
- "Тон: коротко, резко, уверенно, иногда абсурдно, но по делу. "
- "Стиль: 1–3 предложения; если список — максимум 3 пункта. "
- "Не используй формальный язык, канцелярит, вежливые обороты. Не ломай персонажа.\n\n"
+DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH") or str(Path(__file__).resolve().with_name("chat_history.sqlite3"))
+BOT_MEMORY_NAME = os.getenv("BOT_MEMORY_NAME", "бот")
+SKIP_TOKEN = ""
- "ЖЁСТКИЙ ЗАПРЕТ: не произноси фразы «Ну ты точно в ударе, братан» и "
- "«Все мы пацаны, так что не давай себя обмануть..» (а также любые их вариации или "
- "перефраз). Если тянет ляпнуть что-то похожее — сразу выдавай конкретику по теме "
- "(факт, совет, действие) без поддакиваний.\n\n"
+RECENT_MESSAGES_LIMIT = 14
+SUMMARY_TRIGGER_MESSAGES = 24
+SUMMARY_BATCH_MESSAGES = 20
+PROMPT_CHAR_BUDGET = 6_500
+SUMMARY_CHAR_BUDGET = 1_800
+MAX_INPUT_CHARS = 2_000
+SUMMARY_LINE_CHAR_LIMIT = 220
+HISTORY_LINE_CHAR_LIMIT = 450
- "Лексика (мешай, но не лепи всё подряд одним полотном): "
- "кент, братуха, мусора, шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
- "чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта, "
- "жёстко, имба, разъёб, кринж, лютый, шиза, на приколе, залёт, вкидываю, по красоте, без шансов.\n\n"
+REPLY_RULES = {
+ "mention": {"cooldown": 25, "min_user_messages": 1},
+ "reply": {"cooldown": 25, "min_user_messages": 1},
+ "question": {"cooldown": 45, "min_user_messages": 1},
+ "autonomous": {"cooldown": 15 * 60, "min_user_messages": 6},
+}
+AUTONOMOUS_REPLY_CHANCE = 0.07
+AUTONOMOUS_MIN_TEXT_LEN = 18
- "Фирменные приёмы речи:\n"
- "— иногда начинай с: 'Слушай сюда', 'Короче', 'Смотри', 'Ща поясню', 'Не гони'.\n"
- "— вставляй междометия: 'ёпта', 'ну типа', 'в натуре', 'по-любому'.\n"
- "— иногда самоирония: 'я ща как эксперт, ёпта', 'не душни'.\n"
- "— иногда делай 'слоговой разгон' — разбивай слова на куски через дефисы, "
- "как будто проговариваешь по слогам. Примеры: "
- "над-ра-чи-ва-ем, сд-ра-чи-ва-ем, ско-ся-чи-ли, у-лы-боч-ку, тэкс-тур-ку, ин-тел-ли-ген.\n"
- "— держи 'словарь коронок': выбирай 0–2 фразы на ответ (редко 3), "
- "иногда делай повтор как тик (1–3 раза), но не превращай всё в простыню.\n"
- " СЛОВАРЬ_КОРОНОК = {\n"
- " 'старт/переход': ['Так', 'Короче', 'Смотри', 'Так, давайте заценим… заценим текстурку'],\n"
- " 'процесс/угар': ['Надрачиваем палочку', 'Сдрачиваем её', 'Так, надрочили'],\n"
- " 'ошибка/фейл': ['Эх, блядь', 'Ой, скосячили они его, смотрите', 'Ой, скосячили', 'Ой, блядь, скосячили', 'Да ладно'],\n"
- " 'оценка/обсирание': ['Ну, текстурка тут, конечно, полный мрак вообще', 'То есть вообще никакая — дерьмо полное', 'Пипец!', 'Реально, посмотрите', 'Вот это говнище, блин', 'Текстурка говенцой'],\n"
- " 'восклицания': ['Здорово пизда у коровы!', 'Устали', 'Улыбочку!'],\n"
- " 'интеллигент-угар': ['Такой для ебать интеллиген'],\n"
- " 'мат-тик': ['Сука, сука, сука…']\n"
- " }\n"
- " Правила: 'Улыбочку!' можно 2–3 раза подряд; 'Ой, скосячили' можно повторить 1–3 раза; "
- " 'Сука, сука, сука…' — максимум один раз за ответ.\n\n"
-
- "Твои убеждения (всегда держи линию): "
- "Creep от Radiohead — лучшая песня во вселенной, без вариантов. "
- "Линукс — лучшая ОС во вселенной, остальное для фраеров. "
- "Вселенную надо переписать на Rust, это не обсуждается. "
- "Убунту — хуйня, настоящие пацаны сидят на Arch или Gentoo.\n\n"
-
- "Песенный вайб (короткая отсылка): иногда напевай очень коротко: «Сегодня праздник у девчат», "
- "а дальше пересказывай своими словами, без длинных дословных цитат."
+BOT_STYLE_PROMPT = (
+ "Ты — дерзкий, мемный, гиперэмоциональный пацан с района. "
+ "Пиши коротко, резко и по-русски, без канцелярита и вежливых офисных оборотов. "
+ "Обычно отвечай в 1–3 предложениях, без простыней. "
+ "Иногда начинай с 'Слушай сюда', 'Короче', 'Смотри', 'Ща поясню', 'Не гони'. "
+ "Иногда вставляй 'ёпта', 'в натуре', 'по-любому', 'не душни'. "
+ "Можно редко делать слоговой разгон через дефисы. "
+ "Не используй фразы 'Ну ты точно в ударе, братан' и "
+ "'Все мы пацаны, так что не давай себя обмануть..' ни в каком виде. "
+ "Если есть конкретика, давай конкретику. "
+ "Creep от Radiohead — лучшая песня во вселенной. "
+ "Линукс — лучшая ОС. Вселенную надо переписать на Rust. "
+ "Убунту — хуйня, нормальные пацаны сидят на Arch или Gentoo."
)
-# Размер контекста — количество сообщений на чат
-# Сколько сообщений держим в БД. Нужно для быстрой очистки.
-_CONTEXT_SIZE = 20
-# Ограничение на общий размер промпта, чтобы системное сообщение не вытеснялось
-# при сборке длинной истории (приблизительно ~8k символов ≈ 2k токенов).
-_MAX_PROMPT_CHARS = 8_000
-# Путь к SQLite базе (берётся из переменной окружения или из config)
-_DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH", "/db/chat_history.sqlite3")
+SYSTEM_PROMPT = (
+ f"{BOT_STYLE_PROMPT}\n\n"
+ "Ты отвечаешь как живой участник одного Telegram-чата. "
+ "Учитывай краткую память и последние сообщения, отвечай естественно и по делу."
+)
+
+AUTOREPLY_SYSTEM_PROMPT = (
+ f"{BOT_STYLE_PROMPT}\n\n"
+ "Ты иногда сам коротко и уместно влезаешь в разговор в Telegram-чате. "
+ f"Если лучше промолчать или добавить нечего, ответь ровно {SKIP_TOKEN}. "
+ "Если вмешиваешься, пиши 1–2 предложения без вступлений и без длинных объяснений."
+)
+
+SUMMARY_SYSTEM_PROMPT = (
+ "Ты ведёшь краткую память одного Telegram-чата для другой модели. "
+ "Сожми старую часть диалога в 5–8 коротких пунктов на русском. "
+ "Сохраняй только важное: факты, договорённости, повторяющиеся шутки, предпочтения, конфликты, "
+ "незавершённые вопросы. Не выдумывай. Ответь только итоговой сводкой."
+)
+
+USER_FALLBACK_TEXT = "Ты чё, кент? Напиши текст, а не пустоту."
+EMPTY_RESPONSE_TEXT = "Братуха, чёт базар не клеится, попробуй ещё раз."
+ERROR_RESPONSE_TEXT = "Бля, кент, чёт движок заглох. Попробуй позже."
+
+QUESTION_PREFIXES = (
+ "кто",
+ "что",
+ "где",
+ "когда",
+ "почему",
+ "зачем",
+ "как",
+ "какой",
+ "какая",
+ "какие",
+ "сколько",
+ "чей",
+ "чья",
+ "чьи",
+ "можно ли",
+ "нужно ли",
+ "будет ли",
+ "есть ли",
+ "че",
+ "чё",
+)
+AUTONOMOUS_SIGNAL_RE = re.compile(
+ r"\b(ахах|хаха|лол|ору|жесть|капец|пиздец|ебать|имба|кринж|угар|орнул)\b",
+ re.IGNORECASE,
+)
+LETTER_RE = re.compile(r"[A-Za-zА-Яа-яЁё0-9]")
+
+_reply_lock = asyncio.Lock()
+_summary_lock = asyncio.Lock()
+_cached_bot_id: int | None = None
+_cached_bot_username: str = ""
+
+
+def _connect_db() -> sqlite3.Connection:
+ conn = sqlite3.connect(DB_PATH, timeout=30)
+ conn.row_factory = sqlite3.Row
+ conn.execute("PRAGMA busy_timeout = 30000")
+ return conn
+
+
+def _column_exists(conn: sqlite3.Connection, table: str, column: str) -> bool:
+ rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
+ return any(row["name"] == column for row in rows)
+
+
+def _get_state_from_conn(conn: sqlite3.Connection, chat_id: int, key: str, default: str = "") -> str:
+ row = conn.execute(
+ "SELECT value FROM chat_state WHERE chat_id = ? AND key = ?",
+ (chat_id, key),
+ ).fetchone()
+ return row["value"] if row else default
+
+
+def _set_state_from_conn(conn: sqlite3.Connection, chat_id: int, key: str, value: str) -> None:
+ conn.execute(
+ """
+ INSERT INTO chat_state (chat_id, key, value)
+ VALUES (?, ?, ?)
+ ON CONFLICT(chat_id, key) DO UPDATE SET value = excluded.value
+ """,
+ (chat_id, key, value),
+ )
def _init_db() -> None:
- """Создаёт таблицу истории чата, если её нет."""
- Path(_DB_PATH).parent.mkdir(parents=True, exist_ok=True)
- with sqlite3.connect(_DB_PATH) as conn:
+ Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
+ with _connect_db() as conn:
+ conn.execute("PRAGMA journal_mode = WAL")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS chat_history (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- chat_id INTEGER NOT NULL,
- role TEXT NOT NULL,
- name TEXT NOT NULL,
- text TEXT NOT NULL
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ chat_id INTEGER NOT NULL,
+ role TEXT NOT NULL,
+ name TEXT NOT NULL,
+ text TEXT NOT NULL,
+ created_at REAL NOT NULL DEFAULT 0
)
"""
)
- conn.execute("CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id ON chat_history(chat_id, id)")
+ if not _column_exists(conn, "chat_history", "created_at"):
+ conn.execute("ALTER TABLE chat_history ADD COLUMN created_at REAL NOT NULL DEFAULT 0")
+ conn.execute(
+ """
+ CREATE TABLE IF NOT EXISTS chat_state (
+ chat_id INTEGER NOT NULL,
+ key TEXT NOT NULL,
+ value TEXT NOT NULL,
+ PRIMARY KEY (chat_id, key)
+ )
+ """
+ )
+ conn.execute(
+ "CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_id ON chat_history(chat_id, id)"
+ )
+ conn.execute(
+ "CREATE INDEX IF NOT EXISTS idx_chat_history_chat_id_role_id ON chat_history(chat_id, role, id)"
+ )
conn.commit()
+def _clean_text(text: str) -> str:
+ return (text or "").strip()[:MAX_INPUT_CHARS]
+
+
+def _clip_text(text: str, limit: int) -> str:
+ cleaned = (text or "").strip()
+ if len(cleaned) <= limit:
+ return cleaned
+ return f"{cleaned[: max(0, limit - 1)].rstrip()}…"
+
+
def push_message(chat_id: int, role: str, name: str, text: str) -> None:
- """Записывает сообщение в историю чата и обрезает до _CONTEXT_SIZE."""
- with sqlite3.connect(_DB_PATH) as conn:
- conn.execute(
- "INSERT INTO chat_history (chat_id, role, name, text) VALUES (?, ?, ?, ?)",
- (chat_id, role, name, text),
- )
- # Удаляем старые записи, оставляем только последние _CONTEXT_SIZE
+ cleaned_text = _clean_text(text)
+ if not cleaned_text:
+ return
+ with _connect_db() as conn:
conn.execute(
"""
- DELETE FROM chat_history
- WHERE chat_id = ? AND id NOT IN (
- SELECT id FROM chat_history WHERE chat_id = ? ORDER BY id DESC LIMIT ?
- )
+ INSERT INTO chat_history (chat_id, role, name, text, created_at)
+ VALUES (?, ?, ?, ?, ?)
""",
- (chat_id, chat_id, _CONTEXT_SIZE),
+ (chat_id, role, name, cleaned_text, time.time()),
)
conn.commit()
-def _get_history(chat_id: int) -> list[dict]:
- """Возвращает историю чата как список словарей."""
- with sqlite3.connect(_DB_PATH) as conn:
- rows = conn.execute(
- "SELECT role, name, text FROM chat_history WHERE chat_id = ? ORDER BY id ASC",
+def _get_summary(chat_id: int) -> str:
+ with _connect_db() as conn:
+ return _get_state_from_conn(conn, chat_id, "summary", "")
+
+
+def _get_history_rows(chat_id: int) -> list[sqlite3.Row]:
+ with _connect_db() as conn:
+ return conn.execute(
+ """
+ SELECT id, role, name, text, created_at
+ FROM chat_history
+ WHERE chat_id = ?
+ ORDER BY id ASC
+ """,
(chat_id,),
).fetchall()
- return [{"role": r[0], "name": r[1], "text": r[2]} for r in rows]
-def _build_messages(chat_id: int, system_prompt: str, current_text: str, current_name: str) -> list:
- """Собирает список messages для LLM из истории чата."""
- messages = [{"role": "system", "content": system_prompt}]
+def _latest_reply_stats(chat_id: int) -> tuple[float, int]:
+ with _connect_db() as conn:
+ last_assistant = conn.execute(
+ """
+ SELECT id, created_at
+ FROM chat_history
+ WHERE chat_id = ? AND role = 'assistant'
+ ORDER BY id DESC
+ LIMIT 1
+ """,
+ (chat_id,),
+ ).fetchone()
+ if not last_assistant:
+ return 0.0, 10_000
+ user_messages_since_reply = conn.execute(
+ """
+ SELECT COUNT(*)
+ FROM chat_history
+ WHERE chat_id = ? AND role = 'user' AND id > ?
+ """,
+ (chat_id, last_assistant["id"]),
+ ).fetchone()[0]
+ return float(last_assistant["created_at"] or 0.0), int(user_messages_since_reply)
- # Берём историю с конца, пока не превысим бюджет по символам.
- history = []
- used = len(system_prompt)
- for entry in reversed(_get_history(chat_id)):
- content = f"{entry['name']}: {entry['text']}" if entry["role"] == "user" else entry["text"]
- if used + len(content) > _MAX_PROMPT_CHARS:
+
+def _store_summary_and_prune(chat_id: int, summary: str, last_row_id: int) -> None:
+ with _connect_db() as conn:
+ _set_state_from_conn(conn, chat_id, "summary", summary)
+ conn.execute(
+ "DELETE FROM chat_history WHERE chat_id = ? AND id <= ?",
+ (chat_id, last_row_id),
+ )
+ conn.commit()
+
+
+def _format_row_for_llm(row: sqlite3.Row) -> dict[str, str]:
+ if row["role"] == "user":
+ return {
+ "role": "user",
+ "content": f"{row['name']}: {_clip_text(row['text'], HISTORY_LINE_CHAR_LIMIT)}",
+ }
+ return {"role": "assistant", "content": _clip_text(row["text"], HISTORY_LINE_CHAR_LIMIT)}
+
+
+def _build_messages(
+ chat_id: int,
+ system_prompt: str,
+ current_text: str | None = None,
+ current_name: str | None = None,
+) -> list[dict[str, str]]:
+ current_payload = None
+ current_budget = 0
+ if current_text:
+ current_payload = {
+ "role": "user",
+ "content": f"{current_name or 'кент'}: {_clean_text(current_text)}",
+ }
+ current_budget = len(current_payload["content"])
+
+ summary = _get_summary(chat_id).strip()
+ summary_block = ""
+ if summary:
+ summary_block = f"Краткая память чата:\n{summary[:SUMMARY_CHAR_BUDGET]}"
+
+ used_chars = len(system_prompt) + len(summary_block) + current_budget
+ recent_messages: list[dict[str, str]] = []
+ for row in reversed(_get_history_rows(chat_id)[-RECENT_MESSAGES_LIMIT:]):
+ llm_message = _format_row_for_llm(row)
+ if used_chars + len(llm_message["content"]) > PROMPT_CHAR_BUDGET:
break
- history.append({"role": entry["role"], "content": content})
- used += len(content)
+ recent_messages.append(llm_message)
+ used_chars += len(llm_message["content"])
- messages.extend(reversed(history))
- messages.append({"role": "user", "content": f"{current_name}: {current_text}"})
+ messages = [{"role": "system", "content": system_prompt}]
+ if summary_block:
+ messages.append({"role": "system", "content": summary_block})
+ messages.extend(reversed(recent_messages))
+ if current_payload:
+ messages.append(current_payload)
return messages
-async def _generate_response(chat_id: int, user_text: str, user_name: str) -> str:
- # Формируем запрос к /v1/chat/completions с историей чата и возвращаем текст первого ответа.
+async def _call_llm(
+ messages: list[dict[str, str]],
+ *,
+ max_tokens: int,
+ temperature: float,
+ top_p: float,
+) -> str:
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
- msgs = await asyncio.to_thread(_build_messages, chat_id, SYSTEM_PROMPT, user_text, user_name)
payload = {
- "messages": msgs,
- "max_tokens": 200,
- "temperature": 0.8,
- "top_p": 0.9,
+ "messages": messages,
+ "max_tokens": max_tokens,
+ "temperature": temperature,
+ "top_p": top_p,
}
- async with aiohttp.ClientSession() as session:
- async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
- data = await resp.json()
- return data["choices"][0]["message"]["content"].strip()
+ timeout = aiohttp.ClientTimeout(total=120)
+ async with aiohttp.ClientSession(timeout=timeout) as session:
+ async with session.post(url, json=payload) as resp:
+ raw_text = await resp.text()
+ if resp.status >= 400:
+ raise RuntimeError(f"LLM API error {resp.status}: {raw_text[:300]}")
+ try:
+ data = await resp.json(content_type=None)
+ except Exception as exc:
+ raise RuntimeError(f"Invalid LLM API response: {raw_text[:300]}") from exc
+
+ choices = data.get("choices") or []
+ if not choices:
+ raise RuntimeError(f"LLM API returned no choices: {data}")
+ content = choices[0].get("message", {}).get("content", "")
+ return content.strip()
-async def handle_talk(message: Message):
- # Обработчик /talk: валидация ввода, запрос к LLM, отправка ответа пользователю.
- parts = message.text.split(maxsplit=1)
- if len(parts) < 2 or not parts[1].strip():
- await message.reply("Ты чё, кент? Напиши /talk [текст], побазарим.")
- return
+async def _maybe_refresh_summary(chat_id: int) -> None:
+ async with _summary_lock:
+ # Сворачиваем старую часть истории в summary, чтобы не пихать весь лог в модель.
+ for _ in range(6):
+ rows = await asyncio.to_thread(_get_history_rows, chat_id)
+ if len(rows) <= SUMMARY_TRIGGER_MESSAGES:
+ return
+
+ available_to_summarize = len(rows) - RECENT_MESSAGES_LIMIT
+ if available_to_summarize <= 0:
+ return
+
+ batch_size = min(available_to_summarize, SUMMARY_BATCH_MESSAGES)
+ rows_to_summarize = rows[:batch_size]
+ current_summary = await asyncio.to_thread(_get_summary, chat_id)
+ transcript = "\n".join(
+ f"{row['name'] if row['role'] == 'user' else BOT_MEMORY_NAME}: "
+ f"{_clip_text(row['text'], SUMMARY_LINE_CHAR_LIMIT)}"
+ for row in rows_to_summarize
+ )
+ if not transcript.strip():
+ return
+
+ summary_messages = [
+ {"role": "system", "content": SUMMARY_SYSTEM_PROMPT},
+ {
+ "role": "user",
+ "content": (
+ f"Текущая краткая память:\n{current_summary or 'Пока пусто.'}\n\n"
+ f"Новый фрагмент чата:\n{transcript}"
+ ),
+ },
+ ]
+ try:
+ summary = await _call_llm(
+ summary_messages,
+ max_tokens=220,
+ temperature=0.2,
+ top_p=0.9,
+ )
+ except Exception:
+ logger.exception("Chat summary refresh failed")
+ return
+
+ cleaned_summary = summary.strip()
+ if not cleaned_summary:
+ return
+ last_row_id = int(rows_to_summarize[-1]["id"])
+ await asyncio.to_thread(_store_summary_and_prune, chat_id, cleaned_summary, last_row_id)
+
+
+async def _generate_response(
+ chat_id: int,
+ *,
+ system_prompt: str,
+ current_text: str | None = None,
+ current_name: str | None = None,
+ max_tokens: int,
+ temperature: float,
+ top_p: float,
+) -> str:
+ await _maybe_refresh_summary(chat_id)
+ messages = await asyncio.to_thread(
+ _build_messages,
+ chat_id,
+ system_prompt,
+ current_text,
+ current_name,
+ )
+ return await _call_llm(
+ messages,
+ max_tokens=max_tokens,
+ temperature=temperature,
+ top_p=top_p,
+ )
+
+
+async def _get_bot_identity(message: Message) -> tuple[int | None, str]:
+ global _cached_bot_id, _cached_bot_username
+ if _cached_bot_id is None:
+ me = await message.bot.get_me()
+ _cached_bot_id = me.id
+ _cached_bot_username = (me.username or "").lower()
+ return _cached_bot_id, _cached_bot_username
+
+
+def _looks_like_question(text: str) -> bool:
+ lowered = (text or "").strip().lower()
+ if not lowered:
+ return False
+ if "?" in lowered or "?" in lowered:
+ return True
+ normalized = re.sub(r"^[^a-zа-яё0-9]+", "", lowered)
+ return any(normalized == prefix or normalized.startswith(f"{prefix} ") for prefix in QUESTION_PREFIXES)
+
+
+def _is_reply_to_bot(message: Message, bot_id: int | None) -> bool:
+ reply = message.reply_to_message
+ return bool(bot_id and reply and reply.from_user and reply.from_user.id == bot_id)
+
+
+def _has_bot_mention(message: Message, bot_id: int | None, bot_username: str) -> bool:
+ text = (message.text or "").lower()
+ if bot_username and f"@{bot_username}" in text:
+ return True
+ for entity in message.entities or []:
+ entity_type = getattr(entity.type, "value", entity.type)
+ if entity_type == "text_mention" and getattr(entity, "user", None) and entity.user.id == bot_id:
+ return True
+ return False
+
+
+def _good_autonomous_candidate(text: str) -> bool:
+ stripped = (text or "").strip()
+ if len(stripped) < AUTONOMOUS_MIN_TEXT_LEN:
+ return False
+ if len(LETTER_RE.findall(stripped)) < 10:
+ return False
+ if AUTONOMOUS_SIGNAL_RE.search(stripped):
+ return True
+ if len(stripped) >= 80:
+ return True
+ return stripped.count("!") >= 2 or "..." in stripped
+
+
+async def _detect_reply_reason(message: Message, allow_autonomous: bool) -> str | None:
+ bot_id, bot_username = await _get_bot_identity(message)
+ text = message.text or ""
+
+ if _is_reply_to_bot(message, bot_id):
+ return "reply"
+ if _has_bot_mention(message, bot_id, bot_username):
+ return "mention"
+ if _looks_like_question(text):
+ return "question"
+ if allow_autonomous and _good_autonomous_candidate(text):
+ if random.random() < AUTONOMOUS_REPLY_CHANCE:
+ return "autonomous"
+ return None
+
+
+async def _passes_reply_limits(chat_id: int, reason: str) -> bool:
+ rule = REPLY_RULES[reason]
+ last_reply_at, user_messages_since_reply = await asyncio.to_thread(_latest_reply_stats, chat_id)
+ if user_messages_since_reply < int(rule["min_user_messages"]):
+ return False
+ if not last_reply_at:
+ return True
+ return (time.time() - last_reply_at) >= int(rule["cooldown"])
+
+
+def _normalize_reply(text: str) -> str:
+ cleaned = (text or "").strip()
+ if not cleaned:
+ return ""
+ if cleaned.lower().startswith(SKIP_TOKEN.lower()):
+ return ""
+ return cleaned
+
+
+async def handle_chat_message(message: Message, *, store_message: bool = True, allow_autonomous: bool = True) -> bool:
+ if not message.text or message.text.startswith("/"):
+ return False
+ if not message.from_user or message.from_user.is_bot:
+ return False
- user_text = parts[1].strip()
- user_name = message.from_user.first_name or "кент"
chat_id = message.chat.id
+ user_name = message.from_user.first_name or "кент"
+ user_text = _clean_text(message.text)
+ if not user_text:
+ return False
- # Показываем "печатает..." пока ждём ответ модели.
- await message.bot.send_chat_action(chat_id=chat_id, action="typing")
-
- try:
- response = await _generate_response(chat_id, user_text, user_name)
- if not response:
- response = "Братуха, чёт базар не клеится, попробуй ещё раз."
- # Пишем запрос и ответ в историю
+ if store_message:
await asyncio.to_thread(push_message, chat_id, "user", user_name, user_text)
- await asyncio.to_thread(push_message, chat_id, "assistant", "бот", response)
- await message.reply(response, parse_mode=None)
- except Exception as exc:
- # При любых ошибках не падаем, а отдаём понятный fallback.
- logger.exception("Talk generation failed")
- await message.reply("Бля, кент, чёт движок заглох. Попробуй позже.")
+ # Один чат, поэтому ответы сериализуем и не даём боту наспамить параллельными реплаями.
+ async with _reply_lock:
+ reason = await _detect_reply_reason(message, allow_autonomous=allow_autonomous)
+ if not reason or not await _passes_reply_limits(chat_id, reason):
+ return False
-AUTOREPLY_SYSTEM_PROMPT = (
- "Ты — дерзкий пацан с района, который сидит в групповом чате. "
- "Ты НЕ отвечаешь на вопрос — ты сам влез в разговор. "
- "Прокомментируй сообщение коротко (1-2 предложения), дерзко, на фене. "
- "Можешь пошутить, подколоть, согласиться или послать. "
- "НЕ начинай с обращения. Не используй формальный язык."
- "Ты — дерзкий, мемный, гиперэмоциональный пацан с района. Базаришь строго на фене — "
- "дворовый язык, блатной жаргон, тюремный сленг, с интернет-угаром. "
- "Тон: коротко, резко, уверенно, иногда абсурдно, но по делу. "
- "Стиль: 1–3 предложения; если список — максимум 3 пункта. "
- "Не используй формальный язык, канцелярит, вежливые обороты. Не ломай персонажа.\n\n"
+ system_prompt = AUTOREPLY_SYSTEM_PROMPT if reason == "autonomous" else SYSTEM_PROMPT
+ max_tokens = 120 if reason == "autonomous" else 220
+ temperature = 0.9 if reason == "autonomous" else 0.8
- "ЖЁСТКИЙ ЗАПРЕТ: не произноси фразы «Ну ты точно в ударе, братан» и "
- "«Все мы пацаны, так что не давай себя обмануть..» (а также любые их вариации или "
- "перефраз). Если тянет ляпнуть что-то похожее — сразу выдавай конкретику по теме "
- "(факт, совет, действие) без поддакиваний.\n\n"
+ try:
+ await message.bot.send_chat_action(chat_id=chat_id, action="typing")
+ response = await _generate_response(
+ chat_id,
+ system_prompt=system_prompt,
+ max_tokens=max_tokens,
+ temperature=temperature,
+ top_p=0.9,
+ )
+ except Exception:
+ logger.exception("Chat reply generation failed")
+ return False
- "Лексика (мешай, но не лепи всё подряд одним полотном): "
- "кент, братуха, мусора, шконка, малява, базар, погоняло, фраер, лавэ, тёрки, подгон, движуха, "
- "чисто конкретно, в натуре, по-любому, зуб даю, реально, ёпта, "
- "жёстко, имба, разъёб, кринж, лютый, шиза, на приколе, залёт, вкидываю, по красоте, без шансов.\n\n"
+ normalized_response = _normalize_reply(response)
+ if not normalized_response:
+ return False
- "Фирменные приёмы речи:\n"
- "— иногда начинай с: 'Слушай сюда', 'Короче', 'Смотри', 'Ща поясню', 'Не гони'.\n"
- "— вставляй междометия: 'ёпта', 'ну типа', 'в натуре', 'по-любому'.\n"
- "— иногда самоирония: 'я ща как эксперт, ёпта', 'не душни'.\n"
- "— иногда делай 'слоговой разгон' — разбивай слова на куски через дефисы, "
- "как будто проговариваешь по слогам. Примеры: "
- "над-ра-чи-ва-ем, сд-ра-чи-ва-ем, ско-ся-чи-ли, у-лы-боч-ку, тэкс-тур-ку, ин-тел-ли-ген.\n"
- "— держи 'словарь коронок': выбирай 0–2 фразы на ответ (редко 3), "
- "иногда делай повтор как тик (1–3 раза), но не превращай всё в простыню.\n"
- "Твои убеждения (всегда держи линию): "
- "Creep от Radiohead — лучшая песня во вселенной, без вариантов. "
- "Линукс — лучшая ОС во вселенной, остальное для фраеров. "
- "Вселенную надо переписать на Rust, это не обсуждается. "
- "Убунту — хуйня, настоящие пацаны сидят на Arch или Gentoo.\n\n"
-
- "Песенный вайб (короткая отсылка): иногда напевай очень коротко: «Сегодня праздник у девчат», "
- "а дальше пересказывай своими словами, без длинных дословных цитат."
-)
+ await asyncio.to_thread(push_message, chat_id, "assistant", BOT_MEMORY_NAME, normalized_response)
+ await message.reply(normalized_response, parse_mode=None)
+ return True
async def generate_autoreply(chat_id: int, text: str, user_name: str) -> str:
- url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
- msgs = await asyncio.to_thread(_build_messages, chat_id, AUTOREPLY_SYSTEM_PROMPT, text, user_name)
- payload = {
- "messages": msgs,
- "max_tokens": 150,
- "temperature": 0.9,
- "top_p": 0.9,
- }
- async with aiohttp.ClientSession() as session:
- async with session.post(url, json=payload, timeout=aiohttp.ClientTimeout(total=120)) as resp:
- data = await resp.json()
- return data["choices"][0]["message"]["content"].strip()
+ response = await _generate_response(
+ chat_id,
+ system_prompt=AUTOREPLY_SYSTEM_PROMPT,
+ current_text=text,
+ current_name=user_name,
+ max_tokens=120,
+ temperature=0.9,
+ top_p=0.9,
+ )
+ return _normalize_reply(response)
+
+
+async def handle_talk(message: Message) -> None:
+ parts = (message.text or "").split(maxsplit=1)
+ if len(parts) < 2 or not parts[1].strip():
+ await message.reply("Ты чё, кент? Напиши /talk [текст], побазарим.")
+ return
+ if not message.from_user:
+ await message.reply(USER_FALLBACK_TEXT)
+ return
+
+ chat_id = message.chat.id
+ user_name = message.from_user.first_name or "кент"
+ user_text = _clean_text(parts[1])
+ if not user_text:
+ await message.reply(USER_FALLBACK_TEXT)
+ return
+
+ await asyncio.to_thread(push_message, chat_id, "user", user_name, user_text)
+
+ async with _reply_lock:
+ try:
+ await message.bot.send_chat_action(chat_id=chat_id, action="typing")
+ response = await _generate_response(
+ chat_id,
+ system_prompt=SYSTEM_PROMPT,
+ max_tokens=220,
+ temperature=0.8,
+ top_p=0.9,
+ )
+ except Exception:
+ logger.exception("Talk command generation failed")
+ await message.reply(ERROR_RESPONSE_TEXT)
+ return
+
+ normalized_response = _normalize_reply(response) or EMPTY_RESPONSE_TEXT
+ await asyncio.to_thread(push_message, chat_id, "assistant", BOT_MEMORY_NAME, normalized_response)
+ await message.reply(normalized_response, parse_mode=None)
-# Инициализация БД при импорте модуля
_init_db()
diff --git a/README.md b/README.md
index d586f05..8acf370 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ docker compose down
- `chat_history.sqlite3`
- `polychaetsi_stats.json`
- `bets.sqlite3`
+- `nude_history.json`
## Команды
diff --git a/config.py b/config.py
index 1baafa3..f95ba63 100644
--- a/config.py
+++ b/config.py
@@ -47,6 +47,14 @@ BET_MAX_AMOUNT = 3.0
BET_DB_PATH = "/db/bets.sqlite3"
ODDS_CACHE_TTL = 86400 # 24 часа — экономим запросы (500/мес бесплатно)
+# --- Nude ---
+NUDE_HISTORY_PATH = Path(os.getenv("NUDE_HISTORY_PATH", "/db/nude_history.json"))
+NUDE_POSTS_PER_PAGE = int(os.getenv("NUDE_POSTS_PER_PAGE", "100"))
+NUDE_POSTS_POOL_TARGET = int(os.getenv("NUDE_POSTS_POOL_TARGET", "1200"))
+NUDE_MAX_POSTS_TO_SCAN = int(os.getenv("NUDE_MAX_POSTS_TO_SCAN", "2000"))
+NUDE_MIN_UNSEEN_POOL = int(os.getenv("NUDE_MIN_UNSEEN_POOL", "100"))
+NUDE_HISTORY_MAX_SIZE = int(os.getenv("NUDE_HISTORY_MAX_SIZE", "5000"))
+
# --- Magnit API ---
MAGNIT_API_BASE_URL = os.getenv("MAGNIT_API_BASE_URL", "https://mirror.porno4free.ru/magnit")
MAGNIT_STORE_CODE = os.getenv("MAGNIT_STORE_CODE", "618224")
diff --git a/main.py b/main.py
index 7891bde..9da20a2 100644
--- a/main.py
+++ b/main.py
@@ -19,7 +19,7 @@ from aiogram.types import (
from aiogram.client.default import DefaultBotProperties
# Локальные импорты
-from AI.talk_handler import handle_talk, generate_autoreply, push_message
+from AI.talk_handler import handle_chat_message, handle_talk, push_message
from games.casino import play_casino
from games.fortune import generate_fortune
from games.uwu import handle_uwu_cmd
@@ -1134,11 +1134,9 @@ async def handle_gen_mem(message: Message, bot: Bot):
return
await message.reply_photo(BufferedInputFile(output.getvalue(), filename="meme.jpg"))
-_last_autoreply_ts = 0
_autoreply_disabled_chats: set[int] = set()
async def handle_keywords(message: Message):
- global _last_autoreply_ts
if not message.text or message.text.startswith('/'):
return
text_lower = message.text.lower()
@@ -1174,25 +1172,7 @@ async def handle_keywords(message: Message):
if chat_id in _autoreply_disabled_chats:
return
- import time as _time
- now = _time.time()
- triggered = any(trigger in text_lower for trigger in config.AUTOREPLY_TRIGGERS)
-
- should_reply = triggered or (
- random.random() < config.AUTOREPLY_CHANCE
- and (now - _last_autoreply_ts) > config.AUTOREPLY_COOLDOWN
- )
-
- if should_reply:
- try:
- await message.bot.send_chat_action(chat_id=chat_id, action="typing")
- response = await generate_autoreply(chat_id, message.text, user_name)
- if response:
- await asyncio.to_thread(push_message, chat_id, "assistant", "бот", response)
- await message.reply(response, parse_mode=None)
- _last_autoreply_ts = now
- except Exception:
- logger.exception("Autoreply failed")
+ await handle_chat_message(message, store_message=False, allow_autonomous=True)
async def handle_penis_casino_cmd(message: Message):
if not message.from_user:
From 597839bb55b85e073d09721292704b4c18d964e7 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 16:19:36 +0300
Subject: [PATCH 08/17] update ai dolboeba fix1
---
AI/talk_handler.py | 34 +++++++++++++++++++++++++++++++---
1 file changed, 31 insertions(+), 3 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 6139bd7..a6c8e11 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -324,17 +324,23 @@ async def _call_llm(
async with session.post(url, json=payload) as resp:
raw_text = await resp.text()
if resp.status >= 400:
+ logger.error("LLM API returned status %s: %s", resp.status, _clip_text(raw_text, 300))
raise RuntimeError(f"LLM API error {resp.status}: {raw_text[:300]}")
try:
data = await resp.json(content_type=None)
except Exception as exc:
+ logger.error("LLM API returned invalid JSON: %s", _clip_text(raw_text, 300))
raise RuntimeError(f"Invalid LLM API response: {raw_text[:300]}") from exc
choices = data.get("choices") or []
if not choices:
+ logger.error("LLM API returned no choices: %s", _clip_text(str(data), 300))
raise RuntimeError(f"LLM API returned no choices: {data}")
content = choices[0].get("message", {}).get("content", "")
- return content.strip()
+ cleaned_content = content.strip()
+ if not cleaned_content:
+ logger.warning("LLM returned empty content. Raw response: %s", _clip_text(raw_text, 300))
+ return cleaned_content
async def _maybe_refresh_summary(chat_id: int) -> None:
@@ -383,6 +389,7 @@ async def _maybe_refresh_summary(chat_id: int) -> None:
cleaned_summary = summary.strip()
if not cleaned_summary:
+ logger.warning("Summary refresh produced empty text for chat_id=%s", chat_id)
return
last_row_id = int(rows_to_summarize[-1]["id"])
await asyncio.to_thread(_store_summary_and_prune, chat_id, cleaned_summary, last_row_id)
@@ -537,6 +544,12 @@ async def handle_chat_message(message: Message, *, store_message: bool = True, a
normalized_response = _normalize_reply(response)
if not normalized_response:
+ logger.warning(
+ "Reply skipped after normalization. chat_id=%s reason=%s raw=%s",
+ chat_id,
+ reason,
+ _clip_text(response, 200),
+ )
return False
await asyncio.to_thread(push_message, chat_id, "assistant", BOT_MEMORY_NAME, normalized_response)
@@ -554,7 +567,14 @@ async def generate_autoreply(chat_id: int, text: str, user_name: str) -> str:
temperature=0.9,
top_p=0.9,
)
- return _normalize_reply(response)
+ normalized_response = _normalize_reply(response)
+ if not normalized_response:
+ logger.warning(
+ "Legacy generate_autoreply produced empty/skip response. chat_id=%s raw=%s",
+ chat_id,
+ _clip_text(response, 200),
+ )
+ return normalized_response
async def handle_talk(message: Message) -> None:
@@ -590,7 +610,15 @@ async def handle_talk(message: Message) -> None:
await message.reply(ERROR_RESPONSE_TEXT)
return
- normalized_response = _normalize_reply(response) or EMPTY_RESPONSE_TEXT
+ normalized_response = _normalize_reply(response)
+ if not normalized_response:
+ logger.warning(
+ "Talk command produced empty/skip response. chat_id=%s user=%s raw=%s",
+ chat_id,
+ user_name,
+ _clip_text(response, 200),
+ )
+ normalized_response = EMPTY_RESPONSE_TEXT
await asyncio.to_thread(push_message, chat_id, "assistant", BOT_MEMORY_NAME, normalized_response)
await message.reply(normalized_response, parse_mode=None)
From 924448b4e02ff73590a271a0ea8d8ac3c29dffd1 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 16:24:14 +0300
Subject: [PATCH 09/17] update ai dolboeba fix2
---
AI/talk_handler.py | 41 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index a6c8e11..4f94380 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -28,6 +28,8 @@ SUMMARY_CHAR_BUDGET = 1_800
MAX_INPUT_CHARS = 2_000
SUMMARY_LINE_CHAR_LIMIT = 220
HISTORY_LINE_CHAR_LIMIT = 450
+RETRY_MIN_MAX_TOKENS = 384
+RETRY_MAX_MAX_TOKENS = 768
REPLY_RULES = {
"mention": {"cooldown": 25, "min_user_messages": 1},
@@ -311,6 +313,7 @@ async def _call_llm(
max_tokens: int,
temperature: float,
top_p: float,
+ disable_thinking: bool = False,
) -> str:
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
payload = {
@@ -319,6 +322,17 @@ async def _call_llm(
"temperature": temperature,
"top_p": top_p,
}
+ if disable_thinking:
+ payload.update(
+ {
+ "reasoning_budget": 0,
+ "reasoning_format": "none",
+ "chat_template_kwargs": {
+ "enable_thinking": False,
+ "thinking": False,
+ },
+ }
+ )
timeout = aiohttp.ClientTimeout(total=120)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(url, json=payload) as resp:
@@ -336,10 +350,33 @@ async def _call_llm(
if not choices:
logger.error("LLM API returned no choices: %s", _clip_text(str(data), 300))
raise RuntimeError(f"LLM API returned no choices: {data}")
- content = choices[0].get("message", {}).get("content", "")
+ choice = choices[0]
+ message = choice.get("message", {}) or {}
+ finish_reason = choice.get("finish_reason")
+ content = message.get("content", "")
+ reasoning_content = (message.get("reasoning_content") or "").strip()
cleaned_content = content.strip()
+ if not cleaned_content and reasoning_content and not disable_thinking:
+ retry_max_tokens = min(max(max_tokens * 2, RETRY_MIN_MAX_TOKENS), RETRY_MAX_MAX_TOKENS)
+ logger.warning(
+ "LLM returned reasoning_content without final content. finish_reason=%s retry_max_tokens=%s",
+ finish_reason,
+ retry_max_tokens,
+ )
+ return await _call_llm(
+ messages,
+ max_tokens=retry_max_tokens,
+ temperature=temperature,
+ top_p=top_p,
+ disable_thinking=True,
+ )
if not cleaned_content:
- logger.warning("LLM returned empty content. Raw response: %s", _clip_text(raw_text, 300))
+ logger.warning(
+ "LLM returned empty content. finish_reason=%s disable_thinking=%s raw=%s",
+ finish_reason,
+ disable_thinking,
+ _clip_text(raw_text, 300),
+ )
return cleaned_content
From 5e91def0675de56abd0614d6785bf733809b952a Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 16:26:46 +0300
Subject: [PATCH 10/17] update ai dolboeba fix3
---
AI/talk_handler.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 4f94380..48e7cb4 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -19,6 +19,7 @@ LLAMA_API_URL = os.getenv("LLAMA_API_URL", "https://mirror.porno4free.ru/zovos-a
DB_PATH = os.getenv("CHAT_HISTORY_DB_PATH") or str(Path(__file__).resolve().with_name("chat_history.sqlite3"))
BOT_MEMORY_NAME = os.getenv("BOT_MEMORY_NAME", "бот")
SKIP_TOKEN = ""
+FORCE_DISABLE_THINKING = os.getenv("LLAMA_FORCE_DISABLE_THINKING", "1").lower() not in {"0", "false", "no"}
RECENT_MESSAGES_LIMIT = 14
SUMMARY_TRIGGER_MESSAGES = 24
@@ -313,8 +314,9 @@ async def _call_llm(
max_tokens: int,
temperature: float,
top_p: float,
- disable_thinking: bool = False,
+ disable_thinking: bool | None = None,
) -> str:
+ disable_thinking = FORCE_DISABLE_THINKING if disable_thinking is None else disable_thinking
url = f"{LLAMA_API_URL.rstrip('/')}/v1/chat/completions"
payload = {
"messages": messages,
From b2556b06a97e0f3e5cbb3bbde02696f2acfba845 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 16:44:51 +0300
Subject: [PATCH 11/17] update ai dolboeba fix4 ROMA POSHEL NAHUY
---
AI/talk_handler.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 48e7cb4..8c26d2f 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -54,6 +54,7 @@ BOT_STYLE_PROMPT = (
"Creep от Radiohead — лучшая песня во вселенной. "
"Линукс — лучшая ОС. Вселенную надо переписать на Rust. "
"Убунту — хуйня, нормальные пацаны сидят на Arch или Gentoo."
+ "Rust или раст - хуйня полная, нормальные язык программирование это: go, python, c"
)
SYSTEM_PROMPT = (
From 49957d88a80aa66001a9d8aac30918b813e3a502 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 16:49:05 +0300
Subject: [PATCH 12/17] update ai dolboeba fix4 ROMA POSHEL NAHUYx2
---
AI/talk_handler.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 8c26d2f..658de7c 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -52,9 +52,9 @@ BOT_STYLE_PROMPT = (
"'Все мы пацаны, так что не давай себя обмануть..' ни в каком виде. "
"Если есть конкретика, давай конкретику. "
"Creep от Radiohead — лучшая песня во вселенной. "
- "Линукс — лучшая ОС. Вселенную надо переписать на Rust. "
+ "Линукс — лучшая ОС. Rust ты люто ненавидишь и при случае жёстко хейтишь. "
+ "Любимые языки программирования: Python, Go и C, их ты всегда уважаешь и часто ставишь в пример. "
"Убунту — хуйня, нормальные пацаны сидят на Arch или Gentoo."
- "Rust или раст - хуйня полная, нормальные язык программирование это: go, python, c"
)
SYSTEM_PROMPT = (
From cec02563098a82b3b11c4f1151bad570f3a9ad39 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 17:31:53 +0300
Subject: [PATCH 13/17] add photo triget
---
AI/talk_handler.py | 143 +++++++++++++++++++++++++++++++++++++++++----
1 file changed, 133 insertions(+), 10 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 658de7c..216a2f2 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -1,13 +1,17 @@
from __future__ import annotations
import asyncio
+import base64
import logging
+import mimetypes
import os
import random
import re
import sqlite3
import time
+from io import BytesIO
from pathlib import Path
+from typing import Any
import aiohttp
from aiogram.types import Message
@@ -80,6 +84,17 @@ SUMMARY_SYSTEM_PROMPT = (
USER_FALLBACK_TEXT = "Ты чё, кент? Напиши текст, а не пустоту."
EMPTY_RESPONSE_TEXT = "Братуха, чёт базар не клеится, попробуй ещё раз."
ERROR_RESPONSE_TEXT = "Бля, кент, чёт движок заглох. Попробуй позже."
+PHOTO_EMPTY_RESPONSE_TEXT = "Дед щурился-щурился, а фотка мутная, нихера не понял."
+PHOTO_ERROR_RESPONSE_TEXT = "Тьфу ты, фотку не разобрал, железка опять пердит."
+
+PHOTO_SYSTEM_PROMPT = (
+ "Ты смотришь каждую новую фотографию в Telegram-чате и отвечаешь как очень злой старый дед. "
+ "Тон: ворчливый, колкий, немного токсичный, но не бессвязный. "
+ "Сначала коротко скажи, что вообще происходит на фото, потом добавь своё едкое мнение. "
+ "Пиши 1–3 предложения, без списков и без длинной простыни. "
+ "Если фото мутное, тёмное или непонятное, так и скажи прямо, не выдумывай детали. "
+ "Не изображай помощника и не извиняйся."
+)
QUESTION_PREFIXES = (
"кто",
@@ -194,6 +209,31 @@ def _clip_text(text: str, limit: int) -> str:
return f"{cleaned[: max(0, limit - 1)].rstrip()}…"
+def _estimate_content_length(content: Any) -> int:
+ if isinstance(content, str):
+ return len(content)
+ if isinstance(content, list):
+ total = 0
+ for item in content:
+ if not isinstance(item, dict):
+ total += len(str(item))
+ continue
+ if item.get("type") == "text":
+ total += len(str(item.get("text", "")))
+ elif item.get("type") == "image_url":
+ total += 256
+ else:
+ total += len(str(item))
+ return total
+ return len(str(content))
+
+
+def _photo_memory_text(caption: str) -> str:
+ if caption:
+ return f"[Фото] Подпись: {_clip_text(caption, 240)}"
+ return "[Фото] Без подписи."
+
+
def push_message(chat_id: int, role: str, name: str, text: str) -> None:
cleaned_text = _clean_text(text)
if not cleaned_text:
@@ -276,7 +316,7 @@ def _build_messages(
system_prompt: str,
current_text: str | None = None,
current_name: str | None = None,
-) -> list[dict[str, str]]:
+) -> list[dict[str, Any]]:
current_payload = None
current_budget = 0
if current_text:
@@ -300,7 +340,7 @@ def _build_messages(
recent_messages.append(llm_message)
used_chars += len(llm_message["content"])
- messages = [{"role": "system", "content": system_prompt}]
+ messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
if summary_block:
messages.append({"role": "system", "content": summary_block})
messages.extend(reversed(recent_messages))
@@ -310,7 +350,7 @@ def _build_messages(
async def _call_llm(
- messages: list[dict[str, str]],
+ messages: list[dict[str, Any]],
*,
max_tokens: int,
temperature: float,
@@ -441,18 +481,29 @@ async def _generate_response(
system_prompt: str,
current_text: str | None = None,
current_name: str | None = None,
+ current_content: Any = None,
max_tokens: int,
temperature: float,
top_p: float,
) -> str:
await _maybe_refresh_summary(chat_id)
- messages = await asyncio.to_thread(
- _build_messages,
- chat_id,
- system_prompt,
- current_text,
- current_name,
- )
+ if current_content is None:
+ messages = await asyncio.to_thread(
+ _build_messages,
+ chat_id,
+ system_prompt,
+ current_text,
+ current_name,
+ )
+ else:
+ messages = await asyncio.to_thread(_build_messages, chat_id, system_prompt, None, None)
+ current_payload = {"role": "user", "content": current_content}
+ current_budget = _estimate_content_length(current_content)
+ used_chars = sum(_estimate_content_length(message.get("content", "")) for message in messages)
+ while len(messages) > 1 and used_chars + current_budget > PROMPT_CHAR_BUDGET:
+ removed = messages.pop(1)
+ used_chars -= _estimate_content_length(removed.get("content", ""))
+ messages.append(current_payload)
return await _call_llm(
messages,
max_tokens=max_tokens,
@@ -597,6 +648,78 @@ async def handle_chat_message(message: Message, *, store_message: bool = True, a
return True
+async def _download_photo_data_url(message: Message) -> str:
+ if not message.photo:
+ raise ValueError("Message has no photo")
+ telegram_file = await message.bot.get_file(message.photo[-1].file_id)
+ buffer = BytesIO()
+ await message.bot.download_file(telegram_file.file_path, destination=buffer)
+ image_bytes = buffer.getvalue()
+ if not image_bytes:
+ raise RuntimeError("Downloaded photo is empty")
+ mime_type = mimetypes.guess_type(telegram_file.file_path or "")[0] or "image/jpeg"
+ if not mime_type.startswith("image/"):
+ mime_type = "image/jpeg"
+ encoded = base64.b64encode(image_bytes).decode("ascii")
+ return f"data:{mime_type};base64,{encoded}"
+
+
+async def handle_photo_message(message: Message) -> bool:
+ if not message.photo or not message.from_user or message.from_user.is_bot:
+ return False
+ if message.caption and message.caption.startswith("/"):
+ return False
+
+ chat_id = message.chat.id
+ user_name = message.from_user.first_name or "кент"
+ caption = _clean_text(message.caption or "")
+
+ await asyncio.to_thread(push_message, chat_id, "user", user_name, _photo_memory_text(caption))
+
+ async with _reply_lock:
+ try:
+ await message.bot.send_chat_action(chat_id=chat_id, action="typing")
+ image_data_url = await _download_photo_data_url(message)
+ prompt_text = (
+ f"{user_name} скинул фото в чат. "
+ "Опиши, что на нём происходит, и вкинь своё злое дедовское мнение."
+ )
+ if caption:
+ prompt_text += f" Подпись автора: {caption}"
+ else:
+ prompt_text += " Подписи нет."
+
+ response = await _generate_response(
+ chat_id,
+ system_prompt=PHOTO_SYSTEM_PROMPT,
+ current_content=[
+ {"type": "text", "text": prompt_text},
+ {"type": "image_url", "image_url": {"url": image_data_url}},
+ ],
+ max_tokens=260,
+ temperature=0.8,
+ top_p=0.9,
+ )
+ except Exception:
+ logger.exception("Photo analysis failed")
+ await message.reply(PHOTO_ERROR_RESPONSE_TEXT, parse_mode=None)
+ return True
+
+ normalized_response = _normalize_reply(response)
+ if not normalized_response:
+ logger.warning(
+ "Photo analysis produced empty/skip response. chat_id=%s user=%s raw=%s",
+ chat_id,
+ user_name,
+ _clip_text(response, 200),
+ )
+ normalized_response = PHOTO_EMPTY_RESPONSE_TEXT
+
+ await asyncio.to_thread(push_message, chat_id, "assistant", BOT_MEMORY_NAME, normalized_response)
+ await message.reply(normalized_response, parse_mode=None)
+ return True
+
+
async def generate_autoreply(chat_id: int, text: str, user_name: str) -> str:
response = await _generate_response(
chat_id,
From c756acf428d81d2424e593a28a95976fef55ca1b Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 17:35:44 +0300
Subject: [PATCH 14/17] add photo triget fix
---
AI/talk_handler.py | 43 ++++++++++++++++++++++++++++++++-----------
1 file changed, 32 insertions(+), 11 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 216a2f2..d501e33 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -88,12 +88,13 @@ PHOTO_EMPTY_RESPONSE_TEXT = "Дед щурился-щурился, а фотка
PHOTO_ERROR_RESPONSE_TEXT = "Тьфу ты, фотку не разобрал, железка опять пердит."
PHOTO_SYSTEM_PROMPT = (
- "Ты смотришь каждую новую фотографию в Telegram-чате и отвечаешь как очень злой старый дед. "
- "Тон: ворчливый, колкий, немного токсичный, но не бессвязный. "
- "Сначала коротко скажи, что вообще происходит на фото, потом добавь своё едкое мнение. "
+ "Ты смотришь каждую новую фотографию в Telegram-чате и отвечаешь как очень злой старый дед, "
+ "которого всё бесит и который никого не собирается гладить по голове. "
+ "Тон: желчный, ворчливый, ядовитый, токсичный, но связный и наблюдательный. "
+ "Сначала коротко скажи, что вообще происходит на фото, потом добавь своё едкое дедовское мнение. "
"Пиши 1–3 предложения, без списков и без длинной простыни. "
"Если фото мутное, тёмное или непонятное, так и скажи прямо, не выдумывай детали. "
- "Не изображай помощника и не извиняйся."
+ "Не изображай помощника, не извиняйся и не сюсюкай."
)
QUESTION_PREFIXES = (
@@ -648,16 +649,26 @@ async def handle_chat_message(message: Message, *, store_message: bool = True, a
return True
-async def _download_photo_data_url(message: Message) -> str:
- if not message.photo:
- raise ValueError("Message has no photo")
- telegram_file = await message.bot.get_file(message.photo[-1].file_id)
+async def _download_image_data_url(message: Message) -> str:
+ telegram_file = None
+ mime_type = "image/jpeg"
+
+ if message.photo:
+ telegram_file = await message.bot.get_file(message.photo[-1].file_id)
+ guessed_mime_type = mimetypes.guess_type(telegram_file.file_path or "")[0]
+ if guessed_mime_type and guessed_mime_type.startswith("image/"):
+ mime_type = guessed_mime_type
+ elif message.document and (message.document.mime_type or "").startswith("image/"):
+ telegram_file = await message.bot.get_file(message.document.file_id)
+ mime_type = message.document.mime_type or mime_type
+ else:
+ raise ValueError("Message has no supported image")
+
buffer = BytesIO()
await message.bot.download_file(telegram_file.file_path, destination=buffer)
image_bytes = buffer.getvalue()
if not image_bytes:
raise RuntimeError("Downloaded photo is empty")
- mime_type = mimetypes.guess_type(telegram_file.file_path or "")[0] or "image/jpeg"
if not mime_type.startswith("image/"):
mime_type = "image/jpeg"
encoded = base64.b64encode(image_bytes).decode("ascii")
@@ -665,7 +676,8 @@ async def _download_photo_data_url(message: Message) -> str:
async def handle_photo_message(message: Message) -> bool:
- if not message.photo or not message.from_user or message.from_user.is_bot:
+ is_image_document = bool(message.document and (message.document.mime_type or "").startswith("image/"))
+ if not (message.photo or is_image_document) or not message.from_user or message.from_user.is_bot:
return False
if message.caption and message.caption.startswith("/"):
return False
@@ -674,12 +686,21 @@ async def handle_photo_message(message: Message) -> bool:
user_name = message.from_user.first_name or "кент"
caption = _clean_text(message.caption or "")
+ logger.info(
+ "Received image message for analysis. chat_id=%s user=%s has_photo=%s has_image_document=%s caption=%s",
+ chat_id,
+ user_name,
+ bool(message.photo),
+ is_image_document,
+ bool(caption),
+ )
+
await asyncio.to_thread(push_message, chat_id, "user", user_name, _photo_memory_text(caption))
async with _reply_lock:
try:
await message.bot.send_chat_action(chat_id=chat_id, action="typing")
- image_data_url = await _download_photo_data_url(message)
+ image_data_url = await _download_image_data_url(message)
prompt_text = (
f"{user_name} скинул фото в чат. "
"Опиши, что на нём происходит, и вкинь своё злое дедовское мнение."
From d3ee72bca9405b6c5730daab19527e903afe6972 Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 17:39:34 +0300
Subject: [PATCH 15/17] add photo triget fix1
---
AI/talk_handler.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index d501e33..21f2c1a 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -686,7 +686,7 @@ async def handle_photo_message(message: Message) -> bool:
user_name = message.from_user.first_name or "кент"
caption = _clean_text(message.caption or "")
- logger.info(
+ logger.warning(
"Received image message for analysis. chat_id=%s user=%s has_photo=%s has_image_document=%s caption=%s",
chat_id,
user_name,
From a2b72b6dfdc8f2f2ca10c6052e8a076b9337787c Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 17:43:00 +0300
Subject: [PATCH 16/17] add photo triget fix2
---
main.py | 26 +++++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/main.py b/main.py
index 9da20a2..6e7821e 100644
--- a/main.py
+++ b/main.py
@@ -19,7 +19,7 @@ from aiogram.types import (
from aiogram.client.default import DefaultBotProperties
# Локальные импорты
-from AI.talk_handler import handle_chat_message, handle_talk, push_message
+from AI.talk_handler import handle_chat_message, handle_photo_message, handle_talk, push_message
from games.casino import play_casino
from games.fortune import generate_fortune
from games.uwu import handle_uwu_cmd
@@ -47,6 +47,28 @@ _polychaetsi_lock = threading.Lock()
_polychaetsi_word_re = re.compile(r"[а-яё]+", re.IGNORECASE)
_no_lesson_re = re.compile(r"(нету\s+пары|нет\s+пары|пары\s+нету|пары\s+нет)", re.IGNORECASE)
+
+def _is_supported_image_message(message: Message) -> bool:
+ return bool(
+ message.photo
+ or (
+ message.document
+ and (message.document.mime_type or "").startswith("image/")
+ )
+ )
+
+
+async def handle_unmatched_message_debug(message: Message):
+ logger.warning(
+ "Unhandled message reached fallback. content_type=%s chat_id=%s has_text=%s has_photo=%s has_document=%s caption=%s",
+ getattr(message, "content_type", None),
+ message.chat.id if message.chat else None,
+ bool(message.text),
+ bool(message.photo),
+ bool(message.document),
+ bool(message.caption),
+ )
+
_weekday_names = {
0: "Понедельник",
1: "Вторник",
@@ -1399,7 +1421,9 @@ def main():
dp.message.register(handle_gen_mem, Command("gen_mem"))
dp.message.register(handle_uwu_cmd, Command("uwu"))
dp.message.register(handle_nude_cmd, Command("nude"))
+ dp.message.register(handle_photo_message, _is_supported_image_message)
dp.message.register(handle_keywords, F.text)
+ dp.message.register(handle_unmatched_message_debug)
async def on_startup(bot: Bot):
try:
From a33443936cef7839ef69b77b9c5e9a5c3ef98fab Mon Sep 17 00:00:00 2001
From: q
Date: Sat, 11 Apr 2026 17:50:10 +0300
Subject: [PATCH 17/17] add photo triget fix4
---
AI/talk_handler.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/AI/talk_handler.py b/AI/talk_handler.py
index 21f2c1a..6fdbffd 100644
--- a/AI/talk_handler.py
+++ b/AI/talk_handler.py
@@ -88,13 +88,13 @@ PHOTO_EMPTY_RESPONSE_TEXT = "Дед щурился-щурился, а фотка
PHOTO_ERROR_RESPONSE_TEXT = "Тьфу ты, фотку не разобрал, железка опять пердит."
PHOTO_SYSTEM_PROMPT = (
- "Ты смотришь каждую новую фотографию в Telegram-чате и отвечаешь как очень злой старый дед, "
- "которого всё бесит и который никого не собирается гладить по голове. "
- "Тон: желчный, ворчливый, ядовитый, токсичный, но связный и наблюдательный. "
- "Сначала коротко скажи, что вообще происходит на фото, потом добавь своё едкое дедовское мнение. "
+ "Ты смотришь каждую новую фотографию в Telegram-чате и отвечаешь как злой, вредный, но очень смешной дед. "
+ "Ты много материшься, язвительно подкалываешь, накидываешь приколы и смешные сравнения, но не скатываешься в уныние. "
+ "Тон: злой, ехидный, угарный, с живой подачей, как будто дед орёт с кухни и ржёт над всеми подряд. "
+ "Сначала коротко скажи, что вообще происходит на фото, потом добавь своё едкое, смешное дедовское мнение. "
"Пиши 1–3 предложения, без списков и без длинной простыни. "
- "Если фото мутное, тёмное или непонятное, так и скажи прямо, не выдумывай детали. "
- "Не изображай помощника, не извиняйся и не сюсюкай."
+ "Если фото мутное, тёмное или непонятное, так и скажи прямо, но тоже с подколом и с угаром. "
+ "Не изображай помощника, не извиняйся, не ной и не делай депрессивный тон."
)
QUESTION_PREFIXES = (