@melite/models-mask 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.
- package/LICENSE +557 -0
- package/README.md +42 -2
- package/lib/actions.d.ts +61 -0
- package/lib/actions.js +502 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +109 -0
- package/package.json +34 -4
package/lib/actions.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { impactDetector } from '@melite/comfyui-card-kit/impact';
|
|
2
|
+
const isSpell = (s) => s.startsWith('$');
|
|
3
|
+
const personDetector = impactDetector('person');
|
|
4
|
+
const faceDetector = impactDetector('face');
|
|
5
|
+
const handDetector = impactDetector('hand');
|
|
6
|
+
/** The family's three weight names — the lane's own constants (the
|
|
7
|
+
* LANE_FILES pattern, the h3 precedent: clip/vae_video/vae_audio
|
|
8
|
+
* never ride a row either). No variant of this family swaps a
|
|
9
|
+
* weight and no user picks one, so they live beside the build that
|
|
10
|
+
* wires them; the card's provisions declare the downloads, and
|
|
11
|
+
* image/mask/tests/card.spec.ts pins the agreement (a weight the
|
|
12
|
+
* lane loads that no provision downloads is a silent
|
|
13
|
+
* wrong-weights run — the same law the variant cells carry). */
|
|
14
|
+
export const MASK_WEIGHTS = {
|
|
15
|
+
checkpoint: 'sam3.1_multiplex_fp16.safetensors',
|
|
16
|
+
unet: 'rt_detr_v4-x-hgnet_fp32.safetensors',
|
|
17
|
+
sam2: 'sam2.1_hiera_large.pt',
|
|
18
|
+
};
|
|
19
|
+
/** The family's facts door: the lane reads NOTHING off a row (the
|
|
20
|
+
* three weights are the family's constants above), so the door
|
|
21
|
+
* returns them closed — an empty loader name is unrepresentable,
|
|
22
|
+
* which is the old refusal law (a pin that is not a non-empty
|
|
23
|
+
* string refused here) held by construction. The registry still
|
|
24
|
+
* exercises this door at registration over the empty family row.
|
|
25
|
+
* @param _raw the resolved lane_inputs (empty on this family)
|
|
26
|
+
* @returns the three weights the lane wires into its loaders */
|
|
27
|
+
export function parseMaskLaneFacts(_raw) {
|
|
28
|
+
return { checkpoint: MASK_WEIGHTS.checkpoint, unet: MASK_WEIGHTS.unet, sam2: MASK_WEIGHTS.sam2 };
|
|
29
|
+
}
|
|
30
|
+
/** THE TUNE VOCABULARY IS DERIVED FROM ITS OWN TYPE (the census's
|
|
31
|
+
* L1): the runtime list and the compile-time type were written side
|
|
32
|
+
* by side, so adding a key to one and not the other was silent — the
|
|
33
|
+
* gate accepted a key the type did not carry (or refused one it
|
|
34
|
+
* did). The keys are the type's own members now; the assertion
|
|
35
|
+
* below fails if the two ever part company. */
|
|
36
|
+
export const TUNE_KEYS = [
|
|
37
|
+
'threshold', 'refine', 'dilation', 'detection_threshold', 'box_expansion',
|
|
38
|
+
'hint_threshold', 'detection_dilation', 'crop_factor', 'drop_size',
|
|
39
|
+
];
|
|
40
|
+
/** THE MASK SELECTION ACTION (the family's one declared lane): the
|
|
41
|
+
* input_image admission stays on the card; this is the lane alone.
|
|
42
|
+
* TWO selection families: the SAM3 CLIP/RT-DETR leg (checkpoint +
|
|
43
|
+
* unet weights) and the SAM2 impact-pack arm (sam2 + the pack's own
|
|
44
|
+
* specialized detectors — person on segm, face/hand on their bbox
|
|
45
|
+
* models — the detector proposes, SAM2 refines). */
|
|
46
|
+
export const select = {
|
|
47
|
+
id: "select",
|
|
48
|
+
inputs: [],
|
|
49
|
+
outputs: [{ name: "default", kind: "MASK" }],
|
|
50
|
+
params: [],
|
|
51
|
+
facts: parseMaskLaneFacts,
|
|
52
|
+
build({ p, facts, g, runId }) {
|
|
53
|
+
const li = { checkpoint: facts.checkpoint, unet: facts.unet, sam2: facts.sam2 }; // the family's weight constants (MASK_WEIGHTS above)
|
|
54
|
+
const prefix = `${runId}/mask`; // the arm's "%s/mask" % run_id
|
|
55
|
+
const samThreshold = p.sam_threshold;
|
|
56
|
+
// undeclared legacy renderer keys ride the door's explicit-params
|
|
57
|
+
// overlay (see schema header) — schema-required paint_mask renders
|
|
58
|
+
// ''-absent at compose (image-gen precedent), refine_iterations
|
|
59
|
+
// falls back to its old schema pin 2 when no explicit names it.
|
|
60
|
+
const refineIterations = (p.refine_iterations ?? 2);
|
|
61
|
+
const dilation = p.sam_dilation;
|
|
62
|
+
|
|
63
|
+
// ImpactSimpleDetectorSEGS: sub_bbox_expansion +
|
|
64
|
+
// sam_mask_hint_threshold were hard-pinned and unexposed): the
|
|
65
|
+
// lane-flat seat is the PRIMARY sam2's fold; absent (a sam3
|
|
66
|
+
// primary, pre-fold benches) = the pack's own defaults.
|
|
67
|
+
const bboxExpansion = p.box_expansion ?? 0;
|
|
68
|
+
const hintThreshold = p.hint_threshold ?? 0.7;
|
|
69
|
+
|
|
70
|
+
// three hard-pinned knobs): node-faithful defaults (10 / 3 / 10 —
|
|
71
|
+
// what the lane always shipped), now per-entry overridable.
|
|
72
|
+
const detectionDilation = p.detection_dilation ?? 10;
|
|
73
|
+
const cropFactor = p.crop_factor ?? 3.0;
|
|
74
|
+
const dropSize = p.drop_size ?? 10;
|
|
75
|
+
const exportBoxes = p.export_boxes;
|
|
76
|
+
const viaRtdetr = p.primary_via_rtdetr;
|
|
77
|
+
// the lazy loader trio (see the header note — created on first use)
|
|
78
|
+
// THE SPELLED ANCHOR (the mask-as-consumer half of the gap
|
|
79
|
+
// audit: a producer-wired input_image rides every tensor slot
|
|
80
|
+
// verbatim, never a LoadImage — ROW-4 refuses producer links in
|
|
81
|
+
// file slots, so the loader builds for paths only).
|
|
82
|
+
let imgH = null;
|
|
83
|
+
let ckpt = null;
|
|
84
|
+
let rtdetrModel = null;
|
|
85
|
+
const img = () => (imgH ??= isSpell(p.input_image)
|
|
86
|
+
? p.input_image
|
|
87
|
+
: g.node('LoadImage', { image: p.input_image }).out(0));
|
|
88
|
+
const ckptNode = () => (ckpt ??= g.node('CheckpointLoaderSimple', { ckpt_name: li.checkpoint }));
|
|
89
|
+
const rtdetrLoader = () => (rtdetrModel ??= g.node('UNETLoader', { unet_name: li.unet, weight_dtype: 'default' }).out(0));
|
|
90
|
+
const clipCond = (text) => g.node('CLIPTextEncode', { text, clip: ckptNode().out(1) }).out(0);
|
|
91
|
+
const detect = (conditioning, bboxes, tune) => {
|
|
92
|
+
const inputs = {
|
|
93
|
+
model: ckptNode().out(0),
|
|
94
|
+
image: img(),
|
|
95
|
+
threshold: tune?.threshold ?? samThreshold,
|
|
96
|
+
refine_iterations: tune?.refine ?? refineIterations,
|
|
97
|
+
individual_masks: false,
|
|
98
|
+
};
|
|
99
|
+
if (conditioning)
|
|
100
|
+
inputs.conditioning = conditioning;
|
|
101
|
+
|
|
102
|
+
// the old DEFAULT_BBOXES autofilled {0,0,512,512} on every
|
|
103
|
+
// unscoped detection — a top-left QUARTER "hint" on any image
|
|
104
|
+
// larger than 512, silently blinding the selection everywhere
|
|
105
|
+
// else (an empty mask on a 1024px input, proven live).
|
|
106
|
+
// Unscoped = bboxes rides ABSENT = search the WHOLE image; the
|
|
107
|
+
// drawn Region scope (and RT-DETR's own boxes) still thread as
|
|
108
|
+
// the explicit hint when they exist.
|
|
109
|
+
if (bboxes !== undefined)
|
|
110
|
+
inputs.bboxes = bboxes;
|
|
111
|
+
return g.node('SAM3_Detect', inputs).out(0); // (masks, bboxes) → masks
|
|
112
|
+
};
|
|
113
|
+
const dilate = (mask, d) => (d ?? dilation) ? g.node('GrowMask', { mask, expand: d ?? dilation, tapered_corners: true }).out(0) : mask;
|
|
114
|
+
// the lane's declared output is the mask it saved last (the card's
|
|
115
|
+
// output binding reads that SaveImage); the handle rides the same
|
|
116
|
+
// node, so nothing is created, moved or renumbered by declaring it
|
|
117
|
+
let saved = null;
|
|
118
|
+
const save = (mask, name, d) => {
|
|
119
|
+
const m2i = g.node('MaskToImage', { mask: dilate(mask, d) });
|
|
120
|
+
saved = g.node('SaveImage', { images: m2i.out(0), filename_prefix: `${prefix}/${name}` }).out(0);
|
|
121
|
+
};
|
|
122
|
+
// THE RECT SCOPE (the drawn box — the bbox-canvas leaf's
|
|
123
|
+
// [x, y, w, h] pixels): a zeros W×H mask plus a ones w×h box at
|
|
124
|
+
// (x, y) (MaskComposite add), multiplied over the selection. CORE
|
|
125
|
+
// nodes only (SolidMask + MaskComposite + GetImageSizeAndCount —
|
|
126
|
+
// all live on every engine; the scope never depends on the
|
|
127
|
+
// impact arm that made the mask).
|
|
128
|
+
const rectMask = (scope) => {
|
|
129
|
+
const size = g.node('GetImageSizeAndCount', { image: img() });
|
|
130
|
+
const zeros = g.node('SolidMask', { value: 0, width: size.out(1), height: size.out(2) }).out(0);
|
|
131
|
+
const ones = g.node('SolidMask', { value: 1, width: scope.width, height: scope.height }).out(0);
|
|
132
|
+
return g.node('MaskComposite', { destination: zeros, source: ones, x: scope.x, y: scope.y, operation: 'add' }).out(0);
|
|
133
|
+
};
|
|
134
|
+
const scopeMask = (mask, scope) => g.node('MaskComposite', { destination: mask, source: rectMask(scope), x: 0, y: 0, operation: 'multiply' }).out(0);
|
|
135
|
+
// THE SAM2 IMPACT-PACK ARM (the operator's SAM2: the detector
|
|
136
|
+
// proposes + SAM2 refines; the mask card declares the packs +
|
|
137
|
+
// weights, the compose door refuses without them — never a
|
|
138
|
+
// silent CLIP leg). UltralyticsDetectorProvider (subpack, the
|
|
139
|
+
// card's PINNED detector model: segm/person for person,
|
|
140
|
+
// bbox/face + bbox/hand for face/hand) → SegmDetectorSEGS or
|
|
141
|
+
// BboxDetectorSEGS (image + the detector boxes + the card's
|
|
142
|
+
// labels) → SEGS → SAMDetectorCombined (PINNED sam2 model,
|
|
143
|
+
// center-1 — the pack's own best-practice default) → MASK. The
|
|
144
|
+
// drawn box scopes after (the rect scope above).
|
|
145
|
+
let samH = null;
|
|
146
|
+
let segmH = null;
|
|
147
|
+
let faceH = null;
|
|
148
|
+
let handH = null;
|
|
149
|
+
const samLoader = () => (samH ??= g.node('SAMLoader', { model_name: li.sam2, device_mode: 'AUTO' }).out(0));
|
|
150
|
+
|
|
151
|
+
// pack's own detailer-pipe-test wires SegmDetectorSEGS — a
|
|
152
|
+
// segmentation detector whose boxes come from CLASS-PIXEL masks
|
|
153
|
+
// (tighter crops, no center-point guess). The detector model
|
|
154
|
+
// comes from the card kit's detector catalog
|
|
155
|
+
|
|
156
|
+
// downloaded + provision-declared that night).
|
|
157
|
+
const segmProvider = () =>
|
|
158
|
+
|
|
159
|
+
// wiring it into SegmDetectorSEGS.segm_detector 400s the run
|
|
160
|
+
// with return_type_mismatch; the provider's own output list
|
|
161
|
+
// is ["BBOX_DETECTOR", "SEGM_DETECTOR"]).
|
|
162
|
+
(segmH ??= g.node(personDetector.providerNode, { model_name: personDetector.graphPath }).out(personDetector.providerOutput));
|
|
163
|
+
const faceProvider = () => (faceH ??= g.node(faceDetector.providerNode, { model_name: faceDetector.graphPath }).out(faceDetector.providerOutput));
|
|
164
|
+
const handProvider = () => (handH ??= g.node(handDetector.providerNode, { model_name: handDetector.graphPath }).out(handDetector.providerOutput));
|
|
165
|
+
|
|
166
|
+
// "ONLY the ones shown in the Impact Pack examples" — person,
|
|
167
|
+
// face, hand, each on the pack's OWN specialized detector, exactly
|
|
168
|
+
// as detailer-pipe-test.json wires them). A mixed set (person +
|
|
169
|
+
// face) cannot share one detector call — split it across two sam2
|
|
170
|
+
// entries, each with its own export. Refuses LOUD, never a
|
|
171
|
+
// half-detected entry.
|
|
172
|
+
const sam2detect = (labels, scope, tune) => {
|
|
173
|
+
if (labels === '') {
|
|
174
|
+
throw new Error('maskT1: a sam2 selection needs labels (a detector class word — the card refuses empty)');
|
|
175
|
+
}
|
|
176
|
+
const words = labels.split(',').map((w) => w.trim()).filter((w) => w !== '');
|
|
177
|
+
const fams = new Set(words.map((w) => w === 'person' ? 'segm' : 'bbox'));
|
|
178
|
+
if (fams.size > 1) {
|
|
179
|
+
throw new Error(`maskT1: one detector per sam2 entry — '${labels}' mixes detector families (person rides segm, face/hand ride bbox). Split it across two sam2 entries.`);
|
|
180
|
+
}
|
|
181
|
+
const segmLane = words[0] === 'person';
|
|
182
|
+
// (ImpactSimpleDetectorSEGS was AUDITED and REJECTED for this
|
|
183
|
+
// lane: it carries NO labels input — the class filter, the
|
|
184
|
+
// operator's core demand, cannot ride it. The pack's own
|
|
185
|
+
// labels-filtering test workflow (detailer-pipe-test.json)
|
|
186
|
+
// wires BboxDetectorSEGS + SAMDetectorCombined exactly as
|
|
187
|
+
// this lane does.)
|
|
188
|
+
// THE WORD→FACTS MAP (the stroke's pin law, verbatim shape):
|
|
189
|
+
// person rides the segm facts, face its bbox facts, EVERYTHING
|
|
190
|
+
// else (hand, the 'all' wildcard, unstated words) the hand bbox
|
|
191
|
+
// facts with labels verbatim — the pre-stroke ternary's exact
|
|
192
|
+
// graph, only the pin source moved (caller strings → pack facts).
|
|
193
|
+
const detector = segmLane ? personDetector
|
|
194
|
+
: words[0] === 'face' ? faceDetector : handDetector;
|
|
195
|
+
const provider = segmLane ? segmProvider()
|
|
196
|
+
: words[0] === 'face' ? faceProvider() : handProvider();
|
|
197
|
+
const segs = g.node(detector.detectorNode, {
|
|
198
|
+
[detector.detectorInput]: provider,
|
|
199
|
+
image: img(),
|
|
200
|
+
threshold: (tune?.detection_threshold ?? p.detection_threshold ?? 0.5),
|
|
201
|
+
dilation: tune?.detection_dilation ?? detectionDilation,
|
|
202
|
+
crop_factor: tune?.crop_factor ?? cropFactor,
|
|
203
|
+
drop_size: tune?.drop_size ?? dropSize,
|
|
204
|
+
labels,
|
|
205
|
+
}).out(0);
|
|
206
|
+
// THE PER-INSTANCE CROPS (the gap audit's #3/#4 export half):
|
|
207
|
+
// SEGSToImageList unwraps one crop per detected instance —
|
|
208
|
+
// PreviewImage saves each — the per-instance readback the
|
|
209
|
+
// pack's own examples ride (SEGSToImageList in
|
|
210
|
+
// detailer-pipe-test + 5-prompt-per-tile). Automatic: the
|
|
211
|
+
// crops ARE the manifest's granular sibling.
|
|
212
|
+
g.node('SaveImage', {
|
|
213
|
+
images: g.node('SEGSToImageList', { segs }).out(0),
|
|
214
|
+
filename_prefix: `${runId}/instance`,
|
|
215
|
+
});
|
|
216
|
+
const refined = g.node('SAMDetectorCombined', {
|
|
217
|
+
sam_model: samLoader(),
|
|
218
|
+
segs,
|
|
219
|
+
image: img(),
|
|
220
|
+
detection_hint: 'center-1',
|
|
221
|
+
dilation: tune?.dilation ?? dilation,
|
|
222
|
+
threshold: tune?.threshold ?? samThreshold,
|
|
223
|
+
bbox_expansion: tune?.box_expansion ?? bboxExpansion,
|
|
224
|
+
mask_hint_threshold: tune?.hint_threshold ?? hintThreshold,
|
|
225
|
+
mask_hint_use_negative: 'False',
|
|
226
|
+
}).out(0);
|
|
227
|
+
const mask = scope ? scopeMask(refined, scope) : refined;
|
|
228
|
+
return { mask, segs };
|
|
229
|
+
};
|
|
230
|
+
let primaryMask = null;
|
|
231
|
+
let primarySegs = null;
|
|
232
|
+
// THE PRIMARY SCOPE (the first standalone entry's drawn box —
|
|
233
|
+
// the card threads it as scope_bbox; absent = unscoped). Every
|
|
234
|
+
// SELECTION primary honors it; the paint primary never does (the
|
|
235
|
+
// strokes ARE the mask — a scope would cut them).
|
|
236
|
+
const scopeQuad = p.scope_bbox;
|
|
237
|
+
const primaryScope = scopeQuad && scopeQuad.length === 4
|
|
238
|
+
? { x: scopeQuad[0], y: scopeQuad[1], width: scopeQuad[2], height: scopeQuad[3] }
|
|
239
|
+
: undefined;
|
|
240
|
+
// ── the RT-DETR proposal lane (primary body + box manifest) ──
|
|
241
|
+
let rtdetrBoxes = null;
|
|
242
|
+
if (exportBoxes || viaRtdetr) {
|
|
243
|
+
|
|
244
|
+
// NO CAP ("0 = no cap" on the card's schema) and `|| 100` both
|
|
245
|
+
// capped it and coerced every falsy value — so a default-omitted
|
|
246
|
+
// request silently dropped detections past 100.
|
|
247
|
+
const stated = p.max_instances;
|
|
248
|
+
const cap = stated === undefined || stated === 0 ? 0 : stated;
|
|
249
|
+
rtdetrBoxes = g.node('RTDETR_detect', {
|
|
250
|
+
model: rtdetrLoader(),
|
|
251
|
+
image: img(),
|
|
252
|
+
threshold: p.detection_threshold,
|
|
253
|
+
class_name: 'person',
|
|
254
|
+
max_detections: cap,
|
|
255
|
+
}).out(0);
|
|
256
|
+
}
|
|
257
|
+
const primaryClass = p.primary_class;
|
|
258
|
+
const primaryPrompt = p.primary_prompt;
|
|
259
|
+
const viaSam2 = p.primary_via_sam2;
|
|
260
|
+
const primarySam2Labels = p.primary_sam2_labels;
|
|
261
|
+
if (viaRtdetr && rtdetrBoxes) {
|
|
262
|
+
primaryMask = detect(undefined, rtdetrBoxes);
|
|
263
|
+
if (primaryScope)
|
|
264
|
+
primaryMask = scopeMask(primaryMask, primaryScope);
|
|
265
|
+
save(primaryMask, 'mask_combined');
|
|
266
|
+
}
|
|
267
|
+
else if (viaSam2) {
|
|
268
|
+
const det = sam2detect(primarySam2Labels ?? 'all', primaryScope);
|
|
269
|
+
primaryMask = det.mask;
|
|
270
|
+
primarySegs = det.segs;
|
|
271
|
+
save(primaryMask, 'mask_combined');
|
|
272
|
+
|
|
273
|
+
// going to select figure one or figure two"): the pack's own
|
|
274
|
+
// ImpactSEGSPicker takes the primary detection's SEGS + a
|
|
275
|
+
// 1-based pick ("2" = the second detected instance) and each
|
|
276
|
+
// figure saves its own refined mask (mask_figure_<n>) — the
|
|
277
|
+
// agent selects a figure by composing with figures: [2], no
|
|
278
|
+
// manual box anywhere. The per-instance image crops
|
|
279
|
+
// (run/instance) are the visual index into the same numbering.
|
|
280
|
+
const figs = p.sam2_figures;
|
|
281
|
+
if (figs !== undefined) {
|
|
282
|
+
if (!Array.isArray(figs) || figs.some((n) => !Number.isInteger(n) || n < 1)) {
|
|
283
|
+
throw new Error(`maskT1: sam2_figures must be 1-based instance numbers — got ${JSON.stringify(figs).slice(0, 60)}`);
|
|
284
|
+
}
|
|
285
|
+
for (const n of [...new Set(figs)]) {
|
|
286
|
+
const picked = g.node('ImpactSEGSPicker', { picks: String(n), segs: primarySegs }).out(0);
|
|
287
|
+
// THE FIGURES RIDE THE SAME TWO-STEP (the operator's
|
|
288
|
+
|
|
289
|
+
// YOLO/Segm, THEN extract the mask off the filtered area"):
|
|
290
|
+
// the picker NARROWS the SEGS to figure <n>, then
|
|
291
|
+
// SAMDetectorCombined refines THAT subset exactly as the
|
|
292
|
+
// combined arm does — per-figure SAM masks for EVERY class
|
|
293
|
+
// (person's YOLO segm OR face/hand's bare bbox crops alike;
|
|
294
|
+
// SegsToCombinedMask alone would have rendered step-one
|
|
295
|
+
// masks — rectangles on the bbox classes). Same loader
|
|
296
|
+
// node, same pack-test scalars, the primary's own
|
|
297
|
+
// dilation/threshold.
|
|
298
|
+
const figMask = g.node('SAMDetectorCombined', {
|
|
299
|
+
sam_model: samLoader(),
|
|
300
|
+
segs: picked,
|
|
301
|
+
image: img(),
|
|
302
|
+
detection_hint: 'center-1',
|
|
303
|
+
dilation,
|
|
304
|
+
threshold: samThreshold,
|
|
305
|
+
bbox_expansion: bboxExpansion,
|
|
306
|
+
mask_hint_threshold: hintThreshold,
|
|
307
|
+
mask_hint_use_negative: 'False',
|
|
308
|
+
}).out(0);
|
|
309
|
+
save(primaryScope ? scopeMask(figMask, primaryScope) : figMask, `mask_figure_${n}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (primaryClass) {
|
|
314
|
+
// the drawn Region threads as the detect HINT too (the wall
|
|
315
|
+
// caught it: exports threaded theirs, the primary did not —
|
|
316
|
+
// the hint narrows the search, the multiply guarantees bounds)
|
|
317
|
+
primaryMask = detect(clipCond(primaryClass), primaryScope);
|
|
318
|
+
if (primaryScope)
|
|
319
|
+
primaryMask = scopeMask(primaryMask, primaryScope);
|
|
320
|
+
save(primaryMask, 'mask_combined');
|
|
321
|
+
}
|
|
322
|
+
else if (primaryPrompt) {
|
|
323
|
+
primaryMask = detect(clipCond(primaryPrompt), primaryScope);
|
|
324
|
+
if (primaryScope)
|
|
325
|
+
primaryMask = scopeMask(primaryMask, primaryScope);
|
|
326
|
+
save(primaryMask, 'mask_combined');
|
|
327
|
+
}
|
|
328
|
+
else if (p.primary_is_paint) {
|
|
329
|
+
// NOTE: the engine primary `painted` and the exports' `paint_masked`
|
|
330
|
+
// are DISTINCT locals — a paint primary plus a paint export renders
|
|
331
|
+
// TWO LoadImageMask nodes (byte-identical inputs, two nodes). The
|
|
332
|
+
// probe pins this.
|
|
333
|
+
const painted = g.node('LoadImageMask', { image: (p.paint_mask ?? ''), channel: 'alpha' }).out(0);
|
|
334
|
+
primaryMask = painted;
|
|
335
|
+
save(painted, 'mask_combined');
|
|
336
|
+
}
|
|
337
|
+
// ── per-name exports ──
|
|
338
|
+
// the third arm (optional) = the per-entry bbox [x, y, w, h];
|
|
339
|
+
// the fourth arm (optional) = the per-entry TUNE (stroke 95 —
|
|
340
|
+
// every knob a mask source owns rides its bag entry): threshold
|
|
341
|
+
// / refine / dilation / detection_threshold overrides for THIS
|
|
342
|
+
// export's selection. Malformed refuses LOUD (a drifted bag is
|
|
343
|
+
// drift, never a skip); absent arms ride the lane-flat values
|
|
344
|
+
// (the primary's tune — the card threads it flat).
|
|
345
|
+
const exports = p.exports ?? [];
|
|
346
|
+
const kinds = p.export_kind ?? [];
|
|
347
|
+
const entryTune = (raw) => {
|
|
348
|
+
if (raw === undefined || raw === null)
|
|
349
|
+
return undefined;
|
|
350
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
351
|
+
throw new Error(`maskT1: an export tune must be an object (threshold/refine/dilation/detection_threshold) — got ${JSON.stringify(raw).slice(0, 60)}`);
|
|
352
|
+
}
|
|
353
|
+
const t = raw;
|
|
354
|
+
|
|
355
|
+
// once covered only 4 of 9 keys — the 5 newer knobs flowed
|
|
356
|
+
// unvalidated to node inputs, an engine 400 instead of a loud
|
|
357
|
+
// lane refusal). Every value in a tune is a finite number.
|
|
358
|
+
// the key set is the type's own members (TUNE_KEYS is derived),
|
|
359
|
+
// so the membership test narrows the runtime string
|
|
360
|
+
const known = new Set(TUNE_KEYS);
|
|
361
|
+
const unknown = Object.keys(t).filter((k) => !known.has(k));
|
|
362
|
+
if (unknown.length > 0) {
|
|
363
|
+
throw new Error(`maskT1: an export tune carries unknown key(s) ${unknown.join(', ')} — the vocabulary is ${TUNE_KEYS.join('/')}`);
|
|
364
|
+
}
|
|
365
|
+
for (const key of Object.keys(t)) {
|
|
366
|
+
const v = t[key];
|
|
367
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
368
|
+
throw new Error(`maskT1: an export tune's '${key}' must be a finite number — got ${JSON.stringify(v).slice(0, 40)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return t;
|
|
372
|
+
};
|
|
373
|
+
// SCOPE VALIDATION (the lane's own gate — the card threads the
|
|
374
|
+
// primary entry's box here; a malformed quad refuses LOUD, never
|
|
375
|
+
// a silently unscoped primary).
|
|
376
|
+
if (scopeQuad && scopeQuad.length !== 4) {
|
|
377
|
+
throw new Error(`scope_bbox refused on the mask-studio card: needs exactly [x, y, w, h] pixels (got ${scopeQuad.length} values)`);
|
|
378
|
+
}
|
|
379
|
+
// PER-ENTRY PAINT PAYLOADS (the ordered-draw law, stroke 78): each
|
|
380
|
+
// draw owns its PNG — cached by path (two draws on one payload
|
|
381
|
+
// render ONE LoadImageMask; two payloads render two).
|
|
382
|
+
const paintByPath = new Map();
|
|
383
|
+
const loadPaint = (path) => {
|
|
384
|
+
let m = paintByPath.get(path);
|
|
385
|
+
if (m === undefined) {
|
|
386
|
+
m = g.node('LoadImageMask', { image: path, channel: 'alpha' }).out(0);
|
|
387
|
+
paintByPath.set(path, m);
|
|
388
|
+
}
|
|
389
|
+
return m;
|
|
390
|
+
};
|
|
391
|
+
// THE PER-ENTRY BBOX (stroke 78): the export's THIRD arm —
|
|
392
|
+
// [x, y, w, h] pixels, the bbox-canvas leaf's drawn rectangle.
|
|
393
|
+
// Malformed refuses LOUD (a drifted bag is drift, never a skip);
|
|
394
|
+
// absent falls to the scope, then the renderer default.
|
|
395
|
+
const entryBbox = (raw) => {
|
|
396
|
+
if (raw === undefined || raw === null)
|
|
397
|
+
return undefined;
|
|
398
|
+
if (!Array.isArray(raw) || raw.length !== 4) {
|
|
399
|
+
throw new Error(`maskT1: an export bbox must be [x, y, w, h] pixels — got ${JSON.stringify(raw).slice(0, 60)}`);
|
|
400
|
+
}
|
|
401
|
+
const quad = raw.map(Number);
|
|
402
|
+
if (quad.some((v) => !Number.isFinite(v))) {
|
|
403
|
+
throw new Error(`maskT1: an export bbox needs four finite numbers — got ${JSON.stringify(raw).slice(0, 60)}`);
|
|
404
|
+
}
|
|
405
|
+
return { x: quad[0], y: quad[1], width: quad[2], height: quad[3] };
|
|
406
|
+
};
|
|
407
|
+
const n = Math.min(exports.length, kinds.length); // zip: the shorter arm wins
|
|
408
|
+
for (let i = 0; i < n; i++) {
|
|
409
|
+
const trip = exports[i];
|
|
410
|
+
const name = trip[0];
|
|
411
|
+
const payload = trip[1];
|
|
412
|
+
const kind = kinds[i];
|
|
413
|
+
if (kind === 'paint') {
|
|
414
|
+
// per-entry first (the draw's own PNG), the lane-global paint
|
|
415
|
+
// second (the studio flat-mode contract stays)
|
|
416
|
+
const path = typeof payload === 'string' && payload !== '' ? payload : (p.paint_mask ?? '');
|
|
417
|
+
if (path === '') {
|
|
418
|
+
throw new Error(`maskT1: paint export '${name}' needs a payload — the drawn PNG is the mask`);
|
|
419
|
+
}
|
|
420
|
+
save(loadPaint(path), `mask_${name}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
let conditioning;
|
|
424
|
+
if (kind !== 'sam2' && payload) {
|
|
425
|
+
// the text lane rides EVEN under scope (boxes to segment within
|
|
426
|
+
// + the vocabulary) — a scope never silently drops the prompt.
|
|
427
|
+
// sam2 exports skip it (their payload is the detector labels
|
|
428
|
+
// word — the detector proposes, SAM2 refines, never CLIP).
|
|
429
|
+
conditioning = clipCond(payload);
|
|
430
|
+
}
|
|
431
|
+
// THE PER-ENTRY BOX (stroke 78): the export's OWN third arm —
|
|
432
|
+
// the card-wide scope_bbox is the PRIMARY's box and never leaks
|
|
433
|
+
// into an export (an export without its own box runs unscoped).
|
|
434
|
+
const bboxes = entryBbox(trip[2]);
|
|
435
|
+
// THE PER-ENTRY TUNE (stroke 95): the export's OWN fourth arm.
|
|
436
|
+
const tune = entryTune(trip[3]);
|
|
437
|
+
if (kind === 'sam2') {
|
|
438
|
+
if (typeof payload !== 'string' || payload === '') {
|
|
439
|
+
throw new Error(`maskT1: sam2 export '${name}' needs labels (a detector class word — the card refuses empty)`);
|
|
440
|
+
}
|
|
441
|
+
const sel = sam2detect(payload, bboxes ? { x: bboxes.x, y: bboxes.y, width: bboxes.width, height: bboxes.height } : undefined, tune);
|
|
442
|
+
save(sel.mask, `mask_${name}`, tune?.dilation);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const sel = detect(conditioning, bboxes, tune);
|
|
446
|
+
save(sel, `mask_${name}`, tune?.dilation);
|
|
447
|
+
}
|
|
448
|
+
// THE ORDERED UNION CHAIN (stroke 78 — the ordered-draw law): a
|
|
449
|
+
// draw STACKS — each payload unions onto the running mask (the
|
|
450
|
+
// previous outputs), in array order, one composite per hop; the
|
|
451
|
+
// terminal mask saves as combined_union. Replaces nothing — the
|
|
452
|
+
// single-payload boolean arm below keeps the flat-mode contract.
|
|
453
|
+
const paintChain = p.paint_masks;
|
|
454
|
+
if (paintChain !== undefined) {
|
|
455
|
+
if (!Array.isArray(paintChain) || paintChain.some((e) => typeof e !== 'string')) {
|
|
456
|
+
throw new Error('maskT1: paint_masks must be an array of payload paths (the ordered draw stack)');
|
|
457
|
+
}
|
|
458
|
+
if (primaryMask === null && paintChain.length > 0) {
|
|
459
|
+
throw new Error('maskT1: paint_masks stacks ONTO a mask — add a standalone source (sam2/sam3) first');
|
|
460
|
+
}
|
|
461
|
+
if (paintChain.length > 0 && primaryMask !== null) {
|
|
462
|
+
let acc = dilate(primaryMask);
|
|
463
|
+
for (const path of paintChain) {
|
|
464
|
+
if (path === '')
|
|
465
|
+
continue;
|
|
466
|
+
acc = g.node('MaskComposite', {
|
|
467
|
+
destination: acc,
|
|
468
|
+
source: loadPaint(path),
|
|
469
|
+
x: 0,
|
|
470
|
+
y: 0,
|
|
471
|
+
operation: 'add',
|
|
472
|
+
}).out(0);
|
|
473
|
+
}
|
|
474
|
+
save(acc, 'mask_combined_union');
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// paint unions into combined (flat-mode export_paint contract —
|
|
478
|
+
// the global payload's mask, ONE hop)
|
|
479
|
+
if (p.union_paint_into_combined && primaryMask) {
|
|
480
|
+
const globalPath = (p.paint_mask ?? '');
|
|
481
|
+
if (globalPath !== '') {
|
|
482
|
+
const combined = g.node('MaskComposite', {
|
|
483
|
+
destination: dilate(primaryMask), // a SECOND GrowMask on the primary under union
|
|
484
|
+
source: loadPaint(globalPath),
|
|
485
|
+
x: 0,
|
|
486
|
+
y: 0,
|
|
487
|
+
operation: 'add',
|
|
488
|
+
}).out(0);
|
|
489
|
+
save(combined, 'mask_combined_union');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// ── the box manifest (drawn) — the instance readback ──
|
|
493
|
+
if (exportBoxes && rtdetrBoxes) {
|
|
494
|
+
const drawn = g.node('DrawBBoxes', { bboxes: rtdetrBoxes, image: img() });
|
|
495
|
+
return g.node('SaveImage', { images: drawn.out(0), filename_prefix: `${prefix}/detect_boxes` }).out(0);
|
|
496
|
+
}
|
|
497
|
+
// no primary and no export is refused upstream (the admission law),
|
|
498
|
+
// so a composed lane always has a mask handle to declare
|
|
499
|
+
return saved ?? primaryMask ?? img();
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
//# sourceMappingURL=actions.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Plugin } from "@deepseek-ai/cordis";
|
|
2
|
+
import type { ModelDecl } from '@melite/comfyui-card-kit/model-contract';
|
|
3
|
+
import type { ProvisionRow } from '@melite/comfyui-card-kit/model-contract';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export declare const PROVISIONS: ProvisionRow[];
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export declare const DETECTOR_PROVISIONS: ProvisionRow[];
|
|
22
|
+
export declare const DECL: ModelDecl;
|
|
23
|
+
export declare const family: Plugin;
|
|
24
|
+
export { select, parseMaskLaneFacts, MASK_WEIGHTS, type MaskLaneFacts, type MaskLaneParams } from "./actions.js";
|
|
25
|
+
export { family as default };
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { IMPACT_DETECTOR_PROVISIONS } from "@melite/comfyui-card-kit";
|
|
2
|
+
import { select } from "./actions.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export const PROVISIONS = [
|
|
13
|
+
{
|
|
14
|
+
|
|
15
|
+
// existed since stroke ~40 but the weight was NEVER fetched —
|
|
16
|
+
// the engine silently dropped the whole mask arm). The official
|
|
17
|
+
// ComfyUI conversion.
|
|
18
|
+
name: "sam3.1_multiplex_fp16.safetensors",
|
|
19
|
+
url: "https://huggingface.co/Comfy-Org/sam3.1/resolve/main/checkpoints/sam3.1_multiplex_fp16.safetensors",
|
|
20
|
+
folder: "checkpoints",
|
|
21
|
+
sha256: "9ba99c92703c2e8b4f47de2d34a539bb8e18923049e238b780d70dbe6368eb03",
|
|
22
|
+
size_bytes: 1745546848,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "rt_detr_v4-x-hgnet_fp32.safetensors",
|
|
26
|
+
url: "https://huggingface.co/Comfy-Org/SDPose/resolve/main/diffusion_models/rt_detr_v4-x-hgnet_fp32.safetensors",
|
|
27
|
+
folder: "diffusion_models",
|
|
28
|
+
sha256: "1ba4a7794c2f8b176327c887ba38fbe0f977049cbef149960288aa923d0239f3",
|
|
29
|
+
size_bytes: 247790660,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "sam2.1_hiera_large.pt",
|
|
33
|
+
url: "https://huggingface.co/facebook/sam2.1-hiera-large/resolve/main/sam2.1_hiera_large.pt",
|
|
34
|
+
folder: "sams",
|
|
35
|
+
sha256: "2647878d5dfa5098f2f8649825738a9345572bae2d4350a2468587ece47dd318",
|
|
36
|
+
size_bytes: 898083611,
|
|
37
|
+
},
|
|
38
|
+
...IMPACT_DETECTOR_PROVISIONS,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export const DETECTOR_PROVISIONS = [...IMPACT_DETECTOR_PROVISIONS];
|
|
48
|
+
// THE TOOL FAMILY (the F1 shape): a selection tool, not a generative
|
|
49
|
+
// model — `unoffered: true` registers the atom (the actions admission
|
|
50
|
+
// gate runs at boot; the card's declaredAction binding resolves
|
|
51
|
+
// against it) while serving no catalog row, so no model dropdown
|
|
52
|
+
// gains a selection tool as a pickable base. The family id is
|
|
53
|
+
// mask-select, NOT the card's id — the h3 pattern (family h3, card
|
|
54
|
+
// h3-timeline): the card's id must keep resolving to the CARD (the
|
|
55
|
+
// form-spec door asks the models seat first, and a family key
|
|
56
|
+
// shadows the card arm — discovery.spec's mask envelope wall catches
|
|
57
|
+
// exactly that), and the bench plane keeps the self-naming strip for
|
|
58
|
+
// era rows. The three weights are the family's constants (actions.ts
|
|
59
|
+
// MASK_WEIGHTS — the LANE_FILES pattern), so the row carries no
|
|
60
|
+
// lane_inputs. ONE variant, ONE address, no cells: the family's id
|
|
61
|
+
// is its own route — and its six weights DOWNLOAD through the family
|
|
62
|
+
// provisions above, like every other model (no separate
|
|
63
|
+
// card-provision behavior).
|
|
64
|
+
export const DECL = {
|
|
65
|
+
"id": "mask-select",
|
|
66
|
+
"label": "Mask",
|
|
67
|
+
"description": "The mask studio's selection lane — SAM3 CLIP/RT-DETR detection, the SAM2 impact-pack arm, and paint exports, as one declared action the mask card binds.",
|
|
68
|
+
"genre": "image",
|
|
69
|
+
"unoffered": true,
|
|
70
|
+
"provisions": PROVISIONS,
|
|
71
|
+
"variants": [
|
|
72
|
+
{
|
|
73
|
+
"id": "select",
|
|
74
|
+
"label": "Select",
|
|
75
|
+
"group": "",
|
|
76
|
+
"route": "mask-select",
|
|
77
|
+
"cells": [],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
// axes — the anny shape. The undeclared 'Variant' literal the
|
|
82
|
+
|
|
83
|
+
// solo category the canon gives the one-row family.
|
|
84
|
+
"variant_groups": [],
|
|
85
|
+
// THE FAMILY DOOR (the atom vocabulary, stroke 0): the select
|
|
86
|
+
// action's facts parse runs over the family row and every variant
|
|
87
|
+
// merge AT REGISTRATION — here it closes over the family's own
|
|
88
|
+
// weight constants (an empty row is the family's whole truth).
|
|
89
|
+
"actions": [select],
|
|
90
|
+
};
|
|
91
|
+
export const family = {
|
|
92
|
+
name: "family-mask",
|
|
93
|
+
inject: ["models"],
|
|
94
|
+
apply(ctx) {
|
|
95
|
+
ctx.effect(() => ctx.models.registerFamily(DECL));
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
// The family's offered actions (the atom vocabulary): the select
|
|
99
|
+
// segment the mask card binds. The card imports this beside the DECL
|
|
100
|
+
// — the family's whole offer, one face. PROVISIONS rides too: the
|
|
101
|
+
// card serves the family's rows to its arm-weight gate (the
|
|
102
|
+
// provisions-identity pin, image/mask card.spec).
|
|
103
|
+
export { select, parseMaskLaneFacts, MASK_WEIGHTS } from "./actions.js";
|
|
104
|
+
// The Loader-row face: DSH loads plugin rows by bare specifier and
|
|
105
|
+
// unwraps `exports.default ?? exports` (vendor/cordis_plugins/loader
|
|
106
|
+
// unwrapExports) — the default export IS the row-loadable plugin. The
|
|
107
|
+
// named `family` stays the library face (engine loadFamilies).
|
|
108
|
+
export { family as default };
|
|
109
|
+
//# sourceMappingURL=index.js.map
|