@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,86 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { OcrGgml } = require('../..')
|
|
4
|
+
const { QvacErrorAddonOcrGgml, ERR_CODES } = require('../..')
|
|
5
|
+
const test = require('brittle')
|
|
6
|
+
const { isMobile } = require('./utils')
|
|
7
|
+
|
|
8
|
+
const MOBILE_TIMEOUT = 600 * 1000
|
|
9
|
+
const DESKTOP_TIMEOUT = 30 * 1000
|
|
10
|
+
const TEST_TIMEOUT = isMobile ? MOBILE_TIMEOUT : DESKTOP_TIMEOUT
|
|
11
|
+
|
|
12
|
+
test('load() rejects when langList is missing', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
13
|
+
const ocrGgml = new OcrGgml({
|
|
14
|
+
params: {
|
|
15
|
+
pathDetector: 'models/craft_mlt_25k.gguf',
|
|
16
|
+
pathRecognizer: 'models/latin_g2.gguf'
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await ocrGgml.load()
|
|
22
|
+
t.fail('Should have thrown for missing langList')
|
|
23
|
+
} catch (err) {
|
|
24
|
+
t.ok(err instanceof QvacErrorAddonOcrGgml, 'Should throw QvacErrorAddonOcrGgml')
|
|
25
|
+
t.is(err.code, ERR_CODES.MISSING_REQUIRED_PARAMETER, 'Error code should be MISSING_REQUIRED_PARAMETER')
|
|
26
|
+
t.ok(err.message.includes('langList'), 'Error message should mention langList')
|
|
27
|
+
t.pass('Correctly rejected missing langList')
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('load() rejects when langList is empty array after filtering', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
32
|
+
const ocrGgml = new OcrGgml({
|
|
33
|
+
params: {
|
|
34
|
+
pathDetector: 'models/craft_mlt_25k.gguf',
|
|
35
|
+
pathRecognizer: 'models/latin_g2.gguf',
|
|
36
|
+
langList: ['klingon', 'elvish', 'dothraki']
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await ocrGgml.load()
|
|
42
|
+
t.fail('Should have thrown for all-unsupported languages')
|
|
43
|
+
} catch (err) {
|
|
44
|
+
t.ok(err instanceof QvacErrorAddonOcrGgml, 'Should throw QvacErrorAddonOcrGgml')
|
|
45
|
+
t.is(err.code, ERR_CODES.UNSUPPORTED_LANGUAGE, 'Error code should be UNSUPPORTED_LANGUAGE')
|
|
46
|
+
t.pass('Correctly rejected all-unsupported language list')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('load() rejects when pathDetector is missing', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
51
|
+
const ocrGgml = new OcrGgml({
|
|
52
|
+
params: {
|
|
53
|
+
pathRecognizer: 'models/latin_g2.gguf',
|
|
54
|
+
langList: ['en']
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await ocrGgml.load()
|
|
60
|
+
t.fail('Should have thrown for missing pathDetector')
|
|
61
|
+
} catch (err) {
|
|
62
|
+
t.ok(err instanceof QvacErrorAddonOcrGgml, 'Should throw QvacErrorAddonOcrGgml')
|
|
63
|
+
t.is(err.code, ERR_CODES.MISSING_REQUIRED_PARAMETER, 'Error code should be MISSING_REQUIRED_PARAMETER')
|
|
64
|
+
t.ok(err.message.includes('pathDetector'), 'Error message should mention pathDetector')
|
|
65
|
+
t.pass('Correctly rejected missing pathDetector')
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('load() rejects when pathRecognizer is missing', { timeout: TEST_TIMEOUT }, async function (t) {
|
|
70
|
+
const ocrGgml = new OcrGgml({
|
|
71
|
+
params: {
|
|
72
|
+
pathDetector: 'models/craft_mlt_25k.gguf',
|
|
73
|
+
langList: ['en']
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await ocrGgml.load()
|
|
79
|
+
t.fail('Should have thrown for missing pathRecognizer')
|
|
80
|
+
} catch (err) {
|
|
81
|
+
t.ok(err instanceof QvacErrorAddonOcrGgml, 'Should throw QvacErrorAddonOcrGgml')
|
|
82
|
+
t.is(err.code, ERR_CODES.MISSING_REQUIRED_PARAMETER, 'Error code should be MISSING_REQUIRED_PARAMETER')
|
|
83
|
+
t.ok(err.message.includes('pathRecognizer'), 'Error message should mention pathRecognizer')
|
|
84
|
+
t.pass('Correctly rejected missing recognizer path')
|
|
85
|
+
}
|
|
86
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
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 = 60 * 1000 // 1 minute for desktop
|
|
9
|
+
const TEST_TIMEOUT = isMobile ? MOBILE_TIMEOUT : DESKTOP_TIMEOUT
|
|
10
|
+
|
|
11
|
+
test('Test for a fix of missing end of job event', { 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/unrecognizable_text.bmp')
|
|
15
|
+
|
|
16
|
+
t.comment('Testing with image: ' + imagePath)
|
|
17
|
+
|
|
18
|
+
const ocrGgml = new OcrGgml({
|
|
19
|
+
params: {
|
|
20
|
+
pathDetector: detectorPath,
|
|
21
|
+
pathRecognizer: recognizerPath,
|
|
22
|
+
langList: ['en']
|
|
23
|
+
},
|
|
24
|
+
opts: { stats: true }
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await ocrGgml.load()
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
let errorReceived = false
|
|
31
|
+
let responseCompleted = false
|
|
32
|
+
|
|
33
|
+
const response = await ocrGgml.run({
|
|
34
|
+
path: imagePath,
|
|
35
|
+
options: { paragraph: false }
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await response
|
|
39
|
+
.onUpdate(output => {
|
|
40
|
+
t.ok(Array.isArray(output), 'output should be an array')
|
|
41
|
+
})
|
|
42
|
+
.onError(error => {
|
|
43
|
+
errorReceived = true
|
|
44
|
+
t.fail('Unexpected error received: ' + JSON.stringify(error))
|
|
45
|
+
})
|
|
46
|
+
.await()
|
|
47
|
+
.then(() => {
|
|
48
|
+
responseCompleted = true
|
|
49
|
+
t.pass('Response completed successfully - JobEnded event was received')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
t.ok(!errorReceived, 'No error should be received')
|
|
53
|
+
t.ok(responseCompleted, 'Response should complete - JobEnded event was received')
|
|
54
|
+
t.pass('Pipeline completed successfully without hanging')
|
|
55
|
+
} catch (err) {
|
|
56
|
+
t.fail(`Error in test: ${err}`)
|
|
57
|
+
} finally {
|
|
58
|
+
await ocrGgml.unload()
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
60
|
+
}
|
|
61
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Regression test for #1756.
|
|
4
|
+
//
|
|
5
|
+
// The native OCR binding fires output/stats/error callbacks synchronously
|
|
6
|
+
// from inside `binding.runJob(...)`. If `_runInternal` awaits `runJob` before
|
|
7
|
+
// calling `_job.start()`, those callbacks land while the job handler has no
|
|
8
|
+
// active QvacResponse and are silently dropped. This test pins the ordering
|
|
9
|
+
// by stubbing `this.addon` with a `runJob` that mirrors the native semantics
|
|
10
|
+
// (callbacks fired synchronously inside the call) and asserts the response
|
|
11
|
+
// receives them.
|
|
12
|
+
|
|
13
|
+
const test = require('brittle')
|
|
14
|
+
const { OcrGgml } = require('../..')
|
|
15
|
+
|
|
16
|
+
function createOcr () {
|
|
17
|
+
const ocr = new OcrGgml({
|
|
18
|
+
params: {
|
|
19
|
+
pathDetector: 'unused',
|
|
20
|
+
pathRecognizer: 'unused',
|
|
21
|
+
langList: ['en']
|
|
22
|
+
},
|
|
23
|
+
opts: { stats: true }
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
ocr._readImage = () => ({ data: Buffer.from('stub'), isEncoded: true })
|
|
27
|
+
|
|
28
|
+
return ocr
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('output callbacks fired synchronously inside runJob are not dropped', async t => {
|
|
32
|
+
const ocr = createOcr()
|
|
33
|
+
const expectedOutput = [[[[0, 0], [10, 0], [10, 10], [0, 10]], 'hello', 0.99]]
|
|
34
|
+
|
|
35
|
+
ocr.addon = {
|
|
36
|
+
runJob: async () => {
|
|
37
|
+
ocr._addonOutputCallback(ocr.addon, 'Output', expectedOutput)
|
|
38
|
+
ocr._addonOutputCallback(ocr.addon, 'JobEnded', { totalTime: 1 })
|
|
39
|
+
},
|
|
40
|
+
cancel: () => {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const response = await ocr.run({ path: 'stub' })
|
|
44
|
+
const result = await response.await()
|
|
45
|
+
|
|
46
|
+
t.alike(result, [expectedOutput], 'output emitted before runJob await must reach the response')
|
|
47
|
+
t.alike(response.stats, { totalTime: 1 }, 'stats emitted before runJob await must reach the response')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('error callbacks fired synchronously inside runJob are not dropped', async t => {
|
|
51
|
+
const ocr = createOcr()
|
|
52
|
+
|
|
53
|
+
ocr.addon = {
|
|
54
|
+
runJob: async () => {
|
|
55
|
+
ocr._addonOutputCallback(ocr.addon, 'Error', null, 'native failure')
|
|
56
|
+
},
|
|
57
|
+
cancel: () => {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const response = await ocr.run({ path: 'stub' })
|
|
61
|
+
await t.exception(response.await(), /native failure/, 'error emitted before runJob await must reach the response')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('runJob rejection is routed through _job.fail and clears the active response', async t => {
|
|
65
|
+
const ocr = createOcr()
|
|
66
|
+
const failure = new Error('runJob rejected')
|
|
67
|
+
|
|
68
|
+
ocr.addon = {
|
|
69
|
+
runJob: async () => { throw failure },
|
|
70
|
+
cancel: () => {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await t.exception(ocr.run({ path: 'stub' }), /runJob rejected/, 'runJob rejection must propagate to the caller')
|
|
74
|
+
t.is(ocr._job.active, null, 'failed run must clear the active response so the next start() does not see a stale job')
|
|
75
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shell wrapper for bare integration tests.
|
|
3
|
+
# Bare.exit() hangs when native addon handles keep the event loop alive,
|
|
4
|
+
# so run-with-exit.js writes an .exit-code file when tests finish.
|
|
5
|
+
# This script monitors for that file and terminates bare cleanly.
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
EXIT_CODE_FILE="$SCRIPT_DIR/.exit-code"
|
|
9
|
+
TIMEOUT=${INTEGRATION_TEST_TIMEOUT:-600}
|
|
10
|
+
|
|
11
|
+
rm -f "$EXIT_CODE_FILE"
|
|
12
|
+
|
|
13
|
+
bare "$SCRIPT_DIR/run-with-exit.js" &
|
|
14
|
+
BARE_PID=$!
|
|
15
|
+
|
|
16
|
+
ELAPSED=0
|
|
17
|
+
while [ ! -f "$EXIT_CODE_FILE" ]; do
|
|
18
|
+
sleep 1
|
|
19
|
+
ELAPSED=$((ELAPSED + 1))
|
|
20
|
+
if ! kill -0 "$BARE_PID" 2>/dev/null; then
|
|
21
|
+
wait "$BARE_PID" 2>/dev/null
|
|
22
|
+
BARE_EC=$?
|
|
23
|
+
if [ -f "$EXIT_CODE_FILE" ]; then
|
|
24
|
+
break
|
|
25
|
+
fi
|
|
26
|
+
exit "$BARE_EC"
|
|
27
|
+
fi
|
|
28
|
+
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
|
|
29
|
+
echo "# integration-test timeout (${TIMEOUT}s) — forcing exit"
|
|
30
|
+
kill "$BARE_PID" 2>/dev/null
|
|
31
|
+
sleep 1
|
|
32
|
+
kill -9 "$BARE_PID" 2>/dev/null
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
EC=$(cat "$EXIT_CODE_FILE")
|
|
38
|
+
rm -f "$EXIT_CODE_FILE"
|
|
39
|
+
kill "$BARE_PID" 2>/dev/null
|
|
40
|
+
sleep 1
|
|
41
|
+
kill -9 "$BARE_PID" 2>/dev/null
|
|
42
|
+
wait "$BARE_PID" 2>/dev/null
|
|
43
|
+
exit "$EC"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Native addon keeps the event loop alive, so brittle's beforeExit never fires
|
|
4
|
+
// and the process hangs after all tests pass. This wrapper detects completion
|
|
5
|
+
// via two mechanisms: (1) intercepting brittle's TAP summary via console.log,
|
|
6
|
+
// and (2) polling test count stability. When done it writes the exit code to
|
|
7
|
+
// .exit-code so the shell wrapper (run-tests.sh) can kill bare and propagate
|
|
8
|
+
// the correct status.
|
|
9
|
+
|
|
10
|
+
const fs = require('bare-fs')
|
|
11
|
+
const path = require('bare-path')
|
|
12
|
+
|
|
13
|
+
const origLog = console.log
|
|
14
|
+
|
|
15
|
+
// Patch console.log BEFORE loading tests so brittle's _log captures our version.
|
|
16
|
+
console.log = function () {
|
|
17
|
+
origLog.apply(console, arguments)
|
|
18
|
+
if (arguments.length === 1) {
|
|
19
|
+
const arg = arguments[0]
|
|
20
|
+
if (arg === '# ok' || arg === '# not ok') {
|
|
21
|
+
flushAndWriteExitCode()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
require('./all.js')
|
|
27
|
+
|
|
28
|
+
const RUNNER = Symbol.for('brittle-runner')
|
|
29
|
+
|
|
30
|
+
let utils = null
|
|
31
|
+
try { utils = require('./utils.js') } catch (_) {}
|
|
32
|
+
|
|
33
|
+
function flushAndWriteExitCode () {
|
|
34
|
+
if (utils && typeof utils.flushPerfReport === 'function') {
|
|
35
|
+
try { utils.flushPerfReport() } catch (_) {}
|
|
36
|
+
}
|
|
37
|
+
writeExitCode()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeExitCode () {
|
|
41
|
+
let code = 0
|
|
42
|
+
if (global.Bare && global.Bare.exitCode !== undefined) code = global.Bare.exitCode
|
|
43
|
+
try {
|
|
44
|
+
fs.writeFileSync(path.join(__dirname, '.exit-code'), String(code))
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Backup: poll test count stability in case console.log interception fails.
|
|
49
|
+
let lastCount = 0
|
|
50
|
+
let stableTicks = 0
|
|
51
|
+
let done = false
|
|
52
|
+
const stabilityPoll = setInterval(function () {
|
|
53
|
+
if (done) { clearInterval(stabilityPoll); return }
|
|
54
|
+
const runner = global[RUNNER]
|
|
55
|
+
if (!runner || !runner.started) return
|
|
56
|
+
|
|
57
|
+
const count = runner.tests.count
|
|
58
|
+
if (count > 0 && count === lastCount && runner.next === null) {
|
|
59
|
+
stableTicks++
|
|
60
|
+
} else {
|
|
61
|
+
stableTicks = 0
|
|
62
|
+
}
|
|
63
|
+
lastCount = count
|
|
64
|
+
|
|
65
|
+
if (stableTicks >= 3) {
|
|
66
|
+
done = true
|
|
67
|
+
clearInterval(stabilityPoll)
|
|
68
|
+
flushAndWriteExitCode()
|
|
69
|
+
try { runner.end() } catch (e) {}
|
|
70
|
+
}
|
|
71
|
+
}, 5000)
|