Воркед

This commit is contained in:
kromskii2 committed 2025-03-15 09:36:28 +07:00
1 parent ac32c55ab9
commit ee94d8e529
1138 files changed
+55771 -77901

No files matched your search

Binary file not shown.
Binary file not shown.
@@ -0,0 +1,71 @@
from enum import Enum, auto
from typing import List, Dict
class NegativePromptCategory(Enum):
"""Категории негативных промптов"""
QUALITY = auto()
ARTIFACTS = auto()
COMPOSITION = auto()
ANATOMY = auto()
STYLE = auto()
class NegativePromptData:
"""Класс для хранения данных о негативном промпте"""
def __init__(self, text: str, description: str = "", weight: float = 1.0):
self.text = text
self.description = description
self.weight = weight
class NegativePromptCategories:
"""Класс для работы с категориями негативных промптов"""
@staticmethod
def get_prompts() -> Dict[NegativePromptCategory, List[NegativePromptData]]:
"""Возвращает словарь с негативными промптами по категориям"""
return {
NegativePromptCategory.QUALITY: [
NegativePromptData("low quality", "Низкое качество"),
NegativePromptData("blurry", "Размытое изображение"),
NegativePromptData("pixelated", "Пикселизация"),
NegativePromptData("jpeg artifacts", "JPEG артефакты"),
NegativePromptData("compression artifacts", "Артефакты сжатия"),
],
NegativePromptCategory.ARTIFACTS: [
NegativePromptData("watermark", "Водяные знаки"),
NegativePromptData("signature", "Подписи"),
NegativePromptData("text", "Текст на изображении"),
NegativePromptData("frame", "Рамки"),
NegativePromptData("duplicate", "Дубликаты элементов"),
],
NegativePromptCategory.COMPOSITION: [
NegativePromptData("bad composition", "Плохая композиция"),
NegativePromptData("unbalanced", "Несбалансированная композиция"),
NegativePromptData("cropped", "Обрезанное изображение"),
NegativePromptData("out of frame", "Выход за пределы кадра"),
],
NegativePromptCategory.ANATOMY: [
NegativePromptData("bad anatomy", "Неправильная анатомия"),
NegativePromptData("extra limbs", "Лишние конечности"),
NegativePromptData("missing limbs", "Отсутствующие конечности"),
NegativePromptData("deformed", "Деформации"),
NegativePromptData("mutated", "Мутации"),
],
NegativePromptCategory.STYLE: [
NegativePromptData("3d render", "3D рендер"),
NegativePromptData("cartoon", "Мультяшный стиль"),
NegativePromptData("painting", "Живопись"),
NegativePromptData("sketch", "Набросок"),
NegativePromptData("anime", "Аниме"),
],
}
@staticmethod
def get_category_name(category: NegativePromptCategory) -> str:
"""Возвращает название категории на русском"""
return {
NegativePromptCategory.QUALITY: "Качество",
NegativePromptCategory.ARTIFACTS: "Артефакты",
NegativePromptCategory.COMPOSITION: "Композиция",
NegativePromptCategory.ANATOMY: "Анатомия",
NegativePromptCategory.STYLE: "Стиль",
}[category]
+86
View File
@@ -0,0 +1,86 @@
from enum import Enum, auto
from typing import List, Dict
class PromptCategory(Enum):
"""Категории промптов"""
STYLE = auto()
LIGHTING = auto()
COMPOSITION = auto()
CAMERA = auto()
COLORS = auto()
MOOD = auto()
QUALITY = auto()
class PromptData:
"""Класс для хранения данных о промпте"""
def __init__(self, text: str, description: str = "", weight: float = 1.0):
self.text = text
self.description = description
self.weight = weight
class PromptCategories:
"""Класс для работы с категориями промптов"""
@staticmethod
def get_prompts() -> Dict[PromptCategory, List[PromptData]]:
"""Возвращает словарь с промптами по категориям"""
return {
PromptCategory.STYLE: [
PromptData("digital art", "Цифровое искусство"),
PromptData("oil painting", "Масляная живопись"),
PromptData("watercolor", "Акварель"),
PromptData("pencil sketch", "Карандашный набросок"),
PromptData("anime style", "Аниме стиль"),
PromptData("photorealistic", "Фотореалистичный"),
],
PromptCategory.LIGHTING: [
PromptData("soft lighting", "Мягкое освещение"),
PromptData("dramatic lighting", "Драматическое освещение"),
PromptData("cinematic lighting", "Кинематографическое освещение"),
PromptData("natural light", "Естественное освещение"),
PromptData("studio lighting", "Студийное освещение"),
],
PromptCategory.COMPOSITION: [
PromptData("rule of thirds", "Правило третей"),
PromptData("symmetrical", "Симметричная композиция"),
PromptData("centered composition", "Центральная композиция"),
PromptData("dynamic pose", "Динамичная поза"),
],
PromptCategory.CAMERA: [
PromptData("close-up shot", "Крупный план"),
PromptData("wide angle", "Широкий угол"),
PromptData("bokeh", "Боке"),
PromptData("depth of field", "Глубина резкости"),
],
PromptCategory.COLORS: [
PromptData("vibrant colors", "Яркие цвета"),
PromptData("pastel colors", "Пастельные цвета"),
PromptData("monochrome", "Монохром"),
PromptData("high contrast", "Высокий контраст"),
],
PromptCategory.MOOD: [
PromptData("peaceful", "Умиротворенное"),
PromptData("mysterious", "Таинственное"),
PromptData("energetic", "Энергичное"),
PromptData("melancholic", "Меланхоличное"),
],
PromptCategory.QUALITY: [
PromptData("highly detailed", "Высокая детализация"),
PromptData("8k uhd", "Высокое разрешение"),
PromptData("masterpiece", "Шедевр"),
PromptData("best quality", "Лучшее качество"),
],
}
@staticmethod
def get_category_name(category: PromptCategory) -> str:
"""Возвращает название категории на русском"""
return {
PromptCategory.STYLE: "Стили",
PromptCategory.LIGHTING: "Освещение",
PromptCategory.COMPOSITION: "Композиция",
PromptCategory.CAMERA: "Камера",
PromptCategory.COLORS: "Цвета",
PromptCategory.MOOD: "Настроение",
PromptCategory.QUALITY: "Качество",
}[category]
+86
View File
@@ -0,0 +1,86 @@
from typing import Dict
class PromptTranslations:
"""Класс для работы с переводами промптов"""
@staticmethod
def get_translations() -> Dict[str, str]:
"""Возвращает словарь с переводами промптов"""
return {
# Стили
"digital art": "цифровое искусство",
"oil painting": "масляная живопись",
"watercolor": "акварель",
"pencil sketch": "карандашный набросок",
"anime style": "аниме стиль",
"photorealistic": "фотореалистичный",
# Освещение
"soft lighting": "мягкое освещение",
"dramatic lighting": "драматическое освещение",
"cinematic lighting": "кинематографическое освещение",
"natural light": "естественное освещение",
"studio lighting": "студийное освещение",
# Композиция
"rule of thirds": "правило третей",
"symmetrical": "симметричный",
"centered composition": "центральная композиция",
"dynamic pose": "динамичная поза",
# Камера
"close-up shot": "крупный план",
"wide angle": "широкий угол",
"bokeh": "боке",
"depth of field": "глубина резкости",
# Цвета
"vibrant colors": "яркие цвета",
"pastel colors": "пастельные цвета",
"monochrome": "монохром",
"high contrast": "высокий контраст",
# Настроение
"peaceful": "умиротворенный",
"mysterious": "таинственный",
"energetic": "энергичный",
"melancholic": "меланхоличный",
# Качество
"highly detailed": "высокая детализация",
"8k uhd": "8k uhd",
"masterpiece": "шедевр",
"best quality": "лучшее качество",
# Негативные промпты
"low quality": "низкое качество",
"blurry": "размытый",
"pixelated": "пикселизированный",
"jpeg artifacts": "jpeg артефакты",
"compression artifacts": "артефакты сжатия",
"watermark": "водяной знак",
"signature": "подпись",
"text": "текст",
"frame": "рамка",
"duplicate": "дубликат",
"bad composition": "плохая композиция",
"unbalanced": "несбалансированный",
"cropped": "обрезанный",
"out of frame": "за пределами кадра",
"bad anatomy": "плохая анатомия",
"extra limbs": "лишние конечности",
"missing limbs": "отсутствующие конечности",
"deformed": "деформированный",
"mutated": "мутированный",
"3d render": "3d рендер",
"cartoon": "мультяшный",
"painting": "живопись",
"sketch": "набросок",
"anime": "аниме",
}
@staticmethod
def translate(text: str) -> str:
"""Переводит промпт на русский язык"""
translations = PromptTranslations.get_translations()
return translations.get(text.lower(), text)
+82 -16
View File
@@ -2,6 +2,17 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QComboBox, QPushButton, QLabel)
from PyQt6.QtCore import pyqtSignal
import requests
from typing import List, Dict
import os
from dotenv import load_dotenv
# Загружаем переменные окружения
load_dotenv()
class Model:
def __init__(self, name: str, title: str = None):
self.name = name
self.title = title or name
class ModelManager(QWidget):
model_changed = pyqtSignal(str)
@@ -9,6 +20,8 @@ class ModelManager(QWidget):
def __init__(self, api_url, parent=None):
super().__init__(parent)
self.api_url = api_url
self.models = []
self.current_model = None
self.setup_ui()
self.load_models()
@@ -97,33 +110,86 @@ class ModelManager(QWidget):
try:
response = requests.get(f"{self.api_url}/sdapi/v1/sd-models")
if response.status_code == 200:
models = response.json()
current_model = self.model_combo.currentText()
models_data = response.json()
self.models = [
Model(model.get('title', ''), model.get('model_name', ''))
for model in models_data
]
# Получаем текущую модель
options_response = requests.get(f"{self.api_url}/sdapi/v1/options")
if options_response.status_code == 200:
current_model_name = options_response.json().get('sd_model_checkpoint')
self.current_model = next(
(model for model in self.models if model.name == current_model_name),
self.models[0] if self.models else None
)
# Обновляем UI
self.model_combo.clear()
for model in models:
self.model_combo.addItem(model.get('title', ''))
for model in self.models:
self.model_combo.addItem(model.title)
# Устанавливаем текущую модель
if self.current_model:
index = self.model_combo.findText(self.current_model.title)
if index >= 0:
self.model_combo.setCurrentIndex(index)
# Восстанавливаем выбранную модель, если она всё ещё доступна
index = self.model_combo.findText(current_model)
if index >= 0:
self.model_combo.setCurrentIndex(index)
except Exception as e:
print(f"Ошибка при загрузке моделей: {str(e)}")
self.models = []
self.current_model = None
def on_model_changed(self, model_name):
def on_model_changed(self, model_title):
"""Обработка смены модели"""
model = next(
(m for m in self.models if m.title == model_title),
None
)
if model:
try:
response = requests.post(
f"{self.api_url}/sdapi/v1/options",
json={"sd_model_checkpoint": model.name}
)
if response.status_code == 200:
self.current_model = model
self.model_changed.emit(model.name)
except Exception as e:
print(f"Ошибка при смене модели: {str(e)}")
def update_api_url(self, new_url):
"""Обновить URL API"""
self.api_url = new_url
self.load_models()
def get_models(self):
"""Возвращает список доступных моделей"""
return self.models
def get_model_names(self):
"""Возвращает список имен моделей"""
return [model.name for model in self.models]
def get_current_model(self):
"""Возвращает текущую модель"""
return self.current_model
def switch_model(self, model_name: str) -> bool:
"""Переключение на другую модель"""
try:
response = requests.post(
f"{self.api_url}/sdapi/v1/options",
json={"sd_model_checkpoint": model_name}
)
if response.status_code == 200:
self.model_changed.emit(model_name)
self.current_model = next(
(model for model in self.models if model.name == model_name),
None
)
return True
return False
except Exception as e:
print(f"Ошибка при смене модели: {str(e)}")
def update_api_url(self, new_url):
"""Обновить URL API"""
self.api_url = new_url
self.load_models()
print(f"Ошибка переключения модели: {str(e)}")
return False
Binary file not shown.
+210 -168
View File
@@ -1,195 +1,237 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QTextEdit,
QSpinBox, QDoubleSpinBox, QComboBox)
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QSpinBox,
QComboBox, QProgressBar)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QPixmap
import requests
from ..styles.style_constants import StyleConstants
from ..styles.icons import IconManager, IconType
from ..styles.page import BasePage
from ..styles.theme import Colors, Spacing, Fonts
from ..styles.theme_manager import ThemeManager
class HomePage(BasePage):
def __init__(self, parent=None):
def __init__(self, model_manager, sampler_manager, size_preset_manager, parent=None):
super().__init__(parent)
self.theme_manager = ThemeManager.get_instance()
self.setup_content()
self.model_manager = model_manager
self.sampler_manager = sampler_manager
self.size_preset_manager = size_preset_manager
self.icon_manager = IconManager.get_instance()
self.setup_ui()
def setup_content(self):
"""Настройка содержимого страницы"""
colors = self.theme_manager.current_theme.get_colors()
spacing = self.theme_manager.current_theme.get_spacing()
def setup_ui(self):
# Верхняя панель с настройками
settings_panel = QHBoxLayout()
settings_panel.setContentsMargins(
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
settings_panel.setSpacing(StyleConstants.SPACING['md'])
# Заголовок
header = QLabel("Генерация изображений")
header.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 18px;
font-weight: bold;
}}
""")
self.layout.addWidget(header)
# Выбор модели
model_label = QLabel("Модель:")
settings_panel.addWidget(model_label)
# Промпт
prompt_layout = QVBoxLayout()
self.model_combo = QComboBox()
self.model_combo.currentTextChanged.connect(self.on_model_changed)
settings_panel.addWidget(self.model_combo)
# Выбор сэмплера
sampler_label = QLabel("Сэмплер:")
settings_panel.addWidget(sampler_label)
self.sampler_combo = QComboBox()
self.sampler_combo.currentTextChanged.connect(self.on_sampler_changed)
settings_panel.addWidget(self.sampler_combo)
# Настройки размера
size_label = QLabel("Размер:")
settings_panel.addWidget(size_label)
self.width_spin = QSpinBox()
self.width_spin.setRange(64, 2048)
self.width_spin.setSingleStep(64)
self.width_spin.setValue(512)
settings_panel.addWidget(self.width_spin)
self.height_spin = QSpinBox()
self.height_spin.setRange(64, 2048)
self.height_spin.setSingleStep(64)
self.height_spin.setValue(512)
settings_panel.addWidget(self.height_spin)
settings_panel.addStretch()
self.layout.addLayout(settings_panel)
# Панель промптов
prompts_panel = QVBoxLayout()
prompts_panel.setContentsMargins(
StyleConstants.SPACING['lg'],
0,
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
prompts_panel.setSpacing(StyleConstants.SPACING['md'])
# Позитивный промпт
prompt_label = QLabel("Промпт:")
prompt_label.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
}}
""")
self.prompt_input = QTextEdit()
self.prompt_input.setPlaceholderText("Опишите изображение, которое хотите сгенерировать...")
self.prompt_input.setMinimumHeight(100)
prompts_panel.addWidget(prompt_label)
prompt_layout.addWidget(prompt_label)
prompt_layout.addWidget(self.prompt_input)
self.layout.addLayout(prompt_layout)
self.prompt_edit = QLineEdit()
self.prompt_edit.setPlaceholderText("Опишите желаемое изображение...")
prompts_panel.addWidget(self.prompt_edit)
# Негативный промпт
negative_prompt_layout = QVBoxLayout()
negative_prompt_label = QLabel("Негативный промпт:")
negative_prompt_label.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
}}
""")
self.negative_prompt_input = QTextEdit()
self.negative_prompt_input.setPlaceholderText("Опишите, что не должно появиться на изображении...")
self.negative_prompt_input.setMinimumHeight(100)
prompts_panel.addWidget(negative_prompt_label)
negative_prompt_layout.addWidget(negative_prompt_label)
negative_prompt_layout.addWidget(self.negative_prompt_input)
self.layout.addLayout(negative_prompt_layout)
self.negative_prompt_edit = QLineEdit()
self.negative_prompt_edit.setPlaceholderText("Опишите нежелательные элементы...")
prompts_panel.addWidget(self.negative_prompt_edit)
# Параметры генерации
params_layout = QHBoxLayout()
self.layout.addLayout(prompts_panel)
# Размер изображения
size_layout = QVBoxLayout()
size_label = QLabel("Размер:")
size_label.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
}}
""")
self.size_combo = QComboBox()
self.size_combo.addItems(["512x512", "768x768", "1024x1024"])
size_layout.addWidget(size_label)
size_layout.addWidget(self.size_combo)
params_layout.addLayout(size_layout)
# Панель генерации
generate_panel = QHBoxLayout()
generate_panel.setContentsMargins(
StyleConstants.SPACING['lg'],
0,
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
generate_panel.setSpacing(StyleConstants.SPACING['md'])
# Количество шагов
steps_layout = QVBoxLayout()
steps_label = QLabel("Шаги:")
steps_label.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
}}
""")
self.steps_spin = QSpinBox()
self.steps_spin.setRange(1, 150)
self.steps_spin.setValue(20)
steps_layout.addWidget(steps_label)
steps_layout.addWidget(self.steps_spin)
params_layout.addLayout(steps_layout)
self.steps_spin.setPrefix("Шаги: ")
generate_panel.addWidget(self.steps_spin)
# CFG Scale
cfg_layout = QVBoxLayout()
cfg_label = QLabel("CFG Scale:")
cfg_label.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
}}
""")
self.cfg_spin = QDoubleSpinBox()
self.cfg_spin.setRange(1.0, 30.0)
self.cfg_spin.setValue(7.0)
self.cfg_spin.setSingleStep(0.5)
cfg_layout.addWidget(cfg_label)
cfg_layout.addWidget(self.cfg_spin)
params_layout.addLayout(cfg_layout)
self.cfg_spin = QSpinBox()
self.cfg_spin.setRange(1, 30)
self.cfg_spin.setValue(7)
self.cfg_spin.setPrefix("CFG: ")
generate_panel.addWidget(self.cfg_spin)
# Сэмплер
sampler_layout = QVBoxLayout()
sampler_label = QLabel("Сэмплер:")
sampler_label.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
}}
""")
self.sampler_combo = QComboBox()
self.sampler_combo.addItems(["Euler a", "Euler", "DPM++ 2M Karras"])
sampler_layout.addWidget(sampler_label)
sampler_layout.addWidget(self.sampler_combo)
params_layout.addLayout(sampler_layout)
self.batch_size_spin = QSpinBox()
self.batch_size_spin.setRange(1, 4)
self.batch_size_spin.setValue(1)
self.batch_size_spin.setPrefix("Количество: ")
generate_panel.addWidget(self.batch_size_spin)
# Добавляем параметры на страницу
self.layout.addLayout(params_layout)
self.generate_btn = QPushButton("Сгенерировать")
self.generate_btn.setIcon(self.icon_manager.get_icon(IconType.GENERATE))
self.generate_btn.clicked.connect(self.generate_images)
generate_panel.addWidget(self.generate_btn)
# Кнопки управления
buttons_layout = QHBoxLayout()
buttons_layout.setSpacing(spacing.MEDIUM)
generate_panel.addStretch()
self.layout.addLayout(generate_panel)
self.generate_button = QPushButton("Сгенерировать")
self.generate_button.setStyleSheet(f"""
QPushButton {{
min-width: 150px;
background-color: {colors['primary']};
color: white;
border: none;
border-radius: {spacing.TINY}px;
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
}}
# Прогресс-бар
progress_layout = QHBoxLayout()
progress_layout.setContentsMargins(
StyleConstants.SPACING['lg'],
0,
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
self.progress_bar = QProgressBar()
self.progress_bar.setTextVisible(True)
self.progress_bar.setFormat("%p% - %v/%m шагов")
progress_layout.addWidget(self.progress_bar)
self.layout.addLayout(progress_layout)
# Область для результатов
result_layout = QVBoxLayout()
result_layout.setContentsMargins(
StyleConstants.SPACING['lg'],
0,
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
self.result_label = QLabel()
self.result_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
result_layout.addWidget(self.result_label)
self.layout.addLayout(result_layout)
self.layout.addStretch()
# Обновляем UI
self.update_ui()
def update_ui(self):
"""Обновление UI после загрузки данных"""
# Обновляем список моделей
self.model_combo.clear()
for model in self.model_manager.get_models():
self.model_combo.addItem(model.title, model.name)
# Устанавливаем текущую модель
current_model = self.model_manager.get_current_model()
if current_model:
index = self.model_combo.findData(current_model.name)
if index >= 0:
self.model_combo.setCurrentIndex(index)
# Обновляем список сэмплеров
self.sampler_combo.clear()
for sampler in self.sampler_manager.get_samplers():
self.sampler_combo.addItem(sampler.title, sampler.name)
# Устанавливаем текущий сэмплер
current_sampler = self.sampler_manager.get_current_sampler()
if current_sampler:
index = self.sampler_combo.findData(current_sampler.name)
if index >= 0:
self.sampler_combo.setCurrentIndex(index)
def on_model_changed(self, model_title):
"""Обработка изменения модели"""
model_name = self.model_combo.currentData()
if model_name:
self.model_manager.switch_model(model_name)
def on_sampler_changed(self, sampler_title):
"""Обработка изменения сэмплера"""
sampler_name = self.sampler_combo.currentData()
if sampler_name:
self.sampler_manager.set_current_sampler(sampler_name)
def set_resolution(self, width: int, height: int):
"""Установка разрешения"""
self.width_spin.setValue(width)
self.height_spin.setValue(height)
def generate_images(self):
"""Генерация изображений"""
try:
# Получаем параметры
params = {
"prompt": self.prompt_edit.text(),
"negative_prompt": self.negative_prompt_edit.text(),
"steps": self.steps_spin.value(),
"cfg_scale": self.cfg_spin.value(),
"batch_size": self.batch_size_spin.value(),
"width": self.width_spin.value(),
"height": self.height_spin.value(),
"sampler_name": self.sampler_combo.currentData(),
}
QPushButton:hover {{
background-color: {colors['hover']};
}}
# Отправляем запрос
response = requests.post(
f"{self.model_manager.api_url}/sdapi/v1/txt2img",
json=params
)
QPushButton:pressed {{
background-color: {colors['active']};
}}
""")
if response.status_code == 200:
result = response.json()
# TODO: Отобразить сгенерированные изображения
print("Изображения успешно сгенерированы")
else:
print(f"Ошибка генерации: {response.status_code}")
self.clear_button = QPushButton("Очистить")
self.clear_button.setStyleSheet(f"""
QPushButton {{
min-width: 150px;
background-color: transparent;
color: {colors['primary']};
border: 1px solid {colors['primary']};
border-radius: {spacing.TINY}px;
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
}}
QPushButton:hover {{
background-color: {colors['hover']};
color: white;
border: none;
}}
QPushButton:pressed {{
background-color: {colors['active']};
color: white;
border: none;
}}
""")
buttons_layout.addWidget(self.generate_button)
buttons_layout.addWidget(self.clear_button)
buttons_layout.addStretch()
self.layout.addLayout(buttons_layout)
# Растягиваем пространство
self.layout.addStretch()
except Exception as e:
print(f"Ошибка при генерации: {str(e)}")
+124 -157
View File
@@ -1,178 +1,145 @@
from PyQt6.QtWidgets import (QLabel, QPushButton, QHBoxLayout,
QVBoxLayout, QLineEdit, QListWidget,
QListWidgetItem, QFrame)
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QLineEdit, QSpinBox,
QComboBox, QProgressBar, QScrollArea, QFrame)
from PyQt6.QtCore import Qt
from modules.styles.page import BasePage
from modules.styles.theme import Colors
from modules.styles.theme_manager import ThemeManager
from ..styles.style_constants import StyleConstants
from ..styles.icons import IconManager, IconType
from ..styles.page import BasePage
class ModelItem(QFrame):
def __init__(self, model_name: str, parent=None):
class ModelCard(QFrame):
"""Карточка модели"""
def __init__(self, model, parent=None):
super().__init__(parent)
self.model_name = model_name
self.theme_manager = ThemeManager.get_instance()
self.model = model
self.setup_ui()
def setup_ui(self):
"""Настройка UI элемента модели"""
colors = self.theme_manager.current_theme.get_colors()
spacing = self.theme_manager.current_theme.get_spacing()
layout = QHBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
# Название модели
name_label = QLabel(self.model_name)
name_label.setStyleSheet(f"color: {colors['text']};")
layout.addWidget(name_label)
# Растягиваем пространство
layout.addStretch()
# Кнопки управления
select_btn = QPushButton("Выбрать")
select_btn.setMinimumWidth(100)
select_btn.setStyleSheet(f"""
QPushButton {{
background-color: {colors['primary']};
color: white;
border: none;
border-radius: {spacing.TINY}px;
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
}}
QPushButton:hover {{
background-color: {colors['hover']};
}}
QPushButton:pressed {{
background-color: {colors['active']};
}}
""")
layout.addWidget(select_btn)
delete_btn = QPushButton("Удалить")
delete_btn.setMinimumWidth(100)
delete_btn.setStyleSheet(f"""
QPushButton {{
background-color: {colors['error']};
color: white;
border: none;
border-radius: {spacing.TINY}px;
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
}}
QPushButton:hover {{
background-color: {colors['hover']};
}}
QPushButton:pressed {{
background-color: {colors['active']};
}}
""")
layout.addWidget(delete_btn)
# Стили
self.setStyleSheet(f"""
QFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: {spacing.TINY}px;
background-color: {StyleConstants.COLORS['dark']['surface']};
border-radius: {StyleConstants.RADIUS['md']}px;
padding: {StyleConstants.SPACING['md']}px;
margin-bottom: {StyleConstants.SPACING['md']}px;
}}
""")
class ModelsPage(BasePage):
def __init__(self, parent=None):
super().__init__(parent)
self.theme_manager = ThemeManager.get_instance()
self.setup_content()
def setup_content(self):
"""Настройка содержимого страницы"""
colors = self.theme_manager.current_theme.get_colors()
spacing = self.theme_manager.current_theme.get_spacing()
# Заголовок
header = QLabel("Модели")
header.setStyleSheet(f"""
QLabel {{
color: {colors['text']};
font-size: 24px;
font-weight: bold;
color: {StyleConstants.COLORS['dark']['text']};
}}
""")
self.layout.addWidget(header)
# Поиск и добавление
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Поиск моделей...")
self.search_input.setStyleSheet(f"""
QLineEdit {{
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
border-radius: {spacing.TINY}px;
padding: {spacing.SMALL}px;
}}
QLineEdit:focus {{
border: 1px solid {colors['primary']};
}}
""")
search_layout.addWidget(self.search_input)
self.add_btn = QPushButton("Добавить модель")
self.add_btn.setMinimumWidth(150)
self.add_btn.setStyleSheet(f"""
QPushButton {{
background-color: {colors['primary']};
background-color: {StyleConstants.COLORS['primary']};
color: white;
border: none;
border-radius: {spacing.TINY}px;
padding: {spacing.SMALL}px {spacing.MEDIUM}px;
border-radius: {StyleConstants.RADIUS['sm']}px;
padding: {StyleConstants.SPACING['sm']}px {StyleConstants.SPACING['md']}px;
}}
QPushButton:hover {{
background-color: {colors['hover']};
}}
QPushButton:pressed {{
background-color: {colors['active']};
}}
""")
search_layout.addWidget(self.add_btn)
self.layout.addLayout(search_layout)
# Список моделей
self.models_list = QListWidget()
self.models_list.setSpacing(8)
self.models_list.setStyleSheet(f"""
QListWidget {{
background-color: transparent;
border: none;
}}
QListWidget::item {{
padding: 0;
background-color: transparent;
background-color: {StyleConstants.COLORS['secondary']};
}}
""")
# Добавляем тестовые модели
test_models = [
"stable-diffusion-v1-5",
"stable-diffusion-v2-1",
"stable-diffusion-xl-base-1.0"
]
layout = QVBoxLayout(self)
layout.setSpacing(StyleConstants.SPACING['sm'])
for model in test_models:
item = QListWidgetItem(self.models_list)
model_widget = ModelItem(model)
item.setSizeHint(model_widget.sizeHint())
self.models_list.addItem(item)
self.models_list.setItemWidget(item, model_widget)
# Название модели
title = QLabel(self.model.title)
title.setStyleSheet(f"""
font-size: {StyleConstants.FONTS['size']['lg']};
font-weight: {StyleConstants.FONTS['weight']['bold']};
""")
layout.addWidget(title)
self.layout.addWidget(self.models_list)
# Имя файла модели
name = QLabel(self.model.name)
name.setStyleSheet(f"""
font-size: {StyleConstants.FONTS['size']['sm']};
color: {StyleConstants.COLORS['dark']['text_secondary']};
""")
layout.addWidget(name)
# Кнопки управления
buttons_layout = QHBoxLayout()
select_btn = QPushButton("Выбрать")
select_btn.setIcon(IconManager.get_instance().get_icon(IconType.SUCCESS))
buttons_layout.addWidget(select_btn)
delete_btn = QPushButton("Удалить")
delete_btn.setIcon(IconManager.get_instance().get_icon(IconType.DELETE))
buttons_layout.addWidget(delete_btn)
buttons_layout.addStretch()
layout.addLayout(buttons_layout)
class ModelsPage(BasePage):
"""Страница управления моделями"""
def __init__(self, model_manager, parent=None):
super().__init__(parent)
self.model_manager = model_manager
self.setup_ui()
def setup_ui(self):
# Верхняя панель
top_panel = QHBoxLayout()
top_panel.setContentsMargins(
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
top_panel.setSpacing(StyleConstants.SPACING['md'])
# Поиск
self.search_edit = QLineEdit()
self.search_edit.setPlaceholderText("Поиск моделей...")
top_panel.addWidget(self.search_edit)
# Кнопка обновления
refresh_btn = QPushButton("Обновить")
refresh_btn.setIcon(IconManager.get_instance().get_icon(IconType.REFRESH))
refresh_btn.clicked.connect(self.update_ui)
top_panel.addWidget(refresh_btn)
# Кнопка добавления
add_btn = QPushButton("Добавить")
add_btn.setIcon(IconManager.get_instance().get_icon(IconType.ADD))
top_panel.addWidget(add_btn)
top_panel.addStretch()
self.layout.addLayout(top_panel)
# Область прокрутки для моделей
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# Контейнер для карточек моделей
self.models_container = QWidget()
self.models_layout = QVBoxLayout(self.models_container)
self.models_layout.setContentsMargins(
StyleConstants.SPACING['lg'],
0,
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
self.models_layout.setSpacing(0)
scroll_area.setWidget(self.models_container)
self.layout.addWidget(scroll_area)
# Обновляем UI
self.update_ui()
def update_ui(self):
"""Обновление списка моделей"""
# Очищаем контейнер
for i in reversed(range(self.models_layout.count())):
widget = self.models_layout.itemAt(i).widget()
if widget:
widget.deleteLater()
# Добавляем карточки моделей
for model in self.model_manager.get_models():
card = ModelCard(model)
self.models_layout.addWidget(card)
# Добавляем растягивающийся элемент в конец
self.models_layout.addStretch()
+46 -19
View File
@@ -2,6 +2,17 @@ from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QComboBox, QLabel)
from PyQt6.QtCore import pyqtSignal
import requests
from typing import List, Dict
import os
from dotenv import load_dotenv
# Загружаем переменные окружения
load_dotenv()
class Sampler:
def __init__(self, name: str, title: str = None):
self.name = name
self.title = title or name
class SamplerManager(QWidget):
sampler_changed = pyqtSignal(str)
@@ -71,28 +82,25 @@ class SamplerManager(QWidget):
layout.addStretch()
def load_samplers(self):
"""Загрузить список доступных сэмплеров"""
"""Загрузка списка сэмплеров с сервера"""
try:
response = requests.get(f"{self.api_url}/sdapi/v1/samplers")
if response.status_code == 200:
samplers = response.json()
current_sampler = self.sampler_combo.currentText()
samplers_data = response.json()
self.samplers = [
Sampler(sampler['name'], sampler.get('title'))
for sampler in samplers_data
]
self.sampler_combo.clear()
for sampler in samplers:
self.sampler_combo.addItem(sampler.get('name', ''))
# Восстанавливаем выбранный сэмплер, если он всё ещё доступен
index = self.sampler_combo.findText(current_sampler)
if index >= 0:
self.sampler_combo.setCurrentIndex(index)
elif self.sampler_combo.count() > 0:
# По умолчанию выбираем Euler a
default_index = self.sampler_combo.findText("Euler a")
if default_index >= 0:
self.sampler_combo.setCurrentIndex(default_index)
# По умолчанию выбираем Euler a
self.current_sampler = next(
(sampler for sampler in self.samplers if sampler.name == "Euler a"),
self.samplers[0] if self.samplers else None
)
except Exception as e:
print(f"Ошибка при загрузке сэмплеров: {str(e)}")
print(f"Ошибка загрузки сэмплеров: {str(e)}")
self.samplers = []
self.current_sampler = None
def on_sampler_changed(self, sampler_name):
"""Обработка смены сэмплера"""
@@ -103,6 +111,25 @@ class SamplerManager(QWidget):
self.api_url = new_url
self.load_samplers()
def get_samplers(self):
"""Возвращает список доступных сэмплеров"""
return self.samplers
def get_sampler_names(self):
"""Возвращает список имен сэмплеров"""
return [sampler.name for sampler in self.samplers]
def get_current_sampler(self):
"""Получить текущий выбранный сэмплер"""
return self.sampler_combo.currentText()
"""Возвращает текущий сэмплер"""
return self.current_sampler
def set_current_sampler(self, sampler_name: str) -> bool:
"""Установка текущего сэмплера"""
sampler = next(
(s for s in self.samplers if s.name == sampler_name),
None
)
if sampler:
self.current_sampler = sampler
return True
return False
+31 -1
View File
@@ -1,6 +1,14 @@
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFrame
from PyQt6.QtCore import Qt, pyqtSignal
from .size_presets import SIZE_PRESETS
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class SizePreset:
name: str
width: int
height: int
class SizePresetManager(QWidget):
size_changed = pyqtSignal(int, int) # Сигнал для изменения размера
@@ -8,6 +16,17 @@ class SizePresetManager(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.presets = [
SizePreset("512x512", 512, 512),
SizePreset("768x768", 768, 768),
SizePreset("1024x1024", 1024, 1024),
SizePreset("512x768", 512, 768),
SizePreset("768x512", 768, 512),
SizePreset("512x1024", 512, 1024),
SizePreset("1024x512", 1024, 512),
SizePreset("768x1024", 768, 1024),
SizePreset("1024x768", 1024, 768),
]
def setup_ui(self):
layout = QVBoxLayout(self)
@@ -90,4 +109,15 @@ class SizePresetManager(QWidget):
def on_preset_clicked(self, width, height):
"""Обработчик нажатия на кнопку пресета"""
self.size_changed.emit(width, height)
self.size_changed.emit(width, height)
def get_preset_names(self) -> List[str]:
"""Возвращает список имен пресетов"""
return [preset.name for preset in self.presets]
def get_preset(self, name: str) -> SizePreset:
"""Возвращает пресет по имени"""
return next(
(preset for preset in self.presets if preset.name == name),
None
)
Binary file not shown.
Binary file not shown.
+40 -35
View File
@@ -5,20 +5,26 @@ import requests
import os
class IconType(Enum):
# Навигация
"""Типы иконок"""
HOME = auto()
MODELS = auto()
SETTINGS = auto()
GENERATE = auto()
ADD = auto()
DELETE = auto()
REFRESH = auto()
SAVE = auto()
CLOSE = auto()
SUCCESS = auto()
# Навигация
MENU = "menu"
BACK = "arrow-back"
FORWARD = "arrow-forward"
# Действия
ADD = "add"
REMOVE = "remove"
EDIT = "edit"
DELETE = "delete"
SAVE = "save"
REFRESH = "refresh"
SEARCH = "search"
# Медиа
@@ -30,13 +36,11 @@ class IconType(Enum):
STOP = "stop"
# Статусы
SUCCESS = "check-circle"
WARNING = "warning"
ERROR = "error"
INFO = "info"
# Интерфейс
CLOSE = "close"
MINIMIZE = "minimize"
MAXIMIZE = "maximize"
RESTORE = "restore"
@@ -46,48 +50,49 @@ class IconType(Enum):
DARK_MODE = "dark-mode"
class IconManager:
"""Менеджер иконок"""
_instance = None
_icons = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(IconManager, cls).__new__(cls)
cls._instance._download_icons()
return cls._instance
def __init__(self):
self.icons = {}
self.load_icons()
@classmethod
def get_instance(cls):
"""Получить экземпляр менеджера иконок (Singleton)"""
if cls._instance is None:
cls._instance = cls()
cls._instance = IconManager()
return cls._instance
def _download_icons(self):
"""Загрузка иконок из интернета"""
def load_icons(self):
"""Загрузка иконок"""
icons_dir = Path("resources/icons")
icons_dir.mkdir(parents=True, exist_ok=True)
icons = {
IconType.HOME: "https://api.iconify.design/mdi/home.svg",
IconType.IMAGE: "https://api.iconify.design/mdi/image.svg",
IconType.SETTINGS: "https://api.iconify.design/mdi/cog.svg"
# Сопоставление типов иконок с файлами
icon_files = {
IconType.HOME: "home.svg",
IconType.MODELS: "models.svg",
IconType.SETTINGS: "settings.svg",
IconType.GENERATE: "generate.svg",
IconType.ADD: "add.svg",
IconType.DELETE: "delete.svg",
IconType.REFRESH: "refresh.svg",
IconType.SAVE: "save.svg",
IconType.CLOSE: "close.svg",
IconType.SUCCESS: "success.svg",
}
for icon_type, url in icons.items():
icon_path = icons_dir / f"{icon_type.name.lower()}.svg"
if not icon_path.exists():
try:
response = requests.get(url)
response.raise_for_status()
with open(icon_path, "wb") as f:
f.write(response.content)
except Exception as e:
print(f"Ошибка при загрузке иконки {icon_type}: {e}")
self._icons[icon_type] = QIcon(str(icon_path))
# Загружаем иконки
for icon_type, filename in icon_files.items():
icon_path = icons_dir / filename
if icon_path.exists():
self.icons[icon_type] = QIcon(str(icon_path))
else:
print(f"Иконка не найдена: {icon_path}")
def get_icon(self, icon_type: IconType) -> QIcon:
"""Получение иконки по типу"""
return self._icons.get(icon_type, QIcon())
"""Получить иконку по типу"""
return self.icons.get(icon_type, QIcon())
def get_themed_icon(self, icon_type: IconType, theme: str) -> QIcon:
"""Получение иконки с учетом темы"""
+45 -18
View File
@@ -2,15 +2,17 @@ from PyQt6.QtWidgets import QWidget, QStackedWidget, QVBoxLayout
from PyQt6.QtCore import QPropertyAnimation, QParallelAnimationGroup, QEasingCurve, Qt, QPoint
from .theme import Theme, Colors
from .theme_manager import ThemeManager
from .style_constants import StyleConstants
class BasePage(QWidget):
"""Базовый класс для страниц"""
def __init__(self, parent=None):
super().__init__(parent)
self.theme_manager = ThemeManager.get_instance()
self.setup_ui()
self.setup_base_ui()
def setup_ui(self):
"""Базовая настройка страницы"""
def setup_base_ui(self):
"""Настройка базового UI"""
colors = self.theme_manager.current_theme.get_colors()
self.setStyleSheet(f"""
QWidget {{
@@ -18,11 +20,19 @@ class BasePage(QWidget):
}}
""")
# Основной layout
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(24, 24, 24, 24)
self.layout.setSpacing(16)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Создаем анимации
self.enter_animation = QPropertyAnimation(self, b"pos")
self.enter_animation.setDuration(250)
self.enter_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
self.exit_animation = QPropertyAnimation(self, b"pos")
self.exit_animation.setDuration(250)
self.exit_animation.setEasingCurve(QEasingCurve.Type.OutCubic)
def enter_animation(self):
"""Анимация появления страницы"""
# Создаем группу анимаций
@@ -78,26 +88,43 @@ class BasePage(QWidget):
self.anim_group.start()
class PageContainer(QStackedWidget):
"""Контейнер для страниц с анимацией переходов"""
def __init__(self, parent=None):
super().__init__(parent)
self.pages = {}
self.current_page = None
def add_page(self, name: str, page: BasePage):
"""Добавление новой страницы"""
"""Добавить страницу"""
self.pages[name] = page
self.addWidget(page)
def show_page(self, name: str):
"""Показ страницы по имени"""
if name in self.pages:
# Запускаем анимацию исчезновения текущей страницы
current_page = self.currentWidget()
if isinstance(current_page, BasePage):
current_page.leave_animation()
"""Показать страницу с анимацией"""
if name not in self.pages:
return
# Показываем новую страницу
new_page = self.pages[name]
self.setCurrentWidget(new_page)
next_page = self.pages[name]
if self.current_page == next_page:
return
# Запускаем анимацию появления новой страницы
new_page.enter_animation()
# Получаем размеры виджета
width = self.width()
# Настраиваем анимации
if self.current_page:
# Анимация выхода текущей страницы
self.current_page.exit_animation.setStartValue(QPoint(0, 0))
self.current_page.exit_animation.setEndValue(QPoint(-width, 0))
self.current_page.exit_animation.start()
# Анимация входа следующей страницы
next_page.move(width, 0)
next_page.enter_animation.setStartValue(QPoint(width, 0))
next_page.enter_animation.setEndValue(QPoint(0, 0))
next_page.enter_animation.start()
# Показываем страницу
self.setCurrentWidget(next_page)
self.current_page = next_page
+181
View File
@@ -0,0 +1,181 @@
from typing import Dict
class StyleConstants:
# Цветовая схема
COLORS = {
'primary': '#7E57C2', # Основной цвет
'secondary': '#B39DDB', # Вторичный цвет
'accent': '#536DFE', # Акцентный цвет
'success': '#66BB6A', # Цвет успеха
'warning': '#FFA726', # Цвет предупреждения
'error': '#EF5350', # Цвет ошибки
# Светлая тема
'light': {
'background': 'qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #FFFFFF, stop:1 #F5F5F5)',
'surface': '#FFFFFF',
'text': '#212121',
'text_secondary': '#757575',
'border': '#E0E0E0',
'hover': '#F5F5F5',
'pressed': '#EEEEEE'
},
# Темная тема
'dark': {
'background': 'qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1A1A1A, stop:1 #2D2D2D)',
'surface': '#2D2D2D',
'text': '#FFFFFF',
'text_secondary': '#B0B0B0',
'border': '#404040',
'hover': '#353535',
'pressed': '#404040'
}
}
# Размеры и отступы (в пикселях)
SPACING = {
'xxs': 4,
'xs': 8,
'sm': 12,
'md': 16,
'lg': 24,
'xl': 32,
'xxl': 48
}
# Радиусы скругления (в пикселях)
RADIUS = {
'sm': 4,
'md': 8,
'lg': 12,
'xl': 16,
'pill': 9999
}
# Тени
SHADOWS = {
'sm': '0 2px 4px rgba(0,0,0,0.1)',
'md': '0 4px 8px rgba(0,0,0,0.1)',
'lg': '0 8px 16px rgba(0,0,0,0.1)'
}
# Анимации
ANIMATIONS = {
'duration': {
'fast': 150,
'normal': 250,
'slow': 350
},
'easing': 'cubic-bezier(0.4, 0, 0.2, 1)'
}
# Шрифты
FONTS = {
'family': {
'primary': 'Inter',
'secondary': 'Roboto'
},
'size': {
'xs': '12px',
'sm': '14px',
'md': '16px',
'lg': '18px',
'xl': '24px',
'xxl': '32px'
},
'weight': {
'regular': '400',
'medium': '500',
'semibold': '600',
'bold': '700'
}
}
@classmethod
def get_base_styles(cls, theme: str = 'light') -> str:
"""Возвращает базовые стили для приложения"""
colors = cls.COLORS[theme]
return f"""
/* Основные стили */
QMainWindow {{
background: {colors['background']};
}}
QWidget {{
color: {colors['text']};
font-family: {cls.FONTS['family']['primary']};
font-size: {cls.FONTS['size']['md']};
}}
/* Кнопки */
QPushButton {{
background-color: {cls.COLORS['primary']};
color: white;
border: none;
border-radius: {cls.RADIUS['md']}px;
padding: {cls.SPACING['sm']}px {cls.SPACING['md']}px;
font-weight: {cls.FONTS['weight']['medium']};
}}
QPushButton:hover {{
background-color: {cls.COLORS['secondary']};
}}
QPushButton:pressed {{
background-color: {cls.COLORS['accent']};
}}
/* Поля ввода */
QLineEdit, QTextEdit {{
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
border-radius: {cls.RADIUS['md']}px;
padding: {cls.SPACING['sm']}px;
selection-background-color: {cls.COLORS['secondary']};
}}
QLineEdit:focus, QTextEdit:focus {{
border-color: {cls.COLORS['primary']};
}}
/* Выпадающие списки */
QComboBox {{
background-color: {colors['surface']};
color: {colors['text']};
border: 1px solid {colors['border']};
border-radius: {cls.RADIUS['md']}px;
padding: {cls.SPACING['sm']}px;
}}
QComboBox:hover {{
border-color: {cls.COLORS['primary']};
}}
/* Полосы прокрутки */
QScrollBar:vertical {{
background-color: {colors['surface']};
width: 12px;
margin: 0;
}}
QScrollBar::handle:vertical {{
background-color: {cls.COLORS['secondary']};
min-height: 20px;
border-radius: {cls.RADIUS['sm']}px;
}}
QScrollBar:horizontal {{
background-color: {colors['surface']};
height: 12px;
margin: 0;
}}
QScrollBar::handle:horizontal {{
background-color: {cls.COLORS['secondary']};
min-width: 20px;
border-radius: {cls.RADIUS['sm']}px;
}}
"""
+91
View File
@@ -0,0 +1,91 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTextEdit, QComboBox)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QTextCursor
from ..styles.style_constants import StyleConstants
from ..styles.icons import IconManager, IconType
class ConsoleWindow(QWidget):
"""Окно для консольного вывода"""
def __init__(self, parent=None):
super().__init__(parent)
self.icon_manager = IconManager.get_instance()
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Консоль")
self.resize(600, 400)
# Основной layout
layout = QVBoxLayout(self)
layout.setSpacing(StyleConstants.SPACING['md'])
layout.setContentsMargins(
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
# Верхняя панель
top_panel = QHBoxLayout()
# Уровень логирования
self.log_level = QComboBox()
self.log_level.addItems(["INFO", "WARNING", "ERROR", "DEBUG"])
top_panel.addWidget(self.log_level)
# Кнопки
clear_btn = QPushButton("Очистить")
clear_btn.setIcon(self.icon_manager.get_icon(IconType.DELETE))
clear_btn.clicked.connect(self.clear_console)
top_panel.addWidget(clear_btn)
save_btn = QPushButton("Сохранить")
save_btn.setIcon(self.icon_manager.get_icon(IconType.SAVE))
top_panel.addWidget(save_btn)
top_panel.addStretch()
layout.addLayout(top_panel)
# Консоль
self.console = QTextEdit()
self.console.setReadOnly(True)
self.console.setStyleSheet(f"""
QTextEdit {{
background-color: {StyleConstants.COLORS['dark']['background']};
color: {StyleConstants.COLORS['dark']['text']};
font-family: 'Consolas', monospace;
font-size: {StyleConstants.FONTS['size']['md']};
padding: {StyleConstants.SPACING['md']}px;
border-radius: {StyleConstants.RADIUS['md']}px;
}}
""")
layout.addWidget(self.console)
# Применяем стили
self.setStyleSheet(StyleConstants.get_base_styles('light'))
def log(self, message: str, level: str = "INFO"):
"""Добавление сообщения в консоль"""
color = {
"INFO": StyleConstants.COLORS['primary'],
"WARNING": StyleConstants.COLORS['warning'],
"ERROR": StyleConstants.COLORS['error'],
"DEBUG": StyleConstants.COLORS['secondary']
}.get(level, StyleConstants.COLORS['primary'])
# Форматируем сообщение
formatted_message = f'<span style="color: {color}">[{level}]</span> {message}<br>'
# Добавляем сообщение в консоль
cursor = self.console.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.End)
cursor.insertHtml(formatted_message)
# Прокручиваем к последнему сообщению
self.console.setTextCursor(cursor)
self.console.ensureCursorVisible()
def clear_console(self):
"""Очистка консоли"""
self.console.clear()
+153
View File
@@ -0,0 +1,153 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTextEdit, QComboBox,
QScrollArea, QFrame, QCheckBox)
from PyQt6.QtCore import Qt, pyqtSignal
from ..styles.style_constants import StyleConstants
from ..styles.icons import IconManager, IconType
class NegativePromptCategory(QFrame):
"""Виджет категории негативных промптов"""
def __init__(self, title: str, prompts: dict, parent=None):
super().__init__(parent)
self.title = title
self.prompts = prompts
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Заголовок категории
title_label = QLabel(self.title)
title_label.setStyleSheet(f"""
font-size: {StyleConstants.FONTS['size']['lg']};
font-weight: {StyleConstants.FONTS['weight']['bold']};
margin-bottom: {StyleConstants.SPACING['sm']};
""")
layout.addWidget(title_label)
# Список промптов с чекбоксами
for prompt, description in self.prompts.items():
prompt_frame = QFrame()
prompt_layout = QVBoxLayout(prompt_frame)
checkbox = QCheckBox(prompt)
checkbox.setStyleSheet(f"""
font-size: {StyleConstants.FONTS['size']['md']};
font-weight: {StyleConstants.FONTS['weight']['medium']};
""")
description_label = QLabel(description)
description_label.setStyleSheet(f"""
color: {StyleConstants.COLORS['light']['text_secondary']};
font-size: {StyleConstants.FONTS['size']['sm']};
""")
description_label.setWordWrap(True)
prompt_layout.addWidget(checkbox)
prompt_layout.addWidget(description_label)
layout.addWidget(prompt_frame)
self.setStyleSheet(f"""
QFrame {{
background-color: {StyleConstants.COLORS['light']['surface']};
border-radius: {StyleConstants.RADIUS['lg']};
padding: {StyleConstants.SPACING['md']};
margin-bottom: {StyleConstants.SPACING['md']};
}}
""")
class NegativePromptWindow(QWidget):
"""Окно для работы с негативными промптами"""
prompts_selected = pyqtSignal(list)
def __init__(self, parent=None):
super().__init__(parent)
self.icon_manager = IconManager.get_instance()
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Негативные промпты")
self.resize(800, 600)
# Основной layout
layout = QVBoxLayout(self)
layout.setSpacing(StyleConstants.SPACING['md'])
layout.setContentsMargins(
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
# Верхняя панель
top_panel = QHBoxLayout()
# Поиск
self.search_box = QComboBox()
self.search_box.setEditable(True)
self.search_box.setPlaceholderText("Поиск негативных промптов...")
self.search_box.setMinimumWidth(300)
top_panel.addWidget(self.search_box)
# Кнопки
select_all_btn = QPushButton("Выбрать все")
select_all_btn.setIcon(self.icon_manager.get_icon(IconType.SUCCESS))
top_panel.addWidget(select_all_btn)
clear_btn = QPushButton("Очистить")
clear_btn.setIcon(self.icon_manager.get_icon(IconType.CLOSE))
top_panel.addWidget(clear_btn)
top_panel.addStretch()
layout.addLayout(top_panel)
# Область прокрутки для категорий
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# Контейнер для категорий
categories_widget = QWidget()
self.categories_layout = QVBoxLayout(categories_widget)
# Добавляем тестовые категории
test_categories = {
"Качество": {
"Низкое качество": "Удаляет артефакты низкого качества и размытость",
"Шум и зернистость": "Убирает нежелательный шум и зернистость",
"Искажения": "Предотвращает искажения пропорций и перспективы"
},
"Артефакты": {
"Двойные элементы": "Убирает дублирование частей изображения",
"Размытые лица": "Улучшает четкость и детализацию лиц",
"Неправильная анатомия": "Корректирует анатомические ошибки"
}
}
for title, prompts in test_categories.items():
category = NegativePromptCategory(title, prompts)
self.categories_layout.addWidget(category)
self.categories_layout.addStretch()
scroll_area.setWidget(categories_widget)
layout.addWidget(scroll_area)
# Нижняя панель
bottom_panel = QHBoxLayout()
apply_btn = QPushButton("Применить")
apply_btn.setIcon(self.icon_manager.get_icon(IconType.SUCCESS))
apply_btn.setStyleSheet(f"""
background-color: {StyleConstants.COLORS['success']};
""")
bottom_panel.addWidget(apply_btn)
cancel_btn = QPushButton("Отмена")
cancel_btn.setIcon(self.icon_manager.get_icon(IconType.CLOSE))
bottom_panel.addWidget(cancel_btn)
bottom_panel.addStretch()
layout.addLayout(bottom_panel)
# Применяем стили
self.setStyleSheet(StyleConstants.get_base_styles('light'))
+117
View File
@@ -0,0 +1,117 @@
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTextEdit, QComboBox,
QScrollArea, QFrame)
from PyQt6.QtCore import Qt, pyqtSignal
from ..styles.style_constants import StyleConstants
from ..styles.icons import IconManager, IconType
class PromptCategory(QFrame):
"""Виджет категории промптов"""
def __init__(self, title: str, prompts: list, parent=None):
super().__init__(parent)
self.title = title
self.prompts = prompts
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Заголовок категории
title_label = QLabel(self.title)
title_label.setStyleSheet(f"""
font-size: {StyleConstants.FONTS['size']['lg']};
font-weight: {StyleConstants.FONTS['weight']['bold']};
margin-bottom: {StyleConstants.SPACING['sm']};
""")
layout.addWidget(title_label)
# Список промптов
for prompt in self.prompts:
btn = QPushButton(prompt)
btn.setStyleSheet(f"""
text-align: left;
padding: {StyleConstants.SPACING['sm']};
margin-bottom: {StyleConstants.SPACING['xs']};
""")
layout.addWidget(btn)
self.setStyleSheet(f"""
QFrame {{
background-color: {StyleConstants.COLORS['light']['surface']};
border-radius: {StyleConstants.RADIUS['lg']};
padding: {StyleConstants.SPACING['md']};
margin-bottom: {StyleConstants.SPACING['md']};
}}
""")
class PromptWindow(QWidget):
"""Окно для работы с промптами"""
prompt_selected = pyqtSignal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.icon_manager = IconManager.get_instance()
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Библиотека промптов")
self.resize(800, 600)
# Основной layout
layout = QVBoxLayout(self)
layout.setSpacing(StyleConstants.SPACING['md'])
layout.setContentsMargins(
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg'],
StyleConstants.SPACING['lg']
)
# Верхняя панель
top_panel = QHBoxLayout()
# Поиск
self.search_box = QComboBox()
self.search_box.setEditable(True)
self.search_box.setPlaceholderText("Поиск промптов...")
self.search_box.setMinimumWidth(300)
top_panel.addWidget(self.search_box)
# Кнопки
add_btn = QPushButton("Добавить промпт")
add_btn.setIcon(self.icon_manager.get_icon(IconType.ADD))
top_panel.addWidget(add_btn)
refresh_btn = QPushButton("Обновить")
refresh_btn.setIcon(self.icon_manager.get_icon(IconType.REFRESH))
top_panel.addWidget(refresh_btn)
top_panel.addStretch()
layout.addLayout(top_panel)
# Область прокрутки для категорий
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
# Контейнер для категорий
categories_widget = QWidget()
self.categories_layout = QVBoxLayout(categories_widget)
# Добавляем тестовые категории
test_categories = {
"Стили": ["Фотореалистичный", "Аниме", "Масляная живопись", "Акварель"],
"Освещение": ["Дневной свет", "Закат", "Ночное освещение", "Студийный свет"],
"Композиция": ["Крупный план", "Панорама", "Портрет", "Пейзаж"]
}
for title, prompts in test_categories.items():
category = PromptCategory(title, prompts)
self.categories_layout.addWidget(category)
self.categories_layout.addStretch()
scroll_area.setWidget(categories_widget)
layout.addWidget(scroll_area)
# Применяем стили
self.setStyleSheet(StyleConstants.get_base_styles('light'))