Twilioでの通話記録の追加、提案および要約の表示
次のステップを使用して設定を実行するか、トランスクリプトを追加するためのコードを含む圧縮ファイルをダウンロードしてすぐに試すことができます。また、このトピックで説明する提案と要約を示します。 詳細は、「圧縮ファイルからOJETアプリケーションを起動する方法」を参照してください。
Twilio設定を構成して、電話のライブRAWオーディオ・ストリームを特定の宛先に送信できます。 そこから、任意のリアルタイム音声SDKを使用して、オーディオストリームをテキストに変換できます。 このテキストは、Fusionでのレンダリングに使用されます。
アーキテクチャの概要を次に示します。
グラフィックを挿入
図の概要を次に示します。
- ストリームを有効にするためのTwilioアプリケーションの構成
- リアルタイム転写のためのリアルタイム転写サービスの初期化
- リアルタイム・オーディオ・ストリームを
realtime-transcriptionサービスに渡します feedLiveTranscriptAPIをコールして、Fusionでトランスクリプトをレンダリングします
ストリームを有効にするためのTwilioアプリケーションの構成
ストリームは、生成AIおよびCTIの概要で構築したクイック・スタート・アプリケーションから有効にする必要があります。
handler.jsファイルのvoiceResponseファンクションからストリームを有効にするには、voiceResponseファンクションを初期化した後に次のコードを追加します。
const start = twiml.start();
start.stream({
name: 'Example Audio Stream',
url: 'wss://twilio-node-voice-stream.onrender.com/',
track: 'both_tracks'
});
Twilioストリームの詳細は、https://www.twilio.com/docs/voice/twiml/streamを参照してください。
また、クイック・スタート・アプリケーションからメディア・ツールバー・アプリケーションにオーディオ・ストリームを転送するロジックを追加する必要もあります。 これはWebSocketによって行われます。
このため、クイック・スタート・アプリケーションのindex.jsファイルで、次の例に示すようにWebソケットを初期化します。
//...
const WebSocket = require('ws');
//...
// const server = http.createServer(app);
const wss = new WebSocket.Server({server});
var callSidWebSocketMap = new Map();
var streamSidCallSidMap = new Map();
//...
wss.on("connection", function connection(ws) {
console.log("New Connection Initiated");
ws.on("message", function incoming(message) {
const msg = JSON.parse(message);
switch (msg.event) {
case "registerClient":
console.log('New client registered: ' + JSON.stringify(msg));
callSidWebSocketMap.set(msg.accountSid, ws);
break;
case "connected":
console.log(`A new call has connected.`);
break;
case "start":
console.log('New stream started: ' + JSON.stringify(msg));
streamSidCallSidMap.set(msg.start.streamSid, msg.start.accountSid)
break;
case "media":
var twilioData;
if (msg?.media?.track === 'outbound') {
twilioData = "{\"event\":\"media\",\"streamId\":\""+msg?.streamSid+"\",\"from\":\"agent\",\"payload\":\"" + msg?.media?.payload + "\"}";
} else {
twilioData = "{\"event\":\"media\",\"streamId\":\""+msg?.streamSid+"\",\"payload\":\"" + msg?.media?.payload + "\"}";
}
const clientWebSocket = callSidWebSocketMap.get(streamSidCallSidMap.get(msg.streamSid));
if (clientWebSocket) {
console.log(`Sending media payload to clientWebSocket`);
clientWebSocket.send(twilioData);
} else {
console.log(`Client not found`);
}
console.log(twilioData);
break;
case "stop":
console.log('Call Has Ended' + JSON.stringify(msg));
callSidWebSocketMap.get(msg?.stop?.accountSid)?.close();
break;
}
});
});
//...
リアルタイム転写のためのリアルタイム転写サービスの初期化
- リアルタイム音声記録アプリをダウンロードし、その内容をディレクトリに解凍します。
- 内容をディレクトリに抽出します。
index.tsファイルで、compartmentId変数をOCI音声サービスを持つコンパートメントIDで更新します。 config.jsファイルをOCI構成で更新します。- 端末で
oci-speechディレクトリを開き、npm installコマンドを実行します。 npm run buildコマンドを使用してプロジェクトを構築します。npm run startコマンドを実行してサービスを起動します。oci-speechサービスのWebソケットが実行されています。 TwilioからのオーディオストリームをこのWebSocketに送信して、トランスクリプション結果を取得するだけです。
リアルタイム音声ストリームをリアルタイム翻訳サービスに渡す
vendorHandler.tsファイルで、次のクラス変数を初期化します。
export class VendorHandler implements ICtiVendorHandler {
// ...
private static messageIds: string[] = [];
private static TWILIO_SERVICE: string = 'https://twilio-node-voice-stream.onrender.com'; // Your quick-start application URL
private static RT_SPEECH_SERVICE: string = 'wss://phoenix339284.appsdev.fusionappsdphx1.oraclevcn.com:8004/'; // Your real-time speech transcription service URL
private static TWILIO_WS_URL: string = 'wss://twilio-node-voice-stream.onrender.com/'; // Your quick-start application WebSocket URL
private static transcriptionServerWsForAgent: WebSocket;
// ...
}
次の関数を定義します。
// This function is the entry point for transcription
private initTranscription(accountSid: string): void {
VendorHandler.transcriptionServerWsForAgent = new WebSocket(VendorHandler.RT_SPEECH_SERVICE); // WebSocket connection to real-time speech transcription service
let self = this;
// 1. Initialize WebSocket connection to real-time speech transcription service
VendorHandler.transcriptionServerWsForAgent.addEventListener("open", (event) => {
// 2. Once the WebSocket connection to realtime speech transcript service is success,
// Initialize WebSocket connection to Twilio to get the audio stream
this.initializeWebsocketConnectionToTwilio(accountSid);
});
// 3. The transcription results from the real
VendorHandler.transcriptionServerWsForAgent.addEventListener("message", async (event) => {
await this.handleTranscriptResponseFromSpeechService(event, self);
});
}
// This function initializes the WebSocket connection to Twilio
private initializeWebsocketConnectionToTwilio(accountSid: string): void {
let twilioServerWs: WebSocket = new WebSocket(VendorHandler.TWILIO_WS_URL);
// 2.1. Initialize WebSocket connection to your Twilio quick-start application
twilioServerWs.addEventListener("open", (event) => {
// 2.2. Once the WebSocket connection to Twilio is success,
// Send registerClient message to Twilio to register the client for transcription
twilioServerWs.send(JSON.stringify({"event":"registerClient", "accountSid": accountSid}));
});
twilioServerWs.addEventListener("message", async (event) => {
// 2.3. Here, you will receive the audio stream payload and you need to pass this to
// your real-time speech service websocket for getting the transcript results.
this.handleAudioStreamFromTwilio(event);
});
twilioServerWs.addEventListener("error", (err) => {
console.log("Message from server ", err);
});
}
// This function forwards the audio stream to real-time transcript service
private handleAudioStreamFromTwilio(event: any): void {
const msg = JSON.parse(event.data);
switch (msg.event) {
case "media":
// 2.3.1. Send the audio stream to your real-time transcript service in a specific format as returned from generatePayloadForTranscriptServer function
VendorHandler.transcriptionServerWsForAgent.send(JSON.stringify(this.generatePayloadForTranscriptServer(msg)));
break;
}
}
// This function generates the payload to transcription function in a specific format
private generatePayloadForTranscriptServer(message: any): any {
return {
"callId": message.streamId,
"role": message.from === 'agent' ? 'AGENT' : 'END_USER',
"message": message.payload
}
}
// This function handles the results generated from the real-time speech transcript service
private async handleTranscriptResponseFromSpeechService(event: any, self: any): Promise<void> {
let state = "STARTED";
const responseFromServer = JSON.parse(event.data);
const role: string = responseFromServer.role == 'AGENT' ? 'AGENT' : 'END_USER'
if (responseFromServer.final) {
state = "CLOSED"
} else {
if (VendorHandler.messageIds.includes(responseFromServer?.messageId)) {
state = "INPROGRESS";
} else {
VendorHandler.messageIds.push(responseFromServer?.messageId)
}
}
// Invoke UEF API to add the transcript message to the engagement panel.
await self.integrationEventsHandler.addRealTimeTranscript(responseFromServer?.messageId, responseFromServer?.transcript, role, state);
}
受信および送信イベント・ハンドラからinitTranscription関数を起動します。
public incomingCallCallback = (call: Call) => {
this.initTranscription(call.parameters.AccountSid);
//...
}
public async makeOutboundCall(phoneNumber: string, eventId: string) {
//...
// if (this.device) {
// this.call = await this.device.connect({ params });
this.initTranscription(this.call.parameters.AccountSid);
//...
// }
}
完了コード
着信コールを受け入れるためのvendorHandler.tsファイルの完全なコードを次に示します:
import { ICtiVendorHandler } from './ICtiVendorHandler';
import { Device, Call } from '@twilio/voice-sdk';
import {IntegrationEventsHandler} from "../integrationEventsHandler";
export class VendorHandler implements ICtiVendorHandler {
private twilio: any;
private device: Device | null;
private integrationEventsHandler: IntegrationEventsHandler;
private call: Call | null;
public idAndToken: any;
private static messageIds: string[] = [];
private static TWILIO_SERVICE: string = 'https://twilio-node-voice-stream.onrender.com';
private static RT_SPEECH_SERVICE: string = 'wss://phoenix339284.appsdev.fusionappsdphx1.oraclevcn.com:8004/';
private static TWILIO_WS_URL: string = 'wss://twilio-node-voice-stream.onrender.com/';
private static transcriptionServerWsForAgent: WebSocket;
constructor(integrationEventsHandler: IntegrationEventsHandler) {
this.twilio = (window as any).Twilio;
this.device = null;
this.idAndToken = null;
this.integrationEventsHandler = integrationEventsHandler;
this.call = null;
}
public async makeAgentAvailable(): Promise<void> {
this.idAndToken = await this.getIdAndToken();
this.device = new this.twilio.Device(this.idAndToken.token, {
logLevel: 1,
codecPreferences: ["opus", "pcmu"],
enableRingingState: true
});
let resolve: Function;
let reject: Function;
if (this.device) {
this.device.on("registered", () => {
console.log("Registration completed ...")
resolve();
});
this.device.on("error", (deviceError: any) => {
console.error("Registration Failed ...", deviceError);
reject();
});
this.device.on("incoming", this.incomingCallCallback);
this.device.register();
}
return new Promise((res: Function, rej: Function) => {
resolve = res;
reject = rej;
});
}
public async makeAgentUnavailable() {
throw new Error('Method not implemented.');
}
public async makeOutboundCall(phoneNumber: string, eventId: string) {
const params = {
To: phoneNumber,
};
if (this.device) {
this.call = await this.device.connect({ params });
this.initTranscription(this.call.parameters.AccountSid);
this.call.on("accept", () => { this.integrationEventsHandler.outboundCallAcceptedHandler(eventId) });
this.call.on("disconnect", () => { this.integrationEventsHandler.callHangupHandler(eventId) });
this.call.on("cancel", () => { this.integrationEventsHandler.callRejectedHandler(eventId) });
}
}
public async acceptCall() {
if (this.call) {
this.call.accept();
}
}
public async rejectCall() {
if (this.call) {
this.call.reject();
}
}
public async hangupCall() {
if (this.call) {
this.call.disconnect();
}
}
private initializeWebsocketConnectionToTwilio(accountSid: string): void {
let twilioServerWs: WebSocket = new WebSocket(VendorHandler.TWILIO_WS_URL);
twilioServerWs.addEventListener("open", (event) => {
twilioServerWs.send(JSON.stringify({"event":"registerClient", "accountSid": accountSid}));
});
twilioServerWs.addEventListener("message", async (event) => {
this.handleAudioStreamFromTwilio(event);
});
twilioServerWs.addEventListener("error", (err) => {
console.log("Message from server ", err);
});
}
private handleAudioStreamFromTwilio(event: any): void {
const msg = JSON.parse(event.data);
switch (msg.event) {
case "media":
VendorHandler.transcriptionServerWsForAgent.send(JSON.stringify(this.generatePayloadForTranscriptServer(msg)));
break;
}
}
private generatePayloadForTranscriptServer(message: any): any {
return {
"callId": message.streamId,
"role": message.from === 'agent' ? 'AGENT' : 'END_USER',
"message": message.payload
}
}
private async handleTranscriptResponseFromSpeechService(event: any, self: any): Promise<void> {
let state = "STARTED";
const responseFromServer = JSON.parse(event.data);
const role: string = responseFromServer.role == 'AGENT' ? 'AGENT' : 'END_USER'
if (responseFromServer.final) {
state = "CLOSED"
} else {
if (VendorHandler.messageIds.includes(responseFromServer?.messageId)) {
state = "INPROGRESS";
} else {
VendorHandler.messageIds.push(responseFromServer?.messageId)
}
}
await self.integrationEventsHandler.addRealTimeTranscript(responseFromServer?.messageId, responseFromServer?.transcript, role, state);
}
private initTranscription(accountSid: string): void {
VendorHandler.transcriptionServerWsForAgent = new WebSocket(VendorHandler.RT_SPEECH_SERVICE);
let self = this;
VendorHandler.transcriptionServerWsForAgent.addEventListener("open", (event) => {
this.initializeWebsocketConnectionToTwilio(accountSid);
});
VendorHandler.transcriptionServerWsForAgent.addEventListener("message", async (event) => {
await this.handleTranscriptResponseFromSpeechService(event, self);
});
}
private async getIdAndToken(): Promise<any> {
const headers: Headers = (new Headers()) as Headers;
const url: string = `${VendorHandler.TWILIO_SERVICE}/token`; // Replace this url with the url of the deployed node app
headers.set('Accept', 'application/json');
const request: Request = new Request(url, {
method: 'GET',
headers: headers
}) as Request;
const idAndToken: Response = await fetch(request);
this.idAndToken = await idAndToken.json();
return this.idAndToken;
}
public incomingCallCallback = (call: Call) => {
this.initTranscription(call.parameters.AccountSid);
this.integrationEventsHandler.incomingCallHandler(call.parameters.From, call.parameters.CallSid);
this.call = call;
this.call.on("cancel", () => { this.integrationEventsHandler.callRejectedHandler(call.parameters.CallSid) });
this.call.on("disconnect", () => { this.integrationEventsHandler.callHangupHandler(call.parameters.CallSid) });
this.call.on("reject", () => { this.integrationEventsHandler.callRejectedHandler(call.parameters.CallSid) });
}
public async sendTextMessage(suggestionData: IMcaOnToolbarInteractionCommandData, resolveRef: Function): Promise<void> {
var myHeaders = new Headers();
myHeaders.append("Authorization", 'Basic QUM0NzJjNjZmYTU0ZTRiNzNhYWExZTg1Yzk4Nzc1YmRjZjo3Mzg5NjlkYzBkMjNjMTVhMGEwNzE1NDY0N2ZiNjNhYg=='); // Add your authorization header here
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("To", this.call?.parameters.From || '');
urlencoded.append("From", "+13087374071");// Your TWILIO Number
urlencoded.append("Body", suggestionData.inData.metadata.originalSuggestionText + " Please refer: " + suggestionData.inData.metadata.externalUrl);
var requestOptions: any = {
method: 'POST',
headers: myHeaders,
body: urlencoded,
redirect: 'follow'
};
fetch(`${VendorHandler.TWILIO_SERVICE}`, requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
}
}
進捗の確認
これらのステップを完了したら、OJET serveを使用してアプリケーションを起動し、Fusionアプリケーションにサインインします。 メディア・ツールバーを開き、エージェントの空き状況ボタンをクリックして、エージェントを呼び出せるようにします。 次に、カスタマ・ケア番号へのコールを開始します。 受信コール通知は、メディア・ツールバー・アプリケーションおよびFusionウィンドウに表示されます。 このコールは、メディア・ツールバー・アプリケーションまたはFusionアプリケーションから受け入れることができます。 会話が開始されると、リアルタイム・トランスクリプトがFusionエンゲージメント・パネルにレンダリングされます。