zorya-classifier 0.2.0 → 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.
- package/README.md +24 -8
- package/dist/app/classifier-config.d.ts +30 -0
- package/dist/app/classifier-config.js +46 -0
- package/dist/app/classifier-contracts.d.ts +32 -0
- package/dist/app/classifier-contracts.js +1 -0
- package/dist/app/classifier-representation.d.ts +5 -0
- package/dist/app/classifier-representation.js +33 -0
- package/dist/app/classifier-runtime.d.ts +7 -0
- package/dist/app/classifier-runtime.js +71 -0
- package/dist/app/classifier-spatial.d.ts +31 -0
- package/dist/app/classifier-spatial.js +78 -0
- package/dist/app/cli-arguments.d.ts +2 -2
- package/dist/app/create-classifier.d.ts +4 -29
- package/dist/app/create-classifier.js +37 -237
- package/dist/cli.js +1 -1
- package/dist/entities/classification.d.ts +98 -0
- package/dist/entities/classification.js +28 -0
- package/dist/entities/contracts.d.ts +6 -471
- package/dist/entities/contracts.js +6 -97
- package/dist/entities/decision.d.ts +211 -0
- package/dist/entities/decision.js +33 -0
- package/dist/entities/geometry.d.ts +37 -0
- package/dist/entities/geometry.js +26 -0
- package/dist/entities/image.d.ts +39 -0
- package/dist/entities/image.js +15 -0
- package/dist/entities/localization.d.ts +125 -0
- package/dist/entities/localization.js +13 -0
- package/dist/entities/object-proposals.d.ts +4 -1
- package/dist/entities/segmentation-metadata.d.ts +70 -0
- package/dist/entities/segmentation-metadata.js +21 -1
- package/dist/entities/segmentation-verification.d.ts +1 -0
- package/dist/entities/segmentation.d.ts +6 -1
- package/dist/entities/validation.d.ts +2 -0
- package/dist/entities/validation.js +9 -0
- package/dist/entities/whole-object.d.ts +4 -0
- package/dist/features/choice-beam-layout.js +2 -7
- package/dist/features/proposals/presentation.d.ts +21 -0
- package/dist/features/proposals/presentation.js +122 -0
- package/dist/features/proposals/validation.d.ts +138 -0
- package/dist/features/proposals/validation.js +125 -0
- package/dist/features/segmentation/box-check.js +10 -23
- package/dist/features/segmentation/mask.js +2 -7
- package/dist/features/segmentation/region-result.d.ts +18 -0
- package/dist/features/segmentation/region-result.js +171 -0
- package/dist/features/segmentation/source-mask.d.ts +6 -0
- package/dist/features/segmentation/source-mask.js +32 -0
- package/dist/features/segmentation/validation.d.ts +9 -0
- package/dist/features/segmentation/validation.js +94 -0
- package/dist/infrastructure/decision-transport/errors.d.ts +8 -0
- package/dist/infrastructure/decision-transport/errors.js +34 -0
- package/dist/infrastructure/decision-transport/images.d.ts +1 -0
- package/dist/infrastructure/decision-transport/images.js +52 -0
- package/dist/infrastructure/{openrouter-common.d.ts → decision-transport/request.d.ts} +6 -15
- package/dist/infrastructure/decision-transport/request.js +162 -0
- package/dist/infrastructure/decision-transport/response.d.ts +1 -0
- package/dist/infrastructure/decision-transport/response.js +72 -0
- package/dist/infrastructure/decision-transport/route-identity.d.ts +11 -0
- package/dist/infrastructure/decision-transport/route-identity.js +23 -0
- package/dist/infrastructure/openrouter-vision.js +1 -1
- package/dist/infrastructure/openrouter.js +7 -19
- package/dist/infrastructure/raster.js +3 -8
- package/dist/infrastructure/segmentation/grabcut/index.d.ts +5 -1
- package/dist/infrastructure/segmentation/grabcut/index.js +6 -5
- package/dist/infrastructure/segmentation/grabcut/masks.d.ts +49 -0
- package/dist/infrastructure/segmentation/grabcut/masks.js +89 -0
- package/dist/infrastructure/segmentation/grabcut/model-asset.d.ts +4 -0
- package/dist/infrastructure/segmentation/grabcut/model-asset.js +4 -0
- package/dist/infrastructure/segmentation/grabcut/runtime.d.ts +70 -0
- package/dist/infrastructure/segmentation/grabcut/runtime.js +96 -0
- package/dist/infrastructure/segmentation/grabcut/worker-inference.d.ts +4 -43
- package/dist/infrastructure/segmentation/grabcut/worker-inference.js +4 -187
- package/dist/infrastructure/segmentation/mobile-sam/index.d.ts +3 -58
- package/dist/infrastructure/segmentation/mobile-sam/index.js +2 -472
- package/dist/infrastructure/segmentation/mobile-sam/input.d.ts +46 -0
- package/dist/infrastructure/segmentation/mobile-sam/input.js +83 -0
- package/dist/infrastructure/segmentation/mobile-sam/model-assets.d.ts +8 -0
- package/dist/infrastructure/segmentation/mobile-sam/model-assets.js +8 -0
- package/dist/infrastructure/segmentation/mobile-sam/object-proposals.d.ts +7 -0
- package/dist/infrastructure/segmentation/mobile-sam/object-proposals.js +124 -0
- package/dist/infrastructure/segmentation/mobile-sam/options.d.ts +43 -0
- package/dist/infrastructure/segmentation/mobile-sam/options.js +19 -0
- package/dist/infrastructure/segmentation/mobile-sam/proposal-masks.d.ts +26 -0
- package/dist/infrastructure/segmentation/mobile-sam/proposal-masks.js +71 -0
- package/dist/infrastructure/segmentation/mobile-sam/segmentation-masks.d.ts +112 -0
- package/dist/infrastructure/segmentation/mobile-sam/segmentation-masks.js +230 -0
- package/dist/infrastructure/segmentation/mobile-sam/segmenter.d.ts +22 -0
- package/dist/infrastructure/segmentation/mobile-sam/segmenter.js +268 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-expand.d.ts +7 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-expand.js +188 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-inference.d.ts +6 -149
- package/dist/infrastructure/segmentation/mobile-sam/worker-inference.js +6 -845
- package/dist/infrastructure/segmentation/mobile-sam/worker-models.d.ts +14 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-models.js +72 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-proposals.d.ts +7 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-proposals.js +159 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-segment.d.ts +4 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-segment.js +126 -0
- package/dist/infrastructure/segmentation/native-protocol.d.ts +59 -34
- package/dist/infrastructure/segmentation/native-protocol.js +26 -16
- package/dist/infrastructure/segmentation/proposal-presentation-input.d.ts +20 -0
- package/dist/infrastructure/segmentation/proposal-presentation-input.js +107 -0
- package/dist/infrastructure/segmentation/proposal-presentation-pixels.d.ts +8 -0
- package/dist/infrastructure/segmentation/proposal-presentation-pixels.js +93 -0
- package/dist/infrastructure/segmentation/proposal-presentation.js +10 -201
- package/dist/infrastructure/vision-chat/options.d.ts +62 -0
- package/dist/infrastructure/vision-chat/options.js +46 -0
- package/dist/infrastructure/vision-chat/request.d.ts +23 -0
- package/dist/infrastructure/vision-chat/request.js +109 -0
- package/dist/infrastructure/vision-chat/response.d.ts +3 -0
- package/dist/infrastructure/vision-chat/response.js +105 -0
- package/dist/infrastructure/vision-chat.d.ts +4 -61
- package/dist/infrastructure/vision-chat.js +8 -271
- package/dist/openai-compatible.js +1 -1
- package/dist/processes/extract-from-region.js +11 -216
- package/dist/processes/extract.d.ts +2 -8
- package/dist/processes/extract.js +12 -128
- package/dist/processes/proposal-location.js +5 -251
- package/docs/0.2.0-plan.md +3 -3
- package/docs/README.ru.md +42 -23
- package/docs/acceptance.md +5 -5
- package/docs/agents.ru.md +95 -0
- package/docs/api.md +4 -2
- package/docs/architecture.md +33 -5
- package/docs/architecture.ru.md +146 -0
- package/docs/benchmarks.md +4 -2
- package/docs/development.ru.md +141 -0
- package/docs/operations.md +4 -4
- package/docs/provenance.json +5 -1
- package/docs/publishing.md +8 -0
- package/docs/refactoring-plan.ru.md +183 -0
- package/docs/v02-usage.md +8 -6
- package/docs/vision-results.md +2 -0
- package/package.json +5 -3
- package/dist/infrastructure/boundary-view.d.ts +0 -2
- package/dist/infrastructure/boundary-view.js +0 -263
- package/dist/infrastructure/openrouter-common.js +0 -317
- package/dist/infrastructure/text-representation.d.ts +0 -23
- package/dist/infrastructure/text-representation.js +0 -226
- package/docs/evidence/v02/categorical-heldout-v4/README.md +0 -35
- package/docs/evidence/v02/proposal-resource-soak-261dc23.md +0 -15
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InferenceSession } from 'onnxruntime-node';
|
|
2
|
+
import type { NativeSegmentationRequest } from '../native-protocol.js';
|
|
3
|
+
import { decoderIdentity, encoderIdentity } from './model-assets.js';
|
|
4
|
+
export declare function verifiedBytes(file: string, identity: {
|
|
5
|
+
bytes: number;
|
|
6
|
+
sha256: string;
|
|
7
|
+
}): Promise<Buffer<ArrayBuffer>>;
|
|
8
|
+
export declare function buildMobileSamSessionOptions(options: Extract<NativeSegmentationRequest, {
|
|
9
|
+
kind: 'mobile-sam';
|
|
10
|
+
}>['options']): InferenceSession.SessionOptions;
|
|
11
|
+
export declare function probeMobileSam(options: Extract<NativeSegmentationRequest, {
|
|
12
|
+
kind: 'probe-mobile-sam';
|
|
13
|
+
}>['options']): Promise<void>;
|
|
14
|
+
export { encoderIdentity, decoderIdentity };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { decoderIdentity, encoderIdentity } from './model-assets.js';
|
|
4
|
+
export async function verifiedBytes(file, identity) {
|
|
5
|
+
const handle = await open(file, 'r');
|
|
6
|
+
try {
|
|
7
|
+
const stat = await handle.stat();
|
|
8
|
+
if (!stat.isFile() || stat.size !== identity.bytes)
|
|
9
|
+
throw new Error('Pinned model size mismatch');
|
|
10
|
+
const bytes = Buffer.alloc(identity.bytes);
|
|
11
|
+
let offset = 0;
|
|
12
|
+
while (offset < bytes.length) {
|
|
13
|
+
const read = await handle.read(bytes, offset, bytes.length - offset, null);
|
|
14
|
+
if (read.bytesRead === 0)
|
|
15
|
+
throw new Error('Pinned model read was incomplete');
|
|
16
|
+
offset += read.bytesRead;
|
|
17
|
+
}
|
|
18
|
+
if ((await handle.read(Buffer.alloc(1), 0, 1, null)).bytesRead !== 0)
|
|
19
|
+
throw new Error('Pinned model changed while reading');
|
|
20
|
+
if (createHash('sha256').update(bytes).digest('hex') !== identity.sha256)
|
|
21
|
+
throw new Error('Pinned model SHA256 mismatch');
|
|
22
|
+
return bytes;
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await handle.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function buildMobileSamSessionOptions(options) {
|
|
29
|
+
return {
|
|
30
|
+
executionProviders: options.executionProvider === 'dml'
|
|
31
|
+
? [{ name: 'dml', deviceId: options.deviceId ?? 0 }]
|
|
32
|
+
: ['cpu'],
|
|
33
|
+
intraOpNumThreads: options.threads,
|
|
34
|
+
interOpNumThreads: 1,
|
|
35
|
+
executionMode: 'sequential',
|
|
36
|
+
enableMemPattern: options.executionProvider !== 'dml',
|
|
37
|
+
graphOptimizationLevel: 'all',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function probeMobileSam(options) {
|
|
41
|
+
const ort = await import('onnxruntime-node');
|
|
42
|
+
if (!ort
|
|
43
|
+
.listSupportedBackends()
|
|
44
|
+
.some((item) => item.name === options.executionProvider && item.bundled))
|
|
45
|
+
throw new Error('Explicitly requested ONNX execution provider is unavailable');
|
|
46
|
+
const sessionOptions = buildMobileSamSessionOptions(options);
|
|
47
|
+
const encoderBytes = await verifiedBytes(options.encoderPath, encoderIdentity);
|
|
48
|
+
const decoderBytes = await verifiedBytes(options.decoderPath, decoderIdentity);
|
|
49
|
+
let encoder;
|
|
50
|
+
let decoder;
|
|
51
|
+
let releaseFailed = false;
|
|
52
|
+
try {
|
|
53
|
+
encoder = await ort.InferenceSession.create(encoderBytes, sessionOptions);
|
|
54
|
+
decoder = await ort.InferenceSession.create(decoderBytes, sessionOptions);
|
|
55
|
+
if (!encoder.inputNames.includes('input_image') ||
|
|
56
|
+
!encoder.outputNames.includes('image_embeddings') ||
|
|
57
|
+
!decoder.inputNames.includes('image_embeddings') ||
|
|
58
|
+
!decoder.outputNames.includes('masks') ||
|
|
59
|
+
!decoder.outputNames.includes('iou_predictions'))
|
|
60
|
+
throw new Error('Pinned MobileSAM graph tensor names are incompatible');
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
const closed = await Promise.allSettled([
|
|
64
|
+
...(encoder ? [encoder.release()] : []),
|
|
65
|
+
...(decoder ? [decoder.release()] : []),
|
|
66
|
+
]);
|
|
67
|
+
releaseFailed = closed.some((item) => item.status === 'rejected');
|
|
68
|
+
}
|
|
69
|
+
if (releaseFailed)
|
|
70
|
+
throw new Error('MobileSAM probe session release failed');
|
|
71
|
+
}
|
|
72
|
+
export { encoderIdentity, decoderIdentity };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NativeObjectProposalReply, NativeSegmentationRequest } from '../native-protocol.js';
|
|
2
|
+
type ProposalRequest = Extract<NativeSegmentationRequest, {
|
|
3
|
+
kind: 'mobile-sam-proposals';
|
|
4
|
+
}>;
|
|
5
|
+
/** One encoder pass followed by a profile-bounded set of positive-point decoder calls. */
|
|
6
|
+
export declare function runMobileSamObjectProposalTask(request: ProposalRequest): Promise<NativeObjectProposalReply>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { objectProposalLimits } from '../../../entities/object-proposals.js';
|
|
3
|
+
import { decoderIdentity, encoderIdentity, verifiedBytes } from './worker-models.js';
|
|
4
|
+
import { mobileSamProposalPoints, packMobileSamProposalCandidates } from './proposal-masks.js';
|
|
5
|
+
/** One encoder pass followed by a profile-bounded set of positive-point decoder calls. */
|
|
6
|
+
export async function runMobileSamObjectProposalTask(request) {
|
|
7
|
+
const ort = await import('onnxruntime-node');
|
|
8
|
+
if (!ort.listSupportedBackends().some((item) => item.name === 'cpu' && item.bundled))
|
|
9
|
+
throw new Error('Explicitly requested ONNX CPU execution provider is unavailable');
|
|
10
|
+
const sessionOptions = {
|
|
11
|
+
executionProviders: ['cpu'],
|
|
12
|
+
intraOpNumThreads: request.options.threads,
|
|
13
|
+
interOpNumThreads: 1,
|
|
14
|
+
executionMode: 'sequential',
|
|
15
|
+
enableMemPattern: true,
|
|
16
|
+
graphOptimizationLevel: 'all',
|
|
17
|
+
};
|
|
18
|
+
const encoder = await ort.InferenceSession.create(await verifiedBytes(request.options.encoderPath, encoderIdentity), sessionOptions);
|
|
19
|
+
let decoder;
|
|
20
|
+
let embeddingTensor;
|
|
21
|
+
let reply;
|
|
22
|
+
let releaseFailed = false;
|
|
23
|
+
try {
|
|
24
|
+
decoder = await ort.InferenceSession.create(await verifiedBytes(request.options.decoderPath, decoderIdentity), sessionOptions);
|
|
25
|
+
const rgba = Buffer.from(request.rgba, 'base64');
|
|
26
|
+
const pixels = request.width * request.height;
|
|
27
|
+
if (rgba.length !== pixels * 4 || pixels > 2_097_152)
|
|
28
|
+
throw new Error('Invalid or oversized RGBA proposal input');
|
|
29
|
+
const scale = 1024 / Math.max(request.width, request.height);
|
|
30
|
+
const resizedWidth = Math.round(request.width * scale);
|
|
31
|
+
const resizedHeight = Math.round(request.height * scale);
|
|
32
|
+
const pipeline = sharp(rgba, {
|
|
33
|
+
raw: { width: request.width, height: request.height, channels: 4 },
|
|
34
|
+
});
|
|
35
|
+
let rgb;
|
|
36
|
+
try {
|
|
37
|
+
rgb = await pipeline
|
|
38
|
+
.removeAlpha()
|
|
39
|
+
.resize(resizedWidth, resizedHeight, { fit: 'fill', kernel: 'linear' })
|
|
40
|
+
.raw()
|
|
41
|
+
.toBuffer();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
pipeline.destroy();
|
|
45
|
+
}
|
|
46
|
+
if (rgb.length !== resizedWidth * resizedHeight * 3)
|
|
47
|
+
throw new Error('Invalid resized RGB proposal input');
|
|
48
|
+
const imageTensor = new ort.Tensor('float32', Float32Array.from(rgb), [
|
|
49
|
+
resizedHeight,
|
|
50
|
+
resizedWidth,
|
|
51
|
+
3,
|
|
52
|
+
]);
|
|
53
|
+
try {
|
|
54
|
+
const encoded = await encoder.run({ input_image: imageTensor });
|
|
55
|
+
const embedding = encoded.image_embeddings;
|
|
56
|
+
if (embedding instanceof ort.Tensor)
|
|
57
|
+
embeddingTensor = embedding;
|
|
58
|
+
if (!(embedding instanceof ort.Tensor) ||
|
|
59
|
+
embedding.type !== 'float32' ||
|
|
60
|
+
JSON.stringify(embedding.dims) !== JSON.stringify([1, 256, 64, 64]) ||
|
|
61
|
+
!(embedding.data instanceof Float32Array))
|
|
62
|
+
throw new Error('Unexpected MobileSAM encoder output');
|
|
63
|
+
const xScale = resizedWidth / request.width;
|
|
64
|
+
const yScale = resizedHeight / request.height;
|
|
65
|
+
const proposals = [];
|
|
66
|
+
let packedBytes = 0;
|
|
67
|
+
const points = mobileSamProposalPoints(request.seedRegion, request.sampling);
|
|
68
|
+
for (let pointIndex = 0; pointIndex < points.length; pointIndex++) {
|
|
69
|
+
const point = points[pointIndex];
|
|
70
|
+
if (!point)
|
|
71
|
+
throw new Error('Missing MobileSAM proposal point');
|
|
72
|
+
let coordinates;
|
|
73
|
+
let labels;
|
|
74
|
+
let maskInput;
|
|
75
|
+
let hasMaskInput;
|
|
76
|
+
let originalSize;
|
|
77
|
+
let decoded;
|
|
78
|
+
try {
|
|
79
|
+
coordinates = new ort.Tensor('float32', Float32Array.of(point.x * xScale, point.y * yScale, point.x * xScale, point.y * yScale), [1, 2, 2]);
|
|
80
|
+
labels = new ort.Tensor('float32', Float32Array.of(1, -1), [1, 2]);
|
|
81
|
+
maskInput = new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]);
|
|
82
|
+
hasMaskInput = new ort.Tensor('float32', Float32Array.of(0), [1]);
|
|
83
|
+
originalSize = new ort.Tensor('float32', Float32Array.of(request.height, request.width), [
|
|
84
|
+
2,
|
|
85
|
+
]);
|
|
86
|
+
decoded = await decoder.run({
|
|
87
|
+
image_embeddings: embedding,
|
|
88
|
+
point_coords: coordinates,
|
|
89
|
+
point_labels: labels,
|
|
90
|
+
mask_input: maskInput,
|
|
91
|
+
has_mask_input: hasMaskInput,
|
|
92
|
+
orig_im_size: originalSize,
|
|
93
|
+
});
|
|
94
|
+
const masks = decoded.masks;
|
|
95
|
+
const scores = decoded.iou_predictions;
|
|
96
|
+
if (!(masks instanceof ort.Tensor) ||
|
|
97
|
+
masks.type !== 'float32' ||
|
|
98
|
+
JSON.stringify(masks.dims) !== JSON.stringify([1, 4, request.height, request.width]) ||
|
|
99
|
+
!(masks.data instanceof Float32Array) ||
|
|
100
|
+
!(scores instanceof ort.Tensor) ||
|
|
101
|
+
scores.type !== 'float32' ||
|
|
102
|
+
JSON.stringify(scores.dims) !== JSON.stringify([1, 4]) ||
|
|
103
|
+
!(scores.data instanceof Float32Array) ||
|
|
104
|
+
!scores.data.every(Number.isFinite))
|
|
105
|
+
throw new Error('Unexpected MobileSAM proposal decoder output');
|
|
106
|
+
const candidates = packMobileSamProposalCandidates({
|
|
107
|
+
width: request.width,
|
|
108
|
+
height: request.height,
|
|
109
|
+
rgba,
|
|
110
|
+
roi: request.roi,
|
|
111
|
+
pointIndex,
|
|
112
|
+
logits: masks.data,
|
|
113
|
+
scores: scores.data,
|
|
114
|
+
});
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
packedBytes += Buffer.from(candidate.mask, 'base64').byteLength;
|
|
117
|
+
if (packedBytes > objectProposalLimits.packedBytes)
|
|
118
|
+
throw new Error('Packed MobileSAM proposals exceed the aggregate byte limit');
|
|
119
|
+
proposals.push(candidate);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
coordinates?.dispose();
|
|
124
|
+
labels?.dispose();
|
|
125
|
+
maskInput?.dispose();
|
|
126
|
+
hasMaskInput?.dispose();
|
|
127
|
+
originalSize?.dispose();
|
|
128
|
+
if (decoded)
|
|
129
|
+
for (const tensor of Object.values(decoded))
|
|
130
|
+
if (tensor instanceof ort.Tensor)
|
|
131
|
+
tensor.dispose();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
reply = {
|
|
135
|
+
id: request.id,
|
|
136
|
+
status: 'proposals',
|
|
137
|
+
width: request.width,
|
|
138
|
+
height: request.height,
|
|
139
|
+
proposals,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
embeddingTensor?.dispose();
|
|
144
|
+
imageTensor.dispose();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
const releases = await Promise.allSettled([
|
|
149
|
+
encoder.release(),
|
|
150
|
+
...(decoder ? [decoder.release()] : []),
|
|
151
|
+
]);
|
|
152
|
+
releaseFailed = releases.some((release) => release.status === 'rejected');
|
|
153
|
+
}
|
|
154
|
+
if (releaseFailed)
|
|
155
|
+
throw new Error('MobileSAM proposal session release failed');
|
|
156
|
+
if (!reply)
|
|
157
|
+
throw new Error('MobileSAM completed without proposals');
|
|
158
|
+
return reply;
|
|
159
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { buildMobileSamSessionOptions, decoderIdentity, encoderIdentity, verifiedBytes, } from './worker-models.js';
|
|
3
|
+
import { collectMobileSamSegmentationCandidates, mobileSamSegmentationPrompt, selectMobileSamSegmentationMask, } from './segmentation-masks.js';
|
|
4
|
+
export async function runMobileSamTask(request) {
|
|
5
|
+
// Deliberately loaded inside the owned child. Core/entry imports never initialize ONNX Runtime.
|
|
6
|
+
const ort = await import('onnxruntime-node');
|
|
7
|
+
const providers = ort.listSupportedBackends();
|
|
8
|
+
if (!providers.some((item) => item.name === request.options.executionProvider && item.bundled))
|
|
9
|
+
throw new Error('Explicitly requested ONNX execution provider is unavailable');
|
|
10
|
+
const sessionOptions = buildMobileSamSessionOptions(request.options);
|
|
11
|
+
const encoder = await ort.InferenceSession.create(await verifiedBytes(request.options.encoderPath, encoderIdentity), sessionOptions);
|
|
12
|
+
let decoder;
|
|
13
|
+
let result;
|
|
14
|
+
let releaseFailed = false;
|
|
15
|
+
try {
|
|
16
|
+
decoder = await ort.InferenceSession.create(await verifiedBytes(request.options.decoderPath, decoderIdentity), sessionOptions);
|
|
17
|
+
const rgba = Buffer.from(request.rgba, 'base64');
|
|
18
|
+
if (rgba.length !== request.width * request.height * 4)
|
|
19
|
+
throw new Error('Invalid RGBA input size');
|
|
20
|
+
const scale = 1024 / Math.max(request.width, request.height);
|
|
21
|
+
const resizedWidth = Math.round(request.width * scale);
|
|
22
|
+
const resizedHeight = Math.round(request.height * scale);
|
|
23
|
+
if (resizedWidth < 1 || resizedHeight < 1)
|
|
24
|
+
throw new Error('Unrepresentable image aspect ratio');
|
|
25
|
+
const pipeline = sharp(rgba, {
|
|
26
|
+
raw: { width: request.width, height: request.height, channels: 4 },
|
|
27
|
+
});
|
|
28
|
+
let rgb;
|
|
29
|
+
try {
|
|
30
|
+
rgb = await pipeline
|
|
31
|
+
.removeAlpha()
|
|
32
|
+
.resize(resizedWidth, resizedHeight, { fit: 'fill', kernel: 'linear' })
|
|
33
|
+
.raw()
|
|
34
|
+
.toBuffer();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
pipeline.destroy();
|
|
38
|
+
}
|
|
39
|
+
if (rgb.length !== resizedWidth * resizedHeight * 3)
|
|
40
|
+
throw new Error('Invalid resized RGB data');
|
|
41
|
+
const imageTensor = new ort.Tensor('float32', Float32Array.from(rgb), [
|
|
42
|
+
resizedHeight,
|
|
43
|
+
resizedWidth,
|
|
44
|
+
3,
|
|
45
|
+
]);
|
|
46
|
+
try {
|
|
47
|
+
const encoded = await encoder.run({ input_image: imageTensor });
|
|
48
|
+
const embedding = encoded.image_embeddings;
|
|
49
|
+
if (!(embedding instanceof ort.Tensor) ||
|
|
50
|
+
embedding.type !== 'float32' ||
|
|
51
|
+
JSON.stringify(embedding.dims) !== JSON.stringify([1, 256, 64, 64]) ||
|
|
52
|
+
!(embedding.data instanceof Float32Array))
|
|
53
|
+
throw new Error('Unexpected MobileSAM encoder output');
|
|
54
|
+
const xScale = resizedWidth / request.width;
|
|
55
|
+
const yScale = resizedHeight / request.height;
|
|
56
|
+
const prompt = mobileSamSegmentationPrompt({
|
|
57
|
+
prompt: request.prompt,
|
|
58
|
+
seedRegion: request.seedRegion,
|
|
59
|
+
wholeRegion: request.wholeRegion,
|
|
60
|
+
xScale,
|
|
61
|
+
yScale,
|
|
62
|
+
});
|
|
63
|
+
const decoded = await decoder.run({
|
|
64
|
+
image_embeddings: embedding,
|
|
65
|
+
point_coords: new ort.Tensor('float32', prompt.coordinates, [1, 2, 2]),
|
|
66
|
+
point_labels: new ort.Tensor('float32', prompt.labels, [1, 2]),
|
|
67
|
+
mask_input: new ort.Tensor('float32', new Float32Array(256 * 256), [1, 1, 256, 256]),
|
|
68
|
+
has_mask_input: new ort.Tensor('float32', Float32Array.of(0), [1]),
|
|
69
|
+
orig_im_size: new ort.Tensor('float32', Float32Array.of(request.height, request.width), [
|
|
70
|
+
2,
|
|
71
|
+
]),
|
|
72
|
+
});
|
|
73
|
+
const masks = decoded.masks;
|
|
74
|
+
const scores = decoded.iou_predictions;
|
|
75
|
+
if (!(masks instanceof ort.Tensor) ||
|
|
76
|
+
masks.type !== 'float32' ||
|
|
77
|
+
JSON.stringify(masks.dims) !== JSON.stringify([1, 4, request.height, request.width]) ||
|
|
78
|
+
!(masks.data instanceof Float32Array) ||
|
|
79
|
+
!(scores instanceof ort.Tensor) ||
|
|
80
|
+
scores.type !== 'float32' ||
|
|
81
|
+
JSON.stringify(scores.dims) !== JSON.stringify([1, 4]) ||
|
|
82
|
+
!(scores.data instanceof Float32Array) ||
|
|
83
|
+
!scores.data.every(Number.isFinite))
|
|
84
|
+
throw new Error('Unexpected MobileSAM decoder output');
|
|
85
|
+
const candidates = collectMobileSamSegmentationCandidates({
|
|
86
|
+
width: request.width,
|
|
87
|
+
height: request.height,
|
|
88
|
+
rgba,
|
|
89
|
+
logits: masks.data,
|
|
90
|
+
scores: scores.data,
|
|
91
|
+
roi: request.roi,
|
|
92
|
+
seedRegion: request.seedRegion,
|
|
93
|
+
prompt: request.prompt,
|
|
94
|
+
});
|
|
95
|
+
const mask = selectMobileSamSegmentationMask({
|
|
96
|
+
candidates,
|
|
97
|
+
width: request.width,
|
|
98
|
+
height: request.height,
|
|
99
|
+
prompt: request.prompt,
|
|
100
|
+
});
|
|
101
|
+
result = {
|
|
102
|
+
id: request.id,
|
|
103
|
+
status: 'segmented',
|
|
104
|
+
width: request.width,
|
|
105
|
+
height: request.height,
|
|
106
|
+
mask: Buffer.from(mask).toString('base64'),
|
|
107
|
+
method: request.prompt === 'seed-region' ? 'mobile-sam-seed-point-v1' : 'mobile-sam-box-prompt',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
imageTensor.dispose();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
const releases = await Promise.allSettled([
|
|
116
|
+
encoder.release(),
|
|
117
|
+
...(decoder ? [decoder.release()] : []),
|
|
118
|
+
]);
|
|
119
|
+
releaseFailed = releases.some((release) => release.status === 'rejected');
|
|
120
|
+
}
|
|
121
|
+
if (releaseFailed)
|
|
122
|
+
throw new Error('MobileSAM native session release failed');
|
|
123
|
+
if (!result)
|
|
124
|
+
throw new Error('MobileSAM completed without a mask result');
|
|
125
|
+
return result;
|
|
126
|
+
}
|