@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,607 @@
1
+ /**
2
+ * Text-to-pose client — submit text prompts, poll for results, manage cache.
3
+ *
4
+ * Backend: custom_nodes/melite-poser-nodes/server.py (proxied via melite-head)
5
+ * Endpoints:
6
+ * POST /poser/text-to-pose Submit generation request
7
+ * GET /poser/text-to-pose/jobs/{job_id} Poll job status
8
+ * DELETE /poser/text-to-pose/jobs/{job_id} Cancel running job
9
+ * GET /poser/text-to-pose/cache List cached entries
10
+ * DELETE /poser/text-to-pose/cache/{hash} Delete cache entry
11
+ * POST /poser/text-to-pose/cache/clear Clear all cache entries
12
+ */
13
+ import { apiFetch } from "@melite/catalog/api";
14
+ import { decodeB64ToBuffer } from "./b64.js";
15
+ import { encodeBufferToB64 } from "./poseCodec.js";
16
+ import { downloadBase64Payload } from "./downloads.js";
17
+ import { SOMA_VERTEX_COUNT } from "./poseCodec.js";
18
+ import { sleep } from "@melite/catalog/sleep";
19
+ // ── Constants ──────────────────────────────────────────────────────────────
20
+ // runs off the main thread.
21
+ /** Slice a single frame's vertices out of a multi-frame base64 vertex buffer.
22
+ *
23
+ * Async because decoding the (potentially huge) multi-frame buffer is offloaded
24
+ * to the browser's native base64 decoder. Returns a base64 string containing
25
+ * just the one frame's float32 positions.
26
+ *
27
+ * @param multiB64 base64-encoded buffer of shape (T, V, 3) float32
28
+ * @param frameIdx which frame to slice (clamped to [0, T-1])
29
+ * @param vertexCount V — defaults to the SOMA-77 canonical count
30
+ * @returns base64-encoded single-frame buffer, or null on decode failure */
31
+ export async function sliceFrameAsB64(multiB64, frameIdx, vertexCount = SOMA_VERTEX_COUNT) {
32
+ try {
33
+ const stride = vertexCount * 3;
34
+ const buf = await decodeB64ToBuffer(multiB64);
35
+ const multi = new Float32Array(buf);
36
+ const totalFrames = Math.floor(multi.length / stride);
37
+ if (totalFrames < 1)
38
+ return null;
39
+ const idx = Math.max(0, Math.min(frameIdx, totalFrames - 1));
40
+ const singleFlat = new Float32Array(stride);
41
+ singleFlat.set(multi.subarray(idx * stride, (idx + 1) * stride));
42
+ return encodeBufferToB64(new Uint8Array(singleFlat.buffer));
43
+ }
44
+ catch (e) {
45
+ console.error("[text-to-pose] sliceFrameAsB64 failed:", e);
46
+ return null;
47
+ }
48
+ }
49
+ // ── Submit ─────────────────────────────────────────────────────────────────
50
+ /**
51
+ * Submit a text-to-pose generation request.
52
+ * Returns immediately with a job_id — poll via pollJob() or waitForJob().
53
+ */
54
+ export async function submitTextToPose(req) {
55
+ const res = await apiFetch("/poser/text-to-pose", {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify(req),
59
+ });
60
+ // Read text first — the server may return a non-JSON error (405, 502, etc.)
61
+ // and res.json() would throw an opaque V8 parse error that hides the cause.
62
+ const text = await res.text();
63
+ if (!res.ok) {
64
+ let detail = text.slice(0, 300);
65
+ try {
66
+ const j = JSON.parse(text);
67
+ detail = j.error || detail;
68
+ }
69
+ catch { /* not JSON */ }
70
+ throw new Error(`Submit failed: HTTP ${res.status} — ${detail}`);
71
+ }
72
+ try {
73
+ return JSON.parse(text);
74
+ }
75
+ catch (e) {
76
+ throw new Error(`Submit returned invalid JSON: ${text.slice(0, 200)}`);
77
+ }
78
+ }
79
+ // ── Poll ───────────────────────────────────────────────────────────────────
80
+ /**
81
+ * Poll job status once. Returns the current status + result if completed.
82
+ */
83
+ export async function pollJob(jobId, signal) {
84
+ const res = await apiFetch(`/poser/text-to-pose/jobs/${jobId}`, { signal });
85
+ if (res.status === 404) {
86
+ throw new Error(`Job ${jobId} not found`);
87
+ }
88
+ const text = await res.text();
89
+ if (!res.ok) {
90
+ throw new Error(`Poll failed: HTTP ${res.status} — ${text.slice(0, 200)}`);
91
+ }
92
+ try {
93
+ return JSON.parse(text);
94
+ }
95
+ catch (e) {
96
+ throw new Error(`Poll returned invalid JSON: ${text.slice(0, 200)}`);
97
+ }
98
+ }
99
+ /**
100
+ * Wait for a job to reach a terminal state (completed/failed/cancelled).
101
+ * Polls every `intervalMs` until `timeoutMs`.
102
+ *
103
+ * Calls `onUpdate` on each poll if provided — useful for progress UI.
104
+ */
105
+ export async function waitForJob(jobId, opts = {}) {
106
+ const interval = opts.intervalMs ?? 500;
107
+ const timeout = opts.timeoutMs ?? 120_000; // 2 min default (inference is 15-30s)
108
+ const deadline = Date.now() + timeout;
109
+ const signal = opts.signal;
110
+ while (Date.now() < deadline) {
111
+ if (signal?.aborted)
112
+ throw new DOMException("Aborted", "AbortError");
113
+ const status = await pollJob(jobId, signal);
114
+ opts.onUpdate?.(status);
115
+ if (status.status === "completed" && status.result) {
116
+ return status.result;
117
+ }
118
+ if (status.status === "failed") {
119
+ throw new Error(status.error || `Job failed (${status.error_class || "unknown"})`);
120
+ }
121
+ if (status.status === "cancelled") {
122
+ throw new Error("Job was cancelled");
123
+ }
124
+ await sleep(interval, signal);
125
+ }
126
+ throw new Error(`Job ${jobId} timed out after ${timeout}ms`);
127
+ }
128
+ // ── Cancel ─────────────────────────────────────────────────────────────────
129
+ export async function cancelJob(jobId) {
130
+ const res = await apiFetch(`/poser/text-to-pose/jobs/${jobId}`, {
131
+ method: "DELETE",
132
+ });
133
+ if (!res.ok && res.status !== 404) {
134
+ throw new Error(`Cancel failed: HTTP ${res.status}`);
135
+ }
136
+ }
137
+ // ── Convenience: submit + wait in one call ─────────────────────────────────
138
+ /**
139
+ * Submit a text-to-pose request and wait for the result.
140
+ * This is the simplest call for UI components that just want the final motion.
141
+ */
142
+ export async function generatePose(req, opts) {
143
+ const { job_id } = await submitTextToPose(req);
144
+ return waitForJob(job_id, opts);
145
+ }
146
+ // ── Cache ──────────────────────────────────────────────────────────────────
147
+ export async function listPoseCache() {
148
+ const res = await apiFetch("/poser/text-to-pose/cache");
149
+ if (!res.ok)
150
+ throw new Error(`Cache list failed: HTTP ${res.status}`);
151
+ return res.json();
152
+ }
153
+ export async function deletePoseCacheEntry(hash) {
154
+ const res = await apiFetch(`/poser/text-to-pose/cache/${hash}`, {
155
+ method: "DELETE",
156
+ });
157
+ if (!res.ok && res.status !== 404) {
158
+ throw new Error(`Cache delete failed: HTTP ${res.status}`);
159
+ }
160
+ }
161
+ export async function clearPoseCache() {
162
+ const res = await apiFetch("/poser/text-to-pose/cache/clear", {
163
+ method: "POST",
164
+ });
165
+ if (!res.ok)
166
+ throw new Error(`Cache clear failed: HTTP ${res.status}`);
167
+ const body = await res.json();
168
+ return body.cleared;
169
+ }
170
+ export async function listModels() {
171
+ const res = await apiFetch("/poser/text-to-pose/models");
172
+ if (!res.ok)
173
+ throw new Error(`Models list failed: HTTP ${res.status}`);
174
+ return res.json();
175
+ }
176
+ export async function listRegistry() {
177
+ const res = await apiFetch("/poser/text-to-pose/registry");
178
+ if (!res.ok)
179
+ throw new Error(`Registry fetch failed: HTTP ${res.status}`);
180
+ return res.json();
181
+ }
182
+ export async function listExamples(model) {
183
+ const q = model ? `?model=${encodeURIComponent(model)}` : "";
184
+ const res = await apiFetch(`/poser/text-to-pose/examples${q}`);
185
+ if (!res.ok)
186
+ throw new Error(`Examples list failed: HTTP ${res.status}`);
187
+ return res.json();
188
+ }
189
+ export async function loadExample(model, exampleId, loadGt = false) {
190
+ const q = loadGt ? `?load_gt=true` : "";
191
+ const res = await apiFetch(`/poser/text-to-pose/examples/${model}/${exampleId}${q}`);
192
+ if (!res.ok)
193
+ throw new Error(`Example load failed: HTTP ${res.status}`);
194
+ return res.json();
195
+ }
196
+ // ── BVH export ─────────────────────────────────────────────────────────────
197
+ export async function exportBVH(framesSoma77, framesRotmat, fps) {
198
+ const res = await apiFetch("/poser/text-to-pose/export/bvh", {
199
+ method: "POST",
200
+ headers: { "Content-Type": "application/json" },
201
+ body: JSON.stringify({
202
+ frames_soma77_3d: framesSoma77,
203
+ frames_soma77_rotmat: framesRotmat,
204
+ fps,
205
+ }),
206
+ });
207
+ if (!res.ok) {
208
+ const body = await res.json();
209
+ throw new Error(body.error || `BVH export failed: HTTP ${res.status}`);
210
+ }
211
+ return res.json();
212
+ }
213
+ /** Download a BVH file from SOMA-77 motion data. */
214
+ export async function downloadBVH(motion, standardTpose = false) {
215
+ if (!motion.frames_soma77_3d || !motion.frames_soma77_rotmat) {
216
+ throw new Error("Motion data missing SOMA-77 positions or rotations");
217
+ }
218
+ // Re-fetch BVH with the standard_tpose option if requested.
219
+ const res = await apiFetch("/poser/text-to-pose/export/bvh", {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/json" },
222
+ body: JSON.stringify({
223
+ frames_soma77_3d: motion.frames_soma77_3d,
224
+ frames_soma77_rotmat: motion.frames_soma77_rotmat,
225
+ fps: motion.fps,
226
+ standard_tpose: standardTpose,
227
+ }),
228
+ });
229
+ if (!res.ok) {
230
+ const body = await res.json();
231
+ throw new Error(body.error || `BVH export failed: HTTP ${res.status}`);
232
+ }
233
+ const { bvh } = await res.json();
234
+ const blob = new Blob([bvh], { type: "text/plain" });
235
+ const url = URL.createObjectURL(blob);
236
+ const a = document.createElement("a");
237
+ a.href = url;
238
+ a.download = `pose_${motion.model}_${Date.now()}.bvh`;
239
+ a.click();
240
+ URL.revokeObjectURL(url);
241
+ }
242
+
243
+ /** Export motion as AMASS NPZ (SMPL-X compatible). */
244
+ export async function downloadAMASSNpz(motion) {
245
+ if (!motion.frames_soma77_3d || !motion.frames_soma77_rotmat) {
246
+ throw new Error("Motion data missing SOMA-77 positions or rotations");
247
+ }
248
+ const res = await apiFetch("/poser/text-to-pose/export/amass", {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify({
252
+ frames_soma77_3d: motion.frames_soma77_3d,
253
+ frames_soma77_rotmat: motion.frames_soma77_rotmat,
254
+ fps: motion.fps,
255
+ }),
256
+ });
257
+ if (!res.ok) {
258
+ const body = await res.json();
259
+ throw new Error(body.error || `AMASS export failed: HTTP ${res.status}`);
260
+ }
261
+ const { data_b64 } = await res.json();
262
+ downloadBase64Payload(data_b64, `pose_amass_${Date.now()}.npz`, "application/octet-stream");
263
+ }
264
+
265
+ export async function downloadG1Csv(motion) {
266
+ if (!motion.frames_soma77_3d || !motion.frames_soma77_rotmat) {
267
+ throw new Error("Motion data missing SOMA-77 positions or rotations");
268
+ }
269
+ const res = await apiFetch("/poser/text-to-pose/export/csv", {
270
+ method: "POST",
271
+ headers: { "Content-Type": "application/json" },
272
+ body: JSON.stringify({
273
+ frames_soma77_3d: motion.frames_soma77_3d,
274
+ frames_soma77_rotmat: motion.frames_soma77_rotmat,
275
+ fps: motion.fps,
276
+ }),
277
+ });
278
+ if (!res.ok) {
279
+ const body = await res.json();
280
+ throw new Error(body.error || `CSV export failed: HTTP ${res.status}`);
281
+ }
282
+ const { data_b64 } = await res.json();
283
+ downloadBase64Payload(data_b64, `pose_g1_${Date.now()}.csv`, "text/csv");
284
+ }
285
+ /**
286
+ * Export motion as a rigged FBX file (Mixamo-compatible joint names).
287
+ *
288
+ * The backend renders an FBX 7.4.0 ASCII skeleton with the full 77-joint
289
+ * hierarchy and baked animation — importable by Blender, Maya, Unity, and
290
+ * Unreal. Mixamo naming enables automatic retargeting onto Mixamo rigs.
291
+ */
292
+ export async function downloadFBX(motion, mixamo = true) {
293
+ if (!motion.frames_soma77_3d || !motion.frames_soma77_rotmat) {
294
+ throw new Error("Motion data missing SOMA-77 positions or rotations");
295
+ }
296
+ const res = await apiFetch("/poser/text-to-pose/export/fbx", {
297
+ method: "POST",
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({
300
+ frames_soma77_3d: motion.frames_soma77_3d,
301
+ frames_soma77_rotmat: motion.frames_soma77_rotmat,
302
+ fps: motion.fps,
303
+ mixamo,
304
+ }),
305
+ });
306
+ if (!res.ok) {
307
+ const body = await res.json();
308
+ throw new Error(body.error || `FBX export failed: HTTP ${res.status}`);
309
+ }
310
+ const { fbx } = await res.json();
311
+ const blob = new Blob([fbx], { type: "application/octet-stream" });
312
+ const url = URL.createObjectURL(blob);
313
+ const a = document.createElement("a");
314
+ a.href = url;
315
+ a.download = `pose_${motion.model}_${Date.now()}.fbx`;
316
+ a.click();
317
+ URL.revokeObjectURL(url);
318
+ }
319
+ /**
320
+ * Render a 2D pose video server-side via matplotlib + imageio.
321
+ * Higher-quality and browser-independent than client-side MediaRecorder.
322
+ */
323
+ export async function renderServerVideo(motion, opts = {}) {
324
+ const res = await apiFetch("/poser/text-to-pose/export/video", {
325
+ method: "POST",
326
+ headers: { "Content-Type": "application/json" },
327
+ body: JSON.stringify({
328
+ frames_flat: motion.frames_flat,
329
+ fps: motion.fps,
330
+ width: opts.width ?? 512,
331
+ height: opts.height ?? 512,
332
+ background: opts.background ?? "#0d2847",
333
+ }),
334
+ });
335
+ if (!res.ok) {
336
+ const body = await res.json();
337
+ throw new Error(body.error || `Video render failed: HTTP ${res.status}`);
338
+ }
339
+ const { video_b64, mime } = await res.json();
340
+ const bin = atob(video_b64);
341
+ const bytes = new Uint8Array(bin.length);
342
+ for (let i = 0; i < bin.length; i++)
343
+ bytes[i] = bin.charCodeAt(i);
344
+ return new Blob([bytes], { type: mime || "video/mp4" });
345
+ }
346
+ /** Render and download a server-side 2D pose video. */
347
+ export async function downloadServerVideo(motion, opts) {
348
+ const blob = await renderServerVideo(motion, opts);
349
+ const url = URL.createObjectURL(blob);
350
+ const a = document.createElement("a");
351
+ a.href = url;
352
+ a.download = `pose_video_${Date.now()}.mp4`;
353
+ a.click();
354
+ URL.revokeObjectURL(url);
355
+ }
356
+ export async function listConstraints() {
357
+ const res = await apiFetch("/poser/text-to-pose/constraints");
358
+ if (!res.ok)
359
+ throw new Error(`Constraints list failed: HTTP ${res.status}`);
360
+ const body = await res.json();
361
+ return body.entries ?? [];
362
+ }
363
+ export async function saveConstraints(name, constraints) {
364
+ const res = await apiFetch("/poser/text-to-pose/constraints/save", {
365
+ method: "POST",
366
+ headers: { "Content-Type": "application/json" },
367
+ body: JSON.stringify({ name, constraints }),
368
+ });
369
+ if (!res.ok) {
370
+ const body = await res.json();
371
+ throw new Error(body.error || `Save failed: HTTP ${res.status}`);
372
+ }
373
+ }
374
+ export async function loadConstraints(filename) {
375
+ const res = await apiFetch(`/poser/text-to-pose/constraints/${filename}`);
376
+ if (!res.ok)
377
+ throw new Error(`Load failed: HTTP ${res.status}`);
378
+ const body = await res.json();
379
+ return body.constraints;
380
+ }
381
+ export async function deleteConstraints(filename) {
382
+ const res = await apiFetch(`/poser/text-to-pose/constraints/${filename}`, {
383
+ method: "DELETE",
384
+ });
385
+ if (!res.ok && res.status !== 404) {
386
+ throw new Error(`Delete failed: HTTP ${res.status}`);
387
+ }
388
+ }
389
+ export async function listScenes() {
390
+ const res = await apiFetch("/poser/text-to-pose/scenes");
391
+ if (!res.ok)
392
+ throw new Error(`Scenes list failed: HTTP ${res.status}`);
393
+ const body = await res.json();
394
+ return body.entries ?? [];
395
+ }
396
+ export async function saveScene(name, scene) {
397
+ const res = await apiFetch("/poser/text-to-pose/scenes/save", {
398
+ method: "POST",
399
+ headers: { "Content-Type": "application/json" },
400
+ body: JSON.stringify({ name, scene }),
401
+ });
402
+ if (!res.ok) {
403
+ const body = await res.json();
404
+ throw new Error(body.error || `Save failed: HTTP ${res.status}`);
405
+ }
406
+ }
407
+ export async function loadScene(filename) {
408
+ const res = await apiFetch(`/poser/text-to-pose/scenes/${filename}`);
409
+ if (!res.ok)
410
+ throw new Error(`Load failed: HTTP ${res.status}`);
411
+ const body = await res.json();
412
+ return body.scene;
413
+ }
414
+ export async function deleteScene(filename) {
415
+ const res = await apiFetch(`/poser/text-to-pose/scenes/${filename}`, {
416
+ method: "DELETE",
417
+ });
418
+ if (!res.ok && res.status !== 404) {
419
+ throw new Error(`Delete failed: HTTP ${res.status}`);
420
+ }
421
+ }
422
+ // ── Helpers ────────────────────────────────────────────────────────────────
423
+ /**
424
+ * Pick a specific frame from a PoseMotion's flat keypoints.
425
+ * Clamps to valid range.
426
+ */
427
+ export function pickFrame(motion, idx) {
428
+ const clamped = Math.max(0, Math.min(idx, motion.frame_count - 1));
429
+ return motion.frames_flat[clamped];
430
+ }
431
+ // ── Interactive re-skinning (edit mode) ────────────────────────────────────
432
+ /**
433
+ * Fetch the 77-joint SOMA-71 rest-pose (bind-pose) positions for the
434
+ * requested identity model. Used by Pose Studio's "Load Model" button to
435
+ * enter Edit Mode WITHOUT generating a motion first — the user can grab
436
+ * joints and pose the character manually.
437
+ *
438
+ * The endpoint accepts both GET (returns the neutral-default joints) and
439
+ * POST (applies the supplied identity morphs first, returns joints that
440
+ * match the shaped body). The POST path is the fully-dynamic one: Anny's
441
+ * height/proportions sliders reshape BOTH the mesh AND the skeleton, so
442
+ * the edit-mode wires always match the body the user sees. Without it the
443
+ * skeleton would stay one fixed size (the static SOMA-77 reference) while
444
+ * the mesh grew/shrank underneath it.
445
+ *
446
+ * Calls GET /poser/skin/bind_joints?identity_model=… when no identity spec
447
+ * is supplied, POST /poser/skin/bind_joints with a JSON body otherwise.
448
+ */
449
+ export async function fetchBindJoints(identityModel = "anny",
450
+ /**
451
+ * Optional identity spec — when ANY of these is supplied, the call
452
+ * switches to POST and the backend applies them via
453
+ * `SomaXSkin.set_identity(coeffs, local_changes=, custom_targets=)`
454
+ * before reading joints, so the returned positions match the shaped
455
+ * body. Mirrors the deformSomaJoints / shaped-mesh contract.
456
+ */
457
+ identityCoeffs, localChanges, customTargets) {
458
+ const hasSpec = (identityCoeffs && identityCoeffs.length > 0) ||
459
+ (localChanges && Object.keys(localChanges).length > 0) ||
460
+ (customTargets && Object.keys(customTargets).length > 0);
461
+ if (!hasSpec) {
462
+ const res = await apiFetch(`/poser/skin/bind_joints?identity_model=${encodeURIComponent(identityModel)}`);
463
+ if (!res.ok) {
464
+ const txt = await res.text();
465
+ throw new Error(`Failed to load bind joints: ${res.status} ${txt}`);
466
+ }
467
+ return res.json();
468
+ }
469
+ const body = { identity_model: identityModel };
470
+ if (identityCoeffs && identityCoeffs.length > 0)
471
+ body.identity_coeffs = identityCoeffs;
472
+ if (localChanges && Object.keys(localChanges).length > 0)
473
+ body.local_changes = localChanges;
474
+ if (customTargets && Object.keys(customTargets).length > 0)
475
+ body.custom_targets = customTargets;
476
+ const res = await apiFetch("/poser/skin/bind_joints", {
477
+ method: "POST",
478
+ headers: { "Content-Type": "application/json" },
479
+ body: JSON.stringify(body),
480
+ });
481
+ if (!res.ok) {
482
+ const txt = await res.text();
483
+ throw new Error(`Failed to load bind joints: ${res.status} ${txt}`);
484
+ }
485
+ return res.json();
486
+ }
487
+ /**
488
+ * Preload (warm up) a model on the GPU so the first Generate is fast.
489
+ *
490
+ * NOTE: The old forge /admin/load endpoint was removed in the 3-container
491
+ * architecture cleanup. The KimodoPoseService singleton inside ComfyUI
492
+ * now loads lazily on first inference and stays resident, so explicit
493
+ * preloading is unnecessary. This is kept as a no-op so the frontend's
494
+ * "Load Model" button doesn't error.
495
+ */
496
+ export async function preloadForgeService(_service, _model) {
497
+ return { status: "ok", service: _service };
498
+ }
499
+ /**
500
+ * Re-skin the mesh for edited SOMA-77 joint positions.
501
+ * Calls /poser/skin/deform_soma with binary response for speed.
502
+ * Used during interactive pose editing — debounced by the caller.
503
+ *
504
+ * @param joints Float32Array of 77 x 3 joint positions
505
+ * @param identityModel Which mesh to skin ("soma" | "mhr" | "anny"). Must
506
+ * match the mesh the viewer is rendering, otherwise the deformed vertices
507
+ * won't match the bind-mesh topology and the pose will look wrong.
508
+ * @param jointsRot Optional Float32Array of 77 x 9 global rotation matrices
509
+ * (row-major 3x3 per joint). When provided, the backend skins directly with
510
+ * these exact rotations (most accurate). When omitted, the backend derives
511
+ * approximate rotations from bone directions.
512
+ * @returns base64-encoded float32 deformed vertices (V x 3) for one frame
513
+ */
514
+ export async function deformSomaJoints(joints, identityModel = "anny", jointsRot,
515
+ /**
516
+ * Identity spec — when supplied, the backend applies them via
517
+ * `SomaXSkin.set_identity(coeffs, local_changes=, custom_targets=)` before
518
+ * skinning so the re-skinned verts match the loaded character (not the
519
+ * default soma bind pose). Without these, edit-mode re-skin would snap the
520
+ * mesh back to the un-morphed body on the first joint drag. All three are
521
+ * optional and model-specific (Anny uses all three; MHR ignores
522
+ * local_changes/custom_targets; SOMA ignores all three).
523
+ */
524
+ identityCoeffs, localChanges, customTargets,
525
+ /**
526
+ * Pre-edit global rotation matrices (77×9, row-major 3×3 per joint) captured
527
+ * BEFORE the user dragged any joint. When supplied (and `jointsRot` is null),
528
+ * the backend recomputes the swing (bone direction) from `joints` while
529
+ * preserving the twist (roll around the bone axis) from these — so dragging
530
+ * a joint updates the bone direction without erasing forearm pronation,
531
+ * neck yaw, etc. This is the edit-mode drag path; `jointsRot` (explicit
532
+ * rotations) takes precedence when both are set.
533
+ */
534
+ baseRotmats) {
535
+ // Send as flat nested array (77 joints only - small payload)
536
+ const frames = [];
537
+ for (let j = 0; j < 77; j++) {
538
+ frames.push([joints[j * 3], joints[j * 3 + 1], joints[j * 3 + 2]]);
539
+ }
540
+ const body = {
541
+ frames_soma77_3d: [frames],
542
+ identity_model: identityModel,
543
+ binary: true,
544
+ };
545
+ // Include the identity spec when supplied so the re-skin matches the
546
+ // loaded character. The backend's deform_soma handler threads these
547
+ // through SomaXSkin.set_identity before LBS — same path as the shaped
548
+ // bind-mesh POST and the generate skinning call.
549
+ if (identityCoeffs && identityCoeffs.length > 0) {
550
+ body.identity_coeffs = identityCoeffs;
551
+ }
552
+ if (localChanges && Object.keys(localChanges).length > 0) {
553
+ body.local_changes = localChanges;
554
+ }
555
+ if (customTargets && Object.keys(customTargets).length > 0) {
556
+ body.custom_targets = customTargets;
557
+ }
558
+ // Pack rotation matrices as (1, 77, 3, 3) nested array when provided.
559
+ if (jointsRot && jointsRot.length === 77 * 9) {
560
+ const rots = _packRotmats(jointsRot);
561
+ body.frames_soma77_rotmat = [rots];
562
+ }
563
+ // Pre-edit rotations: swing recomputed from positions, twist preserved.
564
+ // Sent as a separate field so the backend can apply the swing-twist
565
+ // decomposition rather than using them directly (jointsRot above).
566
+ if (baseRotmats && baseRotmats.length === 77 * 9) {
567
+ const rots = _packRotmats(baseRotmats);
568
+ body.frames_soma77_base_rotmat = [rots];
569
+ }
570
+ const res = await apiFetch("/poser/skin/deform_soma", {
571
+ method: "POST",
572
+ headers: { "Content-Type": "application/json" },
573
+ body: JSON.stringify(body),
574
+ });
575
+ if (!res.ok) {
576
+ const txt = await res.text();
577
+ throw new Error(`Re-skin failed: ${res.status} ${txt}`);
578
+ }
579
+ const data = await res.json();
580
+ if (data.error)
581
+ throw new Error(data.error);
582
+ return data.vertices_b64;
583
+ }
584
+ /**
585
+ * Pack a flat (77×9,) Float32Array of row-major 3×3 rotation matrices into
586
+ * the (77, 3, 3) nested-array shape the deform_soma endpoint expects.
587
+ * Shared by the `frames_soma77_rotmat` and `frames_soma77_base_rotmat` fields.
588
+ */
589
+ function _packRotmats(flat) {
590
+ const rots = [];
591
+ for (let j = 0; j < 77; j++) {
592
+ const r = [];
593
+ for (let row = 0; row < 3; row++) {
594
+ r.push([
595
+ flat[j * 9 + row * 3],
596
+ flat[j * 9 + row * 3 + 1],
597
+ flat[j * 9 + row * 3 + 2],
598
+ ]);
599
+ }
600
+ rots.push(r);
601
+ }
602
+ return rots;
603
+ }
604
+ // ── SOMA-77 skeleton bones (for overlay rendering) ────────────────────────
605
+ // Joint name → parent-joint index, matching vendor/kimodo/kimodo/skeleton/soma_skeleton.py
606
+ // SOMASkeleton77 bone order. Used by PoseViewer3D to draw bone lines.
607
+ //# sourceMappingURL=textToPose.js.map
@@ -0,0 +1,25 @@
1
+ export declare class UploadRefusedError extends Error {
2
+ }
3
+ /** The rail: File → POST /v1/uploads → the estate path. */
4
+ export declare function uploadToEstate(file: File): Promise<string>;
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+ export declare function estateInputUrl(value: string): string | null;
25
+ //# sourceMappingURL=uploadToEstate.d.ts.map