Transcription des appels téléphoniques avec Twilio Media Streams, Java, WebSockets et Spring Boot

February 03, 2020
Rédigé par

Transcription des appels téléphoniques à l'aide de Twilio Media Streams avec Java, WebSockets et Spring Boot

WebSockets est une technologie Web utilisée pour créer des connexions bidirectionnelles de longue durée entre un client et un serveur Web sur Internet. Avec Twilio Media Streams, vous pouvez diffuser le son en temps réel d'un appel téléphonique vers votre application Web à l'aide de WebSockets.

Ce blog post vous montrera comment créer un serveur WebSocket en Java à l'aide de Spring Boot qui recevra le son en temps réel d'un appel téléphonique et transmettra les données audio à la fonction de synthèse vocale de Google pour fournir une transcription en direct des voix de l'appel.

Configuration requise

Pour suivre cette procédure, vous devez disposer des éléments suivants :

Si vous voulez passer directement à la fin, vous pouvez consulter le projet terminé sur GitHub.

Mise en route

Le moyen le plus rapide de créer un nouveau projet avec Spring Boot est d'utiliser Spring Initializr. Laissez les entrées Project (Projet), Language (Langage) et la version Spring Boot à leurs valeurs par défaut. Pour Group (Groupe) et Artifact (Artefact) dans les métadonnées du projet, vous êtes libre de choisir, tant que vous suivez les conventions de dénomination Maven. Dans l'exemple de code, j'ai utilisé lol.gilliard comme groupe et websockets-transcription pour l'artefact.

En bas de la page, ajoutez la dépendance WebSocket, puis cliquez sur le bouton « Generate » (Générer) pour générer et télécharger le projet. Il sera téléchargé sous forme de fichier zip que vous pourrez décompresser et importer dans votre IDE préféré.

Capture d'écran de Spring Initializr comme décrit dans le texte.

Création du serveur Websocket

Spring sera en mesure de gérer la création de la connexion WebSocket, ce qui nous laissera le travail de gestion des données. Les données sont envoyées par WebSockets sous forme de petits blocs appelés « Messages ».

Spring attend de nous que nous écrivions un code capable de traiter ces messages. La manière la plus simple de le faire est d'étendre l'élément AbstractWebSocketHandler de Spring.

Création du gestionnaire WebSocket

Le projet que vous avez téléchargé à partir de Spring Initializr a une classe unique dans un sous-répertoire sous src/main/Java appelé <nom_votre_artefact>Application.java et, dans le même package, vous devez créer une nouvelle classe appelée TwilioMediaStreamsHandler, avec le code suivant :

// Note: package name will depend on your group and artifact name
// Your IDE should be able to help you here
package lol.gilliard.websocketstranscription;

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

public class TwilioMediaStreamsHandler extends AbstractWebSocketHandler {

  @Override
  public void afterConnectionEstablished(WebSocketSession session) {
     System.out.println("New connection has been established");
  }

  @Override
  public void handleTextMessage(WebSocketSession webSocketSession, TextMessage textMessage) {
     System.out.println("Message received, length is " + textMessage.getPayloadLength());
  }

  @Override
  public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
     System.out.println("Connection closed");
  }
}

Voir le code complet sur GitHub

Lorsqu'un nouveau client WebSocket se connecte, la méthode afterConnectionEstablished est appelée. Ensuite, la méthode handleTextMessage est appelée à plusieurs reprises, chaque fois qu'il y a un message. Lorsque la connexion est fermée, la méthode afterConnectionClosed est appelée.

Notez que l'élément WebSocketSession est transmis à toutes ces méthodes, ce qui permet à l'application de suivre simultanément plusieurs connexions WebSocket.

La prise en charge WebSocket de Spring peut gérer les messages binaires et les messages texte à l'aide de méthodes distinctes. Twilio Media Streams fournit les données audio encodées en JSON, c'est pourquoi il est seulement nécessaire de remplacer l'élément handleTextMessage. La première itération de ce code imprime la taille de chaque message dans System.out pour vérifier que les messages sont reçus, ce qui est fait dans le corps de la méthode handleTextMessage :

