Logs -> modpack_builder.log
ai_module.py
from transformers import GPT2Tokenizer, GPT2LMHeadModel
import torch
import spacy
from utils import load_config
import logging
from deep_translator import GoogleTranslator  # Использование deep-translator

class AIModule:
    def __init__(self):
        config = load_config()
        self.model_name = config.get('gpt_model_name', 'sberbank-ai/rugpt3small_based_on_gpt2')
        self.max_tokens = config.get('max_tokens', 100)

        logging.info(f"Загрузка модели {self.model_name}...")
        self.tokenizer = GPT2Tokenizer.from_pretrained(self.model_name)
        self.model = GPT2LMHeadModel.from_pretrained(self.model_name)
        self.model.eval()
        if torch.cuda.is_available():
            self.model.to('cuda')
            logging.info("Используется GPU для обработки модели.")
        else:
            logging.info("Используется CPU для обработки модели.")
        
        # Установите pad_token_id to eos_token_id для подавления предупреждений
        if self.tokenizer.pad_token is None:
            self.tokenizer.pad_token = self.tokenizer.eos_token
            self.model.config.pad_token_id = self.tokenizer.eos_token_id

        # Загрузка русской модели spaCy
        try:
            self.nlp = spacy.load("ru_core_news_sm")
            logging.info("Модель spaCy загружена успешно.")
        except Exception as e:
            logging.error(f"Ошибка при загрузке модели spaCy: {e}")
            self.nlp = None

        # Инициализация переводчика с использованием deep_translator
        try:
            self.translator = GoogleTranslator(source='ru', target='en')
            logging.info("Переводчик GoogleTranslator инициализирован.")
        except Exception as e:
            logging.error(f"Ошибка при инициализации переводчика: {e}")
            self.translator = None

    def analyze_description(self, description):
        try:
            # Убедитесь, что description является строкой
            if not isinstance(description, str):
                logging.warning(f"Ожидалась строка для описания, но получено {type(description)}. Преобразование в строку.")
                description = str(description)

            # Извлечение ключевых слов с помощью spaCy (POS-теги)
            keywords = set()
            if self.nlp:
                doc = self.nlp(description)
                for token in doc:
                    if token.pos_ in ('NOUN', 'PROPN'):
                        keywords.add(token.lemma_.lower())
            else:
                logging.warning("spaCy не загружен. Использование только GPT-2 для извлечения ключевых слов.")

            # Генерация дополнительных ключевых слов с помощью GPT-2
            prompt = (
                "Извлеките релевантные ключевые слова и темы из следующего описания модпака для Minecraft. "
                "Выведите их в виде списка, разделенного запятыми.\n\n"
                f"Описание: {description}\n\nКлючевые слова и темы:"
            )
            
            inputs = self.tokenizer.encode(prompt, return_tensors='pt')
            if torch.cuda.is_available():
                inputs = inputs.to('cuda')

            # Создание attention_mask
            attention_mask = torch.ones_like(inputs)

            with torch.no_grad():
                outputs = self.model.generate(
                    inputs,
                    attention_mask=attention_mask,  # Явная передача attention_mask
                    max_length=inputs.shape[1] + self.max_tokens,
                    num_beams=5,                # Используем beam search для более точной генерации
                    early_stopping=True,
                    do_sample=False,            # Отключаем сэмплинг, т.к. используем beam search
                    pad_token_id=self.tokenizer.eos_token_id
                )

            generated_text = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
            logging.debug(f"Generated text from GPT-2: {generated_text}")

            # Извлечение ключевых слов после prompt
            try:
                keywords_segment = generated_text.split("Ключевые слова и темы:")[1]
                gpt_keywords = keywords_segment.strip().split(",")
                gpt_keywords = [kw.strip().lower() for kw in gpt_keywords if kw.strip()]
                # Удаление артефактов, если они присутствуют
                gpt_keywords = [kw for kw in gpt_keywords if not kw.startswith('.zip') and not kw.startswith('download')]
            except IndexError:
                logging.warning("Не удалось извлечь ключевые слова из GPT-2.")
                gpt_keywords = []

            # Объединение ключевых слов из spaCy и GPT-2
            combined_keywords = list(keywords.union(gpt_keywords))
            logging.info(f"Извлеченные ключевые слова: {combined_keywords}")

            # Перевод на английский с использованием deep_translator
            if combined_keywords and self.translator:
                try:
                    translated_keywords = [self.translator.translate(kw).lower() for kw in combined_keywords]
                    logging.info(f"Переведенные ключевые слова: {translated_keywords}")
                except Exception as e:
                    logging.error(f"Ошибка при переводе ключевых слов: {e}")
                    translated_keywords = combined_keywords  # Фоллбэк на оригинальные ключевые слова
            else:
                translated_keywords = combined_keywords  # Если переводчик не инициализирован или ключевые слова отсутствуют

            return translated_keywords

        except Exception as e:
            logging.error(f"Ошибка при анализе описания: {e}", exc_info=True)
            return []

