Update app.py
Browse filesGuide neural sprites by camera and palette
app.py
CHANGED
|
@@ -126,6 +126,7 @@ PRIMARY_LORA_MODEL = os.environ.get("PRIMARY_LORA_MODEL", "latent-consistency/lc
|
|
| 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",
|
|
@@ -1371,6 +1372,70 @@ def draw_sprite(spec: AssetSpec, rng: random.Random) -> bytes:
|
|
| 1371 |
image.save(out, format="PNG")
|
| 1372 |
return out.getvalue()
|
| 1373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1374 |
shadow = (int(cx - 36 * scale), int(cy + 34 * scale), int(cx + 36 * scale), int(cy + 48 * scale))
|
| 1375 |
draw.ellipse(shadow, fill=(0, 0, 0, 65))
|
| 1376 |
|
|
@@ -1476,7 +1541,11 @@ def diffusion_negative_prompt(spec: AssetSpec) -> str:
|
|
| 1476 |
"particle effect, particles, confetti, sparkles, floating dots, glowing orbs, embers, lens flare"
|
| 1477 |
+ camera_negative
|
| 1478 |
)
|
| 1479 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1480 |
|
| 1481 |
|
| 1482 |
def build_camera_control_image(spec: AssetSpec, width: int = 1024, height: int = 576) -> Image.Image:
|
|
@@ -1581,10 +1650,24 @@ def primary_diffusion_prompt(spec: AssetSpec) -> str:
|
|
| 1581 |
)
|
| 1582 |
return (
|
| 1583 |
f"{spec.prompt} One complete isolated game sprite only, centered, fully visible, readable silhouette, "
|
| 1584 |
-
"camera angle exactly
|
|
|
|
| 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:
|
|
@@ -1606,18 +1689,15 @@ def primary_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[byt
|
|
| 1606 |
"guidance_scale": PRIMARY_GUIDANCE_SCALE,
|
| 1607 |
"generator": generator,
|
| 1608 |
}
|
| 1609 |
-
if
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
|
| 1614 |
-
image
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
|
| 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)
|
|
@@ -2033,7 +2113,7 @@ def model_source_note(image_model: str) -> str:
|
|
| 2033 |
if "via fal-ai" in lowered or "via replicate" in lowered:
|
| 2034 |
return "routed image-to-image refinement鈥攑reserves a layout guide but consumes account inference credit"
|
| 2035 |
if "zerogpu" in lowered:
|
| 2036 |
-
return "locally hosted neural diffusion鈥攏o per-image API credit;
|
| 2037 |
return "diffusion output鈥攈igher visual variety; verify subject, perspective, alpha edges, and prompt compliance"
|
| 2038 |
|
| 2039 |
|
|
@@ -2042,7 +2122,7 @@ def model_configuration_summary() -> str:
|
|
| 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:
|
|
@@ -2089,8 +2169,8 @@ def model_configuration_summary() -> str:
|
|
| 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.
|
| 2093 |
-
"camera
|
| 2094 |
"development fallback and is blocked in the deployed Space when the primary model fails."
|
| 2095 |
)
|
| 2096 |
|
|
|
|
| 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 |
+
PRIMARY_SPRITE_STRENGTH = float(os.environ.get("PRIMARY_SPRITE_STRENGTH", "0.72"))
|
| 130 |
USE_PRIMARY_IMAGE_MODEL = os.environ.get("USE_PRIMARY_IMAGE_MODEL", "1") == "1"
|
| 131 |
REQUIRE_PRIMARY_IMAGE_MODEL = os.environ.get(
|
| 132 |
"REQUIRE_PRIMARY_IMAGE_MODEL",
|
|
|
|
| 1372 |
image.save(out, format="PNG")
|
| 1373 |
return out.getvalue()
|
| 1374 |
|
| 1375 |
+
if spec.camera == "side_view":
|
| 1376 |
+
# Profile silhouette used both by the procedural fallback and as the
|
| 1377 |
+
# neural img2img camera guide. A single visible eye and forward nose
|
| 1378 |
+
# prevent the model from reverting to its common front-facing pose.
|
| 1379 |
+
draw.ellipse((cx - 28 * scale, cy + 31 * scale, cx + 31 * scale, cy + 46 * scale), fill=(0, 0, 0, 55))
|
| 1380 |
+
draw.rounded_rectangle(
|
| 1381 |
+
(cx - 15 * scale, cy - 16 * scale, cx + 17 * scale, cy + 32 * scale),
|
| 1382 |
+
radius=max(4, int(10 * scale)),
|
| 1383 |
+
fill=outline,
|
| 1384 |
+
)
|
| 1385 |
+
draw.rounded_rectangle(
|
| 1386 |
+
(cx - 10 * scale, cy - 11 * scale, cx + 12 * scale, cy + 27 * scale),
|
| 1387 |
+
radius=max(3, int(7 * scale)),
|
| 1388 |
+
fill=body,
|
| 1389 |
+
)
|
| 1390 |
+
draw.ellipse((cx - 17 * scale, cy - 50 * scale, cx + 17 * scale, cy - 16 * scale), fill=outline)
|
| 1391 |
+
draw.ellipse((cx - 12 * scale, cy - 45 * scale, cx + 13 * scale, cy - 20 * scale), fill=accent)
|
| 1392 |
+
draw.polygon(
|
| 1393 |
+
((cx + 12 * scale, cy - 36 * scale), (cx + 24 * scale, cy - 31 * scale), (cx + 12 * scale, cy - 27 * scale)),
|
| 1394 |
+
fill=accent,
|
| 1395 |
+
)
|
| 1396 |
+
draw.ellipse((cx + 5 * scale, cy - 37 * scale, cx + 10 * scale, cy - 32 * scale), fill=(255, 255, 255, 245))
|
| 1397 |
+
draw.line((cx + 7 * scale, cy - 4 * scale, cx + 35 * scale, cy + 11 * scale), fill=outline, width=max(5, int(9 * scale)))
|
| 1398 |
+
draw.line((cx + 7 * scale, cy - 4 * scale, cx + 35 * scale, cy + 11 * scale), fill=trim, width=max(2, int(5 * scale)))
|
| 1399 |
+
draw.line((cx - 5 * scale, cy + 27 * scale, cx - 19 * scale, cy + 52 * scale), fill=outline, width=max(5, int(10 * scale)))
|
| 1400 |
+
draw.line((cx + 7 * scale, cy + 27 * scale, cx + 24 * scale, cy + 50 * scale), fill=outline, width=max(5, int(10 * scale)))
|
| 1401 |
+
if "bow" in role_prompt or "archer" in role_prompt or "ranger" in role_prompt:
|
| 1402 |
+
draw.arc((cx + 25 * scale, cy - 19 * scale, cx + 56 * scale, cy + 35 * scale), 95, 265, fill=trim, width=max(2, int(4 * scale)))
|
| 1403 |
+
out = io.BytesIO()
|
| 1404 |
+
image.save(out, format="PNG")
|
| 1405 |
+
return out.getvalue()
|
| 1406 |
+
|
| 1407 |
+
if spec.camera == "isometric":
|
| 1408 |
+
# Offset limbs and facial features establish a three-quarter pose rather
|
| 1409 |
+
# than the symmetric front view used by the generic fallback.
|
| 1410 |
+
draw.ellipse((cx - 31 * scale, cy + 29 * scale, cx + 38 * scale, cy + 46 * scale), fill=(0, 0, 0, 58))
|
| 1411 |
+
draw.polygon(
|
| 1412 |
+
(
|
| 1413 |
+
(cx - 21 * scale, cy - 12 * scale),
|
| 1414 |
+
(cx + 12 * scale, cy - 20 * scale),
|
| 1415 |
+
(cx + 26 * scale, cy + 24 * scale),
|
| 1416 |
+
(cx - 7 * scale, cy + 35 * scale),
|
| 1417 |
+
),
|
| 1418 |
+
fill=outline,
|
| 1419 |
+
)
|
| 1420 |
+
draw.polygon(
|
| 1421 |
+
(
|
| 1422 |
+
(cx - 14 * scale, cy - 8 * scale),
|
| 1423 |
+
(cx + 8 * scale, cy - 14 * scale),
|
| 1424 |
+
(cx + 19 * scale, cy + 20 * scale),
|
| 1425 |
+
(cx - 4 * scale, cy + 27 * scale),
|
| 1426 |
+
),
|
| 1427 |
+
fill=body,
|
| 1428 |
+
)
|
| 1429 |
+
draw.ellipse((cx - 18 * scale, cy - 49 * scale, cx + 18 * scale, cy - 14 * scale), fill=outline)
|
| 1430 |
+
draw.ellipse((cx - 12 * scale, cy - 43 * scale, cx + 13 * scale, cy - 19 * scale), fill=accent)
|
| 1431 |
+
draw.ellipse((cx + 4 * scale, cy - 35 * scale, cx + 9 * scale, cy - 30 * scale), fill=(255, 255, 255, 245))
|
| 1432 |
+
draw.line((cx + 11 * scale, cy - 4 * scale, cx + 39 * scale, cy + 9 * scale), fill=trim, width=max(4, int(8 * scale)))
|
| 1433 |
+
draw.line((cx - 3 * scale, cy + 28 * scale, cx - 22 * scale, cy + 50 * scale), fill=outline, width=max(5, int(10 * scale)))
|
| 1434 |
+
draw.line((cx + 12 * scale, cy + 24 * scale, cx + 31 * scale, cy + 43 * scale), fill=outline, width=max(5, int(10 * scale)))
|
| 1435 |
+
out = io.BytesIO()
|
| 1436 |
+
image.save(out, format="PNG")
|
| 1437 |
+
return out.getvalue()
|
| 1438 |
+
|
| 1439 |
shadow = (int(cx - 36 * scale), int(cy + 34 * scale), int(cx + 36 * scale), int(cy + 48 * scale))
|
| 1440 |
draw.ellipse(shadow, fill=(0, 0, 0, 65))
|
| 1441 |
|
|
|
|
| 1541 |
"particle effect, particles, confetti, sparkles, floating dots, glowing orbs, embers, lens flare"
|
| 1542 |
+ camera_negative
|
| 1543 |
)
|
| 1544 |
+
return (
|
| 1545 |
+
common
|
| 1546 |
+
+ ", landscape, environment scene, room, horizon, opaque background, drop shadow, "
|
| 1547 |
+
"trees, foliage, ground, floor, scenery, background objects, frame, border"
|
| 1548 |
+
)
|
| 1549 |
|
| 1550 |
|
| 1551 |
def build_camera_control_image(spec: AssetSpec, width: int = 1024, height: int = 576) -> Image.Image:
|
|
|
|
| 1650 |
)
|
| 1651 |
return (
|
| 1652 |
f"{spec.prompt} One complete isolated game sprite only, centered, fully visible, readable silhouette, "
|
| 1653 |
+
"camera angle and pose exactly matching the supplied guide, on a plain uniform white studio background. "
|
| 1654 |
+
"No scenery, floor, trees, foliage, frame, or environmental props."
|
| 1655 |
)
|
| 1656 |
|
| 1657 |
|
| 1658 |
+
def primary_image_guide(spec: AssetSpec, index: int, run_id: int) -> Image.Image:
|
| 1659 |
+
"""Build a clean camera-and-palette guide for the neural image-to-image pass."""
|
| 1660 |
+
guide_png = local_asset_png(spec, index, run_id)
|
| 1661 |
+
guide = Image.open(io.BytesIO(guide_png)).convert("RGBA")
|
| 1662 |
+
if is_background_spec(spec):
|
| 1663 |
+
return guide.convert("RGB").resize((1344, 768), Image.LANCZOS)
|
| 1664 |
+
|
| 1665 |
+
guide = guide.resize((1024, 1024), Image.LANCZOS)
|
| 1666 |
+
white = Image.new("RGB", guide.size, (255, 255, 255))
|
| 1667 |
+
white.paste(guide.convert("RGB"), mask=guide.getchannel("A"))
|
| 1668 |
+
return white
|
| 1669 |
+
|
| 1670 |
+
|
| 1671 |
def primary_diffusion_png(spec: AssetSpec, index: int, run_id: int) -> tuple[bytes | None, str | None]:
|
| 1672 |
"""Generate every production asset with SSD-1B; backgrounds use camera-guided img2img."""
|
| 1673 |
if PRIMARY_MODEL_ERROR:
|
|
|
|
| 1689 |
"guidance_scale": PRIMARY_GUIDANCE_SCALE,
|
| 1690 |
"generator": generator,
|
| 1691 |
}
|
| 1692 |
+
if PRIMARY_IMAGE_TO_IMAGE_PIPE is None:
|
| 1693 |
+
return None, "primary camera-guided image-to-image pipeline is unavailable"
|
| 1694 |
+
guide = primary_image_guide(spec, index, run_id)
|
| 1695 |
+
strength = PRIMARY_BACKGROUND_STRENGTH if is_background_spec(spec) else PRIMARY_SPRITE_STRENGTH
|
| 1696 |
+
image = PRIMARY_IMAGE_TO_IMAGE_PIPE(
|
| 1697 |
+
image=guide,
|
| 1698 |
+
strength=strength,
|
| 1699 |
+
**common,
|
| 1700 |
+
).images[0]
|
|
|
|
|
|
|
|
|
|
| 1701 |
return polish_diffusion_asset(image, spec), None
|
| 1702 |
except Exception as exc:
|
| 1703 |
return None, short_error(exc)
|
|
|
|
| 2113 |
if "via fal-ai" in lowered or "via replicate" in lowered:
|
| 2114 |
return "routed image-to-image refinement鈥攑reserves a layout guide but consumes account inference credit"
|
| 2115 |
if "zerogpu" in lowered:
|
| 2116 |
+
return "locally hosted neural diffusion鈥攏o per-image API credit; assets are camera-guided and still require visual review"
|
| 2117 |
return "diffusion output鈥攈igher visual variety; verify subject, perspective, alpha edges, and prompt compliance"
|
| 2118 |
|
| 2119 |
|
|
|
|
| 2122 |
sprite_chain = []
|
| 2123 |
primary_source = f"{PRIMARY_IMAGE_MODEL} + {PRIMARY_LORA_MODEL} on ZeroGPU"
|
| 2124 |
if USE_PRIMARY_IMAGE_MODEL:
|
| 2125 |
+
sprite_chain.append(f"camera-guided {primary_source}")
|
| 2126 |
if USE_DIFFUSION_FOR_SPRITES:
|
| 2127 |
sprite_chain.append(FREE_IMAGE_MODEL)
|
| 2128 |
if USE_HF_IMAGE_PROVIDER:
|
|
|
|
| 2169 |
f"prompts: `{prompt_source}` 路 sprites: `{sprite_source}` 路 backgrounds: `{background_source}` 路 "
|
| 2170 |
f"remote image fallback: `{remote_fallback}` 路 neural image models: `{neural_status}` 路 "
|
| 2171 |
f"neural enforcement: `{enforcement}` 路 primary readiness: `{readiness}`. "
|
| 2172 |
+
"Every production image is generated by the primary neural model. Sprites and backgrounds start from deterministic "
|
| 2173 |
+
"camera-and-palette guides and are refined with image-to-image diffusion; the procedural renderer remains an emergency "
|
| 2174 |
"development fallback and is blocked in the deployed Space when the primary model fails."
|
| 2175 |
)
|
| 2176 |
|