{
    "sourceFile": "gui/processing_handler.py",
    "activeCommit": 0,
    "commits": [
        {
            "activePatchIndex": 8,
            "patches": [
                {
                    "date": 1745236414824,
                    "content": "Index: \n===================================================================\n--- \n+++ \n"
                },
                {
                    "date": 1745262321751,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -22,8 +22,9 @@\n     from main import process_single_asset_wrapper\r\n     # Import exceptions if needed for type hinting or specific handling\r\n     from configuration import ConfigurationError\r\n     from asset_processor import AssetProcessingError\r\n+    import config as core_config # <<< ADDED IMPORT\r\n     BACKEND_AVAILABLE = True\r\n except ImportError as e:\r\n     print(f\"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}\")\r\n     # Define placeholders if imports fail, so the GUI doesn't crash immediately\r\n@@ -63,12 +64,13 @@\n     @property\r\n     def is_running(self):\r\n         return self._is_running\r\n \r\n-    def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int):\r\n+    def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,\r\n+                         run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str): # <<< ADDED PARAMS\r\n         \"\"\"\r\n-        Starts the asset processing task. This method should be called\r\n-        when the handler is moved to a separate thread.\r\n+        Starts the asset processing task and optionally runs Blender scripts afterwards.\r\n+        This method should be called when the handler is moved to a separate thread.\r\n         \"\"\"\r\n         if self._is_running:\r\n             log.warning(\"Processing is already running.\")\r\n             self.status_message.emit(\"Processing already in progress.\", 3000)\r\n@@ -169,11 +171,78 @@\n             # Mark all remaining as failed\r\n             failed_count = total_files - processed_count - skipped_count\r\n \r\n         finally:\r\n-            final_message = f\"Processing finished. Processed: {processed_count}, Skipped: {skipped_count}, Failed: {failed_count}\"\r\n+            # --- Blender Script Execution (Optional) ---\r\n+            if run_blender and not self._cancel_requested:\r\n+                log.info(\"Asset processing complete. Checking for Blender script execution.\")\r\n+                self.status_message.emit(\"Asset processing complete. Starting Blender scripts...\", 0)\r\n+                blender_exe = self._find_blender_executable()\r\n+                if blender_exe:\r\n+                    script_dir = Path(__file__).parent.parent / \"blenderscripts\" # Go up one level from gui/\r\n+                    nodegroup_script_path = script_dir / \"create_nodegroups.py\"\r\n+                    materials_script_path = script_dir / \"create_materials.py\"\r\n+                    asset_output_root = output_dir_str # Use the same output dir\r\n+\r\n+                    # Run Nodegroup Script\r\n+                    if nodegroup_blend_path and Path(nodegroup_blend_path).is_file():\r\n+                        if nodegroup_script_path.is_file():\r\n+                            log.info(\"-\" * 20 + \" Running Nodegroup Script \" + \"-\" * 20)\r\n+                            self.status_message.emit(f\"Running Blender nodegroup script on {Path(nodegroup_blend_path).name}...\", 0)\r\n+                            success_ng = self._run_blender_script_subprocess(\r\n+                                blender_exe_path=blender_exe,\r\n+                                blend_file_path=nodegroup_blend_path,\r\n+                                python_script_path=str(nodegroup_script_path),\r\n+                                asset_root_dir=asset_output_root\r\n+                            )\r\n+                            if not success_ng:\r\n+                                log.error(\"Blender node group script execution failed.\")\r\n+                                self.status_message.emit(\"Blender nodegroup script failed.\", 5000)\r\n+                            else:\r\n+                                log.info(\"Blender nodegroup script finished successfully.\")\r\n+                                self.status_message.emit(\"Blender nodegroup script finished.\", 3000)\r\n+                        else:\r\n+                            log.error(f\"Node group script not found: {nodegroup_script_path}\")\r\n+                            self.status_message.emit(f\"Error: Nodegroup script not found.\", 5000)\r\n+                    elif run_blender and nodegroup_blend_path: # Log if path was provided but invalid\r\n+                         log.warning(f\"Nodegroup blend path provided but invalid: {nodegroup_blend_path}\")\r\n+                         self.status_message.emit(f\"Warning: Invalid Nodegroup .blend path.\", 5000)\r\n+\r\n+\r\n+                    # Run Materials Script (only if nodegroup script was attempted or not needed)\r\n+                    if materials_blend_path and Path(materials_blend_path).is_file():\r\n+                         if materials_script_path.is_file():\r\n+                            log.info(\"-\" * 20 + \" Running Materials Script \" + \"-\" * 20)\r\n+                            self.status_message.emit(f\"Running Blender materials script on {Path(materials_blend_path).name}...\", 0)\r\n+                            success_mat = self._run_blender_script_subprocess(\r\n+                                blender_exe_path=blender_exe,\r\n+                                blend_file_path=materials_blend_path,\r\n+                                python_script_path=str(materials_script_path),\r\n+                                asset_root_dir=asset_output_root\r\n+                            )\r\n+                            if not success_mat:\r\n+                                log.error(\"Blender material script execution failed.\")\r\n+                                self.status_message.emit(\"Blender material script failed.\", 5000)\r\n+                            else:\r\n+                                log.info(\"Blender material script finished successfully.\")\r\n+                                self.status_message.emit(\"Blender material script finished.\", 3000)\r\n+                         else:\r\n+                            log.error(f\"Material script not found: {materials_script_path}\")\r\n+                            self.status_message.emit(f\"Error: Material script not found.\", 5000)\r\n+                    elif run_blender and materials_blend_path: # Log if path was provided but invalid\r\n+                         log.warning(f\"Materials blend path provided but invalid: {materials_blend_path}\")\r\n+                         self.status_message.emit(f\"Warning: Invalid Materials .blend path.\", 5000)\r\n+\r\n+                else:\r\n+                    log.warning(\"Blender executable not found. Skipping Blender script execution.\")\r\n+                    self.status_message.emit(\"Warning: Blender executable not found. Skipping scripts.\", 5000)\r\n+            elif self._cancel_requested:\r\n+                 log.info(\"Processing was cancelled. Skipping Blender script execution.\")\r\n+            # --- End Blender Script Execution ---\r\n+\r\n+            final_message = f\"Finished. Processed: {processed_count}, Skipped: {skipped_count}, Failed: {failed_count}\"\r\n             log.info(final_message)\r\n-            self.status_message.emit(final_message, 5000)\r\n+            self.status_message.emit(final_message, 5000) # Show final summary\r\n             self.processing_finished.emit(processed_count, skipped_count, failed_count)\r\n             self._is_running = False\r\n             self._executor = None\r\n             self._futures = {} # Clear futures\r\n@@ -206,5 +275,57 @@\n              log.debug(\"Executor shutdown requested.\")\r\n \r\n         # Note: True cancellation of running ProcessPoolExecutor tasks is complex.\r\n         # This implementation primarily prevents processing further results and\r\n-        # attempts to cancel pending/unstarted tasks.\n\\ No newline at end of file\n+        # attempts to cancel pending/unstarted tasks.\r\n+\r\n+    def _find_blender_executable(self) -> Optional[str]:\r\n+        \"\"\"Finds the Blender executable path from config or system PATH.\"\"\"\r\n+        try:\r\n+            blender_exe_config = getattr(core_config, 'BLENDER_EXECUTABLE_PATH', None)\r\n+            if blender_exe_config:\r\n+                p = Path(blender_exe_config)\r\n+                if p.is_file():\r\n+                    log.info(f\"Using Blender executable from config: {p}\")\r\n+                    return str(p.resolve())\r\n+                else:\r\n+                    log.warning(f\"Blender path in config not found: '{blender_exe_config}'. Trying PATH.\")\r\n+            else:\r\n+                 log.info(\"BLENDER_EXECUTABLE_PATH not set in config. Trying PATH.\")\r\n+\r\n+            blender_exe = shutil.which(\"blender\")\r\n+            if blender_exe:\r\n+                log.info(f\"Found Blender executable in PATH: {blender_exe}\")\r\n+                return blender_exe\r\n+            else:\r\n+                log.warning(\"Could not find 'blender' in system PATH.\")\r\n+                return None\r\n+        except Exception as e:\r\n+            log.error(f\"Error checking Blender executable path: {e}\")\r\n+            return None\r\n+\r\n+    def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str) -> bool:\r\n+        \"\"\"Internal helper to run a single Blender script via subprocess.\"\"\"\r\n+        command = [\r\n+            blender_exe_path,\r\n+            \"-b\",\r\n+            blend_file_path,\r\n+            \"--python\", python_script_path,\r\n+            \"--\",\r\n+            asset_root_dir,\r\n+            \"-S\"\r\n+        ]\r\n+        log.debug(f\"Executing Blender command: {' '.join(command)}\")\r\n+        try:\r\n+            result = subprocess.run(command, capture_output=True, text=True, check=False, encoding='utf-8') # Specify encoding\r\n+            log.info(f\"Blender script '{Path(python_script_path).name}' finished with exit code: {result.returncode}\")\r\n+            if result.stdout: log.debug(f\"Blender stdout:\\n{result.stdout.strip()}\")\r\n+            if result.stderr:\r\n+                if result.returncode != 0: log.error(f\"Blender stderr:\\n{result.stderr.strip()}\")\r\n+                else: log.warning(f\"Blender stderr (RC=0):\\n{result.stderr.strip()}\")\r\n+            return result.returncode == 0\r\n+        except FileNotFoundError:\r\n+            log.error(f\"Blender executable not found at: {blender_exe_path}\")\r\n+            return False\r\n+        except Exception as e:\r\n+            log.exception(f\"Error running Blender script '{Path(python_script_path).name}': {e}\")\r\n+            return False\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745262398584,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -3,8 +3,12 @@\n from pathlib import Path\r\n from concurrent.futures import ProcessPoolExecutor, as_completed\r\n import time # For potential delays if needed\r\n \r\n+import subprocess # <<< ADDED IMPORT\r\n+import shutil # <<< ADDED IMPORT\r\n+from typing import Optional # <<< ADDED IMPORT\r\n+\r\n # --- PySide6 Imports ---\r\n # Inherit from QObject to support signals/slots for thread communication\r\n from PySide6.QtCore import QObject, Signal\r\n \r\n"
                },
                {
                    "date": 1745263152618,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -310,8 +310,9 @@\n     def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str) -> bool:\r\n         \"\"\"Internal helper to run a single Blender script via subprocess.\"\"\"\r\n         command = [\r\n             blender_exe_path,\r\n+            \"--factory-startup\", # <<< ADDED FLAG\r\n             \"-b\",\r\n             blend_file_path,\r\n             \"--python\", python_script_path,\r\n             \"--\",\r\n"
                },
                {
                    "date": 1745265314700,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -316,9 +316,9 @@\n             blend_file_path,\r\n             \"--python\", python_script_path,\r\n             \"--\",\r\n             asset_root_dir,\r\n-            \"-S\"\r\n+            # Removed incorrect -S flag (for setting scene, not saving)\r\n         ]\r\n         log.debug(f\"Executing Blender command: {' '.join(command)}\")\r\n         try:\r\n             result = subprocess.run(command, capture_output=True, text=True, check=False, encoding='utf-8') # Specify encoding\r\n"
                },
                {
                    "date": 1745265551001,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -310,11 +310,12 @@\n     def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str) -> bool:\r\n         \"\"\"Internal helper to run a single Blender script via subprocess.\"\"\"\r\n         command = [\r\n             blender_exe_path,\r\n-            \"--factory-startup\", # <<< ADDED FLAG\r\n+            \"--factory-startup\",\r\n             \"-b\",\r\n             blend_file_path,\r\n+            \"--log\", \"*\", # <<< ADDED BLENDER LOGGING FLAG\r\n             \"--python\", python_script_path,\r\n             \"--\",\r\n             asset_root_dir,\r\n             # Removed incorrect -S flag (for setting scene, not saving)\r\n"
                },
                {
                    "date": 1745266629402,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -216,13 +216,15 @@\n                     if materials_blend_path and Path(materials_blend_path).is_file():\r\n                          if materials_script_path.is_file():\r\n                             log.info(\"-\" * 20 + \" Running Materials Script \" + \"-\" * 20)\r\n                             self.status_message.emit(f\"Running Blender materials script on {Path(materials_blend_path).name}...\", 0)\r\n+                            # Pass the nodegroup blend path as the second argument to the script\r\n                             success_mat = self._run_blender_script_subprocess(\r\n                                 blender_exe_path=blender_exe,\r\n                                 blend_file_path=materials_blend_path,\r\n                                 python_script_path=str(materials_script_path),\r\n-                                asset_root_dir=asset_output_root\r\n+                                asset_root_dir=asset_output_root,\r\n+                                nodegroup_blend_file_path_arg=nodegroup_blend_path # Pass the nodegroup path\r\n                             )\r\n                             if not success_mat:\r\n                                 log.error(\"Blender material script execution failed.\")\r\n                                 self.status_message.emit(\"Blender material script failed.\", 5000)\r\n@@ -306,24 +308,30 @@\n         except Exception as e:\r\n             log.error(f\"Error checking Blender executable path: {e}\")\r\n             return None\r\n \r\n-    def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str) -> bool:\r\n+    def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str, nodegroup_blend_file_path_arg: Optional[str] = None) -> bool:\r\n         \"\"\"Internal helper to run a single Blender script via subprocess.\"\"\"\r\n-        command = [\r\n+        command_base = [\r\n             blender_exe_path,\r\n             \"--factory-startup\",\r\n             \"-b\",\r\n             blend_file_path,\r\n             \"--log\", \"*\", # <<< ADDED BLENDER LOGGING FLAG\r\n             \"--python\", python_script_path,\r\n             \"--\",\r\n             asset_root_dir,\r\n-            # Removed incorrect -S flag (for setting scene, not saving)\r\n         ]\r\n-        log.debug(f\"Executing Blender command: {' '.join(command)}\")\r\n+        # Add nodegroup blend file path if provided (for create_materials script)\r\n+        if nodegroup_blend_file_path_arg:\r\n+            command = command_base + [nodegroup_blend_file_path_arg]\r\n+        else:\r\n+            command = command_base\r\n+        log.debug(f\"Executing Blender command: {' '.join(map(str, command))}\") # Ensure all parts are strings for join\r\n         try:\r\n-            result = subprocess.run(command, capture_output=True, text=True, check=False, encoding='utf-8') # Specify encoding\r\n+            # Ensure all parts of the command are strings for subprocess\r\n+            str_command = [str(part) for part in command]\r\n+            result = subprocess.run(str_command, capture_output=True, text=True, check=False, encoding='utf-8') # Specify encoding\r\n             log.info(f\"Blender script '{Path(python_script_path).name}' finished with exit code: {result.returncode}\")\r\n             if result.stdout: log.debug(f\"Blender stdout:\\n{result.stdout.strip()}\")\r\n             if result.stderr:\r\n                 if result.returncode != 0: log.error(f\"Blender stderr:\\n{result.stderr.strip()}\")\r\n"
                },
                {
                    "date": 1745266683815,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,345 @@\n+# gui/processing_handler.py\r\n+import logging\r\n+from pathlib import Path\r\n+from concurrent.futures import ProcessPoolExecutor, as_completed\r\n+import time # For potential delays if needed\r\n+\r\n+import subprocess # <<< ADDED IMPORT\r\n+import shutil # <<< ADDED IMPORT\r\n+from typing import Optional # <<< ADDED IMPORT\r\n+\r\n+# --- PySide6 Imports ---\r\n+# Inherit from QObject to support signals/slots for thread communication\r\n+from PySide6.QtCore import QObject, Signal\r\n+\r\n+# --- Backend Imports ---\r\n+# Need to import the worker function and potentially config/processor if needed directly\r\n+# Adjust path to ensure modules can be found relative to this file's location\r\n+import sys\r\n+script_dir = Path(__file__).parent\r\n+project_root = script_dir.parent\r\n+if str(project_root) not in sys.path:\r\n+    sys.path.insert(0, str(project_root))\r\n+\r\n+try:\r\n+    # Import the worker function from main.py\r\n+    from main import process_single_asset_wrapper\r\n+    # Import exceptions if needed for type hinting or specific handling\r\n+    from configuration import ConfigurationError\r\n+    from asset_processor import AssetProcessingError\r\n+    import config as core_config # <<< ADDED IMPORT\r\n+    BACKEND_AVAILABLE = True\r\n+except ImportError as e:\r\n+    print(f\"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}\")\r\n+    # Define placeholders if imports fail, so the GUI doesn't crash immediately\r\n+    process_single_asset_wrapper = None\r\n+    ConfigurationError = Exception\r\n+    AssetProcessingError = Exception\r\n+    BACKEND_AVAILABLE = False\r\n+\r\n+log = logging.getLogger(__name__)\r\n+# Basic config if logger hasn't been set up elsewhere\r\n+if not log.hasHandlers():\r\n+     logging.basicConfig(level=logging.INFO, format='%(levelname)s (Handler): %(message)s')\r\n+\r\n+\r\n+class ProcessingHandler(QObject):\r\n+    \"\"\"\r\n+    Handles the execution of the asset processing pipeline in a way that\r\n+    can be run in a separate thread and communicate progress via signals.\r\n+    \"\"\"\r\n+    # --- Signals ---\r\n+    # Emitted for overall progress bar update\r\n+    progress_updated = Signal(int, int) # current_count, total_count\r\n+    # Emitted for updating status of individual files in the list\r\n+    file_status_updated = Signal(str, str, str) # input_path_str, status (\"processing\", \"processed\", \"skipped\", \"failed\"), message\r\n+    # Emitted when the entire batch processing is finished\r\n+    processing_finished = Signal(int, int, int) # processed_count, skipped_count, failed_count\r\n+    # Emitted for general status messages to the status bar\r\n+    status_message = Signal(str, int) # message, timeout_ms\r\n+\r\n+    def __init__(self, parent=None):\r\n+        super().__init__(parent)\r\n+        self._executor = None\r\n+        self._futures = {} # Store future->input_path mapping\r\n+        self._is_running = False\r\n+        self._cancel_requested = False\r\n+\r\n+    @property\r\n+    def is_running(self):\r\n+        return self._is_running\r\n+\r\n+    def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,\r\n+                         run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str): # <<< ADDED PARAMS\r\n+        \"\"\"\r\n+        Starts the asset processing task and optionally runs Blender scripts afterwards.\r\n+        This method should be called when the handler is moved to a separate thread.\r\n+        \"\"\"\r\n+        if self._is_running:\r\n+            log.warning(\"Processing is already running.\")\r\n+            self.status_message.emit(\"Processing already in progress.\", 3000)\r\n+            return\r\n+\r\n+        if not BACKEND_AVAILABLE or not process_single_asset_wrapper:\r\n+            log.error(\"Backend modules or worker function not available. Cannot start processing.\")\r\n+            self.status_message.emit(\"Error: Backend components missing. Cannot process.\", 5000)\r\n+            self.processing_finished.emit(0, 0, len(input_paths)) # Emit finished with all failed\r\n+            return\r\n+\r\n+        self._is_running = True\r\n+        self._cancel_requested = False\r\n+        self._futures = {} # Reset futures\r\n+        total_files = len(input_paths)\r\n+        processed_count = 0\r\n+        skipped_count = 0\r\n+        failed_count = 0\r\n+        completed_count = 0\r\n+\r\n+        log.info(f\"Starting processing run: {total_files} assets, Preset='{preset_name}', Workers={num_workers}, Overwrite={overwrite}\")\r\n+        self.status_message.emit(f\"Starting processing for {total_files} items...\", 0) # Persistent message\r\n+\r\n+        try:\r\n+            # Use 'with' statement for ProcessPoolExecutor for cleanup\r\n+            with ProcessPoolExecutor(max_workers=num_workers) as executor:\r\n+                self._executor = executor # Store for potential cancellation\r\n+\r\n+                # Submit tasks\r\n+                for input_path in input_paths:\r\n+                    if self._cancel_requested: break # Check before submitting more\r\n+                    log.debug(f\"Submitting task for: {input_path}\")\r\n+                    future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=False) # Pass verbose=False\r\n+                    self._futures[future] = input_path # Map future back to input path\r\n+                    # Optionally emit \"processing\" status here\r\n+                    self.file_status_updated.emit(input_path, \"processing\", \"\")\r\n+\r\n+                if self._cancel_requested:\r\n+                     log.info(\"Processing cancelled during task submission.\")\r\n+                     # Count remaining unsubmitted tasks as failed/cancelled\r\n+                     failed_count = total_files - len(self._futures)\r\n+\r\n+                # Process completed futures\r\n+                for future in as_completed(self._futures):\r\n+                    completed_count += 1\r\n+                    input_path = self._futures[future] # Get original path\r\n+                    asset_name = Path(input_path).name\r\n+                    status = \"failed\" # Default status\r\n+                    error_message = \"Unknown error\"\r\n+\r\n+                    if self._cancel_requested:\r\n+                        # If cancelled after submission, try to get result but count as failed\r\n+                        status = \"failed\"\r\n+                        error_message = \"Cancelled\"\r\n+                        failed_count += 1\r\n+                        # Don't try future.result() if cancelled, it might raise CancelledError\r\n+                    else:\r\n+                        try:\r\n+                            # Get result tuple: (input_path_str, status_string, error_message_or_None)\r\n+                            result_tuple = future.result()\r\n+                            _, status, error_message = result_tuple\r\n+                            error_message = error_message or \"\" # Ensure it's a string\r\n+\r\n+                            # Increment counters based on status\r\n+                            if status == \"processed\":\r\n+                                processed_count += 1\r\n+                            elif status == \"skipped\":\r\n+                                skipped_count += 1\r\n+                            elif status == \"failed\":\r\n+                                failed_count += 1\r\n+                            else:\r\n+                                log.warning(f\"Unknown status '{status}' received for {asset_name}. Counting as failed.\")\r\n+                                failed_count += 1\r\n+                                error_message = f\"Unknown status: {status}\"\r\n+\r\n+                        except Exception as e:\r\n+                            # Catch errors if the future itself fails (e.g., worker process crashed hard)\r\n+                            log.exception(f\"Critical worker failure for {asset_name}: {e}\")\r\n+                            failed_count += 1 # Count crashes as failures\r\n+                            status = \"failed\"\r\n+                            error_message = f\"Worker process crashed: {e}\"\r\n+\r\n+                    # Emit progress signals\r\n+                    self.progress_updated.emit(completed_count, total_files)\r\n+                    self.file_status_updated.emit(input_path, status, error_message)\r\n+\r\n+                    # Check for cancellation again after processing each result\r\n+                    if self._cancel_requested:\r\n+                        log.info(\"Cancellation detected after processing a result.\")\r\n+                        # Count remaining unprocessed futures as failed/cancelled\r\n+                        remaining_futures = total_files - completed_count\r\n+                        failed_count += remaining_futures\r\n+                        break # Exit the as_completed loop\r\n+\r\n+        except Exception as pool_exc:\r\n+            log.exception(f\"An error occurred with the process pool: {pool_exc}\")\r\n+            self.status_message.emit(f\"Error during processing: {pool_exc}\", 5000)\r\n+            # Mark all remaining as failed\r\n+            failed_count = total_files - processed_count - skipped_count\r\n+\r\n+        finally:\r\n+            # --- Blender Script Execution (Optional) ---\r\n+            if run_blender and not self._cancel_requested:\r\n+                log.info(\"Asset processing complete. Checking for Blender script execution.\")\r\n+                self.status_message.emit(\"Asset processing complete. Starting Blender scripts...\", 0)\r\n+                blender_exe = self._find_blender_executable()\r\n+                if blender_exe:\r\n+                    script_dir = Path(__file__).parent.parent / \"blenderscripts\" # Go up one level from gui/\r\n+                    nodegroup_script_path = script_dir / \"create_nodegroups.py\"\r\n+                    materials_script_path = script_dir / \"create_materials.py\"\r\n+                    asset_output_root = output_dir_str # Use the same output dir\r\n+\r\n+                    # Run Nodegroup Script\r\n+                    if nodegroup_blend_path and Path(nodegroup_blend_path).is_file():\r\n+                        if nodegroup_script_path.is_file():\r\n+                            log.info(\"-\" * 20 + \" Running Nodegroup Script \" + \"-\" * 20)\r\n+                            self.status_message.emit(f\"Running Blender nodegroup script on {Path(nodegroup_blend_path).name}...\", 0)\r\n+                            success_ng = self._run_blender_script_subprocess(\r\n+                                blender_exe_path=blender_exe,\r\n+                                blend_file_path=nodegroup_blend_path,\r\n+                                python_script_path=str(nodegroup_script_path),\r\n+                                asset_root_dir=asset_output_root\r\n+                            )\r\n+                            if not success_ng:\r\n+                                log.error(\"Blender node group script execution failed.\")\r\n+                                self.status_message.emit(\"Blender nodegroup script failed.\", 5000)\r\n+                            else:\r\n+                                log.info(\"Blender nodegroup script finished successfully.\")\r\n+                                self.status_message.emit(\"Blender nodegroup script finished.\", 3000)\r\n+                        else:\r\n+                            log.error(f\"Node group script not found: {nodegroup_script_path}\")\r\n+                            self.status_message.emit(f\"Error: Nodegroup script not found.\", 5000)\r\n+                    elif run_blender and nodegroup_blend_path: # Log if path was provided but invalid\r\n+                         log.warning(f\"Nodegroup blend path provided but invalid: {nodegroup_blend_path}\")\r\n+                         self.status_message.emit(f\"Warning: Invalid Nodegroup .blend path.\", 5000)\r\n+\r\n+\r\n+                    # Run Materials Script (only if nodegroup script was attempted or not needed)\r\n+                    if materials_blend_path and Path(materials_blend_path).is_file():\r\n+                         if materials_script_path.is_file():\r\n+                            log.info(\"-\" * 20 + \" Running Materials Script \" + \"-\" * 20)\r\n+                            self.status_message.emit(f\"Running Blender materials script on {Path(materials_blend_path).name}...\", 0)\r\n+                            # Pass the nodegroup blend path as the second argument to the script\r\n+                            success_mat = self._run_blender_script_subprocess(\r\n+                                blender_exe_path=blender_exe,\r\n+                                blend_file_path=materials_blend_path,\r\n+                                python_script_path=str(materials_script_path),\r\n+                                asset_root_dir=asset_output_root,\r\n+                                nodegroup_blend_file_path_arg=nodegroup_blend_path # Pass the nodegroup path\r\n+                            )\r\n+                            if not success_mat:\r\n+                                log.error(\"Blender material script execution failed.\")\r\n+                                self.status_message.emit(\"Blender material script failed.\", 5000)\r\n+                            else:\r\n+                                log.info(\"Blender material script finished successfully.\")\r\n+                                self.status_message.emit(\"Blender material script finished.\", 3000)\r\n+                         else:\r\n+                            log.error(f\"Material script not found: {materials_script_path}\")\r\n+                            self.status_message.emit(f\"Error: Material script not found.\", 5000)\r\n+                    elif run_blender and materials_blend_path: # Log if path was provided but invalid\r\n+                         log.warning(f\"Materials blend path provided but invalid: {materials_blend_path}\")\r\n+                         self.status_message.emit(f\"Warning: Invalid Materials .blend path.\", 5000)\r\n+\r\n+                else:\r\n+                    log.warning(\"Blender executable not found. Skipping Blender script execution.\")\r\n+                    self.status_message.emit(\"Warning: Blender executable not found. Skipping scripts.\", 5000)\r\n+            elif self._cancel_requested:\r\n+                 log.info(\"Processing was cancelled. Skipping Blender script execution.\")\r\n+            # --- End Blender Script Execution ---\r\n+\r\n+            final_message = f\"Finished. Processed: {processed_count}, Skipped: {skipped_count}, Failed: {failed_count}\"\r\n+            log.info(final_message)\r\n+            self.status_message.emit(final_message, 5000) # Show final summary\r\n+            self.processing_finished.emit(processed_count, skipped_count, failed_count)\r\n+            self._is_running = False\r\n+            self._executor = None\r\n+            self._futures = {} # Clear futures\r\n+\r\n+    def request_cancel(self):\r\n+        \"\"\"Requests cancellation of the ongoing processing task.\"\"\"\r\n+        if not self._is_running:\r\n+            log.warning(\"Cancel requested but no processing is running.\")\r\n+            return\r\n+\r\n+        if self._cancel_requested:\r\n+            log.warning(\"Cancellation already requested.\")\r\n+            return\r\n+\r\n+        log.info(\"Cancellation requested.\")\r\n+        self.status_message.emit(\"Cancellation requested...\", 3000)\r\n+        self._cancel_requested = True\r\n+\r\n+        # Attempt to shutdown the executor - this might cancel pending tasks\r\n+        # but won't forcefully stop running ones. `cancel_futures=True` is Python 3.9+\r\n+        if self._executor:\r\n+             log.debug(\"Requesting executor shutdown...\")\r\n+             # For Python 3.9+: self._executor.shutdown(wait=False, cancel_futures=True)\r\n+             # For older Python:\r\n+             self._executor.shutdown(wait=False)\r\n+             # Manually try cancelling futures that haven't started\r\n+             for future in self._futures:\r\n+                 if not future.running() and not future.done():\r\n+                     future.cancel()\r\n+             log.debug(\"Executor shutdown requested.\")\r\n+\r\n+        # Note: True cancellation of running ProcessPoolExecutor tasks is complex.\r\n+        # This implementation primarily prevents processing further results and\r\n+        # attempts to cancel pending/unstarted tasks.\r\n+\r\n+    def _find_blender_executable(self) -> Optional[str]:\r\n+        \"\"\"Finds the Blender executable path from config or system PATH.\"\"\"\r\n+        try:\r\n+            blender_exe_config = getattr(core_config, 'BLENDER_EXECUTABLE_PATH', None)\r\n+            if blender_exe_config:\r\n+                p = Path(blender_exe_config)\r\n+                if p.is_file():\r\n+                    log.info(f\"Using Blender executable from config: {p}\")\r\n+                    return str(p.resolve())\r\n+                else:\r\n+                    log.warning(f\"Blender path in config not found: '{blender_exe_config}'. Trying PATH.\")\r\n+            else:\r\n+                 log.info(\"BLENDER_EXECUTABLE_PATH not set in config. Trying PATH.\")\r\n+\r\n+            blender_exe = shutil.which(\"blender\")\r\n+            if blender_exe:\r\n+                log.info(f\"Found Blender executable in PATH: {blender_exe}\")\r\n+                return blender_exe\r\n+            else:\r\n+                log.warning(\"Could not find 'blender' in system PATH.\")\r\n+                return None\r\n+        except Exception as e:\r\n+            log.error(f\"Error checking Blender executable path: {e}\")\r\n+            return None\r\n+\r\n+    def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str, nodegroup_blend_file_path_arg: Optional[str] = None) -> bool:\r\n+        \"\"\"Internal helper to run a single Blender script via subprocess.\"\"\"\r\n+        command_base = [\r\n+            blender_exe_path,\r\n+            \"--factory-startup\",\r\n+            \"-b\",\r\n+            blend_file_path,\r\n+            \"--log\", \"*\", # <<< ADDED BLENDER LOGGING FLAG\r\n+            \"--python\", python_script_path,\r\n+            \"--\",\r\n+            asset_root_dir,\r\n+        ]\r\n+        # Add nodegroup blend file path if provided (for create_materials script)\r\n+        if nodegroup_blend_file_path_arg:\r\n+            command = command_base + [nodegroup_blend_file_path_arg]\r\n+        else:\r\n+            command = command_base\r\n+        log.debug(f\"Executing Blender command: {' '.join(map(str, command))}\") # Ensure all parts are strings for join\r\n+        try:\r\n+            # Ensure all parts of the command are strings for subprocess\r\n+            str_command = [str(part) for part in command]\r\n+            result = subprocess.run(str_command, capture_output=True, text=True, check=False, encoding='utf-8') # Specify encoding\r\n+            log.info(f\"Blender script '{Path(python_script_path).name}' finished with exit code: {result.returncode}\")\r\n+            if result.stdout: log.debug(f\"Blender stdout:\\n{result.stdout.strip()}\")\r\n+            if result.stderr:\r\n+                if result.returncode != 0: log.error(f\"Blender stderr:\\n{result.stderr.strip()}\")\r\n+                else: log.warning(f\"Blender stderr (RC=0):\\n{result.stderr.strip()}\")\r\n+            return result.returncode == 0\r\n+        except FileNotFoundError:\r\n+            log.error(f\"Blender executable not found at: {blender_exe_path}\")\r\n+            return False\r\n+        except Exception as e:\r\n+            log.exception(f\"Error running Blender script '{Path(python_script_path).name}': {e}\")\r\n+            return False\n\\ No newline at end of file\n"
                },
                {
                    "date": 1745314372244,
                    "content": "Index: \n===================================================================\n--- \n+++ \n@@ -69,9 +69,9 @@\n     def is_running(self):\r\n         return self._is_running\r\n \r\n     def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,\r\n-                         run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str): # <<< ADDED PARAMS\r\n+                         run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str, verbose: bool): # <<< ADDED verbose PARAM\r\n         \"\"\"\r\n         Starts the asset processing task and optionally runs Blender scripts afterwards.\r\n         This method should be called when the handler is moved to a separate thread.\r\n         \"\"\"\r\n@@ -106,9 +106,9 @@\n                 # Submit tasks\r\n                 for input_path in input_paths:\r\n                     if self._cancel_requested: break # Check before submitting more\r\n                     log.debug(f\"Submitting task for: {input_path}\")\r\n-                    future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=False) # Pass verbose=False\r\n+                    future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=verbose) # Pass verbose flag from GUI\r\n                     self._futures[future] = input_path # Map future back to input path\r\n                     # Optionally emit \"processing\" status here\r\n                     self.file_status_updated.emit(input_path, \"processing\", \"\")\r\n \r\n@@ -341,350 +341,5 @@\n             log.error(f\"Blender executable not found at: {blender_exe_path}\")\r\n             return False\r\n         except Exception as e:\r\n             log.exception(f\"Error running Blender script '{Path(python_script_path).name}': {e}\")\r\n-            return False\n-# gui/processing_handler.py\r\n-import logging\r\n-from pathlib import Path\r\n-from concurrent.futures import ProcessPoolExecutor, as_completed\r\n-import time # For potential delays if needed\r\n-\r\n-import subprocess # <<< ADDED IMPORT\r\n-import shutil # <<< ADDED IMPORT\r\n-from typing import Optional # <<< ADDED IMPORT\r\n-\r\n-# --- PySide6 Imports ---\r\n-# Inherit from QObject to support signals/slots for thread communication\r\n-from PySide6.QtCore import QObject, Signal\r\n-\r\n-# --- Backend Imports ---\r\n-# Need to import the worker function and potentially config/processor if needed directly\r\n-# Adjust path to ensure modules can be found relative to this file's location\r\n-import sys\r\n-script_dir = Path(__file__).parent\r\n-project_root = script_dir.parent\r\n-if str(project_root) not in sys.path:\r\n-    sys.path.insert(0, str(project_root))\r\n-\r\n-try:\r\n-    # Import the worker function from main.py\r\n-    from main import process_single_asset_wrapper\r\n-    # Import exceptions if needed for type hinting or specific handling\r\n-    from configuration import ConfigurationError\r\n-    from asset_processor import AssetProcessingError\r\n-    import config as core_config # <<< ADDED IMPORT\r\n-    BACKEND_AVAILABLE = True\r\n-except ImportError as e:\r\n-    print(f\"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}\")\r\n-    # Define placeholders if imports fail, so the GUI doesn't crash immediately\r\n-    process_single_asset_wrapper = None\r\n-    ConfigurationError = Exception\r\n-    AssetProcessingError = Exception\r\n-    BACKEND_AVAILABLE = False\r\n-\r\n-log = logging.getLogger(__name__)\r\n-# Basic config if logger hasn't been set up elsewhere\r\n-if not log.hasHandlers():\r\n-     logging.basicConfig(level=logging.INFO, format='%(levelname)s (Handler): %(message)s')\r\n-\r\n-\r\n-class ProcessingHandler(QObject):\r\n-    \"\"\"\r\n-    Handles the execution of the asset processing pipeline in a way that\r\n-    can be run in a separate thread and communicate progress via signals.\r\n-    \"\"\"\r\n-    # --- Signals ---\r\n-    # Emitted for overall progress bar update\r\n-    progress_updated = Signal(int, int) # current_count, total_count\r\n-    # Emitted for updating status of individual files in the list\r\n-    file_status_updated = Signal(str, str, str) # input_path_str, status (\"processing\", \"processed\", \"skipped\", \"failed\"), message\r\n-    # Emitted when the entire batch processing is finished\r\n-    processing_finished = Signal(int, int, int) # processed_count, skipped_count, failed_count\r\n-    # Emitted for general status messages to the status bar\r\n-    status_message = Signal(str, int) # message, timeout_ms\r\n-\r\n-    def __init__(self, parent=None):\r\n-        super().__init__(parent)\r\n-        self._executor = None\r\n-        self._futures = {} # Store future->input_path mapping\r\n-        self._is_running = False\r\n-        self._cancel_requested = False\r\n-\r\n-    @property\r\n-    def is_running(self):\r\n-        return self._is_running\r\n-\r\n-    def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,\r\n-                         run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str): # <<< ADDED PARAMS\r\n-        \"\"\"\r\n-        Starts the asset processing task and optionally runs Blender scripts afterwards.\r\n-        This method should be called when the handler is moved to a separate thread.\r\n-        \"\"\"\r\n-        if self._is_running:\r\n-            log.warning(\"Processing is already running.\")\r\n-            self.status_message.emit(\"Processing already in progress.\", 3000)\r\n-            return\r\n-\r\n-        if not BACKEND_AVAILABLE or not process_single_asset_wrapper:\r\n-            log.error(\"Backend modules or worker function not available. Cannot start processing.\")\r\n-            self.status_message.emit(\"Error: Backend components missing. Cannot process.\", 5000)\r\n-            self.processing_finished.emit(0, 0, len(input_paths)) # Emit finished with all failed\r\n-            return\r\n-\r\n-        self._is_running = True\r\n-        self._cancel_requested = False\r\n-        self._futures = {} # Reset futures\r\n-        total_files = len(input_paths)\r\n-        processed_count = 0\r\n-        skipped_count = 0\r\n-        failed_count = 0\r\n-        completed_count = 0\r\n-\r\n-        log.info(f\"Starting processing run: {total_files} assets, Preset='{preset_name}', Workers={num_workers}, Overwrite={overwrite}\")\r\n-        self.status_message.emit(f\"Starting processing for {total_files} items...\", 0) # Persistent message\r\n-\r\n-        try:\r\n-            # Use 'with' statement for ProcessPoolExecutor for cleanup\r\n-            with ProcessPoolExecutor(max_workers=num_workers) as executor:\r\n-                self._executor = executor # Store for potential cancellation\r\n-\r\n-                # Submit tasks\r\n-                for input_path in input_paths:\r\n-                    if self._cancel_requested: break # Check before submitting more\r\n-                    log.debug(f\"Submitting task for: {input_path}\")\r\n-                    future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=False) # Pass verbose=False\r\n-                    self._futures[future] = input_path # Map future back to input path\r\n-                    # Optionally emit \"processing\" status here\r\n-                    self.file_status_updated.emit(input_path, \"processing\", \"\")\r\n-\r\n-                if self._cancel_requested:\r\n-                     log.info(\"Processing cancelled during task submission.\")\r\n-                     # Count remaining unsubmitted tasks as failed/cancelled\r\n-                     failed_count = total_files - len(self._futures)\r\n-\r\n-                # Process completed futures\r\n-                for future in as_completed(self._futures):\r\n-                    completed_count += 1\r\n-                    input_path = self._futures[future] # Get original path\r\n-                    asset_name = Path(input_path).name\r\n-                    status = \"failed\" # Default status\r\n-                    error_message = \"Unknown error\"\r\n-\r\n-                    if self._cancel_requested:\r\n-                        # If cancelled after submission, try to get result but count as failed\r\n-                        status = \"failed\"\r\n-                        error_message = \"Cancelled\"\r\n-                        failed_count += 1\r\n-                        # Don't try future.result() if cancelled, it might raise CancelledError\r\n-                    else:\r\n-                        try:\r\n-                            # Get result tuple: (input_path_str, status_string, error_message_or_None)\r\n-                            result_tuple = future.result()\r\n-                            _, status, error_message = result_tuple\r\n-                            error_message = error_message or \"\" # Ensure it's a string\r\n-\r\n-                            # Increment counters based on status\r\n-                            if status == \"processed\":\r\n-                                processed_count += 1\r\n-                            elif status == \"skipped\":\r\n-                                skipped_count += 1\r\n-                            elif status == \"failed\":\r\n-                                failed_count += 1\r\n-                            else:\r\n-                                log.warning(f\"Unknown status '{status}' received for {asset_name}. Counting as failed.\")\r\n-                                failed_count += 1\r\n-                                error_message = f\"Unknown status: {status}\"\r\n-\r\n-                        except Exception as e:\r\n-                            # Catch errors if the future itself fails (e.g., worker process crashed hard)\r\n-                            log.exception(f\"Critical worker failure for {asset_name}: {e}\")\r\n-                            failed_count += 1 # Count crashes as failures\r\n-                            status = \"failed\"\r\n-                            error_message = f\"Worker process crashed: {e}\"\r\n-\r\n-                    # Emit progress signals\r\n-                    self.progress_updated.emit(completed_count, total_files)\r\n-                    self.file_status_updated.emit(input_path, status, error_message)\r\n-\r\n-                    # Check for cancellation again after processing each result\r\n-                    if self._cancel_requested:\r\n-                        log.info(\"Cancellation detected after processing a result.\")\r\n-                        # Count remaining unprocessed futures as failed/cancelled\r\n-                        remaining_futures = total_files - completed_count\r\n-                        failed_count += remaining_futures\r\n-                        break # Exit the as_completed loop\r\n-\r\n-        except Exception as pool_exc:\r\n-            log.exception(f\"An error occurred with the process pool: {pool_exc}\")\r\n-            self.status_message.emit(f\"Error during processing: {pool_exc}\", 5000)\r\n-            # Mark all remaining as failed\r\n-            failed_count = total_files - processed_count - skipped_count\r\n-\r\n-        finally:\r\n-            # --- Blender Script Execution (Optional) ---\r\n-            if run_blender and not self._cancel_requested:\r\n-                log.info(\"Asset processing complete. Checking for Blender script execution.\")\r\n-                self.status_message.emit(\"Asset processing complete. Starting Blender scripts...\", 0)\r\n-                blender_exe = self._find_blender_executable()\r\n-                if blender_exe:\r\n-                    script_dir = Path(__file__).parent.parent / \"blenderscripts\" # Go up one level from gui/\r\n-                    nodegroup_script_path = script_dir / \"create_nodegroups.py\"\r\n-                    materials_script_path = script_dir / \"create_materials.py\"\r\n-                    asset_output_root = output_dir_str # Use the same output dir\r\n-\r\n-                    # Run Nodegroup Script\r\n-                    if nodegroup_blend_path and Path(nodegroup_blend_path).is_file():\r\n-                        if nodegroup_script_path.is_file():\r\n-                            log.info(\"-\" * 20 + \" Running Nodegroup Script \" + \"-\" * 20)\r\n-                            self.status_message.emit(f\"Running Blender nodegroup script on {Path(nodegroup_blend_path).name}...\", 0)\r\n-                            success_ng = self._run_blender_script_subprocess(\r\n-                                blender_exe_path=blender_exe,\r\n-                                blend_file_path=nodegroup_blend_path,\r\n-                                python_script_path=str(nodegroup_script_path),\r\n-                                asset_root_dir=asset_output_root\r\n-                            )\r\n-                            if not success_ng:\r\n-                                log.error(\"Blender node group script execution failed.\")\r\n-                                self.status_message.emit(\"Blender nodegroup script failed.\", 5000)\r\n-                            else:\r\n-                                log.info(\"Blender nodegroup script finished successfully.\")\r\n-                                self.status_message.emit(\"Blender nodegroup script finished.\", 3000)\r\n-                        else:\r\n-                            log.error(f\"Node group script not found: {nodegroup_script_path}\")\r\n-                            self.status_message.emit(f\"Error: Nodegroup script not found.\", 5000)\r\n-                    elif run_blender and nodegroup_blend_path: # Log if path was provided but invalid\r\n-                         log.warning(f\"Nodegroup blend path provided but invalid: {nodegroup_blend_path}\")\r\n-                         self.status_message.emit(f\"Warning: Invalid Nodegroup .blend path.\", 5000)\r\n-\r\n-\r\n-                    # Run Materials Script (only if nodegroup script was attempted or not needed)\r\n-                    if materials_blend_path and Path(materials_blend_path).is_file():\r\n-                         if materials_script_path.is_file():\r\n-                            log.info(\"-\" * 20 + \" Running Materials Script \" + \"-\" * 20)\r\n-                            self.status_message.emit(f\"Running Blender materials script on {Path(materials_blend_path).name}...\", 0)\r\n-                            # Pass the nodegroup blend path as the second argument to the script\r\n-                            success_mat = self._run_blender_script_subprocess(\r\n-                                blender_exe_path=blender_exe,\r\n-                                blend_file_path=materials_blend_path,\r\n-                                python_script_path=str(materials_script_path),\r\n-                                asset_root_dir=asset_output_root,\r\n-                                nodegroup_blend_file_path_arg=nodegroup_blend_path # Pass the nodegroup path\r\n-                            )\r\n-                            if not success_mat:\r\n-                                log.error(\"Blender material script execution failed.\")\r\n-                                self.status_message.emit(\"Blender material script failed.\", 5000)\r\n-                            else:\r\n-                                log.info(\"Blender material script finished successfully.\")\r\n-                                self.status_message.emit(\"Blender material script finished.\", 3000)\r\n-                         else:\r\n-                            log.error(f\"Material script not found: {materials_script_path}\")\r\n-                            self.status_message.emit(f\"Error: Material script not found.\", 5000)\r\n-                    elif run_blender and materials_blend_path: # Log if path was provided but invalid\r\n-                         log.warning(f\"Materials blend path provided but invalid: {materials_blend_path}\")\r\n-                         self.status_message.emit(f\"Warning: Invalid Materials .blend path.\", 5000)\r\n-\r\n-                else:\r\n-                    log.warning(\"Blender executable not found. Skipping Blender script execution.\")\r\n-                    self.status_message.emit(\"Warning: Blender executable not found. Skipping scripts.\", 5000)\r\n-            elif self._cancel_requested:\r\n-                 log.info(\"Processing was cancelled. Skipping Blender script execution.\")\r\n-            # --- End Blender Script Execution ---\r\n-\r\n-            final_message = f\"Finished. Processed: {processed_count}, Skipped: {skipped_count}, Failed: {failed_count}\"\r\n-            log.info(final_message)\r\n-            self.status_message.emit(final_message, 5000) # Show final summary\r\n-            self.processing_finished.emit(processed_count, skipped_count, failed_count)\r\n-            self._is_running = False\r\n-            self._executor = None\r\n-            self._futures = {} # Clear futures\r\n-\r\n-    def request_cancel(self):\r\n-        \"\"\"Requests cancellation of the ongoing processing task.\"\"\"\r\n-        if not self._is_running:\r\n-            log.warning(\"Cancel requested but no processing is running.\")\r\n-            return\r\n-\r\n-        if self._cancel_requested:\r\n-            log.warning(\"Cancellation already requested.\")\r\n-            return\r\n-\r\n-        log.info(\"Cancellation requested.\")\r\n-        self.status_message.emit(\"Cancellation requested...\", 3000)\r\n-        self._cancel_requested = True\r\n-\r\n-        # Attempt to shutdown the executor - this might cancel pending tasks\r\n-        # but won't forcefully stop running ones. `cancel_futures=True` is Python 3.9+\r\n-        if self._executor:\r\n-             log.debug(\"Requesting executor shutdown...\")\r\n-             # For Python 3.9+: self._executor.shutdown(wait=False, cancel_futures=True)\r\n-             # For older Python:\r\n-             self._executor.shutdown(wait=False)\r\n-             # Manually try cancelling futures that haven't started\r\n-             for future in self._futures:\r\n-                 if not future.running() and not future.done():\r\n-                     future.cancel()\r\n-             log.debug(\"Executor shutdown requested.\")\r\n-\r\n-        # Note: True cancellation of running ProcessPoolExecutor tasks is complex.\r\n-        # This implementation primarily prevents processing further results and\r\n-        # attempts to cancel pending/unstarted tasks.\r\n-\r\n-    def _find_blender_executable(self) -> Optional[str]:\r\n-        \"\"\"Finds the Blender executable path from config or system PATH.\"\"\"\r\n-        try:\r\n-            blender_exe_config = getattr(core_config, 'BLENDER_EXECUTABLE_PATH', None)\r\n-            if blender_exe_config:\r\n-                p = Path(blender_exe_config)\r\n-                if p.is_file():\r\n-                    log.info(f\"Using Blender executable from config: {p}\")\r\n-                    return str(p.resolve())\r\n-                else:\r\n-                    log.warning(f\"Blender path in config not found: '{blender_exe_config}'. Trying PATH.\")\r\n-            else:\r\n-                 log.info(\"BLENDER_EXECUTABLE_PATH not set in config. Trying PATH.\")\r\n-\r\n-            blender_exe = shutil.which(\"blender\")\r\n-            if blender_exe:\r\n-                log.info(f\"Found Blender executable in PATH: {blender_exe}\")\r\n-                return blender_exe\r\n-            else:\r\n-                log.warning(\"Could not find 'blender' in system PATH.\")\r\n-                return None\r\n-        except Exception as e:\r\n-            log.error(f\"Error checking Blender executable path: {e}\")\r\n-            return None\r\n-\r\n-    def _run_blender_script_subprocess(self, blender_exe_path: str, blend_file_path: str, python_script_path: str, asset_root_dir: str, nodegroup_blend_file_path_arg: Optional[str] = None) -> bool:\r\n-        \"\"\"Internal helper to run a single Blender script via subprocess.\"\"\"\r\n-        command_base = [\r\n-            blender_exe_path,\r\n-            \"--factory-startup\",\r\n-            \"-b\",\r\n-            blend_file_path,\r\n-            \"--log\", \"*\", # <<< ADDED BLENDER LOGGING FLAG\r\n-            \"--python\", python_script_path,\r\n-            \"--\",\r\n-            asset_root_dir,\r\n-        ]\r\n-        # Add nodegroup blend file path if provided (for create_materials script)\r\n-        if nodegroup_blend_file_path_arg:\r\n-            command = command_base + [nodegroup_blend_file_path_arg]\r\n-        else:\r\n-            command = command_base\r\n-        log.debug(f\"Executing Blender command: {' '.join(map(str, command))}\") # Ensure all parts are strings for join\r\n-        try:\r\n-            # Ensure all parts of the command are strings for subprocess\r\n-            str_command = [str(part) for part in command]\r\n-            result = subprocess.run(str_command, capture_output=True, text=True, check=False, encoding='utf-8') # Specify encoding\r\n-            log.info(f\"Blender script '{Path(python_script_path).name}' finished with exit code: {result.returncode}\")\r\n-            if result.stdout: log.debug(f\"Blender stdout:\\n{result.stdout.strip()}\")\r\n-            if result.stderr:\r\n-                if result.returncode != 0: log.error(f\"Blender stderr:\\n{result.stderr.strip()}\")\r\n-                else: log.warning(f\"Blender stderr (RC=0):\\n{result.stderr.strip()}\")\r\n-            return result.returncode == 0\r\n-        except FileNotFoundError:\r\n-            log.error(f\"Blender executable not found at: {blender_exe_path}\")\r\n-            return False\r\n-        except Exception as e:\r\n-            log.exception(f\"Error running Blender script '{Path(python_script_path).name}': {e}\")\r\n             return False\n\\ No newline at end of file\n"
                }
            ],
            "date": 1745236414824,
            "name": "Commit-0",
            "content": "# gui/processing_handler.py\r\nimport logging\r\nfrom pathlib import Path\r\nfrom concurrent.futures import ProcessPoolExecutor, as_completed\r\nimport time # For potential delays if needed\r\n\r\n# --- PySide6 Imports ---\r\n# Inherit from QObject to support signals/slots for thread communication\r\nfrom PySide6.QtCore import QObject, Signal\r\n\r\n# --- Backend Imports ---\r\n# Need to import the worker function and potentially config/processor if needed directly\r\n# Adjust path to ensure modules can be found relative to this file's location\r\nimport sys\r\nscript_dir = Path(__file__).parent\r\nproject_root = script_dir.parent\r\nif str(project_root) not in sys.path:\r\n    sys.path.insert(0, str(project_root))\r\n\r\ntry:\r\n    # Import the worker function from main.py\r\n    from main import process_single_asset_wrapper\r\n    # Import exceptions if needed for type hinting or specific handling\r\n    from configuration import ConfigurationError\r\n    from asset_processor import AssetProcessingError\r\n    BACKEND_AVAILABLE = True\r\nexcept ImportError as e:\r\n    print(f\"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}\")\r\n    # Define placeholders if imports fail, so the GUI doesn't crash immediately\r\n    process_single_asset_wrapper = None\r\n    ConfigurationError = Exception\r\n    AssetProcessingError = Exception\r\n    BACKEND_AVAILABLE = False\r\n\r\nlog = logging.getLogger(__name__)\r\n# Basic config if logger hasn't been set up elsewhere\r\nif not log.hasHandlers():\r\n     logging.basicConfig(level=logging.INFO, format='%(levelname)s (Handler): %(message)s')\r\n\r\n\r\nclass ProcessingHandler(QObject):\r\n    \"\"\"\r\n    Handles the execution of the asset processing pipeline in a way that\r\n    can be run in a separate thread and communicate progress via signals.\r\n    \"\"\"\r\n    # --- Signals ---\r\n    # Emitted for overall progress bar update\r\n    progress_updated = Signal(int, int) # current_count, total_count\r\n    # Emitted for updating status of individual files in the list\r\n    file_status_updated = Signal(str, str, str) # input_path_str, status (\"processing\", \"processed\", \"skipped\", \"failed\"), message\r\n    # Emitted when the entire batch processing is finished\r\n    processing_finished = Signal(int, int, int) # processed_count, skipped_count, failed_count\r\n    # Emitted for general status messages to the status bar\r\n    status_message = Signal(str, int) # message, timeout_ms\r\n\r\n    def __init__(self, parent=None):\r\n        super().__init__(parent)\r\n        self._executor = None\r\n        self._futures = {} # Store future->input_path mapping\r\n        self._is_running = False\r\n        self._cancel_requested = False\r\n\r\n    @property\r\n    def is_running(self):\r\n        return self._is_running\r\n\r\n    def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int):\r\n        \"\"\"\r\n        Starts the asset processing task. This method should be called\r\n        when the handler is moved to a separate thread.\r\n        \"\"\"\r\n        if self._is_running:\r\n            log.warning(\"Processing is already running.\")\r\n            self.status_message.emit(\"Processing already in progress.\", 3000)\r\n            return\r\n\r\n        if not BACKEND_AVAILABLE or not process_single_asset_wrapper:\r\n            log.error(\"Backend modules or worker function not available. Cannot start processing.\")\r\n            self.status_message.emit(\"Error: Backend components missing. Cannot process.\", 5000)\r\n            self.processing_finished.emit(0, 0, len(input_paths)) # Emit finished with all failed\r\n            return\r\n\r\n        self._is_running = True\r\n        self._cancel_requested = False\r\n        self._futures = {} # Reset futures\r\n        total_files = len(input_paths)\r\n        processed_count = 0\r\n        skipped_count = 0\r\n        failed_count = 0\r\n        completed_count = 0\r\n\r\n        log.info(f\"Starting processing run: {total_files} assets, Preset='{preset_name}', Workers={num_workers}, Overwrite={overwrite}\")\r\n        self.status_message.emit(f\"Starting processing for {total_files} items...\", 0) # Persistent message\r\n\r\n        try:\r\n            # Use 'with' statement for ProcessPoolExecutor for cleanup\r\n            with ProcessPoolExecutor(max_workers=num_workers) as executor:\r\n                self._executor = executor # Store for potential cancellation\r\n\r\n                # Submit tasks\r\n                for input_path in input_paths:\r\n                    if self._cancel_requested: break # Check before submitting more\r\n                    log.debug(f\"Submitting task for: {input_path}\")\r\n                    future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=False) # Pass verbose=False\r\n                    self._futures[future] = input_path # Map future back to input path\r\n                    # Optionally emit \"processing\" status here\r\n                    self.file_status_updated.emit(input_path, \"processing\", \"\")\r\n\r\n                if self._cancel_requested:\r\n                     log.info(\"Processing cancelled during task submission.\")\r\n                     # Count remaining unsubmitted tasks as failed/cancelled\r\n                     failed_count = total_files - len(self._futures)\r\n\r\n                # Process completed futures\r\n                for future in as_completed(self._futures):\r\n                    completed_count += 1\r\n                    input_path = self._futures[future] # Get original path\r\n                    asset_name = Path(input_path).name\r\n                    status = \"failed\" # Default status\r\n                    error_message = \"Unknown error\"\r\n\r\n                    if self._cancel_requested:\r\n                        # If cancelled after submission, try to get result but count as failed\r\n                        status = \"failed\"\r\n                        error_message = \"Cancelled\"\r\n                        failed_count += 1\r\n                        # Don't try future.result() if cancelled, it might raise CancelledError\r\n                    else:\r\n                        try:\r\n                            # Get result tuple: (input_path_str, status_string, error_message_or_None)\r\n                            result_tuple = future.result()\r\n                            _, status, error_message = result_tuple\r\n                            error_message = error_message or \"\" # Ensure it's a string\r\n\r\n                            # Increment counters based on status\r\n                            if status == \"processed\":\r\n                                processed_count += 1\r\n                            elif status == \"skipped\":\r\n                                skipped_count += 1\r\n                            elif status == \"failed\":\r\n                                failed_count += 1\r\n                            else:\r\n                                log.warning(f\"Unknown status '{status}' received for {asset_name}. Counting as failed.\")\r\n                                failed_count += 1\r\n                                error_message = f\"Unknown status: {status}\"\r\n\r\n                        except Exception as e:\r\n                            # Catch errors if the future itself fails (e.g., worker process crashed hard)\r\n                            log.exception(f\"Critical worker failure for {asset_name}: {e}\")\r\n                            failed_count += 1 # Count crashes as failures\r\n                            status = \"failed\"\r\n                            error_message = f\"Worker process crashed: {e}\"\r\n\r\n                    # Emit progress signals\r\n                    self.progress_updated.emit(completed_count, total_files)\r\n                    self.file_status_updated.emit(input_path, status, error_message)\r\n\r\n                    # Check for cancellation again after processing each result\r\n                    if self._cancel_requested:\r\n                        log.info(\"Cancellation detected after processing a result.\")\r\n                        # Count remaining unprocessed futures as failed/cancelled\r\n                        remaining_futures = total_files - completed_count\r\n                        failed_count += remaining_futures\r\n                        break # Exit the as_completed loop\r\n\r\n        except Exception as pool_exc:\r\n            log.exception(f\"An error occurred with the process pool: {pool_exc}\")\r\n            self.status_message.emit(f\"Error during processing: {pool_exc}\", 5000)\r\n            # Mark all remaining as failed\r\n            failed_count = total_files - processed_count - skipped_count\r\n\r\n        finally:\r\n            final_message = f\"Processing finished. Processed: {processed_count}, Skipped: {skipped_count}, Failed: {failed_count}\"\r\n            log.info(final_message)\r\n            self.status_message.emit(final_message, 5000)\r\n            self.processing_finished.emit(processed_count, skipped_count, failed_count)\r\n            self._is_running = False\r\n            self._executor = None\r\n            self._futures = {} # Clear futures\r\n\r\n    def request_cancel(self):\r\n        \"\"\"Requests cancellation of the ongoing processing task.\"\"\"\r\n        if not self._is_running:\r\n            log.warning(\"Cancel requested but no processing is running.\")\r\n            return\r\n\r\n        if self._cancel_requested:\r\n            log.warning(\"Cancellation already requested.\")\r\n            return\r\n\r\n        log.info(\"Cancellation requested.\")\r\n        self.status_message.emit(\"Cancellation requested...\", 3000)\r\n        self._cancel_requested = True\r\n\r\n        # Attempt to shutdown the executor - this might cancel pending tasks\r\n        # but won't forcefully stop running ones. `cancel_futures=True` is Python 3.9+\r\n        if self._executor:\r\n             log.debug(\"Requesting executor shutdown...\")\r\n             # For Python 3.9+: self._executor.shutdown(wait=False, cancel_futures=True)\r\n             # For older Python:\r\n             self._executor.shutdown(wait=False)\r\n             # Manually try cancelling futures that haven't started\r\n             for future in self._futures:\r\n                 if not future.running() and not future.done():\r\n                     future.cancel()\r\n             log.debug(\"Executor shutdown requested.\")\r\n\r\n        # Note: True cancellation of running ProcessPoolExecutor tasks is complex.\r\n        # This implementation primarily prevents processing further results and\r\n        # attempts to cancel pending/unstarted tasks."
        }
    ]
}