Gemini Nano

Gemini Nano でブラウザ完結のメール下書きツールを作る — Chrome Prompt API ハンズオン

何を作ったか

てちこまツール — メール下書き (端末内 AI) を追加しました。

箇条書きを放り込むと、日本語のビジネスメール本文に整えてくれるだけのツールなのですが、中身は Chrome に組み込まれた Gemini Nano (window.LanguageModel) を呼んでいて、入力も生成結果もブラウザの外には出ていきません。

ChatGPT や Claude に貼りたいけど、

  • 取引先名・顧客名が混ざってる
  • 金額が書いてある
  • 社内ドキュメントの断片が入ってる

みたいな理由で「ちょっと気が引けるな…」ってなって結局自分で書く、というパターンがわりとあります。そこを気軽にさばきたかった、というのが動機です。

Chrome Prompt API を 3 行で

  • Chrome 138 以降にビルトインされた、端末内で動く LLM (= Gemini Nano) への JS API
  • window.LanguageModel 経由で、セッションを作って prompt() / promptStreaming() を呼ぶだけ
  • モデル本体は初回に一度だけダウンロードされて、以降はオフラインでも動く

API キーも課金もなく、入力は一切サーバーに飛びません。そのかわり Chrome 側で一定のハード要件があったり、まだ Origin Trial ベースの機能だったり、と癖はあります。

Chrome 側のセットアップ

