Real-Time Sentiment Analysis on Messages
Tags: javascript, analytics, showdev
Published: 2021-04-09 15:34:03

Symbl.ai's API for sentiment analyzes messages in real-time, returning their polarity with a suggestion. If the polarity is below .5, the suggestion is negative. If above, the suggestion is positive. The suggestion, however, may be modified. If, for instance, you want to program a switch to toggle suggestions, the switch statement could return results for granulated polarities, since Symbl.ai's API for sentiment is fully programmable. 

In the following blog you create a web app with which to map sentiments directly to message IDs in real-time with Symbl.ai's sentiment analysis API over a WebSocket in JavaScript running locally on a Python server. The result is a table of message IDs with sentiments. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o56hhh691r8ld4y7ph85.png)

A table of message IDs with sentiments may not seem like a whole lot but for a fully programmable API, there can be nothing better than generic functionality. So it is important to disclaim at the outset that the web app wastes absolutely no time on a user interface beyond what is required to demonstrate the generic functionality of the API. 

## Create the Web APP

In the web app, create the following files: index.html with a source folder containing an index.js file together with a style.css file. 

In the style.css file, add the following lines:

```css
body {
  font-family: sans-serif;
}
```

In the index.html, add the following lines: 

```html
<!DOCTYPE html>
<html>

<head>
	<title>Parcel Sandbox</title>
	<meta charset="UTF-8" />

	<script src="src/index.js">
	</script>
</head>

<body>
	<div id="app"></div>

	<button type="button" onclick="openSocket()">Click Me!</button>

	<div id="table-parent">
	</div>
</body>

</html>
```

The index.html achieves two goals. It creates a button for triggering the `openSocket()` method. The second is a table. In the `openSocket()` you program the JavaScript WebSocket for Symbl.ai's WebSocket for its platform. In the table you program a log, logging the message ID with its polarity score. You do not return to these two files later. 

## Sentiment Analysis: WebSocket in JavaScript & Events

The first step to sentiment analysis on messages with Symbl.ai is to establish a WebSocket. The second step is make a call to the sentiment API as the WebSocket's handler for events handles events the speaker makes.  

