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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -0
  3. package/cordis.patch.yml +7 -0
  4. package/lib/artifacts.d.ts +100 -0
  5. package/lib/artifacts.d.ts.map +1 -0
  6. package/lib/artifacts.js +97 -0
  7. package/lib/artifacts.js.map +1 -0
  8. package/lib/config.d.ts +77 -0
  9. package/lib/config.d.ts.map +1 -0
  10. package/lib/config.js +51 -0
  11. package/lib/config.js.map +1 -0
  12. package/lib/documents.d.ts +62 -0
  13. package/lib/documents.d.ts.map +1 -0
  14. package/lib/documents.js +173 -0
  15. package/lib/documents.js.map +1 -0
  16. package/lib/events.d.ts +161 -0
  17. package/lib/events.d.ts.map +1 -0
  18. package/lib/events.js +158 -0
  19. package/lib/events.js.map +1 -0
  20. package/lib/filename.d.ts +47 -0
  21. package/lib/filename.d.ts.map +1 -0
  22. package/lib/filename.js +77 -0
  23. package/lib/filename.js.map +1 -0
  24. package/lib/index.d.ts +85 -0
  25. package/lib/index.d.ts.map +1 -0
  26. package/lib/index.js +1761 -0
  27. package/lib/index.js.map +1 -0
  28. package/lib/levels.d.ts +24 -0
  29. package/lib/levels.d.ts.map +1 -0
  30. package/lib/levels.js +52 -0
  31. package/lib/levels.js.map +1 -0
  32. package/lib/plan.d.ts +103 -0
  33. package/lib/plan.d.ts.map +1 -0
  34. package/lib/plan.js +210 -0
  35. package/lib/plan.js.map +1 -0
  36. package/lib/recognize.d.ts +36 -0
  37. package/lib/recognize.d.ts.map +1 -0
  38. package/lib/recognize.js +390 -0
  39. package/lib/recognize.js.map +1 -0
  40. package/lib/records.d.ts +91 -0
  41. package/lib/records.d.ts.map +1 -0
  42. package/lib/records.js +130 -0
  43. package/lib/records.js.map +1 -0
  44. package/lib/render.d.ts +19 -0
  45. package/lib/render.d.ts.map +1 -0
  46. package/lib/render.js +45 -0
  47. package/lib/render.js.map +1 -0
  48. package/lib/sandbox.d.ts +54 -0
  49. package/lib/sandbox.d.ts.map +1 -0
  50. package/lib/sandbox.js +101 -0
  51. package/lib/sandbox.js.map +1 -0
  52. package/lib/types.d.ts +147 -0
  53. package/lib/types.d.ts.map +1 -0
  54. package/lib/types.js +7 -0
  55. package/lib/types.js.map +1 -0
  56. package/lib/worker.d.ts +107 -0
  57. package/lib/worker.d.ts.map +1 -0
  58. package/lib/worker.js +143 -0
  59. package/lib/worker.js.map +1 -0
  60. package/package.json +98 -0
  61. package/python/README.md +125 -0
  62. package/python/assemble.py +358 -0
  63. package/python/clean.py +197 -0
  64. package/python/layout.py +403 -0
  65. package/python/ocr_worker.py +516 -0
  66. package/python/requirements.txt +16 -0
  67. package/python/source.py +182 -0
  68. package/scripts/setup.mjs +251 -0
  69. package/tsconfig.json +30 -0
  70. package/tsdown.config.ts +18 -0
