feat: Visual Asset Management System - Gallery, Organization & Smart Labeling

IMPLEMENTED SYSTEMS:
 Thumbnail Grid Gallery (1,166 images, searchable, filterable)
 Smart Asset Organizer (auto-categorize, rename, organize into Slovenian folders)
 Hover Preview documentation (VS Code integration)
 Smart Auto-Labeling (descriptive naming convention)

FILES:
- tools/asset_gallery.html: Interactive web gallery with modal preview
- scripts/smart_asset_organizer.py: Automated organization script
- docs/VISUAL_ASSET_SYSTEM.md: Complete documentation

FEATURES:
- Live search & category filters
- Modal image preview
- Dry-run mode for safe testing
- Slovenian folder structure (liki, biomi, zgradbe, oprema, etc.)
- Auto-labeling with {category}_{description}_style32.png format
- Organization manifest tracking

Asset Count: 1,166 images (576 MB)
Ready for ADHD-friendly visual workflow
This commit is contained in:
2026-01-04 19:04:33 +01:00
parent 908c048e4e
commit aefe53275f
3 changed files with 1120 additions and 0 deletions

271
scripts/smart_asset_organizer.py Executable file
View File

@@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""
Smart Asset Organization System
Reorganizes all assets into logical folder structure with smart naming
"""
import os
import shutil
from pathlib import Path
from typing import Dict, List
import json
# Base directories
PROJECT_ROOT = Path(__file__).parent.parent
ASSETS_DIR = PROJECT_ROOT / "assets"
SLIKE_DIR = ASSETS_DIR / "slike 🟢"
STYLE32_DIR = ASSETS_DIR / "images" / "STYLE_32_SESSION_JAN_04"
# Target organization structure
TARGET_STRUCTURE = {
"liki": { # Characters
"gronk": ["gronk", "grok"],
"kai": ["kai"],
"ana": ["ana"],
"zombiji": ["zombie"],
"npcs": ["npc", "farmer", "priest", "blacksmith", "merchant", "mayor", "lawyer"]
},
"biomi": { # Biomes
"desert": ["desert", "sand"],
"gore": ["mountain", "rock", "boulder"],
"džungla": ["jungle", "tropical"],
"močvirje": ["swamp", "mud"],
"arktika": ["arctic", "ice", "snow"]
},
"zgradbe": { # Buildings
"hiše": ["house", "home", "farmhouse"],
"javne": ["church", "town_hall", "clinic", "bakery", "tavern"],
"kmetijske": ["barn", "silo", "greenhouse", "windmill"],
"delavnice": ["workshop", "blacksmith", "laboratory", "mint"]
},
"objekti": { # Props
"pohištvo": ["chair", "table", "bed", "sofa", "wardrobe"],
"shranjevanje": ["chest", "barrel", "crate", "shelf"],
"razsvetljava": ["lantern", "torch", "lamp"],
"dekoracije": ["statue", "fountain", "monument", "grave"]
},
"narava": { # Nature
"rastline": ["plant", "tree", "bush", "flower", "grass"],
"pridelki": ["crop", "wheat", "carrot", "tomato", "potato", "cabbage"],
"živali": ["cow", "sheep", "chicken", "pig", "wolf", "rabbit"]
},
"oprema": { # Equipment
"orožje": ["sword", "axe", "bow", "spear", "dagger", "mace", "weapon"],
"orodja": ["pickaxe", "shovel", "hoe", "hammer", "saw", "tool"],
"zaščita": ["armor", "shield", "helmet"]
},
"vmesnik": { # UI
"gumbi": ["button"],
"ikone": ["icon"],
"vrstice": ["bar"],
"okna": ["panel", "window", "dialogue"]
},
"teren": ["terrain"], # Terrain tiles
"notranjost": ["interior"], # Interior objects
"učinki": ["effect", "particle", "spark", "glow"], # VFX
"kreature_mutanti": ["mutant", "radioactive", "three_eyed", "two_headed"] # Mutants
}
def detect_category_and_subfolder(filename: str) -> tuple:
"""
Detect the appropriate category and subfolder for a file based on its name
Returns: (category, subfolder) or (category, None) if no subfolder
"""
lower_name = filename.lower()
# Check nested categories first
for category, subfolders in TARGET_STRUCTURE.items():
if isinstance(subfolders, dict):
for subfolder, keywords in subfolders.items():
if any(keyword in lower_name for keyword in keywords):
return (category, subfolder)
else:
# Simple list of keywords
if any(keyword in lower_name for keyword in subfolders):
return (category, None)
return ("ostalo", None) # Default: "other"
def generate_smart_name(filepath: Path, category: str, subfolder: str = None) -> str:
"""
Generate a smart, descriptive name for an asset
Format: {category}_{content_description}_style32.png (if from Style 32)
"""
filename = filepath.stem
extension = filepath.suffix
# Check if it's from Style 32
is_style32 = "STYLE_32" in str(filepath) or "style32" in filename.lower()
# Clean up existing name
clean_name = filename.lower()
clean_name = clean_name.replace("_style32", "").replace("style32", "")
clean_name = clean_name.replace("interior_", "").replace("terrain_", "")
# Remove timestamp patterns (e.g., _1767549405033)
import re
clean_name = re.sub(r'_\d{13}', '', clean_name)
# Build new name
parts = []
if category != "ostalo":
parts.append(category)
if subfolder:
parts.append(subfolder)
parts.append(clean_name)
if is_style32:
parts.append("style32")
new_name = "_".join(parts) + extension
return new_name
def organize_assets(dry_run: bool = True):
"""
Organize all assets into the target structure
Args:
dry_run: If True, only print what would be done without moving files
"""
print("🗂️ Starting Asset Organization...")
print(f"📁 Source directories:")
print(f" - {SLIKE_DIR}")
print(f" - {STYLE32_DIR}")
print()
moved_count = 0
renamed_count = 0
skipped_count = 0
# Create manifest for tracking
manifest = {
"total_processed": 0,
"moved": 0,
"renamed": 0,
"skipped": 0,
"assets": []
}
# Process all PNG files in both directories
all_images = []
all_images.extend(SLIKE_DIR.rglob("*.png"))
all_images.extend(STYLE32_DIR.glob("*.png"))
print(f"📊 Found {len(all_images)} images to process\n")
for img_path in all_images:
category, subfolder = detect_category_and_subfolder(img_path.name)
new_name = generate_smart_name(img_path, category, subfolder)
# Determine target directory
target_dir = SLIKE_DIR / category
if subfolder:
target_dir = target_dir / subfolder
target_path = target_dir / new_name
# Check if file already exists at target
if target_path.exists() and target_path != img_path:
print(f"⚠️ SKIP (exists): {img_path.name}")
skipped_count += 1
continue
# Create directory if needed
if not dry_run:
target_dir.mkdir(parents=True, exist_ok=True)
# Show what we're doing
if img_path != target_path:
action = "MOVE" if img_path.parent != target_path.parent else "RENAME"
print(f"{action}: {img_path.name}")
print(f"{target_path.relative_to(ASSETS_DIR)}")
print()
if action == "MOVE":
moved_count += 1
else:
renamed_count += 1
# Actually move/rename if not dry run
if not dry_run:
shutil.move(str(img_path), str(target_path))
# Add to manifest
manifest["assets"].append({
"original": str(img_path.relative_to(PROJECT_ROOT)),
"new": str(target_path.relative_to(PROJECT_ROOT)),
"category": category,
"subfolder": subfolder,
"action": action.lower()
})
# Update manifest totals
manifest["total_processed"] = len(all_images)
manifest["moved"] = moved_count
manifest["renamed"] = renamed_count
manifest["skipped"] = skipped_count
# Save manifest
if not dry_run:
manifest_path = PROJECT_ROOT / "docs" / "ASSET_ORGANIZATION_MANIFEST.json"
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
print(f"\n📄 Manifest saved to: {manifest_path}")
# Print summary
print("\n" + "="*60)
print("📊 ORGANIZATION SUMMARY")
print("="*60)
print(f"Total Processed: {len(all_images)}")
print(f"Moved: {moved_count}")
print(f"Renamed: {renamed_count}")
print(f"Skipped: {skipped_count}")
if dry_run:
print("\n⚠️ DRY RUN MODE - No files were actually moved")
print(" Run with --execute to apply changes")
else:
print("\n✅ Organization complete!")
def print_category_preview():
"""Print a preview of how files would be categorized"""
print("\n📂 CATEGORY STRUCTURE PREVIEW:")
print("="*60)
for category, subfolders in TARGET_STRUCTURE.items():
if isinstance(subfolders, dict):
print(f"\n📁 {category.upper()}/")
for subfolder in subfolders.keys():
print(f" └── {subfolder}/")
else:
print(f"\n📁 {category.upper()}/")
if __name__ == "__main__":
import sys
print("🎨 DolinaSmrti - Smart Asset Organization System")
print("="*60)
# Check if --execute flag is provided
execute = "--execute" in sys.argv or "-e" in sys.argv
preview_only = "--preview" in sys.argv or "-p" in sys.argv
if preview_only:
print_category_preview()
else:
print(f"\nMode: {'EXECUTE' if execute else 'DRY RUN'}")
print()
organize_assets(dry_run=not execute)
if not execute:
print("\n💡 TIP: Run with --execute to actually move files")
print(" Run with --preview to see category structure")