Add multilingual sentiment node

(but not publish - yet)
This commit is contained in:
Dave Conway-Jones 2018-07-19 12:39:07 +01:00
parent 93584d8071
commit 50fc196c6f
No known key found for this signature in database
GPG Key ID: 9E7F9C73F5168CD4
7 changed files with 480 additions and 0 deletions

View File

@ -0,0 +1,175 @@
<script type="text/x-red" data-template-name="sentiment">
<div class="form-row">
<label for="node-input-lang"><i class="fa fa-language"></i> <span data-i18n="sentiment.label.language"></span></label>
<select type="text" id="node-input-lang" style="width:70%;">
<option value="">set by msg.lang</option>
<option value="af">Afrikaans</option>
<option value="sq">Albanian</option>
<option value="am">Amharic</option>
<option value="ar">Arabic</option>
<option value="hy">Armenian</option>
<option value="az">Azerbaijani</option>
<option value="bn">Bangla</option>
<option value="eu">Basque</option>
<option value="be">Belarusian</option>
<option value="bs">Bosnian</option>
<option value="bg">Bulgarian</option>
<option value="my">Burmese</option>
<option value="ca">Catalan</option>
<option value="ceb">Cebuano</option>
<option value="zh">Chinese</option>
<option value="zh-tw">Chinese (Taiwanese)</option>
<option value="co">Corsican</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl">Dutch</option>
<option value="en">English</option>
<option value="eo">Esperanto</option>
<option value="et">Estonian</option>
<option value="fi">Finnish</option>
<option value="fr">French</option>
<option value="fy">Frisian</option>
<option value="gl">Galician</option>
<option value="ka">Georgian</option>
<option value="de">German</option>
<option value="el">Greek</option>
<option value="gu">Gujarati</option>
<option value="ht">Haitian Creole</option>
<option value="ha">Hausa</option>
<option value="haw">Hawaiian</option>
<option value="hi">Hindi</option>
<option value="hmn">Hmong</option>
<option value="hu">Hungarian</option>
<option value="is">Icelandic</option>
<option value="ig">Igbo</option>
<option value="id">Indonesian</option>
<option value="ga">Irish</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="kn">Kannada</option>
<option value="kk">Kazakh</option>
<option value="km">Khmer</option>
<option value="ko">Korean</option>
<option value="ku">Kurdish</option>
<option value="ky">Kyrgyz</option>
<option value="lo">Lao</option>
<option value="la">Latin</option>
<option value="lv">Latvian</option>
<option value="lt">Lithuanian</option>
<option value="lb">Luxembourgish</option>
<option value="mk">Macedonian</option>
<option value="mg">Malagasy</option>
<option value="ms">Malay</option>
<option value="ml">Malayalam</option>
<option value="mt">Maltese</option>
<option value="mi">Maori</option>
<option value="mr">Marathi</option>
<option value="mn">Mongolian</option>
<option value="ne">Nepali</option>
<option value="no">Norwegian</option>
<option value="ny">Nyanja</option>
<option value="ps">Pashto</option>
<option value="fa">Persian</option>
<option value="pl">Polish</option>
<option value="pt">Portuguese</option>
<option value="pa">Punjabi</option>
<option value="ro">Romanian</option>
<option value="ru">Russian</option>
<option value="sm">Samoan</option>
<option value="gd">Scottish Gaelic</option>
<option value="sr">Serbian</option>
<option value="sn">Shona</option>
<option value="sd">Sindhi</option>
<option value="si">Sinhala</option>
<option value="sk">Slovak</option>
<option value="sl">Slovenian</option>
<option value="so">Somali</option>
<option value="st">Southern Sotho</option>
<option value="es">Spanish</option>
<option value="su">Sundanese</option>
<option value="sw">Swahili</option>
<option value="sv">Swedish</option>
<option value="tl">Tagalog</option>
<option value="tg">Tajik</option>
<option value="ta">Tamil</option>
<option value="te">Telugu</option>
<option value="th">Thai</option>
<option value="tr">Turkish</option>
<option value="uk">Ukrainian</option>
<option value="ur">Urdu</option>
<option value="uz">Uzbek</option>
<option value="vi">Vietnamese</option>
<option value="cy">Welsh</option>
<option value="xh">Xhosa</option>
<option value="yi">Yiddish</option>
<option value="yo">Yoruba</option>
<option value="zu">Zulu</option>
</select>
</div>
<div class="form-row">
<label for="node-input-property"><i class="fa fa-ellipsis-h"></i> <span data-i18n="node-red:common.label.property"></span></label>
<input type="text" id="node-input-property" style="width:70%;"/>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
</div>
</script>
<script type="text/x-red" data-help-name="sentiment">
<p>Analyses the chosen property, default <code>payload</code>, and adds a <code>sentiment</code> object.</p>
<h3>Outputs</h3>
<dl class="message-properties">
<dt>sentiment <span class="property-type">object</span></dt>
<dd>contains the resulting AFINN-111 sentiment.</dd>
<dt>sentiment.score <span class="property-type">number</span></dt>
<dd>the sentiment score.</dd>
</dl>
<h3>Inputs</h3>
<dl class="message-properties">
<dt>words <span class="property-type">object</span></dt>
<dd>an object of words and scores to override or add words can be supplied - <code>{ word:score,... }</code>.</dd>
</dl>
<dl class="message-properties">
<dt>lang <span class="property-type">string</span></dt>
<dd>Two letter cldr code to specify the language to use - from the list below</dd>
</dl>
<h3>Details</h3>
<p>A score greater than zero is positive and less than zero is negative.</p>
<p>The score typically ranges from -5 to +5, but can go higher and lower.</p>
<p>See <a href="https://github.com/marcellobarile/multilang-sentiment/blob/develop/README.md" target="_blank">the Sentiment docs here</a>.</p>
<p>The node can also be configured to let the language be specified by setting <code>msg.lang</code> to one of the codes below</p>
<p>The language codes supported are - af, am, ar, az, be, bg, bn, bs, ca, ceb, co, cs, cy, da, de, el, en, eo, es, et, eu, fa, fi,
fr, fy, ga, gd, gl, gu, ha, haw, hi, hmn, hr, ht, hu, hy, id, ig, is, it, iw, ja, jw, ka, kk, km, kn, ko, ku, ky, la, lb, lo, lt,
lv, mg, mi, mk, ml, mn, mr, ms, mt, my, ne, nl, no, ny, pa, pl, ps, pt, ro, ru, sd, si, sk, sl, sm, sn, so, sq, sr, st, su, sv,
sw, ta, te, tg, th, tl, tr, uk, ur, uz, vi, xh, yi, yo, zh, zh-tw, zu</p>
</script>
<script type="text/javascript">
RED.nodes.registerType('sentiment',{
category: 'analysis-function',
color:"#E6E0F8",
defaults: {
name: {value:""},
property: {value:"payload",required:true},
lang: {value:"en"}
},
inputs:1,
outputs:1,
icon: "arrow-in.png",
label: function() {
return this.name||"sentiment";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
if (this.property === undefined) {
$("#node-input-property").val("payload");
}
$("#node-input-property").typedInput({default:'msg',types:['msg']});
}
});
</script>

