Build a live comment feature with sentiment analysis using Flask and Vue
You will need Python 3.6+ and Flask installed on your machine.
In this tutorial, we’ll see how we can get the overall feeling of our users after they might have read our post and added their comments. We’ll build a simple blog where users can comment. Then we process the comment to determine the percentages of people that find the post interesting and those who don’t.
As technologies are advancing, the way we process data is also taking a huge turn around. Taking advantage of natural language processing, we can determine from a group of comments, how our users feel about our blog post.
We also don’t have to reload a page to see a new comment from a blog post. We can make comments visible in realtime to every user.
We’ll be using Channels, Vue.js and Flask to build the app.
Here is a preview of what the final app will look like:
Prerequisite
This tutorial uses the following:
You should have some familiarity with Python development to follow along with this tutorial. If you are not familiar with Vue but still want to follow along, you can go through the basics of Vue in the documentation to get you up to speed in a couple of minutes.
Before we start, let’s get your environment ready. Check that you have the appropriate installation and setup on your machine.
Open up a terminal on your machine and execute the below code:
$ python --version
If you have a Python 3.6+ installed on your machine, you will have a similar text printed out as python 3.6.0
. If you got an output similar to “Command not found”, you need to install Python on your machine. Head over to Python’s official website to download and get it installed.
If you have gotten all that installed, let’s proceed.
Creating a Pusher account
We’ll use Pusher Channels to handle all realtime functionalities. Before we can start using Pusher Channels, we need to get our API key. We need an account to be able to get the API key.
Head over to Pusher and log in to your account or create a new account if you don’t have one already. Once you are logged in, create a new app and then copy the app API keys.
Setting up the backend app
Let’s create our backend app that will be responsible for handling all communication to Pusher Channels and getting the sentiment of a comment.
Create the following files and folder in a folder named live-comment-sentiment
in any convenient location on your system:
live-comment-sentiment
├── .env
├── .flaskenv
├── app.py
├── requirements.txt
├── static
│ ├── custom.js
│ └── style.css
└── templates
└── index.html
└── base.html
Creating a virtual environment
It’s a good idea to have an isolated environment when working with Python. virtualenv is a tool to create an isolated Python environment. It creates a folder which contains all the necessary executables to use the packages that a Python project would need.
From your command line, change your directory to the Flask project root folder, execute the below command:
$ python3 -m venv env
Or:
$ python -m venv env
The command to use depends on which associates with your Python 3 installation.
Then, activate the virtual environment:
$ source env/bin/activate
If you are using Windows, activate the virtualenv with the below command:
> \path\to\env\Scripts\activate
This is meant to be a full path to the activate script. Replace \path\to
with your correct path name.
Next, add the Flask configuration setting to the .flaskenv
file:
FLASK_APP=app.py
FLASK_ENV=development
This will instruct Flask to use app.py
as the main entry file and start up the project in development mode.
Now, add your Pusher API keys to the .env
file:
PUSHER_APP_ID=app_id
PUSHER_APP_KEY=key
PUSHER_APP_SECRET=secret
PUSHER_APP_CLUSTER=cluster
Make sure to replace app_id
, key
, secret
and cluster
with your own Pusher keys which you have noted down earlier.
Next, create a Flask instance by adding the below code to app.py
:
# app.py
from flask import Flask, jsonify, render_template, request
from textblob import TextBlob
import pusher
import os
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
# run Flask app
if __name__ == "__main__":
app.run()
In the code above, after we instantiate Flask using app = Flask(__name__)
, we created a new route - /
which renders an index.html
file from the templates folder.
Now, add the following python packages to the requirements.txt
file:
Flask==1.0.2
python-dotenv==0.8.2
pusher==2.0.1
textblob==0.15.1
The packages we added:
- python-dotenv: this library will be used by Flask to load environment configurations files.
- pusher: this is the Pusher Python library that makes it easy to interact with its API.
- textblob: a Python library which provides a simple API for common natural language processing (NLP).
Next, install the library by executing the below command:
$ pip install -r requirements.txt
Once the packages are done installing, start up Flask:
$ flask run
If there is no error, our Flask app will now be available on port 5000. If you visit http://localhost:5000, you will see a blank page. This is because the templates/index.html
file is empty, which is ok for now.
Setting up TextBlob
To get the sentiment from comments, we’ll use the TextBlob Python library which provides a simple API for common natural language processing (NLP). We already have the library installed. What we’ll do now is install the necessary data that TextBlob will need.
From your terminal, make sure you are in the project root folder. Also, make sure your virtualenv is activated. Then execute the below function.
# Download NLTK corpora
$ python -m textblob.download_corpora lite
This will download the necessary NLTK corpora (trained models).
Initialize the Pusher Python library
Initialize the Pusher Python library by adding the below code to app.py
just after the app = Flask(__name__)
line:
# app.py
pusher = pusher.Pusher(
app_id=os.getenv('PUSHER_APP_ID'),
key=os.getenv('PUSHER_APP_KEY'),
secret=os.getenv('PUSHER_APP_SECRET'),
cluster=os.getenv('PUSHER_APP_CLUSTER'),
ssl=True)
Now we are fully set.
Setting up the frontend
We’ll create a simple page for adding comments. Since we won’t be building a full blog website, we won’t be saving the comments to a database.
Adding the base layout
We’ll use the template inheritance approach to build our views, which makes it possible to reuse the layouts instead of repeating some markup across pages.
Add the following markup to the templates/base.html
file:
<!-- /templates/base.html -->
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<title>Live comment</title>
</head>
<body>
<div class="container" id="app">
{% block content %} {% endblock %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://js.pusher.com/4.1/pusher.min.js"></script>
<script src="{{ url_for('static', filename='custom.js')}}"></script>
</body>
</html>
This is the base layout for our view. All other views will inherit from the base file.
In this file, we have added some libraries. This includes:
- Bootstrap
- Pusher JavaScript library
- Vue.js
The blog page
This will serve as the landing page of the application. Add the following to the templates/index.html
file:
<!-- /templates/index.html -->
{% extends 'base.html' %}
{% block content %}
<div class="grid-container">
<header class="header text-center">
<img src="https://cdn1.imggmi.com/uploads/2018/10/13/1d5cff977fd6e3aac498e581ef681a1a-full.png">
</header>
<main class="content">
<div class="content-text">
Our pioneering and unique technology is based on state-of-the-art <br/>
machine learning and computer vision techniques. Combining deep neural <br/>
networks and spectral graph theory with the computing... <br/>
</div>
</main>
<section class="mood">
<div class="row">
<div class="col text-center">
<div class="mood-percentage">[[ happy ]]%</div>
<div>Happy</div>
</div>
<div class="col text-center">
<div class="mood-percentage">[[ neutral ]]%</div>
<div>Neutral</div>
</div>
<div class="col text-center">
<div class="mood-percentage">[[ sad ]]%</div>
<div>Sad</div>
</div>
</div>
</section>
<section class="comment-section">
<div v-for="comment in comments">
<comment
:comment="comment"
v-bind:key="comment.id"
>
</comment>
</div>
</section>
<section class="form-section">
<form class="form" @submit.prevent="addComment">
<div class="form-group">
<input
type="text"
class="form-control"
v-model="username"
placeholder="Enter username">
</div>
<div class="form-group">
<textarea
class="form-control"
v-model="comment"
rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-block">Add comment</button>
</form>
</section>
</div>
{% endblock %}
In the preceding code:
-
In the
<section class="mood">… </section>
, we added three placeholders - [[ happy ]], [[ neutral ]] and [[ sad ]], which is the percentages of the moods of users who added comments. These placeholders will be replaced by their actual values when Vue takes over the page DOM (mounted).Notice we are using
[[ ]]
instead of the normal Vue placeholders -{{ }}
. This is because we are using Jinja2 template that comes bundled with Flask to render our page. The Jinja2 uses{{ }}
placeholder to hold variables that will be substituted to their real values and so do Vue by default. So to avoid conflicts, we will change Vue to use[[ ]]
instead. -
In the
<section class="comment-section">
section, we are rendering the comments to the page. -
Next, is the
<section class="form-section">… </section>
, where we added a form for adding new comments. Also in the inputs fields, we declare a two-way data binding using the v-model directive. -
In the form section -
<form class="form" @submit.prevent="addComment">…
, notice that we have the@submit.prevent
directive. This will prevent the form from submitting normally when the user adds a new comment. Then we call theaddComment
function to add a comment. We don’t have theaddComment
function declared anywhere yet. We’ll do this when we initialize Vue.
Now, add some styles to the page. Add the below styles to the static/style.css
file:
body {
width: 100%;
height: 100%;
}
.grid-container {
display: grid;
grid-template-rows: 250px auto auto 1fr;
grid-template-columns: repeat(3, 1fr);
grid-gap: 20px;
grid-template-areas:
'. header .'
'content content content'
'mood mood mood'
'comment-section comment-section comment-section'
'form-section form-section form-section';
}
.content {
grid-area: content;
}
.comment-section {
grid-area: comment-section;
}
.content-text {
font-style: oblique;
font-size: 27px;
}
.mood {
grid-area: mood;
}
.header {
grid-area: header;
}
.form-section {
grid-area: form-section;
}
.comment {
border: 1px solid rgb(240, 237, 237);
border-radius: 4px;
margin: 15px 0px 5px 60px;
font-family: monospace;
}
.comment-text {
padding-top: 10px;
font-size: 17px;
}
.form {
margin-top: 50px;
}
.mood-percentage {
border: 1px solid gray;
min-height: 50px;
padding-top: 10px;
font-size: 30px;
font-weight: bolder;
}
Now we have all our user interface ready. If you visit the app URL again, you will see a similar page as below:
Initializing Channels
Now let’s initialize Channels. Since we have added the Pusher JavaScript library already, we’ll go ahead and initialize it.
Add the below code to the static/custom.js
file:
// Initiatilze Pusher JavaScript library
var pusher = new Pusher('<PUSHER-APP-KEY>', {
cluster: '<CLUSTER>',
forceTLS: true
});
Replace <PUSHER-APP-KEY>
and <CLUSTER>
with your correct Pusher app details you noted down earlier.
Creating the comment component
If you view the /templates/index.html
file, in the <section class="comment-section">
section, you will notice we are calling the <comment>
component which we have not created yet. We need to create this component. Also, notice inside the file, we are calling the v-for (v-for="comment in comments"
) directive to render the comments.
Let’s create the component. Add the below code to static/custom.js
:
Vue.component('comment', {
props: ['comment'],
template: `
<div class="row comment">
<div class="col-md-2">
<img
src="https://cdn1.imggmi.com/uploads/2018/10/13/1d5cff977fd6e3aac498e581ef681a1a-full.png"
class="img-responsive"
width="90"
height="90"
>
</div>
<div class="col-md-10 comment-text text-left" v-html="comment.comment"> </div>
</div>
`
})
Initialize Vue
Now let’s initialize Vue to take over the DOM manipulation.
Add the below code to the static/custom.js
file:
var app = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
username: '',
comment: '',
comments: [],
happy: 0,
sad: 0,
neutral: 0,
socket_id: ""
},
methods: {},
created () {},
})
In the preceding code:
- We initialize Vue using
var app = new Vue(…
passing to it a key-value object. - Next, we tell Vue the part on the page to watch using
el:
'``#app'
. The#app
is the ID we have declared in the/templates/base.html
. - Next, using
delimiters: ['[[', ']]'],
, we change the default Vue delimiter from{{ }}
to[[ ]]
so that it does not interfere with that of Jinja2. - Then we defined some states using
data: {….
. - Finally, we have
methods: {},
andcreated () {},
. We’ll add all the function we’ll declare inside themethods: {}
block and then thecreated () {}
is for adding code that will execute once Vue instance is created.
Next, add a function to update the sentiment score. Add the below code to the methods: {}
block of the static/custom.js
file:
updateSentiments () {
// Initialize the mood to 0
let [happy, neutral, sad] = [0, 0, 0];
// loop through all comments, then get the total of each mood
for (comment of this.comments) {
if (comment.sentiment > 0.4) {
happy++;
} else if (comment.sentiment < 0) {
sad++;
} else {
neutral++;
}
}
const total_comments = this.comments.length;
// Get the percentage of each mood
this.sad = ((sad/total_comments) * 100).toFixed();
this.happy = ((happy/total_comments) * 100).toFixed();
this.neutral = ((neutral/total_comments) * 100).toFixed()
// Return an object of the mood values
return {happy, neutral, sad}
},
In the code above, we created a function that will loop through all the comments to get the number of each mood that appeared. Then we get the percentage of each mood then return their corresponding values.
Next, add a function to add a new comment. Add the below code to the methods: {} block right after the code you added above:
addComment () {
fetch("/add_comment", {
method: "post",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: this.comments.length,
username: this.username,
comment: this.comment,
socket_id: this.socket_id
})
})
.then( response => response.json() )
.then( data => {
// Add the new comment to the comments state data
this.comments.push({
id: data.id,
username: data.username,
comment: data.comment,
sentiment: data.sentiment
})
// Update the sentiment score
this.updateSentiments();
})
this.username = "";
this.comment = "";
},
Here, we created a function that makes a request to the /add_comment
route to get the sentiment of a comment. Once we receive a response, we add the comment to the comments state. Then we call this.updateSentiments()
to update the sentiment percentage. This function will be called each time a user wants to add a new comment.
Next, let’s make comments visible to others in realtime. Add the below code to the created () {}
block in the static/custom.js:
// Set the socket ID
pusher.connection.bind('connected', () => {
this.socket_id = pusher.connection.socket_id;
});
// Subscribe to the live-comments channel
var channel = pusher.subscribe('live-comments');
// Bind the subscribed channel (live-comments) to the new-comment event
channel.bind('new-comment', (data) => {
this.comments.push(data);
// Update the sentiment score
this.updateSentiments();
});
Get sentiments from comments and make comments realtime
Now, let’s add a function to get the sentiment of a message and then trigger a new-comment
event whenever a user adds a comment. Add the below code to app.py
# ./api/app.py
@app.route('/add_comment', methods=["POST"])
def add_comment():
# Extract the request data
request_data = request.get_json()
id = request_data.get('id', '')
username = request_data.get('username', '')
comment = request_data.get('comment', '')
socket_id = request_data.get('socket_id', '')
# Get the sentiment of a comment
text = TextBlob(comment)
sentiment = text.polarity
comment_data = {
"id": id,
"username": username,
"comment": comment,
"sentiment": sentiment,
}
# Trigger an event to Pusher
pusher.trigger(
"live-comments", 'new-comment', comment_data, socket_id
)
return jsonify(comment_data)
The sentiment property returns a tuple of the form (polarity, subjectivity) where polarity ranges from -1.0 to 1.0 and subjectivity ranges from 0.0 to 1.0. We will only use the polarity property.
In the pusher.trigger(…
, method, we are passing the socket_id
so that the user triggering the event won’t get back the data sent.
Testing the app
Congrats! Now we have our live comments with sentiments. To test the app, open the app in your browser on two or more different tabs, then add comments and see them appear in realtime on other tabs.
Here is some sample comment you can try out:
- The post is terrible! - Sad (Negative)
- I love the way this is going - Happy (Positive)
- This is amazingly simple to use. What great fun! - Happy (Positive)
If you are getting an error or nothing is working. Stop the server (Press CTRL+C) and then restart it ($ flask run
).
Conclusion
In this tutorial, we built a live comment with sentiment analysis. We used Vue for DOM manipulation, Flask for the server side and Channels for realtime functionality. We used the TextBlob python library to detect mood from text.
19 November 2018
by Gideon Onwuka