Files
tg_bot_comf/bot.py
T
2026-08-30 18:02:00 +06:00

456 lines
17 KiB
Python

import asyncio
import json
import os
import sqlite3
import tempfile
import uuid
from datetime import datetime
from PIL import Image
import subprocess
import requests
import shutil
import glob
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes, ConversationHandler, CallbackQueryHandler
RULES_CALLBACK = "show_rules"
BACK_CALLBACK = "back_to_main"
COMFYUI_WORKFLOW = "video_generate.json"
TELEGRAM_TOKEN = "TELEGRAM_TOKEN"
COMFYUI_URL = "http://127.0.0.1:8188"
COMFYUI_OUTPUT_DIR = "output/"
COMFYUI_INPUT_DIR = "input/"
COMFYUI_COMMON_PROMT = ["woman take off her clothes, full nudity, full naked, pyssy, boobs, nipples. camera zooms out",""]
COMFYUI_MAX_SIZE = 580
COMFYUI_COST = 20
ADMIN_IDS = [1145483994, 498845556]
WAITING_CONFIRM = 0
def init_db():
conn = sqlite3.connect('bot.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
username TEXT,
balance INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
c.execute('''CREATE TABLE IF NOT EXISTS admin_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
admin_id INTEGER NOT NULL,
admin_username TEXT,
target_user_id INTEGER NOT NULL,
tokens_added INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
conn.commit()
conn.close()
def log_admin_action(admin_id, admin_username, target_user_id, tokens):
"""Записывает действие админа в файл"""
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_entry = (
f"[{timestamp}] Admin {admin_id} (@{admin_username}) "
f"added {tokens} tokens to user {target_user_id}\n"
)
with open('admin_actions.log', 'a', encoding='utf-8') as f:
f.write(log_entry)
def get_user_balance(user_id):
conn = sqlite3.connect('bot.db')
c = conn.cursor()
c.execute('SELECT balance FROM users WHERE user_id = ?', (user_id,))
result = c.fetchone()
conn.close()
return result[0] if result else 0
def add_user(user_id, username):
conn = sqlite3.connect('bot.db')
c = conn.cursor()
c.execute('INSERT OR IGNORE INTO users (user_id, username) VALUES (?, ?)', (user_id, username))
conn.commit()
conn.close()
def update_balance(user_id, amount):
conn = sqlite3.connect('bot.db')
c = conn.cursor()
c.execute('UPDATE users SET balance = balance + ? WHERE user_id = ?', (amount, user_id))
conn.commit()
conn.close()
def deduct_balance(user_id, amount):
conn = sqlite3.connect('bot.db')
c = conn.cursor()
c.execute('UPDATE users SET balance = balance - ? WHERE user_id = ?', (amount, user_id))
conn.commit()
conn.close()
def resize_image(image_path, max_size=COMFYUI_MAX_SIZE):
with Image.open(image_path) as img:
img = img.convert('RGBA')
width, height = img.size
if width > height:
new_width = min(width, max_size)
new_height = int(height * new_width / width)
else:
new_height = min(height, max_size)
new_width = int(width * new_height / height)
resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
return new_width, new_height, resized
def find_video_output_node(workflow_dict):
for node_id, node in workflow_dict.items():
inputs = node.get("inputs", {})
if isinstance(inputs, dict) and "filename_prefix" in inputs:
return node_id
raise ValueError("No video output node with 'filename_prefix' found in workflow!")
def find_latest_video(output_dir, extensions=('.mp4', '.webm', '.mkv')):
"""Ищет самый свежий видеофайл в output_dir"""
all_videos = []
for root, _, files in os.walk(output_dir):
for f in files:
if f.lower().endswith(extensions):
full_path = os.path.join(root, f)
all_videos.append(full_path)
if not all_videos:
return None
return max(all_videos, key=os.path.getmtime)
def modify_workflow(workflow_str, prompt, image_filename, width, height, video_prefix):
wf_raw = json.loads(workflow_str)
wf = {}
for key, value in wf_raw.items():
clean_key = key.strip()
if isinstance(value, dict) and 'inputs' in value:
inputs_raw = value['inputs']
if isinstance(inputs_raw, dict):
clean_inputs = {}
for in_key, in_val in inputs_raw.items():
clean_inputs[in_key.strip()] = in_val
value = {k: v for k, v in value.items() if k != 'inputs'}
value['inputs'] = clean_inputs
wf[clean_key] = value
if '93' in wf and 'inputs' in wf['93']:
wf['93']['inputs']['text'] = f"{COMFYUI_COMMON_PROMT}"
height = (height // 16) * 16
width = (width // 16) * 16
if '183' in wf and 'inputs' in wf['183']:
wf['183']['inputs']['value'] = height
if '184' in wf and 'inputs' in wf['184']:
wf['184']['inputs']['value'] = width
if '193' in wf and 'inputs' in wf['193']:
wf['193']['inputs']['image'] = image_filename
if '214' in wf and 'inputs' in wf['214']:
wf['214']['inputs']['filename_prefix'] = video_prefix
return json.dumps(wf)
def find_video_by_prefix(output_dir, prefix, extensions=('.mp4', '.webm', '.mkv')):
for ext in extensions:
pattern = os.path.join(output_dir, '**', f"{prefix}*{ext}")
matches = glob.glob(pattern, recursive=True)
if matches:
return max(matches, key=os.path.getmtime)
return None
async def run_comfyui_workflow(workflow_json_str, input_image_path, video_prefix, update: Update):
workflow_dict = json.loads(workflow_json_str)
client_id = str(uuid.uuid4())
async with aiohttp.ClientSession() as session:
async with session.post(f"{COMFYUI_URL}/prompt", json={
"prompt": workflow_dict,
"client_id": client_id
}) as resp:
if resp.status != 200:
raise Exception(f"ComfyUI API error {resp.status}")
prompt_id = (await resp.json())["prompt_id"]
progress_msg = await update.message.reply_text("🚀 Запуск генерации...")
uri = f"ws://127.0.0.1:8188/ws?clientId={client_id}"
try:
async with websockets.connect(uri) as websocket:
while True:
try:
msg = await asyncio.wait_for(websocket.recv(), timeout=300.0)
data = json.loads(msg)
if data["type"] == "executing":
node = data["data"].get("node")
if node is None:
break
try:
await progress_msg.edit_text("🔄 Обрабатываю...")
except BadRequest as e:
if "Message is not modified" not in str(e):
raise
elif data["type"] == "progress":
value = data["data"].get("value", 0)
max_val = data["data"].get("max", 1)
pct = int(100 * value / max_val) if max_val else 0
await progress_msg.edit_text(f"⏳ Прогресс: {pct}%")
elif data["type"] == "execution_error":
err = data["data"].get("exception_message", "Ошибка")
raise Exception(err)
except asyncio.TimeoutError:
break
except Exception as e:
print(f"WebSocket error: {e}")
await progress_msg.edit_text("🎥 Собираю видео...")
for _ in range(60):
await asyncio.sleep(2)
video_path = find_latest_video(COMFYUI_OUTPUT_DIR)
if video_path and os.path.getsize(video_path) > 1024:
await progress_msg.edit_text("✅ Готово!")
return video_path
raise Exception("Видео не найдено")
def clean_metadata(video_path):
temp_dir = os.path.dirname(video_path)
with tempfile.NamedTemporaryFile(suffix='.mp4', dir=temp_dir, delete=False) as tmp:
temp_path = tmp.name
try:
subprocess.run([
'ffmpeg', '-y', '-i', video_path,
'-map_metadata', '-1',
'-c:v', 'libx264',
'-c:a', 'aac',
'-pix_fmt', 'yuv420p',
temp_path
], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
shutil.copy2(temp_path, video_path)
os.unlink(temp_path)
return video_path
except Exception as e:
if os.path.exists(temp_path):
os.unlink(temp_path)
raise Exception(f"Metadata cleanup failed: {str(e)}")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
add_user(user.id, user.username)
balance = get_user_balance(user.id)
keyboard = [[InlineKeyboardButton("ПРАВИЛА", callback_data=RULES_CALLBACK)]]
await update.message.reply_text(
f"👋🏻 Привет, Творец!\n\n"
"Рада видеть тебя в SecretRoom\n\n"
"✅ Любая фантазия о которой ты мечтал оживёт в этом боте\n\n"
"Жми «🔮 Оживить фото» и наслаждайся\n\n"
"Стоимость: 20 токен за запрос.\n\n"
f"Ваш баланс: {balance} токенов.",
reply_markup=InlineKeyboardMarkup(keyboard),
)
return ConversationHandler.END
async def handle_admin_add(update: Update, context: ContextTypes.DEFAULT_TYPE):
if update.effective_user.id not in ADMIN_IDS:
await update.message.reply_text("У вас нет прав администратора.")
return
try:
tokens = int(context.args[0])
target_id = int(context.args[1])
update_balance(target_id, tokens)
log_admin_action(
update.effective_user.id,
update.effective_user.username or 'unknown',
target_id,
tokens
)
await update.message.reply_text(f"Добавлено {tokens} токенов пользователю {target_id}.")
except (IndexError, ValueError):
await update.message.reply_text("Использование: /add <токены> <user_id>")
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
photo_file = await update.message.photo[-1].get_file()
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
temp_image = tmp.name
await photo_file.download_to_drive(temp_image)
width, height, resized_img = resize_image(temp_image)
resized_path = temp_image.replace('.png', '_resized.png')
resized_img.save(resized_path, format='PNG')
os.unlink(temp_image)
context.user_data['image_path'] = resized_path
context.user_data['width'] = width
context.user_data['height'] = height
video_prefix = f"video_{uuid.uuid4().hex[:12]}"
context.user_data['video_prefix'] = video_prefix
cost = COMFYUI_COST
balance = get_user_balance(update.effective_user.id)
keyboard = [
[InlineKeyboardButton("✅ Начать (Пресет 1)", callback_data="preset_1")],
[InlineKeyboardButton("🎭 Начать (Пресет 2)", callback_data="preset_2")],
[InlineKeyboardButton("❌ Отмена", callback_data="cancel_gen")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
f"Стоимость генерации: {cost} токенов.\n"
f"Ваш баланс: {balance}\n"
"Выберите пресет для генерации:",
reply_markup=reply_markup
)
return ConversationHandler.END
async def handle_photo_anywhere(update: Update, context: ContextTypes.DEFAULT_TYPE):
return await handle_photo(update, context)
async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer()
if query.data == "preset_1":COMFYUI_COMMON_PROMT[0]
if query.data == "preset_2":COMFYUI_COMMON_PROMT[1]
if query.data == RULES_CALLBACK:
keyboard = [[InlineKeyboardButton("← Назад", callback_data=BACK_CALLBACK)]]
await query.message.reply_text(
"🖼 Творец, отправь фото ниже!:\n\n"
"Выбери фото человека или из аниме🙈:\n\n"
"Для лучшего результата следуй этим правилам:\n\n"
"✅ Фотография в полный рост\n"
"✅ Девушка смотрит прямо в камеру\n"
"✅ Хорошее освещение\n"
"❌ Нет солнцезащитных очков\n"
"❌ Не закрывать лицо волосами",
reply_markup=InlineKeyboardMarkup(keyboard)
)
await query.message.delete()
elif query.data == "preset_1":
await query.answer()
await start_generation(update, context, preset_index=0)
elif query.data == "preset_2":
await query.answer()
await start_generation(update, context, preset_index=1)
elif query.data == "cancel_gen":
await query.answer()
image_path = context.user_data.get('image_path')
if image_path and os.path.exists(image_path):
os.unlink(image_path)
context.user_data.clear()
await query.message.edit_text("Операция отменена.")
elif query.data == BACK_CALLBACK:
context.user_data.clear()
user = update.effective_user
add_user(user.id, user.username)
balance = get_user_balance(user.id)
keyboard = [[InlineKeyboardButton("ПРАВИЛА", callback_data=RULES_CALLBACK)]]
await query.message.reply_text(
f"👋🏻 Привет, Творец!\n\n"
"Рада видеть тебя в SecretRoom\n\n"
"✅ Любая фантазия о которой ты мечтал оживёт в этом боте\n\n"
"Жми «🔮 Оживить фото» и наслаждайся\n\n"
"Стоимость: 20 токен за запрос.\n\n"
f"Ваш баланс: {balance} токенов.",
reply_markup=InlineKeyboardMarkup(keyboard)
)
await query.message.delete()
async def cancel(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("Отменено.")
image_path = context.user_data.get('image_path')
if image_path and os.path.exists(image_path):
os.unlink(image_path)
context.user_data.clear()
return ConversationHandler.END
async def start_generation(update: Update, context: ContextTypes.DEFAULT_TYPE, preset_index: int):
query = update.callback_query
user_id = update.effective_user.id
cost = COMFYUI_COST
balance = get_user_balance(user_id)
if balance < cost:
await query.edit_message_text("❌ Недостаточно токенов.")
context.user_data.clear()
return
deduct_balance(user_id, cost)
await query.edit_message_text("🚀 Начинаю генерацию...")
try:
image_path = context.user_data['image_path']
video_prefix = context.user_data['video_prefix']
selected_prompt = COMFYUI_COMMON_PROMT[preset_index]
os.makedirs(COMFYUI_INPUT_DIR, exist_ok=True)
image_filename = f"user_image_{uuid.uuid4().hex[:12]}.png"
input_image_path = os.path.join(COMFYUI_INPUT_DIR, image_filename)
shutil.copy(image_path, input_image_path)
with open(COMFYUI_WORKFLOW, 'r', encoding='utf-8') as f:
workflow_base = f.read()
modified_workflow = modify_workflow(
workflow_base,
selected_prompt,
image_filename,
context.user_data['width'],
context.user_data['height'],
video_prefix
)
video_path = await run_comfyui_workflow(modified_workflow, input_image_path, video_prefix)
video_path = clean_metadata(video_path)
with open(video_path, 'rb') as video_file:
await query.message.reply_video(video_file, supports_streaming=True)
await query.message.reply_text("✅ Генерация завершена!")
for p in [image_path, input_image_path, video_path]:
if os.path.exists(p):
os.unlink(p)
except Exception as e:
await query.message.reply_text(f"❌ Ошибка генерации: {str(e)}")
update_balance(user_id, cost)
new_balance = get_user_balance(user_id)
await query.message.reply_text(f"Ваш баланс: {new_balance} токенов.")
context.user_data.clear()
def main():
init_db()
app = Application.builder().token(TELEGRAM_TOKEN).build()
conv_handler = ConversationHandler(
entry_points=[MessageHandler(filters.PHOTO & ~filters.COMMAND, handle_photo)],
states={},
fallbacks=[],
)
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("add", handle_admin_add))
app.add_handler(conv_handler)
app.add_handler(CallbackQueryHandler(handle_callback))
print(f"Бот запущен. Output dir: {COMFYUI_OUTPUT_DIR}")
app.run_polling()
if __name__ == '__main__':
main()