Structured ecological data extraction and refinement from scientific literature.
EcoExtract automates the extraction of structured data from PDFs using OCR and LLMs. It's domain-agnostic and works with any JSON schema you define.
graph LR
A[PDF Papers] -->|ohseer| B[OCR]
B -->|ecoextract| C[Metadata Extraction]
B -->|ecoextract| D[Data Extraction]
C --> E[SQLite Database]
D --> E
E -.->|optional| F[Refinement]
F -.-> E
E -->|ecoreview| G[Human Review]
G --> H[Validated Data]
style A fill:#e1f5ff
style H fill:#c8e6c9
style G fill:#fff9c4
style E fill:#f0f0f0
| Package | Purpose | Links |
|---|---|---|
| ohseer | OCR processing (Mistral, Tensorlake, Claude) | GitHub |
| ecoextract | AI-powered extraction pipeline | Docs | GitHub |
| ecoreview | Interactive Shiny review app | GitHub |
Quick Links:
- Complete Guide (Installation, workflow, review, accuracy)
- Configuration Guide (Custom schemas and prompts)
- Accuracy Metrics Guide (Understanding accuracy calculations)
Recommended: renv (reproducible, locks dependency versions)
renv::init()
renv::install("n8layman/ohseer") # OCR processing
renv::install("n8layman/ecoextract") # Data extraction
renv::install("n8layman/ecoreview") # Review app (optional)
renv::snapshot()Alternative: pak
pak::pak("n8layman/ohseer")
pak::pak("n8layman/ecoextract")
pak::pak("n8layman/ecoreview") # optionalSee the Complete Guide for troubleshooting.
EcoExtract uses ellmer for LLM interactions and ohseer for OCR.
Required API keys:
- Mistral (OCR, default): https://console.mistral.ai/
- Anthropic Claude (extraction): https://console.anthropic.com/
Optional providers:
- Tensorlake (alternative OCR): https://www.tensorlake.ai/
- Google Gemini (extraction fallback): https://aistudio.google.com/apikey
- OpenAI (embedding-based deduplication): https://platform.openai.com/api-keys
Create a .env file in your project root (make sure it's in .gitignore first!):
ANTHROPIC_API_KEY=your_anthropic_api_key_here
MISTRAL_API_KEY=your_mistral_api_key_here
# Optional providers
TENSORLAKE_API_KEY=your_tensorlake_api_key_here
GOOGLE_API_KEY=your_google_api_key_here
OPENAI_API_KEY=your_openai_api_key_hereThe .env file is automatically loaded when R starts in the project directory. See the Complete Guide for detailed setup instructions.
By default, ecoextract uses anthropic/claude-sonnet-5 for extraction and mistral for OCR. To use different providers, pass the model or ocr_provider parameters to process_documents(). Supported LLM providers: Anthropic (anthropic/), Google Gemini (google_gemini/), OpenAI (openai/), Mistral (mistral/), Groq (groq/).
Note: OpenAI models require schemas to follow OpenAI's structured output rules -- specifically "additionalProperties": false on every object and all properties listed in required. ecoextract sets "additionalProperties": false on every object before sending a schema (except to Gemini, which rejects it); listing properties in required is up to the schema. Schemas generated by init_ecoextract() follow these rules by default. See the Configuration Guide for details.
library(ecoextract)
# Process all PDFs in a folder through the 4-step pipeline:
# OCR -> Metadata -> Extraction -> Refinement (optional)
results <- process_documents(
pdf_path = "path/to/pdfs/",
db_conn = "ecoextract_records.db"
)
# Retrieve your data
records <- get_records()
export_db(filename = "extracted_data.csv")EcoExtract supports tiered model fallback to handle content refusals (e.g., Claude refusing disease/biosecurity papers). Provide a vector of models to try sequentially:
# Single model (default)
process_documents(
pdf_path = "papers/",
model = "anthropic/claude-sonnet-5"
)
# Tiered fallback: try Claude, then Gemini (1M context), then Mistral
process_documents(
pdf_path = "papers/",
model = c(
"anthropic/claude-sonnet-5",
"google_gemini/gemini-2.5-flash",
"mistral/mistral-large-latest"
)
)Audit logging: The database tracks which model succeeded for each step (metadata, extraction, refinement) in *_llm_model columns. All failed attempts with error messages and timestamps are logged in *_log columns for debugging.
API keys: Add keys for fallback providers to your .env:
ANTHROPIC_API_KEY=your_anthropic_key
GOOGLE_API_KEY=your_google_key
OPENAI_API_KEY=your_openai_key
MISTRAL_API_KEY=your_mistral_keyEcoExtract supports multiple OCR providers through ohseer. By default it uses Mistral, but you can switch to Tensorlake or Claude:
# Use default provider (Mistral)
process_documents("papers/")
# Use Tensorlake OCR
process_documents(
pdf_path = "papers/",
ocr_provider = "tensorlake"
)
# Use Claude OCR
process_documents(
pdf_path = "papers/",
ocr_provider = "claude"
)
# Use Claude OCR
process_documents(
pdf_path = "papers/",
ocr_provider = "claude"
)
# OCR fallback: try Mistral, then Tensorlake if Mistral fails
process_documents(
pdf_path = "papers/",
ocr_provider = c("mistral", "tensorlake")
)
# Increase OCR timeout for large documents
process_documents(
pdf_path = "papers/",
ocr_timeout = 300 # 5 minutes
)Provider fallback: When multiple providers are specified, ohseer automatically tries each in order until one succeeds. The OCR provider used for each document is tracked in the ocr_provider column of the documents table.
- Smart skip logic -- Re-running
process_documents()skips completed steps. Forced re-runs automatically invalidate downstream steps. - Parallel processing -- Process multiple documents simultaneously with
workers = 4(requirescrewpackage). - Deduplication -- Three methods:
"llm"(default),"embedding", or"jaccard". - Human review -- Edit, add, and delete records in the ecoreview Shiny app with full audit trail.
- Accuracy metrics -- Calculate detection recall, field precision, F1, and edit severity after review.
See the Complete Guide for details on all features.
EcoExtract is domain-agnostic and works with any JSON schema:
# Create custom config directory with templates
init_ecoextract()
# Edit the generated files:
# - ecoextract/schema.json # Define your data structure
# - ecoextract/extraction_prompt.md # Describe what to extract
# The package automatically uses these files
process_documents("pdfs/", "records.db")Schema requirements: top-level records property (array of objects), each field with type and description, JSON Schema draft-07 format.
Custom metadata: The metadata step extracts bibliographic fields (title, authors, year, DOI, ...) by default. For corpora that are not journal articles, add ecoextract/metadata_schema.json and ecoextract/metadata_prompt.md (or pass metadata_schema_file / metadata_prompt_file to process_documents()) to extract your own document-level fields, or set run_metadata = FALSE to skip the step. New fields become columns in the documents table. The schema's x-record-id-fields names the fields that record IDs are built from: metadata fields, document_id, or file_name (default: first_author_lastname and publication_year, giving Smith_2020_1_r1). List every metadata field in required with nullable types, and keep nullable fields to 16 or fewer so the schema works on older models such as Claude Sonnet 4.6.
See the Configuration Guide for complete details and examples.
When every model in model fails for a step, the step's status (e.g. metadata_status in the results and in the documents table) lists each model's error.
Unexplained schema errors? Try a newer model before changing your schema. Older models enforce much tighter structured-output limits than current ones, and report them as Schema is too complex or another HTTP 400 that reads like a schema problem. The same schema can fail on anthropic/claude-sonnet-4-6 or anthropic/claude-haiku-4-5 and pass on anthropic/claude-sonnet-5. If every model in your fallback list fails with the same schema error, check whether they are all older models. See Unexplained Schema Errors for the measured limits.
Content refusals: Some models refuse papers on sensitive topics (e.g. select agents). Add a model from another provider to model as a fallback (see Model Fallback).
See the Complete Guide for more.
process_documents()- Complete 4-step workflow (OCR -> Metadata -> Extract -> Refine)
init_ecoextract_database()- Initialize database with schemainit_ecoextract()- Create project config directory with template schema and prompts
get_documents()- Query documents and their metadata from databaseget_records()- Query extracted records from databaseget_ocr_markdown()- Get OCR markdown text for a documentget_ocr_pages()- Get OCR markdown for each page, with stored images embeddedget_ocr_html_preview()- Render OCR output with embedded images as HTMLget_db_stats()- Get document and record counts from databaseexport_db()- Export records with metadata to tibble or CSV fileexport_bibtex()- Export document bibliography to BibTeX formatcalculate_accuracy()- Calculate detection recall, field precision, F1, and edit severity after human review
The main branch is protected. All changes must go through a pull request. PRs must pass the full CI suite before merging, which includes:
R CMD CHECK(no errors, no warnings)- Unit and integration tests (
devtools::test()) — integration tests require live API keys and perform real extractions
See CONTRIBUTING.md for development setup and guidelines.
devtools::test() # Run all tests
devtools::check() # Run package checksIntegration tests require API keys in a .env file and perform live extractions against real APIs — they verify API response structure, not content accuracy.
The main branch is protected. All changes must be submitted as a pull request. PRs must pass the full CI suite before merging:
R CMD CHECKwith no errors or warnings- Full test suite including integration tests (requires live API keys configured in CI)
This ensures every merge has been validated against real API behavior, not just mocks.
ecoextract/
├── R/
│ ├── workflow.R # Main process_documents() workflow + skip/cascade logic
│ ├── ocr.R # OCR processing
│ ├── metadata.R # Publication metadata extraction
│ ├── extraction.R # Data extraction functions
│ ├── refinement.R # Data refinement functions
│ ├── deduplication.R # Record deduplication (LLM, embedding, Jaccard)
│ ├── database.R # Database operations
│ ├── bibtex.R # Bibliography export (BibTeX format)
│ ├── getters.R # Data access functions (get_*, export_db, export_bibtex)
│ ├── config_loader.R # Configuration file loading + init_ecoextract()
│ ├── prompts.R # Prompt loading
│ ├── utils.R # Utilities
│ ├── config.R # Package configuration
│ └── ecoextract-package.R # Package metadata
├── inst/
│ ├── extdata/ # Schema files
│ │ ├── schema.json
│ │ └── metadata_schema.json
│ └── prompts/ # System prompts
│ ├── extraction_prompt.md
│ ├── extraction_context.md
│ ├── metadata_prompt.md
│ ├── metadata_context.md
│ ├── refinement_prompt.md
│ ├── refinement_context.md
│ └── deduplication_prompt.md
├── tests/testthat/ # Tests
├── vignettes/ # Package vignettes
├── DESCRIPTION
├── NAMESPACE
├── CONTRIBUTING.md # Development guide
└── README.md
ellmer- Structured LLM outputsohseer- OCR processingdplyr- Data manipulationDBI&RSQLite- Database operationsjsonlite- JSON handlingglue- String interpolationstringr&stringi- String manipulationdigest- Hashingtidyllm- LLM deduplication
- Tensorlake - OCR processing (via ohseer)
- Anthropic Claude / OpenAI / other LLM providers - Data extraction and refinement (via ellmer)
GPL (>= 3)