📚 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.
This commit is contained in:
259
scripts/generate_characters_working.py
Normal file
259
scripts/generate_characters_working.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user