Live-Transkribieren von Telefonanrufen

September 12, 2019
Autor:in:

Live-Transkribieren von Anrufen

Hallo und danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Live Transcribing Phone Calls using Twilio Media Streams and Google Speech-to-Text.

Mit Twilio Media-Streams können wir jetzt die Funktionen unserer auf Twilio basierenden Sprachanwendung erweitern. Denn sie ermöglichen den Zugriff auf den Audiodatenstream von Anrufen in Echtzeit. So können wir beispielsweise Programme erstellen, die die Sprache eines Telefonanrufs live in ein Browserfenster transkribieren, eine Sentimentanalyse der Sprache bei einem Telefonanruf durchführen oder sogar Stimmbiometrie nutzen, um Personen zu erkennen.

In diesem Blog zeige ich Schritt für Schritt, wie wir Sprache von einem Telefonanruf mit Hilfe von Twilio und Google Speech-to-Text mit Node.js live im Browser in Text transkribieren.

Wenn du die Schritt-für-Schritt-Anleitungen überspringen möchtest, kannst du mein GitHub-Repository klonen und anhand der Infodatei die Einrichtung durchführen. Oder wenn du dir lieber ein Video ansiehst, dann findest du eine beispielhafte Vorgehensweise hier.

Voraussetzungen

Bevor wir beginnen, benötigen wir noch Folgendes:

Einrichten des lokalen Servers

Twilio Media-Streams nutzen die WebSocket API, um die Audiodaten des Telefonanrufs live in die Anwendung zu streamen. Beginnen wir mit dem Einrichten eines Servers, der WebSocket-Verbindungen verarbeiten kann.

Wir öffnen das Terminal und erstellen einen neuen Projektordner sowie eine index.js-Datei.

mkdir twilio-streams
cd twilio-streams
touch index.js

Zur Verarbeitung von HTTP-Anfragen verwenden wir das integrierte http-Modul von Nodes sowie Express. Für die WebSocket-Verbindungen verwenden wir ws, einen einfachen WebSocket-Client für Node.

Zur Installation von ws und express führen wir im Terminal die folgenden Befehle aus:

npm install ws express

Wir öffnen die index.js-Datei und fügen den folgenden Code hinzu, um den Server einzurichten.

const WebSocket = require("ws");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const wss = new WebSocket.Server({ server });

wss.on("connection", function connection(ws) {
    console.log("Neue Verbindung initiiert");
});

//Handle HTTP Request
app.get("/", (_, res) => res.send("Hallo Welt"));

console.log("Gestartet auf Port 8080");
server.listen(8080);

Wir speichern und führen die index.js-Datei mit node index.js aus. Dann öffnen wir den Browser und navigieren zu http://localhost:8080. Der Browser sollte Hello World anzeigen.

Hallo Welt im Browser

 

Nachdem wir jetzt wissen, dass HTTP-Anfragen funktionieren, können wir unsere WebSocket-Verbindung testen. Wir öffnen die Konsole des Browsers und führen den folgenden Befehl aus:

new WebSocket('ws://localhost:8080')

Wenn wir zum Terminal zurückkehren, sollten wir ein Protokoll mit folgendem Text sehen: New Connection Initiated.

Connect to WebSocket Server from browser

 

Einrichten von Telefonanrufen

Wir richten nun unsere Twilio-Nummer ein, um eine Verbindung zum WebSocket-Server herzustellen.

Zuerst müssen wir den Server so ändern, dass er WebSocket-Nachrichten verarbeiten kann. Diese werden über Twilio gesendet, sobald das Streaming des Telefonanrufs beginnt. Es gibt vier wichtige Nachrichtenereignisse, die wir überwachen möchten: „connected“, „start“, „media“ und „stop“.

  • Connected: wenn Twilio erfolgreich eine WebSocket-Verbindung zu einem Server herstellt
  • Start: wenn Twilio mit dem Streaming von Datenpaketen beginnt
  • Media: kodierte Medienpakete (Audiodaten)
  • Stop: wenn das Streaming endet, wird das Stoppereignis gesendet.

