Pixelの通話録音をGemini APIでテキスト化して要約する(スマホとPC両方使える)

お勧め

前提条件(なぜ、Google PixelのGoogle電話アプリ限定の話になっているのか?)

日本では法規制の問題などがあって検討事項が多い様子。なので、スマホの通話録音に参入する会社(Googleのスマホ電話アプリなど)もあれば撤退する会社もあります(楽天モバイルのrakuten linkなど)。で、同じAndroidとは言え、各社深いレベルまでカスタマイズしているので、実装不不可能になってたりして、各社の機種ごとの対応が違っているのが現状です。なので、Google PixelのGoogle電話アプリ特化した内容にせざるを得なくなりました。ご容赦ください。

Pixel 6以降では通話録音機能が利用可能ですが、日本語のAI要約は2025年12月時点で非対応です。そこで、録音ファイルをGoogleドライブ経由でPCに取り込み、Gemini APIで要約する仕組みを構築してみました。

本記事の趣旨

これはアプリとして公開するものではありません。 自分用の通話内容確認ツールとして構築した仕組みを、技術ブログとして共有するものです。

後述する法的リスクの観点から、この種の機能を一般向けアプリとして開発・販売することは現実的ではありません。あくまで「技術的にはこう実現できる」という情報共有であり、利用は自己責任となります。


    1. 本記事の趣旨
  1. ⚠️ 法的制約と注意事項(重要)
    1. 録音自体の法的位置づけ
    2. 注意が必要なのは「その後の取り扱い」
    3. 外部AIサービスへのデータ送信
    4. 想定される使い方
  2. 本記事で紹介する内容
  3. 全体の流れ
  4. コード実行環境の選択
  5. 対象機種
    1. Pixelの通話録音機能の現状(2025年12月)
  6. 前提条件
  7. Part 1: スマホ側の設定
    1. 1-1. 通話録音を有効化
    2. 1-2. 自動録音の設定(推奨)
    3. 1-3. 録音ファイルをGoogleドライブに保存する手順
  8. Part 2: コード実行環境の準備
    1. 2-1. Gemini APIキーの取得(共通)
      1. モデル選択(2025年12月時点)
      2. 料金目安(Gemini 2.5 Flash)
      3. APIキーの取得手順
    2. 2-2A. ローカルPCで実行する場合(ちょっと面倒くさい。エンジニア向け)
      1. 必要なもの
      2. セットアップ
      3. スクリプトの作成
      4. 使い方
      5. Windows用の起動ショートカット
    3. 2-2B. Google Colabで実行する場合(環境構築不要:これ、webで設定できますが、やり方を見当付けないといけなかったので、ちょっとお勧め出来ません)
      1. 手順
      2. セル1: Googleドライブをマウント
      3. セル2: ライブラリをインストール
      4. セル3: APIキーを設定
      5. セル4: 要約関数を定義
      6. セル5: 要約を実行
      7. 使い方のコツ
    4. 2-2C. Google Apps Script(完全自動化)- スマホだけで完結(お勧めです。私はこれにしました)
      1. トレードオフ
      2. 仕組み
      3. セットアップ手順
      4. 動作確認
      5. 注意事項
  9. Part 3: 出力サンプル
  10. Part 4: 実際の運用フロー
    1. パターン1: PCメインで使う人(ローカルアプリ)
    2. パターン2: スマホメインで使う人(Google Colab)
    3. パターン3: 完全自動化(Google Apps Script)★おすすめ
  11. Part 5: 活用シーン
    1. 営業・商談
    2. プロジェクト管理
    3. カスタマーサポート
    4. 個人の備忘録
  12. Part 6: 自分用のリマインダー連携
    1. Googleカレンダーに手動登録:有料公開するならこれも自動登録までしますけど、今回はそこまでしません。
    2. Google Keepでタスク管理:有料公開するならこれも自動登録までしますけど、今回はそこまでしません。
  13. トラブルシューティング
    1. 「APIキーが無効です」エラー
    2. 「ファイル形式がサポートされていません」エラー
    3. ローカルアプリが起動しない
    4. Windowsでpipインストール時にエラーが出る
    5. ローカルアプリにアクセスできない
    6. Google Colabで「ドライブが見つからない」
    7. 要約の精度が低い
  14. まとめ
    1. ビジネス活用のポイント
    2. 運用パターンの参考
  15. 参考リンク

⚠️ 法的制約と注意事項(重要)

この仕組みを使う前に必ず読んでください。

録音自体の法的位置づけ

日本では、通話の当事者が相手の同意なく録音する「秘密録音」は違法ではないとされています

最高裁判決(平成12年7月12日)でも、「一方の当事者が相手方との会話を録音することは、たとえ相手方の同意を得ないで行われたものであっても、違法ではない」と判示されています。ただし、個別の状況により判断が異なる場合もあるため、不安な場合は専門家への相談をおすすめします。

注意が必要なのは「その後の取り扱い」

録音データの取り扱いを誤ると、以下の法的リスクが生じる可能性があります。

リスク該当する可能性のある法律
録音内容を第三者に共有・公開プライバシー侵害(民法709条 不法行為)
相手の社会的評価を下げる内容を公開名誉毀損罪(刑法230条1項)
個人情報を含む録音を不適切に扱う個人情報保護法違反

特に注意すべき点

  1. 要約結果を第三者に共有しない – 通話相手は「あなた」に情報を開示したのであって、第三者への共有を許容していない
  2. SNSやブログに要約を投稿しない – プライバシー侵害・名誉毀損のリスク
  3. 業務利用する場合は社内ルールを確認 – 企業によっては通話録音の取り扱いに関する規定があります

外部AIサービスへのデータ送信

本記事で紹介する方法は、録音ファイルをGemini API(Googleのサーバー)に送信します。

プランデータの扱い推奨
無料枠モデル改善に使用される可能性あり❌ 非推奨
従量課金(有料)再学習に使用されない✅ 推奨

通話録音には個人情報・機密情報が含まれる可能性が高いため、従量課金プランの使用をおすすめします。

無料枠で試す場合は、テスト用の録音(自分一人で話した内容など)に限定し、実際の通話録音は有料プランで処理することをご検討ください。

想定される使い方

個人的な備忘録として使用(自分だけが閲覧する用途) ✅ 相手に「録音している」ことを事前に伝える(コールセンター方式) ✅ 要約結果は自分の端末内で管理し、外部に共有しない

要約結果をSNS・ブログ・社内チャットに投稿相手の同意なく第三者に共有

本記事は個人利用を前提とした技術情報の共有であり、法的アドバイスではありません。ご留意ください。


本記事で紹介する内容

  • 通話録音をGoogleドライブ経由で保存・管理する方法
  • Gemini APIで日本語要約を生成する仕組み
  • 要約結果をGoogleドライブに保存し、PCでもスマホでも閲覧可能にする構成

全体の流れ

