Spaces:
Running on Zero
Running on Zero
Normalize generated sprite scale
Browse files
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
|
| 354 |
-
"
|
|
|
|
| 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,
|
| 1737 |
subject = spec.role.replace("_", " ")
|
| 1738 |
return (
|
| 1739 |
-
f"{camera} Single isolated {subject} subject. One complete body, one pose, centered and fully visible
|
| 1740 |
-
f"
|
| 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 =
|
| 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
|
| 1894 |
-
"""Return alpha-component areas and bounding boxes, largest first."""
|
| 1895 |
-
image =
|
| 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
|
| 1938 |
-
"""Return
|
| 1939 |
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
| 1940 |
-
|
| 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 |
-
|
| 1950 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <
|
| 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
|
|
|
|
| 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 |
)
|