tryharness 0.7.23 → 0.7.26
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/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- package/package.json +3 -3
package/lib/ocr.mjs
CHANGED
|
@@ -14,24 +14,54 @@ import jpeg from "jpeg-js";
|
|
|
14
14
|
export const MAX_PIXELS = 40e6;
|
|
15
15
|
// The detector's own settings (its inference.yml: thresh 0.2, box_thresh 0.45, unclip_ratio 1.4) at a long side of
|
|
16
16
|
// 960 to 2,048 pixels: a retina screenshot is shrunk to 2,048 across, a small image is enlarged.
|
|
17
|
-
const DET = {
|
|
17
|
+
const DET = {
|
|
18
|
+
min: 960,
|
|
19
|
+
max: 2_048,
|
|
20
|
+
thresh: 0.2,
|
|
21
|
+
boxThresh: 0.45,
|
|
22
|
+
unclip: 1.4,
|
|
23
|
+
mean: [0.485, 0.456, 0.406],
|
|
24
|
+
std: [0.229, 0.224, 0.225],
|
|
25
|
+
};
|
|
18
26
|
const REC = { height: 48, mean: [0.5, 0.5, 0.5], std: [0.5, 0.5, 0.5] };
|
|
19
27
|
// Boxes read per scheduler job, so a long image does not hold the ONNX lane from the firewall's checks.
|
|
20
28
|
const REC_PER_JOB = 24;
|
|
21
29
|
|
|
22
|
-
export const imageKind = (bytes) =>
|
|
23
|
-
|
|
30
|
+
export const imageKind = (bytes) =>
|
|
31
|
+
bytes?.length > 8 &&
|
|
32
|
+
bytes[0] === 0x89 &&
|
|
33
|
+
bytes[1] === 0x50 &&
|
|
34
|
+
bytes[2] === 0x4e &&
|
|
35
|
+
bytes[3] === 0x47
|
|
36
|
+
? "png"
|
|
37
|
+
: bytes?.length > 3 &&
|
|
38
|
+
bytes[0] === 0xff &&
|
|
39
|
+
bytes[1] === 0xd8 &&
|
|
40
|
+
bytes[2] === 0xff
|
|
41
|
+
? "jpeg"
|
|
42
|
+
: null;
|
|
24
43
|
|
|
25
44
|
/** { width, height, data: RGBA } from PNG or JPEG bytes; null for any other format. Throws on a broken or oversized image. */
|
|
26
45
|
export function decodeImage(bytes) {
|
|
27
|
-
const buffer = Buffer.from(
|
|
46
|
+
const buffer = Buffer.from(
|
|
47
|
+
bytes.buffer ?? bytes,
|
|
48
|
+
bytes.byteOffset ?? 0,
|
|
49
|
+
bytes.byteLength ?? bytes.length
|
|
50
|
+
);
|
|
28
51
|
const kind = imageKind(buffer);
|
|
29
52
|
if (kind === "png") {
|
|
30
|
-
if (buffer.readUInt32BE(16) * buffer.readUInt32BE(20) > MAX_PIXELS)
|
|
53
|
+
if (buffer.readUInt32BE(16) * buffer.readUInt32BE(20) > MAX_PIXELS)
|
|
54
|
+
throw new Error("the image is too large to check");
|
|
31
55
|
const { width, height, data } = pngjs.PNG.sync.read(buffer);
|
|
32
56
|
return { width, height, data };
|
|
33
57
|
}
|
|
34
|
-
if (kind === "jpeg")
|
|
58
|
+
if (kind === "jpeg")
|
|
59
|
+
return jpeg.decode(buffer, {
|
|
60
|
+
useTArray: true,
|
|
61
|
+
formatAsRGBA: true,
|
|
62
|
+
maxResolutionInMP: MAX_PIXELS / 1e6,
|
|
63
|
+
maxMemoryUsageInMB: 1_024,
|
|
64
|
+
});
|
|
35
65
|
return null;
|
|
36
66
|
}
|
|
37
67
|
|
|
@@ -39,13 +69,22 @@ export function decodeImage(bytes) {
|
|
|
39
69
|
export function characterList(yml) {
|
|
40
70
|
const lines = yml.split(/\r?\n/);
|
|
41
71
|
const out = [];
|
|
42
|
-
for (const line of lines.slice(
|
|
72
|
+
for (const line of lines.slice(
|
|
73
|
+
lines.findIndex((each) => /^\s*character_dict:\s*$/.test(each)) + 1
|
|
74
|
+
)) {
|
|
43
75
|
const item = line.match(/^\s*- (.*)$/);
|
|
44
76
|
if (!item) break;
|
|
45
77
|
const value = item[1];
|
|
46
|
-
out.push(
|
|
78
|
+
out.push(
|
|
79
|
+
value.startsWith("'")
|
|
80
|
+
? value.slice(1, -1).replace(/''/g, "'")
|
|
81
|
+
: value.startsWith('"')
|
|
82
|
+
? JSON.parse(value)
|
|
83
|
+
: value
|
|
84
|
+
);
|
|
47
85
|
}
|
|
48
|
-
if (!out.length)
|
|
86
|
+
if (!out.length)
|
|
87
|
+
throw new Error("the recogniser's character list is missing");
|
|
49
88
|
return out;
|
|
50
89
|
}
|
|
51
90
|
|
|
@@ -57,10 +96,16 @@ function taps(start, length, count, limit) {
|
|
|
57
96
|
for (let i = 0; i < count; i += 1) {
|
|
58
97
|
const list = [];
|
|
59
98
|
if (scale > 1) {
|
|
60
|
-
const a = start + i * scale,
|
|
61
|
-
|
|
99
|
+
const a = start + i * scale,
|
|
100
|
+
b = a + scale;
|
|
101
|
+
for (let p = Math.floor(a); p < b; p += 1) {
|
|
102
|
+
const w = Math.min(b, p + 1) - Math.max(a, p);
|
|
103
|
+
if (w > 0) list.push(at(p), w / scale);
|
|
104
|
+
}
|
|
62
105
|
} else {
|
|
63
|
-
const c = start + (i + 0.5) * scale - 0.5,
|
|
106
|
+
const c = start + (i + 0.5) * scale - 0.5,
|
|
107
|
+
p = Math.floor(c),
|
|
108
|
+
f = c - p;
|
|
64
109
|
list.push(at(p), 1 - f, at(p + 1), f);
|
|
65
110
|
}
|
|
66
111
|
out[i] = list;
|
|
@@ -70,18 +115,30 @@ function taps(start, length, count, limit) {
|
|
|
70
115
|
|
|
71
116
|
/** A region of the image resampled to width x height as a normalized BGR tensor (PaddleOCR's models read OpenCV's BGR); transparency is laid on white. */
|
|
72
117
|
function tensorOf(image, [x0, y0, x1, y1], width, height, { mean, std }) {
|
|
73
|
-
const xs = taps(x0, x1 - x0, width, image.width),
|
|
74
|
-
|
|
118
|
+
const xs = taps(x0, x1 - x0, width, image.width),
|
|
119
|
+
ys = taps(y0, y1 - y0, height, image.height);
|
|
120
|
+
const plane = width * height,
|
|
121
|
+
out = new Float32Array(3 * plane),
|
|
122
|
+
data = image.data,
|
|
123
|
+
stride = image.width * 4;
|
|
75
124
|
for (let j = 0; j < height; j += 1) {
|
|
76
125
|
const ty = ys[j];
|
|
77
126
|
for (let i = 0; i < width; i += 1) {
|
|
78
127
|
const tx = xs[i];
|
|
79
|
-
let r = 0,
|
|
128
|
+
let r = 0,
|
|
129
|
+
g = 0,
|
|
130
|
+
b = 0;
|
|
80
131
|
for (let v = 0; v < ty.length; v += 2) {
|
|
81
|
-
const row = ty[v] * stride,
|
|
132
|
+
const row = ty[v] * stride,
|
|
133
|
+
wy = ty[v + 1];
|
|
82
134
|
for (let u = 0; u < tx.length; u += 2) {
|
|
83
|
-
const p = row + tx[u] * 4,
|
|
84
|
-
|
|
135
|
+
const p = row + tx[u] * 4,
|
|
136
|
+
w = wy * tx[u + 1],
|
|
137
|
+
a = data[p + 3] / 255,
|
|
138
|
+
white = 255 * (1 - a);
|
|
139
|
+
r += (data[p] * a + white) * w;
|
|
140
|
+
g += (data[p + 1] * a + white) * w;
|
|
141
|
+
b += (data[p + 2] * a + white) * w;
|
|
85
142
|
}
|
|
86
143
|
}
|
|
87
144
|
const k = j * width + i;
|
|
@@ -96,23 +153,43 @@ function tensorOf(image, [x0, y0, x1, y1], width, height, { mean, std }) {
|
|
|
96
153
|
// DB post-processing: the regions above the threshold whose mean probability passes the box threshold, each grown by
|
|
97
154
|
// area x unclip / perimeter on every side (the detector is trained on text shrunk by that much).
|
|
98
155
|
function regions(map, width, height) {
|
|
99
|
-
const seen = new Uint8Array(width * height),
|
|
156
|
+
const seen = new Uint8Array(width * height),
|
|
157
|
+
stack = new Int32Array(width * height),
|
|
158
|
+
boxes = [];
|
|
100
159
|
for (let start = 0; start < map.length; start += 1) {
|
|
101
160
|
if (seen[start] || map[start] <= DET.thresh) continue;
|
|
102
161
|
seen[start] = 1;
|
|
103
|
-
let top = 0,
|
|
162
|
+
let top = 0,
|
|
163
|
+
x0 = width,
|
|
164
|
+
y0 = height,
|
|
165
|
+
x1 = -1,
|
|
166
|
+
y1 = -1,
|
|
167
|
+
sum = 0,
|
|
168
|
+
count = 0;
|
|
104
169
|
stack[top++] = start;
|
|
105
|
-
const visit = (q) => {
|
|
170
|
+
const visit = (q) => {
|
|
171
|
+
if (!seen[q] && map[q] > DET.thresh) {
|
|
172
|
+
seen[q] = 1;
|
|
173
|
+
stack[top++] = q;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
106
176
|
while (top) {
|
|
107
|
-
const p = stack[--top],
|
|
108
|
-
|
|
109
|
-
|
|
177
|
+
const p = stack[--top],
|
|
178
|
+
x = p % width,
|
|
179
|
+
y = (p - x) / width;
|
|
180
|
+
sum += map[p];
|
|
181
|
+
count += 1;
|
|
182
|
+
if (x < x0) x0 = x;
|
|
183
|
+
if (x > x1) x1 = x;
|
|
184
|
+
if (y < y0) y0 = y;
|
|
185
|
+
if (y > y1) y1 = y;
|
|
110
186
|
if (x > 0) visit(p - 1);
|
|
111
187
|
if (x < width - 1) visit(p + 1);
|
|
112
188
|
if (y > 0) visit(p - width);
|
|
113
189
|
if (y < height - 1) visit(p + width);
|
|
114
190
|
}
|
|
115
|
-
const w = x1 - x0 + 1,
|
|
191
|
+
const w = x1 - x0 + 1,
|
|
192
|
+
h = y1 - y0 + 1;
|
|
116
193
|
if (Math.min(w, h) < 3 || sum / count < DET.boxThresh) continue;
|
|
117
194
|
const d = (w * h * DET.unclip) / (2 * (w + h));
|
|
118
195
|
boxes.push([x0 - d, y0 - d, x1 + 1 + d, y1 + 1 + d]);
|
|
@@ -126,37 +203,70 @@ function regions(map, width, height) {
|
|
|
126
203
|
// the probability map is stitched from rows at least MARGIN from a band's edge. Lines are never cut: regions are found
|
|
127
204
|
// on the whole map. On the image-redaction eval's screens this found what one pass did, and the OCR models' memory
|
|
128
205
|
// levelled off at 544 MB instead of growing past 2 GB (160-row bands; 256 rows: 766 MB and about 10% faster).
|
|
129
|
-
const BAND = 160,
|
|
206
|
+
const BAND = 160,
|
|
207
|
+
MARGIN = 32,
|
|
208
|
+
WINDOW = BAND + 2 * MARGIN;
|
|
130
209
|
|
|
131
210
|
async function detect(image, { ort, graph }) {
|
|
132
211
|
const long = Math.max(image.width, image.height);
|
|
133
212
|
const ratio = Math.min(DET.max, Math.max(DET.min, long)) / long;
|
|
134
|
-
const width = Math.max(32, Math.round((image.width * ratio) / 32) * 32),
|
|
213
|
+
const width = Math.max(32, Math.round((image.width * ratio) / 32) * 32),
|
|
214
|
+
height = Math.max(32, Math.round((image.height * ratio) / 32) * 32);
|
|
135
215
|
const columns = Math.ceil(width / 512) * 512;
|
|
136
216
|
const session = await graph("det/inference.onnx");
|
|
137
|
-
const sx = image.width / width,
|
|
217
|
+
const sx = image.width / width,
|
|
218
|
+
sy = image.height / height;
|
|
138
219
|
const map = new Float32Array(width * height);
|
|
139
220
|
for (let top = 0; top < height; top += BAND) {
|
|
140
|
-
const a = Math.max(0, Math.min(top - MARGIN, height - WINDOW)),
|
|
141
|
-
|
|
221
|
+
const a = Math.max(0, Math.min(top - MARGIN, height - WINDOW)),
|
|
222
|
+
rows = Math.min(WINDOW, height - a);
|
|
223
|
+
const pixels = tensorOf(
|
|
224
|
+
image,
|
|
225
|
+
[0, a * sy, image.width, (a + rows) * sy],
|
|
226
|
+
width,
|
|
227
|
+
rows,
|
|
228
|
+
DET
|
|
229
|
+
),
|
|
230
|
+
input = new Float32Array(3 * WINDOW * columns);
|
|
142
231
|
// Padding repeats the edge pixels: a flat block beside the image made the detector see text in empty space.
|
|
143
|
-
for (let c = 0; c < 3; c += 1)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
232
|
+
for (let c = 0; c < 3; c += 1)
|
|
233
|
+
for (let y = 0; y < WINDOW; y += 1) {
|
|
234
|
+
const from = (c * rows + Math.min(y, rows - 1)) * width,
|
|
235
|
+
to = (c * WINDOW + y) * columns;
|
|
236
|
+
input.set(pixels.subarray(from, from + width), to);
|
|
237
|
+
input.fill(pixels[from + width - 1], to + width, to + columns);
|
|
238
|
+
}
|
|
239
|
+
const band = (
|
|
240
|
+
await session.run({
|
|
241
|
+
[session.inputNames[0]]: new ort.Tensor("float32", input, [
|
|
242
|
+
1,
|
|
243
|
+
3,
|
|
244
|
+
WINDOW,
|
|
245
|
+
columns,
|
|
246
|
+
]),
|
|
247
|
+
})
|
|
248
|
+
)[session.outputNames[0]].data;
|
|
249
|
+
for (let y = top; y < Math.min(height, top + BAND); y += 1)
|
|
250
|
+
map.set(
|
|
251
|
+
band.subarray((y - a) * columns, (y - a) * columns + width),
|
|
252
|
+
y * width
|
|
253
|
+
);
|
|
150
254
|
}
|
|
151
255
|
return regions(map, width, height)
|
|
152
|
-
.map(([x0, y0, x1, y1]) => [
|
|
256
|
+
.map(([x0, y0, x1, y1]) => [
|
|
257
|
+
Math.max(0, x0 * sx),
|
|
258
|
+
Math.max(0, y0 * sy),
|
|
259
|
+
Math.min(image.width, x1 * sx),
|
|
260
|
+
Math.min(image.height, y1 * sy),
|
|
261
|
+
])
|
|
153
262
|
.filter(([x0, y0, x1, y1]) => x1 - x0 >= 4 && y1 - y0 >= 4);
|
|
154
263
|
}
|
|
155
264
|
|
|
156
265
|
// ONNX Runtime keeps memory for every input shape it has run (400 crop widths: 108 MB grew to 513 MB, October 14), so
|
|
157
266
|
// crops are padded on the right to a few widths, with 0 after normalization as PaddleOCR's own batches pad. Over this
|
|
158
267
|
// eval's screens that changed 16 of 845 lines, by spacing or a dash, none of a planted value.
|
|
159
|
-
const bucket = (width) =>
|
|
268
|
+
const bucket = (width) =>
|
|
269
|
+
width <= 512 ? Math.ceil(width / 32) * 32 : Math.ceil(width / 64) * 64;
|
|
160
270
|
|
|
161
271
|
// CTC greedy decoding that keeps, for each character, the output steps it was read over (each step is a fixed slice of
|
|
162
272
|
// the crop's width).
|
|
@@ -164,32 +274,66 @@ async function recognise(image, box, characters, { ort, graph }) {
|
|
|
164
274
|
const session = await graph("rec/inference.onnx");
|
|
165
275
|
const [x0, y0, x1, y1] = box;
|
|
166
276
|
const width = Math.max(8, Math.round((REC.height * (x1 - x0)) / (y1 - y0)));
|
|
167
|
-
const padded = bucket(width),
|
|
168
|
-
|
|
277
|
+
const padded = bucket(width),
|
|
278
|
+
crop = tensorOf(image, box, width, REC.height, REC),
|
|
279
|
+
tensor = new Float32Array(3 * REC.height * padded);
|
|
280
|
+
for (let row = 0; row < 3 * REC.height; row += 1)
|
|
281
|
+
tensor.set(crop.subarray(row * width, (row + 1) * width), row * padded);
|
|
169
282
|
const input = new ort.Tensor("float32", tensor, [1, 3, REC.height, padded]);
|
|
170
|
-
const out = (await session.run({ [session.inputNames[0]]: input }))[
|
|
171
|
-
|
|
283
|
+
const out = (await session.run({ [session.inputNames[0]]: input }))[
|
|
284
|
+
session.outputNames[0]
|
|
285
|
+
];
|
|
286
|
+
const [, all, classes] = out.dims,
|
|
287
|
+
data = out.data;
|
|
172
288
|
const steps = Math.min(all, Math.ceil((all * width) / padded));
|
|
173
289
|
const space = classes === characters.length + 2 ? classes - 1 : -1;
|
|
174
290
|
const pixels = ((x1 - x0) / width) * (padded / all);
|
|
175
291
|
const read = [];
|
|
176
|
-
let total = 0,
|
|
292
|
+
let total = 0,
|
|
293
|
+
previous = 0;
|
|
177
294
|
for (let t = 0; t < steps; t += 1) {
|
|
178
|
-
let best = 0,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
295
|
+
let best = 0,
|
|
296
|
+
p = data[t * classes];
|
|
297
|
+
for (let c = 1; c < classes; c += 1)
|
|
298
|
+
if (data[t * classes + c] > p) {
|
|
299
|
+
p = data[t * classes + c];
|
|
300
|
+
best = c;
|
|
301
|
+
}
|
|
302
|
+
if (best && best === previous)
|
|
303
|
+
read[read.length - 1].to = x0 + (t + 1) * pixels;
|
|
304
|
+
else if (best) {
|
|
305
|
+
read.push({
|
|
306
|
+
ch: best === space ? " " : (characters[best - 1] ?? ""),
|
|
307
|
+
from: x0 + t * pixels,
|
|
308
|
+
to: x0 + (t + 1) * pixels,
|
|
309
|
+
});
|
|
310
|
+
total += p;
|
|
311
|
+
}
|
|
182
312
|
previous = best;
|
|
183
313
|
}
|
|
184
314
|
// One entry per UTF-16 unit, so chars lines up with the text's indexes.
|
|
185
315
|
const text = read.map((each) => each.ch).join("");
|
|
186
|
-
const chars = read.flatMap(({ ch, from, to }) =>
|
|
187
|
-
|
|
188
|
-
|
|
316
|
+
const chars = read.flatMap(({ ch, from, to }) =>
|
|
317
|
+
Array.from({ length: ch.length }, () => [Math.floor(from), Math.ceil(to)])
|
|
318
|
+
);
|
|
319
|
+
const lead = text.length - text.trimStart().length,
|
|
320
|
+
kept = text.trim().length;
|
|
321
|
+
return {
|
|
322
|
+
text: text.trim(),
|
|
323
|
+
box: [Math.floor(x0), Math.floor(y0), Math.ceil(x1), Math.ceil(y1)],
|
|
324
|
+
score: read.length ? total / read.length : 0,
|
|
325
|
+
chars: chars.slice(lead, lead + kept),
|
|
326
|
+
};
|
|
189
327
|
}
|
|
190
328
|
|
|
191
329
|
// Top to bottom, left to right within a row (rows: boxes whose tops are within a quarter of their combined height).
|
|
192
|
-
const readingOrder = (lines) =>
|
|
330
|
+
const readingOrder = (lines) =>
|
|
331
|
+
lines.sort((a, b) =>
|
|
332
|
+
Math.abs(a.box[1] - b.box[1]) <
|
|
333
|
+
(a.box[3] - a.box[1] + b.box[3] - b.box[1]) / 4
|
|
334
|
+
? a.box[0] - b.box[0]
|
|
335
|
+
: a.box[1] - b.box[1]
|
|
336
|
+
);
|
|
193
337
|
|
|
194
338
|
export function createOcr({ inference }) {
|
|
195
339
|
let characters = null;
|
|
@@ -201,17 +345,36 @@ export function createOcr({ inference }) {
|
|
|
201
345
|
const started = performance.now();
|
|
202
346
|
const image = input?.data && input.width ? input : decodeImage(input);
|
|
203
347
|
if (!image) throw new Error("only PNG and JPEG images can be read");
|
|
204
|
-
const boxes = await inference.onnx("ocr", (model) => detect(image, model), {
|
|
348
|
+
const boxes = await inference.onnx("ocr", (model) => detect(image, model), {
|
|
349
|
+
lane,
|
|
350
|
+
});
|
|
205
351
|
const lines = [];
|
|
206
352
|
for (let i = 0; i < boxes.length; i += REC_PER_JOB) {
|
|
207
|
-
lines.push(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
353
|
+
lines.push(
|
|
354
|
+
...(await inference.onnx(
|
|
355
|
+
"ocr",
|
|
356
|
+
async (model) => {
|
|
357
|
+
characters ||= characterList(
|
|
358
|
+
fs.readFileSync(
|
|
359
|
+
path.join(model.dir, "rec", "inference.yml"),
|
|
360
|
+
"utf8"
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
const read = [];
|
|
364
|
+
for (const box of boxes.slice(i, i + REC_PER_JOB))
|
|
365
|
+
read.push(await recognise(image, box, characters, model));
|
|
366
|
+
return read;
|
|
367
|
+
},
|
|
368
|
+
{ lane }
|
|
369
|
+
))
|
|
370
|
+
);
|
|
213
371
|
}
|
|
214
|
-
return {
|
|
372
|
+
return {
|
|
373
|
+
width: image.width,
|
|
374
|
+
height: image.height,
|
|
375
|
+
lines: readingOrder(lines.filter((line) => line.text)),
|
|
376
|
+
ms: Math.round(performance.now() - started),
|
|
377
|
+
};
|
|
215
378
|
};
|
|
216
379
|
return { read };
|
|
217
380
|
}
|