From b43b2522d73ebfbac05effc70b2c482863fa33c6 Mon Sep 17 00:00:00 2001 From: Rusfort Date: Thu, 15 May 2025 20:52:58 +0200 Subject: [PATCH] Implemented Item type priority handling ( DISP16 ) --- .roo/mcp.json | 3 +- .roo/rules-orchestrator/rules.md | 0 .roomodes | 14 +- .../01_User_Guide/11_Usage_Autotest.md | 6 +- Presets/Dinesen.json | 24 +- Presets/Poliigon.json | 115 +++++- TestFiles/Test-BoucleChunky001.json | 6 +- autotest.py | 51 ++- config/file_type_definitions.json | 6 +- configuration.py | 105 ++++-- .../length.bin | Bin 40000 -> 40000 bytes .../conport_vector_data/chroma.sqlite3 | Bin 163840 -> 163840 bytes context_portal/context.db | Bin 344064 -> 344064 bytes gui/main_window.py | 40 +- gui/prediction_handler.py | 341 ++++++++++-------- gui/preset_editor_widget.py | 80 ++-- 16 files changed, 537 insertions(+), 254 deletions(-) create mode 100644 .roo/rules-orchestrator/rules.md diff --git a/.roo/mcp.json b/.roo/mcp.json index ca3d3f3..4c651cc 100644 --- a/.roo/mcp.json +++ b/.roo/mcp.json @@ -38,7 +38,8 @@ "delete_system_pattern_by_id", "get_conport_schema", "get_recent_activity_summary", - "semantic_search_conport" + "semantic_search_conport", + "search_system_patterns_fts" ] } } diff --git a/.roo/rules-orchestrator/rules.md b/.roo/rules-orchestrator/rules.md new file mode 100644 index 0000000..e69de29 diff --git a/.roomodes b/.roomodes index 1f2c591..49e5a66 100644 --- a/.roomodes +++ b/.roomodes @@ -1,3 +1,15 @@ { - "customModes": [] + "customModes": [ + { + "slug": "Task-Initiator", + "name": "Task Initiator", + "roleDefinition": "You are Task Initiator. Your exclusive function is comprehensive initial context gathering, focusing solely on ConPort data. Do NOT perform other tasks or use direct file system tools for context gathering.", + "customInstructions": "1. First, execute standard initial context setup procedures (as per global ConPort strategy).\n2. Next, if a specific user request is pending, YOU, as Task Initiator, should analyze it and proactively gather relevant information, strictly by querying ConPort. Your process for this is:\n a. Identify the key subject(s) of the request.\n b. Loosely search relevant ConPort data for information or summaries related to these identified subject(s).\n3. After completing both standard setup AND any ConPort-based task-specific gathering, briefly report the overall context status. This report must cover ConPort initialization and summarize any specific information found (or explicitly not found) within ConPort relevant to the user's request.\n4. Then, output `[TASK_INITIATOR_COMPLETE]`.\n5. Finally, to address the user's main request with the context you've gathered (or confirmed is missing from ConPort), use the `switch_mode` tool to transition to the determined most appropriate mode by analysing the initial request. you should ALWAYS finish context-gathering before switching modes.", + "groups": [ + "mcp", + "read" + ], + "source": "project" + } + ] } \ No newline at end of file diff --git a/Documentation/01_User_Guide/11_Usage_Autotest.md b/Documentation/01_User_Guide/11_Usage_Autotest.md index d4ee010..d900e38 100644 --- a/Documentation/01_User_Guide/11_Usage_Autotest.md +++ b/Documentation/01_User_Guide/11_Usage_Autotest.md @@ -34,7 +34,7 @@ The script accepts several command-line arguments to configure the test run. If * A string to search for within the application logs generated during the test run. If found, matching log lines (with context) will be highlighted. * Default: None * `--additional-lines NUM_LINES` (optional): - * When using `--search`, this specifies how many lines of context before and after each matching log line should be displayed. + * When using `--search`, this specifies how many lines of context before and after each matching log line should be displayed. A good non-zero value is 1-2. * Default: `0` **Example Usage:** @@ -80,4 +80,6 @@ When executed, `autotest.py` performs the following steps: * `1`: Test failed at some point (e.g., rule mismatch, processing error, traceback found). * **Output Directory:** Inspect the contents of the specified output directory to manually verify the processed assets if needed. -This automated test helps ensure the stability of the core processing logic when driven by GUI-equivalent actions. \ No newline at end of file +This automated test helps ensure the stability of the core processing logic when driven by GUI-equivalent actions. + +Note: Under some conditions, the autotest will exit with errorcode "3221226505". This has no consequence and can therefor be ignore. \ No newline at end of file diff --git a/Presets/Dinesen.json b/Presets/Dinesen.json index f91cf36..12ef98b 100644 --- a/Presets/Dinesen.json +++ b/Presets/Dinesen.json @@ -1,5 +1,5 @@ { - "preset_name": "Dinesen Custom", + "preset_name": "Dinesen", "supplier_name": "Dinesen", "notes": "Preset for standard Poliigon downloads. Prioritizes _xxx16 files. Moves previews etc. to Extra/. Assumes Metal/Rough workflow.", "source_naming": { @@ -10,11 +10,7 @@ }, "glossiness_keywords": [ "GLOSS" - ], - "bit_depth_variants": { - "NRM": "*_NRM16*", - "DISP": "*_DISP16*" - } + ] }, "move_to_extra_patterns": [ "*_Preview*", @@ -25,7 +21,8 @@ "*.pdf", "*.url", "*.htm*", - "*_Fabric.*" + "*_Fabric.*", + "*_DISP_*METALNESS*" ], "map_type_mapping": [ { @@ -46,6 +43,11 @@ "NORM*", "NRM*", "N" + ], + "priority_keywords": [ + "*_NRM16*", + "*_NM16*", + "*Normal16*" ] }, { @@ -75,6 +77,14 @@ "DISP", "HEIGHT", "BUMP" + ], + "priority_keywords": [ + "*_DISP16*", + "*_DSP16*", + "*DSP16*", + "*DISP16*", + "*Displacement16*", + "*Height16*" ] }, { diff --git a/Presets/Poliigon.json b/Presets/Poliigon.json index 5587475..b17f6ad 100644 --- a/Presets/Poliigon.json +++ b/Presets/Poliigon.json @@ -10,11 +10,7 @@ }, "glossiness_keywords": [ "GLOSS" - ], - "bit_depth_variants": { - "NRM": "*_NRM16*", - "DISP": "*_DISP16*" - } + ] }, "move_to_extra_patterns": [ "*_Preview*", @@ -28,7 +24,114 @@ "*_Fabric.*", "*_Albedo*" ], - "map_type_mapping": [], + "map_type_mapping": [ + { + "target_type": "MAP_COL", + "keywords": [ + "COLOR*", + "COL", + "COL-*", + "DIFFUSE", + "DIF", + "ALBEDO" + ] + }, + { + "target_type": "MAP_NRM", + "keywords": [ + "NORMAL*", + "NORM*", + "NRM*", + "N" + ], + "priority_keywords": [ + "*_NRM16*", + "*_NM16*", + "*Normal16*" + ] + }, + { + "target_type": "MAP_ROUGH", + "keywords": [ + "ROUGHNESS", + "ROUGH" + ] + }, + { + "target_type": "MAP_GLOSS", + "keywords": [ + "GLOSS" + ] + }, + { + "target_type": "MAP_AO", + "keywords": [ + "AMBIENTOCCLUSION", + "AO" + ] + }, + { + "target_type": "MAP_DISP", + "keywords": [ + "DISPLACEMENT", + "DISP", + "HEIGHT", + "BUMP" + ], + "priority_keywords": [ + "*_DISP16*", + "*_DSP16*", + "*DSP16*", + "*DISP16*", + "*Displacement16*", + "*Height16*" + ] + }, + { + "target_type": "MAP_REFL", + "keywords": [ + "REFLECTION", + "REFL", + "SPECULAR", + "SPEC" + ] + }, + { + "target_type": "MAP_SSS", + "keywords": [ + "SSS", + "SUBSURFACE*" + ] + }, + { + "target_type": "MAP_FUZZ", + "keywords": [ + "FUZZ" + ] + }, + { + "target_type": "MAP_IDMAP", + "keywords": [ + "IDMAP" + ] + }, + { + "target_type": "MAP_MASK", + "keywords": [ + "OPAC*", + "TRANSP*", + "MASK*", + "ALPHA*" + ] + }, + { + "target_type": "MAP_METAL", + "keywords": [ + "METAL*", + "METALLIC" + ] + } + ], "asset_category_rules": { "model_patterns": [ "*.fbx", diff --git a/TestFiles/Test-BoucleChunky001.json b/TestFiles/Test-BoucleChunky001.json index 292d275..be94cb5 100644 --- a/TestFiles/Test-BoucleChunky001.json +++ b/TestFiles/Test-BoucleChunky001.json @@ -1,9 +1,9 @@ -{ + { "source_rules": [ { "input_path": "BoucleChunky001.zip", "supplier_identifier": "Dinesen", - "preset_name": null, + "preset_name": "Dinesen", "assets": [ { "asset_name": "BoucleChunky001", @@ -26,7 +26,7 @@ }, { "file_path": "BoucleChunky001_DISP_1K_METALNESS.png", - "item_type": "EXTRA", + "item_type": "EXTRA", "target_asset_name_override": "BoucleChunky001" }, { diff --git a/autotest.py b/autotest.py index 15771e3..5b38ba0 100644 --- a/autotest.py +++ b/autotest.py @@ -298,15 +298,32 @@ class AutoTester(QObject): def run_test(self) -> None: """Orchestrates the test steps.""" - logger.info("Starting test run...") - + # Load expected rules first to potentially get the preset name + self._load_expected_rules() # Moved here if not self.expected_rules_data: # Ensure rules were loaded logger.error("Expected rules not loaded. Aborting test.") self.cleanup_and_exit(success=False) return + # Determine preset to use: from expected rules if available, else from CLI args + preset_to_use = self.cli_args.preset # Default + if self.expected_rules_data.get("source_rules") and \ + isinstance(self.expected_rules_data["source_rules"], list) and \ + len(self.expected_rules_data["source_rules"]) > 0 and \ + isinstance(self.expected_rules_data["source_rules"][0], dict) and \ + self.expected_rules_data["source_rules"][0].get("preset_name"): + preset_to_use = self.expected_rules_data["source_rules"][0]["preset_name"] + logger.info(f"Overriding preset with value from expected_rules.json: '{preset_to_use}'") + else: + logger.info(f"Using preset from CLI arguments: '{preset_to_use}' (this was self.cli_args.preset)") + # If preset_to_use is still self.cli_args.preset, ensure it's logged correctly + # The variable preset_to_use will hold the correct value to be used throughout. + + logger.info("Starting test run...") # Moved after preset_to_use definition + # Add a specific summary log for essential context - logger.info(f"Autotest Context: Input='{self.cli_args.zipfile.name}', Preset='{self.cli_args.preset}', Output='{self.cli_args.outputdir}'") + # This now correctly uses preset_to_use + logger.info(f"Autotest Context: Input='{self.cli_args.zipfile.name}', Preset='{preset_to_use}', Output='{self.cli_args.outputdir}'") # Step 1: Load ZIP self.test_step = "LOADING_ZIP" @@ -326,20 +343,25 @@ class AutoTester(QObject): # Step 2: Select Preset self.test_step = "SELECTING_PRESET" - logger.info(f"Step 2: Selecting preset: {self.cli_args.preset}") # KEEP INFO - Passes filter + # Use preset_to_use (which is now correctly defined earlier) + logger.info(f"Step 2: Selecting preset: {preset_to_use}") # KEEP INFO - Passes filter + # The print statement below already uses preset_to_use, which is good. + print(f"DEBUG: Attempting to select preset: '{preset_to_use}' (derived from expected: {preset_to_use == self.expected_rules_data.get('source_rules',[{}])[0].get('preset_name') if self.expected_rules_data.get('source_rules') else 'N/A'}, cli_arg: {self.cli_args.preset})") preset_found = False preset_list_widget = self.main_window.preset_editor_widget.editor_preset_list for i in range(preset_list_widget.count()): item = preset_list_widget.item(i) - if item and item.text() == self.cli_args.preset: + if item and item.text() == preset_to_use: # Use preset_to_use preset_list_widget.setCurrentItem(item) - logger.debug(f"Preset '{self.cli_args.preset}' selected.") + logger.debug(f"Preset '{preset_to_use}' selected.") + print(f"DEBUG: Successfully selected preset '{item.text()}' in GUI.") preset_found = True break if not preset_found: - logger.error(f"Preset '{self.cli_args.preset}' not found in the list.") + logger.error(f"Preset '{preset_to_use}' not found in the list.") available_presets = [preset_list_widget.item(i).text() for i in range(preset_list_widget.count())] logger.debug(f"Available presets: {available_presets}") + print(f"DEBUG: Failed to find preset '{preset_to_use}'. Available: {available_presets}") self.cleanup_and_exit(success=False) return @@ -449,8 +471,6 @@ class AutoTester(QObject): else: logger.warning("Log console or output widget not found. Cannot retrieve logs.") - self._process_and_display_logs(all_logs_text) - logger.info("Log analysis completed.") # Final Step logger.info("Test run completed successfully.") # KEEP INFO - Passes filter @@ -523,7 +543,7 @@ class AutoTester(QObject): comparable_sources_list.append({ "input_path": Path(source_rule_obj.input_path).name, # Use only the filename "supplier_identifier": source_rule_obj.supplier_identifier, - "preset_name": source_rule_obj.preset_name, + "preset_name": source_rule_obj.preset_name, # This is the actual preset name from the SourceRule object "assets": comparable_asset_list }) logger.debug("Conversion to comparable dictionary finished.") @@ -569,6 +589,8 @@ class AutoTester(QObject): if not self._compare_list_of_rules(actual_value, expected_value, "FileRule", current_context, "file_path"): item_match = False else: # Regular field comparison + if key == "preset_name": + print(f"DEBUG: Comparing preset_name: Actual='{actual_value}', Expected='{expected_value}' for {item_type_name} ({current_context})") if actual_value != expected_value: # Handle None vs "None" string for preset_name specifically if it's a common issue if key == "preset_name" and actual_value is None and expected_value == "None": @@ -602,6 +624,11 @@ class AutoTester(QObject): list_match = False # Count mismatch is an error # If counts differ, we still try to match what we can to provide more detailed feedback, # but the overall list_match will remain False. + if item_type_name == "FileRule": + print(f"DEBUG: FileRule count mismatch for {parent_context}. Actual: {len(actual_list)}, Expected: {len(expected_list)}") + print(f"DEBUG: Actual FileRule paths: {[item.get(item_key_field) for item in actual_list]}") + print(f"DEBUG: Expected FileRule paths: {[item.get(item_key_field) for item in expected_list]}") + actual_items_map = {item.get(item_key_field): item for item in actual_list if item.get(item_key_field) is not None} @@ -765,6 +792,10 @@ class AutoTester(QObject): def cleanup_and_exit(self, success: bool = True) -> None: """Cleans up and exits the application.""" + # Retrieve logs before clearing the handler + all_logs_text = "" # This variable is not used by _process_and_display_logs anymore, but kept for signature compatibility if needed elsewhere. + self._process_and_display_logs(all_logs_text) # Process and display logs BEFORE clearing the buffer + global autotest_memory_handler if autotest_memory_handler: logger.debug("Clearing memory log handler buffer and removing handler.") diff --git a/config/file_type_definitions.json b/config/file_type_definitions.json index b8aac76..427c6bb 100644 --- a/config/file_type_definitions.json +++ b/config/file_type_definitions.json @@ -195,14 +195,16 @@ "FILE_IGNORE": { "bit_depth_rule": "", "color": "#673d35", - "description": "File to be ignored", + "description": "File identified to be ignored due to prioritization rules (e.g., a lower bit-depth version when a higher one is present).", + "category": "Ignored", "examples": [ "Thumbs.db", ".DS_Store" ], "is_grayscale": false, "keybind": "X", - "standard_type": "" + "standard_type": "", + "details": {} } } } \ No newline at end of file diff --git a/configuration.py b/configuration.py index b494d25..6e64044 100644 --- a/configuration.py +++ b/configuration.py @@ -104,8 +104,8 @@ class Configuration: Raises: ConfigurationError: If core config or preset cannot be loaded/validated. """ - log.debug(f"Initializing Configuration with preset: '{preset_name}'") - self.preset_name = preset_name + log.debug(f"Initializing Configuration with preset filename stem: '{preset_name}'") + self._preset_filename_stem = preset_name # Store the stem used for loading # 1. Load core settings self._core_settings: dict = self._load_core_config() @@ -129,12 +129,16 @@ class Configuration: self._llm_settings: dict = self._load_llm_config() # 7. Load preset settings (conceptually overrides combined base + user for shared keys) - self._preset_settings: dict = self._load_preset(preset_name) + self._preset_settings: dict = self._load_preset(self._preset_filename_stem) # Use the stored stem + # Store the actual preset name read from the file content + self.actual_internal_preset_name = self._preset_settings.get("preset_name", self._preset_filename_stem) + log.info(f"Configuration instance: Loaded preset file '{self._preset_filename_stem}.json', internal preset_name is '{self.actual_internal_preset_name}'") + # 8. Validate and compile (after all base/user/preset settings are established) self._validate_configs() self._compile_regex_patterns() - log.info(f"Configuration loaded successfully using preset: '{self.preset_name}'") + log.info(f"Configuration loaded successfully using preset: '{self.actual_internal_preset_name}'") # Changed self.preset_name to self.actual_internal_preset_name def _compile_regex_patterns(self): @@ -143,8 +147,8 @@ class Configuration: self.compiled_extra_regex: list[re.Pattern] = [] self.compiled_model_regex: list[re.Pattern] = [] self.compiled_bit_depth_regex_map: dict[str, re.Pattern] = {} - # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index) - self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int]]] = {} + # Map: base_map_type -> list of tuples: (compiled_regex, original_keyword, rule_index, is_priority) + self.compiled_map_keyword_regex: dict[str, list[tuple[re.Pattern, str, int, bool]]] = {} for pattern in self.move_to_extra_patterns: try: @@ -179,28 +183,53 @@ class Configuration: for rule_index, mapping_rule in enumerate(self.map_type_mapping): if not isinstance(mapping_rule, dict) or \ - 'target_type' not in mapping_rule or \ - 'keywords' not in mapping_rule or \ - not isinstance(mapping_rule['keywords'], list): - log.warning(f"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type' and 'keywords' list.") + 'target_type' not in mapping_rule: # Removed 'keywords' check here as it's handled below + log.warning(f"Skipping invalid map_type_mapping rule at index {rule_index}: {mapping_rule}. Expected dict with 'target_type'.") continue target_type = mapping_rule['target_type'].upper() - source_keywords = mapping_rule['keywords'] + + # Ensure 'keywords' exists and is a list, default to empty list if not found or not a list + regular_keywords = mapping_rule.get('keywords', []) + if not isinstance(regular_keywords, list): + log.warning(f"Rule {rule_index} for target '{target_type}' has 'keywords' but it's not a list. Treating as empty.") + regular_keywords = [] + + priority_keywords = mapping_rule.get('priority_keywords', []) # Optional, defaults to empty list + if not isinstance(priority_keywords, list): + log.warning(f"Rule {rule_index} for target '{target_type}' has 'priority_keywords' but it's not a list. Treating as empty.") + priority_keywords = [] - - for keyword in source_keywords: + # Process regular keywords + for keyword in regular_keywords: if not isinstance(keyword, str): - log.warning(f"Skipping non-string keyword '{keyword}' in rule {rule_index} for target '{target_type}'.") - continue + log.warning(f"Skipping non-string regular keyword '{keyword}' in rule {rule_index} for target '{target_type}'.") + continue + try: + kw_regex_part = _fnmatch_to_regex(keyword) + # Ensure the keyword is treated as a whole word or is at the start/end of a segment + regex_str = rf"(?:^|{separator})({kw_regex_part})(?:$|{separator})" + compiled_regex = re.compile(regex_str, re.IGNORECASE) + # Add False for is_priority + temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index, False)) + log.debug(f" Compiled regular keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}") + except re.error as e: + log.warning(f"Failed to compile regular map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.") + + # Process priority keywords + for keyword in priority_keywords: + if not isinstance(keyword, str): + log.warning(f"Skipping non-string priority keyword '{keyword}' in rule {rule_index} for target '{target_type}'.") + continue try: kw_regex_part = _fnmatch_to_regex(keyword) regex_str = rf"(?:^|{separator})({kw_regex_part})(?:$|{separator})" compiled_regex = re.compile(regex_str, re.IGNORECASE) - temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index)) - log.debug(f" Compiled keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}") + # Add True for is_priority + temp_compiled_map_regex[target_type].append((compiled_regex, keyword, rule_index, True)) + log.debug(f" Compiled priority keyword '{keyword}' (rule {rule_index}) for target '{target_type}' as regex: {regex_str}") except re.error as e: - log.warning(f"Failed to compile map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.") + log.warning(f"Failed to compile priority map keyword regex '{keyword}' for target type '{target_type}': {e}. Skipping keyword.") self.compiled_map_keyword_regex = dict(temp_compiled_map_regex) log.debug(f"Compiled map keyword regex keys: {list(self.compiled_map_keyword_regex.keys())}") @@ -343,31 +372,43 @@ class Configuration: ] for key in required_preset_keys: if key not in self._preset_settings: - raise ConfigurationError(f"Preset '{self.preset_name}' is missing required key: '{key}'.") + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json' (internal name: '{self.actual_internal_preset_name}') is missing required key: '{key}'.") # Validate map_type_mapping structure (new format) if not isinstance(self._preset_settings['map_type_mapping'], list): - raise ConfigurationError(f"Preset '{self.preset_name}': 'map_type_mapping' must be a list.") + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': 'map_type_mapping' must be a list.") for index, rule in enumerate(self._preset_settings['map_type_mapping']): if not isinstance(rule, dict): - raise ConfigurationError(f"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' must be a dictionary.") + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Rule at index {index} in 'map_type_mapping' must be a dictionary.") if 'target_type' not in rule or not isinstance(rule['target_type'], str): - raise ConfigurationError(f"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.") + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Rule at index {index} in 'map_type_mapping' is missing 'target_type' string.") valid_file_type_keys = self._file_type_definitions.keys() if rule['target_type'] not in valid_file_type_keys: raise ConfigurationError( - f"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' " + f"Preset file '{self._preset_filename_stem}.json': Rule at index {index} in 'map_type_mapping' " f"has an invalid 'target_type': '{rule['target_type']}'. " f"Must be one of {list(valid_file_type_keys)}." ) - if 'keywords' not in rule or not isinstance(rule['keywords'], list): - raise ConfigurationError(f"Preset '{self.preset_name}': Rule at index {index} in 'map_type_mapping' is missing 'keywords' list.") - for kw_index, keyword in enumerate(rule['keywords']): - if not isinstance(keyword, str): - raise ConfigurationError(f"Preset '{self.preset_name}': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.") + # 'keywords' is optional if 'priority_keywords' is present and not empty, + # but if 'keywords' IS present, it must be a list of strings. + if 'keywords' in rule: + if not isinstance(rule['keywords'], list): + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Rule at index {index} in 'map_type_mapping' has 'keywords' but it's not a list.") + for kw_index, keyword in enumerate(rule['keywords']): + if not isinstance(keyword, str): + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Keyword at index {kw_index} in rule {index} ('{rule['target_type']}') must be a string.") + elif not ('priority_keywords' in rule and rule['priority_keywords']): # if 'keywords' is not present, 'priority_keywords' must be + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Rule at index {index} in 'map_type_mapping' must have 'keywords' or non-empty 'priority_keywords'.") + # Validate priority_keywords if present + if 'priority_keywords' in rule: + if not isinstance(rule['priority_keywords'], list): + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Rule at index {index} in 'map_type_mapping' has 'priority_keywords' but it's not a list.") + for prio_kw_index, prio_keyword in enumerate(rule['priority_keywords']): + if not isinstance(prio_keyword, str): + raise ConfigurationError(f"Preset file '{self._preset_filename_stem}.json': Priority keyword at index {prio_kw_index} in rule {index} ('{rule['target_type']}') must be a string.") if not isinstance(self._core_settings.get('TARGET_FILENAME_PATTERN'), str): raise ConfigurationError("Core config 'TARGET_FILENAME_PATTERN' must be a string.") @@ -406,7 +447,13 @@ class Configuration: @property def supplier_name(self) -> str: return self._preset_settings.get('supplier_name', 'DefaultSupplier') - + + @property + def internal_display_preset_name(self) -> str: + """Returns the 'preset_name' field from within the loaded preset JSON, + or falls back to the filename stem if not present.""" + return self.actual_internal_preset_name + @property def default_asset_category(self) -> str: """Gets the default asset category from core settings.""" diff --git a/context_portal/conport_vector_data/3712b223-f80b-4c07-b57a-5cf7c8175c86/length.bin b/context_portal/conport_vector_data/3712b223-f80b-4c07-b57a-5cf7c8175c86/length.bin index bb1821a6e0903f5e946692827afbaa97caa22938..f37abd3ef857bb5c561d6cc82f566b8fbed6075d 100644 GIT binary patch literal 40000 zcmeIuF%bYD3;?knGa^vO0#Z8E7OpsSkY9e!sJ>^5s66Xk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 GeipcwBm_wS literal 40000 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 delta 47 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 O+lf^rZr^`f9sMtt;TpgI delta 2558 zcmYjSZKxb)6`ph6cfMz5cJIgDdz0Lo?Y%|X7?U)KY5c-ewAK$atpP!_WU~A2?wH+~ z?ab_^QtNKb^$YtWmlam4&>xCQerZ>|qJjz$g-Qq%p?_LLDe;FtQV5|M1kah>Tskl_ z&z$$XGv}P=IcG+r<wdL-VJ)at?CyUucAm& zB`pLHARA#GvbBm*HKIDh_Xv?j14fRJS=5RN3IT?CTGjhG+c&{%Mje&*JN)FKo^(3N zCB}h(R9XP~eiZ32>9WlQy`^H6GJY58o#aAT?0W$8b{IDbY=9NTjc5=x`s`*35#}fCuVH$;TQHG-w937tRxvuNDCEv3h&+`Ju3S7tWEO*f|p!f~Mdd@&1#tlJH zx)pS&mQk+ic|WBkfkZsJP}%B9Tvut#R$a&qH0N3oC`hp(r$dcYo-1lkN?Z+JqLC(b zk`M1*Cly-M9@Ipf7;O_QrL334MTHhX&sccEr>fV8ftfmlF{EY(k++rbiII{tZIS}- zKS)jBR4m`Mi9*M6OGKh)+r+c8S`PfO>j!UW_8}(J2BEE*XD(aX1D8*I^B$Q!B6Ekx zL<*KZi=7VAYi(weO`cz{xiY7d)`(7-Wh!U4%H-KsaBYZkkVFkO902oz$}cGu^HYsx zRhXs`V~aW<&eO1iJg(NzOyXwL>ZiQs&W0EI)L%}KH4iXek+ZQykdl>68cm2KxgjH zFCZj*H@RsUptOlWcg^)Z-w#~Zc1k77^Gd#5rpBa}wJuk;@3BRl2^p~k7PTf3(MWnZ z43Q9zZAyg1P0TS^e0v*$PZqhy9Z;lty`#dEJp(jmZOIP{+=~pTj~a^q8T3GLW+Jrh3ER)M1NjzO577QAoJk7%v`)p9lHKHcN zWhr@@GUVaiWaoIIHwpxd{xDzN(w2TUhuV?7 z$8P8FZ)S`Tqo-PTS@`{>P_Y?`3Oz@$@ftzio@p8#P+`)COH1S)t3rWx5=9 z>GF_SqU)Cbr>QymS5QHrE$KB)H>sJItW8aS=?a{GCwT+{wc!2!Go_k$-Tj_>pYt8Z zx3Ab$^L6v6epT+HS@jA$f!pK5w>~m{+4Uy3m*H>IFWfy&6J66kH$HUtv-=$#j*zkc)MFui`qL(2)it9ik*uxr)et-zcm_U5j64E`60 z2mN1t!#o3Y;!n&o;zpTS{xqL*8(yXRZuJKK5q4j#$ivqS(6)$|ok!MoH*er;!H;jr Gv;PM+G3Vg` diff --git a/gui/main_window.py b/gui/main_window.py index 257ec21..16dd54f 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -311,7 +311,7 @@ class MainWindow(QMainWindow): log.info(f"Added {added_count} new asset paths: {newly_added_paths}") self.statusBar().showMessage(f"Added {added_count} asset(s). Updating preview...", 3000) - mode, selected_preset_text = self.preset_editor_widget.get_selected_preset_mode() + mode, selected_display_name, preset_file_path = self.preset_editor_widget.get_selected_preset_mode() if mode == "llm": log.info(f"LLM Interpretation selected. Preparing LLM prediction for {len(newly_added_paths)} new paths.") @@ -330,8 +330,9 @@ class MainWindow(QMainWindow): log.info(f"Delegating {len(llm_requests_to_queue)} LLM requests to the handler.") self.llm_interaction_handler.queue_llm_requests_batch(llm_requests_to_queue) # The handler manages starting its own processing internally. - elif mode == "preset" and selected_preset_text: - log.info(f"Preset '{selected_preset_text}' selected. Triggering prediction for {len(newly_added_paths)} new paths.") + elif mode == "preset" and selected_display_name and preset_file_path: + preset_name_for_loading = preset_file_path.stem + log.info(f"Preset '{selected_display_name}' (file: {preset_name_for_loading}.json) selected. Triggering prediction for {len(newly_added_paths)} new paths.") if self.prediction_thread and not self.prediction_thread.isRunning(): log.debug("Starting prediction thread from add_input_paths.") self.prediction_thread.start() @@ -343,7 +344,8 @@ class MainWindow(QMainWindow): self._source_file_lists[input_path_str] = file_list self._pending_predictions.add(input_path_str) log.debug(f"Added '{input_path_str}' to pending predictions. Current pending: {self._pending_predictions}") - self.start_prediction_signal.emit(input_path_str, file_list, selected_preset_text) + # Pass the filename stem for loading, not the display name + self.start_prediction_signal.emit(input_path_str, file_list, preset_name_for_loading) else: log.warning(f"Skipping prediction for {input_path_str} due to extraction error.") elif mode == "placeholder": @@ -446,7 +448,12 @@ class MainWindow(QMainWindow): self.statusBar().showMessage("No assets added to process.", 3000) return - mode, selected_preset_name = self.preset_editor_widget.get_selected_preset_mode() + # mode, selected_preset_name, preset_file_path are relevant here if processing depends on the *loaded* preset's config + # For now, _on_process_requested uses the rules already in unified_model, which should have been generated + # using the correct preset context. The preset name itself isn't directly used by the processing engine, + # as the SourceRule object already contains the necessary preset-derived information or the preset name string. + # We'll rely on the SourceRule objects in unified_model.get_all_source_rules() to be correct. + # mode, selected_display_name, preset_file_path = self.preset_editor_widget.get_selected_preset_mode() output_dir_str = settings.get("output_dir") @@ -694,7 +701,7 @@ class MainWindow(QMainWindow): log.error("RuleBasedPredictionHandler not loaded. Cannot update preview.") self.statusBar().showMessage("Error: Prediction components not loaded.", 5000) return - mode, selected_preset_name = self.preset_editor_widget.get_selected_preset_mode() + mode, selected_display_name, preset_file_path = self.preset_editor_widget.get_selected_preset_mode() if mode == "placeholder": log.debug("Update preview called with placeholder preset selected. Showing existing raw inputs (detailed view).") @@ -749,9 +756,10 @@ class MainWindow(QMainWindow): # Do not return here; let the function exit normally after handling LLM case. # The standard prediction path below will be skipped because mode is 'llm'. - elif mode == "preset" and selected_preset_name: - log.info(f"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items using Preset='{selected_preset_name}'") - self.statusBar().showMessage(f"Updating preview for '{selected_preset_name}'...", 0) + elif mode == "preset" and selected_display_name and preset_file_path: + preset_name_for_loading = preset_file_path.stem + log.info(f"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items using Preset Display='{selected_display_name}' (File Stem='{preset_name_for_loading}')") + self.statusBar().showMessage(f"Updating preview for '{selected_display_name}'...", 0) log.debug("Clearing accumulated rules for new standard preview batch.") self._accumulated_rules.clear() @@ -764,8 +772,8 @@ class MainWindow(QMainWindow): for input_path_str in input_paths: file_list = self._extract_file_list(input_path_str) if file_list is not None: - log.debug(f"[{time.time():.4f}] Emitting start_prediction_signal for: {input_path_str} with {len(file_list)} files.") - self.start_prediction_signal.emit(input_path_str, file_list, selected_preset_name) + log.debug(f"[{time.time():.4f}] Emitting start_prediction_signal for: {input_path_str} with {len(file_list)} files, using preset file stem: {preset_name_for_loading}.") + self.start_prediction_signal.emit(input_path_str, file_list, preset_name_for_loading) # Pass stem for loading else: log.warning(f"[{time.time():.4f}] Skipping standard prediction signal for {input_path_str} due to extraction error.") else: @@ -1066,13 +1074,13 @@ class MainWindow(QMainWindow): log.debug(f"<-- Exiting _handle_prediction_completion for '{input_path}'") - @Slot(str, str) - def _on_preset_selection_changed(self, mode: str, preset_name: str | None): + @Slot(str, str, Path) # mode, display_name, file_path (Path can be None) + def _on_preset_selection_changed(self, mode: str, display_name: str | None, file_path: Path | None ): """ Handles changes in the preset editor selection (preset, LLM, placeholder). Switches between PresetEditorWidget and LLMEditorWidget. """ - log.info(f"Preset selection changed: mode='{mode}', preset_name='{preset_name}'") + log.info(f"Preset selection changed: mode='{mode}', display_name='{display_name}', file_path='{file_path}'") if mode == "llm": log.debug("Switching editor stack to LLM Editor Widget.") @@ -1094,11 +1102,11 @@ class MainWindow(QMainWindow): self.editor_stack.setCurrentWidget(self.preset_editor_widget.json_editor_container) # The PresetEditorWidget's internal logic handles disabling/clearing the editor fields. - if mode == "preset" and preset_name: + if mode == "preset" and display_name: # Use display_name for window title # This might be redundant if the editor handles its own title updates on save/load # but good for consistency. unsaved = self.preset_editor_widget.editor_unsaved_changes - self.setWindowTitle(f"Asset Processor Tool - {preset_name}{'*' if unsaved else ''}") + self.setWindowTitle(f"Asset Processor Tool - {display_name}{'*' if unsaved else ''}") elif mode == "llm": self.setWindowTitle("Asset Processor Tool - LLM Interpretation") else: diff --git a/gui/prediction_handler.py b/gui/prediction_handler.py index 9cdd641..1473e8b 100644 --- a/gui/prediction_handler.py +++ b/gui/prediction_handler.py @@ -39,10 +39,9 @@ if not log.hasHandlers(): def classify_files(file_list: List[str], config: Configuration) -> Dict[str, List[Dict[str, Any]]]: """ - Analyzes a list of files based on configuration rules using a two-pass approach - to group them by asset and determine initial file properties. - Pass 1: Identifies and classifies prioritized bit depth variants. - Pass 2: Classifies extras, general maps (downgrading if primary exists), and ignores. + Analyzes a list of files based on configuration rules to group them by asset + and determine initial file properties, applying prioritization based on + 'priority_keywords' in map_type_mapping. Args: file_list: List of absolute file paths. @@ -53,19 +52,21 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis Example: { 'AssetName1': [ - {'file_path': '/path/to/AssetName1_DISP16.png', 'item_type': 'DISP', 'asset_name': 'AssetName1'}, - {'file_path': '/path/to/AssetName1_DISP.png', 'item_type': 'EXTRA', 'asset_name': 'AssetName1'}, - {'file_path': '/path/to/AssetName1_Color.png', 'item_type': 'COL', 'asset_name': 'AssetName1'} + {'file_path': '/path/to/AssetName1_DISP16.png', 'item_type': 'MAP_DISP', 'asset_name': 'AssetName1'}, + {'file_path': '/path/to/AssetName1_Color.png', 'item_type': 'MAP_COL', 'asset_name': 'AssetName1'} ], # ... other assets } + Files marked as "FILE_IGNORE" will also be included in the output. Returns an empty dict if classification fails or no files are provided. """ - temp_grouped_files = defaultdict(list) - extra_files_to_associate = [] - primary_asset_names = set() - primary_assignments = set() - processed_in_pass1 = set() + classified_files_info: Dict[str, List[Dict[str, Any]]] = defaultdict(list) + file_matches: Dict[str, List[Tuple[str, int, bool]]] = defaultdict(list) # {file_path: [(target_type, rule_index, is_priority), ...]} + files_to_ignore: Set[str] = set() + + # --- DEBUG: Log the input file_list --- + log.info(f"DEBUG_ROO_CLASSIFY_INPUT: classify_files received file_list (len={len(file_list)}): {file_list}") + # --- END DEBUG --- # --- Validation --- if not file_list or not config: @@ -73,20 +74,20 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis return {} if not hasattr(config, 'compiled_map_keyword_regex') or not config.compiled_map_keyword_regex: log.warning("Classification skipped: Missing compiled map keyword regex in config.") + # Proceeding might still classify EXTRA/FILE_IGNORE if those rules exist if not hasattr(config, 'compiled_extra_regex'): log.warning("Configuration object missing 'compiled_extra_regex'. Cannot classify extra files.") - if not hasattr(config, 'compiled_bit_depth_regex_map'): - log.warning("Configuration object missing 'compiled_bit_depth_regex_map'. Cannot prioritize bit depth variants.") + compiled_extra_regex = [] # Provide default to avoid errors + else: + compiled_extra_regex = getattr(config, 'compiled_extra_regex', []) compiled_map_regex = getattr(config, 'compiled_map_keyword_regex', {}) - compiled_extra_regex = getattr(config, 'compiled_extra_regex', []) - compiled_bit_depth_regex_map = getattr(config, 'compiled_bit_depth_regex_map', {}) + # Note: compiled_bit_depth_regex_map is no longer used for primary classification logic here num_map_rules = sum(len(patterns) for patterns in compiled_map_regex.values()) num_extra_rules = len(compiled_extra_regex) - num_bit_depth_rules = len(compiled_bit_depth_regex_map) - log.debug(f"Starting classification for {len(file_list)} files using {num_map_rules} map keyword patterns, {num_bit_depth_rules} bit depth patterns, and {num_extra_rules} extra patterns.") + log.debug(f"Starting classification for {len(file_list)} files using {num_map_rules} map keyword patterns and {num_extra_rules} extra patterns.") # --- Asset Name Extraction Helper --- def get_asset_name(f_path: Path, cfg: Configuration) -> str: @@ -120,155 +121,179 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis log.warning(f"Asset name extraction resulted in empty string for '{filename}'. Using stem: '{asset_name}'.") return asset_name - # --- Pass 1: Prioritized Bit Depth Variants --- - log.debug("--- Starting Classification Pass 1: Prioritized Variants ---") + # --- Pass 1: Collect all potential matches for each file --- + # For each file, find all map_type_mapping rules it matches (both regular and priority keywords). + # Store the target_type, original rule_index, and whether it was a priority match. + log.debug("--- Starting Classification Pass 1: Collect Potential Matches ---") + file_matches: Dict[str, List[Tuple[str, int, bool]]] = defaultdict(list) # {file_path: [(target_type, rule_index, is_priority), ...]} + files_classified_as_extra: Set[str] = set() # Files already classified as EXTRA + + compiled_map_regex = getattr(config, 'compiled_map_keyword_regex', {}) + compiled_extra_regex = getattr(config, 'compiled_extra_regex', []) + for file_path_str in file_list: file_path = Path(file_path_str) filename = file_path.name asset_name = get_asset_name(file_path, config) - processed = False - for target_type, variant_regex in compiled_bit_depth_regex_map.items(): - match = variant_regex.search(filename) - if match: - log.debug(f"PASS 1: File '{filename}' matched PRIORITIZED bit depth variant for type '{target_type}'.") - matched_item_type = target_type + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: Processing file: {file_path_str}") - if (asset_name, matched_item_type) in primary_assignments: - log.warning(f"PASS 1: Primary assignment ({asset_name}, {matched_item_type}) already exists. File '{filename}' will be handled in Pass 2.") - else: - primary_assignments.add((asset_name, matched_item_type)) - log.debug(f" PASS 1: Added primary assignment: ({asset_name}, {matched_item_type})") - primary_asset_names.add(asset_name) - - temp_grouped_files[asset_name].append({ - 'file_path': file_path_str, - 'item_type': matched_item_type, - 'asset_name': asset_name - }) - processed_in_pass1.add(file_path_str) - processed = True - break # Stop checking other variant patterns for this file - - log.debug(f"--- Finished Pass 1. Primary assignments made: {primary_assignments} ---") - - # --- Pass 2: Extras, General Maps, Ignores --- - log.debug("--- Starting Classification Pass 2: Extras, General Maps, Ignores ---") - for file_path_str in file_list: - if file_path_str in processed_in_pass1: - log.debug(f"PASS 2: Skipping '{Path(file_path_str).name}' (processed in Pass 1).") - continue - - file_path = Path(file_path_str) - filename = file_path.name - asset_name = get_asset_name(file_path, config) + # Check for EXTRA files first is_extra = False - is_map = False - - # 1. Check for Extra Files FIRST in Pass 2 for extra_pattern in compiled_extra_regex: if extra_pattern.search(filename): - log.debug(f"PASS 2: File '{filename}' matched EXTRA pattern: {extra_pattern.pattern}") - extra_files_to_associate.append((file_path_str, filename)) + if "BoucleChunky001_DISP_1K_METALNESS.png" in filename and extra_pattern.search(filename): + log.info(f"DEBUG_ROO: EXTRA MATCH: File '{filename}' matched EXTRA pattern: {extra_pattern.pattern}") + log.debug(f"PASS 1: File '{filename}' matched EXTRA pattern: {extra_pattern.pattern}") + # For EXTRA, we assign it directly and don't check map rules for this file + classified_files_info[asset_name].append({ + 'file_path': file_path_str, + 'item_type': "EXTRA", + 'asset_name': asset_name + }) + files_classified_as_extra.add(file_path_str) is_extra = True break - if is_extra: - continue + if "BoucleChunky001_DISP_1K_METALNESS.png" in filename and not is_extra: # after the extra loop + log.info(f"DEBUG_ROO: EXTRA CHECK FAILED for {filename}. is_extra: {is_extra}") - # 2. Check for General Map Files in Pass 2 + if "BoucleChunky001_DISP_1K_METALNESS.png" in filename and not is_extra: + log.info(f"DEBUG_ROO: EXTRA CHECK FAILED for {filename}. is_extra: {is_extra}") + + if is_extra: + continue # Move to the next file + + # If not EXTRA, check for MAP matches (collect all potential matches) for target_type, patterns_list in compiled_map_regex.items(): - for compiled_regex, original_keyword, rule_index in patterns_list: + for compiled_regex, original_keyword, rule_index, is_priority in patterns_list: match = compiled_regex.search(filename) if match: - try: - # map_type_mapping_list = config.map_type_mapping # Old gloss logic source - # matched_rule_details = map_type_mapping_list[rule_index] # Old gloss logic source - # is_gloss_flag = matched_rule_details.get('is_gloss_source', False) # Old gloss logic - log.debug(f" PASS 2: Match found! Rule Index: {rule_index}, Keyword: '{original_keyword}', Target: '{target_type}'") # Removed Gloss from log - except Exception as e: - log.exception(f" PASS 2: Error accessing rule details for index {rule_index}: {e}") + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: PASS 1 MAP MATCH: File '{filename}' matched keyword '{original_keyword}' (priority: {is_priority}) for target type '{target_type}' (Rule Index: {rule_index}).") + log.debug(f" PASS 1: File '{filename}' matched keyword '{original_keyword}' (priority: {is_priority}) for target type '{target_type}' (Rule Index: {rule_index}).") + file_matches[file_path_str].append((target_type, rule_index, is_priority)) - # *** Crucial Check: Has a prioritized variant claimed this type? *** - if (asset_name, target_type) in primary_assignments: - log.debug(f"PASS 2: File '{filename}' matched '{original_keyword}' for type '{target_type}', but primary already assigned via Pass 1. Classifying as EXTRA.") - matched_item_type = "EXTRA" - # is_gloss_flag = False # Old gloss logic - else: - log.debug(f"PASS 2: File '{filename}' matched '{original_keyword}' for item_type '{target_type}'.") - matched_item_type = target_type + log.debug(f"--- Finished Pass 1. Collected matches for {len(file_matches)} files. ---") - temp_grouped_files[asset_name].append({ - 'file_path': file_path_str, - 'item_type': matched_item_type, - 'asset_name': asset_name - }) - is_map = True - break - if is_map: - break + # --- Pass 2: Determine Trumped Regular Matches --- + # Identify which regular matches are trumped by a priority match for the same rule_index within the asset. + log.debug("--- Starting Classification Pass 2: Determine Trumped Regular Matches ---") - # 3. Handle Unmatched Files in Pass 2 (Not Extra, Not Map) - if not is_extra and not is_map: - log.debug(f"PASS 2: File '{filename}' did not match any map/extra pattern. Grouping under asset '{asset_name}' as FILE_IGNORE.") - temp_grouped_files[asset_name].append({ - 'file_path': file_path_str, - 'item_type': "FILE_IGNORE", - 'asset_name': asset_name - }) + trumped_regular_matches: Set[Tuple[str, int]] = set() # Set of (file_path_str, rule_index) pairs that are trumped - log.debug("--- Finished Pass 2 ---") + # First, determine which rule_indices have *any* priority match across the entire asset + rule_index_has_priority_match_in_asset: Set[int] = set() + for file_path_str, matches in file_matches.items(): + for match_target, match_rule_index, match_is_priority in matches: + if match_is_priority: + rule_index_has_priority_match_in_asset.add(match_rule_index) - # --- Determine Primary Asset Name for Extra Association (using Pass 1 results) --- - final_primary_asset_name = None - if primary_asset_names: - primary_map_asset_names_pass1 = [ - f_info['asset_name'] - for asset_files in temp_grouped_files.values() - for f_info in asset_files - if f_info['asset_name'] in primary_asset_names and (f_info['asset_name'], f_info['item_type']) in primary_assignments - ] - if primary_map_asset_names_pass1: - name_counts = Counter(primary_map_asset_names_pass1) - most_common_names = name_counts.most_common() - final_primary_asset_name = most_common_names[0][0] - if len(most_common_names) > 1 and most_common_names[0][1] == most_common_names[1][1]: - tied_names = sorted([name for name, count in most_common_names if count == most_common_names[0][1]]) - final_primary_asset_name = tied_names[0] - log.warning(f"Multiple primary asset names tied for most common based on Pass 1: {tied_names}. Using '{final_primary_asset_name}' for associating extra files.") - log.debug(f"Determined primary asset name for extras based on Pass 1 primary maps: '{final_primary_asset_name}'") - else: - log.warning("Primary asset names set (from Pass 1) was populated, but no corresponding groups found. Falling back.") + log.debug(f" Rule indices with priority matches in asset: {sorted(list(rule_index_has_priority_match_in_asset))}") - if not final_primary_asset_name: - if temp_grouped_files and extra_files_to_associate: - fallback_name = sorted(temp_grouped_files.keys())[0] - final_primary_asset_name = fallback_name - log.warning(f"No primary map files found in Pass 1. Associating extras with first group found alphabetically: '{final_primary_asset_name}'.") - elif extra_files_to_associate: - log.warning(f"Could not determine any asset name to associate {len(extra_files_to_associate)} extra file(s) with. They will be ignored.") - else: - log.debug("No primary asset name determined (no maps or extras found).") + # Then, for each file, check its matches against the rules that had priority matches + for file_path_str in file_list: + if file_path_str in files_classified_as_extra: + continue + + matches_for_this_file = file_matches.get(file_path_str, []) + + # Determine if this file has any priority match for a given rule_index + file_has_priority_match_for_rule: Dict[int, bool] = defaultdict(bool) + for match_target, match_rule_index, match_is_priority in matches_for_this_file: + if match_is_priority: + file_has_priority_match_for_rule[match_rule_index] = True + + # Determine if this file has any regular match for a given rule_index + file_has_regular_match_for_rule: Dict[int, bool] = defaultdict(bool) + for match_target, match_rule_index, match_is_priority in matches_for_this_file: + if not match_is_priority: + file_has_regular_match_for_rule[match_rule_index] = True + + # Identify trumped regular matches for this file + for match_target, match_rule_index, match_is_priority in matches_for_this_file: + if not match_is_priority: # Only consider regular matches + if match_rule_index in rule_index_has_priority_match_in_asset: + # This regular match is for a rule_index that had a priority match somewhere in the asset + if not file_has_priority_match_for_rule[match_rule_index]: + # And this specific file did NOT have a priority match for this rule_index + trumped_regular_matches.add((file_path_str, match_rule_index)) + log.debug(f" File '{Path(file_path_str).name}': Regular match for Rule Index {match_rule_index} is trumped.") + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: TRUMPED: File '{Path(file_path_str).name}': Regular match for Rule Index {match_rule_index} (target {match_target}) is trumped.") + if "BoucleChunky001" in file_path_str: # Check if it was actually added by checking the set, or just log if the condition was met + if (file_path_str, match_rule_index) in trumped_regular_matches: + log.info(f"DEBUG_ROO: TRUMPED: File '{Path(file_path_str).name}': Regular match for Rule Index {match_rule_index} (target {match_target}) is trumped.") - # --- Associate Extra Files (collected in Pass 2) --- - if final_primary_asset_name and extra_files_to_associate: - log.debug(f"Associating {len(extra_files_to_associate)} extra file(s) with primary asset '{final_primary_asset_name}'") - for file_path_str, filename in extra_files_to_associate: - if not any(f['file_path'] == file_path_str for f in temp_grouped_files[final_primary_asset_name]): - temp_grouped_files[final_primary_asset_name].append({ - 'file_path': file_path_str, - 'item_type': "EXTRA", - 'asset_name': final_primary_asset_name - }) + log.debug(f"--- Finished Pass 2. Identified {len(trumped_regular_matches)} trumped regular matches. ---") + + # --- Pass 3: Final Assignment & Inter-Entry Resolution --- + # Iterate through files, apply ignore rules, and then apply earliest rule wins for remaining valid matches. + log.debug("--- Starting Classification Pass 3: Final Assignment ---") + + final_file_assignments: Dict[str, str] = {} # {file_path: final_item_type} + + + for file_path_str in file_list: + # Check if the file was already classified as EXTRA in Pass 1 and added to classified_files_info + if file_path_str in files_classified_as_extra: + log.debug(f" Final Assignment: Skipping '{Path(file_path_str).name}' as it was already classified as EXTRA in Pass 1.") + continue # Skip this file in Pass 3 as it's already handled + + asset_name = get_asset_name(Path(file_path_str), config) # Need asset name for the final output structure + + # Get valid matches for this file after considering intra-entry priority trumps regular + valid_matches = [] + for match_target, match_rule_index, match_is_priority in file_matches.get(file_path_str, []): + if (file_path_str, match_rule_index) not in trumped_regular_matches: + valid_matches.append((match_target, match_rule_index, match_is_priority)) + log.debug(f" File '{Path(file_path_str).name}': Valid match - Target: '{match_target}', Rule Index: {match_rule_index}, Priority: {match_is_priority}") else: - log.debug(f"Skipping duplicate association of extra file: {filename}") - elif extra_files_to_associate: - pass + log.debug(f" File '{Path(file_path_str).name}': Invalid match (trumped by priority) - Target: '{match_target}', Rule Index: {match_rule_index}, Priority: {match_is_priority}") + + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: PASS 3 PRE-ASSIGN: File '{Path(file_path_str).name}'. Valid matches: {valid_matches}") + + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: PASS 3 PRE-ASSIGN: File '{Path(file_path_str).name}'. Valid matches: {valid_matches}") + + final_item_type = "FILE_IGNORE" # Default to ignore if no valid matches + if valid_matches: + # Apply earliest rule wins among valid matches + best_match = min(valid_matches, key=lambda x: x[1]) # Find match with lowest rule_index + final_item_type = best_match[0] # Assign the target_type of the best match + log.debug(f" File '{Path(file_path_str).name}': Best valid match -> Target: '{best_match[0]}', Rule Index: {best_match[1]}. Final type: '{final_item_type}'.") + else: + log.debug(f" File '{Path(file_path_str).name}'': No valid matches after filtering. Final type: '{final_item_type}'.") + + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: PASS 3 FINAL ASSIGN: File '{Path(file_path_str).name}' -> Final Type: '{final_item_type}'") + final_file_assignments[file_path_str] = final_item_type + + if "BoucleChunky001" in file_path_str: + log.info(f"DEBUG_ROO: PASS 3 FINAL ASSIGN: File '{Path(file_path_str).name}' -> Final Type: '{final_item_type}'") + + # Add the file info to the classified_files_info structure + log.info(f"DEBUG_ROO: PASS 3 APPEND: Appending file '{Path(file_path_str).name}' with type '{final_item_type}' to classified_files_info['{asset_name}']") + classified_files_info[asset_name].append({ + 'file_path': file_path_str, + 'item_type': final_item_type, + 'asset_name': asset_name + }) + log.debug(f" Final Grouping: '{Path(file_path_str).name}' -> '{final_item_type}' (Asset: '{asset_name}')") - log.debug(f"Classification complete. Found {len(temp_grouped_files)} potential assets.") - return dict(temp_grouped_files) + log.debug(f"Classification complete. Found {len(classified_files_info)} potential assets.") + # Enhanced logging for the content of classified_files_info + boucle_chunky_data = { + key: val for key, val in classified_files_info.items() + if 'BoucleChunky001' in key or any('BoucleChunky001' in (f_info.get('file_path','')) for f_info in val) + } + import json # Make sure json is imported if not already at top of file + log.info(f"DEBUG_ROO: Final classified_files_info for BoucleChunky001 (content): \n{json.dumps(boucle_chunky_data, indent=2)}") + return dict(classified_files_info) class RuleBasedPredictionHandler(BasePredictionHandler): @@ -367,7 +392,8 @@ class RuleBasedPredictionHandler(BasePredictionHandler): source_rule = SourceRule( input_path=input_source_identifier, supplier_identifier=supplier_identifier, - preset_name=preset_name + # Use the internal display name from the config object + preset_name=config.internal_display_preset_name ) asset_rules = [] file_type_definitions = config._core_settings.get('FILE_TYPE_DEFINITIONS', {}) @@ -402,7 +428,7 @@ class RuleBasedPredictionHandler(BasePredictionHandler): break if determined_by_rule: break - + # Check for Decal type based on keywords in asset name (if not already Model) if not determined_by_rule and "Decal" in asset_type_keys: decal_keywords = asset_category_rules.get('decal_keywords', []) @@ -444,11 +470,11 @@ class RuleBasedPredictionHandler(BasePredictionHandler): if item_def and item_def.get('standard_type') in material_indicators: has_material_map = True break - + if has_material_map: predicted_asset_type = "Surface" log.debug(f"Asset '{asset_name}' classified as 'Surface' due to material indicators.") - + # 3. Final validation: Ensure predicted_asset_type is a valid key. if predicted_asset_type not in asset_type_keys: log.warning(f"Derived AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. " @@ -463,23 +489,22 @@ class RuleBasedPredictionHandler(BasePredictionHandler): base_item_type = file_info['item_type'] target_asset_name_override = file_info['asset_name'] final_item_type = base_item_type - if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]: - final_item_type = f"MAP_{base_item_type}" + # The classification logic now returns the final item_type directly, + # including "FILE_IGNORE" and correctly prioritized MAP_ types. + # No need for the old MAP_ prefixing logic here. - if file_type_definitions and final_item_type not in file_type_definitions and base_item_type not in ["FILE_IGNORE", "EXTRA"]: - log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS. Setting to FILE_IGNORE.") + # Validate the final_item_type against definitions, unless it's EXTRA or FILE_IGNORE + if final_item_type not in ["EXTRA", "FILE_IGNORE"] and file_type_definitions and final_item_type not in file_type_definitions: + log.warning(f"Predicted ItemType '{final_item_type}' for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS. Setting to FILE_IGNORE.") final_item_type = "FILE_IGNORE" - # is_gloss_source_value = file_info.get('is_gloss_source', False) # Removed - file_rule = FileRule( file_path=file_info['file_path'], item_type=final_item_type, - item_type_override=final_item_type, + item_type_override=final_item_type, # item_type_override defaults to item_type target_asset_name_override=target_asset_name_override, output_format_override=None, - # is_gloss_source=is_gloss_source_value if isinstance(is_gloss_source_value, bool) else False, # Removed resolution_override=None, channel_merge_instructions={}, ) @@ -489,6 +514,18 @@ class RuleBasedPredictionHandler(BasePredictionHandler): source_rule.assets = asset_rules source_rules_list.append(source_rule) + # DEBUG: Log the structure of the source_rule being emitted + if source_rule and source_rule.assets: + for asset_r_idx, asset_r in enumerate(source_rule.assets): + log.info(f"DEBUG_ROO_EMIT: Source '{input_source_identifier}', Asset {asset_r_idx} ('{asset_r.asset_name}') has {len(asset_r.files)} FileRules.") + for fr_idx, fr in enumerate(asset_r.files): + log.info(f"DEBUG_ROO_EMIT: FR {fr_idx}: Path='{fr.file_path}', Type='{fr.item_type}', TargetAsset='{fr.target_asset_name_override}'") + elif source_rule: + log.info(f"DEBUG_ROO_EMIT: Emitting SourceRule for {input_source_identifier} but it has no assets.") + else: + log.info(f"DEBUG_ROO_EMIT: Attempting to emit for {input_source_identifier}, but source_rule object is None.") + # END DEBUG + except Exception as e: log.exception(f"Error building rule hierarchy for source '{input_source_identifier}': {e}") raise RuntimeError(f"Error building rule hierarchy: {e}") from e diff --git a/gui/preset_editor_widget.py b/gui/preset_editor_widget.py index 01ce841..b06c7a4 100644 --- a/gui/preset_editor_widget.py +++ b/gui/preset_editor_widget.py @@ -36,8 +36,8 @@ class PresetEditorWidget(QWidget): # Signal emitted when presets list changes (saved, deleted, new) presets_changed_signal = Signal() # Signal emitted when the selected preset (or LLM/Placeholder) changes - # Emits: mode ("preset", "llm", "placeholder"), preset_name (str or None) - preset_selection_changed_signal = Signal(str, str) + # Emits: mode ("preset", "llm", "placeholder"), display_name (str or None), file_path (Path or None) + preset_selection_changed_signal = Signal(str, str, Path) def __init__(self, parent=None): super().__init__(parent) @@ -296,8 +296,22 @@ class PresetEditorWidget(QWidget): log.warning(msg) else: for preset_path in presets: - item = QListWidgetItem(preset_path.stem) - item.setData(Qt.ItemDataRole.UserRole, preset_path) + preset_display_name = preset_path.stem # Fallback + try: + with open(preset_path, 'r', encoding='utf-8') as f: + preset_content = json.load(f) + internal_name = preset_content.get("preset_name") + if internal_name and isinstance(internal_name, str) and internal_name.strip(): + preset_display_name = internal_name.strip() + else: + log.warning(f"Preset file {preset_path.name} is missing 'preset_name' or it's empty. Using filename stem '{preset_path.stem}' as display name.") + except json.JSONDecodeError: + log.error(f"Failed to parse JSON from {preset_path.name}. Using filename stem '{preset_path.stem}' as display name.") + except Exception as e: + log.error(f"Error reading {preset_path.name}: {e}. Using filename stem '{preset_path.stem}' as display name.") + + item = QListWidgetItem(preset_display_name) + item.setData(Qt.ItemDataRole.UserRole, preset_path) # Store the path for loading self.editor_preset_list.addItem(item) log.info(f"Loaded {len(presets)} presets into editor list.") @@ -525,7 +539,8 @@ class PresetEditorWidget(QWidget): log.debug(f"PresetEditor: currentItemChanged signal triggered. current: {current_item.text() if current_item else 'None'}") mode = "placeholder" - preset_name = None + display_name_to_emit = None # Changed from preset_name + file_path_to_emit = None # New variable for Path # Check for unsaved changes before proceeding if self.check_unsaved_changes(): @@ -540,41 +555,53 @@ class PresetEditorWidget(QWidget): # Determine mode and preset name based on selection if current_item: item_data = current_item.data(Qt.ItemDataRole.UserRole) + current_display_text = current_item.text() # This is the internal name from populate_presets + if item_data == "__PLACEHOLDER__": log.debug("Placeholder item selected.") self._clear_editor() self._set_editor_enabled(False) mode = "placeholder" + display_name_to_emit = None + file_path_to_emit = None self._last_valid_preset_name = None # Clear last valid name elif item_data == "__LLM__": log.debug("LLM Interpretation item selected.") self._clear_editor() self._set_editor_enabled(False) mode = "llm" - # Keep _last_valid_preset_name as it was - elif isinstance(item_data, Path): - log.debug(f"Loading preset for editing: {current_item.text()}") - preset_path = item_data - self._load_preset_for_editing(preset_path) - self._last_valid_preset_name = preset_path.stem + display_name_to_emit = None # LLM mode has no specific preset display name + file_path_to_emit = None + # Keep _last_valid_preset_name as it was (it should be the display name) + elif isinstance(item_data, Path): # item_data is the Path object for a preset + log.debug(f"Loading preset for editing: {current_display_text}") + preset_file_path_obj = item_data + self._load_preset_for_editing(preset_file_path_obj) + # _last_valid_preset_name should store the display name for delegate use + self._last_valid_preset_name = current_display_text mode = "preset" - preset_name = self._last_valid_preset_name - else: + display_name_to_emit = current_display_text + file_path_to_emit = preset_file_path_obj + else: # Should not happen if list is populated correctly log.error(f"Invalid data type for preset path: {type(item_data)}. Clearing editor.") self._clear_editor() self._set_editor_enabled(False) - mode = "placeholder" # Treat as placeholder on error + mode = "placeholder" + display_name_to_emit = None + file_path_to_emit = None self._last_valid_preset_name = None - else: + else: # No current_item (e.g., list cleared) log.debug("No preset selected. Clearing editor.") self._clear_editor() self._set_editor_enabled(False) mode = "placeholder" + display_name_to_emit = None + file_path_to_emit = None self._last_valid_preset_name = None - # Emit the signal regardless of what was selected - log.debug(f"Emitting preset_selection_changed_signal: mode='{mode}', preset_name='{preset_name}'") - self.preset_selection_changed_signal.emit(mode, preset_name) + # Emit the signal with all three arguments + log.debug(f"Emitting preset_selection_changed_signal: mode='{mode}', display_name='{display_name_to_emit}', file_path='{file_path_to_emit}'") + self.preset_selection_changed_signal.emit(mode, display_name_to_emit, file_path_to_emit) def _gather_editor_data(self) -> dict: """Gathers data from all editor UI widgets and returns a dictionary.""" @@ -757,22 +784,25 @@ class PresetEditorWidget(QWidget): # --- Public Access Methods for MainWindow --- - def get_selected_preset_mode(self) -> tuple[str, str | None]: + def get_selected_preset_mode(self) -> tuple[str, str | None, Path | None]: """ - Returns the current selection mode and preset name (if applicable). - Returns: tuple(mode_string, preset_name_string_or_None) + Returns the current selection mode, display name, and file path for loading. + Returns: tuple(mode_string, display_name_string_or_None, file_path_or_None) mode_string can be "preset", "llm", "placeholder" """ current_item = self.editor_preset_list.currentItem() if current_item: item_data = current_item.data(Qt.ItemDataRole.UserRole) + display_text = current_item.text() # This is now the internal name + if item_data == "__PLACEHOLDER__": - return "placeholder", None + return "placeholder", None, None elif item_data == "__LLM__": - return "llm", None + return "llm", None, None # LLM mode doesn't have a specific preset file path elif isinstance(item_data, Path): - return "preset", item_data.stem - return "placeholder", None # Default or if no item selected + # For a preset, display_text is the internal name, item_data is the Path + return "preset", display_text, item_data # Return internal name and path + return "placeholder", None, None # Default or if no item selected def get_last_valid_preset_name(self) -> str | None: """