Сегодня мы развлечемся и напишем бота в Telegram, который будет делать мемы и стикеры из изображений, используя библиотеку Pillow.

Наш бот будет накладывать текст на фото или трансформировать фото в подобие стикера. Да, функционал самый простой, если нужно что-то более продвинутое, можно воспользоваться фотошопом или нейронками. Или разобраться глубже в том, как написать дополнительную функциональность.
Лично я использую бота, создающего стикеры и мемы, когда надо отправить что-то в чате как реакцию на происходящее с юмором, не тратя время на поиск подходящего мема.
В завершении статьи, мы развернем проект в облаке Amvera, используя git push, буквально за четыре команды в IDE.
Ссылка на демо работающего бота.
Создание бота, создающего стикеры и мемы
Для создания бота нам нужно перейти в BotFather и получить токен для API. Он нам потребуется позже.
Написание бота с использованием библиотеки aiogram 3
Перед тем как приступить к написанию бота, мы установим необходимые библиотеки для работы бота.
Переходим в командную строку (cmd) и прописываем команду ниже.
pip install aiogram pillow
Приступим к написанию кода.
-
Создаём и открываем файл main.py.
-
Импортируем библиотеки.
import logging
import asyncio
import random
import os
from aiogram import Bot, Dispatcher, Router, types
from aiogram.filters import CommandStart, StateFilter
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
from stickers import create_sticker
from mem import create_meme
Экземпляр бота выглядит следующим образом.
logging.basicConfig(level=logging.INFO)
bot = Bot(token=os.environ["TOKEN"]) # токен, который будет в качестве переменной на хостинге
dp = Dispatcher() # переменная диспетчера
router = Router() # переменная роутера
async def main():
dp.include_router(router) # инициализация роутера
await dp.start_polling(bot) # запуск бота
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO) # инициализация логгера
try:
asyncio.run(main()) # запускаем функцию main в асинхронном режиме
except KeyboardInterrupt:
print('Бот выключен')
Дополнительно создадим файлы mem.py, stickers.py а также шрифт для мемов, который можно будет скачать в исходнике бота на Github в конце статьи.
Мы сделали основу бота, теперь создадим команды
-
Переходим в файл mem.py и вставляем код ниже.
import io
from PIL import Image, ImageDraw, ImageFont
from aiogram.types import BufferedInputFile
async def create_meme(photo_bytes: bytes, text: str) -> BufferedInputFile:
try:
image = Image.open(io.BytesIO(photo_bytes)).convert("RGB") # открываем файл изображения в байтах
except Exception as e:
print(f"Ошибка открытия изображения: {e}")
return None
draw = ImageDraw.Draw(image)
# размер шрифта
font_size = int(image.width / 10)
try:
font = ImageFont.truetype("fonts/font.ttf", size=font_size) # выбираем шрифт из папки fonts, присваиваем размер шрифта
except IOError: # если шрифт не был найден
print("Шрифт не найден. Используем шрифт по умолчанию.")
font = ImageFont.load_default() # используем стандартный шрифт
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# проверка, чтобы текст не выходил за пределы изображения
if text_width > image.width:
font_size = int(font_size * (image.width / text_width))
font = ImageFont.truetype("fonts/font.ttf", size=font_size)
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
x = (image.width - text_width) // 2
y = image.height - text_height - 20
# проверка, чтобы текст не выходил за нижнюю границу изображения
if y < 0:
y = 10 # отступ сверху, если текст слишком большой
draw.text((x, y), text, fill="white", font=font) # "пишем" текст на картинке в белом цвете
meme_bytes = io.BytesIO()
image.save(meme_bytes, format='JPEG') # сохарняем фото
meme_bytes.seek(0)
# конвертируем фото в байты
meme_bytes_data = meme_bytes.getvalue()
return BufferedInputFile(meme_bytes_data, filename='meme.jpg')
Переходим в файл stickers.py и вставляем код ниже.
from aiogram import types
from aiogram.types import BufferedInputFile
from PIL import Image
import io
async def create_sticker(photo: types.PhotoSize, bot) -> BufferedInputFile:
photo_file = await bot.download(photo.file_id) # скачиваем фото
image = Image.open(photo_file) # открываем фото
image = image.resize((512, 512)) # размер стикера Telegram
sticker_bytes = io.BytesIO()
image.save(sticker_bytes, format='PNG') # сохраняем фото
sticker_bytes.seek(0)
sticker = BufferedInputFile(sticker_bytes.getvalue(), filename='sticker.png') # создаем стикер
return sticker
Напишем состояния FSM
from stickers import create_sticker
from mem import create_meme
class Form(StatesGroup): # Состояния
waiting_memphoto = State()
waiting_stickphoto = State()
waiting_text_for_mem = State()
Напишем переменные
choicestart = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="
Мем из фотографии", callback_data="mem")],
[InlineKeyboardButton(text="
Стикер из фотографии", callback_data="sticker")],
]) # Кнопки
cancel = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="
Назад", callback_data="cancel")]
])
errors_in_text = {
"
Я не умею добавить текст в виде ФАЙЛА
",
"
Ты умеешь превращать файл в текст? Я тоже нет.",
"
Как бы так сказать... Ты отправил файл вместо текста.",
"
Срочно организуй мне текст",
"
Пожалуйста, введите текст. Если он будет смешным, я обещаю улыбнуться",
"
Напишите текст, который заставит меня забыть о том, что я бот"
} # Рандомное сообщение об ошибке (по приколу)
Напишем команду старт
@router.message(StateFilter(None), CommandStart()) # команда /start
async def send_welcome(message: Message):
await message.answer("
Данный бот может сделать из мем или стикер из фотографииnnВыберите наиболее подходящий вариант для Вас.", reply_markup=choicestart) # отправка сообщения пользователю и появление кнопок с выбором
Напишем обработчик колбеков, для работы с клавиатурой
@router.callback_query() # обработка колбеков
async def process_callback(callback_query: CallbackQuery, state: FSMContext):
data = callback_query.data
# Если пользователь нажал на кнопки:
if data == "mem":
await state.set_state(Form.waiting_text_for_mem) # присваивание состояния пользователю
await callback_query.bot.edit_message_text(
text="
Вы выбрали вариант: <b>Мем из фотографии</b>nВведите текст, который желаете поставить.",
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id,
parse_mode="HTML",
reply_markup=cancel
) # редактирование сообщения
if data == "sticker":
await state.set_state(Form.waiting_stickphoto)
await callback_query.bot.edit_message_text(
text="
Вы выбрали вариант: <b>Стикер из фотографии</b>nСкиньте фотографию для создания",
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id,
parse_mode="HTML",
reply_markup=cancel
)
if data == "cancel":
await state.clear() # очистка состояния, если пользователь нажал на кнопку отмены
await callback_query.bot.edit_message_text(
text="
Данный бот может сделает из мем или стикер из фотографииnnВыберите наиболее подходящий вариант для Вас.",
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id,
parse_mode=None,
reply_markup=choicestart
)
Напишем сами обработчики для создания мемов и стикеров.
@router.message(Form.waiting_text_for_mem) # обработчик сообщения, если у пользователя есть состояние
async def wait_for_mem_text(message: Message, state: FSMContext):
if message.content_type in [types.ContentType.PHOTO, types.ContentType.VIDEO, types.ContentType.DOCUMENT, ...]: # если в сообщении присутствуют любые файлы
errors_list = list(errors_in_text)
await message.answer(random.choice(errors_list)) # выбор рандомного сообщения об ошибке
else:
await state.update_data(text=message.text) # сохраняем наш текст в состояние
await state.set_state(Form.waiting_memphoto) # присваиваем другое состояние
await message.answer("<b>Мем из фотографии</b>nnХорошо, текст есть. Отправьте мне фото, из которого хотите сделать мем.", parse_mode="HTML") # отправка сообщения
@router.message(Form.waiting_memphoto)
async def wait_for_mem_photo(message: Message, state: FSMContext):
if message.content_type == types.ContentType.PHOTO: # если в сообщении ТОЛЬКО фото
photo = message.photo[-1] # получаем фото
file_id = photo.file_id # получаем уникальный айди фото
file_info = await bot.get_file(file_id) # получаем файл
photo_bytes_io = await bot.download_file(file_info.file_path) # сохраняем файл
# конвертируем фото в байты
photo_bytes = photo_bytes_io.getvalue()
data = await state.get_data() # получаем все сохраненные данные из состояний
text = data.get("text") # получаем сохраненный текст
meme = await create_meme(photo_bytes, text) # создаем мем
if meme: # если мем создан
await bot.send_photo(message.chat.id, meme) # отправляем фото
await state.clear() # очищаем состояние
await message.answer("
Мем успешно создан", reply_markup=cancel) # сообщение об успешном создании мема, добавление кнопки "назад"
else:
await message.answer("
Ошибка создания мема. Попробуйте еще раз.") # сообщение об ошибке
else:
await message.answer("
Упс...nnВы отправили мне <b>некорректный</b> тип файла! Пожалуйста, отправьте фотографию.", parse_mode="HTML") # сообщение об ошибке, если пользователь отправил не фотографию
@router.message(Form.waiting_stickphoto)
async def wait_for_stick_photo(message: Message, state: FSMContext):
if message.content_type == types.ContentType.PHOTO:
photo = message.photo[-1]
sticker = await create_sticker(photo, bot) # создаем стикер
await bot.send_sticker(message.chat.id, sticker) # отправляем стикер
await state.clear()
await message.answer("
Стикер успешно создан", reply_markup=cancel)
else:
await message.answer("
Упс...nnВы отправили мне фотографию <b>некорректно</b>!", parse_mode="HTML")
-
Создадим файл requirements.txt и запишем все необходимые зависимости для последующего деплоя.
В нашем случае requirements.txt выглядит вот так.
aiogram==3.17.0
pillow==10.4.0
Подготовка к деплою бота Amvera
Развернем бота на удаленном сервере двумя альтернативными способами. Через загрузку файлов в интерфейсе и через команду git push amvera master в IDE.
Для этого нам понадобятся файлы amvera.yml и requirements.txt.
Для создания amvera.yml:
-
Воспользуйтесь генератором конфигурационных файлов Amvera.
-
Выберите в качестве окружения Python.
-
Укажите версию Python 3.10.
-
В поле "Введите путь до файла requirements.txt" укажите requirements.txt.
-
В поле "Введите путь до файла .py" укажите main.py.
-
Нажмите кнопку "Generate YAML".
-
Сохраните файл amvera.yml в корне репозитория бота.
Полученный yаml файл:
meta:
environment: python
toolchain:
name: pip
version: 3.10
build:
requirementsPath: requirements.txt
run:
scriptName: main.py
persistenceMount: /data
containerPort: 80
Деплой через интерфейс осуществляется в несколько простых шагов:
-
Нажмите кнопку "Создать проект".
-
Выберите тип проекта "Приложение".
-
Введите название вашего проекта.
-
Выберите подходящий тариф.
-
Нажмите кнопку "Далее".
-
Загрузите файлы бота в репозиторий приложения
-
Нажимаем кнопку “Далее” и завершить.


Далее нам нужно создать «секрет». Переходим в наше приложение и нажимаем «Переменные», создать секрет. В секрет мы пропишем токен нашего бота.

После того как мы создали секрет c токеном бота, нажимаем перезапустить (пересборка для применения токена не требуется).
Приложение будет пересобрано и готово к работе.
Для того чтобы не загружать обновления каждый раз через сайт, выполним деплой через Git
Развертывание через git push обеспечивает значительное удобство, позволяя обновлять проект всего несколькими командами в терминале, без необходимости использования веб-интерфейса облачной платформы.
Деплой через git push:

Установим связь с репозиторием. Скопируем команду, расположенную ниже, и вставьте в вашу командную строку (терминал).

-
Локально: cd "путь к папке", git init.
-
Amvera: Создать проект, выбрать метод "Через Git" (на самом деле вы всегда можете подключить Git позже).
-
Терминал: Вставить команду подключения репозитория.
-
Коммит: git add ., git commit -m "initial commit".
-
Деплой: git push amvera master.
Проверьте лог сборки/приложения при ошибках.
Теперь мы можем создавать простейшие стикеры и мемы в собственноручно написанном инструменте!
Ссылка на демо работающего бота.
Автор: ovchinnikovproger