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
272 lines
8.8 KiB
Python
Executable File
272 lines
8.8 KiB
Python
Executable File
#!/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")
|