Asset-Frameworker/.lh/gui/processing_handler.py.json
2025-04-29 18:26:13 +02:00

50 lines
70 KiB
JSON

{
"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."
}
]
}