config.yaml
minecraft_version: "1.20.1"
platform: "forge"  # или "fabric"
modrinth_api_url: "https://api.modrinth.com/v2"
gpt_model_name: "sberbank-ai/rugpt3small_based_on_gpt2"  # Используем русскую модель GPT-2
max_tokens: 100
log_level: "INFO"  # Уровень логирования: DEBUG, INFO, WARNING, ERROR, CRITICAL
mods_directory: "mods"  # Путь к папке для сохранения модов (относительно директории проекта)

gui_interface.py
import tkinter as tk
from tkinter import scrolledtext, messagebox
from ai_module import AIModule
from mod_downloader import search_mods, download_mod, create_readme
from user_preferences import update_preferences
from utils import load_config
import threading
import logging
import queue

class ModpackBuilderGUI:
    def __init__(self, master):
        self.master = master
        self.master.title("Конструктор Модпаков Minecraft")
        
        self.description_label = tk.Label(master, text="Описание Модпака:")
        self.description_label.pack(pady=5)
        
        self.description_text = scrolledtext.ScrolledText(master, width=60, height=10)
        self.description_text.pack(pady=5)
        
        self.build_button = tk.Button(master, text="Создать Модпак", command=self.build_modpack)
        self.build_button.pack(pady=10)
        
        self.status_label = tk.Label(master, text="Статус: Ожидание", fg="blue")
        self.status_label.pack(pady=5)
        
        self.ai = AIModule()
        self.queue = queue.Queue()
        self.master.after(100, self.process_queue)

    def build_modpack(self):
        description = self.description_text.get("1.0", tk.END).strip()
        if not description:
            messagebox.showwarning("Внимание", "Пожалуйста, введите описание модпака.")
            return
        self.update_status("Статус: Анализ описания...", "orange")
        threading.Thread(target=self.process_modpack, args=(description,)).start()

    def process_modpack(self, description):
        try:
            keywords = self.ai.analyze_description(description)
            if not keywords:
                logging.warning("Не удалось извлечь ключевые слова из описания.")
                self.queue.put(("Ошибка: Не удалось извлечь ключевые слова.", "red"))
                return
            
            logging.info(f"Извлеченные ключевые слова: {keywords}")
            self.queue.put((f"Ключевые слова: {', '.join(keywords)}", "blue"))
            
            self.queue.put(("Поиск модов...", "orange"))
            mods = search_mods(keywords)
            if not mods:
                logging.warning("Не удалось найти подходящие моды.")
                self.queue.put(("Ошибка: Не удалось найти подходящие моды.", "red"))
                return
            
            logging.info(f"Найдено {len(mods)} модов.")
            self.queue.put((f"Найдено {len(mods)} модов. Скачивание...", "orange"))
            
            for mod in mods:
                download_mod(mod)
            
            create_readme(mods)
            update_preferences(keywords)
            
            self.queue.put(("Модпак успешно создан!", "green"))
            messagebox.showinfo("Успех", "Модпак успешно создан!")
        except Exception as e:
            logging.error(f"Ошибка при создании модпака: {e}", exc_info=True)
            self.queue.put(("Произошла ошибка при создании модпака.", "red"))

    def update_status(self, message, color):
        self.status_label.config(text=f"{message}", fg=color)

    def process_queue(self):
        try:
            while True:
                message, color = self.queue.get_nowait()
                self.update_status(f"Статус: {message}", color)
        except queue.Empty:
            pass
        self.master.after(100, self.process_queue)

