13分

AWSサーバーレスで話者分離+文字起こしパイプラインを約1ドルで構築する

awsserverlessmachine-learningpython

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サーバーレスサービスで自前パイプラインを構築しました。

要件

  1. 月額固定費ゼロ(使った分だけ課金)
  2. 最大8時間の長時間動画に対応
  3. 話者分離(誰が何を話したか)
  4. 高精度な日本語文字起こし
  5. LLMによる要約・分析
  6. 低コスト(動画1本あたり約1ドル)
  7. 完全サーバーレス

システムアーキテクチャ

┌─────────────────────────────────────────────────────────────────┐
│                          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)                           │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

データフロー詳細

  1. ExtractAudio: video.mp4 → audio.wav(16kHz モノラル)
  2. ChunkAudio: audio.wav → chunk_0.wav, chunk_1.wav, ...(8分+30秒オーバーラップ)
  3. DiarizeChunks (Map x5): pyannote で各チャンクの話者分離
  4. MergeSpeakers: 埋め込みベクトルのコサイン類似度でグローバル話者統合
  5. SplitBySpeaker: 話者セグメントごとに音声分割
  6. TranscribeSegments (Map x10): faster-whisper で文字起こし
  7. AggregateResults: 結果統合 → transcript.json
  8. LLMAnalysis: gpt-4o-mini で構造化分析 → analysis.json

サーバーレスの理由:月額固定費の最小化

コンポーネント課金モデル月額固定費
Lambda実行ごと$0
Step Functions遷移ごと$0
S3ストレージ+リクエスト$0〜
DynamoDBオンデマンド$0
EventBridgeイベントごと$0
Cognito50K MAU まで無料$0
AppSyncリクエストごと$0〜
Secrets Managerシークレットごと$0.80/月(2シークレット)
ECRイメージストレージ0.500.50〜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 ─┤
                    └─      ...       ─┘

設計ポイント:

  1. 8分チャンク + 30秒オーバーラップ: Lambdaの15分制限内に収まり、境界の話者変化もキャプチャ
  2. 埋め込みベクトルによる話者統合: チャンク間でSPEAKER_00が別人でも、コサイン類似度クラスタリングで統合
  3. Map Stateの並列実行: 話者分離x5、文字起こしx10の並列処理で高速化

技術選定と理由

技術理由vs. 代替案
pyannote.audio 3.1最新の話者分離精度、HuggingFace連携AWS Transcribeの話者分離より高精度
faster-whisperWhisperの4〜8倍高速、int8量子化対応OpenAI Whisper APIはより高コスト
gpt-4o-miniStructured Outputs対応、低コストClaude は当時Structured Outputs未対応
Lambda + Container最大10GBイメージ、コールドスタート許容可ECS Fargateは常時起動コストが懸念
Step Functions複雑なワークフロー管理、エラーハンドリングSQS + Lambdaは状態管理が複雑

コンポーネント実装詳細

1. ExtractAudio Lambda

動画から16kHzモノラルWAVを抽出 — Whisper推奨のサンプルレート。

extract_audio.py
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_audio.py
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を取得してください。商用利用のライセンス要件を確認してください。

diarize_chunk.py
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

埋め込みベクトルのコサイン類似度でチャンク間の話者をクラスタリング。

merge_speakers.py
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モデル)で高速文字起こし。

transcribe.py
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で構造化分析。

llm_analysis.py
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 APIgpt-4o-mini (300K入力 + 8K出力トークン)$0.10
合計約$2.3

pyannote.audioとfaster-whisperを活用することで、AWS Transcribe(0.024/=8時間で0.024/分 = 8時間で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-stack.ts
// CDK: Map Stateの結果を破棄
const transcribeSegments = new sfn.Map(this, "TranscribeSegments", {
  itemsPath: "$.segment_files",
  maxConcurrency: 10,
  resultPath: sfn.JsonPath.DISCARD,  // ← これがポイント
});
transcribe_lambda.py
# 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 のモンキーパッチ

patch_torch.py
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タイムアウト。

解決策: ビルド時にモデルを含める

Dockerfile
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.py

4. 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で公開しています。

参考資料