LeafCat79 commited on
Commit
84d05ed
·
verified ·
1 Parent(s): 33fe473

Update app.py

Browse files

Make SSD-1B ZeroGPU the required image generator

Files changed (1) hide show
  1. app.py +223 -57
app.py CHANGED
@@ -22,6 +22,11 @@ try:
22
  except Exception:
23
  InferenceClient = None
24
 
 
 
 
 
 
25
 
26
  STARTER_HTML = """<!DOCTYPE html>
27
  <html>
@@ -116,6 +121,16 @@ class IntegrationReport:
116
 
117
 
118
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
 
 
 
 
 
 
 
 
 
 
119
  FREE_IMAGE_MODEL = os.environ.get("FREE_IMAGE_MODEL", "segmind/tiny-sd")
120
  FREE_IMAGE_STEPS = int(os.environ.get("FREE_IMAGE_STEPS", "5"))
121
  USE_HF_PROMPT_PROVIDER = os.environ.get("USE_HF_PROMPT_PROVIDER", "0") == "1"
@@ -136,6 +151,20 @@ FREE_DIFFUSION_PIPE = None
136
  FREE_DIFFUSION_ERROR = None
137
  CONTROLNET_PIPE = None
138
  CONTROLNET_ERROR = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
 
141
  def slugify(value: str) -> str:
@@ -530,7 +559,7 @@ def palette_for(text: str) -> list[tuple[int, int, int]]:
530
  (("sci-fi", "scifi", "space", "arena", "metal", "shooter"), [(30, 216, 236), (235, 64, 96), (18, 22, 31), (245, 192, 51)]),
531
  ]
532
  for words, colors in themed:
533
- if any(word in lowered for word in words):
534
  return colors
535
 
536
  digest = hashlib.sha256(text.encode("utf-8")).digest()
@@ -672,7 +701,7 @@ def draw_top_down_background(spec: AssetSpec, rng: random.Random) -> bytes:
672
  colors = palette_for(spec.prompt)
673
  width, height = spec.width, spec.height
674
 
675
- if any(word in prompt for word in ("sci-fi", "scifi", "cyberpunk", "space station", "spaceship hangar", "metal arena")):
676
  image = Image.new("RGBA", (width, height), (18, 22, 31, 255))
677
  draw = ImageDraw.Draw(image, "RGBA")
678
  panel = 64
@@ -690,7 +719,7 @@ def draw_top_down_background(spec: AssetSpec, rng: random.Random) -> bytes:
690
  draw.line((0, y, width, y), fill=(34, 215, 236, 95), width=2)
691
  return finish_png(image)
692
 
693
- if any(word in prompt for word in ("desert", "sand", "dune", "badlands", "wasteland", "canyon")):
694
  image = Image.new("RGBA", (width, height), (202, 166, 99, 255))
695
  draw = ImageDraw.Draw(image, "RGBA")
696
  for _ in range(26):
@@ -716,7 +745,7 @@ def draw_top_down_background(spec: AssetSpec, rng: random.Random) -> bytes:
716
  draw.line(shifted, fill=(105, 77, 48, 155), width=5, joint="curve")
717
  return finish_png(image)
718
 
719
- if any(word in prompt for word in ("garden", "flower", "grass", "meadow", "forest", "jungle", "path")):
720
  image = Image.new("RGBA", (width, height), (64, 132, 70, 255))
721
  draw = ImageDraw.Draw(image, "RGBA")
722
  for _ in range(85):
@@ -753,7 +782,7 @@ def draw_top_down_background(spec: AssetSpec, rng: random.Random) -> bytes:
753
  draw.ellipse((x - inner, y - inner, x + inner, y + inner), fill=(58, rng.randint(126, 166), 69, 245))
754
  return finish_png(image)
755
 
756
- if any(word in prompt for word in ("dungeon", "castle", "stone", "crypt")):
757
  image = Image.new("RGBA", (width, height), (48, 50, 62, 255))
758
  draw = ImageDraw.Draw(image, "RGBA")
759
  tile = 54
@@ -767,7 +796,7 @@ def draw_top_down_background(spec: AssetSpec, rng: random.Random) -> bytes:
767
  )
768
  return finish_png(image)
769
 
770
- if any(word in prompt for word in ("lava", "fire", "volcano")):
771
  image = Image.new("RGBA", (width, height), (52, 23, 18, 255))
772
  draw = ImageDraw.Draw(image, "RGBA")
773
  for _ in range(24):
@@ -781,7 +810,7 @@ def draw_top_down_background(spec: AssetSpec, rng: random.Random) -> bytes:
781
  draw.line(points, fill=(255, 202, 64, 210), width=rng.randint(2, 5), joint="curve")
782
  return finish_png(image)
783
 
784
- if any(word in prompt for word in ("ocean", "sea", "water", "underwater")):
785
  image = Image.new("RGBA", (width, height), (18, 114, 158, 255))
786
  draw = ImageDraw.Draw(image, "RGBA")
787
  for _ in range(42):
@@ -836,7 +865,7 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
836
  return draw_isometric_background(spec, rng)
837
 
838
  if (
839
- any(word in prompt for word in ("sci-fi", "scifi", "cyberpunk", "space station", "spaceship hangar", "metal arena", "hazard grid"))
840
  and not is_space_background_prompt(prompt)
841
  ):
842
  image = Image.new("RGBA", (spec.width, spec.height), (18, 22, 31, 255))
@@ -863,11 +892,6 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
863
  [(x + stripe, y), (x + stripe + 12, y), (x + stripe - 12, y + 34), (x + stripe - 24, y + 34)],
864
  fill=(245, 192, 51, 210),
865
  )
866
- for _ in range(18):
867
- x = rng.randint(0, spec.width)
868
- y = rng.randint(0, spec.height)
869
- r = rng.randint(10, 32)
870
- draw.ellipse((x - r, y - r, x + r, y + r), outline=(224, 54, 85, 100), width=2)
871
  out = io.BytesIO()
872
  image.save(out, format="PNG")
873
  return out.getvalue()
@@ -901,7 +925,7 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
901
  image.save(out, format="PNG")
902
  return out.getvalue()
903
 
904
- if any(word in prompt for word in ("garden", "flower", "grass", "meadow", "forest", "path")):
905
  sky = (184, 224, 239)
906
  grass = (80, 151, 82)
907
  image = Image.new("RGBA", (spec.width, spec.height), sky + (255,))
@@ -927,13 +951,16 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
927
  ],
928
  fill=path_color,
929
  )
930
- for _ in range(18):
931
- x = rng.randint(0, spec.width)
932
- y = rng.randint(horizon + 48, spec.height - 18)
933
- stem = rng.randint(5, 12)
934
- draw.line((x, y + stem, x, y), fill=(43, 109, 53, 210), width=2)
935
- petal = rng.choice(colors) + (230,)
936
- draw.ellipse((x - 4, y - 3, x + 4, y + 5), fill=petal)
 
 
 
937
  for _ in range(10):
938
  x = rng.randint(-40, spec.width)
939
  y = rng.randint(horizon - 35, horizon + 20)
@@ -942,7 +969,7 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
942
  image.save(out, format="PNG")
943
  return out.getvalue()
944
 
945
- if any(word in prompt for word in ("lava", "fire", "volcano")):
946
  image = Image.new("RGBA", (spec.width, spec.height), (52, 23, 18, 255))
947
  draw = ImageDraw.Draw(image, "RGBA")
948
  for y in range(0, spec.height, 58):
@@ -951,16 +978,20 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
951
  x = rng.randint(-80, spec.width)
952
  y = rng.randint(0, spec.height)
953
  draw.line((x, y, x + rng.randint(120, 320), y + rng.randint(-20, 20)), fill=(255, 111, 43, 220), width=rng.randint(5, 12))
954
- for _ in range(45):
955
- x = rng.randint(0, spec.width)
956
- y = rng.randint(0, spec.height)
957
- r = rng.randint(5, 20)
958
- draw.ellipse((x - r, y - r, x + r, y + r), fill=(255, 192, 53, rng.randint(70, 140)))
 
 
 
 
959
  out = io.BytesIO()
960
  image.save(out, format="PNG")
961
  return out.getvalue()
962
 
963
- if any(word in prompt for word in ("dungeon", "castle", "stone", "crypt")):
964
  image = Image.new("RGBA", (spec.width, spec.height), (48, 50, 62, 255))
965
  draw = ImageDraw.Draw(image, "RGBA")
966
  tile = 54
@@ -968,15 +999,11 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
968
  for x in range(0, spec.width, tile):
969
  shade = rng.randint(48, 78)
970
  draw.rectangle((x, y, x + tile - 2, y + tile - 2), fill=(shade, shade + 2, shade + 8, 255), outline=(27, 29, 38, 190))
971
- for _ in range(22):
972
- x = rng.randint(0, spec.width)
973
- y = rng.randint(0, spec.height)
974
- draw.ellipse((x - 8, y - 8, x + 8, y + 8), fill=(185, 146, 80, 110))
975
  out = io.BytesIO()
976
  image.save(out, format="PNG")
977
  return out.getvalue()
978
 
979
- if any(word in prompt for word in ("ocean", "sea", "water", "underwater")):
980
  image = Image.new("RGBA", (spec.width, spec.height), (8, 54, 92, 255))
981
  draw = ImageDraw.Draw(image, "RGBA")
982
  for y in range(spec.height):
@@ -1000,20 +1027,16 @@ def draw_background(spec: AssetSpec, rng: random.Random) -> bytes:
1000
  color = tuple(int(colors[2][i] * (1 - mix) + colors[0][i] * mix) for i in range(3))
1001
  draw.line((0, y, spec.width, y), fill=color + (255,))
1002
 
1003
- for _ in range(34):
1004
- x = rng.randint(-80, spec.width)
1005
- y = rng.randint(-40, spec.height)
1006
- w = rng.randint(80, 260)
1007
- h = rng.randint(36, 150)
1008
- fill = rng.choice(colors) + (rng.randint(45, 110),)
1009
- draw.ellipse((x, y, x + w, y + h), fill=fill)
1010
-
1011
- for _ in range(80):
1012
- x = rng.randint(0, spec.width)
1013
- y = rng.randint(0, spec.height)
1014
- size = rng.randint(2, 7)
1015
- fill = rng.choice(colors) + (rng.randint(90, 190),)
1016
- draw.rectangle((x, y, x + size, y + size), fill=fill)
1017
 
1018
  out = io.BytesIO()
1019
  image.save(out, format="PNG")
@@ -1449,7 +1472,8 @@ def diffusion_negative_prompt(spec: AssetSpec) -> str:
1449
  }.get(spec.camera, "")
1450
  return (
1451
  common
1452
- + ", person, character, player, hero, creature, monster, vehicle, mascot, foreground subject"
 
1453
  + camera_negative
1454
  )
1455
  return common + ", landscape, environment scene, room, horizon, opaque background, drop shadow"
@@ -1507,6 +1531,104 @@ def build_camera_control_image(spec: AssetSpec, width: int = 1024, height: int =
1507
  return image.convert("RGB")
1508
 
1509
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1510
  def controlnet_background_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
1511
  """Run SDXL Depth ControlNet when a CUDA Space explicitly enables it."""
1512
  global CONTROLNET_PIPE, CONTROLNET_ERROR
@@ -1677,7 +1799,7 @@ def hf_structure_background_png(spec: AssetSpec, index: int, run_id: int) -> tup
1677
 
1678
  def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str, str | None, str]:
1679
  is_background = is_background_spec(spec)
1680
- if not is_background and not USE_DIFFUSION_FOR_SPRITES and not USE_HF_IMAGE_PROVIDER:
1681
  png_content = local_asset_png(spec, index, run_id)
1682
  return (
1683
  png_bytes_to_data_uri(png_content),
@@ -1687,6 +1809,7 @@ def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str,
1687
  )
1688
  background_models_enabled = any(
1689
  (
 
1690
  USE_CONTROLNET_FOR_BACKGROUNDS,
1691
  USE_HF_STRUCTURE_PROVIDER,
1692
  USE_DIFFUSION_FOR_BACKGROUNDS,
@@ -1706,7 +1829,18 @@ def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str,
1706
  source = ""
1707
  errors: list[str] = []
1708
 
1709
- if is_background and USE_CONTROLNET_FOR_BACKGROUNDS:
 
 
 
 
 
 
 
 
 
 
 
1710
  png_content, error = controlnet_background_png(spec, index, run_id)
1711
  source = f"{CONTROLNET_BASE_MODEL} + {CONTROLNET_MODEL}"
1712
  if error:
@@ -1737,7 +1871,7 @@ def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str,
1737
  return (
1738
  png_bytes_to_data_uri(png_content),
1739
  write_gallery_image(png_content, spec.role),
1740
- "; ".join(errors) if source == "local procedural fallback" and errors else None,
1741
  source,
1742
  )
1743
 
@@ -1898,12 +2032,17 @@ def model_source_note(image_model: str) -> str:
1898
  return "depth-controlled diffusion—stronger camera structure; still requires visual perspective review"
1899
  if "via fal-ai" in lowered or "via replicate" in lowered:
1900
  return "routed image-to-image refinement—preserves a layout guide but consumes account inference credit"
 
 
1901
  return "diffusion output—higher visual variety; verify subject, perspective, alpha edges, and prompt compliance"
1902
 
1903
 
1904
  def model_configuration_summary() -> str:
1905
  prompt_source = HF_PROMPT_MODEL if USE_HF_PROMPT_PROVIDER else "local prompt interpreter"
1906
  sprite_chain = []
 
 
 
1907
  if USE_DIFFUSION_FOR_SPRITES:
1908
  sprite_chain.append(FREE_IMAGE_MODEL)
1909
  if USE_HF_IMAGE_PROVIDER:
@@ -1911,6 +2050,8 @@ def model_configuration_summary() -> str:
1911
  sprite_chain.append("shape-aware procedural renderer (no neural model)")
1912
  sprite_source = " → ".join(sprite_chain)
1913
  background_chain = []
 
 
1914
  if USE_CONTROLNET_FOR_BACKGROUNDS:
1915
  background_chain.append(f"{CONTROLNET_BASE_MODEL} + {CONTROLNET_MODEL}")
1916
  if USE_HF_STRUCTURE_PROVIDER:
@@ -1928,18 +2069,29 @@ def model_configuration_summary() -> str:
1928
  remote_sources.append(HF_IMAGE_MODEL)
1929
  remote_fallback = " → ".join(remote_sources) if remote_sources else "disabled"
1930
  neural_models = []
 
 
1931
  if USE_DIFFUSION_FOR_SPRITES or USE_DIFFUSION_FOR_BACKGROUNDS:
1932
  neural_models.append(FREE_IMAGE_MODEL)
1933
  if USE_CONTROLNET_FOR_BACKGROUNDS:
1934
  neural_models.append(f"{CONTROLNET_BASE_MODEL} + {CONTROLNET_MODEL}")
1935
  neural_models.extend(remote_sources)
1936
  neural_status = ", ".join(dict.fromkeys(neural_models)) if neural_models else "none active—procedural code only"
 
 
 
 
 
 
 
1937
  return (
1938
  "**Configured model pipeline:** "
1939
  f"prompts: `{prompt_source}` · sprites: `{sprite_source}` · backgrounds: `{background_source}` · "
1940
- f"remote image fallback: `{remote_fallback}` · neural image models: `{neural_status}`. "
1941
- "Every background carries an explicit camera contract. The free procedural path enforces that contract "
1942
- "deterministically; model-based refinements are more varied and still require visual perspective review."
 
 
1943
  )
1944
 
1945
 
@@ -2015,6 +2167,7 @@ def empty_generation_result(message: str, html_code: str = ""):
2015
  )
2016
 
2017
 
 
2018
  def generate_images_and_game(
2019
  html_code: str,
2020
  roles: str,
@@ -2050,14 +2203,17 @@ def generate_images_and_game(
2050
  errors.append(f"prompt model: {prompt_error}")
2051
 
2052
  for index, spec in enumerate(specs):
2053
- data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
 
 
 
2054
  assets[spec.role] = data_uri
2055
  gallery_paths[spec.role] = gallery_path
2056
  image_models[spec.role] = image_model
2057
  png_content = base64.b64decode(data_uri.split(",", 1)[1])
2058
  quality[spec.role] = validate_asset_png(png_content, spec)
2059
  if error:
2060
- errors.append(f"{spec.role}: image model failed ({error}); used local procedural fallback")
2061
 
2062
  state = {
2063
  "html_code": html_code,
@@ -2079,6 +2235,7 @@ def generate_images_and_game(
2079
  )
2080
 
2081
 
 
2082
  def regenerate_selected_asset(state: dict, selected_role: str, approved_roles: list[str]):
2083
  if not state or not selected_role:
2084
  return (
@@ -2101,7 +2258,16 @@ def regenerate_selected_asset(state: dict, selected_role: str, approved_roles: l
2101
 
2102
  spec = specs[index]
2103
  run_id = time.time_ns()
2104
- data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
 
 
 
 
 
 
 
 
 
2105
  state["assets"][selected_role] = data_uri
2106
  state["gallery_paths"][selected_role] = gallery_path
2107
  state["image_models"][selected_role] = image_model
 
22
  except Exception:
23
  InferenceClient = None
24
 
25
+ try:
26
+ import spaces as hf_spaces
27
+ except Exception:
28
+ hf_spaces = None
29
+
30
 
31
  STARTER_HTML = """<!DOCTYPE html>
