LeafCat79 commited on
Commit
4d60f00
·
verified ·
1 Parent(s): 1a0aaee

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +299 -0
app.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import re
4
+ import time
5
+ from dataclasses import dataclass
6
+ from html import escape
7
+ from urllib.parse import quote
8
+
9
+ import gradio as gr
10
+ import requests
11
+ from PIL import Image
12
+
13
+
14
+ POLLINATIONS_URL = (
15
+ "https://image.pollinations.ai/prompt/{prompt}"
16
+ "?width={width}&height={height}&model=flux&nologo=true&seed={seed}"
17
+ )
18
+
19
+
20
+ STARTER_HTML = """<!DOCTYPE html>
21
+ <html>
22
+ <head>
23
+ <meta charset="UTF-8" />
24
+ <title>Mini Forest Game</title>
25
+ <style>
26
+ body { margin: 0; background: #111; display: grid; place-items: center; min-height: 100vh; }
27
+ canvas { border: 2px solid #222; background: #222; image-rendering: pixelated; }
28
+ </style>
29
+ </head>
30
+ <body>
31
+ <canvas id="gameCanvas" width="800" height="450"></canvas>
32
+ <script>
33
+ const canvas = document.getElementById("gameCanvas");
34
+ const ctx = canvas.getContext("2d");
35
+
36
+ const background = new Image();
37
+ background.src = "sprite_background.png";
38
+ const playerImg = new Image();
39
+ playerImg.src = "sprite_player.png";
40
+
41
+ const keys = new Set();
42
+ const player = { x: 380, y: 205, w: 48, h: 48, speed: 4 };
43
+
44
+ window.addEventListener("keydown", e => keys.add(e.key));
45
+ window.addEventListener("keyup", e => keys.delete(e.key));
46
+
47
+ function update() {
48
+ if (keys.has("a") || keys.has("ArrowLeft")) player.x -= player.speed;
49
+ if (keys.has("d") || keys.has("ArrowRight")) player.x += player.speed;
50
+ if (keys.has("w") || keys.has("ArrowUp")) player.y -= player.speed;
51
+ if (keys.has("s") || keys.has("ArrowDown")) player.y += player.speed;
52
+ player.x = Math.max(0, Math.min(canvas.width - player.w, player.x));
53
+ player.y = Math.max(0, Math.min(canvas.height - player.h, player.y));
54
+ }
55
+
56
+ function draw() {
57
+ ctx.drawImage(background, 0, 0, canvas.width, canvas.height);
58
+ ctx.drawImage(playerImg, player.x, player.y, player.w, player.h);
59
+ ctx.fillStyle = "white";
60
+ ctx.font = "18px sans-serif";
61
+ ctx.fillText("Use WASD or arrow keys", 18, 28);
62
+ }
63
+
64
+ function loop() {
65
+ update();
66
+ draw();
67
+ requestAnimationFrame(loop);
68
+ }
69
+ loop();
70
+ </script>
71
+ </body>
72
+ </html>"""
73
+
74
+
75
+ DEFAULT_ROLES = """player: top-down pixel-art adventurer hero, transparent background, bright readable silhouette
76
+ background: enchanted forest clearing game background, top-down view, soft moonlight, detailed but not too busy"""
77
+
78
+
79
+ @dataclass
80
+ class AssetSpec:
81
+ role: str
82
+ prompt: str
83
+ filename: str
84
+ width: int
85
+ height: int
86
+
87
+
88
+ def slugify(value: str) -> str:
89
+ value = re.sub(r"[^a-zA-Z0-9]+", "_", value.strip().lower()).strip("_")
90
+ return value or "asset"
91
+
92
+
93
+ def parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]:
94
+ specs: list[AssetSpec] = []
95
+ for line in raw_roles.splitlines():
96
+ line = line.strip()
97
+ if not line or line.startswith("#"):
98
+ continue
99
+
100
+ if ":" in line:
101
+ role, prompt = line.split(":", 1)
102
+ elif "=" in line:
103
+ role, prompt = line.split("=", 1)
104
+ else:
105
+ role, prompt = line, line
106
+
107
+ role = role.strip()
108
+ prompt = prompt.strip() or role
109
+ slug = slugify(role)
110
+ is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
111
+ width, height = (800, 450) if is_background else (128, 128)
112
+ filename = f"sprite_{slug}.png"
113
+ full_prompt = (
114
+ f"{prompt}, {style_hint}, game asset, clean readable shape, "
115
+ "no text, no watermark"
116
+ ).strip(", ")
117
+ specs.append(AssetSpec(role=role, prompt=full_prompt, filename=filename, width=width, height=height))
118
+ return specs
119
+
120
+
121
+ def image_to_data_uri(content: bytes, width: int, height: int) -> str:
122
+ image = Image.open(io.BytesIO(content)).convert("RGBA")
123
+ image = image.resize((width, height), Image.LANCZOS)
124
+ out = io.BytesIO()
125
+ image.save(out, format="PNG")
126
+ return "data:image/png;base64," + base64.b64encode(out.getvalue()).decode("ascii")
127
+
128
+
129
+ def placeholder_data_uri(role: str, width: int, height: int) -> str:
130
+ label = escape(role[:18])
131
+ svg = f"""<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">
132
+ <rect width="100%" height="100%" fill="#222"/>
133
+ <rect x="4" y="4" width="{width - 8}" height="{height - 8}" rx="12" fill="#3b82f6"/>
134
+ <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="white" font-family="Arial" font-size="18">{label}</text>
135
+ </svg>"""
136
+ return "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode("ascii")
137
+
138
+
139
+ def generate_asset(spec: AssetSpec, index: int) -> tuple[str, str | None]:
140
+ seed = abs(hash((spec.role, spec.prompt, index))) % 999999
141
+ url = POLLINATIONS_URL.format(
142
+ prompt=quote(spec.prompt),
143
+ width=spec.width,
144
+ height=spec.height,
145
+ seed=seed,
146
+ )
147
+
148
+ for attempt in range(3):
149
+ try:
150
+ response = requests.get(url, timeout=120)
151
+ response.raise_for_status()
152
+ return image_to_data_uri(response.content, spec.width, spec.height), None
153
+ except Exception as exc:
154
+ if attempt < 2:
155
+ time.sleep(8)
156
+ else:
157
+ return placeholder_data_uri(spec.role, spec.width, spec.height), str(exc)
158
+ return placeholder_data_uri(spec.role, spec.width, spec.height), "Unknown generation error"
159
+
160
+
161
+ def replacement_names(spec: AssetSpec) -> set[str]:
162
+ slug = slugify(spec.role)
163
+ names = {
164
+ spec.filename,
165
+ f"{slug}.png",
166
+ f"{slug}.jpg",
167
+ f"{slug}.jpeg",
168
+ f"{slug}.webp",
169
+ f"asset_{slug}.png",
170
+ f"{spec.role.strip()}.png",
171
+ f"{{{{{slug}}}}}",
172
+ f"{{{slug}}}",
173
+ }
174
+ if slug == "background":
175
+ names.update({"background.png", "sprite_background.jpg", "background.jpg"})
176
+ if slug == "player":
177
+ names.update({"player.png", "sprite_player.jpg", "hero.png"})
178
+ if slug == "enemy":
179
+ names.update({"enemy.png", "monster.png", "sprite_enemy.jpg"})
180
+ return names
181
+
182
+
183
+ def embed_assets(html_code: str, assets: dict[str, str], specs: list[AssetSpec]) -> str:
184
+ output = html_code
185
+ manifest_lines = ["<!-- Embedded game assets generated by Image Generator for HTML Games"]
186
+
187
+ for spec in specs:
188
+ data_uri = assets[spec.role]
189
+ manifest_lines.append(f"{spec.role}: {spec.filename}")
190
+ for name in replacement_names(spec):
191
+ output = output.replace(f'"{name}"', f'"{data_uri}"')
192
+ output = output.replace(f"'{name}'", f"'{data_uri}'")
193
+ output = output.replace(name, data_uri)
194
+
195
+ manifest_lines.append("-->")
196
+ manifest = "\n".join(manifest_lines) + "\n"
197
+ asset_map = ",\n".join(
198
+ f" {slugify(spec.role)!r}: {assets[spec.role]!r}" for spec in specs
199
+ )
200
+ helper_script = f"""<script>
201
+ window.GENERATED_GAME_ASSETS = {{
202
+ {asset_map}
203
+ }};
204
+ </script>"""
205
+
206
+ if "</head>" in output:
207
+ output = output.replace("</head>", helper_script + "\n</head>", 1)
208
+ elif "<body" in output:
209
+ output = output.replace("<body", helper_script + "\n<body", 1)
210
+ else:
211
+ output = helper_script + "\n" + output
212
+
213
+ return manifest + output
214
+
215
+
216
+ def build_preview(html_code: str) -> str:
217
+ encoded = base64.b64encode(html_code.encode("utf-8")).decode("ascii")
218
+ return (
219
+ f'<iframe src="data:text/html;base64,{encoded}" '
220
+ 'style="width:100%;height:560px;border:1px solid #333;border-radius:8px;background:#000;" '
221
+ 'sandbox="allow-scripts" title="Game preview"></iframe>'
222
+ )
223
+
224
+
225
+ def generate_images_and_game(html_code: str, roles: str, style_hint: str):
226
+ if not html_code.strip():
227
+ return "", "Paste HTML game code first.", [], ""
228
+
229
+ specs = parse_assets(roles, style_hint or "pixel art style")
230
+ if not specs:
231
+ return html_code, "Add at least one asset role, like `player: brave knight`.", [], build_preview(html_code)
232
+
233
+ assets: dict[str, str] = {}
234
+ gallery = []
235
+ errors = []
236
+
237
+ for index, spec in enumerate(specs):
238
+ data_uri, error = generate_asset(spec, index)
239
+ assets[spec.role] = data_uri
240
+ gallery.append((data_uri, f"{spec.role} -> {spec.filename}"))
241
+ if error:
242
+ errors.append(f"{spec.role}: fallback used ({error})")
243
+
244
+ rewritten = embed_assets(html_code, assets, specs)
245
+ status = f"Generated and embedded {len(specs)} asset(s)."
246
+ if errors:
247
+ status += "\n\n" + "\n".join(errors)
248
+ return rewritten, status, gallery, build_preview(rewritten)
249
+
250
+
251
+ with gr.Blocks(title="Image Generator for HTML Games") as demo:
252
+ gr.Markdown(
253
+ "# Image Generator for HTML Games\n"
254
+ "Paste an HTML canvas game, list the image roles you want, and generate a rewritten version "
255
+ "with the images embedded directly into the code."
256
+ )
257
+
258
+ with gr.Row():
259
+ with gr.Column(scale=1):
260
+ roles = gr.Textbox(
261
+ label="Image roles to generate",
262
+ value=DEFAULT_ROLES,
263
+ lines=8,
264
+ info="One per line: role: image description. Example: player: blue robot hero",
265
+ )
266
+ style = gr.Textbox(
267
+ label="Shared visual style",
268
+ value="cohesive 2D pixel-art game style, transparent subject sprites where possible",
269
+ lines=2,
270
+ )
271
+ generate_btn = gr.Button("Generate Images + Embed Game", variant="primary")
272
+ status = gr.Markdown("Ready.")
273
+ gallery = gr.Gallery(label="Generated assets", columns=2, height=300)
274
+
275
+ with gr.Column(scale=2):
276
+ html_input = gr.Code(
277
+ label="Original HTML game code",
278
+ value=STARTER_HTML,
279
+ language="html",
280
+ lines=18,
281
+ )
282
+ output_code = gr.Code(
283
+ label="Rewritten HTML with embedded images",
284
+ language="html",
285
+ lines=18,
286
+ )
287
+
288
+ gr.Markdown("## Game preview")
289
+ preview = gr.HTML(build_preview(STARTER_HTML))
290
+
291
+ generate_btn.click(
292
+ fn=generate_images_and_game,
293
+ inputs=[html_input, roles, style],
294
+ outputs=[output_code, status, gallery, preview],
295
+ )
296
+
297
+
298
+ if __name__ == "__main__":
299
+ demo.launch()