Spaces:
Running on Zero
Running on Zero
Add deterministic asset integration workflow
Browse files- .gitignore +3 -0
- README.md +20 -2
- app.py +375 -137
.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
.pytest_cache/
|
README.md
CHANGED
|
@@ -11,9 +11,27 @@ pinned: false
|
|
| 11 |
|
| 12 |
# Image Generator for HTML Games
|
| 13 |
|
| 14 |
-
Paste an HTML game, describe asset roles like `player`, `background`, or `enemy`, and generate
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
If the Space has an `HF_TOKEN` secret, it first uses the text model in `HF_PROMPT_MODEL` to interpret the code, roles, and theme into image prompts. The default prompt model is `Qwen/Qwen2.5-Coder-7B-Instruct`.
|
| 19 |
|
|
|
|
| 11 |
|
| 12 |
# Image Generator for HTML Games
|
| 13 |
|
| 14 |
+
Paste an HTML game, select its game type and camera perspective, describe asset roles like `player`, `background`, or `enemy`, and generate game-ready images. The Space rewrites the game only when every requested role has a deterministic integration point.
|
| 15 |
|
| 16 |
+
## Deterministic asset contract
|
| 17 |
+
|
| 18 |
+
Reference each requested role with either an exact filename or an explicit `GAME_ASSETS` hook:
|
| 19 |
+
|
| 20 |
+
```js
|
| 21 |
+
const playerImage = new Image();
|
| 22 |
+
playerImage.src = GAME_ASSETS.player; // or "sprite_player.png"
|
| 23 |
+
|
| 24 |
+
const backgroundImage = new Image();
|
| 25 |
+
backgroundImage.src = GAME_ASSETS.background; // or "sprite_background.png"
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
The generator injects an immutable `window.GAME_ASSETS` manifest whose keys are normalized role names. It does not monkey-patch browser image APIs, guess aliases, or silently replace canvas primitives such as `fillRect()`, `arc()`, and `fill()`.
|
| 29 |
+
|
| 30 |
+
If a requested role has no deterministic hook, assets are still generated for review, but no rewritten game is returned. The status explains which hooks are missing and the preview is explicitly the unchanged original game.
|
| 31 |
+
|
| 32 |
+
Generated sprites receive automated dimension, alpha-channel, and corner-transparency checks. These checks do not establish semantic or artistic correctness, so every role also has a manual approval control and per-role regeneration.
|
| 33 |
+
|
| 34 |
+
The app combines the game type, perspective, theme, and pasted code into explicit per-role image prompts, then generates PNG assets and embeds contract-compatible assets as base64 data URIs.
|
| 35 |
|
| 36 |
If the Space has an `HF_TOKEN` secret, it first uses the text model in `HF_PROMPT_MODEL` to interpret the code, roles, and theme into image prompts. The default prompt model is `Qwen/Qwen2.5-Coder-7B-Instruct`.
|
| 37 |
|
app.py
CHANGED
|
@@ -105,6 +105,15 @@ class StylePlan:
|
|
| 105 |
tags: tuple[str, ...]
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 109 |
FREE_IMAGE_MODEL = os.environ.get("FREE_IMAGE_MODEL", "segmind/tiny-sd")
|
| 110 |
FREE_IMAGE_STEPS = int(os.environ.get("FREE_IMAGE_STEPS", "5"))
|
|
@@ -189,10 +198,16 @@ def interpret_style_hint(style_hint: str) -> StylePlan:
|
|
| 189 |
lighting = "balanced game lighting"
|
| 190 |
linework = "clear readable silhouette and controlled edges"
|
| 191 |
|
| 192 |
-
if has("top-down", "top down", "
|
| 193 |
camera = "top-down readable game camera"
|
| 194 |
elif has("platformer", "side-scroller", "side scroller"):
|
| 195 |
camera = "side-view platformer camera"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
else:
|
| 197 |
camera = "game-ready camera angle matching the role"
|
| 198 |
|
|
@@ -211,18 +226,31 @@ def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
|
|
| 211 |
)
|
| 212 |
else:
|
| 213 |
asset_instruction = (
|
| 214 |
-
"Create one complete standalone 2D sprite of the whole subject, centered
|
| 215 |
"single object, transparent or plain background, readable silhouette. Not a texture map, not a tiled "
|
| 216 |
"pattern, not a material swatch, not a UV unwrap, not a 3D model skin."
|
| 217 |
)
|
| 218 |
return (
|
| 219 |
-
f"{role} asset: {prompt}.
|
|
|
|
| 220 |
f"{plan.texture}; {plan.lighting}; {plan.linework}; {plan.camera}. "
|
| 221 |
f"{asset_instruction} "
|
| 222 |
"Game asset, readable at small size, no text, no watermark."
|
| 223 |
)
|
| 224 |
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
def parse_role_lines(raw_roles: str) -> list[tuple[str, str]]:
|
| 227 |
parsed: list[tuple[str, str]] = []
|
| 228 |
for line in raw_roles.splitlines():
|
|
@@ -555,7 +583,10 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
|
|
| 555 |
draw = ImageDraw.Draw(image, "RGBA")
|
| 556 |
prompt = spec.prompt.lower()
|
| 557 |
|
| 558 |
-
if
|
|
|
|
|
|
|
|
|
|
| 559 |
image = Image.new("RGBA", (spec.width, spec.height), (18, 22, 31, 255))
|
| 560 |
draw = ImageDraw.Draw(image, "RGBA")
|
| 561 |
panel = 64
|
|
@@ -750,6 +781,34 @@ def draw_sprite(spec: AssetSpec, rng: random.Random) -> bytes:
|
|
| 750 |
accent = colors[1] + (255,)
|
| 751 |
trim = colors[3] + (255,)
|
| 752 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
if any(word in role_prompt for word in ("car", "racing", "racer", "buggy", "vehicle", "truck")):
|
| 754 |
shadow = (int(cx - 43 * scale), int(cy + 28 * scale), int(cx + 43 * scale), int(cy + 42 * scale))
|
| 755 |
draw.ellipse(shadow, fill=(0, 0, 0, 58))
|
|
@@ -1149,151 +1208,124 @@ def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str,
|
|
| 1149 |
|
| 1150 |
def replacement_names(spec: AssetSpec) -> set[str]:
|
| 1151 |
slug = slugify(spec.role)
|
| 1152 |
-
|
| 1153 |
spec.filename,
|
| 1154 |
f"{slug}.png",
|
| 1155 |
f"{slug}.jpg",
|
| 1156 |
f"{slug}.jpeg",
|
| 1157 |
f"{slug}.webp",
|
| 1158 |
-
f"asset_{slug}.png",
|
| 1159 |
-
f"{spec.role.strip()}.png",
|
| 1160 |
f"{{{{{slug}}}}}",
|
| 1161 |
f"{{{slug}}}",
|
| 1162 |
}
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
names.update({"player.png", "sprite_player.jpg", "hero.png"})
|
| 1167 |
-
if slug == "enemy":
|
| 1168 |
-
names.update({"enemy.png", "monster.png", "sprite_enemy.jpg"})
|
| 1169 |
-
if slug == "bullet":
|
| 1170 |
-
names.update({"bullet.png", "projectile.png", "laser.png", "shot.png", "sprite_bullet.jpg"})
|
| 1171 |
-
return names
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
def asset_aliases(spec: AssetSpec) -> list[str]:
|
| 1175 |
slug = slugify(spec.role)
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
"
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1189 |
|
| 1190 |
|
| 1191 |
def embed_assets(html_code: str, assets: dict[str, str], specs: list[AssetSpec]) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
output = html_code
|
| 1193 |
manifest_lines = ["<!-- Embedded game assets generated by Image Generator for HTML Games"]
|
| 1194 |
-
background_uri = None
|
| 1195 |
asset_map: dict[str, str] = {}
|
| 1196 |
-
alias_map: dict[str, list[str]] = {}
|
| 1197 |
|
| 1198 |
for spec in specs:
|
| 1199 |
data_uri = assets[spec.role]
|
| 1200 |
slug = slugify(spec.role)
|
| 1201 |
asset_map[slug] = data_uri
|
| 1202 |
-
alias_map[slug] = asset_aliases(spec)
|
| 1203 |
manifest_lines.append(f"{spec.role}: {spec.filename}")
|
| 1204 |
-
if is_background_spec(spec) and background_uri is None:
|
| 1205 |
-
background_uri = data_uri
|
| 1206 |
for name in replacement_names(spec):
|
| 1207 |
output = output.replace(f'"{name}"', f'"{data_uri}"')
|
| 1208 |
output = output.replace(f"'{name}'", f"'{data_uri}'")
|
| 1209 |
-
|
|
|
|
| 1210 |
|
| 1211 |
manifest_lines.append("-->")
|
| 1212 |
manifest = "\n".join(manifest_lines) + "\n"
|
| 1213 |
asset_json = json.dumps(asset_map)
|
| 1214 |
-
alias_json = json.dumps(alias_map)
|
| 1215 |
-
background_json = json.dumps(background_uri)
|
| 1216 |
helper_script = f"""<script>
|
| 1217 |
(function () {{
|
| 1218 |
var ASSETS = {asset_json};
|
| 1219 |
-
|
| 1220 |
window.GENERATED_GAME_ASSETS = ASSETS;
|
| 1221 |
-
|
| 1222 |
-
function basename(value) {{
|
| 1223 |
-
return String(value || "").split("?")[0].split("#")[0].split("/").pop().toLowerCase();
|
| 1224 |
-
}}
|
| 1225 |
-
|
| 1226 |
-
function pickAsset(value) {{
|
| 1227 |
-
var text = String(value || "").toLowerCase();
|
| 1228 |
-
if (!text || text.indexOf("data:image/") === 0) return value;
|
| 1229 |
-
var file = basename(text);
|
| 1230 |
-
for (var role in ASSETS) {{
|
| 1231 |
-
var aliases = ALIASES[role] || [role];
|
| 1232 |
-
for (var i = 0; i < aliases.length; i++) {{
|
| 1233 |
-
var alias = String(aliases[i]).toLowerCase();
|
| 1234 |
-
if (!alias) continue;
|
| 1235 |
-
if (file === alias || file === alias + ".png" || file === "sprite_" + alias + ".png") return ASSETS[role];
|
| 1236 |
-
if (file.indexOf(alias) !== -1 || text.indexOf("/" + alias) !== -1 || text.indexOf("_" + alias) !== -1) return ASSETS[role];
|
| 1237 |
-
}}
|
| 1238 |
-
}}
|
| 1239 |
-
return value;
|
| 1240 |
-
}}
|
| 1241 |
-
|
| 1242 |
-
var descriptor = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
|
| 1243 |
-
if (descriptor && descriptor.set && !HTMLImageElement.prototype.__generatedAssetMapper) {{
|
| 1244 |
-
Object.defineProperty(HTMLImageElement.prototype, "src", {{
|
| 1245 |
-
get: function () {{ return descriptor.get.call(this); }},
|
| 1246 |
-
set: function (value) {{ descriptor.set.call(this, pickAsset(value)); }},
|
| 1247 |
-
configurable: true,
|
| 1248 |
-
enumerable: descriptor.enumerable
|
| 1249 |
-
}});
|
| 1250 |
-
HTMLImageElement.prototype.__generatedAssetMapper = true;
|
| 1251 |
-
}}
|
| 1252 |
-
|
| 1253 |
-
var originalDrawImage = CanvasRenderingContext2D.prototype.drawImage;
|
| 1254 |
-
if (!CanvasRenderingContext2D.prototype.__generatedAssetDrawGuard) {{
|
| 1255 |
-
CanvasRenderingContext2D.prototype.drawImage = function (image) {{
|
| 1256 |
-
try {{
|
| 1257 |
-
if (image instanceof HTMLImageElement) {{
|
| 1258 |
-
var current = image.getAttribute("src") || image.src || "";
|
| 1259 |
-
var mapped = pickAsset(current);
|
| 1260 |
-
if (mapped && mapped !== current) image.src = mapped;
|
| 1261 |
-
if (!image.complete || image.naturalWidth === 0 || image.naturalHeight === 0) {{
|
| 1262 |
-
var looksLikeBackground = /background|backdrop|scene|map|level|bg/i.test(current);
|
| 1263 |
-
var coversCanvas = arguments.length >= 5 && arguments[1] === 0 && arguments[2] === 0 &&
|
| 1264 |
-
arguments[3] >= this.canvas.width * 0.8 && arguments[4] >= this.canvas.height * 0.8;
|
| 1265 |
-
if (looksLikeBackground || coversCanvas) {{
|
| 1266 |
-
this.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
| 1267 |
-
}}
|
| 1268 |
-
return;
|
| 1269 |
-
}}
|
| 1270 |
-
}}
|
| 1271 |
-
return originalDrawImage.apply(this, arguments);
|
| 1272 |
-
}} catch (error) {{
|
| 1273 |
-
return;
|
| 1274 |
-
}}
|
| 1275 |
-
}};
|
| 1276 |
-
CanvasRenderingContext2D.prototype.__generatedAssetDrawGuard = true;
|
| 1277 |
-
}}
|
| 1278 |
-
|
| 1279 |
-
window.addEventListener("DOMContentLoaded", function () {{
|
| 1280 |
-
document.querySelectorAll("img").forEach(function (img) {{
|
| 1281 |
-
var mapped = pickAsset(img.getAttribute("src") || img.src);
|
| 1282 |
-
if (mapped !== (img.getAttribute("src") || img.src)) img.src = mapped;
|
| 1283 |
-
}});
|
| 1284 |
-
var background = {background_json};
|
| 1285 |
-
if (!background) return;
|
| 1286 |
-
document.querySelectorAll("canvas").forEach(function (canvas) {{
|
| 1287 |
-
canvas.style.backgroundImage = "url(" + background + ")";
|
| 1288 |
-
canvas.style.backgroundSize = "cover";
|
| 1289 |
-
canvas.style.backgroundPosition = "center";
|
| 1290 |
-
}});
|
| 1291 |
-
}});
|
| 1292 |
}})();
|
| 1293 |
</script>"""
|
| 1294 |
|
| 1295 |
-
if "<
|
| 1296 |
-
output =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1297 |
elif "<body" in output:
|
| 1298 |
output = output.replace("<body", helper_script + "\n<body", 1)
|
| 1299 |
else:
|
|
@@ -1325,19 +1357,102 @@ def summarize_model_sources(rows: list[tuple[str, str, str]]) -> str:
|
|
| 1325 |
return f"prompt={', '.join(prompt_sources)}; image={', '.join(image_sources)}"
|
| 1326 |
|
| 1327 |
|
| 1328 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
if not html_code.strip():
|
| 1330 |
-
return "
|
| 1331 |
|
| 1332 |
-
|
| 1333 |
-
|
|
|
|
| 1334 |
if not specs:
|
| 1335 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1336 |
|
| 1337 |
assets: dict[str, str] = {}
|
| 1338 |
-
|
|
|
|
|
|
|
| 1339 |
errors = []
|
| 1340 |
-
model_rows = []
|
| 1341 |
run_id = time.time_ns()
|
| 1342 |
if prompt_error:
|
| 1343 |
errors.append(f"prompt model: {prompt_error}")
|
|
@@ -1345,19 +1460,71 @@ def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
|
| 1345 |
for index, spec in enumerate(specs):
|
| 1346 |
data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
|
| 1347 |
assets[spec.role] = data_uri
|
| 1348 |
-
|
| 1349 |
-
|
|
|
|
|
|
|
| 1350 |
if error:
|
| 1351 |
errors.append(f"{spec.role}: image model failed ({error}); used local procedural fallback")
|
| 1352 |
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1357 |
)
|
| 1358 |
-
if errors:
|
| 1359 |
-
status += "\n\n" + "\n".join(errors)
|
| 1360 |
-
return rewritten, status, gallery, build_prompt_preview(specs), build_model_report(model_rows), build_preview(rewritten)
|
| 1361 |
|
| 1362 |
|
| 1363 |
def check_hf_token() -> str:
|
|
@@ -1382,34 +1549,74 @@ def check_hf_token() -> str:
|
|
| 1382 |
with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
| 1383 |
gr.Markdown(
|
| 1384 |
"# Image Generator for HTML Games\n"
|
| 1385 |
-
"
|
| 1386 |
-
"
|
|
|
|
| 1387 |
)
|
| 1388 |
|
|
|
|
|
|
|
| 1389 |
with gr.Row():
|
| 1390 |
with gr.Column(scale=1):
|
| 1391 |
roles = gr.Textbox(
|
| 1392 |
label="Image roles to generate",
|
| 1393 |
lines=8,
|
| 1394 |
placeholder=ROLE_PLACEHOLDER,
|
|
|
|
| 1395 |
info="One per line: role: image description. Example: player: blue robot hero",
|
| 1396 |
)
|
| 1397 |
-
|
| 1398 |
-
label="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1399 |
lines=2,
|
| 1400 |
-
placeholder="
|
| 1401 |
)
|
| 1402 |
generate_btn = gr.Button("Generate Images + Embed Game", variant="primary")
|
| 1403 |
status = gr.Markdown("Ready.")
|
| 1404 |
token_btn = gr.Button("Check HF Token")
|
| 1405 |
token_status = gr.Markdown("")
|
| 1406 |
gallery = gr.Gallery(label="Generated assets", columns=2, height=300)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1407 |
|
| 1408 |
with gr.Column(scale=2):
|
| 1409 |
html_input = gr.Textbox(
|
| 1410 |
label="Original HTML game code",
|
| 1411 |
lines=18,
|
| 1412 |
placeholder="Paste your full HTML game code here.",
|
|
|
|
| 1413 |
)
|
| 1414 |
output_code = gr.Code(
|
| 1415 |
label="Rewritten HTML with embedded images",
|
|
@@ -1426,14 +1633,45 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 1426 |
lines=5,
|
| 1427 |
interactive=False,
|
| 1428 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1429 |
|
| 1430 |
gr.Markdown("## Game preview")
|
| 1431 |
preview = gr.HTML("")
|
| 1432 |
|
| 1433 |
generate_btn.click(
|
| 1434 |
fn=generate_images_and_game,
|
| 1435 |
-
inputs=[html_input, roles,
|
| 1436 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1437 |
)
|
| 1438 |
token_btn.click(fn=check_hf_token, inputs=None, outputs=token_status)
|
| 1439 |
|
|
|
|
| 105 |
tags: tuple[str, ...]
|
| 106 |
|
| 107 |
|
| 108 |
+
@dataclass
|
| 109 |
+
class IntegrationReport:
|
| 110 |
+
supported: bool
|
| 111 |
+
mode: str
|
| 112 |
+
referenced_roles: tuple[str, ...]
|
| 113 |
+
missing_roles: tuple[str, ...]
|
| 114 |
+
warnings: tuple[str, ...]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 118 |
FREE_IMAGE_MODEL = os.environ.get("FREE_IMAGE_MODEL", "segmind/tiny-sd")
|
| 119 |
FREE_IMAGE_STEPS = int(os.environ.get("FREE_IMAGE_STEPS", "5"))
|
|
|
|
| 198 |
lighting = "balanced game lighting"
|
| 199 |
linework = "clear readable silhouette and controlled edges"
|
| 200 |
|
| 201 |
+
if has("top-down", "top down", "overhead"):
|
| 202 |
camera = "top-down readable game camera"
|
| 203 |
elif has("platformer", "side-scroller", "side scroller"):
|
| 204 |
camera = "side-view platformer camera"
|
| 205 |
+
elif has("isometric", "3/4 view", "three-quarter"):
|
| 206 |
+
camera = "consistent isometric three-quarter game camera"
|
| 207 |
+
elif has("first-person", "first person"):
|
| 208 |
+
camera = "first-person game camera"
|
| 209 |
+
elif has("front-facing", "front facing"):
|
| 210 |
+
camera = "front-facing game camera"
|
| 211 |
else:
|
| 212 |
camera = "game-ready camera angle matching the role"
|
| 213 |
|
|
|
|
| 226 |
)
|
| 227 |
else:
|
| 228 |
asset_instruction = (
|
| 229 |
+
"Create one complete standalone 2D sprite of the whole subject, centered and fully visible, "
|
| 230 |
"single object, transparent or plain background, readable silhouette. Not a texture map, not a tiled "
|
| 231 |
"pattern, not a material swatch, not a UV unwrap, not a 3D model skin."
|
| 232 |
)
|
| 233 |
return (
|
| 234 |
+
f"{role} asset: {prompt}. Creative brief: {style_hint}. "
|
| 235 |
+
f"Style interpretation: {plan.medium}; {plan.palette}; "
|
| 236 |
f"{plan.texture}; {plan.lighting}; {plan.linework}; {plan.camera}. "
|
| 237 |
f"{asset_instruction} "
|
| 238 |
"Game asset, readable at small size, no text, no watermark."
|
| 239 |
)
|
| 240 |
|
| 241 |
|
| 242 |
+
def build_style_context(game_type: str, perspective: str, theme: str) -> str:
|
| 243 |
+
parts = []
|
| 244 |
+
if game_type and game_type != "Other / custom":
|
| 245 |
+
parts.append(f"Game type: {game_type}")
|
| 246 |
+
if perspective and perspective != "Auto-detect from code":
|
| 247 |
+
parts.append(f"Required camera perspective: {perspective}")
|
| 248 |
+
normalized_theme = (theme or "").strip()
|
| 249 |
+
if normalized_theme:
|
| 250 |
+
parts.append(f"Theme and visual direction: {normalized_theme}")
|
| 251 |
+
return ". ".join(parts) or "cohesive game-ready 2D art"
|
| 252 |
+
|
| 253 |
+
|
| 254 |
def parse_role_lines(raw_roles: str) -> list[tuple[str, str]]:
|
| 255 |
parsed: list[tuple[str, str]] = []
|
| 256 |
for line in raw_roles.splitlines():
|
|
|
|
| 583 |
draw = ImageDraw.Draw(image, "RGBA")
|
| 584 |
prompt = spec.prompt.lower()
|
| 585 |
|
| 586 |
+
if (
|
| 587 |
+
any(word in prompt for word in ("sci-fi", "scifi", "arena", "metal", "hazard", "grid", "shooter"))
|
| 588 |
+
and not any(word in prompt for word in ("space", "starfield", "nebula", "galaxy", "cosmic"))
|
| 589 |
+
):
|
| 590 |
image = Image.new("RGBA", (spec.width, spec.height), (18, 22, 31, 255))
|
| 591 |
draw = ImageDraw.Draw(image, "RGBA")
|
| 592 |
panel = 64
|
|
|
|
| 781 |
accent = colors[1] + (255,)
|
| 782 |
trim = colors[3] + (255,)
|
| 783 |
|
| 784 |
+
if role in ("asteroid", "rock", "meteor") or any(
|
| 785 |
+
word in role_prompt for word in ("asteroid", "meteor", "space rock")
|
| 786 |
+
):
|
| 787 |
+
points = []
|
| 788 |
+
for step in range(12):
|
| 789 |
+
angle = (math.pi * 2 * step / 12) - math.pi / 2
|
| 790 |
+
radius = rng.randint(int(34 * scale), int(49 * scale))
|
| 791 |
+
points.append((cx + math.cos(angle) * radius, cy + math.sin(angle) * radius))
|
| 792 |
+
draw.polygon(points, fill=outline)
|
| 793 |
+
inner = [
|
| 794 |
+
(cx + (x - cx) * 0.84, cy + (y - cy) * 0.84)
|
| 795 |
+
for x, y in points
|
| 796 |
+
]
|
| 797 |
+
draw.polygon(inner, fill=body)
|
| 798 |
+
for _ in range(7):
|
| 799 |
+
crater_x = cx + rng.randint(-25, 25) * scale
|
| 800 |
+
crater_y = cy + rng.randint(-25, 25) * scale
|
| 801 |
+
crater_r = rng.randint(4, 11) * scale
|
| 802 |
+
draw.ellipse(
|
| 803 |
+
(crater_x - crater_r, crater_y - crater_r, crater_x + crater_r, crater_y + crater_r),
|
| 804 |
+
fill=outline[:3] + (105,),
|
| 805 |
+
outline=trim[:3] + (155,),
|
| 806 |
+
width=max(1, int(2 * scale)),
|
| 807 |
+
)
|
| 808 |
+
out = io.BytesIO()
|
| 809 |
+
image.save(out, format="PNG")
|
| 810 |
+
return out.getvalue()
|
| 811 |
+
|
| 812 |
if any(word in role_prompt for word in ("car", "racing", "racer", "buggy", "vehicle", "truck")):
|
| 813 |
shadow = (int(cx - 43 * scale), int(cy + 28 * scale), int(cx + 43 * scale), int(cy + 42 * scale))
|
| 814 |
draw.ellipse(shadow, fill=(0, 0, 0, 58))
|
|
|
|
| 1208 |
|
| 1209 |
def replacement_names(spec: AssetSpec) -> set[str]:
|
| 1210 |
slug = slugify(spec.role)
|
| 1211 |
+
return {
|
| 1212 |
spec.filename,
|
| 1213 |
f"{slug}.png",
|
| 1214 |
f"{slug}.jpg",
|
| 1215 |
f"{slug}.jpeg",
|
| 1216 |
f"{slug}.webp",
|
|
|
|
|
|
|
| 1217 |
f"{{{{{slug}}}}}",
|
| 1218 |
f"{{{slug}}}",
|
| 1219 |
}
|
| 1220 |
+
|
| 1221 |
+
|
| 1222 |
+
def code_references_role(html_code: str, spec: AssetSpec) -> bool:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1223 |
slug = slugify(spec.role)
|
| 1224 |
+
escaped_slug = re.escape(slug)
|
| 1225 |
+
hook_patterns = (
|
| 1226 |
+
rf"(?:window\.)?GAME_ASSETS\s*\.\s*{escaped_slug}\b",
|
| 1227 |
+
rf"(?:window\.)?GAME_ASSETS\s*\[\s*['\"]{escaped_slug}['\"]\s*\]",
|
| 1228 |
+
)
|
| 1229 |
+
if any(re.search(pattern, html_code, flags=re.I) for pattern in hook_patterns):
|
| 1230 |
+
return True
|
| 1231 |
+
lowered = html_code.lower()
|
| 1232 |
+
return any(name.lower() in lowered for name in replacement_names(spec))
|
| 1233 |
+
|
| 1234 |
+
|
| 1235 |
+
def analyze_integration(html_code: str, specs: list[AssetSpec]) -> IntegrationReport:
|
| 1236 |
+
referenced = tuple(spec.role for spec in specs if code_references_role(html_code, spec))
|
| 1237 |
+
missing = tuple(spec.role for spec in specs if spec.role not in referenced)
|
| 1238 |
+
primitive_calls = sorted(
|
| 1239 |
+
set(
|
| 1240 |
+
re.findall(
|
| 1241 |
+
r"\.(arc|ellipse|fillRect|strokeRect|moveTo|lineTo|fill|stroke)\s*\(",
|
| 1242 |
+
html_code,
|
| 1243 |
+
)
|
| 1244 |
+
)
|
| 1245 |
+
)
|
| 1246 |
+
warnings = []
|
| 1247 |
+
if primitive_calls:
|
| 1248 |
+
warnings.append(
|
| 1249 |
+
"Canvas primitive drawing detected ("
|
| 1250 |
+
+ ", ".join(primitive_calls)
|
| 1251 |
+
+ "). Primitive geometry is preserved; it is never guessed or silently replaced."
|
| 1252 |
+
)
|
| 1253 |
+
if missing:
|
| 1254 |
+
warnings.append(
|
| 1255 |
+
"Missing deterministic asset hook(s): "
|
| 1256 |
+
+ ", ".join(f"GAME_ASSETS.{slugify(role)}" for role in missing)
|
| 1257 |
+
+ "."
|
| 1258 |
+
)
|
| 1259 |
+
if missing:
|
| 1260 |
+
return IntegrationReport(False, "unsupported", referenced, missing, tuple(warnings))
|
| 1261 |
+
return IntegrationReport(True, "explicit asset contract", referenced, (), tuple(warnings))
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def validate_asset_png(content: bytes, spec: AssetSpec) -> list[str]:
|
| 1265 |
+
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
| 1266 |
+
warnings = []
|
| 1267 |
+
if image.size != (spec.width, spec.height):
|
| 1268 |
+
warnings.append(f"unexpected dimensions {image.width}x{image.height}")
|
| 1269 |
+
alpha = image.getchannel("A")
|
| 1270 |
+
extrema = alpha.getextrema()
|
| 1271 |
+
if is_background_spec(spec):
|
| 1272 |
+
if extrema[0] < 255:
|
| 1273 |
+
warnings.append("background contains transparency")
|
| 1274 |
+
else:
|
| 1275 |
+
transparent_pixels = sum(alpha.histogram()[:16])
|
| 1276 |
+
transparent_ratio = transparent_pixels / max(1, image.width * image.height)
|
| 1277 |
+
if transparent_ratio < 0.12:
|
| 1278 |
+
warnings.append("sprite background is not sufficiently transparent")
|
| 1279 |
+
corners = (
|
| 1280 |
+
alpha.getpixel((0, 0)),
|
| 1281 |
+
alpha.getpixel((image.width - 1, 0)),
|
| 1282 |
+
alpha.getpixel((0, image.height - 1)),
|
| 1283 |
+
alpha.getpixel((image.width - 1, image.height - 1)),
|
| 1284 |
+
)
|
| 1285 |
+
if any(value > 16 for value in corners):
|
| 1286 |
+
warnings.append("sprite has opaque corner pixels")
|
| 1287 |
+
return warnings
|
| 1288 |
|
| 1289 |
|
| 1290 |
def embed_assets(html_code: str, assets: dict[str, str], specs: list[AssetSpec]) -> str:
|
| 1291 |
+
report = analyze_integration(html_code, specs)
|
| 1292 |
+
if not report.supported:
|
| 1293 |
+
raise ValueError("Cannot embed assets without deterministic GAME_ASSETS hooks or role filenames")
|
| 1294 |
+
|
| 1295 |
output = html_code
|
| 1296 |
manifest_lines = ["<!-- Embedded game assets generated by Image Generator for HTML Games"]
|
|
|
|
| 1297 |
asset_map: dict[str, str] = {}
|
|
|
|
| 1298 |
|
| 1299 |
for spec in specs:
|
| 1300 |
data_uri = assets[spec.role]
|
| 1301 |
slug = slugify(spec.role)
|
| 1302 |
asset_map[slug] = data_uri
|
|
|
|
| 1303 |
manifest_lines.append(f"{spec.role}: {spec.filename}")
|
|
|
|
|
|
|
| 1304 |
for name in replacement_names(spec):
|
| 1305 |
output = output.replace(f'"{name}"', f'"{data_uri}"')
|
| 1306 |
output = output.replace(f"'{name}'", f"'{data_uri}'")
|
| 1307 |
+
if name.startswith("{"):
|
| 1308 |
+
output = output.replace(name, data_uri)
|
| 1309 |
|
| 1310 |
manifest_lines.append("-->")
|
| 1311 |
manifest = "\n".join(manifest_lines) + "\n"
|
| 1312 |
asset_json = json.dumps(asset_map)
|
|
|
|
|
|
|
| 1313 |
helper_script = f"""<script>
|
| 1314 |
(function () {{
|
| 1315 |
var ASSETS = {asset_json};
|
| 1316 |
+
window.GAME_ASSETS = Object.freeze(Object.assign({{}}, window.GAME_ASSETS || {{}}, ASSETS));
|
| 1317 |
window.GENERATED_GAME_ASSETS = ASSETS;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1318 |
}})();
|
| 1319 |
</script>"""
|
| 1320 |
|
| 1321 |
+
if re.search(r"<head(?:\s[^>]*)?>", output, flags=re.I):
|
| 1322 |
+
output = re.sub(
|
| 1323 |
+
r"(<head(?:\s[^>]*)?>)",
|
| 1324 |
+
lambda match: match.group(1) + "\n" + helper_script,
|
| 1325 |
+
output,
|
| 1326 |
+
count=1,
|
| 1327 |
+
flags=re.I,
|
| 1328 |
+
)
|
| 1329 |
elif "<body" in output:
|
| 1330 |
output = output.replace("<body", helper_script + "\n<body", 1)
|
| 1331 |
else:
|
|
|
|
| 1357 |
return f"prompt={', '.join(prompt_sources)}; image={', '.join(image_sources)}"
|
| 1358 |
|
| 1359 |
|
| 1360 |
+
def build_quality_report(specs: list[AssetSpec], quality: dict[str, list[str]]) -> str:
|
| 1361 |
+
lines = []
|
| 1362 |
+
for spec in specs:
|
| 1363 |
+
warnings = quality.get(spec.role, [])
|
| 1364 |
+
lines.append(f"{spec.role}: {'PASS' if not warnings else 'WARNING — ' + '; '.join(warnings)}")
|
| 1365 |
+
lines.append("Semantic subject/style accuracy still requires visual approval by a human.")
|
| 1366 |
+
return "\n".join(lines)
|
| 1367 |
+
|
| 1368 |
+
|
| 1369 |
+
def render_generation_state(state: dict, action: str, errors: list[str] | None = None):
|
| 1370 |
+
html_code = state["html_code"]
|
| 1371 |
+
specs = state["specs"]
|
| 1372 |
+
assets = state["assets"]
|
| 1373 |
+
integration = analyze_integration(html_code, specs)
|
| 1374 |
+
model_rows = [
|
| 1375 |
+
(spec.role, state["prompt_model"], state["image_models"][spec.role])
|
| 1376 |
+
for spec in specs
|
| 1377 |
+
]
|
| 1378 |
+
if integration.supported:
|
| 1379 |
+
rewritten = embed_assets(html_code, assets, specs)
|
| 1380 |
+
preview_html = build_preview(rewritten)
|
| 1381 |
+
status = (
|
| 1382 |
+
f"{action} {len(specs)} asset(s) and embedded them through the deterministic "
|
| 1383 |
+
f"GAME_ASSETS contract using {summarize_model_sources(model_rows)}."
|
| 1384 |
+
)
|
| 1385 |
+
else:
|
| 1386 |
+
rewritten = ""
|
| 1387 |
+
preview_html = build_preview(html_code)
|
| 1388 |
+
status = (
|
| 1389 |
+
f"{action} {len(specs)} asset(s), but did not rewrite the game because deterministic "
|
| 1390 |
+
"asset hooks are missing. The preview below is the unchanged original game."
|
| 1391 |
+
)
|
| 1392 |
+
if integration.warnings:
|
| 1393 |
+
status += "\n\n" + "\n".join(f"- {warning}" for warning in integration.warnings)
|
| 1394 |
+
if errors:
|
| 1395 |
+
status += "\n\n" + "\n".join(f"- {error}" for error in errors)
|
| 1396 |
+
gallery = [
|
| 1397 |
+
(state["gallery_paths"][spec.role], f"{spec.role} -> {spec.filename}")
|
| 1398 |
+
for spec in specs
|
| 1399 |
+
]
|
| 1400 |
+
return (
|
| 1401 |
+
rewritten,
|
| 1402 |
+
status,
|
| 1403 |
+
gallery,
|
| 1404 |
+
build_prompt_preview(specs),
|
| 1405 |
+
build_model_report(model_rows),
|
| 1406 |
+
build_quality_report(specs, state["quality"]),
|
| 1407 |
+
preview_html,
|
| 1408 |
+
)
|
| 1409 |
+
|
| 1410 |
+
|
| 1411 |
+
def empty_generation_result(message: str, html_code: str = ""):
|
| 1412 |
+
return (
|
| 1413 |
+
"",
|
| 1414 |
+
message,
|
| 1415 |
+
[],
|
| 1416 |
+
"",
|
| 1417 |
+
"",
|
| 1418 |
+
"",
|
| 1419 |
+
build_preview(html_code) if html_code.strip() else "",
|
| 1420 |
+
{},
|
| 1421 |
+
gr.Dropdown(choices=[], value=None),
|
| 1422 |
+
gr.CheckboxGroup(choices=[], value=[]),
|
| 1423 |
+
)
|
| 1424 |
+
|
| 1425 |
+
|
| 1426 |
+
def generate_images_and_game(
|
| 1427 |
+
html_code: str,
|
| 1428 |
+
roles: str,
|
| 1429 |
+
game_type: str,
|
| 1430 |
+
perspective: str,
|
| 1431 |
+
theme: str,
|
| 1432 |
+
):
|
| 1433 |
if not html_code.strip():
|
| 1434 |
+
return empty_generation_result("Paste HTML game code first.")
|
| 1435 |
|
| 1436 |
+
style_context = build_style_context(game_type, perspective, theme)
|
| 1437 |
+
role_lines, prompt_map, prompt_model, prompt_error = build_prompt_map(html_code, roles, style_context)
|
| 1438 |
+
specs = parse_assets(roles, style_context, prompt_map)
|
| 1439 |
if not specs:
|
| 1440 |
+
return empty_generation_result(
|
| 1441 |
+
"Add at least one asset role, like `player: brave knight`.",
|
| 1442 |
+
html_code,
|
| 1443 |
+
)
|
| 1444 |
+
slugs = [slugify(spec.role) for spec in specs]
|
| 1445 |
+
if len(slugs) != len(set(slugs)):
|
| 1446 |
+
return empty_generation_result(
|
| 1447 |
+
"Each asset role must have a unique name after normalization.",
|
| 1448 |
+
html_code,
|
| 1449 |
+
)
|
| 1450 |
|
| 1451 |
assets: dict[str, str] = {}
|
| 1452 |
+
gallery_paths: dict[str, str] = {}
|
| 1453 |
+
image_models: dict[str, str] = {}
|
| 1454 |
+
quality: dict[str, list[str]] = {}
|
| 1455 |
errors = []
|
|
|
|
| 1456 |
run_id = time.time_ns()
|
| 1457 |
if prompt_error:
|
| 1458 |
errors.append(f"prompt model: {prompt_error}")
|
|
|
|
| 1460 |
for index, spec in enumerate(specs):
|
| 1461 |
data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
|
| 1462 |
assets[spec.role] = data_uri
|
| 1463 |
+
gallery_paths[spec.role] = gallery_path
|
| 1464 |
+
image_models[spec.role] = image_model
|
| 1465 |
+
png_content = base64.b64decode(data_uri.split(",", 1)[1])
|
| 1466 |
+
quality[spec.role] = validate_asset_png(png_content, spec)
|
| 1467 |
if error:
|
| 1468 |
errors.append(f"{spec.role}: image model failed ({error}); used local procedural fallback")
|
| 1469 |
|
| 1470 |
+
state = {
|
| 1471 |
+
"html_code": html_code,
|
| 1472 |
+
"specs": specs,
|
| 1473 |
+
"assets": assets,
|
| 1474 |
+
"gallery_paths": gallery_paths,
|
| 1475 |
+
"prompt_model": prompt_model,
|
| 1476 |
+
"image_models": image_models,
|
| 1477 |
+
"quality": quality,
|
| 1478 |
+
"run_id": run_id,
|
| 1479 |
+
}
|
| 1480 |
+
rendered = render_generation_state(state, "Generated", errors)
|
| 1481 |
+
role_choices = [spec.role for spec in specs]
|
| 1482 |
+
return (
|
| 1483 |
+
*rendered,
|
| 1484 |
+
state,
|
| 1485 |
+
gr.Dropdown(choices=role_choices, value=role_choices[0]),
|
| 1486 |
+
gr.CheckboxGroup(choices=role_choices, value=[]),
|
| 1487 |
+
)
|
| 1488 |
+
|
| 1489 |
+
|
| 1490 |
+
def regenerate_selected_asset(state: dict, selected_role: str, approved_roles: list[str]):
|
| 1491 |
+
if not state or not selected_role:
|
| 1492 |
+
return (
|
| 1493 |
+
"",
|
| 1494 |
+
"Generate the initial asset set before regenerating a role.",
|
| 1495 |
+
[],
|
| 1496 |
+
"",
|
| 1497 |
+
"",
|
| 1498 |
+
"",
|
| 1499 |
+
"",
|
| 1500 |
+
state or {},
|
| 1501 |
+
gr.CheckboxGroup(choices=[], value=[]),
|
| 1502 |
+
)
|
| 1503 |
+
specs = state["specs"]
|
| 1504 |
+
index = next((i for i, spec in enumerate(specs) if spec.role == selected_role), None)
|
| 1505 |
+
if index is None:
|
| 1506 |
+
rendered = render_generation_state(state, "Kept")
|
| 1507 |
+
roles = [spec.role for spec in specs]
|
| 1508 |
+
return (*rendered, state, gr.CheckboxGroup(choices=roles, value=approved_roles or []))
|
| 1509 |
+
|
| 1510 |
+
spec = specs[index]
|
| 1511 |
+
run_id = time.time_ns()
|
| 1512 |
+
data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
|
| 1513 |
+
state["assets"][selected_role] = data_uri
|
| 1514 |
+
state["gallery_paths"][selected_role] = gallery_path
|
| 1515 |
+
state["image_models"][selected_role] = image_model
|
| 1516 |
+
png_content = base64.b64decode(data_uri.split(",", 1)[1])
|
| 1517 |
+
state["quality"][selected_role] = validate_asset_png(png_content, spec)
|
| 1518 |
+
state["run_id"] = run_id
|
| 1519 |
+
errors = [f"{selected_role}: {error}"] if error else []
|
| 1520 |
+
rendered = render_generation_state(state, f"Regenerated {selected_role}; retained", errors)
|
| 1521 |
+
roles = [item.role for item in specs]
|
| 1522 |
+
retained_approvals = [role for role in (approved_roles or []) if role != selected_role]
|
| 1523 |
+
return (
|
| 1524 |
+
*rendered,
|
| 1525 |
+
state,
|
| 1526 |
+
gr.CheckboxGroup(choices=roles, value=retained_approvals),
|
| 1527 |
)
|
|
|
|
|
|
|
|
|
|
| 1528 |
|
| 1529 |
|
| 1530 |
def check_hf_token() -> str:
|
|
|
|
| 1549 |
with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
| 1550 |
gr.Markdown(
|
| 1551 |
"# Image Generator for HTML Games\n"
|
| 1552 |
+
"Generate game assets and embed them only when the submitted game exposes deterministic "
|
| 1553 |
+
"`GAME_ASSETS.<role>` hooks or exact role filenames such as `sprite_player.png`. "
|
| 1554 |
+
"Primitive canvas geometry is preserved and never silently guessed."
|
| 1555 |
)
|
| 1556 |
|
| 1557 |
+
generation_state = gr.State({})
|
| 1558 |
+
|
| 1559 |
with gr.Row():
|
| 1560 |
with gr.Column(scale=1):
|
| 1561 |
roles = gr.Textbox(
|
| 1562 |
label="Image roles to generate",
|
| 1563 |
lines=8,
|
| 1564 |
placeholder=ROLE_PLACEHOLDER,
|
| 1565 |
+
value=DEFAULT_ROLES,
|
| 1566 |
info="One per line: role: image description. Example: player: blue robot hero",
|
| 1567 |
)
|
| 1568 |
+
game_type = gr.Dropdown(
|
| 1569 |
+
label="Game type",
|
| 1570 |
+
choices=[
|
| 1571 |
+
"Top-down action / RPG",
|
| 1572 |
+
"Platformer / side-scroller",
|
| 1573 |
+
"Isometric strategy / simulation",
|
| 1574 |
+
"First-person",
|
| 1575 |
+
"Card / board game",
|
| 1576 |
+
"Other / custom",
|
| 1577 |
+
],
|
| 1578 |
+
value="Top-down action / RPG",
|
| 1579 |
+
)
|
| 1580 |
+
perspective = gr.Dropdown(
|
| 1581 |
+
label="Camera perspective",
|
| 1582 |
+
choices=[
|
| 1583 |
+
"Top-down / overhead",
|
| 1584 |
+
"Side view",
|
| 1585 |
+
"Isometric 3/4 view",
|
| 1586 |
+
"First-person",
|
| 1587 |
+
"Front-facing",
|
| 1588 |
+
"Auto-detect from code",
|
| 1589 |
+
],
|
| 1590 |
+
value="Top-down / overhead",
|
| 1591 |
+
)
|
| 1592 |
+
theme = gr.Textbox(
|
| 1593 |
+
label="Theme and shared visual style",
|
| 1594 |
lines=2,
|
| 1595 |
+
placeholder="Describe the setting, palette, medium, mood, and constraints.",
|
| 1596 |
)
|
| 1597 |
generate_btn = gr.Button("Generate Images + Embed Game", variant="primary")
|
| 1598 |
status = gr.Markdown("Ready.")
|
| 1599 |
token_btn = gr.Button("Check HF Token")
|
| 1600 |
token_status = gr.Markdown("")
|
| 1601 |
gallery = gr.Gallery(label="Generated assets", columns=2, height=300)
|
| 1602 |
+
selected_role = gr.Dropdown(
|
| 1603 |
+
label="Asset to regenerate",
|
| 1604 |
+
choices=[],
|
| 1605 |
+
interactive=True,
|
| 1606 |
+
)
|
| 1607 |
+
regenerate_btn = gr.Button("Regenerate selected asset")
|
| 1608 |
+
approved_roles = gr.CheckboxGroup(
|
| 1609 |
+
label="Visually approved assets",
|
| 1610 |
+
choices=[],
|
| 1611 |
+
info="Approve assets only after checking subject, perspective, style, and transparency.",
|
| 1612 |
+
)
|
| 1613 |
|
| 1614 |
with gr.Column(scale=2):
|
| 1615 |
html_input = gr.Textbox(
|
| 1616 |
label="Original HTML game code",
|
| 1617 |
lines=18,
|
| 1618 |
placeholder="Paste your full HTML game code here.",
|
| 1619 |
+
value=STARTER_HTML,
|
| 1620 |
)
|
| 1621 |
output_code = gr.Code(
|
| 1622 |
label="Rewritten HTML with embedded images",
|
|
|
|
| 1633 |
lines=5,
|
| 1634 |
interactive=False,
|
| 1635 |
)
|
| 1636 |
+
quality_report = gr.Textbox(
|
| 1637 |
+
label="Automated asset validation",
|
| 1638 |
+
lines=6,
|
| 1639 |
+
interactive=False,
|
| 1640 |
+
)
|
| 1641 |
|
| 1642 |
gr.Markdown("## Game preview")
|
| 1643 |
preview = gr.HTML("")
|
| 1644 |
|
| 1645 |
generate_btn.click(
|
| 1646 |
fn=generate_images_and_game,
|
| 1647 |
+
inputs=[html_input, roles, game_type, perspective, theme],
|
| 1648 |
+
outputs=[
|
| 1649 |
+
output_code,
|
| 1650 |
+
status,
|
| 1651 |
+
gallery,
|
| 1652 |
+
prompt_preview,
|
| 1653 |
+
model_report,
|
| 1654 |
+
quality_report,
|
| 1655 |
+
preview,
|
| 1656 |
+
generation_state,
|
| 1657 |
+
selected_role,
|
| 1658 |
+
approved_roles,
|
| 1659 |
+
],
|
| 1660 |
+
)
|
| 1661 |
+
regenerate_btn.click(
|
| 1662 |
+
fn=regenerate_selected_asset,
|
| 1663 |
+
inputs=[generation_state, selected_role, approved_roles],
|
| 1664 |
+
outputs=[
|
| 1665 |
+
output_code,
|
| 1666 |
+
status,
|
| 1667 |
+
gallery,
|
| 1668 |
+
prompt_preview,
|
| 1669 |
+
model_report,
|
| 1670 |
+
quality_report,
|
| 1671 |
+
preview,
|
| 1672 |
+
generation_state,
|
| 1673 |
+
approved_roles,
|
| 1674 |
+
],
|
| 1675 |
)
|
| 1676 |
token_btn.click(fn=check_hf_token, inputs=None, outputs=token_status)
|
| 1677 |
|