32
  <html>
 
121
 
122
 
123
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
124
+ 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_GUIDANCE_SCALE = float(os.environ.get("PRIMARY_GUIDANCE_SCALE", "1.5"))
128
+ PRIMARY_BACKGROUND_STRENGTH = float(os.environ.get("PRIMARY_BACKGROUND_STRENGTH", "0.72"))
129
+ USE_PRIMARY_IMAGE_MODEL = os.environ.get("USE_PRIMARY_IMAGE_MODEL", "1") == "1"
130
+ REQUIRE_PRIMARY_IMAGE_MODEL = os.environ.get(
131
+ "REQUIRE_PRIMARY_IMAGE_MODEL",
132
+ "1" if os.environ.get("SPACE_ID") else "0",
133
+ ) == "1"
134
  FREE_IMAGE_MODEL = os.environ.get("FREE_IMAGE_MODEL", "segmind/tiny-sd")
135
  FREE_IMAGE_STEPS = int(os.environ.get("FREE_IMAGE_STEPS", "5"))
136
  USE_HF_PROMPT_PROVIDER = os.environ.get("USE_HF_PROMPT_PROVIDER", "0") == "1"
 
151
  FREE_DIFFUSION_ERROR = None
152
  CONTROLNET_PIPE = None
153
  CONTROLNET_ERROR = None
