@qvac/ocr-ggml 0.0.1 → 0.1.1
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 +179 -0
- package/NOTICE +63 -0
- package/README.md +330 -0
- package/addonLogging.d.ts +7 -0
- package/addonLogging.js +4 -0
- package/binding.js +21 -0
- package/index.d.ts +145 -0
- package/index.js +383 -0
- package/lib/error.js +80 -0
- package/ocr-ggml.js +121 -0
- package/package.json +96 -7
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.0_1.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.2_1.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.2_2.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.6_1.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv9.0_1.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv9.2_1.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv9.2_2.so +0 -0
- package/prebuilds/android-arm64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__ocr-ggml.bare.exports +4277 -0
- package/prebuilds/darwin-x64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/darwin-x64/qvac__ocr-ggml.bare.exports +4322 -0
- package/prebuilds/ios-arm64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/ios-arm64/qvac__ocr-ggml.bare.exports +4262 -0
- package/prebuilds/ios-arm64-simulator/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/ios-arm64-simulator/qvac__ocr-ggml.bare.exports +4262 -0
- package/prebuilds/ios-x64-simulator/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/ios-x64-simulator/qvac__ocr-ggml.bare.exports +4311 -0
- package/prebuilds/linux-arm64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/linux-x64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/win32-x64/qvac__ocr-ggml.bare +0 -0
- package/prebuilds/win32-x64/qvac__ocr-ggml.bare.exports +0 -0
- package/test/integration/canvas-size.test.js +75 -0
- package/test/integration/doctr-basic.test.js +124 -0
- package/test/integration/doctr-clinical-chemistry.test.js +64 -0
- package/test/integration/doctr-ct-scan.test.js +66 -0
- package/test/integration/doctr-lab-results.test.js +65 -0
- package/test/integration/doctr-liver-function.test.js +67 -0
- package/test/integration/doctr-models.test.js +132 -0
- package/test/integration/doctr-param-validation.test.js +83 -0
- package/test/integration/error-handling.test.js +302 -0
- package/test/integration/full-coverage.test.js +292 -0
- package/test/integration/full-ocr-suite.test.js +83 -0
- package/test/integration/image-formats.test.js +168 -0
- package/test/integration/large-images.test.js +81 -0
- package/test/integration/lifecycle.test.js +269 -0
- package/test/integration/ocr-basic.test.js +70 -0
- package/test/integration/param-validation.test.js +86 -0
- package/test/integration/pipeline.test.js +61 -0
- package/test/integration/run-internal-ordering.test.js +75 -0
- package/test/integration/run-tests.sh +43 -0
- package/test/integration/run-with-exit.js +71 -0
- package/test/integration/utils.js +694 -0
- package/test/mobile/integration-runtime.cjs +71 -0
- package/test/mobile/integration.auto.cjs +102 -0
- package/test/mobile/test-groups.json +55 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { OcrGgml } = require('../..')
|
|
4
|
+
const test = require('brittle')
|
|
5
|
+
const { isMobile, getImagePath, ensureModelPath } = require('./utils')
|
|
6
|
+
|
|
7
|
+
test('Full OCR test suite', { timeout: 40 * 60 * 1000, skip: isMobile }, async function (t) {
|
|
8
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
9
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
10
|
+
|
|
11
|
+
const testCases = [
|
|
12
|
+
{
|
|
13
|
+
imagePath: '/test/images/basic_test.bmp',
|
|
14
|
+
expectedTexts: ['tilted', 'normal', 'vertical'],
|
|
15
|
+
options: { paragraph: false }
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
imagePath: '/test/images/basic_test.jpg',
|
|
19
|
+
expectedTexts: ['tilted', 'normal', 'vertical'],
|
|
20
|
+
options: { paragraph: false }
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
imagePath: '/test/images/basic_test.png',
|
|
24
|
+
expectedTexts: ['tilted', 'normal', 'vertical'],
|
|
25
|
+
options: { paragraph: false }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
imagePath: '/test/images/english.bmp',
|
|
29
|
+
expectedTexts: ['health', 'world', 'water', 'hands', 'symptoms'],
|
|
30
|
+
options: { paragraph: false }
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const ocrGgml = new OcrGgml({
|
|
35
|
+
params: {
|
|
36
|
+
pathDetector: detectorPath,
|
|
37
|
+
pathRecognizer: recognizerPath,
|
|
38
|
+
langList: ['en']
|
|
39
|
+
},
|
|
40
|
+
opts: { stats: true }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await ocrGgml.load()
|
|
44
|
+
t.pass('OCR model loaded')
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
for (const testCase of testCases) {
|
|
48
|
+
const imagePath = getImagePath(testCase.imagePath)
|
|
49
|
+
t.comment('\n\nImage Path: ' + testCase.imagePath)
|
|
50
|
+
|
|
51
|
+
const response = await ocrGgml.run({ path: imagePath, options: testCase.options })
|
|
52
|
+
|
|
53
|
+
await response
|
|
54
|
+
.onUpdate(output => {
|
|
55
|
+
t.ok(Array.isArray(output), testCase.imagePath + ': output should be an array')
|
|
56
|
+
const texts = output.map(o => o[1])
|
|
57
|
+
t.comment('Detected texts: ' + JSON.stringify(texts))
|
|
58
|
+
|
|
59
|
+
for (const expected of testCase.expectedTexts) {
|
|
60
|
+
const found = texts.some(w => w.toLowerCase().includes(expected.toLowerCase()))
|
|
61
|
+
t.ok(found, testCase.imagePath + `: should detect "${expected}"`)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.onError(error => {
|
|
65
|
+
t.fail(testCase.imagePath + ': unexpected error: ' + JSON.stringify(error))
|
|
66
|
+
})
|
|
67
|
+
.await()
|
|
68
|
+
|
|
69
|
+
t.comment('OCR processing complete for ' + testCase.imagePath)
|
|
70
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
t.fail('Error running test suite: ' + err)
|
|
74
|
+
} finally {
|
|
75
|
+
try {
|
|
76
|
+
await ocrGgml.unload()
|
|
77
|
+
t.comment('Successfully unloaded model')
|
|
78
|
+
} catch (err) {
|
|
79
|
+
t.comment('unload() failed: ' + err.message)
|
|
80
|
+
}
|
|
81
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
82
|
+
}
|
|
83
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { OcrGgml } = require('../..')
|
|
4
|
+
const test = require('brittle')
|
|
5
|
+
const { isMobile, getImagePath, ensureModelPath } = require('./utils')
|
|
6
|
+
|
|
7
|
+
const MOBILE_TIMEOUT = 600 * 1000 // 10 minutes for mobile
|
|
8
|
+
const DESKTOP_TIMEOUT = 120 * 1000 // 2 minutes for desktop
|
|
9
|
+
const TEST_TIMEOUT = isMobile ? MOBILE_TIMEOUT : DESKTOP_TIMEOUT
|
|
10
|
+
|
|
11
|
+
const IMAGE_FORMAT_EXPECTED_TEXTS = ['tilted', 'normal', 'vertical']
|
|
12
|
+
|
|
13
|
+
test('OCR processes JPEG images correctly', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
14
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
15
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
16
|
+
const imagePath = getImagePath('/test/images/basic_test.jpg')
|
|
17
|
+
|
|
18
|
+
t.comment('Testing JPEG format with image: ' + imagePath)
|
|
19
|
+
|
|
20
|
+
const ocrGgml = new OcrGgml({
|
|
21
|
+
params: {
|
|
22
|
+
pathDetector: detectorPath,
|
|
23
|
+
pathRecognizer: recognizerPath,
|
|
24
|
+
langList: ['en']
|
|
25
|
+
},
|
|
26
|
+
opts: { stats: true }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
await ocrGgml.load()
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await ocrGgml.run({
|
|
33
|
+
path: imagePath,
|
|
34
|
+
options: { paragraph: false }
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await response
|
|
38
|
+
.onUpdate(output => {
|
|
39
|
+
t.ok(Array.isArray(output), 'JPEG: output should be an array')
|
|
40
|
+
t.ok(output.length === IMAGE_FORMAT_EXPECTED_TEXTS.length, `JPEG: output length should be ${IMAGE_FORMAT_EXPECTED_TEXTS.length}, got ${output.length}`)
|
|
41
|
+
|
|
42
|
+
const texts = output.map(o => o[1])
|
|
43
|
+
t.comment('JPEG output texts: ' + JSON.stringify(texts))
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < IMAGE_FORMAT_EXPECTED_TEXTS.length; i++) {
|
|
46
|
+
t.ok(texts.includes(IMAGE_FORMAT_EXPECTED_TEXTS[i]), `JPEG: should contain text "${IMAGE_FORMAT_EXPECTED_TEXTS[i]}"`)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.onError(error => {
|
|
50
|
+
t.fail('JPEG: unexpected error: ' + JSON.stringify(error))
|
|
51
|
+
})
|
|
52
|
+
.await()
|
|
53
|
+
|
|
54
|
+
t.pass('JPEG format processing completed successfully')
|
|
55
|
+
} finally {
|
|
56
|
+
await ocrGgml.unload()
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('OCR processes PNG images correctly', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
62
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
63
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
64
|
+
const imagePath = getImagePath('/test/images/basic_test.png')
|
|
65
|
+
|
|
66
|
+
t.comment('Testing PNG format with image: ' + imagePath)
|
|
67
|
+
|
|
68
|
+
const ocrGgml = new OcrGgml({
|
|
69
|
+
params: {
|
|
70
|
+
pathDetector: detectorPath,
|
|
71
|
+
pathRecognizer: recognizerPath,
|
|
72
|
+
langList: ['en']
|
|
73
|
+
},
|
|
74
|
+
opts: { stats: true }
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await ocrGgml.load()
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await ocrGgml.run({
|
|
81
|
+
path: imagePath,
|
|
82
|
+
options: { paragraph: false }
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await response
|
|
86
|
+
.onUpdate(output => {
|
|
87
|
+
t.ok(Array.isArray(output), 'PNG: output should be an array')
|
|
88
|
+
t.ok(output.length === IMAGE_FORMAT_EXPECTED_TEXTS.length, `PNG: output length should be ${IMAGE_FORMAT_EXPECTED_TEXTS.length}, got ${output.length}`)
|
|
89
|
+
|
|
90
|
+
const texts = output.map(o => o[1])
|
|
91
|
+
t.comment('PNG output texts: ' + JSON.stringify(texts))
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < IMAGE_FORMAT_EXPECTED_TEXTS.length; i++) {
|
|
94
|
+
t.ok(texts.includes(IMAGE_FORMAT_EXPECTED_TEXTS[i]), `PNG: should contain text "${IMAGE_FORMAT_EXPECTED_TEXTS[i]}"`)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.onError(error => {
|
|
98
|
+
t.fail('PNG: unexpected error: ' + JSON.stringify(error))
|
|
99
|
+
})
|
|
100
|
+
.await()
|
|
101
|
+
|
|
102
|
+
t.pass('PNG format processing completed successfully')
|
|
103
|
+
} finally {
|
|
104
|
+
await ocrGgml.unload()
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('BMP and JPEG produce consistent results', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
110
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
111
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
112
|
+
const bmpPath = getImagePath('/test/images/basic_test.bmp')
|
|
113
|
+
const jpgPath = getImagePath('/test/images/basic_test.jpg')
|
|
114
|
+
|
|
115
|
+
const ocrGgml = new OcrGgml({
|
|
116
|
+
params: {
|
|
117
|
+
pathDetector: detectorPath,
|
|
118
|
+
pathRecognizer: recognizerPath,
|
|
119
|
+
langList: ['en']
|
|
120
|
+
},
|
|
121
|
+
opts: { stats: true }
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await ocrGgml.load()
|
|
125
|
+
|
|
126
|
+
let bmpTexts = []
|
|
127
|
+
let jpegTexts = []
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const bmpResponse = await ocrGgml.run({
|
|
131
|
+
path: bmpPath,
|
|
132
|
+
options: { paragraph: false }
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
await bmpResponse
|
|
136
|
+
.onUpdate(output => {
|
|
137
|
+
bmpTexts = output.map(o => o[1]).sort()
|
|
138
|
+
})
|
|
139
|
+
.await()
|
|
140
|
+
|
|
141
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
142
|
+
|
|
143
|
+
const jpegResponse = await ocrGgml.run({
|
|
144
|
+
path: jpgPath,
|
|
145
|
+
options: { paragraph: false }
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
await jpegResponse
|
|
149
|
+
.onUpdate(output => {
|
|
150
|
+
jpegTexts = output.map(o => o[1]).sort()
|
|
151
|
+
})
|
|
152
|
+
.await()
|
|
153
|
+
|
|
154
|
+
t.comment('BMP texts: ' + JSON.stringify(bmpTexts))
|
|
155
|
+
t.comment('JPEG texts: ' + JSON.stringify(jpegTexts))
|
|
156
|
+
|
|
157
|
+
t.ok(bmpTexts.length === jpegTexts.length, 'BMP and JPEG should detect same number of text regions')
|
|
158
|
+
|
|
159
|
+
for (const text of bmpTexts) {
|
|
160
|
+
t.ok(jpegTexts.includes(text), `JPEG should also detect text "${text}" found in BMP`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
t.pass('BMP and JPEG produce consistent results')
|
|
164
|
+
} finally {
|
|
165
|
+
await ocrGgml.unload()
|
|
166
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
167
|
+
}
|
|
168
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { OcrGgml } = require('../..')
|
|
4
|
+
const test = require('brittle')
|
|
5
|
+
const { isMobile, getImagePath, ensureModelPath } = require('./utils')
|
|
6
|
+
|
|
7
|
+
const MOBILE_TIMEOUT = 600 * 1000 // 10 minutes for mobile
|
|
8
|
+
const DESKTOP_TIMEOUT = 120 * 1000 // 2 minutes for desktop
|
|
9
|
+
const TEST_TIMEOUT = isMobile ? MOBILE_TIMEOUT : DESKTOP_TIMEOUT
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test for internal image resizing.
|
|
13
|
+
* Images larger than 1200px are resized internally, but bounding box
|
|
14
|
+
* coordinates should be returned in original image space.
|
|
15
|
+
*/
|
|
16
|
+
test('Large images are resized internally with coordinates in original space', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
17
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
18
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
19
|
+
|
|
20
|
+
// unrecognizable_text.bmp is 2467x128 - long axis crosses the 1200px threshold
|
|
21
|
+
const imagePath = getImagePath('/test/images/unrecognizable_text.bmp')
|
|
22
|
+
const originalImageWidth = 2467
|
|
23
|
+
const originalImageHeight = 128
|
|
24
|
+
|
|
25
|
+
t.comment('Testing internal resize with image: ' + imagePath + ' (' + originalImageWidth + 'x' + originalImageHeight + ')')
|
|
26
|
+
|
|
27
|
+
const ocrGgml = new OcrGgml({
|
|
28
|
+
params: {
|
|
29
|
+
pathDetector: detectorPath,
|
|
30
|
+
pathRecognizer: recognizerPath,
|
|
31
|
+
langList: ['en']
|
|
32
|
+
},
|
|
33
|
+
opts: { stats: true }
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await ocrGgml.load()
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await ocrGgml.run({
|
|
40
|
+
path: imagePath,
|
|
41
|
+
options: { paragraph: false }
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
await response
|
|
45
|
+
.onUpdate(output => {
|
|
46
|
+
t.ok(Array.isArray(output), 'output should be an array')
|
|
47
|
+
t.ok(output.length > 0, 'should detect at least one text region')
|
|
48
|
+
t.comment('Detected ' + output.length + ' text regions')
|
|
49
|
+
|
|
50
|
+
// Check that coordinates are in original image space (can exceed 1200)
|
|
51
|
+
let hasCoordBeyondResizeThreshold = false
|
|
52
|
+
let maxX = 0
|
|
53
|
+
let maxY = 0
|
|
54
|
+
|
|
55
|
+
for (const item of output) {
|
|
56
|
+
const coords = item[0] // [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
|
|
57
|
+
for (const point of coords) {
|
|
58
|
+
if (point[0] > maxX) maxX = point[0]
|
|
59
|
+
if (point[1] > maxY) maxY = point[1]
|
|
60
|
+
if (point[0] > 1200 || point[1] > 1200) {
|
|
61
|
+
hasCoordBeyondResizeThreshold = true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
t.comment('Max X coordinate: ' + maxX.toFixed(1) + ', Max Y coordinate: ' + maxY.toFixed(1))
|
|
67
|
+
t.ok(hasCoordBeyondResizeThreshold, 'coordinates should be in original image space (some coords > 1200)')
|
|
68
|
+
t.ok(maxX <= originalImageWidth, 'max X should not exceed original image width')
|
|
69
|
+
t.ok(maxY <= originalImageHeight, 'max Y should not exceed original image height')
|
|
70
|
+
})
|
|
71
|
+
.onError(error => {
|
|
72
|
+
t.fail('unexpected error: ' + JSON.stringify(error))
|
|
73
|
+
})
|
|
74
|
+
.await()
|
|
75
|
+
|
|
76
|
+
t.pass('Large image resize test completed successfully')
|
|
77
|
+
} finally {
|
|
78
|
+
await ocrGgml.unload()
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
80
|
+
}
|
|
81
|
+
})
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { OcrGgml } = require('../..')
|
|
4
|
+
const test = require('brittle')
|
|
5
|
+
const { isMobile, getImagePath, ensureModelPath } = require('./utils')
|
|
6
|
+
|
|
7
|
+
const MOBILE_TIMEOUT = 600 * 1000
|
|
8
|
+
const DESKTOP_TIMEOUT = 120 * 1000
|
|
9
|
+
const TEST_TIMEOUT = isMobile ? MOBILE_TIMEOUT : DESKTOP_TIMEOUT
|
|
10
|
+
|
|
11
|
+
async function createAndLoadOcr (t) {
|
|
12
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
13
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
14
|
+
const imagePath = getImagePath('/test/images/basic_test.bmp')
|
|
15
|
+
|
|
16
|
+
const ocrGgml = new OcrGgml({
|
|
17
|
+
params: {
|
|
18
|
+
pathDetector: detectorPath,
|
|
19
|
+
pathRecognizer: recognizerPath,
|
|
20
|
+
langList: ['en']
|
|
21
|
+
},
|
|
22
|
+
opts: { stats: true }
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return { ocrGgml, imagePath }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('Load, run, unload - basic lifecycle', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
29
|
+
const { ocrGgml, imagePath } = await createAndLoadOcr(t)
|
|
30
|
+
|
|
31
|
+
await ocrGgml.load()
|
|
32
|
+
t.pass('Model loaded successfully')
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await ocrGgml.run({
|
|
36
|
+
path: imagePath,
|
|
37
|
+
options: { paragraph: false }
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await response
|
|
41
|
+
.onUpdate(output => {
|
|
42
|
+
t.ok(Array.isArray(output), 'Output should be an array')
|
|
43
|
+
t.ok(output.length > 0, 'Should detect at least one text region')
|
|
44
|
+
})
|
|
45
|
+
.onError(error => {
|
|
46
|
+
t.fail('Unexpected error: ' + JSON.stringify(error))
|
|
47
|
+
})
|
|
48
|
+
.await()
|
|
49
|
+
|
|
50
|
+
t.pass('Run completed successfully')
|
|
51
|
+
} finally {
|
|
52
|
+
await ocrGgml.unload()
|
|
53
|
+
t.pass('Unload completed successfully')
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('Load, unload, reload - model can be reloaded after unload', { timeout: TEST_TIMEOUT * 2 }, async function (t) {
|
|
59
|
+
const { ocrGgml, imagePath } = await createAndLoadOcr(t)
|
|
60
|
+
|
|
61
|
+
// First load
|
|
62
|
+
await ocrGgml.load()
|
|
63
|
+
t.pass('First load successful')
|
|
64
|
+
|
|
65
|
+
const response1 = await ocrGgml.run({
|
|
66
|
+
path: imagePath,
|
|
67
|
+
options: { paragraph: false }
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
let firstRunTexts = []
|
|
71
|
+
await response1
|
|
72
|
+
.onUpdate(output => {
|
|
73
|
+
firstRunTexts = output.map(o => o[1])
|
|
74
|
+
})
|
|
75
|
+
.await()
|
|
76
|
+
|
|
77
|
+
t.ok(firstRunTexts.length > 0, 'First run should produce output')
|
|
78
|
+
t.comment('First run texts: ' + JSON.stringify(firstRunTexts))
|
|
79
|
+
|
|
80
|
+
// Unload
|
|
81
|
+
await ocrGgml.unload()
|
|
82
|
+
t.pass('Unload successful')
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
84
|
+
|
|
85
|
+
// Reload
|
|
86
|
+
await ocrGgml.load()
|
|
87
|
+
t.pass('Reload successful')
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response2 = await ocrGgml.run({
|
|
91
|
+
path: imagePath,
|
|
92
|
+
options: { paragraph: false }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
let secondRunTexts = []
|
|
96
|
+
await response2
|
|
97
|
+
.onUpdate(output => {
|
|
98
|
+
secondRunTexts = output.map(o => o[1])
|
|
99
|
+
})
|
|
100
|
+
.await()
|
|
101
|
+
|
|
102
|
+
t.ok(secondRunTexts.length > 0, 'Second run after reload should produce output')
|
|
103
|
+
t.comment('Second run texts: ' + JSON.stringify(secondRunTexts))
|
|
104
|
+
|
|
105
|
+
t.is(firstRunTexts.length, secondRunTexts.length, 'Both runs should detect same number of regions')
|
|
106
|
+
for (const text of firstRunTexts) {
|
|
107
|
+
t.ok(secondRunTexts.includes(text), `Reloaded model should detect "${text}"`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
t.pass('Model reload produced consistent results')
|
|
111
|
+
} finally {
|
|
112
|
+
await ocrGgml.unload()
|
|
113
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('Double unload does not crash', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
118
|
+
const { ocrGgml } = await createAndLoadOcr(t)
|
|
119
|
+
|
|
120
|
+
await ocrGgml.load()
|
|
121
|
+
t.pass('Model loaded')
|
|
122
|
+
|
|
123
|
+
await ocrGgml.unload()
|
|
124
|
+
t.pass('First unload successful')
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await ocrGgml.unload()
|
|
128
|
+
t.pass('Second unload did not throw')
|
|
129
|
+
} catch (err) {
|
|
130
|
+
t.comment('Second unload threw: ' + err.message)
|
|
131
|
+
t.pass('Second unload threw an error (acceptable behavior)')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('Run before load throws error', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
138
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
139
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
140
|
+
const imagePath = getImagePath('/test/images/basic_test.bmp')
|
|
141
|
+
|
|
142
|
+
const ocrGgml = new OcrGgml({
|
|
143
|
+
params: {
|
|
144
|
+
pathDetector: detectorPath,
|
|
145
|
+
pathRecognizer: recognizerPath,
|
|
146
|
+
langList: ['en']
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await ocrGgml.run({
|
|
152
|
+
path: imagePath,
|
|
153
|
+
options: { paragraph: false }
|
|
154
|
+
})
|
|
155
|
+
t.fail('Should have thrown when running before load')
|
|
156
|
+
} catch (err) {
|
|
157
|
+
t.ok(err, 'Should throw an error when running before load')
|
|
158
|
+
t.comment('Error: ' + err.message)
|
|
159
|
+
t.pass('Correctly prevented run before load')
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('Run after unload throws error', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
164
|
+
const { ocrGgml, imagePath } = await createAndLoadOcr(t)
|
|
165
|
+
|
|
166
|
+
await ocrGgml.load()
|
|
167
|
+
await ocrGgml.unload()
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await ocrGgml.run({
|
|
172
|
+
path: imagePath,
|
|
173
|
+
options: { paragraph: false }
|
|
174
|
+
})
|
|
175
|
+
t.fail('Should have thrown when running after unload')
|
|
176
|
+
} catch (err) {
|
|
177
|
+
t.ok(err, 'Should throw an error when running after unload')
|
|
178
|
+
t.comment('Error: ' + err.message)
|
|
179
|
+
t.pass('Correctly prevented run after unload')
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('Cancellation during inference does not crash', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
184
|
+
const { ocrGgml, imagePath } = await createAndLoadOcr(t)
|
|
185
|
+
|
|
186
|
+
await ocrGgml.load()
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const response = await ocrGgml.run({
|
|
190
|
+
path: imagePath,
|
|
191
|
+
options: { paragraph: false }
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// Cancel immediately after starting
|
|
195
|
+
if (ocrGgml.addon && ocrGgml.addon.cancel) {
|
|
196
|
+
await ocrGgml.addon.cancel()
|
|
197
|
+
t.pass('Cancel called without crashing')
|
|
198
|
+
} else {
|
|
199
|
+
t.comment('addon.cancel not available, skipping cancel test')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// After cancel, the response may never settle (no JobEnded event).
|
|
203
|
+
// Race against a short timeout so we don't hang the whole suite.
|
|
204
|
+
const CANCEL_WAIT_MS = 5000
|
|
205
|
+
try {
|
|
206
|
+
await Promise.race([
|
|
207
|
+
response.await(),
|
|
208
|
+
new Promise(function (resolve, reject) {
|
|
209
|
+
setTimeout(function () { reject(new Error('cancel: response did not settle')) }, CANCEL_WAIT_MS)
|
|
210
|
+
})
|
|
211
|
+
])
|
|
212
|
+
t.comment('Response completed despite cancel (inference may have finished first)')
|
|
213
|
+
} catch (err) {
|
|
214
|
+
t.comment('Response after cancel: ' + err.message)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
t.pass('Cancellation handled gracefully')
|
|
218
|
+
} finally {
|
|
219
|
+
try {
|
|
220
|
+
await ocrGgml.unload()
|
|
221
|
+
} catch (err) {
|
|
222
|
+
t.comment('Unload after cancel: ' + err.message)
|
|
223
|
+
}
|
|
224
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('Performance parameters are accepted without error', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
229
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
230
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
231
|
+
const imagePath = getImagePath('/test/images/basic_test.bmp')
|
|
232
|
+
|
|
233
|
+
const ocrGgml = new OcrGgml({
|
|
234
|
+
params: {
|
|
235
|
+
pathDetector: detectorPath,
|
|
236
|
+
pathRecognizer: recognizerPath,
|
|
237
|
+
langList: ['en'],
|
|
238
|
+
magRatio: 1.5,
|
|
239
|
+
recognizerBatchSize: 4,
|
|
240
|
+
lowConfidenceThreshold: 0.3
|
|
241
|
+
},
|
|
242
|
+
opts: { stats: true }
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
await ocrGgml.load()
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const response = await ocrGgml.run({
|
|
249
|
+
path: imagePath,
|
|
250
|
+
options: { paragraph: false }
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
await response
|
|
254
|
+
.onUpdate(output => {
|
|
255
|
+
t.ok(Array.isArray(output), 'Output with custom params should be an array')
|
|
256
|
+
t.ok(output.length > 0, 'Should still detect text with custom performance params')
|
|
257
|
+
t.comment('Detected ' + output.length + ' regions with custom performance params')
|
|
258
|
+
})
|
|
259
|
+
.onError(error => {
|
|
260
|
+
t.fail('Unexpected error with performance params: ' + JSON.stringify(error))
|
|
261
|
+
})
|
|
262
|
+
.await()
|
|
263
|
+
|
|
264
|
+
t.pass('Performance parameters accepted and inference completed')
|
|
265
|
+
} finally {
|
|
266
|
+
await ocrGgml.unload()
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
268
|
+
}
|
|
269
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { OcrGgml } = require('../..')
|
|
4
|
+
const test = require('brittle')
|
|
5
|
+
const { isMobile, platform, getImagePath, ensureModelPath, formatOCRPerformanceMetrics } = require('./utils')
|
|
6
|
+
|
|
7
|
+
const MOBILE_TIMEOUT = 600 * 1000 // 10 minutes for mobile
|
|
8
|
+
const DESKTOP_TIMEOUT = 120 * 1000 // 2 minutes for desktop
|
|
9
|
+
const TEST_TIMEOUT = isMobile ? MOBILE_TIMEOUT : DESKTOP_TIMEOUT
|
|
10
|
+
|
|
11
|
+
test('OCR basic test', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
12
|
+
const detectorPath = await ensureModelPath('detector_craft')
|
|
13
|
+
const recognizerPath = await ensureModelPath('recognizer_latin')
|
|
14
|
+
const imagePath = getImagePath('/test/images/basic_test.bmp')
|
|
15
|
+
|
|
16
|
+
t.comment('Testing basic OCR with image: ' + imagePath)
|
|
17
|
+
t.comment('Platform: ' + platform + ', isMobile: ' + isMobile)
|
|
18
|
+
|
|
19
|
+
const ocrGgml = new OcrGgml({
|
|
20
|
+
params: {
|
|
21
|
+
pathDetector: detectorPath,
|
|
22
|
+
pathRecognizer: recognizerPath,
|
|
23
|
+
langList: ['en']
|
|
24
|
+
},
|
|
25
|
+
opts: { stats: true }
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await ocrGgml.load()
|
|
29
|
+
t.pass('OCR model loaded successfully')
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const response = await ocrGgml.run({
|
|
33
|
+
path: imagePath,
|
|
34
|
+
options: { paragraph: false }
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
let outputTexts = []
|
|
38
|
+
|
|
39
|
+
await response
|
|
40
|
+
.onUpdate(output => {
|
|
41
|
+
t.ok(Array.isArray(output), 'output should be an array')
|
|
42
|
+
t.ok(output.length === 3, `output length should be 3, got ${output.length}`)
|
|
43
|
+
outputTexts = output.map(o => o[1])
|
|
44
|
+
t.ok(outputTexts.includes('tilted'), 'should contain "tilted"')
|
|
45
|
+
t.ok(outputTexts.includes('normal'), 'should contain "normal"')
|
|
46
|
+
t.ok(outputTexts.includes('vertical'), 'should contain "vertical"')
|
|
47
|
+
})
|
|
48
|
+
.onError(error => {
|
|
49
|
+
t.fail('unexpected error: ' + JSON.stringify(error))
|
|
50
|
+
})
|
|
51
|
+
.await()
|
|
52
|
+
|
|
53
|
+
// Display stats
|
|
54
|
+
const stats = response.stats || {}
|
|
55
|
+
t.comment('Native addon stats: ' + JSON.stringify(stats))
|
|
56
|
+
t.comment(formatOCRPerformanceMetrics('[EasyOCR basic_test] [CPU]', stats, outputTexts))
|
|
57
|
+
|
|
58
|
+
t.pass('OCR basic test completed successfully')
|
|
59
|
+
} catch (e) {
|
|
60
|
+
t.fail('OCR test failed: ' + e.message)
|
|
61
|
+
throw e
|
|
62
|
+
} finally {
|
|
63
|
+
try {
|
|
64
|
+
await ocrGgml.unload()
|
|
65
|
+
} catch (e) {
|
|
66
|
+
t.comment('unload() error: ' + e.message)
|
|
67
|
+
}
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
69
|
+
}
|
|
70
|
+
})
|