●SHEETS — Fill with Gemini expands to 28 more languages, so you can build spreadsheets in your own language with natural prompts●ALPHAEVOLVE — Gemini Enterprise makes AlphaEvolve generally available, autonomously discovering optimized code solutions●SPARK — Gemini Spark, a personal AI agent for macOS, organizes folders and runs workflows across Workspace●OMNI — Gemini Omni blends text, photos, and video for high-quality creation, plus custom AI avatars of you●GEMINI35 — Gemini 3.5 Pro slips to July 17 for a full rebuild, introducing a 2M-token context and Deep Think●DEEPTHINK — The 3.5 Pro rebuild targets better math reasoning, SVG scene generation, and image quality●SHEETS — Fill with Gemini expands to 28 more languages, so you can build spreadsheets in your own language with natural prompts●ALPHAEVOLVE — Gemini Enterprise makes AlphaEvolve generally available, autonomously discovering optimized code solutions●SPARK — Gemini Spark, a personal AI agent for macOS, organizes folders and runs workflows across Workspace●OMNI — Gemini Omni blends text, photos, and video for high-quality creation, plus custom AI avatars of you●GEMINI35 — Gemini 3.5 Pro slips to July 17 for a full rebuild, introducing a 2M-token context and Deep Think●DEEPTHINK — The 3.5 Pro rebuild targets better math reasoning, SVG scene generation, and image quality
When a Whole Chapter Vanished From My Long-PDF Summary — Field Notes on Auditing Coverage
Chapter-wise summarize-then-merge works well on long PDFs, but a lost extraction or a merge step can drop an entire chapter while the pipeline finishes without a single error. This walks through catching those silent drops with one number—coverage—and re-running only the chapters that fell out.
The other day I nearly made a call from an AI summary of a 480-page spec: "this product is relaxed about its limits." In reality, sitting roughly in the middle of the document, there was an entire chapter dedicated to constraints and limits. Not a trace of it survived in the summary. The pipeline had run to completion without raising a single error.
The chapter-wise summarize-then-merge pattern works well on long PDFs. But the very shape of it—split into chapters, summarize in parallel—hides a blind spot: when one chapter quietly goes missing, nothing tells you. Here are my field notes on catching that loss with a single number called coverage, and on a self-auditing pipeline that automatically re-runs only the chapters that fell out.
The Summary Finishes Cleanly Even After a Chapter Disappears
Single-shot summarization degrades in an obvious way: the back half gets thin. Chapter splitting fails more insidiously. Because it treats chapters as a set, dropping one element still leaves the remaining elements to compose into perfectly clean prose. The reader sees no gap.
The paths by which a chapter disappears fall into three groups.
Path
What is happening
Typical symptom
Missing outline
Some chapters are not registered in the PDF bookmarks, so they never enter the chapter list
Chapter count is lower than the table of contents. Body text gets absorbed into neighbors
Empty text
The pages are scanned images and extract_text() returns an empty string
A chapter with no body collapses into a one-liner like "the appendix contains various details"
Dissolving in merge
During the final merge, a thin chapter gets dropped by the model
The chapter survives in the per-chapter summary but its claims vanish from the final one
None of these are failures of the model. They are structural failures of the pipeline. That is precisely why a smarter model does not fix them. What needs fixing is the mechanism that measures whether every chapter reached the final artifact—and names the ones that did not.
Put One Number in Place: Coverage
Before stacking up elaborate quality metrics, decide on just one number. Of the source chapters, the fraction whose claims are reflected in the final summary. Call it coverage.
Symbol
Definition
S
The set of chapters extracted from the source PDF (those with non-empty bodies)
R
The set of chapters whose claims can be found reflected in the final summary (R ⊆ S)
Coverage
|R| / |S|. 1.0 means every chapter is represented
The moment coverage drops below 1.0, some chapter failed to reach the final artifact. The important discipline is not to overload this number as an overall quality score. Coverage is only a detector for dropped chapters; the quality of the summary itself is measured separately. Load a single number with too much meaning and its threshold becomes ambiguous, and the check quietly stops mattering in practice.
As a second safety net at the extraction stage, watch each chapter's body length too. A chapter with an extremely short body—say, under a tenth of the overall median—goes onto a warning list as a candidate for the empty-text path. It is a gate that rejects broken input before it ever reaches summarization.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Separates the three quiet paths by which a chapter disappears—missing outline, empty text, and dissolving during merge—by symptom
✦Detects the loss with a single coverage number and re-runs only the chapters below threshold, in a working Python audit pipeline
✦Routes the verification pass to gemini-flash-latest and gemini-embedding-2, keeping audit cost under a tenth of the main summary
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Below is a minimal build that extracts chapters, summarizes them in parallel, then measures coverage at the end and re-runs only the chapters that fell below threshold. The main summary uses a strong reasoning model; the verification pass is routed to a lightweight model and embeddings to keep audit cost down.
# pip install google-genai pypdf numpy# env: GOOGLE_API_KEY="YOUR_API_KEY"import asyncioimport osimport statisticsfrom dataclasses import dataclass, fieldimport numpy as npfrom pypdf import PdfReaderfrom google import genaiclient = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])SUMMARY_MODEL = "gemini-2.5-pro" # chapter + merge summariesVERIFY_MODEL = "gemini-flash-latest" # coverage checks (cheap, fast)EMBED_MODEL = "gemini-embedding-2" # chapter vs. summary proximity@dataclassclass Chapter: idx: int title: str body: str summary: str = "" covered: bool = False notes: list[str] = field(default_factory=list)def extract_chapters(pdf_path: str) -> list[Chapter]: reader = PdfReader(pdf_path) top = [o for o in reader.outline if not isinstance(o, list)] chapters: list[Chapter] = [] for i, entry in enumerate(top): start = reader.get_destination_page_number(entry) end = (reader.get_destination_page_number(top[i + 1]) if i + 1 < len(top) else len(reader.pages)) body = "\n".join(reader.pages[p].extract_text() or "" for p in range(start, end)) chapters.append(Chapter(idx=i, title=entry.title, body=body)) return chaptersdef flag_thin_chapters(chapters: list[Chapter]) -> None: """Catch the empty-text path at the input stage.""" lengths = [len(c.body) for c in chapters if c.body.strip()] if not lengths: return floor = statistics.median(lengths) * 0.1 for c in chapters: if len(c.body.strip()) < floor: c.notes.append(f"body too short ({len(c.body.strip())} chars); suspect extraction gap")
Run flag_thin_chapters at extraction time to mark chapters with thin bodies. Skip this and an empty body flows straight into summarization, after which you can no longer tell "did it drop out, or was there never anything there."
Next come the chapter summaries, the merge, and the coverage check. The merge carries explicit chapter numbers so the final summary can be mechanically reconciled against "which chapters were touched."
async def summarize_chapter(ch: Chapter) -> None: prompt = ( f"The following is the body of Chapter {ch.idx}, '{ch.title}', from a technical document.\n" "Summarize its claims in 3-5 bullet points. Avoid references to other chapters.\n\n" f"Body:\n{ch.body[:120000]}" ) resp = await asyncio.to_thread( client.models.generate_content, model=SUMMARY_MODEL, contents=prompt) ch.summary = resp.text.strip()async def merge_summaries(chapters: list[Chapter]) -> str: joined = "\n\n".join(f"[ch{c.idx}] {c.title}\n{c.summary}" for c in chapters) prompt = ( "Below are per-chapter summaries of one document. Summarize the overall argument in " "400 words, and at the very end list the chapters you referenced as [ch0, ch3, ...].\n\n" f"{joined}" ) resp = await asyncio.to_thread( client.models.generate_content, model=SUMMARY_MODEL, contents=prompt) return resp.text.strip()def embed(texts: list[str]) -> np.ndarray: resp = client.models.embed_content(model=EMBED_MODEL, contents=texts) return np.array([e.values for e in resp.embeddings])def audit_coverage(chapters: list[Chapter], final: str, threshold: float = 0.62) -> float: """Judge coverage by semantic proximity of chapter summaries to the final one.""" live = [c for c in chapters if c.summary] vecs = embed([c.summary for c in live] + [final]) final_vec = vecs[-1] for c, v in zip(live, vecs[:-1]): sim = float(v @ final_vec / (np.linalg.norm(v) * np.linalg.norm(final_vec))) c.covered = sim >= threshold if not c.covered: c.notes.append(f"weak reflection in final summary (similarity {sim:.2f})") covered = sum(c.covered for c in live) return covered / len(live) if live else 0.0
Using embeddings for the coverage check is cheaper and more stable than asking a generative model "is this chapter included?" for each chapter. The similarity threshold of 0.62 is a conservative starting value; run a few real documents, look at the distribution, and tune from there. The optimum shifts by document genre, so do not trust a fixed value.
Re-run Only the Dropped Chapters, By Name
When coverage falls below 1.0, do not redo the whole thing—re-run the merge targeting only the chapters that were not covered. This is where the audit earns its keep. A full re-run spikes both cost and latency, but recovering one dropped chapter is cheap.
async def summarize_pdf(pdf_path: str, max_retries: int = 2) -> dict: chapters = extract_chapters(pdf_path) flag_thin_chapters(chapters) sem = asyncio.Semaphore(4) # conservative concurrency to dodge free-tier 429s async def bounded(c: Chapter): async with sem: await summarize_chapter(c) await asyncio.gather(*(bounded(c) for c in chapters if c.body.strip())) final = await merge_summaries(chapters) coverage = audit_coverage(chapters, final) attempt = 0 while coverage < 1.0 and attempt < max_retries: dropped = [c for c in chapters if c.summary and not c.covered] emphasis = ", ".join(f"Chapter {c.idx} '{c.title}'" for c in dropped) final = await merge_summaries(chapters) + ( f"\n\n(Extra instruction: the claims of {emphasis} must appear in the body.)") coverage = audit_coverage(chapters, final) attempt += 1 return { "coverage": coverage, "dropped": [c.title for c in chapters if c.summary and not c.covered], "thin": [c.title for c in chapters if any("too short" in n for n in c.notes)], "summary": final, }if __name__ == "__main__": report = asyncio.run(summarize_pdf("./spec.pdf")) print(f"coverage: {report['coverage']:.0%}") if report["dropped"]: print("dropped in merge:", report["dropped"]) if report["thin"]: print("suspect extraction:", report["thin"]) print(report["summary"])
The return value is a report rather than a bare number so that "which chapters dropped" and "which chapters look mis-extracted" stay in a human-readable form. Even when the number is green, if chapters sit in thin, I distrust the input itself. The audit exists not to hand down a pass/fail, but to point at what to fix next.
Where to Draw the Thresholds
Once you introduce coverage, the next question is where to stop. In my own runs, I have settled on the following split.
Situation
Handling
Coverage 1.0, no thin chapters
Accept as is
Coverage < 1.0 (dissolved in merge)
Re-merge naming the dropped chapters. Up to two retries
Chapters remain in thin
Redo extraction. Send scanned chapters as multimodal input, page range narrowed
Still below 1.0 after retries
Route to human review. Do not auto-accept the summary
The cost breakdown is worth a look too. Because the verification pass leans on a lightweight model and embeddings, the extra cost of the audit stays under a tenth of the main summary. Re-merging a dropped chapter takes only chapter summaries as input, so that too is cheap. In other words, the design directly mirrors an asymmetry: redoing everything is expensive, but detecting a dropped chapter and picking it back up is cheap.
What I Kept Tripping Over
Building this up, I stepped in the same holes more than once. Worth writing down.
Treat the embedding threshold as one magic number and you will drop chapters the instant the genre changes. Boilerplate-heavy documents like contracts show uniformly high inter-chapter similarity, so the threshold has to rise too, or you get false "everything is included" verdicts. Do not skip the step of running a few documents and looking at the distribution.
Push the merge's extra instruction too hard and the dropped chapter alone grows unnaturally long, throwing off the overall balance. Keep the supplement to "must include," and leave the length spec to the original instruction—that stayed stable.
And the biggest lesson: do not relax just because coverage reads 1.0. "Reflected" and "correctly reflected" are different claims. Coverage only guarantees that nothing has disappeared. Whether the contents are right is, in the end, best settled by leaving a final pass to someone who knows the document.
The Next Step
If you have a single PDF over 200 pages on hand, count its chapters and run it through both the naive single-shot summary and this auditing pipeline. The moment a "vanished chapter" you would never have noticed shows up by name in dropped, the point of keeping this number lands.
Coverage is not a tool for making summaries smarter. It is a tool for refusing to let a summary stay silent when it quietly drops something. Once the losses become visible, the unease of handing long documents to a machine shrinks considerably. As an indie developer running apps solo, I am still tuning this myself, but I hope it helps with a document of yours.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.