ChromeのAI APIをさわってみる
TL;DR
- Chrome の 組み込みの AI API を試してみる。
- Translator API & Language Detector API & Summarizer API
Chrome AI API
Chrome は Gemini Nano という LLM モデルを使用して、組み込みの AI API を提供しています。これはローカル PC で実行するように設計されており、現在モバイルでは動作しません。
ローカルデバイス内である程度処理が完結するようになっています。
以下が現在の API の一覧です。
- Writer API: テキスト生成
- Rewriter API: テキストの書き換え
- Summarizer API: テキスト要約
- Translator API: 翻訳
- Language Detector API: 言語検出
- Prompt API: Gemini Nano モデルへの自然言語のリクエスト
しかし、Translator API と Language Detector API と Summarizer API は Chrome v138 以上の安定版で利用可能ですが、他の API は試験運用になっています。
またこれらの API は Chrome でのみ利用可能で、他のブラウザでは動作しません。
現在は WICG で議論中で、将来的にクロスブラウザ化を目指しているようです。
触った感想としては、Translator API と Language Detector API は非常に高速に動作します。
Summarizer API は 400 文字程度のテキストでも結構時間がかかる印象があります。
Translator API
Translator API が提供するメソッドは 主に availability と translate の 2 つ。
availability は、指定された言語の翻訳モデルがダウンロード可能かどうかを確認します。ダウンロード可能な場合は、ダウンロードを開始することができます。
translate は、指定されたテキストを翻訳し、Translator オブジェクトを返します。
Demo
Claude に生成してもらったデモ
export function TranslatorDemo() {
const [supported, setSupported] = useState<boolean | null>(null);
const [input, setInput] = useState('');
const [detected, setDetected] = useState<Detected | null>(null);
const [targetLang, setTargetLang] = useState<'en' | 'ja' | 'es'>('ja');
const [output, setOutput] = useState('');
const [detector, setDetector] = useState<any>(null);
const [isTranslating, setIsTranslating] = useState(false);
useEffect(() => {
const func = async () => {
if (!('LanguageDetector' in window)) {
setSupported(false);
return;
}
setSupported(true);
const created = await window.LanguageDetector!.create();
setDetector(created);
};
func();
}, []);
useEffect(() => {
const func = async () => {
if (!detector) return;
if (!input.trim()) {
setDetected(null);
return;
}
const r = await detector.detect(input.trim());
console.log('Detected languages:', r);
const [res]: Detected[] = await detector.detect(input.trim());
setDetected(res);
};
func();
}, [input, detector]);
const tagToLabel = (tag: string, locale = 'en') =>
new Intl.DisplayNames([locale], { type: 'language' }).of(tag) ?? tag;
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!detector) return;
setIsTranslating(true);
setOutput('Translating...');
try {
const [{ detectedLanguage }] = await detector.detect(input.trim());
if (!('Translator' in window)) return;
const availability = await window.Translator!.availability({
sourceLanguage: detectedLanguage,
targetLanguage: targetLang,
});
if (availability === 'downloading') {
setOutput('Translation model is downloading. Please wait...');
} else if (availability === 'downloadable') {
setOutput('Downloading translation model...');
}
const translator = await window.Translator!.create({
sourceLanguage: detectedLanguage,
targetLanguage: targetLang,
monitor(m: any) {
m.addEventListener('downloadprogress', (e: any) => {
const progress = Math.round(e.loaded * 100);
setOutput(`Downloading translation model... ${progress}%`);
});
},
});
const translated = await translator.translate(input.trim());
setOutput(translated);
} catch (err) {
console.error(err);
if (err instanceof Error && err.message.includes('user gesture')) {
setOutput(
'Translation requires user interaction. Please try clicking the translate button again.'
);
} else {
setOutput('An error occurred. Please try again.');
}
} finally {
setIsTranslating(false);
}
};
if (supported === false)
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<div className="max-w-md mx-auto text-center">
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<div className="text-red-600 text-lg font-semibold mb-2">
Browser Not Supported
</div>
<p className="text-red-700">
Your browser does not support the Language Detector / Translator
APIs. Please try using a recent version of Chrome with
experimental features enabled.
</p>
</div>
</div>
</div>
);
if (supported === null) return null;
return (
<div className="min-h-screen bg-gray-50 py-8 px-4">
<div className="max-w-2xl mx-auto">
<h1 className="text-3xl font-bold text-center mb-8 text-gray-900">
Chrome Translator Demo
</h1>
<form
onSubmit={onSubmit}
className="space-y-6 bg-white p-6 rounded-lg shadow-md">
<div>
<label
htmlFor="input"
className="block text-sm font-medium text-gray-700 mb-2">
Enter text to translate
</label>
<textarea
id="input"
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent"
rows={5}
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type something…"
/>
</div>
<div className="text-sm text-gray-600 bg-gray-50 p-3 rounded-md">
{detected
? `${(detected.confidence * 100).toFixed(
1
)}% sure this is ${tagToLabel(detected.detectedLanguage)}`
: 'Not sure what language this is'}
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label
htmlFor="target"
className="text-sm font-medium text-gray-700">
Translate to:
</label>
<select
id="target"
value={targetLang}
onChange={(e) => setTargetLang(e.target.value as any)}
className="border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent">
<option value="en">English</option>
<option value="ja">日本語</option>
<option value="es">Español</option>
</select>
</div>
<button
type="submit"
disabled={!input.trim() || isTranslating}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors flex items-center gap-2">
{isTranslating && (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{isTranslating ? 'Translating...' : 'Translate'}
</button>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Translation
</label>
<output className="block w-full p-3 border border-gray-300 rounded-md min-h-[3rem] whitespace-pre-wrap bg-gray-50">
{output}
</output>
</div>
</form>
</div>
</div>
);
}
Language Detector API
Language Detector API は、ユーザーが入力したテキストの言語を検出するための API です。
const detector = await window.LanguageDetector!.create();
const [res]: = await detector.detect(input.trim());
detect メソッドから返された結果は、検出された言語とその信頼度を含むオブジェクトの配列です。
イメージ
0: {confidence: 0.5316367149353027, detectedLanguage: 'en'}
1: {confidence: 0.14246660470962524, detectedLanguage: 'ar-Latn'}
2: {confidence: 0.05346524342894554, detectedLanguage: 'vi'}
3: {confidence: 0.028155824169516563, detectedLanguage: 'hi-Latn'}
4: {confidence: 0.024134792387485504, detectedLanguage: 'fy'}
Summarizer API
Summarizer API は、ユーザーが入力したテキストを要約するための API です。要約の種類や形式、長さを指定して要約を生成できます。
const session = await window.Summarizer!.create({
type: 'tldr', // 'tldr' or 'headline'
format: 'plain-text', // 'plain-text' or 'markdown'
length: 'medium', // 'short', 'medium', or 'long'
});
const summary = await session.summarize(input);
Demo
Claude に生成してもらったデモ
export function Summarizer() {
const [apiState, setApiState] = useState<
'loading' | 'unavailable' | 'unsupported' | 'ready'
>('loading');
const [input, setInput] = useState('');
const [summaryType, setSummaryType] = useState<AISummarizerType>('tldr');
const [summaryFormat, setSummaryFormat] =
useState<AISummarizerFormat>('plain-text');
const [summaryLength, setSummaryLength] =
useState<AISummarizerLength>('medium');
const [charCount, setCharCount] = useState('');
const [output, setOutput] = useState('');
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const func = async () => {
if (!window.Summarizer) {
setApiState('unavailable');
return;
}
const availability = await window.Summarizer.availability();
if (availability === 'available' || availability === 'downloadable') {
setApiState('ready');
} else {
setApiState('unsupported');
}
};
func();
}, []);
const scheduleSummarization = () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
debounceTimer.current = setTimeout(runSummarization, 1000);
};
const runSummarization = async () => {
if (!input.trim()) {
setOutput('');
setCharCount('');
return;
}
setOutput('Generating summary…');
try {
const session = await window.Summarizer!.create({
type: summaryType,
format: summaryFormat,
length: summaryLength,
});
const usage = await session.measureInputUsage(input);
setCharCount(`${usage.toFixed()} of ${session.inputQuota}`);
const summary = await session.summarize(input);
session.destroy();
setOutput(summary);
} catch (err) {
console.error(err);
setOutput('An error occurred. Please try again.');
}
};
if (apiState === 'loading') return null;
if (apiState === 'unavailable')
return (
<p className="mt-4 text-red-600">
Your browser does not expose the Summarizer API.
</p>
);
if (apiState === 'unsupported')
return (
<p className="mt-4 text-red-600">
This device can’t run the on-device Summarizer model.
</p>
);
return (
<div className="space-y-4 max-w-2xl">
<h1 className="text-3xl font-bold text-center mb-8 text-gray-900">
Chrome Summarizar Demo
</h1>
<textarea
className="w-full p-2 border rounded"
rows={8}
placeholder="Type or paste some text…"
value={input}
onChange={(e) => {
setInput(e.target.value);
scheduleSummarization();
}}
/>
<div className="flex flex-wrap gap-4">
<label className="flex items-center gap-2">
Type
<select
value={summaryType}
onChange={(e) => {
setSummaryType(e.target.value as AISummarizerType);
scheduleSummarization();
}}
className="border rounded p-1">
<option value="tldr">TL;DR</option>
<option value="headline">Headline</option>
</select>
</label>
<label className="flex items-center gap-2">
Format
<select
value={summaryFormat}
onChange={(e) => {
setSummaryFormat(e.target.value as AISummarizerFormat);
scheduleSummarization();
}}
className="border rounded p-1">
<option value="plain-text">Plain text</option>
<option value="markdown">Markdown</option>
</select>
</label>
<label className="flex items-center gap-2">
Length
<select
value={summaryLength}
onChange={(e) => {
setSummaryLength(e.target.value as AISummarizerLength);
scheduleSummarization();
}}
className="border rounded p-1">
<option value="short">Short</option>
<option value="medium">Medium</option>
<option value="long">Long</option>
</select>
</label>
</div>
{charCount && (
<p className="text-sm text-gray-600">Characters used: {charCount}</p>
)}
<div className="border rounded p-2 min-h-[4rem] whitespace-pre-wrap">
{output}
</div>
</div>
);
}
Discussion