diff --git a/AI-onnx/Instructions_for_downloading_AI_models.txt b/AI-onnx/Instructions_for_downloading_AI_models.txt
index 11c9442..aa28de4 100644
--- a/AI-onnx/Instructions_for_downloading_AI_models.txt
+++ b/AI-onnx/Instructions_for_downloading_AI_models.txt
@@ -1 +1,2 @@
Download the AI-onnx.zip file from the releases section, prioritizing the latest available version.
+and extract the contents of the zip file to this directory.
\ No newline at end of file
diff --git a/Assets/Instructions_for_downloading_Assets_Files.txt b/Assets/Instructions_for_downloading_Assets_Files.txt
new file mode 100644
index 0000000..1051f1a
--- /dev/null
+++ b/Assets/Instructions_for_downloading_Assets_Files.txt
@@ -0,0 +1 @@
+Download the Assets.zip from the releases section, prioritizing the latest version, and extract the contents of the zip file to this directory.
\ No newline at end of file
diff --git a/Assets/Instructions_for_downloading_bin_files.txt b/Assets/Instructions_for_downloading_bin_files.txt
deleted file mode 100644
index 4442cd1..0000000
--- a/Assets/Instructions_for_downloading_bin_files.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Download the Assets_bin.zip file from the releases section, prioritizing the latest available version, and extract the ffmpeg and exiftool bins to this directory.
-https://github.com/Ivan-Ayub97/Warlock-Studio/releases/download/v5.0/AI-onnx.zip
diff --git a/Assets/Warlock-Studio_Manual.pdf b/Assets/Warlock-Studio_Manual.pdf
index 554002d..f58679b 100644
Binary files a/Assets/Warlock-Studio_Manual.pdf and b/Assets/Warlock-Studio_Manual.pdf differ
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6910c87..c923fee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,52 @@
-_Warlock-Studio | Changelog History_
+Warlock-Studio | Changelog History
+
+# Version 5.1.1
+
+**Release date:** December 26, 2025
+**Kernel Version:** 5.1.1 (Codename: Obsidian-Warlock)
+**Architecture:** Component-Based / Modular Component System
+
+### Core Architectural Refactoring & Modularization
+
+The application codebase has undergone a foundational restructuring, migrating from a monolithic single-script paradigm to a segregated, component-oriented architecture. This transition enforces strict **Separation of Concerns (SoC)**, significantly reducing the cyclomatic complexity of the main execution thread and establishing a scalable foundation for future feature integration.
+
+#### Decoupled Theme Engine (`warlock_theme.py`)
+
+- **Centralized Style Governance (Single Source of Truth):** The application’s visual styling logic has been abstracted from the runtime logic. Previously hardcoded hexadecimal color values—scattered redundantly across `Warlock-Studio5.1.py` and `warlock_preferences.py`—have been consolidated into a dedicated **`warlock_theme.py`** module. This creates a unified style dictionary that is imported dynamically by all subsystems.
+- **Atomic Variable Propagation:** The refactoring implements a direct import strategy (`from warlock_theme import *`), ensuring that any modification to the core palette propagates atomically across the Main Window, Preferences Dialog, and Splash Screen interfaces without the risk of visual regression or variable mismatch between window contexts.
+
+#### Autonomous Initialization Subsystem (`splash_screen.py`)
+
+- **Logic Extraction & Encapsulation:** The `SplashScreen` class, formerly an inner class tightly coupled to the main application scope, has been externalized into a standalone **`splash_screen.py`** module. This promotes code reusability and cleaner namespace management within the primary entry point.
+- **Dependency Injection Pattern:** The new subsystem implements a dependency injection pattern, accepting `asset_loader` callables and theme dictionaries as constructor arguments. This decouples the asset resolution logic from the UI rendering thread, enhancing modularity and testing capabilities.
+
+### Boot Sequence Optimization & Performance
+
+The application initialization timeline has been re-engineered to optimize perceived performance, reduce thread blocking during startup, and provide richer visual feedback during the model pre-loading phase.
+
+#### Latency Reduction & Timeline Compression
+
+- **Parametric Duration Control:** Replaced the legacy hardcoded blocking delay (`window.after(11000)`) with a flexible, parametric duration logic. The initialization sequence has been compressed to a total of **6,500ms**, representing a **~40.9% reduction** in boot latency compared to version 5.1 (11,000ms), while maintaining sufficient buffer time for `onnxruntime` provider checks.
+- **Event-Driven Handover:** The transition from the splash context to the main application window is now managed via a completion callback (`on_complete`), ensuring a frame-perfect handover between the `CTkToplevel` splash instance and the main `DnDCTk` root window.
+
+#### Advanced Particle Rendering Engine
+
+- **Canvas-Based Vector Animation:** The static bitmap banner system has been superseded by a dynamic particle engine rendered via `ctk.CTkCanvas`. The engine manages the vector trajectory, velocity (`dx`, `dy`), and boundary collision detection for **35 concurrent particles**, providing immediate visual feedback that confirms the application process is active and responsive.
+
+### Visual Identity (UI/UX)
+
+#### Chromatic Recalibration
+
+- **Black Point Adjustment (OLED Optimization):** The primary background styling constant has been shifted from Dark Grey (`#0A0A0A`) to **Absolute Black** (`#000000`). This adjustment maximizes contrast ratios on OLED and high-end IPS displays, providing a seamless "borderless" aesthetic.
+- **Accent Shift & Hierarchy:** The high-saturation Neon Yellow (`#FDEF2F`) accents have been deprecated in favor of **Metallic Gold** (`#FBC02D`) and **Deep Amber** (`#FFC107`). This shift establishes a more sophisticated visual hierarchy and improves text legibility against the darkened substrates.
+- **Secondary Element Refinement:**
+ - **Panel/Widget Backgrounds:** Darkened from `#303030` to `#1A1A1A` (Charcoal) to reduce visual noise.
+ - **Hover States:** Replaced the bright `#D41C1C` with a deeper **Ruby Red** (`#C62828`) for subtler interactive feedback.
+
+#### Interface Consistency & Layout Fixes
+
+- **Background Dissonance Resolution:** Rectified a visual bug where the _Preferences_ window rendered a lighter background (`#121212`) than the _Main Window_ (`#0A0A0A`). Both interfaces now inherit the strict `BACKGROUND_COLOR` constant from the theme engine, ensuring absolute visual continuity.
+- **Widget Layout Optimization:** The `MENU_LIST_SEPARATOR` constant has been optimized from an extended character string (`• • • ...`) to a compact glyph set (`•••`). This correction resolves width-calculation anomalies in `CTkOptionMenu` widgets, preventing unnecessary horizontal expansion and padding issues in model selection dropdowns.
# Version 5.1
diff --git a/Manual/Warlock-Studio_Manual.tex b/Manual/Warlock-Studio_Manual.tex
index c37f6f2..52e95c4 100644
--- a/Manual/Warlock-Studio_Manual.tex
+++ b/Manual/Warlock-Studio_Manual.tex
@@ -1,102 +1,261 @@
-\documentclass[a4paper,11pt]{article}
+\documentclass[a4paper,11pt,oneside]{report}
% =========================================================
-% PACKAGES & CONFIGURATION
+% 1. OPTIMIZED PACKAGES & CORE CONFIG
% =========================================================
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
-\usepackage[margin=1in, bottom=1.2in]{geometry}
+% Slightly wider margins for a modern tech doc feel, tall header space
+\usepackage[margin=1.1in, top=1.3in, bottom=1.3in, headheight=30pt, footskip=40pt]{geometry}
+
+% --- TYPOGRAPHY & MICRO-OPTIMIZATION ---
+\usepackage[bitstream-charter]{mathdesign} % XCharter: A robust, professional serif for tech docs
+\usepackage[scaled=0.92]{helvet} % Clean sans-serif for headers
+\usepackage{courier} % Standardized monospaced font
+\usepackage{microtype} % Essential for professional text justification and kerning
+\usepackage{parskip} % Modern paragraph spacing (no indent)
+\linespread{1.05} % Slightly open up line spacing for readability
+
\usepackage{xcolor}
\usepackage{graphicx}
-\usepackage{titlesec}
+\usepackage[export]{adjustbox} % Allows 'valign=m' for inline images in footer
+\usepackage[explicit]{titlesec} % 'explicit' enables advanced header designs
\usepackage{fancyhdr}
-\usepackage{tcolorbox}
-\usepackage{fontawesome5} % For icons
-\usepackage{booktabs}
+\usepackage[most]{tcolorbox} % 'most' loads advanced skins, breakable, etc.
+\usepackage{fontawesome5}
+\usepackage{booktabs} % Professional table rules
\usepackage{tabularx}
\usepackage{multirow}
\usepackage{tikz}
+\usetikzlibrary{shapes.geometric, arrows.meta, positioning, shadows.blur, calc, patterns, backgrounds}
\usepackage{float}
-\usepackage{hyperref}
+\usepackage[hidelinks, colorlinks=true, breaklinks=true]{hyperref}
\usepackage{enumitem}
-
-\usetikzlibrary{shapes.geometric, arrows, positioning}
-\tcbuselibrary{skins, breakable}
+\usepackage{listings}
+\usepackage{menukeys}
+\usepackage{colortbl}
% =========================================================
-% WARLOCK STUDIO BRANDING COLORS (From Source Code)
+% 2. BRANDING COLOR PALETTE (MODERNIZED)
% =========================================================
-\definecolor{WarlockBg}{HTML}{0A0A0A} % Main Background
-\definecolor{WarlockRed}{HTML}{D41C1C} % Accent Red
-\definecolor{WarlockGold}{HTML}{FDEF2F} % Accent Gold
-\definecolor{WarlockDark}{HTML}{303030} % Widget Background
-\definecolor{WarlockText}{HTML}{FFFFFF} % White Text
-\definecolor{WarlockGray}{HTML}{B5B4B4} % Secondary Text
+% Core Brand Colors (Preserved)
+\definecolor{WarlockBg}{HTML}{050505} % Deep Black
+\definecolor{WarlockPanel}{HTML}{1A1A1A} % Widget Background
+\definecolor{WarlockRed}{HTML}{D32F2F} % Slightly richer Red for print/screen
+\definecolor{WarlockGold}{HTML}{FFA000} % Slightly warmer Gold
+\definecolor{WarlockText}{HTML}{FAFAFA} % Off-white for dark backgrounds
+\definecolor{WarlockGray}{HTML}{757575} % Neutral Gray
+\definecolor{WarlockSuccess}{HTML}{388E3C} % Professional Green
+\definecolor{WarlockBlue}{HTML}{1976D2} % Deep professional Blue
+% Functional UI Colors for the Document
+\definecolor{DocInk}{HTML}{212121} % Soft black for main text (easier on eyes than pure black)
+\definecolor{DocCodeBg}{HTML}{F5F7F9} % Very light gray-blue for code blocks
+\definecolor{DocHeaderBg}{HTML}{F0F2F5} % Light background for header bars
-% Header and Footer
+% Hyperlinks Setup
+\hypersetup{
+ linkcolor=WarlockRed,
+ filecolor=WarlockGold,
+ urlcolor=WarlockBlue,
+ citecolor=WarlockGray,
+ pdftitle={Warlock-Studio v5.1.1 Master Manual},
+ pdfauthor={Ivan-Ayub97}
+}
+
+% =========================================================
+% 3. ADVANCED STYLING MACROS
+% =========================================================
+
+% --- HEADER & FOOTER ARCHITECTURE ---
\pagestyle{fancy}
\fancyhf{}
-\renewcommand{\headrulewidth}{0pt}
-\renewcommand{\footrulewidth}{2pt}
-\renewcommand{\footrule}{\hbox to\headwidth{\color{WarlockRed}\leaders\hrule height \footrulewidth\hfill}}
+\renewcommand{\headrulewidth}{0pt} % Remove default ugly line
+\renewcommand{\footrulewidth}{0pt}
-\fancyhead[R]{\textbf{\textcolor{WarlockDark}{Warlock-Studio v5.1}}}
-\fancyhead[L]{\textcolor{WarlockGray}{User Manual}}
-\fancyfoot[C]{\thepage}
-\fancyfoot[R]{\includegraphics[height=0.8cm]{logo.png}} % Logo at footer
+% Modern Header with colored accent bar
+\fancyhead[L]{%
+ \begin{tikzpicture}[overlay, remember picture]%
+ \fill[WarlockRed] (current page.north west) rectangle ($(current page.north east)+(0,-4pt)$);%
+ \end{tikzpicture}%
+ \sffamily\bfseries\footnotesize\textcolor{DocInk!70}{WARLOCK-STUDIO \textcolor{WarlockRed}{v5.1.1}}%
+}
+\fancyhead[R]{\sffamily\footnotesize\textcolor{WarlockGray}{MASTER OPERATING MANUAL}}
-% Section Styling
+% Clean Technical Footer
+\fancyfoot[L]{\sffamily\footnotesize\textcolor{WarlockGray}{\today}}
+\fancyfoot[C]{%
+ \begin{tikzpicture}%
+ \node[circle, fill=WarlockRed, inner sep=2pt, text=white, font=\sffamily\bfseries\footnotesize] {\thepage};%
+ \end{tikzpicture}%
+}
+% FOOTER MODIFIED HERE: Replaced Heart with miniature logo.png
+\fancyfoot[R]{\sffamily\footnotesize Developed by \textbf{Ivan-Ayub97} \ \includegraphics[height=1.3em, keepaspectratio, valign=m]{logo.png}}
+
+
+% --- CHAPTER & SECTION HEADERS (ARCHITECTURAL STYLE) ---
+
+% Chapter Header: Solid, structured, modern block
+\titleformat{\chapter}[block]
+{\sffamily}
+{}
+{0pt}
+{%
+ \begin{tikzpicture}[remember picture, overlay]
+ % Colored structural bar
+ \fill[DocHeaderBg] ($(current page.north west)+ (0,-2cm)$) rectangle ($(current page.north east)+ (0,-5.5cm)$);
+ % Red accent line
+ \draw[WarlockRed, line width=3pt] ($(current page.north west)+ (1.1in,-5.5cm)$) -- ($(current page.north east)+ (-1.1in,-5.5cm)$);
+ \end{tikzpicture}%
+ \vspace{-1.5cm}
+ \begin{flushleft}
+ \begin{tikzpicture}
+ \node[opacity=0.1, font=\sffamily\bfseries\fontsize{80}{80}\selectfont, color=WarlockGray, anchor=south west] at (-0.5,0.2) {\thechapter};
+ \node[font=\sffamily\bfseries\huge\color{DocInk}, anchor=south west, align=left, inner sep=0] at (0,0.5) {\strut #1};
+ \end{tikzpicture}
+ \end{flushleft}
+ \vspace{1cm}
+}
+\titlespacing{\chapter}{0pt}{0pt}{20pt}
+
+% Section Header: Clean and prominent
\titleformat{\section}
-{\color{WarlockRed}\normalfont\Large\bfseries\uppercase}
-{\thesection}{1em}{}[{\titlerule[1pt]}]
+ {\sffamily\Large\bfseries\color{WarlockRed}}
+ {\thesection \ \ $\cdot$ \ \ }{0pt}{#1}
+% Subsection Header: Technical
\titleformat{\subsection}
-{\color{WarlockDark}\normalfont\large\bfseries}
-{\thesubsection}{1em}{}
+ {\sffamily\large\bfseries\color{DocInk}}
+ {\thesubsection}{0.5em}{#1}
-% Custom Boxes for Information grouping
-\newtcolorbox{infobox}[1]{
- colback=WarlockDark!10!white,
- colframe=WarlockDark,
- fonttitle=\bfseries,
- title=#1,
- arc=0mm,
- boxrule=1pt,
- leftrule=4pt
+% Subsubsection Header: Subtle
+\titleformat{\subsubsection}
+ {\sffamily\normalsize\bfseries\color{WarlockGray}}
+ {\thesubsubsection}{0.5em}{#1}
+
+% --- MODERNIZED UI BOXES (CARDS) ---
+
+% Base style for all UI cards - sleek, no heavy borders, subtle shadow
+\tcbset{
+ mkui_card/.style={
+ enhanced,
+ breakable,
+ frame hidden, % No full border
+ interior style={fill=white}, % White background by default
+ boxrule=0pt,
+ leftrule=5pt, % Thick left border only
+ arc=3mm, % Softer corners
+ shadow={0mm}{0mm}{0mm}{0pt}{black!5}, % Reset shadow
+ fuzzy shadow={0mm}{-2mm}{3mm}{0.5mm}{black!15}, % Modern soft shadow
+ fonttitle=\sffamily\bfseries,
+ fontupper=\small,
+ toptitle=4pt, bottomtitle=4pt, left=10pt, right=10pt, top=10pt, bottom=10pt,
+ before skip=1.5em, after skip=1.5em,
+ title label={#1 \quad}
+ }
}
-\newtcolorbox{warningbox}[1]{
- colback=red!5!white,
- colframe=WarlockRed,
- fonttitle=\bfseries,
- title=\faExclamationTriangle\ #1,
- arc=0mm,
- boxrule=1pt,
- leftrule=4pt,
- coltitle=white
+% Concept Box (Gold/Yellow)
+\newtcolorbox{conceptbox}[1]{
+ mkui_card={\faLightbulb},
+ borderline west={5pt}{0pt}{WarlockGold},
+ coltitle=WarlockGold!30!black,
+ title={CONCEPT: #1},
+ colbacktitle=WarlockGold!10
}
+% Technical Deep Dive (Gray/Dark)
+\newtcolorbox{techbox}[1]{
+ mkui_card={\faCogs},
+ borderline west={5pt}{0pt}{WarlockPanel},
+ interior style={fill=WarlockPanel!5},
+ coltitle=WarlockText,
+ title={TECHNICAL DEEP DIVE: #1},
+ colbacktitle=WarlockPanel
+}
+
+% Warning Box (Red)
+\newtcolorbox{dangerbox}[1]{
+ mkui_card={\faExclamationTriangle},
+ borderline west={5pt}{0pt}{WarlockRed},
+ coltitle=WarlockRed!30!black,
+ title={CRITICAL WARNING: #1},
+ colbacktitle=WarlockRed!10
+}
+
+% Scenario/Solution Box (Blue)
+\newtcolorbox{scenariobox}[1]{
+ mkui_card={\faStethoscope},
+ borderline west={5pt}{0pt}{WarlockBlue},
+ coltitle=WarlockBlue!30!black,
+ title={SCENARIO: #1},
+ colbacktitle=WarlockBlue!10
+}
+
+% Tip Box (Green)
\newtcolorbox{tipbox}[1]{
- colback=WarlockGold!10!white,
- colframe=orange!80!black,
- fonttitle=\bfseries,
- title=\faLightbulb\ #1,
- arc=0mm,
- boxrule=1pt,
- leftrule=4pt,
- coltitle=black
+ mkui_card={\faInfoCircle},
+ borderline west={5pt}{0pt}{WarlockSuccess},
+ coltitle=WarlockSuccess!30!black,
+ title={PRO TIP: #1},
+ colbacktitle=WarlockSuccess!10
}
-% Hyperlink Setup
-\hypersetup{
- colorlinks=true,
- linkcolor=WarlockRed,
- filecolor=magenta,
- urlcolor=blue,
- pdftitle={Warlock-Studio Manual},
+% --- LISTINGS & CODE ---
+\lstset{
+ basicstyle=\ttfamily\footnotesize\color{DocInk},
+ backgroundcolor=\color{DocCodeBg},
+ breaklines=true,
+ frame=trbl, % full thin frame
+ frameround=ffff, % rounded corners
+ framesep=8pt,
+ framerule=0.5pt,
+ rulecolor=\color{WarlockGray!30},
+ keywordstyle=\color{WarlockBlue}\bfseries,
+ commentstyle=\color{WarlockSuccess}\itshape,
+ stringstyle=\color{WarlockGold!80!black},
+ numberstyle=\tiny\sffamily\color{WarlockGray!60},
+ numbers=left,
+ numbersep=10pt,
+ captionpos=b,
+ abovecaptionskip=10pt
}
+% --- MODERN TIKZ DIAGRAM STYLES ---
+\tikzset{
+ % Base node style with gradient and soft shadow
+ base_node/.style={
+ rectangle, rounded corners=4pt, minimum width=3cm, minimum height=1.2cm,
+ text centered, font=\sffamily\small, line width=0.8pt,
+ blur shadow={shadow xshift=0pt, shadow yshift=-2pt, shadow blur radius=4pt, shadow opacity=20}
+ },
+ startstop/.style={
+ base_node, draw=WarlockRed!80!black,
+ top color=WarlockRed!10, bottom color=WarlockRed!5, text=DocInk
+ },
+ process/.style={
+ base_node, draw=WarlockGray!80!black,
+ top color=white, bottom color=DocHeaderBg, text=DocInk
+ },
+ decision/.style={
+ diamond, aspect=2, minimum width=3.5cm, minimum height=1.5cm,
+ text centered, font=\sffamily\small\bfseries, line width=0.8pt,
+ draw=WarlockBlue!80!black, top color=WarlockBlue!10, bottom color=WarlockBlue!5, text=WarlockBlue!40!black,
+ blur shadow={shadow xshift=0pt, shadow yshift=-2pt, shadow blur radius=4pt, shadow opacity=20}
+ },
+ % Modern arrow style
+ arrow/.style={
+ draw=WarlockGray!80, line width=1.2pt, -{Latex[length=3mm, width=2mm]}, rounded corners=5pt
+ },
+ line/.style={draw, -latex', thick, color=DocInk}
+}
+
+% --- LIST STYLES ---
+\setlist[itemize,1]{label={\textcolor{WarlockRed}{\faAngleRight}}, leftmargin=1.5em, itemsep=0.4em}
+\setlist[itemize,2]{label={\textcolor{WarlockGray}{\faCircle[small]}}, leftmargin=1.5em, itemsep=0.2em}
+\setlist[enumerate,1]{label={\sffamily\bfseries\arabic*.}, font=\color{WarlockRed}, leftmargin=1.5em, itemsep=0.4em}
+
+
% =========================================================
% DOCUMENT CONTENT
% =========================================================
@@ -105,227 +264,487 @@
% --- COVER PAGE ---
\begin{titlepage}
- \begin{center}
- \vspace*{2cm}
- \includegraphics[width=0.4\textwidth]{logo.png}\\[1cm]
+ \pagecolor{WarlockBg}
+ \color{WarlockText}
+ \begin{tikzpicture}[remember picture, overlay]
+ % Subtle Technical Background Pattern
+ \begin{scope}[opacity=0.05, WarlockGray]
+ \draw[step=2cm, very thin] (current page.south west) grid (current page.north east);
+ \foreach \x in {0,10,...,\paperwidth} \draw[line width=2pt] (\x,0) -- (\x,\paperheight);
+ \end{scope}
+ % Dynamic diagonal accents
+ \draw[WarlockRed, line width=4pt, opacity=0.8] ($(current page.north west)+(2in,0)$) -- ($(current page.south west)+(0,4in)$);
+ \draw[WarlockGold, line width=2pt, opacity=0.6] ($(current page.north west)+(2.5in,0)$) -- ($(current page.south west)+(0,3.5in)$);
+ \end{tikzpicture}
- {\Huge \textbf{WARLOCK-STUDIO}}\\[0.5cm]
- {\Large \textit{AI Upscaling \& Interpolation Suite}}\\[0.2cm]
- {\large Version 5.1}
+ \begin{center}
+ \vspace*{4cm}
+ % Logo Area with Glow Effect - MODIFIED TO USE logo.png
+ \begin{tikzpicture}
+ % The glowing container circle
+ \node[circle, minimum size=4cm, fill=WarlockPanel, blur shadow={shadow blur radius=15pt, shadow opacity=40, shadow color=WarlockRed}] (logo) at (0,0) {};
+ % The actual image node inside the circle
+ \node at (0,0) {\includegraphics[width=3cm, height=3cm, keepaspectratio]{logo.png}};
+ \end{tikzpicture}
\vspace{2cm}
- \begin{tcolorbox}[colback=WarlockDark, colframe=WarlockRed, width=0.8\textwidth, arc=2mm]
- \centering \textcolor{white}{\textbf{\Large USER OPERATING MANUAL}}
+ {\sffamily\bfseries\fontsize{36}{36}\selectfont WARLOCK-STUDIO} \\[0.5cm]
+ {\sffamily\Large\textcolor{WarlockGray}{\textit{The Comprehensive AI Media Enhancement Suite}}} \\[1.5cm]
+
+ {\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}
+ \end{tcolorbox}
+ \vspace{2cm}
+
+ \begin{tcolorbox}[mkui_card, interior style={fill=WarlockPanel}, borderline west={4pt}{0pt}{WarlockGold}, width=0.9\textwidth]
+ \centering
+ \sffamily\textcolor{WarlockGray}{
+ Architecture \ $\cdot$ \ Workflows \ $\cdot$ \ Troubleshooting \ $\cdot$ \ Advanced Config}
\end{tcolorbox}
\vfill
- \textbf{Developed by Ivan-Ayub97}\\
- \today
+ \begin{flushright}
+ \sffamily\small
+ \textbf{Author:} Ivan-Ayub97 \\
+ \textcolor{WarlockGray}{Generated for Documentation Purposes} \\
+ \today
+ \end{flushright}
\end{center}
\end{titlepage}
+\pagecolor{white}
+\color{DocInk}
-\tableofcontents
+% --- TABLE OF CONTENTS ---
+{
+ \hypersetup{linkcolor=DocInk}
+ \sffamily
+ \tableofcontents
+}
\newpage
-% ---------------------------------------------------------
-% SECTION 1: INTRODUCTION
-% ---------------------------------------------------------
-\section{Introduction}
-Warlock-Studio is a comprehensive GUI application designed for AI-driven image and video enhancement. It integrates state-of-the-art neural networks to perform Upscaling, Denoising, Face Restoration, and Frame Interpolation.
+% =========================================================
+% CHAPTER 1: THE CORE PHILOSOPHY
+% =========================================================
+\chapter{The Core Philosophy}
-Built on Python, CustomTkinter, and ONNX Runtime, Warlock-Studio optimizes hardware resources (CPU and GPU) to deliver great results.
+\section{What is Warlock-Studio?}
+Warlock-Studio is not just an "app"; it is an \textbf{AI Orchestrator}.
-\begin{infobox}{System Requirements}
+In the world of AI, processing a single image or video involves dozens of complex steps: managing memory buffers, tiling large images to fit in VRAM, extracting frames, running neural network inference (ONNX), and re-encoding video streams.
+
+Warlock-Studio automates this entire pipeline. It acts as a bridge between high-performance libraries (OpenCV, MoviePy, ONNX Runtime) and the user, providing a "Drag \& Drop" experience for industrial-grade tasks.
+
+\section{Capabilities at a Glance}
\begin{itemize}
- \item \textbf{OS:} Windows 10/11 (x64)
- \item \textbf{RAM:} Minimum 8GB (16GB+ recommended)
- \item \textbf{GPU:} NVIDIA (CUDA), AMD/Intel (DirectML), or CPU (Slow fallback)
- \item \textbf{Dependencies:} FFmpeg (included in assets), Visual C++ Redistributable.
-\end{itemize}
-\end{infobox}
-
-% ---------------------------------------------------------
-% SECTION 2: INTERFACE & USAGE
-% ---------------------------------------------------------
-\section{Interface Overview & Usage}
-The interface is divided into functional blocks designed for a linear workflow: \textit{Load $\rightarrow$ Configure $\rightarrow$ Process}.
-
-\subsection{1. Input Section}
-Located on the left side (or top, depending on layout), this area handles file ingestion.
-\begin{itemize}
- \item \textbf{Drag \& Drop:} You can drag images or videos directly onto the window.
- \item \textbf{Manual Select:} Clicking the button opens a file dialog.
- \item \textbf{File List:} Selected files appear in a scrollable list showing resolution, duration, and calculated output resolution based on current settings.
-\end{itemize}
-
-\subsection{2. AI Configuration}
-This is the core control panel.
-\begin{description}
- \item[AI Model:] Selects the neural network architecture (see Chapter 3).
- \item[AI Multithreading:] Controls how many frames are processed simultaneously.
+ \item \textbf{Upscaling:} Increasing resolution while adding detail (up to 8K+).
+ \item \textbf{Restoration:} Removing noise (Denoising) and fixing broken faces (GFPGAN).
+ \item \textbf{FluidFrames (Interpolation):} Generating new frames to make video smoother (30fps $\to$ 60fps).
+ \item \textbf{Smart Rendering:}
\begin{itemize}
- \item \textit{Recommendation:} Set to "2 threads" for mid-range GPUs. Use "OFF" (1 thread) for high-resolution upscaling (4K) to save VRAM.
- \end{itemize}
- \item[Frame Generation (RIFE):] Only active when RIFE models are selected. Interpolates frames to increase smoothness (e.g., 30fps $\rightarrow$ 60fps).
-\end{description}
-
-\subsection{3. Hardware & Performance}
-\begin{itemize}
- \item \textbf{GPU Selection:} Choose specific GPU or "Auto".
- \item \textbf{VRAM Limiter:} \textbf{Crucial Setting.} This defines the tile size for processing.
- \begin{itemize}
- \item \textit{Integrated Graphics:} Set to 2GB or lower.
- \item \textit{Dedicated GPU (e.g., RTX 3060):} Set to match your card's VRAM (e.g., 6GB-8GB).
+ \item \textbf{Tiling Engine:} Processes images larger than your GPU memory.
+ \item \textbf{Auto-Fallback:} Automatically switches to CPU encoding if GPU encoding fails.
+ \item \textbf{Resume System:} Crashed during a 2-hour render? Warlock resumes from the exact frame.
\end{itemize}
\end{itemize}
-\begin{tipbox}{Tiling Technology}
-Warlock-Studio uses "Tiling". If an image is too large for VRAM, it splits the image into small squares, processes them, and merges them back. The \textbf{VRAM Limiter} controls the size of these squares.
-\end{tipbox}
-
-\subsection{4. Resolution Control}
-\begin{itemize}
- \item \textbf{Input Resolution \%:} Downscales the image \textit{before} AI processing. Useful for speeding up 4K video processing (e.g., set to 50\%).
- \item \textbf{Output Resolution \%:} Downscales the image \textit{after} AI processing.
-\end{itemize}
-
-\subsection{5. Output Settings}
-\begin{itemize}
- \item \textbf{Image Ext:} PNG (Lossless), JPG (Fast), BMP/TIFF (Uncompressed).
- \item \textbf{Video Ext:} MP4, MKV, AVI, MOV.
- \item \textbf{Video Codec:}
- \begin{itemize}
- \item \textbf{x264/x265:} CPU Encoding (High quality, slow).
- \item \textbf{NVENC:} NVIDIA Hardware (Fast).
- \item \textbf{AMF:} AMD Hardware.
- \item \textbf{QSV:} Intel Hardware.
- \end{itemize}
-\end{itemize}
-
-% ---------------------------------------------------------
-% SECTION 3: AI MODELS EXPLAINED
-% ---------------------------------------------------------
-\newpage
-\section{AI Models Library}
-Warlock-Studio includes varied models optimized for specific scenarios. Use this table to choose the right tool.
+\section{System Requirements Matrix}
+Strict adherence to these requirements ensures stability.
\begin{table}[h!]
\centering
-\renewcommand{\arraystretch}{1.3}
-\begin{tabularx}{\textwidth}{|l|l|X|}
-\hline
-\rowcolor{WarlockDark} \textcolor{white}{\textbf{Category}} & \textcolor{white}{\textbf{Model Name}} & \textcolor{white}{\textbf{Best Use Case}} \\ \hline
-\multirow{2}{*}{\textbf{Denoising}} & IRCNN\_Mx1 & Removing grain/noise without changing resolution. Fast. \\
- & IRCNN\_Lx1 & Heavier denoising for very grainy sources. \\ \hline
-\multirow{2}{*}{\textbf{Anime / Art}} & RealESR\_Animex4 & \textbf{Best for Cartoons/Anime.} Removes compression artifacts and sharpens lines. \\
- & RealESR\_Gx4 & General purpose fast upscaling. \\ \hline
-\multirow{4}{*}{\textbf{Realistic}} & BSRGANx4 & \textbf{Best for Real World video.} Adds texture and realistic details. \\
- & BSRGANx2 & 2x version of above. Slightly faster. \\
- & RealESRGANx4 & Good balance between sharpness and texture. \\
- & RealESRNetx4 & Smoother look, less texture hallucination. \\ \hline
-\textbf{Faces} & GFPGAN & \textbf{Face Restoration.} Miraculous recovery of blurry/small faces. \\ \hline
-\textbf{Interpolation} & RIFE / Lite & Increasing Frame Rate (30$\rightarrow$60fps). Creates intermediate frames. \\ \hline
+\sffamily\small
+\renewcommand{\arraystretch}{1.6}
+\setlength{\tabcolsep}{12pt}
+% Header row styling
+\rowcolors{1}{DocHeaderBg}{white}
+\begin{tabularx}{\textwidth}{l l X}
+\toprule
+\rowcolor{WarlockPanel} \textbf{\textcolor{WarlockGold}{Component}} & \textbf{\textcolor{WarlockText}{Minimum}} & \textbf{\textcolor{WarlockText}{Recommended}} \\
+\midrule
+\textbf{OS} & Windows 10 (64-bit) & Windows 11 (64-bit) \\
+\textbf{CPU} & Quad-Core (i5/Ryzen 5) & 8-Core+ (i7/Ryzen 7) for fast encoding \\
+\textbf{RAM} & 8 GB & \textbf{16 GB - 32 GB} (Crucial for 4K video) \\
+\textbf{GPU} & Integrated (Vega/Intel HD) & \textbf{NVIDIA RTX 3060} (or equivalent) with 8GB+ VRAM \\
+\textbf{Storage} & HDD (Slow) & \textbf{NVMe SSD} (Required for high-speed frame extraction) \\
+\textbf{Libraries} & Visual C++ Redist. & Latest GPU Drivers \\
+\bottomrule
\end{tabularx}
-\caption{Warlock-Studio Model Reference Guide}
+\caption{Hardware Compatibility Specification}
\end{table}
-\subsection*{When NOT to use certain models:}
+% =========================================================
+% CHAPTER 2: ANATOMY OF THE INTERFACE
+% =========================================================
+\chapter{Anatomy of the Interface}
+
+This chapter breaks down every pixel of the GUI. Warlock-Studio uses a \textbf{"Top-Down"} workflow design.
+
+\section{1. The Input Manager (File Queue)}
+Located on the left/top. It has two modes:
+
+\subsection{A. The Drop Zone}
+When empty, it shows a large "Drag \& Drop" area. It accepts:
\begin{itemize}
- \item Do \textbf{not} use \textit{RealESR\_Animex4} on realistic photos; it will make skin look like plastic (oil painting effect).
- \item Do \textbf{not} use \textit{GFPGAN} on non-human subjects or high-quality faces (it might alter facial features slightly).
+ \item \textbf{Images:} \texttt{.jpg, .png, .bmp, .tiff, .webp}
+ \item \textbf{Videos:} \texttt{.mp4, .mkv, .avi, .mov, .webm}
\end{itemize}
-% ---------------------------------------------------------
-% SECTION 4: WORKFLOW DIAGRAMS
-% ---------------------------------------------------------
-\section{Process Workflows}
+\subsection{B. The Active Queue}
+Once files are added, it transforms into a list. Each card displays:
+\begin{itemize}
+ \item \textbf{Thumbnail:} Visual verification of the file.
+ \item \textbf{Meta-Info:} Resolution, Duration, FPS, File Size.
+ \item \textbf{Projection:} A dynamic calculation showing:
+ \[ \text{Input Res} \xrightarrow{\text{Input \%}} \text{AI Input} \xrightarrow{\text{Upscale}} \text{AI Output} \xrightarrow{\text{Output \%}} \text{Final Res} \]
+\end{itemize}
-\subsection{Video Upscaling Pipeline}
-Understanding the internal process helps in troubleshooting speed issues.
+\begin{tipbox}{Queue Secrets}
+Use the \faArrowUp\ button to prioritize urgent files. Use the \faTimes\ button to remove files. Warlock processes files \textbf{sequentially} (top to bottom).
+\end{tipbox}
-\vspace{0.5cm}
+\section{2. The Toolbar (Global Tools)}
+Floating in the top right:
+\begin{itemize}
+ \item \textbf{\faBook\ Manual:} Opens this specific PDF document.
+ \item \textbf{\faCog\ Preferences:} Opens the \texttt{warlock\_preferences} window (See Chapter 6).
+\end{itemize}
+
+\section{3. The Control Panel (The Brain)}
+This is where you program the logic for the batch.
+
+\subsection{Row 1: AI Model Selection}
+The dropdown list contains categorized models. (See Chapter 3 for details).
+
+\subsection{Row 2: Contextual Action (Blending vs. Frame Gen)}
+This menu changes dynamically based on the AI Model selected.
+
+\begin{itemize}
+ \item \textbf{Scenario A: Upscaling Model Selected}
+ \begin{itemize}
+ \item \textbf{Menu:} \textit{AI Blending}
+ \item \textbf{Function:} Mixes the original image with the AI result to recover natural texture/grain.
+ \item \textbf{Values:} OFF (0\%), Low (30\%), Medium (50\%), High (70\%).
+ \end{itemize}
+
+ \item \textbf{Scenario B: RIFE (Interpolation) Selected}
+ \begin{itemize}
+ \item \textbf{Menu:} \textit{Frame Generation}
+ \item \textbf{Function:} Determines how many new frames to create.
+ \item \textbf{Values:} x2 (Double FPS), x4 (Quadruple FPS), Slowmotion (Increase frames but slow playback).
+ \end{itemize}
+\end{itemize}
+
+\subsection{Row 3: AI Multithreading}
+\textbf{Function:} Controls how many video frames are processed in parallel.
+\begin{itemize}
+ \item \textbf{OFF (1 Thread):} Safest. Essential for 4K video or low-VRAM GPUs.
+ \item \textbf{2 Threads:} Sweet spot for 1080p video on mid-range GPUs.
+ \item \textbf{4-8 Threads:} High VRAM usage. Only for RTX 3090/4090 processing SD content.
+\end{itemize}
+
+\subsection{Row 4: Resolution Orchestration}
+\begin{itemize}
+ \item \textbf{Input Resolution \%:} Downscales \textit{before} AI.
+ \item \textbf{Output Resolution \%:} Downscales \textit{after} AI.
+\end{itemize}
+
+\subsection{Row 5: Hardware Limits (The Safety Valve)}
+\begin{itemize}
+ \item \textbf{GPU Selection:} Choose specific card or "Auto".
+ \item \textbf{VRAM Limiter (GB):} \textbf{CRITICAL.} This defines the maximum tile size.
+\end{itemize}
+
+\section{4. The Integrated Console (The Voice)}
+New in v5.1.1. It provides a live feed of the backend operations.
+\begin{itemize}
+ \item \textcolor{WarlockSuccess}{\textbf{[SUCCESS]}}: Task completed.
+ \item \textcolor{WarlockBlue}{\textbf{[INFO]}}: Progress updates (e.g., "Extracting frames 45\%").
+ \item \textcolor{WarlockRed}{\textbf{[ERROR]}}: Critical failures (e.g., "Out of Memory").
+ \item \textbf{[FFMPEG]}: Raw encoding logs from the video engine.
+\end{itemize}
+
+% =========================================================
+% CHAPTER 3: THE MODEL LIBRARY
+% =========================================================
+\chapter{The Model Library: A Field Guide}
+
+Warlock-Studio includes a curated selection of ONNX models. Using the wrong model for the task is the \#1 cause of poor results.
+
+\begin{table}[h!]
+\centering
+\sffamily\small
+\renewcommand{\arraystretch}{1.4}
+\setlength{\tabcolsep}{10pt}
+\rowcolors{1}{DocHeaderBg}{white}
+\begin{tabularx}{\textwidth}{l l X X}
+\toprule
+\rowcolor{WarlockPanel}
+\textbf{\textcolor{WarlockGold}{Model}} &
+\textbf{\textcolor{WarlockText}{Type}} &
+\textbf{\textcolor{WarlockText}{BEST Use Case}} &
+\textbf{\textcolor{WarlockText}{DO NOT Use When...}} \\
+\midrule
+
+\textbf{RealESR\_Animex4} & Upscale (x4) & \textbf{2D Animation, Cartoons, Line Art.} Removes compression artifacts perfectly. & Input is a real photograph. It will turn skin into "plastic" or "oil paint". \\
+
+\textbf{RealESR\_Gx4} & Upscale (x4) & \textbf{General Purpose.} Good balance for art and clean photos. & Input is extremely noisy or grainy. \\
+
+\textbf{BSRGANx4} & Upscale (x4) & \textbf{Real World Video.} Adds realistic texture. Handles blur and JPEG compression well. & You need a perfectly clean, noiseless image (it generates grain). \\
+
+\textbf{BSRGANx2} & Upscale (x2) & Same as above, but faster. Ideal for 1080p $\to$ 4K. & Same as above. \\
+
+\textbf{RealESRGANx4} & Upscale (x4) & \textbf{Text \& Logos.} Extremely sharp edges. & Input contains faces (might look over-sharpened). \\
+
+\textbf{IRCNN\_M|Lx1} & Denoise (x1) & \textbf{Cleaning.} Removing noise before upscaling. & You want to increase resolution (it stays at x1). \\
+
+\textbf{GFPGAN} & Restoration & \textbf{Faces.} Restores eyes/mouths in blurry photos. & Image has no faces, or faces are already high quality (it might alter identity). \\
+
+\textbf{RIFE|RIFELite} & Interpolation & \textbf{Smoothing Video.} Increasing frame rate. & You want to increase resolution/sharpness. \\
+\bottomrule
+\end{tabularx}
+\caption{Comprehensive Model Usage Matrix}
+\end{table}
+
+% =========================================================
+% CHAPTER 4: ADVANCED CONCEPTS
+% =========================================================
+\chapter{Advanced Concepts (The Engine Room)}
+
+\section{Tiling: How to Process 8K on a 2GB Card}
+
+\begin{conceptbox}{What is Tiling?}
+Neural networks require massive amounts of memory. Processing a 4K image in one go might require 12GB of VRAM.
+
+\textbf{The Solution:} Warlock-Studio chops the image into smaller squares (Tiles), processes each square individually, and stitches them back together seamlessly.
+
+The \textbf{GPU VRAM (GB)} setting controls the size of these squares.
+\begin{itemize}
+ \item \textbf{High Setting (e.g., 8GB):} Large tiles. Fewer tiles to process. Faster speed.
+ \item \textbf{Low Setting (e.g., 2GB):} Small tiles. More tiles to process. Slower, but **impossible to crash**.
+\end{itemize}
+\end{conceptbox}
+
+\section{Auto-Fallback Encoding}
+Video encoding is prone to driver failures. Warlock-Studio implements a "Self-Healing" mechanism.
+
+\begin{itemize}
+ \item \textbf{Attempt 1:} The user selects a Hardware Encoder (e.g., \texttt{h264\_nvenc}).
+ \item \textbf{Failure:} The GPU driver crashes or is busy.
+ \item \textbf{Detection:} Warlock detects the non-zero exit code from FFmpeg.
+ \item \textbf{Attempt 2 (Auto):} The system immediately switches to \texttt{libx264} (CPU Encoder).
+ \item \textbf{Result:} The video renders successfully (albeit slower), preventing a "wasted night" of rendering.
+\end{itemize}
+
+% =========================================================
+% CHAPTER 5: WORKFLOWS
+% =========================================================
+\chapter{Workflows}
+
+\section{FluidFrames (RIFE Integration)}
+RIFE (Real-Time Intermediate Flow Estimation) works differently from Upscalers.
+
+\begin{figure}[H]
+\centering
+\begin{tikzpicture}[node distance=2cm, auto]
+ \node (frame1) [startstop, width=2.8cm] {Frame 1};
+ \node (frame2) [startstop, right of=frame1, xshift=5.5cm, width=2.8cm] {Frame 2};
+ \node (ai) [process, below of=frame1, xshift=3.75cm, width=4cm] {\sffamily\bfseries RIFE AI Analysis};
+ \node (newframe) [decision, below of=ai, yshift=-0.5cm] {Generate Frame 1.5};
+
+ \draw [arrow] (frame1.south) -- (ai.north);
+ \draw [arrow] (frame2.south) -- (ai.north);
+ \draw [arrow] (ai.south) -- (newframe.north);
+
+ % Modern dashed indicator
+ \draw [arrow, dashed, draw=WarlockBlue, line width=1.5pt] (newframe.east) -- ++(2,0) node[right, text width=2.5cm, align=left, font=\sffamily\small\color{WarlockBlue}] {Inserted seamlessly between 1 \& 2};
+\end{tikzpicture}
+\caption{RIFE Logic: Creating frames from thin air}
+\end{figure}
+
+\section{The Video Upscaling Pipeline}
\begin{center}
-\begin{tikzpicture}[node distance=1.5cm]
- \tikzstyle{startstop} = [rectangle, rounded corners, minimum width=3cm, minimum height=1cm,text centered, draw=black, fill=WarlockRed!30]
- \tikzstyle{process} = [rectangle, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=WarlockGold!30]
- \tikzstyle{decision} = [diamond, minimum width=3cm, minimum height=1cm, text centered, draw=black, fill=blue!10]
- \tikzstyle{arrow} = [thick,->,>=stealth]
+\begin{tikzpicture}[node distance=1.8cm, every node/.style={font=\sffamily\footnotesize}]
+ % Define a local style for this specific chart
+ \tikzset{pipe_arrow/.style={arrow, draw=WarlockGray!60, line width=2pt}}
- \node (in) [startstop] {Input Video};
- \node (extract) [process, below of=in] {Extract Frames (FFmpeg)};
- \node (resize1) [process, below of=extract] {Input Resize \%};
- \node (ai) [process, below of=resize1] {AI Inference (ONNX)};
- \node (blend) [decision, below of=ai, yshift=-0.5cm] {Blending?};
- \node (merge) [process, below of=blend, yshift=-0.5cm] {Merge Upscale + Original};
- \node (encode) [process, below of=merge] {Encode Video (FFmpeg)};
- \node (out) [startstop, below of=encode] {Output Video};
+ \node (start) [startstop] {User Clicks Start};
+ \node (check) [decision, below of=start, yshift=-0.2cm] {Is Video?};
- \draw [arrow] (in) -- (extract);
- \draw [arrow] (extract) -- (resize1);
- \draw [arrow] (resize1) -- (ai);
- \draw [arrow] (ai) -- (blend);
- \draw [arrow] (blend) -- node[anchor=east] {Yes} (merge);
- \draw [arrow] (blend.east) -- ++(1,0) |- node[anchor=south] {No} (encode.east);
- \draw [arrow] (merge) -- (encode);
- \draw [arrow] (encode) -- (out);
+ % Image Path
+ \node (img) [process, right of=check, xshift=4.5cm] {Process Image (Single Step)};
+ % Video Path
+ \node (extract) [process, below of=check, yshift=-0.8cm] {Extract Frames (FFmpeg)};
+
+ % Main Loop Container
+ \begin{scope}[on background layer]
+ \node[rectangle, draw=WarlockGold!50, dashed, line width=2pt, rounded corners=10pt, fill=WarlockGold!5, fit=(extract)(resize)(model)(blend)(save), label={[anchor=north west, font=\sffamily\bfseries\color{WarlockGold!80!black}, xshift=10pt, yshift=-5pt]Processing Loop}] (loopbox) {};
+ \end{scope}
+
+ \node (loop) [process, below of=extract] {Loop Through Frames};
+ \node (resize) [process, below of=loop] {Resize Input \%};
+ \node (model) [process, below of=resize, top color=WarlockRed!5, bottom color=WarlockRed!15, draw=WarlockRed!50] {\bfseries AI Inference (Tiling)};
+ \node (blend) [process, below of=model] {Blend / Post-Process};
+ \node (save) [process, below of=blend] {Save Temp Frame};
+
+ \node (checkdone) [decision, right of=save, xshift=4.5cm] {More Frames?};
+ \node (encode) [process, below of=save, yshift=-1cm] {Encode Video (FFmpeg)};
+ \node (done) [startstop, below of=encode] {Task Complete};
+
+ % Connections
+ \draw [pipe_arrow] (start) -- (check);
+ \draw [pipe_arrow] (check) -- node[above] {\textbf{No}} (img);
+ \draw [pipe_arrow] (check) -- node[right] {\textbf{Yes}} (extract);
+ \draw [pipe_arrow] (extract) -- (loop);
+ \draw [pipe_arrow] (loop) -- (resize);
+ \draw [pipe_arrow] (resize) -- (model);
+ \draw [pipe_arrow] (model) -- (blend);
+ \draw [pipe_arrow] (blend) -- (save);
+ \draw [pipe_arrow] (save) -- (checkdone);
+ % Loop back arrow
+ \draw [pipe_arrow, rounded corners=15pt] (checkdone.north) |- node[near start, right] {\textbf{Yes}} ($(loop.east)+(0.5,0)$) -- (loop.east);
+ \draw [pipe_arrow] (checkdone.south) |- node[near start, right] {\textbf{No}} (encode.east);
+ \draw [pipe_arrow] (encode) -- (done);
\end{tikzpicture}
\end{center}
-% ---------------------------------------------------------
-% SECTION 5: TROUBLESHOOTING
-% ---------------------------------------------------------
-\newpage
-\section{Troubleshooting & Error Codes}
+% =========================================================
+% CHAPTER 6: PREFERENCES (THE GEAR MENU)
+% =========================================================
+\chapter{Preferences \& Configuration}
-The integrated console (bottom of the app) provides real-time logs. Here are common errors and fixes.
+The \texttt{warlock\_preferences.py} module handles persistent settings.
-\subsection{Common Runtime Errors}
-
-\begin{table}[h!]
-\centering
-\begin{tabularx}{\textwidth}{|l|X|}
-\hline
-\rowcolor{WarlockRed} \textcolor{white}{\textbf{Error / Symptom}} & \textcolor{white}{\textbf{Solution}} \\ \hline
-\textbf{CUDA / Out of Memory} & The AI model requires more VRAM than available. \newline \textbf{Fix:} Lower the "GPU VRAM" setting (e.g., set to 2). Lower "AI Multithreading" to OFF. \\ \hline
-\textbf{FFmpeg not found} & The application cannot process video/audio. \newline \textbf{Fix:} Ensure \texttt{ffmpeg.exe} is in the \texttt{Assets/} folder. \\ \hline
-\textbf{Gray/Black Output} & Often caused by incompatible Video Codecs. \newline \textbf{Fix:} Switch output codec to \texttt{x264} (Software) or check GPU driver updates. \\ \hline
-\textbf{Process Stops Immediately} & File path issue. \newline \textbf{Fix:} Avoid special characters or emojis in filenames/folders. Move files to a simple path like \texttt{C:/Upscale/}. \\ \hline
-\textbf{DLL Load Failed} & Missing Visual C++ dependencies. \newline \textbf{Fix:} Install latest MSVC Redistributable. \\ \hline
-\end{tabularx}
-\end{table}
-
-\begin{warningbox}{Checkpoint Recovery}
-If the app crashes during a long video upscale, \textbf{do not delete the temporary folder}. Warlock-Studio will detect the processed frames and resume from where it left off automatically upon restarting the same job.
-\end{warningbox}
-
-\subsection{Performance Tuning Tips}
+\section{General Tab}
\begin{itemize}
- \item \textbf{Slow Speed?} Ensure "Process Priority" in Preferences is set to "High". Check if you are using CPU instead of GPU (Console will say \texttt{CPUExecutionProvider}).
- \item \textbf{Low Quality?} Try disabling "Blending" (set to OFF). Increase "Input Resolution \%" to 100.
- \item \textbf{Glitchy Video?} If using Interpolation (RIFE), scene changes might look weird. This is a limitation of current AI flow generation.
+ \item \textbf{App Theme:} Switch between Dark, Light, or System.
+ \item \textbf{UI Scaling:} Essential for 4K monitors (Set to 125\%).
+ \item \textbf{Window Opacity:} Aesthetic transparency.
+ \item \textbf{Auto-Close:} Shuts down the app after the queue finishes.
\end{itemize}
-% ---------------------------------------------------------
-% SECTION 6: PREFERENCES
-% ---------------------------------------------------------
-\section{Preferences Menu}
-Accessible via the \faCog\ icon in the top right.
-
+\section{Performance Tab}
\begin{itemize}
- \item \textbf{App Theme:} Switch between Dark/Light modes.
- \item \textbf{ONNX Provider:} Force specific backend (CUDA vs DirectML). \textit{Auto} is recommended.
- \item \textbf{Clean Temp Files:} Removes leftover \texttt{.tmp} files and frame folders from crashed sessions.
- \item \textbf{Extended Logging:} Enables detailed debug logs for error reporting.
+ \item \textbf{ONNX Provider:}
+ \begin{itemize}
+ \item \textit{Auto:} Recommended.
+ \item \textit{CUDA:} Forces NVIDIA acceleration.
+ \item \textit{DirectML:} Forces AMD/Intel acceleration.
+ \item \textit{CPU:} Debug mode (Slow).
+ \end{itemize}
+ \item \textbf{Process Priority:} Increases the Windows priority of the FFmpeg subprocess.
\end{itemize}
-\vspace{2cm}
+\section{Integrations Tab}
+Allows manual definition of paths for \texttt{ffmpeg.exe} and \texttt{exiftool.exe} if the auto-detection in \texttt{Assets/} fails.
+
+\section{Logs \& Maintenance}
+\begin{itemize}
+ \item \textbf{Clean Temp Files:} A "Panic Button" to clear disk space. Deletes all \texttt{*.tmp}, \texttt{*.checkpoint}, and temporary frame folders.
+ \item \textbf{Export Debug Zip:} Compresses all logs and config files into a ZIP to share with the developer for bug reporting.
+\end{itemize}
+
+% =========================================================
+% CHAPTER 7: TROUBLESHOOTING GUIDE (THE MECHANIC)
+% =========================================================
+\chapter{Troubleshooting \& Solutions}
+
+This section covers 90\% of user reports.
+
+\section{Crash Scenarios}
+
+\begin{scenariobox}{Crash: "Out of Memory" / "Allocation Failed"}
+\textbf{Symptoms:} The app closes instantly or the console turns red with memory errors.
+\textbf{Cause:} The AI is trying to create a Tile larger than available VRAM.
+\textbf{Solution:}
+\begin{enumerate}
+ \item Set \textbf{AI Multithreading} to \textbf{OFF}.
+ \item Lower \textbf{GPU VRAM (GB)} setting (e.g., from 4 to 2).
+ \item Reduce \textbf{Input Resolution \%} (e.g., to 75\%).
+\end{enumerate}
+\end{scenariobox}
+
+\begin{scenariobox}{Crash: "Timeout Detection Recovery (TDR)"}
+\textbf{Symptoms:} The screen flickers, goes black for a second, and the app reports an error.
+\textbf{Cause:} Windows reset the GPU driver because a single tile took too long to process.
+\textbf{Solution:} Lower the \textbf{GPU VRAM (GB)} setting drastically (e.g., to 1 or 2). Smaller tiles process faster, preventing the timeout.
+\end{scenariobox}
+
+\begin{scenariobox}{Stuck: "Extracting Frames..."}
+\textbf{Symptoms:} The progress bar doesn't move for minutes.
+\textbf{Cause:} The input video is huge (e.g., a 2-hour movie) or stored on a slow HDD.
+\textbf{Solution:} Be patient. Check the "Temp" folder in your file explorer to see if PNG files are being created. Move source files to an SSD.
+\end{scenariobox}
+
+\section{Quality Issues}
+
+\begin{scenariobox}{Result: "Oily" or "Plastic" Faces}
+\textbf{Cause:} Using \texttt{RealESR\_Animex4} on real photos.
+\textbf{Solution:} Switch to \texttt{RealESR\_Gx4} or \texttt{BSRGANx4}.
+\end{scenariobox}
+
+\begin{scenariobox}{Result: Weird artifacts in Video Motion}
+\textbf{Cause:} RIFE Interpolation struggles with scene cuts (sudden changes in camera).
+\textbf{Solution:} This is a limitation of current AI tech. Try \texttt{RIFE\_Lite} which is sometimes less aggressive.
+\end{scenariobox}
+
+\section{The Checkpoint System}
+\begin{dangerbox}{Do NOT delete Temp Files!}
+If Warlock-Studio crashes at frame 1500 of 2000:
+\begin{enumerate}
+ \item Re-open the app.
+ \item Load the \textbf{exact same video}.
+ \item Choose the \textbf{exact same settings}.
+ \item Click Start.
+\end{enumerate}
+The system will detect the existing frames and verify the \texttt{.checkpoint} file. It will output: \texttt{[INFO] Resuming video upscaling...} and start at frame 1501.
+\end{dangerbox}
+
+% =========================================================
+% CHAPTER 8: CREDITS
+% =========================================================
+\chapter*{Credits}
+\addcontentsline{toc}{chapter}{Credits}
+
+\begin{minipage}{0.55\textwidth}
+ \sffamily
+ \section*{Development Team}
+ \textbf{Lead Developer:} Ivan-Ayub97 \\
+
+ \section*{Core Technologies Stack}
+ We stand on the shoulders of giants.
+ \begin{itemize}
+ \item \textbf{UI Framework:} \textit{CustomTkinter} (TomSchimansky)
+ \item \textbf{AI Inference Engine:} \textit{ONNX Runtime} (Microsoft)
+ \item \textbf{Computer Vision:} \textit{OpenCV}
+ \item \textbf{Video Processing Backbone:} \textit{FFmpeg}
+ \end{itemize}
+\end{minipage}
+\hfill
+\begin{minipage}{0.4\textwidth}
+ \begin{tcolorbox}[mkui_card, interior style={fill=DocHeaderBg}, borderline west={4pt}{0pt}{WarlockGray}]
+ \centering
+ \color{DocInk}
+ \sffamily
+ \faCodeBranch \ \ \textbf{Open Source Philosophy} \\
+ \vspace{0.5em}
+ \footnotesize
+ Licensed under MIT. \\
+ Collaboration drives innovation.
+ \end{tcolorbox}
+\end{minipage}
+
+\vfill
\begin{center}
- \textit{Warlock-Studio is an open-source tool. \\ Thank you for using it.}
+ \sffamily
+ \textcolor{WarlockGray!50}{\rule{0.7\linewidth}{1pt}} \\
+ \vspace{0.8cm}
+ \textcolor{WarlockGray}{\textit{Warlock-Studio v5.1.1 | Engineering Documentation}}
\end{center}
\end{document}
diff --git a/README.md b/README.md
index 3dddeb3..7598546 100644
--- a/README.md
+++ b/README.md
@@ -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/)
@@ -55,22 +55,10 @@ Inspired by [Djdefrag](https://github.com/Djdefrag) tools such as **QualityScale
[GUI.webm](https://github.com/user-attachments/assets/45b06dd7-47ac-46b9-aa3d-c0baf483147b)
-
-

-
----
-
-## 🚀 Version 5.1 Highlights (Decoupling & Precision)
-
-Version 5.1 introduces significant structural and algorithmic improvements, transforming the application's stability and file handling efficiency.
-
-- **Asynchronous Architecture (New `FileQueueManager`):** File I/O operations (metadata reading, thumbnail generation) are now offloaded to background threads. This eliminates UI freezes (ANR) during batch processing and ensures a non-blocking user experience.
-- **Critical AI Correction:** Implemented **Dynamic Padding** for the RIFE frame interpolation algorithm. This surgically corrects a mathematical deficiency, guaranteeing artifact-free video output regardless of the source video's resolution (eliminates black/green edge artifacts).
-- **Preferences Redesign:** The Settings panel has been completely re-engineered into a modern **Sidebar Navigation** system, improving categorization and usability.
-- **Diagnostic Suite:** Added an integrated Real-Time Log Viewer and an automatic **Debug Package Export** tool to streamline error reporting.
-- **Enhanced Stability:** Introduced **Binary Path Overrides** for FFmpeg/ExifTool, rigorous thread-safe UI updates, and safer process shutdown handling.
-
+
+
+
---
@@ -96,12 +84,12 @@ Version 5.1 introduces significant structural and algorithmic improvements, tran
## 🖥️ System Requirements
-| Component | Minimum Specification | Recommended Specification |
-| :---------- | :-------------------- | :------------------------------------- |
-| **OS** | Windows 10 (64-bit) | Windows 11 (64-bit) |
-| **RAM** | 8 GB | 16 GB+ (Recommended for 4K Video) |
-| GPU | DirectX 12 Compatible | NVIDIA RTX 2060 / AMD RX 6700 XT |
-| **VRAM** | 4 GB | 8 GB+ (NEO Engine auto-tunes limits) |
+| Component | Minimum Specification | Recommended Specification |
+| :---------- | :-------------------- | :------------------------------------- |
+| **OS** | Windows 10 (64-bit) | Windows 11 (64-bit) |
+| **RAM** | 8 GB | 16 GB+ (Recommended for 4K Video) |
+| GPU | DirectX 12 Compatible | NVIDIA RTX 2060 / AMD RX 6700 XT |
+| **VRAM** | 4 GB | 8 GB+ (NEO Engine auto-tunes limits) |
| **Storage** | HDD Space | NVMe SSD (Highly recommended for RIFE) |
---
@@ -148,9 +136,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) |
-
-
-
-
-
-
diff --git a/Setup.iss b/Setup.iss
index 2b7f6be..3f369c4 100644
--- a/Setup.iss
+++ b/Setup.iss
@@ -1,64 +1,144 @@
+; =========================================================
+; WARLOCK-STUDIO – INSTALLER SCRIPT (STABLE)
+; =========================================================
+
+; ---------------------------------------------------------
+; FIRMA DIGITAL
+; ---------------------------------------------------------
+#define SignToolPath "C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\signtool.exe"
+#define CertPath "-------------------------------"
+#define CertPass "******"
+
+; ---------------------------------------------------------
+; DEFINICIONES
+; ---------------------------------------------------------
#define AppName "Warlock-Studio"
-#define AppVersion "5.1"
-#define AppPublisher "Ivan-Ayub97 on GH| Ivanayub1997 on SF"
+#define AppVersion "5.1.1"
+#define AppPublisher "Ivan-Ayub97"
#define AppURL "https://github.com/Ivan-Ayub97/Warlock-Studio"
#define AppExeName "Warlock-Studio.exe"
+#define SourcePath "..\Warlock-Studio-main"
+; =========================================================
+; SETUP
+; =========================================================
[Setup]
-; --- Configuración Básica ---
+
AppName={#AppName}
AppVersion={#AppVersion}
AppPublisher={#AppPublisher}
+AppPublisherURL={#AppURL}
AppSupportURL={#AppURL}
AppUpdatesURL={#AppURL}
+
+AppId={{***************************************}
+
+; ---- INSTALACIÓN EN DOCUMENTOS ----
DefaultDirName={userdocs}\{#AppName}
DefaultGroupName={#AppName}
AllowNoIcons=yes
-PrivilegesRequired=none
-AppId={{7CC447B5-CDCF-494D-A432-378B744C0EE6}
+PrivilegesRequired=lowest
-; --- Configuración del Instalador ---
+; ---- ASISTENTE ----
+WizardStyle=modern
+DisableWelcomePage=no
+DisableDirPage=no
+DisableProgramGroupPage=no
+
+; ---- SALIDA ----
OutputDir=Output
-; CHANGED: Filename reflects it's a full/offline installer
OutputBaseFilename=Warlock-Studio-{#AppVersion}-Full-Installer
-SetupIconFile=..\Warlock-Studio\logo.ico
Compression=lzma2/max
SolidCompression=yes
-WizardStyle=modern
+SetupLogging=yes
-; --- Imágenes del Asistente ---
-WizardImageFile=..\Warlock-Studio\Assets\wizard-image.bmp
-WizardSmallImageFile=..\Warlock-Studio\Assets\wizard-small.bmp
+; ---- ESTÉTICA ----
+SetupIconFile={#SourcePath}\logo.ico
UninstallDisplayIcon={app}\{#AppExeName}
+WizardImageFile={#SourcePath}\Assets\wizard-image.bmp
+WizardSmallImageFile={#SourcePath}\Assets\wizard-small.bmp
+; ---- FIRMA ----
+SignTool=MySignTool
+
+; =========================================================
+; LANGUAGES (SEGURO)
+; =========================================================
[Languages]
-Name: "english"; MessagesFile: "compiler:Default.isl"; LicenseFile: "..\Warlock-Studio\License.txt"
+Name: "english"; MessagesFile: "compiler:Default.isl"; LicenseFile: "{#SourcePath}\Assets\License.txt"
+; Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl"; LicenseFile: "{#SourcePath}\Assets\License.txt"
+; =========================================================
+; TASKS (OPCIONES DEL USUARIO)
+; =========================================================
[Tasks]
-; The download task has been removed as all files are now included.
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+Name: "desktopicon"; Description: "Create a desktop icon"; GroupDescription: "Additional options:"; Flags: unchecked
+Name: "autorun"; Description: "Run Warlock-Studio when Windows starts"; GroupDescription: "Startup:"; Flags: unchecked
+Name: "userdata"; Description: "Create user data folder in Documents"; GroupDescription: "Data:"; Flags: checkedonce
+; =========================================================
+; FILES
+; =========================================================
[Files]
-; --- Archivos Básicos de la Aplicación ---
-Source: "..\Warlock-Studio\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
-Source: "..\Warlock-Studio\logo.ico"; DestDir: "{app}"; Flags: ignoreversion
-; --- CHANGED: Package the entire '_internal' folder and its contents ---
-Source: "..\Warlock-Studio\_internal\*"; DestDir: "{app}\_internal"; Flags: recursesubdirs createallsubdirs
+Source: "{#SourcePath}\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "{#SourcePath}\logo.ico"; DestDir: "{app}"; Flags: ignoreversion
+Source: "{#SourcePath}\_internal\*"; DestDir: "{app}\_internal"; \
+Flags: ignoreversion recursesubdirs createallsubdirs
+
+Source: "{#SourcePath}\Assets\*"; DestDir: "{app}\Assets"; \
+Flags: ignoreversion recursesubdirs createallsubdirs
+
+; =========================================================
+; ICONS
+; =========================================================
[Icons]
-Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\logo.ico"; WorkingDir: "{app}"
-Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
-Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; IconFilename: "{app}\logo.ico"; WorkingDir: "{app}"; Tasks: desktopicon
-; --- REMOVED: The entire [Code] section for downloading is no longer needed. ---
+Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
+Name: "{group}\Uninstall {#AppName}"; Filename: "{uninstallexe}"
+Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; \
+Tasks: desktopicon
+
+; =========================================================
+; REGISTRY
+; =========================================================
+[Registry]
+
+Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \
+ValueType: string; ValueName: "{#AppName}"; \
+ValueData: """{app}\{#AppExeName}"""; Tasks: autorun
+
+; =========================================================
+; RUN
+; =========================================================
[Run]
-Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+Filename: "{app}\{#AppExeName}"; \
+Description: "Launch {#AppName}"; \
+Flags: nowait postinstall skipifsilent
+
+; =========================================================
+; UNINSTALL
+; =========================================================
[UninstallDelete]
-; This section is still needed to clean up the installed folder on uninstall.
Type: filesandordirs; Name: "{app}\_internal"
Type: filesandordirs; Name: "{app}\Assets"
+Type: filesandordirs; Name: "{userdocs}\{#AppName}\UserData"
-; --- REMOVED: [Messages] section related to downloading is no longer needed. ---
\ No newline at end of file
+; =========================================================
+; CODE
+; =========================================================
+[Code]
+
+procedure CurStepChanged(CurStep: TSetupStep);
+begin
+ if (CurStep = ssInstall) and WizardIsTaskSelected('userdata') then
+ begin
+ ForceDirectories(ExpandConstant('{userdocs}\{#AppName}\UserData'));
+ end;
+end;
+
+[SignTools]
+Name: "MySignTool"; Command: """{#SignToolPath}"" sign /f ""{#CertPath}"" /p ""{#CertPass}"" /fd sha256 /tr http://timestamp.digicert.com /td sha256 $f"
diff --git a/Warlock-Studio.py b/Warlock-Studio.py
index 9cd2547..a7fa5e2 100644
--- a/Warlock-Studio.py
+++ b/Warlock-Studio.py
@@ -24,6 +24,7 @@ from os import cpu_count as os_cpu_count
from os import devnull as os_devnull
from os import listdir as os_listdir
from os import makedirs as os_makedirs
+from os import path as os_path
from os import remove as os_remove
from os import sep as os_separator
from os.path import abspath as os_path_abspath
@@ -48,6 +49,7 @@ from webbrowser import open as open_browser
import customtkinter as ctk
import cv2
+import numpy as np
# ONNX Runtime imports
import onnxruntime
# GUI imports (CustomTkinter & TkinterDnD)
@@ -107,7 +109,8 @@ from console import IntegratedConsole, console
from drag_drop import DnDCTk, enable_drag_and_drop
# Importa la clase de tu archivo (asumiendo que se llama file_queue_manager.py)
from file_queue_manager import FileQueueManager
-from warlock_preferences import PreferencesButton # Importación local
+from splash_screen import SplashScreen
+from warlock_preferences import ConfigManager, PreferencesButton
# Redirigir inmediatamente para capturar logs de importación
console.setup_redirection()
@@ -129,7 +132,7 @@ def find_by_relative_path(relative_path: str) -> str:
app_name = "Warlock-Studio"
-version = "5.1"
+version = "5.1.1"
# Supported File Extensions
supported_image_extensions = [".jpg", ".jpeg",
@@ -147,33 +150,33 @@ supported_file_extensions = supported_image_extensions + supported_video_extensi
# -----------------------------------------------------------------------------
# Fondo: Negro casi puro, igual que el fondo del banner para máximo contraste
-background_color = "#0A0A0A"
+background_color = "#000000"
# Nombre de la app: Plata metálico, inspirado en el texto "STUDIO"
-app_name_color = "#FAF600"
+app_name_color = "#FBC02D"
# Paneles: Gris oscuro neutro, permite que el rojo y dorado resalten sin competir
-widget_background_color = "#303030"
+widget_background_color = "#1A1A1A"
# Texto principal: Blanco puro para legibilidad máxima
-text_color = "#FFFFFF"
+text_color = "#F5F5F5"
# Texto secundario: Dorado pálido/desaturado, para no cansar la vista pero mantener la identidad
-secondary_text_color = "#B5B4B4"
+secondary_text_color = "#9E9E9E"
# Acento: El amarillo dorado brillante del sombrero y los destellos (Sparkles)
-accent_color = "#FDEF2F"
+accent_color = "#FFC107"
# Hover de botones: El rojo vibrante del relleno del texto "WARLOCK"
-button_hover_color = "#D41C1C"
+button_hover_color = "#C62828"
# Bordes: Un dorado oscuro muy sutil, imitando el borde del logo sin ser chillón
-border_color = "#E2340D"
+border_color = "#2D2D2D"
# Botones info/secundarios: El rojo sangre oscuro del fondo del círculo del logo
-info_button_color = "#770000"
+info_button_color = "#7F1500"
# Advertencias: Naranja dorado, sacado del sombreado del sombrero
-warning_color = "#FFA000"
+warning_color = "#FF6F00"
# Éxito: Verde brillante, necesario para contraste funcional
-success_color = "#00E676"
+success_color = "#00C853"
# Error: Rojo carmesí intenso, similar al borde de las letras "WARLOCK"
-error_color = "#81091F"
+error_color = "#B71C1C"
# Resaltado: Amarillo luz, como el centro de los destellos (estrellas)
-highlight_color = "#FFFF8D"
+highlight_color = "#FFF59D"
# Scrollbars: Rojo vino oscuro translúcido, para mantener la temática sin distraer
-scrollbar_color = "#420505"
+scrollbar_color = "#000000"
# -----------------------------------------------------------------------------
# AI MODEL LISTS & CONFIGURATION
@@ -191,7 +194,7 @@ VRAM_model_usage = {
'GFPGAN': 1.8,
}
-MENU_LIST_SEPARATOR = ["• • • • • • • • • • • •"]
+MENU_LIST_SEPARATOR = ["•••"]
SRVGGNetCompact_models_list = ["RealESR_Gx4", "RealESR_Animex4"]
BSRGAN_models_list = ["BSRGANx4", "BSRGANx2", "RealESRGANx4", "RealESRNetx4"]
IRCNN_models_list = ["IRCNN_Mx1", "IRCNN_Lx1"]
@@ -228,8 +231,25 @@ OUTPUT_PATH_CODED = "Same path as input files"
DOCUMENT_PATH = os_path_join(os_path_expanduser('~'), 'Documents')
USER_PREFERENCE_PATH = find_by_relative_path(
f"{DOCUMENT_PATH}{os_separator}{app_name}_{version}_UserPreference.json")
-FFMPEG_EXE_PATH = find_by_relative_path(f"Assets{os_separator}ffmpeg.exe")
-EXIFTOOL_EXE_PATH = find_by_relative_path(f"Assets{os_separator}exiftool.exe")
+
+# --- INTEGRACIÓN DE PREFERENCIAS: RUTAS PERSONALIZADAS ---
+_app_config = ConfigManager.load_config()
+
+# Lógica FFmpeg
+_custom_ffmpeg = _app_config.get("custom_ffmpeg_path", "")
+if _custom_ffmpeg and os_path_exists(_custom_ffmpeg):
+ FFMPEG_EXE_PATH = _custom_ffmpeg
+else:
+ FFMPEG_EXE_PATH = find_by_relative_path(f"Assets{os_separator}ffmpeg.exe")
+
+# Lógica ExifTool
+_custom_exiftool = _app_config.get("custom_exiftool_path", "")
+if _custom_exiftool and os_path_exists(_custom_exiftool):
+ EXIFTOOL_EXE_PATH = _custom_exiftool
+else:
+ EXIFTOOL_EXE_PATH = find_by_relative_path(
+ f"Assets{os_separator}exiftool.exe")
+# ---------------------------------------------------------
ECTRACTION_FRAMES_FOR_CPU = 30
MULTIPLE_FRAMES_TO_SAVE = 8
@@ -330,6 +350,63 @@ little_menu_width = 98
# -----------------------------------------------------------------------------
def create_onnx_session(model_path: str, selected_gpu: str) -> InferenceSession:
+ """
+ Creates an ONNX inference session respecting User Preferences for backend execution.
+ """
+ if not os_path_exists(model_path):
+ raise FileNotFoundError(f"AI model file not found: {model_path}")
+
+ # Cargar preferencias
+ config = ConfigManager.load_config()
+ provider_pref = config.get("onnx_provider_preference", "Auto")
+
+ # Mapear selección de GUI a Device ID
+ device_id_map = {'GPU 1': 0, 'GPU 2': 1, 'GPU 3': 2, 'GPU 4': 3}
+ target_device_id = device_id_map.get(selected_gpu, 0)
+
+ # Definir opciones de proveedores
+ cuda_opts = {'device_id': target_device_id}
+ dml_opts = {'device_id': target_device_id}
+
+ # Construir lista de prioridad basada en preferencias
+ providers_to_try = []
+
+ if provider_pref == "CUDA":
+ providers_to_try.append(('CUDAExecutionProvider', cuda_opts))
+ elif provider_pref == "DirectML":
+ providers_to_try.append(('DmlExecutionProvider', dml_opts))
+ elif provider_pref == "CPU":
+ providers_to_try.append(('CPUExecutionProvider', None))
+ elif provider_pref == "OpenVINO":
+ providers_to_try.append(('OpenVINOExecutionProvider', None))
+ else: # AUTO
+ providers_to_try.append(('CUDAExecutionProvider', cuda_opts))
+ providers_to_try.append(('DmlExecutionProvider', dml_opts))
+ providers_to_try.append(('CPUExecutionProvider', None))
+
+ # Asegurar que siempre hay fallbacks si falla la preferencia principal
+ if ('CPUExecutionProvider', None) not in providers_to_try:
+ providers_to_try.append(('CPUExecutionProvider', None))
+
+ available_providers = onnxruntime.get_available_providers()
+
+ for provider, options in providers_to_try:
+ if provider in available_providers:
+ try:
+ session_options = [options] if options else None
+ session = InferenceSession(
+ path_or_bytes=model_path,
+ providers=[provider],
+ provider_options=session_options
+ )
+ print(
+ f"[AI] Loaded model with provider: {provider} (Pref: {provider_pref})")
+ return session
+ except Exception as e:
+ print(f"[AI WARNING] Failed to load {provider}: {e}")
+ continue
+
+ raise RuntimeError("Critical: Failed to load AI model with any provider.")
"""
Creates an ONNX inference session by selecting the best available provider.
Fixes: Correct type for device_id (int) and handles 'Auto' properly.
@@ -932,9 +1009,7 @@ class AI_upscale:
class AI_interpolation:
- # -------------------------------------------------------------------------
- # CLASS INIT
- # -------------------------------------------------------------------------
+ # CLASS INIT FUNCTIONS
def __init__(
self,
@@ -944,6 +1019,7 @@ class AI_interpolation:
input_resize_factor: int,
output_resize_factor: int,
):
+
# Passed variables
self.AI_model_name = AI_model_name
self.frame_gen_factor = frame_gen_factor
@@ -954,10 +1030,6 @@ class AI_interpolation:
# Calculated variables
self.AI_model_path = find_by_relative_path(
f"AI-onnx{os_separator}{self.AI_model_name}_fp32.onnx")
-
- # RIFE requiere múltiplos de 32 para evitar artefactos
- self.divisor = 32
-
self.inferenceSession = self._load_inferenceSession()
def _load_inferenceSession(self) -> InferenceSession:
@@ -969,18 +1041,18 @@ class AI_interpolation:
print(f"[AI ERROR] {error_msg}")
raise RuntimeError(error_msg)
- # -------------------------------------------------------------------------
- # INTERNAL UTILS
- # -------------------------------------------------------------------------
+ # INTERNAL CLASS FUNCTIONS
def get_image_mode(self, image: numpy_ndarray) -> str:
if image is None:
raise ValueError("Image is None")
shape = image.shape
- if len(shape) == 2:
+ if len(shape) == 2: # Grayscale: 2D array (rows, cols)
return "Grayscale"
+ # RGB: 3D array with 3 channels
elif len(shape) == 3 and shape[2] == 3:
return "RGB"
+ # RGBA: 3D array with 4 channels
elif len(shape) == 3 and shape[2] == 4:
return "RGBA"
else:
@@ -989,15 +1061,16 @@ class AI_interpolation:
def get_image_resolution(self, image: numpy_ndarray) -> tuple:
height = image.shape[0]
width = image.shape[1]
+
return height, width
def resize_with_input_factor(self, image: numpy_ndarray) -> numpy_ndarray:
+
old_height, old_width = self.get_image_resolution(image)
new_width = int(old_width * self.input_resize_factor)
new_height = int(old_height * self.input_resize_factor)
- # Mantenemos esto simple, el padding real se hace en la inferencia
new_width = new_width if new_width % 2 == 0 else new_width + 1
new_height = new_height if new_height % 2 == 0 else new_height + 1
@@ -1009,6 +1082,7 @@ class AI_interpolation:
return image
def resize_with_output_factor(self, image: numpy_ndarray) -> numpy_ndarray:
+
old_height, old_width = self.get_image_resolution(image)
new_width = int(old_width * self.output_resize_factor)
@@ -1024,42 +1098,7 @@ class AI_interpolation:
else:
return image
- # -------------------------------------------------------------------------
- # PADDING & CROPPING (CRITICAL FIX FOR RIFE STRIPES)
- # -------------------------------------------------------------------------
-
- def pad_image_to_divisor(self, image: numpy_ndarray) -> tuple[numpy_ndarray, int, int]:
- """
- Añade bordes negros a la imagen para que sus dimensiones sean múltiplos de self.divisor (32).
- Retorna la imagen con padding y las dimensiones del padding añadido.
- """
- h, w = image.shape[:2]
-
- # Calcular cuánto falta para llegar al siguiente múltiplo de 32
- pad_h = (self.divisor - (h % self.divisor)) % self.divisor
- pad_w = (self.divisor - (w % self.divisor)) % self.divisor
-
- if pad_h == 0 and pad_w == 0:
- return image, 0, 0
-
- # Aplicar padding (top, bottom, left, right) -> Solo rellenamos abajo y derecha
- # Usamos cv2.BORDER_REFLECT o BORDER_REPLICATE para reducir artefactos en bordes
- image_padded = cv2.copyMakeBorder(
- image, 0, pad_h, 0, pad_w, cv2.BORDER_REFLECT)
-
- return image_padded, pad_h, pad_w
-
- def crop_padding(self, image: numpy_ndarray, pad_h: int, pad_w: int) -> numpy_ndarray:
- """Recorta la imagen para eliminar el padding añadido previamente."""
- if pad_h == 0 and pad_w == 0:
- return image
-
- h, w = image.shape[:2]
- return image[:h-pad_h, :w-pad_w]
-
- # -------------------------------------------------------------------------
- # AI CORE FUNCTIONS
- # -------------------------------------------------------------------------
+ # AI CLASS FUNCTIONS
def concatenate_images(self, image1: numpy_ndarray, image2: numpy_ndarray) -> numpy_ndarray:
# Optimización: Normalizar in-place para reducir uso de memoria
@@ -1092,34 +1131,14 @@ class AI_interpolation:
case _: return (onnx_output * 255).astype(uint8)
def AI_interpolation(self, image1: numpy_ndarray, image2: numpy_ndarray) -> numpy_ndarray:
- """
- Ejecuta la interpolación asegurando dimensiones correctas.
- """
- # 1. Aplicar Padding a ambas imágenes (Critical Fix)
- img1_padded, pad_h, pad_w = self.pad_image_to_divisor(image1)
- # Asumimos que img2 tiene el mismo tamaño que img1
- img2_padded, _, _ = self.pad_image_to_divisor(image2)
-
- # 2. Preprocesamiento estándar
- image = self.concatenate_images(
- img1_padded, img2_padded).astype(float32)
+ image = self.concatenate_images(image1, image2).astype(float32)
image = self.preprocess_image(image)
-
- # 3. Inferencia
onnx_output = self.onnxruntime_inference(image)
-
- # 4. Postprocesamiento
onnx_output = self.postprocess_output(onnx_output)
- output_image_padded = self.de_normalize_image(onnx_output, 255)
-
- # 5. Eliminar Padding (Critical Fix)
- output_image = self.crop_padding(output_image_padded, pad_h, pad_w)
-
+ output_image = self.de_normalize_image(onnx_output, 255)
return output_image
- # -------------------------------------------------------------------------
- # ORCHESTRATION
- # -------------------------------------------------------------------------
+ # EXTERNAL FUNCTION
def AI_orchestration(self, image1: numpy_ndarray, image2: numpy_ndarray) -> List[numpy_ndarray]:
"""Generate interpolated frames between two input images."""
@@ -5713,131 +5732,6 @@ class App():
place_upscale_button()
-class SplashScreen(CTkToplevel):
- def __init__(self):
- super().__init__()
-
- # Configure window
- self.title("Warlock-Studio")
- self.overrideredirect(True)
- # Remove window decorations
- self.attributes('-topmost', True)
-
- # Calculate window position for center of screen
- screen_width = self.winfo_screenwidth()
- screen_height = self.winfo_screenheight()
- default_width = int(screen_width * 0.4)
- default_height = int(screen_height * 0.3)
- self.geometry(f"{default_width}x{default_height}")
-
- # Set default window size
- window_width = 460
- window_height = 340
-
- # Try to load banner image
- banner_path = find_by_relative_path(f"Assets{os_separator}banner.png")
- try:
- self.banner_image = CTkImage(
- pillow_image_open(banner_path),
- size=(450, 200) # Adjust size as needed
- )
- has_banner = True
- except Exception as e:
- print(f"[SPLASH] Could not load splash banner: {e}")
- has_banner = False
- window_height = 400 # Smaller height if no banner
-
- # Center window
- x = (screen_width - window_width) // 2
- y = (screen_height - window_height) // 2
- self.geometry(f"{window_width}x{window_height}+{x}+{y}")
-
- # Configure appearance to match app
- # Usar color de fondo definido
- self.configure(fg_color=background_color)
-
- # Create banner or title
- if has_banner:
- self.banner_label = CTkLabel(
- self,
- image=self.banner_image,
- text=""
- )
- self.banner_label.pack(pady=(30, 15))
- else:
- # Fallback to text title if image not found
- title_label = CTkLabel(
- self,
- text="Warlock-Studio",
- font=CTkFont(family="Segoe UI", size=28, weight="bold"),
- text_color=app_name_color # Usar color del nombre de la app
- )
- title_label.pack(pady=(50, 20))
-
- # Create status frame with progress messages
- status_frame = CTkFrame(
- self,
- fg_color=widget_background_color, # Usar color de widget definido
- corner_radius=10
- )
- status_frame.pack(pady=10, padx=20, fill="x")
-
- self.status_label = CTkLabel(
- status_frame,
- text="Loading AI-ONNX models...",
- font=CTkFont(family="Segoe UI", size=12, weight="bold"),
- text_color=accent_color # Usar color amarillo para el texto de estado
- )
- self.status_label.pack(pady=10, padx=10)
-
- # Create version label
- version_label = CTkLabel(
- self,
- text=f"Version {version} Developed by Ivan-Ayub97",
- font=CTkFont(family="Segoe UI", size=10),
- text_color=secondary_text_color # Usar color de texto secundario
- )
- version_label.pack(pady=(0, 10))
-
- # Define enough messages to fill 15 seconds (~1.5s por mensaje)
- self.messages = [
- "Preparing environment...",
- "Loading AI-ONNX models...",
- "Initializing FFmpeg...",
- "Almost ready..."
- ]
-
- # Start loading animation
- self._loading_step = 0
- self.update_loading_text()
-
- # Splash duration: 10 seconds
- self.after(10000, self.start_fade_out)
-
- def update_loading_text(self):
- """Update the loading message every 1.5 seconds"""
- if self._loading_step < len(self.messages):
- self.status_label.configure(text=self.messages[self._loading_step])
- self._loading_step += 1
- self.after(1500, self.update_loading_text)
-
- def start_fade_out(self):
- """Start the fade out animation"""
- self._fade_step = 1.0
- self.fade_out()
-
- def fade_out(self):
- """Smoothly fade out the splash screen"""
- if self._fade_step > 0:
- # Use cosine for smooth fade
- opacity = cos((1.0 - self._fade_step) * pi/2)
- self.attributes('-alpha', opacity)
- self._fade_step -= 0.05
- self.after(40, self.fade_out)
- else:
- self.destroy()
-
-
def log_startup_info():
"""
Imprime la información de inicio una vez que la consola gráfica está activa.
@@ -5953,11 +5847,35 @@ if __name__ == "__main__":
# Imprimir la info de inicio (saldrá en la nueva consola integrada)
log_startup_info()
- # Mostrar Splash Screen
- splash = SplashScreen()
- # Programar mostrar la ventana principal después del splash (11 segundos)
- window.after(11000, window.deiconify)
+ # ------------------------------------------------------------
+ # CONFIGURACIÓN DEL SPLASH SCREEN (CORREGIDO)
+ # ------------------------------------------------------------
+ # 1. Empaquetar colores para el módulo externo
+ splash_theme = {
+ 'bg': background_color,
+ 'widget_bg': widget_background_color,
+ 'accent': accent_color,
+ 'app_name': app_name_color,
+ 'text_sec': secondary_text_color
+ }
+
+ # 2. Instanciar SplashScreen pasando los argumentos requeridos
+ # Esto evita el TypeError que estabas teniendo
+ splash = SplashScreen(
+ root_window=window, # <--- CAMBIO AQUÍ (antes root_window=window)
+ app_title=app_name,
+ version=version,
+ asset_loader=find_by_relative_path, # Función para buscar assets
+ theme_colors=splash_theme, # Diccionario de colores
+ duration_ms=6000 # Duración: 6 segundos
+ )
+
+ # 3. Programar aparición de la ventana principal
+ # Se añade un pequeño retardo extra (6500ms) sobre la duración del splash (6000ms)
+ window.after(6500, window.deiconify)
+
+ # ------------------------------------------------------------
# Inicialización de Variables de UI
info_message = StringVar()
selected_output_path = StringVar()
diff --git a/Warlock-Studio.spec b/Warlock-Studio.spec
index 175a878..1bc2e5f 100644
--- a/Warlock-Studio.spec
+++ b/Warlock-Studio.spec
@@ -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'],
+ ['Warlock-Studio.py', 'drag_drop.py', 'console.py', 'warlock_preferences.py', 'file_queue_manager.py', 'splash_screen.py', 'warlock_theme.py'],
pathex=[],
binaries=[],
datas=[
diff --git a/desktop.ini b/desktop.ini
new file mode 100644
index 0000000..ff58df7
--- /dev/null
+++ b/desktop.ini
@@ -0,0 +1,2 @@
+[LocalizedFileNames]
+warlock_preferences.py=@warlock_preferences.py,0
diff --git a/file_queue_manager.py b/file_queue_manager.py
index f123282..c004dde 100644
--- a/file_queue_manager.py
+++ b/file_queue_manager.py
@@ -1,7 +1,6 @@
import logging
import os
import queue
-import threading
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, List, Optional, Tuple
@@ -13,12 +12,21 @@ from PIL import Image
# --- CONFIGURACIÓN Y ESTILOS ---
class Theme:
- BG_COLOR = "#101010" # Fondo general un poco más oscuro
- ITEM_BG = "#3A3A3A" # Fondo de cada tarjeta
+ BG_COLOR = "#101010" # Fondo general
+ ITEM_BG = "#3A3A3A" # Fondo de la tarjeta (sin procesar)
+
+ # Nuevo: Colores para la barra de progreso y estados
+ PROGRESS_BG = "#2E7D32" # Verde oscuro para el fondo de progreso (permite leer texto blanco)
+ COMPLETED_BG = "#1B5E20" # Verde más oscuro para completado
+ PROCESSING_BORDER = "#F7F7F7" # Borde blanco/dorado cuando está procesando
+
TEXT_MAIN = "#FFFFFF"
- TEXT_SEC = "#FDEF2F"
- ACCENT = "#F7F7F7" # Dorado original
- ERROR = "#CF6679" # Rojo suave para errores
+ TEXT_SEC = "#FDEF2F" # Amarillo (info secundaria)
+ TEXT_DONE = "#AAAAAA" # Gris para archivos completados
+
+ ACCENT = "#F7F7F7" # Dorado/Blanco principal
+ ERROR = "#CF6679" # Rojo error
+
BTN_HOVER_DANGER = "#B00020"
BTN_HOVER_NORMAL = "#4A4A4A"
BORDER_COLOR = "#555555"
@@ -28,7 +36,7 @@ class Theme:
FONT_MONO = ("Consolas", 10)
-# Configuración de logging para debug
+# Configuración de logging
logging.basicConfig(level=logging.ERROR,
format='%(asctime)s - %(levelname)s - %(message)s')
@@ -41,9 +49,14 @@ class QueueItem:
self.name = os.path.basename(path)
self.info_text = "Cargando info..."
self.thumbnail: Optional[ctk.CTkImage] = None
+
+ # Estado
self.is_loaded = False
self.has_error = False
- self.id = id(self) # Identificador único para referenciar widgets
+ self.is_completed = False
+ self.progress = 0.0 # 0.0 a 1.0
+
+ self.id = id(self) # Identificador único
class FileQueueManager(ctk.CTkScrollableFrame):
@@ -55,35 +68,38 @@ class FileQueueManager(ctk.CTkScrollableFrame):
# Estado interno
self.queue_items: List[QueueItem] = []
- # Mapa para acceder a widgets por ID de item
+ # Mapa para acceder a widgets por ID de item o por PATH
self._widget_refs: Dict[int, dict] = {}
+ self._path_to_id: Dict[str, int] = {}
# Factores de redimensionado
self.upscale_factor = 1
self.input_resize_factor = 0
self.output_resize_factor = 0
- # Sistema de hilos para no congelar la GUI
+ # Sistema de hilos
self.executor = ThreadPoolExecutor(max_workers=2)
self.msg_queue = queue.Queue()
# Grid layout básico
self.grid_columnconfigure(0, weight=1)
- # Iniciar loop de verificación de mensajes (para actualizaciones desde hilos)
+ # Iniciar loop de verificación
self._check_queue()
def _check_queue(self):
- """Revisa la cola de mensajes para actualizar la UI desde el hilo principal."""
+ """Revisa la cola de mensajes para actualizar la UI desde hilos secundarios."""
try:
while True:
task_type, item_id, data = self.msg_queue.get_nowait()
- if task_type == "UPDATE_ITEM":
- self._update_item_ui(item_id, data)
+ if task_type == "UPDATE_ITEM_DATA":
+ self._update_item_ui_data(item_id, data)
+ elif task_type == "UPDATE_PROGRESS":
+ self._render_progress_update(item_id, data)
except queue.Empty:
pass
finally:
- self.after(100, self._check_queue)
+ self.after(50, self._check_queue) # Revisar cada 50ms para fluidez
def add_files(self, file_paths: List[str]):
"""Agrega archivos y lanza la carga en segundo plano."""
@@ -95,90 +111,104 @@ class FileQueueManager(ctk.CTkScrollableFrame):
if norm_path not in existing_paths and os.path.exists(path):
new_item = QueueItem(norm_path)
self.queue_items.append(new_item)
+
+ # Mapeo rápido Path -> ID
+ self._path_to_id[norm_path] = new_item.id
added_any = True
# Renderizar placeholder inmediatamente
self._render_item(new_item, index=len(self.queue_items)-1)
- # Lanzar tarea pesada en background
+ # Lanzar tarea pesada
self.executor.submit(self._worker_load_info, new_item)
if added_any:
self._update_layout_indices()
def _worker_load_info(self, item: QueueItem):
- """Método que corre en otro hilo (Thread) para procesar OpenCV."""
+ """Worker en segundo plano para leer metadatos e imágenes."""
try:
info_text, pil_img = self._extract_file_data(item.path)
- # Crear CTkImage debe hacerse preferiblemente al volver al main,
- # pero PIL Image es seguro pasarlo.
- item.info_text = info_text
- item.is_loaded = True
-
- # Enviamos resultados al hilo principal
- self.msg_queue.put(
- ("UPDATE_ITEM", item.id, {"info": info_text, "img": pil_img}))
-
+ # Pre-calcular CTkImage en el hilo principal enviando el PIL
+ self.msg_queue.put(("UPDATE_ITEM_DATA", item.id, {
+ "info": info_text,
+ "img_pil": pil_img,
+ "is_loaded": True
+ }))
except Exception as e:
logging.error(f"Error procesando {item.path}: {e}")
- item.has_error = True
- item.info_text = "Error al leer archivo"
- self.msg_queue.put(("UPDATE_ITEM", item.id, {"error": True}))
+ self.msg_queue.put(("UPDATE_ITEM_DATA", item.id, {"error": True}))
- def _update_item_ui(self, item_id, data):
- """Actualiza un widget específico una vez que el hilo terminó."""
+ def _update_item_ui_data(self, item_id, data):
+ """Actualiza metadatos (texto/icono) tras la carga."""
widgets = self._widget_refs.get(item_id)
- if not widgets:
+ item = next((x for x in self.queue_items if x.id == item_id), None)
+
+ if not widgets or not item:
return
if "error" in data:
- widgets['lbl_info'].configure(
- text="⚠️ Archivo corrupto o ilegible", text_color=Theme.ERROR)
+ item.has_error = True
+ widgets['lbl_info'].configure(text="⚠️ Error de lectura", text_color=Theme.ERROR)
+ widgets['card'].configure(border_color=Theme.ERROR)
return
- # Actualizar texto
- widgets['lbl_info'].configure(text=data["info"])
+ # Actualizar datos del objeto
+ if "info" in data:
+ item.info_text = data["info"]
+ widgets['lbl_info'].configure(text=data["info"])
- # Actualizar imagen si existe
- if data["img"]:
- ctk_img = ctk.CTkImage(data["img"], size=data["img"].size)
- # Guardamos referencia en el objeto item para que no se pierda
- item = next((x for x in self.queue_items if x.id == item_id), None)
- if item:
- item.thumbnail = ctk_img
- widgets['lbl_icon'].configure(
- image=ctk_img, text="") # Quitar texto placeholder
+ if "img_pil" in data and data["img_pil"]:
+ # Crear CTkImage en el hilo principal (obligatorio por Tkinter)
+ ctk_img = ctk.CTkImage(data["img_pil"], size=data["img_pil"].size)
+ item.thumbnail = ctk_img
+ widgets['lbl_icon'].configure(image=ctk_img, text="")
+
+ item.is_loaded = True
+
+ # --- RENDERIZADO VISUAL ---
def _render_item(self, item: QueueItem, index: int):
- """Dibuja una tarjeta para el archivo."""
- row_idx = index + 1 # +1 para dejar espacio al botón Clean arriba si fuera necesario
+ """Dibuja la tarjeta del archivo con soporte para barra de progreso de fondo."""
+ row_idx = index + 1
- # Frame contenedor (Tarjeta)
+ # 1. Frame contenedor (Tarjeta)
card = ctk.CTkFrame(self, fg_color=Theme.ITEM_BG, corner_radius=8,
border_width=1, border_color=Theme.BORDER_COLOR)
card.grid(row=row_idx, column=0, sticky="ew", padx=10, pady=5)
card.grid_columnconfigure(1, weight=1)
- # 1. Icono / Thumbnail
- lbl_icon = ctk.CTkLabel(card, text="⏳", width=64,
- height=64, fg_color="#222", corner_radius=6)
+ # 2. Barra de Progreso (Fondo dinámico)
+ # Se coloca con .place() para estar "detrás" del contenido del grid,
+ # pero para que funcione, los widgets encima deben tener fg_color="transparent".
+ progress_bar = ctk.CTkFrame(card, fg_color=Theme.PROGRESS_BG, corner_radius=8, height=0)
+ # Inicialmente ancho 0. relheight=1 ocupa toda la altura de la tarjeta.
+ progress_bar.place(relx=0, rely=0, relheight=1, relwidth=item.progress)
+
+ # 3. Contenido (Grid)
+
+ # Icono / Thumbnail
+ lbl_icon = ctk.CTkLabel(card, text="⏳", width=64, height=64,
+ fg_color="#222", corner_radius=6)
lbl_icon.grid(row=0, column=0, rowspan=2, padx=8, pady=8)
- # Si ya teníamos la imagen (ej: reordenando lista), la ponemos directo
if item.thumbnail:
lbl_icon.configure(image=item.thumbnail, text="")
+ elif item.is_completed:
+ lbl_icon.configure(text="✓")
- # 2. Información
- lbl_name = ctk.CTkLabel(
- card, text=item.name, font=Theme.FONT_MAIN, text_color=Theme.ACCENT, anchor="w")
+ # Nombre
+ lbl_name = ctk.CTkLabel(card, text=item.name, font=Theme.FONT_MAIN,
+ text_color=Theme.TEXT_MAIN, anchor="w", fg_color="transparent")
lbl_name.grid(row=0, column=1, sticky="w", padx=5, pady=(8, 0))
+ # Info técnica
lbl_info = ctk.CTkLabel(card, text=item.info_text, font=Theme.FONT_MONO,
- text_color=Theme.TEXT_SEC, justify="left", anchor="w")
+ text_color=Theme.TEXT_SEC, justify="left", anchor="w", fg_color="transparent")
lbl_info.grid(row=1, column=1, sticky="w", padx=5, pady=(0, 8))
- # 3. Controles (Frame derecho)
+ # 4. Controles (Botones)
ctrl_frame = ctk.CTkFrame(card, fg_color="transparent")
ctrl_frame.grid(row=0, column=2, rowspan=2, padx=8, sticky="e")
@@ -186,7 +216,6 @@ class FileQueueManager(ctk.CTkScrollableFrame):
btn_up = ctk.CTkButton(ctrl_frame, text="▲", width=28, height=28,
fg_color="transparent", border_width=1, border_color=Theme.TEXT_SEC,
hover_color=Theme.BTN_HOVER_NORMAL, text_color=Theme.TEXT_MAIN,
- font=("Arial", 12),
command=lambda: self.move_up(item))
btn_up.pack(side="left", padx=2)
@@ -198,28 +227,33 @@ class FileQueueManager(ctk.CTkScrollableFrame):
command=lambda: self.remove_item(item))
btn_del.pack(side="left", padx=2)
- # Guardar referencias para actualizaciones asíncronas
+ # 5. Guardar referencias
self._widget_refs[item.id] = {
"card": card,
+ "progress_bar": progress_bar,
"lbl_info": lbl_info,
+ "lbl_name": lbl_name,
"lbl_icon": lbl_icon,
- "btn_up": btn_up # Guardamos por si queremos deshabilitar el primero
+ "btn_del": btn_del,
+ "btn_up": btn_up
}
- # Renderizar encabezado si es el primer elemento
+ # Aplicar estado visual si ya estaba completado (útil al redibujar)
+ if item.is_completed:
+ self._apply_completed_style(item.id)
+
+ # Renderizar encabezado si es el primero
if index == 0:
self._render_header()
def _render_header(self):
- """Dibuja el botón de limpiar todo si hay elementos."""
- # Limpiar header anterior si existe
+ """Dibuja el botón de limpiar todo."""
for w in self.grid_slaves(row=0):
w.destroy()
if self.queue_items:
header_frame = ctk.CTkFrame(self, fg_color="transparent")
- header_frame.grid(row=0, column=0, sticky="ew",
- padx=10, pady=(5, 0))
+ header_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(5, 0))
ctk.CTkLabel(header_frame, text=f"Queue: {len(self.queue_items)} Files",
font=Theme.FONT_SUB, text_color=Theme.TEXT_SEC).pack(side="left")
@@ -231,34 +265,127 @@ class FileQueueManager(ctk.CTkScrollableFrame):
command=self.clean_file_list).pack(side="right")
def _update_layout_indices(self):
- """Re-dibuja toda la lista. Útil para reordenar o borrar."""
- # Nota: En una app muy grande esto sería ineficiente, pero para <100 archivos está bien
- # destruir y recrear widgets garantiza orden visual correcto.
-
- # Limpiar UI actual
+ """Redibuja toda la lista para mantener orden visual."""
for widget in self.winfo_children():
widget.destroy()
self._widget_refs.clear()
- # Redibujar
if self.queue_items:
self._render_header()
for i, item in enumerate(self.queue_items):
self._render_item(item, i)
else:
- # Lista vacía
if self.on_queue_empty_callback:
self.on_queue_empty_callback()
- # --- LÓGICA DE DATOS ---
+ # --- API PÚBLICA PARA ACTUALIZACIÓN DE PROGRESO (Desde Warlock-Studio) ---
+
+ def update_file_progress_by_path(self, file_path: str, progress: float):
+ """
+ Actualiza la barra de progreso de un archivo específico.
+ :param file_path: Ruta del archivo.
+ :param progress: Float de 0.0 a 100.0 (o 0.0 a 1.0).
+ """
+ # Normalizar path
+ norm_path = os.path.normpath(file_path)
+ item_id = self._path_to_id.get(norm_path)
+
+ if item_id:
+ # Normalizar a rango 0.0 - 1.0
+ if progress > 1.0:
+ progress = progress / 100.0
+
+ # Enviar a la cola del hilo principal
+ self.msg_queue.put(("UPDATE_PROGRESS", item_id, {"progress": progress}))
+
+ def mark_file_processing(self, file_path: str):
+ """Marca visualmente que el archivo se está procesando actualmente."""
+ norm_path = os.path.normpath(file_path)
+ item_id = self._path_to_id.get(norm_path)
+ if item_id:
+ self.msg_queue.put(("UPDATE_PROGRESS", item_id, {"status": "processing"}))
+
+ def mark_file_completed(self, file_path: str):
+ """Marca el archivo como completado (barra llena, checkmark)."""
+ norm_path = os.path.normpath(file_path)
+ item_id = self._path_to_id.get(norm_path)
+ if item_id:
+ self.msg_queue.put(("UPDATE_PROGRESS", item_id, {"status": "completed"}))
+
+ def mark_file_error(self, file_path: str):
+ """Marca el archivo con error."""
+ norm_path = os.path.normpath(file_path)
+ item_id = self._path_to_id.get(norm_path)
+ if item_id:
+ self.msg_queue.put(("UPDATE_PROGRESS", item_id, {"status": "error"}))
+
+ # --- LÓGICA INTERNA DE ACTUALIZACIÓN VISUAL ---
+
+ def _render_progress_update(self, item_id, data):
+ """Ejecutado por _check_queue en el hilo principal."""
+ widgets = self._widget_refs.get(item_id)
+ item = next((x for x in self.queue_items if x.id == item_id), None)
+
+ if not widgets or not item:
+ return
+
+ # Actualizar barra de progreso
+ if "progress" in data:
+ prog = max(0.0, min(1.0, data["progress"]))
+ item.progress = prog
+ # Actualizamos ancho relativo (0 a 1)
+ widgets['progress_bar'].place(relwidth=prog)
+
+ # Actualizar Estados
+ if "status" in data:
+ status = data["status"]
+
+ if status == "processing":
+ widgets['card'].configure(border_color=Theme.PROCESSING_BORDER, border_width=2)
+ # Asegurar que la barra sea visible
+ widgets['progress_bar'].place(relwidth=max(0.05, item.progress))
+
+ elif status == "completed":
+ item.progress = 1.0
+ item.is_completed = True
+ self._apply_completed_style(item_id)
+
+ elif status == "error":
+ item.has_error = True
+ widgets['card'].configure(border_color=Theme.ERROR)
+ widgets['lbl_info'].configure(text_color=Theme.ERROR)
+
+ def _apply_completed_style(self, item_id):
+ """Aplica el estilo 'Completado' a los widgets."""
+ widgets = self._widget_refs.get(item_id)
+ if not widgets:
+ return
+
+ # Barra llena en verde oscuro
+ widgets['progress_bar'].configure(fg_color=Theme.COMPLETED_BG)
+ widgets['progress_bar'].place(relwidth=1.0)
+
+ # Borde normal
+ widgets['card'].configure(border_color=Theme.COMPLETED_BG, border_width=1)
+
+ # Icono Check
+ widgets['lbl_icon'].configure(image=None, text="✓", font=("Arial", 24, "bold"), text_color="#FFF")
+
+ # Texto atenuado
+ widgets['lbl_name'].configure(text_color=Theme.TEXT_DONE)
+
+ # Deshabilitar controles
+ widgets['btn_up'].configure(state="disabled")
+
+ # --- GESTIÓN DE LISTA ---
def move_up(self, item: QueueItem):
try:
idx = self.queue_items.index(item)
if idx > 0:
- self.queue_items[idx], self.queue_items[idx -
- 1] = self.queue_items[idx - 1], self.queue_items[idx]
+ self.queue_items[idx], self.queue_items[idx - 1] = \
+ self.queue_items[idx - 1], self.queue_items[idx]
self._update_layout_indices()
except ValueError:
pass
@@ -266,27 +393,28 @@ class FileQueueManager(ctk.CTkScrollableFrame):
def remove_item(self, item: QueueItem):
if item in self.queue_items:
self.queue_items.remove(item)
- # Eliminar referencias para liberar memoria
if item.id in self._widget_refs:
del self._widget_refs[item.id]
+
+ # Limpiar mapa de paths
+ if item.path in self._path_to_id:
+ del self._path_to_id[item.path]
+
self._update_layout_indices()
def clean_file_list(self):
self.queue_items.clear()
self._widget_refs.clear()
+ self._path_to_id.clear()
self._update_layout_indices()
def get_selected_file_list(self) -> List[str]:
return [item.path for item in self.queue_items if not item.has_error]
def regenerate_all_info(self):
- """Recalcula el texto de redimensionado sin recargar imágenes."""
+ """Recalcula info para actualizaciones masivas de configuración."""
for item in self.queue_items:
- if not item.has_error and item.is_loaded:
- # Recalculamos solo el texto basado en lo que ya sabemos (o recargamos dims rápidas)
- # Para optimizar, asumimos que podemos volver a lanzar el worker o
- # simplemente recalcular si guardáramos width/height en el objeto.
- # Por simplicidad y robustez, relanzamos el worker ligero.
+ if not item.has_error and item.is_loaded and not item.is_completed:
self.executor.submit(self._worker_load_info, item)
# --- SETTERS ---
@@ -299,18 +427,16 @@ class FileQueueManager(ctk.CTkScrollableFrame):
def set_output_resize_factor(self, f):
self.output_resize_factor = f
- # --- UTILIDADES ESTÁTICAS (Lógica pura) ---
+ # --- UTILIDADES DE IMAGEN/VIDEO ---
def _extract_file_data(self, file_path) -> Tuple[str, Optional[Image.Image]]:
- """Extrae info y genera thumbnail PIL (Seguro para Threads)."""
+ """Extrae info y thumbnail. Seguro para threads."""
info_str = "Info no disponible"
pil_image = None
try:
- exts_video = ['.mp4', '.avi', '.mkv',
- '.mov', '.wmv', '.flv', '.webm']
- is_video = any(file_path.lower().endswith(ext)
- for ext in exts_video)
+ exts_video = ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm']
+ is_video = any(file_path.lower().endswith(ext) for ext in exts_video)
width, height = 0, 0
@@ -322,24 +448,22 @@ class FileQueueManager(ctk.CTkScrollableFrame):
frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS)
- # Leer frame central para thumbnail
+ # Thumbnail del frame central
try:
if frames > 100:
- cap.set(cv2.CAP_PROP_POS_FRAMES, 50)
+ cap.set(cv2.CAP_PROP_POS_FRAMES, min(frames//2, 50))
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_image = self._process_thumbnail(frame)
except:
- pass # Fallo silencioso solo en thumbnail
+ pass
dur = frames/fps if fps > 0 else 0
m, s = divmod(int(dur), 60)
info_str = f"Video: {m}m:{s:02d}s • {frames} frames • {width}x{height}\n"
cap.release()
else:
- # Imagen
- # cv2.imdecode es mejor para rutas con caracteres especiales en Windows
img_array = np.fromfile(file_path, np.uint8)
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
@@ -349,7 +473,6 @@ class FileQueueManager(ctk.CTkScrollableFrame):
pil_image = self._process_thumbnail(img)
info_str = f"Image: {width}x{height}\n"
- # Añadir datos de redimensionado calculado
if width > 0 and height > 0:
info_str += self._calculate_resize_text(width, height)
@@ -360,42 +483,32 @@ class FileQueueManager(ctk.CTkScrollableFrame):
return info_str, pil_image
def _process_thumbnail(self, img_array) -> Image.Image:
- """Redimensiona y recorta imagen para thumbnail."""
h, w = img_array.shape[:2]
target_size = 64
-
- # Mantener aspect ratio dentro de 64x64
scale = target_size / max(h, w)
new_w, new_h = int(w * scale), int(h * scale)
-
- resized = cv2.resize(img_array, (new_w, new_h),
- interpolation=cv2.INTER_AREA)
+ resized = cv2.resize(img_array, (new_w, new_h), interpolation=cv2.INTER_AREA)
return Image.fromarray(resized)
def _calculate_resize_text(self, w, h) -> str:
- """Lógica pura de cálculo de dimensiones."""
if self.upscale_factor <= 0:
return ""
- iw = int(w * (self.input_resize_factor/100)
- ) if self.input_resize_factor else w
- ih = int(h * (self.input_resize_factor/100)
- ) if self.input_resize_factor else h
+ iw = int(w * (self.input_resize_factor/100)) if self.input_resize_factor else w
+ ih = int(h * (self.input_resize_factor/100)) if self.input_resize_factor else h
uw, uh = int(iw * self.upscale_factor), int(ih * self.upscale_factor)
- ow = int(uw * (self.output_resize_factor/100)
- ) if self.output_resize_factor else uw
- oh = int(uh * (self.output_resize_factor/100)
- ) if self.output_resize_factor else uh
+ ow = int(uw * (self.output_resize_factor/100)) if self.output_resize_factor else uw
+ oh = int(uh * (self.output_resize_factor/100)) if self.output_resize_factor else uh
txt = ""
if self.input_resize_factor:
- txt += f"Input AI ({int(self.input_resize_factor)}%) ➜ {iw}x{ih}\n"
+ txt += f"In: {int(self.input_resize_factor)}% ➜ {iw}x{ih}\n"
- txt += f"Output AI (x{self.upscale_factor}) ➜ {uw}x{uh}"
+ txt += f"AI x{self.upscale_factor} ➜ {uw}x{uh}"
if self.output_resize_factor:
- txt += f"\nFinal Output ({int(self.output_resize_factor)}%) ➜ {ow}x{oh}"
+ txt += f"\nOut: {int(self.output_resize_factor)}% ➜ {ow}x{oh}"
return txt
diff --git a/rsc/Capture1.png b/rsc/Capture1.png
deleted file mode 100644
index 90eea67..0000000
Binary files a/rsc/Capture1.png and /dev/null differ
diff --git a/rsc/Capture2.png b/rsc/Capture2.png
index 026ed4f..c88a635 100644
Binary files a/rsc/Capture2.png and b/rsc/Capture2.png differ
diff --git a/rsc/Capture3.png b/rsc/Capture3.png
new file mode 100644
index 0000000..2d43054
Binary files /dev/null and b/rsc/Capture3.png differ
diff --git a/rsc/Capture4.png b/rsc/Capture4.png
new file mode 100644
index 0000000..6ffbf03
Binary files /dev/null and b/rsc/Capture4.png differ
diff --git a/rsc/Capture5.png b/rsc/Capture5.png
new file mode 100644
index 0000000..8d55d77
Binary files /dev/null and b/rsc/Capture5.png differ
diff --git a/splash_screen.py b/splash_screen.py
new file mode 100644
index 0000000..0a5350d
--- /dev/null
+++ b/splash_screen.py
@@ -0,0 +1,234 @@
+import math
+import os
+import random
+from typing import Callable, Optional
+
+import customtkinter as ctk
+from PIL import Image
+
+
+class SplashScreen(ctk.CTkToplevel):
+ def __init__(
+ self,
+ root_window,
+ app_title: str,
+ version: str,
+ asset_loader: Callable[[str], str],
+ theme_colors: dict,
+ # CAMBIO 1: Duración ultrarrápida (1.2 segundos)
+ duration_ms: int = 1200,
+ # CAMBIO 2: Callback para ejecutar al finalizar
+ on_complete: Optional[Callable] = None
+ ):
+ super().__init__(root_window)
+
+ # --- CONFIGURACIÓN DE DATOS ---
+ self.asset_loader = asset_loader
+ self.theme = theme_colors
+ self.duration_ms = duration_ms
+ self.on_complete = on_complete # Guardamos la función a ejecutar al final
+
+ # Reducimos los pasos para que coincidan con la duración corta
+ self.steps_total = 60
+ self.current_step = 0
+ self.animation_running = True
+
+ # --- CONFIGURACIÓN DE PARTÍCULAS ---
+ self.particles = []
+ self.num_particles = 35 # Ligero y rápido
+
+ # --- CONFIGURACIÓN DE VENTANA (AL FRENTE) ---
+ self.overrideredirect(True)
+ self.attributes('-topmost', True)
+ self.lift()
+ self.focus_force()
+
+ # --- GEOMETRÍA Y CENTRADO ---
+ screen_width = self.winfo_screenwidth()
+ screen_height = self.winfo_screenheight()
+
+ width = 500
+ height = 320
+
+ x = (screen_width - width) // 2
+ y = (screen_height - height) // 2
+ self.geometry(f"{width}x{height}+{x}+{y}")
+
+ # --- CONSTRUCCIÓN DE UI ---
+ self._setup_ui(app_title, version, width, height)
+
+ # --- INICIAR ANIMACIONES ---
+ self._init_particles(width, height)
+ self._animate_particles()
+
+ # Iniciar carga casi inmediatamente
+ self.after(20, self._animate_loading)
+
+ def _setup_ui(self, app_title, version, width, height):
+ """Construye las capas de la interfaz."""
+
+ # --- 1. CAPA DE FONDO (Canvas) ---
+ self.canvas = ctk.CTkCanvas(
+ self,
+ width=width,
+ height=height,
+ bg=self.theme['bg'],
+ highlightthickness=0,
+ bd=0
+ )
+ self.canvas.pack(fill="both", expand=True)
+
+ # Borde decorativo
+ self.canvas.create_rectangle(
+ 2, 2, width-2, height-2,
+ outline=self.theme['accent'],
+ width=2
+ )
+
+ # --- 2. CAPA DE CONTENIDO ---
+
+ # A. Carga del Banner
+ banner_path = self.asset_loader(f"Assets{os.sep}banner.png")
+ has_banner = False
+
+ if os.path.exists(banner_path):
+ try:
+ pil_img = Image.open(banner_path)
+ banner_ctk = ctk.CTkImage(
+ light_image=pil_img, dark_image=pil_img, size=(460, 180))
+
+ self.banner_label = ctk.CTkLabel(
+ self,
+ image=banner_ctk,
+ text=""
+ )
+ self.banner_label.place(relx=0.5, rely=0.1, anchor="n")
+ has_banner = True
+ except Exception as e:
+ print(f"[SPLASH] Error cargando banner: {e}")
+
+ # B. Título de respaldo
+ if not has_banner:
+ self.title_label = ctk.CTkLabel(
+ self,
+ text=app_title.upper(),
+ font=ctk.CTkFont(family="Segoe UI", size=32, weight="bold"),
+ text_color=self.theme['app_name']
+ )
+ self.title_label.place(relx=0.5, rely=0.15, anchor="n")
+
+ # C. Barra de Progreso
+ self.progress_bar = ctk.CTkProgressBar(
+ self,
+ width=300,
+ height=8,
+ corner_radius=4,
+ progress_color=self.theme['accent'],
+ fg_color=self.theme['widget_bg'],
+ border_width=0
+ )
+ self.progress_bar.set(0)
+ self.progress_bar.place(relx=0.5, rely=0.7, anchor="center")
+
+ # D. Texto de estado
+ self.status_label = ctk.CTkLabel(
+ self,
+ text="Starting...",
+ font=ctk.CTkFont(family="Consolas", size=11),
+ text_color=self.theme['text_sec']
+ )
+ self.status_label.place(relx=0.5, rely=0.8, anchor="center")
+
+ # E. Etiqueta de Versión
+ self.version_label = ctk.CTkLabel(
+ self,
+ text=f"v{version} | Developed by Ivan-Ayub97",
+ font=ctk.CTkFont(family="Segoe UI", size=10),
+ text_color=self.theme['accent']
+ )
+ self.version_label.place(relx=0.5, rely=0.93, anchor="center")
+
+ def _init_particles(self, width, height):
+ particle_color = self.theme.get('text_sec', '#888888')
+ for _ in range(self.num_particles):
+ size = random.randint(2, 4)
+ x = random.randint(20, width - 20)
+ y = random.randint(20, height - 20)
+ # Un poco más rápido para dar sensación de fluidez
+ dx = random.uniform(-1.0, 1.0)
+ dy = random.uniform(-1.0, 1.0)
+
+ particle = {
+ 'id': self.canvas.create_oval(
+ x, y, x+size, y+size,
+ fill=particle_color,
+ outline=""
+ ),
+ 'x': x, 'y': y, 'dx': dx, 'dy': dy,
+ 'width': width, 'height': height
+ }
+ self.particles.append(particle)
+
+ def _animate_particles(self):
+ if not self.winfo_exists() or not self.animation_running:
+ return
+
+ for p in self.particles:
+ p['x'] += p['dx']
+ p['y'] += p['dy']
+
+ if p['x'] <= 5 or p['x'] >= p['width'] - 5:
+ p['dx'] *= -1
+ if p['y'] <= 5 or p['y'] >= p['height'] - 5:
+ p['dy'] *= -1
+
+ self.canvas.coords(p['id'], p['x'], p['y'], p['x']+3, p['y']+3)
+
+ self.after(33, self._animate_particles)
+
+ def _animate_loading(self):
+ messages = ["Loading...", "Modules...", "UI...", "Starting..."]
+
+ if not self.winfo_exists():
+ return
+
+ self.lift()
+ self.attributes('-topmost', True)
+
+ # Cálculo seguro de tiempo
+ step_interval = max(10, self.duration_ms // self.steps_total)
+
+ if self.current_step <= self.steps_total:
+ progress = self.current_step / self.steps_total
+ self.progress_bar.set(progress)
+
+ if self.current_step % 10 == 0:
+ msg_index = int((progress * 100) / (100 / len(messages)))
+ if msg_index < len(messages):
+ self.status_label.configure(text=messages[msg_index])
+
+ self.current_step += 1
+ self.after(step_interval, self._animate_loading)
+ else:
+ self.start_fade_out()
+
+ def start_fade_out(self):
+ self.animation_running = False
+ self._fade_step = 1.0
+ self._fade_out()
+
+ def _fade_out(self):
+ if not self.winfo_exists():
+ return
+
+ if self._fade_step > 0:
+ # Desvanecimiento muy rápido
+ self.attributes('-alpha', self._fade_step)
+ self._fade_step -= 0.1 # Salta de 10% en 10% para velocidad
+ self.after(20, self._fade_out)
+ else:
+ # 1. Destruimos la splash
+ self.destroy()
+ # 2. CAMBIO CRÍTICO: Ejecutamos el callback para abrir el programa principal
+ if self.on_complete:
+ self.on_complete()
diff --git a/warlock_preferences.py b/warlock_preferences.py
index 93eca14..31435e7 100644
--- a/warlock_preferences.py
+++ b/warlock_preferences.py
@@ -7,21 +7,18 @@ import subprocess
import sys
import threading
import time
-# GUI Imports
-import tkinter as tk
import webbrowser
import zipfile
from datetime import datetime
from tkinter import filedialog, messagebox
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Tuple
import customtkinter as ctk
-# Hardware Info Imports
import psutil
import requests
from packaging import version as pkg_version
-# Optional Imports Handling
+# Integración con Hardware (Opcional)
try:
import wmi
except ImportError:
@@ -31,104 +28,172 @@ try:
except ImportError:
GPUtil = None
-# =============================================================================
-# CONSTANTES Y TEMA VISUAL (WARLOCK IDENTITY)
-# =============================================================================
+# -----------------------------------------------------------------------------
+# IMPORTAR TEMA Y PUENTE DE COMPATIBILIDAD (FIX FINAL)
+# -----------------------------------------------------------------------------
+from warlock_theme import *
-CONFIG_FILE = "warlock_config.json"
-
-THEME = {
- "bg_main": "#121212", # Fondo principal muy oscuro
- "bg_sidebar": "#1E1E1E", # Fondo barra lateral
- "card_bg": "#252525", # Fondo de tarjetas
- "card_hover": "#2F2F2F", # Hover tarjetas
- "text_main": "#FFFFFF", # Texto blanco puro
- "text_dim": "#A0A0A0", # Texto secundario
- "accent": "#E63946", # Rojo Warlock Vibrante
- "accent_hover": "#C92A35", # Rojo oscuro hover
- "gold": "#FDEF2F", # Dorado (Highlights)
- "success": "#00E676", # Verde éxito
- "error": "#CF6679", # Rojo error
- "border": "#333333", # Bordes sutiles
- "input_bg": "#151515" # Fondo de inputs
+# --- THEME BRIDGE ---
+# Mapea los nombres que usa Preferences a los nombres que existen en WarlockTheme
+_aliases = {
+ "bg_sidebar": "widget_bg",
+ "bg_main": "bg",
+ "card_bg": "widget_bg",
+ "card_hover": "btn_hover",
+ "text_dim": "text_sec",
+ "input_bg": "entry_bg",
+ "gold": "accent",
+ "border": "widget_border" # <--- AGREGADO: Esto soluciona tu error actual
}
-FONTS = {
- "header": ("Segoe UI", 24, "bold"),
- "subheader": ("Segoe UI", 16, "bold"),
- "label": ("Segoe UI", 12),
- "label_bold": ("Segoe UI", 12, "bold"),
- "small": ("Segoe UI", 10),
- "mono": ("Consolas", 11)
-}
+# Aplicar los alias de forma segura
+for _new_key, _existing_key in _aliases.items():
+ if _new_key not in THEME:
+ # Si la clave original tampoco existe, usamos un gris oscuro por seguridad
+ THEME[_new_key] = THEME.get(_existing_key, "#333333")
+
+# Aseguramos compatibilidad inversa extra si el tema es muy antiguo
+if "info" not in THEME:
+ THEME["info"] = "#0277BD"
+if "success" not in THEME:
+ THEME["success"] = "#00C853"
+if "error" not in THEME:
+ THEME["error"] = "#B71C1C"
# =============================================================================
-# GESTOR DE CONFIGURACIÓN (CENTRALIZADO)
+# UTILIDADES DE UI (TOOLTIPS)
+# =============================================================================
+
+
+class ToolTip:
+ """Muestra un mensaje flotante al pasar el mouse sobre un widget."""
+
+ def __init__(self, widget, text, delay=500):
+ self.widget = widget
+ self.text = text
+ self.delay = delay
+ self.id = None
+ self.tw = None
+ self.widget.bind("", self.enter)
+ self.widget.bind("", self.leave)
+ self.widget.bind("", self.leave)
+
+ def enter(self, event=None):
+ self.id = self.widget.after(self.delay, self.show)
+
+ def leave(self, event=None):
+ if self.id:
+ self.widget.after_cancel(self.id)
+ self.id = None
+ self.hide()
+
+ def show(self):
+ if self.tw:
+ return
+ x, y, cx, cy = self.widget.bbox("insert")
+ x += self.widget.winfo_rootx() + 25
+ y += self.widget.winfo_rooty() + 20
+
+ self.tw = ctk.CTkToplevel(self.widget)
+ self.tw.wm_overrideredirect(True)
+ self.tw.wm_geometry(f"+{x}+{y}")
+
+ # Estilo del Tooltip
+ label = ctk.CTkLabel(
+ self.tw,
+ text=self.text,
+ justify='left',
+ bg_color="#2B2B2B",
+ fg_color="#2B2B2B",
+ text_color="#FFFFFF",
+ corner_radius=4,
+ font=("Segoe UI", 10),
+ padx=8, pady=4
+ )
+ label.pack()
+
+ # Borde sutil
+ frame = ctk.CTkFrame(self.tw, width=0, height=0,
+ border_width=1, border_color="#555")
+ frame.pack()
+
+ def hide(self):
+ if self.tw:
+ self.tw.destroy()
+ self.tw = None
+
+# =============================================================================
+# GESTOR DE CONFIGURACIÓN (CENTRALIZADO & EXPANDIDO)
# =============================================================================
class ConfigManager:
- # Configuración por defecto expandida
+ # Configuración Maestra con todas las opciones nuevas
DEFAULT_CONFIG = {
- # General
- "app_theme": "Dark",
- "ui_scaling": 1.0,
- "window_opacity": 1.0,
+ # --- GENERAL ---
+ "app_theme": "Dark", # Dark, Light, System
+ "ui_scaling": 1.0, # 0.8 - 1.25
+ "window_opacity": 1.0, # 0.5 - 1.0
"keep_window_on_top": False,
"check_updates_on_startup": True,
+ "start_minimized": False, # Nuevo
+ "notifications_enabled": True, # Nuevo
+ "play_sounds": True, # Nuevo
- # Rendimiento
+ # --- APARIENCIA AVANZADA ---
+ "corner_radius": 10, # Nuevo: Redondez de ventanas
+ "font_size_offset": 0, # Nuevo: Ajuste global de tamaño letra
+
+ # --- RENDIMIENTO ---
"process_priority": "Normal",
- "onnx_provider_preference": "Auto", # Auto, CUDA, DirectML, CPU
+ "onnx_provider_preference": "Auto",
"auto_close_on_finish": False,
- "max_cpu_threads": 0, # 0 = Auto
+ "max_cpu_threads": 0, # 0 = Auto
+ "gpu_memory_limit": 0, # 0 = No limit (Nuevo)
- # Integraciones (Rutas manuales)
+ # --- SALIDA Y ARCHIVOS (NUEVO) ---
+ "default_output_dir": "", # Vacío = Carpeta del proyecto
+ "filename_pattern": "Warlock_Output_{date}_{time}", # Patrón de nombre
+ "auto_save_logs": True,
+ "backup_count": 5,
+
+ # --- RUTAS MANUALES ---
"custom_ffmpeg_path": "",
"custom_exiftool_path": "",
- # Logs
+ # --- DEBUG ---
"extended_logging": False
}
@staticmethod
def get_base_path():
- """Obtiene la ruta base de instalación (funciona en .py y .exe)."""
if getattr(sys, 'frozen', False):
- # Si es un ejecutable (PyInstaller), usa la carpeta del .exe
return os.path.dirname(sys.executable)
else:
- # Si es script, usa la carpeta del script
return os.path.dirname(os.path.abspath(__file__))
@staticmethod
def get_assets_path():
- """Obtiene la ruta de assets compatible con PyInstaller --onefile"""
base_dir = getattr(sys, '_MEIPASS', os.path.dirname(
os.path.abspath(__file__)))
return os.path.join(base_dir, "Assets")
@staticmethod
def get_config_path():
- return os.path.join(ConfigManager.get_base_path(), CONFIG_FILE)
+ return os.path.join(ConfigManager.get_base_path(), "warlock_config.json")
@staticmethod
def load_config() -> Dict[str, Any]:
path = ConfigManager.get_config_path()
config = ConfigManager.DEFAULT_CONFIG.copy()
-
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
- # Merge seguro
for k, v in data.items():
- # Permitimos nuevas claves si el usuario edita manualmente,
- # pero priorizamos la estructura existente
config[k] = v
except Exception as e:
- print(f"[Config] Error loading config: {e}")
-
+ print(f"[Config] Warn: {e}")
return config
@staticmethod
@@ -138,7 +203,7 @@ class ConfigManager:
with open(path, "w", encoding="utf-8") as f:
json.dump(config_data, f, indent=4)
except Exception as e:
- print(f"[Config] Error saving config: {e}")
+ print(f"[Config] Error saving: {e}")
@staticmethod
def reset_config():
@@ -146,7 +211,7 @@ class ConfigManager:
return ConfigManager.DEFAULT_CONFIG.copy()
# =============================================================================
-# HARDWARE SCANNER (NEO ENGINE OPTIMIZED)
+# HARDWARE SCANNER
# =============================================================================
@@ -156,34 +221,23 @@ class HardwareScanner:
info = {
"os": f"{platform.system()} {platform.release()}",
"cpu": platform.processor(),
- "ram_total": 0,
- "ram_used": 0,
- "gpu": "Unknown",
- "vram": 0
+ "ram_total": 0, "ram_used": 0,
+ "gpu": "Integrated / Unknown", "vram": 0
}
-
try:
- # RAM
mem = psutil.virtual_memory()
info["ram_total"] = round(mem.total / (1024**3), 2)
info["ram_used"] = round(mem.used / (1024**3), 2)
- # CPU Name Cleanup
if wmi and platform.system() == "Windows":
try:
c = wmi.WMI()
- cpu = c.Win32_Processor()[0]
- info["cpu"] = cpu.Name.strip()
+ info["cpu"] = c.Win32_Processor()[0].Name.strip()
except:
pass
- elif platform.system() == "Darwin":
- info["cpu"] = subprocess.check_output(
- ["sysctl", "-n", "machdep.cpu.brand_string"]).strip().decode('utf-8')
- # GPU (Prioridad: NVIDIA > AMD/Intel Dedicada > Integrada)
+ # Detección GPU mejorada
gpu_found = False
-
- # 1. GPUtil (NVIDIA)
if GPUtil:
try:
gpus = GPUtil.getGPUs()
@@ -194,34 +248,26 @@ class HardwareScanner:
except:
pass
- # 2. WMI (Fallback Windows)
if not gpu_found and wmi and platform.system() == "Windows":
try:
c = wmi.WMI()
best_vram = -1
for g in c.Win32_VideoController():
- v_bytes = 0
try:
- # Algunas implementaciones WMI devuelven enteros negativos para valores altos
- v_bytes = int(g.AdapterRAM)
+ v_bytes = int(g.AdapterRAM) if g.AdapterRAM else 0
if v_bytes < 0:
v_bytes += 2**32
+ v_gb = round(v_bytes / (1024**3), 2)
+ if v_gb > best_vram:
+ best_vram = v_gb
+ info["gpu"] = g.Name
+ info["vram"] = v_gb
except:
continue
-
- v_gb = round(v_bytes / (1024**3), 2)
- # Buscamos la GPU con más VRAM (generalmente la dedicada)
- if v_gb > best_vram:
- best_vram = v_gb
- info["gpu"] = g.Name
- info["vram"] = v_gb
except:
pass
-
except Exception as e:
- print(f"Hardware scan error: {e}")
- info["os"] = f"Error scanning: {str(e)}"
-
+ info["os"] = f"Error: {e}"
return info
# =============================================================================
@@ -240,8 +286,6 @@ class UpdateManager:
if r.status_code == 200:
data = r.json()
tag = data.get("tag_name", "v0.0").lstrip("v")
-
- # Comparación semántica de versiones
if pkg_version.parse(tag) > pkg_version.parse(self.current):
return True, tag, data.get("html_url"), data.get("body")
return False, self.current, None, None
@@ -249,7 +293,7 @@ class UpdateManager:
return None, str(e), None, None
# =============================================================================
-# COMPONENTES UI (WIDGETS)
+# COMPONENTES UI
# =============================================================================
@@ -258,26 +302,36 @@ class SidebarButton(ctk.CTkButton):
color = THEME["card_bg"] if is_selected else "transparent"
text_col = THEME["accent"] if is_selected else THEME["text_dim"]
- super().__init__(master, text=f" {icon_char} {text}", anchor="w",
- fg_color=color, hover_color=THEME["card_hover"],
- text_color=text_col, height=45, corner_radius=8,
- font=FONTS["label_bold"], command=command)
+ super().__init__(
+ master, text=f" {icon_char} {text}", anchor="w",
+ fg_color=color, hover_color=THEME["card_hover"],
+ text_color=text_col, height=45, corner_radius=6,
+ font=("Segoe UI", 12, "bold"), command=command,
+ border_width=0
+ )
class SettingRow(ctk.CTkFrame):
- def __init__(self, master, title, desc=None):
+ def __init__(self, master, title, desc=None, tooltip_text=None):
super().__init__(master, fg_color="transparent")
- self.pack(fill="x", pady=10)
+ self.pack(fill="x", pady=8)
lbl_frame = ctk.CTkFrame(self, fg_color="transparent")
- lbl_frame.pack(side="left", fill="both", expand=True)
+ lbl_frame.pack(side="left", fill="both", expand=True, padx=(5, 0))
- ctk.CTkLabel(lbl_frame, text=title, font=FONTS["label_bold"],
- text_color=THEME["text_main"], anchor="w").pack(fill="x")
+ t_lbl = ctk.CTkLabel(lbl_frame, text=title, font=("Segoe UI", 12, "bold"),
+ text_color=THEME["text_main"], anchor="w")
+ t_lbl.pack(fill="x")
+
+ if tooltip_text:
+ ToolTip(t_lbl, tooltip_text)
if desc:
- ctk.CTkLabel(lbl_frame, text=desc, font=FONTS["small"],
- text_color=THEME["text_dim"], anchor="w").pack(fill="x")
+ d_lbl = ctk.CTkLabel(lbl_frame, text=desc, font=("Segoe UI", 10),
+ text_color=THEME["text_dim"], anchor="w")
+ d_lbl.pack(fill="x")
+ if tooltip_text and not getattr(t_lbl, "tw", None):
+ ToolTip(d_lbl, tooltip_text)
self.control_area = ctk.CTkFrame(self, fg_color="transparent")
self.control_area.pack(side="right")
@@ -294,531 +348,473 @@ class PreferencesWindow(ctk.CTkToplevel):
def __init__(self, master, version, owner, repo):
super().__init__(master)
- # Configuración Ventana
- self.title("Preferences")
- self.geometry("750x500")
+ # Config Inicial
+ self.title("Warlock Preferences")
+ self.geometry("850x600")
self.minsize(850, 600)
self.configure(fg_color=THEME["bg_main"])
- # Intentar centrar la ventana respecto al padre
- try:
- self.after(10, self.lift)
- self.after(10, self.focus_force)
- except:
- pass
-
- # Datos
+ # Cargar datos
self.config = ConfigManager.load_config()
self.version = version
self.owner = owner
self.repo = repo
- self.cached_hw_info = None # Cache para no escanear constantemente
+ self.cached_hw_info = None
- # Layout Principal (Grid 2 columnas: Sidebar | Content)
+ # Grid Principal
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.setup_sidebar()
self.setup_content_area()
-
- # Cargar pestaña inicial
self.show_page("general")
- # Icono (Intentar cargar)
- try:
- assets = ConfigManager.get_assets_path()
- icon_path = os.path.join(assets, "logo.ico")
- if os.path.exists(icon_path):
- self.iconbitmap(icon_path)
- except:
- pass
+ # Intentar poner al frente
+ self.after(200, lambda: self.attributes('-topmost', True))
+ self.lift()
+ self.focus_force()
def setup_sidebar(self):
self.sidebar = ctk.CTkFrame(
- self, width=240, corner_radius=0, fg_color=THEME["bg_sidebar"])
+ self, width=250, corner_radius=0, fg_color=THEME["bg_sidebar"])
self.sidebar.grid(row=0, column=0, sticky="nsew")
self.sidebar.grid_propagate(False)
- # Logo / Título
- title_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent")
- title_frame.pack(pady=30, padx=20, fill="x")
-
- ctk.CTkLabel(title_frame, text="Warlock-Studio", font=("Impact", 28),
+ # Header Sidebar
+ head = ctk.CTkFrame(self.sidebar, fg_color="transparent")
+ head.pack(pady=30, padx=20, fill="x")
+ ctk.CTkLabel(head, text="PREFERENCES", font=("Impact", 24),
text_color=THEME["accent"]).pack(anchor="w")
- ctk.CTkLabel(title_frame, text="CONFIG", font=("Arial", 10, "bold"),
- text_color=THEME["text_dim"]).pack(anchor="w", pady=(0, 10))
+ ctk.CTkLabel(head, text="Warlock Studio Control Center", font=(
+ "Segoe UI", 10), text_color=THEME["text_dim"]).pack(anchor="w")
- # Separador
- ctk.CTkFrame(self.sidebar, height=2, fg_color=THEME["border"]).pack(
- fill="x", padx=20, pady=(0, 20))
-
- # Botones de Navegación
+ # Botones Navegación
self.nav_btns = {}
pages = [
- ("general", "General", "⚙"),
- ("performance", "Performance & AI", "🚀"),
- ("integrations", "Paths & Tools", "🔗"),
- ("logs", "Logs & Maintenance", "🛠"),
+ ("general", "General & UI", "⚙"),
+ ("output", "Output & Files", "📂"),
+ ("performance", "Performance / AI", "🚀"),
+ ("integrations", "Tools & Paths", "🔗"),
+ ("logs", "Maintenance", "🛠"),
("about", "About & Update", "ℹ")
]
for pid, text, icon in pages:
btn = SidebarButton(self.sidebar, text, icon,
lambda p=pid: self.show_page(p))
- btn.pack(fill="x", padx=15, pady=5)
+ btn.pack(fill="x", padx=15, pady=4)
self.nav_btns[pid] = btn
- # Footer Sidebar
- footer = ctk.CTkFrame(self.sidebar, fg_color="transparent")
- footer.pack(side="bottom", pady=20, fill="x")
- ctk.CTkLabel(footer, text=f"v{self.version}", font=FONTS["small"],
- text_color=THEME["text_dim"]).pack()
-
def setup_content_area(self):
self.content = ctk.CTkScrollableFrame(
self, fg_color="transparent", corner_radius=0)
- self.content.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
+ self.content.grid(row=0, column=1, sticky="nsew", padx=25, pady=25)
+
+ # Scrollbar styling hack
+ try:
+ self.content._scrollbar.configure(
+ width=12, fg_color=THEME["bg_main"])
+ except:
+ pass
def show_page(self, page_id):
- # Actualizar estado botones sidebar
+ # Actualizar estilo botones
for pid, btn in self.nav_btns.items():
- is_active = (pid == page_id)
- btn.configure(fg_color=THEME["card_bg"] if is_active else "transparent",
- text_color=THEME["accent"] if is_active else THEME["text_dim"])
+ btn.configure(fg_color=THEME["card_bg"] if pid == page_id else "transparent",
+ text_color=THEME["accent"] if pid == page_id else THEME["text_dim"])
- # Limpiar contenido
+ # Limpiar frame
for widget in self.content.winfo_children():
widget.destroy()
- # Construir página
- if page_id == "general":
- self.build_general()
- elif page_id == "performance":
- self.build_performance()
- elif page_id == "integrations":
- self.build_integrations()
- elif page_id == "logs":
- self.build_logs()
- elif page_id == "about":
- self.build_about()
-
- # =========================================================================
- # CONSTRUCTORES DE PÁGINAS
- # =========================================================================
+ # Renderizar
+ method = getattr(self, f"build_{page_id}", None)
+ if method:
+ # Animación simple de entrada (fade in simulado por orden de pack)
+ title = page_id.replace("_", " ").title()
+ if page_id == "output":
+ title = "Output Management"
+ self.header(title)
+ method()
def header(self, text):
- ctk.CTkLabel(self.content, text=text, font=FONTS["header"],
- text_color=THEME["text_main"]).pack(anchor="w", pady=(0, 20))
+ ctk.CTkLabel(self.content, text=text, font=("Segoe UI", 26, "bold"),
+ text_color=THEME["text_main"]).pack(anchor="w", pady=(0, 25))
+ ctk.CTkFrame(self.content, height=2, fg_color=THEME["border"]).pack(
+ fill="x", pady=(0, 20))
+
+ # =========================================================================
+ # BUILDERS (PÁGINAS)
+ # =========================================================================
def build_general(self):
- self.header("General Settings")
-
- # UI Appearance
- grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
- grp.pack(fill="x", pady=10, padx=5)
+ # --- SECCIÓN VISUAL ---
+ self.sub_header("Visual Appearance")
+ grp_vis = self.create_group()
# Theme
- r1 = SettingRow(grp, "App Theme",
- "Select the visual style of the application")
- om = ctk.CTkOptionMenu(grp, values=["Dark", "Light", "System"],
- command=lambda v: self.save(
- "app_theme", v, lambda: ctk.set_appearance_mode(v)),
- fg_color=THEME["input_bg"], button_color=THEME["accent"], width=150)
- om.set(self.config.get("app_theme", "Dark"))
- r1.add_widget(om)
+ r_theme = SettingRow(grp_vis, "Application Theme",
+ "Light or Dark mode preference")
+ om_theme = ctk.CTkOptionMenu(grp_vis, values=["Dark", "Light", "System"],
+ command=lambda v: self.save(
+ "app_theme", v, lambda: ctk.set_appearance_mode(v)),
+ fg_color=THEME["input_bg"], button_color=THEME["accent"], width=140)
+ om_theme.set(self.config.get("app_theme", "Dark"))
+ r_theme.add_widget(om_theme)
- # UI Scaling
- r_scale = SettingRow(
- grp, "UI Scaling", "Adjust the size of the interface elements")
- om_scale = ctk.CTkOptionMenu(grp, values=["80%", "90%", "100%", "110%", "125%"],
- command=self.set_ui_scaling,
- fg_color=THEME["input_bg"], button_color=THEME["accent"], width=150)
- current_scale = f"{int(self.config.get('ui_scaling', 1.0) * 100)}%"
- om_scale.set(current_scale)
- r_scale.add_widget(om_scale)
+ # Corner Radius
+ r_rad = SettingRow(grp_vis, "Window Corner Radius",
+ "Roundness of interface elements (Restart required)")
+ sl_rad = ctk.CTkSlider(grp_vis, from_=0, to=20, number_of_steps=20,
+ command=lambda v: self.save_debounced(
+ "corner_radius", int(v)),
+ progress_color=THEME["accent"], button_color=THEME["gold"])
+ sl_rad.set(self.config.get("corner_radius", 10))
+ r_rad.add_widget(sl_rad)
- # Opacity
- r2 = SettingRow(grp, "Window Opacity",
- "Adjust transparency (0.5 - 1.0)")
+ # Opacidad
+ r_op = SettingRow(grp_vis, "Window Opacity",
+ "Transparency level of the main window")
+ sl_op = ctk.CTkSlider(grp_vis, from_=0.5, to=1.0, number_of_steps=50,
+ command=lambda v: self.update_opacity(v),
+ progress_color=THEME["accent"], button_color=THEME["gold"])
+ sl_op.set(self.config.get("window_opacity", 1.0))
+ sl_op.bind("",
+ lambda e: ConfigManager.save_config(self.config))
+ r_op.add_widget(sl_op)
- def update_opacity(val):
- # Convertir a float porque el slider devuelve float pero a veces muchos decimales
- v = float(val)
- self.master.attributes("-alpha", v) # Aplica al padre
- self.attributes("-alpha", v) # Aplica a esta ventana también
- self.config["window_opacity"] = v
+ # Scaling
+ r_sca = SettingRow(grp_vis, "UI Scaling",
+ "Increase size for 4K monitors")
+ om_sca = ctk.CTkOptionMenu(grp_vis, values=["80%", "90%", "100%", "110%", "125%", "150%"],
+ command=self.set_ui_scaling,
+ fg_color=THEME["input_bg"], button_color=THEME["accent"], width=140)
+ om_sca.set(f"{int(self.config.get('ui_scaling', 1.0)*100)}%")
+ r_sca.add_widget(om_sca)
- sl = ctk.CTkSlider(grp, from_=0.5, to=1.0, number_of_steps=50,
- command=update_opacity,
- progress_color=THEME["accent"], button_color=THEME["gold"])
- sl.set(self.config.get("window_opacity", 1.0))
- # Bind release para guardar configuración y no saturar disco
- sl.bind("",
- lambda event: ConfigManager.save_config(self.config))
- r2.add_widget(sl)
+ # --- SECCIÓN COMPORTAMIENTO ---
+ self.sub_header("Behavior & System", pady=20)
+ grp_beh = self.create_group()
- # Toggles
- # Usamos winfo_toplevel() para asegurar que obtenemos la ventana raíz
- root_win = self.master.winfo_toplevel()
+ self.add_switch(grp_beh, "Always on Top", "keep_window_on_top",
+ cmd=lambda v: self.master.winfo_toplevel().attributes("-topmost", v))
+ self.add_switch(grp_beh, "Notifications Enabled",
+ "notifications_enabled", "Show toast messages on finish")
+ self.add_switch(grp_beh, "Play Sounds", "play_sounds",
+ "Audio feedback for actions")
+ self.add_switch(grp_beh, "Check Updates on Startup",
+ "check_updates_on_startup")
- r3 = SettingRow(grp, "Always on Top",
- "Keep the main window above others")
- sw1 = ctk.CTkSwitch(grp, text="", command=lambda: self.save_bool("keep_window_on_top", sw1, lambda: root_win.attributes("-topmost", sw1.get())),
- progress_color=THEME["accent"], button_color="white")
- if self.config.get("keep_window_on_top", False):
- sw1.select()
- r3.add_widget(sw1)
+ def build_output(self):
+ # --- GESTIÓN DE ARCHIVOS ---
+ self.sub_header("File Management")
+ grp = self.create_group()
- r4 = SettingRow(grp, "Check Updates on Startup",
- "Automatically check Github for new versions")
- sw2 = ctk.CTkSwitch(grp, text="", command=lambda: self.save_bool("check_updates_on_startup", sw2),
- progress_color=THEME["accent"])
- if self.config.get("check_updates_on_startup", True):
- sw2.select()
- r4.add_widget(sw2)
+ # Default Directory
+ ctk.CTkLabel(grp, text="Default Output Directory", font=(
+ "Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(10, 0))
- r5 = SettingRow(grp, "Auto-Close on Finish",
- "Close app automatically when processing ends")
- sw3 = ctk.CTkSwitch(grp, text="", command=lambda: self.save_bool("auto_close_on_finish", sw3),
- progress_color=THEME["accent"])
- if self.config.get("auto_close_on_finish", False):
- sw3.select()
- r5.add_widget(sw3)
+ dir_row = ctk.CTkFrame(grp, fg_color="transparent")
+ dir_row.pack(fill="x", padx=10, pady=5)
- def set_ui_scaling(self, value):
- scale_map = {"80%": 0.8, "90%": 0.9,
- "100%": 1.0, "110%": 1.1, "125%": 1.2}
- new_scale = scale_map.get(value, 1.0)
- ctk.set_widget_scaling(new_scale)
- self.save("ui_scaling", new_scale)
+ self.ent_dir = ctk.CTkEntry(
+ dir_row, fg_color=THEME["input_bg"], border_color=THEME["border"])
+ self.ent_dir.pack(side="left", fill="x", expand=True, padx=(0, 10))
+ self.ent_dir.insert(0, self.config.get("default_output_dir", ""))
+ self.ent_dir.configure(state="readonly")
+
+ ctk.CTkButton(dir_row, text="Browse...", width=100, fg_color=THEME["accent"], text_color="#000",
+ command=self.browse_output_dir).pack(side="right")
+
+ ctk.CTkButton(dir_row, text="Reset", width=60, fg_color=THEME["bg_main"],
+ command=lambda: self.save_dir("")).pack(side="right", padx=5)
+
+ # Filename Pattern
+ r_pat = SettingRow(grp, "Filename Pattern",
+ "Use {date}, {time}, {original} as placeholders")
+ ent_pat = ctk.CTkEntry(
+ grp, fg_color=THEME["input_bg"], border_color=THEME["border"], width=250)
+ ent_pat.insert(0, self.config.get(
+ "filename_pattern", "Warlock_{date}_{time}"))
+ ent_pat.bind("", lambda e: self.save(
+ "filename_pattern", ent_pat.get()))
+ r_pat.add_widget(ent_pat)
+
+ # Options
+ self.add_switch(grp, "Auto-Save Processing Logs",
+ "auto_save_logs", "Save a .txt log with each export")
def build_performance(self):
- self.header("Performance & AI")
-
- # Hardware Info Card
+ # --- HARDWARE INFO ---
self.hw_frame = ctk.CTkFrame(
self.content, fg_color=THEME["card_bg"], border_color=THEME["accent"], border_width=1)
self.hw_frame.pack(fill="x", pady=(0, 20))
- # Loading Label
- self.lbl_hw_loading = ctk.CTkLabel(
- self.hw_frame, text="Scanning Hardware...", text_color=THEME["text_dim"])
- self.lbl_hw_loading.pack(pady=20)
-
- # Iniciar scan en hilo para no congelar
if self.cached_hw_info:
self._display_hw_info(self.cached_hw_info)
else:
+ ctk.CTkLabel(self.hw_frame, text="Scanning System Hardware...",
+ text_color=THEME["accent"]).pack(pady=20)
threading.Thread(target=self._run_hw_scan, daemon=True).start()
- # Settings
- grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
- grp.pack(fill="x")
+ # --- AI SETTINGS ---
+ self.sub_header("Artificial Intelligence Engine")
+ grp = self.create_group()
- # ONNX Provider
- r1 = SettingRow(grp, "ONNX Provider",
- "Force specific AI execution backend (Restart Required)")
- om1 = ctk.CTkOptionMenu(grp, values=["Auto", "CUDA", "DirectML", "CPU"],
- command=lambda v: self.save(
- "onnx_provider_preference", v),
- fg_color=THEME["input_bg"], button_color=THEME["accent"])
- om1.set(self.config.get("onnx_provider_preference", "Auto"))
- r1.add_widget(om1)
+ # Provider
+ r_prov = SettingRow(grp, "Inference Backend", "Hardware acceleration provider (Requires Restart)",
+ tooltip_text="CUDA: NVIDIA GPUs (Fastest)\nDirectML: AMD/Intel/NVIDIA (Compatible)\nCPU: Slowest, universal compatibility.")
+ om_prov = ctk.CTkOptionMenu(grp, values=["Auto", "CUDA", "DirectML", "CPU", "OpenVINO"],
+ command=lambda v: self.save(
+ "onnx_provider_preference", v),
+ fg_color=THEME["input_bg"], button_color=THEME["accent"])
+ om_prov.set(self.config.get("onnx_provider_preference", "Auto"))
+ r_prov.add_widget(om_prov)
- # Process Priority
- r2 = SettingRow(grp, "Process Priority",
- "System priority for FFmpeg and AI threads")
- om2 = ctk.CTkOptionMenu(grp, values=["Normal", "Above Normal", "High"],
- command=lambda v: self.save(
- "process_priority", v),
- fg_color=THEME["input_bg"], button_color=THEME["accent"])
- om2.set(self.config.get("process_priority", "Normal"))
- r2.add_widget(om2)
+ # Priority
+ r_prio = SettingRow(grp, "Process Priority",
+ "OS CPU scheduling priority")
+ om_prio = ctk.CTkOptionMenu(grp, values=["Normal", "Above Normal", "High", "Realtime"],
+ command=lambda v: self.save(
+ "process_priority", v),
+ fg_color=THEME["input_bg"], button_color=THEME["accent"])
+ om_prio.set(self.config.get("process_priority", "Normal"))
+ r_prio.add_widget(om_prio)
- def _run_hw_scan(self):
- self.cached_hw_info = HardwareScanner.get_system_info()
- # Actualizar UI en main thread
- self.after(0, lambda: self._display_hw_info(self.cached_hw_info))
-
- def _display_hw_info(self, hw):
-
- # Seguridad: evitar error cuando el frame ya no existe
- if not hasattr(self, "hw_frame"):
- return
-
- if not str(self.hw_frame) or not self.hw_frame.winfo_exists():
- return
-
- for w in self.hw_frame.winfo_children():
- w.destroy()
-
- lbl_grid = ctk.CTkFrame(self.hw_frame, fg_color="transparent")
- lbl_grid.pack(padx=20, pady=15, fill="x")
-
- items = [
- ("OS:", hw['os']),
- ("CPU Model:", hw['cpu']),
- ("RAM Usage:", f"{hw['ram_used']} GB / {hw['ram_total']} GB"),
- ("GPU Model:", hw['gpu']),
- ("VRAM:", f"{hw['vram']} GB")
- ]
-
- for i, (k, v) in enumerate(items):
- ctk.CTkLabel(lbl_grid, text=k, font=FONTS["label_bold"], text_color=THEME["text_dim"]).grid(
- row=i//2, column=(i % 2)*2, sticky="w", padx=(0, 10), pady=2)
- ctk.CTkLabel(lbl_grid, text=v, font=FONTS["label"], text_color=THEME["text_main"]).grid(
- row=i//2, column=(i % 2)*2+1, sticky="w", padx=(0, 40), pady=2)
+ self.add_switch(grp, "Auto-Close After Task", "auto_close_on_finish")
def build_integrations(self):
- self.header("Paths & Tools")
+ ctk.CTkLabel(self.content, text="External Tools Configuration", font=(
+ "Segoe UI", 12), text_color=THEME["text_dim"]).pack(anchor="w")
- desc = ctk.CTkLabel(self.content, text="Manually configure paths for external tools if auto-detection fails. Leave empty to use built-in/default paths.",
- text_color=THEME["text_dim"], font=FONTS["small"], wraplength=500, justify="left")
- desc.pack(anchor="w", pady=(0, 20))
-
- grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
- grp.pack(fill="x")
-
- # FFmpeg
+ grp = self.create_group()
self.create_path_selector(
- grp, "FFmpeg Executable", "custom_ffmpeg_path", "ffmpeg.exe")
-
- # ExifTool
+ grp, "FFmpeg Binary", "custom_ffmpeg_path", "ffmpeg.exe")
self.create_path_selector(
- grp, "ExifTool Executable", "custom_exiftool_path", "exiftool.exe")
-
- def create_path_selector(self, parent, title, config_key, file_filter):
- f = ctk.CTkFrame(parent, fg_color="transparent")
- f.pack(fill="x", padx=10, pady=10)
-
- ctk.CTkLabel(f, text=title, font=FONTS["label_bold"]).pack(anchor="w")
-
- row = ctk.CTkFrame(f, fg_color="transparent")
- row.pack(fill="x", pady=(5, 0))
-
- entry = ctk.CTkEntry(
- row, fg_color=THEME["input_bg"], border_color=THEME["border"])
- entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
-
- current_val = self.config.get(config_key, "")
- entry.insert(0, current_val)
-
- # Validación visual simple
- if current_val and not os.path.exists(current_val):
- entry.configure(text_color=THEME["error"])
-
- def pick():
- path = filedialog.askopenfilename(
- filetypes=[("Executables", "*.exe"), ("All Files", "*.*")])
- if path:
- entry.delete(0, "end")
- entry.insert(0, path)
- entry.configure(text_color=THEME["text_main"])
- self.save(config_key, path)
-
- def clear():
- entry.delete(0, "end")
- self.save(config_key, "")
- entry.configure(text_color=THEME["text_main"])
-
- btn_pick = ctk.CTkButton(
- row, text="Browse", width=80, fg_color=THEME["border"], command=pick)
- btn_pick.pack(side="left", padx=5)
-
- btn_clear = ctk.CTkButton(
- row, text="✖", width=30, fg_color=THEME["bg_main"], hover_color=THEME["accent"], command=clear)
- btn_clear.pack(side="left", padx=(5, 0))
+ grp, "ExifTool Binary", "custom_exiftool_path", "exiftool.exe")
def build_logs(self):
- self.header("Logs & Maintenance")
+ # --- MAINTENANCE ---
+ grp = self.create_group()
- grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
- grp.pack(fill="x", pady=10)
+ # Tools
+ btn_row = ctk.CTkFrame(grp, fg_color="transparent")
+ btn_row.pack(fill="x", padx=10, pady=10)
- # Actions Row
- act_row = ctk.CTkFrame(grp, fg_color="transparent")
- act_row.pack(fill="x", padx=10, pady=10)
+ self.create_tool_btn(btn_row, "Open Logs Folder",
+ self.open_logs_dir, THEME["border"])
+ self.create_tool_btn(btn_row, "Clean Temp Files",
+ self.clean_temp_files, THEME["info"])
+ self.create_tool_btn(btn_row, "Export Debug Zip",
+ self.export_debug_info, THEME["accent"], text_col="#000")
- ctk.CTkButton(act_row, text="Open Logs Folder", fg_color=THEME["border"],
- command=self.open_logs_dir).pack(side="left", padx=5)
+ # Reset Danger Zone
+ ctk.CTkFrame(grp, height=1, fg_color="#444").pack(
+ fill="x", pady=15, padx=10)
- ctk.CTkButton(act_row, text="Clean Temp Files", fg_color=THEME["accent"],
- command=self.clean_temp_files).pack(side="left", padx=5)
-
- ctk.CTkButton(act_row, text="Export Debug Zip", fg_color="#444",
- command=self.export_debug_info).pack(side="left", padx=5)
-
- ctk.CTkButton(act_row, text="Reset Config", fg_color="transparent", border_color="#FF0000", border_width=1, text_color="#FF0000",
- command=self.reset_all).pack(side="right", padx=5)
+ dz_row = ctk.CTkFrame(grp, fg_color="transparent")
+ dz_row.pack(fill="x", padx=10, pady=(0, 10))
+ ctk.CTkLabel(dz_row, text="Danger Zone:", text_color=THEME["error"], font=(
+ "Segoe UI", 12, "bold")).pack(side="left")
+ ctk.CTkButton(dz_row, text="Factory Reset", fg_color="transparent", border_color=THEME["error"],
+ border_width=1, text_color=THEME["error"], hover_color=THEME["error"],
+ command=self.reset_all).pack(side="right")
# Log Viewer
- ctk.CTkLabel(grp, text="Latest Log Preview:",
- font=FONTS["label_bold"]).pack(anchor="w", padx=10, pady=(10, 0))
-
+ ctk.CTkLabel(self.content, text="Latest Log Preview", font=(
+ "Segoe UI", 14, "bold"), pady=10).pack(anchor="w")
log_box = ctk.CTkTextbox(
- grp, height=300, fg_color=THEME["input_bg"], font=FONTS["mono"])
- log_box.pack(fill="x", padx=10, pady=10)
-
- # Load Log Content
- base = ConfigManager.get_base_path()
- log_folder_pattern = os.path.join(base, "*_Logs")
- log_folders = glob.glob(log_folder_pattern)
-
- if log_folders:
- log_folder = log_folders[0]
- log_path_pattern = os.path.join(log_folder, "*.log")
- files = glob.glob(log_path_pattern)
-
- if files:
- latest = max(files, key=os.path.getmtime)
- try:
- with open(latest, "r", encoding="utf-8", errors="ignore") as f:
- content = f.read()
- log_box.insert("0.0", content)
- log_box.see("end") # Auto scroll al final
- except Exception as e:
- log_box.insert("0.0", f"Error reading log file: {e}")
- else:
- log_box.insert(
- "0.0", "Log folder found, but no .log files present.")
- else:
- log_box.insert("0.0", "No logs folder found.")
+ self.content, height=250, fg_color="#111", text_color="#DDD", font=("Consolas", 10))
+ log_box.pack(fill="x")
+ self.load_log_preview(log_box)
def build_about(self):
- self.header("About")
-
card = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
card.pack(fill="x", pady=10)
- ctk.CTkLabel(card, text="Warlock-Studio", font=("Impact", 48),
- text_color=THEME["accent"]).pack(pady=(30, 5))
- ctk.CTkLabel(
- card, text=f"Version {self.version}", font=FONTS["label_bold"]).pack()
- ctk.CTkLabel(card, text="Developed by Ivan-Ayub97",
- font=FONTS["small"], text_color=THEME["text_dim"]).pack(pady=(0, 20))
+ # Hero
+ ctk.CTkLabel(card, text="WARLOCK STUDIO", font=(
+ "Impact", 42), text_color=THEME["accent"]).pack(pady=(30, 0))
+ ctk.CTkLabel(card, text="Professional Media Suite", font=(
+ "Segoe UI", 12, "bold"), text_color=THEME["text_main"]).pack()
+ ctk.CTkLabel(card, text=f"v{self.version} | Build 2024", font=(
+ "Consolas", 10), text_color=THEME["text_dim"]).pack(pady=5)
- # Buttons
- btn_row = ctk.CTkFrame(card, fg_color="transparent")
- btn_row.pack(pady=20)
+ ctk.CTkButton(card, text="Visit GitHub", fg_color="#24292e", hover_color="#000", width=120,
+ command=lambda: webbrowser.open(f"https://github.com/{self.owner}/{self.repo}")).pack(pady=20)
- ctk.CTkButton(btn_row, text="GitHub Repository", fg_color="#333", width=140,
- command=lambda: webbrowser.open(f"https://github.com/{self.owner}/{self.repo}")).pack(side="left", padx=10)
-
- # License View
- lbl_lic = ctk.CTkLabel(card, text="LICENSE AGREEMENT",
- font=FONTS["label_bold"], text_color=THEME["text_dim"])
- lbl_lic.pack(pady=(10, 5))
-
- # Intento robusto de cargar licencia
- l_txt = "License file not found."
- try:
- # Opción 1: Assets/license.txt
- assets_path = ConfigManager.get_assets_path()
- lic_path = os.path.join(assets_path, "license.txt")
- if not os.path.exists(lic_path):
- # Opción 2: Raíz/license.txt
- lic_path = os.path.join(
- ConfigManager.get_base_path(), "license.txt")
-
- if os.path.exists(lic_path):
- with open(lic_path, "r", encoding="utf-8") as f:
- l_txt = f.read()
- except:
- pass
-
- ld = ctk.CTkTextbox(card, height=150, fg_color="#111111",
- text_color=THEME["text_dim"], font=("Consolas", 10))
- ld.pack(fill="x", pady=10, padx=20)
- ld.insert("0.0", l_txt)
- ld.configure(state="disabled")
-
- # Update Checker
- upd_frame = ctk.CTkFrame(self.content, fg_color="transparent")
- upd_frame.pack(fill="x", pady=20)
-
- self.btn_upd = ctk.CTkButton(upd_frame, text="Check for Updates", height=50, font=FONTS["subheader"],
- fg_color=THEME["border"], hover_color=THEME["success"], command=self.check_updates)
- self.btn_upd.pack(fill="x")
+ # Update
+ self.btn_upd = ctk.CTkButton(self.content, text="Check for Updates", height=45, fg_color=THEME["border"],
+ hover_color=THEME["success"], command=self.check_updates)
+ self.btn_upd.pack(fill="x", pady=10)
# =========================================================================
- # LOGICA DE ACCIONES
+ # HELPERS & LOGIC
# =========================================================================
+ def sub_header(self, text, pady=5):
+ ctk.CTkLabel(self.content, text=text, font=("Segoe UI", 14, "bold"),
+ text_color=THEME["accent"]).pack(anchor="w", pady=(pady, 5))
+
+ def create_group(self):
+ f = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
+ f.pack(fill="x", pady=(0, 20))
+ return f
+
+ def add_switch(self, parent, text, key, desc=None, cmd=None):
+ r = SettingRow(parent, text, desc)
+ sw = ctk.CTkSwitch(
+ parent, text="", progress_color=THEME["accent"], onvalue=True, offvalue=False)
+
+ def callback():
+ val = sw.get()
+ self.config[key] = bool(val)
+ ConfigManager.save_config(self.config)
+ if cmd:
+ cmd(val)
+
+ sw.configure(command=callback)
+ if self.config.get(key, False):
+ sw.select()
+ r.add_widget(sw)
+
+ def create_tool_btn(self, parent, text, cmd, color, text_col="#FFF"):
+ ctk.CTkButton(parent, text=text, command=cmd, fg_color=color,
+ text_color=text_col, width=120).pack(side="left", padx=5)
+
+ def update_opacity(self, val):
+ v = float(val)
+ self.master.winfo_toplevel().attributes("-alpha", v)
+ self.attributes("-alpha", v)
+ self.config["window_opacity"] = v
+
+ def set_ui_scaling(self, value):
+ scale_map = {"80%": 0.8, "90%": 0.9, "100%": 1.0,
+ "110%": 1.1, "125%": 1.2, "150%": 1.5}
+ new = scale_map.get(value, 1.0)
+ ctk.set_widget_scaling(new)
+ self.save("ui_scaling", new)
+
def save(self, key, value, callback=None):
self.config[key] = value
ConfigManager.save_config(self.config)
if callback:
callback()
- def save_bool(self, key, switch_widget, callback=None):
- val = bool(switch_widget.get())
- self.save(key, val, callback)
+ def save_debounced(self, key, value):
+ self.config[key] = value
+ def save_dir(self, path):
+ self.config["default_output_dir"] = path
+ ConfigManager.save_config(self.config)
+ self.ent_dir.configure(state="normal")
+ self.ent_dir.delete(0, "end")
+ self.ent_dir.insert(0, path)
+ self.ent_dir.configure(state="readonly")
+
+ def browse_output_dir(self):
+ p = filedialog.askdirectory()
+ if p:
+ self.save_dir(p)
+
+ def create_path_selector(self, parent, title, config_key, file_filter):
+ f = ctk.CTkFrame(parent, fg_color="transparent")
+ f.pack(fill="x", padx=10, pady=10)
+ ctk.CTkLabel(f, text=title, font=(
+ "Segoe UI", 12, "bold")).pack(anchor="w")
+
+ row = ctk.CTkFrame(f, fg_color="transparent")
+ row.pack(fill="x", pady=(5, 0))
+
+ ent = ctk.CTkEntry(
+ row, fg_color=THEME["input_bg"], border_color=THEME["border"])
+ ent.pack(side="left", fill="x", expand=True, padx=(0, 5))
+ ent.insert(0, self.config.get(config_key, ""))
+
+ def pick():
+ p = filedialog.askopenfilename(
+ filetypes=[("Executables", "*.exe"), ("All", "*.*")])
+ if p:
+ ent.delete(0, "end")
+ ent.insert(0, p)
+ self.save(config_key, p)
+
+ ctk.CTkButton(row, text="Browse", width=70,
+ fg_color=THEME["border"], command=pick).pack(side="left")
+
+ # --- SYSTEM INFO & LOGS ---
+ def _run_hw_scan(self):
+ self.cached_hw_info = HardwareScanner.get_system_info()
+ self.after(0, lambda: self._display_hw_info(self.cached_hw_info))
+
+ def _display_hw_info(self, hw):
+ if not hasattr(self, "hw_frame") or not self.hw_frame.winfo_exists():
+ return
+ for w in self.hw_frame.winfo_children():
+ w.destroy()
+
+ grid = ctk.CTkFrame(self.hw_frame, fg_color="transparent")
+ grid.pack(padx=15, pady=10, fill="x")
+
+ data = [("OS", hw['os']), ("CPU", hw['cpu']), ("RAM",
+ f"{hw['ram_used']} / {hw['ram_total']} GB"), ("GPU", hw['gpu']), ("VRAM", f"{hw['vram']} GB")]
+
+ for i, (k, v) in enumerate(data):
+ ctk.CTkLabel(grid, text=k+":", font=("Segoe UI", 11, "bold"),
+ text_color=THEME["text_dim"]).grid(row=i//2, column=(i % 2)*2, sticky="w", padx=(0, 5))
+ ctk.CTkLabel(grid, text=v, font=("Segoe UI", 11), text_color=THEME["text_main"]).grid(
+ row=i//2, column=(i % 2)*2+1, sticky="w", padx=(0, 25))
+
+ def load_log_preview(self, textbox):
+ base = ConfigManager.get_base_path()
+ logs = glob.glob(os.path.join(base, "*_Logs", "*.log"))
+ if logs:
+ latest = max(logs, key=os.path.getmtime)
+ try:
+ with open(latest, "r", encoding="utf-8", errors="ignore") as f:
+ textbox.insert("0.0", f.read())
+ except:
+ textbox.insert("0.0", "Error reading log.")
+ else:
+ textbox.insert("0.0", "No logs found.")
+
+ # --- ACTIONS ---
def open_logs_dir(self):
base = ConfigManager.get_base_path()
- log_dirs = glob.glob(os.path.join(base, "*_Logs"))
- target = log_dirs[0] if log_dirs else base
-
- try:
- if platform.system() == "Windows":
- os.startfile(target)
- else:
- webbrowser.open(target)
- except Exception as e:
- messagebox.showerror("Error", f"Could not open folder: {e}")
+ d = glob.glob(os.path.join(base, "*_Logs"))
+ if d:
+ os.startfile(d[0]) if platform.system(
+ ) == "Windows" else webbrowser.open(d[0])
+ else:
+ messagebox.showinfo("Info", "No log folder yet.")
def clean_temp_files(self):
- if not messagebox.askyesno("Confirm Cleanup", "Are you sure you want to delete all temporary and intermediate files? This action cannot be undone."):
- return
-
- base = ConfigManager.get_base_path()
- patterns = ["*.tmp", "*.checkpoint", "*.part", "temp_*",
- "*_frames.txt", "Pipeline_temp.json", "warlock_config.json.bak"]
- count = 0
-
- for p in patterns:
- for f in glob.glob(os.path.join(base, p)):
- try:
- os.remove(f)
- count += 1
- except:
- pass
-
- messagebox.showinfo("Cleanup", f"Deleted {count} temporary files.")
+ if messagebox.askyesno("Clean", "Delete temporary files?"):
+ c = 0
+ for p in ["*.tmp", "*.part", "temp_*"]:
+ for f in glob.glob(os.path.join(ConfigManager.get_base_path(), p)):
+ try:
+ os.remove(f)
+ c += 1
+ except:
+ pass
+ messagebox.showinfo("Done", f"Removed {c} files.")
def export_debug_info(self):
- """Genera un ZIP con logs y config para facilitar reporte de bugs."""
- save_path = filedialog.asksaveasfilename(defaultextension=".zip",
- initialfile=f"Warlock_Debug_{datetime.now().strftime('%Y%m%d')}.zip")
- if not save_path:
- return
-
- try:
- base = ConfigManager.get_base_path()
- with zipfile.ZipFile(save_path, 'w', zipfile.ZIP_DEFLATED) as zf:
- # Agregar Config
- cfg_path = os.path.join(base, CONFIG_FILE)
- if os.path.exists(cfg_path):
- zf.write(cfg_path, CONFIG_FILE)
-
- # Agregar Logs recientes
- log_dirs = glob.glob(os.path.join(base, "*_Logs"))
- if log_dirs:
- for f in glob.glob(os.path.join(log_dirs[0], "*.log")):
- zf.write(f, os.path.join("Logs", os.path.basename(f)))
-
- messagebox.showinfo("Export Successful",
- f"Debug info saved to:\n{save_path}")
- except Exception as e:
- messagebox.showerror("Export Failed", str(e))
+ path = filedialog.asksaveasfilename(
+ defaultextension=".zip", initialfile=f"Debug_{datetime.now().strftime('%Y%m%d')}.zip")
+ if path:
+ with zipfile.ZipFile(path, 'w') as z:
+ cfg = ConfigManager.get_config_path()
+ if os.path.exists(cfg):
+ z.write(cfg, "config.json")
+ messagebox.showinfo("Export", "Debug info saved.")
def reset_all(self):
- if messagebox.askyesno("Reset", "Are you sure you want to reset ALL preferences to default? The application will restart."):
+ if messagebox.askyesno("RESET", "Reset ALL settings to default? App will close."):
ConfigManager.reset_config()
- messagebox.showinfo(
- "Reset", "Configuration reset. The application will now close.")
self.master.destroy()
- sys.exit(0)
+ sys.exit()
def check_updates(self):
self.btn_upd.configure(text="Checking...", state="disabled")
@@ -826,76 +822,64 @@ class PreferencesWindow(ctk.CTkToplevel):
def _update_thread(self):
um = UpdateManager(self.owner, self.repo, self.version)
- is_new, tag, url, body = um.check()
+ is_new, tag, url, _ = um.check()
- def ui_cb():
+ def cb():
self.btn_upd.configure(state="normal")
- if is_new is True:
+ if is_new:
self.btn_upd.configure(
- text=f"Update Available: {tag}", fg_color=THEME["success"], hover_color="#00C853")
- if messagebox.askyesno("Update Found", f"Version {tag} is available.\n\nOpen download page?"):
+ text=f"Update Available: {tag}", fg_color=THEME["success"])
+ if messagebox.askyesno("Update", "Open download page?"):
webbrowser.open(url)
- elif is_new is False:
- self.btn_upd.configure(
- text="You are up to date", fg_color=THEME["border"], hover_color=THEME["card_hover"])
else:
self.btn_upd.configure(
- text=f"Error checking updates: {tag}", fg_color=THEME["accent"], hover_color=THEME["accent_hover"])
-
- self.after(0, ui_cb)
+ text="Up to date", fg_color=THEME["border"])
+ self.after(0, cb)
# =============================================================================
-# BOTÓN DE ACCESO (INTEGRACIÓN)
+# BOTÓN DE INTEGRACIÓN (MAIN UI)
# =============================================================================
class PreferencesButton(ctk.CTkButton):
def __init__(self, master, current_version, repo_owner="Ivan-Ayub97", repo_name="Warlock-Studio", **kwargs):
- super().__init__(master, text="⚙", width=40, height=28,
+ super().__init__(master, text="⚙", width=40, height=30,
fg_color=THEME["bg_sidebar"], border_color=THEME["border"], border_width=1,
hover_color=THEME["accent"], text_color=THEME["text_main"],
command=self.open_window, **kwargs)
self.ver = current_version
- self.repo_data = (repo_owner, repo_name)
-
- # Aplicar configuraciones iniciales
- # Usamos after para asegurar que el widget esté cargado en la jerarquía
- self.after(100, self._apply_startup)
+ self.repo = (repo_owner, repo_name)
+ self.after(500, self._apply_startup)
def open_window(self):
- # Evitar múltiples ventanas
for w in self.winfo_toplevel().winfo_children():
if isinstance(w, PreferencesWindow):
w.lift()
w.focus_force()
return
- # Pasamos el toplevel actual como master
- PreferencesWindow(self.winfo_toplevel(), self.ver, *self.repo_data)
+ PreferencesWindow(self.winfo_toplevel(), self.ver, *self.repo)
def _apply_startup(self):
- """Aplica la configuración al iniciar la app de forma segura."""
c = ConfigManager.load_config()
-
- # Obtener la ventana raíz real (no el frame donde está este botón)
root = self.winfo_toplevel()
- # Tema
+ # Tema y Escala
try:
ctk.set_appearance_mode(c.get("app_theme", "Dark"))
except:
pass
-
- # Escala
try:
ctk.set_widget_scaling(c.get("ui_scaling", 1.0))
except:
pass
- # Opacidad
- alpha = c.get("window_opacity", 1.0)
- if 0.1 <= alpha <= 1.0:
- root.attributes("-alpha", alpha)
+ # Atributos de Ventana
+ # CORRECCIÓN: Aplicamos el valor directamente (sea True o False)
+ # Esto fuerza a la ventana a bajar si la opción está desactivada,
+ # limpiando el estado heredado del Splash Screen.
+ should_be_topmost = c.get("keep_window_on_top", False)
+ root.attributes("-topmost", should_be_topmost)
- # Siempre Visible
- top = c.get("keep_window_on_top", False)
- root.attributes("-topmost", top)
+ # Opacidad
+ if 0.5 <= c.get("window_opacity", 1.0) <= 1.0:
+ root.attributes("-alpha", c["window_opacity"])
diff --git a/warlock_theme.py b/warlock_theme.py
new file mode 100644
index 0000000..81879ff
--- /dev/null
+++ b/warlock_theme.py
@@ -0,0 +1,197 @@
+import math
+
+# -----------------------------------------------------------------------------
+# WARLOCK STUDIO: THEME & COLORS (PROFESSIONAL EDITION)
+# -----------------------------------------------------------------------------
+# Este módulo define la paleta de colores centralizada para la aplicación.
+# Diseño: Dark / Gold / Professional
+# -----------------------------------------------------------------------------
+
+
+class WarlockColors:
+ """
+ Definición estática de la paleta de colores.
+ Centraliza los códigos HEX para facilitar cambios globales.
+ """
+
+ # --- BASE & FONDO ---
+ # Negro profundo mate. Evitamos el negro absoluto (#000) en grandes áreas,
+ # pero lo usamos aquí como base para superponer capas.
+ BACKGROUND_MAIN = "#000000"
+
+ # Gris carbón muy oscuro. Fondo para paneles laterales y tarjetas.
+ BACKGROUND_WIDGET = "#1A1A1A"
+
+ # Un tono ligeramente más claro para campos de entrada (Entries) y Dropdowns.
+ BACKGROUND_ENTRY = "#232323"
+
+ # --- IDENTIDAD & ACENTOS ---
+ # "Oro Metálico". Elegante, legible y define la marca Warlock.
+ APP_TITLE = "#FBC02D"
+
+ # "Ámbar Intenso". Color principal de interacción (Checkboxes, Switches, Sliders).
+ ACCENT = "#FFC107"
+
+ # Versión más oscura del acento para estados "pressed" o bordes activos.
+ ACCENT_DARK = "#FFB300"
+
+ # "Crema Pálido". Para resaltar texto seleccionado o búsquedas.
+ HIGHLIGHT = "#FFF59D"
+
+ # --- TEXTO ---
+ # Blanco humo ("White Smoke"). Evita la fatiga visual del blanco puro (#FFFFFF).
+ TEXT_PRIMARY = "#F5F5F5"
+
+ # Gris Pizarra. Para etiquetas, subtítulos y descripciones.
+ TEXT_SECONDARY = "#9E9E9E"
+
+ # Gris oscuro. Para texto deshabilitado o placeholders.
+ TEXT_DISABLED = "#616161"
+
+ # --- INTERACCIÓN (BOTONES) ---
+ # "Rojo Sangre/Rubí". Para Hover en botones principales o acciones destructivas.
+ BUTTON_HOVER = "#C62828"
+
+ # "Vino Tinto". Para botones secundarios, info o estados inactivos pero visibles.
+ BUTTON_SECONDARY = "#7F1500"
+
+ # Color cuando se presiona click.
+ BUTTON_PRESSED = "#8E0000"
+
+ # --- ESTADOS DEL SISTEMA ---
+ # "Naranja Quemado". Advertencias legibles sobre fondo oscuro.
+ STATUS_WARNING = "#FF6F00"
+
+ # Verde Esmeralda. Éxito, confirmaciones, logs positivos.
+ STATUS_SUCCESS = "#00C853"
+
+ # "Rojo Ladrillo". Errores críticos.
+ STATUS_ERROR = "#B71C1C"
+
+ # Azul informativo (para logs neutrales o información).
+ STATUS_INFO = "#0277BD"
+
+ # --- BORDES & SEPARADORES ---
+ # Gris cálido muy oscuro. Sutil para dividir secciones.
+ BORDER = "#2D2D2D"
+
+ # Borde activo (cuando un input tiene el foco).
+ BORDER_FOCUS = "#424242"
+
+ # --- SCROLLBARS ---
+ # Rojo Caoba translúcido/oscuro. Discreto.
+ SCROLLBAR_FG = "#3E2723"
+ SCROLLBAR_BG = "#0D0D0D" # Casi negro para el riel del scroll
+
+
+class ColorUtils:
+ """
+ Utilidades para manipular colores hexadecimales en tiempo de ejecución.
+ Útil para generar variaciones de colores para animaciones o estados.
+ """
+
+ @staticmethod
+ def hex_to_rgb(hex_color: str) -> tuple:
+ """Convierte '#RRGGBB' a una tupla (r, g, b)."""
+ hex_color = hex_color.lstrip('#')
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
+
+ @staticmethod
+ def rgb_to_hex(rgb: tuple) -> str:
+ """Convierte una tupla (r, g, b) a '#RRGGBB'."""
+ return '#{:02x}{:02x}{:02x}'.format(*rgb)
+
+ @staticmethod
+ def adjust_brightness(hex_color: str, factor: float) -> str:
+ """
+ Ajusta el brillo de un color.
+ :param factor: > 1 para aclarar, < 1 para oscurecer.
+ Ej: 1.2 es 20% más claro. 0.8 es 20% más oscuro.
+ """
+ r, g, b = ColorUtils.hex_to_rgb(hex_color)
+
+ # Ajustar y asegurar que esté entre 0-255
+ r = min(255, max(0, int(r * factor)))
+ g = min(255, max(0, int(g * factor)))
+ b = min(255, max(0, int(b * factor)))
+
+ return ColorUtils.rgb_to_hex((r, g, b))
+
+# -----------------------------------------------------------------------------
+# DICCIONARIO DE TEMA (EXPORTACIÓN PRINCIPAL)
+# -----------------------------------------------------------------------------
+# Mapea las constantes a claves semánticas que la UI puede consumir directamente.
+# Se incluyen claves compatibles con CustomTkinter (fg_color, hover_color, etc).
+# -----------------------------------------------------------------------------
+
+
+THEME = {
+ # --- General ---
+ "bg": WarlockColors.BACKGROUND_MAIN,
+ "app_name": WarlockColors.APP_TITLE,
+ "accent": WarlockColors.ACCENT,
+
+ # --- Widgets (Frames, Cards) ---
+ "widget_bg": WarlockColors.BACKGROUND_WIDGET,
+ "widget_border": WarlockColors.BORDER,
+
+ # --- Texto ---
+ "text_main": WarlockColors.TEXT_PRIMARY,
+ "text_sec": WarlockColors.TEXT_SECONDARY,
+ "text_disabled": WarlockColors.TEXT_DISABLED,
+
+ # --- Botones (Estados) ---
+ "btn_fg": WarlockColors.BACKGROUND_WIDGET, # Color base del botón
+ "btn_hover": WarlockColors.BUTTON_HOVER, # Color al pasar mouse
+ "btn_secondary": WarlockColors.BUTTON_SECONDARY,
+ "btn_pressed": WarlockColors.BUTTON_PRESSED,
+
+ # --- Entradas de Texto (Inputs) ---
+ "entry_bg": WarlockColors.BACKGROUND_ENTRY,
+ "entry_border": WarlockColors.BORDER,
+ "entry_focus": WarlockColors.BORDER_FOCUS,
+
+ # --- Feedback / Estados ---
+ "success": WarlockColors.STATUS_SUCCESS,
+ "warning": WarlockColors.STATUS_WARNING,
+ "error": WarlockColors.STATUS_ERROR,
+ "info": WarlockColors.STATUS_INFO,
+
+ # --- Varios ---
+ "highlight": WarlockColors.HIGHLIGHT,
+ "scrollbar": WarlockColors.SCROLLBAR_FG,
+
+ # --- Compatibilidad Legacy (Mantiene compatibilidad con tu código anterior) ---
+ "background_color": WarlockColors.BACKGROUND_MAIN,
+ "app_name_color": WarlockColors.APP_TITLE,
+ "widget_background_color": WarlockColors.BACKGROUND_WIDGET,
+ "text_color": WarlockColors.TEXT_PRIMARY,
+ "secondary_text_color": WarlockColors.TEXT_SECONDARY,
+ "accent_color": WarlockColors.ACCENT,
+ "button_hover_color": WarlockColors.BUTTON_HOVER,
+ "border_color": WarlockColors.BORDER,
+ "info_button_color": WarlockColors.BUTTON_SECONDARY,
+ "warning_color": WarlockColors.STATUS_WARNING,
+ "success_color": WarlockColors.STATUS_SUCCESS,
+ "error_color": WarlockColors.STATUS_ERROR,
+ "highlight_color": WarlockColors.HIGHLIGHT,
+ "scrollbar_color": WarlockColors.SCROLLBAR_FG
+}
+
+# -----------------------------------------------------------------------------
+# VARIABLES GLOBALES DIRECTAS (OPCIONALES PARA ACCESO RÁPIDO)
+# -----------------------------------------------------------------------------
+BACKGROUND_COLOR = WarlockColors.BACKGROUND_MAIN
+APP_NAME_COLOR = WarlockColors.APP_TITLE
+WIDGET_BACKGROUND_COLOR = WarlockColors.BACKGROUND_WIDGET
+TEXT_COLOR = WarlockColors.TEXT_PRIMARY
+SECONDARY_TEXT_COLOR = WarlockColors.TEXT_SECONDARY
+ACCENT_COLOR = WarlockColors.ACCENT
+BUTTON_HOVER_COLOR = WarlockColors.BUTTON_HOVER
+BORDER_COLOR = WarlockColors.BORDER
+INFO_BUTTON_COLOR = WarlockColors.BUTTON_SECONDARY
+WARNING_COLOR = WarlockColors.STATUS_WARNING
+SUCCESS_COLOR = WarlockColors.STATUS_SUCCESS
+ERROR_COLOR = WarlockColors.STATUS_ERROR
+HIGHLIGHT_COLOR = WarlockColors.HIGHLIGHT
+SCROLLBAR_COLOR = WarlockColors.SCROLLBAR_FG