Update-6.0

This commit is contained in:
Iván Eduardo Chavez Ayub
2026-02-15 02:32:54 -06:00
parent 6f55090593
commit 3021c69edd
26 changed files with 1668 additions and 755 deletions
-116
View File
@@ -1,116 +0,0 @@
SOFTWARE LICENSE AGREEMENT AND TERMS OF USE FOR WARLOCK-STUDIO
---
PREAMBLE
This Software License Agreement ("Agreement") constitutes a legally binding
contract between you, either as an individual or on behalf of an entity
("USER"), and Iván Eduardo Chavez Ayub ("AUTHOR"), regarding the
Warlock-Studio software and all its associated files, documentation,
and materials (collectively, the "SOFTWARE").
By installing, copying, downloading, accessing, or otherwise using the
SOFTWARE, the USER expressly consents to and agrees to be bound by all
terms and conditions stipulated in this Agreement.
IF THE USER DOES NOT AGREE WITH ALL THE TERMS OF THIS AGREEMENT, THEY MUST
NOT INSTALL, USE, OR COPY THE SOFTWARE AND MUST IMMEDIATELY CANCEL THE
INSTALLATION PROCESS.
---
SECTION I: THE MIT LICENSE
The original and legally binding text of the MIT License is presented below.
This text governs the use of the core SOFTWARE.
MIT License
Copyright (c) 2025 Iván Eduardo Chavez Ayub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
SECTION II: ADDITIONAL TERMS, NOTICES, AND ACKNOWLEDGEMENTS
1. Project Description
Warlock-Studio is a software application developed by Iván Eduardo Chavez
Ayub (GitHub profile: @Ivan-Ayub97).
The project is based on open-source
tools such as QualityScaler, FluidFrames, and RealScaler, originally
developed by Djdefrag (GitHub profile: @Djdefrag).
The main objective of
Warlock-Studio is to provide an intuitive graphical interface for
enhancing and upscaling image resolution through the use of artificial
intelligence models.
2. 🧩 THIRD-PARTY COMPONENTS AND APPLICABLE LICENSES
The SOFTWARE integrates various third-party technologies and components.
The use of the SOFTWARE is conditioned not only on compliance with this
Agreement but also with the license terms of each of these components.
The following is a list of components and their respective licenses:
- QualityScaler, RealScaler, FluidFrames: MIT License (Djdefrag)
- RIFE: Apache 2.0 License (hzwer, Megvii Research)
- Real-ESRGAN, RealESRGAN-G, RealESR-Anime, RealESR-Net: BSD 3-Clause / Apache 2.0 License (Xintao Wang)
- GFPGAN: MIT License (TencentARC, Xintao Wang)
- BSRGAN: Apache 2.0 License (Kai Zhang)
- IRCNN: BSD / Mixed License (Kai Zhang)
- ONNX Runtime: MIT License (Microsoft)
- FFmpeg: LGPL-2.1 / GPL License (FFmpeg Team)
- ExifTool: Perl Artistic License (Phil Harvey)
- DirectML: MIT License (Microsoft)
- Python: PSF License (Python Software Foundation)
- Inno Setup: Custom Inno License (Jordan Russell)
3. ⚖️ LIMITATION OF LIABILITY CLAUSE (EXTENDED)
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE
AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES; INCLUDING, BUT NOT LIMITED
TO, LOSS OF DATA, BUSINESS INTERRUPTION, OR HARDWARE OR SYSTEM FAILURE
ARISING FROM THE USE, MISUSE, OR INABILITY TO USE THE SOFTWARE.
THIS SOFTWARE IS PROVIDED FOR EDUCATIONAL, CREATIVE, AND RESEARCH PURPOSES.
IT IS NOT CERTIFIED FOR IMPLEMENTATION IN CRITICAL OR COMMERCIAL
INFRASTRUCTURES WITHOUT PRIOR INDEPENDENT VALIDATION.
THE USER ASSUMES
ALL RISK ASSOCIATED WITH ITS USE.
4. 💼 INTELLECTUAL PROPERTY NOTICE
The brand, the name "Warlock-Studio" and its associated logos are the
exclusive intellectual property of Iván Eduardo Chavez Ayub.
The use of
these elements for commercial purposes is strictly prohibited without the
prior written consent of the AUTHOR.
Any redistribution or modification of
the source code must preserve the original copyright notices and all
references to the licenses contained herein.
5. ✍️ ACCEPTANCE OF TERMS
By proceeding with the installation and by using the SOFTWARE, the USER
acknowledges having read, understood, and accepted all the terms set forth
in the MIT License (Section I) and the Additional Terms (Section II) of
this Agreement.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📧 CONTACT INFORMATION
For any inquiries or communications related to this Agreement or the SOFTWARE,
you may contact the Author:
Name: Iván Eduardo Chavez Ayub
Email: negroayub97@gmail.com
GitHub: [https://github.com/Ivan-Ayub97](https://github.com/Ivan-Ayub97)
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

+62
View File
@@ -1,5 +1,67 @@
Warlock-Studio | Changelog History
# Version 6.0
**Release date:** February 13, 2026
**Kernel Version:** 6.0.0 (Codename: Chainweaver)
**Architecture:** Orchestrated Processing Chain System
### Processing Chain Orchestrator (Step-Based Pipelines)
Introduces a first-class pipeline system where each file is processed through an ordered list of steps. Every step consumes the previous steps output automatically, enabling complex multi-stage workflows without manual intervention.
- **Sequential Step Execution:** Images/videos flow through a chain, with explicit output-to-input handover between steps.
- **Scoped Output Routing:** Intermediate artifacts are written to dedicated temporary folders; only the final step targets the user-selected output directory.
- **Per-Step Configuration:** Each step can define model, GPU, resize factors, blending, output extension and video codec independently.
- **In-App Status Trace:** Clear progress messages per step (load, process, interpolate, encode), aiding auditing and troubleshooting.
### Interpolation Integration (RIFE as a Chain Step)
RIFE (and RIFE Lite) interpolation is now usable directly as a step within chains, harmonizing with upscaling and restoration operations.
- **Video-Only Validation:** If an interpolation step receives an image, the step is gracefully skipped and logged.
- **Generation Factors:** Supports x2, x4, x8 and Slowmotion variants, with automatic container selection for intermediate outputs.
- **Codec Awareness:** Respects per-step codec selection; falls back to sane defaults when omitted.
### Model Discovery & Step Editor Enhancements
The step editor exposes a combobox sourced from auto-discovered ONNX models within the `AI-onnx/` directory.
- **Auto-Discovery:** Normalizes common naming schemes (e.g., GFPGAN variants collapse to “GFPGAN”).
- **Resilient UI:** If a saved model name is not present, it is injected as a selectable value to preserve existing chains.
- **Consistent UX:** Aligns with the overall Preferences/Theme system, retaining established layout and style.
### Output Type Safety & Smart Corrections
Prevents invalid output combinations through proactive checks and automatic corrections.
- **Extension Guardrails:** Enforces valid image extensions on image steps and valid containers on video steps; incorrect selections are corrected (e.g., `.mp4` for video, `.png` for image).
- **Graceful Skips:** Incompatible steps do not break chains; they emit a clear status message and continue.
### Reliability & Memory Hygiene
Improves robustness under constrained hardware conditions and long-running chains.
- **Adaptive Tiles per Step:** VRAM limits are applied per-model using a multiplier table to derive safe tile resolutions.
- **Inter-Step Cleanup:** Instances are freed between steps and memory buffers are compacted to avoid fragmentation.
- **Stable Precision Paths:** Face restoration enforces float32 processing with safe alpha-channel recomposition.
### UI/UX Modernization & Visual Identity
Refines the applications look and feel to a modern, polished aesthetic while preserving the Warlock palette (black, red, yellow, white, gray).
- **Version Surfacing:** The application now displays version 6.0 consistently across splash, header, and preferences (About).
- **Rounded Corners:** Frames, buttons, entries, option menus, and the splash progress bar adopt rounded corners. The corner radius is configurable under Preferences and applied across the UI.
- **Vibrant Accents:** Warlock Gold and primary accents are tuned for higher vibrancy (#FFD700 title, #FFC107 accent); button hover uses a richer red for clearer affordance without sacrificing readability.
- **Consistency:** The main window, drop zone, and controls use the same corner radius and accent logic, ensuring a unified visual language throughout.
### Documentation & Guidance
- **README Updated:** Adds “New in v6.0 — Process Chaining” and updates the version badge to 6.0, detailing chain creation, model auto-discovery, and smart output handling.
- **Manual Updated:** The engineering manual (LaTeX) reflects v6.0 in metadata and includes a new section, “The Processing Chain Editor,” explaining step creation, interpolation constraints, output routing, and reliability tips.
---
# Version 5.1.1
**Release date:** December 26, 2025
+34 -3
View File
@@ -59,7 +59,7 @@
filecolor=WarlockGold,
urlcolor=WarlockBlue,
citecolor=WarlockGray,
pdftitle={Warlock-Studio v5.1.1 Master Manual},
pdftitle={Warlock-Studio v6.0 Master Manual},
pdfauthor={Ivan-Ayub97}
}
@@ -294,7 +294,7 @@
{\sffamily\bfseries\fontsize{24}{30}\selectfont MASTER OPERATING MANUAL} \\[0.5cm]
\begin{tcolorbox}[colback=WarlockRed, colframe=WarlockRed, width=4cm, arc=5mm, boxrule=0pt, halign=center]
\sffamily\bfseries\textcolor{WarlockText}{Version 5.1.1}
\sffamily\bfseries\textcolor{WarlockText}{Version 6.0}
\end{tcolorbox}
\vspace{2cm}
@@ -467,6 +467,37 @@ New in v5.1.1. It provides a live feed of the backend operations.
\item \textbf{[FFMPEG]}: Raw encoding logs from the video engine.
\end{itemize}
\section{5. The Processing Chain Editor}
This module lets you build multi-step workflows. Each step runs sequentially; the output of a step becomes the input to the next step automatically.
\subsection{Adding Steps}
\begin{itemize}
\item Click \textbf{Add Step}. Choose \textbf{Model Name} from the combobox. Values are auto-discovered from \texttt{AI-onnx/}.
\item Configure \textbf{GPU}, \textbf{Input/Output Resize}, \textbf{Blending}, \textbf{VRAM Limit}, \textbf{Extension}, and \textbf{Video Codec}.
\item Reorder with \textbf{Up/Down}. Disable a step with \textbf{Bypass} to skip it temporarily.
\end{itemize}
\subsection{Interpolation Steps (RIFE)}
\begin{itemize}
\item Use \textbf{Frame Generation} to select \textit{x2, x4, x8} or \textit{Slowmotion} variants.
\item Interpolation requires \textbf{video input}. If the current file is an image, the step is skipped and logged.
\item Intermediate containers default to \texttt{.mp4} unless the last step targets a different format.
\end{itemize}
\subsection{Output Routing}
\begin{itemize}
\item Intermediate results go to a temporary folder per step. The final step writes to the chosen output directory.
\item Extensions and codecs are corrected automatically based on media type to prevent invalid outputs.
\item Optional: keep extracted frames on the final step for inspection.
\end{itemize}
\subsection{Reliability \& Performance}
\begin{itemize}
\item VRAM-aware tiling is applied per step to avoid memory errors on large inputs.
\item Memory is cleaned between steps to maintain stability over long chains.
\item Face restoration uses stable float32 precision with proper alpha handling.
\end{itemize}
% =========================================================
% CHAPTER 3: THE MODEL LIBRARY
% =========================================================
@@ -744,7 +775,7 @@ The system will detect the existing frames and verify the \texttt{.checkpoint} f
\sffamily
\textcolor{WarlockGray!50}{\rule{0.7\linewidth}{1pt}} \\
\vspace{0.8cm}
\textcolor{WarlockGray}{\textit{Warlock-Studio v5.1.1 | Engineering Documentation}}
\textcolor{WarlockGray}{\textit{Warlock-Studio v6.0 | Engineering Documentation}}
\end{center}
\end{document}
+14 -15
View File
@@ -7,7 +7,7 @@
![License: MIT](https://img.shields.io/badge/License-MIT-0A3B1E?style=for-the-badge&logo=open-source-initiative&logoColor=FFD43B&labelColor=1B1818)
[![Last Commit](https://img.shields.io/github/last-commit/Ivan-Ayub97/Warlock-Studio?style=for-the-badge&logo=git&color=6A1B9A&logoColor=FFD43B&labelColor=1B1818)](https://github.com/Ivan-Ayub97/Warlock-Studio/commits/main)
![Version 5.1.1](https://img.shields.io/badge/Version-5.1.1-660066?style=for-the-badge&logo=tag&logoColor=FFD43B&labelColor=1B1818)
![Version 6](https://img.shields.io/badge/Version-6.0-660066?style=for-the-badge&logo=tag&logoColor=FFD43B&labelColor=1B1818)
[![Downloads Total](https://img.shields.io/github/downloads/Ivan-Ayub97/Warlock-Studio/total?style=for-the-badge&logo=github&color=2E2E2E&labelColor=1B1818&logoColor=FFD43B)](https://github.com/Ivan-Ayub97/Warlock-Studio/releases)
[![SF Downloads](https://img.shields.io/sourceforge/dt/warlock-studio?style=for-the-badge&logo=sourceforge&color=C45500&logoColor=FFD43B&labelColor=1B1818)](https://sourceforge.net/projects/warlock-studio/)
@@ -35,7 +35,7 @@ It is inspired by and based on [Djdefrag](https://github.com/Djdefrag) tools suc
<tr>
</td>
<td align="center" style="vertical-align:top; padding:10px;">
<a href="https://github.com/Ivan-Ayub97/Warlock-Studio/releases/download/v5.1.1/Warlock-Studio-5.1.1x64.exe">
<a href="https://github.com/Ivan-Ayub97/Warlock-Studio/releases/download/v6.0/Warlock-Studio-6.0x64.exe">
<img src="rsc/GitHub_Logo_WS.png" alt="Download from GitHub"
width="300" style="display:block; margin:auto; margin-bottom:10px;" />
</a>
@@ -57,8 +57,6 @@ It is inspired by and based on [Djdefrag](https://github.com/Djdefrag) tools suc
![UICapture](rsc/Capture2.png)
![UICapture](rsc/Capture3.png)
![UICapture](rsc/Capture4.png)
![UICapture](rsc/Capture5.png)
---
@@ -77,6 +75,7 @@ It is inspired by and based on [Djdefrag](https://github.com/Djdefrag) tools suc
- **AI Upscaling & Restoration** Utilize **Real-ESRGAN, BSRGAN, RealESRNet, RealESR_Animex4, and IRCNN** models for denoising, super-resolution, and detail recovery.
- **Face Restoration (GFPGAN)** Recover facial details from low-resolution or blurry images and video frames.
- **Frame Interpolation (RIFE)** Smooth motion or generate slow-motion content with **2×, 4×, or 8× interpolation**.
- **Process Chaining** Build sequential workflows by chaining steps. Mix **upscaling**, **face restoration**, and **interpolation**; each steps output becomes the next steps input automatically. Includes model auto-discovery, per-step GPU/codec settings, and smart validation (e.g., RIFE requires video).
- **Advanced Hardware Acceleration** Intelligent provider selection prioritizes **CUDA**, falls back to **DirectML**, and finally **CPU** for maximum compatibility.
- **Batch Processing** Process multiple media files simultaneously, saving time and effort.
- **Custom Workflows** Fine-grained control over models, resolution, output formats, and quality parameters.
@@ -84,6 +83,17 @@ It is inspired by and based on [Djdefrag](https://github.com/Djdefrag) tools suc
---
## 🆕 New in v6.0 — Process Chaining
- Create multi-step pipelines; order steps to run sequentially per file.
- RIFE interpolation integrates as a chain step for video sources (graceful skip on images).
- Per-step model selection via a combobox fed by auto-discovered ONNX models in `AI-onnx/`.
- Automatic output routing: intermediate steps use temp folders; the final step writes to your chosen output path.
- Smart extension/codec correction by media type to prevent invalid outputs.
- Memory-safe execution with per-step VRAM tile sizing and cleanup between steps.
---
## 🖥️ System Requirements
<div align="center">
@@ -154,14 +164,3 @@ We welcome contributions from the community.
| **Python** | PSF License | Python Software Foundation | [Official Site](https://www.python.org) |
| **PyInstaller** | GPLv2+ | PyInstaller Team | [GitHub](https://github.com/pyinstaller/pyinstaller) |
| **Inno Setup** | Custom | Jordan Russell | [Official Site](http://www.jrsoftware.org/isinfo.php) |
+10 -11
View File
@@ -13,7 +13,7 @@
; DEFINICIONES
; ---------------------------------------------------------
#define AppName "Warlock-Studio"
#define AppVersion "5.1.1"
#define AppVersion "6.0"
#define AppPublisher "Ivan-Ayub97"
#define AppURL "https://github.com/Ivan-Ayub97/Warlock-Studio"
#define AppExeName "Warlock-Studio.exe"
@@ -31,7 +31,7 @@ AppPublisherURL={#AppURL}
AppSupportURL={#AppURL}
AppUpdatesURL={#AppURL}
AppId={{***************************************}
AppId={{D1168ED1-6227-441F-8B88-EE6DBD45F336}
; ---- INSTALACIÓN EN DOCUMENTOS ----
DefaultDirName={userdocs}\{#AppName}
@@ -53,10 +53,10 @@ SolidCompression=yes
SetupLogging=yes
; ---- ESTÉTICA ----
SetupIconFile={#SourcePath}\logo.ico
SetupIconFile= C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\logo.ico
UninstallDisplayIcon={app}\{#AppExeName}
WizardImageFile={#SourcePath}\Assets\wizard-image.bmp
WizardSmallImageFile={#SourcePath}\Assets\wizard-small.bmp
WizardImageFile= C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\Assets\wizard-image.bmp
WizardSmallImageFile=C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\Assets\wizard-small.bmp
; ---- FIRMA ----
SignTool=MySignTool
@@ -65,8 +65,7 @@ SignTool=MySignTool
; LANGUAGES (SEGURO)
; =========================================================
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"; LicenseFile: "{#SourcePath}\Assets\License.txt"
; Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"; LicenseFile: "{#SourcePath}\Assets\License.txt"
Name: "english"; MessagesFile: "compiler:Default.isl"; LicenseFile: "C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\Assets\License.txt"
; =========================================================
; TASKS (OPCIONES DEL USUARIO)
@@ -81,13 +80,13 @@ Name: "userdata"; Description: "Create user data folder in Documents"; GroupDesc
; =========================================================
[Files]
Source: "{#SourcePath}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}\logo.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\logo.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourcePath}\_internal\*"; DestDir: "{app}\_internal"; \
Source: "C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\_internal\*"; DestDir: "{app}\_internal"; \
Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#SourcePath}\Assets\*"; DestDir: "{app}\Assets"; \
Source: "C:\Users\negro\Desktop\Warlock-Studio-main\Warlock-Studio\Assets\*"; DestDir: "{app}\Assets"; \
Flags: ignoreversion recursesubdirs createallsubdirs
; =========================================================
+827 -569
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ block_cipher = None
a = Analysis(
# 1. Aquí agregamos los dos archivos nuevos a la lista de scripts
['Warlock-Studio.py', 'drag_drop.py', 'console.py', 'warlock_preferences.py', 'file_queue_manager.py', 'splash_screen.py', 'warlock_theme.py'],
['Warlock-Studio.py', 'drag_drop.py', 'console.py', 'warlock_preferences.py','processing_chain.py', 'file_queue_manager.py', 'splash_screen.py', 'warlock_theme.py'],
pathex=[],
binaries=[],
datas=[
+20 -37
View File
@@ -1,37 +1,20 @@
# drag_drop.py
from customtkinter import CTk
from tkinterdnd2 import DND_ALL, TkinterDnD
# 1. Clase envoltorio que combina CustomTkinter con TkinterDnD
class DnDCTk(CTk, TkinterDnD.DnDWrapper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.TkdndVersion = TkinterDnD._require(self)
# 2. Función para registrar widgets y conectar tu lógica
def enable_drag_and_drop(window, target_widgets, callback_function):
"""
Activa Drag & Drop en los widgets especificados.
:param window: La ventana principal (debe ser instancia de DnDCTk)
:param target_widgets: Lista de widgets (botones, labels) donde se pueden soltar archivos.
:param callback_function: La función de tu app principal que recibe la lista de archivos.
"""
def _internal_drop_event(event):
# TkinterDnD a veces devuelve las rutas con llaves {} si tienen espacios
# window.tk.splitlist se encarga de limpiarlas correctamente
if event.data:
files = window.tk.splitlist(event.data)
# Llamamos a tu función principal pasando la lista limpia
callback_function(files)
for widget in target_widgets:
# Registramos el widget para aceptar cualquier cosa (archivos)
widget.drop_target_register(DND_ALL)
# Conectamos el evento 'Drop' con nuestra función interna
widget.dnd_bind('<<Drop>>', _internal_drop_event)
from customtkinter import CTk
from tkinterdnd2 import DND_FILES, TkinterDnD
class DnDCTk(CTk, TkinterDnD.DnDWrapper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
TkinterDnD.DnDWrapper.__init__(self)
self.TkdndVersion = TkinterDnD._require(self)
def enable_drag_and_drop(window, target_widgets, callback_function):
def _internal_drop_event(event):
if event.data:
files = window.tk.splitlist(event.data)
callback_function(files)
for widget in target_widgets:
widget.drop_target_register(DND_FILES)
widget.dnd_bind('<<Drop>>', _internal_drop_event)
+697
View File
@@ -0,0 +1,697 @@
import copy
import json
import logging
import os
import sys
import uuid
import tkinter as tk
from dataclasses import asdict, dataclass, field
from tkinter import filedialog, messagebox
from typing import Any, Callable, Dict, List, Optional, Tuple
import customtkinter as ctk
# -----------------------------------------------------------------------------
# CONFIGURACIÓN Y LOGGING
# -----------------------------------------------------------------------------
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger("ChainManager")
# Constantes Globales
GPU_OPTIONS = ["Auto", "GPU 1", "GPU 2", "GPU 3", "GPU 4"]
FRAME_GEN_OPTIONS = ["OFF", "x2", "x4", "x8",
"Slowmotion x2", "Slowmotion x4", "Slowmotion x8"]
# Lista combinada de códecs
VIDEO_CODECS = ["x264", "x265", "h264_nvenc", "hevc_nvenc",
"h264_amf", "hevc_amf", "h264_qsv", "hevc_qsv"]
IMG_EXTENSIONS = [".png", ".jpg", ".bmp", ".tiff", ".webp"]
VID_EXTENSIONS = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
# Colores del Tema (Coincidentes con Warlock Studio)
COLOR_BG = "#000000"
COLOR_WIDGET = "#1A1A1A"
COLOR_ACCENT = "#FFC107"
COLOR_TEXT = "#F5F5F5"
COLOR_TEXT_SEC = "#9E9E9E"
COLOR_BTN_HOVER = "#C62828"
COLOR_BORDER = "#2D2D2D"
COLOR_SUCCESS = "#00C853"
COLOR_ERROR = "#B71C1C"
# -----------------------------------------------------------------------------
# 0. HELPERS
# -----------------------------------------------------------------------------
def _resolve_asset_path(filename: str) -> str:
"""Busca robustamente el archivo en la carpeta Assets (Soporte PyInstaller)."""
try:
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
candidates = [
os.path.join(base_path, "Assets", filename),
os.path.join(base_path, "..", "Assets", filename),
os.path.join(os.getcwd(), "Assets", filename)
]
for path in candidates:
if os.path.exists(path):
return path
return ""
except Exception as e:
logger.warning(f"Error resolving asset path: {e}")
return ""
def _resolve_folder_path(foldername: str) -> str:
try:
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
candidates = [
os.path.join(base_path, foldername),
os.path.join(base_path, "..", foldername),
os.path.join(os.getcwd(), foldername)
]
for path in candidates:
if os.path.isdir(path):
return path
return ""
except Exception as e:
logger.warning(f"Error resolving folder path: {e}")
return ""
def list_available_models() -> List[str]:
ai_dir = _resolve_folder_path("AI-onnx")
if not ai_dir:
return []
candidates = []
try:
for fname in os.listdir(ai_dir):
if not fname.lower().endswith(".onnx"):
continue
name = fname[:-5] # remove .onnx
# remove common precision suffixes
for suf in ["_fp16", "_fp32", ".fp16", ".fp32"]:
if name.endswith(suf):
name = name[: -len(suf)]
# map GFPGAN variants to base
if name.startswith("GFPGAN"):
name = "GFPGAN"
candidates.append(name)
# Make unique while preserving order
seen = set()
result = []
for n in candidates:
if n not in seen:
seen.add(n)
result.append(n)
return result
except Exception as e:
logger.warning(f"Error listing models: {e}")
return []
# -----------------------------------------------------------------------------
# 1. DATA MODEL (MODELO DE DATOS)
# -----------------------------------------------------------------------------
@dataclass
class ProcessingStep:
"""
Representa un nodo de procesamiento en la cadena.
"""
model_name: str
input_resize: float
output_resize: float
blending: float
vram_limit: float
extension: str
video_codec: Optional[str] = None
frame_gen: str = "OFF"
keep_frames: bool = False
gpu: str = "Auto"
# Metadatos internos
id: str = field(default_factory=lambda: str(uuid.uuid4()))
enabled: bool = True
expanded: bool = False
# Versionado para migraciones futuras
version: int = 1
@property
def is_video_operation(self) -> bool:
"""Determina si el paso es intrínsecamente de video."""
is_rife = "RIFE" in self.model_name.upper()
has_interpolation = self.frame_gen != "OFF"
is_vid_ext = self.extension.lower() in VID_EXTENSIONS
return is_rife or has_interpolation or is_vid_ext
@property
def estimated_scale_factor(self) -> float:
"""Calcula el factor de escala total del paso (In * AI_Factor * Out)."""
# Estimación básica del modelo
model_factor = 1.0
name = self.model_name.upper()
if "X2" in name: model_factor = 2.0
elif "X4" in name: model_factor = 4.0
elif "X8" in name: model_factor = 8.0
return self.input_resize * model_factor * self.output_resize
def validate(self) -> List[str]:
"""Devuelve una lista de advertencias si la configuración es sospechosa."""
warnings = []
if self.is_video_operation and self.extension not in VID_EXTENSIONS:
warnings.append(f"Video operation '{self.model_name}' has image extension '{self.extension}'.")
if not self.is_video_operation and self.extension in VID_EXTENSIONS:
# Esto podría ser válido (crear video de imagen), pero es raro como paso intermedio
warnings.append(f"Image operation exporting directly to video container.")
if self.input_resize <= 0 or self.output_resize <= 0:
warnings.append("Resize factors must be > 0.")
return warnings
def get_summary(self) -> str:
icon = "🎬" if self.is_video_operation else "🖼️"
state = "" if self.enabled else "(BYPASS)"
details = []
if self.input_resize != 1.0: details.append(f"In:{int(self.input_resize*100)}%")
# Detectar escala del modelo
scale_txt = ""
if "X2" in self.model_name.upper(): scale_txt = " (x2)"
elif "X4" in self.model_name.upper(): scale_txt = " (x4)"
if self.output_resize != 1.0: details.append(f"Out:{int(self.output_resize*100)}%")
if self.frame_gen != "OFF": details.append(f"Gen:{self.frame_gen}")
detail_str = f"| {', '.join(details)}" if details else ""
return f"{icon} {self.model_name}{scale_txt} {detail_str} {state}"
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
@staticmethod
def from_dict(data: Dict[str, Any]) -> 'ProcessingStep':
known_keys = ProcessingStep.__annotations__.keys()
filtered = {k: v for k, v in data.items() if k in known_keys}
if 'id' not in filtered: filtered['id'] = str(uuid.uuid4())
return ProcessingStep(**filtered)
# -----------------------------------------------------------------------------
# 2. UI COMPONENTS
# -----------------------------------------------------------------------------
class ToolTip:
"""Tooltip nativo para Tkinter."""
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tip_window = None
widget.bind("<Enter>", self.show_tip)
widget.bind("<Leave>", self.hide_tip)
def show_tip(self, event=None):
if self.tip_window or not self.text: return
x, y, _, _ = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + 25
y += self.widget.winfo_rooty() + 25
self.tip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
background="#333333", foreground="#FFFFFF", relief=tk.SOLID, borderwidth=1,
font=("Arial", "9", "normal"))
label.pack(ipadx=5, ipady=2)
def hide_tip(self, event=None):
if self.tip_window:
self.tip_window.destroy()
self.tip_window = None
class ValidatedEntry(ctk.CTkEntry):
def __init__(self, master, is_float=True, min_val=0.0, max_val=9999.0, **kwargs):
super().__init__(master, **kwargs)
self.is_float = is_float
self.min_val = min_val
self.max_val = max_val
vcmd = (self.register(self._validate), '%P')
self.configure(validate="key", validatecommand=vcmd)
self.bind("<FocusOut>", self._on_focus_out)
def _validate(self, new_value):
if new_value == "": return True
try:
val = float(new_value) if self.is_float else int(new_value)
return True
except ValueError:
return False
def _on_focus_out(self, event):
val = self.get()
if not val: return
try:
num = float(val)
if num < self.min_val: self.delete(0, "end"); self.insert(0, str(self.min_val))
elif num > self.max_val: self.delete(0, "end"); self.insert(0, str(self.max_val))
except: pass
class StepEditorDialog(ctk.CTkToplevel):
"""Editor modal avanzado."""
def __init__(self, parent, step: ProcessingStep, on_save_callback: Callable):
super().__init__(parent)
self.title(f"Edit Step")
self.geometry("450x650")
self.resizable(False, False)
self.configure(fg_color=COLOR_BG)
icon_path = _resolve_asset_path("logo.ico")
if icon_path: self.after(200, lambda: self.iconbitmap(icon_path))
self.step = step
self.on_save = on_save_callback
self.widgets = {}
self.transient(parent)
self.grab_set()
self.main_frame = ctk.CTkScrollableFrame(self, fg_color=COLOR_WIDGET, label_text="Step Configuration")
self.main_frame.pack(fill="both", expand=True, padx=10, pady=10)
self._build_ui()
self._populate_values()
self._build_buttons()
def _build_ui(self):
row = 0
# Model & Hardware
self._header("AI Model & Hardware", row); row += 1
self._label("Model Name:", row)
try:
model_values = list_available_models()
except Exception:
model_values = []
if not model_values:
model_values = ["RealESR_Gx4", "RealESR_Animex4", "BSRGANx4", "BSRGANx2", "RealESRGANx4", "RealESRNetx4", "IRCNN_Mx1", "IRCNN_Lx1", "GFPGAN", "RIFE", "RIFE_Lite"]
self.widgets['model_name'] = ctk.CTkComboBox(self.main_frame, values=model_values, width=220)
self.widgets['model_name'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
self._label("Compute Device:", row)
self.widgets['gpu'] = ctk.CTkComboBox(self.main_frame, values=GPU_OPTIONS, width=220)
self.widgets['gpu'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
# Scaling
self._header("Resolution & Scaling", row); row += 1
self._label("Input Resize (0.1 - 1.0):", row)
self.widgets['input_resize'] = ValidatedEntry(self.main_frame, min_val=0.1, max_val=1.0, width=220)
self.widgets['input_resize'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
self._label("Output Resize (0.1 - 8.0):", row)
self.widgets['output_resize'] = ValidatedEntry(self.main_frame, min_val=0.1, max_val=8.0, width=220)
self.widgets['output_resize'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
# Advanced
self._header("Advanced Processing", row); row += 1
self._label("Blending (0.0 - 1.0):", row)
self.widgets['blending'] = ValidatedEntry(self.main_frame, min_val=0.0, max_val=1.0, width=220)
self.widgets['blending'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
self._label("VRAM Limit (GB):", row)
self.widgets['vram_limit'] = ValidatedEntry(self.main_frame, min_val=0.0, max_val=24.0, width=220)
self.widgets['vram_limit'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
# Output
self._header("Output Format", row); row += 1
self._label("Extension:", row)
exts = list(set(IMG_EXTENSIONS + VID_EXTENSIONS)); exts.sort()
self.widgets['extension'] = ctk.CTkComboBox(self.main_frame, values=exts, width=220)
self.widgets['extension'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
self._label("Video Codec:", row)
self.widgets['video_codec'] = ctk.CTkComboBox(self.main_frame, values=[""] + VIDEO_CODECS, width=220)
self.widgets['video_codec'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
self._label("Frame Generation:", row)
self.widgets['frame_gen'] = ctk.CTkComboBox(self.main_frame, values=FRAME_GEN_OPTIONS, width=220)
self.widgets['frame_gen'].grid(row=row, column=1, sticky="e", padx=5, pady=5)
row += 1
self.widgets['keep_frames'] = ctk.CTkCheckBox(self.main_frame, text="Keep Intermediate Frames", text_color=COLOR_TEXT)
self.widgets['keep_frames'].grid(row=row, column=0, columnspan=2, pady=15)
def _header(self, text, r):
lbl = ctk.CTkLabel(self.main_frame, text=text, font=("Roboto", 13, "bold"), text_color=COLOR_ACCENT, anchor="w")
lbl.grid(row=r, column=0, columnspan=2, sticky="ew", pady=(15, 5), padx=5)
# Separator
sep = ctk.CTkFrame(self.main_frame, height=2, fg_color=COLOR_BORDER)
sep.grid(row=r, column=0, columnspan=2, sticky="ews", pady=(0, 0))
def _label(self, text, r):
ctk.CTkLabel(self.main_frame, text=text, text_color=COLOR_TEXT_SEC, anchor="w").grid(row=r, column=0, sticky="w", padx=10)
def _populate_values(self):
s = self.step
# Para ComboBox, usar set()
try:
self.widgets['model_name'].set(s.model_name)
except Exception:
# Fallback: si no está en lista, agregarlo dinámicamente
current_vals = list(self.widgets['model_name'].cget("values"))
if s.model_name and s.model_name not in current_vals:
current_vals = [s.model_name] + current_vals
self.widgets['model_name'].configure(values=current_vals)
self.widgets['model_name'].set(s.model_name)
self.widgets['gpu'].set(s.gpu)
self.widgets['input_resize'].insert(0, str(s.input_resize))
self.widgets['output_resize'].insert(0, str(s.output_resize))
self.widgets['blending'].insert(0, str(s.blending))
self.widgets['vram_limit'].insert(0, str(s.vram_limit))
if s.extension not in self.widgets['extension'].cget("values"):
self.widgets['extension'].set(s.extension)
else:
self.widgets['extension'].set(s.extension)
self.widgets['video_codec'].set(s.video_codec or "")
self.widgets['frame_gen'].set(s.frame_gen)
if s.keep_frames: self.widgets['keep_frames'].select()
def _build_buttons(self):
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=20, pady=20)
ctk.CTkButton(btn_frame, text="Cancel", fg_color="#424242", hover_color="#616161",
command=self.destroy).pack(side="left", expand=True, padx=5)
ctk.CTkButton(btn_frame, text="Save Changes", fg_color=COLOR_SUCCESS, hover_color="#00E676", text_color="white",
command=self.save).pack(side="right", expand=True, padx=5)
def save(self):
try:
self.step.model_name = self.widgets['model_name'].get()
self.step.gpu = self.widgets['gpu'].get()
self.step.input_resize = float(self.widgets['input_resize'].get())
self.step.output_resize = float(self.widgets['output_resize'].get())
self.step.blending = float(self.widgets['blending'].get())
self.step.vram_limit = float(self.widgets['vram_limit'].get())
self.step.extension = self.widgets['extension'].get()
codec = self.widgets['video_codec'].get()
self.step.video_codec = codec if codec.strip() else None
self.step.frame_gen = self.widgets['frame_gen'].get()
self.step.keep_frames = bool(self.widgets['keep_frames'].get())
self.on_save(self.step)
self.destroy()
except ValueError as e:
messagebox.showerror("Validation Error", f"Check numeric fields: {e}")
class ModernStepCard(ctk.CTkFrame):
"""Tarjeta visual que representa un paso."""
def __init__(self, master, step: ProcessingStep, index: int, total_steps: int, callbacks: Dict):
color_bg = COLOR_WIDGET if step.enabled else "#111111"
color_border = COLOR_BORDER if step.enabled else "#222222"
super().__init__(master, corner_radius=8, border_width=1, fg_color=color_bg, border_color=color_border)
self.step = step
self.callbacks = callbacks
self.index = index
self.grid_columnconfigure(1, weight=1)
# --- HEADER ---
header = ctk.CTkFrame(self, fg_color="transparent")
header.pack(fill="x", padx=8, pady=8)
# Índice y Icono
icon = "🎬" if step.is_video_operation else "🖼️"
ctk.CTkLabel(header, text=f"{index+1}", font=("Arial", 16, "bold"), text_color=COLOR_ACCENT, width=25).pack(side="left")
ctk.CTkLabel(header, text=icon, font=("Arial", 16)).pack(side="left", padx=(0, 5))
# Título
title_txt = step.model_name if step.expanded else step.get_summary()
title_col = COLOR_TEXT if step.enabled else "gray"
self.lbl_title = ctk.CTkLabel(header, text=title_txt, font=("Roboto", 13, "bold"), text_color=title_col, anchor="w")
self.lbl_title.pack(side="left", fill="x", expand=True, padx=5)
# Validación Warning
warnings = step.validate()
if warnings and step.enabled:
warn_lbl = ctk.CTkLabel(header, text="⚠️", text_color=COLOR_ACCENT)
warn_lbl.pack(side="right", padx=5)
ToolTip(warn_lbl, "\n".join(warnings))
# Switch y Expand
self.switch = ctk.CTkSwitch(header, text="", width=35, height=20, command=self._on_toggle,
onvalue=True, offvalue=False, progress_color=COLOR_SUCCESS)
if step.enabled: self.switch.select()
else: self.switch.deselect()
self.switch.pack(side="right", padx=5)
btn_exp = ctk.CTkButton(header, text="" if not step.expanded else "", width=25, height=25,
fg_color="transparent", text_color="gray", hover_color="#333333",
command=self._toggle_expand)
btn_exp.pack(side="right")
# --- BODY EXPANDIDO ---
if step.expanded and step.enabled:
body = ctk.CTkFrame(self, fg_color="transparent")
body.pack(fill="x", padx=10, pady=(0, 10))
# Info Grid
self._info_row(body, 0, "Input Scale:", f"{int(step.input_resize*100)}%", "Output Scale:", f"{int(step.output_resize*100)}%")
self._info_row(body, 1, "VRAM Limit:", f"{step.vram_limit} GB", "Blending:", str(step.blending))
self._info_row(body, 2, "GPU:", step.gpu, "Extension:", step.extension)
if step.frame_gen != "OFF":
self._info_row(body, 3, "Frame Gen:", step.frame_gen, "Codec:", step.video_codec or "Auto")
# Action Buttons
actions = ctk.CTkFrame(body, fg_color="transparent", height=30)
actions.grid(row=99, column=0, columnspan=4, sticky="ew", pady=(15, 0))
# Move
if index > 0:
self._btn(actions, "", lambda: callbacks['move'](step.id, -1), "left")
if index < total_steps - 1:
self._btn(actions, "", lambda: callbacks['move'](step.id, 1), "left")
# CRUD
self._btn(actions, "🗑 Delete", lambda: callbacks['delete'](step.id), "right", COLOR_ERROR, "#D32F2F")
self._btn(actions, "⧉ Clone", lambda: callbacks['clone'](step.id), "right", "#5E35B1", "#7E57C2")
self._btn(actions, "✎ Edit", lambda: callbacks['edit'](step.id), "right", "#1976D2", "#42A5F5")
def _info_row(self, master, r, t1, v1, t2, v2):
self._cell(master, r, 0, t1, v1)
self._cell(master, r, 1, t2, v2)
def _cell(self, master, r, c, title, value):
f = ctk.CTkFrame(master, fg_color="transparent")
f.grid(row=r, column=c, sticky="w", padx=5, pady=2)
ctk.CTkLabel(f, text=title, font=("Arial", 11), text_color=COLOR_TEXT_SEC).pack(side="left")
ctk.CTkLabel(f, text=str(value), font=("Arial", 11, "bold"), text_color=COLOR_TEXT).pack(side="left", padx=5)
def _btn(self, master, txt, cmd, side, col="#424242", hov="#616161"):
ctk.CTkButton(master, text=txt, width=50, height=24, fg_color=col, hover_color=hov,
font=("Arial", 11), command=cmd).pack(side=side, padx=2)
def _on_toggle(self):
self.step.enabled = bool(self.switch.get())
self.callbacks['refresh']()
def _toggle_expand(self):
self.step.expanded = not self.step.expanded
self.callbacks['refresh']()
# -----------------------------------------------------------------------------
# 3. MANAGER CONTROLLER
# -----------------------------------------------------------------------------
class ChainManager(ctk.CTkToplevel):
def __init__(self, parent, get_current_settings_callback: Callable):
super().__init__(parent)
self.title("Workflow Chain Manager")
self.geometry("500x750")
self.minsize(450, 500)
self.configure(fg_color=COLOR_BG)
self.transient(parent)
icon_path = _resolve_asset_path("logo.ico")
if icon_path: self.after(200, lambda: self.iconbitmap(icon_path))
self.get_current_settings = get_current_settings_callback
self.steps: List[ProcessingStep] = []
# Layout
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self._build_toolbar()
# Scroll Area
self.scroll_frame = ctk.CTkScrollableFrame(self, fg_color="#121212", label_text="Processing Pipeline")
self.scroll_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
self._build_bottom_panel()
self.refresh_ui()
def _build_toolbar(self):
tb = ctk.CTkFrame(self, height=50, fg_color="transparent")
tb.grid(row=0, column=0, sticky="ew", padx=10, pady=10)
ctk.CTkLabel(tb, text="Active Chain", font=("Roboto", 18, "bold"), text_color=COLOR_ACCENT).pack(side="left", padx=5)
self.lbl_stats = ctk.CTkLabel(tb, text="", font=("Arial", 11), text_color="gray")
self.lbl_stats.pack(side="left", padx=15)
self._tb_btn(tb, "🗑 Clear", self.clear_chain, COLOR_ERROR, "#D32F2F")
self._tb_btn(tb, "💾 Save", self.save_preset, "#455A64", "#607D8B")
self._tb_btn(tb, "📂 Load", self.load_preset, "#455A64", "#607D8B")
def _tb_btn(self, master, txt, cmd, col, hov):
ctk.CTkButton(master, text=txt, width=60, height=28, fg_color=col, hover_color=hov,
font=("Arial", 11, "bold"), command=cmd).pack(side="right", padx=2)
def _build_bottom_panel(self):
panel = ctk.CTkFrame(self, height=80, fg_color=COLOR_WIDGET)
panel.grid(row=2, column=0, sticky="ew")
self.btn_add = ctk.CTkButton(panel, text=" APPEND CURRENT SETTINGS AS STEP",
command=self.add_step_from_gui,
fg_color="#00695C", hover_color="#00897B",
height=45, font=("Roboto", 13, "bold"))
self.btn_add.pack(padx=20, pady=15, fill="x")
def add_step_from_gui(self):
try:
s = self.get_current_settings()
if not s['model'] or "•••" in s['model']:
messagebox.showerror("Error", "Please select a valid AI Model in the main window first.")
return
# Inferencia automática de tipo de extensión
is_rife = "RIFE" in s['model'].upper()
has_gen = s.get('frame_gen', "OFF") != "OFF"
# Si es RIFE o tiene FrameGen, forzamos modo Video si la extensión no es explícita
# Prioridad: Extensión de video si está seleccionada, sino .mp4
if is_rife or has_gen:
ext = s.get('ext_vid') if s.get('ext_vid') in VID_EXTENSIONS else ".mp4"
else:
# Upscalers normales usan extensión de imagen a menos que el usuario haya seleccionado explicitamente video en main
# Pero en la cadena, paso intermedio suele ser imagen a menos que sea el final.
# Por defecto tomamos la configuración visual actual.
ext = s.get('ext_img') if s.get('ext_img') in IMG_EXTENSIONS else ".png"
new_step = ProcessingStep(
model_name=s['model'],
input_resize=s['input_resize'],
output_resize=s['output_resize'],
blending=s['blending'],
vram_limit=s['vram'],
extension=ext,
video_codec=s.get('codec'),
frame_gen=s.get('frame_gen', "OFF"),
keep_frames=s.get('keep_frames', False),
gpu=s.get('gpu', "Auto")
)
self.steps.append(new_step)
self.refresh_ui()
self.after(100, lambda: self.scroll_frame._parent_canvas.yview_moveto(1.0))
except Exception as e:
messagebox.showerror("Error", f"Could not capture settings: {e}")
# --- Lógica CRUD ---
def _move(self, uid, direction):
idx = next((i for i, s in enumerate(self.steps) if s.id == uid), -1)
if idx == -1: return
n_idx = idx + direction
if 0 <= n_idx < len(self.steps):
self.steps[idx], self.steps[n_idx] = self.steps[n_idx], self.steps[idx]
self.refresh_ui()
def _delete(self, uid):
self.steps = [s for s in self.steps if s.id != uid]
self.refresh_ui()
def _clone(self, uid):
idx = next((i for i, s in enumerate(self.steps) if s.id == uid), -1)
if idx != -1:
cloned = copy.deepcopy(self.steps[idx])
cloned.id = str(uuid.uuid4())
self.steps.insert(idx + 1, cloned)
self.refresh_ui()
def _edit(self, uid):
step = next((s for s in self.steps if s.id == uid), None)
if step:
StepEditorDialog(self, step, lambda x: self.refresh_ui())
def clear_chain(self):
if self.steps and messagebox.askyesno("Confirm", "Clear entire chain?"):
self.steps = []
self.refresh_ui()
def get_chain(self) -> List[ProcessingStep]:
return [s for s in self.steps if s.enabled]
# --- Rendering ---
def refresh_ui(self):
for w in self.scroll_frame.winfo_children(): w.destroy()
if not self.steps:
f = ctk.CTkFrame(self.scroll_frame, fg_color="transparent")
f.pack(pady=60)
ctk.CTkLabel(f, text="Workflow is Empty", font=("Arial", 16, "bold"), text_color="gray").pack()
ctk.CTkLabel(f, text="Configure main window settings\nand click 'Append' below.", text_color="#555").pack(pady=5)
self.lbl_stats.configure(text="")
else:
cbs = {'move': self._move, 'delete': self._delete, 'clone': self._clone, 'edit': self._edit, 'refresh': self.refresh_ui}
total_scale = 1.0
for i, step in enumerate(self.steps):
if step.enabled: total_scale *= step.estimated_scale_factor
ModernStepCard(self.scroll_frame, step, i, len(self.steps), cbs).pack(fill="x", pady=6, padx=5)
self.lbl_stats.configure(text=f"Est. Scale: x{total_scale:.2f}")
# --- JSON Persistence ---
def save_preset(self):
if not self.steps: return
path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("Warlock Preset", "*.json")])
if path:
try:
data = {"version": 1, "steps": [s.to_dict() for s in self.steps]}
with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=4)
messagebox.showinfo("Saved", "Workflow saved.")
except Exception as e:
messagebox.showerror("Error", f"Save failed: {e}")
def load_preset(self):
path = filedialog.askopenfilename(filetypes=[("Warlock Preset", "*.json")])
if path:
try:
with open(path, 'r', encoding='utf-8') as f: data = json.load(f)
# Soporte legacy (si el json es una lista directa)
items = data if isinstance(data, list) else data.get("steps", [])
self.steps = [ProcessingStep.from_dict(i) for i in items]
self.refresh_ui()
except Exception as e:
messagebox.showerror("Error", f"Load failed: {e}")
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 66 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 56 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

+1 -1
View File
@@ -122,7 +122,7 @@ class SplashScreen(ctk.CTkToplevel):
self,
width=300,
height=8,
corner_radius=4,
corner_radius=8,
progress_color=self.theme['accent'],
fg_color=self.theme['widget_bg'],
border_width=0
+2 -2
View File
@@ -27,7 +27,7 @@ class WarlockColors:
# --- IDENTIDAD & ACENTOS ---
# "Oro Metálico". Elegante, legible y define la marca Warlock.
APP_TITLE = "#FBC02D"
APP_TITLE = "#FFD700"
# "Ámbar Intenso". Color principal de interacción (Checkboxes, Switches, Sliders).
ACCENT = "#FFC107"
@@ -50,7 +50,7 @@ class WarlockColors:
# --- INTERACCIÓN (BOTONES) ---
# "Rojo Sangre/Rubí". Para Hover en botones principales o acciones destructivas.
BUTTON_HOVER = "#C62828"
BUTTON_HOVER = "#D32F2F"
# "Vino Tinto". Para botones secundarios, info o estados inactivos pero visibles.
BUTTON_SECONDARY = "#7F1500"