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.