System.out.println("Message received, length is " + textMessage.getPayloadLength());

Voir la source sur GitHub

Configuration de Spring pour utiliser notre gestionnaire

Une connexion WebSocket est établie par le client qui envoie une requête HTTP régulière. Le serveur informe le client que ce point de terminaison attend des données WebSocket avec une liaison qui commence par une réponse HTTP 101 Switching Protocols. Le client le confirme et commence à envoyer des messages. En procédant à une légère configuration, Spring peut gérer tout cela pour nous.

Dans le même package contenant vos classes existantes, créez une nouvelle classe appelée WebSocketConfig. Cette classe configure Spring pour s'assurer que les requêtes vers un chemin particulier (dans notre cas /messages) seront traitées par notre code WebSocketHandler ci-dessus.

// Note: package name will depend on your group and artifact name
// Your IDE should be able to help you here
package lol.gilliard.websocketstranscription;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
  
}

Cela ne se compile pas tel quel, car pour l'implémentation de WebSocketConfigurer, nous devons implémenter une méthode appelée registerWebSocketHandlers :

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
   registry.addHandler(new TwilioMediaStreamsHandler(), "/messages").setAllowedOrigins("*");
}

Voir le code complet sur GitHub

Diffusion d'un appel téléphonique

Cela suffit pour gérer les clients WebSocket ; nous devons maintenant configurer quelque chose pour envoyer des données à notre point de terminaison WebSocket. Entrez dans Twilio Media Streams.

Twilio 101

Vous pouvez acheter un numéro de téléphone Twilio et configurer ce qui se passe quand quelqu'un l'appelle en créant un webhook qui répond avec un langage de configuration que nous aimons appeler TwiML.

L'application Spring Boot servira ce TwiML et prendra en charge les connexions WebSocket. Utilisez la bibliothèque Twilio Java Helper, en ajoutant ce qui suit à la section <dependencies> de pom.xml, à côté de la dépendance spring-boot-starter-websocket :

<dependency>
  <groupId>com.twilio.sdk</groupId>
  <artifactId>twilio</artifactId>
  <version>7.47.2</version>
</dependency>

Ensuite, créez une classe appelée TwiMLController dans le même package que vos autres classes qui servira l'élément TwiML :

// Note: package name will depend on your group and artifact name
// Your IDE should be able to help you here
package lol.gilliard.websocketstranscription;

import com.twilio.twiml.VoiceResponse;
import com.twilio.twiml.voice.Pause;
import com.twilio.twiml.voice.Say;
import com.twilio.twiml.voice.Start;
import com.twilio.twiml.voice.Stream;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.util.UriComponentsBuilder;

@Controller
public class TwiMLController {

   @GetMapping(value = "/twiml", produces = "application/xml")
   @ResponseBody
   public String getStreamsTwiml(UriComponentsBuilder uriInfo) {
       String wssUrl = "wss://" + uriInfo.build().getHost() + "/messages";

       return new VoiceResponse.Builder()
           .say(new Say.Builder("Hello! Start talking and the live audio will be streamed to your app").build())
           .start(new Start.Builder().stream(new Stream.Builder().url(wssUrl).build()).build())
           .pause(new Pause.Builder().length(30).build())
           .build().toXml();
   }
}

Voir le code complet sur GitHub

L'élément TwiML créé ici comporte 3 parties :

  • Say : un message de bienvenue
  • Start the Media Stream : en utilisant le même nom d'hôte que la requête TwiML et un chemin d'accès /messages
  • Pause : pause de 30 secondes pour donner à l'appelant le temps de parler. Au bout de 30 secondes, l'appel est terminé, mais l'appelant peut bien sûr raccrocher avant s'il le souhaite.

