Implemented Item type priority handling ( DISP16 )
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user