【スマホ】
通話 → 自動録音 → 通話履歴から共有 → Googleドライブに保存
                    (3タップ)

【PC または Google Colab】
Googleドライブから取得 → Pythonスクリプト → Gemini API要約
    ↓
要約結果をGoogleドライブに保存
    ↓
【どこからでも閲覧】
PC → Googleドライブで確認
スマホ → Googleドライブアプリで確認

完全自動化ではありませんが、要約結果はGoogleドライブ経由でどこからでも確認できます。

コード実行環境の選択

Pythonスクリプトを動かす環境は2つの選択肢がある:

環境メリットデメリット
ローカルPCオフラインでも動作、高速Python環境構築が必要
Google Colab環境構築不要、ブラウザだけでOKネット必須、毎回ノートブック開く

おすすめ

  • PC作業が多い人 → ローカルPC
  • 環境構築が面倒な人 → Google Colab

対象機種

Pixelの通話録音機能の現状(2025年12月)

機種通話録音文字起こしAI要約
Pixel 9 / 9 Pro / 9 Pro XL / 9 Pro Fold✅ 日本語対応英語のみ
Pixel 6〜8、Pixel 9a

2025年12月時点では、どの機種でも日本語のAI要約機能は提供されていません。 これが本記事の仕組みを構築したきっかけです。


前提条件

項目要件
スマホGoogle Pixel 6以降(Android 14以上)
PCWindows / Mac / Linux(Python 3.10以上)またはGoogle Colab
GoogleアカウントGoogleドライブが使える状態
Gemini APIAPIキー取得済み + 課金有効化(従量課金推奨)

Part 1: スマホ側の設定

1-1. 通話録音を有効化

  1. 電話アプリ → 左上の 設定
  2. 通話アシスト通話の録音
  3. 「通話の録音をオンにする」 を有効化

1-2. 自動録音の設定(推奨)

同じ画面で 「通話の自動録音」 を設定:

設定項目説明
連絡先に登録されていない相手との通話を自動的に録音する知らない番号からの着信を自動録音
これらの電話番号との通話を自動的に録音特定の番号を指定して自動録音

注意: 録音を開始すると、相手に「この通話の録音を開始しました」というアナウンスが流れます。

1-3. 録音ファイルをGoogleドライブに保存する手順

通話終了後、以下の操作で録音をGoogleドライブに保存:

  1. 電話アプリ → 通話履歴から録音した通話をタップ
  2. 録音の 共有ボタン(↗) をタップ
  3. 「ドライブ」 を選択
  4. 保存先フォルダを選択(例:通話録音)→ 保存

Tips: Googleドライブに「通話録音」フォルダを事前に作成しておくと管理しやすくなります。


Part 2: コード実行環境の準備

2-1. Gemini APIキーの取得(共通)

⚠️ 重要: 無料プランは再学習に使用される可能性あり

Gemini APIには複数のプランがある:

プラン料金データの扱い
無料枠¥0モデル改善に使用される可能性あり
従量課金(Pay-as-you-go)使った分だけ再学習に使用されない

通話録音には個人情報や機密情報が含まれる可能性が高いです。従量課金プランの使用を強くおすすめします。

モデル選択(2025年12月時点)

モデル特徴推奨用途
gemini-2.5-flashバランス型、コスパ良好一般的な通話要約(推奨)
gemini-2.5-pro最高精度、複雑な推論に強い重要な商談・複雑な内容
gemini-2.0-flash安定版、最も低コストコスト最優先の場合

※ Gemini 1.5シリーズは廃止済みです。使用すると404エラーになります。

料金目安(Gemini 2.5 Flash)

項目料金
テキスト入力$0.30 / 100万トークン
音声入力$1.00 / 100万トークン
テキスト出力$2.50 / 100万トークン

実際のコスト例:

  • 3分の通話要約 → 約¥5〜10
  • 10分の通話要約 → 約¥15〜30
  • 1日5件の通話を要約 → 月額 約¥2,000〜¥5,000

個人利用であれば月数千円程度の見込みです。ビジネス用途の通話記録管理としては比較的低コストと考えられます。

APIキーの取得手順

  1. Google AI Studio にアクセス
  2. Googleアカウントでログイン
  3. 「Get API key」→「Create API key」
  4. 生成されたAPIキーをメモ
  5. 課金を有効化: 左メニュー「Billing」→ クレジットカード登録

注意: 課金を有効化しないと無料枠として扱われ、データが再学習に使用される可能性があります。


2-2A. ローカルPCで実行する場合(ちょっと面倒くさい。エンジニア向け)

必要なもの

  • Python 3.10以上
  • pip(Pythonパッケージマネージャー)

セットアップ

# 作業フォルダを作成
# 作業フォルダを作成
mkdir call-summarizer
cd call-summarizer

# 必要なライブラリをインストール
pip install google-generativeai flask

スクリプトの作成

以下の内容で app.py を作成:

#!/usr/bin/env python3
"""
通話録音要約アプリ(ローカルWebサービス)
ブラウザで http://localhost:5000 を開いて使用
Google Drive同期フォルダから録音ファイルを選択可能
"""

import os
import json
from datetime import datetime
from pathlib import Path
from flask import Flask, render_template_string, request, jsonify
import google.generativeai as genai

app = Flask(__name__)

# 設定ファイルのパス
CONFIG_FILE = Path(__file__).parent / "config.json"
# デフォルトの保存先
DEFAULT_OUTPUT_DIR = Path(__file__).parent / "要約"
# デフォルトの録音フォルダ(Google Drive)
DEFAULT_RECORDING_DIR = r"G:\マイドライブ\通話録音"

