@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.
- package/LICENSE +557 -0
- package/README.md +37 -2
- package/lib/b64.d.ts +2 -0
- package/lib/b64.js +7 -0
- package/lib/bodyPresets.d.ts +40 -0
- package/lib/bodyPresets.js +77 -0
- package/lib/captionSplitter.d.ts +49 -0
- package/lib/captionSplitter.js +172 -0
- package/lib/cardMutations.d.ts +52 -0
- package/lib/cardMutations.js +67 -0
- package/lib/downloads.d.ts +18 -0
- package/lib/downloads.js +64 -0
- package/lib/extractVideoFrame.d.ts +23 -0
- package/lib/extractVideoFrame.js +114 -0
- package/lib/glbLoad.d.ts +54 -0
- package/lib/glbLoad.js +96 -0
- package/lib/glbSkin.d.ts +14 -0
- package/lib/glbSkin.js +124 -0
- package/lib/handPose.d.ts +46 -0
- package/lib/handPose.js +122 -0
- package/lib/index.d.ts +25 -0
- package/lib/index.js +25 -0
- package/lib/keyframeExtract.d.ts +46 -0
- package/lib/keyframeExtract.js +89 -0
- package/lib/maskPrep.d.ts +88 -0
- package/lib/maskPrep.js +386 -0
- package/lib/motionConstraints.d.ts +237 -0
- package/lib/motionConstraints.js +369 -0
- package/lib/motionEnvelope.d.ts +56 -0
- package/lib/motionEnvelope.js +59 -0
- package/lib/motionNpz.d.ts +120 -0
- package/lib/motionNpz.js +497 -0
- package/lib/npz.d.ts +24 -0
- package/lib/npz.js +158 -0
- package/lib/openPoseCanvas.d.ts +17 -0
- package/lib/openPoseCanvas.js +90 -0
- package/lib/pngMeta.d.ts +42 -0
- package/lib/pngMeta.js +212 -0
- package/lib/pngMetaInfo.d.ts +14 -0
- package/lib/pngMetaInfo.js +127 -0
- package/lib/poseCodec.d.ts +20 -0
- package/lib/poseCodec.js +61 -0
- package/lib/poseTransforms.d.ts +66 -0
- package/lib/poseTransforms.js +227 -0
- package/lib/poserSkin.d.ts +75 -0
- package/lib/poserSkin.js +141 -0
- package/lib/prompt-tags.json +132 -0
- package/lib/promptTags.d.ts +42 -0
- package/lib/promptTags.js +116 -0
- package/lib/psdExport.d.ts +28 -0
- package/lib/psdExport.js +272 -0
- package/lib/readFile.d.ts +9 -0
- package/lib/readFile.js +16 -0
- package/lib/renderSnapshot.d.ts +34 -0
- package/lib/renderSnapshot.js +55 -0
- package/lib/somaSkin.d.ts +82 -0
- package/lib/somaSkin.js +267 -0
- package/lib/textToPose.d.ts +394 -0
- package/lib/textToPose.js +607 -0
- package/lib/uploadToEstate.d.ts +25 -0
- package/lib/uploadToEstate.js +97 -0
- package/package.json +149 -4
package/lib/glbLoad.d.ts
ADDED
|
@@ -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
|
package/lib/glbSkin.d.ts
ADDED
|
@@ -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
|
package/lib/handPose.js
ADDED
|
@@ -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
|