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,72 @@
|
|
|
1
|
+
import { active, failure, invalid, ownedError } from './errors.js';
|
|
2
|
+
function statusError(status) {
|
|
3
|
+
const code = status === 401 || status === 403
|
|
4
|
+
? 'AUTHENTICATION'
|
|
5
|
+
: status === 413
|
|
6
|
+
? 'RESOURCE_LIMIT'
|
|
7
|
+
: status === 415
|
|
8
|
+
? 'UNSUPPORTED'
|
|
9
|
+
: 'PROVIDER';
|
|
10
|
+
return ownedError(code, 'Model provider rejected the request', { status });
|
|
11
|
+
}
|
|
12
|
+
export async function readResponse(response, signal, limit) {
|
|
13
|
+
const reader = response.body?.getReader();
|
|
14
|
+
if (!reader) {
|
|
15
|
+
active(signal);
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
throw statusError(response.status);
|
|
18
|
+
throw invalid('INVALID_RESPONSE', 'Provider response has no body');
|
|
19
|
+
}
|
|
20
|
+
let cancelTask;
|
|
21
|
+
function cancel() {
|
|
22
|
+
cancelTask ??= reader.cancel().then(() => true, () => false);
|
|
23
|
+
}
|
|
24
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
25
|
+
let caught;
|
|
26
|
+
let output = '';
|
|
27
|
+
let complete = false;
|
|
28
|
+
try {
|
|
29
|
+
active(signal);
|
|
30
|
+
if (!response.ok)
|
|
31
|
+
throw statusError(response.status);
|
|
32
|
+
const length = response.headers.get('content-length');
|
|
33
|
+
if (length !== null && (!/^\d+$/u.test(length) || Number(length) > limit))
|
|
34
|
+
throw ownedError('RESOURCE_LIMIT', 'Provider response exceeds byte limit');
|
|
35
|
+
const storage = Buffer.allocUnsafe(limit);
|
|
36
|
+
let bytes = 0;
|
|
37
|
+
while (true) {
|
|
38
|
+
active(signal);
|
|
39
|
+
const next = await reader.read();
|
|
40
|
+
active(signal);
|
|
41
|
+
if (next.done)
|
|
42
|
+
break;
|
|
43
|
+
if (next.value.byteLength > limit - bytes)
|
|
44
|
+
throw ownedError('RESOURCE_LIMIT', 'Provider response exceeds byte limit');
|
|
45
|
+
storage.set(next.value, bytes);
|
|
46
|
+
bytes += next.value.byteLength;
|
|
47
|
+
}
|
|
48
|
+
complete = true;
|
|
49
|
+
try {
|
|
50
|
+
output = new TextDecoder('utf-8', { fatal: true }).decode(storage.subarray(0, bytes));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw invalid('INVALID_RESPONSE', 'Provider returned invalid UTF-8');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
caught = error;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
signal.removeEventListener('abort', cancel);
|
|
61
|
+
if (!complete)
|
|
62
|
+
cancel();
|
|
63
|
+
const released = cancelTask ? await cancelTask : true;
|
|
64
|
+
reader.releaseLock();
|
|
65
|
+
if (!released)
|
|
66
|
+
caught = failure(caught, signal, true);
|
|
67
|
+
}
|
|
68
|
+
if (caught !== undefined)
|
|
69
|
+
throw failure(caught, signal);
|
|
70
|
+
active(signal);
|
|
71
|
+
return output;
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface ResponseModelIdentity {
|
|
2
|
+
model: string;
|
|
3
|
+
responseModelIds?: readonly string[] | undefined;
|
|
4
|
+
}
|
|
5
|
+
export declare function acceptsResponseModel(value: string, identity: ResponseModelIdentity): boolean;
|
|
6
|
+
interface PinnedProviderIdentity {
|
|
7
|
+
provider?: string | undefined;
|
|
8
|
+
providerName?: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function unexpectedResponseProvider(envelopeProvider: string | null | undefined, pinned: PinnedProviderIdentity): boolean;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function routeName(value) {
|
|
2
|
+
return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
3
|
+
}
|
|
4
|
+
export function acceptsResponseModel(value, identity) {
|
|
5
|
+
if (value === identity.model || identity.responseModelIds?.includes(value))
|
|
6
|
+
return true;
|
|
7
|
+
if (!value.startsWith(identity.model + '-'))
|
|
8
|
+
return false;
|
|
9
|
+
const suffix = value.slice(identity.model.length + 1);
|
|
10
|
+
if (!/^\d{4}-?\d{2}-?\d{2}$/u.test(suffix))
|
|
11
|
+
return false;
|
|
12
|
+
const digits = suffix.replaceAll('-', ''), date = `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
|
|
13
|
+
const parsed = new Date(date + 'T00:00:00.000Z');
|
|
14
|
+
return Number.isFinite(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === date;
|
|
15
|
+
}
|
|
16
|
+
export function unexpectedResponseProvider(envelopeProvider, pinned) {
|
|
17
|
+
if (!envelopeProvider)
|
|
18
|
+
return false;
|
|
19
|
+
const expected = pinned.providerName ?? pinned.provider;
|
|
20
|
+
if (expected === undefined)
|
|
21
|
+
return false;
|
|
22
|
+
return routeName(envelopeProvider) !== routeName(expected);
|
|
23
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createVisionChat, visionOptionsSchema } from './vision-chat.js';
|
|
2
|
-
import { validated } from './
|
|
2
|
+
import { validated } from './decision-transport/errors.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
const schema = visionOptionsSchema
|
|
5
5
|
.omit({ endpoint: true, headers: true })
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ClassifierError } from '../entities/errors.js';
|
|
3
3
|
import { acceptsChoiceMass } from '../shared/choice-mass.js';
|
|
4
|
-
import { active,
|
|
4
|
+
import { active, failure, invalid, ownedError, validated } from './decision-transport/errors.js';
|
|
5
|
+
import { validateImage } from './decision-transport/images.js';
|
|
6
|
+
import { key, preflightJson, probability, requestLimit, requestSchema, responseLimit, } from './decision-transport/request.js';
|
|
7
|
+
import { readResponse } from './decision-transport/response.js';
|
|
8
|
+
import { acceptsResponseModel, unexpectedResponseProvider, } from './decision-transport/route-identity.js';
|
|
5
9
|
const endpoint = 'https://openrouter.ai/api/alpha/decisions';
|
|
6
10
|
const responseSchema = z
|
|
7
11
|
.object({
|
|
@@ -59,9 +63,6 @@ const optionsSchema = legacyOptionsSchema
|
|
|
59
63
|
probabilityRoundingStep: z.literal(0.01).optional(),
|
|
60
64
|
})
|
|
61
65
|
.strict();
|
|
62
|
-
function routeName(value) {
|
|
63
|
-
return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
|
|
64
|
-
}
|
|
65
66
|
function serialize(input, maxBytes, options) {
|
|
66
67
|
const envelope = {
|
|
67
68
|
model: options.model,
|
|
@@ -83,18 +84,6 @@ function serialize(input, maxBytes, options) {
|
|
|
83
84
|
validateImage(part.image_url.url);
|
|
84
85
|
return { request, body };
|
|
85
86
|
}
|
|
86
|
-
function acceptedModel(value, options) {
|
|
87
|
-
if (value === options.model || options.responseModelIds?.includes(value))
|
|
88
|
-
return true;
|
|
89
|
-
if (!value.startsWith(options.model + '-'))
|
|
90
|
-
return false;
|
|
91
|
-
const suffix = value.slice(options.model.length + 1);
|
|
92
|
-
if (!/^\d{4}-?\d{2}-?\d{2}$/u.test(suffix))
|
|
93
|
-
return false;
|
|
94
|
-
const digits = suffix.replaceAll('-', ''), date = `${digits.slice(0, 4)}-${digits.slice(4, 6)}-${digits.slice(6, 8)}`;
|
|
95
|
-
const parsed = new Date(date + 'T00:00:00.000Z');
|
|
96
|
-
return Number.isFinite(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === date;
|
|
97
|
-
}
|
|
98
87
|
function result(raw, request, response, durationMs, options) {
|
|
99
88
|
let json;
|
|
100
89
|
try {
|
|
@@ -104,10 +93,9 @@ function result(raw, request, response, durationMs, options) {
|
|
|
104
93
|
throw invalid('INVALID_RESPONSE', 'Provider returned invalid JSON');
|
|
105
94
|
}
|
|
106
95
|
const value = validated(responseSchema, json, 'INVALID_RESPONSE');
|
|
107
|
-
if (value.model && !
|
|
96
|
+
if (value.model && !acceptsResponseModel(value.model, options))
|
|
108
97
|
throw invalid('INVALID_RESPONSE', 'Provider returned an unexpected model');
|
|
109
|
-
if (value.provider &&
|
|
110
|
-
routeName(value.provider) !== routeName(options.providerName ?? options.provider))
|
|
98
|
+
if (value.provider && unexpectedResponseProvider(value.provider, options))
|
|
111
99
|
throw invalid('INVALID_RESPONSE', 'Provider returned an unexpected route');
|
|
112
100
|
const questionIds = Object.keys(request.questions);
|
|
113
101
|
if (Object.keys(value.answers).length !== questionIds.length)
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { limits, parse, pixelRectSchema } from '../entities/contracts.js';
|
|
4
|
+
import { containsRect } from '../entities/geometry.js';
|
|
4
5
|
import { ClassifierError, assertActive } from '../entities/errors.js';
|
|
5
|
-
function contains(outer, inner) {
|
|
6
|
-
return (inner.left >= outer.left &&
|
|
7
|
-
inner.top >= outer.top &&
|
|
8
|
-
inner.left + inner.width <= outer.left + outer.width &&
|
|
9
|
-
inner.top + inner.height <= outer.top + outer.height);
|
|
10
|
-
}
|
|
11
6
|
async function normalizeImage(bytes, signal) {
|
|
12
7
|
assertActive(signal);
|
|
13
8
|
const pipeline = sharp(bytes, { limitInputPixels: limits.imagePixels, failOn: 'error' });
|
|
@@ -40,7 +35,7 @@ async function normalizeImage(bytes, signal) {
|
|
|
40
35
|
}
|
|
41
36
|
async function cropImage(image, region, signal, scale = 1) {
|
|
42
37
|
assertActive(signal);
|
|
43
|
-
if (!
|
|
38
|
+
if (!containsRect({ left: 0, top: 0, width: image.width, height: image.height }, region))
|
|
44
39
|
throw new ClassifierError('INVALID_INPUT', 'ROI lies outside oriented source image');
|
|
45
40
|
const pipeline = sharp(image.bytes, { limitInputPixels: limits.imagePixels })
|
|
46
41
|
.extract(region)
|
|
@@ -198,7 +193,7 @@ export function createVisionRepresentation(raster) {
|
|
|
198
193
|
content: pixelRectSchema,
|
|
199
194
|
}),
|
|
200
195
|
}), json, 'INVALID_RESPONSE');
|
|
201
|
-
if (!
|
|
196
|
+
if (!containsRect({ left: 0, top: 0, ...transform.sourceImageTransform.output }, transform.sourceImageTransform.content))
|
|
202
197
|
throw new ClassifierError('INVALID_RESPONSE', 'Vision raster content lies outside output');
|
|
203
198
|
metadata.views.push({
|
|
204
199
|
id: view.id,
|
|
@@ -7,7 +7,11 @@ declare const optionsSchema: z.ZodObject<{
|
|
|
7
7
|
}, {
|
|
8
8
|
assetPath: string;
|
|
9
9
|
}>;
|
|
10
|
+
/** @experimental Options for the unqualified GrabCut segmentation factory. */
|
|
10
11
|
export type GrabCutSegmenterOptions = z.input<typeof optionsSchema>;
|
|
11
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Explicit pinned OpenCV GrabCut adapter. The supplied native asset is never downloaded.
|
|
14
|
+
* @experimental Native segmentation adapter; unqualified for general mask or object accuracy.
|
|
15
|
+
*/
|
|
12
16
|
export declare function createGrabCutSegmenter(options: GrabCutSegmenterOptions): Promise<SegmentationPort>;
|
|
13
17
|
export {};
|
|
@@ -6,17 +6,18 @@ import { NativeChildPool } from '../native-child.js';
|
|
|
6
6
|
import { nativeErrorReplySchema, nativeSegmentationReplySchema } from '../native-protocol.js';
|
|
7
7
|
import { verifyPinnedAsset } from '../verify-asset.js';
|
|
8
8
|
import { probeBackend } from '../probe-backend.js';
|
|
9
|
+
import { grabcutOpenCvArtifact } from './model-asset.js';
|
|
9
10
|
const optionsSchema = z.object({ assetPath: z.string().min(1).max(4096) }).strict();
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Explicit pinned OpenCV GrabCut adapter. The supplied native asset is never downloaded.
|
|
13
|
+
* @experimental Native segmentation adapter; unqualified for general mask or object accuracy.
|
|
14
|
+
*/
|
|
11
15
|
export async function createGrabCutSegmenter(options) {
|
|
12
16
|
const checked = optionsSchema.safeParse(options);
|
|
13
17
|
if (!checked.success)
|
|
14
18
|
throw new ClassifierError('INVALID_INPUT', 'Invalid GrabCut options');
|
|
15
19
|
const parsed = checked.data;
|
|
16
|
-
await verifyPinnedAsset(parsed.assetPath,
|
|
17
|
-
bytes: 10_964_323,
|
|
18
|
-
sha256: '63366510248adf3a7eddf3e793dd825404efb7df3749f4d6f8557c7fa4ca8aa0',
|
|
19
|
-
}, 'OpenCV.js');
|
|
20
|
+
await verifyPinnedAsset(parsed.assetPath, grabcutOpenCvArtifact, 'OpenCV.js');
|
|
20
21
|
const workerUrl = new URL(import.meta.url.endsWith('.ts') ? '../native-worker.ts' : '../native-worker.js', import.meta.url);
|
|
21
22
|
await probeBackend(workerUrl, 'grabcut', { assetPath: parsed.assetPath });
|
|
22
23
|
const pool = new NativeChildPool({
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { NativeSegmentationRequest } from '../native-protocol.js';
|
|
2
|
+
export declare function getBytes(mat: {
|
|
3
|
+
data?: unknown;
|
|
4
|
+
}, length: number): Uint8Array;
|
|
5
|
+
export declare function initializeGrabCutMask(input: {
|
|
6
|
+
flags: Uint8Array;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
seedRegion: {
|
|
10
|
+
left: number;
|
|
11
|
+
top: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
wholeRegion: {
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
roi: {
|
|
22
|
+
left: number;
|
|
23
|
+
top: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
};
|
|
27
|
+
prompt: 'whole-region' | 'seed-region';
|
|
28
|
+
labels: {
|
|
29
|
+
background: number;
|
|
30
|
+
foreground: number;
|
|
31
|
+
probableBackground: number;
|
|
32
|
+
probableForeground: number;
|
|
33
|
+
};
|
|
34
|
+
}): void;
|
|
35
|
+
/** Run native GrabCut, then reacquire Mat.data before copying its post-call flags. */
|
|
36
|
+
export declare function runGrabCutAndCopyMask(mat: {
|
|
37
|
+
data?: unknown;
|
|
38
|
+
}, width: number, height: number, roi: {
|
|
39
|
+
left: number;
|
|
40
|
+
top: number;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
}, runNative: () => void, labels: {
|
|
44
|
+
foreground: number;
|
|
45
|
+
probableForeground: number;
|
|
46
|
+
}): Uint8Array;
|
|
47
|
+
export declare function seededComponents(mask: Uint8Array, width: number, height: number, input: NativeSegmentationRequest & {
|
|
48
|
+
kind: 'grabcut';
|
|
49
|
+
}): Uint8Array;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function getBytes(mat, length) {
|
|
3
|
+
const bytes = z.instanceof(Uint8Array).parse(mat.data);
|
|
4
|
+
if (bytes.length !== length)
|
|
5
|
+
throw new Error('OpenCV matrix dimensions do not match');
|
|
6
|
+
return bytes;
|
|
7
|
+
}
|
|
8
|
+
export function initializeGrabCutMask(input) {
|
|
9
|
+
if (input.flags.length !== input.width * input.height)
|
|
10
|
+
throw new Error('GrabCut mask dimensions do not match source');
|
|
11
|
+
input.flags.fill(input.labels.background);
|
|
12
|
+
if (input.prompt === 'whole-region') {
|
|
13
|
+
for (let y = input.wholeRegion.top; y < input.wholeRegion.top + input.wholeRegion.height; y++)
|
|
14
|
+
for (let x = input.wholeRegion.left; x < input.wholeRegion.left + input.wholeRegion.width; x++)
|
|
15
|
+
input.flags[y * input.width + x] = input.labels.probableForeground;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (let y = input.roi.top; y < input.roi.top + input.roi.height; y++)
|
|
19
|
+
for (let x = input.roi.left; x < input.roi.left + input.roi.width; x++)
|
|
20
|
+
input.flags[y * input.width + x] = input.labels.probableBackground;
|
|
21
|
+
}
|
|
22
|
+
const seedLeft = Math.max(input.seedRegion.left, input.roi.left);
|
|
23
|
+
const seedTop = Math.max(input.seedRegion.top, input.roi.top);
|
|
24
|
+
const seedRight = Math.min(input.seedRegion.left + input.seedRegion.width, input.roi.left + input.roi.width);
|
|
25
|
+
const seedBottom = Math.min(input.seedRegion.top + input.seedRegion.height, input.roi.top + input.roi.height);
|
|
26
|
+
if (seedLeft >= seedRight || seedTop >= seedBottom)
|
|
27
|
+
throw new Error('GrabCut seed region does not intersect hard ROI');
|
|
28
|
+
for (let y = seedTop; y < seedBottom; y++)
|
|
29
|
+
for (let x = seedLeft; x < seedRight; x++)
|
|
30
|
+
input.flags[y * input.width + x] = input.labels.foreground;
|
|
31
|
+
}
|
|
32
|
+
/** Run native GrabCut, then reacquire Mat.data before copying its post-call flags. */
|
|
33
|
+
export function runGrabCutAndCopyMask(mat, width, height, roi, runNative, labels) {
|
|
34
|
+
runNative();
|
|
35
|
+
const flags = getBytes(mat, width * height);
|
|
36
|
+
const output = new Uint8Array(width * height);
|
|
37
|
+
for (let y = roi.top; y < roi.top + roi.height; y++) {
|
|
38
|
+
for (let x = roi.left; x < roi.left + roi.width; x++) {
|
|
39
|
+
const at = y * width + x;
|
|
40
|
+
const flag = flags[at];
|
|
41
|
+
if (flag === labels.foreground || flag === labels.probableForeground)
|
|
42
|
+
output[at] = 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return output;
|
|
46
|
+
}
|
|
47
|
+
export function seededComponents(mask, width, height, input) {
|
|
48
|
+
const visited = new Uint8Array(mask.length);
|
|
49
|
+
const output = new Uint8Array(mask.length);
|
|
50
|
+
const stack = [];
|
|
51
|
+
for (let start = 0; start < mask.length; start++) {
|
|
52
|
+
if (mask[start] !== 1 || visited[start] === 1)
|
|
53
|
+
continue;
|
|
54
|
+
stack.push(start);
|
|
55
|
+
visited[start] = 1;
|
|
56
|
+
const component = [];
|
|
57
|
+
let hitsSeed = false;
|
|
58
|
+
while (stack.length) {
|
|
59
|
+
const at = stack.pop();
|
|
60
|
+
if (at === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
component.push(at);
|
|
63
|
+
const x = at % width;
|
|
64
|
+
const y = Math.floor(at / width);
|
|
65
|
+
if (x >= input.seedRegion.left &&
|
|
66
|
+
y >= input.seedRegion.top &&
|
|
67
|
+
x < input.seedRegion.left + input.seedRegion.width &&
|
|
68
|
+
y < input.seedRegion.top + input.seedRegion.height)
|
|
69
|
+
hitsSeed = true;
|
|
70
|
+
const neighbors = [at - 1, at + 1, at - width, at + width];
|
|
71
|
+
for (const neighbor of neighbors) {
|
|
72
|
+
if (neighbor < 0 ||
|
|
73
|
+
neighbor >= mask.length ||
|
|
74
|
+
visited[neighbor] === 1 ||
|
|
75
|
+
mask[neighbor] !== 1)
|
|
76
|
+
continue;
|
|
77
|
+
const nx = neighbor % width;
|
|
78
|
+
if (Math.abs(nx - x) + Math.abs(Math.floor(neighbor / width) - y) !== 1)
|
|
79
|
+
continue;
|
|
80
|
+
visited[neighbor] = 1;
|
|
81
|
+
stack.push(neighbor);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (hitsSeed)
|
|
85
|
+
for (const index of component)
|
|
86
|
+
output[index] = 1;
|
|
87
|
+
}
|
|
88
|
+
return output;
|
|
89
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const cvSchema: z.ZodObject<{
|
|
3
|
+
Mat: z.ZodType<(...args: unknown[]) => unknown, z.ZodTypeDef, (...args: unknown[]) => unknown>;
|
|
4
|
+
Rect: z.ZodType<(...args: unknown[]) => unknown, z.ZodTypeDef, (...args: unknown[]) => unknown>;
|
|
5
|
+
cvtColor: z.ZodType<(...args: unknown[]) => unknown, z.ZodTypeDef, (...args: unknown[]) => unknown>;
|
|
6
|
+
grabCut: z.ZodType<(...args: unknown[]) => unknown, z.ZodTypeDef, (...args: unknown[]) => unknown>;
|
|
7
|
+
COLOR_RGBA2RGB: z.ZodNumber;
|
|
8
|
+
CV_8UC4: z.ZodNumber;
|
|
9
|
+
CV_8UC3: z.ZodNumber;
|
|
10
|
+
CV_8UC1: z.ZodNumber;
|
|
11
|
+
GC_BGD: z.ZodNumber;
|
|
12
|
+
GC_FGD: z.ZodNumber;
|
|
13
|
+
GC_PR_BGD: z.ZodNumber;
|
|
14
|
+
GC_PR_FGD: z.ZodNumber;
|
|
15
|
+
GC_INIT_WITH_MASK: z.ZodNumber;
|
|
16
|
+
GC_INIT_WITH_RECT: z.ZodNumber;
|
|
17
|
+
}, "strip", z.ZodTypeAny, {
|
|
18
|
+
Mat: (...args: unknown[]) => unknown;
|
|
19
|
+
Rect: (...args: unknown[]) => unknown;
|
|
20
|
+
cvtColor: (...args: unknown[]) => unknown;
|
|
21
|
+
grabCut: (...args: unknown[]) => unknown;
|
|
22
|
+
COLOR_RGBA2RGB: number;
|
|
23
|
+
CV_8UC4: number;
|
|
24
|
+
CV_8UC3: number;
|
|
25
|
+
CV_8UC1: number;
|
|
26
|
+
GC_BGD: number;
|
|
27
|
+
GC_FGD: number;
|
|
28
|
+
GC_PR_BGD: number;
|
|
29
|
+
GC_PR_FGD: number;
|
|
30
|
+
GC_INIT_WITH_MASK: number;
|
|
31
|
+
GC_INIT_WITH_RECT: number;
|
|
32
|
+
}, {
|
|
33
|
+
Mat: (...args: unknown[]) => unknown;
|
|
34
|
+
Rect: (...args: unknown[]) => unknown;
|
|
35
|
+
cvtColor: (...args: unknown[]) => unknown;
|
|
36
|
+
grabCut: (...args: unknown[]) => unknown;
|
|
37
|
+
COLOR_RGBA2RGB: number;
|
|
38
|
+
CV_8UC4: number;
|
|
39
|
+
CV_8UC3: number;
|
|
40
|
+
CV_8UC1: number;
|
|
41
|
+
GC_BGD: number;
|
|
42
|
+
GC_FGD: number;
|
|
43
|
+
GC_PR_BGD: number;
|
|
44
|
+
GC_PR_FGD: number;
|
|
45
|
+
GC_INIT_WITH_MASK: number;
|
|
46
|
+
GC_INIT_WITH_RECT: number;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function loadOpenCv(path: string): Promise<{
|
|
49
|
+
Mat: (...args: unknown[]) => unknown;
|
|
50
|
+
Rect: (...args: unknown[]) => unknown;
|
|
51
|
+
cvtColor: (...args: unknown[]) => unknown;
|
|
52
|
+
grabCut: (...args: unknown[]) => unknown;
|
|
53
|
+
COLOR_RGBA2RGB: number;
|
|
54
|
+
CV_8UC4: number;
|
|
55
|
+
CV_8UC3: number;
|
|
56
|
+
CV_8UC1: number;
|
|
57
|
+
GC_BGD: number;
|
|
58
|
+
GC_FGD: number;
|
|
59
|
+
GC_PR_BGD: number;
|
|
60
|
+
GC_PR_FGD: number;
|
|
61
|
+
GC_INIT_WITH_MASK: number;
|
|
62
|
+
GC_INIT_WITH_RECT: number;
|
|
63
|
+
}>;
|
|
64
|
+
export declare function probeGrabCut(assetPath: string): Promise<void>;
|
|
65
|
+
interface CvMat {
|
|
66
|
+
data?: unknown;
|
|
67
|
+
delete(): void;
|
|
68
|
+
}
|
|
69
|
+
export declare function matrix(cv: z.infer<typeof cvSchema>, args?: number[]): CvMat;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { Script } from 'node:vm';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { grabcutOpenCvArtifact } from './model-asset.js';
|
|
8
|
+
const cvSchema = z.object({
|
|
9
|
+
Mat: z.custom((value) => typeof value === 'function'),
|
|
10
|
+
Rect: z.custom((value) => typeof value === 'function'),
|
|
11
|
+
cvtColor: z.custom((value) => typeof value === 'function'),
|
|
12
|
+
grabCut: z.custom((value) => typeof value === 'function'),
|
|
13
|
+
COLOR_RGBA2RGB: z.number().int(),
|
|
14
|
+
CV_8UC4: z.number().int(),
|
|
15
|
+
CV_8UC3: z.number().int(),
|
|
16
|
+
CV_8UC1: z.number().int(),
|
|
17
|
+
GC_BGD: z.number().int(),
|
|
18
|
+
GC_FGD: z.number().int(),
|
|
19
|
+
GC_PR_BGD: z.number().int(),
|
|
20
|
+
GC_PR_FGD: z.number().int(),
|
|
21
|
+
GC_INIT_WITH_MASK: z.number().int(),
|
|
22
|
+
GC_INIT_WITH_RECT: z.number().int(),
|
|
23
|
+
});
|
|
24
|
+
async function readVerifiedAsset(path) {
|
|
25
|
+
const handle = await open(path, 'r');
|
|
26
|
+
try {
|
|
27
|
+
const stat = await handle.stat();
|
|
28
|
+
if (!stat.isFile() || stat.size !== grabcutOpenCvArtifact.bytes)
|
|
29
|
+
throw new Error('Pinned OpenCV size mismatch');
|
|
30
|
+
const source = Buffer.alloc(grabcutOpenCvArtifact.bytes);
|
|
31
|
+
let offset = 0;
|
|
32
|
+
while (offset < source.length) {
|
|
33
|
+
const read = await handle.read(source, offset, source.length - offset, null);
|
|
34
|
+
if (!read.bytesRead)
|
|
35
|
+
throw new Error('Pinned OpenCV asset read was incomplete');
|
|
36
|
+
offset += read.bytesRead;
|
|
37
|
+
}
|
|
38
|
+
if ((await handle.read(Buffer.alloc(1), 0, 1, null)).bytesRead !== 0)
|
|
39
|
+
throw new Error('Pinned OpenCV asset changed while being read');
|
|
40
|
+
if (createHash('sha256').update(source).digest('hex') !== grabcutOpenCvArtifact.sha256)
|
|
41
|
+
throw new Error('Pinned OpenCV SHA256 mismatch');
|
|
42
|
+
return source.toString('utf8');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await handle.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function loadOpenCv(path) {
|
|
49
|
+
path = resolve(path);
|
|
50
|
+
const source = await readVerifiedAsset(path);
|
|
51
|
+
const wrapper = { exports: {} };
|
|
52
|
+
const factory = new Script('(function(exports,require,module,__filename,__dirname){' + source + '\n})', { filename: path }).runInThisContext();
|
|
53
|
+
if (typeof factory !== 'function')
|
|
54
|
+
throw new Error('Pinned OpenCV asset is not a module');
|
|
55
|
+
Reflect.apply(factory, undefined, [
|
|
56
|
+
wrapper.exports,
|
|
57
|
+
createRequire(path),
|
|
58
|
+
wrapper,
|
|
59
|
+
path,
|
|
60
|
+
dirname(path),
|
|
61
|
+
]);
|
|
62
|
+
const exports = wrapper.exports;
|
|
63
|
+
if (typeof exports === 'object' && exports !== null && 'then' in exports) {
|
|
64
|
+
const then = Reflect.get(exports, 'then');
|
|
65
|
+
if (typeof then === 'function') {
|
|
66
|
+
return await new Promise((resolve, reject) => {
|
|
67
|
+
try {
|
|
68
|
+
Reflect.apply(then, exports, [
|
|
69
|
+
(value) => {
|
|
70
|
+
const checked = cvSchema.safeParse(value);
|
|
71
|
+
if (checked.success)
|
|
72
|
+
resolve(checked.data);
|
|
73
|
+
else
|
|
74
|
+
reject(new Error('Pinned OpenCV namespace is invalid'));
|
|
75
|
+
},
|
|
76
|
+
reject,
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
reject(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return cvSchema.parse(exports);
|
|
86
|
+
}
|
|
87
|
+
export async function probeGrabCut(assetPath) {
|
|
88
|
+
await loadOpenCv(assetPath);
|
|
89
|
+
}
|
|
90
|
+
const matShapeSchema = z
|
|
91
|
+
.object({ delete: z.custom((value) => typeof value === 'function') })
|
|
92
|
+
.passthrough();
|
|
93
|
+
const matSchema = z.custom((value) => matShapeSchema.safeParse(value).success);
|
|
94
|
+
export function matrix(cv, args = []) {
|
|
95
|
+
return matSchema.parse(Reflect.construct(cv.Mat, args));
|
|
96
|
+
}
|
|
@@ -1,47 +1,8 @@
|
|
|
1
1
|
import type { NativeSegmentationReply, NativeSegmentationRequest } from '../native-protocol.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
labels: {
|
|
27
|
-
background: number;
|
|
28
|
-
foreground: number;
|
|
29
|
-
probableBackground: number;
|
|
30
|
-
probableForeground: number;
|
|
31
|
-
};
|
|
32
|
-
}): void;
|
|
33
|
-
/** Run native GrabCut, then reacquire Mat.data before copying its post-call flags. */
|
|
34
|
-
export declare function runGrabCutAndCopyMask(mat: {
|
|
35
|
-
data?: unknown;
|
|
36
|
-
}, width: number, height: number, roi: {
|
|
37
|
-
left: number;
|
|
38
|
-
top: number;
|
|
39
|
-
width: number;
|
|
40
|
-
height: number;
|
|
41
|
-
}, runNative: () => void, labels: {
|
|
42
|
-
foreground: number;
|
|
43
|
-
probableForeground: number;
|
|
44
|
-
}): Uint8Array;
|
|
2
|
+
import { initializeGrabCutMask, runGrabCutAndCopyMask } from './masks.js';
|
|
3
|
+
import { probeGrabCut } from './runtime.js';
|
|
4
|
+
export { initializeGrabCutMask, runGrabCutAndCopyMask };
|
|
5
|
+
export { probeGrabCut };
|
|
45
6
|
export declare function runGrabCutTask(request: Extract<NativeSegmentationRequest, {
|
|
46
7
|
kind: 'grabcut';
|
|
47
8
|
}>): Promise<NativeSegmentationReply>;
|