|
Download README.md from FineEnvs/DesignGym: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/FineEnvs/DesignGym/resolve/main/README.md
- Command line
-
hf download hf://spaces/FineEnvs/DesignGym/README.md
-
curl -L -o README.md https://huggingface.co/spaces/FineEnvs/DesignGym/resolve/main/README.md
13 kB
| title: DesignGym | |
| emoji: 🎨 | |
| colorFrom: indigo | |
| colorTo: pink | |
| sdk: docker | |
| app_port: 8000 | |
| base_path: /web | |
| pinned: false | |
| tags: [openenv, mcp, computer-use, graphic-design, crello] | |
| short_description: Recreate real graphic designs via MCP, HTML or a real editor | |
| # DesignGym | |
| An [OpenEnv](https://github.com/openenv/OpenEnv) environment for rebuilding real graphic designs from | |
| their parts. Each task is a [Crello](https://huggingface.co/datasets/cyberagent/crello) template taken apart | |
| into its pre-rendered layers and its typed copy. The agent puts it back together on a canvas and is scored | |
| against a render of the original. | |
| The package, the CLI, the OpenEnv name and the `DESIGN_CANVAS_*` settings keep the id `design_canvas` | |
| (`design-canvas`). Clients, datasets and traces already use it. | |
| - **Two tasks.** `mode="reference"`: here is the target image, recreate it. `mode="description"`: here is a | |
| written brief naming every asset and line of copy, build it. | |
| - **Three surfaces over one document.** `surface="tools"`: structured MCP edits. `surface="html"`: one page of | |
| restricted HTML. `surface="gui"`: mouse and keyboard on a real browser editor, seeing only screenshots. | |
| - **One renderer, one reward.** Every surface edits the same document, and every score comes from the same | |
| skia renderer, so a GUI episode and a tool-call episode are directly comparable. | |
| ## Run it | |
| ```bash | |
| design-canvas prepare # build the task pack from Crello (~1.4 GB download, 30 s to convert) | |
| design-canvas serve --port 8007 # OpenEnv server + editor + playground | |
| ``` | |
| - Editor: `http://127.0.0.1:8007/editor/` · Playground: `/web/` · MCP: `/mcp` · Task API: `/design_canvas/...` | |
| - Or `docker build -t design-canvas . && docker run -p 8000:8000 design-canvas` (builds the pack into the image). | |
| The pack is a small offline sample. To serve **the whole corpus** (every playable Crello design) from the | |
| [FineEnvs/crello-bucket](https://huggingface.co/buckets/FineEnvs/crello-bucket) bucket, point the server at the | |
| corpus manifest: | |
| ```bash | |
| DESIGN_CANVAS_CORPUS=corpus-manifest.json design-canvas serve --port 8007 | |
| ``` | |
| The catalog (every task's ground truth, brief and difficulty) is one SQLite file copied from the bucket at | |
| startup, so listing tasks and `reset` never download images. A design's assets are fetched the first time an | |
| episode uses it: its 100-design row group is read (over HTTP locally, from the read-only `/corpus` mount on the | |
| Space), checked against the indexed record, and cached on disk under a byte budget (`DESIGN_CANVAS_CACHE`). | |
| Set `DESIGN_CANVAS_SOURCE_ROOT` to a mounted or downloaded copy of the bucket to read it as local files. | |
| ```bash | |
| design-canvas corpus build --out artifacts/corpus-index --workers 12 # convert + index every design | |
| design-canvas corpus publish --out artifacts/corpus-index # upload the catalog to the bucket | |
| design-canvas evalset build --out ../../evalsets/crello-test-300.json # freeze the evaluation set | |
| design-canvas evalset export ../../evalsets/crello-test-300.json --out artifacts/evalpack # offline copy | |
| ``` | |
| Computer-use data: record verified GUI trajectories, then write them as a Hub-ready `cua-v1` dataset | |
| (`cua_format.py`: OpenAI-style messages with pixel tool calls, embedded screenshots, per-message `trainable`; | |
| `windows()` derives SFT rows with the same screenshot window the Tinker RL adapter uses): | |
| ```bash | |
| design-canvas export-cua --split train --out artifacts/cua --limit 200 # record (webp screenshots) | |
| design-canvas export-hf --cua artifacts/cua/cua.jsonl --out artifacts/hf/cua # cua-v1 Parquet + dataset card | |
| ``` | |
| Check a running server end to end (HTTP routes, target gating, expert replays scoring 1.0 on tools and HTML in both | |
| modes, an empty submission scoring 0, concurrent episodes, and with `--gui` two verified recorder episodes; the client | |
| must load the same task source as the server): | |
| ```bash | |
| DESIGN_CANVAS_CORPUS=artifacts/corpus-index .venv/bin/python -m design_canvas.verify_server --server http://127.0.0.1:8007 --gui | |
| ``` | |
| Watch Claude work, live: | |
| ```bash | |
| claude mcp add design-canvas -- design-canvas mcp | |
| ``` | |
| then ask it to "start a design task and recreate it", and open the `editor_url` that `start_task` returns. | |
| For more throughput, `design-canvas serve --workers N` (or `DESIGN_CANVAS_WORKERS=N`) runs N server processes behind | |
| one URL ([`server/router.py`](server/router.py)). Don't use uvicorn's own `--workers`: OpenEnv's client creates a | |
| session over HTTP and then attaches a WebSocket to it, and the built-in editor's page calls back for its episode, | |
| so both must reach the process that holds the state. The router keeps every session and episode on its worker. | |
| Measured locally (`scripts/loadtest.py`, 4 workers, no errors): | |
| - tools: about 2,900 episodes per minute, 3× one process; | |
| - built-in GUI editor: 128 concurrent episodes at about 190 actions per second, 2× one process. | |
| The router itself saturates one core at about 300–450 tool calls per second, so 4–6 workers is the useful range. | |
| On the private Space [FineEnvs/DesignGym](https://huggingface.co/spaces/FineEnvs/DesignGym) (`scripts/deploy_space.py`: cpu-upgrade, 8 vCPU / 32 GB, 4 workers × 48 sessions, the full | |
| corpus read over HTTP), tested from India, where the network round trip is about 0.22 s: | |
| | Load | Result | Throughput | Step p50 / p95 | | |
| |---|---|---|---| | |
| | Tools, 192 concurrent | 575/576 ok | 643 episodes/min, 110 calls/s | 0.26 / 0.8 s | | |
| | Built-in GUI editor, 128 concurrent | 256/256 ok | 75 actions/s | 0.46 / 1.3 s | | |
| - The one failure arrived at the capacity edge, just before a closing session was freed. | |
| - At capacity, cold resets take 7–10 s; each new design is fetched once. | |
| - More than 192 sessions are refused cleanly ("Server at capacity") after every worker has been tried. | |
| - OpenEnv's client has no option for auth headers. `scripts/loadtest.py --hf-token-env HF_TOKEN` shows the two | |
| wrappers a client needs to reach a private Space. | |
| ## Talk to it | |
| ```python | |
| from openenv.core.mcp_client import MCPToolClient | |
| from openenv.core.env_server.mcp_types import CallToolAction | |
| with MCPToolClient(base_url="http://127.0.0.1:8007").sync() as env: | |
| obs = env.reset(split="test", index=3, mode="reference", surface="tools") | |
| print(obs.observation.metadata["instructions"]) # the task, as the model should see it | |
| tools = env.list_tools() # depends on the surface | |
| env.step(CallToolAction(tool_name="add_image", arguments={"asset_id": "a2", "x": 0, "y": 0, "width": 1080})) | |
| out = env.step(CallToolAction(tool_name="submit", arguments={})) | |
| print(out.reward, out.observation.metadata["grade"]) | |
| ``` | |
| `reset` arguments: `split`, `index` or `task_id`, `mode` (`reference` | `description`), `surface` | |
| (`tools` | `html` | `gui`), `skin` (`figma` | `canva` | `photoshop` | `slides` | `powerpoint` | `inkscape` editor layouts; random per `seed`; | |
| or a real app, below), | |
| `theme`, `file_dialog` (`mac` | `windows` | `linux`: the look of the file window images are opened from; random per `seed`), | |
| `coordinates` (`pixels`, or `normalized` for a 0-1000 grid), `max_steps`, | |
| `reward_mode` (`final`, or `dense` for per-edit score deltas that sum to the final score). | |
| The Task API lists tasks without giving them away: `GET /design_canvas/splits`, | |
| `POST /design_canvas/num_tasks`, `/task`, `/task_range`. | |
| ## Tools | |
| | Surface | Tools | | |
| |---|---| | |
| | all | `get_task`, `view_reference`, `submit`, `start_task` (for MCP clients with no `reset`) | | |
| | tools, html | `view_canvas`, `view_assets` (a labelled contact sheet), `list_fonts` | | |
| | tools | `add_image`, `add_text`, `add_shape`, `update_element`, `delete_element`, `duplicate_element`, `reorder_element`, `align_elements`, `set_background`, `list_elements`, `undo`, `redo` | | |
| | html | `write_html`, `read_html` | | |
| | gui | `screenshot`, `left_click`, `double_click`, `triple_click`, `right_click`, `mouse_move`, `left_click_drag`, `scroll`, `type`, `key`, `wait` | | |
| Images come back as MCP image content blocks, so a vision model sees pixels, not base64. | |
| ## Real apps | |
| The GUI surface can also put the model in a real design app instead of the built-in editor: `skin="penpot"` | |
| (Penpot in the env's browser), `"impress"` (LibreOffice Impress), `"inkscape-desktop"` or `"gimp"` (on a Linux | |
| desktop). The tools, the prompt's task and the reward are the same. At `reset` the env sets the app up with a | |
| blank canvas at the task's size and the task's assets loaded. On `submit` it reads the app's own document back | |
| (Penpot's file, the slide, the SVG, the layers) as a design document and scores it like any other. | |
| The client always talks to one env server, and that server runs the apps: | |
| - **Locally.** Run Penpot with `realapps/penpot/docker-compose.yaml` and the desktop image from | |
| `realapps/desktop/`. The server finds them at `DESIGN_CANVAS_PENPOT_URL` and `DESIGN_CANVAS_DESKTOP_URLS`. | |
| - **On Daytona.** Set `DESIGN_CANVAS_APPS=daytona` and `DAYTONA_API_KEY`. The server keeps its own pool of | |
| private sandboxes (`realapps/daytona.py`): one per running desktop episode (`DESIGN_CANVAS_DAYTONA_DESKTOPS`, | |
| default 2, built from a snapshot of the desktop image) and one Penpot sandbox shared by all Penpot episodes. | |
| It reaches them through their preview URLs with the access token in a header, and the browser may load that | |
| app and nothing else. Sandboxes are reused across server restarts and stop when idle. | |
| `python -m design_canvas.realapps.daytona up | status | down` starts, lists or deletes them. | |
| A real-app episode's `reset` observation carries `app` (`{"skin", "name", "real": true}`), and its `editor_url` is | |
| the app's live screen (`/api/episodes/{id}/screen.png`). `GET /api/apps` lists the apps and where they run | |
| (`?check=1` also asks each endpoint or sandbox whether it is up). In the `/web` playground, pick a real app under | |
| **Editor**: the Editor tab shows its screen, and a click on the screen is a click in the app. The Expert tab | |
| writes the target into the app, and **Read the document from the app** shows what Submit will score. | |
| Expert round trips (the ground truth written into the app and read back) score 1.000 in Penpot and Inkscape, | |
| 0.975 to 0.999 in Impress, and 0.953 to 0.991 in GIMP, which has no mixed-colour text runs. | |
| ## Reward | |
| `0.60·structure + 0.30·visual + 0.05·order + 0.05·background` in reference mode. Structure matches elements | |
| one-to-one (Hungarian) on content, geometry and typography; visual is multi-scale SSIM and colour layout against | |
| `render(ground truth)`, normalised so an empty canvas scores 0. Description mode loosens geometry and adds | |
| coverage. Replaying the ground truth scores exactly 1.000 on every task. Details: | |
| [`core/score.py`](core/score.py) and the project README. | |
| `DESIGN_CANVAS_REWARD=3` (or `grade(..., version=3)`) selects reward v3 for reference mode, calibrated against | |
| a pairwise visual judge (Claude Opus 5.5, both orders) on frontier and Qwen attempts across all three surfaces: | |
| - **Coarse-to-fine geometry.** Half the tight kernel, half a loose one (15% of the diagonal), scaled by √position, | |
| so a roughly placed element in the right region earns credit, but 1.0 still needs exact placement. | |
| - **Typography that multiplies.** Font and colour scale the rest of the style match, and style scales placement. | |
| - **Copy counts triple in structure.** Every unmatched submitted element counts against the score. | |
| - **Gated pixel terms.** Visual, order and background count in proportion to min(1, structure/0.3), so colour | |
| patches and scattered dumps (~0.25 under v1) score ~0. | |
| Results: | |
| - **Validation:** agrees with the judge on 89.9% of 556 pairs vs 85.4% for v1 (+33/−8, p<0.001), and on 91.4% | |
| of Qwen pairs. | |
| - **Held-out test tasks:** 88.4% vs 84.2% (p=0.005), including 271 GUI pairs. | |
| - **Description mode:** unchanged. | |
| ## Layout | |
| ``` | |
| core/ document model, ops, fonts, skia renderer, reward, HTML profile, briefs, expert, Crello conversion | |
| server/ OpenEnv environment, editor API + live sync, GUI driver (Playwright), Gradio playground, stdio MCP | |
| realapps/ real apps behind the GUI surface: Penpot, the desktop agent (Impress, Inkscape, GIMP), Daytona pool | |
| web/ the editor: vanilla JS, six layouts (Studio / Template / Pixel / Deck / Present / Vector), no build step | |
| scripts/ parity.py (browser vs renderer vs Crello), gui_expert.py (scripted computer-use agent) | |
| tests/ core, reward invariants over every task, OpenEnv contract, browser E2E, GUI agent | |
| rollout.py real models on every surface (Anthropic, OpenAI, Hugging Face router) | |
| ``` | |
| Data: Crello is CDLA-Permissive-2.0; the templates belong to VistaCreate. The pack is built locally from the | |
| dataset and not redistributed here. | |
| ## Served corpus | |
| 22,688 Crello designs (train 18,954, validation 1,817, test 1,917), read on demand from the [FineEnvs/crello-bucket](https://huggingface.co/buckets/FineEnvs/crello-bucket) bucket mounted at `/corpus`. Snapshot `aafdb0e86816e6af`. | |