Spaces:
Running on Zero
Running on Zero
Enforce top-down sprite perspective
Browse files
app.py
CHANGED
|
@@ -50,6 +50,7 @@ class AssetSpec:
|
|
| 50 |
variation: str = ""
|
| 51 |
expected_subjects: int | None = 1
|
| 52 |
silhouette: str = "any"
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
@dataclass
|
|
@@ -183,6 +184,23 @@ def compact_prompt_words(text: str, limit: int) -> str:
|
|
| 183 |
return " ".join(words[:limit])
|
| 184 |
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
def primary_camera_phrase(camera: str, is_background: bool) -> str:
|
| 187 |
if camera == "top_down":
|
| 188 |
return "Strict orthographic overhead view directly from above."
|
|
@@ -718,6 +736,7 @@ def parse_assets(
|
|
| 718 |
filename=filename,
|
| 719 |
width=width,
|
| 720 |
height=height,
|
|
|
|
| 721 |
camera=camera,
|
| 722 |
group=group,
|
| 723 |
variant_index=variant_index,
|
|
@@ -1758,18 +1777,21 @@ def diffusion_dimensions(spec: AssetSpec) -> tuple[int, int]:
|
|
| 1758 |
|
| 1759 |
def diffusion_negative_prompt(spec: AssetSpec) -> str:
|
| 1760 |
camera_negative = {
|
| 1761 |
-
"top_down":
|
|
|
|
|
|
|
|
|
|
| 1762 |
"isometric": ", eye-level view, front view, inconsistent perspective, horizon",
|
| 1763 |
"side_view": ", overhead view, top-down view, isometric view",
|
| 1764 |
}.get(spec.camera, "")
|
| 1765 |
if spec.composition in {"single_subject", "icon", "animation_frame"}:
|
| 1766 |
-
|
| 1767 |
-
"multiple subjects, duplicate character, repeated subject, character sheet, turnaround, lineup,
|
| 1768 |
-
"multiple poses, cropped subject, scenery, landscape, environment,
|
| 1769 |
-
"background props, drop shadow, text, watermark, blurry, soft focus, out of focus, low detail
|
| 1770 |
-
"compression artifacts"
|
| 1771 |
-
+ camera_negative
|
| 1772 |
)
|
|
|
|
|
|
|
| 1773 |
if spec.composition == "seamless":
|
| 1774 |
return "visible seams, borders, frame, perspective mockup, text, watermark, blurry, soft focus, low detail" + camera_negative
|
| 1775 |
if spec.composition == "sprite_sheet":
|
|
@@ -1920,9 +1942,17 @@ def primary_diffusion_prompt(spec: AssetSpec) -> str:
|
|
| 1920 |
camera = f"User-defined camera: {compact_prompt_words(spec.camera_instruction, 10)}."
|
| 1921 |
variation = f"Distinct variation: {compact_prompt_words(spec.variation, 8)}. " if spec.variation else ""
|
| 1922 |
if spec.composition == "single_subject":
|
| 1923 |
-
description =
|
| 1924 |
variation = f"Variation: {compact_prompt_words(spec.variation, 4)}. " if spec.variation else ""
|
| 1925 |
subject = (spec.group or spec.role).replace("_", " ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1926 |
return (
|
| 1927 |
f"{camera} Single isolated {subject} subject. One complete centered body in one pose. "
|
| 1928 |
"Fill eighty percent of the frame with even margins. "
|
|
@@ -1944,7 +1974,13 @@ def primary_diffusion_prompt(spec: AssetSpec) -> str:
|
|
| 1944 |
|
| 1945 |
def primary_generation_dimensions(spec: AssetSpec) -> tuple[int, int]:
|
| 1946 |
if not is_background_spec(spec):
|
| 1947 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1948 |
aspect = spec.width / max(1, spec.height)
|
| 1949 |
if aspect >= 1.35:
|
| 1950 |
return 1344, 768
|
|
@@ -2016,6 +2052,11 @@ def primary_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[byt
|
|
| 2016 |
if has_implausibly_thin_foreground_subject(content, spec):
|
| 2017 |
last_failure_detail = "the last output contained an implausibly thin foreground silhouette"
|
| 2018 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2019 |
if has_undersized_foreground_subject(content):
|
| 2020 |
last_failure_detail = "the last output foreground occupied too little of the sprite canvas"
|
| 2021 |
continue
|
|
@@ -2359,6 +2400,65 @@ def has_implausibly_thin_foreground_subject(content: bytes, spec: AssetSpec) ->
|
|
| 2359 |
)
|
| 2360 |
|
| 2361 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2362 |
def has_undersized_foreground_subject(content: bytes) -> bool:
|
| 2363 |
"""Detect sprites whose visible model foreground occupies too little of its game canvas."""
|
| 2364 |
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
|
@@ -2673,6 +2773,8 @@ def validate_asset_png(content: bytes, spec: AssetSpec) -> list[str]:
|
|
| 2673 |
)
|
| 2674 |
elif spec.expected_subjects == 1 and has_implausibly_thin_foreground_subject(content, spec):
|
| 2675 |
warnings.append("sprite foreground silhouette is implausibly thin")
|
|
|
|
|
|
|
| 2676 |
elif spec.expected_subjects == 1 and has_undersized_foreground_subject(content):
|
| 2677 |
warnings.append("foreground subject occupies too little of the output canvas")
|
| 2678 |
if spec.composition == "seamless" and image.width > 1 and image.height > 1:
|
|
|
|
| 50 |
variation: str = ""
|
| 51 |
expected_subjects: int | None = 1
|
| 52 |
silhouette: str = "any"
|
| 53 |
+
description: str = ""
|
| 54 |
|
| 55 |
|
| 56 |
@dataclass
|
|
|
|
| 184 |
return " ".join(words[:limit])
|
| 185 |
|
| 186 |
|
| 187 |
+
def compact_subject_description(text: str, limit: int) -> str:
|
| 188 |
+
"""Keep user subject details while removing camera/composition words supplied elsewhere."""
|
| 189 |
+
value = text or ""
|
| 190 |
+
value = re.sub(r"^\s*(?:one\s+single|single|one)\s+", "", value, flags=re.I)
|
| 191 |
+
value = re.sub(
|
| 192 |
+
r"\b(?:viewed|seen)\s+(?:directly\s+)?from\s+(?:an?\s+)?(?:overhead|above)\b",
|
| 193 |
+
" ",
|
| 194 |
+
value,
|
| 195 |
+
flags=re.I,
|
| 196 |
+
)
|
| 197 |
+
value = re.sub(r"\b(?:strict\s+)?(?:top[- ]down|overhead)\b", " ", value, flags=re.I)
|
| 198 |
+
value = re.sub(r"\b(?:fully\s+)?isolated\b", " ", value, flags=re.I)
|
| 199 |
+
value = re.sub(r"\s+([,.;:])", r"\1", value)
|
| 200 |
+
value = re.sub(r"([,.;:])(?:\s*[,.;:])+", r"\1", value)
|
| 201 |
+
return compact_prompt_words(value, limit)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
def primary_camera_phrase(camera: str, is_background: bool) -> str:
|
| 205 |
if camera == "top_down":
|
| 206 |
return "Strict orthographic overhead view directly from above."
|
|
|
|
| 736 |
filename=filename,
|
| 737 |
width=width,
|
| 738 |
height=height,
|
| 739 |
+
description=interpreted_prompt,
|
| 740 |
camera=camera,
|
| 741 |
group=group,
|
| 742 |
variant_index=variant_index,
|
|
|
|
| 1777 |
|
| 1778 |
def diffusion_negative_prompt(spec: AssetSpec) -> str:
|
| 1779 |
camera_negative = {
|
| 1780 |
+
"top_down": (
|
| 1781 |
+
", sky, horizon, eye-level view, front view, front-facing facade, upright portrait, "
|
| 1782 |
+
"standing toward camera, face looking at viewer, vanishing point"
|
| 1783 |
+
),
|
| 1784 |
"isometric": ", eye-level view, front view, inconsistent perspective, horizon",
|
| 1785 |
"side_view": ", overhead view, top-down view, isometric view",
|
| 1786 |
}.get(spec.camera, "")
|
| 1787 |
if spec.composition in {"single_subject", "icon", "animation_frame"}:
|
| 1788 |
+
negative = (
|
| 1789 |
+
"multiple subjects, duplicate character, repeated subject, character sheet, turnaround, lineup, "
|
| 1790 |
+
"alternate views, multiple poses, cropped subject, scenery, landscape, environment, ground plane, "
|
| 1791 |
+
"background props, drop shadow, text, watermark, blurry, soft focus, out of focus, low detail"
|
|
|
|
|
|
|
| 1792 |
)
|
| 1793 |
+
camera_prefix = camera_negative.lstrip(", ")
|
| 1794 |
+
return compact_prompt_words(f"{camera_prefix}, {negative}" if camera_prefix else negative, 55)
|
| 1795 |
if spec.composition == "seamless":
|
| 1796 |
return "visible seams, borders, frame, perspective mockup, text, watermark, blurry, soft focus, low detail" + camera_negative
|
| 1797 |
if spec.composition == "sprite_sheet":
|
|
|
|
| 1942 |
camera = f"User-defined camera: {compact_prompt_words(spec.camera_instruction, 10)}."
|
| 1943 |
variation = f"Distinct variation: {compact_prompt_words(spec.variation, 8)}. " if spec.variation else ""
|
| 1944 |
if spec.composition == "single_subject":
|
| 1945 |
+
description = compact_subject_description(spec.description or spec.prompt, 10 if variation else 16)
|
| 1946 |
variation = f"Variation: {compact_prompt_words(spec.variation, 4)}. " if spec.variation else ""
|
| 1947 |
subject = (spec.group or spec.role).replace("_", " ")
|
| 1948 |
+
if spec.camera == "top_down":
|
| 1949 |
+
return compact_prompt_words(
|
| 1950 |
+
f"Top-down game sprite. Camera directly above the {subject}, looking straight down. "
|
| 1951 |
+
"Show top surfaces and a compact foreshortened silhouette, never an upright front-facing portrait. "
|
| 1952 |
+
f"{description}. Exactly one complete isolated subject, centered on uniform white. "
|
| 1953 |
+
f"{variation}Crisp game-ready details. No scene.",
|
| 1954 |
+
55,
|
| 1955 |
+
)
|
| 1956 |
return (
|
| 1957 |
f"{camera} Single isolated {subject} subject. One complete centered body in one pose. "
|
| 1958 |
"Fill eighty percent of the frame with even margins. "
|
|
|
|
| 1974 |
|
| 1975 |
def primary_generation_dimensions(spec: AssetSpec) -> tuple[int, int]:
|
| 1976 |
if not is_background_spec(spec):
|
| 1977 |
+
if spec.camera in {"top_down", "isometric"}:
|
| 1978 |
+
return 1024, 1024
|
| 1979 |
+
if spec.camera == "side_view":
|
| 1980 |
+
return 1152, 896
|
| 1981 |
+
if spec.camera in {"front_view", "first_person"}:
|
| 1982 |
+
return 896, 1152
|
| 1983 |
+
return 1024, 1024
|
| 1984 |
aspect = spec.width / max(1, spec.height)
|
| 1985 |
if aspect >= 1.35:
|
| 1986 |
return 1344, 768
|
|
|
|
| 2052 |
if has_implausibly_thin_foreground_subject(content, spec):
|
| 2053 |
last_failure_detail = "the last output contained an implausibly thin foreground silhouette"
|
| 2054 |
continue
|
| 2055 |
+
if has_front_view_like_top_down_silhouette(content, spec):
|
| 2056 |
+
last_failure_detail = (
|
| 2057 |
+
"the last output retained a tall portrait-like silhouette instead of the requested overhead view"
|
| 2058 |
+
)
|
| 2059 |
+
continue
|
| 2060 |
if has_undersized_foreground_subject(content):
|
| 2061 |
last_failure_detail = "the last output foreground occupied too little of the sprite canvas"
|
| 2062 |
continue
|
|
|
|
| 2400 |
)
|
| 2401 |
|
| 2402 |
|
| 2403 |
+
def has_front_view_like_top_down_silhouette(content: bytes, spec: AssetSpec) -> bool:
|
| 2404 |
+
"""Conservatively reject upright portrait silhouettes for overhead sprite contracts.
|
| 2405 |
+
|
| 2406 |
+
Alpha geometry cannot prove camera perspective, but a tall, canvas-filling
|
| 2407 |
+
humanoid is a strong signal that the generator ignored an overhead request.
|
| 2408 |
+
Broader non-humanoid checks apply only when the user's own description asks
|
| 2409 |
+
for a radial, wide, or multi-legged body. Narrow top-down props and vehicles
|
| 2410 |
+
therefore remain valid user-configurable assets.
|
| 2411 |
+
"""
|
| 2412 |
+
if spec.camera != "top_down" or spec.composition not in {"single_subject", "icon", "animation_frame"}:
|
| 2413 |
+
return False
|
| 2414 |
+
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
| 2415 |
+
bbox = material_foreground_bbox_from_image(image)
|
| 2416 |
+
if bbox is None:
|
| 2417 |
+
return False
|
| 2418 |
+
x0, y0, x1, y1 = bbox
|
| 2419 |
+
bbox_width = x1 - x0 + 1
|
| 2420 |
+
bbox_height = y1 - y0 + 1
|
| 2421 |
+
if bbox_width < 1:
|
| 2422 |
+
return True
|
| 2423 |
+
aspect = bbox_height / bbox_width
|
| 2424 |
+
height_ratio = bbox_height / max(1, image.height)
|
| 2425 |
+
if height_ratio < 0.72:
|
| 2426 |
+
return False
|
| 2427 |
+
|
| 2428 |
+
if spec.silhouette == "humanoid" and aspect >= 1.35:
|
| 2429 |
+
return True
|
| 2430 |
+
|
| 2431 |
+
description = f"{spec.description} {spec.prompt}"
|
| 2432 |
+
broad_shape_requested = contains_any_term(
|
| 2433 |
+
description,
|
| 2434 |
+
(
|
| 2435 |
+
"broad",
|
| 2436 |
+
"radial",
|
| 2437 |
+
"wide-bodied",
|
| 2438 |
+
"wide bodied",
|
| 2439 |
+
"six-legged",
|
| 2440 |
+
"six legged",
|
| 2441 |
+
"four-legged",
|
| 2442 |
+
"four legged",
|
| 2443 |
+
"multi-legged",
|
| 2444 |
+
"multi legged",
|
| 2445 |
+
"quadruped",
|
| 2446 |
+
"spider-like",
|
| 2447 |
+
"spider like",
|
| 2448 |
+
"crab-like",
|
| 2449 |
+
"crab like",
|
| 2450 |
+
),
|
| 2451 |
+
)
|
| 2452 |
+
if broad_shape_requested and aspect >= 1.45:
|
| 2453 |
+
return True
|
| 2454 |
+
|
| 2455 |
+
narrow_subject_requested = contains_any_term(
|
| 2456 |
+
description,
|
| 2457 |
+
("missile", "rocket", "sword", "spear", "beam", "projectile", "tower", "pole"),
|
| 2458 |
+
)
|
| 2459 |
+
return not narrow_subject_requested and aspect >= 2.0
|
| 2460 |
+
|
| 2461 |
+
|
| 2462 |
def has_undersized_foreground_subject(content: bytes) -> bool:
|
| 2463 |
"""Detect sprites whose visible model foreground occupies too little of its game canvas."""
|
| 2464 |
image = Image.open(io.BytesIO(content)).convert("RGBA")
|
|
|
|
| 2773 |
)
|
| 2774 |
elif spec.expected_subjects == 1 and has_implausibly_thin_foreground_subject(content, spec):
|
| 2775 |
warnings.append("sprite foreground silhouette is implausibly thin")
|
| 2776 |
+
elif spec.expected_subjects == 1 and has_front_view_like_top_down_silhouette(content, spec):
|
| 2777 |
+
warnings.append("top-down sprite silhouette remains strongly portrait-like")
|
| 2778 |
elif spec.expected_subjects == 1 and has_undersized_foreground_subject(content):
|
| 2779 |
warnings.append("foreground subject occupies too little of the output canvas")
|
| 2780 |
if spec.composition == "seamless" and image.width > 1 and image.height > 1:
|