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.
- package/README.md +3 -3
- 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 -34
- package/dist/app/create-classifier.js +37 -237
- package/dist/entities/classification.d.ts +98 -0
- package/dist/entities/classification.js +28 -0
- package/dist/entities/contracts.d.ts +6 -475
- 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/segmentation-metadata.d.ts +70 -0
- package/dist/entities/segmentation-metadata.js +21 -1
- package/dist/entities/validation.d.ts +2 -0
- package/dist/entities/validation.js +9 -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.js +2 -4
- 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 -66
- package/dist/infrastructure/segmentation/mobile-sam/index.js +2 -478
- 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/README.ru.md +15 -12
- package/docs/agents.ru.md +95 -0
- package/docs/architecture.md +31 -5
- package/docs/architecture.ru.md +146 -0
- package/docs/benchmarks.md +3 -1
- package/docs/development.ru.md +141 -0
- package/docs/provenance.json +5 -1
- package/docs/publishing.md +5 -3
- package/docs/refactoring-plan.ru.md +183 -0
- package/package.json +4 -2
- 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
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
import { isProxy } from 'node:util/types';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { ClassifierError } from '../entities/errors.js';
|
|
4
|
-
export const requestLimit = 32 * 1024 * 1024;
|
|
5
|
-
export const responseLimit = 1024 * 1024;
|
|
6
|
-
export const probability = z.number().finite().min(0).max(1);
|
|
7
|
-
export const key = z
|
|
8
|
-
.string()
|
|
9
|
-
.min(1)
|
|
10
|
-
.max(128)
|
|
11
|
-
.refine((value) => value !== '__proto__');
|
|
12
|
-
const text = z.string().min(1).max(64_000);
|
|
13
|
-
const imageSchema = z
|
|
14
|
-
.object({
|
|
15
|
-
type: z.literal('image_url'),
|
|
16
|
-
image_url: z.object({ url: z.string().max(22_369_656), detail: z.literal('high') }).strict(),
|
|
17
|
-
})
|
|
18
|
-
.strict();
|
|
19
|
-
const questionSchema = z.discriminatedUnion('type', [
|
|
20
|
-
z
|
|
21
|
-
.object({
|
|
22
|
-
type: z.literal('choice'),
|
|
23
|
-
rankingSize: z.union([z.literal(1), z.literal(4)]).default(4),
|
|
24
|
-
instructions: text,
|
|
25
|
-
criteria: z
|
|
26
|
-
.record(key, text)
|
|
27
|
-
.refine((value) => Object.keys(value).length >= 2 && Object.keys(value).length <= 255),
|
|
28
|
-
})
|
|
29
|
-
.strict(),
|
|
30
|
-
z.object({ type: z.literal('noul'), instructions: text }).strict(),
|
|
31
|
-
]);
|
|
32
|
-
export const requestSchema = z
|
|
33
|
-
.object({
|
|
34
|
-
state: z
|
|
35
|
-
.array(z.union([text, imageSchema]))
|
|
36
|
-
.min(1)
|
|
37
|
-
.max(64)
|
|
38
|
-
.refine((value) => value.filter((part) => typeof part !== 'string').length <= 9),
|
|
39
|
-
questions: z
|
|
40
|
-
.record(key, questionSchema)
|
|
41
|
-
.refine((value) => Object.keys(value).length >= 1 && Object.keys(value).length <= 16),
|
|
42
|
-
})
|
|
43
|
-
.strict();
|
|
44
|
-
const localErrors = new WeakSet();
|
|
45
|
-
export function ownedError(code, message, details = {}) {
|
|
46
|
-
const error = new ClassifierError(code, message, details);
|
|
47
|
-
localErrors.add(error);
|
|
48
|
-
return error;
|
|
49
|
-
}
|
|
50
|
-
export function invalid(code, message) {
|
|
51
|
-
return ownedError(code, message);
|
|
52
|
-
}
|
|
53
|
-
export function active(signal) {
|
|
54
|
-
if (!signal.aborted)
|
|
55
|
-
return;
|
|
56
|
-
const timeout = signal.reason instanceof ClassifierError && signal.reason.code === 'TIMEOUT';
|
|
57
|
-
throw ownedError(timeout ? 'TIMEOUT' : 'ABORTED', timeout ? 'Operation timed out' : 'Operation cancelled');
|
|
58
|
-
}
|
|
59
|
-
export function validated(schema, input, code) {
|
|
60
|
-
const parsed = schema.safeParse(input);
|
|
61
|
-
if (!parsed.success)
|
|
62
|
-
throw invalid(code, 'Decision schema validation failed');
|
|
63
|
-
return parsed.data;
|
|
64
|
-
}
|
|
65
|
-
function imageSize(bytes, format) {
|
|
66
|
-
if (format === 'png') {
|
|
67
|
-
if (bytes.length < 33 ||
|
|
68
|
-
!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])) ||
|
|
69
|
-
bytes.readUInt32BE(8) !== 13 ||
|
|
70
|
-
bytes.toString('ascii', 12, 16) !== 'IHDR')
|
|
71
|
-
throw invalid('INVALID_INPUT', 'Invalid PNG image header');
|
|
72
|
-
return [bytes.readUInt32BE(16), bytes.readUInt32BE(20)];
|
|
73
|
-
}
|
|
74
|
-
if (bytes[0] !== 255 || bytes[1] !== 216)
|
|
75
|
-
throw invalid('INVALID_INPUT', 'Invalid JPEG image header');
|
|
76
|
-
let offset = 2;
|
|
77
|
-
while (offset + 3 < bytes.length) {
|
|
78
|
-
if (bytes[offset++] !== 255)
|
|
79
|
-
break;
|
|
80
|
-
while (bytes[offset] === 255)
|
|
81
|
-
offset++;
|
|
82
|
-
const marker = bytes[offset++];
|
|
83
|
-
if (marker === undefined || marker === 218 || marker === 217)
|
|
84
|
-
break;
|
|
85
|
-
if (offset + 2 > bytes.length)
|
|
86
|
-
break;
|
|
87
|
-
const length = bytes.readUInt16BE(offset);
|
|
88
|
-
if (length < 2 || offset + length > bytes.length)
|
|
89
|
-
break;
|
|
90
|
-
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker)) {
|
|
91
|
-
if (length < 8)
|
|
92
|
-
break;
|
|
93
|
-
return [bytes.readUInt16BE(offset + 5), bytes.readUInt16BE(offset + 3)];
|
|
94
|
-
}
|
|
95
|
-
offset += length;
|
|
96
|
-
}
|
|
97
|
-
throw invalid('INVALID_INPUT', 'Invalid JPEG image dimensions');
|
|
98
|
-
}
|
|
99
|
-
export function validateImage(url) {
|
|
100
|
-
const prefix = /^data:image\/(png|jpeg);base64,/u.exec(url);
|
|
101
|
-
if (!prefix)
|
|
102
|
-
throw invalid('INVALID_INPUT', 'Only inline PNG or JPEG images are supported');
|
|
103
|
-
const encoded = url.slice(prefix[0].length);
|
|
104
|
-
const padding = encoded.endsWith('==') ? 2 : Number(encoded.endsWith('='));
|
|
105
|
-
if (!encoded.length ||
|
|
106
|
-
encoded.length % 4 !== 0 ||
|
|
107
|
-
!/^[A-Za-z0-9+/]+$/u.test(encoded.slice(0, encoded.length - padding)))
|
|
108
|
-
throw invalid('INVALID_INPUT', 'Invalid image base64');
|
|
109
|
-
const bytes = Buffer.from(encoded, 'base64');
|
|
110
|
-
if (bytes.length > 16 * 1024 * 1024 || bytes.toString('base64') !== encoded)
|
|
111
|
-
throw invalid('INVALID_INPUT', 'Invalid image byte representation');
|
|
112
|
-
const [width, height] = imageSize(bytes, prefix[1] ?? '');
|
|
113
|
-
if (width < 280 || height < 280 || width > 1568 || height > 1568)
|
|
114
|
-
throw invalid('INVALID_INPUT', 'Prepared image sides must be 280..1568 pixels');
|
|
115
|
-
}
|
|
116
|
-
/** Bounded owned snapshot; schema parsing never receives caller iterators or accessors. */
|
|
117
|
-
export function preflightJson(input, limit) {
|
|
118
|
-
let remaining = limit;
|
|
119
|
-
let nodes = 0;
|
|
120
|
-
const ancestors = new Set();
|
|
121
|
-
function charge(bytes) {
|
|
122
|
-
remaining -= bytes;
|
|
123
|
-
if (remaining < 0)
|
|
124
|
-
throw ownedError('RESOURCE_LIMIT', 'Decision request exceeds byte limit');
|
|
125
|
-
}
|
|
126
|
-
function string(value) {
|
|
127
|
-
charge(2);
|
|
128
|
-
for (let index = 0; index < value.length; index++) {
|
|
129
|
-
const code = value.charCodeAt(index);
|
|
130
|
-
if (code < 32)
|
|
131
|
-
charge([8, 9, 10, 12, 13].includes(code) ? 2 : 6);
|
|
132
|
-
else if (code === 34 || code === 92)
|
|
133
|
-
charge(2);
|
|
134
|
-
else if (code < 128)
|
|
135
|
-
charge(1);
|
|
136
|
-
else if (code < 2048)
|
|
137
|
-
charge(2);
|
|
138
|
-
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
139
|
-
const next = value.charCodeAt(index + 1);
|
|
140
|
-
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
141
|
-
charge(4);
|
|
142
|
-
index++;
|
|
143
|
-
}
|
|
144
|
-
else
|
|
145
|
-
charge(6);
|
|
146
|
-
}
|
|
147
|
-
else
|
|
148
|
-
charge(code >= 0xdc00 && code <= 0xdfff ? 6 : 3);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function visit(value, depth) {
|
|
152
|
-
if (++nodes > 10_000 || depth > 6)
|
|
153
|
-
throw invalid('INVALID_INPUT', 'Decision structure exceeds supported limits');
|
|
154
|
-
if (typeof value === 'string') {
|
|
155
|
-
string(value);
|
|
156
|
-
return value;
|
|
157
|
-
}
|
|
158
|
-
if (value === null || typeof value === 'boolean') {
|
|
159
|
-
charge(value === false ? 5 : 4);
|
|
160
|
-
return value;
|
|
161
|
-
}
|
|
162
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
163
|
-
charge(String(value).length);
|
|
164
|
-
return value;
|
|
165
|
-
}
|
|
166
|
-
if (typeof value !== 'object' || value === null)
|
|
167
|
-
throw invalid('INVALID_INPUT', 'Unsupported decision value');
|
|
168
|
-
if (isProxy(value))
|
|
169
|
-
throw invalid('INVALID_INPUT', 'Decision input must contain plain values');
|
|
170
|
-
// Zod reads declared object fields even when they are not enumerable; JSON does not.
|
|
171
|
-
for (const name of [
|
|
172
|
-
'state',
|
|
173
|
-
'questions',
|
|
174
|
-
'type',
|
|
175
|
-
'image_url',
|
|
176
|
-
'url',
|
|
177
|
-
'detail',
|
|
178
|
-
'instructions',
|
|
179
|
-
'criteria',
|
|
180
|
-
]) {
|
|
181
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
182
|
-
if (descriptor && !descriptor.enumerable)
|
|
183
|
-
throw invalid('INVALID_INPUT', 'Decision fields must be enumerable');
|
|
184
|
-
}
|
|
185
|
-
if (ancestors.has(value))
|
|
186
|
-
throw invalid('INVALID_INPUT', 'Circular decision structure');
|
|
187
|
-
ancestors.add(value);
|
|
188
|
-
charge(2);
|
|
189
|
-
if (Array.isArray(value)) {
|
|
190
|
-
const copy = [];
|
|
191
|
-
if (value.length > 64)
|
|
192
|
-
throw invalid('INVALID_INPUT', 'Decision array exceeds supported limits');
|
|
193
|
-
for (let index = 0; index < value.length; index++) {
|
|
194
|
-
if (index)
|
|
195
|
-
charge(1);
|
|
196
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
197
|
-
if (!descriptor || !Object.hasOwn(descriptor, 'value'))
|
|
198
|
-
throw invalid('INVALID_INPUT', 'Decision input must contain plain values');
|
|
199
|
-
copy.push(visit(descriptor.value, depth + 1));
|
|
200
|
-
}
|
|
201
|
-
ancestors.delete(value);
|
|
202
|
-
return copy;
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
const copy = {};
|
|
206
|
-
const prototype = Object.getPrototypeOf(value);
|
|
207
|
-
if (prototype !== Object.prototype && prototype !== null)
|
|
208
|
-
throw invalid('INVALID_INPUT', 'Decision input must contain plain objects');
|
|
209
|
-
let count = 0;
|
|
210
|
-
for (const name in value) {
|
|
211
|
-
if (!Object.hasOwn(value, name))
|
|
212
|
-
continue;
|
|
213
|
-
if (++count > 255)
|
|
214
|
-
throw invalid('INVALID_INPUT', 'Decision object exceeds supported limits');
|
|
215
|
-
if (count > 1)
|
|
216
|
-
charge(1);
|
|
217
|
-
string(name);
|
|
218
|
-
charge(1);
|
|
219
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
220
|
-
if (!descriptor || !Object.hasOwn(descriptor, 'value'))
|
|
221
|
-
throw invalid('INVALID_INPUT', 'Decision input must contain plain values');
|
|
222
|
-
Object.defineProperty(copy, name, {
|
|
223
|
-
value: visit(descriptor.value, depth + 1),
|
|
224
|
-
enumerable: true,
|
|
225
|
-
configurable: true,
|
|
226
|
-
writable: true,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
ancestors.delete(value);
|
|
230
|
-
return copy;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
return visit(input, 0);
|
|
234
|
-
}
|
|
235
|
-
function statusError(status) {
|
|
236
|
-
const code = status === 401 || status === 403
|
|
237
|
-
? 'AUTHENTICATION'
|
|
238
|
-
: status === 413
|
|
239
|
-
? 'RESOURCE_LIMIT'
|
|
240
|
-
: status === 415
|
|
241
|
-
? 'UNSUPPORTED'
|
|
242
|
-
: 'PROVIDER';
|
|
243
|
-
return ownedError(code, 'Model provider rejected the request', { status });
|
|
244
|
-
}
|
|
245
|
-
export function failure(error, signal, cleanupFailed = false) {
|
|
246
|
-
const known = error instanceof ClassifierError && localErrors.has(error);
|
|
247
|
-
const incompleteCleanup = cleanupFailed || (known && error.details.cleanupFailed === true);
|
|
248
|
-
try {
|
|
249
|
-
active(signal);
|
|
250
|
-
}
|
|
251
|
-
catch (aborted) {
|
|
252
|
-
if (aborted instanceof ClassifierError)
|
|
253
|
-
return ownedError(aborted.code, aborted.message, incompleteCleanup ? { cleanupFailed: true } : {});
|
|
254
|
-
}
|
|
255
|
-
return ownedError(known ? error.code : 'PROVIDER', known ? error.message : 'Model provider transport failed', { ...(known ? error.details : {}), ...(incompleteCleanup ? { cleanupFailed: true } : {}) });
|
|
256
|
-
}
|
|
257
|
-
export async function readResponse(response, signal, limit) {
|
|
258
|
-
const reader = response.body?.getReader();
|
|
259
|
-
if (!reader) {
|
|
260
|
-
active(signal);
|
|
261
|
-
if (!response.ok)
|
|
262
|
-
throw statusError(response.status);
|
|
263
|
-
throw invalid('INVALID_RESPONSE', 'Provider response has no body');
|
|
264
|
-
}
|
|
265
|
-
let cancelTask;
|
|
266
|
-
function cancel() {
|
|
267
|
-
cancelTask ??= reader.cancel().then(() => true, () => false);
|
|
268
|
-
}
|
|
269
|
-
signal.addEventListener('abort', cancel, { once: true });
|
|
270
|
-
let caught;
|
|
271
|
-
let output = '';
|
|
272
|
-
let complete = false;
|
|
273
|
-
try {
|
|
274
|
-
active(signal);
|
|
275
|
-
if (!response.ok)
|
|
276
|
-
throw statusError(response.status);
|
|
277
|
-
const length = response.headers.get('content-length');
|
|
278
|
-
if (length !== null && (!/^\d+$/u.test(length) || Number(length) > limit))
|
|
279
|
-
throw ownedError('RESOURCE_LIMIT', 'Provider response exceeds byte limit');
|
|
280
|
-
const storage = Buffer.allocUnsafe(limit);
|
|
281
|
-
let bytes = 0;
|
|
282
|
-
while (true) {
|
|
283
|
-
active(signal);
|
|
284
|
-
const next = await reader.read();
|
|
285
|
-
active(signal);
|
|
286
|
-
if (next.done)
|
|
287
|
-
break;
|
|
288
|
-
if (next.value.byteLength > limit - bytes)
|
|
289
|
-
throw ownedError('RESOURCE_LIMIT', 'Provider response exceeds byte limit');
|
|
290
|
-
storage.set(next.value, bytes);
|
|
291
|
-
bytes += next.value.byteLength;
|
|
292
|
-
}
|
|
293
|
-
complete = true;
|
|
294
|
-
try {
|
|
295
|
-
output = new TextDecoder('utf-8', { fatal: true }).decode(storage.subarray(0, bytes));
|
|
296
|
-
}
|
|
297
|
-
catch {
|
|
298
|
-
throw invalid('INVALID_RESPONSE', 'Provider returned invalid UTF-8');
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
catch (error) {
|
|
302
|
-
caught = error;
|
|
303
|
-
}
|
|
304
|
-
finally {
|
|
305
|
-
signal.removeEventListener('abort', cancel);
|
|
306
|
-
if (!complete)
|
|
307
|
-
cancel();
|
|
308
|
-
const released = cancelTask ? await cancelTask : true;
|
|
309
|
-
reader.releaseLock();
|
|
310
|
-
if (!released)
|
|
311
|
-
caught = failure(caught, signal, true);
|
|
312
|
-
}
|
|
313
|
-
if (caught !== undefined)
|
|
314
|
-
throw failure(caught, signal);
|
|
315
|
-
active(signal);
|
|
316
|
-
return output;
|
|
317
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { PixelRect } from '../entities/contracts.js';
|
|
2
|
-
import type { ImageRepresentationPort, RepresentationView } from '../entities/representation.js';
|
|
3
|
-
export interface TextPixelView {
|
|
4
|
-
id: string;
|
|
5
|
-
role: RepresentationView['role'];
|
|
6
|
-
sourceRegion: PixelRect;
|
|
7
|
-
width: number;
|
|
8
|
-
height: number;
|
|
9
|
-
/** Clean, resized, row-major RGBA8. Grid labels remain separately available as metadata. */
|
|
10
|
-
data: Uint8Array;
|
|
11
|
-
grid?: RepresentationView['grid'];
|
|
12
|
-
}
|
|
13
|
-
export interface TextRepresentationEncoder {
|
|
14
|
-
readonly profile: string;
|
|
15
|
-
/** Hard UTF-8 upper bound for one encoded view at these dimensions. */
|
|
16
|
-
maxEncodedBytes(width: number, height: number): number;
|
|
17
|
-
encode(view: TextPixelView, signal: AbortSignal): Promise<string>;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Build request-wide text representations from clean canonical pixels. The injected encoder
|
|
21
|
-
* is intentionally internal so research codecs do not become public package exports.
|
|
22
|
-
*/
|
|
23
|
-
export declare function createTextRepresentation(encoder: TextRepresentationEncoder): ImageRepresentationPort;
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import sharp from 'sharp';
|
|
2
|
-
import { limits } from '../entities/contracts.js';
|
|
3
|
-
import { ClassifierError, assertActive } from '../entities/errors.js';
|
|
4
|
-
import { representationLimits } from '../entities/representation.js';
|
|
5
|
-
import { readCanonicalPixels } from './raster.js';
|
|
6
|
-
function isContained(outer, inner) {
|
|
7
|
-
return (inner.left >= outer.left &&
|
|
8
|
-
inner.top >= outer.top &&
|
|
9
|
-
inner.left + inner.width <= outer.left + outer.width &&
|
|
10
|
-
inner.top + inner.height <= outer.top + outer.height);
|
|
11
|
-
}
|
|
12
|
-
function key(region) {
|
|
13
|
-
return `${region.left},${region.top},${region.width},${region.height}`;
|
|
14
|
-
}
|
|
15
|
-
function dimensions(region, maxSide) {
|
|
16
|
-
const scale = Math.min(1, maxSide / Math.max(region.width, region.height));
|
|
17
|
-
return {
|
|
18
|
-
width: Math.max(1, Math.round(region.width * scale)),
|
|
19
|
-
height: Math.max(1, Math.round(region.height * scale)),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
function assertGridGeometry(view, width, height) {
|
|
23
|
-
if (!view.grid)
|
|
24
|
-
return;
|
|
25
|
-
const map = (rect) => ({
|
|
26
|
-
left: Math.round(((rect.left - view.sourceRegion.left) * width) / view.sourceRegion.width),
|
|
27
|
-
top: Math.round(((rect.top - view.sourceRegion.top) * height) / view.sourceRegion.height),
|
|
28
|
-
width: Math.round((rect.width * width) / view.sourceRegion.width),
|
|
29
|
-
height: Math.round((rect.height * height) / view.sourceRegion.height),
|
|
30
|
-
});
|
|
31
|
-
const mapped = view.grid.cells.map((cell) => map(cell.region));
|
|
32
|
-
if (view.grid.cells.some((cell) => !isContained(view.sourceRegion, cell.region)))
|
|
33
|
-
throw new ClassifierError('INVALID_INPUT', 'Grid geometry lies outside its source region');
|
|
34
|
-
if (mapped.some((rect) => rect.width < 1 || rect.height < 1))
|
|
35
|
-
throw new ClassifierError('RESOURCE_LIMIT', 'Distinct grid geometry is indistinguishable at the allocated resolution', { component: 'representation' });
|
|
36
|
-
const sourceKeys = new Set(view.grid.cells.map((cell) => key(cell.region)));
|
|
37
|
-
const mappedKeys = new Set(mapped.map((rect) => key(rect)));
|
|
38
|
-
if (sourceKeys.size !== mappedKeys.size)
|
|
39
|
-
throw new ClassifierError('RESOURCE_LIMIT', 'Distinct grid geometry collapses at the allocated resolution', {
|
|
40
|
-
component: 'representation',
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
async function cleanPixels(source, sourcePixels, view, width, height, signal) {
|
|
44
|
-
const region = view.sourceRegion;
|
|
45
|
-
if (!isContained({ left: 0, top: 0, width: source.width, height: source.height }, region))
|
|
46
|
-
throw new ClassifierError('INVALID_INPUT', 'Representation view lies outside the oriented source');
|
|
47
|
-
const raw = Buffer.from(sourcePixels.buffer, sourcePixels.byteOffset, sourcePixels.byteLength);
|
|
48
|
-
const pipeline = sharp(raw, {
|
|
49
|
-
raw: { width: source.width, height: source.height, channels: 4 },
|
|
50
|
-
limitInputPixels: limits.imagePixels,
|
|
51
|
-
})
|
|
52
|
-
.extract(region)
|
|
53
|
-
.resize(width, height, { kernel: 'nearest' })
|
|
54
|
-
.raw();
|
|
55
|
-
try {
|
|
56
|
-
const result = await pipeline.toBuffer({ resolveWithObject: true });
|
|
57
|
-
assertActive(signal);
|
|
58
|
-
if (result.info.channels !== 4 || result.data.byteLength !== width * height * 4)
|
|
59
|
-
throw new ClassifierError('INVALID_RESPONSE', 'Sharp returned an invalid canonical pixel view');
|
|
60
|
-
return {
|
|
61
|
-
id: view.id,
|
|
62
|
-
role: view.role,
|
|
63
|
-
sourceRegion: region,
|
|
64
|
-
width,
|
|
65
|
-
height,
|
|
66
|
-
data: result.data,
|
|
67
|
-
...(view.grid ? { grid: view.grid } : {}),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
pipeline.destroy();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function aliasesFor(view) {
|
|
75
|
-
return {
|
|
76
|
-
id: view.id,
|
|
77
|
-
role: view.role,
|
|
78
|
-
sourceRegion: view.sourceRegion,
|
|
79
|
-
...(view.grid ? { grid: view.grid } : {}),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
function gridMetadata(grid) {
|
|
83
|
-
if (!grid)
|
|
84
|
-
return undefined;
|
|
85
|
-
return {
|
|
86
|
-
cells: grid.cells.map((cell) => ({
|
|
87
|
-
id: cell.id,
|
|
88
|
-
column: cell.column,
|
|
89
|
-
row: cell.row,
|
|
90
|
-
region: cell.region,
|
|
91
|
-
label: grid.labels.get(cell.id) ?? null,
|
|
92
|
-
})),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
function makePlan(groups, maxSide, encoder) {
|
|
96
|
-
const plan = [];
|
|
97
|
-
let totalBytes = 0;
|
|
98
|
-
for (const group of groups) {
|
|
99
|
-
const size = dimensions(group.sourceRegion, maxSide);
|
|
100
|
-
const metadataText = JSON.stringify({
|
|
101
|
-
representationView: {
|
|
102
|
-
id: group.aliases[0]?.id ?? group.primary.id,
|
|
103
|
-
aliases: group.aliases.map((alias) => ({
|
|
104
|
-
id: alias.id,
|
|
105
|
-
role: alias.role,
|
|
106
|
-
sourceRegion: alias.sourceRegion,
|
|
107
|
-
})),
|
|
108
|
-
sourceRegion: group.sourceRegion,
|
|
109
|
-
dimensions: size,
|
|
110
|
-
scaleX: size.width / group.sourceRegion.width,
|
|
111
|
-
scaleY: size.height / group.sourceRegion.height,
|
|
112
|
-
grid: group.aliases.flatMap((alias) => {
|
|
113
|
-
const value = gridMetadata(alias.grid);
|
|
114
|
-
return value === undefined ? [] : [{ viewId: alias.id, cells: value.cells }];
|
|
115
|
-
}),
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
const bound = encoder.maxEncodedBytes(size.width, size.height);
|
|
119
|
-
const metadataBytes = Buffer.byteLength(metadataText, 'utf8');
|
|
120
|
-
if (!Number.isSafeInteger(bound) ||
|
|
121
|
-
bound < 0 ||
|
|
122
|
-
bound > representationLimits.partCharacters ||
|
|
123
|
-
metadataText.length > representationLimits.partCharacters)
|
|
124
|
-
return undefined;
|
|
125
|
-
totalBytes += metadataBytes + bound;
|
|
126
|
-
plan.push({ group, ...size, metadataText, maxEncodedBytes: bound });
|
|
127
|
-
}
|
|
128
|
-
return totalBytes <= representationLimits.totalBytes ? plan : undefined;
|
|
129
|
-
}
|
|
130
|
-
function buildGroups(views) {
|
|
131
|
-
const groups = new Map();
|
|
132
|
-
for (const view of views) {
|
|
133
|
-
const regionKey = key(view.sourceRegion);
|
|
134
|
-
const prior = groups.get(regionKey);
|
|
135
|
-
if (!prior) {
|
|
136
|
-
groups.set(regionKey, {
|
|
137
|
-
sourceRegion: view.sourceRegion,
|
|
138
|
-
primary: view,
|
|
139
|
-
aliases: [aliasesFor(view)],
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
prior.aliases.push(aliasesFor(view));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return [...groups.values()];
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Build request-wide text representations from clean canonical pixels. The injected encoder
|
|
150
|
-
* is intentionally internal so research codecs do not become public package exports.
|
|
151
|
-
*/
|
|
152
|
-
export function createTextRepresentation(encoder) {
|
|
153
|
-
return {
|
|
154
|
-
async prepare(input) {
|
|
155
|
-
assertActive(input.signal);
|
|
156
|
-
if (input.views.length > representationLimits.maxViews)
|
|
157
|
-
throw new ClassifierError('RESOURCE_LIMIT', 'A representation request may contain at most nine views', {
|
|
158
|
-
component: 'representation',
|
|
159
|
-
});
|
|
160
|
-
const groups = buildGroups(input.views);
|
|
161
|
-
for (const group of groups) {
|
|
162
|
-
if (!isContained({ left: 0, top: 0, width: input.source.width, height: input.source.height }, group.sourceRegion))
|
|
163
|
-
throw new ClassifierError('INVALID_INPUT', 'Representation view lies outside the oriented source');
|
|
164
|
-
}
|
|
165
|
-
let plan;
|
|
166
|
-
for (let side = representationLimits.maxSide; side >= 1 && plan === undefined; side -= 1)
|
|
167
|
-
plan = makePlan(groups, side, encoder);
|
|
168
|
-
if (!plan)
|
|
169
|
-
throw new ClassifierError('RESOURCE_LIMIT', 'No bounded text representation allocation fits the request', {
|
|
170
|
-
component: 'representation',
|
|
171
|
-
});
|
|
172
|
-
for (const item of plan)
|
|
173
|
-
for (const alias of item.group.aliases)
|
|
174
|
-
if (alias.grid)
|
|
175
|
-
assertGridGeometry({ ...item.group.primary, sourceRegion: item.group.sourceRegion, grid: alias.grid }, item.width, item.height);
|
|
176
|
-
const decoded = await readCanonicalPixels(input.source, input.signal);
|
|
177
|
-
const pixelViews = [];
|
|
178
|
-
for (const item of plan)
|
|
179
|
-
pixelViews.push(await cleanPixels(input.source, decoded.data, item.group.primary, item.width, item.height, input.signal));
|
|
180
|
-
const state = [];
|
|
181
|
-
const metadata = { profile: encoder.profile, bytes: 0, views: [] };
|
|
182
|
-
for (let index = 0; index < pixelViews.length; index += 1) {
|
|
183
|
-
const view = pixelViews[index];
|
|
184
|
-
const allocation = plan[index];
|
|
185
|
-
if (!view || !allocation)
|
|
186
|
-
throw new ClassifierError('INVALID_RESPONSE', 'Text representation allocation was incomplete');
|
|
187
|
-
assertActive(input.signal);
|
|
188
|
-
state.push(allocation.metadataText);
|
|
189
|
-
metadata.bytes += Buffer.byteLength(allocation.metadataText, 'utf8');
|
|
190
|
-
let text;
|
|
191
|
-
try {
|
|
192
|
-
text = await encoder.encode(view, input.signal);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
if (error instanceof ClassifierError && error.code === 'RESOURCE_LIMIT')
|
|
196
|
-
throw new ClassifierError(error.code, error.message, {
|
|
197
|
-
...error.details,
|
|
198
|
-
component: 'representation',
|
|
199
|
-
});
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
const bytes = Buffer.byteLength(text, 'utf8');
|
|
203
|
-
if (text.length > representationLimits.partCharacters || bytes > allocation.maxEncodedBytes)
|
|
204
|
-
throw new ClassifierError('RESOURCE_LIMIT', 'Encoder output exceeds its declared per-view bound', {
|
|
205
|
-
component: 'representation',
|
|
206
|
-
});
|
|
207
|
-
if (metadata.bytes + bytes > representationLimits.totalBytes)
|
|
208
|
-
throw new ClassifierError('RESOURCE_LIMIT', 'Encoded request exceeds the aggregate UTF-8 byte limit', {
|
|
209
|
-
component: 'representation',
|
|
210
|
-
});
|
|
211
|
-
metadata.bytes += bytes;
|
|
212
|
-
state.push(text);
|
|
213
|
-
metadata.views.push({
|
|
214
|
-
id: view.id,
|
|
215
|
-
sourceRegion: view.sourceRegion,
|
|
216
|
-
width: view.width,
|
|
217
|
-
height: view.height,
|
|
218
|
-
scaleX: view.width / view.sourceRegion.width,
|
|
219
|
-
scaleY: view.height / view.sourceRegion.height,
|
|
220
|
-
content: { left: 0, top: 0, width: view.width, height: view.height },
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
return { state, metadata };
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|