ページの一覧

ドキュメント

Web・アプリでつなぐ

ブラウザやアプリから AI と話す手順。セッションの作成、client_token と WebRTC の接続、声で返す・文章だけ返すの違い、avacast との組み合わせ。

Markdown で見る

ブラウザやスマートフォンのアプリからは、WebRTC で voicast の音声サーバーにつなぎます。API キーはサービスのサーバーだけに置き、ブラウザやアプリには使い捨ての client_token だけを渡します。

サービスのサーバー ── API キーで POST /v1/sessions ──→ voicast
        │ client_token・webrtc_url・ice_servers だけを渡す
ブラウザ・アプリ ── client_token 付きの WebRTC の offer ──→ voicast の音声サーバー

1. セッションを作る(サーバー)#

bash
curl https://api.voicast.jp/v1/sessions \
  -H "Authorization: Bearer $VOICAST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tenant_id": "ten_…", "output": "audio" }'
json
{
  "id": "sess_…",
  "tenant_id": "ten_…",
  "agent_id": "agt_…",
  "output": "audio",
  "client_token": "vct_…",
  "expires_at": 1790000120000,
  "webrtc_url": "https://voice.voicast.jp/v1/webrtc/offer",
  "ice_servers": [{ "urls": "stun:stun.cloudflare.com:3478" }]
}
入力 必須 内容
tenant_id ○ テナントの ID
agent_id 会話の設定の ID。省くとテナントの既定
output audio(声で返す。既定)か text(返事の文章だけを返す)
  • client_token は 2 分で切れ、1 回だけ使えます。つなぎ直すときは新しいセッションを作ります。
  • 会話の設定は、つないだときの最新の版を使います。
  • ice_servers は RTCPeerConnection の iceServers にそのまま渡します。STUN のほか、voicast が TURN を用意しているときは、期限の短い資格情報(username・credential)付きの TURN が入ります。会社のネットワークなど UDP が通らないところでも、TURN があればつながりやすくなります。
  • 残高が無いなどで新しい通話を断っているときは 402 です(エラーと制限)。テナントが止まっていれば 409 tenant_disabled、会話の設定が決まらなければ 422 no_agent です。

2. つなぐ(ブラウザ・アプリ)#

SDK を使う#

SDK の voicast/web は、マイクの取得・WebRTC の接続・イベントの受け取りをまとめたものです。

ts
import { VoiceSession } from 'voicast/web';

// client_token・webrtc_url・ice_servers は、自社のサーバーから受け取る
const session = new VoiceSession({ clientToken, webrtcUrl, iceServers });
session.on('user-transcript', ({ text, final }) => showUser(text, final));
session.on('bot-output', ({ text }) => showBot(text));
session.on('closed', ({ reason }) => console.log('closed', reason));
await session.start(); // マイクの許可を求めてからつなぐ

hangupButton.onclick = () => session.stop();

ブラウザは、利用者の操作(クリックなど)の中でないと音を出せないことがあります。start() はボタンを押したときに呼びます。

Pipecat のクライアントを使う#

音声サーバーは Pipecat の SmallWebRTC と同じ方式です。Pipecat のクライアント(JavaScript・iOS・Android)の SmallWebRTC のトランスポートで、接続先に webrtc_url を、requestData に { token: client_token } を渡します。

自分で WebRTC をつなぐ#

ほかの WebRTC の実装からも、次の手順でつなげます。

  1. マイクの音声トラックを足した RTCPeerConnection と、データチャンネル(名前は自由。例 chat)を作る
  2. offer を作り、ICE の候補を集め終えてから、webrtc_url に { sdp, type, token } を POST する
  3. 返った { sdp, type, pc_id } を answer として入れる
  4. あとから見つかった ICE の候補は、PATCH webrtc_url に { pc_id, candidates: [{ candidate, sdp_mid, sdp_mline_index }] } で足す
  5. データチャンネルが開いたら RTVI の client-ready を送り、そのあと 1 秒ごとに ping の文字列を送る(音声サーバーは数秒途切れると切れたとみなします)
js
const pc = new RTCPeerConnection({ iceServers }); // セッションの ice_servers
const mic = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
pc.addTransceiver(mic.getAudioTracks()[0], { direction: 'sendrecv' });
pc.ontrack = (e) => { audioEl.srcObject = e.streams[0]; audioEl.play(); };

