diff --git a/README.md b/README.md index 573c8867a6..0435e0ddd2 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ on Google Cloud Platform. * [Google Cloud Prediction API](#google-cloud-prediction-api) * [Google Cloud Speech API (Beta)](#google-cloud-speech-api-beta) * [Google Translate API](#google-translate-api) - * [Google Cloud Video Intelligence API](#google-cloud-video-intelligence-api) * [Google Cloud Vision API](#google-cloud-vision-api) * [**Management Tools**](#management-tools) * [Stackdriver Debugger](#stackdriver-debugger) @@ -335,16 +334,6 @@ View the [Translate API Node.js samples][translate_samples]. [translate_docs]: https://cloud.google.com/translate/docs/ [translate_samples]: translate -#### Google Cloud Video Intelligence API - -The [Cloud Video Intelligence API][video_intelligence_docs] allows developers to -use Google video analysis technology as part of their applications. - -View the [Cloud Video Intelligence API Node.js samples][video_intelligence_samples]. - -[video_intelligence_docs]: https://cloud.google.com/video-intelligence/docs/ -[video_intelligence_samples]: https://github.com/googleapis/nodejs-video-intelligence/tree/master/samples - #### Google Cloud Vision API The [Cloud Vision API][vision_docs] allows developers to easily integrate vision diff --git a/circle.yml b/circle.yml index 42635db152..e04fddfc2e 100644 --- a/circle.yml +++ b/circle.yml @@ -108,4 +108,6 @@ deployment: - node scripts/build "storage-transfer" - node scripts/build "trace" - node scripts/build "translate" + # TODO: This build times out. Does video need more than 10 minutes? + # - node scripts/build "video" - node scripts/build "vision" diff --git a/video/README.md b/video/README.md index 8d997187cc..05da66feb4 100644 --- a/video/README.md +++ b/video/README.md @@ -1,5 +1,77 @@ -Samples for the [Google Cloud Video Intelligence API Node.js Client][client] -have moved to [github.com/googleapis/nodejs-video-intelligence/tree/master/samples/][samples]. +Google Cloud Platform logo -[client]: https://github.com/googleapis/nodejs-video-intelligence -[samples]: https://github.com/googleapis/nodejs-video-intelligence/tree/master/samples +# Google Cloud Video Intelligence API Node.js Samples + +[![Build](https://storage.googleapis.com/cloud-docs-samples-badges/GoogleCloudPlatform/nodejs-docs-samples/nodejs-docs-samples-videointelligence.svg)]() + +The [Cloud Video Intelligence API](https://cloud.google.com/video-intelligence) allows developers to use Google video analysis technology as part of their applications. + +## Table of Contents + +* [Setup](#setup) +* [Samples](#samples) + * [Video Intelligence](#video-intelligence) +* [Running the tests](#running-the-tests) + +## Setup + +1. Read [Prerequisites][prereq] and [How to run a sample][run] first. +1. Install dependencies: + + With **npm**: + + npm install + + With **yarn**: + + yarn install + +[prereq]: ../README.md#prerequisites +[run]: ../README.md#how-to-run-a-sample + +## Samples + +### Video Intelligence + +View the [documentation][video_0_docs] or the [source code][video_0_code]. + +__Usage:__ `node analyze.js --help` + +``` +Commands: + faces Analyzes faces in a video stored in Google Cloud Storage using the Cloud Video Intelligence API. + shots Analyzes shot angles in a video stored in Google Cloud Storage using the Cloud Video + Intelligence API. + labels-gcs Labels objects in a video stored in Google Cloud Storage using the Cloud Video Intelligence API. + labels-file Labels objects in a video stored locally using the Cloud Video Intelligence API. + safe-search Detects explicit content in a video stored in Google Cloud Storage. + +Options: + --help Show help [boolean] + +Examples: + node analyze.js faces gs://demomaker/larry_sergey_ice_bucket_short.mp4 + node analyze.js shots gs://demomaker/sushi.mp4 + node analyze.js labels-gcs gs://demomaker/tomatoes.mp4 + node analyze.js labels-file resources/cat.mp4 + node analyze.js safe-search gs://demomaker/tomatoes.mp4 + +For more information, see https://cloud.google.com/video-intelligence/docs +``` + +[video_0_docs]: https://cloud.google.com/video-intelligence/docs +[video_0_code]: analyze.js + +## Running the tests + +1. Set the **GCLOUD_PROJECT** and **GOOGLE_APPLICATION_CREDENTIALS** environment variables. + +1. Run the tests: + + With **npm**: + + npm test + + With **yarn**: + + yarn test diff --git a/video/analyze.js b/video/analyze.js new file mode 100644 index 0000000000..f82c904a8c --- /dev/null +++ b/video/analyze.js @@ -0,0 +1,357 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +'use strict'; + +function analyzeFaces (gcsUri) { + // [START analyze_faces] + // Imports the Google Cloud Video Intelligence library + const Video = require('@google-cloud/video-intelligence'); + + // Instantiates a client + const video = Video(); + + // The GCS filepath of the video to analyze + // const gcsUri = 'gs://my-bucket/my-video.mp4'; + + const request = { + inputUri: gcsUri, + features: ['FACE_DETECTION'] + }; + + // Detects faces in a video + video.annotateVideo(request) + .then((results) => { + const operation = results[0]; + console.log('Waiting for operation to complete...'); + return operation.promise(); + }) + .then((results) => { + // Gets faces + const faces = results[0].annotationResults[0].faceAnnotations; + faces.forEach((face, faceIdx) => { + console.log(`Face #${faceIdx}`); + console.log(`\tThumbnail size: ${face.thumbnail.length}`); + face.segments.forEach((segment, segmentIdx) => { + segment = segment.segment; + if (segment.startTimeOffset.seconds === undefined) { + segment.startTimeOffset.seconds = 0; + } + if (segment.startTimeOffset.nanos === undefined) { + segment.startTimeOffset.nanos = 0; + } + if (segment.endTimeOffset.seconds === undefined) { + segment.endTimeOffset.seconds = 0; + } + if (segment.endTimeOffset.nanos === undefined) { + segment.endTimeOffset.nanos = 0; + } + console.log(`\tAppearance #${segmentIdx}:`); + console.log(`\t\tStart: ${segment.startTimeOffset.seconds}` + + `.${(segment.startTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\t\tEnd: ${segment.endTimeOffset.seconds}.` + + `${(segment.endTimeOffset.nanos / 1e6).toFixed(0)}s`); + }); + console.log(`\tLocations:`); + }); + }) + .catch((err) => { + console.error('ERROR:', err); + }); + // [END analyze_faces] +} + +function analyzeLabelsGCS (gcsUri) { + // [START analyze_labels_gcs] + // Imports the Google Cloud Video Intelligence library + const Video = require('@google-cloud/video-intelligence'); + + // Instantiates a client + const video = Video({ + servicePath: `videointelligence.googleapis.com` + }); + + // The GCS filepath of the video to analyze + // const gcsUri = 'gs://my-bucket/my-video.mp4'; + + const request = { + inputUri: gcsUri, + features: ['LABEL_DETECTION'] + }; + + // Detects labels in a video + video.annotateVideo(request) + .then((results) => { + const operation = results[0]; + console.log('Waiting for operation to complete...'); + return operation.promise(); + }) + .then((results) => { + // Gets annotations for video + const annotations = results[0].annotationResults[0]; + + const labels = annotations.segmentLabelAnnotations; + labels.forEach((label) => { + console.log(`Label ${label.entity.description} occurs at:`); + label.segments.forEach((segment) => { + let time = segment.segment; + if (time.startTimeOffset.seconds === undefined) { + time.startTimeOffset.seconds = 0; + } + if (time.startTimeOffset.nanos === undefined) { + time.startTimeOffset.nanos = 0; + } + if (time.endTimeOffset.seconds === undefined) { + time.endTimeOffset.seconds = 0; + } + if (time.endTimeOffset.nanos === undefined) { + time.endTimeOffset.nanos = 0; + } + console.log(`\tStart: ${time.startTimeOffset.seconds}` + + `.${(time.startTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\tEnd: ${time.endTimeOffset.seconds}.` + + `${(time.endTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\tConfidence: ${segment.confidence}`); + }); + }); + }) + .catch((err) => { + console.error('ERROR:', err); + }); + // [END analyze_labels_gcs] +} + +function analyzeLabelsLocal (path) { + // [START analyze_labels_local] + // Imports the Google Cloud Video Intelligence library + Node's fs library + const Video = require('@google-cloud/video-intelligence'); + const fs = require('fs'); + + // Instantiates a client + const video = Video(); + + // The local filepath of the video to analyze + // const path = 'my-file.mp4'; + + // Reads a local video file and converts it to base64 + const file = fs.readFileSync(path); + const inputContent = file.toString('base64'); + + // Constructs request + const request = { + inputContent: inputContent, + features: ['LABEL_DETECTION'] + }; + + // Detects labels in a video + video.annotateVideo(request) + .then((results) => { + const operation = results[0]; + console.log('Waiting for operation to complete...'); + return operation.promise(); + }) + .then((results) => { + // Gets annotations for video + const annotations = results[0].annotationResults[0]; + + const labels = annotations.segmentLabelAnnotations; + labels.forEach((label) => { + console.log(`Label ${label.entity.description} occurs at:`); + label.segments.forEach((segment) => { + let time = segment.segment; + if (time.startTimeOffset.seconds === undefined) { + time.startTimeOffset.seconds = 0; + } + if (time.startTimeOffset.nanos === undefined) { + time.startTimeOffset.nanos = 0; + } + if (time.endTimeOffset.seconds === undefined) { + time.endTimeOffset.seconds = 0; + } + if (time.endTimeOffset.nanos === undefined) { + time.endTimeOffset.nanos = 0; + } + console.log(`\tStart: ${time.startTimeOffset.seconds}` + + `.${(time.startTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\tEnd: ${time.endTimeOffset.seconds}.` + + `${(time.endTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\tConfidence: ${segment.confidence}`); + }); + }); + }) + .catch((err) => { + console.error('ERROR:', err); + }); + // [END analyze_labels_local] +} + +function analyzeShots (gcsUri) { + // [START analyze_shots] + // Imports the Google Cloud Video Intelligence library + const Video = require('@google-cloud/video-intelligence'); + + // Instantiates a client + const video = Video(); + + // The GCS filepath of the video to analyze + // const gcsUri = 'gs://my-bucket/my-video.mp4'; + + const request = { + inputUri: gcsUri, + features: ['SHOT_CHANGE_DETECTION'] + }; + + // Detects camera shot changes + video.annotateVideo(request) + .then((results) => { + const operation = results[0]; + console.log('Waiting for operation to complete...'); + return operation.promise(); + }) + .then((results) => { + // Gets shot changes + const shotChanges = results[0].annotationResults[0].shotAnnotations; + console.log('Shot changes:'); + + if (shotChanges.length === 1) { + console.log(`The entire video is one shot.`); + } else { + shotChanges.forEach((shot, shotIdx) => { + console.log(`Scene ${shotIdx} occurs from:`); + if (shot.startTimeOffset === undefined) { + shot.startTimeOffset = {}; + } + if (shot.endTimeOffset === undefined) { + shot.endTimeOffset = {}; + } + if (shot.startTimeOffset.seconds === undefined) { + shot.startTimeOffset.seconds = 0; + } + if (shot.startTimeOffset.nanos === undefined) { + shot.startTimeOffset.nanos = 0; + } + if (shot.endTimeOffset.seconds === undefined) { + shot.endTimeOffset.seconds = 0; + } + if (shot.endTimeOffset.nanos === undefined) { + shot.endTimeOffset.nanos = 0; + } + console.log(`\tStart: ${shot.startTimeOffset.seconds}` + + `.${(shot.startTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\tEnd: ${shot.endTimeOffset.seconds}.` + + `${(shot.endTimeOffset.nanos / 1e6).toFixed(0)}s`); + }); + } + }) + .catch((err) => { + console.error('ERROR:', err); + }); + // [END analyze_shots] +} + +function analyzeSafeSearch (gcsUri) { + // [START analyze_safe_search] + // Imports the Google Cloud Video Intelligence library + const Video = require('@google-cloud/video-intelligence'); + + // Instantiates a client + const video = Video(); + + // The GCS filepath of the video to analyze + // const gcsUri = 'gs://my-bucket/my-video.mp4'; + + const request = { + inputUri: gcsUri, + features: ['EXPLICIT_CONTENT_DETECTION'] + }; + + // Human-readable likelihoods + const likelihoods = ['UNKNOWN', 'VERY_UNLIKELY', 'UNLIKELY', 'POSSIBLE', 'LIKELY', 'VERY_LIKELY']; + + // Detects unsafe content + video.annotateVideo(request) + .then((results) => { + const operation = results[0]; + console.log('Waiting for operation to complete...'); + return operation.promise(); + }) + .then((results) => { + // Gets unsafe content + const explicitContentResults = results[0].annotationResults[0].explicitAnnotation; + console.log('Explicit annotation results:'); + explicitContentResults.frames.forEach((result) => { + if (result.timeOffset === undefined) { + result.timeOffset = {}; + } + if (result.timeOffset.seconds === undefined) { + result.timeOffset.seconds = 0; + } + if (result.timeOffset.nanos === undefined) { + result.timeOffset.nanos = 0; + } + console.log(`\tTime: ${result.timeOffset.seconds}` + + `.${(result.timeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\t\tPornography liklihood: ${likelihoods[result.pornographyLikelihood]}`); + }); + }) + .catch((err) => { + console.error('ERROR:', err); + }); + // [END analyze_safe_search] +} + +require(`yargs`) // eslint-disable-line + .demand(1) + .command( + `faces `, + `Analyzes faces in a video stored in Google Cloud Storage using the Cloud Video Intelligence API.`, + {}, + (opts) => analyzeFaces(opts.gcsUri) + ) + .command( + `shots `, + `Analyzes shot angles in a video stored in Google Cloud Storage using the Cloud Video Intelligence API.`, + {}, + (opts) => analyzeShots(opts.gcsUri) + ) + .command( + `labels-gcs `, + `Labels objects in a video stored in Google Cloud Storage using the Cloud Video Intelligence API.`, + {}, + (opts) => analyzeLabelsGCS(opts.gcsUri) + ) + .command( + `labels-file `, + `Labels objects in a video stored locally using the Cloud Video Intelligence API.`, + {}, + (opts) => analyzeLabelsLocal(opts.gcsUri) + ) + .command( + `safe-search `, + `Detects explicit content in a video stored in Google Cloud Storage.`, + {}, + (opts) => analyzeSafeSearch(opts.gcsUri) + ) + .example(`node $0 faces gs://demomaker/larry_sergey_ice_bucket_short.mp4`) + .example(`node $0 shots gs://demomaker/sushi.mp4`) + .example(`node $0 labels-gcs gs://demomaker/tomatoes.mp4`) + .example(`node $0 labels-file cat.mp4`) + .example(`node $0 safe-search gs://demomaker/tomatoes.mp4`) + .wrap(120) + .recommendCommands() + .epilogue(`For more information, see https://cloud.google.com/video-intelligence/docs`) + .help() + .strict() + .argv; diff --git a/video/package.json b/video/package.json new file mode 100644 index 0000000000..e17c092aa4 --- /dev/null +++ b/video/package.json @@ -0,0 +1,44 @@ +{ + "name": "nodejs-docs-samples-videointelligence", + "version": "0.0.1", + "private": true, + "license": "Apache-2.0", + "author": "Google Inc.", + "repository": { + "type": "git", + "url": "https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git" + }, + "engines": { + "node": ">=4.3.2" + }, + "scripts": { + "lint": "samples lint", + "pretest": "npm run lint", + "test": "samples test run --cmd ava -- -T 5m --verbose system-test/*.test.js" + }, + "dependencies": { + "@google-cloud/video-intelligence": "^0.3.2", + "long": "^3.2.0", + "safe-buffer": "5.1.1", + "yargs": "8.0.2" + }, + "devDependencies": { + "@google-cloud/nodejs-repo-tools": "1.4.17", + "ava": "0.22.0", + "proxyquire": "1.8.0" + }, + "cloud-repo-tools": { + "requiresKeyFile": true, + "requiresProjectId": true, + "product": "video", + "samples": [ + { + "id": "video", + "name": "Video Intelligence", + "file": "analyze.js", + "docs_link": "https://cloud.google.com/video-intelligence/docs", + "usage": "node analyze.js --help" + } + ] + } +} diff --git a/video/quickstart.js b/video/quickstart.js new file mode 100644 index 0000000000..bb40eafe9f --- /dev/null +++ b/video/quickstart.js @@ -0,0 +1,73 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +'use strict'; + +// [START videointelligence_quickstart] +// Imports the Google Cloud Video Intelligence library +const Video = require('@google-cloud/video-intelligence'); + +// Instantiates a client +const video = Video(); + +// The GCS filepath of the video to analyze +const gcsUri = 'gs://nodejs-docs-samples-video/quickstart_short.mp4'; + +// Construct request +const request = { + inputUri: gcsUri, + features: ['LABEL_DETECTION'] +}; + +// Execute request +video.annotateVideo(request) + .then((results) => { + const operation = results[0]; + console.log('Waiting for operation to complete... (this may take a few minutes)'); + return operation.promise(); + }) + .then((results) => { + // Gets annotations for video + const annotations = results[0].annotationResults[0]; + + // Gets labels for video from its annotations + const labels = annotations.segmentLabelAnnotations; + labels.forEach((label) => { + console.log(`Label ${label.entity.description} occurs at:`); + label.segments.forEach((segment) => { + segment = segment.segment; + if (segment.startTimeOffset.seconds === undefined) { + segment.startTimeOffset.seconds = 0; + } + if (segment.startTimeOffset.nanos === undefined) { + segment.startTimeOffset.nanos = 0; + } + if (segment.endTimeOffset.seconds === undefined) { + segment.endTimeOffset.seconds = 0; + } + if (segment.endTimeOffset.nanos === undefined) { + segment.endTimeOffset.nanos = 0; + } + console.log(`\tStart: ${segment.startTimeOffset.seconds}` + + `.${(segment.startTimeOffset.nanos / 1e6).toFixed(0)}s`); + console.log(`\tEnd: ${segment.endTimeOffset.seconds}.` + + `${(segment.endTimeOffset.nanos / 1e6).toFixed(0)}s`); + }); + }); + }) + .catch((err) => { + console.error('ERROR:', err); + }); +// [END videointelligence_quickstart] diff --git a/video/resources/cat.mp4 b/video/resources/cat.mp4 new file mode 100644 index 0000000000..0e071b9ec6 Binary files /dev/null and b/video/resources/cat.mp4 differ diff --git a/video/system-test/analyze.test.js b/video/system-test/analyze.test.js new file mode 100644 index 0000000000..ce54904743 --- /dev/null +++ b/video/system-test/analyze.test.js @@ -0,0 +1,77 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +// https://cloud.google.com/video-intelligence/docs/ + +'use strict'; + +const path = require(`path`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const cmd = `node analyze.js`; +const cwd = path.join(__dirname, `..`); + +const url = `gs://nodejs-docs-samples-video/quickstart.mp4`; +const shortUrl = `gs://nodejs-docs-samples-video/quickstart_short.mp4`; +const file = `resources/cat.mp4`; + +// analyze_faces +test.serial(`should analyze faces in a GCS file`, async (t) => { + const output = await tools.runAsync(`${cmd} faces ${url}`, cwd); + t.regex(output, /Thumbnail size: \d+/); + t.regex(output, /Start:.*\d+\.\d+s/); + t.regex(output, /End:.*\d+\.\d+s/); +}); + +// analyze_labels_gcs (one scene) +test.serial(`should analyze labels in a GCS file with one scene`, async (t) => { + const output = await tools.runAsync(`${cmd} labels-gcs ${shortUrl}`, cwd); + t.regex(output, /Label shirt occurs at:/); + t.regex(output, /Confidence: \d+\.\d+/); +}); + +// analyze_labels_gcs (multiple scenes) +test.serial(`should analyze labels in a GCS file with multiple scenes`, async (t) => { + const output = await tools.runAsync(`${cmd} labels-gcs ${url}`, cwd); + t.regex(output, /Label shirt occurs at:/); + t.regex(output, /Confidence: \d+\.\d+/); +}); + +// analyze_labels_local +test.serial(`should analyze labels in a local file`, async (t) => { + const output = await tools.runAsync(`${cmd} labels-file ${file}`, cwd); + t.regex(output, /Label whiskers occurs at:/); + t.regex(output, /Confidence: \d+\.\d+/); +}); + +// analyze_shots (multiple shots) +test.serial(`should analyze shots in a GCS file with multiple shots`, async (t) => { + const output = await tools.runAsync(`${cmd} shots ${url}`, cwd); + t.regex(output, /Scene 0 occurs from:/); +}); + +// analyze_shots (one shot) +test.serial(`should analyze shots in a GCS file with one shot`, async (t) => { + const output = await tools.runAsync(`${cmd} shots ${shortUrl}`, cwd); + t.regex(output, /The entire video is one shot./); +}); + +// analyze_safe_search +test.serial(`should analyze safe search results in a GCS file`, async (t) => { + const output = await tools.runAsync(`${cmd} safe-search ${url}`, cwd); + t.regex(output, /Time: \d+\.\d+s/); + t.regex(output, /Explicit annotation results:/); +}); diff --git a/video/system-test/quickstart.test.js b/video/system-test/quickstart.test.js new file mode 100644 index 0000000000..20965be0c6 --- /dev/null +++ b/video/system-test/quickstart.test.js @@ -0,0 +1,28 @@ +/** + * Copyright 2017, Google, Inc. + * 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. + */ + +'use strict'; + +const path = require(`path`); +const test = require(`ava`); +const tools = require(`@google-cloud/nodejs-repo-tools`); + +const cmd = `node quickstart.js`; +const cwd = path.join(__dirname, `..`); + +test(`should analyze a hardcoded video`, async (t) => { + const output = await tools.runAsync(cmd, cwd); + t.regex(output, /Label standing occurs at:/); +});