@melite/formats 0.1.4-alpha → 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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ ---
2
+ description: Pure browser-side media codecs and math — NPZ, skins, poses, masks, image metadata — fetch-free by law.
3
+ kind: package-library
4
+ ---
5
+
6
+ # formats
7
+
8
+ `@melite/formats` holds the browser-side media codecs and math the studios share: motion files, character skins, pose codecs, mask prep, image metadata. Pure transforms by law — no render loops, no server coupling; `three` types appear only where geometry demands.
9
+
10
+ ## Contract
11
+
12
+ The package root re-exports the leaves; each is also a declared subpath. The manifest also declares the standard `./package.json` re-export.
13
+
14
+ - Motion — `./npz` (the minimal NPZ reader), `./motionNpz` (the motion card's client-side engine), `./motionEnvelope` (audio-reactive amplitude), `./motionConstraints` (the constraint snapshot core), and `./keyframeExtract` (anchor-frame picks).
15
+ - Skins and rig — `./somaSkin` (neutral SOMA-77 linear-blend skinning), `./glbSkin` (a SOMAX-rigged GLB onto the same substrate), `./poserSkin` (the shared bind-mesh), `./handPose` (SOMA-77 finger presets), and `./glbLoad` (the one DRACO-configured GLTFLoader).
16
+ - Pose — `./poseCodec` (byte codecs and bone topology), `./poseTransforms` (COCO-18 transforms), `./openPoseCanvas` (the stick-figure renderer), `./textToPose` (the poser client), `./bodyPresets` (labeled archetype presets), and `./downloads` (artifact export triggers).
17
+ - Media plumbing — `./pngMeta` and `./pngMetaInfo` (PNG tEXt chunk embed and read), `./b64`, `./extractVideoFrame`, `./captionSplitter`, `./promptTags`, `./renderSnapshot` (the one-frame scene-to-JPEG snapshotter), `./psdExport` (the layered PSD writer), `./maskPrep`, `./cardMutations` (the declarative card-mutation gate), `./readFile` (the one slot-attach read), and `./uploadToEstate` (the one browser-file to estate-path rail).
18
+
19
+ ## Model Experience
20
+
21
+ ### Request context and condition
22
+
23
+ #### What the model sees
24
+
25
+ The codecs contribute nothing. `./textToPose` submits a caller's prompt segments to the poser backend for downstream pose generation; `./uploadToEstate` moves bytes the user chose.
26
+
27
+ #### Token effect
28
+
29
+ Conditional — only the text-to-pose path sends prompts for downstream generation; the codecs and the upload rail cost nothing.
30
+
31
+ #### KV Cache effect
32
+
33
+ Independent — the poser calls are one-shot jobs, not conversation turns.
34
+
35
+ ## Known Limitations and Deferred Work
36
+
37
+ - **Minimal format coverage by design** — the NPZ reader and the PSD writer cover the estate's own subset (8-bit RGB layers); broader format support lands only when a surface needs it.
38
+ - **No `./invariant` companion** — the codecs are deterministic pure functions over their inputs; there is no cross-package runtime relationship to assert.
package/lib/index.d.ts CHANGED
@@ -21,6 +21,5 @@ export * from "./motionEnvelope.js";
21
21
  export * from "./extractVideoFrame.js";
22
22
  export * from "./keyframeExtract.js";
23
23
  export * from "./handPose.js";
24
- export * from "./capabilityCards.js";
25
24
  export * from "./cardMutations.js";
26
25
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -21,6 +21,5 @@ export * from "./motionEnvelope.js";
21
21
  export * from "./extractVideoFrame.js";
22
22
  export * from "./keyframeExtract.js";
23
23
  export * from "./handPose.js";
24
- export * from "./capabilityCards.js";
25
24
  export * from "./cardMutations.js";
26
25
  //# sourceMappingURL=index.js.map
@@ -51,13 +51,6 @@ export declare function renderCustomPose(keypoints: Keypoints, opts?: {
51
51
  width?: number;
52
52
  height?: number;
53
53
  }): Promise<Blob>;
54
- /**
55
- * Convert a Blob to a base64 data URL — for saving to the asset store
56
- * (which expects data URLs, not object URLs, for persistence).
57
- * Delegates to the shared readAsDataURL (@melite/ui SlotSquare — the
58
- * ONE read path every slot-shaped attach uses).
59
- */
60
- export declare function blobToDataUrl(blob: Blob): Promise<string>;
61
54
  /**
62
55
  * Convert 2D flat COCO-18 keypoints to approximate 3D positions.
63
56
  *
@@ -2,7 +2,7 @@
2
2
  * Pose Studio — keypoint transform utilities.
3
3
  *
4
4
  * COCO-18 keypoints: 18 joints × (x, y, confidence) = 54 numbers in a flat
5
- * array. This is the same format DWPose outputs (see media/workflows/utils/dwpose.py)
5
+ * array. This is the same COCO-18 format an upstream DWPose estimator outputs
6
6
  * and the same format the /poser/* backend endpoints accept.
7
7
  *
8
8
  * The Pose Studio UI lets the user pick a base preset, then tweak it with
@@ -14,7 +14,6 @@
14
14
  * have smaller X values than left-side ones).
15
15
  */
16
16
  import { apiFetch } from "@melite/catalog";
17
- import { readAsDataURL } from "./readFile.js";
18
17
  // ── COCO-18 keypoint indices ─────────────────────────────────────────────
19
18
  export const KP = {
20
19
  NOSE: 0, NECK: 1,
@@ -165,15 +164,6 @@ export async function renderCustomPose(keypoints, opts = {}) {
165
164
  }
166
165
  return res.blob();
167
166
  }
168
- /**
169
- * Convert a Blob to a base64 data URL — for saving to the asset store
170
- * (which expects data URLs, not object URLs, for persistence).
171
- * Delegates to the shared readAsDataURL (@melite/ui SlotSquare — the
172
- * ONE read path every slot-shaped attach uses).
173
- */
174
- export function blobToDataUrl(blob) {
175
- return readAsDataURL(blob);
176
- }
177
167
  // ── 2D → approximate 3D projection ──────────────────────────────────────────
178
168
  /** SOMA coordinate space constants. */
179
169
  const SOMA_SCALE = 0.6; // maps 1024px width to ~0.6 SOMA units (hip-to-hip)
package/lib/poserSkin.js CHANGED
@@ -22,7 +22,11 @@ import { apiFetch } from "@melite/catalog";
22
22
  // entry Pose Studio needs, zero network). All shaped meshes share SOMA's
23
23
  // 18056-vertex layout; the cache key incorporates identity_model +
24
24
  // coeffs + local_changes + custom_targets + preview_pose so identical
25
- // shapes hit the cache on re-fetch.
25
+ // shapes hit the cache on re-fetch. BOUNDED (the registry-growth law):
26
+ // one entry is a full mesh (megabytes), a slider drag mints a
27
+ // signature per stop, so an unbounded module Map would grow with the
28
+ // session — LRU at MESH_CACHE_CAP, the oldest insertion pruned.
29
+ const MESH_CACHE_CAP = 16;
26
30
  const _meshCache = new Map();
27
31
  export async function fetchBindMesh(query = {}) {
28
32
  const { identityModel, identityCoeffs, localChanges, previewPose, customTargets, } = query;
@@ -51,8 +55,12 @@ export async function fetchBindMesh(query = {}) {
51
55
  const poseSig = previewPose && previewPose !== "tpose" ? previewPose : null;
52
56
  const cacheKey = [key, coeffsSig, lcEntries, ctEntries, poseSig].filter(Boolean).join("|");
53
57
  const cached = _meshCache.get(cacheKey);
54
- if (cached)
58
+ if (cached) {
59
+ // LRU, not FIFO-of-birth: a re-read refreshes the insertion
60
+ _meshCache.delete(cacheKey);
61
+ _meshCache.set(cacheKey, cached);
55
62
  return cached;
63
+ }
56
64
  const posePayload = poseSig ? { preview_pose: poseSig } : {};
57
65
  let data;
58
66
  if (coeffsSig || lcEntries || ctEntries) {
@@ -87,6 +95,13 @@ export async function fetchBindMesh(query = {}) {
87
95
  throw new Error(`Failed to fetch mesh (${key}): ${res.status}`);
88
96
  data = (await res.json());
89
97
  }
98
+ if (_meshCache.size >= MESH_CACHE_CAP) {
99
+ // the map is non-empty at cap — take the OLDEST insertion
100
+ for (const oldest of _meshCache.keys()) {
101
+ _meshCache.delete(oldest);
102
+ break;
103
+ }
104
+ }
90
105
  _meshCache.set(cacheKey, data);
91
106
  return data;
92
107
  }
@@ -161,7 +161,7 @@ export declare function submitTextToPose(req: TextToPoseRequest): Promise<Submit
161
161
  /**
162
162
  * Poll job status once. Returns the current status + result if completed.
163
163
  */
164
- export declare function pollJob(jobId: string): Promise<JobStatusResponse>;
164
+ export declare function pollJob(jobId: string, signal?: AbortSignal): Promise<JobStatusResponse>;
165
165
  /**
166
166
  * Wait for a job to reach a terminal state (completed/failed/cancelled).
167
167
  * Polls every `intervalMs` until `timeoutMs`.
@@ -172,6 +172,11 @@ export declare function waitForJob(jobId: string, opts?: {
172
172
  intervalMs?: number;
173
173
  timeoutMs?: number;
174
174
  onUpdate?: (status: JobStatusResponse) => void;
175
+
176
+
177
+
178
+
179
+ signal?: AbortSignal;
175
180
  }): Promise<TextToPoseResult>;
176
181
  export declare function cancelJob(jobId: string): Promise<void>;
177
182
  /**
@@ -182,6 +187,7 @@ export declare function generatePose(req: TextToPoseRequest, opts?: {
182
187
  intervalMs?: number;
183
188
  timeoutMs?: number;
184
189
  onUpdate?: (status: JobStatusResponse) => void;
190
+ signal?: AbortSignal;
185
191
  }): Promise<TextToPoseResult>;
186
192
  export declare function listPoseCache(): Promise<CacheListResponse>;
187
193
  export declare function deletePoseCacheEntry(hash: string): Promise<void>;
package/lib/textToPose.js CHANGED
@@ -80,8 +80,8 @@ export async function submitTextToPose(req) {
80
80
  /**
81
81
  * Poll job status once. Returns the current status + result if completed.
82
82
  */
83
- export async function pollJob(jobId) {
84
- const res = await apiFetch(`/poser/text-to-pose/jobs/${jobId}`);
83
+ export async function pollJob(jobId, signal) {
84
+ const res = await apiFetch(`/poser/text-to-pose/jobs/${jobId}`, { signal });
85
85
  if (res.status === 404) {
86
86
  throw new Error(`Job ${jobId} not found`);
87
87
  }
@@ -106,8 +106,11 @@ export async function waitForJob(jobId, opts = {}) {
106
106
  const interval = opts.intervalMs ?? 500;
107
107
  const timeout = opts.timeoutMs ?? 120_000; // 2 min default (inference is 15-30s)
108
108
  const deadline = Date.now() + timeout;
109
+ const signal = opts.signal;
109
110
  while (Date.now() < deadline) {
110
- const status = await pollJob(jobId);
111
+ if (signal?.aborted)
112
+ throw new DOMException("Aborted", "AbortError");
113
+ const status = await pollJob(jobId, signal);
111
114
  opts.onUpdate?.(status);
112
115
  if (status.status === "completed" && status.result) {
113
116
  return status.result;
@@ -118,7 +121,7 @@ export async function waitForJob(jobId, opts = {}) {
118
121
  if (status.status === "cancelled") {
119
122
  throw new Error("Job was cancelled");
120
123
  }
121
- await sleep(interval);
124
+ await sleep(interval, signal);
122
125
  }
123
126
  throw new Error(`Job ${jobId} timed out after ${timeout}ms`);
124
127
  }
package/package.json CHANGED
@@ -3,9 +3,9 @@
3
3
  "license": "SSPL-1.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@melite/catalog": "0.1.4-alpha",
7
- "@melite/contracts": "0.1.4-alpha",
8
- "@melite/three-client": "0.1.4-alpha"
6
+ "@melite/catalog": "0.1.5-pre-alpha",
7
+ "@melite/contracts": "0.1.5-pre-alpha",
8
+ "@melite/three-client": "0.1.5-pre-alpha"
9
9
  },
10
10
  "exports": {
11
11
  "./b64": {
@@ -16,10 +16,6 @@
16
16
  "types": "./lib/bodyPresets.d.ts",
17
17
  "default": "./lib/bodyPresets.js"
18
18
  },
19
- "./capabilityCards": {
20
- "types": "./lib/capabilityCards.d.ts",
21
- "default": "./lib/capabilityCards.js"
22
- },
23
19
  "./captionSplitter": {
24
20
  "types": "./lib/captionSplitter.d.ts",
25
21
  "default": "./lib/captionSplitter.js"
@@ -37,10 +33,12 @@
37
33
  "default": "./lib/extractVideoFrame.js"
38
34
  },
39
35
  "./glbLoad": {
40
- "types": "./lib/glbLoad.d.ts"
36
+ "types": "./lib/glbLoad.d.ts",
37
+ "default": "./lib/glbLoad.js"
41
38
  },
42
39
  "./glbSkin": {
43
- "types": "./lib/glbSkin.d.ts"
40
+ "types": "./lib/glbSkin.d.ts",
41
+ "default": "./lib/glbSkin.js"
44
42
  },
45
43
  "./handPose": {
46
44
  "types": "./lib/handPose.d.ts",
@@ -63,7 +61,8 @@
63
61
  "default": "./lib/motionEnvelope.js"
64
62
  },
65
63
  "./motionNpz": {
66
- "types": "./lib/motionNpz.d.ts"
64
+ "types": "./lib/motionNpz.d.ts",
65
+ "default": "./lib/motionNpz.js"
67
66
  },
68
67
  "./npz": {
69
68
  "types": "./lib/npz.d.ts",
@@ -115,17 +114,19 @@
115
114
  "default": "./lib/textToPose.js"
116
115
  },
117
116
  "./renderSnapshot": {
118
- "types": "./lib/renderSnapshot.d.ts"
117
+ "types": "./lib/renderSnapshot.d.ts",
118
+ "default": "./lib/renderSnapshot.js"
119
119
  },
120
120
  "./somaSkin": {
121
121
  "types": "./lib/somaSkin.d.ts",
122
122
  "default": "./lib/somaSkin.js"
123
123
  },
124
124
  ".": {
125
- "types": "./lib/index.d.ts"
125
+ "types": "./lib/index.d.ts",
126
+ "default": "./lib/index.js"
126
127
  }
127
128
  },
128
- "version": "0.1.4-alpha",
129
+ "version": "0.1.5-pre-alpha",
129
130
  "types": "./src/index.ts",
130
131
  "devDependencies": {
131
132
  "@types/node": "^22.20.0",
@@ -1,54 +0,0 @@
1
- import { type CapabilityEntry, type CapabilityRoute, type Catalog, type FamilyEntry } from "@melite/catalog";
2
- import type { CardGenre } from "@melite/contracts";
3
- /** A card ready to land on the canvas (what onPick hands the store).
4
- *
5
- * ``kind`` is optional — capabilities/families always produce
6
- * ``"generation"`` (the default), so the existing helpers don't set it
7
- * and the canvas defaults to "generation". The Input / Media Prep tile
8
- * sets ``kind: "input"`` so the canvas routes the stage through the
9
- * doc-driven input family (POST /v1/flows/start, 1-step inline
10
- * doc) — NOT the generation wire. This is rule 4: the shape carries
11
- * the kind, so pick → addStage never silently misroutes. */
12
- export interface PickedCard {
13
- service: string;
14
- title: string;
15
- genre?: CardGenre;
16
- params: Record<string, unknown>;
17
- kind?: string;
18
- }
19
- /**
20
- * Seed params for a route: the backend-computed defaults + an empty prompt
21
- * ONLY when the model's form actually serves a prompt field
22
- * (prompt_in_form — backend-computed from the builder schema) + a null
23
- * seed for EVERY input slot the model declares (input_slots — multi-slot
24
- * forms like edit's three images / video's two frames get one null per
25
- * slot, so the card renders all its squares from the start). Unknown
26
- * prompt_in_form → seed (old behavior).
27
- */
28
- export declare function seededParams(cat: Catalog | null, route: string, base?: Record<string, unknown>): Record<string, unknown>;
29
- /**
30
- * Resolve a capability tile (+ optional route override) to a card. The
31
- * default tile click uses cap.resolved; the tile's model switcher passes a
32
- * different CapabilityRoute — same capability intent, model switched
33
- * underneath. The route's params are backend-precomputed (capability
34
- * default_params as fallback, model's own defaults winning), so switching
35
- * the model correctly swaps the per-model recipe (steps / cfg / sampler).
36
- */
37
- export declare function capabilityToCard(cat: Catalog | null, cap: CapabilityEntry, route: CapabilityRoute): PickedCard;
38
- /** Resolve a specific family entry (the "All models" list) to a card. */
39
- export declare function familyEntryToCard(cat: Catalog | null, familyId: string, fam: FamilyEntry): PickedCard;
40
- export declare const CAPABILITY_DRAG_MIME = "application/tech-noir-capability";
41
- /** What a capability tile writes to the drag data store. Carries the fully
42
- * resolved card (service + title + params + kind) so the drop target can
43
- * apply it WITHOUT a catalog lookup — the sidebar already paid that cost
44
- * via capabilityToCard. ``capId`` is diagnostics-only (not read by the
45
- * drop target; useful for e2e traces). */
46
- export type CapabilityDropPayload = PickedCard & {
47
- capId?: string;
48
- };
49
- /** Serialize a picked card onto the drag data store. */
50
- export declare function capabilityDragPayload(capId: string, card: PickedCard): string;
51
- /** Parse the capability MIME off a drop event. null = absent or malformed
52
- * (the drop target falls through to the asset/file path). */
53
- export declare function parseCapabilityDrop(raw: string): CapabilityDropPayload | null;
54
- //# sourceMappingURL=capabilityCards.d.ts.map
@@ -1,127 +0,0 @@
1
- // capabilityCards — the ONE capability→card derivation path.
2
- //
3
- // A capability tile (Generate / Edit / Upscale / ...) resolves to a card
4
- // descriptor: the backend-precomputed default model route + merged params
5
- // (capabilities.<id>.resolved). This module is the single implementation —
6
- // the ServicePicker dialog AND the Library's right sidebar (LibrarySidebar)
7
- // both derive their "what lands on the canvas" from these helpers, so a
8
- // catalog or schema change cannot make the two surfaces drift.
9
- //
10
-
11
- // • an empty prompt is seeded ONLY when the model's form actually serves
12
- // a prompt field (prompt_in_form — backend-computed from the builder
13
- // schema); a prompt-less card (upscale) must never carry one
14
- // • a null input slot is seeded when the model's schema has an input
15
- // (the card modal renders the attach control for it)
16
- import { familyGenre, narrowGenre } from "@melite/catalog";
17
- /**
18
- * Seed params for a route: the backend-computed defaults + an empty prompt
19
- * ONLY when the model's form actually serves a prompt field
20
- * (prompt_in_form — backend-computed from the builder schema) + a null
21
- * seed for EVERY input slot the model declares (input_slots — multi-slot
22
- * forms like edit's three images / video's two frames get one null per
23
- * slot, so the card renders all its squares from the start). Unknown
24
- * prompt_in_form → seed (old behavior).
25
- */
26
- export function seededParams(cat, route, base = {}) {
27
- const model = cat?.models[route];
28
- const slots = model?.input_slots?.length
29
- ? model.input_slots
30
- : (model?.input ? [model.input] : []);
31
- const hasPrompt = model?.prompt_in_form ?? true; // unknown → seed (old behavior)
32
- return {
33
- ...base,
34
- ...(hasPrompt ? { prompt: "" } : {}),
35
- ...(slots.length ? Object.fromEntries(slots.map((k) => [k, null])) : {}),
36
- };
37
- }
38
- /**
39
- * Resolve a capability tile (+ optional route override) to a card. The
40
- * default tile click uses cap.resolved; the tile's model switcher passes a
41
- * different CapabilityRoute — same capability intent, model switched
42
- * underneath. The route's params are backend-precomputed (capability
43
- * default_params as fallback, model's own defaults winning), so switching
44
- * the model correctly swaps the per-model recipe (steps / cfg / sampler).
45
- */
46
- export function capabilityToCard(cat, cap, route) {
47
- // Model-less capabilities (kind:"input") carry their params VERBATIM — no
48
- // prompt/slot seeding. Those are generation-form concerns (prompt_in_form,
49
- // input_slots); input's editor is the fixed-field InputCardEditor, which
50
- // owns crop/scale/trim/volume/mask. Seeding a prompt here would give the
51
-
52
- // regression, generalized). The kind rides through to PickedCard so the
53
- // canvas routes the stage through executeInput, not executeGeneration.
54
- const isModelLess = cap.kind !== undefined && cap.kind !== "generation";
55
- return {
56
- service: route.model,
57
- title: cap.label,
58
- genre: genreForRoute(cat, route.model),
59
- params: isModelLess ? { ...route.params } : seededParams(cat, route.model, route.params),
60
- ...(isModelLess ? { kind: cap.kind } : {}),
61
- };
62
- }
63
- /** Resolve a specific family entry (the "All models" list) to a card. */
64
- export function familyEntryToCard(cat, familyId, fam) {
65
- // Route resolution — the SAME chain the backend's capability resolver
66
- // and familiesForForm use: the catalog-declared ``default_model`` (when
67
- // it is a member), else the member whose key equals the family id, else
68
- // the FIRST member. NEVER the bare familyId: mask_studio's family id
69
- // and model key differ (mask_studio vs mask-studio), and a family-id
70
- // service is a PHANTOM ROUTE — no models[] entry, no familyGenre, and
71
- // no studio registry match (the "mask studio does not appear" bug,
72
-
73
- // card zoomed as a plain form card). The catalog now declares
74
- // default_model for every id-divergent family; this chain is the
75
- // backstop for the next one.
76
- const declared = fam.default_model;
77
- const members = Object.entries(cat?.models ?? {})
78
- .filter(([, m]) => m.family === familyId)
79
- .map(([k]) => k);
80
- const route = (declared && members.includes(declared) ? declared : undefined) ??
81
- members.find((k) => k === familyId) ??
82
- members[0] ??
83
- familyId;
84
- return {
85
- service: route,
86
- title: fam.label ?? familyId,
87
- genre: narrowGenre(fam.genre) ?? genreForRoute(cat, route),
88
- params: seededParams(cat, route, cat?.models[route]?.defaults ?? {}),
89
- };
90
- }
91
- /** Genre for a model route — the catalog's familyGenre derivation. */
92
- function genreForRoute(cat, route) {
93
- return cat ? familyGenre(route, cat) : undefined;
94
- }
95
-
96
- // A capability tile in the LibrarySidebar is DRAGGABLE onto a card in the
97
- // canvas — "clicking and dragging from the right sidebar onto the middle
98
- // part should populate the card". The drop REPURPOSES the target card: its
99
- // service + params + title become the dragged capability's resolved card
100
- // (the same shape a click would `addStage`, applied to an EXISTING stage).
101
- //
102
- // The MIME is deliberately distinct from `application/tech-noir-asset`
103
- // (AppSidebar's asset drag) so a card's drop handler can tell "populate
104
- // this card's identity" (capability) from "attach an asset to a slot"
105
- // (asset) — one drop target, two contracts, no ambiguity.
106
- export const CAPABILITY_DRAG_MIME = "application/tech-noir-capability";
107
- /** Serialize a picked card onto the drag data store. */
108
- export function capabilityDragPayload(capId, card) {
109
- return JSON.stringify({ ...card, capId });
110
- }
111
- /** Parse the capability MIME off a drop event. null = absent or malformed
112
- * (the drop target falls through to the asset/file path). */
113
- export function parseCapabilityDrop(raw) {
114
- if (!raw)
115
- return null;
116
- try {
117
- const p = JSON.parse(raw);
118
- if (typeof p.service === "string" && typeof p.title === "string" && p.params && typeof p.params === "object") {
119
- return p;
120
- }
121
- return null;
122
- }
123
- catch { /* not our payload — the JSON probe answers absent (row 28) */
124
- return null;
125
- }
126
- }
127
- //# sourceMappingURL=capabilityCards.js.map