interactive_interface.py
# interactive_interface.py
import cmd2
from cmd2 import Cmd, with_argparser, with_category
import argparse
from mod_downloader import search_mods, download_mod, create_readme
from ai_module import AIModule
from user_preferences import update_preferences
from utils import load_config
import logging
import os

class InteractiveInterface(Cmd):
    intro = "Добро пожаловать в конструктор модпаков Minecraft. Введите 'help' или '?' для списка команд.\n"
    prompt = "(modpack-builder) "

    def __init__(self):
        super().__init__()
        self.description = ""
        self.keywords = []
        self.mods = []
        self.config = load_config()
        self.ai = AIModule()

    def do_set_description(self, statement):
        """Установить описание модпака: set_description <описание>"""
        logging.info(f"Тип 'statement' до преобразования: {type(statement)}")
        if hasattr(statement, 'arg') and isinstance(statement.arg, str):
            description = statement.arg
        else:
            description = str(statement)
        logging.info(f"Тип 'description' после преобразования: {type(description)}")
        logging.info(f"Описание модпака установлено: {description}")

        self.description = description
        self.keywords = self.ai.analyze_description(self.description)

        if self.keywords:
            logging.info(f"Извлеченные ключевые слова: {self.keywords}")
            print(f"Ключевые слова: {', '.join(self.keywords)}")
        else:
            logging.warning("Не удалось извлечь ключевые слова.")

    def do_search_mods(self, arg):
        """Поиск модов по текущим ключевым словам: search_mods"""
        if not self.keywords:
            print("Сначала установите описание модпака с помощью команды 'set_description'.")
            return
        self.mods = search_mods(self.keywords)
        if self.mods:
            logging.info(f"Найдено {len(self.mods)} модов.")
            for idx, mod in enumerate(self.mods, 1):
                print(f"{idx}. {mod.get('title', 'Нет названия')} - {mod.get('description', 'Нет описания')}")
        else:
            logging.warning("Не удалось найти подходящие моды.")

    def do_download_mods(self, arg):
        """Скачать найденные моды: download_mods"""
        if not self.mods:
            print("Сначала выполните поиск модов с помощью команды 'search_mods'.")
            return
        for mod in self.mods:
            download_mod(mod)  # Используем дефолтный путь из mod_downloader.py
        create_readme(self.mods)  # README также будет создан в папке модов
        update_preferences(self.keywords)
        logging.info("Моды скачаны и README создан.")

    def do_confirm_pack(self, arg):
        """Подтвердить сборку модпака: confirm_pack"""
        if not self.mods:
            print("Сначала выполните поиск и скачивание модов.")
            return
        confirmation = self.select("Подтвердить сборку модпака?", ["да", "нет"])
        if confirmation == "да":
            logging.info("Модпак успешно создан!")
            print("Модпак успешно создан!")
        else:
            logging.info("Сборка модпака отменена.")
            print("Сборка модпака отменена.")

    def do_add_mod(self, arg):
        """Добавить мод по имени: add_mod <имя_мода>"""
        # Реализуйте логику добавления мода по имени
        print("Команда 'add_mod' пока не реализована.")

    def do_remove_mod(self, arg):
        """Удалить мод по номеру: remove_mod <номер>"""
        try:
            mod_num = int(arg) - 1
            if 0 <= mod_num < len(self.mods):
                removed_mod = self.mods.pop(mod_num)
                logging.info(f"Мод удален: {removed_mod.get('title')}")
                print(f"Мод '{removed_mod.get('title')}' удален.")
            else:
                print("Неверный номер мода.")
        except ValueError:
            print("Пожалуйста, введите число.")

    def do_list_mods(self, arg):
        """Список всех скачанных модов: list_mods"""
        if not self.mods:
            print("Нет скачанных модов.")
            return
        for idx, mod in enumerate(self.mods, 1):
            print(f"{idx}. {mod.get('title', 'Нет названия')} - {mod.get('description', 'Нет описания')}")

    def do_exit(self, arg):
        """Выйти из программы: exit"""
        print("Выход из конструктора модпаков.")
        return True

    def do_EOF(self, arg):
        """Завершить программу с помощью Ctrl+D (Unix) или Ctrl+Z (Windows)"""
        print("\nВыход из конструктора модпаков.")
        return True

    def exit_program(self):
        print("Выход из конструктора модпаков.")
        self.do_exit(None)

