Проанализируй мой код и исправь ошибку в конце, которую ты увидишь в логах. Сейчас вышлю всё. Логи: 2025-01-08 14:41:12,335 - INFO - Загруженные команды: {'commands': {'начать': {'response': 'Что вы хотите сделать?', 'keyboard': 'main_menu'}, 'лицензия': {'response': 'Выберите персонажа:', 'keyboard': 'character_menu', 'level': 'character_menu'}, 'назад': {'action': 'back'}, 'меню': {'action': 'main_menu'}, 'создать случайную лицензию': {'action': 'random_license'}, 'Безумная Мита': {'action': 'select_character', 'albums_keyboard': 'mad_mita_albums'}, 'Добрая Мита': {'action': 'select_character', 'albums_keyboard': 'good_mita_albums'}, 'Кепочка': {'action': 'select_character', 'albums_keyboard': 'cap_albums'}, 'Мила': {'action': 'select_character', 'albums_keyboard': 'mila_albums'}, 'Сонная Мита': {'action': 'select_character', 'albums_keyboard': 'sleepy_mita_albums'}, 'Коротковолосая Мита': {'action': 'select_character', 'albums_keyboard': 'short_hair_mita_albums'}, '2D Мита': {'action': 'select_character', 'albums_keyboard': '2d_mita_albums'}, 'Призрачная Мита': {'action': 'select_character', 'albums_keyboard': 'ghost_mita_albums'}, 'Чиби Мита': {'action': 'select_character', 'albums_keyboard': 'chibi_mita_albums'}, 'Мита-ядро': {'action': 'select_character', 'albums_keyboard': 'core_mita_albums'}, 'default': {'response': 'Неизвестная команда. Пожалуйста, попробуйте снова.', 'keyboard': 'main_menu'}}, 'characters': {'Безумная Мита': {'albums_keyboard': 'mad_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Добрая Мита': {'albums_keyboard': 'good_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Кепочка': {'albums_keyboard': 'cap_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Мила': {'albums_keyboard': 'mila_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Сонная Мита': {'albums_keyboard': 'sleepy_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Коротковолосая Мита': {'albums_keyboard': 'short_hair_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, '2D Мита': {'albums_keyboard': '2d_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Призрачная Мита': {'albums_keyboard': 'ghost_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Чиби Мита': {'albums_keyboard': 'chibi_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}, 'Мита-ядро': {'albums_keyboard': 'core_mita_albums', 'albums_names': ['Фанатские Арты', 'Творчество Подписчиков', 'Скришоты из MiSide', 'Косплей']}}, 'keyboards': {'main_menu': [{'text': 'Лицензия', 'color': 'primary', 'layer': 1}], 'character_menu': [{'text': 'Безумная Мита', 'color': 'primary', 'layer': 1}, {'text': 'Добрая Мита', 'color': 'primary', 'layer': 1}, {'text': 'Кепочка', 'color': 'primary', 'layer': 1}, {'text': 'Мила', 'color': 'primary', 'layer': 2}, {'text': 'Сонная Мита', 'color': 'primary', 'layer': 2}, {'text': 'Коротковолосая Мита', 'color': 'primary', 'layer': 2}, {'text': '2D Мита', 'color': 'primary', 'layer': 3}, {'text': 'Призрачная Мита', 'color': 'primary', 'layer': 3}, {'text': 'Чиби Мита', 'color': 'primary', 'layer': 3}, {'text': 'Мита-ядро', 'color': 'primary', 'layer': 3}, {'text': 'Создать случайную лицензию', 'color': 'secondary', 'layer': 4}, {'text': 'Назад', 'color': 'negative', 'layer': 5}, {'text': 'Меню', 'color': 'negative', 'layer': 5}], 'mad_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'безумная мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'безумная мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'безумная мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'безумная мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'good_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'добрая мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'добрая мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'добрая мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'добрая мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'cap_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'кепочка_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'кепочка_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'кепочка_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'кепочка_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'mila_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'мила_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'мила_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'мила_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'мила_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'sleepy_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'сонная мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'сонная мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'сонная мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'сонная мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'short_hair_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'коротковолосая мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'коротковолосая мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'коротковолосая мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'коротковолосая мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], '2d_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': '2d мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': '2d мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': '2d мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': '2d мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'ghost_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'призрачная мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'призрачная мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'призрачная мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'призрачная мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'chibi_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'чиби мита_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'чиби мита_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'чиби мита_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'чиби мита_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'core_mita_albums': [{'text': 'Фанатские Арты', 'color': 'primary', 'layer': 1, 'album_key': 'мита-ядро_1'}, {'text': 'Творчество Подписчиков', 'color': 'primary', 'layer': 1, 'album_key': 'мита-ядро_2'}, {'text': 'Скришоты из MiSide', 'color': 'primary', 'layer': 2, 'album_key': 'мита-ядро_3'}, {'text': 'Косплей', 'color': 'primary', 'layer': 2, 'album_key': 'мита-ядро_4'}, {'text': 'Назад', 'color': 'negative', 'layer': 3}, {'text': 'Меню', 'color': 'negative', 'layer': 3}], 'license_creation_menu': [{'text': 'Создать еще из этого альбома', 'color': 'positive', 'layer': 1, 'action': 'create_same'}, {'text': 'Создать еще из другого альбома', 'color': 'primary', 'layer': 1, 'action': 'create_another'}, {'text': 'Назад', 'color': 'negative', 'layer': 2}, {'text': 'Меню', 'color': 'negative', 'layer': 2}]}}
2025-01-08 14:41:12,735 - INFO - Бот запущен. Ожидание сообщений...
2025-01-08 14:41:12,735 - INFO - Загрузка списка альбомов...
2025-01-08 14:41:12,735 - INFO - Получение фотографий из альбома: добрая мита_1 (305939525)
2025-01-08 14:41:13,049 - INFO - Получение фотографий из альбома: добрая мита_2 (305942127)
2025-01-08 14:41:13,486 - WARNING - В альбоме добрая мита_2 (305942127) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:13,486 - INFO - Получение фотографий из альбома: добрая мита_3 (305942129)
2025-01-08 14:41:13,944 - WARNING - В альбоме добрая мита_3 (305942129) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:13,944 - INFO - Получение фотографий из альбома: добрая мита_4 (305942132)
2025-01-08 14:41:14,406 - WARNING - В альбоме добрая мита_4 (305942132) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:14,406 - INFO - Получение фотографий из альбома: безумная мита_1 (305939523)
2025-01-08 14:41:14,858 - INFO - Получение фотографий из альбома: безумная мита_2 (305942071)
2025-01-08 14:41:15,309 - WARNING - В альбоме безумная мита_2 (305942071) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:15,310 - INFO - Получение фотографий из альбома: безумная мита_3 (305942080)
2025-01-08 14:41:15,779 - WARNING - В альбоме безумная мита_3 (305942080) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:15,779 - INFO - Получение фотографий из альбома: безумная мита_4 (305942076)
2025-01-08 14:41:16,233 - INFO - Получение фотографий из альбома: кепочка_1 (305939453)
2025-01-08 14:41:16,752 - INFO - Получение фотографий из альбома: кепочка_2 (305941751)
2025-01-08 14:41:17,193 - INFO - Получение фотографий из альбома: кепочка_3 (305941752)
2025-01-08 14:41:17,684 - WARNING - В альбоме кепочка_3 (305941752) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:17,684 - INFO - Получение фотографий из альбома: кепочка_4 (305941754)
2025-01-08 14:41:18,135 - WARNING - В альбоме кепочка_4 (305941754) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:18,135 - INFO - Получение фотографий из альбома: мила_1 (305939520)
2025-01-08 14:41:18,635 - INFO - Получение фотографий из альбома: мила_2 (305941761)
2025-01-08 14:41:19,084 - WARNING - В альбоме мила_2 (305941761) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:19,085 - INFO - Получение фотографий из альбома: мила_3 (305942042)
2025-01-08 14:41:19,523 - WARNING - В альбоме мила_3 (305942042) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:19,523 - INFO - Получение фотографий из альбома: мила_4 (305942045)
2025-01-08 14:41:19,966 - WARNING - В альбоме мила_4 (305942045) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:19,966 - INFO - Получение фотографий из альбома: сонная мита_1 (305939522)
2025-01-08 14:41:20,433 - INFO - Получение фотографий из альбома: сонная мита_2 (305942050)
2025-01-08 14:41:20,966 - WARNING - В альбоме сонная мита_2 (305942050) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:20,966 - INFO - Получение фотографий из альбома: сонная мита_3 (305942052)
2025-01-08 14:41:21,458 - INFO - Получение фотографий из альбома: сонная мита_4 (305942053)
2025-01-08 14:41:21,961 - WARNING - В альбоме сонная мита_4 (305942053) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:21,962 - INFO - Получение фотографий из альбома: коротковолосая мита_1 (305939626)
2025-01-08 14:41:22,405 - INFO - Получение фотографий из альбома: коротковолосая мита_2 (305942152)
2025-01-08 14:41:22,862 - WARNING - В альбоме коротковолосая мита_2 (305942152) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:22,862 - INFO - Получение фотографий из альбома: коротковолосая мита_3 (305942155)
2025-01-08 14:41:23,330 - WARNING - В альбоме коротковолосая мита_3 (305942155) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:23,330 - INFO - Получение фотографий из альбома: коротковолосая мита_4 (305942160)
2025-01-08 14:41:23,791 - WARNING - В альбоме коротковолосая мита_4 (305942160) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:23,791 - INFO - Получение фотографий из альбома: 2d мита_1 (305941253)
2025-01-08 14:41:24,245 - WARNING - В альбоме 2d мита_1 (305941253) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:24,245 - INFO - Получение фотографий из альбома: 2d мита_2 (305942269)
2025-01-08 14:41:24,728 - WARNING - В альбоме 2d мита_2 (305942269) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:24,728 - INFO - Получение фотографий из альбома: 2d мита_3 (305942281)
2025-01-08 14:41:25,196 - WARNING - В альбоме 2d мита_3 (305942281) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:25,196 - INFO - Получение фотографий из альбома: 2d мита_4 (305942288)
2025-01-08 14:41:25,768 - WARNING - В альбоме 2d мита_4 (305942288) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:25,768 - INFO - Получение фотографий из альбома: призрачная мита_1 (305941245)
2025-01-08 14:41:26,240 - WARNING - В альбоме призрачная мита_1 (305941245) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:26,240 - INFO - Получение фотографий из альбома: призрачная мита_2 (305942245)
2025-01-08 14:41:26,776 - WARNING - В альбоме призрачная мита_2 (305942245) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:26,777 - INFO - Получение фотографий из альбома: призрачная мита_3 (305942250)
2025-01-08 14:41:27,248 - WARNING - В альбоме призрачная мита_3 (305942250) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:27,249 - INFO - Получение фотографий из альбома: призрачная мита_4 (305942254)
2025-01-08 14:41:27,708 - WARNING - В альбоме призрачная мита_4 (305942254) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:27,708 - INFO - Получение фотографий из альбома: чиби мита_1 (305941223)
2025-01-08 14:41:28,150 - WARNING - В альбоме чиби мита_1 (305941223) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:28,150 - INFO - Получение фотографий из альбома: чиби мита_2 (305942167)
2025-01-08 14:41:28,596 - WARNING - В альбоме чиби мита_2 (305942167) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:28,596 - INFO - Получение фотографий из альбома: чиби мита_3 (305942172)
2025-01-08 14:41:29,062 - WARNING - В альбоме чиби мита_3 (305942172) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:29,062 - INFO - Получение фотографий из альбома: чиби мита_4 (305942174)
2025-01-08 14:41:29,557 - WARNING - В альбоме чиби мита_4 (305942174) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:29,557 - INFO - Получение фотографий из альбома: мита-ядро_1 (305941262)
2025-01-08 14:41:30,005 - WARNING - В альбоме мита-ядро_1 (305941262) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:30,006 - INFO - Получение фотографий из альбома: мита-ядро_2 (305942321)
2025-01-08 14:41:30,456 - WARNING - В альбоме мита-ядро_2 (305942321) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:30,456 - INFO - Получение фотографий из альбома: мита-ядро_3 (305942326)
2025-01-08 14:41:30,907 - WARNING - В альбоме мита-ядро_3 (305942326) нет фотографий или доступ к альбому закрыт.
2025-01-08 14:41:52,798 - INFO - Новое событие: <vk_api.longpoll.Event object at 0x7c399d49d0>
2025-01-08 14:41:52,798 - INFO - Новое сообщение из event.text: Начать
2025-01-08 14:41:52,798 - INFO - Новое сообщение от 389769778: Начать (до handle_command)
2025-01-08 14:41:52,798 - INFO - Проверка команды: Начать в COMMANDS['commands']
2025-01-08 14:41:52,973 - INFO - Новое сообщение от 389769778: Начать (после handle_command)
2025-01-08 14:41:55,217 - INFO - Новое событие: <vk_api.longpoll.Event object at 0x7c483f4cd0>
2025-01-08 14:41:55,217 - INFO - Новое сообщение из event.text: Лицензия
2025-01-08 14:41:55,217 - INFO - Новое сообщение от 389769778: Лицензия (до handle_command)
2025-01-08 14:41:55,217 - INFO - Проверка команды: Лицензия в COMMANDS['commands']
2025-01-08 14:41:55,559 - INFO - Новое сообщение от 389769778: Лицензия (после handle_command) Файл bot.py: 
import vk_api
from vk_api.longpoll import VkLongPoll, VkEventType
from vk_api.keyboard import VkKeyboard, VkKeyboardColor
import random
import datetime
import io
import requests
import logging
import os
import json
from license_generator import ImageProcessor
import concurrent.futures
import time
from threading import Lock
import functools

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

CONFIG_FILE = "config.json"
COMMANDS_FILE = "commands.json"


def load_config(config_path):
    try:
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
            return config
    except FileNotFoundError:
        logging.error(f"Файл конфигурации не найден: {config_path}")
        return None
    except json.JSONDecodeError:
        logging.error(f"Ошибка декодирования JSON в файле: {config_path}")
        return None

def load_commands(commands_path):
    try:
        with open(commands_path, 'r', encoding='utf-8') as f:
            commands = json.load(f)
            return commands
    except FileNotFoundError:
        logging.error(f"Файл команд не найден: {commands_path}")
        return None
    except json.JSONDecodeError:
        logging.error(f"Ошибка декодирования JSON в файле: {commands_path}")
        return None


CONFIG = load_config(CONFIG_FILE)
if not CONFIG:
    exit(1)
COMMANDS = load_commands(COMMANDS_FILE)
if not COMMANDS:
    exit(1)

if not isinstance(COMMANDS, dict):
   logging.error("Ошибка: COMMANDS не является словарем")
   exit(1)
logging.info(f"Загруженные команды: {COMMANDS}")

def retry(func, retries=3, delay=1):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        for attempt in range(retries):
            try:
                return func(*args, **kwargs)
            except (requests.exceptions.RequestException, vk_api.exceptions.ApiError) as e:
                logging.error(f"Ошибка в {func.__name__}, попытка {attempt + 1}: {e}. Повтор через {delay} секунд.")
                if attempt < retries - 1:
                   time.sleep(delay)
        logging.error(f"Сбой в {func.__name__} после {retries} попыток.")
        return None
    return wrapper

class VkBot:
    def get_group_name(self):
      try:
        group_info = self.vk.groups.getById(group_id=self.config["group_id"], fields='screen_name')
        return group_info[0]['screen_name']
      except vk_api.exceptions.ApiError as e:
        logging.error(f"Ошибка при получении имени группы: {e}")
        return None
    
    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.executor = concurrent.futures.ThreadPoolExecutor(max_workers=num_threads)
        self.user_processing = {}
        self.cache_lock = Lock()
        self.user_state = {}

    def create_keyboard(self, keyboard_name):
        if keyboard_name not in COMMANDS["keyboards"]:
            return None
        keyboard = VkKeyboard(one_time=False)
        buttons = sorted(COMMANDS["keyboards"][keyboard_name], key=lambda x: x.get("layer", 1))
        current_layer = 1
        for button in buttons:
           if button.get("layer", 1) != current_layer:
              keyboard.add_line()
              current_layer = button.get("layer", 1)
           keyboard.add_button(button["text"], color=button["color"])
        return keyboard.get_keyboard()
    
    def load_all_albums(self):
       logging.info("Загрузка списка альбомов...")
       owner_id = self.config["group_id"]
       try:
          for album_name, album_id in self.config["albums_urls"].items():
             logging.info(f"Получение фотографий из альбома: {album_name} ({album_id})")
             try:
                photos = self.vk_user.photos.get(owner_id=owner_id, album_id=album_id, count=1)['items']
                if not photos:
                   logging.warning(f"В альбоме {album_name} ({album_id}) нет фотографий или доступ к альбому закрыт.")
             except vk_api.exceptions.ApiError as e:
                logging.error(f"Ошибка при получении альбома {album_name}: {e}")

       except Exception as e:
          logging.error(f"Ошибка при загрузке списка альбомов: {e}")
    
    @retry
    def get_random_photo(self, album_id):
        owner_id = self.config["group_id"]
        logging.info(f"Запрос фото. Owner ID: {owner_id}, Album ID: {album_id}")
        photos = self.vk_user.photos.get(owner_id=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']

    @retry
    def check_is_don(self, user_id):
        return self.vk.groups.isMember(group_id=self.config["donate_group_id"], user_id=user_id)

    @retry
    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)

    @retry
    def send_image_vk(self, image, user_id):
            image_bytes = io.BytesIO()
            image.save(image_bytes, format="PNG")
            image_bytes.seek(0)
            upload_url = self.vk.photos.getMessagesUploadServer(peer_id=user_id)["upload_url"]
            upload_response = requests.post(upload_url, files={'photo': ('image.png', image_bytes)}).json()
            if "photo" not in upload_response:
                logging.error(f"Ошибка загрузки: {upload_response}")
                return
            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"Изображение отправлено: {user_id}.")
    
    def _get_license_data(self, user_id, selected_character, album_id) -> tuple:
        is_don = self.check_is_don(user_id)
        if is_don is None:
            raise ValueError("Не удалось проверить статус дона.")
        
        photo_url = self.get_random_photo(album_id)
        if not photo_url:
            raise ValueError("Не удалось получить фото")
    
        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)}"
        
        group_name = self.get_group_name()
        if group_name is None:
            raise ValueError("Не удалось получить имя группы")
        return user_name, date_start, date_end, license_id, photo_url, is_don, group_name
    
    def _create_qr_code(self, user_id: int, is_don: bool):
        if is_don:
            return self.image_processor.generate_qr_code(f"https://vk.com/id{user_id}")
        return self.image_processor.generate_qr_code(self.config["qr_default_url"])

    def _send_license_image(self, user_id: int, image):
        if image:
            self.send_image_vk(image, user_id)
            with self.cache_lock:
                self.user_processing[user_id] = False
        else:
            self.send_vk_message(user_id, "Ошибка создания лицензии")
            with self.cache_lock:
               self.user_processing[user_id] = False

    def process_license_async(self, user_id, selected_character, album_id):
        with self.cache_lock:
            if self.user_processing.get(user_id, False):
                self.send_vk_message(user_id, "Пожалуйста, дождитесь завершения предыдущего запроса.")
                return
            self.user_processing[user_id] = True
        self.executor.submit(self.process_license, user_id, selected_character, album_id)
    
    def process_license(self, user_id, selected_character, album_id):
        try:
             user_name, date_start, date_end, license_id, photo_url, is_don, group_name = self._get_license_data(user_id, selected_character, album_id)
            
             def download_image_with_retry(url, retries=3, delay=1):
                 for attempt in range(retries):
                    try:
                        background = self.image_processor.download_image(url)
                        if background:
                            return background
                    except requests.exceptions.RequestException as e:
                         logging.error(f"Ошибка при загрузке фото, попытка {attempt + 1}: {e}")
                         if attempt < retries - 1:
                           time.sleep(delay)
                    except Exception as e:
                         logging.error(f"Неизвестная ошибка при загрузке фото, попытка {attempt + 1}: {e}")
                         if attempt < retries - 1:
                           time.sleep(delay)
                 return None
            
             background = download_image_with_retry(photo_url)

             if not background:
                self.send_vk_message(user_id, "Не удалось загрузить фон")
                with self.cache_lock:
                    self.user_processing[user_id] = False
                return
             user_data = {
                'user_name': user_name,
                'date_start': date_start,
                'date_end': date_end,
                'license_id': license_id,
                'license_text': f"ЛИЦЕНЗИЯ НА {selected_character.upper()}",
                'group_name': group_name  # Добавляем короткое имя группы
            }
             qr_code = self._create_qr_code(user_id, is_don)

             def callback(license_image):
                self._send_license_image(user_id, license_image)

             self.image_processor.process_license_async(user_id, background, user_data, qr_code, callback)

        except ValueError as e:
            self.send_vk_message(user_id, str(e))
            logging.error(f"Ошибка обработки: {e}")
            with self.cache_lock:
                self.user_processing[user_id] = False
        except Exception as e:
            self.send_vk_message(user_id, f"Ошибка: {e}")
            logging.error(f"Ошибка обработки: {e}")
            with self.cache_lock:
                self.user_processing[user_id] = False

    def shutdown(self):
        logging.info("Завершение работы ботом...")
        self.executor.shutdown(wait=True)
        logging.info("Все потоки завершены.")

    def handle_command(self, user_id, user_text):
        state = self.user_state.get(user_id, {"menu": "main"})
        if user_text == "назад":
            if state["menu"] == "character":
                self.user_state[user_id] = {"menu": "main"}
                self.send_vk_message(user_id, "Что вы хотите сделать?", keyboard=self.create_keyboard("main_menu"))
            elif state["menu"] == "albums":
                self.user_state[user_id] = {"menu": "character"}
                self.send_vk_message(user_id, "Выберите персонажа для лицензии:", keyboard=self.create_keyboard("character_menu"))
            elif state["menu"] == "license_options":
                  character_name = state.get("character")
                  if character_name:
                     self.user_state[user_id] = {"menu": "albums", "character": character_name}
                     character_data = COMMANDS["characters"].get(character_name)
                     if character_data:
                       keyboard_name = character_data.get("albums_keyboard")
                       keyboard = self.create_keyboard(keyboard_name)
                       self.send_vk_message(user_id, f"Выберите альбом для персонажа {character_data.get('text')}", keyboard=keyboard)
                  else:
                    self.user_state[user_id] = {"menu": "main"}
                    self.send_vk_message(user_id, "Что вы хотите сделать?", keyboard=self.create_keyboard("main_menu"))
            else:
                self.user_state[user_id] = {"menu": "main"}
                self.send_vk_message(user_id, "Что вы хотите сделать?", keyboard=self.create_keyboard("main_menu"))

        elif user_text == "меню":
            self.user_state[user_id] = {"menu": "main"}
            self.send_vk_message(user_id, "Что вы хотите сделать?", keyboard=self.create_keyboard("main_menu"))
        else:
            logging.info(f"Проверка команды: {user_text} в COMMANDS['commands']")
            command = COMMANDS["commands"].get(user_text, COMMANDS["commands"]["default"])
            if "response" in command:
                keyboard = self.create_keyboard(command.get("keyboard"))
                self.send_vk_message(user_id, command["response"], keyboard=keyboard)
                if user_text == "лицензия":
                  self.user_state[user_id] = {"menu": "character"}
            elif state["menu"] == "character":
                character_data = COMMANDS["characters"].get(user_text)
                if character_data:
                    albums_keyboard = character_data.get("albums_keyboard")
                    if albums_keyboard:
                      self.user_state[user_id] = {"menu": "albums", "character": user_text}
                      keyboard = self.create_keyboard(albums_keyboard)
                      self.send_vk_message(user_id, f"Выберите альбом для персонажа {character_data.get('text')}", keyboard=keyboard)
                    else:
                       if user_text == "создать случайную лицензию":
                           characters = list(COMMANDS["characters"].keys())
                           if "создать случайную лицензию" in characters:
                              characters.remove("создать случайную лицензию")
                           random_character = random.choice(characters)
                           albums_keyboard = COMMANDS["characters"].get(random_character).get("albums_keyboard")
                           if albums_keyboard:
                               albums = COMMANDS["albums"].get(albums_keyboard)
                               if albums:
                                 random_album = random.choice(albums)
                                 self.process_license_async(user_id, random_character, random_album["album_id"])
                           self.user_state[user_id] = {"menu": "license_options", "character": random_character}
                else:
                   self.send_vk_message(user_id, "Неизвестная команда", keyboard = self.create_keyboard("back_and_menu"))
            elif state["menu"] == "albums":
                  character_name = state.get("character")
                  albums_keyboard = COMMANDS["characters"].get(character_name).get("albums_keyboard")
                  if albums_keyboard:
                     #Здесь нужно заменить на COMMANDS["albums"]  но такого ключа нет
                     for album_name, album_id in self.config["albums_urls"].items():
                       if album_name == user_text:
                            self.process_license_async(user_id, character_name, album_id)
                            self.user_state[user_id] = {"menu": "license_options", "character": character_name, "album": album_name}
                            self.send_vk_message(user_id, "Лицензия создана", keyboard=self.create_keyboard("license_creation_menu"))
                            return
                     self.send_vk_message(user_id, "Неизвестная команда", keyboard = self.create_keyboard("back_and_menu"))
            elif state["menu"] == "license_options":
                 self.send_vk_message(user_id, "Лицензия создана", keyboard=self.create_keyboard("license_creation_menu"))
            else:
               self.send_vk_message(user_id, "Неизвестная команда")

    def run(self):
        logging.info("Бот запущен. Ожидание сообщений...")
        self.load_all_albums()
        while True:
            try:
                for event in self.longpoll.listen():
                    if event.type == VkEventType.MESSAGE_NEW and event.to_me:
                        logging.info(f"Новое событие: {event}")
                        logging.info(f"Новое сообщение из event.text: {event.text}")
                        user_id = event.user_id
                        user_text = event.text
                        logging.info(f"Новое сообщение от {user_id}: {user_text} (до handle_command)")
                        self.handle_command(user_id, user_text)
                        logging.info(f"Новое сообщение от {user_id}: {user_text} (после handle_command)")

            except requests.exceptions.RequestException as e:
                logging.error(f"Проблема с сетью: {e}. Переподключение через 5 секунд...")
                time.sleep(5)
            except vk_api.exceptions.ApiError as e:
                logging.error(f"Проблема с API VK: {e}. Переподключение через 5 секунд...")
                time.sleep(5)
            except Exception as e:
                 logging.error(f"Неизвестная ошибка в основном цикле: {e}. Переподключение через 5 секунд...")
                 time.sleep(5)
            finally:
                pass

if __name__ == "__main__":
    image_processor = ImageProcessor(CONFIG)
    num_threads = 8
    bot = VkBot(CONFIG, image_processor, num_threads=num_threads)
    bot.run()
Файл license_generator.py: 
import os
import json
from typing import Tuple, List, Dict, Any, Optional, TypeVar, Generic
from collections import OrderedDict
import requests
import io
from PIL import Image, ImageDraw, ImageFont
import random
import math
import qrcode
import numpy as np
import functools
from concurrent.futures import ThreadPoolExecutor, Future
import multiprocessing
import time
from threading import Semaphore, Lock  # Импортируем Lock
import hashlib
import vk_api
from vk_api.keyboard import VkKeyboard, VkKeyboardColor, VkKeyboardButton
from vk_api.utils import get_random_id
import logging

T = TypeVar('T')


class LRUCache(Generic[T]):
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache: OrderedDict[Any, T] = OrderedDict()
        self.lock = Lock()  # Добавляем блокировку

    def get(self, key: Any) -> Optional[T]:
        with self.lock:
            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:
        with self.lock:
            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:
        with self.lock:
            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()}
        self.executor = ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())
        self.last_request_time: Dict[int, float] = {}
        self.request_semaphore = Semaphore(self.config["max_concurrent_requests"])
        self.downloaded_images = set()
        self.vk_session = None
        self.vk = None
        self.upload_session = None
        
    def init_vk_session(self, token):
        self.vk_session = vk_api.VkApi(token=token)
        self.vk = self.vk_session.get_api()
        self.upload_session = vk_api.VkUpload(self.vk_session)


    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.downloaded_images:
            cached_image = self.image_cache.get(image_url)
            if cached_image:
                return cached_image
        try:
            response = requests.get(image_url, stream=True, timeout=10)
            response.raise_for_status()
            image = Image.open(io.BytesIO(response.content)).convert("RGBA")
            self.image_cache.put(image_url, image)
            self.downloaded_images.add(image_url)
            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_process(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]) -> np.ndarray:
      x1, y1 = point1
      x2, y2 = point2
      dx, dy = x2 - x1, y2 - y1
      length = math.sqrt(dx ** 2 + dy ** 2)
      if length == 0:
        return np.full((size[1], size[0], 4), start_color, dtype=np.uint8)
      x, y = np.meshgrid(np.arange(size[0]), np.arange(size[1]))
      px, py = x - x1, y - y1
      t = np.clip((px * dx + py * dy) / length ** 2, 0, 1).astype(np.float64)
      start_color_array = np.array(start_color, dtype=np.float64)
      end_color_array = np.array(end_color, dtype=np.float64)
      lerped_colors = (start_color_array + (end_color_array - start_color_array) * t[:, :, None]).astype(np.uint8)
      return lerped_colors

    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_array = self._create_gradient_background_process(size, start_color, end_color, point1, point2)
        gradient = Image.fromarray(gradient_array)
        self.gradient_cache.put(cache_key, gradient)
        return gradient
    

    @functools.lru_cache(maxsize=32)
    def create_rounded_mask(self, width: int, height: int, radius: int) -> Image.Image:
        mask = Image.new('L', (width, height), 0)
        ImageDraw.Draw(mask).rounded_rectangle((0, 0, width, height), radius=radius, fill=255)
        return mask

    @functools.lru_cache(maxsize=32)
    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

    @functools.lru_cache(maxsize=32)
    def create_cut_gradient_mask(self, width: int, height: int, corner_radius: int) -> Image.Image:
      mask = Image.new('L', (width, height), 0)
      ImageDraw.Draw(mask).rounded_rectangle((0, 0, width, height), radius=corner_radius, fill=255)
      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
      top = (h - target_height) // 2
      cropped_image = resized_image.crop((left, top, left + target_width, top + target_height))
      self.resized_cache.put(cache_key, cropped_image)
      return cropped_image
    

    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:
        fill_color = tuple(fill) if isinstance(fill, list) else fill
        if stroke_width > 0:
           color = (*stroke_color[:3], int(stroke_color[3] * stroke_opacity))
           draw.text((x - stroke_width, y), text, font=font, fill=color)
           draw.text((x + stroke_width, y), text, font=font, fill=color)
           draw.text((x, y - stroke_width), text, font=font, fill=color)
           draw.text((x, y + stroke_width), text, font=font, fill=color)
        draw.text((x, y), text, font=font, fill=fill_color)

    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:
        fill_color = tuple(color) if isinstance(color, list) else color
        outline_color = tuple(stroke_color) if isinstance(stroke_color, list) else stroke_color
        if stroke_width > 0:
            draw.rounded_rectangle((x, y, x + width, y + height), radius=radius, fill=fill_color, outline=outline_color,
                                  width=stroke_width)
        else:
            draw.rounded_rectangle((x, y, x + width, y + height), radius=radius, fill=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 mirror_image(self, image: Image.Image) -> Image.Image:
        return image.transpose(Image.FLIP_LEFT_RIGHT)
    
    def _image_to_key(self, image: Image.Image) -> str:
        image_bytes = io.BytesIO()
        image.save(image_bytes, format="PNG")
        image_bytes = image_bytes.getvalue()
        return hashlib.md5(image_bytes).hexdigest()
        

    
    def _analyze_image_colors_internal(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 = np.array(img, dtype=np.uint8)
            width, height, _ = img_array.shape
            line_colors = []
            for y in range(height):
                for x in range(width):
                    if img_array[y, x, 3] > 0:
                        line_colors.append(tuple(img_array[y, x, :3]))

            # Получаем уникальные цвета и выбираем 3-4 случайных цвета
            unique_colors = list(set(line_colors))
            if not unique_colors:
                return []
            num_colors = min(len(unique_colors), 4)
            selected_colors = random.sample(unique_colors, num_colors)
            
            return [tuple(c) + (255,) for c in selected_colors]
    
    def analyze_image_colors(self, image: Image.Image) -> List[Tuple[int, int, int, int]]:
          image_key = self._image_to_key(image)
          cached_colors = self.gradient_cache.get(image_key)
          if cached_colors:
             return cached_colors
          colors = self._analyze_image_colors_internal(image)
          self.gradient_cache.put(image_key, colors)
          return colors
          

    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]

        return random.sample(colors, 2)
    
    def process_license_async(self, user_id: int, background: Image.Image, user_data: Dict[str, Any],
                             qr_code: Optional[Image.Image], callback):
         
        current_time = time.time()
        if user_id in self.last_request_time and (current_time - self.last_request_time[user_id] < self.config["request_interval"]):
            return
        self.last_request_time[user_id] = current_time

        def safe_callback(image):
            try:
                callback(image)
            except Exception:
                pass
            finally:
                self.request_semaphore.release()

                
        self.request_semaphore.acquire()
        self.send_vk_message(user_id, "Начинаю обработку лицензии, ожидайте...")
        future = self.executor.submit(self.create_license_image, background, user_data, qr_code)
        
        def timeout_message(future: Future):
              if not future.done():
                 self.send_vk_message(user_id, "Бот сейчас загружен, придётся подождать больше обычного, прости за неудобства")
                 
        timer = self.executor.submit(timeout_message, future)
        
        future.add_done_callback(lambda f: timer.cancel())
        future.add_done_callback(lambda f: safe_callback(f.result()))
    

    def create_license_image(self, background: Image.Image, user_data: Dict[str, Any],
                             qr_code: Optional[Image.Image]) -> Optional[Image.Image]:
        start_time = time.time()
        if not background:
            return None
        
        iw, ih = 2560, 1440
        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 = Image.composite(gradient_background, Image.new('RGBA', (gw, gh), (0, 0, 0, 0)), gradient_mask)
        
        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": 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:
            pass
        
        end_time = time.time()
        return main_background
    
    def send_vk_message(self, user_id: int, message: str, keyboard=None):
        try:
            if not self.vk:
                return
            self.vk.messages.send(peer_id=user_id, random_id=get_random_id(), message=message,
                                keyboard=keyboard)
        except vk_api.exceptions.ApiError:
            pass
        except Exception:
            pass

    def send_image_vk(self, image: Image.Image, user_id: int, keyboard=None):
        try:
            image_bytes = io.BytesIO()
            image.save(image_bytes, format="PNG")
            image_bytes.seek(0)
            photo = self.upload_session.photo_messages(image_bytes)[0]

            self.vk.messages.send(peer_id=user_id, random_id=get_random_id(),
                                      attachment=f"photo{photo['owner_id']}_{photo['id']}", keyboard = keyboard)

        except vk_api.exceptions.ApiError:
            pass
        except requests.exceptions.RequestException:
            pass
        except Exception:
            pass
Файл config.json: {
    "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,
  "donate_group_id": 228820799,
  "qr_default_url": "https://vk.com/m_mitka",
  "albums_urls": {
       "добрая мита_1": "305939525",
        "добрая мита_2": "305942127",
        "добрая мита_3": "305942129",
         "добрая мита_4": "305942132",
        "безумная мита_1": "305939523",
         "безумная мита_2": "305942071",
        "безумная мита_3": "305942080",
         "безумная мита_4": "305942076",
      "кепочка_1": "305939453",
      "кепочка_2": "305941751",
      "кепочка_3": "305941752",
       "кепочка_4": "305941754",
        "мила_1": "305939520",
         "мила_2": "305941761",
          "мила_3": "305942042",
           "мила_4": "305942045",
         "сонная мита_1": "305939522",
           "сонная мита_2": "305942050",
            "сонная мита_3": "305942052",
             "сонная мита_4": "305942053",
         "коротковолосая мита_1": "305939626",
         "коротковолосая мита_2": "305942152",
           "коротковолосая мита_3": "305942155",
           "коротковолосая мита_4": "305942160",
        "2d мита_1": "305941253",
        "2d мита_2": "305942269",
          "2d мита_3": "305942281",
           "2d мита_4": "305942288",
        "призрачная мита_1": "305941245",
         "призрачная мита_2": "305942245",
          "призрачная мита_3": "305942250",
            "призрачная мита_4": "305942254",
       "чиби мита_1": "305941223",
       "чиби мита_2": "305942167",
      "чиби мита_3": "305942172",
        "чиби мита_4": "305942174",
         "мита-ядро_1": "305941262",
          "мита-ядро_2": "305942321",
          "мита-ядро_3": "305942326"
  },
    "qr_default_url": "https://vk.com/club228820799",
    "font_config": {
        "main_font": {
            "file": "Шрифт - 10733.otf",
            "size": 85
        },
        "big_font": {
            "file": "Шрифт - 10733.otf",
            "size": 85
        },
        "id_font": {
            "file": "Шрифт - 10733.otf",
            "size": 85
        },
        "small_font": {
            "file": "Шрифт - 10733.otf",
            "size": 85
        },
        "arkadia_font": {
            "file": "Шрифт - Appetite-Rounded.ttf",
            "size": 138
        }
    },
    "text_config": {
        "issued_by": {
            "text": "ВЫДАНО: @{group_name}",
            "x": 310,
            "y": 150,
            "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": 310,
            "y": 250,
            "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": 310,
            "y": 350,
            "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": 310,
            "y": 450,
            "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": 310,
            "y": 800,
            "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": 310,
            "y": 900,
            "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": 310,
            "y": 1055,
            "font": "arkadia_font",
            "fill": [
                0,
                0,
                0
            ],
            "stroke_width": 5,
            "stroke_color": [
                255,
                0,
                0
            ],
            "stroke_opacity": 1.0
        }
    },
    "gradient_config": {
        "x": 250,
        "y": 81,
        "width": 1065,
        "height": 1280,
        "radius": 153,
        "layer": 3
    },
    "background_config": {
        "x": 1316,
        "y": 81,
        "size": [
            2040,
            2160
        ],
        "radius": 153,
        "target_width": 998,
        "target_height": 1280,
        "horizontal_percentage": 0.7,
        "layer": 2,
        "mirror_offset_x": -2040,
        "mirror_offset_y": 0
    },
    "rectangle_config": {
        "white_rect": {
            "size": [
                2062,
                1280
            ],
            "x": 250,
            "y": 81,
            "color": [
                255,
                255,
                255,
                255
            ],
            "stroke_width": 0,
            "stroke_color": [
                0,
                0,
                0,
                255
            ],
            "radius": 153,
            "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,
    "request_interval": 10,
    "max_concurrent_requests": 4
} Файл commands.json: {
  "commands": {
    "начать": {
      "response": "Что вы хотите сделать?",
      "keyboard": "main_menu"
    },
    "лицензия": {
      "response": "Выберите персонажа:",
      "keyboard": "character_menu",
      "level": "character_menu"
    },
    "назад": {
      "action": "back"
    },
    "меню": {
      "action": "main_menu"
    },
    "создать случайную лицензию": {
      "action": "random_license"
    },
     "Безумная Мита": {
      "action": "select_character",
      "albums_keyboard": "mad_mita_albums"
    },
     "Добрая Мита": {
      "action": "select_character",
      "albums_keyboard": "good_mita_albums"
     },
    "Кепочка": {
      "action": "select_character",
      "albums_keyboard": "cap_albums"
    },
     "Мила": {
       "action": "select_character",
      "albums_keyboard": "mila_albums"
     },
     "Сонная Мита": {
       "action": "select_character",
      "albums_keyboard": "sleepy_mita_albums"
    },
    "Коротковолосая Мита": {
       "action": "select_character",
      "albums_keyboard": "short_hair_mita_albums"
    },
    "2D Мита": {
       "action": "select_character",
      "albums_keyboard": "2d_mita_albums"
    },
     "Призрачная Мита": {
       "action": "select_character",
      "albums_keyboard": "ghost_mita_albums"
     },
     "Чиби Мита": {
        "action": "select_character",
      "albums_keyboard": "chibi_mita_albums"
    },
    "Мита-ядро": {
       "action": "select_character",
      "albums_keyboard": "core_mita_albums"
    },
    "default": {
        "response": "Неизвестная команда. Пожалуйста, попробуйте снова.",
        "keyboard": "main_menu"
    }
  },
  "characters": {
    "Безумная Мита": {
        "albums_keyboard": "mad_mita_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
     },
    "Добрая Мита": {
        "albums_keyboard": "good_mita_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
     },
    "Кепочка": {
        "albums_keyboard": "cap_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
    },
    "Мила": {
        "albums_keyboard": "mila_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
     },
    "Сонная Мита": {
        "albums_keyboard": "sleepy_mita_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
     },
    "Коротковолосая Мита": {
        "albums_keyboard": "short_hair_mita_albums",        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
    },
    "2D Мита": {
         "albums_keyboard": "2d_mita_albums",
         "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
     },
    "Призрачная Мита": {
        "albums_keyboard": "ghost_mita_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
    },
    "Чиби Мита": {
        "albums_keyboard": "chibi_mita_albums",
        "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
    },
    "Мита-ядро": {
      "albums_keyboard": "core_mita_albums",
      "albums_names": ["Фанатские Арты", "Творчество Подписчиков", "Скришоты из MiSide", "Косплей"]
    }
  },
  "keyboards": {
    "main_menu": [
      {
        "text": "Лицензия",
        "color": "primary",
        "layer": 1
      }
    ],
    "character_menu": [
      {
        "text": "Безумная Мита",
        "color": "primary",
        "layer": 1
      },
      {
        "text": "Добрая Мита",
        "color": "primary",
        "layer": 1
      },
      {
        "text": "Кепочка",
        "color": "primary",
        "layer": 1
      },
        {
          "text": "Мила",
          "color": "primary",
          "layer": 2
      },
      {
        "text": "Сонная Мита",
        "color": "primary",
        "layer": 2
      },
        {
          "text": "Коротковолосая Мита",
          "color": "primary",
          "layer": 2
      },
        {
            "text": "2D Мита",
             "color": "primary",
            "layer": 3
         },
      {
           "text": "Призрачная Мита",
           "color": "primary",
          "layer": 3
       },
       {
            "text": "Чиби Мита",
             "color": "primary",
             "layer": 3
        },
       {
          "text": "Мита-ядро",
           "color": "primary",
          "layer": 3
       },
      {
        "text": "Создать случайную лицензию",
        "color": "secondary",
        "layer": 4
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 5
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 5
      }
    ],
    "mad_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "безумная мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key": "безумная мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "безумная мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "безумная мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
    "good_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "добрая мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key": "добрая мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "добрая мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "добрая мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
    "cap_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "кепочка_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key": "кепочка_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "кепочка_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "кепочка_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
     "mila_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "мила_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key": "мила_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "мила_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "мила_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
     "sleepy_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "сонная мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key": "сонная мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "сонная мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "сонная мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
     "short_hair_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "коротковолосая мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key": "коротковолосая мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "коротковолосая мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "коротковолосая мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
    "2d_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "2d мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
         "album_key": "2d мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "2d мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "2d мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
      "ghost_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key": "призрачная мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
         "album_key": "призрачная мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
        "album_key": "призрачная мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key": "призрачная мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
     "chibi_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
         "album_key":"чиби мита_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
         "album_key":"чиби мита_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
         "album_key":"чиби мита_3"
      },
      {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
        "album_key":"чиби мита_4"
      },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
      "core_mita_albums": [
      {
        "text": "Фанатские Арты",
        "color": "primary",
        "layer": 1,
        "album_key":"мита-ядро_1"
      },
      {
        "text": "Творчество Подписчиков",
        "color": "primary",
        "layer": 1,
        "album_key":"мита-ядро_2"
      },
      {
        "text": "Скришоты из MiSide",
        "color": "primary",
        "layer": 2,
         "album_key":"мита-ядро_3"
      },
          {
        "text": "Косплей",
        "color": "primary",
        "layer": 2,
         "album_key":"мита-ядро_4"
        },
      {
        "text": "Назад",
        "color": "negative",
        "layer": 3
      },
      {
        "text": "Меню",
        "color": "negative",
        "layer": 3
      }
    ],
    "license_creation_menu": [
      {
          "text": "Создать еще из этого альбома",
          "color": "positive",
          "layer": 1,
          "action": "create_same"
        },
        {
           "text": "Создать еще из другого альбома",
           "color": "primary",
           "layer": 1,
           "action": "create_another"
        },
        {
          "text": "Назад",
          "color": "negative",
          "layer": 2
        },
        {
          "text": "Меню",
          "color": "negative",
          "layer": 2
        }
    ]
  }
}