Spaces:
Running on Zero
Running on Zero
Generate separate roles and reject multi-view sprite sheets
Browse files
README.md
CHANGED
|
@@ -29,7 +29,20 @@ The generator injects an immutable `window.GAME_ASSETS` manifest whose keys are
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
The UI reports the configured prompt and image pipeline and labels every role with its actual source. Every asset carries a normalized camera contract (`top_down`, `side_view`, `isometric`, `first_person`, or `front_view`). Top-down background prompts explicitly forbid a sky, horizon, eye-level view, and front-facing facades.
|
| 35 |
|
|
@@ -49,7 +62,7 @@ segmind/SSD-1B + latent-consistency/lcm-lora-ssd-1b
|
|
| 49 |
|
| 50 |
SSD-1B is an Apache-2.0 distilled SDXL model. The LCM adapter enables low-step generation. The weights run inside the Space's ZeroGPU allocation, so the app does not consume Hugging Face Inference Provider credits and does not require a paid image API.
|
| 51 |
|
| 52 |
-
Sprites and backgrounds are generated directly from their written prompts. No procedurally drawn sprite, palette, silhouette, camera guide, or background layout is passed into the production model.
|
| 53 |
|
| 54 |
The deployed Space is fail-closed: if the primary neural model cannot run, generation stops with a visible error instead of silently returning procedural art and calling it model output. The procedural renderer remains available for local development and diagnostics only and cannot influence successful public generations.
|
| 55 |
|
|
@@ -64,6 +77,7 @@ PRIMARY_IMAGE_MODEL=segmind/SSD-1B
|
|
| 64 |
PRIMARY_LORA_MODEL=latent-consistency/lcm-lora-ssd-1b
|
| 65 |
PRIMARY_IMAGE_STEPS=4
|
| 66 |
PRIMARY_SPRITE_STEPS=6
|
|
|
|
| 67 |
PRIMARY_GUIDANCE_SCALE=1.5
|
| 68 |
```
|
| 69 |
|
|
|
|
| 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 |
+
## Multiple character sprites
|
| 33 |
+
|
| 34 |
+
Define each distinct character as its own role so the model produces a separate PNG and the game can address it independently:
|
| 35 |
+
|
| 36 |
+
```text
|
| 37 |
+
player: agile desert ranger with a leather bow
|
| 38 |
+
enemy_grunt: compact orange patrol robot
|
| 39 |
+
enemy_boss: towering red armored commander
|
| 40 |
+
npc_merchant: elderly traveling potion seller
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
The generator runs one neural image request per role rather than intentionally combining character roles into a contact sheet. The image model can still violate the one-subject instruction, so every result requires visual review. When the submitted HTML references additional deterministic hooks such as `GAME_ASSETS.enemy_grunt`, `GAME_ASSETS.enemy_boss`, or `sprite_npc_merchant.png`, missing roles are automatically added to the generation set. Explicit role descriptions remain preferable because code identifiers alone provide limited visual direction. Repeated instances of the same enemy should reuse one role, while visually distinct character types should use separate roles.
|
| 44 |
+
|
| 45 |
+
Generated sprites receive automated dimension, alpha-channel, corner-transparency, and significant-foreground-component checks. A multi-subject sprite is rejected and regenerated with a fresh model seed rather than cropped or procedurally rearranged. These checks do not establish semantic or artistic correctness, so every role also has a manual approval control and per-role regeneration.
|
| 46 |
|
| 47 |
The UI reports the configured prompt and image pipeline and labels every role with its actual source. Every asset carries a normalized camera contract (`top_down`, `side_view`, `isometric`, `first_person`, or `front_view`). Top-down background prompts explicitly forbid a sky, horizon, eye-level view, and front-facing facades.
|
| 48 |
|
|
|
|
| 62 |
|
| 63 |
SSD-1B is an Apache-2.0 distilled SDXL model. The LCM adapter enables low-step generation. The weights run inside the Space's ZeroGPU allocation, so the app does not consume Hugging Face Inference Provider credits and does not require a paid image API.
|
| 64 |
|
| 65 |
+
Sprites and backgrounds are generated directly from their written prompts. No procedurally drawn sprite, palette, silhouette, camera guide, or background layout is passed into the production model. Sprite prompts use a portrait canvas and positive single-subject composition language; the negative prompt separately rejects sheets, lineups, repeated subjects, and multiple views. After transparency extraction, sprites with zero or multiple significant foreground components are regenerated with a new seed up to `PRIMARY_SPRITE_ATTEMPTS` times. If every attempt fails, the deployed Space returns an explicit model-generation error rather than returning a contact sheet. Background negative prompts reject characters and generic particle overlays.
|
| 66 |
|
| 67 |
The deployed Space is fail-closed: if the primary neural model cannot run, generation stops with a visible error instead of silently returning procedural art and calling it model output. The procedural renderer remains available for local development and diagnostics only and cannot influence successful public generations.
|
| 68 |
|
|
|
|
| 77 |
PRIMARY_LORA_MODEL=latent-consistency/lcm-lora-ssd-1b
|
| 78 |
PRIMARY_IMAGE_STEPS=4
|
| 79 |
PRIMARY_SPRITE_STEPS=6
|
| 80 |
+
PRIMARY_SPRITE_ATTEMPTS=3
|
| 81 |
PRIMARY_GUIDANCE_SCALE=1.5
|
| 82 |
```
|
| 83 |
|
app.py
CHANGED
|
@@ -87,6 +87,8 @@ DEFAULT_ROLES = """player: top-down pixel-art adventurer hero, transparent backg
|
|
| 87 |
background: enchanted forest clearing game background, top-down view, soft moonlight, detailed but not too busy"""
|
| 88 |
|
| 89 |
ROLE_PLACEHOLDER = """player: blue robot hero sprite
|
|
|
|
|
|
|
| 90 |
background: empty space station floor map"""
|
| 91 |
|
| 92 |
|
|
@@ -125,6 +127,7 @@ PRIMARY_IMAGE_MODEL = os.environ.get("PRIMARY_IMAGE_MODEL", "segmind/SSD-1B")
|
|
| 125 |
PRIMARY_LORA_MODEL = os.environ.get("PRIMARY_LORA_MODEL", "latent-consistency/lcm-lora-ssd-1b")
|
| 126 |
PRIMARY_IMAGE_STEPS = int(os.environ.get("PRIMARY_IMAGE_STEPS", "4"))
|
| 127 |
PRIMARY_SPRITE_STEPS = int(os.environ.get("PRIMARY_SPRITE_STEPS", "6"))
|
|
|
|
| 128 |
PRIMARY_GUIDANCE_SCALE = float(os.environ.get("PRIMARY_GUIDANCE_SCALE", "1.5"))
|
| 129 |
USE_PRIMARY_IMAGE_MODEL = os.environ.get("USE_PRIMARY_IMAGE_MODEL", "1") == "1"
|
| 130 |
REQUIRE_PRIMARY_IMAGE_MODEL = os.environ.get(
|
|
@@ -324,10 +327,9 @@ def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
|
|
| 324 |
)
|
| 325 |
else:
|
| 326 |
asset_instruction = (
|
| 327 |
-
"Create
|
| 328 |
-
"
|
| 329 |
-
"
|
| 330 |
-
"not a tiled pattern, not a material swatch, not a UV unwrap, not a 3D model skin."
|
| 331 |
)
|
| 332 |
return (
|
| 333 |
f"{role} asset: {prompt}. Creative brief: {style_hint}. "
|
|
@@ -335,7 +337,7 @@ def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
|
|
| 335 |
f"{plan.texture}; {plan.lighting}; {plan.linework}; {plan.camera}. "
|
| 336 |
f"Camera contract: {camera_prompt_contract(camera, is_background)} "
|
| 337 |
f"{asset_instruction} "
|
| 338 |
-
"Game asset, readable at small size,
|
| 339 |
)
|
| 340 |
|
| 341 |
|
|
@@ -370,6 +372,52 @@ def parse_role_lines(raw_roles: str) -> list[tuple[str, str]]:
|
|
| 370 |
return parsed
|
| 371 |
|
| 372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
def infer_code_context(html_code: str) -> str:
|
| 374 |
text = html_code[:12000]
|
| 375 |
filenames = sorted(set(re.findall(r"['\"]([^'\"]+?\.(?:png|jpg|jpeg|webp|gif))['\"]", text, flags=re.I)))
|
|
@@ -428,7 +476,8 @@ def hf_prompt_json(html_code: str, role_lines: list[tuple[str, str]], style_hint
|
|
| 428 |
"each key is the exact role name and each value is one concise text-to-image prompt. "
|
| 429 |
"Each prompt must specify: subject silhouette/shape, camera angle, art style, palette, "
|
| 430 |
"transparent background for sprites/items, full scene for backgrounds, no text, no watermark. "
|
| 431 |
-
"Make different roles visually distinct and suitable for embedding in an HTML game."
|
|
|
|
| 432 |
)
|
| 433 |
user_text = (
|
| 434 |
f"HTML/game context summary: {infer_code_context(html_code)}\n\n"
|
|
@@ -481,18 +530,27 @@ def hf_prompt_json(html_code: str, role_lines: list[tuple[str, str]], style_hint
|
|
| 481 |
return None, short_error(exc)
|
| 482 |
|
| 483 |
|
| 484 |
-
def build_prompt_map(
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
local_map = local_prompt_map(role_lines, style_hint)
|
| 487 |
ai_map, prompt_error = hf_prompt_json(html_code, role_lines, style_hint)
|
| 488 |
if ai_map and all(role in ai_map for role, _ in role_lines):
|
| 489 |
-
return role_lines, ai_map, HF_PROMPT_MODEL, None
|
| 490 |
-
return role_lines, local_map, "local prompt interpreter", prompt_error
|
| 491 |
|
| 492 |
|
| 493 |
-
def parse_assets(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
specs: list[AssetSpec] = []
|
| 495 |
-
for role, prompt in parse_role_lines(raw_roles):
|
| 496 |
slug = slugify(role)
|
| 497 |
is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
|
| 498 |
width, height = (800, 450) if is_background else (128, 128)
|
|
@@ -1660,13 +1718,11 @@ def primary_diffusion_prompt(spec: AssetSpec) -> str:
|
|
| 1660 |
"Use connected environmental forms and clean terrain structure. No characters and no decorative overlay."
|
| 1661 |
)
|
| 1662 |
return (
|
| 1663 |
-
f"{spec.prompt}
|
| 1664 |
-
"
|
| 1665 |
-
"
|
| 1666 |
-
"the written description. "
|
| 1667 |
-
"
|
| 1668 |
-
"Add recognizable costume, equipment, materials, and character details. "
|
| 1669 |
-
"No scenery, floor, trees, foliage, frame, or environmental props."
|
| 1670 |
)
|
| 1671 |
|
| 1672 |
|
|
@@ -1682,22 +1738,36 @@ def primary_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[byt
|
|
| 1682 |
try:
|
| 1683 |
import torch
|
| 1684 |
|
| 1685 |
-
|
| 1686 |
-
|
| 1687 |
-
|
| 1688 |
-
|
| 1689 |
-
|
| 1690 |
-
|
| 1691 |
-
"
|
| 1692 |
-
|
| 1693 |
-
|
| 1694 |
-
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
-
|
| 1700 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1701 |
except Exception as exc:
|
| 1702 |
return None, short_error(exc)
|
| 1703 |
|
|
@@ -1761,7 +1831,20 @@ def polish_diffusion_asset(image: Image.Image, spec: AssetSpec) -> bytes:
|
|
| 1761 |
if not is_background_spec(spec):
|
| 1762 |
# Text-to-image models do not produce real transparency. This makes sprites
|
| 1763 |
# usable by fading out colors similar to the generated corner background.
|
| 1764 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1765 |
corners = [
|
| 1766 |
small.getpixel((0, 0)),
|
| 1767 |
small.getpixel((127, 0)),
|
|
@@ -1788,6 +1871,48 @@ def polish_diffusion_asset(image: Image.Image, spec: AssetSpec) -> bytes:
|
|
| 1788 |
return out.getvalue()
|
| 1789 |
|
| 1790 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1791 |
def free_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
|
| 1792 |
global FREE_DIFFUSION_PIPE, FREE_DIFFUSION_ERROR
|
| 1793 |
if FREE_DIFFUSION_ERROR:
|
|
@@ -2033,6 +2158,11 @@ def validate_asset_png(content: bytes, spec: AssetSpec) -> list[str]:
|
|
| 2033 |
)
|
| 2034 |
if any(value > 16 for value in corners):
|
| 2035 |
warnings.append("sprite has opaque corner pixels")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2036 |
return warnings
|
| 2037 |
|
| 2038 |
|
|
@@ -2171,8 +2301,9 @@ def model_configuration_summary() -> str:
|
|
| 2171 |
f"remote image fallback: `{remote_fallback}` 路 neural image models: `{neural_status}` 路 "
|
| 2172 |
f"neural enforcement: `{enforcement}` 路 primary readiness: `{readiness}`. "
|
| 2173 |
"Every production image is generated directly from its written prompt by the primary text-to-image model. "
|
| 2174 |
-
"
|
| 2175 |
-
"
|
|
|
|
| 2176 |
)
|
| 2177 |
|
| 2178 |
|
|
@@ -2216,6 +2347,13 @@ def render_generation_state(state: dict, action: str, errors: list[str] | None =
|
|
| 2216 |
)
|
| 2217 |
if integration.warnings:
|
| 2218 |
status += "\n\n" + "\n".join(f"- {warning}" for warning in integration.warnings)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2219 |
if errors:
|
| 2220 |
status += "\n\n" + "\n".join(f"- {error}" for error in errors)
|
| 2221 |
gallery = [
|
|
@@ -2260,8 +2398,12 @@ def generate_images_and_game(
|
|
| 2260 |
return empty_generation_result("Paste HTML game code first.")
|
| 2261 |
|
| 2262 |
style_context = build_style_context(game_type, perspective, theme)
|
| 2263 |
-
role_lines, prompt_map, prompt_model, prompt_error = build_prompt_map(
|
| 2264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2265 |
if not specs:
|
| 2266 |
return empty_generation_result(
|
| 2267 |
"Add at least one asset role, like `player: brave knight`.",
|
|
@@ -2304,6 +2446,7 @@ def generate_images_and_game(
|
|
| 2304 |
"prompt_model": prompt_model,
|
| 2305 |
"image_models": image_models,
|
| 2306 |
"quality": quality,
|
|
|
|
| 2307 |
"run_id": run_id,
|
| 2308 |
}
|
| 2309 |
rendered = render_generation_state(state, "Generated", errors)
|
|
@@ -2539,7 +2682,7 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 2539 |
<h1>Turn your game code into a visual world.</h1>
|
| 2540 |
<p>Describe the art direction, generate camera-aware assets, and embed them into deterministic image hooks鈥攚ithout rewriting your game logic.</p>
|
| 2541 |
<div class="hero-chips">
|
| 2542 |
-
<span>
|
| 2543 |
</div>
|
| 2544 |
</header>
|
| 2545 |
"""
|
|
@@ -2566,7 +2709,10 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 2566 |
lines=8,
|
| 2567 |
placeholder=ROLE_PLACEHOLDER,
|
| 2568 |
value=DEFAULT_ROLES,
|
| 2569 |
-
info=
|
|
|
|
|
|
|
|
|
|
| 2570 |
)
|
| 2571 |
with gr.Row():
|
| 2572 |
game_type = gr.Dropdown(
|
|
|
|
| 87 |
background: enchanted forest clearing game background, top-down view, soft moonlight, detailed but not too busy"""
|
| 88 |
|
| 89 |
ROLE_PLACEHOLDER = """player: blue robot hero sprite
|
| 90 |
+
enemy_grunt: compact orange patrol robot
|
| 91 |
+
enemy_boss: towering red armored commander
|
| 92 |
background: empty space station floor map"""
|
| 93 |
|
| 94 |
|
|
|
|
| 127 |
PRIMARY_LORA_MODEL = os.environ.get("PRIMARY_LORA_MODEL", "latent-consistency/lcm-lora-ssd-1b")
|
| 128 |
PRIMARY_IMAGE_STEPS = int(os.environ.get("PRIMARY_IMAGE_STEPS", "4"))
|
| 129 |
PRIMARY_SPRITE_STEPS = int(os.environ.get("PRIMARY_SPRITE_STEPS", "6"))
|
| 130 |
+
PRIMARY_SPRITE_ATTEMPTS = max(1, int(os.environ.get("PRIMARY_SPRITE_ATTEMPTS", "3")))
|
| 131 |
PRIMARY_GUIDANCE_SCALE = float(os.environ.get("PRIMARY_GUIDANCE_SCALE", "1.5"))
|
| 132 |
USE_PRIMARY_IMAGE_MODEL = os.environ.get("USE_PRIMARY_IMAGE_MODEL", "1") == "1"
|
| 133 |
REQUIRE_PRIMARY_IMAGE_MODEL = os.environ.get(
|
|
|
|
| 327 |
)
|
| 328 |
else:
|
| 329 |
asset_instruction = (
|
| 330 |
+
"Create a complete standalone 2D game character or object as a single centered subject. "
|
| 331 |
+
"Present one complete body in one pose from the required camera direction, fully visible with broad empty "
|
| 332 |
+
"margin on every side. Use a clean uniform white studio field and a readable game-scale silhouette."
|
|
|
|
| 333 |
)
|
| 334 |
return (
|
| 335 |
f"{role} asset: {prompt}. Creative brief: {style_hint}. "
|
|
|
|
| 337 |
f"{plan.texture}; {plan.lighting}; {plan.linework}; {plan.camera}. "
|
| 338 |
f"Camera contract: {camera_prompt_contract(camera, is_background)} "
|
| 339 |
f"{asset_instruction} "
|
| 340 |
+
"Game asset, readable at small size, with a clean unlabeled presentation."
|
| 341 |
)
|
| 342 |
|
| 343 |
|
|
|
|
| 372 |
return parsed
|
| 373 |
|
| 374 |
|
| 375 |
+
def extract_code_asset_roles(html_code: str) -> list[str]:
|
| 376 |
+
"""Return deterministic GAME_ASSETS and sprite filename roles in source order."""
|
| 377 |
+
matches: list[tuple[int, str]] = []
|
| 378 |
+
patterns = (
|
| 379 |
+
r"(?:window\.)?GAME_ASSETS\s*\.\s*([a-zA-Z_$][\w$]*)",
|
| 380 |
+
r"(?:window\.)?GAME_ASSETS\s*\[\s*['\"]([a-zA-Z0-9_-]+)['\"]\s*\]",
|
| 381 |
+
r"['\"]sprite_([a-zA-Z0-9_-]+)\.(?:png|jpg|jpeg|webp|gif)['\"]",
|
| 382 |
+
)
|
| 383 |
+
for pattern in patterns:
|
| 384 |
+
for match in re.finditer(pattern, html_code, flags=re.I):
|
| 385 |
+
matches.append((match.start(), slugify(match.group(1))))
|
| 386 |
+
|
| 387 |
+
roles: list[str] = []
|
| 388 |
+
seen: set[str] = set()
|
| 389 |
+
for _, role in sorted(matches, key=lambda item: item[0]):
|
| 390 |
+
if role not in seen:
|
| 391 |
+
seen.add(role)
|
| 392 |
+
roles.append(role)
|
| 393 |
+
return roles
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def inferred_role_prompt(role: str) -> str:
|
| 397 |
+
"""Create a conservative prompt when code exposes a role without an art brief."""
|
| 398 |
+
slug = slugify(role)
|
| 399 |
+
label = slug.replace("_", " ")
|
| 400 |
+
if any(word in slug for word in ("background", "backdrop", "scene", "map", "level")):
|
| 401 |
+
return f"complete {label} game environment"
|
| 402 |
+
if any(word in slug for word in ("player", "hero", "enemy", "boss", "npc", "character", "ally", "companion")):
|
| 403 |
+
return f"distinct full-body {label} character"
|
| 404 |
+
return f"distinct {label} game asset"
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def resolve_role_lines(html_code: str, raw_roles: str) -> tuple[list[tuple[str, str]], list[str]]:
|
| 408 |
+
"""Merge user-authored roles with missing deterministic roles found in game code."""
|
| 409 |
+
role_lines = parse_role_lines(raw_roles)
|
| 410 |
+
seen = {slugify(role) for role, _ in role_lines}
|
| 411 |
+
inferred: list[str] = []
|
| 412 |
+
for role in extract_code_asset_roles(html_code):
|
| 413 |
+
if role in seen:
|
| 414 |
+
continue
|
| 415 |
+
seen.add(role)
|
| 416 |
+
inferred.append(role)
|
| 417 |
+
role_lines.append((role, inferred_role_prompt(role)))
|
| 418 |
+
return role_lines, inferred
|
| 419 |
+
|
| 420 |
+
|
| 421 |
def infer_code_context(html_code: str) -> str:
|
| 422 |
text = html_code[:12000]
|
| 423 |
filenames = sorted(set(re.findall(r"['\"]([^'\"]+?\.(?:png|jpg|jpeg|webp|gif))['\"]", text, flags=re.I)))
|
|
|
|
| 476 |
"each key is the exact role name and each value is one concise text-to-image prompt. "
|
| 477 |
"Each prompt must specify: subject silhouette/shape, camera angle, art style, palette, "
|
| 478 |
"transparent background for sprites/items, full scene for backgrounds, no text, no watermark. "
|
| 479 |
+
"Make different roles visually distinct and suitable for embedding in an HTML game. "
|
| 480 |
+
"Describe only one role per prompt; never combine multiple character roles into one image."
|
| 481 |
)
|
| 482 |
user_text = (
|
| 483 |
f"HTML/game context summary: {infer_code_context(html_code)}\n\n"
|
|
|
|
| 530 |
return None, short_error(exc)
|
| 531 |
|
| 532 |
|
| 533 |
+
def build_prompt_map(
|
| 534 |
+
html_code: str,
|
| 535 |
+
raw_roles: str,
|
| 536 |
+
style_hint: str,
|
| 537 |
+
) -> tuple[list[tuple[str, str]], dict[str, str], str, str | None, list[str]]:
|
| 538 |
+
role_lines, inferred_roles = resolve_role_lines(html_code, raw_roles)
|
| 539 |
local_map = local_prompt_map(role_lines, style_hint)
|
| 540 |
ai_map, prompt_error = hf_prompt_json(html_code, role_lines, style_hint)
|
| 541 |
if ai_map and all(role in ai_map for role, _ in role_lines):
|
| 542 |
+
return role_lines, ai_map, HF_PROMPT_MODEL, None, inferred_roles
|
| 543 |
+
return role_lines, local_map, "local prompt interpreter", prompt_error, inferred_roles
|
| 544 |
|
| 545 |
|
| 546 |
+
def parse_assets(
|
| 547 |
+
raw_roles: str,
|
| 548 |
+
style_hint: str,
|
| 549 |
+
prompt_map: dict[str, str] | None = None,
|
| 550 |
+
role_lines: list[tuple[str, str]] | None = None,
|
| 551 |
+
) -> list[AssetSpec]:
|
| 552 |
specs: list[AssetSpec] = []
|
| 553 |
+
for role, prompt in role_lines if role_lines is not None else parse_role_lines(raw_roles):
|
| 554 |
slug = slugify(role)
|
| 555 |
is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
|
| 556 |
width, height = (800, 450) if is_background else (128, 128)
|
|
|
|
| 1718 |
"Use connected environmental forms and clean terrain structure. No characters and no decorative overlay."
|
| 1719 |
)
|
| 1720 |
return (
|
| 1721 |
+
f"{spec.prompt} Production-ready isolated 2D game asset. A single centered subject fills roughly seventy "
|
| 1722 |
+
"percent of a portrait frame. One complete body, one pose, and one strict camera direction are fully visible "
|
| 1723 |
+
"with broad empty margin on every side. Derive the anatomy, body shape, pose, costume, equipment, materials, "
|
| 1724 |
+
"and recognizable details directly from the written description. Use a plain uniform white studio field, a "
|
| 1725 |
+
"readable silhouette, and a clean unlabeled presentation."
|
|
|
|
|
|
|
| 1726 |
)
|
| 1727 |
|
| 1728 |
|
|
|
|
| 1738 |
try:
|
| 1739 |
import torch
|
| 1740 |
|
| 1741 |
+
is_background = is_background_spec(spec)
|
| 1742 |
+
attempts = 1 if is_background else PRIMARY_SPRITE_ATTEMPTS
|
| 1743 |
+
width, height = (1344, 768) if is_background else (768, 1024)
|
| 1744 |
+
last_component_count = 0
|
| 1745 |
+
for attempt in range(attempts):
|
| 1746 |
+
seed = abs(hash(f"primary|{spec.role}|{spec.prompt}|{index}|{run_id}|{attempt}")) % 2147483647
|
| 1747 |
+
generator = torch.Generator(device="cuda").manual_seed(seed)
|
| 1748 |
+
image = PRIMARY_TEXT_PIPE(
|
| 1749 |
+
prompt=primary_diffusion_prompt(spec),
|
| 1750 |
+
negative_prompt=diffusion_negative_prompt(spec),
|
| 1751 |
+
num_inference_steps=PRIMARY_IMAGE_STEPS if is_background else PRIMARY_SPRITE_STEPS,
|
| 1752 |
+
guidance_scale=PRIMARY_GUIDANCE_SCALE,
|
| 1753 |
+
generator=generator,
|
| 1754 |
+
num_images_per_prompt=1,
|
| 1755 |
+
width=width,
|
| 1756 |
+
height=height,
|
| 1757 |
+
).images[0]
|
| 1758 |
+
content = polish_diffusion_asset(image, spec)
|
| 1759 |
+
if is_background:
|
| 1760 |
+
return content, None
|
| 1761 |
+
|
| 1762 |
+
last_component_count = len(significant_foreground_component_areas(content))
|
| 1763 |
+
if last_component_count == 1:
|
| 1764 |
+
return content, None
|
| 1765 |
+
|
| 1766 |
+
if last_component_count > 1:
|
| 1767 |
+
detail = f"the last output contained {last_component_count} significant foreground subjects"
|
| 1768 |
+
else:
|
| 1769 |
+
detail = "the last output did not contain one significant foreground subject"
|
| 1770 |
+
return None, f"single-subject validation failed after {attempts} model attempts; {detail}"
|
| 1771 |
except Exception as exc:
|
| 1772 |
return None, short_error(exc)
|
| 1773 |
|
|
|
|
| 1831 |
if not is_background_spec(spec):
|
| 1832 |
# Text-to-image models do not produce real transparency. This makes sprites
|
| 1833 |
# usable by fading out colors similar to the generated corner background.
|
| 1834 |
+
# Preserve the model's portrait composition instead of stretching it square.
|
| 1835 |
+
corner_source = image.resize((64, 64), Image.LANCZOS)
|
| 1836 |
+
source_corners = [
|
| 1837 |
+
corner_source.getpixel((0, 0)),
|
| 1838 |
+
corner_source.getpixel((63, 0)),
|
| 1839 |
+
corner_source.getpixel((0, 63)),
|
| 1840 |
+
corner_source.getpixel((63, 63)),
|
| 1841 |
+
]
|
| 1842 |
+
source_bg = tuple(sum(pixel[i] for pixel in source_corners) // len(source_corners) for i in range(3))
|
| 1843 |
+
contained = image.copy()
|
| 1844 |
+
contained.thumbnail((128, 128), Image.LANCZOS)
|
| 1845 |
+
small = Image.new("RGBA", (128, 128), (*source_bg, 255))
|
| 1846 |
+
offset = ((128 - contained.width) // 2, (128 - contained.height) // 2)
|
| 1847 |
+
small.alpha_composite(contained, dest=offset)
|
| 1848 |
corners = [
|
| 1849 |
small.getpixel((0, 0)),
|
| 1850 |
small.getpixel((127, 0)),
|
|
|
|
| 1871 |
return out.getvalue()
|
| 1872 |
|
| 1873 |
|
| 1874 |
+
def significant_foreground_component_areas(content: bytes) -> list[int]:
|
| 1875 |
+
"""Return material alpha components, ignoring small detached details and cleanup noise."""
|
| 1876 |
+
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
| 1877 |
+
width, height = image.size
|
| 1878 |
+
alpha = image.getchannel("A").tobytes()
|
| 1879 |
+
foreground = bytearray(1 if value >= 64 else 0 for value in alpha)
|
| 1880 |
+
areas: list[int] = []
|
| 1881 |
+
|
| 1882 |
+
for start in range(width * height):
|
| 1883 |
+
if not foreground[start]:
|
| 1884 |
+
continue
|
| 1885 |
+
foreground[start] = 0
|
| 1886 |
+
stack = [start]
|
| 1887 |
+
area = 0
|
| 1888 |
+
while stack:
|
| 1889 |
+
current = stack.pop()
|
| 1890 |
+
area += 1
|
| 1891 |
+
x = current % width
|
| 1892 |
+
y = current // width
|
| 1893 |
+
for dy in (-1, 0, 1):
|
| 1894 |
+
ny = y + dy
|
| 1895 |
+
if ny < 0 or ny >= height:
|
| 1896 |
+
continue
|
| 1897 |
+
for dx in (-1, 0, 1):
|
| 1898 |
+
if dx == 0 and dy == 0:
|
| 1899 |
+
continue
|
| 1900 |
+
nx = x + dx
|
| 1901 |
+
if nx < 0 or nx >= width:
|
| 1902 |
+
continue
|
| 1903 |
+
neighbor = ny * width + nx
|
| 1904 |
+
if foreground[neighbor]:
|
| 1905 |
+
foreground[neighbor] = 0
|
| 1906 |
+
stack.append(neighbor)
|
| 1907 |
+
areas.append(area)
|
| 1908 |
+
|
| 1909 |
+
if not areas:
|
| 1910 |
+
return []
|
| 1911 |
+
largest = max(areas)
|
| 1912 |
+
minimum = max(64, int(largest * 0.22), int(width * height * 0.005))
|
| 1913 |
+
return sorted((area for area in areas if area >= minimum), reverse=True)
|
| 1914 |
+
|
| 1915 |
+
|
| 1916 |
def free_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
|
| 1917 |
global FREE_DIFFUSION_PIPE, FREE_DIFFUSION_ERROR
|
| 1918 |
if FREE_DIFFUSION_ERROR:
|
|
|
|
| 2158 |
)
|
| 2159 |
if any(value > 16 for value in corners):
|
| 2160 |
warnings.append("sprite has opaque corner pixels")
|
| 2161 |
+
component_areas = significant_foreground_component_areas(content)
|
| 2162 |
+
if not component_areas:
|
| 2163 |
+
warnings.append("sprite has no significant foreground subject")
|
| 2164 |
+
elif len(component_areas) > 1:
|
| 2165 |
+
warnings.append(f"sprite contains {len(component_areas)} significant foreground subjects")
|
| 2166 |
return warnings
|
| 2167 |
|
| 2168 |
|
|
|
|
| 2301 |
f"remote image fallback: `{remote_fallback}` 路 neural image models: `{neural_status}` 路 "
|
| 2302 |
f"neural enforcement: `{enforcement}` 路 primary readiness: `{readiness}`. "
|
| 2303 |
"Every production image is generated directly from its written prompt by the primary text-to-image model. "
|
| 2304 |
+
"Sprite outputs must pass single-subject alpha-component validation and are regenerated with a new seed up to "
|
| 2305 |
+
f"{PRIMARY_SPRITE_ATTEMPTS} times. No procedural guide is supplied to the model. The procedural renderer is "
|
| 2306 |
+
"development-only and is blocked in the deployed Space when the primary model fails."
|
| 2307 |
)
|
| 2308 |
|
| 2309 |
|
|
|
|
| 2347 |
)
|
| 2348 |
if integration.warnings:
|
| 2349 |
status += "\n\n" + "\n".join(f"- {warning}" for warning in integration.warnings)
|
| 2350 |
+
inferred_roles = state.get("inferred_roles", [])
|
| 2351 |
+
if inferred_roles:
|
| 2352 |
+
status += (
|
| 2353 |
+
"\n\n- Added separate asset roles from deterministic game-code hooks: "
|
| 2354 |
+
+ ", ".join(inferred_roles)
|
| 2355 |
+
+ ". Add explicit descriptions for these roles to improve art direction."
|
| 2356 |
+
)
|
| 2357 |
if errors:
|
| 2358 |
status += "\n\n" + "\n".join(f"- {error}" for error in errors)
|
| 2359 |
gallery = [
|
|
|
|
| 2398 |
return empty_generation_result("Paste HTML game code first.")
|
| 2399 |
|
| 2400 |
style_context = build_style_context(game_type, perspective, theme)
|
| 2401 |
+
role_lines, prompt_map, prompt_model, prompt_error, inferred_roles = build_prompt_map(
|
| 2402 |
+
html_code,
|
| 2403 |
+
roles,
|
| 2404 |
+
style_context,
|
| 2405 |
+
)
|
| 2406 |
+
specs = parse_assets(roles, style_context, prompt_map, role_lines)
|
| 2407 |
if not specs:
|
| 2408 |
return empty_generation_result(
|
| 2409 |
"Add at least one asset role, like `player: brave knight`.",
|
|
|
|
| 2446 |
"prompt_model": prompt_model,
|
| 2447 |
"image_models": image_models,
|
| 2448 |
"quality": quality,
|
| 2449 |
+
"inferred_roles": inferred_roles,
|
| 2450 |
"run_id": run_id,
|
| 2451 |
}
|
| 2452 |
rendered = render_generation_state(state, "Generated", errors)
|
|
|
|
| 2682 |
<h1>Turn your game code into a visual world.</h1>
|
| 2683 |
<p>Describe the art direction, generate camera-aware assets, and embed them into deterministic image hooks鈥攚ithout rewriting your game logic.</p>
|
| 2684 |
<div class="hero-chips">
|
| 2685 |
+
<span>Multiple character roles</span><span>Game-ready PNGs</span><span>Safe code integration</span>
|
| 2686 |
</div>
|
| 2687 |
</header>
|
| 2688 |
"""
|
|
|
|
| 2709 |
lines=8,
|
| 2710 |
placeholder=ROLE_PLACEHOLDER,
|
| 2711 |
value=DEFAULT_ROLES,
|
| 2712 |
+
info=(
|
| 2713 |
+
"One separate role per character or asset, for example player, enemy_grunt, and enemy_boss. "
|
| 2714 |
+
"Missing deterministic GAME_ASSETS roles are added from the submitted code."
|
| 2715 |
+
),
|
| 2716 |
)
|
| 2717 |
with gr.Row():
|
| 2718 |
game_type = gr.Dropdown(
|