@arkyc/ocr 1.0.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/dist/drivers/ai.d.mts +120 -0
- package/dist/drivers/ai.d.mts.map +1 -0
- package/dist/drivers/ai.mjs +454 -0
- package/dist/drivers/ai.mjs.map +1 -0
- package/dist/drivers/external.d.mts +17 -0
- package/dist/drivers/external.d.mts.map +1 -0
- package/dist/drivers/external.mjs +34 -0
- package/dist/drivers/external.mjs.map +1 -0
- package/dist/drivers/mock.d.mts +16 -0
- package/dist/drivers/mock.d.mts.map +1 -0
- package/dist/drivers/mock.mjs +34 -0
- package/dist/drivers/mock.mjs.map +1 -0
- package/dist/drivers/preprocess.d.mts +51 -0
- package/dist/drivers/preprocess.d.mts.map +1 -0
- package/dist/drivers/preprocess.mjs +50 -0
- package/dist/drivers/preprocess.mjs.map +1 -0
- package/dist/drivers/tesseract.d.mts +75 -0
- package/dist/drivers/tesseract.d.mts.map +1 -0
- package/dist/drivers/tesseract.mjs +175 -0
- package/dist/drivers/tesseract.mjs.map +1 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.mjs +10 -0
- package/dist/parsers/generic.d.mts +8 -0
- package/dist/parsers/generic.d.mts.map +1 -0
- package/dist/parsers/generic.mjs +84 -0
- package/dist/parsers/generic.mjs.map +1 -0
- package/dist/parsers/mrz.d.mts +8 -0
- package/dist/parsers/mrz.d.mts.map +1 -0
- package/dist/parsers/mrz.mjs +149 -0
- package/dist/parsers/mrz.mjs.map +1 -0
- package/dist/parsers/registry.d.mts +49 -0
- package/dist/parsers/registry.d.mts.map +1 -0
- package/dist/parsers/registry.mjs +100 -0
- package/dist/parsers/registry.mjs.map +1 -0
- package/dist/parsers/types.d.mts +43 -0
- package/dist/parsers/types.d.mts.map +1 -0
- package/dist/registry.d.mts +20 -0
- package/dist/registry.d.mts.map +1 -0
- package/dist/registry.mjs +36 -0
- package/dist/registry.mjs.map +1 -0
- package/dist/types.d.mts +48 -0
- package/dist/types.d.mts.map +1 -0
- package/package.json +32 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/drivers/external.ts
|
|
2
|
+
/**
|
|
3
|
+
* Generic HTTP OCR driver: POSTs the base64 image to a configured endpoint and
|
|
4
|
+
* expects an {@link OcrResultData}-shaped JSON response.
|
|
5
|
+
*/
|
|
6
|
+
var ExternalOcrDriver = class {
|
|
7
|
+
config;
|
|
8
|
+
name = "external";
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
if (!config.endpoint) throw new Error("ExternalOcrDriver requires config.endpoint");
|
|
12
|
+
}
|
|
13
|
+
async extract(request) {
|
|
14
|
+
const res = await fetch(this.config.endpoint, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: {
|
|
17
|
+
"content-type": "application/json",
|
|
18
|
+
...this.config.apiKey ? { authorization: `Bearer ${this.config.apiKey}` } : {}
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
image: Buffer.from(request.image).toString("base64"),
|
|
22
|
+
backImage: request.backImage?.length ? Buffer.from(request.backImage).toString("base64") : null,
|
|
23
|
+
documentType: request.documentType ?? null,
|
|
24
|
+
country: request.country ?? null
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) throw new Error(`ExternalOcrDriver request failed with status ${res.status}`);
|
|
28
|
+
return await res.json();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { ExternalOcrDriver };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=external.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.mjs","names":[],"sources":["../../src/drivers/external.ts"],"sourcesContent":["import type { OcrResultData } from '@arkyc/types'\nimport type { OcrConfig, OcrDriver, OcrRequest } from '../types'\n\n/**\n * Generic HTTP OCR driver: POSTs the base64 image to a configured endpoint and\n * expects an {@link OcrResultData}-shaped JSON response.\n */\nexport class ExternalOcrDriver implements OcrDriver {\n readonly name = 'external'\n\n constructor(private readonly config: OcrConfig) {\n if (!config.endpoint) throw new Error('ExternalOcrDriver requires config.endpoint')\n }\n\n async extract(request: OcrRequest): Promise<OcrResultData> {\n const res = await fetch(this.config.endpoint as string, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(this.config.apiKey ? { authorization: `Bearer ${this.config.apiKey}` } : {}),\n },\n body: JSON.stringify({\n image: Buffer.from(request.image).toString('base64'),\n backImage: request.backImage?.length ? Buffer.from(request.backImage).toString('base64') : null,\n documentType: request.documentType ?? null,\n country: request.country ?? null,\n }),\n })\n\n if (!res.ok) {\n throw new Error(`ExternalOcrDriver request failed with status ${res.status}`)\n }\n\n return (await res.json()) as OcrResultData\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,oBAAb,MAAoD;CAGrB;CAF7B,OAAgB;CAEhB,YAAY,QAAoC;EAAnB,KAAA,SAAA;EAC3B,IAAI,CAAC,OAAO,UAAU,MAAM,IAAI,MAAM,4CAA4C;CACpF;CAEA,MAAM,QAAQ,SAA6C;EACzD,MAAM,MAAM,MAAM,MAAM,KAAK,OAAO,UAAoB;GACtD,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,GAAI,KAAK,OAAO,SAAS,EAAE,eAAe,UAAU,KAAK,OAAO,SAAS,IAAI,CAAC;GAChF;GACA,MAAM,KAAK,UAAU;IACnB,OAAO,OAAO,KAAK,QAAQ,KAAK,CAAC,CAAC,SAAS,QAAQ;IACnD,WAAW,QAAQ,WAAW,SAAS,OAAO,KAAK,QAAQ,SAAS,CAAC,CAAC,SAAS,QAAQ,IAAI;IAC3F,cAAc,QAAQ,gBAAgB;IACtC,SAAS,QAAQ,WAAW;GAC9B,CAAC;EACH,CAAC;EAED,IAAI,CAAC,IAAI,IACP,MAAM,IAAI,MAAM,gDAAgD,IAAI,QAAQ;EAG9E,OAAQ,MAAM,IAAI,KAAK;CACzB;AACF"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { OcrDriver, OcrRequest } from "../types.mjs";
|
|
2
|
+
import { OcrResultData } from "@arkyc/types";
|
|
3
|
+
|
|
4
|
+
//#region src/drivers/mock.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic OCR driver for development + tests. Returns fixed identity
|
|
7
|
+
* fields; `hints` steer the confidence and expiry so a caller can drive a
|
|
8
|
+
* session toward any decision.
|
|
9
|
+
*/
|
|
10
|
+
declare class MockOcrDriver implements OcrDriver {
|
|
11
|
+
readonly name = "mock";
|
|
12
|
+
extract(request: OcrRequest): Promise<OcrResultData>;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { MockOcrDriver };
|
|
16
|
+
//# sourceMappingURL=mock.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.d.mts","names":[],"sources":["../../src/drivers/mock.ts"],"mappings":";;;;;;AAUA;;;cAAa,aAAA,YAAyB,SAAA;EAAA,SAC3B,IAAA;EAEH,OAAA,CAAQ,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,aAAA;AAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/drivers/mock.ts
|
|
2
|
+
const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic OCR driver for development + tests. Returns fixed identity
|
|
5
|
+
* fields; `hints` steer the confidence and expiry so a caller can drive a
|
|
6
|
+
* session toward any decision.
|
|
7
|
+
*/
|
|
8
|
+
var MockOcrDriver = class {
|
|
9
|
+
name = "mock";
|
|
10
|
+
async extract(request) {
|
|
11
|
+
const confidence = clamp01(request.hints?.confidence ?? .92);
|
|
12
|
+
return {
|
|
13
|
+
fields: {
|
|
14
|
+
firstName: "Ada",
|
|
15
|
+
lastName: "Lovelace",
|
|
16
|
+
fullName: "Ada Lovelace",
|
|
17
|
+
dateOfBirth: "1990-01-01",
|
|
18
|
+
documentNumber: "X1234567",
|
|
19
|
+
expiryDate: request.hints?.expired ? "2000-01-01" : "2035-01-01",
|
|
20
|
+
nationality: request.country ?? "GB"
|
|
21
|
+
},
|
|
22
|
+
confidence,
|
|
23
|
+
raw: {
|
|
24
|
+
provider: "mock",
|
|
25
|
+
confidence,
|
|
26
|
+
documentType: request.documentType ?? null
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { MockOcrDriver };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=mock.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.mjs","names":[],"sources":["../../src/drivers/mock.ts"],"sourcesContent":["import type { OcrResultData } from '@arkyc/types'\nimport type { OcrDriver, OcrRequest } from '../types'\n\nconst clamp01 = (n: number): number => Math.min(1, Math.max(0, n))\n\n/**\n * Deterministic OCR driver for development + tests. Returns fixed identity\n * fields; `hints` steer the confidence and expiry so a caller can drive a\n * session toward any decision.\n */\nexport class MockOcrDriver implements OcrDriver {\n readonly name = 'mock'\n\n async extract(request: OcrRequest): Promise<OcrResultData> {\n const confidence = clamp01(request.hints?.confidence ?? 0.92)\n\n return {\n fields: {\n firstName: 'Ada',\n lastName: 'Lovelace',\n fullName: 'Ada Lovelace',\n dateOfBirth: '1990-01-01',\n documentNumber: 'X1234567',\n expiryDate: request.hints?.expired ? '2000-01-01' : '2035-01-01',\n nationality: request.country ?? 'GB',\n },\n confidence,\n raw: { provider: 'mock', confidence, documentType: request.documentType ?? null },\n }\n }\n}\n"],"mappings":";AAGA,MAAM,WAAW,MAAsB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;;;;;;AAOjE,IAAa,gBAAb,MAAgD;CAC9C,OAAgB;CAEhB,MAAM,QAAQ,SAA6C;EACzD,MAAM,aAAa,QAAQ,QAAQ,OAAO,cAAc,GAAI;EAE5D,OAAO;GACL,QAAQ;IACN,WAAW;IACX,UAAU;IACV,UAAU;IACV,aAAa;IACb,gBAAgB;IAChB,YAAY,QAAQ,OAAO,UAAU,eAAe;IACpD,aAAa,QAAQ,WAAW;GAClC;GACA;GACA,KAAK;IAAE,UAAU;IAAQ;IAAY,cAAc,QAAQ,gBAAgB;GAAK;EAClF;CACF;AACF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/drivers/preprocess.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Transforms raw image bytes to improve OCR legibility. Returns the (possibly
|
|
4
|
+
* unchanged) bytes — a preprocessor must never throw; on failure it returns the
|
|
5
|
+
* input so recognition still runs.
|
|
6
|
+
*/
|
|
7
|
+
type OcrPreprocessor = (image: Uint8Array) => Promise<Uint8Array>;
|
|
8
|
+
/** Tunables for the default {@link sharpPreprocessor}. */
|
|
9
|
+
interface PreprocessOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Upscale images narrower than this (px) to give the engine more pixels per
|
|
12
|
+
* glyph — the single biggest win for the small OCR-B MRZ band. Default 1600.
|
|
13
|
+
*/
|
|
14
|
+
minWidth?: number;
|
|
15
|
+
}
|
|
16
|
+
/** A no-op preprocessor: passes the original bytes through unchanged. */
|
|
17
|
+
declare const passthrough: OcrPreprocessor;
|
|
18
|
+
/** Minimal structural type for the lazily-imported `sharp` module. */
|
|
19
|
+
interface SharpInstance {
|
|
20
|
+
metadata(): Promise<{
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
}>;
|
|
24
|
+
grayscale(): SharpInstance;
|
|
25
|
+
normalise(): SharpInstance;
|
|
26
|
+
median(size: number): SharpInstance;
|
|
27
|
+
sharpen(): SharpInstance;
|
|
28
|
+
resize(options: {
|
|
29
|
+
width: number;
|
|
30
|
+
withoutEnlargement: boolean;
|
|
31
|
+
}): SharpInstance;
|
|
32
|
+
png(): SharpInstance;
|
|
33
|
+
toBuffer(): Promise<Buffer>;
|
|
34
|
+
}
|
|
35
|
+
type SharpFactory = (input: Buffer) => SharpInstance;
|
|
36
|
+
/**
|
|
37
|
+
* Build a preprocessor over an injected `sharp` factory: grayscale → contrast
|
|
38
|
+
* normalise → light denoise → sharpen → upscale small images. This lifts faint
|
|
39
|
+
* document text and the OCR-B MRZ band without the aggressive binarisation that
|
|
40
|
+
* destroys detail under uneven lighting. Any failure falls back to the original.
|
|
41
|
+
*/
|
|
42
|
+
declare function buildSharpPreprocessor(sharp: SharpFactory, options?: PreprocessOptions): OcrPreprocessor;
|
|
43
|
+
/**
|
|
44
|
+
* The default preprocessor, backed by the optional `sharp` package and resolved
|
|
45
|
+
* once. If `sharp` isn't installed it degrades to {@link passthrough}, so OCR
|
|
46
|
+
* still runs on the raw bytes (just without the legibility boost).
|
|
47
|
+
*/
|
|
48
|
+
declare function defaultPreprocessor(options?: PreprocessOptions): Promise<OcrPreprocessor>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { OcrPreprocessor, PreprocessOptions, buildSharpPreprocessor, defaultPreprocessor, passthrough };
|
|
51
|
+
//# sourceMappingURL=preprocess.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preprocess.d.mts","names":[],"sources":["../../src/drivers/preprocess.ts"],"mappings":";;AAKA;;;;KAAY,eAAA,IAAmB,KAAA,EAAO,UAAA,KAAe,OAAA,CAAQ,UAAA;;UAG5C,iBAAA;EAH2C;;;;EAQ1D,QAAQ;AAAA;AAR6D;AAAA,cAY1D,WAAA,EAAa,eAAwC;;UAGxD,aAAA;EACR,QAAA,IAAY,OAAA;IAAU,KAAA;IAAgB,MAAA;EAAA;EACtC,SAAA,IAAa,aAAA;EACb,SAAA,IAAa,aAAA;EACb,MAAA,CAAO,IAAA,WAAe,aAAA;EACtB,OAAA,IAAW,aAAA;EACX,MAAA,CAAO,OAAA;IAAW,KAAA;IAAe,kBAAA;EAAA,IAAgC,aAAA;EACjE,GAAA,IAAO,aAAA;EACP,QAAA,IAAY,OAAA,CAAQ,MAAA;AAAA;AAAA,KAEjB,YAAA,IAAgB,KAAA,EAAO,MAAA,KAAW,aAAa;;;;;;;iBAQpC,sBAAA,CAAuB,KAAA,EAAO,YAAA,EAAc,OAAA,GAAS,iBAAA,GAAyB,eAAA;;;;;;iBA0BxE,mBAAA,CAAoB,OAAA,GAAS,iBAAA,GAAyB,OAAA,CAAQ,eAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/drivers/preprocess.ts
|
|
2
|
+
/** A no-op preprocessor: passes the original bytes through unchanged. */
|
|
3
|
+
const passthrough = async (image) => image;
|
|
4
|
+
/**
|
|
5
|
+
* Build a preprocessor over an injected `sharp` factory: grayscale → contrast
|
|
6
|
+
* normalise → light denoise → sharpen → upscale small images. This lifts faint
|
|
7
|
+
* document text and the OCR-B MRZ band without the aggressive binarisation that
|
|
8
|
+
* destroys detail under uneven lighting. Any failure falls back to the original.
|
|
9
|
+
*/
|
|
10
|
+
function buildSharpPreprocessor(sharp, options = {}) {
|
|
11
|
+
const minWidth = options.minWidth ?? 1600;
|
|
12
|
+
return async (image) => {
|
|
13
|
+
try {
|
|
14
|
+
const input = Buffer.from(image);
|
|
15
|
+
const { width } = await sharp(input).metadata();
|
|
16
|
+
let pipe = sharp(input).grayscale().normalise().median(1).sharpen();
|
|
17
|
+
if (width && width < minWidth) pipe = pipe.resize({
|
|
18
|
+
width: minWidth,
|
|
19
|
+
withoutEnlargement: false
|
|
20
|
+
});
|
|
21
|
+
const out = await pipe.png().toBuffer();
|
|
22
|
+
return new Uint8Array(out);
|
|
23
|
+
} catch {
|
|
24
|
+
return image;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
let cached;
|
|
29
|
+
/**
|
|
30
|
+
* The default preprocessor, backed by the optional `sharp` package and resolved
|
|
31
|
+
* once. If `sharp` isn't installed it degrades to {@link passthrough}, so OCR
|
|
32
|
+
* still runs on the raw bytes (just without the legibility boost).
|
|
33
|
+
*/
|
|
34
|
+
async function defaultPreprocessor(options = {}) {
|
|
35
|
+
if (cached) return cached;
|
|
36
|
+
const moduleName = "sharp";
|
|
37
|
+
try {
|
|
38
|
+
cached = buildSharpPreprocessor((await import(
|
|
39
|
+
/* @vite-ignore */
|
|
40
|
+
moduleName
|
|
41
|
+
)).default, options);
|
|
42
|
+
} catch {
|
|
43
|
+
cached = passthrough;
|
|
44
|
+
}
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { buildSharpPreprocessor, defaultPreprocessor, passthrough };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=preprocess.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preprocess.mjs","names":[],"sources":["../../src/drivers/preprocess.ts"],"sourcesContent":["/**\n * Transforms raw image bytes to improve OCR legibility. Returns the (possibly\n * unchanged) bytes — a preprocessor must never throw; on failure it returns the\n * input so recognition still runs.\n */\nexport type OcrPreprocessor = (image: Uint8Array) => Promise<Uint8Array>\n\n/** Tunables for the default {@link sharpPreprocessor}. */\nexport interface PreprocessOptions {\n /**\n * Upscale images narrower than this (px) to give the engine more pixels per\n * glyph — the single biggest win for the small OCR-B MRZ band. Default 1600.\n */\n minWidth?: number\n}\n\n/** A no-op preprocessor: passes the original bytes through unchanged. */\nexport const passthrough: OcrPreprocessor = async (image) => image\n\n/** Minimal structural type for the lazily-imported `sharp` module. */\ninterface SharpInstance {\n metadata(): Promise<{ width?: number; height?: number }>\n grayscale(): SharpInstance\n normalise(): SharpInstance\n median(size: number): SharpInstance\n sharpen(): SharpInstance\n resize(options: { width: number; withoutEnlargement: boolean }): SharpInstance\n png(): SharpInstance\n toBuffer(): Promise<Buffer>\n}\ntype SharpFactory = (input: Buffer) => SharpInstance\n\n/**\n * Build a preprocessor over an injected `sharp` factory: grayscale → contrast\n * normalise → light denoise → sharpen → upscale small images. This lifts faint\n * document text and the OCR-B MRZ band without the aggressive binarisation that\n * destroys detail under uneven lighting. Any failure falls back to the original.\n */\nexport function buildSharpPreprocessor(sharp: SharpFactory, options: PreprocessOptions = {}): OcrPreprocessor {\n const minWidth = options.minWidth ?? 1600\n return async (image) => {\n try {\n const input = Buffer.from(image)\n const base = sharp(input)\n const { width } = await base.metadata()\n let pipe = sharp(input).grayscale().normalise().median(1).sharpen()\n if (width && width < minWidth) {\n pipe = pipe.resize({ width: minWidth, withoutEnlargement: false })\n }\n const out = await pipe.png().toBuffer()\n return new Uint8Array(out)\n } catch {\n return image\n }\n }\n}\n\nlet cached: OcrPreprocessor | undefined\n\n/**\n * The default preprocessor, backed by the optional `sharp` package and resolved\n * once. If `sharp` isn't installed it degrades to {@link passthrough}, so OCR\n * still runs on the raw bytes (just without the legibility boost).\n */\nexport async function defaultPreprocessor(options: PreprocessOptions = {}): Promise<OcrPreprocessor> {\n if (cached) return cached\n const moduleName = 'sharp'\n try {\n const mod = (await import(/* @vite-ignore */ moduleName)) as unknown as { default: SharpFactory }\n cached = buildSharpPreprocessor(mod.default, options)\n } catch {\n cached = passthrough\n }\n return cached\n}\n"],"mappings":";;AAiBA,MAAa,cAA+B,OAAO,UAAU;;;;;;;AAqB7D,SAAgB,uBAAuB,OAAqB,UAA6B,CAAC,GAAoB;CAC5G,MAAM,WAAW,QAAQ,YAAY;CACrC,OAAO,OAAO,UAAU;EACtB,IAAI;GACF,MAAM,QAAQ,OAAO,KAAK,KAAK;GAE/B,MAAM,EAAE,UAAU,MADL,MAAM,KACQ,CAAC,CAAC,SAAS;GACtC,IAAI,OAAO,MAAM,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ;GAClE,IAAI,SAAS,QAAQ,UACnB,OAAO,KAAK,OAAO;IAAE,OAAO;IAAU,oBAAoB;GAAM,CAAC;GAEnE,MAAM,MAAM,MAAM,KAAK,IAAI,CAAC,CAAC,SAAS;GACtC,OAAO,IAAI,WAAW,GAAG;EAC3B,QAAQ;GACN,OAAO;EACT;CACF;AACF;AAEA,IAAI;;;;;;AAOJ,eAAsB,oBAAoB,UAA6B,CAAC,GAA6B;CACnG,IAAI,QAAQ,OAAO;CACnB,MAAM,aAAa;CACnB,IAAI;EAEF,SAAS,wBAAuB,MADb;;GAA0B;GACT,SAAS,OAAO;CACtD,QAAQ;EACN,SAAS;CACX;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { OcrDriver, OcrRequest } from "../types.mjs";
|
|
2
|
+
import { DocumentParserRegistry } from "../parsers/registry.mjs";
|
|
3
|
+
import { OcrPreprocessor } from "./preprocess.mjs";
|
|
4
|
+
import { OcrResultData } from "@arkyc/types";
|
|
5
|
+
|
|
6
|
+
//#region src/drivers/tesseract.d.ts
|
|
7
|
+
/** Options for a single recognition pass. */
|
|
8
|
+
interface RecognizeOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Constrain recognition to the {@link MRZ_CHARSET}. This forces ambiguous OCR-B
|
|
11
|
+
* glyphs into the machine-readable zone's alphabet (e.g. `O`→`0`, `I`→`1`),
|
|
12
|
+
* dramatically improving the numeric MRZ lines (dates, check digits).
|
|
13
|
+
*/
|
|
14
|
+
mrz?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Reads text from an image; returns text + an engine confidence in [0, 100]. */
|
|
17
|
+
type TesseractRecognize = (image: Uint8Array, language: string, options?: RecognizeOptions) => Promise<{
|
|
18
|
+
text: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
}>;
|
|
21
|
+
interface TesseractOcrOptions {
|
|
22
|
+
/** Recognition language(s), default `eng`. */
|
|
23
|
+
language?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Parser registry used to turn recognized text into fields. Defaults to the
|
|
26
|
+
* MRZ-backed registry; pass one with your country/type parsers registered.
|
|
27
|
+
*/
|
|
28
|
+
registry?: DocumentParserRegistry;
|
|
29
|
+
/** Injectable recognizer (tests); defaults to a lazily-loaded `tesseract.js`. */
|
|
30
|
+
recognize?: TesseractRecognize;
|
|
31
|
+
/**
|
|
32
|
+
* Injectable image preprocessor run on each side before recognition. Defaults
|
|
33
|
+
* to a lazily-loaded `sharp` pass (grayscale/normalise/upscale) that degrades
|
|
34
|
+
* to a no-op when `sharp` isn't installed. Pass `false` to disable it.
|
|
35
|
+
*/
|
|
36
|
+
preprocess?: OcrPreprocessor | false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* In-process OCR via Tesseract.js. Recognizes text from the document image, then
|
|
40
|
+
* runs it through the {@link DocumentParserRegistry} to extract structured fields.
|
|
41
|
+
* `tesseract.js` is imported lazily so it is only loaded when this driver runs.
|
|
42
|
+
*/
|
|
43
|
+
declare class TesseractOcrDriver implements OcrDriver {
|
|
44
|
+
readonly name = "tesseract";
|
|
45
|
+
private readonly language;
|
|
46
|
+
private readonly registry;
|
|
47
|
+
private readonly recognizeImpl?;
|
|
48
|
+
private readonly preprocessOption?;
|
|
49
|
+
private preprocessImpl?;
|
|
50
|
+
constructor(options?: TesseractOcrOptions);
|
|
51
|
+
extract(request: OcrRequest): Promise<OcrResultData>;
|
|
52
|
+
/** Parse every candidate text and return the best-ranked, highest-scoring result. */
|
|
53
|
+
private bestOf;
|
|
54
|
+
/** Parse one candidate text and attach its stage rank + blended confidence. */
|
|
55
|
+
private score;
|
|
56
|
+
/**
|
|
57
|
+
* Blend the parser's structural confidence with the engine's self-reported
|
|
58
|
+
* confidence — parser-dominant, because what the parser extracted matters more
|
|
59
|
+
* than how sure Tesseract felt about each glyph (it is pessimistic on the OCR-B
|
|
60
|
+
* MRZ font and busy document backgrounds). For the `mrz` stage the result is
|
|
61
|
+
* check-digit-verified ground truth, so the engine only lifts the score and can
|
|
62
|
+
* never drag a verified read down. Other stages aren't self-verifying, so the
|
|
63
|
+
* engine's confidence carries more weight.
|
|
64
|
+
*/
|
|
65
|
+
private scoreConfidence;
|
|
66
|
+
/** Recognize text, or `null` if the engine can't read the image. */
|
|
67
|
+
private tryRecognize;
|
|
68
|
+
/** Run the configured preprocessor (resolved once), or pass bytes through. */
|
|
69
|
+
private preprocess;
|
|
70
|
+
/** Lazily load `tesseract.js` and adapt it to {@link TesseractRecognize}. */
|
|
71
|
+
private loadRecognizer;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { RecognizeOptions, TesseractOcrDriver, TesseractOcrOptions, TesseractRecognize };
|
|
75
|
+
//# sourceMappingURL=tesseract.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tesseract.d.mts","names":[],"sources":["../../src/drivers/tesseract.ts"],"mappings":";;;;;;;UAeiB,gBAAA;EAAA;;;;AAMZ;EAAH,GAAG;AAAA;;KAIO,kBAAA,IACV,KAAA,EAAO,UAAA,EACP,QAAA,UACA,OAAA,GAAU,gBAAA,KACP,OAAA;EAAU,IAAA;EAAc,UAAA;AAAA;AAAA,UA0BZ,mBAAA;EA7BR;EA+BP,QAAA;EA9BA;;;;EAmCA,QAAA,GAAW,sBAAA;EAjCgB;EAmC3B,SAAA,GAAY,kBAAA;EAnCyB;AA0BvC;;;;EAeE,UAAA,GAAa,eAAA;AAAA;;;;;;cAQF,kBAAA,YAA8B,SAAA;EAAA,SAChC,IAAA;EAAA,iBACQ,QAAA;EAAA,iBACA,QAAA;EAAA,iBACA,aAAA;EAAA,iBACA,gBAAA;EAAA,QACT,cAAA;cAEI,OAAA,GAAS,mBAAA;EAOf,OAAA,CAAQ,OAAA,EAAS,UAAA,GAAa,OAAA,CAAQ,aAAA;EAPvB;EAAA,QA2Db,MAAA;EApDoC;EAAA,QAgEpC,KAAA;EA/EiC;;;;;;;;;EAAA,QA0GjC,eAAA;;UAOM,YAAA;EAzGF;EAAA,QAuHE,UAAA;EAhHS;EAAA,QAyHT,cAAA;AAAA"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createDocumentParserRegistry } from "../parsers/registry.mjs";
|
|
2
|
+
import { defaultPreprocessor } from "./preprocess.mjs";
|
|
3
|
+
//#region src/drivers/tesseract.ts
|
|
4
|
+
const clamp01 = (n) => Math.min(1, Math.max(0, n));
|
|
5
|
+
/** MRZ (machine-readable zone) charset — uppercase letters, digits and the filler. */
|
|
6
|
+
const MRZ_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<";
|
|
7
|
+
/** Relative quality of each parse stage; higher wins when picking the best result. */
|
|
8
|
+
const STAGE_RANK = {
|
|
9
|
+
mrz: 3,
|
|
10
|
+
custom: 2,
|
|
11
|
+
generic: 1,
|
|
12
|
+
none: 0
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* In-process OCR via Tesseract.js. Recognizes text from the document image, then
|
|
16
|
+
* runs it through the {@link DocumentParserRegistry} to extract structured fields.
|
|
17
|
+
* `tesseract.js` is imported lazily so it is only loaded when this driver runs.
|
|
18
|
+
*/
|
|
19
|
+
var TesseractOcrDriver = class {
|
|
20
|
+
name = "tesseract";
|
|
21
|
+
language;
|
|
22
|
+
registry;
|
|
23
|
+
recognizeImpl;
|
|
24
|
+
preprocessOption;
|
|
25
|
+
preprocessImpl;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.language = options.language ?? "eng";
|
|
28
|
+
this.registry = options.registry ?? createDocumentParserRegistry();
|
|
29
|
+
this.recognizeImpl = options.recognize;
|
|
30
|
+
this.preprocessOption = options.preprocess;
|
|
31
|
+
}
|
|
32
|
+
async extract(request) {
|
|
33
|
+
const sides = [];
|
|
34
|
+
if (request.image?.length) sides.push(request.image);
|
|
35
|
+
if (request.backImage?.length) sides.push(request.backImage);
|
|
36
|
+
const reads = [];
|
|
37
|
+
for (const image of sides) {
|
|
38
|
+
const read = await this.tryRecognize(image);
|
|
39
|
+
if (read) reads.push({
|
|
40
|
+
text: read.text,
|
|
41
|
+
engine: read.confidence
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (reads.length === 0) return {
|
|
45
|
+
fields: {},
|
|
46
|
+
confidence: 0,
|
|
47
|
+
raw: {
|
|
48
|
+
engine: "tesseract",
|
|
49
|
+
empty: true
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const candidates = [...reads];
|
|
53
|
+
if (reads.length > 1) candidates.push({
|
|
54
|
+
text: reads.map((r) => r.text).join("\n"),
|
|
55
|
+
engine: avg(reads.map((r) => r.engine))
|
|
56
|
+
});
|
|
57
|
+
let best = this.bestOf(candidates, request);
|
|
58
|
+
if (best.rank < STAGE_RANK.mrz) {
|
|
59
|
+
const mrzReads = [];
|
|
60
|
+
for (const image of sides) {
|
|
61
|
+
const read = await this.tryRecognize(image, { mrz: true });
|
|
62
|
+
if (read?.text.trim()) mrzReads.push({
|
|
63
|
+
text: read.text,
|
|
64
|
+
engine: read.confidence
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (mrzReads.length) {
|
|
68
|
+
const mrzBest = this.bestOf(mrzReads, request);
|
|
69
|
+
if (mrzBest.rank > best.rank || mrzBest.rank === best.rank && mrzBest.score > best.score) best = mrzBest;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
fields: best.parsed.fields,
|
|
74
|
+
confidence: best.score,
|
|
75
|
+
raw: {
|
|
76
|
+
engine: "tesseract",
|
|
77
|
+
stage: best.stage,
|
|
78
|
+
text: best.text,
|
|
79
|
+
parser: best.parsed.raw
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Parse every candidate text and return the best-ranked, highest-scoring result. */
|
|
84
|
+
bestOf(candidates, request) {
|
|
85
|
+
let best = null;
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const scored = this.score(candidate, request);
|
|
88
|
+
if (!best || scored.rank > best.rank || scored.rank === best.rank && scored.score > best.score) best = scored;
|
|
89
|
+
}
|
|
90
|
+
return best;
|
|
91
|
+
}
|
|
92
|
+
/** Parse one candidate text and attach its stage rank + blended confidence. */
|
|
93
|
+
score(candidate, request) {
|
|
94
|
+
const parsed = this.registry.parse({
|
|
95
|
+
text: candidate.text,
|
|
96
|
+
country: request.country,
|
|
97
|
+
documentType: request.documentType
|
|
98
|
+
});
|
|
99
|
+
const stage = parsed.raw?.stage;
|
|
100
|
+
const hasFields = Object.keys(parsed.fields).length > 0;
|
|
101
|
+
return {
|
|
102
|
+
parsed,
|
|
103
|
+
stage,
|
|
104
|
+
rank: STAGE_RANK[stage ?? "none"] ?? 0,
|
|
105
|
+
score: hasFields ? this.scoreConfidence(parsed.confidence, candidate.engine / 100, stage) : 0,
|
|
106
|
+
text: candidate.text
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Blend the parser's structural confidence with the engine's self-reported
|
|
111
|
+
* confidence — parser-dominant, because what the parser extracted matters more
|
|
112
|
+
* than how sure Tesseract felt about each glyph (it is pessimistic on the OCR-B
|
|
113
|
+
* MRZ font and busy document backgrounds). For the `mrz` stage the result is
|
|
114
|
+
* check-digit-verified ground truth, so the engine only lifts the score and can
|
|
115
|
+
* never drag a verified read down. Other stages aren't self-verifying, so the
|
|
116
|
+
* engine's confidence carries more weight.
|
|
117
|
+
*/
|
|
118
|
+
scoreConfidence(parserConfidence, engine, stage) {
|
|
119
|
+
const e = clamp01(engine);
|
|
120
|
+
if (stage === "mrz") return clamp01(parserConfidence * .9 + e * .1);
|
|
121
|
+
return clamp01(parserConfidence * .6 + e * .4);
|
|
122
|
+
}
|
|
123
|
+
/** Recognize text, or `null` if the engine can't read the image. */
|
|
124
|
+
async tryRecognize(image, options) {
|
|
125
|
+
try {
|
|
126
|
+
const prepared = await this.preprocess(image);
|
|
127
|
+
return await (this.recognizeImpl ?? await this.loadRecognizer())(prepared, this.language, options);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Run the configured preprocessor (resolved once), or pass bytes through. */
|
|
133
|
+
async preprocess(image) {
|
|
134
|
+
if (this.preprocessOption === false) return image;
|
|
135
|
+
if (!this.preprocessImpl) this.preprocessImpl = this.preprocessOption ?? await defaultPreprocessor();
|
|
136
|
+
return this.preprocessImpl(image);
|
|
137
|
+
}
|
|
138
|
+
/** Lazily load `tesseract.js` and adapt it to {@link TesseractRecognize}. */
|
|
139
|
+
async loadRecognizer() {
|
|
140
|
+
const moduleName = "tesseract.js";
|
|
141
|
+
let mod;
|
|
142
|
+
try {
|
|
143
|
+
mod = await import(
|
|
144
|
+
/* @vite-ignore */
|
|
145
|
+
moduleName
|
|
146
|
+
);
|
|
147
|
+
} catch {
|
|
148
|
+
throw new Error("OCR driver 'tesseract' requires the 'tesseract.js' package. Install it with: pnpm add tesseract.js -F @arkyc/ocr");
|
|
149
|
+
}
|
|
150
|
+
return async (image, language, options) => {
|
|
151
|
+
const worker = await mod.createWorker(language);
|
|
152
|
+
try {
|
|
153
|
+
if (options?.mrz) await worker.setParameters({
|
|
154
|
+
tessedit_char_whitelist: MRZ_CHARSET,
|
|
155
|
+
tessedit_pageseg_mode: "6"
|
|
156
|
+
});
|
|
157
|
+
const { data } = await worker.recognize(Buffer.from(image));
|
|
158
|
+
return {
|
|
159
|
+
text: data.text,
|
|
160
|
+
confidence: data.confidence
|
|
161
|
+
};
|
|
162
|
+
} finally {
|
|
163
|
+
await worker.terminate();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
/** Mean of a non-empty list of numbers. */
|
|
169
|
+
function avg(values) {
|
|
170
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
export { TesseractOcrDriver };
|
|
174
|
+
|
|
175
|
+
//# sourceMappingURL=tesseract.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tesseract.mjs","names":[],"sources":["../../src/drivers/tesseract.ts"],"sourcesContent":["import type { OcrResultData } from '@arkyc/types'\nimport type { OcrDriver, OcrRequest } from '../types'\nimport { createDocumentParserRegistry, type DocumentParserRegistry } from '../parsers/registry'\nimport type { ParseOutput } from '../parsers/types'\nimport { defaultPreprocessor, type OcrPreprocessor } from './preprocess'\n\nconst clamp01 = (n: number): number => Math.min(1, Math.max(0, n))\n\n/** MRZ (machine-readable zone) charset — uppercase letters, digits and the filler. */\nconst MRZ_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789<'\n\n/** Relative quality of each parse stage; higher wins when picking the best result. */\nconst STAGE_RANK: Record<string, number> = { mrz: 3, custom: 2, generic: 1, none: 0 }\n\n/** Options for a single recognition pass. */\nexport interface RecognizeOptions {\n /**\n * Constrain recognition to the {@link MRZ_CHARSET}. This forces ambiguous OCR-B\n * glyphs into the machine-readable zone's alphabet (e.g. `O`→`0`, `I`→`1`),\n * dramatically improving the numeric MRZ lines (dates, check digits).\n */\n mrz?: boolean\n}\n\n/** Reads text from an image; returns text + an engine confidence in [0, 100]. */\nexport type TesseractRecognize = (\n image: Uint8Array,\n language: string,\n options?: RecognizeOptions,\n) => Promise<{ text: string; confidence: number }>\n\n/** Minimal structural type for the lazily-imported `tesseract.js` module. */\ninterface TesseractModule {\n createWorker(language?: string): Promise<{\n setParameters(params: Record<string, string>): Promise<unknown>\n recognize(image: Buffer): Promise<{ data: { text: string; confidence: number } }>\n terminate(): Promise<void>\n }>\n}\n\n/** A candidate OCR text to parse, with the engine confidence that produced it. */\ninterface Candidate {\n text: string\n engine: number\n}\n\n/** A parse result with the metadata used to rank it against other candidates. */\ninterface Scored {\n parsed: ParseOutput\n stage?: string\n rank: number\n score: number\n text: string\n}\n\nexport interface TesseractOcrOptions {\n /** Recognition language(s), default `eng`. */\n language?: string\n /**\n * Parser registry used to turn recognized text into fields. Defaults to the\n * MRZ-backed registry; pass one with your country/type parsers registered.\n */\n registry?: DocumentParserRegistry\n /** Injectable recognizer (tests); defaults to a lazily-loaded `tesseract.js`. */\n recognize?: TesseractRecognize\n /**\n * Injectable image preprocessor run on each side before recognition. Defaults\n * to a lazily-loaded `sharp` pass (grayscale/normalise/upscale) that degrades\n * to a no-op when `sharp` isn't installed. Pass `false` to disable it.\n */\n preprocess?: OcrPreprocessor | false\n}\n\n/**\n * In-process OCR via Tesseract.js. Recognizes text from the document image, then\n * runs it through the {@link DocumentParserRegistry} to extract structured fields.\n * `tesseract.js` is imported lazily so it is only loaded when this driver runs.\n */\nexport class TesseractOcrDriver implements OcrDriver {\n readonly name = 'tesseract'\n private readonly language: string\n private readonly registry: DocumentParserRegistry\n private readonly recognizeImpl?: TesseractRecognize\n private readonly preprocessOption?: OcrPreprocessor | false\n private preprocessImpl?: OcrPreprocessor\n\n constructor(options: TesseractOcrOptions = {}) {\n this.language = options.language ?? 'eng'\n this.registry = options.registry ?? createDocumentParserRegistry()\n this.recognizeImpl = options.recognize\n this.preprocessOption = options.preprocess\n }\n\n async extract(request: OcrRequest): Promise<OcrResultData> {\n // Read both sides — the MRZ may be on the front (passports) or the back\n // (TD1 ID cards, residence permits).\n const sides: Uint8Array[] = []\n if (request.image?.length) sides.push(request.image)\n if (request.backImage?.length) sides.push(request.backImage)\n\n const reads: Candidate[] = []\n for (const image of sides) {\n const read = await this.tryRecognize(image)\n if (read) reads.push({ text: read.text, engine: read.confidence })\n }\n\n if (reads.length === 0) {\n // Nothing readable (empty or unreadable images, or engine failure): return\n // empty so the decision engine routes on low confidence (manual review).\n return { fields: {}, confidence: 0, raw: { engine: 'tesseract', empty: true } }\n }\n\n // Parse each side ALONE, plus the combination. The MRZ lives on a single\n // side, and mixing both sides' text into one parse lets the other side's\n // stray long lines capture the MRZ's line slots — so a clean single side can\n // read where front+back together cannot. We keep the best-ranked result.\n const candidates: Candidate[] = [...reads]\n if (reads.length > 1) {\n candidates.push({ text: reads.map((r) => r.text).join('\\n'), engine: avg(reads.map((r) => r.engine)) })\n }\n let best = this.bestOf(candidates, request)\n\n // Legibility fallback: if nothing parsed as an MRZ, retry each side\n // constrained to the OCR-B charset (forces O→0 / I→1 on the numeric lines),\n // which often rescues an MRZ the unconstrained pass mangled.\n if (best.rank < STAGE_RANK.mrz!) {\n const mrzReads: Candidate[] = []\n for (const image of sides) {\n const read = await this.tryRecognize(image, { mrz: true })\n if (read?.text.trim()) mrzReads.push({ text: read.text, engine: read.confidence })\n }\n if (mrzReads.length) {\n const mrzBest = this.bestOf(mrzReads, request)\n if (mrzBest.rank > best.rank || (mrzBest.rank === best.rank && mrzBest.score > best.score)) best = mrzBest\n }\n }\n\n return {\n fields: best.parsed.fields,\n confidence: best.score,\n raw: { engine: 'tesseract', stage: best.stage, text: best.text, parser: best.parsed.raw },\n }\n }\n\n /** Parse every candidate text and return the best-ranked, highest-scoring result. */\n private bestOf(candidates: Candidate[], request: OcrRequest): Scored {\n let best: Scored | null = null\n for (const candidate of candidates) {\n const scored = this.score(candidate, request)\n if (!best || scored.rank > best.rank || (scored.rank === best.rank && scored.score > best.score)) {\n best = scored\n }\n }\n return best!\n }\n\n /** Parse one candidate text and attach its stage rank + blended confidence. */\n private score(candidate: Candidate, request: OcrRequest): Scored {\n const parsed = this.registry.parse({\n text: candidate.text,\n country: request.country,\n documentType: request.documentType,\n })\n const stage = (parsed.raw as { stage?: string } | undefined)?.stage\n const hasFields = Object.keys(parsed.fields).length > 0\n return {\n parsed,\n stage,\n rank: STAGE_RANK[stage ?? 'none'] ?? 0,\n // A result with no extracted fields carries no confidence, whatever the engine felt.\n score: hasFields ? this.scoreConfidence(parsed.confidence, candidate.engine / 100, stage) : 0,\n text: candidate.text,\n }\n }\n\n /**\n * Blend the parser's structural confidence with the engine's self-reported\n * confidence — parser-dominant, because what the parser extracted matters more\n * than how sure Tesseract felt about each glyph (it is pessimistic on the OCR-B\n * MRZ font and busy document backgrounds). For the `mrz` stage the result is\n * check-digit-verified ground truth, so the engine only lifts the score and can\n * never drag a verified read down. Other stages aren't self-verifying, so the\n * engine's confidence carries more weight.\n */\n private scoreConfidence(parserConfidence: number, engine: number, stage?: string): number {\n const e = clamp01(engine)\n if (stage === 'mrz') return clamp01(parserConfidence * 0.9 + e * 0.1)\n return clamp01(parserConfidence * 0.6 + e * 0.4)\n }\n\n /** Recognize text, or `null` if the engine can't read the image. */\n private async tryRecognize(\n image: Uint8Array,\n options?: RecognizeOptions,\n ): Promise<{ text: string; confidence: number } | null> {\n try {\n const prepared = await this.preprocess(image)\n const recognize = this.recognizeImpl ?? (await this.loadRecognizer())\n return await recognize(prepared, this.language, options)\n } catch {\n return null\n }\n }\n\n /** Run the configured preprocessor (resolved once), or pass bytes through. */\n private async preprocess(image: Uint8Array): Promise<Uint8Array> {\n if (this.preprocessOption === false) return image\n if (!this.preprocessImpl) {\n this.preprocessImpl = this.preprocessOption ?? (await defaultPreprocessor())\n }\n return this.preprocessImpl(image)\n }\n\n /** Lazily load `tesseract.js` and adapt it to {@link TesseractRecognize}. */\n private async loadRecognizer(): Promise<TesseractRecognize> {\n const moduleName = 'tesseract.js'\n let mod: TesseractModule\n try {\n mod = (await import(/* @vite-ignore */ moduleName)) as unknown as TesseractModule\n } catch {\n throw new Error(\n \"OCR driver 'tesseract' requires the 'tesseract.js' package. Install it with: pnpm add tesseract.js -F @arkyc/ocr\",\n )\n }\n return async (image, language, options) => {\n const worker = await mod.createWorker(language)\n try {\n if (options?.mrz) {\n await worker.setParameters({\n tessedit_char_whitelist: MRZ_CHARSET,\n // Treat the input as a single uniform block (the MRZ band's fixed lines).\n tessedit_pageseg_mode: '6',\n })\n }\n const { data } = await worker.recognize(Buffer.from(image))\n return { text: data.text, confidence: data.confidence }\n } finally {\n await worker.terminate()\n }\n }\n }\n}\n\n/** Mean of a non-empty list of numbers. */\nfunction avg(values: number[]): number {\n return values.reduce((a, b) => a + b, 0) / values.length\n}\n"],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { OcrConfig, OcrDriver, OcrDriverName, OcrRequest } from "./types.mjs";
|
|
2
|
+
import { OcrDriverFactory } from "./registry.mjs";
|
|
3
|
+
import { MockOcrDriver } from "./drivers/mock.mjs";
|
|
4
|
+
import { ExternalOcrDriver } from "./drivers/external.mjs";
|
|
5
|
+
import { DocumentParser, ParseInput, ParseOutput } from "./parsers/types.mjs";
|
|
6
|
+
import { DocumentParserRegistry, ParseStage, createDocumentParserRegistry } from "./parsers/registry.mjs";
|
|
7
|
+
import { OcrPreprocessor, PreprocessOptions, buildSharpPreprocessor, defaultPreprocessor, passthrough } from "./drivers/preprocess.mjs";
|
|
8
|
+
import { RecognizeOptions, TesseractOcrDriver, TesseractOcrOptions, TesseractRecognize } from "./drivers/tesseract.mjs";
|
|
9
|
+
import { AiExtraction, AiVisionExtract, AnthropicOcrDriver, AnthropicOcrOptions, DEFAULT_AI_MODEL, anthropicVision, applyAuthenticity, scoreConfidence } from "./drivers/ai.mjs";
|
|
10
|
+
import { mrzParser } from "./parsers/mrz.mjs";
|
|
11
|
+
import { genericTextParser } from "./parsers/generic.mjs";
|
|
12
|
+
export { type AiExtraction, type AiVisionExtract, AnthropicOcrDriver, type AnthropicOcrOptions, DEFAULT_AI_MODEL, type DocumentParser, DocumentParserRegistry, ExternalOcrDriver, MockOcrDriver, OcrConfig, OcrDriver, OcrDriverFactory, OcrDriverName, type OcrPreprocessor, OcrRequest, type ParseInput, type ParseOutput, type ParseStage, type PreprocessOptions, type RecognizeOptions, TesseractOcrDriver, type TesseractOcrOptions, type TesseractRecognize, anthropicVision, applyAuthenticity, buildSharpPreprocessor, createDocumentParserRegistry, defaultPreprocessor, genericTextParser, mrzParser, passthrough, scoreConfidence };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AnthropicOcrDriver, DEFAULT_AI_MODEL, anthropicVision, applyAuthenticity, scoreConfidence } from "./drivers/ai.mjs";
|
|
2
|
+
import { ExternalOcrDriver } from "./drivers/external.mjs";
|
|
3
|
+
import { MockOcrDriver } from "./drivers/mock.mjs";
|
|
4
|
+
import { mrzParser } from "./parsers/mrz.mjs";
|
|
5
|
+
import { genericTextParser } from "./parsers/generic.mjs";
|
|
6
|
+
import { DocumentParserRegistry, createDocumentParserRegistry } from "./parsers/registry.mjs";
|
|
7
|
+
import { buildSharpPreprocessor, defaultPreprocessor, passthrough } from "./drivers/preprocess.mjs";
|
|
8
|
+
import { TesseractOcrDriver } from "./drivers/tesseract.mjs";
|
|
9
|
+
import { OcrDriverFactory } from "./registry.mjs";
|
|
10
|
+
export { AnthropicOcrDriver, DEFAULT_AI_MODEL, DocumentParserRegistry, ExternalOcrDriver, MockOcrDriver, OcrDriverFactory, TesseractOcrDriver, anthropicVision, applyAuthenticity, buildSharpPreprocessor, createDocumentParserRegistry, defaultPreprocessor, genericTextParser, mrzParser, passthrough, scoreConfidence };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DocumentParser } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/parsers/generic.d.ts
|
|
4
|
+
/** The generic best-effort text parser. */
|
|
5
|
+
declare function genericTextParser(): DocumentParser;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { genericTextParser };
|
|
8
|
+
//# sourceMappingURL=generic.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic.d.mts","names":[],"sources":["../../src/parsers/generic.ts"],"mappings":";;;;iBA0FgB,iBAAA,IAAqB,cAAc"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/parsers/generic.ts
|
|
2
|
+
/**
|
|
3
|
+
* A best-effort parser for documents without (or with an unreadable) MRZ. It
|
|
4
|
+
* scrapes dates and a document-number-like token from the raw OCR text via
|
|
5
|
+
* regex. Low confidence by design — it's the last resort when the MRZ parser
|
|
6
|
+
* can't read a machine-readable zone (most ID/licence fronts).
|
|
7
|
+
*/
|
|
8
|
+
const MONTHS = {
|
|
9
|
+
jan: 1,
|
|
10
|
+
feb: 2,
|
|
11
|
+
mar: 3,
|
|
12
|
+
apr: 4,
|
|
13
|
+
may: 5,
|
|
14
|
+
jun: 6,
|
|
15
|
+
jul: 7,
|
|
16
|
+
aug: 8,
|
|
17
|
+
sep: 9,
|
|
18
|
+
oct: 10,
|
|
19
|
+
nov: 11,
|
|
20
|
+
dec: 12
|
|
21
|
+
};
|
|
22
|
+
const pad2 = (n) => n < 10 ? `0${n}` : `${n}`;
|
|
23
|
+
function isoDate(y, m, d) {
|
|
24
|
+
if (m < 1 || m > 12 || d < 1 || d > 31 || y < 1900 || y > 2100) return null;
|
|
25
|
+
return `${y}-${pad2(m)}-${pad2(d)}`;
|
|
26
|
+
}
|
|
27
|
+
/** Extract plausible calendar dates from text, ascending and de-duplicated. */
|
|
28
|
+
function extractDates(text) {
|
|
29
|
+
const found = /* @__PURE__ */ new Set();
|
|
30
|
+
for (const m of text.matchAll(/\b(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})\b/g)) {
|
|
31
|
+
const iso = isoDate(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
32
|
+
if (iso) found.add(iso);
|
|
33
|
+
}
|
|
34
|
+
for (const m of text.matchAll(/\b(\d{1,2})[-/.](\d{1,2})[-/.](\d{4})\b/g)) {
|
|
35
|
+
const a = Number(m[1]);
|
|
36
|
+
const b = Number(m[2]);
|
|
37
|
+
const y = Number(m[3]);
|
|
38
|
+
const iso = b > 12 ? isoDate(y, a, b) : isoDate(y, b, a);
|
|
39
|
+
if (iso) found.add(iso);
|
|
40
|
+
}
|
|
41
|
+
for (const m of text.matchAll(/\b(\d{1,2})[-\s]([A-Za-z]{3,})[-\s](\d{4})\b/g)) {
|
|
42
|
+
const month = MONTHS[m[2].slice(0, 3).toLowerCase()];
|
|
43
|
+
if (month) {
|
|
44
|
+
const iso = isoDate(Number(m[3]), month, Number(m[1]));
|
|
45
|
+
if (iso) found.add(iso);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...found].sort();
|
|
49
|
+
}
|
|
50
|
+
/** Pick a document-number-like token: 6–12 chars, has a digit, prefer alphanumeric. */
|
|
51
|
+
function extractDocumentNumber(text) {
|
|
52
|
+
const tokens = (text.toUpperCase().match(/\b[A-Z0-9]{6,12}\b/g) ?? []).filter((t) => /[0-9]/.test(t) && !/^(19|20)\d{2}$/.test(t));
|
|
53
|
+
return tokens.find((t) => /[A-Z]/.test(t)) ?? tokens[0];
|
|
54
|
+
}
|
|
55
|
+
var GenericTextParser = class {
|
|
56
|
+
name = "generic";
|
|
57
|
+
parse(input) {
|
|
58
|
+
const text = input.lines ? input.lines.join("\n") : input.text;
|
|
59
|
+
const dates = extractDates(text);
|
|
60
|
+
const documentNumber = extractDocumentNumber(text);
|
|
61
|
+
if (dates.length === 0 && !documentNumber) return null;
|
|
62
|
+
const fields = {};
|
|
63
|
+
if (documentNumber) fields.documentNumber = documentNumber;
|
|
64
|
+
if (dates.length >= 1) fields.dateOfBirth = dates[0];
|
|
65
|
+
if (dates.length >= 2) fields.expiryDate = dates[dates.length - 1];
|
|
66
|
+
return {
|
|
67
|
+
fields,
|
|
68
|
+
confidence: .35,
|
|
69
|
+
raw: {
|
|
70
|
+
format: "generic",
|
|
71
|
+
dates,
|
|
72
|
+
documentNumber
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
/** The generic best-effort text parser. */
|
|
78
|
+
function genericTextParser() {
|
|
79
|
+
return new GenericTextParser();
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { genericTextParser };
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=generic.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generic.mjs","names":[],"sources":["../../src/parsers/generic.ts"],"sourcesContent":["import type { IsoDate, OcrFields } from '@arkyc/types'\nimport type { DocumentParser, ParseInput, ParseOutput } from './types'\n\n/**\n * A best-effort parser for documents without (or with an unreadable) MRZ. It\n * scrapes dates and a document-number-like token from the raw OCR text via\n * regex. Low confidence by design — it's the last resort when the MRZ parser\n * can't read a machine-readable zone (most ID/licence fronts).\n */\n\nconst MONTHS: Record<string, number> = {\n jan: 1,\n feb: 2,\n mar: 3,\n apr: 4,\n may: 5,\n jun: 6,\n jul: 7,\n aug: 8,\n sep: 9,\n oct: 10,\n nov: 11,\n dec: 12,\n}\n\nconst pad2 = (n: number): string => (n < 10 ? `0${n}` : `${n}`)\n\nfunction isoDate(y: number, m: number, d: number): IsoDate | null {\n if (m < 1 || m > 12 || d < 1 || d > 31 || y < 1900 || y > 2100) return null\n return `${y}-${pad2(m)}-${pad2(d)}` as IsoDate\n}\n\n/** Extract plausible calendar dates from text, ascending and de-duplicated. */\nfunction extractDates(text: string): IsoDate[] {\n const found = new Set<IsoDate>()\n\n // ISO-ish: YYYY-MM-DD / YYYY.MM.DD / YYYY/MM/DD\n for (const m of text.matchAll(/\\b(\\d{4})[-/.](\\d{1,2})[-/.](\\d{1,2})\\b/g)) {\n const iso = isoDate(Number(m[1]), Number(m[2]), Number(m[3]))\n if (iso) found.add(iso)\n }\n // Day/Month first: DD-MM-YYYY / DD.MM.YYYY / DD/MM/YYYY (US MM/DD/YYYY when month>12).\n for (const m of text.matchAll(/\\b(\\d{1,2})[-/.](\\d{1,2})[-/.](\\d{4})\\b/g)) {\n const a = Number(m[1])\n const b = Number(m[2])\n const y = Number(m[3])\n const iso = b > 12 ? isoDate(y, a, b) : isoDate(y, b, a)\n if (iso) found.add(iso)\n }\n // DD MON YYYY: \"12 JAN 2020\", \"12-Jan-2020\"\n for (const m of text.matchAll(/\\b(\\d{1,2})[-\\s]([A-Za-z]{3,})[-\\s](\\d{4})\\b/g)) {\n const month = MONTHS[m[2]!.slice(0, 3).toLowerCase()]\n if (month) {\n const iso = isoDate(Number(m[3]), month, Number(m[1]))\n if (iso) found.add(iso)\n }\n }\n\n return [...found].sort()\n}\n\n/** Pick a document-number-like token: 6–12 chars, has a digit, prefer alphanumeric. */\nfunction extractDocumentNumber(text: string): string | undefined {\n const tokens = (text.toUpperCase().match(/\\b[A-Z0-9]{6,12}\\b/g) ?? []).filter(\n (t) => /[0-9]/.test(t) && !/^(19|20)\\d{2}$/.test(t),\n )\n return tokens.find((t) => /[A-Z]/.test(t)) ?? tokens[0]\n}\n\nclass GenericTextParser implements DocumentParser {\n readonly name = 'generic'\n\n parse(input: ParseInput): ParseOutput | null {\n const text = input.lines ? input.lines.join('\\n') : input.text\n const dates = extractDates(text)\n const documentNumber = extractDocumentNumber(text)\n\n if (dates.length === 0 && !documentNumber) return null\n\n const fields: OcrFields = {}\n if (documentNumber) fields.documentNumber = documentNumber\n // Earliest date is most likely the date of birth; latest the expiry.\n if (dates.length >= 1) fields.dateOfBirth = dates[0]\n if (dates.length >= 2) fields.expiryDate = dates[dates.length - 1]\n\n return { fields, confidence: 0.35, raw: { format: 'generic', dates, documentNumber } }\n }\n}\n\n/** The generic best-effort text parser. */\nexport function genericTextParser(): DocumentParser {\n return new GenericTextParser()\n}\n"],"mappings":";;;;;;;AAUA,MAAM,SAAiC;CACrC,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;AAEA,MAAM,QAAQ,MAAuB,IAAI,KAAK,IAAI,MAAM,GAAG;AAE3D,SAAS,QAAQ,GAAW,GAAW,GAA2B;CAChE,IAAI,IAAI,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,OAAO;CACvE,OAAO,GAAG,EAAE,GAAG,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC;AAClC;;AAGA,SAAS,aAAa,MAAyB;CAC7C,MAAM,wBAAQ,IAAI,IAAa;CAG/B,KAAK,MAAM,KAAK,KAAK,SAAS,0CAA0C,GAAG;EACzE,MAAM,MAAM,QAAQ,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,CAAC;EAC5D,IAAI,KAAK,MAAM,IAAI,GAAG;CACxB;CAEA,KAAK,MAAM,KAAK,KAAK,SAAS,0CAA0C,GAAG;EACzE,MAAM,IAAI,OAAO,EAAE,EAAE;EACrB,MAAM,IAAI,OAAO,EAAE,EAAE;EACrB,MAAM,IAAI,OAAO,EAAE,EAAE;EACrB,MAAM,MAAM,IAAI,KAAK,QAAQ,GAAG,GAAG,CAAC,IAAI,QAAQ,GAAG,GAAG,CAAC;EACvD,IAAI,KAAK,MAAM,IAAI,GAAG;CACxB;CAEA,KAAK,MAAM,KAAK,KAAK,SAAS,+CAA+C,GAAG;EAC9E,MAAM,QAAQ,OAAO,EAAE,EAAE,CAAE,MAAM,GAAG,CAAC,CAAC,CAAC,YAAY;EACnD,IAAI,OAAO;GACT,MAAM,MAAM,QAAQ,OAAO,EAAE,EAAE,GAAG,OAAO,OAAO,EAAE,EAAE,CAAC;GACrD,IAAI,KAAK,MAAM,IAAI,GAAG;EACxB;CACF;CAEA,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK;AACzB;;AAGA,SAAS,sBAAsB,MAAkC;CAC/D,MAAM,UAAU,KAAK,YAAY,CAAC,CAAC,MAAM,qBAAqB,KAAK,CAAC,EAAA,CAAG,QACpE,MAAM,QAAQ,KAAK,CAAC,KAAK,CAAC,iBAAiB,KAAK,CAAC,CACpD;CACA,OAAO,OAAO,MAAM,MAAM,QAAQ,KAAK,CAAC,CAAC,KAAK,OAAO;AACvD;AAEA,IAAM,oBAAN,MAAkD;CAChD,OAAgB;CAEhB,MAAM,OAAuC;EAC3C,MAAM,OAAO,MAAM,QAAQ,MAAM,MAAM,KAAK,IAAI,IAAI,MAAM;EAC1D,MAAM,QAAQ,aAAa,IAAI;EAC/B,MAAM,iBAAiB,sBAAsB,IAAI;EAEjD,IAAI,MAAM,WAAW,KAAK,CAAC,gBAAgB,OAAO;EAElD,MAAM,SAAoB,CAAC;EAC3B,IAAI,gBAAgB,OAAO,iBAAiB;EAE5C,IAAI,MAAM,UAAU,GAAG,OAAO,cAAc,MAAM;EAClD,IAAI,MAAM,UAAU,GAAG,OAAO,aAAa,MAAM,MAAM,SAAS;EAEhE,OAAO;GAAE;GAAQ,YAAY;GAAM,KAAK;IAAE,QAAQ;IAAW;IAAO;GAAe;EAAE;CACvF;AACF;;AAGA,SAAgB,oBAAoC;CAClD,OAAO,IAAI,kBAAkB;AAC/B"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DocumentParser } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/parsers/mrz.d.ts
|
|
4
|
+
/** The MRZ parser — a country-agnostic default for machine-readable documents. */
|
|
5
|
+
declare function mrzParser(): DocumentParser;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { mrzParser };
|
|
8
|
+
//# sourceMappingURL=mrz.d.mts.map
|