@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 +38 -0
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -1
- package/lib/poseTransforms.d.ts +0 -7
- package/lib/poseTransforms.js +1 -11
- package/lib/poserSkin.js +17 -2
- package/lib/textToPose.d.ts +7 -1
- package/lib/textToPose.js +7 -4
- package/package.json +14 -13
- package/lib/capabilityCards.d.ts +0 -54
- package/lib/capabilityCards.js +0 -127
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
|
package/lib/poseTransforms.d.ts
CHANGED
|
@@ -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
|
*
|
package/lib/poseTransforms.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/lib/textToPose.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
7
|
-
"@melite/contracts": "0.1.
|
|
8
|
-
"@melite/three-client": "0.1.
|
|
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.
|
|
129
|
+
"version": "0.1.5-pre-alpha",
|
|
129
130
|
"types": "./src/index.ts",
|
|
130
131
|
"devDependencies": {
|
|
131
132
|
"@types/node": "^22.20.0",
|
package/lib/capabilityCards.d.ts
DELETED
|
@@ -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
|
package/lib/capabilityCards.js
DELETED
|
@@ -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
|