LeafCat79 commited on
Commit
f905190
·
verified ·
1 Parent(s): 0758bcb

Normalize generated sprite scale

Browse files
Files changed (1) hide show
  1. app.py +96 -17
app.py CHANGED
@@ -350,8 +350,9 @@ def build_asset_prompt(role: str, prompt: str, style_hint: str) -> str:
350
  else:
351
  asset_instruction = (
352
  "Create a complete standalone 2D game character or object as a single centered subject. "
353
- "Present one complete body in one pose from the required camera direction, fully visible with broad empty "
354
- "margin on every side. Use a clean uniform white studio field and a readable game-scale silhouette."
 
355
  )
356
  return (
357
  f"{role} asset: {prompt}. Creative brief: {style_hint}. "
@@ -1733,11 +1734,11 @@ def primary_diffusion_prompt(spec: AssetSpec) -> str:
1733
  f"{camera} Empty playable 2D game environment with connected terrain and clear gameplay space. "
1734
  f"{description}"
1735
  )
1736
- description = compact_prompt_words(spec.prompt, 24)
1737
  subject = spec.role.replace("_", " ")
1738
  return (
1739
- f"{camera} Single isolated {subject} subject. One complete body, one pose, centered and fully visible with "
1740
- f"broad empty margins on a uniform white field. {description}"
1741
  )
1742
 
1743
 
