@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.
Files changed (60) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +218 -0
  3. package/dist/box-DAfzwfhA.d.ts +119 -0
  4. package/dist/config-g6IrKlDC.d.ts +80 -0
  5. package/dist/data-to-image-DoZ4jQ3R.js +54 -0
  6. package/dist/data-to-image-DoZ4jQ3R.js.map +1 -0
  7. package/dist/detect/index.d.ts +71 -0
  8. package/dist/detect/index.js +2 -0
  9. package/dist/detect-q8AI_Jdj.js +274 -0
  10. package/dist/detect-q8AI_Jdj.js.map +1 -0
  11. package/dist/detector-output-C0Qt-jEq.d.ts +13 -0
  12. package/dist/detector-output-lyF1Mqb8.js +13 -0
  13. package/dist/detector-output-lyF1Mqb8.js.map +1 -0
  14. package/dist/display/index.d.ts +66 -0
  15. package/dist/display/index.js +237 -0
  16. package/dist/display/index.js.map +1 -0
  17. package/dist/document-B8I61TiY.d.ts +16 -0
  18. package/dist/entity-CedtRhU1.d.ts +22 -0
  19. package/dist/entity-D6Hxaugj.js +13 -0
  20. package/dist/entity-D6Hxaugj.js.map +1 -0
  21. package/dist/image-CAH2rLv9.js +511 -0
  22. package/dist/image-CAH2rLv9.js.map +1 -0
  23. package/dist/image-Dc5TSg46.d.ts +18 -0
  24. package/dist/image-DoZDJkcR.js +37 -0
  25. package/dist/image-DoZDJkcR.js.map +1 -0
  26. package/dist/image-draw-boxes-De0QbFv9.js +285 -0
  27. package/dist/image-draw-boxes-De0QbFv9.js.map +1 -0
  28. package/dist/index.d.ts +269 -0
  29. package/dist/index.js +11 -0
  30. package/dist/model-cache-BEaqqRZ9.js +182 -0
  31. package/dist/model-cache-BEaqqRZ9.js.map +1 -0
  32. package/dist/model-cache-BhFYpfZz.d.ts +36 -0
  33. package/dist/ner/index.d.ts +293 -0
  34. package/dist/ner/index.js +2 -0
  35. package/dist/ner-SsZLZ6ed.js +1028 -0
  36. package/dist/ner-SsZLZ6ed.js.map +1 -0
  37. package/dist/ocr/index.d.ts +440 -0
  38. package/dist/ocr/index.js +3 -0
  39. package/dist/ocr-OHX2WM3e.js +1294 -0
  40. package/dist/ocr-OHX2WM3e.js.map +1 -0
  41. package/dist/ort-CXDoPrtw.js +73 -0
  42. package/dist/ort-CXDoPrtw.js.map +1 -0
  43. package/dist/params-DapwK9Ns.js +37 -0
  44. package/dist/params-DapwK9Ns.js.map +1 -0
  45. package/dist/pdf/index.d.ts +123 -0
  46. package/dist/pdf/index.js +2 -0
  47. package/dist/pdf-BQl0dneD.js +417 -0
  48. package/dist/pdf-BQl0dneD.js.map +1 -0
  49. package/dist/pipeline-DACqGkpN.js +240 -0
  50. package/dist/pipeline-DACqGkpN.js.map +1 -0
  51. package/dist/pipeline-DeLO-OCE.d.ts +139 -0
  52. package/dist/registry/index.d.ts +169 -0
  53. package/dist/registry/index.js +1061 -0
  54. package/dist/registry/index.js.map +1 -0
  55. package/dist/text-ahMLpxN9.js +109 -0
  56. package/dist/text-ahMLpxN9.js.map +1 -0
  57. package/dist/worker/index.d.ts +105 -0
  58. package/dist/worker/index.js +180 -0
  59. package/dist/worker/index.js.map +1 -0
  60. package/package.json +135 -0