ツール側で検出して案内を出していますが、要点だけ書いておきます。

  1. Chrome 138+ にアップデート (chrome://version で確認)
  2. 2 つのフラグを有効化
    • chrome://flags/#optimization-guide-on-device-model → Enabled (まずはこれでOK)
    • chrome://flags/#prompt-api-for-gemini-nano → Enabled
  3. Chrome を再起動
  4. chrome://components を開いて「Optimization Guide On Device Model」の「アップデートを確認」をクリック → モデル本体 (約 1.5〜2.4GB) がダウンロードされる
  5. chrome://on-device-internals でモデル状態やエラー詳細が見られる

ハード要件の目安はストレージの空き 22GB、RAM 16GB、GPU VRAM 4GB 以上あたりです。満たさない端末で unavailable のままになる場合は、フラグ 1 つ目を Enabled BypassPerfRequirement に切り替えるとハード要件を緩められます (ただし性能次第では動作が重くなります)。

chrome://flags で Enables optimization guide on device と Prompt API for Gemini Nano を有効化した画面
要件未達端末で試したときの設定例。ローエンド機では BypassPerfRequirement で救済、通常は単に Enabled で足りる

モバイル Chrome、他社ブラウザでは使えません。2026 年 4 月時点ではまだデスクトップ Chrome 限定です。

実装の主要パーツ

TypeScript + Astro で書いていますが、ポイントはどのフレームワークでも共通です。

1. availability を 4 ステートで見る

LanguageModel.availability() は 'available' | 'downloadable' | 'downloading' | 'unavailable' のどれかを返します。

export type Availability = 'unavailable' | 'downloadable' | 'downloading' | 'available';

export function isSupported(): boolean {
  return typeof globalThis !== 'undefined' && typeof globalThis.LanguageModel !== 'undefined';
}

export async function checkAvailability(): Promise<Availability> {
  if (!isSupported()) return 'unavailable';
  try {
    return await globalThis.LanguageModel!.availability({
      expectedInputs: [{ type: 'text', languages: ['ja'] }],
      expectedOutputs: [{ type: 'text', languages: ['ja'] }],
    });
  } catch {
    return 'unavailable';
  }
}

UI 側はこの 4 ステートにそれぞれ対応する表示を用意します。

  • available : そのまま生成ボタン活性化
  • downloadable : 「モデルを準備する」ボタンを見せて、押されたら create() する
  • downloading : プログレス表示 + 2 秒ごとに availability() をポーリングして状態遷移を検知
  • unavailable : 諦めてもらうか、Chrome 側セットアップ手順を案内

downloading 状態への遷移は別タブでの操作などでも起こり得るので、最初から downloading を受けても慌てない作りにしておくと安全でした。

2. モデルダウンロードは明示クリック起点にする

Chrome の仕様で、モデルのダウンロード開始にはユーザー操作が必要です。ページロード時に自動で create() を呼ぶと弾かれるので、downloadable のときだけボタンを表示して、そのクリックで LanguageModel.create() を呼びます。

monitor オプションで進捗 (0〜1 の ratio) が取れるので、そのまま <progress> に流し込みます。

export async function createSession(
  onDownloadProgress?: (ratio: number) => void,
): Promise<LMSession> {
  if (!isSupported()) throw new Error('この端末では Chrome の AI API が利用できません');
  return globalThis.LanguageModel!.create({
    expectedInputs: [{ type: 'text', languages: ['ja'] }],
    expectedOutputs: [{ type: 'text', languages: ['ja'] }],
    initialPrompts: [{ role: 'system', content: SYSTEM_PROMPT }],
    monitor: onDownloadProgress
      ? (m) => m.addEventListener('downloadprogress', (e) => onDownloadProgress(e.loaded))
      : undefined,
    temperature: 0.6,
    topK: 3,
  });
}

3. システムプロンプトは initialPrompts で固定する

Nano はコンテキストが狭い (6,144 トークンくらい) ので、毎回のユーザー入力にシステム指示を混ぜていると余白を食い潰します。セッション作成時に initialPrompts: [{ role: 'system', content: ... }] で一度だけ渡しておくと、その後の prompt() では指示を省略できて気持ちいいです。

用途 (謝罪・催促・日程調整など) とトーン (丁寧・標準・親しみ) はシステムに固定せず、毎回のユーザープロンプトに差し込むようにしました。こうしておくと、同じセッションを使い回せます。

4. 生成はストリーミング + AbortController

prompt() でも動きますが、体感速度は promptStreaming() の方が断然よいです。返ってくるのは ReadableStream<string> なので、読みながら DOM に追記していくだけで「AI っぽい」出力になります。

export async function draftMail(
  session: LMSession,
  input: MailInput,
  onChunk: (text: string) => void,
  signal?: AbortSignal,
): Promise<string> {
  const stream = session.promptStreaming(buildUserPrompt(input));
  const reader = stream.getReader();
  let full = '';
  try {
    while (true) {
      if (signal?.aborted) throw new DOMException('aborted', 'AbortError');
      const { value, done } = await reader.read();
      if (done) break;
      if (value) {
        full += value;
        onChunk(value);
      }
    }
  } finally {
    reader.releaseLock();
  }
  return full;
}

AbortController を噛ませて「停止」ボタンで途中キャンセルできるようにしておきます。Nano は小さいとはいえ、端末によっては数秒〜十数秒の生成になるので、止められる UI は大事でした。

5. 出力フォーマットは擬似テンプレで縛る

Nano はあまり賢くない (クラウドの大型モデルに比べて) ので、自由に書かせると前置きや Markdown 記法を混ぜてきます。出力フォーマットをユーザープロンプト末尾で明示すると安定しました。

出力フォーマット (厳守):
件名: <件名>

<本文>

--- 箇条書き ---
・来週の定例の議題確認
・資料は事前共有

temperature: 0.6 / topK: 3 で揺れを抑えめ、くらいが自分の肌感ではバランスよかったです。

ハマったところ

availability が downloadable でも create() が失敗する

Chrome の user gesture チェックが入っているので、ページロード直後に自動で create() を呼ぶと NotAllowedError になります。ボタンクリックのハンドラ内で呼ぶようにしたら解消しました。

WSL の Chrome では VRAM 要件で unavailable が返る

開発中に気付いたのですが、WSL 内の Chrome だと GPU 要件を満たさず unavailable になることがありました。実機の Chrome で試す or フラグ optimization-guide-on-device-model を BypassPerfRequirement にしておくと、ハード要件の一部を緩められます。

コンテキストが狭いので長文は苦手

6K トークン程度なので、長い箇条書きを一気に食わせると生成が破綻します。「1 通分を 1 リクエストで」くらいの粒度が安全でした。コンテキスト溢れを UI 側で事前検知するところまではまだ入れていません (TODO)。

日本語の敬語のゆらぎはわりと出る

「お世話になっております」が「お世話になります」になったり、結びの挨拶が微妙だったりはします。丁寧にしたいときは文体を「丁寧」にして、それでも合わなければ生成結果を手直しするのが現実的でした。

使い方

メール下書きツールの画面
用途と文体を選んで箇条書きを入れるだけ

操作は 3 ステップです。

  1. 用途を選ぶ (日程調整 / 問い合わせ返信 / 謝罪 / 催促 / 社外カジュアル)
  2. 文体を選ぶ (標準 / 丁寧 / 親しみ)
  3. 箇条書きを書く → 「メール本文を作成」

件名は空欄なら AI が内容から考えてくれます。自分で案がある場合は入れておくと、それをベースに整えてくれます。

追加の指示で微調整する

用途テンプレで物足りないときは「追加の指示を書く (任意・上級者向け)」を開くと、自由記述のプロンプト欄が出てきます。

  • 結論を先頭に書く
  • 150字以内にまとめる
  • 署名は「てちこま」で締める

みたいな指示を入れておくと、そのまま毎回のプロンプトに織り込まれます。ChatGPT で言うところの「カスタム指示」の超シンプル版くらいの感覚です。

どこにも送信されないのか

要点としては、

  • 生成部分は window.LanguageModel を直接叩いているだけで、自前のバックエンドは経由していません
  • 入力と生成結果は localStorage に再訪時の復元用として保存されるだけです
  • AdSense と Google Analytics は他のページと同条件で入っています (メール本文自体は送られません)

オフライン確認もできて、機内モードにしてもちゃんと生成されます。

今後やりたいこと

  • コンテキスト長の事前チェック (長すぎる箇条書きを UI で警告する)
  • 過去の生成履歴のローカル保存 + 呼び出し
  • 用途テンプレをユーザー側で追加・編集できるようにする
  • 他の「端末内 AI と相性がいい」ネタを足していく

Gemini Nano は「すごいモデル」ではないですが、「機密だからクラウド AI には貼れないな」という場面ではむしろ正解でした。同じくらいの粒度のツール (ログ要約・コミットメッセージ・エラー解説など) を順次試していく予定です。

ブラウザ完結の AI ツールをやってみたい方の参考になれば嬉しいです。