📚 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:
393
scripts/generate_all_characters_complete.py
Normal file
393
scripts/generate_all_characters_complete.py
Normal file
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
COMPLETE CHARACTER ANIMATION GENERATOR
|
||||
Generates ALL animations for Kai, Ana, and Gronk (435 PNG total)
|
||||
Date: 1.1.2026
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
import google.generativeai as genai
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
# ==================== CONFIGURATION ====================
|
||||
# Try both environment variable names
|
||||
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 one of:")
|
||||
print(" export GEMINI_API_KEY='your-key-here'")
|
||||
print(" export GOOGLE_API_KEY='your-key-here'")
|
||||
raise ValueError("API key not set!")
|
||||
|
||||
# NOTE: Using deprecated google.generativeai for compatibility
|
||||
# TODO: Migrate to google.genai in future
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', category=FutureWarning)
|
||||
|
||||
genai.configure(api_key=API_KEY)
|
||||
print(f"✅ API Key configured (first 10 chars): {API_KEY[:10]}...")
|
||||
|
||||
OUTPUT_DIR = Path("assets/slike")
|
||||
DELAY_BETWEEN_CALLS = 15 # seconds (safe rate limiting)
|
||||
|
||||
# Art Style Constants
|
||||
STYLE_BASE = """
|
||||
Dark Hand-Drawn 2D Stylized Indie Game Art.
|
||||
CRITICAL REQUIREMENTS:
|
||||
- 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)
|
||||
"""
|
||||
|
||||
# ==================== CHARACTER DEFINITIONS ====================
|
||||
|
||||
KAI_BASE = """
|
||||
Character: KAI MARKOVIĆ (Age 17, Male Protagonist)
|
||||
- Green & Pink dreadlocks (distinctive!)
|
||||
- Tactical survivor outfit (torn, weathered)
|
||||
- Lean athletic build
|
||||
- Determined expression
|
||||
- REFERENCE: Check konsistentno/kai_master_styleA_reference.png
|
||||
"""
|
||||
|
||||
ANA_BASE = """
|
||||
Character: ANA MARKOVIĆ (Age 17, Female Twin)
|
||||
- Pink dreadlocks (matches Kai's style but all pink)
|
||||
- Explorer vest, cargo pants
|
||||
- Slender athletic build
|
||||
- Intelligent, caring expression
|
||||
- REFERENCE: Check konsistentno/ana_master_styleA_reference.png
|
||||
"""
|
||||
|
||||
GRONK_BASE = """
|
||||
Character: GRONK (Zen Troll Companion)
|
||||
- Large green troll, 7ft tall
|
||||
- Pink mohawk hair
|
||||
- Multiple piercings (ears, nose)
|
||||
- Vape pen accessory (signature item!)
|
||||
- Baggy pants, no shirt
|
||||
- Chill, relaxed expression
|
||||
- REFERENCE: Check konsistentno/gronk_master_styleA_reference.png
|
||||
"""
|
||||
|
||||
# ==================== ANIMATION DEFINITIONS ====================
|
||||
|
||||
KAI_ANIMATIONS = {
|
||||
# PHASE 1: Core Gameplay (72 PNG)
|
||||
"walk_north": {"frames": 4, "desc": "walking away from camera, back view"},
|
||||
"walk_northeast": {"frames": 4, "desc": "walking diagonal up-right"},
|
||||
"walk_east": {"frames": 4, "desc": "walking right, side view (right profile)"},
|
||||
"walk_southeast": {"frames": 4, "desc": "walking diagonal down-right"},
|
||||
"walk_south": {"frames": 4, "desc": "walking toward camera, front view"},
|
||||
"walk_southwest": {"frames": 4, "desc": "walking diagonal down-left"},
|
||||
"walk_west": {"frames": 4, "desc": "walking left, side view (left profile)"},
|
||||
"walk_northwest": {"frames": 4, "desc": "walking diagonal up-left"},
|
||||
|
||||
"idle_front": {"frames": 4, "desc": "idle standing, front view, breathing animation"},
|
||||
"idle_side": {"frames": 4, "desc": "idle standing, side profile, breathing animation"},
|
||||
|
||||
"attack_north": {"frames": 4, "desc": "sword swing upward (attacking north)"},
|
||||
"attack_east": {"frames": 4, "desc": "sword swing right (attacking east)"},
|
||||
"attack_south": {"frames": 4, "desc": "sword swing downward (attacking south)"},
|
||||
"attack_west": {"frames": 4, "desc": "sword swing left (attacking west)"},
|
||||
|
||||
"dig_north": {"frames": 4, "desc": "digging with shovel upward"},
|
||||
"dig_east": {"frames": 4, "desc": "digging with shovel to the right"},
|
||||
"dig_south": {"frames": 4, "desc": "digging with shovel downward"},
|
||||
"dig_west": {"frames": 4, "desc": "digging with shovel to the left"},
|
||||
|
||||
# PHASE 2: Expanded (47 PNG)
|
||||
"run_north": {"frames": 4, "desc": "running away, back view, faster pace"},
|
||||
"run_northeast": {"frames": 4, "desc": "running diagonal up-right"},
|
||||
"run_east": {"frames": 4, "desc": "running right, side view"},
|
||||
"run_southeast": {"frames": 4, "desc": "running diagonal down-right"},
|
||||
"run_south": {"frames": 4, "desc": "running toward camera, front view"},
|
||||
"run_southwest": {"frames": 4, "desc": "running diagonal down-left"},
|
||||
"run_west": {"frames": 4, "desc": "running left, side view"},
|
||||
"run_northwest": {"frames": 4, "desc": "running diagonal up-left"},
|
||||
|
||||
"plant_front": {"frames": 3, "desc": "planting seeds, kneeling, front view"},
|
||||
"plant_side": {"frames": 3, "desc": "planting seeds, kneeling, side view"},
|
||||
|
||||
"harvest_front": {"frames": 3, "desc": "harvesting crops, bending down, front view"},
|
||||
"harvest_side": {"frames": 3, "desc": "harvesting crops, bending down, side view"},
|
||||
|
||||
"hurt": {"frames": 3, "desc": "taking damage, recoiling, pain expression"},
|
||||
|
||||
# PHASE 3: Polish (27 PNG)
|
||||
"sad": {"frames": 4, "desc": "crying, hands on face, emotional"},
|
||||
"happy": {"frames": 4, "desc": "celebrating, arms raised, joyful"},
|
||||
"thinking": {"frames": 3, "desc": "hand on chin, pondering"},
|
||||
"shocked": {"frames": 2, "desc": "surprised, eyes wide, mouth open"},
|
||||
|
||||
"use_item_front": {"frames": 3, "desc": "using item from inventory, front view"},
|
||||
"use_item_side": {"frames": 3, "desc": "using item from inventory, side view"},
|
||||
|
||||
"eat": {"frames": 3, "desc": "eating food, bringing to mouth"},
|
||||
|
||||
"death": {"frames": 5, "desc": "death animation, collapsing to ground"},
|
||||
|
||||
# PHASE 4: Advanced (10 PNG)
|
||||
"zombie_command": {"frames": 4, "desc": "telepathic gesture, hand raised, concentration"},
|
||||
"telepathy_effect": {"frames": 6, "desc": "psychic energy emanating, glowing effect"},
|
||||
}
|
||||
|
||||
ANA_ANIMATIONS = {
|
||||
# PHASE 1: Core (56 PNG)
|
||||
"walk_north": {"frames": 4, "desc": "walking away, back view"},
|
||||
"walk_northeast": {"frames": 4, "desc": "walking diagonal up-right"},
|
||||
"walk_east": {"frames": 4, "desc": "walking right, side view"},
|
||||
"walk_southeast": {"frames": 4, "desc": "walking diagonal down-right"},
|
||||
"walk_south": {"frames": 4, "desc": "walking toward camera, front view"},
|
||||
"walk_southwest": {"frames": 4, "desc": "walking diagonal down-left"},
|
||||
"walk_west": {"frames": 4, "desc": "walking left, side view"},
|
||||
"walk_northwest": {"frames": 4, "desc": "walking diagonal up-left"},
|
||||
|
||||
"idle_front": {"frames": 4, "desc": "idle standing, front view"},
|
||||
"idle_side": {"frames": 4, "desc": "idle standing, side profile"},
|
||||
|
||||
"research_front": {"frames": 4, "desc": "writing in notebook, studying"},
|
||||
"research_side": {"frames": 4, "desc": "writing in notebook, side view"},
|
||||
|
||||
"heal_front": {"frames": 4, "desc": "applying bandage, medical care"},
|
||||
"heal_side": {"frames": 4, "desc": "applying bandage, side view"},
|
||||
|
||||
# PHASE 2: Expanded (46 PNG)
|
||||
"run_north": {"frames": 4, "desc": "running away, back view"},
|
||||
"run_northeast": {"frames": 4, "desc": "running diagonal up-right"},
|
||||
"run_east": {"frames": 4, "desc": "running right"},
|
||||
"run_southeast": {"frames": 4, "desc": "running diagonal down-right"},
|
||||
"run_south": {"frames": 4, "desc": "running toward camera"},
|
||||
"run_southwest": {"frames": 4, "desc": "running diagonal down-left"},
|
||||
"run_west": {"frames": 4, "desc": "running left"},
|
||||
"run_northwest": {"frames": 4, "desc": "running diagonal up-left"},
|
||||
|
||||
"examine_front": {"frames": 3, "desc": "examining with magnifying glass"},
|
||||
"examine_side": {"frames": 3, "desc": "examining, side view"},
|
||||
|
||||
"mix_potion": {"frames": 4, "desc": "mixing ingredients in flask"},
|
||||
|
||||
"worried": {"frames": 4, "desc": "worried expression, hand to head"},
|
||||
|
||||
# PHASE 3: Polish (18 PNG)
|
||||
"relief": {"frames": 4, "desc": "relief, exhaling, relaxed"},
|
||||
"twin_bond_glow": {"frames": 6, "desc": "glowing psychic connection effect"},
|
||||
"flashback_pose": {"frames": 3, "desc": "memory pose, ethereal"},
|
||||
"death": {"frames": 5, "desc": "death animation, collapsing"},
|
||||
|
||||
# PHASE 4: Advanced (18 PNG)
|
||||
"collect_sample_front": {"frames": 3, "desc": "collecting sample with vial"},
|
||||
"collect_sample_side": {"frames": 3, "desc": "collecting sample, side view"},
|
||||
|
||||
"defend": {"frames": 4, "desc": "defensive stance with staff"},
|
||||
"cure_cast": {"frames": 5, "desc": "casting cure spell, magical gesture"},
|
||||
"hurt": {"frames": 3, "desc": "taking damage, recoiling"},
|
||||
}
|
||||
|
||||
GRONK_ANIMATIONS = {
|
||||
# PHASE 1: Core (72 PNG)
|
||||
"walk_north": {"frames": 4, "desc": "heavy lumbering walk, back view"},
|
||||
"walk_northeast": {"frames": 4, "desc": "heavy walk diagonal up-right"},
|
||||
"walk_east": {"frames": 4, "desc": "heavy walk right, side view"},
|
||||
"walk_southeast": {"frames": 4, "desc": "heavy walk diagonal down-right"},
|
||||
"walk_south": {"frames": 4, "desc": "heavy walk toward camera, front view"},
|
||||
"walk_southwest": {"frames": 4, "desc": "heavy walk diagonal down-left"},
|
||||
"walk_west": {"frames": 4, "desc": "heavy walk left, side view"},
|
||||
"walk_northwest": {"frames": 4, "desc": "heavy walk diagonal up-left"},
|
||||
|
||||
"idle_front": {"frames": 4, "desc": "idle, chill stance, front view"},
|
||||
"idle_side": {"frames": 4, "desc": "idle, chill stance, side view"},
|
||||
|
||||
"vape_front": {"frames": 6, "desc": "vaping, exhaling smoke cloud, front view"},
|
||||
"vape_side": {"frames": 6, "desc": "vaping, exhaling smoke, side view"},
|
||||
|
||||
"smash_north": {"frames": 5, "desc": "club smash upward"},
|
||||
"smash_east": {"frames": 5, "desc": "club smash right"},
|
||||
"smash_south": {"frames": 5, "desc": "club smash downward"},
|
||||
"smash_west": {"frames": 5, "desc": "club smash left"},
|
||||
|
||||
# PHASE 2: Expanded (47 PNG)
|
||||
"run_north": {"frames": 4, "desc": "slow lumbering run, back view"},
|
||||
"run_northeast": {"frames": 4, "desc": "lumbering run diagonal up-right"},
|
||||
"run_east": {"frames": 4, "desc": "lumbering run right"},
|
||||
"run_southeast": {"frames": 4, "desc": "lumbering run diagonal down-right"},
|
||||
"run_south": {"frames": 4, "desc": "lumbering run toward camera"},
|
||||
"run_southwest": {"frames": 4, "desc": "lumbering run diagonal down-left"},
|
||||
"run_west": {"frames": 4, "desc": "lumbering run left"},
|
||||
"run_northwest": {"frames": 4, "desc": "lumbering run diagonal up-left"},
|
||||
|
||||
"lift_heavy_front": {"frames": 4, "desc": "lifting heavy object, straining"},
|
||||
"lift_heavy_side": {"frames": 4, "desc": "lifting heavy object, side view"},
|
||||
|
||||
"laugh": {"frames": 4, "desc": "laughing, belly laugh, jovial"},
|
||||
"hurt": {"frames": 3, "desc": "taking damage, grimacing"},
|
||||
|
||||
# PHASE 3: Polish (16 PNG)
|
||||
"meditate": {"frames": 4, "desc": "zen meditation pose, sitting, serene"},
|
||||
"confused": {"frames": 3, "desc": "confused, scratching head"},
|
||||
"chill": {"frames": 3, "desc": "ultra relaxed, peace sign"},
|
||||
|
||||
"death": {"frames": 5, "desc": "death animation, collapsing heavily"},
|
||||
|
||||
# PHASE 4: Advanced (6 PNG)
|
||||
"block": {"frames": 3, "desc": "blocking with raised arms"},
|
||||
"taunt": {"frames": 4, "desc": "taunting enemies, beckoning"},
|
||||
}
|
||||
|
||||
# ==================== GENERATION FUNCTIONS ====================
|
||||
|
||||
def generate_animation_frame(character_name, character_base, animation_name, frame_num, animation_desc, style=STYLE_BASE):
|
||||
"""Generate a single animation frame"""
|
||||
|
||||
prompt = f"""
|
||||
{style}
|
||||
|
||||
{character_base}
|
||||
|
||||
ANIMATION: {animation_name} - Frame {frame_num}
|
||||
Description: {animation_desc}
|
||||
|
||||
Frame {frame_num} timing notes:
|
||||
- Frame 1: Start pose
|
||||
- Frame 2: Mid-motion
|
||||
- Frame 3: Peak/impact
|
||||
- Frame 4: Follow-through/return
|
||||
|
||||
Generate this specific frame showing clear progression in the animation cycle.
|
||||
Maintain character consistency (check reference in konsistentno/ folder).
|
||||
"""
|
||||
|
||||
try:
|
||||
print(f" 🎨 Generating {character_name} - {animation_name} - Frame {frame_num}...")
|
||||
|
||||
model = genai.GenerativeModel('gemini-2.0-flash-exp')
|
||||
response = model.generate_content([prompt])
|
||||
|
||||
if response.parts and len(response.parts) > 0:
|
||||
image_data = response.parts[0].inline_data.data
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
return image
|
||||
else:
|
||||
print(f" ❌ No image generated for {animation_name} frame {frame_num}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error generating {animation_name} frame {frame_num}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
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()} ANIMATIONS")
|
||||
print(f"{'='*60}")
|
||||
print(f"Total animations: {len(animations_dict)}")
|
||||
print(f"Total frames: {total_frames}")
|
||||
print(f"Output directory: {char_dir}")
|
||||
print()
|
||||
|
||||
for anim_name, anim_data in animations_dict.items():
|
||||
frames_count = anim_data["frames"]
|
||||
desc = anim_data["desc"]
|
||||
|
||||
print(f"\n📹 Animation: {anim_name} ({frames_count} frames)")
|
||||
|
||||
for frame_num in range(1, frames_count + 1):
|
||||
current_frame += 1
|
||||
|
||||
# Generate frame
|
||||
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 (256x256)
|
||||
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" ✅ Saved preview: {preview_filename}")
|
||||
|
||||
# Progress
|
||||
progress = (current_frame / total_frames) * 100
|
||||
print(f" 📊 Progress: {current_frame}/{total_frames} ({progress:.1f}%)")
|
||||
|
||||
# Rate limiting
|
||||
print(f" ⏳ Waiting {DELAY_BETWEEN_CALLS}s...")
|
||||
time.sleep(DELAY_BETWEEN_CALLS)
|
||||
|
||||
print(f"\n✅ {character_name.upper()} COMPLETE! Generated {total_frames} frames ({total_frames * 2} files with previews)")
|
||||
return total_frames
|
||||
|
||||
|
||||
# ==================== MAIN EXECUTION ====================
|
||||
|
||||
def main():
|
||||
"""Generate all character animations"""
|
||||
|
||||
print("=" * 80)
|
||||
print("🎮 DOLINASMRTI - COMPLETE CHARACTER ANIMATION GENERATOR")
|
||||
print("=" * 80)
|
||||
print(f"Date: 2026-01-01")
|
||||
print(f"Target: 435 PNG (all 3 characters, all animations)")
|
||||
print(f"API Delay: {DELAY_BETWEEN_CALLS}s between calls")
|
||||
print()
|
||||
|
||||
start_time = time.time()
|
||||
total_generated = 0
|
||||
|
||||
# Generate Kai
|
||||
kai_frames = generate_character_animations("Kai", KAI_BASE, KAI_ANIMATIONS)
|
||||
total_generated += kai_frames
|
||||
|
||||
# Generate Ana
|
||||
ana_frames = generate_character_animations("Ana", ANA_BASE, ANA_ANIMATIONS)
|
||||
total_generated += ana_frames
|
||||
|
||||
# Generate Gronk
|
||||
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 generated: {total_generated}")
|
||||
print(f"Total files created: {total_generated * 2} (with previews)")
|
||||
print(f"Time elapsed: {hours}h {minutes}min")
|
||||
print(f"\nCharacter breakdown:")
|
||||
print(f" - Kai: {kai_frames} frames")
|
||||
print(f" - Ana: {ana_frames} frames")
|
||||
print(f" - Gronk: {gronk_frames} frames")
|
||||
print("\n✅ All character animations ready for game integration!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user