- 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.
260 lines
8.6 KiB
Python
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()
|