@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,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
|