154
+ PRIMARY_TEXT_PIPE = None
155
+ PRIMARY_IMAGE_TO_IMAGE_PIPE = None
156
+ PRIMARY_MODEL_ERROR = None
157
+
158
+
159
+ def gpu_task(duration: int):
160
+ """Use a real ZeroGPU allocation on Hugging Face and remain importable in local tests."""
161
+ if hf_spaces is not None:
162
+ return hf_spaces.GPU(duration=duration)
163
+
164
+ def identity(function):
165
+ return function
166
+
167
+ return identity
168
 
169
 
170
  def slugify(value: str) -> str:
 
559
  (("sci-fi", "scifi", "space", "arena", "metal", "shooter"), [(30, 216, 236), (235, 64, 96), (18, 22, 31), (245, 192, 51)]),
560
  ]
561
  for words, colors in themed:
562
+ if contains_any_term(lowered, words):
563
  return colors
564
 
565
  digest = hashlib.sha256(text.encode("utf-8")).digest()
 
701
  colors = palette_for(spec.prompt)
702
  width, height = spec.width, spec.height
703
 
704
+ if contains_any_term(prompt, ("sci-fi", "scifi", "cyberpunk", "space station", "spaceship hangar", "metal arena")):
705
  image = Image.new("RGBA", (width, height), (18, 22, 31, 255))
