Files
novafarma/scripts/generate_character_animations.py

241 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
🎬 CHARACTER ANIMATION GENERATOR
Generates complete animation sets for Kai, Ana, Gronk
Based on CHARACTER_ANIMATION_MANIFEST.md
"""
import os
import sys
import time
from pathlib import Path
from PIL import Image
try:
import google.generativeai as genai
except ImportError:
print("❌ google-generativeai not installed!")
sys.exit(1)
# Configuration
REPO = Path("/Users/davidkotnik/repos/novafarma")
LIKI = REPO / "assets/slike/liki"
# API Setup
api_key = os.environ.get("GEMINI_API_KEY")
if not api_key:
print("❌ GEMINI_API_KEY not set!")
sys.exit(1)
genai.configure(api_key=api_key)
# Characters and their master references
CHARACTERS = {
"kai": {
"styleA_ref": "kai_master_styleA_reference.png",
"styleB_ref": "kai_master_styleB_reference.png",
"description": "young male adventurer with green hair and backpack"
},
"ana": {
"styleA_ref": "ana_master_styleA_reference.png",
"styleB_ref": "ana_master_styleB_reference.png",
"description": "young girl explorer with pink dreadlocks, green vest, cargo shorts"
},
"gronk": {
"styleA_ref": "gronk_master_styleA_reference.png",
"styleB_ref": "gronk_master_styleB_reference.png",
"description": "large friendly orc with green skin, pink dreadlocks, pink hoodie, vape pen"
}
}
# Animation definitions - PRIORITY 1: CORE MOVEMENT
ANIMATIONS_P1 = {
"walk": {
"frames": 4,
"directions": ["north", "south", "east", "west"],
"prompts": {
"north": "walking away from camera, back view, {frame} of 4 walk cycle",
"south": "walking toward camera, front view, {frame} of 4 walk cycle",
"east": "walking to the right, side view facing right, {frame} of 4 walk cycle",
"west": "walking to the left, side view facing left, {frame} of 4 walk cycle"
}
},
"idle": {
"frames": 1,
"directions": ["north", "south", "east", "west"],
"prompts": {
"north": "standing idle, back view",
"south": "standing idle, front view",
"east": "standing idle, side view facing right",
"west": "standing idle, side view facing left"
}
}
}
STYLE_PROMPTS = {
"stylea": "2D game character sprite, cartoon vector art style with bold black outlines, flat colors, cute playful aesthetic, isolated on pure white background",
"styleb": "2D game character sprite, dark hand-drawn gritty noir style with dramatic shadows, high contrast, sketchy atmospheric lines, isolated on pure white background"
}
def create_preview(image_path: Path, size=256):
"""Create preview version"""
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 failed: {e}")
return None
def generate_animation_frame(character, action, direction, frame, style, log_file):
"""Generate single animation frame"""
char_data = CHARACTERS[character]
style_suffix = style
# Build filename
if frame > 0:
filename = f"{character}_{action}_{direction}_{frame}_{style_suffix}.png"
else:
filename = f"{character}_{action}_{direction}_{style_suffix}.png"
output_path = LIKI / character / filename
# Skip if exists
if output_path.exists():
print(f" ⏭️ {filename} exists")
return True
# Build prompt
anim_data = ANIMATIONS_P1[action]
action_prompt = anim_data["prompts"][direction]
if "{frame}" in action_prompt:
action_prompt = action_prompt.format(frame=frame)
style_prompt = STYLE_PROMPTS[style_suffix]
full_prompt = f"""{style_prompt}
Character: {char_data['description']}
EXACTLY match the visual style and appearance from the master reference image.
Action: {action_prompt}
Full body visible, centered composition, game asset sprite.
Maintain complete visual consistency with reference - same colors, proportions, clothing details.
"""
start = time.time()
try:
print(f" 🎨 Generating: {filename}")
log_file.write(f"{time.strftime('%H:%M:%S')} - {filename}\n")
log_file.flush()
model = genai.GenerativeModel("gemini-2.5-flash")
response = model.generate_content([full_prompt])
if hasattr(response, '_result') and response._result.candidates:
image_data = response._result.candidates[0].content.parts[0].inline_data.data
# Save
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(image_data)
# Preview
create_preview(output_path)
elapsed = time.time() - start
print(f" ✅ Saved ({elapsed:.1f}s)")
log_file.write(f"{time.strftime('%H:%M:%S')} - SUCCESS ({elapsed:.1f}s)\n")
log_file.flush()
return True
else:
print(f" ❌ No image data")
log_file.write(f"{time.strftime('%H:%M:%S')} - FAILED - No data\n")
log_file.flush()
return False
except Exception as e:
print(f" ❌ Error: {e}")
log_file.write(f"{time.strftime('%H:%M:%S')} - ERROR: {e}\n")
log_file.flush()
return False
def main():
# Create log
log_dir = REPO / "logs"
log_dir.mkdir(exist_ok=True)
log_file = open(log_dir / f"character_animations_{time.strftime('%Y%m%d_%H%M%S')}.log", 'w')
print("="*70)
print("🎬 CHARACTER ANIMATION GENERATOR - PRIORITY 1")
print("="*70)
print("\n3 Characters × 20 frames × 2 styles = 120 PNG\n")
log_file.write(f"CHARACTER ANIMATION GENERATION - {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
log_file.write("="*70 + "\n\n")
log_file.flush()
stats = {'total': 0, 'success': 0, 'failed': 0}
try:
for character in CHARACTERS.keys():
print(f"\n{'='*70}")
print(f"👤 {character.upper()}")
print(f"{'='*70}")
for style in ["stylea", "styleb"]:
print(f"\n🎨 Style: {style}")
for action, anim_data in ANIMATIONS_P1.items():
print(f"\n 📹 {action.upper()}")
for direction in anim_data["directions"]:
frames = anim_data["frames"]
if frames == 1:
stats['total'] += 1
if generate_animation_frame(character, action, direction, 0, style, log_file):
stats['success'] += 1
else:
stats['failed'] += 1
time.sleep(15) # Rate limiting
else:
for frame in range(1, frames + 1):
stats['total'] += 1
if generate_animation_frame(character, action, direction, frame, style, log_file):
stats['success'] += 1
else:
stats['failed'] += 1
time.sleep(15) # Rate limiting
# Progress
progress = (stats['total'] / 120) * 100
print(f"\n📊 Overall Progress: {progress:.1f}% | ✅ {stats['success']} | ❌ {stats['failed']}")
# Final summary
print("\n" + "="*70)
print("🎉 GENERATION COMPLETE!")
print("="*70)
print(f"✅ Success: {stats['success']}/120")
print(f"❌ Failed: {stats['failed']}/120")
print(f"📊 Success rate: {(stats['success']/120)*100:.1f}%")
log_file.write(f"\n{'='*70}\n")
log_file.write(f"COMPLETE - Success: {stats['success']}, Failed: {stats['failed']}\n")
except KeyboardInterrupt:
print(f"\n\n⚠️ INTERRUPTED at {stats['total']}/120")
log_file.write(f"\n\nINTERRUPTED at {stats['total']}/120\n")
finally:
log_file.close()
if __name__ == "__main__":
main()