puffinparse 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {puffinparse-0.1.4 → puffinparse-0.1.6}/Cargo.lock +5 -5
- {puffinparse-0.1.4 → puffinparse-0.1.6}/Cargo.toml +2 -2
- {puffinparse-0.1.4 → puffinparse-0.1.6}/PKG-INFO +30 -4
- {puffinparse-0.1.4 → puffinparse-0.1.6}/README.md +29 -3
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/bench.rs +139 -2
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/http.rs +1 -1
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/model.rs +181 -16
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/pricing.json +20 -1
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/provider.rs +1 -1
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/anthropic.rs +136 -18
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/gemini.rs +52 -3
- puffinparse-0.1.6/crates/puffinparse-core/src/providers/local.rs +340 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/mod.rs +5 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/openai.rs +10 -3
- puffinparse-0.1.6/crates/puffinparse-core/src/providers/opendocrouter.rs +1332 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/paddleocr.rs +68 -5
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/tesseract.rs +11 -110
- puffinparse-0.1.6/crates/puffinparse-core/src/providers/vllm.rs +843 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/types.rs +54 -5
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/README.md +1 -1
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/anthropic_messages_parse_auto_tool.json +36 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/opendocrouter_get_expanded.json +36 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/opendocrouter_parse_failed.json +21 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/opendocrouter_parse_layout.json +61 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/opendocrouter_parse_partial.json +45 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/paddleocr_vl_layout_parsing.json +59 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/vllm_dots_mocr_chat.json +26 -0
- puffinparse-0.1.6/crates/puffinparse-core/tests/fixtures/vllm_infinity_parser2_chat.json +27 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/pyproject.toml +1 -1
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/jobs.py +3 -2
- puffinparse-0.1.4/crates/puffinparse-core/src/providers/local.rs +0 -162
- {puffinparse-0.1.4 → puffinparse-0.1.6}/LICENSE +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/Cargo.toml +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/README.md +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/bench/tables.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/compat/extend.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/compat/llamaparse.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/compat/mod.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/compat/reducto.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/compat/roundtrip.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/error.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/jobs.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/lib.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/pricing.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/azure.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/datalab.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/docling.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/extend.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/google_documentai.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/landingai.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/llamaparse.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/mathpix.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/mistral.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/reducto.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/textract.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/unstructured.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/upstage.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/providers/vlm.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/router.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/testutil.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/src/util.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/benchmark_datasets.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/anthropic_messages_extract.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/anthropic_messages_parse.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/azure_invoice.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/azure_layout.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/azure_read.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/datalab_convert.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/docling_headings.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/docling_multipage.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/extend_extract_run.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/extend_parse_run.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/extend_parse_run_url.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/extend_parse_run_url_output.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/gemini_extract_invoice.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/gemini_parse_multipage.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/google_documentai_form.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/google_documentai_layout.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/google_documentai_ocr.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/landingai_extract.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/landingai_parse.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/llamaparse_extract_job.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/llamaparse_result_json.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/mathpix_pdf_lines.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/mathpix_text.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/mistral_annotation.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/mistral_ocr.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/mistral_ocr_blocks.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/openai_responses_extract.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/openai_responses_parse.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/paddleocr_layout_parsing.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/paddleocr_ocr.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/reducto_extract.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/reducto_extract_plain.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/reducto_parse.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/reducto_parse_url.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/reducto_parse_url_result.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/rules_text_sparse_note.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/tesseract_headings.tsv +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/textract_detect_text.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/textract_forms.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/textract_layout.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/textract_queries.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/unstructured_elements.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/fixtures/upstage_document_parse.json +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/live.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-core/tests/live_jobs.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-python/Cargo.toml +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/crates/puffinparse-python/src/lib.rs +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/__init__.py +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/_core.pyi +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/exceptions.py +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/main.py +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/py.typed +0 -0
- {puffinparse-0.1.4 → puffinparse-0.1.6}/python/puffinparse/types.py +0 -0
|
@@ -1395,7 +1395,7 @@ dependencies = [
|
|
|
1395
1395
|
|
|
1396
1396
|
[[package]]
|
|
1397
1397
|
name = "puffinparse-cli"
|
|
1398
|
-
version = "0.1.
|
|
1398
|
+
version = "0.1.6"
|
|
1399
1399
|
dependencies = [
|
|
1400
1400
|
"anyhow",
|
|
1401
1401
|
"chrono",
|
|
@@ -1415,7 +1415,7 @@ dependencies = [
|
|
|
1415
1415
|
|
|
1416
1416
|
[[package]]
|
|
1417
1417
|
name = "puffinparse-core"
|
|
1418
|
-
version = "0.1.
|
|
1418
|
+
version = "0.1.6"
|
|
1419
1419
|
dependencies = [
|
|
1420
1420
|
"async-trait",
|
|
1421
1421
|
"bytes",
|
|
@@ -1439,7 +1439,7 @@ dependencies = [
|
|
|
1439
1439
|
|
|
1440
1440
|
[[package]]
|
|
1441
1441
|
name = "puffinparse-node"
|
|
1442
|
-
version = "0.1.
|
|
1442
|
+
version = "0.1.6"
|
|
1443
1443
|
dependencies = [
|
|
1444
1444
|
"bytes",
|
|
1445
1445
|
"napi",
|
|
@@ -1453,7 +1453,7 @@ dependencies = [
|
|
|
1453
1453
|
|
|
1454
1454
|
[[package]]
|
|
1455
1455
|
name = "puffinparse-python"
|
|
1456
|
-
version = "0.1.
|
|
1456
|
+
version = "0.1.6"
|
|
1457
1457
|
dependencies = [
|
|
1458
1458
|
"bytes",
|
|
1459
1459
|
"puffinparse-core",
|
|
@@ -1468,7 +1468,7 @@ dependencies = [
|
|
|
1468
1468
|
|
|
1469
1469
|
[[package]]
|
|
1470
1470
|
name = "puffinparse-server"
|
|
1471
|
-
version = "0.1.
|
|
1471
|
+
version = "0.1.6"
|
|
1472
1472
|
dependencies = [
|
|
1473
1473
|
"axum",
|
|
1474
1474
|
"bytes",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/puffinparse-core", "crates/puffinparse-python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.1.
|
|
6
|
+
version = "0.1.6"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
rust-version = "1.80"
|
|
9
9
|
license = "MIT"
|
|
@@ -15,7 +15,7 @@ keywords = ["ocr", "document-parsing", "pdf", "api", "benchmark"]
|
|
|
15
15
|
categories = ["api-bindings", "text-processing"]
|
|
16
16
|
|
|
17
17
|
[workspace.dependencies]
|
|
18
|
-
puffinparse-core = { path = "crates/puffinparse-core", version = "0.1.
|
|
18
|
+
puffinparse-core = { path = "crates/puffinparse-core", version = "0.1.6" }
|
|
19
19
|
reqwest = { version = "0.13", default-features = false, features = ["json", "multipart", "rustls", "stream", "gzip"] }
|
|
20
20
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "time", "sync"] }
|
|
21
21
|
serde = { version = "1", features = ["derive"] }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puffinparse
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -61,7 +61,7 @@ Two things make switching real rather than aspirational. **Modes**: every call n
|
|
|
61
61
|
|
|
62
62
|
| | |
|
|
63
63
|
|---|---|
|
|
64
|
-
| **Providers (v0.1)** |
|
|
64
|
+
| **Providers (v0.1)** | 20 providers · 79 models · 3 modes. **Live-verified** against the real APIs: [Reducto](https://reducto.ai), [Extend](https://extend.ai), [LlamaParse](https://cloud.llamaindex.ai). **Verified locally**: the self-hosted Tesseract and Docling. **Docs-only** (implemented from the provider's API documentation and tested against fixture payloads, not yet run live): Mistral, Azure, Textract, Gemini, OpenAI, Anthropic, Mathpix, Datalab, Unstructured, Upstage, Landing AI, Google Document AI, OpenDocRouter and the self-hosted PaddleOCR and vLLM presets ([help verify them](https://github.com/ajinkyashejul/puffinparse/issues/10)). [Full table](#model-names) |
|
|
65
65
|
| **Modes** | `parse` (markdown + blocks), `ocr` (plain text + boxes), `extract` (JSON from a schema) |
|
|
66
66
|
| **Core** | Rust (`puffinparse-core`): `reqwest` + `tokio`, no vendor SDKs, `#![forbid(unsafe_code)]` |
|
|
67
67
|
| **SDKs** | Python 3.9+ (sync + async, fully typed) and Node.js / TypeScript ([`js/`](https://github.com/ajinkyashejul/puffinparse/blob/main/js/README.md)), both on the same Rust core |
|
|
@@ -216,8 +216,9 @@ result = puffinparse.retrieve(job) # Job (still pending) or ParseRe
|
|
|
216
216
|
result = puffinparse.handle_webhook(request.json(), model="reducto") # verify the signature first
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
Reducto, Extend and
|
|
220
|
-
where one exists (Extend only has workspace-level webhooks
|
|
219
|
+
Reducto, Extend, LlamaParse and OpenDocRouter support jobs; `webhook_url` maps to each provider's
|
|
220
|
+
per-job webhook where one exists (Extend only has workspace-level webhooks and OpenDocRouter has
|
|
221
|
+
none, so it is rejected there). See
|
|
221
222
|
[`docs/SPEC.md`](https://github.com/ajinkyashejul/puffinparse/blob/main/docs/SPEC.md) §15.
|
|
222
223
|
|
|
223
224
|
### TypeScript / Node.js
|
|
@@ -332,6 +333,8 @@ page per provider.
|
|
|
332
333
|
| `gemini/3.5-flash` | parse, ocr, extract | $0.00945 · $0.00945 · $0.00495 | Gemini 3.5 Flash: frontier Flash generation (GA 2026-05-19) |
|
|
333
334
|
| `gemini/3.5-flash-lite` | parse, ocr, extract | $0.00245 · $0.00245 · $0.0012 | Gemini 3.5 Flash-Lite: low-latency 3.x tier (GA 2026-07-21) |
|
|
334
335
|
| `gemini/3.8-flash` | parse, ocr, extract | $0.004125 · $0.004125 · $0.00225 | Gemini 3.8 Flash: newest Flash model (GA 2026-09-02, introductory pricing) |
|
|
336
|
+
| `gemini/3.8-flash-low` | parse, ocr, extract | $0.004125 · $0.004125 · $0.00225 | Gemini 3.8 Flash with thinkingConfig.thinkingLevel=low (cheaper, faster transcription) |
|
|
337
|
+
| `gemini/3-flash-preview` | parse, ocr, extract | $0.00315 · $0.00315 · $0.00165 | Gemini 3 Flash preview (gemini-3-flash-preview; PDF and image input) |
|
|
335
338
|
|
|
336
339
|
**OpenAI** · `OPENAI_API_KEY` · docs-only — Vision transcription through the Responses API. Per-page prices are estimates, as for Gemini.
|
|
337
340
|
|
|
@@ -341,6 +344,7 @@ page per provider.
|
|
|
341
344
|
| `openai/gpt-5.6-terra` | parse, ocr, extract | $0.0114 · $0.0114 · $0.0114 | OpenAI Responses API, gpt-5.6-terra (balanced capability/price) |
|
|
342
345
|
| `openai/gpt-5.6-sol` | parse, ocr, extract | $0.02 · $0.02 · $0.02 | OpenAI Responses API, gpt-5.6-sol (flagship GPT-5.6) |
|
|
343
346
|
| `openai/gpt-6-astra` | parse, ocr, extract | $0.05 · $0.05 · $0.05 | OpenAI Responses API, gpt-6-astra (most capable, most expensive) |
|
|
347
|
+
| `openai/gpt-6-luna` | parse, ocr, extract | $0.0005 · $0.0005 · $0.0005 | OpenAI Responses API, gpt-6-luna (cheapest GPT-6, image and PDF input) |
|
|
344
348
|
|
|
345
349
|
**Anthropic (Claude)** · `ANTHROPIC_API_KEY` · docs-only — Vision transcription through the Messages API. Per-page prices are estimates.
|
|
346
350
|
|
|
@@ -349,6 +353,8 @@ page per provider.
|
|
|
349
353
|
| `anthropic/claude-sonnet-5` | parse `*`, ocr `*`, extract `*` | $0.01 · $0.01 · $0.01 | Claude Messages API, claude-sonnet-5 (balanced vision transcription) |
|
|
350
354
|
| `anthropic/claude-haiku-4-5` | parse, ocr, extract | $0.005 · $0.005 · $0.005 | Claude Messages API, claude-haiku-4-5 (cheapest, 200K context) |
|
|
351
355
|
| `anthropic/claude-opus-5` | parse, ocr, extract | $0.025 · $0.025 · $0.025 | Claude Messages API, claude-opus-5 (highest accuracy) |
|
|
356
|
+
| `anthropic/claude-opus-5-5` | parse, ocr, extract | $0.02 · $0.02 · $0.02 | Claude Messages API, claude-opus-5-5 (current Opus; tool_choice auto, no forced tool) |
|
|
357
|
+
| `anthropic/claude-haiku-5-5` | parse, ocr, extract | $0.0005 · $0.0005 · $0.0005 | Claude Messages API, claude-haiku-5-5 (cheapest Claude, 1M context) |
|
|
352
358
|
|
|
353
359
|
**Mathpix** · `MATHPIX_APP_KEY` (+ `MATHPIX_APP_ID`) · docs-only — Maths-first OCR: Mathpix Markdown with LaTeX, line and word polygons.
|
|
354
360
|
|
|
@@ -395,13 +401,33 @@ page per provider.
|
|
|
395
401
|
| `google_documentai/form` | parse, ocr, extract `*` | $0.03 · $0.03 · $0.03 | Form Parser processor: paragraphs + tables, entities as extraction |
|
|
396
402
|
| `google_documentai/prebuilt` | parse, ocr, extract | $0.03 · $0.03 · $0.03 | Prebuilt or custom extractor (invoice, W2, ...): entities as extraction |
|
|
397
403
|
|
|
404
|
+
**OpenDocRouter** · `OPEN_DOC_ROUTER_API_KEY` · docs-only — LlamaIndex's hosted router: frontier VLMs and open OCR models behind one endpoint, billed per token at the providers' prices, per-page status and charge, layout boxes on request. Model ids keep the router's own `<vendor>/<model>` after the provider name. Prices here are the site's average charge per page (price version 2026-10-06); `cost_usd` is the actual `charge_usd`.
|
|
405
|
+
|
|
406
|
+
| Model | Modes | List price / page (parse · ocr · extract) | Notes |
|
|
407
|
+
|---|---|---|---|
|
|
408
|
+
| `opendocrouter/google/gemini-3.8-flash-low` | parse `*`, ocr `*` | $0.005908 · $0.005908 · — | Gemini 3.8 Flash (low thinking); the router's quickstart model |
|
|
409
|
+
| `opendocrouter/google/gemini-3-flash` | parse, ocr | $0.019667 · $0.019667 · — | Gemini 3 Flash |
|
|
410
|
+
| `opendocrouter/anthropic/claude-opus-5-5` | parse, ocr | $0.04882 · $0.04882 · — | Claude Opus 5.5 (frontier, most expensive) |
|
|
411
|
+
| `opendocrouter/anthropic/claude-haiku-5-5` | parse, ocr | $0.001225 · $0.001225 · — | Claude Haiku 5.5 |
|
|
412
|
+
| `opendocrouter/openai/gpt-5.6-terra` | parse, ocr | $0.019886 · $0.019886 · — | GPT-5.6 Terra |
|
|
413
|
+
| `opendocrouter/openai/gpt-6-luna` | parse, ocr | $0.000798 · $0.000798 · — | GPT-6 Luna (cheapest frontier model) |
|
|
414
|
+
| `opendocrouter/infly/infinity-parser2-flash` | parse, ocr | $0.004344 · $0.004344 · — | Infinity-Parser2-Flash (open model hosted by the router) |
|
|
415
|
+
| `opendocrouter/opendatalab/mineru2.5-pro` | parse, ocr | $0.000861 · $0.000861 · — | MinerU2.5-Pro (open model hosted by the router) |
|
|
416
|
+
| `opendocrouter/xingchen-agi/teleocr` | parse, ocr | $0.002702 · $0.002702 · — | TeleOCR (open model hosted by the router) |
|
|
417
|
+
| `opendocrouter/rednote-hilab/dots.mocr` | parse, ocr | $0.00397 · $0.00397 · — | dots.mocr (open model hosted by the router) |
|
|
418
|
+
| `opendocrouter/paddlepaddle/paddleocr-vl-1.6` | parse, ocr | $0.002111 · $0.002111 · — | PaddleOCR-VL-1.6 (open model hosted by the router) |
|
|
419
|
+
|
|
398
420
|
**Self-hosted engines** · no key · $0/page — out-of-process: a local binary or a server you run. Tesseract and Docling are verified locally; PaddleOCR is docs-only.
|
|
421
|
+
**Self-hosted engines** · no key · $0/page — out-of-process: a local binary or a server you run. Tesseract and Docling are verified locally; PaddleOCR and vLLM are docs-only.
|
|
399
422
|
|
|
400
423
|
| Model | Modes | List price / page (parse · ocr · extract) | Notes |
|
|
401
424
|
|---|---|---|---|
|
|
402
425
|
| `tesseract/default` | ocr `*` (native), parse `*` | $0 · $0 · — | local `tesseract` binary (`TESSERACT_CMD`), PDFs via `pdftoppm`; word/line boxes + confidences, no layout model; verified locally |
|
|
403
426
|
| `docling/default` | parse `*`, ocr `*` | $0 · $0 · — | your docling-serve (`DOCLING_BASE_URL`): layout, tables, OCR; verified locally |
|
|
404
427
|
| `paddleocr/default` | ocr `*` (native), parse `*` (PP-StructureV3) | $0 · $0 · — | your PaddleOCR serving (`PADDLEOCR_BASE_URL`); docs-only |
|
|
428
|
+
| `paddleocr/vl` | parse, ocr | $0 · $0 · — | your PaddleOCR-VL pipeline serving (`PADDLEOCR_VL_BASE_URL`; PaddleOCR-VL-1.6 by default): layout + 0.9B VLM; docs-only |
|
|
429
|
+
| `vllm/infinity-parser2-flash` | parse `*`, ocr `*` | $0 · $0 · — | `infly/Infinity-Parser2-Flash` on your `vllm serve` (`VLLM_BASE_URL`): page images, layout blocks with boxes; docs-only |
|
|
430
|
+
| `vllm/dots.mocr` | parse, ocr | $0 · $0 · — | `rednote-hilab/dots.mocr` on your `vllm serve` (`VLLM_BASE_URL`): page images, layout blocks with boxes; docs-only |
|
|
405
431
|
|
|
406
432
|
### Keep your Reducto / Extend / LlamaParse code
|
|
407
433
|
|
|
@@ -27,7 +27,7 @@ Two things make switching real rather than aspirational. **Modes**: every call n
|
|
|
27
27
|
|
|
28
28
|
| | |
|
|
29
29
|
|---|---|
|
|
30
|
-
| **Providers (v0.1)** |
|
|
30
|
+
| **Providers (v0.1)** | 20 providers · 79 models · 3 modes. **Live-verified** against the real APIs: [Reducto](https://reducto.ai), [Extend](https://extend.ai), [LlamaParse](https://cloud.llamaindex.ai). **Verified locally**: the self-hosted Tesseract and Docling. **Docs-only** (implemented from the provider's API documentation and tested against fixture payloads, not yet run live): Mistral, Azure, Textract, Gemini, OpenAI, Anthropic, Mathpix, Datalab, Unstructured, Upstage, Landing AI, Google Document AI, OpenDocRouter and the self-hosted PaddleOCR and vLLM presets ([help verify them](https://github.com/ajinkyashejul/puffinparse/issues/10)). [Full table](#model-names) |
|
|
31
31
|
| **Modes** | `parse` (markdown + blocks), `ocr` (plain text + boxes), `extract` (JSON from a schema) |
|
|
32
32
|
| **Core** | Rust (`puffinparse-core`): `reqwest` + `tokio`, no vendor SDKs, `#![forbid(unsafe_code)]` |
|
|
33
33
|
| **SDKs** | Python 3.9+ (sync + async, fully typed) and Node.js / TypeScript ([`js/`](https://github.com/ajinkyashejul/puffinparse/blob/main/js/README.md)), both on the same Rust core |
|
|
@@ -182,8 +182,9 @@ result = puffinparse.retrieve(job) # Job (still pending) or ParseRe
|
|
|
182
182
|
result = puffinparse.handle_webhook(request.json(), model="reducto") # verify the signature first
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
Reducto, Extend and
|
|
186
|
-
where one exists (Extend only has workspace-level webhooks
|
|
185
|
+
Reducto, Extend, LlamaParse and OpenDocRouter support jobs; `webhook_url` maps to each provider's
|
|
186
|
+
per-job webhook where one exists (Extend only has workspace-level webhooks and OpenDocRouter has
|
|
187
|
+
none, so it is rejected there). See
|
|
187
188
|
[`docs/SPEC.md`](https://github.com/ajinkyashejul/puffinparse/blob/main/docs/SPEC.md) §15.
|
|
188
189
|
|
|
189
190
|
### TypeScript / Node.js
|
|
@@ -298,6 +299,8 @@ page per provider.
|
|
|
298
299
|
| `gemini/3.5-flash` | parse, ocr, extract | $0.00945 · $0.00945 · $0.00495 | Gemini 3.5 Flash: frontier Flash generation (GA 2026-05-19) |
|
|
299
300
|
| `gemini/3.5-flash-lite` | parse, ocr, extract | $0.00245 · $0.00245 · $0.0012 | Gemini 3.5 Flash-Lite: low-latency 3.x tier (GA 2026-07-21) |
|
|
300
301
|
| `gemini/3.8-flash` | parse, ocr, extract | $0.004125 · $0.004125 · $0.00225 | Gemini 3.8 Flash: newest Flash model (GA 2026-09-02, introductory pricing) |
|
|
302
|
+
| `gemini/3.8-flash-low` | parse, ocr, extract | $0.004125 · $0.004125 · $0.00225 | Gemini 3.8 Flash with thinkingConfig.thinkingLevel=low (cheaper, faster transcription) |
|
|
303
|
+
| `gemini/3-flash-preview` | parse, ocr, extract | $0.00315 · $0.00315 · $0.00165 | Gemini 3 Flash preview (gemini-3-flash-preview; PDF and image input) |
|
|
301
304
|
|
|
302
305
|
**OpenAI** · `OPENAI_API_KEY` · docs-only — Vision transcription through the Responses API. Per-page prices are estimates, as for Gemini.
|
|
303
306
|
|
|
@@ -307,6 +310,7 @@ page per provider.
|
|
|
307
310
|
| `openai/gpt-5.6-terra` | parse, ocr, extract | $0.0114 · $0.0114 · $0.0114 | OpenAI Responses API, gpt-5.6-terra (balanced capability/price) |
|
|
308
311
|
| `openai/gpt-5.6-sol` | parse, ocr, extract | $0.02 · $0.02 · $0.02 | OpenAI Responses API, gpt-5.6-sol (flagship GPT-5.6) |
|
|
309
312
|
| `openai/gpt-6-astra` | parse, ocr, extract | $0.05 · $0.05 · $0.05 | OpenAI Responses API, gpt-6-astra (most capable, most expensive) |
|
|
313
|
+
| `openai/gpt-6-luna` | parse, ocr, extract | $0.0005 · $0.0005 · $0.0005 | OpenAI Responses API, gpt-6-luna (cheapest GPT-6, image and PDF input) |
|
|
310
314
|
|
|
311
315
|
**Anthropic (Claude)** · `ANTHROPIC_API_KEY` · docs-only — Vision transcription through the Messages API. Per-page prices are estimates.
|
|
312
316
|
|
|
@@ -315,6 +319,8 @@ page per provider.
|
|
|
315
319
|
| `anthropic/claude-sonnet-5` | parse `*`, ocr `*`, extract `*` | $0.01 · $0.01 · $0.01 | Claude Messages API, claude-sonnet-5 (balanced vision transcription) |
|
|
316
320
|
| `anthropic/claude-haiku-4-5` | parse, ocr, extract | $0.005 · $0.005 · $0.005 | Claude Messages API, claude-haiku-4-5 (cheapest, 200K context) |
|
|
317
321
|
| `anthropic/claude-opus-5` | parse, ocr, extract | $0.025 · $0.025 · $0.025 | Claude Messages API, claude-opus-5 (highest accuracy) |
|
|
322
|
+
| `anthropic/claude-opus-5-5` | parse, ocr, extract | $0.02 · $0.02 · $0.02 | Claude Messages API, claude-opus-5-5 (current Opus; tool_choice auto, no forced tool) |
|
|
323
|
+
| `anthropic/claude-haiku-5-5` | parse, ocr, extract | $0.0005 · $0.0005 · $0.0005 | Claude Messages API, claude-haiku-5-5 (cheapest Claude, 1M context) |
|
|
318
324
|
|
|
319
325
|
**Mathpix** · `MATHPIX_APP_KEY` (+ `MATHPIX_APP_ID`) · docs-only — Maths-first OCR: Mathpix Markdown with LaTeX, line and word polygons.
|
|
320
326
|
|
|
@@ -361,13 +367,33 @@ page per provider.
|
|
|
361
367
|
| `google_documentai/form` | parse, ocr, extract `*` | $0.03 · $0.03 · $0.03 | Form Parser processor: paragraphs + tables, entities as extraction |
|
|
362
368
|
| `google_documentai/prebuilt` | parse, ocr, extract | $0.03 · $0.03 · $0.03 | Prebuilt or custom extractor (invoice, W2, ...): entities as extraction |
|
|
363
369
|
|
|
370
|
+
**OpenDocRouter** · `OPEN_DOC_ROUTER_API_KEY` · docs-only — LlamaIndex's hosted router: frontier VLMs and open OCR models behind one endpoint, billed per token at the providers' prices, per-page status and charge, layout boxes on request. Model ids keep the router's own `<vendor>/<model>` after the provider name. Prices here are the site's average charge per page (price version 2026-10-06); `cost_usd` is the actual `charge_usd`.
|
|
371
|
+
|
|
372
|
+
| Model | Modes | List price / page (parse · ocr · extract) | Notes |
|
|
373
|
+
|---|---|---|---|
|
|
374
|
+
| `opendocrouter/google/gemini-3.8-flash-low` | parse `*`, ocr `*` | $0.005908 · $0.005908 · — | Gemini 3.8 Flash (low thinking); the router's quickstart model |
|
|
375
|
+
| `opendocrouter/google/gemini-3-flash` | parse, ocr | $0.019667 · $0.019667 · — | Gemini 3 Flash |
|
|
376
|
+
| `opendocrouter/anthropic/claude-opus-5-5` | parse, ocr | $0.04882 · $0.04882 · — | Claude Opus 5.5 (frontier, most expensive) |
|
|
377
|
+
| `opendocrouter/anthropic/claude-haiku-5-5` | parse, ocr | $0.001225 · $0.001225 · — | Claude Haiku 5.5 |
|
|
378
|
+
| `opendocrouter/openai/gpt-5.6-terra` | parse, ocr | $0.019886 · $0.019886 · — | GPT-5.6 Terra |
|
|
379
|
+
| `opendocrouter/openai/gpt-6-luna` | parse, ocr | $0.000798 · $0.000798 · — | GPT-6 Luna (cheapest frontier model) |
|
|
380
|
+
| `opendocrouter/infly/infinity-parser2-flash` | parse, ocr | $0.004344 · $0.004344 · — | Infinity-Parser2-Flash (open model hosted by the router) |
|
|
381
|
+
| `opendocrouter/opendatalab/mineru2.5-pro` | parse, ocr | $0.000861 · $0.000861 · — | MinerU2.5-Pro (open model hosted by the router) |
|
|
382
|
+
| `opendocrouter/xingchen-agi/teleocr` | parse, ocr | $0.002702 · $0.002702 · — | TeleOCR (open model hosted by the router) |
|
|
383
|
+
| `opendocrouter/rednote-hilab/dots.mocr` | parse, ocr | $0.00397 · $0.00397 · — | dots.mocr (open model hosted by the router) |
|
|
384
|
+
| `opendocrouter/paddlepaddle/paddleocr-vl-1.6` | parse, ocr | $0.002111 · $0.002111 · — | PaddleOCR-VL-1.6 (open model hosted by the router) |
|
|
385
|
+
|
|
364
386
|
**Self-hosted engines** · no key · $0/page — out-of-process: a local binary or a server you run. Tesseract and Docling are verified locally; PaddleOCR is docs-only.
|
|
387
|
+
**Self-hosted engines** · no key · $0/page — out-of-process: a local binary or a server you run. Tesseract and Docling are verified locally; PaddleOCR and vLLM are docs-only.
|
|
365
388
|
|
|
366
389
|
| Model | Modes | List price / page (parse · ocr · extract) | Notes |
|
|
367
390
|
|---|---|---|---|
|
|
368
391
|
| `tesseract/default` | ocr `*` (native), parse `*` | $0 · $0 · — | local `tesseract` binary (`TESSERACT_CMD`), PDFs via `pdftoppm`; word/line boxes + confidences, no layout model; verified locally |
|
|
369
392
|
| `docling/default` | parse `*`, ocr `*` | $0 · $0 · — | your docling-serve (`DOCLING_BASE_URL`): layout, tables, OCR; verified locally |
|
|
370
393
|
| `paddleocr/default` | ocr `*` (native), parse `*` (PP-StructureV3) | $0 · $0 · — | your PaddleOCR serving (`PADDLEOCR_BASE_URL`); docs-only |
|
|
394
|
+
| `paddleocr/vl` | parse, ocr | $0 · $0 · — | your PaddleOCR-VL pipeline serving (`PADDLEOCR_VL_BASE_URL`; PaddleOCR-VL-1.6 by default): layout + 0.9B VLM; docs-only |
|
|
395
|
+
| `vllm/infinity-parser2-flash` | parse `*`, ocr `*` | $0 · $0 · — | `infly/Infinity-Parser2-Flash` on your `vllm serve` (`VLLM_BASE_URL`): page images, layout blocks with boxes; docs-only |
|
|
396
|
+
| `vllm/dots.mocr` | parse, ocr | $0 · $0 · — | `rednote-hilab/dots.mocr` on your `vllm serve` (`VLLM_BASE_URL`): page images, layout blocks with boxes; docs-only |
|
|
371
397
|
|
|
372
398
|
### Keep your Reducto / Extend / LlamaParse code
|
|
373
399
|
|
|
@@ -20,7 +20,10 @@ pub use tables::{extract_tables, teds_grid, Grid};
|
|
|
20
20
|
/// * `1` — initial scorer (pipe tables only, exact rule matching, headline in `char_similarity`).
|
|
21
21
|
/// * `2` — HTML tables, `teds_grid`, punctuation-spacing-insensitive rule matching, fuzzy
|
|
22
22
|
/// `bag_of_sentences`, entity decoding, explicit `Summary::score`.
|
|
23
|
-
|
|
23
|
+
/// * `3` — single `*` / `_` emphasis stripped, inline HTML tags (`<sup>`, `<i>`) no longer split
|
|
24
|
+
/// words, dot leaders collapsed, figure markup and image descriptions dropped from transcript
|
|
25
|
+
/// predictions ([`strip_figures`]), and `table_cell` rules can match the header row.
|
|
26
|
+
pub const SCORER_VERSION: u32 = 3;
|
|
24
27
|
|
|
25
28
|
/// A `bag_of_sentences` sentence counts as present when some window of the prediction is within
|
|
26
29
|
/// this normalised character similarity of it (`1 - edit_distance / |sentence|`).
|
|
@@ -67,6 +70,7 @@ pub fn normalize(s: &str, opts: NormalizeOptions) -> String {
|
|
|
67
70
|
.replace(['\u{2018}', '\u{2019}'], "'")
|
|
68
71
|
.replace(['\u{201C}', '\u{201D}'], "\"")
|
|
69
72
|
.replace(['\u{2013}', '\u{2014}'], "-");
|
|
73
|
+
let s = collapse_dot_leaders(&s);
|
|
70
74
|
let s = if opts.case_insensitive { s.to_lowercase() } else { s };
|
|
71
75
|
let mut out = String::with_capacity(s.len());
|
|
72
76
|
let mut last_space = true;
|
|
@@ -86,6 +90,98 @@ pub fn normalize(s: &str, opts: NormalizeOptions) -> String {
|
|
|
86
90
|
out.trim().to_string()
|
|
87
91
|
}
|
|
88
92
|
|
|
93
|
+
/// Dot leaders (`Chapter 1 ........ 7`, `. . . .`) become one space: whether a parser keeps the
|
|
94
|
+
/// dots of a table of contents says nothing about its reading. Four or more dots, optionally
|
|
95
|
+
/// single-spaced; a three-dot ellipsis is text and stays.
|
|
96
|
+
fn collapse_dot_leaders(s: &str) -> String {
|
|
97
|
+
let chars: Vec<char> = s.chars().collect();
|
|
98
|
+
let mut out = String::with_capacity(s.len());
|
|
99
|
+
let mut i = 0;
|
|
100
|
+
while i < chars.len() {
|
|
101
|
+
if chars[i] == '.' {
|
|
102
|
+
// Walk a run of dots separated by at most one space.
|
|
103
|
+
let (mut j, mut dots, mut end) = (i, 0, i);
|
|
104
|
+
while j < chars.len() {
|
|
105
|
+
if chars[j] == '.' {
|
|
106
|
+
dots += 1;
|
|
107
|
+
j += 1;
|
|
108
|
+
end = j;
|
|
109
|
+
} else if chars[j] == ' ' && chars.get(j + 1) == Some(&'.') {
|
|
110
|
+
j += 1;
|
|
111
|
+
} else {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if dots >= 4 {
|
|
116
|
+
out.push(' ');
|
|
117
|
+
i = end;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
out.push(chars[i]);
|
|
122
|
+
i += 1;
|
|
123
|
+
}
|
|
124
|
+
out
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Figure content a parser *adds*: `<figure>…</figure>` blocks (a description or caption a vision
|
|
128
|
+
/// model wrote) and markdown images (``). Transcript truths carry no figure or
|
|
129
|
+
/// chart content (DP-Bench drops them, as upstream ignores predictions inside figure regions), so
|
|
130
|
+
/// this text is not a reading of the page and is not scored. Tags are matched case-insensitively;
|
|
131
|
+
/// an unclosed `<figure>` runs to the end.
|
|
132
|
+
pub fn strip_figures(s: &str) -> String {
|
|
133
|
+
let lower = s.to_ascii_lowercase();
|
|
134
|
+
let mut out = String::with_capacity(s.len());
|
|
135
|
+
let mut i = 0;
|
|
136
|
+
while let Some(rel) = lower[i..].find("<figure") {
|
|
137
|
+
let start = i + rel;
|
|
138
|
+
// `<figure>` or `<figure ...>`, not `<figures>`.
|
|
139
|
+
let next = lower.as_bytes().get(start + 7).copied();
|
|
140
|
+
if !matches!(next, Some(b'>' | b' ' | b'\t' | b'\n' | b'\r' | b'/')) {
|
|
141
|
+
out.push_str(&s[i..start + 7]);
|
|
142
|
+
i = start + 7;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
out.push_str(&s[i..start]);
|
|
146
|
+
out.push(' ');
|
|
147
|
+
i = match lower[start..].find("</figure>") {
|
|
148
|
+
Some(e) => start + e + "</figure>".len(),
|
|
149
|
+
None => s.len(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
out.push_str(&s[i..]);
|
|
153
|
+
strip_markdown_images(&out)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// `` → nothing (`[` without a following `](…)` is kept as text).
|
|
157
|
+
fn strip_markdown_images(s: &str) -> String {
|
|
158
|
+
let mut out = String::with_capacity(s.len());
|
|
159
|
+
let mut rest = s;
|
|
160
|
+
while let Some(at) = rest.find("![") {
|
|
161
|
+
let after = &rest[at + 2..];
|
|
162
|
+
let image_end = after.find("](").and_then(|close| {
|
|
163
|
+
let alt = &after[..close];
|
|
164
|
+
if alt.contains('\n') {
|
|
165
|
+
return None;
|
|
166
|
+
}
|
|
167
|
+
after[close + 2..].find(')').map(|p| at + 2 + close + 2 + p + 1)
|
|
168
|
+
});
|
|
169
|
+
match image_end {
|
|
170
|
+
Some(end) => {
|
|
171
|
+
out.push_str(&rest[..at]);
|
|
172
|
+
out.push(' ');
|
|
173
|
+
rest = &rest[end..];
|
|
174
|
+
}
|
|
175
|
+
None => {
|
|
176
|
+
out.push_str(&rest[..at + 2]);
|
|
177
|
+
rest = &rest[at + 2..];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
out.push_str(rest);
|
|
182
|
+
out
|
|
183
|
+
}
|
|
184
|
+
|
|
89
185
|
/// Scores for one (prediction, truth) pair. All in `0..=1` unless noted.
|
|
90
186
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
|
|
91
187
|
pub struct Metrics {
|
|
@@ -127,6 +223,7 @@ pub struct Metrics {
|
|
|
127
223
|
|
|
128
224
|
/// Compute all metrics for a prediction vs truth (both raw; normalisation applied internally).
|
|
129
225
|
pub fn score(pred: &str, truth: &str, opts: NormalizeOptions) -> Metrics {
|
|
226
|
+
let pred = &strip_figures(pred);
|
|
130
227
|
let p = normalize(pred, opts);
|
|
131
228
|
let t = normalize(truth, opts);
|
|
132
229
|
let p_chars: Vec<char> = p.chars().collect();
|
|
@@ -918,7 +1015,9 @@ fn check_table_cell(cache: &PredictionCache<'_>, cell: &CellRule, cs: bool, k: u
|
|
|
918
1015
|
}
|
|
919
1016
|
None => None,
|
|
920
1017
|
};
|
|
921
|
-
|
|
1018
|
+
// The header row is a row too: a cell rule may be about a header cell (olmOCR's neighbour
|
|
1019
|
+
// tests are), and a parser that marks the header correctly must not lose that rule.
|
|
1020
|
+
for row in std::iter::once(&table.header).chain(&table.rows) {
|
|
922
1021
|
if !row_matches(row, row_header.as_deref(), k) {
|
|
923
1022
|
continue;
|
|
924
1023
|
}
|
|
@@ -1040,6 +1139,44 @@ pub fn metrics_from_rules(score: &RuleScore) -> Metrics {
|
|
|
1040
1139
|
mod tests {
|
|
1041
1140
|
use super::*;
|
|
1042
1141
|
|
|
1142
|
+
#[test]
|
|
1143
|
+
fn scorer_v3_dot_leaders() {
|
|
1144
|
+
let o = NormalizeOptions::default();
|
|
1145
|
+
assert_eq!(normalize("Intro ........ 7", o), "intro 7");
|
|
1146
|
+
assert_eq!(normalize("Intro . . . . . 7", o), "intro 7");
|
|
1147
|
+
assert_eq!(normalize("Wait... what", o), "wait... what");
|
|
1148
|
+
assert_eq!(score("Intro 7", "Intro .......... 7", o).char_similarity, 1.0);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
#[test]
|
|
1152
|
+
fn scorer_v3_strips_figures_from_transcripts() {
|
|
1153
|
+
assert_eq!(strip_figures("a <figure><caption>A bar chart</caption></figure> b"), "a b");
|
|
1154
|
+
assert_eq!(strip_figures("a <FIGURE id=\"f\">x</Figure> b"), "a b");
|
|
1155
|
+
assert_eq!(strip_figures("<figures> stay"), "<figures> stay");
|
|
1156
|
+
assert_eq!(strip_figures("see  here"), "see here");
|
|
1157
|
+
assert_eq!(strip_figures("[link](x) and  and ![ no close");
|
|
1158
|
+
let o = NormalizeOptions::default();
|
|
1159
|
+
let truth = "Revenue grew.";
|
|
1160
|
+
let pred = "Revenue grew.\n<figure><caption>The chart shows growth</caption></figure>";
|
|
1161
|
+
assert_eq!(score(pred, truth, o).char_similarity, 1.0);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
#[test]
|
|
1165
|
+
fn scorer_v3_emphasis_and_inline_tags() {
|
|
1166
|
+
let o = NormalizeOptions::default();
|
|
1167
|
+
assert_eq!(score("An *italic* word", "An italic word", o).char_similarity, 1.0);
|
|
1168
|
+
assert_eq!(score("the 9<sup>th</sup> day", "the 9th day", o).char_similarity, 1.0);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
#[test]
|
|
1172
|
+
fn scorer_v3_table_cell_can_match_the_header_row() {
|
|
1173
|
+
let pred = "| Name | Value |\n|---|---|\n| a | 1 |";
|
|
1174
|
+
let c = CellRule { value: "Value".into(), row_header: Some("Name".into()), col_header: None };
|
|
1175
|
+
let r = Rule { cell: Some(c), ..rule("hdr", RuleType::TableCell) };
|
|
1176
|
+
let got = score_rules(pred, &[r], NormalizeOptions::default());
|
|
1177
|
+
assert_eq!(got.passed, 1, "{got:?}");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1043
1180
|
#[test]
|
|
1044
1181
|
fn levenshtein_basic() {
|
|
1045
1182
|
let a: Vec<char> = "kitten".chars().collect();
|
|
@@ -106,7 +106,7 @@ where
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
fn is_transient(e: &Error) -> bool {
|
|
109
|
+
pub(crate) fn is_transient(e: &Error) -> bool {
|
|
110
110
|
matches!(e.kind, ErrorKind::RateLimit | ErrorKind::Network)
|
|
111
111
|
|| (e.kind == ErrorKind::Provider && matches!(e.status_code, Some(500 | 502 | 503 | 504)))
|
|
112
112
|
}
|