Skip to main content

The Summarizer API

So. The Summarizer API is the one you reach for when the job is dead simple: long text in, short text out. You could ask the raw Prompt API to "summarize this in three bullets" and hope it holds the shape — but this is the same Gemini Nano running a task-specific LoRA adapter and a decoder config pointed at exactly that one task, so it holds. Key points, a TL;DR, a headline, a teaser. By the end of this page you'll gate it on availability(), stream a summary a few words at a time, and steer its tone with a single line of sharedContext.

What you'll build​

  • Gate Summarizer.create() behind an availability() check and a download monitor.
  • Pick the output shape with type, format, and length — key-points, tldr, teaser, or headline.
  • Summarize in one call with summarize(), or stream deltas with summarizeStreaming() and append them.
  • Frame every summary in your domain with sharedContext, and refine a single input with per-call context.
  • Survive a QuotaExceededError on very long input, and destroy() the summarizer on teardown.
Prerequisites

Desktop Chrome with built-in AI switched on. If Summarizer isn't on self yet, work through Setup & the availability lifecycle first, and keep the compatibility matrix handy for what's stable where. This page assumes you already know the four availability states — it won't re-teach them.

Create a summarizer​

Same four-step loop as everything else in this course: ask if it's there, create it, use it, tear it down. Feature-detect the Summarizer global, check availability(), then create() with the shape you want. Summarizer went stable in Chrome 138 and needs no flag on current builds — the old #summarization-api-for-gemini-nano toggle only matters on ancient ones. The model downloads on the first create(), the same multi-GB one-time hit as the Prompt API, so wire the monitor from the start.

demo.js
if (typeof Summarizer === 'undefined') {
// No built-in Summarizer here — degrade gracefully (see lesson 2).
}

const status = await Summarizer.availability();
// "unavailable" | "downloadable" | "downloading" | "available"

// Starting the model download needs a user gesture, so run create() from a click
// handler (or guard on navigator.userActivation.isActive when it's downloadable).
if (status !== 'unavailable') {
const summarizer = await Summarizer.create({
type: 'key-points', // 'key-points' | 'tldr' | 'teaser' | 'headline'
format: 'markdown', // 'markdown' | 'plain-text'
length: 'medium', // 'short' | 'medium' | 'long'
monitor(m) {
m.addEventListener('downloadprogress', (e) => {
// e.loaded is a 0..1 fraction; e.total exists and is always 1.
console.log(`Downloading model… ${Math.round(e.loaded * 100)}%`);
});
},
});

const summary = await summarizer.summarize('… long article text …');
console.log(summary);
summarizer.destroy();
}

First create() on a cold machine blocks on that download. That's the whole reason the monitor goes in on line one, not bolted on later.

Pick a type and a length​

Four shapes, one size knob. type decides what the summary is; length decides how much of it you get. Don't overthink it — pick the shape that matches where the text is going.

typeWhat you get
key-pointsA bulleted list of the main claims. The default, and the one you'll reach for most.
tldrA sentence or two — the classic above-the-fold summary.
teaserA hook that makes someone want to read the full thing.
headlineOne line, news-desk cadence.

Here's the one that trips people: the TL;DR type is spelled 'tldr' — no semicolon. Not 'tl;dr', not 'tl-dr'. Write the semicolon and create() throws a TypeError — 'tl;dr' is not a valid SummarizerType — before you ever see a summary. Copy it from the docs, not from memory.

demo.js
// A one-line headline for a news card:
const headliner = await Summarizer.create({ type: 'headline', length: 'short' });

const tldr = await Summarizer.create({ type: 'tldr', length: 'short' }); // no semicolon

// Roomy key-points for a docs page:
const notes = await Summarizer.create({
type: 'key-points',
format: 'markdown',
length: 'long',
});

Four more knobs ride on create() when you need them: a preference performance hint ('auto' | 'speed' | 'capability'), plus the language tags expectedInputLanguages, expectedContextLanguages, and outputLanguage — BCP-47 codes that tell the model what's coming in and what you want back. Ask for a language combo the build can't serve and create() rejects with a NotSupportedError. Whatever you pass here, pass the same to availability() so the check matches what you'll actually create.

Summarize once, or stream the deltas​

Two ways out, same as the Prompt API. summarize() hands you the whole summary when it's done — one await, one string. summarizeStreaming() hands you a ReadableStream you loop over with for await, and here's the part that bites: the chunks are deltas, not the full summary each time. Each chunk is the next slice.

