ブラウザ内蔵機能だけで日本語に要約

Summarizer API + LanguageDetector API + Translator API


概要

- `Summarizer API` で要約すると、現状は英語での翻訳しかできないため、今後仕様が変わっても対応可能なように `LanguageDetector API` で翻訳結果の言語を判定し、その後に日本語へ翻訳することで、ブラウザの機能のみで多言語のテキストを日本語に要約が可能 - `Summarizer API` `LanguageDetector API` `Translator API` は、2025/07/02現在、PC版のChrome 138 以降でのみ利用可能 - ChromeのビルトインGemini nanoを使うことで、モデルをダウンロードした後はブラウザ内で処理が完結

デモ

結果

要約:

検出言語:

日本語訳:

各APIのポイント

Summarizer API の type

| タイプ名 | 何をしてくれる? | 典型的な長さ※ | 生成される形 | 想定ユースケース | |----------|------------------|----------------|--------------|------------------| | `key-points` | 元文の重要ポイントを箇条書きで抽出。読み手が「結論だけざっと把握」できるようにする。 | `short` = 3項目
`medium` = 5項目
`long` = 7項目 | ・`bullet list`(Markdown)
・`plain text`リスト | ⭐ 報告書 / 論文の要点整理
⭐ 会議メモの即時共有 | | `tldr` | "要はこういうこと"を文で凝縮。忙しい人向けの最速サマリ。 | 1 / 3 / 5 文 | 段落(文章) | ⭐ 長いブログの冒頭に「TL;DR」
⭐ チャットの議事録要約 | | `teaser` | 面白い / 興味をそそる部分にフォーカスし、続きを読みたくさせる。宣伝文句寄り。 | 1 / 3 / 5 文 | キャッチーな導入文 | ⭐ 記事・動画の紹介文
⭐ EC商品の説明文冒頭 | | `headline` | 記事タイトル風の一文で本質を伝達。単語数制限付きで、より「見出し」らしいリズム。 | 12 / 17 / 22 語 | 一文(終止符なしが多い) | ⭐ CMSで自動見出し生成
⭐ SNSカード用タイトル | ### ポイント - まず「目的」を考える - 速攻理解 ⇒ `tldr` - 覚えておくべき要点を並べたい ⇒ `key-points` - 興味を引いて本文へ誘導 ⇒ `teaser` - 見出しだけ自動生成 ⇒ `headline` - 出力フォーマットが固定 - `key-points` は常に箇条書き、`headline` は必ず一文など - 後処理のパースが楽になる - 長さは `length` で微調整 - 文章タイプは「文数」、箇条書きは「箇条数」、`headline` は「語数」が伸びるイメージ

Summarizer API のポイント

- 機能検出 - `'Summarizer' in self` で API 対応を確認 - モデル状態の確認 - `Summarizer.availability()` は `unavailable` / `downloadable` / `downloading` / `available` の 4 段階で返す - モデルのダウンロード進捗 - `Summarizer.create({monitor(...)})` で `downloadprogress` イベントを購読し、UIに反映 - 要約の実行方法 - バッチ処理: `summarize()` - ストリーミング: `summarizeStreaming()` (`for-await` で逐次更新) - パラメータ - `type`, `length`, `format`, `sharedContext` は 生成時に固定 なので、オプションを変えたい場合は新しい `Summarizer.create()` を呼ぶ

Language Detector API のポイント

| ステップ | 役割 | 主な API | |----------|------|----------| | ① 機能検出 | `LanguageDetector` が存在するか確認 | `if ('LanguageDetector' in self)` | | ② 利用可否判定 | モデルの有無・DL 必要性を取得 | `LanguageDetector.availability()` | | ③ セッション生成 | 必要ならモデルを DL し、進捗を監視 | `LanguageDetector.create({ monitor })` | | ④ 言語判定 | テキストを渡して候補を取得 | `detector.detect(text)` |

Translator API のポイント

