🎨 Add 42 new demo assets - Kai animations, zombies, buildings, terrain, environment

Generated assets (dual-style):
- Kai run animations (East/West, 16 frames)
- Kai portrait (neutral, 2 styles)
- Zombie walk/attack cycles (6 frames)
- Buildings: shack, campfire, well, chest (8 assets)
- Terrain: stone path, grass variation (4 tiles)
- Environment: oak tree, rock, bush, storage (10 objects)

Total: 42 new PNG files (21 base × 2 styles)
+ Batch generation scripts and manifests
+ Demo readiness checklist
This commit is contained in:
2025-12-31 00:53:19 +01:00
parent d0043ec390
commit 4f0a430c42
55 changed files with 1220 additions and 1 deletions

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""
Batch Asset Generation for NovaFarma Demo
Generates all missing assets with dual-style system
"""
import json
from pathlib import Path
# Asset definitions with proper organization
ASSET_QUEUE = {
"kai_animations": {
"category": "demo/characters",
"assets": [
# Run East (4 frames)
{"name": "kai_run_east_1", "prompt": "Kai running EAST, frame 1/4, left leg forward, right arm back"},
{"name": "kai_run_east_2", "prompt": "Kai running EAST, frame 2/4, mid-stride, both feet off ground"},
{"name": "kai_run_east_3", "prompt": "Kai running EAST, frame 3/4, right leg forward, left arm back"},
{"name": "kai_run_east_4", "prompt": "Kai running EAST, frame 4/4, landing pose"},
# Run West (4 frames)
{"name": "kai_run_west_1", "prompt": "Kai running WEST, frame 1/4, left leg forward"},
{"name": "kai_run_west_2", "prompt": "Kai running WEST, frame 2/4, mid-stride"},
{"name": "kai_run_west_3", "prompt": "Kai running WEST, frame 3/4, right leg forward"},
{"name": "kai_run_west_4", "prompt": "Kai running WEST, frame 4/4, landing"},
# Weapon actions
{"name": "kai_sword_swing_east", "prompt": "Kai swinging sword to the east, mid-swing action"},
{"name": "kai_sword_swing_west", "prompt": "Kai swinging sword to the west, mid-swing action"},
{"name": "kai_axe_chop", "prompt": "Kai chopping with axe overhead"},
# Portraits
{"name": "kai_portrait_neutral", "prompt": "Kai portrait, neutral serious expression, close-up face"},
{"name": "kai_portrait_happy", "prompt": "Kai portrait, slight smile, determined happy"},
{"name": "kai_portrait_sad", "prompt": "Kai portrait, sad expression, concerned"},
]
},
"zombies": {
"category": "demo/characters",
"assets": [
# Zombie walk
{"name": "zombie_walk_1", "prompt": "Zombie worker shambling, walk frame 1/4, left leg forward"},
{"name": "zombie_walk_2", "prompt": "Zombie worker shambling, walk frame 2/4, dragging feet"},
{"name": "zombie_walk_3", "prompt": "Zombie worker shambling, walk frame 3/4, right leg forward"},
{"name": "zombie_walk_4", "prompt": "Zombie worker shambling, walk frame 4/4, stumbling"},
# Zombie attack
{"name": "zombie_attack_1", "prompt": "Zombie worker attacking, frame 1/4, arms reaching forward"},
{"name": "zombie_attack_2", "prompt": "Zombie worker attacking, frame 2/4, lunging motion"},
{"name": "zombie_attack_3", "prompt": "Zombie worker attacking, frame 3/4, biting motion"},
{"name": "zombie_attack_4", "prompt": "Zombie worker attacking, frame 4/4, return stance"},
# Variants
{"name": "zombie_runner", "prompt": "Fast zombie runner, athletic pose, sprinting stance"},
{"name": "zombie_bloated", "prompt": "Bloated zombie, swollen belly, slow heavy build"},
{"name": "zombie_corpse", "prompt": "Zombie corpse on ground, defeated, lying down"},
]
},
"terrain": {
"category": "demo/terrain",
"assets": [
# Grass variations
{"name": "grass_tile_2", "prompt": "Grass tile variation 2, slightly different grass pattern"},
{"name": "grass_tile_3", "prompt": "Grass tile variation 3, with small flowers"},
{"name": "grass_tile_4", "prompt": "Grass tile variation 4, with rocks"},
# Stone path
{"name": "stone_path_straight", "prompt": "Stone path tile, straight section, cobblestones"},
{"name": "stone_path_corner", "prompt": "Stone path corner tile, 90 degree turn"},
{"name": "stone_path_cross", "prompt": "Stone path crossroads, 4-way intersection"},
{"name": "stone_path_end", "prompt": "Stone path end cap, rounded edge"},
# Corners
{"name": "grass_corner_ne", "prompt": "Grass to dirt corner, northeast transition"},
{"name": "grass_corner_nw", "prompt": "Grass to dirt corner, northwest transition"},
{"name": "grass_corner_se", "prompt": "Grass to dirt corner, southeast transition"},
{"name": "grass_corner_sw", "prompt": "Grass to dirt corner, southwest transition"},
]
},
"environment": {
"category": "demo/environment",
"assets": [
# Trees
{"name": "oak_tree", "prompt": "Large oak tree, full canopy, healthy green leaves"},
{"name": "oak_tree_stump", "prompt": "Cut oak tree stump, chopped down, rings visible"},
{"name": "pine_tree", "prompt": "Pine tree, conical shape, dark green needles"},
{"name": "dead_tree_2", "prompt": "Dead tree variant 2, bare twisted branches"},
# Rocks
{"name": "rock_small", "prompt": "Small rock, collectible stone, grey granite"},
{"name": "rock_medium", "prompt": "Medium rock, obstacle size, moss covered"},
{"name": "rock_large_2", "prompt": "Large boulder, imposing size, cracked surface"},
# Plants
{"name": "bush_green_2", "prompt": "Green bush, leafy shrub, decorative foliage"},
{"name": "bush_berries", "prompt": "Berry bush, red berries visible, harvestable"},
{"name": "flower_yellow", "prompt": "Yellow flowers, small cluster, wildflowers"},
{"name": "flower_purple", "prompt": "Purple flowers, delicate petals, garden flowers"},
{"name": "grass_tall", "prompt": "Tall grass patch, wild overgrown grass swaying"},
{"name": "weeds", "prompt": "Weed patch, unwanted plants, brown dried weeds"},
]
},
"buildings": {
"category": "demo/buildings",
"assets": [
{"name": "shack", "prompt": "Wooden shack, upgraded tent, small cabin with door"},
{"name": "campfire_lit", "prompt": "Campfire burning, orange flames, cooking fire active"},
{"name": "water_well", "prompt": "Stone water well, bucket and rope, medieval well"},
{"name": "storage_chest_large", "prompt": "Large storage chest, wooden trunk, iron reinforced"},
{"name": "scarecrow", "prompt": "Farm scarecrow, straw figure, tattered clothes, pole"},
{"name": "compost_bin", "prompt": "Wooden compost bin, organic waste, farm structure"},
]
},
"npcs": {
"category": "demo/npcs",
"assets": [
# Trader
{"name": "npc_trader_idle", "prompt": "Merchant trader NPC, standing idle, holding goods bag"},
{"name": "npc_trader_portrait", "prompt": "Trader portrait, friendly smile, merchant hat"},
# Blacksmith
{"name": "npc_blacksmith_idle", "prompt": "Blacksmith NPC, muscular build, leather apron, hammer"},
{"name": "npc_blacksmith_portrait", "prompt": "Blacksmith portrait, soot on face, serious expression"},
# Healer
{"name": "npc_healer_idle", "prompt": "Healer NPC, robed figure, staff with crystal"},
{"name": "npc_healer_portrait", "prompt": "Healer portrait, kind eyes, hood covering hair"},
# Traveler
{"name": "npc_traveler_idle", "prompt": "Mysterious traveler NPC, cloaked, walking staff"},
{"name": "npc_traveler_portrait", "prompt": "Traveler portrait, hood obscuring face, mysterious"},
]
}
}
# Style templates
STYLE_A_SUFFIX = "Style A: Bold cartoon vector art - thick black outlines, flat vibrant colors, clean cel-shaded style, cheerful indie game aesthetic, white background."
STYLE_B_SUFFIX = "Style B: Dark gritty noir art - bold black ink outlines, high-contrast desaturated tones, heavy shadows, sketchy crosshatch textures, moody post-apocalyptic atmosphere, black background."
# Character base descriptions
KAI_BASE = """Kai teenage survivor character, dark forest green thick dreadlocks (#2D5016),
medium skin tone, large ear gauges, nose piercing and lip piercing,
serious determined expression,
wearing weathered blue-grey denim jacket with dirt stains,
beige t-shirt underneath,
torn blue jeans ripped at knees,
brown leather combat boots,
brown survival backpack with straps and pockets,
athletic lean build."""
ZOMBIE_BASE = """Zombie worker character, decaying grey-green skin, tattered brown work clothes,
shambling posture, blank white eyes, exposed bones visible,
post-apocalyptic undead laborer."""
def generate_manifest():
"""Generate complete manifest for batch generation"""
manifest = {
"total_assets": 0,
"batches": []
}
for batch_name, batch_data in ASSET_QUEUE.items():
batch = {
"name": batch_name,
"category": batch_data["category"],
"assets": []
}
for asset in batch_data["assets"]:
# Determine character base
char_base = ""
if "kai" in asset["name"]:
char_base = KAI_BASE
elif "zombie" in asset["name"]:
char_base = ZOMBIE_BASE
# Create full prompts for both styles
base_prompt = f"{char_base}\n{asset['prompt']}" if char_base else asset["prompt"]
asset_entry = {
"name": asset["name"],
"styleA_prompt": f"{base_prompt}\n{STYLE_A_SUFFIX}",
"styleB_prompt": f"{base_prompt}\n{STYLE_B_SUFFIX}",
"target_dir": f"assets/images/{batch_data['category']}"
}
batch["assets"].append(asset_entry)
manifest["total_assets"] += 2 # Both styles
manifest["batches"].append(batch)
return manifest
def main():
manifest = generate_manifest()
# Save manifest
output_file = Path("BATCH_GENERATION_MANIFEST.json")
with open(output_file, 'w') as f:
json.dump(manifest, f, indent=2)
print(f"✅ Generated manifest: {output_file}")
print(f"📊 Total assets to generate: {manifest['total_assets']}")
print(f"📦 Total batches: {len(manifest['batches'])}")
for batch in manifest["batches"]:
print(f"{batch['name']}: {len(batch['assets'])} base assets (×2 styles = {len(batch['assets'])*2})")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,252 @@
#!/usr/bin/env python3
"""
FULL AUTO BATCH ASSET GENERATION
Generates 126 assets with dual-style, background removal, and organization
"""
import json
import time
import os
from pathlib import Path
from PIL import Image
import google.generativeai as genai
# Configure Gemini
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
def create_preview(image_path: Path, size=256):
"""Create preview version of image"""
try:
img = Image.open(image_path)
preview = img.resize((size, size), Image.Resampling.LANCZOS)
preview_path = image_path.parent / f"{image_path.stem}_preview_{size}x{size}.png"
preview.save(preview_path, 'PNG', optimize=True)
return preview_path
except Exception as e:
print(f" ⚠️ Preview creation failed: {e}")
return None
def generate_and_save(asset_name, prompt, style, target_dir, log_file):
"""Generate single image and save to proper location"""
start_time = time.time()
try:
# Generate image filename
filename = f"{asset_name}_{style}.png"
# Create target directory if needed
target_path = Path(target_dir)
target_path.mkdir(parents=True, exist_ok=True)
# Generate image
print(f" 🎨 Generating: {filename}")
log_file.write(f"{time.strftime('%H:%M:%S')} - Generating {filename}\n")
log_file.flush()
model = genai.GenerativeModel('gemini-2.0-flash-exp')
response = model.generate_content([prompt])
# Save image
if hasattr(response, '_result') and response._result.candidates:
image_data = response._result.candidates[0].content.parts[0].inline_data.data
output_path = target_path / filename
with open(output_path, 'wb') as f:
f.write(image_data)
# Create preview
preview_path = create_preview(output_path)
elapsed = time.time() - start_time
print(f" ✅ Saved: {filename} ({elapsed:.1f}s)")
log_file.write(f"{time.strftime('%H:%M:%S')} - SUCCESS {filename} ({elapsed:.1f}s)\n")
log_file.flush()
return {
'success': True,
'file': str(output_path),
'preview': str(preview_path) if preview_path else None,
'time': elapsed
}
else:
print(f" ❌ Generation failed: No image data")
log_file.write(f"{time.strftime('%H:%M:%S')} - FAILED {filename} - No image data\n")
log_file.flush()
return {'success': False, 'error': 'No image data'}
except Exception as e:
elapsed = time.time() - start_time
print(f" ❌ Error: {e}")
log_file.write(f"{time.strftime('%H:%M:%S')} - ERROR {filename} - {e}\n")
log_file.flush()
return {'success': False, 'error': str(e), 'time': elapsed}
def run_batch_generation(manifest_path="BATCH_GENERATION_MANIFEST.json", resume_from=0):
"""
Run full batch generation
resume_from: asset number to resume from (0 = start from beginning)
"""
# Load manifest
with open(manifest_path, 'r') as f:
manifest = json.load(f)
total_assets = manifest['total_assets']
print("=" * 70)
print("🚀 FULL AUTO BATCH ASSET GENERATION")
print("=" * 70)
print(f"\n📊 Total assets: {total_assets}")
print(f"📦 Batches: {len(manifest['batches'])}")
print(f"⏱️ Estimated time: {total_assets * 15 / 60:.0f} minutes")
print(f"🔄 Resume from: Asset #{resume_from}")
print("\n" + "=" * 70)
# Create logs directory
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
# Open log file
log_filename = log_dir / f"batch_generation_{time.strftime('%Y%m%d_%H%M%S')}.log"
log_file = open(log_filename, 'w')
log_file.write(f"BATCH GENERATION LOG - {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
log_file.write(f"Total assets: {total_assets}\n")
log_file.write(f"Resume from: {resume_from}\n")
log_file.write("=" * 70 + "\n\n")
log_file.flush()
# Statistics
stats = {
'total': 0,
'success': 0,
'failed': 0,
'skipped': 0,
'total_time': 0
}
asset_counter = 0
try:
# Process each batch
for batch in manifest['batches']:
print(f"\n{'='*70}")
print(f"📦 BATCH: {batch['name']} ({batch['category']})")
print(f"{'='*70}")
log_file.write(f"\n{'='*70}\n")
log_file.write(f"BATCH: {batch['name']}\n")
log_file.write(f"{'='*70}\n\n")
log_file.flush()
# Process each asset in batch
for asset in batch['assets']:
# Generate styleA
asset_counter += 1
if asset_counter <= resume_from:
stats['skipped'] += 1
print(f"\n⏭️ [{asset_counter}/{total_assets}] Skipping: {asset['name']}_styleA")
continue
stats['total'] += 1
print(f"\n📸 [{asset_counter}/{total_assets}] {asset['name']}_styleA")
result = generate_and_save(
asset['name'],
asset['styleA_prompt'],
'styleA',
asset['target_dir'],
log_file
)
if result['success']:
stats['success'] += 1
stats['total_time'] += result.get('time', 0)
else:
stats['failed'] += 1
# Small delay to avoid rate limits
time.sleep(2)
# Generate styleB
asset_counter += 1
if asset_counter <= resume_from:
stats['skipped'] += 1
print(f"\n⏭️ [{asset_counter}/{total_assets}] Skipping: {asset['name']}_styleB")
continue
stats['total'] += 1
print(f"\n📸 [{asset_counter}/{total_assets}] {asset['name']}_styleB")
result = generate_and_save(
asset['name'],
asset['styleB_prompt'],
'styleB',
asset['target_dir'],
log_file
)
if result['success']:
stats['success'] += 1
stats['total_time'] += result.get('time', 0)
else:
stats['failed'] += 1
# Progress update
progress = (asset_counter / total_assets) * 100
avg_time = stats['total_time'] / stats['success'] if stats['success'] > 0 else 15
remaining = (total_assets - asset_counter) * avg_time / 60
print(f"\n📊 Progress: {progress:.1f}% | Success: {stats['success']} | Failed: {stats['failed']} | ETA: {remaining:.0f} min")
# Small delay
time.sleep(2)
# Final summary
print("\n" + "=" * 70)
print("✅ BATCH GENERATION COMPLETE!")
print("=" * 70)
print(f"\n📊 FINAL STATISTICS:")
print(f" Total processed: {stats['total']}")
print(f" ✅ Success: {stats['success']}")
print(f" ❌ Failed: {stats['failed']}")
print(f" ⏭️ Skipped: {stats['skipped']}")
print(f" ⏱️ Total time: {stats['total_time'] / 60:.1f} minutes")
print(f" 📈 Success rate: {stats['success']/stats['total']*100:.1f}%")
print(f"\n📝 Log file: {log_filename}")
log_file.write(f"\n{'='*70}\n")
log_file.write(f"GENERATION COMPLETE\n")
log_file.write(f"{'='*70}\n")
log_file.write(f"Total processed: {stats['total']}\n")
log_file.write(f"Success: {stats['success']}\n")
log_file.write(f"Failed: {stats['failed']}\n")
log_file.write(f"Success rate: {stats['success']/stats['total']*100:.1f}%\n")
except KeyboardInterrupt:
print(f"\n\n⚠️ INTERRUPTED at asset #{asset_counter}")
print(f"To resume: python3 scripts/batch_generation_runner.py --resume {asset_counter}")
log_file.write(f"\n\nINTERRUPTED at asset #{asset_counter}\n")
except Exception as e:
print(f"\n\n❌ CRITICAL ERROR: {e}")
log_file.write(f"\n\nCRITICAL ERROR: {e}\n")
finally:
log_file.close()
def main():
import argparse
parser = argparse.ArgumentParser(description='Batch asset generation')
parser.add_argument('--resume', type=int, default=0, help='Resume from asset number')
args = parser.parse_args()
run_batch_generation(resume_from=args.resume)
if __name__ == "__main__":
main()

View File

@@ -230,7 +230,7 @@ def main():
base_dir = Path('assets/images')
# Skip these directories
skip_dirs = {'demo', 'demo_originals_with_white_bg'}
skip_dirs = {'demo_originals_with_white_bg'}
total_organized = 0