# HTMLテンプレート
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>通話録音 要約アプリ</title>
    <style>
        * { box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            max-width: 900px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        h1 { color: #333; margin-bottom: 10px; }
        .subtitle { color: #666; margin-bottom: 30px; }
        .card {
            background: white;
            border-radius: 8px;
            padding: 20px;
            margin-bottom: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .tabs {
            display: flex;
            border-bottom: 2px solid #e0e0e0;
            margin-bottom: 20px;
        }
        .tab {
            padding: 10px 20px;
            cursor: pointer;
            border-bottom: 2px solid transparent;
            margin-bottom: -2px;
            color: #666;
        }
        .tab.active {
            border-bottom-color: #4285f4;
            color: #4285f4;
            font-weight: bold;
        }
        .tab-content { display: none; }
        .tab-content.active { display: block; }
        .file-list {
            max-height: 400px;
            overflow-y: auto;
            border: 1px solid #e0e0e0;
            border-radius: 4px;
        }
        .file-item {
            display: flex;
            align-items: center;
            padding: 12px 15px;
            border-bottom: 1px solid #f0f0f0;
            cursor: pointer;
            transition: background 0.2s;
        }
        .file-item:hover { background: #f5f5f5; }
        .file-item.selected { background: #e8f0fe; }
        .file-item:last-child { border-bottom: none; }
        .file-icon { font-size: 24px; margin-right: 12px; }
        .file-info { flex: 1; }
        .file-name { font-weight: 500; color: #333; }
        .file-meta { font-size: 12px; color: #888; margin-top: 2px; }
        .file-size { color: #666; font-size: 13px; }
        .drop-zone {
            border: 2px dashed #ccc;
            border-radius: 8px;
            padding: 40px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s;
        }
        .drop-zone:hover, .drop-zone.dragover {
            border-color: #4285f4;
            background: #e8f0fe;
        }
        .drop-zone input { display: none; }
        .btn {
            background: #4285f4;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin-top: 15px;
        }
        .btn:hover { background: #3367d6; }
        .btn:disabled { background: #ccc; cursor: not-allowed; }
        .btn-secondary {
            background: #f5f5f5;
            color: #333;
            border: 1px solid #ddd;
        }
        .btn-secondary:hover { background: #e8e8e8; }
        .result {
            white-space: pre-wrap;
            font-family: "Consolas", "Monaco", monospace;
            background: #f9f9f9;
            padding: 15px;
            border-radius: 4px;
            max-height: 500px;
            overflow-y: auto;
            font-size: 14px;
            line-height: 1.6;
        }
        .status { margin: 15px 0; color: #666; }
        .error { color: #d93025; }
        .success { color: #188038; }
        .settings { margin-bottom: 15px; }
        .settings input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
        }
        .settings label { display: block; margin-bottom: 5px; font-weight: bold; }
        .settings small { color: #888; display: block; margin-top: 4px; }
        .hidden { display: none; }
        .refresh-btn {
            background: none;
            border: none;
            cursor: pointer;
            font-size: 18px;
            padding: 5px 10px;
            color: #666;
        }
        .refresh-btn:hover { color: #4285f4; }
        .header-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .empty-message {
            padding: 40px;
            text-align: center;
            color: #888;
        }
        .selected-file {
            background: #e8f0fe;
            padding: 10px 15px;
            border-radius: 4px;
            margin-top: 10px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }
        .clear-btn {
            background: none;
            border: none;
            cursor: pointer;
            color: #666;
            font-size: 18px;
        }
    </style>
</head>
<body>
    <h1>📞 通話録音 要約アプリ</h1>
    <p class="subtitle">Google Driveの録音フォルダから選択、またはファイルをドラッグ&ドロップ</p>

    <!-- 設定カード(折りたたみ) -->
    <div class="card">
        <div class="header-row">
            <h3>⚙️ 設定</h3>
            <button class="btn-secondary btn" onclick="toggleSettings()" style="margin:0; padding:6px 12px;">
                <span id="settings-toggle-text">表示</span>
            </button>
        </div>
        <div id="settings-panel" class="hidden">
            <div class="settings" style="margin-top:15px;">
                <label>Gemini APIキー</label>
                <input type="password" id="api-key" placeholder="APIキーを入力" value="{{ api_key }}">
            </div>
            <div class="settings">
                <label>録音フォルダ(Google Drive同期フォルダ)</label>
                <input type="text" id="recording-dir" placeholder="例: G:\\マイドライブ\\通話録音" value="{{ recording_dir }}">
                <small>Google Drive for Desktopで同期されたフォルダのパスを指定</small>
            </div>
            <div class="settings">
                <label>要約の保存先</label>
                <input type="text" id="output-dir" placeholder="要約ファイルの保存先" value="{{ output_dir }}">
                <small>Google Driveのフォルダを指定するとスマホからも見れます</small>
            </div>
            <button class="btn" onclick="saveSettings()">設定を保存</button>
            <span id="settings-status" class="status"></span>
        </div>
    </div>

    <!-- ファイル選択カード -->
    <div class="card">
        <h3>📁 録音ファイルを選択</h3>
        
        <div class="tabs">
            <div class="tab active" onclick="switchTab('list')">📂 フォルダから選択</div>
            <div class="tab" onclick="switchTab('upload')">📤 ファイルをアップロード</div>
        </div>

        <!-- タブ1: フォルダから選択 -->
        <div id="tab-list" class="tab-content active">
            <div class="header-row" style="margin-bottom:10px;">
                <span style="color:#666;">録音フォルダ内のファイル</span>
                <button class="refresh-btn" onclick="loadFiles()" title="更新">🔄</button>
            </div>
            <div class="file-list" id="file-list">
                <div class="empty-message">読み込み中...</div>
            </div>
        </div>

        <!-- タブ2: アップロード -->
        <div id="tab-upload" class="tab-content">
            <div class="drop-zone" id="drop-zone">
                <p>ここにファイルをドラッグ&ドロップ<br>または クリックして選択</p>
                <input type="file" id="file-input" accept=".m4a,.mp3,.wav,.ogg,.flac">
            </div>
        </div>

        <!-- 選択中のファイル表示 -->
        <div id="selected-file-display" class="hidden">
            <div class="selected-file">
                <span>📎 <strong id="selected-file-name"></strong></span>
                <button class="clear-btn" onclick="clearSelection()">✕</button>
            </div>
        </div>

        <button class="btn" id="summarize-btn" onclick="summarize()" disabled>要約を生成</button>
        <div class="status" id="status"></div>
    </div>

    <!-- 結果表示 -->
    <div class="card hidden" id="result-card">
        <h3>📝 要約結果</h3>
        <div class="result" id="result"></div>
        <p class="success" id="saved-path"></p>
    </div>

    <script>
        let selectedFilePath = null;
        let selectedFileName = null;
        let uploadedFile = null;

        // 初期化
        document.addEventListener('DOMContentLoaded', () => {
            loadFiles();
            setupDropZone();
        });

        function toggleSettings() {
            const panel = document.getElementById('settings-panel');
            const text = document.getElementById('settings-toggle-text');
            if (panel.classList.contains('hidden')) {
                panel.classList.remove('hidden');
                text.textContent = '非表示';
            } else {
                panel.classList.add('hidden');
                text.textContent = '表示';
            }
        }

        function switchTab(tab) {
            document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
            
            if (tab === 'list') {
                document.querySelector('.tab:nth-child(1)').classList.add('active');
                document.getElementById('tab-list').classList.add('active');
            } else {
                document.querySelector('.tab:nth-child(2)').classList.add('active');
                document.getElementById('tab-upload').classList.add('active');
            }
        }

        function loadFiles() {
            fetch('/list_files')
                .then(res => res.json())
                .then(data => {
                    const container = document.getElementById('file-list');
                    if (!data.success) {
                        container.innerHTML = '<div class="empty-message">' + data.error + '</div>';
                        return;
                    }
                    if (data.files.length === 0) {
                        container.innerHTML = '<div class="empty-message">音声ファイルがありません<br><small>設定で録音フォルダを指定してください</small></div>';
                        return;
                    }
                    container.innerHTML = data.files.map(f => {
                        const safePathAttr = btoa(unescape(encodeURIComponent(f.path)));
                        return `
                        <div class="file-item" data-path="${safePathAttr}" data-name="${f.name}" onclick="selectFileFromData(this)">
                            <span class="file-icon">🎵</span>
                            <div class="file-info">
                                <div class="file-name">${f.name}</div>
                                <div class="file-meta">${f.date} ${f.time}</div>
                            </div>
                            <span class="file-size">${f.size}</span>
                        </div>
                    `}).join('');
                });
        }

        function selectFileFromData(element) {
            const path = decodeURIComponent(escape(atob(element.dataset.path)));
            const name = element.dataset.name;
            selectFile(path, name, element);
        }

        function selectFile(path, name, element) {
            // 選択状態をリセット
            document.querySelectorAll('.file-item').forEach(el => el.classList.remove('selected'));
            element.classList.add('selected');
            
            selectedFilePath = path;
            selectedFileName = name;
            uploadedFile = null;
            
            document.getElementById('selected-file-name').textContent = name;
            document.getElementById('selected-file-display').classList.remove('hidden');
            document.getElementById('summarize-btn').disabled = false;
        }

        function setupDropZone() {
            const dropZone = document.getElementById('drop-zone');
            const fileInput = document.getElementById('file-input');

            dropZone.addEventListener('click', () => fileInput.click());
            dropZone.addEventListener('dragover', (e) => {
                e.preventDefault();
                dropZone.classList.add('dragover');
            });
            dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
            dropZone.addEventListener('drop', (e) => {
                e.preventDefault();
                dropZone.classList.remove('dragover');
                if (e.dataTransfer.files.length) {
                    handleUpload(e.dataTransfer.files[0]);
                }
            });
            fileInput.addEventListener('change', () => {
                if (fileInput.files.length) {
                    handleUpload(fileInput.files[0]);
                }
            });
        }

        function handleUpload(file) {
            uploadedFile = file;
            selectedFilePath = null;
            selectedFileName = file.name;
            
            document.querySelectorAll('.file-item').forEach(el => el.classList.remove('selected'));
            document.getElementById('selected-file-name').textContent = file.name;
            document.getElementById('selected-file-display').classList.remove('hidden');
            document.getElementById('summarize-btn').disabled = false;
        }

        function clearSelection() {
            selectedFilePath = null;
            selectedFileName = null;
            uploadedFile = null;
            document.querySelectorAll('.file-item').forEach(el => el.classList.remove('selected'));
            document.getElementById('selected-file-display').classList.add('hidden');
            document.getElementById('summarize-btn').disabled = true;
        }

        function saveSettings() {
            const apiKey = document.getElementById('api-key').value;
            const recordingDir = document.getElementById('recording-dir').value;
            const outputDir = document.getElementById('output-dir').value;
            
            fetch('/save_settings', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({api_key: apiKey, recording_dir: recordingDir, output_dir: outputDir})
            })
            .then(res => res.json())
            .then(data => {
                document.getElementById('settings-status').textContent = data.message;
                document.getElementById('settings-status').className = 'status ' + (data.success ? 'success' : 'error');
                if (data.success) {
                    loadFiles();  // フォルダが変わったら再読み込み
                }
            });
        }

        function summarize() {
            const status = document.getElementById('status');
            const resultCard = document.getElementById('result-card');
            const result = document.getElementById('result');
            const savedPath = document.getElementById('saved-path');
            
            status.textContent = '⏳ 処理中...';
            status.className = 'status';
            document.getElementById('summarize-btn').disabled = true;
            resultCard.classList.add('hidden');

            if (uploadedFile) {
                // アップロードファイルの場合
                const formData = new FormData();
                formData.append('file', uploadedFile);
                
                fetch('/summarize_upload', {
                    method: 'POST',
                    body: formData
                })
                .then(res => res.json())
                .then(handleResult)
                .catch(handleError);
            } else if (selectedFilePath) {
                // フォルダから選択の場合
                fetch('/summarize_path', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({path: selectedFilePath})
                })
                .then(res => res.json())
                .then(handleResult)
                .catch(handleError);
            }
        }

        function handleResult(data) {
            const status = document.getElementById('status');
            const resultCard = document.getElementById('result-card');
            const result = document.getElementById('result');
            const savedPath = document.getElementById('saved-path');
            
            if (data.success) {
                status.textContent = '✅ 完了';
                status.className = 'status success';
                result.textContent = data.summary;
                savedPath.textContent = '保存先: ' + data.saved_path;
                resultCard.classList.remove('hidden');
            } else {
                status.textContent = '❌ エラー: ' + data.error;
                status.className = 'status error';
            }
            document.getElementById('summarize-btn').disabled = false;
        }

        function handleError(err) {
            document.getElementById('status').textContent = '❌ エラー: ' + err;
            document.getElementById('status').className = 'status error';
            document.getElementById('summarize-btn').disabled = false;
        }
    </script>
</body>
</html>
"""

def load_config():
    """設定ファイルを読み込み"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"api_key": "", "recording_dir": DEFAULT_RECORDING_DIR, "output_dir": str(DEFAULT_OUTPUT_DIR)}

def save_config(config):
    """設定ファイルを保存"""
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)

def format_size(size_bytes):
    """ファイルサイズを読みやすい形式に変換"""
    if size_bytes < 1024:
        return f"{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"{size_bytes / 1024:.1f} KB"
    else:
        return f"{size_bytes / (1024 * 1024):.1f} MB"

def get_audio_files(directory):
    """指定ディレクトリから音声ファイル一覧を取得(日時順)"""
    audio_extensions = {'.m4a', '.mp3', '.wav', '.ogg', '.flac', '.aac'}
    files = []
    
    dir_path = Path(directory)
    if not dir_path.exists():
        return []
    
    for file_path in dir_path.iterdir():
        if file_path.is_file() and file_path.suffix.lower() in audio_extensions:
            stat = file_path.stat()
            mtime = datetime.fromtimestamp(stat.st_mtime)
            files.append({
                'name': file_path.name,
                'path': str(file_path),
                'date': mtime.strftime('%Y/%m/%d'),
                'time': mtime.strftime('%H:%M'),
                'timestamp': stat.st_mtime,
                'size': format_size(stat.st_size)
            })
    
    # 日時の新しい順にソート
    files.sort(key=lambda x: x['timestamp'], reverse=True)
    return files

def summarize_audio(audio_path, api_key):
    """音声ファイルをGemini APIで要約"""
    genai.configure(api_key=api_key)
    audio_file = genai.upload_file(audio_path)
    
    # モデル設定(精度重視なら gemini-2.5-pro に変更)
    model = genai.GenerativeModel("gemini-2.5-flash")
    
    prompt = """
この通話録音を分析し、以下の形式で日本語で要約してください。
ビジネス用途を想定し、次のアクションが明確になるよう整理してください。
出力はプレーンテキスト形式で、Markdownは使わないでください。

■ 基本情報
・通話相手:(名前・会社名・役職が分かれば記載、不明なら「不明」)
・通話目的:(1行で)

■ 通話概要
(2-3文で通話の内容を要約)

■ 主なポイント
・(重要な点を箇条書きで3-5個)

■ 決定事項
・(合意した内容、確定した事項。なければ「特になし」)

■ 次のアクション(TODO)
・自分: (やるべきこと)【期限: ○月○日】
・相手: (相手がやること)【期限: ○月○日】
※アクションがなければ「特になし」と記載

■ フォローアップ
・次回連絡予定:(日時が決まっていれば記載、なければ「未定」)
・確認待ち事項:(相手からの返答待ちなど。なければ「なし」)

■ 備考
(その他気になった点、注意事項など。なければ省略可)
"""
    
    response = model.generate_content([audio_file, prompt])
    return response.text

def save_summary(summary, audio_name, output_dir):
    """要約をテキストファイルとして保存"""
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)
    
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    stem = Path(audio_name).stem
    save_path = output_path / f"要約_{stem}_{timestamp}.txt"
    
    content = f"""通話要約
{'=' * 40}

元ファイル: {audio_name}
生成日時: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}

{'=' * 40}

{summary}
"""
    with open(save_path, "w", encoding="utf-8") as f:
        f.write(content)
    
    return str(save_path)


@app.route("/")
def index():
    config = load_config()
    return render_template_string(
        HTML_TEMPLATE,
        api_key=config.get("api_key", ""),
        recording_dir=config.get("recording_dir", ""),
        output_dir=config.get("output_dir", str(DEFAULT_OUTPUT_DIR))
    )

@app.route("/save_settings", methods=["POST"])
def api_save_settings():
    data = request.json
    config = {
        "api_key": data.get("api_key", ""),
        "recording_dir": data.get("recording_dir", ""),
        "output_dir": data.get("output_dir", str(DEFAULT_OUTPUT_DIR))
    }
    save_config(config)
    return jsonify({"success": True, "message": "設定を保存しました"})

@app.route("/list_files")
def api_list_files():
    config = load_config()
    recording_dir = config.get("recording_dir") or DEFAULT_RECORDING_DIR
    
    if not Path(recording_dir).exists():
        return jsonify({"success": False, "error": f"フォルダが見つかりません: {recording_dir}"})
    
    files = get_audio_files(recording_dir)
    return jsonify({"success": True, "files": files})

@app.route("/summarize_path", methods=["POST"])
def api_summarize_path():
    """フォルダから選択したファイルを要約"""
    config = load_config()
    api_key = config.get("api_key")
    output_dir = config.get("output_dir", str(DEFAULT_OUTPUT_DIR))
    
    if not api_key:
        return jsonify({"success": False, "error": "APIキーが設定されていません"})
    
    data = request.json
    file_path = data.get("path")
    
    if not file_path or not Path(file_path).exists():
        return jsonify({"success": False, "error": "ファイルが見つかりません"})
    
    try:
        summary = summarize_audio(file_path, api_key)
        saved_path = save_summary(summary, Path(file_path).name, output_dir)
        return jsonify({"success": True, "summary": summary, "saved_path": saved_path})
    except Exception as e:
        return jsonify({"success": False, "error": str(e)})

@app.route("/summarize_upload", methods=["POST"])
def api_summarize_upload():
    """アップロードされたファイルを要約"""
    config = load_config()
    api_key = config.get("api_key")
    output_dir = config.get("output_dir", str(DEFAULT_OUTPUT_DIR))
    
    if not api_key:
        return jsonify({"success": False, "error": "APIキーが設定されていません"})
    
    if "file" not in request.files:
        return jsonify({"success": False, "error": "ファイルがありません"})
    
    file = request.files["file"]
    if file.filename == "":
        return jsonify({"success": False, "error": "ファイルが選択されていません"})
    
    # 一時保存
    temp_dir = Path(__file__).parent / "temp"
    temp_dir.mkdir(exist_ok=True)
    temp_path = temp_dir / file.filename
    file.save(temp_path)
    
    try:
        summary = summarize_audio(str(temp_path), api_key)
        saved_path = save_summary(summary, file.filename, output_dir)
        return jsonify({"success": True, "summary": summary, "saved_path": saved_path})
    except Exception as e:
        return jsonify({"success": False, "error": str(e)})
    finally:
        if temp_path.exists():
            temp_path.unlink()


if __name__ == "__main__":
    print("=" * 50)
    print("通話録音 要約アプリを起動しました")
    print("ブラウザで http://localhost:5000 を開いてください")
    print("終了するには Ctrl+C を押してください")
    print("=" * 50)
    
    # ブラウザを自動で開く(Windows)
    import webbrowser
    webbrowser.open("http://localhost:5000")
    
    app.run(host="127.0.0.1", port=5000, debug=False)

使い方

1. 起動

python app.py

自動的にブラウザが開きます。開かない場合は手動で http://localhost:5000 にアクセスしてください。

2. 初期設定

「設定」→「表示」をクリックして以下を入力:

  • Gemini APIキー: Google AI Studioで取得したAPIキー
  • 録音フォルダ: Google Drive同期フォルダ内の録音保存場所
    • 例: G:\マイドライブ\通話録音
    • 例: C:\Users\ユーザー名\Google Drive\通話録音
  • 要約の保存先: 要約ファイルの保存場所(Google Driveフォルダにするとスマホから見れる)
    • 例: G:\マイドライブ\通話要約

3. ファイルを選択

2つの方法がある:

  • フォルダから選択: 録音フォルダ内のファイル一覧が表示されます。日時順にソートされているので、最新の録音が一番上に表示されます。
  • ファイルをアップロード: ドラッグ&ドロップまたはクリックして選択

4. 要約を生成

「要約を生成」ボタンをクリックします。数十秒〜1分程度で結果が表示されます。

5. 結果の確認

  • 画面に要約が表示される
  • 同時に保存先フォルダにテキストファイルとして保存される
  • Google Driveに保存していれば、スマホのGoogleドライブアプリからも確認できる

Windows用の起動ショートカット

毎回コマンドを打つのが面倒なら、バッチファイルを作成:

start_app.bat:

@echo off
cd /d "%~dp0"
python app.py

このファイルをダブルクリックするだけで起動できます。


2-2B. Google Colabで実行する場合(環境構築不要:これ、webで設定できますが、やり方を見当付けないといけなかったので、ちょっとお勧め出来ません)

PCにPythonをインストールしたくない場合、Google Colabを使えばブラウザだけで実行できます。

手順

  1. Google Colab にアクセス
  2. 「ノートブックを新規作成」
  3. 以下のコードをセルに貼り付けて実行

セル1: Googleドライブをマウント

from google.colab import drive
drive.mount('/content/drive')

実行すると認証画面が出るので、Googleアカウントでログインしてください。

セル2: ライブラリをインストール

!pip install -q google-generativeai

セル3: APIキーを設定

import os

# ここにAPIキーを入力(他人に見せないよう注意)
os.environ["GEMINI_API_KEY"] = "your-api-key-here"

セル4: 要約関数を定義

import google.generativeai as genai
from datetime import datetime

def summarize_call(audio_path: str, save_to_drive: bool = True) -> str:
    """通話録音を要約"""
    api_key = os.environ.get("GEMINI_API_KEY")
    genai.configure(api_key=api_key)
    
    print(f"処理中: {audio_path}")
    
    # 音声ファイルをアップロード
    audio_file = genai.upload_file(audio_path)
    
    # モデル設定(精度重視なら gemini-2.5-pro、コスト重視なら gemini-2.5-flash)
    model = genai.GenerativeModel("gemini-2.5-flash")
    
    prompt = """
この通話録音を分析し、以下の形式で日本語で要約してください。
ビジネス用途を想定し、次のアクションが明確になるよう整理してください。
出力はプレーンテキスト形式で、Markdownは使わないでください。

■ 基本情報
・通話相手:(名前・会社名・役職が分かれば記載、不明なら「不明」)
・通話目的:(1行で)

■ 通話概要
(2-3文で通話の内容を要約)

■ 主なポイント
・(重要な点を箇条書きで3-5個)

■ 決定事項
・(合意した内容、確定した事項。なければ「特になし」)

■ 次のアクション(TODO)
・自分: (やるべきこと)【期限: ○月○日】
・相手: (相手がやること)【期限: ○月○日】
※アクションがなければ「特になし」と記載

■ フォローアップ
・次回連絡予定:(日時が決まっていれば記載、なければ「未定」)
・確認待ち事項:(相手からの返答待ちなど。なければ「なし」)

■ 備考
(その他気になった点、注意事項など。なければ省略可)
"""
    
    response = model.generate_content([audio_file, prompt])
    summary = response.text
    
    # Googleドライブに保存(テキスト形式)
    if save_to_drive:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"要約_{timestamp}.txt"
        # 保存先フォルダ(必要に応じて変更)
        save_path = f"/content/drive/MyDrive/通話要約/{filename}"
        
        # フォルダがなければ作成
        os.makedirs(os.path.dirname(save_path), exist_ok=True)
        
        content = f"""通話要約
{'=' * 40}

生成日時: {datetime.now()}

{'=' * 40}

{summary}
"""
        
        with open(save_path, "w", encoding="utf-8") as f:
            f.write(content)
        
        print(f"✅ 保存しました: {save_path}")
    
    return summary

print("準備完了!")

セル5: 要約を実行

# Googleドライブ内の録音ファイルのパスを指定
audio_path = "/content/drive/MyDrive/通話録音/recording.m4a"

# 要約を実行
result = summarize_call(audio_path)
print(result)

使い方のコツ

  1. 録音ファイルの場所を確認: 左サイドバーのフォルダアイコン → driveMyDrive → 録音を保存したフォルダを探す
  2. パスをコピー: ファイルを右クリック →「パスをコピー」
  3. セル5の audio_path に貼り付けて実行

要約結果は自動的に MyDrive/通話要約/ フォルダに保存されます。スマホのGoogleドライブアプリからすぐに確認できます。


2-2C. Google Apps Script(完全自動化)- スマホだけで完結(お勧めです。私はこれにしました)

PCを使わず、録音をGoogle Driveに保存するだけで自動的に要約が生成される方式です。

トレードオフ

項目内容
メリット完全自動。スマホで録音保存→数分後に要約完成
デメリット音声データがGoogleサーバーで処理される

⚠️ 注意: 契約金額・個人情報など機密性の高い内容を含む通話には使用しないでください。日程調整・進捗確認など一般的なビジネス通話向けです。

仕組み

スマホ: 通話録音 → 共有 → Google Drive「通話録音」フォルダに保存
    ↓(5分ごとに自動チェック)
Google Apps Script: 新しいファイルを検出 → Gemini APIで要約 → テキスト保存
    ↓
スマホ: Googleドライブアプリで要約を確認(数分後には完成)

セットアップ手順

1. Google Apps Scriptを開く

https://script.google.com/ にアクセス → 「新しいプロジェクト」

2. コードを貼り付け

デフォルトのコードを全て削除し、以下を貼り付け:

/**
 * 通話録音自動要約スクリプト
 * Google Drive内の録音フォルダを監視し、新しいファイルをGemini APIで要約
 */

// ===== 設定 =====
const CONFIG = {
  GEMINI_API_KEY: 'YOUR_API_KEY_HERE',  // Google AI StudioのAPIキー
  RECORDING_FOLDER_NAME: '通話録音',      // 録音を保存するフォルダ名
  SUMMARY_FOLDER_NAME: '通話要約',        // 要約を保存するフォルダ名
  MODEL: 'gemini-2.5-flash'              // 使用するモデル
};

// 要約プロンプト
const PROMPT = `
この通話録音を分析し、以下の形式で日本語で要約してください。
ビジネス用途を想定し、次のアクションが明確になるよう整理してください。
出力はプレーンテキスト形式で、Markdownは使わないでください。

■ 基本情報
・通話相手:(名前・会社名・役職が分かれば記載、不明なら「不明」)
・通話目的:(1行で)

■ 通話概要
(2-3文で通話の内容を要約)

■ 主なポイント
・(重要な点を箇条書きで3-5個)

■ 決定事項
・(合意した内容、確定した事項。なければ「特になし」)

■ 次のアクション(TODO)
・自分: (やるべきこと)【期限: ○月○日】
・相手: (相手がやること)【期限: ○月○日】
※アクションがなければ「特になし」と記載

■ フォローアップ
・次回連絡予定:(日時が決まっていれば記載、なければ「未定」)
・確認待ち事項:(相手からの返答待ちなど。なければ「なし」)

■ 備考
(その他気になった点、注意事項など。なければ省略可)
`;

/**
 * メイン処理: 新しい録音ファイルを検出して要約
 */
function processNewRecordings() {
  const recordingFolder = getOrCreateFolder(CONFIG.RECORDING_FOLDER_NAME);
  const summaryFolder = getOrCreateFolder(CONFIG.SUMMARY_FOLDER_NAME);
  
  // 音声ファイルを取得
  const audioTypes = ['audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'];
  const files = recordingFolder.getFiles();
  
  while (files.hasNext()) {
    const file = files.next();
    const mimeType = file.getMimeType();
    
    // 音声ファイルかチェック
    if (!audioTypes.some(type => mimeType.includes(type.split('/')[1]))) {
      continue;
    }
    
    // 既に処理済みかチェック(要約ファイルが存在するか)
    const summaryFileName = '要約_' + file.getName().replace(/\.[^.]+$/, '') + '.txt';
    const existingSummaries = summaryFolder.getFilesByName(summaryFileName);
    if (existingSummaries.hasNext()) {
      continue;  // 既に要約済み
    }
    
    // 要約を生成
    Logger.log('処理中: ' + file.getName());
    try {
      const summary = summarizeAudio(file);
      saveSummary(summary, file.getName(), summaryFolder);
      Logger.log('完了: ' + file.getName());
    } catch (e) {
      Logger.log('エラー: ' + file.getName() + ' - ' + e.message);
    }
    
    // API制限対策で少し待機
    Utilities.sleep(2000);
  }
}

/**
 * Gemini APIで音声ファイルを要約
 */
function summarizeAudio(file) {
  // ファイルをBase64エンコード
  const blob = file.getBlob();
  const base64Data = Utilities.base64Encode(blob.getBytes());
  const mimeType = file.getMimeType();
  
  // Gemini API呼び出し
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${CONFIG.MODEL}:generateContent?key=${CONFIG.GEMINI_API_KEY}`;
  
  const payload = {
    contents: [{
      parts: [
        {
          inline_data: {
            mime_type: mimeType,
            data: base64Data
          }
        },
        {
          text: PROMPT
        }
      ]
    }]
  };
  
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  const response = UrlFetchApp.fetch(url, options);
  const result = JSON.parse(response.getContentText());
  
  if (result.error) {
    throw new Error(result.error.message);
  }
  
  return result.candidates[0].content.parts[0].text;
}

/**
 * 要約をテキストファイルとして保存
 */
function saveSummary(summary, originalFileName, folder) {
  const timestamp = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss');
  const fileName = '要約_' + originalFileName.replace(/\.[^.]+$/, '') + '.txt';
  
  const content = `通話要約
========================================

元ファイル: ${originalFileName}
生成日時: ${timestamp}

========================================

${summary}
`;
  
  folder.createFile(fileName, content, MimeType.PLAIN_TEXT);
}

/**
 * フォルダを取得または作成
 */
function getOrCreateFolder(folderName) {
  const folders = DriveApp.getFoldersByName(folderName);
  if (folders.hasNext()) {
    return folders.next();
  }
  return DriveApp.createFolder(folderName);
}

/**
 * 初期セットアップ: フォルダ作成とトリガー設定
 */
function setup() {
  // フォルダ作成
  getOrCreateFolder(CONFIG.RECORDING_FOLDER_NAME);
  getOrCreateFolder(CONFIG.SUMMARY_FOLDER_NAME);
  
  // 既存のトリガーを削除
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => {
    if (trigger.getHandlerFunction() === 'processNewRecordings') {
      ScriptApp.deleteTrigger(trigger);
    }
  });
  
  // 5分ごとに実行するトリガーを設定
  ScriptApp.newTrigger('processNewRecordings')
    .timeBased()
    .everyMinutes(5)
    .create();
  
  Logger.log('セットアップ完了');
  Logger.log('録音フォルダ: ' + CONFIG.RECORDING_FOLDER_NAME);
  Logger.log('要約フォルダ: ' + CONFIG.SUMMARY_FOLDER_NAME);
  Logger.log('5分ごとに自動チェックします');
}

/**
 * 手動テスト用
 */
function testRun() {
  processNewRecordings();
}

3. APIキーを設定

コード冒頭の YOUR_API_KEY_HERE を、Google AI Studioで取得したAPIキーに置き換え:

GEMINI_API_KEY: 'AIzaSy...',  // 実際のAPIキーを入力

4. 初期セットアップを実行

  • 上部のドロップダウンで「setup」を選択
  • 「実行」ボタンをクリック
  • 初回は認証画面が出るので「許可」

これで以下が自動設定される:

  • Google Driveに「通話録音」「通話要約」フォルダが作成
  • 5分ごとに自動チェックするトリガーが設定

5. スマホで使う

  1. 通話終了後、電話アプリ → 通話履歴 → 録音を共有
  2. Google Driveの「通話録音」フォルダを選択して保存
  3. 5分以内に「通話要約」フォルダに要約が自動生成される
  4. Googleドライブアプリで要約を確認

動作確認

testRun 関数を実行すると、録音フォルダ内のファイルを即座に処理できます。

注意事項

  • 従量課金が必要: 無料枠はデータがモデル改善に使用される可能性あり
  • ファイルサイズ制限: 大きなファイル(長時間通話)は処理できない場合あり
  • API制限: 短時間に大量処理するとレート制限に引っかかる
  • 重複処理防止: 同じファイル名の要約が既にあればスキップ

Part 3: 出力サンプル

実際にどのような要約が生成されるかのサンプル:

通話要約
========================================

元ファイル: 通話録音_20241220_143052.m4a
生成日時: 2024-12-20 14:45:23

========================================

■ 基本情報
・通話相手: 山田太郎さん(ABC商事 営業部)
・通話目的: 来月の納品スケジュールの確認

■ 通話概要
ABC商事の山田さんから、1月納品予定の製品Aについてスケジュール確認の連絡。
当初1月15日納品予定だったが、先方の倉庫都合で1月20日以降に変更希望。
こちらは対応可能と回答し、正式な納品日は来週中に確定する流れとなった。

■ 主なポイント
・製品Aの納品日を1月15日→1月20日以降に変更希望
・理由は先方の倉庫改装工事(1月10日〜18日)
・数量は当初予定通り500個で変更なし
・請求書の日付も納品日に合わせて変更が必要

■ 決定事項
・納品日を1月20日以降に変更することで合意
・数量500個は変更なし

■ 次のアクション(TODO)
・自分: 倉庫に1月20日以降の納品可能日を確認【期限: 12/23(月)】
・自分: 確定した納品日を山田さんにメール連絡【期限: 12/25(水)】
・相手: 届いたメールを確認して最終承認【期限: 12/26(木)】

■ フォローアップ
・次回連絡予定: 12月25日(水)にこちらからメール
・確認待ち事項: なし

■ 備考
山田さんは年末12/28〜1/5まで休暇予定。急ぎの連絡は12/27までに。

このように、通話後に「誰が何をいつまでにやるか」が整理される形で出力されます。 テキスト形式なので、Googleドライブアプリでそのまま閲覧できます。


Part 4: 実際の運用フロー

パターン1: PCメインで使う人(ローカルアプリ)

1. 通話終了(スマホ)
   └→ 電話アプリ → 通話履歴 → 録音を共有 → Googleドライブの「通話録音」フォルダに保存

2. PCで作業中に要約
   └→ アプリを起動(start_app.batをダブルクリック)
   └→ ファイル一覧から録音を選択(日時順で表示)
   └→ 「要約を生成」をクリック

3. 要約がGoogleドライブに保存される
   └→ PCでもスマホでも閲覧可能

ポイント: Google Drive for Desktopで同期していれば、スマホで保存した録音がPCのファイル一覧に自動で表示されます。

パターン2: スマホメインで使う人(Google Colab)

1. 通話終了(スマホ)
   └→ 電話アプリ → 通話履歴 → 録音を共有 → Googleドライブに保存

2. 要約したいときにGoogle Colabを開く(PCでもスマホブラウザでも可)
   └→ ノートブックを開いて実行
   └→ 要約がGoogleドライブの「通話要約」フォルダに保存される

3. スマホのGoogleドライブアプリで要約を確認

パターン3: 完全自動化(Google Apps Script)★おすすめ

1. 通話終了(スマホ)
   └→ 電話アプリ → 通話履歴 → 録音を共有 → Googleドライブの「通話録音」フォルダに保存

2. 何もしない(5分待つだけ)
   └→ GASが自動で新しいファイルを検出
   └→ Gemini APIで要約生成
   └→ 「通話要約」フォルダにテキスト保存

3. スマホのGoogleドライブアプリで要約を確認

メリット: PC不要。録音を保存するだけで数分後には要約が完成しています。 注意: 機密性の高い通話には使用しないでください(日程調整・進捗確認など一般的な通話向けです)。

Tips:

  • ローカルアプリは start_app.bat をダブルクリックするだけで起動、ブラウザも自動で開く
  • Google Colabのノートブックは保存しておけば、次回から開いてセル5だけ実行すればOK
  • GAS版は初期設定後は完全放置でOK

Part 5: 活用シーン

営業・商談

  • 顧客との商談内容を記録
  • 「言った・言わない」のトラブル防止
  • 次回訪問前に前回の内容を確認

プロジェクト管理

  • 打ち合わせ後のTODOを自動抽出
  • 期限付きタスクを漏れなく把握
  • チームへの共有資料として活用

カスタマーサポート

  • 問い合わせ内容の記録
  • 対応履歴の蓄積
  • エスカレーション時の情報共有

個人の備忘録

  • 重要な電話の内容を後から確認
  • 約束した日時・場所の確認
  • 「あの時なんて言ってたっけ」を解消

Part 6: 自分用のリマインダー連携

要約に含まれる「次のアクション」や「次回連絡予定」を自分のカレンダーに登録すると、フォローアップ漏れを防げます。

Googleカレンダーに手動登録:有料公開するならこれも自動登録までしますけど、今回はそこまでしません。

要約の「次のアクション」セクションを見て、期限のあるタスクをカレンダーに登録:

  1. 要約ファイルを開く
  2. 「次のアクション」の期限を確認
  3. Googleカレンダーに予定として登録

注意: 要約内容をカレンダーのタイトルや説明にコピペする際は、相手の個人情報(氏名・電話番号など)を含めないようご注意ください。「○○社へ連絡」程度に留めることをおすすめします。

Google Keepでタスク管理:有料公開するならこれも自動登録までしますけど、今回はそこまでしません。

要約のTODOをGoogle Keepにコピーしてチェックリスト化:

  1. 要約の「次のアクション」をコピー
  2. Google Keepで新しいメモ → チェックリストとして貼り付け
  3. 完了したらチェック

⚠️ 他サービスへの連携について

法的制約のセクションで述べた通り、通話内容を第三者に共有することは避けるべきです。以下の連携はおすすめしません

  • ❌ Slack/Teamsへの自動通知(チームメンバーへの共有)
  • ❌ Notionの共有ワークスペースへの保存
  • ❌ 社内WikiやConfluenceへの投稿

要約は自分だけがアクセスできる場所に保存し、タスク管理も自分用のツールで行うことをおすすめします。


トラブルシューティング

「APIキーが無効です」エラー

  • Google AI StudioでAPIキーが有効か確認してください
  • ローカルアプリ: 設定画面でAPIキーが正しく入力されているか確認してください
  • Colab: セル3でAPIキーを設定したか確認してください
  • 課金が有効化されているか確認してください(無料枠を使い切った可能性があります)

「ファイル形式がサポートされていません」エラー

Gemini APIは以下の形式をサポートしています:

  • 音声: MP3, WAV, AIFF, AAC, OGG, FLAC, M4A

Pixelの通話録音は通常 .m4a 形式で保存されるので問題ないはずです。

ローカルアプリが起動しない

  • Python 3.10以上がインストールされているか確認してください
  • 必要なライブラリがインストールされているか確認してください: pip install google-generativeai flask
  • ポート5000が他のアプリに使用されていないか確認してください

Windowsでpipインストール時にエラーが出る

以下のようなエラーが出る場合:

ERROR: Could not install packages due to an OSError: [WinError 2]

対処法1: 管理者権限で実行

  1. スタートメニューで「cmd」と検索します
  2. 「コマンドプロンプト」を右クリック → 「管理者として実行」を選択します
  3. 再度 pip install google-generativeai flask を実行します

対処法2: –userオプションを使う

pip install --user google-generativeai flask

対処法3: 一度アンインストールしてから再インストール

pip uninstall grpcio grpcio-status
pip install google-generativeai flask

ローカルアプリにアクセスできない

  • ブラウザで http://localhost:5000 を開いているか確認してください(httpsではなくhttp
  • ファイアウォールでブロックされていないか確認してください
  • コマンドプロンプト/ターミナルでエラーが出ていないか確認してください

Google Colabで「ドライブが見つからない」

  • セル1のドライブマウントを実行したか確認してください
  • 認証画面でGoogleアカウントにログインしたか確認してください
  • 左サイドバーの更新ボタンを押してください

要約の精度が低い

  • 録音の音質が悪い場合、認識精度が下がります
  • 通話時間が長い場合(30分以上)、要約が不完全になることがあります
  • プロンプトを調整して試してみてください

まとめ

方式手間セキュリティおすすめ用途
ローカルアプリPCで操作◎ 高い機密性の高い通話
Google Colabブラウザで実行○ 普通PCがない時
GAS自動化完全自動△ 注意が必要日程調整・進捗確認など

ビジネス活用のポイント

  1. 次のアクションが明確になりやすい – 「誰が」「何を」「いつまでに」が整理されて出力されます
  2. フォローアップ漏れの防止に役立つ – 次回連絡予定・確認待ち事項が自動抽出されます
  3. どこからでも閲覧可能 – 外出先でもスマホから要約を確認できます
  4. 履歴として蓄積できる – Googleドライブに時系列で保存され、後から検索できます
  5. 比較的低コストで運用可能 – 月¥2,000〜¥5,000程度(利用頻度による)

運用パターンの参考

GAS自動化が向いているケース(一般的な通話)

  • 日程調整、進捗確認、簡単な相談など
  • 録音を保存するだけで数分後に要約が生成されます
  • ※契約金額・個人情報など機密情報を含む通話には使用を避けてください

ローカルアプリが向いているケース(機密性を重視する通話)

  • 契約交渉、金額の話、人事関連など
  • データはAPIに送られますが、従量課金であれば再学習には使用されないとされています

参考リンク

コメント

タイトルとURLをコピーしました