View File

@ -0,0 +1,28 @@
module.exports = function(RED) {
"use strict";
var sentiment = require('multilang-sentiment');
function SentimentNode(n) {
RED.nodes.createNode(this,n);
this.lang = n.lang;
this.property = n.property||"payload";
var node = this;
this.on("input", function(msg) {
var value = RED.util.getMessageProperty(msg,node.property);
if (value !== undefined) {
if (msg.hasOwnProperty("overrides")) {
msg.extras = msg.overrides;
delete msg.overrides;
}
sentiment(value, node.lang || msg.lang || 'en', {words: msg.extras || null}, function (err, result) {
msg.sentiment = result;
node.send(msg);
});
}
else { node.send(msg); } // If no matching property - just pass it on.
});
}
RED.nodes.registerType("sentiment",SentimentNode);
}

View File

@ -0,0 +1,14 @@
Copyright 2016, 2018 JS Foundation and other contributors, https://js.foundation/
Copyright 2013-2016 IBM Corp.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,31 @@
node-red-node-sentiment
========================
A <a href="http://nodered.org" target="new">Node-RED</a> node that scores incoming words
using the AFINN-165 wordlist and attaches a sentiment.score property to the msg.
Install
-------
This is a node that should be installed by default by Node-RED so you should not have to install it manually. If you do then run the following command in your Node-RED user directory - typically `~/.node-red`
npm install node-red-node-sentiment
Usage
-----
Uses the AFINN-165 wordlist to attempt to assign scores to words in text.
Attaches `msg.sentiment` to the msg and within that `msg.sentiment.score` holds the score.
Supports multiple languages. These can be preselected in the node configuration. You can also set it so that `msg.lang` can be used to set the language dynamically if required. The cldr language codes supported are:
af, am, ar, az, be, bg, bn, bs, ca, ceb, co, cs, cy, da, de, el, en, eo, es, et, eu, fa, fi,
fr, fy, ga, gd, gl, gu, ha, haw, hi, hmn, hr, ht, hu, hy, id, ig, is, it, iw, ja, jw, ka, kk, km, kn, ko, ku, ky, la, lb, lo, lt,
lv, mg, mi, mk, ml, mn, mr, ms, mt, my, ne, nl, no, ny, pa, pl, ps, pt, ro, ru, sd, si, sk, sl, sm, sn, so, sq, sr, st, su, sv,
sw, ta, te, tg, th, tl, tr, uk, ur, uz, vi, xh, yi, yo, zh, zh-tw, zu
A score greater than zero is positive and less than zero is negative. The score typically ranges from -5 to +5, but can go higher and lower.
See the <a href="https://github.com/marcellobarile/multilang-sentiment/blob/develop/README.md" target="_blank">Multilang Sentiment docs here</a>.</p>

