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.
- package/LICENSE +674 -0
- package/README.md +109 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/bin/cozyclay.mjs +194 -0
- package/dist/ardy/cskel27-rest.json +557 -0
- package/dist/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/dist/assets/basis_transcoder-o4Hde_L7.js +19 -0
- package/dist/assets/draco_decoder-C32yEggz.wasm +0 -0
- package/dist/assets/draco_decoder-Z1_iN-Ht.wasm +0 -0
- package/dist/assets/draco_decoder-fzg4nYZr.js +34 -0
- package/dist/assets/draco_wasm_wrapper-DxJM36Ib.js +117 -0
- package/dist/assets/draco_wasm_wrapper-fZCQGLGb.js +116 -0
- package/dist/assets/index-BW_S1YRy.js +4395 -0
- package/dist/assets/index-nRmmJgap.css +1 -0
- package/dist/demo/walk-then-stop.npz +0 -0
- package/dist/fonts/instrument-serif-italic-latin.woff2 +0 -0
- package/dist/fonts/instrument-serif-latin.woff2 +0 -0
- package/dist/fonts/inter-latin.woff2 +0 -0
- package/dist/index.html +13 -0
- package/dist/models/x-bot-tpose.fbx +0 -0
- package/dist/models/y-bot-tpose.fbx +0 -0
- package/package.json +80 -0
- package/src/App.jsx +3840 -0
- package/src/ardy/client.js +140 -0
- package/src/ardy/convert.js +313 -0
- package/src/ardy/cskel27-neutral.js +39 -0
- package/src/ardy/cskel27.js +68 -0
- package/src/ardy/export.js +157 -0
- package/src/ardy/ik.js +610 -0
- package/src/ardy/npz.js +520 -0
- package/src/ardy/playback.js +414 -0
- package/src/ardy/prompt-clips.js +16 -0
- package/src/ardy/timeline-coordinates.js +17 -0
- package/src/ardy/timeline-resize.js +11 -0
- package/src/ardy/timeline.jsx +723 -0
- package/src/ardy/to-cskel27.js +205 -0
- package/src/ardy/waypoints.js +364 -0
- package/src/camera-follow.js +366 -0
- package/src/camera-move.js +286 -0
- package/src/controls.jsx +245 -0
- package/src/dualview.jsx +305 -0
- package/src/hierarchy-model.js +77 -0
- package/src/hierarchy-panel.jsx +365 -0
- package/src/history.js +90 -0
- package/src/main.jsx +10 -0
- package/src/object-catalog.jsx +90 -0
- package/src/object-gizmo.jsx +755 -0
- package/src/planview.jsx +653 -0
- package/src/poses.js +424 -0
- package/src/posestudio.jsx +811 -0
- package/src/props.jsx +302 -0
- package/src/room.jsx +64 -0
- package/src/scene-history.js +125 -0
- package/src/scene-objects.js +420 -0
- package/src/shot-authoring.js +113 -0
- package/src/shot.js +239 -0
- package/src/styles.css +5639 -0
- package/src/ui.jsx +391 -0
- package/src/use-render-activity.js +96 -0
- package/tools/ardy/BRIDGE.md +255 -0
- package/tools/ardy/README.md +136 -0
- package/tools/ardy/__pycache__/cclay_sequence_generate.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +1427 -0
- package/tools/ardy/cclay_motion_edit.py +445 -0
- package/tools/ardy/cclay_sequence_generate.py +595 -0
- package/tools/ardy/dump-npz.py +205 -0
- package/tools/ardy/extract-rest.mjs +299 -0
- package/tools/ardy/npz.mjs +335 -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/ardy/pose-to-npz.mjs +106 -0
- package/tools/ardy/run-edit-on-box.sh +73 -0
- package/tools/ardy/run-on-box.sh +568 -0
- package/tools/ardy/run-sequence-on-box.sh +162 -0
- package/tools/ardy/visual-qa.mjs +188 -0
- package/tools/ardy/vq-car.mjs +52 -0
- package/tools/dev-full.mjs +29 -0
- package/tools/process-supervisor.mjs +63 -0
- package/tools/qa-browser.mjs +84 -0
- package/tools/qa-crop3.mjs +22 -0
- package/tools/qa-playview.mjs +49 -0
- package/tools/qa-screenshot.mjs +24 -0
- package/tools/qa-visual.mjs +51 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-on-box.sh - generate an ARDY clip on the remote box.
|
|
3
|
+
#
|
|
4
|
+
# run-on-box.sh [<pose.npz>] --base <motion-id|npz-path> --prompt "<prompt>" \
|
|
5
|
+
# --duration <seconds> [--dst-frame <N>] [--src-frame <N>] [--seed <S>] \
|
|
6
|
+
# [--root-2d FRAME X Z HEADING ...] [--output <local.npz>] [--dry-run]
|
|
7
|
+
|
|
8
|
+
#
|
|
9
|
+
# The pose npz is an ARDY motion npz (it must carry local_rot_mats and
|
|
10
|
+
# posed_joints; the frame at SRC_FRAME holds the pose). It is pushed to a
|
|
11
|
+
# fresh remote temp dir and handed to the generator as
|
|
12
|
+
# --pose-from <tmp>/pose.npz <src-frame> <dst-frame>
|
|
13
|
+
# which pins the FULL-BODY pose onto <dst-frame> of the new clip. The base
|
|
14
|
+
# motion supplies the unconstrained first pass (--base) and the generated
|
|
15
|
+
# npz is pulled back. Nothing is ever written inside the ARDY checkout: the
|
|
16
|
+
# base npz is read from ~/ardy/outputs/ (or outputs/omb/), the pose and the
|
|
17
|
+
# result live under the remote temp dir, and the temp dir is removed on
|
|
18
|
+
# exit.
|
|
19
|
+
#
|
|
20
|
+
# Three modes, chosen by what is given:
|
|
21
|
+
# pose <pose.npz> present (the default): the pose npz is pushed to a
|
|
22
|
+
# fresh remote temp dir and handed to the constrained generator
|
|
23
|
+
# as --pose-from <tmp>/pose.npz <src-frame> <dst-frame>, pinning
|
|
24
|
+
# the FULL-BODY pose onto <dst-frame> of the new clip. Requires
|
|
25
|
+
# --base and --dst-frame.
|
|
26
|
+
# waypoints no pose npz but one or more --root-2d groups: the constrained
|
|
27
|
+
# generator runs WITHOUT --pose-from, so the clip follows the
|
|
28
|
+
# path and prompt only, with no full-body pose constraint.
|
|
29
|
+
# --dst-frame is ignored. --base is optional: given, it is the
|
|
30
|
+
# unconstrained first pass; omitted, the script free-generates
|
|
31
|
+
# the base clip itself first (two-pass mode: scripts/generate.py
|
|
32
|
+
# with the same prompt/duration/seed under the remote temp dir,
|
|
33
|
+
# then the constrained pass with --base pointing at that npz).
|
|
34
|
+
# free no pose npz and no --root-2d: scripts/generate.py on the box,
|
|
35
|
+
# the unconstrained generator (the constrained script refuses a
|
|
36
|
+
# run with no target, pose or waypoint). --base and --dst-frame
|
|
37
|
+
# are ignored.
|
|
38
|
+
#
|
|
39
|
+
# Each --root-2d group pins the root on the X/Z ground plane at one 0-based
|
|
40
|
+
# clip frame (ARDY is Y-up; the horizontal plane is X and Z in meters, Y is
|
|
41
|
+
# not constrained). X and Z must be within -20..20, HEADING is a yaw in
|
|
42
|
+
# radians within -2π..2π or the literal 'none' to leave facing free. The
|
|
43
|
+
# request carries 2..32 sparse authored keys starting at frame 0. Intermediate
|
|
44
|
+
# root positions are left for the model to generate. Values are validated
|
|
45
|
+
# locally and %q-quoted into the remote command like all other args.
|
|
46
|
+
#
|
|
47
|
+
# CPU is forced with CUDA_VISIBLE_DEVICES="" and that is deliberate: the
|
|
48
|
+
# box's RTX 3070 has only 8.2 GB VRAM against 94 GB RAM, and nvidia-smi is
|
|
49
|
+
# broken there (NVML driver/library version mismatch) even though
|
|
50
|
+
# torch.cuda.is_available() still returns True - so without the override the
|
|
51
|
+
# generator's `device = "cuda:0" if torch.cuda.is_available() else "cpu"`
|
|
52
|
+
# line would pick a GPU that cannot hold the model. The preflight probe
|
|
53
|
+
# below mirrors that exact line under the same env and prints the device the
|
|
54
|
+
# generator will actually use.
|
|
55
|
+
#
|
|
56
|
+
# env (names shared with CozyClay scripts/ardy/sync-to-box):
|
|
57
|
+
# CCLAY_ARDY_HOST ssh destination for the ARDY host (required)
|
|
58
|
+
# CCLAY_ARDY_REPO ARDY checkout on the box (default $HOME/ardy)
|
|
59
|
+
# CCLAY_ARDY_VENV venv python on the box (default ~/ardy/.venv/bin/python)
|
|
60
|
+
set -euo pipefail
|
|
61
|
+
|
|
62
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
63
|
+
HOST="${CCLAY_ARDY_HOST:-}"
|
|
64
|
+
# REMOTE's default is escaped so the REMOTE shell expands $HOME (the same
|
|
65
|
+
# trick sync-to-box uses); VENV_PY's default keeps a literal ~ so the REMOTE
|
|
66
|
+
# shell tilde-expands it. Both resolve to the same ~/ardy on the box.
|
|
67
|
+
REMOTE="${CCLAY_ARDY_REPO:-\$HOME/ardy}"
|
|
68
|
+
# The box carries two venvs and they are NOT interchangeable: .venv runs the
|
|
69
|
+
# CPU text-encoder service, .venv-cuda is what the motion generator was actually
|
|
70
|
+
# run under (scripts/generate.py in generate_pc_monitor.log, 1060 MiB of VRAM).
|
|
71
|
+
VENV_PY="${CCLAY_ARDY_VENV:-~/ardy/.venv-cuda/bin/python}"
|
|
72
|
+
# Text encoder URL the generator falls back to when TEXT_ENCODER_URL is unset:
|
|
73
|
+
# ardy/model/registry.py:47 DEFAULT_TEXT_ENCODER_URL. Generation dies without it,
|
|
74
|
+
# so the preflight checks it rather than letting the run fail minutes in.
|
|
75
|
+
ENCODER_URL="${CCLAY_ARDY_ENCODER_URL:-http://127.0.0.1:9550/}"
|
|
76
|
+
# Force the motion model onto the CPU as well. Off by default: the encoder --
|
|
77
|
+
# the part that does not fit beside the model in 8.2 GB -- already runs on the
|
|
78
|
+
# CPU as its own service, and the model itself only needs about 1 GB.
|
|
79
|
+
FORCE_CPU=0
|
|
80
|
+
|
|
81
|
+
# BatchMode: never hang on a password prompt mid-pipeline. ConnectTimeout
|
|
82
|
+
# fails fast on a dead host; ServerAlive* drops a wedged connection instead
|
|
83
|
+
# of leaving the user staring at a silent ssh (30 s interval * 240 = 2 h).
|
|
84
|
+
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=240)
|
|
85
|
+
SCP_OPTS=(-o BatchMode=yes -o ConnectTimeout=10)
|
|
86
|
+
|
|
87
|
+
usage() {
|
|
88
|
+
cat >&2 <<'EOF'
|
|
89
|
+
usage: run-on-box.sh [<pose.npz>] --base <motion-id|npz-path> --prompt "<prompt>" \
|
|
90
|
+
--duration <seconds> [--dst-frame <N>] [--src-frame <N>] [--seed <S>] \
|
|
91
|
+
[--root-2d FRAME X Z HEADING ...] [--output <local.npz>] [--dry-run]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
<pose.npz> OPTIONAL local ARDY motion npz (local_rot_mats +
|
|
95
|
+
posed_joints) whose frame <src-frame> holds the CozyClay
|
|
96
|
+
pose to honour. Omit it to generate without a full-body pose
|
|
97
|
+
constraint: with --root-2d groups the clip is
|
|
98
|
+
path/prompt-only constrained (with --base, or two-pass: the
|
|
99
|
+
free generator produces the base clip first); without them
|
|
100
|
+
it is free generation (scripts/generate.py on the box).
|
|
101
|
+
--base base motion on the box: a bare id or bare <name>.npz
|
|
102
|
+
(looked up in ~/ardy/outputs/<name>.npz, then
|
|
103
|
+
~/ardy/outputs/omb/<name>.npz), or a path containing /
|
|
104
|
+
relative to the ARDY checkout / absolute. Required in pose
|
|
105
|
+
mode; optional in waypoints mode (omitted = two-pass: the
|
|
106
|
+
free generator first produces the base clip on the box);
|
|
107
|
+
ignored in free mode.
|
|
108
|
+
--prompt generation prompt for the constrained clip
|
|
109
|
+
--duration clip duration in seconds (20 fps, 0.15..1200)
|
|
110
|
+
--dst-frame clip frame to pin the pose onto, 0 <= dst < duration*20
|
|
111
|
+
(pose mode only)
|
|
112
|
+
--src-frame frame of <pose.npz> to copy (default 0; pose mode only)
|
|
113
|
+
--seed optional seed for reproducible results
|
|
114
|
+
--output local destination for the generated npz
|
|
115
|
+
(default tools/ardy/out/<pose>-constrained.npz, or
|
|
116
|
+
tools/ardy/out/<epoch>-generated.npz without a pose)
|
|
117
|
+
|
|
118
|
+
--dry-run run the preflight checks, print the exact remote command,
|
|
119
|
+
and exit without pushing, generating, or pulling
|
|
120
|
+
--root-2d repeatable root waypoint: FRAME (0-based, inside the clip),
|
|
121
|
+
X and Z in meters within -20..20 (ARDY is Y-up; the ground
|
|
122
|
+
plane is X/Z, Y is not constrained), HEADING a yaw in
|
|
123
|
+
radians within -2π..2π or the literal 'none' to leave
|
|
124
|
+
facing free. Supply 2..32 sparse keys beginning at frame 0;
|
|
125
|
+
ARDY generates all intermediate root motion.
|
|
126
|
+
|
|
127
|
+
env:
|
|
128
|
+
CCLAY_ARDY_HOST ssh destination for the ARDY host (required)
|
|
129
|
+
CCLAY_ARDY_REPO ARDY checkout on the box (default $HOME/ardy)
|
|
130
|
+
--cpu also run the motion model on the CPU (slower; the text encoder
|
|
131
|
+
is on the CPU either way, as its own service)
|
|
132
|
+
|
|
133
|
+
env:
|
|
134
|
+
CCLAY_ARDY_VENV venv python on the box (default ~/ardy/.venv-cuda/bin/python)
|
|
135
|
+
CCLAY_ARDY_ENCODER_URL text encoder service (default http://127.0.0.1:9550/)
|
|
136
|
+
EOF
|
|
137
|
+
exit 2
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
POSE_NPZ="" # legacy positional pose; normalized into POSE_ARGS after parsing
|
|
141
|
+
POSE_ARGS=() # repeatable triples: LOCAL_NPZ SRC_FRAME DST_FRAME
|
|
142
|
+
BASE=""
|
|
143
|
+
PROMPT=""
|
|
144
|
+
DURATION=""
|
|
145
|
+
DST_FRAME=""
|
|
146
|
+
SRC_FRAME=0
|
|
147
|
+
SEED=""
|
|
148
|
+
OUTPUT=""
|
|
149
|
+
DRY_RUN=0
|
|
150
|
+
ROOT_2D_ARGS=()
|
|
151
|
+
|
|
152
|
+
while [[ $# -gt 0 ]]; do
|
|
153
|
+
case "$1" in
|
|
154
|
+
--base)
|
|
155
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --base needs a motion id or npz path" >&2; usage; }
|
|
156
|
+
BASE="$2"; shift 2 ;;
|
|
157
|
+
--prompt)
|
|
158
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --prompt needs text" >&2; usage; }
|
|
159
|
+
PROMPT="$2"; shift 2 ;;
|
|
160
|
+
--duration)
|
|
161
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --duration needs seconds" >&2; usage; }
|
|
162
|
+
[[ "$2" =~ ^[0-9]+([.][0-9]+)?$ ]] || { echo "run-on-box: --duration must be a number, got '$2'" >&2; usage; }
|
|
163
|
+
DURATION="$2"; shift 2 ;;
|
|
164
|
+
--dst-frame)
|
|
165
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --dst-frame needs a frame number" >&2; usage; }
|
|
166
|
+
[[ "$2" =~ ^[0-9]+$ ]] || { echo "run-on-box: --dst-frame must be a non-negative integer, got '$2'" >&2; usage; }
|
|
167
|
+
DST_FRAME="$2"; shift 2 ;;
|
|
168
|
+
--src-frame)
|
|
169
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --src-frame needs a frame number" >&2; usage; }
|
|
170
|
+
[[ "$2" =~ ^[0-9]+$ ]] || { echo "run-on-box: --src-frame must be a non-negative integer, got '$2'" >&2; usage; }
|
|
171
|
+
SRC_FRAME="$2"; shift 2 ;;
|
|
172
|
+
--seed)
|
|
173
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --seed needs an integer" >&2; usage; }
|
|
174
|
+
[[ "$2" =~ ^[0-9]+$ ]] || { echo "run-on-box: --seed must be a non-negative integer, got '$2'" >&2; usage; }
|
|
175
|
+
SEED="$2"; shift 2 ;;
|
|
176
|
+
--pose-from)
|
|
177
|
+
[[ $# -ge 4 ]] || { echo "run-on-box: --pose-from needs LOCAL_NPZ SRC_FRAME DST_FRAME" >&2; usage; }
|
|
178
|
+
[[ "$3" =~ ^[0-9]+$ && "$4" =~ ^[0-9]+$ ]] || { echo "run-on-box: --pose-from frames must be non-negative integers" >&2; usage; }
|
|
179
|
+
POSE_ARGS+=("$2" "$3" "$4"); shift 4 ;;
|
|
180
|
+
--root-2d)
|
|
181
|
+
[[ $# -ge 5 ]] || { echo "run-on-box: --root-2d needs FRAME X Z HEADING" >&2; usage; }
|
|
182
|
+
ROOT_2D_ARGS+=("$2" "$3" "$4" "$5"); shift 5 ;;
|
|
183
|
+
--cpu)
|
|
184
|
+
FORCE_CPU=1; shift ;;
|
|
185
|
+
--output)
|
|
186
|
+
[[ $# -ge 2 ]] || { echo "run-on-box: --output needs a path" >&2; usage; }
|
|
187
|
+
OUTPUT="$2"; shift 2 ;;
|
|
188
|
+
--dry-run)
|
|
189
|
+
DRY_RUN=1; shift ;;
|
|
190
|
+
-h|--help)
|
|
191
|
+
usage ;;
|
|
192
|
+
-*)
|
|
193
|
+
echo "run-on-box: unknown option '$1'" >&2; usage ;;
|
|
194
|
+
*)
|
|
195
|
+
[[ -z "$POSE_NPZ" ]] || { echo "run-on-box: one pose npz only, got '$1'" >&2; usage; }
|
|
196
|
+
POSE_NPZ="$1"; shift ;;
|
|
197
|
+
esac
|
|
198
|
+
done
|
|
199
|
+
|
|
200
|
+
[[ -n "$HOST" ]] || {
|
|
201
|
+
echo "run-on-box: CCLAY_ARDY_HOST is required (for example: user@ardy-host)" >&2
|
|
202
|
+
exit 2
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Normalize the legacy positional pose into the repeatable pose list.
|
|
206
|
+
if [[ -n "$POSE_NPZ" ]]; then
|
|
207
|
+
[[ -n "$DST_FRAME" ]] || usage
|
|
208
|
+
POSE_ARGS+=("$POSE_NPZ" "$SRC_FRAME" "$DST_FRAME")
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
# Mode by what is given: one or more pose npz files pin the full body,
|
|
212
|
+
# waypoints alone constrain the path, and neither is free generation.
|
|
213
|
+
MODE=""
|
|
214
|
+
if [[ ${#POSE_ARGS[@]} -gt 0 ]]; then
|
|
215
|
+
MODE="pose"
|
|
216
|
+
elif [[ ${#ROOT_2D_ARGS[@]} -gt 0 ]]; then
|
|
217
|
+
MODE="waypoints"
|
|
218
|
+
else
|
|
219
|
+
MODE="free"
|
|
220
|
+
fi
|
|
221
|
+
# Waypoints run the constrained generator: ONE model sampling call for the
|
|
222
|
+
# whole clip. ARDY's trained window is 10 s — frames beyond it sit outside
|
|
223
|
+
# the model's temporal horizon and the motion visibly degrades, so the last
|
|
224
|
+
# line of defence refuses what every upstream layer should already have.
|
|
225
|
+
if [[ "$MODE" == "waypoints" ]] && ! awk -v d="$DURATION" 'BEGIN { exit !(d <= 10) }'; then
|
|
226
|
+
echo "run-on-box: --root-2d requires --duration <= 10 s (one-shot constrained generation; ARDY trained window), got ${DURATION}s" >&2
|
|
227
|
+
exit 1
|
|
228
|
+
fi
|
|
229
|
+
# Waypoints without --base is two-pass: the free generator first produces
|
|
230
|
+
# the base clip on the box (pass 1), then the constrained pass uses it.
|
|
231
|
+
TWO_PASS=0
|
|
232
|
+
[[ "$MODE" == "waypoints" && -z "$BASE" ]] && TWO_PASS=1
|
|
233
|
+
IMPLICIT_POSE_BASE=0
|
|
234
|
+
|
|
235
|
+
[[ -n "$PROMPT" && -n "$DURATION" ]] || usage
|
|
236
|
+
if [[ "$MODE" == "pose" && -z "$BASE" ]]; then
|
|
237
|
+
# Internal parser/reference baseline only. Full-body poses are the actual
|
|
238
|
+
# constraints; users never select a semantic base motion.
|
|
239
|
+
BASE="${CCLAY_ARDY_REFERENCE_BASE:-stand-upright-facing-forward-bring-both--0722140757}"
|
|
240
|
+
IMPLICIT_POSE_BASE=1
|
|
241
|
+
fi
|
|
242
|
+
# --- local validation, mirroring the CozyClay wrapper's bounds -------------
|
|
243
|
+
if [[ "$MODE" == "pose" ]]; then
|
|
244
|
+
for ((i = 0; i < ${#POSE_ARGS[@]}; i += 3)); do
|
|
245
|
+
pose_path="${POSE_ARGS[$i]}"
|
|
246
|
+
[[ -f "$pose_path" ]] || { echo "run-on-box: pose npz not found: $pose_path" >&2; exit 1; }
|
|
247
|
+
done
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
[[ -n "$PROMPT" ]] || { echo "run-on-box: --prompt must not be empty" >&2; exit 1; }
|
|
251
|
+
# The clip is int(duration * 20) frames at ARDY's 20 fps, capped at 1200 s
|
|
252
|
+
# like the wrapper; the remote rejects anything below 3 frames because
|
|
253
|
+
# inter-frame continuity is undefined there.
|
|
254
|
+
awk -v d="$DURATION" 'BEGIN { exit !(d > 0 && d <= 1200) }' || {
|
|
255
|
+
echo "run-on-box: --duration must be > 0 and <= 1200 seconds, got '$DURATION'" >&2; exit 1; }
|
|
256
|
+
CLIP_FRAMES="$(awk -v d="$DURATION" 'BEGIN { printf "%d", d * 20 }')"
|
|
257
|
+
[[ "$CLIP_FRAMES" -ge 3 ]] || {
|
|
258
|
+
echo "run-on-box: --duration ${DURATION}s yields ${CLIP_FRAMES} frame(s) at 20 fps; a clip needs at least 3" >&2; exit 1; }
|
|
259
|
+
if [[ "$MODE" == "pose" ]]; then
|
|
260
|
+
for ((i = 0; i < ${#POSE_ARGS[@]}; i += 3)); do
|
|
261
|
+
dst="${POSE_ARGS[$((i + 2))]}"
|
|
262
|
+
awk -v f="$dst" -v m="$CLIP_FRAMES" 'BEGIN { exit !(f < m) }' || {
|
|
263
|
+
echo "run-on-box: --pose-from destination $dst is outside 0..$((CLIP_FRAMES - 1)) (duration ${DURATION}s at 20 fps)" >&2; exit 1; }
|
|
264
|
+
done
|
|
265
|
+
fi
|
|
266
|
+
# --- --root-2d validation: each group, then the set as a whole -------------
|
|
267
|
+
# Mirrors the bridge contract: 2..32 sparse keys beginning at frame 0,
|
|
268
|
+
# strictly ascending, X/Z in meters within -20..20, HEADING a finite number
|
|
269
|
+
# of radians within -2π..2π or the literal 'none'.
|
|
270
|
+
FLOAT_RE='^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?$'
|
|
271
|
+
validate_root_2d() {
|
|
272
|
+
local n="${#ROOT_2D_ARGS[@]}" i frame x z heading prev=-1 count
|
|
273
|
+
[[ $((n % 4)) -eq 0 ]] || { echo "run-on-box: internal error: --root-2d group list is misaligned" >&2; exit 1; }
|
|
274
|
+
count=$((n / 4))
|
|
275
|
+
if [[ "$count" -ne 0 && ( "$count" -lt 2 || "$count" -gt 32 ) ]]; then
|
|
276
|
+
echo "run-on-box: --root-2d needs 2..32 sparse waypoints, got $count" >&2
|
|
277
|
+
exit 1
|
|
278
|
+
fi
|
|
279
|
+
for ((i = 0; i < n; i += 4)); do
|
|
280
|
+
frame="${ROOT_2D_ARGS[$i]}"
|
|
281
|
+
x="${ROOT_2D_ARGS[$((i + 1))]}"
|
|
282
|
+
z="${ROOT_2D_ARGS[$((i + 2))]}"
|
|
283
|
+
heading="${ROOT_2D_ARGS[$((i + 3))]}"
|
|
284
|
+
[[ "$frame" =~ ^[0-9]+$ ]] || { echo "run-on-box: --root-2d FRAME must be a non-negative integer, got '$frame'" >&2; exit 1; }
|
|
285
|
+
awk -v f="$frame" -v m="$CLIP_FRAMES" 'BEGIN { exit !(f >= 0 && f < m) }' || {
|
|
286
|
+
echo "run-on-box: --root-2d frame $frame is outside 0..$((CLIP_FRAMES - 1)) (duration ${DURATION}s at 20 fps)" >&2
|
|
287
|
+
exit 1
|
|
288
|
+
}
|
|
289
|
+
awk -v f="$frame" -v p="$prev" 'BEGIN { exit !(f > p) }' || {
|
|
290
|
+
echo "run-on-box: --root-2d frames must be strictly ascending; frame $frame repeats or precedes frame $prev" >&2
|
|
291
|
+
exit 1
|
|
292
|
+
}
|
|
293
|
+
if [[ $i -eq 0 && "$frame" -ne 0 ]]; then
|
|
294
|
+
echo "run-on-box: --root-2d start must be at frame 0, got frame $frame" >&2
|
|
295
|
+
exit 1
|
|
296
|
+
fi
|
|
297
|
+
prev="$frame"
|
|
298
|
+
[[ "$x" =~ $FLOAT_RE ]] || { echo "run-on-box: --root-2d X must be a number, got '$x'" >&2; exit 1; }
|
|
299
|
+
awk -v v="$x" 'BEGIN { exit !(v >= -20 && v <= 20) }' || {
|
|
300
|
+
echo "run-on-box: --root-2d X $x is outside -20..20 meters" >&2
|
|
301
|
+
exit 1
|
|
302
|
+
}
|
|
303
|
+
[[ "$z" =~ $FLOAT_RE ]] || { echo "run-on-box: --root-2d Z must be a number, got '$z'" >&2; exit 1; }
|
|
304
|
+
awk -v v="$z" 'BEGIN { exit !(v >= -20 && v <= 20) }' || {
|
|
305
|
+
echo "run-on-box: --root-2d Z $z is outside -20..20 meters" >&2
|
|
306
|
+
exit 1
|
|
307
|
+
}
|
|
308
|
+
if [[ "$heading" != "none" ]]; then
|
|
309
|
+
[[ "$heading" =~ $FLOAT_RE ]] || {
|
|
310
|
+
echo "run-on-box: --root-2d HEADING must be a number of radians or the literal 'none', got '$heading'" >&2
|
|
311
|
+
exit 1
|
|
312
|
+
}
|
|
313
|
+
awk -v h="$heading" 'BEGIN { pi = atan2(0, -1); exit !(h >= -2*pi && h <= 2*pi) }' || {
|
|
314
|
+
echo "run-on-box: --root-2d HEADING $heading rad is outside -2π..2π" >&2
|
|
315
|
+
exit 1
|
|
316
|
+
}
|
|
317
|
+
fi
|
|
318
|
+
done
|
|
319
|
+
}
|
|
320
|
+
validate_root_2d
|
|
321
|
+
# SRC_FRAME indexes the pose npz; its real bound is only knowable on the
|
|
322
|
+
# box, so like the reference wrapper we accept any non-negative integer here
|
|
323
|
+
# and let the remote range-check it against the npz's frame count.
|
|
324
|
+
if [[ "$MODE" == "pose" ]]; then
|
|
325
|
+
FIRST_POSE="${POSE_ARGS[0]}"
|
|
326
|
+
OUTPUT="${OUTPUT:-$HERE/out/$(basename "${FIRST_POSE%.npz}")-constrained.npz}"
|
|
327
|
+
else
|
|
328
|
+
OUTPUT="${OUTPUT:-$HERE/out/$(date +%s)-generated.npz}"
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# --- preflight: fail fast, cheapest first ---------------------------------
|
|
333
|
+
if ! ssh "${SSH_OPTS[@]}" "$HOST" ":" </dev/null; then
|
|
334
|
+
echo "run-on-box: cannot ssh to ${HOST} (BatchMode). Check network reachability and the SSH key agent." >&2
|
|
335
|
+
exit 1
|
|
336
|
+
fi
|
|
337
|
+
echo "run-on-box: ssh to ${HOST} ok"
|
|
338
|
+
|
|
339
|
+
# Free generation runs scripts/generate.py (the constrained script refuses a
|
|
340
|
+
# run with no target, pose or waypoint); every other mode runs the
|
|
341
|
+
# constrained generator. Two-pass waypoints mode needs BOTH scripts:
|
|
342
|
+
# generate.py for the free base pass, then the constrained script.
|
|
343
|
+
GEN_SCRIPT="scripts/cclay_constrained_generate.py"
|
|
344
|
+
[[ "$MODE" == "free" ]] && GEN_SCRIPT="scripts/generate.py"
|
|
345
|
+
GEN_CHECK="test -x ${VENV_PY} && test -f ${REMOTE}/${GEN_SCRIPT}"
|
|
346
|
+
GEN_LABEL="${REMOTE}/${GEN_SCRIPT}"
|
|
347
|
+
if [[ "$TWO_PASS" -eq 1 || "$IMPLICIT_POSE_BASE" -eq 1 ]]; then
|
|
348
|
+
GEN_CHECK="test -x ${VENV_PY} && test -f ${REMOTE}/scripts/generate.py && test -f ${REMOTE}/scripts/cclay_constrained_generate.py"
|
|
349
|
+
GEN_LABEL="${REMOTE}/scripts/generate.py and ${REMOTE}/scripts/cclay_constrained_generate.py"
|
|
350
|
+
fi
|
|
351
|
+
# shellcheck disable=SC2029 # REMOTE/VENV_PY expand remotely by design
|
|
352
|
+
if ! ssh "${SSH_OPTS[@]}" "$HOST" "$GEN_CHECK" </dev/null; then
|
|
353
|
+
echo "run-on-box: ${HOST} is missing ${VENV_PY} or ${GEN_LABEL}; sync the ARDY sources to the box first (CozyClay scripts/ardy/sync-to-box --apply)" >&2
|
|
354
|
+
exit 1
|
|
355
|
+
fi
|
|
356
|
+
echo "run-on-box: venv python and generator script present on ${HOST}"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# Resolve --base on the box: anything containing a slash is a repo-relative
|
|
360
|
+
# or absolute path and passes through; a bare id or bare <name>.npz is
|
|
361
|
+
# looked up under outputs/ then outputs/omb/ (repo-relative, i.e.
|
|
362
|
+
# ~/ardy/outputs*). Free mode takes no base, and waypoints mode without one
|
|
363
|
+
# is two-pass: the pass-1 output is a fresh absolute temp path verified with
|
|
364
|
+
# a plain test -f at the pass-1 step, never through this candidate lookup.
|
|
365
|
+
if [[ -n "$BASE" ]]; then
|
|
366
|
+
if [[ "$BASE" == */* ]]; then
|
|
367
|
+
BASE_CANDIDATES="$(printf '%q' "$BASE")"
|
|
368
|
+
BASE_LOOKED="'${BASE}'"
|
|
369
|
+
else
|
|
370
|
+
BASE_NAME="${BASE%.npz}"
|
|
371
|
+
BASE_CANDIDATES="$(printf '%q' "outputs/${BASE_NAME}.npz") $(printf '%q' "outputs/omb/${BASE_NAME}.npz")"
|
|
372
|
+
BASE_LOOKED="outputs/${BASE_NAME}.npz or outputs/omb/${BASE_NAME}.npz under ${REMOTE}"
|
|
373
|
+
fi
|
|
374
|
+
# shellcheck disable=SC2029 # REMOTE expands remotely by design
|
|
375
|
+
if ! BASE_RESOLVED="$(ssh "${SSH_OPTS[@]}" "$HOST" "cd ${REMOTE} && for c in ${BASE_CANDIDATES}; do if test -f \"\$c\"; then printf '%s' \"\$c\"; exit 0; fi; done; exit 1" </dev/null)"; then
|
|
376
|
+
echo "run-on-box: base motion '${BASE}' does not exist on ${HOST} (looked for ${BASE_LOOKED})" >&2
|
|
377
|
+
exit 1
|
|
378
|
+
fi
|
|
379
|
+
echo "run-on-box: base motion on ${HOST}: ${BASE_RESOLVED}"
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# The hidden upright reference is only a convenient baseline when it is long
|
|
383
|
+
# enough. A loaded clip can be longer than that fixture; in that case generate
|
|
384
|
+
# an exact-duration base first instead of letting cclay_constrained_generate.py
|
|
385
|
+
# fail after model load with "base has N frames but requested M".
|
|
386
|
+
if [[ "$IMPLICIT_POSE_BASE" -eq 1 ]]; then
|
|
387
|
+
# shellcheck disable=SC2029 # resolved path is selected from the box listing
|
|
388
|
+
if ! BASE_FRAMES="$(ssh "${SSH_OPTS[@]}" "$HOST" \
|
|
389
|
+
"cd ${REMOTE} && ${VENV_PY} -c 'import numpy as np,sys; print(np.load(sys.argv[1])[\"local_rot_mats\"].shape[0])' $(printf '%q' "$BASE_RESOLVED")" </dev/null)"; then
|
|
390
|
+
echo "run-on-box: could not inspect implicit pose base ${BASE_RESOLVED}" >&2
|
|
391
|
+
exit 1
|
|
392
|
+
fi
|
|
393
|
+
[[ "$BASE_FRAMES" =~ ^[0-9]+$ ]] || {
|
|
394
|
+
echo "run-on-box: implicit pose base returned invalid frame count '${BASE_FRAMES}'" >&2
|
|
395
|
+
exit 1
|
|
396
|
+
}
|
|
397
|
+
if [[ "$BASE_FRAMES" -lt "$CLIP_FRAMES" ]]; then
|
|
398
|
+
echo "run-on-box: implicit pose base has ${BASE_FRAMES} frames; will free-generate ${CLIP_FRAMES}-frame base first"
|
|
399
|
+
BASE_RESOLVED=""
|
|
400
|
+
TWO_PASS=1
|
|
401
|
+
fi
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# Probe the device the generator will use, with the SAME env it will run
|
|
406
|
+
# under (CUDA_VISIBLE_DEVICES="" included). The one-liner mirrors the
|
|
407
|
+
# generator's `device = "cuda:0" if torch.cuda.is_available() else "cpu"`
|
|
408
|
+
# exactly, so what is printed is what the generation will pick.
|
|
409
|
+
# shellcheck disable=SC2029 # REMOTE/VENV_PY expand remotely by design
|
|
410
|
+
CUDA_ENV=""
|
|
411
|
+
[[ "$FORCE_CPU" -eq 1 ]] && CUDA_ENV='CUDA_VISIBLE_DEVICES="" '
|
|
412
|
+
|
|
413
|
+
# The text encoder is a separate CPU service; without it the generator loads the
|
|
414
|
+
# model, then dies. Check it before spending minutes on the rest.
|
|
415
|
+
if ! ENCODER_CODE="$(ssh "${SSH_OPTS[@]}" "$HOST" "curl -s -o /dev/null -w '%{http_code}' -m 10 ${ENCODER_URL}" </dev/null)"; then
|
|
416
|
+
echo "run-on-box: could not reach ${HOST} to probe the text encoder" >&2
|
|
417
|
+
exit 1
|
|
418
|
+
fi
|
|
419
|
+
if [[ "$ENCODER_CODE" != "200" ]]; then
|
|
420
|
+
echo "run-on-box: text encoder at ${ENCODER_URL} answered '${ENCODER_CODE}', not 200." >&2
|
|
421
|
+
echo "run-on-box: start it with: ssh ${HOST} 'cd ~/ardy && .venv/bin/python -u scripts/run_text_encoder_server.py --host 127.0.0.1 --port 9550 --device cpu'" >&2
|
|
422
|
+
exit 1
|
|
423
|
+
fi
|
|
424
|
+
echo "run-on-box: text encoder ${ENCODER_URL} responding (cpu service)"
|
|
425
|
+
|
|
426
|
+
if ! DEVICE="$(ssh "${SSH_OPTS[@]}" "$HOST" "cd ${REMOTE} && ${CUDA_ENV}${VENV_PY} -c 'import torch; print(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")'" </dev/null)"; then
|
|
427
|
+
echo "run-on-box: device probe on ${HOST} failed (venv python erroring?)" >&2
|
|
428
|
+
exit 1
|
|
429
|
+
fi
|
|
430
|
+
[[ -n "$DEVICE" ]] || { echo "run-on-box: device probe on ${HOST} returned nothing" >&2; exit 1; }
|
|
431
|
+
echo "run-on-box: generator will use device '${DEVICE}' on ${HOST}"
|
|
432
|
+
|
|
433
|
+
# --- the generator invocation ---------------------------------------------
|
|
434
|
+
# Args are shell-quoted with printf %q so prompts and paths survive the
|
|
435
|
+
# remote shell. The output base is absolute (under the remote temp dir), so
|
|
436
|
+
# the generator's _resolve_output_base honors it instead of placing it under
|
|
437
|
+
# outputs/.
|
|
438
|
+
# scripts/generate.py is the unconstrained generator: its prompt is
|
|
439
|
+
# positional and it has no --base / --pose-from / --root-2d. Used for free
|
|
440
|
+
# mode and for the two-pass base pass (pass 1); the npz lands at
|
|
441
|
+
# <out_base>.npz.
|
|
442
|
+
build_free_cmd() {
|
|
443
|
+
local out_base="$1" cmd
|
|
444
|
+
cmd="cd ${REMOTE} && ${CUDA_ENV}${VENV_PY} scripts/generate.py $(printf '%q' "$PROMPT")"
|
|
445
|
+
cmd+=" --duration $(printf '%q' "$DURATION")"
|
|
446
|
+
cmd+=" --output $(printf '%q' "$out_base")"
|
|
447
|
+
if [[ -n "$SEED" ]]; then
|
|
448
|
+
cmd+=" --seed $(printf '%q' "$SEED")"
|
|
449
|
+
fi
|
|
450
|
+
printf '%s' "$cmd"
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
build_remote_cmd() {
|
|
454
|
+
local tmp_dir="$1" cmd i
|
|
455
|
+
# Two-pass waypoints mode has no user base: BASE_RESOLVED stays empty and
|
|
456
|
+
# pass 1's output npz (under the same remote temp dir, cleaned by the EXIT
|
|
457
|
+
# trap) becomes the --base of the constrained pass.
|
|
458
|
+
local base_arg="${BASE_RESOLVED:-${tmp_dir}/base.npz}"
|
|
459
|
+
if [[ "$MODE" == "free" ]]; then
|
|
460
|
+
cmd="$(build_free_cmd "${tmp_dir}/out")"
|
|
461
|
+
else
|
|
462
|
+
cmd="cd ${REMOTE} && ${CUDA_ENV}${VENV_PY} scripts/cclay_constrained_generate.py"
|
|
463
|
+
cmd+=" --prompt $(printf '%q' "$PROMPT")"
|
|
464
|
+
cmd+=" --duration $(printf '%q' "$DURATION")"
|
|
465
|
+
cmd+=" --base $(printf '%q' "$base_arg")"
|
|
466
|
+
if [[ "$MODE" == "pose" ]]; then
|
|
467
|
+
for ((i = 0; i < ${#POSE_ARGS[@]}; i += 3)); do
|
|
468
|
+
pose_index=$((i / 3))
|
|
469
|
+
cmd+=" --pose-from $(printf '%q' "${tmp_dir}/pose-${pose_index}.npz")"
|
|
470
|
+
cmd+=" $(printf '%q' "${POSE_ARGS[$((i + 1))]}") $(printf '%q' "${POSE_ARGS[$((i + 2))]}")"
|
|
471
|
+
done
|
|
472
|
+
fi
|
|
473
|
+
# Each validated --root-2d group is %q-quoted value by value, so no
|
|
474
|
+
# waypoint text can ever reach the remote shell unquoted.
|
|
475
|
+
for ((i = 0; i < ${#ROOT_2D_ARGS[@]}; i += 4)); do
|
|
476
|
+
cmd+=" --root-2d $(printf '%q' "${ROOT_2D_ARGS[$i]}")"
|
|
477
|
+
cmd+=" $(printf '%q' "${ROOT_2D_ARGS[$((i + 1))]}")"
|
|
478
|
+
cmd+=" $(printf '%q' "${ROOT_2D_ARGS[$((i + 2))]}")"
|
|
479
|
+
cmd+=" $(printf '%q' "${ROOT_2D_ARGS[$((i + 3))]}")"
|
|
480
|
+
done
|
|
481
|
+
cmd+=" --output $(printf '%q' "${tmp_dir}/out")"
|
|
482
|
+
if [[ -n "$SEED" ]]; then
|
|
483
|
+
cmd+=" --seed $(printf '%q' "$SEED")"
|
|
484
|
+
fi
|
|
485
|
+
fi
|
|
486
|
+
printf '%s' "$cmd"
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
if [[ $DRY_RUN -eq 1 ]]; then
|
|
491
|
+
REMOTE_TMP_PLACEHOLDER="<mktemp-dir-on-box>"
|
|
492
|
+
MODE_LABEL="free generation (generate.py)"
|
|
493
|
+
[[ "$MODE" == "pose" ]] && MODE_LABEL="full-body pose pin"
|
|
494
|
+
[[ "$MODE" == "waypoints" ]] && MODE_LABEL="frame-0 root start + prompt (no pose)"
|
|
495
|
+
[[ "$MODE" == "waypoints" && "$TWO_PASS" -eq 1 ]] && MODE_LABEL="frame-0 root start + prompt (no pose) - two-pass (free base first)"
|
|
496
|
+
echo "run-on-box: DRY RUN - preflight done, nothing pushed, nothing generated, nothing pulled."
|
|
497
|
+
echo "run-on-box: mode ${MODE} - ${MODE_LABEL}"
|
|
498
|
+
[[ "$MODE" == "pose" ]] || echo "run-on-box: pose npz <none>"
|
|
499
|
+
[[ "$MODE" != "pose" ]] || echo "run-on-box: pose pins $(( ${#POSE_ARGS[@]} / 3 ))"
|
|
500
|
+
if [[ "$TWO_PASS" -eq 1 ]]; then
|
|
501
|
+
echo "run-on-box: base motion pass-1 output ${REMOTE_TMP_PLACEHOLDER}/base.npz on ${HOST} (free-generated first)"
|
|
502
|
+
else
|
|
503
|
+
[[ -z "${BASE_RESOLVED:-}" ]] || echo "run-on-box: base motion ${BASE_RESOLVED} on ${HOST}"
|
|
504
|
+
fi
|
|
505
|
+
echo "run-on-box: device ${DEVICE}"
|
|
506
|
+
echo "run-on-box: root-2d $(( ${#ROOT_2D_ARGS[@]} / 4 )) waypoint(s)"
|
|
507
|
+
[[ "$MODE" != "pose" ]] || echo "run-on-box: would push $(( ${#POSE_ARGS[@]} / 3 )) pose npz file(s) to ${HOST}:${REMOTE_TMP_PLACEHOLDER}"
|
|
508
|
+
if [[ "$TWO_PASS" -eq 1 ]]; then
|
|
509
|
+
echo "run-on-box: pass 1/2 would run ssh ${HOST} \"$(build_free_cmd "${REMOTE_TMP_PLACEHOLDER}/base")\""
|
|
510
|
+
echo "run-on-box: pass 2/2 would run ssh ${HOST} \"$(build_remote_cmd "$REMOTE_TMP_PLACEHOLDER")\""
|
|
511
|
+
else
|
|
512
|
+
echo "run-on-box: would run ssh ${HOST} \"$(build_remote_cmd "$REMOTE_TMP_PLACEHOLDER")\""
|
|
513
|
+
fi
|
|
514
|
+
echo "run-on-box: would pull scp -q -o BatchMode=yes ${HOST}:${REMOTE_TMP_PLACEHOLDER}/out.npz ${OUTPUT}"
|
|
515
|
+
echo "run-on-box: would clean ssh ${HOST} \"rm -rf ${REMOTE_TMP_PLACEHOLDER}\""
|
|
516
|
+
exit 0
|
|
517
|
+
fi
|
|
518
|
+
|
|
519
|
+
mkdir -p "$(dirname "$OUTPUT")"
|
|
520
|
+
|
|
521
|
+
# Fresh temp dir on the box; the EXIT trap removes it even on failure, and
|
|
522
|
+
# cleanup failure never masks the real result. Guard the path (must be
|
|
523
|
+
# absolute, as mktemp guarantees) before it can reach the cleanup rm -rf.
|
|
524
|
+
if ! REMOTE_TMP="$(ssh "${SSH_OPTS[@]}" "$HOST" "mktemp -d" </dev/null)"; then
|
|
525
|
+
echo "run-on-box: could not create a temp dir on ${HOST} (mktemp failed)" >&2
|
|
526
|
+
exit 1
|
|
527
|
+
fi
|
|
528
|
+
[[ "$REMOTE_TMP" == /* ]] || { echo "run-on-box: remote mktemp returned a non-absolute path '${REMOTE_TMP}'; aborting" >&2; exit 1; }
|
|
529
|
+
trap 'ssh "${SSH_OPTS[@]}" "$HOST" "rm -rf ${REMOTE_TMP}" </dev/null >/dev/null 2>&1 || true' EXIT
|
|
530
|
+
|
|
531
|
+
if [[ "$TWO_PASS" -eq 1 ]]; then
|
|
532
|
+
# Pass 1: free-generate the base clip with the same prompt/duration/seed
|
|
533
|
+
# the constrained pass will use. Its npz lives under REMOTE_TMP, so the
|
|
534
|
+
# EXIT trap removes it with everything else.
|
|
535
|
+
PASS1_CMD="$(build_free_cmd "${REMOTE_TMP}/base")"
|
|
536
|
+
echo "run-on-box: pass 1/2: free-generating base clip on ${HOST} ..."
|
|
537
|
+
# shellcheck disable=SC2029 # PASS1_CMD is a remote command by design
|
|
538
|
+
ssh "${SSH_OPTS[@]}" "$HOST" "$PASS1_CMD" </dev/null
|
|
539
|
+
BASE_RESOLVED="${REMOTE_TMP}/base.npz"
|
|
540
|
+
# The pass-1 output is a fresh absolute temp path, not a listed base: no
|
|
541
|
+
# BASE_CANDIDATES lookup, just verify the file appeared.
|
|
542
|
+
# shellcheck disable=SC2029 # REMOTE_TMP expands remotely by design
|
|
543
|
+
if ! ssh "${SSH_OPTS[@]}" "$HOST" "test -f ${BASE_RESOLVED}" </dev/null; then
|
|
544
|
+
echo "run-on-box: pass 1/2 failed: ${BASE_RESOLVED} was not produced on ${HOST}" >&2
|
|
545
|
+
exit 1
|
|
546
|
+
fi
|
|
547
|
+
echo "run-on-box: pass 1/2: base clip at ${HOST}:${BASE_RESOLVED}"
|
|
548
|
+
fi
|
|
549
|
+
|
|
550
|
+
if [[ "$MODE" == "pose" ]]; then
|
|
551
|
+
for ((i = 0; i < ${#POSE_ARGS[@]}; i += 3)); do
|
|
552
|
+
pose_index=$((i / 3))
|
|
553
|
+
pose_path="${POSE_ARGS[$i]}"
|
|
554
|
+
echo "run-on-box: pushing pose $((pose_index + 1))/$(( ${#POSE_ARGS[@]} / 3 )) to ${HOST}:${REMOTE_TMP}/pose-${pose_index}.npz ..."
|
|
555
|
+
scp -q "${SCP_OPTS[@]}" "$pose_path" "$HOST:${REMOTE_TMP}/pose-${pose_index}.npz"
|
|
556
|
+
done
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
REMOTE_CMD="$(build_remote_cmd "$REMOTE_TMP")"
|
|
560
|
+
echo "run-on-box: generating on ${HOST} ..."
|
|
561
|
+
# shellcheck disable=SC2029 # REMOTE_CMD is a remote command by design
|
|
562
|
+
ssh "${SSH_OPTS[@]}" "$HOST" "$REMOTE_CMD" </dev/null
|
|
563
|
+
|
|
564
|
+
echo "run-on-box: pulling ${HOST}:${REMOTE_TMP}/out.npz -> ${OUTPUT} ..."
|
|
565
|
+
# shellcheck disable=SC2029 # HOST:REMOTE_TMP is a remote path by design
|
|
566
|
+
scp -q "${SCP_OPTS[@]}" "$HOST:${REMOTE_TMP}/out.npz" "$OUTPUT"
|
|
567
|
+
[[ -s "$OUTPUT" ]] || { echo "run-on-box: pulled ${OUTPUT} is empty; the generation failed" >&2; exit 1; }
|
|
568
|
+
echo "run-on-box: done - ${OUTPUT} ($(wc -c < "$OUTPUT" | tr -d ' ') bytes)"
|