← 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