@hraness/kb 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../clip/persist.js";
|
|
2
|
+
import { sanitizeTerminalText } from "../clip/terminal.js";
|
|
3
|
+
import { yamlString } from "../clip/lib.js";
|
|
4
|
+
import type {
|
|
5
|
+
PdfCaptureStatus,
|
|
6
|
+
PdfDocumentMetadata,
|
|
7
|
+
PdfImageCandidate,
|
|
8
|
+
PdfImageSemanticMetadata,
|
|
9
|
+
} from "./model.js";
|
|
10
|
+
import type { PdfLayoutBlock } from "./layout.js";
|
|
11
|
+
import { pdfImageAssetPath } from "./persist.js";
|
|
12
|
+
|
|
13
|
+
export type ResolvedPdfImage = {
|
|
14
|
+
readonly image: PdfImageCandidate;
|
|
15
|
+
readonly kind: "text" | "mixed" | "visual";
|
|
16
|
+
readonly method: "agent" | "manual" | "tesseract" | "unclassified";
|
|
17
|
+
readonly markdown: string;
|
|
18
|
+
readonly alt: string | null;
|
|
19
|
+
readonly confidence: number | null;
|
|
20
|
+
readonly wordCount: number;
|
|
21
|
+
readonly metadata: PdfImageSemanticMetadata | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function escapeInline(value: string): string {
|
|
25
|
+
return sanitizeTerminalText(value)
|
|
26
|
+
.replace(/\\/gu, "\\\\")
|
|
27
|
+
.replace(/([`*_[\]{}<>#])/gu, "\\$1")
|
|
28
|
+
.replace(/\s+/gu, " ")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function metadataLine(metadata: PdfImageSemanticMetadata | null): string | null {
|
|
33
|
+
if (metadata === null) return null;
|
|
34
|
+
const values = [
|
|
35
|
+
metadata.platform === undefined ? null : `platform: ${metadata.platform}`,
|
|
36
|
+
metadata.contentType === undefined ? null : `type: ${metadata.contentType}`,
|
|
37
|
+
metadata.channel === undefined ? null : `channel: ${metadata.channel}`,
|
|
38
|
+
metadata.author === undefined ? null : `author: ${metadata.author}`,
|
|
39
|
+
metadata.timestamp === undefined ? null : `timestamp: ${metadata.timestamp}`,
|
|
40
|
+
metadata.participants === undefined ? null : `participants: ${metadata.participants.join(", ")}`,
|
|
41
|
+
].filter((value): value is string => value !== null);
|
|
42
|
+
return values.length === 0 ? null : `*Image metadata — ${escapeInline(values.join("; "))}*`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function imageMarkdown(image: ResolvedPdfImage): string {
|
|
46
|
+
const assetPath = pdfImageAssetPath(image.image);
|
|
47
|
+
const semantic = image.metadata?.contentType ?? image.metadata?.platform;
|
|
48
|
+
const fallbackAlt = semantic === undefined
|
|
49
|
+
? `PDF image from page ${image.image.page}`
|
|
50
|
+
: `${semantic} from page ${image.image.page}`;
|
|
51
|
+
const alt = escapeInline(image.alt ?? fallbackAlt);
|
|
52
|
+
const lines = [``];
|
|
53
|
+
const metadata = metadataLine(image.metadata);
|
|
54
|
+
if (metadata !== null) lines.push("", metadata);
|
|
55
|
+
if (image.kind === "text" || image.kind === "mixed") {
|
|
56
|
+
const heading = image.metadata?.platform === undefined
|
|
57
|
+
? "Text visible in image"
|
|
58
|
+
: `Text visible in ${escapeInline(image.metadata.platform)} image`;
|
|
59
|
+
lines.push("", `#### ${heading}`, "", image.markdown.trim());
|
|
60
|
+
}
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function markdownContent(
|
|
65
|
+
blocks: readonly PdfLayoutBlock[],
|
|
66
|
+
images: ReadonlyMap<string, ResolvedPdfImage>,
|
|
67
|
+
): { readonly content: string; readonly headingCount: number; readonly textBlockCount: number } {
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
let currentPage: number | null = null;
|
|
70
|
+
let headingCount = 0;
|
|
71
|
+
let textBlockCount = 0;
|
|
72
|
+
for (const block of blocks) {
|
|
73
|
+
if (currentPage !== block.page) {
|
|
74
|
+
if (lines.length > 0) lines.push("");
|
|
75
|
+
lines.push(`<!-- pdf-page: ${block.page} -->`, "");
|
|
76
|
+
currentPage = block.page;
|
|
77
|
+
}
|
|
78
|
+
if (block.kind === "image") {
|
|
79
|
+
const resolved = images.get(block.image.id);
|
|
80
|
+
if (resolved === undefined) continue;
|
|
81
|
+
lines.push(imageMarkdown(resolved), "");
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (block.kind === "heading") {
|
|
85
|
+
headingCount += 1;
|
|
86
|
+
lines.push(`${"#".repeat(Math.max(2, Math.min(6, block.level)))} ${escapeInline(block.text)}`, "");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
textBlockCount += 1;
|
|
90
|
+
if (block.kind === "list-item") lines.push(`- ${escapeInline(block.text)}`, "");
|
|
91
|
+
else lines.push(escapeInline(block.text), "");
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
content: lines.join("\n").trim(),
|
|
95
|
+
headingCount,
|
|
96
|
+
textBlockCount,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type BuildPdfMarkdownOptions = {
|
|
101
|
+
readonly slug: string;
|
|
102
|
+
readonly originalFilename: string;
|
|
103
|
+
readonly sourceSha256: string;
|
|
104
|
+
readonly sourceUrl?: string;
|
|
105
|
+
readonly capturedDate: string;
|
|
106
|
+
readonly status: PdfCaptureStatus;
|
|
107
|
+
readonly metadata: PdfDocumentMetadata;
|
|
108
|
+
readonly blocks: readonly PdfLayoutBlock[];
|
|
109
|
+
readonly images: ReadonlyMap<string, ResolvedPdfImage>;
|
|
110
|
+
readonly embeddedPlatforms: readonly string[];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** Build one deterministic PDF note; only capturedDate varies between reruns. */
|
|
114
|
+
export function buildPdfMarkdown(options: BuildPdfMarkdownOptions): {
|
|
115
|
+
readonly markdown: string;
|
|
116
|
+
readonly headingCount: number;
|
|
117
|
+
readonly textBlockCount: number;
|
|
118
|
+
} {
|
|
119
|
+
const filenameTitle = options.originalFilename.replace(/\.pdf$/iu, "").trim();
|
|
120
|
+
const title = options.metadata.title ?? (filenameTitle === "" ? options.slug : filenameTitle);
|
|
121
|
+
const body = markdownContent(options.blocks, options.images);
|
|
122
|
+
const frontmatter = [
|
|
123
|
+
"---",
|
|
124
|
+
`title: ${yamlString(title)}`,
|
|
125
|
+
`source: ${yamlString("source.pdf")}`,
|
|
126
|
+
`source_type: ${yamlString("pdf")}`,
|
|
127
|
+
`source_original_filename: ${yamlString(options.originalFilename)}`,
|
|
128
|
+
`source_sha256: ${yamlString(options.sourceSha256)}`,
|
|
129
|
+
...(options.sourceUrl === undefined ? [] : [`source_url: ${yamlString(options.sourceUrl)}`]),
|
|
130
|
+
`pages: ${options.metadata.pageCount}`,
|
|
131
|
+
`clipped: ${yamlString(options.capturedDate)}`,
|
|
132
|
+
`capture_status: ${yamlString(options.status)}`,
|
|
133
|
+
`capture_method: ${yamlString("poppler")}`,
|
|
134
|
+
...(options.metadata.author === null ? [] : [`author: ${yamlString(options.metadata.author)}`]),
|
|
135
|
+
...(options.metadata.subject === null ? [] : [`description: ${yamlString(options.metadata.subject)}`]),
|
|
136
|
+
...(options.metadata.createdAt === null ? [] : [`created: ${yamlString(options.metadata.createdAt)}`]),
|
|
137
|
+
...(options.embeddedPlatforms.length === 0
|
|
138
|
+
? []
|
|
139
|
+
: [`embedded_platforms: [${options.embeddedPlatforms.map(yamlString).join(", ")}]`]),
|
|
140
|
+
"---",
|
|
141
|
+
"",
|
|
142
|
+
`# ${escapeInline(title)}`,
|
|
143
|
+
"",
|
|
144
|
+
"[Open the source PDF](source.pdf)",
|
|
145
|
+
"",
|
|
146
|
+
body.content,
|
|
147
|
+
"",
|
|
148
|
+
].join("\n");
|
|
149
|
+
return {
|
|
150
|
+
markdown: `${redactSensitiveText(frontmatter).trimEnd()}\n`,
|
|
151
|
+
headingCount: body.headingCount,
|
|
152
|
+
textBlockCount: body.textBlockCount,
|
|
153
|
+
};
|
|
154
|
+
}
|
package/src/pdf/model.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
export const PDF_CAPTURE_MANIFEST_SCHEMA_VERSION = 1 as const;
|
|
2
|
+
export const PDF_CAPTURE_MANIFEST_FILENAME = "capture.json";
|
|
3
|
+
export const PDF_CAPTURE_SOURCE_FILENAME = "source.pdf";
|
|
4
|
+
export const PDF_CAPTURE_ANNOTATIONS_FILENAME = "annotations.json";
|
|
5
|
+
|
|
6
|
+
export type PdfCaptureStatus = "complete" | "partial";
|
|
7
|
+
|
|
8
|
+
export type PdfBounds = {
|
|
9
|
+
readonly top: number;
|
|
10
|
+
readonly left: number;
|
|
11
|
+
readonly width: number;
|
|
12
|
+
readonly height: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type PdfDocumentMetadata = {
|
|
16
|
+
readonly title: string | null;
|
|
17
|
+
readonly author: string | null;
|
|
18
|
+
readonly subject: string | null;
|
|
19
|
+
readonly keywords: string | null;
|
|
20
|
+
readonly creator: string | null;
|
|
21
|
+
readonly producer: string | null;
|
|
22
|
+
readonly createdAt: string | null;
|
|
23
|
+
readonly modifiedAt: string | null;
|
|
24
|
+
readonly pageCount: number;
|
|
25
|
+
readonly encrypted: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PdfRemoteSource = {
|
|
29
|
+
readonly requestedUrl: string;
|
|
30
|
+
readonly finalUrl: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type PdfTextFragment = PdfBounds & {
|
|
34
|
+
readonly text: string;
|
|
35
|
+
readonly fontId: string;
|
|
36
|
+
readonly fontSize: number;
|
|
37
|
+
readonly bold: boolean;
|
|
38
|
+
readonly italic: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PdfImageCandidate = PdfBounds & {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly page: number;
|
|
44
|
+
readonly sourcePath: string;
|
|
45
|
+
readonly bytes: number;
|
|
46
|
+
readonly sha256: string;
|
|
47
|
+
readonly mimeType: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type PdfPageLayout = {
|
|
51
|
+
readonly page: number;
|
|
52
|
+
readonly width: number;
|
|
53
|
+
readonly height: number;
|
|
54
|
+
readonly text: readonly PdfTextFragment[];
|
|
55
|
+
readonly images: readonly PdfImageCandidate[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type PdfInspection = {
|
|
59
|
+
readonly inputPath: string;
|
|
60
|
+
readonly originalFilename: string;
|
|
61
|
+
readonly sourceBytes: number;
|
|
62
|
+
readonly sourceSha256: string;
|
|
63
|
+
readonly metadata: PdfDocumentMetadata;
|
|
64
|
+
readonly processedPages: number;
|
|
65
|
+
readonly pages: readonly PdfPageLayout[];
|
|
66
|
+
readonly popplerVersion: string | null;
|
|
67
|
+
readonly warnings: readonly string[];
|
|
68
|
+
/**
|
|
69
|
+
* The caller owns this directory. Image candidate paths remain valid until
|
|
70
|
+
* the caller removes it.
|
|
71
|
+
*/
|
|
72
|
+
readonly workspaceDirectory: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type PdfImageSemanticMetadata = {
|
|
76
|
+
readonly platform?: string;
|
|
77
|
+
readonly contentType?: string;
|
|
78
|
+
readonly channel?: string;
|
|
79
|
+
readonly author?: string;
|
|
80
|
+
readonly timestamp?: string;
|
|
81
|
+
readonly participants?: readonly string[];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type PdfImageInterpretation =
|
|
85
|
+
| {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly sha256: string;
|
|
88
|
+
readonly kind: "text" | "mixed";
|
|
89
|
+
readonly markdown: string;
|
|
90
|
+
readonly metadata?: PdfImageSemanticMetadata;
|
|
91
|
+
readonly method?: "agent" | "manual";
|
|
92
|
+
}
|
|
93
|
+
| {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly sha256: string;
|
|
96
|
+
readonly kind: "visual";
|
|
97
|
+
readonly alt?: string;
|
|
98
|
+
readonly metadata?: PdfImageSemanticMetadata;
|
|
99
|
+
readonly method?: "agent" | "manual";
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type PdfOcrResult = {
|
|
103
|
+
readonly kind: "mixed" | "visual";
|
|
104
|
+
readonly text: string;
|
|
105
|
+
readonly markdown: string;
|
|
106
|
+
readonly confidence: number | null;
|
|
107
|
+
readonly wordCount: number;
|
|
108
|
+
readonly warnings: readonly string[];
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export type PdfManifestAsset = {
|
|
112
|
+
readonly path: string;
|
|
113
|
+
readonly mimeType: string;
|
|
114
|
+
readonly bytes: number;
|
|
115
|
+
readonly sha256: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type PdfManifestImage = PdfBounds & {
|
|
119
|
+
readonly id: string;
|
|
120
|
+
readonly page: number;
|
|
121
|
+
readonly asset: PdfManifestAsset;
|
|
122
|
+
readonly kind: "text" | "mixed" | "visual";
|
|
123
|
+
readonly method: "agent" | "manual" | "tesseract" | "unclassified";
|
|
124
|
+
readonly confidence: number | null;
|
|
125
|
+
readonly wordCount: number;
|
|
126
|
+
readonly metadata: PdfImageSemanticMetadata | null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type PdfCaptureManifest = {
|
|
130
|
+
readonly schemaVersion: typeof PDF_CAPTURE_MANIFEST_SCHEMA_VERSION;
|
|
131
|
+
readonly kind: "pdf";
|
|
132
|
+
readonly capturedAt: string;
|
|
133
|
+
readonly status: PdfCaptureStatus;
|
|
134
|
+
readonly source: {
|
|
135
|
+
readonly originalFilename: string;
|
|
136
|
+
readonly path: typeof PDF_CAPTURE_SOURCE_FILENAME;
|
|
137
|
+
readonly mimeType: "application/pdf";
|
|
138
|
+
readonly bytes: number;
|
|
139
|
+
readonly sha256: string;
|
|
140
|
+
readonly requestedUrl?: string;
|
|
141
|
+
readonly finalUrl?: string;
|
|
142
|
+
};
|
|
143
|
+
readonly document: PdfDocumentMetadata & {
|
|
144
|
+
readonly processedPages: number;
|
|
145
|
+
};
|
|
146
|
+
readonly extraction: {
|
|
147
|
+
readonly layout: "pdftohtml-xml";
|
|
148
|
+
readonly popplerVersion: string | null;
|
|
149
|
+
readonly ocr: "tesseract" | "annotations" | "mixed" | "unavailable";
|
|
150
|
+
readonly headingCount: number;
|
|
151
|
+
readonly textBlockCount: number;
|
|
152
|
+
readonly imageCount: number;
|
|
153
|
+
readonly textImageCount: number;
|
|
154
|
+
readonly mixedImageCount: number;
|
|
155
|
+
readonly visualImageCount: number;
|
|
156
|
+
};
|
|
157
|
+
readonly images: readonly PdfManifestImage[];
|
|
158
|
+
readonly annotations: {
|
|
159
|
+
readonly path: typeof PDF_CAPTURE_ANNOTATIONS_FILENAME;
|
|
160
|
+
readonly count: number;
|
|
161
|
+
readonly bytes: number;
|
|
162
|
+
readonly sha256: string;
|
|
163
|
+
} | null;
|
|
164
|
+
readonly embeddedPlatforms: readonly string[];
|
|
165
|
+
readonly warnings: readonly string[];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type PdfCaptureOptions = {
|
|
169
|
+
readonly inputPath: string;
|
|
170
|
+
readonly outputBase: string;
|
|
171
|
+
readonly remoteSource?: PdfRemoteSource;
|
|
172
|
+
readonly slug?: string;
|
|
173
|
+
readonly force?: boolean;
|
|
174
|
+
readonly interpretations?: readonly PdfImageInterpretation[];
|
|
175
|
+
readonly timeoutMs?: number;
|
|
176
|
+
readonly maxPdfBytes?: number;
|
|
177
|
+
readonly maxPages?: number;
|
|
178
|
+
readonly maxImages?: number;
|
|
179
|
+
readonly maxAssetBytes?: number;
|
|
180
|
+
readonly maxTotalAssetBytes?: number;
|
|
181
|
+
readonly workspaceDirectory?: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export type PdfInspectOptions = Pick<
|
|
185
|
+
PdfCaptureOptions,
|
|
186
|
+
"inputPath" | "timeoutMs" | "maxPdfBytes" | "maxPages" | "maxImages" | "maxAssetBytes" | "maxTotalAssetBytes"
|
|
187
|
+
> & {
|
|
188
|
+
readonly workspaceDirectory: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type PdfCaptureOutcome = {
|
|
192
|
+
readonly status: PdfCaptureStatus;
|
|
193
|
+
readonly slug: string;
|
|
194
|
+
readonly outputDirectory: string;
|
|
195
|
+
readonly markdownPath: string;
|
|
196
|
+
readonly sourcePath: string;
|
|
197
|
+
readonly wordCount: number;
|
|
198
|
+
readonly pageCount: number;
|
|
199
|
+
readonly processedPages: number;
|
|
200
|
+
readonly imageCount: number;
|
|
201
|
+
readonly warnings: readonly string[];
|
|
202
|
+
readonly markdown: string;
|
|
203
|
+
readonly manifest: PdfCaptureManifest;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export type PdfToolCommand = {
|
|
207
|
+
readonly command: readonly string[];
|
|
208
|
+
readonly timeoutMs: number;
|
|
209
|
+
readonly maxOutputBytes: number;
|
|
210
|
+
readonly cwd?: string;
|
|
211
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export type PdfToolCommandResult = {
|
|
215
|
+
readonly stdout: string;
|
|
216
|
+
readonly stderr: string;
|
|
217
|
+
readonly exitCode: number;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export type PdfToolRunner = (specification: PdfToolCommand) => Promise<PdfToolCommandResult>;
|
|
221
|
+
|
|
222
|
+
export type PdfToolPaths = {
|
|
223
|
+
readonly pdfinfo: string;
|
|
224
|
+
readonly pdftohtml: string;
|
|
225
|
+
readonly tesseract: string | null;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export type PdfCaptureDependencies = {
|
|
229
|
+
readonly runTool?: PdfToolRunner;
|
|
230
|
+
readonly tools?: Partial<PdfToolPaths>;
|
|
231
|
+
readonly which?: (name: string) => string | null;
|
|
232
|
+
readonly exists?: (path: string) => boolean;
|
|
233
|
+
readonly now?: () => Date;
|
|
234
|
+
};
|
package/src/pdf/ocr.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../clip/persist.js";
|
|
2
|
+
import { sanitizeTerminalText } from "../clip/terminal.js";
|
|
3
|
+
import type {
|
|
4
|
+
PdfImageCandidate,
|
|
5
|
+
PdfOcrResult,
|
|
6
|
+
PdfToolRunner,
|
|
7
|
+
} from "./model.js";
|
|
8
|
+
|
|
9
|
+
type OcrWord = {
|
|
10
|
+
readonly page: number;
|
|
11
|
+
readonly block: number;
|
|
12
|
+
readonly paragraph: number;
|
|
13
|
+
readonly line: number;
|
|
14
|
+
readonly word: number;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
readonly confidence: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseInteger(value: string | undefined): number | null {
|
|
20
|
+
if (value === undefined || !/^-?\d+$/u.test(value)) return null;
|
|
21
|
+
const parsed = Number(value);
|
|
22
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseConfidence(value: string | undefined): number | null {
|
|
26
|
+
if (value === undefined) return null;
|
|
27
|
+
const parsed = Number(value);
|
|
28
|
+
return Number.isFinite(parsed) && parsed >= 0 && parsed <= 100 ? parsed : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function escapeMarkdownText(value: string): string {
|
|
32
|
+
return value
|
|
33
|
+
.replace(/\\/gu, "\\\\")
|
|
34
|
+
.replace(/([`*_[\]{}<>#])/gu, "\\$1");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ParsedTesseractTsv = {
|
|
38
|
+
readonly text: string;
|
|
39
|
+
readonly markdown: string;
|
|
40
|
+
readonly confidence: number | null;
|
|
41
|
+
readonly wordCount: number;
|
|
42
|
+
readonly substantive: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Parse Tesseract TSV into stable reading-order text and a quoted Markdown block. */
|
|
46
|
+
export function parseTesseractTsv(value: string): ParsedTesseractTsv {
|
|
47
|
+
const lines = value.split(/\r?\n/gu);
|
|
48
|
+
const header = lines[0]?.split("\t") ?? [];
|
|
49
|
+
const positions = new Map(header.map((name, index) => [name, index]));
|
|
50
|
+
const required = ["page_num", "block_num", "par_num", "line_num", "word_num", "conf", "text"] as const;
|
|
51
|
+
if (!required.every((name) => positions.has(name))) {
|
|
52
|
+
throw new Error("Tesseract TSV is missing required columns");
|
|
53
|
+
}
|
|
54
|
+
const words: OcrWord[] = [];
|
|
55
|
+
for (const row of lines.slice(1)) {
|
|
56
|
+
if (row === "") continue;
|
|
57
|
+
const fields = row.split("\t");
|
|
58
|
+
const at = (name: typeof required[number]): string | undefined => {
|
|
59
|
+
const index = positions.get(name);
|
|
60
|
+
return index === undefined ? undefined : fields[index];
|
|
61
|
+
};
|
|
62
|
+
const text = sanitizeTerminalText(at("text") ?? "").trim();
|
|
63
|
+
const confidence = parseConfidence(at("conf"));
|
|
64
|
+
const word = parseInteger(at("word_num"));
|
|
65
|
+
const page = parseInteger(at("page_num"));
|
|
66
|
+
const block = parseInteger(at("block_num"));
|
|
67
|
+
const paragraph = parseInteger(at("par_num"));
|
|
68
|
+
const line = parseInteger(at("line_num"));
|
|
69
|
+
if (
|
|
70
|
+
text === ""
|
|
71
|
+
|| confidence === null
|
|
72
|
+
|| word === null
|
|
73
|
+
|| page === null
|
|
74
|
+
|| block === null
|
|
75
|
+
|| paragraph === null
|
|
76
|
+
|| line === null
|
|
77
|
+
) continue;
|
|
78
|
+
words.push({
|
|
79
|
+
page,
|
|
80
|
+
block,
|
|
81
|
+
paragraph,
|
|
82
|
+
line,
|
|
83
|
+
word,
|
|
84
|
+
text,
|
|
85
|
+
confidence,
|
|
86
|
+
});
|
|
87
|
+
if (words.length >= 100_000) break;
|
|
88
|
+
}
|
|
89
|
+
words.sort((left, right) =>
|
|
90
|
+
left.page - right.page
|
|
91
|
+
|| left.block - right.block
|
|
92
|
+
|| left.paragraph - right.paragraph
|
|
93
|
+
|| left.line - right.line
|
|
94
|
+
|| left.word - right.word);
|
|
95
|
+
const grouped = new Map<string, string[]>();
|
|
96
|
+
for (const word of words) {
|
|
97
|
+
const lineIdentity = `${word.page}:${word.block}:${word.paragraph}:${word.line}`;
|
|
98
|
+
const current = grouped.get(lineIdentity) ?? [];
|
|
99
|
+
current.push(word.text);
|
|
100
|
+
grouped.set(lineIdentity, current);
|
|
101
|
+
}
|
|
102
|
+
const textLines = [...grouped.values()]
|
|
103
|
+
.map((entries) => entries.join(" ").replace(/\s+/gu, " ").trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
const text = redactSensitiveText(textLines.join("\n"));
|
|
106
|
+
const weightedCharacters = words.reduce((sum, word) => sum + word.text.length, 0);
|
|
107
|
+
const confidence = weightedCharacters === 0
|
|
108
|
+
? null
|
|
109
|
+
: words.reduce((sum, word) => sum + word.confidence * word.text.length, 0) / weightedCharacters;
|
|
110
|
+
const alphanumericCount = [...text].filter((character) => /[\p{Letter}\p{Number}]/u.test(character)).length;
|
|
111
|
+
const substantive = words.length >= 3
|
|
112
|
+
&& alphanumericCount >= 12
|
|
113
|
+
&& confidence !== null
|
|
114
|
+
&& confidence >= 35;
|
|
115
|
+
const markdown = substantive
|
|
116
|
+
? text.split("\n").map((line) => `> ${escapeMarkdownText(line)}`).join("\n")
|
|
117
|
+
: "";
|
|
118
|
+
return {
|
|
119
|
+
text,
|
|
120
|
+
markdown,
|
|
121
|
+
confidence,
|
|
122
|
+
wordCount: words.length,
|
|
123
|
+
substantive,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Classify one image with local OCR; substantive results remain mixed visual/text evidence. */
|
|
128
|
+
export async function ocrPdfImage(
|
|
129
|
+
image: PdfImageCandidate,
|
|
130
|
+
options: {
|
|
131
|
+
readonly tesseractPath: string | null;
|
|
132
|
+
readonly timeoutMs: number;
|
|
133
|
+
readonly runTool: PdfToolRunner;
|
|
134
|
+
},
|
|
135
|
+
): Promise<PdfOcrResult> {
|
|
136
|
+
if (options.tesseractPath === null) {
|
|
137
|
+
return {
|
|
138
|
+
kind: "visual",
|
|
139
|
+
text: "",
|
|
140
|
+
markdown: "",
|
|
141
|
+
confidence: null,
|
|
142
|
+
wordCount: 0,
|
|
143
|
+
warnings: ["Tesseract is unavailable; retained the image without OCR text."],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const result = await options.runTool({
|
|
148
|
+
command: [
|
|
149
|
+
options.tesseractPath,
|
|
150
|
+
image.sourcePath,
|
|
151
|
+
"stdout",
|
|
152
|
+
"--psm",
|
|
153
|
+
"6",
|
|
154
|
+
"tsv",
|
|
155
|
+
],
|
|
156
|
+
timeoutMs: options.timeoutMs,
|
|
157
|
+
maxOutputBytes: 16 * 1024 * 1024,
|
|
158
|
+
});
|
|
159
|
+
if (result.exitCode !== 0) {
|
|
160
|
+
return {
|
|
161
|
+
kind: "visual",
|
|
162
|
+
text: "",
|
|
163
|
+
markdown: "",
|
|
164
|
+
confidence: null,
|
|
165
|
+
wordCount: 0,
|
|
166
|
+
warnings: ["Tesseract could not read this image; retained it as visual evidence."],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const parsed = parseTesseractTsv(result.stdout);
|
|
170
|
+
return {
|
|
171
|
+
kind: parsed.substantive ? "mixed" : "visual",
|
|
172
|
+
text: parsed.text,
|
|
173
|
+
markdown: parsed.markdown,
|
|
174
|
+
confidence: parsed.confidence,
|
|
175
|
+
wordCount: parsed.wordCount,
|
|
176
|
+
warnings: parsed.substantive
|
|
177
|
+
? []
|
|
178
|
+
: ["Tesseract did not find sufficiently confident text; retained the image as visual evidence."],
|
|
179
|
+
};
|
|
180
|
+
} catch {
|
|
181
|
+
return {
|
|
182
|
+
kind: "visual",
|
|
183
|
+
text: "",
|
|
184
|
+
markdown: "",
|
|
185
|
+
confidence: null,
|
|
186
|
+
wordCount: 0,
|
|
187
|
+
warnings: ["Tesseract OCR failed; retained the image as visual evidence."],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|