cozyclay 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +59 -24
  2. package/THIRD_PARTY_NOTICES.md +31 -0
  3. package/bin/cozyclay.mjs +105 -11
  4. package/dist/CNAME +1 -0
  5. package/dist/app/index.html +57 -0
  6. package/dist/assets/app-Cgpk2hwX.js +4803 -0
  7. package/dist/assets/app-DgZvaAE1.css +1 -0
  8. package/dist/favicon.ico +0 -0
  9. package/dist/icons/apple-touch-icon.png +0 -0
  10. package/dist/icons/icon-192.png +0 -0
  11. package/dist/icons/icon-32.png +0 -0
  12. package/dist/icons/icon-512.png +0 -0
  13. package/dist/icons/icon-maskable-512.png +0 -0
  14. package/dist/index.html +406 -5
  15. package/dist/manifest.webmanifest +42 -0
  16. package/dist/media/cozyclay-demo-poster.jpg +0 -0
  17. package/dist/media/cozyclay-logo.png +0 -0
  18. package/dist/robots.txt +4 -0
  19. package/dist/sitemap.xml +9 -0
  20. package/dist/sw.js +127 -0
  21. package/package.json +15 -3
  22. package/src/App.jsx +1393 -648
  23. package/src/ardy/client.js +5 -4
  24. package/src/ardy/mp4-duration.js +102 -0
  25. package/src/ardy/timeline-coordinates.js +11 -0
  26. package/src/ardy/timeline.jsx +528 -67
  27. package/src/ardy/waypoints.js +14 -9
  28. package/src/camera-block.js +60 -0
  29. package/src/camera-follow.js +49 -10
  30. package/src/camera-rail-schedule.js +176 -0
  31. package/src/controls.jsx +16 -2
  32. package/src/cuts.js +184 -0
  33. package/src/generation/client.js +56 -0
  34. package/src/generation/session.js +47 -0
  35. package/src/generation/shot-spec.js +40 -0
  36. package/src/generation/use-generation.js +55 -0
  37. package/src/hierarchy-model.js +9 -17
  38. package/src/hierarchy-panel.jsx +170 -23
  39. package/src/locale-toggle.jsx +18 -0
  40. package/src/locale.js +34 -0
  41. package/src/main.jsx +5 -0
  42. package/src/object-catalog.jsx +32 -3
  43. package/src/planview.jsx +174 -66
  44. package/src/pose-extract/browser.js +42 -0
  45. package/src/pose-extract/fit.js +194 -0
  46. package/src/pose-extract/index.js +53 -0
  47. package/src/pose-extract/keyframes.js +74 -0
  48. package/src/pose-extract/landmarks.js +162 -0
  49. package/src/poses.js +114 -0
  50. package/src/posestudio.jsx +75 -13
  51. package/src/pwa.js +42 -0
  52. package/src/result-modal.jsx +124 -0
  53. package/src/scenes.js +223 -0
  54. package/src/shot-authoring.js +204 -77
  55. package/src/shot.js +8 -4
  56. package/src/styles.css +839 -38
  57. package/src/ui.jsx +54 -5
  58. package/tools/ardy/BRIDGE.md +70 -4
  59. package/tools/ardy/README.md +55 -0
  60. package/tools/ardy/bridge.mjs +89 -389
  61. package/tools/ardy/cclay_constrained_generate.py +449 -0
  62. package/tools/ardy/cclay_motion_edit.py +74 -9
  63. package/tools/ardy/cclay_sequence_generate.py +68 -7
  64. package/tools/ardy/cclay_worker.py +304 -0
  65. package/tools/ardy/merge-text-encoder.py +160 -0
  66. package/tools/ardy/run-local.mjs +233 -0
  67. package/tools/ardy/runners/index.mjs +23 -0
  68. package/tools/ardy/runners/local.mjs +452 -0
  69. package/tools/ardy/runners/proc.mjs +125 -0
  70. package/tools/ardy/runners/remote.mjs +246 -0
  71. package/tools/ardy/setup-local.mjs +189 -0
  72. package/tools/ardy/setup-text-encoder-on-box.sh +93 -0
  73. package/tools/ardy/setup-text-encoder.py +296 -0
  74. package/tools/generation/README.md +29 -0
  75. package/tools/generation/bridge.mjs +149 -0
  76. package/tools/generation/job-store.mjs +51 -0
  77. package/tools/generation/providers/kling.mjs +76 -0
  78. package/tools/generation/providers/runway.mjs +101 -0
  79. package/tools/generation/providers/seedance.mjs +87 -0
  80. package/tools/generation/providers/shared.mjs +49 -0
  81. package/tools/generation/providers/veo.mjs +84 -0
  82. package/tools/metrics/archive-traffic.mjs +138 -0
  83. package/dist/assets/index-BW_S1YRy.js +0 -4395
  84. package/dist/assets/index-nRmmJgap.css +0 -1
  85. package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -0
  86. package/tools/ardy/out/gen-1786443326924-c6019e-generated.npz +0 -0
  87. package/tools/ardy/out/gen-1786443609325-0053db-generated.npz +0 -0
  88. package/tools/ardy/out/gen-1786443835628-749ed1-generated.npz +0 -0
  89. package/tools/ardy/out/gen-1786462605247-03cb19-generated.npz +0 -0
  90. package/tools/qa-browser.mjs +0 -84
  91. package/tools/qa-crop3.mjs +0 -22
  92. package/tools/qa-playview.mjs +0 -49
  93. package/tools/qa-screenshot.mjs +0 -24
  94. package/tools/qa-visual.mjs +0 -51
