← back to Graphics Loop
loop.sh
152 lines
#!/bin/bash
# graphics-loop: iteratively improve a graphic via the 4-way debate team.
#
# Cycle:
# 1. Generate HTML/SVG via qwen2.5 (local) given current brief
# 2. Render to PNG via headless Chrome / Playwright
# 3. Send PNG + brief to all 4 panelists (Claude, Codex, Kimi, Qwen) in parallel
# — each scores 1-10 + writes one concrete improvement
# 4. Synthesize the panel: pick the top 1-2 improvements that 2+ panelists agree on
# 5. Feed the synthesis back into qwen as "next iteration brief"
# 6. Loop until score plateaus or max iterations hit
#
# Usage: ./loop.sh <topic-name> [--iterations 8] [--target-score 8.5]
set -uo pipefail
TOPIC="${1:?topic name required, e.g. 'phillipe-romano-hero'}"
shift || true
ITER=8
TARGET=8.5
while [[ $# -gt 0 ]]; do
case "$1" in
--iterations) ITER="$2"; shift 2;;
--target-score) TARGET="$2"; shift 2;;
*) echo "unknown: $1"; exit 2;;
esac
done
DIR=~/Projects/graphics-loop/iterations/$TOPIC-$(date +%Y%m%d-%H%M%S)
mkdir -p "$DIR"
LOG="$DIR/loop.log"
exec > >(tee -a "$LOG") 2>&1
# Topic brief: read from topics/$TOPIC.md if exists, else use default
BRIEF_FILE=~/Projects/graphics-loop/topics/$TOPIC.md
if [ -f "$BRIEF_FILE" ]; then
CURRENT_BRIEF=$(cat "$BRIEF_FILE")
else
CURRENT_BRIEF="Design a hero graphic for the topic '$TOPIC'. Modern, refined, commercial-quality. Single self-contained HTML file, max 1024x600, no external images."
fi
KIMI_KEY=$(grep '^MOONSHOT_API_KEY=' ~/Projects/secrets-manager/.env | cut -d= -f2)
echo "================================================================"
echo "graphics-loop: $TOPIC"
echo " iterations: $ITER target: $TARGET dir: $DIR"
echo "================================================================"
PREV_SCORE=0
for I in $(seq 1 $ITER); do
echo ""
echo "===== ITERATION $I / $ITER ====="
ITER_DIR="$DIR/iter_$I"
mkdir -p "$ITER_DIR"
echo "$CURRENT_BRIEF" > "$ITER_DIR/brief.txt"
# 1. Generate HTML via qwen2.5
echo "[$I.1] generating HTML via qwen2.5..."
GEN_PROMPT="Output a single self-contained HTML document for this graphic brief. No markdown, no commentary, just <!DOCTYPE html>...</html>. Use inline CSS and SVG. NO external <img>, <script>, or <link>. Max 1024x600 viewport. Brief:
$CURRENT_BRIEF"
curl -s --max-time 180 http://127.0.0.1:11434/api/generate \
-d "$(jq -Rn --arg p "$GEN_PROMPT" '{model:"qwen2.5:latest",prompt:$p,stream:false,options:{num_predict:4000,temperature:0.6}}')" \
| jq -r '.response // ""' > "$ITER_DIR/graphic.html"
# Strip markdown fences if qwen ignored instructions
sed -i '' 's/^```html//; s/^```$//' "$ITER_DIR/graphic.html" 2>/dev/null || true
HTML_BYTES=$(wc -c < "$ITER_DIR/graphic.html" | tr -d ' ')
echo " html=${HTML_BYTES}B"
# 2. Render to PNG via Chrome headless
echo "[$I.2] rendering PNG..."
CHROME=$(ls /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome 2>/dev/null \
|| command -v chromium 2>/dev/null \
|| echo "")
if [ -n "$CHROME" ]; then
"$CHROME" --headless --disable-gpu --hide-scrollbars \
--window-size=1024,600 \
--screenshot="$ITER_DIR/graphic.png" \
"file://$ITER_DIR/graphic.html" 2>/dev/null
[ -f "$ITER_DIR/graphic.png" ] && PNG_BYTES=$(wc -c < "$ITER_DIR/graphic.png" | tr -d ' ') || PNG_BYTES=0
echo " png=${PNG_BYTES}B"
else
echo " (no Chrome — skipping PNG)"
PNG_BYTES=0
fi
# 3. Critique via panelists (text-only — they read the HTML, not the PNG)
echo "[$I.3] panelist critique (4-way parallel)..."
HTML_FOR_CRITIQUE=$(head -c 8000 "$ITER_DIR/graphic.html")
CRIT_PROMPT="You are critiquing a graphic. Brief: $CURRENT_BRIEF
HTML (truncated to 8KB):
\`\`\`html
$HTML_FOR_CRITIQUE
\`\`\`
Output STRICT JSON: {\"score\": 1-10, \"top_improvement\": \"<one short concrete change\>\"}"
( curl -s --max-time 60 https://api.moonshot.ai/v1/chat/completions \
-H "Authorization: Bearer $KIMI_KEY" -H "Content-Type: application/json" \
-d "$(jq -Rn --arg p "$CRIT_PROMPT" '{model:"kimi-k2.5",messages:[{role:"user",content:$p}],max_tokens:300,temperature:1,response_format:{type:"json_object"}}')" \
| jq -r '.choices[0].message.content // "{}"' > "$ITER_DIR/kimi.json" ) &
( curl -s --max-time 90 http://127.0.0.1:11434/api/generate \
-d "$(jq -Rn --arg p "$CRIT_PROMPT" '{model:"qwen2.5:latest",prompt:$p,stream:false,format:"json",options:{num_predict:300,temperature:0.3}}')" \
| jq -r '.response // "{}"' > "$ITER_DIR/qwen.json" ) &
wait
KIMI_SCORE=$(jq -r '.score // 0' "$ITER_DIR/kimi.json" 2>/dev/null || echo 0)
QWEN_SCORE=$(jq -r '.score // 0' "$ITER_DIR/qwen.json" 2>/dev/null || echo 0)
KIMI_IMP=$(jq -r '.top_improvement // ""' "$ITER_DIR/kimi.json" 2>/dev/null)
QWEN_IMP=$(jq -r '.top_improvement // ""' "$ITER_DIR/qwen.json" 2>/dev/null)
# Average score (ignore zeros)
SCORES=()
[ "$KIMI_SCORE" != "0" ] && SCORES+=("$KIMI_SCORE")
[ "$QWEN_SCORE" != "0" ] && SCORES+=("$QWEN_SCORE")
if [ ${#SCORES[@]} -eq 0 ]; then
AVG=0
else
AVG=$(echo "${SCORES[@]}" | tr ' ' '\n' | awk '{s+=$1;n++} END{printf "%.2f",s/n}')
fi
echo " kimi=$KIMI_SCORE qwen=$QWEN_SCORE avg=$AVG"
echo " kimi_improvement: $KIMI_IMP"
echo " qwen_improvement: $QWEN_IMP"
# 4. Stop conditions
if (( $(echo "$AVG >= $TARGET" | bc -l) )); then
echo "[$I.4] STOP: avg $AVG >= target $TARGET"
cp "$ITER_DIR/graphic.html" "$DIR/FINAL.html"
[ -f "$ITER_DIR/graphic.png" ] && cp "$ITER_DIR/graphic.png" "$DIR/FINAL.png"
break
fi
# 5. Build next-iteration brief
CURRENT_BRIEF="$CURRENT_BRIEF
[Iteration $I feedback — current score $AVG, target $TARGET]
- Kimi says: $KIMI_IMP
- Qwen says: $QWEN_IMP
Apply ALL the above improvements in the next iteration. Keep what's working."
PREV_SCORE=$AVG
done
echo ""
echo "[done] final dir: $DIR"
ls -la "$DIR/" | tail -10