Read it. Question it. Take it with you. An open, versioned JSON format for interactive research papers, plus a viewer designed for a 390 px phone screen.
The source paper stays the main content. Generation adds page-linked glossary entries, study cards, charts backed by original table cells, and section narration. It does not replace the paper with a summary.
Python 3.11 · FastAPI · Pydantic · PyMuPDF · Jinja2 · stdlib SQLite · vanilla JS · vendored Chart.js. All inference uses a hosted OpenAI-compatible endpoint. No GPU, model download, or JavaScript framework is required. GitHub sign-in protects private libraries and paid tools.
Full hosted app: ipdf-tvvn.onrender.com. Upload → personalized generation → study → browser or ElevenLabs listening. The free service has temporary storage; export editions to retain them.
Public static reader: spacenets7.github.io/ipdf.
Open an exported .ipdf.json file or explore the authored interactive sample
(glossary, study cards and browser narration). The site includes a live format
preview, a local JSON file picker and a downloadable sample. Files are
read in the browser. GitHub Pages cannot run FastAPI, PDF ingestion or hosted
generation; use the full app below for those features. API keys stay server-side.
To rebuild the Pages viewer after changing render code, run
python scripts/build_pages.py and commit docs/. Pages publishes main:/docs.
The Week 3 write-up is in SUBMISSION.md, with the video link left for the author to add. SUBMISSION_NOTES.md contains the DEV publishing handoff. The post has not been published to DEV.
python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\Activate.ps1
pip install -r requirements.txt
cp .env.example .env # Windows: Copy-Item .env.example .env
# Set LLM_API_KEY and LLM_MODEL in .env. Do not commit this file.
uvicorn app.main:app --host 127.0.0.1 --port 8000Open iPDF. Upload a text-based PDF or enter an arXiv ID
(e.g. 1706.03762), then choose beginner/intermediate/expert. Creation returns a
job ID immediately; the page polls progress while extraction and generation run
in background threads. Restart the app after changing .env.
Without provider credentials, the app still boots and produces an explicitly labelled source-only edition with original pages and exports. It does not invent learning blocks. Configure the model and submit the paper again to create the annotated edition under a different cache identity.
For a phone on the same network, bind --host 0.0.0.0 and open
http://YOUR_LAPTOP_LAN_IP:8000 on the phone. Allow the port through your laptop's
firewall if needed. Use an HTTPS host for normal remote/mobile access.
| Variable | Default / requirement |
|---|---|
LLM_BASE_URL |
https://router.huggingface.co/v1 |
LLM_API_KEY |
Required for generation, no default |
LLM_MODEL |
Required for generation, no hardcoded model |
DEEP_BASE_URL, DEEP_API_KEY, DEEP_MODEL |
Optional Ultramarine; all three must be set, otherwise use LLM_* |
TTS_PROVIDER |
browser (default) or elevenlabs |
ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID |
Optional hosted speech credentials |
ELEVENLABS_MODEL |
eleven_flash_v2_5 |
FAST_PRICE_IN, FAST_PRICE_OUT |
USD / million input and output tokens (PRICE_IN/PRICE_OUT aliases supported) |
DEEP_PRICE_IN, DEEP_PRICE_OUT |
USD / million tokens for optional DEEP |
TTS_PRICE_PER_1K_CHARS |
USD / 1,000 ElevenLabs characters |
LLM_TIMEOUT, LLM_MAX_TOKENS |
90 seconds / 3,500 output tokens |
LLM_RATE_LIMIT_MAX_WAIT |
60 seconds; respect shorter provider retry delays and stop on longer resets |
DATA_DIR |
data/: SQLite, source PDFs, jobs, provider audio and caches |
MAX_PDF_PAGES |
80; uploads also limited to 25 MB |
MAX_GENERATION_SECTIONS |
20; explicit processing limit, with a visible issue when a paper is longer |
PORT |
8000; the Docker startup command honors the host's $PORT |
Choose an available chat model and exact provider-qualified identifier from
Hugging Face Inference Providers,
or set another hosted compatible endpoint. The app uses the OpenAI Python SDK;
it does not require an OpenAI account. Ultramarine's endpoint/model are supplied
entirely through DEEP_*; no endpoint or model name is embedded in the code.
Optional DEEP handles chart table selection and claim grounding. FAST handles glossary, cards and narration. Incomplete DEEP configuration uses FAST. DEEP transport failures retry and fall back to FAST; schema-invalid outputs are dropped after at most two retries. Actual provider/model metadata accompanies each block. Keys remain on the server and are absent from JSON/HTML exports.
Create a GitHub OAuth App with callback https://YOUR_HOST/auth/github/callback.
Configure GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and APP_BASE_URL on the
server (CLIENT_ID / CLIENT_SECRET aliases are accepted). Local callback URLs
must also be registered with GitHub when testing actual local OAuth. No account
or repository scopes are requested; the app needs only GitHub public identity.
GitHub access tokens are not stored. App sessions use opaque HttpOnly cookies,
server-side token hashes, expiry, PKCE and browser-bound single-use OAuth state.
Upload up to three PDFs at once, then return to Library. Each account's papers, PDFs, audio, jobs, prerequisite maps, reading position and conversations are access-checked on the server. Editions are private until their owner selects Share by link; shared editions allow public reading, not paid tutor use. Downloaded exports are independent copies and cannot be revoked by unsharing.
Preparation continues without a browser while the process runs: learning blocks, a grounded prerequisite map, and cached narration when ElevenLabs is selected. Jobs store their arguments in SQLite and recover interrupted registered tasks on startup, with bounded automatic restart attempts and manual retry controls. This requires the database and uploads to survive. The chosen free Render plan may sleep or clear its temporary disk: overnight readiness and retained account history are not guaranteed. The Library displays that limit. Always-on compute and a persistent disk would be required for dependable unattended overnight use. No paid hosting upgrade was enabled.
The personal tutor uses the selected section, exact source quotations, a bounded conversation history and account-specific concept familiarity. It declines unsupported questions and separately checks supported answers with grounding. Learning paths order prerequisites before dependent concepts and reject cycles. The path currently samples the first 20 sections (1,800 characters each); it is not an exhaustive knowledge graph. Typed questions work everywhere. Optional browser speech input requires microphone permission and browser support; it is not a deployed ElevenLabs realtime Agent. Answers can use ElevenLabs TTS with browser speech fallback. Reader position is account-bound; browser speech resumes at the section boundary, whereas hosted MP3 can resume at a saved time offset.
Defaults: three paper jobs per account/day, twenty tutor jobs per account/day, sixty tutor jobs across the service/day. These complement the provider usage limits below. Counters are local to the database and reset when free hosting loses that database.
Choose a reader level, prior background, goal (understand / implement / critique), and a 5 / 15 / 30-minute explanation-depth target. The target guides generation; it is not a guaranteed audio duration. Every preference is stored in the edition, and generation caches distinguish reader profiles and hosted endpoints.
Study cards can include grounded multiple-choice checks. After a missed answer,
the next narration uses the already-generated simpler script. Quiz attempts,
section-level playback position and speed stay in browser storage. Export or
reset learning history in the reader. This is personal adaptation, not model
training or proof of improved comprehension. python scripts/evaluate_learning.py ipdf-learning.json reports first/latest outcomes for repeated questions; a
controlled evaluation is still needed before claiming learning gains.
Choose browser speech to conserve credits, or ElevenLabs for on-demand cached
MP3 narration. TTS_CHARACTER_BUDGET defaults to 8,000 successful synthesized
characters per database, and MAX_LLM_CALLS_PER_JOB defaults to 60 successful
responses (grounding and repairs included). These are usage controls, not dollar
spending guarantees. MAX_CREATE_JOBS_PER_DAY defaults to 20. Provider-side limits
are authoritative. A free Render restart clears its ephemeral database and these
local counters; do not rely on them as durable account-wide limits.
For Render API deployment, add RENDER_API_KEY to the ignored local .env and
run python scripts/deploy_render.py --create. It selects the free Docker plan,
reuses a matching existing service, and forwards only the app configuration
allowlist. It never sends the Render management key to the app. If the key sees
multiple workspaces, set RENDER_OWNER_ID. Run without --create to inspect status.
flowchart TD
A[PDF upload / arXiv ID] --> O[Orchestrator + background jobs]
O --> I[ingest: PyMuPDF source evidence]
I --> S[page-tagged sections / tables / figures]
S --> O
O --> G[generate: independent hosted plugins]
G --> B[glossary / cards / charts / narration]
B --> V[Pydantic iPDF 0.1 validation]
V --> D[(SQLite JSON + plugin cache)]
D --> R[render: JSON-only mobile viewer]
R --> X[self-contained HTML / raw JSON]
R --> T[on-demand ElevenLabs or browser speech]
app/ingest/, app/generate/, and app/render/ do not import or call one another.
app/orchestrate.py coordinates them. The renderer consumes validated iPDF JSON
only; it cannot invoke an LLM. Shared services are llm.py, extract.py, db.py
and audio.py. The original LazyDigest source is preserved, unmounted, under
app/modules/digest/, with its original tests and UI.
See SPEC.md, ipdf.schema.json and
app/schema.py. Version is exactly "ipdf_version": "0.1".
Every generated block stores its source page, plugin, prompt version and actual model name. Section references and page bounds are validated. Level changes glossary depth, question difficulty and narration style. Cache identity includes paper content hash, level, FAST/DEEP models and prompt versions. Validated plugin outputs are reused without new provider calls.
Chart plans contain row/column indices, never model-generated values. Values
are parsed from the PDF's extracted table grid and independently rechecked by
the document validator. Captions link to from Table N, p.X. Ambiguous grids,
fragmented decimals, uncertainty pairs and unreadable numeric cells are omitted.
Extraction may therefore omit usable tables; it favors source traceability over
filling the page with charts. Raster figures are extracted as images; vector-only
figures may be absent. Scanned/encrypted PDFs are rejected with an actionable
message; OCR is outside this version.
- Original sections stay in page order. Tap an underlined term for a popover; desktop hover also exposes its definition. Save cards to a browser-local deck.
- Study-card answers expand in place. Chart values have an inspectable table. Every learning block links to the corresponding original PDF page.
- Top play reads narration in source order, highlights and scrolls to the current section, and supports previous/next and 0.75×–1.5× speed.
- Walking mode offers large controls and “A little simpler, please”, which
replays the already-generated
simpler_scriptfor the current section. This action never invokes generation. - Media Session handlers provide play/pause/track controls where supported. Reliable phone lock-screen/background behavior depends on the browser and OS; hosted MP3 playback generally has broader support than browser speech. No physical-phone lock-screen test has been claimed.
- ElevenLabs uses its plain
text-to-speech REST API.
Audio is requested only on demand, generated in a background job, and cached by
script + voice + model. Missing keys, quota, authentication and network errors
switch to
window.speechSynthesisand display the fallback. Browser voices vary by device and may themselves use the device vendor's speech service.
Interactive HTML contains the validated JSON, CSS, JavaScript, vendored Chart.js, original PDF bytes, extracted images, and any already-cached hosted audio. It has no CDN dependency. It can be opened independently of this app. Unrequested audio is not synthesized during export; browser speech remains the fallback. Device speech availability determines offline narration support.
Raw JSON is the versioned format for other viewers/tools. It includes source URLs and generated scripts; it does not automatically embed source PDF bytes or uncached audio. Raw server-relative links require the originating server. HTML exports embed the source PDF and turn its page links into a local Blob URL.
Study decks live in localStorage on the current browser; exports include learning blocks, not the reader's browser-local saved/deleted deck state.
docker compose up --build
# Or:
docker build -t ipdf .
docker run --env-file .env -e PORT=8000 -p 8000:8000 -v "$PWD/data:/app/data" ipdfThe container contains one FastAPI app. It downloads no inference weights and
requires no GPU or speech runtime. render.yaml is a Render Blueprint using the
free web-service plan: connect this repo, select the Blueprint, and enter
LLM_API_KEY and LLM_MODEL in Render's environment settings. Its Docker command
binds to 0.0.0.0:$PORT; /health needs no provider calls.
Render's free service has an
ephemeral filesystem and spins down when idle.
SQLite, uploads and generated audio are therefore temporary there. Export the
.ipdf.html file to retain a paper; use a persistent volume on a host that offers
one for durable server storage. Serialized jobs can resume after restart only
when that database and its source files survive. The public Docker service is
deployed on Render. New papers require GitHub sign-in and are private by default;
only explicitly shared editions are publicly readable.
| Route | Result |
|---|---|
POST /ipdf/create |
Multipart pdf or arxiv_id, level; HTTP 202 + job_id |
GET /ipdf/status/{job} |
Persistent queued/running/ready/error state, progress and result |
GET /ipdf/{id} |
Mobile viewer |
GET /ipdf/{id}.json |
Validated raw JSON download |
GET /ipdf/{id}/export |
Self-contained .ipdf.html download |
POST /audio/{id}/{section}?simpler=true |
HTTP 202 + audio job; synthesis only, no LLM |
GET /media/papers/{hash}.pdf |
Stored original PDF |
GET /media/audio/{hash}.mp3 |
Cached hosted audio |
GET /health, GET /spec |
App readiness / human-readable specification |
Create/audio return quickly; extraction and remote provider work run in a bounded two-thread job pool. Job status is persisted in SQLite. Eight pending/running jobs are allowed at once. Source files are capped at 25 MB.
pytest -q # mocked clients only, no model/key needed
python scripts/validate_ingest.py # three real arXiv PDFs, page/table evidence
python scripts/validate_chart_evidence.py # deterministic source-cell checks, no LLM
python scripts/validate_hosted.py # REAL hosted output: one paper, all 3 levels
python scripts/validate_hosted.py --max-sections 1 # explicit smaller hosted smoke test
python scripts/validate_e2e.py # running HTTP app, 3 real PDFs, view + exportsReal validation artifacts are in ignored data/validation/; the portable account
of what was actually checked is VALIDATION.md. The UI fixture
script creates an explicitly labelled mocked document for interaction QA. It is
never counted as hosted generation evidence.
iPDF is independent of a model vendor. Hosted open-weight models can be swapped
by changing LLM_BASE_URL, LLM_API_KEY, and LLM_MODEL, without changing the
format, viewer, prompts' version metadata, or the ingest pipeline. The application
can be self-hosted on an ordinary CPU laptop or web server while inference stays
at the configured hosted provider. Open JSON and self-contained HTML let the
paper outlive a particular service. The source code and schema remain inspectable.
Actual token counts and configured dollar costs are recorded per job and included in JSON provenance. Cached generation adds no provider usage. Set actual rates before measuring costs; zero/unset prices do not mean free hosted generation.
| Real paper | Pages | Source-only HTTP run | Hosted generation cost |
|---|---|---|---|
| Attention Is All You Need | 15 | 5.86 s | Full-paper cost not measured |
| BERT | 16 | 6.06 s | Full-paper cost not measured |
| Deep Residual Learning | 12 | 7.11 s | Full-paper cost not measured |
These runs made no LLM requests and are not AI-quality benchmarks. For a measured
hosted run, cost = (input_tokens × input_rate + output_tokens × output_rate) / 1e6,
plus optional ElevenLabs characters × TTS_PRICE_PER_1K_CHARS / 1000. Grounding and
validation retries are included in recorded token use. Provider billing remains
the authoritative amount. No made-up Ultramarine or ElevenLabs bill is reported.
Subsequent small live checks passed for Groq, Ultramarine and ElevenLabs. A FAST-only generation attempt produced five grounded glossary entries but hit Groq's rate limit before the three-level pipeline completed. See VALIDATION.md for the limits of that check. Browser speech stays the default; ElevenLabs is opt-in and synthesizes only requested segments.
- Better column/heading reconstruction and vector-figure extraction.
- A format conformance suite and independent JSON import/viewer.
- Optional persistent object storage for free-host deployments.
- Physical Android/iOS background and lock-screen playback checks.
- Persistent hosting for reliable overnight preparation; bibliography links.
- Spaced repetition and portable personal study decks.
Na'aman Hirschfeld's validation-retry account describes returning the validation error and rejected payload to the next attempt. Commenters also distinguish formatting repair from repeated semantic mistakes. iPDF uses bounded repair plus separate grounding and numeric-evidence checks. Peter Ndumia's PDF extraction account describes rejecting false table grids before downstream use. These are two implementation accounts, not a claim of broad community consensus.