@arcships/light-ocr 0.1.0 → 0.3.0

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/js/index.cjs CHANGED
@@ -7,7 +7,10 @@ const { loadNative } = require('./load-native.cjs');
7
7
 
8
8
  const DEFAULT_MODEL = 'ppocrv6-small';
9
9
  const MODEL_PACKAGE = '@arcships/light-ocr-model-ppocrv6-small';
10
- const EXPECTED_BUNDLE_ID = 'ppocrv6-small-onnx-20260714.1';
10
+ const CPU_BUNDLE_ID = 'ppocrv6-small-onnx-20260714.2';
11
+ const APPLE_BUNDLE_ID = 'ppocrv6-small-apple-20260715.1';
12
+ const WEBGPU_BUNDLE_ID = 'ppocrv6-small-webgpu-20260719.1';
13
+ const NATIVE_BUNDLE_ID = 'ppocrv6-small-native-20260719.1';
11
14
 
12
15
  class OcrError extends Error {
13
16
  constructor(code, message, detail) {
@@ -49,7 +52,7 @@ function abortReason(signal) {
49
52
  : signal.reason;
50
53
  }
51
54
 
52
- function resolveBuiltInBundle(model) {
55
+ function resolveBuiltInBundle(model, requireApple) {
53
56
  if (model !== DEFAULT_MODEL) {
54
57
  throw new OcrError(
55
58
  'invalid_argument',
@@ -76,11 +79,14 @@ function resolveBuiltInBundle(model) {
76
79
  cause instanceof Error ? cause.message : String(cause),
77
80
  );
78
81
  }
79
- if (manifest.bundleId !== EXPECTED_BUNDLE_ID) {
82
+ const compatibleBundleIds = requireApple
83
+ ? [APPLE_BUNDLE_ID, NATIVE_BUNDLE_ID]
84
+ : [CPU_BUNDLE_ID, APPLE_BUNDLE_ID, WEBGPU_BUNDLE_ID, NATIVE_BUNDLE_ID];
85
+ if (!compatibleBundleIds.includes(manifest.bundleId)) {
80
86
  throw new OcrError(
81
87
  'package_load_failed',
82
88
  'The installed model package is incompatible with this light-ocr release',
83
- `expected ${EXPECTED_BUNDLE_ID}, received ${String(manifest.bundleId)}`,
89
+ `expected ${compatibleBundleIds.join(' or ')}, received ${String(manifest.bundleId)}`,
84
90
  );
85
91
  }
86
92
  return path.dirname(manifestPath);
@@ -101,7 +107,8 @@ function resolveCreateOptions(options) {
101
107
  }
102
108
  if (hasBundlePath) return options;
103
109
  const model = hasModel ? options.model : DEFAULT_MODEL;
104
- const resolved = { ...options, bundlePath: resolveBuiltInBundle(model) };
110
+ const requireApple = options.execution?.provider === 'apple';
111
+ const resolved = { ...options, bundlePath: resolveBuiltInBundle(model, requireApple) };
105
112
  delete resolved.model;
106
113
  return resolved;
107
114
  }
@@ -117,6 +124,14 @@ class OcrEngineImpl {
117
124
  }
118
125
 
119
126
  recognize(image, options = {}) {
127
+ return this.#recognize('recognize', image, options);
128
+ }
129
+
130
+ recognizeEncoded(data, options = {}) {
131
+ return this.#recognize('recognizeEncoded', data, options);
132
+ }
133
+
134
+ #recognize(nativeMethod, image, options) {
120
135
  let signal;
121
136
  let nativeOptions;
122
137
  try {
@@ -136,7 +151,7 @@ class OcrEngineImpl {
136
151
 
137
152
  let operation;
138
153
  try {
139
- operation = this.#native.recognize(image, nativeOptions);
154
+ operation = this.#native[nativeMethod](image, nativeOptions);
140
155
  } catch (error) {
141
156
  return Promise.reject(normalizeNativeError(error));
142
157
  }
@@ -188,13 +203,16 @@ class OcrEngineImpl {
188
203
  }
189
204
  }
190
205
 
191
- let binding;
206
+ let nativeRuntime;
192
207
 
193
208
  async function createEngine(options) {
194
209
  try {
195
210
  const resolvedOptions = resolveCreateOptions(options);
196
- if (!binding) binding = loadNative();
197
- const nativeEngine = await binding.createEngine(resolvedOptions);
211
+ if (!nativeRuntime) nativeRuntime = loadNative();
212
+ const nativeEngine = await nativeRuntime.binding.createEngine(
213
+ resolvedOptions,
214
+ nativeRuntime.runtimePolicy,
215
+ );
198
216
  return new OcrEngineImpl(nativeEngine);
199
217
  } catch (error) {
200
218
  throw normalizeNativeError(error);
package/js/index.d.ts CHANGED
@@ -1,14 +1,30 @@
1
1
  /// <reference types="node" />
2
2
 
3
3
  export type PixelFormat = 'gray8' | 'rgb8' | 'bgr8' | 'rgba8';
4
- export type DetectionStrategy = 'bounded' | 'upstreamExact';
4
+ export type DetectionStrategy = 'bounded' | 'tiled' | 'upstreamExact';
5
5
  export type BuiltInModel = 'ppocrv6-small';
6
+ export type ExecutionProvider = 'auto' | 'cpu' | 'apple' | 'webgpu';
7
+ export type SessionFallback = 'error' | 'cpu';
8
+ export type CpuPartition = 'allow' | 'forbid';
9
+ export type PerformanceHint = 'latency' | 'throughput';
10
+ /** WebGPU accepts auto/fp32; fp16 is reserved for the Apple provider. */
11
+ export type Precision = 'auto' | 'fp32' | 'fp16';
6
12
 
7
13
  export interface DetectionOptions {
8
14
  readonly strategy?: DetectionStrategy;
9
15
  readonly maxSide?: number;
10
16
  }
11
17
 
18
+ export interface ExecutionOptions {
19
+ /** Only providers shipped and qualified by this release appear in this union. */
20
+ readonly provider?: ExecutionProvider;
21
+ readonly sessionFallback?: SessionFallback;
22
+ readonly cpuPartition?: CpuPartition;
23
+ readonly deviceId?: number;
24
+ readonly performanceHint?: PerformanceHint;
25
+ readonly precision?: Precision;
26
+ }
27
+
12
28
  export interface RawImage {
13
29
  readonly data: Uint8Array;
14
30
  readonly width: number;
@@ -23,6 +39,7 @@ export interface ResourceLimits {
23
39
  readonly maxPixels: number;
24
40
  readonly maxDetectionSide: number;
25
41
  readonly maxDetectionCandidates: number;
42
+ readonly maxDetectionTiles: number;
26
43
  readonly maxRecognitionBatchSize: number;
27
44
  readonly maxRecognitionWidth: number;
28
45
  readonly maxTemporaryBytes: number;
@@ -35,10 +52,14 @@ export interface CreateEngineOptions {
35
52
  readonly interOpThreads?: number;
36
53
  readonly recognitionScoreThreshold?: number;
37
54
  readonly recognitionBatchSize?: number;
38
- readonly reducedLimits?: ResourceLimits;
55
+ readonly reducedLimits?: Omit<ResourceLimits, 'maxDetectionTiles'> & {
56
+ /** Omission preserves the 0.1 reducedLimits source shape. */
57
+ readonly maxDetectionTiles?: number;
58
+ };
39
59
  readonly queueCapacity?: number;
40
60
  readonly maxPendingInputBytes?: number;
41
61
  readonly detection?: DetectionOptions;
62
+ readonly execution?: ExecutionOptions;
42
63
  }
43
64
 
44
65
  export interface RecognizeOptions {
@@ -63,6 +84,20 @@ export interface RecognitionBatchShape {
63
84
  readonly batchSize: number;
64
85
  readonly height: number;
65
86
  readonly width: number;
87
+ readonly computeUnit: 'cpu' | 'ane' | 'gpu';
88
+ readonly modelId: string;
89
+ readonly shapeBucket: string;
90
+ }
91
+ export interface DetectionPassShape {
92
+ readonly tileOrdinal: number;
93
+ readonly x: number;
94
+ readonly y: number;
95
+ readonly width: number;
96
+ readonly height: number;
97
+ readonly tensorWidth: number;
98
+ readonly tensorHeight: number;
99
+ readonly contourCandidates: number;
100
+ readonly rawCandidates: number;
66
101
  }
67
102
  export interface Diagnostics {
68
103
  readonly rejectedLines: readonly RejectedLine[];
@@ -71,14 +106,20 @@ export interface Diagnostics {
71
106
  readonly acceptedBoxes: number;
72
107
  readonly detectionInputWidth: number;
73
108
  readonly detectionInputHeight: number;
109
+ readonly rawDetectionBoxes: number;
110
+ readonly suppressedDuplicateBoxes: number;
111
+ readonly maxLiveDetectionPassBuffers: number;
112
+ readonly detectionPasses: readonly DetectionPassShape[];
74
113
  readonly recognitionBatchShapes: readonly RecognitionBatchShape[];
75
114
  }
76
115
  export interface TimingUs {
77
116
  readonly total: number;
117
+ readonly decode: number;
78
118
  readonly inputValidation: number;
79
119
  readonly detectionPreprocess: number;
80
120
  readonly detectionInference: number;
81
121
  readonly detectionPostprocess: number;
122
+ readonly detectionMerge: number;
82
123
  readonly cropAndSort: number;
83
124
  readonly recognitionPreprocess: number;
84
125
  readonly recognitionInference: number;
@@ -92,16 +133,89 @@ export interface OcrResult {
92
133
  readonly timingUs: TimingUs;
93
134
  readonly diagnostics?: Diagnostics;
94
135
  }
136
+ export interface TiledDetectionInfo {
137
+ readonly contractVersion: 'tiled-v1';
138
+ readonly tileSide: 1280;
139
+ readonly minimumOverlap: 128;
140
+ readonly artificialBoundaryMargin: 32;
141
+ readonly mergeIouThreshold: 0.5;
142
+ readonly mergeIosThreshold: 0.8;
143
+ }
144
+ export interface ProviderCapabilityInfo {
145
+ readonly provider: string;
146
+ readonly packageIncluded: boolean;
147
+ readonly deviceAvailable: boolean;
148
+ /** True only when this exact hardware family has reviewed qualification evidence. */
149
+ readonly deviceValidated: boolean;
150
+ }
151
+ export interface SessionExecutionInfo {
152
+ readonly requestedProvider: string;
153
+ readonly actualProviderChain: readonly string[];
154
+ readonly device: string;
155
+ readonly deviceFamily: string;
156
+ readonly operatingSystem: string;
157
+ readonly precision: string;
158
+ readonly shapePolicy: string;
159
+ readonly modelId: string;
160
+ readonly modelSha256: string;
161
+ readonly runtime: string;
162
+ readonly runtimeVersion: string;
163
+ readonly providerVersion: string;
164
+ readonly modelCacheStatus: string;
165
+ readonly qualificationId: string;
166
+ /** False means the open macOS compatibility path is experimental on this device. */
167
+ readonly deviceValidated: boolean;
168
+ readonly sessionFallback: boolean;
169
+ readonly fallbackReason?: string;
170
+ }
171
+ export type CreationReason =
172
+ | 'adapter_unavailable' | 'model_compute_unsupported'
173
+ | 'device_memory_insufficient' | 'driver_version_unsupported'
174
+ | 'package_corrupt' | 'artifact_hash_mismatch' | 'provider_abi_mismatch'
175
+ | 'internal_assertion_failed' | 'unrecoverable_load_failed';
176
+ export type CreationAttemptStatus = 'selected' | 'skipped' | 'fatal';
177
+ export interface CreationAttempt {
178
+ readonly provider: string;
179
+ readonly status: CreationAttemptStatus;
180
+ readonly creationReason?: CreationReason;
181
+ readonly errorCode?: CoreErrorCode;
182
+ }
183
+ export interface CreationTrace {
184
+ readonly requestedProvider: string;
185
+ readonly policyId?: string;
186
+ readonly policyVersion?: number;
187
+ readonly orderedCandidates: readonly string[];
188
+ readonly attempts: readonly CreationAttempt[];
189
+ readonly selectedProvider?: string;
190
+ }
191
+ export interface ExecutionInfo {
192
+ readonly requestedProvider: ExecutionProvider;
193
+ readonly sessionFallback: SessionFallback;
194
+ readonly cpuPartition: CpuPartition;
195
+ readonly deviceId?: number;
196
+ readonly performanceHint: PerformanceHint;
197
+ readonly requestedPrecision: Precision;
198
+ readonly providerCapabilities: readonly ProviderCapabilityInfo[];
199
+ readonly selectionTrace: CreationTrace;
200
+ readonly sessions: {
201
+ readonly detection: SessionExecutionInfo;
202
+ readonly recognition: SessionExecutionInfo;
203
+ };
204
+ }
95
205
  export interface EngineInfo {
96
206
  readonly coreVersion: string;
97
207
  readonly modelBundleId: string;
98
208
  readonly modelBundleSchemaVersion: string;
209
+ readonly normalizedConfigSchemaVersion: string;
99
210
  readonly backend: string;
211
+ /** @deprecated Use execution.sessions for stage-specific provider details. */
100
212
  readonly executionProvider: string;
213
+ readonly execution: ExecutionInfo;
101
214
  readonly capabilities: {
102
215
  readonly detection: boolean;
103
216
  readonly recognition: boolean;
104
217
  readonly textlineOrientation: boolean;
218
+ readonly tiledDetection: boolean;
105
219
  };
106
220
  readonly concurrencyMode: 'serialized_reject_when_busy';
107
221
  readonly limits: ResourceLimits & { readonly maxConcurrentCalls: 1 };
@@ -109,6 +223,7 @@ export interface EngineInfo {
109
223
  readonly interOpThreads: number;
110
224
  readonly detectionStrategy: DetectionStrategy;
111
225
  readonly detectionMaxSide: number;
226
+ readonly tiledDetection?: TiledDetectionInfo;
112
227
  readonly defaultRecognitionScoreThreshold: number;
113
228
  readonly defaultRecognitionBatchSize: number;
114
229
  readonly adapter: {
@@ -134,11 +249,13 @@ export class OcrError extends Error {
134
249
  readonly name: 'OcrError';
135
250
  readonly code: OcrErrorCode;
136
251
  readonly detail?: string;
252
+ readonly creationTrace?: CreationTrace;
137
253
  }
138
254
 
139
255
  export interface OcrEngine {
140
256
  readonly info: EngineInfo;
141
257
  recognize(image: RawImage, options?: RecognizeOptions): Promise<OcrResult>;
258
+ recognizeEncoded(data: Uint8Array, options?: RecognizeOptions): Promise<OcrResult>;
142
259
  close(): Promise<void>;
143
260
  }
144
261
 
@@ -1,26 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('node:crypto');
3
4
  const fs = require('node:fs');
4
5
  const path = require('node:path');
5
6
 
6
- function candidatePaths() {
7
- const candidates = [];
8
- if (process.env.LIGHT_OCR_NODE_BINARY) {
9
- candidates.push(path.resolve(process.env.LIGHT_OCR_NODE_BINARY));
10
- }
11
- candidates.push(path.join(__dirname, 'native', 'light_ocr_node.node'));
12
- candidates.push(
13
- path.join(
14
- __dirname,
15
- '..',
16
- 'prebuilds',
17
- `${process.platform}-${process.arch}`,
18
- 'light_ocr_node.node',
19
- ),
20
- );
21
- return candidates;
22
- }
23
-
24
7
  function adapterError(code, message, detail, cause) {
25
8
  const error = new Error(message, cause === undefined ? undefined : { cause });
26
9
  error.name = 'OcrError';
@@ -29,52 +12,474 @@ function adapterError(code, message, detail, cause) {
29
12
  return error;
30
13
  }
31
14
 
32
- function platformPackage() {
15
+ function platformIdentity() {
33
16
  const key = `${process.platform}-${process.arch}`;
34
- const packages = {
35
- 'darwin-arm64': '@arcships/light-ocr-darwin-arm64',
36
- 'darwin-x64': '@arcships/light-ocr-darwin-x64',
37
- 'win32-x64': '@arcships/light-ocr-win32-x64',
17
+ const identities = {
18
+ 'darwin-arm64': { id: 'macos-arm64', os: 'darwin', architecture: 'arm64' },
19
+ 'darwin-x64': { id: 'macos-x64', os: 'darwin', architecture: 'x86_64' },
20
+ 'win32-x64': { id: 'windows-x64', os: 'win32', architecture: 'x86_64' },
38
21
  };
39
22
  if (key === 'linux-x64') {
40
23
  const report = process.report?.getReport?.();
41
24
  if (report?.header?.glibcVersionRuntime) {
42
- return '@arcships/light-ocr-linux-x64-gnu';
25
+ return { id: 'linux-x64', os: 'linux', architecture: 'x86_64', libc: 'glibc' };
43
26
  }
44
27
  throw adapterError(
45
28
  'unsupported_platform',
46
29
  'light-ocr currently supports Linux x64 with glibc only',
47
- `${process.platform}-${process.arch}`,
48
- );
49
- }
50
- const packageName = packages[key];
51
- if (!packageName) {
52
- throw adapterError(
53
- 'unsupported_platform',
54
- `light-ocr does not support ${key}`,
55
30
  key,
56
31
  );
57
32
  }
58
- return packageName;
33
+ const identity = identities[key];
34
+ if (!identity) {
35
+ throw adapterError('unsupported_platform', `light-ocr does not support ${key}`, key);
36
+ }
37
+ return identity;
59
38
  }
60
39
 
61
- function loadNative() {
62
- const candidates = candidatePaths();
63
- const binary = candidates.find((candidate) => fs.existsSync(candidate));
64
- if (binary) return require(binary);
40
+ function platformPackage() {
41
+ const packages = {
42
+ 'macos-arm64': '@arcships/light-ocr-darwin-arm64',
43
+ 'macos-x64': '@arcships/light-ocr-darwin-x64',
44
+ 'windows-x64': '@arcships/light-ocr-win32-x64',
45
+ 'linux-x64': '@arcships/light-ocr-linux-x64-gnu',
46
+ };
47
+ return packages[platformIdentity().id];
48
+ }
49
+
50
+ function exactKeys(value, expected, field) {
51
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
52
+ throw adapterError('package_load_failed', `${field} must be an object`);
53
+ }
54
+ const actual = Object.keys(value).sort();
55
+ const wanted = [...expected].sort();
56
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
57
+ throw adapterError('package_load_failed', `${field} fields are invalid`);
58
+ }
59
+ }
60
+
61
+ function safeArtifactPath(root, value, field) {
62
+ if (typeof value !== 'string' || value === '' || value.includes('\0')) {
63
+ throw adapterError('package_load_failed', `${field} must be a package-relative path`);
64
+ }
65
+ const normalized = value.replaceAll('\\', '/');
66
+ if (
67
+ path.posix.isAbsolute(normalized) ||
68
+ /^[A-Za-z]:/.test(normalized) ||
69
+ normalized.split('/').some((part) => part === '..' || part === '' || part === '.')
70
+ ) {
71
+ throw adapterError('package_load_failed', `${field} escapes the native package`, value);
72
+ }
73
+ const resolved = path.resolve(root, ...normalized.split('/'));
74
+ const relative = path.relative(path.resolve(root), resolved);
75
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
76
+ throw adapterError('package_load_failed', `${field} escapes the native package`, value);
77
+ }
78
+ return resolved;
79
+ }
65
80
 
66
- const packageName = platformPackage();
81
+ function sha256(filename) {
82
+ return crypto.createHash('sha256').update(fs.readFileSync(filename)).digest('hex');
83
+ }
84
+
85
+ function verifyArtifact(root, artifact, field) {
86
+ exactKeys(artifact, ['path', 'bytes', 'sha256'], field);
87
+ const filename = safeArtifactPath(root, artifact.path, `${field}.path`);
88
+ let stats;
67
89
  try {
68
- return require(packageName);
90
+ stats = fs.lstatSync(filename);
69
91
  } catch (cause) {
92
+ throw adapterError('package_load_failed', 'Descriptor artifact is missing', artifact.path, cause);
93
+ }
94
+ if (!stats.isFile() || stats.isSymbolicLink()) {
95
+ throw adapterError('package_load_failed', 'Descriptor artifact is not a regular file', artifact.path);
96
+ }
97
+ if (!Number.isSafeInteger(artifact.bytes) || artifact.bytes < 1 || stats.size !== artifact.bytes) {
98
+ throw adapterError('package_load_failed', 'Descriptor artifact byte count mismatch', artifact.path);
99
+ }
100
+ if (!/^[a-f0-9]{64}$/.test(artifact.sha256 || '') || sha256(filename) !== artifact.sha256) {
101
+ throw adapterError('package_load_failed', 'Descriptor artifact hash mismatch', artifact.path);
102
+ }
103
+ return filename;
104
+ }
105
+
106
+ function sameArtifact(left, right) {
107
+ return left?.path === right?.path && left?.bytes === right?.bytes &&
108
+ left?.sha256 === right?.sha256;
109
+ }
110
+
111
+ function validateRuntimeDescriptor(descriptorPath) {
112
+ const absoluteDescriptor = path.resolve(descriptorPath);
113
+ const nativeDirectory = path.dirname(absoluteDescriptor);
114
+ if (
115
+ path.basename(absoluteDescriptor) !== 'runtime-descriptor.json' ||
116
+ path.basename(nativeDirectory) !== 'native'
117
+ ) {
70
118
  throw adapterError(
71
119
  'package_load_failed',
72
- `Unable to load ${packageName}`,
73
- 'Reinstall @arcships/light-ocr without --omit=optional and verify that the ' +
74
- 'current platform is supported.',
120
+ 'Native runtime descriptor must use the package native/runtime-descriptor.json path',
121
+ absoluteDescriptor,
122
+ );
123
+ }
124
+ let descriptor;
125
+ try {
126
+ const nativeStats = fs.lstatSync(nativeDirectory);
127
+ if (!nativeStats.isDirectory() || nativeStats.isSymbolicLink()) {
128
+ throw new Error('native payload root is not a regular directory');
129
+ }
130
+ const stats = fs.lstatSync(absoluteDescriptor);
131
+ if (!stats.isFile() || stats.isSymbolicLink()) throw new Error('not a regular file');
132
+ descriptor = JSON.parse(fs.readFileSync(absoluteDescriptor, 'utf8'));
133
+ } catch (cause) {
134
+ throw adapterError(
135
+ 'package_load_failed',
136
+ 'Unable to read the native runtime descriptor',
137
+ absoluteDescriptor,
75
138
  cause,
76
139
  );
77
140
  }
141
+ exactKeys(
142
+ descriptor,
143
+ ['schemaVersion', 'platform', 'runtime', 'qualificationOnly', 'released',
144
+ 'autoPolicy', 'providers', 'addon'],
145
+ 'runtime descriptor',
146
+ );
147
+ if (descriptor.schemaVersion !== '2.0') {
148
+ throw adapterError('package_load_failed', 'Unsupported native runtime descriptor schema');
149
+ }
150
+ const root = path.dirname(path.dirname(absoluteDescriptor));
151
+ const expected = platformIdentity();
152
+ const expectedPlatformKeys = expected.libc
153
+ ? ['id', 'os', 'architecture', 'libc']
154
+ : ['id', 'os', 'architecture'];
155
+ exactKeys(descriptor.platform, expectedPlatformKeys, 'platform');
156
+ const actual = descriptor.platform;
157
+ if (
158
+ actual.id !== expected.id ||
159
+ actual.os !== expected.os ||
160
+ actual.architecture !== expected.architecture ||
161
+ (expected.libc && actual.libc !== expected.libc)
162
+ ) {
163
+ throw adapterError('package_load_failed', 'Native runtime descriptor platform mismatch');
164
+ }
165
+ if (
166
+ typeof descriptor.released !== 'boolean' ||
167
+ typeof descriptor.qualificationOnly !== 'boolean' ||
168
+ descriptor.qualificationOnly === descriptor.released
169
+ ) {
170
+ throw adapterError('package_load_failed', 'Native runtime descriptor release flags are invalid');
171
+ }
172
+
173
+ exactKeys(descriptor.autoPolicy, ['id', 'version', 'providers'], 'autoPolicy');
174
+ const policy = descriptor.autoPolicy;
175
+ if (
176
+ typeof policy.id !== 'string' || policy.id === '' ||
177
+ !Number.isSafeInteger(policy.version) || policy.version < 1 || policy.version > 0xffffffff ||
178
+ !Array.isArray(policy.providers) || policy.providers.length === 0 ||
179
+ policy.providers.length > 3 || policy.providers.at(-1) !== 'cpu' ||
180
+ new Set(policy.providers).size !== policy.providers.length
181
+ ) {
182
+ throw adapterError('package_load_failed', 'Native runtime descriptor Auto policy is invalid');
183
+ }
184
+
185
+ exactKeys(descriptor.runtime, ['flavor', 'kind', 'version', 'abi', 'artifacts'], 'runtime');
186
+ const runtime = descriptor.runtime;
187
+ const expectedRuntimes = {
188
+ cpu: { kind: 'onnxruntime-cpu', version: '1.22.0', abi: 'onnxruntime-c-api-22' },
189
+ webgpu: {
190
+ kind: 'onnxruntime-plugin-webgpu',
191
+ version: '1.24.4',
192
+ abi: 'onnxruntime-c-api-24-plugin-ep-0.1',
193
+ },
194
+ };
195
+ const expectedRuntime = expectedRuntimes[runtime.flavor];
196
+ if (
197
+ !expectedRuntime || runtime.kind !== expectedRuntime.kind ||
198
+ runtime.version !== expectedRuntime.version || runtime.abi !== expectedRuntime.abi ||
199
+ !Array.isArray(runtime.artifacts) || runtime.artifacts.length === 0
200
+ ) {
201
+ throw adapterError('package_load_failed', 'Native runtime descriptor ABI identity is invalid');
202
+ }
203
+ if (runtime.flavor === 'webgpu' && !['linux', 'win32'].includes(actual.os)) {
204
+ throw adapterError('package_load_failed', 'WebGPU runtime is not supported on this platform');
205
+ }
206
+ if (runtime.flavor !== 'webgpu' && descriptor.qualificationOnly) {
207
+ throw adapterError('package_load_failed', 'CPU runtime cannot be qualification-only');
208
+ }
209
+
210
+ const addon = verifyArtifact(root, descriptor.addon, 'addon');
211
+ const runtimePaths = new Set();
212
+ const verifiedRuntime = new Map();
213
+ runtime.artifacts.forEach((artifact, index) => {
214
+ const filename = verifyArtifact(root, artifact, `runtime.artifacts[${index}]`);
215
+ if (runtimePaths.has(artifact.path)) {
216
+ throw adapterError('package_load_failed', 'Runtime artifact inventory contains a duplicate path');
217
+ }
218
+ runtimePaths.add(artifact.path);
219
+ verifiedRuntime.set(artifact.path, filename);
220
+ });
221
+
222
+ exactKeys(descriptor.providers, Object.keys(descriptor.providers), 'providers');
223
+ const availableProviders = Object.keys(descriptor.providers);
224
+ if (
225
+ availableProviders.length === 0 ||
226
+ new Set(availableProviders).size !== availableProviders.length ||
227
+ availableProviders.some((provider) => !['cpu', 'apple', 'webgpu'].includes(provider)) ||
228
+ !descriptor.providers.cpu
229
+ ) {
230
+ throw adapterError('package_load_failed', 'Native runtime descriptor provider policy is invalid');
231
+ }
232
+ let webgpuLibrary = '';
233
+ let webgpuProviderBytes = 0;
234
+ let webgpuProviderSha256 = '';
235
+ for (const [providerId, provider] of Object.entries(descriptor.providers)) {
236
+ const expectedKeys = providerId === 'webgpu'
237
+ ? ['runtimeProvider', 'providerVersion', 'qualificationId', 'providerLibrary', 'artifacts']
238
+ : ['runtimeProvider', 'qualificationId', 'artifacts'];
239
+ exactKeys(provider, expectedKeys, `providers.${providerId}`);
240
+ if (
241
+ typeof provider.qualificationId !== 'string' || provider.qualificationId === '' ||
242
+ !Array.isArray(provider.artifacts) || provider.artifacts.length === 0
243
+ ) {
244
+ throw adapterError('package_load_failed', `Provider ${providerId} identity is invalid`);
245
+ }
246
+ const expectedProviderName = {
247
+ cpu: 'CPUExecutionProvider',
248
+ apple: 'CoreML',
249
+ webgpu: 'WebGpuExecutionProvider',
250
+ }[providerId];
251
+ if (provider.runtimeProvider !== expectedProviderName) {
252
+ throw adapterError('package_load_failed', `Provider ${providerId} runtime identity is invalid`);
253
+ }
254
+ const providerPaths = new Set();
255
+ provider.artifacts.forEach((artifact, index) => {
256
+ verifyArtifact(root, artifact, `providers.${providerId}.artifacts[${index}]`);
257
+ if (providerPaths.has(artifact.path)) {
258
+ throw adapterError('package_load_failed', `Provider ${providerId} has duplicate artifacts`);
259
+ }
260
+ providerPaths.add(artifact.path);
261
+ if (providerId !== 'apple' && !runtimePaths.has(artifact.path)) {
262
+ throw adapterError('package_load_failed', `Provider ${providerId} artifact is outside runtime inventory`);
263
+ }
264
+ if (providerId === 'apple' && artifact.path !== descriptor.addon.path) {
265
+ throw adapterError('package_load_failed', 'Apple provider artifact must be the native addon');
266
+ }
267
+ });
268
+ if (providerId === 'webgpu') {
269
+ if (provider.providerVersion !== '0.1.0') {
270
+ throw adapterError('package_load_failed', 'WebGPU provider version is invalid');
271
+ }
272
+ webgpuLibrary = verifyArtifact(root, provider.providerLibrary, 'providers.webgpu.providerLibrary');
273
+ const declared = provider.artifacts.find(
274
+ (artifact) => artifact.path === provider.providerLibrary.path,
275
+ );
276
+ const expectedBasename = actual.os === 'win32'
277
+ ? 'onnxruntime_providers_webgpu.dll'
278
+ : 'libonnxruntime_providers_webgpu.so';
279
+ if (
280
+ !declared || !sameArtifact(declared, provider.providerLibrary) ||
281
+ path.basename(webgpuLibrary) !== expectedBasename
282
+ ) {
283
+ throw adapterError('package_load_failed', 'WebGPU provider library contract is invalid');
284
+ }
285
+ webgpuProviderBytes = provider.providerLibrary.bytes;
286
+ webgpuProviderSha256 = provider.providerLibrary.sha256;
287
+ }
288
+ }
289
+
290
+ const coreName = actual.os === 'win32'
291
+ ? 'onnxruntime.dll'
292
+ : actual.os === 'darwin'
293
+ ? 'libonnxruntime.1.22.0.dylib'
294
+ : 'libonnxruntime.so.1';
295
+ const runtimeNames = [...verifiedRuntime.values()].map((filename) => path.basename(filename)).sort();
296
+ const expectedRuntimeNames = runtime.flavor === 'webgpu'
297
+ ? actual.os === 'win32'
298
+ ? ['dxcompiler.dll', 'dxil.dll', 'onnxruntime.dll', 'onnxruntime_providers_webgpu.dll']
299
+ : ['libonnxruntime.so.1', 'libonnxruntime_providers_webgpu.so']
300
+ : [coreName];
301
+ if (
302
+ runtimeNames.length !== expectedRuntimeNames.length ||
303
+ runtimeNames.some((name, index) => name !== expectedRuntimeNames[index])
304
+ ) {
305
+ throw adapterError('package_load_failed', 'Native runtime artifact set is incomplete');
306
+ }
307
+ const cpuArtifacts = descriptor.providers.cpu.artifacts;
308
+ if (
309
+ cpuArtifacts.length !== 1 ||
310
+ path.basename(safeArtifactPath(root, cpuArtifacts[0].path, 'CPU artifact path')) !== coreName
311
+ ) {
312
+ throw adapterError('package_load_failed', 'CPU provider does not reference the core runtime');
313
+ }
314
+
315
+ const actualPayload = new Set();
316
+ const inventory = (directory, relativeDirectory) => {
317
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
318
+ const relative = `${relativeDirectory}/${entry.name}`;
319
+ const filename = path.join(directory, entry.name);
320
+ if (entry.isSymbolicLink()) {
321
+ throw adapterError('package_load_failed', 'Native runtime payload contains a symlink', relative);
322
+ }
323
+ if (entry.isDirectory()) {
324
+ inventory(filename, relative);
325
+ } else if (entry.isFile() && relative !== 'native/runtime-descriptor.json') {
326
+ actualPayload.add(relative);
327
+ } else if (!entry.isFile()) {
328
+ throw adapterError('package_load_failed', 'Native runtime payload is not a regular file', relative);
329
+ }
330
+ }
331
+ };
332
+ inventory(nativeDirectory, 'native');
333
+ const referenced = new Set([descriptor.addon.path, ...runtimePaths]);
334
+ if (
335
+ actualPayload.size !== referenced.size ||
336
+ [...actualPayload].some((filename) => !referenced.has(filename))
337
+ ) {
338
+ throw adapterError('package_load_failed', 'Native runtime descriptor payload inventory mismatch');
339
+ }
340
+
341
+ const expectedPolicy = runtime.flavor === 'webgpu'
342
+ ? ['webgpu', 'cpu']
343
+ : actual.id === 'macos-arm64'
344
+ ? ['apple', 'cpu']
345
+ : ['cpu'];
346
+ const expectedAvailable = runtime.flavor === 'webgpu'
347
+ ? ['cpu', 'webgpu']
348
+ : actual.id === 'macos-arm64'
349
+ ? ['apple', 'cpu']
350
+ : ['cpu'];
351
+ const sortedAvailable = [...availableProviders].sort();
352
+ if (
353
+ policy.providers.length !== expectedPolicy.length ||
354
+ policy.providers.some((provider, index) => provider !== expectedPolicy[index]) ||
355
+ sortedAvailable.length !== expectedAvailable.length ||
356
+ sortedAvailable.some((provider, index) => provider !== expectedAvailable[index]) ||
357
+ policy.providers.some((provider) => !availableProviders.includes(provider))
358
+ ) {
359
+ throw adapterError(
360
+ 'package_load_failed',
361
+ 'Native runtime descriptor providers disagree with platform capabilities',
362
+ );
363
+ }
364
+ const providerQualificationIds = sortedAvailable.map(
365
+ (providerId) => descriptor.providers[providerId].qualificationId,
366
+ );
367
+ const runtimePolicy = Object.freeze({
368
+ id: policy.id,
369
+ version: policy.version,
370
+ platformId: actual.id,
371
+ runtimeFlavor: runtime.flavor,
372
+ runtimeVersion: runtime.version,
373
+ runtimeAbi: runtime.abi,
374
+ qualificationOnly: descriptor.qualificationOnly,
375
+ released: descriptor.released,
376
+ orderedCandidates: Object.freeze([...policy.providers]),
377
+ availableProviders: Object.freeze(sortedAvailable),
378
+ providerQualificationIds: Object.freeze(providerQualificationIds),
379
+ webgpuProviderLibrary: webgpuLibrary,
380
+ webgpuProviderBytes,
381
+ webgpuProviderSha256,
382
+ });
383
+ return {
384
+ addon,
385
+ descriptor: Object.freeze(descriptor),
386
+ descriptorPath: absoluteDescriptor,
387
+ runtimePolicy,
388
+ };
389
+ }
390
+
391
+ function resolveDevelopmentInput() {
392
+ if (!process.env.LIGHT_OCR_NODE_BINARY) return undefined;
393
+ const binary = path.resolve(process.env.LIGHT_OCR_NODE_BINARY);
394
+ const descriptor = process.env.LIGHT_OCR_RUNTIME_DESCRIPTOR
395
+ ? path.resolve(process.env.LIGHT_OCR_RUNTIME_DESCRIPTOR)
396
+ : path.join(path.dirname(binary), 'runtime-descriptor.json');
397
+ return { binary, descriptor };
398
+ }
399
+
400
+ function validateNativeContract(binding, runtimePolicy) {
401
+ const contract = binding?.runtimeContract;
402
+ const fields = [
403
+ 'policyId',
404
+ 'policyVersion',
405
+ 'platformId',
406
+ 'runtimeFlavor',
407
+ 'runtimeVersion',
408
+ 'runtimeAbi',
409
+ 'qualificationOnly',
410
+ 'released',
411
+ ];
412
+ const policyFields = {
413
+ policyId: runtimePolicy.id,
414
+ policyVersion: runtimePolicy.version,
415
+ platformId: runtimePolicy.platformId,
416
+ runtimeFlavor: runtimePolicy.runtimeFlavor,
417
+ runtimeVersion: runtimePolicy.runtimeVersion,
418
+ runtimeAbi: runtimePolicy.runtimeAbi,
419
+ qualificationOnly: runtimePolicy.qualificationOnly,
420
+ released: runtimePolicy.released,
421
+ };
422
+ if (
423
+ !contract ||
424
+ typeof contract !== 'object' ||
425
+ fields.some((field) => contract[field] !== policyFields[field]) ||
426
+ !Array.isArray(contract.orderedCandidates) ||
427
+ !Array.isArray(contract.availableProviders) ||
428
+ !Array.isArray(contract.providerQualificationIds) ||
429
+ contract.orderedCandidates.length !== runtimePolicy.orderedCandidates.length ||
430
+ contract.orderedCandidates.some(
431
+ (provider, index) => provider !== runtimePolicy.orderedCandidates[index],
432
+ ) ||
433
+ contract.availableProviders.length !== runtimePolicy.availableProviders.length ||
434
+ contract.availableProviders.some(
435
+ (provider, index) => provider !== runtimePolicy.availableProviders[index],
436
+ ) ||
437
+ contract.providerQualificationIds.length !== runtimePolicy.providerQualificationIds.length ||
438
+ contract.providerQualificationIds.some(
439
+ (qualificationId, index) => qualificationId !== runtimePolicy.providerQualificationIds[index],
440
+ )
441
+ ) {
442
+ throw adapterError(
443
+ 'package_load_failed',
444
+ 'Runtime descriptor is incompatible with the native addon ABI or capabilities',
445
+ );
446
+ }
447
+ }
448
+
449
+ function loadNative() {
450
+ const development = resolveDevelopmentInput();
451
+ let input;
452
+ if (development) {
453
+ input = development;
454
+ } else {
455
+ const packageName = platformPackage();
456
+ try {
457
+ const binary = require.resolve(packageName);
458
+ input = { binary, descriptor: path.join(path.dirname(binary), 'runtime-descriptor.json') };
459
+ } catch (cause) {
460
+ throw adapterError(
461
+ 'package_load_failed',
462
+ `Unable to locate ${packageName}`,
463
+ 'Reinstall @arcships/light-ocr without --omit=optional and verify that the current platform is supported.',
464
+ cause,
465
+ );
466
+ }
467
+ }
468
+
469
+ if (!fs.existsSync(input.binary)) {
470
+ throw adapterError('package_load_failed', 'Native addon is missing', input.binary);
471
+ }
472
+ const verified = validateRuntimeDescriptor(input.descriptor);
473
+ if (path.resolve(input.binary) !== path.resolve(verified.addon)) {
474
+ throw adapterError('package_load_failed', 'Runtime descriptor addon path mismatch', input.binary);
475
+ }
476
+ try {
477
+ const binding = require(verified.addon);
478
+ validateNativeContract(binding, verified.runtimePolicy);
479
+ return Object.freeze({ binding, runtimePolicy: verified.runtimePolicy });
480
+ } catch (cause) {
481
+ throw adapterError('package_load_failed', 'Unable to load the verified native addon', '', cause);
482
+ }
78
483
  }
79
484
 
80
- module.exports = { loadNative };
485
+ module.exports = { loadNative, validateRuntimeDescriptor };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/arcships/light-ocr/issues"
4
4
  },
5
5
  "dependencies": {
6
- "@arcships/light-ocr-model-ppocrv6-small": "0.1.0"
6
+ "@arcships/light-ocr-model-ppocrv6-small": "0.3.0"
7
7
  },
8
8
  "description": "Offline PP-OCRv6 OCR for Node.js, powered by an embeddable C++ core",
9
9
  "engines": {
@@ -36,10 +36,10 @@
36
36
  "module": "./js/index.mjs",
37
37
  "name": "@arcships/light-ocr",
38
38
  "optionalDependencies": {
39
- "@arcships/light-ocr-darwin-arm64": "0.1.0",
40
- "@arcships/light-ocr-darwin-x64": "0.1.0",
41
- "@arcships/light-ocr-linux-x64-gnu": "0.1.0",
42
- "@arcships/light-ocr-win32-x64": "0.1.0"
39
+ "@arcships/light-ocr-darwin-arm64": "0.3.0",
40
+ "@arcships/light-ocr-darwin-x64": "0.3.0",
41
+ "@arcships/light-ocr-linux-x64-gnu": "0.3.0",
42
+ "@arcships/light-ocr-win32-x64": "0.3.0"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public",
@@ -51,5 +51,5 @@
51
51
  },
52
52
  "type": "commonjs",
53
53
  "types": "./js/index.d.ts",
54
- "version": "0.1.0"
54
+ "version": "0.3.0"
55
55
  }