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