File size: 13,367 Bytes
df84e45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23df34a
df84e45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
"""`design-canvas` command line.

    design-canvas prepare [--train 200 --validation 60 --test 100]   build the task pack
    design-canvas serve [--port 8000] [--workers N]                OpenEnv server + editor (N processes, one URL)
    design-canvas mcp                                              stdio MCP server for Claude & co.
    design-canvas export-sft --split train --out sft.jsonl         expert trajectories
    design-canvas export-hf --cua artifacts/cua/cua.jsonl --out artifacts/hf/cua   cua-v1 dataset (Hub-ready)
    design-canvas smoke                                            offline end-to-end check

    design-canvas corpus build --out DIR          index all of FineEnvs/crello-bucket
    design-canvas corpus publish --out DIR        upload the index next to the data
    design-canvas corpus stats                    what the served corpus holds
    design-canvas evalset build --out FILE        freeze the stratified evaluation set
    design-canvas evalset verify FILE             check a frozen set against the corpus
    design-canvas evalset export FILE --out DIR   write it as an offline task pack
    design-canvas evalset subset FILE --name N --size K [--within lite]   nest a smaller subset in the set
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import time
from pathlib import Path


def _pack_arg(p: argparse.ArgumentParser):
    p.add_argument("--pack", default=os.environ.get("DESIGN_CANVAS_PACK"),
                   help="task pack directory (default ~/.cache/design_canvas/pack)")


def cmd_prepare(args) -> int:
    from huggingface_hub import hf_hub_download

    from .core.pack import CRELLO_REPO, CRELLO_REVISION, DEFAULT_PACK, build_pack

    out = Path(args.pack or DEFAULT_PACK).expanduser()

    def dl(name):
        return hf_hub_download(CRELLO_REPO, name, repo_type="dataset", revision=CRELLO_REVISION)

    shards = {"train": "data/train-{:05d}-of-00031.parquet",
              "validation": "data/validation-{:05d}-of-00003.parquet",
              "test": "data/test-{:05d}-of-00004.parquet"}
    per = {"train": args.train, "validation": args.validation, "test": args.test}
    sources = {}
    for split, pattern in shards.items():
        if per[split] <= 0:
            continue
        # Each shard holds ~600 templates; about 60% pass the filters.
        need = max(1, min(args.max_shards, 1 + per[split] // 350))
        sources[split] = [dl(pattern.format(i)) for i in range(need)]
        print(f"{split}: {len(sources[split])} shard(s)")
    t = time.time()
    manifest = build_pack(out, sources, per, dl("resources/fonts.pickle"),
                          max_elements=args.max_elements)
    print(json.dumps(manifest, indent=2))
    print(f"pack ready at {out} in {time.time() - t:.0f}s")
    return 0


def cmd_serve(args) -> int:
    if args.pack:
        os.environ["DESIGN_CANVAS_PACK"] = str(Path(args.pack).expanduser())
    os.environ["PORT"] = str(args.port)
    from .server.app import main

    print(f"DesignGym on http://{args.host}:{args.port}  "
          f"(editor /editor/, playground /web/, MCP /mcp)")
    main(host=args.host, port=args.port, workers=args.workers)
    return 0


def cmd_mcp(args) -> int:
    if args.pack:
        os.environ["DESIGN_CANVAS_PACK"] = str(Path(args.pack).expanduser())
    from .server.stdio_mcp import run

    run(editor_port=args.editor_port)
    return 0


def cmd_export_sft(args) -> int:
    from .core.pack import load_pack
    from .train_data import export

    pack = load_pack(args.pack)
    n = export(pack, args.split, Path(args.out), surfaces=args.surfaces.split(","),
               modes=args.modes.split(","), limit=args.limit)
    print(f"wrote {n} trajectories to {args.out}")
    return 0


def cmd_export_cua(args) -> int:
    from .core.pack import load_pack
    from .cua import export

    summary = export(load_pack(args.pack), args.split, Path(args.out), limit=args.limit, start=args.start,
                     modes=tuple(args.modes.split(",")), seed=args.seed, workers=args.workers,
                     server=args.server, max_elements=args.max_elements, image_format=args.image_format)
    print(json.dumps(summary, indent=2))
    return 0


def cmd_export_hf(args) -> int:
    from .cua_format import Window, export_hf

    summary = export_hf(Path(args.cua), Path(args.out), image_format=args.image_format, min_reward=args.min_reward,
                        window=Window(args.max_images, args.keep_images), shard_mb=args.shard_mb,
                        with_windows=args.windows)
    print(json.dumps(summary, indent=2))
    return 0


def cmd_smoke(args) -> int:
    from .smoke import run

    return run(args.pack)


def _corpus(args):
    from .core.corpus import CorpusPack

    manifest = args.corpus or os.environ.get("DESIGN_CANVAS_CORPUS")
    if not manifest:
        raise SystemExit("set --corpus or DESIGN_CANVAS_CORPUS to a corpus manifest")
    return CorpusPack(manifest)


def cmd_corpus(args) -> int:
    from .core import corpus

    if args.action == "build":
        source = corpus.BucketSource(args.bucket, args.source_root)
        m = corpus.build_index(args.out, source, max_elements=args.max_elements, workers=args.workers)
        print(json.dumps({k: m[k] for k in ("snapshot_id", "splits", "rejected", "difficulty")}, indent=2))
    elif args.action == "publish":
        print(corpus.publish_index(args.out, args.bucket))
    elif args.action == "warm":
        pack = _corpus(args)
        out = pack.warm(args.splits.split(","), workers=min(args.workers, 8),
                        progress=lambda g, n, d: print(f"  {g}/{n} row groups, {d} designs", flush=True))
        out["failed"] = out["failed"][:20]
        print(json.dumps(out, indent=2))
        return 1 if out["failed"] else 0
    else:
        pack = _corpus(args)
        print(json.dumps({**pack.stats(), "splits": pack.manifest["splits"],
                          "rejected": pack.manifest["rejected"]}, indent=2))
    return 0


def cmd_evalset(args) -> int:
    from .core import evalset as ev

    pack = _corpus(args)
    if args.action == "build":
        record = ev.build(pack, split=args.split, size=args.size, lite=args.lite, seed=args.seed,
                          validate=not args.no_validate)
        ev.save(record, args.out)
        print(json.dumps({"evalset_id": record["evalset_id"], **record["summary"],
                          "lite": ev.summarize(record, "lite"), "failures": record["failures"]}, indent=2))
        return 1 if record["failures"] else 0
    record = ev.load(args.file, pack.snapshot_id)
    ev.check_against(record, pack)
    if args.action == "calibrate":
        from .core.difficulty import calibrate

        rewards: dict[str, list[float]] = {}
        for trace in args.trace:
            for line in Path(trace).read_text().splitlines():
                r = json.loads(line)
                if r.get("evalset_task") and not r.get("error"):
                    rewards.setdefault(r["evalset_task"], []).append(r["reward"] or 0.0)
        ids = {e["task_id"] for e in record["tasks"]}
        rows = [r for r in pack.rows(record["split"]) if r["task_id"] in ids]
        print(json.dumps(calibrate(rows, rewards, pack.manifest["difficulty"]["weights"]), indent=2))
        return 0
    if args.action == "export":
        print(json.dumps(ev.export_pack(pack, record, args.out), indent=2))
    elif args.action == "subset":
        ev.add_subset(record, args.name, args.size, args.within)
        ev.save(record, args.file)
        print(json.dumps({"evalset_id": record["evalset_id"], "subset": args.name,
                          **ev.summarize(record, args.name), "task_ids": record["subsets"][args.name]}, indent=2))
    else:
        print(json.dumps({"evalset_id": record["evalset_id"], "ok": True, **record["summary"]}, indent=2))
    return 0


def main(argv=None) -> int:
    parser = argparse.ArgumentParser(prog="design-canvas", description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="cmd", required=True)

    p = sub.add_parser("prepare", help="build the task pack from Crello")
    _pack_arg(p)
    p.add_argument("--train", type=int, default=200)
    p.add_argument("--validation", type=int, default=60)
    p.add_argument("--test", type=int, default=100)
    p.add_argument("--max-elements", type=int, default=20)
    p.add_argument("--max-shards", type=int, default=4)
    p.set_defaults(fn=cmd_prepare)

    p = sub.add_parser("serve", help="run the OpenEnv server with the editor")
    _pack_arg(p)
    p.add_argument("--host", default="127.0.0.1")
    p.add_argument("--port", type=int, default=int(os.environ.get("PORT", "8007")))
    p.add_argument("--workers", type=int, default=int(os.environ.get("DESIGN_CANVAS_WORKERS", "1")),
                   help="server processes behind one URL (a session-aware router); 1 = a single process")
    p.set_defaults(fn=cmd_serve)

    p = sub.add_parser("mcp", help="stdio MCP server (e.g. claude mcp add design-canvas -- ...)")
    _pack_arg(p)
    p.add_argument("--editor-port", type=int, default=8007,
                   help="also serve the live editor on this port (0 to disable)")
    p.set_defaults(fn=cmd_mcp)

    p = sub.add_parser("export-sft", help="write expert trajectories as chat JSONL")
    _pack_arg(p)
    p.add_argument("--split", default="train")
    p.add_argument("--out", default="sft.jsonl")
    p.add_argument("--surfaces", default="tools,html")
    p.add_argument("--modes", default="reference,description")
    p.add_argument("--limit", type=int, default=None)
    p.set_defaults(fn=cmd_export_sft)

    p = sub.add_parser("export-cua", help="record verified computer-use trajectories (screenshots + actions)")
    _pack_arg(p)
    p.add_argument("--split", default="train")
    p.add_argument("--out", default="artifacts/cua")
    p.add_argument("--limit", type=int, default=20)
    p.add_argument("--start", type=int, default=0)
    p.add_argument("--modes", default="reference,description")
    p.add_argument("--seed", type=int, default=0)
    p.add_argument("--workers", type=int, default=2)
    p.add_argument("--max-elements", type=int, default=12)
    p.add_argument("--image-format", default="webp", choices=["webp", "png", "jpeg"])
    p.add_argument("--server", default=None, help="a running server with DESIGN_CANVAS_DEBUG=1; default in-process")
    p.set_defaults(fn=cmd_export_cua)

    p = sub.add_parser("export-hf", help="write recorded trajectories as a cua-v1 Hugging Face dataset folder")
    p.add_argument("--cua", required=True, help="the recorder's cua.jsonl (images next to it)")
    p.add_argument("--out", required=True)
    p.add_argument("--image-format", default="keep", choices=["keep", "png", "jpeg"],
                   help="keep = the recorder's format (WebP), smallest")
    p.add_argument("--min-reward", type=float, default=0.98)
    p.add_argument("--max-images", type=int, default=8)
    p.add_argument("--keep-images", type=int, default=3)
    p.add_argument("--shard-mb", type=int, default=500)
    p.add_argument("--windows", action="store_true", help="also store the SFT windows view (duplicates screenshots)")
    p.set_defaults(fn=cmd_export_hf)

    p = sub.add_parser("smoke", help="offline end-to-end check")
    _pack_arg(p)
    p.set_defaults(fn=cmd_smoke)

    p = sub.add_parser("corpus", help="index, publish or inspect the bucket-backed corpus")
    p.add_argument("action", choices=["build", "publish", "stats", "warm"])
    p.add_argument("--out", default="artifacts/corpus-index", help="index directory (build, publish)")
    p.add_argument("--bucket", default="FineEnvs/crello-bucket")
    p.add_argument("--source-root", default=None, help="local copy or mount of the bucket")
    p.add_argument("--max-elements", type=int, default=30)
    p.add_argument("--workers", type=int, default=8)
    p.add_argument("--corpus", default=None, help="corpus manifest (stats, warm)")
    p.add_argument("--splits", default="train,validation,test", help="splits to download ahead of time (warm)")
    p.set_defaults(fn=cmd_corpus)

    p = sub.add_parser("evalset", help="build, verify or export the frozen evaluation set")
    p.add_argument("action", choices=["build", "verify", "export", "calibrate", "subset"])
    p.add_argument("file", nargs="?", help="frozen set (verify, export, subset)")
    p.add_argument("--name", help="subset name (subset)")
    p.add_argument("--within", default="lite", help="subset to nest inside (subset)")
    p.add_argument("--out", help="output file (build) or pack directory (export)")
    p.add_argument("--corpus", default=None, help="corpus manifest (default DESIGN_CANVAS_CORPUS)")
    p.add_argument("--split", default="test")
    p.add_argument("--size", type=int, default=300, help="set size (build) or subset size (subset)")
    p.add_argument("--lite", type=int, default=60)
    p.add_argument("--seed", type=int, default=42)
    p.add_argument("--no-validate", action="store_true")
    p.add_argument("--trace", action="append", default=[], help="rollout trace.jsonl (calibrate)")
    p.set_defaults(fn=cmd_evalset)

    args = parser.parse_args(argv)
    return args.fn(args)


if __name__ == "__main__":
    sys.exit(main())