Wir ändern die index.js-Datei so, dass Nachrichten protokolliert werden, wenn jede einzelne dieser Nachrichten auf dem Server eingeht.

const WebSocket = require("ws");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const wss = new WebSocket.Server({ server });

wss.on("connection", function connection(ws) {
    console.log("Neue Verbindung initiiert");

    ws.on("message", function incoming(message) {
        const msg = JSON.parse(message);
        switch (msg.event) {
            case "connected":
                console.log(`Neuer Anruf verbunden`);
                break;
            case "start":
                console.log(`Starte den Media-Stream ${msg.streamSid}`);
                break;
            case "media":
                console.log(`Empfange Audio`)
                break;
            case "stop":
                console.log(`Anruf beendet`);
                break;
        }
    });
});

//Handle HTTP Request
app.get("/", (_, res) => res.send("Hallo Welt"));

app.post("/", (req, res) => {
    res.set("Content-Type", "text/xml");
    res.send(`
      <Response>
        <Start>
         <Stream url="wss://${req.headers.host}/"/>
        </Start>
        <Say language="de-DE">Hallo. Ich werde die Tonspur für die nächsten 60 Sekunden an einen WebSocket weiterstreamen.</Say>
        <Pause length="60" />
      </Response>`);
});

console.log("Gestartet auf Port 8080");
server.listen(8080);

Jetzt müssen wir die Webhook unsere Twilio-Nummer so einrichten, um mit dem Streaming von Audiodaten auf den Server zu beginnen. Mit einer TwiML-Antwort können wir steuern, was passiert, wenn wir unsere Twilio-Nummer anrufen. Wir erstellen eine HTTP-Route, die TwiML zurückgibt und Twilio anweist, Audiodaten vom Anruf auf den Server zu streamen. Dazu fügen wir der index.js-Datei die folgende POST-Route hinzu.

const WebSocket = require("ws");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const wss = new WebSocket.Server({ server });

wss.on("connection", function connection(ws) {
    console.log("Neue Verbindung initiiert");

    ws.on("message", function incoming(message) {
        const msg = JSON.parse(message);
        switch (msg.event) {
            case "connected":
                console.log(`Neuer Anruf verbunden`);
                break;
            case "start":
                console.log(`Starte den Media-Stream ${msg.streamSid}`);
                break;
            case "media":
                console.log(`Empfange Audio`)
                break;
            case "stop":
                console.log(`Anruf beendet`);
                break;
        }
    });
});

//Handle HTTP Request
app.get("/", (_, res) => res.send("Hallo Welt"));

app.post("/", (req, res) => {
    res.set("Content-Type", "text/xml");
    res.send(`
      <Response>
        <Start>
         <Stream url="wss://${req.headers.host}/"/>
        </Start>
        <Say language="de-DE">Hallo. Ich werde die Tonspur für die nächsten 60 Sekunden an einen WebSocket weiterstreamen.</Say>
        <Pause length="60" />
      </Response>`);
});

console.log("Gestartet auf Port 8080");
server.listen(8080);

Nun da die WebHook fertig ist, kaufen wir eine Telefonnummer. Im Terminal führen wir den folgenden Befehl aus. Ich habe den Ländercode DE verwendet, um eine Mobiltelefonnummer zu kaufen, aber das kann ganz einfach in eine Nummer eines anderen Landes geändert werden. Wir sollten uns die ausgewählte Twilio Nummer notieren, sobald die Antwort zurückgegeben wurde.

twilio api:core:available-phone-numbers:mobile:list --country-code DE
twilio api:core:incoming-phone-numbers:create --bundle-sid=BUxxxx  --address-sid=ADxxxx --friendly-name=cli-purchase --phone-number=<Eine der Nummer des vorigen Befehls>

