@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,272 @@
1
+ // ─── Big-endian byte writer ──────────────────────────────────────────────────
2
+ class ByteWriter {
3
+ chunks = [];
4
+ view = new DataView(new ArrayBuffer(8));
5
+ u8(v) {
6
+ const b = new Uint8Array(1);
7
+ b[0] = v & 0xff;
8
+ this.chunks.push(b);
9
+ }
10
+ u16(v) {
11
+ const dv = new DataView(new ArrayBuffer(2));
12
+ dv.setUint16(0, v, false);
13
+ this.chunks.push(new Uint8Array(dv.buffer));
14
+ }
15
+ /** signed 16 */
16
+ i16(v) {
17
+ const dv = new DataView(new ArrayBuffer(2));
18
+ dv.setInt16(0, v, false);
19
+ this.chunks.push(new Uint8Array(dv.buffer));
20
+ }
21
+ u32(v) {
22
+ const dv = new DataView(new ArrayBuffer(4));
23
+ dv.setUint32(0, v >>> 0, false);
24
+ this.chunks.push(new Uint8Array(dv.buffer));
25
+ }
26
+ /** signed 32 */
27
+ i32(v) {
28
+ const dv = new DataView(new ArrayBuffer(4));
29
+ dv.setInt32(0, v | 0, false);
30
+ this.chunks.push(new Uint8Array(dv.buffer));
31
+ }
32
+ bytes(b) { this.chunks.push(b); }
33
+ ascii(s) {
34
+ const b = new Uint8Array(s.length);
35
+ for (let i = 0; i < s.length; i++)
36
+ b[i] = s.charCodeAt(i) & 0xff;
37
+ this.chunks.push(b);
38
+ }
39
+ size() { return this.chunks.reduce((n, c) => n + c.length, 0); }
40
+ toUint8() {
41
+ const out = new Uint8Array(this.size());
42
+ let off = 0;
43
+ for (const c of this.chunks) {
44
+ out.set(c, off);
45
+ off += c.length;
46
+ }
47
+ return out;
48
+ }
49
+ }
50
+ function loadImageData(dataUrl, w, h) {
51
+ return new Promise((resolve, reject) => {
52
+ const img = new Image();
53
+ img.onload = () => {
54
+ const c = document.createElement('canvas');
55
+ c.width = w;
56
+ c.height = h;
57
+ const ctx = c.getContext('2d');
58
+ ctx.clearRect(0, 0, w, h);
59
+ ctx.drawImage(img, 0, 0, w, h);
60
+ resolve(ctx.getImageData(0, 0, w, h).data);
61
+ };
62
+ img.onerror = () => reject(new Error('Failed to decode layer for PSD'));
63
+ img.src = dataUrl;
64
+ });
65
+ }
66
+ /** Map our BlendMode to a PSD 4-char blend key. Unsupported → 'norm'. */
67
+ function blendKey(mode) {
68
+ const map = {
69
+ normal: 'norm',
70
+ multiply: 'mul ',
71
+ screen: 'scrn',
72
+ overlay: 'over',
73
+ darken: 'dark',
74
+ lighten: 'lite',
75
+ 'color-dodge': 'd dg',
76
+ 'color-burn': 'div ',
77
+ 'hard-light': 'hLit',
78
+ 'soft-light': 'sLit',
79
+ difference: 'diff',
80
+ exclusion: 'smud',
81
+ hue: 'hue ',
82
+ saturation: 'sat ',
83
+ color: 'colr',
84
+ luminosity: 'lum ',
85
+ };
86
+ return map[mode] ?? 'norm';
87
+ }
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+ function even(n) { return n % 2 === 0 ? n : n + 1; }
96
+ /**
97
+ * Build a layered PSD Blob. `width`/`height` define the canvas; every layer is
98
+ * composited into that extent. The merged composite flattens visible layers.
99
+ */
100
+ export async function buildPsd(layers, width, height, mergedRgba) {
101
+ const w = Math.max(1, Math.round(width));
102
+ const h = Math.max(1, Math.round(height));
103
+ // Decode all layers into RGBA planes.
104
+ const decoded = [];
105
+ for (const l of layers) {
106
+ if (!l.dataUrl)
107
+ continue;
108
+ try {
109
+ const rgba = await loadImageData(l.dataUrl, w, h);
110
+ decoded.push({ name: l.name, width: w, height: h, rgba, blendMode: l.blendMode, opacity: l.opacity, visible: l.visible });
111
+ }
112
+ catch (err) {
113
+ console.warn('[psd] skipping undecodable layer:', l.name, err);
114
+ }
115
+ }
116
+ // Merged composite (flattened visible layers) for the Image Data section.
117
+ let merged = mergedRgba;
118
+ if (!merged) {
119
+ const c = document.createElement('canvas');
120
+ c.width = w;
121
+ c.height = h;
122
+ const ctx = c.getContext('2d');
123
+ for (const l of decoded) {
124
+ if (!l.visible)
125
+ continue;
126
+ ctx.globalAlpha = l.opacity;
127
+ ctx.globalCompositeOperation = l.blendMode === 'normal' ? 'source-over' : l.blendMode;
128
+ const lc = document.createElement('canvas');
129
+ lc.width = w;
130
+ lc.height = h;
131
+ lc.getContext('2d').putImageData(new ImageData(l.rgba.slice(), w, h), 0, 0);
132
+ ctx.drawImage(lc, 0, 0);
133
+ }
134
+ ctx.globalAlpha = 1;
135
+ ctx.globalCompositeOperation = 'source-over';
136
+ merged = ctx.getImageData(0, 0, w, h).data;
137
+ }
138
+ // ─── Build layer records + channel image data ───
139
+ // Per layer channels: 0=R, 1=G, 2=B, -1=transparency.
140
+ const CHANNEL_IDS = [0, 1, 2, -1];
141
+ const numChannels = CHANNEL_IDS.length;
142
+ const records = new ByteWriter();
143
+ const channelData = new ByteWriter();
144
+ // Channel plane size = w*h bytes (raw, +2 for compression type).
145
+ const planeSize = w * h;
146
+ for (const layer of decoded) {
147
+ // Split RGBA into planes.
148
+ const planes = {
149
+ 0: new Uint8Array(planeSize),
150
+ 1: new Uint8Array(planeSize),
151
+ 2: new Uint8Array(planeSize),
152
+ [-1]: new Uint8Array(planeSize),
153
+ };
154
+ for (let i = 0; i < planeSize; i++) {
155
+ planes[0][i] = layer.rgba[i * 4];
156
+ planes[1][i] = layer.rgba[i * 4 + 1];
157
+ planes[2][i] = layer.rgba[i * 4 + 2];
158
+ planes[-1][i] = layer.rgba[i * 4 + 3];
159
+ }
160
+ // ── Layer record ──
161
+ records.i32(0); // top
162
+ records.i32(0); // left
163
+ records.i32(h); // bottom
164
+ records.i32(w); // right
165
+ records.u16(numChannels); // channel count
166
+ for (const id of CHANNEL_IDS) {
167
+ records.i16(id); // channel id
168
+ records.u32(2 + planeSize); // channel data length (compression + raw)
169
+ }
170
+ records.ascii('8BIM'); // blend mode signature
171
+ records.ascii(blendKey(layer.blendMode)); // blend key (4 chars)
172
+ records.u8(Math.round(layer.opacity * 255)); // opacity
173
+ records.u8(0); // clipping (base)
174
+ const flags = layer.visible ? 0b00000000 : 0b00000010; // bit1 set = hidden
175
+ records.u8(flags); // flags
176
+ records.u8(0); // filler
177
+ // ── Extra data: mask(0) + blending ranges(0) + name ──
178
+ const extra = new ByteWriter();
179
+ extra.u32(0); // mask data length
180
+ extra.u32(0); // blending ranges length
181
+ // Pascal string name, padded to multiple of 4 (including length byte).
182
+ const nameBytes = new TextEncoder().encode(layer.name.slice(0, 255));
183
+ const nameBuf = new ByteWriter();
184
+ nameBuf.u8(nameBytes.length);
185
+ nameBuf.bytes(nameBytes);
186
+ const nameLen = nameBuf.size();
187
+ const pad = (4 - (nameLen % 4)) % 4;
188
+ nameBuf.bytes(new Uint8Array(pad));
189
+ extra.bytes(nameBuf.toUint8());
190
+ const extraBytes = extra.toUint8();
191
+ records.u32(extraBytes.length);
192
+ records.bytes(extraBytes);
193
+ // ── Channel image data for this layer ──
194
+ for (const id of CHANNEL_IDS) {
195
+ channelData.u16(0); // compression: raw
196
+ channelData.bytes(planes[id]);
197
+ }
198
+ }
199
+ // Layer count (signed; negative ⇒ first alpha = merged transparency).
200
+ // We use positive count.
201
+ const layerInfoBody = new ByteWriter();
202
+ layerInfoBody.i16(decoded.length);
203
+ layerInfoBody.bytes(records.toUint8());
204
+ layerInfoBody.bytes(channelData.toUint8());
205
+ const layerInfoBytes = layerInfoBody.toUint8();
206
+ // Layer info section: 4-byte length (even-padded) + body.
207
+ const layerInfoPadded = even(layerInfoBytes.length);
208
+ const layerSection = new ByteWriter();
209
+ layerSection.u32(layerInfoPadded);
210
+ layerSection.bytes(layerInfoBytes);
211
+
212
+
213
+ if (layerInfoPadded > layerInfoBytes.length) {
214
+ layerSection.bytes(new Uint8Array(layerInfoPadded - layerInfoBytes.length));
215
+ }
216
+ // Layer and Mask Information: 4-byte length + layer section.
217
+ const layerMaskBytes = layerSection.toUint8();
218
+ const layerMaskPadded = even(layerMaskBytes.length);
219
+ // ─── Merged image data: 3 RGB planes (raw) ───
220
+ const mergedPlanes = [
221
+ new Uint8Array(planeSize),
222
+ new Uint8Array(planeSize),
223
+ new Uint8Array(planeSize),
224
+ ];
225
+ for (let i = 0; i < planeSize; i++) {
226
+ mergedPlanes[0][i] = merged[i * 4];
227
+ mergedPlanes[1][i] = merged[i * 4 + 1];
228
+ mergedPlanes[2][i] = merged[i * 4 + 2];
229
+ }
230
+ const imageData = new ByteWriter();
231
+ imageData.u16(0); // compression: raw
232
+ for (const p of mergedPlanes)
233
+ imageData.bytes(p);
234
+ // ─── Assemble file ───
235
+ const out = new ByteWriter();
236
+ // Header
237
+ out.ascii('8BPS'); // signature
238
+ out.u16(1); // version
239
+ out.bytes(new Uint8Array(6)); // reserved
240
+ out.u16(3); // channels (RGB)
241
+ out.u32(h); // height
242
+ out.u32(w); // width
243
+ out.u16(8); // depth
244
+ out.u16(3); // color mode (RGB)
245
+ // Color mode data
246
+ out.u32(0);
247
+ // Image resources
248
+ out.u32(0);
249
+ // Layer and mask information
250
+ out.u32(layerMaskPadded);
251
+ out.bytes(layerMaskBytes);
252
+
253
+
254
+ if (layerMaskPadded > layerMaskBytes.length) {
255
+ out.bytes(new Uint8Array(layerMaskPadded - layerMaskBytes.length));
256
+ }
257
+ // Image data
258
+ out.bytes(imageData.toUint8());
259
+ return new Blob([out.toUint8()], { type: 'image/vnd.adobe.photoshop' });
260
+ }
261
+ /** Trigger a browser download for a Blob. */
262
+ export function downloadBlob(blob, filename) {
263
+ const url = URL.createObjectURL(blob);
264
+ const a = document.createElement('a');
265
+ a.href = url;
266
+ a.download = filename;
267
+ document.body.appendChild(a);
268
+ a.click();
269
+ a.remove();
270
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
271
+ }
272
+ //# sourceMappingURL=psdExport.js.map
@@ -0,0 +1,9 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+ /** Read a File / Blob and resolve its data: URL. */
8
+ export declare function readAsDataURL(file: Blob): Promise<string>;
9
+ //# sourceMappingURL=readFile.d.ts.map
@@ -0,0 +1,16 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+ /** Read a File / Blob and resolve its data: URL. */
8
+ export function readAsDataURL(file) {
9
+ return new Promise((resolve, reject) => {
10
+ const reader = new FileReader();
11
+ reader.onload = () => resolve(reader.result);
12
+ reader.onerror = () => reject(reader.error ?? new Error("read failed"));
13
+ reader.readAsDataURL(file);
14
+ });
15
+ }
16
+ //# sourceMappingURL=readFile.js.map
@@ -0,0 +1,34 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+ import * as THREE from '@melite/three-client/client';
19
+ export interface SnapshotOptions {
20
+ /** Square output size (256 — plenty for a tile even at 2× DPR). */
21
+ size?: number;
22
+ /** JPEG quality (0.82 — photo-like renders, 4–6× smaller than PNG). */
23
+ jpegQuality?: number;
24
+ /** Device-pixel-ratio cap (1.5 — the tile is too small for more). */
25
+ maxDpr?: number;
26
+ /** Object whose geometry/materials are disposed after the snapshot
27
+ * (the scene graph the caller built for this render). */
28
+ disposeRoot?: THREE.Object3D;
29
+ }
30
+ /** Render `scene` through `camera` ONCE and return a JPEG data URL.
31
+ * The renderer is created, used, and torn down within this call —
32
+ * no persistent WebGL context survives a snapshot. */
33
+ export declare function snapshotSceneToJpeg(scene: THREE.Scene, camera: THREE.Camera, opts?: SnapshotOptions): string;
34
+ //# sourceMappingURL=renderSnapshot.d.ts.map
@@ -0,0 +1,55 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+ import * as THREE from '@melite/three-client/client';
19
+ /** Render `scene` through `camera` ONCE and return a JPEG data URL.
20
+ * The renderer is created, used, and torn down within this call —
21
+ * no persistent WebGL context survives a snapshot. */
22
+ export function snapshotSceneToJpeg(scene, camera, opts = {}) {
23
+ const size = opts.size ?? 256;
24
+ const quality = opts.jpegQuality ?? 0.82;
25
+ const maxDpr = opts.maxDpr ?? 1.5;
26
+ const renderer = new THREE.WebGLRenderer({
27
+ antialias: true,
28
+ alpha: false,
29
+ preserveDrawingBuffer: true,
30
+ });
31
+ try {
32
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, maxDpr));
33
+ renderer.setSize(size, size, false);
34
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
35
+ renderer.toneMappingExposure = 1.0;
36
+ renderer.render(scene, camera);
37
+ return renderer.domElement.toDataURL("image/jpeg", quality);
38
+ }
39
+ finally {
40
+ // Free GPU memory + the context itself immediately — the snapshot
41
+ // is already captured, so nothing live should remain per tile.
42
+ if (opts.disposeRoot) {
43
+ opts.disposeRoot.traverse((obj) => {
44
+ if (obj instanceof THREE.Mesh) {
45
+ obj.geometry?.dispose();
46
+ const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
47
+ mats.forEach((m) => m?.dispose());
48
+ }
49
+ });
50
+ }
51
+ renderer.dispose();
52
+ renderer.forceContextLoss();
53
+ }
54
+ }
55
+ //# sourceMappingURL=renderSnapshot.js.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * somaSkin — neutral SOMA-77 mesh skinning, a direct port of the kimodo demo's
3
+ * vendor/kimodo/kimodo/viz/soma_skin.py SOMASkin.skin()+lbs().
4
+ *
5
+ * The demo (and only the demo) renders Kimodo motions on a NEUTRAL body via
6
+ * LBS weights stored in skin_standard.npz (bind_vertices + faces +
7
+ * bind_rig_transform + lbs_indices + lbs_weights). That is the "base SOMAX
8
+ * mesh" that actually deforms — distinct from the poser backend's `soma`
9
+ * engine (a static mesh with no LBS weights) and from `anny`/`mhr` (parametric
10
+ * bodies with their own proportions). This module reproduces the demo's math
11
+ * client-side so the asset preview shows a Kimodo motion on the same neutral
12
+ * mesh the demo uses, with no parametric body shape imposed.
13
+ *
14
+ * Flow per frame:
15
+ * fk[j] = [[global_rot[j], global_pos[j]], [0,0,0,1]] (4×4, per joint)
16
+ * affine[j] = (fk[j] · inv(bind_rig[j]))[:3, :] (3×4, per joint)
17
+ * v' = Σ_w lbs_weights[v,w] · (affine[lbs_indices[v,w]] · [bind_v, 1])
18
+ *
19
+ * Units: meters throughout (Kimodo's native unit). No cm↔m conversion — that
20
+ * was a poser-route artifact that doesn't apply here.
21
+ */
22
+ export declare const SKIN_URL = "/v1/models/kimodo/provisions/skin_standard.npz";
23
+ export interface SomaSkin {
24
+ bindVertices: Float32Array;
25
+ faces: Uint32Array;
26
+ lbsIndices: Uint32Array;
27
+ lbsWeights: Float32Array;
28
+ bindRigInv: Float32Array;
29
+ /** (J, 3) METERS — the neutral / rest-pose joint positions, the TS
30
+ * equivalent of upstream Kimodo's `skeleton.neutral_joints`
31
+ * (vendor/kimodo/.../skeleton/base.py:72). Extracted from the
32
+ * translation column of each joint's forward bind transform so the
33
+ * motion card can render + pose + export BEFORE any GPU inference
34
+ * matching the Kimodo demo (app.py:460 "initialize to character
35
+ * default (rest) pose"). Without this the card treated the rest pose
36
+ * as "nothing" and gated every feature on a generated motion NPZ. */
37
+ restJoints: Float32Array;
38
+ /** (J, 9) row-major — the neutral / rest-pose joint global rotations
39
+ * (3×3 block of each forward bind transform). Pairs with restJoints to
40
+ * form a 1-frame "rest motion" that deformFrame / patchFrame consume
41
+ * unchanged, so refine + export + repose all work on the rest pose. */
42
+ restRots: Float32Array;
43
+ vertexCount: number;
44
+ faceCount: number;
45
+
46
+
47
+
48
+ jointCount: number;
49
+
50
+
51
+
52
+ influences: number;
53
+ }
54
+ /** Invert a 4×4 matrix (column-major-agnostic: stored row-major as 16 floats). */
55
+ export declare function invert4(m: Float32Array): Float32Array;
56
+ /** Extract the rest pose from row-major FORWARD bind transforms (J×16):
57
+ * translation column (tx, ty, tz) = indices 3, 7, 11 of each 4×4, and the
58
+ * 3×3 rotation block = 0,1,2 / 4,5,6 / 8,9,10. This is the TS equivalent
59
+ * of Kimodo's neutral_joints — the motion card's pre-inference pose (see
60
+ * SomaSkin.restJoints doc). Shared by both loaders: the npz carries the
61
+ * forward binds directly; the GLB adapter inverts glTF boneInverses into
62
+ * the same row-major forward shape first. */
63
+ export declare function restPoseFromBind(bindRig: Float32Array, jointCount: number): {
64
+ restJoints: Float32Array;
65
+ restRots: Float32Array;
66
+ };
67
+ /** Load + cache the neutral SOMA skin (one fetch per session). */
68
+ export declare function loadSomaSkin(): Promise<SomaSkin>;
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+ export declare function deformFrame(globalRot: Float32Array, globalPos: Float32Array, skin: SomaSkin, out: Float32Array): void;
82
+ //# sourceMappingURL=somaSkin.d.ts.map