TL;DR
- 月額固定費 約2ドル(Secrets Manager + ECR + ログ)で話者分離+文字起こしパイプラインを運用
- AWS Step Functions + Lambda による完全サーバーレスアーキテクチャ
- pyannote.audio 3.1 で話者分離、faster-whisper で文字起こし、gpt-4o-mini でLLM分析
- 8時間動画の処理費用 約2.3ドル(x86、Free Tier なし)— AWS Transcribeの約5倍コスト効率
- States.DataLimitExceeded などのハマりどころと解決策を深掘り
リポジトリ: github.com/ekusiadadus/ek-transcript
はじめに
ユーザーインタビュー動画の分析が増えてきました。既存ソリューションを評価したところ:
- AWS Transcribe: 8時間で約11.52ドル($0.024/分)、話者分離の精度もいまひとつ
- 商用SaaS: 月額50〜200ドルの固定費、使わない月も課金
- 常時起動GPUサーバー: EC2 g4dn.xlarge で月額380ドル以上 — 個人利用には高すぎる
最大の問題は月額固定費でした。 月に数回しか使わないのに毎月課金される。月額固定費ほぼゼロの従量課金が最優先でした。
そこで、AWSサーバーレスサービスで自前パイプラインを構築しました。
要件
- 月額固定費ゼロ(使った分だけ課金)
- 最大8時間の長時間動画に対応
- 話者分離(誰が何を話したか)
- 高精度な日本語文字起こし
- LLMによる要約・分析
- 低コスト(動画1本あたり約1ドル)
- 完全サーバーレス
システムアーキテクチャ
┌─────────────────────────────────────────────────────────────────┐
│ AWS Cloud │
├─────────────────────────────────────────────────────────────────┤
│ │
│ S3 (Input) ──▶ EventBridge ──▶ Lambda (StartPipeline) │
│ uploads/ (Object Created) │ │
│ ▼ │
│ DynamoDB (InterviewsTable) │
│ │ │
│ ▼ │
│ ┌────────────────── Step Functions ─────────────────────────┐ │
│ │ │ │
│ │ ExtractAudio ──▶ ChunkAudio ──▶ DiarizeChunks (Map x5) │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ S3 (Output) MergeSpeakers ◀────────────┘ │ │
│ │ ▲ │ │ │
│ │ │ ▼ │ │
│ │ │ SplitBySpeaker ──▶ Transcribe (Map x10) │ │
│ │ │ │ │ │
│ │ │ AggregateResults ◀────┘ │ │
│ │ │ │ │ │
│ │ └────────── LLMAnalysis ◀────┘ │ │
│ │ (gpt-4o-mini) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘データフロー詳細
- ExtractAudio: video.mp4 → audio.wav(16kHz モノラル)
- ChunkAudio: audio.wav → chunk_0.wav, chunk_1.wav, ...(8分+30秒オーバーラップ)
- DiarizeChunks (Map x5): pyannote で各チャンクの話者分離
- MergeSpeakers: 埋め込みベクトルのコサイン類似度でグローバル話者統合
- SplitBySpeaker: 話者セグメントごとに音声分割
- TranscribeSegments (Map x10): faster-whisper で文字起こし
- AggregateResults: 結果統合 → transcript.json
- LLMAnalysis: gpt-4o-mini で構造化分析 → analysis.json
サーバーレスの理由:月額固定費の最小化
| コンポーネント | 課金モデル | 月額固定費 |
|---|---|---|
| Lambda | 実行ごと | $0 |
| Step Functions | 遷移ごと | $0 |
| S3 | ストレージ+リクエスト | $0〜 |
| DynamoDB | オンデマンド | $0 |
| EventBridge | イベントごと | $0 |
| Cognito | 50K MAU まで無料 | $0 |
| AppSync | リクエストごと | $0〜 |
| Secrets Manager | シークレットごと | $0.80/月(2シークレット) |
| ECR | イメージストレージ | 1.00/月 |
| CloudWatch Logs | ログストレージ | 〜$0.05/月 |
実際の月額固定費
Secrets Manager: $0.80/月 (OpenAI + HuggingFace, 2シークレット)
ECR: $0.50〜$1.00/月 (MLモデル入りDockerイメージ, 8イメージ)
CloudWatch Logs: 〜$0.05/月 (Step Functions + Lambdaログ)
───────────────────────────────────────────
合計: 〜$1.50〜$2.00/月使わない月でも約2ドルだけ。 商用SaaSの月額50〜200ドルと比べて、年間576〜2,376ドルの節約。
設計の変遷
初期設計:単純な逐次処理
[Video] → ExtractAudio → Diarize → SplitBySpeaker → Transcribe → LLMAnalysis
│
(単一Lambdaで全音声を処理)問題点:
- Lambdaの15分タイムアウトでは8時間音声の話者分離が完了しない
- pyannote.audioのメモリ使用量が膨大(10GB以上)
- 逐次処理では総処理時間が長すぎる
代替案:ECS Fargate処理
評価:
- GPUインスタンス(g4dn.xlarge)のコストが高い($0.526/時間)
- 8時間動画で4ドル以上
- スポットインスタンスは信頼性に懸念
現在の設計:並列チャンク処理
┌─ DiarizeChunk_0 ─┐
[Video] → Chunk → ├─ DiarizeChunk_1 ─┤ → Merge → Split → Transcribe(並列) → LLM
├─ DiarizeChunk_2 ─┤
└─ ... ─┘設計ポイント:
- 8分チャンク + 30秒オーバーラップ: Lambdaの15分制限内に収まり、境界の話者変化もキャプチャ
- 埋め込みベクトルによる話者統合: チャンク間でSPEAKER_00が別人でも、コサイン類似度クラスタリングで統合
- Map Stateの並列実行: 話者分離x5、文字起こしx10の並列処理で高速化
技術選定と理由
| 技術 | 理由 | vs. 代替案 |
|---|---|---|
| pyannote.audio 3.1 | 最新の話者分離精度、HuggingFace連携 | AWS Transcribeの話者分離より高精度 |
| faster-whisper | Whisperの4〜8倍高速、int8量子化対応 | OpenAI Whisper APIはより高コスト |
| gpt-4o-mini | Structured Outputs対応、低コスト | Claude は当時Structured Outputs未対応 |
| Lambda + Container | 最大10GBイメージ、コールドスタート許容可 | ECS Fargateは常時起動コストが懸念 |
| Step Functions | 複雑なワークフロー管理、エラーハンドリング | SQS + Lambdaは状態管理が複雑 |
コンポーネント実装詳細
1. ExtractAudio Lambda
動画から16kHzモノラルWAVを抽出 — Whisper推奨のサンプルレート。
def extract_audio(input_path: str, output_path: str) -> None:
"""Extract 16kHz mono WAV from video"""
cmd = [
"ffmpeg", "-i", input_path,
"-vn", # No video
"-acodec", "pcm_s16le", # 16-bit PCM
"-ar", "16000", # 16kHz
"-ac", "1", # Mono
"-y", output_path,
]
subprocess.run(cmd, check=True)2. ChunkAudio Lambda
8分チャンク + 30秒オーバーラップに分割。オーバーラップにより境界の話者変化を正確にキャプチャ。
CHUNK_DURATION = 480 # 8 minutes
OVERLAP_DURATION = 30 # 30-second overlap
# chunk_0: 0–510s (effective: 0–480)
# chunk_1: 450–960s (effective: 480–960)
# chunk_2: 900–1410s (effective: 960–1440)3. DiarizeChunk Lambda(並列実行)
pyannote.audio 3.1で話者分離。各話者の埋め込みベクトルを抽出しS3に保存。
pyannote.audio ライセンス注意: pyannote/speaker-diarization-3.1はHugging Faceでのライセンス同意が必要です。モデルページでライセンス条項に同意し、HF_TOKENを取得してください。商用利用のライセンス要件を確認してください。
from pyannote.audio import Pipeline
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
token=hf_token,
)
if torch.cuda.is_available():
pipeline.to(torch.device("cuda"))
diarization = pipeline({"waveform": audio_tensor, "sample_rate": sample_rate})
speaker_embeddings = extract_speaker_embeddings(audio_path, segments)4. MergeSpeakers Lambda
埋め込みベクトルのコサイン類似度でチャンク間の話者をクラスタリング。
from sklearn.cluster import AgglomerativeClustering
from sklearn.metrics.pairwise import cosine_similarity
similarity_matrix = cosine_similarity(all_embeddings)
distance_matrix = 1 - similarity_matrix
clustering = AgglomerativeClustering(
n_clusters=None,
distance_threshold=1 - 0.75, # 75%以上の類似度 = 同一話者
metric="precomputed",
linkage="average",
)
labels = clustering.fit_predict(distance_matrix)5. Transcribe Lambda(並列実行)
faster-whisper(mediumモデル)で高速文字起こし。
from faster_whisper import WhisperModel
model = WhisperModel("medium", device="cpu", compute_type="int8")
segments, info = model.transcribe(audio_path, language="ja", beam_size=5)
text = "".join([seg.text for seg in segments])6. LLMAnalysis Lambda
gpt-4o-miniのStructured Outputsで構造化分析。
from openai import OpenAI
completion = client.beta.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": ANALYSIS_PROMPT},
{"role": "user", "content": f"Analyze this:\n{transcript}"},
],
response_format=AnalysisResult,
)コスト内訳(8時間動画の例、2025年12月時点)
前提:
- リージョン: us-east-1
- Lambda: x86_64(arm64は約20%安い)
- Free Tierなし、リトライなし
- Map並列度: 話者分離x5、文字起こしx10
| サービス | 計算 | コスト |
|---|---|---|
| Lambda (Diarize) | 10GB x 600s x 6チャンク = 36,000 GB-s | $0.60 |
| Lambda (Transcribe) | 2.94GB x 30s x 900コール = 79,380 GB-s | $1.32 |
| Lambda (Other) | ExtractAudio, Chunk, Merge, Split, Aggregate, LLM | $0.10 |
| Step Functions | 約6,000遷移 x $0.025/1K | $0.15 |
| S3 | 読み書き + 一時ストレージ | $0.02 |
| OpenAI API | gpt-4o-mini (300K入力 + 8K出力トークン) | $0.10 |
| 合計 | 約$2.3 |
pyannote.audioとfaster-whisperを活用することで、AWS Transcribe(11.52)の約5倍のコスト効率を実現。arm64なら約$1.9に下がり、約6倍効率的。
実装のハマりどころと解決策
1. States.DataLimitExceeded(256KB制限)
症状: 900以上のセグメント処理時、Step FunctionsのMap Stateでエラー発生。
States.DataLimitExceeded - The state/task returned a result with a size
exceeding the maximum number of bytes service limit.原因: Step Functionsには256KBのペイロード制限があり、Map Stateの結果をすべて集約すると超過。
解決策:
// CDK: Map Stateの結果を破棄
const transcribeSegments = new sfn.Map(this, "TranscribeSegments", {
itemsPath: "$.segment_files",
maxConcurrency: 10,
resultPath: sfn.JsonPath.DISCARD, // ← これがポイント
});# Lambda側: 結果をS3に保存
s3.put_object(
Bucket=bucket,
Key=f"transcribe_results/{segment_name}.json",
Body=json.dumps(result_data, ensure_ascii=False),
)
# Step Functionsにはメタデータだけ返す
return {"bucket": bucket, "result_key": result_key}2. PyTorch 2.6+ の torch.load 問題
症状: pyannote.audioのモデル読み込みでエラー。
FutureWarning: You are using `torch.load` with `weights_only=False`解決策: torch.load のモンキーパッチ
import torch
_orig_torch_load = torch.load
def _torch_load_legacy(*args, **kwargs):
"""Always call torch.load with weights_only=False"""
kwargs["weights_only"] = False
return _orig_torch_load(*args, **kwargs)
torch.load = _torch_load_legacy # pyannote import の前に適用モンキーパッチのリスク: PyTorch内部を変更するため将来のバージョンで壊れる可能性があります。可能であれば、pyannote.audioのsafetensors対応や公式ワークアラウンドを待ってください。
3. Lambda コンテナのモデルダウンロード戦略
問題: HuggingFaceモデル(pyannote、whisper)は数GBあり、コールドスタート時のダウンロードではLambdaタイムアウト。
解決策: ビルド時にモデルを含める
FROM public.ecr.aws/lambda/python:3.11
ENV HF_HOME=/var/task/models
RUN pip install huggingface_hub
RUN python -c "from huggingface_hub import snapshot_download; \
snapshot_download('pyannote/speaker-diarization-3.1', token='${HF_TOKEN}')"HF_TOKENはビルド引数として渡し、最終イメージに含めない:
ARG HF_TOKEN
RUN --mount=type=secret,id=hf_token \
HF_TOKEN=$(cat /run/secrets/hf_token) python download_models.py4. 8分チャンク長の選定
| チャンク長 | 結果 |
|---|---|
| 5分 | 話者分離精度が低下(コンテキスト不足) |
| 10分 | Lambdaメモリ不足(10GBにギリギリ) |
| 15分 | Lambdaの15分タイムアウト超過 |
| 8分 | 精度・メモリ・時間の最適バランス |
30秒オーバーラップの理由:
- 話者の切り替えは通常2〜3秒のギャップがある
- 30秒あれば境界の話者変化を確実にキャプチャ
- これ以上長くすると冗長処理が増えコスト増
今後の計画:Google Meet自動連携
Google Meet REST APIの自動録画機能(2025年4月追加)を活用した自動録画・分析を計画中。
Google Calendar (会議スケジュール)
│
▼ Cloud Functions (Calendar Webhook)
Google Meet Space (自動録画有効)
│
▼ 録画完了
Google Drive (録画保存)
│
▼ Workspace Events API + Pub/Sub
EventBridge (Cross-Cloud)
│
▼
Lambda (DownloadRecording)
│
▼
S3 → Step Functions (既存パイプライン)
│
▼
DynamoDB + AppSync → ダッシュボードまとめ
- 月額固定費 約2ドルで話者分離+文字起こしパイプラインを運用
- AWS Step Functions + Lambda で完全サーバーレス — 使った分だけ課金
- pyannote.audio + faster-whisper + gpt-4o-mini で高品質・低コスト
- 8時間動画で約2.3ドル(AWS Transcribeの約5倍コスト効率)
- 並列チャンク処理 + 埋め込みクラスタリングで長時間音声に対応
- 256KB制限は
resultPath: DISCARD+ S3パススルーで解決
全コードはGitHubで公開しています。