dsh-ab-ocr 0.1.0
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 +21 -0
- package/README.md +378 -0
- package/cordis.patch.yml +7 -0
- package/lib/artifacts.d.ts +100 -0
- package/lib/artifacts.d.ts.map +1 -0
- package/lib/artifacts.js +97 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/config.d.ts +77 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +51 -0
- package/lib/config.js.map +1 -0
- package/lib/documents.d.ts +62 -0
- package/lib/documents.d.ts.map +1 -0
- package/lib/documents.js +173 -0
- package/lib/documents.js.map +1 -0
- package/lib/events.d.ts +161 -0
- package/lib/events.d.ts.map +1 -0
- package/lib/events.js +158 -0
- package/lib/events.js.map +1 -0
- package/lib/filename.d.ts +47 -0
- package/lib/filename.d.ts.map +1 -0
- package/lib/filename.js +77 -0
- package/lib/filename.js.map +1 -0
- package/lib/index.d.ts +85 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1761 -0
- package/lib/index.js.map +1 -0
- package/lib/levels.d.ts +24 -0
- package/lib/levels.d.ts.map +1 -0
- package/lib/levels.js +52 -0
- package/lib/levels.js.map +1 -0
- package/lib/plan.d.ts +103 -0
- package/lib/plan.d.ts.map +1 -0
- package/lib/plan.js +210 -0
- package/lib/plan.js.map +1 -0
- package/lib/recognize.d.ts +36 -0
- package/lib/recognize.d.ts.map +1 -0
- package/lib/recognize.js +390 -0
- package/lib/recognize.js.map +1 -0
- package/lib/records.d.ts +91 -0
- package/lib/records.d.ts.map +1 -0
- package/lib/records.js +130 -0
- package/lib/records.js.map +1 -0
- package/lib/render.d.ts +19 -0
- package/lib/render.d.ts.map +1 -0
- package/lib/render.js +45 -0
- package/lib/render.js.map +1 -0
- package/lib/sandbox.d.ts +54 -0
- package/lib/sandbox.d.ts.map +1 -0
- package/lib/sandbox.js +101 -0
- package/lib/sandbox.js.map +1 -0
- package/lib/types.d.ts +147 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +7 -0
- package/lib/types.js.map +1 -0
- package/lib/worker.d.ts +107 -0
- package/lib/worker.d.ts.map +1 -0
- package/lib/worker.js +143 -0
- package/lib/worker.js.map +1 -0
- package/package.json +98 -0
- package/python/README.md +125 -0
- package/python/assemble.py +358 -0
- package/python/clean.py +197 -0
- package/python/layout.py +403 -0
- package/python/ocr_worker.py +516 -0
- package/python/requirements.txt +16 -0
- package/python/source.py +182 -0
- package/scripts/setup.mjs +251 -0
- package/tsconfig.json +30 -0
- package/tsdown.config.ts +18 -0
package/python/source.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Document sources: one still image, or a PDF rendered one page at a time.
|
|
2
|
+
|
|
3
|
+
Every source reports how many pages it carries and renders one page on demand,
|
|
4
|
+
so the caller can OCR a page and drop its pixels before asking for the next. No
|
|
5
|
+
source keeps more than the page currently being processed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
#: Bytes read at a time while digesting a source, so a large scan is not held twice.
|
|
15
|
+
DIGEST_CHUNK = 1024 * 1024
|
|
16
|
+
|
|
17
|
+
#: Extensions handled as a still image (a multi-frame file is read frame by frame).
|
|
18
|
+
IMAGE_SUFFIXES = frozenset({
|
|
19
|
+
'.png', '.jpg', '.jpeg', '.jpe', '.bmp', '.dib', '.tif', '.tiff',
|
|
20
|
+
'.webp', '.gif', '.jp2', '.pbm', '.pgm', '.ppm', '.tga', '.ico',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
#: Extensions handled as a PDF.
|
|
24
|
+
PDF_SUFFIXES = frozenset({'.pdf'})
|
|
25
|
+
|
|
26
|
+
#: Smallest render scale, so an absurd maxPixels still yields a readable page.
|
|
27
|
+
MIN_SCALE = 0.2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnsupportedDocument(Exception):
|
|
31
|
+
"""Raised when a path is neither a readable image nor a readable PDF."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def source_digest(path: str) -> str:
|
|
35
|
+
"""Digest a source file's bytes.
|
|
36
|
+
|
|
37
|
+
The caller names a recognition after this, so the name follows the document's
|
|
38
|
+
content and not merely its length: two files that happen to weigh the same are
|
|
39
|
+
still two recognitions, and a file that has not changed is always the same one.
|
|
40
|
+
@param path: the source file to read.
|
|
41
|
+
@returns: the lowercase hex digest of its bytes.
|
|
42
|
+
"""
|
|
43
|
+
import hashlib
|
|
44
|
+
|
|
45
|
+
digest = hashlib.sha256()
|
|
46
|
+
with open(path, 'rb') as handle:
|
|
47
|
+
for chunk in iter(lambda: handle.read(DIGEST_CHUNK), b''):
|
|
48
|
+
digest.update(chunk)
|
|
49
|
+
return digest.hexdigest()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class RenderedPage:
|
|
54
|
+
"""One page's pixels, in the channel order the OCR engine expects."""
|
|
55
|
+
|
|
56
|
+
index: int
|
|
57
|
+
width: int
|
|
58
|
+
height: int
|
|
59
|
+
image: Any
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Document:
|
|
63
|
+
"""A page source the worker renders and releases one page at a time."""
|
|
64
|
+
|
|
65
|
+
def page_count(self) -> int:
|
|
66
|
+
"""Return the number of pages this source carries."""
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
def render(self, index: int) -> RenderedPage:
|
|
70
|
+
"""Render one zero-based page into a BGR array."""
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
"""Release the underlying file handle or document."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def document_kind(path: str) -> str:
|
|
78
|
+
"""Classify a path as a PDF, an image, or unsupported."""
|
|
79
|
+
suffix = os.path.splitext(path)[1].lower()
|
|
80
|
+
if suffix in PDF_SUFFIXES:
|
|
81
|
+
return 'pdf'
|
|
82
|
+
if suffix in IMAGE_SUFFIXES:
|
|
83
|
+
return 'image'
|
|
84
|
+
return 'unsupported'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ImageDocument(Document):
|
|
88
|
+
"""A still image, or every frame of a multi-frame image file."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, path: str, max_pixels: int) -> None:
|
|
91
|
+
from PIL import Image
|
|
92
|
+
|
|
93
|
+
self._image = Image.open(path)
|
|
94
|
+
self._frames = int(getattr(self._image, 'n_frames', 1))
|
|
95
|
+
self._index = 0
|
|
96
|
+
self._max_pixels = max_pixels
|
|
97
|
+
if self._frames > 1:
|
|
98
|
+
# Force a decode now so a corrupt later frame fails before any OCR.
|
|
99
|
+
self._image.seek(0)
|
|
100
|
+
|
|
101
|
+
def page_count(self) -> int:
|
|
102
|
+
return self._frames
|
|
103
|
+
|
|
104
|
+
def render(self, index: int) -> RenderedPage:
|
|
105
|
+
# Imported here so the worker's protocol helpers load on an interpreter
|
|
106
|
+
# that carries no OCR package, which is where the unit suite runs.
|
|
107
|
+
import numpy as np
|
|
108
|
+
|
|
109
|
+
if index != self._index:
|
|
110
|
+
self._image.seek(index)
|
|
111
|
+
self._index = index
|
|
112
|
+
frame = self._image.convert('RGB')
|
|
113
|
+
array = np.asarray(frame)
|
|
114
|
+
array = np.ascontiguousarray(array[:, :, ::-1])
|
|
115
|
+
pixels = self._max_pixels
|
|
116
|
+
if pixels > 0 and array.shape[0] * array.shape[1] > pixels:
|
|
117
|
+
step = max(1, int(np.ceil(np.sqrt(array.shape[0] * array.shape[1] / pixels))))
|
|
118
|
+
array = np.ascontiguousarray(array[::step, ::step])
|
|
119
|
+
return RenderedPage(index=index + 1, width=array.shape[1], height=array.shape[0], image=array)
|
|
120
|
+
|
|
121
|
+
def close(self) -> None:
|
|
122
|
+
self._image.close()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PdfDocument(Document):
|
|
126
|
+
"""A PDF, rendered with PDFium at a scale derived from the requested DPI."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, path: str, dpi: int, max_pixels: int) -> None:
|
|
129
|
+
import pypdfium2 as pdfium
|
|
130
|
+
|
|
131
|
+
self._pdfium = pdfium
|
|
132
|
+
self._document = pdfium.PdfDocument(path)
|
|
133
|
+
self._dpi = dpi
|
|
134
|
+
self._max_pixels = max_pixels
|
|
135
|
+
|
|
136
|
+
def page_count(self) -> int:
|
|
137
|
+
return len(self._document)
|
|
138
|
+
|
|
139
|
+
def render(self, index: int) -> RenderedPage:
|
|
140
|
+
import numpy as np
|
|
141
|
+
|
|
142
|
+
page = self._document[index]
|
|
143
|
+
try:
|
|
144
|
+
scale = self._dpi / 72.0
|
|
145
|
+
if self._max_pixels > 0:
|
|
146
|
+
width_points, height_points = page.get_size()
|
|
147
|
+
pixels = (width_points * scale) * (height_points * scale)
|
|
148
|
+
if pixels > self._max_pixels:
|
|
149
|
+
scale = max(MIN_SCALE, scale * (self._max_pixels / pixels) ** 0.5)
|
|
150
|
+
bitmap = page.render(scale=scale)
|
|
151
|
+
try:
|
|
152
|
+
# PDFium's default bitmap is BGRA, which is the channel order the
|
|
153
|
+
# OCR engine's detector and recognizer were trained on.
|
|
154
|
+
array = np.ascontiguousarray(bitmap.to_numpy())
|
|
155
|
+
finally:
|
|
156
|
+
bitmap.close()
|
|
157
|
+
finally:
|
|
158
|
+
# Closing each page immediately is what keeps a long document's
|
|
159
|
+
# resident cost flat: PDFium holds per-page caches otherwise.
|
|
160
|
+
page.close()
|
|
161
|
+
return RenderedPage(index=index + 1, width=array.shape[1], height=array.shape[0], image=array)
|
|
162
|
+
|
|
163
|
+
def close(self) -> None:
|
|
164
|
+
self._document.close()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def open_document(path: str, dpi: int, max_pixels: int) -> Document:
|
|
168
|
+
"""Open a path as a page source.
|
|
169
|
+
|
|
170
|
+
@param path: path to a PDF or a still image.
|
|
171
|
+
@param dpi: render resolution for PDF pages.
|
|
172
|
+
@param max_pixels: ceiling on one rendered page's pixel count; 0 disables it.
|
|
173
|
+
@returns: a document that renders one page per call.
|
|
174
|
+
"""
|
|
175
|
+
kind = document_kind(path)
|
|
176
|
+
if kind == 'pdf':
|
|
177
|
+
return PdfDocument(path, dpi, max_pixels)
|
|
178
|
+
if kind == 'image':
|
|
179
|
+
return ImageDocument(path, max_pixels)
|
|
180
|
+
raise UnsupportedDocument(
|
|
181
|
+
'unsupported document: ' + os.path.basename(path) + ' is neither a PDF nor a supported image'
|
|
182
|
+
)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the Python environment the OCR worker runs in.
|
|
3
|
+
*
|
|
4
|
+
* The OCR engine and its model weights ship inside the RapidOCR wheel, so
|
|
5
|
+
* "installing the model" and "installing the dependencies" are the same act:
|
|
6
|
+
* a virtual environment holding `python/requirements.txt`. This script performs
|
|
7
|
+
* that act, verifies it with the worker's own `--self-test`, and reports what it
|
|
8
|
+
* found — including the model files the wheel actually carries, so a failure to
|
|
9
|
+
* recognize can be told apart from a failure to install.
|
|
10
|
+
*
|
|
11
|
+
* It is idempotent: a run against a working environment verifies and exits
|
|
12
|
+
* without touching the network. `--force` rebuilds the environment from scratch.
|
|
13
|
+
* @module @deepseek-ai/dsh-ab-ocr/scripts/setup
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from 'node:child_process'
|
|
17
|
+
import { existsSync, readFileSync, rmSync } from 'node:fs'
|
|
18
|
+
import { dirname, join, resolve } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const PACKAGE = resolve(HERE, '..')
|
|
23
|
+
const PYTHON_DIR = join(PACKAGE, 'python')
|
|
24
|
+
const REQUIREMENTS = join(PYTHON_DIR, 'requirements.txt')
|
|
25
|
+
const WORKER = join(PYTHON_DIR, 'ocr_worker.py')
|
|
26
|
+
const VENV = join(PYTHON_DIR, '.venv')
|
|
27
|
+
|
|
28
|
+
/** Interpreter names to try, in order, when no path is given. */
|
|
29
|
+
const CANDIDATES = ['python', 'python3', 'py']
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run one command and capture its streams.
|
|
33
|
+
* @param command - the executable.
|
|
34
|
+
* @param args - its arguments.
|
|
35
|
+
* @returns the exit status and the two streams.
|
|
36
|
+
*/
|
|
37
|
+
function run(command, args) {
|
|
38
|
+
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true })
|
|
39
|
+
return {
|
|
40
|
+
status: result.status,
|
|
41
|
+
stdout: result.stdout ?? '',
|
|
42
|
+
stderr: result.stderr ?? '',
|
|
43
|
+
error: result.error,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find a base interpreter that runs.
|
|
49
|
+
* @param requested - an explicit path from the command line or the environment.
|
|
50
|
+
* @returns the working command, or undefined when none of the candidates run.
|
|
51
|
+
*/
|
|
52
|
+
function findInterpreter(requested) {
|
|
53
|
+
const tried = requested === undefined ? CANDIDATES : [requested]
|
|
54
|
+
for (const candidate of tried) {
|
|
55
|
+
const probe = run(candidate, ['--version'])
|
|
56
|
+
if (probe.error === undefined && probe.status === 0) return candidate
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The interpreter inside the virtual environment.
|
|
63
|
+
* @returns its path, which differs by platform.
|
|
64
|
+
*/
|
|
65
|
+
function venvInterpreter() {
|
|
66
|
+
return process.platform === 'win32'
|
|
67
|
+
? join(VENV, 'Scripts', 'python.exe')
|
|
68
|
+
: join(VENV, 'bin', 'python')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Ask the worker to report its environment.
|
|
73
|
+
* @param python - the interpreter to ask.
|
|
74
|
+
* @returns the parsed report, or undefined when the worker did not answer.
|
|
75
|
+
*/
|
|
76
|
+
function selfTest(python) {
|
|
77
|
+
const result = run(python, [WORKER, '--self-test'])
|
|
78
|
+
const line = result.stdout.split('\n').find(entry => entry.trim() !== '')
|
|
79
|
+
if (line === undefined) return undefined
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(line)
|
|
82
|
+
} catch {
|
|
83
|
+
// A worker that cannot start prints nothing parseable; the caller reports the
|
|
84
|
+
// absence rather than this parse.
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The model files the installed wheel carries.
|
|
91
|
+
* @param python - the interpreter holding RapidOCR.
|
|
92
|
+
* @returns file names mapped to their size in bytes, or an empty record.
|
|
93
|
+
*/
|
|
94
|
+
function modelFiles(python) {
|
|
95
|
+
const script = [
|
|
96
|
+
'import json, os, rapidocr',
|
|
97
|
+
"d = os.path.join(os.path.dirname(rapidocr.__file__), 'models')",
|
|
98
|
+
"names = sorted(n for n in os.listdir(d) if n.endswith('.onnx')) if os.path.isdir(d) else []",
|
|
99
|
+
'print(json.dumps({n: os.path.getsize(os.path.join(d, n)) for n in names}))',
|
|
100
|
+
].join('\n')
|
|
101
|
+
const result = run(python, ['-c', script])
|
|
102
|
+
if (result.status !== 0) return {}
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(result.stdout.trim())
|
|
105
|
+
} catch {
|
|
106
|
+
return {}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse the command line.
|
|
112
|
+
* @param argv - the arguments after the script name.
|
|
113
|
+
* @returns the options, or a message describing what is wrong.
|
|
114
|
+
*/
|
|
115
|
+
function parseArgs(argv) {
|
|
116
|
+
const options = { force: false, prefetch: false, json: false, python: process.env.DSH_OCR_PYTHON }
|
|
117
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
118
|
+
const flag = argv[index]
|
|
119
|
+
if (flag === '--force') options.force = true
|
|
120
|
+
else if (flag === '--prefetch') options.prefetch = true
|
|
121
|
+
else if (flag === '--json') options.json = true
|
|
122
|
+
else if (flag === '--python') {
|
|
123
|
+
const value = argv[index += 1]
|
|
124
|
+
if (value === undefined || value.startsWith('--')) return { error: '--python needs an interpreter path' }
|
|
125
|
+
options.python = value
|
|
126
|
+
} else return { error: `unknown option ${JSON.stringify(flag)}` }
|
|
127
|
+
}
|
|
128
|
+
return options
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Lay out one version of a requirement line for the report.
|
|
133
|
+
* @param requirements - the requirement file's text.
|
|
134
|
+
* @returns the requirement lines, comments dropped.
|
|
135
|
+
*/
|
|
136
|
+
function requirements(requirements) {
|
|
137
|
+
return readFileSync(requirements, 'utf8')
|
|
138
|
+
.split('\n')
|
|
139
|
+
.map(line => line.trim())
|
|
140
|
+
.filter(line => line !== '' && !line.startsWith('#'))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Install one environment and verify it.
|
|
145
|
+
* @param options - the parsed command line.
|
|
146
|
+
* @returns the process exit status.
|
|
147
|
+
*/
|
|
148
|
+
function main(options) {
|
|
149
|
+
const say = (message) => {
|
|
150
|
+
if (!options.json) console.log(message)
|
|
151
|
+
}
|
|
152
|
+
const base = findInterpreter(options.python)
|
|
153
|
+
if (base === undefined) {
|
|
154
|
+
console.error('setup: no Python interpreter found; pass --python <path> or set DSH_OCR_PYTHON')
|
|
155
|
+
return 2
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const python = venvInterpreter()
|
|
159
|
+
const existing = existsSync(python)
|
|
160
|
+
say(`setup: base interpreter ${base}`)
|
|
161
|
+
say(`setup: environment ${VENV}${existing ? '' : ' (creating)'}`)
|
|
162
|
+
|
|
163
|
+
if (options.force && existsSync(VENV)) {
|
|
164
|
+
rmSync(VENV, { recursive: true, force: true })
|
|
165
|
+
say('setup: removed the existing environment (--force)')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!existsSync(python)) {
|
|
169
|
+
const created = run(base, ['-m', 'venv', VENV])
|
|
170
|
+
if (created.status !== 0) {
|
|
171
|
+
console.error(`setup: could not create the environment\n${created.stderr.trim()}`)
|
|
172
|
+
return 3
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let report = selfTest(python)
|
|
177
|
+
if (report?.ready !== true) {
|
|
178
|
+
say('setup: installing ' + requirements(REQUIREMENTS).join(', '))
|
|
179
|
+
// Prefer a local wheels cache shipped with the plugin (python/wheels) so
|
|
180
|
+
// setup works offline; pip still falls back to the configured index for any
|
|
181
|
+
// wheel not present locally.
|
|
182
|
+
const wheelsDir = join(PYTHON_DIR, 'wheels')
|
|
183
|
+
const findLinks = existsSync(wheelsDir)
|
|
184
|
+
? ['--find-links', wheelsDir]
|
|
185
|
+
: []
|
|
186
|
+
if (findLinks.length) say(`setup: using local wheel cache ${wheelsDir}`)
|
|
187
|
+
// Bootstrap the build tools from the local cache so --no-build-isolation can
|
|
188
|
+
// compile any source distribution (e.g. antlr4-python3-runtime) without
|
|
189
|
+
// reaching the network. Standard venvs ship these, but an offline venv may
|
|
190
|
+
// not, and a build-isolation subprocess would otherwise hit the index.
|
|
191
|
+
if (findLinks.length) {
|
|
192
|
+
const bootstrap = run(python, ['-m', 'pip', 'install', '--disable-pip-version-check', ...findLinks, 'setuptools', 'wheel'])
|
|
193
|
+
if (bootstrap.status !== 0) {
|
|
194
|
+
console.error(`setup: could not bootstrap build tools\n${bootstrap.stderr.trim()}`)
|
|
195
|
+
return 3
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const install = run(python, ['-m', 'pip', 'install', '--disable-pip-version-check', '--no-build-isolation', ...findLinks, '-r', REQUIREMENTS])
|
|
199
|
+
if (install.status !== 0) {
|
|
200
|
+
console.error(`setup: pip failed\n${install.stderr.trim()}`)
|
|
201
|
+
return 3
|
|
202
|
+
}
|
|
203
|
+
report = selfTest(python)
|
|
204
|
+
}
|
|
205
|
+
if (report?.ready !== true) {
|
|
206
|
+
console.error('setup: the environment still fails its own check')
|
|
207
|
+
for (const [name, version] of Object.entries(report?.modules ?? {})) {
|
|
208
|
+
if (version === null || String(name).endsWith('Error')) console.error(` ${name}: ${version ?? 'missing'}`)
|
|
209
|
+
}
|
|
210
|
+
return 3
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const models = modelFiles(python)
|
|
214
|
+
if (options.prefetch) {
|
|
215
|
+
say('setup: loading the engine and its models')
|
|
216
|
+
const warm = run(python, ['-c', 'import logging; logging.disable(logging.CRITICAL); from rapidocr import RapidOCR; RapidOCR()'])
|
|
217
|
+
if (warm.status !== 0) {
|
|
218
|
+
console.error(`setup: the engine did not load\n${warm.stderr.trim()}`)
|
|
219
|
+
return 3
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const summary = {
|
|
224
|
+
python,
|
|
225
|
+
base,
|
|
226
|
+
version: report.python,
|
|
227
|
+
modules: report.modules,
|
|
228
|
+
models,
|
|
229
|
+
prefetched: options.prefetch,
|
|
230
|
+
}
|
|
231
|
+
if (options.json) {
|
|
232
|
+
console.log(JSON.stringify(summary, null, 2))
|
|
233
|
+
return 0
|
|
234
|
+
}
|
|
235
|
+
say(`setup: ready — Python ${report.python}`)
|
|
236
|
+
for (const [name, version] of Object.entries(report.modules)) say(` ${name} ${version}`)
|
|
237
|
+
const names = Object.keys(models)
|
|
238
|
+
const bytes = Object.values(models).reduce((total, size) => total + size, 0)
|
|
239
|
+
say(names.length === 0
|
|
240
|
+
? 'setup: no model files found beside the wheel'
|
|
241
|
+
: `setup: ${names.length} model file(s), ${(bytes / 1048576).toFixed(1)} MB — ${names.join(', ')}`)
|
|
242
|
+
return 0
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const OPTIONS = parseArgs(process.argv.slice(2))
|
|
246
|
+
if ('error' in OPTIONS) {
|
|
247
|
+
console.error(`setup: ${OPTIONS.error}`)
|
|
248
|
+
process.exitCode = 2
|
|
249
|
+
} else {
|
|
250
|
+
process.exitCode = main(OPTIONS)
|
|
251
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"rewriteRelativeImportExtensions": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUncheckedIndexedAccess": true,
|
|
15
|
+
"exactOptionalPropertyTypes": true,
|
|
16
|
+
"noImplicitOverride": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"jsx": "react-jsx",
|
|
21
|
+
"types": [
|
|
22
|
+
"node"
|
|
23
|
+
],
|
|
24
|
+
"rootDir": "src",
|
|
25
|
+
"outDir": "lib"
|
|
26
|
+
},
|
|
27
|
+
"include": [
|
|
28
|
+
"src"
|
|
29
|
+
]
|
|
30
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Bundle config: one node entry (host tool). */
|
|
2
|
+
import { defineConfig } from 'tsdown'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
name: "dsh-ab-ocr",
|
|
6
|
+
entry: ['lib/index.js'],
|
|
7
|
+
outDir: 'lib',
|
|
8
|
+
format: ['esm'],
|
|
9
|
+
platform: 'node',
|
|
10
|
+
target: 'es2024',
|
|
11
|
+
fixedExtension: false,
|
|
12
|
+
dts: false,
|
|
13
|
+
clean: false,
|
|
14
|
+
deps: {
|
|
15
|
+
neverBundle: [/^@deepseek-ai\//],
|
|
16
|
+
alwaysBundle: (specifier) => !specifier.startsWith('@deepseek-ai/') && !specifier.startsWith('node:'),
|
|
17
|
+
},
|
|
18
|
+
})
|