@echofiles/echo-pdf 0.6.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 +25 -1
- package/bin/echo-pdf.js +59 -17
- 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 +2 -1
- package/dist/provider-client.js +1 -1
- package/dist/provider-keys.js +4 -1
- package/dist/types.d.ts +1 -1
- package/echo-pdf.config.json +11 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,16 @@ echo-pdf semantic ./sample.pdf
|
|
|
68
68
|
|
|
69
69
|
`echo-pdf semantic` now uses the CLI profile's provider/model/api-key settings. If the selected provider or model is missing, it fails early with a clear setup error instead of quietly dropping back to a weaker path.
|
|
70
70
|
|
|
71
|
+
For a local OpenAI-compatible LLM server, point a provider at `http://localhost:...` and leave `apiKeyEnv` empty in `echo-pdf.config.json`. Then configure the CLI profile without a dummy key:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
echo-pdf provider set --provider ollama --api-key ""
|
|
75
|
+
echo-pdf model set --provider ollama --model llava:13b
|
|
76
|
+
echo-pdf semantic ./sample.pdf --provider ollama
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This works for local OpenAI-compatible servers such as Ollama, llama.cpp, vLLM, LM Studio, or LocalAI, as long as the selected model supports vision input.
|
|
80
|
+
|
|
71
81
|
What these commands map to:
|
|
72
82
|
|
|
73
83
|
- `document` -> `get_document`
|
|
@@ -75,6 +85,8 @@ What these commands map to:
|
|
|
75
85
|
- `semantic` -> `get_semantic_document_structure`
|
|
76
86
|
- `page` -> `get_page_content`
|
|
77
87
|
- `render` -> `get_page_render`
|
|
88
|
+
- `tables` -> `get_page_tables_latex`
|
|
89
|
+
- `formulas` -> `get_page_formulas_latex`
|
|
78
90
|
|
|
79
91
|
By default, `echo-pdf` writes reusable artifacts into a local workspace:
|
|
80
92
|
|
|
@@ -91,6 +103,10 @@ By default, `echo-pdf` writes reusable artifacts into a local workspace:
|
|
|
91
103
|
renders/
|
|
92
104
|
0001.scale-2.json
|
|
93
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
|
|
94
110
|
```
|
|
95
111
|
|
|
96
112
|
These artifacts are meant to be inspected, cached, and reused by downstream local tools.
|
|
@@ -105,13 +121,21 @@ import {
|
|
|
105
121
|
get_semantic_document_structure,
|
|
106
122
|
get_page_content,
|
|
107
123
|
get_page_render,
|
|
124
|
+
get_page_tables_latex,
|
|
125
|
+
get_page_formulas_latex,
|
|
108
126
|
} from "@echofiles/echo-pdf/local"
|
|
109
127
|
|
|
110
128
|
const document = await get_document({ pdfPath: "./sample.pdf" })
|
|
111
129
|
const structure = await get_document_structure({ pdfPath: "./sample.pdf" })
|
|
112
|
-
const semantic = await get_semantic_document_structure({
|
|
130
|
+
const semantic = await get_semantic_document_structure({
|
|
131
|
+
pdfPath: "./sample.pdf",
|
|
132
|
+
provider: "openai",
|
|
133
|
+
model: "gpt-4.1-mini",
|
|
134
|
+
})
|
|
113
135
|
const page1 = await get_page_content({ pdfPath: "./sample.pdf", pageNumber: 1 })
|
|
114
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" })
|
|
115
139
|
```
|
|
116
140
|
|
|
117
141
|
Notes:
|
package/bin/echo-pdf.js
CHANGED
|
@@ -9,24 +9,26 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
const PROJECT_CONFIG_FILE = path.resolve(__dirname, "../echo-pdf.config.json")
|
|
11
11
|
const PROJECT_CONFIG = JSON.parse(fs.readFileSync(PROJECT_CONFIG_FILE, "utf-8"))
|
|
12
|
-
const PROVIDER_ENTRIES = Object.entries(PROJECT_CONFIG.providers || {})
|
|
13
|
-
const PROVIDER_ALIASES = PROVIDER_ENTRIES.map(([alias]) => alias)
|
|
14
|
-
const PROVIDER_ALIAS_BY_TYPE = new Map(PROVIDER_ENTRIES.map(([alias, provider]) => [provider.type, alias]))
|
|
15
|
-
const PROVIDER_SET_NAMES = Array.from(new Set(PROVIDER_ENTRIES.flatMap(([alias, provider]) => [alias, provider.type])))
|
|
16
12
|
const PROJECT_DEFAULT_MODEL = String(PROJECT_CONFIG.agent?.defaultModel || "").trim()
|
|
17
13
|
|
|
14
|
+
const getProviderEntries = () => Object.entries(PROJECT_CONFIG.providers || {})
|
|
15
|
+
const getProviderAliases = () => getProviderEntries().map(([alias]) => alias)
|
|
16
|
+
const getProviderAliasByType = () => new Map(getProviderEntries().map(([alias, provider]) => [provider.type, alias]))
|
|
17
|
+
const getProviderSetNames = () => Array.from(new Set(getProviderEntries().flatMap(([alias, provider]) => [alias, provider.type])))
|
|
18
|
+
|
|
18
19
|
const emptyProviders = () =>
|
|
19
|
-
Object.fromEntries(
|
|
20
|
+
Object.fromEntries(getProviderAliases().map((providerAlias) => [providerAlias, { apiKey: "" }]))
|
|
20
21
|
|
|
21
22
|
const resolveProviderAliasInput = (input) => {
|
|
22
23
|
if (typeof input !== "string" || input.trim().length === 0) {
|
|
23
24
|
throw new Error("provider is required")
|
|
24
25
|
}
|
|
25
26
|
const raw = input.trim()
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const providerAliases = getProviderAliases()
|
|
28
|
+
if (providerAliases.includes(raw)) return raw
|
|
29
|
+
const fromType = getProviderAliasByType().get(raw)
|
|
28
30
|
if (fromType) return fromType
|
|
29
|
-
throw new Error(`provider must be one of: ${
|
|
31
|
+
throw new Error(`provider must be one of: ${getProviderSetNames().join(", ")}`)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function resolveDefaultProviderAlias() {
|
|
@@ -34,7 +36,7 @@ function resolveDefaultProviderAlias() {
|
|
|
34
36
|
if (typeof configured === "string" && configured.trim().length > 0) {
|
|
35
37
|
return resolveProviderAliasInput(configured.trim())
|
|
36
38
|
}
|
|
37
|
-
return
|
|
39
|
+
return getProviderAliases()[0] || "openai"
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const DEFAULT_PROVIDER_ALIAS = resolveDefaultProviderAlias()
|
|
@@ -73,7 +75,7 @@ const getProfile = (config, name) => {
|
|
|
73
75
|
}
|
|
74
76
|
const profile = config.profiles[profileName]
|
|
75
77
|
if (!profile.providers || typeof profile.providers !== "object") profile.providers = {}
|
|
76
|
-
for (const providerAlias of
|
|
78
|
+
for (const providerAlias of getProviderAliases()) {
|
|
77
79
|
if (!profile.providers[providerAlias] || typeof profile.providers[providerAlias] !== "object") {
|
|
78
80
|
profile.providers[providerAlias] = { apiKey: "" }
|
|
79
81
|
}
|
|
@@ -102,7 +104,7 @@ const parseFlags = (args) => {
|
|
|
102
104
|
if (!token?.startsWith("--")) continue
|
|
103
105
|
const key = token.slice(2)
|
|
104
106
|
const next = args[i + 1]
|
|
105
|
-
if (
|
|
107
|
+
if (typeof next !== "string" || next.startsWith("--")) {
|
|
106
108
|
flags[key] = true
|
|
107
109
|
} else {
|
|
108
110
|
flags[key] = next
|
|
@@ -147,7 +149,7 @@ const readEnvApiKey = (providerAlias) => {
|
|
|
147
149
|
const buildProviderApiKeys = (config, profileName) => {
|
|
148
150
|
const profile = getProfile(config, profileName)
|
|
149
151
|
const providerApiKeys = {}
|
|
150
|
-
for (const [providerAlias, providerConfig] of
|
|
152
|
+
for (const [providerAlias, providerConfig] of getProviderEntries()) {
|
|
151
153
|
const apiKey = profile.providers?.[providerAlias]?.apiKey || profile.providers?.[providerConfig.type]?.apiKey || ""
|
|
152
154
|
providerApiKeys[providerAlias] = apiKey
|
|
153
155
|
providerApiKeys[providerConfig.type] = apiKey
|
|
@@ -171,8 +173,8 @@ const resolveLocalSemanticContext = (flags) => {
|
|
|
171
173
|
}
|
|
172
174
|
const providerApiKeys = buildProviderApiKeys(config, profileName)
|
|
173
175
|
const configuredApiKey = typeof providerApiKeys[provider] === "string" ? providerApiKeys[provider].trim() : ""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const apiKeyEnv = PROJECT_CONFIG.providers?.[provider]?.apiKeyEnv || ""
|
|
177
|
+
if (apiKeyEnv && !configuredApiKey && !readEnvApiKey(provider)) {
|
|
176
178
|
throw new Error(
|
|
177
179
|
[
|
|
178
180
|
`semantic requires an API key for provider "${provider}".`,
|
|
@@ -213,7 +215,7 @@ const loadLocalDocumentApi = async () => {
|
|
|
213
215
|
return import(LOCAL_DOCUMENT_DIST_ENTRY.href)
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
const LOCAL_PRIMITIVE_COMMANDS = ["document", "structure", "semantic", "page", "render"]
|
|
218
|
+
const LOCAL_PRIMITIVE_COMMANDS = ["document", "structure", "semantic", "page", "render", "tables", "formulas"]
|
|
217
219
|
const REMOVED_DOCUMENT_ALIAS_TO_PRIMITIVE = {
|
|
218
220
|
index: "document",
|
|
219
221
|
get: "document",
|
|
@@ -300,6 +302,40 @@ const runLocalPrimitiveCommand = async (command, subcommand, rest, flags) => {
|
|
|
300
302
|
return
|
|
301
303
|
}
|
|
302
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
|
+
|
|
303
339
|
throw new Error(`Unsupported local primitive command: ${primitive}`)
|
|
304
340
|
}
|
|
305
341
|
|
|
@@ -311,13 +347,19 @@ const usage = () => {
|
|
|
311
347
|
process.stdout.write(` semantic <file.pdf> [--provider alias] [--model model] [--profile name] [--workspace DIR] [--force-refresh]\n`)
|
|
312
348
|
process.stdout.write(` page <file.pdf> --page <N> [--workspace DIR] [--force-refresh]\n`)
|
|
313
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`)
|
|
314
352
|
process.stdout.write(`\nLocal config commands:\n`)
|
|
315
|
-
process.stdout.write(` provider set --provider <${
|
|
316
|
-
process.stdout.write(` provider use --provider <${
|
|
353
|
+
process.stdout.write(` provider set --provider <${getProviderSetNames().join("|")}> --api-key <KEY> [--profile name]\n`)
|
|
354
|
+
process.stdout.write(` provider use --provider <${getProviderAliases().join("|")}> [--profile name]\n`)
|
|
317
355
|
process.stdout.write(` provider list [--profile name]\n`)
|
|
318
356
|
process.stdout.write(` model set --model <model-id> [--provider alias] [--profile name]\n`)
|
|
319
357
|
process.stdout.write(` model get [--provider alias] [--profile name]\n`)
|
|
320
358
|
process.stdout.write(` model list [--profile name]\n`)
|
|
359
|
+
process.stdout.write(`\nLocal LLM example (no auth):\n`)
|
|
360
|
+
process.stdout.write(` echo-pdf provider set --provider ollama --api-key \"\"\n`)
|
|
361
|
+
process.stdout.write(` echo-pdf model set --provider ollama --model llava:13b\n`)
|
|
362
|
+
process.stdout.write(` echo-pdf semantic ./sample.pdf --provider ollama\n`)
|
|
321
363
|
}
|
|
322
364
|
|
|
323
365
|
const main = async () => {
|
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProviderType } from "./types.js";
|
|
2
2
|
export interface EchoPdfProviderConfig {
|
|
3
3
|
readonly type: ProviderType;
|
|
4
|
-
readonly apiKeyEnv
|
|
4
|
+
readonly apiKeyEnv?: string;
|
|
5
5
|
readonly baseUrl?: string;
|
|
6
6
|
readonly headers?: Record<string, string>;
|
|
7
7
|
readonly timeoutMs?: number;
|
|
@@ -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
|
}
|
package/dist/provider-client.js
CHANGED
|
@@ -30,7 +30,7 @@ const toAuthHeader = (config, providerAlias, provider, env, runtimeApiKeys) => {
|
|
|
30
30
|
provider,
|
|
31
31
|
runtimeApiKeys,
|
|
32
32
|
});
|
|
33
|
-
return { Authorization: `Bearer ${token}` };
|
|
33
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
34
34
|
};
|
|
35
35
|
const withTimeout = async (url, init, timeoutMs) => {
|
|
36
36
|
const ctrl = new AbortController();
|
package/dist/provider-keys.js
CHANGED
|
@@ -16,6 +16,9 @@ export const runtimeProviderKeyCandidates = (_config, providerAlias, provider) =
|
|
|
16
16
|
return Array.from(new Set([...aliases, ...types]));
|
|
17
17
|
};
|
|
18
18
|
export const resolveProviderApiKey = (input) => {
|
|
19
|
+
const envKey = typeof input.provider.apiKeyEnv === "string" ? input.provider.apiKeyEnv.trim() : "";
|
|
20
|
+
if (!envKey)
|
|
21
|
+
return "";
|
|
19
22
|
const candidates = runtimeProviderKeyCandidates(input.config, input.providerAlias, input.provider);
|
|
20
23
|
for (const candidate of candidates) {
|
|
21
24
|
const value = input.runtimeApiKeys?.[candidate];
|
|
@@ -23,5 +26,5 @@ export const resolveProviderApiKey = (input) => {
|
|
|
23
26
|
return value.trim();
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
return readRequiredEnv(input.env,
|
|
29
|
+
return readRequiredEnv(input.env, envKey);
|
|
27
30
|
};
|
package/dist/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": {
|
|
@@ -37,6 +38,15 @@
|
|
|
37
38
|
"chatCompletionsPath": "/chat/completions",
|
|
38
39
|
"modelsPath": "/models"
|
|
39
40
|
}
|
|
41
|
+
},
|
|
42
|
+
"ollama": {
|
|
43
|
+
"type": "openai-compatible",
|
|
44
|
+
"apiKeyEnv": "",
|
|
45
|
+
"baseUrl": "http://127.0.0.1:11434/v1",
|
|
46
|
+
"endpoints": {
|
|
47
|
+
"chatCompletionsPath": "/chat/completions",
|
|
48
|
+
"modelsPath": "/models"
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
}
|
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": {
|