@melite/formats 0.0.0-stage → 0.1.5-pre-alpha

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 (62) hide show
  1. package/LICENSE +557 -0
  2. package/README.md +37 -2
  3. package/lib/b64.d.ts +2 -0
  4. package/lib/b64.js +7 -0
  5. package/lib/bodyPresets.d.ts +40 -0
  6. package/lib/bodyPresets.js +77 -0
  7. package/lib/captionSplitter.d.ts +49 -0
  8. package/lib/captionSplitter.js +172 -0
  9. package/lib/cardMutations.d.ts +52 -0
  10. package/lib/cardMutations.js +67 -0
  11. package/lib/downloads.d.ts +18 -0
  12. package/lib/downloads.js +64 -0
  13. package/lib/extractVideoFrame.d.ts +23 -0
  14. package/lib/extractVideoFrame.js +114 -0
  15. package/lib/glbLoad.d.ts +54 -0
  16. package/lib/glbLoad.js +96 -0
  17. package/lib/glbSkin.d.ts +14 -0
  18. package/lib/glbSkin.js +124 -0
  19. package/lib/handPose.d.ts +46 -0
  20. package/lib/handPose.js +122 -0
  21. package/lib/index.d.ts +25 -0
  22. package/lib/index.js +25 -0
  23. package/lib/keyframeExtract.d.ts +46 -0
  24. package/lib/keyframeExtract.js +89 -0
  25. package/lib/maskPrep.d.ts +88 -0
  26. package/lib/maskPrep.js +386 -0
  27. package/lib/motionConstraints.d.ts +237 -0
  28. package/lib/motionConstraints.js +369 -0
  29. package/lib/motionEnvelope.d.ts +56 -0
  30. package/lib/motionEnvelope.js +59 -0
  31. package/lib/motionNpz.d.ts +120 -0
  32. package/lib/motionNpz.js +497 -0
  33. package/lib/npz.d.ts +24 -0
  34. package/lib/npz.js +158 -0
  35. package/lib/openPoseCanvas.d.ts +17 -0
  36. package/lib/openPoseCanvas.js +90 -0
  37. package/lib/pngMeta.d.ts +42 -0
  38. package/lib/pngMeta.js +212 -0
  39. package/lib/pngMetaInfo.d.ts +14 -0
  40. package/lib/pngMetaInfo.js +127 -0
  41. package/lib/poseCodec.d.ts +20 -0
  42. package/lib/poseCodec.js +61 -0
  43. package/lib/poseTransforms.d.ts +66 -0
  44. package/lib/poseTransforms.js +227 -0
  45. package/lib/poserSkin.d.ts +75 -0
  46. package/lib/poserSkin.js +141 -0
  47. package/lib/prompt-tags.json +132 -0
  48. package/lib/promptTags.d.ts +42 -0
  49. package/lib/promptTags.js +116 -0
  50. package/lib/psdExport.d.ts +28 -0
  51. package/lib/psdExport.js +272 -0
  52. package/lib/readFile.d.ts +9 -0
  53. package/lib/readFile.js +16 -0
  54. package/lib/renderSnapshot.d.ts +34 -0
  55. package/lib/renderSnapshot.js +55 -0
  56. package/lib/somaSkin.d.ts +82 -0
  57. package/lib/somaSkin.js +267 -0
  58. package/lib/textToPose.d.ts +394 -0
  59. package/lib/textToPose.js +607 -0
  60. package/lib/uploadToEstate.d.ts +25 -0
  61. package/lib/uploadToEstate.js +97 -0
  62. package/package.json +149 -4
