@echofiles/echo-pdf 0.7.0 → 0.8.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/README.md +10 -0
- package/bin/echo-pdf.js +37 -1
- package/dist/local/formulas.d.ts +2 -0
- package/dist/local/formulas.js +71 -0
- package/dist/local/index.d.ts +3 -1
- package/dist/local/index.js +2 -0
- package/dist/local/tables.d.ts +2 -0
- package/dist/local/tables.js +71 -0
- package/dist/pdf-types.d.ts +1 -0
- package/echo-pdf.config.json +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,8 @@ What these commands map to:
|
|
|
85
85
|
- `semantic` -> `get_semantic_document_structure`
|
|
86
86
|
- `page` -> `get_page_content`
|
|
87
87
|
- `render` -> `get_page_render`
|
|
88
|
+
- `tables` -> `get_page_tables_latex`
|
|
89
|
+
- `formulas` -> `get_page_formulas_latex`
|
|
88
90
|
|
|
89
91
|
By default, `echo-pdf` writes reusable artifacts into a local workspace:
|
|
90
92
|
|
|
@@ -101,6 +103,10 @@ By default, `echo-pdf` writes reusable artifacts into a local workspace:
|
|
|
101
103
|
renders/
|
|
102
104
|
0001.scale-2.json
|
|
103
105
|
0001.scale-2.png
|
|
106
|
+
tables/
|
|
107
|
+
0001.scale-2.provider-openai.model-gpt-4.1-mini.prompt-<hash>.json
|
|
108
|
+
formulas/
|
|
109
|
+
0001.scale-2.provider-openai.model-gpt-4.1-mini.prompt-<hash>.json
|
|
104
110
|
```
|
|
105
111
|
|
|
106
112
|
These artifacts are meant to be inspected, cached, and reused by downstream local tools.
|
|
@@ -115,6 +121,8 @@ import {
|
|
|
115
121
|
get_semantic_document_structure,
|
|
116
122
|
get_page_content,
|
|
117
123
|
get_page_render,
|
|
124
|
+
get_page_tables_latex,
|
|
125
|
+
get_page_formulas_latex,
|
|
118
126
|
} from "@echofiles/echo-pdf/local"
|
|
119
127
|
|
|
120
128
|
const document = await get_document({ pdfPath: "./sample.pdf" })
|
|
@@ -126,6 +134,8 @@ const semantic = await get_semantic_document_structure({
|
|
|
126
134
|
})
|
|
127
135
|
const page1 = await get_page_content({ pdfPath: "./sample.pdf", pageNumber: 1 })
|
|
128
136
|
const render1 = await get_page_render({ pdfPath: "./sample.pdf", pageNumber: 1, scale: 2 })
|
|
137
|
+
const tables = await get_page_tables_latex({ pdfPath: "./sample.pdf", pageNumber: 1, provider: "openai", model: "gpt-4.1-mini" })
|
|
138
|
+
const formulas = await get_page_formulas_latex({ pdfPath: "./sample.pdf", pageNumber: 1, provider: "openai", model: "gpt-4.1-mini" })
|
|
129
139
|
```
|
|
130
140
|
|
|
131
141
|
Notes:
|
package/bin/echo-pdf.js
CHANGED
|
@@ -215,7 +215,7 @@ const loadLocalDocumentApi = async () => {
|
|
|
215
215
|
return import(LOCAL_DOCUMENT_DIST_ENTRY.href)
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
const LOCAL_PRIMITIVE_COMMANDS = ["document", "structure", "semantic", "page", "render"]
|
|
218
|
+
const LOCAL_PRIMITIVE_COMMANDS = ["document", "structure", "semantic", "page", "render", "tables", "formulas"]
|
|
219
219
|
const REMOVED_DOCUMENT_ALIAS_TO_PRIMITIVE = {
|
|
220
220
|
index: "document",
|
|
221
221
|
get: "document",
|
|
@@ -302,6 +302,40 @@ const runLocalPrimitiveCommand = async (command, subcommand, rest, flags) => {
|
|
|
302
302
|
return
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
if (primitive === "tables") {
|
|
306
|
+
const semanticContext = resolveLocalSemanticContext(flags)
|
|
307
|
+
const local = await loadLocalDocumentApi()
|
|
308
|
+
print(await local.get_page_tables_latex({
|
|
309
|
+
pdfPath,
|
|
310
|
+
workspaceDir,
|
|
311
|
+
forceRefresh,
|
|
312
|
+
pageNumber,
|
|
313
|
+
renderScale,
|
|
314
|
+
provider: semanticContext.provider,
|
|
315
|
+
model: semanticContext.model,
|
|
316
|
+
providerApiKeys: semanticContext.providerApiKeys,
|
|
317
|
+
prompt: typeof flags.prompt === "string" ? flags.prompt : undefined,
|
|
318
|
+
}))
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (primitive === "formulas") {
|
|
323
|
+
const semanticContext = resolveLocalSemanticContext(flags)
|
|
324
|
+
const local = await loadLocalDocumentApi()
|
|
325
|
+
print(await local.get_page_formulas_latex({
|
|
326
|
+
pdfPath,
|
|
327
|
+
workspaceDir,
|
|
328
|
+
forceRefresh,
|
|
329
|
+
pageNumber,
|
|
330
|
+
renderScale,
|
|
331
|
+
provider: semanticContext.provider,
|
|
332
|
+
model: semanticContext.model,
|
|
333
|
+
providerApiKeys: semanticContext.providerApiKeys,
|
|
334
|
+
prompt: typeof flags.prompt === "string" ? flags.prompt : undefined,
|
|
335
|
+
}))
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
305
339
|
throw new Error(`Unsupported local primitive command: ${primitive}`)
|
|
306
340
|
}
|
|
307
341
|
|
|
@@ -313,6 +347,8 @@ const usage = () => {
|
|
|
313
347
|
process.stdout.write(` semantic <file.pdf> [--provider alias] [--model model] [--profile name] [--workspace DIR] [--force-refresh]\n`)
|
|
314
348
|
process.stdout.write(` page <file.pdf> --page <N> [--workspace DIR] [--force-refresh]\n`)
|
|
315
349
|
process.stdout.write(` render <file.pdf> --page <N> [--scale N] [--workspace DIR] [--force-refresh]\n`)
|
|
350
|
+
process.stdout.write(` tables <file.pdf> --page <N> [--provider alias] [--model model] [--scale N] [--prompt text] [--workspace DIR] [--force-refresh]\n`)
|
|
351
|
+
process.stdout.write(` formulas <file.pdf> --page <N> [--provider alias] [--model model] [--scale N] [--prompt text] [--workspace DIR] [--force-refresh]\n`)
|
|
316
352
|
process.stdout.write(`\nLocal config commands:\n`)
|
|
317
353
|
process.stdout.write(` provider set --provider <${getProviderSetNames().join("|")}> --api-key <KEY> [--profile name]\n`)
|
|
318
354
|
process.stdout.write(` provider use --provider <${getProviderAliases().join("|")}> [--profile name]\n`)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/// <reference path="../node/compat.d.ts" />
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { toDataUrl } from "../file-utils.js";
|
|
5
|
+
import { visionRecognize } from "../provider-client.js";
|
|
6
|
+
import { ensureRenderArtifact, indexDocumentInternal } from "./document.js";
|
|
7
|
+
import { buildStructuredArtifactPath, ensurePageNumber, fileExists, matchesSourceSnapshot, normalizeFormulaItems, pageLabel, parseJsonObject, readJson, resolveAgentSelection, resolveConfig, resolveEnv, resolveRenderScale, writeJson, } from "./shared.js";
|
|
8
|
+
const DEFAULT_FORMULA_PROMPT = "Detect all displayed mathematical formulas from this PDF page image. " +
|
|
9
|
+
"Return JSON only. Schema: " +
|
|
10
|
+
'{ "formulas": [{ "latexMath": "LaTeX math expression", "label": "optional equation label", "evidenceText": "optional" }] }. ' +
|
|
11
|
+
"Use LaTeX math notation. Do not include inline prose math or trivial single-symbol expressions. " +
|
|
12
|
+
"If no displayed formulas are found, return {\"formulas\":[]}.";
|
|
13
|
+
export const get_page_formulas_latex = async (request) => {
|
|
14
|
+
const env = resolveEnv(request.env);
|
|
15
|
+
const config = resolveConfig(request.config, env);
|
|
16
|
+
const { record } = await indexDocumentInternal(request);
|
|
17
|
+
ensurePageNumber(record.pageCount, request.pageNumber);
|
|
18
|
+
const { provider, model } = resolveAgentSelection(config, request);
|
|
19
|
+
const renderScale = resolveRenderScale(config, request.renderScale);
|
|
20
|
+
const prompt = typeof request.prompt === "string" && request.prompt.trim().length > 0
|
|
21
|
+
? request.prompt.trim()
|
|
22
|
+
: DEFAULT_FORMULA_PROMPT;
|
|
23
|
+
const formulasDir = path.join(record.artifactPaths.documentDir, "formulas");
|
|
24
|
+
const artifactPath = buildStructuredArtifactPath(formulasDir, request.pageNumber, renderScale, provider, model, prompt);
|
|
25
|
+
if (!request.forceRefresh && await fileExists(artifactPath)) {
|
|
26
|
+
const cached = await readJson(artifactPath);
|
|
27
|
+
if (matchesSourceSnapshot(cached, record)) {
|
|
28
|
+
return { ...cached, cacheStatus: "reused" };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const renderArtifact = await ensureRenderArtifact({
|
|
32
|
+
pdfPath: request.pdfPath,
|
|
33
|
+
workspaceDir: request.workspaceDir,
|
|
34
|
+
forceRefresh: request.forceRefresh,
|
|
35
|
+
config,
|
|
36
|
+
pageNumber: request.pageNumber,
|
|
37
|
+
renderScale: request.renderScale,
|
|
38
|
+
});
|
|
39
|
+
const imageBytes = new Uint8Array(await readFile(renderArtifact.imagePath));
|
|
40
|
+
const imageDataUrl = toDataUrl(imageBytes, renderArtifact.mimeType);
|
|
41
|
+
const response = await visionRecognize({
|
|
42
|
+
config,
|
|
43
|
+
env,
|
|
44
|
+
providerAlias: provider,
|
|
45
|
+
model,
|
|
46
|
+
prompt,
|
|
47
|
+
imageDataUrl,
|
|
48
|
+
runtimeApiKeys: request.providerApiKeys,
|
|
49
|
+
});
|
|
50
|
+
const parsed = parseJsonObject(response);
|
|
51
|
+
const formulas = normalizeFormulaItems(parsed?.formulas);
|
|
52
|
+
const pageArtifactPath = path.join(record.artifactPaths.pagesDir, `${pageLabel(request.pageNumber)}.json`);
|
|
53
|
+
const artifact = {
|
|
54
|
+
documentId: record.documentId,
|
|
55
|
+
pageNumber: request.pageNumber,
|
|
56
|
+
renderScale,
|
|
57
|
+
sourceSizeBytes: record.sizeBytes,
|
|
58
|
+
sourceMtimeMs: record.mtimeMs,
|
|
59
|
+
provider,
|
|
60
|
+
model,
|
|
61
|
+
prompt,
|
|
62
|
+
imagePath: renderArtifact.imagePath,
|
|
63
|
+
pageArtifactPath,
|
|
64
|
+
renderArtifactPath: renderArtifact.artifactPath,
|
|
65
|
+
artifactPath,
|
|
66
|
+
generatedAt: new Date().toISOString(),
|
|
67
|
+
formulas,
|
|
68
|
+
};
|
|
69
|
+
await writeJson(artifactPath, artifact);
|
|
70
|
+
return { ...artifact, cacheStatus: "fresh" };
|
|
71
|
+
};
|
package/dist/local/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export type { LocalDocumentArtifactPaths, LocalDocumentMetadata, LocalDocumentRequest, LocalDocumentStructure, LocalDocumentStructureNode, LocalPageContent, LocalPageContentRequest, LocalPageRenderArtifact, LocalPageRenderRequest, LocalSemanticDocumentRequest, LocalSemanticDocumentStructure, LocalSemanticStructureNode, } from "./types.js";
|
|
1
|
+
export type { LocalDocumentArtifactPaths, LocalDocumentMetadata, LocalDocumentRequest, LocalDocumentStructure, LocalDocumentStructureNode, LocalFormulaArtifactItem, LocalPageContent, LocalPageContentRequest, LocalPageFormulasArtifact, LocalPageFormulasRequest, LocalPageRenderArtifact, LocalPageRenderRequest, LocalPageTablesArtifact, LocalPageTablesRequest, LocalSemanticDocumentRequest, LocalSemanticDocumentStructure, LocalSemanticStructureNode, LocalTableArtifactItem, } from "./types.js";
|
|
2
2
|
export { get_document, get_document_structure, get_page_content, get_page_render } from "./document.js";
|
|
3
|
+
export { get_page_formulas_latex } from "./formulas.js";
|
|
3
4
|
export { get_semantic_document_structure } from "./semantic.js";
|
|
5
|
+
export { get_page_tables_latex } from "./tables.js";
|
package/dist/local/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { get_document, get_document_structure, get_page_content, get_page_render } from "./document.js";
|
|
2
|
+
export { get_page_formulas_latex } from "./formulas.js";
|
|
2
3
|
export { get_semantic_document_structure } from "./semantic.js";
|
|
4
|
+
export { get_page_tables_latex } from "./tables.js";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/// <reference path="../node/compat.d.ts" />
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { toDataUrl } from "../file-utils.js";
|
|
5
|
+
import { visionRecognize } from "../provider-client.js";
|
|
6
|
+
import { ensureRenderArtifact, indexDocumentInternal } from "./document.js";
|
|
7
|
+
import { buildStructuredArtifactPath, ensurePageNumber, fileExists, matchesSourceSnapshot, normalizeTableItems, pageLabel, parseJsonObject, readJson, resolveAgentSelection, resolveConfig, resolveEnv, resolveRenderScale, writeJson, } from "./shared.js";
|
|
8
|
+
const DEFAULT_TABLE_PROMPT = "Detect all tabular structures from this PDF page image. " +
|
|
9
|
+
"Return JSON only. Schema: " +
|
|
10
|
+
'{ "tables": [{ "latexTabular": "\\\\begin{tabular}...\\\\end{tabular}", "caption": "optional", "evidenceText": "optional" }] }. ' +
|
|
11
|
+
"Each table must be a complete LaTeX tabular environment. " +
|
|
12
|
+
"If no tables are found, return {\"tables\":[]}.";
|
|
13
|
+
export const get_page_tables_latex = async (request) => {
|
|
14
|
+
const env = resolveEnv(request.env);
|
|
15
|
+
const config = resolveConfig(request.config, env);
|
|
16
|
+
const { record } = await indexDocumentInternal(request);
|
|
17
|
+
ensurePageNumber(record.pageCount, request.pageNumber);
|
|
18
|
+
const { provider, model } = resolveAgentSelection(config, request);
|
|
19
|
+
const renderScale = resolveRenderScale(config, request.renderScale);
|
|
20
|
+
const prompt = typeof request.prompt === "string" && request.prompt.trim().length > 0
|
|
21
|
+
? request.prompt.trim()
|
|
22
|
+
: (config.agent.tablePrompt || DEFAULT_TABLE_PROMPT);
|
|
23
|
+
const tablesDir = path.join(record.artifactPaths.documentDir, "tables");
|
|
24
|
+
const artifactPath = buildStructuredArtifactPath(tablesDir, request.pageNumber, renderScale, provider, model, prompt);
|
|
25
|
+
if (!request.forceRefresh && await fileExists(artifactPath)) {
|
|
26
|
+
const cached = await readJson(artifactPath);
|
|
27
|
+
if (matchesSourceSnapshot(cached, record)) {
|
|
28
|
+
return { ...cached, cacheStatus: "reused" };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const renderArtifact = await ensureRenderArtifact({
|
|
32
|
+
pdfPath: request.pdfPath,
|
|
33
|
+
workspaceDir: request.workspaceDir,
|
|
34
|
+
forceRefresh: request.forceRefresh,
|
|
35
|
+
config,
|
|
36
|
+
pageNumber: request.pageNumber,
|
|
37
|
+
renderScale: request.renderScale,
|
|
38
|
+
});
|
|
39
|
+
const imageBytes = new Uint8Array(await readFile(renderArtifact.imagePath));
|
|
40
|
+
const imageDataUrl = toDataUrl(imageBytes, renderArtifact.mimeType);
|
|
41
|
+
const response = await visionRecognize({
|
|
42
|
+
config,
|
|
43
|
+
env,
|
|
44
|
+
providerAlias: provider,
|
|
45
|
+
model,
|
|
46
|
+
prompt,
|
|
47
|
+
imageDataUrl,
|
|
48
|
+
runtimeApiKeys: request.providerApiKeys,
|
|
49
|
+
});
|
|
50
|
+
const parsed = parseJsonObject(response);
|
|
51
|
+
const tables = normalizeTableItems(parsed?.tables);
|
|
52
|
+
const pageArtifactPath = path.join(record.artifactPaths.pagesDir, `${pageLabel(request.pageNumber)}.json`);
|
|
53
|
+
const artifact = {
|
|
54
|
+
documentId: record.documentId,
|
|
55
|
+
pageNumber: request.pageNumber,
|
|
56
|
+
renderScale,
|
|
57
|
+
sourceSizeBytes: record.sizeBytes,
|
|
58
|
+
sourceMtimeMs: record.mtimeMs,
|
|
59
|
+
provider,
|
|
60
|
+
model,
|
|
61
|
+
prompt,
|
|
62
|
+
imagePath: renderArtifact.imagePath,
|
|
63
|
+
pageArtifactPath,
|
|
64
|
+
renderArtifactPath: renderArtifact.artifactPath,
|
|
65
|
+
artifactPath,
|
|
66
|
+
generatedAt: new Date().toISOString(),
|
|
67
|
+
tables,
|
|
68
|
+
};
|
|
69
|
+
await writeJson(artifactPath, artifact);
|
|
70
|
+
return { ...artifact, cacheStatus: "fresh" };
|
|
71
|
+
};
|
package/dist/pdf-types.d.ts
CHANGED
package/echo-pdf.config.json
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"agent": {
|
|
9
9
|
"defaultProvider": "openai",
|
|
10
10
|
"defaultModel": "",
|
|
11
|
-
"tablePrompt": "Detect all tabular structures from this PDF page image. Output only valid LaTeX tabular environments, no explanations, no markdown fences."
|
|
11
|
+
"tablePrompt": "Detect all tabular structures from this PDF page image. Output only valid LaTeX tabular environments, no explanations, no markdown fences.",
|
|
12
|
+
"formulaPrompt": ""
|
|
12
13
|
},
|
|
13
14
|
"providers": {
|
|
14
15
|
"openai": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echofiles/echo-pdf",
|
|
3
3
|
"description": "Local-first PDF document component core with CLI, workspace artifacts, and reusable page primitives.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://pdf.echofile.ai/",
|
|
7
7
|
"repository": {
|