@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.
- package/dist/lib/derivers/pdf.d.ts.map +1 -1
- package/dist/lib/derivers/pdf.js +29 -10
- package/dist/lib/derivers/pdf.js.map +1 -1
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +9 -7
- package/dist/skill.js.map +1 -1
- package/package.json +6 -6
- package/src/derivers.integration.test.ts +90 -6
- package/src/lib/derivers/pdf.ts +47 -16
- package/src/skill.ts +11 -9
|
@@ -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;
|
|
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"}
|
package/dist/lib/derivers/pdf.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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+
|
|
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 (
|
|
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:
|
|
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":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../../src/lib/derivers/pdf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAU3D,MAAM,6BAA6B,GAAG,EAAE,CAAC;AAGzC,MAAM,aAAa,GAAG,EAAE,CAAC;AACzB,MAAM,OAAO,GAAG,KAAK,CAAC;AAEtB,MAAM,cAAc,GAAG,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAEvD,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAGhC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAmB,CAAC;AAO3C,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE;IACtC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACvC,IAAI,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC1B,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;QACpI,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC,CAAC;AAIF,MAAM,SAAS,GAAG,CAAC,MAAyB,EAAmB,EAAE,CAC7D,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IAC5B,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;QACrG,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACrC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC;aAAM,CAAC;YACJ,MAAM,CAAC,KAAK,CAAC,CAAC;QAClB,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEP,MAAM,cAAc,GAAG,KAAK,EAAE,OAAe,EAAE,UAAkB,EAAqB,EAAE;IACpF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC;IACxD,IAAI,CAAC;QACD,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,EAAE;YACrI,OAAO,EAAE,OAAO;SACnB,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;QACvF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,EAAE,CAAC;QACd,CAAC;QAED,MAAM,KAAK,GAAG,CAAC,MAAM,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACrF,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5F,CAAC;YAAS,CAAC;QACP,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,KAAwB,EAAU,EAAE,CACzD,KAAK,CAAC,MAAM,KAAK,CAAC;IACd,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,WAAW,KAAK,GAAG,CAAC,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAG9H,MAAM,cAAc,GAAG,CAAC,OAAe,EAAE,OAA2B,EAAY,EAAE,CAC9E,OAAO,KAAK,CAAC;IACT,CAAC,CAAC,EAAE;IACJ,CAAC,CAAC;QACI,WAAW,OAAO,+CAA+C,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,OAAO,IAAI,EAAE;KACvJ,CAAC;AAEZ,MAAM,gBAAgB,GAAG,CAAC,KAAwB,EAAyC,EAAE;IACzF,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,EAAE,QAAQ,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;AACxF,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAY;IAI/B,IAAI,IAAI;QACJ,OAAO,YAAY,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC;IAChD,CAAC;IACD,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,KAAK,EAAE,OAAO,EAAuB,EAAE;QAC3C,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC5E,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,EAAE,IAA2C,CAAC;QAC/D,MAAM,KAAK,GAAG,OAAO,IAAI,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACtG,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACzE,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACrE,IAAI,UAAU,GAAG,6BAA6B,GAAG,UAAU,EAAE,CAAC;YAC1D,MAAM,MAAM,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;YAC3C,IAAI,CAAC,YAAY,EAAE,EAAE,CAAC;gBAClB,OAAO;oBACH,QAAQ,EAAE,EAAE;oBACZ,KAAK;oBACL,KAAK,EAAE,CAAC,+BAA+B,UAAU,QAAQ,MAAM,0CAA0C,CAAC;iBAC7G,CAAC;YACN,CAAC;YACD,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;YAC7D,MAAM,KAAK,GAAG;gBACV,kDAAkD,UAAU,CAAC,MAAM,OAAO,UAAU,QAAQ,MAAM,gEAAgE;aACrK,CAAC;YACF,IAAI,UAAU,GAAG,aAAa,EAAE,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC,gBAAgB,aAAa,6FAA6F,CAAC,CAAC;YAC3I,CAAC;YACD,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;YACnE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC;QAC/D,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;IACjD,CAAC;CACJ,CAAC"}
|
package/dist/skill.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
5
|
-
standalone: "a scan is OCR'd when `
|
|
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
|
|
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
|
|
11
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
73
|
-
"@intentic/base": "1.
|
|
74
|
-
"@intentic/constants": "1.
|
|
75
|
-
"@intentic/webq": "1.
|
|
76
|
-
"@intentic/workspace-ignore": "1.
|
|
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
|
|
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
|
-
|
|
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), `
|
|
120
|
-
|
|
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+
|
|
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");
|
package/src/lib/derivers/pdf.ts
CHANGED
|
@@ -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
|
|
14
|
-
//
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
// that it's here.
|
|
68
|
-
//
|
|
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+
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
17
|
-
standalone: "a scan is OCR'd when `
|
|
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
|
|
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
|
|
24
|
-
|
|
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
|