706
  draw = ImageDraw.Draw(image, "RGBA")
707
  panel = 64
 
719
  draw.line((0, y, width, y), fill=(34, 215, 236, 95), width=2)
720
  return finish_png(image)
721
 
722
+ if contains_any_term(prompt, ("desert", "sand", "dune", "badlands", "wasteland", "canyon")):
723
  image = Image.new("RGBA", (width, height), (202, 166, 99, 255))
724
  draw = ImageDraw.Draw(image, "RGBA")
725
  for _ in range(26):
 
745
  draw.line(shifted, fill=(105, 77, 48, 155), width=5, joint="curve")
746
  return finish_png(image)
747
 
748
+ if contains_any_term(prompt, ("garden", "flower", "grass", "meadow", "forest", "jungle", "path")):
749
  image = Image.new("RGBA", (width, height), (64, 132, 70, 255))
750
  draw = ImageDraw.Draw(image, "RGBA")
751
  for _ in range(85):
 
782
  draw.ellipse((x - inner, y - inner, x + inner, y + inner), fill=(58, rng.randint(126, 166), 69, 245))
783
  return finish_png(image)
784
 
785
+ if contains_any_term(prompt, ("dungeon", "castle", "stone", "crypt")):
786
  image = Image.new("RGBA", (width, height), (48, 50, 62, 255))