const dc = pc.createDataChannel('chat', { ordered: true });
dc.onopen = () => {
  dc.send(JSON.stringify({ label: 'rtvi-ai', type: 'client-ready', id: crypto.randomUUID(), data: { version: '1.0.0' } }));
  setInterval(() => dc.readyState === 'open' && dc.send(`ping: ${Date.now()}`), 1000);
};
dc.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.label === 'rtvi-ai') onRtvi(msg); // 下の「届くもの」
};

await pc.setLocalDescription(await pc.createOffer());
await iceGatheringComplete(pc);
const r = await fetch(webrtcUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ sdp: pc.localDescription.sdp, type: pc.localDescription.type, token: clientToken }),
});
if (!r.ok) throw new Error(`つなげません(${r.status})`); // 403: トークンの期限切れ・使用済み
const answer = await r.json();
await pc.setRemoteDescription({ type: answer.type, sdp: answer.sdp });

音声サーバーがつなぎ直しを求めたときは、データチャンネルに { type: 'signalling', message: { type: 'renegotiate' } } が届きます。新しい offer に pc_id と restart_pc: false を付けて、同じ webrtc_url に POST します。

届くもの#

返事の声は、output が audio なら音声トラックで届きます。データチャンネルには RTVI のメッセージ(label: 'rtvi-ai')が届きます。

type 内容
bot-ready AI の準備ができた
user-transcription 相手の発言の文字(data.text・data.final)
bot-output AI が話した文(output: 'audio' のとき)
bot-started-speaking・bot-stopped-speaking AI が話し始めた・話し終えた
server-message voicast の知らせ(下)

文章だけを返す(output: text)#

output: 'text' のセッションは声を出さず、返事の文章を 1 文ずつ server-message で届けます。アバター(avacast など)や自社の音声合成に話させるとき、画面に文字で出すときに使います。

ts
type VoicastMessage =
  | { type: 'voicast.reply'; text: string }    // 返事の 1 文(あいさつ・締めの言葉も同じ形)
  | { type: 'voicast.interrupted' }            // 相手が話し始めた。話している途中の返事を止める
  | { type: 'voicast.end'; outcome: string };  // 会話を終える(このあと接続が閉じる)

相手の声は、output によらずマイクの音声トラックで送ります。

avacast と組み合わせる#

avacast は、写真から作ったアバターをリアルタイムに話させる API です。voicast が聞き取りと返事の文章を、avacast が顔と声を受け持つと、画面の中の受付係と会話できます。

ts
import { VoiceSession } from 'voicast/web';
import { AvacastSession } from 'https://avacast.jp/sdk/v1.js';

// どちらのトークンも自社のサーバーで作る(voicast は output: 'text' のセッション)
const { voicast, avacast } = await fetch('/api/start-reception', { method: 'POST' }).then((r) => r.json());

const avatar = new AvacastSession(avacast.client_token, {
  signalingUrl: avacast.webrtc.signaling_url,
  iceServers: avacast.webrtc.ice_servers,
});
avatar.attach(document.getElementById('avatar'));
await avatar.start();

const talk = new VoiceSession({
  clientToken: voicast.client_token,
  webrtcUrl: voicast.webrtc_url,
  iceServers: voicast.ice_servers,
});
talk.on('reply', ({ text }) => avatar.speak(text));     // voicast の返事を avacast に話させる
talk.on('interrupted', () => avatar.interrupt());       // 相手が話し始めたら止める
talk.on('end', ({ outcome }) => showResult(outcome));
await talk.start();

自社のサーバーでは、voicast の POST /v1/sessions(output: 'text')と avacast のセッションの作成を並べて呼び、両方のトークンだけを画面に返します。

結果#

通話が終わると、電話と同じく通話の記録・Webhook が残ります。Web・アプリの通話は channel: 'web' で、セッションの id(sess_…)が provider_call_id になります。

bash
curl https://api.voicast.jp/v1/calls/by-provider/sess_… \
  -H "Authorization: Bearer $VOICAST_API_KEY"

注意#

  • 相手のマイクに AI の声が回り込むと、AI が自分の声に反応することがあります。ブラウザのエコーキャンセル(echoCancellation: true)を有効にします。ヘッドホンだとさらに安定します。
  • 会社のネットワークなどで UDP が通らないと、つながらないことがあります。セッションの ice_servers を必ず渡してください(TURN が入っていれば、その経路でつなぎます)。
  • つなぐ段階(offer)で断られたときは 403 です(トークンの期限切れ・使用済み、料金の都合など)。料金の都合はいずれ 402 にそろえる予定なので、403 と 402 のどちらも「つなげない」として扱います。