#!/usr/bin/env python3 """Generate category banner images using Google Gemini via Chrome DevTools Protocol (CDP). Extracts generated images from canvas, converts to JPG & WebP, and saves to public/categories/. """ import base64 import io import json import os import re import sys import time import urllib.request from PIL import Image from playwright.sync_api import sync_playwright CHROME_WS_HOST = "192.168.1.1:9232" OUTPUT_DIR = "D:/dev/hoelee-blog/public/categories" PASTE_FILE = "C:/Users/hoelee/AppData/Local/hermes/composer-pastes/pasted_content_2026-10-25_17-59-35-878_5bec0b.txt" CATEGORIES = [ "ai", "case-studies", "devops", "engineering", "notes", "tutorials", "web3" ] def load_prompts(paste_file_path): with open(paste_file_path, "r", encoding="utf-8") as f: text = f.read() prompts = {} for cat in CATEGORIES: pattern = rf'### \d+ — [^\n]*?{re.escape(cat.replace("-", " "))}[\s\S]*?(Create (?:a|a premium|a sophisticated|a refined)[\s\S]*?16:9 aspect ratio\.)' m = re.search(pattern, text, re.I) if m: prompts[cat] = m.group(1).strip() else: raise ValueError(f"Could not find prompt for category: {cat}") return prompts def get_browser_ws(host): url = f"http://{host}/json/version" req = urllib.request.Request(url, headers={"User-Agent": "HermesCDP"}) with urllib.request.urlopen(req, timeout=5) as r: data = json.loads(r.read().decode()) return data["webSocketDebuggerUrl"] def generate_banner(browser, slug, prompt, force=False): os.makedirs(OUTPUT_DIR, exist_ok=True) jpg_path = os.path.join(OUTPUT_DIR, f"{slug}.jpg") webp_path = os.path.join(OUTPUT_DIR, f"{slug}.webp") if not force and os.path.exists(jpg_path) and os.path.getsize(jpg_path) > 10000: print(f"[{slug}] Banner already exists ({os.path.getsize(jpg_path):,} bytes). Skipping.") return True print(f"\n==========================================") print(f"[{slug}] Starting banner generation...") print(f"==========================================") context = browser.contexts[0] page = context.new_page() try: page.bring_to_front() page.goto("https://gemini.google.com/app", wait_until="domcontentloaded", timeout=60000) # Wait for input editor page.wait_for_selector('[role="textbox"]', state="visible", timeout=60000) time.sleep(2) box = page.locator('[role="textbox"]').first box.click() full_prompt = f"Generate an image: {prompt}" box.fill(full_prompt) time.sleep(1) send_btn = page.locator('button[aria-label="Send message"]') send_btn.click() print(f"[{slug}] Prompt submitted. Waiting for Imagen generation...") # Poll for image up to 90 seconds image_found = False for poll_idx in range(30): time.sleep(3) info = page.evaluate("""() => { const rs = document.querySelectorAll('model-response'); const last = rs[rs.length - 1]; if (!last) return null; const imgs = Array.from(last.querySelectorAll('img')).map(img => ({ src: img.src ? img.src.slice(0, 60) : '', w: img.naturalWidth, h: img.naturalHeight, complete: img.complete })); return { text: (last.innerText || '').slice(0, 80), imgs: imgs, stop: !!document.querySelector('button[aria-label*="Stop"]') }; }""") if info and info.get("imgs") and any(img["w"] > 100 for img in info["imgs"]): print(f"[{slug}] Image fully mounted after {(poll_idx + 1) * 3}s!") image_found = True break elif info and info.get("text"): status_snippet = info["text"].replace("\n", " ").strip() print(f"[{slug}] {(poll_idx + 1) * 3}s - status: {status_snippet}") if not image_found: print(f"[{slug}] ERROR: Timed out waiting for image to generate.", file=sys.stderr) return False # Extract canvas base64 data_url = page.evaluate("""() => { const img = Array.from(document.querySelectorAll('model-response img')).find(x => x.naturalWidth > 100); if (!img) return null; const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); return canvas.toDataURL('image/png'); }""") if not data_url or "," not in data_url: print(f"[{slug}] ERROR: Canvas extraction failed.", file=sys.stderr) return False b64_str = data_url.split(",", 1)[1] raw_bytes = base64.b64decode(b64_str) print(f"[{slug}] Raw PNG extracted: {len(raw_bytes):,} bytes") # Convert to JPG and WebP using PIL img = Image.open(io.BytesIO(raw_bytes)) width, height = img.size print(f"[{slug}] Image dimensions: {width}x{height}") # If RGBA, paste on dark background #101514 if img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info): bg = Image.new("RGB", img.size, (16, 21, 20)) # #101514 bg.paste(img, mask=img.split()[-1] if img.mode == "RGBA" else None) final_img = bg else: final_img = img.convert("RGB") final_img.save(jpg_path, "JPEG", quality=95, optimize=True) final_img.save(webp_path, "WEBP", quality=92) print(f"[{slug}] Successfully saved:") print(f" -> JPG: {jpg_path} ({os.path.getsize(jpg_path):,} bytes)") print(f" -> WEBP: {webp_path} ({os.path.getsize(webp_path):,} bytes)") return True except Exception as e: print(f"[{slug}] Exception during generation: {e}", file=sys.stderr) return False finally: page.close() def main(): prompts = load_prompts(PASTE_FILE) print(f"Loaded {len(prompts)} prompts from paste file.") ws_url = get_browser_ws(CHROME_WS_HOST) with sync_playwright() as p: browser = p.chromium.connect_over_cdp(ws_url) print("Connected to DSM Chrome over CDP.") results = {} for cat in CATEGORIES: ok = generate_banner(browser, cat, prompts[cat], force=False) results[cat] = ok time.sleep(3) # breathing room between requests print("\n================ SUMMARY ================") for cat, ok in results.items(): status = "OK" if ok else "FAILED" print(f" {cat:15s}: {status}") if __name__ == "__main__": main()