|
1 | 1 | /** |
2 | | - * Copyright 2013,2015 IBM Corp. |
| 2 | + * Copyright 2013,2015, 2016 IBM Corp. |
3 | 3 | * |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the 'License'); |
5 | 5 | * you may not use this file except in compliance with the License. |
|
14 | 14 | * limitations under the License. |
15 | 15 | **/ |
16 | 16 |
|
| 17 | +// AlchemyAPI Image Analysis functions supported by this node |
17 | 18 | var FEATURE_RESPONSES = { |
18 | 19 | imageFaces: 'imageFaces', |
19 | 20 | imageLink: "image", |
20 | | - imageKeywords: "imageKeywords" |
| 21 | + imageKeywords: "imageKeywords", |
| 22 | + imageText: "sceneTextLines" |
21 | 23 | }; |
22 | 24 |
|
| 25 | + |
23 | 26 | 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. |
26 | 47 |
|
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, |
29 | 50 |
|
30 | | - var apikey; |
| 51 | + var apikey, s_apikey; |
31 | 52 |
|
32 | 53 | var service = cfenv.getAppEnv().getServiceCreds(/alchemy/i); |
33 | 54 |
|
34 | 55 | if (service) { |
35 | | - apikey = service.apikey; |
| 56 | + s_apikey = service.apikey; |
36 | 57 | } |
37 | 58 |
|
38 | 59 | RED.httpAdmin.get('/alchemy-image-analysis/vcap', function (req, res) { |
39 | 60 | res.json(service ? {bound_service: true} : null); |
40 | 61 | }); |
41 | 62 |
|
| 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 | + |
42 | 106 | function AlchemyImageAnalysisNode (config) { |
43 | 107 | RED.nodes.createNode(this, config); |
44 | 108 | var node = this; |
45 | 109 |
|
46 | 110 | this.on('input', function (msg) { |
47 | 111 | if (!msg.payload) { |
| 112 | + this.status({fill:'red', shape:'ring', text:'missing payload'}); |
48 | 113 | var message = 'Missing property: msg.payload'; |
49 | 114 | node.error(message, msg); |
50 | 115 | return; |
51 | 116 | } |
52 | 117 |
|
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({}); |
54 | 121 |
|
55 | 122 | if (!apikey) { |
| 123 | + this.status({fill:'red', shape:'ring', text:'missing credentials'}); |
56 | 124 | var message ='Missing Alchemy API service credentials'; |
57 | 125 | node.error(message, msg); |
58 | 126 | return; |
59 | 127 | } |
60 | 128 |
|
61 | | - var alchemy = new AlchemyAPI(apikey); |
62 | | - |
| 129 | + // Check which single feature has been requested. |
63 | 130 | var feature = config["image-feature"]; |
64 | 131 |
|
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); |
70 | 146 | } |
| 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 | + } |
71 | 181 |
|
72 | | - msg.result = response[FEATURE_RESPONSES[feature]]; |
73 | | - node.send(msg) |
74 | | - }) |
75 | 182 | }); |
76 | 183 | } |
77 | 184 |
|
|
0 commit comments