It's version 0.4

This commit is contained in:
Niken
2025-10-19 14:28:41 +03:00
parent 772d3d5b83
commit 7b653d4dcc
32 changed files with 775 additions and 326 deletions
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+57 -37
View File
@@ -12,8 +12,8 @@ import uuid
from config import Config
# Настройка кодировки для всего приложения
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
logger = logging.getLogger(__name__)
@@ -24,13 +24,15 @@ LIMIT = 2 * 1024 * 1024 * 1024 # 2 ГБ
async def safe_filename(name: str) -> str:
"""Создает безопасное имя файла"""
# Нормализуем Unicode символы
normalized = unicodedata.normalize('NFKD', name)
normalized = unicodedata.normalize("NFKD", name)
# Убираем акценты и специальные символы, оставляем только ASCII
ascii_name = normalized.encode('ascii', 'ignore').decode('ascii')
ascii_name = normalized.encode("ascii", "ignore").decode("ascii")
# Заменяем проблемные символы
safe_name = "".join(c if c.isalnum() or c in (' ', '-', '_', '.') else '_' for c in ascii_name)
safe_name = "".join(
c if c.isalnum() or c in (" ", "-", "_", ".") else "_" for c in ascii_name
)
# Убираем множественные подчеркивания и обрезаем длину
safe_name = '_'.join(filter(None, safe_name.split('_')))
safe_name = "_".join(filter(None, safe_name.split("_")))
return safe_name[:100] or f"video_{uuid.uuid4().hex[:8]}"
@@ -38,21 +40,23 @@ async def get_video_info(url: str) -> dict:
"""Получает информацию о видео через yt-dlp"""
try:
process = await asyncio.create_subprocess_exec(
'yt-dlp',
'--dump-json',
'--no-playlist',
"yt-dlp",
"--dump-json",
"--no-playlist",
url,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
result = json.loads(stdout.decode('utf-8', errors='ignore'))
logger.info(f"Информация о видео получена: {result.get('title', 'Unknown')}")
result = json.loads(stdout.decode("utf-8", errors="ignore"))
logger.info(
f"Информация о видео получена: {result.get('title', 'Unknown')}"
)
return result
else:
error_msg = stderr.decode('utf-8', errors='ignore')
error_msg = stderr.decode("utf-8", errors="ignore")
logger.warning(f"yt-dlp ошибка: {error_msg}")
except Exception as e:
@@ -75,38 +79,54 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]:
duration = 0
if video_info:
title = await safe_filename(video_info.get('title', 'Unknown_Title'))
uploader = await safe_filename(video_info.get('uploader', 'Unknown_Uploader'))
duration = video_info.get('duration', 0)
title = await safe_filename(video_info.get("title", "Unknown_Title"))
uploader = await safe_filename(
video_info.get("uploader", "Unknown_Uploader")
)
duration = video_info.get("duration", 0)
logger.info(f"Обработано видео: {title}")
# ОПТИМИЗИРОВАННЫЕ НАСТРОЙКИ ДЛЯ СКОРОСТИ
download_process = await asyncio.create_subprocess_exec(
'yt-dlp',
'-f', 'bestvideo[height<=720][filesize<800M]+bestaudio/best[height<=720][filesize<800M]',
'--no-playlist',
'-o', output_template,
'--ignore-errors',
'--no-warnings',
'--format-sort', 'quality,res:720,size:800M',
'--concurrent-fragments', '4',
"yt-dlp",
"-f",
"bestvideo[height<=720][filesize<800M]+bestaudio/best[height<=720][filesize<800M]",
"--no-playlist",
"-o",
output_template,
"--ignore-errors",
"--no-warnings",
"--format-sort",
"quality,res:720,size:800M",
"--concurrent-fragments",
"4",
url,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(download_process.communicate(), timeout=600) # Уменьшил таймаут
stdout, stderr = await asyncio.wait_for(
download_process.communicate(), timeout=600
) # Уменьшил таймаут
# Остальной код без изменений...
if download_process.returncode != 0:
error_msg = stderr.decode('utf-8', errors='ignore') if stderr else "Unknown error"
error_msg = (
stderr.decode("utf-8", errors="ignore")
if stderr
else "Unknown error"
)
logger.error(f"Ошибка скачивания: {error_msg}")
raise Exception(f"Ошибка скачивания: {error_msg}")
mp4_files = glob.glob(os.path.join(temp_dir, "*.mp4"))
if not mp4_files:
video_files = glob.glob(os.path.join(temp_dir, "*.*"))
video_files = [f for f in video_files if f.lower().endswith(('.mp4', '.mkv', '.avi', '.mov', '.webm'))]
video_files = [
f
for f in video_files
if f.lower().endswith((".mp4", ".mkv", ".avi", ".mov", ".webm"))
]
if video_files:
mp4_files = [video_files[0]]
@@ -123,25 +143,27 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]:
dbx = dropbox.Dropbox(DROPBOX_TOKEN)
dropbox_path = f"/{final_filename}"
logger.info(f"Загружаем файл в Dropbox: {dropbox_path} (размер: {size / (1024 * 1024):.1f} MB)")
logger.info(
f"Загружаем файл в Dropbox: {dropbox_path} (размер: {size / (1024 * 1024):.1f} MB)"
)
with open(actual_file, "rb") as f:
file_data = f.read()
dbx.files_upload(
file_data,
dropbox_path,
mode=dropbox.files.WriteMode("overwrite")
mode=dropbox.files.WriteMode("overwrite"),
)
shared_link = dbx.sharing_create_shared_link_with_settings(dropbox_path)
link = shared_link.url.replace("?dl=0", "?dl=1")
metadata = {
'title': title,
'uploader': uploader,
'duration': duration,
'filesize': size,
'quality': 'optimized for speed'
"title": title,
"uploader": uploader,
"duration": duration,
"filesize": size,
"quality": "optimized for speed",
}
logger.info(f"Успешно загружено в Dropbox: {link}")
@@ -154,5 +176,3 @@ async def download_mp4_to_dropbox(url: str) -> tuple[str, dict]:
except Exception as e:
logger.error(f"Общая ошибка: {e}")
raise e
+8 -7
View File
@@ -2,18 +2,16 @@ import logging
from aiogram import Dispatcher, Bot
from aiogram.filters import Command
from models.state import BotState
from utils.antispam import admin_required
from .dowmp4 import download_mp4_to_dropbox
logger = logging.getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("dowmp4"))
#@admin_required(4)
# @admin_required(4)
async def dowmp4_handler(message):
"""Обработчик команды /dowmp4"""
try:
@@ -22,7 +20,9 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await message.answer("Пожалуйста, укажите URL видео после команды /dowmp4")
return
processing_msg = await message.answer("⏳ Начинаю обработку видео... Это может занять несколько минут.")
processing_msg = await message.answer(
"⏳ Начинаю обработку видео... Это может занять несколько минут."
)
try:
# Скачиваем и загружаем в Dropbox
@@ -44,12 +44,13 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await message.answer(
f"✅ **Видео успешно обработано!**\n\n{caption}",
parse_mode="Markdown",
disable_web_page_preview=True
disable_web_page_preview=True,
)
except ValueError as e:
await message.answer(f"❌ Ошибка: {str(e)}")
except Exception as e:
logger.error(f"Ошибка при обработке /dowmp4: {e}", exc_info=True)
await message.answer("❌ Произошла ошибка при обработке видео. Попробуйте позже.")
await message.answer(
"❌ Произошла ошибка при обработке видео. Попробуйте позже."
)
+9
View File
@@ -0,0 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+190
View File
@@ -0,0 +1,190 @@
import logging
import base64
from io import BytesIO
import asyncio
import aiohttp
from aiogram import Dispatcher, Bot
from aiogram.types import Message, FSInputFile, BufferedInputFile
from aiogram.filters import Command
from models.state import BotState
from config import Config
from storage.message_storage import save_message
from transformers import pipeline
from utils.antispam import saving
logger = logging.getLogger(__name__)
SD_URL = "http://192.168.31.95:7860/sdapi/v1/txt2img"
# Загружаем пайплайн перевода один раз при старте (синхронный)
translator = pipeline("translation", model="Helsinki-NLP/opus-mt-ru-en")
async def translate_to_en(text: str) -> str:
try:
# выполняем перевод в отдельном потоке, чтобы не блокировать event loop
result = await asyncio.to_thread(translator, text, max_length=512)
return result[0]["translation_text"]
except Exception as e:
logger.error(f"Ошибка перевода: {e}")
return text
async def generate_img2img(prompt: str, init_image: BytesIO) -> BytesIO | None:
"""
Генерация изображения по методу img2img.
:param prompt: текстовый промт (уже переведённый на английский)
:param init_image: входное изображение в BytesIO
:return: BytesIO с результатом или None при ошибке
"""
try:
# кодируем входное изображение в base64
init_image_base64 = base64.b64encode(init_image.getvalue()).decode("utf-8")
payload = {
"init_images": [init_image_base64],
"prompt": prompt,
"negative_prompt": "blurry, low quality, bad anatomy, watermark, text, cropped",
"steps": 20, # можно 1520
"width": 1024, # лучше подставлять размеры исходного фото
"height": 1024,
"sampler_name": "Euler a", # мягкий и стабильный для img2img
"Schedule_type": "Karras",
"cfg_scale": 6, # чуть ниже, чем для txt2img
"seed": -1,
"denoising_strength": 0.8, # 0.3–0.5 для «сохранить стиль», 0.6–0.8 для «перерисовать»
"restore_faces": True, # если работаешь с людьми
"override_settings": {
"sd_model_checkpoint": "waiNSFWIllustrious_v150.safetensors"
},
}
async with aiohttp.ClientSession() as session:
async with session.post(
SD_URL.replace("txt2img", "img2img"), json=payload
) as resp:
if resp.status != 200:
logger.error(f"Stable Diffusion img2img API error: {resp.status}")
return None
r = await resp.json()
image_base64 = r["images"][0]
return BytesIO(base64.b64decode(image_base64))
except Exception as e:
logger.error(f"Ошибка img2img: {e}")
return None
# sd_xl_base_1.0.safetensors
# waiNSFWIllustrious_v150.safetensors
async def generate_image(prompt: str) -> BytesIO | None:
payload = {
"prompt": prompt,
"negative_prompt": "blurry, low quality, bad anatomy, watermark, text, cropped",
"steps": 20,
"width": 1024,
"height": 1024,
"sampler_name": "Euler a", # сэмплер
"cfg_scale": 7, # насколько строго следовать промту
"seed": -1, # -1 = случайный сид
"batch_size": 1, # сколько картинок за раз
"n_iter": 1, # сколько раз повторить генерацию
"restore_faces": False, # восстановление лиц
"tiling": False, # тайлинг для текстур
"enable_hr": False, # highres fix (двухэтапная генерация)
"denoising_strength": 0.7, # сила денойзинга (актуально при enable_hr или img2img)
"hr_scale": 2, # во сколько раз увеличить при highres fix
"hr_upscaler": "Latent", # апскейлер для highres fix
"override_settings": {
"sd_model_checkpoint": "waiNSFWIllustrious_v150.safetensors" # выбор модели
},
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(SD_URL, json=payload) as resp:
if resp.status != 200:
logger.error(f"Stable Diffusion API error: {resp.status}")
return None
r = await resp.json()
image_base64 = r["images"][0]
return BytesIO(base64.b64decode(image_base64))
except Exception as e:
logger.error(f"Ошибка генерации изображения: {e}")
return None
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("draw"))
async def draw(message: Message):
save_message(message.chat.id, message.message_id)
if message.from_user.id in Config.BAN:
msg = await message.reply("Вы в бане")
save_message(msg.chat.id, msg.message_id)
else:
user_prompt = message.text.replace("/draw", "").strip()
if not user_prompt:
confirm_msg = await message.answer("❗ Укажи промт после команды /draw")
save_message(confirm_msg.chat.id, confirm_msg.message_id)
return
en_prompt = await translate_to_en(user_prompt)
logger.info(f"Промт переведен: {user_prompt} -> {en_prompt}")
img_bytes = await generate_image(en_prompt)
if img_bytes:
img_bytes.seek(0)
photo = BufferedInputFile(img_bytes.read(), filename="result.png")
msg = await bot.send_photo(chat_id=message.chat.id, photo=photo)
save_message(msg.chat.id, msg.message_id)
else:
error_msg = await message.answer("⚠️ Ошибка при генерации изображения.")
save_message(error_msg.chat.id, error_msg.message_id)
@dp.message(Command("img2img"))
@saving
async def img2img_with_caption(message: Message, bot: Bot):
raw_caption = message.caption or ""
user_prompt = raw_caption.replace("/img2img", "").strip()
if not user_prompt:
await message.answer(
"❗ Укажи промт в подписи к фото после команды /img2img"
)
return
en_prompt = await translate_to_en(user_prompt)
logger.info(f"Промт для img2img переведен: {user_prompt} -> {en_prompt}")
try:
if message.photo:
# Берём последнее (самое большое) фото
photo = message.photo[-1].file_id
# Отправляем в SD API по file_id (как в iadmin)
# Здесь отличие: SD API требует base64, поэтому file_id нужно скачать
# Но логика построена как в iadmin — сначала берём file_id
file = await bot.get_file(photo)
file_bytes = await bot.download_file(file.file_path)
img_bytes = await generate_img2img(
en_prompt, BytesIO(file_bytes.read())
)
if img_bytes:
img_bytes.seek(0)
photo = BufferedInputFile(
img_bytes.read(), filename="img2img_result.png"
)
msg = await bot.send_photo(chat_id=message.chat.id, photo=photo)
else:
msg = await message.answer("⚠️ Ошибка при img2img генерации.")
else:
msg = await message.answer("❗ Пришли фото с подписью /img2img <промт>")
save_message(msg.chat.id, msg.message_id)
except Exception as e:
await message.answer(f"⚠️ Ошибка: {e}")
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+56 -40
View File
@@ -5,6 +5,7 @@ import logging
import glob
import json
import requests
from typing import Optional
from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3, APIC, error
@@ -16,13 +17,14 @@ async def get_video_info(url: str) -> dict:
"""Получает информацию о видео через yt-dlp"""
try:
process = await asyncio.create_subprocess_exec(
'yt-dlp',
'--dump-json',
'--no-playlist',
'--cookies', '~/myfirstprogramm/addons/example_addon/cookies.txt',
"yt-dlp",
"--dump-json",
"--no-playlist",
"--cookies",
"~/myfirstprogramm/addons/example_addon/cookies.txt",
url,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
@@ -33,17 +35,19 @@ async def get_video_info(url: str) -> dict:
return None
async def download_thumbnail(thumbnail_url: str) -> tuple[bytes, str]:
async def download_thumbnail(
thumbnail_url: str,
) -> tuple[Optional[bytes], Optional[str]]:
"""Скачивает обложку видео и возвращает данные и MIME тип"""
try:
response = requests.get(thumbnail_url, timeout=10)
if response.status_code == 200:
if 'jpeg' in thumbnail_url or 'jpg' in thumbnail_url:
mime_type = 'image/jpeg'
elif 'png' in thumbnail_url:
mime_type = 'image/png'
if "jpeg" in thumbnail_url or "jpg" in thumbnail_url:
mime_type = "image/jpeg"
elif "png" in thumbnail_url:
mime_type = "image/png"
else:
mime_type = response.headers.get('Content-Type', 'image/jpeg')
mime_type = response.headers.get("Content-Type", "image/jpeg")
return response.content, mime_type
except Exception as e:
logger.warning(f"Не удалось скачать обложку: {e}")
@@ -59,19 +63,19 @@ def apply_metadata(mp3_path: str, metadata: dict):
audio = EasyID3()
audio.save(mp3_path)
audio['title'] = metadata.get('title', 'Unknown Title')
audio['artist'] = metadata.get('performer', 'Unknown Artist')
audio["title"] = metadata.get("title", "Unknown Title")
audio["artist"] = metadata.get("performer", "Unknown Artist")
audio.save(mp3_path)
if metadata.get('thumbnail_data'):
if metadata.get("thumbnail_data"):
audio = ID3(mp3_path)
audio.add(
APIC(
encoding=3,
mime=metadata.get('thumbnail_mime', 'image/jpeg'),
mime=metadata.get("thumbnail_mime", "image/jpeg"),
type=3, # front cover
desc='Cover',
data=metadata['thumbnail_data']
desc="Cover",
data=metadata["thumbnail_data"],
)
)
audio.save(mp3_path)
@@ -96,27 +100,32 @@ async def download_mp3_isolated(url: str) -> tuple[str, dict]:
duration = 0
if video_info:
title = video_info.get('title', 'Unknown Title')
uploader = video_info.get('uploader', 'Unknown Artist')
thumbnail_url = video_info.get('thumbnail')
if not thumbnail_url and video_info.get('thumbnails'):
thumbnails = video_info.get('thumbnails', [])
title = video_info.get("title", "Unknown Title")
uploader = video_info.get("uploader", "Unknown Artist")
thumbnail_url = video_info.get("thumbnail")
if not thumbnail_url and video_info.get("thumbnails"):
thumbnails = video_info.get("thumbnails", [])
if thumbnails:
thumbnail_url = thumbnails[-1].get('url')
duration = video_info.get('duration', 0)
thumbnail_url = thumbnails[-1].get("url")
duration = video_info.get("duration", 0)
logger.info(f"Получена информация о видео: {title}")
process = await asyncio.create_subprocess_exec(
'yt-dlp',
'-x', '--audio-format', 'mp3',
'--cookies', '~/myfirstprogramm/addons/example_addon/cookies.txt',
'--audio-quality', '320K',
'--no-playlist',
'-o', output_template,
'--ignore-errors',
"yt-dlp",
"-x",
"--audio-format",
"mp3",
"--cookies",
"~/myfirstprogramm/addons/example_addon/cookies.txt",
"--audio-quality",
"320K",
"--no-playlist",
"-o",
output_template,
"--ignore-errors",
url,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300)
@@ -125,24 +134,31 @@ async def download_mp3_isolated(url: str) -> tuple[str, dict]:
if mp3_files:
actual_file = mp3_files[0]
if os.path.getsize(actual_file) > 1000:
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as final_file:
with tempfile.NamedTemporaryFile(
suffix=".mp3", delete=False
) as final_file:
final_filename = final_file.name
with open(actual_file, 'rb') as src, open(final_filename, 'wb') as dst:
with (
open(actual_file, "rb") as src,
open(final_filename, "wb") as dst,
):
dst.write(src.read())
thumbnail_data, mime_type = None, None
if thumbnail_url:
thumbnail_data, mime_type = await download_thumbnail(thumbnail_url)
thumbnail_data, mime_type = await download_thumbnail(
thumbnail_url
)
if thumbnail_data:
logger.info(f"Обложка скачана: {thumbnail_url}")
metadata = {
'title': title,
'performer': uploader,
'duration': duration,
'thumbnail_data': thumbnail_data,
'thumbnail_mime': mime_type
"title": title,
"performer": uploader,
"duration": duration,
"thumbnail_data": thumbnail_data,
"thumbnail_mime": mime_type,
}
# Прописываем теги в MP3
+23 -16
View File
@@ -3,11 +3,14 @@ from aiogram.filters import Command
from models.state import BotState
from utils.antispam import admin_required
from logging import getLogger
from .dowloadmp3_to_youtube import *
from .dowloadmp3_to_youtube import download_mp3_isolated
import tempfile
import asyncio
from os import path, unlink
logger = getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("dowmp3"))
@admin_required(5)
@@ -18,7 +21,9 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return
url = args[1]
logger.info(f"Получена команда /dowmp3 от user_id={message.from_user.id}, url={url}")
logger.info(
f"Получена команда /dowmp3 от user_id={message.from_user.id}, url={url}"
)
status_msg = await message.reply("⏳ Скачиваю аудио... Это займет 1-2 минуты")
@@ -29,30 +34,32 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if file_size < 1000:
raise Exception("Файл слишком маленький")
await status_msg.edit_text(f"✅ Аудио готово! Отправляю...")
await status_msg.edit_text("✅ Аудио готово! Отправляю...")
# Подготавливаем аудио файл
audio_input = types.FSInputFile(filename)
# Базовые параметры
send_params = {
'audio': audio_input,
'title': metadata['title'][:64],
'performer': metadata['performer'][:64],
'duration': int(metadata['duration']) if metadata['duration'] else None,
'caption': f"🎵 {metadata['title']}\n👤 {metadata['performer']}"
"audio": audio_input,
"title": metadata["title"][:64],
"performer": metadata["performer"][:64],
"duration": int(metadata["duration"]) if metadata["duration"] else None,
"caption": f"🎵 {metadata['title']}\n👤 {metadata['performer']}",
}
# Добавляем обложку если есть
if metadata['thumbnail_data']:
if metadata["thumbnail_data"]:
try:
# Создаем временный файл для обложки
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as thumb_file:
with tempfile.NamedTemporaryFile(
suffix=".jpg", delete=False
) as thumb_file:
thumb_filename = thumb_file.name
thumb_file.write(metadata['thumbnail_data'])
thumb_file.write(metadata["thumbnail_data"])
# Используем FSInputFile для обложки
send_params['thumbnail'] = types.FSInputFile(thumb_filename)
send_params["thumbnail"] = types.FSInputFile(thumb_filename)
logger.info("Обложка добавлена к сообщению")
except Exception as e:
@@ -62,7 +69,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await message.answer_audio(**send_params)
# Удаляем временный файл обложки если создавали
if 'thumb_filename' in locals() and path.exists(thumb_filename):
if "thumb_filename" in locals() and path.exists(thumb_filename):
unlink(thumb_filename)
await status_msg.delete()
@@ -74,8 +81,8 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
await status_msg.edit_text(f"❌ Ошибка: {str(e)}")
logger.error(f"Ошибка при скачивании: {e}")
finally:
if 'filename' in locals() and path.exists(filename):
if "filename" in locals() and path.exists(filename):
try:
unlink(filename)
except:
pass
except OSError:
pass
+9
View File
@@ -0,0 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+105
View File
@@ -0,0 +1,105 @@
import base64
import aiohttp
import logging
from aiogram import Dispatcher, Bot
from aiogram.types import Message
from utils.antispam import saving, save_message
from aiogram.filters import Command
from models.state import BotState
logger = logging.getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
chat_history = {}
MAX_HISTORY = 20 # храним последние 20 сообщений (user+assistant)
@dp.message(Command("gpt"))
@saving
async def ask_gpt(message: Message):
chat_id = message.chat.id
if chat_id not in chat_history:
chat_history[chat_id] = []
content_blocks = []
user_prompt = None
# Текст после команды или caption
if message.text:
parts = message.text.split(maxsplit=1)
if len(parts) > 1:
user_prompt = parts[1]
if message.caption:
user_prompt = message.caption
if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt})
# Фото → base64 → image_url
if message.photo:
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
content_blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
}
)
if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото")
return
url = "http://192.168.31.197:1234/v1/chat/completions"
# Добавляем новое сообщение в историю
chat_history[chat_id].append({"role": "user", "content": content_blocks})
# Ограничиваем историю (оставляем последние MAX_HISTORY сообщений)
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
payload = {
"model": "qwen/qwen3-vl-4b",
"messages": chat_history[ chat_id],
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, json=payload) as resp:
if resp.status != 200:
error_text = await resp.text()
await message.reply(
f"❌ Ошибка LM Studio: {resp.status} {error_text}"
)
return
data = await resp.json()
reply_text = data["choices"][0]["message"]["content"]
# Сохраняем ответ ассистента в историю
chat_history[chat_id].append(
{
"role": "assistant",
"content": [{"type": "text", "text": reply_text}],
}
)
# Ограничиваем снова (чтобы не разрасталось)
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
msg = await message.reply(f"🤖 Ответ:\n{reply_text}")
save_message(msg.chat.id, msg.message_id)
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@dp.message(Command("clear"))
async def clear(message: Message):
chat_history.pop(message.chat.id, None)
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+3 -3
View File
@@ -5,10 +5,11 @@ from models.state import BotState
from config import Config
import logging
from utils.antispam import admin_required
from storage.message_storage import save_message # импортируем функцию
from storage.message_storage import save_message # импортируем функцию
logger = logging.getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("hello"))
@admin_required(1)
@@ -20,8 +21,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
try:
name = Config.Names.get(admin_id, "Админ")
msg = await bot.send_message(
chat_id=admin_id,
text=f"🤖 Я готов к работе, господин {name}!"
chat_id=admin_id, text=f"🤖 Я готов к работе, господин {name}!"
)
# сохраняем сообщение, отправленное админу
save_message(msg.chat.id, msg.message_id)
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+2 -1
View File
@@ -9,9 +9,10 @@ API_URL = "http://127.0.0.1:7700/speak"
logger = getLogger(__name__)
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("id"))
@saving
async def id(message: Message):
id = message.from_user.id
msg = await message.reply(str(id))
msg = await message.reply(str(id))
+1
View File
@@ -2,6 +2,7 @@ import importlib
import sys
from pathlib import Path
class AddonManager:
def __init__(self, dp, state, bot):
self.dp = dp
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+23 -30
View File
@@ -1,6 +1,4 @@
from config import Config
import aiohttp
from aiogram.types import BufferedInputFile
from utils.antispam import admin_required
from aiogram import Dispatcher, Bot
from aiogram.types import Message
@@ -11,22 +9,7 @@ from aiogram.types import PollAnswer
from storage.message_storage import save_message
logger = getLogger(__name__)
API_URL = "http://127.0.0.1:7700/speak"
from config import Config
import aiohttp
from aiogram.types import BufferedInputFile
from utils.antispam import admin_required
from aiogram import Dispatcher, Bot
from aiogram.types import Message
from models.state import BotState
from aiogram.filters import Command
from logging import getLogger
from aiogram.types import PollAnswer
from storage.message_storage import save_message
logger = getLogger(__name__)
API_URL = "http://127.0.0.1:7700/speak"
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("poll"))
@@ -37,9 +20,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
poll_msg = await bot.send_poll(
chat_id=chat_id,
question="Кто опоздает?",
options=["Я", "Не знаю", "Наверное"],
options=["Я", "Я очень сильно опоздаю", "Наверное"],
is_anonymous=False,
allows_multiple_answers=False
allows_multiple_answers=False,
)
await bot.pin_chat_message(
chat_id, poll_msg.message_id, disable_notification=False
)
# сохраняем сам опрос
save_message(poll_msg.chat.id, poll_msg.message_id)
@@ -48,9 +34,6 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
except Exception as e:
logger.error(f"Ошибка при отправке в чат {chat_id}: {e}")
confirm_msg = await message.answer("✅ Сообщение отправлено.")
save_message(confirm_msg.chat.id, confirm_msg.message_id)
@dp.poll_answer()
async def handle_poll_answer(poll_answer: PollAnswer):
user = poll_answer.user
@@ -60,20 +43,30 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
username = f"@{user.username}" if user.username else user.first_name
# всегда пишем в первый чат из Config.CHAT_IDS
# 6394047531
if option_ids and option_ids[0] == 0:
msg = await bot.send_message(
chat_id=6394047531,
text=f"{username} опоздает"
chat_id=6394047531, text=f"{username} опоздает"
)
save_message(msg.chat.id, msg.message_id)
elif option_ids and option_ids[0] == 1:
msg = await bot.send_message(
chat_id=6394047531, text=f"{username} сильно опоздает"
)
save_message(msg.chat.id, msg.message_id)
elif option_ids and option_ids[0] == 2:
msg = await bot.send_message(
chat_id=6394047531, text=f"{username} возможно опоздает"
)
save_message(msg.chat.id, msg.message_id)
elif not option_ids:
msg = await bot.send_message(
chat_id=6394047531, text=f"{username} Отменил свой голос"
)
save_message(msg.chat.id, msg.message_id)
else:
msg = await bot.send_message(
chat_id=6394047531,
text=f"{username} выбрал вариант {option_ids}"
chat_id=6394047531, text=f"{username} выбрал вариант {option_ids}"
)
save_message(msg.chat.id, msg.message_id)
+2
View File
@@ -1,7 +1,9 @@
def register(dp, state, bot):
from . import handlers
handlers.register_handlers(dp, state, bot)
def unregister(dp):
# Здесь можно удалить хендлеры, если нужно
dp.message_handlers.handlers.clear()
+57 -16
View File
@@ -1,16 +1,22 @@
from config import Config
import aiohttp
import ssl
import certifi
from aiogram.types import BufferedInputFile
from utils.antispam import admin_required
from storage.message_storage import save_message
from utils.antispam import admin_required, saving
from aiogram import Dispatcher, Bot
from aiogram.types import Message
from models.state import BotState
from aiogram.filters import Command
from logging import getLogger
logger = getLogger(__name__)
API_URL = "http://127.0.0.1:7700/speak"
ssl_context = ssl.create_default_context(cafile=certifi.where())
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("vadmin"))
@admin_required(0)
@@ -21,11 +27,26 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return
phrase = parts[1]
# Запрос к TTS API
# URL с параметрами модели и голоса
url = f"{Config.DEEPGRAM_TTS_URL}model=aura-2-andromeda-en"
headers = {
"Authorization": f"Token {Config.DEEPGRAM_API_KEY}",
"Content-Type": "application/json",
}
# В JSON только text
payload = {"text": phrase}
async with aiohttp.ClientSession() as session:
async with session.post(API_URL, json={"text": phrase}) as resp:
async with session.post(
url, headers=headers, json=payload, ssl=ssl_context
) as resp:
if resp.status != 200:
await message.reply("Ошибка генерации аудио")
error_text = await resp.text()
await message.reply(
f"Ошибка генерации аудио: {resp.status} {error_text}"
)
return
audio_bytes = await resp.read()
audio_file = BufferedInputFile(audio_bytes, filename="speech.wav")
@@ -43,8 +64,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@admin_required(0)
async def admin(message: Message):
raw_text = message.text or message.caption
if not raw_text and not (message.photo or message.document or message.audio or message.video):
await message.reply("❌ Укажи текст или прикрепи файл/медиа: /admin <сообщение>")
if not raw_text and not (
message.photo or message.document or message.audio or message.video
):
await message.reply(
"❌ Укажи текст или прикрепи файл/медиа: /admin <сообщение>"
)
return
# Отрезаем саму команду (/admin)
@@ -60,15 +85,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
elif message.document:
# Документ
await bot.send_document(chat_id, message.document.file_id, caption=text_to_send)
await bot.send_document(
chat_id, message.document.file_id, caption=text_to_send
)
elif message.audio:
# Аудио (музыка)
await bot.send_audio(chat_id, message.audio.file_id, caption=text_to_send)
await bot.send_audio(
chat_id, message.audio.file_id, caption=text_to_send
)
elif message.video:
# Видео
await bot.send_video(chat_id, message.video.file_id, caption=text_to_send)
await bot.send_video(
chat_id, message.video.file_id, caption=text_to_send
)
else:
# Только текст
@@ -85,8 +116,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@admin_required(0)
async def id_admin(message: Message):
raw_text = message.text or message.caption
if not raw_text and not (message.photo or message.document or message.audio or message.video):
await message.reply("❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <chat_id> <сообщение>")
if not raw_text and not (
message.photo or message.document or message.audio or message.video
):
await message.reply(
"❌ Укажи ID чата и текст или прикрепи файл/медиа: /iadmin <chat_id> <сообщение>"
)
return
# Отрезаем саму команду (/iadmin)
@@ -111,19 +146,25 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
elif message.document:
# Документ
await bot.send_document(chat_id, message.document.file_id, caption=text_to_send)
await bot.send_document(
chat_id, message.document.file_id, caption=text_to_send
)
elif message.audio:
# Аудио (музыка)
await bot.send_audio(chat_id, message.audio.file_id, caption=text_to_send)
await bot.send_audio(
chat_id, message.audio.file_id, caption=text_to_send
)
elif message.video:
# Видео
await bot.send_video(chat_id, message.video.file_id, caption=text_to_send)
await bot.send_video(
chat_id, message.video.file_id, caption=text_to_send
)
else:
# Только текст
await bot.send_message(chat_id, text_to_send)
await bot.send_message(chat_id, text_to_send, parse_mode="Markdown")
logger.info(f"Сообщение отправлено в чат {chat_id}")
await message.answer("✅ Сообщение отправлено.")