File size: 6,230 Bytes
df84e45 ec7f10b df84e45 23df34a ec7f10b df84e45 23df34a df84e45 1d12f00 ec7f10b df84e45 ec7f10b c6d9f46 df84e45 ec7f10b df84e45 ec7f10b df84e45 1d12f00 df84e45 1d12f00 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 | """Publish the environment as a Hugging Face Space that serves the full corpus.
The Space image holds code and the corpus manifest only. The catalog is copied from the bucket at startup, and
each design's pixels are read from it the first time an episode uses it: over HTTP by default, or with
--transport mount from the bucket attached read-only at /corpus.
python scripts/deploy_space.py --space FineEnvs/DesignGym \\
--manifest corpus-manifest.json --private
Refuses to deploy unless the manifest's catalog is published in the bucket
with the exact size recorded in the manifest.
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
import tempfile
from pathlib import Path
HERE = Path(__file__).resolve().parents[1]
IGNORE = shutil.ignore_patterns(".venv", "__pycache__", ".pytest_cache", ".ruff_cache", "*.egg-info",
"tests", "artifacts")
def main(argv=None) -> int:
from huggingface_hub import HfApi, Volume
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--space", required=True, help="e.g. FineEnvs/DesignGym")
ap.add_argument("--manifest", default=str(HERE / "corpus-manifest.json"))
ap.add_argument("--hardware", default="cpu-upgrade", help="Space hardware (GUI episodes run Chromium)")
ap.add_argument("--private", action="store_true")
ap.add_argument("--max-envs", type=int, default=32, help="concurrent environment sessions per worker")
ap.add_argument("--workers", type=int, default=4, help="server processes behind the Space's one URL")
ap.add_argument("--reward", default="3", help="DESIGN_CANVAS_REWARD (3 = reward v3, as the evals use)")
ap.add_argument("--daytona-desktops", type=int, default=0,
help="run the real apps on Daytona with this many desktops (0 = no real apps); the key and URL "
"are copied from DAYTONA_API_KEY / DAYTONA_API_URL into the Space's secrets")
ap.add_argument("--transport", choices=["http", "mount"], default="http",
help="read designs over HTTP (default) or from the bucket mounted at /corpus; the mount returned "
"I/O errors under the workers' random reads (2026-10-05)")
args = ap.parse_args(argv)
manifest = json.loads(Path(args.manifest).read_text())
bucket = manifest["bucket_id"]
api = HfApi()
catalog = manifest["catalog"]
found = {r.path: r.size for r in api.get_bucket_paths_info(bucket, [catalog["path"]])}
if found.get(catalog["path"]) != catalog["size"]:
sys.exit(f"publish the index first: {catalog['path']} is not in {bucket} at the recorded size")
with tempfile.TemporaryDirectory(prefix="design-canvas-space-") as tmp:
staging = Path(tmp)
shutil.copytree(HERE, staging, dirs_exist_ok=True, ignore=IGNORE)
shutil.copyfile(args.manifest, staging / "corpus-manifest.json")
with (staging / "README.md").open("a") as card:
n = sum(manifest["splits"].values())
card.write("\n## Served corpus\n\n")
card.write(f"{n:,} Crello designs ({', '.join(f'{k} {v:,}' for k, v in manifest['splits'].items())}), "
f"read on demand from the [{bucket}](https://huggingface.co/buckets/{bucket}) bucket "
f"mounted at `/corpus`. Snapshot `{manifest['snapshot_id'][:16]}`.\n")
api.create_repo(args.space, repo_type="space", space_sdk="docker", private=args.private,
exist_ok=True, space_hardware=args.hardware)
commit = api.upload_folder(repo_id=args.space, repo_type="space", folder_path=staging,
commit_message=f"Serve Crello corpus {manifest['snapshot_id'][:12]} "
"from the mounted bucket")
runtime = api.space_info(args.space).runtime
volumes = [v for v in (runtime.volumes or []) if v.mount_path != "/corpus"] if runtime else []
if args.transport == "mount":
volumes.append(Volume(type="bucket", source=bucket, mount_path="/corpus", read_only=True))
if volumes:
api.set_space_volumes(args.space, volumes=volumes)
elif runtime and runtime.volumes:
api.delete_space_volumes(args.space) # the Hub refuses an empty list
current = api.get_space_variables(args.space)
if args.transport == "http" and "DESIGN_CANVAS_SOURCE_ROOT" in current:
api.delete_space_variable(args.space, "DESIGN_CANVAS_SOURCE_ROOT")
for key, value in {"MAX_CONCURRENT_ENVS": str(args.max_envs),
"DESIGN_CANVAS_WORKERS": str(args.workers),
"DESIGN_CANVAS_REWARD": args.reward,
**({"DESIGN_CANVAS_SOURCE_ROOT": "/corpus"} if args.transport == "mount" else {}),
"DESIGN_CANVAS_PUBLIC_URL": f"https://{args.space.replace('/', '-').lower()}.hf.space"}.items():
if key not in current or current[key].value != value:
api.add_space_variable(args.space, key, value)
if args.daytona_desktops > 0: # the Space's own pool, so it never takes a local server's desktops
import os
for key, value in {"DESIGN_CANVAS_APPS": "daytona", "DESIGN_CANVAS_DAYTONA_POOL": "space",
"DESIGN_CANVAS_DAYTONA_DESKTOPS": str(args.daytona_desktops)}.items():
if key not in current or current[key].value != value:
api.add_space_variable(args.space, key, value)
for key in ("DAYTONA_API_KEY", "DAYTONA_API_URL"):
if os.environ.get(key):
api.add_space_secret(args.space, key, os.environ[key])
elif "DESIGN_CANVAS_APPS" in current:
api.delete_space_variable(args.space, "DESIGN_CANVAS_APPS")
print(json.dumps({"space": args.space, "commit": commit.oid, "snapshot_id": manifest["snapshot_id"],
"bucket": bucket, "transport": args.transport, "daytona_desktops": args.daytona_desktops,
"url": f"https://huggingface.co/spaces/{args.space}"}, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
|