Skip to content

Commit

Permalink
vision: return errors (#1334)
Browse files Browse the repository at this point in the history
* vision: return errors

* tests

* test coverage fixer

* tests

* docs

* add error handling example

* elaborate error handling
  • Loading branch information
stephenplusplus authored and callmehiphop committed Jun 2, 2016
1 parent 4184a1a commit a0da750
Show file tree
Hide file tree
Showing 5 changed files with 568 additions and 216 deletions.
7 changes: 4 additions & 3 deletions lib/common/grpc-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var Service = require('./service.js');
* @const {object} - A map of protobuf codes to HTTP status codes.
* @private
*/
var HTTP_ERROR_CODE_MAP = {
var GRPC_ERROR_CODE_TO_HTTP = {
0: {
code: 200,
message: 'OK'
Expand Down Expand Up @@ -268,8 +268,8 @@ GrpcService.prototype.request = function(protoOpts, reqOpts, callback) {

service[protoOpts.method](reqOpts, grpcOpts, function(err, resp) {
if (err) {
if (HTTP_ERROR_CODE_MAP[err.code]) {
respError = extend(err, HTTP_ERROR_CODE_MAP[err.code]);
if (GRPC_ERROR_CODE_TO_HTTP[err.code]) {
respError = extend(err, GRPC_ERROR_CODE_TO_HTTP[err.code]);
onResponse(null, respError);
return;
}
Expand Down Expand Up @@ -443,3 +443,4 @@ GrpcService.prototype.getGrpcCredentials_ = function(callback) {
};

module.exports = GrpcService;
module.exports.GRPC_ERROR_CODE_TO_HTTP = GRPC_ERROR_CODE_TO_HTTP;
283 changes: 207 additions & 76 deletions lib/vision/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ var request = require('request');
*/
var File = require('../storage/file.js');

/**
* @type {module:common/grpc-service}
* @private
*/
var GrpcService = require('../common/grpc-service.js');

/**
* @type {module:common/service}
* @private
Expand Down Expand Up @@ -206,6 +212,10 @@ Vision.prototype.annotate = function(requests, callback) {
* `config.types`). Additionally, if multiple images were provided, you will
* receive an array of detection objects, each representing an image. See
* the examples below for more information.
* @param {object[]} callback.detections.errors - It's possible for part of your
* request to be completed successfully, while a single feature request was
* not successful. Each returned detection will have an `errors` array,
* including any of these errors which may have occurred.
* @param {object} callback.apiResponse - Raw API response.
*
* @example
Expand Down Expand Up @@ -259,6 +269,18 @@ Vision.prototype.annotate = function(requests, callback) {
* // }
* // ]
* });
*
* //-
* // It's possible for part of your request to be completed successfully, while
* // a single feature request was not successful. Each returned detection will
* // have an `errors` array, including any of these errors which may have
* // occurred.
* //-
* vision.detect('malformed-image.jpg', types, function(err, detections) {
* if (detections.faces.errors.length > 0) {
* // Errors occurred while trying to use this image for a face annotation.
* }
* });
*/
Vision.prototype.detect = function(images, options, callback) {
var self = this;
Expand Down Expand Up @@ -292,6 +314,37 @@ Vision.prototype.detect = function(images, options, callback) {
text: 'TEXT_DETECTION'
};

var typeShortNameToRespName = {
face: 'faceAnnotations',
faces: 'faceAnnotations',

label: 'labelAnnotations',
labels: 'labelAnnotations',

landmark: 'landmarkAnnotations',
landmarks: 'landmarkAnnotations',

logo: 'logoAnnotations',
logos: 'logoAnnotations',

properties: 'imagePropertiesAnnotation',

safeSearch: 'safeSearchAnnotation',

text: 'textAnnotations'
};

var typeRespNameToShortName = {
errors: 'errors',
faceAnnotations: 'faces',
imagePropertiesAnnotation: 'properties',
labelAnnotations: 'labels',
landmarkAnnotations: 'landmarks',
logoAnnotations: 'logos',
safeSearchAnnotation: 'safeSearch',
textAnnotations: 'text'
};

Vision.findImages_(images, function(err, images) {
if (err) {
callback(err);
Expand Down Expand Up @@ -333,13 +386,120 @@ Vision.prototype.detect = function(images, options, callback) {
return;
}

function mergeArrayOfObjects(arr) {
return extend.apply(null, arr);
var originalResp = extend(true, {}, resp);

var detections = images
.map(groupDetectionsByImage)
.map(assignTypeToEmptyAnnotations)
.map(combineErrors)
.map(flattenAnnotations)
.map(decorateAnnotations);

// If only a single image was given, expose it from the array.
callback(null, isSingleImage ? detections[0] : detections, originalResp);

function groupDetectionsByImage() {
// detections = [
// // Image one:
// [
// {
// faceAnnotations: {},
// labelAnnotations: {},
// ...
// }
// ],
//
// // Image two:
// [
// {
// faceAnnotations: {},
// labelAnnotations: {},
// ...
// }
// ]
// ]
return annotations.splice(0, types.length);
}

function assignTypeToEmptyAnnotations(annotations) {
// Before:
// [
// {}, // What annotation type was attempted?
// { labelAnnotations: {...} }
// ]
//
// After:
// [
// { faceAnnotations: {} },
// { labelAnnotations: {...} }
// ]
return annotations.map(function(annotation, index) {
var detectionType = types[index];
var typeName = typeShortNameToRespName[detectionType];

if (is.empty(annotation) || annotation.error) {
var isPlural = typeName.charAt(typeName.length - 1) === 's';
annotation[typeName] = isPlural ? [] : {};
}

return annotation;
});
}

function combineErrors(annotations) {
// Before:
// [
// {
// faceAnnotations: [],
// error: {...}
// },
// {
// imagePropertiesAnnotation: {},
// error: {...}
// }
// ]

// After:
// [
// faceAnnotations: [],
// imagePropertiesAnnotation: {},
// errors: [
// {...},
// {...}
// ]
// ]
var errors = [];

annotations.forEach(function(annotation) {
var annotationKey = Object.keys(annotation)[0];

if (annotationKey === 'error') {
errors.push(annotation.error);
delete annotation.error;
}

return annotation;
});

annotations.push({
errors: errors
});

return annotations;
}

function flattenAnnotations(annotations) {
return extend.apply(null, annotations);
}

function formatAnnotationBuilder(type) {
return function(annotation) {
if (is.empty(annotation)) {
return annotation;
}

var formatMethodMap = {
errors: Vision.formatError_,
faceAnnotations: Vision.formatFaceAnnotation_,
imagePropertiesAnnotation: Vision.formatImagePropertiesAnnotation_,
labelAnnotations: Vision.formatEntityAnnotation_,
Expand All @@ -353,85 +513,41 @@ Vision.prototype.detect = function(images, options, callback) {
};
}

var originalResp = extend(true, {}, resp);

var detections = images
.map(function() {
// Group detections by image...
//
// detections = [
// // Image one:
// [
// {
// faceAnnotations: {},
// labelAnnotations: {},
// ...
// }
// ],
//
// // Image two:
// [
// {
// faceAnnotations: {},
// labelAnnotations: {},
// ...
// }
// ]
// ]
return annotations.splice(0, types.length);
})
.map(mergeArrayOfObjects)
.map(function(annotations) {
if (Object.keys(annotations).length === 0) {
// No annotations found, represent as an empty result set.
return [];
}

for (var annotationType in annotations) {
if (annotations.hasOwnProperty(annotationType)) {
var annotationGroup = arrify(annotations[annotationType]);

var formattedAnnotationGroup = annotationGroup
.map(formatAnnotationBuilder(annotationType));

// An annotation can be singular, e.g. SafeSearch. It is either
// violent or not. Unlike face detection, where there can be
// multiple results.
//
// Be sure the original type (object or array) is preserved and
// not wrapped in an array if it wasn't originally.
if (!is.array(annotations[annotationType])) {
formattedAnnotationGroup = formattedAnnotationGroup[0];
}

var typeFullNameToShortName = {
faceAnnotations: 'faces',
imagePropertiesAnnotation: 'properties',
labelAnnotations: 'labels',
landmarkAnnotations: 'landmarks',
logoAnnotations: 'logos',
safeSearchAnnotation: 'safeSearch',
textAnnotations: 'text'
};

delete annotations[annotationType];
var typeShortName = typeFullNameToShortName[annotationType];
annotations[typeShortName] = formattedAnnotationGroup;
function decorateAnnotations(annotations) {
for (var annotationType in annotations) {
if (annotations.hasOwnProperty(annotationType)) {
var annotationGroup = arrify(annotations[annotationType]);

var formattedAnnotationGroup = annotationGroup
.map(formatAnnotationBuilder(annotationType));

// An annotation can be singular, e.g. SafeSearch. It is either
// violent or not. Unlike face detection, where there can be
// multiple results.
//
// Be sure the original type (object or array) is preserved and
// not wrapped in an array if it wasn't originally.
if (!is.array(annotations[annotationType])) {
formattedAnnotationGroup = formattedAnnotationGroup[0];
}
}

if (types.length === 1) {
// Only a single detection type was asked for, so no need to box in
// the results. Make them accessible without using a key.
var key = Object.keys(annotations)[0];
annotations = annotations[key];
delete annotations[annotationType];
var typeShortName = typeRespNameToShortName[annotationType];
annotations[typeShortName] = formattedAnnotationGroup;
}
}

return annotations;
});
if (types.length === 1) {
// Only a single detection type was asked for, so no need to box in
// the results. Make them accessible without using a key.
var key = Object.keys(annotations)[0];
var errors = annotations.errors;
annotations = annotations[key];
annotations.errors = errors;
}

// If only a single image was given, expose it from the array.
callback(null, isSingleImage ? detections[0] : detections, originalResp);
return annotations;
}
});
});
};
Expand Down Expand Up @@ -1271,6 +1387,21 @@ Vision.formatEntityAnnotation_ = function(entityAnnotation, options) {
return formattedEntityAnnotation;
};

/**
* Format a raw error from the API.
*
* @private
*/
Vision.formatError_ = function(err) {
var httpError = GrpcService.GRPC_ERROR_CODE_TO_HTTP[err.code];

if (httpError) {
err.code = httpError.code;
}

return err;
};

/**
* Format a raw face annotation response from the API.
*
Expand Down
Loading

0 comments on commit a0da750

Please sign in to comment.