diff --git a/analysis/sentiment/72-sentiment.html b/analysis/sentiment/72-sentiment.html new file mode 100644 index 00000000..bf25617e --- /dev/null +++ b/analysis/sentiment/72-sentiment.html @@ -0,0 +1,175 @@ + + + + + + diff --git a/analysis/sentiment/72-sentiment.js b/analysis/sentiment/72-sentiment.js new file mode 100644 index 00000000..915d5530 --- /dev/null +++ b/analysis/sentiment/72-sentiment.js @@ -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); +} diff --git a/analysis/sentiment/LICENSE b/analysis/sentiment/LICENSE new file mode 100644 index 00000000..7fe18b61 --- /dev/null +++ b/analysis/sentiment/LICENSE @@ -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. diff --git a/analysis/sentiment/README.md b/analysis/sentiment/README.md new file mode 100644 index 00000000..0266b9f2 --- /dev/null +++ b/analysis/sentiment/README.md @@ -0,0 +1,31 @@ +node-red-node-sentiment +======================== + +A Node-RED 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 Multilang Sentiment docs here.
diff --git a/analysis/sentiment/locales/en-US/72-sentiment.json b/analysis/sentiment/locales/en-US/72-sentiment.json new file mode 100644 index 00000000..b65aad90 --- /dev/null +++ b/analysis/sentiment/locales/en-US/72-sentiment.json @@ -0,0 +1,8 @@ +{ + "sentiment": { + "sentiment": "sentiment", + "label": { + "language": "Language" + } + } +} diff --git a/analysis/sentiment/package.json b/analysis/sentiment/package.json new file mode 100644 index 00000000..0a18be7d --- /dev/null +++ b/analysis/sentiment/package.json @@ -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" + } +} diff --git a/test/analysis/sentiment/72-sentiment_spec.js b/test/analysis/sentiment/72-sentiment_spec.js new file mode 100644 index 00000000..dc61c536 --- /dev/null +++ b/test/analysis/sentiment/72-sentiment_spec.js @@ -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}); + }); + }); + +});