diff --git a/Assets/Instructions_for_downloading_bin_files.txt b/Assets/Instructions_for_downloading_bin_files.txt index 452e8f4..69667ca 100644 --- a/Assets/Instructions_for_downloading_bin_files.txt +++ b/Assets/Instructions_for_downloading_bin_files.txt @@ -1,2 +1,2 @@ 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/v4.3/Assets.zip +https://github.com/Ivan-Ayub97/Warlock-Studio/releases/download/v4.2/Assets.zip diff --git a/Assets/License.txt b/Assets/License.txt new file mode 100644 index 0000000..82d0d38 --- /dev/null +++ b/Assets/License.txt @@ -0,0 +1,116 @@ +SOFTWARE LICENSE AGREEMENT AND TERMS OF USE FOR WARLOCK-STUDIO + +--- + +PREAMBLE + +This Software License Agreement ("Agreement") constitutes a legally binding +contract between you, either as an individual or on behalf of an entity +("USER"), and Iván Eduardo Chavez Ayub ("AUTHOR"), regarding the +Warlock-Studio software and all its associated files, documentation, +and materials (collectively, the "SOFTWARE"). +By installing, copying, downloading, accessing, or otherwise using the +SOFTWARE, the USER expressly consents to and agrees to be bound by all +terms and conditions stipulated in this Agreement. +IF THE USER DOES NOT AGREE WITH ALL THE TERMS OF THIS AGREEMENT, THEY MUST +NOT INSTALL, USE, OR COPY THE SOFTWARE AND MUST IMMEDIATELY CANCEL THE +INSTALLATION PROCESS. + +--- + +SECTION I: THE MIT LICENSE + +The original and legally binding text of the MIT License is presented below. +This text governs the use of the core SOFTWARE. + +MIT License + +Copyright (c) 2025 Iván Eduardo Chavez Ayub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +--- + +SECTION II: ADDITIONAL TERMS, NOTICES, AND ACKNOWLEDGEMENTS + +1. Project Description + Warlock-Studio is a software application developed by Iván Eduardo Chavez + Ayub (GitHub profile: @Ivan-Ayub97). + The project is based on open-source + tools such as QualityScaler, FluidFrames, and RealScaler, originally + developed by Djdefrag (GitHub profile: @Djdefrag). + The main objective of + Warlock-Studio is to provide an intuitive graphical interface for + enhancing and upscaling image resolution through the use of artificial + intelligence models. +2. 🧩 THIRD-PARTY COMPONENTS AND APPLICABLE LICENSES + The SOFTWARE integrates various third-party technologies and components. + The use of the SOFTWARE is conditioned not only on compliance with this + Agreement but also with the license terms of each of these components. + The following is a list of components and their respective licenses: + + - QualityScaler, RealScaler, FluidFrames: MIT License (Djdefrag) + - RIFE: Apache 2.0 License (hzwer, Megvii Research) + - Real-ESRGAN, RealESRGAN-G, RealESR-Anime, RealESR-Net: BSD 3-Clause / Apache 2.0 License (Xintao Wang) + - GFPGAN: MIT License (TencentARC, Xintao Wang) + - BSRGAN: Apache 2.0 License (Kai Zhang) + - IRCNN: BSD / Mixed License (Kai Zhang) + - ONNX Runtime: MIT License (Microsoft) + - FFmpeg: LGPL-2.1 / GPL License (FFmpeg Team) + - ExifTool: Perl Artistic License (Phil Harvey) + - DirectML: MIT License (Microsoft) + - Python: PSF License (Python Software Foundation) + - Inno Setup: Custom Inno License (Jordan Russell) + +3. ⚖️ LIMITATION OF LIABILITY CLAUSE (EXTENDED) + TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE + AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES; INCLUDING, BUT NOT LIMITED + TO, LOSS OF DATA, BUSINESS INTERRUPTION, OR HARDWARE OR SYSTEM FAILURE + ARISING FROM THE USE, MISUSE, OR INABILITY TO USE THE SOFTWARE. + THIS SOFTWARE IS PROVIDED FOR EDUCATIONAL, CREATIVE, AND RESEARCH PURPOSES. + IT IS NOT CERTIFIED FOR IMPLEMENTATION IN CRITICAL OR COMMERCIAL + INFRASTRUCTURES WITHOUT PRIOR INDEPENDENT VALIDATION. + THE USER ASSUMES + ALL RISK ASSOCIATED WITH ITS USE. +4. 💼 INTELLECTUAL PROPERTY NOTICE + The brand, the name "Warlock-Studio" and its associated logos are the + exclusive intellectual property of Iván Eduardo Chavez Ayub. +The use of + these elements for commercial purposes is strictly prohibited without the + prior written consent of the AUTHOR. + Any redistribution or modification of + the source code must preserve the original copyright notices and all + references to the licenses contained herein. +5. ✍️ ACCEPTANCE OF TERMS + By proceeding with the installation and by using the SOFTWARE, the USER + acknowledges having read, understood, and accepted all the terms set forth + in the MIT License (Section I) and the Additional Terms (Section II) of + this Agreement. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📧 CONTACT INFORMATION + +For any inquiries or communications related to this Agreement or the SOFTWARE, +you may contact the Author: + +Name: Iván Eduardo Chavez Ayub +Email: negroayub97@gmail.com +GitHub: [https://github.com/Ivan-Ayub97](https://github.com/Ivan-Ayub97) diff --git a/Assets/Warlock-Studio_Manual.pdf b/Assets/Warlock-Studio_Manual.pdf new file mode 100644 index 0000000..dd6eeca Binary files /dev/null and b/Assets/Warlock-Studio_Manual.pdf differ diff --git a/Assets/clear_icon.png b/Assets/clear_icon.png new file mode 100644 index 0000000..02efe63 Binary files /dev/null and b/Assets/clear_icon.png differ diff --git a/Assets/info_icon.png b/Assets/info_icon.png new file mode 100644 index 0000000..320cbb7 Binary files /dev/null and b/Assets/info_icon.png differ diff --git a/Assets/logo.ico b/Assets/logo.ico new file mode 100644 index 0000000..f1983d0 Binary files /dev/null and b/Assets/logo.ico differ diff --git a/Assets/logo.png b/Assets/logo.png new file mode 100644 index 0000000..99f0acc Binary files /dev/null and b/Assets/logo.png differ diff --git a/Assets/stop_icon.png b/Assets/stop_icon.png new file mode 100644 index 0000000..fba0768 Binary files /dev/null and b/Assets/stop_icon.png differ diff --git a/Assets/upscale_icon.png b/Assets/upscale_icon.png new file mode 100644 index 0000000..19119c4 Binary files /dev/null and b/Assets/upscale_icon.png differ diff --git a/Assets/wizard-image.bmp b/Assets/wizard-image.bmp new file mode 100644 index 0000000..f8b4493 Binary files /dev/null and b/Assets/wizard-image.bmp differ diff --git a/Assets/wizard-small.bmp b/Assets/wizard-small.bmp new file mode 100644 index 0000000..83dea3a Binary files /dev/null and b/Assets/wizard-small.bmp differ diff --git a/CHANGELOG.md b/CHANGELOG.md index bba48e1..8d9aad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,48 +1,179 @@ +# Warlock-Studio - Comprehensive Changelog History + +--- + +## Version 5.0 + +**Release date:** November 22, 2025 +**Kernel Version:** 5.0.0 (Codename: NEO-Refactor) +**Architecture:** Modular / Component-Based + +### 1. Architectural Re-engineering & System Decoupling + +The application core has undergone a foundational transformation, migrating from a legacy monolithic script architecture (v4.3) to a distributed, component-oriented modular system. This refactoring significantly reduces cyclomatic complexity, enforces Separation of Concerns (SoC), and isolates failure domains for improved fault tolerance. + +#### 1.1. Component Atomization & Isolation + +The legacy `Warlock-Studio (1).py` codebase has been segmented into specialized operational subsystems: + +- **`Warlock-Studio.py` (Core Orchestrator):** + + - Retains exclusive control over the application entry point, the `App` class instantiation, and the main GUI event loop (`mainloop`). + - Manages the high-level orchestration of `multiprocessing.Process` spawning for AI workloads, ensuring UI thread non-blocking behavior. + +- **`warlock_preferences.py` (State & Hardware Abstraction Layer):** + + - A newly isolated subsystem responsible for persistent state management via JSON serialization. + - Hosts the **"NEO Engine"**, a heuristic hardware diagnostic suite. + - Manages OTA (Over-The-Air) update logic via asynchronous polling of the GitHub Releases API. + +- **`console.py` (I/O Stream Management):** + + - Encapsulates the `IntegratedConsole` widget logic and the `StreamRedirector` class. + - Implements a thread-safe **Singleton Pattern** (`ConsoleManager`) to intercept standard output (`sys.stdout`) and error streams (`sys.stderr`) at the interpreter level, redirecting them to the GUI buffer in real-time. + +- **`drag_drop.py` (OS Event Wrapper):** + - Provides an abstraction layer over `tkinterdnd2`. + - Implements the `DnDCTk` class, which inherits from `CTk` and `TkinterDnD.DnDWrapper`, injecting native OS file drag-and-drop event listeners into the custom UI toolkit. + +### 2. "NEO Engine": Telemetry, Heuristics & Persistence + +#### 2.1. Advanced Hardware Introspection (`HardwareScanner` Class) + +Implemented a robust Hardware Abstraction Layer (HAL) within `warlock_preferences.py`, utilizing `wmi`, `psutil`, and `GPUtil` libraries for deep system profiling: + +- **CPU Topology Analysis:** Differentiates between physical and logical cores to optimize `cpu_count` allocation for FFmpeg encoding and frame extraction threads. +- **Memory Mapping:** Performs real-time analysis of total vs. available RAM to preemptively throttle batch sizes and prevent page file thrashing (swapping). +- **GPU Heuristic Detection:** Implements a deterministic priority chain for VRAM detection: + 1. **Priority A:** `GPUtil` (Direct NVIDIA API access). + 2. **Priority B:** `WMI` (Win32_VideoController query for AMD/Intel). + 3. **Fallback:** Synthetic estimation based on system heuristics. +- **Algorithmic Optimization (`get_smart_recommendations`):** A logic engine that accepts hardware telemetry and computes the **"Safe VRAM Limit"** via the formula `max(0.5, vram_gb - 1.5)`. It dynamically prescribes Tile Resolutions and Thread Concurrency based on hardware tiers (Low-End vs. High-End). + +#### 2.2. Serialized Configuration Management (`ConfigManager`) + +- **JSON State Persistence:** Deprecated hardcoded global variables in favor of a structured `warlock_config.json` file. +- **Robust Serialization:** The `ConfigManager` handles the marshaling and unmarshaling of user preferences (UI scaling factors, window opacity, process priority). +- **Integrity Validation:** The `load_config` method implements schema validation, automatically injecting default key-value pairs if the configuration file is corrupted or missing fields during boot. + +### 3. AI Inference Engine Stabilization (ONNX Runtime Backend) + +#### 3.1. Deterministic Session Initialization + +- **Strict Type Enforcement:** The `create_onnx_session` factory was hardened to enforce strict integer typing (`int`) for the `device_id` parameter. + - _Correction:_ In v4.3, passing device IDs as string literals (e.g., `"0"`) caused silent initialization failures on strict `DirectML` backends. The new logic creates an explicit mapping table (`'GPU 1' -> 0`). +- **Hierarchical Execution Priority:** Implemented a failover chain for `ExecutionProviders` to maximize hardware acceleration compatibility: + 1. **Primary:** `CUDAExecutionProvider` (NVIDIA Optimized via cuDNN). + 2. **Secondary:** `DmlExecutionProvider` (DirectX 12 Abstraction Layer). + 3. **Failover:** `CPUExecutionProvider` (Universal x64 instruction set). + +#### 3.2. Precision Standardization in Face Restoration (`AI_face_restoration`) + +- **Mandatory Float32 Inference:** The experimental `fp16` (half-precision) auto-detection logic used in v4.3 was deprecated. + - _Rationale:_ It caused `NaN` (Not a Number) tensor propagation on older GPUs lacking dedicated Tensor Cores. The engine now defaults to **`float32`** unless the ONNX model metadata explicitly mandates a lower precision dtype. +- **"Split-Merge" Channel Strategy:** A sophisticated pipeline was developed to handle RGBA (Transparency) artifacts: + 1. **Channel Splitting:** The Alpha channel is isolated from the RGB tensor. + 2. **Hybrid Scaling:** RGB channels are processed via the Neural Network; the Alpha channel is scaled using **Bicubic Interpolation** (`INTER_CUBIC`) to preserve edge fidelity without introducing GAN-generated noise into the transparency mask. + 3. **Tensor Reconstruction:** Post-inference concatenation (`numpy.concatenate`) reassembles the final image tensor. + +#### 3.3. Heuristic OOM (Out-Of-Memory) Recovery + +- **Recursive Dynamic Tiling:** The `upscale_video_frames_async` pipeline now wraps inference calls within a specialized `try...except` block listening for specific `cuda`, `memory`, or `allocation` exceptions. +- **Recovery Algorithm:** Upon trapping a VRAM overflow event, the orchestrator intercepts the crash, dynamically downscales the `max_resolution` (Tile Size) by a factor of 2 (e.g., `100% -> 50%`), and triggers a recursive retry of the failed frame. This ensures batch job completion even during transient memory spikes. + +### 4. Video Processing Pipeline & FFmpeg Orchestration + +#### 4.1. Redundant Encoding Pipeline (Codec Fallback Loop) + +The `video_encoding` function has been refactored into a transactional state machine with automatic rollback and retry capabilities: + +- **Attempt 0 (Hardware Acceleration):** Initiates encoding using the user-selected hardware codec (e.g., `hevc_nvenc`, `h264_amf`, `hevc_qsv`). +- **Failure Handling:** Captures `subprocess.CalledProcessError` or non-zero exit codes (indicative of driver timeouts or resource locking). +- **Attempt 1 (Software Failover):** Automatically switches context to the CPU-based `libx264` encoder. This guarantees output file delivery even in catastrophic GPU driver failure scenarios. + +#### 4.2. Lossless Intermediate Serialization + +- **PNG Standardization:** The `extract_video_frames` function has strictly deprecated the use of `.jpg` (v4.3) for temporary frame storage. The pipeline now mandates **`.png`** containers, eliminating compression artifacts (generation loss) before the pixel data enters the neural network input layer. + +#### 4.3. Temporal Frame Synchronization + +- **FPS Multiplier Logic:** An explicit `fps_multiplier` argument was injected into the `video_encoding` signature. + - _Functionality:_ This allows the orchestrator to mathematically calculate the target framerate (`source_fps * multiplier`) specifically for **RIFE Interpolation** tasks (x2, x4, x8), ensuring frame-perfect Audio/Video synchronization in the final container muxing. + +### 5. UI/UX & Advanced Diagnostic Tools + +#### 5.1. Integrated Debugging Console (`IntegratedConsole`) + +- **Stream Interception:** The `StreamRedirector` class hooks into the Python interpreter's I/O layer to capture `sys.stdout` and `sys.stderr`. +- **Real-Time Visualization:** A dedicated `CTkTextbox` widget renders logs with Regex-based syntax highlighting (INFO=Blue, ERROR=Red, WARNING=Yellow, SUCCESS=Green), empowering end-users to diagnose underlying FFmpeg or ONNX runtime issues without external CLI tools. + +#### 5.2. Dynamic DOM Adaptability (FluidFrames Integration) + +- **Contextual Widget Injection:** Implemented the `clear_dynamic_menus` routine to manipulate the widget tree (DOM) at runtime. + - _Behavior:_ Upon selecting a **RIFE** model, "Blending" controls are destroyed and "Frame Generation" selectors (Slowmotion factors) are dynamically injected into the layout. This prevents invalid configuration states at the UI level. + +#### 5.3. Window Management & Responsive Layout + +- **Reactive Geometry Engine:** The `window.resizable(True, True)` flag was enabled, and the initial viewport geometry was increased to **85%** of screen height. The `grid` and `place` geometry managers were recalibrated to respond dynamically to window resizing events, accommodating the new bottom-docked console. + +### 6. Deployment & System Integration Strategy + +#### 6.1. Relocation of Default Installation Directory + +- **Privilege Escalation Mitigation:** The default installation target within the Inno Setup script (`.iss`) has been migrated from `%ProgramFiles%` to **`%userprofile%\Documents\Warlock-Studio`**. +- **Write-Access Assurance:** This critical deployment change addresses `PermissionDenied` (WinError 5) exceptions encountered in v4.3 on systems with strict UAC (User Account Control) policies. By deploying to the user space, the application guarantees atomic read/write access for: + - Serialization of the **`warlock_config.json`** file. + - Real-time log appending to the **`Warlock_Logs`** directory. + - Execution of the internal **PDF Manual** without invoking elevated Administrator privileges. + - Unimpeded execution of the **NEO Engine** hardware probes without file system virtualization or sandbox interference. + +--- + ## Version 4.3 **Release date:** November 15, 2025 -### **1. Critical Stability & Process Synchronization** +### 1. Critical Stability & Process Synchronization -#### **1.1. Forced Writer Thread Synchronization (Critical Stability Fix)** +#### 1.1. Forced Writer Thread Synchronization (Critical Stability Fix) - Implemented explicit and mandatory synchronization using the `t.join()` function for **all frame writing threads** (`writer_threads`) within the main video orchestrator (`upscale_orchestrator`). - This change is **critical for workflow integrity**, ensuring that all AI-upscaled frames are completely written and closed on disk before initiating the FFmpeg video encoding process. - **Technical Impact:** Resolves intermittent encoding failures (`File Not Found` or `Input/Output error`) that could occur when FFmpeg attempted to read files still being written by concurrent threads. -#### **1.2. Graceful Termination for Process Monitoring Thread** +#### 1.2. Graceful Termination for Process Monitoring Thread - The status monitoring thread (`upscale_monitor_thread`) has been enhanced to handle unexpected failures or abrupt terminations of the main upscaling process. - An explicit `break` statement was added to the `except Exception` block that manages errors reading from the status queue (`process_status_q`). - **Technical Impact:** If the main upscaling process (`Process`) fails or hangs, the monitoring thread now **terminates its execution gracefully and immediately** instead of becoming a "zombie thread," allowing the user interface to correctly reset global status controls. -#### **1.3. Reinforced Frame Sequence Integrity Validation** +#### 1.3. Reinforced Frame Sequence Integrity Validation - A robust verification function was introduced to validate the integrity of the frame sequence on disk before starting the AI upscaling. - This measure is vital for `Resume` operations and now explicitly validates: - The **physical existence** of every frame path (`os_path_exists`). - The **readability** of the image file (`image_read`), preventing segmentation faults or memory errors if the file is corrupted or incomplete. -### **2. Performance Improvements and Technical Refinements** +### 2. Performance Improvements and Technical Refinements -#### **2.1. Aggressive Post-Process Memory Optimization** +#### 2.1. Aggressive Post-Process Memory Optimization - A memory optimization step (`optimize_memory_usage`) with calls to `gc.collect()` was implemented after the last upscaled frames are saved to disk and before video encoding starts. - **Purpose:** To reduce peak RAM usage by aggressively freeing any residual NumPy arrays and image references held in memory, ensuring the system has maximum memory available for the FFmpeg encoding subprocess. -#### **2.2. Clarification of Tiling and VRAM Logic** +#### 2.2. Clarification of Tiling and VRAM Logic - Detailed internal documentation (`# comments`) was added to the validation function (`check_upscale_settings`) to **explain the calculation logic for tile size** based on the user's VRAM limiter and the AI model's usage factor. - **Clarified Formula:** `tiles_resolution = (ModelUsageFactor * VRAM_GB) * 100`. This is a technical refinement ensuring transparency and justification for the internal calculation. -#### **2.3. Robustness and Clarification in Video Encoding (FFmpeg Command Line) 🎬** +#### 2.3. Robustness and Clarification in Video Encoding (FFmpeg Command Line) 🎬 - **Explicit Stream Mapping:** Within `create_video_with_ffmpeg`, the FFmpeg command line now uses **more explicit and safer stream mapping** (`-map 0:v:0 -map 0:a:0?`). This ensures that the first video stream is selected obligatorily, and the first audio stream is selected optionally, resolving ambiguity issues in inputs containing multiple video or audio streams. - **Data Stream Exclusion:** The **`-dn`** (Disable Data stream) argument was introduced into the base FFmpeg command line. This instructs the encoder to ignore unnecessary _data streams_ (such as subtitles or complex metadata) that are not relevant to the video or audio, **simplifying the muxing process and improving compatibility** across various players. -### **3. Aesthetic and UI/UX Refinements** +### 3. Aesthetic and UI/UX Refinements -#### **3.1. Complete Aesthetic Overhaul: Crimson Gold Dark Theme 🌑** +#### 3.1. Complete Aesthetic Overhaul: Crimson Gold Dark Theme 🌑 - The application's entire color scheme was replaced (migrating from a v4.2.1 red/yellow palette) with a high-contrast **"Crimson Gold Dark Theme"**: - **Background:** Deep Black (`#121212`). @@ -50,71 +181,67 @@ - **Primary Accent:** Pure Gold (`#FFD700`). - Dropdown menus (`CTkOptionMenu`) were styled with explicit **`corner_radius=0`** for a more modern, defined, and flat aesthetic. -#### **3.2. Detailed Visualization of File Information in Queue** +#### 3.2. Detailed Visualization of File Information in Queue - Clarity of information in the queue widget (`FileWidget.add_file_information`) was improved by breaking down the complete resolution transformation sequence into three lines, eliminating ambiguity about intermediate dimensions: - **AI Input:** Shows the final input resolution. - **AI Output:** Shows the native resolution after upscaling by the AI model factor. - **Video Output:** Shows the final resolution of the encoded video. -#### **3.3. Visual Consistency and Updated Menu Separators** +#### 3.3. Visual Consistency and Updated Menu Separators - The visual separator in all dropdown menus has been updated from the string `"<------------------>"` (v4.2.1) to a cleaner, more discreet sequence of dots: **`• • • • • • • • • • • •`**. -#### **3.4. Refined Dynamic Menu Logic** +#### 3.4. Refined Dynamic Menu Logic - The conditional visibility logic for menus in `select_AI_from_menu` was refined for a smoother user experience: - The **Frame Interpolation** control is only visible for RIFE models. - The **Blending** control is only visible for Upscaling/Facial Restoration models (with automatic disabling logic for GFPGAN). - - --- + +--- ## Version 4.2.1 **Release date:** 27 October 2025 -### **1. Critical Bug Fixes & Core Functionality** +### 1. Critical Bug Fixes & Core Functionality -#### **1.1. Resolved Critical Audio Passthrough Failure in Video Encoding** +#### 1.1. Resolved Critical Audio Passthrough Failure in Video Encoding - Addressed a major bug introduced in v4.2 where all generated videos were encoded without audio. This was caused by the accidental omission of the `ffprobe.exe` executable (a key component of the FFmpeg suite) from the application's `Assets` directory and the final build package. - The `video_encoding` pipeline relies on media analysis (performed by `ffmpeg` or `ffprobe`) to detect the presence of audio streams in the source video. Due to the missing executable, the `audio_info_command` subprocess would fail, causing the application to incorrectly assume all input videos had no audio. - By restoring the `ffprobe.exe` binary to the application bundle, the audio detection step now executes successfully, enabling the proper copying (`-c:a copy`) or re-encoding (`-c:a aac`) of the original audio track into the final processed video. -#### **1.2. Fixed Invalid UI State Persistence** +#### 1.2. Fixed Invalid UI State Persistence - Addressed a critical UI logic bug where AI model settings could persist incorrectly when switching between model types. The `select_AI_from_menu` function now properly resets conflicting global variables: - **RIFE Selection:** When an interpolation model (e.g., RIFE, RIFE_Lite) is selected, the **`selected_blending_factor`** is now automatically reset to `0` (OFF), as blending is not a valid operation for this model. - **Upscaling/Face Selection:** Conversely, when any upscaling or face restoration model (e.g., BSRGAN, GFPGAN) is selected, the **`selected_frame_generation_option`** is automatically reset to `OFF`. - This prevents users from applying invalid or non-functional setting combinations, ensuring a more intuitive and error-free workflow. -#### **1.3. Fixed File Thumbnail Generation Crash** +#### 1.3. Fixed File Thumbnail Generation Crash - Reworked the `FileWidget.extract_file_icon` method to be more robust during the conversion of an OpenCV (NumPy) image to a `CTkImage` for file previews. - The process no longer relies on the `mode='RGB'` parameter within `pillow_image_fromarray`, which could fail with certain `numpy` array layouts. - It now uses an explicit, two-step conversion (`pil_img = pillow_image_fromarray(source_icon)` followed by `pil_img = pil_img.convert("RGB")`), ensuring correct channel order and preventing potential `Pillow`/`numpy` compatibility crashes when loading file thumbnails. ---- +### 2. UI Corrections & Usability Enhancements -### **2. UI Corrections & Usability Enhancements** - -#### **2.1. Corrected Supported File Extension List** +#### 2.1. Corrected Supported File Extension List - The helper text on the file selection screen has been corrected to more accurately list the supported file extensions. It now properly includes `jpeg` and `tiff` and removes incorrect entries (`heic`, `gif`, `mpg`, `qt`, `3gp`) to align with the application's actual processing capabilities. -#### **2.2. Implemented Fixed Application Window** +#### 2.2. Implemented Fixed Application Window - The main application window is now non-resizable (`window.resizable(False, False)`). This change was implemented to ensure a stable and predictable UI layout, preventing the relative-coordinate-based widget placement (`relx`, `rely`) from breaking, overlapping, or scaling improperly when the window is resized. -#### **2.3. Improved Dialog Window Behavior** +#### 2.3. Improved Dialog Window Behavior - Removed the `self.attributes("-topmost", True)` flag from the `MessageBox` class (which handles error and info dialogs). This stops popup windows from aggressively forcing themselves to be the top-most window on the entire desktop. Dialogs now behave as standard application-modal windows, resolving a key usability issue where they could "steal focus" or obstruct other programs. ---- +### 3. Thematic & Visual Redesign -### **3. Thematic & Visual Redesign** - -#### **3.1. New "Inferno" Thematic Redesign** +#### 3.1. New "Inferno" Thematic Redesign - Version 4.2.1 introduces a significant visual overhaul, moving away from the previous gold-and-grey theme to a high-contrast, dark-mode "inferno" theme. This new palette uses a deep red background with bright yellow and white text, designed to improve readability and reduce eye strain. @@ -131,11 +258,11 @@ The color scheme has been updated as follows: | **Warning Color** | `#E02CDA` (Magenta) | `#FF8C00` (Orange) | | **Error Color** | `#070087` (Dark Blue) | `#DC143C` (Crimson) | -#### **3.2. Monospaced Typography Shift** +#### 3.2. Monospaced Typography Shift - The application's global font has been changed from `"Segoe UI"` to `"Consola"`. This provides a more uniform, technical aesthetic across all UI elements, enhancing readability for file names, numeric values, and status messages. -#### **3.3. Dropdown Menu Readability** +#### 3.3. Dropdown Menu Readability - Updated the visual separators in dropdown menus from `----` to `<------------------>` for clearer, more pronounced grouping of model categories. @@ -145,49 +272,37 @@ The color scheme has been updated as follows: **Release date:** 6 October 2025 -### **1. Core Architecture & Performance Engineering** +### 1. Core Architecture & Performance Engineering -#### **1.1. Advanced ONNX Runtime Integration & Execution Provider Strategy** +#### 1.1. Advanced ONNX Runtime Integration & Execution Provider Strategy - **Intelligent Provider Prioritization & Fallback Mechanism**: The core AI model loading architecture has been fundamentally re-engineered for superior performance and resilience. The new implementation leverages a prioritized provider list (`['CUDAExecutionProvider', 'DmlExecutionProvider', 'CPUExecutionProvider']`) within the `onnxruntime.InferenceSession` constructor. It first attempts to initialize a session using the **`CUDAExecutionProvider`**, which offers the highest performance by interfacing directly with NVIDIA's CUDA cores and Tensor Cores. If this fails (due to lack of an NVIDIA GPU, driver issues, or CUDA toolkit incompatibility), the system gracefully falls back and attempts initialization with the **`DmlExecutionProvider`**, utilizing Windows' DirectML API for broader hardware acceleration across various GPU vendors (NVIDIA, AMD, Intel). As a final failsafe, if no GPU acceleration provider can be initialized, it defaults to the **`CPUExecutionProvider`**, ensuring the application remains functional on any machine, albeit with reduced performance. - - **Centralized Session Management & Code Refactoring**: A new, unified function, `create_onnx_session`, has been introduced to abstract and centralize all ONNX session creation logic. This eliminates the redundant and error-prone boilerplate code that was previously duplicated within the `_load_inferenceSession` method of each individual AI class (`AI_upscale`, `AI_interpolation`, `AI_face_restoration`). This refactoring adheres to the **Don't Repeat Yourself (DRY)** principle, significantly improving code maintainability, reducing the surface area for bugs, and ensuring a consistent and robust model loading strategy across the entire application. - - **Enhanced Error Handling & Diagnostics**: The `try...except` block encapsulating the session creation loop is now more sophisticated. It logs specific warnings when a provider fails to initialize and clearly indicates which provider it is falling back to. This provides transparent diagnostics that are critical for troubleshooting performance issues related to hardware acceleration. -#### **1.2. PyInstaller Packaging & Distribution Overhaul** +#### 1.2. PyInstaller Packaging & Distribution Overhaul - **Aggressive Dependency Pruning for Size Optimization**: The PyInstaller `.spec` file has been meticulously optimized to drastically reduce the final distribution size. An extensive `excludes` list has been implemented to explicitly instruct PyInstaller's analysis engine to ignore and not bundle large, non-essential packages. This prevents the recursive inclusion of entire scientific and machine learning ecosystems like `torch`, `transformers`, `matplotlib`, `pandas`, `scipy`, and `PyQt5`, which are often pulled in as sub-dependencies but are not required for this application's runtime. This strategic pruning reduces the package size by hundreds of megabytes. - - **Robust Hidden Import Declaration for Runtime Stability**: The `.spec` file now includes a `hiddenimports` list containing `onnxruntime.capi._pybind_state`, `onnxruntime.providers`, and `moviepy.editor`. This is critical for ensuring runtime stability, as these modules are often loaded dynamically (e.g., via `__import__` or C extensions) in a way that PyInstaller's static analysis cannot detect. Explicitly declaring them forces their inclusion, preventing `ModuleNotFoundError` crashes when the packaged application attempts to access these components. - - **Strategic Shift to "One-Folder" Distribution**: The build strategy has been transitioned from a "one-file" mode to a more robust and efficient "one-folder" mode. This is achieved by using the `COLLECT` block in the `.spec` file. Instead of bundling all dependencies into a single large executable that must be decompressed to a temporary directory (`_MEIPASS`) on every launch, the "one-folder" approach places the executable alongside its required `.dll`, `.pyd`, and data files. This results in significantly faster application startup times and avoids potential conflicts with antivirus software or system permissions related to executing from temporary locations. ---- +### 2. Installer & Distribution Strategy Refinement -### **2. Installer & Distribution Strategy Refinement** - -#### **2.1. Transition to a Full Offline Installer Model** +#### 2.1. Transition to a Full Offline Installer Model - **Self-Contained & Hermetic Package**: The application's distribution model has been fundamentally shifted from a lightweight online/web installer to a comprehensive **offline installer**. All required runtime assets, most notably the large AI model files (`.onnx`), are now bundled directly within the Inno Setup executable. This creates a fully self-contained package that guarantees a successful installation without any external dependencies. - - **Elimination of Network Dependencies & Points of Failure**: This architectural change enhances installation reliability exponentially. It completely removes the dependency on an active internet connection during setup and eliminates critical points of failure, such as GitHub/SourceForge server downtime, network interruptions, firewalls, or changes in download URLs. The user is assured of a complete, atomic installation from a single authoritative file. - - **Radical Simplification of Installer Logic**: As a direct result of bundling all assets, the entire Pascal Script `[Code]` section in `Setup.iss` has been **completely removed**. This eliminates dozens of lines of complex logic responsible for creating custom download pages, handling HTTP requests, managing user cancellations, and, most critically, invoking external processes like PowerShell for archive extraction (`Expand-Archive`). This simplification makes the installer script significantly more robust, predictable, and easier to maintain. - - **Streamlined User Experience**: The installer's UI has been simplified by removing the "Download AI Models" task from the `[Tasks]` section. This avoids user confusion and streamlines the setup process, as the action is no longer necessary. The output filename is also now explicitly suffixed with `-Full-Installer` to clearly communicate its offline nature. ---- +### 3. UI, UX & Developer Experience Enhancements -### **3. UI, UX & Developer Experience Enhancements** - -#### **3.1. Enhanced Application Startup & Initial Feedback** +#### 3.1. Enhanced Application Startup & Initial Feedback - **Professional Splash Screen Implementation**: A new `SplashScreen` class provides immediate visual feedback upon application launch. This improves the _perceived performance_ by displaying a branded loading screen with status messages while core components and AI models are being initialized in the background. It prevents the appearance of a hung or unresponsive application during the initial loading phase. - - **Runtime Environment Diagnostics**: At startup, the application now programmatically queries and prints the available ONNX Runtime providers by calling `onnxruntime.get_available_providers()`. This information is outputted to the console, serving as an invaluable, zero-effort diagnostic tool. It allows both end-users and developers to instantly verify which hardware acceleration backends are detected and available to the application, aiding in performance tuning and troubleshooting. -#### **3.2. Improved Debugging and Diagnosability** +#### 3.2. Improved Debugging and Diagnosability - **Persistent Console for Runtime Output**: The PyInstaller build configuration was modified to set `console=True`. This forces the application to run with an attached console window that captures the `stdout` and `stderr` streams. This is a critical enhancement for diagnostics, as it makes all print statements, logging output, warnings, and unhandled exception tracebacks immediately visible, providing a clear and persistent record of the application's runtime behavior for effective bug reporting and debugging. @@ -197,51 +312,51 @@ The color scheme has been updated as follows: **Release date:** 1 August 2025 -### Model Enhancement and Utilization +### 1. Model Enhancement and Utilization -#### 1.1 **Enhanced GPU Utilization and Error Handling** +#### 1.1. Enhanced GPU Utilization and Error Handling - **Robust Provider Configuration**: Added a private method `_select_providers` to intelligently select ONNX runtime providers based on the chosen GPU and improve model execution efficiency. - **Dynamic Fall-back Mechanism**: Enhanced `_load_inferenceSession` to prioritize loading models on GPU providers and gracefully fallback to CPU providers if GPU initialization fails, coupled with detailed logging. - **Improved Model Initialization**: Ensured comprehensive validation and error handling during model loading to enhance stability across various hardware environments. -### Compatibility and Runtime Improvements +### 2. Compatibility and Runtime Improvements -#### 2.1 **Addressed NumPy and OpenCV Compatibility** +#### 2.1. Addressed NumPy and OpenCV Compatibility - **Resolved Import Errors**: Fixed critical compatibility issues between NumPy 2.x and OpenCV by downgrading to NumPy 1.26.4, preventing `_ARRAY_API not found` and `numpy.core.multiarray failed to import` errors. - **Critical Module Validation**: Ensured that all critical libraries (OpenCV, ONNX Runtime, CustomTkinter) load successfully without compatibility warnings, enhancing overall application reliability. - **Runtime Environment Stability**: Resolved module loading conflicts that previously caused application crashes during startup. -### Performance Optimization +### 3. Performance Optimization -#### 3.1 **Memory and Resource Management** +#### 3.1. Memory and Resource Management - **Contiguous Memory Utilization**: Enhanced use of `numpy.ascontiguousarray` throughout image processing pipelines to optimize memory usage during intensive AI processing tasks. - **GPU Memory Error Recovery**: Improved handling of GPU memory allocation failures with automatic fallback to CPU processing when DirectML providers are unavailable. - **Processing Pipeline Optimization**: Streamlined AI model loading and inference execution for better resource utilization across different hardware configurations. -### Code Quality and Error Handling +### 4. Code Quality and Error Handling -#### 4.1 **Enhanced Error Resilience** +#### 4.1. Enhanced Error Resilience - **Syntax Error Resolution**: Fixed critical syntax error in `_load_inferenceSession` method that prevented proper AI model initialization. - **Improved Error Messaging**: Enhanced logging capabilities with more informative error messages and warnings for better user diagnostics and troubleshooting. - **Graceful Degradation**: Implemented improved fallback strategies for GPU resource issues, effectively preventing application crashes by dynamically adjusting processing pathways. - **Provider Validation**: Added comprehensive validation for ONNX runtime providers with automatic fallback from GPU to CPU execution when hardware acceleration is unavailable. -### UI and User Experience +### 5. UI and User Experience -#### 5.1 **Application Stability and Feedback** +#### 5.1. Application Stability and Feedback - **Startup Reliability**: Resolved critical startup issues that prevented the application from launching due to module compatibility problems. - **Processing Status Updates**: Enhanced real-time feedback during AI model loading and image/video processing operations. - **Error Notification**: Improved error dialogs and status messages to provide clearer information about processing states and potential issues. - **Hardware Compatibility**: Better detection and handling of different GPU configurations, with informative warnings when falling back to CPU processing. -### Technical Improvements +### 6. Technical Improvements -#### 6.1 **Model Loading Architecture** +#### 6.1. Model Loading Architecture - **Modular Provider Selection**: Separated provider selection logic into dedicated methods for better code organization and maintainability. - **Robust Model Validation**: Enhanced file existence checking and model integrity validation before attempting to load AI models. @@ -253,9 +368,9 @@ The color scheme has been updated as follows: **Release date:** 27 July 2025 -### Model Cleanup and Optimization +### 1. Model Cleanup and Optimization -#### 1.1 **SuperResolution-10 Model Removal** +#### 1.1. SuperResolution-10 Model Removal - **Model Deprecation**: Removed the SuperResolution-10 model from the application due to performance and compatibility issues. - **Code Cleanup**: Eliminated the dedicated `AI_super_resolution` class and all related processing pipelines. @@ -264,7 +379,7 @@ The color scheme has been updated as follows: - **Streamlined Processing**: Simplified the upscaling orchestrator by removing SuperResolution-specific routing logic. - **Model List Cleanup**: Removed `SuperResolution_models_list` from the main AI models collection. -#### 1.2 **Performance Improvements** +#### 1.2. Performance Improvements - **Reduced Memory Footprint**: Application now uses less memory without the SuperResolution-10 model overhead. - **Simplified Code Paths**: Cleaner processing logic with fewer conditional branches for model selection. @@ -278,57 +393,57 @@ The color scheme has been updated as follows: ### 1. AI Model Integration & Enhancement -#### 1.1 **SuperResolution-10 Model Implementation** +#### 1.1. SuperResolution-10 Model Implementation - **New AI Model Integration**: Added support for the SuperResolution-10 model, providing advanced super-resolution capabilities with 10x upscaling factor. This model is specifically designed for very low-resolution images and excels at significant resolution increases. - **Specialized Processing Pipeline**: Created a dedicated `AI_super_resolution` class that inherits from the new `AI_model_base` class, implementing proper preprocessing (CHW format conversion, normalization) and postprocessing (HWC format conversion, value clipping) for optimal results. - **Model Information Integration**: Added comprehensive model information in the AI model selector dialog, including year (2023), function (high-resolution image enhancement), and specialized use cases. -#### 1.2 **AI Architecture Improvements** +#### 1.2. AI Architecture Improvements - **Base Class Implementation**: Created the missing `AI_model_base` class that provides common functionality for all AI models, including ONNX model loading with GPU acceleration support and proper error handling. - **Enhanced Model Loading**: Implemented robust model loading with provider selection (DML, CPU) and comprehensive error handling for missing model files. - **VRAM Management**: Added VRAM usage information for SuperResolution-10 (0.8 GB) to help users optimize their GPU memory usage. -#### 2. Code Quality & Stability +### 2. Code Quality & Stability -##### 2.1 **Import System Optimization** +#### 2.1. Import System Optimization - **Fixed Import Errors**: Resolved critical `NameError: name 'numpy_ndarray' is not defined` by properly organizing imports at the top of the file. - **Consolidated Imports**: Removed duplicate import sections and properly structured the import hierarchy for better maintainability. - **Type Annotation Fixes**: Corrected type annotations throughout the codebase to use the proper imported numpy types. -##### 2.2 **Enhanced Error Handling** +#### 2.2. Enhanced Error Handling - **Model Loading Resilience**: Implemented try-catch blocks for model loading operations with meaningful error messages. - **Graceful Degradation**: Added fallback mechanisms that return the original image if super-resolution enhancement fails, ensuring the application never crashes. - **Debug Information**: Enhanced logging with model loading status and error reporting for better troubleshooting. -#### 3. User Interface Updates +### 3. User Interface Updates -##### 3.1 **Model Selection Enhancement** +#### 3.1. Model Selection Enhancement - **Updated Model List**: SuperResolution-10 is now properly integrated into the AI model dropdown menu and categorized appropriately. - **Information Dialog Updates**: Added detailed information about the SuperResolution-10 model in the help dialog, including its capabilities and recommended use cases. - **Model Orchestration**: Enhanced the upscaling orchestrator to properly detect and route SuperResolution model tasks to the appropriate processing pipeline. -#### 4. Technical Improvements +### 4. Technical Improvements -##### 4.1 **Processing Pipeline Optimization** +#### 4.1. Processing Pipeline Optimization - **Specialized Image Processing**: Implemented dedicated image processing functions for super-resolution models that handle the unique requirements of the SuperResolution-10 model. - **Memory Efficiency**: Optimized image preprocessing and postprocessing to minimize memory usage during super-resolution operations. - **Performance Monitoring**: Added processing time tracking for super-resolution operations to help users understand processing performance. -#### 4.2 **Integration Completeness** +#### 4.2. Integration Completeness - **Full Model Integration**: SuperResolution-10 is now fully integrated into all aspects of the application, from model selection to processing to output generation. - **Consistent User Experience**: The super-resolution workflow follows the same patterns as other AI models, ensuring a consistent user experience. - **Quality Assurance**: Implemented comprehensive testing to ensure the SuperResolution-10 model works correctly with both individual images and batch processing. -### 5. **Smart AI Model Distribution System** +### 5. Smart AI Model Distribution System -#### 5.1 **Automatic Model Download** +#### 5.1. Automatic Model Download - **Lightweight Installer**: Significantly reduced installer size from 1.4GB to approximately 300MB by removing AI models from the installation package. - **On-Demand Download**: Implemented intelligent model downloading system that automatically fetches required AI models (327MB) when the application is first launched. @@ -336,13 +451,13 @@ The color scheme has been updated as follows: - **Fallback URLs**: Integrated multiple download sources (GitHub Releases, SourceForge) to ensure reliable model availability. - **Resume Capability**: Download system supports resuming interrupted downloads and validates file integrity. -#### 5.2 **PyInstaller Optimization** +#### 5.2. PyInstaller Optimization - **Optimized Packaging**: Updated `.spec` file to exclude AI model directory from executable packaging, reducing final executable size by over 1GB. - **Enhanced Dependencies**: Added model downloader module to the build process with proper hidden imports for requests, threading, and file handling libraries. - **Improved Compression**: Increased optimization level and added module exclusions to further reduce executable size. -#### 5.3 **Installation Experience** +#### 5.3. Installation Experience - **Smart Setup Script**: Created enhanced Inno Setup configuration that can optionally download models during installation or defer to first-run. - **User Choice**: Users can choose between offline installation (models downloaded on first run) or full installation with models included. @@ -357,7 +472,7 @@ The color scheme has been updated as follows: ### 1. Major Features & Core Capabilities -#### 1.1 **AI-Powered Face Restoration (GFPGAN)** +#### 1.1. AI-Powered Face Restoration (GFPGAN) - **New `AI_face_restoration` Class**: A new, specialized class has been implemented to handle face restoration models. This class is architected to manage the unique preprocessing and post-processing requirements of models like GFPGAN, distinct from standard upscaling models. - **GFPGAN Model Integration**: The GFPGAN v1.4 model has been added to the AI model repository and is now selectable from the UI. It is listed under a new `Face_restoration_models_list` category. The main orchestrator (`upscale_orchestrator`) now detects when a face restoration model is selected and routes the task to the appropriate `AI_face_restoration` instance. @@ -365,7 +480,7 @@ The color scheme has been updated as follows: ### 2. UI/UX Modernisation -#### 2.1 **Complete Thematic Redesign** +#### 2.1. Complete Thematic Redesign - The application has undergone a significant visual overhaul with a new, professionally designed color scheme to improve aesthetics and user comfort during long sessions. The new theme provides better contrast and a more modern look. @@ -378,104 +493,104 @@ The color scheme has been updated as follows: | Button Hover | `#FF6666` (Light Red) | `background_color` | | Info Button | `#B22222` (Dark Red) | `widget_background_color` | -#### 2.2 **Enhanced Splash Screen** +#### 2.2. Enhanced Splash Screen - **Dynamic Progress Bar**: The splash screen now features a `CTkProgressBar` to provide visual feedback on the application's loading status, enhancing the startup experience. - **Smooth Fade-Out Animation**: A new `fade_out` method using a cosine function has been implemented for a smooth, animated exit transition instead of an abrupt disappearance. - **Improved Information Display**: The splash screen now prominently displays the application version number. -#### 2.3 **Redesigned and Resizable Message Boxes** +#### 2.3. Redesigned and Resizable Message Boxes - The `MessageBox` class was significantly improved to handle large blocks of text, such as detailed error messages. It now implements a `CTkScrollableFrame`, ensuring that content is always accessible without forcing the dialog to an unmanageable size. - The dialogs now have defined `minsize` and `maxsize` properties for better window management. -#### 2.4 **Improved UI Readability** +#### 2.4. Improved UI Readability - The main AI model dropdown menu is now logically grouped by model type (Upscaling, Denoising, Face Restoration, Interpolation), with a `MENU_LIST_SEPARATOR` between categories. This makes it easier for users to find and select the appropriate AI model for their task. ### 3. Performance and Code Optimisation -#### 4.0 **Memory Optimisation with Contiguous Arrays** +#### 3.1. Memory Optimisation with Contiguous Arrays - Widespread use of `numpy.ascontiguousarray` has been implemented across the codebase. This is applied during critical image handling steps in `AI_upscale.preprocess_image`, `AI_interpolation.concatenate_images`, and the new `AI_face_restoration.preprocess_face_image` class. This ensures data is aligned in memory, which can significantly speed up operations in backend libraries like OpenCV and ONNX Runtime. -#### 3.2 **Refined Data Type Handling** +#### 3.2. Refined Data Type Handling - The `AI_upscale` class now explicitly ensures input images are converted to `float32` before normalization, improving precision and preventing potential data type mismatches during inference. - The `AI_face_restoration` class is configured to intelligently select between `float16` and `float32` based on the specific model's requirements (`fp16: True` in config), further optimizing performance and VRAM usage for compatible models. ### 4. Codebase Health and Maintainability -#### 4.1 **Specialised Class for Face Restoration** +#### 4.1. Specialised Class for Face Restoration - The logic for face restoration has been fully encapsulated within the new `AI_face_restoration` class, separating it from the general-purpose `AI_upscale` class. This object-oriented approach makes the code more modular, readable, and easier to extend with different face enhancement models in the future. -#### 4.2 **Robust BGRA to BGR Conversion** +#### 4.2. Robust BGRA to BGR Conversion - The application now explicitly handles images with an alpha channel (4-channel BGRA) when using face restoration models. A new import for `COLOR_BGRA2BGR` was added, and it is used within `preprocess_face_image` to convert images to the 3-channel BGR format expected by the GFPGAN model. This prevents runtime errors and ensures correct processing of PNGs or other images with transparency. --- -## Version 2.2 +## Version 2.2 -**Release date:** 7 July 2025 +**Release date:** 7 July 2025 ### 1. Major Enhancements and Stability Overhaul -1.1 **Comprehensive Logging System** +#### 1.1. Comprehensive Logging System - Implemented a full-featured `logging` system that writes to files in the user's `Documents` folder (`warlock_studio.log` and `error_log.txt`). This provides detailed diagnostics for debugging without relying solely on console output. - A unified `log_and_report_error` function centralizes error handling, ensuring all critical issues are both logged and displayed to the user. - 1.2 **Proactive Environment Validation** +#### 1.2. Proactive Environment Validation - The application now performs pre-flight checks before processing begins to prevent common failures. - Includes validation for Python version, required modules (`validate_environment`), FFmpeg availability, disk space, and available RAM (`validate_system_requirements`). - Verifies that all input file paths exist and are accessible (`validate_file_paths`) and that the output directory is writable (`validate_output_path`). - 1.3 **Resilient Video Encoding Pipeline** +#### 1.3. Resilient Video Encoding Pipeline - The entire `video_encoding` function was overhauled for maximum reliability. - **Codec Fallback:** The system now tests for hardware codec availability (NVENC, AMF, QSV) before encoding. If a selected hardware encoder is not functional, it automatically falls back to the highly compatible `libx264` software encoder. - **Robust Audio Handling:** Implements a fallback chain for audio processing. It first attempts to directly copy the audio stream; if that fails, it attempts to re-encode it; if that also fails, it finalizes the video without audio, ensuring a video file is always produced. - 1.4 **Graceful Shutdown and Cleanup** +#### 1.4. Graceful Shutdown and Cleanup - Implemented `atexit` and `signal` handlers to ensure that temporary files are cleaned up and child processes are terminated safely, even on unexpected exits. - Replaced abrupt `process.kill()` calls with the more graceful `process.terminate()` to allow for cleaner process shutdown. ### 2. Performance and Memory Optimization -2.1 **Aggressive Memory Management** +#### 2.1. Aggressive Memory Management - Video frame processing (`upscale_video_frames_async`) no longer holds large numbers of frames in RAM. It now writes small batches to disk and immediately calls the garbage collector (`gc.collect()`) to free memory, dramatically reducing the risk of crashes on long videos. - 2.2 **Dynamic GPU VRAM Error Recovery** +#### 2.2. Dynamic GPU VRAM Error Recovery - The AI orchestration logic can now recover from GPU "out of memory" errors during tiling. If an error is detected, it automatically reduces the tile resolution and retries the operation on that specific frame, preventing a total process failure. ### 3. Critical Bug Fixes -4.0 **Resolved Video Encoding Race Condition** +#### 3.1. Resolved Video Encoding Race Condition - Fixed a critical bug where video encoding could start before all frame-writing threads were complete. The system now tracks all writer threads and explicitly waits for them to finish (`thread.join()`) before beginning the final video encoding, preventing corrupted or incomplete videos. - 3.2 **Corrected Persistent Stop Flag** +#### 3.2. Corrected Persistent Stop Flag - The `stop_thread_flag` is now reset (`.clear()`) at the start of each "Make Magic" execution, fixing a bug where a previously stopped job would prevent a new one from running. - 3.3 **Eliminated Status Update Race Condition** +#### 3.3. Eliminated Status Update Race Condition - Implemented a `threading.Lock` (`global_status_lock`) to protect shared flags that update the GUI. This prevents race conditions where multiple threads could attempt to modify the status simultaneously. ### 4. UI / UX Refinements -4.1 **Updated Splash Screen** +#### 4.1. Updated Splash Screen - Reduced splash screen duration to 10 seconds for a faster application start-up. - Corrected asset path to `Assets/banner.png` for proper display. - 4.2 **New Application Theme** +#### 4.2. New Application Theme | Element | New Value | Old Value (v2.1) | | :---------------- | :--------------- | :------------------- | @@ -485,40 +600,40 @@ The color scheme has been updated as follows: ### 5. Codebase Health and Maintainability -5.1 **Enhanced Checkpointing and Recovery** +#### 5.1. Enhanced Checkpointing and Recovery - Added functions (`create_checkpoint`, `load_checkpoint`) to save and resume the progress of video frame processing, allowing recovery from interruptions. - 5.2 **Hardened Core Methods** +#### 5.2. Hardened Core Methods - Core methods in AI classes now include checks for `None` inputs and feature default fallbacks (`case _:`) in `match` statements to prevent unexpected errors with unsupported data. --- -## Version 2.1 +## Version 2.1 -**Release date:** 23 June 2025 +**Release date:** 23 June 2025 ### 1. Major Enhancements and Stability Overhaul -1.1 **Robust Error Handling** +#### 1.1. Robust Error Handling - Model loading (`AI_upscale`, `AI_interpolation`) wrapped in `try…except FileNotFoundError, OSError`; meaningful error messages propagate to GUI. -- `extract_video_frames()` validates file existence, `cv2.VideoCapture.isOpened()`, and frame count > 0. +- `extract_video_frames()` validates file existence, `cv2.VideoCapture.isOpened()`, and frame count > 0. - `video_encoding()` captures `subprocess.CalledProcessError`, logs `stderr`, and continues with fallback strategies. - Audio passthrough failures now trigger a silent audio‑less encode instead of total job abort. - 1.2 **Safe Thread and Process Management** +#### 1.2. Safe Thread and Process Management - Deprecated error‑raising thread stop replaced with `threading.Event` (`stop_thread_flag`) polled at defined checkpoints. - 1.3 **Resilient Core Processing** +#### 1.3. Resilient Core Processing - `copy_file_metadata()` now verifies `exiftool.exe` availability and the existence of source/target before execution. ### 2. UI / UX Refinements -2.1 **Refined Colour Palette** +#### 2.1. Refined Colour Palette | Element | New Value | | ----------------- | ------------------------------- | @@ -528,94 +643,110 @@ The color scheme has been updated as follows: ### 3. Code‑base Maintainability -4.0 **Improved Code Organisation** +#### 3.1. Improved Code Organisation - File‑extension lists extracted to `filetypes.py` as `SUPPORTED_IMAGE_EXTENSIONS`, `SUPPORTED_VIDEO_EXTENSIONS`. - 3.2 **Dependency and Initialisation** +#### 3.2. Dependency and Initialisation - Added imports: `shutil.move`, `subprocess.CalledProcessError`, `threading.Event`. - Global variables initialised in `init_globals()` for deterministic start‑up. --- -## Version 2.0 +## Version 2.0 -**Release date:** 6 June 2025 +**Release date:** 6 June 2025 ### 1. Major Features -1.1 **AI Frame Interpolation Support** (`AI_interpolation` class) +#### 1.1. AI Frame Interpolation Support (`AI_interpolation` class) - Supports RIFE‑based ONNX models; generates 1 (×2), 3 (×4), or 7 (×8) intermediate frames. - Provides both real‑time preview and batch processing modes. - Integrates with `FrameScheduler` for temporal upscaling pipelines. - 1.2 **RIFE Models Integration** +#### 1.2. RIFE Models Integration - Added **RIFE** and **RIFE_Lite** to model repository. - `RIFE_models_list` enumerates available checkpoints; `AI_models_list` now merges SRVGGNetCompact, BSRGAN, IRCNN, and RIFE families. ### 2. Enhancements -2.1 **Visual/UI Redesign** +#### 2.1. Visual/UI Redesign - Application renamed to **“Warlock‑Studio”** (with hyphen). - New dark palette (`#121212`, `#454242`) with bright white text (`#FFFFFF`) and accent red (`#FF0E0E`). - 2.2 **Version‑Specific User Preferences** +#### 2.2. Version‑Specific User Preferences - User configuration stored as `Warlock-Studio_._UserPreference.json` to avoid backward‑compatibility clashes. - 2.3 **Modular and Scalable Layout System** +#### 2.3. Modular and Scalable Layout System - Added GUI constants defined in `layout_constants.py` (e.g., `OFFSET_Y_OPTIONS`, `COLUMN_1_5`). - 2.4 **Extended File‑Type Compatibility** +#### 2.4. Extended File‑Type Compatibility - Updated `SUPPORTED_FILE_EXTENSIONS` and `SUPPORTED_VIDEO_EXTENSIONS` to include modern codecs (e.g., HEIC, AVIF, WebM). - 2.5 **Improved GPU Execution Support** +#### 2.5. Improved GPU Execution Support -- `provider_options` enumerates up to four DirectML devices (Auto, GPU 1 – GPU 4); selection persists across sessions. +- `provider_options` enumerates up to four DirectML devices (Auto, GPU 1 – GPU 4); selection persists across sessions. ### 3. Technical Refinements -4.0 **Model List Structure** +#### 3.1. Model List Structure - Menu drop‑downs now grouped by category separated by `MENU_LIST_SEPARATOR` for readability. - 3.2 **Advanced Interpolation Logic** +#### 3.2. Advanced Interpolation Logic - Implements tree‑based frame generation (e.g., D→A‑B‑C) with dependency tracking to avoid redundant inference passes. - 3.3 **Improved Numeric Precision and Post‑Processing** +#### 3.3. Improved Numeric Precision and Post‑Processing - Normalisation uses 32‑bit floats with epsilon guarding; RGBA conversion paths optimised using `numexpr`. ### 4. UI / UX Refinements -4.1 **Resizable Message Dialogs** (`MessageBox`; Tk `resizable(True, True)`). -4.2 **Improved Dialog Formatting** – uniform spacing, font hierarchy, and default‑value display. +#### 4.1. Resizable Message Dialogs + +- `MessageBox`; Tk `resizable(True, True)`. + +#### 4.2. Improved Dialog Formatting + +- Uniform spacing, font hierarchy, and default‑value display. ### 5. Minor Fixes +#### 5.1. Code Corrections + - Corrected typo `ttext_color` → `text_color`. - Expanded inline comments and reorganised sections for clarity. --- -## Version 1.1 +## Version 1.1 -**Release date:** 20 May 2025 +**Release date:** 20 May 2025 ### 1. Major Improvements -1.1 **Program Start‑up Optimisation** – launch time reduced via lazy module loading. -1.2 **Model Loading Improvements** – parallel prefetch and checksum verification. -1.3 **General Performance Optimisation** – core refactor, improved I/O scheduling, and smarter resource allocation. +#### 1.1. Program Start‑up Optimisation + +- Launch time reduced via lazy module loading. + +#### 1.2. Model Loading Improvements + +- Parallel prefetch and checksum verification. + +#### 1.3. General Performance Optimisation + +- Core refactor, improved I/O scheduling, and smarter resource allocation. ### 2. Minor Fixes -- User‑interface tweaks for better accessibility (focus indicators, tab order). +#### 2.1. Accessibility +- User‑interface tweaks for better accessibility (focus indicators, tab order). diff --git a/Manual/Warlock-Studio_Manual.pdf b/Manual/Warlock-Studio_Manual.pdf deleted file mode 100644 index d9ace1c..0000000 Binary files a/Manual/Warlock-Studio_Manual.pdf and /dev/null differ diff --git a/Manual/Warlock-Studio_Manual.tex b/Manual/Warlock-Studio_Manual.tex index 06a6eb6..09c509b 100644 --- a/Manual/Warlock-Studio_Manual.tex +++ b/Manual/Warlock-Studio_Manual.tex @@ -56,7 +56,7 @@ % --- CORPORATE COLOR PALETTE --- \definecolor{WarlockRed}{HTML}{C11919} \definecolor{WarlockGold}{HTML}{ECD125} -\definecolor{WarlockDark}{HTML}{1A1C1E} +\definecolor{WarlockDark}{HTML}{1B1818} % Updated to match v5.0 Theme \definecolor{WarlockGray}{HTML}{333333} \definecolor{WarlockLightGray}{HTML}{F0F0F0} \definecolor{WarlockWhite}{HTML}{FFFFFF} @@ -79,6 +79,9 @@ \definecolor{WarnBorder}{HTML}{FFBE0B} \definecolor{QuickStartFill}{HTML}{E6FFFA} \definecolor{QuickStartBorder}{HTML}{00796B} +% New Palette for NEO Engine +\definecolor{NeoFill}{HTML}{F3E5F5} +\definecolor{NeoBorder}{HTML}{8E44AD} % --- DEFAULT TEXT COLOR APPLICATION --- \color{WarlockGray} @@ -93,11 +96,11 @@ linkcolor=WarlockRed, filecolor=WarlockRed, urlcolor=WarlockRed, - pdftitle={Warlock-Studio | Technical Documentation and User Manual}, + pdftitle={Warlock-Studio v5.0 | Technical Documentation and User Manual}, pdfauthor={Iván Eduardo Chavez Ayub} } -% --- SECTION & SUBSECTION TITLES (CORRECTED) --- +% --- SECTION & SUBSECTION TITLES --- \newcommand{\SectionColor}{WarlockGray} % Default color \newcommand{\setsectioncolor}[1]{\renewcommand{\SectionColor}{#1}} @@ -162,15 +165,26 @@ shadow={2mm}{-1mm}{0mm}{black!20!white}, #1 } +% New box for NEO Engine / Hardware features +\newtcolorbox{neobox}[2][]{ + enhanced, breakable, + colback=NeoFill, colframe=NeoBorder, + fonttitle=\bfseries, coltitle=NeoBorder!80!black, + title=\faMicrochip\hspace{0.5em}#2, + attach boxed title to top left={yshift=-2mm, xshift=3mm}, + boxed title style={colback=NeoBorder, sharp corners}, + coltext=WarlockDark, + shadow={2mm}{-1mm}{0mm}{black!20!white}, + #1 +} % --- INLINE CODE COMMAND --- -% Use detokenize to preserve special characters and avoid problematic hyphenation \newcommand{\inlinecode}[1]{\colorbox{WarlockLightGray}{\small\texttt{\detokenize{#1}}}} % --- HEADER & FOOTER --- \pagestyle{fancy} \fancyhf{} -\fancyhead[L]{\textit{Warlock-Studio v4.3}} +\fancyhead[L]{\textit{Warlock-Studio v5.0}} \fancyhead[R]{\leftmark} \fancyfoot[L]{\includegraphics[height=0.8cm]{logo.png}} \fancyfoot[C]{\thepage} @@ -184,7 +198,7 @@ % ======================================================================================= \begin{document} -% --- REDESIGNED TITLE PAGE (KEEPING ORIGINAL STYLE FROM YOUR BASE CODE) --- +% --- REDESIGNED TITLE PAGE --- \begin{titlepage} \begin{tcolorbox}[ enhanced, sharp corners, @@ -205,7 +219,7 @@ \color{WarlockWhite} {\Large\bfseries Technical Documentation and User Guide\par} \vspace{0.2cm} - {\large Software Version: 4.3\par} + {\large Software Version: 5.0 (NEO-Refactor)\par} \vspace{0.4cm} \color{WarlockGold} \rule{0.6\textwidth}{1pt}\par @@ -226,9 +240,12 @@ % SECTION 1: INTRODUCTION % ======================================================================================= \setsectioncolor{IntroColor} -\section{Introduction to Warlock-Studio} -Welcome to Warlock-Studio — an AI-powered suite for digital media enhancement. -It provides advanced tools for super-resolution, artifact removal, and frame generation through an intuitive interface that delivers professional-quality results with minimal effort. +\section{Introduction to Warlock-Studio v5.0} +Welcome to Warlock-Studio v5.0 — a major evolutionary leap in AI-powered media enhancement. +This version introduces a robust \textbf{Modular Architecture} and the new \textbf{NEO Engine}, optimizing +stability, hardware diagnostics, and processing efficiency. It provides advanced tools for super-resolution, +artifact removal, and frame generation through an intuitive interface that now includes an \textbf{Integrated Console} +and native \textbf{Drag \& Drop} support. % ======================================================================================= % SECTION 2: QUICK START GUIDE @@ -236,18 +253,19 @@ It provides advanced tools for super-resolution, artifact removal, and frame gen \setsectioncolor{QuickStartColor} \section{Quick Start Guide} \begin{quickstartbox}{Accelerated Media Enhancement Procedure} -Follow these 5 steps to process your first media file in less than a minute. +Follow these steps to process your media using the new v5.0 workflow. \begin{enumerate} - \item \textbf{Load Files:} Click the \textbf{"SELECT FILES"} button and choose one or more image or video files for processing. + \item \textbf{Load Files (Drag \& Drop):} You can now simply \textbf{drag and drop} your image or video files directly onto the application window. Alternatively, click the \textbf{"Select Files"} button. \item \textbf{AI Model Selection:} In the \textbf{"AI model"} dropdown menu, select an inference model. \begin{itemize} - \item For photorealistic images, \inlinecode{BSRGANx4} is recommended for its ability to reconstruct fine textures. - \item For animation or illustrated content, \inlinecode{RealESR_Animex4} is the optimal choice for preserving sharp edges. - \item For video sequences, \inlinecode{RealESR_Gx4} offers an excellent balance between computational performance and perceptual quality. + \item For photorealistic images, \inlinecode{BSRGANx4} is recommended for texture reconstruction. + \item For animation/cartoons, \inlinecode{RealESR_Animex4} preserves sharp edges. + \item For video, \inlinecode{RealESR_Gx4} balances speed and quality. + \item For increasing framerate, use \inlinecode{RIFE} models (note: Blending controls will hide automatically). \end{itemize} - \item \textbf{Adjust Input Resolution:} For a quick first pass, set \textbf{"Input resolution"} to \texttt{75}\%. This significantly reduces the computational load with an often imperceptible loss in final quality. - \item \textbf{Verify VRAM Limit:} Ensure the value in \textbf{"GPU VRAM (GB)"} is equal to or less than your graphics card's dedicated video memory. An initial value of \texttt{4} GB is a safe setting for most modern hardware. - \item \textbf{Start Processing:} Click \textbf{"Make Magic"} to initiate the processing pipeline. The output files will be generated in the specified directory or, by default, in the same location as the source files. + \item \textbf{Verify Hardware (NEO Engine):} Click the \textbf{Gear Icon} (\faCog) to open Preferences. Check the \textbf{Hardware Diagnostics} to see the "Recommended Tiles" and "Safe VRAM Limit" calculated specifically for your PC. + \item \textbf{Adjust Settings:} Set the \textbf{"GPU VRAM (GB)"} based on the recommendation. For a quick test, set \textbf{"Input resolution"} to \texttt{75}\%. + \item \textbf{Start Processing:} Click \textbf{"Make Magic"}. You can now monitor real-time progress and logs via the new \textbf{Integrated Console} at the bottom of the window. \end{enumerate} \end{quickstartbox} @@ -255,14 +273,22 @@ Follow these 5 steps to process your first media file in less than a minute. % SECTION 3: INSTALLATION & ARCHITECTURE % ======================================================================================= \setsectioncolor{InstallColor} -\section{Installation and System Architecture} -\subsection{\faDownload\ Installation Process} -Warlock-Studio uses a self-contained offline installer, simplifying deployment. +\section{Installation and Modular Architecture} +\subsection{\faDownload\ Installation Process \& Path Change} +Warlock-Studio uses a self-contained offline installer. + +\begin{warnbox}{Critical: Installation Directory Change} +In version 5.0, the default installation directory has been migrated from \texttt{Program Files} to: +\begin{center} +\inlinecode{\%userprofile\%\\Documents\\Warlock-Studio} +\end{center} +\textbf{Reason:} This change prevents "Permission Denied" errors on Windows systems with strict UAC. It ensures the application has full read/write access to generate the \inlinecode{warlock_config.json}, write real-time logs, and manage video checkpoints without requiring constant Administrator privileges. +\end{warnbox} + \begin{enumerate}[leftmargin=*] - \item \textbf{Obtaining the Executable:} Download the `Warlock-Studio-Setup.exe` file from the official repositories on GitHub or SourceForge. - \item \textbf{Run with Elevated Privileges:} Right-click the installer and select "Run as administrator." This step is crucial to ensure the application has the necessary permissions to interact with low-level GPU drivers. - \item \textbf{Installation Wizard:} Follow the on-screen instructions. The inclusion of all AI models and dependencies eliminates the need for additional downloads during this process. - \item \textbf{Launch the Application:} Once the installation is complete, Warlock-Studio can be launched from the Start Menu or the desktop shortcut. + \item \textbf{Obtaining the Executable:} Download the `Warlock-Studio-Setup.exe` (Full Installer) from the official repositories. + \item \textbf{Run the Installer:} Run the setup. It will automatically default to your Documents folder. + \item \textbf{Launch:} Open Warlock-Studio via the Desktop shortcut. \end{enumerate} \subsection{\faMicrochip\ System Requirements} @@ -273,39 +299,33 @@ Warlock-Studio uses a self-contained offline installer, simplifying deployment. \textbf{Component} & \textbf{Technical Specification} \\ \midrule Operating System & Windows 11 or Windows 10 (64-bit architecture required). \\ - RAM & 8 GB (minimum), 16 GB (recommended for high-resolution video processing). \\ - Graphics Card (GPU) & \textbf{Mandatory Requirement:} GPU with support for the \textbf{DirectX 12} API. \\ - & \textbf{NVIDIA (for CUDA):} Maxwell architecture (GTX 900 Series) or newer. Studio drivers are recommended for stability. \\ - & \textbf{AMD/Intel (for DirectML):} Any modern GPU with updated drivers that support DirectX 12 Feature Level 12.0+. \\ - & \textbf{4+ GB of VRAM} is recommended to avoid memory bottlenecks. \\ - Storage & 2 GB of free disk space. Using a Solid State Drive (SSD) drastically improves I/O performance during video sequence processing. \\ + RAM & 8 GB (minimum), 16 GB (recommended). \\ + Graphics Card (GPU) & \textbf{Mandatory Requirement:} GPU with \textbf{DirectX 12} support. \\ + & \textbf{NVIDIA:} CUDA support (Maxwell or newer). \\ + & \textbf{AMD/Intel:} DirectML support. \\ + & \textbf{4+ GB of VRAM} is recommended. \\ + Storage & 2 GB of free disk space. SSD strongly recommended for video I/O. \\ \bottomrule \end{tabularx} - \caption{Hardware and software specifications for optimal performance of Warlock-Studio.} + \caption{Requirements for v5.0. The NEO Engine will verify these upon launch.} \end{table} -\subsection{\faFolderOpen\ File Structure and Dependencies} -\begin{infobox}{Self-Contained Components} -Warlock-Studio operates as a fully self-contained environment. -All critical assets, dependencies, and user data are automatically managed by the application, requiring no manual configuration from the user. +\subsection{\faPuzzlePiece\ Modular File Architecture (v5.0)} +\begin{infobox}{From Monolithic to Modular} +Version 5.0 abandons the single-script structure. The application is now composed of specialized modules to improve stability and maintainability. \end{infobox} \begin{itemize}[leftmargin=*] - \item \textbf{Core Assets:} - The executables \inlinecode{ffmpeg.exe} (for video encoding/decoding) and \inlinecode{exiftool.exe} (for metadata preservation) are stored in the \texttt{Assets} directory. - Their paths are dynamically resolved through the \inlinecode{find_by_relative_path} function, which accurately locates resources in both development and bundled environments (using the \inlinecode{_MEIPASS} directory created by PyInstaller). - - \item \textbf{AI Models:} - All AI inference models are provided in interoperable \texttt{.onnx} format and stored within the \texttt{AI-onnx} directory. - This ensures cross-platform compatibility and seamless hardware acceleration through ONNX Runtime. - - \item \textbf{User Preferences:} - The configuration file \inlinecode{USER_PREFERENCE_PATH} is automatically generated in the user’s \textbf{Documents} folder (\inlinecode{os_path_expanduser('~')}). - It stores GUI layout, last-used settings, and model selections between sessions. - - \item \textbf{Diagnostic Logs:} - Log files (\inlinecode{MAIN_LOG_FILENAME} and \inlinecode{ERROR_LOG_FILENAME}) are created in a dedicated subfolder inside \textbf{Documents}. - These serve as vital diagnostic resources, recording operational events, system validation reports, and runtime exceptions for troubleshooting. + \item \textbf{\inlinecode{Warlock-Studio.py} (Core Orchestrator):} + Manages the main GUI event loop and spawns multiprocessing tasks for AI inference. + \item \textbf{\inlinecode{warlock_preferences.py} (State Manager):} + Houses the \textbf{NEO Engine} for hardware telemetry, the \inlinecode{ConfigManager} for JSON persistence, and the OTA Update Manager. + \item \textbf{\inlinecode{console.py} (I/O Manager):} + Controls the new \textbf{Integrated Console}, redirecting \texttt{stdout} and \texttt{stderr} streams to the GUI for real-time debugging. + \item \textbf{\inlinecode{drag_drop.py} (Event Wrapper):} + Implements the \inlinecode{DnDCTk} class to handle native OS Drag \& Drop events. + \item \textbf{Assets:} + Includes \inlinecode{ffmpeg.exe}, \inlinecode{exiftool.exe}, and the AI Models (`.onnx`) in the \texttt{AI-onnx} directory. \end{itemize} @@ -315,300 +335,178 @@ All critical assets, dependencies, and user data are automatically managed by th \setsectioncolor{ModelsColor} \section{Detailed Analysis of Inference Models} -\begin{infobox}{Model Selection Overview} -Selecting the appropriate AI inference model is the most critical decision -in Warlock-Studio’s enhancement pipeline. It directly determines the balance -between visual quality, computational efficiency, and hardware resource usage. -Each model implements distinct neural architectures and loss functions, optimized -for different types of input data and output fidelity targets. - -The following section provides a comprehensive technical breakdown and comparative -analysis to support informed model selection. +\begin{infobox}{Dynamic Interface Adaptation} +In v5.0, the interface adapts to your model selection. Selecting a \textbf{RIFE} model will automatically hide "Blending" controls and reveal "Frame Generation" options. Selecting an \textbf{Upscaling} model does the reverse. \end{infobox} - \subsection{\faTable\ Model Comparison Matrix} \begin{longtable}{p{2.8cm} p{1.8cm} p{1.2cm} p{1.5cm} p{7.2cm}} \toprule -\textbf{Model} & \textbf{Primary Function} & \textbf{Scale} & \textbf{VRAM (GB)} & \textbf{Use Case and Technical Considerations} \\ +\textbf{Model} & \textbf{Function} & \textbf{Scale} & \textbf{VRAM} & \textbf{Use Case} \\ \midrule \endhead -\multicolumn{5}{c}{\textit{\textbf{\faEraser\ Denoising Models}}} \\ +\multicolumn{5}{c}{\textit{\textbf{\faEraser\ Denoising}}} \\ \midrule -\texttt{IRCNN\_Mx1} & Denoise & x1 & 4.0 & Moderate-level noise reduction. Ideal for suppressing JPEG compression artifacts and low-ISO sensor noise in old photographs. \\ -\texttt{IRCNN\_Lx1} & Denoise & x1 & 4.0 & Intensive noise reduction algorithm. Optimal for severely degraded images with pronounced luminance and chrominance noise. \\ +\texttt{IRCNN\_Mx1} & Denoise & x1 & 4.0 & Moderate noise reduction (JPEG artifacts). \\ +\texttt{IRCNN\_Lx1} & Denoise & x1 & 4.0 & Intensive noise reduction for degraded images. \\ \midrule -\multicolumn{5}{c}{\textit{\textbf{\faTachometerAlt\ High-Fidelity Upscaling Models (Computationally Intensive)}}} \\ +\multicolumn{5}{c}{\textit{\textbf{\faTachometerAlt\ High-Fidelity Upscaling}}} \\ \midrule -\texttt{BSRGANx4} & Upscale & x4 & 0.6 & Generative Adversarial Network optimized for realistic texture synthesis. It is the model of choice for portraits, nature photography, and where the preservation of fine details is paramount. \\ -\texttt{BSRGANx2} & Upscale & x2 & 0.7 & A variant with a reduced scaling factor. It offers a perceptual quality similar to the x4 version but with lower computational cost, ideal for moderate upscaling needs. \\ -\texttt{RealESRGANx4} & Upscale & x4 & 0.6 & A robust general-purpose model. It excels in reconstructing a wide variety of content, including textures, landscapes, and architectural elements. \\ -\texttt{RealESRNetx4} & Upscale & x4 & 2.2 & An alternative to RealESRGAN, often producing results with fewer "hallucinatory" artifacts. It can offer a better balance between speed and fidelity on certain GPU architectures. \\ +\texttt{BSRGANx4} & Upscale & x4 & 0.6 & Photorealistic texture synthesis. Best for portraits/nature. \\ +\texttt{RealESRGANx4} & Upscale & x4 & 0.6 & General-purpose robust reconstruction. \\ \midrule -\multicolumn{5}{c}{\textit{\textbf{\faBolt\ High-Speed Upscaling Models (Lightweight)}}} \\ +\multicolumn{5}{c}{\textit{\textbf{\faBolt\ High-Speed Upscaling}}} \\ \midrule -\texttt{RealESR\_Gx4} & Upscale & x4 & 2.2 & A lightweight and fast model, optimized for real-time or near-real-time video processing. It offers an excellent compromise between performance and visual quality. \\ -\texttt{RealESR\_Animex4} & Upscale & x4 & 2.2 & Specialized for non-photorealistic content. It preserves sharp lines and flat colors, avoiding the smoothing artifacts common in models trained on natural data. \\ +\texttt{RealESR\_Gx4} & Upscale & x4 & 2.2 & Fastest model. Optimized for video. \\ +\texttt{RealESR\_Animex4} & Upscale & x4 & 2.2 & Optimized for Anime/Cartoons (clean lines). \\ \midrule -\multicolumn{5}{c}{\textit{\textbf{\faUserCircle\ Facial Restoration Models}}} \\ +\multicolumn{5}{c}{\textit{\textbf{\faUserCircle\ Facial Restoration}}} \\ \midrule -\texttt{GFPGAN} & Restore & x1 & 1.8 & Generative Adversarial Network with a facial prior. It not only upscales but also reconstructs and enhances damaged or low-resolution facial features in photographs. \\ +\texttt{GFPGAN} & Restore & x1 & 1.8 & Face reconstruction. v5.0 enforces \textbf{Float32} precision for stability. \\ \midrule -\multicolumn{5}{c}{\textit{\textbf{\faFilm\ Frame Interpolation Models (Video Only)}}} \\ +\multicolumn{5}{c}{\textit{\textbf{\faFilm\ Frame Interpolation (FluidFrames)}}} \\ \midrule -\texttt{RIFE} & Interpolate & N/A & \textasciitilde{}1.5 & High-quality algorithm for motion interpolation. It generates intermediate frames to increase the fluidity of a video (e.g., from 30 to 60 FPS). \\ -\texttt{RIFE\_Lite} & Interpolate & N/A & \textasciitilde{}1.2 & An optimized variant of RIFE, designed for GPUs with limited VRAM. It offers faster processing at the cost of a slight reduction in interpolation accuracy. \\ +\texttt{RIFE} & Interpolate & N/A & \textasciitilde{}1.5 & Generates intermediate frames (x2, x4, x8). \\ +\texttt{RIFE\_Lite} & Interpolate & N/A & \textasciitilde{}1.2 & Faster variant for lower-end GPUs. \\ \midrule \bottomrule -\caption{Technical guide for the selection of AI models. VRAM values are base estimates derived from the \inlinecode{VRAM_model_usage} dictionary in the source code and may vary.} +\caption{Technical guide for AI models. VRAM values are base estimates.} \label{tab:modelos} \end{longtable} % ======================================================================================= -% SECTION 5: BEST PRACTICES & OPTIMIZATION +% SECTION 5: OPTIMIZATION & NEO ENGINE % ======================================================================================= \setsectioncolor{OptimizeColor} -\section{Performance Optimization and Best Practices} +\section{Performance Optimization and NEO Engine} + +\subsection{\faMagic\ The NEO Engine} +\begin{neobox}{Automatic Hardware Heuristics} +Warlock-Studio v5.0 introduces the \textbf{NEO Engine} (located in \inlinecode{warlock_preferences.py}). This system scans your CPU, RAM, and GPU capabilities in real-time to generate \textbf{Smart Recommendations}. +\end{neobox} -\subsection{\faSlidersH\ Critical Performance Parameters} \begin{itemize}[leftmargin=*, itemsep=2pt] - \item \textbf{Input Resolution \%:} This is the most influential parameter on processing speed. A value between \textbf{50\% and 75\%} implements an initial downsampling before upscaling, drastically reducing the computational load with minimal impact on the final perceptual quality. - -\item \textbf{GPU VRAM Limiter (GB):} Defines the video memory budget. -This value (from \inlinecode{selected_VRAM_limiter.get()}) is used in a precise formula -to calculate the internal \inlinecode{tiles_resolution} parameter: - -\begin{center} -\begin{tcolorbox}[ - enhanced, - colback=WarlockLightGray!30, - colframe=OptimizeColor!80!black, - boxrule=0.6pt, - arc=2pt, - left=4pt,right=4pt,top=2pt,bottom=2pt, - width=0.9\linewidth -] -\small -\texttt{vram\_factor = VRAM\_model\_usage[model] * VRAM\_GB\_input}\\ -\texttt{tiles\_resolution = int(vram\_factor * 100)} -\end{tcolorbox} -\end{center} - -This \inlinecode{tiles_resolution} (e.g., \texttt{880px} for a 4~GB card using -\inlinecode{RealESR_Gx4}) is then used as the maximum tile dimension for image processing, -ensuring full stability and preventing \textbf{Out Of Memory (OOM)} errors during inference. - - - \item \textbf{AI Multithreading:} (Video only) Allows for the parallel processing of multiple frames via a \inlinecode{ThreadPool}. It significantly increases performance on systems with multi-core CPUs, but at the cost of higher VRAM and CPU consumption. - - \item \textbf{AI Blending:} Mitigates visual artifacts. This value (from \inlinecode{selected_blending_factor}) is passed to the \inlinecode{blend_images_and_save} function, which uses OpenCV's \inlinecode{addWeighted} to perform an alpha blend between the original upscaled image and the AI-processed image. - - \item \textbf{System Validation:} Before starting, the \inlinecode{validate_system_requirements} function automatically checks for the presence of \inlinecode{ffmpeg.exe}, available disk space, and available system RAM (using the \inlinecode{psutil} library). + \item \textbf{Safe VRAM Limit:} The engine calculates a safe buffer using the formula: $\max(0.5, \text{Physical VRAM} - 1.5 \text{ GB})$. + \item \textbf{Recommended Tiles:} It suggests the optimal \inlinecode{tiles_resolution} to maximize speed while preventing Out-Of-Memory (OOM) crashes. + \item \textbf{Thread Concurrency:} It analyzes your CPU topology (physical vs. logical cores) to suggest safe multithreading levels for video processing. \end{itemize} -\subsection{\faTrophy\ Tips for Maximum Quality Results} - -\begin{tcolorbox}[ - enhanced, breakable, - colback=QuickStartFill, - colframe=QuickStartBorder, - coltitle=QuickStartBorder!80!black, - title=\faStar\hspace{0.5em}\textbf{Expert Recommendations for Optimal Output}, - fonttitle=\bfseries, - sharp corners, - boxrule=0.6pt, - shadow={1mm}{-1mm}{0mm}{black!10!white} -] - -\begin{description}[leftmargin=*, style=nextline, itemsep=1em] - - \item[\textbf{Maximizing Visual Fidelity}] - To achieve the highest possible perceptual quality, use the \inlinecode{BSRGANx4} or \inlinecode{RealESRGANx4} models with an \textbf{Input Resolution} of \textbf{100\%}. - Although computationally demanding, this configuration ensures maximum reconstruction of micro-textures and reduces information loss, especially on portrait or landscape imagery. - - \item[\textbf{Workflow for Video Restoration}] - A robust two-stage restoration pipeline for archival or degraded footage involves: - \begin{enumerate}[nosep, leftmargin=*] - \item \textbf{Stage 1 — Denoising:} Apply a denoising model such as \inlinecode{IRCNN\_Mx1} to clean sensor noise and compression artifacts. - \item \textbf{Stage 2 — Upscaling:} Use an upscaling model such as \inlinecode{RealESR\_Gx4} on the cleaned output to reconstruct fine detail while maintaining temporal coherence. - \end{enumerate} - - \item[\textbf{Impact of SSD Storage}] - A Solid State Drive (SSD) significantly improves throughput during video restoration. - The extraction (\inlinecode{extract\_video\_frames}), writing (\inlinecode{save\_frames\_on\_disk}), and reassembly (\inlinecode{video\_encoding}) of thousands of frames represent the main I/O bottlenecks; SSDs mitigate these delays and improve system responsiveness. - - \item[\textbf{Frame Persistence for Experimentation}] - For testing different encoding settings (e.g., codecs or bitrates), enable the \textbf{"Keep frames"} option (\inlinecode{selected\_keep\_frames = True}). - This preserves the processed frames on disk, allowing video re-encoding without repeating the AI inference phase — saving time and extending the experimental workflow. - -\end{description} -\end{tcolorbox} +\subsection{\faSlidersH\ Critical Parameters} +\begin{itemize}[leftmargin=*, itemsep=2pt] + \item \textbf{Input Resolution \%:} Setting this to \textbf{75\%} drastically reduces load with minimal quality loss. + \item \textbf{AI Multithreading:} (Video only) Processes multiple frames in parallel. Use the NEO Engine's recommendation to avoid system freezing. + \item \textbf{Keep Frames:} Enable this (\inlinecode{selected_keep_frames = True}) if you plan to experiment with different video encoding codecs later. +\end{itemize} % ======================================================================================= % SECTION 6: TROUBLESHOOTING % ======================================================================================= \setsectioncolor{TroubleColor} \section{Diagnostics and Troubleshooting} -\begin{warnbox}{Primary Cause of Errors} -The \textbf{\#1 cause} of processing failures is \textbf{non-standard characters} in file paths and filenames. Avoid using: \texttt{', ", @, \#, \$, \%, \&, *, [, ], ?, etc.}. +\begin{warnbox}{Integrated Console} +Use the new **Integrated Console** at the bottom of the app window to view real-time error logs, warnings, and processing status. You can search, copy, and save these logs. \end{warnbox} \begin{description}[leftmargin=*, style=nextline, itemsep=0.8em] - \item[\faBan\ Error: "FFmpeg encoding failed..."] - \textbf{Diagnosis:} The \inlinecode{video_encoding} function, which uses \inlinecode{subprocess_run}, has failed. The code specifically checks for several causes: - \begin{itemize}[nosep, leftmargin=*] - \item \textbf{"Invalid argument"}: Most commonly caused by special characters in file paths. - \item \textbf{"Unknown encoder"}: The selected video codec (e.g., \inlinecode{hevc_nvenc}) is not supported by your FFmpeg build or hardware. - \item \textbf{"Device or resource busy"}: Your GPU's hardware encoder is being used by another application (e.g., OBS, ShadowPlay). - \end{itemize} - \textbf{Solution:} Rename files to remove special characters. For encoder errors, select a different codec (e.g., \inlinecode{x264} or \inlinecode{x265}). + \item[\faBan\ Error: "FFmpeg encoding failed..." / Fallback Active] + \textbf{Diagnosis:} The selected hardware codec (e.g., \inlinecode{hevc_nvenc}) failed due to driver issues or resource locking. + \textbf{v5.0 Solution:} The system now features an \textbf{Automatic Fallback}. If the GPU encoder fails, it automatically switches to the CPU-based \inlinecode{libx264} encoder to ensure the video is finished. Check the console for yellow warnings indicating this switch. - \item[\faRocket\ Error: "Failed to load model" or Execution Provider Failure] - \textbf{Diagnosis:} Failure in the initialization of the selected hardware backend, caught within the \inlinecode{create_onnx_session} function. - \textbf{Solution:} The system is designed to automatically fall back to a functional provider in the hierarchy (CUDA -> DirectML -> CPU). To resolve the root cause: - \begin{enumerate}[nosep, leftmargin=*] - \item \textbf{For CUDA errors:} Verify the installation of the latest NVIDIA drivers (Game Ready or Studio). - \item \textbf{For DirectML errors:} Ensure that the Windows operating system is fully updated and that you have the latest drivers for your GPU (NVIDIA, AMD, or Intel). - \end{enumerate} + \item[\faMemory\ Error: "Out of memory" / OOM Recovery] + \textbf{Diagnosis:} VRAM exhaustion during tiling. + \textbf{v5.0 Solution:} The application detects this exception and triggers \textbf{Recursive Dynamic Tiling}. It automatically halves the tile resolution (e.g., 100\% $\to$ 50\%) and retries the frame. You do not need to restart the process manually. - \item[\faMemory\ Error: "out of memory" (OOM) or "allocation" failure] - \textbf{Diagnosis:} The GPU's VRAM has been exhausted. This error is caught within the \inlinecode{upscale_video_frames_async} function. - \textbf{Solution:} - \begin{enumerate}[nosep, leftmargin=*] - \item Lower the \textbf{VRAM Limiter} to a value equal to or less than your GPU's physical VRAM. - \item Decrease the \textbf{Input Resolution \%} to 75\% or less. - \item \textbf{Automatic Recovery:} The code will attempt an automatic recovery. It progressively reduces the processing tile size (by lowering the \inlinecode{AI_instance.max_resolution} variable) and retries the failed frame. - \end{enumerate} + \item[\faRocket\ Error: "Failed to load model" (ONNX)] + \textbf{Diagnosis:} Issue initializing the execution provider. + \textbf{Solution:} v5.0 implements a strict priority chain: CUDA $\to$ DirectML $\to$ CPU. Ensure your GPU drivers are up to date. If using an older NVIDIA card, the system may default to DirectML or CPU. - \item[\faTachometerAlt\ Error: "cannot convert float NaN to integer"] - \textbf{Diagnosis:} This specific error is caught in the main \inlinecode{upscale_orchestrator}. It indicates a GPU driver timeout (TDR - Timeout Detection and Recovery), often caused by hardware overload or overheating, which returns a "Not a Number" (NaN) value instead of pixel data. - \textbf{Solution:} Restart the process \textbf{without deleting the generated frames folder}. The application will detect the existing frames and resume the task from the point of failure. + \item[\faTachometerAlt\ Error: "NaN" (Not a Number)] + \textbf{Diagnosis:} GPU Driver Timeout (TDR). + \textbf{Solution:} Restart the process without deleting the temp frames folder. The app will resume from the last successful frame. \end{description} % ======================================================================================= % SECTION 7: ADVANCED TECHNICAL ARCHITECTURE % ======================================================================================= \setsectioncolor{ArchColor} -\section{Software Architecture Analysis} +\section{Software Architecture Analysis (v5.0)} -\subsection{\faCogs\ Inference Engine and Hardware Abstraction} -Warlock-Studio is built upon a multi-layered inference engine powered by \textbf{ONNX Runtime}. The \inlinecode{create_onnx_session} function abstracts the underlying hardware and prioritizes Execution Providers to maximize performance: -\begin{enumerate}[leftmargin=*] - \item \textbf{CUDA (\texttt{CUDAExecutionProvider}):} The highest performance option, leveraging the parallel computing architecture of NVIDIA GPUs. - \item \textbf{DirectML (\texttt{DmlExecutionProvider}):} If CUDA is not available, the system falls back to DirectML. This Microsoft API translates neural network operations into native \textbf{DirectX 12} calls, ensuring broad hardware compatibility (NVIDIA, AMD, Intel). - \item \textbf{CPU (\texttt{CPUExecutionProvider}):} This is the last resort provider. If no GPU acceleration is viable, the application runs the model on the CPU, guaranteeing universal functionality at the cost of significantly lower performance. -\end{enumerate} +\subsection{\faCogs\ Modular Inference Engine} +Warlock-Studio v5.0 utilizes a robust \textbf{ONNX Runtime} backend managed by the \inlinecode{create_onnx_session} factory in the core orchestrator. It enforces strict integer typing for device IDs to ensure compatibility with rigid DirectML backends. -\subsection{\faThLarge\ Dynamic Tiling and Memory Management} -To process high-resolution media, the \inlinecode{AI_upscale_with_tilling} function is invoked. It programmatically subdivides a large frame into smaller "tiles" based on the \inlinecode{max_resolution} variable. This \inlinecode{max_resolution} (confusingly named \inlinecode{tiles_resolution} in the GUI logic) is the pixel dimension calculated directly from the user's \textbf{VRAM Limiter} input, ensuring that no single tile exceeds the GPU's memory budget. +\subsection{\faSyncAlt\ Lossless Intermediate Pipeline} +In v5.0, the video extraction pipeline (\inlinecode{extract_video_frames}) strictly enforces the use of \textbf{.PNG} containers for temporary frames. This eliminates the generation loss previously caused by JPEG artifacts before the image entered the neural network. -\subsection{\faSyncAlt\ Resume and Checkpointing Functionality} -If a video process is interrupted, the frames already processed and written to disk are preserved. Upon restarting the same task, the \inlinecode{check_video_upscaling_resume} function detects these partial files by checking for their existence. It then returns a list of only the *original* frames that still need processing, allowing the \inlinecode{upscale_video} orchestrator to resume the task from the point of failure. - -\subsection{\faBolt\ Asynchronous Frame Writing} -During video upscaling, the main processing loop in \inlinecode{upscale_video_frames_async} does not write to disk directly. Instead, it batches processed frames in memory. Once a batch is ready (defaulting to \inlinecode{MULTIPLE_FRAMES_TO_SAVE = 8}), it calls \inlinecode{save_frames_on_disk}, which spawns a new \inlinecode{Thread} from the `threading` module to run \inlinecode{save_multiple_upscaled_frame_async}. This decouples the GPU-bound inference from the I/O-bound disk writing, maximizing GPU utilization. - -% ======================================================================================= -% SECTION 8: PROCESSING PIPELINE DIAGRAM (FIXED TO PAGE WIDTH) -% ======================================================================================= -\section{Processing Pipeline (Diagram)} +\subsection{\faThLarge\ Architecture Diagram (Modular)} \noindent -The following diagram illustrates the high-level processing pipeline. - -\begin{figure}[H] - \centering - % Resize to text width to avoid overflow - \resizebox{\textwidth}{!}{% - \begin{tikzpicture}[node distance=12mm, every node/.style={font=\small}, >=Stealth] - \tikzset{ - box/.style={rectangle, rounded corners=4pt, draw=WarlockDark!80!black, fill=WarlockDark!6, minimum width=48mm, minimum height=9mm, align=center, drop shadow}, - proc/.style={rectangle, rounded corners=3pt, draw=WarlockGold!70!black, fill=WarlockGold!12, minimum width=48mm, minimum height=9mm, align=center}, - disk/.style={cylinder, shape border rotate=90, draw=WarlockGray!70!black, fill=WarlockLightGray, minimum width=10mm, minimum height=10mm, align=center}, - arrow/.style={->, thick, draw=WarlockDark!80!black} - } - \node[box] (ingest) {\textbf{Input Ingest}\\(images \& video)}; - \node[proc, right=28mm of ingest] (analysis) {\textbf{Preprocessing}\\format, resize, color space}; - \node[proc, right=28mm of analysis] (model) {\textbf{AI Inference}\\selected model(s)}; - \node[box, right=28mm of model] (post) {\textbf{Postprocess}\\blend, alpha, denoise}; - \node[disk, below=8mm of model] (cache) {\textbf{Frame Cache}}; - \node[proc, right=28mm of post] (encode) {\textbf{Encoding}\\ffmpeg reassembly}; - - \draw[arrow] (ingest) -- (analysis); - \draw[arrow] (analysis) -- (model); - \draw[arrow] (model) -- (post); - \draw[arrow] (post) -- (encode); - \draw[arrow] (model) -- (cache); - \draw[arrow] (cache) -- (post); - - % Decorative bracket and label under pipeline - \draw[decorate, decoration={brace, amplitude=6pt}, thick, WarlockGold] ($(analysis.south west)+(0,-6mm)$) -- ($(post.south east)+(0,-6mm)$) node[midway, below=9mm]{\small \textbf{GPU-Accelerated AI Pipeline}}; - \end{tikzpicture} - } - \caption{High-level processing pipeline (ingest $\rightarrow$ inference $\rightarrow$ encode).} -\end{figure} - -% ======================================================================================= -% SECTION 10: ARCHITECTURE DIAGRAM (FIXED) -% ======================================================================================= -\section{Software Architecture (Diagram)} -\noindent -Component-level architecture showing logical components and their relationship to hardware. +Updated component-level architecture illustrating the new modular design and the interaction between the GUI, the NEO Engine, and the Core Orchestrator. \begin{figure}[H] \centering \resizebox{\textwidth}{!}{% \begin{tikzpicture}[node distance=12mm, every node/.style={font=\small}] \tikzset{ - comp/.style={rectangle, rounded corners=3pt, draw=WarlockDark!70!black, fill=WarlockDark!6, minimum width=42mm, minimum height=9mm, align=center, drop shadow}, - hw/.style={rectangle, rounded corners=3pt, draw=WarlockGold!70!black, fill=WarlockGold!10, minimum width=42mm, minimum height=9mm, align=center}, - line/.style={-Latex, thick, color=WarlockGray!80!black} + module/.style={rectangle, rounded corners=3pt, draw=WarlockDark!70!black, fill=WarlockDark!6, minimum width=42mm, minimum height=10mm, align=center, drop shadow}, + core/.style={rectangle, rounded corners=3pt, draw=WarlockGold!80!black, fill=WarlockGold!15, minimum width=42mm, minimum height=10mm, align=center, drop shadow}, + hw/.style={rectangle, rounded corners=3pt, draw=WarlockGray!80!black, fill=WarlockLightGray, minimum width=42mm, minimum height=10mm, align=center}, + line/.style={-Latex, thick, color=WarlockGray!90!black} } - \node[comp] (gui) {GUI / Preferences}; - \node[comp, right=28mm of gui] (orchestrator) {Orchestrator / Job Queue}; - \node[comp, right=28mm of orchestrator] (ai) {AI Core / ONNX Wrapper}; - \node[comp, right=28mm of ai] (io) {I/O \& Encoding (FFmpeg)}; - \node[hw, below=12mm of ai] (gpu) {GPU (CUDA / DirectML)}; - \node[hw, below=12mm of io] (disk) {Disk / SSD}; - \draw[line] (gui) -- (orchestrator); - \draw[line] (orchestrator) -- (ai); - \draw[line] (ai) -- (io); - \draw[line, dashed] (ai) -- (gpu) node[midway, right]{\small Inference}; + % Nodes + \node[module] (gui) {\textbf{GUI Frontend}\\(Main Window +\\Drag\&Drop Wrapper)}; + \node[module, left=15mm of gui] (prefs) {\textbf{Warlock Prefs}\\(NEO Engine +\\Update Manager)}; + \node[module, right=15mm of gui] (console) {\textbf{Console Manager}\\(Stream Redirection)}; + + \node[core, below=15mm of gui] (orch) {\textbf{Core Orchestrator}\\(Multiprocessing)}; + + \node[module, below=15mm of orch] (ai) {\textbf{AI Engine}\\(ONNX Runtime)}; + \node[module, right=15mm of ai] (io) {\textbf{I/O \& Encoding}\\(FFmpeg / Fallback)}; + + \node[hw, below=12mm of ai] (gpu) {\textbf{Hardware}\\(CUDA / DirectML)}; + \node[hw, below=12mm of io] (disk) {\textbf{Storage}\\(Logs/Config/Frames)}; + + % Connections + \draw[line] (prefs) -- (gui); + \draw[line] (console) -- (gui); + \draw[line] (gui) -- (orch); + \draw[line] (orch) -- (ai); + \draw[line] (orch) -- (io); + \draw[line] (ai) -- (gpu); \draw[line] (io) -- (disk); - \draw[line, dashed] (orchestrator) -- (disk) node[midway, right]{\small Checkpoints / Frames}; + \draw[line, dashed] (prefs) |- (disk); + \end{tikzpicture} } - \caption{Component-level architecture (logical components and hardware).} + \caption{Warlock-Studio v5.0 Modular Component Architecture.} \end{figure} - % ======================================================================================= -% SECTION 9: TECHNICAL GLOSSARY +% SECTION 8: GLOSSARY % ======================================================================================= \setsectioncolor{GlossaryColor} \section{Glossary} \begin{description}[leftmargin=*, style=nextline, itemsep=0.8em] - \item[ONNX Runtime] (Open Neural Network Exchange) A high-performance, cross-platform inference engine for AI models. It allows Warlock-Studio to run models in a hardware-agnostic manner. - \item[CUDA] (Compute Unified Device Architecture) A parallel computing platform and programming model API created by NVIDIA. It enables general-purpose acceleration on NVIDIA GPUs. - \item[DirectML] (Direct Machine Learning) A low-level Microsoft API that uses DirectX 12 to provide GPU-accelerated AI on a wide range of DX12-compatible hardware. - \item[VRAM] (Video RAM) High-speed random-access memory dedicated to a graphics card, used to store textures, framebuffers, and other data critical for rendering and computation on the GPU. - \item[Tiling] A technique of dividing a large image into smaller "tiles" to be processed individually. It is an essential mechanism for handling resolutions that exceed available VRAM. - \item[Codec] (Coder-Decoder) A software or hardware algorithm/device that compresses and decompresses digital video data. Examples of software codecs are x264 (H.264) and x265 (HEVC). Hardware codecs (NVENC, AMF, QSV) use dedicated encoders on the GPU to accelerate this process. + \item[NEO Engine] The new heuristic subsystem in v5.0 responsible for hardware scanning, diagnostics, and configuration recommendation. + \item[Modular Architecture] A software design technique that splits the code into separate, independent modules (`console`, `preferences`, `core`) to improve maintainability. + \item[ONNX Runtime] The cross-platform engine used to run the AI models. v5.0 enforces strict device ID typing. + \item[OOM Recovery] (Out Of Memory) An automatic mechanism that reduces tile size when VRAM is exhausted to prevent crashes. + \item[DirectML] (Direct Machine Learning) API used for GPU acceleration on AMD and Intel cards. \end{description} % ======================================================================================= -% SECTION 11: SUPPORT & CONTRIBUTIONS +% SECTION 9: SUPPORT & CONTRIBUTIONS % ======================================================================================= \setsectioncolor{SupportColor} \section{Support and Community} \begin{itemize}[leftmargin=*] - \item \textbf{\faBug\ Reporting Issues:} If you encounter a reproducible bug, please open an "Issue" on the GitHub repository. It is essential to attach the \inlinecode{error_log.txt} file (located in your \textbf{Documents} folder) for effective diagnosis. - \item \textbf{\faCodeBranch\ Code Contributions:} Contributions to the source code are welcome. It is recommended to follow the standard workflow: fork the repository, create a new branch for the feature or fix, and submit a "Pull Request" for review. - \item \textbf{\faEnvelope\ Direct Contact:} For general inquiries or technical support that does not constitute a software bug, you can contact the author at \href{mailto:negroayub97@gmail.com}{\texttt{negroayub97@gmail.com}}. + \item \textbf{\faBook\ Manual:} Click the \textbf{Book Icon} in the app header to open this PDF document. + \item \textbf{\faBug\ Reporting Issues:} Report bugs on GitHub. Please attach the \inlinecode{error_log.txt} file located in your \textbf{Documents} folder. + \item \textbf{\faSync\ Updates:} Use the internal \textbf{Update Manager} (Gear Icon $\to$ Check Updates) to download the latest version directly. + \item \textbf{\faEnvelope\ Contact:} For non-bug related inquiries: \href{mailto:negroayub97@gmail.com}{\texttt{negroayub97@gmail.com}}. \end{itemize} \vspace{1cm} \centering -\textbf{Thank you for using Warlock-Studio.} +\textbf{Thank you for using Warlock-Studio v5.0.} % ======================================================================================= % END OF DOCUMENT diff --git a/README.md b/README.md index 0881d7b..d79cb15 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![Warlock-Studio banner](Assets/banner.png)
- + [![Version](https://img.shields.io/badge/Version-4.3-FF4500?style=for-the-badge&logo=git&logoColor=white)](https://github.com/Ivan-Ayub97/Warlock-Studio/releases/tag/4.3) [![License](https://img.shields.io/badge/License-MIT-6A0DAD?style=for-the-badge&logo=open-source-initiative&logoColor=white)](LICENSE) [![Last Commit](https://img.shields.io/github/last-commit/Ivan-Ayub97/Warlock-Studio?style=for-the-badge&color=2E8B57&logo=git&logoColor=white)](https://github.com/Ivan-Ayub97/Warlock-Studio/commits/main) @@ -10,36 +10,27 @@ [![Python](https://img.shields.io/badge/Python-3.10+-3776AB?style=for-the-badge&logo=python&logoColor=FFD700)](https://www.python.org/) [![Downloads](https://img.shields.io/github/downloads/Ivan-Ayub97/Warlock-Studio/total.svg?style=for-the-badge&color=FFD700&logo=download&logoColor=black)](https://github.com/Ivan-Ayub97/Warlock-Studio/releases) - -Inspired by [Djdefrag](https://github.com/Djdefrag) tools such as **QualityScaler** and **FluidFrames**, Warlock-Studio provides a unified, high-performance platform for **upscaling, restoration, denoising, and frame interpolation**. +Inspired by [Djdefrag](https://github.com/Djdefrag) tools such as **QualityScaler** and **FluidFrames**, **Warlock-Studio** provides a unified, high-performance platform for **upscaling, restoration, denoising, and frame interpolation**. --- -`Fixed missing audio in generated videos.` -`Corrected dark output issue in GFPGAN face restoration.` -`Improved overall stability and synchronization.` - -See CHANGELOG.md for full details. - ---- ## 📥 Download Installer
You can download the installer from either option below:
-
Warlock-Studio on SourceForge + width="190" style="display:block; margin:auto; margin-bottom:1px;" /> - + Download from GitHub @@ -47,12 +38,11 @@ See CHANGELOG.md for full details.
- --- ## 🖼️ Interface Previews -![Main interface](rsc/Capture.png) +![interface](rsc/Capture.png) --- @@ -80,84 +70,11 @@ See CHANGELOG.md for full details. ## 🖥️ System Requirements -- **OS:** Windows 11 or higher (64-bit) -- **RAM:** 8GB+ recommended -- **GPU:** NVIDIA (for CUDA), AMD, or Intel GPU with up-to-date drivers recommended -- **Storage:** Sufficient free space for input and processed media - ---- - -## 📊 Model Comparison - -| Model File | Use Case | Speed | Quality | Notes | -| :----------------- | :--------------------------- | :-------- | :-------- | :---------------------------------- | -| **GFPGANv1.4** | Face restoration | High | High | Optimal for portraits | -| **BSRGANx2** | 2× upscale + denoising | Medium | Very High | Suitable for lightly degraded media | -| **BSRGANx4** | 4× upscale + denoising | Low | Very High | For heavily degraded content | -| **RIFE** | Frame interpolation | High | High | Smooth motion, slow-motion support | -| **RIFE-Lite** | Lightweight interpolation | Very High | Medium | Faster, lower resource usage | -| **RealESRGANx4** | General 4× upscaling | Medium | High | Balanced performance | -| **RealESRNetx4** | Subtle restoration | Medium | High | Preserves natural image texture | -| **RealSRx4_Anime** | Anime / line-art enhancement | Medium | High | Sharp edges for 2D art | -| **IRCNN_L** | Light denoising | High | Medium | Mild artifact removal | -| **IRCNN_M** | Medium denoising | High | Medium | Stronger artifact cleanup | - ---- - -## 📂 Project Structure - -
- -``` -Warlock-Studio/ -├── AI-onnx/ # Pre-trained ONNX models for AI processing -│ ├── BSRGANx2_fp16.onnx -│ ├── BSRGANx4_fp16.onnx -│ ├── GFPGANv1.4.fp16.onnx -│ ├── IRCNN_Lx1_fp16.onnx -│ ├── IRCNN_Mx1_fp16.onnx -│ ├── RealESR_Animex4_fp16.onnx -│ ├── RealESR_Gx4_fp16.onnx -│ ├── RealESRGANx4_fp16.onnx -│ ├── RealESRNetx4_fp16.onnx -│ ├── RealSRx4_Anime_fp16.onnx -│ ├── RIFE_fp32.onnx -│ └── RIFE_Lite_fp32.onnx -│ -├── Assets/ # Application assets and third-party binaries -│ ├── banner.png -│ ├── clear_icon.png -│ ├── exiftool.exe -│ ├── ffmpeg.exe -│ ├── ffmprobe.exe -│ ├── ffmplay.exe -│ ├── info_icon.png -│ ├── logo.ico -│ ├── logo.png -│ ├── stop_icon.png -│ ├── upscale_icon.png -│ ├── wizard-image.bmp -│ └── wizard-small.bmp -│ -├── rsc/ # UI previews and branding resources -│ ├── Capture.png -│ ├── image_comparison.png -│ ├── CaptureCONSOLE.png -│ └── GitHub_Logo_WS.png -│ -├── Warlock-Studio.py # Main application script -├── Warlock-Studio.spec # PyInstaller specification file -├── Setup.iss # Inno Setup installer script -├── README.md # Project overview -├── CHANGELOG.md # Version history and updates -├── LICENSE # MIT License information -├── NOTICE.md # Legal notices and attributions -├── CODE_OF_CONDUCT.md # Contributor guidelines -├── CONTRIBUTING.md # Contribution guide -└── SECURITY.md # Security reporting policies -``` - -
+- **OS:** Windows 11 or Windows 10 (64-bit) +- **RAM:** 8GB (minimum) / 16GB+ (recommended for 4K Video) +- **GPU:** DirectX 12 compatible graphics card. NVIDIA (for CUDA), AMD, or Intel GPU with up-to-date drivers. +- **VRAM:** 4GB+ recommended. The **NEO Engine** automatically tunes limits on startup. +- **Storage:** Sufficient free space for input and processed media. SSD highly recommended for video I/O. --- @@ -176,50 +93,24 @@ We welcome contributions from the community: ## 📜 License © 2025 Iván Eduardo Chavez Ayub -Licensed under **MIT**. Additional terms and attributions are provided in `NOTICE.md`. +Licensed under **MIT**. Additional terms and attributions are provided in **NOTICE.md**. ### 📊 Integrated Technologies & Licenses -| Technology | License | Author / Maintainer | Source | -| ------------- | --------------------- | --------------------------------------- | ---------------------------------------------------------- | -| QualityScaler | MIT | [Djdefrag](https://github.com/Djdefrag) | [GitHub](https://github.com/Djdefrag/QualityScaler) | -| FluidFrames | MIT | [Djdefrag](https://github.com/Djdefrag) | [GitHub](https://github.com/Djdefrag/FluidFrames) | -| Real-ESRGAN | BSD 3-Clause / Apache | Xintao Wang | [GitHub](https://github.com/xinntao/Real-ESRGAN) | -| GFPGAN | Apache 2.0 | TencentARC / Xintao Wang | [GitHub](https://github.com/TencentARC/GFPGAN) | -| RIFE | Apache 2.0 | hzwer | [GitHub](https://github.com/megvii-research/ECCV2022-RIFE) | -| BSRGAN | Apache 2.0 | Kai Zhang | [GitHub](https://github.com/cszn/BSRGAN) | -| IRCNN | BSD / Mixed | Kai Zhang | [GitHub](https://github.com/cszn/IRCNN) | -| ONNX Runtime | MIT | Microsoft | [GitHub](https://github.com/microsoft/onnxruntime) | -| FFmpeg | LGPL / GPL | FFmpeg Team | [Official Site](https://ffmpeg.org) | -| ExifTool | Artistic License | Phil Harvey | [Official Site](https://exiftool.org/) | -| 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) | +| Technology    | License                | Author / Maintainer                      | Source                                                      | +| -------------- | ---------------------- | ---------------------------------------- | ----------------------------------------------------------- | +| QualityScaler | MIT                    | [Djdefrag](https://github.com/Djdefrag) | [GitHub](https://github.com/Djdefrag/QualityScaler)        | +| FluidFrames    | MIT                    | [Djdefrag](https://github.com/Djdefrag) | [GitHub](https://github.com/Djdefrag/FluidFrames)          | +| Real-ESRGAN    | BSD 3-Clause / Apache | Xintao Wang                              | [GitHub](https://github.com/xinntao/Real-ESRGAN)            | +| GFPGAN        | Apache 2.0            | TencentARC / Xintao Wang                | [GitHub](https://github.com/TencentARC/GFPGAN)              | +| RIFE          | Apache 2.0            | hzwer                                    | [GitHub](https://github.com/megvii-research/ECCV2022-RIFE) | +| BSRGAN        | Apache 2.0            | Kai Zhang                                | [GitHub](https://github.com/cszn/BSRGAN)                    | +| IRCNN          | BSD / Mixed            | Kai Zhang                                | [GitHub](https://github.com/cszn/IRCNN)                    | +| ONNX Runtime  | MIT                    | Microsoft                                | [GitHub](https://github.com/microsoft/onnxruntime)          | +| FFmpeg        | LGPL / GPL            | FFmpeg Team                              | [Official Site](https://ffmpeg.org)                        | +| ExifTool      | Artistic License      | Phil Harvey                              | [Official Site](https://exiftool.org/)                      | +| 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 99d6eeb..a20cd72 100644 --- a/Setup.iss +++ b/Setup.iss @@ -1,10 +1,6 @@ -; =================================================================== -; Warlock-Studio 4.3 - Inno Setup Script (Full Offline Installer) -; =================================================================== - #define AppName "Warlock-Studio" -#define AppVersion "4.3" -#define AppPublisher "Iván Eduardo Chavez Ayub" +#define AppVersion "5.0" +#define AppPublisher "Ivan-Ayub97|Ivanayub1997" #define AppURL "https://github.com/Ivan-Ayub97/Warlock-Studio" #define AppExeName "Warlock-Studio.exe" @@ -15,11 +11,10 @@ AppVersion={#AppVersion} AppPublisher={#AppPublisher} AppSupportURL={#AppURL} AppUpdatesURL={#AppURL} -DefaultDirName={autopf}\{#AppName} +DefaultDirName={userdocs}\{#AppName} DefaultGroupName={#AppName} AllowNoIcons=yes -PrivilegesRequired=admin -ArchitecturesInstallIn64BitMode=x64 +PrivilegesRequired=none ; --- Configuración del Instalador --- OutputDir=Output diff --git a/Warlock-Studio.py b/Warlock-Studio.py index 7800443..0fe7121 100644 --- a/Warlock-Studio.py +++ b/Warlock-Studio.py @@ -8,6 +8,7 @@ import signal import subprocess import sys import traceback +import warnings from contextlib import contextmanager from datetime import datetime from functools import cache @@ -17,7 +18,7 @@ from json import load as json_load from math import cos, pi from multiprocessing import Process from multiprocessing import Queue as multiprocessing_Queue -from multiprocessing import freeze_support as multiprocessing_freeze_support +from multiprocessing import freeze_support from multiprocessing.pool import ThreadPool from os import cpu_count as os_cpu_count from os import devnull as os_devnull @@ -47,7 +48,7 @@ from webbrowser import open as open_browser # ONNX Runtime imports import onnxruntime -# GUI imports +# GUI imports (CustomTkinter & TkinterDnD) from customtkinter import (CTk, CTkButton, CTkEntry, CTkFont, CTkFrame, CTkImage, CTkLabel, CTkOptionMenu, CTkProgressBar, CTkScrollableFrame, CTkToplevel, filedialog, @@ -64,6 +65,7 @@ from cv2 import imdecode as opencv_imdecode from cv2 import imencode as opencv_imencode from cv2 import imread as image_read from cv2 import resize as opencv_resize +# MoviePy imports from moviepy.video.io import ImageSequenceClip # Third-party library imports from natsort import natsorted @@ -89,58 +91,90 @@ from onnxruntime import InferenceSession from PIL import Image from PIL.Image import fromarray as pillow_image_fromarray from PIL.Image import open as pillow_image_open +from tkinterdnd2 import DND_ALL, TkinterDnD -# Define supported file extensions +# ----------------------------------------------------------------------------- +# CONFIGURATION & CONSTANTS +# ---------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# INITIALIZE CONSOLE REDIRECTION EARLY +# ----------------------------------------------------------------------------- +from console import IntegratedConsole, console +# Local imports +from drag_drop import DnDCTk, enable_drag_and_drop +from warlock_preferences import PreferencesButton # Importación local + +# Redirigir inmediatamente para capturar logs de importación +console.setup_redirection() + +# Suppress specific warnings to keep console clean +warnings.filterwarnings("ignore", category=UserWarning) + +# Handle PyInstaller or Normal Execution Path + + +def find_by_relative_path(relative_path: str) -> str: + """ + Resuelve rutas absolutas para recursos, funcionando tanto en desarrollo + como cuando el script está empaquetado con PyInstaller (--onefile). + """ + base_path = getattr(sys, '_MEIPASS', os_path_dirname( + os_path_abspath(__file__))) + return os_path_join(base_path, relative_path) + + +# Application Info +app_name = "Warlock-Studio" +version = "5.0" + +# Supported File Extensions supported_image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".webp"] supported_video_extensions = [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm"] supported_file_extensions = supported_image_extensions + supported_video_extensions -if sys.stdout is None: - sys.stdout = open(os_devnull, "w") -if sys.stderr is None: - sys.stderr = open(os_devnull, "w") +# ----------------------------------------------------------------------------- +# THEME & COLORS +# ----------------------------------------------------------------------------- +# Fondo: Negro casi puro, igual que el fondo del banner para máximo contraste +background_color = "#1B1818" +# Nombre de la app: Plata metálico, inspirado en el texto "STUDIO" +app_name_color = "#FF4848" +# Paneles: Gris oscuro neutro, permite que el rojo y dorado resalten sin competir +widget_background_color = "#2A2727" +# Texto principal: Blanco puro para legibilidad máxima +text_color = "#FFFFFF" +# Texto secundario: Dorado pálido/desaturado, para no cansar la vista pero mantener la identidad +secondary_text_color = "#CAC9C9" +# Acento: El amarillo dorado brillante del sombrero y los destellos (Sparkles) +accent_color = "#FDEF2F" +# Hover de botones: El rojo vibrante del relleno del texto "WARLOCK" +button_hover_color = "#D41C1C" +# Bordes: Un dorado oscuro muy sutil, imitando el borde del logo sin ser chillón +border_color = "#E2340D" +# Botones info/secundarios: El rojo sangre oscuro del fondo del círculo del logo +info_button_color = "#212121" +# Advertencias: Naranja dorado, sacado del sombreado del sombrero +warning_color = "#FFA000" +# Éxito: Verde brillante, necesario para contraste funcional +success_color = "#00E676" +# Error: Rojo carmesí intenso, similar al borde de las letras "WARLOCK" +error_color = "#B00020" +# Resaltado: Amarillo luz, como el centro de los destellos (estrellas) +highlight_color = "#FFFF8D" +# Scrollbars: Rojo vino oscuro translúcido, para mantener la temática sin distraer +scrollbar_color = "#420505" -def find_by_relative_path(relative_path: str) -> str: - base_path = getattr(sys, '_MEIPASS', os_path_dirname( - os_path_abspath(__file__))) - return os_path_join(base_path, relative_path) - - -app_name = "Warlock-Studio" -version = "4.3" - -# 🌑 Crimson Gold Dark Theme - -background_color = "#121212" # Negro profundo con leve calidez -app_name_color = "#FFFFFF" # Blanco puro, nítido sobre fondo oscuro -# Rojo vino muy oscuro (para paneles y marcos) -widget_background_color = "#4B0000" -text_color = "#FFFFFF" # Blanco principal para texto -# Gris claro suave, para subtítulos o texto menos importante -secondary_text_color = "#C8C8C8" -# Dorado puro (detalle de lujo y contraste) -accent_color = "#FFD700" -button_hover_color = "#FF4444" # Rojo claro brillante, resalta sin saturar -border_color = "#2A2A2A" # Gris oscuro para contornos discretos -# Rojo sangre sobrio (botones secundarios) -info_button_color = "#8B0000" -warning_color = "#E6C200" # Dorado cálido para alertas y avisos -# Verde neón tenue (no rompe la estética) -success_color = "#3FE55B" -error_color = "#B00020" # Rojo carmesí oscuro para errores -# Dorado-anaranjado suave (resalta elementos activos) -highlight_color = "#FFB84C" -# Rojo oscuro translúcido para barras y scrolls -scrollbar_color = "#660000" - +# ----------------------------------------------------------------------------- +# AI MODEL LISTS & CONFIGURATION +# ----------------------------------------------------------------------------- VRAM_model_usage = { 'RealESR_Gx4': 2.2, 'RealESR_Animex4': 2.2, - 'RealESRNetx4': 2.2, + 'RealESRNetx4': 2.2, 'BSRGANx4': 0.6, 'BSRGANx2': 0.7, 'RealESRGANx4': 0.6, @@ -159,6 +193,7 @@ RIFE_models_list = ["RIFE", "RIFE_Lite"] AI_models_list = (SRVGGNetCompact_models_list + MENU_LIST_SEPARATOR + BSRGAN_models_list + MENU_LIST_SEPARATOR + IRCNN_models_list + MENU_LIST_SEPARATOR + Face_restoration_models_list + MENU_LIST_SEPARATOR + RIFE_models_list) + frame_interpolation_models_list = RIFE_models_list frame_generation_options_list = [ "x2", "x4", "x8", "Slowmotion x2", "Slowmotion x4", "Slowmotion x8" @@ -176,7 +211,10 @@ video_codec_list = [ "h264_amf", "hevc_amf", MENU_LIST_SEPARATOR[0], "h264_qsv", "hevc_qsv", ] -# -- FluidFrames: Integrate conditional interpolation option -- + +# ----------------------------------------------------------------------------- +# PATHS & USER PREFERENCES +# ----------------------------------------------------------------------------- OUTPUT_PATH_CODED = "Same path as input files" DOCUMENT_PATH = os_path_join(os_path_expanduser('~'), 'Documents') @@ -192,54 +230,65 @@ COMPLETED_STATUS = "Completed" ERROR_STATUS = "Error" STOP_STATUS = "Stop" +# Check External Tools if os_path_exists(FFMPEG_EXE_PATH): print(f"[{app_name}] ffmpeg.exe found") else: - print(f"[{app_name}] ffmpeg.exe not found, please install ffmpeg.exe following the guide") + print(f"[{app_name}] WARNING: ffmpeg.exe not found. Video functionality will be limited.") + +# Load User Preferences (with Error Handling) +# Default Values +default_AI_model = AI_models_list[0] +default_AI_multithreading = AI_multithreading_list[0] +default_gpu = gpus_list[0] +default_keep_frames = keep_frames_list[1] +default_image_extension = image_extension_list[0] +default_video_extension = video_extension_list[0] +default_video_codec = video_codec_list[0] +default_blending = blending_list[1] +default_output_path = OUTPUT_PATH_CODED +default_input_resize_factor = str(50) +default_output_resize_factor = str(100) +default_VRAM_limiter = str(4) if os_path_exists(USER_PREFERENCE_PATH): - print(f"[{app_name}] Preference file exist") - with open(USER_PREFERENCE_PATH, "r") as json_file: - json_data = json_load(json_file) - default_AI_model = json_data.get( - "default_AI_model", AI_models_list[0]) - default_AI_multithreading = json_data.get( - "default_AI_multithreading", AI_multithreading_list[0]) - default_gpu = json_data.get( - "default_gpu", gpus_list[0]) - default_keep_frames = json_data.get( - "default_keep_frames", keep_frames_list[1]) - default_image_extension = json_data.get( - "default_image_extension", image_extension_list[0]) - default_video_extension = json_data.get( - "default_video_extension", video_extension_list[0]) - default_video_codec = json_data.get( - "default_video_codec", video_codec_list[0]) - default_blending = json_data.get( - "default_blending", blending_list[1]) - default_output_path = json_data.get( - "default_output_path", OUTPUT_PATH_CODED) - default_input_resize_factor = json_data.get( - "default_input_resize_factor", str(50)) - default_output_resize_factor = json_data.get( - "default_output_resize_factor", str(100)) - default_VRAM_limiter = json_data.get( - "default_VRAM_limiter", str(4)) - + print(f"[{app_name}] Preference file exists") + try: + with open(USER_PREFERENCE_PATH, "r") as json_file: + json_data = json_load(json_file) + # Safe .get() calls to prevent KeyErrors if fields are missing in old config versions + default_AI_model = json_data.get( + "default_AI_model", default_AI_model) + default_AI_multithreading = json_data.get( + "default_AI_multithreading", default_AI_multithreading) + default_gpu = json_data.get("default_gpu", default_gpu) + default_keep_frames = json_data.get( + "default_keep_frames", default_keep_frames) + default_image_extension = json_data.get( + "default_image_extension", default_image_extension) + default_video_extension = json_data.get( + "default_video_extension", default_video_extension) + default_video_codec = json_data.get( + "default_video_codec", default_video_codec) + default_blending = json_data.get( + "default_blending", default_blending) + default_output_path = json_data.get( + "default_output_path", default_output_path) + default_input_resize_factor = json_data.get( + "default_input_resize_factor", default_input_resize_factor) + default_output_resize_factor = json_data.get( + "default_output_resize_factor", default_output_resize_factor) + default_VRAM_limiter = json_data.get( + "default_VRAM_limiter", default_VRAM_limiter) + except (JSONDecodeError, Exception) as e: + print(f"[{app_name}] Error reading preference file ({e}). Using defaults.") else: print(f"[{app_name}] Preference file does not exist, using default coded value") - default_AI_model = AI_models_list[0] - default_AI_multithreading = AI_multithreading_list[0] - default_gpu = gpus_list[0] - default_keep_frames = keep_frames_list[1] - default_image_extension = image_extension_list[0] - default_video_extension = video_extension_list[0] - default_video_codec = video_codec_list[0] - default_blending = blending_list[1] - default_output_path = OUTPUT_PATH_CODED - default_input_resize_factor = str(50) - default_output_resize_factor = str(100) - default_VRAM_limiter = str(4) + + +# ----------------------------------------------------------------------------- +# GUI LAYOUT CONSTANTS +# ----------------------------------------------------------------------------- offset_y_options = 0.0825 row1 = 0.125 @@ -268,62 +317,78 @@ little_textbox_width = 74 little_menu_width = 98 -# ... (después de tus imports y variables globales) +# ----------------------------------------------------------------------------- +# ONNX SESSION HELPER +# ----------------------------------------------------------------------------- def create_onnx_session(model_path: str, selected_gpu: str) -> InferenceSession: """ Creates an ONNX inference session by selecting the best available provider. - Priority: CUDA -> DmlExecutionProvider -> CPU. + Fixes: Correct type for device_id (int) and handles 'Auto' properly. """ if not os_path_exists(model_path): raise FileNotFoundError(f"AI model file not found: {model_path}") - # Map the GUI selection to the numerical device_id - device_id_map = {'GPU 1': "0", 'GPU 2': "1", 'GPU 3': "2", 'GPU 4': "3"} - # Default to 0 for 'Auto' or if not found - device_id = device_id_map.get(selected_gpu, "0") + # Map the GUI selection to the numerical device_id (INTEGER) + # 'Auto' defaults to 0, but we handle it differently in logic + device_id_map = {'GPU 1': 0, 'GPU 2': 1, 'GPU 3': 2, 'GPU 4': 3} - # List of providers in order of priority - providers_priority = [ - ('CUDAExecutionProvider', [{'device_id': device_id}]), - ('DmlExecutionProvider', [ - {'device_id': device_id, "performance_preference": "high_performance"}]), - ('CPUExecutionProvider', None) - ] + target_device_id = device_id_map.get(selected_gpu, 0) + is_auto = selected_gpu == "Auto" + + # Providers priority + providers_to_try = [] + + # 1. CUDA (NVIDIA) + cuda_options = {'device_id': target_device_id} + providers_to_try.append(('CUDAExecutionProvider', cuda_options)) + + # 2. DirectML (AMD/Intel/Windows) + # DirectML expects device_id as string in some versions, int in others. + # We use int standard here, usually works with modern ort-dml. + dml_options = {'device_id': target_device_id} + providers_to_try.append(('DmlExecutionProvider', dml_options)) + + # 3. CPU (Fallback) + providers_to_try.append(('CPUExecutionProvider', None)) available_providers = onnxruntime.get_available_providers() - session = None - for provider, options in providers_priority: + for provider, options in providers_to_try: if provider in available_providers: try: - # Ensure the options format is correct for the providers list - provider_options = options[0] if options else {} + # Si es Auto y estamos en CUDA, intentamos sin forzar device_id + # a menos que sea explícitamente necesario, pero usualmente ID 0 es seguro. + # La corrección clave aquí es pasar options como diccionario, no lista de diccionario. + + session_options = [options] if options else None + session = InferenceSession( path_or_bytes=model_path, providers=[provider], - provider_options=[provider_options] + provider_options=session_options ) print( - f"[AI] Successfully loaded model '{os_path_basename(model_path)}' using '{provider}'") + f"[AI] Loaded model with provider: {provider} (Device ID: {target_device_id})") return session except Exception as e: - print( - f"[AI WARNING] Failed to load model with {provider}: {e}") - print(f"[AI WARNING] Falling back to the next available provider...") + print(f"[AI WARNING] Failed to load {provider}: {e}") + continue - if session is None: - raise RuntimeError( - f"Failed to load AI model '{os_path_basename(model_path)}' with any available provider.") - - return session + # Final fallback attempt without options (let ONNX decide) + try: + return InferenceSession(model_path, providers=['CPUExecutionProvider']) + except Exception as e: + raise RuntimeError(f"Critical: Failed to load AI model. Error: {e}") # Enhanced Model Utilization and Error Handling class AI_upscale: - # CLASS INIT FUNCTIONS + # ------------------------------------------------------------------------- + # CLASS INIT + # ------------------------------------------------------------------------- def __init__( self, @@ -333,414 +398,528 @@ class AI_upscale: output_resize_factor: int, max_resolution: int ): - - # Passed variables + # Parámetros recibidos self.AI_model_name = AI_model_name self.directml_gpu = directml_gpu self.input_resize_factor = input_resize_factor self.output_resize_factor = output_resize_factor self.max_resolution = max_resolution - # Calculated variables + # Variables calculadas self.AI_model_path = find_by_relative_path( f"AI-onnx{os_separator}{self.AI_model_name}_fp16.onnx") self.upscale_factor = self._get_upscale_factor() + + # La sesión se carga bajo demanda o al iniciar, según la lógica del orquestador. + # Inicializamos en None para permitir una carga diferida si fuera necesario. self.inferenceSession = None def _get_upscale_factor(self) -> int: + """Determina el factor de escala basado en el nombre del modelo.""" if "x1" in self.AI_model_name: return 1 elif "x2" in self.AI_model_name: return 2 elif "x4" in self.AI_model_name: return 4 + # Valor por defecto seguro + return 1 def _load_inferenceSession(self) -> None: - """Carga la sesión de inferencia utilizando la función centralizada.""" + """Carga la sesión de inferencia utilizando la función centralizada robusta.""" + if self.inferenceSession is not None: + return + try: self.inferenceSession = create_onnx_session( self.AI_model_path, self.directml_gpu) except Exception as e: error_msg = f"Failed to load AI model {os_path_basename(self.AI_model_path)}: {str(e)}" - print(f"[AI ERROR] {error_msg}") + logging.error(f"[AI ERROR] {error_msg}") raise RuntimeError(error_msg) - def _select_providers(self): - # Este método ya no es necesario gracias a create_onnx_session - pass - - # INTERNAL CLASS FUNCTIONS + # ------------------------------------------------------------------------- + # IMAGE UTILS + # ------------------------------------------------------------------------- def get_image_mode(self, image: numpy_ndarray) -> str: + """Determina el modo de la imagen (Grayscale, RGB, RGBA).""" if image is None: raise ValueError("Image is None") - shape = image.shape - if len(shape) == 2: # Grayscale: 2D array (rows, cols) + + # Validación de dimensiones + if image.ndim == 2: 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: - raise ValueError(f"Unsupported image shape: {shape}") + elif image.ndim == 3: + channels = image.shape[2] + if channels == 3: + return "RGB" + elif channels == 4: + return "RGBA" + elif channels == 1: + return "Grayscale" + + raise ValueError(f"Unsupported image shape: {image.shape}") def get_image_resolution(self, image: numpy_ndarray) -> tuple: + """Retorna (alto, ancho).""" height = image.shape[0] width = image.shape[1] - return height, width def calculate_target_resolution(self, image: numpy_ndarray) -> tuple: + """Calcula la resolución esperada después del escalado por el modelo.""" height, width = self.get_image_resolution(image) target_height = height * self.upscale_factor target_width = width * self.upscale_factor - return target_height, target_width - def resize_with_input_factor(self, image: numpy_ndarray) -> numpy_ndarray: + def _ensure_even_dimensions(self, dim: int) -> int: + """Asegura que una dimensión sea par (necesario para algunos modelos/codecs).""" + return dim if dim % 2 == 0 else dim + 1 + def resize_with_input_factor(self, image: numpy_ndarray) -> numpy_ndarray: + """Redimensiona la imagen de entrada según el porcentaje configurado.""" 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) - 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 + # Asegurar dimensiones mínimas y pares + new_width = max(2, self._ensure_even_dimensions(new_width)) + new_height = max(2, self._ensure_even_dimensions(new_height)) - if self.input_resize_factor > 1: - return opencv_resize(image, (new_width, new_height), interpolation=INTER_CUBIC) - elif self.input_resize_factor < 1: - return opencv_resize(image, (new_width, new_height), interpolation=INTER_AREA) - else: + if self.input_resize_factor == 1.0 and (new_width == old_width and new_height == old_height): return image - def resize_with_output_factor(self, image: numpy_ndarray) -> numpy_ndarray: + interpolation = INTER_CUBIC if self.input_resize_factor > 1 else INTER_AREA + return opencv_resize(image, (new_width, new_height), interpolation=interpolation) + def resize_with_output_factor(self, image: numpy_ndarray) -> numpy_ndarray: + """Redimensiona la imagen de salida final según el porcentaje configurado.""" old_height, old_width = self.get_image_resolution(image) new_width = int(old_width * self.output_resize_factor) new_height = int(old_height * self.output_resize_factor) - 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 + # Asegurar dimensiones mínimas y pares + new_width = max(2, self._ensure_even_dimensions(new_width)) + new_height = max(2, self._ensure_even_dimensions(new_height)) - if self.output_resize_factor > 1: - return opencv_resize(image, (new_width, new_height), interpolation=INTER_CUBIC) - elif self.output_resize_factor < 1: - return opencv_resize(image, (new_width, new_height), interpolation=INTER_AREA) - else: + if self.output_resize_factor == 1.0 and (new_width == old_width and new_height == old_height): return image - # VIDEO CLASS FUNCTIONS + interpolation = INTER_CUBIC if self.output_resize_factor > 1 else INTER_AREA + return opencv_resize(image, (new_width, new_height), interpolation=interpolation) + + # ------------------------------------------------------------------------- + # VIDEO UTILS + # ------------------------------------------------------------------------- def calculate_multiframes_supported_by_gpu(self, video_frame_path: str) -> int: - resized_video_frame = self.resize_with_input_factor( - image_read(video_frame_path)) - height, width = self.get_image_resolution(resized_video_frame) - image_pixels = height * width - max_supported_pixels = self.max_resolution * self.max_resolution + """Calcula cuántos frames simultáneos caben en la VRAM basándose en max_resolution.""" + try: + # Leer solo para obtener dimensiones, no decodificar todo si es muy grande + # Sin embargo, necesitamos aplicar el resize factor para ser precisos + frame = image_read(video_frame_path) + resized_frame = self.resize_with_input_factor(frame) - frames_simultaneously = max_supported_pixels // image_pixels + height, width = self.get_image_resolution(resized_frame) + image_pixels = height * width - print( - f" Frames supported simultaneously by GPU: {frames_simultaneously}") + # max_resolution se usa como lado de un cuadrado para estimar área soportada + max_supported_pixels = self.max_resolution * self.max_resolution - return frames_simultaneously + # Evitar división por cero + if image_pixels == 0: + return 1 - # TILLING FUNCTIONS + frames_simultaneously = max_supported_pixels // image_pixels + + # Limitar a un mínimo de 1 y un máximo seguro (ej. 8 o 16) + frames_simultaneously = max(1, min(frames_simultaneously, 16)) + + print( + f"[AI] Frames supported simultaneously by GPU estimate: {frames_simultaneously}") + return int(frames_simultaneously) + + except Exception as e: + print( + f"[AI WARNING] Could not calculate multiframes support: {e}. Defaulting to 1.") + return 1 + + # ------------------------------------------------------------------------- + # TILING (MOSAICO) FUNCTIONS + # ------------------------------------------------------------------------- def image_need_tilling(self, image: numpy_ndarray) -> bool: height, width = self.get_image_resolution(image) image_pixels = height * width max_supported_pixels = self.max_resolution * self.max_resolution - - if image_pixels > max_supported_pixels: - return True - else: - return False + return image_pixels > max_supported_pixels def add_alpha_channel(self, image: numpy_ndarray) -> numpy_ndarray: - if image.shape[2] == 3: + """Añade un canal alpha opaco (255) a una imagen RGB.""" + if image.ndim == 3 and image.shape[2] == 3: alpha = numpy_full( (image.shape[0], image.shape[1], 1), 255, dtype=uint8) - image = numpy_concatenate((image, alpha), axis=2) + return numpy_concatenate((image, alpha), axis=2) return image def calculate_tiles_number(self, image: numpy_ndarray) -> tuple: - height, width = self.get_image_resolution(image) + # Cálculo seguro de tiles redondeando hacia arriba tiles_x = (width + self.max_resolution - 1) // self.max_resolution tiles_y = (height + self.max_resolution - 1) // self.max_resolution return tiles_x, tiles_y - def split_image_into_tiles(self, image: numpy_ndarray, tiles_x: int, tiles_y: int) -> list[numpy_ndarray]: - + def split_image_into_tiles(self, image: numpy_ndarray) -> list[tuple]: + """ + Divide la imagen guardando sus coordenadas originales. + Retorna: Lista de tuplas (tile_image, x, y) + """ img_height, img_width = self.get_image_resolution(image) - tile_width = img_width // tiles_x - tile_height = img_height // tiles_y - tiles = [] + # Calculamos los pasos asegurando que cubrimos toda la imagen + for y in range(0, img_height, self.max_resolution): + for x in range(0, img_width, self.max_resolution): + # Definir recorte asegurando no salirnos de la imagen + h_crop = min(self.max_resolution, img_height - y) + w_crop = min(self.max_resolution, img_width - x) - for y in range(tiles_y): - y_start = y * tile_height - y_end = (y + 1) * tile_height + tile = image[y:y+h_crop, x:x+w_crop] - for x in range(tiles_x): - x_start = x * tile_width - x_end = (x + 1) * tile_width - tile = image[y_start:y_end, x_start:x_end] - tiles.append(tile) + # Guardamos: (Imagen recortada, coordenada X original, coordenada Y original) + # Usamos copy() para asegurar que sea contiguo en memoria + tiles.append((tile.copy(), x, y)) return tiles - def combine_tiles_into_image(self, image: numpy_ndarray, tiles: list[numpy_ndarray], t_height: int, t_width: int, num_tiles_x: int) -> numpy_ndarray: + def combine_tiles_into_image( + self, + target_height: int, + target_width: int, + tiles_data: list[tuple], + output_channels: int + ) -> numpy_ndarray: + """ + Recompone la imagen usando las coordenadas exactas escaladas. + """ + # Crear lienzo vacío + if output_channels == 1: + tiled_image = numpy_zeros( + (target_height, target_width), dtype=uint8) + else: + tiled_image = numpy_zeros( + (target_height, target_width, output_channels), dtype=uint8) - match self.get_image_mode(image): - case "Grayscale": tiled_image = numpy_zeros((t_height, t_width, 3), dtype=uint8) - case "RGB": tiled_image = numpy_zeros((t_height, t_width, 3), dtype=uint8) - case "RGBA": tiled_image = numpy_zeros((t_height, t_width, 4), dtype=uint8) - # Default fallback - case _: tiled_image = numpy_zeros((t_height, t_width, 3), dtype=uint8) + for tile, orig_x, orig_y in tiles_data: + # Calcular la nueva posición basada en el factor de escala + # Nota: Upscale factor puede ser float, así que convertimos a int + new_y = int(orig_y * self.upscale_factor) + new_x = int(orig_x * self.upscale_factor) - for tile_index in range(len(tiles)): - actual_tile = tiles[tile_index] + h, w = tile.shape[:2] - tile_height, tile_width = self.get_image_resolution(actual_tile) + # Insertar tile en la posición calculada + # Verificamos límites por seguridad (aunque no debería pasar con matemáticas correctas) + end_y = min(new_y + h, target_height) + end_x = min(new_x + w, target_width) - row = tile_index // num_tiles_x - col = tile_index % num_tiles_x - y_start = row * tile_height - y_end = y_start + tile_height - x_start = col * tile_width - x_end = x_start + tile_width + # Ajustar recorte del tile si se sale del lienzo (clipping) + tile_h_crop = end_y - new_y + tile_w_crop = end_x - new_x - match self.get_image_mode(image): - case "Grayscale": tiled_image[y_start:y_end, x_start:x_end] = actual_tile - case "RGB": tiled_image[y_start:y_end, x_start:x_end] = actual_tile - case "RGBA": tiled_image[y_start:y_end, x_start:x_end] = self.add_alpha_channel(actual_tile) - # Default fallback - case _: tiled_image[y_start:y_end, x_start:x_end] = actual_tile + if output_channels > 1: + tiled_image[new_y:end_y, new_x:end_x, + :] = tile[:tile_h_crop, :tile_w_crop, :] + else: + tiled_image[new_y:end_y, + new_x:end_x] = tile[:tile_h_crop, :tile_w_crop] return tiled_image - # AI CLASS FUNCTIONS + # ------------------------------------------------------------------------- + # AI PROCESSING (PRE/INFERENCE/POST) + # ------------------------------------------------------------------------- def normalize_image(self, image: numpy_ndarray) -> tuple: - range = 255 - if numpy_max(image) > 256: - range = 65535 - normalized_image = image / range + """Normaliza la imagen a rango 0-1 float32 de forma precisa.""" + # Detección robusta de tipo + if image.dtype == uint8: + max_val = 255.0 + elif image.dtype == numpy.uint16: + max_val = 65535.0 + elif image.dtype == float32 or image.dtype == float16: + # Si ya es float, asumimos que podría estar en rango 0-1 o 0-255 + # Verificación heurística simple + if numpy_max(image) > 1.0: + max_val = 255.0 + else: + max_val = 1.0 + else: + # Fallback seguro + max_val = 255.0 - return normalized_image, range + if max_val == 1.0: + return image.astype(float32), 1 + + normalized = image.astype(float32) / max_val + return normalized, int(max_val) def preprocess_image(self, image: numpy_ndarray) -> numpy_ndarray: - # Optimización: Usar ascontiguousarray para mejor rendimiento de memoria + """HWC (Height, Width, Channels) -> NCHW (Batch, Channels, Height, Width).""" image = numpy_ascontiguousarray(image) - image = numpy_transpose(image, (2, 0, 1)) - image = numpy_expand_dims(image, axis=0) - + image = numpy_transpose(image, (2, 0, 1)) # HWC -> CHW + image = numpy_expand_dims(image, axis=0) # CHW -> NCHW return image def onnxruntime_inference(self, image: numpy_ndarray) -> numpy_ndarray: + """Ejecuta la inferencia ONNX.""" + if self.inferenceSession is None: + self._load_inferenceSession() - # IO BINDING - # io_binding = self.inferenceSession.io_binding() - # io_binding.bind_cpu_input(self.inferenceSession.get_inputs()[0].name, image.astype(float16)) - # io_binding.bind_output(self.inferenceSession.get_outputs()[0].name) - # self.inferenceSession.run_with_iobinding(io_binding) - # onnx_output = io_binding.copy_outputs_to_cpu()[0] + input_name = self.inferenceSession.get_inputs()[0].name + onnx_input = {input_name: image} - onnx_input = {self.inferenceSession.get_inputs()[0].name: image} + # Ejecutar (run devuelve una lista, tomamos el primer output) onnx_output = self.inferenceSession.run(None, onnx_input)[0] - return onnx_output def postprocess_output(self, onnx_output: numpy_ndarray) -> numpy_ndarray: - onnx_output = numpy_squeeze(onnx_output, axis=0) - onnx_output = numpy_clip(onnx_output, 0, 1) - onnx_output = numpy_transpose(onnx_output, (1, 2, 0)) - + """NCHW -> HWC y Clip 0-1.""" + onnx_output = numpy_squeeze(onnx_output, axis=0) # Remove batch dim + onnx_output = numpy_clip(onnx_output, 0, 1) # Ensure valid range + onnx_output = numpy_transpose(onnx_output, (1, 2, 0)) # CHW -> HWC return onnx_output def de_normalize_image(self, onnx_output: numpy_ndarray, max_range: int) -> numpy_ndarray: - match max_range: - case 255: return (onnx_output * max_range).astype(uint8) - case 65535: return (onnx_output * max_range).round().astype(float32) - # Default fallback to 255 - case _: return (onnx_output * 255).astype(uint8) + """Float 0-1 -> Uint8/16 0-MaxRange con redondeo bancario seguro.""" + # Clip para evitar overflow por artefactos de IA + onnx_output = numpy_clip(onnx_output, 0.0, 1.0) + + if max_range == 65535: + return (onnx_output * 65535.0).round().astype(numpy.uint16) + else: + # Default a 255 (uint8) + return (onnx_output * 255.0).round().astype(uint8) + + # ------------------------------------------------------------------------- + # CORE UPSCALE LOGIC + # ------------------------------------------------------------------------- def AI_upscale(self, image: numpy_ndarray) -> numpy_ndarray: try: - # Validación de entrada if image is None or image.size == 0: - raise ValueError("Imagen de entrada inválida o vacía") + raise ValueError("Input image is empty") - # Optimización: Usar memoria contigua antes de procesar - image = numpy_ascontiguousarray(image, dtype=float32) - image_mode = self.get_image_mode(image) - image, range = self.normalize_image(image) + image = numpy_ascontiguousarray(image) + height, width = image.shape[:2] - match image_mode: - case "RGB": - image = self.preprocess_image(image) - onnx_output = self.onnxruntime_inference(image) - onnx_output = self.postprocess_output(onnx_output) - output_image = self.de_normalize_image(onnx_output, range) + # --- MANEJO ROBUSTO DE CANALES (Fix 7) --- - return output_image + # Caso 1: Escala de grises (2D) -> Convertir a 3D RGB + if image.ndim == 2: + image = opencv_cvtColor(image, COLOR_GRAY2RGB) + is_grayscale_output = True + has_alpha = False - case "RGBA": - # Enhanced RGBA processing with improved alpha channel handling - try: - # Extract alpha channel (preserve original precision) - alpha_original = image[:, :, 3] - # Get RGB channels - rgb_image = image[:, :, :3] + # Caso 2: RGBA (4 Canales) -> Separar Alpha + elif image.ndim == 3 and image.shape[2] == 4: + channels_rgb = image[:, :, :3] + channel_alpha = image[:, :, 3] + image = channels_rgb # Procesaremos solo RGB con la IA + has_alpha = True + is_grayscale_output = False - # Store original alpha properties for quality preservation - alpha_dtype = alpha_original.dtype - alpha_min, alpha_max = numpy_min( - alpha_original), numpy_max(alpha_original) + # Caso 3: RGB Estándar + else: + has_alpha = False + is_grayscale_output = False - # Ensure proper data types for processing - rgb_image = rgb_image.astype(float32) - alpha_original = alpha_original.astype(float32) + # --- INFERENCIA --- + # Normalizar RGB + image_norm, range_val = self.normalize_image(image) + processed_input = self.preprocess_image(image_norm) - # Process RGB channels through AI model - processed_rgb = self.preprocess_image(rgb_image) - onnx_output_rgb = self.onnxruntime_inference( - processed_rgb) - onnx_output_rgb = self.postprocess_output( - onnx_output_rgb) + # Ejecutar ONNX + onnx_output = self.onnxruntime_inference(processed_input) + post_output = self.postprocess_output(onnx_output) - # Enhanced alpha channel processing - # Method 1: Use simple upscaling for alpha (faster, maintains transparency structure) - target_height, target_width = onnx_output_rgb.shape[:2] + # Desnormalizar + upscaled_rgb = self.de_normalize_image(post_output, range_val) - # Upscale alpha using the same factor as RGB - if alpha_original.shape != (target_height, target_width): - # Use area interpolation for downscaling, cubic for upscaling - if target_height * target_width > alpha_original.shape[0] * alpha_original.shape[1]: - alpha_upscaled = opencv_resize( - alpha_original, (target_width, target_height), interpolation=INTER_CUBIC) - else: - alpha_upscaled = opencv_resize( - alpha_original, (target_width, target_height), interpolation=INTER_AREA) - else: - alpha_upscaled = alpha_original.copy() + # --- RECONSTRUCCIÓN --- - # Optional: Apply AI processing to alpha channel for high-quality results - # This is computationally expensive but provides better results - try: - if self.upscale_factor > 1: # Only for actual upscaling - # Convert alpha to 3-channel grayscale for AI processing - alpha_3channel = numpy_stack( - [alpha_original] * 3, axis=-1) - processed_alpha = self.preprocess_image( - alpha_3channel) - onnx_output_alpha = self.onnxruntime_inference( - processed_alpha) - onnx_output_alpha = self.postprocess_output( - onnx_output_alpha) - # Extract single channel from AI-processed alpha - alpha_ai_processed = opencv_cvtColor( - onnx_output_alpha, COLOR_RGB2GRAY) + # Si teníamos Alpha, lo escalamos por separado y lo unimos + if has_alpha: + target_h, target_w = upscaled_rgb.shape[:2] + # Usamos Lanczos o Cubic para el canal alpha (suavidad) + # Importante: cv2.resize usa (width, height) + upscaled_alpha = opencv_resize( + channel_alpha, (target_w, + target_h), interpolation=INTER_CUBIC + ) + # Expandir dims si es necesario para concatenar + if upscaled_alpha.ndim == 2: + upscaled_alpha = numpy_expand_dims(upscaled_alpha, axis=2) - # Blend AI-processed alpha with simple upscaled alpha (preserves structure) - alpha_blend_factor = 0.7 # Favor AI processing but keep some original structure - alpha_upscaled = opencv_addWeighted( - alpha_ai_processed.astype( - float32), alpha_blend_factor, - alpha_upscaled.astype( - float32), 1.0 - alpha_blend_factor, 0 - ) - except Exception as alpha_ai_error: - logging.debug( - f"AI alpha processing failed, using simple upscaling: {str(alpha_ai_error)}") - # Continue with simple upscaled alpha + return numpy_concatenate((upscaled_rgb, upscaled_alpha), axis=2) - # Ensure alpha values are in valid range - alpha_upscaled = numpy_clip(alpha_upscaled, 0, 1) + # Si era grayscale original, devolver grayscale + if is_grayscale_output: + return opencv_cvtColor(upscaled_rgb, COLOR_RGB2GRAY) - # Combine RGB and Alpha channels - if len(alpha_upscaled.shape) == 2: - alpha_upscaled = numpy_expand_dims( - alpha_upscaled, axis=-1) - - # Create final RGBA image - output_image = numpy_concatenate( - (onnx_output_rgb, alpha_upscaled), axis=2) - - # Denormalize the complete RGBA image - output_image = self.de_normalize_image( - output_image, range) - - except Exception as rgba_error: - logging.error( - f"RGBA processing error: {str(rgba_error)}") - # Fallback: process as RGB and add opaque alpha - rgb_processed = self.preprocess_image(image[:, :, :3]) - onnx_output = self.onnxruntime_inference(rgb_processed) - onnx_output = self.postprocess_output(onnx_output) - - # Add opaque alpha channel - h, w = onnx_output.shape[:2] - alpha_opaque = numpy_full( - (h, w, 1), 1.0, dtype=onnx_output.dtype) - output_image = numpy_concatenate( - (onnx_output, alpha_opaque), axis=2) - output_image = self.de_normalize_image( - output_image, range) - - return output_image - - case "Grayscale": - image = opencv_cvtColor(image, COLOR_GRAY2RGB) - - image = self.preprocess_image(image) - onnx_output = self.onnxruntime_inference(image) - onnx_output = self.postprocess_output(onnx_output) - output_image = opencv_cvtColor(onnx_output, COLOR_RGB2GRAY) - output_image = self.de_normalize_image(output_image, range) - - return output_image - - case _: - raise ValueError( - f"Modo de imagen no soportado: {image_mode}") + return upscaled_rgb except Exception as e: - logging.error(f"Error en AI_upscale: {str(e)}") - raise RuntimeError(f"Fallo en el escalado de imagen: {str(e)}") + logging.error(f"AI Upscale Core Error: {str(e)}") + raise RuntimeError(f"Upscaling failed: {str(e)}") + try: + if image is None or image.size == 0: + raise ValueError("Input image is empty") + + # Asegurar memoria contigua + image = numpy_ascontiguousarray(image) + image_mode = self.get_image_mode(image) + + # Normalización inicial + image_norm, range_val = self.normalize_image(image) + + if image_mode == "RGB": + processed_input = self.preprocess_image(image_norm) + onnx_output = self.onnxruntime_inference(processed_input) + post_output = self.postprocess_output(onnx_output) + return self.de_normalize_image(post_output, range_val) + + elif image_mode == "Grayscale": + # Convertir a RGB para la IA, luego volver a gris + image_rgb = opencv_cvtColor(image, COLOR_GRAY2RGB) + image_rgb_norm, _ = self.normalize_image(image_rgb) + + processed_input = self.preprocess_image(image_rgb_norm) + onnx_output = self.onnxruntime_inference(processed_input) + post_output = self.postprocess_output(onnx_output) + + output_rgb = self.de_normalize_image(post_output, range_val) + return opencv_cvtColor(output_rgb, COLOR_RGB2GRAY) + + elif image_mode == "RGBA": + # Estrategia Optimizada: + # 1. Separar RGB y Alpha + # 2. Escalar RGB con IA + # 3. Escalar Alpha con Bicubic (más rápido y evita artefactos de IA en máscaras) + + # Separar canales (manteniendo uint8 original para alpha) + alpha_channel = image[:, :, 3] + rgb_channel = image[:, :, :3] + + # Procesar RGB + # Llamada recursiva manejada como RGB simple para aprovechar la lógica existente + # Nota: Para evitar recursión infinita si hay bugs, usamos la lógica inline aquí + rgb_norm, r_val = self.normalize_image(rgb_channel) + rgb_input = self.preprocess_image(rgb_norm) + rgb_output_onnx = self.onnxruntime_inference(rgb_input) + rgb_output_post = self.postprocess_output(rgb_output_onnx) + upscaled_rgb = self.de_normalize_image(rgb_output_post, r_val) + + # Procesar Alpha + # Calcular dimensiones objetivo basadas en la salida RGB + target_h, target_w = upscaled_rgb.shape[:2] + + # Usar CUBIC para bordes suaves o AREA si fuera downscale (raro aquí) + interpolation = INTER_CUBIC if self.upscale_factor > 1 else INTER_AREA + upscaled_alpha = opencv_resize( + alpha_channel, (target_w, target_h), interpolation=interpolation) + + # Asegurar dimensión extra para concatenar + if upscaled_alpha.ndim == 2: + upscaled_alpha = numpy_expand_dims(upscaled_alpha, axis=2) + + # Recombinar + return numpy_concatenate((upscaled_rgb, upscaled_alpha), axis=2) + + else: + raise ValueError(f"Unsupported image mode: {image_mode}") + + except Exception as e: + logging.error(f"AI Upscale Core Error: {str(e)}") + raise RuntimeError(f"Upscaling failed: {str(e)}") def AI_upscale_with_tilling(self, image: numpy_ndarray) -> numpy_ndarray: - t_height, t_width = self.calculate_target_resolution(image) - tiles_x, tiles_y = self.calculate_tiles_number(image) - tiles_list = self.split_image_into_tiles(image, tiles_x, tiles_y) - tiles_list = [self.AI_upscale(tile) for tile in tiles_list] + try: + target_h, target_w = self.calculate_target_resolution(image) - return self.combine_tiles_into_image(image, tiles_list, t_height, t_width, tiles_x) + # 1. Obtener tiles con coordenadas + tiles_data = self.split_image_into_tiles(image) + processed_tiles = [] - # EXTERNAL FUNCTION + print( + f"[AI] Tiling enabled: processing {len(tiles_data)} tiles...") + + # 2. Procesar cada tile + for tile_img, x, y in tiles_data: + upscaled_tile = self.AI_upscale(tile_img) + processed_tiles.append((upscaled_tile, x, y)) + + # Liberar memoria inmediata + del tile_img + + del tiles_data + gc.collect() + + # 3. Detectar canales de salida + sample = processed_tiles[0][0] + channels = sample.shape[2] if sample.ndim == 3 else 1 + + # 4. Recombinar con precisión + final_image = self.combine_tiles_into_image( + target_h, target_w, processed_tiles, channels + ) + + return final_image + + except Exception as e: + logging.error(f"Tiling Error: {str(e)}") + raise RuntimeError(f"Tiled upscaling failed: {str(e)}") + + # ------------------------------------------------------------------------- + # ORCHESTRATION (PUBLIC API) + # ------------------------------------------------------------------------- def AI_orchestration(self, image: numpy_ndarray) -> numpy_ndarray: - - if self.inferenceSession == None: + """Punto de entrada principal para procesar una imagen.""" + if self.inferenceSession is None: self._load_inferenceSession() - resized_image = self.resize_with_input_factor(image) + # 1. Redimensionar entrada (si aplica input % < 100) + try: + resized_input = self.resize_with_input_factor(image) + except Exception as e: + logging.warning( + f"Input resizing failed: {e}. Using original image.") + resized_input = image - if self.image_need_tilling(resized_image): - upscaled_image = self.AI_upscale_with_tilling(resized_image) + # 2. Decidir si usar Tiling o Directo + # Si el modelo no escala (x1), el tiling se basa en tamaño puro. + # Si escala, hay que tener cuidado con la expansión de memoria. + if self.image_need_tilling(resized_input): + upscaled_image = self.AI_upscale_with_tilling(resized_input) else: - upscaled_image = self.AI_upscale(resized_image) + upscaled_image = self.AI_upscale(resized_input) - return self.resize_with_output_factor(upscaled_image) + # 3. Redimensionar salida (si aplica output % != 100) + try: + final_image = self.resize_with_output_factor(upscaled_image) + except Exception as e: + logging.warning( + f"Output resizing failed: {e}. Using upscaled image directly.") + final_image = upscaled_image + + return final_image # AI INTERPOLATION for frame generation ----------------- @@ -1162,6 +1341,63 @@ class AI_face_restoration: # LÓGICA PRINCIPAL # ------------------- def face_restoration(self, image: numpy_ndarray) -> numpy_ndarray: + """ + Aplica restauración facial. + CORREGIDO: Usa float32 estándar para máxima compatibilidad y estabilidad. + Eliminada la detección frágil de fp16. + """ + if self.inferenceSession is None: + self._load_inferenceSession() + + # Guardar datos originales + original_h, original_w = self.get_image_resolution(image) + + # Manejo de Alpha (similar a la corrección #7) + original_alpha = None + if len(image.shape) == 3 and image.shape[2] == 4: + original_alpha = image[:, :, 3] + image = image[:, :, :3] # Quedarse solo con RGB + + # 1. Redimensionar entrada si el usuario lo pidió + resized_input = self.resize_with_input_factor(image) + + # 2. Preprocesar (Normalizar a 0-1 float32 y HWC->NCHW) + preprocessed, _ = self.preprocess_face_image(resized_input) + + # 3. Inferencia (Directa en float32) + try: + input_name = self.inferenceSession.get_inputs()[0].name + output_name = self.inferenceSession.get_outputs()[0].name + + # Ejecutar sin castings extraños + output = self.inferenceSession.run( + [output_name], {input_name: preprocessed})[0] + + except Exception as e: + raise RuntimeError(f"GFPGAN inference failed: {e}") + + # 4. Postprocesar + # Nota: pasamos el tamaño del resized_input para que el modelo devuelva + # la cara restaurada en la escala de trabajo actual + current_h, current_w = resized_input.shape[:2] + restored_face = self.postprocess_face_image( + output, (current_h, current_w)) + + # 5. Restaurar Alpha si existía + if original_alpha is not None: + target_h, target_w = restored_face.shape[:2] + upscaled_alpha = opencv_resize( + original_alpha, (target_w, target_h), interpolation=INTER_CUBIC + ) + if upscaled_alpha.ndim == 2: + upscaled_alpha = numpy_expand_dims(upscaled_alpha, axis=2) + restored_face = numpy_concatenate( + (restored_face, upscaled_alpha), axis=2) + + # 6. Redimensionar salida final si el usuario lo pidió + final_image = self.resize_with_output_factor(restored_face) + + return final_image """ Orquestador principal: aplica restauración facial usando el modelo ONNX. Retorna la imagen restaurada (preservando alpha cuando sea necesario). @@ -1261,15 +1497,24 @@ class AI_face_restoration: # ------------------- # Orquestador público # ------------------- + def AI_orchestration(self, image: numpy_ndarray) -> numpy_ndarray: """ Método público que otros módulos llaman para aplicar restauración facial. + Maneja errores críticos de memoria re-lanzándolos. """ try: return self.face_restoration(image) except Exception as e: + error_str = str(e).lower() + # --- CAMBIO CRÍTICO: Detectar errores de memoria --- + # Si es un error de memoria (CUDA OOM), lo relanzamos para que el + # orquestador principal active la reducción de tiles/resolución. + if any(k in error_str for k in ['memory', 'cuda', 'allocation', 'resource']): + raise e + + # Si es otro error (ej. datos corruptos), logueamos y devolvemos original print(f"[FACE RESTORATION ERROR] {str(e)}") - # En caso de fallo devolvemos la imagen original (no alterada) return image @@ -1299,8 +1544,7 @@ class MessageBox(CTkToplevel): self.title('') - # --> ESTA LÍNEA DEBE SER ELIMINADA O COMENTADA - # self.attributes("-topmost", True) # stay on top + self.attributes("-topmost", True) # stay on top self.protocol("WM_DELETE_WINDOW", self._on_closing) @@ -1313,7 +1557,7 @@ class MessageBox(CTkToplevel): self.maxsize(900, 800) # Set initial window size based on content - self.geometry("750x600") + self.geometry("600x400") def _ok_event( self, @@ -2282,7 +2526,7 @@ def cleanup_incomplete_frames(target_directory: str, expected_count: int) -> Non files = os_listdir(target_directory) frame_files = [f for f in files if f.startswith( - 'frame_') and f.endswith('.jpg')] + 'frame_') and f.endswith('.png')] if len(frame_files) < expected_count: print( @@ -2335,7 +2579,7 @@ def extract_frame_number_from_path(frame_path: str) -> int: """Extract frame number from frame file path.""" try: filename = os_path_basename(frame_path) - # Extract number from patterns like "frame_001.jpg" + # Extract number from patterns like "frame_001.png" import re match = re.search(r'frame_(\d+)', filename) if match: @@ -2452,10 +2696,15 @@ def validate_system_requirements() -> bool: def create_dir(name_dir: str) -> None: - if os_path_exists(name_dir): - remove_directory(name_dir) - if not os_path_exists(name_dir): - os_makedirs(name_dir, mode=0o777) + """ + Crea un directorio si no existe. + CORREGIDO: Ya no borra el directorio si existe (evita pérdida de datos). + """ + try: + if not os_path_exists(name_dir): + os_makedirs(name_dir, exist_ok=True) + except Exception as e: + print(f"[ERROR] Could not create directory {name_dir}: {e}") def stop_thread() -> None: @@ -2529,8 +2778,27 @@ def image_read(file_path: str) -> numpy_ndarray: raise RuntimeError(error_msg) -def image_write(file_path: str, file_data: numpy_ndarray, file_extension: str = ".jpg") -> None: - opencv_imencode(file_extension, file_data)[1].tofile(file_path) +def image_write(file_path: str, file_data: numpy_ndarray, file_extension: str = ".png") -> None: + """ + Escribe la imagen en disco de forma robusta, manejando rutas Unicode y buffer de escritura. + """ + try: + # 1. Codificar la imagen a memoria (buffer) usando la extensión correcta + success, buffer = opencv_imencode(file_extension, file_data) + + if not success: + raise RuntimeError( + f"Could not encode image format: {file_extension}") + + # 2. Escribir el buffer al disco usando manejo estándar de archivos + # Esto evita problemas con rutas non-ASCII que tiene cv2.imwrite + with open(file_path, "wb") as f: + f.write(buffer.tobytes()) + + except Exception as e: + # Loguear el error pero permitir que el proceso principal lo maneje si es necesario + logging.error(f"Failed to write image to {file_path}: {str(e)}") + raise RuntimeError(f"Failed to write image: {str(e)}") def copy_file_metadata(original_file_path: str, upscaled_file_path: str) -> None: @@ -2645,7 +2913,7 @@ def prepare_output_video_frame_filename( to_append += "_Blending-High" # Selected image extension - to_append += f".jpg" + to_append += f".png" output_path += to_append @@ -3126,12 +3394,11 @@ def video_encoding( video_output_path: str, upscaled_frame_paths: list[str], selected_video_codec: str, + fps_multiplier: int = 1, ) -> None: """ Video encoding function for Warlock-Studio. - - Toma los frames mejorados por IA y los recompone en un video final. - - Detecta y conserva (o re-codifica) el audio original del video. - - Maneja errores de FFmpeg de forma robusta y limpia temporales. + INCLUDES AUTOMATIC FALLBACK TO X264. """ try: @@ -3147,73 +3414,132 @@ def video_encoding( os_remove(temp_file) except Exception as e: print( - f"[WARNING] No se pudo eliminar temporal {temp_file}: {e}") + f"[WARNING] Temporary file could not be deleted {temp_file}: {e}") # --- Obtener FPS del video original --- try: video_fps = get_video_fps(video_path) if video_fps <= 0 or video_fps > 1000: raise ValueError(f"FPS inválido: {video_fps}") - video_fps_str = f"{video_fps:.6f}" + + # Calcular FPS finales + final_fps = video_fps * fps_multiplier + video_fps_str = f"{final_fps:.6f}" + except Exception as e: print( - f"[WARNING] No se pudieron obtener los FPS: {e}, usando 30.0 por defecto") + f"[WARNING] Could not obtain FPS: {e}, using 30.0 by default") video_fps_str = "30.000000" # --- Crear lista de frames para FFmpeg --- if not create_frame_list_file(upscaled_frame_paths, txt_path): - raise RuntimeError("Error al crear el archivo de lista de frames") + raise RuntimeError("Error creating the frames list file") - # --- Configurar codificación principal --- - codec_settings = get_video_codec_settings( - selected_video_codec, {'fps': video_fps_str}) - encoding_command = build_encoding_command( - video_path, txt_path, no_audio_path, codec_settings, video_fps_str) + # ============================================================================== + # LOGICA DE FALLBACK (INTENTOS DE CODIFICACIÓN) + # ============================================================================== - print(f"[FFMPEG] Iniciando codificación con {codec_settings['codec']}") - print( - f"[FFMPEG] Procesando {len(upscaled_frame_paths)} frames a {video_fps_str} FPS") + # Intentaremos máximo 2 veces: + # Intento 0: El códec seleccionado por el usuario (ej. h264_nvenc) + # Intento 1: Fallback a CPU (x264) si el anterior falla - # --- Ejecutar FFmpeg para generar video sin audio --- - try: - result = subprocess_run( - encoding_command, - check=True, - capture_output=True, - text=True, - timeout=3600 - ) - if not os_path_exists(no_audio_path): - raise RuntimeError( - "FFmpeg terminó pero el archivo de salida no existe") - output_size = os_path_getsize(no_audio_path) - if output_size < 1024: - raise RuntimeError( - f"Archivo de salida sospechosamente pequeño: {output_size} bytes") - print( - f"[FFMPEG] Codificación de video completada ({output_size / (1024*1024):.1f} MB)") - except subprocess.TimeoutExpired: - error_msg = "FFmpeg excedió el tiempo límite (1 hora)" - log_and_report_error(error_msg) - write_process_status( - process_status_q, f"{ERROR_STATUS}{error_msg}") - return - except CalledProcessError as e: - error_details = e.stderr if e.stderr else str(e) - error_msg = f"FFmpeg falló durante codificación: {error_details}" - if "Unknown encoder" in error_details: - error_msg += "\nEl códec seleccionado no está soportado. Prueba con x264." - elif "Device or resource busy" in error_details: - error_msg += "\nEl codificador GPU está ocupado. Prueba codificación por software." - elif "Invalid data" in error_details: - error_msg += "\nLos datos de los frames podrían estar dañados." - log_and_report_error(error_msg) - write_process_status( - process_status_q, f"{ERROR_STATUS}{error_msg}") - return + encoding_success = False + + for attempt in range(2): + try: + # Determinar qué códec usar en este intento + current_codec = selected_video_codec + + if attempt == 1: + # SI ESTAMOS EN EL INTENTO 1, ACTIVAMOS EL FALLBACK + process_status_q.put( + f"[LOG] [WARNING] Hardware encoding failed. Switching to CPU fallback (x264)...") + current_codec = 'x264' + + # --- Configurar codificación --- + codec_settings = get_video_codec_settings( + current_codec, {'fps': video_fps_str}) + encoding_command = build_encoding_command( + video_path, txt_path, no_audio_path, codec_settings, video_fps_str) + + process_status_q.put( + f"[LOG] [FFMPEG] Attempt {attempt+1}: Encoding with {codec_settings['codec']}") + + # --- Ejecutar FFmpeg --- + process = subprocess.Popen( + encoding_command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding='utf-8', + errors='replace', + creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0 + ) + + # Leer salida + for line in process.stdout: + line = line.strip() + if line: + process_status_q.put(f"[LOG] {line}") + + process.wait() + + # Verificar éxito + if process.returncode != 0: + # Si falló y es el primer intento, lanzamos excepción para que el 'except' la capture y active el fallback + if attempt == 0: + raise RuntimeError( + f"FFmpeg failed with code {process.returncode}") + else: + # Si falló el fallback (x264), ya es un error fatal + raise RuntimeError("FFmpeg CPU Fallback also failed.") + + if not os_path_exists(no_audio_path): + if attempt == 0: + raise RuntimeError("Output file missing") + else: + raise RuntimeError( + "Output file missing after fallback") + + # Si llegamos aquí, todo salió bien + output_size = os_path_getsize(no_audio_path) + if output_size < 1024: + if attempt == 0: + raise RuntimeError("Output file too small") + else: + raise RuntimeError( + "Output file too small after fallback") + + process_status_q.put( + f"[LOG] [FFMPEG] Encoding complete ({output_size / (1024*1024):.1f} MB)") + encoding_success = True + break # Salir del bucle de intentos + + except Exception as e: + # Si falló el intento 0 (Hardware), limpiamos y permitimos que el bucle continúe al intento 1 (CPU) + if attempt == 0: + process_status_q.put( + f"[LOG] [WARNING] Primary encoding failed: {e}. Preparing fallback...") + if os_path_exists(no_audio_path): + try: + os_remove(no_audio_path) + except: + pass + continue # Continúa al siguiente ciclo del for (fallback) + else: + # Si falló el intento 1, relanzamos el error final + raise e + + if not encoding_success: + raise RuntimeError("Encoding failed after all attempts.") + + # ============================================================================== + # FIN LOGICA DE FALLBACK - CONTINUA DETECCIÓN DE AUDIO + # ============================================================================== # --- Detección de audio --- - print("[FFMPEG] Verificando pista de audio del video original...") + process_status_q.put( + "[LOG] [FFMPEG] Checking audio track of original video...") ffprobe_path = None try: @@ -3235,21 +3561,16 @@ def video_encoding( if ffprobe_path: try: probe_cmd = [ - ffprobe_path, - "-v", "error", - "-select_streams", "a", + ffprobe_path, "-v", "error", "-select_streams", "a", "-show_entries", "stream=codec_name", - "-of", "default=noprint_wrappers=1:nokey=1", - video_path + "-of", "default=noprint_wrappers=1:nokey=1", video_path ] probe = subprocess_run( probe_cmd, capture_output=True, text=True, timeout=30) audio_codec = probe.stdout.strip() - print(f"[FFPROBE] stdout: {probe.stdout.strip()}") - print(f"[FFPROBE] stderr: {probe.stderr.strip()}") has_audio = bool(audio_codec) except Exception as e: - print(f"[WARNING] No se pudo detectar audio con ffprobe: {e}") + print(f"[WARNING] Audio probe failed: {e}") has_audio = False if not ffprobe_path or not has_audio: @@ -3258,103 +3579,84 @@ def video_encoding( probe = subprocess_run( probe_cmd, capture_output=True, text=True, timeout=20) stderr_output = probe.stderr or probe.stdout or "" - print(f"[FFMPEG PROBE] Primeras líneas del stderr:") - print("\n".join(stderr_output.splitlines()[:10])) - for line in stderr_output.splitlines(): - if "Audio:" in line: - has_audio = True - audio_codec = line.strip() - break - except Exception as e: - print(f"[WARNING] Fallback de detección con FFmpeg falló: {e}") - has_audio = False + if "Audio:" in stderr_output: + has_audio = True + except Exception: + pass - print(f"[FFMPEG] has_audio={has_audio}, audio_codec={audio_codec!r}") + process_status_q.put( + f"[LOG] [FFMPEG] has_audio={has_audio}, codec={audio_codec}") - # --- Estrategias de audio --- if has_audio: - # Estrategia 1: copiar pista original audio_copy_cmd = [ - FFMPEG_EXE_PATH, - "-y", "-loglevel", "error", - "-i", video_path, - "-i", no_audio_path, - "-c:v", "copy", - "-c:a", "copy", - "-map", "1:v:0", - "-map", "0:a:0", + FFMPEG_EXE_PATH, "-y", "-loglevel", "error", + "-i", video_path, "-i", no_audio_path, + "-c:v", "copy", "-c:a", "copy", + "-map", "1:v:0", "-map", "0:a", "-shortest", video_output_path ] try: - print("[FFMPEG] Intentando copiar pista de audio...") - res = subprocess_run( - audio_copy_cmd, check=True, capture_output=True, text=True, timeout=600) - print(f"[FFMPEG] stdout:\n{res.stdout}") - print(f"[FFMPEG] stderr:\n{res.stderr}") + process_status_q.put( + "[LOG] [FFMPEG] Trying to copy audio track...") + subprocess_run(audio_copy_cmd, check=True, + capture_output=True, text=True) + if os_path_exists(no_audio_path): os_remove(no_audio_path) - print("[FFMPEG] Copia de audio completada exitosamente.") + process_status_q.put( + "[LOG] [FFMPEG] Audio copy completed successfully.") return except Exception as e: - print(f"[WARNING] Copia de audio falló: {e}") - print("[FFMPEG] Intentando re-codificar audio a AAC...") + process_status_q.put( + f"[LOG] [WARNING] Audio copy failed, re-encoding... ({e})") - # Estrategia 2: re-codificar audio audio_reencode_cmd = [ - FFMPEG_EXE_PATH, - "-y", "-loglevel", "error", - "-i", video_path, - "-i", no_audio_path, - "-c:v", "copy", - "-c:a", "aac", - "-b:a", "128k", - "-map", "1:v:0", - "-map", "0:a:0", + FFMPEG_EXE_PATH, "-y", "-loglevel", "error", + "-i", video_path, "-i", no_audio_path, + "-c:v", "copy", "-c:a", "aac", "-b:a", "192k", + "-map", "1:v:0", "-map", "0:a", "-shortest", video_output_path ] try: - res = subprocess_run( - audio_reencode_cmd, check=True, capture_output=True, text=True, timeout=600) - print(f"[FFMPEG] Re-codificación de audio completada.") + subprocess_run(audio_reencode_cmd, check=True, + capture_output=True, text=True) if os_path_exists(no_audio_path): os_remove(no_audio_path) + process_status_q.put( + "[LOG] [FFMPEG] Audio re-encoding completed.") return except Exception as audio_error: - print( - f"[WARNING] Re-codificación de audio falló: {audio_error}") + process_status_q.put( + f"[LOG] [WARNING] Audio re-encoding failed: {audio_error}") - # Estrategia 3: sin audio try: if os_path_exists(no_audio_path): shutil_move(no_audio_path, video_output_path) - print("[FFMPEG] Video final sin pista de audio (fallback).") + process_status_q.put( + "[LOG] [FFMPEG] Final video saved WITHOUT audio (fallback).") return except Exception as move_error: - raise RuntimeError( - f"No se pudo mover archivo final sin audio: {move_error}") + raise RuntimeError(f"Could not move final file: {move_error}") else: - # Video original sin pista de audio try: shutil_move(no_audio_path, video_output_path) - print( - "[FFMPEG] Video guardado sin pista de audio (originalmente mudo).") + process_status_q.put( + "[LOG] [FFMPEG] Video saved (original was muted).") return except Exception as move_error: - raise RuntimeError( - f"No se pudo guardar video sin audio: {move_error}") + raise RuntimeError(f"Video could not be saved: {move_error}") except Exception as e: - error_msg = f"Error general en video_encoding: {str(e)}" + error_msg = f"General error in video_encoding: {str(e)}" log_and_report_error(error_msg) write_process_status(process_status_q, f"{ERROR_STATUS}{error_msg}") - # Limpieza de temporales if 'txt_path' in locals() and os_path_exists(txt_path): try: os_remove(txt_path) - except Exception: + except: pass @@ -3384,7 +3686,7 @@ def get_video_frames_for_upscaling_resume( # Only file names directory_files = os_listdir(target_directory) original_frames_path = [ - file for file in directory_files if file.endswith('.jpg')] + file for file in directory_files if file.endswith('.png')] original_frames_path = [ file for file in original_frames_path if selected_AI_model not in file] @@ -3425,7 +3727,7 @@ def blend_images_and_save( starting_image: numpy_ndarray, upscaled_image: numpy_ndarray, starting_image_importance: float, - file_extension: str = ".jpg" + file_extension: str = ".png" ) -> None: def add_alpha_channel(image: numpy_ndarray) -> numpy_ndarray: @@ -3461,29 +3763,23 @@ def blend_images_and_save( starting_image, (target_width, target_height)) try: - # Get image modes for both images starting_mode = get_image_mode(starting_image) upscaled_mode = get_image_mode(upscaled_image) - # Ensure both images have the same number of channels if starting_mode == "RGBA" or upscaled_mode == "RGBA": - # Convert both to RGBA if either is RGBA if starting_mode != "RGBA": starting_image = add_alpha_channel(starting_image) if upscaled_mode != "RGBA": upscaled_image = add_alpha_channel(upscaled_image) elif starting_mode == "RGB" and upscaled_mode != "RGB": - # Convert grayscale to RGB if needed if upscaled_mode == "Grayscale": upscaled_image = opencv_cvtColor( upscaled_image, COLOR_GRAY2RGB) elif upscaled_mode == "RGB" and starting_mode != "RGB": - # Convert grayscale to RGB if needed if starting_mode == "Grayscale": starting_image = opencv_cvtColor( starting_image, COLOR_GRAY2RGB) - # Ensure images have the same dtype if starting_image.dtype != upscaled_image.dtype: upscaled_image = upscaled_image.astype(starting_image.dtype) @@ -3497,75 +3793,125 @@ def blend_images_and_save( image_write(target_path, upscaled_image, file_extension) -# ==== CORE PROCESSING SECTION ==== +# ==== CORE PROCESSING SECTION (CORREGIDO) ==== def check_upscale_steps() -> None: - """Monitorea el estado del proceso de escalado en un hilo separado.""" - global stop_thread_flag - sleep(1) + global stop_thread_flag, app + + # Limpiar flag al iniciar + stop_thread_flag.clear() while not stop_thread_flag.is_set(): try: + # Lectura no bloqueante actual_step = read_process_status() - if actual_step == COMPLETED_STATUS: - info_message.set(f"All files completed!") - stop_upscale_process() - stop_thread_flag.set() # Señaliza la finalización del hilo - break # Sal del bucle + if actual_step is None: + # Si no hay mensajes, verificar si el proceso sigue vivo + # Si el proceso murió inesperadamente sin mandar "Completed" o "Stop", salimos + global process_upscale_orchestrator + if process_upscale_orchestrator and not process_upscale_orchestrator.is_alive() and not stop_thread_flag.is_set(): + # El proceso murió silenciosamente, forzamos el stop + actual_step = STOP_STATUS + else: + # Simplemente esperar y reintentar + sleep(0.1) + continue - elif actual_step == STOP_STATUS: - info_message.set(f"Magic stopped") - stop_upscale_process() - stop_thread_flag.set() # Señaliza la finalización del hilo - break # Sal del bucle + # --- DETECTAR LOGS --- + if actual_step.startswith("[LOG]"): + log_text = actual_step.replace("[LOG] ", "") + console.write_log(log_text) + continue - elif ERROR_STATUS in actual_step: - info_message.set(f"Error while upscaling :(") - error_to_show = actual_step.replace(ERROR_STATUS, "") - show_error_message(error_to_show.strip()) - stop_thread_flag.set() # Señaliza la finalización del hilo - break # Sal del bucle + # --- LÓGICA DE FINALIZACIÓN (Completado, Stop o Error) --- + if actual_step in [COMPLETED_STATUS, STOP_STATUS] or ERROR_STATUS in actual_step: + + if actual_step == COMPLETED_STATUS: + info_message.set("All files completed!") + console.write_log( + "Process Completed Successfully", "SUCCESS") + elif actual_step == STOP_STATUS: + info_message.set("Process Stopped") + console.write_log("Process stopped by user", "WARNING") + elif ERROR_STATUS in actual_step: + err_msg = actual_step.replace(ERROR_STATUS, "") + info_message.set("Error occurred") + console.write_log(f"Error: {err_msg}", "ERROR") + show_error_message(err_msg) + + # 1. Detener proceso físico + stop_upscale_process() + + # 2. Romper el bucle + stop_thread_flag.set() + + # 3. RESTAURAR UI (Regresar botón a "Make Magic") + # Usamos .after para asegurar que corra en el hilo principal de la UI + window.after(100, place_upscale_button) + break + + # --- ESTADOS INTERMEDIOS --- else: info_message.set(actual_step) + # Opcional, puede ser mucho spam + console.write_log(f"Status: {actual_step}", "INFO") - sleep(1) except Exception as e: - # Si hay un error al leer la cola, el proceso principal probablemente murió. - print(f"[MONITOR] Error reading process status: {str(e)}") - # Sal del bucle para terminar el hilo. + print(f"[MONITOR ERROR] {e}") + stop_thread_flag.set() + window.after(100, place_upscale_button) break - # Se asegura de que el botón de re-inicio aparezca al final - place_upscale_button() - def read_process_status() -> str: - return process_status_q.get() + try: + # Intentar leer con un timeout muy corto (0.05s) + # Esto evita que la GUI se congele si no hay mensajes + return process_status_q.get(timeout=0.05) + except Exception: + # Si la cola está vacía (TimeOut), devolvemos None + return None def write_process_status(process_status_q: multiprocessing_Queue, step: str) -> None: - - print(f"{step}") - while not process_status_q.empty(): - process_status_q.get() + # CORRECCIÓN CRITICA: NO VACIAR LA COLA ANTES DE ESCRIBIR + # Esto borraba los logs antes de que pudieran leerse. + print(f"[QUEUE] Put: {step}") process_status_q.put(f"{step}") def stop_upscale_process() -> None: global process_upscale_orchestrator try: - process_upscale_orchestrator - except NameError: - pass - else: - # Fix 4.1: Use terminate() instead of kill() for safer process termination - process_upscale_orchestrator.terminate() + if 'process_upscale_orchestrator' in globals() and process_upscale_orchestrator: + if process_upscale_orchestrator.is_alive(): + print("Terminating process...") + process_upscale_orchestrator.terminate() + # Esperar máx 1 segundo a que muera, si no, continuar + process_upscale_orchestrator.join(timeout=1.0) + if process_upscale_orchestrator.is_alive(): + # Si sigue vivo (zombie), kill forzado (Python 3.7+) + try: + process_upscale_orchestrator.kill() + except: + pass + + process_upscale_orchestrator = None + except Exception as e: + print(f"Error stopping process: {e}") def stop_button_command() -> None: + # 1. Notificar visualmente inmediato + info_message.set("Stopping...") + + # 2. Matar el proceso inmediatamente stop_upscale_process() - write_process_status(process_status_q, f"{STOP_STATUS}") + + # 3. Enviar señal a la cola para que el hilo de monitoreo (check_upscale_steps) + # sepa que debe salir y restaurar la UI. + write_process_status(process_status_q, STOP_STATUS) def upscale_button_command() -> None: @@ -3585,6 +3931,8 @@ def upscale_button_command() -> None: global selected_frame_generation_option global process_upscale_orchestrator global stop_thread_flag + global process_upscale_orchestrator + global stop_thread_flag # Fix 2.2: Clear stop_thread_flag at the beginning of each execution stop_thread_flag.clear() @@ -3643,7 +3991,7 @@ def fluidframes_interpolation_pipeline( selected_generation_option, selected_image_extension, selected_video_extension, selected_video_codec, input_resize_factor, output_resize_factor, cpu_number, selected_keep_frames): ''' - This function runs all the FluidFrames video/image interpolation generation logic in one go for Warlock Studio. + This function runs all the FluidFrames video/image interpolation generation logic in one go for Warlock-Studio. ''' try: frame_gen_factor, slowmotion = check_frame_generation_option( @@ -3753,49 +4101,63 @@ def fluidframes_video_interpolate( process_status_q, video_path, file_number, selected_output_path, AI_instance, selected_AI_model, frame_gen_factor, slowmotion, selected_image_extension, selected_video_extension, selected_video_codec, input_resize_factor, output_resize_factor, cpu_number, selected_keep_frames): + # Step 1. Setup output dirs target_directory = prepare_output_video_directory_name( video_path, selected_output_path, selected_AI_model, frame_gen_factor, slowmotion, input_resize_factor, output_resize_factor) video_output_path = prepare_output_video_filename( video_path, selected_output_path, selected_AI_model, frame_gen_factor, slowmotion, input_resize_factor, output_resize_factor, selected_video_extension) + # Step 2. Extract video frames write_process_status( process_status_q, f"{file_number}. Extracting video frames") + + # --- CAMBIO CRÍTICO: Forzar .png para extracción temporal --- + temp_extraction_ext = ".png" + extracted_frames_paths = extract_video_frames( - process_status_q, file_number, target_directory, AI_instance, video_path, cpu_number, selected_image_extension) - # Step 3. Prepare output/gen frame names + process_status_q, file_number, target_directory, AI_instance, video_path, cpu_number, temp_extraction_ext) + + # Step 3. Prepare output/gen frame names (asegurar que usan png) total_frames_paths = prepare_output_video_frame_filenames( - extracted_frames_paths, selected_AI_model, frame_gen_factor, selected_image_extension) - only_generated_frames_paths = prepare_output_video_frame_to_generate_filenames( - extracted_frames_paths, selected_AI_model, frame_gen_factor, selected_image_extension) - # Step 4. Interpolated frames generation (calls AI orchestration) + extracted_frames_paths, selected_AI_model, frame_gen_factor, temp_extraction_ext) + + # Step 4. Interpolated frames generation write_process_status( process_status_q, f"{file_number}. Video frame generation") global global_processing_times_list global_processing_times_list = [] + for frame_index in range(len(extracted_frames_paths)-1): frame_1_path = extracted_frames_paths[frame_index] frame_2_path = extracted_frames_paths[frame_index+1] frame_1 = image_read(frame_1_path) frame_2 = image_read(frame_2_path) + start_timer = timer() generated_frames = AI_instance.AI_orchestration(frame_1, frame_2) - # Save generated frames + + # Save generated frames (usando la misma extensión temporal .png) generated_frames_paths = prepare_generated_frames_paths( - os_path_splitext(frame_1_path)[0], selected_AI_model, selected_image_extension, frame_gen_factor) + os_path_splitext(frame_1_path)[0], selected_AI_model, temp_extraction_ext, frame_gen_factor) + for i, gen_frame in enumerate(generated_frames): image_write(generated_frames_paths[i], gen_frame) + end_timer = timer() - processing_time = end_timer - start_timer - global_processing_times_list.append(processing_time) - # Step 5. Save/copy/cleanup - cleanup handled at end of process + global_processing_times_list.append(end_timer - start_timer) + # Step 6. Video encoding write_process_status( process_status_q, f"{file_number}. Encoding frame-generated video") - video_encoding( - process_status_q, video_path, video_output_path, total_frames_paths, selected_video_codec) - # Step 7. Cleanup after video interpolation processing + # --- CAMBIO CRÍTICO: Calcular multiplicador de FPS para FFmpeg --- + fps_multiplier = 1 if slowmotion else frame_gen_factor + + video_encoding( + process_status_q, video_path, video_output_path, total_frames_paths, selected_video_codec, fps_multiplier) + + # Step 7. Cleanup if not selected_keep_frames and os_path_exists(target_directory): try: remove_directory(target_directory) @@ -3807,21 +4169,21 @@ def fluidframes_video_interpolate( def upscale_orchestrator( - process_status_q: multiprocessing_Queue, - selected_file_list: list, - selected_output_path: str, - selected_AI_model: str, - selected_AI_multithreading: int, - input_resize_factor: int, - output_resize_factor: int, - selected_gpu: str, - tiles_resolution: int, - selected_blending_factor: float, - selected_keep_frames: bool, - selected_image_extension: str, - selected_video_extension: str, - selected_video_codec: str, - cpu_number: int, + process_status_q: multiprocessing_Queue, + selected_file_list: list, + selected_output_path: str, + selected_AI_model: str, + selected_AI_multithreading: int, + input_resize_factor: int, + output_resize_factor: int, + selected_gpu: str, + tiles_resolution: int, + selected_blending_factor: float, + selected_keep_frames: bool, + selected_image_extension: str, + selected_video_extension: str, + selected_video_codec: str, + cpu_number: int, ) -> None: global global_status_lock @@ -3830,30 +4192,40 @@ def upscale_orchestrator( try: write_process_status(process_status_q, f"Loading AI model") - # Check if the selected model is a face restoration model + # Instanciar modelos if selected_AI_model in Face_restoration_models_list: AI_upscale_instance_list = [ - AI_face_restoration(selected_AI_model, selected_gpu, - input_resize_factor, output_resize_factor, tiles_resolution) + AI_face_restoration( + selected_AI_model, + selected_gpu, + input_resize_factor, + output_resize_factor, + tiles_resolution + ) for _ in range(selected_AI_multithreading) ] else: AI_upscale_instance_list = [ - AI_upscale(selected_AI_model, selected_gpu, - input_resize_factor, output_resize_factor, tiles_resolution) + AI_upscale( + selected_AI_model, + selected_gpu, + input_resize_factor, + output_resize_factor, + tiles_resolution + ) for _ in range(selected_AI_multithreading) ] how_many_files = len(selected_file_list) for file_number in range(how_many_files): file_path = selected_file_list[file_number] - file_number = file_number + 1 + display_number = file_number + 1 # Fix index for display if check_if_file_is_video(file_path): upscale_video( process_status_q, file_path, - file_number, + display_number, selected_output_path, AI_upscale_instance_list, selected_AI_model, @@ -3870,7 +4242,7 @@ def upscale_orchestrator( upscale_image( process_status_q, file_path, - file_number, + display_number, selected_output_path, AI_upscale_instance_list[0], selected_AI_model, @@ -3885,36 +4257,51 @@ def upscale_orchestrator( except Exception as exception: error_message = str(exception) + # Enviamos el error a la consola visual + write_process_status( + process_status_q, f"[LOG] [ERROR] Detalle técnico: {error_message}") + if "cannot convert float NaN to integer" in error_message: + friendly_msg = "Timeout del Driver de GPU. Intenta reiniciar sin borrar los frames." write_process_status( - process_status_q, - f"{ERROR_STATUS}An error occurred during video upscaling, likely due to a GPU driver timeout.\n" - "Restart the process without deleting the upscaled frames to resume and complete the upscaling." - ) + process_status_q, f"{ERROR_STATUS}{friendly_msg}") + elif "memory" in error_message.lower(): + # Error específico de memoria + write_process_status( + process_status_q, f"{ERROR_STATUS}Memoria VRAM insuficiente. Baja la resolución de 'Tiles' o el 'Input %'.") else: - log_and_report_error(error_message) write_process_status( - process_status_q, f"{ERROR_STATUS} {error_message}") + process_status_q, f"{ERROR_STATUS}{error_message}") + + # Mantener impresión en consola terminal por si acaso + print(f"[ORCHESTRATOR ERROR] {error_message}") # ==== IMAGE PROCESSING SECTION ==== def upscale_image( - process_status_q: multiprocessing_Queue, - image_path: str, - file_number: int, - selected_output_path: str, - AI_instance: AI_upscale, - selected_AI_model: str, - selected_image_extension: str, - input_resize_factor: int, - output_resize_factor: int, - selected_blending_factor: float + process_status_q: multiprocessing_Queue, + image_path: str, + file_number: int, + selected_output_path: str, + AI_instance: AI_upscale, + selected_AI_model: str, + selected_image_extension: str, + input_resize_factor: int, + output_resize_factor: int, + selected_blending_factor: float ) -> None: starting_image = image_read(image_path) upscaled_image_path = prepare_output_image_filename( - image_path, selected_output_path, selected_AI_model, input_resize_factor, output_resize_factor, selected_image_extension, selected_blending_factor) + image_path, + selected_output_path, + selected_AI_model, + input_resize_factor, + output_resize_factor, + selected_image_extension, + selected_blending_factor + ) write_process_status( process_status_q, f"{file_number}. Enchanting your image. Be patient...") @@ -3929,8 +4316,11 @@ def upscale_image( selected_image_extension ) else: - image_write(upscaled_image_path, upscaled_image, - selected_image_extension) + image_write( + upscaled_image_path, + upscaled_image, + selected_image_extension + ) copy_file_metadata(image_path, upscaled_image_path) @@ -4009,9 +4399,13 @@ def upscale_video( upscaled_frame_paths_to_save: list[str], selected_blending_factor: float ) -> None: - nonlocal writer_threads # Access the outer scope variable + nonlocal writer_threads # Accedemos a la lista de hilos externa + + # --- CORRECCIÓN: Limpieza de hilos muertos --- + # Antes de crear uno nuevo, eliminamos de la lista los que ya terminaron + writer_threads = [t for t in writer_threads if t.is_alive()] + # --------------------------------------------- - # Fix 2.1: Track writer threads to ensure all frames are written before encoding t = Thread( target=save_multiple_upscaled_frame_async, args=( @@ -4260,7 +4654,7 @@ def upscale_video( write_process_status( process_status_q, f"{file_number}. Extracting video frames") extracted_frames_paths = extract_video_frames( - process_status_q, file_number, target_directory, AI_upscale_instance_list[0], video_path, cpu_number, ".jpg") + process_status_q, file_number, target_directory, AI_upscale_instance_list[0], video_path, cpu_number, ".png") upscaled_frame_paths = [prepare_output_video_frame_filename( frame_path, selected_AI_model, input_resize_factor, output_resize_factor, selected_blending_factor) for frame_path in extracted_frames_paths] @@ -4289,8 +4683,11 @@ def upscale_video( # 6. Video encoding write_process_status( process_status_q, f"{file_number}. Encoding upscaled video") + + # --- CORRECCIÓN: Argumento fps_multiplier --- + # El upscaling no cambia FPS, por lo tanto multiplier es 1 video_encoding(process_status_q, video_path, video_output_path, - upscaled_frame_paths, selected_video_codec) + upscaled_frame_paths, selected_video_codec, fps_multiplier=1) # 7. Delete frames folder if not selected_keep_frames: @@ -4346,7 +4743,7 @@ def user_input_checks() -> bool: info_message.set("Please select a file") return False - if len(selected_file_list) <= 0: + if not selected_file_list or len(selected_file_list) <= 0: info_message.set("Please select a file") return False @@ -4370,48 +4767,40 @@ def user_input_checks() -> bool: info_message.set("Please select the AI model") return False - # Input resize factor + # --- FIX: STRICT NORMALIZATION OF RESIZE FACTORS --- try: - input_resize_factor = int( - float(str(selected_input_resize_factor.get()))) + # Obtener valor crudo (ej: "50" o "100") + raw_input = float(str(selected_input_resize_factor.get())) + # Convertir estrictamente a factor (ej: 0.5 o 1.0) + input_resize_factor = raw_input / 100.0 + + if input_resize_factor <= 0: + raise ValueError("Value must be > 0") except (ValueError, TypeError): - info_message.set("Input resolution % must be a number") + info_message.set("Input resolution % must be a valid number > 0") return False - if input_resize_factor > 0: - input_resize_factor = input_resize_factor/100 - else: - info_message.set("Input resolution % must be a value > 0") - return False - - # Output resize factor try: - output_resize_factor = int( - float(str(selected_output_resize_factor.get()))) + raw_output = float(str(selected_output_resize_factor.get())) + output_resize_factor = raw_output / 100.0 + + if output_resize_factor <= 0: + raise ValueError("Value must be > 0") except (ValueError, TypeError): - info_message.set("Output resolution % must be a number") + info_message.set("Output resolution % must be a valid number > 0") return False + # --------------------------------------------------- - if output_resize_factor > 0: - output_resize_factor = output_resize_factor/100 - else: - info_message.set("Output resolution % must be a value > 0") - return False - -# VRAM limiter + # VRAM limiter try: - vram_gb = int(float(str(selected_VRAM_limiter.get()))) + vram_gb = float(str(selected_VRAM_limiter.get())) if vram_gb <= 0: info_message.set("GPU VRAM value must be a value > 0") return False - vram_multiplier = VRAM_model_usage.get(selected_AI_model) - if vram_multiplier is None: - vram_multiplier = 1 # Default for interpolation models or unknowns + vram_multiplier = VRAM_model_usage.get(selected_AI_model, 1.0) - # El cálculo original parece confuso. Esta es una interpretación más clara: - # Se asume que el VRAM Limiter es la VRAM en GB y se multiplica por un factor y 100. - # Si el modelo 'RealESR_Gx4' (factor 2.2) y VRAM es 4GB, tiles_resolution sería ~880. + # Cálculo corregido: VRAM (GB) * Multiplicador * 100 (Base tile size) selected_vram_factor = vram_multiplier * vram_gb tiles_resolution = int(selected_vram_factor * 100) @@ -4457,14 +4846,26 @@ def get_upscale_factor() -> int: return upscale_factor -def open_files_action(): - +def open_files_action(files=None): + # Función auxiliar interna def check_supported_selected_files(uploaded_file_list: list) -> list: return [file for file in uploaded_file_list if any(supported_extension in file for supported_extension in supported_file_extensions)] - info_message.set("Selecting files") + info_message.set("Processing files...") + + # --- CORRECCIÓN AQUÍ --- + if files: + # Caso A: Viene de Drag & Drop + # El módulo drag_drop.py YA nos envía la lista limpia (es una tupla), + # así que solo la convertimos a lista y listo. + uploaded_files_list = list(files) + else: + # Caso B: Viene del Botón (files es None) + # Abrimos el explorador de archivos manualmente + info_message.set("Selecting files") + uploaded_files_list = list(filedialog.askopenfilenames()) + # ----------------------- - uploaded_files_list = list(filedialog.askopenfilenames()) uploaded_files_counter = len(uploaded_files_list) supported_files_list = check_supported_selected_files(uploaded_files_list) @@ -4490,7 +4891,10 @@ def open_files_action(): file_widget.place(relx=0.0, rely=0.0, relwidth=0.5, relheight=1.0) info_message.set("Ready to be being enchanted!") else: - info_message.set("Not supported files :(") + if uploaded_files_counter > 0: + info_message.set("Not supported files :(") + else: + info_message.set("No files selected") def open_output_path_action(): @@ -4500,9 +4904,9 @@ def open_output_path_action(): else: selected_output_path.set(asked_selected_output_path) - # ==== GUI MENU SELECTION SECTION ==== + def select_AI_from_menu(selected_option: str) -> None: global selected_AI_model global selected_blending_factor # <-- AÑADIR @@ -4538,7 +4942,7 @@ def select_AI_from_menu(selected_option: str) -> None: place_video_codec_keep_frames_menus() place_image_video_output_menus() place_output_path_textbox() - place_message_label() + place_integrated_console() place_upscale_button() @@ -4632,10 +5036,11 @@ def place_dynamic_rife_interpolator(): def place_loadFile_section(): + # Crear el frame de fondo para la sección de carga background = CTkFrame( master=window, fg_color=background_color, corner_radius=1) - # --> ESTE TEXTO HA SIDO ACTUALIZADO + # Texto informativo sobre formatos soportados text_drop = (" SUPPORTED FILES \n\n " + "IMAGES • jpg, jpeg, png, bmp, tiff, tif, webp \n " + "VIDEOS • mp4, avi, mkv, mov, wmv, flv, webm ") @@ -4653,11 +5058,12 @@ def place_loadFile_section(): corner_radius=10 ) + # Botón para seleccionar archivos manualmente input_file_button = CTkButton( master=window, command=open_files_action, - text="SELECT FILES", - width=140, + text="Select Files or Drag & Drop", + width=150, height=30, font=bold12, border_width=1, @@ -4668,10 +5074,20 @@ def place_loadFile_section(): hover_color=button_hover_color ) + # Colocar elementos en la interfaz background.place(relx=0.0, rely=0.0, relwidth=0.5, relheight=1.0) input_file_text.place(relx=0.25, rely=0.4, anchor="center") input_file_button.place(relx=0.25, rely=0.5, anchor="center") + # --- CORRECCIÓN DRAG & DROP --- + # Registramos 'background', 'input_file_button' y 'input_file_text' + # para maximizar el área de detección de archivos. + enable_drag_and_drop( + window, + [background, input_file_button, input_file_text], + open_files_action + ) + def place_app_name(): background = CTkFrame( @@ -5194,29 +5610,49 @@ def place_output_path_textbox(): active_button = create_active_button( command=open_output_path_action, text="SELECT", width=60, height=25) - background.place(relx=0.75, rely=row10, +# --- CAMBIO: Usamos 'row8' en lugar de 'row10' para subirlo --- + background.place(relx=0.75, rely=row8, relwidth=0.48, anchor="center") - info_button.place(relx=column_info1, rely=row10 - + info_button.place(relx=column_info1, rely=row8 - 0.003, anchor="center") active_button.place(relx=column_info1 + 0.052, - rely=row10, anchor="center") - option_menu.place(relx=column_2 - 0.008, rely=row10, + rely=row8, anchor="center") + option_menu.place(relx=column_2 - 0.008, rely=row8, anchor="center") -def place_message_label(): - message_label = CTkLabel( +def place_integrated_console(): + """ + Crea y coloca la consola integrada, desplazada hacia abajo + para dejar espacio a los botones superiores. + """ + + # 1. Crear el widget + console_widget = IntegratedConsole( master=window, - textvariable=info_message, - height=26, - width=200, - font=bold11, - fg_color=accent_color, - text_color=background_color, - anchor="center", - corner_radius=1 + fg_color=widget_background_color, + border_color=accent_color, + border_width=1, + corner_radius=5 ) - message_label.place(relx=0.83, rely=0.9495, anchor="center") + + # 2. Posicionamiento + # rely=0.89: Bajamos la consola al fondo. + # relheight=0.18: Altura ajustada para no salirse de la pantalla. + console_widget.place( + relx=0.75, + rely=0.89, + relwidth=0.48, + relheight=0.18, + anchor="center" + ) + + # 3. Conexión lógica + console.set_widget(console_widget) + + # Mensaje inicial + console.write_log( + f"[{app_name}] System initialized. Console ready.", "SUCCESS") def place_stop_button(): @@ -5224,11 +5660,12 @@ def place_stop_button(): command=stop_button_command, text="STOP", icon=stop_icon, - width=140, - height=30, + width=240, # Mismo ancho que el botón de inicio + height=28, # Altura delgada (antes 30 o 45) border_color=error_color ) - stop_button.place(relx=0.75 - 0.1, rely=0.95, anchor="center") + # Posición: rely=0.77 lo pone ARRIBA de la consola + stop_button.place(relx=0.75, rely=0.77, anchor="center") def place_upscale_button(): @@ -5236,10 +5673,12 @@ def place_upscale_button(): command=upscale_button_command, text="Make Magic", icon=upscale_icon, - width=140, - height=30 + width=240, # Mismo ancho que Stop + height=28 # Altura delgada (antes 45) ) - upscale_button.place(relx=0.75 - 0.1, rely=0.95, anchor="center") + + # Posición: Exactamente la misma que el botón de Stop + upscale_button.place(relx=0.75, rely=0.77, anchor="center") # ==== MAIN APPLICATION SECTION ==== @@ -5301,20 +5740,24 @@ class App(): self.toplevel_window = None window.protocol("WM_DELETE_WINDOW", on_app_close) - window.title('') + window.title(f"Warlock-Studio") # Get screen width and height screen_width = window.winfo_screenwidth() screen_height = window.winfo_screenheight() - # Set to 80% of the screen by default, centered + + # --- CAMBIO REALIZADO AQUÍ (Aumentado a 85% para ser más grande) --- + # Set to 85% of the screen by default, centered default_width = int(screen_width * 0.8) - default_height = int(screen_height * 0.8) + default_height = int(screen_height * 0.87) + # ------------------------------------------------------------------ + x_position = (screen_width - default_width) // 2 y_position = (screen_height - default_height) // 2 window.geometry( f"{default_width}x{default_height}+{x_position}+{y_position}") # --> AQUÍ ESTÁ EL CAMBIO - window.resizable(False, False) + window.resizable(True, True) window.iconbitmap(find_by_relative_path( "Assets" + os_separator + "logo.ico")) @@ -5340,19 +5783,19 @@ class App(): place_image_video_output_menus() - place_message_label() + place_integrated_console() place_upscale_button() -# Splash Screen class for application startup - class SplashScreen(CTkToplevel): def __init__(self): super().__init__() # Configure window - self.title("") - self.overrideredirect(True) # Remove window decorations + 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() @@ -5362,8 +5805,8 @@ class SplashScreen(CTkToplevel): self.geometry(f"{default_width}x{default_height}") # Set default window size - window_width = 500 - window_height = 300 + window_width = 460 + window_height = 340 # Try to load banner image banner_path = find_by_relative_path(f"Assets{os_separator}banner.png") @@ -5376,7 +5819,7 @@ class SplashScreen(CTkToplevel): except Exception as e: print(f"[SPLASH] Could not load splash banner: {e}") has_banner = False - window_height = 200 # Smaller height if no banner + window_height = 400 # Smaller height if no banner # Center window x = (screen_width - window_width) // 2 @@ -5399,7 +5842,7 @@ class SplashScreen(CTkToplevel): # Fallback to text title if image not found title_label = CTkLabel( self, - text="Warlock Studio", + text="Warlock-Studio", font=CTkFont(family="Segoe UI", size=28, weight="bold"), text_color=app_name_color # Usar color del nombre de la app ) @@ -5421,22 +5864,10 @@ class SplashScreen(CTkToplevel): ) self.status_label.pack(pady=10, padx=10) - # Create progress bar - self.progress_bar = CTkProgressBar( - status_frame, - width=400, - height=10, - progress_color=accent_color, # Usar color amarillo dorado - fg_color=border_color, # Usar color de borde - border_width=1 - ) - self.progress_bar.pack(pady=(0, 10), padx=10) - self.progress_bar.set(0) # Start at 0% - # Create version label version_label = CTkLabel( self, - text=f"Version {version}", + 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 ) @@ -5481,39 +5912,85 @@ class SplashScreen(CTkToplevel): self.destroy() +def log_startup_info(): + """ + Imprime la información de inicio una vez que la consola gráfica está activa. + """ + # 1. Check FFmpeg + if os_path_exists(FFMPEG_EXE_PATH): + print(f"[{app_name}] ffmpeg.exe found") + else: + print( + f"[{app_name}] WARNING: ffmpeg.exe not found. Video functionality will be limited.") + + # 2. Check Preferences + if os_path_exists(USER_PREFERENCE_PATH): + print(f"[{app_name}] Preference file exists") + else: + print( + f"[{app_name}] Preference file does not exist, using default coded value") + + # 3. Check ONNX Providers + try: + from onnxruntime import get_available_providers + providers = get_available_providers() + print("Available ONNX Runtime Providers:") + for p in providers: + print(f"- {p}") + except ImportError as e: + print(f"Error: The onnxruntime library is not installed. {e}") + +# --- CÓDIGO PARA EL BOTÓN DEL MANUAL (Pegar antes de if __name__ == "__main__":) --- + + +def open_manual_action(): + """Función para abrir el PDF del manual.""" + manual_path = find_by_relative_path( + f"Assets{os_separator}Warlock-Studio_Manual.pdf") + + if os_path_exists(manual_path): + try: + # Abre el PDF con el visor predeterminado del sistema + if os.name == 'nt': # Windows + os.startfile(manual_path) + else: # macOS / Linux + subprocess_run(['open' if sys.platform == + 'darwin' else 'xdg-open', manual_path]) + console.write_log("Manual opened successfully", "SUCCESS") + except Exception as e: + log_and_report_error(f"Could not open manual: {e}") + else: + show_error_message("Manual file not found in Assets folder.") + + +class ManualButton(CTkButton): + """Clase para el botón de ayuda/manual.""" + + def __init__(self, master, **kwargs): + # Intenta buscar el icono 'manual_icon' en las variables globales + # Si no existe, usa texto + icon_img = globals().get('manual_icon', None) + text_val = "" if icon_img else "📖 Help" + + super().__init__(master, text=text_val, image=icon_img, width=40, height=28, + fg_color=widget_background_color, border_color=border_color, border_width=1, + hover_color=button_hover_color, text_color=text_color, + command=open_manual_action, **kwargs) + +# ----------------------------------------------------------------------------------- + + if __name__ == "__main__": - multiprocessing_freeze_support() + freeze_support() + from warlock_preferences import ConfigManager, PreferencesButton set_appearance_mode("Dark") - # --- Start of added and modified code --- - - # Define the function to get and print ONNX providers - def show_providers(): - """Gets and prints the available ONNX Runtime providers to the console.""" - try: - from onnxruntime import get_available_providers - providers = get_available_providers() - print("Available ONNX Runtime Providers:") - for p in providers: - print(f"- {p}") - except ImportError as e: - print(f"Error: The onnxruntime library is not installed. {e}") - - # Call the function to display the providers at startup - show_providers() - - # --- End of added and modified code --- - - # Create custom theme + # Configurar tema visual import customtkinter - from customtkinter import set_default_color_theme + customtkinter.set_default_color_theme("dark-blue") - # Configure custom theme with defined colors - customtkinter.set_default_color_theme("dark-blue") # Base theme - - # Override some global CustomTkinter colors + # Aplicar overrides de tema para consistencia visual try: - # Apply custom colors globally customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] = [ widget_background_color, widget_background_color] customtkinter.ThemeManager.theme["CTkButton"]["fg_color"] = [ @@ -5543,37 +6020,27 @@ if __name__ == "__main__": process_status_q = multiprocessing_Queue(maxsize=1) - # Create main window but keep it hidden initially - window = CTk() - window.withdraw() # Hide main window temporarily + # Inicializar ventana principal (oculta para mostrar el splash primero) + window = DnDCTk() + window.withdraw() - # Create and show splash screen + # 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) - # Schedule showing the main window after splash finishes - window.after(11000, window.deiconify) # 10s + fade time - + # Inicialización de Variables de UI info_message = StringVar() selected_output_path = StringVar() selected_input_resize_factor = StringVar() selected_output_resize_factor = StringVar() selected_VRAM_limiter = StringVar() - global selected_file_list - global selected_AI_model - global selected_gpu - global selected_keep_frames - global selected_AI_multithreading - global selected_image_extension - global selected_video_extension - global selected_video_codec - global selected_blending_factor - global selected_frame_generation_option - global tiles_resolution - global input_resize_factor - + # Inicializar variables globales seleccionadas con los defaults cargados selected_file_list = [] - selected_AI_model = default_AI_model selected_gpu = default_gpu selected_image_extension = default_image_extension @@ -5591,34 +6058,29 @@ if __name__ == "__main__": selected_keep_frames = False selected_blending_factor = {"OFF": 0, "Low": 0.3, - "Medium": 0.5, "High": 0.7}.get(default_blending) - - selected_frame_generation_option = "OFF" # Initialize frame generation option - - # Initialize global variables that are used in video processing - global stop_thread_flag - global global_processing_times_list - global global_upscaled_frames_paths - global global_can_i_update_status - global output_resize_factor - global tiles_resolution + "Medium": 0.5, "High": 0.7}.get(default_blending, 0) + selected_frame_generation_option = "OFF" + # Inicializar variables de control global stop_thread_flag = Event() global_processing_times_list = [] global_upscaled_frames_paths = [] global_can_i_update_status = False output_resize_factor = 1.0 - tiles_resolution = 800 # Default value + tiles_resolution = 800 + # Asignar valores por defecto a los campos de texto selected_input_resize_factor.set(default_input_resize_factor) selected_output_resize_factor.set(default_output_resize_factor) selected_VRAM_limiter.set(default_VRAM_limiter) selected_output_path.set(default_output_path) - info_message.set("Ready for the wonderful show!") + info_message.set("Ready for the show!") + # Añadir listeners para actualizar widgets cuando cambien los valores selected_input_resize_factor.trace_add('write', update_file_widget) selected_output_resize_factor.trace_add('write', update_file_widget) + # Definición de Fuentes e Iconos font = "Consola" bold8 = CTkFont(family=font, size=8, weight="bold") bold9 = CTkFont(family=font, size=9, weight="bold") @@ -5637,6 +6099,7 @@ if __name__ == "__main__": bold23 = CTkFont(family=font, size=23, weight="bold") bold24 = CTkFont(family=font, size=24, weight="bold") +# Cargar Iconos stop_icon = CTkImage(pillow_image_open(find_by_relative_path( f"Assets{os_separator}stop_icon.png")), size=(15, 15)) upscale_icon = CTkImage(pillow_image_open(find_by_relative_path( @@ -5646,6 +6109,36 @@ if __name__ == "__main__": info_icon = CTkImage(pillow_image_open(find_by_relative_path( f"Assets{os_separator}info_icon.png")), size=(18, 18)) + # --- NUEVO: Carga del icono del manual con seguridad --- + try: + manual_icon = CTkImage(pillow_image_open(find_by_relative_path( + f"Assets{os_separator}manual_icon.png")), size=(20, 20)) + except: + manual_icon = None # Fallback si no existe la imagen + # ------------------------------------------------------- + + # Inicializar la Aplicación Principal app = App(window) window.update() + + # Inicializar Botón de Preferencias (AQUÍ SE CONECTA LA CONSOLA Y CONFIGURACIÓN) + from warlock_preferences import PreferencesButton + preferences_btn = PreferencesButton( + master=window, + current_version=version, + repo_owner="Ivan-Ayub97", + repo_name="Warlock-Studio" + ) + # Posición en la esquina superior derecha + preferences_btn.place(relx=0.95, rely=0.05, anchor="center") + preferences_btn.lift() + + # --- NUEVO: Botón del Manual --- + # Colocado a la izquierda del engranaje (relx=0.88) + manual_btn = ManualButton(master=window) + manual_btn.place(relx=0.88, rely=0.05, anchor="center") + manual_btn.lift() + # ------------------------------- + + # Iniciar Bucle Principal window.mainloop() diff --git a/Warlock-Studio.spec b/Warlock-Studio.spec new file mode 100644 index 0000000..5d1f0fe --- /dev/null +++ b/Warlock-Studio.spec @@ -0,0 +1,73 @@ +# -*- mode: python ; coding: utf-8 -*- + +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'], + pathex=[], + binaries=[], + datas=[ + ('AI-onnx', 'AI-onnx'), + ('Assets', 'Assets') + ], + hiddenimports=[ + 'onnxruntime.capi._pybind_state', + 'onnxruntime.providers', + 'moviepy.editor' + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[ + # Frameworks de Machine Learning pesados + 'torch', 'torchaudio', 'torchvision', 'transformers', 'accelerate', 'diffusers', + 'lightning', 'pytorch-lightning', + + # Librerías de Ciencia de Datos y Gráficos + 'matplotlib', 'pandas', 'scipy', 'scikit-learn', 'scikit-image', + + # Otros Toolkits de GUI + 'PyQt5', 'PyQt5-Qt5', 'PyQt5_sip', 'dearpygui', 'QtAwesome', 'QtPy', + + # Herramientas de desarrollo y testing + 'pytest', 'unittest', 'poetry', 'virtualenv', + + # Compiladores y librerías de bajo nivel que no se usan + 'numba', 'llvmlite', + + # Otros paquetes grandes no relacionados + 'pygame', 'PyMuPDF', 'aiohttp', 'httpx', 'GitPython', 'musicbrainzngs', 'mido', 'rtmidi', 'simpleaudio', 'vulkan', 'vgamepad' + ], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='Warlock-Studio', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + icon='logo.ico', +) + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='Warlock-Studio', +) diff --git a/console.py b/console.py new file mode 100644 index 0000000..4dbdaaf --- /dev/null +++ b/console.py @@ -0,0 +1,310 @@ +import sys +import threading +import tkinter as tk +from datetime import datetime +from tkinter import filedialog + +import customtkinter as ctk + + +class ConsoleColors: + """Definición de paleta de colores para los logs.""" + TEXT_DEFAULT = "#E0E0E0" + TIMESTAMP = "#808080" + DEBUG = "#A0A0A0" + INFO = "#61AFEF" # Azul claro + SUCCESS = "#98C379" # Verde pastel + WARNING = "#E5C07B" # Amarillo/Naranja + ERROR = "#E06C75" # Rojo suave + CRITICAL = "#FF0000" # Rojo intenso + HIGHLIGHT = "#D19A66" # Color para resaltar búsquedas + + +class IntegratedConsole(ctk.CTkFrame): + """ + Un widget de consola completo con barra de herramientas, + búsqueda y visualización de logs coloreados. + """ + + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + + # Configuración del Grid + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=1) # El textbox se expande + + # --- 1. BARRA DE HERRAMIENTAS (Toolbar) --- + self.toolbar = ctk.CTkFrame(self, height=30, fg_color="transparent") + self.toolbar.grid(row=0, column=0, sticky="ew", padx=2, pady=(2, 5)) + + # Botón Limpiar + self.btn_clear = ctk.CTkButton( + self.toolbar, text="🗑 Clear", width=60, height=24, + font=("Segoe UI", 11), fg_color="#3E3E3E", hover_color="#505050", + command=self.clear_console + ) + self.btn_clear.pack(side="left", padx=2) + + # Botón Copiar + self.btn_copy = ctk.CTkButton( + self.toolbar, text="📋 Copy", width=60, height=24, + font=("Segoe UI", 11), fg_color="#3E3E3E", hover_color="#505050", + command=self.copy_all + ) + self.btn_copy.pack(side="left", padx=2) + + # Botón Guardar + self.btn_save = ctk.CTkButton( + self.toolbar, text="💾 Save", width=60, height=24, + font=("Segoe UI", 11), fg_color="#3E3E3E", hover_color="#505050", + command=self.save_to_file + ) + self.btn_save.pack(side="left", padx=2) + + # Separador visual (Spacer) + ctk.CTkLabel(self.toolbar, text="|", text_color="gray").pack( + side="left", padx=5) + + # Checkbox Auto-Scroll + self.auto_scroll_var = ctk.BooleanVar(value=True) + self.chk_autoscroll = ctk.CTkCheckBox( + self.toolbar, text="Auto-scroll", variable=self.auto_scroll_var, + font=("Segoe UI", 11), width=80, height=20, checkbox_width=18, checkbox_height=18 + ) + self.chk_autoscroll.pack(side="left", padx=5) + + # Barra de Búsqueda + self.entry_search = ctk.CTkEntry( + self.toolbar, placeholder_text="🔍 Search...", width=120, height=24, + font=("Segoe UI", 11) + ) + self.entry_search.pack(side="right", padx=2) + self.entry_search.bind("", self.on_search) + + # --- 2. ÁREA DE TEXTO (Logs) --- + self.textbox = ctk.CTkTextbox( + self, + font=("Consolas", 13), # Fuente monoespaciada para mejor lectura + fg_color="#1E1E1E", # Fondo oscuro tipo editor + text_color=ConsoleColors.TEXT_DEFAULT, + wrap="word", + state="disabled" + ) + self.textbox.grid(row=1, column=0, sticky="nsew", padx=2, pady=2) + + # Configuración de Tags (Colores) + self.textbox.tag_config( + "TIMESTAMP", foreground=ConsoleColors.TIMESTAMP) + self.textbox.tag_config("DEBUG", foreground=ConsoleColors.DEBUG) + self.textbox.tag_config("INFO", foreground=ConsoleColors.INFO) + self.textbox.tag_config("SUCCESS", foreground=ConsoleColors.SUCCESS) + self.textbox.tag_config("WARNING", foreground=ConsoleColors.WARNING) + self.textbox.tag_config("ERROR", foreground=ConsoleColors.ERROR) + self.textbox.tag_config("CRITICAL", foreground=ConsoleColors.CRITICAL) + self.textbox.tag_config( + "SEARCH_HIGHLIGHT", background="#444400", foreground="#FFFFFF") + + # Menú contextual (Click derecho) + self.context_menu = tk.Menu(self, tearoff=0, bg="#2b2b2b", fg="white") + self.context_menu.add_command(label="Copy All", command=self.copy_all) + self.context_menu.add_command( + label="Clear Console", command=self.clear_console) + self.textbox.bind("", self.show_context_menu) + + def write_log(self, text, level="INFO"): + """Método principal para escribir en la consola.""" + try: + self.textbox.configure(state="normal") + + # Timestamp + timestamp = datetime.now().strftime("[%H:%M:%S]") + self.textbox.insert("end", f"{timestamp} ", "TIMESTAMP") + + # Etiqueta de Nivel (opcional, visualmente ayuda) + self.textbox.insert("end", f"[{level.upper()}] ", level.upper()) + + # Mensaje + # Aseguramos que termine en nueva línea + if not text.endswith("\n"): + text += "\n" + + self.textbox.insert("end", text, level.upper()) + + # Auto-scroll + if self.auto_scroll_var.get(): + self.textbox.see("end") + + self.textbox.configure(state="disabled") + except Exception as e: + print(f"Error writing to console widget: {e}") + + # --- Funcionalidades de la Barra de Herramientas --- + + def clear_console(self): + self.textbox.configure(state="normal") + self.textbox.delete("0.0", "end") + self.textbox.configure(state="disabled") + + def copy_all(self): + try: + all_text = self.textbox.get("0.0", "end") + self.clipboard_clear() + self.clipboard_append(all_text) + self.update() # Necesario para finalizar la operación de portapapeles + except Exception: + pass + + def save_to_file(self): + try: + filename = filedialog.asksaveasfilename( + defaultextension=".txt", + filetypes=[("Text Files", "*.txt"), + ("Log Files", "*.log"), ("All Files", "*.*")], + title="Save Log File" + ) + if filename: + with open(filename, "w", encoding="utf-8") as f: + f.write(self.textbox.get("0.0", "end")) + except Exception as e: + self.write_log(f"Error saving file: {e}", "ERROR") + + def on_search(self, event=None): + """Resalta texto en tiempo real.""" + search_str = self.entry_search.get() + + # Limpiar tags anteriores de búsqueda + self.textbox.tag_remove("SEARCH_HIGHLIGHT", "1.0", "end") + + if not search_str: + return + + # Buscar y resaltar + start_pos = "1.0" + while True: + start_pos = self.textbox.search( + search_str, start_pos, stopindex="end", nocase=True) + if not start_pos: + break + end_pos = f"{start_pos}+{len(search_str)}c" + self.textbox.tag_add("SEARCH_HIGHLIGHT", start_pos, end_pos) + start_pos = end_pos + + def show_context_menu(self, event): + try: + self.context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + + +class StreamRedirector: + """Redirige stdout/stderr hacia el ConsoleManager.""" + + def __init__(self, console_manager, stream_type): + self.console_manager = console_manager + self.stream_type = stream_type + self.original_stream = getattr(sys, stream_type) + + def write(self, message): + if message.strip(): # Ignorar líneas vacías + self.console_manager.enqueue_message(message, self.stream_type) + + # Mantener salida en terminal real por seguridad + try: + self.original_stream.write(message) + self.original_stream.flush() + except Exception: + pass + + def flush(self): + try: + self.original_stream.flush() + except Exception: + pass + + +class ConsoleManager: + """ + Singleton que gestiona la lógica de logs y la conexión con la GUI. + Thread-safe. + """ + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(ConsoleManager, cls).__new__(cls) + cls._instance.buffer = [] + cls._instance.widget_frame = None # Referencia al Frame contenedor + cls._instance.lock = threading.Lock() + cls._instance.is_setup = False + return cls._instance + + def setup_redirection(self): + """Configura la redirección de sys.stdout y sys.stderr.""" + if not self.is_setup: + sys.stdout = StreamRedirector(self, "stdout") + sys.stderr = StreamRedirector(self, "stderr") + self.is_setup = True + print("[SYSTEM] Output redirected to Warlock Integrated Console.") + + def set_widget(self, widget_frame: IntegratedConsole): + """Vincula el widget gráfico (IntegratedConsole) y vuelca el buffer.""" + with self.lock: + self.widget_frame = widget_frame + + # Volcar buffer acumulado + for msg, stream_type in self.buffer: + self._process_and_dispatch(msg, stream_type) + self.buffer = [] + + def enqueue_message(self, message, stream_type): + """Punto de entrada desde stdout/stderr.""" + with self.lock: + if self.widget_frame: + self._process_and_dispatch(message, stream_type) + else: + self.buffer.append((message, stream_type)) + + def _process_and_dispatch(self, message, stream_type="info"): + """Determina el nivel del log y lo envía a la GUI.""" + try: + tag = "INFO" + msg_lower = message.lower() + + # Lógica de detección de nivel automática + if stream_type == "stderr": + tag = "ERROR" + elif "error" in msg_lower or "exception" in msg_lower or "failed" in msg_lower: + tag = "ERROR" + elif "warning" in msg_lower: + tag = "WARNING" + elif "success" in msg_lower or "completed" in msg_lower: + tag = "SUCCESS" + elif "debug" in msg_lower: + tag = "DEBUG" + elif "critical" in msg_lower: + tag = "CRITICAL" + + # Enviar al hilo principal + self.widget_frame.after( + 0, lambda m=message, t=tag: self.widget_frame.write_log(m, t)) + except Exception: + pass + + def write_log(self, message, tag="INFO"): + """ + Método público para logs manuales. + Uso: console.write_log("Proceso terminado", "SUCCESS") + """ + with self.lock: + if self.widget_frame: + try: + self.widget_frame.after( + 0, lambda m=message, t=tag: self.widget_frame.write_log(m, t)) + except Exception: + print(f"[{tag}] {message}") + else: + print(f"[{tag}] {message}") + + +# Instancia global +console = ConsoleManager() diff --git a/drag_drop.py b/drag_drop.py new file mode 100644 index 0000000..be48409 --- /dev/null +++ b/drag_drop.py @@ -0,0 +1,37 @@ +# drag_drop.py +from customtkinter import CTk +from tkinterdnd2 import DND_ALL, TkinterDnD + +# 1. Clase envoltorio que combina CustomTkinter con TkinterDnD + + +class DnDCTk(CTk, TkinterDnD.DnDWrapper): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.TkdndVersion = TkinterDnD._require(self) + +# 2. Función para registrar widgets y conectar tu lógica + + +def enable_drag_and_drop(window, target_widgets, callback_function): + """ + Activa Drag & Drop en los widgets especificados. + + :param window: La ventana principal (debe ser instancia de DnDCTk) + :param target_widgets: Lista de widgets (botones, labels) donde se pueden soltar archivos. + :param callback_function: La función de tu app principal que recibe la lista de archivos. + """ + + def _internal_drop_event(event): + # TkinterDnD a veces devuelve las rutas con llaves {} si tienen espacios + # window.tk.splitlist se encarga de limpiarlas correctamente + if event.data: + files = window.tk.splitlist(event.data) + # Llamamos a tu función principal pasando la lista limpia + callback_function(files) + + for widget in target_widgets: + # Registramos el widget para aceptar cualquier cosa (archivos) + widget.drop_target_register(DND_ALL) + # Conectamos el evento 'Drop' con nuestra función interna + widget.dnd_bind('<>', _internal_drop_event) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2188f23..0000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -onnxruntime>=1.18.0 -numpy>=1.24.0 - -customtkinter>=5.2.0 -Pillow>=10.0.0 - -opencv-python>=4.8.0 - -moviepy>=1.0.3 - -natsort>=8.4.0 - -tk>=0.1.0 diff --git a/rsc/Capture.png b/rsc/Capture.png index 3f6c00d..8dccb2c 100644 Binary files a/rsc/Capture.png and b/rsc/Capture.png differ diff --git a/warlock_preferences.py b/warlock_preferences.py new file mode 100644 index 0000000..a30c22b --- /dev/null +++ b/warlock_preferences.py @@ -0,0 +1,811 @@ +import ctypes +import glob +import json +import os +import platform +import shutil +import socket +import subprocess +import sys +import threading +import tkinter as tk +import webbrowser +import zipfile +from datetime import datetime +from tkinter import filedialog, messagebox +from typing import Any, Dict, List, Optional + +import customtkinter as ctk +# --- IMPORTACIONES DEL MOTOR NEO (Hardware Info) --- +import psutil +import requests +from packaging import version as pkg_version + +try: + import wmi +except ImportError: + wmi = None +try: + import GPUtil +except ImportError: + GPUtil = None + +# ----------------------------------------------------------------------------- +# CONSTANTES DE ESTILO & TEMAS +# ----------------------------------------------------------------------------- +THEME = { + "bg": "#1B1818", + "widget_bg": "#2A2727", + "card_bg": "#212121", + "text": "#FFFFFF", + "text_sec": "#CAC9C9", + "accent": "#FDEF2F", # Amarillo Dorado (Warlock) + "accent_hover": "#D4C428", + "title": "#FF3232", # Rojo Warlock + "hover": "#D41C1C", # Rojo oscuro + "border": "#E2340D", + "success": "#00E676", + "error": "#B00020", + "warning": "#FFA000", + "info_bg": "#1E3A8A", + "scroll_bg": "#181818" +} + +CONFIG_FILE = "warlock_config.json" + +# ----------------------------------------------------------------------------- +# UTILIDADES DE UI +# ----------------------------------------------------------------------------- + + +def get_font(size: int, weight: str = "normal", family: str = "Segoe UI") -> tuple: + """Genera una fuente base.""" + return (family, size, weight) + +# ----------------------------------------------------------------------------- +# GESTOR DE CONFIGURACIÓN +# ----------------------------------------------------------------------------- + + +class ConfigManager: + DEFAULT_CONFIG = { + "check_updates_on_startup": True, + "keep_window_on_top": False, + "ui_scaling": 1.0, + "font_scale": 1.0, + "window_opacity": 1.0, + "app_theme": "Dark", + "process_priority": "Normal", + "auto_close_on_finish": False, + "auto_clean_temp": False, + "notifications_enabled": True, + "last_gpu_index": "Auto" + } + + @staticmethod + def get_config_path(): + if getattr(sys, 'frozen', False): + base_dir = os.path.dirname(sys.executable) + else: + base_dir = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(base_dir, CONFIG_FILE) + + @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) + for k, v in data.items(): + if k in config: + if isinstance(v, type(config[k])) or (isinstance(config[k], float) and isinstance(v, int)): + config[k] = v + except Exception as e: + print(f"[Config] Error loading/validating config: {e}") + + return config + + @staticmethod + def save_config(config_data: Dict[str, Any]): + try: + path = ConfigManager.get_config_path() + 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}") + + @staticmethod + def reset_config(): + ConfigManager.save_config(ConfigManager.DEFAULT_CONFIG) + return ConfigManager.DEFAULT_CONFIG.copy() + +# ----------------------------------------------------------------------------- +# ESCÁNER DE HARDWARE (INTEGRACIÓN MOTOR NEO) +# ----------------------------------------------------------------------------- + + +class HardwareScanner: + @staticmethod + def get_specs_detailed() -> Dict[str, Any]: + """ + Utiliza el motor de detección de NEO (WMI/PSUTIL/GPUTIL) + adaptado para devolver el diccionario que espera Warlock. + """ + specs = { + "os": f"{platform.system()} {platform.release()} ({platform.architecture()[0]})", + "cpu_name": platform.processor(), + "cpu_cores": os.cpu_count() or 4, + "ram_total": 0.0, + "ram_used": 0.0, + "gpu_name": "Integrated / Unknown", + "gpu_vram": 0, + "disk_free": 0.0, + } + + try: + # 1. CPU y SO (Motor NEO: WMI Processor) + if wmi: + try: + c = wmi.WMI() + # SO Info detallada + specs["os"] = f"{platform.system()} {platform.release()} {platform.machine()}" + + # CPU Info detallada + proc_obj = c.Win32_Processor()[0] + specs["cpu_name"] = proc_obj.Name.strip() + specs["cpu_cores"] = proc_obj.NumberOfLogicalProcessors + except Exception: + pass # Fallback a los valores por defecto de platform + + # 2. RAM (Motor NEO: Psutil + WMI logic adaptation) + mem = psutil.virtual_memory() + specs["ram_total"] = round(mem.total / (1024**3), 2) + specs["ram_used"] = round(mem.used / (1024**3), 2) + + # 3. Almacenamiento (Motor NEO logic) + total_free_space = 0 + for p in psutil.disk_partitions(): + try: + usage = psutil.disk_usage(p.mountpoint) + total_free_space += usage.free + except PermissionError: + continue + specs["disk_free"] = round(total_free_space / (1024**3), 2) + + # 4. GPU (Motor NEO: GPUtil Priority -> WMI Fallback) + gpu_found = False + + # Intento A: GPUtil (NVIDIA) + if GPUtil: + try: + gpus = GPUtil.getGPUs() + if gpus: + best_gpu = gpus[0] + specs["gpu_name"] = f"[NVIDIA] {best_gpu.name}" + specs["gpu_vram"] = round( + best_gpu.memoryTotal / 1024, 2) # GPUtil returns MB + gpu_found = True + except Exception: + pass + + # Intento B: WMI (AMD / Intel / Fallback) + if not gpu_found and wmi: + try: + c = wmi.WMI() + for gpu in c.Win32_VideoController(): + # Lógica simple para elegir la GPU dedicada si hay varias + name = gpu.Name + vram_bytes = 0 + try: + # AdapterRAM suele devolver bytes, pero a veces es negativo en int32 overflow + vram_bytes = int(gpu.AdapterRAM) + if vram_bytes < 0: + vram_bytes += 2**32 + except: + vram_bytes = 0 + + vram_gb = round(vram_bytes / (1024**3), 2) + + # Preferir GPU con más VRAM o que no sea "Intel" si ya tenemos una integrada + current_best = specs["gpu_vram"] + # Si encontramos una con más VRAM o es la primera dedicada que vemos + if vram_gb > current_best or (vram_gb > 0 and "Intel" not in name and specs["gpu_name"] == "Integrated / Unknown"): + specs["gpu_name"] = name + specs["gpu_vram"] = vram_gb + except Exception: + pass + + except Exception as e: + print(f"[HardwareScanner] Error using NEO Engine: {e}") + + return specs + + @staticmethod + def get_smart_recommendations(specs: Dict) -> Dict: + rec = {} + vram = specs.get("gpu_vram", 0) + ram = specs.get("ram_total", 0) + + # Cálculo de seguridad Warlock + safe_vram = max(0.5, vram - 1.5) + rec_tiles = int(safe_vram * 3.5) + if rec_tiles < 2: + rec_tiles = 2 + + rec["Recommended Tiles"] = f"{rec_tiles}" + rec["Safe VRAM Limit"] = f"{safe_vram:.1f} GB" + + if vram >= 8 and ram >= 16: + rec["AI Model Class"] = "High End (RealESRGANx4 / BSRGANx4)" + rec["Multithreading"] = "2 - 4 Threads" + elif vram >= 4: + rec["AI Model Class"] = "Mid Range (RealESR_Animex4)" + rec["Multithreading"] = "2 Threads" + else: + rec["AI Model Class"] = "Low End (RealESR_Gx4 / RIFE Lite)" + rec["Multithreading"] = "OFF (1 Thread)" + + return rec + +# ----------------------------------------------------------------------------- +# UPDATE MANAGER +# ----------------------------------------------------------------------------- + + +class UpdateManager: + def __init__(self, owner, repo, version): + self.api_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + self.current_ver = version + + def check_update(self): + try: + r = requests.get(self.api_url, timeout=10) + if r.status_code == 200: + data = r.json() + tag = data.get("tag_name", "v0.0").lstrip("v") + + if pkg_version.parse(tag) > pkg_version.parse(self.current_ver): + return True, tag, data.get("assets", []), data.get("body", "No changelog provided.") + return False, self.current_ver, None, None + except Exception as e: + return None, str(e), None, None + + def download(self, url, path, progress_callback): + try: + with requests.get(url, stream=True, timeout=30) as r: + r.raise_for_status() + total = int(r.headers.get('content-length', 0)) + downloaded = 0 + with open(path, 'wb') as f: + for chunk in r.iter_content(8192): + f.write(chunk) + downloaded += len(chunk) + if progress_callback and total > 0: + progress_callback(downloaded / total) + return path + except Exception as e: + raise e + +# ----------------------------------------------------------------------------- +# COMPONENTES UI PERSONALIZADOS (Blender Style) +# ----------------------------------------------------------------------------- + + +class SettingCard(ctk.CTkFrame): + def __init__(self, master, title, **kwargs): + super().__init__(master, fg_color="transparent", **kwargs) + self.pack(fill="x", pady=(5, 0), padx=5) + + h = ctk.CTkFrame(self, fg_color="transparent") + h.pack(fill="x", pady=(5, 5)) + + ctk.CTkLabel(h, text="—", text_color=THEME["border"], font=( + "Arial", 12, "bold")).pack(side="left", padx=(0, 5)) + ctk.CTkLabel(h, text=title.upper(), text_color=THEME["text_sec"], font=get_font( + 11, "bold")).pack(side="left") + ctk.CTkFrame(self, height=1, fg_color=THEME["widget_bg"]).pack( + fill="x", padx=0, pady=(0, 5)) + + +class CollapsibleMenu(ctk.CTkFrame): + def __init__(self, master, title, **kwargs): + super().__init__(master, + fg_color=THEME["card_bg"], border_color=THEME["border"], border_width=1, **kwargs) + self.pack(fill="x", pady=8, padx=15) + self.grid_columnconfigure(0, weight=1) + + self.header_frame = ctk.CTkFrame( + self, fg_color=THEME["widget_bg"], height=40, corner_radius=0) + self.header_frame.grid(row=0, column=0, sticky="ew") + self.header_frame.grid_columnconfigure(0, weight=1) + + ctk.CTkLabel(self.header_frame, text=title.upper(), font=get_font( + 13, "bold"), text_color=THEME["accent"], padx=15, anchor="w").grid(row=0, column=0, sticky="ew", pady=5) + + self.is_expanded = True + self.toggle_button = ctk.CTkButton(self.header_frame, text="▼", width=30, height=30, + fg_color=THEME["widget_bg"], hover_color=THEME["hover"], + text_color=THEME["text"], command=self.toggle) + self.toggle_button.grid(row=0, column=1, padx=10, pady=5) + + self.content_frame = ctk.CTkFrame( + self, fg_color=THEME["card_bg"], corner_radius=0) + self.content_frame.grid( + row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) + self.content_frame.grid_columnconfigure(0, weight=1) + + def toggle(self): + if self.is_expanded: + self.content_frame.grid_remove() + self.toggle_button.configure(text="▶") + else: + self.content_frame.grid( + row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) + self.toggle_button.configure(text="▼") + self.is_expanded = not self.is_expanded + + +class DownloadWindow(ctk.CTkToplevel): + def __init__(self, master, filename): + super().__init__(master) + self.title("Update Downloader") + self.geometry("400x150") + self.attributes("-topmost", True) + self.configure(fg_color=THEME["bg"]) + + try: + x = master.winfo_x() + (master.winfo_width()//2) - 200 + y = master.winfo_y() + (master.winfo_height()//2) - 75 + self.geometry(f"+{x}+{y}") + except: + pass + + ctk.CTkLabel(self, text=f"Downloading {filename}...", text_color=THEME["accent"], font=get_font( + 12)).pack(pady=(30, 10)) + + self.prog = ctk.CTkProgressBar( + self, width=300, progress_color=THEME["success"]) + self.prog.pack(pady=10) + self.prog.set(0) + + self.lbl = ctk.CTkLabel(self, text="0%", text_color=THEME["text_sec"]) + self.lbl.pack() + + def update_progress(self, val): + self.prog.set(val) + self.lbl.configure(text=f"{int(val*100)}%") + self.update_idletasks() + +# ----------------------------------------------------------------------------- +# VENTANA PRINCIPAL DE PREFERENCIAS +# ----------------------------------------------------------------------------- + + +class PreferencesWindow(ctk.CTkToplevel): + def __init__(self, master, version, owner, repo): + super().__init__(master) + self.title("Warlock-Studio - Preferences") + self.geometry("650x750") + self.configure(fg_color=THEME["bg"]) + self.attributes("-topmost", True) + + try: + if getattr(sys, 'frozen', False): + base_dir = os.path.dirname(sys.executable) + else: + base_dir = os.path.dirname(os.path.abspath(__file__)) + icon_path = os.path.join(base_dir, "Assets", "logo.ico") + self.iconbitmap(icon_path) + except: + pass + + self.curr_ver = version + self.upd_mgr = UpdateManager(owner, repo, version) + self.config = ConfigManager.load_config() + + self.create_ui() + self.after(100, lambda: self.focus_force()) + + def create_ui(self): + # HEADER + head = ctk.CTkFrame(self, fg_color="transparent", height=80) + head.pack(fill="x", padx=25, pady=15) + + t_frame = ctk.CTkFrame(head, fg_color="transparent") + t_frame.pack(side="left") + + ctk.CTkLabel(t_frame, text="WARLOCK", font=("Impact", 36), + text_color=THEME["title"]).pack(side="left") + ctk.CTkLabel(t_frame, text="- STUDIO", font=("Impact", 36), + text_color=THEME["text"]).pack(side="left") + + ver_badge = ctk.CTkLabel(head, text=f"v{self.curr_ver}", fg_color=THEME["widget_bg"], + text_color=THEME["accent"], corner_radius=6, padx=12, font=get_font(12, "bold")) + ver_badge.pack(side="right", padx=10) + + self.status_lbl = ctk.CTkLabel( + head, text="", font=get_font(11, "bold")) + self.status_lbl.pack(side="right", padx=10) + + # SCROLLABLE CONTENT + self.scroll_frame = ctk.CTkScrollableFrame(self, fg_color=THEME["bg"], scrollbar_button_color=THEME["border"], + label_text="SETTINGS MENU", label_text_color=THEME["text_sec"], + label_font=get_font(10, "bold")) + self.scroll_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20)) + self.scroll_frame.grid_columnconfigure(0, weight=1) + + self._build_general_menu() + self._build_system_menu() + self._build_updates_menu() + self._build_logs_menu() + self._build_about_menu() + + self.after(500, self._run_scan) + + # --- MENUS --- + + def _build_general_menu(self): + menu = CollapsibleMenu(self.scroll_frame, "General Settings") + frame = menu.content_frame + + c1 = SettingCard(frame, "USER INTERFACE") + self._add_dropdown(frame, "Theme Mode", "app_theme", ["Dark", "Light", "System"], + lambda v: self._update("app_theme", v, lambda: ctk.set_appearance_mode(v))) + self._add_slider(frame, "Window Opacity", "window_opacity", 0.5, 1.0, 50, + lambda v: self._update("window_opacity", v, lambda: self.master.attributes("-alpha", v))) + self._add_slider(frame, "UI Scaling", "ui_scaling", 0.8, 1.5, 7, + lambda v: self._update("ui_scaling", v, lambda: ctk.set_widget_scaling(v))) + + c2 = SettingCard(frame, "BEHAVIOR") + self._add_switch(frame, "Keep Window on Top", "keep_window_on_top", + lambda: self.master.attributes("-topmost", self.config["keep_window_on_top"])) + self._add_switch(frame, "Check Updates on Startup", "check_updates_on_startup", + lambda: self._update("check_updates_on_startup", self.config["check_updates_on_startup"])) + self._add_switch(frame, "Auto-Close App when Task Finishes", "auto_close_on_finish", + lambda: self._update("auto_close_on_finish", self.config["auto_close_on_finish"])) + + c3 = SettingCard(frame, "MAINTENANCE") + row = ctk.CTkFrame(frame, fg_color="transparent") + row.pack(fill="x", padx=5, pady=10) + ctk.CTkButton(row, text="Clean Temporary Files", fg_color="#4A0000", hover_color=THEME["error"], border_color=THEME["error"], border_width=1, + command=self._clean_temp).pack(side="left", expand=True, fill="x", padx=(0, 5)) + ctk.CTkButton(row, text="Reset All Settings", fg_color="transparent", border_color=THEME["warning"], border_width=1, text_color=THEME["warning"], + hover_color="#332200", command=self._reset_settings).pack(side="right", expand=True, fill="x", padx=(5, 0)) + + def _build_system_menu(self): + self.hw_menu = CollapsibleMenu( + self.scroll_frame, "System & AI Configuration") + frame = self.hw_menu.content_frame + + SettingCard(frame, "HARDWARE DIAGNOSTICS (NEO ENGINE)") + self.hw_content = ctk.CTkFrame(frame, fg_color="transparent") + self.hw_content.pack(fill="both", padx=10, pady=5) + + self.btn_scan = ctk.CTkButton(frame, text="Run Hardware Scan", fg_color=THEME["border"], hover_color=THEME["hover"], + command=self._run_scan, height=35) + self.btn_scan.pack(pady=15, padx=10) + + SettingCard(frame, "PROCESSING PRIORITY") + self._add_dropdown(frame, "FFmpeg/AI Priority", "process_priority", ["Normal", "Above Normal", "High"], + lambda v: self._update("process_priority", v)) + + def _run_scan(self): + self.btn_scan.configure(state="disabled", text="Scanning System...") + for widget in self.hw_content.winfo_children(): + widget.destroy() + ctk.CTkLabel(self.hw_content, text="Scanning with NEO Engine... Please wait.", + text_color=THEME["text_sec"]).pack(pady=20) + threading.Thread(target=self._thread_scan, daemon=True).start() + + def _thread_scan(self): + specs = HardwareScanner.get_specs_detailed() + recs = HardwareScanner.get_smart_recommendations(specs) + if self.winfo_exists(): + self.after(0, lambda: self._render_specs(specs, recs)) + + def _render_specs(self, specs, recs): + if not self.winfo_exists(): + return + self.btn_scan.configure(state="normal", text="Refresh Hardware Info") + for widget in self.hw_content.winfo_children(): + widget.destroy() + + grid = ctk.CTkFrame(self.hw_content, fg_color="transparent") + grid.pack(fill="x") + grid.grid_columnconfigure(1, weight=1) + + items = [("CPU", specs['cpu_name']), ("Cores", f"{specs['cpu_cores']} Logical Cores"), + ("RAM", + f"{specs['ram_total']} GB (Used: {specs['ram_used']} GB)"), + ("GPU", specs['gpu_name']), ("VRAM", f"{specs['gpu_vram']} GB"), ("OS", specs['os'])] + + for i, (label, val) in enumerate(items): + ctk.CTkLabel(grid, text=label, font=get_font(11, "bold"), text_color=THEME["text_sec"], anchor="w").grid( + row=i, column=0, sticky="w", pady=4, padx=5) + ctk.CTkLabel(grid, text=str(val), font=get_font(11), text_color=THEME["accent"], anchor="w").grid( + row=i, column=1, sticky="w", padx=20, pady=4) + + rec_frame = ctk.CTkFrame( + self.hw_content, fg_color="#1A1A1A", corner_radius=6) + rec_frame.pack(fill="x", padx=10, pady=20) + ctk.CTkLabel(rec_frame, text="RECOMMENDED AI SETTINGS", + text_color=THEME["success"], font=get_font(11, "bold")).pack(pady=(10, 10)) + + rec_grid = ctk.CTkFrame(rec_frame, fg_color="transparent") + rec_grid.pack(pady=(0, 10), padx=10) + r_items = [("Tiles Resolution", recs.get("Recommended Tiles", "N/A")), + ("VRAM Limit", recs.get("Safe VRAM Limit", "N/A")), + ("AI Threads", recs.get("Multithreading", "OFF"))] + for i, (k, v) in enumerate(r_items): + ctk.CTkLabel(rec_grid, text=k + ": ", text_color=THEME["text_sec"], anchor="e").grid( + row=i, column=0, padx=(10, 5), pady=2, sticky="e") + ctk.CTkLabel(rec_grid, text=v, text_color="#FFFFFF", font=get_font( + 12, "bold"), anchor="w").grid(row=i, column=1, padx=(0, 10), pady=2, sticky="w") + + def _build_updates_menu(self): + menu = CollapsibleMenu(self.scroll_frame, "Updates & Changelog") + frame = menu.content_frame + v_frame = ctk.CTkFrame(frame, fg_color="transparent") + v_frame.pack(pady=15) + ctk.CTkLabel(v_frame, text="CURRENT VERSION", font=get_font( + 10), text_color=THEME["text_sec"]).pack() + ctk.CTkLabel(v_frame, text=f"v{self.curr_ver}", font=( + "Impact", 42), text_color=THEME["accent"]).pack() + self.btn_upd = ctk.CTkButton(frame, text="Check for Updates", height=45, + fg_color=THEME["border"], hover_color=THEME["hover"], font=get_font(14, "bold"), command=self._check_update) + self.btn_upd.pack(fill="x", padx=60, pady=(0, 20)) + ctk.CTkLabel(frame, text="CHANGELOG / STATUS:", font=get_font(11, "bold"), + text_color=THEME["text_sec"], anchor="w").pack(fill="x", pady=(10, 5), padx=5) + self.upd_log = ctk.CTkTextbox(frame, height=200, fg_color="#111111", text_color=THEME["text"], font=( + "Consolas", 11), border_width=1, border_color=THEME["widget_bg"]) + self.upd_log.pack(fill="x", expand=True, pady=5, padx=5) + self.upd_log.insert( + "0.0", "Click 'Check for Updates' to connect to GitHub repository...") + + def _check_update(self): + self.btn_upd.configure(state="disabled", text="Checking GitHub...") + self.upd_log.delete("0.0", "end") + self.upd_log.insert("end", "Connecting to repository...\n") + threading.Thread(target=self._thread_upd, daemon=True).start() + + def _thread_upd(self): + res = self.upd_mgr.check_update() + if self.winfo_exists(): + self.after(0, lambda: self._res_upd(res)) + + def _res_upd(self, res): + is_new, tag, assets, body = res + self.btn_upd.configure(state="normal", text="Check Again") + self.upd_log.delete("0.0", "end") + if is_new is None: + self.upd_log.insert("end", f"Connection Error: {tag}\n") + elif is_new: + self.upd_log.insert( + "end", f"NEW UPDATE AVAILABLE: v{tag}\n\n--- CHANGELOG ---\n\n{body}\n") + if messagebox.askyesno("Update Found", f"Version v{tag} available. Download?"): + target = next( + (a for a in assets if a['name'].endswith('.exe')), None) + if target: + self._start_download( + target['browser_download_url'], target['name']) + else: + webbrowser.open( + "https://github.com/Ivan-Ayub97/Warlock-Studio/releases") + else: + self.upd_log.insert( + "end", f"You are using the latest version (v{tag}).") + + def _start_download(self, url, name): + dw = DownloadWindow(self, name) + + def t(): + try: + tmp = os.path.join(os.getenv('TEMP'), name) + self.upd_mgr.download(url, tmp, dw.update_progress) + dw.destroy() + if messagebox.askyesno("Install", "Download complete. Install now?"): + os.startfile(tmp) + sys.exit(0) + except Exception as e: + dw.destroy() + messagebox.showerror("Download Error", str(e)) + threading.Thread(target=t, daemon=True).start() + + def _build_logs_menu(self): + menu = CollapsibleMenu(self.scroll_frame, "Log Management") + frame = menu.content_frame + ctrl = ctk.CTkFrame(frame, fg_color="transparent") + ctrl.pack(fill="x", padx=5, pady=5) + ctk.CTkButton(ctrl, text="Refresh Logs", width=120, + fg_color=THEME["widget_bg"], command=self._load_logs).pack(side="left", padx=5) + ctk.CTkButton(ctrl, text="Export All Logs (.zip)", width=160, + fg_color=THEME["info_bg"], command=self._export_logs).pack(side="right", padx=5) + self.log_text = ctk.CTkTextbox(frame, font=("Consolas", 11), fg_color="#111111", text_color="#DDDDDD", + wrap="none", border_width=1, border_color=THEME["widget_bg"], height=200) + self.log_text.pack(fill="x", expand=True, padx=5, pady=5) + self._load_logs() + + def _load_logs(self): + self.log_text.delete("0.0", "end") + log_dir = os.path.join(os.path.expanduser('~'), 'Documents') + patterns = [os.path.join(log_dir, "Warlock*Logs", "*.log"), + os.path.join(log_dir, "Warlock*Logs", "*.txt")] + found_files = [] + for p in patterns: + found_files.extend(glob.glob(p)) + found_files.sort(key=os.path.getmtime, reverse=True) + if not found_files: + self.log_text.insert("end", "No log files found.") + return + latest = found_files[0] + self.log_text.insert( + "end", f"--- LOADING LOG: {os.path.basename(latest)} ---\n\n") + try: + with open(latest, "r", encoding="utf-8", errors='replace') as f: + self.log_text.insert("end", f.read()) + except Exception as e: + self.log_text.insert("end", f"Error reading log: {e}") + + def _export_logs(self): + try: + save_path = filedialog.asksaveasfilename( + defaultextension=".zip", filetypes=[("ZIP", "*.zip")]) + if not save_path: + return + log_dir = os.path.join(os.path.expanduser('~'), 'Documents') + folders = glob.glob(os.path.join(log_dir, "Warlock*Logs")) + if not folders: + return + with zipfile.ZipFile(save_path, 'w') as zf: + for folder in folders: + for root, _, files in os.walk(folder): + for file in files: + zf.write(os.path.join(root, file), arcname=file) + messagebox.showinfo("Export", "Logs exported successfully!") + except Exception as e: + messagebox.showerror("Error", str(e)) + + def _build_about_menu(self): + menu = CollapsibleMenu(self.scroll_frame, "About Warlock-Studio") + frame = menu.content_frame + ctk.CTkLabel(frame, text="WARLOCK-STUDIO", font=("Impact", + 40), text_color=THEME["title"]).pack(pady=(20, 5)) + ctk.CTkLabel(frame, text=f"Version {self.curr_ver}", font=get_font( + 12), text_color=THEME["accent"]).pack(pady=(0, 20)) + SettingCard(frame, "DEVELOPER & PROJECT") + ctk.CTkLabel(frame, text="Developed by Ivan-Ayub97", + font=get_font(12, "bold")).pack(pady=5) + link_frame = ctk.CTkFrame(frame, fg_color="transparent") + link_frame.pack(pady=10) + ctk.CTkButton(link_frame, text="GitHub Repository", fg_color="#24292e", width=140, command=lambda: webbrowser.open( + "https://github.com/Ivan-Ayub97/Warlock-Studio")).pack(side="left", padx=10) + ctk.CTkButton(link_frame, text="SourceForge", fg_color="#EE7600", width=140, command=lambda: webbrowser.open( + "https://sourceforge.net/projects/warlock-studio/")).pack(side="left", padx=10) + + SettingCard(frame, "LICENSES") + base_dir = getattr(sys, '_MEIPASS', os.path.dirname( + os.path.abspath(__file__))) + license_path = os.path.join(base_dir, "Assets", "license.txt") + try: + with open(license_path, "r", encoding="utf-8") as f: + l_txt = f.read() + except: + l_txt = "License file not found." + ld = ctk.CTkTextbox(frame, height=200, fg_color="#111111", + text_color=THEME["text"], font=("Consolas", 10)) + ld.pack(fill="x", expand=True, pady=10, padx=20) + ld.insert("0.0", l_txt) + ld.configure(state="disabled") + + # --- HELPERS --- + + def _update(self, key, val, callback=None): + if isinstance(val, float): + val = round(val, 2) + self.config[key] = val + ConfigManager.save_config(self.config) + if callback: + callback() + self.status_lbl.configure(text="Saved ✓", text_color=THEME["success"]) + self.after(2000, lambda: self.status_lbl.configure(text="")) + + def _add_slider(self, parent, text, key, min_v, max_v, steps, cmd): + f = ctk.CTkFrame(parent, fg_color="transparent") + f.pack(fill="x", padx=5, pady=5) + ctk.CTkLabel(f, text=text, width=140, anchor="w", + font=get_font(11)).pack(side="left") + val_lbl = ctk.CTkLabel( + f, text=f"{self.config.get(key, min_v):.2f}", width=40, text_color=THEME["accent"]) + val_lbl.pack(side="right") + + def internal_cmd(v): + val_lbl.configure(text=f"{v:.2f}") + cmd(v) + s = ctk.CTkSlider(f, from_=min_v, to=max_v, number_of_steps=steps, command=internal_cmd, + progress_color=THEME["accent"], button_color=THEME["accent"], button_hover_color=THEME["accent_hover"]) + s.set(self.config.get(key, min_v)) + s.pack(side="right", fill="x", expand=True, padx=10) + + def _add_switch(self, parent, text, key, cmd): + def internal_cmd(): + self.config[key] = bool(sw.get()) + ConfigManager.save_config(self.config) + if cmd: + cmd() + self.status_lbl.configure( + text="Saved ✓", text_color=THEME["success"]) + self.after(1500, lambda: self.status_lbl.configure(text="")) + sw = ctk.CTkSwitch(parent, text=text, command=internal_cmd, font=get_font(11), + progress_color=THEME["accent"], button_color="#FFFFFF", button_hover_color="#EEEEEE") + if self.config.get(key, False): + sw.select() + sw.pack(anchor="w", padx=10, pady=8) + + def _add_dropdown(self, parent, text, key, values, cmd): + f = ctk.CTkFrame(parent, fg_color="transparent") + f.pack(fill="x", padx=5, pady=5) + ctk.CTkLabel(f, text=text, width=140, anchor="w", + font=get_font(11)).pack(side="left") + om = ctk.CTkOptionMenu(f, values=values, command=cmd, font=get_font(11), + fg_color=THEME["widget_bg"], button_color=THEME["accent"], + button_hover_color=THEME["accent_hover"], text_color=THEME["bg"]) + om.set(self.config.get(key, values[0])) + om.pack(side="right", fill="x", expand=True, padx=10) + + def _clean_temp(self): + n = 0 + cwd = os.getcwd() + patterns = ["*.tmp", "*.checkpoint", + "*.part", "temp_*", "*_frames.txt"] + for p in patterns: + for f in glob.glob(os.path.join(cwd, p)): + try: + os.remove(f) + n += 1 + except: + pass + messagebox.showinfo( + "Clean", f"Cleanup complete.\nRemoved {n} temporary files.") + + def _reset_settings(self): + if messagebox.askyesno("Reset", "Restore default settings?"): + ConfigManager.reset_config() + self.destroy() + +# ----------------------------------------------------------------------------- +# BOTÓN PRINCIPAL (INTEGRACIÓN) +# ----------------------------------------------------------------------------- + + +class PreferencesButton(ctk.CTkButton): + def __init__(self, master, current_version, repo_owner="Ivan-Ayub97", repo_name="Warlock-Studio", **kwargs): + super().__init__(master, text="⚙", width=100, height=28, + fg_color=THEME["widget_bg"], border_color=THEME["border"], border_width=1, + hover_color=THEME["hover"], text_color=THEME["text"], + command=self.open_window, **kwargs) + self.ver = current_version + self.repo = (repo_owner, repo_name) + self._apply_startup_config() + + def open_window(self): + for w in self.master.winfo_children(): + if isinstance(w, PreferencesWindow): + w.lift() + w.focus_force() + return + PreferencesWindow(self.master, self.ver, *self.repo) + + def _apply_startup_config(self): + c = ConfigManager.load_config() + try: + ctk.set_appearance_mode(c.get("app_theme", "Dark")) + ctk.set_widget_scaling(c.get("ui_scaling", 1.0)) + except: + pass + if c.get("window_opacity", 1.0) < 1.0: + self.master.attributes("-alpha", c["window_opacity"]) + if c.get("keep_window_on_top", False): + self.master.attributes("-topmost", True)