Damit Twilio eine Verbindung zum lokalen Server herstellen kann, müssen wir den Port für das Internet verfügbar machen. Wir verwenden hierfür ngrok und führen den folgenden Befehl in einem neuen Terminalfenster aus:

ngrok http 8080

Es sollte eine Ausgabe mit einer Weiterleitungsadresse erscheinen und wir kopieren die https-URL in die Zwischenablage. 

Forwarding                    https://xxxxxxxx.ngrok.io -> http://localhost:8080

ngrok terminal session showing the forwarding URL

 

Zum Abschluss aktualisieren wir die Telefonnummer, so dass eingehende Anrufe via ngrok auf Localhost-URL verweisen:

twilio phone-numbers:update <die gekaufte Rufnummer> --voice-url <die "forwarding URL">

Wir öffnen das Terminalfenster und starten die index.js-Datei erneut damit unsere Änderungen wirksam werden. Wenn wir jetzt unsere Twilio-Telefonnummer anrufen, sollten wir die folgende Aufforderung hören: „Hallo. Ich werde die Tonspur für die nächsten 60 Sekunden an einen WebSocket weiterstreamen.“ Das Terminal wird mehrfach Empfange Audio protokollieren.

Eingehender Audiostream

 

Transkribieren von Sprache in Text

Wir haben jetzt erreicht, dass Audiodaten unseres Anrufs auf den Server gestreamt werden. In diesem Blogpost verwenden wir die Speech-to-Text-API der Google Cloud Platform, um die Sprachdaten des Telefonanrufs zu transkribieren.

Bevor wir damit beginnen können, müssen wir noch folgende Voraussetzungen erfüllen. Dies können wir entweder über die Cloud Console oder das Cloud SDK erledigen

  1. Ein neues GCP-Projekt einrichten
  2. Die Google Speech-to-Text-API für das Projekt aktivieren
  3. Ein Dienstkonto erstellen
  4. Einen privaten Schlüssel für das Dienstkonto erstellen
  5. Den Schlüßel als JSON herunterladen
  6. Wir legen die Umgebungsvariable GOOGLE_APPLICATION_CREDENTIALS auf den relativen Dateipfad der JSON-Datei fest, die unseren Dienst Konto Schlüßel enthält.

Wir führen den folgenden Befehl aus, um die Google Cloud Speech-to-Text-Clientbibliotheken zu installieren.

npm install --save @google-cloud/speech

Jetzt können wir die API in unserem Code verwenden.

Zuerst fügen wir den Speech-Client aus der Google Speech-to-Text-Bibliothek ein und dann konfigurieren wir einen Transcription Request. Damit wir die Ergebnisse der Live-Transkription erhalten, müssen wir interimResults auf den Wert „true“ festlegen. Außerdem habe ich den Sprachcode auf de-DE eingestellt. Das lässt sich aber ganz einfach auf eine andere Sprachregion ändern.

const WebSocket = require("ws");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const wss = new WebSocket.Server({ server });

//Include Google Speech to Text
const speech = require("@google-cloud/speech");
const client = new speech.SpeechClient();

//Configure Transcription Request
const request = {
    config: {
        encoding: "MULAW",
        sampleRateHertz: 8000,
        languageCode: "de-DE"
    },
    interimResults: true
};

wss.on("connection", function connection(ws) {
    console.log("Neue Verbindung initiiert");

    ws.on("message", function incoming(message) {
        const msg = JSON.parse(message);
        switch (msg.event) {
            case "connected":
                console.log(`Neuer Anruf verbunden`);
                break;
            case "start":
                console.log(`Starte den Media-Stream ${msg.streamSid}`);
                break;
            case "media":
                console.log(`Empfange Audio`)
                break;
            case "stop":
                console.log(`Anruf beendet`);
                break;
        }
    });
});

//Handle HTTP Request
app.get("/", (_, res) => res.send("Hallo Welt"));

