機械翻訳について

Twilioでの通話記録の追加、提案および要約の表示

次のステップを使用して設定を実行するか、トランスクリプトを追加するためのコードを含む圧縮ファイルをダウンロードしてすぐに試すことができます。また、このトピックで説明する提案と要約を示します。 詳細は、「圧縮ファイルからOJETアプリケーションを起動する方法」を参照してください。

Twilio設定を構成して、電話のライブRAWオーディオ・ストリームを特定の宛先に送信できます。 そこから、任意のリアルタイム音声SDKを使用して、オーディオストリームをテキストに変換できます。 このテキストは、Fusionでのレンダリングに使用されます。

アーキテクチャの概要を次に示します。

グラフィックを挿入

図の概要を次に示します。

  1. ストリームを有効にするためのTwilioアプリケーションの構成
  2. リアルタイム転写のためのリアルタイム転写サービスの初期化
  3. リアルタイム・オーディオ・ストリームをrealtime-transcriptionサービスに渡します
  4. feedLiveTranscript APIをコールして、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;
    }
  });
});
//...

リアルタイム転写のためのリアルタイム転写サービスの初期化

  1. リアルタイム音声記録アプリをダウンロードし、その内容をディレクトリに解凍します。
  2. 内容をディレクトリに抽出します。 index.tsファイルで、compartmentId変数をOCI音声サービスを持つコンパートメントIDで更新します。
  3. config.jsファイルをOCI構成で更新します。
  4. 端末でoci-speechディレクトリを開き、npm installコマンドを実行します。
  5. npm run buildコマンドを使用してプロジェクトを構築します。
  6. 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エンゲージメント・パネルにレンダリングされます。