← back to Pattern Vault

scripts/postgen_vision.py

65 lines

#!/usr/bin/env python3
"""settlement-post-gen-vision equivalent, run on LOCAL ollama vision ($0).
Re-checks each produced image for Part B prohibited elements (birds/butterflies/
bananas/grapes) + Part A signals, then applies the settlement verdict.
BLOCK requires full Part A (directional foliage + open space + >1 ink color) AND a Part B element."""
import os, io, json, base64, urllib.request, glob
from PIL import Image

OUT = "/Users/macstudio3/Projects/pattern-vault/assets/generated"
OLLAMA = "http://127.0.0.1:11434/api/generate"
MODEL = "qwen2.5vl:7b"

Q = ("You are a strict wallpaper-pattern compliance inspector. Look ONLY at what is literally "
     "drawn in this image. Answer STRICT JSON, no prose:\n"
     '{"birds": <true/false: any bird or bird silhouette present>,'
     '"butterflies": <true/false: any butterfly or moth>,'
     '"bananas": <true/false: banana fruit or banana leaves>,'
     '"grapes": <true/false: grape clusters>,'
     '"foliage_directional": <true/false: repeating leaves/fronds pointing in varied directions>,'
     '"open_space": <true/false: visible empty background between motifs>,'
     '"multiple_ink_colors": <true/false: foliage/motifs use more than one color>,'
     '"describe": "<=12 words what the pattern actually shows"}')

def jpeg_b64(path):
    im = Image.open(path).convert("RGB")
    im.thumbnail((768, 768))
    b = io.BytesIO(); im.save(b, "JPEG", quality=88)
    return base64.b64encode(b.getvalue()).decode()

def ask(path):
    payload = {"model": MODEL, "prompt": Q, "images": [jpeg_b64(path)],
               "stream": False, "format": "json", "options": {"temperature": 0}}
    req = urllib.request.Request(OLLAMA, data=json.dumps(payload).encode(),
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=180) as r:
        resp = json.load(r)["response"]
    return json.loads(resp)

def verdict(d):
    partB = any([d.get("birds"), d.get("butterflies"), d.get("bananas"), d.get("grapes")])
    partA = all([d.get("foliage_directional"), d.get("open_space"), d.get("multiple_ink_colors")])
    # Settlement binding: violation requires FULL Part A AND a Part B element.
    if partB and partA:
        return "BLOCK", "full Part A + Part B prohibited element"
    if partB:
        # Part B element present but Part A not fully satisfied -> not a violation, but flag for review.
        return "NEEDS-REVIEW", "Part B element present without full Part A (manual check)"
    return "PASS", "no Part B prohibited element"

rows = []
for p in sorted(glob.glob(f"{OUT}/BRIEF-*.png")):
    try:
        d = ask(p)
    except Exception as e:
        rows.append({"file": os.path.basename(p), "verdict": "ERROR", "err": str(e)[:80]});
        print(f"{os.path.basename(p)}: ERROR {e}", flush=True); continue
    v, why = verdict(d)
    rows.append({"file": os.path.basename(p), "verdict": v, "why": why, "vision": d})
    flags = [k for k in ("birds","butterflies","bananas","grapes") if d.get(k)]
    print(f"{os.path.basename(p)}: {v}  partB={flags or 'none'}  | {d.get('describe','')}", flush=True)

json.dump(rows, open(f"{OUT}/_postgen_vision.json", "w"), indent=2)
n_pass = sum(1 for r in rows if r["verdict"] == "PASS")
print(f"\n=== {n_pass}/{len(rows)} PASS  (model={MODEL}, $0 local) ===")