app.post("/", (req, res) => {
    res.set("Content-Type", "text/xml");
    res.send(`
      <Response>
        <Start>
         <Stream url="wss://${req.headers.host}/"/>
        </Start>
        <Say language="de-DE">Hallo. Ich werde die Tonspur für die nächsten 60 Sekunden an einen WebSocket weiterstreamen.</Say>
        <Pause length="60" />
      </Response>`);
});

console.log("Gestartet auf Port 8080");
server.listen(8080);

Jetzt erstellen wir einen neuen Stream, um Audiodaten vom Server an die Google-API zu senden. Wir nennen ihn recognizeStream und wir schreiben die Datenpakete von unserem Telefonanruf in diesen Stream. Wenn der Anruf beendet ist, rufen wir .destroy() auf, um den Stream zu beenden.

const WebSocket = require("ws");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const wss = new WebSocket.Server({ server });

//Include Google Speech to Text
const speech = require("@google-cloud/speech");
const client = new speech.SpeechClient();

//Configure Transcription Request
const request = {
    config: {
        encoding: "MULAW",
        sampleRateHertz: 8000,
        languageCode: "de-DE"
    },
    interimResults: true
};

wss.on("connection", function connection(ws) {
    console.log("Neue Verbindung initiiert");

    let recognizeStream = null;

    ws.on("message", function incoming(message) {
        const msg = JSON.parse(message);
        switch (msg.event) {
            case "connected":
                console.log(`Neuer Anruf verbunden`);

                // Create Stream to the Google Speech to Text API
                recognizeStream = client
                    .streamingRecognize(request)
                    .on("error", console.error)
                    .on("data", data => {
                        console.log(data.results[0].alternatives[0].transcript);
                    });
                break;
            case "start":
                console.log(`Starte den Media-Stream ${msg.streamSid}`);
                break;
            case "media":
                // Write Media Packets to the recognize stream
                recognizeStream.write(msg.media.payload);
                break;
            case "stop":
                console.log(`Anruf beendet`);
                recognizeStream.destroy();
                break;
        }
    });
});

//Handle HTTP Request
app.get("/", (_, res) => res.send("Hallo Welt"));

app.post("/", (req, res) => {
    res.set("Content-Type", "text/xml");
    res.send(`
      <Response>
        <Start>
         <Stream url="wss://${req.headers.host}/"/>
        </Start>
        <Say language="de-DE">Hallo. Ich werde die Tonspur für die nächsten 60 Sekunden an einen WebSocket weiterstreamen.</Say>
        <Pause length="60" />
      </Response>`);
});

console.log("Gestartet auf Port 8080");
server.listen(8080);

Dann starten wir den Server neu, rufen unsere Twilio-Telefonnummer an und beginnen zu sprechen. Im Terminal sollten wir jetzt Zwischenergebnisse der Transkription sehen.

Diktierter Text in der Konsole

 

Senden der Live-Transkription zum Browser

Ein Vorteil der Verwendung von WebSockets besteht darin, dass wir Nachrichten an andere Clients, einschließlich Browser, fließend übertragen können.

Nun passen wir den Code so an, dass die Zwischenergebnisse der Transskription an alle Clients übertragen werden. Außerdem ändern wir die GET-Route. Anstatt „Hello World“ senden wir eine HTML-Datei. Dazu benötigen wir auch das path-Paket, deshalb dürfen wir es nicht vergessen, dieses Paket einzubinden.

Wir ändern die index.js-Datei wie folgt.

const WebSocket = require("ws");
const express = require("express");
const app = express();
const server = require("http").createServer(app);
const wss = new WebSocket.Server({ server });
const path = require("path");

//Include Google Speech to Text
const speech = require("@google-cloud/speech");
const client = new speech.SpeechClient();

//Configure Transcription Request
const request = {
    config: {
        encoding: "MULAW",
        sampleRateHertz: 8000,
        languageCode: "de-DE"
    },
    interimResults: true
};

