Serve Crello corpus aafdb0e86816 from the mounted bucket
Browse files- Dockerfile +1 -1
- README.md +6 -3
- __init__.py +1 -1
- cli.py +1 -1
- scripts/deploy_space.py +2 -2
- server/app.py +2 -2
- server/gradio_ui.py +2 -2
- server/router.py +1 -1
- web/index.html +1 -1
- web/js/main.js +1 -1
Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
FROM python:3.12-slim
|
| 3 |
|
| 4 |
ENV PYTHONUNBUFFERED=1 UV_LINK_MODE=copy PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
|
|
|
|
| 1 |
+
# DesignGym: OpenEnv server + editor + headless Chromium for the GUI surface.
|
| 2 |
FROM python:3.12-slim
|
| 3 |
|
| 4 |
ENV PYTHONUNBUFFERED=1 UV_LINK_MODE=copy PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
|
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🎨
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: pink
|
|
@@ -11,13 +11,16 @@ tags: [openenv, mcp, computer-use, graphic-design, crello]
|
|
| 11 |
short_description: Recreate real graphic designs via MCP, HTML or a real editor
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
An [OpenEnv](https://github.com/huggingface/OpenEnv) environment for rebuilding real graphic designs from
|
| 17 |
their parts. Each task is a [Crello](https://huggingface.co/datasets/cyberagent/crello) template taken apart
|
| 18 |
into its pre-rendered layers and its typed copy. The agent puts it back together on a canvas and is scored
|
| 19 |
against a render of the original.
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
- **Two tasks.** `mode="reference"`: here is the target image, recreate it. `mode="description"`: here is a
|
| 22 |
written brief naming every asset and line of copy, build it.
|
| 23 |
- **Three surfaces over one document.** `surface="tools"`: structured MCP edits. `surface="html"`: one page of
|
|
@@ -92,7 +95,7 @@ Measured locally (`scripts/loadtest.py`, 4 workers, no errors):
|
|
| 92 |
|
| 93 |
The router itself saturates one core at about 300–450 tool calls per second, so 4–6 workers is the useful range.
|
| 94 |
|
| 95 |
-
On
|
| 96 |
corpus read over HTTP), tested from India, where the network round trip is about 0.22 s:
|
| 97 |
|
| 98 |
| Load | Result | Throughput | Step p50 / p95 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: DesignGym
|
| 3 |
emoji: 🎨
|
| 4 |
colorFrom: indigo
|
| 5 |
colorTo: pink
|
|
|
|
| 11 |
short_description: Recreate real graphic designs via MCP, HTML or a real editor
|
| 12 |
---
|
| 13 |
|
| 14 |
+
# DesignGym
|
| 15 |
|
| 16 |
An [OpenEnv](https://github.com/huggingface/OpenEnv) environment for rebuilding real graphic designs from
|
| 17 |
their parts. Each task is a [Crello](https://huggingface.co/datasets/cyberagent/crello) template taken apart
|
| 18 |
into its pre-rendered layers and its typed copy. The agent puts it back together on a canvas and is scored
|
| 19 |
against a render of the original.
|
| 20 |
|
| 21 |
+
The package, the CLI, the OpenEnv name and the `DESIGN_CANVAS_*` settings keep the id `design_canvas`
|
| 22 |
+
(`design-canvas`). Clients, datasets and traces already use it.
|
| 23 |
+
|
| 24 |
- **Two tasks.** `mode="reference"`: here is the target image, recreate it. `mode="description"`: here is a
|
| 25 |
written brief naming every asset and line of copy, build it.
|
| 26 |
- **Three surfaces over one document.** `surface="tools"`: structured MCP edits. `surface="html"`: one page of
|
|
|
|
| 95 |
|
| 96 |
The router itself saturates one core at about 300–450 tool calls per second, so 4–6 workers is the useful range.
|
| 97 |
|
| 98 |
+
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
|
| 99 |
corpus read over HTTP), tested from India, where the network round trip is about 0.22 s:
|
| 100 |
|
| 101 |
| Load | Result | Throughput | Step p50 / p95 |
|
__init__.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
__version__ = "0.1.0"
|
|
|
|
| 1 |
+
"""DesignGym (package design_canvas): recreate graphic designs in a real editor."""
|
| 2 |
|
| 3 |
__version__ = "0.1.0"
|
cli.py
CHANGED
|
@@ -67,7 +67,7 @@ def cmd_serve(args) -> int:
|
|
| 67 |
os.environ["PORT"] = str(args.port)
|
| 68 |
from .server.app import main
|
| 69 |
|
| 70 |
-
print(f"
|
| 71 |
f"(editor /editor/, playground /web/, MCP /mcp)")
|
| 72 |
main(host=args.host, port=args.port, workers=args.workers)
|
| 73 |
return 0
|
|
|
|
| 67 |
os.environ["PORT"] = str(args.port)
|
| 68 |
from .server.app import main
|
| 69 |
|
| 70 |
+
print(f"DesignGym on http://{args.host}:{args.port} "
|
| 71 |
f"(editor /editor/, playground /web/, MCP /mcp)")
|
| 72 |
main(host=args.host, port=args.port, workers=args.workers)
|
| 73 |
return 0
|
scripts/deploy_space.py
CHANGED
|
@@ -4,7 +4,7 @@ The Space image holds code and the corpus manifest only. The catalog is copied f
|
|
| 4 |
each design's pixels are read from it the first time an episode uses it: over HTTP by default, or with
|
| 5 |
--transport mount from the bucket attached read-only at /corpus.
|
| 6 |
|
| 7 |
-
python scripts/deploy_space.py --space FineEnvs/
|
| 8 |
--manifest corpus-manifest.json --private
|
| 9 |
|
| 10 |
Refuses to deploy unless the manifest's catalog is published in the bucket
|
|
@@ -29,7 +29,7 @@ def main(argv=None) -> int:
|
|
| 29 |
from huggingface_hub import HfApi, Volume
|
| 30 |
|
| 31 |
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 32 |
-
ap.add_argument("--space", required=True, help="e.g. FineEnvs/
|
| 33 |
ap.add_argument("--manifest", default=str(HERE / "corpus-manifest.json"))
|
| 34 |
ap.add_argument("--hardware", default="cpu-upgrade", help="Space hardware (GUI episodes run Chromium)")
|
| 35 |
ap.add_argument("--private", action="store_true")
|
|
|
|
| 4 |
each design's pixels are read from it the first time an episode uses it: over HTTP by default, or with
|
| 5 |
--transport mount from the bucket attached read-only at /corpus.
|
| 6 |
|
| 7 |
+
python scripts/deploy_space.py --space FineEnvs/DesignGym \\
|
| 8 |
--manifest corpus-manifest.json --private
|
| 9 |
|
| 10 |
Refuses to deploy unless the manifest's catalog is published in the bucket
|
|
|
|
| 29 |
from huggingface_hub import HfApi, Volume
|
| 30 |
|
| 31 |
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 32 |
+
ap.add_argument("--space", required=True, help="e.g. FineEnvs/DesignGym")
|
| 33 |
ap.add_argument("--manifest", default=str(HERE / "corpus-manifest.json"))
|
| 34 |
ap.add_argument("--hardware", default="cpu-upgrade", help="Space hardware (GUI episodes run Chromium)")
|
| 35 |
ap.add_argument("--private", action="store_true")
|
server/app.py
CHANGED
|
@@ -46,10 +46,10 @@ def create_server():
|
|
| 46 |
env_name=ENV_NAME,
|
| 47 |
max_concurrent_envs=int(os.environ.get("MAX_CONCURRENT_ENVS", "64")),
|
| 48 |
gradio_builder=gradio_builder,
|
| 49 |
-
custom_tab_name="
|
| 50 |
custom_tab_primary=True,
|
| 51 |
show_default_tab=True,
|
| 52 |
-
title_override="
|
| 53 |
)
|
| 54 |
mount_editor(app, pack)
|
| 55 |
|
|
|
|
| 46 |
env_name=ENV_NAME,
|
| 47 |
max_concurrent_envs=int(os.environ.get("MAX_CONCURRENT_ENVS", "64")),
|
| 48 |
gradio_builder=gradio_builder,
|
| 49 |
+
custom_tab_name="DesignGym",
|
| 50 |
custom_tab_primary=True,
|
| 51 |
show_default_tab=True,
|
| 52 |
+
title_override="DesignGym · OpenEnv",
|
| 53 |
)
|
| 54 |
mount_editor(app, pack)
|
| 55 |
|
server/gradio_ui.py
CHANGED
|
@@ -151,7 +151,7 @@ def build_ui(pack: TaskPack) -> gr.Blocks:
|
|
| 151 |
"opacity:.6;border:1px dashed #555;border-radius:8px'>Start a task to open the "
|
| 152 |
"editor.</div>")
|
| 153 |
src = f"/editor/?episode={ep_id}&skin={skin}"
|
| 154 |
-
return (f"<div style='width:100%'><iframe src='{src}' title='
|
| 155 |
f"style='display:block;width:100%;height:calc(100vh - 120px);min-height:640px;"
|
| 156 |
f"border:1px solid #444;border-radius:8px;"
|
| 157 |
f"background:#fff' allow='clipboard-write'></iframe>"
|
|
@@ -363,7 +363,7 @@ def build_ui(pack: TaskPack) -> gr.Blocks:
|
|
| 363 |
gr.HTML("<style>.gradio-container{max-width:100% !important;padding-left:16px !important;"
|
| 364 |
"padding-right:16px !important}.gradio-container .main,.gradio-container .wrap"
|
| 365 |
"{max-width:100% !important}</style>", padding=False)
|
| 366 |
-
gr.Markdown("#
|
| 367 |
gr.Markdown(INTRO)
|
| 368 |
episode = gr.State(None)
|
| 369 |
with gr.Row():
|
|
|
|
| 151 |
"opacity:.6;border:1px dashed #555;border-radius:8px'>Start a task to open the "
|
| 152 |
"editor.</div>")
|
| 153 |
src = f"/editor/?episode={ep_id}&skin={skin}"
|
| 154 |
+
return (f"<div style='width:100%'><iframe src='{src}' title='DesignGym editor' "
|
| 155 |
f"style='display:block;width:100%;height:calc(100vh - 120px);min-height:640px;"
|
| 156 |
f"border:1px solid #444;border-radius:8px;"
|
| 157 |
f"background:#fff' allow='clipboard-write'></iframe>"
|
|
|
|
| 363 |
gr.HTML("<style>.gradio-container{max-width:100% !important;padding-left:16px !important;"
|
| 364 |
"padding-right:16px !important}.gradio-container .main,.gradio-container .wrap"
|
| 365 |
"{max-width:100% !important}</style>", padding=False)
|
| 366 |
+
gr.Markdown("# DesignGym — try it")
|
| 367 |
gr.Markdown(INTRO)
|
| 368 |
episode = gr.State(None)
|
| 369 |
with gr.Row():
|
server/router.py
CHANGED
|
@@ -299,7 +299,7 @@ def serve(host: str, port: int, workers: int) -> None:
|
|
| 299 |
try:
|
| 300 |
for u in urls:
|
| 301 |
_wait_healthy(u, procs)
|
| 302 |
-
print(f"
|
| 303 |
flush=True)
|
| 304 |
uvicorn.run(build_app(urls), host=host, port=port, ws_ping_interval=60, ws_ping_timeout=300,
|
| 305 |
log_level="warning")
|
|
|
|
| 299 |
try:
|
| 300 |
for u in urls:
|
| 301 |
_wait_healthy(u, procs)
|
| 302 |
+
print(f"DesignGym on http://{host}:{port}: {workers} workers on ports {base}-{base + workers - 1}",
|
| 303 |
flush=True)
|
| 304 |
uvicorn.run(build_app(urls), host=host, port=port, ws_ping_interval=60, ws_ping_timeout=300,
|
| 305 |
log_level="warning")
|
web/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>
|
| 7 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%237d2ae8'/%3E%3Crect x='7' y='7' width='11' height='11' rx='2' fill='white'/%3E%3Ccircle cx='21' cy='21' r='6' fill='%2300c4cc'/%3E%3C/svg%3E">
|
| 8 |
<link rel="stylesheet" href="css/base.css">
|
| 9 |
<link rel="stylesheet" href="css/figma.css">
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>DesignGym</title>
|
| 7 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%237d2ae8'/%3E%3Crect x='7' y='7' width='11' height='11' rx='2' fill='white'/%3E%3Ccircle cx='21' cy='21' r='6' fill='%2300c4cc'/%3E%3C/svg%3E">
|
| 8 |
<link rel="stylesheet" href="css/base.css">
|
| 9 |
<link rel="stylesheet" href="css/figma.css">
|
web/js/main.js
CHANGED
|
@@ -57,7 +57,7 @@ class App extends Emitter {
|
|
| 57 |
this.store.on("fatal", (msg) => this.fatal(msg));
|
| 58 |
await this.store.connect();
|
| 59 |
await ensureFonts([...(this.store.task.fonts_used || []), ...this.store.doc.elements.filter((e) => e.kind === "text").map((e) => e.font_family)]);
|
| 60 |
-
document.title = `${this.store.task.title} ·
|
| 61 |
this.stage.fit();
|
| 62 |
this.stage.render();
|
| 63 |
this.refreshAll();
|
|
|
|
| 57 |
this.store.on("fatal", (msg) => this.fatal(msg));
|
| 58 |
await this.store.connect();
|
| 59 |
await ensureFonts([...(this.store.task.fonts_used || []), ...this.store.doc.elements.filter((e) => e.kind === "text").map((e) => e.font_family)]);
|
| 60 |
+
document.title = `${this.store.task.title} · DesignGym`;
|
| 61 |
this.stage.fit();
|
| 62 |
this.stage.render();
|
| 63 |
this.refreshAll();
|