@stabrise/scaledp 0.1.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/LICENSE +661 -0
- package/README.md +218 -0
- package/dist/box-DAfzwfhA.d.ts +119 -0
- package/dist/config-g6IrKlDC.d.ts +80 -0
- package/dist/data-to-image-DoZ4jQ3R.js +54 -0
- package/dist/data-to-image-DoZ4jQ3R.js.map +1 -0
- package/dist/detect/index.d.ts +71 -0
- package/dist/detect/index.js +2 -0
- package/dist/detect-q8AI_Jdj.js +274 -0
- package/dist/detect-q8AI_Jdj.js.map +1 -0
- package/dist/detector-output-C0Qt-jEq.d.ts +13 -0
- package/dist/detector-output-lyF1Mqb8.js +13 -0
- package/dist/detector-output-lyF1Mqb8.js.map +1 -0
- package/dist/display/index.d.ts +66 -0
- package/dist/display/index.js +237 -0
- package/dist/display/index.js.map +1 -0
- package/dist/document-B8I61TiY.d.ts +16 -0
- package/dist/entity-CedtRhU1.d.ts +22 -0
- package/dist/entity-D6Hxaugj.js +13 -0
- package/dist/entity-D6Hxaugj.js.map +1 -0
- package/dist/image-CAH2rLv9.js +511 -0
- package/dist/image-CAH2rLv9.js.map +1 -0
- package/dist/image-Dc5TSg46.d.ts +18 -0
- package/dist/image-DoZDJkcR.js +37 -0
- package/dist/image-DoZDJkcR.js.map +1 -0
- package/dist/image-draw-boxes-De0QbFv9.js +285 -0
- package/dist/image-draw-boxes-De0QbFv9.js.map +1 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.js +11 -0
- package/dist/model-cache-BEaqqRZ9.js +182 -0
- package/dist/model-cache-BEaqqRZ9.js.map +1 -0
- package/dist/model-cache-BhFYpfZz.d.ts +36 -0
- package/dist/ner/index.d.ts +293 -0
- package/dist/ner/index.js +2 -0
- package/dist/ner-SsZLZ6ed.js +1028 -0
- package/dist/ner-SsZLZ6ed.js.map +1 -0
- package/dist/ocr/index.d.ts +440 -0
- package/dist/ocr/index.js +3 -0
- package/dist/ocr-OHX2WM3e.js +1294 -0
- package/dist/ocr-OHX2WM3e.js.map +1 -0
- package/dist/ort-CXDoPrtw.js +73 -0
- package/dist/ort-CXDoPrtw.js.map +1 -0
- package/dist/params-DapwK9Ns.js +37 -0
- package/dist/params-DapwK9Ns.js.map +1 -0
- package/dist/pdf/index.d.ts +123 -0
- package/dist/pdf/index.js +2 -0
- package/dist/pdf-BQl0dneD.js +417 -0
- package/dist/pdf-BQl0dneD.js.map +1 -0
- package/dist/pipeline-DACqGkpN.js +240 -0
- package/dist/pipeline-DACqGkpN.js.map +1 -0
- package/dist/pipeline-DeLO-OCE.d.ts +139 -0
- package/dist/registry/index.d.ts +169 -0
- package/dist/registry/index.js +1061 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/text-ahMLpxN9.js +109 -0
- package/dist/text-ahMLpxN9.js.map +1 -0
- package/dist/worker/index.d.ts +105 -0
- package/dist/worker/index.js +180 -0
- package/dist/worker/index.js.map +1 -0
- package/package.json +135 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
import { n as Pipeline } from "../pipeline-DACqGkpN.js";
|
|
2
|
+
import { n as DataToImage, t as DATA_TO_IMAGE_DEFAULTS } from "../data-to-image-DoZ4jQ3R.js";
|
|
3
|
+
import { a as ImageCropBoxes, i as IMAGE_CROP_BOXES_DEFAULTS, n as ImageDrawBoxes, t as IMAGE_DRAW_BOXES_DEFAULTS } from "../image-draw-boxes-De0QbFv9.js";
|
|
4
|
+
import { d as PdfToImage, l as PDF_TO_IMAGE_DEFAULTS, n as PdfToDocument, t as PDF_TO_DOCUMENT_DEFAULTS } from "../pdf-BQl0dneD.js";
|
|
5
|
+
import { C as LINE_ORIENTATION_DEFAULTS, P as DbnetOnnxDetector, T as DEFAULT_ORIENTATION_MODEL, a as TesseractOcr, b as PADDLE_OCR_PRESETS, d as PADDLE_RECOGNIZER_DEFAULTS, f as PaddleTextDetector, i as TESSERACT_OCR_DEFAULTS, j as DBNET_DETECTOR_DEFAULTS, k as DETECTOR_MODELS, n as TesseractRecognizer, p as PaddleTextRecognizer, t as TESSERACT_RECOGNIZER_DEFAULTS, u as PADDLE_DETECTOR_DEFAULTS, w as LineOrientationDetector } from "../ocr-OHX2WM3e.js";
|
|
6
|
+
import { d as NER_MODELS, n as GlinerNer, p as modelSizeBytes, t as GLINER_NER_DEFAULTS } from "../ner-SsZLZ6ed.js";
|
|
7
|
+
import { a as YoloOnnxDetector, i as YOLO_DETECTOR_DEFAULTS, n as SignatureDetector, t as FaceDetector } from "../detect-q8AI_Jdj.js";
|
|
8
|
+
//#region src/registry/catalog.ts
|
|
9
|
+
/**
|
|
10
|
+
* The stage catalogue: one `StageSpec` per concrete stage.
|
|
11
|
+
*
|
|
12
|
+
* Transcribed from `docs/stages.md`, which is the same table in prose. Every
|
|
13
|
+
* `defaults` entry is the stage's own frozen constant rather than a copy, and
|
|
14
|
+
* every enum's options come from the model registries that already exist
|
|
15
|
+
* (`PADDLE_OCR_PRESETS`, `NER_MODELS`, `DETECTOR_MODELS`) rather than a second
|
|
16
|
+
* hand-maintained list.
|
|
17
|
+
*
|
|
18
|
+
* Importing this module costs no ML runtime: every engine in the library is
|
|
19
|
+
* reached through a dynamic `import()` inside `ocr/ort.ts`, `pdf/pdfjs.ts`,
|
|
20
|
+
* `ocr/paddle-service.ts` and `ocr/tesseract.ts`, so pulling in the twelve stage
|
|
21
|
+
* classes pulls in no `onnxruntime-web`, `pdfjs-dist`, `ppu-paddle-ocr` or
|
|
22
|
+
* `tesseract-wasm`. It does pull in every stage's code, which is why this lives
|
|
23
|
+
* behind its own subpath instead of the root barrel.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Widen a stage's typed defaults into an indexable record.
|
|
27
|
+
*
|
|
28
|
+
* `*_DEFAULTS` are typed as their stage's params interface, and an interface has
|
|
29
|
+
* no index signature -- so it is not assignable to `Record<string, unknown>`
|
|
30
|
+
* even though every key is a string. The values are frozen and read-only either
|
|
31
|
+
* way; only the type changes.
|
|
32
|
+
*/
|
|
33
|
+
const asRecord = (defaults) => defaults;
|
|
34
|
+
const IMAGE_TYPES = Object.freeze([
|
|
35
|
+
{
|
|
36
|
+
value: "png",
|
|
37
|
+
label: "PNG",
|
|
38
|
+
title: "Lossless; the safe default for OCR"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: "webp",
|
|
42
|
+
label: "WebP",
|
|
43
|
+
title: "Smaller, lossy"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
value: "jpeg",
|
|
47
|
+
label: "JPEG",
|
|
48
|
+
title: "Smallest, lossy; artefacts can cost accuracy"
|
|
49
|
+
}
|
|
50
|
+
]);
|
|
51
|
+
const STRATEGIES = Object.freeze([
|
|
52
|
+
{
|
|
53
|
+
value: "per-box",
|
|
54
|
+
label: "Per box",
|
|
55
|
+
title: "Word-level boxes, as detected"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: "per-line",
|
|
59
|
+
label: "Per line",
|
|
60
|
+
title: "Merge boxes that share a line"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: "cross-line",
|
|
64
|
+
label: "Cross line",
|
|
65
|
+
title: "Merge across line breaks too"
|
|
66
|
+
}
|
|
67
|
+
]);
|
|
68
|
+
/**
|
|
69
|
+
* Fields `ImageDrawBoxes` can render above a box.
|
|
70
|
+
*
|
|
71
|
+
* The label is built by reading these off the box or entity, so the valid names
|
|
72
|
+
* are the schema's own field names -- `Box` for a detector or OCR result,
|
|
73
|
+
* `Entity` for NER output. Numbers are formatted to two decimals.
|
|
74
|
+
*/
|
|
75
|
+
const LABEL_FIELDS = Object.freeze([
|
|
76
|
+
{
|
|
77
|
+
value: "text",
|
|
78
|
+
label: "text",
|
|
79
|
+
title: "Box: the recognised text"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
value: "score",
|
|
83
|
+
label: "score",
|
|
84
|
+
title: "Box or entity: confidence, 0-1"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
value: "angle",
|
|
88
|
+
label: "angle",
|
|
89
|
+
title: "Box: degrees about its centre"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
value: "entity_group",
|
|
93
|
+
label: "entity_group",
|
|
94
|
+
title: "Entity: the label it matched"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
value: "word",
|
|
98
|
+
label: "word",
|
|
99
|
+
title: "Entity: the matched text"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
value: "x",
|
|
103
|
+
label: "x",
|
|
104
|
+
title: "Box: top-left x"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
value: "y",
|
|
108
|
+
label: "y",
|
|
109
|
+
title: "Box: top-left y"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
value: "width",
|
|
113
|
+
label: "width",
|
|
114
|
+
title: "Box: the longer side"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
value: "height",
|
|
118
|
+
label: "height",
|
|
119
|
+
title: "Box: the shorter side"
|
|
120
|
+
}
|
|
121
|
+
]);
|
|
122
|
+
const OCR_PRESETS = Object.freeze(PADDLE_OCR_PRESETS.map((preset) => ({
|
|
123
|
+
value: preset.value,
|
|
124
|
+
label: preset.label,
|
|
125
|
+
title: preset.scripts.join(", ")
|
|
126
|
+
})));
|
|
127
|
+
const NER_MODEL_OPTIONS = Object.freeze(NER_MODELS.map((model) => ({
|
|
128
|
+
value: model.id,
|
|
129
|
+
label: `${model.name} · ${model.languages.join("/")}${model.private ? " · private" : ""}`,
|
|
130
|
+
title: `${model.arch}, ${Math.round(modelSizeBytes(model) / 1e6)} MB, ${model.repo}`,
|
|
131
|
+
disabled: model.private === true
|
|
132
|
+
})));
|
|
133
|
+
const DBNET_MODEL_OPTIONS = Object.freeze(DETECTOR_MODELS.filter((model) => model.kind === "dbnet-onnx" && model.repo).map((model) => ({
|
|
134
|
+
value: model.repo,
|
|
135
|
+
label: model.name,
|
|
136
|
+
title: model.notes
|
|
137
|
+
})));
|
|
138
|
+
const column = (key, label, extra = {}) => ({
|
|
139
|
+
key,
|
|
140
|
+
kind: "column",
|
|
141
|
+
label,
|
|
142
|
+
...extra
|
|
143
|
+
});
|
|
144
|
+
/**
|
|
145
|
+
* The six params every stage inherits from `BaseStageParams`.
|
|
146
|
+
*
|
|
147
|
+
* `inputCol` and `outputCol` are the wiring a builder actually cares about;
|
|
148
|
+
* the rest is plumbing, so it is marked advanced. Stages that read several
|
|
149
|
+
* columns pass `multiInput` -- there `inputCol` is inherited but unused, and
|
|
150
|
+
* showing it as the input would be a lie.
|
|
151
|
+
*/
|
|
152
|
+
function baseParams(options = {}) {
|
|
153
|
+
return [
|
|
154
|
+
options.input === "unused" ? column("inputCol", "Input column", {
|
|
155
|
+
advanced: true,
|
|
156
|
+
help: "Inherited but unused: this stage reads the columns listed in inputCols."
|
|
157
|
+
}) : column("inputCol", "Input column", {
|
|
158
|
+
help: options.input?.help,
|
|
159
|
+
accepts: options.input?.accepts
|
|
160
|
+
}),
|
|
161
|
+
column("outputCol", "Output column", { help: options.output?.help }),
|
|
162
|
+
column("pathCol", "Path column", {
|
|
163
|
+
advanced: true,
|
|
164
|
+
help: "Row field holding the source path, copied onto the output."
|
|
165
|
+
}),
|
|
166
|
+
column("pageCol", "Page column", {
|
|
167
|
+
advanced: true,
|
|
168
|
+
help: "Row field holding the page index for multi-page inputs."
|
|
169
|
+
}),
|
|
170
|
+
{
|
|
171
|
+
key: "keepInputData",
|
|
172
|
+
kind: "boolean",
|
|
173
|
+
label: "Keep input column",
|
|
174
|
+
advanced: true,
|
|
175
|
+
help: "Keep the input column in the output rows instead of dropping it."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
key: "propagateError",
|
|
179
|
+
kind: "boolean",
|
|
180
|
+
label: "Throw on error",
|
|
181
|
+
advanced: true,
|
|
182
|
+
help: "Throw instead of recording the failure in the output’s exception field."
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
const imageType = {
|
|
187
|
+
key: "imageType",
|
|
188
|
+
kind: "enum",
|
|
189
|
+
label: "Image format",
|
|
190
|
+
options: IMAGE_TYPES
|
|
191
|
+
};
|
|
192
|
+
const scoreThreshold = (help) => ({
|
|
193
|
+
key: "scoreThreshold",
|
|
194
|
+
kind: "number",
|
|
195
|
+
label: "Score threshold",
|
|
196
|
+
min: 0,
|
|
197
|
+
max: 1,
|
|
198
|
+
step: .05,
|
|
199
|
+
help
|
|
200
|
+
});
|
|
201
|
+
const lang = {
|
|
202
|
+
key: "lang",
|
|
203
|
+
kind: "stringList",
|
|
204
|
+
label: "Languages",
|
|
205
|
+
required: true,
|
|
206
|
+
help: "Tesseract language codes, e.g. eng or eng, deu. Each needs its traineddata file."
|
|
207
|
+
};
|
|
208
|
+
const keepFormatting = {
|
|
209
|
+
key: "keepFormatting",
|
|
210
|
+
kind: "boolean",
|
|
211
|
+
label: "Keep formatting",
|
|
212
|
+
help: "Rebuild the original layout with spaces and blank lines."
|
|
213
|
+
};
|
|
214
|
+
const lineTolerance = {
|
|
215
|
+
key: "lineTolerance",
|
|
216
|
+
kind: "number",
|
|
217
|
+
label: "Line tolerance",
|
|
218
|
+
min: 0,
|
|
219
|
+
step: 1,
|
|
220
|
+
help: "Line-grouping tolerance in pixels; 0 derives it from character height."
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* `SignatureDetector` and `FaceDetector` are `YoloOnnxDetector` with different
|
|
224
|
+
* pre-set params (`src/detect/index.ts`), so they take the same parameters.
|
|
225
|
+
*
|
|
226
|
+
* One caveat those two carry: they spread their options straight into `super`
|
|
227
|
+
* rather than going through `resolveParams`, so their validators never run --
|
|
228
|
+
* an empty `model` is accepted there and fails later, at init.
|
|
229
|
+
*/
|
|
230
|
+
const yoloParams = Object.freeze([
|
|
231
|
+
...baseParams({ input: { accepts: ["image"] } }),
|
|
232
|
+
{
|
|
233
|
+
key: "model",
|
|
234
|
+
kind: "string",
|
|
235
|
+
label: "Model",
|
|
236
|
+
required: true,
|
|
237
|
+
help: "Hugging Face repo id, or a URL when self-hosting. No default."
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
key: "labels",
|
|
241
|
+
kind: "stringList",
|
|
242
|
+
label: "Labels",
|
|
243
|
+
help: "Class index → label. Empty falls back to class_<n>."
|
|
244
|
+
},
|
|
245
|
+
scoreThreshold("Drop detections below this confidence."),
|
|
246
|
+
{
|
|
247
|
+
key: "iouThreshold",
|
|
248
|
+
kind: "number",
|
|
249
|
+
label: "IoU threshold",
|
|
250
|
+
min: 0,
|
|
251
|
+
max: 1,
|
|
252
|
+
step: .05,
|
|
253
|
+
help: "Overlap above which two same-class boxes count as duplicates."
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: "padding",
|
|
257
|
+
kind: "number",
|
|
258
|
+
label: "Padding",
|
|
259
|
+
min: 0,
|
|
260
|
+
max: 1,
|
|
261
|
+
step: .01,
|
|
262
|
+
help: "Grow each box by this fraction of its size, to avoid clipping edges."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
key: "outputType",
|
|
266
|
+
kind: "string",
|
|
267
|
+
label: "Output type",
|
|
268
|
+
advanced: true,
|
|
269
|
+
help: "Recorded as DetectorOutput.type."
|
|
270
|
+
}
|
|
271
|
+
]);
|
|
272
|
+
const STAGE_SPECS = Object.freeze([
|
|
273
|
+
{
|
|
274
|
+
type: "DataToImage",
|
|
275
|
+
label: "Image bytes",
|
|
276
|
+
group: "Read",
|
|
277
|
+
subpath: "@stabrise/scaledp",
|
|
278
|
+
summary: "Wrap raw image bytes as an Image without re-encoding them.",
|
|
279
|
+
consumes: ["bytes"],
|
|
280
|
+
produces: "image",
|
|
281
|
+
defaults: asRecord(DATA_TO_IMAGE_DEFAULTS),
|
|
282
|
+
params: [
|
|
283
|
+
...baseParams({ input: { help: "Raw file bytes, normally the content column." } }),
|
|
284
|
+
{
|
|
285
|
+
...imageType,
|
|
286
|
+
help: "Encoding recorded on the output; the bytes pass through unchanged."
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
key: "resolution",
|
|
290
|
+
kind: "number",
|
|
291
|
+
label: "Resolution",
|
|
292
|
+
min: 0,
|
|
293
|
+
step: 1,
|
|
294
|
+
help: "DPI to record when the row carries no resolution field. 0 leaves it unknown."
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
type: "PdfToImage",
|
|
300
|
+
label: "PDF pages",
|
|
301
|
+
group: "Read",
|
|
302
|
+
subpath: "@stabrise/scaledp/pdf",
|
|
303
|
+
summary: "Rasterise every page of a PDF, one output row per page.",
|
|
304
|
+
consumes: ["bytes"],
|
|
305
|
+
produces: "image",
|
|
306
|
+
peer: "pdfjs-dist",
|
|
307
|
+
expands: true,
|
|
308
|
+
defaults: asRecord(PDF_TO_IMAGE_DEFAULTS),
|
|
309
|
+
params: [
|
|
310
|
+
...baseParams({ input: { help: "Raw PDF bytes, normally the content column." } }),
|
|
311
|
+
{
|
|
312
|
+
key: "resolution",
|
|
313
|
+
kind: "number",
|
|
314
|
+
label: "Resolution (DPI)",
|
|
315
|
+
min: 36,
|
|
316
|
+
max: 600,
|
|
317
|
+
step: 1,
|
|
318
|
+
help: "Render DPI. 300 matches ScaleDP; 200 is a good speed/accuracy trade."
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
key: "pageLimit",
|
|
322
|
+
kind: "number",
|
|
323
|
+
label: "Page limit",
|
|
324
|
+
min: 0,
|
|
325
|
+
step: 1,
|
|
326
|
+
help: "0 renders every page."
|
|
327
|
+
},
|
|
328
|
+
imageType
|
|
329
|
+
]
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
type: "PdfToDocument",
|
|
333
|
+
label: "PDF text layer",
|
|
334
|
+
group: "Read",
|
|
335
|
+
subpath: "@stabrise/scaledp/pdf",
|
|
336
|
+
summary: "Read a PDF’s embedded text layer, skipping OCR entirely.",
|
|
337
|
+
consumes: ["bytes"],
|
|
338
|
+
produces: "document",
|
|
339
|
+
peer: "pdfjs-dist",
|
|
340
|
+
expands: true,
|
|
341
|
+
defaults: asRecord(PDF_TO_DOCUMENT_DEFAULTS),
|
|
342
|
+
params: [
|
|
343
|
+
...baseParams({ input: { help: "Raw PDF bytes, normally the content column." } }),
|
|
344
|
+
{
|
|
345
|
+
key: "resolution",
|
|
346
|
+
kind: "number",
|
|
347
|
+
label: "Resolution (DPI)",
|
|
348
|
+
min: 36,
|
|
349
|
+
max: 600,
|
|
350
|
+
step: 1,
|
|
351
|
+
help: "Pixel space the boxes are expressed in. Match PdfToImage to align them."
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "pageLimit",
|
|
355
|
+
kind: "number",
|
|
356
|
+
label: "Page limit",
|
|
357
|
+
min: 0,
|
|
358
|
+
step: 1,
|
|
359
|
+
help: "0 reads every page."
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
key: "splitWords",
|
|
363
|
+
kind: "boolean",
|
|
364
|
+
label: "Split words",
|
|
365
|
+
help: "Split pdf.js line runs into word boxes. Off yields run-level boxes."
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
type: "PaddleTextDetector",
|
|
371
|
+
label: "PaddleOCR detector",
|
|
372
|
+
group: "Detect",
|
|
373
|
+
subpath: "@stabrise/scaledp/ocr",
|
|
374
|
+
summary: "Find text regions with the detection half of a PaddleOCR preset. Regions are line-level.",
|
|
375
|
+
consumes: ["image"],
|
|
376
|
+
produces: "boxes",
|
|
377
|
+
peer: "ppu-paddle-ocr",
|
|
378
|
+
cache: {
|
|
379
|
+
kind: "paddle-preset",
|
|
380
|
+
param: "preset"
|
|
381
|
+
},
|
|
382
|
+
defaults: asRecord(PADDLE_DETECTOR_DEFAULTS),
|
|
383
|
+
params: [
|
|
384
|
+
...baseParams({ input: { accepts: ["image"] } }),
|
|
385
|
+
{
|
|
386
|
+
key: "preset",
|
|
387
|
+
kind: "enum",
|
|
388
|
+
label: "Preset",
|
|
389
|
+
options: OCR_PRESETS,
|
|
390
|
+
help: "Language/script pairing. Detection and recognition share the download."
|
|
391
|
+
},
|
|
392
|
+
scoreThreshold("Drop regions below this confidence. 0 keeps everything.")
|
|
393
|
+
]
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
type: "DbnetOnnxDetector",
|
|
397
|
+
label: "DBNet detector",
|
|
398
|
+
group: "Detect",
|
|
399
|
+
subpath: "@stabrise/scaledp/ocr",
|
|
400
|
+
summary: "The DBNet ONNX detector ScaleDP uses server-side. Finds rotated regions, one box per text line.",
|
|
401
|
+
consumes: ["image"],
|
|
402
|
+
produces: "boxes",
|
|
403
|
+
peer: "onnxruntime-web",
|
|
404
|
+
cache: {
|
|
405
|
+
kind: "hf-repo",
|
|
406
|
+
param: "model",
|
|
407
|
+
approxBytes: 48e5
|
|
408
|
+
},
|
|
409
|
+
defaults: asRecord(DBNET_DETECTOR_DEFAULTS),
|
|
410
|
+
params: [
|
|
411
|
+
...baseParams({ input: { accepts: ["image"] } }),
|
|
412
|
+
{
|
|
413
|
+
key: "model",
|
|
414
|
+
kind: "enum",
|
|
415
|
+
label: "Model",
|
|
416
|
+
options: DBNET_MODEL_OPTIONS,
|
|
417
|
+
allowCustom: true,
|
|
418
|
+
help: "Hugging Face repo id, or a URL when self-hosting."
|
|
419
|
+
},
|
|
420
|
+
scoreThreshold("Mean in-box probability a candidate must reach."),
|
|
421
|
+
{
|
|
422
|
+
key: "binaryThreshold",
|
|
423
|
+
kind: "number",
|
|
424
|
+
label: "Binary threshold",
|
|
425
|
+
min: 0,
|
|
426
|
+
max: 1,
|
|
427
|
+
step: .05,
|
|
428
|
+
help: "Probability above which a pixel counts as text."
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
key: "unclipRatio",
|
|
432
|
+
kind: "number",
|
|
433
|
+
label: "Unclip ratio",
|
|
434
|
+
min: .5,
|
|
435
|
+
max: 5,
|
|
436
|
+
step: .1,
|
|
437
|
+
help: "How far to grow each box; DB shrinks text regions during training."
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
key: "mergeBoxes",
|
|
441
|
+
kind: "boolean",
|
|
442
|
+
label: "Merge boxes",
|
|
443
|
+
help: "Merge overlapping boxes that share a line. Usually changes nothing here — this model already returns one region per line, so no two boxes overlap."
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
type: "YoloOnnxDetector",
|
|
449
|
+
label: "YOLO detector",
|
|
450
|
+
group: "Detect",
|
|
451
|
+
subpath: "@stabrise/scaledp/detect",
|
|
452
|
+
summary: "Detect arbitrary objects with a YOLO ONNX model.",
|
|
453
|
+
consumes: ["image"],
|
|
454
|
+
produces: "boxes",
|
|
455
|
+
peer: "onnxruntime-web",
|
|
456
|
+
cache: {
|
|
457
|
+
kind: "hf-repo",
|
|
458
|
+
param: "model"
|
|
459
|
+
},
|
|
460
|
+
defaults: asRecord(YOLO_DETECTOR_DEFAULTS),
|
|
461
|
+
params: yoloParams
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
type: "SignatureDetector",
|
|
465
|
+
label: "Signature detector",
|
|
466
|
+
group: "Detect",
|
|
467
|
+
subpath: "@stabrise/scaledp/detect",
|
|
468
|
+
summary: "YOLO pre-set for StabRise/signature_detection.",
|
|
469
|
+
consumes: ["image"],
|
|
470
|
+
produces: "boxes",
|
|
471
|
+
peer: "onnxruntime-web",
|
|
472
|
+
cache: {
|
|
473
|
+
kind: "hf-repo",
|
|
474
|
+
param: "model"
|
|
475
|
+
},
|
|
476
|
+
defaults: asRecord({
|
|
477
|
+
...YOLO_DETECTOR_DEFAULTS,
|
|
478
|
+
model: "StabRise/signature_detection",
|
|
479
|
+
labels: ["signature"],
|
|
480
|
+
outputCol: "signatures",
|
|
481
|
+
outputType: "signature",
|
|
482
|
+
scoreThreshold: .2
|
|
483
|
+
}),
|
|
484
|
+
params: yoloParams
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
type: "FaceDetector",
|
|
488
|
+
label: "Face detector",
|
|
489
|
+
group: "Detect",
|
|
490
|
+
subpath: "@stabrise/scaledp/detect",
|
|
491
|
+
summary: "YOLO pre-set for StabRise/face_detection.",
|
|
492
|
+
consumes: ["image"],
|
|
493
|
+
produces: "boxes",
|
|
494
|
+
peer: "onnxruntime-web",
|
|
495
|
+
cache: {
|
|
496
|
+
kind: "hf-repo",
|
|
497
|
+
param: "model"
|
|
498
|
+
},
|
|
499
|
+
defaults: asRecord({
|
|
500
|
+
...YOLO_DETECTOR_DEFAULTS,
|
|
501
|
+
model: "StabRise/face_detection",
|
|
502
|
+
labels: ["face"],
|
|
503
|
+
outputCol: "faces",
|
|
504
|
+
outputType: "face",
|
|
505
|
+
scoreThreshold: .2
|
|
506
|
+
}),
|
|
507
|
+
params: yoloParams
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
type: "LineOrientationDetector",
|
|
511
|
+
label: "Line orientation",
|
|
512
|
+
group: "Transform",
|
|
513
|
+
subpath: "@stabrise/scaledp/ocr",
|
|
514
|
+
summary: "Classify each detected region 0°/180° and turn the inverted ones.",
|
|
515
|
+
consumes: ["image", "boxes"],
|
|
516
|
+
produces: "image",
|
|
517
|
+
alsoProduces: [{
|
|
518
|
+
param: "orientationCol",
|
|
519
|
+
kind: "orientations"
|
|
520
|
+
}],
|
|
521
|
+
peer: "onnxruntime-web",
|
|
522
|
+
cache: {
|
|
523
|
+
kind: "hf-repo",
|
|
524
|
+
param: "model",
|
|
525
|
+
approxBytes: 9e6
|
|
526
|
+
},
|
|
527
|
+
expands: true,
|
|
528
|
+
defaults: asRecord(LINE_ORIENTATION_DEFAULTS),
|
|
529
|
+
params: [
|
|
530
|
+
...baseParams({
|
|
531
|
+
input: "unused",
|
|
532
|
+
output: { help: "The corrected page image." }
|
|
533
|
+
}),
|
|
534
|
+
{
|
|
535
|
+
key: "inputCols",
|
|
536
|
+
kind: "columns",
|
|
537
|
+
label: "Input columns",
|
|
538
|
+
arity: 2,
|
|
539
|
+
accepts: ["image", "boxes"],
|
|
540
|
+
help: "The page image, then the detector output whose regions to classify."
|
|
541
|
+
},
|
|
542
|
+
column("orientationCol", "Orientation column", { help: "Per-region 0_degree / 180_degree labels." }),
|
|
543
|
+
{
|
|
544
|
+
key: "model",
|
|
545
|
+
kind: "string",
|
|
546
|
+
label: "Model",
|
|
547
|
+
help: `Hugging Face repo id. Defaults to ${DEFAULT_ORIENTATION_MODEL}.`
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
key: "correct",
|
|
551
|
+
kind: "boolean",
|
|
552
|
+
label: "Correct",
|
|
553
|
+
help: "Turn the inverted regions. Off classifies only, leaving the page as-is."
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
key: "onlyRotated",
|
|
557
|
+
kind: "boolean",
|
|
558
|
+
label: "Only rotated",
|
|
559
|
+
help: "Classify only already-rotated boxes, where the signal is. Off also catches upside-down horizontal text, at a false-positive cost."
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
key: "padding",
|
|
563
|
+
kind: "number",
|
|
564
|
+
label: "Padding",
|
|
565
|
+
min: 0,
|
|
566
|
+
step: 1,
|
|
567
|
+
help: "Grow each box before cropping, so glyph edges are not clipped."
|
|
568
|
+
},
|
|
569
|
+
imageType
|
|
570
|
+
]
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
type: "PaddleTextRecognizer",
|
|
574
|
+
label: "PaddleOCR",
|
|
575
|
+
group: "Recognise",
|
|
576
|
+
subpath: "@stabrise/scaledp/ocr",
|
|
577
|
+
summary: "Detect and read a page in one pass. Ignores any separate detector’s boxes.",
|
|
578
|
+
consumes: ["image"],
|
|
579
|
+
produces: "document",
|
|
580
|
+
peer: "ppu-paddle-ocr",
|
|
581
|
+
cache: {
|
|
582
|
+
kind: "paddle-preset",
|
|
583
|
+
param: "preset"
|
|
584
|
+
},
|
|
585
|
+
defaults: asRecord(PADDLE_RECOGNIZER_DEFAULTS),
|
|
586
|
+
params: [
|
|
587
|
+
...baseParams({ input: { accepts: ["image"] } }),
|
|
588
|
+
{
|
|
589
|
+
key: "preset",
|
|
590
|
+
kind: "enum",
|
|
591
|
+
label: "Preset",
|
|
592
|
+
options: OCR_PRESETS,
|
|
593
|
+
help: "Language/script pairing. Pick the one matching your documents."
|
|
594
|
+
},
|
|
595
|
+
scoreThreshold("Drop words below this confidence."),
|
|
596
|
+
{
|
|
597
|
+
key: "strategy",
|
|
598
|
+
kind: "enum",
|
|
599
|
+
label: "Strategy",
|
|
600
|
+
help: "How the detected regions are grouped. It cannot subdivide them: the boxes are whatever the preset’s detector found, which is line-level.",
|
|
601
|
+
options: STRATEGIES
|
|
602
|
+
},
|
|
603
|
+
keepFormatting,
|
|
604
|
+
lineTolerance
|
|
605
|
+
]
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
type: "TesseractOcr",
|
|
609
|
+
label: "Tesseract",
|
|
610
|
+
group: "Recognise",
|
|
611
|
+
subpath: "@stabrise/scaledp/ocr",
|
|
612
|
+
summary: "Read a whole page with Tesseract, detecting layout itself. The one stage here that returns word-level boxes.",
|
|
613
|
+
consumes: ["image"],
|
|
614
|
+
produces: "document",
|
|
615
|
+
peer: "tesseract-wasm",
|
|
616
|
+
defaults: asRecord(TESSERACT_OCR_DEFAULTS),
|
|
617
|
+
params: [
|
|
618
|
+
...baseParams({ input: { accepts: ["image"] } }),
|
|
619
|
+
lang,
|
|
620
|
+
scoreThreshold("Drop words below this confidence."),
|
|
621
|
+
keepFormatting,
|
|
622
|
+
lineTolerance
|
|
623
|
+
]
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
type: "TesseractRecognizer",
|
|
627
|
+
label: "Tesseract on boxes",
|
|
628
|
+
group: "Recognise",
|
|
629
|
+
subpath: "@stabrise/scaledp/ocr",
|
|
630
|
+
summary: "Read exactly the regions a detector found, cropping and turning each one. Returns a box per region, or per word with “Box level”.",
|
|
631
|
+
consumes: ["image", "boxes"],
|
|
632
|
+
produces: "document",
|
|
633
|
+
peer: "tesseract-wasm",
|
|
634
|
+
cache: {
|
|
635
|
+
kind: "hf-repo",
|
|
636
|
+
param: "oriModel",
|
|
637
|
+
approxBytes: 9e6
|
|
638
|
+
},
|
|
639
|
+
defaults: asRecord(TESSERACT_RECOGNIZER_DEFAULTS),
|
|
640
|
+
params: [
|
|
641
|
+
...baseParams({ input: "unused" }),
|
|
642
|
+
{
|
|
643
|
+
key: "inputCols",
|
|
644
|
+
kind: "columns",
|
|
645
|
+
label: "Input columns",
|
|
646
|
+
arity: 2,
|
|
647
|
+
accepts: ["image", "boxes"],
|
|
648
|
+
help: "The page image, then the detector output whose regions to read."
|
|
649
|
+
},
|
|
650
|
+
lang,
|
|
651
|
+
{
|
|
652
|
+
key: "boxLevel",
|
|
653
|
+
kind: "enum",
|
|
654
|
+
label: "Box level",
|
|
655
|
+
options: [{
|
|
656
|
+
value: "region",
|
|
657
|
+
label: "One box per region",
|
|
658
|
+
title: "ScaleDP’s behaviour: each detected region keeps its own box, carrying everything read inside it"
|
|
659
|
+
}, {
|
|
660
|
+
value: "word",
|
|
661
|
+
label: "One box per word",
|
|
662
|
+
title: "Tesseract’s own word boxes, mapped back into page coordinates"
|
|
663
|
+
}],
|
|
664
|
+
help: "The detectors here are line-level, so “region” gives line boxes. Pick “word” to get one box per word instead."
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
key: "scaleFactor",
|
|
668
|
+
kind: "number",
|
|
669
|
+
label: "Scale factor",
|
|
670
|
+
min: .1,
|
|
671
|
+
max: 8,
|
|
672
|
+
step: .1,
|
|
673
|
+
help: "Resize the page by this factor before cropping."
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
key: "padding",
|
|
677
|
+
kind: "number",
|
|
678
|
+
label: "Padding",
|
|
679
|
+
min: 0,
|
|
680
|
+
step: 1,
|
|
681
|
+
help: "Grow each box before cropping. ScaleDP hardcodes 5."
|
|
682
|
+
},
|
|
683
|
+
scoreThreshold("Drop words below this confidence."),
|
|
684
|
+
keepFormatting,
|
|
685
|
+
lineTolerance,
|
|
686
|
+
{
|
|
687
|
+
key: "detectLineOrientation",
|
|
688
|
+
kind: "boolean",
|
|
689
|
+
label: "Detect line orientation",
|
|
690
|
+
help: "Classify each crop 0°/180° and turn the inverted ones."
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
key: "onlyRotated",
|
|
694
|
+
kind: "boolean",
|
|
695
|
+
label: "Only rotated",
|
|
696
|
+
help: "Read only rotated or inverted boxes. On, an ordinary page returns nothing."
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
key: "oriModel",
|
|
700
|
+
kind: "string",
|
|
701
|
+
label: "Orientation model",
|
|
702
|
+
advanced: true,
|
|
703
|
+
help: `Hugging Face repo id. Defaults to ${DEFAULT_ORIENTATION_MODEL}.`
|
|
704
|
+
}
|
|
705
|
+
]
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
type: "GlinerNer",
|
|
709
|
+
label: "GLiNER entities",
|
|
710
|
+
group: "Understand",
|
|
711
|
+
subpath: "@stabrise/scaledp/ner",
|
|
712
|
+
summary: "Find named entities in a document, scored against labels you write.",
|
|
713
|
+
consumes: ["document"],
|
|
714
|
+
produces: "ner",
|
|
715
|
+
peer: "@huggingface/transformers",
|
|
716
|
+
cache: {
|
|
717
|
+
kind: "ner-id",
|
|
718
|
+
param: "model"
|
|
719
|
+
},
|
|
720
|
+
defaults: asRecord(GLINER_NER_DEFAULTS),
|
|
721
|
+
params: [
|
|
722
|
+
...baseParams({ input: { accepts: ["document"] } }),
|
|
723
|
+
{
|
|
724
|
+
key: "model",
|
|
725
|
+
kind: "enum",
|
|
726
|
+
label: "Model",
|
|
727
|
+
options: NER_MODEL_OPTIONS,
|
|
728
|
+
help: "Registry id. Private repos need configure({ auth })."
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
key: "labels",
|
|
732
|
+
kind: "stringList",
|
|
733
|
+
label: "Labels",
|
|
734
|
+
required: true,
|
|
735
|
+
help: "GLiNER scores a label by its prompt text, so other wording asks a different question."
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
key: "threshold",
|
|
739
|
+
kind: "number",
|
|
740
|
+
label: "Threshold",
|
|
741
|
+
min: 0,
|
|
742
|
+
max: 1,
|
|
743
|
+
step: .05,
|
|
744
|
+
help: "Minimum score an entity must reach."
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
key: "whiteList",
|
|
748
|
+
kind: "stringList",
|
|
749
|
+
label: "Only these groups",
|
|
750
|
+
help: "Keep only these entity groups; empty keeps everything."
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
key: "chunkLength",
|
|
754
|
+
kind: "number",
|
|
755
|
+
label: "Chunk length",
|
|
756
|
+
min: 1,
|
|
757
|
+
step: 1,
|
|
758
|
+
advanced: true,
|
|
759
|
+
help: "Tokens per inference window."
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
key: "chunkStride",
|
|
763
|
+
kind: "number",
|
|
764
|
+
label: "Chunk stride",
|
|
765
|
+
min: 1,
|
|
766
|
+
step: 1,
|
|
767
|
+
advanced: true,
|
|
768
|
+
help: "Window step. Below chunk length, windows overlap."
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
key: "normaliseCasing",
|
|
772
|
+
kind: "boolean",
|
|
773
|
+
label: "Normalise casing",
|
|
774
|
+
help: "Title-case runs of capitals first. GLiNER1 models are cased; scans are often all caps."
|
|
775
|
+
}
|
|
776
|
+
]
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
type: "ImageDrawBoxes",
|
|
780
|
+
label: "Draw boxes",
|
|
781
|
+
group: "Transform",
|
|
782
|
+
subpath: "@stabrise/scaledp",
|
|
783
|
+
summary: "Annotate a page with any boxes or entities found so far.",
|
|
784
|
+
consumes: ["image", "boxes"],
|
|
785
|
+
produces: "image",
|
|
786
|
+
terminal: true,
|
|
787
|
+
defaults: asRecord(IMAGE_DRAW_BOXES_DEFAULTS),
|
|
788
|
+
params: [
|
|
789
|
+
...baseParams({ input: "unused" }),
|
|
790
|
+
{
|
|
791
|
+
key: "inputCols",
|
|
792
|
+
kind: "columns",
|
|
793
|
+
label: "Input columns",
|
|
794
|
+
minArity: 2,
|
|
795
|
+
accepts: ["image", "boxes"],
|
|
796
|
+
help: "The image first, then one or more box, document or entity columns."
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
key: "color",
|
|
800
|
+
kind: "color",
|
|
801
|
+
label: "Colour",
|
|
802
|
+
help: "One colour for every box. Unset colours by group instead."
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
key: "filled",
|
|
806
|
+
kind: "boolean",
|
|
807
|
+
label: "Filled",
|
|
808
|
+
help: "Fill boxes as well as outlining them."
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
key: "lineWidth",
|
|
812
|
+
kind: "number",
|
|
813
|
+
label: "Line width",
|
|
814
|
+
min: 0,
|
|
815
|
+
max: 20,
|
|
816
|
+
step: 1
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
key: "textSize",
|
|
820
|
+
kind: "number",
|
|
821
|
+
label: "Text size",
|
|
822
|
+
min: 4,
|
|
823
|
+
max: 72,
|
|
824
|
+
step: 1
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
key: "displayDataList",
|
|
828
|
+
kind: "stringList",
|
|
829
|
+
label: "Label fields",
|
|
830
|
+
options: LABEL_FIELDS,
|
|
831
|
+
help: "Rendered above each box, joined by \":\". Pick \"text\" to read the OCR output back off the page."
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
key: "padding",
|
|
835
|
+
kind: "number",
|
|
836
|
+
label: "Padding",
|
|
837
|
+
min: 0,
|
|
838
|
+
step: 1,
|
|
839
|
+
help: "Grow each box by this many pixels before drawing."
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
key: "whiteList",
|
|
843
|
+
kind: "stringList",
|
|
844
|
+
label: "Only these groups",
|
|
845
|
+
help: "Draw only these entity groups; empty draws all."
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
key: "blackList",
|
|
849
|
+
kind: "stringList",
|
|
850
|
+
label: "Never these groups",
|
|
851
|
+
help: "Never draw these entity groups."
|
|
852
|
+
},
|
|
853
|
+
imageType
|
|
854
|
+
]
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
type: "ImageCropBoxes",
|
|
858
|
+
label: "Crop boxes",
|
|
859
|
+
group: "Transform",
|
|
860
|
+
subpath: "@stabrise/scaledp",
|
|
861
|
+
summary: "Cut each detected region out of the page, one output row per crop.",
|
|
862
|
+
consumes: ["image", "boxes"],
|
|
863
|
+
produces: "image",
|
|
864
|
+
alsoProduces: [{
|
|
865
|
+
param: "boxCol",
|
|
866
|
+
kind: "box"
|
|
867
|
+
}],
|
|
868
|
+
expands: true,
|
|
869
|
+
defaults: asRecord(IMAGE_CROP_BOXES_DEFAULTS),
|
|
870
|
+
params: [
|
|
871
|
+
...baseParams({ input: "unused" }),
|
|
872
|
+
{
|
|
873
|
+
key: "inputCols",
|
|
874
|
+
kind: "columns",
|
|
875
|
+
label: "Input columns",
|
|
876
|
+
arity: 2,
|
|
877
|
+
accepts: ["image", "boxes"],
|
|
878
|
+
help: "The page image, then the box column to cut from it."
|
|
879
|
+
},
|
|
880
|
+
column("boxCol", "Box column", { help: "Where each crop’s source box is written." }),
|
|
881
|
+
{
|
|
882
|
+
key: "padding",
|
|
883
|
+
kind: "number",
|
|
884
|
+
label: "Padding",
|
|
885
|
+
min: 0,
|
|
886
|
+
step: 1,
|
|
887
|
+
help: "Grow each box by this many pixels before cropping."
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
key: "limit",
|
|
891
|
+
kind: "number",
|
|
892
|
+
label: "Limit",
|
|
893
|
+
min: 0,
|
|
894
|
+
step: 1,
|
|
895
|
+
help: "Maximum crops per page; 0 means all of them."
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
key: "autoRotate",
|
|
899
|
+
kind: "boolean",
|
|
900
|
+
label: "Auto-rotate",
|
|
901
|
+
help: "Turn portrait crops a quarter turn, so text reads horizontally."
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
key: "returnEmpty",
|
|
905
|
+
kind: "boolean",
|
|
906
|
+
label: "Return empty",
|
|
907
|
+
help: "Emit the whole page when nothing was detected, instead of failing."
|
|
908
|
+
},
|
|
909
|
+
imageType
|
|
910
|
+
]
|
|
911
|
+
}
|
|
912
|
+
]);
|
|
913
|
+
/** Name → constructor, for building a stage from a `StageDescriptor`. */
|
|
914
|
+
const STAGE_CLASSES = Object.freeze({
|
|
915
|
+
DataToImage,
|
|
916
|
+
DbnetOnnxDetector,
|
|
917
|
+
FaceDetector,
|
|
918
|
+
GlinerNer,
|
|
919
|
+
ImageCropBoxes,
|
|
920
|
+
ImageDrawBoxes,
|
|
921
|
+
LineOrientationDetector,
|
|
922
|
+
PaddleTextDetector,
|
|
923
|
+
PaddleTextRecognizer,
|
|
924
|
+
PdfToDocument,
|
|
925
|
+
PdfToImage,
|
|
926
|
+
SignatureDetector,
|
|
927
|
+
TesseractOcr,
|
|
928
|
+
TesseractRecognizer,
|
|
929
|
+
YoloOnnxDetector
|
|
930
|
+
});
|
|
931
|
+
//#endregion
|
|
932
|
+
//#region src/registry/codegen.ts
|
|
933
|
+
/** Structural equality, deep enough for param values: scalars and string lists. */
|
|
934
|
+
function same(a, b) {
|
|
935
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
936
|
+
return a === b;
|
|
937
|
+
}
|
|
938
|
+
/** A value as TypeScript source, in the quote style the library is written in. */
|
|
939
|
+
function literal(value) {
|
|
940
|
+
if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
941
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
942
|
+
if (Array.isArray(value)) return `[${value.map(literal).join(", ")}]`;
|
|
943
|
+
return JSON.stringify(value) ?? "undefined";
|
|
944
|
+
}
|
|
945
|
+
/** The options that differ from the stage's defaults, as `key: value` source. */
|
|
946
|
+
function changedOptions(descriptor) {
|
|
947
|
+
const spec = STAGE_SPECS.find((candidate) => candidate.type === descriptor.type);
|
|
948
|
+
const options = descriptor.options ?? {};
|
|
949
|
+
return Object.keys(options).filter((key) => !spec || !same(spec.defaults[key], options[key])).map((key) => `${key}: ${literal(options[key])}`);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Import lines, grouped by subpath.
|
|
953
|
+
*
|
|
954
|
+
* The root comes first because `Pipeline` lives there and every pipeline needs
|
|
955
|
+
* it; the engine subpaths follow in alphabetical order, which is what biome's
|
|
956
|
+
* import sorting would produce anyway.
|
|
957
|
+
*/
|
|
958
|
+
function importLines(descriptors, root) {
|
|
959
|
+
const bySubpath = /* @__PURE__ */ new Map();
|
|
960
|
+
bySubpath.set(root, /* @__PURE__ */ new Set(["Pipeline"]));
|
|
961
|
+
for (const descriptor of descriptors) {
|
|
962
|
+
const spec = STAGE_SPECS.find((candidate) => candidate.type === descriptor.type);
|
|
963
|
+
if (!spec) continue;
|
|
964
|
+
const names = bySubpath.get(spec.subpath) ?? /* @__PURE__ */ new Set();
|
|
965
|
+
names.add(descriptor.type);
|
|
966
|
+
bySubpath.set(spec.subpath, names);
|
|
967
|
+
}
|
|
968
|
+
return [...bySubpath.entries()].sort(([a], [b]) => a === root ? -1 : b === root ? 1 : a.localeCompare(b)).map(([subpath, names]) => `import { ${[...names].sort().join(", ")} } from '${subpath}'`);
|
|
969
|
+
}
|
|
970
|
+
function pipelineCode(descriptors, options = {}) {
|
|
971
|
+
const variable = options.variable ?? "pipeline";
|
|
972
|
+
const pad = " ".repeat(options.indent ?? 4);
|
|
973
|
+
const root = STAGE_SPECS.find((spec) => spec.subpath.endsWith("/scaledp"))?.subpath ?? "@stabrise/scaledp";
|
|
974
|
+
const stages = descriptors.map((descriptor) => {
|
|
975
|
+
const entries = changedOptions(descriptor);
|
|
976
|
+
if (entries.length === 0) return `${pad}new ${descriptor.type}(),`;
|
|
977
|
+
const inline = `${pad}new ${descriptor.type}({ ${entries.join(", ")} }),`;
|
|
978
|
+
if (inline.length <= 100) return inline;
|
|
979
|
+
return [
|
|
980
|
+
`${pad}new ${descriptor.type}({`,
|
|
981
|
+
...entries.map((entry) => `${pad}${pad}${entry},`),
|
|
982
|
+
`${pad}}),`
|
|
983
|
+
].join("\n");
|
|
984
|
+
});
|
|
985
|
+
const body = [
|
|
986
|
+
`const ${variable} = new Pipeline([`,
|
|
987
|
+
...stages,
|
|
988
|
+
"])"
|
|
989
|
+
];
|
|
990
|
+
if (options.imports === false) return body.join("\n");
|
|
991
|
+
return [
|
|
992
|
+
...importLines(descriptors, root),
|
|
993
|
+
"",
|
|
994
|
+
...body
|
|
995
|
+
].join("\n");
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/registry/index.ts
|
|
999
|
+
/**
|
|
1000
|
+
* @stabrise/scaledp/registry -- describe, build and serialise pipelines as data.
|
|
1001
|
+
*
|
|
1002
|
+
* A pipeline is an ordered array of stages, each a class with a params object.
|
|
1003
|
+
* That is fine to write by hand, but a UI that assembles one needs two things
|
|
1004
|
+
* the classes themselves do not carry at run time: a description of each
|
|
1005
|
+
* stage's parameters, and a way back from JSON to live objects.
|
|
1006
|
+
*
|
|
1007
|
+
* import { STAGE_SPECS, pipelineFromDescriptors } from '@stabrise/scaledp/registry'
|
|
1008
|
+
*
|
|
1009
|
+
* const stages = [
|
|
1010
|
+
* { type: 'PdfToImage', options: { resolution: 200 } },
|
|
1011
|
+
* { type: 'PaddleTextRecognizer', options: { keepFormatting: true } },
|
|
1012
|
+
* ]
|
|
1013
|
+
* const rows = await pipelineFromDescriptors(stages).transform(file)
|
|
1014
|
+
*
|
|
1015
|
+
* `StageDescriptor[]` is plain JSON, and the same shape the worker protocol
|
|
1016
|
+
* already sends across the boundary -- so a saved pipeline, a posted message and
|
|
1017
|
+
* a pipeline built here are all the same data.
|
|
1018
|
+
*
|
|
1019
|
+
* This subpath pulls in every stage class. It pulls in no ML runtime: engines
|
|
1020
|
+
* are reached through dynamic `import()` and stay lazy.
|
|
1021
|
+
*/
|
|
1022
|
+
/** Metadata for one stage, by exported class name. */
|
|
1023
|
+
function getStageSpec(type) {
|
|
1024
|
+
return STAGE_SPECS.find((spec) => spec.type === type);
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Build one stage from its serialised form.
|
|
1028
|
+
*
|
|
1029
|
+
* Throws on an unknown type, and lets the stage's own constructor validators
|
|
1030
|
+
* throw on a bad param -- an unknown OCR preset should fail here, where the
|
|
1031
|
+
* message can name the offending field, rather than several stages later.
|
|
1032
|
+
*/
|
|
1033
|
+
function createStage(descriptor) {
|
|
1034
|
+
const Ctor = STAGE_CLASSES[descriptor.type];
|
|
1035
|
+
if (!Ctor) throw new Error(`Unknown stage "${descriptor.type}". See STAGE_SPECS for the stages this build knows about.`);
|
|
1036
|
+
return new Ctor(descriptor.options);
|
|
1037
|
+
}
|
|
1038
|
+
/** Build a whole pipeline from its serialised form. */
|
|
1039
|
+
function pipelineFromDescriptors(descriptors) {
|
|
1040
|
+
return new Pipeline(descriptors.map(createStage));
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* The serialised form of a live stage.
|
|
1044
|
+
*
|
|
1045
|
+
* `stage.params` is fully resolved -- defaults merged in -- so this round-trips
|
|
1046
|
+
* exactly but is more verbose than the options originally passed.
|
|
1047
|
+
*/
|
|
1048
|
+
function describeStage(stage) {
|
|
1049
|
+
return {
|
|
1050
|
+
type: stage.name,
|
|
1051
|
+
options: { ...stage.params }
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
/** The serialised form of a live pipeline. */
|
|
1055
|
+
function describePipeline(pipeline) {
|
|
1056
|
+
return pipeline.stages.map(describeStage);
|
|
1057
|
+
}
|
|
1058
|
+
//#endregion
|
|
1059
|
+
export { STAGE_CLASSES, STAGE_SPECS, createStage, describePipeline, describeStage, getStageSpec, pipelineCode, pipelineFromDescriptors };
|
|
1060
|
+
|
|
1061
|
+
//# sourceMappingURL=index.js.map
|