package/lib/worker.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Transport to the OCR worker process. The worker speaks newline-delimited
3
+ * JSON: one event per line on stdout, diagnostics on stderr. This module owns
4
+ * the process lifetime, the spec handoff, the timeout, and the cancellation, so
5
+ * the tool's entry module only has to interpret events.
6
+ * @module @deepseek-ai/dsh-ab-ocr/worker
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ /** How much of the worker's diagnostics is kept for an error message. */
10
+ const STDERR_TAIL = 4000;
11
+ /**
12
+ * Split a run of text into whole lines, returning what is left of a partial one.
13
+ * @param buffered - text received so far, including any partial final line.
14
+ * @param chunk - newly received text.
15
+ * @returns the complete lines and the remaining partial line.
16
+ */
17
+ export function splitLines(buffered, chunk) {
18
+ const combined = buffered + chunk;
19
+ const parts = combined.split('\n');
20
+ const rest = parts.pop() ?? '';
21
+ return { lines: parts, rest };
22
+ }
23
+ /**
24
+ * Parse one event line, ignoring a line that is not a JSON object.
25
+ * @param line - one complete stdout line.
26
+ * @returns the parsed event, or undefined for a line that is not one.
27
+ */
28
+ export function parseEvent(line) {
29
+ const trimmed = line.trim();
30
+ if (trimmed === '')
31
+ return undefined;
32
+ try {
33
+ const value = JSON.parse(trimmed);
34
+ if (typeof value !== 'object' || value === null || typeof value.event !== 'string') {
35
+ return undefined;
36
+ }
37
+ return value;
38
+ }
39
+ catch {
40
+ // The worker writes only JSON on stdout, so a line that does not parse is a
41
+ // diagnostic that escaped the stream, not an event to act on.
42
+ return undefined;
43
+ }
44
+ }
45
+ /**
46
+ * Run the worker once, feeding it a spec and reporting its events.
47
+ * @param invocation - executable, script, and time ceiling.
48
+ * @param args - extra command-line arguments.
49
+ * @param input - text written to the worker's stdin, or an empty string to close it immediately.
50
+ * @param onEvent - called with each parsed event as it arrives; never awaited, so a
51
+ * slow consumer cannot stall the read.
52
+ * @param signal - caller cancellation.
53
+ * @returns the events, diagnostics, and exit state.
54
+ */
55
+ function spawnWorker(invocation, args, input, onEvent, signal) {
56
+ return new Promise((resolve) => {
57
+ const child = spawn(invocation.python, [invocation.script, ...args], {
58
+ stdio: ['pipe', 'pipe', 'pipe'],
59
+ windowsHide: true,
60
+ });
61
+ const events = [];
62
+ let stdout = '';
63
+ let stderr = '';
64
+ let timedOut = false;
65
+ let aborted = signal?.aborted === true;
66
+ let settled = false;
67
+ const timer = invocation.timeoutMs > 0
68
+ ? setTimeout(() => { timedOut = true; child.kill(); }, invocation.timeoutMs)
69
+ : undefined;
70
+ const onAbort = () => { aborted = true; child.kill(); };
71
+ signal?.addEventListener('abort', onAbort, { once: true });
72
+ const accept = (line) => {
73
+ const event = parseEvent(line);
74
+ if (event === undefined)
75
+ return;
76
+ events.push(event);
77
+ onEvent(event);
78
+ };
79
+ const settle = (exitCode) => {
80
+ if (settled)
81
+ return;
82
+ settled = true;
83
+ if (timer !== undefined)
84
+ clearTimeout(timer);
85
+ signal?.removeEventListener('abort', onAbort);
86
+ const tail = splitLines(stdout, '');
87
+ for (const line of [...tail.lines, tail.rest])
88
+ accept(line);
89
+ resolve({ events, stderr, exitCode, timedOut, aborted });
90
+ };
91
+ child.stdout.setEncoding('utf8');
92
+ child.stdout.on('data', (chunk) => {
93
+ const split = splitLines(stdout, chunk);
94
+ stdout = split.rest;
95
+ for (const line of split.lines)
96
+ accept(line);
97
+ });
98
+ child.stderr.setEncoding('utf8');
99
+ child.stderr.on('data', (chunk) => {
100
+ stderr = (stderr + chunk).slice(-STDERR_TAIL);
101
+ });
102
+ // A worker that cannot even start reports the failure here rather than on close.
103
+ child.on('error', (error) => {
104
+ stderr = (stderr + error.message).slice(-STDERR_TAIL);
105
+ settle(null);
106
+ });
107
+ child.on('close', (code) => settle(code));
108
+ child.stdin.on('error', () => {
109
+ // The worker closed its input early, which its exit code already reports.
110
+ });
111
+ child.stdin.end(input);
112
+ });
113
+ }
114
+ /**
115
+ * Ask the worker to report its environment.
116
+ * @param invocation - executable, script, and the ceiling this check may take.
117
+ * @param signal - caller cancellation.
118
+ * @returns the report and the diagnostics, whether or not the worker started.
119
+ */
120
+ export async function runSelfTest(invocation, signal) {
121
+ const run = await spawnWorker(invocation, ['--self-test'], '', () => { }, signal);
122
+ return {
123
+ report: run.events.find(event => event.event === 'ready'),
124
+ stderr: run.stderr,
125
+ exitCode: run.exitCode,
126
+ };
127
+ }
128
+ /**
129
+ * Run one batch of documents through the worker.
130
+ *
131
+ * The event callback sees each event as it is parsed, which is what lets the
132
+ * caller persist a finished page before the next one is recognized instead of
133
+ * holding the whole document until the process exits.
134
+ * @param invocation - executable, script, and time ceiling.
135
+ * @param spec - the documents and options to process.
136
+ * @param onEvent - called with each parsed event as it arrives.
137
+ * @param signal - caller cancellation.
138
+ * @returns the events, diagnostics, and exit state.
139
+ */
140
+ export function runSpec(invocation, spec, onEvent, signal) {
141
+ return spawnWorker(invocation, [], JSON.stringify(spec), onEvent, signal);
142
+ }
143
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAuE1C,yEAAyE;AACzE,MAAM,WAAW,GAAG,IAAI,CAAA;AAExB;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,QAAgB,EAAE,KAAa;IACxD,MAAM,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAA;IACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAClC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;IAC9B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;AAC/B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IAC3B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,SAAS,CAAA;IACpC,IAAI,CAAC;QACH,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAQ,KAAqB,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACpG,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,OAAO,KAAoB,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;QAC5E,8DAA8D;QAC9D,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,WAAW,CAClB,UAA4B,EAC5B,IAAc,EACd,KAAa,EACb,OAAqC,EACrC,MAA+B;IAE/B,OAAO,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE;YACnE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,WAAW,EAAE,IAAI;SAClB,CAAC,CAAA;QACF,MAAM,MAAM,GAAkB,EAAE,CAAA;QAChC,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,QAAQ,GAAG,KAAK,CAAA;QACpB,IAAI,OAAO,GAAG,MAAM,EAAE,OAAO,KAAK,IAAI,CAAA;QACtC,IAAI,OAAO,GAAG,KAAK,CAAA;QACnB,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,GAAG,CAAC;YACpC,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,QAAQ,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA,CAAC,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC;YAC3E,CAAC,CAAC,SAAS,CAAA;QACb,MAAM,OAAO,GAAG,GAAS,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA,CAAC,CAAC,CAAA;QAC5D,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QAE1D,MAAM,MAAM,GAAG,CAAC,IAAY,EAAQ,EAAE;YACpC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;YAC9B,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAM;YAC/B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAClB,OAAO,CAAC,KAAK,CAAC,CAAA;QAChB,CAAC,CAAA;QAED,MAAM,MAAM,GAAG,CAAC,QAAuB,EAAQ,EAAE;YAC/C,IAAI,OAAO;gBAAE,OAAM;YACnB,OAAO,GAAG,IAAI,CAAA;YACd,IAAI,KAAK,KAAK,SAAS;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAA;YAC5C,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC7C,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;YACnC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAA;YAC3D,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;QAC1D,CAAC,CAAA;QAED,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;QAChC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;YACvC,MAAM,GAAG,KAAK,CAAC,IAAI,CAAA;YACnB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAA;QAC9C,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;QAChC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAA;QAC/C,CAAC,CAAC,CAAA;QACF,iFAAiF;QACjF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAY,EAAE,EAAE;YACjC,MAAM,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,CAAA;YACrD,MAAM,CAAC,IAAI,CAAC,CAAA;QACd,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAmB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QAExD,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAC3B,0EAA0E;QAC5E,CAAC,CAAC,CAAA;QACF,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACxB,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAA4B,EAC5B,MAAoB;IAEpB,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,UAAU,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,GAAE,CAAC,EAAE,MAAM,CAAC,CAAA;IAChF,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC;QACzD,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAA;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CACrB,UAA4B,EAC5B,IAAgB,EAChB,OAAqC,EACrC,MAAoB;IAEpB,OAAO,WAAW,CAAC,UAAU,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;AAC3E,CAAC"}
package/package.json ADDED
@@ -0,0 +1,98 @@
1
+ {
2
+ "name": "dsh-ab-ocr",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "default": "./lib/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "lib/**/*",
16
+ "python/ocr_worker.py",
17
+ "python/assemble.py",
18
+ "python/clean.py",
19
+ "python/layout.py",
20
+ "python/source.py",
21
+ "python/requirements.txt",
22
+ "python/README.md",
23
+ "scripts/setup.mjs",
24
+ "cordis.patch.yml",
25
+ "README.md",
26
+ "LICENSE",
27
+ "tsconfig.json",
28
+ "tsdown.config.ts"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json && tsdown",
32
+ "clean": "rimraf lib",
33
+ "setup": "node scripts/setup.mjs",
34
+ "typecheck": "tsc -p tsconfig.json --noEmit",
35
+ "test": "node --test",
36
+ "test:python": "python -m unittest discover -s python/tests -t python",
37
+ "lint": "publint && attw --pack . --profile esm-only",
38
+ "format": "prettier --write \"src/**/*.{ts,tsx,mjs}\" \"tests/**/*.mjs\" \"*.{json,yml,md}\"",
39
+ "verify": "npm run typecheck && npm run test && npm run build && npm run lint",
40
+ "prepublishOnly": "npm run verify",
41
+ "prepack": "npm run build",
42
+ "release": "npm publish --access public --no-git-checks"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ }
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
55
+ "directory": ".dsh/plugins/dsh-ab-ocr"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/deepseek-ai/deepseek-harness/issues"
59
+ },
60
+ "homepage": "https://github.com/deepseek-ai/deepseek-harness/tree/main/.dsh/plugins/dsh-ab-ocr",
61
+ "license": "MIT",
62
+ "keywords": [
63
+ "deepseek-harness",
64
+ "dsh-plugin",
65
+ "cordis-plugin",
66
+ "ocr",
67
+ "document-extraction",
68
+ "pdf",
69
+ "out-of-tree"
70
+ ],
71
+ "engines": {
72
+ "node": ">=18"
73
+ },
74
+ "dependencies": {
75
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
76
+ "@deepseek-ai/schemastery": "^3.18.4"
77
+ },
78
+ "peerDependencies": {
79
+ "@deepseek-ai/cordis": "*"
80
+ },
81
+ "devDependencies": {
82
+ "@deepseek-ai/cordis": "^4.0.4",
83
+ "@deepseek-ai/cordis-plugin-include": "^1.0.9",
84
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
85
+ "@deepseek-ai/dsh-fs": "^0.1.7-rc.1",
86
+ "@deepseek-ai/dsh-fs-sandbox": "^0.1.7-rc.1",
87
+ "@deepseek-ai/dsh-sandbox": "^0.1.7-rc.1",
88
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.7-rc.1",
89
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.1",
90
+ "@types/node": "^22.10.2",
91
+ "tsdown": "^0.22.2",
92
+ "typescript": "^6.0.3",
93
+ "rimraf": "^6.0.1",
94
+ "publint": "^0.2.0",
95
+ "@arethetypeswrong/cli": "^0.18.3",
96
+ "prettier": "^3.3.0"
97
+ }
98
+ }
@@ -0,0 +1,125 @@
1
+ # OCR worker
2
+
3
+ The Python half of `ocr`. The tool spawns it once per call, hands it a JSON job
4
+ spec on stdin, and reads newline-delimited JSON events back from stdout. It
5
+ recognizes and merges; the tool writes every file.
6
+
7
+ ## Files
8
+
9
+ | File | Role |
10
+ |---|---|
11
+ | `ocr_worker.py` | entry: reads the spec, owns the engine lifetime, reports each page, runs the merge |
12
+ | `source.py` | opens a PDF or a still image and renders one page on demand |
13
+ | `layout.py` | turns engine boxes into reading-order lines, finds columns, infers outline levels |
14
+ | `clean.py` | identifies folios and running heads, and joins the lines a wrap split |
15
+ | `assemble.py` | the merge pass: page records in, one Markdown document out |
16
+
17
+ ## Environment
18
+
19
+ The tool looks for a virtual environment at `python/.venv` beside this
20
+ directory and falls back to `python` on `PATH`. Override it with the
21
+ `pythonPath` configuration field.
22
+
23
+ Create the environment once. Installing into it needs permission to create
24
+ directories with a private mode, so run this outside a restricted shell:
25
+
26
+ ```sh
27
+ python -m venv python/.venv
28
+ python/.venv/Scripts/python -m pip install -r python/requirements.txt
29
+ ```
30
+
31
+ Confirm it with the worker's own check, which is what the tool runs before every
32
+ call:
33
+
34
+ ```sh
35
+ python/.venv/Scripts/python python/ocr_worker.py --self-test
36
+ ```
37
+
38
+ ## Protocol
39
+
40
+ stdin is one JSON object:
41
+
42
+ ```json
43
+ {
44
+ "engineLifetime": "perDocument",
45
+ "jobs": [
46
+ { "mode": "recognize", "id": "1", "input": "D:/docs/a.pdf", "pages": "1-5,8",
47
+ "dpi": 200, "maxPixels": 12000000, "textScore": 0.5, "detectColumns": true,
48
+ "maxPages": 2000, "merge": { "detectHeadings": true } },
49
+ { "mode": "assemble", "id": "2", "input": "D:/docs/a.pdf",
50
+ "record": { "pages": [ { "index": 5, "width": 595.0, "height": 842.0,
51
+ "lines": [ { "text": "...", "x0": 1, "y0": 2, "x1": 3, "y1": 4, "height": 12.0 } ] } ] },
52
+ "merge": { "levelOverrides": { "h2": 3 } } }
53
+ ]
54
+ }
55
+ ```
56
+
57
+ `pages` is a comma-separated list of single pages and inclusive ranges, counted
58
+ from one, as a person would write them in a print dialog; a range left open
59
+ after its dash runs to the last page. It resolves to the explicit ascending list
60
+ of page numbers to process, so `"1-5,8"` on a twelve-page document is
61
+ `[1, 2, 3, 4, 5, 8]`. A malformed piece, a page below one, an end before its
62
+ start, and a selection that names no page of the document are errors for that
63
+ job.
64
+
65
+ A `recognize` job renders and recognizes the document it names. An `assemble`
66
+ job opens no document and builds no engine: it rebuilds the pages from the page
67
+ records the caller kept and runs the merge alone.
68
+
69
+ `merge` carries the merge pass's keys, all optional and in camelCase:
70
+ `removePageNumbers`, `removeRunningHeads`, `runningHeadRatio`,
71
+ `runningHeadMinPages`, `detectHeadings`, `headingMinRatio`, `indentRatio`,
72
+ `paragraphGapRatio`, `outlineCandidateRatio`, `maxOutlineCandidates`, and
73
+ `levelOverrides`.
74
+
75
+ stdout is one JSON object per line:
76
+
77
+ | Event | Fields |
78
+ |---|---|
79
+ | `start` | `id`, `input`, `pages` (the explicit list), `totalPages` |
80
+ | `page` | `id`, `page`, `totalPages`, `lines`, `chars`, `text`, and `geometry` — the page record an assemble job rebuilds from |
81
+ | `released` | `id`, `reason` |
82
+ | `done` | `id`, `input`, `pages`, `totalPages`, `lines`, `headings`, `chars`, `droppedPageNumbers`, `droppedRunningHeads`, `joinedAcrossPages`, `recognized`, `seconds`, `markdown`, `outline`, `outlineTruncated` |
83
+ | `error` | `id`, `input`, `message` |
84
+ | `end` | `documents`, `failures` |
85
+
86
+ An assemble job emits no `page` events: its `pages` and `recognized` are the
87
+ number of pages in its record, and its `seconds` is the merge's own duration.
88
+
89
+ The worker reads only the document each recognize job names. It writes no file:
90
+ every artifact is written by the caller through the mounted filesystem
91
+ capability, so a deployment's file policy sees the write. Diagnostics go to
92
+ stderr.
93
+
94
+ ## Outline pass
95
+
96
+ `done` closes with `outline`, the lines whose outline level the heuristic may
97
+ have read wrongly, and with `outlineTruncated` when `maxOutlineCandidates` (400
98
+ by default) cut that list short. A caller that wants a model to check the
99
+ outline sends these candidates to the model and comes back with one `assemble`
100
+ job per document whose `merge` carries `levelOverrides`; nothing is recognized
101
+ twice.
102
+
103
+ A kept line becomes a candidate when any of these holds: the heuristic gives it
104
+ a level, it opens with a section number, it is at least `outlineCandidateRatio`
105
+ (1.05 by default) times the body glyph height, or it opens with a named section
106
+ such as an abstract or a reference list. A list item that carries no number
107
+ never does. Ids are `h1`, `h2`, … in document order, and each candidate reports
108
+ its `page`, its normalized `text`, the heuristic `level` (or `null`), its
109
+ glyph-height `ratio`, and its section `numbered` when it opens with one.
110
+
111
+ `levelOverrides` maps a candidate id to the heading level to use in place of the
112
+ heuristic one: 1 to 6, with anything above 6 clamped to 6. A value below 1, like
113
+ `null`, makes the line body text, which is how a candidate the heuristic wrongly
114
+ called a heading is demoted. A candidate the map does not mention keeps the
115
+ heuristic's own decision, and a value that is not an integer is an error for
116
+ that job.
117
+
118
+ ## Tests
119
+
120
+ The unit tests cover the pure layout, cleaning, merge, and job-protocol rules
121
+ and need no third-party package:
122
+
123
+ ```sh
124
+ python -m unittest discover -s python/tests -t python
125
+ ```