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
@@ -1,66 +1,3 @@
1
- import { z } from 'zod';
2
- import type { SegmentationPort } from '../../../entities/segmentation.js';
3
- import type { ObjectProposalPort } from '../../../entities/object-proposals.js';
4
- import { NativeChildPool } from '../native-child.js';
5
- import { type NativeSegmentationRequest } from '../native-protocol.js';
6
- declare const optionsSchema: z.ZodObject<{
7
- encoderPath: z.ZodString;
8
- decoderPath: z.ZodString;
9
- executionProvider: z.ZodDefault<z.ZodEnum<["cpu", "dml"]>>;
10
- deviceId: z.ZodOptional<z.ZodNumber>;
11
- threads: z.ZodDefault<z.ZodNumber>;
12
- }, "strict", z.ZodTypeAny, {
13
- encoderPath: string;
14
- decoderPath: string;
15
- executionProvider: "cpu" | "dml";
16
- threads: number;
17
- deviceId?: number | undefined;
18
- }, {
19
- encoderPath: string;
20
- decoderPath: string;
21
- executionProvider?: "cpu" | "dml" | undefined;
22
- deviceId?: number | undefined;
23
- threads?: number | undefined;
24
- }>;
25
- /** @experimental Options for the unqualified MobileSAM segmentation factory. */
26
- export type MobileSamSegmenterOptions = z.input<typeof optionsSchema>;
27
- declare const proposalOptionsSchema: z.ZodObject<{
28
- encoderPath: z.ZodString;
29
- decoderPath: z.ZodString;
30
- executionProvider: z.ZodDefault<z.ZodLiteral<"cpu">>;
31
- threads: z.ZodDefault<z.ZodNumber>;
32
- }, "strict", z.ZodTypeAny, {
33
- encoderPath: string;
34
- decoderPath: string;
35
- executionProvider: "cpu";
36
- threads: number;
37
- }, {
38
- encoderPath: string;
39
- decoderPath: string;
40
- executionProvider?: "cpu" | undefined;
41
- threads?: number | undefined;
42
- }>;
43
- /** @experimental Options for the unqualified MobileSAM object-proposal factory. */
44
- export type MobileSamObjectProposalsOptions = z.input<typeof proposalOptionsSchema>;
45
- type SerializedTask = (Omit<Extract<NativeSegmentationRequest, {
46
- kind: 'mobile-sam';
47
- }>, 'id' | 'rgba'> & {
48
- rgba: string;
49
- }) | (Omit<Extract<NativeSegmentationRequest, {
50
- kind: 'mobile-sam-expand';
51
- }>, 'id' | 'rgba'> & {
52
- rgba: string;
53
- });
54
- /**
55
- * Explicit pinned MobileSAM inference in a separately owned Node child process.
56
- * @experimental Native segmentation adapter; unqualified for general mask or object accuracy.
57
- */
58
- export declare function createMobileSamSegmenter(options: MobileSamSegmenterOptions): Promise<SegmentationPort>;
59
- /** Internal adapter seam: pool ownership stays with the caller. */
60
- export declare function createMobileSamPortForPool(pool: Pick<NativeChildPool<SerializedTask>, 'runLazy' | 'settle' | 'dispose'>, parsed: z.output<typeof optionsSchema>): SegmentationPort & Required<Pick<SegmentationPort, 'expand'>>;
61
- /**
62
- * Explicit CPU-only, caller-owned one-shot 3x3-point / 36-mask proposal adapter.
63
- * @experimental Native proposal adapter; unqualified for general spatial or mask accuracy.
64
- */
65
- export declare function createMobileSamObjectProposals(options: MobileSamObjectProposalsOptions): Promise<ObjectProposalPort>;
66
- export {};
1
+ export type { MobileSamSegmenterOptions, MobileSamObjectProposalsOptions } from './options.js';
2
+ export { createMobileSamSegmenter, createMobileSamPortForPool } from './segmenter.js';
3
+ export { createMobileSamObjectProposals } from './object-proposals.js';
@@ -1,478 +1,2 @@
1
- import { performance } from 'node:perf_hooks';
2
- import { ClassifierError, assertActive } from '../../../entities/errors.js';
3
- import { z } from 'zod';
4
- import { objectProposalLimits } from '../../../entities/object-proposals.js';
5
- import { limits, pixelRectSchema } from '../../../entities/contracts.js';
6
- import { NativeChildPool } from '../native-child.js';
7
- import { verifyPinnedAsset } from '../verify-asset.js';
8
- import { probeBackend } from '../probe-backend.js';
9
- import { nativeErrorReplySchema, nativeMobileSamExpansionReplySchema, nativeObjectProposalReplySchema, nativeSegmentationReplySchema, } from '../native-protocol.js';
10
- import { packFullSourceBinaryMask, unpackFullSourceBinaryMask, validateMobileSamPreviousMask, } from './worker-inference.js';
11
- const optionsSchema = z
12
- .object({
13
- encoderPath: z.string().min(1).max(4096),
14
- decoderPath: z.string().min(1).max(4096),
15
- executionProvider: z.enum(['cpu', 'dml']).default('cpu'),
16
- deviceId: z.number().int().min(0).max(32).optional(),
17
- threads: z.number().int().min(1).max(8).default(2),
18
- })
19
- .strict();
20
- const proposalOptionsSchema = z
21
- .object({
22
- encoderPath: z.string().min(1).max(4096),
23
- decoderPath: z.string().min(1).max(4096),
24
- executionProvider: z.literal('cpu').default('cpu'),
25
- threads: z.number().int().min(1).max(8).default(2),
26
- })
27
- .strict();
28
- /**
29
- * Explicit pinned MobileSAM inference in a separately owned Node child process.
30
- * @experimental Native segmentation adapter; unqualified for general mask or object accuracy.
31
- */
32
- export async function createMobileSamSegmenter(options) {
33
- const checked = optionsSchema.safeParse(options);
34
- if (!checked.success)
35
- throw new ClassifierError('INVALID_INPUT', 'Invalid MobileSAM options');
36
- const parsed = checked.data;
37
- if (parsed.executionProvider === 'dml' && process.platform !== 'win32')
38
- throw new ClassifierError('UNSUPPORTED', 'DirectML is supported only on Windows');
39
- try {
40
- import.meta.resolve('onnxruntime-node');
41
- }
42
- catch {
43
- throw new ClassifierError('UNSUPPORTED', 'onnxruntime-node is not installed');
44
- }
45
- await verifyPinnedAsset(parsed.encoderPath, {
46
- bytes: 28_157_093,
47
- sha256: '580f5fb648ea1062c0aabc26217aed56921985f03f0cbbd852bba81d760cc749',
48
- }, 'MobileSAM encoder');
49
- await verifyPinnedAsset(parsed.decoderPath, {
50
- bytes: 16_496_559,
51
- sha256: '8976b90a87ba50a6a72217a5ff994f7d25ce16f2229fcc1ed259e1294c622ffe',
52
- }, 'MobileSAM decoder');
53
- const sourceWorker = import.meta.url.endsWith('.ts');
54
- const workerUrl = new URL(sourceWorker ? '../native-worker.ts' : '../native-worker.js', import.meta.url);
55
- await probeBackend(workerUrl, 'mobile-sam', { options: parsed });
56
- const pool = new NativeChildPool({
57
- workerUrl,
58
- maxActive: 1,
59
- maxWaiting: 2,
60
- jobTimeoutMs: 10_000,
61
- abortGraceMs: 250,
62
- encode: (job) => Buffer.from(JSON.stringify({ id: job.id, ...job.payload })),
63
- decode: (raw, id) => {
64
- const error = nativeErrorReplySchema.safeParse(raw);
65
- if (error.success) {
66
- if (error.data.id !== id)
67
- throw new ClassifierError('INVALID_RESPONSE', 'Native child error ID mismatch');
68
- throw new ClassifierError(error.data.code, error.data.message);
69
- }
70
- const reply = nativeSegmentationReplySchema.safeParse(raw);
71
- if (reply.success && reply.data.id === id)
72
- return reply.data;
73
- const expansion = nativeMobileSamExpansionReplySchema.safeParse(raw);
74
- if (!expansion.success || expansion.data.id !== id)
75
- throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid or mismatched reply');
76
- return expansion.data;
77
- },
78
- });
79
- return createMobileSamPortForPool(pool, parsed);
80
- }
81
- /** Internal adapter seam: pool ownership stays with the caller. */
82
- export function createMobileSamPortForPool(pool, parsed) {
83
- return {
84
- async segment(input) {
85
- const started = performance.now();
86
- assertActive(input.signal);
87
- let image;
88
- let maskData;
89
- try {
90
- const raw = await pool.runLazy(input.signal, () => {
91
- image = snapshotInput(input);
92
- const rgba = Buffer.from(image.data);
93
- let rgbaBase64;
94
- try {
95
- rgbaBase64 = rgba.toString('base64');
96
- }
97
- finally {
98
- rgba.fill(0);
99
- }
100
- return {
101
- kind: 'mobile-sam',
102
- width: image.width,
103
- height: image.height,
104
- rgba: rgbaBase64,
105
- seedRegion: image.seedRegion,
106
- wholeRegion: image.wholeRegion,
107
- roi: image.roi,
108
- prompt: image.prompt,
109
- options: parsed,
110
- };
111
- });
112
- if (!image)
113
- throw new ClassifierError('INVALID_RESPONSE', 'Native segmentation omitted its input snapshot');
114
- const checkedReply = nativeSegmentationReplySchema.safeParse(raw);
115
- if (!checkedReply.success ||
116
- checkedReply.data.width !== image.width ||
117
- checkedReply.data.height !== image.height)
118
- throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid segmentation reply');
119
- maskData = Buffer.from(checkedReply.data.mask, 'base64');
120
- if (maskData.length !== image.width * image.height ||
121
- maskData.toString('base64') !== checkedReply.data.mask)
122
- throw new ClassifierError('INVALID_RESPONSE', 'Native child mask dimensions or encoding are invalid');
123
- return {
124
- status: 'segmented',
125
- mask: {
126
- data: new Uint8Array(maskData),
127
- width: image.width,
128
- height: image.height,
129
- format: 'binary-u8',
130
- },
131
- metadata: {
132
- method: checkedReply.data.method,
133
- backend: 'mobile-sam',
134
- durationMs: Math.max(0, performance.now() - started),
135
- input: { width: image.width, height: image.height },
136
- },
137
- };
138
- }
139
- finally {
140
- maskData?.fill(0);
141
- image?.data.fill(0);
142
- }
143
- },
144
- async expand(input, previousMask) {
145
- const started = performance.now();
146
- assertActive(input.signal);
147
- let image;
148
- let previous;
149
- let resultData;
150
- let transferred = false;
151
- try {
152
- const raw = await pool.runLazy(input.signal, () => {
153
- image = snapshotInput(input);
154
- previous = snapshotPreviousMask(input, previousMask, image);
155
- const packedPrevious = packFullSourceBinaryMask(previous, image.width, image.height);
156
- let previousMaskBase64;
157
- try {
158
- const packedBuffer = Buffer.from(packedPrevious);
159
- try {
160
- previousMaskBase64 = packedBuffer.toString('base64');
161
- }
162
- finally {
163
- packedBuffer.fill(0);
164
- }
165
- }
166
- finally {
167
- packedPrevious.fill(0);
168
- }
169
- const rgbaBuffer = Buffer.from(image.data);
170
- let rgbaBase64;
171
- try {
172
- rgbaBase64 = rgbaBuffer.toString('base64');
173
- }
174
- finally {
175
- rgbaBuffer.fill(0);
176
- }
177
- return {
178
- kind: 'mobile-sam-expand',
179
- width: image.width,
180
- height: image.height,
181
- rgba: rgbaBase64,
182
- previousMask: previousMaskBase64,
183
- seedRegion: image.seedRegion,
184
- wholeRegion: image.wholeRegion,
185
- roi: image.roi,
186
- prompt: image.prompt,
187
- options: parsed,
188
- };
189
- });
190
- if (!image || !previous)
191
- throw new ClassifierError('INVALID_RESPONSE', 'Native expansion omitted its input snapshot');
192
- const checkedReply = nativeMobileSamExpansionReplySchema.safeParse(raw);
193
- if (!checkedReply.success ||
194
- checkedReply.data.width !== image.width ||
195
- checkedReply.data.height !== image.height)
196
- throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid expansion reply');
197
- if (checkedReply.data.status === 'unknown')
198
- return {
199
- status: 'unknown',
200
- reason: 'no-enclosing-mask',
201
- metadata: {
202
- method: checkedReply.data.method,
203
- backend: 'mobile-sam',
204
- durationMs: Math.max(0, performance.now() - started),
205
- input: { width: image.width, height: image.height },
206
- },
207
- };
208
- const packedBytes = Buffer.from(checkedReply.data.mask, 'base64');
209
- let data;
210
- try {
211
- if (packedBytes.toString('base64') !== checkedReply.data.mask)
212
- throw new ClassifierError('INVALID_RESPONSE', 'Native expansion mask encoding is invalid');
213
- try {
214
- data = unpackFullSourceBinaryMask(packedBytes, image.width, image.height);
215
- resultData = data;
216
- }
217
- catch {
218
- throw new ClassifierError('INVALID_RESPONSE', 'Native expansion packed mask is invalid');
219
- }
220
- }
221
- finally {
222
- packedBytes.fill(0);
223
- }
224
- let oldStats;
225
- let newStats;
226
- try {
227
- oldStats = validateMobileSamPreviousMask({
228
- width: image.width,
229
- height: image.height,
230
- rgba: image.data,
231
- previousMask: previous,
232
- roi: image.roi,
233
- });
234
- newStats = validateMobileSamPreviousMask({
235
- width: image.width,
236
- height: image.height,
237
- rgba: image.data,
238
- previousMask: data,
239
- roi: image.roi,
240
- });
241
- }
242
- catch {
243
- data.fill(0);
244
- throw new ClassifierError('INVALID_RESPONSE', 'Native expanded mask is invalid');
245
- }
246
- let hasNewForeground = false;
247
- for (let index = 0; index < data.length; index++) {
248
- if (previous[index] === 1 && data[index] !== 1) {
249
- data.fill(0);
250
- throw new ClassifierError('INVALID_RESPONSE', 'Native expansion lost prior foreground');
251
- }
252
- if (data[index] === 1 && previous[index] === 0)
253
- hasNewForeground = true;
254
- }
255
- if (!hasNewForeground ||
256
- newStats.area <= oldStats.area ||
257
- newStats.bounds.left > oldStats.bounds.left ||
258
- newStats.bounds.top > oldStats.bounds.top ||
259
- newStats.bounds.right < oldStats.bounds.right ||
260
- newStats.bounds.bottom < oldStats.bounds.bottom ||
261
- (newStats.bounds.left === oldStats.bounds.left &&
262
- newStats.bounds.top === oldStats.bounds.top &&
263
- newStats.bounds.right === oldStats.bounds.right &&
264
- newStats.bounds.bottom === oldStats.bounds.bottom)) {
265
- data.fill(0);
266
- throw new ClassifierError('INVALID_RESPONSE', 'Native expansion is not a strict enclosing mask');
267
- }
268
- transferred = true;
269
- return {
270
- status: 'segmented',
271
- mask: { data, width: image.width, height: image.height, format: 'binary-u8' },
272
- metadata: {
273
- method: checkedReply.data.method,
274
- backend: 'mobile-sam',
275
- durationMs: Math.max(0, performance.now() - started),
276
- input: { width: image.width, height: image.height },
277
- qualityScore: checkedReply.data.qualityScore,
278
- },
279
- };
280
- }
281
- finally {
282
- image?.data.fill(0);
283
- previous?.fill(0);
284
- if (resultData && !transferred)
285
- resultData.fill(0);
286
- }
287
- },
288
- settle: () => pool.settle(),
289
- dispose: () => pool.dispose(),
290
- };
291
- }
292
- /**
293
- * Explicit CPU-only, caller-owned one-shot 3x3-point / 36-mask proposal adapter.
294
- * @experimental Native proposal adapter; unqualified for general spatial or mask accuracy.
295
- */
296
- export async function createMobileSamObjectProposals(options) {
297
- const checked = proposalOptionsSchema.safeParse(options);
298
- if (!checked.success)
299
- throw new ClassifierError('INVALID_INPUT', 'Invalid MobileSAM proposal options');
300
- const parsed = checked.data;
301
- try {
302
- import.meta.resolve('onnxruntime-node');
303
- }
304
- catch {
305
- throw new ClassifierError('UNSUPPORTED', 'onnxruntime-node is not installed');
306
- }
307
- await verifyPinnedAsset(parsed.encoderPath, {
308
- bytes: 28_157_093,
309
- sha256: '580f5fb648ea1062c0aabc26217aed56921985f03f0cbbd852bba81d760cc749',
310
- }, 'MobileSAM encoder');
311
- await verifyPinnedAsset(parsed.decoderPath, {
312
- bytes: 16_496_559,
313
- sha256: '8976b90a87ba50a6a72217a5ff994f7d25ce16f2229fcc1ed259e1294c622ffe',
314
- }, 'MobileSAM decoder');
315
- const sourceWorker = import.meta.url.endsWith('.ts');
316
- const workerUrl = new URL(sourceWorker ? '../native-worker.ts' : '../native-worker.js', import.meta.url);
317
- // Probe the explicitly requested CPU provider in an owned child, disposing it before return.
318
- await probeBackend(workerUrl, 'mobile-sam', { options: { ...parsed, executionProvider: 'cpu' } });
319
- const pool = new NativeChildPool({
320
- workerUrl,
321
- maxActive: 1,
322
- maxWaiting: 2,
323
- jobTimeoutMs: 10_000,
324
- abortGraceMs: 250,
325
- encode: (job) => Buffer.from(JSON.stringify({ id: job.id, ...job.payload })),
326
- decode: (raw, id) => {
327
- const error = nativeErrorReplySchema.safeParse(raw);
328
- if (error.success) {
329
- if (error.data.id !== id)
330
- throw new ClassifierError('INVALID_RESPONSE', 'Native child error ID mismatch');
331
- throw new ClassifierError(error.data.code, error.data.message);
332
- }
333
- const reply = nativeObjectProposalReplySchema.safeParse(raw);
334
- if (!reply.success || reply.data.id !== id)
335
- throw new ClassifierError('INVALID_RESPONSE', 'Native child returned invalid proposals');
336
- return reply.data;
337
- },
338
- });
339
- return {
340
- async propose(input) {
341
- const started = performance.now();
342
- assertActive(input.signal);
343
- let image;
344
- const raw = await pool.runLazy(input.signal, () => {
345
- const snapshot = snapshotProposalInput(input);
346
- image = snapshot.image;
347
- return {
348
- kind: 'mobile-sam-proposals',
349
- width: image.width,
350
- height: image.height,
351
- rgba: Buffer.from(image.data).toString('base64'),
352
- seedRegion: snapshot.seedRegion,
353
- roi: snapshot.roi,
354
- options: { ...parsed, executionProvider: 'cpu' },
355
- };
356
- });
357
- const reply = nativeObjectProposalReplySchema.parse(raw);
358
- if (reply.width !== image.width || reply.height !== image.height)
359
- throw new ClassifierError('INVALID_RESPONSE', 'Proposal dimensions do not match the input');
360
- const expectedBytes = Math.ceil((image.width * image.height) / 8);
361
- let aggregateBytes = 0;
362
- const proposals = reply.proposals.map((proposal, index) => {
363
- const expectedId = `C${String(index).padStart(2, '0')}`;
364
- const bytes = Buffer.from(proposal.mask, 'base64');
365
- aggregateBytes += bytes.byteLength;
366
- if (proposal.id !== expectedId ||
367
- bytes.byteLength !== expectedBytes ||
368
- bytes.toString('base64') !== proposal.mask ||
369
- aggregateBytes > objectProposalLimits.packedBytes)
370
- throw new ClassifierError('INVALID_RESPONSE', 'Proposal masks are malformed or oversized');
371
- const unusedBits = expectedBytes * 8 - image.width * image.height;
372
- const allowed = unusedBits ? (1 << (8 - unusedBits)) - 1 : 255;
373
- if ((bytes[expectedBytes - 1] & ~allowed) !== 0)
374
- throw new ClassifierError('INVALID_RESPONSE', 'Proposal mask has nonzero padding bits');
375
- return {
376
- id: proposal.id,
377
- mask: {
378
- data: new Uint8Array(bytes),
379
- width: image.width,
380
- height: image.height,
381
- format: 'binary-lsb0',
382
- },
383
- qualityScore: proposal.qualityScore,
384
- };
385
- });
386
- return {
387
- proposals,
388
- metadata: {
389
- method: 'mobile-sam-nine-point-proposals-v1',
390
- backend: 'mobile-sam',
391
- durationMs: Math.max(0, performance.now() - started),
392
- input: { width: image.width, height: image.height },
393
- },
394
- };
395
- },
396
- settle: () => pool.settle(),
397
- dispose: () => pool.dispose(),
398
- };
399
- }
400
- function snapshotInput(input) {
401
- const image = input.image;
402
- const pixels = image.width * image.height;
403
- const prompt = input.prompt ?? 'whole-region';
404
- if (image.format !== 'rgba8' ||
405
- !(image.data instanceof Uint8Array) ||
406
- !Number.isSafeInteger(image.width) ||
407
- !Number.isSafeInteger(image.height) ||
408
- !Number.isSafeInteger(pixels) ||
409
- image.width < 1 ||
410
- image.height < 1 ||
411
- pixels > limits.imagePixels ||
412
- image.data.length !== pixels * 4)
413
- throw new ClassifierError('INVALID_INPUT', 'Segmentation requires canonical RGBA8 within the image pixel limit');
414
- if ((prompt !== 'whole-region' && prompt !== 'seed-region') ||
415
- !pixelRectSchema.safeParse(input.seedRegion).success ||
416
- !pixelRectSchema.safeParse(input.wholeRegion).success ||
417
- !pixelRectSchema.safeParse(input.roi).success)
418
- throw new ClassifierError('INVALID_INPUT', 'Segmentation regions must be positive integer rectangles');
419
- for (const region of [input.seedRegion, input.wholeRegion, input.roi])
420
- if (region.left + region.width > image.width || region.top + region.height > image.height)
421
- throw new ClassifierError('INVALID_INPUT', 'Segmentation region lies outside the source image');
422
- return {
423
- data: new Uint8Array(image.data),
424
- width: image.width,
425
- height: image.height,
426
- seedRegion: { ...input.seedRegion },
427
- wholeRegion: { ...input.wholeRegion },
428
- roi: { ...input.roi },
429
- prompt,
430
- };
431
- }
432
- function snapshotPreviousMask(input, previousMask, image) {
433
- if (!previousMask ||
434
- previousMask.format !== 'binary-u8' ||
435
- !(previousMask.data instanceof Uint8Array) ||
436
- previousMask.width !== image.width ||
437
- previousMask.height !== image.height ||
438
- previousMask.data.length !== image.width * image.height)
439
- throw new ClassifierError('INVALID_INPUT', 'Expansion requires a full-source binary mask');
440
- try {
441
- validateMobileSamPreviousMask({
442
- width: image.width,
443
- height: image.height,
444
- rgba: image.data,
445
- previousMask: previousMask.data,
446
- roi: image.roi,
447
- });
448
- }
449
- catch {
450
- throw new ClassifierError('INVALID_INPUT', 'Expansion mask must be nonempty binary foreground inside visible ROI pixels');
451
- }
452
- return new Uint8Array(previousMask.data);
453
- }
454
- function snapshotProposalInput(input) {
455
- const { image } = input;
456
- const pixels = image.width * image.height;
457
- if (pixels > objectProposalLimits.imagePixels)
458
- throw new ClassifierError('RESOURCE_LIMIT', 'Proposal image exceeds the two-megapixel limit');
459
- if (image.format !== 'rgba8' ||
460
- !(image.data instanceof Uint8Array) ||
461
- !Number.isSafeInteger(image.width) ||
462
- !Number.isSafeInteger(image.height) ||
463
- !Number.isSafeInteger(pixels) ||
464
- image.width < 1 ||
465
- image.height < 1 ||
466
- image.data.length !== pixels * 4)
467
- throw new ClassifierError('INVALID_INPUT', 'Proposals require canonical RGBA8 pixels');
468
- for (const region of [input.seedRegion, input.roi])
469
- if (!pixelRectSchema.safeParse(region).success ||
470
- region.left + region.width > image.width ||
471
- region.top + region.height > image.height)
472
- throw new ClassifierError('INVALID_INPUT', 'Proposal region lies outside the source image');
473
- return {
474
- image: { data: new Uint8Array(image.data), width: image.width, height: image.height },
475
- seedRegion: { ...input.seedRegion },
476
- roi: { ...input.roi },
477
- };
478
- }
1
+ export { createMobileSamSegmenter, createMobileSamPortForPool } from './segmenter.js';
2
+ export { createMobileSamObjectProposals } from './object-proposals.js';
@@ -0,0 +1,46 @@
1
+ import type { BinaryMask, SegmentationInput } from '../../../entities/segmentation.js';
2
+ import type { ObjectProposalInput } from '../../../entities/object-proposals.js';
3
+ export declare function snapshotInput(input: SegmentationInput): {
4
+ data: Uint8Array<ArrayBuffer>;
5
+ width: number;
6
+ height: number;
7
+ seedRegion: {
8
+ left: number;
9
+ top: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+ wholeRegion: {
14
+ left: number;
15
+ top: number;
16
+ width: number;
17
+ height: number;
18
+ };
19
+ roi: {
20
+ left: number;
21
+ top: number;
22
+ width: number;
23
+ height: number;
24
+ };
25
+ prompt: "whole-region" | "seed-region";
26
+ };
27
+ export declare function snapshotPreviousMask(input: SegmentationInput, previousMask: BinaryMask, image: ReturnType<typeof snapshotInput>): Uint8Array<ArrayBuffer>;
28
+ export declare function snapshotProposalInput(input: ObjectProposalInput): {
29
+ image: {
30
+ data: Uint8Array<ArrayBuffer>;
31
+ width: number;
32
+ height: number;
33
+ };
34
+ seedRegion: {
35
+ left: number;
36
+ top: number;
37
+ width: number;
38
+ height: number;
39
+ };
40
+ roi: {
41
+ left: number;
42
+ top: number;
43
+ width: number;
44
+ height: number;
45
+ };
46
+ };
@@ -0,0 +1,83 @@
1
+ import { ClassifierError } from '../../../entities/errors.js';
2
+ import { objectProposalLimits } from '../../../entities/object-proposals.js';
3
+ import { limits, pixelRectSchema } from '../../../entities/contracts.js';
4
+ import { validateMobileSamPreviousMask } from './segmentation-masks.js';
5
+ export function snapshotInput(input) {
6
+ const image = input.image;
7
+ const pixels = image.width * image.height;
8
+ const prompt = input.prompt ?? 'whole-region';
9
+ if (image.format !== 'rgba8' ||
10
+ !(image.data instanceof Uint8Array) ||
11
+ !Number.isSafeInteger(image.width) ||
12
+ !Number.isSafeInteger(image.height) ||
13
+ !Number.isSafeInteger(pixels) ||
14
+ image.width < 1 ||
15
+ image.height < 1 ||
16
+ pixels > limits.imagePixels ||
17
+ image.data.length !== pixels * 4)
18
+ throw new ClassifierError('INVALID_INPUT', 'Segmentation requires canonical RGBA8 within the image pixel limit');
19
+ if ((prompt !== 'whole-region' && prompt !== 'seed-region') ||
20
+ !pixelRectSchema.safeParse(input.seedRegion).success ||
21
+ !pixelRectSchema.safeParse(input.wholeRegion).success ||
22
+ !pixelRectSchema.safeParse(input.roi).success)
23
+ throw new ClassifierError('INVALID_INPUT', 'Segmentation regions must be positive integer rectangles');
24
+ for (const region of [input.seedRegion, input.wholeRegion, input.roi])
25
+ if (region.left + region.width > image.width || region.top + region.height > image.height)
26
+ throw new ClassifierError('INVALID_INPUT', 'Segmentation region lies outside the source image');
27
+ return {
28
+ data: new Uint8Array(image.data),
29
+ width: image.width,
30
+ height: image.height,
31
+ seedRegion: { ...input.seedRegion },
32
+ wholeRegion: { ...input.wholeRegion },
33
+ roi: { ...input.roi },
34
+ prompt,
35
+ };
36
+ }
37
+ export function snapshotPreviousMask(input, previousMask, image) {
38
+ if (!previousMask ||
39
+ previousMask.format !== 'binary-u8' ||
40
+ !(previousMask.data instanceof Uint8Array) ||
41
+ previousMask.width !== image.width ||
42
+ previousMask.height !== image.height ||
43
+ previousMask.data.length !== image.width * image.height)
44
+ throw new ClassifierError('INVALID_INPUT', 'Expansion requires a full-source binary mask');
45
+ try {
46
+ validateMobileSamPreviousMask({
47
+ width: image.width,
48
+ height: image.height,
49
+ rgba: image.data,
50
+ previousMask: previousMask.data,
51
+ roi: image.roi,
52
+ });
53
+ }
54
+ catch {
55
+ throw new ClassifierError('INVALID_INPUT', 'Expansion mask must be nonempty binary foreground inside visible ROI pixels');
56
+ }
57
+ return new Uint8Array(previousMask.data);
58
+ }
59
+ export function snapshotProposalInput(input) {
60
+ const { image } = input;
61
+ const pixels = image.width * image.height;
62
+ if (pixels > objectProposalLimits.imagePixels)
63
+ throw new ClassifierError('RESOURCE_LIMIT', 'Proposal image exceeds the two-megapixel limit');
64
+ if (image.format !== 'rgba8' ||
65
+ !(image.data instanceof Uint8Array) ||
66
+ !Number.isSafeInteger(image.width) ||
67
+ !Number.isSafeInteger(image.height) ||
68
+ !Number.isSafeInteger(pixels) ||
69
+ image.width < 1 ||
70
+ image.height < 1 ||
71
+ image.data.length !== pixels * 4)
72
+ throw new ClassifierError('INVALID_INPUT', 'Proposals require canonical RGBA8 pixels');
73
+ for (const region of [input.seedRegion, input.roi])
74
+ if (!pixelRectSchema.safeParse(region).success ||
75
+ region.left + region.width > image.width ||
76
+ region.top + region.height > image.height)
77
+ throw new ClassifierError('INVALID_INPUT', 'Proposal region lies outside the source image');
78
+ return {
79
+ image: { data: new Uint8Array(image.data), width: image.width, height: image.height },
80
+ seedRegion: { ...input.seedRegion },
81
+ roi: { ...input.roi },
82
+ };
83
+ }