Improve style-aware asset generation
Browse files
app.py
CHANGED
|
@@ -3,6 +3,7 @@ import hashlib
|
|
| 3 |
import io
|
| 4 |
import json
|
| 5 |
import math
|
|
|
|
| 6 |
import random
|
| 7 |
import re
|
| 8 |
import tempfile
|
|
@@ -10,9 +11,10 @@ import time
|
|
| 10 |
from dataclasses import dataclass
|
| 11 |
from html import escape
|
| 12 |
from urllib.parse import quote
|
|
|
|
| 13 |
|
| 14 |
import gradio as gr
|
| 15 |
-
from PIL import Image, ImageDraw, ImageFont
|
| 16 |
|
| 17 |
|
| 18 |
STARTER_HTML = """<!DOCTYPE html>
|
|
@@ -83,11 +85,112 @@ class AssetSpec:
|
|
| 83 |
height: int
|
| 84 |
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
def slugify(value: str) -> str:
|
| 87 |
value = re.sub(r"[^a-zA-Z0-9]+", "_", value.strip().lower()).strip("_")
|
| 88 |
return value or "asset"
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]:
|
| 92 |
specs: list[AssetSpec] = []
|
| 93 |
for line in raw_roles.splitlines():
|
|
@@ -108,10 +211,7 @@ def parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]:
|
|
| 108 |
is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
|
| 109 |
width, height = (800, 450) if is_background else (128, 128)
|
| 110 |
filename = f"sprite_{slug}.png"
|
| 111 |
-
full_prompt = (
|
| 112 |
-
f"{prompt}, {style_hint}, game asset, clean readable shape, "
|
| 113 |
-
"no text, no watermark"
|
| 114 |
-
).strip(", ")
|
| 115 |
specs.append(AssetSpec(role=role, prompt=full_prompt, filename=filename, width=width, height=height))
|
| 116 |
return specs
|
| 117 |
|
|
@@ -148,6 +248,16 @@ def write_gallery_image(content: bytes, role: str) -> str:
|
|
| 148 |
|
| 149 |
def palette_for(text: str) -> list[tuple[int, int, int]]:
|
| 150 |
lowered = text.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
themed = [
|
| 152 |
(("lava", "fire", "volcano", "hell"), [(139, 28, 18), (239, 84, 34), (42, 18, 16), (255, 198, 71)]),
|
| 153 |
(("ice", "snow", "frost", "arctic"), [(75, 151, 191), (187, 235, 255), (24, 63, 96), (240, 252, 255)]),
|
|
@@ -189,6 +299,98 @@ def palette_for(text: str) -> list[tuple[int, int, int]]:
|
|
| 189 |
]
|
| 190 |
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
|
| 193 |
colors = palette_for(spec.prompt)
|
| 194 |
image = Image.new("RGBA", (spec.width, spec.height), colors[2] + (255,))
|
|
@@ -530,8 +732,10 @@ def local_asset_png(spec: AssetSpec, index: int, run_id: int) -> bytes:
|
|
| 530 |
seed_text = f"{spec.role}|{spec.prompt}|{index}|{run_id}"
|
| 531 |
rng = random.Random(seed_text)
|
| 532 |
if is_background_spec(spec):
|
| 533 |
-
|
| 534 |
-
|
|
|
|
|
|
|
| 535 |
|
| 536 |
|
| 537 |
def placeholder_png_bytes(role: str, width: int, height: int) -> bytes:
|
|
@@ -549,12 +753,52 @@ def placeholder_png_bytes(role: str, width: int, height: int) -> bytes:
|
|
| 549 |
return out.getvalue()
|
| 550 |
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str, str | None]:
|
| 553 |
-
png_content =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
return (
|
| 555 |
png_bytes_to_data_uri(png_content),
|
| 556 |
write_gallery_image(png_content, spec.role),
|
| 557 |
-
None,
|
| 558 |
)
|
| 559 |
|
| 560 |
|
|
@@ -696,13 +940,17 @@ def build_preview(html_code: str) -> str:
|
|
| 696 |
)
|
| 697 |
|
| 698 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
| 700 |
if not html_code.strip():
|
| 701 |
-
return "", "Paste HTML game code first.", [], ""
|
| 702 |
|
| 703 |
specs = parse_assets(roles, style_hint or "pixel art style")
|
| 704 |
if not specs:
|
| 705 |
-
return html_code, "Add at least one asset role, like `player: brave knight`.", [], build_preview(html_code)
|
| 706 |
|
| 707 |
assets: dict[str, str] = {}
|
| 708 |
gallery = []
|
|
@@ -717,10 +965,12 @@ def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
|
| 717 |
errors.append(f"{spec.role}: fallback used ({error})")
|
| 718 |
|
| 719 |
rewritten = embed_assets(html_code, assets, specs)
|
| 720 |
-
|
|
|
|
|
|
|
| 721 |
if errors:
|
| 722 |
-
status += "\n\n" + "\n".join(errors)
|
| 723 |
-
return rewritten, status, gallery, build_preview(rewritten)
|
| 724 |
|
| 725 |
|
| 726 |
with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
@@ -759,6 +1009,11 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 759 |
language="html",
|
| 760 |
lines=18,
|
| 761 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
|
| 763 |
gr.Markdown("## Game preview")
|
| 764 |
preview = gr.HTML(build_preview(STARTER_HTML))
|
|
@@ -766,7 +1021,7 @@ with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
| 766 |
generate_btn.click(
|
| 767 |
fn=generate_images_and_game,
|
| 768 |
inputs=[html_input, roles, style],
|
| 769 |
-
outputs=[output_code, status, gallery, preview],
|
| 770 |
)
|
| 771 |
|
| 772 |
|
|
|
|
| 3 |
import io
|
| 4 |
import json
|
| 5 |
import math
|
| 6 |
+
import os
|
| 7 |
import random
|
| 8 |
import re
|
| 9 |
import tempfile
|
|
|
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from html import escape
|
| 13 |
from urllib.parse import quote
|
| 14 |
+
from urllib.request import Request, urlopen
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
+
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
| 18 |
|
| 19 |
|
| 20 |
STARTER_HTML = """<!DOCTYPE html>
|
|
|
|
| 85 |
height: int
|
| 86 |
|
| 87 |
|
| 88 |
+
@dataclass
|
| 89 |
+
class StylePlan:
|
| 90 |
+
medium: str
|
| 91 |
+
palette: str
|
| 92 |
+
texture: str
|
| 93 |
+
lighting: str
|
| 94 |
+
linework: str
|
| 95 |
+
camera: str
|
| 96 |
+
tags: tuple[str, ...]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 100 |
+
HF_IMAGE_MODEL = os.environ.get("HF_IMAGE_MODEL", "black-forest-labs/FLUX.1-schnell")
|
| 101 |
+
HF_IMAGE_ENDPOINT = f"https://api-inference.huggingface.co/models/{HF_IMAGE_MODEL}"
|
| 102 |
+
|
| 103 |
+
|
| 104 |
def slugify(value: str) -> str:
|
| 105 |
value = re.sub(r"[^a-zA-Z0-9]+", "_", value.strip().lower()).strip("_")
|
| 106 |
return value or "asset"
|
| 107 |
|
| 108 |
|
| 109 |
+
def interpret_style_hint(style_hint: str) -> StylePlan:
|
| 110 |
+
text = (style_hint or "").lower()
|
| 111 |
+
tags: list[str] = []
|
| 112 |
+
|
| 113 |
+
def has(*words: str) -> bool:
|
| 114 |
+
return any(word in text for word in words)
|
| 115 |
+
|
| 116 |
+
if has("watercolor", "ink wash", "gouache", "paper"):
|
| 117 |
+
tags.append("watercolor")
|
| 118 |
+
medium = "watercolor game illustration on textured paper"
|
| 119 |
+
palette = "soft layered washes, muted pigments, gentle color bleed"
|
| 120 |
+
texture = "visible paper grain, translucent edges, organic brush pooling"
|
| 121 |
+
lighting = "soft ambient light with low contrast"
|
| 122 |
+
linework = "loose ink accents, imperfect hand-painted contour lines"
|
| 123 |
+
elif has("vector", "flat", "icon", "logo"):
|
| 124 |
+
tags.append("vector")
|
| 125 |
+
medium = "clean vector game art"
|
| 126 |
+
palette = "flat separated color fields with bold accent colors"
|
| 127 |
+
texture = "smooth fills, no painterly grain"
|
| 128 |
+
lighting = "graphic cel shading with crisp highlights"
|
| 129 |
+
linework = "thick precise outlines and hard-edged silhouettes"
|
| 130 |
+
elif has("clay", "claymation", "stop motion", "plasticine"):
|
| 131 |
+
tags.append("clay")
|
| 132 |
+
medium = "claymation stop-motion game asset"
|
| 133 |
+
palette = "chunky colored clay with warm handmade tones"
|
| 134 |
+
texture = "fingerprint-like clay bumps, rounded sculpted forms"
|
| 135 |
+
lighting = "soft studio light with small specular highlights"
|
| 136 |
+
linework = "no ink outlines, shape defined by soft shadows"
|
| 137 |
+
elif has("oil", "painterly", "impasto", "canvas"):
|
| 138 |
+
tags.append("oil")
|
| 139 |
+
medium = "oil-painted fantasy game art"
|
| 140 |
+
palette = "rich blended colors with deep shadows"
|
| 141 |
+
texture = "visible bristle strokes and canvas-like texture"
|
| 142 |
+
lighting = "dramatic directional light"
|
| 143 |
+
linework = "painted edges instead of hard outlines"
|
| 144 |
+
elif has("pixel", "8-bit", "16-bit", "retro"):
|
| 145 |
+
tags.append("pixel")
|
| 146 |
+
medium = "retro pixel-art game asset"
|
| 147 |
+
palette = "limited palette with readable color ramps"
|
| 148 |
+
texture = "sharp square pixels and no anti-aliasing"
|
| 149 |
+
lighting = "simple two-tone pixel shading"
|
| 150 |
+
linework = "one-pixel dark outline and blocky silhouette"
|
| 151 |
+
elif has("anime", "manga"):
|
| 152 |
+
tags.append("anime")
|
| 153 |
+
medium = "anime game illustration"
|
| 154 |
+
palette = "clean saturated colors with expressive accents"
|
| 155 |
+
texture = "smooth cel-shaded surfaces"
|
| 156 |
+
lighting = "bright rim light and glossy highlights"
|
| 157 |
+
linework = "confident manga-style outlines"
|
| 158 |
+
elif has("cyber", "neon", "synthwave"):
|
| 159 |
+
tags.append("vector")
|
| 160 |
+
tags.append("neon")
|
| 161 |
+
medium = "neon cyberpunk arcade game art"
|
| 162 |
+
palette = "electric cyan, magenta, violet, and black"
|
| 163 |
+
texture = "glowing edges, glossy panels, light bloom"
|
| 164 |
+
lighting = "high contrast neon rim lighting"
|
| 165 |
+
linework = "hard sci-fi outlines with luminous accents"
|
| 166 |
+
else:
|
| 167 |
+
tags.append("illustration")
|
| 168 |
+
medium = "cohesive 2D game illustration"
|
| 169 |
+
palette = "distinct theme-driven colors with clear value contrast"
|
| 170 |
+
texture = "clean readable game asset finish"
|
| 171 |
+
lighting = "balanced game lighting"
|
| 172 |
+
linework = "clear readable silhouette and controlled edges"
|
| 173 |
+
|
| 174 |
+
if has("top-down", "top down", "shooter", "rpg", "arena"):
|
| 175 |
+
camera = "top-down readable game camera"
|
| 176 |
+
elif has("platformer", "side-scroller", "side scroller"):
|
| 177 |
+
camera = "side-view platformer camera"
|
| 178 |
+
else:
|
| 179 |
+
camera = "game-ready camera angle matching the role"
|
| 180 |
+
|
| 181 |
+
return StylePlan(medium, palette, texture, lighting, linework, camera, tuple(tags))
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
|
| 185 |
+
plan = interpret_style_hint(style_hint)
|
| 186 |
+
return (
|
| 187 |
+
f"{role} asset: {prompt}. Style interpretation: {plan.medium}; {plan.palette}; "
|
| 188 |
+
f"{plan.texture}; {plan.lighting}; {plan.linework}; {plan.camera}. "
|
| 189 |
+
"Transparent background for characters and items unless the role is a background. "
|
| 190 |
+
"Game asset, readable at small size, no text, no watermark."
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
def parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]:
|
| 195 |
specs: list[AssetSpec] = []
|
| 196 |
for line in raw_roles.splitlines():
|
|
|
|
| 211 |
is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
|
| 212 |
width, height = (800, 450) if is_background else (128, 128)
|
| 213 |
filename = f"sprite_{slug}.png"
|
| 214 |
+
full_prompt = build_asset_prompt(role, prompt, style_hint)
|
|
|
|
|
|
|
|
|
|
| 215 |
specs.append(AssetSpec(role=role, prompt=full_prompt, filename=filename, width=width, height=height))
|
| 216 |
return specs
|
| 217 |
|
|
|
|
| 248 |
|
| 249 |
def palette_for(text: str) -> list[tuple[int, int, int]]:
|
| 250 |
lowered = text.lower()
|
| 251 |
+
if any(word in lowered for word in ("watercolor", "ink wash", "gouache", "paper grain")):
|
| 252 |
+
return [(113, 151, 168), (207, 180, 158), (241, 232, 214), (154, 118, 148)]
|
| 253 |
+
if any(word in lowered for word in ("clay", "claymation", "stop-motion", "plasticine")):
|
| 254 |
+
return [(204, 105, 83), (238, 175, 104), (107, 72, 61), (242, 213, 151)]
|
| 255 |
+
if any(word in lowered for word in ("oil", "painterly", "impasto", "canvas")):
|
| 256 |
+
return [(98, 45, 122), (213, 154, 70), (38, 28, 42), (230, 212, 156)]
|
| 257 |
+
if any(word in lowered for word in ("vector", "flat", "logo")):
|
| 258 |
+
return [(42, 125, 246), (255, 81, 128), (18, 24, 38), (255, 221, 64)]
|
| 259 |
+
if any(word in lowered for word in ("anime", "manga")):
|
| 260 |
+
return [(78, 151, 236), (255, 139, 173), (42, 45, 76), (255, 238, 166)]
|
| 261 |
themed = [
|
| 262 |
(("lava", "fire", "volcano", "hell"), [(139, 28, 18), (239, 84, 34), (42, 18, 16), (255, 198, 71)]),
|
| 263 |
(("ice", "snow", "frost", "arctic"), [(75, 151, 191), (187, 235, 255), (24, 63, 96), (240, 252, 255)]),
|
|
|
|
| 299 |
]
|
| 300 |
|
| 301 |
|
| 302 |
+
def style_tags_for(prompt: str) -> set[str]:
|
| 303 |
+
lowered = prompt.lower()
|
| 304 |
+
tags = set()
|
| 305 |
+
groups = {
|
| 306 |
+
"watercolor": ("watercolor", "ink wash", "gouache", "paper grain"),
|
| 307 |
+
"vector": ("vector", "flat", "logo", "hard-edged", "graphic"),
|
| 308 |
+
"clay": ("clay", "claymation", "stop-motion", "plasticine", "sculpted"),
|
| 309 |
+
"oil": ("oil", "painterly", "impasto", "canvas", "bristle"),
|
| 310 |
+
"pixel": ("pixel", "8-bit", "16-bit", "retro"),
|
| 311 |
+
"anime": ("anime", "manga", "cel-shaded"),
|
| 312 |
+
"neon": ("neon", "cyberpunk", "synthwave", "glowing"),
|
| 313 |
+
}
|
| 314 |
+
for tag, words in groups.items():
|
| 315 |
+
if any(word in lowered for word in words):
|
| 316 |
+
tags.add(tag)
|
| 317 |
+
return tags
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
def apply_style_finish(content: bytes, spec: AssetSpec, rng: random.Random) -> bytes:
|
| 321 |
+
tags = style_tags_for(spec.prompt)
|
| 322 |
+
if not tags:
|
| 323 |
+
return content
|
| 324 |
+
|
| 325 |
+
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
| 326 |
+
draw = ImageDraw.Draw(image, "RGBA")
|
| 327 |
+
is_bg = is_background_spec(spec)
|
| 328 |
+
|
| 329 |
+
if "watercolor" in tags:
|
| 330 |
+
wash = Image.new("RGBA", image.size, (245, 238, 220, 34 if is_bg else 12))
|
| 331 |
+
image = Image.alpha_composite(image, wash)
|
| 332 |
+
draw = ImageDraw.Draw(image, "RGBA")
|
| 333 |
+
for _ in range(60 if is_bg else 18):
|
| 334 |
+
x = rng.randint(-20, image.width)
|
| 335 |
+
y = rng.randint(-20, image.height)
|
| 336 |
+
r = rng.randint(10, 70 if is_bg else 24)
|
| 337 |
+
color = rng.choice(palette_for(spec.prompt)) + (rng.randint(18, 55),)
|
| 338 |
+
draw.ellipse((x - r, y - r, x + r, y + r), fill=color)
|
| 339 |
+
image = image.filter(ImageFilter.GaussianBlur(0.45))
|
| 340 |
+
|
| 341 |
+
if "oil" in tags:
|
| 342 |
+
for _ in range(120 if is_bg else 34):
|
| 343 |
+
x = rng.randint(0, image.width)
|
| 344 |
+
y = rng.randint(0, image.height)
|
| 345 |
+
length = rng.randint(8, 38 if is_bg else 16)
|
| 346 |
+
color = rng.choice(palette_for(spec.prompt)) + (rng.randint(75, 145),)
|
| 347 |
+
draw.line((x, y, x + rng.randint(-length, length), y + rng.randint(-length, length)), fill=color, width=rng.randint(2, 5))
|
| 348 |
+
image = ImageEnhance.Contrast(image).enhance(1.12)
|
| 349 |
+
|
| 350 |
+
if "clay" in tags:
|
| 351 |
+
image = ImageEnhance.Color(image).enhance(0.82)
|
| 352 |
+
image = ImageEnhance.Contrast(image).enhance(1.18)
|
| 353 |
+
draw = ImageDraw.Draw(image, "RGBA")
|
| 354 |
+
for _ in range(45 if is_bg else 14):
|
| 355 |
+
x = rng.randint(0, image.width)
|
| 356 |
+
y = rng.randint(0, image.height)
|
| 357 |
+
r = rng.randint(3, 18 if is_bg else 8)
|
| 358 |
+
draw.ellipse((x - r, y - r, x + r, y + r), outline=(255, 238, 205, rng.randint(25, 70)), width=2)
|
| 359 |
+
image = image.filter(ImageFilter.SMOOTH_MORE)
|
| 360 |
+
|
| 361 |
+
if "vector" in tags:
|
| 362 |
+
image = ImageEnhance.Contrast(image).enhance(1.35)
|
| 363 |
+
image = ImageEnhance.Color(image).enhance(1.25)
|
| 364 |
+
if not is_bg:
|
| 365 |
+
alpha = image.getchannel("A")
|
| 366 |
+
outline = alpha.filter(ImageFilter.MaxFilter(7)).filter(ImageFilter.GaussianBlur(0.2))
|
| 367 |
+
outlined = Image.new("RGBA", image.size, (12, 16, 28, 255))
|
| 368 |
+
outlined.putalpha(outline)
|
| 369 |
+
image = Image.alpha_composite(outlined, image)
|
| 370 |
+
|
| 371 |
+
if "anime" in tags:
|
| 372 |
+
image = ImageEnhance.Color(image).enhance(1.35)
|
| 373 |
+
image = ImageEnhance.Contrast(image).enhance(1.18)
|
| 374 |
+
draw = ImageDraw.Draw(image, "RGBA")
|
| 375 |
+
for _ in range(8 if is_bg else 4):
|
| 376 |
+
x = rng.randint(0, image.width)
|
| 377 |
+
y = rng.randint(0, image.height)
|
| 378 |
+
draw.line((x, y, x + rng.randint(20, 80), y - rng.randint(10, 50)), fill=(255, 255, 255, 80), width=2)
|
| 379 |
+
|
| 380 |
+
if "neon" in tags:
|
| 381 |
+
glow = image.filter(ImageFilter.GaussianBlur(4 if is_bg else 3))
|
| 382 |
+
image = Image.blend(glow, image, 0.72)
|
| 383 |
+
image = ImageEnhance.Color(image).enhance(1.45)
|
| 384 |
+
|
| 385 |
+
if "pixel" in tags:
|
| 386 |
+
small = image.resize((max(1, image.width // 4), max(1, image.height // 4)), Image.Resampling.NEAREST)
|
| 387 |
+
image = small.resize(image.size, Image.Resampling.NEAREST)
|
| 388 |
+
|
| 389 |
+
out = io.BytesIO()
|
| 390 |
+
image.save(out, format="PNG")
|
| 391 |
+
return out.getvalue()
|
| 392 |
+
|
| 393 |
+
|
| 394 |
def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
|
| 395 |
colors = palette_for(spec.prompt)
|
| 396 |
image = Image.new("RGBA", (spec.width, spec.height), colors[2] + (255,))
|
|
|
|
| 732 |
seed_text = f"{spec.role}|{spec.prompt}|{index}|{run_id}"
|
| 733 |
rng = random.Random(seed_text)
|
| 734 |
if is_background_spec(spec):
|
| 735 |
+
content = draw_background(spec, rng)
|
| 736 |
+
else:
|
| 737 |
+
content = draw_sprite(spec, rng)
|
| 738 |
+
return apply_style_finish(content, spec, rng)
|
| 739 |
|
| 740 |
|
| 741 |
def placeholder_png_bytes(role: str, width: int, height: int) -> bytes:
|
|
|
|
| 753 |
return out.getvalue()
|
| 754 |
|
| 755 |
|
| 756 |
+
def hf_image_png(spec: AssetSpec, index: int, run_id: int) -> bytes | None:
|
| 757 |
+
if not HF_TOKEN:
|
| 758 |
+
return None
|
| 759 |
+
|
| 760 |
+
payload = {
|
| 761 |
+
"inputs": spec.prompt,
|
| 762 |
+
"parameters": {
|
| 763 |
+
"width": spec.width,
|
| 764 |
+
"height": spec.height,
|
| 765 |
+
"num_inference_steps": 4,
|
| 766 |
+
"guidance_scale": 0.0,
|
| 767 |
+
"seed": abs(hash(f"{spec.role}|{spec.prompt}|{index}|{run_id}")) % 2147483647,
|
| 768 |
+
},
|
| 769 |
+
"options": {"wait_for_model": True},
|
| 770 |
+
}
|
| 771 |
+
request = Request(
|
| 772 |
+
HF_IMAGE_ENDPOINT,
|
| 773 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 774 |
+
headers={
|
| 775 |
+
"Authorization": f"Bearer {HF_TOKEN}",
|
| 776 |
+
"Content-Type": "application/json",
|
| 777 |
+
"Accept": "image/png",
|
| 778 |
+
},
|
| 779 |
+
method="POST",
|
| 780 |
+
)
|
| 781 |
+
try:
|
| 782 |
+
with urlopen(request, timeout=90) as response:
|
| 783 |
+
content_type = response.headers.get("content-type", "")
|
| 784 |
+
content = response.read()
|
| 785 |
+
if "image" not in content_type:
|
| 786 |
+
return None
|
| 787 |
+
return image_to_png_bytes(content, spec.width, spec.height)
|
| 788 |
+
except Exception:
|
| 789 |
+
return None
|
| 790 |
+
|
| 791 |
+
|
| 792 |
def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str, str | None]:
|
| 793 |
+
png_content = hf_image_png(spec, index, run_id)
|
| 794 |
+
source = "hf"
|
| 795 |
+
if png_content is None:
|
| 796 |
+
png_content = local_asset_png(spec, index, run_id)
|
| 797 |
+
source = "local style fallback"
|
| 798 |
return (
|
| 799 |
png_bytes_to_data_uri(png_content),
|
| 800 |
write_gallery_image(png_content, spec.role),
|
| 801 |
+
None if source == "hf" else source,
|
| 802 |
)
|
| 803 |
|
| 804 |
|
|
|
|
| 940 |
)
|
| 941 |
|
| 942 |
|
| 943 |
+
def build_prompt_preview(specs: list[AssetSpec]) -> str:
|
| 944 |
+
return "\n\n".join(f"{spec.role}:\n{spec.prompt}" for spec in specs)
|
| 945 |
+
|
| 946 |
+
|
| 947 |
def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
| 948 |
if not html_code.strip():
|
| 949 |
+
return "", "Paste HTML game code first.", [], "", ""
|
| 950 |
|
| 951 |
specs = parse_assets(roles, style_hint or "pixel art style")
|
| 952 |
if not specs:
|
| 953 |
+
return html_code, "Add at least one asset role, like `player: brave knight`.", [], "", build_preview(html_code)
|
| 954 |
|
| 955 |
assets: dict[str, str] = {}
|
| 956 |
gallery = []
|
|
|
|
| 965 |
errors.append(f"{spec.role}: fallback used ({error})")
|
| 966 |
|
| 967 |
rewritten = embed_assets(html_code, assets, specs)
|
| 968 |
+
using_hf = bool(HF_TOKEN)
|
| 969 |
+
source = f"HF image model `{HF_IMAGE_MODEL}`" if using_hf else "local style fallback"
|
| 970 |
+
status = f"Generated and embedded {len(specs)} fresh asset(s) using {source}. Run {str(run_id)[-6:]}."
|
| 971 |
if errors:
|
| 972 |
+
status += "\n\n" + "\n".join(f"{item}: no HF image returned, used local style fallback" for item in [e.split(':', 1)[0] for e in errors])
|
| 973 |
+
return rewritten, status, gallery, build_prompt_preview(specs), build_preview(rewritten)
|
| 974 |
|
| 975 |
|
| 976 |
with gr.Blocks(title="Image Generator for HTML Games") as demo:
|
|
|
|
| 1009 |
language="html",
|
| 1010 |
lines=18,
|
| 1011 |
)
|
| 1012 |
+
prompt_preview = gr.Textbox(
|
| 1013 |
+
label="Interpreted image prompts",
|
| 1014 |
+
lines=8,
|
| 1015 |
+
interactive=False,
|
| 1016 |
+
)
|
| 1017 |
|
| 1018 |
gr.Markdown("## Game preview")
|
| 1019 |
preview = gr.HTML(build_preview(STARTER_HTML))
|
|
|
|
| 1021 |
generate_btn.click(
|
| 1022 |
fn=generate_images_and_game,
|
| 1023 |
inputs=[html_input, roles, style],
|
| 1024 |
+
outputs=[output_code, status, gallery, prompt_preview, preview],
|
| 1025 |
)
|
| 1026 |
|
| 1027 |
|