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

38 lines
44 KiB
JSON

{
"sourceFile": "gui/prediction_handler.py",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 5,
"patches": [
{
"date": 1745236106567,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1745317773906,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -40,9 +40,9 @@\n Handles running predictions in a separate thread to avoid GUI freezes.\r\n \"\"\"\r\n # --- Signals ---\r\n # Emits a list of dictionaries, each representing a file row for the table\r\n- # Dict format: {'original_path': str, 'predicted_name': str | None, 'status': str, 'details': str | None, 'source_asset': str}\r\n+ # Dict format: {'original_path': str, 'predicted_asset_name': str | None, 'predicted_output_name': str | None, 'status': str, 'details': str | None, 'source_asset': str}\r\n prediction_results_ready = Signal(list)\r\n # Emitted when all predictions for a batch are done\r\n prediction_finished = Signal()\r\n # Emitted for status updates\r\n@@ -78,34 +78,45 @@\n log.error(f\"Detailed prediction failed critically for {input_path_str}. Adding asset-level error.\")\r\n # Add a single error entry for the whole asset if the method returns None\r\n asset_results.append({\r\n 'original_path': source_asset_name, # Use asset name as placeholder\r\n- 'predicted_name': None,\r\n+ 'predicted_asset_name': None, # New key\r\n+ 'predicted_output_name': None, # New key\r\n 'status': 'Error',\r\n 'details': 'Critical prediction failure (check logs)',\r\n 'source_asset': source_asset_name\r\n })\r\n else:\r\n log.debug(f\"Received {len(detailed_predictions)} detailed predictions for {input_path_str}.\")\r\n- # Add source_asset key to each dictionary and append to the main list\r\n+ # Add source_asset key and ensure correct keys exist\r\n for prediction_dict in detailed_predictions:\r\n- prediction_dict['source_asset'] = source_asset_name\r\n- asset_results.append(prediction_dict)\r\n+ # Ensure all expected keys are present, even if None\r\n+ result_entry = {\r\n+ 'original_path': prediction_dict.get('original_path', '[Missing Path]'),\r\n+ 'predicted_asset_name': prediction_dict.get('predicted_asset_name'), # New key\r\n+ 'predicted_output_name': prediction_dict.get('predicted_output_name'), # New key\r\n+ 'status': prediction_dict.get('status', 'Error'),\r\n+ 'details': prediction_dict.get('details', '[Missing Details]'),\r\n+ 'source_asset': source_asset_name # Add the source asset identifier\r\n+ }\r\n+ asset_results.append(result_entry)\r\n \r\n except AssetProcessingError as e: # Catch errors during processor instantiation or prediction setup\r\n log.error(f\"Asset processing error during prediction setup for {input_path_str}: {e}\")\r\n asset_results.append({\r\n 'original_path': source_asset_name,\r\n- 'predicted_name': None,\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n 'status': 'Error',\r\n 'details': f'Asset Error: {e}',\r\n 'source_asset': source_asset_name\r\n })\r\n except Exception as e: # Catch unexpected errors\r\n log.exception(f\"Unexpected error during prediction for {input_path_str}: {e}\")\r\n asset_results.append({\r\n 'original_path': source_asset_name,\r\n- 'predicted_name': None,\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n 'status': 'Error',\r\n 'details': f'Unexpected Error: {e}',\r\n 'source_asset': source_asset_name\r\n })\r\n@@ -180,9 +191,10 @@\n # We might not know which input path failed here easily without more mapping\r\n # Add a generic error?\r\n all_file_results.append({\r\n 'original_path': '[Unknown Asset - Executor Error]',\r\n- 'predicted_name': None,\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n 'status': 'Error',\r\n 'details': f'Executor Error: {exc}',\r\n 'source_asset': '[Unknown]'\r\n })\r\n@@ -192,9 +204,10 @@\n self.status_message.emit(f\"Error during prediction setup: {pool_exc}\", 5000)\r\n # Add a generic error if the pool fails\r\n all_file_results.append({\r\n 'original_path': '[Prediction Pool Error]',\r\n- 'predicted_name': None,\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n 'status': 'Error',\r\n 'details': f'Pool Error: {pool_exc}',\r\n 'source_asset': '[System]'\r\n })\r\n"
},
{
"date": 1745332997337,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,228 @@\n+# gui/prediction_handler.py\r\n+import logging\r\n+from pathlib import Path\r\n+import time # For potential delays if needed\r\n+import os # For cpu_count\r\n+from concurrent.futures import ThreadPoolExecutor, as_completed # For parallel prediction\r\n+\r\n+# --- PySide6 Imports ---\r\n+from PySide6.QtCore import QObject, Signal\r\n+\r\n+# --- Backend Imports ---\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+ from configuration import Configuration, ConfigurationError\r\n+ from asset_processor import AssetProcessor, AssetProcessingError\r\n+ BACKEND_AVAILABLE = True\r\n+except ImportError as e:\r\n+ print(f\"ERROR (PredictionHandler): Failed to import backend modules: {e}\")\r\n+ # Define placeholders if imports fail\r\n+ Configuration = None\r\n+ AssetProcessor = 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 (PredictHandler): %(message)s')\r\n+\r\n+\r\n+class PredictionHandler(QObject):\r\n+ \"\"\"\r\n+ Handles running predictions in a separate thread to avoid GUI freezes.\r\n+ \"\"\"\r\n+ # --- Signals ---\r\n+ # Emits a list of dictionaries, each representing a file row for the table\r\n+ # Dict format: {'original_path': str, 'predicted_asset_name': str | None, 'predicted_output_name': str | None, 'status': str, 'details': str | None, 'source_asset': str}\r\n+ prediction_results_ready = Signal(list)\r\n+ # Emitted when all predictions for a batch are done\r\n+ prediction_finished = Signal()\r\n+ # Emitted for status updates\r\n+ status_message = Signal(str, int)\r\n+\r\n+ def __init__(self, parent=None):\r\n+ super().__init__(parent)\r\n+ self._is_running = False\r\n+ # No explicit cancel needed for prediction for now, it should be fast per-item\r\n+\r\n+ @property\r\n+ def is_running(self):\r\n+ return self._is_running\r\n+\r\n+ def _predict_single_asset(self, input_path_str: str, config: Configuration) -> list[dict]:\r\n+ \"\"\"\r\n+ Helper method to predict a single asset. Runs within the ThreadPoolExecutor.\r\n+ Returns a list of prediction dictionaries for the asset, or a single error dict.\r\n+ \"\"\"\r\n+ input_path = Path(input_path_str)\r\n+ source_asset_name = input_path.name # For reference in the results\r\n+ asset_results = []\r\n+ try:\r\n+ # Create AssetProcessor instance (needs dummy output path)\r\n+ # Ensure AssetProcessor is thread-safe or create a new instance per thread.\r\n+ # Based on its structure (using temp dirs), creating new instances should be safe.\r\n+ processor = AssetProcessor(input_path, config, Path(\".\")) # Dummy output path\r\n+\r\n+ # Get detailed file predictions\r\n+ detailed_predictions = processor.get_detailed_file_predictions()\r\n+\r\n+ if detailed_predictions is None:\r\n+ log.error(f\"Detailed prediction failed critically for {input_path_str}. Adding asset-level error.\")\r\n+ # Add a single error entry for the whole asset if the method returns None\r\n+ asset_results.append({\r\n+ 'original_path': source_asset_name, # Use asset name as placeholder\r\n+ 'predicted_asset_name': None, # New key\r\n+ 'predicted_output_name': None, # New key\r\n+ 'status': 'Error',\r\n+ 'details': 'Critical prediction failure (check logs)',\r\n+ 'source_asset': source_asset_name\r\n+ })\r\n+ else:\r\n+ log.debug(f\"Received {len(detailed_predictions)} detailed predictions for {input_path_str}.\")\r\n+ # Add source_asset key and ensure correct keys exist\r\n+ for prediction_dict in detailed_predictions:\r\n+ # Ensure all expected keys are present, even if None\r\n+ result_entry = {\r\n+ 'original_path': prediction_dict.get('original_path', '[Missing Path]'),\r\n+ 'predicted_asset_name': prediction_dict.get('predicted_asset_name'), # New key\r\n+ 'predicted_output_name': prediction_dict.get('predicted_output_name'), # New key\r\n+ 'status': prediction_dict.get('status', 'Error'),\r\n+ 'details': prediction_dict.get('details', '[Missing Details]'),\r\n+ 'source_asset': source_asset_name # Add the source asset identifier\r\n+ }\r\n+ asset_results.append(result_entry)\r\n+\r\n+ except AssetProcessingError as e: # Catch errors during processor instantiation or prediction setup\r\n+ log.error(f\"Asset processing error during prediction setup for {input_path_str}: {e}\")\r\n+ asset_results.append({\r\n+ 'original_path': source_asset_name,\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n+ 'status': 'Error',\r\n+ 'details': f'Asset Error: {e}',\r\n+ 'source_asset': source_asset_name\r\n+ })\r\n+ except Exception as e: # Catch unexpected errors\r\n+ log.exception(f\"Unexpected error during prediction for {input_path_str}: {e}\")\r\n+ asset_results.append({\r\n+ 'original_path': source_asset_name,\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n+ 'status': 'Error',\r\n+ 'details': f'Unexpected Error: {e}',\r\n+ 'source_asset': source_asset_name\r\n+ })\r\n+ finally:\r\n+ # Cleanup for the single asset prediction if needed (AssetProcessor handles its own temp dir)\r\n+ pass\r\n+ return asset_results\r\n+\r\n+\r\n+ def run_prediction(self, input_paths: list[str], preset_name: str):\r\n+ \"\"\"\r\n+ Runs the prediction logic for the given paths and preset using a ThreadPoolExecutor.\r\n+ This method is intended to be run in a separate QThread.\r\n+ \"\"\"\r\n+ if self._is_running:\r\n+ log.warning(\"Prediction is already running.\")\r\n+ return\r\n+ if not BACKEND_AVAILABLE:\r\n+ log.error(\"Backend modules not available. Cannot run prediction.\")\r\n+ self.status_message.emit(\"Error: Backend components missing.\", 5000)\r\n+ self.prediction_finished.emit()\r\n+ return\r\n+ if not preset_name:\r\n+ log.warning(\"No preset selected for prediction.\")\r\n+ self.status_message.emit(\"No preset selected.\", 3000)\r\n+ self.prediction_finished.emit()\r\n+ return\r\n+\r\n+ self._is_running = True\r\n+ thread_id = QThread.currentThreadId() # Get current thread ID\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] --> Entered PredictionHandler.run_prediction. Starting run for {len(input_paths)} items, Preset='{preset_name}'\")\r\n+ self.status_message.emit(f\"Updating preview for {len(input_paths)} items...\", 0)\r\n+\r\n+ config = None # Load config once if possible\r\n+ try:\r\n+ config = Configuration(preset_name)\r\n+ except ConfigurationError as e:\r\n+ log.error(f\"Failed to load configuration for preset '{preset_name}': {e}\")\r\n+ self.status_message.emit(f\"Error loading preset '{preset_name}': {e}\", 5000)\r\n+ # Emit error for all items? Or just finish? Finish for now.\r\n+ self.prediction_finished.emit()\r\n+ self._is_running = False\r\n+ return\r\n+ except Exception as e:\r\n+ log.exception(f\"Unexpected error loading configuration for preset '{preset_name}': {e}\")\r\n+ self.status_message.emit(f\"Unexpected error loading preset '{preset_name}'.\", 5000)\r\n+ self.prediction_finished.emit()\r\n+ return\r\n+\r\n+ all_file_results = [] # Accumulate results here\r\n+ futures = []\r\n+ # Determine number of workers - use half the cores, minimum 1, max 8?\r\n+ max_workers = min(max(1, (os.cpu_count() or 1) // 2), 8)\r\n+ log.info(f\"Using ThreadPoolExecutor with max_workers={max_workers} for prediction.\")\r\n+\r\n+ try:\r\n+ with ThreadPoolExecutor(max_workers=max_workers) as executor:\r\n+ # Submit tasks for each input path\r\n+ for input_path_str in input_paths:\r\n+ future = executor.submit(self._predict_single_asset, input_path_str, config)\r\n+ futures.append(future)\r\n+\r\n+ # Process results as they complete\r\n+ for future in as_completed(futures):\r\n+ try:\r\n+ # Result is a list of dicts for one asset\r\n+ asset_result_list = future.result()\r\n+ if asset_result_list: # Check if list is not empty\r\n+ all_file_results.extend(asset_result_list)\r\n+ except Exception as exc:\r\n+ # This catches errors within the future execution itself if not handled by _predict_single_asset\r\n+ log.error(f'Prediction task generated an exception: {exc}', exc_info=True)\r\n+ # We might not know which input path failed here easily without more mapping\r\n+ # Add a generic error?\r\n+ all_file_results.append({\r\n+ 'original_path': '[Unknown Asset - Executor Error]',\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n+ 'status': 'Error',\r\n+ 'details': f'Executor Error: {exc}',\r\n+ 'source_asset': '[Unknown]'\r\n+ })\r\n+\r\n+ except Exception as pool_exc:\r\n+ log.exception(f\"An error occurred with the prediction ThreadPoolExecutor: {pool_exc}\")\r\n+ self.status_message.emit(f\"Error during prediction setup: {pool_exc}\", 5000)\r\n+ # Add a generic error if the pool fails\r\n+ all_file_results.append({\r\n+ 'original_path': '[Prediction Pool Error]',\r\n+ 'predicted_asset_name': None,\r\n+ 'predicted_output_name': None,\r\n+ 'status': 'Error',\r\n+ 'details': f'Pool Error: {pool_exc}',\r\n+ 'source_asset': '[System]'\r\n+ })\r\n+\r\n+ # Emit the combined list of detailed file results at the end\r\n+ log.info(f\"Parallel prediction run finished. Preparing to emit {len(all_file_results)} file results.\")\r\n+ # <<< Add logging before emit >>>\r\n+ log.debug(f\"Type of all_file_results before emit: {type(all_file_results)}\")\r\n+ try:\r\n+ log.debug(f\"Content of all_file_results (first 5) before emit: {all_file_results[:5]}\")\r\n+ except Exception as e:\r\n+ log.error(f\"Error logging all_file_results content: {e}\")\r\n+ # <<< End added logging >>>\r\n+ self.prediction_results_ready.emit(all_file_results)\r\n+ self.status_message.emit(\"Preview update complete.\", 3000)\r\n+ self.prediction_finished.emit()\r\n+ self._is_running = False\n\\ No newline at end of file\n"
},
{
"date": 1745333025359,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -213,243 +213,20 @@\n 'source_asset': '[System]'\r\n })\r\n \r\n # Emit the combined list of detailed file results at the end\r\n- log.info(f\"Parallel prediction run finished. Preparing to emit {len(all_file_results)} file results.\")\r\n+ # Note: thread_id was already defined earlier in this function\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Parallel prediction run finished. Preparing to emit {len(all_file_results)} file results.\")\r\n # <<< Add logging before emit >>>\r\n- log.debug(f\"Type of all_file_results before emit: {type(all_file_results)}\")\r\n+ log.debug(f\"[{time.time():.4f}][T:{thread_id}] Type of all_file_results before emit: {type(all_file_results)}\")\r\n try:\r\n- log.debug(f\"Content of all_file_results (first 5) before emit: {all_file_results[:5]}\")\r\n+ log.debug(f\"[{time.time():.4f}][T:{thread_id}] Content of all_file_results (first 5) before emit: {all_file_results[:5]}\")\r\n except Exception as e:\r\n- log.error(f\"Error logging all_file_results content: {e}\")\r\n+ log.error(f\"[{time.time():.4f}][T:{thread_id}] Error logging all_file_results content: {e}\")\r\n # <<< End added logging >>>\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Emitting prediction_results_ready signal...\")\r\n self.prediction_results_ready.emit(all_file_results)\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Emitted prediction_results_ready signal.\")\r\n self.status_message.emit(\"Preview update complete.\", 3000)\r\n self.prediction_finished.emit()\r\n- self._is_running = False\n-# gui/prediction_handler.py\r\n-import logging\r\n-from pathlib import Path\r\n-import time # For potential delays if needed\r\n-import os # For cpu_count\r\n-from concurrent.futures import ThreadPoolExecutor, as_completed # For parallel prediction\r\n-\r\n-# --- PySide6 Imports ---\r\n-from PySide6.QtCore import QObject, Signal\r\n-\r\n-# --- Backend Imports ---\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- from configuration import Configuration, ConfigurationError\r\n- from asset_processor import AssetProcessor, AssetProcessingError\r\n- BACKEND_AVAILABLE = True\r\n-except ImportError as e:\r\n- print(f\"ERROR (PredictionHandler): Failed to import backend modules: {e}\")\r\n- # Define placeholders if imports fail\r\n- Configuration = None\r\n- AssetProcessor = 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 (PredictHandler): %(message)s')\r\n-\r\n-\r\n-class PredictionHandler(QObject):\r\n- \"\"\"\r\n- Handles running predictions in a separate thread to avoid GUI freezes.\r\n- \"\"\"\r\n- # --- Signals ---\r\n- # Emits a list of dictionaries, each representing a file row for the table\r\n- # Dict format: {'original_path': str, 'predicted_asset_name': str | None, 'predicted_output_name': str | None, 'status': str, 'details': str | None, 'source_asset': str}\r\n- prediction_results_ready = Signal(list)\r\n- # Emitted when all predictions for a batch are done\r\n- prediction_finished = Signal()\r\n- # Emitted for status updates\r\n- status_message = Signal(str, int)\r\n-\r\n- def __init__(self, parent=None):\r\n- super().__init__(parent)\r\n self._is_running = False\r\n- # No explicit cancel needed for prediction for now, it should be fast per-item\r\n-\r\n- @property\r\n- def is_running(self):\r\n- return self._is_running\r\n-\r\n- def _predict_single_asset(self, input_path_str: str, config: Configuration) -> list[dict]:\r\n- \"\"\"\r\n- Helper method to predict a single asset. Runs within the ThreadPoolExecutor.\r\n- Returns a list of prediction dictionaries for the asset, or a single error dict.\r\n- \"\"\"\r\n- input_path = Path(input_path_str)\r\n- source_asset_name = input_path.name # For reference in the results\r\n- asset_results = []\r\n- try:\r\n- # Create AssetProcessor instance (needs dummy output path)\r\n- # Ensure AssetProcessor is thread-safe or create a new instance per thread.\r\n- # Based on its structure (using temp dirs), creating new instances should be safe.\r\n- processor = AssetProcessor(input_path, config, Path(\".\")) # Dummy output path\r\n-\r\n- # Get detailed file predictions\r\n- detailed_predictions = processor.get_detailed_file_predictions()\r\n-\r\n- if detailed_predictions is None:\r\n- log.error(f\"Detailed prediction failed critically for {input_path_str}. Adding asset-level error.\")\r\n- # Add a single error entry for the whole asset if the method returns None\r\n- asset_results.append({\r\n- 'original_path': source_asset_name, # Use asset name as placeholder\r\n- 'predicted_asset_name': None, # New key\r\n- 'predicted_output_name': None, # New key\r\n- 'status': 'Error',\r\n- 'details': 'Critical prediction failure (check logs)',\r\n- 'source_asset': source_asset_name\r\n- })\r\n- else:\r\n- log.debug(f\"Received {len(detailed_predictions)} detailed predictions for {input_path_str}.\")\r\n- # Add source_asset key and ensure correct keys exist\r\n- for prediction_dict in detailed_predictions:\r\n- # Ensure all expected keys are present, even if None\r\n- result_entry = {\r\n- 'original_path': prediction_dict.get('original_path', '[Missing Path]'),\r\n- 'predicted_asset_name': prediction_dict.get('predicted_asset_name'), # New key\r\n- 'predicted_output_name': prediction_dict.get('predicted_output_name'), # New key\r\n- 'status': prediction_dict.get('status', 'Error'),\r\n- 'details': prediction_dict.get('details', '[Missing Details]'),\r\n- 'source_asset': source_asset_name # Add the source asset identifier\r\n- }\r\n- asset_results.append(result_entry)\r\n-\r\n- except AssetProcessingError as e: # Catch errors during processor instantiation or prediction setup\r\n- log.error(f\"Asset processing error during prediction setup for {input_path_str}: {e}\")\r\n- asset_results.append({\r\n- 'original_path': source_asset_name,\r\n- 'predicted_asset_name': None,\r\n- 'predicted_output_name': None,\r\n- 'status': 'Error',\r\n- 'details': f'Asset Error: {e}',\r\n- 'source_asset': source_asset_name\r\n- })\r\n- except Exception as e: # Catch unexpected errors\r\n- log.exception(f\"Unexpected error during prediction for {input_path_str}: {e}\")\r\n- asset_results.append({\r\n- 'original_path': source_asset_name,\r\n- 'predicted_asset_name': None,\r\n- 'predicted_output_name': None,\r\n- 'status': 'Error',\r\n- 'details': f'Unexpected Error: {e}',\r\n- 'source_asset': source_asset_name\r\n- })\r\n- finally:\r\n- # Cleanup for the single asset prediction if needed (AssetProcessor handles its own temp dir)\r\n- pass\r\n- return asset_results\r\n-\r\n-\r\n- def run_prediction(self, input_paths: list[str], preset_name: str):\r\n- \"\"\"\r\n- Runs the prediction logic for the given paths and preset using a ThreadPoolExecutor.\r\n- This method is intended to be run in a separate QThread.\r\n- \"\"\"\r\n- if self._is_running:\r\n- log.warning(\"Prediction is already running.\")\r\n- return\r\n- if not BACKEND_AVAILABLE:\r\n- log.error(\"Backend modules not available. Cannot run prediction.\")\r\n- self.status_message.emit(\"Error: Backend components missing.\", 5000)\r\n- self.prediction_finished.emit()\r\n- return\r\n- if not preset_name:\r\n- log.warning(\"No preset selected for prediction.\")\r\n- self.status_message.emit(\"No preset selected.\", 3000)\r\n- self.prediction_finished.emit()\r\n- return\r\n-\r\n- self._is_running = True\r\n- log.info(f\"Starting prediction run for {len(input_paths)} items, Preset='{preset_name}'\")\r\n- self.status_message.emit(f\"Updating preview for {len(input_paths)} items...\", 0)\r\n-\r\n- config = None # Load config once if possible\r\n- try:\r\n- config = Configuration(preset_name)\r\n- except ConfigurationError as e:\r\n- log.error(f\"Failed to load configuration for preset '{preset_name}': {e}\")\r\n- self.status_message.emit(f\"Error loading preset '{preset_name}': {e}\", 5000)\r\n- # Emit error for all items? Or just finish? Finish for now.\r\n- self.prediction_finished.emit()\r\n- self._is_running = False\r\n- return\r\n- except Exception as e:\r\n- log.exception(f\"Unexpected error loading configuration for preset '{preset_name}': {e}\")\r\n- self.status_message.emit(f\"Unexpected error loading preset '{preset_name}'.\", 5000)\r\n- self.prediction_finished.emit()\r\n- return\r\n-\r\n- all_file_results = [] # Accumulate results here\r\n- futures = []\r\n- # Determine number of workers - use half the cores, minimum 1, max 8?\r\n- max_workers = min(max(1, (os.cpu_count() or 1) // 2), 8)\r\n- log.info(f\"Using ThreadPoolExecutor with max_workers={max_workers} for prediction.\")\r\n-\r\n- try:\r\n- with ThreadPoolExecutor(max_workers=max_workers) as executor:\r\n- # Submit tasks for each input path\r\n- for input_path_str in input_paths:\r\n- future = executor.submit(self._predict_single_asset, input_path_str, config)\r\n- futures.append(future)\r\n-\r\n- # Process results as they complete\r\n- for future in as_completed(futures):\r\n- try:\r\n- # Result is a list of dicts for one asset\r\n- asset_result_list = future.result()\r\n- if asset_result_list: # Check if list is not empty\r\n- all_file_results.extend(asset_result_list)\r\n- except Exception as exc:\r\n- # This catches errors within the future execution itself if not handled by _predict_single_asset\r\n- log.error(f'Prediction task generated an exception: {exc}', exc_info=True)\r\n- # We might not know which input path failed here easily without more mapping\r\n- # Add a generic error?\r\n- all_file_results.append({\r\n- 'original_path': '[Unknown Asset - Executor Error]',\r\n- 'predicted_asset_name': None,\r\n- 'predicted_output_name': None,\r\n- 'status': 'Error',\r\n- 'details': f'Executor Error: {exc}',\r\n- 'source_asset': '[Unknown]'\r\n- })\r\n-\r\n- except Exception as pool_exc:\r\n- log.exception(f\"An error occurred with the prediction ThreadPoolExecutor: {pool_exc}\")\r\n- self.status_message.emit(f\"Error during prediction setup: {pool_exc}\", 5000)\r\n- # Add a generic error if the pool fails\r\n- all_file_results.append({\r\n- 'original_path': '[Prediction Pool Error]',\r\n- 'predicted_asset_name': None,\r\n- 'predicted_output_name': None,\r\n- 'status': 'Error',\r\n- 'details': f'Pool Error: {pool_exc}',\r\n- 'source_asset': '[System]'\r\n- })\r\n-\r\n- # Emit the combined list of detailed file results at the end\r\n- log.info(f\"Parallel prediction run finished. Preparing to emit {len(all_file_results)} file results.\")\r\n- # <<< Add logging before emit >>>\r\n- log.debug(f\"Type of all_file_results before emit: {type(all_file_results)}\")\r\n- try:\r\n- log.debug(f\"Content of all_file_results (first 5) before emit: {all_file_results[:5]}\")\r\n\\ No newline at end of file\n- except Exception as e:\r\n- log.error(f\"Error logging all_file_results content: {e}\")\r\n- # <<< End added logging >>>\r\n- self.prediction_results_ready.emit(all_file_results)\r\n- self.status_message.emit(\"Preview update complete.\", 3000)\r\n- self.prediction_finished.emit()\r\n- self._is_running = False\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] <-- Exiting PredictionHandler.run_prediction.\")\n\\ No newline at end of file\n"
},
{
"date": 1745333390361,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -145,9 +145,9 @@\n self.prediction_finished.emit()\r\n return\r\n \r\n self._is_running = True\r\n- thread_id = QThread.currentThreadId() # Get current thread ID\r\n+ thread_id = QThread.currentThread() # Get current thread object\r\n log.info(f\"[{time.time():.4f}][T:{thread_id}] --> Entered PredictionHandler.run_prediction. Starting run for {len(input_paths)} items, Preset='{preset_name}'\")\r\n self.status_message.emit(f\"Updating preview for {len(input_paths)} items...\", 0)\r\n \r\n config = None # Load config once if possible\r\n"
},
{
"date": 1745333453367,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -5,9 +5,9 @@\n import os # For cpu_count\r\n from concurrent.futures import ThreadPoolExecutor, as_completed # For parallel prediction\r\n \r\n # --- PySide6 Imports ---\r\n-from PySide6.QtCore import QObject, Signal\r\n+from PySide6.QtCore import QObject, Signal, QThread # Import QThread\r\n \r\n # --- Backend Imports ---\r\n # Adjust path to ensure modules can be found relative to this file's location\r\n import sys\r\n"
}
],
"date": 1745236106567,
"name": "Commit-0",
"content": "# gui/prediction_handler.py\r\nimport logging\r\nfrom pathlib import Path\r\nimport time # For potential delays if needed\r\nimport os # For cpu_count\r\nfrom concurrent.futures import ThreadPoolExecutor, as_completed # For parallel prediction\r\n\r\n# --- PySide6 Imports ---\r\nfrom PySide6.QtCore import QObject, Signal\r\n\r\n# --- Backend Imports ---\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 from configuration import Configuration, ConfigurationError\r\n from asset_processor import AssetProcessor, AssetProcessingError\r\n BACKEND_AVAILABLE = True\r\nexcept ImportError as e:\r\n print(f\"ERROR (PredictionHandler): Failed to import backend modules: {e}\")\r\n # Define placeholders if imports fail\r\n Configuration = None\r\n AssetProcessor = 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 (PredictHandler): %(message)s')\r\n\r\n\r\nclass PredictionHandler(QObject):\r\n \"\"\"\r\n Handles running predictions in a separate thread to avoid GUI freezes.\r\n \"\"\"\r\n # --- Signals ---\r\n # Emits a list of dictionaries, each representing a file row for the table\r\n # Dict format: {'original_path': str, 'predicted_name': str | None, 'status': str, 'details': str | None, 'source_asset': str}\r\n prediction_results_ready = Signal(list)\r\n # Emitted when all predictions for a batch are done\r\n prediction_finished = Signal()\r\n # Emitted for status updates\r\n status_message = Signal(str, int)\r\n\r\n def __init__(self, parent=None):\r\n super().__init__(parent)\r\n self._is_running = False\r\n # No explicit cancel needed for prediction for now, it should be fast per-item\r\n\r\n @property\r\n def is_running(self):\r\n return self._is_running\r\n\r\n def _predict_single_asset(self, input_path_str: str, config: Configuration) -> list[dict]:\r\n \"\"\"\r\n Helper method to predict a single asset. Runs within the ThreadPoolExecutor.\r\n Returns a list of prediction dictionaries for the asset, or a single error dict.\r\n \"\"\"\r\n input_path = Path(input_path_str)\r\n source_asset_name = input_path.name # For reference in the results\r\n asset_results = []\r\n try:\r\n # Create AssetProcessor instance (needs dummy output path)\r\n # Ensure AssetProcessor is thread-safe or create a new instance per thread.\r\n # Based on its structure (using temp dirs), creating new instances should be safe.\r\n processor = AssetProcessor(input_path, config, Path(\".\")) # Dummy output path\r\n\r\n # Get detailed file predictions\r\n detailed_predictions = processor.get_detailed_file_predictions()\r\n\r\n if detailed_predictions is None:\r\n log.error(f\"Detailed prediction failed critically for {input_path_str}. Adding asset-level error.\")\r\n # Add a single error entry for the whole asset if the method returns None\r\n asset_results.append({\r\n 'original_path': source_asset_name, # Use asset name as placeholder\r\n 'predicted_name': None,\r\n 'status': 'Error',\r\n 'details': 'Critical prediction failure (check logs)',\r\n 'source_asset': source_asset_name\r\n })\r\n else:\r\n log.debug(f\"Received {len(detailed_predictions)} detailed predictions for {input_path_str}.\")\r\n # Add source_asset key to each dictionary and append to the main list\r\n for prediction_dict in detailed_predictions:\r\n prediction_dict['source_asset'] = source_asset_name\r\n asset_results.append(prediction_dict)\r\n\r\n except AssetProcessingError as e: # Catch errors during processor instantiation or prediction setup\r\n log.error(f\"Asset processing error during prediction setup for {input_path_str}: {e}\")\r\n asset_results.append({\r\n 'original_path': source_asset_name,\r\n 'predicted_name': None,\r\n 'status': 'Error',\r\n 'details': f'Asset Error: {e}',\r\n 'source_asset': source_asset_name\r\n })\r\n except Exception as e: # Catch unexpected errors\r\n log.exception(f\"Unexpected error during prediction for {input_path_str}: {e}\")\r\n asset_results.append({\r\n 'original_path': source_asset_name,\r\n 'predicted_name': None,\r\n 'status': 'Error',\r\n 'details': f'Unexpected Error: {e}',\r\n 'source_asset': source_asset_name\r\n })\r\n finally:\r\n # Cleanup for the single asset prediction if needed (AssetProcessor handles its own temp dir)\r\n pass\r\n return asset_results\r\n\r\n\r\n def run_prediction(self, input_paths: list[str], preset_name: str):\r\n \"\"\"\r\n Runs the prediction logic for the given paths and preset using a ThreadPoolExecutor.\r\n This method is intended to be run in a separate QThread.\r\n \"\"\"\r\n if self._is_running:\r\n log.warning(\"Prediction is already running.\")\r\n return\r\n if not BACKEND_AVAILABLE:\r\n log.error(\"Backend modules not available. Cannot run prediction.\")\r\n self.status_message.emit(\"Error: Backend components missing.\", 5000)\r\n self.prediction_finished.emit()\r\n return\r\n if not preset_name:\r\n log.warning(\"No preset selected for prediction.\")\r\n self.status_message.emit(\"No preset selected.\", 3000)\r\n self.prediction_finished.emit()\r\n return\r\n\r\n self._is_running = True\r\n log.info(f\"Starting prediction run for {len(input_paths)} items, Preset='{preset_name}'\")\r\n self.status_message.emit(f\"Updating preview for {len(input_paths)} items...\", 0)\r\n\r\n config = None # Load config once if possible\r\n try:\r\n config = Configuration(preset_name)\r\n except ConfigurationError as e:\r\n log.error(f\"Failed to load configuration for preset '{preset_name}': {e}\")\r\n self.status_message.emit(f\"Error loading preset '{preset_name}': {e}\", 5000)\r\n # Emit error for all items? Or just finish? Finish for now.\r\n self.prediction_finished.emit()\r\n self._is_running = False\r\n return\r\n except Exception as e:\r\n log.exception(f\"Unexpected error loading configuration for preset '{preset_name}': {e}\")\r\n self.status_message.emit(f\"Unexpected error loading preset '{preset_name}'.\", 5000)\r\n self.prediction_finished.emit()\r\n return\r\n\r\n all_file_results = [] # Accumulate results here\r\n futures = []\r\n # Determine number of workers - use half the cores, minimum 1, max 8?\r\n max_workers = min(max(1, (os.cpu_count() or 1) // 2), 8)\r\n log.info(f\"Using ThreadPoolExecutor with max_workers={max_workers} for prediction.\")\r\n\r\n try:\r\n with ThreadPoolExecutor(max_workers=max_workers) as executor:\r\n # Submit tasks for each input path\r\n for input_path_str in input_paths:\r\n future = executor.submit(self._predict_single_asset, input_path_str, config)\r\n futures.append(future)\r\n\r\n # Process results as they complete\r\n for future in as_completed(futures):\r\n try:\r\n # Result is a list of dicts for one asset\r\n asset_result_list = future.result()\r\n if asset_result_list: # Check if list is not empty\r\n all_file_results.extend(asset_result_list)\r\n except Exception as exc:\r\n # This catches errors within the future execution itself if not handled by _predict_single_asset\r\n log.error(f'Prediction task generated an exception: {exc}', exc_info=True)\r\n # We might not know which input path failed here easily without more mapping\r\n # Add a generic error?\r\n all_file_results.append({\r\n 'original_path': '[Unknown Asset - Executor Error]',\r\n 'predicted_name': None,\r\n 'status': 'Error',\r\n 'details': f'Executor Error: {exc}',\r\n 'source_asset': '[Unknown]'\r\n })\r\n\r\n except Exception as pool_exc:\r\n log.exception(f\"An error occurred with the prediction ThreadPoolExecutor: {pool_exc}\")\r\n self.status_message.emit(f\"Error during prediction setup: {pool_exc}\", 5000)\r\n # Add a generic error if the pool fails\r\n all_file_results.append({\r\n 'original_path': '[Prediction Pool Error]',\r\n 'predicted_name': None,\r\n 'status': 'Error',\r\n 'details': f'Pool Error: {pool_exc}',\r\n 'source_asset': '[System]'\r\n })\r\n\r\n # Emit the combined list of detailed file results at the end\r\n log.info(f\"Parallel prediction run finished. Preparing to emit {len(all_file_results)} file results.\")\r\n # <<< Add logging before emit >>>\r\n log.debug(f\"Type of all_file_results before emit: {type(all_file_results)}\")\r\n try:\r\n log.debug(f\"Content of all_file_results (first 5) before emit: {all_file_results[:5]}\")\r\n except Exception as e:\r\n log.error(f\"Error logging all_file_results content: {e}\")\r\n # <<< End added logging >>>\r\n self.prediction_results_ready.emit(all_file_results)\r\n self.status_message.emit(\"Preview update complete.\", 3000)\r\n self.prediction_finished.emit()\r\n self._is_running = False"
}
]
}