#!/usr/bin/env python3 """ OVERNIGHT AUTOMATION SCRIPT - Dino Valley & Beyond Waits for quota reset, then automatically generates remaining assets """ import os import time import json import requests from datetime import datetime, timedelta, timezone from pathlib import Path import base64 # ======================================== # CONFIGURATION # ======================================== PROJECT_ROOT = Path("/Users/davidkotnik/repos/novafarma") ASSET_ROOT = PROJECT_ROOT / "assets/slike" LOG_FILE = PROJECT_ROOT / "auto_generation_log.txt" # Vertex AI Configuration PROJECT_ID = "gen-lang-client-0428644398" LOCATION = "us-central1" MODEL = "imagen-3.0-generate-001" # Quota reset time (from error message) QUOTA_RESET_TIME = datetime.fromisoformat("2026-01-03T01:40:39+01:00") # Generation limits MAX_IMAGES_PER_HOUR = 60 # Conservative (actual is 60/min) BATCH_SIZE = 10 # Generate in batches for checkpointing DELAY_BETWEEN_IMAGES = 1.2 # Seconds (safe rate limiting) # Style definitions STYLE_33_ENTITIES = """exact same art style as reference. Very thick black outlines (4-5px), flat colors NO gradients, pastel colors with dark gothic accents, NO pupils (white eyes), cute-dark aesthetic, clean vector cartoon""" STYLE_30_VEGETATION = """exact Garden Story cozy art style. Soft rounded shapes, gentle pastel colors, NO thick outlines (thin 1-2px), watercolor-like soft shading, cute botanical illustration, clean cozy plant art""" # ======================================== # REMAINING DINO VALLEY ASSETS (18) # ======================================== DINO_VALLEY_REMAINING = { "creatures": [ { "name": "boss_thunder_raptor", "prompt": "Thunder Raptor boss in {STYLE}. Pastel purple/blue raptor with lightning patterns, large empty white eyes NO pupils, dark gothic electric accents. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "dino_nest_eggs_special", "prompt": "Special glowing dinosaur nest in {STYLE}. Pastel brown nest with magical glowing eggs, dark gothic mystical aura. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "zombie_prehistoric", "prompt": "Prehistoric zombie caveman in {STYLE}. Pastel green/gray decayed caveman, torn tribal clothes, large empty white eyes NO pupils, dark gothic rot. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "zombie_dino_raptor", "prompt": "Zombie raptor dinosaur in {STYLE}. Pastel gray/green undead raptor with exposed bones, large empty white eyes NO pupils, dark gothic decay. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "zombie_bone", "prompt": "Skeleton zombie in {STYLE}. White/cream animated skeleton warrior, large empty eye sockets, dark gothic cracks. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "zombie_tar", "prompt": "Tar zombie in {STYLE}. Black sticky tar monster humanoid, glowing pastel orange eyes, dark gothic dripping texture. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "npc_tribal_hunter", "prompt": "Tribal hunter NPC in {STYLE}. Pastel brown skinned caveman with bone spear, tribal tattoos, large empty white eyes NO pupils, dark gothic war paint, friendly stance. 3/4 angle. Chroma green background (#00FF00)." }, { "name": "npc_shaman", "prompt": "Tribal shaman NPC in {STYLE}. Elderly caveman with bone staff and feather headdress, pastel skin, large empty white eyes NO pupils, dark gothic mystical symbols, wise pose. 3/4 angle. Chroma green background (#00FF00)." }, ] } # ======================================== # UTILITY FUNCTIONS # ======================================== def log(message): """Log message to file and console""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_msg = f"[{timestamp}] {message}" print(log_msg) with open(LOG_FILE, 'a') as f: f.write(log_msg + "\n") def wait_for_quota_reset(): """Wait until quota resets""" # Make now timezone-aware to match QUOTA_RESET_TIME now = datetime.now(timezone.utc).astimezone() wait_seconds = (QUOTA_RESET_TIME - now).total_seconds() if wait_seconds > 0: log(f"⏰ Waiting for quota reset at {QUOTA_RESET_TIME.strftime('%H:%M')}") log(f" Sleeping for {int(wait_seconds/60)} minutes...") time.sleep(wait_seconds + 10) # Add 10 sec buffer log("✅ Quota should be reset now!") else: log("✅ Quota already reset!") def get_access_token(): """Get Google Cloud access token""" import subprocess result = subprocess.run( ['gcloud', 'auth', 'print-access-token'], capture_output=True, text=True ) return result.stdout.strip() def generate_image_vertex(prompt, output_path): """Generate image using Vertex AI Imagen 3.0""" access_token = get_access_token() url = f"https://{LOCATION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{LOCATION}/publishers/google/models/{MODEL}:predict" headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } payload = { "instances": [ { "prompt": prompt } ], "parameters": { "sampleCount": 1, "aspectRatio": "1:1", "safetyFilterLevel": "block_few", "personGeneration": "allow_all" } } try: response = requests.post(url, headers=headers, json=payload, timeout=60) response.raise_for_status() result = response.json() # Extract and save image if "predictions" in result and len(result["predictions"]) > 0: image_data = result["predictions"][0]["bytesBase64Encoded"] image_bytes = base64.b64decode(image_data) # Save to file output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, 'wb') as f: f.write(image_bytes) log(f" ✅ Saved: {output_path.name}") return True else: log(f" ❌ No image in response") return False except requests.exceptions.HTTPError as e: if e.response.status_code == 429: log(f" ⚠️ Quota exhausted again, waiting 5 minutes...") time.sleep(300) return False else: log(f" ❌ HTTP Error: {e}") return False except Exception as e: log(f" ❌ Error: {str(e)}") return False def git_commit_checkpoint(message): """Create git commit checkpoint""" import subprocess try: subprocess.run(['git', 'add', '-A'], cwd=PROJECT_ROOT, check=True) subprocess.run(['git', 'commit', '-m', message], cwd=PROJECT_ROOT, check=True) log(f" 📦 Git commit: {message}") except Exception as e: log(f" ⚠️ Git commit failed: {e}") # ======================================== # MAIN GENERATION LOGIC # ======================================== def generate_dino_valley_remaining(): """Generate remaining Dino Valley assets""" log("🦖 Starting Dino Valley completion...") total = len(DINO_VALLEY_REMAINING["creatures"]) completed = 0 for asset in DINO_VALLEY_REMAINING["creatures"]: name = asset["name"] prompt_template = asset["prompt"] # Use Style 33 for entities prompt = prompt_template.replace("{STYLE}", STYLE_33_ENTITIES) output_path = ASSET_ROOT / "biomes/dino_valley/creatures" / f"{name}.png" # Skip if already exists if output_path.exists(): log(f"⏭️ Skipping {name} (already exists)") completed += 1 continue log(f"🖼️ Generating {name} ({completed+1}/{total})") success = generate_image_vertex(prompt, output_path) if success: completed += 1 # Checkpoint every 5 images if completed % 5 == 0: git_commit_checkpoint(f"🦖 Dino Valley auto: {completed}/{total} creatures") # Rate limiting time.sleep(DELAY_BETWEEN_IMAGES) log(f"✅ Dino Valley complete! {completed}/{total} assets generated") git_commit_checkpoint(f"🎉 DINO VALLEY COMPLETE - All {completed} creatures done!") return completed # ======================================== # MAIN EXECUTION # ======================================== if __name__ == "__main__": log("="*60) log("🚀 OVERNIGHT AUTOMATION STARTED") log("="*60) # Wait for quota reset wait_for_quota_reset() # Generate remaining Dino Valley dino_count = generate_dino_valley_remaining() log("="*60) log(f"✅ AUTOMATION COMPLETE!") log(f"📊 Total generated: {dino_count} assets") log(f"💰 Estimated cost: €{dino_count * 0.40:.2f}") log("="*60)