#!/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()