A resumable pipeline that builds a JSON database of structured records from the scientific literature. It ships configured for ALD process recipes and ALE etch recipes, and an extraction profile system lets you retarget it at any domain (MOF synthesis, CVD, electrolytes, ...) by declaring your own fields and prompts — no code changes. The pipeline:
- harvest — pulls metadata for every journal article matching your query
(default: the active profile's query, e.g.
"atomic layer deposition" OR "atomic layer epitaxy"forald) from OpenAlex (free, no key, covers essentially all DOI-registered articles), with checkpointed cursor pagination. - download — fetches the legal open-access PDF for each paper
(OpenAlex
best_oa_location, then Unpaywall). Anything without a working OA copy goes to a manual queue: you get a clickable HTML/CSV list, download those PDFs through your institution's library access, drop them intodata/manual_inbox/, and the tool files them. - extract — a cheap triage pass ("does this paper report the authors' own ALD experiment?") followed by full recipe extraction (forced structured tool output, validated with pydantic). Units are normalized: °C, s, Torr, Å/cycle, W, nm. Runs on the LLM provider of your choice — Anthropic, OpenAI, Google Gemini, or any local/OpenAI-compatible server (Ollama, LM Studio, vLLM) — switch with one line in the config, no code changes.
- export — writes
ald_recipes.json(per-paper, with provenance), plus flatrecipes_flat.jsonandrecipes_flat.csv.
Nothing is ever lost on errors or Ctrl-C: SQLite is the single source of truth, every unit of work commits independently, all file writes are atomic, and every raw LLM response is cached to disk before parsing — so a crash, rate limit, or parsing bug never re-spends tokens. Re-running any command simply continues from where it stopped.
Requires Python >= 3.11. The simplest way on any OS is the one-click launcher, which builds the environment and starts the app for you. On Windows it needs no execution-policy change:
- Windows — double-click
start-gui-windows.bat - macOS — double-click
start-gui-macos.commandin Finder - Linux — run
./start-gui-linux.shfrom a terminal
(See The dashboard (GUI) below, including the one-time
chmod +x note for macOS and Linux.)
To set the environment up by hand instead:
macOS / Linux (bash/zsh):
python3 -m venv .venv
source .venv/bin/activate
pip install -e . # run from the alpminer source folderWindows (PowerShell): you do not have to activate the environment, and skipping activation avoids the "running scripts is disabled on this system" error entirely. Just call the venv's own executables by path:
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e . # run from the source folder
.\.venv\Scripts\alpminer.exe --version # use alpminer.exe for any commandPrefer typing plain alpminer? You can activate with
.\.venv\Scripts\Activate.ps1; if PowerShell blocks it, allow signed local
scripts for your user once with Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, then activate again. The launcher and the direct-exe commands
above both skip this step.
Confirm it worked with alpminer --version (it should print alpminer 1.0.0).
Troubleshooting: ModuleNotFoundError: No module named 'pydantic_core._pydantic_core' (or a similar error mentioning fitz /
PyMuPDF). This means the virtual environment was created with one Python
version and is now run with another, so its compiled packages no longer match
the interpreter — most often from re-running python -m venv .venv over an
existing .venv after a Python upgrade. Rebuild the environment cleanly
(--clear wipes the stale packages) and reinstall:
py -3 -m venv --clear .venv
.\.venv\Scripts\python.exe -m pip install -e .Everything above is also available without the terminal. This is the easiest way to start the app:
alpminer gui # opens http://127.0.0.1:8642 in your browserOne-click launcher (no terminal needed). Each OS has a double-clickable launcher in the code folder that creates or reuses the virtual environment and then starts the GUI. It rebuilds the environment automatically if the folder was copied to another computer or Python changed, so it keeps working after a move:
- Windows — double-click
start-gui-windows.bat. - macOS — double-click
start-gui-macos.commandin Finder. The first time, you may need to make it executable once:chmod +x start-gui-macos.command(a download or unzip can drop the executable bit). - Linux — run
./start-gui-linux.shfrom a terminal (alsochmod +xit once if needed; double-click-to-run depends on your file manager).
Keep the launcher's window open while you use the app; closing it, or pressing Ctrl-C, stops the server.
On Windows, you can also run the command directly in PowerShell once the venv is activated; if you did not activate it, use the full path instead:
.\.venv\Scripts\alpminer.exe guiRun it in your project folder (or an empty one — it offers to set the
project up). The dashboard has six tabs: Dashboard (live stage bars,
recipe count, quick run), Pipeline (run any stage with its options; a
Stop button safely ends a job — progress commits per paper), Manual queue
(open the next N links as browser tabs, auto-file PDFs as they land, point
the watcher at any folder), Recipes (search, browse, and inspect every
extracted recipe with a DOI link back to the source), Settings
(every alpminer.toml option as a form, plus API keys — keys entered here
live only in the running process and are never written to disk or echoed
back), and About (a guided walkthrough of the whole workflow, from
first setup to exported dataset). A console strip at the bottom streams live logs from whatever is
running. The server binds to 127.0.0.1 only; --port changes the port and
--no-browser skips opening a tab.
alpminer.toml has a provider setting: "anthropic" (default), "openai",
"gemini", or "openai_compatible" (for local/other servers). Only the
matching API key needs to be set. In the GUI Settings tab, pick the provider
from the dropdown; the two model boxes always show the pair used by the
selected provider.
| Anthropic | OpenAI | Gemini | OpenAI-compatible (local/other) | |
|---|---|---|---|---|
| Cost | pay-as-you-go (~$0.02–0.04/paper) | pay-as-you-go | free tier available | free if local (Ollama/LM Studio) |
| Get a key | console.anthropic.com → API Keys | platform.openai.com → API keys | aistudio.google.com/apikey, no credit card | your server's console (none for local) |
| Env var | ANTHROPIC_API_KEY |
OPENAI_API_KEY |
GEMINI_API_KEY |
OPENAI_API_KEY (or set api_key_env) |
| Default models | claude-sonnet-4-6 / claude-haiku-4-5 |
gpt-4o / gpt-4o-mini |
gemini-2.5-flash / gemini-2.5-flash-lite |
whatever you name |
| Notes | reference implementation; most tested | uses api.openai.com |
free tier has a small daily request quota (current limits) — fine for pilots, too small for large campaigns | Ollama, LM Studio, vLLM, DeepSeek — any /v1/chat/completions server with function calling; set base_url under [provider_settings] |
Keys are pasted exactly as your provider shows them — alpminer makes no
assumptions about their format. To use Gemini, set provider = "gemini" and
put your key in the GEMINI_API_KEY environment variable (export GEMINI_API_KEY=... on macOS/Linux, $env:GEMINI_API_KEY = "..." in Windows
PowerShell). For an OpenAI-compatible endpoint:
provider = "openai_compatible"
[provider_settings]
base_url = "http://localhost:11434/v1" # e.g. Ollama; omit for api.openai.com
# api_key_env = "OPENAI_API_KEY" # env var holding the key, if anyFully custom LLMs are plugins: alpminer providers new my_llm scaffolds
llm_providers/my_llm.py in your project with a documented, one-method
contract (call_tool(model, system, user_text, tool, max_tokens) -> dict).
Implement it, set provider = "my_llm", put any settings it needs under
[provider_settings], and the whole pipeline — caching, retries, resume —
works unchanged. alpminer providers list shows what's available.
Everything else (harvest, download, manual queue, export, resumability) is identical regardless of provider.
macOS / Linux (bash/zsh):
mkdir ald-db && cd ald-db
alpminer init --email you@example.edu # writes alpminer.toml + data/
export GEMINI_API_KEY="your-key-here" # or ANTHROPIC_API_KEY / OPENAI_API_KEY
alpminer run --limit 25 # pilot: 25 papers end to end
alpminer status # progress dashboard + token estimateWindows (PowerShell):
mkdir ald-db; cd ald-db
alpminer init --email you@example.edu # writes alpminer.toml + data\
$env:GEMINI_API_KEY = "your-key-here" # or ANTHROPIC_API_KEY / OPENAI_API_KEY
alpminer run --limit 25 # pilot: 25 papers end to end
alpminer status # progress dashboard + token estimatePaste the key exactly as your provider shows it; alpminer makes no assumptions
about its format. The $env: form sets the key for the current PowerShell
window only. To keep it across sessions, run setx GEMINI_API_KEY "your-key-here" once and open a new window (setx does not affect the
window it runs in). The key is read from the environment, never written to
alpminer.toml.
Review data/exports/ald_recipes.json from the pilot, tune the config if
needed, then scale up (these commands are the same in every shell):
alpminer run # or the stages individually:
alpminer harvest # (resumable; safe to interrupt)
alpminer download
alpminer manual list # -> data/exports/manual_queue.html
# ...download those PDFs via the library, save as <paper_id>.pdf
# into data/manual_inbox/, then:
alpminer manual ingest
alpminer extract
alpminer exportEvery command is idempotent — run it as many times as you like; completed work is skipped, failed work is retried.
Publishers block bulk scraping and paywalled downloads violate their terms, so alpminer only auto-downloads legal open-access copies and asks you for the rest. Automating downloads through a university proxy login is the one thing this tool deliberately does not do: publishers detect scripted proxy traffic and respond by suspending access for the whole institution.
The queue is populated as soon as you harvest — every harvested paper is "awaiting a PDF" until a file arrives for it, so you can start dropping PDFs immediately without first running a download pass. Auto-download and manually filed PDFs both pull papers out of the queue as their files land; a paper the auto-downloader has already given up on is flagged so you know it genuinely needs your library access (an untried one just shows "not tried yet").
The manual loop is designed to be fast anyway:
alpminer manual open --n 10 # opens the next 10 DOI links as browser tabs
# ...click Download on each (through your library access); ANY filename works
alpminer manual watch # run in a second terminal: files each PDF the
# moment it lands, matched by the DOI on page 1No renaming is needed: watch/ingest match each PDF to its paper by the
DOI printed on its first page (falling back to an exact title match against
any harvested paper still awaiting a PDF, or to the old <paper_id>.pdf
filename convention, which is still matched first).
Files the browser is still writing (.crdownload, .part) are ignored until
complete; an accidentally saved login page is rejected with a warning and
left in place; a PDF for a paper already in the store is reported as a
duplicate.
Watch can cover several folders at once, and folders outside the project inbox are copy-only — matched PDFs are copied in and your originals stay exactly where they were; unrelated PDFs in shared folders are ignored silently:
alpminer manual watch --dir "C:\Users\you\Downloads" --include-temp--include-temp adds the system temp folder, which catches viewers and
plugins that drop PDF copies there. An honest note on browser internals:
Chrome/Edge's built-in PDF viewer streams into a proprietary cache and does
NOT leave whole .pdf files in temp, so "just viewing" a paper is not
reliably capturable — the dependable one-keystroke flow remains Ctrl+S (or
the download button) into any watched folder, where the watcher files it
within seconds.
Roughly 40–55% of the ALD literature has a legal OA copy, so expect a substantial manual queue if you truly want every article — the pipeline runs fine on the OA subset while you work through it incrementally.
Papers not in OpenAlex. Have a PDF that OpenAlex does not index (a preprint, a book chapter, an internal report)? Add it directly:
alpminer add path/to/paper.pdf # optional: --title "…" --doi 10.x/…
alpminer extract # then process it like any other paperThe DOI and title are read from the PDF when you omit them. In the GUI, the Manual queue tab has an Add a paper not in OpenAlex card that does the same by file path.
Curating the queue (GUI). The Manual queue tab lists every paper still awaiting a PDF, paginated 50 at a time, with a search box (title, DOI, journal, or id) to jump straight to a paper. Each row has an Open button (opens its DOI/landing page) and a Remove button; checkboxes plus Remove selected handle whole pages at once. Removing takes a paper out of the queue: it stops being counted as awaiting a PDF, is skipped by Open next tabs, and is no longer matched by a dropped PDF — but nothing is deleted. Removed papers collect in a Removed papers list at the bottom of the tab, where Restore (or Restore all) brings them back, so removal is always reversible. The same works from the CLI:
alpminer manual remove W4406076005 W4406071753 # take papers out (kept)
alpminer manual restore # list removed papers
alpminer manual restore --all # bring them all backA profile defines what gets extracted: the record fields (name, type, description, limits), the triage and extraction prompts, the default literature query, and the unit conventions. Built-ins:
ald— deposition recipes (30 fields: precursors, timings, GPC, ...)ale— atomic layer etching recipes (27 fields: modification/removal chemistry, EPC, ion energy, synergy, ...)
Switch with one line in alpminer.toml (profile = "ale") or the Settings
tab. To target a new domain — MOF synthesis, perovskites, anything:
alpminer profiles new mof_synthesis # writes profiles/mof_synthesis.tomlEdit the generated file: each [[field]] you declare becomes a slot the LLM
must fill, a validated database column, and a CSV/JSON export column; the
two prompts define relevance and extraction rules in your domain's language.
Project profiles shadow built-ins of the same name, so you can also copy and
tune ald itself. alpminer profiles list shows everything available.
You don't have to hand-edit TOML: the Settings tab has an Extraction rules editor where you change the OpenAlex query, the triage and extraction prompts, and add, remove, or rename the extracted fields (name, type, required, description) directly in the browser. Saving writes a project copy that shadows the built-in, so the 30/27 built-in fields are a starting point, not a limit. Changing fields affects future extractions; reset the project (or start a new folder) if you want a clean slate.
Create a profile under your own name. Two GUI paths, both ending in a
project profile named by you (snake_case; built-in and already-taken names
are refused), written to profiles/<name>.toml, with the project switched to
it:
- From the dropdown: pick + Create new profile… in the Extraction profile selector; a panel asks for the name and whether to start from a blank starter template or a copy of any existing profile (built-in or your own).
- From the editor: tweak the loaded rules and click Save as new profile… to save the edited state under a new name.
The built-ins are never modified, and your named profiles survive a factory reset.
Deleting a profile. Select a custom profile in the dropdown and click Delete this profile… (shown only for profiles you created). The active profile must be switched away from first, and built-ins can never be deleted — a project copy shadowing a built-in's name is reverted via Reset to factory settings instead. Data already extracted with a deleted profile stays in the database.
Restore factory settings. Edited things and want the shipped defaults back? The Settings tab's Reset settings to factory defaults button reverts everything on that tab — provider, models, query, limits, and any GUI-edited extraction rules — to what alpminer ships with, keeping your contact email and data folder. It does not touch harvested papers, PDFs, or recipes; that is the separate Reset project data in the Danger zone.
One project = one profile: the database locks to the first profile used, and
extraction refuses a mismatch with instructions (start a new folder for a
new domain). Exports carry the profile name, its units block, and a
versioned schema tag (alpminer.records.v2). Raw LLM caches from alpminer
1.x reparse unchanged.
| key | default | meaning |
|---|---|---|
email |
— (required) | OpenAlex/Unpaywall polite-pool contact |
query |
'' (→ profile default) |
title+abstract search (quotes = phrase); empty uses the active profile's default query |
from_year / to_year |
unset | publication-year window |
profile |
"ald" |
extraction profile: ald, ale, or your own (see Extraction profiles) |
provider |
"anthropic" |
anthropic, openai, gemini, openai_compatible, or a plugin name |
[models.<provider>] |
shipped pairs | every provider gets the same two slots: extraction (reads full papers) and triage (cheap relevance filter); switching providers never mixes model names |
[provider_settings] |
empty | free-form table passed to openai_compatible/plugin providers (base_url, api_key_env) |
triage_enabled |
true |
disable to send everything to extraction |
max_paper_chars |
250000 | text cap per paper (~62k tokens) |
max_output_tokens |
8000 | raise if a recipe-dense paper truncates |
request_delay_s |
1.0 | politeness delay between HTTP requests |
Models are declared per provider, all providers equal:
[models.anthropic]
extraction = "claude-sonnet-4-6"
triage = "claude-haiku-4-5"
[models.openai_compatible] # your local Ollama/vLLM/LM Studio server
extraction = "llama3.1:70b"
triage = "llama3.1:8b"(Configs from earlier versions that used flat keys like
gemini_extraction_model still load; the next save rewrites them in this
format.) The API key is read from an environment variable only, named after
the provider (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or
whatever api_key_env names), and is never written to disk.
OpenAlex indexes on the order of 80–100k articles matching "atomic layer
deposition". A typical article is ~10–15k input tokens, so full-corpus
extraction is a real budget item (roughly: triage on everything at Haiku
rates + extraction on the ~60–70% that pass triage at Sonnet rates).
alpminer status shows the estimated remaining input tokens at any point,
plus the actual spend so far — every provider response's reported token
usage is accumulated per project (cached re-runs add nothing), so estimate
and reality can be compared as a run progresses. On large corpora,
extract_workers = 3 (Settings or alpminer.toml) runs that many LLM calls
in parallel to cut wall-clock time; keep it modest to respect rate limits.
Use from_year/to_year or a narrower query to carve the corpus into
affordable slices; the DB accumulates across runs, and re-running never
re-pays for anything already cached in data/raw_llm/. On provider = "gemini", the practical ceiling is the free-tier daily request quota rather
than dollars — alpminer extract will simply need to be re-run on
consecutive days to work through a large corpus once that quota is hit.
material (required), technique, metal_precursor(+abbrev),
co_reactant(+abbrev), additional_reactants, substrate, reactor,
carrier_gas, deposition_temperature_c, temperature_window_c,
precursor_source_temperature_c, pulse/purge_metal_s,
pulse/purge_coreactant_s, cycle_sequence, number_of_cycles,
pressure_torr, plasma_power_w, plasma_gas, gpc_angstrom_per_cycle,
gpc_as_reported, film_thickness_nm, crystallinity, application,
notes, evidence_location (section/table pointer for spot-checking),
confidence (0–1). Every exported record also carries source metadata
(paper_id, doi, title, year, journal) plus an ocr boolean:
true means the paper's text came from OCR rather than the PDF's own text
layer, so OCR-sourced values can be filtered or double-checked.
Extraction rules enforced by the prompt: only processes performed by the
authors of that paper (no cited-literature parameters, reviews, or purely
computational work); one recipe per distinct chemistry (parameter sweeps
become a temperature_window_c, not N recipes); unknown values stay null —
never guessed.
Derived GPC. The LLM is told never to infer an unstated value, so a paper
that gives a film thickness and a cycle count but no growth-per-cycle leaves
gpc_angstrom_per_cycle null. After extraction, the pipeline fills that gap
deterministically from film_thickness_nm × 10 ÷ number_of_cycles (nm → Å)
and appends an auto-derived … provenance note so a computed rate is never
mistaken for a reported one; gpc_as_reported stays empty in that case. A GPC
the paper actually reports (directly or via gpc_as_reported) always wins and
is never overwritten. The same rule applies to any custom profile that
declares a rate/thickness/cycles field trio (see PER_CYCLE_DERIVATIONS).
| failure | behavior |
|---|---|
| network drop / 429 / 5xx | exponential-backoff retries, then the item is marked and skipped; batch continues |
| Ctrl-C | clean exit between items; everything completed is committed |
| PDF is an HTML login page | rejected by magic-byte check → manual queue |
| scanned PDF (no text layer) | OCR'd automatically when pip install "alpminer[ocr]" + Tesseract are present; otherwise flagged text: failed in status (not silently dropped) |
| LLM output truncated | marked failed with instruction to raise max_output_tokens |
| LLM returns invalid JSON/fields | raw response already cached; paper marked failed; fix/re-run reparses free |
| extraction failed papers | automatically retried on the next alpminer extract |
pip install -e ".[dev]"
python -m pytest tests/ # offline (network + LLM calls are mocked) except
# one real-browser smoke test, which skips itself
# when no working headless browser is foundFirst public release. ALPminer runs the whole pipeline end to end (harvest, download, manual queue, triage, extraction, export), with:
- Any LLM provider, all equal: Anthropic, OpenAI, Gemini, or a
local/OpenAI-compatible server (Ollama, LM Studio, vLLM), chosen in one
line. Models live in per-provider
[models.<provider>]tables, so switching providers never mixes model names; fully custom providers are plugins. API keys are read from the environment and never written to disk. - Web dashboard: run every stage, curate the manual queue, browse and search recipes, edit extraction rules, and manage profiles from the browser. One-click launchers for Windows, macOS, and Linux set the environment up for you.
- Domain profiles: built-in
aldandale, plus create, copy, or delete your own for any process (each[[field]]is at once the LLM slot, the validation rule, and the export column). - Two-pass extraction: a cheap triage pass filters irrelevant papers, then a stronger model extracts parameters with forced structured output validated by pydantic, normalizes units, and derives growth-per-cycle from thickness and cycle count when a paper omits it.
- OCR fallback for scanned PDFs (inline or deferred), with an
ocrflag on every exported record so OCR-sourced values stay distinguishable. - Built for large corpora: parallel workers, per-paper commits, raw response caching, and real token/cost accounting. Every command is resumable and idempotent, so a crash, rate limit, or Ctrl-C never re-spends tokens or loses progress.
- Portable projects: relative paths, PDF paths that self-heal after a
move, and a
.gitignorethat keeps data and secrets out of the repository.
Before a large run. The live OpenAlex, Unpaywall, and provider calls
follow their documented APIs but are exercised against mocks in the test
suite, so run the --limit 25 pilot first and spot-check
data/exports/ald_recipes.json against a few papers you know well (the
evidence_location field makes this fast) before committing budget to a
large slice.
If you use ALPminer in your research, please cite it. Citation metadata is in
CITATION.cff, and GitHub's "Cite this repository" button
(top-right of the repo page) generates APA and BibTeX from it. Once the
release is archived on Zenodo, please cite the DOI: