API

音声通話APIの録音をS3に保存し解析する実装|Vonage

AI音声認識技術の進化と応用

音声通話APIで録音した音声がどこに何日置かれるかを確認しないまま本番へ出すと、あとから「先月の通話を聞き直したい」に応えられません。Vonage Voice APIの録音はVonage側に30日しか残らないためです。この記事は、recordアクションで録音を取り、recording_urlからダウンロードしてAmazon S3へ退避し、Amazon Transcribeで解析可能な状態にするまでを、公式ドキュメントとAWS Price List APIの実測値で組み立てます。

まとめ:音声通話APIの録音をS3で保管・解析する要点

Vonage Voice APIの録音は完了後30日でVonageが自動削除するため、長期保管が要件なら自前でS3へ退避する実装が必須です。ダッシュボードでS3バケットを登録するだけで録音が届く機能は、Video API(OpenTok)のアーカイブとVonage Business CloudのCompany Call Recordingsにはありますが、CPaaSのVoice APIにはありません。退避処理は、eventUrlへ届くwebhookのrecording_urlを、録音を作成したのと同じapplication keyで署名したJWTを添えて取得し、S3へPUTする流れになります。

保管側は、128KB未満の録音がライフサイクルで遷移しない既定を踏みます。解析側は、splitconversationにしたステレオ録音とAmazon Transcribeのチャンネル識別で話者を分離できますが、日本語は個人情報の自動マスキングの対象外です。

音声通話APIの機能範囲と、録音に保存設計が要る理由

音声通話APIが担う機能の範囲

音声通話API(Programmable Voice API)は、電話の発信・着信・IVRの分岐・録音・音声認識といった電話交換機側の機能を、HTTPリクエストとwebhookで制御できるようにしたものです。Vonage Voice APIでは通話中の動作をNCCO(Nexmo Call Control Object)というJSON配列で指示し、talkで読み上げ、connectで接続、recordで録音、inputで音声認識やDTMF入力を受け取ります。

どの事業者を選ぶかは提供体制と料金体系の話になるため、VonageとTwilio比較|CPaaS選定で見る料金・提供体制・SMS制約で扱っています。ここからは、事業者を決めたあとに必ず設計が必要になる録音の保管と解析に絞ります。

Vonage側に録音が残るのは30日間

公式ドキュメントは録音の保持について「After your recording is complete, it is stored by Vonage for 30 days before being automatically deleted」と明記しています。日本語版も「録音が完了すると、Vonageによって30日間保存された後、自動的に削除されます」と同じ内容です。recording_urlは恒久的なファイル置き場ではなく、31日目以降はダウンロードできなくなる受け渡し用のURLだと考えてください。

コンプライアンス保管や機械学習用データセットとしての再利用を見込むなら、この30日が設計の起点です。録音完了webhookを受けてから30日以内に必ず自前ストレージへ移す仕組みを、リトライと監視込みで用意します。

recordアクションの設計:後段の解析に耐える録音の取り方

主要パラメータと指定できる範囲

NCCOリファレンスに記載されたrecordアクションのパラメータのうち、保存と解析に影響するものを整理します。

パラメータ 既定値 指定できる範囲・注記
format mp3(3チャンネル以上はwav) mp3 / wav / ogg
split 記載なし conversation(ステレオ録音を有効化)
channels 記載なし 最大32。split=conversationが前提
endOnSilence 記載なし 3〜10秒
timeOut 記載なし 3〜7200秒(2時間)
endOnKey 記載なし * / # / 任意の1桁

録音の終了条件はendOnSilenceの無音タイマー、endOnKeyのキー入力、timeOutの最大長のうち先に成立したもので決まります。上限が7200秒である以上、それを超える長時間通話を1ファイルで残す設計は成立しません。eventUrlは値が1つでも配列で渡します。

split=conversationのステレオ録音と話者分離コストの削減

splitconversationを指定すると、送信音声と受信音声が別チャンネルに記録されます。公式ドキュメントは「最初の発信者が話した音声は左チャンネルになり、最初の発信者が聞いた音声は右チャンネルになります」と説明しています。左右がそのまま話者に対応するため、後段で音響的に話者を推定する必要がなくなります。