@@ -0,0 +1,54 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+ import * as THREE from '@melite/three-client/client';
27
+ import { GLTFLoader } from '@melite/three-client/client';
28
+ import type { GLTF } from '@melite/three-client/client';
29
+ /** A GLTFLoader configured with the estate's shared DRACO decoder —
30
+ * for parse-based consumers (glbSkin parses pre-fetched bytes). */
31
+ export declare function makeGLTFLoader(): GLTFLoader;
32
+ /** Load a GLB/GLTF from a URL with the estate's shared DRACO-configured
33
+ * loader. One loader instance + one decoder worker pool for every
34
+ * consumer (the decoder worker is reused, never respawned per view). */
35
+ export declare function loadGLB(url: string): Promise<GLTF>;
36
+ /**
37
+ * Normalize a loaded scene to a known viewing size: center X/Z, scale
38
+ * the largest dimension to `targetHeight`, feet on y=0. Mutates the
39
+ * root IN PLACE (scale + position) and returns the computed box.
40
+ *
41
+ * The shared normalization every viewer applies — TRELLIS props,
42
+ * SOMAX/Anny characters, and composite uploads arrive at wildly
43
+ * different scales (cm, m, 100× unit boxes); this is the one place
44
+ * that reconciles them, so the poster tile, the dialog viewer, and
45
+ * the stage floor all frame the mesh identically.
46
+ */
47
+ export declare function fitObjectToHeight(root: THREE.Object3D, targetHeight?: number): THREE.Box3;
48
+ /** The historical spelling (the card-base glb-stage's name for the
49
+ * same algorithm). The default is ONE value: the two copies said
50
+ * 1.8 and 1.6, so the same mesh framed in a card stage and framed
51
+ * in the gallery came out at different heights. 1.6 is the de-facto
52
+ * law — every explicit call site already passed it. */
53
+ export { fitObjectToHeight as autoFitToHeight };
54
+ //# sourceMappingURL=glbLoad.d.ts.map
package/lib/glbLoad.js ADDED
@@ -0,0 +1,96 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+ import * as THREE from '@melite/three-client/client';
27
+ import { GLTFLoader } from '@melite/three-client/client';
28
+ import { DRACOLoader } from '@melite/three-client/client';
29
+ let _draco = null;
30
+ let _loader = null;
31
+ // import.meta.env is Vite runtime truth; the cast states the read
32
+ // locally (a global ImportMeta augmentation collides with the
33
+
34
+
35
+
36
+
37
+
38
+
39
+ const base = import.meta.env?.BASE_URL ?? "/";
40
+ function dracoLoader() {
41
+ if (!_draco) {
42
+ _draco = new DRACOLoader();
43
+ // LOCAL decoder (offline, deterministic — no CDN, no SPOF). The
44
+ // trailing slash matters: DRACOLoader concatenates filenames onto
45
+ // the path verbatim.
46
+ _draco.setDecoderPath(`${base}draco/gltf/`);
47
+ }
48
+ return _draco;
49
+ }
50
+ /** A GLTFLoader configured with the estate's shared DRACO decoder —
51
+ * for parse-based consumers (glbSkin parses pre-fetched bytes). */
52
+ export function makeGLTFLoader() {
53
+ const loader = new GLTFLoader();
54
+ loader.setDRACOLoader(dracoLoader());
55
+ return loader;
56
+ }
57
+ /** Load a GLB/GLTF from a URL with the estate's shared DRACO-configured
58
+ * loader. One loader instance + one decoder worker pool for every
59
+ * consumer (the decoder worker is reused, never respawned per view). */
60
+ export function loadGLB(url) {
61
+ if (!_loader)
62
+ _loader = makeGLTFLoader();
63
+ return new Promise((resolve, reject) => {
64
+ _loader.load(url, resolve, undefined, reject);
65
+ });
66
+ }
67
+ /**
68
+ * Normalize a loaded scene to a known viewing size: center X/Z, scale
69
+ * the largest dimension to `targetHeight`, feet on y=0. Mutates the
70
+ * root IN PLACE (scale + position) and returns the computed box.
71
+ *
72
+ * The shared normalization every viewer applies — TRELLIS props,
73
+ * SOMAX/Anny characters, and composite uploads arrive at wildly
74
+ * different scales (cm, m, 100× unit boxes); this is the one place
75
+ * that reconciles them, so the poster tile, the dialog viewer, and
76
+ * the stage floor all frame the mesh identically.
77
+ */
78
+ export function fitObjectToHeight(root, targetHeight = 1.6) {
79
+ const box = new THREE.Box3().setFromObject(root);
80
+ const size = box.getSize(new THREE.Vector3());
81
+ const center = box.getCenter(new THREE.Vector3());
82
+ const maxDim = Math.max(size.x, size.y, size.z);
83
+ const scale = maxDim > 0 ? targetHeight / maxDim : 1;
84
+ root.scale.setScalar(scale);
85
+ root.position.x = -center.x * scale;
86
+ root.position.z = -center.z * scale;
87
+ root.position.y = -box.min.y * scale;
88
+ return box;
89
+ }
90
+ /** The historical spelling (the card-base glb-stage's name for the
91
+ * same algorithm). The default is ONE value: the two copies said
92
+ * 1.8 and 1.6, so the same mesh framed in a card stage and framed
93
+ * in the gallery came out at different heights. 1.6 is the de-facto
94
+ * law — every explicit call site already passed it. */
95
+ export { fitObjectToHeight as autoFitToHeight };
96
+ //# sourceMappingURL=glbLoad.js.map
@@ -0,0 +1,14 @@
1
+ import { type SomaSkin } from "./somaSkin.js";
2
+ export declare class NotSomaxGlbError extends Error {
3
+ constructor(reason: string);
4
+ }
5
+ /**
6
+ * Load a SOMAX-rigged character GLB as a SomaSkin.
7
+ *
8
+ * @param url a data: URL (studio upload) or a same-origin asset URL
9
+ * (/editor/assets/... — asset tray or a wired upstream card).
10
+ * @throws NotSomaxGlbError when the GLB has no 77-joint skinned mesh.
11
+ * @throws Error on fetch/parse failures.
12
+ */
13
+ export declare function loadGlbSkin(url: string): Promise<SomaSkin>;
14
+ //# sourceMappingURL=glbSkin.d.ts.map
package/lib/glbSkin.js ADDED
@@ -0,0 +1,124 @@
1
+ import { makeGLTFLoader } from "./glbLoad.js";
2
+ import { invert4, restPoseFromBind } from "./somaSkin.js";
3
+ /** SOMA-77 — the rig every SOMAX GLB carries (skin.name "SOMA-Mixamo"). */
4
+ const SOMA_JOINTS = 77;
5
+ /** glTF skins are VEC4 — four influences per vertex. */
6
+ const GLTF_INFLUENCES = 4;
7
+ export class NotSomaxGlbError extends Error {
8
+ constructor(reason) {
9
+ super(`Not a SOMAX-rigged GLB (77-joint SOMA skeleton required): ${reason}`);
10
+ this.name = "NotSomaxGlbError";
11
+ }
12
+ }
13
+ /** Column-major 16 (three.js Matrix4.elements) → row-major Float32Array. */
14
+ function colMajorToRowMajor(elements) {
15
+ const out = new Float32Array(16);
16
+ for (let r = 0; r < 4; r++) {
17
+ for (let c = 0; c < 4; c++) {
18
+ out[r * 4 + c] = elements[c * 4 + r];
19
+ }
20
+ }
21
+ return out;
22
+ }
23
+ /**
24
+ * Load a SOMAX-rigged character GLB as a SomaSkin.
25
+ *
26
+ * @param url a data: URL (studio upload) or a same-origin asset URL
27
+ * (/editor/assets/... — asset tray or a wired upstream card).
28
+ * @throws NotSomaxGlbError when the GLB has no 77-joint skinned mesh.
29
+ * @throws Error on fetch/parse failures.
30
+ */
31
+ export async function loadGlbSkin(url) {
32
+ const res = await fetch(url);
33
+ if (!res.ok)
34
+ throw new Error(`Failed to fetch character GLB: ${res.status}`);
35
+ const buffer = await res.arrayBuffer();
36
+ // The shared loader kernel — DRACO-configured (a compressed character
37
+ // GLB parses identically here and in every viewer; before the kernel,
38
+ // this bare loader was the ONE GLB path in the estate with no decoder).
39
+ const loader = makeGLTFLoader();
40
+ const gltf = await new Promise((resolve, reject) => loader.parse(buffer, "", resolve, reject));
41
+ // Collect EVERY 77-joint skinned mesh (composite characters layer
42
+ // body + outfit + hair on one shared rig).
43
+ const meshes = [];
44
+ gltf.scene.traverse((obj) => {
45
+ const m = obj;
46
+ if (!m.isSkinnedMesh || !m.geometry)
47
+ return;
48
+ if (m.skeleton?.bones?.length === SOMA_JOINTS)
49
+ meshes.push(m);
50
+ });
51
+ if (meshes.length === 0) {
52
+ throw new NotSomaxGlbError("no skinned mesh with 77 joints found (static meshes and other rigs cannot play SOMA-77 motion)");
53
+ }
54
+ // ── The rig terms (shared by every merged mesh — one rig per SOMAX GLB)
55
+ const skeleton = meshes[0].skeleton;
56
+ const J = skeleton.bones.length;
57
+ const bindRigInv = new Float32Array(J * 16);
58
+ for (let j = 0; j < J; j++) {
59
+ // boneInverses ARE the inverse bind matrices — bindRigInv verbatim,
60
+ // transposed from three.js column-major into somaSkin's row-major.
61
+ bindRigInv.set(colMajorToRowMajor(skeleton.boneInverses[j].elements), j * 16);
62
+ }
63
+ // Forward bind = invert(inverse bind); its translation + rotation blocks
64
+ // are the rest pose (the same extraction the npz loader runs).
65
+ const bindRigFwd = new Float32Array(J * 16);
66
+ for (let j = 0; j < J; j++) {
67
+ bindRigFwd.set(invert4(bindRigInv.subarray(j * 16, j * 16 + 16)), j * 16);
68
+ }
69
+ const { restJoints, restRots } = restPoseFromBind(bindRigFwd, J);
70
+ // ── Merge the meshes: positions + faces + per-vertex skin attributes
71
+ let vertexCount = 0;
72
+ let faceCount = 0;
73
+ for (const m of meshes) {
74
+ vertexCount += m.geometry.attributes.position.count;
75
+ if (!m.geometry.index) {
76
+ throw new NotSomaxGlbError(`mesh "${m.name}" is not triangulated (no index buffer)`);
77
+ }
78
+ faceCount += m.geometry.index.count / 3;
79
+ }
80
+ const bindVertices = new Float32Array(vertexCount * 3);
81
+ const faces = new Uint32Array(faceCount * 3);
82
+ const lbsIndices = new Uint32Array(vertexCount * GLTF_INFLUENCES);
83
+ const lbsWeights = new Float32Array(vertexCount * GLTF_INFLUENCES);
84
+ let vOff = 0;
85
+ let iOff = 0;
86
+ for (const m of meshes) {
87
+ const pos = m.geometry.attributes.position;
88
+ bindVertices.set(pos.array, vOff * 3);
89
+ const si = m.geometry.attributes.skinIndex;
90
+ const sw = m.geometry.attributes.skinWeight;
91
+ if (!si || !sw) {
92
+ throw new NotSomaxGlbError(`mesh "${m.name}" has no skinIndex/skinWeight attributes (not skinned)`);
93
+ }
94
+ if (si.itemSize !== GLTF_INFLUENCES || sw.itemSize !== GLTF_INFLUENCES) {
95
+ throw new NotSomaxGlbError(`mesh "${m.name}" skin attributes are ${si.itemSize}-wide (glTF requires VEC4)`);
96
+ }
97
+ lbsIndices.set(si.array, vOff * GLTF_INFLUENCES);
98
+ lbsWeights.set(sw.array, vOff * GLTF_INFLUENCES);
99
+ // Non-null by the counting pass's own refusal (`!m.geometry.index`
100
+ // threw above) — the assertion rides that enforced invariant, not
101
+
102
+ const idx = m.geometry.index;
103
+ const base = vOff;
104
+ for (let i = 0; i < idx.count; i++) {
105
+ faces[iOff + i] = idx.array[i] + base;
106
+ }
107
+ iOff += idx.count;
108
+ vOff += pos.count;
109
+ }
110
+ return {
111
+ bindVertices,
112
+ faces,
113
+ lbsIndices,
114
+ lbsWeights,
115
+ bindRigInv,
116
+ restJoints,
117
+ restRots,
118
+ vertexCount,
119
+ faceCount,
120
+ jointCount: J,
121
+ influences: GLTF_INFLUENCES,
122
+ };
123
+ }
124
+ //# sourceMappingURL=glbSkin.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * handPose — finger joint indices and helpers for SOMA-77 hand pose presets
3
+ * (Kimodo-TO method #3: pose libraries for fingers).
4
+ *
5
+ * The SOMA-77 skeleton stores JOINT POSITIONS (not rotations), so a finger
6
+ * preset is a snapshot of finger-joint positions. To make presets portable
7
+ * across wrist positions, we store them as OFFSETS relative to the wrist
8
+ * joint (LeftHand=14, RightHand=42). On apply, the offset is re-based to
9
+ * the current wrist position.
10
+ *
11
+ * NOTE: This translation-only rebasing ignores wrist rotation. If the wrist
12
+ * has rotated since the preset was captured, the fingers will point in the
13
+ * original direction. For MVP this is acceptable — the user can either
14
+ * recapture the preset at the new orientation, or fine-tune manually after
15
+ * applying. A full rotation-aware version would derive the wrist frame from
16
+ * the wrist→knuckle direction and re-orient offsets accordingly.
17
+ *
18
+ * Joint layout (vendor/kimodo/kimodo/skeleton/definitions.py SOMASkeleton77):
19
+ * LeftHand(14) [thumb 15-18, index 19-23, middle 24-28, ring 29-33, pinky 34-38]
20
+ * RightHand(42) [thumb 43-46, index 47-51, middle 52-56, ring 57-61, pinky 62-66]
21
+ */
22
+ /** Wrist joint index per hand side. */
23
+ export declare const WRIST_INDEX: Record<"left" | "right", number>;
24
+ /** Finger joint indices per hand side (wrist NOT included — wrist is the
25
+ * anchor that offsets are relative to). */
26
+ export declare const FINGER_JOINTS: Record<"left" | "right", number[]>;
27
+ /**
28
+ * Extract a hand pose from a flat 77×3 joint array as wrist-relative offsets.
29
+ * The result is a sparse {jointIdx: [dx, dy, dz]} map (only finger joints).
30
+ * The wrist position is subtracted so the preset is portable.
31
+ */
32
+ export declare function captureHandPose(side: "left" | "right", joints: Float32Array | number[]): Record<number, [number, number, number]>;
33
+ /**
34
+ * Apply a captured hand pose to a flat 77×3 joint array. Each finger joint
35
+ * is set to the current wrist position + the stored offset. Only finger
36
+ * joints are touched — the rest of the pose (including the wrist) is left
37
+ * intact. Returns a new Float32Array (does not mutate the input).
38
+ */
39
+ export declare function applyHandPose(side: "left" | "right", joints: Float32Array | number[], offsets: Record<number, [number, number, number]>): Float32Array;
40
+ /** Encode a hand-pose offset map as a plain number[] for the pose library.
41
+ * Stores the wrist-relative offsets inline at their joint indices in a 77×3
42
+ * array (zero elsewhere). Round-trips with decodeHandPose. */
43
+ export declare function encodeHandPose(side: "left" | "right", offsets: Record<number, [number, number, number]>): number[];
44
+ /** Decode a stored hand-pose number[] back into an offset map. */
45
+ export declare function decodeHandPose(arr: number[]): Record<number, [number, number, number]>;
46
+ //# sourceMappingURL=handPose.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * handPose — finger joint indices and helpers for SOMA-77 hand pose presets
3
+ * (Kimodo-TO method #3: pose libraries for fingers).
4
+ *
5
+ * The SOMA-77 skeleton stores JOINT POSITIONS (not rotations), so a finger
6
+ * preset is a snapshot of finger-joint positions. To make presets portable
7
+ * across wrist positions, we store them as OFFSETS relative to the wrist
8
+ * joint (LeftHand=14, RightHand=42). On apply, the offset is re-based to
9
+ * the current wrist position.
10
+ *
11
+ * NOTE: This translation-only rebasing ignores wrist rotation. If the wrist
12
+ * has rotated since the preset was captured, the fingers will point in the
13
+ * original direction. For MVP this is acceptable — the user can either
14
+ * recapture the preset at the new orientation, or fine-tune manually after
15
+ * applying. A full rotation-aware version would derive the wrist frame from
16
+ * the wrist→knuckle direction and re-orient offsets accordingly.
17
+ *
18
+ * Joint layout (vendor/kimodo/kimodo/skeleton/definitions.py SOMASkeleton77):
19
+ * LeftHand(14) [thumb 15-18, index 19-23, middle 24-28, ring 29-33, pinky 34-38]
20
+ * RightHand(42) [thumb 43-46, index 47-51, middle 52-56, ring 57-61, pinky 62-66]
21
+ */
22
+ /** Wrist joint index per hand side. */
23
+ export const WRIST_INDEX = {
24
+ left: 14,
25
+ right: 42,
26
+ };
27
+ /** Finger joint indices per hand side (wrist NOT included — wrist is the
28
+ * anchor that offsets are relative to). */
29
+ export const FINGER_JOINTS = {
30
+ left: [
31
+ 15, 16, 17, 18, // thumb (4 joints)
32
+ 19, 20, 21, 22, 23, // index (5)
33
+ 24, 25, 26, 27, 28, // middle (5)
34
+ 29, 30, 31, 32, 33, // ring (5)
35
+ 34, 35, 36, 37, 38, // pinky (5)
36
+ ],
37
+ right: [
38
+ 43, 44, 45, 46,
39
+ 47, 48, 49, 50, 51,
40
+ 52, 53, 54, 55, 56,
41
+ 57, 58, 59, 60, 61,
42
+ 62, 63, 64, 65, 66,
43
+ ],
44
+ };
45
+ /**
46
+ * Extract a hand pose from a flat 77×3 joint array as wrist-relative offsets.
47
+ * The result is a sparse {jointIdx: [dx, dy, dz]} map (only finger joints).
48
+ * The wrist position is subtracted so the preset is portable.
49
+ */
50
+ export function captureHandPose(side, joints) {
51
+ const wrist = WRIST_INDEX[side];
52
+ const wx = joints[wrist * 3];
53
+ const wy = joints[wrist * 3 + 1];
54
+ const wz = joints[wrist * 3 + 2];
55
+ const out = {};
56
+ for (const idx of FINGER_JOINTS[side]) {
57
+ out[idx] = [
58
+ joints[idx * 3] - wx,
59
+ joints[idx * 3 + 1] - wy,
60
+ joints[idx * 3 + 2] - wz,
61
+ ];
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Apply a captured hand pose to a flat 77×3 joint array. Each finger joint
67
+ * is set to the current wrist position + the stored offset. Only finger
68
+ * joints are touched — the rest of the pose (including the wrist) is left
69
+ * intact. Returns a new Float32Array (does not mutate the input).
70
+ */
71
+ export function applyHandPose(side, joints, offsets) {
72
+ const out = joints instanceof Float32Array
73
+ ? new Float32Array(joints)
74
+ : new Float32Array(joints);
75
+ const wrist = WRIST_INDEX[side];
76
+ const wx = out[wrist * 3];
77
+ const wy = out[wrist * 3 + 1];
78
+ const wz = out[wrist * 3 + 2];
79
+ for (const idx of FINGER_JOINTS[side]) {
80
+ const off = offsets[idx];
81
+ if (!off)
82
+ continue;
83
+ out[idx * 3] = wx + off[0];
84
+ out[idx * 3 + 1] = wy + off[1];
85
+ out[idx * 3 + 2] = wz + off[2];
86
+ }
87
+ return out;
88
+ }
89
+ /** Encode a hand-pose offset map as a plain number[] for the pose library.
90
+ * Stores the wrist-relative offsets inline at their joint indices in a 77×3
91
+ * array (zero elsewhere). Round-trips with decodeHandPose. */
92
+ export function encodeHandPose(side, offsets) {
93
+ const arr = new Array(77 * 3).fill(0);
94
+ for (const idx of FINGER_JOINTS[side]) {
95
+ const off = offsets[idx];
96
+ if (!off)
97
+ continue;
98
+ arr[idx * 3] = off[0];
99
+ arr[idx * 3 + 1] = off[1];
100
+ arr[idx * 3 + 2] = off[2];
101
+ }
102
+ return arr;
103
+ }
104
+ /** Decode a stored hand-pose number[] back into an offset map. */
105
+ export function decodeHandPose(arr) {
106
+ const out = {};
107
+ // The encoding stores offsets only at finger-joint indices; we don't need
108
+ // to know the side to decode (the indices tell us). Scan all 77 joints.
109
+ for (let idx = 0; idx < 77; idx++) {
110
+ const x = arr[idx * 3];
111
+ const y = arr[idx * 3 + 1];
112
+ const z = arr[idx * 3 + 2];
113
+ // Skip zeros (uninitialized slots). Note: this conflates genuinely-zero
114
+ // offsets with empty slots, but a real finger offset is never exactly at
115
+ // the wrist (would mean a zero-length bone), so this is safe in practice.
116
+ if (x === 0 && y === 0 && z === 0)
117
+ continue;
118
+ out[idx] = [x, y, z];
119
+ }
120
+ return out;
121
+ }
122
+ //# sourceMappingURL=handPose.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /** @melite/formats — pure media codecs + math (npz, skins, masks, presets).
2
+ * Fetch-free by law; three.js types only where geometry demands. */
3
+ export * from "./npz.js";
4
+ export * from "./somaSkin.js";
5
+ export * from "./motionNpz.js";
6
+ export * from "./glbSkin.js";
7
+ export * from "./maskPrep.js";
8
+ export * from "./bodyPresets.js";
9
+ export * from "./poseCodec.js";
10
+ export * from "./motionConstraints.js";
11
+ export * from "./openPoseCanvas.js";
12
+ export * from "./downloads.js";
13
+ export * from "./b64.js";
14
+ export * from "./poseTransforms.js";
15
+ export * from "./psdExport.js";
16
+ export * from "./pngMetaInfo.js";
17
+ export * from "./pngMeta.js";
18
+ export * from "./captionSplitter.js";
19
+ export * from "./promptTags.js";
20
+ export * from "./motionEnvelope.js";
21
+ export * from "./extractVideoFrame.js";
22
+ export * from "./keyframeExtract.js";
23
+ export * from "./handPose.js";
24
+ export * from "./cardMutations.js";
25
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /** @melite/formats — pure media codecs + math (npz, skins, masks, presets).
2
+ * Fetch-free by law; three.js types only where geometry demands. */
3
+ export * from "./npz.js";
4
+ export * from "./somaSkin.js";
5
+ export * from "./motionNpz.js";
6
+ export * from "./glbSkin.js";
7
+ export * from "./maskPrep.js";
8
+ export * from "./bodyPresets.js";
9
+ export * from "./poseCodec.js";
10
+ export * from "./motionConstraints.js";
11
+ export * from "./openPoseCanvas.js";
12
+ export * from "./downloads.js";
13
+ export * from "./b64.js";
14
+ export * from "./poseTransforms.js";
15
+ export * from "./psdExport.js";
16
+ export * from "./pngMetaInfo.js";
17
+ export * from "./pngMeta.js";
18
+ export * from "./captionSplitter.js";
19
+ export * from "./promptTags.js";
20
+ export * from "./motionEnvelope.js";
21
+ export * from "./extractVideoFrame.js";
22
+ export * from "./keyframeExtract.js";
23
+ export * from "./handPose.js";
24
+ export * from "./cardMutations.js";
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * keyframeExtract — pick anchor frames from a SOMA-77 motion trajectory.
3
+ *
4
+ * Used by Pose Studio's "Auto Keyframes" action (Kimodo-TO (g)) to turn a
5
+ * generated motion into a sparse set of keyframe constraints without the
6
+ * user scrubbing and pinning by hand.
7
+ *
8
+ * Two regimes:
9
+ *
10
+ * 1. Multi-prompt motion → the semantic boundaries are already known:
11
+ * each prompt segment's start_frame / end_frame is a natural keyframe
12
+ * (the motion changes intent there). No signal processing needed.
13
+ *
14
+ * 2. Single-prompt motion → run velocity-minima detection on the joint
15
+ * trajectory. Per-frame velocity = total squared displacement of all
16
+ * 77 joints from the previous frame. Local MINIMA of this signal are
17
+ * the holds / extremes (top of a reach, contact frame, pause) — clean,
18
+ * readable anchor poses. Local MAXIMA are mid-motion blur frames and
19
+ * make BAD anchors, so we explicitly look for minima, not peaks.
20
+ *
21
+ * Both paths return frame indices, deduped and sorted, with endpoints and a
22
+ * minimum spacing enforced.
23
+ */
24
+ import type { PromptSegmentOutput } from "./textToPose.js";
25
+ export interface ExtractOptions {
26
+ /** Minimum frames between extracted keyframes (anti-clustering). */
27
+ minSpacing?: number;
28
+ /** Always include frame 0 and the last frame as anchors. Default true. */
29
+ includeEndpoints?: boolean;
30
+ }
31
+ /**
32
+ * Extract keyframe frame indices from a motion's per-frame joint trajectory.
33
+ *
34
+ * @param framesJoints Per-frame joint positions. Shape: [frame][joint][3].
35
+ * (result.motion.frames_soma77_3d from the pose API.)
36
+ * @param segments Optional prompt segments. When present (>1), their
37
+ * boundaries are used directly instead of velocity
38
+ * detection — they ARE the semantic keyframes.
39
+ */
40
+ export declare function extractKeyframeFrames(framesJoints: number[][][], segments?: PromptSegmentOutput[], opts?: ExtractOptions): number[];
41
+ /**
42
+ * Per-frame velocity (total squared joint displacement). Exposed for the
43
+ * optional velocity-curve visualization in the timeline.
44
+ */
45
+ export declare function frameVelocities(framesJoints: number[][][]): number[];
46
+ //# sourceMappingURL=keyframeExtract.d.ts.map
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Extract keyframe frame indices from a motion's per-frame joint trajectory.
3
+ *
4
+ * @param framesJoints Per-frame joint positions. Shape: [frame][joint][3].
5
+ * (result.motion.frames_soma77_3d from the pose API.)
6
+ * @param segments Optional prompt segments. When present (>1), their
7
+ * boundaries are used directly instead of velocity
8
+ * detection — they ARE the semantic keyframes.
9
+ */
10
+ export function extractKeyframeFrames(framesJoints, segments, opts = {}) {
11
+ if (!framesJoints?.length)
12
+ return [];
13
+ const total = framesJoints.length;
14
+ const minSpacing = opts.minSpacing ?? Math.max(2, Math.floor(total / 12));
15
+ const includeEndpoints = opts.includeEndpoints ?? true;
16
+ let raw;
17
+ if (segments && segments.length > 1) {
18
+ // Multi-prompt: segment boundaries are the keyframes.
19
+ raw = new Set();
20
+ for (const seg of segments) {
21
+ raw.add(seg.start_frame);
22
+ raw.add(seg.end_frame);
23
+ }
24
+ // Also include the peak if provided — it's often a strong anchor.
25
+ }
26
+ else {
27
+ // Single-prompt (or no segments): velocity-minima detection.
28
+ raw = new Set(velocityMinima(framesJoints));
29
+ }
30
+ // Enforce endpoints.
31
+ if (includeEndpoints) {
32
+ raw.add(0);
33
+ raw.add(total - 1);
34
+ }
35
+ // Dedupe + sort + enforce min spacing (greedy — keep first, skip too-close).
36
+ const sorted = Array.from(raw).sort((a, b) => a - b)
37
+ .filter(f => f >= 0 && f < total);
38
+ const out = [];
39
+ for (const f of sorted) {
40
+ if (out.length === 0 || f - out[out.length - 1] >= minSpacing) {
41
+ out.push(f);
42
+ }
43
+ else if (f === total - 1) {
44
+ // Always keep the last frame even if close to the previous — it's
45
+
46
+ // non-empty and f - prev < minSpacing with f === total - 1, so the
47
+ // end-anchor unconditionally replaces its too-close predecessor.
48
+ out[out.length - 1] = f;
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ /**
54
+ * Per-frame velocity (total squared joint displacement). Exposed for the
55
+ * optional velocity-curve visualization in the timeline.
56
+ */
57
+ export function frameVelocities(framesJoints) {
58
+ const v = new Array(framesJoints.length).fill(0);
59
+ for (let f = 1; f < framesJoints.length; f++) {
60
+ const a = framesJoints[f - 1];
61
+ const b = framesJoints[f];
62
+ let sum = 0;
63
+ const n = Math.min(a.length, b.length);
64
+ for (let j = 0; j < n; j++) {
65
+ const aj = a[j], bj = b[j];
66
+ const dx = bj[0] - aj[0];
67
+ const dy = bj[1] - aj[1];
68
+ const dz = bj[2] - aj[2];
69
+ sum += dx * dx + dy * dy + dz * dz;
70
+ }
71
+ v[f] = sum;
72
+ }
73
+ v[0] = v[1] ?? 0;
74
+ return v;
75
+ }
76
+ /** Local minima of the velocity curve → holds / extremes = good anchors. */
77
+ function velocityMinima(framesJoints) {
78
+ const v = frameVelocities(framesJoints);
79
+ const minima = [];
80
+ // A frame is a local minimum if it's lower than its neighbors. Use a
81
+ // smoothing window of 2 to avoid jitter-induced false minima.
82
+ for (let f = 2; f < v.length - 2; f++) {
83
+ if (v[f] < v[f - 1] && v[f] < v[f + 1] && v[f] < v[f - 2] && v[f] < v[f + 2]) {
84
+ minima.push(f);
85
+ }
86
+ }
87
+ return minima;
88
+ }
89
+ //# sourceMappingURL=keyframeExtract.js.map