🐥

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>
  );
}

参考リンク

GitHubで編集を提案

Discussion