{
  "action": "record",
  "format": "mp3",
  "split": "conversation",
  "channels": 2,
  "endOnSilence": 5,
  "timeOut": 1800,
  "beepStart": true,
  "eventUrl": ["https://example.com/webhooks/recording"]
}

channelsは最大32まで指定できますが、後述するAmazon Transcribeのチャンネル識別は2チャンネルまでです。1対1の通話を解析へ回すなら2を超えないでください。3チャンネル以上ではformatの既定がwavへ変わり、保存先のキー名や解析ジョブのMediaFormatにも影響します。

録音をS3へ退避する実装

S3への直接連携の所在:Video API・VBCとVoice APIの違い

「VonageとS3の連携」で検索すると、バケットを指定するだけでファイルが届く設定が2つ見つかります。Video API(旧OpenTok)のアーカイブは、ダッシュボードのProject配下でバケットを登録すると「Recorded archives are uploaded to the Amazon S3 bucket you specify」という挙動になります。Vonage Business Cloud(UCaaS)のCompany Call Recordingsは、Recording Storageで「Turn on exporting」を有効にし、制限的なバケットポリシーならarn:aws:iam::679275463210:role/ccra-s3-copy-roles3:PutObjectを許可します。

いずれもCPaaSのVoice APIとは別製品です。Voice APIの録音ガイド・NCCOリファレンス・Webhookリファレンスに、顧客側ストレージへ直接配信する設定項目の記載はありません。用意されているのはeventUrlへのwebhook通知と、そこに含まれるダウンロードURLだけです。

eventUrlに届くwebhookのフィールド

録音が完了すると、VonageはeventUrlへ次のフィールドを含むリクエストを送ります。

