@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.
Files changed (57) hide show
  1. package/LICENSE +179 -0
  2. package/NOTICE +63 -0
  3. package/README.md +330 -0
  4. package/addonLogging.d.ts +7 -0
  5. package/addonLogging.js +4 -0
  6. package/binding.js +21 -0
  7. package/index.d.ts +145 -0
  8. package/index.js +383 -0
  9. package/lib/error.js +80 -0
  10. package/ocr-ggml.js +121 -0
  11. package/package.json +96 -7
  12. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.0_1.so +0 -0
  13. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.2_1.so +0 -0
  14. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.2_2.so +0 -0
  15. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv8.6_1.so +0 -0
  16. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv9.0_1.so +0 -0
  17. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv9.2_1.so +0 -0
  18. package/prebuilds/android-arm64/qvac__ocr-ggml/libqvac-ggml-cpu-android_armv9.2_2.so +0 -0
  19. package/prebuilds/android-arm64/qvac__ocr-ggml.bare +0 -0
  20. package/prebuilds/darwin-arm64/qvac__ocr-ggml.bare +0 -0
  21. package/prebuilds/darwin-arm64/qvac__ocr-ggml.bare.exports +4277 -0
  22. package/prebuilds/darwin-x64/qvac__ocr-ggml.bare +0 -0
  23. package/prebuilds/darwin-x64/qvac__ocr-ggml.bare.exports +4322 -0
  24. package/prebuilds/ios-arm64/qvac__ocr-ggml.bare +0 -0
  25. package/prebuilds/ios-arm64/qvac__ocr-ggml.bare.exports +4262 -0
  26. package/prebuilds/ios-arm64-simulator/qvac__ocr-ggml.bare +0 -0
  27. package/prebuilds/ios-arm64-simulator/qvac__ocr-ggml.bare.exports +4262 -0
  28. package/prebuilds/ios-x64-simulator/qvac__ocr-ggml.bare +0 -0
  29. package/prebuilds/ios-x64-simulator/qvac__ocr-ggml.bare.exports +4311 -0
  30. package/prebuilds/linux-arm64/qvac__ocr-ggml.bare +0 -0
  31. package/prebuilds/linux-x64/qvac__ocr-ggml.bare +0 -0
  32. package/prebuilds/win32-x64/qvac__ocr-ggml.bare +0 -0
  33. package/prebuilds/win32-x64/qvac__ocr-ggml.bare.exports +0 -0
  34. package/test/integration/canvas-size.test.js +75 -0
  35. package/test/integration/doctr-basic.test.js +124 -0
  36. package/test/integration/doctr-clinical-chemistry.test.js +64 -0
  37. package/test/integration/doctr-ct-scan.test.js +66 -0
  38. package/test/integration/doctr-lab-results.test.js +65 -0
  39. package/test/integration/doctr-liver-function.test.js +67 -0
  40. package/test/integration/doctr-models.test.js +132 -0
  41. package/test/integration/doctr-param-validation.test.js +83 -0
  42. package/test/integration/error-handling.test.js +302 -0
  43. package/test/integration/full-coverage.test.js +292 -0
  44. package/test/integration/full-ocr-suite.test.js +83 -0
  45. package/test/integration/image-formats.test.js +168 -0
  46. package/test/integration/large-images.test.js +81 -0
  47. package/test/integration/lifecycle.test.js +269 -0
  48. package/test/integration/ocr-basic.test.js +70 -0
  49. package/test/integration/param-validation.test.js +86 -0
  50. package/test/integration/pipeline.test.js +61 -0
  51. package/test/integration/run-internal-ordering.test.js +75 -0
  52. package/test/integration/run-tests.sh +43 -0
  53. package/test/integration/run-with-exit.js +71 -0
  54. package/test/integration/utils.js +694 -0
  55. package/test/mobile/integration-runtime.cjs +71 -0
  56. package/test/mobile/integration.auto.cjs +102 -0
  57. 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)