@scanmate/ocr 0.0.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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +947 -0
- package/dist/src/index.d.ts +37 -0
- package/dist/src/ocr-engine/index.d.ts +5 -0
- package/dist/src/ocr-engine/ocr-engine.contract.d.ts +54 -0
- package/dist/src/ocr-engine/tesseract-engine.client.d.ts +71 -0
- package/dist/src/page-reading/index.d.ts +8 -0
- package/dist/src/page-reading/match-words.use-case.d.ts +65 -0
- package/dist/src/page-reading/ocr-pages.use-case.d.ts +17 -0
- package/dist/src/page-reading/ocr-report.contract.d.ts +154 -0
- package/dist/src/page-reading/recheck-run.use-case.d.ts +49 -0
- package/dist/src/text-normalisation/confusables.mapper.d.ts +12 -0
- package/dist/src/text-normalisation/diacritics.mapper.d.ts +15 -0
- package/dist/src/text-normalisation/index.d.ts +6 -0
- package/dist/src/text-normalisation/normalise-text.use-case.d.ts +36 -0
- package/dist/src/text-similarity/compare-texts.use-case.d.ts +30 -0
- package/dist/src/text-similarity/index.d.ts +5 -0
- package/dist/src/text-similarity/similarity-metrics.use-case.d.ts +35 -0
- package/package.json +56 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@scanmate/ocr` - how closely a scan's text matches the original's.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { ocrPages } from '@scanmate/ocr'
|
|
6
|
+
*
|
|
7
|
+
* const report = await ocrPages(alignedPages) // or enhanced pages
|
|
8
|
+
* report.score // the document, 0-1
|
|
9
|
+
* report.pages[0].metrics.wordRecall // one of ten measures
|
|
10
|
+
* report.pages[0].differences // what differs, and where
|
|
11
|
+
* report.pages[0].original.text // both texts, in full
|
|
12
|
+
* report.pages[0].scanned.text
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* The original's text is its text layer when it has one: exact, so every error
|
|
16
|
+
* is the scan's. The scan is read with tesseract - WebAssembly, offline, with
|
|
17
|
+
* nothing written to disk unless configured to be. Its words are placed back on
|
|
18
|
+
* the original's canvas and matched run by run to what the original printed
|
|
19
|
+
* there, so reading order never counts as a difference and every difference
|
|
20
|
+
* has a place on the page.
|
|
21
|
+
*
|
|
22
|
+
* Text is normalised before it is compared: Unicode compatibility forms,
|
|
23
|
+
* typography, line-end hyphens, diacritics, case, and OCR noise.
|
|
24
|
+
*/
|
|
25
|
+
export { ocrPages } from './page-reading/index.js';
|
|
26
|
+
export type { OcrOptions, OcrReport, PageOcr, PlacedText, PositionedText, ReadablePage, RunReading, SideText, TextDifference } from './page-reading/index.js';
|
|
27
|
+
export { createTesseractEngine, DEFAULT_TESSERACT_OPTIONS } from './ocr-engine/index.js';
|
|
28
|
+
export type { OcrEngine, OcrLine, OcrWord, RecognisedText, RecogniseHints, TesseractCacheOptions, TesseractEngine, TesseractEngineOptions, TesseractSettings } from './ocr-engine/index.js';
|
|
29
|
+
export { compareTexts } from './text-similarity/index.js';
|
|
30
|
+
export type { ScoreMetric, TextMetrics } from './text-similarity/index.js';
|
|
31
|
+
export { DEFAULT_NORMALISE, normaliseText, tokenise } from './text-normalisation/index.js';
|
|
32
|
+
export type { NormaliseOptions } from './text-normalisation/index.js';
|
|
33
|
+
export { claimWords, DEFAULT_RECHECK_PASSES, judgeRun, judgeRuns, matchWords, recheckRun } from './page-reading/index.js';
|
|
34
|
+
export type { Claims, MatchOptions, Recheck, RecheckOptions, RecheckPass, Reference, Verdict, WordMatch } from './page-reading/index.js';
|
|
35
|
+
export { cosine, dice, jaccard, jaroWinkler, levenshtein, levenshteinSimilarity, wordDistance, wordRecall } from './text-similarity/index.js';
|
|
36
|
+
export { diacriticsMap, foldConfusables, foldDiacritics } from './text-normalisation/index.js';
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Reading an image: the engine seam, and tesseract behind it. */
|
|
2
|
+
export type { OcrEngine, OcrLine, OcrWord, RecognisedText, RecogniseHints } from './ocr-engine.contract.js';
|
|
3
|
+
export { createTesseractEngine, DEFAULT_TESSERACT_OPTIONS } from './tesseract-engine.client.js';
|
|
4
|
+
export type { TesseractCacheOptions, TesseractEngine, TesseractEngineOptions, TesseractSettings } from './tesseract-engine.client.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Raster } from '@scanmate/ink';
|
|
2
|
+
/**
|
|
3
|
+
* The seam between reading and everything done with what was read.
|
|
4
|
+
*
|
|
5
|
+
* `ocrPages` needs text with positions and nothing else, so any engine can sit
|
|
6
|
+
* behind it: the bundled tesseract, a native one, a cloud service. It also
|
|
7
|
+
* means a batch pays an engine's start-up - loading tens of megabytes of model
|
|
8
|
+
* into a WASM heap - once, not once a page: create one engine, pass it to
|
|
9
|
+
* every call, terminate it when done.
|
|
10
|
+
*/
|
|
11
|
+
export interface OcrEngine {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly version: string;
|
|
14
|
+
readonly languages: readonly string[];
|
|
15
|
+
/** Read an image: a raster, or encoded bytes. Positions come back in the image's pixels. */
|
|
16
|
+
recognise(image: Raster | Uint8Array, hints?: RecogniseHints): Promise<RecognisedText>;
|
|
17
|
+
/** Release the engine. Safe to call twice. */
|
|
18
|
+
terminate(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* What the caller knows about an image that the engine would otherwise guess.
|
|
22
|
+
* An engine that cannot use a hint ignores it.
|
|
23
|
+
*/
|
|
24
|
+
export interface RecogniseHints {
|
|
25
|
+
/**
|
|
26
|
+
* `'page'` (the default) finds the layout itself; `'line'` is one line of
|
|
27
|
+
* text, `'word'` a single word - what a crop around one printed run is.
|
|
28
|
+
*/
|
|
29
|
+
layout?: 'page' | 'line' | 'word';
|
|
30
|
+
/** The only characters that may be read - digits and separators, for a figure. */
|
|
31
|
+
characters?: string;
|
|
32
|
+
}
|
|
33
|
+
/** A word as read, with its box in pixels of the image read. */
|
|
34
|
+
export interface OcrWord {
|
|
35
|
+
text: string;
|
|
36
|
+
/** 0-100, the engine's own confidence. */
|
|
37
|
+
confidence: number;
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
43
|
+
export interface OcrLine {
|
|
44
|
+
text: string;
|
|
45
|
+
words: OcrWord[];
|
|
46
|
+
}
|
|
47
|
+
export interface RecognisedText {
|
|
48
|
+
/** Plain text, in the engine's reading order. */
|
|
49
|
+
text: string;
|
|
50
|
+
/** 0-100, for the whole image. */
|
|
51
|
+
confidence: number;
|
|
52
|
+
lines: OcrLine[];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=ocr-engine.contract.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { OcrEngine } from './ocr-engine.contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Tesseract, through tesseract.js: WebAssembly, nothing to install on the host.
|
|
4
|
+
*
|
|
5
|
+
* Language data comes from the `@tesseract.js-data/<language>` packages -
|
|
6
|
+
* English is a dependency; any other language is `npm install
|
|
7
|
+
* @tesseract.js-data/<code>` - and is read from disk, never downloaded. `best`
|
|
8
|
+
* is the "best_int" LSTM model: measured on real scans it read as well as the
|
|
9
|
+
* standard model (word recall 0.917 against 0.917) at a quarter of the size.
|
|
10
|
+
*
|
|
11
|
+
* Nothing is written anywhere unless configured to be. That matters where the
|
|
12
|
+
* code is deployed read-only, as on Azure Functions: tesseract.js otherwise
|
|
13
|
+
* caches unpacked language data in the current directory. The two things that
|
|
14
|
+
* can write are both explicit, and both go under `cache.path`, which defaults to
|
|
15
|
+
* the OS temp directory:
|
|
16
|
+
*
|
|
17
|
+
* - `cache.method` other than `'none'` keeps tesseract.js's unpacked data there;
|
|
18
|
+
* - reading more than one language stages their files into one folder there,
|
|
19
|
+
* because tesseract.js takes a single data folder for all of them. (It also
|
|
20
|
+
* accepts the data itself instead of a folder, but in 7.0.0 initialises with
|
|
21
|
+
* the data where the language code belongs, so that route is closed.)
|
|
22
|
+
*
|
|
23
|
+
* What was resolved is on `engine.settings`, so it can be logged and checked.
|
|
24
|
+
*/
|
|
25
|
+
export interface TesseractEngineOptions {
|
|
26
|
+
/** Tesseract language codes, read together. Default `['eng']`. */
|
|
27
|
+
languages?: readonly string[];
|
|
28
|
+
/** `'best'` (the default) is the LSTM "best_int" model; `'standard'` the larger 4.0.0 one. */
|
|
29
|
+
model?: 'best' | 'standard';
|
|
30
|
+
/** A folder holding `<code>.traineddata.gz` per language, instead of the data packages. */
|
|
31
|
+
languageData?: string;
|
|
32
|
+
cache?: TesseractCacheOptions;
|
|
33
|
+
/** Tesseract page segmentation mode. Default `3`, fully automatic - right for whole pages. */
|
|
34
|
+
pageSegmentation?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface TesseractCacheOptions {
|
|
37
|
+
/**
|
|
38
|
+
* - `'none'` (the default): read language data from its package on every
|
|
39
|
+
* start, write nothing.
|
|
40
|
+
* - `'write'`: keep tesseract.js's unpacked data under `path` and reuse it.
|
|
41
|
+
* - `'readOnly'`: reuse data a previous run left under `path`; never write.
|
|
42
|
+
* - `'refresh'`: rewrite it.
|
|
43
|
+
*/
|
|
44
|
+
method?: 'none' | 'write' | 'readOnly' | 'refresh';
|
|
45
|
+
/** Where anything written goes. Default a `scanmate-ocr` folder in the OS temp directory. */
|
|
46
|
+
path?: string;
|
|
47
|
+
}
|
|
48
|
+
/** What an engine resolved its options to. */
|
|
49
|
+
export interface TesseractSettings {
|
|
50
|
+
languages: readonly string[];
|
|
51
|
+
model: 'best' | 'standard';
|
|
52
|
+
/** The folder the language data is read from. */
|
|
53
|
+
languageData: string;
|
|
54
|
+
cacheMethod: NonNullable<TesseractCacheOptions['method']>;
|
|
55
|
+
cachePath: string;
|
|
56
|
+
pageSegmentation: number;
|
|
57
|
+
}
|
|
58
|
+
export interface TesseractEngine extends OcrEngine {
|
|
59
|
+
readonly settings: TesseractSettings;
|
|
60
|
+
}
|
|
61
|
+
export declare const DEFAULT_TESSERACT_OPTIONS: {
|
|
62
|
+
readonly languages: readonly ["eng"];
|
|
63
|
+
readonly model: "best";
|
|
64
|
+
readonly cache: {
|
|
65
|
+
readonly method: "none";
|
|
66
|
+
readonly path: string;
|
|
67
|
+
};
|
|
68
|
+
readonly pageSegmentation: 3;
|
|
69
|
+
};
|
|
70
|
+
export declare function createTesseractEngine(options?: TesseractEngineOptions): Promise<TesseractEngine>;
|
|
71
|
+
//# sourceMappingURL=tesseract-engine.client.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Reading aligned pages and scoring the scan's text against the original's. */
|
|
2
|
+
export { claimWords, judgeRun, judgeRuns, matchWords } from './match-words.use-case.js';
|
|
3
|
+
export type { Claims, MatchOptions, Reference, Verdict, WordMatch } from './match-words.use-case.js';
|
|
4
|
+
export { ocrPages } from './ocr-pages.use-case.js';
|
|
5
|
+
export type { OcrOptions, OcrReport, PageOcr, PlacedText, PositionedText, ReadablePage, RunReading, SideText, TextDifference } from './ocr-report.contract.js';
|
|
6
|
+
export { DEFAULT_RECHECK_PASSES, recheckRun } from './recheck-run.use-case.js';
|
|
7
|
+
export type { Recheck, RecheckOptions, RecheckPass } from './recheck-run.use-case.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { NormaliseOptions } from '../text-normalisation/index.js';
|
|
2
|
+
import type { PlacedText, TextDifference } from './ocr-report.contract.js';
|
|
3
|
+
/**
|
|
4
|
+
* Put the scan's words back where the original printed them, then judge each
|
|
5
|
+
* run of the original by what was read in its place.
|
|
6
|
+
*
|
|
7
|
+
* After alignment the scan sits on the original's canvas, so a word read at
|
|
8
|
+
* some place belongs to the run of the original printed at that place. Each
|
|
9
|
+
* word goes to the run whose box - grown a little, since OCR boxes hug ink and
|
|
10
|
+
* alignment is good to a pixel or two - holds the word's centre; where boxes
|
|
11
|
+
* overlap, the smallest wins. Words no run claims were added. Runs no word
|
|
12
|
+
* reached are missing, and runs whose words read differently changed.
|
|
13
|
+
*
|
|
14
|
+
* Figures are held to a stricter standard than words. A run whose digits read
|
|
15
|
+
* back differently has changed, however alike the rest is: "Total 1,250.00"
|
|
16
|
+
* read as "Total 7,250.00" is 93% similar, and exactly the alteration a check
|
|
17
|
+
* exists to catch. Words keep the fuzzy tolerance OCR needs.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here depends on reading order, which is the point: a two-column
|
|
20
|
+
* page read down one column and then the other is still matched run for run.
|
|
21
|
+
*/
|
|
22
|
+
export interface Reference extends PlacedText {
|
|
23
|
+
endsLine?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface MatchOptions {
|
|
26
|
+
normalise: NormaliseOptions;
|
|
27
|
+
matchThreshold: number;
|
|
28
|
+
minWordConfidence: number;
|
|
29
|
+
}
|
|
30
|
+
/** Each run's reading, in the order of `references`, and the words no run claimed, grouped by line. */
|
|
31
|
+
export interface Claims {
|
|
32
|
+
found: string[];
|
|
33
|
+
added: PlacedText[][];
|
|
34
|
+
}
|
|
35
|
+
export interface WordMatch {
|
|
36
|
+
/** The original's runs, each followed by what was read there, then added words. */
|
|
37
|
+
alignedText: string;
|
|
38
|
+
/** The original's runs in order, as one text. */
|
|
39
|
+
expectedText: string;
|
|
40
|
+
differences: TextDifference[];
|
|
41
|
+
}
|
|
42
|
+
/** How one run reads back: agreeing, or which difference it is. */
|
|
43
|
+
export type Verdict = {
|
|
44
|
+
agrees: true;
|
|
45
|
+
} | {
|
|
46
|
+
agrees: false;
|
|
47
|
+
kind: 'changed' | 'missing';
|
|
48
|
+
reason: 'numbers' | 'text';
|
|
49
|
+
similarity: number;
|
|
50
|
+
};
|
|
51
|
+
/** A run that is only a figure: digits and the marks figures are written with. */
|
|
52
|
+
export declare const FIGURE: RegExp;
|
|
53
|
+
export declare function claimWords(references: readonly Reference[], words: readonly PlacedText[], lineOf: readonly number[], options: MatchOptions): Claims;
|
|
54
|
+
/**
|
|
55
|
+
* The rule every run is held to - on the page, and again when it is re-read on
|
|
56
|
+
* its own. A run that is only a figure agrees when its digits do, in order:
|
|
57
|
+
* OCR trades commas for full stops constantly, and `5,768,700.00` read as
|
|
58
|
+
* `5.768.700 00` is the same amount. Any other run must also keep its digits,
|
|
59
|
+
* and read within the similarity threshold.
|
|
60
|
+
*/
|
|
61
|
+
export declare function judgeRun(expected: string, found: string, options: MatchOptions): Verdict;
|
|
62
|
+
export declare function judgeRuns(references: readonly Reference[], claims: Claims, options: MatchOptions): WordMatch;
|
|
63
|
+
/** Claim and judge in one go, for a page read once. */
|
|
64
|
+
export declare function matchWords(references: readonly Reference[], words: readonly PlacedText[], lineOf: readonly number[], options: MatchOptions): WordMatch;
|
|
65
|
+
//# sourceMappingURL=match-words.use-case.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OcrOptions, OcrReport, ReadablePage } from './ocr-report.contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read every aligned page and say how closely the scan's text matches the
|
|
4
|
+
* original's - per page, and for the document.
|
|
5
|
+
*
|
|
6
|
+
* The scan is read from its enhanced image when there is one, otherwise from
|
|
7
|
+
* the aligned scan enlarged to `targetDpi`. The original comes from its text
|
|
8
|
+
* layer when it has one, which is exact, so every error counted is the scan's;
|
|
9
|
+
* otherwise it is read the same way. Both are then compared run by run of the
|
|
10
|
+
* original, on the shared canvas, rather than in whatever order the engine
|
|
11
|
+
* read them.
|
|
12
|
+
*
|
|
13
|
+
* One engine serves the whole call. Pass `engine` to share one across calls;
|
|
14
|
+
* it is then left running.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ocrPages<Page extends ReadablePage>(pages: readonly Page[], options?: OcrOptions): Promise<OcrReport>;
|
|
17
|
+
//# sourceMappingURL=ocr-pages.use-case.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { AlignedPage, PageImage, ProgressCallback } from '@scanmate/ink';
|
|
2
|
+
import type { OcrEngine, TesseractEngineOptions } from '../ocr-engine/index.js';
|
|
3
|
+
import type { RecheckOptions } from './recheck-run.use-case.js';
|
|
4
|
+
import type { NormaliseOptions } from '../text-normalisation/index.js';
|
|
5
|
+
import type { ScoreMetric, TextMetrics } from '../text-similarity/index.js';
|
|
6
|
+
/** A run of text placed on the page, in PDF points from the top-left - what `@scanmate/extract` calls a text item. */
|
|
7
|
+
export interface PositionedText {
|
|
8
|
+
text: string;
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
endsLine?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* What `ocrPages` reads: an aligned page, and optionally its enhanced image
|
|
17
|
+
* (`@scanmate/enhance`) and the original's text layer (`@scanmate/extract`).
|
|
18
|
+
* Only the original's text layer is ever used. A scan's own text layer is
|
|
19
|
+
* ignored, deliberately - hidden or stale text must not vouch for what the
|
|
20
|
+
* paper shows.
|
|
21
|
+
*/
|
|
22
|
+
export type ReadablePage = AlignedPage & {
|
|
23
|
+
enhanced?: PageImage;
|
|
24
|
+
metadata?: {
|
|
25
|
+
original?: {
|
|
26
|
+
textItems?: readonly PositionedText[] | null;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export interface OcrOptions {
|
|
31
|
+
/** An engine to use and leave running. Default: a tesseract engine made for this call and terminated after it. */
|
|
32
|
+
engine?: OcrEngine;
|
|
33
|
+
/** Settings for that default engine: languages, model, cache. */
|
|
34
|
+
tesseract?: TesseractEngineOptions;
|
|
35
|
+
/**
|
|
36
|
+
* The original's text: `'auto'` (the default) takes its text layer when it
|
|
37
|
+
* has one - exact, and no OCR errors on that side - and reads it otherwise;
|
|
38
|
+
* `'text-layer'` insists; `'ocr'` always reads it.
|
|
39
|
+
*/
|
|
40
|
+
original?: 'auto' | 'text-layer' | 'ocr';
|
|
41
|
+
/** Enlarge images below this before reading them. Default `300`; `null` reads them as they are. An `enhanced` image is read as it is. */
|
|
42
|
+
targetDpi?: number | null;
|
|
43
|
+
/** Resolution of a page that does not say. Default `150`. */
|
|
44
|
+
assumeDpi?: number;
|
|
45
|
+
normalise?: NormaliseOptions;
|
|
46
|
+
/** Which metric is a page's `score`. Default `'levenshteinSimilarity'`. */
|
|
47
|
+
scoreMetric?: ScoreMetric;
|
|
48
|
+
/** A run of the original read back below this similarity is a difference. Default `0.8`. */
|
|
49
|
+
matchThreshold?: number;
|
|
50
|
+
/** Scanned words not on any run of the original count as added only from this confidence. Default `60`. */
|
|
51
|
+
minWordConfidence?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Re-read each run the page reading doubted, cropped and enlarged, before
|
|
54
|
+
* calling it a difference; it is cleared only when several passes agree with
|
|
55
|
+
* the original. On by default, since a false difference costs a person's
|
|
56
|
+
* time; `false` reports the page reading as it is. See `recheckRun`.
|
|
57
|
+
*/
|
|
58
|
+
recheck?: RecheckOptions | false;
|
|
59
|
+
onProgress?: ProgressCallback;
|
|
60
|
+
}
|
|
61
|
+
/** A word or line, placed on the page in points from the top-left. */
|
|
62
|
+
export interface PlacedText {
|
|
63
|
+
text: string;
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
/** 0-100; `null` for the text layer, which is not a reading. */
|
|
69
|
+
confidence?: number | null;
|
|
70
|
+
}
|
|
71
|
+
export interface SideText {
|
|
72
|
+
source: 'text-layer' | 'ocr';
|
|
73
|
+
/** In the source's own order: the text layer's, or the engine's reading order. */
|
|
74
|
+
text: string;
|
|
75
|
+
/** 0-100 for OCR; `null` for a text layer. */
|
|
76
|
+
confidence: number | null;
|
|
77
|
+
lines: PlacedText[];
|
|
78
|
+
words: PlacedText[];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* - `changed`: a run of the original, read back as something else;
|
|
82
|
+
* - `missing`: a run of the original with nothing read where it should be;
|
|
83
|
+
* - `added`: words read where the original has none.
|
|
84
|
+
*/
|
|
85
|
+
export interface TextDifference {
|
|
86
|
+
kind: 'changed' | 'missing' | 'added';
|
|
87
|
+
expected: string | null;
|
|
88
|
+
found: string | null;
|
|
89
|
+
/** Normalised Levenshtein similarity of the two; `0` for missing or added. */
|
|
90
|
+
similarity: number;
|
|
91
|
+
/**
|
|
92
|
+
* Why a run counts as changed: its figures differ (`'numbers'`, held to an
|
|
93
|
+
* exact match, since one digit is the whole point), or its words fell below
|
|
94
|
+
* the match threshold (`'text'`). Always `'text'` for missing and added.
|
|
95
|
+
*/
|
|
96
|
+
reason: 'numbers' | 'text';
|
|
97
|
+
/** Where on the page, in points from the top-left. */
|
|
98
|
+
x: number;
|
|
99
|
+
y: number;
|
|
100
|
+
width: number;
|
|
101
|
+
height: number;
|
|
102
|
+
}
|
|
103
|
+
/** One run of the original, and what the scan reads in its place. */
|
|
104
|
+
export interface RunReading {
|
|
105
|
+
/** The original's text. */
|
|
106
|
+
text: string;
|
|
107
|
+
/** What was read there - by the page reading, or by the recheck that cleared it. */
|
|
108
|
+
found: string;
|
|
109
|
+
/** The reading agrees with the original, by the rule every run is judged by. */
|
|
110
|
+
agrees: boolean;
|
|
111
|
+
/** The run was re-read on its own. */
|
|
112
|
+
rechecked: boolean;
|
|
113
|
+
/** Where the original prints it, in points from the top-left. */
|
|
114
|
+
x: number;
|
|
115
|
+
y: number;
|
|
116
|
+
width: number;
|
|
117
|
+
height: number;
|
|
118
|
+
}
|
|
119
|
+
export interface PageOcr {
|
|
120
|
+
page: number;
|
|
121
|
+
original: SideText;
|
|
122
|
+
scanned: SideText;
|
|
123
|
+
/** Every run of the original with its reading, in the original's order - what a search for required content consults. */
|
|
124
|
+
runs: RunReading[];
|
|
125
|
+
/**
|
|
126
|
+
* The scan's words re-ordered to follow the original: each run of the
|
|
127
|
+
* original followed by what was read in its place, then any added words.
|
|
128
|
+
* This, not the engine's reading order, is what the metrics compare - so a
|
|
129
|
+
* two-column page read column-wise is not a page of errors.
|
|
130
|
+
*/
|
|
131
|
+
alignedText: string;
|
|
132
|
+
score: number;
|
|
133
|
+
metrics: TextMetrics;
|
|
134
|
+
differences: TextDifference[];
|
|
135
|
+
/** Runs the page reading doubted and re-read on their own, and how many of them the re-reading cleared. */
|
|
136
|
+
rechecks: {
|
|
137
|
+
attempted: number;
|
|
138
|
+
cleared: number;
|
|
139
|
+
};
|
|
140
|
+
warnings: string[];
|
|
141
|
+
}
|
|
142
|
+
export interface OcrReport {
|
|
143
|
+
/** Page scores weighted by each page's expected characters - a three-word page does not outvote a dense one. */
|
|
144
|
+
score: number;
|
|
145
|
+
/** Unweighted mean of page scores; the two disagreeing says the short pages read differently. */
|
|
146
|
+
pageMean: number;
|
|
147
|
+
pages: PageOcr[];
|
|
148
|
+
engine: {
|
|
149
|
+
name: string;
|
|
150
|
+
version: string;
|
|
151
|
+
languages: readonly string[];
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=ocr-report.contract.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Raster } from '@scanmate/ink';
|
|
2
|
+
import type { OcrEngine } from '../ocr-engine/index.js';
|
|
3
|
+
import type { MatchOptions, Reference } from './match-words.use-case.js';
|
|
4
|
+
/**
|
|
5
|
+
* A second look at one run the page reading got wrong, before calling it a
|
|
6
|
+
* difference.
|
|
7
|
+
*
|
|
8
|
+
* Read as part of a whole page, a short run - a figure in a table, a word on
|
|
9
|
+
* a shaded bar - is at the mercy of the engine's layout analysis and of one
|
|
10
|
+
* resolution. Cropped to itself, enlarged further, read as a single line (or
|
|
11
|
+
* word, or as digits only when it is a figure), light-on-dark text inverted,
|
|
12
|
+
* contrast stretched, it often reads cleanly. Measured on real scans at 120
|
|
13
|
+
* and 144 dpi, cropped multi-pass reading confirmed 96-97% of every printed
|
|
14
|
+
* value on the page; at 93 dpi, a third.
|
|
15
|
+
*
|
|
16
|
+
* The danger is the opposite error: try enough readings and one may land on
|
|
17
|
+
* the expected text by chance - on a 93-dpi scan a single agreeing pass
|
|
18
|
+
* accepted 4 of 61 forged digits. So a run is cleared only when `agree`
|
|
19
|
+
* separate passes (two by default) read it as the original prints it, by the
|
|
20
|
+
* same rule the page is judged by: figures exactly, words within the threshold.
|
|
21
|
+
*/
|
|
22
|
+
export interface RecheckPass {
|
|
23
|
+
/** Resolution to enlarge the crop to; a crop already finer is not reduced. */
|
|
24
|
+
dpi: number;
|
|
25
|
+
/** Read the crop as one line of text, or as one word. */
|
|
26
|
+
layout: 'line' | 'word';
|
|
27
|
+
/** Stretch the crop's contrast to full black and white first. */
|
|
28
|
+
stretch?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface RecheckOptions {
|
|
31
|
+
/** The readings to try, in order. Default {@link DEFAULT_RECHECK_PASSES}. */
|
|
32
|
+
passes?: readonly RecheckPass[];
|
|
33
|
+
/** Passes that must agree with the original to clear a run. Default `2`. */
|
|
34
|
+
agree?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const DEFAULT_RECHECK_PASSES: readonly RecheckPass[];
|
|
37
|
+
export interface Recheck {
|
|
38
|
+
cleared: boolean;
|
|
39
|
+
/** An agreeing reading when cleared; `null` otherwise. */
|
|
40
|
+
reading: string | null;
|
|
41
|
+
/** Passes tried, and how many agreed. */
|
|
42
|
+
passes: number;
|
|
43
|
+
agreed: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function recheckRun(engine: OcrEngine, image: {
|
|
46
|
+
raster: Raster;
|
|
47
|
+
dpi: number;
|
|
48
|
+
}, run: Reference, options: MatchOptions & RecheckOptions): Promise<Recheck>;
|
|
49
|
+
//# sourceMappingURL=recheck-run.use-case.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characters and pairs OCR mistakes for one another, folded to one form each.
|
|
3
|
+
*
|
|
4
|
+
* Off by default, and deliberately so: `0` for `o` and `5` for `s` are OCR's
|
|
5
|
+
* commonest errors, and the very substitutions someone altering an amount or a
|
|
6
|
+
* reference would make. Folding them raises a score by hiding exactly what a
|
|
7
|
+
* check is for. Use it to compare prose, never figures.
|
|
8
|
+
*
|
|
9
|
+
* Applied after case folding, to lower-case text.
|
|
10
|
+
*/
|
|
11
|
+
export declare function foldConfusables(text: string): string;
|
|
12
|
+
//# sourceMappingURL=confusables.mapper.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Letters with diacritics, ligatures and look-alike forms, folded to plain
|
|
3
|
+
* Latin: `é` to `e`, `Æ` to `AE`, a circled or full-width `A` to `A`.
|
|
4
|
+
*
|
|
5
|
+
* OCR drops and invents accents - a scanned `é` reads as `e` or `è` as the
|
|
6
|
+
* print allows - so text is compared with them folded away. Some bases are two
|
|
7
|
+
* letters, so folding can lengthen a string. The table is the project's own
|
|
8
|
+
* diacritics map (86 bases, 874 letters), kept as escapes so every code point
|
|
9
|
+
* is visible in review; it is expanded into a lookup once, on first use.
|
|
10
|
+
*/
|
|
11
|
+
/** Every folded letter and what it folds to. */
|
|
12
|
+
export declare function diacriticsMap(): ReadonlyMap<string, string>;
|
|
13
|
+
/** Replace every letter in the table by its plain base; everything else is kept as it is. */
|
|
14
|
+
export declare function foldDiacritics(text: string): string;
|
|
15
|
+
//# sourceMappingURL=diacritics.mapper.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Making OCR text and a document's own text comparable: the noise OCR adds, removed. */
|
|
2
|
+
export { foldConfusables } from './confusables.mapper.js';
|
|
3
|
+
export { diacriticsMap, foldDiacritics } from './diacritics.mapper.js';
|
|
4
|
+
export { DEFAULT_NORMALISE, normaliseText, tokenise } from './normalise-text.use-case.js';
|
|
5
|
+
export type { NormaliseOptions } from './normalise-text.use-case.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text made comparable: the differences OCR introduces that say nothing about
|
|
3
|
+
* the document are removed, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Each step is a judgement about which noise to forgive, so each can be turned
|
|
6
|
+
* off. In order:
|
|
7
|
+
*
|
|
8
|
+
* 1. Unicode NFKC - ligatures (`fi`), full-width and compatibility forms.
|
|
9
|
+
* 2. Typography - curly quotes, dashes, ellipses and odd spaces to plain ASCII.
|
|
10
|
+
* 3. De-hyphenation - `informa-` + newline + `tion` is `information`, or every
|
|
11
|
+
* wrapped word counts as an edit.
|
|
12
|
+
* 4. Diacritics - `é` to `e`, `Æ` to `AE`.
|
|
13
|
+
* 5. Case.
|
|
14
|
+
* 6. Noise - tokens with no letter, digit or currency sign: table rules read
|
|
15
|
+
* as `|`, underlines as `___`, specks as `.` or `~`.
|
|
16
|
+
* 7. Punctuation (off by default) - it carries real signal: `1,250.00` is not
|
|
17
|
+
* `125000`.
|
|
18
|
+
* 8. OCR confusables (off by default) - `0/o`, `1/l/i`, `5/s`, `rn/m`. Folding
|
|
19
|
+
* them forgives exactly the substitutions a forger would make.
|
|
20
|
+
* 9. Whitespace collapsed and trimmed.
|
|
21
|
+
*/
|
|
22
|
+
export interface NormaliseOptions {
|
|
23
|
+
nfkc?: boolean;
|
|
24
|
+
typography?: boolean;
|
|
25
|
+
dehyphenate?: boolean;
|
|
26
|
+
diacritics?: boolean;
|
|
27
|
+
caseFold?: boolean;
|
|
28
|
+
dropNoise?: boolean;
|
|
29
|
+
stripPunctuation?: boolean;
|
|
30
|
+
confusables?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare const DEFAULT_NORMALISE: Required<NormaliseOptions>;
|
|
33
|
+
export declare function normaliseText(text: string, options?: NormaliseOptions): string;
|
|
34
|
+
/** Normalised words, in order. */
|
|
35
|
+
export declare function tokenise(text: string, options?: NormaliseOptions): string[];
|
|
36
|
+
//# sourceMappingURL=normalise-text.use-case.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NormaliseOptions } from '../text-normalisation/index.js';
|
|
2
|
+
/** Every measure of how alike an expected text and an actual one are, after normalisation. */
|
|
3
|
+
export interface TextMetrics {
|
|
4
|
+
/** Characters of edit between the normalised texts. */
|
|
5
|
+
levenshtein: number;
|
|
6
|
+
/** `1 - levenshtein / longer length`. */
|
|
7
|
+
levenshteinSimilarity: number;
|
|
8
|
+
/** Share of distinct words in common. */
|
|
9
|
+
jaccard: number;
|
|
10
|
+
/** Character-bigram overlap. */
|
|
11
|
+
dice: number;
|
|
12
|
+
/** Word-frequency cosine. */
|
|
13
|
+
cosine: number;
|
|
14
|
+
/** Only meaningful on short strings; reported for completeness. */
|
|
15
|
+
jaroWinkler: number;
|
|
16
|
+
/** Character error rate: edits over the expected length. Can exceed 1. */
|
|
17
|
+
characterErrorRate: number;
|
|
18
|
+
/** Word error rate: word edits over the expected word count. Can exceed 1. */
|
|
19
|
+
wordErrorRate: number;
|
|
20
|
+
/** Share of expected words present, each used once. */
|
|
21
|
+
wordRecall: number;
|
|
22
|
+
/** Actual length over expected length - near 0 when a page read as nothing. */
|
|
23
|
+
lengthRatio: number;
|
|
24
|
+
/** Normalised expected length, in characters: the weight this text carries in a total. */
|
|
25
|
+
characters: number;
|
|
26
|
+
}
|
|
27
|
+
export type ScoreMetric = 'levenshteinSimilarity' | 'wordRecall' | 'jaccard' | 'dice' | 'cosine';
|
|
28
|
+
/** Normalise both texts the same way, then measure them every way. */
|
|
29
|
+
export declare function compareTexts(expected: string, actual: string, options?: NormaliseOptions): TextMetrics;
|
|
30
|
+
//# sourceMappingURL=compare-texts.use-case.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** How alike two texts are, measured the several ways they can differ. */
|
|
2
|
+
export { compareTexts } from './compare-texts.use-case.js';
|
|
3
|
+
export type { ScoreMetric, TextMetrics } from './compare-texts.use-case.js';
|
|
4
|
+
export { cosine, dice, jaccard, jaroWinkler, levenshtein, levenshteinSimilarity, wordDistance, wordRecall } from './similarity-metrics.use-case.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ways to say how alike two texts are. They fail differently, which is why
|
|
3
|
+
* several are reported rather than one:
|
|
4
|
+
*
|
|
5
|
+
* - edit distance counts every insertion, deletion and substitution, so it
|
|
6
|
+
* notices a changed digit - and punishes text that reflowed;
|
|
7
|
+
* - set and bag measures (Jaccard, Dice, cosine, recall) ignore order, so they
|
|
8
|
+
* survive reflow and a reading order OCR got wrong - and miss a swapped clause;
|
|
9
|
+
* - character and word error rates are what anyone who works with OCR expects
|
|
10
|
+
* to see;
|
|
11
|
+
* - Jaro-Winkler is for short strings - names, references - and meaningless on
|
|
12
|
+
* a page.
|
|
13
|
+
*
|
|
14
|
+
* All take text already normalised; similarities run from 0 to 1.
|
|
15
|
+
*/
|
|
16
|
+
/** Characters inserted, deleted or substituted to turn one text into the other (Myers' bit-parallel algorithm). */
|
|
17
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
18
|
+
/** `1 - distance / longer length`: 1 for identical, 0 for nothing in common. */
|
|
19
|
+
export declare function levenshteinSimilarity(a: string, b: string): number;
|
|
20
|
+
/** Words inserted, deleted or substituted, as edit distance over words rather than characters. */
|
|
21
|
+
export declare function wordDistance(a: readonly string[], b: readonly string[]): number;
|
|
22
|
+
/** Shared distinct words over all distinct words. */
|
|
23
|
+
export declare function jaccard(a: readonly string[], b: readonly string[]): number;
|
|
24
|
+
/** Sørensen-Dice over character bigrams, counted with multiplicity. */
|
|
25
|
+
export declare function dice(a: string, b: string): number;
|
|
26
|
+
/** Cosine of the two word-frequency vectors. */
|
|
27
|
+
export declare function cosine(a: readonly string[], b: readonly string[]): number;
|
|
28
|
+
/** Share of the expected words found, each found word used once - the question "is what should be there, there?". */
|
|
29
|
+
export declare function wordRecall(expected: readonly string[], actual: readonly string[]): number;
|
|
30
|
+
/**
|
|
31
|
+
* Jaro-Winkler: characters matched within a window, transpositions counted, a
|
|
32
|
+
* bonus for a shared prefix of up to four. Tuned for short strings.
|
|
33
|
+
*/
|
|
34
|
+
export declare function jaroWinkler(a: string, b: string, prefixScale?: number): number;
|
|
35
|
+
//# sourceMappingURL=similarity-metrics.use-case.d.ts.map
|