Append. Don't replace.

demo.js
// Whole summary at once:
const summary = await summarizer.summarize(article);

// Or stream incremental deltas and append them:
let text = '';
const stream = summarizer.summarizeStreaming(article);
for await (const chunk of stream) {
text += chunk; // deltas — append, never replace
render(text);
}

Frame it with sharedContext​

This is where the Summarizer stops being generic. sharedContext is a string you set once at create() that rides along on every summarize() call on that instance — a system-prompt-level frame telling the model what this text is and what to protect. Same article, different frame, different summary. When one specific input needs its own steer, pass a per-call context and skip building a whole new instance.

demo.js
const clinical = await Summarizer.create({
type: 'key-points',
sharedContext: 'A section of a medical journal article. Preserve drug names and dosages exactly.',
});

// A per-call context refines one specific input without a new instance:
const summary = await clinical.summarize(caseReport, {
context: 'Reader is a triage nurse skimming for red flags.',
});

Without it, the summary is correct and lifeless — the kind of bullet list that reads the same whether it's compressing a Stripe incident report, a Notion meeting note, or a Postgres post-mortem. With it, the output starts sounding like it belongs in your product. One string. Load-bearing.

Destroy the summarizer​

A live summarizer pins GPU memory the same way a LanguageModel session does, and Nano is not small. Leave a fistful of them hanging and you'll starve the next feature that asks the model for anything. So destroy() when you're done, and wire it to teardown so a closed tab doesn't leak.

Free it.

demo.js
summarizer.destroy();

// Wire it to teardown so a closed tab doesn't leak GPU memory:
window.addEventListener('beforeunload', () => summarizer?.destroy());

// Summarizing after destroy() throws AbortError — create a new one instead.
Try it

Run it locally: open 06-summarizer/index.html from the chrome-ai-course repo in desktop Chrome. Or use the hosted demo: on-device summarizer (with the API walkthrough).

Expected: you paste text, pick a type and length, and the summary streams in a few words at a time; switch the type to headline and re-run to watch the same text collapse to a single line.

Requires: desktop Chrome with Gemini Nano available — see Setup & the availability lifecycle.

Gotchas & troubleshooting​

Most of these are the Prompt API's gotchas with the serial numbers filed off: you didn't wait for the download, you overran the quota, or you treated a streaming delta like a snapshot.

create() never resolves

Symptom: the first create() hangs with no error. Cause: the multi-GB model is downloading and you didn't wire a monitor, so there's nothing to wait on. Fix: pass monitor(m) and read e.loaded (a 0..1 fraction) for progress. create() resolves when the download finishes.

create() throws a TypeError

Symptom: create() throws before you hold a summarizer. Cause: an invalid enum value — usually type: 'tl;dr' (with the semicolon) instead of 'tldr'. Fix: use the exact enum values — 'tldr' has no semicolon — and copy them from the docs, not from memory.

create() rejects with NotSupportedError

Symptom: create() rejects with NotSupportedError. Cause: a language combo the build can't serve — an expectedInputLanguages / expectedContextLanguages / outputLanguage tag it doesn't support. (A device that can't run built-in AI at all reports availability() === 'unavailable', not this.) Fix: probe the exact config with availability({ type, format, length, expectedInputLanguages }) before you commit to it, and pass availability() the same options as create().

QuotaExceededError on a long document

Symptom: summarize() rejects with QuotaExceededError. Cause: the input is longer than the model can take in one pass. Fix: chunk it — split the document into sections, summarize each, then summarize the summaries.

The streamed summary shows only the last few words

Symptom: the output flickers and ends on a fragment. Cause: you're replacing the output with each chunk, but summarizeStreaming() yields deltas, not the full summary so far. Fix: accumulate — text += chunk — then render the running text.

AbortError when you summarize

Symptom: summarize() throws AbortError. Cause: you already called destroy() on that summarizer. Fix: a destroyed summarizer is gone — create a new one. And always destroy() on teardown so orphaned instances don't exhaust GPU memory.

Recap​

  • Summarizer.availability() gates create(); wire a monitor for the first-run download.
  • type, format, and length set the shape — and the TL;DR type is spelled 'tldr', no semicolon.
  • summarize() returns the whole thing; summarizeStreaming() yields deltas you append.
  • sharedContext frames every call on the instance; per-call context refines a single input.
  • destroy() frees the model, and a destroyed summarizer doesn't come back.

Everyone else is paying a server to shorten a sentence.

Next steps​


Next: Translator + Language Detector.