cozyclay 1.0.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 (85) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +109 -0
  3. package/THIRD_PARTY_NOTICES.md +29 -0
  4. package/bin/cozyclay.mjs +194 -0
  5. package/dist/ardy/cskel27-rest.json +557 -0
  6. package/dist/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  7. package/dist/assets/basis_transcoder-o4Hde_L7.js +19 -0
  8. package/dist/assets/draco_decoder-C32yEggz.wasm +0 -0
  9. package/dist/assets/draco_decoder-Z1_iN-Ht.wasm +0 -0
  10. package/dist/assets/draco_decoder-fzg4nYZr.js +34 -0
  11. package/dist/assets/draco_wasm_wrapper-DxJM36Ib.js +117 -0
  12. package/dist/assets/draco_wasm_wrapper-fZCQGLGb.js +116 -0
  13. package/dist/assets/index-BW_S1YRy.js +4395 -0
  14. package/dist/assets/index-nRmmJgap.css +1 -0
  15. package/dist/demo/walk-then-stop.npz +0 -0
  16. package/dist/fonts/instrument-serif-italic-latin.woff2 +0 -0
  17. package/dist/fonts/instrument-serif-latin.woff2 +0 -0
  18. package/dist/fonts/inter-latin.woff2 +0 -0
  19. package/dist/index.html +13 -0
  20. package/dist/models/x-bot-tpose.fbx +0 -0
  21. package/dist/models/y-bot-tpose.fbx +0 -0
  22. package/package.json +80 -0
  23. package/src/App.jsx +3840 -0
  24. package/src/ardy/client.js +140 -0
  25. package/src/ardy/convert.js +313 -0
  26. package/src/ardy/cskel27-neutral.js +39 -0
  27. package/src/ardy/cskel27.js +68 -0
  28. package/src/ardy/export.js +157 -0
  29. package/src/ardy/ik.js +610 -0
  30. package/src/ardy/npz.js +520 -0
  31. package/src/ardy/playback.js +414 -0
  32. package/src/ardy/prompt-clips.js +16 -0
  33. package/src/ardy/timeline-coordinates.js +17 -0
  34. package/src/ardy/timeline-resize.js +11 -0
  35. package/src/ardy/timeline.jsx +723 -0
  36. package/src/ardy/to-cskel27.js +205 -0
  37. package/src/ardy/waypoints.js +364 -0
  38. package/src/camera-follow.js +366 -0
  39. package/src/camera-move.js +286 -0
  40. package/src/controls.jsx +245 -0
  41. package/src/dualview.jsx +305 -0
  42. package/src/hierarchy-model.js +77 -0
  43. package/src/hierarchy-panel.jsx +365 -0
  44. package/src/history.js +90 -0
  45. package/src/main.jsx +10 -0
  46. package/src/object-catalog.jsx +90 -0
  47. package/src/object-gizmo.jsx +755 -0
  48. package/src/planview.jsx +653 -0
  49. package/src/poses.js +424 -0
  50. package/src/posestudio.jsx +811 -0
  51. package/src/props.jsx +302 -0
  52. package/src/room.jsx +64 -0
  53. package/src/scene-history.js +125 -0
  54. package/src/scene-objects.js +420 -0
  55. package/src/shot-authoring.js +113 -0
  56. package/src/shot.js +239 -0
  57. package/src/styles.css +5639 -0
  58. package/src/ui.jsx +391 -0
  59. package/src/use-render-activity.js +96 -0
  60. package/tools/ardy/BRIDGE.md +255 -0
  61. package/tools/ardy/README.md +136 -0
  62. package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -0
  63. package/tools/ardy/bridge.mjs +1427 -0
  64. package/tools/ardy/cclay_motion_edit.py +445 -0
  65. package/tools/ardy/cclay_sequence_generate.py +595 -0
  66. package/tools/ardy/dump-npz.py +205 -0
  67. package/tools/ardy/extract-rest.mjs +299 -0
  68. package/tools/ardy/npz.mjs +335 -0
  69. package/tools/ardy/out/gen-1786443326924-c6019e-generated.npz +0 -0
  70. package/tools/ardy/out/gen-1786443609325-0053db-generated.npz +0 -0
  71. package/tools/ardy/out/gen-1786443835628-749ed1-generated.npz +0 -0
  72. package/tools/ardy/out/gen-1786462605247-03cb19-generated.npz +0 -0
  73. package/tools/ardy/pose-to-npz.mjs +106 -0
  74. package/tools/ardy/run-edit-on-box.sh +73 -0
  75. package/tools/ardy/run-on-box.sh +568 -0
  76. package/tools/ardy/run-sequence-on-box.sh +162 -0
  77. package/tools/ardy/visual-qa.mjs +188 -0
  78. package/tools/ardy/vq-car.mjs +52 -0
  79. package/tools/dev-full.mjs +29 -0
  80. package/tools/process-supervisor.mjs +63 -0
  81. package/tools/qa-browser.mjs +84 -0
  82. package/tools/qa-crop3.mjs +22 -0
  83. package/tools/qa-playview.mjs +49 -0
  84. package/tools/qa-screenshot.mjs +24 -0
  85. package/tools/qa-visual.mjs +51 -0
