openpencil/scripts/self-loop.sh

114 lines
4.1 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# Self-contained develop→generate→render→audit loop, zero human in the loop.
#
# scripts/self-loop.sh <run-name> <prompts.txt> [out-dir]
#
# prompts.txt: one prompt per line (# comments / blank lines skipped).
# For each prompt N:
# 1. GENERATE op-smoke (orchestrator DIRECT by default; set
# OPENPENCIL_SMOKE_LOOP=1 for the agentic loop)
# → <out>/<run>/pNN.op
# 2. RENDER openpencil-desktop --render-shots — the REAL jian-core
# (taffy) layout + jian-skia paint, the exact desktop canvas
# pipeline → <out>/<run>/pNN_shots/*.png
# 3. AUDIT op-smoke OPENPENCIL_SMOKE_AUDIT — real-layout geometry
# diagnostics (collapse / table overflow / text & frame
# overflow / sibling jam), ALSO computed by jian, so
# generation feedback, audit and final pixels share ONE
# layout engine → <out>/<run>/pNN.audit.json
# Finally assembles <out>/<run>/scorecard.json.
#
# Vision scoring is the agent's job afterwards: Read each shot PNG and score
# it against openpencil-docs/self-loop/rubric.md (same rubric scores the
# Pencil reference anchors, giving the quantified gap).
#
# Model env (required): OPENPENCIL_LLM_PROVIDER / OPENPENCIL_LLM_BASE_URL /
# OPENPENCIL_LLM_API_KEY / OPENPENCIL_ORCHESTRATOR_MODEL.
set -uo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
RUN="${1:?run name required}"
PROMPTS="${2:?prompts file required}"
OUT="${3:-/tmp/self-loop}/$RUN"
mkdir -p "$OUT"
SMOKE="$REPO/target/release/op-smoke"
DESKTOP="$REPO/target/release/openpencil-desktop"
[ -x "$SMOKE" ] && [ -x "$DESKTOP" ] || {
echo "build first: cargo build --release -p op-smoke -p op-host-desktop" >&2
exit 2
}
i=0
while IFS= read -r prompt; do
case "$prompt" in ''|'#'*) continue ;; esac
i=$((i + 1))
tag=$(printf 'p%02d' "$i")
op="$OUT/$tag.op"
printf '%s' "$prompt" >"$OUT/$tag.prompt"
echo "== [$tag] ${prompt:0:80}..." >&2
if [ -n "${OPENPENCIL_SMOKE_LOOP:-}" ]; then
OPENPENCIL_SMOKE_OUT="$op" "$SMOKE" "$prompt" \
>"$OUT/$tag.stdout" 2>"$OUT/$tag.log"
else
OPENPENCIL_SMOKE_DIRECT=1 OPENPENCIL_SMOKE_OUT="$op" "$SMOKE" "$prompt" \
>"$OUT/$tag.stdout" 2>"$OUT/$tag.log"
fi
gen_ok=false
[ -s "$op" ] && gen_ok=true
render_ok=false
shots="$OUT/${tag}_shots"
if [ "$gen_ok" = true ]; then
mkdir -p "$shots"
"$DESKTOP" --render-shots "$op" "$shots" 2 >/dev/null 2>&1 \
&& [ -n "$(ls -A "$shots" 2>/dev/null)" ] && render_ok=true
fi
if [ "$gen_ok" = true ]; then
OPENPENCIL_SMOKE_AUDIT="$op" "$SMOKE" audit >"$OUT/$tag.audit.json" 2>/dev/null || true
fi
GEN_OK="$gen_ok" RENDER_OK="$render_ok" TAG="$tag" OUT_DIR="$OUT" python3 - <<'PY'
import json, os
out, tag = os.environ["OUT_DIR"], os.environ["TAG"]
row = {
"tag": tag,
"prompt": open(f"{out}/{tag}.prompt").read()[:120],
"genOk": os.environ["GEN_OK"] == "true",
"renderOk": os.environ["RENDER_OK"] == "true",
"nodes": 0,
"auditIssues": -1,
}
try:
d = json.load(open(f"{out}/{tag}.op"))
def cnt(n):
return 1 + sum(cnt(c) for c in n.get("children", []))
roots = d.get("children") or d.get("pages", [{}])[0].get("children", [])
row["nodes"] = sum(cnt(x) for x in roots)
except Exception:
pass
try:
row["auditIssues"] = json.load(open(f"{out}/{tag}.audit.json"))["issueCount"]
except Exception:
pass
json.dump(row, open(f"{out}/{tag}.row.json", "w"), ensure_ascii=False)
print(f" gen={row['genOk']} render={row['renderOk']} nodes={row['nodes']} audit_issues={row['auditIssues']}")
PY
done <"$PROMPTS"
RUN_NAME="$RUN" OUT_DIR="$OUT" python3 - <<'PY'
import glob, json, os
out = os.environ["OUT_DIR"]
rows = [json.load(open(p)) for p in sorted(glob.glob(f"{out}/p*.row.json"))]
clean = sum(1 for r in rows if r["renderOk"] and r["auditIssues"] == 0)
card = {
"run": os.environ["RUN_NAME"],
"prompts": len(rows),
"structurallyClean": clean,
"rows": rows,
}
json.dump(card, open(f"{out}/scorecard.json", "w"), indent=1, ensure_ascii=False)
print(json.dumps(card, indent=1, ensure_ascii=False))
PY
echo "scorecard → $OUT/scorecard.json" >&2