Initial commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dinlo
2026-05-31 18:45:24 +08:00
commit e6fab5a094
4 changed files with 744 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
# ComfyUI Generator with LAN Ollama
PyQt5 приложение для генерации изображений в ComfyUI с использованием локальных и облачных LLM для улучшения промптов.
## Описание
Графическое приложение для работы с ComfyUI, которое использует языковые модели (Ollama, LM Studio, облачные API) для автоматического улучшения и перевода промптов перед генерацией изображений.
## Возможности
- 🎨 Интеграция с ComfyUI через WebSocket API
- 🤖 Поддержка множества LLM провайдеров:
- Ollama (локально)
- LM Studio (локальная сеть)
- Облачные API (Gemini, DeepSeek, Grok)
- 🌐 Автоматический перевод промптов
- ✨ Улучшение промптов через AI
- 📊 Детальное логирование процесса
- 💾 Сохранение настроек
## Установка
```bash
pip install PyQt5 websocket-client requests Pillow
```
## Конфигурация
Настройки хранятся в `settings.json`:
```json
{
"comfyui": {
"server_address": "192.168.1.118:8188"
},
"ollama": {
"base_url": "http://127.0.0.1:11434/v1"
},
"lmstudio": {
"base_url": "http://192.168.1.118:1234/v1",
"api_key": "lm-studio",
"models": ["luna-ai-llama2"]
},
"cloud_ai": {
"base_url": "https://neuroapi.host/v1",
"api_key": "YOUR_KEY_HERE",
"models": ["gemini-2.5-flash", "deepseek-v3.2"]
}
}
```
## Использование
```bash
python app.py
```
1. Введите промпт на любом языке
2. Выберите модель для улучшения промпта
3. Настройте параметры генерации
4. Нажмите "Генерировать"
## Логи
Детальные логи сохраняются в `app_detailed_log.txt`.
+568
View File
@@ -0,0 +1,568 @@
import sys
import json
import os
import random
import urllib.request
import urllib.parse
import websocket
import requests
import uuid
import io
import logging
import re
import copy
from PIL import Image
from PIL.PngImagePlugin import PngInfo
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QTextEdit, QSpinBox, QComboBox,
QPushButton, QLabel, QFileDialog, QInputDialog, QMessageBox,
QCheckBox, QProgressBar, QFrame, QGroupBox, QSizePolicy)
from PyQt5.QtGui import QPixmap, QImage, QFont, QIcon
from PyQt5.QtCore import Qt, QThread, pyqtSignal
# ================= ЛОГИРОВАНИЕ =================
logger = logging.getLogger("ComfyUIRemote")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s | %(levelname)-8s | %(message)s')
file_handler = logging.FileHandler("app_detailed_log.txt", encoding='utf-8')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# ================= КОНФИГУРАЦИЯ =================
CONFIG_FILE = "settings.json"
DEFAULT_CONFIG = {
"comfyui": {"server_address": "192.168.1.118:8188"},
"lmstudio": {
"base_url": "http://192.168.1.118:1234/v1",
"api_key": "lm-studio",
"models": ["luna-ai-llama2"]
},
"cloud_ai": {
"base_url": "https://neuroapi.host/v1",
"api_key": "ВАШ_КЛЮЧ_ЗДЕСЬ",
"models": ["gemini-2.5-flash", "deepseek-v3.2", "grok-4-fast-reasoning"]
},
"ollama": {
"base_url": "http://127.0.0.1:11434/v1"
}
}
# ================= WORKFLOWS =================
# 1. Базовый Text-to-Image (стандартный изначальный воркфлоу)
WORKFLOW_T2I = {
"39": {"inputs": {"filename_prefix": "ComfyUI", "images": ["35:8", 0]}, "class_type": "SaveImage"},
"35:30": {"inputs": {"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}, "class_type": "CLIPLoader"},
"35:29": {"inputs": {"vae_name": "ae.safetensors"}, "class_type": "VAELoader"},
"35:28": {"inputs": {"unet_name": "SwarmUI_Z-Image-Turbo-FP8Mix.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
"35:27": {"inputs": {"text": "", "clip": ["35:30", 0]}, "class_type": "CLIPTextEncode"},
"35:13": {"inputs": {"width": 1024, "height": 1024, "batch_size": 1}, "class_type": "EmptySD3LatentImage"},
"35:3": {"inputs": {"seed": 0, "steps": 4, "cfg": 1, "sampler_name": "res_multistep", "scheduler": "simple", "denoise": 1, "model": ["35:11", 0], "positive": ["35:27", 0], "negative": ["35:33", 0], "latent_image": ["35:13", 0]}, "class_type": "KSampler"},
"35:11": {"inputs": {"shift": 3, "model": ["35:28", 0]}, "class_type": "ModelSamplingAuraFlow"},
"35:33": {"inputs": {"conditioning": ["35:27", 0]}, "class_type": "ConditioningZeroOut"},
"35:8": {"inputs": {"samples": ["35:3", 0], "vae": ["35:29", 0]}, "class_type": "VAEDecode"}
}
# 2. Image Editing (Qwen-Image-Edit)
WORKFLOW_I2I = {
"60": {"inputs": {"filename_prefix": "ComfyUI", "images": ["102:8", 0]}, "class_type": "SaveImage"},
"78": {"inputs": {"image": "image_qwen_image_edit_input_image.png"}, "class_type": "LoadImage"},
"93": {"inputs": {"upscale_method": "lanczos", "megapixels": 1.5, "resolution_steps": 1, "image": ["78", 0]}, "class_type": "ImageScaleToTotalPixels"},
"102:39": {"inputs": {"vae_name": "qwen_image_vae.safetensors"}, "class_type": "VAELoader"},
"102:77": {"inputs": {"prompt": "", "clip": ["102:38", 0], "vae": ["102:39", 0], "image": ["78", 0]}, "class_type": "TextEncodeQwenImageEdit"},
"102:75": {"inputs": {"strength": 1, "model": ["102:66", 0]}, "class_type": "CFGNorm"},
"102:66": {"inputs": {"shift": 3, "model": ["102:108", 0]}, "class_type": "ModelSamplingAuraFlow"},
"102:8": {"inputs": {"samples": ["102:3", 0], "vae": ["102:39", 0]}, "class_type": "VAEDecode"},
"102:38": {"inputs": {"clip_name": "qwen_2.5_vl_7b_fp8_scaled.safetensors", "type": "qwen_image", "device": "default"}, "class_type": "CLIPLoader"},
"102:76": {"inputs": {"prompt": "", "clip": ["102:38", 0], "vae": ["102:39", 0], "image": ["78", 0]}, "class_type": "TextEncodeQwenImageEdit"},
"102:88": {"inputs": {"pixels": ["78", 0], "vae": ["102:39", 0]}, "class_type": "VAEEncode"},
"102:89": {"inputs": {"lora_name": "Qwen-Image-Edit-Lightning-4steps-V1.0-bf16.safetensors", "strength_model": 1, "model": ["102:37", 0]}, "class_type": "LoraLoaderModelOnly"},
"102:37": {"inputs": {"unet_name": "qwen_image_edit_fp8_e4m3fn.safetensors", "weight_dtype": "default"}, "class_type": "UNETLoader"},
"102:105": {"inputs": {"value": 1}, "class_type": "PrimitiveFloat"},
"102:106": {"inputs": {"value": 20}, "class_type": "PrimitiveInt"},
"102:103": {"inputs": {"value": 4}, "class_type": "PrimitiveInt"},
"102:107": {"inputs": {"value": 2.5}, "class_type": "PrimitiveFloat"},
"102:111": {"inputs": {"value": False}, "class_type": "PrimitiveBoolean"},
"102:3": {"inputs": {"seed": 0, "steps": ["102:110", 0], "cfg": ["102:109", 0], "sampler_name": "euler", "scheduler": "simple", "denoise": 1, "model": ["102:75", 0], "positive": ["102:76", 0], "negative": ["102:77", 0], "latent_image": ["102:88", 0]}, "class_type": "KSampler"},
"102:109": {"inputs": {"switch": ["102:111", 0], "on_false": ["102:107", 0], "on_true": ["102:105", 0]}, "class_type": "ComfySwitchNode"},
"102:110": {"inputs": {"switch": ["102:111", 0], "on_false": ["102:106", 0], "on_true": ["102:103", 0]}, "class_type": "ComfySwitchNode"},
"102:108": {"inputs": {"switch": ["102:111", 0], "on_false": ["102:37", 0], "on_true": ["102:89", 0]}, "class_type": "ComfySwitchNode"}
}
# ================= СТИЛИ (QSS) =================
DARK_STYLE = """
QMainWindow { background-color: #1e1e2e; }
QGroupBox { color: #cdd6f4; font-weight: bold; border: 1px solid #45475a; margin-top: 15px; border-radius: 8px; padding-top: 20px; }
QLabel { color: #bac2de; font-size: 13px; }
QTextEdit { background-color: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 5px; padding: 10px; selection-background-color: #585b70; }
QSpinBox, QComboBox { background-color: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 4px; padding: 5px; min-height: 30px; }
QPushButton { background-color: #45475a; color: #cdd6f4; border-radius: 6px; padding: 10px; font-weight: bold; }
QPushButton:hover { background-color: #585b70; }
QPushButton#start_btn { background-color: #a6e3a1; color: #11111b; font-size: 14px; }
QPushButton#start_btn:hover { background-color: #94e2d5; }
QPushButton#save_btn { background-color: #f38ba8; color: #11111b; }
QProgressBar { border: 1px solid #45475a; border-radius: 5px; text-align: center; color: white; height: 15px; }
QProgressBar::chunk { background-color: #89b4fa; }
QCheckBox { color: #bac2de; font-size: 13px; spacing: 8px; }
"""
# ================= WORKERS =================
class ComfyUIWorker(QThread):
finished_signal = pyqtSignal(QPixmap, bytes)
error_signal = pyqtSignal(str)
progress_signal = pyqtSignal(int, int)
def __init__(self, workflow, server_address, upload_image_path=None, image_node_id=None):
super().__init__()
self.workflow = workflow
self.server_address = server_address
self.client_id = str(uuid.uuid4())
self.upload_image_path = upload_image_path
self.image_node_id = image_node_id
def run(self):
ws = None
try:
# 1. Если задан путь к картинке (I2I Workflow) -> загружаем ее на сервер
if self.upload_image_path and self.image_node_id:
try:
with open(self.upload_image_path, 'rb') as f:
upload_url = f"http://{self.server_address}/upload/image"
files = {'image': f}
req = requests.post(upload_url, files=files)
if req.status_code == 200:
# Получаем реальное имя файла на сервере
filename = req.json()['name']
self.workflow[self.image_node_id]["inputs"]["image"] = filename
else:
self.error_signal.emit("Failed to upload image to ComfyUI.")
return
except Exception as e:
self.error_signal.emit(f"Image upload error: {str(e)}")
return
# 2. Подключаемся к WebSocket и отправляем workflow
ws_url = f"ws://{self.server_address}/ws?clientId={self.client_id}"
ws = websocket.WebSocket()
ws.connect(ws_url)
prompt_url = f"http://{self.server_address}/prompt"
data = json.dumps({"prompt": self.workflow, "client_id": self.client_id}).encode('utf-8')
req = urllib.request.Request(prompt_url, data=data)
with urllib.request.urlopen(req) as response:
prompt_id = json.loads(response.read())['prompt_id']
# Ждем выполнения
while True:
out = ws.recv()
if isinstance(out, str):
message = json.loads(out)
if message['type'] == 'progress':
self.progress_signal.emit(message['data']['value'], message['data']['max'])
if message['type'] == 'executing' and message['data']['node'] is None:
if message['data']['prompt_id'] == prompt_id: break
# Получаем результат
with urllib.request.urlopen(f"http://{self.server_address}/history/{prompt_id}") as response:
history = json.loads(response.read())[prompt_id]
for node_id in history['outputs']:
node_output = history['outputs'][node_id]
if 'images' in node_output:
img_data = node_output['images'][0]
view_url = f"http://{self.server_address}/view?filename={img_data['filename']}&subfolder={img_data['subfolder']}&type={img_data['type']}"
with urllib.request.urlopen(view_url) as img_res:
img_bytes = img_res.read()
self.finished_signal.emit(QPixmap.fromImage(QImage.fromData(img_bytes)), img_bytes)
return
self.error_signal.emit("Generation finished, but no image found in outputs.")
except Exception as e:
self.error_signal.emit(str(e))
finally:
if ws: ws.close()
class LLMWorker(QThread):
finished_signal = pyqtSignal(str)
error_signal = pyqtSignal(str)
def __init__(self, base_url, api_key, model, prompt, provider="default"):
super().__init__()
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.model = model
self.prompt = prompt
self.provider = provider
def run(self):
try:
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
if self.provider == "ollama":
system_instruction = (
"You are a professional image prompt engineer. "
"Task: Translate the user's input into ENGLISH (if it's not already) and expand it into a highly detailed visual prompt. "
"CRITICAL RULES: "
"1. MUST BE IN ENGLISH ONLY. Do NOT output Russian words. "
"2. NO REASONING, NO THOUGHTS. Do not explain your process. "
"3. OUTPUT EXACTLY THE RAW PROMPT. No introductions like 'Here is...' or 'Sure'. "
)
temperature = 0.6
else:
system_instruction = "Improve prompt for AI image generation. Expanded English description only."
temperature = 0.7
payload = {
"model": self.model,
"messages": [
{"role": "system", "content": system_instruction},
{"role": "user", "content": self.prompt}
],
"temperature": temperature
}
response = requests.post(f"{self.base_url}/chat/completions", headers=headers, json=payload, timeout=180)
if response.status_code == 200:
result_text = response.json()['choices'][0]['message']['content'].strip()
result_text = re.sub(r'<think>.*?</think>', '', result_text, flags=re.DOTALL).strip()
if self.provider == "ollama":
if result_text.startswith('"') and result_text.endswith('"'):
result_text = result_text[1:-1].strip()
self.finished_signal.emit(result_text)
else:
self.error_signal.emit(f"Error {response.status_code}: {response.text}")
except requests.exceptions.Timeout:
self.error_signal.emit("Тайм-аут: Сервер слишком долго загружал модель или генерировал ответ. Попробуйте еще раз.")
except Exception as e:
self.error_signal.emit(str(e))
# ================= ГЛАВНОЕ ОКНО =================
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("ComfyUI Studio v3")
self.resize(1280, 850)
self.setStyleSheet(DARK_STYLE)
self.load_config()
self.current_bytes = None
self.selected_image_path = None
self.init_ui()
def load_config(self):
if not os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'w', encoding='utf-8') as f: json.dump(DEFAULT_CONFIG, f, indent=4)
self.config = DEFAULT_CONFIG
else:
try:
with open(CONFIG_FILE, 'r', encoding='utf-8') as f: self.config = json.load(f)
except: self.config = DEFAULT_CONFIG
def init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
main_layout.setSpacing(20)
main_layout.setContentsMargins(15, 15, 15, 15)
left_panel = QFrame()
left_panel.setMinimumWidth(550)
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 0, 0, 0)
# Выбор Workflow
wf_group = QGroupBox("Workflow Selector")
wfg_layout = QVBoxLayout()
self.workflow_cb = QComboBox()
self.workflow_cb.addItems([
"Text-to-Image (Default Turbo)",
"Image Editing (Qwen I2I)"
])
self.workflow_cb.currentIndexChanged.connect(self.on_workflow_changed)
wfg_layout.addWidget(self.workflow_cb)
wf_group.setLayout(wfg_layout)
left_layout.addWidget(wf_group)
# Prompt Input
prompt_group = QGroupBox("Prompt Input")
pg_layout = QVBoxLayout()
self.prompt_edit = QTextEdit()
self.prompt_edit.setPlaceholderText("Describe your image idea here...")
self.prompt_edit.setMinimumHeight(150)
pg_layout.addWidget(self.prompt_edit)
self.neg_check = QCheckBox("Use Negative Prompt")
self.neg_check.toggled.connect(lambda c: self.neg_edit.setVisible(c))
pg_layout.addWidget(self.neg_check)
self.neg_edit = QTextEdit()
self.neg_edit.setPlaceholderText("Describe what to exclude...")
self.neg_edit.setMinimumHeight(80)
self.neg_edit.setVisible(False)
pg_layout.addWidget(self.neg_edit)
prompt_group.setLayout(pg_layout)
left_layout.addWidget(prompt_group, 3)
# Блок Изображения (Только для Image Editing)
self.image_picker_group = QGroupBox("Input Image Settings")
ip_layout = QVBoxLayout()
btn_img_layout = QHBoxLayout()
self.btn_select_image = QPushButton("Select Local Image")
self.btn_select_image.clicked.connect(self.select_input_image)
self.lbl_input_image = QLabel("No image selected")
self.lbl_input_image.setWordWrap(True)
btn_img_layout.addWidget(self.btn_select_image)
btn_img_layout.addWidget(self.lbl_input_image)
self.fast_edit_cb = QCheckBox("Enable 4-Steps LoRA (Fast Edit)")
self.fast_edit_cb.setChecked(True)
ip_layout.addLayout(btn_img_layout)
ip_layout.addWidget(self.fast_edit_cb)
self.image_picker_group.setLayout(ip_layout)
self.image_picker_group.setVisible(False) # Скрыто по умолчанию
left_layout.addWidget(self.image_picker_group)
# Settings Group
settings_group = QGroupBox("Core Parameters")
sg_layout = QVBoxLayout()
row1 = QHBoxLayout()
self.step_s = QSpinBox(); self.step_s.setRange(1, 100); self.step_s.setValue(4)
row1.addWidget(QLabel("Steps:")); row1.addWidget(self.step_s)
self.seed_s = QSpinBox(); self.seed_s.setRange(0, 2147483647); self.seed_s.setValue(random.randint(0, 1000000))
row1.addWidget(QLabel("Seed:")); row1.addWidget(self.seed_s)
sg_layout.addLayout(row1)
row2 = QHBoxLayout()
self.size_cb = QComboBox()
self.size_cb.addItems(["1024x1024", "832x1216", "1216x832", "1920x1080"])
row2.addWidget(QLabel("Resolution:")); row2.addWidget(self.size_cb)
self.font_cb = QComboBox()
self.font_cb.addItems(["10", "12", "14", "16", "18", "20", "24", "26"]); self.font_cb.setCurrentText("14")
self.font_cb.currentIndexChanged.connect(self.update_font)
row2.addWidget(QLabel("Text Size:")); row2.addWidget(self.font_cb)
sg_layout.addLayout(row2)
self.vae_cb = QComboBox()
self.vae_cb.addItem("ae.safetensors")
sg_layout.addWidget(QLabel("VAE Checkpoint (T2I):"))
sg_layout.addWidget(self.vae_cb)
settings_group.setLayout(sg_layout)
left_layout.addWidget(settings_group, 1)
# AI Tools Group
ai_group = QGroupBox("AI Prompt Assistance")
ag_layout = QHBoxLayout()
self.btn_lms = QPushButton("LM Studio")
self.btn_lms.clicked.connect(self.use_lms)
self.btn_ollama = QPushButton("Ollama")
self.btn_ollama.setStyleSheet("color: #fab387;")
self.btn_ollama.clicked.connect(self.use_ollama)
self.btn_ai = QPushButton("NeuroAPI")
self.btn_ai.setStyleSheet("color: #ca9ee6;")
self.btn_ai.clicked.connect(self.use_ai)
ag_layout.addWidget(self.btn_lms)
ag_layout.addWidget(self.btn_ollama)
ag_layout.addWidget(self.btn_ai)
ai_group.setLayout(ag_layout)
left_layout.addWidget(ai_group)
main_layout.addWidget(left_panel, 2)
# ПРАВАЯ ПАНЕЛЬ (ПРОСМОТР)
right_panel = QFrame()
right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(0, 0, 0, 0)
self.img_lbl = QLabel("Image Viewer")
self.img_lbl.setAlignment(Qt.AlignCenter)
self.img_lbl.setStyleSheet("background-color: #11111b; border-radius: 10px; border: 2px solid #313244;")
self.img_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
right_layout.addWidget(self.img_lbl)
self.pbar = QProgressBar()
right_layout.addWidget(self.pbar)
controls = QHBoxLayout()
self.btn_start = QPushButton("▶ START GENERATION")
self.btn_start.setObjectName("start_btn")
self.btn_start.setFixedHeight(55)
self.btn_start.clicked.connect(self.run_comfy)
btn_upd = QPushButton("🔄")
btn_upd.setFixedWidth(60); btn_upd.setFixedHeight(55)
btn_upd.clicked.connect(self.refresh)
btn_sav = QPushButton("💾 SAVE IMAGE")
btn_sav.setObjectName("save_btn")
btn_sav.setFixedHeight(55)
btn_sav.clicked.connect(self.save_img)
controls.addWidget(self.btn_start, 3)
controls.addWidget(btn_upd, 1)
controls.addWidget(btn_sav, 2)
right_layout.addLayout(controls)
main_layout.addWidget(right_panel, 3)
self.update_font()
self.fetch_vaes_from_comfy()
def update_font(self):
f = QFont(); f.setPointSize(int(self.font_cb.currentText()))
self.prompt_edit.setFont(f); self.neg_edit.setFont(f)
def fetch_vaes_from_comfy(self):
server = self.config['comfyui']['server_address']
try:
resp = requests.get(f"http://{server}/object_info/VAELoader", timeout=2)
if resp.status_code == 200:
vaes = resp.json()['VAELoader']['input']['required']['vae_name'][0]
self.vae_cb.clear(); self.vae_cb.addItems(vaes)
except: pass
def on_workflow_changed(self):
idx = self.workflow_cb.currentIndex()
if idx == 1: # Qwen Image Edit (I2I)
self.image_picker_group.setVisible(True)
self.step_s.setEnabled(False) # Регулируется через Fast Mode
self.size_cb.setEnabled(False) # Регулируется через исходное изображение
self.vae_cb.setEnabled(False) # Воркфлоу требует специфичный qwen_image_vae
else: # Text-to-Image Default
self.image_picker_group.setVisible(False)
self.step_s.setEnabled(True)
self.size_cb.setEnabled(True)
self.vae_cb.setEnabled(True)
def select_input_image(self):
path, _ = QFileDialog.getOpenFileName(self, "Select Input Image", "", "Images (*.png *.jpg *.jpeg *.webp)")
if path:
self.selected_image_path = path
self.lbl_input_image.setText(os.path.basename(path))
def run_comfy(self):
txt = self.prompt_edit.toPlainText().strip()
if not txt:
QMessageBox.warning(self, "Warning", "Prompt cannot be empty!")
return
self.btn_start.setEnabled(False)
self.btn_start.setText("G E N E R A T I N G . . .")
idx = self.workflow_cb.currentIndex()
upload_path = None
image_node = None
if idx == 0:
# --- Text to Image ---
wf = copy.deepcopy(WORKFLOW_T2I)
wf["35:27"]["inputs"]["text"] = txt
wf["35:3"]["inputs"]["steps"] = self.step_s.value()
wf["35:3"]["inputs"]["seed"] = self.seed_s.value()
w, h = map(int, self.size_cb.currentText().split('x'))
wf["35:13"]["inputs"]["width"] = w
wf["35:13"]["inputs"]["height"] = h
if self.vae_cb.currentText():
wf["35:29"]["inputs"]["vae_name"] = self.vae_cb.currentText()
elif idx == 1:
# --- Image to Image / Qwen Edit ---
if not self.selected_image_path:
QMessageBox.warning(self, "Warning", "Please select an input image first!")
self.btn_start.setEnabled(True)
self.btn_start.setText("▶ START GENERATION")
return
wf = copy.deepcopy(WORKFLOW_I2I)
wf["102:76"]["inputs"]["prompt"] = txt
neg_txt = self.neg_edit.toPlainText().strip() if self.neg_check.isChecked() else ""
wf["102:77"]["inputs"]["prompt"] = neg_txt
wf["102:3"]["inputs"]["seed"] = self.seed_s.value()
# Включаем или выключаем 4-steps LoRA (True = 4 Steps/CFG 1, False = 20 Steps/CFG 2.5)
wf["102:111"]["inputs"]["value"] = self.fast_edit_cb.isChecked()
upload_path = self.selected_image_path
image_node = "78" # Узел LoadImage в Qwen-воркфлоу
# Передаем параметры в Worker
self.worker = ComfyUIWorker(wf, self.config['comfyui']['server_address'], upload_path, image_node)
self.worker.finished_signal.connect(self.on_comfy_finished)
self.worker.error_signal.connect(self.on_comfy_error)
self.worker.progress_signal.connect(lambda v, m: self.pbar.setValue(int(v/m*100) if m > 0 else 0))
self.worker.start()
def on_comfy_finished(self, pix, raw):
self.current_bytes = raw
self.img_lbl.setPixmap(pix.scaled(self.img_lbl.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation))
self.btn_start.setEnabled(True)
self.btn_start.setText("▶ START GENERATION")
def on_comfy_error(self, err):
QMessageBox.critical(self, "Generation Error", err)
self.btn_start.setEnabled(True)
self.btn_start.setText("▶ START GENERATION")
def refresh(self):
self.seed_s.setValue(random.randint(0, 2147483647))
self.run_comfy()
def save_img(self):
if not self.current_bytes: return
path, _ = QFileDialog.getSaveFileName(self, "Save Image", "generated_art.png", "*.png")
if path:
img = Image.open(io.BytesIO(self.current_bytes))
img.save(path)
def use_lms(self):
models = self.config['lmstudio'].get('models', [])
m, ok = QInputDialog.getItem(self, "LM Studio", "Select Model:", models, 0, False)
if ok: self._run_llm(self.config['lmstudio']['base_url'], self.config['lmstudio']['api_key'], m, "lmstudio")
def use_ollama(self):
url = self.config.get('ollama', {}).get('base_url', 'http://127.0.0.1:11434/v1')
try:
resp = requests.get(f"{url}/models", timeout=3)
if resp.status_code == 200:
models = [m['id'] for m in resp.json().get('data', [])]
else:
raise Exception(f"HTTP Error {resp.status_code}")
except Exception as e:
QMessageBox.warning(self, "Ollama Error", f"Failed to fetch models from Ollama.\nEnsure Ollama is running.\nError: {e}")
return
if not models:
QMessageBox.warning(self, "Ollama Info", "No models found. Please pull a model first.")
return
m, ok = QInputDialog.getItem(self, "Ollama", "Select Model:", models, 0, False)
if ok: self._run_llm(url, "ollama", m, "ollama")
def use_ai(self):
models = self.config['cloud_ai'].get('models', [])
m, ok = QInputDialog.getItem(self, "NeuroAPI", "Select Model:", models, 0, False)
if ok: self._run_llm(self.config['cloud_ai']['base_url'], self.config['cloud_ai']['api_key'], m, "cloud_ai")
def _run_llm(self, url, key, model, provider="default"):
p = self.prompt_edit.toPlainText().strip()
if not p: return
self.prompt_edit.setReadOnly(True)
self.llm_worker = LLMWorker(url, key, model, p, provider)
self.llm_worker.finished_signal.connect(self._on_llm_ok)
self.llm_worker.error_signal.connect(self._on_llm_err)
self.llm_worker.start()
def _on_llm_ok(self, t):
self.prompt_edit.setPlainText(t); self.prompt_edit.setReadOnly(False)
def _on_llm_err(self, e):
QMessageBox.warning(self, "AI Error", e); self.prompt_edit.setReadOnly(False)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setFont(QFont("Segoe UI", 10))
window = MainWindow(); window.show()
sys.exit(app.exec_())
+75
View File
@@ -0,0 +1,75 @@
2026-04-19 12:52:58,876 | INFO | Запуск LLM: Модель=gemini-2.5-flash, Провайдер=https://api.neuroapi.host/v1
2026-04-19 12:52:59,193 | ERROR | Критическая ошибка LLM Worker
Traceback (most recent call last):
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connection.py", line 204, in _new_conn
sock = connection.create_connection(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\util\connection.py", line 60, in create_connection
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\socket.py", line 976, in getaddrinfo
for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
socket.gaierror: [Errno 11001] getaddrinfo failed
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connectionpool.py", line 787, in urlopen
response = self._make_request(
^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connectionpool.py", line 488, in _make_request
raise new_e
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connectionpool.py", line 464, in _make_request
self._validate_conn(conn)
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connectionpool.py", line 1093, in _validate_conn
conn.connect()
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connection.py", line 759, in connect
self.sock = sock = self._new_conn()
^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connection.py", line 211, in _new_conn
raise NameResolutionError(self.host, self, e) from e
urllib3.exceptions.NameResolutionError: HTTPSConnection(host='api.neuroapi.host', port=443): Failed to resolve 'api.neuroapi.host' ([Errno 11001] getaddrinfo failed)
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\requests\adapters.py", line 644, in send
resp = conn.urlopen(
^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\connectionpool.py", line 841, in urlopen
retries = retries.increment(
^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\urllib3\util\retry.py", line 535, in increment
raise MaxRetryError(_pool, url, reason) from reason # type: ignore[arg-type]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='api.neuroapi.host', port=443): Max retries exceeded with url: /v1/chat/completions (Caused by NameResolutionError("HTTPSConnection(host='api.neuroapi.host', port=443): Failed to resolve 'api.neuroapi.host' ([Errno 11001] getaddrinfo failed)"))
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\dimir\proects\comfy-gen-lan\app.py", line 147, in run
response = requests.post(
^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\requests\api.py", line 115, in post
return request("post", url, data=data, json=json, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\requests\api.py", line 59, in request
return session.request(method=method, url=url, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\requests\sessions.py", line 589, in request
resp = self.send(prep, **send_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\requests\sessions.py", line 703, in send
r = adapter.send(request, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\dimir\AppData\Local\Programs\Python\Python312\Lib\site-packages\requests\adapters.py", line 677, in send
raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='api.neuroapi.host', port=443): Max retries exceeded with url: /v1/chat/completions (Caused by NameResolutionError("HTTPSConnection(host='api.neuroapi.host', port=443): Failed to resolve 'api.neuroapi.host' ([Errno 11001] getaddrinfo failed)"))
2026-04-19 12:58:43,108 | INFO | LLM запрос: URL=https://api.neuroapi.host/v1, Model=gemini-2.5-flash
2026-04-19 12:58:43,722 | ERROR | DNS не смог найти api.neuroapi.host. Попытка использовать резервный IP.
2026-04-19 12:59:19,120 | INFO | LLM запрос: URL=https://api.neuroapi.host/v1, Model=gemini-2.5-flash
2026-04-19 12:59:19,207 | ERROR | DNS не смог найти api.neuroapi.host. Попытка использовать резервный IP.
2026-04-19 12:59:38,499 | INFO | LLM запрос: URL=https://api.neuroapi.host/v1, Model=gemini-2.5-flash
2026-04-19 12:59:38,589 | ERROR | DNS не смог найти api.neuroapi.host. Попытка использовать резервный IP.
2026-04-19 13:17:01,504 | INFO | LLM запрос: URL=https://neuroapi.host/v1, Model=deepseek-v3.2
+36
View File
@@ -0,0 +1,36 @@
{
"comfyui": {
"server_address": "192.168.1.118:8000"
},
"lmstudio": {
"base_url": "http://192.168.1.118:1234/v1",
"api_key": "lm-studio",
"models": ["llama-3.2-8x3b-moe-dark-champion-instruct-uncensored-abliterated-18.4b", "gemma-4-e4b-uncensored-hauhaucs-aggressive"]
},
"cloud_ai": {
"base_url": "https://neuroapi.host/v1",
"api_key": "sk-BWsZWXaqUueU3r2U58wbGPsvGKVT9tc87YiLKeVQ5mn4WqHM",
"models": [
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"deepseek-v3.2",
"glm-4.6v-flash",
"glm-4.7",
"grok-4-fast-non-reasoning",
"grok-4-fast-reasoning",
"grok-4-1-fast-reasoning"
],
"proxy": ""
},
"neuroapi_gemini": {
"base_url": "https://neuroapi.host/v1",
"api_key": "sk-BWsZWXaqUueU3r2U58wbGPsvGKVT9tc87YiLKeVQ5mn4WqHM",
"models": [
"gemini-3-pro-image-preview",
"gemini-3.1-flash-image-preview"
]
},
"ollama": {
"base_url": "http://127.0.0.1:11434/v1"
}
}