787
  draw = ImageDraw.Draw(image, "RGBA")
788
  tile = 54
 
796
  )
797
  return finish_png(image)
798
 
799
+ if contains_any_term(prompt, ("lava", "fire", "volcano")):
800
  image = Image.new("RGBA", (width, height), (52, 23, 18, 255))
801
  draw = ImageDraw.Draw(image, "RGBA")
802
  for _ in range(24):
 
810
  draw.line(points, fill=(255, 202, 64, 210), width=rng.randint(2, 5), joint="curve")
811
  return finish_png(image)
812
 
813
+ if contains_any_term(prompt, ("ocean", "sea", "water", "underwater")):
814
  image = Image.new("RGBA", (width, height), (18, 114, 158, 255))
815
  draw = ImageDraw.Draw(image, "RGBA")
816
  for _ in range(42):
 
865
  return draw_isometric_background(spec, rng)
866
 
867
  if (
868
+ contains_any_term(prompt, ("sci-fi", "scifi", "cyberpunk", "space station", "spaceship hangar", "metal arena", "hazard grid"))
869
  and not is_space_background_prompt(prompt)
870
  ):
871
  image = Image.new("RGBA", (spec.width, spec.height), (18, 22, 31, 255))
 
892
  [(x + stripe, y), (x + stripe + 12, y), (x + stripe - 12, y + 34), (x + stripe - 24, y + 34)],
893
  fill=(245, 192, 51, 210),
894
  )
 
 
 
 
 
895
  out = io.BytesIO()
896
  image.save(out, format="PNG")
897
  return out.getvalue()
 
925
  image.save(out, format="PNG")
926
  return out.getvalue()
927
 
928
+ if contains_any_term(prompt, ("garden", "flower", "grass", "meadow", "forest", "path")):
929
  sky = (184, 224, 239)
930
  grass = (80, 151, 82)
931
  image = Image.new("RGBA", (spec.width, spec.height), sky + (255,))
 
951
  ],
952
  fill=path_color,
953
  )
954
+ # Small colored flowers easily read as a generic particle overlay. Only add
955
+ # them when the request explicitly calls for a flower-rich environment.
956
+ if contains_any_term(prompt, ("garden", "flower", "flowers", "meadow")):
957
+ for _ in range(18):
958
+ x = rng.randint(0, spec.width)
959
+ y = rng.randint(horizon + 48, spec.height - 18)
960
+ stem = rng.randint(5, 12)
961
+ draw.line((x, y + stem, x, y), fill=(43, 109, 53, 210), width=2)
962
+ petal = rng.choice(colors) + (230,)
963
+ draw.rectangle((x - 3, y - 2, x + 3, y + 3), fill=petal)
964
  for _ in range(10):
965
  x = rng.randint(-40, spec.width)
966
  y = rng.randint(horizon - 35, horizon + 20)
 
969
  image.save(out, format="PNG")
970
  return out.getvalue()
971
 
972
+ if contains_any_term(prompt, ("lava", "fire", "volcano")):
973
  image = Image.new("RGBA", (spec.width, spec.height), (52, 23, 18, 255))
974
  draw = ImageDraw.Draw(image, "RGBA")
975
  for y in range(0, spec.height, 58):
 
978
  x = rng.randint(-80, spec.width)
979
  y = rng.randint(0, spec.height)
980
  draw.line((x, y, x + rng.randint(120, 320), y + rng.randint(-20, 20)), fill=(255, 111, 43, 220), width=rng.randint(5, 12))
981
+ # Keep lava as connected terrain cracks. Random glowing circles caused a
982
+ # persistent confetti/particle layer in every lava-background trial.
983
+ for x in range(-40, spec.width + 80, 150):
984
+ y = int(spec.height * 0.68) + rng.randint(-24, 24)
985
+ draw.polygon(
986
+ ((x, y), (x + 86, y - 18), (x + 132, y + 4), (x + 52, y + 27)),
987
+ fill=(82, 42, 30, 245),
988
+ outline=(119, 55, 34, 255),
989
+ )
990
  out = io.BytesIO()
991
  image.save(out, format="PNG")
992
  return out.getvalue()
993
 
994
+ if contains_any_term(prompt, ("dungeon", "castle", "stone", "crypt")):
995
  image = Image.new("RGBA", (spec.width, spec.height), (48, 50, 62, 255))
996
  draw = ImageDraw.Draw(image, "RGBA")
997
  tile = 54
 
999
  for x in range(0, spec.width, tile):
