auto-sync: 2026-04-15 01:00:01
This commit is contained in:
@@ -88,3 +88,4 @@
|
||||
{"op":"update","id":"proj_proxy_vm","properties":{"description":"Proxy VM (vpn-srv) — VLESS Reality прокси. Задача #1 (Wi-Fi Homenet_vpn transparent proxy) — ГОТОВО (TPROXY, 12.04.2026). Задача #2 (HA Telegram) — DONE.","status":"active"},"timestamp":"2026-04-12T08:26:00Z"}
|
||||
{"op": "update", "id": "proj_proxy_vm", "properties": {"description": "Proxy VM (vpn-srv) \u2014 VLESS Reality \u043f\u0440\u043e\u043a\u0441\u0438. \u0417\u0430\u0434\u0430\u0447\u0430 #1 (Wi-Fi Homenet_vpn transparent proxy) \u2014 DONE (nat REDIRECT + MSS clamp 1280, 12.04.2026). \u0417\u0430\u0434\u0430\u0447\u0430 #2 (HA Telegram \u0447\u0435\u0440\u0435\u0437 SOCKS5) \u2014 DONE (10.04.2026)."}, "timestamp": "2026-04-12T12:10:52.824669+00:00"}
|
||||
{"op":"create","entity":{"id":"proj_kids_helper","type":"Project","properties":{"title":"Детский помощник — агент для родителей","description":"Персональный помощник по вопросам ребёнка: одежда, обувь, игрушки, обучение, события. Отдельный Telegram-бот, изолированная память. Workspace: workspace-kids. ТЗ: tasks/kids-helper/TZ.md.","status":"todo","priority":"medium","created":"2026-04-12T16:54:00Z","labels":["agent","kids","telegram"],"path":"tasks/kids-helper/"},"created":"2026-04-12T16:54:00Z"}
|
||||
{"op":"create","entity":{"id":"proj_bytik","type":"Project","properties":{"title":"Байтик — детский ИИ-помощник","description":"Telegram-бот Байтик: ИИ-помощник для Егора (8 лет, ДР 17.04.2018). Отвечает на вопросы, утренняя рассылка 7:30 MSK с энциклопедическими фактами, поздравления с праздниками. Строгая детская цензура. LLM: Qwen 3.6-Plus. Инфраструктура: mva154, изолированный workspace.","status":"planned","created":"2026-04-14T21:55:00Z","path":"tasks/bytik/","labels":["agent","telegram","kids","ai"]},"created":"2026-04-14T21:55:00Z"}}
|
||||
|
||||
5
tasks/bytik/.env.example
Normal file
5
tasks/bytik/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
ALLOWED_CHAT_ID=your_group_chat_id
|
||||
OPENROUTER_API_KEY=your_openrouter_api_key
|
||||
OPENROUTER_MODEL=openrouter/qwen/qwen3.6-plus
|
||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||
172
tasks/bytik/TZ.md
Normal file
172
tasks/bytik/TZ.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# ТЗ: Telegram-бот «Байтик» 🤖
|
||||
|
||||
> Детский ИИ-помощник для Егора (8 лет, ДР 17.04.2018)
|
||||
> Проект: `proj_bytik` | Дата создания: 2026-04-14
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Telegram-бот, который:
|
||||
- Отвечает на вопросы ребёнка простым языком
|
||||
- Присылает 1-2 раза в день энциклопедические факты (утро 7:30 MSK)
|
||||
- Поздравляет с праздниками и ДР
|
||||
- Работает **только в одной группе** (whitelist chat_id)
|
||||
- Строгая детская цензура 0+
|
||||
|
||||
---
|
||||
|
||||
## 2. Требования к боту
|
||||
|
||||
### 2.1 Whitelist (критично)
|
||||
- **Разрешённый chat_id:** будет прописан в `.env` (`ALLOWED_CHAT_ID`)
|
||||
- Бот принимает сообщения **только** из этой группы
|
||||
- Сообщения из ЛС и других групп — **полный игнор** (без ответа)
|
||||
- При добавлении в новую группу — автоматически `leave_chat`
|
||||
- Проверка: `message.chat.id` для text, `callback_query.message.chat.id` для inline
|
||||
|
||||
### 2.2 Обработка сообщений
|
||||
- Получает текст от пользователя в разрешённом чате
|
||||
- Отправляет промпт в LLM (OpenRouter API, Qwen 3.6-Plus)
|
||||
- Получает ответ → отправляет в Telegram
|
||||
- Поддерживает диалог (хранит последние 5-10 сообщений как контекст)
|
||||
|
||||
### 2.3 Системный промпт (обязателен)
|
||||
```
|
||||
Ты — Байтик, дружелюбный робот-помощник для мальчика Егора (8 лет).
|
||||
|
||||
Правила:
|
||||
- Объясняй сложные вещи просто, с примерами, которые понятны 8-летнему
|
||||
- Используй эмодзи, чтобы было весело
|
||||
- Если спрашивают на русском — отвечай на русском
|
||||
- Иногда (когда уместно) отвечай на простом английском, по типу "Did you know..." — это помогает учить язык
|
||||
- Если вопрос про что-то тревожное (смерть, война, болезни): мягко уйди от темы, а если настаивает — скажи "Это серьёзный вопрос, лучше спроси у папы!"
|
||||
- НИКОГДА не используй ненормативную лексику, страшные подробности, или взрослые темы
|
||||
- Если не знаешь ответа — честно скажи, но предложи "давай поищем вместе!"
|
||||
- Иногда используй смешные факты и шутки
|
||||
- Будь терпелив — ребёнок может задавать один и тот же вопрос много раз
|
||||
- Используй короткие ответы для детей до 12 лет, не более 200 слов
|
||||
```
|
||||
|
||||
### 2.4 Утренняя рассылка
|
||||
- **Время:** 07:30 MSK (04:30 UTC)
|
||||
- Присылать 1 интересный энциклопедический факт
|
||||
- Темы по интересам Егора: машины, самолёты, корабли (Титаник!), лего, животные, география, математика, IT, Minecraft
|
||||
- **Формат:** текст + эмодзи, иногда картинка
|
||||
- Можно иногда отправлять голосовое сообщение (через ElevenLabs TTS)
|
||||
|
||||
### 2.5 Праздники и ДР
|
||||
- **17 апреля:** поздравление с ДР (особое, весёлое)
|
||||
- **1 января:** Новый Год
|
||||
- **23 февраля:** День защитника Отечества
|
||||
- **8 марта:** Международный женский день
|
||||
- **12 апреля:** День космонавтики (Егор любит, будет в тему!)
|
||||
- **Другие:** можно добавить в конфиг
|
||||
|
||||
### 2.6 Голосовые сообщения (опционально)
|
||||
- Иногда факты/ответы — голосом
|
||||
- ElevenLabs TTS (или Yandex SpeechKit fallback)
|
||||
- Конвертация в OGG Opus (требование Telegram)
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектура
|
||||
|
||||
### 3.1 Технологии
|
||||
- **Язык:** Python 3.11+
|
||||
- **Telegram:** `aiogram` 3.x
|
||||
- **LLM:** OpenRouter API (`openrouter/openrouter/auto` или явно `openrouter/qwen/qwen3.6-plus`)
|
||||
- **TTS (опционально):** ElevenLabs API / Yandex SpeechKit
|
||||
- **Конфиг:** `.env` (python-dotenv)
|
||||
- **Хранение диалога:** JSON-файлы по user_id в `data/chat_history/`
|
||||
|
||||
### 3.2 Структура проекта
|
||||
```
|
||||
bytik/
|
||||
├── bot.py # Точка входа
|
||||
├── config.py # Загрузка конфига из .env
|
||||
├── prompts.py # Системный промпт + факты
|
||||
├── llm.py # Обёртка над OpenRouter API
|
||||
├── tts.py # Голосовые сообщения
|
||||
├── scheduler.py # Планировщик (рассылка + праздники)
|
||||
├── chat_history.py # Хранение контекста диалогов
|
||||
├── facts.json # Энциклопедические факты
|
||||
├── holidays.json # Праздники
|
||||
├── data/
|
||||
│ └── chat_history/ # JSON файлы с историей
|
||||
├── .env # Секреты
|
||||
├── .env.example # Пример
|
||||
└── requirements.txt # Зависимости
|
||||
```
|
||||
|
||||
### 3.3 .env.example
|
||||
```
|
||||
BOT_TOKEN=<telegram_bot_token>
|
||||
ALLOWED_CHAT_ID=<group_chat_id>
|
||||
OPENROUTER_API_KEY=<key>
|
||||
OPENROUTER_MODEL=openrouter/qwen/qwen3.6-plus
|
||||
ELEVENLABS_API_KEY=<key> # optional
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Инфраструктура
|
||||
|
||||
- **Хост:** mva154 (контейнер OpenClaw) или vpn-srv
|
||||
- **Запуск:** как Docker container (предпочтительно) или systemd service
|
||||
- **Бэкапы:** chat_history JSON-файлы
|
||||
- **Логи:** `logs/bytik.log`
|
||||
|
||||
---
|
||||
|
||||
## 5. Безопасность
|
||||
|
||||
### 5.1 Whitelist — ОБЯЗАТЕЛЬНО
|
||||
- Все входящие сообщения проходят фильтр `message.chat.id == ALLOWED_CHAT_ID`
|
||||
- Невалидные сообщения — молчаливый игнор
|
||||
- При `my_chat_member: left` или добавлении в новую группу → `leave_chat()`
|
||||
|
||||
### 5.2 Контент-фильтр
|
||||
- LLM промпт уже содержит ограничения
|
||||
- Валидация ответа: если содержит мат / adult контент → не отправлять, логировать
|
||||
- Можно добавить дополнительный фильтр (список запрещённых слов)
|
||||
|
||||
---
|
||||
|
||||
## 6. Этапы реализации
|
||||
|
||||
### Фаза 1 (MVP)
|
||||
- Бот принимает сообщения из whitelist группы
|
||||
- Отправляет в LLM, получает ответ, шлёт в Telegram
|
||||
- Системный промпт с детской цензурой
|
||||
|
||||
### Фаза 2
|
||||
- Утренняя рассылка (7:30 MSK)
|
||||
- Праздничные поздравления
|
||||
- Контекст диалога (последние N сообщений)
|
||||
|
||||
### Фаза 3
|
||||
- Голосовые сообщения (TTS)
|
||||
- Картинки (генерация или подбор)
|
||||
|
||||
---
|
||||
|
||||
## 7. Критерии приёмки
|
||||
|
||||
- [ ] Бот отвечает только в разрешённой группе
|
||||
- [ ] Игнорирует ЛС и другие группы
|
||||
- [ ] Системный промпт блокирует взрослый контент
|
||||
- [ ] Утренняя рассылка в 07:30 MSK
|
||||
- [ ] Поздравление с ДР Егора (17.04)
|
||||
- [ ] Ответы простые, понятные 8-летнему
|
||||
- [ ] Контекст диалога сохраняется
|
||||
- [ ] Логи пишутся в файл
|
||||
|
||||
---
|
||||
|
||||
## 8. Примечания
|
||||
|
||||
- День рождения Егора — 17.04.2018 (скоро 8 лет!)
|
||||
- Интересы: машины, самолёты, корабли, лего, животные, география, математика, IT, Minecraft
|
||||
- LLM: **Qwen 3.6-Plus** (OpenRouter)
|
||||
- Дедлайн: фаза 1 готова к 16.04.2026 (чтобы ДР поздравить!)
|
||||
121
tasks/bytik/bot.py
Normal file
121
tasks/bytik/bot.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from aiogram import Bot, Dispatcher, types
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import BotCommand
|
||||
|
||||
from config import BOT_TOKEN, ALLOWED_CHAT_ID
|
||||
from prompts import SYSTEM_PROMPT
|
||||
from llm import ask_llm
|
||||
from chat_history import load_history, add_message
|
||||
from scheduler import BytikScheduler
|
||||
|
||||
LOG_DIR = "logs"
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
RotatingFileHandler(
|
||||
os.path.join(LOG_DIR, "bytik.log"),
|
||||
maxBytes=5*1024*1024,
|
||||
backupCount=3,
|
||||
encoding="utf-8",
|
||||
),
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
dp = Dispatcher()
|
||||
scheduler = None
|
||||
|
||||
def is_allowed_chat(chat_id: int) -> bool:
|
||||
return chat_id == ALLOWED_CHAT_ID
|
||||
|
||||
@dp.message(Command("start"))
|
||||
async def cmd_start(message: types.Message):
|
||||
if not is_allowed_chat(message.chat.id):
|
||||
logger.warning(f"Попытка /start из неразрешённого чата: {message.chat.id}")
|
||||
return
|
||||
await message.answer("Привет, Егор! 👋 Я Байтик, твой робот-помощник! Спрашивай меня о чём угодно! 🤖")
|
||||
|
||||
@dp.message(Command("clear"))
|
||||
async def cmd_clear(message: types.Message):
|
||||
if not is_allowed_chat(message.chat.id):
|
||||
return
|
||||
from chat_history import clear_history
|
||||
clear_history(message.from_user.id)
|
||||
await message.answer("История наших разговоров очищена! 🧹")
|
||||
|
||||
@dp.message()
|
||||
async def handle_message(message: types.Message):
|
||||
if not message.text:
|
||||
return
|
||||
|
||||
if not is_allowed_chat(message.chat.id):
|
||||
logger.debug(f"Игнорирую сообщение из чата {message.chat.id}")
|
||||
return
|
||||
|
||||
user_message = message.text.strip()
|
||||
logger.info(f"Сообщение от user {message.from_user.id}: {user_message[:100]}")
|
||||
|
||||
typing_msg = await message.answer("Думаю... 🤔")
|
||||
|
||||
history = load_history(message.from_user.id)
|
||||
messages_for_llm = [{"role": "system", "content": SYSTEM_PROMPT}] + history
|
||||
messages_for_llm.append({"role": "user", "content": user_message})
|
||||
|
||||
add_message(message.from_user.id, "user", user_message)
|
||||
|
||||
answer = await ask_llm(messages_for_llm)
|
||||
|
||||
add_message(message.from_user.id, "assistant", answer)
|
||||
|
||||
try:
|
||||
await typing_msg.edit_text(answer)
|
||||
except Exception:
|
||||
await message.answer(answer)
|
||||
|
||||
@dp.my_chat_member()
|
||||
async def handle_bot_status_change(update: types.ChatMemberUpdated):
|
||||
chat_id = update.chat.id
|
||||
new_status = update.new_chat_member.status
|
||||
|
||||
if new_status == "member" and chat_id != ALLOWED_CHAT_ID:
|
||||
logger.warning(f"Бот добавлен в неразрешённый чат {chat_id}. Покидаю.")
|
||||
await bot.leave_chat(chat_id)
|
||||
elif new_status == "left":
|
||||
logger.info(f"Бот удалён из чата {chat_id}")
|
||||
|
||||
async def set_commands():
|
||||
commands = [
|
||||
BotCommand(command="start", description="Начать общение"),
|
||||
BotCommand(command="clear", description="Очистить историю диалога"),
|
||||
]
|
||||
await bot.set_my_commands(commands)
|
||||
|
||||
async def main():
|
||||
global scheduler
|
||||
await set_commands()
|
||||
|
||||
scheduler = BytikScheduler(bot)
|
||||
scheduler.start()
|
||||
|
||||
logger.info(f"Бот запущен. Whitelist chat_id: {ALLOWED_CHAT_ID}")
|
||||
await dp.start_polling(bot)
|
||||
|
||||
async def on_shutdown():
|
||||
if scheduler:
|
||||
scheduler.shutdown()
|
||||
await bot.session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
logger.info("Бот остановлен")
|
||||
45
tasks/bytik/chat_history.py
Normal file
45
tasks/bytik/chat_history.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HISTORY_DIR = "data/chat_history"
|
||||
MAX_MESSAGES = 10
|
||||
|
||||
os.makedirs(HISTORY_DIR, exist_ok=True)
|
||||
|
||||
def _get_file_path(user_id: int) -> str:
|
||||
return os.path.join(HISTORY_DIR, f"{user_id}.json")
|
||||
|
||||
def load_history(user_id: int) -> List[Dict[str, str]]:
|
||||
path = _get_file_path(user_id)
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return []
|
||||
|
||||
def save_history(user_id: int, history: List[Dict[str, str]]):
|
||||
path = _get_file_path(user_id)
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(history, f, ensure_ascii=False, indent=2)
|
||||
except IOError as e:
|
||||
logger.error(f"Не удалось сохранить историю для user {user_id}: {e}")
|
||||
|
||||
def add_message(user_id: int, role: str, content: str):
|
||||
history = load_history(user_id)
|
||||
history.append({"role": role, "content": content})
|
||||
if len(history) > MAX_MESSAGES:
|
||||
history = history[-MAX_MESSAGES:]
|
||||
save_history(user_id, history)
|
||||
|
||||
def clear_history(user_id: int):
|
||||
path = _get_file_path(user_id)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
logger.info(f"История очищена для user {user_id}")
|
||||
15
tasks/bytik/config.py
Normal file
15
tasks/bytik/config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
ALLOWED_CHAT_ID = int(os.getenv("ALLOWED_CHAT_ID", "0"))
|
||||
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "openrouter/qwen/qwen3.6-plus")
|
||||
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
|
||||
|
||||
if not BOT_TOKEN:
|
||||
raise ValueError("BOT_TOKEN не установлен в .env")
|
||||
if not OPENROUTER_API_KEY:
|
||||
raise ValueError("OPENROUTER_API_KEY не установлен в .env")
|
||||
54
tasks/bytik/llm.py
Normal file
54
tasks/bytik/llm.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import aiohttp
|
||||
import logging
|
||||
from config import OPENROUTER_API_KEY, OPENROUTER_MODEL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FORBIDDEN_WORDS = [
|
||||
"мат", "блять", "сука", "хуй", "пизда", "ебать",
|
||||
"fuck", "shit", "bitch", "asshole",
|
||||
]
|
||||
|
||||
def is_safe_text(text: str) -> bool:
|
||||
text_lower = text.lower()
|
||||
for word in FORBIDDEN_WORDS:
|
||||
if word in text_lower:
|
||||
logger.warning(f"Обнаружено запрещённое слово: {word}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def ask_llm(messages: list) -> str:
|
||||
url = "https://openrouter.ai/api/v1/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/bytik-bot",
|
||||
"X-Title": "Bytik Bot",
|
||||
}
|
||||
payload = {
|
||||
"model": OPENROUTER_MODEL,
|
||||
"messages": messages,
|
||||
"max_tokens": 500,
|
||||
"temperature": 0.7,
|
||||
}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, json=payload, headers=headers) as resp:
|
||||
if resp.status != 200:
|
||||
error_text = await resp.text()
|
||||
logger.error(f"OpenRouter error: {resp.status} - {error_text}")
|
||||
return "Ой, я что-то запутался. Попробуй спросить ещё раз! 🤔"
|
||||
|
||||
data = await resp.json()
|
||||
answer = data["choices"][0]["message"]["content"].strip()
|
||||
|
||||
if not is_safe_text(answer):
|
||||
logger.warning("Ответ от LLM содержит запрещённые слова. Блокируем.")
|
||||
return "Извини, я не могу это обсудить. Давай поговорим о чём-то другом! 😊"
|
||||
|
||||
return answer
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при запросе к LLM: {e}")
|
||||
return "Извини, я пока не могу ответить. Попробуй позже! 🤖"
|
||||
41
tasks/bytik/prompts.py
Normal file
41
tasks/bytik/prompts.py
Normal file
@@ -0,0 +1,41 @@
|
||||
SYSTEM_PROMPT = """Ты — Байтик, дружелюбный робот-помощник для мальчика Егора (8 лет).
|
||||
|
||||
Правила:
|
||||
- Объясняй сложные вещи просто, с примерами, которые понятны 8-летнему
|
||||
- Используй эмодзи, чтобы было весело 😊
|
||||
- Если спрашивают на русском — отвечай на русском
|
||||
- Иногда (когда уместно) отвечай на простом английском, по типу "Did you know..." — это помогает учить язык
|
||||
- Если вопрос про что-то тревожное (смерть, война, болезни): мягко уйди от темы, а если настаивает — скажи "Это серьёзный вопрос, лучше спроси у папы!"
|
||||
- НИКОГДА не используй ненормативную лексику, страшные подробности, или взрослые темы
|
||||
- Если не знаешь ответа — честно скажи, но предложи "давай поищем вместе!"
|
||||
- Иногда используй смешные факты и шутки
|
||||
- Будь терпелив — ребёнок может задавать один и тот же вопрос много раз
|
||||
- Используй короткие ответы для детей до 12 лет, не более 200 слов
|
||||
"""
|
||||
|
||||
FACTS = [
|
||||
"🚗 Знаешь ли ты, что самый быстрый автомобиль в мире развивает скорость более 490 км/ч? Это быстрее, чем летит самолёт при посадке!",
|
||||
"✈️ Самая большая птица в мире — альбатрос. Размах его крыльев может быть больше 3 метров!",
|
||||
"🚢 Титаник был самым большим кораблём своего времени. Его длина была как 4 футбольных поля!",
|
||||
"🧱 Из обычного набора LEGO можно собрать более 915 миллионов разных комбинаций!",
|
||||
"🦁 Лев рычит так громко, что его слышно за 8 километров. Это как услышать крик друга с другого конца города!",
|
||||
"🌍 Самая большая страна в мире — Россия. Чтобы пересечь её на поезде, нужно ехать целую неделю!",
|
||||
"🔢 Математика: если сложить все числа от 1 до 100, получится 5050. Попробуй проверить!",
|
||||
"💻 Первый компьютер был размером с большую комнату. А теперь помещается в карман!",
|
||||
"⛏️ В Minecraft один блок = 1 кубический метр. Значит, если ты строишь дом 10x10, он размером с настоящую комнату!",
|
||||
"🚀 12 апреля 1961 года Юрий Гагарин стал первым человеком в космосе. Его полёт длился всего 108 минут, но это изменило весь мир!",
|
||||
"🐘 Слон — единственное животное, которое не умеет прыгать. Но он может бегать со скоростью 40 км/ч!",
|
||||
"🌊 В океане больше историй, чем во всех книгах мира. Мы исследовали только 5% океана!",
|
||||
"🦕 Динозавры жили на Земле 165 миллионов лет. А люди — всего 300 тысяч лет!",
|
||||
"🎮 Самая первая видеоигра называлась Pong. В ней был только мячик и две ракетки!",
|
||||
"🌙 На Луне нет ветра, поэтому следы астронавтов останутся там на миллионы лет!",
|
||||
]
|
||||
|
||||
HOLIDAYS = {
|
||||
"01-01": "🎄 С Новым Годом, Егор! Пусть этот год будет полон приключений, новых открытий и крутых подарков! 🎁",
|
||||
"02-23": "🎖️ С Днём защитника Отечества! Ты будущий защитник, как папа! 💪",
|
||||
"03-08": "🌸 С 8 Марта! Поздравь маму и всех девочек вокруг! Они самые лучшие! 💐",
|
||||
"04-12": "🚀 С Днём космонавтики! Сегодня день, когда человек впервые полетел в космос! Ты тоже когда-нибудь полетишь? ⭐",
|
||||
"04-17": "🎉🎂 С ДНЁМ РОЖДЕНИЯ, ЕГОР! Тебе сегодня 8 лет! Расти умным, смелым и счастливым! Пусть сбудутся все мечты! 🎈🎁🚀",
|
||||
"05-09": "🎖️ С Днём Победы! Это день памяти и благодарности тем, кто защитил нашу страну.",
|
||||
}
|
||||
4
tasks/bytik/requirements.txt
Normal file
4
tasks/bytik/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
aiogram>=3.0.0
|
||||
python-dotenv>=1.0.0
|
||||
aiohttp>=3.8.0
|
||||
APScheduler>=3.10.0
|
||||
53
tasks/bytik/scheduler.py
Normal file
53
tasks/bytik/scheduler.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from prompts import FACTS, HOLIDAYS
|
||||
from config import ALLOWED_CHAT_ID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BytikScheduler:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.scheduler = AsyncIOScheduler(timezone="Europe/Moscow")
|
||||
|
||||
def start(self):
|
||||
self.scheduler.add_job(
|
||||
self.send_morning_fact,
|
||||
"cron",
|
||||
hour=7,
|
||||
minute=30,
|
||||
timezone="Europe/Moscow",
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
self.check_holidays,
|
||||
"cron",
|
||||
hour=8,
|
||||
minute=0,
|
||||
timezone="Europe/Moscow",
|
||||
)
|
||||
self.scheduler.start()
|
||||
logger.info("Планировщик запущен")
|
||||
|
||||
def shutdown(self):
|
||||
self.scheduler.shutdown()
|
||||
logger.info("Планировщик остановлен")
|
||||
|
||||
async def send_morning_fact(self):
|
||||
import random
|
||||
fact = random.choice(FACTS)
|
||||
try:
|
||||
await self.bot.send_message(chat_id=ALLOWED_CHAT_ID, text=fact)
|
||||
logger.info(f"Утренняя рассылка отправлена: {fact[:50]}...")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка утренней рассылки: {e}")
|
||||
|
||||
async def check_holidays(self):
|
||||
today = datetime.now().strftime("%m-%d")
|
||||
if today in HOLIDAYS:
|
||||
message = HOLIDAYS[today]
|
||||
try:
|
||||
await self.bot.send_message(chat_id=ALLOWED_CHAT_ID, text=message)
|
||||
logger.info(f"Праздничное поздравление отправлено: {today}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки праздничного сообщения: {e}")
|
||||
80
tasks/bytik/tts.py
Normal file
80
tasks/bytik/tts.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import logging
|
||||
from config import ELEVENLABS_API_KEY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def text_to_speech(text: str) -> bytes | None:
|
||||
if not ELEVENLABS_API_KEY:
|
||||
logger.debug("ElevenLabs API key не установлен, TTS пропущен")
|
||||
return None
|
||||
|
||||
logger.info(f"Генерация TTS: {text[:50]}...")
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
headers = {
|
||||
"xi-api-key": ELEVENLABS_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"text": text,
|
||||
"model_id": "eleven_monolingual_v1",
|
||||
"voice_settings": {
|
||||
"stability": 0.5,
|
||||
"similarity_boost": 0.75,
|
||||
}
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
"https://api.elevenlabs.io/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM/stream",
|
||||
json=payload,
|
||||
headers=headers,
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
logger.error(f"ElevenLabs error: {resp.status}")
|
||||
return None
|
||||
mp3_data = await resp.read()
|
||||
|
||||
mp3_path = None
|
||||
ogg_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as mp3_tmp:
|
||||
mp3_tmp.write(mp3_data)
|
||||
mp3_path = mp3_tmp.name
|
||||
|
||||
ogg_fd, ogg_path = tempfile.mkstemp(suffix=".ogg")
|
||||
os.close(ogg_fd)
|
||||
|
||||
ffmpeg_cmd = [
|
||||
"ffmpeg", "-y",
|
||||
"-i", mp3_path,
|
||||
"-acodec", "libopus",
|
||||
"-b:a", "48k",
|
||||
"-vbr", "on",
|
||||
ogg_path,
|
||||
]
|
||||
subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
with open(ogg_path, "rb") as f:
|
||||
ogg_data = f.read()
|
||||
|
||||
return ogg_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при конвертации TTS: {e}")
|
||||
return None
|
||||
finally:
|
||||
for path in [mp3_path, ogg_path]:
|
||||
if path and os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
except ImportError:
|
||||
logger.warning("aiohttp не установлен, TTS недоступен")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка TTS: {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user