wss.on("connection", function connection(ws) {
    console.log("Neue Verbindung initiiert");

    let recognizeStream = null;

    ws.on("message", function incoming(message) {
        const msg = JSON.parse(message);
        switch (msg.event) {
            case "connected":
                console.log(`Neuer Anruf verbunden`);

                // Create Stream to the Google Speech to Text API
                recognizeStream = client
                    .streamingRecognize(request)
                    .on("error", console.error)
                    .on("data", data => {
                        console.log(data.results[0].alternatives[0].transcript);
                        wss.clients.forEach(client => {
                            if (client.readyState === WebSocket.OPEN) {
                                client.send(
                                    JSON.stringify({
                                        event: "interim-transcription",
                                        text: data.results[0].alternatives[0].transcript
                                    })
                                );
                            }
                        });
                    });
                break;
            case "start":
                console.log(`Starte den Media-Stream ${msg.streamSid}`);
                break;
            case "media":
                // Write Media Packets to the recognize stream
                recognizeStream.write(msg.media.payload);
                break;
            case "stop":
                console.log(`Anruf beendet`);
                recognizeStream.destroy();
                break;
        }
    });
});

//Handle HTTP Request
app.get("/", (req, res) => res.sendFile(path.join(__dirname, "/index.html")));

app.post("/", (req, res) => {
    res.set("Content-Type", "text/xml");
    res.send(`
      <Response>
        <Start>
         <Stream url="wss://${req.headers.host}/"/>
        </Start>
        <Say language="de-DE">Hallo. Ich werde die Tonspur für die nächsten 60 Sekunden an einen WebSocket weiterstreamen.</Say>
        <Pause length="60" />
      </Response>`);
});

console.log("Gestartet auf Port 8080");
server.listen(8080);

Jetzt können wir eine neue index.html-Datei erstellen um die Zwischentranskriptionen zu verarbeiten und im Browser anzuzeigen.

<!DOCTYPE html>
<html>
  <head>
    <title>Live-Transkribieren mit Twilio Media-Streams</title>
    <style>
      body {
        text-align: center;
      }
      img {
        height: 70px;
      }
      #transcription-container {
        font-size: 6em;
      }
    </style>
  </head>
  <body>
    <h1>Live-Transkribieren mit Twilio Media-Streams</h1>
    <img
       src="/content/dam/twilio-com/global/en/blog/legacy/2019/anrufe-live-transkribieren/Twilio_logo_D8D2oky.png" 
    />
    <h3>
      Rufe deine Twilio Nummer an und beginne zu sprechen. Deine Worte werden
      auf magische Weise hier auftauchen.
    </h3>
    <p id="transcription-container"></p>
    <script>
      document.addEventListener("DOMContentLoaded", (event) => {
        webSocket = new WebSocket("ws://localhost:8080");
        webSocket.onmessage = function (msg) {
          const data = JSON.parse(msg.data);
          if (data.event === "interim-transcription") {
            document.getElementById("transcription-container").innerHTML =
              data.text;
          }
        };
      });
    </script>
  </body>
</html>

Jetzt müssen wir den Server nur noch neu starten und laden http://localhost:8080 im Browser. Wenn wir unsere Twilio-Telefonnummer anrufen, wird das, was wir sagen, im Browser angezeigt.

Diktierter Text in Browser

 

Zusammenfassung

Gut gemacht! Wir haben gesehen, wie wir uns Twilio Media-Streams zunutze machen können, um unsere Sprachanwendungen zu erweitern. Da wir jetzt eine Live-Transkription vorliegen haben, können wir versuchen, den Text mit der Google Translation-API zu übersetzen, um eine Übersetzung App zu erstellen, oder eine Sentimentanalyse am Stream der Audiodaten durchzuführen, um die Emotionen hinter der Sprache zu ermitteln.

Wenn du noch Fragen oder Feedback hast oder mir einfach deine Ergebnisse zeigen willst, dann findest du mich unter: