lacolaco's marginalia

Reading and Developing

ブログに記事要約機能を実装した(Built-in AI on Chrome)

image

このブログに記事の内容を短く要約する機能を実装した。ブラウザの組み込みAI機能を使っているので、2025年12月末の時点ではChromeでのみ動作する。

組み込みAI API

Chromeはブラウザに組み込まれたGemini Nanoモデルを呼び出すAPIを提供している。今回利用したのはSummerizer APIで、Chrome 138から安定版で提供されている。他にもいくつかのAPIがあり、まだOrigin Trial段階のものもある。

Summarizer APIはその名の通り、テキストを与えたら要約してくれるAPIだ。

いまのところ、window.Summarizerオブジェクトを通じてAPIを呼び出すようになっている。このブログでは記事にロケール(ja or en) があり、それぞれのロケールに合わせた言語で要約を出力するよう、次のような設定でSummarizerを呼び出している。

export async function createSummarizer(locale: string): Promise<Summarizer> {
  if (typeof Summarizer === 'undefined') {
    throw new Error('Summarizer API is not available');
  }

  const outputLanguage = locale === 'en' ? 'en' : 'ja';
  const options: SummarizerCreateOptions = {
    type: 'tldr',
    format: 'markdown',
    length: 'medium',
    outputLanguage,
    expectedInputLanguages: [outputLanguage],
  };

  return await Summarizer.create(options);
}

要約にもいくつか種類があり、今回はtldr 形式でmediumサイズの要約を生成している。

image

このあたりの設定値などをTypeScriptで書くための型定義は@types/dom-chromium-aiというパッケージが提供されている。これを読み込むようにすれば問題ない。

要約のストリーミング生成

今回はじめて組み込みAIを使ってみたが、要約の生成はけっこう時間がかかる。完全に完了するまで待って文字列を出していると体験がよくなかったので、ストリーミングで少しずつ表示を更新できるようにした。といってもSummarizer APIがsummarizeStreamingというメソッドを持っているのでそれを使っているだけだ。

/**
 * テキストをストリーミングで要約する
 * @param text 要約対象のテキスト
 * @param options 要約オプション
 * @param onChunk チャンク受信時のコールバック(累積テキストを受け取る)
 */
export async function summarizeTextStream(
  text: string,
  options: SummarizeOptions,
  onChunk: (text: string) => void,
): Promise<void> {
  const { locale, maxLength = DEFAULT_MAX_LENGTH, signal } = options;

  if (!text.trim()) {
    throw new Error('Text is empty');
  }

  const truncatedText = text.slice(0, maxLength);
  const summarizer = await createSummarizer(locale);

  try {
    const stream = summarizer.summarizeStreaming(truncatedText);
    const reader = stream.getReader();

    while (true) {
      // キャンセルされた場合は中断
      if (signal?.aborted) {
        await reader.cancel();
        break;
      }
      const { done, value } = await reader.read();
      if (done) break;
      onChunk(value);
    }
  } finally {
    summarizer.destroy();
  }
}

あとはボタンが押されたときにこの処理を呼び出してチャンクをUIに反映させればよい。けっこうあっさり実装できた。

Feature Detection

この機能が使えるのはまだ限られたブラウザだけなので、非対応のブラウザではUIを表示しないように機能検出を丁寧にしている。APIの有無だけでなく、Summarizer.availability()メソッドで組み込みモデルの利用可否を取得している。

/**
 * Summarizer APIの利用可能状態を確認する
 * @param locale 記事の言語(言語別の利用可能状態を確認)
 * @returns 'available' | 'downloadable' | 'downloading' | 'unavailable' | 'unsupported'
 */
export async function checkSummarizerAvailability(locale?: string): Promise<AvailabilityResult> {
  // Feature Detection: Summarizer APIが存在するか
  if (typeof Summarizer === 'undefined') {
    return 'unsupported';
  }

  try {
    const outputLanguage = locale === 'en' ? 'en' : 'ja';
    const availability = await Summarizer.availability({
      type: 'tldr',
      format: 'markdown',
      length: 'medium',
      outputLanguage,
      expectedInputLanguages: [outputLanguage],
    });
    return availability;
  } catch (error) {
    console.error('Summarizer availability check failed:', error);
    return 'unsupported';
  }
}

Availabilitydownloadable である場合、モデルがまだダウンロードされていない。組み込みAI APIはユースケースごとにチューニングされたモデルが用意されているらしいから、おそらくそのブラウザプロファイルではじめて要約APIを使うときにモデルがダウンロードされるだろう。

モデルが利用可能であればreadyが返される。利用できない場合はunavailableだ。たとえばモデルをダウンロードする容量がディスクに残っていなかったり、マシンパワーが十分じゃなかったりすると返されるようだ。

image

このあたりも考慮して機能が利用可能だと判断できる場合に要約ボタンを表示している。

まとめ

組み込みAI APIを使って記事要約機能を実装したが、想像以上に簡単に実装できた。Feature Detectionもしっかり行えるため、プログレッシブエンハンスメントとして実験的な機能を組み込みやすい。APIもそれなりに使いやすい形にはなってると思うので、他のブラウザでも使えるようになることに期待する。