@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 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,2 @@
1
+ import type { LocalPageFormulasArtifact, LocalPageFormulasRequest } from "./types.js";
2
+ export declare const get_page_formulas_latex: (request: LocalPageFormulasRequest) => Promise<LocalPageFormulasArtifact>;
@@ -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
+ };
@@ -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";
@@ -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,2 @@
1
+ import type { LocalPageTablesArtifact, LocalPageTablesRequest } from "./types.js";
2
+ export declare const get_page_tables_latex: (request: LocalPageTablesRequest) => Promise<LocalPageTablesArtifact>;
@@ -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
+ };
@@ -21,6 +21,7 @@ export interface EchoPdfConfig {
21
21
  readonly defaultProvider: string;
22
22
  readonly defaultModel: string;
23
23
  readonly tablePrompt: string;
24
+ readonly formulaPrompt?: string;
24
25
  };
25
26
  readonly providers: Record<string, EchoPdfProviderConfig>;
26
27
  }
@@ -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.7.0",
4
+ "version": "0.8.0",
5
5
  "type": "module",
6
6
  "homepage": "https://pdf.echofile.ai/",
7
7
  "repository": {