zorya-classifier 0.2.1 → 0.2.2

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 (126) hide show
  1. package/README.md +3 -3
  2. package/dist/app/classifier-config.d.ts +30 -0
  3. package/dist/app/classifier-config.js +46 -0
  4. package/dist/app/classifier-contracts.d.ts +32 -0
  5. package/dist/app/classifier-contracts.js +1 -0
  6. package/dist/app/classifier-representation.d.ts +5 -0
  7. package/dist/app/classifier-representation.js +33 -0
  8. package/dist/app/classifier-runtime.d.ts +7 -0
  9. package/dist/app/classifier-runtime.js +71 -0
  10. package/dist/app/classifier-spatial.d.ts +31 -0
  11. package/dist/app/classifier-spatial.js +78 -0
  12. package/dist/app/cli-arguments.d.ts +2 -2
  13. package/dist/app/create-classifier.d.ts +4 -34
  14. package/dist/app/create-classifier.js +37 -237
  15. package/dist/entities/classification.d.ts +98 -0
  16. package/dist/entities/classification.js +28 -0
  17. package/dist/entities/contracts.d.ts +6 -475
  18. package/dist/entities/contracts.js +6 -97
  19. package/dist/entities/decision.d.ts +211 -0
  20. package/dist/entities/decision.js +33 -0
  21. package/dist/entities/geometry.d.ts +37 -0
  22. package/dist/entities/geometry.js +26 -0
  23. package/dist/entities/image.d.ts +39 -0
  24. package/dist/entities/image.js +15 -0
  25. package/dist/entities/localization.d.ts +125 -0
  26. package/dist/entities/localization.js +13 -0
  27. package/dist/entities/segmentation-metadata.d.ts +70 -0
  28. package/dist/entities/segmentation-metadata.js +21 -1
  29. package/dist/entities/validation.d.ts +2 -0
  30. package/dist/entities/validation.js +9 -0
  31. package/dist/features/choice-beam-layout.js +2 -7
  32. package/dist/features/proposals/presentation.d.ts +21 -0
  33. package/dist/features/proposals/presentation.js +122 -0
  34. package/dist/features/proposals/validation.d.ts +138 -0
  35. package/dist/features/proposals/validation.js +125 -0
  36. package/dist/features/segmentation/box-check.js +10 -23
  37. package/dist/features/segmentation/mask.js +2 -7
  38. package/dist/features/segmentation/region-result.d.ts +18 -0
  39. package/dist/features/segmentation/region-result.js +171 -0
  40. package/dist/features/segmentation/source-mask.d.ts +6 -0
  41. package/dist/features/segmentation/source-mask.js +32 -0
  42. package/dist/features/segmentation/validation.d.ts +9 -0
  43. package/dist/features/segmentation/validation.js +94 -0
  44. package/dist/infrastructure/decision-transport/errors.d.ts +8 -0
  45. package/dist/infrastructure/decision-transport/errors.js +34 -0
  46. package/dist/infrastructure/decision-transport/images.d.ts +1 -0
  47. package/dist/infrastructure/decision-transport/images.js +52 -0
  48. package/dist/infrastructure/{openrouter-common.d.ts → decision-transport/request.d.ts} +6 -15
  49. package/dist/infrastructure/decision-transport/request.js +162 -0
  50. package/dist/infrastructure/decision-transport/response.d.ts +1 -0
  51. package/dist/infrastructure/decision-transport/response.js +72 -0
  52. package/dist/infrastructure/decision-transport/route-identity.d.ts +11 -0
  53. package/dist/infrastructure/decision-transport/route-identity.js +23 -0
  54. package/dist/infrastructure/openrouter-vision.js +1 -1
  55. package/dist/infrastructure/openrouter.js +7 -19
  56. package/dist/infrastructure/raster.js +3 -8
  57. package/dist/infrastructure/segmentation/grabcut/index.js +2 -4
  58. package/dist/infrastructure/segmentation/grabcut/masks.d.ts +49 -0
  59. package/dist/infrastructure/segmentation/grabcut/masks.js +89 -0
  60. package/dist/infrastructure/segmentation/grabcut/model-asset.d.ts +4 -0
  61. package/dist/infrastructure/segmentation/grabcut/model-asset.js +4 -0
  62. package/dist/infrastructure/segmentation/grabcut/runtime.d.ts +70 -0
  63. package/dist/infrastructure/segmentation/grabcut/runtime.js +96 -0
  64. package/dist/infrastructure/segmentation/grabcut/worker-inference.d.ts +4 -43
  65. package/dist/infrastructure/segmentation/grabcut/worker-inference.js +4 -187
  66. package/dist/infrastructure/segmentation/mobile-sam/index.d.ts +3 -66
  67. package/dist/infrastructure/segmentation/mobile-sam/index.js +2 -478
  68. package/dist/infrastructure/segmentation/mobile-sam/input.d.ts +46 -0
  69. package/dist/infrastructure/segmentation/mobile-sam/input.js +83 -0
  70. package/dist/infrastructure/segmentation/mobile-sam/model-assets.d.ts +8 -0
  71. package/dist/infrastructure/segmentation/mobile-sam/model-assets.js +8 -0
  72. package/dist/infrastructure/segmentation/mobile-sam/object-proposals.d.ts +7 -0
  73. package/dist/infrastructure/segmentation/mobile-sam/object-proposals.js +124 -0
  74. package/dist/infrastructure/segmentation/mobile-sam/options.d.ts +43 -0
  75. package/dist/infrastructure/segmentation/mobile-sam/options.js +19 -0
  76. package/dist/infrastructure/segmentation/mobile-sam/proposal-masks.d.ts +26 -0
  77. package/dist/infrastructure/segmentation/mobile-sam/proposal-masks.js +71 -0
  78. package/dist/infrastructure/segmentation/mobile-sam/segmentation-masks.d.ts +112 -0
  79. package/dist/infrastructure/segmentation/mobile-sam/segmentation-masks.js +230 -0
  80. package/dist/infrastructure/segmentation/mobile-sam/segmenter.d.ts +22 -0
  81. package/dist/infrastructure/segmentation/mobile-sam/segmenter.js +268 -0
  82. package/dist/infrastructure/segmentation/mobile-sam/worker-expand.d.ts +7 -0
  83. package/dist/infrastructure/segmentation/mobile-sam/worker-expand.js +188 -0
  84. package/dist/infrastructure/segmentation/mobile-sam/worker-inference.d.ts +6 -149
  85. package/dist/infrastructure/segmentation/mobile-sam/worker-inference.js +6 -845
  86. package/dist/infrastructure/segmentation/mobile-sam/worker-models.d.ts +14 -0
  87. package/dist/infrastructure/segmentation/mobile-sam/worker-models.js +72 -0
  88. package/dist/infrastructure/segmentation/mobile-sam/worker-proposals.d.ts +7 -0
  89. package/dist/infrastructure/segmentation/mobile-sam/worker-proposals.js +159 -0
  90. package/dist/infrastructure/segmentation/mobile-sam/worker-segment.d.ts +4 -0
  91. package/dist/infrastructure/segmentation/mobile-sam/worker-segment.js +126 -0
  92. package/dist/infrastructure/segmentation/native-protocol.d.ts +59 -34
  93. package/dist/infrastructure/segmentation/native-protocol.js +26 -16
  94. package/dist/infrastructure/segmentation/proposal-presentation-input.d.ts +20 -0
  95. package/dist/infrastructure/segmentation/proposal-presentation-input.js +107 -0
  96. package/dist/infrastructure/segmentation/proposal-presentation-pixels.d.ts +8 -0
  97. package/dist/infrastructure/segmentation/proposal-presentation-pixels.js +93 -0
  98. package/dist/infrastructure/segmentation/proposal-presentation.js +10 -201
  99. package/dist/infrastructure/vision-chat/options.d.ts +62 -0
  100. package/dist/infrastructure/vision-chat/options.js +46 -0
  101. package/dist/infrastructure/vision-chat/request.d.ts +23 -0
  102. package/dist/infrastructure/vision-chat/request.js +109 -0
  103. package/dist/infrastructure/vision-chat/response.d.ts +3 -0
  104. package/dist/infrastructure/vision-chat/response.js +105 -0
  105. package/dist/infrastructure/vision-chat.d.ts +4 -61
  106. package/dist/infrastructure/vision-chat.js +8 -271
  107. package/dist/openai-compatible.js +1 -1
  108. package/dist/processes/extract-from-region.js +11 -216
  109. package/dist/processes/extract.d.ts +2 -8
  110. package/dist/processes/extract.js +12 -128
  111. package/dist/processes/proposal-location.js +5 -251
  112. package/docs/README.ru.md +15 -12
  113. package/docs/agents.ru.md +95 -0
  114. package/docs/architecture.md +31 -5
  115. package/docs/architecture.ru.md +146 -0
  116. package/docs/benchmarks.md +3 -1
  117. package/docs/development.ru.md +141 -0
  118. package/docs/provenance.json +5 -1
  119. package/docs/publishing.md +5 -3
  120. package/docs/refactoring-plan.ru.md +183 -0
  121. package/package.json +4 -2
  122. package/dist/infrastructure/boundary-view.d.ts +0 -2
  123. package/dist/infrastructure/boundary-view.js +0 -263
  124. package/dist/infrastructure/openrouter-common.js +0 -317
  125. package/dist/infrastructure/text-representation.d.ts +0 -23
  126. package/dist/infrastructure/text-representation.js +0 -226
