Update-6.0
@@ -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)
|
||||
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 625 KiB |
|
Before Width: | Height: | Size: 105 KiB |
@@ -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 step’s 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 application’s 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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||

|
||||
|
||||
[](https://github.com/Ivan-Ayub97/Warlock-Studio/commits/main)
|
||||

|
||||

|
||||
|
||||
[](https://github.com/Ivan-Ayub97/Warlock-Studio/releases)
|
||||
[](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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -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 step’s output becomes the next step’s 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) |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
; =========================================================
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 54 KiB |
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||