LeafCat79 commited on
Commit
c352e81
·
verified ·
1 Parent(s): 3f66212

Enforce top-down sprite perspective

Browse files
Files changed (1) hide show
  1. app.py +111 -9
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": ", sky, horizon, eye-level view, front view, front-facing facade, vanishing point",
 
 
 
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
- return (
1767
- "multiple subjects, duplicate character, repeated subject, character sheet, turnaround, lineup, alternate views, "
1768
- "multiple poses, cropped subject, scenery, landscape, environment, environmental framing, ground plane, "
1769
- "background props, drop shadow, text, watermark, blurry, soft focus, out of focus, low detail, smeared details, "
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 = compact_prompt_words(spec.prompt, 6 if variation else 10)
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
- return 768, 1024
 
 
 
 
 
 
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: