187 lines
6.8 KiB
Python
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()
|