main.py
# main.py
import logging
from interactive_interface import InteractiveInterface
from gui_interface import ModpackBuilderGUI
from utils import load_config
import argparse
import tkinter as tk
import os

def setup_logging():
    config = load_config()
    log_level_str = config.get('log_level', 'INFO').upper()
    log_level = getattr(logging, log_level_str, logging.INFO)

    log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
    os.makedirs(log_dir, exist_ok=True)
    log_file = os.path.join(log_dir, 'modpack_builder.log')

    logging.basicConfig(
        filename=log_file,
        level=log_level,
        format='%(asctime)s:%(levelname)s:%(message)s',
        filemode='a'
    )
    console = logging.StreamHandler()
    console.setLevel(log_level)
    formatter = logging.Formatter('%(levelname)s:%(message)s')
    console.setFormatter(formatter)
    logging.getLogger('').addHandler(console)

def choose_interface(use_gui):
    if use_gui:
        root = tk.Tk()
        app = ModpackBuilderGUI(root)
        root.mainloop()
    else:
        interface = InteractiveInterface()
        interface.cmdloop()

def main():
    setup_logging()
    parser = argparse.ArgumentParser(description="Конструктор Модпаков Minecraft")
    parser.add_argument('--gui', action='store_true', help='Использовать графический интерфейс')
    parser.add_argument('--config', type=str, default='config.yaml', help='Путь к файлу конфигурации')
    args = parser.parse_args()

    try:
        choose_interface(args.gui)
    except Exception as e:
        logging.error(f"Произошла непредвиденная ошибка: {e}", exc_info=True)

if __name__ == "__main__":
    main()

mod_downloader.py
# mod_downloader.py
import os
import requests
from utils import load_config
import logging
import concurrent.futures

def get_mods_directory():
    config = load_config()
    # Используем путь из config.yaml или дефолтный 'mods'
    mods_dir = config.get('mods_directory', 'mods')
    # Получаем абсолютный путь к 'mods' папке относительно директории проекта
    script_dir = os.path.dirname(os.path.abspath(__file__))
    project_dir = os.path.dirname(script_dir)
    mods_path = os.path.join(project_dir, mods_dir)
    os.makedirs(mods_path, exist_ok=True)
    logging.info(f"Моды будут сохранены в: {mods_path}")
    return mods_path

def search_mods(keywords):
    config = load_config()
    query = " ".join(keywords)
    url = f"{config['modrinth_api_url']}/search"
    params = {
        'query': query,
        'limit': 20,  # Увеличено количество для лучшей сортировки
        'version': config['minecraft_version'],
        'loader': config['platform'],
        'facets': ['categories:utilities', 'categories:magic', 'categories:adventure']  # Пример категорий
    }
    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        hits = response.json().get('hits', [])
        # Сортировка модов по количеству загрузок
        sorted_hits = sorted(hits, key=lambda x: x.get('downloads', 0), reverse=True)
        # Удаление дубликатов по слагу
        seen_slugs = set()
        unique_mods = []
        for mod in sorted_hits:
            slug = mod.get('slug')
            if slug and slug not in seen_slugs:
                unique_mods.append(mod)
                seen_slugs.add(slug)
            if len(unique_mods) >= 10:
                break
        logging.info(f"Возвращено {len(unique_mods)} уникальных и популярных модов.")
        return unique_mods
    except requests.RequestException as e:
        logging.error(f"Ошибка при поиске модов: {e}")
        return []