フィールド 内容
recording_url ダウンロード先(https://api.nexmo.com/v1/files/{id} 形式)
recording_uuid この録音の一意な識別子
conversation_uuid この会話の一意な識別子
size 録音ファイルのサイズ(バイト)
start_time / end_time 録音の開始・終了時刻(ISO 8601)
timestamp イベントの発生時刻(ISO 8601)

S3のオブジェクトキーにはrecording_uuidを使います。webhookが再送されても同じキーに上書きされるだけなので、重複ファイルが増えません。conversation_uuidをプレフィックスに入れると、CRMの通話履歴から該当ファイルを引けます。実サイズはsizeで分かるため、後述する128KBの閾値に対する分布は運用開始直後に確認できます。

JWTでのダウンロードとS3へのPUT

公式ドキュメントは「You will need to authenticate with a JWT signed by the same application key that created the recording in order to download the recording file」と記載しています。別のアプリケーションの鍵で署名したJWTでは取得できません。curlなら次の形です。

curl $VOICE_RECORDING_URL \
  -H "Authorization: Bearer $JWT" \
  --output recording.mp3

受信側では、webhookが本当にVonageから来たかを先に検証します。署名付きコールバックを有効にするとAuthorizationヘッダーにHS256で署名されたJWTが載り、ダッシュボードのsignature secretで検証できます。省くと、第三者がPOSTを打つだけで任意のURLの中身を自社バケットへ書き込める状態になります。

import os
import tempfile

import boto3
import jwt
from flask import Flask, request
from vonage import Auth, Vonage

app = Flask(__name__)
s3 = boto3.client("s3", region_name="ap-northeast-1")
vonage_client = Vonage(
    Auth(
        application_id=os.environ["VONAGE_APPLICATION_ID"],
        private_key=os.environ["VONAGE_PRIVATE_KEY_PATH"],
    )
)
BUCKET = os.environ["RECORDING_BUCKET"]
SIGNATURE_SECRET = os.environ["VONAGE_SIGNATURE_SECRET"]


@app.route("/webhooks/recording", methods=["POST"])
def on_recording():
    token = request.headers.get("Authorization", "").removeprefix("Bearer ")
    try:
        jwt.decode(token, SIGNATURE_SECRET, algorithms=["HS256"])
    except jwt.PyJWTError:
        return "", 401

    event = request.get_json(force=True)
    ext = os.environ.get("RECORDING_FORMAT", "mp3")
    key = "voice/{}/{}.{}".format(
        event["conversation_uuid"], event["recording_uuid"], ext
    )

    with tempfile.NamedTemporaryFile(suffix="." + ext) as tmp:
        vonage_client.voice.download_recording(event["recording_url"], tmp.name)
        s3.upload_file(
            tmp.name,
            BUCKET,
            key,
            ExtraArgs={
                "Metadata": {
                    "recording-uuid": event["recording_uuid"],
                    "start-time": event["start_time"],
                    "end-time": event["end_time"],
                }
            },
        )
    return "", 204

暗号化の指定は不要です。S3は2023年1月5日以降、新規オブジェクトにSSE-S3(AES-256)を自動適用しており、公式FAQは「You can no longer disable encryption for new object uploads」と明記しています。鍵の管理主体を自社に寄せる要件があるときだけ、ExtraArgsServerSideEncryptionSSEKMSKeyIdを足してSSE-KMSへ切り替えます。

webhookの非同期化とSQSへの分離

上のコードはダウンロードとアップロードをwebhookのリクエスト内で完結させた最小形で、本番構成としては推奨しません。VonageのEvent URLはconnect timeoutが1秒、socket timeoutが10秒で、2xx以外を返すと1回だけリトライされます。録音の取得はVonage側のネットワークとファイルサイズに依存するため、10秒に収まる保証がありません。

本番では、webhookハンドラは署名検証とJSONのSQS投入だけを行って即座に204を返し、ワーカー(LambdaやECSタスク)がキューを読んでダウンロードとPUTを行う形に分けます。オブジェクトキーがrecording_uuidである以上、同じメッセージを複数回処理しても結果は変わらず、キューの再配信をそのまま冪等性の担保に使えます。

S3側の保管設計で踏みやすい2つの罠

128KB未満のオブジェクトが遷移しない既定挙動

録音は日が経つほど参照されなくなるため、ライフサイクルで安価なクラスへ落とす設計が自然に見えます。ここで2024年9月の仕様変更が効きます。公式ドキュメントは「Starting September 2024, the default behavior prevents objects smaller than 128 KB from being transitioned to any storage class」としており、128KB未満は既定ではどのストレージクラスにも遷移しません。遷移リクエスト料金がオブジェクトごとに発生し、小さいオブジェクトでは保管料の削減額を上回るためです。

import boto3

s3 = boto3.client("s3", region_name="ap-northeast-1")
s3.put_bucket_lifecycle_configuration(
    Bucket="my-voice-recordings",
    LifecycleConfiguration={
        "Rules": [
            {
                "ID": "voice-recordings-archive",
                "Status": "Enabled",
                "Filter": {
                    "And": {
                        "Prefix": "voice/",
                        "ObjectSizeGreaterThan": 131072,
                    }
                },
                "Transitions": [
                    {"Days": 30, "StorageClass": "STANDARD_IA"},
                    {"Days": 180, "StorageClass": "GLACIER_IR"},
                ],
                "Expiration": {"Days": 2555},
            }
        ]
    },
)

閾値をこの128KB(131,072バイト)より下げても得はしません。Standard-IA・One Zone-IA・Glacier Instant Retrievalは最小課金オブジェクトサイズが128KBで、公式ドキュメントも「If an object is less than 128 KB, Amazon S3 charges you for 128 KB」と明記しています。短い録音をIAへ落とすと、S3 Standardに置いたままより高くつきます。既定の閾値そのものを変える必要があるなら、put_bucket_lifecycle_configurationTransitionDefaultMinimumObjectSizeで指定します。Expirationの2555日は7年で、通話記録の保管年限を7年と定める社内規程に合わせた例です。

2024年9月より前の既存設定は旧挙動を保持しますが、ルールを作成・編集・削除した時点で新しい既定へ切り替わります。過去に作った設定を触ったあとで小さいファイルが遷移しなくなる形で表面化するため、変更時は遷移件数のメトリクスを確認してください。

Glacier Flexible/Deep Archiveの40KBメタデータ課金

Glacier Flexible RetrievalとDeep Archiveへ遷移させたオブジェクトには、オブジェクト名などのための8KBがS3標準料金で、インデックスと関連メタデータのための32KBが遷移先のGlacier料金で加算されます。合計40KBです。この加算はGlacier Instant Retrievalには適用されません。数十KBの録音を数百万件この2クラスへ送ると、メタデータの課金分が本体の保管料と同じ桁になります。

最低保管日数も効きます。Standard-IAは30日、Glacier Instant RetrievalとFlexible Retrievalは90日、Deep Archiveは180日で、これより早く削除・上書き・他クラスへ遷移させると残期間分が日割りで請求されます。多段の遷移を1本のルールに書くときは、前段の最低保管日数を跨ぐ必要があります。「30日でGlacier Instant Retrieval、60日でDeep Archive」は組めず、後段は前段の遷移日に90日を足した120日目以降にしか置けません。各クラスの取り出し時間と料金の比較はAmazon S3 Glacierとは?3つのアーカイブストレージクラスと取り出し時間・料金・採用判断を実装者目線で解説にまとめています。

保存した録音の解析:日本語で使える機能と使えない機能

Amazon Transcribeの日本語対応マトリクス

S3へ置いた録音をAmazon Transcribeへ渡す前に、ja-JPで何が使えるかを確認します。公式の対応言語表から日本語の行を抜き出すと次のとおりです。

機能 ja-JPの対応
バッチ文字起こし 対応
ストリーミング文字起こし 対応
数値表記の変換 対応
カスタム言語モデル(CLM) 対応
カスタム語彙の頭字語 非対応
個人情報の自動マスキング(Redaction) 非対応
Call Analytics ポストコールのみ

実装への影響が大きいのはRedactionです。使えるのは英語の各方言に加えてfr-FR・fr-CA・it-IT・pt-PT・pt-BR・es-US・es-ESで、ja-JPは対象外です。日本語では文字起こし後に自前のマスキングを挟む前提で設計します。表に話者分離(ShowSpeakerLabels)の行が無いのは、対応言語表に話者分離の列が存在しない、つまり言語別の制限が置かれていないためです。

チャンネル識別による左右チャンネルと話者の対応づけ

Transcribeのチャンネル識別は「If your audio has two channels, you can use channel identification to transcribe the speech from each channel separately」という機能で、出力にchannel_labelsセクションが追加され、各発話がch_0ch_1のラベルとともに返ります。Vonageのsplit: conversationで録った左右チャンネルがそのままこの2つに対応するため、発信者と着信者の発話をラベルで分離できます。

import boto3

transcribe = boto3.client("transcribe", region_name="ap-northeast-1")
transcribe.start_transcription_job(
    TranscriptionJobName="call-20260820-0001",
    LanguageCode="ja-JP",
    MediaFormat="mp3",
    Media={"MediaFileUri": "s3://my-voice-recordings/voice/CON-xxxx/rec-xxxx.mp3"},
    OutputBucketName="my-voice-transcripts",
    OutputKey="ja/",
    Settings={"ChannelIdentification": True},
)

公式ドキュメントは「Amazon Transcribe doesn’t currently support audio with more than two channels」とも書いています。channelsを3以上にした録音をこのジョブへ渡す構成は成立しないため、多者通話ならSettingsShowSpeakerLabelsMaxSpeakerLabels(2〜30)に切り替えて音響的な話者分離へ倒します。分離精度の比較は、gpt-4o-transcribe-diarizeとは?OpenAIの話者分離対応音声認識モデルを解説が参考になります。

Amazon Transcribeの東京リージョン単価と月額試算

AWS Price List APIのap-northeast-1向け価格ファイル(publicationDate 2026年6月17日)から、Amazon Transcribeの秒単価を抜き出します。

種別 秒単価(USD) 分換算(USD)
標準バッチ 0.0001 0.006
ストリーミング 0.0001667 0.010
Call Analytics(〜25万分) 0.0005 0.030
Call Analytics(25万〜100万分) 0.00031 0.0186
カスタム言語モデル(〜25万分) 0.0001 0.006

1日1,000通話、平均5分の窓口なら月150,000分です。標準バッチは月900ドル、Call Analyticsのポストコール分析まで通すと月4,500ドルで、5倍の開きになります。日本語ではCall Analyticsのリアルタイム分析が使えず、通話要約は英語の全方言、issue detectionはen-AU・en-GB・en-USに限定されます。要約や応対品質のスコアリングが目的なら、標準バッチで文字起こしを作ってLLMへ渡す構成のほうが費用対効果を出しやすくなります。Transcribe以外の選択肢と日本語精度の比較は文字起こしAPIの比較と選び方|主要7サービスの料金・日本語対応・使い方【2026年最新】、Google側の料金体系はGoogle Cloud Speech-to-Textとは?料金・無料枠・日本語対応・APIの使い方を解説で確認できます。

この構成を採るべきでない場面

録音してS3へ置き、あとからバッチで解析する構成は、次の3つの要件には向きません。要件定義の段階でどれかに当てはまるなら、別の構成を先に検討してください。

  • 通話中に判断したい場合。recordのwebhookは録音終了後にしか飛ばないため、オペレーター支援やリアルタイムのエスカレーション判定には間に合わない。WebSocketで音声ストリームを受ける構成を最初から選ぶ。
  • 3者以上の通話を話者ごとに正確に分けたい場合。チャンネル分離の恩恵は1対1の通話に限られるため、音響的な話者分離の精度が要件を満たすかを先に検証する。
  • 個人情報の自動マスキングを前提にする場合。ja-JPはRedactionの対象外なので、決済情報を口頭で扱う業務では該当区間の録音を止め、番号入力をDTMFへ寄せる設計のほうが確実。

よくある質問

音声通話APIでは何ができますか?

発信・着信・IVRの分岐・録音・音声認識・通話の転送を、HTTPリクエストとwebhookから制御できます。Vonage Voice APIでは通話中の動作をNCCOというJSON配列で記述し、配列は上から順に評価されます。connectrecordのように接続と録音を同じNCCOへ並べることもできます。

Vonage Voice APIの録音はいつまで残りますか?

完了後30日間Vonageに保存され、そのあと自動的に削除されます。31日目以降はrecording_urlからダウンロードできなくなるため、長期保管が要件なら30日以内にS3などへ退避する処理が必須です。この30日はVonage側の保持期間で、S3へ移したあとの保持年限は自分のライフサイクル設定のExpirationで決まります。

録音ファイルはどうやってダウンロードしますか?

recording_urlhttps://api.nexmo.com/v1/files/{id} 形式)へ、JWTをBearerトークンとして付けてGETします。JWTは、その録音を作成したのと同じアプリケーションの秘密鍵で署名したものである必要があります。Python SDKではclient.voice.download_recording(url, path)が同じ処理を行います。

Vonageの通話録音をS3へ直接保存する設定はありますか?

Vonage Business CloudのCompany Call Recordingsにはバケット名を指定した自動エクスポートがあり、Video API(OpenTok)のアーカイブにも同様の設定があります。CPaaSのVoice APIにはこの設定がなく、webhookを受けて自分でダウンロードとPUTを行う実装になります。

日本語の通話録音で個人情報の自動マスキングは使えますか?

Amazon Transcribeの対応言語表では、ja-JPはRedactionが非対応です。対応するのは英語の各方言とfr-FR・fr-CA・it-IT・pt-PT・pt-BR・es-US・es-ESで、日本語ではマスキングを文字起こし結果への後処理として自前で実装します。マスキングを指定するContentRedactionSettingsの中ではなくstart_transcription_jobのトップレベル引数で、RedactionTypeRedactionOutputが必須です。

関連記事

資料請求

RELATED POSTS 関連記事