@@ -0,0 +1,255 @@
1
+ # ARDY dev bridge (`tools/ardy/bridge.mjs`)
2
+
3
+ A dependency-free Node (`node:http` only) sidecar that makes the full ARDY
4
+ loop drivable from the CozyClay UI without turning CozyClay into a backend
5
+ app.
6
+
7
+ CozyClay stays a static SPA: `vite build` emits a `dist/` that serves from
8
+ anywhere with no server. The box work — pose → npz conversion and the remote
9
+ constrained generation — cannot run in a browser, so the bridge exposes it as
10
+ four HTTP endpoints on **127.0.0.1:5181** and the Vite dev server proxies
11
+ `/ardy` to it. The bridge is **optional and dev-only**: when it is not
12
+ running, the app works exactly as before with the generate affordance visibly
13
+ unavailable, and the production build never depends on it.
14
+
15
+ ## Start / stop
16
+
17
+ ```sh
18
+ node tools/ardy/bridge.mjs # listens on 127.0.0.1:5181
19
+ node tools/ardy/bridge.mjs --port 5182 # or COZYCLAY_BRIDGE_PORT=5182
20
+ ```
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).
24
+
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:
27
+
28
+ | env | default | meaning |
29
+ | --- | --- | --- |
30
+ | `COZYCLAY_BRIDGE_PORT` | `5181` | listen port (loopback only) |
31
+ | `CCLAY_ARDY_HOST` | required | ssh destination for the ARDY host |
32
+ | `CCLAY_ARDY_REPO` | `$HOME/ardy` | ARDY checkout on the box |
33
+ | `CCLAY_ARDY_VENV` | `~/ardy/.venv-cuda/bin/python` | generator venv python on the box |
34
+ | `CCLAY_ARDY_ENCODER_URL` | `http://127.0.0.1:9550/` | text-encoder service |
35
+
36
+ The box carries two venvs and they are **not interchangeable**: the CPU
37
+ `.venv` runs the text-encoder service and is used for the numpy-only read
38
+ steps (frame counts, reference dumps); the `.venv-cuda` venv runs the motion
39
+ generator and is what the device probe and generation use.
40
+
41
+ ## Endpoints
42
+
43
+ ### `GET /ardy/health`
44
+
45
+ Probes the box in one ssh round trip: the host answering, the encoder URL's
46
+ HTTP status, and the device the generator venv would pick (the exact probe
47
+ line `run-on-box.sh` uses, under the same env).
48
+
49
+ ```json
50
+ 200 {"ok":true,"host":"user@ardy-host","encoder":200,"device":"cuda:0"}
51
+ 503 {"ok":false,"reason":"<human readable>"}
52
+ ```
53
+
54
+ Successes and failures are cached for 5 s so the UI's health polling does not
55
+ hammer ssh.
56
+
57
+ ### `GET /ardy/bases`
58
+
59
+ Lists `~/ardy/outputs/*.npz` and `~/ardy/outputs/omb/*.npz` on the box with
60
+ real frame counts, read in a single ssh call (one `np.load(...)['posed_joints'].shape[0]`
61
+ per file, via a python heredoc on the box — never one ssh call per file).
62
+ Files that fail to load are skipped with a note on the bridge's stderr.
63
+
64
+ ```json
65
+ 200 {"bases":[{"id":"...","path":"outputs/omb/....npz","frames":80}, ...]}
66
+ ```
67
+
68
+ Cached for 120 s; the list changes rarely. If the same name exists under both
69
+ `outputs/` and `outputs/omb/`, both entries are listed and generation picks
70
+ the first — the same lookup order `run-on-box.sh` uses.
71
+
72
+ ### `POST /ardy/generate`
73
+
74
+ Multi-block autoregressive request:
75
+
76
+ ```json
77
+ {
78
+ "prompt": "a person walking",
79
+ "duration": 4,
80
+ "posePin": false,
81
+ "segments": [
82
+ {"startFrame": 0, "endFrame": 40, "prompt": "a person walking"},
83
+ {"startFrame": 40, "endFrame": 80, "prompt": "the person stops"}
84
+ ],
85
+ "seed": 2
86
+ }
87
+ ```
88
+
89
+ For a single-prompt refinement request, `poses` is the source of truth. Each entry carries an authored
90
+ full-body pose plus ARDY-space root XYZ in metres. Frames must be strictly
91
+ ascending and are converted independently with the floor-aligned canonical
92
+ CoreSkeleton27 reference. No user-selected motion supplies root height.
93
+ `run-on-box.sh` passes every entry as a repeated
94
+ `--pose-from <npz> 0 <frame>` constraint. The remote
95
+ `FullBodyConstraintSet` pins joint positions, root X/Y/Z, and heading at that
96
+ frame.
97
+
98
+ `segments` is optional. When present it must contain 2..64 contiguous,
99
+ non-overlapping ranges covering frame 0 through `duration * 20`; each range
100
+ has its own non-empty prompt. The bridge passes the complete schedule to
101
+ `cclay_sequence_generate.py` in one remote process. ARDY stays loaded and
102
+ each block receives the previous block's normalized motion tail through
103
+ `init_history_sequence`; no per-block process restart, NPZ concatenation, or
104
+ crossfade occurs. Segment mode requires `posePin:false`.
105
+
106
+ `segments` + `waypoints` together run the sequence generator with a root
107
+ path: the Root2D constraint set is built once over the whole rollout
108
+ (rollout-global frames, origin facing +Z) and each chained call receives its
109
+ own slice with history frames zeroed — the interactive demo's streaming
110
+ pattern. The 10 s trained window then binds each prompt block (the bridge
111
+ rejects any block longer than that), not the total clip, so a root path can
112
+ ride a rollout longer than 10 s. Without `segments`, `waypoints` still run
113
+ the one-shot constrained generator and the whole clip must fit 10 s.
114
+
115
+ A single-prompt rough generation sends `posePin:false` and no `segments`; it
116
+ remains a plain text-to-motion run. Once that motion is loaded, CozyClay can
117
+ send the composite motion+IK poses and roots as `poses` for constrained
118
+ single-prompt regeneration.
119
+ The legacy single `pose` + `dstFrame` form remains accepted and is normalized
120
+ to one `poses` entry.
121
+
122
+ For a loaded multi-block motion with authored IK keys, CozyClay sends
123
+ `motionEdit` instead of replaying the whole prompt schedule. The edit owns a
124
+ source motion URL, an editable interval, history/future context lengths, and
125
+ sparse authored keys. Each key names only the IK tracks actually changed.
126
+ `cclay_motion_edit.py` supplies the preceding source frames through ARDY's
127
+ `init_history_sequence`, supplies following source frames as observed future
128
+ context, and creates position/rotation masks only for the named joints.
129
+ Root position/heading use a separate track: body-only edits preserve the
130
+ source root exactly, while hips edits author a smooth root offset. Contact
131
+ arrays stay source-owned unless a foot/leg track was edited.
132
+
133
+ Pending browser IK keys remain a non-destructive layer until this call
134
+ finishes and the returned motion loads. On success they are recorded as
135
+ committed edit metadata and removed from the pending IK map, preventing the
136
+ same correction from being applied again over the generated motion.
137
+
138
+ `regenerateSegments` remains accepted as a legacy bridge payload; the
139
+ CozyClay UI no longer uses it. This task intentionally does not remove that
140
+ old route.
141
+
142
+ `base` is optional legacy/debug input only. The CozyClay UI does not expose
143
+ it. Pose conversion uses the canonical neutral skeleton, while the shell uses
144
+ a hidden upright NPZ only to satisfy the current remote script's parser and
145
+ baseline report. That NPZ does not supply the authored pose root.
146
+
147
+ On success the response is NDJSON:
148
+
149
+ ```json
150
+ {"event":"status","message":"..."}
151
+ {"event":"report","report":{}}
152
+ {"event":"done","output":"<absolute path>","bytes":123,"motionUrl":"/ardy/motions/<run-id>"}
153
+ ```
154
+
155
+ Validation rejects malformed roots, duplicate/out-of-range pose frames,
156
+ non-contiguous segments, empty prompts, invalid durations/seeds, and mixed
157
+ segment+waypoint requests with a 400 naming the offending field. Child process
158
+ groups are killed on disconnect. A `done` event is emitted only after the
159
+ requested output exists and its byte count matches the shell marker.
160
+
161
+ ### `GET /ardy/motions/<run-id>`
162
+
163
+ Serves the exact npz produced by a successful generation. `<run-id>` is the
164
+ id embedded in the `done` event's `motionUrl` — it is a lookup key, never a
165
+ path. On success the response is `200` with
166
+ `Content-Type: application/octet-stream` and
167
+ `Content-Disposition: attachment; filename="gen-<ts>-<rand>-constrained.npz"`
168
+ (pose runs) or `"gen-<ts>-<rand>-generated.npz"` (`posePin: false` runs).
169
+
170
+ The bridge keeps an in-memory run-id → absolute path allowlist that is
171
+ populated **only after this process generated the file and verified it on
172
+ disk** (see the `done` verification above). Unknown, expired, or
173
+ never-this-process ids return `404` JSON; a path is never accepted from the
174
+ URL, query, or body. Served files are always under `tools/ardy/out/` — the
175
+ allowlist only ever holds paths the bridge itself joined there, and the
176
+ check is re-applied at serve time.
177
+
178
+ ## Root coordinate convention (`--root-2d`)
179
+
180
+ ARDY is **Y-up**: the character stands on the X/Z ground plane, and root
181
+ positions are horizontal X and Z in **meters** relative to the ARDY origin
182
+ (Y is not constrained by root guidance). A path request carries **2..32
183
+ sparse keys** beginning at frame **0**, with strictly ascending frame
184
+ indices. CozyClay converts authored scene positions into clip-local X/Z and
185
+ uses `heading:null`, leaving facing free. Each key is forwarded to
186
+ `run-on-box.sh` as `--root-2d FRAME X Z none`; ARDY generates every
187
+ intermediate frame. The script `%q`-quotes every value into the remote
188
+ generator invocation.
189
+
190
+ ## Failure semantics
191
+
192
+ - Before the stream starts (bad fields, unreachable box): plain JSON
193
+ `400`/`503` responses.
194
+ - After the stream starts: failures arrive as an `error` event, then the
195
+ stream ends.
196
+ - A client disconnect kills the detached child process group (SIGTERM, then
197
+ SIGKILL after 3 s) — the bash script and the ssh session die together, so
198
+ no remote generation is orphaned.
199
+ - `OPTIONS` preflight is answered with `204` and **no CORS headers**, so a
200
+ cross-origin browser preflight always fails; unknown paths return `404`
201
+ JSON; wrong methods on known paths return `405`.
202
+
203
+ ## Security posture
204
+
205
+ - **Loopback only.** The server binds `127.0.0.1`; the host is not
206
+ configurable. This process shells out to a machine that can run GPU work,
207
+ so it must never be reachable from the network.
208
+ - **Browser access is same-origin proxy-only; there is no CORS.** The bridge
209
+ never sends `Access-Control-Allow-*` headers on any response (JSON, NDJSON,
210
+ binary, or `OPTIONS`), so the browser's same-origin policy is the
211
+ enforcement boundary: a page from any other origin — including an arbitrary
212
+ website the operator happens to have open — cannot read a response, and the
213
+ `POST /ardy/generate` preflight (JSON bodies are never "simple" requests)
214
+ is refused before it reaches the bridge. Loopback alone is insufficient:
215
+ binding `127.0.0.1` only keeps other *hosts* out, while a request from the
216
+ operator's own browser arrives from the operator's machine, on the loopback
217
+ interface, indistinguishable from a legitimate local call. The CozyClay UI
218
+ always talks to the bridge through the Vite dev server's same-origin
219
+ `/ardy` proxy (server-side forwarding, no browser CORS involved); direct
220
+ browser access from another origin is unsupported by design. `curl` and
221
+ other non-browser clients are unaffected — they do not enforce
222
+ same-origin policy.
223
+ - **argv arrays, never shell strings.** Request data (prompt, base, frames,
224
+ waypoints) is passed to `spawn` as separate argv entries; no request value
225
+ is ever interpolated into a shell string. The only remote shell strings are
226
+ built from the box's own listing (regex-whitelisted to
227
+ `outputs/(omb/)?[A-Za-z0-9._-]+\.npz`) or from operator env vars.
228
+ - **Everything is validated**: prompt length, duration/dstFrame ranges, base
229
+ whitelist, frame-zero root start bounds, body size cap, `seed`/`cpu` types.
230
+ - **ssh hardening**: `BatchMode` (never a password prompt), `ConnectTimeout`,
231
+ `ServerAlive*` — the same options `run-on-box.sh` uses.
232
+ - **No writes to `~/ardy`**: the dump script is copied to `/tmp` on the box
233
+ and removed afterwards; all artifacts land under the gitignored
234
+ `tools/ardy/out/`.
235
+ - **Served npz files are allowlisted, not addressed.** `GET /ardy/motions/<run-id>`
236
+ resolves the id against an in-memory map populated only after this process
237
+ generated and verified the file; a path never comes from the URL, query, or
238
+ body, and nothing outside `tools/ardy/out/` is ever served.
239
+
240
+ ## Caching
241
+
242
+ | data | cache | why |
243
+ | --- | --- | --- |
244
+ | health | 5 s (success and failure) | the UI polls it; a dead box must not become an ssh stampede |
245
+ | bases | 120 s | the list changes rarely |
246
+ | reference dumps | on disk under `tools/ardy/out/refs/` | same base, reused across requests |
247
+
248
+ ## Relationship to the rest of the repo
249
+
250
+ - `tools/ardy/pose-to-npz.mjs`, `tools/ardy/run-on-box.sh`,
251
+ `tools/ardy/dump-npz.py`, `src/ardy/*` and `test/ardy/*` are the verified,
252
+ reusable pieces; the bridge calls them, never reimplements them.
253
+ - The Vite dev server (`vite.config.js`) proxies `/ardy` to
254
+ `127.0.0.1:5181`; the production `dist/` build contains no proxy, no
255
+ server code, and no dependency on the bridge.
@@ -0,0 +1,136 @@
1
+ # CozyClay -> ARDY pose bridge
2
+
3
+ `run-on-box.sh` pushes a synthetic pose `.npz` produced from a CozyClay pose
4
+ into the ARDY text-to-motion pipeline on the remote box, runs a constrained
5
+ generation pass that honours that pose, and pulls the resulting motion back.
6
+ This is the "long tail" of the bridge: the pose is authored in CozyClay,
7
+ converted to an ARDY motion npz locally, and this script is what actually
8
+ gets the box to generate with it.
9
+
10
+ ## Remote ARDY host
11
+
12
+ The bridge expects an SSH-accessible machine where ARDY is already installed.
13
+ CozyClay deliberately does not prescribe a VPN, hostname scheme, cloud
14
+ provider, or network topology.
15
+
16
+ Configure the host explicitly before starting the bridge:
17
+
18
+ ```sh
19
+ export CCLAY_ARDY_HOST="<ssh-user>@<ssh-host>"
20
+ ```
21
+
22
+ Optional overrides:
23
+
24
+ - `CCLAY_ARDY_REPO` — ARDY checkout on the remote host (default `$HOME/ardy`)
25
+ - `CCLAY_ARDY_VENV` — generator Python (default `~/ardy/.venv-cuda/bin/python`)
26
+ - `CCLAY_ARDY_ENCODER_URL` — encoder URL as seen from the remote host
27
+
28
+ SSH must work non-interactively with `BatchMode=yes`. Hardware and device
29
+ selection are operator concerns; pass `--cpu` when CPU generation is desired.
30
+
31
+ ## Where the motions live on the box
32
+
33
+ - Base (first-pass, unconstrained) motions: `~/ardy/outputs/*.npz` and
34
+ `~/ardy/outputs/omb/*.npz`. `run-on-box.sh` resolves a bare `--base` id
35
+ against `outputs/<id>.npz` first, then `outputs/omb/<id>.npz`; a bare
36
+ `<name>.npz` is treated the same with the suffix stripped; anything with a
37
+ `/` is used as a repo-relative or absolute path.
38
+ - The base npz must be at least as long as the requested clip — the
39
+ generator rejects a shorter base (`--base npz has N frames but the
40
+ requested clip is M`), so pick a base that covers `--duration`.
41
+ - Nothing is ever written into the checkout: the pose npz and the generated
42
+ output live under a fresh `mktemp -d` dir on the box (usually `/tmp`),
43
+ which an `EXIT` trap removes even on failure.
44
+
45
+ ## The pose npz contract
46
+
47
+ `<pose.npz>` is an ARDY motion npz — the same format as a base motion — that
48
+ must carry `local_rot_mats` and `posed_joints`. One frame is enough: the
49
+ CozyClay pose is baked into `local_rot_mats[src_frame]` (the cskel27
50
+ per-joint local rotations, built from the CozyClay basis quaternions via
51
+ `basis = Rb^T @ L @ Rb` / `L = Rb @ basis @ Rb^T`, where `Rb` is the bone's
52
+ armature-space rest rotation — see CozyClay `motion_retarget.py` /
53
+ `motion_constraints.py`). The src-frame is range-checked against the npz by
54
+ the generator remotely, so an out-of-range `--src-frame` dies on the box
55
+ with a clear message rather than silently.
56
+
57
+ ## The generation grammar
58
+
59
+ The raw generator flag for a full-body pose constraint is:
60
+
61
+ ```
62
+ --pose-from <src-npz> <src-frame> <dst-frame>
63
+ ```
64
+
65
+ It copies **every joint's** pose from `<src-npz>` at `<src-frame>` and pins
66
+ it at `<dst-frame>` of the new clip. It is repeatable, requires `--base`,
67
+ and works for poses no end-effector constraint can express (sitting, lying,
68
+ reaching). The clip is `int(duration * 20)` frames at ARDY's 20 fps, and
69
+ `dst-frame` must satisfy `0 <= dst-frame < duration * 20`; the generator
70
+ also rejects clips under 3 frames.
71
+
72
+ `run-on-box.sh` maps its arguments onto the generator one-to-one:
73
+
74
+ ```
75
+ run-on-box.sh <pose.npz> --base <motion-id|npz-path> --prompt "<prompt>" \
76
+ --duration <seconds> --dst-frame <N> [--src-frame <N>] [--seed <S>] \
77
+ [--output <local.npz>] [--dry-run]
78
+ ```
79
+
80
+ which becomes, on the box (modulo the temp dir):
81
+
82
+ ```
83
+ cd $HOME/ardy && ~/ardy/.venv-cuda/bin/python \
84
+ scripts/cclay_constrained_generate.py \
85
+ --prompt "<prompt>" --duration <seconds> \
86
+ --base <resolved-base> --output <tmp>/out \
87
+ --pose-from <tmp>/pose.npz <src-frame> <dst-frame> [--seed <S>]
88
+ ```
89
+
90
+ The CozyClay wrapper `cclay-ardy-generate` exposes the same feature through
91
+ its own grammar — `--constrain-pose <src-motion-id> <src-frame> <dst-frame>`
92
+ — where `<src-motion-id>` is a motion already staged in the CozyClay
93
+ project (`.cclay/motions/<id>.npz`) rather than a raw npz path. `run-on-box.sh`
94
+ exists for the CozyClay flow, where the pose source is a synthetic npz
95
+ produced by the conversion module and the base motion is a pre-existing
96
+ `~/ardy/outputs/*.npz`; both paths drive the same `--pose-from` mechanism.
97
+
98
+ ## Preflight, safety, idempotence
99
+
100
+ Before anything is pushed, `run-on-box.sh` fails fast, cheapest check
101
+ first:
102
+
103
+ 1. SSH reachability (`BatchMode` + `ConnectTimeout`, so a dead host or a
104
+ missing key errors in seconds, not minutes).
105
+ 2. `~/ardy/.venv/bin/python` and `scripts/cclay_constrained_generate.py`
106
+ exist on the box (points at `sync-to-box --apply` when missing).
107
+ 3. The base motion resolves to a file that exists on the box.
108
+ 4. The device probe: the one-line torch check mirroring the generator's
109
+ device expression under the same environment, printed before launching.
110
+
111
+ `--dry-run` runs all of the above (they are reads only), prints the exact
112
+ push / remote-command / pull / cleanup steps, and exits without connecting
113
+ for the generation step.
114
+
115
+ Every expansion is quoted; `set -euo pipefail` is on; prompts and paths are
116
+ shell-quoted with `printf %q` so they survive the remote shell. The remote
117
+ temp dir is created with `mktemp -d`, removed by an `EXIT` trap (cleanup
118
+ failure never masks the real result), and guarded to be absolute before it
119
+ can reach `rm -rf`. Re-running the same command with the same `--output`
120
+ overwrites it — no state accumulates anywhere.
121
+
122
+ ## Example
123
+
124
+ ```
125
+ tools/ardy/run-on-box.sh /tmp/pose-shooting.npz \
126
+ --base a-person-runs-forward-0722151659 \
127
+ --prompt "a person sprints forward and raises both arms" \
128
+ --duration 5 --dst-frame 60 --seed 7 \
129
+ --output tools/ardy/out/sprint-pose.npz
130
+ ```
131
+
132
+ The generator's stdout passes through (device line, loaded model, per-frame
133
+ constraint lines, final JSON result), and the pulled npz lands at
134
+ `--output` (default `tools/ardy/out/<pose>-constrained.npz`, gitignored).
135
+ The result is a full ARDY motion npz and can be used like any other
136
+ generated motion.