はじめに
2026年7月のアップデートで、Amazon Connect Customerの音声プロバイダーにAgentic Voice(Amazonエージェントボイス)が追加されました。アップデートの概要は以下記事にまとめたので、そちらをご覧ください。
実際に案内文の読み上げを詰める段階に入って、最初につまずいたのが試聴の方法でした。
従来のKazuhaやTomokoなどのPolly製の音声モデルなら、Amazon Pollyのコンソールにテキストを貼るだけで再生を試せます。Agentic Voiceの音声モデルでも同じように任意の文章で再生を試せる方法を探したのですが、執筆時点では公式ドキュメントに見当たりませんでした。
そのため試聴するには、コンタクトフロー内のプロンプト再生文を書き換えて、フローを公開し、実際に架電するしかありません。句読点を1つ動かしたいだけでも、この3ステップが毎回必要となります。
「Agentic VoiceでもPollyのように簡単に試聴したい!」と思い、試聴専用のWebアプリを作りました。ブラウザに文章を貼って音声を選ぶと、架電もフローの更新もなしにその場で読み上げてくれます。
本記事では、システム概要・アーキテクチャ・実装のポイントを紹介します。
本記事の情報は2026年8月時点のものです。
最新情報については公式ドキュメントをご確認ください。
先に結論
試聴専用のWebアプリを作った結果、Agentic Voiceの聞き比べはブラウザで完結するようになりました。
| Before | After | |
|---|---|---|
| 文言の変更 | プロンプト再生文を書き換えてフローを公開 | ブラウザに文章を貼るだけ |
| 試聴 | 電話番号へ架電して聞く | ブラウザでその場で再生 |
| 音声の切り替え | フローの音声設定を変えて再公開 | 画面で音声を選ぶだけ |
動作の様子はこちらです。
1. 従来の試聴方法と課題
Agentic Voiceの音声モデルを利用した文言の試聴は以下の手順で行われます。
- コンタクトフローの「プロンプトの再生」ブロックの文言を書き換える
- フローを保存して公開する
- 割り当てた電話番号に自分の携帯から架電する
- 読み上げを聞く。気になったら1に戻る
文言の長さにもよりますが、句読点の有無による言い回しの変化、数字・アルファベット・漢字の読み方といった細かい点を調整するとなるとこの1周に数分かかることも珍しくありません。
そこで、Pollyのコンソールで確認できるような「文章を入力したら読み上げが返ってくる」仕組みを、Agentic Voice向けに用意することにしました。
2. 使用する主な技術・サービス
まず、このWebアプリを構成する技術とサービスを整理します。
| 技術・サービス | 役割 |
|---|---|
| Amazon Connect Customer(Agentic Voiceを使用) | 音声の合成。プレビュー用フローが文章を読み上げる |
| StartWebRTCContact API | 再生1回ごとに新規コンタクト(WebRTC)を作成 |
| Amazon Chime SDK JS | ブラウザでWebRTC音声を受信・再生 |
| Lambda + API Gateway(HTTP API) | Connect APIを呼び出すバックエンド |
| S3 + CloudFront | フロントエンド(Vite + TypeScript)の配信 |
| AWS CDK(TypeScript) | コンタクトフローを含む全リソースのIaC |
利用の前提条件は公式の設定ガイドに書いてあります。
To use Amazon Connect agentic voice features, make sure Amazon Connect Customer is enabled for your instance. Amazon Connect agentic voice is the default voice provider for Amazon Connect Customer.
音声プロバイダーの選択でAgentic Voice(Amazonエージェントボイス)を選択するには
インスタンス側でAmazon Connect Customerを有効にしている必要があります。
また、リージョンの制約もあります。
2026年8月時点でAgentic Voiceが提供されているのは、バージニア北部、オレゴン、フランクフルト、ロンドン、シドニー、ソウル、東京、シンガポール、カナダ(中部)の9リージョンです。本記事の検証は、2026年8月2日に東京リージョン(ap-northeast-1)で行いました。
3. システム構成図
以下本Webアプリのシステム構成図です。
ブラウザに渡すのはWebRTC参加用の一時情報だけにしています。
AWSの認証情報もContactFlowIdも、フロント側には出しません。
フロントが扱うのはja-jp-aikoのようなアプリ内の論理IDだけで、その論理IDからフローへの対応付けはLambda側の許可リストに閉じています。
4. 構築方法
前提条件
- Agentic Voiceが利用できるAmazon Connect Customerインスタンス(対応リージョンは2章の注意を参照)
- Node.js 20以上
- CDK bootstrap済みのAWSアカウント
手順
リポジトリをクローンして、ConnectのインスタンスIDを渡してデプロイするだけです。
git clone https://github.com/4suke-sun/ACC_AgentVoicePreview.git
cd ACC_AgentVoicePreview
npm ci
# フロントエンドのビルド(デプロイの前提)
npm run build -w @avp/frontend
# デプロイ(コンタクトフロー・Lambda・API・配信一式が作成される)
cd packages/infra
npx cdk deploy -c connectInstanceId=<ConnectインスタンスID>
出力されるWebUrlがプレビュー画面のURLです。ローカル開発やCORS設定などの細かいオプションは、リポジトリのREADMEにまとめています。
5. 動作確認
デプロイで出力されたWebUrlをブラウザで開いて確認しました。文章を貼り、音声を選び、再生ボタンを押すだけです。
動作確認結果
| 検証項目 | 期待する挙動 | 結果 |
|---|---|---|
| AIKO / REN / YUMIKOの再生 | 選んだ音声でブラウザから読み上げが聞こえる | ⭕ |
| 音声の設定の解決 | フローログのGlobalVoiceが渡した音声名と一致する |
⭕ |
| エンジン値の適用 | フローログのGlobalEngineがconnect:agenticになる |
⭕ |
| 再生後の自動切断 | 読み上げ終了後にコンタクトが自動終了する | ⭕ |
| 連打時の冪等性 | 同じpreviewIdで重複コンタクトが作られない |
⭕ |
音声を切り替えて同じ文章を続けて聞けるので、聞き比べは目的どおり速くなりました。読み上げ開始までのわずかな待ち時間だけは残りますが、架電に比べれば気になりません。
6. 実装のポイント
ここからは中身の話です。設計で効いた点と、一番ハマった点を紹介します。
再生1回 = WebRTCコンタクト1件
再生ボタンを1回押すたびに、StartWebRTCContactで新しいWebRTCコンタクトを1件作ります。読み上げたい文章はpreviewText、使いたい音声名はpreviewVoiceとしてコンタクト属性で渡します。フロー側はそれを読んで喋り、読み終わったら切断ブロックでコンタクトが終わります。
このStartWebRTCContactは、もともとアプリやWebサイトから通話をさせるためのAPIです。Chime SDK JSに繋いでブラウザで音を鳴らす手順も公式ドキュメントに載っています。今回はそれを試聴専用に転用しただけ、という位置づけです。
Lambda側の呼び出しはこうなりました。
/**
* 試聴開始: StartWebRTCContact で新規コンタクトを1件作る
* - ClientToken に previewId(UUID)を渡して連打時の重複コンタクトを防ぐ
* - 読み上げ文章と音声名はコンタクト属性でフローへ渡す
*/
const previewId = randomUUID();
const output = await getConnectClient().send(
new StartWebRTCContactCommand({
InstanceId: config.instanceId,
ContactFlowId: contactFlowId,
// 同一再生操作のリトライで重複Contactを作らないための冪等性キー
ClientToken: previewId,
ParticipantDetails: { DisplayName: 'Agentic Voice Preview' },
Attributes: {
previewText: parsed.text,
previewId,
// 動的フローのSet Voiceが参照する音声名
previewVoice: voice.connectVoiceId,
},
}),
);
// ConnectionData(Meeting / Attendee)をそのままフロントへ返す
const body: StartPreviewResponse = {
previewId,
contactId: output.ContactId,
connectionData: {
meeting: output.ConnectionData.Meeting,
attendee: output.ConnectionData.Attendee,
},
};
ClientTokenは地味ですが効きます。公式リファレンスには「作成から7日間有効」「すでに開始済みのコンタクトがある場合はそのコンタクトIDを返す」と書かれているので、通信が不安定でリトライがかかっても、同じpreviewIdなら二重に喋り出しません。
コンタクト属性は全キーバリューの合計で32,768 UTF-8バイトという上限があるため、入力文章側で先に絞っています。
/** Play prompt の制限を考慮した最大文字数(コードポイント数) */
export const MAX_TEXT_LENGTH = 3000;
/** コンタクト属性(合計32KB)を考慮した previewText の最大UTF-8バイト数 */
export const MAX_TEXT_BYTES = 12000;
ブラウザ側は、返ってきたMeetingとAttendeeをChime SDK JSのMeetingSessionConfigurationにそのまま食わせるだけです。試聴専用なのでマイクは要求しません。
/**
* StartWebRTCContact の ConnectionData から Chime SDK セッションを生成する
*/
const logger = new ConsoleLogger('AgenticVoicePreview', LogLevel.WARN);
const configuration = new MeetingSessionConfiguration(data.meeting, data.attendee);
const deviceController = new DefaultDeviceController(logger);
return new DefaultMeetingSession(configuration, logger, deviceController);
停止ボタンからはStopContactを呼びます。すでに終了しているコンタクトにはContactNotFoundExceptionが返るので、これは成功扱いにしました。読み上げが終わってフローが切断した直後に停止を押される、という順序が普通に起きるためです。
コンタクトフローはロケールごとに1本の動的フロー
最初は音声ごとにフローを1本ずつ作りました。AIKO用、REN用、YUMIKO用。3本ならまだ許せますが、他言語も試したくなった時点で破綻が見えます。音声を1つ増やすたびにフローが1本増えるのに、中身は音声名以外ほぼ同じです。
なので、フローはロケールごとに1本にして、音声名はコンタクト属性から動的に解決させました。フローの中身はこれだけです。
[開始] → [ログ設定] → [音声を設定(動的)] → [言語を設定] → [プロンプトの再生] → [切断]
肝は音声設定ブロックで、TextToSpeechVoiceに$.Attributes.previewVoiceを渡しています。CDKが吐くフローJSONの該当箇所を抜粋します。
/** Agentic voice を示す TextToSpeechEngine の値(公式リファレンス未記載) */
export const AGENTIC_VOICE_ENGINE = 'connect:agentic';
// 音声を設定(動的): 音声名はコンタクト属性から解決する
{
Identifier: 'set-voice',
Type: 'UpdateContactTextToSpeechVoice',
Parameters: {
TextToSpeechVoice: '$.Attributes.previewVoice',
TextToSpeechEngine: AGENTIC_VOICE_ENGINE,
},
Transitions: {
NextAction: 'set-language',
// 失敗しても既定音声で喋らせない。必ず切断へ倒す
Errors: [{ NextAction: 'disconnect', ErrorType: 'NoMatchingError' }],
},
},
Set Voiceブロックを動的に設定すること自体は公式にサポートされた機能で、守るべき組み合わせのルールも書かれています。
You can also set language, voice, engine and style dynamically. There are a few configurations that must be followed when modifying the block: If the language is selected dynamically, the voice must also be selected dynamically.
言語を動的にするなら音声も動的にしろ、という縛りです。今回はロケールごとにフローを分けているので、言語コードは固定値で音声だけを動的にしています。
もう1つ意識したのは、どのブロックが失敗しても切断へ倒すことです。音声設定が失敗したまま先へ進むと、既定音声で読み上げてしまいます。試聴ツールとしては、これが一番たちの悪い失敗です。何も聞こえない方が、間違った声を聞かされるよりましだと考えて、全ブロックのエラー遷移を切断に向けました。
ハマりポイント: 音声名とエンジン値が公式に載っていない
ここが一番時間を使ったところです。
Agentic Voiceの音声名(AIKO / REN / YUMIKO)と、エンジン値のconnect:agentic。この2つが**公式ドキュメントのどこにも書かれていません。**2026年8月時点で、以下を当たって見つかりませんでした。
| 探した場所 | 結果 |
|---|---|
| Agentic voice configuration guide | コンソールUIの操作手順のみ。音声名の一覧や表はない |
| Set voiceブロックの管理者ガイド | standard / neural / generativeの3エンジンの説明のみ。Agentic Voiceへの言及自体がない |
| UpdateContactTextToSpeechVoice(フロー言語リファレンス) |
TextToSpeechEngine は「Pollyの音声に紐づくエンジン」という説明だけ |
つまり、コード側に書く具体値の出どころがありません。仕方がないので、実機から逆引きしました。
エンジン値は、コンソールでAgentic Voiceを選んだフローを1本作り、エクスポートしてJSONを開いて確認しました。TextToSpeechEngineに入っていたのがconnect:agenticでした。
音声名のほうは、フローログで突き合わせました。インスタンスのフローログを有効にしたうえで、CloudWatch LogsのLogs Insightsでこのクエリを流します。
fields @timestamp, ContactId, Parameters.GlobalVoice, Parameters.GlobalEngine
| filter ContactFlowModuleType = "SetVoice"
| sort @timestamp desc
フローログ上のブロック名はSetVoice、パラメータはGlobalVoiceとGlobalEngineです。ここにAIKOとconnect:agenticが出れば、コンタクト属性で渡した音声名がSet Voiceブロックで意図通り解決されたと分かります。プロンプトの再生ブロックのログにも、合成に使われたVoiceが記録されていました。
音声名の公式な一覧が存在しないため、**AWS側の追加・変更で無告知に増減し得ます。**本記事のAIKO / REN / YUMIKOは2026年8月2日にコンソールとフローログで確認した名称です。音声を追加するときは、コンソールのSet Voiceブロックで正確な名前を確認してください。
正直なところ、公式ドキュメントを読めば済む話で終わらせたかったです。とはいえ、書いていないものは確かめるしかありません。エクスポートとフローログという2つの一次情報が取れたので、そこは悪くない結果でした。
音声を増やすときの手数
前節の動的フロー化が効くのはここです。同じロケールの音声を足すなら、共有パッケージの音声定義に1エントリ追加するだけで、フローは1本も増えません。
/**
* プレビューで選択できる音声の定義。
* - connectVoiceId は Set Voice ブロックへ動的に渡す音声名
* - 同じロケールなら、追加してもコンタクトフローは増えない
*/
export const PREVIEW_VOICES: readonly PreviewVoice[] = [
{
id: 'ja-jp-aiko',
displayName: '日本語・AIKO',
connectVoiceId: 'AIKO',
locale: 'ja-JP',
},
// 追加する音声はここに1エントリ足すだけ
];
新しいロケールを足したときだけ、CDKのデプロイでフローが1本増えます。
7. 実験用ツールとしての割り切り
このツールには意図的に入れていないものがあります。ごまかさずに書いておきます。
**入力した文章はCloudWatch Logsに平文で保存されます。**フローログを有効にしているため、previewTextがそのまま記録されます。機密情報・個人情報を含む文章は入力しないでください(Lambdaのアプリケーションログ側は文字数のみ記録し、本文は残していません)。
**認証はありません。**画面のURLとAPIエンドポイントを知っていれば誰でも使えます。本番相当で使うなら、フォークして認証やIP制限を足してください。
フローログの平文保存は、6章の音声名の追跡と引き換えです。実際に使われた音声名を確認できることを優先して、有効のままにしました。試聴専用ツールだからこの判断ができる、という前提付きの割り切りです。
料金についても一点。**架電しないから無料、ではありません。**WebRTCで開始したコンタクトも通常の音声コンタクトと同じように課金対象になり得ますし、API GatewayやLambda、CloudWatch Logsの費用もかかります。認証がないことと組み合わせると、URLが漏れた時点で従量課金の蛇口が開きます。
まとめ
本記事では、Agentic Voiceを架電なしでブラウザから試聴するツールの設計を紹介しました。StartWebRTCContactで毎回コンタクトを1件作り、ロケールごとに1本の動的フローで音声名をコンタクト属性から解決させる、という構成です。
作ってよかったのは、フロー公開と架電の往復が消えたことです。文言を10回書き直しても心が折れません。心残りは、音声名とエンジン値が公式に載っていないままの点です。AWS側が値を変えたら黙って壊れるので、定期的にコンソールからフローを再エクスポートして照合するしかなさそうです。
今後は日本語以外のロケールの追加と、同じ文章を複数音声で一気に聞き比べるUIも試してみたいと思います。
ソースコードは以下に置いています。
参考
