Create import base64import ioimport reimport timefrom dataclasses import dataclassfrom html import escapefrom urllib.parse import quoteimport gradio as grimport requestsfrom PIL import ImagePOLLINATIONS_URL = ( "https://image.pollinations.ai/prompt/{prompt}" "?width={width}&height={height}&model=flux&nologo=true&seed={seed}")STARTER_HTML = """<!DOCTYPE html><html><head> <meta charset="UTF-8" /> <title>Mini Forest Game</title> <style> body { margin: 0; background: #111; display: grid; place-items: center; min-height: 100vh; } canvas { border: 2px solid #222; background: #222; image-rendering: pixelated; } </style></head><body> <canvas id="gameCanvas" width="800" height="450"></canvas> <script> const canvas = document.getElementById("gameCanvas"); const ctx = canvas.getContext("2d"); const background = new Image(); background.src = "sprite_background.png"; const playerImg = new Image(); playerImg.src = "sprite_player.png"; const keys = new Set(); const player = { x: 380, y: 205, w: 48, h: 48, speed: 4 }; window.addEventListener("keydown", e => keys.add(e.key)); window.addEventListener("keyup", e => keys.delete(e.key)); function update() { if (keys.has("a") || keys.has("ArrowLeft")) player.x -= player.speed; if (keys.has("d") || keys.has("ArrowRight")) player.x += player.speed; if (keys.has("w") || keys.has("ArrowUp")) player.y -= player.speed; if (keys.has("s") || keys.has("ArrowDown")) player.y += player.speed; player.x = Math.max(0, Math.min(canvas.width - player.w, player.x)); player.y = Math.max(0, Math.min(canvas.height - player.h, player.y)); } function draw() { ctx.drawImage(background, 0, 0, canvas.width, canvas.height); ctx.drawImage(playerImg, player.x, player.y, player.w, player.h); ctx.fillStyle = "white"; ctx.font = "18px sans-serif"; ctx.fillText("Use WASD or arrow keys", 18, 28); } function loop() { update(); draw(); requestAnimationFrame(loop); } loop(); </script></body></html>"""DEFAULT_ROLES = """player: top-down pixel-art adventurer hero, transparent background, bright readable silhouettebackground: enchanted forest clearing game background, top-down view, soft moonlight, detailed but not too busy"""@dataclassclass AssetSpec: role: str prompt: str filename: str width: int height: intdef slugify(value: str) -> str: value = re.sub(r"[^a-zA-Z0-9]+", "_", value.strip().lower()).strip("_") return value or "asset"def parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]: specs: list[AssetSpec] = [] for line in raw_roles.splitlines(): line = line.strip() if not line or line.startswith("#"): continue if ":" in line: role, prompt = line.split(":", 1) elif "=" in line: role, prompt = line.split("=", 1) else: role, prompt = line, line role = role.strip() prompt = prompt.strip() or role slug = slugify(role) is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level")) width, height = (800, 450) if is_background else (128, 128) filename = f"sprite_{slug}.png" full_prompt = ( f"{prompt}, {style_hint}, game asset, clean readable shape, " "no text, no watermark" ).strip(", ") specs.append(AssetSpec(role=role, prompt=full_prompt, filename=filename, width=width, height=height)) return specsdef image_to_data_uri(content: bytes, width: int, height: int) -> str: image = Image.open(io.BytesIO(content)).convert("RGBA") image = image.resize((width, height), Image.LANCZOS) out = io.BytesIO() image.save(out, format="PNG") return "data:image/png;base64," + base64.b64encode(out.getvalue()).decode("ascii")def placeholder_data_uri(role: str, width: int, height: int) -> str: label = escape(role[:18]) svg = f"""<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}"><rect width="100%" height="100%" fill="#222"/><rect x="4" y="4" width="{width - 8}" height="{height - 8}" rx="12" fill="#3b82f6"/><text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="white" font-family="Arial" font-size="18">{label}</text></svg>""" return "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode("ascii")def generate_asset(spec: AssetSpec, index: int) -> tuple[str, str | None]: seed = abs(hash((spec.role, spec.prompt, index))) % 999999 url = POLLINATIONS_URL.format( prompt=quote(spec.prompt), width=spec.width, height=spec.height, seed=seed, ) for attempt in range(3): try: response = requests.get(url, timeout=120) response.raise_for_status() return image_to_data_uri(response.content, spec.width, spec.height), None except Exception as exc: if attempt < 2: time.sleep(8) else: return placeholder_data_uri(spec.role, spec.width, spec.height), str(exc) return placeholder_data_uri(spec.role, spec.width, spec.height), "Unknown generation error"def replacement_names(spec: AssetSpec) -> set[str]: slug = slugify(spec.role) names = { spec.filename, f"{slug}.png", f"{slug}.jpg", f"{slug}.jpeg", f"{slug}.webp", f"asset_{slug}.png", f"{spec.role.strip()}.png", f"{{{{{slug}}}}}", f"{{{slug}}}", } if slug == "background": names.update({"background.png", "sprite_background.jpg", "background.jpg"}) if slug == "player": names.update({"player.png", "sprite_player.jpg", "hero.png"}) if slug == "enemy": names.update({"enemy.png", "monster.png", "sprite_enemy.jpg"}) return namesdef embed_assets(html_code: str, assets: dict[str, str], specs: list[AssetSpec]) -> str: output = html_code manifest_lines = ["<!-- Embedded game assets generated by Image Generator for HTML Games"] for spec in specs: data_uri = assets[spec.role] manifest_lines.append(f"{spec.role}: {spec.filename}") for name in replacement_names(spec): output = output.replace(f'"{name}"', f'"{data_uri}"') output = output.replace(f"'{name}'", f"'{data_uri}'") output = output.replace(name, data_uri) manifest_lines.append("-->") manifest = "\n".join(manifest_lines) + "\n" asset_map = ",\n".join( f" {slugify(spec.role)!r}: {assets[spec.role]!r}" for spec in specs ) helper_script = f"""<script> window.GENERATED_GAME_ASSETS = {{{asset_map} }};</script>""" if "</head>" in output: output = output.replace("</head>", helper_script + "\n</head>", 1) elif "<body" in output: output = output.replace("<body", helper_script + "\n<body", 1) else: output = helper_script + "\n" + output return manifest + outputdef build_preview(html_code: str) -> str: encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii") return ( f'<iframe src="data:text/html;base64,{encoded}" ' 'style="width:100%;height:560px;border:1px solid #333;border-radius:8px;background:#000;" ' 'sandbox="allow-scripts" title="Game preview"></iframe>' )def generate_images_and_game(html_code: str, roles: str, style_hint: str): if not html_code.strip(): return "", "Paste HTML game code first.", [], "" specs = parse_assets(roles, style_hint or "pixel art style") if not specs: return html_code, "Add at least one asset role, like `player: brave knight`.", [], build_preview(html_code) assets: dict[str, str] = {} gallery = [] errors = [] for index, spec in enumerate(specs): data_uri, error = generate_asset(spec, index) assets[spec.role] = data_uri gallery.append((data_uri, f"{spec.role} -> {spec.filename}")) if error: errors.append(f"{spec.role}: fallback used ({error})") rewritten = embed_assets(html_code, assets, specs) status = f"Generated and embedded {len(specs)} asset(s)." if errors: status += "\n\n" + "\n".join(errors) return rewritten, status, gallery, build_preview(rewritten)with gr.Blocks(title="Image Generator for HTML Games") as demo: gr.Markdown( "# Image Generator for HTML Games\n" "Paste an HTML canvas game, list the image roles you want, and generate a rewritten version " "with the images embedded directly into the code." ) with gr.Row(): with gr.Column(scale=1): roles = gr.Textbox( label="Image roles to generate", value=DEFAULT_ROLES, lines=8, info="One per line: role: image description. Example: player: blue robot hero", ) style = gr.Textbox( label="Shared visual style", value="cohesive 2D pixel-art game style, transparent subject sprites where possible", lines=2, ) generate_btn = gr.Button("Generate Images + Embed Game", variant="primary") status = gr.Markdown("Ready.") gallery = gr.Gallery(label="Generated assets", columns=2, height=300) with gr.Column(scale=2): html_input = gr.Code( label="Original HTML game code", value=STARTER_HTML, language="html", lines=18, ) output_code = gr.Code( label="Rewritten HTML with embedded images", language="html", lines=18, ) gr.Markdown("## Game preview") preview = gr.HTML(build_preview(STARTER_HTML)) generate_btn.click( fn=generate_images_and_game, inputs=[html_input, roles, style], outputs=[output_code, status, gallery, preview], )if __name__ == "__main__": demo.launch()
verified