To establish a WebSocket connection, the first step is to sign up for a free account at Symbl.ai. Register for an account at Symbl (i.e., https://platform.symbl.ai/). Grab both your appId and your appSecret. With both of those you authenticate either with a cURL command or with Postman so that you receive your x-api-key. Here is an example with cURL:

```bash
curl -k -X POST "https://api.symbl.ai/oauth2/token:generate" \
     -H "accept: application/json" \
     -H "Content-Type: application/json" \
     -d "{ \"type\": \"application\", \"appId\": \"<appId>\", \"appSecret\": \"<appSecret>\"}"
```

After signing you, you receive free credits with which to make API calls. To make API calls on a WebSocket connection with Symbl.ai, create four `const` in your `index.js` file: 
 
```javascript
 const accessToken = "";
  const uniqueMeetingId = btoa("devrelations@symbl.ai");
  const symblEndpoint = `wss://api.symbl.ai/v1/realtime/insights/${uniqueMeetingId}?access_token=${accessToken}`;
  const ws = new WebSocket(symblEndpoint);
```
The WebSocket connection requires both an unique meeting ID, as well as an `accessToken` (i.e., `x-api-key` you generate in `cURl` or Postman with a request containing an `appId` together an `appSecret`). 

After configuring the endpoint for a WebSocket with Symbl.ai, add the following methods for handling events, 1) `ws.onmessage`, 2) `ws.onerror`, 3) `ws.onclose`. 

```javascript
// Fired when a message is received from the WebSocket server
ws.onmessage = (event) => {
  console.log(event);
};
// Fired when the WebSocket closes unexpectedly due to an error or lost connetion
ws.onerror  = (err) => {
  console.error(err);
};
// Fired when the WebSocket connection has been closed
ws.onclose = (event) => {
  console.info('Connection to websocket closed');
};
```

After these methods are created, create a method called `onopen` in the following way: 

```javascript
// Fired when the connection succeeds.
ws.onopen = (event) => {
  ws.send(JSON.stringify({
    type: 'start_request',
    meetingTitle: 'Establish a WebSocket Connection', // Conversation name
    insightTypes: ['question', 'action_item'], // Will enable insight generation
    config: {
      confidenceThreshold: 0.5,
      languageCode: 'en-US',
      speechRecognition: {
        encoding: 'LINEAR16',
        sampleRateHertz: 44100,
      }
    },
    speaker: {
      userId: 'devrelations@symbl.ai',
      name: 'Developer Relations',
    }
  }));
```

The `onopen` method contains many parts, the most important of which is `speechRecognition` where `encoding` is set to `LINEAR16` while the `hertz` is set to `44100`. To read more about best practices for streaming audio integrations, checkout the following blog: https://symbl.ai/best-practices-for-audio-integrations-with-symbl/. 

The last but not least step is to configure the WebSocket to access the client's device (i.e., microphone).

```javascript
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
const handleSuccess = (stream) => {
  const AudioContext = window.AudioContext;
  const context = new AudioContext();
  const source = context.createMediaStreamSource(stream);
  const processor = context.createScriptProcessor(1024, 1, 1);
  const gainNode = context.createGain();
  source.connect(gainNode);
  gainNode.connect(processor);
  processor.connect(context.destination);
  processor.onaudioprocess = (e) => {
    // convert to 16-bit payload
    const inputData = e.inputBuffer.getChannelData(0) || new Float32Array(this.bufferSize);
    const targetBuffer = new Int16Array(inputData.length);
    for (let index = inputData.length; index > 0; index--) {
        targetBuffer[index] = 32767 * Math.min(1, inputData[index]);
    }
    // Send to websocket
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(targetBuffer.buffer);
    }
  };
};
handleSuccess(stream);
```
A detailed examination of a WebSocket's access to client's device is beyond the scope of the current blog, since our focus is on real-time sentiment analysis on messages.  

The following is the full code for establishing the WebSocket connect: 

```javascript
const uniqueMeetingId = btoa('email@address.com');
const accessToken = '';
const symblEndpoint = `wss://api.symbl.ai/v1/realtime/insights/${uniqueMeetingId}?access_token=${accessToken}`;
const ws = new WebSocket(symblEndpoint);
// Fired when a message is received from the WebSocket server
ws.onmessage = (event) => {
  console.log(event);
};
// Fired when the WebSocket closes unexpectedly due to an error or lost connetion
ws.onerror  = (err) => {
  console.error(err);
};
// Fired when the WebSocket connection has been closed
ws.onclose = (event) => {
  console.info('Connection to websocket closed');
};
// Fired when the connection succeeds.
ws.onopen = (event) => {
  ws.send(JSON.stringify({
    type: 'start_request',
    meetingTitle: 'Websockets How-to', // Conversation name
    insightTypes: ['question', 'action_item'], // Will enable insight generation
    config: {
      confidenceThreshold: 0.5,
      languageCode: 'en-US',
      speechRecognition: {
        encoding: 'LINEAR16',
        sampleRateHertz: 44100,
      }
    },
    speaker: {
      userId: 'example@symbl.ai',
      name: 'Example Sample',
    }
  }));
};
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
const handleSuccess = (stream) => {
  const AudioContext = window.AudioContext;
  const context = new AudioContext();
  const source = context.createMediaStreamSource(stream);
  const processor = context.createScriptProcessor(1024, 1, 1);
  const gainNode = context.createGain();
  source.connect(gainNode);
  gainNode.connect(processor);
  processor.connect(context.destination);
  processor.onaudioprocess = (e) => {
    // convert to 16-bit payload
    const inputData = e.inputBuffer.getChannelData(0) || new Float32Array(this.bufferSize);
    const targetBuffer = new Int16Array(inputData.length);
    for (let index = inputData.length; index > 0; index--) {
        targetBuffer[index] = 32767 * Math.min(1, inputData[index]);
    }
    // Send to websocket
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(targetBuffer.buffer);
    }
  };
};
handleSuccess(stream);
```

Run the code directly in your browser's console without any reference to the web app. If you were able to log messages in the console, you successfully established a WebSocket connection. The next step is to configure `onmessage` to log the polarity score on those messages for realtime sentiment analysis. 

## Symbl.ai's Real-Time Sentiment Analysis API

The next step is to configure `onmessage` to log the polarity score on those messages for realtime sentiment analysis. The first step to analyze sentiments is to log the message IDs. You reconfigure `onmessage` to log message IDs.

### Logging Message IDs 

Our objective now is to make a call to the following API endpoint: 

```bash
https://api.symbl.ai/v1/conversations/${conversationId}/messages?sentiment=true
```

You note that passing `?sentiment=true` into the API is the query parameter for a request to return messages with values for polarities on message IDs. Since the API endpoint requires no more than a `${conversationId}`, the first step is to assign the `conversationId` to a constant. 

```JavaScript
  // You can find the conversationId in event.message.data.conversationId;
  const data = JSON.parse(event.data);
  if (data.type === 'message' && data.message.hasOwnProperty('data')) {
    console.log('conversationId', data.message.data.conversationId);
    const conversationId = data.message.data.conversationId;
console.log('onmessage event', event);
    // You can log sentiments on messages from data.message.data.conversationId 
```

With the `conversationId` the next step is to configure an HTTP request to make a call to the API for sentiment analysis every time that the WebSocket logs an event. To configure an HTTP request to make a call to the API, make the call by configuring the headers as well as the authorization. 

```JavaScript
   const request = new XMLHttpRequest();
    request.responseType = "text";
    const sentimentEndpoint = `https://api.symbl.ai/v1/conversations/${conversationId}/messages?sentiment=true`;
    request.open("GET", sentimentEndpoint)
    request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
    request.setRequestHeader('Content-Type', 'application/json');
    request.onreadystatechange=(e)=> {
      console.log(request.responseText)
    }
    request.send()
  }
