Skip to content

Commit ab37958

Browse files
committed
migrated alchemy vision node from alchemy sdk to node sdk
1 parent af4c083 commit ab37958

3 files changed

Lines changed: 134 additions & 22 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"request": "~2.53.0",
1111
"temp": "^0.8.3",
1212
"qs": "6.x",
13+
"image-type": "^2.0.2",
1314
"watson-developer-cloud": "^1.2.4"
1415
},
1516
"repository": {

services/alchemy_vision/v1.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<option value="imageFaces">Faces</option>
3131
<option value="imageLink">URLs</option>
3232
<option value="imageKeywords">Content Tags</option>
33+
<option value="imageText">Text</option>
3334
</select>
3435
</div>
3536
<div class="form-row">
@@ -42,14 +43,17 @@
4243
<p>Using the Image Analysis node, you can use the Alchemy APIs to analyse images for face detection, content tags and links.</p>
4344
<p>The following features are available for analysis:</p>
4445
<ul>
45-
<li><b>Faces</b>, discover human faces within the images.</li>
46-
<li><b>URLs</b>, discover URLs within the images.</li>
46+
<li><b>Faces</b>, discover human faces within the image.</li>
47+
<li><b>URLs</b>, extract images from a URL webpage.</li>
4748
<li><b>Content Tags</b>, extract content tags from the image.</li>
49+
<li><b>Text</b>, extract text from the image.</li>
4850
</ul>
4951
<p>For full details on the feature details and response values, please see the <a href="http://www.alchemyapi.com/api">Alchemy API documentation</a></p>
5052
<p>The content to be analysed should be passed in on <code>msg.payload</code>.</p>
5153
<p>Valid <code>msg.payload</code> types: URL or Buffer containing raw image bytes.</p>
52-
<p>If you need to send custom parameters along with each feature, set those parameters as children of the <code>msg.alchemy_options</code> object.</p>
54+
<p>If you need to send custom parameters along with each feature, set those parameters as children of the <code>msg.alchemy_options</code> object.
55+
eg. <code>msg.alchemy_options = {knowledgeGraph: 1}</code>
56+
</p>
5357
<br>
5458
<p>Results from the Alchemy API service will made available at <code>msg.result</code>.</p>
5559
</script>

services/alchemy_vision/v1.js

Lines changed: 126 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2013,2015 IBM Corp.
2+
* Copyright 2013,2015, 2016 IBM Corp.
33
*
44
* Licensed under the Apache License, Version 2.0 (the 'License');
55
* you may not use this file except in compliance with the License.
@@ -14,64 +14,171 @@
1414
* limitations under the License.
1515
**/
1616

17+
// AlchemyAPI Image Analysis functions supported by this node
1718
var FEATURE_RESPONSES = {
1819
imageFaces: 'imageFaces',
1920
imageLink: "image",
20-
imageKeywords: "imageKeywords"
21+
imageKeywords: "imageKeywords",
22+
imageText: "sceneTextLines"
2123
};
2224

25+
2326
module.exports = function (RED) {
24-
var cfenv = require('cfenv'),
25-
AlchemyAPI = require('alchemy-api');
27+
var cfenv = require('cfenv');
28+
var watson = require('watson-developer-cloud');
29+
30+
var imageType = require('image-type');
31+
var url = require('url');
32+
var temp = require('temp');
33+
var fileType = require('file-type');
34+
var fs = require('fs');
35+
36+
// temp is being used for file streaming to allow the file to arrive so it can be processed.
37+
temp.track();
38+
39+
// Require the Cloud Foundry Module to pull credentials from bound service
40+
// If they are found then the api key is stored in the variable s_apikey.
41+
//
42+
// This separation between s_apikey and apikey is to allow
43+
// the end user to modify the key redentials when the service is not bound.
44+
// Otherwise, once set apikey is never reset, resulting in a frustrated
45+
// user who, when he errenously enters bad credentials, can't figure out why
46+
// the edited ones are not being taken.
2647

27-
var services = cfenv.getAppEnv().services,
28-
service;
48+
// Taking this line out as codacy was complaining about it.
49+
// var services = cfenv.getAppEnv().services,
2950

30-
var apikey;
51+
var apikey, s_apikey;
3152

3253
var service = cfenv.getAppEnv().getServiceCreds(/alchemy/i);
3354

3455
if (service) {
35-
apikey = service.apikey;
56+
s_apikey = service.apikey;
3657
}
3758

3859
RED.httpAdmin.get('/alchemy-image-analysis/vcap', function (req, res) {
3960
res.json(service ? {bound_service: true} : null);
4061
});
4162

63+
// Utility functions that check for image buffers, urls and stream data in
64+
65+
function imageCheck(data) {
66+
return data instanceof Buffer && imageType(data) !== null;
67+
};
68+
69+
function urlCheck(str) {
70+
var parsed = url.parse(str)
71+
return (!!parsed.hostname && !!parsed.protocol && str.indexOf(' ') < 0);
72+
};
73+
74+
function stream_buffer(file, contents, cb) {
75+
fs.writeFile(file, contents, function (err) {
76+
if (err) throw err;
77+
cb();
78+
});
79+
};
80+
81+
// Utility function that performs the alchemy vision call.
82+
// the cleanup removes the temp storage, and I am not sure whether
83+
// it should be called here or after alchemy returns and passed
84+
// control back to cbdone.
85+
86+
function performAction(params, feature, cbdone, cbcleanup) {
87+
var alchemy_vision = watson.alchemy_vision( { api_key: apikey } );
88+
89+
if (feature == 'imageFaces')
90+
{
91+
alchemy_vision.recognizeFaces(params, cbdone);
92+
} else if (feature == 'imageLink') {
93+
alchemy_vision.getImageLinks(params, cbdone);
94+
} else if (feature == 'imageKeywords') {
95+
alchemy_vision.getImageKeywords(params, cbdone);
96+
} else if (feature == 'imageText') {
97+
alchemy_vision.getImageSceneText(params, cbdone);
98+
}
99+
100+
if (cbcleanup) cbcleanup();
101+
}
102+
103+
104+
// This is the Alchemy Image Node
105+
42106
function AlchemyImageAnalysisNode (config) {
43107
RED.nodes.createNode(this, config);
44108
var node = this;
45109

46110
this.on('input', function (msg) {
47111
if (!msg.payload) {
112+
this.status({fill:'red', shape:'ring', text:'missing payload'});
48113
var message = 'Missing property: msg.payload';
49114
node.error(message, msg);
50115
return;
51116
}
52117

53-
apikey = apikey || this.credentials.apikey;
118+
// If it is present the newly provided user entered key takes precedence over the existing one.
119+
apikey = s_apikey || this.credentials.apikey;
120+
this.status({});
54121

55122
if (!apikey) {
123+
this.status({fill:'red', shape:'ring', text:'missing credentials'});
56124
var message ='Missing Alchemy API service credentials';
57125
node.error(message, msg);
58126
return;
59127
}
60128

61-
var alchemy = new AlchemyAPI(apikey);
62-
129+
// Check which single feature has been requested.
63130
var feature = config["image-feature"];
64131

65-
alchemy[feature](msg.payload, msg.alchemy_options || {}, function (err, response) {
66-
if (err || response.status === "ERROR") {
67-
var message = 'Alchemy API request error: ' + (err ? err : response.statusInfo);
68-
node.error(message, msg);
69-
return;
132+
// Splice in the additional options from msg.alchemy_options
133+
// eg. The user may have entered msg.alchemy_options = {knowledgeGraph: 1};
134+
var params = {};
135+
136+
for (var key in msg.alchemy_options) { params[key] = msg.alchemy_options[key]; }
137+
138+
// This is the callback after the call to the alchemy service.
139+
// Set up as a var within this scope, so it has access to node, msg etc.
140+
// in preparation for the Alchemy service action
141+
var actionComplete = function(err, keywords) {
142+
if (err || keywords.status === 'ERROR') {
143+
node.status({fill:'red', shape:'ring', text:'call to alchmeyapi vision service failed'});
144+
console.log('Error:', msg, err);
145+
node.error(err, msg);
70146
}
147+
else {
148+
msg.result = keywords[FEATURE_RESPONSES[feature]] || [];
149+
msg.fullresult = {};
150+
msg.fullresult['all'] = keywords;
151+
node.send(msg);
152+
}
153+
}
154+
155+
// If the input is an image, need to stream the input in, giving time for the
156+
// data to arrive, before invoking the service.
157+
if (imageCheck(msg.payload)) {
158+
temp.open({suffix: '.' + fileType(msg.payload).ext}, function (err, info) {
159+
if (err) {
160+
this.status({fill:'red', shape:'ring', text:'unable to open image stream'});
161+
var message ='Node has been unable to open the image stream';
162+
node.error(message, msg);
163+
return;
164+
}
165+
166+
stream_buffer(info.path, msg.payload, function () {
167+
params['image'] = fs.createReadStream(info.path);
168+
performAction(params, feature, actionComplete, temp.cleanup);
169+
});
170+
171+
});
172+
} else if (urlCheck(msg.payload)) {
173+
params['url'] = msg.payload;
174+
performAction(params, feature, actionComplete);
175+
} else {
176+
this.status({fill:'red', shape:'ring', text:'payload is invalid'});
177+
var message ='Payload must be either an image buffer or a string representing a url';
178+
node.error(message, msg);
179+
return;
180+
}
71181

72-
msg.result = response[FEATURE_RESPONSES[feature]];
73-
node.send(msg)
74-
})
75182
});
76183
}
77184

0 commit comments

Comments
 (0)