LeafCat79 commited on
Commit
eed87b4
·
verified ·
1 Parent(s): e2b36e5

Use AI prompt and image models

Browse files
Files changed (1) hide show
  1. app.py +129 -13
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 parse_assets(raw_roles: str, style_hint: str) -> list[AssetSpec]:
195
- specs: list[AssetSpec] = []
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 = "hf"
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 == "hf" else 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}: {source}" for role, source in rows)
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
- specs = parse_assets(roles, style_hint or "pixel art style")
 
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, HF_IMAGE_MODEL if error is None and HF_TOKEN else "local style fallback"))
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"HF image model `{HF_IMAGE_MODEL}`" if using_hf else "local style fallback"
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])