Spaces:
Running on RTX PRO 6000
Running on RTX PRO 6000
Commit ·
71ed715
1
Parent(s): 3f92f12
Library media from the artifact bucket (<ARTIFACT_S3_URI>/library-media/<corpus>/media.tar), not an HF dataset — indexing PR #24 (6b52aba), via this image's aws-CLI artifact_store (GPU variant)
Browse files
docker/hf-space/nginx.conf.template
CHANGED
|
@@ -262,7 +262,7 @@ server {
|
|
| 262 |
}
|
| 263 |
# Library media — the Explorer's joints viewer (model.glb + articulation.json), moving-parts and drop-test
|
| 264 |
# clips, per asset of this corpus: static files pipeline/library_media.py fetches at container start from
|
| 265 |
-
#
|
| 266 |
# this corpus) → a JSON 404 and the Explorer shows no media; never the SPA's index.html. Range requests
|
| 267 |
# (Safari's <video>) are nginx's own. add_header drops his server-level headers: his CORS lines re-declared.
|
| 268 |
location /library_media/ {
|
|
|
|
| 262 |
}
|
| 263 |
# Library media — the Explorer's joints viewer (model.glb + articulation.json), moving-parts and drop-test
|
| 264 |
# clips, per asset of this corpus: static files pipeline/library_media.py fetches at container start from
|
| 265 |
+
# the artifact bucket (supervisord program library-media). Not there (not fetched yet, or none for
|
| 266 |
# this corpus) → a JSON 404 and the Explorer shows no media; never the SPA's index.html. Range requests
|
| 267 |
# (Safari's <video>) are nginx's own. add_header drops his server-level headers: his CORS lines re-declared.
|
| 268 |
location /library_media/ {
|
docker/hf-space/supervisord-gpu.conf
CHANGED
|
@@ -122,7 +122,7 @@ stderr_logfile_maxbytes=0
|
|
| 122 |
|
| 123 |
[program:library-media]
|
| 124 |
; SimReady Central's library media for this corpus (the Explorer's joints viewer, moving-parts and drop-test
|
| 125 |
-
; clips), fetched once per container start from
|
| 126 |
; pipeline/library_media.py and nginx.conf.template's /library_media/. Optional: it always exits 0 and is never
|
| 127 |
; restarted; until it has finished (or when there is none) /library_media/ answers 404 and the Explorer shows
|
| 128 |
; no media.
|
|
|
|
| 122 |
|
| 123 |
[program:library-media]
|
| 124 |
; SimReady Central's library media for this corpus (the Explorer's joints viewer, moving-parts and drop-test
|
| 125 |
+
; clips), fetched once per container start from the artifact bucket into .build/library_media/ — see
|
| 126 |
; pipeline/library_media.py and nginx.conf.template's /library_media/. Optional: it always exits 0 and is never
|
| 127 |
; restarted; until it has finished (or when there is none) /library_media/ answers 404 and the Explorer shows
|
| 128 |
; no media.
|
docker/hf-space/supervisord.conf
CHANGED
|
@@ -120,7 +120,7 @@ stderr_logfile_maxbytes=0
|
|
| 120 |
|
| 121 |
[program:library-media]
|
| 122 |
; SimReady Central's library media for this corpus (the Explorer's joints viewer, moving-parts and drop-test
|
| 123 |
-
; clips), fetched once per container start from
|
| 124 |
; pipeline/library_media.py and nginx.conf.template's /library_media/. Optional: it always exits 0 and is never
|
| 125 |
; restarted; until it has finished (or when there is none) /library_media/ answers 404 and the Explorer shows
|
| 126 |
; no media.
|
|
|
|
| 120 |
|
| 121 |
[program:library-media]
|
| 122 |
; SimReady Central's library media for this corpus (the Explorer's joints viewer, moving-parts and drop-test
|
| 123 |
+
; clips), fetched once per container start from the artifact bucket into .build/library_media/ — see
|
| 124 |
; pipeline/library_media.py and nginx.conf.template's /library_media/. Optional: it always exits 0 and is never
|
| 125 |
; restarted; until it has finished (or when there is none) /library_media/ answers 404 and the Explorer shows
|
| 126 |
; no media.
|
pipeline/library_media.py
CHANGED
|
@@ -1,20 +1,21 @@
|
|
| 1 |
"""Library media for the Space's Explorer: the joints viewer's models, moving-parts and drop-test clips.
|
| 2 |
|
| 3 |
SimReady Central builds these per asset — model.glb + articulation.json for each prop with moving joints
|
| 4 |
-
(its scripts/library_media/build.py), the moving-parts clips its lab render pool renders, and the drop tests —
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
``
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
``
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
| 18 |
"""
|
| 19 |
|
| 20 |
from __future__ import annotations
|
|
@@ -22,20 +23,25 @@ from __future__ import annotations
|
|
| 22 |
import json
|
| 23 |
import os
|
| 24 |
import shutil
|
|
|
|
| 25 |
import sys
|
| 26 |
import tarfile
|
| 27 |
import time
|
| 28 |
from pathlib import Path
|
| 29 |
|
| 30 |
-
from .config import BUILD_DIR
|
| 31 |
|
| 32 |
-
LIBRARY_MEDIA_REPO_DEFAULT = "nvidia/simready-central-library-media"
|
| 33 |
ARCHIVE = "media.tar"
|
|
|
|
| 34 |
# The Explorer's index key for this Space (/config.js in nginx.conf.template): the manifest's URL names it.
|
| 35 |
INDEX_KEY = "usdsearch-dev"
|
| 36 |
-
# NOT under .build/restore/: the index restore (restore.download) empties that whole directory at every start.
|
| 37 |
MEDIA_ROOT = BUILD_DIR / "library_media"
|
| 38 |
-
DISABLED = {"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
def valid_manifest(path: Path) -> int | None:
|
|
@@ -73,48 +79,65 @@ def install(src: Path, dest: Path) -> None:
|
|
| 73 |
shutil.rmtree(old, ignore_errors=True)
|
| 74 |
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
def fetch(env: dict[str, str], root: Path = MEDIA_ROOT, download=None) -> int:
|
| 77 |
"""Fetch this corpus's media into ``root/INDEX_KEY``; returns the number of assets installed (0: none)."""
|
| 78 |
-
repo = env.get("LIBRARY_MEDIA_REPO", LIBRARY_MEDIA_REPO_DEFAULT).strip()
|
| 79 |
corpus = env.get("CORPUS_NAME", "").strip()
|
| 80 |
-
if
|
| 81 |
-
print("==> library media: off (
|
| 82 |
return 0
|
| 83 |
if not corpus or "/" in corpus or corpus.startswith("."):
|
| 84 |
print(f"==> library media: no usable CORPUS_NAME ({corpus!r}), skipped")
|
| 85 |
return 0
|
| 86 |
if download is None:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
root.mkdir(parents=True, exist_ok=True)
|
| 91 |
tmp = root / f".{INDEX_KEY}.download"
|
| 92 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 93 |
t0 = time.time()
|
| 94 |
try:
|
| 95 |
-
archive =
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
unpack(archive, tmp / "tree")
|
| 98 |
count = valid_manifest(tmp / "tree" / "manifest.json")
|
| 99 |
if count is None:
|
| 100 |
-
print(f"==> library media: {
|
| 101 |
return 0
|
| 102 |
install(tmp / "tree", root / INDEX_KEY)
|
| 103 |
finally:
|
| 104 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 105 |
size = sum(p.stat().st_size for p in (root / INDEX_KEY).rglob("*") if p.is_file())
|
| 106 |
-
print(f"==> library media: {count} assets from {
|
| 107 |
-
f"in {time.time() - t0:.0f} s")
|
| 108 |
return count
|
| 109 |
|
| 110 |
|
| 111 |
def main() -> int:
|
| 112 |
-
# supervisord runs this as uid 1000 with root's HOME: huggingface_hub's default home (~/.cache/huggingface, its
|
| 113 |
-
# token file and caches) is unreadable there, so it gets one under .build/ unless the image sets its own.
|
| 114 |
-
os.environ.setdefault("HF_HOME", str(BUILD_DIR / "hf-home"))
|
| 115 |
try:
|
| 116 |
fetch(dict(os.environ))
|
| 117 |
-
except Exception as exc: # noqa: BLE001 — media are optional: never a failed program, never a retry loop
|
|
|
|
| 118 |
print(f"==> library media: not fetched ({type(exc).__name__}: {str(exc)[:300]}) — none served", file=sys.stderr)
|
| 119 |
return 0
|
| 120 |
|
|
|
|
| 1 |
"""Library media for the Space's Explorer: the joints viewer's models, moving-parts and drop-test clips.
|
| 2 |
|
| 3 |
SimReady Central builds these per asset — model.glb + articulation.json for each prop with moving joints
|
| 4 |
+
(its scripts/library_media/build.py), the moving-parts clips its lab render pool renders, and the drop tests — and
|
| 5 |
+
they are kept in the artifact bucket next to the index runs, one archive per corpus:
|
| 6 |
+
``<ARTIFACT_S3_URI>/library-media/<CORPUS_NAME>/media.tar``, an uncompressed tar of ``manifest.json`` +
|
| 7 |
+
``<id>/<file>`` (its members are already compressed). Outside ``<corpus>/``, so nothing that lists a corpus's runs
|
| 8 |
+
sees it; corpus-level, not per run, because the media are keyed by the assets' URLs, not by an index build.
|
| 9 |
+
|
| 10 |
+
Run once at container start by supervisord (program ``library-media``), this downloads that ONE object with the
|
| 11 |
+
same aws CLI and credentials as the index restore (artifact_store), unpacks it and installs the tree as
|
| 12 |
+
``.build/library_media/<INDEX_KEY>/``; nginx serves it at /library_media/ (docker/hf-space/nginx.conf.template), where
|
| 13 |
+
the Explorer asks for /library_media/usdsearch-dev/manifest.json — usdsearch-dev being its index key in /config.js.
|
| 14 |
+
NOT under ``.build/restore/``: restore.download empties that whole directory at every start.
|
| 15 |
+
|
| 16 |
+
Nothing here is needed to search. Any failure — no archive for this corpus, no bucket credentials, a network error,
|
| 17 |
+
an unsafe or malformed archive — leaves the previous tree (none at boot): nginx answers 404 and the Explorer shows no
|
| 18 |
+
media. LIBRARY_MEDIA=off disables the fetch.
|
| 19 |
"""
|
| 20 |
|
| 21 |
from __future__ import annotations
|
|
|
|
| 23 |
import json
|
| 24 |
import os
|
| 25 |
import shutil
|
| 26 |
+
import subprocess
|
| 27 |
import sys
|
| 28 |
import tarfile
|
| 29 |
import time
|
| 30 |
from pathlib import Path
|
| 31 |
|
| 32 |
+
from .config import BUILD_DIR, Config
|
| 33 |
|
|
|
|
| 34 |
ARCHIVE = "media.tar"
|
| 35 |
+
PREFIX = "library-media"
|
| 36 |
# The Explorer's index key for this Space (/config.js in nginx.conf.template): the manifest's URL names it.
|
| 37 |
INDEX_KEY = "usdsearch-dev"
|
|
|
|
| 38 |
MEDIA_ROOT = BUILD_DIR / "library_media"
|
| 39 |
+
DISABLED = {"off", "none", "false", "0"}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def archive_uri(cfg: Config, corpus: str) -> str:
|
| 43 |
+
"""Where this corpus's media archive lives in the artifact bucket."""
|
| 44 |
+
return f"{cfg.require('ARTIFACT_S3_URI').rstrip('/')}/{PREFIX}/{corpus}/{ARCHIVE}"
|
| 45 |
|
| 46 |
|
| 47 |
def valid_manifest(path: Path) -> int | None:
|
|
|
|
| 79 |
shutil.rmtree(old, ignore_errors=True)
|
| 80 |
|
| 81 |
|
| 82 |
+
def cli_download(cfg: Config, uri: str, dest: Path, attempts: int = 3) -> bool:
|
| 83 |
+
"""``aws s3 cp`` ``uri`` to ``dest``; False when there is no such object (this image's artifact_store is the
|
| 84 |
+
aws-CLI one, before indexing PR #25's ``download_file``)."""
|
| 85 |
+
from .artifact_store import artifact_env, aws_cmd
|
| 86 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 87 |
+
for attempt in range(1, attempts + 1):
|
| 88 |
+
r = subprocess.run(aws_cmd(cfg) + ["cp", uri, str(dest), "--only-show-errors"],
|
| 89 |
+
capture_output=True, text=True, env=artifact_env(), timeout=1800)
|
| 90 |
+
if r.returncode == 0:
|
| 91 |
+
return True
|
| 92 |
+
err = (r.stderr or r.stdout).strip()
|
| 93 |
+
if "(404)" in err or "Not Found" in err or "does not exist" in err:
|
| 94 |
+
return False
|
| 95 |
+
print(f"==> library media: aws s3 cp failed (attempt {attempt}/{attempts}, exit {r.returncode}): {err[:300]}")
|
| 96 |
+
if attempt < attempts:
|
| 97 |
+
time.sleep(5 * attempt)
|
| 98 |
+
raise RuntimeError(f"aws s3 cp {uri} failed {attempts} times")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
def fetch(env: dict[str, str], root: Path = MEDIA_ROOT, download=None) -> int:
|
| 102 |
"""Fetch this corpus's media into ``root/INDEX_KEY``; returns the number of assets installed (0: none)."""
|
|
|
|
| 103 |
corpus = env.get("CORPUS_NAME", "").strip()
|
| 104 |
+
if env.get("LIBRARY_MEDIA", "").strip().lower() in DISABLED:
|
| 105 |
+
print("==> library media: off (LIBRARY_MEDIA)")
|
| 106 |
return 0
|
| 107 |
if not corpus or "/" in corpus or corpus.startswith("."):
|
| 108 |
print(f"==> library media: no usable CORPUS_NAME ({corpus!r}), skipped")
|
| 109 |
return 0
|
| 110 |
if download is None:
|
| 111 |
+
download = cli_download
|
| 112 |
+
cfg = Config(values=dict(env))
|
| 113 |
+
uri = archive_uri(cfg, corpus)
|
| 114 |
root.mkdir(parents=True, exist_ok=True)
|
| 115 |
tmp = root / f".{INDEX_KEY}.download"
|
| 116 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 117 |
t0 = time.time()
|
| 118 |
try:
|
| 119 |
+
archive = tmp / ARCHIVE
|
| 120 |
+
if not download(cfg, uri, archive):
|
| 121 |
+
print(f"==> library media: no {uri} — none served")
|
| 122 |
+
return 0
|
| 123 |
unpack(archive, tmp / "tree")
|
| 124 |
count = valid_manifest(tmp / "tree" / "manifest.json")
|
| 125 |
if count is None:
|
| 126 |
+
print(f"==> library media: {uri} has no usable manifest.json — none served")
|
| 127 |
return 0
|
| 128 |
install(tmp / "tree", root / INDEX_KEY)
|
| 129 |
finally:
|
| 130 |
shutil.rmtree(tmp, ignore_errors=True)
|
| 131 |
size = sum(p.stat().st_size for p in (root / INDEX_KEY).rglob("*") if p.is_file())
|
| 132 |
+
print(f"==> library media: {count} assets from {uri} ({size / 1e6:.0f} MB) in {time.time() - t0:.0f} s")
|
|
|
|
| 133 |
return count
|
| 134 |
|
| 135 |
|
| 136 |
def main() -> int:
|
|
|
|
|
|
|
|
|
|
| 137 |
try:
|
| 138 |
fetch(dict(os.environ))
|
| 139 |
+
except (Exception, SystemExit) as exc: # noqa: BLE001 — media are optional: never a failed program, never a retry loop
|
| 140 |
+
# (SystemExit too: artifact_store raises it when the bucket credentials are missing.)
|
| 141 |
print(f"==> library media: not fetched ({type(exc).__name__}: {str(exc)[:300]}) — none served", file=sys.stderr)
|
| 142 |
return 0
|
| 143 |
|