package/src/ui.jsx CHANGED
@@ -1,30 +1,75 @@
1
- import { useEffect, useId, useRef, useState } from "react";
1
+ import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
2
3
 
3
4
  // Custom dropdown, structurally matched to the reference (`.dropdown`,
4
5
  // `dd-caret`, `dropdown-menu`, `dropdown-item`, `dd-check`) with the keyboard
5
6
  // support the reference lacks. `highlighted` marks the arrow-key cursor;
6
7
  // the selection itself is `active`.
8
+ //
9
+ // The menu is portaled to document.body and fixed-positioned from the
10
+ // trigger rect: inspector foldouts are `.card { overflow: hidden }` inside a
11
+ // scroll pane, so an in-place absolute menu gets clipped to the card's
12
+ // height (the pose foldout is ~70 px tall against a ~290 px menu). Scrolling
13
+ // or resizing while open closes the menu instead of chasing the trigger.
7
14
  export function Dropdown({ value, options, onChange, ariaLabel }) {
8
15
  const [open, setOpen] = useState(false);
9
16
  const [highlight, setHighlight] = useState(-1);
10
17
  const rootRef = useRef(null);
11
18
  const triggerRef = useRef(null);
19
+ const menuRef = useRef(null);
20
+ const [menuBox, setMenuBox] = useState(null);
12
21
  const uid = useId();
13
22
  const listId = `${uid}-list`;
14
23
  const selectedIndex = options.findIndex((o) => o.value === value);
15
24
  const selected = selectedIndex >= 0 ? options[selectedIndex] : null;
16
25
 
17
- // Close on any mousedown outside the dropdown; effect cleanup also covers
26
+ // Close on any mousedown outside the dropdown; the portaled menu is not
27
+ // inside rootRef, so it is checked separately. Effect cleanup also covers
18
28
  // unmounting while open.
19
29
  useEffect(() => {
20
30
  if (!open) return;
21
31
  const onDocMouseDown = (e) => {
22
- if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
32
+ if (rootRef.current?.contains(e.target)) return;
33
+ if (menuRef.current?.contains(e.target)) return;
34
+ setOpen(false);
23
35
  };
24
36
  document.addEventListener("mousedown", onDocMouseDown);
25
37
  return () => document.removeEventListener("mousedown", onDocMouseDown);
26
38
  }, [open]);
27
39
 
40
+ // Position the portaled menu from the trigger: below when it fits, above
41
+ // otherwise, height capped to the free space. Scroll and resize close the
42
+ // menu — native selects behave the same and it beats a stale position.
43
+ useLayoutEffect(() => {
44
+ if (!open) {
45
+ setMenuBox(null);
46
+ return;
47
+ }
48
+ const rect = triggerRef.current?.getBoundingClientRect();
49
+ if (!rect) return;
50
+ const margin = 8;
51
+ const below = window.innerHeight - rect.bottom - margin;
52
+ const above = rect.top - margin;
53
+ const up = below < 160 && above > below;
54
+ setMenuBox({
55
+ left: rect.left,
56
+ width: rect.width,
57
+ maxHeight: Math.max(120, Math.min(288, up ? above : below)),
58
+ ...(up ? { bottom: window.innerHeight - rect.top + 2 } : { top: rect.bottom + 2 }),
59
+ });
60
+ const onAway = (e) => {
61
+ // Scrolling inside the menu itself must not dismiss it.
62
+ if (e.type === "scroll" && menuRef.current?.contains(e.target)) return;
63
+ setOpen(false);
64
+ };
65
+ window.addEventListener("resize", onAway);
66
+ window.addEventListener("scroll", onAway, true);
67
+ return () => {
68
+ window.removeEventListener("resize", onAway);
69
+ window.removeEventListener("scroll", onAway, true);
70
+ };
71
+ }, [open]);
72
+
28
73
  // Keyboard navigation starts from the current selection.
29
74
  useEffect(() => {
30
75
  if (open) setHighlight(selectedIndex);
@@ -87,12 +132,15 @@ export function Dropdown({ value, options, onChange, ariaLabel }) {
87
132
  <path d="M6 9l6 6 6-6" />
88
133
  </svg>
89
134
  </button>
90
- {open && (
135
+ {open && menuBox && createPortal(
91
136
  <div
92
137
  className="dropdown-menu"
138
+ ref={menuRef}
93
139
  id={listId}
94
140
  role="listbox"
95
141
  aria-activedescendant={highlight >= 0 && highlight < options.length ? `${uid}-opt-${highlight}` : undefined}
142
+ style={{ position: "fixed", zIndex: 80, ...menuBox, overflowY: "auto" }}
143
+ onKeyDown={onKeyDown}
96
144
  >
97
145
  {options.map((opt, i) => (
98
146
  <button
@@ -127,7 +175,8 @@ export function Dropdown({ value, options, onChange, ariaLabel }) {
127
175
  )}
128
176
  </button>
129
177
  ))}
130
- </div>
178
+ </div>,
179
+ document.body,
131
180
  )}
132
181
  </div>
133
182
  );
@@ -19,11 +19,77 @@ node tools/ardy/bridge.mjs # listens on 127.0.0.1:5181
19
19
  node tools/ardy/bridge.mjs --port 5182 # or COZYCLAY_BRIDGE_PORT=5182
20
20
  ```
21
21
 
22
- On startup it logs the bound address. Ctrl-C kills any in-flight generation
23
- process group (the ssh session is closed, so nothing is orphaned on the box).
22
+ On startup it logs the bound address and the selected backend. Ctrl-C kills
23
+ any in-flight generation process group (local children die with the bridge;
24
+ an ssh session is closed, so nothing is orphaned on a box).
24
25
 
25
- The bridge reads the same env vars `run-on-box.sh` reads. The remote SSH host
26
- has no public default and must be configured by the operator:
26
+ ## Backends: local and remote
27
+
28
+ The HTTP surface below is backend-agnostic; where generation actually runs
29
+ is a runner behind it (`tools/ardy/runners/`). Exactly one is selected at
30
+ startup:
31
+
32
+ | env | default | meaning |
33
+ | --- | --- | --- |
34
+ | `CCLAY_ARDY_MODE` | (unset) | `local` or `remote`; unset = remote when `CCLAY_ARDY_HOST` is set, local otherwise |
35
+
36
+ ### Local backend (`runners/local.mjs` → `run-local.mjs`)
37
+
38
+ Generation on this machine, no ssh anywhere. Provision once with
39
+ `npm run ardy:setup` (`tools/ardy/setup-local.mjs`): it clones
40
+ `github.com/nv-tlabs/ardy` (Apache-2.0), creates a venv with torch (CUDA
41
+ wheels on Linux, MPS on Apple Silicon), pre-fetches the
42
+ `nvidia/ARDY-Core-RP-20FPS-Horizon40` checkpoint from HuggingFace, and
43
+ downloads the SHA-256-pinned ungated text-encoder stack (~16.4 GB, resumable)
44
+ via the existing `setup-text-encoder.py` + `merge-text-encoder.py`. No model
45
+ weights ever live in this repo.
46
+
47
+ | env | default | meaning |
48
+ | --- | --- | --- |
49
+ | `CCLAY_ARDY_LOCAL_DIR` | `~/.cozyclay/ardy` | local ARDY checkout |
50
+ | `CCLAY_ARDY_LOCAL_VENV` | `<local-dir>/.venv/bin/python` | generator venv python |
51
+ | `CCLAY_ARDY_ENCODERS_DIR` | `~/.cozyclay/text-encoders` | encoder weights (`TEXT_ENCODERS_DIR`) |
52
+ | `CCLAY_ARDY_ENCODER_URL` | `http://127.0.0.1:9550/` | text-encoder service |
53
+ | `CCLAY_ARDY_ENCODER_DEVICE` | auto (cuda > mps > cpu) | force the encoder's torch device |
54
+ | `CCLAY_ARDY_ENCODER_IDLE_S` | `900` | shut the encoder down after this idle time |
55
+ | `CCLAY_ARDY_DEVICE` | auto (cuda > mps > cpu) | force the generator's torch device |
56
+
57
+ Device selection is `cuda > mps > cpu` on both the generator and the encoder
58
+ (`cclay_pick_device()` in the python scripts; `CCLAY_ARDY_DEVICE` overrides,
59
+ and a request's `cpu:true` maps onto it). `PYTORCH_ENABLE_MPS_FALLBACK=1` is
60
+ set so an MPS-unsupported op falls back to CPU instead of failing the run.
61
+
62
+ Generation runs through a **persistent worker** (`cclay_worker.py`, loopback
63
+ TCP on `CCLAY_ARDY_WORKER_PORT`, default 9552): the bridge prewarms it at
64
+ startup (`CCLAY_ARDY_PREWARM=0` opts out), it loads the motion model once,
65
+ compiles the GPU kernels with a no-text warmup call, and serves jobs from
66
+ the warm process — on this class of hardware that turns a ~2-minute
67
+ per-request cold spawn into seconds. The text encoder (Llama-3-8B, ~16 GB)
68
+ rides **inside the worker** (ARDY's in-process fallback, moved to the GPU as
69
+ fp16 with a NaN-guarded bf16 fallback), and a per-prompt embedding disk
70
+ cache (`~/.cozyclay/embed-cache`) makes every repeated prompt free —
71
+ re-seeding, re-pinning, or re-routing the same prompt skips Llama entirely.
72
+ `run-local.mjs` falls back to a direct one-shot spawn whenever the worker is
73
+ unavailable (and for forced-CPU runs), so the worker is an accelerator,
74
+ never a dependency. The worker idles out after `CCLAY_ARDY_WORKER_IDLE_S`
75
+ (default 3600 s); the standalone gradio encoder service is only started for
76
+ those worker-less fallback runs, idling out after
77
+ `CCLAY_ARDY_ENCODER_IDLE_S` (default 900 s). Health reports
78
+ `encoder: "on-demand"` while the service is down — that is a normal state,
79
+ not an error.
80
+
81
+ Single-prompt runs use the repo-owned `cclay_constrained_generate.py`
82
+ (pose pins via `FullBodyConstraintSet`, root paths via
83
+ `Root2DConstraintSet`, free generation with no constraints — no base motion
84
+ and no two-pass dance needed); segment schedules and motion edits run the
85
+ same `cclay_sequence_generate.py` / `cclay_motion_edit.py` the box does,
86
+ executed in place from this repo.
87
+
88
+ ### Remote backend (`runners/remote.mjs` → `run-*-on-box.sh`)
89
+
90
+ The original ssh flow, byte-for-byte: the bridge reads the same env vars
91
+ `run-on-box.sh` reads. The SSH host has no public default and must be
92
+ configured by the operator:
27
93
 
28
94
  | env | default | meaning |
29
95
  | --- | --- | --- |
@@ -28,6 +28,61 @@ Optional overrides:
28
28
  SSH must work non-interactively with `BatchMode=yes`. Hardware and device
29
29
  selection are operator concerns; pass `--cpu` when CPU generation is desired.
30
30
 
31
+ ## Token-free text encoder setup
32
+
33
+ ARDY's default text-encoder stack (LLM2Vec) resolves its base weights to the
34
+ gated `meta-llama/Meta-Llama-3-8B-Instruct` repository, which is why the
35
+ upstream README asks for a Hugging Face account, gated-access approval, and
36
+ a token on the box. None of that is needed: the same stack is available from
37
+ public, ungated repositories, and ARDY already supports loading it from a
38
+ local directory via `TEXT_ENCODERS_DIR`.
39
+
40
+ ```sh
41
+ CCLAY_ARDY_HOST=user@gpu-box tools/ardy/setup-text-encoder-on-box.sh
42
+ ```
43
+
44
+ copies `setup-text-encoder.py` to the box and downloads (~16.4 GB, resumable;
45
+ re-runs only verify and fill gaps):
46
+
47
+ - `NousResearch/Meta-Llama-3-8B-Instruct` — public mirror of the base
48
+ weights, every shard verified against a pinned SHA-256
49
+ - `McGill-NLP/LLM2Vec-Meta-Llama-3-8B-Instruct-mntp` and `-mntp-supervised`
50
+ — the two LLM2Vec adapters (MIT)
51
+
52
+ into `~/cclay-text-encoders` on the box (`CCLAY_ARDY_ENCODERS_DIR`
53
+ overrides), rewrites each adapter's `base_model_name_or_path` to the local
54
+ base directory so nothing resolves back to the gated repo at runtime, and
55
+ then bakes the base + MNTP-LoRA merge into the runtime path
56
+ (`merge-text-encoder.py`, run in the ARDY encoder venv). The merge step is
57
+ required, not cosmetic: transformers v5 removed the automatic
58
+ adapter-directory resolution ARDY's vendored LLM2Vec loader relies on, so
59
+ the local MNTP path must be a full model. The merged stack is numerically
60
+ identical to the gated flow (cosine ≥ 0.99996 against a live token-based
61
+ encoder service on the same prompts).
62
+ All three revisions are pinned to full commit SHAs — the same stack
63
+ nv-tlabs/ardy issue #9 reproduced against — so every install encodes
64
+ prompts byte-identically.
65
+
66
+ Start the encoder service with the directory exported:
67
+
68
+ ```sh
69
+ ssh $CCLAY_ARDY_HOST
70
+ cd ~/ardy
71
+ TEXT_ENCODERS_DIR=$HOME/cclay-text-encoders \
72
+ .venv/bin/python scripts/run_text_encoder_server.py
73
+ ```
74
+
75
+ `setup-text-encoder-on-box.sh --verify-only` re-hashes an existing tree
76
+ without downloading. To provision a machine directly (no ssh hop), run
77
+ `python3 tools/ardy/setup-text-encoder.py --dest <dir>` on it; the script is
78
+ stdlib-only.
79
+
80
+ Licensing: the base weights are Meta Llama 3 (Meta Llama 3 Community
81
+ License; the LICENSE and USE_POLICY.md land next to the weights). CozyClay
82
+ does not redistribute any model files — the script downloads them from
83
+ their public sources to the operator's own machine. Built with Meta Llama 3.
84
+ See `THIRD_PARTY_NOTICES.md`.
85
+
31
86
  ## Where the motions live on the box
32
87
 
33
88
  - Base (first-pass, unconstrained) motions: `~/ardy/outputs/*.npz` and