Add OpenRouter AI texture generation mode to the asset pipeline
- tools/generate_textures.py: --ai mode (OPENROUTER_API_KEY, --prompt, --name, --model, --size; base64 PNG decode; clamp to <=1024px) with a default batch covering the authored texture set; seedream minimum-size handled by requesting 2048 and downscaling - Generated AI asset set committed under assets/textures/ai/ (15 files, 1024x1024 each), kept separate from the deterministic PIL set - README/MEMORY/TODO document the mode and live verification - (work authored in a parallel session; linted/verified here — 57/57 tests)
This commit is contained in:
1 parent
0abd67fbaa
commit
5b994982bd
19 files changed
+274
-23
No files matched your search
+255
-22
@@ -3,17 +3,93 @@
|
||||
|
||||
All outputs are deterministic (fixed seeds), <=1024px, power-of-two where
|
||||
possible. Run from the repo root: python3 tools/generate_textures.py
|
||||
|
||||
Optional AI mode generates textures through the OpenRouter image API
|
||||
(https://openrouter.ai/docs/api/api-reference/images/generate-an-image.md).
|
||||
Requires an API key in the OPENROUTER_API_KEY environment variable:
|
||||
|
||||
python3 tools/generate_textures.py --ai
|
||||
python3 tools/generate_textures.py --ai --prompt "..." --name foo.png
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import stat
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||
ASSETS = os.path.join(ROOT, "assets", "textures")
|
||||
|
||||
# OpenRouter image generation endpoint and default model (from the API docs).
|
||||
OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images"
|
||||
DEFAULT_IMAGE_MODEL = "bytedance-seed/seedream-4.5"
|
||||
MAX_DIM = 1024 # hard cap on the longest side of any generated texture
|
||||
# px requested from the API; some models (e.g. seedream) need >= 1920
|
||||
GEN_SIZE = 2048
|
||||
|
||||
# Default batch for `--ai`: one entry per game texture (see test_world.gd).
|
||||
# Each entry is (output name, prompt, transparent_background).
|
||||
AI_TEXTURES = [
|
||||
# --- world surfaces (seamless, flat-lit) ---
|
||||
("ai/brick_facade.png",
|
||||
"seamless tileable red brick wall texture, weathered urban facade, "
|
||||
"flat even lighting, frontal view, photorealistic game texture", False),
|
||||
("ai/concrete.png",
|
||||
"seamless tileable grey concrete wall texture, subtle cracks and stains, "
|
||||
"flat even lighting, frontal view, photorealistic game texture", False),
|
||||
("ai/asphalt.png",
|
||||
"seamless tileable dark asphalt road texture, fine grain, "
|
||||
"flat even lighting, frontal view, photorealistic game texture", False),
|
||||
# --- storefront dressing ---
|
||||
("ai/awning.png",
|
||||
"red and white striped storefront awning with scalloped bottom edge, "
|
||||
"frontal view, flat even lighting, game texture", False),
|
||||
("ai/skyline.png",
|
||||
"night city skyline silhouette, dark building shapes with a few lit "
|
||||
"windows, flat graphic style, isolated on transparent background", True),
|
||||
# --- screen-space overlay ---
|
||||
("ai/vignette.png",
|
||||
"radial vignette overlay, black edges fading to fully transparent center, "
|
||||
"grayscale, soft gradient", True),
|
||||
# --- HUD icons (white glyphs on transparent) ---
|
||||
("ai/icons/flashlight_on.png",
|
||||
"simple flat flashlight icon, white glyph on transparent background, "
|
||||
"minimal UI icon", True),
|
||||
("ai/icons/door_locked.png",
|
||||
"simple flat locked door icon with keyhole, white outline glyph on "
|
||||
"transparent background, minimal UI icon", True),
|
||||
("ai/icons/wand.png",
|
||||
"simple flat handheld security wand icon, white glyph on transparent "
|
||||
"background, minimal UI icon", True),
|
||||
("ai/icons/rec_dot.png",
|
||||
"record button icon, red filled dot inside a white circle outline, on "
|
||||
"transparent background, minimal UI icon", True),
|
||||
# --- access artifact cards ---
|
||||
("ai/cards/id_card.png",
|
||||
"driver license ID card, blue plastic card with photo placeholder and "
|
||||
"text lines, flat frontal view, game UI asset", False),
|
||||
("ai/cards/wristband.png",
|
||||
"red event access wristband with strap, flat frontal view, game UI asset",
|
||||
False),
|
||||
("ai/cards/ticket.png",
|
||||
"green admission ticket stub with perforated edge, flat frontal view, "
|
||||
"game UI asset", False),
|
||||
("ai/cards/credential.png",
|
||||
"dark grey staff credential pass with lanyard clip, flat frontal view, "
|
||||
"game UI asset", False),
|
||||
("ai/cards/guest_list.png",
|
||||
"beige guest list document with ruled lines and checkboxes, flat frontal "
|
||||
"view, game UI asset", False),
|
||||
]
|
||||
|
||||
FONT_CANDIDATES = [
|
||||
"/usr/share/fonts/liberation-sans-fonts/LiberationSans-Bold.ttf",
|
||||
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
|
||||
@@ -21,12 +97,14 @@ FONT_CANDIDATES = [
|
||||
"/usr/share/fonts/adwaita-sans-fonts/AdwaitaSans-Bold.ttf",
|
||||
]
|
||||
|
||||
|
||||
def font(size: int):
|
||||
for path in FONT_CANDIDATES:
|
||||
if os.path.exists(path):
|
||||
return ImageFont.truetype(path, size)
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def save(img: Image.Image, name: str) -> None:
|
||||
os.makedirs(ASSETS, exist_ok=True)
|
||||
path = os.path.join(ASSETS, name)
|
||||
@@ -34,6 +112,7 @@ def save(img: Image.Image, name: str) -> None:
|
||||
img.save(path, optimize=True)
|
||||
print(f"wrote {os.path.relpath(path, ROOT)} {img.size}")
|
||||
|
||||
|
||||
def grain(size, base, spread, seed, blur=1.0):
|
||||
rnd = random.Random(seed)
|
||||
img = Image.new("L", (size, size), base)
|
||||
@@ -44,6 +123,7 @@ def grain(size, base, spread, seed, blur=1.0):
|
||||
px[x, y] = max(0, min(255, v))
|
||||
return img.filter(ImageFilter.GaussianBlur(blur))
|
||||
|
||||
|
||||
def brick_facade():
|
||||
size = 512
|
||||
mortar = (118, 110, 104)
|
||||
@@ -68,13 +148,14 @@ def brick_facade():
|
||||
bx = rnd.randint(0, size - 1)
|
||||
by = rnd.randint(0, size - 1)
|
||||
d.point((bx, by), fill=(max(0, base[0] - 18 + rnd.randint(0, 30)),
|
||||
max(0, base[1] - 10 + rnd.randint(0, 22)),
|
||||
max(0, base[2] - 8 + rnd.randint(0, 18))))
|
||||
max(0, base[1] - 10 + rnd.randint(0, 22)),
|
||||
max(0, base[2] - 8 + rnd.randint(0, 18))))
|
||||
# horizontal mortar lines on top of bricks (wraps vertically by drawing at 0 and size)
|
||||
for y in [-mortar_t, size - mortar_t]:
|
||||
d.rectangle([0, y, size, y + mortar_t], fill=mortar)
|
||||
save(img, "brick_facade.png")
|
||||
|
||||
|
||||
def seamless_concrete():
|
||||
size = 512
|
||||
img = grain(size, 150, 26, 7, blur=2.5).convert("RGBA")
|
||||
@@ -83,7 +164,8 @@ def seamless_concrete():
|
||||
for _ in range(90):
|
||||
x, y = rnd.randint(0, size), rnd.randint(0, size)
|
||||
r = rnd.choice([2, 3, 4])
|
||||
d.ellipse([x - r, y - r, x + r, y + r], fill=(0, 0, 0, rnd.randint(8, 22)))
|
||||
d.ellipse([x - r, y - r, x + r, y + r],
|
||||
fill=(0, 0, 0, rnd.randint(8, 22)))
|
||||
for _ in range(8): # subtle crack segments
|
||||
x, y = rnd.randint(0, size), rnd.randint(0, size)
|
||||
pts = [(x, y)]
|
||||
@@ -94,6 +176,7 @@ def seamless_concrete():
|
||||
d.line(pts, fill=(70, 70, 70, 170), width=1)
|
||||
save(img, "concrete.png")
|
||||
|
||||
|
||||
def asphalt():
|
||||
size = 512
|
||||
img = grain(size, 52, 14, 23, blur=1.2).convert("RGBA")
|
||||
@@ -101,9 +184,11 @@ def asphalt():
|
||||
rnd = random.Random(31)
|
||||
for _ in range(600):
|
||||
x, y = rnd.randint(0, size), rnd.randint(0, size)
|
||||
d.point((x, y), fill=(max(0, 40 - rnd.randint(0, 18)),) * 3 + (rnd.randint(40, 110),))
|
||||
d.point((x, y), fill=(max(0, 40 - rnd.randint(0, 18)),)
|
||||
* 3 + (rnd.randint(40, 110),))
|
||||
save(img, "asphalt.png")
|
||||
|
||||
|
||||
def awning():
|
||||
w, h = 1024, 256
|
||||
img = Image.new("RGB", (w, h), (140, 40, 45))
|
||||
@@ -111,7 +196,8 @@ def awning():
|
||||
stripe = 64
|
||||
for i in range(w // stripe):
|
||||
if i % 2 == 1:
|
||||
d.rectangle([i * stripe, 0, (i + 1) * stripe - 2, h], fill=(230, 225, 220))
|
||||
d.rectangle([i * stripe, 0, (i + 1) * stripe - 2, h],
|
||||
fill=(230, 225, 220))
|
||||
# scalloped bottom edge
|
||||
for x in range(0, w, 32):
|
||||
d.arc([x, h - 24, x + 32, h + 40], 0, 180, fill=(80, 20, 24), width=6)
|
||||
@@ -125,6 +211,7 @@ def awning():
|
||||
img = Image.alpha_composite(img.convert("RGBA"), shade)
|
||||
save(img, "awning.png")
|
||||
|
||||
|
||||
def skyline():
|
||||
w, h = 1024, 256
|
||||
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
||||
@@ -141,11 +228,13 @@ def skyline():
|
||||
wx = x + rnd.randint(4, max(4, bw - 8))
|
||||
wy = h - rnd.randint(4, bh - 4)
|
||||
if rnd.random() < 0.5:
|
||||
d.rectangle([wx, wy, wx + 6, wy + 8], fill=(255, 200, 120, 220))
|
||||
d.rectangle([wx, wy, wx + 6, wy + 8],
|
||||
fill=(255, 200, 120, 220))
|
||||
x += bw + rnd.randint(10, 46)
|
||||
d.rectangle([0, h - 6, w, h], fill=(12, 13, 22, 255))
|
||||
save(img, "skyline.png")
|
||||
|
||||
|
||||
def vignette():
|
||||
size = 512
|
||||
img = Image.new("L", (size, size), 0)
|
||||
@@ -159,37 +248,51 @@ def vignette():
|
||||
px[x, y] = int(255 * (t * t))
|
||||
save(img, "vignette.png")
|
||||
|
||||
|
||||
def icon(name, draw_fn, size=64):
|
||||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
draw_fn(d, size)
|
||||
save(img, name)
|
||||
|
||||
|
||||
def icons():
|
||||
def flashlight_on(d, s):
|
||||
d.rounded_rectangle([s*0.30, s*0.30, s*0.62, s*0.62], radius=4, fill=(255,255,255,255))
|
||||
d.polygon([(s*0.62, s*0.30), (s*0.82, s*0.18), (s*0.82, s*0.74), (s*0.62, s*0.62)], fill=(255,214,120,255))
|
||||
d.rounded_rectangle([s*0.30, s*0.30, s*0.62, s*0.62],
|
||||
radius=4, fill=(255, 255, 255, 255))
|
||||
d.polygon([(s*0.62, s*0.30), (s*0.82, s*0.18), (s*0.82, s *
|
||||
0.74), (s*0.62, s*0.62)], fill=(255, 214, 120, 255))
|
||||
|
||||
def door_locked(d, s):
|
||||
d.rectangle([s*0.18, s*0.18, s*0.82, s*0.82], outline=(255,255,255,255), width=5)
|
||||
d.rounded_rectangle([s*0.36, s*0.34, s*0.64, s*0.66], radius=3, fill=(255,255,255,255))
|
||||
d.arc([s*0.40, s*0.28, s*0.60, s*0.50], 180, 360, fill=(255,255,255,255), width=5)
|
||||
d.rectangle([s*0.18, s*0.18, s*0.82, s*0.82],
|
||||
outline=(255, 255, 255, 255), width=5)
|
||||
d.rounded_rectangle([s*0.36, s*0.34, s*0.64, s*0.66],
|
||||
radius=3, fill=(255, 255, 255, 255))
|
||||
d.arc([s*0.40, s*0.28, s*0.60, s*0.50], 180,
|
||||
360, fill=(255, 255, 255, 255), width=5)
|
||||
|
||||
def wand(d, s):
|
||||
d.rounded_rectangle([s*0.30, s*0.10, s*0.44, s*0.90], radius=3, fill=(255,255,255,255))
|
||||
d.ellipse([s*0.24, s*0.06, s*0.50, s*0.32], fill=(120,220,160,255))
|
||||
d.rounded_rectangle([s*0.30, s*0.10, s*0.44, s*0.90],
|
||||
radius=3, fill=(255, 255, 255, 255))
|
||||
d.ellipse([s*0.24, s*0.06, s*0.50, s*0.32], fill=(120, 220, 160, 255))
|
||||
|
||||
def rec(d, s):
|
||||
d.ellipse([s*0.14, s*0.14, s*0.86, s*0.86], outline=(255,255,255,255), width=5)
|
||||
d.ellipse([s*0.14, s*0.14, s*0.86, s*0.86],
|
||||
outline=(255, 255, 255, 255), width=5)
|
||||
d.ellipse([s*0.30, s*0.30, s*0.70, s*0.70], fill=(230, 60, 60, 255))
|
||||
icon("icons/flashlight_on.png", flashlight_on)
|
||||
icon("icons/door_locked.png", door_locked)
|
||||
icon("icons/wand.png", wand)
|
||||
icon("icons/rec_dot.png", rec)
|
||||
|
||||
|
||||
def app_icon():
|
||||
"""Game app icon: dark badge, gold star, white 'S' — used by desktop/AppImage
|
||||
(PNG) and the Windows installer (ICO)."""
|
||||
def badge(d, s):
|
||||
d.ellipse([2, 2, s-2, s-2], fill=(24, 26, 38, 255))
|
||||
d.ellipse([int(s*0.10), int(s*0.10), int(s*0.90), int(s*0.90)], outline=(120, 130, 170, 255), width=max(2, s//64))
|
||||
d.ellipse([int(s*0.10), int(s*0.10), int(s*0.90), int(s*0.90)],
|
||||
outline=(120, 130, 170, 255), width=max(2, s//64))
|
||||
# gold star
|
||||
cx, cy, r = s*0.5, s*0.44, s*0.30
|
||||
pts = []
|
||||
@@ -197,7 +300,8 @@ def app_icon():
|
||||
ang = -90 + i * 36
|
||||
rad = r if i % 2 == 0 else r * 0.42
|
||||
import math
|
||||
pts.append((cx + rad * math.cos(math.radians(ang)), cy + rad * math.sin(math.radians(ang))))
|
||||
pts.append((cx + rad * math.cos(math.radians(ang)),
|
||||
cy + rad * math.sin(math.radians(ang))))
|
||||
d.polygon(pts, fill=(255, 205, 80, 255))
|
||||
icon("icon.png", badge, size=1024)
|
||||
icon("icon.ico", badge, size=256)
|
||||
@@ -205,29 +309,33 @@ def app_icon():
|
||||
# multi-size ICO for Windows (from the 256 render)
|
||||
src = Image.open(os.path.join(ASSETS, "icon.png"))
|
||||
ico_path = os.path.join(ASSETS, "icon.ico")
|
||||
src.save(ico_path, format="ICO", sizes=[(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)])
|
||||
src.save(ico_path, format="ICO", sizes=[
|
||||
(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)])
|
||||
print("wrote assets/textures/icon.ico (multi-size)")
|
||||
|
||||
|
||||
def cards():
|
||||
CW, CH = 256, 128
|
||||
def base(card_name, bg, label, band=(0,0,0,0)):
|
||||
|
||||
def base(card_name, bg, label, band=(0, 0, 0, 0)):
|
||||
img = Image.new("RGBA", (CW, CH), bg + (255,))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.rounded_rectangle([6, 6, CW-6, CH-6], radius=10, outline=(0,0,0,90), width=2)
|
||||
d.rounded_rectangle([6, 6, CW-6, CH-6], radius=10,
|
||||
outline=(0, 0, 0, 90), width=2)
|
||||
if band and band[3] == 255:
|
||||
d.rectangle([0, CH-34, CW, CH], fill=band)
|
||||
f = font(26)
|
||||
d.text((14, 10), label, font=f, fill=(255,255,255,235))
|
||||
d.text((14, 10), label, font=f, fill=(255, 255, 255, 235))
|
||||
# body text lines
|
||||
f2 = font(15)
|
||||
y = 52
|
||||
for _ in range(4):
|
||||
w = 150 - (24 if _ == 3 else 0)
|
||||
d.rectangle([16, y, 16 + w, y + 8], fill=(255,255,255,110))
|
||||
d.rectangle([16, y, 16 + w, y + 8], fill=(255, 255, 255, 110))
|
||||
y += 24
|
||||
# photo square for id-like cards
|
||||
if "photo" in card_name:
|
||||
d.rectangle([CW-84, 14, CW-18, 96], fill=(180,190,205,255))
|
||||
d.rectangle([CW-84, 14, CW-18, 96], fill=(180, 190, 205, 255))
|
||||
save(img, f"cards/{card_name}.png")
|
||||
base("id_card", (46, 92, 168), "CA DRIVER LICENSE", band=(20, 36, 70, 255), )
|
||||
base("wristband", (196, 62, 78), "EVENT ACCESS", band=(150, 35, 50, 255))
|
||||
@@ -235,7 +343,132 @@ def cards():
|
||||
base("credential", (58, 62, 74), "STAFF PASS", band=(20, 22, 28, 255))
|
||||
base("guest_list", (200, 190, 168), "GUEST LIST", band=(120, 90, 60, 255))
|
||||
|
||||
|
||||
def _api_key() -> str:
|
||||
key = os.environ.get("OPENROUTER_API_KEY", "").strip()
|
||||
if not key:
|
||||
sys.exit("error: OPENROUTER_API_KEY is not set; export it before using --ai")
|
||||
return key
|
||||
|
||||
|
||||
def _generate_image(prompt: str, model: str, size: int,
|
||||
transparent: bool = False) -> Image.Image:
|
||||
"""Call the OpenRouter image API and return the decoded PIL image.
|
||||
|
||||
Requests a square image at `size` px and decodes the base64 PNG/JPEG/WebP
|
||||
bytes returned in `data[].b64_json`. The requested size is clamped to at
|
||||
least GEN_SIZE because some models (e.g. seedream) reject small outputs;
|
||||
callers downscale the result to MAX_DIM afterwards. When `transparent` is
|
||||
true, the API is asked for a transparent background (PNG alpha).
|
||||
"""
|
||||
size = max(GEN_SIZE, int(size))
|
||||
body = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"size": f"{size}x{size}",
|
||||
"output_format": "png",
|
||||
"n": 1,
|
||||
}
|
||||
if transparent:
|
||||
body["background"] = "transparent"
|
||||
req = urllib.request.Request(
|
||||
OPENROUTER_IMAGES_URL,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {_api_key()}",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://git.allucanget.biz/zwitschi/security-game",
|
||||
"X-Title": "Step Line",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=300) as resp:
|
||||
payload = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", "replace")
|
||||
sys.exit(f"error: OpenRouter returned HTTP {exc.code}: {detail}")
|
||||
except urllib.error.URLError as exc:
|
||||
sys.exit(f"error: could not reach OpenRouter: {exc.reason}")
|
||||
|
||||
data = payload.get("data") or []
|
||||
if not data or not data[0].get("b64_json"):
|
||||
sys.exit(f"error: OpenRouter returned no image data: {payload}")
|
||||
raw = base64.b64decode(data[0]["b64_json"])
|
||||
try:
|
||||
img = Image.open(io.BytesIO(raw))
|
||||
img.load()
|
||||
except Exception as exc: # noqa: BLE001 - surface any decode failure clearly
|
||||
sys.exit(f"error: could not decode returned image: {exc}")
|
||||
return img
|
||||
|
||||
|
||||
def _fit(img: Image.Image, max_dim: int = MAX_DIM) -> Image.Image:
|
||||
"""Downscale to fit within max_dim on the longest side, preserving aspect."""
|
||||
w, h = img.size
|
||||
longest = max(w, h)
|
||||
if longest <= max_dim:
|
||||
return img
|
||||
scale = max_dim / longest
|
||||
return img.resize((max(1, round(w * scale)), max(1, round(h * scale))),
|
||||
Image.Resampling.LANCZOS)
|
||||
|
||||
|
||||
def generate_ai_textures(prompt: str, name: str, model: str, size: int,
|
||||
transparent: bool = False) -> None:
|
||||
"""Generate one texture via OpenRouter and save it under assets/textures/."""
|
||||
img = _generate_image(prompt, model, size, transparent)
|
||||
img = _fit(img)
|
||||
if transparent:
|
||||
img = img.convert("RGBA")
|
||||
elif img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info):
|
||||
img = img.convert("RGBA")
|
||||
else:
|
||||
img = img.convert("RGB")
|
||||
save(img, name)
|
||||
|
||||
|
||||
def _run_ai(args: argparse.Namespace) -> None:
|
||||
if args.prompt:
|
||||
if not args.name:
|
||||
sys.exit("error: --name is required when --prompt is given")
|
||||
generate_ai_textures(args.prompt, args.name, args.model, args.size)
|
||||
return
|
||||
for name, prompt, transparent in AI_TEXTURES:
|
||||
print(f"generating {name} ...")
|
||||
generate_ai_textures(prompt, name, args.model, args.size, transparent)
|
||||
|
||||
|
||||
def _build_arg_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Step Line textures (authored or via OpenRouter).")
|
||||
parser.add_argument(
|
||||
"--ai", action="store_true",
|
||||
help="generate textures through the OpenRouter image API "
|
||||
"(requires OPENROUTER_API_KEY)")
|
||||
parser.add_argument(
|
||||
"--prompt", default="",
|
||||
help="single prompt for --ai (overrides the default batch)")
|
||||
parser.add_argument(
|
||||
"--name", default="",
|
||||
help="output filename under assets/textures/ for a single --prompt")
|
||||
parser.add_argument(
|
||||
"--model", default=DEFAULT_IMAGE_MODEL,
|
||||
help=f"image model slug (default: {DEFAULT_IMAGE_MODEL})")
|
||||
parser.add_argument(
|
||||
"--size", type=int, default=GEN_SIZE,
|
||||
help=f"square size requested from the API in px (min {GEN_SIZE}); "
|
||||
f"output is downscaled to <= {MAX_DIM} (default: {GEN_SIZE})")
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = _build_arg_parser().parse_args()
|
||||
if args.ai:
|
||||
_run_ai(args)
|
||||
print("done")
|
||||
sys.exit(0)
|
||||
|
||||
brick_facade()
|
||||
seamless_concrete()
|
||||
asphalt()
|
||||
|
||||
Reference in new issue
Block a user