1000
  shade = rng.randint(48, 78)
1001
  draw.rectangle((x, y, x + tile - 2, y + tile - 2), fill=(shade, shade + 2, shade + 8, 255), outline=(27, 29, 38, 190))
 
 
 
 
1002
  out = io.BytesIO()
1003
  image.save(out, format="PNG")
1004
  return out.getvalue()
1005
 
1006
+ if contains_any_term(prompt, ("ocean", "sea", "water", "underwater")):
1007
  image = Image.new("RGBA", (spec.width, spec.height), (8, 54, 92, 255))
1008
  draw = ImageDraw.Draw(image, "RGBA")
1009
  for y in range(spec.height):
 
1027
  color = tuple(int(colors[2][i] * (1 - mix) + colors[0][i] * mix) for i in range(3))
1028
  draw.line((0, y, spec.width, y), fill=color + (255,))
1029
 
1030
+ # Unknown themes still get large connected terrain bands rather than a field
1031
+ # of unrelated dots, which was repeatedly mistaken for a particle effect.
1032
+ for band in range(7):
1033
+ y = int(spec.height * (band + 1) / 8)
1034
+ offset = rng.randint(-28, 28)
1035
+ draw.line(
1036
+ ((0, y + offset), (spec.width, y - offset)),
1037
+ fill=colors[(band + 1) % len(colors)] + (105,),
1038
+ width=rng.randint(18, 46),
1039
+ )
 
 
 
 
1040
 
1041
  out = io.BytesIO()
1042
  image.save(out, format="PNG")
 
1472
  }.get(spec.camera, "")
1473
  return (
1474
  common
1475
+ + ", person, character, player, hero, creature, monster, vehicle, mascot, foreground subject, "
1476
+ "particle effect, particles, confetti, sparkles, floating dots, glowing orbs, embers, lens flare"
1477
  + camera_negative
1478
  )
1479
  return common + ", landscape, environment scene, room, horizon, opaque background, drop shadow"
 
1531
  return image.convert("RGB")
1532
 
1533
 
1534
+ def initialize_primary_image_model() -> None:
1535
+ """Load the production diffusion pipelines once under ZeroGPU CUDA emulation."""
1536
+ global PRIMARY_TEXT_PIPE, PRIMARY_IMAGE_TO_IMAGE_PIPE, PRIMARY_MODEL_ERROR
1537
+ if not USE_PRIMARY_IMAGE_MODEL or PRIMARY_TEXT_PIPE is not None or PRIMARY_MODEL_ERROR:
1538
+ return
1539
+ if not os.environ.get("SPACE_ID"):
1540
+ return
1541
+
1542
+ try:
1543
+ import torch
1544
+ from diffusers import AutoPipelineForImage2Image, AutoPipelineForText2Image, LCMScheduler
1545
+
1546
+ model_kwargs = {
1547
+ "torch_dtype": torch.float16,
1548
+ "variant": "fp16",
1549
+ "use_safetensors": True,
1550
+ }
1551
+ if HF_TOKEN:
1552
+ model_kwargs["token"] = HF_TOKEN
1553
+
1554
+ text_pipe = AutoPipelineForText2Image.from_pretrained(PRIMARY_IMAGE_MODEL, **model_kwargs)
1555
+ text_pipe.scheduler = LCMScheduler.from_config(text_pipe.scheduler.config)
1556
+ text_pipe.load_lora_weights(PRIMARY_LORA_MODEL)
1557
+ text_pipe.fuse_lora()
1558
+ text_pipe = text_pipe.to("cuda")
1559
+ if hasattr(text_pipe, "set_progress_bar_config"):
1560
+ text_pipe.set_progress_bar_config(disable=True)
1561
+ if hasattr(text_pipe, "enable_vae_slicing"):
1562
+ text_pipe.enable_vae_slicing()
1563
+
1564
+ image_pipe = AutoPipelineForImage2Image.from_pipe(text_pipe)
1565
+ image_pipe.scheduler = LCMScheduler.from_config(image_pipe.scheduler.config)
1566
+ if hasattr(image_pipe, "set_progress_bar_config"):
1567
+ image_pipe.set_progress_bar_config(disable=True)
1568
+
1569
+ PRIMARY_TEXT_PIPE = text_pipe
1570
+ PRIMARY_IMAGE_TO_IMAGE_PIPE = image_pipe
1571
+ except Exception as exc:
1572
+ PRIMARY_MODEL_ERROR = short_error(exc)
1573
+
1574
+
1575
+ def primary_diffusion_prompt(spec: AssetSpec) -> str:
1576
+ if is_background_spec(spec):
1577
+ return (
1578
+ f"{spec.prompt} Production-quality 2D game background only. Preserve the supplied camera layout "
1579
+ "and readable gameplay space. Use connected environmental forms and clean terrain structure. "
1580
+ "No characters and no decorative overlay."
1581
+ )
1582
+ return (
1583
+ f"{spec.prompt} One complete isolated game sprite only, centered, fully visible, readable silhouette, "
1584
+ "camera angle exactly as specified, on a plain uniform white studio background."
1585
+ )
1586
+
1587
+
1588
+ def primary_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
1589
+ """Generate every production asset with SSD-1B; backgrounds use camera-guided img2img."""
1590
+ if PRIMARY_MODEL_ERROR:
1591
+ return None, PRIMARY_MODEL_ERROR
1592
+ if PRIMARY_TEXT_PIPE is None:
1593
+ initialize_primary_image_model()
1594
+ if PRIMARY_TEXT_PIPE is None:
1595
+ return None, "primary ZeroGPU diffusion model is not initialized on this runtime"
1596
+
1597
+ try:
1598
+ import torch
1599
+
1600
+ seed = abs(hash(f"primary|{spec.role}|{spec.prompt}|{index}|{run_id}")) % 2147483647
1601
+ generator = torch.Generator(device="cuda").manual_seed(seed)
1602
+ common = {
1603
+ "prompt": primary_diffusion_prompt(spec),
1604
+ "negative_prompt": diffusion_negative_prompt(spec),
1605
+ "num_inference_steps": PRIMARY_IMAGE_STEPS,
1606
+ "guidance_scale": PRIMARY_GUIDANCE_SCALE,
1607
+ "generator": generator,
1608
+ }
1609
+ if is_background_spec(spec):
1610
+ if PRIMARY_IMAGE_TO_IMAGE_PIPE is None:
1611
+ return None, "primary camera-guided image-to-image pipeline is unavailable"
1612
+ guide_png = local_asset_png(spec, index, run_id)
1613
+ guide = Image.open(io.BytesIO(guide_png)).convert("RGB").resize((1344, 768), Image.LANCZOS)
1614
+ image = PRIMARY_IMAGE_TO_IMAGE_PIPE(
1615
+ image=guide,
1616
+ strength=PRIMARY_BACKGROUND_STRENGTH,
1617
+ **common,
1618
+ ).images[0]
1619
+ else:
1620
+ image = PRIMARY_TEXT_PIPE(width=1024, height=1024, **common).images[0]
1621
+ return polish_diffusion_asset(image, spec), None
1622
+ except Exception as exc:
1623
+ return None, short_error(exc)
1624
+
1625
+
1626
+ # Hugging Face ZeroGPU virtualizes CUDA placement during module import. Loading
1627
+ # here keeps the weights resident between decorated requests instead of paying the
1628
+ # model-load cost on every generation.
1629
+ initialize_primary_image_model()
1630
+
1631
+
1632
  def controlnet_background_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