@@ -0,0 +1,274 @@
1
+ import { h as getConfig, i as Stage, s as DetectionError } from "./pipeline-DACqGkpN.js";
2
+ import { _ as boxFromBBox, m as toNchwFloat32, p as toImageData, s as decodeImage, u as letterbox } from "./image-CAH2rLv9.js";
3
+ import { n as ensureModelFiles } from "./model-cache-BEaqqRZ9.js";
4
+ import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
5
+ import { t as createDetectorOutput } from "./detector-output-lyF1Mqb8.js";
6
+ import { t as createSession } from "./ort-CXDoPrtw.js";
7
+ //#region src/detect/yolo-onnx.ts
8
+ /**
9
+ * YOLO object detection on onnxruntime-web.
10
+ *
11
+ * Mirrors `scaledp/models/detectors/YoloOnnxDetector.py`. Note the
12
+ * preprocessing differs from the text detector: YOLO letterboxes with
13
+ * *centred* padding and no mean/std normalisation, where DBNet pads bottom and
14
+ * right and applies ImageNet statistics. Centred padding means the pad offsets
15
+ * must be subtracted before unscaling.
16
+ */
17
+ /** Fallback when the graph declares a symbolic input size. */
18
+ const DEFAULT_YOLO_INPUT = 960;
19
+ const YOLO_DETECTOR_DEFAULTS = Object.freeze({
20
+ ...BASE_STAGE_DEFAULTS,
21
+ inputCol: "image",
22
+ outputCol: "boxes",
23
+ keepInputData: true,
24
+ model: "",
25
+ labels: [],
26
+ scoreThreshold: .2,
27
+ iouThreshold: .5,
28
+ padding: 0,
29
+ outputType: "yolo"
30
+ });
31
+ /** Axis-aligned IoU over xyxy boxes. */
32
+ function iou(a, b) {
33
+ const x0 = Math.max(a[0], b[0]);
34
+ const y0 = Math.max(a[1], b[1]);
35
+ const x1 = Math.min(a[2], b[2]);
36
+ const y1 = Math.min(a[3], b[3]);
37
+ if (x1 <= x0 || y1 <= y0) return 0;
38
+ const intersection = (x1 - x0) * (y1 - y0);
39
+ const union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - intersection;
40
+ return union <= 0 ? 0 : intersection / union;
41
+ }
42
+ /** Per-class non-maximum suppression; different classes never suppress each other. */
43
+ function nonMaximumSuppression(detections, iouThreshold) {
44
+ const byScore = [...detections].sort((a, b) => b.score - a.score);
45
+ const kept = [];
46
+ for (const detection of byScore) if (!kept.some((other) => other.classId === detection.classId && iou(other.bbox, detection.bbox) > iouThreshold)) kept.push(detection);
47
+ return kept;
48
+ }
49
+ /**
50
+ * Decode a YOLO output tensor.
51
+ *
52
+ * Two layouts are handled, because exports differ:
53
+ * - [1, 4 + numClasses, numAnchors] -- the classic v8/v11 transposed form,
54
+ * with cx,cy,w,h then per-class scores.
55
+ * - [1, numDetections, 6] -- an end-to-end NMS export: x0,y0,x1,y1,score,class.
56
+ */
57
+ function decodeYoloOutput(data, dims, scoreThreshold) {
58
+ const out = [];
59
+ if (dims.length === 3 && dims[2] === 6) {
60
+ const count = dims[1];
61
+ for (let i = 0; i < count; i++) {
62
+ const o = i * 6;
63
+ const score = data[o + 4];
64
+ if (score < scoreThreshold) continue;
65
+ out.push({
66
+ bbox: [
67
+ data[o],
68
+ data[o + 1],
69
+ data[o + 2],
70
+ data[o + 3]
71
+ ],
72
+ score,
73
+ classId: Math.round(data[o + 5])
74
+ });
75
+ }
76
+ return out;
77
+ }
78
+ if (dims.length !== 3) throw new DetectionError(`Unsupported YOLO output shape [${dims.join(", ")}]`, "decodeYoloOutput");
79
+ const channels = dims[1];
80
+ const anchors = dims[2];
81
+ const classCount = channels - 4;
82
+ for (let a = 0; a < anchors; a++) {
83
+ let best = -1;
84
+ let bestScore = 0;
85
+ for (let c = 0; c < classCount; c++) {
86
+ const score = data[(4 + c) * anchors + a];
87
+ if (score > bestScore) {
88
+ bestScore = score;
89
+ best = c;
90
+ }
91
+ }
92
+ if (best < 0 || bestScore < scoreThreshold) continue;
93
+ const cx = data[a];
94
+ const cy = data[anchors + a];
95
+ const w = data[2 * anchors + a];
96
+ const h = data[3 * anchors + a];
97
+ out.push({
98
+ bbox: [
99
+ cx - w / 2,
100
+ cy - h / 2,
101
+ cx + w / 2,
102
+ cy + h / 2
103
+ ],
104
+ score: bestScore,
105
+ classId: best
106
+ });
107
+ }
108
+ return out;
109
+ }
110
+ var YoloOnnxDetector = class extends Stage {
111
+ name = "YoloOnnxDetector";
112
+ session = null;
113
+ loading = null;
114
+ inputSize = 960;
115
+ constructor(options = {}) {
116
+ super(resolveParams(YOLO_DETECTOR_DEFAULTS, options, { model: (value) => {
117
+ if (!value) throw new RangeError("model is required");
118
+ } }));
119
+ }
120
+ async init() {
121
+ await this.getSession();
122
+ }
123
+ getSession() {
124
+ if (this.session) return Promise.resolve(this.session);
125
+ if (this.loading) return this.loading;
126
+ this.loading = (async () => {
127
+ const { model } = this.params;
128
+ const spec = /^https?:\/\//.test(model) ? {
129
+ repo: "yolo",
130
+ files: [{ path: model }]
131
+ } : {
132
+ repo: model,
133
+ files: [{ path: "model.onnx" }]
134
+ };
135
+ const bytes = (await ensureModelFiles(spec))[spec.files[0]?.path ?? ""];
136
+ if (!bytes) throw new DetectionError(`Model ${model} not found`, this.name);
137
+ const session = await createSession(bytes, { executionProviders: getConfig().executionProviders });
138
+ this.inputSize = readInputSize(session);
139
+ this.session = session;
140
+ return session;
141
+ })();
142
+ this.loading.catch(() => {
143
+ this.loading = null;
144
+ });
145
+ return this.loading;
146
+ }
147
+ async dispose() {
148
+ await this.session?.release();
149
+ this.session = null;
150
+ this.loading = null;
151
+ }
152
+ async apply(input, row) {
153
+ const image = input;
154
+ if (image?.exception) throw new DetectionError(`Upstream stage failed: ${image.exception}`, this.name);
155
+ if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) throw new DetectionError("Expected an Image with decoded bytes", this.name);
156
+ const bitmap = await decodeImage(image.data);
157
+ let detections;
158
+ try {
159
+ detections = await this.detect(bitmap);
160
+ } finally {
161
+ bitmap.close();
162
+ }
163
+ const bboxes = detections.map((d) => boxFromBBox(d.bbox, {
164
+ text: d.label,
165
+ score: d.score
166
+ }));
167
+ return createDetectorOutput({
168
+ path: String(row[this.params.pathCol] ?? "memory"),
169
+ type: this.params.outputType,
170
+ bboxes
171
+ });
172
+ }
173
+ /** Run the model over one decoded image, returning source-space detections. */
174
+ async detect(source) {
175
+ const session = await this.getSession();
176
+ const size = this.inputSize;
177
+ const fitted = letterbox(source, {
178
+ width: size,
179
+ height: size
180
+ }, {
181
+ padding: "center",
182
+ fill: "#ffffff"
183
+ });
184
+ const tensorData = toNchwFloat32(toImageData(fitted.canvas));
185
+ const { Tensor } = await import("onnxruntime-web");
186
+ const inputName = session.inputNames[0];
187
+ const outputName = session.outputNames[0];
188
+ if (!inputName || !outputName) throw new DetectionError("Model exposes no input or output", this.name);
189
+ const output = (await session.run({ [inputName]: new Tensor("float32", tensorData, [
190
+ 1,
191
+ 3,
192
+ size,
193
+ size
194
+ ]) }))[outputName];
195
+ if (!output) throw new DetectionError(`Model produced no "${outputName}" output`, this.name);
196
+ const raw = decodeYoloOutput(output.data, output.dims, this.params.scoreThreshold);
197
+ const padX = Math.trunc((size - fitted.resized.width) / 2);
198
+ const padY = Math.trunc((size - fitted.resized.height) / 2);
199
+ const { labels, padding } = this.params;
200
+ return nonMaximumSuppression(raw.map(({ bbox, score, classId }) => {
201
+ const x0 = (bbox[0] - padX) / fitted.scale;
202
+ const y0 = (bbox[1] - padY) / fitted.scale;
203
+ const x1 = (bbox[2] - padX) / fitted.scale;
204
+ const y1 = (bbox[3] - padY) / fitted.scale;
205
+ const padW = (x1 - x0) * padding;
206
+ const padH = (y1 - y0) * padding;
207
+ return {
208
+ bbox: [
209
+ Math.max(0, x0 - padW),
210
+ Math.max(0, y0 - padH),
211
+ Math.min(source.width, x1 + padW),
212
+ Math.min(source.height, y1 + padH)
213
+ ],
214
+ score,
215
+ classId,
216
+ label: labels[classId] ?? `class_${classId}`
217
+ };
218
+ }), this.params.iouThreshold);
219
+ }
220
+ onError(message, row) {
221
+ return createDetectorOutput({
222
+ path: String(row[this.params.pathCol] ?? "memory"),
223
+ type: this.params.outputType,
224
+ exception: message
225
+ });
226
+ }
227
+ };
228
+ /** Input side length from the graph, falling back when the dim is symbolic. */
229
+ function readInputSize(session) {
230
+ const height = ((session.inputMetadata?.[0])?.dims)?.[2];
231
+ return typeof height === "number" && height > 0 ? height : 960;
232
+ }
233
+ //#endregion
234
+ //#region src/detect/index.ts
235
+ /**
236
+ * Object detection for @stabrise/scaledp.
237
+ *
238
+ * Mirrors ScaleDP's YOLO detector stages. Requires the optional peer dependency
239
+ * `onnxruntime-web`.
240
+ */
241
+ /** Signature detection, mirroring ScaleDP's `SignatureDetector`. */
242
+ var SignatureDetector = class extends YoloOnnxDetector {
243
+ name = "SignatureDetector";
244
+ constructor(options = {}) {
245
+ super({
246
+ ...YOLO_DETECTOR_DEFAULTS,
247
+ model: "StabRise/signature_detection",
248
+ labels: ["signature"],
249
+ outputCol: "signatures",
250
+ outputType: "signature",
251
+ scoreThreshold: .2,
252
+ ...options
253
+ });
254
+ }
255
+ };
256
+ /** Face detection, mirroring ScaleDP's `FaceDetector`. */
257
+ var FaceDetector = class extends YoloOnnxDetector {
258
+ name = "FaceDetector";
259
+ constructor(options = {}) {
260
+ super({
261
+ ...YOLO_DETECTOR_DEFAULTS,
262
+ model: "StabRise/face_detection",
263
+ labels: ["face"],
264
+ outputCol: "faces",
265
+ outputType: "face",
266
+ scoreThreshold: .2,
267
+ ...options
268
+ });
269
+ }
270
+ };
271
+ //#endregion
272
+ export { YoloOnnxDetector as a, nonMaximumSuppression as c, YOLO_DETECTOR_DEFAULTS as i, SignatureDetector as n, decodeYoloOutput as o, DEFAULT_YOLO_INPUT as r, iou as s, FaceDetector as t };
273
+
274
+ //# sourceMappingURL=detect-q8AI_Jdj.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-q8AI_Jdj.js","names":[],"sources":["../src/detect/yolo-onnx.ts","../src/detect/index.ts"],"sourcesContent":["/**\n * YOLO object detection on onnxruntime-web.\n *\n * Mirrors `scaledp/models/detectors/YoloOnnxDetector.py`. Note the\n * preprocessing differs from the text detector: YOLO letterboxes with\n * *centred* padding and no mean/std normalisation, where DBNet pads bottom and\n * right and applies ImageNet statistics. Centred padding means the pad offsets\n * must be subtracted before unscaling.\n */\n\nimport { getConfig } from '../core/config.js'\nimport { DetectionError } from '../core/errors.js'\nimport { decodeImage, letterbox, toImageData, toNchwFloat32 } from '../core/image.js'\nimport { ensureModelFiles } from '../core/model-cache.js'\nimport { BASE_STAGE_DEFAULTS, type BaseStageParams, resolveParams } from '../core/params.js'\nimport { type Row, Stage } from '../core/pipeline.js'\nimport { createSession } from '../ocr/ort.js'\nimport { type Box, boxFromBBox } from '../schemas/box.js'\nimport { createDetectorOutput, type DetectorOutput } from '../schemas/detector-output.js'\nimport type { ScaleDpImage } from '../schemas/image.js'\n\n/** Fallback when the graph declares a symbolic input size. */\nexport const DEFAULT_YOLO_INPUT = 960\n\nexport interface YoloOnnxDetectorParams extends BaseStageParams {\n model: string\n /** Class index -> label. An empty list falls back to `class_<n>`. */\n labels: readonly string[]\n scoreThreshold: number\n /** IoU above which two same-class boxes are considered duplicates. */\n iouThreshold: number\n /** Grow each box by this fraction of its size, to avoid clipping edges. */\n padding: number\n /** Emitted as DetectorOutput.type. */\n outputType: string\n}\n\nexport const YOLO_DETECTOR_DEFAULTS: YoloOnnxDetectorParams = Object.freeze({\n ...BASE_STAGE_DEFAULTS,\n inputCol: 'image',\n outputCol: 'boxes',\n keepInputData: true,\n model: '',\n labels: [] as readonly string[],\n scoreThreshold: 0.2,\n iouThreshold: 0.5,\n padding: 0,\n outputType: 'yolo',\n})\n\nexport interface Detection {\n /** [x0, y0, x1, y1] in source-image coordinates. */\n bbox: [number, number, number, number]\n score: number\n classId: number\n label: string\n}\n\n/** Axis-aligned IoU over xyxy boxes. */\nexport function iou(a: readonly number[], b: readonly number[]): number {\n const x0 = Math.max(a[0] as number, b[0] as number)\n const y0 = Math.max(a[1] as number, b[1] as number)\n const x1 = Math.min(a[2] as number, b[2] as number)\n const y1 = Math.min(a[3] as number, b[3] as number)\n if (x1 <= x0 || y1 <= y0) return 0\n\n const intersection = (x1 - x0) * (y1 - y0)\n const areaA = ((a[2] as number) - (a[0] as number)) * ((a[3] as number) - (a[1] as number))\n const areaB = ((b[2] as number) - (b[0] as number)) * ((b[3] as number) - (b[1] as number))\n const union = areaA + areaB - intersection\n return union <= 0 ? 0 : intersection / union\n}\n\n/** Per-class non-maximum suppression; different classes never suppress each other. */\nexport function nonMaximumSuppression(detections: readonly Detection[], iouThreshold: number): Detection[] {\n const byScore = [...detections].sort((a, b) => b.score - a.score)\n const kept: Detection[] = []\n for (const detection of byScore) {\n const suppressed = kept.some(\n (other) => other.classId === detection.classId && iou(other.bbox, detection.bbox) > iouThreshold\n )\n if (!suppressed) kept.push(detection)\n }\n return kept\n}\n\n/**\n * Decode a YOLO output tensor.\n *\n * Two layouts are handled, because exports differ:\n * - [1, 4 + numClasses, numAnchors] -- the classic v8/v11 transposed form,\n * with cx,cy,w,h then per-class scores.\n * - [1, numDetections, 6] -- an end-to-end NMS export: x0,y0,x1,y1,score,class.\n */\nexport function decodeYoloOutput(\n data: Float32Array,\n dims: readonly number[],\n scoreThreshold: number\n): { bbox: [number, number, number, number]; score: number; classId: number }[] {\n const out: { bbox: [number, number, number, number]; score: number; classId: number }[] = []\n\n if (dims.length === 3 && dims[2] === 6) {\n const count = dims[1] as number\n for (let i = 0; i < count; i++) {\n const o = i * 6\n const score = data[o + 4] as number\n if (score < scoreThreshold) continue\n out.push({\n bbox: [\n data[o] as number,\n data[o + 1] as number,\n data[o + 2] as number,\n data[o + 3] as number,\n ],\n score,\n classId: Math.round(data[o + 5] as number),\n })\n }\n return out\n }\n\n if (dims.length !== 3) {\n throw new DetectionError(`Unsupported YOLO output shape [${dims.join(', ')}]`, 'decodeYoloOutput')\n }\n\n const channels = dims[1] as number\n const anchors = dims[2] as number\n const classCount = channels - 4\n\n for (let a = 0; a < anchors; a++) {\n let best = -1\n let bestScore = 0\n for (let c = 0; c < classCount; c++) {\n const score = data[(4 + c) * anchors + a] as number\n if (score > bestScore) {\n bestScore = score\n best = c\n }\n }\n if (best < 0 || bestScore < scoreThreshold) continue\n\n const cx = data[a] as number\n const cy = data[anchors + a] as number\n const w = data[2 * anchors + a] as number\n const h = data[3 * anchors + a] as number\n out.push({\n bbox: [cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2],\n score: bestScore,\n classId: best,\n })\n }\n return out\n}\n\nexport class YoloOnnxDetector extends Stage<YoloOnnxDetectorParams> {\n // Annotated as string rather than inferred as a literal, so SignatureDetector\n // and FaceDetector can narrow it to their own names.\n readonly name: string = 'YoloOnnxDetector'\n\n private session: import('onnxruntime-web').InferenceSession | null = null\n private loading: Promise<import('onnxruntime-web').InferenceSession> | null = null\n private inputSize = DEFAULT_YOLO_INPUT\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super(\n resolveParams(YOLO_DETECTOR_DEFAULTS, options, {\n model: (value) => {\n if (!value) throw new RangeError('model is required')\n },\n })\n )\n }\n\n override async init(): Promise<void> {\n await this.getSession()\n }\n\n private getSession(): Promise<import('onnxruntime-web').InferenceSession> {\n if (this.session) return Promise.resolve(this.session)\n if (this.loading) return this.loading\n\n this.loading = (async () => {\n const { model } = this.params\n const spec = /^https?:\\/\\//.test(model)\n ? { repo: 'yolo', files: [{ path: model }] }\n : { repo: model, files: [{ path: 'model.onnx' }] }\n\n const files = await ensureModelFiles(spec)\n const bytes = files[spec.files[0]?.path ?? '']\n if (!bytes) throw new DetectionError(`Model ${model} not found`, this.name)\n\n const session = await createSession(bytes, {\n executionProviders: getConfig().executionProviders,\n })\n this.inputSize = readInputSize(session)\n this.session = session\n return session\n })()\n\n this.loading.catch(() => {\n this.loading = null\n })\n return this.loading\n }\n\n override async dispose(): Promise<void> {\n await this.session?.release()\n this.session = null\n this.loading = null\n }\n\n protected async apply(input: unknown, row: Row): Promise<DetectorOutput> {\n const image = input as ScaleDpImage | undefined\n // Check `exception` first. A failed upstream stage returns a well-formed but\n // empty Image, so testing the bytes first would report \"no decoded bytes\" and\n // bury the real cause.\n if (image?.exception) {\n throw new DetectionError(`Upstream stage failed: ${image.exception}`, this.name)\n }\n if (!image || !(image.data instanceof Uint8Array) || image.data.byteLength === 0) {\n throw new DetectionError('Expected an Image with decoded bytes', this.name)\n }\n\n const bitmap = await decodeImage(image.data)\n let detections: Detection[]\n try {\n detections = await this.detect(bitmap)\n } finally {\n bitmap.close()\n }\n\n const bboxes: Box[] = detections.map((d) => boxFromBBox(d.bbox, { text: d.label, score: d.score }))\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: this.params.outputType,\n bboxes,\n })\n }\n\n /** Run the model over one decoded image, returning source-space detections. */\n async detect(source: ImageBitmap | OffscreenCanvas): Promise<Detection[]> {\n const session = await this.getSession()\n const size = this.inputSize\n\n // Centred padding, no mean/std -- YOLO's convention, unlike DBNet's.\n const fitted = letterbox(\n source,\n { width: size, height: size },\n {\n padding: 'center',\n fill: '#ffffff',\n }\n )\n const tensorData = toNchwFloat32(toImageData(fitted.canvas))\n\n const { Tensor } = await import('onnxruntime-web')\n const inputName = session.inputNames[0]\n const outputName = session.outputNames[0]\n if (!inputName || !outputName) {\n throw new DetectionError('Model exposes no input or output', this.name)\n }\n\n const outputs = await session.run({\n [inputName]: new Tensor('float32', tensorData, [1, 3, size, size]),\n })\n const output = outputs[outputName]\n if (!output) throw new DetectionError(`Model produced no \"${outputName}\" output`, this.name)\n\n const raw = decodeYoloOutput(output.data as Float32Array, output.dims, this.params.scoreThreshold)\n\n const padX = Math.trunc((size - fitted.resized.width) / 2)\n const padY = Math.trunc((size - fitted.resized.height) / 2)\n const { labels, padding } = this.params\n\n const detections: Detection[] = raw.map(({ bbox, score, classId }) => {\n // Undo the centred pad first, then the scale.\n const x0 = (bbox[0] - padX) / fitted.scale\n const y0 = (bbox[1] - padY) / fitted.scale\n const x1 = (bbox[2] - padX) / fitted.scale\n const y1 = (bbox[3] - padY) / fitted.scale\n\n const padW = (x1 - x0) * padding\n const padH = (y1 - y0) * padding\n return {\n bbox: [\n Math.max(0, x0 - padW),\n Math.max(0, y0 - padH),\n Math.min(source.width, x1 + padW),\n Math.min(source.height, y1 + padH),\n ],\n score,\n classId,\n label: labels[classId] ?? `class_${classId}`,\n }\n })\n\n return nonMaximumSuppression(detections, this.params.iouThreshold)\n }\n\n protected onError(message: string, row: Row): DetectorOutput {\n return createDetectorOutput({\n path: String(row[this.params.pathCol] ?? 'memory'),\n type: this.params.outputType,\n exception: message,\n })\n }\n}\n\n/** Input side length from the graph, falling back when the dim is symbolic. */\nfunction readInputSize(session: import('onnxruntime-web').InferenceSession): number {\n const meta = (\n session as unknown as {\n inputMetadata?: { dims?: (number | string)[] }[]\n }\n ).inputMetadata?.[0]\n const dims = meta?.dims\n const height = dims?.[2]\n return typeof height === 'number' && height > 0 ? height : DEFAULT_YOLO_INPUT\n}\n","/**\n * Object detection for @stabrise/scaledp.\n *\n * Mirrors ScaleDP's YOLO detector stages. Requires the optional peer dependency\n * `onnxruntime-web`.\n */\n\nimport { YOLO_DETECTOR_DEFAULTS, YoloOnnxDetector, type YoloOnnxDetectorParams } from './yolo-onnx.js'\n\nexport type { Detection, YoloOnnxDetectorParams } from './yolo-onnx.js'\nexport {\n DEFAULT_YOLO_INPUT,\n decodeYoloOutput,\n iou,\n nonMaximumSuppression,\n YOLO_DETECTOR_DEFAULTS,\n YoloOnnxDetector,\n} from './yolo-onnx.js'\n\n/** Signature detection, mirroring ScaleDP's `SignatureDetector`. */\nexport class SignatureDetector extends YoloOnnxDetector {\n override readonly name = 'SignatureDetector'\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/signature_detection',\n labels: ['signature'],\n outputCol: 'signatures',\n outputType: 'signature',\n scoreThreshold: 0.2,\n ...options,\n })\n }\n}\n\n/** Face detection, mirroring ScaleDP's `FaceDetector`. */\nexport class FaceDetector extends YoloOnnxDetector {\n override readonly name = 'FaceDetector'\n\n constructor(options: Partial<YoloOnnxDetectorParams> = {}) {\n super({\n ...YOLO_DETECTOR_DEFAULTS,\n model: 'StabRise/face_detection',\n labels: ['face'],\n outputCol: 'faces',\n outputType: 'face',\n scoreThreshold: 0.2,\n ...options,\n })\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { n as Box } from "./box-DAfzwfhA.js";
2
+ //#region src/schemas/detector-output.d.ts
3
+ interface DetectorOutput {
4
+ path: string;
5
+ /** Engine that produced the boxes: 'paddle' | 'dbnet-onnx' | 'yolo' | 'tesseract' | ... */
6
+ type: string;
7
+ bboxes: Box[];
8
+ exception: string;
9
+ }
10
+ declare function createDetectorOutput(init?: Partial<DetectorOutput>): DetectorOutput;
11
+ //#endregion
12
+ export { createDetectorOutput as n, DetectorOutput as t };
13
+ //# sourceMappingURL=detector-output-C0Qt-jEq.d.ts.map
@@ -0,0 +1,13 @@
1
+ //#region src/schemas/detector-output.ts
2
+ function createDetectorOutput(init = {}) {
3
+ return {
4
+ path: init.path ?? "memory",
5
+ type: init.type ?? "detector",
6
+ bboxes: init.bboxes ?? [],
7
+ exception: init.exception ?? ""
8
+ };
9
+ }
10
+ //#endregion
11
+ export { createDetectorOutput as t };
12
+
13
+ //# sourceMappingURL=detector-output-lyF1Mqb8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector-output-lyF1Mqb8.js","names":[],"sources":["../src/schemas/detector-output.ts"],"sourcesContent":["/** Port of `scaledp/schemas/DetectorOutput.py`. */\n\nimport type { Box } from './box.js'\n\nexport interface DetectorOutput {\n path: string\n /** Engine that produced the boxes: 'paddle' | 'dbnet-onnx' | 'yolo' | 'tesseract' | ... */\n type: string\n bboxes: Box[]\n exception: string\n}\n\nexport function createDetectorOutput(init: Partial<DetectorOutput> = {}): DetectorOutput {\n return {\n path: init.path ?? 'memory',\n type: init.type ?? 'detector',\n bboxes: init.bboxes ?? [],\n exception: init.exception ?? '',\n }\n}\n"],"mappings":";AAYA,SAAgB,qBAAqB,OAAgC,CAAC,GAAmB;CACrF,OAAO;EACH,MAAM,KAAK,QAAQ;EACnB,MAAM,KAAK,QAAQ;EACnB,QAAQ,KAAK,UAAU,CAAC;EACxB,WAAW,KAAK,aAAa;CACjC;AACJ"}
@@ -0,0 +1,66 @@
1
+ import { t as DetectorOutput } from "../detector-output-C0Qt-jEq.js";
2
+ import { t as Document } from "../document-B8I61TiY.js";
3
+ import { n as NerOutput, t as Entity } from "../entity-CedtRhU1.js";
4
+ import { n as ScaleDpImage } from "../image-Dc5TSg46.js";
5
+ //#region src/display/index.d.ts
6
+ /**
7
+ * A stable colour per entity group.
8
+ *
9
+ * Python picks a random colour each run, so two renders of the same document
10
+ * never match. Hashing the group name keeps 'PERSON' one colour everywhere.
11
+ */
12
+ declare function colorForGroup(name: string): string;
13
+ /** Replace a container's contents with `node`. Accepts a selector or element. */
14
+ declare function renderInto(target: string | HTMLElement, node: Node): HTMLElement;
15
+ interface ShowImageOptions {
16
+ /** CSS width, e.g. '600px' or '100%'. */
17
+ width?: string;
18
+ alt?: string;
19
+ }
20
+ /**
21
+ * An `<img>` for a ScaleDP image. Mirrors `show_image`.
22
+ *
23
+ * The object URL is revoked once the image has decoded -- holding one per page
24
+ * leaks the whole blob for the lifetime of the document.
25
+ */
26
+ declare function showImage(image: ScaleDpImage, options?: ShowImageOptions): HTMLElement;
27
+ interface ShowTextOptions {
28
+ /**
29
+ * Preserve the document's own layout. Correct when the OCR stage ran with
30
+ * `keepFormatting`, which encodes the layout in spaces and blank lines.
31
+ */
32
+ preserveLayout?: boolean;
33
+ maxHeight?: string;
34
+ }
35
+ /** A `<pre>` of the recognized text. Mirrors `show_text`. */
36
+ declare function showText(document_: Document, options?: ShowTextOptions): HTMLElement;
37
+ /** Pretty-printed JSON. Mirrors `show_json`. */
38
+ declare function showJson(value: unknown, indent?: number): HTMLElement;
39
+ interface ShowNerOptions {
40
+ /** Maximum rows; 0 shows all. Python defaults to 20. */
41
+ limit?: number;
42
+ /** Only these groups. */
43
+ whiteList?: readonly string[];
44
+ }
45
+ /** A table of entities. Mirrors `show_ner`. */
46
+ declare function showNer(ner: NerOutput, options?: ShowNerOptions): HTMLElement;
47
+ interface VisualizeNerOptions {
48
+ /** Only highlight these groups. */
49
+ labelsList?: readonly string[];
50
+ /** Render the group name beside each highlight. */
51
+ showLabels?: boolean;
52
+ }
53
+ /**
54
+ * The document text with entities highlighted inline. Mirrors `visualize_ner`.
55
+ *
56
+ * Splices spans by character offset, which is exactly what `Entity.start`/`end`
57
+ * index. Overlapping entities are dropped rather than nested: the highest-
58
+ * scoring one wins, because two spans cannot occupy the same characters in a
59
+ * flat text run.
60
+ */
61
+ declare function visualizeNer(document_: Document, ner: NerOutput, options?: VisualizeNerOptions): HTMLElement;
62
+ /** A summary table of detected boxes. */
63
+ declare function showBoxes(output: DetectorOutput | Document, limit?: number): HTMLElement;
64
+ //#endregion
65
+ export { type DetectorOutput, type Document, type Entity, type NerOutput, type ScaleDpImage, ShowImageOptions, ShowNerOptions, ShowTextOptions, VisualizeNerOptions, colorForGroup, renderInto, showBoxes, showImage, showJson, showNer, showText, visualizeNer };
66
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,237 @@
1
+ //#region src/display/index.ts
2
+ /**
3
+ * A stable colour per entity group.
4
+ *
5
+ * Python picks a random colour each run, so two renders of the same document
6
+ * never match. Hashing the group name keeps 'PERSON' one colour everywhere.
7
+ */
8
+ function colorForGroup(name) {
9
+ let hash = 0;
10
+ for (let i = 0; i < name.length; i++) hash = hash * 31 + name.charCodeAt(i) | 0;
11
+ return `hsl(${Math.abs(hash) % 360}, 70%, 45%)`;
12
+ }
13
+ function element(tag, props = {}) {
14
+ const node = document.createElement(tag);
15
+ if (props.text !== void 0) node.textContent = props.text;
16
+ if (props.className) node.className = props.className;
17
+ if (props.style) Object.assign(node.style, props.style);
18
+ return node;
19
+ }
20
+ /** Replace a container's contents with `node`. Accepts a selector or element. */
21
+ function renderInto(target, node) {
22
+ const host = typeof target === "string" ? document.querySelector(target) : target;
23
+ if (!host) throw new Error(`No element matches ${String(target)}`);
24
+ host.replaceChildren(node);
25
+ return host;
26
+ }
27
+ /**
28
+ * An `<img>` for a ScaleDP image. Mirrors `show_image`.
29
+ *
30
+ * The object URL is revoked once the image has decoded -- holding one per page
31
+ * leaks the whole blob for the lifetime of the document.
32
+ */
33
+ function showImage(image, options = {}) {
34
+ if (image.exception) return errorBlock(image.exception);
35
+ const bytes = new Uint8Array(image.data.byteLength);
36
+ bytes.set(image.data);
37
+ const url = URL.createObjectURL(new Blob([bytes.buffer], { type: `image/${image.imageType}` }));
38
+ const img = element("img", { style: {
39
+ maxWidth: options.width ?? "100%",
40
+ height: "auto"
41
+ } });
42
+ img.alt = options.alt ?? image.path;
43
+ img.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
44
+ img.addEventListener("error", () => URL.revokeObjectURL(url), { once: true });
45
+ img.src = url;
46
+ return img;
47
+ }
48
+ /** A `<pre>` of the recognized text. Mirrors `show_text`. */
49
+ function showText(document_, options = {}) {
50
+ if (document_.exception) return errorBlock(document_.exception);
51
+ return element("pre", {
52
+ text: document_.text,
53
+ className: "scaledp-text",
54
+ style: {
55
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
56
+ fontSize: "12px",
57
+ whiteSpace: options.preserveLayout === false ? "pre-wrap" : "pre",
58
+ overflowX: "auto",
59
+ maxHeight: options.maxHeight ?? "30rem",
60
+ margin: "0"
61
+ }
62
+ });
63
+ }
64
+ /** Pretty-printed JSON. Mirrors `show_json`. */
65
+ function showJson(value, indent = 2) {
66
+ return element("pre", {
67
+ text: typeof value === "string" ? value : JSON.stringify(value, null, indent),
68
+ className: "scaledp-json",
69
+ style: {
70
+ fontFamily: "ui-monospace, monospace",
71
+ fontSize: "12px",
72
+ overflowX: "auto"
73
+ }
74
+ });
75
+ }
76
+ /** A table of entities. Mirrors `show_ner`. */
77
+ function showNer(ner, options = {}) {
78
+ if (ner.exception) return errorBlock(ner.exception);
79
+ const allowed = new Set(options.whiteList ?? []);
80
+ let entities = allowed.size > 0 ? ner.entities.filter((e) => allowed.has(e.entity_group)) : ner.entities;
81
+ const limit = options.limit ?? 20;
82
+ const total = entities.length;
83
+ if (limit > 0) entities = entities.slice(0, limit);
84
+ if (total === 0) return element("p", { text: "No entities found." });
85
+ const table = element("table", { className: "scaledp-ner" });
86
+ table.style.borderCollapse = "collapse";
87
+ const header = table.insertRow();
88
+ for (const label of [
89
+ "Type",
90
+ "Text",
91
+ "Score",
92
+ "Start",
93
+ "End",
94
+ "Boxes"
95
+ ]) {
96
+ const th = document.createElement("th");
97
+ th.textContent = label;
98
+ th.style.cssText = "border:1px solid #ddd;padding:4px 8px;text-align:left";
99
+ header.append(th);
100
+ }
101
+ for (const entity of entities) {
102
+ const tr = table.insertRow();
103
+ const swatch = element("span", { style: {
104
+ display: "inline-block",
105
+ width: "8px",
106
+ height: "8px",
107
+ borderRadius: "50%",
108
+ marginRight: "6px",
109
+ background: colorForGroup(entity.entity_group)
110
+ } });
111
+ [
112
+ entity.entity_group,
113
+ entity.word,
114
+ entity.score.toFixed(3),
115
+ String(entity.start),
116
+ String(entity.end),
117
+ String(entity.boxes.length)
118
+ ].forEach((value, index) => {
119
+ const cell = tr.insertCell();
120
+ cell.style.cssText = "border:1px solid #ddd;padding:4px 8px";
121
+ if (index === 0) cell.append(swatch);
122
+ cell.append(typeof value === "string" ? document.createTextNode(value) : value);
123
+ });
124
+ }
125
+ const wrapper = element("div");
126
+ wrapper.append(table);
127
+ if (limit > 0 && total > limit) wrapper.append(element("p", { text: `Showing ${limit} of ${total} entities.` }));
128
+ return wrapper;
129
+ }
130
+ /**
131
+ * The document text with entities highlighted inline. Mirrors `visualize_ner`.
132
+ *
133
+ * Splices spans by character offset, which is exactly what `Entity.start`/`end`
134
+ * index. Overlapping entities are dropped rather than nested: the highest-
135
+ * scoring one wins, because two spans cannot occupy the same characters in a
136
+ * flat text run.
137
+ */
138
+ function visualizeNer(document_, ner, options = {}) {
139
+ if (document_.exception) return errorBlock(document_.exception);
140
+ if (ner.exception) return errorBlock(ner.exception);
141
+ const allowed = new Set(options.labelsList ?? []);
142
+ const entities = (allowed.size > 0 ? ner.entities.filter((e) => allowed.has(e.entity_group)) : ner.entities).filter((e) => e.start >= 0 && e.end > e.start && e.end <= document_.text.length).sort((a, b) => a.start - b.start || b.score - a.score);
143
+ const container = element("div", {
144
+ className: "scaledp-ner-text",
145
+ style: {
146
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
147
+ fontSize: "12px",
148
+ whiteSpace: "pre-wrap",
149
+ lineHeight: "1.9"
150
+ }
151
+ });
152
+ let cursor = 0;
153
+ for (const entity of entities) {
154
+ if (entity.start < cursor) continue;
155
+ if (entity.start > cursor) container.append(document.createTextNode(document_.text.slice(cursor, entity.start)));
156
+ const color = colorForGroup(entity.entity_group);
157
+ const mark = element("span", {
158
+ text: document_.text.slice(entity.start, entity.end),
159
+ style: {
160
+ background: color,
161
+ color: "#fff",
162
+ borderRadius: "3px",
163
+ padding: "1px 3px"
164
+ }
165
+ });
166
+ mark.title = `${entity.entity_group} (${entity.score.toFixed(3)})`;
167
+ container.append(mark);
168
+ if (options.showLabels) container.append(element("sup", {
169
+ text: entity.entity_group,
170
+ style: {
171
+ color,
172
+ fontSize: "9px",
173
+ marginLeft: "2px"
174
+ }
175
+ }));
176
+ cursor = entity.end;
177
+ }
178
+ container.append(document.createTextNode(document_.text.slice(cursor)));
179
+ return container;
180
+ }
181
+ /** A summary table of detected boxes. */
182
+ function showBoxes(output, limit = 20) {
183
+ if (output.exception) return errorBlock(output.exception);
184
+ const boxes = output.bboxes;
185
+ const table = element("table");
186
+ table.style.borderCollapse = "collapse";
187
+ const header = table.insertRow();
188
+ for (const label of [
189
+ "Text",
190
+ "Score",
191
+ "x",
192
+ "y",
193
+ "w",
194
+ "h",
195
+ "angle"
196
+ ]) {
197
+ const th = document.createElement("th");
198
+ th.textContent = label;
199
+ th.style.cssText = "border:1px solid #ddd;padding:4px 8px;text-align:left";
200
+ header.append(th);
201
+ }
202
+ for (const box of limit > 0 ? boxes.slice(0, limit) : boxes) {
203
+ const tr = table.insertRow();
204
+ for (const value of [
205
+ box.text,
206
+ box.score.toFixed(3),
207
+ String(box.x),
208
+ String(box.y),
209
+ String(box.width),
210
+ String(box.height),
211
+ box.angle.toFixed(1)
212
+ ]) {
213
+ const cell = tr.insertCell();
214
+ cell.style.cssText = "border:1px solid #ddd;padding:4px 8px";
215
+ cell.textContent = value;
216
+ }
217
+ }
218
+ const wrapper = element("div");
219
+ wrapper.append(table);
220
+ if (limit > 0 && boxes.length > limit) wrapper.append(element("p", { text: `Showing ${limit} of ${boxes.length} boxes.` }));
221
+ return wrapper;
222
+ }
223
+ function errorBlock(message) {
224
+ return element("pre", {
225
+ text: message,
226
+ style: {
227
+ color: "#b00020",
228
+ whiteSpace: "pre-wrap",
229
+ fontFamily: "ui-monospace, monospace",
230
+ fontSize: "12px"
231
+ }
232
+ });
233
+ }
234
+ //#endregion
235
+ export { colorForGroup, renderInto, showBoxes, showImage, showJson, showNer, showText, visualizeNer };
236
+
237
+ //# sourceMappingURL=index.js.map