Use AI prompt and image models
Browse files
app.py
CHANGED
|
@@ -99,6 +99,8 @@ class StylePlan:
|
|
| 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:
|
|
@@ -191,8 +193,8 @@ def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
|
|
| 191 |
)
|
| 192 |
|
| 193 |
|
| 194 |
-
def
|
| 195 |
-
|
| 196 |
for line in raw_roles.splitlines():
|
| 197 |
line = line.strip()
|
| 198 |
if not line or line.startswith("#"):
|
|
@@ -204,14 +206,126 @@ def parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]:
|
|
| 204 |
role, prompt = line.split("=", 1)
|
| 205 |
else:
|
| 206 |
role, prompt = line, line
|
| 207 |
-
|
| 208 |
role = role.strip()
|
| 209 |
prompt = prompt.strip() or role
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
slug = slugify(role)
|
| 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 |
|
|
@@ -883,16 +997,17 @@ def hf_image_png(spec: AssetSpec, index: int, run_id: int) -> bytes | None:
|
|
| 883 |
return None
|
| 884 |
|
| 885 |
|
| 886 |
-
def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str, str | None]:
|
| 887 |
png_content = hf_image_png(spec, index, run_id)
|
| 888 |
-
source =
|
| 889 |
if png_content is None:
|
| 890 |
png_content = local_asset_png(spec, index, run_id)
|
| 891 |
source = "local style fallback"
|
| 892 |
return (
|
| 893 |
png_bytes_to_data_uri(png_content),
|
| 894 |
write_gallery_image(png_content, spec.role),
|
| 895 |
-
None if source ==
|
|
|
|
| 896 |
)
|
| 897 |
|
| 898 |
|
|
@@ -1038,15 +1153,16 @@ def build_prompt_preview(specs: list[AssetSpec]) -> str:
|
|
| 1038 |
return "\n\n".join(f"{spec.role}:\n{spec.prompt}" for spec in specs)
|
| 1039 |
|
| 1040 |
|
| 1041 |
-
def build_model_report(rows: list[tuple[str, str]]) -> str:
|
| 1042 |
-
return "\n".join(f"{role}: {
|
| 1043 |
|
| 1044 |
|
| 1045 |
def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
| 1046 |
if not html_code.strip():
|
| 1047 |
return "", "Paste HTML game code first.", [], "", "", ""
|
| 1048 |
|
| 1049 |
-
|
|
|
|
| 1050 |
if not specs:
|
| 1051 |
return html_code, "Add at least one asset role, like `player: brave knight`.", [], "", "", build_preview(html_code)
|
| 1052 |
|
|
@@ -1057,16 +1173,16 @@ def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
|
| 1057 |
run_id = time.time_ns()
|
| 1058 |
|
| 1059 |
for index, spec in enumerate(specs):
|
| 1060 |
-
data_uri, gallery_path, error = generate_asset(spec, index, run_id)
|
| 1061 |
assets[spec.role] = data_uri
|
| 1062 |
gallery.append((gallery_path, f"{spec.role} -> {spec.filename}"))
|
| 1063 |
-
model_rows.append((spec.role,
|
| 1064 |
if error:
|
| 1065 |
errors.append(f"{spec.role}: fallback used ({error})")
|
| 1066 |
|
| 1067 |
rewritten = embed_assets(html_code, assets, specs)
|
| 1068 |
using_hf = bool(HF_TOKEN)
|
| 1069 |
-
source = f"
|
| 1070 |
status = f"Generated and embedded {len(specs)} fresh asset(s) using {source}. Run {str(run_id)[-6:]}."
|
| 1071 |
if errors:
|
| 1072 |
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])
|
|
|
|
| 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 |
+
HF_PROMPT_MODEL = os.environ.get("HF_PROMPT_MODEL", "Qwen/Qwen2.5-Coder-7B-Instruct")
|
| 103 |
+
HF_PROMPT_ENDPOINT = f"https://api-inference.huggingface.co/models/{HF_PROMPT_MODEL}"
|
| 104 |
|
| 105 |
|
| 106 |
def slugify(value: str) -> str:
|
|
|
|
| 193 |
)
|
| 194 |
|
| 195 |
|
| 196 |
+
def parse_role_lines(raw_roles: str) -> list[tuple[str, str]]:
|
| 197 |
+
parsed: list[tuple[str, str]] = []
|
| 198 |
for line in raw_roles.splitlines():
|
| 199 |
line = line.strip()
|
| 200 |
if not line or line.startswith("#"):
|
|
|
|
| 206 |
role, prompt = line.split("=", 1)
|
| 207 |
else:
|
| 208 |
role, prompt = line, line
|
|
|
|
| 209 |
role = role.strip()
|
| 210 |
prompt = prompt.strip() or role
|
| 211 |
+
parsed.append((role, prompt))
|
| 212 |
+
return parsed
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def infer_code_context(html_code: str) -> str:
|
| 216 |
+
text = html_code[:12000]
|
| 217 |
+
filenames = sorted(set(re.findall(r"['\"]([^'\"]+?\.(?:png|jpg|jpeg|webp|gif))['\"]", text, flags=re.I)))
|
| 218 |
+
canvas = re.findall(r"<canvas[^>]*?(?:width=['\"]?(\d+)|height=['\"]?(\d+))", text, flags=re.I)
|
| 219 |
+
controls = []
|
| 220 |
+
lowered = text.lower()
|
| 221 |
+
for label, words in {
|
| 222 |
+
"top-down movement": ("arrowup", "arrowdown", "keys.has(\"w\")", "keys.has('w')"),
|
| 223 |
+
"platformer": ("gravity", "grounded", "platform"),
|
| 224 |
+
"shooting": ("bullet", "shoot", "projectile", "laser"),
|
| 225 |
+
"enemies": ("enemy", "monster", "spawn"),
|
| 226 |
+
}.items():
|
| 227 |
+
if any(word in lowered for word in words):
|
| 228 |
+
controls.append(label)
|
| 229 |
+
return (
|
| 230 |
+
f"Referenced asset filenames: {', '.join(filenames[:24]) or 'none found'}. "
|
| 231 |
+
f"Detected game mechanics: {', '.join(controls) or 'not obvious'}. "
|
| 232 |
+
f"Canvas hints found: {canvas[:4] or 'none'}."
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def local_prompt_map(role_lines: list[tuple[str, str]], style_hint: str) -> dict[str, str]:
|
| 237 |
+
return {role: build_asset_prompt(role, prompt, style_hint) for role, prompt in role_lines}
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def extract_json_object(text: str) -> dict | None:
|
| 241 |
+
match = re.search(r"\{.*\}", text, flags=re.S)
|
| 242 |
+
if not match:
|
| 243 |
+
return None
|
| 244 |
+
try:
|
| 245 |
+
value = json.loads(match.group(0))
|
| 246 |
+
except Exception:
|
| 247 |
+
return None
|
| 248 |
+
return value if isinstance(value, dict) else None
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def hf_prompt_json(html_code: str, role_lines: list[tuple[str, str]], style_hint: str) -> dict[str, str] | None:
|
| 252 |
+
if not HF_TOKEN:
|
| 253 |
+
return None
|
| 254 |
+
|
| 255 |
+
role_block = "\n".join(f"- {role}: {prompt}" for role, prompt in role_lines)
|
| 256 |
+
instruction = (
|
| 257 |
+
"You are a senior game art director and prompt engineer. Read the HTML game context, "
|
| 258 |
+
"the requested asset roles, and the shared theme/style. Return ONLY a JSON object where "
|
| 259 |
+
"each key is the exact role name and each value is one concise text-to-image prompt. "
|
| 260 |
+
"Each prompt must specify: subject silhouette/shape, camera angle, art style, palette, "
|
| 261 |
+
"transparent background for sprites/items, full scene for backgrounds, no text, no watermark. "
|
| 262 |
+
"Make different roles visually distinct and suitable for embedding in an HTML game."
|
| 263 |
+
)
|
| 264 |
+
user_text = (
|
| 265 |
+
f"HTML/game context summary: {infer_code_context(html_code)}\n\n"
|
| 266 |
+
f"Shared theme/style: {style_hint}\n\n"
|
| 267 |
+
f"Asset roles:\n{role_block}\n\n"
|
| 268 |
+
"Return JSON only."
|
| 269 |
+
)
|
| 270 |
+
prompt = f"<|im_start|>system\n{instruction}<|im_end|>\n<|im_start|>user\n{user_text}<|im_end|>\n<|im_start|>assistant\n"
|
| 271 |
+
payload = {
|
| 272 |
+
"inputs": prompt,
|
| 273 |
+
"parameters": {
|
| 274 |
+
"max_new_tokens": 900,
|
| 275 |
+
"temperature": 0.55,
|
| 276 |
+
"top_p": 0.9,
|
| 277 |
+
"return_full_text": False,
|
| 278 |
+
},
|
| 279 |
+
"options": {"wait_for_model": True},
|
| 280 |
+
}
|
| 281 |
+
request = Request(
|
| 282 |
+
HF_PROMPT_ENDPOINT,
|
| 283 |
+
data=json.dumps(payload).encode("utf-8"),
|
| 284 |
+
headers={
|
| 285 |
+
"Authorization": f"Bearer {HF_TOKEN}",
|
| 286 |
+
"Content-Type": "application/json",
|
| 287 |
+
},
|
| 288 |
+
method="POST",
|
| 289 |
+
)
|
| 290 |
+
try:
|
| 291 |
+
with urlopen(request, timeout=90) as response:
|
| 292 |
+
raw = response.read().decode("utf-8", errors="replace")
|
| 293 |
+
parsed = json.loads(raw)
|
| 294 |
+
if isinstance(parsed, list) and parsed:
|
| 295 |
+
text = parsed[0].get("generated_text", "") if isinstance(parsed[0], dict) else str(parsed[0])
|
| 296 |
+
elif isinstance(parsed, dict):
|
| 297 |
+
text = parsed.get("generated_text", parsed.get("text", ""))
|
| 298 |
+
else:
|
| 299 |
+
text = str(parsed)
|
| 300 |
+
obj = extract_json_object(text)
|
| 301 |
+
if not obj:
|
| 302 |
+
return None
|
| 303 |
+
return {
|
| 304 |
+
role: str(obj.get(role, "")).strip()
|
| 305 |
+
for role, _ in role_lines
|
| 306 |
+
if str(obj.get(role, "")).strip()
|
| 307 |
+
}
|
| 308 |
+
except Exception:
|
| 309 |
+
return None
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def build_prompt_map(html_code: str, raw_roles: str, style_hint: str) -> tuple[list[tuple[str, str]], dict[str, str], str]:
|
| 313 |
+
role_lines = parse_role_lines(raw_roles)
|
| 314 |
+
local_map = local_prompt_map(role_lines, style_hint)
|
| 315 |
+
ai_map = hf_prompt_json(html_code, role_lines, style_hint)
|
| 316 |
+
if ai_map and all(role in ai_map for role, _ in role_lines):
|
| 317 |
+
return role_lines, ai_map, HF_PROMPT_MODEL
|
| 318 |
+
return role_lines, local_map, "local prompt interpreter"
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def parse_assets(raw_roles: str, style_hint: str, prompt_map: dict[str, str] | None = None) -> list[AssetSpec]:
|
| 322 |
+
specs: list[AssetSpec] = []
|
| 323 |
+
for role, prompt in parse_role_lines(raw_roles):
|
| 324 |
slug = slugify(role)
|
| 325 |
is_background = any(word in slug for word in ("background", "backdrop", "scene", "map", "level"))
|
| 326 |
width, height = (800, 450) if is_background else (128, 128)
|
| 327 |
filename = f"sprite_{slug}.png"
|
| 328 |
+
full_prompt = (prompt_map or {}).get(role) or build_asset_prompt(role, prompt, style_hint)
|
| 329 |
specs.append(AssetSpec(role=role, prompt=full_prompt, filename=filename, width=width, height=height))
|
| 330 |
return specs
|
| 331 |
|
|
|
|
| 997 |
return None
|
| 998 |
|
| 999 |
|
| 1000 |
+
def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str, str | None, str]:
|
| 1001 |
png_content = hf_image_png(spec, index, run_id)
|
| 1002 |
+
source = HF_IMAGE_MODEL
|
| 1003 |
if png_content is None:
|
| 1004 |
png_content = local_asset_png(spec, index, run_id)
|
| 1005 |
source = "local style fallback"
|
| 1006 |
return (
|
| 1007 |
png_bytes_to_data_uri(png_content),
|
| 1008 |
write_gallery_image(png_content, spec.role),
|
| 1009 |
+
None if source == HF_IMAGE_MODEL else source,
|
| 1010 |
+
source,
|
| 1011 |
)
|
| 1012 |
|
| 1013 |
|
|
|
|
| 1153 |
return "\n\n".join(f"{spec.role}:\n{spec.prompt}" for spec in specs)
|
| 1154 |
|
| 1155 |
|
| 1156 |
+
def build_model_report(rows: list[tuple[str, str, str]]) -> str:
|
| 1157 |
+
return "\n".join(f"{role}: prompt={prompt_model}; image={image_model}" for role, prompt_model, image_model in rows)
|
| 1158 |
|
| 1159 |
|
| 1160 |
def generate_images_and_game(html_code: str, roles: str, style_hint: str):
|
| 1161 |
if not html_code.strip():
|
| 1162 |
return "", "Paste HTML game code first.", [], "", "", ""
|
| 1163 |
|
| 1164 |
+
role_lines, prompt_map, prompt_model = build_prompt_map(html_code, roles, style_hint or "pixel art style")
|
| 1165 |
+
specs = parse_assets(roles, style_hint or "pixel art style", prompt_map)
|
| 1166 |
if not specs:
|
| 1167 |
return html_code, "Add at least one asset role, like `player: brave knight`.", [], "", "", build_preview(html_code)
|
| 1168 |
|
|
|
|
| 1173 |
run_id = time.time_ns()
|
| 1174 |
|
| 1175 |
for index, spec in enumerate(specs):
|
| 1176 |
+
data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
|
| 1177 |
assets[spec.role] = data_uri
|
| 1178 |
gallery.append((gallery_path, f"{spec.role} -> {spec.filename}"))
|
| 1179 |
+
model_rows.append((spec.role, prompt_model, image_model))
|
| 1180 |
if error:
|
| 1181 |
errors.append(f"{spec.role}: fallback used ({error})")
|
| 1182 |
|
| 1183 |
rewritten = embed_assets(html_code, assets, specs)
|
| 1184 |
using_hf = bool(HF_TOKEN)
|
| 1185 |
+
source = f"prompt `{prompt_model}` + image `{HF_IMAGE_MODEL}`" if using_hf else "local prompt interpreter + local style fallback"
|
| 1186 |
status = f"Generated and embedded {len(specs)} fresh asset(s) using {source}. Run {str(run_id)[-6:]}."
|
| 1187 |
if errors:
|
| 1188 |
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])
|