| 項目 | ポイント | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | **対応環境** | *Chrome 138 以降*、デスクトップのみ。モバイル Chrome は未対応 | | **セキュリティ** | Secure Context(HTTPS または `localhost`)必須。HTTP では `Translator` が未定義 ([Stack Overflow][2]) | | **ハードウェア要件** | Windows 10/11・macOS 13+・Linux、
VRAM > 4 GB、Chrome プロファイルのあるドライブに *22 GB 以上* の空き。空きが 10 GB 未満になるとモデルは自動削除 | | **機能検出** | `if ('Translator' in self) { … }` でサポート確認 | | **言語ペアの可用性** | `Translator.availability()` → `'unavailable' / 'downloadable' / 'downloading' / 'available'` の 4 値を返す | | **インスタンス生成** | `Translator.create({ sourceLanguage, targetLanguage, monitor })` で生成。`monitor` で `downloadprogress` を監視し UI に進捗表示 | | **翻訳実行** | `await translator.translate(text)` で翻訳。処理は逐次実行→大量テキストはチャンク化推奨 | | **言語コード** | BCP-47 形式(例: `ja`, `en`, `zh-Hant`)を使用 | | **他 API 連携** | ソース言語不明時は Language Detector API → Translator API の順が便利 | | **UX 改善** | ①モデル DL 中/翻訳中のステータス表示
②エラー時に空き容量やネット状況を案内
③モデルは一度 DL すればキャッシュされ 2 回目以降高速 | | **プライバシー / コスト** | 翻訳は完全に *端末内* で完結 → テキストをクラウドへ送信しない。外部翻訳 API の利用料も不要 |

コード

JavaScript

/* 改行調整: 行内の * の直前に改行を入れる */
const insertBreakBeforeAsterisk = txt => txt.replace(/([^\n])\s*\*/g, '$1\n*');

/* API 初期化ヘルパ */
async function initSummarizer(opt) {
  if (await Summarizer.availability() === 'unavailable')
    throw new Error('Summarizer API 未対応です');
  return Summarizer.create({...opt, format: 'plain-text'});
}
async function initLanguageDetector() {
  if (await LanguageDetector.availability() === 'unavailable')
    throw new Error('LanguageDetector API 未対応です');
  return LanguageDetector.create();
}
async function initTranslator(src) {
  if (await Translator.availability({sourceLanguage: src, targetLanguage: 'ja'}) === 'unavailable')
    throw new Error('Translator API がこの言語ペアをサポートしていません');
  return Translator.create({sourceLanguage: src, targetLanguage: 'ja'});
}

// localhost以外でのHTTPアクセス時の早期リジェクト
/*
if (location.protocol === 'http:') {
  alert('HTTPS でアクセスしてください');
}
*/

/* メイン処理 */
document.getElementById('run').addEventListener('click', async () => {
  const raw = document.getElementById('input').value.trim();
  if (!raw) return alert('テキストを入力してください');

  const type   = document.getElementById('type').value;
  const length = document.getElementById('length').value;
  const useStreaming = document.getElementById('streaming').checked;

  const summaryBox     = document.getElementById('summary');
  const detectedBox    = document.getElementById('detected');
  const translationBox = document.getElementById('translation');

  summaryBox.innerHTML     = '⏳ 要約中...';
  detectedBox.textContent  = '';
  translationBox.innerHTML = '';

  try {
    const summarizer = await initSummarizer({type, length});
    let summary = '';

    /* --- 1) 要約 --- */
    if (useStreaming && summarizer.summarizeStreaming) {
      const stream = await summarizer.summarizeStreaming(raw);
      const reader = stream.getReader();
      while (true) {
        const {value, done} = await reader.read();
        if (done) break;
        summary += value;
        summaryBox.innerHTML = marked.parse(summary); // 部分更新
      }
    } else {
      summary = await summarizer.summarize(raw);
      summaryBox.innerHTML = marked.parse(summary);
    }

    /* --- 2) 言語判定 --- */
    const detector = await initLanguageDetector();
    const [best]   = await detector.detect(summary);
    detectedBox.textContent =
      `${best.detectedLanguage} (信頼度 ${best.confidence.toFixed(2)})`;

    /* --- 3) 翻訳 --- */
    if (best.detectedLanguage !== 'ja') {
      const translator = await initTranslator(best.detectedLanguage);
      const jpFixed = insertBreakBeforeAsterisk(await translator.translate(summary));
      // markedでMarkdownフォーマットの表示
      translationBox.innerHTML = marked.parse(jpFixed);
    } else {
      translationBox.innerHTML = marked.parse('要約はすでに日本語です。');
    }
  } catch (err) {
    alert(err.message);
    summaryBox.innerHTML = detectedBox.textContent =
      translationBox.innerHTML = '';
  }
});

参照