pdf2notes
Turn a page range of a PDF book into descriptive, review-ready study notes (Markdown) — ready to drop into Obsidian or import into Notion.
Works with any OpenAI-compatible chat completion API: NVIDIA NIM
(integrate.api.nvidia.com), OpenAI, Groq, Together AI, OpenRouter, or a
local server (Ollama, vLLM, LM Studio). You bring your own API key.
Setup
pip install -r requirements.txt
Get an API key (NVIDIA NIM example — free tier available)
- Go to https://build.nvidia.com
- Sign in, pick a model (e.g.
meta/llama-3.1-70b-instruct), click "Get API Key" - Export it:
export NVIDIA_API_KEY="nvapi-xxxxxxxxxxxxxxxx"
Any other OpenAI-compatible provider works the same way — just pass
--base-url and --api-key-env for that provider.
Usage
python pdf2notes.py --pdf book.pdf --pages 120-180 --api-key-env NVIDIA_API_KEY
This writes book_notes_120-180.md in the current folder. Requests run
concurrently and are throttled to a requests-per-minute cap, so a 100-page
range finishes in a couple of minutes instead of an hour.
If the run is interrupted, hits a persistent error, or you just Ctrl-C it, rerun the exact same command — chunks already saved in the output file are detected and skipped, so you only pay for/wait on what's missing.
Common options
| Flag | Meaning | Default |
|---|---|---|
--pdf |
Path to the source PDF | required |
--pages |
Page range, e.g. 120-180 (1-indexed, inclusive) |
required |
--output |
Output .md path |
<pdf-name>_notes_<range>.md |
--style |
descriptive, cornell, qa, or outline |
descriptive |
--chunk-chars |
Max source characters sent per API call | 6000 |
--concurrency |
Max simultaneous API requests | 5 |
--rpm |
Max API requests per minute (across all workers) | 40 |
--model |
Model name as your provider expects it | nvidia/llama-3.3-nemotron-super-49b-v1.5 |
--base-url |
OpenAI-compatible API base URL | NVIDIA NIM endpoint |
--api-key-env |
Env var holding your key | NVIDIA_API_KEY |
Tune --rpm to your actual plan. NVIDIA's free tier, OpenAI's free/low
tiers, etc. all cap requests per minute — check your provider's dashboard
and set --rpm a bit under that. Setting it too high just means the tool
eats a 429 and backs off; setting --concurrency too high does the same
without helping speed once you're rpm-bound.
Model IDs get retired. NVIDIA (and other providers) periodically pull
old model IDs from the catalog — you'll get an HTTP 404/410 if that
happens. The tool detects this as non-retryable and fails immediately with
the error instead of burning your rate-limit budget retrying a request
that will never succeed. Check https://build.nvidia.com/models for current
IDs and pass the right one via --model.
Examples
Using OpenAI instead of NVIDIA:
export OPENAI_API_KEY="sk-..."
python pdf2notes.py --pdf book.pdf --pages 1-50 \
--base-url https://api.openai.com/v1 \
--model gpt-4o-mini \
--api-key-env OPENAI_API_KEY
Q&A flashcard-style notes instead of descriptive prose:
python pdf2notes.py --pdf book.pdf --pages 200-260 --style qa
Notes on how it works
- Extracts text from the given page range with
pdfplumber. - Groups pages into chunks (default ~6000 chars each) so each API call covers a coherent span without blowing past context limits.
- Sends each chunk to the model with a prompt tuned to produce learnable notes, not a shortened summary — definitions, examples, and important details are preserved and explained.
- Stitches everything into one Markdown file with YAML frontmatter (title, source, page range, style, timestamp) so it's ready to file straight into Obsidian, or import into Notion via Import → Markdown.
Limitations
- Scanned/image-only PDFs won't extract text — OCR the PDF first (e.g.
with
pytesseract+pdf2image), then run this tool on the OCR'd version. - Very dense chunking settings (
--chunk-charstoo high) can exceed your model's context window — 6000–8000 is a safe default for most models. - Quality depends on the model you point it at — bigger instruction-tuned models produce noticeably better notes than small ones.
- Memory: page text is extracted one page at a time and each page's
internal pdfplumber cache (fonts, layout objects) is flushed immediately,
so memory stays roughly proportional to a chunk's text, not the whole
page range. For genuinely huge ranges (500+ pages in one run), consider
splitting into a couple of
--pagescalls instead of one giant one. - A crash/segfault message after "Done.": if you see a SIGSEGV or
similar right as the process exits but after your notes file is
already complete and readable, it's happening during Python interpreter
teardown, not during note generation — a known quirk of some PDF C
extensions (e.g. pypdfium2, used internally by pdfplumber) not tearing
down cleanly at exit. It doesn't affect your output file. If you want to
suppress the noise, run with
python pdf2notes.py ... ; truein a script, or ignore the exit code.
Metadata
Release files for pdf2notes 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pdf2notes-1.0.2.tar.gz | 12.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pdf2notes-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.3 kB
Release files / pdf2notes-1.0.2.tar.gz
| Download URL | pdf2notes-1.0.2.tar.gz |
|---|---|
| Size | 12.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2be04d22c11b15ff089aa8a1c46162967bba798ff097ab0733c3d4ef2b982e74
|
|
BLAKE2b-256 checksum How to use checksums |
92a707947e57b64c56b841a3ecc2e538a2d4a4c7cd8cfc156185edf46c9af8e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / pdf2notes-1.0.2-py3-none-any.whl
| Download URL | pdf2notes-1.0.2-py3-none-any.whl |
|---|---|
| Size | 11.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ac4fccf29c00d136a570dbd4ddd63f044510a614a66a4086e1dc05679c0e4330
|
|
BLAKE2b-256 checksum How to use checksums |
0025541dca15f908ffd33daa8389b40a33f1af5328e40fa7b98e0d3d82f5c7e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|