@customize-agent/knowledge 4.0.27 → 4.0.28
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.
|
@@ -49,6 +49,9 @@ export declare class TesseractJsProvider implements OcrProvider {
|
|
|
49
49
|
filePath?: string;
|
|
50
50
|
}): Promise<OcrResult>;
|
|
51
51
|
getWarnings(): string[];
|
|
52
|
+
private pushWarning;
|
|
53
|
+
private recognizeWithTimeout;
|
|
54
|
+
private resetWorker;
|
|
52
55
|
private readImageDimensions;
|
|
53
56
|
private isTooSmallForOcr;
|
|
54
57
|
private getWorker;
|
|
@@ -11,17 +11,12 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
import { resolveAndImport, resolvePackage } from './module-resolver.js';
|
|
12
12
|
// ─── 路径工具 ───────────────────────────────────────────────────
|
|
13
13
|
const knowledgeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const OCR_NOISE_SUPPRESSION_KEY = Symbol.for('customize-agent.ocr-noise-suppression');
|
|
14
15
|
const OCR_NATIVE_NOISE_PATTERNS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/^Image too small to scale!!(?:\s*\([^)]*\))?$/u,
|
|
17
|
+
/^Line cannot be recognized!!$/u,
|
|
18
|
+
/^empty image$/iu,
|
|
18
19
|
];
|
|
19
|
-
const OCR_NOISE_SUPPRESSION_KEY = Symbol.for('customize-agent.ocr-noise-suppression');
|
|
20
|
-
function ocrNoiseSuppressionState() {
|
|
21
|
-
const globalState = globalThis;
|
|
22
|
-
globalState[OCR_NOISE_SUPPRESSION_KEY] ??= { depth: 0 };
|
|
23
|
-
return globalState[OCR_NOISE_SUPPRESSION_KEY];
|
|
24
|
-
}
|
|
25
20
|
function tessdataDir() {
|
|
26
21
|
if (process.env.TESSDATA_PREFIX)
|
|
27
22
|
return process.env.TESSDATA_PREFIX;
|
|
@@ -30,78 +25,61 @@ function tessdataDir() {
|
|
|
30
25
|
return pkg;
|
|
31
26
|
return pkg;
|
|
32
27
|
}
|
|
33
|
-
function
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
function ocrNoiseSuppressionState() {
|
|
29
|
+
const globalState = globalThis;
|
|
30
|
+
globalState[OCR_NOISE_SUPPRESSION_KEY] ??= {};
|
|
31
|
+
return globalState[OCR_NOISE_SUPPRESSION_KEY];
|
|
32
|
+
}
|
|
33
|
+
function textFromChunk(chunk) {
|
|
34
|
+
return Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk instanceof Uint8Array ? Buffer.from(chunk).toString('utf8') : typeof chunk === 'string' ? chunk : '';
|
|
35
|
+
}
|
|
36
|
+
function isOcrNoiseLine(line) {
|
|
37
|
+
const text = line.trim();
|
|
38
|
+
return !!text && OCR_NATIVE_NOISE_PATTERNS.some(pattern => pattern.test(text));
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
function filterNativeOcrNoiseText(text) {
|
|
41
|
+
const hasTrailingNewline = /\r?\n$/u.test(text);
|
|
42
|
+
const lines = text.split(/\r?\n/u);
|
|
43
|
+
const nonEmptyLines = lines.filter(line => line.trim());
|
|
44
|
+
if (nonEmptyLines.length > 0 && nonEmptyLines.every(isOcrNoiseLine))
|
|
45
|
+
return '';
|
|
46
|
+
const kept = lines.filter(line => !isOcrNoiseLine(line));
|
|
47
|
+
return kept.join('\n') + (hasTrailingNewline && kept.length > 0 ? '\n' : '');
|
|
48
|
+
}
|
|
49
|
+
function ensureOcrNoiseSuppressed() {
|
|
40
50
|
const state = ocrNoiseSuppressionState();
|
|
41
|
-
if (state.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
if (state.installed)
|
|
52
|
+
return;
|
|
53
|
+
state.installed = true;
|
|
54
|
+
state.stdout = process.stdout.write;
|
|
55
|
+
state.stderr = process.stderr.write;
|
|
56
|
+
state.log = console.log;
|
|
57
|
+
state.warn = console.warn;
|
|
58
|
+
state.error = console.error;
|
|
59
|
+
const filterWrite = (original) => function write(chunk, ...args) {
|
|
60
|
+
const text = textFromChunk(chunk);
|
|
61
|
+
if (!text)
|
|
51
62
|
return original.call(this, chunk, ...args);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return await operation();
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
state.depth -= 1;
|
|
62
|
-
if (state.depth === 0 && state.stdout && state.stderr) {
|
|
63
|
-
process.stdout.write = state.stdout;
|
|
64
|
-
process.stderr.write = state.stderr;
|
|
65
|
-
state.stdout = undefined;
|
|
66
|
-
state.stderr = undefined;
|
|
63
|
+
const filtered = filterNativeOcrNoiseText(text);
|
|
64
|
+
if (!filtered) {
|
|
65
|
+
const callback = args.find((arg) => typeof arg === 'function');
|
|
66
|
+
if (callback)
|
|
67
|
+
process.nextTick(callback);
|
|
68
|
+
return true;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const originalWarn = console.warn;
|
|
74
|
-
const originalError = console.error;
|
|
75
|
-
const filter = (orig) => (...args) => {
|
|
76
|
-
const msg = String(args[0] || '');
|
|
77
|
-
if (msg.includes('Line cannot be recognized') || msg.includes('Image too small to scale')) {
|
|
70
|
+
const nextChunk = typeof chunk === 'string' ? filtered : Buffer.from(filtered, 'utf8');
|
|
71
|
+
return original.call(this, nextChunk, ...args);
|
|
72
|
+
};
|
|
73
|
+
const filterConsole = (original) => (...args) => {
|
|
74
|
+
if (args.length === 1 && typeof args[0] === 'string' && isOcrNoiseLine(args[0]))
|
|
78
75
|
return;
|
|
79
|
-
|
|
80
|
-
orig(...args);
|
|
76
|
+
original(...args);
|
|
81
77
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (result instanceof Promise) {
|
|
88
|
-
return result.finally(() => {
|
|
89
|
-
console.log = originalLog;
|
|
90
|
-
console.warn = originalWarn;
|
|
91
|
-
console.error = originalError;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
console.log = originalLog;
|
|
95
|
-
console.warn = originalWarn;
|
|
96
|
-
console.error = originalError;
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
console.log = originalLog;
|
|
101
|
-
console.warn = originalWarn;
|
|
102
|
-
console.error = originalError;
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
78
|
+
process.stdout.write = filterWrite(state.stdout);
|
|
79
|
+
process.stderr.write = filterWrite(state.stderr);
|
|
80
|
+
console.log = filterConsole(state.log);
|
|
81
|
+
console.warn = filterConsole(state.warn);
|
|
82
|
+
console.error = filterConsole(state.error);
|
|
105
83
|
}
|
|
106
84
|
export class TesseractJsProvider {
|
|
107
85
|
id = 'tesseract.js';
|
|
@@ -154,7 +132,7 @@ export class TesseractJsProvider {
|
|
|
154
132
|
return { text: '', confidence: 0, regions: [], warnings: [`image too small for OCR: ${width}x${height}`] };
|
|
155
133
|
}
|
|
156
134
|
try {
|
|
157
|
-
|
|
135
|
+
ensureOcrNoiseSuppressed();
|
|
158
136
|
let unlock;
|
|
159
137
|
const nextLock = new Promise(resolve => { unlock = resolve; });
|
|
160
138
|
const currentLock = this.workerLock;
|
|
@@ -162,7 +140,8 @@ export class TesseractJsProvider {
|
|
|
162
140
|
await currentLock;
|
|
163
141
|
let result;
|
|
164
142
|
try {
|
|
165
|
-
|
|
143
|
+
const worker = await this.getWorker();
|
|
144
|
+
result = await this.recognizeWithTimeout(worker, pngPath);
|
|
166
145
|
}
|
|
167
146
|
finally {
|
|
168
147
|
unlock();
|
|
@@ -191,6 +170,41 @@ export class TesseractJsProvider {
|
|
|
191
170
|
getWarnings() {
|
|
192
171
|
return [...new Set(this.warnings)].slice(-20);
|
|
193
172
|
}
|
|
173
|
+
pushWarning(message) {
|
|
174
|
+
const text = message.trim();
|
|
175
|
+
if (!text || isOcrNoiseLine(text))
|
|
176
|
+
return;
|
|
177
|
+
this.warnings.push(text);
|
|
178
|
+
if (this.warnings.length > 50)
|
|
179
|
+
this.warnings = this.warnings.slice(-50);
|
|
180
|
+
}
|
|
181
|
+
async recognizeWithTimeout(worker, imagePath) {
|
|
182
|
+
const timeoutMs = 120_000;
|
|
183
|
+
let timeout;
|
|
184
|
+
try {
|
|
185
|
+
return await Promise.race([
|
|
186
|
+
worker.recognize(imagePath),
|
|
187
|
+
new Promise((_, reject) => {
|
|
188
|
+
timeout = setTimeout(() => reject(new Error(`OCR recognition timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
await this.resetWorker().catch(() => undefined);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
if (timeout)
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async resetWorker() {
|
|
202
|
+
const worker = this.worker;
|
|
203
|
+
this.worker = null;
|
|
204
|
+
this.workerPromise = null;
|
|
205
|
+
if (worker)
|
|
206
|
+
await worker.terminate();
|
|
207
|
+
}
|
|
194
208
|
async readImageDimensions(filePath) {
|
|
195
209
|
try {
|
|
196
210
|
const sharpMod = await resolveAndImport('sharp');
|
|
@@ -210,8 +224,12 @@ export class TesseractJsProvider {
|
|
|
210
224
|
async getWorker() {
|
|
211
225
|
if (this.worker)
|
|
212
226
|
return this.worker;
|
|
213
|
-
if (!this.workerPromise)
|
|
214
|
-
this.workerPromise = this.createReusableWorker()
|
|
227
|
+
if (!this.workerPromise) {
|
|
228
|
+
this.workerPromise = this.createReusableWorker().catch(error => {
|
|
229
|
+
this.workerPromise = null;
|
|
230
|
+
throw error;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
215
233
|
this.worker = await this.workerPromise;
|
|
216
234
|
return this.worker;
|
|
217
235
|
}
|
|
@@ -225,9 +243,16 @@ export class TesseractJsProvider {
|
|
|
225
243
|
gzip: false,
|
|
226
244
|
logger: () => undefined,
|
|
227
245
|
errorHandler: (error) => {
|
|
228
|
-
|
|
229
|
-
if (message
|
|
230
|
-
|
|
246
|
+
let message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
|
|
247
|
+
if (!message) {
|
|
248
|
+
try {
|
|
249
|
+
message = JSON.stringify(error);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
message = String(error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
this.pushWarning(message);
|
|
231
256
|
},
|
|
232
257
|
});
|
|
233
258
|
if (typeof worker.setParameters === 'function') {
|
|
@@ -236,10 +261,17 @@ export class TesseractJsProvider {
|
|
|
236
261
|
return worker;
|
|
237
262
|
}
|
|
238
263
|
async dispose() {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
264
|
+
let unlock;
|
|
265
|
+
const nextLock = new Promise(resolve => { unlock = resolve; });
|
|
266
|
+
const currentLock = this.workerLock;
|
|
267
|
+
this.workerLock = currentLock.then(() => nextLock).catch(() => nextLock);
|
|
268
|
+
await currentLock;
|
|
269
|
+
try {
|
|
270
|
+
await this.resetWorker();
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
unlock();
|
|
274
|
+
}
|
|
243
275
|
}
|
|
244
276
|
}
|
|
245
277
|
// ─── 工厂 ───────────────────────────────────────────────────────
|