#!/usr/bin/env python3 """ Generate blog cover images using Google AI (Imagen API). Reads blog articles from ../docs/blog/blog-articles.json and generates cover images for articles that don't have images in ../docs/blog/assets/ """ import os import sys import json import base64 import requests from pathlib import Path def check_google_api_key(): """Check for Google API key in environment.""" api_key = os.getenv('GOOGLE_API_KEY') if not api_key: # Try loading from .env file env_file = Path('.env') if env_file.exists(): with open(env_file, 'r') as f: for line in f: if line.startswith('GOOGLE_API_KEY='): api_key = line.split('=', 1)[1].strip().strip('"').strip("'") break if not api_key: print("āŒ Error: GOOGLE_API_KEY not found!") print("\nPlease set the environment variable:") print("export GOOGLE_API_KEY=your-api-key-here") print("\nOr create a .env file with:") print("GOOGLE_API_KEY=your-api-key-here") sys.exit(1) return api_key def generate_blog_image(title, description, component_type, component_name, install_command, output_path, api_key): """ Generate a blog cover image using Google's Imagen API via AI Studio. Args: title: Article title description: Article description component_type: Type of component (Agent, MCP, Skill, etc.) component_name: Name of the component install_command: Installation command output_path: Path to save the generated image api_key: Google API key """ # Split install command into two lines for better readability if "--agent" in install_command: cmd_parts = install_command.split("--agent ") cmd_line1 = cmd_parts[0].strip() cmd_line2 = "--agent " + cmd_parts[1] elif "--mcp" in install_command: cmd_parts = install_command.split("--mcp ") cmd_line1 = cmd_parts[0].strip() cmd_line2 = "--mcp " + cmd_parts[1] elif "--skill" in install_command: cmd_parts = install_command.split("--skill ") cmd_line1 = cmd_parts[0].strip() cmd_line2 = "--skill " + cmd_parts[1] else: cmd_line1 = install_command cmd_line2 = "" # Simplify the command display to avoid text rendering issues # Show just the essential part if cmd_line2: simple_cmd = cmd_line2.strip() # Just show "--agent folder/name" etc else: simple_cmd = cmd_line1 # Create detailed prompt similar to supabase-claude-code-templates-cover.png prompt = f"""Create a professional blog cover image with this EXACT layout and text: LEFT SIDE (40% width): - Black background (#000000) - Text exactly as "CLAUDE CODE TEMPLATES" in pixelated/retro orange font (#F97316) - Font style: Bold, blocky, retro gaming aesthetic similar to arcade game fonts - Stacked vertically with equal spacing between words - Centered vertically on left side CENTER: - Vertical line divider in dark gray (#333333) 2px width - Full height of image from top to bottom RIGHT SIDE (60% width): - Black background (#000000) - At top: Small badge with text "{component_type}" in solid orange rectangle (#F97316) with rounded corners, black text inside - Below badge: Large bold white text saying "{component_name}" - At bottom: Small gray monospace text showing: "{simple_cmd}" CRITICAL TEXT RENDERING REQUIREMENTS: - All text must be perfectly readable and not corrupted - Use clear, legible fonts - Ensure proper spacing so text doesn't overlap or get cut off - The installation command at bottom should be clearly visible Overall specifications: - Exact dimensions: 1200x675 pixels (16:9 aspect ratio) - Background: Pure black (#000000) - Primary accent color: Orange (#F97316) - Text colors: White (#FFFFFF) for component name, Gray (#999999) for command - Minimalist, professional design - No decorative elements, gradients, or patterns - just text and divider - Clean, modern tech aesthetic similar to terminal/CLI interfaces The layout divides the image into two sections with a vertical line: left side shows "CLAUDE CODE TEMPLATES" in retro orange font, right side shows component type, name, and install command.""" print(f"šŸŽØ Generating image for: {title}") print(f"šŸ“ Prompt length: {len(prompt)} chars") # Google AI Nano Banana (gemini-2.5-flash-image) endpoint url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent" payload = { "contents": [ { "parts": [ { "text": prompt } ] } ] } headers = { "x-goog-api-key": api_key, "Content-Type": "application/json" } try: response = requests.post(url, headers=headers, json=payload) if response.status_code != 200: print(f"āŒ API Error ({response.status_code}): {response.text}") return False result = response.json() # Extract image from Nano Banana response if "candidates" in result and len(result["candidates"]) > 0: candidate = result["candidates"][0] if "content" in candidate and "parts" in candidate["content"]: parts = candidate["content"]["parts"] # Find the inline data part with the image for part in parts: if "inlineData" in part: inline_data = part["inlineData"] # Extract base64 data if "data" in inline_data: image_data = base64.b64decode(inline_data["data"]) # Save image with open(output_path, 'wb') as f: f.write(image_data) print(f"āœ… Image saved to: {output_path}") return True print(f"āš ļø No inline data found in response parts") return False else: print(f"āš ļø Unexpected response structure: {result}") return False else: print(f"āŒ No candidates in response: {result}") return False except Exception as e: print(f"āŒ Error generating image: {e}") return False def main(): """Main function to generate blog images.""" # Get Google API key api_key = check_google_api_key() # Load blog articles blog_json_path = Path(__file__).parent.parent / "docs" / "blog" / "blog-articles.json" if not blog_json_path.exists(): print(f"āŒ Error: Blog articles file not found: {blog_json_path}") sys.exit(1) with open(blog_json_path, 'r') as f: data = json.load(f) articles = data.get("articles", []) assets_dir = blog_json_path.parent / "assets" # Create assets directory if it doesn't exist assets_dir.mkdir(exist_ok=True) print(f"\nšŸ“š Found {len(articles)} articles") print(f"šŸ“ Assets directory: {assets_dir}\n") # Filter articles that need images generated (hosted on aitmpl.com/blog/assets/) articles_needing_images = [] for article in articles: image_url = article.get("image", "") if "aitmpl.com/blog/assets/" in image_url and "-cover.png" in image_url: # Extract filename from URL filename = image_url.split("/")[-1] output_path = assets_dir / filename if not output_path.exists(): articles_needing_images.append({ "article": article, "filename": filename, "output_path": output_path }) else: print(f"ā­ļø Skipping {filename} (already exists)") if not articles_needing_images: print("\nāœ… All blog images already exist!") return print(f"\nšŸŽØ Generating {len(articles_needing_images)} images...\n") # Generate images success_count = 0 for item in articles_needing_images: article = item["article"] filename = item["filename"] output_path = item["output_path"] # Extract component information from article ID article_id = article.get("id", "") # Determine component type and name based on article category category = article.get("category", "") if "agent" in article_id.lower() or category == "Agents": component_type = "AGENT" # Extract agent name from ID (e.g., frontend-developer-agent -> frontend-developer) agent_name = article_id.replace("-agent", "") component_name = agent_name.replace("-", " ").title() # Find actual agent path in components/agents/ import subprocess try: result = subprocess.run( ["find", "cli-tool/components/agents", "-name", f"{agent_name}.md", "-type", "f"], capture_output=True, text=True ) agent_path = result.stdout.strip() if agent_path: # Extract folder/name from path (e.g., development-team/frontend-developer) parts = agent_path.split("/agents/")[1].replace(".md", "") install_command = f"npx claude-code-templates@latest --agent {parts}" else: install_command = f"npx claude-code-templates@latest --agent {agent_name}" except: install_command = f"npx claude-code-templates@latest --agent {agent_name}" elif "mcp" in article_id.lower() or category == "MCP": component_type = "MCP" component_name = article_id.replace("-mcp", "").replace("-", " ").title() install_command = f"npx claude-code-templates@latest --mcp {article_id.replace('-mcp', '')}" elif "skill" in article_id.lower() or category == "Skills": component_type = "SKILL" component_name = article_id.replace("-skill", "").replace("-", " ").title() install_command = f"npx claude-code-templates@latest --skill {article_id}" elif "sandbox" in article_id.lower() or "e2b" in article_id.lower(): component_type = "SANDBOX" component_name = article_id.replace("-", " ").title() install_command = f"npx claude-code-templates@latest --sandbox e2b" else: component_type = "COMPONENT" component_name = article_id.replace("-", " ").title() install_command = f"npx claude-code-templates@latest" print(f"\n{'='*60}") print(f"šŸ“„ Article: {article['title']}") print(f"šŸ·ļø Type: {component_type}") print(f"šŸ“¦ Component: {component_name}") print(f"šŸ’» Command: {install_command}") print(f"šŸ’¾ Output: {filename}") print(f"{'='*60}\n") success = generate_blog_image( title=article["title"], description=article["description"], component_type=component_type, component_name=component_name, install_command=install_command, output_path=str(output_path), api_key=api_key ) if success: success_count += 1 print() # Extra newline for readability print(f"\n{'='*60}") print(f"āœ… Successfully generated {success_count}/{len(articles_needing_images)} images") print(f"{'='*60}\n") if __name__ == "__main__": main()