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.
- package/README.md +59 -24
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/bin/cozyclay.mjs +105 -11
- package/dist/CNAME +1 -0
- package/dist/app/index.html +57 -0
- package/dist/assets/app-Cgpk2hwX.js +4803 -0
- package/dist/assets/app-DgZvaAE1.css +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/icons/apple-touch-icon.png +0 -0
- package/dist/icons/icon-192.png +0 -0
- package/dist/icons/icon-32.png +0 -0
- package/dist/icons/icon-512.png +0 -0
- package/dist/icons/icon-maskable-512.png +0 -0
- package/dist/index.html +406 -5
- package/dist/manifest.webmanifest +42 -0
- package/dist/media/cozyclay-demo-poster.jpg +0 -0
- package/dist/media/cozyclay-logo.png +0 -0
- package/dist/robots.txt +4 -0
- package/dist/sitemap.xml +9 -0
- package/dist/sw.js +127 -0
- package/package.json +15 -3
- package/src/App.jsx +1393 -648
- package/src/ardy/client.js +5 -4
- package/src/ardy/mp4-duration.js +102 -0
- package/src/ardy/timeline-coordinates.js +11 -0
- package/src/ardy/timeline.jsx +528 -67
- package/src/ardy/waypoints.js +14 -9
- package/src/camera-block.js +60 -0
- package/src/camera-follow.js +49 -10
- package/src/camera-rail-schedule.js +176 -0
- package/src/controls.jsx +16 -2
- package/src/cuts.js +184 -0
- package/src/generation/client.js +56 -0
- package/src/generation/session.js +47 -0
- package/src/generation/shot-spec.js +40 -0
- package/src/generation/use-generation.js +55 -0
- package/src/hierarchy-model.js +9 -17
- package/src/hierarchy-panel.jsx +170 -23
- package/src/locale-toggle.jsx +18 -0
- package/src/locale.js +34 -0
- package/src/main.jsx +5 -0
- package/src/object-catalog.jsx +32 -3
- package/src/planview.jsx +174 -66
- package/src/pose-extract/browser.js +42 -0
- package/src/pose-extract/fit.js +194 -0
- package/src/pose-extract/index.js +53 -0
- package/src/pose-extract/keyframes.js +74 -0
- package/src/pose-extract/landmarks.js +162 -0
- package/src/poses.js +114 -0
- package/src/posestudio.jsx +75 -13
- package/src/pwa.js +42 -0
- package/src/result-modal.jsx +124 -0
- package/src/scenes.js +223 -0
- package/src/shot-authoring.js +204 -77
- package/src/shot.js +8 -4
- package/src/styles.css +839 -38
- package/src/ui.jsx +54 -5
- package/tools/ardy/BRIDGE.md +70 -4
- package/tools/ardy/README.md +55 -0
- package/tools/ardy/bridge.mjs +89 -389
- package/tools/ardy/cclay_constrained_generate.py +449 -0
- package/tools/ardy/cclay_motion_edit.py +74 -9
- package/tools/ardy/cclay_sequence_generate.py +68 -7
- package/tools/ardy/cclay_worker.py +304 -0
- package/tools/ardy/merge-text-encoder.py +160 -0
- package/tools/ardy/run-local.mjs +233 -0
- package/tools/ardy/runners/index.mjs +23 -0
- package/tools/ardy/runners/local.mjs +452 -0
- package/tools/ardy/runners/proc.mjs +125 -0
- package/tools/ardy/runners/remote.mjs +246 -0
- package/tools/ardy/setup-local.mjs +189 -0
- package/tools/ardy/setup-text-encoder-on-box.sh +93 -0
- package/tools/ardy/setup-text-encoder.py +296 -0
- package/tools/generation/README.md +29 -0
- package/tools/generation/bridge.mjs +149 -0
- package/tools/generation/job-store.mjs +51 -0
- package/tools/generation/providers/kling.mjs +76 -0
- package/tools/generation/providers/runway.mjs +101 -0
- package/tools/generation/providers/seedance.mjs +87 -0
- package/tools/generation/providers/shared.mjs +49 -0
- package/tools/generation/providers/veo.mjs +84 -0
- package/tools/metrics/archive-traffic.mjs +138 -0
- package/dist/assets/index-BW_S1YRy.js +0 -4395
- package/dist/assets/index-nRmmJgap.css +0 -1
- package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -0
- package/tools/ardy/out/gen-1786443326924-c6019e-generated.npz +0 -0
- package/tools/ardy/out/gen-1786443609325-0053db-generated.npz +0 -0
- package/tools/ardy/out/gen-1786443835628-749ed1-generated.npz +0 -0
- package/tools/ardy/out/gen-1786462605247-03cb19-generated.npz +0 -0
- package/tools/qa-browser.mjs +0 -84
- package/tools/qa-crop3.mjs +0 -22
- package/tools/qa-playview.mjs +0 -49
- package/tools/qa-screenshot.mjs +0 -24
- 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;
|
|
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
|
|
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
|
);
|
package/tools/ardy/BRIDGE.md
CHANGED
|
@@ -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
|
|
23
|
-
process group (
|
|
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
|
-
|
|
26
|
-
|
|
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
|
| --- | --- | --- |
|
package/tools/ardy/README.md
CHANGED
|
@@ -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
|