```

With the request configured, the API endpoint makes a call every time the WebSocket handles an event that a speaker triggers. If you want, run the code in your console. It logs polarity values for message IDs. However, these logs do not map one to the other. The following is the full code for establishing the WebSocket connect: 

```JavaScript
// Fired when a message is received from the WebSocket server
ws.onmessage = (event) => {
  // You can find the conversationId in event.message.data.conversationId;
  const data = JSON.parse(event.data);
  if (data.type === 'message' && data.message.hasOwnProperty('data')) {
    console.log('conversationId', data.message.data.conversationId);
    const conversationId = data.message.data.conversationId;
    console.log('onmessage event', event);
    // You can log sentiments on messages from data.message.data.conversationId 
    const request = new XMLHttpRequest();
    request.responseType = "text";
    const sentimentEndpoint = `https://api.symbl.ai/v1/conversations/${conversationId}/messages?sentiment=true`;
    request.open("GET", sentimentEndpoint)
    request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
    request.setRequestHeader('Content-Type', 'application/json');
    request.onreadystatechange=(e)=> {
      console.log(request.responseText)
    }
    request.send()
  }
};
```

##Refactoring the Code 

It is time to refactor our code to conform to the web app built earlier, as well as log sentiments on messages. In the web app an element called `table-parent` is identified as `<div id="table-parent">`. The call to the API endpoint feeds both the message IDs together with the scores for polarity directly into the `table-parent` in real-time. 

####Refactoring the API call

Refactor the API call in the following way: 

```javascript
if (conversationId) {
        // You can log sentiments on messages from data.message.data.conversationId
        const sentimentEndpoint = `https://api.symbl.ai/v1/conversations/${conversationId}/messages?sentiment=true`;
        const response = await fetch(sentimentEndpoint, {
          method: 'GET',
          mode: 'cors',
          cache: 'no-cache',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${accessToken}`
          }
        });
```

####Configuring the Table

```javascript
const resp = await response.json();
        if (response.ok) {
          let rows = "";
          for (let message of resp.messages) {
            if (cacheTable.indexOf(message.id) === -1) {
              console.log('Polarity: ', message.sentiment.polarity.score);
            }
            rows += `
              <tr>
                <td>${message.id}</td>
                <td>${message.sentiment.polarity.score}</td>
              </tr>
            `
            cacheTable.push(message.id);
          }
          let tableHtml = `
            <table>
              <thead>
                <tr>
                  <th>ID</th>
                  <th>Polarity</th>
                </tr>
              </thead>
              <tbody>
              ${rows}
              </tbody>
            </table>
          `;
          debugger;
          document.querySelector("#table-parent").innerHTML = tableHtml;
        }
      }
    }
```
The `parent-table` updates when a user speaks or listens. 

### Refactored Code 

The following is the fully refactored code for mapping polarity values to message IDs in a table over a WebSocket connection in JavaScript with Symbl.ai's real-time sentiment analysis API: 

```javascript
ws.onmessage = async (event) => {
    // You can find the conversationId in event.message.data.conversationId;
    const data = JSON.parse(event.data);
    if (data.type === 'message' && data.message.hasOwnProperty('data')) {
      console.log('conversationId', data.message.data.conversationId);
      conversationId = data.message.data.conversationId;
      console.log('onmessage event', event);
    }
    if (data.type === 'message_response') {
      for (let message of data.messages) {
        console.log('Transcript (more accurate): ', message.payload.content);
      }
      if (conversationId) {
        // You can log sentiments on messages from data.message.data.conversationId
        const sentimentEndpoint = `https://api.symbl.ai/v1/conversations/${conversationId}/messages?sentiment=true`;
        const response = await fetch(sentimentEndpoint, {
          method: 'GET',
          mode: 'cors',
          cache: 'no-cache',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${accessToken}`
          }
        });
        const resp = await response.json();
        if (response.ok) {
          let rows = "";
          for (let message of resp.messages) {
            if (cacheTable.indexOf(message.id) === -1) {
              console.log('Polarity: ', message.sentiment.polarity.score);
            }
            rows += `
              <tr>
                <td>${message.id}</td>
                <td>${message.sentiment.polarity.score}</td>
              </tr>
            `
            cacheTable.push(message.id);
          }
          let tableHtml = `
            <table>
              <thead>
                <tr>
                  <th>ID</th>
                  <th>Polarity</th>
                </tr>
              </thead>
              <tbody>
              ${rows}
              </tbody>
            </table>
          `;
          debugger;
          document.querySelector("#table-parent").innerHTML = tableHtml;
        }
      }
    }
    if (data.type === 'topic_response') {
      for (let topic of data.topics) {
        console.log('Topic detected: ', topic.phrases)
      }
    }
    if (data.type === 'insight_response') {
      for (let insight of data.insights) {
        console.log('Insight detected: ', insight.payload.content);
      }
    }
    if (data.type === 'message' && data.message.hasOwnProperty('punctuated')) {
      console.log('Live transcript: ', data.message.punctuated.transcript);
    }
    // console.log(`Response type: ${data.type}. Object: `, data);
  };
```
To run the code locally, you have to avoid CORS. At the same time, you need to create an `HTTP` server in Python. Run the following line of code: 

```python3 -m http.server 8000```

Python3 enables an `http` server to run locally on your host. With the application running, hit the `click` button. After hitting the `click` button, you should see message IDs mapped to values for polarity in real-time. If you run Command + Option + J, the following logs appear in your console. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i7botl5w34udv9rzmeb6.png)

##API's Rigor with Symmetrical Augmentations
Symbl.ai's API is rigorous. It provides for sentiments in a way that other APIs do not. Symbl.ai's sentiment analysis, for instance, provides symmetrical augmentations for adverbial enhancements. If, for instance, you check the sentiment for "It is good", the score is .8. If you check the sentiment for "It is really good", the score is .9. What is true for positivity is true for negativity.

##Conclusion
If you were able to successfully integrate Symbl’s API directly into JavaScript’s own software for enabling real-time conversations so that your transcribed a conversation live from the browser, congratulations!
If you look closely at the data, the conversationId may be applied to new API calls to access AI insights for action items, topics, etc… You can hit these API end points with cURL commands, Postman, or take a look at the section on Further Developer down below for ideas.


### Community 
Stuck? Free feel to ask us any questions on our Slack Channel or send us an email at devrelations@symbl.ai