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,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-Node writer for numpy .npz archives: v1.0 .npy members inside a
|
|
3
|
+
* STORED (uncompressed) zip. No numpy on the workstation and no new
|
|
4
|
+
* dependencies -- .npy is a 10-byte prefix plus a little-endian float32
|
|
5
|
+
* payload, and STORED zip keeps CRC-32 and sizes trivial to compute.
|
|
6
|
+
*
|
|
7
|
+
* The only consumer today is the ARDY constrained generator:
|
|
8
|
+
*
|
|
9
|
+
* --pose-from <npz> <src-frame> <dst-frame>
|
|
10
|
+
*
|
|
11
|
+
* cclay_constrained_generate.load_poses reads exactly two float32 members,
|
|
12
|
+
* local_rot_mats (frames, 27, 3, 3) and posed_joints (frames, 27, 3), then
|
|
13
|
+
* runs its own FK. poseArraysToNpzMembers turns one CozyClay frame into
|
|
14
|
+
* that exact 1-frame member set, so a hand-blocked pose can be materialized
|
|
15
|
+
* as a standalone npz without ever hardcoding a rest skeleton: proportions
|
|
16
|
+
* come from the base clip (deriveBoneOffsets) and the 8 joints CozyClay
|
|
17
|
+
* does not author stay IDENTITY, i.e. "at ARDY rest".
|
|
18
|
+
*/
|
|
19
|
+
import { writeFileSync } from "node:fs";
|
|
20
|
+
|
|
21
|
+
const ZIP_LOCAL_SIG = 0x04034b50; // PK\x03\x04
|
|
22
|
+
const ZIP_CENTRAL_SIG = 0x02014b50; // PK\x01\x02
|
|
23
|
+
const ZIP_EOCD_SIG = 0x06054b50; // PK\x05\x06
|
|
24
|
+
|
|
25
|
+
// --- CRC-32 (IEEE 802.3, reflected, polynomial 0xEDB88320) ----------------
|
|
26
|
+
|
|
27
|
+
const CRC_TABLE = (() => {
|
|
28
|
+
const table = new Uint32Array(256);
|
|
29
|
+
for (let n = 0; n < 256; n += 1) {
|
|
30
|
+
let c = n;
|
|
31
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
32
|
+
table[n] = c >>> 0;
|
|
33
|
+
}
|
|
34
|
+
return table;
|
|
35
|
+
})();
|
|
36
|
+
|
|
37
|
+
function crc32(buf) {
|
|
38
|
+
let c = 0xffffffff;
|
|
39
|
+
for (let i = 0; i < buf.length; i += 1) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
|
|
40
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- .npy v1.0 -------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The ASCII dict header, matching numpy's own text byte for byte:
|
|
47
|
+
* {'descr': '<f4', 'fortran_order': False, 'shape': (1, 27, 3, 3), }
|
|
48
|
+
* Shape tuples follow Python repr: `()` for 0-d, `(n,)` for a single
|
|
49
|
+
* element, `(a, b, c)` otherwise.
|
|
50
|
+
*/
|
|
51
|
+
function npyHeader(shape, descr = "<f4") {
|
|
52
|
+
const tuple =
|
|
53
|
+
shape.length === 0 ? "()" : `(${shape.join(", ")}${shape.length === 1 ? "," : ""})`;
|
|
54
|
+
const dict = `{'descr': '${descr}', 'fortran_order': False, 'shape': ${tuple}, }`;
|
|
55
|
+
// Pad so magic(6) + version(2) + len(2) + header is a multiple of 64,
|
|
56
|
+
// i.e. the header-with-newline is 54 mod 64. numpy's loader only reads
|
|
57
|
+
// the length field and ast.literal_eval's the dict, so it accepts either
|
|
58
|
+
// alignment; this repo's convention is that the whole prefix aligns.
|
|
59
|
+
const pad = (53 - (dict.length % 64) + 64) % 64;
|
|
60
|
+
return dict + " ".repeat(pad) + "\n";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildNpy(data, shape) {
|
|
64
|
+
const descr = data instanceof Float32Array ? "<f4" : data instanceof Int32Array ? "<i4" : null;
|
|
65
|
+
if (!descr) throw new Error(`buildNpy: unsupported typed array ${data?.constructor?.name ?? typeof data}`);
|
|
66
|
+
const header = npyHeader(shape, descr);
|
|
67
|
+
const prefix = Buffer.alloc(10);
|
|
68
|
+
prefix.writeUInt8(0x93, 0);
|
|
69
|
+
prefix.write("NUMPY", 1, "ascii");
|
|
70
|
+
prefix.writeUInt8(1, 6); // version 1.0
|
|
71
|
+
prefix.writeUInt8(0, 7);
|
|
72
|
+
prefix.writeUInt16LE(header.length, 8);
|
|
73
|
+
// Float32Array is little-endian on every platform this repo runs on;
|
|
74
|
+
// the view (not a copy) is safe: writeNpz builds the payload and the
|
|
75
|
+
// archive in the same synchronous call.
|
|
76
|
+
const payload = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
77
|
+
return Buffer.concat([prefix, Buffer.from(header, "ascii"), payload]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- ZIP (STORED) -----------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the archive: local headers + payloads, central directory, EOCD.
|
|
84
|
+
* Every size is known up front, so the whole file is one Buffer.
|
|
85
|
+
*/
|
|
86
|
+
function buildZip(entries) {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
const central = [];
|
|
89
|
+
let offset = 0;
|
|
90
|
+
for (const { name, payload } of entries) {
|
|
91
|
+
const nameBuf = Buffer.from(name, "ascii");
|
|
92
|
+
const crc = crc32(payload);
|
|
93
|
+
const local = Buffer.alloc(30);
|
|
94
|
+
local.writeUInt32LE(ZIP_LOCAL_SIG, 0);
|
|
95
|
+
local.writeUInt16LE(20, 4); // version needed to extract
|
|
96
|
+
local.writeUInt16LE(0, 6); // general purpose flags
|
|
97
|
+
local.writeUInt16LE(0, 8); // method: STORED
|
|
98
|
+
local.writeUInt16LE(0, 10); // last mod time
|
|
99
|
+
local.writeUInt16LE(0x21, 12); // last mod date (1980-01-01)
|
|
100
|
+
local.writeUInt32LE(crc, 14);
|
|
101
|
+
local.writeUInt32LE(payload.length, 18); // compressed size
|
|
102
|
+
local.writeUInt32LE(payload.length, 22); // uncompressed size
|
|
103
|
+
local.writeUInt16LE(nameBuf.length, 26);
|
|
104
|
+
local.writeUInt16LE(0, 28); // extra field length
|
|
105
|
+
chunks.push(local, nameBuf, payload);
|
|
106
|
+
central.push({ name: nameBuf, crc, size: payload.length, offset });
|
|
107
|
+
offset += 30 + nameBuf.length + payload.length;
|
|
108
|
+
}
|
|
109
|
+
const centralDirStart = offset;
|
|
110
|
+
const centralChunks = [];
|
|
111
|
+
let centralDirSize = 0;
|
|
112
|
+
for (const entry of central) {
|
|
113
|
+
const c = Buffer.alloc(46);
|
|
114
|
+
c.writeUInt32LE(ZIP_CENTRAL_SIG, 0);
|
|
115
|
+
c.writeUInt16LE(20, 4); // version made by
|
|
116
|
+
c.writeUInt16LE(20, 6); // version needed to extract
|
|
117
|
+
c.writeUInt16LE(0, 8); // general purpose flags
|
|
118
|
+
c.writeUInt16LE(0, 10); // method: STORED
|
|
119
|
+
c.writeUInt16LE(0, 12); // last mod time
|
|
120
|
+
c.writeUInt16LE(0x21, 14); // last mod date
|
|
121
|
+
c.writeUInt32LE(entry.crc, 16);
|
|
122
|
+
c.writeUInt32LE(entry.size, 20); // compressed size
|
|
123
|
+
c.writeUInt32LE(entry.size, 24); // uncompressed size
|
|
124
|
+
c.writeUInt16LE(entry.name.length, 28);
|
|
125
|
+
c.writeUInt16LE(0, 30); // extra field length
|
|
126
|
+
c.writeUInt16LE(0, 32); // file comment length
|
|
127
|
+
c.writeUInt16LE(0, 34); // disk number start
|
|
128
|
+
c.writeUInt16LE(0, 36); // internal attributes
|
|
129
|
+
c.writeUInt32LE(0, 38); // external attributes
|
|
130
|
+
c.writeUInt32LE(entry.offset, 42); // local header offset
|
|
131
|
+
centralChunks.push(c, entry.name);
|
|
132
|
+
centralDirSize += 46 + entry.name.length;
|
|
133
|
+
}
|
|
134
|
+
const eocd = Buffer.alloc(22);
|
|
135
|
+
eocd.writeUInt32LE(ZIP_EOCD_SIG, 0);
|
|
136
|
+
eocd.writeUInt16LE(0, 4); // disk number
|
|
137
|
+
eocd.writeUInt16LE(0, 6); // disk with central directory
|
|
138
|
+
eocd.writeUInt16LE(entries.length, 8); // entries on this disk
|
|
139
|
+
eocd.writeUInt16LE(entries.length, 10); // total entries
|
|
140
|
+
eocd.writeUInt32LE(centralDirSize, 12);
|
|
141
|
+
eocd.writeUInt32LE(centralDirStart, 16);
|
|
142
|
+
eocd.writeUInt16LE(0, 20); // comment length
|
|
143
|
+
return Buffer.concat([...chunks, ...centralChunks, eocd]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- public API -------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Write a numpy .npz archive: one STORED zip entry `<key>.npy` per member.
|
|
150
|
+
* Each member is { data: Float32Array, shape: number[] } in C order.
|
|
151
|
+
* Throws on an empty member set, a member whose shape product does not
|
|
152
|
+
* match its data length, a non-finite value, or a member name that would
|
|
153
|
+
* not make a flat zip entry.
|
|
154
|
+
*/
|
|
155
|
+
export function writeNpz(path, arrays) {
|
|
156
|
+
const names = Object.keys(arrays);
|
|
157
|
+
if (names.length === 0) {
|
|
158
|
+
throw new Error("writeNpz: refusing to write an npz with no members");
|
|
159
|
+
}
|
|
160
|
+
const entries = names.map((name) => {
|
|
161
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name)) {
|
|
162
|
+
throw new Error(`writeNpz: invalid member name ${JSON.stringify(name)}`);
|
|
163
|
+
}
|
|
164
|
+
const member = arrays[name];
|
|
165
|
+
if (
|
|
166
|
+
!member ||
|
|
167
|
+
(!(member.data instanceof Float32Array) && !(member.data instanceof Int32Array)) ||
|
|
168
|
+
!Array.isArray(member.shape)
|
|
169
|
+
) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`writeNpz: member ${JSON.stringify(name)} must be { data: Float32Array|Int32Array, shape: number[] }`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const { data, shape } = member;
|
|
175
|
+
const size = shape.reduce((n, d) => n * d, 1);
|
|
176
|
+
if (size !== data.length) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`writeNpz: member ${JSON.stringify(name)} shape ${JSON.stringify(shape)} ` +
|
|
179
|
+
`has ${size} elements but data has ${data.length}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
183
|
+
if (!Number.isFinite(data[i])) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`writeNpz: member ${JSON.stringify(name)}[${i}] is not finite: ${data[i]}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { name: `${name}.npy`, payload: buildNpy(data, shape) };
|
|
190
|
+
});
|
|
191
|
+
writeFileSync(path, buildZip(entries));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Turn one CozyClay frame of plain nested arrays into the exact 1-frame
|
|
196
|
+
* members load_poses expects: local_rot_mats [1, 27, 3, 3] and posed_joints
|
|
197
|
+
* [1, 27, 3], both float32 C order. Inputs are the ardy.frame.v1 fixture
|
|
198
|
+
* shapes: local_rot_mats [27][3][3] and posed_joints [27][3]. The leading 1
|
|
199
|
+
* is the frames axis, so the generator's `[frame]` / `[frame, root_idx]`
|
|
200
|
+
* indexing works unchanged.
|
|
201
|
+
*/
|
|
202
|
+
export function poseArraysToNpzMembers({ local_rot_mats, posed_joints }) {
|
|
203
|
+
return {
|
|
204
|
+
local_rot_mats: {
|
|
205
|
+
data: flattenToF32(local_rot_mats, [27, 3, 3], "local_rot_mats"),
|
|
206
|
+
shape: [1, 27, 3, 3],
|
|
207
|
+
},
|
|
208
|
+
posed_joints: {
|
|
209
|
+
data: flattenToF32(posed_joints, [27, 3], "posed_joints"),
|
|
210
|
+
shape: [1, 27, 3],
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Depth-first flatten of a nested number array against an expected shape.
|
|
217
|
+
* Throws on a structural mismatch or a non-finite value so a bad pose can
|
|
218
|
+
* never silently serialize.
|
|
219
|
+
*/
|
|
220
|
+
function flattenToF32(value, dims, name) {
|
|
221
|
+
const out = [];
|
|
222
|
+
const walk = (node, depth, path) => {
|
|
223
|
+
if (depth === dims.length) {
|
|
224
|
+
if (typeof node !== "number") {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`poseArraysToNpzMembers: ${name}${path} is not a number: ${node}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (!Number.isFinite(node)) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`poseArraysToNpzMembers: ${name}${path} is not finite: ${node}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
out.push(node);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!Array.isArray(node) || node.length !== dims[depth]) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`poseArraysToNpzMembers: ${name}${path} must be an array of length ` +
|
|
240
|
+
`${dims[depth]} (expected shape ${JSON.stringify(dims)})`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
for (let i = 0; i < node.length; i += 1) walk(node[i], depth + 1, `${path}[${i}]`);
|
|
244
|
+
};
|
|
245
|
+
walk(value, 0, "");
|
|
246
|
+
return Float32Array.from(out);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
/** Convert decoded ARDY motion arrays into writer members. */
|
|
251
|
+
export function motionArraysToNpzMembers({ frames, fps, rotMats, rootPos, posedJoints }) {
|
|
252
|
+
if (!Number.isInteger(frames) || frames < 1) throw new Error("motionArraysToNpzMembers: frames must be positive");
|
|
253
|
+
if (!Number.isInteger(fps) || fps < 1) throw new Error("motionArraysToNpzMembers: fps must be positive");
|
|
254
|
+
const requireLength = (array, length, label) => {
|
|
255
|
+
if (!(array instanceof Float32Array) || array.length !== length) {
|
|
256
|
+
throw new Error(`motionArraysToNpzMembers: ${label} must be Float32Array(${length})`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
requireLength(rotMats, frames * 27 * 9, "rotMats");
|
|
260
|
+
requireLength(rootPos, frames * 3, "rootPos");
|
|
261
|
+
requireLength(posedJoints, frames * 27 * 3, "posedJoints");
|
|
262
|
+
return {
|
|
263
|
+
local_rot_mats: { data: rotMats, shape: [frames, 27, 3, 3] },
|
|
264
|
+
root_positions: { data: rootPos, shape: [frames, 3] },
|
|
265
|
+
posed_joints: { data: posedJoints, shape: [frames, 27, 3] },
|
|
266
|
+
fps: { data: Int32Array.of(fps), shape: [] },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Concatenate contiguous, already world-aligned generated blocks. */
|
|
271
|
+
export function stitchMotionSegments(segments) {
|
|
272
|
+
if (!Array.isArray(segments) || segments.length === 0) {
|
|
273
|
+
throw new Error("stitchMotionSegments: at least one segment is required");
|
|
274
|
+
}
|
|
275
|
+
const fps = segments[0].fps;
|
|
276
|
+
if (!segments.every((segment) => segment.fps === fps)) {
|
|
277
|
+
throw new Error("stitchMotionSegments: every segment must use the same fps");
|
|
278
|
+
}
|
|
279
|
+
const frames = segments.reduce((sum, segment) => sum + segment.frames, 0);
|
|
280
|
+
const concat = (key, stride) => {
|
|
281
|
+
const out = new Float32Array(frames * stride);
|
|
282
|
+
let offset = 0;
|
|
283
|
+
for (const segment of segments) {
|
|
284
|
+
const values = segment[key];
|
|
285
|
+
if (!(values instanceof Float32Array) || values.length !== segment.frames * stride) {
|
|
286
|
+
throw new Error(`stitchMotionSegments: invalid ${key} length for ${segment.frames}-frame segment`);
|
|
287
|
+
}
|
|
288
|
+
out.set(values, offset);
|
|
289
|
+
offset += values.length;
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
frames,
|
|
295
|
+
fps,
|
|
296
|
+
rotMats: concat("rotMats", 27 * 9),
|
|
297
|
+
rootPos: concat("rootPos", 3),
|
|
298
|
+
posedJoints: concat("posedJoints", 27 * 3),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Return a copy of a motion with one equal-fps replacement written at startFrame. */
|
|
303
|
+
export function replaceMotionSegment(base, replacement, startFrame) {
|
|
304
|
+
if (!Number.isInteger(startFrame) || startFrame < 0) {
|
|
305
|
+
throw new Error("replaceMotionSegment: startFrame must be a non-negative integer");
|
|
306
|
+
}
|
|
307
|
+
if (!base || !replacement || base.fps !== replacement.fps) {
|
|
308
|
+
throw new Error("replaceMotionSegment: motions must use the same fps");
|
|
309
|
+
}
|
|
310
|
+
if (startFrame + replacement.frames > base.frames) {
|
|
311
|
+
throw new Error("replaceMotionSegment: replacement extends past the base motion");
|
|
312
|
+
}
|
|
313
|
+
const copyAndReplace = (key, stride) => {
|
|
314
|
+
const source = base[key];
|
|
315
|
+
const patch = replacement[key];
|
|
316
|
+
if (
|
|
317
|
+
!(source instanceof Float32Array) ||
|
|
318
|
+
source.length !== base.frames * stride ||
|
|
319
|
+
!(patch instanceof Float32Array) ||
|
|
320
|
+
patch.length !== replacement.frames * stride
|
|
321
|
+
) {
|
|
322
|
+
throw new Error(`replaceMotionSegment: invalid ${key} array`);
|
|
323
|
+
}
|
|
324
|
+
const out = new Float32Array(source);
|
|
325
|
+
out.set(patch, startFrame * stride);
|
|
326
|
+
return out;
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
frames: base.frames,
|
|
330
|
+
fps: base.fps,
|
|
331
|
+
rotMats: copyAndReplace("rotMats", 27 * 9),
|
|
332
|
+
rootPos: copyAndReplace("rootPos", 3),
|
|
333
|
+
posedJoints: copyAndReplace("posedJoints", 27 * 3),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CozyClayPoseV1 -> a one-frame ARDY pose npz.
|
|
4
|
+
*
|
|
5
|
+
* `cclay_constrained_generate.load_poses` reads exactly two arrays out of a pose
|
|
6
|
+
* source, `local_rot_mats` and `posed_joints`, and its docstring is blunt about
|
|
7
|
+
* why the file has to exist at all: "A FullBodyConstraintSet needs all 27
|
|
8
|
+
* joints, so the pose has to come from an existing npz -- 27 rotations are not
|
|
9
|
+
* authorable by hand." CozyClay authors 19 of them. This fills the remaining 8
|
|
10
|
+
* with identity (ARDY rest) and says so out loud rather than pretending the
|
|
11
|
+
* whole skeleton was blocked.
|
|
12
|
+
*
|
|
13
|
+
* Proportions and root come from the base clip's own reference frame, never from
|
|
14
|
+
* a hardcoded rest skeleton -- same reasoning as
|
|
15
|
+
* `motion_constraints.derive_bone_offsets`, which derives offsets from the clip
|
|
16
|
+
* so it stays correct if ARDY ever reproportions cskel27.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync, mkdirSync } from "node:fs";
|
|
19
|
+
import { dirname, basename, resolve } from "node:path";
|
|
20
|
+
import { poseToCskel27 } from "../../src/ardy/to-cskel27.js";
|
|
21
|
+
import { poseArraysToNpzMembers, writeNpz } from "./npz.mjs";
|
|
22
|
+
|
|
23
|
+
const HERE = new URL(".", import.meta.url).pathname;
|
|
24
|
+
const REPO = resolve(HERE, "../..");
|
|
25
|
+
|
|
26
|
+
function usage(message) {
|
|
27
|
+
if (message) process.stderr.write(`pose-to-npz: ${message}\n\n`);
|
|
28
|
+
process.stderr.write(
|
|
29
|
+
`usage: pose-to-npz.mjs <pose.json> [--reference <frame.json>] [--rest <rest.json>] [--out <npz>]\n\n` +
|
|
30
|
+
` <pose.json> a CozyClayPoseV1 export from the app\n` +
|
|
31
|
+
` --reference optional ardy.frame.v1 skeleton-proportion reference; placement always\n` +
|
|
32
|
+
` comes from pose.root or the floor-aligned canonical neutral skeleton.\n` +
|
|
33
|
+
` --rest cskel27 rest rotations (default public/ardy/cskel27-rest.json)\n` +
|
|
34
|
+
` --out destination npz (default tools/ardy/out/<pose-basename>.npz)\n`,
|
|
35
|
+
);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const argv = process.argv.slice(2);
|
|
40
|
+
if (argv.length === 0 || argv.includes("-h") || argv.includes("--help")) usage();
|
|
41
|
+
|
|
42
|
+
let posePath = "";
|
|
43
|
+
let referencePath = "";
|
|
44
|
+
let restPath = resolve(REPO, "public/ardy/cskel27-rest.json");
|
|
45
|
+
let outPath = "";
|
|
46
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
const next = () => {
|
|
49
|
+
if (i + 1 >= argv.length) usage(`${a} needs a value`);
|
|
50
|
+
i += 1;
|
|
51
|
+
return argv[i];
|
|
52
|
+
};
|
|
53
|
+
if (a === "--reference") referencePath = next();
|
|
54
|
+
else if (a === "--rest") restPath = next();
|
|
55
|
+
else if (a === "--out") outPath = next();
|
|
56
|
+
else if (a.startsWith("-")) usage(`unknown option ${a}`);
|
|
57
|
+
else if (!posePath) posePath = a;
|
|
58
|
+
else usage(`unexpected argument ${a}`);
|
|
59
|
+
}
|
|
60
|
+
if (!posePath) usage("missing <pose.json>");
|
|
61
|
+
|
|
62
|
+
const readJson = (p, label) => {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
process.stderr.write(`pose-to-npz: cannot read ${label} at ${p}: ${error.message}\n`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const pose = readJson(posePath, "pose");
|
|
72
|
+
const rest = readJson(restPath, "rest table");
|
|
73
|
+
const reference = referencePath ? readJson(referencePath, "reference frame") : undefined;
|
|
74
|
+
|
|
75
|
+
if (reference && reference.schema !== "ardy.frame.v1") {
|
|
76
|
+
process.stderr.write(`pose-to-npz: --reference must be schema ardy.frame.v1, got ${reference.schema}\n`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!outPath) {
|
|
81
|
+
outPath = resolve(REPO, "tools/ardy/out", `${basename(posePath).replace(/\.json$/i, "")}.npz`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let result;
|
|
85
|
+
try {
|
|
86
|
+
result = poseToCskel27({ pose, rest, reference });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
process.stderr.write(`pose-to-npz: ${error.message}\n`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
93
|
+
writeNpz(outPath, poseArraysToNpzMembers(result));
|
|
94
|
+
|
|
95
|
+
const root = result.posed_joints[0];
|
|
96
|
+
process.stdout.write(
|
|
97
|
+
`pose-to-npz: wrote ${outPath}\n` +
|
|
98
|
+
`pose-to-npz: rig ${pose.source?.rig ?? "unknown"}, slate ${JSON.stringify(pose.slate ?? "")}\n` +
|
|
99
|
+
(reference
|
|
100
|
+
? `pose-to-npz: skeleton proportions from ${reference.source_npz} frame ${reference.frame}\n`
|
|
101
|
+
: `pose-to-npz: skeleton proportions from floor-aligned canonical neutral\n`) +
|
|
102
|
+
`pose-to-npz: root [${root.map((v) => v.toFixed(4)).join(", ")}]\n` +
|
|
103
|
+
`pose-to-npz: ${result.filled_identity.length} joint(s) left at ARDY rest: ` +
|
|
104
|
+
`${result.filled_identity.map((f) => f.joint).join(", ")}\n` +
|
|
105
|
+
`pose-to-npz: --pose-from pins full-body shape, root XYZ, and heading at its destination frame\n`,
|
|
106
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
HOST="${CCLAY_ARDY_HOST:-}"
|
|
6
|
+
REMOTE="${CCLAY_ARDY_REPO:-\$HOME/ardy}"
|
|
7
|
+
VENV_PY="${CCLAY_ARDY_VENV:-~/ardy/.venv-cuda/bin/python}"
|
|
8
|
+
SCRIPT="$HERE/cclay_motion_edit.py"
|
|
9
|
+
SOURCE=""
|
|
10
|
+
MANIFEST=""
|
|
11
|
+
PROMPT=""
|
|
12
|
+
OUTPUT=""
|
|
13
|
+
SEED=""
|
|
14
|
+
CONTEXT_BEFORE=40
|
|
15
|
+
CONTEXT_AFTER=20
|
|
16
|
+
POSES=()
|
|
17
|
+
|
|
18
|
+
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=240)
|
|
19
|
+
SCP_OPTS=(-o BatchMode=yes -o ConnectTimeout=10)
|
|
20
|
+
|
|
21
|
+
usage() {
|
|
22
|
+
echo "usage: run-edit-on-box.sh --source motion.npz --manifest edits.json --prompt TEXT --output out.npz [--seed N] [--context-before N] [--context-after N]" >&2
|
|
23
|
+
exit 2
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
while [[ $# -gt 0 ]]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
--source) SOURCE="${2:-}"; shift 2 ;;
|
|
29
|
+
--manifest) MANIFEST="${2:-}"; shift 2 ;;
|
|
30
|
+
--prompt) PROMPT="${2:-}"; shift 2 ;;
|
|
31
|
+
--output) OUTPUT="${2:-}"; shift 2 ;;
|
|
32
|
+
--seed) SEED="${2:-}"; shift 2 ;;
|
|
33
|
+
--context-before) CONTEXT_BEFORE="${2:-}"; shift 2 ;;
|
|
34
|
+
--context-after) CONTEXT_AFTER="${2:-}"; shift 2 ;;
|
|
35
|
+
--pose) POSES+=("${2:-}"); shift 2 ;;
|
|
36
|
+
*) usage ;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
[[ -n "$HOST" ]] || {
|
|
41
|
+
echo "run-edit-on-box: CCLAY_ARDY_HOST is required (for example: user@ardy-host)" >&2
|
|
42
|
+
exit 2
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[[ -f "$SOURCE" && -f "$MANIFEST" && -f "$SCRIPT" && -n "$PROMPT" && -n "$OUTPUT" ]] || usage
|
|
46
|
+
[[ ${#POSES[@]} -gt 0 ]] || usage
|
|
47
|
+
[[ "$CONTEXT_BEFORE" =~ ^[0-9]+$ && "$CONTEXT_AFTER" =~ ^[0-9]+$ ]] || usage
|
|
48
|
+
[[ -z "$SEED" || "$SEED" =~ ^[0-9]+$ ]] || usage
|
|
49
|
+
|
|
50
|
+
mkdir -p "$(dirname "$OUTPUT")"
|
|
51
|
+
REMOTE_TMP="$(ssh "${SSH_OPTS[@]}" "$HOST" "mktemp -d" </dev/null)"
|
|
52
|
+
[[ "$REMOTE_TMP" == /* ]] || { echo "run-edit-on-box: unsafe remote temp path" >&2; exit 1; }
|
|
53
|
+
trap 'ssh "${SSH_OPTS[@]}" "$HOST" "rm -rf ${REMOTE_TMP}" </dev/null >/dev/null 2>&1 || true' EXIT
|
|
54
|
+
|
|
55
|
+
scp -q "${SCP_OPTS[@]}" "$SOURCE" "$HOST:${REMOTE_TMP}/source.npz"
|
|
56
|
+
scp -q "${SCP_OPTS[@]}" "$MANIFEST" "$HOST:${REMOTE_TMP}/manifest.json"
|
|
57
|
+
scp -q "${SCP_OPTS[@]}" "$SCRIPT" "$HOST:${REMOTE_TMP}/edit.py"
|
|
58
|
+
for ((i = 0; i < ${#POSES[@]}; i += 1)); do
|
|
59
|
+
[[ -f "${POSES[$i]}" ]] || { echo "run-edit-on-box: missing pose ${POSES[$i]}" >&2; exit 1; }
|
|
60
|
+
scp -q "${SCP_OPTS[@]}" "${POSES[$i]}" "$HOST:${REMOTE_TMP}/pose-${i}.npz"
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
CMD="cd ${REMOTE} && ${VENV_PY} ${REMOTE_TMP}/edit.py"
|
|
64
|
+
CMD+=" --source ${REMOTE_TMP}/source.npz --manifest ${REMOTE_TMP}/manifest.json"
|
|
65
|
+
CMD+=" --prompt $(printf '%q' "$PROMPT") --output ${REMOTE_TMP}/out.npz"
|
|
66
|
+
CMD+=" --context-before ${CONTEXT_BEFORE} --context-after ${CONTEXT_AFTER}"
|
|
67
|
+
[[ -z "$SEED" ]] || CMD+=" --seed ${SEED}"
|
|
68
|
+
|
|
69
|
+
echo "run-edit-on-box: editing with ARDY history and sparse constraints ..."
|
|
70
|
+
ssh "${SSH_OPTS[@]}" "$HOST" "$CMD" </dev/null
|
|
71
|
+
scp -q "${SCP_OPTS[@]}" "$HOST:${REMOTE_TMP}/out.npz" "$OUTPUT"
|
|
72
|
+
[[ -s "$OUTPUT" ]] || { echo "run-edit-on-box: empty output" >&2; exit 1; }
|
|
73
|
+
echo "run-edit-on-box: done - ${OUTPUT} ($(wc -c < "$OUTPUT" | tr -d ' ') bytes)"
|