def download_mod(mod, download_path=None):
    if download_path is None:
        download_path = get_mods_directory()
    else:
        download_path = os.path.abspath(download_path)
    os.makedirs(download_path, exist_ok=True)
    files = mod.get('files', [])
    if files:
        file_url = files[0].get('url')
        file_name = files[0].get('filename')
        try:
            response = requests.get(file_url, stream=True)
            response.raise_for_status()
            with open(os.path.join(download_path, file_name), 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            logging.info(f"Скачан мод: {file_name}")
        except requests.RequestException as e:
            logging.error(f"Не удалось скачать мод {file_name}: {e}")
    else:
        logging.warning(f"Файлы для мода {mod.get('slug', 'unknown')} не найдены.")

def download_mods_parallel(mods, download_path=None, max_workers=5):
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [executor.submit(download_mod, mod, download_path) for mod in mods]
        for future in concurrent.futures.as_completed(futures):
            try:
                future.result()
            except Exception as e:
                logging.error(f"Ошибка при скачивании мода: {e}")

def create_readme(mods, path=None):
    if path is None:
        path = get_mods_directory()
    else:
        path = os.path.abspath(path)
    readme_path = os.path.join(path, 'README.txt')
    try:
        with open(readme_path, 'w', encoding='utf-8') as f:
            f.write("Список модов:\n\n")
            for mod in mods:
                f.write(f"Название: {mod.get('title', 'Нет названия')}\n")
                f.write(f"Описание: {mod.get('description', 'Нет описания')}\n")
                f.write(f"Скачано: {mod.get('downloads', 'N/A')} раз(а)\n")
                f.write(f"Ссылка: {mod.get('url', 'Нет ссылки')}\n\n")
        logging.info("Файл README.txt создан успешно.")
    except Exception as e:
        logging.error(f"Не удалось создать README.txt: {e}")
README.md

requirements.txt
# requirements.txt
requests==2.31.0
PyYAML==6.0
transformers==4.30.0
torch==2.0.0
spacy==3.5.0
cmd2==2.4.0
deep-translator==1.8.1

user_preferences.py
# user_preferences.py
import json
import os
import platform

def get_preferences_path():
    if platform.system() == 'Windows':
        appdata = os.getenv('APPDATA')
        return os.path.join(appdata, 'minecraft_modpack_builder', 'user_preferences.json')
    else:
        home = os.path.expanduser('~')
        return os.path.join(home, '.minecraft_modpack_builder', 'user_preferences.json')

def load_preferences():
    preferences_path = get_preferences_path()
    if os.path.exists(preferences_path):
        with open(preferences_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {}

def save_preferences(preferences):
    preferences_path = get_preferences_path()
    os.makedirs(os.path.dirname(preferences_path), exist_ok=True)
    with open(preferences_path, 'w', encoding='utf-8') as f:
        json.dump(preferences, f, ensure_ascii=False, indent=4)

def update_preferences(keywords):
    preferences = load_preferences()
    for keyword in keywords:
        preferences[keyword] = preferences.get(keyword, 0) + 1
    save_preferences(preferences)

utils.py
# utils.py
import yaml
import os
import logging

_cached_config = None

def load_config(config_path='config.yaml'):
    global _cached_config
    if _cached_config:
        return _cached_config
    if not os.path.exists(config_path):
        logging.error(f"Configuration file {config_path} not found.")
        raise FileNotFoundError(f"Configuration file {config_path} not found.")
    with open(config_path, 'r', encoding='utf-8') as file:
        try:
            config = yaml.safe_load(file)
            # Валидация необходимых полей
            required_fields = ['minecraft_version', 'platform', 'modrinth_api_url', 'gpt_model_name', 'max_tokens', 'log_level', 'mods_directory']
            for field in required_fields:
                if field not in config:
                    logging.error(f"Missing required configuration field: {field}")
                    raise KeyError(f"Missing required configuration field: {field}")
            _cached_config = config
            return _cached_config
        except yaml.YAMLError as e:
            logging.error(f"Error parsing configuration file: {e}")
            raise e