Whisper + Ollama で作る完全ローカル議事録パイプライン——機密会議もクラウド送信ゼロで自動化する
はじめに
「会議の録音データをクラウドに上げるのはちょっと……」
そう感じたことが一度はあるはずです。クライアントとの要件定義、採用候補者との技術面接、競合分析や戦略を含む設計レビュー——守秘義務の高い会議ほど、手動での書き起こしに時間を費やすか、クラウド API に音声を送るリスクを取るかの二択を迫られていました。
Amanity では現在、この問題を Whisper + Ollama の完全ローカルパイプライン で解決して実運用しています。Whisper でローカル書き起こし、Ollama でローカル LLM 議事録生成——音声データはマシンの外に出ず、書き起こし〜要約〜議事録 Markdown の生成まで、すべてが手元で完結します。API 費用もゼロです。
この記事では、その構成と実装を余すところなく公開します。環境構築から本番で踏んだ落とし穴まで、コピーして使えるレベルで書きました。
なぜ「完全ローカル」にこだわるのか
クラウド API が抱える 3 つのリスク
音声書き起こし API(OpenAI Whisper API、Google Cloud Speech-to-Text など)はたしかに高精度で手軽です。しかし、ビジネス利用では以下の 3 点が見過ごせません。
1. データ主権の問題
API に音声を送った瞬間、そのデータはサービス提供者のサーバーに渡ります。各社の利用規約でモデル改善への使用が明記されているケースもあります。機密情報が含まれた音声を送ることの法的・倫理的リスクは、企業規模が大きくなるほど重大になります。
2. 情報漏洩の接触面が広がる
クラウド API を経由する処理は、通信経路・API プロバイダー側のサーバー・ログシステムと、情報が触れる箇所が増えます。内部告発やセキュリティインシデントが API プロバイダー側で起きた場合、自社データが巻き込まれるリスクがあります。
3. コスト管理の困難さ
1 時間の音声を毎週処理すれば、月に数千〜数万円のコストが発生します。会議数が増えるほど費用は青天井です。しかも音声の長さや解像度によってコストが変動するため、予算管理が煩雑になります。
完全ローカルで得られるもの
| 観点 | クラウド API | 完全ローカル |
|---|---|---|
| データの行き先 | API プロバイダーのサーバー | 自分のマシンのみ |
| 月間コスト | 音声時間 × 単価(変動) | ゼロ(電力コストのみ) |
| ネット接続 | 必須 | 不要 |
| 処理速度の上限 | API レート制限あり | ローカルリソース次第 |
| カスタマイズ性 | API の仕様に依存 | 自由に拡張可能 |
ローカル処理のデメリットは「最初の環境構築コスト」と「ハードウェア性能への依存」です。ただし、一度動けばメンテナンスはほぼ不要で、長期的なコスト・セキュリティの優位性は圧倒的になります。
システム構成の全体像
パイプラインは大きく 2 つのコンポーネントで構成されます。
- faster-whisper: 音声ファイルを日本語テキストに書き起こすエンジン
- Ollama: 書き起こしテキストを受け取り、議事録形式の Markdown を生成するローカル LLM
処理の流れ:
- 会議録音ファイル(MP3/WAV/M4A)を入力として渡す
- faster-whisper がタイムスタンプ付きで書き起こす(話者分離なし、セグメント単位)
- 書き起こしテキストを Ollama のローカル LLM にプロンプトで渡す
- LLM が「要約・決定事項・次回アクション(担当者付き)」を抽出して Markdown を生成する
- 日付付きファイル名で指定ディレクトリに保存する
データがローカルを出るのはゼロ。ネットワーク通信はモデルの初回ダウンロード時のみです。
環境構築——macOS / Linux 対応
前提
- Python 3.10 以上
- RAM: 8GB 以上(LLM 7B モデル推奨要件)
- macOS(Apple Silicon または Intel)または Linux
faster-whisper のインストール
faster-whisper は OpenAI の Whisper を CTranslate2 で高速化した実装です。公式 Whisper より 4 倍速く、メモリ消費も半分以下になります。
pip install faster-whispermacOS で Metal(Apple GPU)を使いたい場合は、別途 mlx-whisper も選択肢になります。ただし今回のパイプラインは faster-whisper で統一します(クロスプラットフォーム対応のため)。
モデルサイズの選択:
| モデル | VRAM / RAM | 精度 | 処理速度 |
|---|---|---|---|
tiny | ~1GB | 低 | 最速 |
base | ~1GB | 中低 | 速い |
small | ~2GB | 中 | 普通 |
medium | ~5GB | 高 | 遅い |
large-v3 | ~10GB | 最高 | 最遅 |
日本語議事録の実用水準は medium 以上だが、固有名詞(人名・プロダクト名)が多い会議には large-v3 を強く推奨します。実測では medium で人名の誤認識が複数発生したのに対し、large-v3 は日本語確信度 1.00 で固有名詞も正確でした。ただし M2 Mac / CPU(int8) で 3 分音声の処理に約 212 秒かかります。速度より精度を取るか、チャンク分割と組み合わせるかで選択することをおすすめします(後述)。
Ollama のインストール
# macOS
brew install ollama
# Linux
curl -fsSL https://ollama.com/install.sh | shインストール後、Ollama サービスを起動する:
ollama serveOllama モデル選定マトリクス
議事録生成のような「長文テキストの読解 → 構造化出力」タスクに向くモデルを 4 つ比較しました。いずれも 7〜9B パラメータ帯の無料モデルです。
比較対象モデル
| モデル | サイズ | 日本語対応 | コンテキスト長 | 特徴 |
|---|---|---|---|---|
qwen2.5:7b | 4.7GB | ◎ | 32K | 中国系モデル。日本語の流暢さが高い |
gemma3:9b | 5.4GB | ○ | 8K | Google 製。論理的な文章整形が得意 |
llama3.1:8b | 4.9GB | △ | 128K | Meta 製。コンテキスト長が圧倒的 |
mistral:7b | 4.1GB | △ | 32K | 軽量・高速。日本語は少し不安定 |
評価基準と結果
Amanity の実際の会議録音(3 分、商談の断片)を large-v3 で書き起こし、qwen2.5:7b で議事録を生成して確認しました。他の 3 モデルは公開ベンチマークと社内での短時間試用をもとにした参考値です。
日本語の自然さ(5 段階評価):
| モデル | 日本語品質 | 要約の正確さ | TODO 抽出精度 | 処理時間(M2 Mac / 16GB) |
|---|---|---|---|---|
| qwen2.5:7b | ★★★★★ | ★★★★ | ★★★★ | 22 秒(実測) |
| gemma3:9b | ★★★★ | ★★★★★ | ★★★★ | 約 40 秒(参考値) |
| llama3.1:8b | ★★★ | ★★★★ | ★★★ | 約 30 秒(参考値) |
| mistral:7b | ★★ | ★★★ | ★★★ | 約 20 秒(参考値) |
qwen2.5:7b の実測所見:
断片的な会話(3 分、複数話者、技術用語あり)を渡しても、会議の目的・主要トピック・決定事項を正しく抽出できました。「バッジ処理」「データベースマイグレーション」といった固有の業界用語も文脈を正しく読んでいました。日本語の文章として自然で、そのまま共有できるレベルです。
環境別推奨モデル
| 環境 | 推奨モデル | 理由 |
|---|---|---|
| M1/M2/M3 Mac(16GB+) | qwen2.5:7b | Metal GPU オフロードが効き、日本語品質が最高 |
| M1/M2 Mac(8GB) | mistral:7b | メモリ節約。日本語品質は妥協が必要 |
| NVIDIA GPU(8GB VRAM+) | gemma3:9b または qwen2.5:7b | CUDA が効き処理が速い |
| CPU-only(RAM 16GB+) | qwen2.5:7b | 遅いが品質優先。処理中は他作業を控える |
| Docker コンテナ | mistral:7b | コンテナ環境ではメモリ上限に余裕を持たせる |
モデルのダウンロードは以下のコマンドで実行します:
# 推奨モデルをプル
ollama pull qwen2.5:7b
# 確認
ollama listコア実装
ここからが本題です。書き起こし〜議事録生成までをひとつのスクリプトで完結させます。
ディレクトリ構成
minutes-pipeline/
├── pipeline.py # メインスクリプト
├── cli.sh # CLIラッパー
├── output/ # 議事録出力先
└── requirements.txtrequirements.txt:
faster-whisper>=1.0.0
ollama>=0.3.0pipeline.py(コア実装)
"""
Whisper + Ollama 完全ローカル議事録パイプライン
音声ファイル → 書き起こし → 議事録 Markdown を生成する
"""
import os
import sys
import json
from datetime import datetime
from pathlib import Path
from faster_whisper import WhisperModel
import ollama
# ===== 設定 =====
WHISPER_MODEL_SIZE = "medium" # tiny / base / small / medium / large-v3
WHISPER_DEVICE = "auto" # auto / cpu / cuda / mps(M2 Mac では Metal または CPU が自動選択)
OLLAMA_MODEL = "qwen2.5:7b" # 使用するローカル LLM
OUTPUT_DIR = Path("output") # 議事録の保存先
# ===== プロンプトテンプレート =====
MINUTES_PROMPT = """あなたは優秀な会議秘書です。以下の会議書き起こしテキストを読み、
議事録を作成してください。
## 書き起こしテキスト
{transcript}
## 出力形式(必ず以下の Markdown 形式で出力してください)
# 議事録
**日時**: {meeting_date}
**書き起こし文字数**: {char_count} 字
---
## 1. 会議の概要(3〜5 文)
(会議全体の目的・背景・結論を簡潔に)
## 2. 主要な議論ポイント
(箇条書きで 5〜10 点)
## 3. 決定事項
(「〜することが決定した」の形式で箇条書き。決定がない場合は「なし」と書く)
## 4. 次回アクション(TODO)
(「担当: 〇〇 内容: 〜する 期限: 〜まで」の形式で箇条書き。不明な場合は「担当: 不明」)
## 5. 次回会議について
(次回の議題・日時が言及されていれば記載。なければ「言及なし」)
---
**注意**: 書き起こしに含まれない情報は推測で補わないこと。不明な点は「(聞き取り不明)」と記載すること。
"""
def transcribe_audio(audio_path: str) -> tuple[str, list[dict]]:
"""
faster-whisper で音声ファイルを書き起こす。
Returns: (フルテキスト, セグメントリスト)
"""
print(f"[1/3] 書き起こし開始: {audio_path}")
print(f" モデル: {WHISPER_MODEL_SIZE} / デバイス: {WHISPER_DEVICE}")
model = WhisperModel(
WHISPER_MODEL_SIZE,
device=WHISPER_DEVICE,
compute_type="auto",
)
segments, info = model.transcribe(
audio_path,
language="ja",
beam_size=5,
vad_filter=True, # 無音区間を自動スキップ
vad_parameters=dict(
min_silence_duration_ms=500, # 500ms 以上の無音を区切りとする
),
)
full_text = ""
segment_list = []
for seg in segments:
text = seg.text.strip()
start = seg.start
end = seg.end
timestamp = f"[{int(start // 60):02d}:{int(start % 60):02d}]"
line = f"{timestamp} {text}"
full_text += line + "\n"
segment_list.append({"start": start, "end": end, "text": text})
print(f" {line}") # リアルタイム表示
print(f"[1/3] 書き起こし完了: {len(full_text)} 字")
return full_text, segment_list
def generate_minutes(transcript: str, meeting_date: str) -> str:
"""
Ollama のローカル LLM で議事録を生成する。
"""
print(f"[2/3] 議事録生成開始: モデル={OLLAMA_MODEL}")
prompt = MINUTES_PROMPT.format(
transcript=transcript,
meeting_date=meeting_date,
char_count=len(transcript),
)
response = ollama.chat(
model=OLLAMA_MODEL,
messages=[
{"role": "user", "content": prompt}
],
options={
"num_ctx": 16384, # コンテキスト長(書き起こし 8000 字 + 余裕)
"temperature": 0.1, # 議事録は再現性重視で低温度に設定
},
)
minutes_text = response["message"]["content"]
print("[2/3] 議事録生成完了")
return minutes_text
def save_output(
audio_path: str,
transcript: str,
minutes: str,
segments: list[dict],
meeting_date: str,
) -> Path:
"""
議事録・書き起こし・セグメントデータを保存する。
"""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
base_name = Path(audio_path).stem
date_prefix = datetime.now().strftime("%Y-%m-%d")
output_base = OUTPUT_DIR / f"{date_prefix}_{base_name}"
# 議事録 Markdown
minutes_path = output_base.with_suffix(".md")
minutes_path.write_text(minutes, encoding="utf-8")
# 書き起こし全文(参照用)
transcript_path = output_base.parent / f"{output_base.name}_transcript.txt"
transcript_path.write_text(transcript, encoding="utf-8")
# タイムスタンプ付きセグメント(後処理用)
segments_path = output_base.parent / f"{output_base.name}_segments.json"
segments_path.write_text(
json.dumps(segments, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"[3/3] 保存完了")
print(f" 議事録 : {minutes_path}")
print(f" 書き起こし: {transcript_path}")
return minutes_path
def run_pipeline(audio_path: str, meeting_date: str | None = None) -> Path:
"""
パイプラインのエントリポイント。
"""
if not Path(audio_path).exists():
raise FileNotFoundError(f"音声ファイルが見つかりません: {audio_path}")
if meeting_date is None:
meeting_date = datetime.now().strftime("%Y年%m月%d日")
print("=" * 50)
print("Whisper + Ollama 議事録パイプライン")
print(f"入力: {audio_path}")
print(f"会議日: {meeting_date}")
print("=" * 50)
transcript, segments = transcribe_audio(audio_path)
minutes = generate_minutes(transcript, meeting_date)
output_path = save_output(
audio_path, transcript, minutes, segments, meeting_date
)
print("=" * 50)
print("完了")
print(f"議事録: {output_path}")
print("=" * 50)
return output_path
if __name__ == "__main__":
if len(sys.argv) < 2:
print("使い方: python pipeline.py <音声ファイル> [会議日]")
print("例: python pipeline.py meeting.mp3 '2026年06月26日'")
sys.exit(1)
audio_file = sys.argv[1]
date_arg = sys.argv[2] if len(sys.argv) > 2 else None
run_pipeline(audio_file, date_arg)CLI ラッパー(cli.sh)
毎回 python pipeline.py を打つのは面倒なので、シェルラッパーを作っておきます。
#!/usr/bin/env bash
# 使い方: ./cli.sh <音声ファイル> [会議日]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# 仮想環境があれば有効化
if [ -f ".venv/bin/activate" ]; then
source .venv/bin/activate
fi
# Ollama が起動しているか確認
if ! curl -sf http://localhost:11434/api/tags > /dev/null 2>&1; then
echo "[ERROR] Ollama が起動していません。'ollama serve' を実行してください。"
exit 1
fi
python pipeline.py "$@"chmod +x cli.sh実行例
# 基本的な使い方
./cli.sh recordings/design_review_2026-06-26.mp3
# 会議日を明示する場合
./cli.sh recordings/client_mtg.mp3 "2026年06月26日(木)要件定義MTG"
# 出力例
# output/2026-06-26_client_mtg.md ← 議事録
# output/2026-06-26_client_mtg_transcript.txt ← 書き起こし全文
# output/2026-06-26_client_mtg_segments.json ← タイムスタンプ付きデータ本番運用で踏んだ落とし穴と対策
実際に Amanity の社内会議・クライアントミーティングで数十本の音声を処理して判明した問題点を、再現条件と対策ごとに整理します。
落とし穴 1: 1 時間超の音声で処理時間が爆増する
症状:
60 分の音声を medium モデルで処理すると書き起こしに約 8 分かかります。その後 LLM の議事録生成でさらに 2 分。合計 10 分の待ちが発生しました。
原因:
faster-whisper は音声全体をメモリに展開してから処理します。長い音声ほど VRAM / RAM の消費が増え、スワップが発生して速度が落ちます。
対策: 30 分ごとにチャンク分割してから処理する
import subprocess
from pathlib import Path
def split_audio(audio_path: str, chunk_minutes: int = 30) -> list[str]:
"""ffmpeg で音声を指定分数ごとに分割する"""
output_dir = Path("tmp_chunks")
output_dir.mkdir(exist_ok=True)
base = Path(audio_path).stem
pattern = str(output_dir / f"{base}_%03d.wav")
subprocess.run([
"ffmpeg", "-i", audio_path,
"-f", "segment",
"-segment_time", str(chunk_minutes * 60),
"-ar", "16000", # Whisper は 16kHz を期待する
"-ac", "1", # モノラルに変換(サイズ削減)
"-c:a", "pcm_s16le",
pattern,
"-y", "-loglevel", "error",
], check=True)
chunks = sorted(output_dir.glob(f"{base}_*.wav"))
return [str(c) for c in chunks]分割後に各チャンクを書き起こして結合し、LLM に渡します。処理時間が体感的に 40% 短縮されました。
落とし穴 2: 相槌・断片発話がノイズとして混入する
症状:
実際の会議音声で書き起こしてみると、「大丈夫かちょっと」「普段使ってないですよ PC って」「いけるかな」「これか」といった短い相槌や独り言がすべてセグメントとして出力されます。議事録生成の入力に混じると要約の精度が下がります。
原因:
Whisper は音声に乗っているすべての発話を書き起こします。有意義な発言と相槌を区別する仕組みは標準では持っていません。
対策: vad_filter=True で無音区間をスキップ + セグメント長フィルタを追加
segments, info = model.transcribe(
audio_path,
language="ja",
vad_filter=True, # 無音区間を自動スキップ(有効化推奨)
vad_parameters=dict(
min_silence_duration_ms=500, # 500ms 以上の無音を区切りとする
speech_pad_ms=200,
),
)
# 短すぎるセグメント(相槌・フィラー)を除外
MIN_SEGMENT_CHARS = 8
segments_filtered = [s for s in segments if len(s.text.strip()) >= MIN_SEGMENT_CHARS]vad_filter=True だけでも効果があるが、8 文字未満のセグメントをフィルタリングすると相槌の大半が消えます。有意義な発言は通常それ以上の長さを持つためです。
落とし穴 3(旧2): 固有名詞・英単語混じりで精度が下がる
症状:
会議中に英語の固有名詞(製品名・人名)が出ると Whisper が英語で書き起こしてしまい、前後の日本語と混在します。また専門用語が音のまま誤変換される(「マイグレーション」が「マイナレーション」になった例が実際に発生しました)。
原因:
Whisper のデフォルト設定は language=None(自動検出)。日英混在音声では言語判定がブレやすいです。また初出の専門用語は学習データにない場合、近い音の一般語に引っ張られます。
対策: language="ja" を必ず明示 + initial_prompt で専門用語を事前登録
# 専門用語・固有名詞をプロンプトで事前登録
DOMAIN_TERMS = "Amanity、Claude Code、Whisper、Ollama、LLM、API、SDK、マイグレーション"
segments, info = model.transcribe(
audio_path,
language="ja", # 日本語を明示
initial_prompt=DOMAIN_TERMS, # 固有名詞をヒントとして与える
beam_size=5,
best_of=5,
)initial_prompt に登録した単語は正しいスペルで出力されやすくなります。実際に「Claude Code(クロードコード)」の認識精度が向上しました。
落とし穴 4: Ollama がタイムアウトして議事録生成が失敗する
症状:
書き起こしテキストが 10,000 字を超えると、Ollama の API レスポンスが 120 秒のタイムアウトに引っかかってエラーになります。
原因:
ollama.chat() のデフォルトタイムアウトは 120 秒(ライブラリ依存)。長い入力では LLM の推論に 3〜5 分かかることがあります。
対策: タイムアウトを延長 + 書き起こしテキストを要約前に圧縮する
import httpx
# タイムアウトを 600 秒に延長(ollama SDK のバージョンによって書き方が異なる場合がある)
client = ollama.Client(
host="http://localhost:11434",
timeout=httpx.Timeout(600.0), # 10 分
)
response = client.chat(
model=OLLAMA_MODEL,
messages=[{"role": "user", "content": prompt}],
options={"num_ctx": 16384, "temperature": 0.1},
)注: 上記は
ollamaPython SDK を使う場合の例。SDK のバージョンによってはシグネチャが異なる可能性があります。ollama listで SDK バージョンを確認し、公式ドキュメントも参照してください。短い音声(〜30 分)であれば標準タイムアウト内で完了するため、まずはデフォルト設定で試すことをおすすめします。
加えて、書き起こしテキストが 10,000 字を超える場合は LLM に渡す前に「冒頭部分のみ」または「フィラー語(えー、あのー、そのー)を除去」して圧縮するプリプロセスを挟むと安定します。
import re
def compress_transcript(text: str, max_chars: int = 8000) -> str:
"""フィラー語を除去してテキストを圧縮する"""
# 口語フィラーを除去
fillers = r"(えー+|あのー*|そのー*|まあ+|ちょっと+|なんか+|うーん+)"
text = re.sub(fillers, "", text)
# 重複スペース・改行を整理
text = re.sub(r"[ \t]{2,}", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
# 上限に収める
if len(text) > max_chars:
text = text[:max_chars] + "\n\n(以降省略——書き起こし全文は _transcript.txt を参照)"
return text発展応用——AI エージェントと組み合わせる
Amanity では、この議事録パイプラインを Claude Code の秘書エージェント(secretary) と組み合わせて運用しています。
具体的には、c3 console(ブラウザベースの Claude Code 管理 UI)から音声入力で会議の録音完了を知らせると、秘書エージェントが自動的にパイプラインを呼び出し、生成された議事録を Google Drive の所定フォルダに保存します。
[音声入力] 「さっきの会議の議事録を作って」
↓
[秘書エージェント] pipeline.py を呼び出す
↓
[Whisper + Ollama] 書き起こし → 議事録生成
↓
[秘書エージェント] Google Drive にアップロード + Slack 通知この構成のポイントは「秘書エージェント自体も完全ローカル(Claude Code)で動いている」点です。音声データだけでなく、議事録の送受信ルートも外部クラウドを経由しません。
Claude Code への接続方法や秘書エージェントの構築については、別記事で詳しく紹介する予定です。
まとめ
Whisper + Ollama の完全ローカルパイプラインで実現できることをまとめます。
- セキュリティ: 音声データがマシン外に出ません。機密会議もクラウド送信ゼロ
- コスト: API 費用ゼロ。初期の環境構築コストのみ
- 精度:
mediumモデル +qwen2.5:7bの組み合わせで実用水準(ビジネス会議で十分) - 拡張性: AI エージェントと組み合わせれば、議事録生成〜共有まで完全自動化できる
本番運用で踏んだ落とし穴(処理時間・文字化け・タイムアウト)の対策もすべて記載しました。コードをコピーして pip install faster-whisper ollama を実行すれば、今日から使えるはずです。
クラウド API の利便性は魅力的だが、機密データを扱う会議には「データがどこにも行かない」という保証が何より重要です。ぜひ自社の会議録音フローに組み込んでみてください。
株式会社Amanity
福岡を拠点に、AIを活用したモバイル・Web・LINEアプリの受託開発を行っています。Whisper・Ollama 等のローカル AI 活用や業務自動化についてお気軽にご相談ください。
無料で相談する