Files
novafarma/scripts/generate_characters_working.py
David Kotnik a101d49f2e 📚 Documentation + scripts from today's exploration
- ASSET_COUNT_STATUS_01_01_2026.md - asset tracking
- CHARACTER_PRODUCTION_PLAN.md - character animation plan
- CHARACTER_GENERATION_FINAL_PLAN.md - API alternatives research
- COMFYUI_SETUP_TODAY.md - ComfyUI setup guide
- TASKS_01_01_2026.md - consolidated task list
- FULL_STORY_OVERVIEW.md - game narrative summary
- preview_animations.html - animation preview gallery
- Test scripts for API exploration (test_minimal.py, test_imagen.py)
- Character generation scripts (generate_all_characters_complete.py, generate_characters_working.py)

These were created during API troubleshooting and production planning.
2026-01-01 18:29:50 +01:00

260 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""
CHARACTER ANIMATION GENERATOR - WORKING VERSION
Uses direct REST API instead of deprecated SDK
Generates animations for Kai, Ana, Gronk
Date: 1.1.2026
"""
import os
import time
import json
import base64
from pathlib import Path
import requests
from PIL import Image
import io
# ==================== CONFIGURATION ====================
API_KEY = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
if not API_KEY:
print("❌ ERROR: API key not found!")
print("Please set: export GEMINI_API_KEY='your-key-here'")
exit(1)
print(f"✅ API Key found: {API_KEY[:10]}...")
OUTPUT_DIR = Path("assets/slike")
DELAY_BETWEEN_CALLS = 15 # seconds
API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent"
# ==================== STYLE CONSTANTS ====================
STYLE_BASE = """Dark Hand-Drawn 2D Stylized Indie Game Art.
CRITICAL: Thick bold black outlines (3-4px), exaggerated proportions, warped perspective.
High-contrast noir elements. Centered subject with 10px transparent margin.
32-bit PNG with alpha channel. Clean background (pure transparency)."""
KAI_BASE = """Character: KAI MARKOVIĆ (Age 17, Male Protagonist)
- Green & Pink dreadlocks (distinctive!)
- Tactical survivor outfit (torn, weathered)
- Lean athletic build, determined expression"""
ANA_BASE = """Character: ANA MARKOVIĆ (Age 17, Female Twin)
- Pink dreadlocks (all pink, matches Kai's style)
- Explorer vest, cargo pants
- Slender athletic build, intelligent caring expression"""
GRONK_BASE = """Character: GRONK (Zen Troll Companion)
- Large green troll, 7ft tall
- Pink mohawk hair, multiple piercings
- Vape pen accessory (signature!)
- Baggy pants, no shirt, chill relaxed expression"""
# ==================== SIMPLIFIED ANIMATION SET ====================
# Start with essentials only to test the system
KAI_ANIMATIONS = {
"walk_south": {"frames": 4, "desc": "walking toward camera, front view"},
"walk_east": {"frames": 4, "desc": "walking right, side view"},
"idle_front": {"frames": 4, "desc": "idle standing, front view, breathing"},
"attack_south": {"frames": 4, "desc": "sword swing downward"},
}
ANA_ANIMATIONS = {
"walk_south": {"frames": 4, "desc": "walking toward camera, front view"},
"walk_east": {"frames": 4, "desc": "walking right, side view"},
"idle_front": {"frames": 4, "desc": "idle standing, front view"},
"research_front": {"frames": 4, "desc": "writing in notebook"},
}
GRONK_ANIMATIONS = {
"walk_south": {"frames": 4, "desc": "heavy lumbering walk, front view"},
"walk_east": {"frames": 4, "desc": "heavy walk right, side view"},
"idle_front": {"frames": 4, "desc": "idle, chill stance, front view"},
"vape_front": {"frames": 6, "desc": "vaping, exhaling smoke cloud"},
}
# ==================== GENERATION FUNCTIONS ====================
def generate_image_rest_api(prompt):
"""Generate image using REST API"""
payload = {
"contents": [{
"parts": [{
"text": prompt
}]
}],
"generationConfig": {
"temperature": 0.4,
"topK": 32,
"topP": 1,
"maxOutputTokens": 4096,
}
}
headers = {
"Content-Type": "application/json"
}
url = f"{API_URL}?key={API_KEY}"
try:
response = requests.post(url, headers=headers, json=payload, timeout=60)
response.raise_for_status()
data = response.json()
# Extract image from response
if 'candidates' in data and len(data['candidates']) > 0:
candidate = data['candidates'][0]
if 'content' in candidate and 'parts' in candidate['content']:
for part in candidate['content']['parts']:
if 'inlineData' in part:
image_b64 = part['inlineData']['data']
image_bytes = base64.b64decode(image_b64)
image = Image.open(io.BytesIO(image_bytes))
return image
print(f" ❌ No image in response")
return None
except requests.exceptions.RequestException as e:
print(f" ❌ API Error: {e}")
return None
except Exception as e:
print(f" ❌ Error: {e}")
return None
def generate_animation_frame(character_name, character_base, animation_name, frame_num, animation_desc):
"""Generate a single animation frame"""
prompt = f"""{STYLE_BASE}
{character_base}
ANIMATION: {animation_name} - Frame {frame_num}/4
Description: {animation_desc}
Frame timing:
- Frame 1: Start pose
- Frame 2: Mid-motion
- Frame 3: Peak/impact
- Frame 4: Follow-through
Generate this specific frame showing clear progression.
Maintain character consistency."""
print(f" 🎨 {character_name} - {animation_name} - Frame {frame_num}...")
image = generate_image_rest_api(prompt)
return image
def generate_character_animations(character_name, character_base, animations_dict):
"""Generate all animations for a character"""
char_dir = OUTPUT_DIR / character_name.lower()
char_dir.mkdir(parents=True, exist_ok=True)
total_frames = sum(anim["frames"] for anim in animations_dict.values())
current_frame = 0
print(f"\n{'='*60}")
print(f"🎬 GENERATING {character_name.upper()}")
print(f"{'='*60}")
print(f"Animations: {len(animations_dict)}")
print(f"Total frames: {total_frames}")
print(f"Output: {char_dir}\n")
for anim_name, anim_data in animations_dict.items():
frames_count = anim_data["frames"]
desc = anim_data["desc"]
print(f"\n📹 {anim_name} ({frames_count} frames)")
for frame_num in range(1, frames_count + 1):
current_frame += 1
image = generate_animation_frame(
character_name,
character_base,
anim_name,
frame_num,
desc
)
if image:
# Save original
filename = f"{character_name.lower()}_{anim_name}_frame{frame_num}_1024x1024.png"
filepath = char_dir / filename
image.save(filepath, "PNG")
print(f" ✅ Saved: {filename}")
# Create preview
preview = image.resize((256, 256), Image.Resampling.LANCZOS)
preview_filename = f"{character_name.lower()}_{anim_name}_frame{frame_num}_preview_256x256.png"
preview_filepath = char_dir / preview_filename
preview.save(preview_filepath, "PNG")
print(f" ✅ Preview: {preview_filename}")
# Progress
progress = (current_frame / total_frames) * 100
print(f" 📊 Progress: {current_frame}/{total_frames} ({progress:.1f}%)")
# Rate limiting
if current_frame < total_frames:
print(f" ⏳ Waiting {DELAY_BETWEEN_CALLS}s...")
time.sleep(DELAY_BETWEEN_CALLS)
print(f"\n{character_name.upper()} COMPLETE!")
print(f"Generated: {total_frames} frames ({total_frames * 2} files)\n")
return total_frames
# ==================== MAIN ====================
def main():
print("=" * 80)
print("🎮 DOLINASMRTI - CHARACTER ANIMATION GENERATOR")
print("=" * 80)
print("Date: 2026-01- 01")
print("Mode: ESSENTIAL ANIMATIONS TEST")
print(f"API Delay: {DELAY_BETWEEN_CALLS}s\n")
start_time = time.time()
total_generated = 0
# Generate essentials for each character
kai_frames = generate_character_animations("Kai", KAI_BASE, KAI_ANIMATIONS)
total_generated += kai_frames
ana_frames = generate_character_animations("Ana", ANA_BASE, ANA_ANIMATIONS)
total_generated += ana_frames
gronk_frames = generate_character_animations("Gronk", GRONK_BASE, GRONK_ANIMATIONS)
total_generated += gronk_frames
# Summary
elapsed = time.time() - start_time
hours = int(elapsed // 3600)
minutes = int((elapsed % 3600) // 60)
print("\n" + "=" * 80)
print("🎆 GENERATION COMPLETE!")
print("=" * 80)
print(f"Total frames: {total_generated}")
print(f"Total files: {total_generated * 2}")
print(f"Time: {hours}h {minutes}min")
print(f"\nBreakdown:")
print(f" - Kai: {kai_frames} frames")
print(f" - Ana: {ana_frames} frames")
print(f" - Gronk: {gronk_frames} frames")
print("\n✅ Ready for game integration!")
print("\n💡 View in Chrome: open preview_animations.html")
if __name__ == "__main__":
main()