@intentic/fileq 1.321.0 → 1.323.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.
@@ -1 +1 @@
1
- {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../../../src/lib/derivers/pdf.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,OAAO,EAAE,MAAM,cAAc,CAAC;AAmBxD,eAAO,MAAM,YAAY,QAAO,OAAoD,CAAC;AAsCrF,eAAO,MAAM,UAAU,EAAE,OAqCxB,CAAC"}
1
+ {"version":3,"file":"pdf.d.ts","sourceRoot":"","sources":["../../../src/lib/derivers/pdf.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,OAAO,EAAE,MAAM,cAAc,CAAC;AA6BxD,eAAO,MAAM,YAAY,QAAO,OAQ/B,CAAC;AAmDF,eAAO,MAAM,UAAU,EAAE,OAqCxB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawnSync } from "node:child_process";
2
2
  import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -9,8 +9,28 @@ import { stripRunningFurniture } from "./pdf-furniture.js";
9
9
  const SCAN_THRESHOLD_CHARS_PER_PAGE = 24;
10
10
  const MAX_OCR_PAGES = 20;
11
11
  const OCR_DPI = "200";
12
+ const OCR_TIMEOUT_MS = 60_000 + MAX_OCR_PAGES * 15_000;
12
13
  const run = promisify(execFile);
13
- export const ocrAvailable = () => onPath("tesseract") && onPath("pdftoppm");
14
+ const checked = new Map();
15
+ export const ocrAvailable = () => {
16
+ const path = process.env["PATH"] ?? "";
17
+ let available = checked.get(path);
18
+ if (available === undefined) {
19
+ available = onPath("pdftoppm") && onPath("ocr") && spawnSync("ocr", ["--check"], { stdio: "ignore", timeout: 10_000 }).status === 0;
20
+ checked.set(path, available);
21
+ }
22
+ return available;
23
+ };
24
+ const readPages = (images) => new Promise((resolve, reject) => {
25
+ execFile("ocr", [...images], { timeout: OCR_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
26
+ if (error === null || error.code === 1) {
27
+ resolve(stdout);
28
+ }
29
+ else {
30
+ reject(error);
31
+ }
32
+ });
33
+ });
14
34
  const recognisePages = async (absPath, totalPages) => {
15
35
  const dir = await mkdtemp(join(tmpdir(), "fileq-ocr-"));
16
36
  try {
@@ -18,12 +38,11 @@ const recognisePages = async (absPath, totalPages) => {
18
38
  timeout: 120_000,
19
39
  });
20
40
  const images = (await readdir(dir)).filter((name) => name.endsWith(".png")).toSorted();
21
- const pages = [];
22
- for (const image of images) {
23
- const { stdout } = await run("tesseract", [join(dir, image), "stdout", "-l", "eng"], { timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
24
- pages.push(stdout.replaceAll(/[ \t]+/g, " ").trim());
41
+ if (images.length === 0) {
42
+ return [];
25
43
  }
26
- return pages;
44
+ const pages = (await readPages(images.map((image) => join(dir, image)))).split("\f");
45
+ return images.map((_, index) => (pages[index] ?? "").replaceAll(/[ \t]+/g, " ").trim());
27
46
  }
28
47
  finally {
29
48
  await rm(dir, { recursive: true, force: true });
@@ -43,7 +62,7 @@ const withoutFurniture = (pages) => {
43
62
  };
44
63
  export const pdfDeriver = {
45
64
  get name() {
46
- return ocrAvailable() ? "pdf+ocr" : "pdf";
65
+ return ocrAvailable() ? "pdf+ppocr" : "pdf";
47
66
  },
48
67
  version: 2,
49
68
  derive: async (absPath) => {
@@ -65,10 +84,10 @@ export const pdfDeriver = {
65
84
  }
66
85
  const recognised = await recognisePages(absPath, totalPages);
67
86
  const notes = [
68
- `scanned: text recognised by OCR (tesseract) on ${recognised.length} of ${totalPages} page${plural} — recognised, not exact; check figures against the page image`,
87
+ `scanned: text recognised by OCR (PaddleOCR) on ${recognised.length} of ${totalPages} page${plural} — recognised, not exact; check figures against the page image`,
69
88
  ];
70
89
  if (totalPages > MAX_OCR_PAGES) {
71
- notes.push(`OCR stops at ${MAX_OCR_PAGES} pages: run tesseract over the rest yourself if they matter`);
90
+ notes.push(`OCR stops at ${MAX_OCR_PAGES} pages: draw the rest with pdftoppm and read them with \`ocr <page.png>...\` if they matter`);
72
91
  }
73
92
  const { markdown, notes: stripped } = withoutFurniture(recognised);
74
93
  return { markdown, title, notes: [...notes, ...stripped] };
@@ -1 +1 @@
1
- {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../../src/lib/derivers/pdf.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../../src/lib/derivers/pdf.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAE3B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,YAAY,CAAC;CAC3C;AAmCD,eAAO,MAAM,UAAU,aAAc,cAAc,KAAG,MAsErD,CAAC"}
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAE3B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,YAAY,CAAC;CAC3C;AAqCD,eAAO,MAAM,UAAU,aAAc,cAAc,KAAG,MAsErD,CAAC"}
package/dist/skill.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { DERIVED_DIR } from "./lib/sidecar.js";
2
2
  import { AUTHORING_RULES } from "./skill-authoring.js";
3
3
  const OCR_FORMAT = {
4
- sandbox: "a scan is OCR'd when the image carries tesseract",
5
- standalone: "a scan is OCR'd when `tesseract` is on PATH",
4
+ sandbox: "a scan is OCR'd on this machine by PaddleOCR when the image carries its models",
5
+ standalone: "a scan is OCR'd when an `ocr` command is on PATH",
6
6
  };
7
7
  const OCR_REFUSAL = {
8
- sandbox: `- A scanned PDF on an image without tesseract answers "no usable text layer … OCR is not part of this tier"
8
+ sandbox: `- A scanned PDF on an image without the OCR models answers "no usable text layer … OCR is not part of this
9
+ tier" rather than an empty page; images say "no visual description". Treat those notes as "not generated",
10
+ never as "nothing there". The privacy pack brings the models, and an image carrying it turns the first into
11
+ OCR; \`ocr <image>...\` reads a screenshot's text the same way, without sending it anywhere.`,
12
+ standalone: `- A scanned PDF with no \`ocr\` on PATH answers "no usable text layer … OCR is not part of this tier"
9
13
  rather than an empty page; images say "no visual description". Treat those notes as "not generated",
10
- never as "nothing there". An extension that ships tesseract (office) turns the first into OCR.`,
11
- standalone: `- A scanned PDF with no \`tesseract\` on PATH answers "no usable text layer … OCR is not part of this tier"
12
- rather than an empty page; images say "no visual description". Treat those notes as "not generated",
13
- never as "nothing there". Installing tesseract turns the first into OCR.`,
14
+ never as "nothing there". An \`ocr\` command that prints each image's text, a form feed between images (the
15
+ Intentic sandbox's PaddleOCR reader), turns the first into OCR.`,
14
16
  };
15
17
  const RENDER_TOOLS = {
16
18
  sandbox: `It needs LibreOffice (\`soffice\`) for office formats and \`pdftoppm\` for every format, both in the opt-in
package/dist/skill.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAavD,MAAM,UAAU,GAA2C;IACvD,OAAO,EAAE,kDAAkD;IAC3D,UAAU,EAAE,6CAA6C;CAC5D,CAAC;AAEF,MAAM,WAAW,GAA2C;IACxD,OAAO,EAAE;;iGAEoF;IAC7F,UAAU,EAAE;;2EAE2D;CAC1E,CAAC;AAKF,MAAM,YAAY,GAAG;IACjB,OAAO,EAAE;;4GAE+F;IACxG,UAAU,EAAE;2GAC2F;CACzD,CAAC;AAGnD,MAAM,QAAQ,GAAG;;;;;CAKhB,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAE,IAAI,EAAkB,EAAU,EAAE,CAAC;;;;;;;;;;;;;;;qBAe3C,UAAU,CAAC,IAAI,CAAC;;;;;;;;;;;;+EAY0C,WAAW;;;;EAIxF,WAAW,CAAC,IAAI,CAAC;;;;EAIjB,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;KAW/B,YAAY,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC;;;;EAI/C,eAAe;;;;;;;;;;;;;;;;;;;;CAoBhB,CAAC"}
1
+ {"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAavD,MAAM,UAAU,GAA2C;IACvD,OAAO,EAAE,gFAAgF;IACzF,UAAU,EAAE,kDAAkD;CACjE,CAAC;AAEF,MAAM,WAAW,GAA2C;IACxD,OAAO,EAAE;;;+FAGkF;IAC3F,UAAU,EAAE;;;kEAGkD;CACjE,CAAC;AAKF,MAAM,YAAY,GAAG;IACjB,OAAO,EAAE;;4GAE+F;IACxG,UAAU,EAAE;2GAC2F;CACzD,CAAC;AAGnD,MAAM,QAAQ,GAAG;;;;;CAKhB,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAE,IAAI,EAAkB,EAAU,EAAE,CAAC;;;;;;;;;;;;;;;qBAe3C,UAAU,CAAC,IAAI,CAAC;;;;;;;;;;;;+EAY0C,WAAW;;;;EAIxF,WAAW,CAAC,IAAI,CAAC;;;;EAIjB,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;;;;;;;;;;;KAW/B,YAAY,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC;;;;EAI/C,eAAe;;;;;;;;;;;;;;;;;;;;CAoBhB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/fileq",
3
- "version": "1.321.0",
3
+ "version": "1.323.0",
4
4
  "description": "fileq, agent-native file reading: any binary workspace file (docx, xlsx, pptx, pdf, images, audio, archives) as clean budgeted markdown, cached by content so a repeat read is instant",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -69,11 +69,11 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@intentic/agent-cli": "1.321.0",
73
- "@intentic/base": "1.321.0",
74
- "@intentic/constants": "1.321.0",
75
- "@intentic/webq": "1.321.0",
76
- "@intentic/workspace-ignore": "1.321.0",
72
+ "@intentic/agent-cli": "1.323.0",
73
+ "@intentic/base": "1.323.0",
74
+ "@intentic/constants": "1.323.0",
75
+ "@intentic/webq": "1.323.0",
76
+ "@intentic/workspace-ignore": "1.323.0",
77
77
  "@stricli/core": "1.3.0",
78
78
  "exceljs": "4.4.0",
79
79
  "exifr": "7.1.3",
@@ -1,9 +1,10 @@
1
1
  /* Every deriver against a real file its real parser accepts, in a temp tree. */
2
- import { execFileSync } from "node:child_process";
2
+ import { execFileSync, spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { requires } from "@intentic/testing/requires";
7
+ import { onPath } from "./lib/tools.js";
7
8
  import { archiveDeriver } from "./lib/derivers/archive.js";
8
9
  import { docxDeriver } from "./lib/derivers/docx.js";
9
10
  import { epubDeriver } from "./lib/derivers/epub.js";
@@ -37,6 +38,9 @@ import {
37
38
  zipBytes,
38
39
  } from "./testing.js";
39
40
 
41
+ // Whether python3 can draw a scan: the OCR fixtures are Pillow's pages.
42
+ const pillow = (): boolean => spawnSync("python3", ["-c", "import PIL"], { stdio: "ignore" }).status === 0;
43
+
40
44
  let root: string;
41
45
  const fixture = (name: string, bytes: Uint8Array | string): string => {
42
46
  const path = join(root, name);
@@ -100,7 +104,7 @@ describe("pdf", () => {
100
104
  expect(doc.notes).toEqual([]);
101
105
  });
102
106
 
103
- test("a page with no usable text says scan, not silence (no tesseract on PATH)", async () => {
107
+ test("a page with no usable text says scan, not silence (no ocr on PATH)", async () => {
104
108
  const path = fixture("scan.pdf", pdfBytes("x"));
105
109
  const previousPath = process.env["PATH"];
106
110
  process.env["PATH"] = "/nonexistent";
@@ -114,10 +118,90 @@ describe("pdf", () => {
114
118
  }
115
119
  });
116
120
 
117
- /* The OCR tier, exercised for real where the image carries tesseract + poppler (an extension's layer, and ci-base). */
121
+ // A stand-in `ocr` on PATH, speaking the real one's protocol (src/ocr/cli.ts in the sandbox): `--check` answers
122
+ // whether it can read, and a run prints each image's text with a form feed line between images, leaving an image
123
+ // it could not read empty and exiting 1.
124
+ const standIn = (check: number): string => {
125
+ const bin = mkdtempSync(join(tmpdir(), "fileq-ocr-bin-"));
126
+ writeFileSync(
127
+ join(bin, "ocr"),
128
+ [
129
+ "#!/bin/sh",
130
+ `if [ "$1" = "--check" ]; then exit ${check}; fi`,
131
+ "status=0",
132
+ "first=1",
133
+ 'for image in "$@"; do',
134
+ " [ $first = 1 ] || printf '\\f\\n'",
135
+ " first=0",
136
+ ' case "$image" in',
137
+ " *page-1.png) status=1 ;;",
138
+ ' *) printf \'TOTAL DUE ON %s\\n\' "$(basename "$image" .png)" ;;',
139
+ " esac",
140
+ "done",
141
+ "exit $status",
142
+ "",
143
+ ].join("\n"),
144
+ { mode: 0o755 },
145
+ );
146
+ return bin;
147
+ };
148
+ const withPath = async <T>(bin: string, body: () => Promise<T>): Promise<T> => {
149
+ const previousPath = process.env["PATH"];
150
+ process.env["PATH"] = `${bin}:${previousPath ?? ""}`;
151
+ try {
152
+ return await body();
153
+ } finally {
154
+ process.env["PATH"] = previousPath;
155
+ rmSync(bin, { recursive: true, force: true });
156
+ }
157
+ };
158
+ // A two-page scan, drawn by Pillow: pages of pixels, no text layer.
159
+ const scan = (name: string): string => {
160
+ const path = join(root, name);
161
+ execFileSync("python3", [
162
+ "-c",
163
+ [
164
+ "import sys",
165
+ "from PIL import Image, ImageDraw",
166
+ "pages = [Image.new('RGB', (850, 1100), 'white') for _ in range(2)]",
167
+ "for page in pages: ImageDraw.Draw(page).rectangle((100, 100, 700, 160), fill='black')",
168
+ "pages[0].save(sys.argv[1], save_all=True, append_images=pages[1:], resolution=100.0)",
169
+ ].join("\n"),
170
+ path,
171
+ ]);
172
+ return path;
173
+ };
174
+
175
+ /* fileq's half of the OCR tier, wherever poppler and Pillow are (ci-base carries both): the pages drawn and handed
176
+ to `ocr` in one run, each page keeping its own text, an unreadable page left empty in its place. */
177
+ const poppler = requires(onPath("pdftoppm") && pillow(), "pdftoppm on PATH and python3 with Pillow");
178
+ test.skipIf(!poppler.runs)(poppler.title("a scan's pages go to ocr in one run, and each keeps its own text"), async () => {
179
+ const path = scan("two-pages.pdf");
180
+ await withPath(standIn(0), async () => {
181
+ expect(pdfDeriver.name).toBe("pdf+ppocr");
182
+ const doc = await pdfDeriver.derive(path);
183
+ expect(doc.markdown).toBe("## Page 1\n\n(no text on this page)\n\n## Page 2\n\nTOTAL DUE ON page-2");
184
+ expect(doc.notes.join(" ")).toContain("recognised by OCR (PaddleOCR) on 2 of 2 pages");
185
+ });
186
+ });
187
+
188
+ test.skipIf(!poppler.runs)(poppler.title("an ocr that cannot read (no models) leaves the scan unread, and says so"), async () => {
189
+ const path = scan("unread.pdf");
190
+ await withPath(standIn(2), async () => {
191
+ expect(pdfDeriver.name).toBe("pdf");
192
+ const doc = await pdfDeriver.derive(path);
193
+ expect(doc.markdown).toBe("");
194
+ expect(doc.notes.join(" ")).toContain("OCR is not part of this tier");
195
+ });
196
+ });
197
+
198
+ /* The OCR tier for real, where the image carries the reader: the sandbox's `ocr` with the privacy pack's models. */
118
199
  const DEJAVU = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
119
- const ocr = requires(ocrAvailable() && existsSync(DEJAVU), `tesseract and pdftoppm on PATH, and the DejaVu font at ${DEJAVU}`);
120
- test.skipIf(!ocr.runs)(ocr.title("a scan is recognised by tesseract when the image carries it, and says so"), async () => {
200
+ const ocr = requires(ocrAvailable() && existsSync(DEJAVU), `an ocr command that can read (the privacy pack's models), and the DejaVu font at ${DEJAVU}`, {
201
+ absentOnCi:
202
+ "ocr is the sandbox image's own command (_sandbox/sandbox/src/ocr/cli.ts); CI proves that reader against the same models in the sandbox's readers suite",
203
+ });
204
+ test.skipIf(!ocr.runs)(ocr.title("a scan is recognised by PaddleOCR when the image carries it, and says so"), async () => {
121
205
  const path = join(root, "receipt.pdf");
122
206
  execFileSync("python3", [
123
207
  "-c",
@@ -131,7 +215,7 @@ describe("pdf", () => {
131
215
  path,
132
216
  DEJAVU,
133
217
  ]);
134
- expect(pdfDeriver.name).toBe("pdf+ocr");
218
+ expect(pdfDeriver.name).toBe("pdf+ppocr");
135
219
  const doc = await pdfDeriver.derive(path);
136
220
  expect(doc.markdown).toMatch(/TOTAL DUE 1845/);
137
221
  expect(doc.notes.join(" ")).toContain("recognised, not exact");
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawnSync } from "node:child_process";
2
2
  import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -10,8 +10,9 @@ import { stripRunningFurniture } from "./pdf-furniture.js";
10
10
 
11
11
  // PDF text layer, per page, via unpdf's serverless pdf.js build (no worker, canvas or DOM shims needed for text
12
12
  // extraction).
13
- // A scan with no text layer is rasterised and OCR'd when tesseract is on the image, noted as recognised rather than
14
- // exact; otherwise the sidecar says so instead of an empty page.
13
+ // A scan with no text layer is rasterised and OCR'd when an `ocr` command can read (the Intentic sandbox's own text
14
+ // reader, PaddleOCR's PP-OCRv6 run on this machine, whose models its privacy image pack brings), noted as recognised
15
+ // rather than exact; otherwise the sidecar says so instead of an empty page.
15
16
  // Either way, the deterministic tier never guesses at pixels.
16
17
 
17
18
  // Below this many chars per page, the text layer is furniture, not content: a scan with a vestigial layer.
@@ -20,11 +21,41 @@ const SCAN_THRESHOLD_CHARS_PER_PAGE = 24;
20
21
  // OCR costs seconds per page, so a long scan gets its first pages and a note.
21
22
  const MAX_OCR_PAGES = 20;
22
23
  const OCR_DPI = "200";
24
+ // The reader loads its models once per run and then takes a second or few per page.
25
+ const OCR_TIMEOUT_MS = 60_000 + MAX_OCR_PAGES * 15_000;
23
26
 
24
27
  const run = promisify(execFile);
25
28
 
26
- /** Whether this image can recognise a scan: both the rasteriser and the recogniser on PATH. */
27
- export const ocrAvailable = (): boolean => onPath("tesseract") && onPath("pdftoppm");
29
+ // Asked once per PATH: the answer is a fact of the image, and a stamp is read on every derive.
30
+ const checked = new Map<string, boolean>();
31
+
32
+ /**
33
+ * Whether this machine can recognise a scan: the rasteriser on PATH, and an `ocr` that says it can read (`ocr --check`
34
+ * exits 0). The sandbox image carries the command on every box and its models only with the privacy pack, so being on
35
+ * PATH alone proves nothing.
36
+ */
37
+ export const ocrAvailable = (): boolean => {
38
+ const path = process.env["PATH"] ?? "";
39
+ let available = checked.get(path);
40
+ if (available === undefined) {
41
+ available = onPath("pdftoppm") && onPath("ocr") && spawnSync("ocr", ["--check"], { stdio: "ignore", timeout: 10_000 }).status === 0;
42
+ checked.set(path, available);
43
+ }
44
+ return available;
45
+ };
46
+
47
+ // What `ocr` printed for its images, one page per image: it writes a form feed line between images, and an image it
48
+ // could not read leaves its page empty (exit 1) rather than shifting the ones after it.
49
+ const readPages = (images: readonly string[]): Promise<string> =>
50
+ new Promise((resolve, reject) => {
51
+ execFile("ocr", [...images], { timeout: OCR_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {
52
+ if (error === null || error.code === 1) {
53
+ resolve(stdout);
54
+ } else {
55
+ reject(error);
56
+ }
57
+ });
58
+ });
28
59
 
29
60
  const recognisePages = async (absPath: string, totalPages: number): Promise<string[]> => {
30
61
  const dir = await mkdtemp(join(tmpdir(), "fileq-ocr-"));
@@ -33,12 +64,12 @@ const recognisePages = async (absPath: string, totalPages: number): Promise<stri
33
64
  timeout: 120_000,
34
65
  });
35
66
  const images = (await readdir(dir)).filter((name) => name.endsWith(".png")).toSorted();
36
- const pages: string[] = [];
37
- for (const image of images) {
38
- const { stdout } = await run("tesseract", [join(dir, image), "stdout", "-l", "eng"], { timeout: 120_000, maxBuffer: 16 * 1024 * 1024 });
39
- pages.push(stdout.replaceAll(/[ \t]+/g, " ").trim());
67
+ if (images.length === 0) {
68
+ return [];
40
69
  }
41
- return pages;
70
+ // One run for every page, so the models load once.
71
+ const pages = (await readPages(images.map((image) => join(dir, image)))).split("\f");
72
+ return images.map((_, index) => (pages[index] ?? "").replaceAll(/[ \t]+/g, " ").trim());
42
73
  } finally {
43
74
  await rm(dir, { recursive: true, force: true });
44
75
  }
@@ -63,11 +94,11 @@ const withoutFurniture = (pages: readonly string[]): { markdown: string; notes:
63
94
  };
64
95
 
65
96
  export const pdfDeriver: Deriver = {
66
- // The stamp names OCR capability, since a sidecar written before tesseract was available must read as stale now
67
- // that it's here.
68
- // A static name would leave a pre-OCR sidecar looking current forever.
97
+ // The stamp names OCR capability, since a sidecar written before the reader was available must read as stale now
98
+ // that it's here. A static name would leave a pre-OCR sidecar looking current forever. It names the reader too:
99
+ // `pdf+ocr` was tesseract's, whose pages PaddleOCR reads again.
69
100
  get name(): string {
70
- return ocrAvailable() ? "pdf+ocr" : "pdf";
101
+ return ocrAvailable() ? "pdf+ppocr" : "pdf";
71
102
  },
72
103
  version: 2,
73
104
  derive: async (absPath): Promise<DerivedDoc> => {
@@ -89,10 +120,10 @@ export const pdfDeriver: Deriver = {
89
120
  }
90
121
  const recognised = await recognisePages(absPath, totalPages);
91
122
  const notes = [
92
- `scanned: text recognised by OCR (tesseract) on ${recognised.length} of ${totalPages} page${plural} — recognised, not exact; check figures against the page image`,
123
+ `scanned: text recognised by OCR (PaddleOCR) on ${recognised.length} of ${totalPages} page${plural} — recognised, not exact; check figures against the page image`,
93
124
  ];
94
125
  if (totalPages > MAX_OCR_PAGES) {
95
- notes.push(`OCR stops at ${MAX_OCR_PAGES} pages: run tesseract over the rest yourself if they matter`);
126
+ notes.push(`OCR stops at ${MAX_OCR_PAGES} pages: draw the rest with pdftoppm and read them with \`ocr <page.png>...\` if they matter`);
96
127
  }
97
128
  const { markdown, notes: stripped } = withoutFurniture(recognised);
98
129
  return { markdown, title, notes: [...notes, ...stripped] };
package/src/skill.ts CHANGED
@@ -4,8 +4,8 @@ import { AUTHORING_RULES } from "./skill-authoring.js";
4
4
  // fileq's own teaching: the SKILL.md every agent that has fileq on its PATH is given. It lives with the CLI it describes
5
5
  // so a new flag and its sentence land together, and so the two hosts that ship it cannot drift: the sandbox writes it
6
6
  // into the workspace's skills (the daemon's settings/skills.ts), the Claude Code plugin ships it as its fileq skill.
7
- // Only what the host itself provides differs: the sandbox image carries tesseract through an extension and wires fileq
8
- // as git's textconv; a standalone install has neither until its owner adds them.
7
+ // Only what the host itself provides differs: the sandbox image carries the `ocr` command (PaddleOCR, its models in the
8
+ // privacy image pack) and wires fileq as git's textconv; a standalone install has neither until its owner adds them.
9
9
 
10
10
  export interface FileqSkillHost {
11
11
  // "sandbox": the Intentic sandbox image. "standalone": fileq on anyone's machine (the Claude Code plugin, npm).
@@ -13,17 +13,19 @@ export interface FileqSkillHost {
13
13
  }
14
14
 
15
15
  const OCR_FORMAT: Record<FileqSkillHost["host"], string> = {
16
- sandbox: "a scan is OCR'd when the image carries tesseract",
17
- standalone: "a scan is OCR'd when `tesseract` is on PATH",
16
+ sandbox: "a scan is OCR'd on this machine by PaddleOCR when the image carries its models",
17
+ standalone: "a scan is OCR'd when an `ocr` command is on PATH",
18
18
  };
19
19
 
20
20
  const OCR_REFUSAL: Record<FileqSkillHost["host"], string> = {
21
- sandbox: `- A scanned PDF on an image without tesseract answers "no usable text layer … OCR is not part of this tier"
21
+ sandbox: `- A scanned PDF on an image without the OCR models answers "no usable text layer … OCR is not part of this
22
+ tier" rather than an empty page; images say "no visual description". Treat those notes as "not generated",
23
+ never as "nothing there". The privacy pack brings the models, and an image carrying it turns the first into
24
+ OCR; \`ocr <image>...\` reads a screenshot's text the same way, without sending it anywhere.`,
25
+ standalone: `- A scanned PDF with no \`ocr\` on PATH answers "no usable text layer … OCR is not part of this tier"
22
26
  rather than an empty page; images say "no visual description". Treat those notes as "not generated",
23
- never as "nothing there". An extension that ships tesseract (office) turns the first into OCR.`,
24
- standalone: `- A scanned PDF with no \`tesseract\` on PATH answers "no usable text layer … OCR is not part of this tier"
25
- rather than an empty page; images say "no visual description". Treat those notes as "not generated",
26
- never as "nothing there". Installing tesseract turns the first into OCR.`,
27
+ never as "nothing there". An \`ocr\` command that prints each image's text, a form feed between images (the
28
+ Intentic sandbox's PaddleOCR reader), turns the first into OCR.`,
27
29
  };
28
30
 
29
31
  // `fileq render` shells out to LibreOffice and a PDF rasterizer. The sandbox image does not bake them (LibreOffice is