Привет, у меня есть два кода. Прочитай эти коды и выполни мои задачи ниже:
Код 1 - bot.py:

import vk_api
from vk_api.longpoll import VkLongPoll, VkEventType
from vk_api.keyboard import VkKeyboard, VkKeyboardColor
import random, datetime, io, requests
import logging
import os
import json
from license_generator import ImageProcessor
import threading
import queue

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

CONFIG_FILE = "config.json"
if os.path.exists(CONFIG_FILE):
    with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
        CONFIG = json.load(f)
else:
    CONFIG = {
        "tokens": {
            "group": 'vk1.a.Uep3llMX1BdJSKxGIwgDXKPbAFhw4lXKOzDEmVPKv8OCUEOmQKCV1hrgUL0tDewTY1swKy3L9Ax6Un_rPGPN6iDx6A_reIpBEWSawAhkOa2bB7rvdWGY0tl7-3Os_iFJXOON8dDC_lGBh-BMB3-SuK0HddesVuG8kd1NwtZqeRIkWv0LwWMcdZ2CVhNct_DZJDQAxBF3MP1H-7arVkNHZA',
            "user": 'vk1.a.DNUOWMd4rap0WxmHYBV18UsAIoH32Ode3zOmSSZbcAmb4tvnSYqWfZTERf0Zgh4GrrxAm11-4KxCLKLL2NtBzjJpxNx8hjzJ3fvnrTQTP5oGjx7caRJzS2JLWyCREY6G3CdsFoo1ptw0wzUSCbp2VAFC6VIS-bp21q9Akfs1p291a-eQfngorMIwyRpntC6dF8UnhaI1-cbycYq45UW_2A'
        },
        "group_id": 228820799,
        "admin_id": 850580089,
        "donate_group_id": 228820799,
        "albums_urls": {
            "добрая мита": 308112183,
            "безумная мита": 308112187,
            "сонная мита": 308112198,
            "кепочка": 308112261,
            "мила": 308112193
        },
        "qr_default_url": "https://vk.com/club228820799",
        "font_config": {
            "main_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 48},
            "big_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 64},
            "id_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 36},
            "small_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 32},
            "arkadia_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 70}
        },
        "text_config": {
            "issued_by": {"text": "ВЫДАНО: @MEMESABOUTNEKO", "x": 540, "y": 250, "font": "small_font", "fill": (0, 0, 0),
                          "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "issued_to": {"text": "КОМУ: {user_name}", "x": 540, "y": 300, "font": "main_font", "fill": (0, 0, 0),
                          "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "date_start": {"text": "ДАТА ВЫДАЧИ: {date_start}", "x": 540, "y": 380, "font": "small_font",
                           "fill": (0, 0, 0), "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "date_end": {"text": "ДЕЙСТВУЕТ ДО: {date_end}", "x": 540, "y": 430, "font": "small_font", "fill": (0, 0, 0),
                         "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "license_id": {"text": "ID: {license_id}", "x": 540, "y": 500, "font": "id_font", "fill": (0, 0, 0),
                           "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "license_number": {"text": "НОМЕР: D-1", "x": 540, "y": 550, "font": "id_font", "fill": (0, 0, 0),
                              "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "license_name": {"text": "{license_text}", "x": 1300, "y": 600, "font": "arkadia_font", "fill": (0, 0, 0),
                             "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0}
        },
        "gradient_config": {"x": 490, "y": 159, "width": 2080, "height": 2500, "radius": 300, "layer": 3},
        "background_config": {"x": 2570, "y": 159, "size": (2040, 2160), "radius": 300, "target_width": 1950,
                              "target_height": 2500, "horizontal_percentage": 0.7, "layer": 2,
                              "mirror_offset_x": -2040, "mirror_offset_y": 0},
        "rectangle_config": {
            "white_rect": {"size": (4029, 2500), "x": 490, "y": 159, "color": (255, 255, 255, 255),
                           "stroke_width": 0, "stroke_color": (0, 0, 0, 255), "radius": 300, "layer": 1}},
        "light_background_image": "светлый фон.png",
        "color_analysis_scale_size": (100, 100),
        "min_color_distance": 20,
        "max_color_distance": 200,
        "gradient_center_radius": 0.3,
        "cache_max_size": 128
    }

class VkBot:
    def __init__(self, config, image_processor, num_threads=5):
        self.config = config
        self.image_processor = image_processor
        self.vk_session = vk_api.VkApi(token=self.config["tokens"]["group"])
        self.vk = self.vk_session.get_api()
        self.vk_session_user = vk_api.VkApi(token=self.config["tokens"]["user"])
        self.vk_user = self.vk_session_user.get_api()
        self.longpoll = VkLongPoll(self.vk_session)
        self.task_queue = queue.Queue()
        self.num_threads = num_threads
        self.threads = []
        self.start_worker_threads()

    def create_keyboard(self, buttons):
        keyboard = VkKeyboard(one_time=False)
        for button_text, color in buttons:
            keyboard.add_button(button_text, color=color)
        return keyboard.get_keyboard()

    def start_worker_threads(self):
        for _ in range(self.num_threads):
            thread = threading.Thread(target=self.worker)
            thread.daemon = True
            thread.start()
            self.threads.append(thread)
        logging.info(f"Запущено {self.num_threads} потоков для обработки задач.")

    def worker(self):
        while True:
            user_id, selected_character = self.task_queue.get()
            if user_id is None:
                self.task_queue.task_done()
                break
            self.process_license(user_id, selected_character)
            self.task_queue.task_done()

    def get_random_photo(self, album_id):
        fixed_owner_id = 378938263
        try:
            logging.info(f"Запрос фото. Owner ID: {fixed_owner_id}, Album ID: {album_id}")
            photos = self.vk_user.photos.get(owner_id=fixed_owner_id, album_id=album_id, count=100)['items']
            if not photos:
                logging.warning(f"В альбоме {album_id} нет фотографий или доступ к альбому закрыт.")
                return None
            logging.info("Фото успешно получены.")
            return random.choice(photos)['sizes'][-1]['url']
        except vk_api.exceptions.ApiError as e:
            logging.error(f"Ошибка при получении фото: {e}")
            if "Access denied" in str(e):
                logging.error("Доступ к альбому закрыт. Проверьте настройки конфиденциальности альбома.")
                return None
        except Exception as e:
            logging.error(f"Неизвестная ошибка при получении фото: {e}")
            return None

    def check_is_don(self, user_id):
        try:
            return self.vk.groups.isMember(group_id=self.config["donate_group_id"], user_id=user_id)
        except Exception as e:
            logging.error(f"Ошибка проверки дона: {e}")
            return False

    def send_vk_message(self, user_id, message, keyboard=None):
        self.vk.messages.send(peer_id=user_id, random_id=random.randint(0, 2 ** 20), message=message,
                              keyboard=keyboard)

    def send_image_vk(self, image, user_id):
        max_retries = 3  # Максимальное количество попыток
        retry_delay = 2  # Задержка между попытками в секундах

        for attempt in range(max_retries):
            try:
                image_bytes = io.BytesIO()
                image.save(image_bytes, format="PNG")
                
                upload_url = self.vk.photos.getMessagesUploadServer(peer_id=user_id)["upload_url"]
                upload_response = requests.post(upload_url, files={'photo': ('image.png', image_bytes.getvalue())}, timeout=10).json()
               
                if "photo" not in upload_response:
                    logging.error(f"Ошибка загрузки изображения (попытка {attempt + 1}): {upload_response}")
                    if attempt == max_retries - 1:
                        self.send_vk_message(user_id, "Не удалось отправить изображение")
                        return
                    continue

                photo = self.vk.photos.saveMessagesPhoto(photo=upload_response['photo'], server=upload_response['server'],
                                                        hash=upload_response['hash'])[0]
                self.vk.messages.send(peer_id=user_id, random_id=random.randint(0, 2 ** 20),
                                      attachment=f"photo{photo['owner_id']}_{photo['id']}")
                logging.info(f"Изображение отправлено (после {attempt+1} попыток): {user_id}.")
                return

            except vk_api.exceptions.ApiError as e:
                logging.error(f"Ошибка отправки сообщения VK (попытка {attempt + 1}): {e}")
                if attempt == max_retries - 1:
                    self.send_vk_message(user_id, "Не удалось отправить изображение")
                    return
                time.sleep(retry_delay)
            except requests.exceptions.SSLError as e:
                    logging.error(f"Ошибка SSL (попытка {attempt + 1}): {e}")
                    if attempt == max_retries - 1:
                      self.send_vk_message(user_id, "Не удалось отправить изображение, ошибка SSL")
                      return
                    time.sleep(retry_delay)
            except requests.exceptions.ConnectionError as e:
                logging.error(f"Ошибка соединения (попытка {attempt + 1}): {e}")
                if attempt == max_retries - 1:
                     self.send_vk_message(user_id, "Не удалось отправить изображение, ошибка соединения")
                     return
                time.sleep(retry_delay)
            except Exception as e:
                logging.error(f"Неизвестная ошибка отправки изображения (попытка {attempt + 1}): {e}")
                if attempt == max_retries -1:
                  self.send_vk_message(user_id, "Не удалось отправить изображение, ошибка")
                  return
                time.sleep(retry_delay)

    def process_license(self, user_id, selected_character):
        try:
            is_don = self.check_is_don(user_id)
            album_id = self.config["albums_urls"].get(selected_character.lower())
            if not album_id:
                self.send_vk_message(user_id, f"Альбом {selected_character} не найден")
                return
            photo_url = self.get_random_photo(album_id)
            if not photo_url:
                self.send_vk_message(user_id, "Не удалось получить фото")
                return
            background = self.image_processor.download_image(photo_url)
            if not background:
                self.send_vk_message(user_id, "Не удалось загрузить фон")
                return
            user_info = self.vk.users.get(user_ids=user_id)[0]
            user_name = f"{user_info.get('first_name', 'Noname')} {user_info.get('last_name', '')}"
            today = datetime.date.today()
            date_start = today.strftime("%d.%m.%Y")
            date_end = "Бессрочно" if is_don else (today + datetime.timedelta(days=30)).strftime("%d.%m.%Y")
            license_id = f"{'D' if is_don else 'F'}-{random.randint(10000000, 99999999)}"
            user_data = {'user_name': user_name, 'date_start': date_start, 'date_end': date_end,
                         'license_id': license_id, 'license_text': f"ЛИЦЕНЗИЯ НА {selected_character.upper()}"}
            qr_code = self.image_processor.generate_qr_code(
                f"https://vk.com/id{user_id}") if is_don else self.image_processor.generate_qr_code(
                self.config["qr_default_url"])
            license_image = self.image_processor.create_license_image(background, user_data, qr_code)
            if license_image:
                self.send_image_vk(license_image, user_id)
            else:
                self.send_vk_message(user_id, "Ошибка создания лицензии")
        except Exception as e:
            self.send_vk_message(user_id, f"Ошибка: {e}")
            logging.error(f"Ошибка обработки: {e}")

    def shutdown(self):
        logging.info("Завершение работы ботом...")
        for _ in range(self.num_threads):
            self.task_queue.put((None, None))
        for thread in self.threads:
            thread.join()
        logging.info("Все потоки завершены.")

    def run(self):
        logging.info("Бот запущен. Ожидание сообщений...")
        try:
            for event in self.longpoll.listen():
                if event.type == VkEventType.MESSAGE_NEW and event.to_me:
                    user_id, user_text = event.user_id, event.text.lower()
                    logging.info(f"Новое сообщение от {user_id}: {user_text}")
                    if user_text == "начать":
                        self.send_vk_message(user_id, "Хотите создать лицензию?",
                                              keyboard=self.create_keyboard(
                                                  [("Создать лицензию", VkKeyboardColor.PRIMARY)]))
                    elif user_text == "создать лицензию":
                        self.send_vk_message(user_id, "Какой тип лицензии?",
                                              keyboard=self.create_keyboard(
                                                  [("Добрая Мита", VkKeyboardColor.PRIMARY),
                                                   ("Безумная Мита", VkKeyboardColor.PRIMARY),
                                                   ("Сонная Мита", VkKeyboardColor.PRIMARY),
                                                   ("Кепочка", VkKeyboardColor.PRIMARY),
                                                   ("Мила", VkKeyboardColor.PRIMARY)]))
                    elif user_text == "добрая мита":
                         self.task_queue.put((user_id, "добрая мита"))
                    elif user_text == "безумная мита":
                        self.task_queue.put((user_id, "безумная мита"))
                    elif user_text == "сонная мита":
                        self.task_queue.put((user_id, "сонная мита"))
                    elif user_text == "кепочка":
                        self.task_queue.put((user_id, "кепочка"))
                    elif user_text == "мила":
                        self.task_queue.put((user_id, "мила"))
                    else:
                        self.send_vk_message(user_id, "Неизвестная команда")
        except Exception as e:
            logging.error(f"Произошла ошибка в основном цикле: {e}")
        finally:
            self.shutdown()


if __name__ == "__main__":
    image_processor = ImageProcessor(CONFIG)
    num_threads = 8
    bot = VkBot(CONFIG, image_processor, num_threads=num_threads)
    bot.run()

Код 2 - license_generator:

import time
import vk_api
from vk_api.longpoll import VkLongPoll, VkEventType
from vk_api.keyboard import VkKeyboard, VkKeyboardColor
import random, datetime, io, requests
import logging
import os
import json
from typing import Tuple, List, Dict, Any, Optional, TypeVar, Generic, Callable
from collections import OrderedDict
import math
import qrcode
from PIL import Image, ImageDraw, ImageFont
from functools import lru_cache

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

CONFIG_FILE = "config.json"
if os.path.exists(CONFIG_FILE):
    with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
        CONFIG = json.load(f)
else:
    CONFIG = {
        "tokens": {
            "group": 'vk1.a.Uep3llMX1BdJSKxGIwgDXKPbAFhw4lXKOzDEmVPKv8OCUEOmQKCV1hrgUL0tDewTY1swKy3L9Ax6Un_rPGPN6iDx6A_reIpBEWSawAhkOa2bB7rvdWGY0tl7-3Os_iFJXOON8dDC_lGBh-BMB3-SuK0HddesVuG8kd1NwtZqeRIkWv0LwWMcdZ2CVhNct_DZJDQAxBF3MP1H-7arVkNHZA',
            "user": 'vk1.a.DNUOWMd4rap0WxmHYBV18UsAIoH32Ode3zOmSSZbcAmb4tvnSYqWfZTERf0Zgh4GrrxAm11-4KxCLKLL2NtBzjJpxNx8hjzJ3fvnrTQTP5oGjx7caRJzS2JLWyCREY6G3CdsFoo1ptw0wzUSCbp2VAFC6VIS-bp21q9Akfs1p291a-eQfngorMIwyRpntC6dF8UnhaI1-cbycYq45UW_2A'
        },
        "group_id": 228820799,
        "admin_id": 850580089,
        "donate_group_id": 228820799,
        "albums_urls": {
            "добрая мита": 308112183,
            "безумная мита": 308112187,
            "сонная мита": 308112198,
            "кепочка": 308112261,
            "мила": 308112193
        },
        "qr_default_url": "https://vk.com/club228820799",
        "font_config": {
            "main_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 48},
            "big_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 64},
            "id_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 36},
            "small_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 32},
            "arkadia_font": {"file": "Шрифт - Obelix Pro.ttf", "size": 70}
        },
        "text_config": {
            "issued_by": {"text": "ВЫДАНО: @MEMESABOUTNEKO", "x": 540, "y": 250, "font": "small_font", "fill": (0, 0, 0),
                          "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "issued_to": {"text": "КОМУ: {user_name}", "x": 540, "y": 300, "font": "main_font", "fill": (0, 0, 0),
                          "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "date_start": {"text": "ДАТА ВЫДАЧИ: {date_start}", "x": 540, "y": 380, "font": "small_font",
                           "fill": (0, 0, 0), "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "date_end": {"text": "ДЕЙСТВУЕТ ДО: {date_end}", "x": 540, "y": 430, "font": "small_font", "fill": (0, 0, 0),
                         "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "license_id": {"text": "ID: {license_id}", "x": 540, "y": 500, "font": "id_font", "fill": (0, 0, 0),
                           "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "license_number": {"text": "НОМЕР: D-1", "x": 540, "y": 550, "font": "id_font", "fill": (0, 0, 0),
                              "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0},
            "license_name": {"text": "{license_text}", "x": 1300, "y": 600, "font": "arkadia_font", "fill": (0, 0, 0),
                             "stroke_width": 0, "stroke_color": (0, 0, 0), "stroke_opacity": 1.0}
        },
        "gradient_config": {"x": 490, "y": 159, "width": 2080, "height": 2500, "radius": 300, "layer": 3},
        "background_config": {"x": 2570, "y": 159, "size": (2040, 2160), "radius": 300, "target_width": 1950,
                              "target_height": 2500, "horizontal_percentage": 0.7, "layer": 2,
                              "mirror_offset_x": -2040, "mirror_offset_y": 0},
        "rectangle_config": {
            "white_rect": {"size": (4029, 2500), "x": 490, "y": 159, "color": (255, 255, 255, 255),
                           "stroke_width": 0, "stroke_color": (0, 0, 0, 255), "radius": 300, "layer": 1}},
        "light_background_image": "светлый фон.png",
        "color_analysis_scale_size": (100, 100),
        "min_color_distance": 20,
        "max_color_distance": 200,
        "gradient_center_radius": 0.3,
        "cache_max_size": 128
    }
T = TypeVar('T')
class LRUCache(Generic[T]):
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache: OrderedDict[Any, T] = OrderedDict()

    def get(self, key: Any) -> Optional[T]:
        if key in self.cache:
            self.cache.move_to_end(key)
            return self.cache[key]
        return None

    def put(self, key: Any, value: T) -> None:
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)

    def __contains__(self, key: Any) -> bool:
        return key in self.cache

class ImageProcessor:
    def __init__(self, config: Dict[str, Any]):
        self.config = config
        self.mask_cache: LRUCache[Image.Image] = LRUCache(self.config["cache_max_size"])
        self.gradient_cache: LRUCache[Image.Image] = LRUCache(self.config["cache_max_size"])
        self.resized_cache: LRUCache[Image.Image] = LRUCache(self.config["cache_max_size"])
        self.image_cache: LRUCache[Image.Image] = LRUCache(self.config["cache_max_size"])
        self.fonts: Dict[str, ImageFont.FreeTypeFont] = {
            name: ImageFont.truetype(data["file"], size=data["size"]) if os.path.exists(data["file"]) else ImageFont.load_default()
            for name, data in self.config["font_config"].items()}

    def get_font(self, font_name: str) -> ImageFont.FreeTypeFont:
        return self.fonts[font_name]

    def download_image(self, image_url: str) -> Optional[Image.Image]:
        if image_url in self.image_cache:
            return self.image_cache.get(image_url)
        try:
            response = requests.get(image_url, stream=True)
            response.raise_for_status()
            with Image.open(io.BytesIO(response.content)) as image:
                image = image.convert('RGBA')
                self.image_cache.put(image_url, image)
                logging.info(f"Изображение {image_url} успешно загружено и добавлено в кеш. Размер: {image.size}")
                return image
        except requests.exceptions.RequestException as e:
            logging.error(f"Ошибка при скачивании изображения: {e}")
            return None
        except Exception as e:
            logging.error(f"Ошибка открытия изображения: {e}")
            return None

    def generate_qr_code(self, url: str) -> Image.Image:
        qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=2)
        qr.add_data(url)
        qr.make(fit=True)
        return qr.make_image(fill_color="black", back_color="white")

    def create_rounded_rectangle(self, size: Tuple[int, int], radius: int,
                                 color: Tuple[int, int, int, int]) -> Image.Image:
        rectangle = Image.new('RGBA', size, (0, 0, 0, 0))
        ImageDraw.Draw(rectangle).rounded_rectangle((0, 0, size[0], size[1]), radius=radius, fill=color)
        return rectangle

    def generate_random_corner_points(self, width: int, height: int) -> Tuple[Tuple[int, int], Tuple[int, int]]:
        corners = [(0, 0), (width, 0), (0, height), (width, height)]
        p1 = random.choice(corners)
        corners.remove(p1)
        return p1, random.choice(corners)

    def create_gradient_background(self, size: Tuple[int, int], start_color: Tuple[int, int, int, int],
                                   end_color: Tuple[int, int, int, int], point1: Tuple[int, int],
                                   point2: Tuple[int, int]) -> Image.Image:
        cache_key = hash((size, start_color, end_color, point1, point2))
        cached_gradient = self.gradient_cache.get(cache_key)
        if cached_gradient:
            return cached_gradient
        gradient = Image.new('RGBA', size, start_color)
        draw = ImageDraw.Draw(gradient)
        x1, y1 = point1
        x2, y2 = point2
        dx, dy = x2 - x1, y2 - y1
        length = math.sqrt(dx ** 2 + dy ** 2)
        if length == 0:
            self.gradient_cache.put(cache_key, gradient)
            return gradient
        lerp = lambda t: tuple(int(start_color[i] + (end_color[i] - start_color[i]) * t) for i in range(4))
        for y in range(size[1]):
            for x in range(size[0]):
                px, py = x - x1, y - y1
                t = max(0, min(1, (px * dx + py * dy) / length ** 2))
                draw.point((x, y), fill=lerp(t))
        self.gradient_cache.put(cache_key, gradient)
        return gradient

    def create_cut_gradient_mask(self, width: int, height: int, corner_radius: int) -> Image.Image:
        cache_key = hash((width, height, corner_radius))
        cached_mask = self.mask_cache.get(cache_key)
        if cached_mask:
            return cached_mask
        mask = Image.new('L', (width, height), 0)
        ImageDraw.Draw(mask).rounded_rectangle((0, 0, width, height), radius=corner_radius, fill=255)
        self.mask_cache.put(cache_key, mask)
        return mask

    def crop_and_center_image(self, image: Image.Image, target_width: int, target_height: int) -> Image.Image:
        cache_key = hash((image.tobytes(), target_width, target_height))
        cached_image = self.resized_cache.get(cache_key)
        if cached_image:
            return cached_image
        iw, ih = image.size
        scale = max(target_width / iw, target_height / ih)
        nw, nh = int(iw * scale), int(ih * scale)
        resized_image = image.resize((nw, nh), Image.Resampling.LANCZOS)
        w, h = resized_image.size
        left = (w - target_width) // 2 if w > target_width else 0
        top = (h - target_height) // 2 if h > target_height else 0
        right = left + target_width if w > target_width else w
        bottom = top + target_height if h > target_height else h
        cropped_image = resized_image.crop((left, top, right, bottom))
        self.resized_cache.put(cache_key, cropped_image)
        logging.info(
            f"Изображение обрезано до {target_width}x{target_height} с сохранением пропорций. Оригинал: {iw}x{ih}")
        return cropped_image

    def create_rounded_mask(self, width: int, height: int, radius: int) -> Image.Image:
        cache_key = hash((width, height, radius))
        cached_mask = self.mask_cache.get(cache_key)
        if cached_mask:
            return cached_mask
        mask = Image.new('L', (width, height), 0)
        ImageDraw.Draw(mask).rounded_rectangle((0, 0, width, height), radius=radius, fill=255)
        self.mask_cache.put(cache_key, mask)
        return mask

    def draw_text_with_stroke(self, draw: ImageDraw.ImageDraw, x: int, y: int, text: str,
                              font: ImageFont.FreeTypeFont, fill: Tuple[int, int, int], stroke_width: int,
                              stroke_color: Tuple[int, int, int], stroke_opacity: float) -> None:
        if stroke_width > 0:
            color = (*stroke_color[:3], int(stroke_color[3] * stroke_opacity))
            stroke_text = ""
            for i in range(-stroke_width, stroke_width + 1):
                for j in range(-stroke_width, stroke_width + 1):
                    if math.sqrt(i * i + j * j) <= stroke_width:
                        stroke_text += f"({x + i},{y + j}) {text}\n"
            draw.multiline_text((0, 0), stroke_text, font=font, fill=color)
        draw.text((x, y), text, font=font, fill=fill)

    def draw_rectangle_with_stroke_and_shadow(self, draw: ImageDraw.ImageDraw, x: int, y: int, width: int, height: int,
                                              radius: int, color: Tuple[int, int, int, int], stroke_width: int,
                                              stroke_color: Tuple[int, int, int, int]) -> None:
        if stroke_width > 0:
            draw.rounded_rectangle((x, y, x + width, y + height), radius=radius, fill=color, outline=stroke_color,
                                  width=stroke_width)
        else:
            draw.rounded_rectangle((x, y, x + width, y + height), radius=radius, fill=color)

    def create_rounded_mask_for_qr(self, size: Tuple[int, int], radius: int) -> Image.Image:
        mask = Image.new('L', size, 0)
        ImageDraw.Draw(mask).rounded_rectangle((0, 0, *size), radius=radius, fill=255)
        return mask

    def create_partial_rounded_mask(self, width: int, height: int, radius: int, percentage: float) -> Image.Image:
        mask = Image.new('L', (width, height), 0)
        draw = ImageDraw.Draw(mask)
        round_width = int(width * percentage)
        draw.rectangle((0, 0, round_width, height), fill=255)
        draw.rounded_rectangle((0, 0, round_width, height), radius=radius, fill=255)
        return mask

    def create_image_with_partial_copy(self, image: Image.Image, percentage: float) -> Image.Image:
        width, height = image.size
        copy_width = int(width * percentage)
        partial_copy = Image.new("RGBA", (width, height), (0, 0, 0, 0))
        partial_copy.paste(image.crop((0, 0, copy_width, height)), (0, 0))
        return partial_copy

    def create_composite_image(self, original_image: Image.Image,
                               partial_copy: Optional[Image.Image] = None) -> Image.Image:
        if partial_copy is None:
            return original_image
        composite_image = Image.new("RGBA", original_image.size, (0, 0, 0, 0))
        composite_image.paste(original_image, (0, 0))
        composite_image.paste(partial_copy, (0, 0), partial_copy)
        return composite_image

    def mirror_image(self, image: Image.Image) -> Image.Image:
        return image.transpose(Image.FLIP_LEFT_RIGHT)

    def _calculate_centroid(self, points: List[Tuple[int, int, int]]) -> Tuple[int, int, int]:
        if not points:
            return (0, 0, 0)
        num_points = len(points)
        sum_r = sum(p[0] for p in points)
        sum_g = sum(p[1] for p in points)
        sum_b = sum(p[2] for p in points)
        return (sum_r // num_points, sum_g // num_points, sum_b // num_points)

    def _calculate_distance(self, color1: Tuple[int, int, int], color2: Tuple[int, int, int]) -> float:
        return math.sqrt(sum((color1[i] - color2[i]) ** 2 for i in range(3)))

    def _kmeans_plusplus_init(self, points: List[Tuple[int, int, int]], k: int) -> List[Tuple[int, int, int]]:
        centroids = [random.choice(points)]
        for _ in range(1, k):
            distances = [min([self._calculate_distance(p, c) for c in centroids]) for p in points]
            next_centroid = points[distances.index(max(distances))]
            centroids.append(next_centroid)
        return centroids

    def kmeans(self, points: List[Tuple[int, int, int]], k: int,
              max_iterations: int = 100) -> Tuple[List[Tuple[int, int, int]], Optional[str]]:
        if not points:
           return [], "No points provided"
        if k <= 0:
            return [], "K must be bigger than 0"
        if k > len(points):
            k = len(points)
        centroids = self._kmeans_plusplus_init(points, k)
        for _ in range(max_iterations):
            clusters = [[] for _ in range(k)]
            for point in points:
                distances = [self._calculate_distance(point, centroid) for centroid in centroids]
                closest_centroid = distances.index(min(distances))
                clusters[closest_centroid].append(point)
            new_centroids = []
            for cluster in clusters:
                if cluster:
                    new_centroids.append(self._calculate_centroid(cluster))
                else:
                    new_centroids.append(random.choice(points))
            if all(c1 == c2 for c1, c2 in zip(centroids, new_centroids)):
                break
            centroids = new_centroids
        return centroids, None

    def analyze_image_colors(self, image: Image.Image) -> List[Tuple[int, int, int, int]]:
        scale_size = self.config["color_analysis_scale_size"]
        img = image.resize(scale_size)
        img_array = []
        width, height = img.size
        center_x, center_y = width // 2, height // 2
        radius = min(width, height) * self.config["gradient_center_radius"]
        for x in range(width):
            for y in range(height):
                distance = math.sqrt((x - center_x) ** 2 + (y - center_y) ** 2)
                if distance <= radius:
                    color = img.getpixel((x, y))
                    if color[3] > 0:
                        img_array.append(color[:3])
        if not img_array:
            return []
        k = min(10, len(img_array))
        centroids, error = self.kmeans(img_array, k)
        if error:
            logging.error(f"Error running Kmeans: {error}")
            return []
        return [tuple(c) + (255,) for c in centroids]

    def color_distance(self, color1: Tuple[int, int, int, ], color2: Tuple[int, int, int]) -> float:
        return math.sqrt(sum((color1[i] - color2[i]) ** 2 for i in range(3)))

    def find_close_color(self, main_color: Tuple[int, int, int, ],
                         colors: List[Tuple[int, int, int, ]], min_distance: int,
                         max_distance: int) -> Optional[Tuple[int, int, int, int]]:
        for color in colors:
            if color == main_color:
                continue
            dist = self.color_distance(main_color, color)
            if min_distance <= dist <= max_distance:
                return color
        return None

    def choose_gradient_colors(self, colors: List[Tuple[int, int, int, int]]) -> Tuple[
        Tuple[int, int, int, int], Tuple[int, int, int, int]]:
      if not colors:
          return (255, 255, 255, 255), (0, 0, 0, 255)
      if len(colors) == 1:
        return colors[0], colors[0]

      main_color = colors[0]
      best_match = None
      max_distance = 0
      for color in colors[1:]:
          dist = self.color_distance(main_color, color)
          if dist > max_distance:
              max_distance = dist
              best_match = color
      if best_match:
          return main_color, best_match
      return main_color, colors[1]

    def _apply_background_element(self, main_background: Image.Image, element: Dict[str, Any], final_background: Image.Image, callback: Callable[[float], None]) -> None:
        if element["type"] == "background":
            tw, th = element["config"]["target_width"], element["config"]["target_height"]
            resized = self.crop_and_center_image(final_background, tw, th)
            if resized:
                mask = self.create_rounded_mask(tw, th, element["config"]["radius"])
                h_percent = element["config"].get("horizontal_percentage", 1.0)
                partial_mask = self.create_partial_rounded_mask(tw, th, element["config"]["radius"], h_percent)
                mask.paste(partial_mask, (0, 0), partial_mask)
                resized.putalpha(mask)
                mirrored = self.mirror_image(resized)
                mx, my = element["config"].get("mirror_offset_x", 0), element["config"].get("mirror_offset_y", 0)
                main_background.paste(mirrored,
                                    (element["config"]["x"] - mirrored.width + element["config"]["size"][0] + mx,
                                    element["config"]["y"] + my), mirrored)
                main_background.paste(resized, (element["config"]["x"], element["config"]["y"]), resized)
            callback(0.3)
        elif element["type"] == "rectangle":
            draw = ImageDraw.Draw(main_background)
            x, y = element["config"]["x"], element["config"]["y"]
            w, h = element["config"]["size"][0], element["config"]["size"][1]
            r, c = element["config"]["radius"], element["config"]["color"]
            sw, sc = element["config"]["stroke_width"], element["config"]["stroke_color"]
            self.draw_rectangle_with_stroke_and_shadow(draw, x, y, w, h, r, c, sw, sc)
            callback(0.2)

    def _apply_text_elements(self, main_background: Image.Image, user_data: Dict[str, Any], callback: Callable[[float], None]) -> None:
      draw = ImageDraw.Draw(main_background)
      for index, config in enumerate(self.config["text_config"].values()):
          self.draw_text_with_stroke(draw, config["x"], config["y"], config["text"].format(**user_data),
                                      self.get_font(config["font"]), config["fill"], config["stroke_width"],
                                      config["stroke_color"], config["stroke_opacity"])
          callback(0.5 + index / len(self.config["text_config"]) * 0.2)

    def _apply_qr_code(self, main_background: Image.Image, qr_code: Optional[Image.Image], callback: Callable[[float], None]) -> None:
         if qr_code:
            qr_resized = qr_code.resize((200, 200))
            qr_mask = self.create_rounded_mask_for_qr(qr_resized.size, 20)
            qr_resized = qr_resized.convert('RGBA')
            qr_resized.putalpha(qr_mask)
            main_background.paste(qr_resized, (540, 600), qr_resized)
         callback(0.8)
    
    def _apply_light_background(self, main_background: Image.Image, callback: Callable[[float], None]) -> None:
      try:
          light_background = Image.open(self.config["light_background_image"]).convert("RGBA").resize((5000, 2813))
          main_background.paste(light_background, (0, 0), light_background)
      except Exception as e:
          logging.error(f"Ошибка загрузки фона: {e}")
      callback(0.9)

    def create_license_image_with_progress(self, background: Image.Image, user_data: Dict[str, Any],
                                     qr_code: Optional[Image.Image], callback: Callable[[float], None]) -> Optional[Image.Image]:
        start_time = time.time()
        if not background:
            logging.error("Фон не загружен")
            return None
        iw, ih = 5000, 2813
        main_background = Image.new("RGBA", (iw, ih), (230, 230, 230, 255))
        callback(0.05)
        start_color, end_color = self.choose_gradient_colors(self.analyze_image_colors(background))
        gw, gh = self.config["gradient_config"]["width"], self.config["gradient_config"]["height"]
        p1, p2 = self.generate_random_corner_points(gw, gh)
        gradient_mask = self.create_cut_gradient_mask(gw, gh, self.config["gradient_config"]["radius"])
        gradient_background = self.create_gradient_background((gw, gh), start_color, end_color, p1,
                                                              p2)
        gradient_background.putalpha(gradient_mask)
        callback(0.1)
        final_background = self.create_composite_image(background)
        elements = sorted([*({"type": "rectangle", "config": c} for _, c in self.config["rectangle_config"].items()),
                           {"type": "background", "image": final_background,
                            "config": self.config["background_config"]}], key=lambda x: x["config"].get("layer", 0))
        for element in elements:
           self._apply_background_element(main_background, element, final_background, callback)

        main_background.paste(gradient_background,
                              (self.config["gradient_config"]["x"], self.config["gradient_config"]["y"]),
                              gradient_background)
        callback(0.6)
        self._apply_text_elements(main_background, user_data, callback)
        self._apply_qr_code(main_background, qr_code, callback)
        self._apply_light_background(main_background, callback)
        callback(1.0)
        end_time = time.time()
        duration = end_time - start_time
        logging.info(f"Лицензия сгенерирована за {duration:.2f} секунд.")
        return main_background

    def create_license_image(self, background: Image.Image, user_data: Dict[str, Any],
                             qr_code: Optional[Image.Image]) -> Optional[Image.Image]:
        def empty_callback(progress):
            pass

        return self.create_license_image_with_progress(background, user_data, qr_code, empty_callback)
        iw, ih = 5000, 2813
        main_background = Image.new("RGBA", (iw, ih), (230, 230, 230, 255))
        start_color, end_color = self.choose_gradient_colors(self.analyze_image_colors(background))
        gw, gh = self.config["gradient_config"]["width"], self.config["gradient_config"]["height"]
        p1, p2 = self.generate_random_corner_points(gw, gh)
        gradient_mask = self.create_cut_gradient_mask(gw, gh, self.config["gradient_config"]["radius"])
        gradient_background = self.create_gradient_background((gw, gh), start_color, end_color, p1,
                                                              p2)
        gradient_background.putalpha(gradient_mask)
        final_background = self.create_composite_image(background)
        h_percent = self.config["background_config"].get("horizontal_percentage", 1.0)
        elements = sorted([*({"type": "rectangle", "config": c} for _, c in self.config["rectangle_config"].items()),
                           {"type": "background", "image": final_background,
                            "config": self.config["background_config"]}], key=lambda x: x["config"].get("layer", 0))
        for element in elements:
            if element["type"] == "background":
                tw, th = element["config"]["target_width"], element["config"]["target_height"]
                resized = self.crop_and_center_image(element["image"], tw, th)
                if resized:
                    mask = self.create_rounded_mask(tw, th, element["config"]["radius"])
                    partial_mask = self.create_partial_rounded_mask(tw, th, element["config"]["radius"], h_percent)
                    mask.paste(partial_mask, (0, 0), partial_mask)
                    resized.putalpha(mask)
                    mirrored = self.mirror_image(resized)
                    mx, my = element["config"].get("mirror_offset_x", 0), element["config"].get("mirror_offset_y", 0)
                    main_background.paste(mirrored,
                                          (element["config"]["x"] - mirrored.width + element["config"]["size"][0] + mx,
                                           element["config"]["y"] + my), mirrored)
                    main_background.paste(resized, (element["config"]["x"], element["config"]["y"]), resized)
            elif element["type"] == "rectangle":
                draw = ImageDraw.Draw(main_background)
                x, y = element["config"]["x"], element["config"]["y"]
                w, h = element["config"]["size"][0], element["config"]["size"][1]
                r, c = element["config"]["radius"], element["config"]["color"]
                sw, sc = element["config"]["stroke_width"], element["config"]["stroke_color"]
                self.draw_rectangle_with_stroke_and_shadow(draw, x, y, w, h, r, c, sw, sc)
        main_background.paste(gradient_background,
                              (self.config["gradient_config"]["x"], self.config["gradient_config"]["y"]),
                              gradient_background)
        draw = ImageDraw.Draw(main_background)
        for config in self.config["text_config"].values():
            self.draw_text_with_stroke(draw, config["x"], config["y"], config["text"].format(**user_data),
                                      self.get_font(config["font"]), config["fill"], config["stroke_width"],
                                      config["stroke_color"], config["stroke_opacity"])
        if qr_code:
            qr_resized = qr_code.resize((200, 200))
            qr_mask = self.create_rounded_mask_for_qr(qr_resized.size, 20)
            qr_resized = qr_resized.convert('RGBA')
            qr_resized.putalpha(qr_mask)
            main_background.paste(qr_resized, (540, 600), qr_resized)
        try:
            light_background = Image.open(self.config["light_background_image"]).convert("RGBA").resize((iw, ih))
            main_background.paste(light_background, (0, 0), light_background)
        except Exception as e:
            logging.error(f"Ошибка загрузки фона: {e}")
        return main_background


if __name__ == "__main__":
    # Здесь можно добавить код для тестирования ImageProcessor
    print(f"Image Processor создан!")

Задачи для выполнения:
1 - Оптимизировать оба кода и выслать их в полном виде поочерёдно для дальнейшего их тестирования с моей стороны. Оптимизация включает в себя удаление лишних комментариев лишних и в принципе ускорение работы.
2 - Я хочу, чтобы ты разбил задачи для обработки изображения на части и распределил всю нагрузку на мой процессор Snapdragon 8 + gen 1, у которого 8 ядер. Можно использовать все потоки, но задача должна быть сделана как можно быстрее в случае с лицензиями. Я хочу, чтобы пользователь видел процент выполнения лицензии. Также лучше, как мне кажется, не высылать новое сообщение с процентом, а редактировать старое! Если получится, конечно. И после того как бот вышлет готовую лицензию я хочу, чтобы в сообщении была кнопка "выслать файлом". Так, если лицензию вк очень сильно шакалит, то высылание лицензии файлом сильно поможет с качеством фотографии.
3 - По поводу качества. Я хочу, чтобы ты сделал градиент более качественным. По текущей его обработке видно, как будто бы в нём не хватает цветов. Также прошу заметить, что именно эта часть с градиентом и обработкой изображения занимает больше всего времени. Оптимизируй.

Перечитай все мои пожелания по боту и проверь их реализацию перед тем как мне вышлешь готовые коды.