@@ -0,0 +1,268 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { ClassifierError, assertActive } from '../../../entities/errors.js';
3
+ import { NativeChildPool } from '../native-child.js';
4
+ import { verifyPinnedAsset } from '../verify-asset.js';
5
+ import { probeBackend } from '../probe-backend.js';
6
+ import { nativeErrorReplySchema, nativeMobileSamExpansionReplySchema, nativeSegmentationReplySchema, } from '../native-protocol.js';
7
+ import { decoderIdentity, encoderIdentity } from './model-assets.js';
8
+ import { optionsSchema } from './options.js';
9
+ import { snapshotInput, snapshotPreviousMask } from './input.js';
10
+ import { packFullSourceBinaryMask, unpackFullSourceBinaryMask, validateMobileSamPreviousMask, } from './segmentation-masks.js';
11
+ /**
12
+ * Explicit pinned MobileSAM inference in a separately owned Node child process.
13
+ * @experimental Native segmentation adapter; unqualified for general mask or object accuracy.
14
+ */
15
+ export async function createMobileSamSegmenter(options) {
16
+ const checked = optionsSchema.safeParse(options);
17
+ if (!checked.success)
18
+ throw new ClassifierError('INVALID_INPUT', 'Invalid MobileSAM options');
19
+ const parsed = checked.data;
20
+ if (parsed.executionProvider === 'dml' && process.platform !== 'win32')
21
+ throw new ClassifierError('UNSUPPORTED', 'DirectML is supported only on Windows');
22
+ try {
23
+ import.meta.resolve('onnxruntime-node');
24
+ }
25
+ catch {
26
+ throw new ClassifierError('UNSUPPORTED', 'onnxruntime-node is not installed');
27
+ }
28
+ await verifyPinnedAsset(parsed.encoderPath, encoderIdentity, 'MobileSAM encoder');
29
+ await verifyPinnedAsset(parsed.decoderPath, decoderIdentity, 'MobileSAM decoder');
30
+ const sourceWorker = import.meta.url.endsWith('.ts');
31
+ const workerUrl = new URL(sourceWorker ? '../native-worker.ts' : '../native-worker.js', import.meta.url);
32
+ await probeBackend(workerUrl, 'mobile-sam', { options: parsed });
33
+ const pool = new NativeChildPool({
34
+ workerUrl,
35
+ maxActive: 1,
36
+ maxWaiting: 2,
37
+ jobTimeoutMs: 10_000,
38
+ abortGraceMs: 250,
39
+ encode: (job) => Buffer.from(JSON.stringify({ id: job.id, ...job.payload })),
40
+ decode: (raw, id) => {
41
+ const error = nativeErrorReplySchema.safeParse(raw);
42
+ if (error.success) {
43
+ if (error.data.id !== id)
44
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child error ID mismatch');
45
+ throw new ClassifierError(error.data.code, error.data.message);
46
+ }
47
+ const reply = nativeSegmentationReplySchema.safeParse(raw);
48
+ if (reply.success && reply.data.id === id)
49
+ return reply.data;
50
+ const expansion = nativeMobileSamExpansionReplySchema.safeParse(raw);
51
+ if (!expansion.success || expansion.data.id !== id)
52
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid or mismatched reply');
53
+ return expansion.data;
54
+ },
55
+ });
56
+ return createMobileSamPortForPool(pool, parsed);
57
+ }
58
+ /** Internal adapter seam: pool ownership stays with the caller. */
59
+ export function createMobileSamPortForPool(pool, parsed) {
60
+ return {
61
+ async segment(input) {
62
+ const started = performance.now();
63
+ assertActive(input.signal);
64
+ let image;
65
+ let maskData;
66
+ try {
67
+ const raw = await pool.runLazy(input.signal, () => {
68
+ image = snapshotInput(input);
69
+ const rgba = Buffer.from(image.data);
70
+ let rgbaBase64;
71
+ try {
72
+ rgbaBase64 = rgba.toString('base64');
73
+ }
74
+ finally {
75
+ rgba.fill(0);
76
+ }
77
+ return {
78
+ kind: 'mobile-sam',
79
+ width: image.width,
80
+ height: image.height,
81
+ rgba: rgbaBase64,
82
+ seedRegion: image.seedRegion,
83
+ wholeRegion: image.wholeRegion,
84
+ roi: image.roi,
85
+ prompt: image.prompt,
86
+ options: parsed,
87
+ };
88
+ });
89
+ if (!image)
90
+ throw new ClassifierError('INVALID_RESPONSE', 'Native segmentation omitted its input snapshot');
91
+ const checkedReply = nativeSegmentationReplySchema.safeParse(raw);
92
+ if (!checkedReply.success ||
93
+ checkedReply.data.width !== image.width ||
94
+ checkedReply.data.height !== image.height)
95
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid segmentation reply');
96
+ maskData = Buffer.from(checkedReply.data.mask, 'base64');
97
+ if (maskData.length !== image.width * image.height ||
98
+ maskData.toString('base64') !== checkedReply.data.mask)
99
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child mask dimensions or encoding are invalid');
100
+ return {
101
+ status: 'segmented',
102
+ mask: {
103
+ data: new Uint8Array(maskData),
104
+ width: image.width,
105
+ height: image.height,
106
+ format: 'binary-u8',
107
+ },
108
+ metadata: {
109
+ method: checkedReply.data.method,
110
+ backend: 'mobile-sam',
111
+ durationMs: Math.max(0, performance.now() - started),
112
+ input: { width: image.width, height: image.height },
113
+ },
114
+ };
115
+ }
116
+ finally {
117
+ maskData?.fill(0);
118
+ image?.data.fill(0);
119
+ }
120
+ },
121
+ async expand(input, previousMask) {
122
+ const started = performance.now();
123
+ assertActive(input.signal);
124
+ let image;
125
+ let previous;
126
+ let resultData;
127
+ let transferred = false;
128
+ try {
129
+ const raw = await pool.runLazy(input.signal, () => {
130
+ image = snapshotInput(input);
131
+ previous = snapshotPreviousMask(input, previousMask, image);
132
+ const packedPrevious = packFullSourceBinaryMask(previous, image.width, image.height);
133
+ let previousMaskBase64;
134
+ try {
135
+ const packedBuffer = Buffer.from(packedPrevious);
136
+ try {
137
+ previousMaskBase64 = packedBuffer.toString('base64');
138
+ }
139
+ finally {
140
+ packedBuffer.fill(0);
141
+ }
142
+ }
143
+ finally {
144
+ packedPrevious.fill(0);
145
+ }
146
+ const rgbaBuffer = Buffer.from(image.data);
147
+ let rgbaBase64;
148
+ try {
149
+ rgbaBase64 = rgbaBuffer.toString('base64');
150
+ }
151
+ finally {
152
+ rgbaBuffer.fill(0);
153
+ }
154
+ return {
155
+ kind: 'mobile-sam-expand',
156
+ width: image.width,
157
+ height: image.height,
158
+ rgba: rgbaBase64,
159
+ previousMask: previousMaskBase64,
160
+ seedRegion: image.seedRegion,
161
+ wholeRegion: image.wholeRegion,
162
+ roi: image.roi,
163
+ prompt: image.prompt,
164
+ options: parsed,
165
+ };
166
+ });
167
+ if (!image || !previous)
168
+ throw new ClassifierError('INVALID_RESPONSE', 'Native expansion omitted its input snapshot');
169
+ const checkedReply = nativeMobileSamExpansionReplySchema.safeParse(raw);
170
+ if (!checkedReply.success ||
171
+ checkedReply.data.width !== image.width ||
172
+ checkedReply.data.height !== image.height)
173
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid expansion reply');
174
+ if (checkedReply.data.status === 'unknown')
175
+ return {
176
+ status: 'unknown',
177
+ reason: 'no-enclosing-mask',
178
+ metadata: {
179
+ method: checkedReply.data.method,
180
+ backend: 'mobile-sam',
181
+ durationMs: Math.max(0, performance.now() - started),
182
+ input: { width: image.width, height: image.height },
183
+ },
184
+ };
185
+ const packedBytes = Buffer.from(checkedReply.data.mask, 'base64');
186
+ let data;
187
+ try {
188
+ if (packedBytes.toString('base64') !== checkedReply.data.mask)
189
+ throw new ClassifierError('INVALID_RESPONSE', 'Native expansion mask encoding is invalid');
190
+ try {
191
+ data = unpackFullSourceBinaryMask(packedBytes, image.width, image.height);
192
+ resultData = data;
193
+ }
194
+ catch {
195
+ throw new ClassifierError('INVALID_RESPONSE', 'Native expansion packed mask is invalid');
196
+ }
197
+ }
198
+ finally {
199
+ packedBytes.fill(0);
200
+ }
201
+ let oldStats;
202
+ let newStats;
203
+ try {
204
+ oldStats = validateMobileSamPreviousMask({
205
+ width: image.width,
206
+ height: image.height,
207
+ rgba: image.data,
208
+ previousMask: previous,
209
+ roi: image.roi,
210
+ });
211
+ newStats = validateMobileSamPreviousMask({
212
+ width: image.width,
213
+ height: image.height,
214
+ rgba: image.data,
215
+ previousMask: data,
216
+ roi: image.roi,
217
+ });
218
+ }
219
+ catch {
220
+ data.fill(0);
221
+ throw new ClassifierError('INVALID_RESPONSE', 'Native expanded mask is invalid');
222
+ }
223
+ let hasNewForeground = false;
224
+ for (let index = 0; index < data.length; index++) {
225
+ if (previous[index] === 1 && data[index] !== 1) {
226
+ data.fill(0);
227
+ throw new ClassifierError('INVALID_RESPONSE', 'Native expansion lost prior foreground');
228
+ }
229
+ if (data[index] === 1 && previous[index] === 0)
230
+ hasNewForeground = true;
231
+ }
232
+ if (!hasNewForeground ||
233
+ newStats.area <= oldStats.area ||
234
+ newStats.bounds.left > oldStats.bounds.left ||
235
+ newStats.bounds.top > oldStats.bounds.top ||
236
+ newStats.bounds.right < oldStats.bounds.right ||
237
+ newStats.bounds.bottom < oldStats.bounds.bottom ||
238
+ (newStats.bounds.left === oldStats.bounds.left &&
239
+ newStats.bounds.top === oldStats.bounds.top &&
240
+ newStats.bounds.right === oldStats.bounds.right &&
241
+ newStats.bounds.bottom === oldStats.bounds.bottom)) {
242
+ data.fill(0);
243
+ throw new ClassifierError('INVALID_RESPONSE', 'Native expansion is not a strict enclosing mask');
244
+ }
245
+ transferred = true;
246
+ return {
247
+ status: 'segmented',
248
+ mask: { data, width: image.width, height: image.height, format: 'binary-u8' },
249
+ metadata: {
250
+ method: checkedReply.data.method,
251
+ backend: 'mobile-sam',
252
+ durationMs: Math.max(0, performance.now() - started),
253
+ input: { width: image.width, height: image.height },
254
+ qualityScore: checkedReply.data.qualityScore,
255
+ },
256
+ };
257
+ }
258
+ finally {
259
+ image?.data.fill(0);
260
+ previous?.fill(0);
261
+ if (resultData && !transferred)
262
+ resultData.fill(0);
263
+ }
264
+ },
265
+ settle: () => pool.settle(),
266
+ dispose: () => pool.dispose(),
267
+ };
268
+ }
@@ -0,0 +1,7 @@
1
+ import type { NativeMobileSamExpansionReply, NativeSegmentationRequest } from '../native-protocol.js';
2
+ type ExpansionRequest = Extract<NativeSegmentationRequest, {
3
+ kind: 'mobile-sam-expand';
4
+ }>;
5
+ /** Run one same-seed decode and return only the smallest strict enclosing mask. */
6
+ export declare function runMobileSamExpandTask(request: ExpansionRequest): Promise<NativeMobileSamExpansionReply>;
7
+ export {};
@@ -0,0 +1,188 @@
1
+ import sharp from 'sharp';
2
+ import { buildMobileSamSessionOptions, decoderIdentity, encoderIdentity, verifiedBytes, } from './worker-models.js';
3
+ import { mobileSamSegmentationPrompt, packFullSourceBinaryMask, selectEnclosingMobileSamCandidate, unpackFullSourceBinaryMask, validateMobileSamPreviousMask, } from './segmentation-masks.js';
4
+ /** Run one same-seed decode and return only the smallest strict enclosing mask. */
5
+ export async function runMobileSamExpandTask(request) {
6
+ const ort = await import('onnxruntime-node');
7
+ if (!ort
8
+ .listSupportedBackends()
9
+ .some((item) => item.name === request.options.executionProvider && item.bundled))
10
+ throw new Error('Explicitly requested ONNX execution provider is unavailable');
11
+ const sessionOptions = buildMobileSamSessionOptions(request.options);
12
+ const encoder = await ort.InferenceSession.create(await verifiedBytes(request.options.encoderPath, encoderIdentity), sessionOptions);
13
+ let decoder;
14
+ let imageTensor;
15
+ let embedding;
16
+ let result;
17
+ let releaseFailed = false;
18
+ let rgba;
19
+ let packedPrevious;
20
+ let previousMask;
21
+ let selectedMask;
22
+ try {
23
+ decoder = await ort.InferenceSession.create(await verifiedBytes(request.options.decoderPath, decoderIdentity), sessionOptions);
24
+ rgba = Buffer.from(request.rgba, 'base64');
25
+ packedPrevious = Buffer.from(request.previousMask, 'base64');
26
+ if (rgba.length !== request.width * request.height * 4)
27
+ throw new Error('Invalid expansion RGBA input size');
28
+ previousMask = unpackFullSourceBinaryMask(packedPrevious, request.width, request.height);
29
+ validateMobileSamPreviousMask({
30
+ width: request.width,
31
+ height: request.height,
32
+ rgba,
33
+ previousMask,
34
+ roi: request.roi,
35
+ });
36
+ const scale = 1024 / Math.max(request.width, request.height);
37
+ const resizedWidth = Math.round(request.width * scale);
38
+ const resizedHeight = Math.round(request.height * scale);
39
+ if (resizedWidth < 1 || resizedHeight < 1)
40
+ throw new Error('Unrepresentable image aspect ratio');
41
+ const pipeline = sharp(rgba, {
42
+ raw: { width: request.width, height: request.height, channels: 4 },
43
+ });
44
+ let rgb;
45
+ try {
46
+ rgb = await pipeline
47
+ .removeAlpha()
48
+ .resize(resizedWidth, resizedHeight, { fit: 'fill', kernel: 'linear' })
49
+ .raw()
50
+ .toBuffer();
51
+ }
52
+ finally {
53
+ pipeline.destroy();
54
+ }
55
+ if (rgb.length !== resizedWidth * resizedHeight * 3)
56
+ throw new Error('Invalid resized RGB data');
57
+ imageTensor = new ort.Tensor('float32', Float32Array.from(rgb), [
58
+ resizedHeight,
59
+ resizedWidth,
60
+ 3,
61
+ ]);
62
+ rgb.fill(0);
63
+ const encoded = await encoder.run({ input_image: imageTensor });
64
+ const embeddingTensor = encoded.image_embeddings;
65
+ if (!(embeddingTensor instanceof ort.Tensor) ||
66
+ embeddingTensor.type !== 'float32' ||
67
+ JSON.stringify(embeddingTensor.dims) !== JSON.stringify([1, 256, 64, 64]) ||
68
+ !(embeddingTensor.data instanceof Float32Array))
69
+ throw new Error('Unexpected MobileSAM encoder output');
70
+ embedding = embeddingTensor;
71
+ const prompt = mobileSamSegmentationPrompt({
72
+ prompt: request.prompt,
73
+ seedRegion: request.seedRegion,
74
+ wholeRegion: request.wholeRegion,
75
+ xScale: resizedWidth / request.width,
76
+ yScale: resizedHeight / request.height,
77
+ });
78
+ const pointCoords = new ort.Tensor('float32', prompt.coordinates, [1, 2, 2]);
79
+ const pointLabels = new ort.Tensor('float32', prompt.labels, [1, 2]);
80
+ const maskInput = new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]);
81
+ const hasMaskInput = new ort.Tensor('float32', Float32Array.of(0), [1]);
82
+ const originalSize = new ort.Tensor('float32', Float32Array.of(request.height, request.width), [
83
+ 2,
84
+ ]);
85
+ let decoded;
86
+ try {
87
+ decoded = await decoder.run({
88
+ image_embeddings: embedding,
89
+ point_coords: pointCoords,
90
+ point_labels: pointLabels,
91
+ mask_input: maskInput,
92
+ has_mask_input: hasMaskInput,
93
+ orig_im_size: originalSize,
94
+ });
95
+ const masks = decoded.masks;
96
+ const scores = decoded.iou_predictions;
97
+ if (!(masks instanceof ort.Tensor) ||
98
+ masks.type !== 'float32' ||
99
+ JSON.stringify(masks.dims) !== JSON.stringify([1, 4, request.height, request.width]) ||
100
+ !(masks.data instanceof Float32Array) ||
101
+ !(scores instanceof ort.Tensor) ||
102
+ scores.type !== 'float32' ||
103
+ JSON.stringify(scores.dims) !== JSON.stringify([1, 4]) ||
104
+ !(scores.data instanceof Float32Array))
105
+ throw new Error('Unexpected MobileSAM expansion decoder output');
106
+ const candidate = selectEnclosingMobileSamCandidate({
107
+ width: request.width,
108
+ height: request.height,
109
+ rgba,
110
+ logits: masks.data,
111
+ scores: scores.data,
112
+ previousMask,
113
+ roi: request.roi,
114
+ });
115
+ if (candidate) {
116
+ selectedMask = candidate.mask;
117
+ const packed = packFullSourceBinaryMask(selectedMask, request.width, request.height);
118
+ const packedBuffer = Buffer.from(packed);
119
+ try {
120
+ result = {
121
+ id: request.id,
122
+ status: 'expanded',
123
+ width: request.width,
124
+ height: request.height,
125
+ mask: packedBuffer.toString('base64'),
126
+ method: 'mobile-sam-enclosing-mask-v1',
127
+ qualityScore: candidate.score,
128
+ };
129
+ }
130
+ finally {
131
+ packedBuffer.fill(0);
132
+ packed.fill(0);
133
+ }
134
+ }
135
+ else {
136
+ result = {
137
+ id: request.id,
138
+ status: 'unknown',
139
+ reason: 'no-enclosing-mask',
140
+ width: request.width,
141
+ height: request.height,
142
+ method: 'mobile-sam-enclosing-mask-v1',
143
+ };
144
+ }
145
+ }
146
+ finally {
147
+ prompt.coordinates.fill(0);
148
+ prompt.labels.fill(0);
149
+ for (const tensor of [pointCoords, pointLabels, maskInput, hasMaskInput, originalSize])
150
+ if (tensor.data instanceof Float32Array)
151
+ tensor.data.fill(0);
152
+ pointCoords.dispose();
153
+ pointLabels.dispose();
154
+ maskInput.dispose();
155
+ hasMaskInput.dispose();
156
+ originalSize.dispose();
157
+ if (decoded)
158
+ for (const tensor of Object.values(decoded))
159
+ if (tensor instanceof ort.Tensor) {
160
+ if (tensor.data instanceof Float32Array || tensor.data instanceof Uint8Array)
161
+ tensor.data.fill(0);
162
+ tensor.dispose();
163
+ }
164
+ }
165
+ }
166
+ finally {
167
+ imageTensor?.dispose();
168
+ if (embedding && embedding !== imageTensor) {
169
+ if (embedding.data instanceof Float32Array)
170
+ embedding.data.fill(0);
171
+ embedding.dispose();
172
+ }
173
+ previousMask?.fill(0);
174
+ selectedMask?.fill(0);
175
+ rgba?.fill(0);
176
+ packedPrevious?.fill(0);
177
+ const releases = await Promise.allSettled([
178
+ encoder.release(),
179
+ ...(decoder ? [decoder.release()] : []),
180
+ ]);
181
+ releaseFailed = releases.some((release) => release.status === 'rejected');
182
+ }
183
+ if (releaseFailed)
184
+ throw new Error('MobileSAM expansion session release failed');
185
+ if (!result)
186
+ throw new Error('MobileSAM completed without an expansion result');
187
+ return result;
188
+ }
@@ -1,149 +1,6 @@
1
- import type { NativeMobileSamExpansionReply, NativeSegmentationReply, NativeSegmentationRequest } from '../native-protocol.js';
2
- import type { NativeObjectProposalReply } from '../native-protocol.js';
3
- export declare function probeMobileSam(options: Extract<NativeSegmentationRequest, {
4
- kind: 'probe-mobile-sam';
5
- }>['options']): Promise<void>;
6
- interface MaskCandidate {
7
- token: number;
8
- score: number;
9
- mask: Uint8Array;
10
- }
11
- /** Sort by model quality and use decoder token order as a deterministic tie break. */
12
- export declare function selectBestMaskCandidate<T extends MaskCandidate>(candidates: readonly T[]): T | undefined;
13
- export declare function mobileSamSegmentationPrompt(input: {
14
- prompt: 'whole-region' | 'seed-region';
15
- seedRegion: {
16
- left: number;
17
- top: number;
18
- width: number;
19
- height: number;
20
- };
21
- wholeRegion: {
22
- left: number;
23
- top: number;
24
- width: number;
25
- height: number;
26
- };
27
- xScale: number;
28
- yScale: number;
29
- }): {
30
- coordinates: Float32Array<ArrayBuffer>;
31
- labels: Float32Array<ArrayBuffer>;
32
- };
33
- export declare function collectMobileSamSegmentationCandidates(input: {
34
- width: number;
35
- height: number;
36
- rgba: Uint8Array;
37
- logits: Float32Array;
38
- scores: Float32Array;
39
- roi: {
40
- left: number;
41
- top: number;
42
- width: number;
43
- height: number;
44
- };
45
- seedRegion: {
46
- left: number;
47
- top: number;
48
- width: number;
49
- height: number;
50
- };
51
- prompt: 'whole-region' | 'seed-region';
52
- }): MaskCandidate[];
53
- export declare function selectMobileSamSegmentationMask(input: {
54
- candidates: readonly MaskCandidate[];
55
- width: number;
56
- height: number;
57
- prompt: 'whole-region' | 'seed-region';
58
- }): Uint8Array;
59
- interface EnclosingMobileSamCandidate {
60
- token: number;
61
- score: number;
62
- mask: Uint8Array;
63
- area: number;
64
- bounds: {
65
- left: number;
66
- top: number;
67
- right: number;
68
- bottom: number;
69
- };
70
- }
71
- /** Full-source segmentation masks use the segmentation pixel cap, not the proposal cap. */
72
- export declare function packFullSourceBinaryMask(mask: Uint8Array, width: number, height: number): Uint8Array;
73
- /** Strictly decode the LSB-first full-source mask representation used by expansion IPC. */
74
- export declare function unpackFullSourceBinaryMask(data: Uint8Array, width: number, height: number): Uint8Array;
75
- /** Validate a prior full-source mask and return its foreground area and bounds. */
76
- export declare function validateMobileSamPreviousMask(input: {
77
- width: number;
78
- height: number;
79
- rgba: Uint8Array;
80
- previousMask: Uint8Array;
81
- roi: {
82
- left: number;
83
- top: number;
84
- width: number;
85
- height: number;
86
- };
87
- }): {
88
- area: number;
89
- bounds: {
90
- left: number;
91
- top: number;
92
- right: number;
93
- bottom: number;
94
- };
95
- };
96
- /** Select the smallest actual decoder mask that strictly encloses prior foreground. */
97
- export declare function selectEnclosingMobileSamCandidate(input: {
98
- width: number;
99
- height: number;
100
- rgba: Uint8Array;
101
- logits: Float32Array;
102
- scores: Float32Array;
103
- previousMask: Uint8Array;
104
- roi: {
105
- left: number;
106
- top: number;
107
- width: number;
108
- height: number;
109
- };
110
- }): EnclosingMobileSamCandidate | undefined;
111
- export declare function runMobileSamTask(request: Extract<NativeSegmentationRequest, {
112
- kind: 'mobile-sam';
113
- }>): Promise<NativeSegmentationReply>;
114
- type ExpansionRequest = Extract<NativeSegmentationRequest, {
115
- kind: 'mobile-sam-expand';
116
- }>;
117
- /** Run one same-seed decode and return only the smallest strict enclosing mask. */
118
- export declare function runMobileSamExpandTask(request: ExpansionRequest): Promise<NativeMobileSamExpansionReply>;
119
- type ProposalRequest = Extract<NativeSegmentationRequest, {
120
- kind: 'mobile-sam-proposals';
121
- }>;
122
- /** Produce the fixed row-major 3x3 prompt grid from the actual seed rectangle. */
123
- export declare function mobileSamProposalPoints(rect: {
124
- left: number;
125
- top: number;
126
- width: number;
127
- height: number;
128
- }): Array<{
129
- x: number;
130
- y: number;
131
- }>;
132
- /** Turn one decoder response into four owned packed candidates immediately. */
133
- export declare function packMobileSamProposalCandidates(input: {
134
- width: number;
135
- height: number;
136
- rgba: Uint8Array;
137
- roi: {
138
- left: number;
139
- top: number;
140
- width: number;
141
- height: number;
142
- };
143
- pointIndex: number;
144
- logits: Float32Array;
145
- scores: Float32Array;
146
- }): NativeObjectProposalReply['proposals'];
147
- /** One encoder pass followed by nine independent positive-point decoder calls. */
148
- export declare function runMobileSamObjectProposalTask(request: ProposalRequest): Promise<NativeObjectProposalReply>;
149
- export {};
1
+ export { probeMobileSam } from './worker-models.js';
2
+ export { runMobileSamTask } from './worker-segment.js';
3
+ export { runMobileSamExpandTask } from './worker-expand.js';
4
+ export { runMobileSamObjectProposalTask } from './worker-proposals.js';
5
+ export { selectBestMaskCandidate, mobileSamSegmentationPrompt, collectMobileSamSegmentationCandidates, selectMobileSamSegmentationMask, packFullSourceBinaryMask, unpackFullSourceBinaryMask, validateMobileSamPreviousMask, selectEnclosingMobileSamCandidate, } from './segmentation-masks.js';
6
+ export { mobileSamProposalPoints, packMobileSamProposalCandidates } from './proposal-masks.js';