DEV Community

Anand B
Anand B

Posted on

Portion Cut: paste your exam portion, read only the pages that matter (EmbeddingGemma, in the browser)

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

What I Built

Every semester goes the same way. The professor posts the exam portion as a block of syllabus text, the textbook is a 400-page PDF, and the night before the exam I am flipping between the two trying to work out which chapters actually count. The book never uses the syllabus's wording, half the topics are spread across three chapters, and at least one line on the portion is not in the book at all. You only find that out after twenty minutes of searching.

Portion Cut does that matching for you. Paste the portion, drop in the textbook PDF, and it tells you which pages to read, grouped by syllabus line, with the section names next to each range. It then builds a smaller PDF with only those pages. When a syllabus line probably is not covered by the book, it says so plainly instead of guessing, so you know to look somewhere else.

It is a tool for studying less, which felt right for Sloth Day. It does not summarise the book or write notes for you. It just stops you reading pages you did not need to read.

Everything runs in the browser tab. The PDF is never uploaded, there is no backend, and there is no API key.

Demo

Live: https://anandb71.github.io/portion-cut/

Add ?demo=1 to try it without the 220 MB model download. Demo mode uses page vectors for a sample book that were computed by the same model with the same settings. It is labelled on the page and cannot build the cut PDF, because there is no PDF loaded.

Home screen in demo mode

Reading list for a Python exam portion

The real run, with the model doing the work in the browser on the full 397-page book:

Real run in progress

Real run results

On that run, Portion Cut said to read 99 of 397 pages, found 7 of the 9 syllabus lines, and flagged the other two (sets and regular expressions) as probably not in the book. The book's table of contents confirms neither is covered. The raw log is in screenshots/browser-real-1.json.

Code

GitHub logo Anandb71 / portion-cut

Paste your exam portion, drop the textbook PDF, read only the pages that matter. EmbeddingGemma runs in the browser.

Portion Cut

Paste your exam portion, drop the textbook PDF, and Portion Cut tells you which pages to read. It also builds a smaller PDF with only those pages, and it says plainly when a syllabus line is probably not in the book at all.

Everything runs in the browser tab. The PDF is never uploaded. There is no backend and no API key.

Live: https://anandb71.github.io/portion-cut/ (add ?demo=1 to try it without downloading the model)

Why an open-weight model, on the device

The matching is done by EmbeddingGemma 300M, Google's open-weight embedding model, using the 4-bit ONNX build from onnx-community/embeddinggemma-300m-ONNX and Transformers.js on WebAssembly.

  • The textbook stays with you. Course PDFs are often shared files you should not be pushing to someone else's server. Here the text is pulled out with pdf.js in the tab, embedded in a Web Worker, and stored only in this browser's IndexedDB.
  • No per-page…

How I Built It

The core is EmbeddingGemma 300M, Google's open-weight embedding model, using the 4-bit ONNX build from onnx-community/embeddinggemma-300m-ONNX and run with Transformers.js on WebAssembly inside a Web Worker.

The pipeline is short:

  1. Read. pdf.js pulls the text out of every page. Blank pages, the table of contents and the index are skipped with simple rules. PDF bookmarks become section page ranges.
  2. Embed. Each page goes through EmbeddingGemma with its document prompt, truncated to 512 tokens and, using the model's Matryoshka training, cut down to 256 dimensions so the per-book cache stays small. This is the slow part. The vectors are saved in IndexedDB, so the second portion for the same book is instant.
  3. Match. Each syllabus line is embedded with the query prompt. Comma lists are split into facets, so List: slicing, comprehensions also searches for List slicing and List comprehensions separately. Otherwise one strong facet would hide the others.
  4. Decide. This part is plain code, not the model. A line is reported as "probably not in this book" when its best page does not stand out from the rest of the book, meaning the best score is less than 3.35 standard deviations above the average page. Otherwise it keeps the pages close to its best score, plus the best pages for each facet, and snaps them to whole bookmarked sections, because nobody reads page 137 on its own.
  5. Cut. pdf-lib assembles the smaller PDF in the tab.

Vite, TypeScript and Vitest. The decision logic in src/match.ts has unit tests, and GitHub Actions runs the tests and deploys to Pages on every push.

Did it actually work?

I wanted a number I could defend, so I measured it on one free textbook, OpenStax's Introduction to Python Programming (397 pages), against two portions copied word for word from a public university Python syllabus. I labelled which sections each line needed by hand, from the book's contents, before running the model. I tuned the settings on the first portion only and kept the second for a held-out check.

Modules 3–5 (tuning) Modules 6–7 (held out)
Pages to read 99 of 397 75 of 397
Needed sections found 18 of 21 12 of 16
Kept pages that are on topic 95% 89%
Missing lines flagged 2 of 2 1 of 1
Lines wrongly flagged as missing 0 0

For comparison, a keyword (BM25) baseline with the same page rules, tuned the same way, found 8 of the 16 needed sections on the held-out portion and did not flag any of the missing lines. Keyword search falls apart exactly where the syllabus and the book use different words, which is most of the time.

To be honest about the limits: this is one book and two portions. The "not in this book" threshold was chosen after looking at scores from both, so those three catches are a fit, not a test. It also misses some sections; 12 of 16 on the held-out portion means you should still glance at the contents page.

Why Does Open Innovation Matter?

The book stays on your machine. Course PDFs are often files a classmate shared, or a scan the department put on a drive. Uploading them to someone else's API is not great. Here the text is extracted, embedded and cached without leaving the tab. The network panel shows nothing going out after the model download.

It costs nothing to run, including at 1 AM. A 400-page book is 400 embedding calls, and every new textbook is another 400. With the weights in the browser that costs me nothing to host and the student nothing to use. A static GitHub Pages site is the whole deployment.

It works offline after the first visit. The model is cached by the browser and a small service worker keeps the app shell, so it still works on hostel Wi-Fi or with no connection at all.

Open weights let me fix things myself. The default 4-bit model file used an operator the WebAssembly runtime does not support, so it would not load. Because the weights are open, I switched to the model_no_gather_q4 variant in the same repository and carried on. With a closed endpoint I would have been waiting on someone else. The same goes for Matryoshka truncation: I could choose 256 dimensions because I control how the vectors are stored.

It is slow, and that is fine. The first read of the full book took about 15 minutes on CPU with four WebAssembly threads. A hosted API would be faster. But it is a one-time cost per book, it runs while you make tea, and every portion after that is instant. Slow and steady, as promised.

The Lazy Payoff

On the sample portion, the reading list covered 99 of 397 pages, so about three quarters of the book could be skipped, and two syllabus lines were flagged before I wasted time hunting for them. On the held-out portion it was 75 pages. I have only tried it on this sample book and syllabus so far.

Prize Categories

  • Best Use of Gemma: EmbeddingGemma 300M does all of the matching, on the device, in the browser.

Top comments (0)