feat(recipe_scanner): prioritize local sibling images and persist repairs

Updated image path resolution logic to prioritize local sibling images in the same directory as recipes. When a stored image path differs from a local sibling, the system now automatically updates the recipe file to use the local path and persists this repair. This improves reliability when recipe assets are moved or reorganized, ensuring images remain accessible even if original paths become invalid.
This commit is contained in:
Will Miao
2026-01-08 15:53:01 +08:00
parent b57a317c82
commit f62b3f62be
2 changed files with 47 additions and 30 deletions

View File

@@ -437,14 +437,25 @@ class RecipeScanner:
logger.warning(f"Missing required fields in {recipe_path}") logger.warning(f"Missing required fields in {recipe_path}")
continue continue
# Ensure the image file exists # Ensure the image file exists and prioritize local siblings
image_path = recipe_data.get('file_path') image_path = recipe_data.get('file_path')
if not os.path.exists(image_path): if image_path:
recipe_dir = os.path.dirname(recipe_path) recipe_dir = os.path.dirname(recipe_path)
image_filename = os.path.basename(image_path) image_filename = os.path.basename(image_path)
alternative_path = os.path.join(recipe_dir, image_filename) local_sibling_path = os.path.normpath(os.path.join(recipe_dir, image_filename))
if os.path.exists(alternative_path):
recipe_data['file_path'] = alternative_path # If local sibling exists and stored path is different, prefer local
if os.path.exists(local_sibling_path) and os.path.normpath(image_path) != local_sibling_path:
recipe_data['file_path'] = local_sibling_path
# Persist the repair
try:
with open(recipe_path, 'w', encoding='utf-8') as f:
json.dump(recipe_data, f, indent=4, ensure_ascii=False)
logger.info(f"Updated recipe image path to local sibling: {local_sibling_path}")
except Exception as e:
logger.warning(f"Failed to persist repair for {recipe_path}: {e}")
elif not os.path.exists(image_path):
logger.warning(f"Recipe image not found and no local sibling: {image_path}")
# Ensure loras array exists # Ensure loras array exists
if 'loras' not in recipe_data: if 'loras' not in recipe_data:
@@ -823,31 +834,22 @@ class RecipeScanner:
logger.warning(f"Missing required field '{field}' in {recipe_path}") logger.warning(f"Missing required field '{field}' in {recipe_path}")
return None return None
# Ensure the image file exists # Ensure the image file exists and prioritize local siblings
image_path = recipe_data.get('file_path') image_path = recipe_data.get('file_path')
normalized_image_path = os.path.normpath(image_path) if image_path else image_path
path_updated = False path_updated = False
if image_path and normalized_image_path != image_path: if image_path:
recipe_data['file_path'] = normalized_image_path
image_path = normalized_image_path
path_updated = True
if image_path and not os.path.exists(image_path):
logger.warning(f"Recipe image not found: {image_path}")
# Try to find the image in the same directory as the recipe
recipe_dir = os.path.dirname(recipe_path) recipe_dir = os.path.dirname(recipe_path)
image_filename = os.path.basename(image_path) image_filename = os.path.basename(image_path)
alternative_path = os.path.join(recipe_dir, image_filename) local_sibling_path = os.path.normpath(os.path.join(recipe_dir, image_filename))
if os.path.exists(alternative_path):
normalized_alternative = os.path.normpath(alternative_path) # If local sibling exists and stored path is different, prefer local
recipe_data['file_path'] = normalized_alternative if os.path.exists(local_sibling_path) and os.path.normpath(image_path) != local_sibling_path:
image_path = normalized_alternative recipe_data['file_path'] = local_sibling_path
image_path = local_sibling_path
path_updated = True path_updated = True
logger.info( logger.info("Updated recipe image path to local sibling: %s", local_sibling_path)
"Updated recipe image path to %s after relocating asset", normalized_alternative elif not os.path.exists(image_path):
) logger.warning(f"Recipe image not found and no local sibling: {image_path}")
else:
logger.warning(f"Could not find alternative image path for {image_path}")
if path_updated: if path_updated:
self._write_recipe_file(recipe_path, recipe_data) self._write_recipe_file(recipe_path, recipe_data)
@@ -1446,8 +1448,17 @@ class RecipeScanner:
# Get paginated items # Get paginated items
paginated_items = filtered_data[start_idx:end_idx] paginated_items = filtered_data[start_idx:end_idx]
# Add inLibrary information for each lora # Add inLibrary information and URLs for each recipe
for item in paginated_items: for item in paginated_items:
# Format file path to URL
if 'file_path' in item:
item['file_url'] = self._format_file_url(item['file_path'])
# Format dates for display
for date_field in ['created_date', 'modified']:
if date_field in item:
item[f"{date_field}_formatted"] = self._format_timestamp(item[date_field])
if 'loras' in item: if 'loras' in item:
item['loras'] = [self._enrich_lora_entry(dict(lora)) for lora in item['loras']] item['loras'] = [self._enrich_lora_entry(dict(lora)) for lora in item['loras']]
if item.get('checkpoint'): if item.get('checkpoint'):

View File

@@ -40,10 +40,16 @@ class RecipeCard {
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length; const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length;
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0; const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
// Ensure file_url exists, fallback to file_path if needed // Ensure file_url exists, fallback to API URL if needed
const previewUrl = this.recipe.file_url || let previewUrl = this.recipe.file_url;
(this.recipe.file_path ? `/loras_static/root1/preview/${this.recipe.file_path.split('/').pop()}` : if (!previewUrl) {
'/loras_static/images/no-preview.png'); if (this.recipe.file_path) {
const encodedPath = encodeURIComponent(this.recipe.file_path.replace(/\\/g, '/'));
previewUrl = `/api/lm/previews?path=${encodedPath}`;
} else {
previewUrl = '/loras_static/images/no-preview.png';
}
}
const isDuplicatesMode = getCurrentPageState().duplicatesMode; const isDuplicatesMode = getCurrentPageState().duplicatesMode;
const autoplayOnHover = state?.global?.settings?.autoplay_on_hover === true; const autoplayOnHover = state?.global?.settings?.autoplay_on_hover === true;