@@ -1784,6 +1785,9 @@ def primary_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[byt
1784
  if has_implausibly_thin_foreground_subject(content, spec):
1785
  last_failure_detail = "the last output contained an implausibly thin foreground silhouette"
1786
  continue
 
 
 
1787
  return content, None
1788
 
1789
  return None, f"sprite validation failed after {attempts} model attempts; {last_failure_detail}"
@@ -1882,7 +1886,7 @@ def polish_diffusion_asset(image: Image.Image, spec: AssetSpec) -> bytes:
1882
  elif dist < 125:
1883
  a = max(0, min(a, (dist - 64) * 4))
1884
  pixels[x, y] = (r, g, b, a)
1885
- image = small.resize((spec.width, spec.height), Image.LANCZOS)
1886
  else:
1887
  image = image.resize((spec.width, spec.height), Image.LANCZOS)
1888
  out = io.BytesIO()
@@ -1890,9 +1894,9 @@ def polish_diffusion_asset(image: Image.Image, spec: AssetSpec) -> bytes:
1890
  return out.getvalue()
1891
 
1892
 
1893
- def foreground_component_geometry(content: bytes) -> list[tuple[int, tuple[int, int, int, int]]]:
1894
- """Return alpha-component areas and bounding boxes, largest first."""
1895
- image = Image.open(io.BytesIO(content)).convert("RGBA")
1896
  width, height = image.size
1897
  alpha = image.getchannel("A").tobytes()
1898
  foreground = bytearray(1 if value >= 64 else 0 for value in alpha)
@@ -1934,20 +1938,80 @@ def foreground_component_geometry(content: bytes) -> list[tuple[int, tuple[int,
1934
  return sorted(components, reverse=True)
1935
 
1936
 
1937
- def significant_foreground_component_areas(content: bytes) -> list[int]:
1938
- """Return material alpha components, ignoring only genuinely small detached details."""
1939
  image = Image.open(io.BytesIO(content)).convert("RGBA")
1940
- width, height = image.size
1941
- components = foreground_component_geometry(content)
1942
 
 
 
 
 
 
 
 
1943
  if not components:
1944
  return []
1945
  largest = components[0][0]
 
 
 
 
 
 
 
1946
  # A generated miniature duplicate can be much smaller than the primary view.
1947
  # Keep a small absolute/noise floor, but do not discard material components
1948
  # such as the reported 96px duplicate next to a 487px main silhouette.
1949
- minimum = max(64, int(largest * 0.12), int(width * height * 0.004))
1950
- return [area for area, _ in components if area >= minimum]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1951
 
1952
 
1953
  def has_deep_vertical_foreground_valley(content: bytes) -> bool:
@@ -2012,12 +2076,24 @@ def has_implausibly_thin_foreground_subject(content: bytes, spec: AssetSpec) ->
2012
  bbox_width = x1 - x0 + 1
2013
  bbox_height = y1 - y0 + 1
2014
  return (
2015
- bbox_width < max(14, int(image.width * 0.12))
2016
  and bbox_height >= int(image.height * 0.28)
2017
  and bbox_height >= bbox_width * 3.5
2018
  )
2019
 
2020
 
 
 
 
 
 
 
 
 
 
 
 
 
2021
  def free_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
2022
  global FREE_DIFFUSION_PIPE, FREE_DIFFUSION_ERROR
2023
  if FREE_DIFFUSION_ERROR:
@@ -2270,6 +2346,8 @@ def validate_asset_png(content: bytes, spec: AssetSpec) -> list[str]:
2270
  warnings.append(f"sprite contains {subject_count} significant foreground subjects")
2271
  elif has_implausibly_thin_foreground_subject(content, spec):
2272
  warnings.append("sprite foreground silhouette is implausibly thin")
 
 
2273
  return warnings
2274
 
2275
 
@@ -2409,7 +2487,8 @@ def model_configuration_summary() -> str:
2409
  f"neural enforcement: `{enforcement}` · primary readiness: `{readiness}`. "
2410
  "Every production image is generated directly from its written prompt by the primary text-to-image model. "
2411
  "Critical camera and subject constraints are placed first inside a conservative CLIP prompt budget. "
2412
- "Sprite outputs must pass single-subject alpha-component validation and are regenerated with a new seed up to "
 
2413
  f"{PRIMARY_SPRITE_ATTEMPTS} times. No procedural guide is supplied to the model. The procedural renderer is "
2414
  "development-only and is blocked in the deployed Space when the primary model fails."
2415
  )
 
350
  else:
351
  asset_instruction = (
352
  "Create a complete standalone 2D game character or object as a single centered subject. "
353
+ "Present one complete body in one pose from the required camera direction. Make the visible subject fill "
354
+ "about eighty percent of the frame with modest even margins. Use a clean uniform white studio field and "
355
+ "a readable game-scale silhouette."
356
  )
357
  return (
358
  f"{role} asset: {prompt}. Creative brief: {style_hint}. "
 
1734
  f"{camera} Empty playable 2D game environment with connected terrain and clear gameplay space. "
1735
  f"{description}"
1736
  )
1737
+ description = compact_prompt_words(spec.prompt, 18)
1738
  subject = spec.role.replace("_", " ")
1739
  return (
1740
+ f"{camera} Single isolated {subject} subject. One complete body, one pose, centered and fully visible. "
1741
+ f"Fill about eighty percent of the frame with modest even margins on a uniform white field. {description}"
1742
  )
1743
 
1744
 
 
1785
  if has_implausibly_thin_foreground_subject(content, spec):
1786
  last_failure_detail = "the last output contained an implausibly thin foreground silhouette"
1787
  continue
1788
+ if has_undersized_foreground_subject(content):
1789
+ last_failure_detail = "the last output foreground occupied too little of the sprite canvas"
1790
+ continue
1791
  return content, None
1792
 
1793
  return None, f"sprite validation failed after {attempts} model attempts; {last_failure_detail}"
 
1886
  elif dist < 125:
1887
  a = max(0, min(a, (dist - 64) * 4))
1888
  pixels[x, y] = (r, g, b, a)
1889
+ image = normalize_sprite_foreground(small, spec)
1890
  else:
1891
  image = image.resize((spec.width, spec.height), Image.LANCZOS)
1892
  out = io.BytesIO()
 
1894
  return out.getvalue()
1895
 
1896
 
1897
+ def foreground_component_geometry_from_image(image: Image.Image) -> list[tuple[int, tuple[int, int, int, int]]]:
1898
+ """Return alpha-component areas and bounding boxes for an RGBA image, largest first."""
1899
+ image = image.convert("RGBA")
1900
  width, height = image.size
1901
  alpha = image.getchannel("A").tobytes()
1902
  foreground = bytearray(1 if value >= 64 else 0 for value in alpha)
 
1938
  return sorted(components, reverse=True)
1939
 
1940
 
1941
+ def foreground_component_geometry(content: bytes) -> list[tuple[int, tuple[int, int, int, int]]]:
1942
+ """Return alpha-component areas and bounding boxes, largest first."""
1943
  image = Image.open(io.BytesIO(content)).convert("RGBA")
1944
+ return foreground_component_geometry_from_image(image)
 
1945
 
1946
+
1947
+ def material_foreground_components_from_image(
1948
+ image: Image.Image,
1949
+ ) -> list[tuple[int, tuple[int, int, int, int]]]:
1950
+ """Filter alpha components with the same material/noise contract used by validation."""
1951
+ image = image.convert("RGBA")
1952
+ components = foreground_component_geometry_from_image(image)
1953
  if not components:
1954
  return []
1955
  largest = components[0][0]
1956
+ minimum = max(64, int(largest * 0.12), int(image.width * image.height * 0.004))
1957
+ return [(area, bbox) for area, bbox in components if area >= minimum]
1958
+
1959
+
1960
+ def significant_foreground_component_areas(content: bytes) -> list[int]:
1961
+ """Return material alpha components, ignoring only genuinely small detached details."""
1962
+ image = Image.open(io.BytesIO(content)).convert("RGBA")
1963
  # A generated miniature duplicate can be much smaller than the primary view.
1964
  # Keep a small absolute/noise floor, but do not discard material components
1965
  # such as the reported 96px duplicate next to a 487px main silhouette.
1966
+ return [area for area, _ in material_foreground_components_from_image(image)]
1967
+
1968
+
1969
+ def material_foreground_bbox_from_image(image: Image.Image) -> tuple[int, int, int, int] | None:
1970
+ """Return the union bounding box for all material foreground components."""
1971
+ components = material_foreground_components_from_image(image)
1972
+ if not components:
1973
+ return None
1974
+ boxes = [bbox for _, bbox in components]
1975
+ return (
1976
+ min(box[0] for box in boxes),
1977
+ min(box[1] for box in boxes),
1978
+ max(box[2] for box in boxes),
1979
+ max(box[3] for box in boxes),
1980
+ )
1981
+
1982
+
1983
+ def normalize_sprite_foreground(image: Image.Image, spec: AssetSpec, target_occupancy: float = 0.88) -> Image.Image:
1984
+ """Scale the model foreground to a stable game-sprite occupancy without changing its shape."""
1985
+ image = image.convert("RGBA")
1986
+ bbox = material_foreground_bbox_from_image(image)
1987
+ if bbox is None:
1988
+ return image.resize((spec.width, spec.height), Image.LANCZOS)
1989
+
1990
+ x0, y0, x1, y1 = bbox
1991
+ padding = max(1, int(min(x1 - x0 + 1, y1 - y0 + 1) * 0.02))
1992
+ crop_box = (
1993
+ max(0, x0 - padding),
1994
+ max(0, y0 - padding),
1995
+ min(image.width, x1 + padding + 1),
1996
+ min(image.height, y1 + padding + 1),
1997
+ )
1998
+ subject = image.crop(crop_box)
1999
+ target_width = max(1, int(spec.width * target_occupancy))
2000
+ target_height = max(1, int(spec.height * target_occupancy))
2001
+ scale = min(target_width / subject.width, target_height / subject.height)
2002
+ resized_width = max(1, int(round(subject.width * scale)))
2003
+ resized_height = max(1, int(round(subject.height * scale)))
2004
+ subject = subject.resize((resized_width, resized_height), Image.LANCZOS)
2005
+
2006
+ normalized = Image.new("RGBA", (spec.width, spec.height), (0, 0, 0, 0))
2007
+ x = (spec.width - resized_width) // 2
2008
+ if spec.camera == "top_down":
2009
+ y = (spec.height - resized_height) // 2
2010
+ else:
2011
+ bottom_margin = max(2, int(spec.height * 0.06))
2012
+ y = max(0, spec.height - resized_height - bottom_margin)
2013
+ normalized.alpha_composite(subject, dest=(x, y))
2014
+ return normalized
2015
 
2016
 
2017
  def has_deep_vertical_foreground_valley(content: bytes) -> bool:
 
2076
  bbox_width = x1 - x0 + 1
2077
  bbox_height = y1 - y0 + 1
2078
  return (
2079
+ bbox_width < int(image.width * 0.30)
2080
  and bbox_height >= int(image.height * 0.28)
2081
  and bbox_height >= bbox_width * 3.5
2082
  )
2083
 
2084
 
2085
+ def has_undersized_foreground_subject(content: bytes) -> bool:
2086
+ """Detect sprites whose visible model foreground occupies too little of its game canvas."""
2087
+ image = Image.open(io.BytesIO(content)).convert("RGBA")
2088
+ bbox = material_foreground_bbox_from_image(image)
2089
+ if bbox is None:
2090
+ return True
2091
+ x0, y0, x1, y1 = bbox
2092
+ width_ratio = (x1 - x0 + 1) / max(1, image.width)
2093
+ height_ratio = (y1 - y0 + 1) / max(1, image.height)
2094
+ return max(width_ratio, height_ratio) < 0.68
2095
+
2096
+
2097
  def free_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
2098
  global FREE_DIFFUSION_PIPE, FREE_DIFFUSION_ERROR
2099
  if FREE_DIFFUSION_ERROR:
 
2346
  warnings.append(f"sprite contains {subject_count} significant foreground subjects")
2347
  elif has_implausibly_thin_foreground_subject(content, spec):
2348
  warnings.append("sprite foreground silhouette is implausibly thin")
2349
+ elif has_undersized_foreground_subject(content):
2350
+ warnings.append("sprite foreground occupies too little of the sprite canvas")
2351
  return warnings
2352
 
2353
 
 
2487
  f"neural enforcement: `{enforcement}` · primary readiness: `{readiness}`. "
2488
  "Every production image is generated directly from its written prompt by the primary text-to-image model. "
2489
  "Critical camera and subject constraints are placed first inside a conservative CLIP prompt budget. "
2490
+ "Sprite foregrounds are tightly normalized to a consistent game-canvas occupancy, then must pass subject-count, "
2491
+ "silhouette, and scale validation; failures are regenerated with a new seed up to "
2492
  f"{PRIMARY_SPRITE_ATTEMPTS} times. No procedural guide is supplied to the model. The procedural renderer is "
2493
  "development-only and is blocked in the deployed Space when the primary model fails."
2494
  )