オフラインでも要約が動けば、電波の届かない場所でも使ってもらえる。そう考えて Chrome のコンソールに手元のサンプルを貼り付けた日、返ってきたのは一行だけでした。
Uncaught ReferenceError: ai is not defined
綴りを疑い、フラグを確認し、Chrome を再起動しました。原因はそのどれでもありませんでした。ai という名前空間そのものが、もう存在しなかったのです。
Gemini Nano は「軽量な Gemini」と説明されることが多いのですが、実際につまずくのはモデルの性能ではありません。入口の選択と、モデルがまだ端末に無いかもしれない、という前提の扱い方 です。この2点を外すと、どれだけ正しいプロンプトを書いても一行も実行されません。
Chrome と Android、それぞれの現在地を整理します。
ai.languageModel はもう存在しません
初期の Chrome Built-in AI は ai や window.ai というグローバルの下に各機能がぶら下がる設計でした。ネット上に残るサンプルの多くは、この時期のものです。
現在は、機能ごとに独立したグローバルクラスへ移りました。
// 旧: ai is not defined で落ちます
const session = await ai.languageModel. create ({
systemPrompt: "あなたは親切なアシスタントです。"
});
// 現行: グローバルの LanguageModel を直接呼びます
const session = await LanguageModel. create ({
initialPrompts: [
{ role: 'system' , content: 'あなたは親切なアシスタントです。簡潔に回答してください。' }
]
});
システムプロンプトの渡し方も変わりました。systemPrompt という専用フィールドではなく、initialPrompts の先頭に role: 'system' のメッセージを置きます。会話の履歴を復元したいときも同じ配列に user と assistant を並べるだけで済むので、こちらのほうが素直だと感じます。
要約も同様です。
// 旧
const summarizer = await ai.summarizer. create ({ type: "tl;dr" });
// 現行
const summarizer = await Summarizer. create ({
type: 'tldr' , // tldr / key-points / teaser / headline
length: 'short' ,
format: 'plain-text' ,
});
type の値が "tl;dr" から 'tldr' へ変わっている点は見落としやすいところです。ここを直さずに名前空間だけ直すと、今度は無効なオプションで弾かれます。
各 API の提供状況も、ひとまとめに「Chrome に入った」と言える段階ではありません。2026年7月時点では次のように分かれています。
API Web ページから Chrome 拡張から
Translator / Language Detector Chrome 138 で安定版 Chrome 138 で安定版
Summarizer Chrome 138 で安定版 Chrome 138 で安定版
Prompt Chrome 148 で安定版 Chrome 138 で安定版
Writer / Rewriter デベロッパートライアル デベロッパートライアル
Proofreader デベロッパートライアル デベロッパートライアル
注目したいのは Prompt API の行です。Chrome 拡張からは 138 で安定版になった一方、通常の Web ページから使えるようになったのは 148。同じ API でも、拡張とページで解禁時期が10バージョン離れていました 。「拡張で動いたコードをそのままページに移したら動かない」という現象は、この差から生まれます。
Writer と Rewriter を製品の中核機能に据えるのは、まだ早いと私は考えています。デベロッパートライアル段階の API は、仕様変更の可能性を織り込んだ上で使うものです。詳細は Chrome の Built-in AI API ステータス一覧 が正本になります。
availability() を挟まないと create() は落ちます
名前空間を直しても、まだ動かないことがあります。次に見るのはここです。
Gemini Nano は Chrome に同梱されていません。API はブラウザに組み込まれていますが、モデルは初回利用時にオリジンごとに別途ダウンロードされます 。つまり、あなたのコードが走る瞬間、端末にモデルが無い可能性が常にあります。
そのため、現行の API は「使う前に状態を尋ねる」ことを前提にした形になっています。
// 1. 状態を確認する
// prompt() に渡すのと同じオプションを availability() にも渡すのが重要です
const availability = await LanguageModel. availability ({
expectedInputs: [{ type: 'text' , languages: [ 'ja' ] }],
expectedOutputs: [{ type: 'text' , languages: [ 'ja' ] }],
});
if (availability === 'unavailable' ) {
// 端末要件を満たしていません。クラウドへ回すか、機能を隠します
return fallbackToCloud ();
}
// 2. ユーザー操作を起点にダウンロードを開始する
// availability が 'downloadable' のとき、create() はユーザー操作が必要です
downloadButton. addEventListener ( 'click' , async () => {
const session = await LanguageModel. create ({
monitor ( m ) {
m. addEventListener ( 'downloadprogress' , ( e ) => {
progressBar.value = e.loaded * 100 ;
});
},
});
// 3. ここでようやく実行できます
const result = await session. prompt ( 'この記事を3行で要約してください。' );
console. log (result);
});
ここで見落とされがちなのが2つあります。
ひとつは availability() と prompt() に同じオプションを渡すこと 。言語やモダリティの組み合わせによっては非対応のことがあり、オプションを揃えないと「利用可能」と答えが返ったのに実行時に NotSupportedError が飛ぶ、という食い違いが起きます。
もうひとつは ユーザー操作(user activation)の要件 です。ページ読み込み直後に create() を呼ぶコードは、モデル未ダウンロードの端末で必ず失敗します。数 GB のダウンロードが、ユーザーの同意なく始まらないようにするための仕様です。開発機ではすでにモデルが入っているため通ってしまい、この失敗は本番で初めて表面化します。
個人開発でひとりで検証していると、この2つを飛ばしたコードを「動いた」と判断しかけます。私自身、そうなりかけました。手元の Chrome にモデルが、すでに入っていただけだったのです。オンデバイス AI の検証では、モデルが無い端末での初回体験こそが本番 だと考えるようになりました。
なお availability() が返すのは 'unavailable'・'downloadable'・'downloading'・'available' の4状態です。真偽値ではないので、if (availability) のような書き方をすると 'unavailable' を通してしまいます。
22 GB の空きと、10 GB を切ったときに起きること
「オンデバイスだから制約が無い」わけではありません。Chrome の Built-in AI には、はっきりした端末要件があります。
項目 要件
OS Windows 10 / 11、macOS 13 以降、Linux、ChromeOS(Chromebook Plus)
ストレージ Chrome プロファイルのあるボリュームに 22 GB 以上 の空き
GPU VRAM 4 GB 超
CPU(GPU を使わない場合) RAM 16 GB 以上・4 コア以上
ネットワーク 初回ダウンロード時のみ必要(従量制回線は不可)
22 GB という数字は、モデル本体の大きさではありません。ダウンロードと展開のための余裕を含んだ要件です。
そして運用上、いちばん厄介なのが次の挙動です。ダウンロード後にストレージの空きが 10 GB を下回ると、モデルは端末から削除されます 。要件を再び満たせば自動で再ダウンロードされますが、その間、あなたのアプリのオンデバイス機能は静かに消えます。
これは「一度動いたら動き続ける」という前提が通用しないことを意味します。フォールバック経路は、初回だけのためではなく、いつでも戻ってくる状態のために用意しておく必要があります 。ユーザーの端末容量は、こちらから見えません。
現在のモデルサイズは chrome://on-device-internals で確認できます。挙動が怪しいときは、まずここを見るのが早道です。
ほかに、実装の前に把握しておきたい制約が2つあります。
Web Worker では使えません 。権限ポリシーの確認が絡むため、当面はトップレベルのウィンドウと同一オリジンの iframe のみです。重い推論をワーカーへ逃がす設計は、現時点では組めません。
クロスオリジンの iframe には権限の委譲が要ります 。<iframe src="..." allow="language-model"> のように明示します。
Android は Chrome の API では届きません — 入口は ML Kit GenAI
ここが、いちばん誤解されやすいところだと感じています。
Chrome の Built-in AI API は、Android 版 Chrome では動きません 。iOS 版でも同様です。対応しているのはデスクトップ(と Chromebook Plus)のみ。「スマートフォンで動く Gemini Nano」と「ブラウザで動く Gemini Nano」は、同じモデル名を共有しているだけで、開発者から見た入口はまったく別物です。
Android アプリから Gemini Nano を使う正規の入口は ML Kit の GenAI API です。AICore という Android のシステムサービスの上に乗っており、端末に1つある Gemini Nano を全アプリで共有します。モデルをアプリに同梱する必要はありません。
用意されているのは、要約・校正・書き換え・画像説明といったタスク特化の API と、自由なプロンプトを送れる GenAI Prompt API です。
// build.gradle.kts
// バージョンは公式ドキュメントで最新を確認してください
dependencies {
implementation ( "com.google.mlkit:genai-summarization:<latest>" )
}
そして実装の形は、Chrome とよく似ています。
val options = SummarizerOptions. builder (context)
. setInputType (InputType.ARTICLE)
. setOutputType (OutputType.ONE_BULLET)
. build ()
val summarizer = Summarization. getClient (options)
// 使う前に状態を確認する — Chrome の availability() と同じ役割です
when (summarizer. checkFeatureStatus (). await ()) {
FeatureStatus.UNAVAILABLE -> useCloudFallback ()
FeatureStatus.DOWNLOADABLE -> summarizer. downloadFeature (downloadCallback)
FeatureStatus.DOWNLOADING -> showProgress ()
FeatureStatus.AVAILABLE -> runSummarization (summarizer)
}
checkFeatureStatus() の4状態が、Chrome の availability() の4状態とほぼ対応していることに気づきます。プラットフォームは違っても、設計思想は同じ です。「モデルはあるかもしれないし、無いかもしれない。まず尋ねてから使う」。
だから、この記事の要点を1文にまとめるならこうなります。入口の名前を覚えることより、確認 → ダウンロード → 実行という3段を守ることのほうが、動作を分けます 。旧いサンプルが動かない本当の理由は、名前空間が変わったからではなく、この3段が省略されていたからです。名前空間の変更は、たまたま同時に目に入っただけでした。
対応端末にも幅があります。GenAI API が動くのは、MediaTek Dimensity・Qualcomm Snapdragon・Google Tensor といった対応チップセットを積んだ端末です。すべての Android 端末で使えるわけではないので、FeatureStatus.UNAVAILABLE は例外ではなく通常の分岐として扱ってください。詳しくは ML Kit GenAI API の概要 にまとまっています。
Nano に渡す仕事と、クラウドに残す仕事
要件を満たして動き始めると、次は「何を任せるか」の判断になります。
Nano はモデルが小さい分、できることも絞られます。Prompt API はテキスト・画像・音声を入力に取れますが、出力はテキストのみ 。対応言語も現時点では en / ja / es / de / fr に限られます。
個人開発の範囲で試した限りですが、私の手元での線引きはこうなりました。
性質 Nano に渡す クラウド(Gemini API)に残す
応答速度 入力補完・返信候補など、待たせたくない処理 数秒待てるバッチ処理
データの機微 端末から出したくないメモ・チャットの分類 公開前提のコンテンツ生成
出力の長さ 短い要約・タグ付け・判定 長文の分析・レポート
正確さの要求 外しても実害の小さい候補提示 誤りが許されない抽出・整形
コスト 頻度が高く、単価を積みたくない処理 頻度が低く、品質が要る処理
判定や分類のように出力が短く、構造が決まっている処理 は Nano の得意分野です。しかも Prompt API には responseConstraint があり、JSON Schema を渡して出力を縛れます。
const session = await LanguageModel. create ();
const schema = { type: 'boolean' };
const result = await session. prompt (
`次の文章は問い合わせですか? \n\n ${ text }` ,
{ responseConstraint: schema }
);
console. log ( JSON . parse (result)); // true / false
分類器としての Nano は、素直に使えます。逆に、長文生成を任せると質でも速度でもクラウドに届きません。
もうひとつ、セッション設計で押さえておきたい点があります。会話を続けるとコンテキストは当然埋まっていきます。
console. log ( `${ session . contextUsage } / ${ session . contextWindow }` );
session. addEventListener ( 'contextoverflow' , () => {
// 古いやり取りから順に落とされ始めた合図です
});
溢れた場合、古いプロンプトと応答の組から順に削られます(システムプロンプトだけは残ります)。それでも足りなければ QuotaExceededError が飛びます。長い会話を扱うなら、contextUsage を見ながら自分で区切るほうが挙動を読めます。
なお temperature と topK の調整は、Chrome 拡張の Prompt API か、オリジントライアルを有効にした場合に限られます。通常の Web ページからは、現時点では効きません 。ここは私も一度勘違いして、効かないパラメータを渡し続けていました。
より踏み込んだ振り分けの設計は 0.5B のローカル LLM を「前段ルーター」として使うときの設計 と Gemini API と Apple FoundationModels のハイブリッド推論ルーティング実装メモ にまとめています。Chrome 拡張から使う場合の全体像は Manifest V3 サイドパネルに組む常駐 AI アシスタント が参考になるはずです。
手元の1台で測った、ダウンロードと推論の実時間
数字がないと判断できないので、個人開発の範囲で手元の1台だけで測った参考値を残しておきます。厳密なベンチマークではなく、あくまで「桁感」をつかむためのものです(macOS 14・M2・VRAM 共有・Chrome 148)。
局面 手元での実測(参考値)
初回モデルダウンロード 約3〜6分(回線と時間帯で振れます)
create() の初期化(モデル取得済み) 0.3〜0.8 秒
短い分類プロンプト(真偽1つ) 120〜300 ミリ秒
3行要約(800字入力) 1.5〜3 秒
同じ分類を Gemini Flash(クラウド往復) 400〜900 ミリ秒
読み取れたのは2点です。ひとつ、短い判定なら Nano のほうがクラウド往復より速い こと。ネットワークの往復が消える分、短い判定では体感でおよそ2倍速く、待たされる感覚が消えます。もうひとつ、長文になると差が縮み、やがて逆転する こと。要約のように出力が伸びる処理では、クラウドの大きなモデルのほうが速く、質も上でした。
だから速度で選ぶ場合は、対象を「短くて頻度の高い処理」に絞ることをお勧めします。ここを外して長文生成をオンデバイスに寄せると、速さのためのはずが、遅さを抱え込みます。
初期化コストも無視できません。create() はセッションごとに走るので、分類のたびに作り直すと初期化ぶんが積み上がります。使い回せるセッションを1つ保持して、prompt() だけを繰り返すほうが安定しました。
例外を静かに握りつぶさない — 3つのエラーを分岐にする
動く経路だけを書くと、本番運用でいちばん困るのは「昨日まで動いていたのに、今日は無言で何も起きない」状態です。オンデバイス AI では、これが日常的に起こり得ます。ストレージが 10 GB を切ればモデルは消え、コンテキストが溢れれば例外が飛びます。
そこで、prompt() の呼び出しは3つのエラーを見分けられる形にしておくと、崩れ方が読めるようになります。
async function runOnDevice ( session , text ) {
try {
return await session. prompt (text);
} catch (err) {
// 1. コンテキスト超過 — セッションを畳んで作り直す
if (err.name === 'QuotaExceededError' ) {
session. destroy ();
return { retry: true , reason: 'context-full' };
}
// 2. モデルが実行時に消えていた — 状態を採り直す
if (err.name === 'NotSupportedError' || err.name === 'InvalidStateError' ) {
const state = await LanguageModel. availability ();
if (state !== 'available' ) return fallbackToCloud (text);
}
// 3. それ以外は握りつぶさず、クラウドへ退避する
return fallbackToCloud (text);
}
}
肝は、どのエラーでも最後はクラウドへ戻れること です。オンデバイスは速さと機微の保護のための最適化であって、機能の唯一の入口にしてはいけません。入口を1つに絞ると、モデルが消えた瞬間にアプリの機能そのものが消えます。
そして、モデルが消える瞬間は事前に検知できません。だからこそ、availability() を初回だけでなく、失敗したときにもう一度呼ぶ 設計が効きます。「使う前に尋ねる」を、「落ちたらもう一度尋ねる」まで広げておく。この一手間が、静かな不具合の対処を、目に見える分岐へと変えます。
次の一歩
もし手元のコードが動いていないなら、確認する順番はひとつです。
chrome://on-device-internals を開き、モデルが入っているかを見る
入っていなければ、availability() の戻り値をそのまま console.log する
'downloadable' が返るなら、create() をボタンの click ハンドラの中へ移す
ai. で始まるコードを探して置き換えるのは、そのあとで構いません。名前空間は目に見える違いなので気づきますが、availability() の欠落は何も言わずに失敗します。
そして、ひとつだけ設計に足しておきたいものがあります。オンデバイス機能が使えない端末で、アプリが何を表示するか 。これを先に決めてから実装に入ると、22 GB の要件も、10 GB を切ったときのモデル削除も、想定内の分岐として収まります。