View File

@ -0,0 +1,8 @@
{
"sentiment": {
"sentiment": "sentiment",
"label": {
"language": "Language"
}
}
}

View File

@ -0,0 +1,24 @@
{
"name" : "node-red-node-sentiment",
"version" : "0.1.0",
"description" : "A Node-RED node that uses the AFINN-165 wordlists for sentiment analysis of words translated into multiple languages including emojis.",
"dependencies" : {
"multilang-sentiment" : "^1.1.6"
},
"repository" : {
"type":"git",
"url":"https://github.com/node-red/node-red-nodes/tree/master/analysis/sentiment"
},
"license": "Apache-2.0",
"keywords": [ "node-red", "sentiment", "anaylsis", "AFINN" ],
"node-red" : {
"nodes" : {
"sentiment": "72-sentiment.js"
}
},
"author": {
"name": "Dave Conway-Jones",
"email": "ceejay@vnet.ibm.com",
"url": "http://nodered.org"
}
}

View File

@ -0,0 +1,200 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var sentimentNode = require("../../../analysis/sentiment/72-sentiment.js");
var helper = require("node-red-node-test-helper");
describe('sentiment Node', function() {
before(function(done) {
helper.startServer(done);
});
after(function(done) {
helper.stopServer(done);
});
afterEach(function() {
helper.unload();
});
it('should be loaded', function(done) {
var flow = [{id:"sentimentNode1", type:"sentiment", name: "sentimentNode" }];
helper.load(sentimentNode, flow, function() {
var sentimentNode1 = helper.getNode("sentimentNode1");
sentimentNode1.should.have.property('name', 'sentimentNode');
done();
});
});
it('should pass on msg if no payload', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.not.have.property('sentiment');
msg.topic.should.equal("pass on");
done();
});
var testString = 'good, great, best, brilliant';
jn1.receive({topic:"pass on"});
});
});
it('should add a positive score for good words', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
try {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.above(10);
done();
} catch(err) {
done(err);
}
});
var testString = 'good, great, best, brilliant';
jn1.receive({payload:testString});
});
});
it('should add a positive score for good words (in French)', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]],lang:"fr"},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
try {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.above(5);
done();
} catch(err) {
done(err);
}
});
var testString = 'bon, belle, don du ciel, brillant';
jn1.receive({payload:testString});
});
});
it('should add a positive score for good words - alternative property', function(done) {
var flow = [{id:"jn1",type:"sentiment",property:"foo",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
try {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.above(10);
done();
} catch(err) {
done(err);
}
});
var testString = 'good, great, best, brilliant';
jn1.receive({foo:testString});
});
});
it('should add a negative score for bad words', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.below(-10);
done();
});
var testString = 'bad, horrible, negative, awful';
jn1.receive({payload:testString});
});
});
it('should add a negative score for bad words - alternative property', function(done) {
var flow = [{id:"jn1",type:"sentiment",property:"foo",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.be.below(-10);
done();
});
var testString = 'bad, horrible, negative, awful';
jn1.receive({foo:testString});
});
});
it('should allow you to override word scoring', function(done) {
var flow = [{id:"jn1",type:"sentiment",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.equal(20);
done();
});
var testString = 'sick, wicked';
var overrides = {'sick': 10, 'wicked': 10 };
jn1.receive({payload:testString,overrides:overrides});
});
});
it('should allow you to override word scoring - alternative property', function(done) {
var flow = [{id:"jn1",type:"sentiment",property:"foo",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(sentimentNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('sentiment');
msg.sentiment.should.have.property('score');
msg.sentiment.score.should.be.a.Number();
msg.sentiment.score.should.equal(20);
done();
});
var testString = 'sick, wicked';
var overrides = {'sick': 10, 'wicked': 10 };
jn1.receive({foo:testString,overrides:overrides});
});
});
});