Configuration de Twilio pour utiliser l'application

Pour que Twilio puisse appeler votre application, elle doit être disponible sur une URL accessible au public. La configuration actuelle de l'application fait qu'elle écoutera uniquement sur localhost, qui est probablement (espérons-le !) non accessible depuis Internet. Il existe plusieurs options pour l'hébergement public, comme AWS,DigitalOcean ou Azure, mais à nos fins il est plus simple d'utiliser ngrokNgrok est un outil gratuit qui, une fois installé, peut créer un tunnel temporaire à partir d'une URL publique vers votre localhost.

Démarrez l'exécution de votre application en utilisant cette commande dans un terminal ou via votre IDE :

./mvnw spring-boot:run

Ensuite, démarrez ngrok avec

ngrok http 8080

Vous verrez une URL publique dans la sortie de ngrok, différente de celle ci-dessous, mais également composée de lettres et de chiffres aléatoires :

Capture d&#x27;écran de la sortie ngrok

Vous pouvez la tester en chargeant https://<VOTRE_SOUSDOMAINE_NGROK>.ngrok.io/twiml dans votre navigateur. Vous verrez alors une réponse du type :

<Response>
  <Say>Hello! Start talking and the live audio will be streamed to your app</Say>
  <Start><Stream url="wss://0dd24d67.ngrok.io/messages" /></Start>
  <Pause length="30" />
</Response>

Configuration d'un numéro de téléphone Twilio

L'achat et la configuration d'un numéro de téléphone avec Twilio ne prennent que quelques minutes. Si vous n'avez pas encore de compte Twilio, un compte d'essai gratuit fonctionnera parfaitement pour cette application.

Achat d'un numéro de téléphone

Sur la page des numéros de téléphone de votre console, vous pouvez acheter des numéros de centaines de pays :

Capture d&#x27;écran de la console Twilio achetant un numéro de téléphone

Choisissez une option locale, en vous assurant que vous sélectionnez la fonction Voice :

Capture d&#x27;écran de la console Twilio achetant un numéro de téléphone

Après avoir acheté le numéro, vous verrez l'écran de configuration du numéro de téléphone. Utilisez l'URL ngrok comme ci-dessus (n'oubliez pas le /twiml à la fin), et comme nous avons utilisé l'annotation @GetMapping dans le code, redéfinissez la méthode sur HTTP GET :

Capture d&#x27;écran de la console Twilio configurant les appels entrants

Enregistrez cette configuration et vous êtes prêt à appeler le numéro.

Un chat avec des lunettes de soleil. Légende « Je suis prêt »

Appelez votre nouveau numéro de téléphone. Vous entendez la lecture du message <Say> par un robot, puis Media Stream démarre et la console affiche quelque chose de similaire à ceci pendant que vous parlez :

New connection has been established
Message received, length is 57
Message received, length is 338
Message received, length is 374
... Many more lines like this ...
Message received, length is 379
Message received, length is 194
Connection closed

 Félicitations, vous avez un serveur WebSocket opérationnel avec Spring Boot, recevant des données audio en direct d'un appel téléphonique vers votre numéro Twilio.

Vous pouvez faire beaucoup de choses avec le flux audio. La partie suivante de cet article présentera un exemple : le transfert des données au service de synthèse vocale de Google pour la transcription en direct.

Diffusion de données vers le service de transcription de Google

Le service de synthèse vocale de Google peut accepter des données en streaming, ce qui en fait un outil idéal pour notre projet. Pour l'utiliser, vous devez configurer un projet et télécharger vos identifiants dans un fichier dont l'emplacement est stocké dans la variable d'environnement GOOGLE_APPLICATION_CREDENTIALS. C'est gratuit, mais vous avez besoin d'une carte de crédit pour créer le compte. Vous pouvez suivre les instructions de Google pour faire tout cela. Je vais prendre une tasse de thé et attendre votre retour.

Une tasse de thé fumante

Maintenant que vous avez configuré votre projet Google, nous pouvons continuer.

Vous devez ajouter une nouvelle classe pour extraire les données des messages WebSocket de Twilio et les envoyer à Google dans le bon format. Sur la base de l'exemple de code de Google, j'ai créé une classe qui peut être copiée à partir du repo sur GitHub et utilisée directement. N'oubliez pas que le nom de votre package sera probablement différent selon ce que vous avez choisi pour le groupe et l'artefact au début. Votre IDE devrait vous aider ici.

Vous devez ajouter quelques dépendances supplémentaires dans votre pom.xml (à côté de l'emplacement où vous avez ajouté la dépendance sur la bibliothèque Twilio Helper) :

<dependency>
  <groupId>org.json</groupId>
  <artifactId>json</artifactId>
  <version>20190722</version>
</dependency>

<dependency>
  <groupId>com.google.cloud</groupId>
  <artifactId>google-cloud-speech</artifactId>
  <version>1.22.1</version>
</dependency>

Enfin, il vous faut changer le code dans TwilioMediaStreamsHandler pour utiliser GoogleTextToSpeechService :

package lol.gilliard.websocketstranscription;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import java.util.HashMap;
import java.util.Map;

public class TwilioMediaStreamsHandler extends AbstractWebSocketHandler {

   private Map<WebSocketSession, GoogleTextToSpeechService> sessions = new HashMap<>();

   @Override
   public void afterConnectionEstablished(WebSocketSession session) throws Exception {
       sessions.put(session, new GoogleTextToSpeechService(
           transcription -> {
               System.out.println("Transcription: " + transcription);
           }
       ));
   }

   @Override
   protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) {
       sessions.get(session).send(message.getPayload());
   }

   @Override
   public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
       sessions.get(session).close();
       sessions.remove(session);
   }
}

