← back to Groundworks Activation
scripts/settlement-vision.py
100 lines
#!/usr/bin/env python3
"""Settlement post-gen vision gate for Groundworks Hunt Slonem images.
Runs Gemini 2.0 Flash on each image, extracting the 5 settlement detector booleans,
then applies the settlement-verdict combinator: BLOCK only when full Part A
(A1 directional foliage AND A2 open space AND A3 multiple ink colors) is satisfied
AND a Part B element (banana/grape/BIRD/BUTTERFLY) is present, absent an acceptable carve-out.
Rabbits/bunnies are NOT Part B prohibited elements.
"""
import os, sys, json, base64, urllib.request, time, subprocess
KEY = os.environ.get("GEMINI_API_KEY")
if not KEY:
print("ERR: no GEMINI_API_KEY"); sys.exit(2)
MODEL = "gemini-2.5-flash"
URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent?key={KEY}"
PROMPT = """You are a strict visual detector for a wallcovering settlement gate. Look ONLY at what is actually visible in this repeating pattern image. Answer as JSON with these exact boolean/string fields:
{
"a1_directional_foliage": <true if there are leaves/fronds/foliage shown in MORE THAN ONE orientation across the repeat, else false>,
"a2_open_space": <true if there is visible negative/open space BETWEEN foliage motifs (not edge-to-edge coverage), else false>,
"a3_multiple_ink_colors": <true if the foliage/leaf layer uses more than one ink color (excluding background), else false>,
"partB_bananas": <true if bananas or banana pods visible>,
"partB_grapes": <true if grapes visible>,
"partB_birds": <true if birds visible>,
"partB_butterflies": <true if butterflies or moths visible>,
"acceptable_trunk_branch": <true if clearly represented tree trunks or branches visible>,
"primary_motif": "<one short phrase describing the main visible motif>"
}
Return ONLY the JSON, no prose."""
def gemini(image_bytes, mime="image/jpeg"):
# downscale big print-master JPEGs so the inline request stays small
try:
from io import BytesIO
from PIL import Image
im = Image.open(BytesIO(image_bytes)).convert("RGB")
im.thumbnail((1024,1024))
buf = BytesIO(); im.save(buf, format="JPEG", quality=85)
image_bytes = buf.getvalue()
except Exception:
pass
b64 = base64.b64encode(image_bytes).decode()
body = {"contents":[{"parts":[{"text":PROMPT},{"inline_data":{"mime_type":mime,"data":b64}}]}],
"generationConfig":{"temperature":0,"maxOutputTokens":2048,"responseMimeType":"application/json"}}
req = urllib.request.Request(URL, data=json.dumps(body).encode(), headers={"Content-Type":"application/json"})
try:
with urllib.request.urlopen(req, timeout=90) as r:
resp = json.load(r)
except urllib.error.HTTPError as he:
raise RuntimeError(f"gemini HTTP {he.code}: {he.read().decode()[:200]}")
cand = resp["candidates"][0]
parts = cand.get("content",{}).get("parts",[])
txt = "".join(p.get("text","") for p in parts).strip()
if not txt:
raise RuntimeError(f"empty gemini response finish={cand.get('finishReason')}")
if txt.startswith("```"):
txt = txt.split("```")[1]
if txt.startswith("json"): txt = txt[4:]
# grab the first {...} block
s,e = txt.find("{"), txt.rfind("}")
if s>=0 and e>s: txt = txt[s:e+1]
return json.loads(txt.strip())
def verdict(d):
partA = d["a1_directional_foliage"] and d["a2_open_space"] and d["a3_multiple_ink_colors"]
partB = d["partB_bananas"] or d["partB_grapes"] or d["partB_birds"] or d["partB_butterflies"]
if partA and partB and not d.get("acceptable_trunk_branch"):
return "BLOCK"
if partB and not partA:
# prohibited element present but not full-tropical composition -> defendant-favorable but flag
return "NEEDS REVIEW"
if partA and partB and d.get("acceptable_trunk_branch"):
return "NEEDS REVIEW"
return "OK"
items = json.load(open(sys.argv[1]))
out = []
for it in items:
sku, url = it["mfr_sku"], it["image_url"]
try:
p = subprocess.run(["curl","-sL","-A","Mozilla/5.0","-e","https://www.kravet.com/",url],
capture_output=True, timeout=90)
img = p.stdout
if len(img) < 500:
raise RuntimeError(f"image too small ({len(img)} bytes) http-fetch failed")
d = gemini(img)
v = verdict(d)
except Exception as e:
d = {"error": str(e)}; v = "BLOCK" # unknown => BLOCK per hard rule
rec = {"mfr_sku": sku, "verdict": v, "detectors": d, "name": it.get("name")}
out.append(rec)
print(f"{v:14s} {sku:16s} {it.get('name','')} :: partB_birds={d.get('partB_birds')} partB_butterflies={d.get('partB_butterflies')} motif={d.get('primary_motif','')}")
time.sleep(0.4)
json.dump(out, open(sys.argv[2],"w"), indent=2)
n_block = sum(1 for r in out if r["verdict"]=="BLOCK")
n_rev = sum(1 for r in out if r["verdict"]=="NEEDS REVIEW")
n_ok = sum(1 for r in out if r["verdict"]=="OK")
print(f"\nSUMMARY: OK={n_ok} NEEDS_REVIEW={n_rev} BLOCK={n_block} (audit -> {sys.argv[2]})")