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,8 @@
1
+ export declare const encoderIdentity: Readonly<{
2
+ readonly bytes: 28157093;
3
+ readonly sha256: "580f5fb648ea1062c0aabc26217aed56921985f03f0cbbd852bba81d760cc749";
4
+ }>;
5
+ export declare const decoderIdentity: Readonly<{
6
+ readonly bytes: 16496559;
7
+ readonly sha256: "8976b90a87ba50a6a72217a5ff994f7d25ce16f2229fcc1ed259e1294c622ffe";
8
+ }>;
@@ -0,0 +1,8 @@
1
+ export const encoderIdentity = Object.freeze({
2
+ bytes: 28_157_093,
3
+ sha256: '580f5fb648ea1062c0aabc26217aed56921985f03f0cbbd852bba81d760cc749',
4
+ });
5
+ export const decoderIdentity = Object.freeze({
6
+ bytes: 16_496_559,
7
+ sha256: '8976b90a87ba50a6a72217a5ff994f7d25ce16f2229fcc1ed259e1294c622ffe',
8
+ });
@@ -0,0 +1,7 @@
1
+ import type { ObjectProposalPort } from '../../../entities/object-proposals.js';
2
+ import { type MobileSamObjectProposalsOptions } from './options.js';
3
+ /**
4
+ * Explicit CPU-only proposals: historical 3x3/36 default or opt-in seed-center/four.
5
+ * @experimental Native proposal adapter; unqualified for general spatial or mask accuracy.
6
+ */
7
+ export declare function createMobileSamObjectProposals(options: MobileSamObjectProposalsOptions): Promise<ObjectProposalPort>;
@@ -0,0 +1,124 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { ClassifierError, assertActive } from '../../../entities/errors.js';
3
+ import { objectProposalLimits } from '../../../entities/object-proposals.js';
4
+ import { NativeChildPool } from '../native-child.js';
5
+ import { verifyPinnedAsset } from '../verify-asset.js';
6
+ import { probeBackend } from '../probe-backend.js';
7
+ import { nativeErrorReplySchema, nativeObjectProposalReplySchema, } from '../native-protocol.js';
8
+ import { decoderIdentity, encoderIdentity } from './model-assets.js';
9
+ import { proposalOptionsSchema } from './options.js';
10
+ import { snapshotProposalInput } from './input.js';
11
+ /**
12
+ * Explicit CPU-only proposals: historical 3x3/36 default or opt-in seed-center/four.
13
+ * @experimental Native proposal adapter; unqualified for general spatial or mask accuracy.
14
+ */
15
+ export async function createMobileSamObjectProposals(options) {
16
+ const checked = proposalOptionsSchema.safeParse(options);
17
+ if (!checked.success)
18
+ throw new ClassifierError('INVALID_INPUT', 'Invalid MobileSAM proposal options');
19
+ const parsed = checked.data;
20
+ try {
21
+ import.meta.resolve('onnxruntime-node');
22
+ }
23
+ catch {
24
+ throw new ClassifierError('UNSUPPORTED', 'onnxruntime-node is not installed');
25
+ }
26
+ await verifyPinnedAsset(parsed.encoderPath, encoderIdentity, 'MobileSAM encoder');
27
+ await verifyPinnedAsset(parsed.decoderPath, decoderIdentity, 'MobileSAM decoder');
28
+ const sourceWorker = import.meta.url.endsWith('.ts');
29
+ const workerUrl = new URL(sourceWorker ? '../native-worker.ts' : '../native-worker.js', import.meta.url);
30
+ // Probe the explicitly requested CPU provider in an owned child, disposing it before return.
31
+ const modelOptions = {
32
+ encoderPath: parsed.encoderPath,
33
+ decoderPath: parsed.decoderPath,
34
+ executionProvider: 'cpu',
35
+ threads: parsed.threads,
36
+ };
37
+ await probeBackend(workerUrl, 'mobile-sam', { options: modelOptions });
38
+ const pool = new NativeChildPool({
39
+ workerUrl,
40
+ maxActive: 1,
41
+ maxWaiting: 2,
42
+ jobTimeoutMs: 10_000,
43
+ abortGraceMs: 250,
44
+ encode: (job) => Buffer.from(JSON.stringify({ id: job.id, ...job.payload })),
45
+ decode: (raw, id) => {
46
+ const error = nativeErrorReplySchema.safeParse(raw);
47
+ if (error.success) {
48
+ if (error.data.id !== id)
49
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child error ID mismatch');
50
+ throw new ClassifierError(error.data.code, error.data.message);
51
+ }
52
+ const reply = nativeObjectProposalReplySchema.safeParse(raw);
53
+ if (!reply.success || reply.data.id !== id)
54
+ throw new ClassifierError('INVALID_RESPONSE', 'Native child returned invalid proposals');
55
+ return reply.data;
56
+ },
57
+ });
58
+ return {
59
+ async propose(input) {
60
+ const started = performance.now();
61
+ assertActive(input.signal);
62
+ let image;
63
+ const raw = await pool.runLazy(input.signal, () => {
64
+ const snapshot = snapshotProposalInput(input);
65
+ image = snapshot.image;
66
+ return {
67
+ kind: 'mobile-sam-proposals',
68
+ width: image.width,
69
+ height: image.height,
70
+ rgba: Buffer.from(image.data).toString('base64'),
71
+ seedRegion: snapshot.seedRegion,
72
+ roi: snapshot.roi,
73
+ sampling: parsed.sampling,
74
+ options: modelOptions,
75
+ };
76
+ });
77
+ const reply = nativeObjectProposalReplySchema.parse(raw);
78
+ const expectedCount = parsed.sampling === 'seed-center' ? 4 : 36;
79
+ if (reply.width !== image.width ||
80
+ reply.height !== image.height ||
81
+ reply.proposals.length !== expectedCount)
82
+ throw new ClassifierError('INVALID_RESPONSE', 'Proposal dimensions or candidate count do not match the request');
83
+ const expectedBytes = Math.ceil((image.width * image.height) / 8);
84
+ let aggregateBytes = 0;
85
+ const proposals = reply.proposals.map((proposal, index) => {
86
+ const expectedId = `C${String(index).padStart(2, '0')}`;
87
+ const bytes = Buffer.from(proposal.mask, 'base64');
88
+ aggregateBytes += bytes.byteLength;
89
+ if (proposal.id !== expectedId ||
90
+ bytes.byteLength !== expectedBytes ||
91
+ bytes.toString('base64') !== proposal.mask ||
92
+ aggregateBytes > objectProposalLimits.packedBytes)
93
+ throw new ClassifierError('INVALID_RESPONSE', 'Proposal masks are malformed or oversized');
94
+ const unusedBits = expectedBytes * 8 - image.width * image.height;
95
+ const allowed = unusedBits ? (1 << (8 - unusedBits)) - 1 : 255;
96
+ if ((bytes[expectedBytes - 1] & ~allowed) !== 0)
97
+ throw new ClassifierError('INVALID_RESPONSE', 'Proposal mask has nonzero padding bits');
98
+ return {
99
+ id: proposal.id,
100
+ mask: {
101
+ data: new Uint8Array(bytes),
102
+ width: image.width,
103
+ height: image.height,
104
+ format: 'binary-lsb0',
105
+ },
106
+ qualityScore: proposal.qualityScore,
107
+ };
108
+ });
109
+ return {
110
+ proposals,
111
+ metadata: {
112
+ method: parsed.sampling === 'seed-center'
113
+ ? 'mobile-sam-seed-center-proposals-v1'
114
+ : 'mobile-sam-nine-point-proposals-v1',
115
+ backend: 'mobile-sam',
116
+ durationMs: Math.max(0, performance.now() - started),
117
+ input: { width: image.width, height: image.height },
118
+ },
119
+ };
120
+ },
121
+ settle: () => pool.settle(),
122
+ dispose: () => pool.dispose(),
123
+ };
124
+ }
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export declare const optionsSchema: z.ZodObject<{
3
+ encoderPath: z.ZodString;
4
+ decoderPath: z.ZodString;
5
+ executionProvider: z.ZodDefault<z.ZodEnum<["cpu", "dml"]>>;
6
+ deviceId: z.ZodOptional<z.ZodNumber>;
7
+ threads: z.ZodDefault<z.ZodNumber>;
8
+ }, "strict", z.ZodTypeAny, {
9
+ encoderPath: string;
10
+ decoderPath: string;
11
+ executionProvider: "cpu" | "dml";
12
+ threads: number;
13
+ deviceId?: number | undefined;
14
+ }, {
15
+ encoderPath: string;
16
+ decoderPath: string;
17
+ executionProvider?: "cpu" | "dml" | undefined;
18
+ deviceId?: number | undefined;
19
+ threads?: number | undefined;
20
+ }>;
21
+ /** @experimental Options for the unqualified MobileSAM segmentation factory. */
22
+ export type MobileSamSegmenterOptions = z.input<typeof optionsSchema>;
23
+ export declare const proposalOptionsSchema: z.ZodObject<{
24
+ encoderPath: z.ZodString;
25
+ decoderPath: z.ZodString;
26
+ executionProvider: z.ZodDefault<z.ZodLiteral<"cpu">>;
27
+ threads: z.ZodDefault<z.ZodNumber>;
28
+ sampling: z.ZodDefault<z.ZodEnum<["seed-grid", "seed-center"]>>;
29
+ }, "strict", z.ZodTypeAny, {
30
+ encoderPath: string;
31
+ decoderPath: string;
32
+ executionProvider: "cpu";
33
+ threads: number;
34
+ sampling: "seed-grid" | "seed-center";
35
+ }, {
36
+ encoderPath: string;
37
+ decoderPath: string;
38
+ executionProvider?: "cpu" | undefined;
39
+ threads?: number | undefined;
40
+ sampling?: "seed-grid" | "seed-center" | undefined;
41
+ }>;
42
+ /** @experimental Options for the unqualified MobileSAM object-proposal factory. */
43
+ export type MobileSamObjectProposalsOptions = z.input<typeof proposalOptionsSchema>;
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export const optionsSchema = z
3
+ .object({
4
+ encoderPath: z.string().min(1).max(4096),
5
+ decoderPath: z.string().min(1).max(4096),
6
+ executionProvider: z.enum(['cpu', 'dml']).default('cpu'),
7
+ deviceId: z.number().int().min(0).max(32).optional(),
8
+ threads: z.number().int().min(1).max(8).default(2),
9
+ })
10
+ .strict();
11
+ export const proposalOptionsSchema = z
12
+ .object({
13
+ encoderPath: z.string().min(1).max(4096),
14
+ decoderPath: z.string().min(1).max(4096),
15
+ executionProvider: z.literal('cpu').default('cpu'),
16
+ threads: z.number().int().min(1).max(8).default(2),
17
+ sampling: z.enum(['seed-grid', 'seed-center']).default('seed-grid'),
18
+ })
19
+ .strict();
@@ -0,0 +1,26 @@
1
+ import type { NativeObjectProposalReply } from '../native-protocol.js';
2
+ /** Produce either the historical 3x3 grid or one geometric-center prompt. */
3
+ export declare function mobileSamProposalPoints(rect: {
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ }, sampling?: 'seed-grid' | 'seed-center'): Array<{
9
+ x: number;
10
+ y: number;
11
+ }>;
12
+ /** Turn one decoder response into four owned packed candidates immediately. */
13
+ export declare function packMobileSamProposalCandidates(input: {
14
+ width: number;
15
+ height: number;
16
+ rgba: Uint8Array;
17
+ roi: {
18
+ left: number;
19
+ top: number;
20
+ width: number;
21
+ height: number;
22
+ };
23
+ pointIndex: number;
24
+ logits: Float32Array;
25
+ scores: Float32Array;
26
+ }): NativeObjectProposalReply['proposals'];
@@ -0,0 +1,71 @@
1
+ import { packBinaryMask } from '../../../features/segmentation/packed-mask.js';
2
+ import { inside } from './segmentation-masks.js';
3
+ /** Produce either the historical 3x3 grid or one geometric-center prompt. */
4
+ export function mobileSamProposalPoints(rect, sampling = 'seed-grid') {
5
+ if (!Number.isSafeInteger(rect.left) ||
6
+ !Number.isSafeInteger(rect.top) ||
7
+ !Number.isSafeInteger(rect.width) ||
8
+ !Number.isSafeInteger(rect.height) ||
9
+ rect.left < 0 ||
10
+ rect.top < 0 ||
11
+ rect.width < 1 ||
12
+ rect.height < 1)
13
+ throw new Error('Invalid MobileSAM seed rectangle');
14
+ if (sampling !== 'seed-grid' && sampling !== 'seed-center')
15
+ throw new Error('Invalid MobileSAM proposal sampling profile');
16
+ if (sampling === 'seed-center')
17
+ return [{ x: rect.left + (rect.width - 1) / 2, y: rect.top + (rect.height - 1) / 2 }];
18
+ return Array.from({ length: 9 }, (_, index) => {
19
+ const row = Math.floor(index / 3);
20
+ const column = index % 3;
21
+ return {
22
+ x: rect.left + Math.round(((rect.width - 1) * column) / 2),
23
+ y: rect.top + Math.round(((rect.height - 1) * row) / 2),
24
+ };
25
+ });
26
+ }
27
+ /** Turn one decoder response into four owned packed candidates immediately. */
28
+ export function packMobileSamProposalCandidates(input) {
29
+ const pixels = input.width * input.height;
30
+ if (!Number.isSafeInteger(input.width) ||
31
+ !Number.isSafeInteger(input.height) ||
32
+ input.width < 1 ||
33
+ input.height < 1 ||
34
+ pixels > 2_097_152 ||
35
+ input.rgba.length !== pixels * 4 ||
36
+ input.logits.length !== pixels * 4 ||
37
+ input.scores.length !== 4 ||
38
+ !Number.isInteger(input.pointIndex) ||
39
+ input.pointIndex < 0 ||
40
+ input.pointIndex > 8 ||
41
+ input.roi.left < 0 ||
42
+ input.roi.top < 0 ||
43
+ input.roi.width < 1 ||
44
+ input.roi.height < 1 ||
45
+ input.roi.left + input.roi.width > input.width ||
46
+ input.roi.top + input.roi.height > input.height)
47
+ throw new Error('Invalid MobileSAM proposal response dimensions');
48
+ const proposals = [];
49
+ for (let token = 0; token < 4; token++) {
50
+ const qualityScore = input.scores[token];
51
+ if (qualityScore === undefined || !Number.isFinite(qualityScore))
52
+ throw new Error('Invalid MobileSAM proposal quality score');
53
+ const mask = new Uint8Array(pixels);
54
+ const base = token * pixels;
55
+ for (let index = 0; index < pixels; index++) {
56
+ const value = input.logits[base + index];
57
+ if (value === undefined || !Number.isFinite(value))
58
+ throw new Error('Non-finite MobileSAM proposal logit');
59
+ if (value <= 0 || input.rgba[index * 4 + 3] === 0)
60
+ continue;
61
+ const x = index % input.width;
62
+ const y = Math.floor(index / input.width);
63
+ if (inside(input.roi, x, y))
64
+ mask[index] = 1;
65
+ }
66
+ const id = `C${String(input.pointIndex * 4 + token).padStart(2, '0')}`;
67
+ const packed = packBinaryMask(mask, input.width, input.height);
68
+ proposals.push({ id, mask: Buffer.from(packed.data).toString('base64'), qualityScore });
69
+ }
70
+ return proposals;
71
+ }
@@ -0,0 +1,112 @@
1
+ export declare function inside(rect: {
2
+ left: number;
3
+ top: number;
4
+ width: number;
5
+ height: number;
6
+ }, x: number, y: number): boolean;
7
+ interface MaskCandidate {
8
+ token: number;
9
+ score: number;
10
+ mask: Uint8Array;
11
+ }
12
+ /** Sort by model quality and use decoder token order as a deterministic tie break. */
13
+ export declare function selectBestMaskCandidate<T extends MaskCandidate>(candidates: readonly T[]): T | undefined;
14
+ export declare function mobileSamSegmentationPrompt(input: {
15
+ prompt: 'whole-region' | 'seed-region';
16
+ seedRegion: {
17
+ left: number;
18
+ top: number;
19
+ width: number;
20
+ height: number;
21
+ };
22
+ wholeRegion: {
23
+ left: number;
24
+ top: number;
25
+ width: number;
26
+ height: number;
27
+ };
28
+ xScale: number;
29
+ yScale: number;
30
+ }): {
31
+ coordinates: Float32Array<ArrayBuffer>;
32
+ labels: Float32Array<ArrayBuffer>;
33
+ };
34
+ export declare function collectMobileSamSegmentationCandidates(input: {
35
+ width: number;
36
+ height: number;
37
+ rgba: Uint8Array;
38
+ logits: Float32Array;
39
+ scores: Float32Array;
40
+ roi: {
41
+ left: number;
42
+ top: number;
43
+ width: number;
44
+ height: number;
45
+ };
46
+ seedRegion: {
47
+ left: number;
48
+ top: number;
49
+ width: number;
50
+ height: number;
51
+ };
52
+ prompt: 'whole-region' | 'seed-region';
53
+ }): MaskCandidate[];
54
+ export declare function selectMobileSamSegmentationMask(input: {
55
+ candidates: readonly MaskCandidate[];
56
+ width: number;
57
+ height: number;
58
+ prompt: 'whole-region' | 'seed-region';
59
+ }): Uint8Array;
60
+ interface EnclosingMobileSamCandidate {
61
+ token: number;
62
+ score: number;
63
+ mask: Uint8Array;
64
+ area: number;
65
+ bounds: {
66
+ left: number;
67
+ top: number;
68
+ right: number;
69
+ bottom: number;
70
+ };
71
+ }
72
+ /** Full-source segmentation masks use the segmentation pixel cap, not the proposal cap. */
73
+ export declare function packFullSourceBinaryMask(mask: Uint8Array, width: number, height: number): Uint8Array;
74
+ /** Strictly decode the LSB-first full-source mask representation used by expansion IPC. */
75
+ export declare function unpackFullSourceBinaryMask(data: Uint8Array, width: number, height: number): Uint8Array;
76
+ /** Validate a prior full-source mask and return its foreground area and bounds. */
77
+ export declare function validateMobileSamPreviousMask(input: {
78
+ width: number;
79
+ height: number;
80
+ rgba: Uint8Array;
81
+ previousMask: Uint8Array;
82
+ roi: {
83
+ left: number;
84
+ top: number;
85
+ width: number;
86
+ height: number;
87
+ };
88
+ }): {
89
+ area: number;
90
+ bounds: {
91
+ left: number;
92
+ top: number;
93
+ right: number;
94
+ bottom: number;
95
+ };
96
+ };
97
+ /** Select the smallest actual decoder mask that strictly encloses prior foreground. */
98
+ export declare function selectEnclosingMobileSamCandidate(input: {
99
+ width: number;
100
+ height: number;
101
+ rgba: Uint8Array;
102
+ logits: Float32Array;
103
+ scores: Float32Array;
104
+ previousMask: Uint8Array;
105
+ roi: {
106
+ left: number;
107
+ top: number;
108
+ width: number;
109
+ height: number;
110
+ };
111
+ }): EnclosingMobileSamCandidate | undefined;
112
+ export {};
@@ -0,0 +1,230 @@
1
+ export function inside(rect, x, y) {
2
+ return x >= rect.left && y >= rect.top && x < rect.left + rect.width && y < rect.top + rect.height;
3
+ }
4
+ /** Sort by model quality and use decoder token order as a deterministic tie break. */
5
+ export function selectBestMaskCandidate(candidates) {
6
+ return [...candidates].sort((a, b) => b.score - a.score || a.token - b.token)[0];
7
+ }
8
+ export function mobileSamSegmentationPrompt(input) {
9
+ if (input.prompt === 'whole-region')
10
+ return {
11
+ coordinates: Float32Array.of(input.wholeRegion.left * input.xScale, input.wholeRegion.top * input.yScale, (input.wholeRegion.left + input.wholeRegion.width) * input.xScale, (input.wholeRegion.top + input.wholeRegion.height) * input.yScale),
12
+ labels: Float32Array.of(2, 3),
13
+ };
14
+ const centerX = input.seedRegion.left + (input.seedRegion.width - 1) / 2;
15
+ const centerY = input.seedRegion.top + (input.seedRegion.height - 1) / 2;
16
+ const pointX = centerX * input.xScale;
17
+ const pointY = centerY * input.yScale;
18
+ return {
19
+ coordinates: Float32Array.of(pointX, pointY, pointX, pointY),
20
+ labels: Float32Array.of(1, -1),
21
+ };
22
+ }
23
+ export function collectMobileSamSegmentationCandidates(input) {
24
+ const pixels = input.width * input.height;
25
+ if (input.rgba.length !== pixels * 4 ||
26
+ input.logits.length !== pixels * 4 ||
27
+ input.scores.length !== 4)
28
+ throw new Error('Invalid MobileSAM decoder output dimensions');
29
+ const candidates = [];
30
+ for (let token = 0; token < 4; token++) {
31
+ const mask = new Uint8Array(pixels);
32
+ let seedHit = false;
33
+ for (let index = 0; index < pixels; index++) {
34
+ const value = input.logits[token * pixels + index];
35
+ if (value === undefined || !Number.isFinite(value))
36
+ throw new Error('Non-finite mask logit');
37
+ if (value <= 0)
38
+ continue;
39
+ const x = index % input.width;
40
+ const y = Math.floor(index / input.width);
41
+ if (!inside(input.roi, x, y))
42
+ continue;
43
+ if (input.prompt === 'seed-region' && input.rgba[index * 4 + 3] === 0)
44
+ continue;
45
+ mask[index] = 1;
46
+ if (inside(input.seedRegion, x, y))
47
+ seedHit = true;
48
+ }
49
+ const score = input.scores[token];
50
+ if (score === undefined || !Number.isFinite(score))
51
+ throw new Error('Non-finite MobileSAM quality diagnostic');
52
+ if (seedHit)
53
+ candidates.push({ token, score, mask });
54
+ }
55
+ return candidates;
56
+ }
57
+ export function selectMobileSamSegmentationMask(input) {
58
+ const best = selectBestMaskCandidate(input.candidates);
59
+ if (best)
60
+ return best.mask;
61
+ if (input.prompt === 'seed-region')
62
+ return new Uint8Array(input.width * input.height);
63
+ throw new Error('MobileSAM produced no seed-grounded mask');
64
+ }
65
+ /** Full-source segmentation masks use the segmentation pixel cap, not the proposal cap. */
66
+ export function packFullSourceBinaryMask(mask, width, height) {
67
+ const pixels = Number.isSafeInteger(width) && Number.isSafeInteger(height) ? width * height : NaN;
68
+ if (!Number.isSafeInteger(width) ||
69
+ !Number.isSafeInteger(height) ||
70
+ width < 1 ||
71
+ height < 1 ||
72
+ !Number.isSafeInteger(pixels) ||
73
+ pixels > 16_777_216 ||
74
+ mask.length !== pixels)
75
+ throw new Error('Invalid full-source mask dimensions');
76
+ const packed = new Uint8Array(Math.ceil(pixels / 8));
77
+ for (let index = 0; index < pixels; index++) {
78
+ const value = mask[index];
79
+ if (value !== 0 && value !== 1)
80
+ throw new Error('Full-source mask is not binary');
81
+ if (value === 1)
82
+ packed[index >> 3] = (packed[index >> 3] ?? 0) | (1 << (index & 7));
83
+ }
84
+ return packed;
85
+ }
86
+ /** Strictly decode the LSB-first full-source mask representation used by expansion IPC. */
87
+ export function unpackFullSourceBinaryMask(data, width, height) {
88
+ const pixels = Number.isSafeInteger(width) && Number.isSafeInteger(height) ? width * height : NaN;
89
+ const expectedBytes = Number.isSafeInteger(pixels) ? Math.ceil(pixels / 8) : NaN;
90
+ if (!Number.isSafeInteger(width) ||
91
+ !Number.isSafeInteger(height) ||
92
+ width < 1 ||
93
+ height < 1 ||
94
+ !Number.isSafeInteger(pixels) ||
95
+ pixels > 16_777_216 ||
96
+ data.length !== expectedBytes)
97
+ throw new Error('Invalid packed full-source mask dimensions');
98
+ const unusedBits = expectedBytes * 8 - pixels;
99
+ if (unusedBits > 0) {
100
+ const allowed = (1 << (8 - unusedBits)) - 1;
101
+ if (((data[expectedBytes - 1] ?? 0) & ~allowed) !== 0)
102
+ throw new Error('Packed full-source mask has nonzero padding bits');
103
+ }
104
+ const output = new Uint8Array(pixels);
105
+ for (let index = 0; index < pixels; index++)
106
+ output[index] = ((data[index >> 3] ?? 0) >> (index & 7)) & 1;
107
+ return output;
108
+ }
109
+ /** Validate a prior full-source mask and return its foreground area and bounds. */
110
+ export function validateMobileSamPreviousMask(input) {
111
+ const pixels = Number.isSafeInteger(input.width) && Number.isSafeInteger(input.height)
112
+ ? input.width * input.height
113
+ : NaN;
114
+ if (!Number.isSafeInteger(input.width) ||
115
+ !Number.isSafeInteger(input.height) ||
116
+ input.width < 1 ||
117
+ input.height < 1 ||
118
+ !Number.isSafeInteger(pixels) ||
119
+ pixels > 16_777_216 ||
120
+ input.rgba.length !== pixels * 4 ||
121
+ input.previousMask.length !== pixels ||
122
+ !Number.isSafeInteger(input.roi.left) ||
123
+ !Number.isSafeInteger(input.roi.top) ||
124
+ !Number.isSafeInteger(input.roi.width) ||
125
+ !Number.isSafeInteger(input.roi.height) ||
126
+ input.roi.left < 0 ||
127
+ input.roi.top < 0 ||
128
+ input.roi.width < 1 ||
129
+ input.roi.height < 1 ||
130
+ input.roi.left + input.roi.width > input.width ||
131
+ input.roi.top + input.roi.height > input.height)
132
+ throw new Error('Invalid previous mask dimensions or ROI');
133
+ let area = 0;
134
+ let left = input.width;
135
+ let top = input.height;
136
+ let right = -1;
137
+ let bottom = -1;
138
+ for (let index = 0; index < pixels; index++) {
139
+ const value = input.previousMask[index];
140
+ if (value !== 0 && value !== 1)
141
+ throw new Error('Previous mask is not binary');
142
+ if (value === 0)
143
+ continue;
144
+ const x = index % input.width;
145
+ const y = Math.floor(index / input.width);
146
+ if (!inside(input.roi, x, y) || input.rgba[index * 4 + 3] === 0)
147
+ throw new Error('Previous mask foreground is outside visible ROI pixels');
148
+ area++;
149
+ left = Math.min(left, x);
150
+ top = Math.min(top, y);
151
+ right = Math.max(right, x);
152
+ bottom = Math.max(bottom, y);
153
+ }
154
+ if (area === 0)
155
+ throw new Error('Previous mask has no foreground');
156
+ return { area, bounds: { left, top, right, bottom } };
157
+ }
158
+ /** Select the smallest actual decoder mask that strictly encloses prior foreground. */
159
+ export function selectEnclosingMobileSamCandidate(input) {
160
+ const pixels = input.width * input.height;
161
+ const previous = validateMobileSamPreviousMask(input);
162
+ if (input.logits.length !== pixels * 4 || input.scores.length !== 4)
163
+ throw new Error('Invalid MobileSAM decoder output dimensions');
164
+ let best;
165
+ let currentMask;
166
+ try {
167
+ for (let token = 0; token < 4; token++) {
168
+ const score = input.scores[token];
169
+ if (score === undefined || !Number.isFinite(score))
170
+ throw new Error('Non-finite MobileSAM quality diagnostic');
171
+ const mask = new Uint8Array(pixels);
172
+ currentMask = mask;
173
+ let area = 0;
174
+ let left = input.width;
175
+ let top = input.height;
176
+ let right = -1;
177
+ let bottom = -1;
178
+ let containsPrevious = true;
179
+ const base = token * pixels;
180
+ for (let index = 0; index < pixels; index++) {
181
+ const value = input.logits[base + index];
182
+ if (value === undefined || !Number.isFinite(value))
183
+ throw new Error('Non-finite MobileSAM mask logit');
184
+ const x = index % input.width;
185
+ const y = Math.floor(index / input.width);
186
+ const foreground = value > 0 && input.rgba[index * 4 + 3] !== 0 && inside(input.roi, x, y);
187
+ if (input.previousMask[index] === 1 && !foreground)
188
+ containsPrevious = false;
189
+ if (!foreground)
190
+ continue;
191
+ mask[index] = 1;
192
+ area++;
193
+ left = Math.min(left, x);
194
+ top = Math.min(top, y);
195
+ right = Math.max(right, x);
196
+ bottom = Math.max(bottom, y);
197
+ }
198
+ const strictBoundsGrowth = left <= previous.bounds.left &&
199
+ top <= previous.bounds.top &&
200
+ right >= previous.bounds.right &&
201
+ bottom >= previous.bounds.bottom &&
202
+ (left < previous.bounds.left ||
203
+ top < previous.bounds.top ||
204
+ right > previous.bounds.right ||
205
+ bottom > previous.bounds.bottom);
206
+ if (!containsPrevious || area <= previous.area || !strictBoundsGrowth) {
207
+ mask.fill(0);
208
+ continue;
209
+ }
210
+ const candidate = { token, score, mask, area, bounds: { left, top, right, bottom } };
211
+ if (!best ||
212
+ candidate.area < best.area ||
213
+ (candidate.area === best.area && candidate.score > best.score) ||
214
+ (candidate.area === best.area &&
215
+ candidate.score === best.score &&
216
+ candidate.token < best.token)) {
217
+ best?.mask.fill(0);
218
+ best = candidate;
219
+ }
220
+ else
221
+ mask.fill(0);
222
+ }
223
+ return best;
224
+ }
225
+ catch (error) {
226
+ currentMask?.fill(0);
227
+ best?.mask.fill(0);
228
+ throw error;
229
+ }
230
+ }
@@ -0,0 +1,22 @@
1
+ import type { z } from 'zod';
2
+ import type { SegmentationPort } from '../../../entities/segmentation.js';
3
+ import { NativeChildPool } from '../native-child.js';
4
+ import { type NativeSegmentationRequest } from '../native-protocol.js';
5
+ import { optionsSchema, type MobileSamSegmenterOptions } from './options.js';
6
+ type SerializedTask = (Omit<Extract<NativeSegmentationRequest, {
7
+ kind: 'mobile-sam';
8
+ }>, 'id' | 'rgba'> & {
9
+ rgba: string;
10
+ }) | (Omit<Extract<NativeSegmentationRequest, {
11
+ kind: 'mobile-sam-expand';
12
+ }>, 'id' | 'rgba'> & {
13
+ rgba: string;
14
+ });
15
+ /**
16
+ * Explicit pinned MobileSAM inference in a separately owned Node child process.
17
+ * @experimental Native segmentation adapter; unqualified for general mask or object accuracy.
18
+ */
19
+ export declare function createMobileSamSegmenter(options: MobileSamSegmenterOptions): Promise<SegmentationPort>;
20
+ /** Internal adapter seam: pool ownership stays with the caller. */
21
+ export declare function createMobileSamPortForPool(pool: Pick<NativeChildPool<SerializedTask>, 'runLazy' | 'settle' | 'dispose'>, parsed: z.output<typeof optionsSchema>): SegmentationPort & Required<Pick<SegmentationPort, 'expand'>>;
22
+ export {};