À la ligne 13, une Map de WebSocketSession à GoogleTextToSpeechService est créée, de sorte qu'il est possible de prendre en charge plusieurs appels entrants simultanément sans induire Google en confusion en mélangeant tous les flux audio. Ensuite, à la ligne 26, la charge utile de chaque message entrant est envoyée à GoogleTextToSpeechService, qui est configuré pour imprimer la transcription chaque fois que Google l'envoie.

S'agissant de ngrok, il devrait toujours être en cours d'exécution. Si ce n'est pas le cas, redémarrez-le avec ngrok http 8080. Redémarrez le serveur avec ./mvnw spring-boot:run et appelez à nouveau votre numéro.

Après le message vocal, vous pouvez parler et vous verrez quelque chose comme ceci dans votre console :

Transcription:  Google
Transcription:  Google is
Transcription:  Google is
Transcription:  Google is transcribing
Transcription:  Google is transcribing
Transcription:  Google is transcribing
Transcription:  Google is transcribing the
Transcription:  Google is transcribing the live audio
Transcription:  Google is transcribing the live audio
Transcription:  Google is transcribing the live audio
Transcription:  Google is transcribing the live audio from
Transcription:  Google is transcribing the live audio from
Transcription:  Google is transcribing the live audio from
Transcription:  Google is transcribing the live audio from this
Transcription:  Google is transcribing the live audio from this phone call.

N'est-ce pas merveilleux ce que vous pouvez réaliser avec quelques classes et des services cloud puissants ?

La prochaine étape ?

Une myriade de possibilités s'offre à vous : vous pouvez diffuser le texte à un service de traductionl'enregistrer dans un fichier, vous faire la main sur l'analyse des sentiments ou choisir des mots-clés qui peuvent déclencher un message texte de suivi après l'appel. Je suis impatient de savoir ce que vous allez faire avec Java, WebSockets et Twilio Media Streams. Faites-le moi savoir dans les commentaires ci-dessous ou retrouvez-moi en ligne :

Twitter : @MaximumGilliard

E-mail : mgilliard@twilio.com