import base64
import hashlib
import io
import json
import math
import os
import random
import re
import tempfile
import time
from dataclasses import dataclass
from html import escape
from urllib.parse import quote
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
import gradio as gr
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
try:
from huggingface_hub import InferenceClient
except Exception:
InferenceClient = None
STARTER_HTML = """
Mini Forest Game
"""
DEFAULT_ROLES = """player: top-down pixel-art adventurer hero, transparent background, bright readable silhouette
background: enchanted forest clearing game background, top-down view, soft moonlight, detailed but not too busy"""
ROLE_PLACEHOLDER = """player: blue robot hero sprite
background: empty space station floor map"""
@dataclass
class AssetSpec:
role: str
prompt: str
filename: str
width: int
height: int
@dataclass
class StylePlan:
medium: str
palette: str
texture: str
lighting: str
linework: str
camera: str
tags: tuple[str, ...]
HF_TOKEN = os.environ.get("HF_TOKEN", "")
FREE_IMAGE_MODEL = os.environ.get("FREE_IMAGE_MODEL", "segmind/tiny-sd")
FREE_IMAGE_STEPS = int(os.environ.get("FREE_IMAGE_STEPS", "5"))
USE_HF_PROMPT_PROVIDER = os.environ.get("USE_HF_PROMPT_PROVIDER", "0") == "1"
USE_HF_IMAGE_PROVIDER = os.environ.get("USE_HF_IMAGE_PROVIDER", "0") == "1"
USE_DIFFUSION_FOR_SPRITES = os.environ.get("USE_DIFFUSION_FOR_SPRITES", "0") == "1"
USE_DIFFUSION_FOR_BACKGROUNDS = os.environ.get("USE_DIFFUSION_FOR_BACKGROUNDS", "0") == "1"
HF_IMAGE_MODEL = os.environ.get("HF_IMAGE_MODEL", "black-forest-labs/FLUX.1-schnell")
HF_PROMPT_MODEL = os.environ.get("HF_PROMPT_MODEL", "Qwen/Qwen2.5-Coder-7B-Instruct:fastest")
HF_PROMPT_ENDPOINT = os.environ.get("HF_PROMPT_ENDPOINT", "https://router.huggingface.co/v1/chat/completions")
FREE_DIFFUSION_PIPE = None
FREE_DIFFUSION_ERROR = None
def slugify(value: str) -> str:
value = re.sub(r"[^a-zA-Z0-9]+", "_", value.strip().lower()).strip("_")
return value or "asset"
def interpret_style_hint(style_hint: str) -> StylePlan:
text = (style_hint or "").lower()
tags: list[str] = []
def has(*words: str) -> bool:
return any(word in text for word in words)
if has("watercolor", "ink wash", "gouache", "paper"):
tags.append("watercolor")
medium = "watercolor game illustration on textured paper"
palette = "soft layered washes, muted pigments, gentle color bleed"
texture = "visible paper grain, translucent edges, organic brush pooling"
lighting = "soft ambient light with low contrast"
linework = "loose ink accents, imperfect hand-painted contour lines"
elif has("vector", "flat", "icon", "logo"):
tags.append("vector")
medium = "clean vector game art"
palette = "flat separated color fields with bold accent colors"
texture = "smooth fills, no painterly grain"
lighting = "graphic cel shading with crisp highlights"
linework = "thick precise outlines and hard-edged silhouettes"
elif has("clay", "claymation", "stop motion", "plasticine"):
tags.append("clay")
medium = "claymation stop-motion game asset"
palette = "chunky colored clay with warm handmade tones"
texture = "fingerprint-like clay bumps, rounded sculpted forms"
lighting = "soft studio light with small specular highlights"
linework = "no ink outlines, shape defined by soft shadows"
elif has("oil", "painterly", "impasto", "canvas"):
tags.append("oil")
medium = "oil-painted fantasy game art"
palette = "rich blended colors with deep shadows"
texture = "visible bristle strokes and canvas-like texture"
lighting = "dramatic directional light"
linework = "painted edges instead of hard outlines"
elif has("pixel", "8-bit", "16-bit", "retro"):
tags.append("pixel")
medium = "retro pixel-art game asset"
palette = "limited palette with readable color ramps"
texture = "sharp square pixels and no anti-aliasing"
lighting = "simple two-tone pixel shading"
linework = "one-pixel dark outline and blocky silhouette"
elif has("anime", "manga"):
tags.append("anime")
medium = "anime game illustration"
palette = "clean saturated colors with expressive accents"
texture = "smooth cel-shaded surfaces"
lighting = "bright rim light and glossy highlights"
linework = "confident manga-style outlines"
elif has("cyber", "neon", "synthwave"):
tags.append("vector")
tags.append("neon")
medium = "neon cyberpunk arcade game art"
palette = "electric cyan, magenta, violet, and black"
texture = "glowing edges, glossy panels, light bloom"
lighting = "high contrast neon rim lighting"
linework = "hard sci-fi outlines with luminous accents"
else:
tags.append("illustration")
medium = "cohesive 2D game illustration"
palette = "distinct theme-driven colors with clear value contrast"
texture = "clean readable game asset finish"
lighting = "balanced game lighting"
linework = "clear readable silhouette and controlled edges"
if has("top-down", "top down", "shooter", "rpg", "arena"):
camera = "top-down readable game camera"
elif has("platformer", "side-scroller", "side scroller"):
camera = "side-view platformer camera"
else:
camera = "game-ready camera angle matching the role"
return StylePlan(medium, palette, texture, lighting, linework, camera, tuple(tags))
def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
plan = interpret_style_hint(style_hint)
slug = slugify(role)
is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
if is_background:
asset_instruction = (
"Create one complete 2D game background scene, not a texture tile, not a material sample, "
"not a UV map. Full scene composition for a canvas game. Empty environment only: no player, "
"no character, no creature, no vehicle, no mascot, no foreground subject."
)
else:
asset_instruction = (
"Create one complete standalone 2D sprite of the whole subject, centered, full body or full vehicle, "
"single object, transparent or plain background, readable silhouette. Not a texture map, not a tiled "
"pattern, not a material swatch, not a UV unwrap, not a 3D model skin."
)
return (
f"{role} asset: {prompt}. Style interpretation: {plan.medium}; {plan.palette}; "
f"{plan.texture}; {plan.lighting}; {plan.linework}; {plan.camera}. "
f"{asset_instruction} "
"Game asset, readable at small size, no text, no watermark."
)
def parse_role_lines(raw_roles: str) -> list[tuple[str, str]]:
parsed: list[tuple[str, str]] = []
for line in raw_roles.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if ":" in line:
role, prompt = line.split(":", 1)
elif "=" in line:
role, prompt = line.split("=", 1)
else:
role, prompt = line, line
role = role.strip()
prompt = prompt.strip() or role
parsed.append((role, prompt))
return parsed
def infer_code_context(html_code: str) -> str:
text = html_code[:12000]
filenames = sorted(set(re.findall(r"['\"]([^'\"]+?\.(?:png|jpg|jpeg|webp|gif))['\"]", text, flags=re.I)))
canvas = re.findall(r"