Files
hoelee-blog/scripts/generate_category_banners.py
T

187 lines
6.8 KiB
Python

#!/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()