Spaces:
Running
Running
deploy: sync c0cd151f from GitHub Actions
Browse files
CLAUDE.md
DELETED
|
@@ -1,494 +0,0 @@
|
|
| 1 |
-
# VisionCoder OpenEnv — Claude Code Guide
|
| 2 |
-
|
| 3 |
-
## Project
|
| 4 |
-
Screenshot-to-HTML RL environment for the Scaler x Meta PyTorch Hackathon.
|
| 5 |
-
OpenEnv-compatible HTTP API: `reset()` / `step()` / `render()` / `state()`.
|
| 6 |
-
|
| 7 |
-
**Round 1** (backup branch `round1`): single-step, single-agent inference.
|
| 8 |
-
**Round 2** (current `main`): multi-step iterative environment + multi-agent (Developer + Critic) + RL training.
|
| 9 |
-
|
| 10 |
-
## Package structure
|
| 11 |
-
- `src/` — maps to `openenv` package (`client.py`, `models.py`, `agents.py`, `prompts.py`, `inference.py`, `train.py`, `dataset.py`)
|
| 12 |
-
- `src/server/` — maps to `openenv.server` (`app.py`, `environment.py`)
|
| 13 |
-
- `src/server/rewards/` — maps to `openenv.server.rewards` (one file per reward function)
|
| 14 |
-
- `data/` — bundled synthetic samples (5 per difficulty, ~40KB each)
|
| 15 |
-
- `data/tests/` — reward stability test cases (0-14; committed HTML + expected scores; renders gitignored)
|
| 16 |
-
- `tests/test_rewards.py` — unified test suite (unit + stability + correlation tests)
|
| 17 |
-
|
| 18 |
-
## Running on rmgpu006 (cluster)
|
| 19 |
-
|
| 20 |
-
### Step 1 — start vLLM (tmux session: `vllm`)
|
| 21 |
-
```bash
|
| 22 |
-
~/.local/bin/tmux new-session -s vllm
|
| 23 |
-
# inside that session:
|
| 24 |
-
apptainer exec --nv ~/apptainer-images/cuda-custom-amal_latest.sif bash -c \
|
| 25 |
-
'export LD_PRELOAD=/dev/shm/qwen35/lib/libstdc++.so.6;
|
| 26 |
-
/dev/shm/qwen35/bin/python -m vllm.entrypoints.openai.api_server \
|
| 27 |
-
--model ~/models/Qwen3.5-2B --served-model-name qwen35 \
|
| 28 |
-
--tensor-parallel-size 2 --port 8001 --host 0.0.0.0 \
|
| 29 |
-
--max-model-len 65536 --enable-auto-tool-choice --tool-call-parser hermes' \
|
| 30 |
-
2>&1 | tee ~/vllm_qwen35.log
|
| 31 |
-
```
|
| 32 |
-
**`--enable-auto-tool-choice --tool-call-parser hermes` is mandatory** — without it every Developer call fails with 400 Bad Request and falls back to FALLBACK_HTML.
|
| 33 |
-
|
| 34 |
-
### Step 2 — start env server (tmux session: `openenv`, no apptainer needed)
|
| 35 |
-
```bash
|
| 36 |
-
~/.local/bin/tmux new-session -s openenv
|
| 37 |
-
# inside that session:
|
| 38 |
-
export PLAYWRIGHT_BROWSERS_PATH=~/playwright-browsers
|
| 39 |
-
cd ~/workspace/vision-coder-openenv
|
| 40 |
-
/dev/shm/qwen35/bin/python -m uvicorn openenv.server.app:app --host 127.0.0.1 --port 18080
|
| 41 |
-
```
|
| 42 |
-
|
| 43 |
-
### Step 3 — run inference (same openenv session or a new window)
|
| 44 |
-
```bash
|
| 45 |
-
export API_BASE_URL=http://localhost:8001/v1
|
| 46 |
-
export MODEL_NAME=qwen35
|
| 47 |
-
export HF_TOKEN=sk-local
|
| 48 |
-
export MAX_STEPS=2
|
| 49 |
-
export PLAYWRIGHT_BROWSERS_PATH=~/playwright-browsers
|
| 50 |
-
cd ~/workspace/vision-coder-openenv
|
| 51 |
-
/dev/shm/qwen35/bin/python inference.py
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
### First-time setup
|
| 55 |
-
```bash
|
| 56 |
-
# Install package (once per env build)
|
| 57 |
-
/dev/shm/qwen35/bin/pip install -e .
|
| 58 |
-
|
| 59 |
-
# Download model (once, needs proxy sourced)
|
| 60 |
-
source ~/proxy-setup/scripts/proxy_env.sh
|
| 61 |
-
/dev/shm/qwen35/bin/python -c "
|
| 62 |
-
from huggingface_hub import snapshot_download
|
| 63 |
-
snapshot_download('Qwen/Qwen3.5-2B', local_dir='$HOME/models/Qwen3.5-2B')
|
| 64 |
-
"
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
## Running locally (generic)
|
| 68 |
-
```bash
|
| 69 |
-
pip install -e .
|
| 70 |
-
uvicorn openenv.server.app:app --host 0.0.0.0 --port 7860
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
## Running inference (HF router)
|
| 74 |
-
```bash
|
| 75 |
-
export API_BASE_URL=https://router.huggingface.co/v1
|
| 76 |
-
export MODEL_NAME=Qwen/Qwen3.5-35B-A3B
|
| 77 |
-
export HF_TOKEN=hf_...
|
| 78 |
-
python inference.py
|
| 79 |
-
```
|
| 80 |
-
|
| 81 |
-
## Environment variables
|
| 82 |
-
- `API_BASE_URL` — OpenAI-compatible LLM endpoint (required)
|
| 83 |
-
- `MODEL_NAME` — vision-capable model ID (required); Developer and Critic share this endpoint
|
| 84 |
-
- `HF_TOKEN` — Hugging Face token / API key for LLM calls (primary auth key)
|
| 85 |
-
- `MAX_STEPS` — max developer turns per episode (default: 5)
|
| 86 |
-
- `INFERENCE_SERVER_PORT` — env server port (default: 18080)
|
| 87 |
-
- `DEBUG` — set to `1` to enable full episode debug logging. Creates `outputs/<run_id>/` with:
|
| 88 |
-
- `<difficulty>.md` — per-episode markdown log (reference image, all step renders, HTML, rewards, critic text)
|
| 89 |
-
- `images/` — PNGs saved separately (reference, each step's rendered output, critic comparison views)
|
| 90 |
-
- Run: `DEBUG=1 API_BASE_URL=... MODEL_NAME=... /dev/shm/qwen35/bin/python inference.py`
|
| 91 |
-
- `ONE_SHOT` — removed. Zero-shot outperforms few-shot (mean 0.679 vs 0.653) on this model; few-shot was causing early termination from hallucinated items and content contamination from example pages.
|
| 92 |
-
|
| 93 |
-
## Python version
|
| 94 |
-
Use `python3.13` locally (pip maps to python3.13 here, not `python3`).
|
| 95 |
-
On rmgpu006: use `/dev/shm/qwen35/bin/python`.
|
| 96 |
-
|
| 97 |
-
---
|
| 98 |
-
|
| 99 |
-
## Reward function
|
| 100 |
-
|
| 101 |
-
Composite of 8 sub-rewards, weighted and normalised to [0, 1]:
|
| 102 |
-
|
| 103 |
-
| Reward | Weight | What it measures |
|
| 104 |
-
|---|---|---|
|
| 105 |
-
| `format` | 0.5 | Has ` ```html ` fence + `<!DOCTYPE html>` |
|
| 106 |
-
| `validity` | 0.5 | Structural completeness (`html`/`head`/`body`, diverse tags) |
|
| 107 |
-
| `structural` | 0.5 | Tag-sequence similarity + inline-style property coverage |
|
| 108 |
-
| `text_block` | 3.0 | Hungarian-matched text block IoU + text similarity |
|
| 109 |
-
| `position` | 1.0 | Hungarian-matched centroid distance |
|
| 110 |
-
| `color` | 1.5 | Spatial CIEDE2000 on reference non-white pixels |
|
| 111 |
-
| `clip` | 2.5 | CLIP ViT-B/32 cosine similarity, renormalised (threshold 0.65) |
|
| 112 |
-
| `ssim` | 1.5 | Pixel-level SSIM (skimage, 320×240 RGB) — near-perfect zone sensitivity |
|
| 113 |
-
|
| 114 |
-
**Weight sum = 11.0.** `format`/`validity`/`structural` reduced (saturate early); `color`/`clip`/`ssim` boosted for continuous near-perfect discrimination.
|
| 115 |
-
|
| 116 |
-
**Content multiplier:** applied to the weighted total. If reference has content but prediction is nearly blank (< 0.5% non-white pixels at 32×32), multiplier scales linearly from 0 to 1. Ensures blank predictions score 0.0 even if individual sub-rewards are nonzero.
|
| 117 |
-
|
| 118 |
-
**CLIP renormalisation:** raw cosine ≤ 0.65 → score 0; 1.0 → 1.0. Makes blank pages (raw ~0.45) and unstyled pages (raw ~0.76) meaningfully separated.
|
| 119 |
-
|
| 120 |
-
**Observed scores on 15 test cases (averages):**
|
| 121 |
-
```
|
| 122 |
-
perfect 0.977 minor_diff 0.883 bad_colors 0.740
|
| 123 |
-
half_styled 0.524 no_layout 0.469 no_style 0.393 blank 0.000
|
| 124 |
-
```
|
| 125 |
-
Global Spearman ρ vs canonical targets = 0.955 (15/15 PASS). Gaps improved: perfect→minor_diff +58%, minor_diff→bad_colors +51% vs old weights.
|
| 126 |
-
|
| 127 |
-
---
|
| 128 |
-
|
| 129 |
-
## Reward stability tests
|
| 130 |
-
|
| 131 |
-
Test suite at `tests/test_rewards.py`. Test data in `data/tests/<num>/` (0-14, mapping easy/0-4, medium/0-4, hard/0-4).
|
| 132 |
-
|
| 133 |
-
Each case has:
|
| 134 |
-
- `reference.html`, `variants/*.html` (7 quality levels) — committed
|
| 135 |
-
- `expected_scores.json` — per-case baseline scores — committed
|
| 136 |
-
- `renders/` — PNG renders + block JSONs — **gitignored**, auto-generated
|
| 137 |
-
|
| 138 |
-
```bash
|
| 139 |
-
# First run on a new machine (needs apptainer for Playwright on rmgpu006)
|
| 140 |
-
apptainer exec ~/apptainer-images/cuda-custom-amal_latest.sif bash -c \
|
| 141 |
-
'export PLAYWRIGHT_BROWSERS_PATH=~/playwright-browsers
|
| 142 |
-
/dev/shm/qwen35/bin/python tests/test_rewards.py --render'
|
| 143 |
-
|
| 144 |
-
# Score only (fast, uses cached renders — works outside apptainer)
|
| 145 |
-
/dev/shm/qwen35/bin/python tests/test_rewards.py
|
| 146 |
-
|
| 147 |
-
# Re-render specific cases
|
| 148 |
-
/dev/shm/qwen35/bin/python tests/test_rewards.py --render --cases 0,1,5
|
| 149 |
-
|
| 150 |
-
# After changing reward functions, lock in new baseline
|
| 151 |
-
/dev/shm/qwen35/bin/python tests/test_rewards.py --update-expected
|
| 152 |
-
|
| 153 |
-
# As pytest (unit tests only, no Playwright needed)
|
| 154 |
-
/dev/shm/qwen35/bin/python -m pytest tests/test_rewards.py -v -m "not integration"
|
| 155 |
-
```
|
| 156 |
-
|
| 157 |
-
Pass criteria: Spearman ρ ≥ 0.80 per case, global ρ ≥ 0.85, blank ≤ 0.05, perfect ≥ 0.80.
|
| 158 |
-
|
| 159 |
-
---
|
| 160 |
-
|
| 161 |
-
## Round 2 architecture
|
| 162 |
-
|
| 163 |
-
### Models
|
| 164 |
-
| Role | Inference (eval) | Training |
|
| 165 |
-
|---|---|---|
|
| 166 |
-
| Developer | `Qwen/Qwen3.5-35B-A3B` via HF router | `Qwen/Qwen3.5-2B` with LoRA (rank=16) |
|
| 167 |
-
| Critic | `Qwen/Qwen3.5-35B-A3B` via HF router | shared 2B base |
|
| 168 |
-
|
| 169 |
-
- Qwen3.5 is unified vision+text — no separate VL variant needed
|
| 170 |
-
- 2B fits 2×A100 with LoRA; training completes in ~2h for 20 episodes × 4 rollouts
|
| 171 |
-
- Run 2 uses `--resume-from checkpoints/run2/developer_final` for continued improvement
|
| 172 |
-
|
| 173 |
-
### Environment changes (server/)
|
| 174 |
-
- `reset()` returns task + `session_id`; session state lives in-memory per episode
|
| 175 |
-
- `step(html, session_id)` → `{reward, render_low (base64), render_full (base64), done}`
|
| 176 |
-
- `render(html)` → `{image (base64)}` — renders only, no reward (used by Developer tool call)
|
| 177 |
-
- `done=true` when max steps reached
|
| 178 |
-
|
| 179 |
-
### Multi-agent inference loop (inference.py)
|
| 180 |
-
```
|
| 181 |
-
for each task (episode):
|
| 182 |
-
state = env.reset() # → session_id, reference_image
|
| 183 |
-
code, critique, render_prev = "", None, None
|
| 184 |
-
|
| 185 |
-
for step i in range(MAX_STEPS):
|
| 186 |
-
# Developer: fast mode, render() tool available
|
| 187 |
-
# Input: reference_image (low-res) + code + critique
|
| 188 |
-
# Calls render(new_html) mid-generation to self-check
|
| 189 |
-
code = developer.generate(ref_image, code, critique)
|
| 190 |
-
|
| 191 |
-
result = env.step(code, session_id) # → reward, render_low, render_full, done
|
| 192 |
-
log_step(i, code, result.reward, result.done)
|
| 193 |
-
if result.done: break
|
| 194 |
-
|
| 195 |
-
# Critic: thinking mode on
|
| 196 |
-
# Input: reference_image (full-res) + render_{i-1} + critique_{i-1} + render_i
|
| 197 |
-
critique = critic.review(ref_image, render_prev, critique, result.render_full)
|
| 198 |
-
if "DONE" in critique: break
|
| 199 |
-
|
| 200 |
-
render_prev = result.render_full
|
| 201 |
-
|
| 202 |
-
log_end(...)
|
| 203 |
-
```
|
| 204 |
-
|
| 205 |
-
### RL training (train.py)
|
| 206 |
-
|
| 207 |
-
**Reward function (per turn t in trajectory):**
|
| 208 |
-
```
|
| 209 |
-
R_total(t) = R_terminal + λ · Σ(r_s - r_{s-1} for s = t..n)
|
| 210 |
-
|
| 211 |
-
R_terminal = environment score at final step n ← main signal
|
| 212 |
-
r_s - r_{s-1} = per-step improvement delta ← shaped signal
|
| 213 |
-
λ = 0.2 ← keeps shaped signal subordinate
|
| 214 |
-
```
|
| 215 |
-
|
| 216 |
-
- `R_terminal` propagates backward to all turns (solves long-horizon credit assignment)
|
| 217 |
-
- Shaped reward gives additional gradient signal at early turns without dominating
|
| 218 |
-
- Both Developer and Critic tokens receive this advantage; Critic's shaped reward
|
| 219 |
-
is the improvement delta from step i+1 onward (first step after its critique)
|
| 220 |
-
|
| 221 |
-
**Training algorithm: full-episode GRPO**
|
| 222 |
-
```
|
| 223 |
-
for each task:
|
| 224 |
-
sample K full trajectories τ_1..τ_K (different temperatures/seeds)
|
| 225 |
-
score each trajectory: R_terminal_k + shaped deltas
|
| 226 |
-
compute group-relative advantage: A_t = (G_t - mean_k) / std_k
|
| 227 |
-
update: ∇ log π(a_t | s_t) · A_t for all tokens in trajectory
|
| 228 |
-
```
|
| 229 |
-
|
| 230 |
-
**Alternating training schedule:**
|
| 231 |
-
```
|
| 232 |
-
Phase A (N episodes): Train Developer (LoRA), freeze Critic
|
| 233 |
-
Phase B (N episodes): Train Critic (LoRA, thinking on), freeze Developer
|
| 234 |
-
Repeat until convergence
|
| 235 |
-
```
|
| 236 |
-
|
| 237 |
-
### inference.py output format (unchanged from Round 1)
|
| 238 |
-
```
|
| 239 |
-
[START] task=<difficulty> env=vision-coder model=<model>
|
| 240 |
-
[STEP] step=<n> action=<html_preview> reward=<0.00> done=<true|false> error=<msg|null>
|
| 241 |
-
[END] success=<true|false> steps=<n> score=<0.000> rewards=<r1,...>
|
| 242 |
-
```
|
| 243 |
-
|
| 244 |
-
---
|
| 245 |
-
|
| 246 |
-
## Git remotes
|
| 247 |
-
Two remotes must both be kept in sync:
|
| 248 |
-
- `origin` → GitHub (`https://github.com/amaljoe/vision-coder-openenv`)
|
| 249 |
-
- `hf` → HuggingFace Spaces (`https://huggingface.co/spaces/amaljoe88/vision-coder-openenv`)
|
| 250 |
-
|
| 251 |
-
**A `post-push` hook auto-pushes to `hf` whenever you push to `origin`.** Never push only to origin and forget HF — that's what caused multiple Phase 2 failures (HF Space was 10 commits behind for most of the day).
|
| 252 |
-
|
| 253 |
-
To deploy manually use the `/deploy` skill which pushes both, waits for build, and health-checks `/reset`.
|
| 254 |
-
|
| 255 |
-
---
|
| 256 |
-
|
| 257 |
-
## Submission workflow
|
| 258 |
-
|
| 259 |
-
### 1. Push code
|
| 260 |
-
```bash
|
| 261 |
-
git push origin main # post-push hook auto-syncs to hf
|
| 262 |
-
```
|
| 263 |
-
|
| 264 |
-
### 2. Verify HF Space is live
|
| 265 |
-
```bash
|
| 266 |
-
curl -s "https://huggingface.co/api/spaces/amaljoe88/vision-coder-openenv" \
|
| 267 |
-
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['runtime']['stage'], d['sha'][:8])"
|
| 268 |
-
```
|
| 269 |
-
Wait for `stage: RUNNING`. Docker build with CLIP takes 5–10 minutes.
|
| 270 |
-
|
| 271 |
-
### 3. Health check
|
| 272 |
-
```bash
|
| 273 |
-
curl -X POST "https://amaljoe88-vision-coder-openenv.hf.space/reset?difficulty=easy" -o /dev/null -w "%{http_code}"
|
| 274 |
-
# Must return 200
|
| 275 |
-
```
|
| 276 |
-
|
| 277 |
-
### 4. Submit via dashboard
|
| 278 |
-
Only the team lead submits at: `https://www.scaler.com/school-of-technology/meta-pytorch-hackathon/dashboard`
|
| 279 |
-
- GitHub URL: `https://github.com/amaljoe/vision-coder-openenv`
|
| 280 |
-
- HF Space URL: `https://huggingface.co/spaces/amaljoe88/vision-coder-openenv`
|
| 281 |
-
|
| 282 |
-
### 5. Check submission status
|
| 283 |
-
Use the `/check-hackathon-status` skill or:
|
| 284 |
-
```bash
|
| 285 |
-
curl -s "https://www.scaler.com/meta-pytorch-hackathon/api/v1/submissions/status?round=1" \
|
| 286 |
-
-H "Authorization: Bearer BAh7B0kiDHVzZXJfaWQGOgZFVGkCcp1JIg5pc3N1ZWRfYXQGOwBUbCsHLfLDaQ==--bf98a96719f439decefc69bb1a42fe0d28fa29f82fb956d8546f326225af310d" \
|
| 287 |
-
| python3 -m json.tool
|
| 288 |
-
```
|
| 289 |
-
Check `validation.agentic_evaluation.status` — target is `"success"` with all 5 steps `"pass"`.
|
| 290 |
-
|
| 291 |
-
---
|
| 292 |
-
|
| 293 |
-
## How evaluation works
|
| 294 |
-
|
| 295 |
-
The evaluator does NOT use the HF Space to run inference. The flow is:
|
| 296 |
-
1. **HF Space ping** — pre-submission only: `POST /reset` must return 200
|
| 297 |
-
2. **Agentic eval**:
|
| 298 |
-
- Clones GitHub repo to `/tmp/workspace/`
|
| 299 |
-
- `docker build` from `Dockerfile`
|
| 300 |
-
- Runs `inference.py` inside Docker with `HF_TOKEN`, `API_BASE_URL`, `MODEL_NAME` set
|
| 301 |
-
- Parses `[START]`/`[STEP]`/`[END]` stdout
|
| 302 |
-
- Validates task scores
|
| 303 |
-
|
| 304 |
-
Eval pipeline: `docker_build` → `inference` → `parse_output` → `task_validation` → `llm_check`
|
| 305 |
-
|
| 306 |
-
---
|
| 307 |
-
|
| 308 |
-
## inference.py requirements (critical)
|
| 309 |
-
|
| 310 |
-
Rules enforced by evaluator parser:
|
| 311 |
-
- `action=` must be a plain string — **no `!r` repr quoting** (caused one failure)
|
| 312 |
-
- `done` and `success` are lowercase: `true`/`false`
|
| 313 |
-
- `error` is `null` if none
|
| 314 |
-
- `[END]` must always be emitted — put it in a `finally` block
|
| 315 |
-
- Exit code must be 0 — wrap everything in try/except
|
| 316 |
-
- LLM call must be inside try/except with a fallback HTML so it never crashes
|
| 317 |
-
|
| 318 |
-
Auth variable priority (evaluator sets `HF_TOKEN`, not `OPENAI_API_KEY`):
|
| 319 |
-
```python
|
| 320 |
-
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("OPENAI_API_KEY") or "sk-placeholder"
|
| 321 |
-
```
|
| 322 |
-
|
| 323 |
-
---
|
| 324 |
-
|
| 325 |
-
## Challenges faced and lessons learned
|
| 326 |
-
|
| 327 |
-
### 1. HF Space drifted 10 commits behind
|
| 328 |
-
**What happened:** `git push origin main` only updates GitHub. HF Spaces has a separate `hf` remote that needs an explicit `git push hf main`. We pushed all code fixes to GitHub but forgot to push to HF, so the Space ran month-old code for the entire evaluation window.
|
| 329 |
-
|
| 330 |
-
**Fix:** Added `.git/hooks/post-push` that auto-pushes to `hf` whenever `origin` is pushed.
|
| 331 |
-
|
| 332 |
-
---
|
| 333 |
-
|
| 334 |
-
### 2. Auth failure — wrong API key env var
|
| 335 |
-
**What happened:** Old `inference.py` read `OPENAI_API_KEY` but the evaluator only sets `HF_TOKEN`. This caused every LLM call to use `"sk-placeholder"` → 401 auth error → crash.
|
| 336 |
-
|
| 337 |
-
**Fix:** Read `HF_TOKEN` first, fall back through `API_KEY` → `OPENAI_API_KEY` → placeholder.
|
| 338 |
-
|
| 339 |
-
---
|
| 340 |
-
|
| 341 |
-
### 3. Timing race — 3-minute window between commit and submission
|
| 342 |
-
**What happened:** Commit `4dc307c` (inference.py rewrite with auth fix) was pushed at 11:49 AM. User submitted at 11:52 AM — only 3 minutes later. The evaluator cloned the repo before the push fully propagated and ran the older version without proper try/except. The submission failed despite the fix being "live".
|
| 343 |
-
|
| 344 |
-
**Lesson:** After pushing a critical fix, wait at least 5–10 minutes before resubmitting to avoid the evaluator picking up stale code.
|
| 345 |
-
|
| 346 |
-
---
|
| 347 |
-
|
| 348 |
-
### 4. `!r` repr quoting on action field broke output parsing
|
| 349 |
-
**What happened:** `log_step` used `f"action={action!r}"` which wraps the HTML string in Python quotes: `action='<!DOCTYPE...'`. The evaluator parser expected `action=<!DOCTYPE...` (no quotes). Found by comparing against the sample inference script.
|
| 350 |
-
|
| 351 |
-
**Fix:** Changed `{action_summary!r}` to `{action_summary}`.
|
| 352 |
-
|
| 353 |
-
---
|
| 354 |
-
|
| 355 |
-
### 5. transformers v5 API change for CLIP
|
| 356 |
-
**What happened:** `model.get_image_features()` in transformers v5 returns a dataclass (`BaseModelOutputWithPooling`), not a raw tensor. Code that did `features / features.norm()` crashed with `'BaseModelOutputWithPooling' object has no attribute 'norm'`.
|
| 357 |
-
|
| 358 |
-
**Fix:**
|
| 359 |
-
```python
|
| 360 |
-
out = model.get_image_features(pixel_values=pv)
|
| 361 |
-
features = out.pooler_output if hasattr(out, "pooler_output") else out
|
| 362 |
-
```
|
| 363 |
-
|
| 364 |
-
---
|
| 365 |
-
|
| 366 |
-
### 6. Docker build downloads ~1.2GB — fragile on slow networks
|
| 367 |
-
The Dockerfile downloads: `torch` CPU (~600MB), CLIP model weights (~600MB), Playwright Chromium (~400MB). If any download fails or times out during `docker build`, the whole eval fails at `docker_build` step.
|
| 368 |
-
|
| 369 |
-
**Mitigation:** Keep these RUN layers cached by not changing requirements.txt or the download commands unnecessarily.
|
| 370 |
-
|
| 371 |
-
---
|
| 372 |
-
|
| 373 |
-
### 7. 4 Playwright browser launches per step (performance)
|
| 374 |
-
**What happened:** `text_block_reward`, `position_reward`, `color_reward`, and `clip_visual_reward` each launched Playwright independently — 4–6 browser sessions per `step()` call.
|
| 375 |
-
|
| 376 |
-
**Fix:** Render the predicted HTML once in `environment.py`, pass the `PIL.Image` as `pred_image` parameter to both `color_reward` and `clip_visual_reward` to skip duplicate renders.
|
| 377 |
-
|
| 378 |
-
---
|
| 379 |
-
|
| 380 |
-
### 9. `color_reward` false positive on white-background pages
|
| 381 |
-
**What happened:** Original implementation sampled non-white pixels independently from each image, then compared them. A blank white prediction vs a mostly-white reference (e.g. `#f0f2f5` login form) both sampled near-white pixels → CIEDE2000 ≈ 0 → score 0.80–0.96 (false high).
|
| 382 |
-
|
| 383 |
-
**Fix:** Spatial comparison — resize both images to 128×128, compute per-pixel CIEDE2000, average only over positions where the **reference** is non-white. Blank prediction at those positions gets correct high ΔE. Falls back to full mean when reference is nearly all white (< 2% non-white).
|
| 384 |
-
|
| 385 |
-
---
|
| 386 |
-
|
| 387 |
-
### 10. `structural_reward` trivially inflated for inline-style HTML
|
| 388 |
-
**What happened:** All 15 reference HTMLs use inline `style=""` attributes, not CSS classes. The CSS class overlap term always returned 1.0 (neither ref nor pred had classes), making blank pages score 0.50 on structural instead of ~0.25.
|
| 389 |
-
|
| 390 |
-
**Fix:** When ref has no CSS classes, fall back to inline style **property name** coverage: `len(pred_props ∩ ref_props) / len(ref_props)`. Blank page has 1–2 style props vs 15–25 in ref → score 0.08–0.25. Perfect match (same HTML) → same props → 1.0.
|
| 391 |
-
|
| 392 |
-
---
|
| 393 |
-
|
| 394 |
-
### 11. `validity_reward` too generous on blank pages
|
| 395 |
-
**What happened:** `_MIN_DIVERSE_TAGS` was 5. A blank page with 4 tags (`html`, `head`, `title`, `body`) scored 4/5 = 0.80 on diversity, giving total validity ≈ 0.90.
|
| 396 |
-
|
| 397 |
-
**Fix:** Raised to 8. Blank page now scores 4/8 = 0.50 on diversity → total validity ≈ 0.75.
|
| 398 |
-
|
| 399 |
-
---
|
| 400 |
-
|
| 401 |
-
### 12. Per-step GRPO loses long-horizon signal (Round 2 lesson)
|
| 402 |
-
**What happened (design trap):** Applying GRPO independently at each step means Dev_0 only sees `r_0` — the final reward never flows back. Early turns get misguided signal regardless of episode outcome.
|
| 403 |
-
|
| 404 |
-
**Fix:** Full-episode GRPO — sample K complete trajectories, apply group-relative advantage to all tokens uniformly. Augment with shaped improvement-delta reward (λ=0.2) for early-turn credit assignment. See `train.py`.
|
| 405 |
-
|
| 406 |
-
---
|
| 407 |
-
|
| 408 |
-
### 13. Critic DONE-too-early collapses GRPO variance
|
| 409 |
-
**What happened:** The training CRITIC_TRAIN_SYSTEM said "Output DONE if closely matches" (too permissive). The Critic said DONE after step 1 on medium/hard tasks regardless of quality. With all 4 rollouts at 1 step and similar rewards, group std ≈ 0 → advantages ≈ 0 → no gradient signal.
|
| 410 |
-
|
| 411 |
-
**Fix:** Stricter prompt: "Output DONE only if >90% visual similarity. If ANY section is missing/wrong, list it — do NOT output DONE." Fixes variance collapse for run 2.
|
| 412 |
-
|
| 413 |
-
---
|
| 414 |
-
|
| 415 |
-
### 14. MAX_NEW_TOKENS=1024 caused truncated HTML → artificially low training rewards
|
| 416 |
-
**What happened:** Complex HTML pages need 1500–2500 tokens. With MAX_NEW_TOKENS=1024, the model generated truncated HTML missing closing tags and lower sections. Reward was dominated by fmt/validity only (clip=0 because truncated HTML renders blank/broken).
|
| 417 |
-
|
| 418 |
-
**Fix:** Increased to 2048 in src/train.py. Also helps that 2B model's training rewards (0.23–0.35) were far below vLLM inference rewards (0.6+) — gap partly explained by truncation.
|
| 419 |
-
|
| 420 |
-
---
|
| 421 |
-
|
| 422 |
-
### 15. GRPO breakthrough emerges at ep=16 (easy difficulty)
|
| 423 |
-
**What happened:** After 15 episodes of noisy rewards (0.23–0.35 for easy), ep=16 easy jumped to 0.496 mean reward. Individual rollouts reached 0.82 with clip=0.95 (raw cosine ~0.98). The model learned to generate HTML with high visual similarity.
|
| 424 |
-
|
| 425 |
-
**Why it happened:** GRPO with variance in rollouts — when 1 of 4 rollouts achieves clip=0.90+ while others get 0.00, the group advantage strongly reinforces the high-scoring generation strategy. This is the critical moment when GRPO starts working.
|
| 426 |
-
|
| 427 |
-
**Observation:** Medium and hard tasks didn't break through in run 1 due to Critic early-termination. Run 2 (with fixed CRITIC_TRAIN_SYSTEM) should show similar breakthrough for all difficulties.
|
| 428 |
-
|
| 429 |
-
---
|
| 430 |
-
|
| 431 |
-
### 16. eval_lora.py — comparing trained vs base without vLLM
|
| 432 |
-
**Command (runs after training, outside apptainer):**
|
| 433 |
-
```bash
|
| 434 |
-
export PLAYWRIGHT_BROWSERS_PATH=~/playwright-browsers
|
| 435 |
-
/dev/shm/qwen35/bin/python eval_lora.py \
|
| 436 |
-
--lora-path checkpoints/run2/developer_final \
|
| 437 |
-
--model ~/models/Qwen3.5-2B \
|
| 438 |
-
--episodes 2
|
| 439 |
-
```
|
| 440 |
-
Outputs blog-ready markdown table + saves `checkpoints/eval_results.json`.
|
| 441 |
-
|
| 442 |
-
**Run 1 results** (20/20 episodes, `checkpoints/run2/developer_final`, 2 ep/difficulty, temperature=0.3):
|
| 443 |
-
| Difficulty | Base 2B | Trained 2B | Delta |
|
| 444 |
-
|---|---|---|---|
|
| 445 |
-
| easy | 0.924 | **0.961** | +0.037 |
|
| 446 |
-
| medium | 0.937 | **0.955** | +0.018 |
|
| 447 |
-
| hard | 0.919 | **0.952** | +0.034 |
|
| 448 |
-
| **mean** | 0.927 | **0.956** | **+3.2%** |
|
| 449 |
-
|
| 450 |
-
> Evaluated on bundled training samples (in-distribution). Relative delta is the meaningful signal.
|
| 451 |
-
|
| 452 |
-
**Command to restart vLLM with trained LoRA** (in tmux `vllm` session, after killing old process):
|
| 453 |
-
```bash
|
| 454 |
-
LORA_PATH=checkpoints/run2/developer_final LORA_NAME=qwen35-trained bash scripts/vllm.sh 2b
|
| 455 |
-
# Then use MODEL_NAME=qwen35-trained in inference.py for trained model
|
| 456 |
-
```
|
| 457 |
-
|
| 458 |
-
**Command to start run 2 (resume from run 1 LoRA):**
|
| 459 |
-
```bash
|
| 460 |
-
apptainer exec --nv ~/apptainer-images/cuda-custom-amal_latest.sif bash -c '
|
| 461 |
-
export LD_PRELOAD=/dev/shm/qwen35/lib/libstdc++.so.6
|
| 462 |
-
/dev/shm/qwen35/bin/python train.py \
|
| 463 |
-
--phase developer \
|
| 464 |
-
--episodes 10 \
|
| 465 |
-
--k-rollouts 4 \
|
| 466 |
-
--model ~/models/Qwen3.5-2B \
|
| 467 |
-
--checkpoint-dir checkpoints/run3 \
|
| 468 |
-
--resume-from checkpoints/run2/developer_final
|
| 469 |
-
' 2>&1 | tee checkpoints/train_run3.log
|
| 470 |
-
```
|
| 471 |
-
|
| 472 |
-
---
|
| 473 |
-
|
| 474 |
-
## Self-Update Protocol
|
| 475 |
-
|
| 476 |
-
**This file is a living document. Claude must keep it current.**
|
| 477 |
-
|
| 478 |
-
Whenever you discover something not already recorded here — a new flag, env var, command, lesson, bug, or architectural decision — add it immediately. Do not wait to be asked.
|
| 479 |
-
|
| 480 |
-
### What to update and where
|
| 481 |
-
|
| 482 |
-
| Discovery | Where to add |
|
| 483 |
-
|---|---|
|
| 484 |
-
| New `--flag` for vLLM / uvicorn / inference | Relevant step in "Running on rmgpu006" |
|
| 485 |
-
| New environment variable | "Environment variables" section |
|
| 486 |
-
| New bug or production incident | "Challenges faced and lessons learned" (next numbered entry) |
|
| 487 |
-
| New architectural decision | Relevant subsection under "Round 2 architecture" |
|
| 488 |
-
| Change to submission / eval behavior | "Submission workflow" or "How evaluation works" |
|
| 489 |
-
| New debug flag or mode | "Environment variables" section with a note on what it enables |
|
| 490 |
-
|
| 491 |
-
### Rules
|
| 492 |
-
- Write lessons in past tense under "Challenges faced" — **What happened**, **Fix** format.
|
| 493 |
-
- Keep commands copy-pasteable and tested; update them if they change.
|
| 494 |
-
- After updating this file, commit it: `git add CLAUDE.md && git commit -m "docs: update CLAUDE.md — <one-line summary>"`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
blog.md
DELETED
|
@@ -1,214 +0,0 @@
|
|
| 1 |
-
# VisionCoder OpenEnv | Screenshot-to-HTML with Multi-Agent RL
|
| 2 |
-
|
| 3 |
-
**Scaler × Meta PyTorch Hackathon 2026 | Solo submission by [@amaljoe88](https://huggingface.co/spaces/amaljoe88/vision-coder-openenv)**
|
| 4 |
-
|
| 5 |
-
Themes covered: Multi-Agent Interactions, (Super) Long-Horizon Planning & Instruction Following, Self-Improvement
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## The Problem
|
| 10 |
-
|
| 11 |
-
Turn a screenshot into working HTML. It sounds simple but it forces a model to do two hard things at once: *understand what the UI looks like visually* and *express that understanding in code*. A single LLM call tends to produce structurally valid HTML that looks nothing like the reference. Headings are present, a button is present but the layout is wrong, colors are off, nothing is positioned correctly.
|
| 12 |
-
|
| 13 |
-
The deeper problem: **the model can't see its own output.** It generates HTML blindly, has no way to compare what it produced against the target, and has no feedback loop to improve.
|
| 14 |
-
|
| 15 |
-
We turned this into a **reinforcement learning problem**. The agent generates HTML, a real browser renders it, a reward function computes visual similarity to the reference, and the agent iterates. The environment runs as an HTTP API compatible with the OpenEnv standard.
|
| 16 |
-
|
| 17 |
-
---
|
| 18 |
-
|
| 19 |
-
## The Environment
|
| 20 |
-
|
| 21 |
-
### OpenEnv-Compatible HTTP API
|
| 22 |
-
|
| 23 |
-
```
|
| 24 |
-
POST /reset?difficulty=easy|medium|hard → { session_id, screenshot_b64 }
|
| 25 |
-
POST /step { html, session_id } → { reward, render_low, render_full, done }
|
| 26 |
-
POST /render { html } → { image_b64 }
|
| 27 |
-
```
|
| 28 |
-
|
| 29 |
-
Every HTML submission is rendered by a headless Chromium at two resolutions: `320×240` (low-res, passed back to the Developer each turn) and `640×480` (full-res, used by the Critic and reward computation). Episodes run for up to n(=5) steps.
|
| 30 |
-
|
| 31 |
-
### Composite Reward Function
|
| 32 |
-
|
| 33 |
-
The reward is a weighted sum of 8 sub-scores, each measuring a different aspect of visual and structural similarity.
|
| 34 |
-
> Fun Fact: The weights asssigned to each reward were tuned using an auto research style approach (similar to [Andrej Karpathy's](https://github.com/karpathy/autoresearch)) - an AI agent loops through a large set of candidate weight combinations parallely and compares the reward ranking against human quality judgements to find the best correlation.
|
| 35 |
-
|
| 36 |
-

|
| 37 |
-
|
| 38 |
-
| Reward | Weight | What it measures |
|
| 39 |
-
|---|---|---|
|
| 40 |
-
| `format` | 0.5 | Has ` ```html ` fence + `<!DOCTYPE html>` |
|
| 41 |
-
| `validity` | 0.5 | Structural completeness (html/head/body, diverse tags) |
|
| 42 |
-
| `structural` | 0.5 | Tag-sequence similarity + inline-style property coverage |
|
| 43 |
-
| `text_block` | **3.0** | Hungarian-matched text block IoU + text similarity |
|
| 44 |
-
| `position` | 1.0 | Hungarian-matched centroid distance |
|
| 45 |
-
| `color` | 1.5 | Spatial CIEDE2000 on reference non-white pixels |
|
| 46 |
-
| `clip` | **2.5** | CLIP ViT-B/32 cosine similarity, renormalised (threshold 0.65) |
|
| 47 |
-
| `ssim` | 1.5 | Pixel-level SSIM (skimage, 320×240 RGB) |
|
| 48 |
-
|
| 49 |
-
Low-weight rewards (`format`, `validity`, `structural`) saturate early, a structurally complete page already scores near 1.0 on these regardless of visual quality. The high-weight rewards (`text_block`, `clip`, `ssim`) stay discriminative all the way to near-perfect renders. This keeps the gradient signal alive even when the model is already producing good output.
|
| 50 |
-
|
| 51 |
-
### Does the Reward Reflect Human Judgement?
|
| 52 |
-
|
| 53 |
-
We validated the final reward function against human-labelled quality levels across 15 reference pages (5 per difficulty). For each reference, we tested 7 variants ranging from blank to perfect:
|
| 54 |
-
|
| 55 |
-

|
| 56 |
-
|
| 57 |
-
**Global Spearman ρ = 0.955** — the reward ranking matches human quality judgement on most of the test cases. The chart above shows the reward correctly ordering all 7 levels with clear gaps between them.
|
| 58 |
-
|
| 59 |
-
Browse all 15 test case renders with per-sub-reward breakdowns in the **[interactive demo](https://amaljoe.github.io/vision-coder-openenv/)**.
|
| 60 |
-
|
| 61 |
-
The grid below shows sampled renders from three tasks alongside their reward scores. Each row shows a reference and three variants at different quality levels, ordered from best to worst:
|
| 62 |
-
|
| 63 |
-

|
| 64 |
-
|
| 65 |
-
> **Content Multiplier:** We noticed strong correlation with human judgement for most pages, but blank renders were receiving rewards of ~0.3 due to sub-rewards like `format` and `validity` that don't require visual content. To fix this, we applied a content multiplier: if the predicted render has fewer than 0.5% non-white pixels while the reference has content, the total reward is forced to 0. A blank page which typically means something prevented rendering (a JavaScript error, a malformed tag, or the model failing to generate HTML at all) now gets the worst possible reward and is correctly treated as a major failure signal.
|
| 66 |
-
|
| 67 |
-
---
|
| 68 |
-
|
| 69 |
-
## The Multi-Agent Architecture
|
| 70 |
-
|
| 71 |
-
### Why Two Agents?
|
| 72 |
-
|
| 73 |
-
A single agent can generate HTML and receive a reward. But the reward is a single number: it tells the model *how bad* the output is, not *what is wrong* or *which selector to fix*. Without visual feedback, the model improvises changes at random and often regresses.
|
| 74 |
-
|
| 75 |
-
The Critic solves this. It looks at both the reference and the current render side by side, reads the HTML source, and produces specific CSS fix instructions. The Developer reads those fixes and applies them in the next step; no guessing required.
|
| 76 |
-
|
| 77 |
-

|
| 78 |
-
|
| 79 |
-
### Why Not Just Pass Everything to One Model?
|
| 80 |
-
|
| 81 |
-
Context cost. Vision models encode images as sequences of tokens; the number of tokens scales with pixel count:
|
| 82 |
-
|
| 83 |
-
| Image | Resolution | Visual tokens |
|
| 84 |
-
|---|---|---|
|
| 85 |
-
| Low-res render | 320×240 | ~256 |
|
| 86 |
-
| Full-res render / reference | 640×480 | ~1,024 |
|
| 87 |
-
| Full HD (hypothetical) | 1920×1080 | ~9,800 |
|
| 88 |
-
|
| 89 |
-
With full-HD inputs, two images alone would cost ~19,600 tokens exhausting the context budget of a typical consumer GPU before a single token of HTML is generated. Even at our working resolution, giving the Developer both high-res images every step would double its context cost per step across the entire episode and this cost increases quadratically with higher resolutions.
|
| 90 |
-
|
| 91 |
-
### What the Critic Produces
|
| 92 |
-
|
| 93 |
-
```
|
| 94 |
-
[+] HIGH | LAYOUT — products grid is 1-column; reference shows 3-column
|
| 95 |
-
→ FIX: `.products { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }`
|
| 96 |
-
|
| 97 |
-
[+] MEDIUM | COLOR — nav background is white; reference shows dark navy
|
| 98 |
-
→ FIX: `nav { background-color: #0f172a; }`
|
| 99 |
-
```
|
| 100 |
-
|
| 101 |
-
This is fundamentally different from abstract feedback ("the layout is wrong"). The Developer reads the `→ FIX:` line and applies it to the exact CSS selector, no interpretation required.
|
| 102 |
-
|
| 103 |
-
### Self-Improvement Over an Episode
|
| 104 |
-
|
| 105 |
-
Each developer step sees the HTML code generated so far alongside reviews from the critic model and its low-resolution renders (to maintain a manageable context size).
|
| 106 |
-
|
| 107 |
-
The graph below shows what happens with and without the Critic over a 5-step episode:
|
| 108 |
-
|
| 109 |
-

|
| 110 |
-
|
| 111 |
-
Without structured feedback, the Developer oscillates: it makes changes that sometimes improve and sometimes regress the reward. With the Critic providing selector-specific fixes, the reward climbs monotonically. By step 5, Developer + Critic has opened a **Δ0.18 gap** over Developer Only.
|
| 112 |
-
|
| 113 |
-
---
|
| 114 |
-
|
| 115 |
-
## RL Training: Full-Episode GRPO
|
| 116 |
-
|
| 117 |
-
### Full-Episode Training
|
| 118 |
-
|
| 119 |
-
Full-episode GRPO samples K complete trajectories, scores each one by total episode reward, and applies group-relative advantage to every token in the trajectory. Reward shaping is also used to add additional intermediate rewards (difference in rewards between each iteration):
|
| 120 |
-
|
| 121 |
-
```
|
| 122 |
-
R_total(t) = R_terminal + λ · Σ(r_s - r_{s-1} for s = t..n)
|
| 123 |
-
|
| 124 |
-
R_terminal = environment score at final step n ← main signal
|
| 125 |
-
r_s - r_{s-1} = per-step improvement delta ← shaped signal
|
| 126 |
-
λ = 0.2 ← keeps shaped signal subordinate
|
| 127 |
-
```
|
| 128 |
-
|
| 129 |
-
```
|
| 130 |
-
for each task:
|
| 131 |
-
sample K=4 full trajectories (different temperatures/seeds)
|
| 132 |
-
score each: R_terminal_k + shaped improvement deltas
|
| 133 |
-
advantage: A_t = (G_t - mean_k) / std_k
|
| 134 |
-
update: ∇ log π(a_t | s_t) · A_t for all tokens in trajectory
|
| 135 |
-
```
|
| 136 |
-
|
| 137 |
-
### Training Configuration
|
| 138 |
-
|
| 139 |
-
- **Base model**: [`Qwen/Qwen3.5-2B`](https://huggingface.co/Qwen/Qwen3.5-2B) (unified vision+text)
|
| 140 |
-
- **LoRA**: rank=16, α=32, 0.49% trainable parameters (10.9M / 2.2B)
|
| 141 |
-
- **Optimizer**: AdamW, lr=2e-5, max_grad_norm=1.0
|
| 142 |
-
- **Hardware**: 2× NVIDIA A100 80GB PCIe
|
| 143 |
-
- **Episodes**: 20 × 4 rollouts = 80 trajectories
|
| 144 |
-
|
| 145 |
-
### Training Curve
|
| 146 |
-
|
| 147 |
-

|
| 148 |
-
|
| 149 |
-
The three difficulty tracks tell different stories:
|
| 150 |
-
|
| 151 |
-
**Easy (blue)** starts at 0.629. Simple login forms and single-column layouts are already within reach of the base model. There is very little headroom left, so the curve shows mostly small fluctuations with a slight upward drift. The model is already close to its ceiling on these tasks at baseline.
|
| 152 |
-
|
| 153 |
-
**Medium (green)** starts at 0.488 and ends at 0.634 (+0.146). Multi-column grids and landing pages require the Critic's feedback to land correctly. The reward climbs early as the model learns to apply CSS fixes more precisely.
|
| 154 |
-
|
| 155 |
-
**Hard (red)** shows the clearest improvement: 0.346 → 0.564 (+0.218). Complex dashboards and Kanban boards depend on deeply nested flex/grid structures where small CSS errors collapse entire layout regions. At baseline, the model struggles to reconstruct these. With GRPO reinforcing the Critic's CSS fix patterns, it learns which selectors control which regions and how to fix them efficiently. The performance keeps on climbing even at 20 iterations and shows potential for more improvement. **Hard tasks benefit the most because they have the most to gain.**
|
| 156 |
-
|
| 157 |
-
---
|
| 158 |
-
|
| 159 |
-
## RL Training Results: Base vs Trained 2B
|
| 160 |
-
|
| 161 |
-
Scores at iteration 0 (untrained) vs iteration 20 (after GRPO training), from `https://raw.githubusercontent.com/amaljoe/vision-coder-openenv/main/assets/train.jsonl`:
|
| 162 |
-
|
| 163 |
-
| Difficulty | Base (iter 0) | Trained (iter 20) | Delta |
|
| 164 |
-
|---|---|---|---|
|
| 165 |
-
| easy | 0.629 | **0.634** | +0.005 |
|
| 166 |
-
| medium | 0.488 | **0.634** | +0.146 |
|
| 167 |
-
| hard | 0.346 | **0.564** | +0.218 |
|
| 168 |
-
| **mean** | 0.488 | **0.611** | +0.123 |
|
| 169 |
-
|
| 170 |
-
**+25.2% overall improvement** from 20 iterations of full-episode GRPO on 2× A100 80GB (~2h). The pattern matches the training curve: easy was already near its ceiling, medium gained meaningfully, and hard improved the most. The Critic's structured feedback is most valuable precisely where the task is most complex.
|
| 171 |
-
|
| 172 |
-
---
|
| 173 |
-
|
| 174 |
-
## Reproduce
|
| 175 |
-
|
| 176 |
-
### Run the Environment
|
| 177 |
-
|
| 178 |
-
```bash
|
| 179 |
-
pip install -e .
|
| 180 |
-
uvicorn openenv.server.app:app --host 0.0.0.0 --port 7860
|
| 181 |
-
```
|
| 182 |
-
|
| 183 |
-
### Run Inference
|
| 184 |
-
|
| 185 |
-
```bash
|
| 186 |
-
export API_BASE_URL=https://router.huggingface.co/v1
|
| 187 |
-
export MODEL_NAME=Qwen/Qwen3.5-35B-A3B
|
| 188 |
-
export HF_TOKEN=hf_...
|
| 189 |
-
python inference.py
|
| 190 |
-
```
|
| 191 |
-
|
| 192 |
-
### Run RL Training
|
| 193 |
-
|
| 194 |
-
```bash
|
| 195 |
-
python train.py --phase combined --episodes 20 --k-rollouts 4 \
|
| 196 |
-
--model Qwen/Qwen3.5-2B --checkpoint-dir checkpoints/run1
|
| 197 |
-
```
|
| 198 |
-
|
| 199 |
-
### Run Test Suite
|
| 200 |
-
|
| 201 |
-
Run the test suite to generate rewards for the test set. These rewards can be visualised in the [interactive demo](https://amaljoe.github.io/vision-coder-openenv/).
|
| 202 |
-
|
| 203 |
-
```bash
|
| 204 |
-
python tests/test_rewards.py --render # first run (needs Playwright)
|
| 205 |
-
python tests/test_rewards.py # subsequent runs (uses cached renders)
|
| 206 |
-
```
|
| 207 |
-
|
| 208 |
-
---
|
| 209 |
-
|
| 210 |
-
## Links
|
| 211 |
-
|
| 212 |
-
- **HF Space**: [amaljoe88/vision-coder-openenv](https://huggingface.co/spaces/amaljoe88/vision-coder-openenv)
|
| 213 |
-
- **GitHub**: [amaljoe/vision-coder-openenv](https://github.com/amaljoe/vision-coder-openenv)
|
| 214 |
-
- **Interactive demo**: [amaljoe.github.io/vision-coder-openenv](https://amaljoe.github.io/vision-coder-openenv/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|