← back to Pattern Vault
whimsical-compare/daily/scripts/gen_daily.py
180 lines
#!/usr/bin/env python3
"""
Generate N fresh, ORIGINAL, settlement-passed whimsical wallpaper designs for the
daily Spoonflower drop. Every design is our own invention (randomised style x motif
x palette lane) — never a copy of a marketplace design — and each output is
vision-checked for banned elements (bird/butterfly/banana/grape) before it counts.
Writes files to daily/runs/<date>/ and prints a JSON manifest to stdout.
$0 for the vision check (local qwen). Replicate SDXL is the only paid part (~1c/img).
Usage: python3 gen_daily.py --n 20 --date 2026-07-02 [--seed-base 1234]
"""
import os, re, json, time, sys, base64, random, urllib.request, urllib.error
HERE = os.path.dirname(os.path.abspath(__file__))
DAILY = os.path.dirname(HERE)
OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
VMODEL = "qwen2.5vl:7b"
def argv(flag, d=None):
a = sys.argv
return a[a.index(flag)+1] if flag in a else d
N = int(argv("--n", "20"))
DATE = argv("--date", time.strftime("%Y-%m-%d"))
SEED_BASE = int(argv("--seed-base", str(int(time.time()) % 1000000)))
OUT = f"{DAILY}/runs/{DATE}"
os.makedirs(OUT, exist_ok=True)
tok = None
with open(os.path.expanduser("~/Projects/secrets-manager/.env")) as f:
m = re.search(r"REPLICATE_API_TOKEN=([A-Za-z0-9_-]+)", f.read())
tok = m and m.group(1)
assert tok, "no replicate token"
UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
NEG = ("bird, birds, butterfly, butterflies, banana, bananas, banana leaves, banana pods, grape, grapes, "
"grape clusters, text, watermark, signature, logo, blurry, low quality, jpeg artifacts, seams, "
"mismatched edges, frame, border, vignette")
# whimsical design lanes — all settlement-safe by construction (no banned motifs)
STYLES = ["cottagecore", "mid-century modern", "folk block-print", "scandinavian", "art-nouveau line",
"watercolor storybook", "gouache naive", "hand-cut paper collage", "ditsy calico", "whimsical geometric"]
MOTIFS = ["tiny mushrooms and toadstools", "scattered wildflowers and sprigs", "rolling hills and cottages",
"curling ferns and leaves", "smiling suns and moons", "little teapots and cups",
"garden snails and pebbles", "star and cloud confetti", "citrus slices and dots",
"rainbows and raindrops", "sailboats and waves", "cats curled in flowers",
"hot air balloons", "acorns and oak leaves", "abstract squiggles and blobs",
"polka-dot toadstool rings", "meadow foxgloves", "quilted patchwork hearts"]
PALETTES = ["sage, cream, terracotta", "dusty blue, ochre, ivory", "blush pink, sand, olive",
"mustard, teal, oatmeal", "lilac, moss, butter", "coral, denim, chalk",
"forest green, rust, bone", "peach, mint, taupe", "navy, marigold, linen",
"plum, sage, apricot"]
# ── TRENDING GAP LANES ─────────────────────────────────────────────────────────
# Mapped from trending.designerwallcoverings.com's GAP_STYLES (styles that are demonstrably
# SELLING across Etsy/Spoonflower/Patternbank/retail but thin in our catalog). Generating in
# these lanes = "make what's actually selling", not just whimsical. Settlement-safe by
# construction: NO directional-leaf tropical foliage, NO birds/butterflies/bananas/grapes
# (the vision check still enforces the 4 banned motifs per output). Tropical is deliberately
# EXCLUDED — it sits closest to the DW settlement bone; route tropical through the full
# settlement gate separately, never the daily auto-drop.
TREND_LANES = {
"Art-Deco Geometric": {"style": "art-deco geometric", "desc": "bold graphic gilded and luxe",
"motifs": ["fan and sunburst motifs", "stepped chevron zigzags", "scalloped shell arcs", "metallic linear grid"],
"palettes": ["chartreuse, black, gold", "emerald, brass, cream", "navy, gold, black"]},
"Animal-Skin Print": {"style": "animal-skin print", "desc": "chic maximalist with a modern colour twist",
"motifs": ["cheetah spots", "tiger stripes", "zebra stripes", "leopard rosettes", "snakeskin scales"],
"palettes": ["rose blush, cream, charcoal", "verdant green, black", "claret, sand, ink"]},
"Texture-Effect": {"style": "textured wallcovering effect", "desc": "tactile tonal and sophisticated",
"motifs": ["grasscloth woven texture", "linen weave texture", "plaster limewash texture", "tweed fabric texture"],
"palettes": ["greige, oatmeal, stone", "mushroom, cream", "sage, taupe"]},
"Boho": {"style": "artisanal boho block-print", "desc": "earthy handcrafted and global",
"motifs": ["block-print medallions", "folk geometric borders", "mudcloth marks", "kilim diamonds"],
"palettes": ["terracotta, indigo, cream", "rust, ochre, sand"]},
"Paisley / Nomadic": {"style": "nomadic paisley", "desc": "intricate decorative and heritage",
"motifs": ["kashmiri paisley boteh", "persian teardrop paisley", "mandala medallions"],
"palettes": ["claret, indigo, gold", "plum, teal, cream"]},
"Abstract Blur": {"style": "abstract watercolor", "desc": "soft ethereal ombre and dreamy",
"motifs": ["ombre gradient washes", "inky bleed marks", "marbled fluid swirls", "soft-focus colour drifts"],
"palettes": ["blush, taupe, grey", "dusty blue, ivory", "sage, sand"]},
"Retro Geometric": {"style": "retro mid-century geometric", "desc": "nostalgic bold and structured",
"motifs": ["interlocking semicircles", "atomic starbursts", "mod circle grids", "totem shapes"],
"palettes": ["mustard, teal, cream", "olive, rust, bone"]},
"Mushroom / Goblincore":{"style": "goblincore woodland", "desc": "whimsical cozy and folkloric",
"motifs": ["mushroom clusters and ferns", "toadstool rings and moss", "snails and pebbles among fungi"],
"palettes": ["forest green, brown, cream", "moss, plum, ochre"]},
}
# --lanes: whimsical (default, the original pools) | trending (GAP lanes) | both (50/50 mix)
LANES = argv("--lanes", "whimsical")
def http_json(url, payload=None, headers=None, timeout=180):
hdr = {"User-Agent": UA, "Content-Type": "application/json"}
if headers: hdr.update(headers)
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(url, data=data, headers=hdr, method="POST" if data else "GET")
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.load(r)
def check_output(img_path):
b64 = base64.b64encode(open(img_path, "rb").read()).decode()
try:
out = http_json(f"{OLLAMA}/api/generate", {
"model": VMODEL,
"prompt": 'Reply ONLY JSON {"bird":bool,"butterfly":bool,"banana":bool,"grape":bool} — true only if clearly depicted.',
"images": [b64], "stream": False, "options": {"temperature": 0.2}}, timeout=120)
j = json.loads(re.search(r"\{.*\}", out.get("response", ""), re.S).group(0))
return {k: bool(j.get(k)) for k in ("bird", "butterfly", "banana", "grape")}
except Exception:
return {}
def sdxl(prompt, seed):
ver = http_json("https://api.replicate.com/v1/models/stability-ai/sdxl",
headers={"Authorization": f"Bearer {tok}"})["latest_version"]["id"]
pred = http_json("https://api.replicate.com/v1/predictions", {
"version": ver, "input": {"prompt": prompt, "negative_prompt": NEG, "width": 1024, "height": 1024,
"num_inference_steps": 30, "guidance_scale": 7.5, "seed": seed, "refine": "no_refiner"}},
headers={"Authorization": f"Bearer {tok}"})
for _ in range(90):
if pred.get("status") in ("succeeded", "failed", "canceled"): break
time.sleep(2)
pred = http_json(pred["urls"]["get"], headers={"Authorization": f"Bearer {tok}"})
return pred
ADJ = ["Meadow", "Cottage", "Hollow", "Thicket", "Wren", "Bramble", "Fern", "Marigold", "Juniper", "Poppy",
"Willow", "Clover", "Sorrel", "Heath", "Larkspur", "Nettle", "Foxglove", "Hazel", "Rowan", "Sage"]
NOUN = ["Whimsy", "Ramble", "Frolic", "Wander", "Reverie", "Lullaby", "Trellis", "Gambol", "Ditty", "Wink"]
def title(style, motif, i):
random.seed(SEED_BASE + i * 7)
return f"{random.choice(ADJ)} {random.choice(NOUN)} — {motif.split(' and ')[0].title()}"
random.seed(SEED_BASE)
designs = []
total_secs = 0.0
attempts = 0
i = 0
while len(designs) < N and attempts < N * 3:
attempts += 1
use_trend = LANES == "trending" or (LANES == "both" and random.random() < 0.5)
if use_trend:
lane_label = random.choice(list(TREND_LANES)); L = TREND_LANES[lane_label]
style = L["style"]; motif = random.choice(L["motifs"]); palette = random.choice(L["palettes"])
prompt = (f"Original {style} wallpaper: {motif}, {L['desc']}, our own artistic invention, "
f"not a copy of any existing design. seamless repeating wallpaper pattern, tileable, "
f"flat lay, even lighting, high-end wallcovering. Colorway: {palette}.")
else:
lane_label = "whimsical"
style = random.choice(STYLES); motif = random.choice(MOTIFS); palette = random.choice(PALETTES)
prompt = (f"Original hand-illustrated {style} wallpaper: {motif}, whimsical and charming, playful naive "
f"hand-drawn linework, our own artistic invention, not a copy of any existing design. "
f"seamless repeating wallpaper pattern, tileable, flat lay, even lighting, high-end wallcovering. "
f"Colorway: {palette}.")
seed = SEED_BASE + attempts * 13
pred = sdxl(prompt, seed)
secs = (pred.get("metrics") or {}).get("predict_time", 0) or 0
total_secs += secs
if pred.get("status") != "succeeded":
print(f"[attempt {attempts}] gen {pred.get('status')}", file=sys.stderr); continue
out = pred["output"]; imgurl = out[0] if isinstance(out, list) else out
fn = f"{DATE.replace('-','')}_{len(designs)+1:02d}.png"
dest = f"{OUT}/{fn}"
urllib.request.urlretrieve(imgurl, dest)
chk = check_output(dest)
if any(chk.get(k) for k in ("bird", "butterfly", "banana", "grape")):
os.remove(dest)
print(f"[attempt {attempts}] SETTLEMENT BLOCK {chk} — discarded, retrying", file=sys.stderr)
continue
t = title(style, motif, len(designs))
designs.append({"n": len(designs)+1, "file": dest, "title": t, "style": style,
"motif": motif, "palette": palette, "lane": lane_label, "prompt": prompt,
"settlement": "OK", "predict_secs": round(secs, 1)})
print(f"[{len(designs)}/{N}] {t}", file=sys.stderr)
time.sleep(0.6)
cost = round(total_secs * 0.000725, 4)
manifest = {"date": DATE, "count": len(designs), "requested": N, "attempts": attempts,
"est_cost_usd": cost, "predict_secs": round(total_secs, 1), "designs": designs}
json.dump(manifest, open(f"{OUT}/manifest.json", "w"), indent=2)
print(json.dumps(manifest))