1633
  """Run SDXL Depth ControlNet when a CUDA Space explicitly enables it."""
1634
  global CONTROLNET_PIPE, CONTROLNET_ERROR
 
1799
 
1800
  def generate_asset(spec: AssetSpec, index: int, run_id: int) -> tuple[str, str, str | None, str]:
1801
  is_background = is_background_spec(spec)
1802
+ if not is_background and not USE_PRIMARY_IMAGE_MODEL and not USE_DIFFUSION_FOR_SPRITES and not USE_HF_IMAGE_PROVIDER:
1803
  png_content = local_asset_png(spec, index, run_id)
1804
  return (
1805
  png_bytes_to_data_uri(png_content),
 
1809
  )
1810
  background_models_enabled = any(
1811
  (
1812
+ USE_PRIMARY_IMAGE_MODEL,
1813
  USE_CONTROLNET_FOR_BACKGROUNDS,
1814
  USE_HF_STRUCTURE_PROVIDER,
1815
  USE_DIFFUSION_FOR_BACKGROUNDS,
 
1829
  source = ""
1830
  errors: list[str] = []
1831
 
1832
+ if USE_PRIMARY_IMAGE_MODEL:
1833
+ png_content, error = primary_diffusion_png(spec, index, run_id)
1834
+ source = f"{PRIMARY_IMAGE_MODEL} + {PRIMARY_LORA_MODEL} on ZeroGPU"
1835
+ if error:
1836
+ errors.append(f"primary ZeroGPU diffusion: {error}")
1837
+ if REQUIRE_PRIMARY_IMAGE_MODEL:
1838
+ raise RuntimeError(
1839
+ "Neural image generation is required, but the primary model failed. "
1840
+ f"No procedural image was returned. Details: {error}"
1841
+ )
1842
+
1843
+ if png_content is None and is_background and USE_CONTROLNET_FOR_BACKGROUNDS:
1844
  png_content, error = controlnet_background_png(spec, index, run_id)
1845
  source = f"{CONTROLNET_BASE_MODEL} + {CONTROLNET_MODEL}"
1846
  if error:
 
1871
  return (
1872
  png_bytes_to_data_uri(png_content),
1873
  write_gallery_image(png_content, spec.role),
1874
+ "; ".join(errors) if errors and "ZeroGPU" not in source else None,
1875
  source,
1876
  )
1877
 
 
2032
  return "depth-controlled diffusion—stronger camera structure; still requires visual perspective review"
2033
  if "via fal-ai" in lowered or "via replicate" in lowered:
2034
  return "routed image-to-image refinement—preserves a layout guide but consumes account inference credit"
2035
+ if "zerogpu" in lowered:
2036
+ return "locally hosted neural diffusion—no per-image API credit; backgrounds are camera-guided and still require visual review"
2037
  return "diffusion output—higher visual variety; verify subject, perspective, alpha edges, and prompt compliance"
2038
 
2039
 
2040
  def model_configuration_summary() -> str:
2041
  prompt_source = HF_PROMPT_MODEL if USE_HF_PROMPT_PROVIDER else "local prompt interpreter"
2042
  sprite_chain = []
2043
+ primary_source = f"{PRIMARY_IMAGE_MODEL} + {PRIMARY_LORA_MODEL} on ZeroGPU"
2044
+ if USE_PRIMARY_IMAGE_MODEL:
2045
+ sprite_chain.append(primary_source)
2046
  if USE_DIFFUSION_FOR_SPRITES:
2047
  sprite_chain.append(FREE_IMAGE_MODEL)
2048
  if USE_HF_IMAGE_PROVIDER:
 
2050
  sprite_chain.append("shape-aware procedural renderer (no neural model)")
2051
  sprite_source = " → ".join(sprite_chain)
2052
  background_chain = []
2053
+ if USE_PRIMARY_IMAGE_MODEL:
2054
+ background_chain.append(f"camera-guided {primary_source}")
2055
  if USE_CONTROLNET_FOR_BACKGROUNDS:
2056
  background_chain.append(f"{CONTROLNET_BASE_MODEL} + {CONTROLNET_MODEL}")
2057
  if USE_HF_STRUCTURE_PROVIDER:
 
2069
  remote_sources.append(HF_IMAGE_MODEL)
2070
  remote_fallback = " → ".join(remote_sources) if remote_sources else "disabled"
2071
  neural_models = []
2072
+ if USE_PRIMARY_IMAGE_MODEL:
2073
+ neural_models.append(f"{PRIMARY_IMAGE_MODEL} + {PRIMARY_LORA_MODEL}")
2074
  if USE_DIFFUSION_FOR_SPRITES or USE_DIFFUSION_FOR_BACKGROUNDS:
2075
  neural_models.append(FREE_IMAGE_MODEL)
2076
  if USE_CONTROLNET_FOR_BACKGROUNDS:
2077
  neural_models.append(f"{CONTROLNET_BASE_MODEL} + {CONTROLNET_MODEL}")
2078
  neural_models.extend(remote_sources)
2079
  neural_status = ", ".join(dict.fromkeys(neural_models)) if neural_models else "none active—procedural code only"
2080
+ enforcement = "required; generation stops on model failure" if REQUIRE_PRIMARY_IMAGE_MODEL else "development fallback allowed"
2081
+ if PRIMARY_MODEL_ERROR:
2082
+ readiness = f"startup error: {PRIMARY_MODEL_ERROR}"
2083
+ elif PRIMARY_TEXT_PIPE is not None:
2084
+ readiness = "loaded on ZeroGPU"
2085
+ else:
2086
+ readiness = "loads in the deployed ZeroGPU runtime"
2087
  return (
2088
  "**Configured model pipeline:** "
2089
  f"prompts: `{prompt_source}` · sprites: `{sprite_source}` · backgrounds: `{background_source}` · "
2090
+ f"remote image fallback: `{remote_fallback}` · neural image models: `{neural_status}` · "
2091
+ f"neural enforcement: `{enforcement}` · primary readiness: `{readiness}`. "
2092
+ "Every production image is generated by the primary neural model. Backgrounds start from a deterministic "
2093
+ "camera guide and are refined with image-to-image diffusion; the procedural renderer remains an emergency "
2094
+ "development fallback and is blocked in the deployed Space when the primary model fails."
2095
  )
2096
 
2097
 
 
2167
  )
2168
 
2169
 
2170
+ @gpu_task(duration=120)
2171
  def generate_images_and_game(
2172
  html_code: str,
2173
  roles: str,
 
2203
  errors.append(f"prompt model: {prompt_error}")
2204
 
2205
  for index, spec in enumerate(specs):
2206
+ try:
2207
+ data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
2208
+ except RuntimeError as exc:
2209
+ return empty_generation_result(str(exc), html_code)
2210
  assets[spec.role] = data_uri
2211
  gallery_paths[spec.role] = gallery_path
2212
  image_models[spec.role] = image_model
2213
  png_content = base64.b64decode(data_uri.split(",", 1)[1])
2214
  quality[spec.role] = validate_asset_png(png_content, spec)
2215
  if error:
2216
+ errors.append(f"{spec.role}: generation fallback used ({error}); actual source: {image_model}")
2217
 
2218
  state = {
2219
  "html_code": html_code,
 
2235
  )
2236
 
2237
 
2238
+ @gpu_task(duration=90)
2239
  def regenerate_selected_asset(state: dict, selected_role: str, approved_roles: list[str]):
2240
  if not state or not selected_role:
2241
  return (
 
2258
 
2259
  spec = specs[index]
2260
  run_id = time.time_ns()
2261
+ try:
2262
+ data_uri, gallery_path, error, image_model = generate_asset(spec, index, run_id)
2263
+ except RuntimeError as exc:
2264
+ rendered = render_generation_state(state, "Kept", [str(exc)])
2265
+ roles = [item.role for item in specs]
2266
+ return (
2267
+ *rendered,
2268
+ state,
2269
+ gr.CheckboxGroup(choices=roles, value=approved_roles or []),
2270
+ )
2271
  state["assets"][selected_role] = data_uri
2272
  state["gallery_paths"][selected_role] = gallery_path
2273
  state["image_models"][selected_role] = image_model