File size: 9,352 Bytes
b2f2f57 6b4a4b8 b2f2f57 2115e2f b2f2f57 2115e2f b2f2f57 2115e2f b2f2f57 2115e2f b2f2f57 2115e2f b2f2f57 | 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 | #!/usr/bin/env python3
"""Build, check, and optionally publish the browser demo as a static HF Space."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
REPO = Path(__file__).resolve().parents[1]
GRAPH_NAMES = ("encoder", "cross_cache", "decoder_step")
# ORT chooses JSEP or Asyncify from browser support. Keep both module/binary
# pairs; Vite's hashed binary asset alone does not cover wasmPaths=/ort/.
ORT_FILES = tuple(
f"ort-wasm-simd-threaded.{variant}.{suffix}"
for variant in ("jsep", "asyncify") for suffix in ("mjs", "wasm")
)
def sha256(path: Path) -> str:
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def copy_file(source: Path, target: Path) -> None:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
def package_space(web_dir: Path, graph_dir: Path, ort_dir: Path, output: Path) -> dict:
"""Copy a small allowlist; never upload the working checkout or audio cache."""
manifest = json.loads((graph_dir / "manifest.json").read_text())
if manifest["precision"] != "fp32":
raise ValueError("The public Space requires the checked FP32 export")
for name in GRAPH_NAMES:
graph = graph_dir / f"{name}.onnx"
record = manifest["graphs"][name]
if graph.stat().st_size != record["bytes"] or sha256(graph) != record["sha256"]:
raise ValueError(f"Graph does not match the export manifest: {graph}")
if output.exists() and any(output.iterdir()):
raise ValueError(f"Output must be empty: {output}")
output.mkdir(parents=True, exist_ok=True)
copy_file(web_dir / "local.html", output / "index.html")
copy_file(web_dir / "local.html", output / "local.html")
copy_file(web_dir / "favicon.png", output / "favicon.png")
# The Vite manifest follows the local entry's imports and runtime assets.
# This excludes compare/server pages and unreferenced public audio.
build_manifest = json.loads((web_dir / ".vite/manifest.json").read_text())
visited: set[str] = set()
def copy_entry(key: str) -> None:
if key in visited:
return
visited.add(key)
entry = build_manifest[key]
for field in ("imports", "dynamicImports"):
for dependency in entry.get(field, []):
copy_entry(dependency)
files = [entry["file"], *entry.get("css", []), *entry.get("assets", [])]
for relative in files:
source = (web_dir / relative).resolve()
if not source.is_relative_to(web_dir.resolve()):
raise ValueError(f"Invalid Vite asset path: {relative}")
copy_file(source, output / relative)
copy_entry("local.html")
manifest["checkpoint"]["path"] = Path(manifest["checkpoint"]["path"]).name
(output / "models/fp32").mkdir(parents=True)
(output / "models/fp32/manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
for name in GRAPH_NAMES:
copy_file(graph_dir / f"{name}.onnx", output / f"models/fp32/{name}.onnx")
for filename in ORT_FILES:
copy_file(ort_dir / filename, output / "ort" / filename)
# Only publish the licensed catalog's exact audio/notes, with graph and
# file hashes checked. Never copy the private local examples directory.
catalog = json.loads((web_dir / "examples/catalog.json").read_text())
public_files = ["examples/catalog.json", "examples/CREDITS.md", "soundfonts/GeneralUser_GS.sf2", "soundfonts/LICENSE.txt"]
for example in catalog["examples"]:
for field, digest_field in (("audio_url", "audio_sha256"), ("notes_url", "notes_sha256")):
relative = example[field].lstrip("/")
source = (web_dir / relative).resolve()
if not source.is_relative_to((web_dir / "examples").resolve()):
raise ValueError(f"Invalid example asset: {relative}")
if sha256(source) != example[digest_field]:
raise ValueError(f"Example asset hash mismatch: {relative}")
public_files.append(relative)
prediction = json.loads((web_dir / example["notes_url"].lstrip("/")).read_text())
if prediction["checkpoint_sha256"] != manifest["checkpoint"]["sha256"]:
raise ValueError(f"Example prediction uses another model: {example['id']}")
for relative in public_files:
copy_file(web_dir / relative, output / relative)
copy_file(REPO / "deploy/huggingface/README.md", output / "README.md")
# Keep the exact app source next to the build for review and reproduction.
shutil.copytree(REPO / "demo/web/src", output / "source/demo/web/src")
for filename in ("package.json", "pnpm-lock.yaml", "vite.config.ts", "tsconfig.json", "local.html", "index.html", "compare.html"):
copy_file(REPO / "demo/web" / filename, output / "source/demo/web" / filename)
copy_file(REPO / "demo/web/public/favicon.png", output / "source/demo/web/public/favicon.png")
copy_file(Path(__file__), output / "source/scripts/deploy_huggingface_space.py")
copy_file(REPO / "scripts/check_huggingface_space.mjs", output / "source/scripts/check_huggingface_space.mjs")
copy_file(REPO / "scripts/prepare_public_demo.py", output / "source/scripts/prepare_public_demo.py")
copy_file(REPO / "scripts/decode_demo_tokens.mjs", output / "source/scripts/decode_demo_tokens.mjs")
copy_file(REPO / "configs/demo/public_examples.json", output / "source/configs/demo/public_examples.json")
copy_file(REPO / "deploy/huggingface/GeneralUser-LICENSE.txt", output / "source/deploy/huggingface/GeneralUser-LICENSE.txt")
copy_file(REPO / "deploy/huggingface/README.md", output / "source/deploy/huggingface/README.md")
for relative in public_files:
copy_file(web_dir / relative, output / "source/demo/web/public" / relative)
def git(*args: str) -> str:
return subprocess.check_output(["git", *args], cwd=REPO, text=True).strip()
receipt = {
"schema_version": 1,
"source_commit": git("rev-parse", "HEAD"),
"source_branch": git("branch", "--show-current"),
"source_dirty": bool(git("status", "--porcelain")),
"build_command": "VITE_HENDRIX_SPACE=1 npx --yes pnpm@10.20.0 --dir demo/web build --manifest",
"checkpoint_sha256": manifest["checkpoint"]["sha256"],
"example_catalog": {"count": len(catalog["examples"]), "sha256": sha256(web_dir / "examples/catalog.json")},
"files": {
path.relative_to(output).as_posix(): {"bytes": path.stat().st_size, "sha256": sha256(path)}
for path in sorted(output.rglob("*")) if path.is_file()
},
}
(output / "deployment.json").write_text(json.dumps(receipt, indent=2) + "\n")
return receipt
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-id", default="jackboyla/hendrix")
parser.add_argument("--graph-dir", type=Path, default=REPO / "progress/evaluations/yptf_web_fp32_d26")
parser.add_argument("--output-dir", type=Path, help="An empty staging directory; default: a new directory under progress/evaluations")
parser.add_argument("--upload", action="store_true", help="Create/update a public static Space with the current HF login")
args = parser.parse_args()
api = None
if args.upload:
from huggingface_hub import HfApi, get_token
if not get_token():
raise SystemExit("Log in first with: hf auth login")
api = HfApi()
api.whoami() # Fail before building if credentials are invalid.
subprocess.run(
["npx", "--yes", "pnpm@10.20.0", "--dir", "demo/web", "build", "--manifest"],
cwd=REPO, env={**os.environ, "VITE_HENDRIX_SPACE": "1"}, check=True,
)
evaluations = REPO / "progress/evaluations"
evaluations.mkdir(parents=True, exist_ok=True)
output = args.output_dir.resolve() if args.output_dir else Path(tempfile.mkdtemp(prefix="hf_space_", dir=evaluations))
receipt = package_space(
REPO / "demo/web/dist", args.graph_dir.resolve(),
REPO / "demo/web/node_modules/onnxruntime-web/dist", output,
)
total = sum(record["bytes"] for record in receipt["files"].values())
print(f"Checked package: {output} ({total / 1_000_000:.1f} MB)", flush=True)
if api is not None:
from huggingface_hub.errors import RepositoryNotFoundError
try:
existing = api.space_info(args.repo_id)
except RepositoryNotFoundError:
api.create_repo(args.repo_id, repo_type="space", space_sdk="static", private=False)
else:
if existing.private or existing.sdk != "static":
raise SystemExit("Existing Space must already be public and static; no settings were changed")
# No deletion patterns: preserve unrelated files in an existing Space.
commit = api.upload_folder(
repo_id=args.repo_id, repo_type="space", folder_path=output,
commit_message="Update Hendrix music transcription demo",
)
print(f"Space: https://huggingface.co/spaces/{args.repo_id}")
print(f"Commit: {commit.commit_url}")
if __name__ == "__main__":
main()
|