1. Einführung
Übersicht
In diesem Codelab erstellen Sie einen Cloud Run-Job in Node.js, der eine visuelle Beschreibung jeder Szene in einem Video liefert. Zuerst werden mit der Video Intelligence API die Zeitstempel für Szenenwechsel ermittelt. Anschließend wird mit einer Drittanbieter-Binärdatei namens ffmpeg ein Screenshot für jeden Zeitstempel eines Szenenwechsels aufgenommen. Schließlich wird die visuelle Beschriftung von Vertex AI verwendet, um eine visuelle Beschreibung der Screenshots zu liefern.
In diesem Codelab wird auch gezeigt, wie Sie ffmpeg in Ihrem Cloud Run-Job verwenden, um Bilder aus einem Video zu einem bestimmten Zeitstempel aufzunehmen. Da ffmpeg separat installiert werden muss, wird in diesem Codelab gezeigt, wie Sie ein Dockerfile erstellen, um ffmpeg als Teil Ihres Cloud Run-Jobs zu installieren.
Hier sehen Sie eine Abbildung der Funktionsweise des Cloud Run-Jobs:

Lerninhalte
- Erstellen eines Container-Images mit einem Dockerfile zum Installieren einer Drittanbieter-Binärdatei
- Anwenden des Grundsatzes der geringsten Berechtigung durch Erstellen eines Dienstkontos für den Cloud Run-Job, um andere Google Cloud-Dienste aufzurufen
- Verwenden der Video Intelligence-Clientbibliothek aus einem Cloud Run-Job
- Aufrufen von Google APIs, um die visuelle Beschreibung jeder Szene von Vertex AI abzurufen
2. Einrichtung und Anforderungen
Voraussetzungen
- Sie sind in der Cloud Console angemeldet.
- Sie haben bereits einen Cloud Run-Dienst bereitgestellt. Sie können beispielsweise der Kurzanleitung zum Bereitstellen eines Webdienstes aus Quellcode folgen, um loszulegen.
Cloud Shell aktivieren
- Klicken Sie in der Cloud Console auf Cloud Shell aktivieren
.

Wenn Sie die Cloud Shell zum ersten Mal starten, wird ein Fenster mit einer Beschreibung eingeblendet. Klicken Sie in diesem Fall einfach auf Weiter.

Das Herstellen der Verbindung mit der Cloud Shell sollte nur wenige Augenblicke dauern.

Diese virtuelle Maschine ist mit allen erforderlichen Entwicklungstools ausgestattet. Sie bietet ein persistentes 5-GB-Homeverzeichnis und wird in Google Cloud ausgeführt, wodurch die Netzwerkleistung und die Authentifizierung erheblich verbessert werden. Die meisten, wenn nicht sogar alle Aufgaben in diesem Codelab können mit einem Browser erledigt werden.
Sobald die Verbindung mit der Cloud Shell hergestellt ist, sehen Sie, dass Sie authentifiziert sind und für das Projekt schon Ihre Projekt-ID eingestellt ist.
- Führen Sie in der Cloud Shell den folgenden Befehl aus, um zu prüfen, ob Sie authentifiziert sind:
gcloud auth list
Befehlsausgabe
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Führen Sie in der Cloud Shell den folgenden Befehl aus, um zu prüfen, ob der gcloud-Befehl Ihr Projekt kennt:
gcloud config list project
Befehlsausgabe
[core] project = <PROJECT_ID>
Ist dies nicht der Fall, können Sie die Einstellung mit diesem Befehl vornehmen:
gcloud config set project <PROJECT_ID>
Befehlsausgabe
Updated property [core/project].
3. APIs aktivieren und Umgebungsvariablen festlegen
Bevor Sie mit diesem Codelab beginnen können, müssen Sie mehrere APIs aktivieren. Für dieses Codelab müssen Sie die folgenden APIs verwenden. Sie können diese APIs mit dem folgenden Befehl aktivieren:
gcloud services enable run.googleapis.com \
storage.googleapis.com \
cloudbuild.googleapis.com \
videointelligence.googleapis.com \
aiplatform.googleapis.com
Anschließend können Sie Umgebungsvariablen festlegen, die in diesem Codelab verwendet werden.
REGION=<YOUR-REGION> PROJECT_ID=<YOUR-PROJECT-ID> PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') JOB_NAME=video-describer-job BUCKET_ID=$PROJECT_ID-video-describer SERVICE_ACCOUNT="cloud-run-job-video" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
4. Dienstkonto erstellen
Sie erstellen ein Dienstkonto für den Cloud Run-Job, mit dem auf Cloud Storage, Vertex AI und die Video Intelligence API zugegriffen werden kann.
Erstellen Sie zuerst das Dienstkonto.
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run Video Scene Image Describer service account"
Gewähren Sie dem Dienstkonto dann Zugriff auf den Cloud Storage-Bucket und die Vertex AI APIs.
# to view & download storage bucket objects gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/storage.objectViewer # to call the Vertex AI imagetext model gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/aiplatform.user
5. Cloud Storage-Bucket erstellen
Erstellen Sie mit dem folgenden Befehl einen Cloud Storage-Bucket, in den Sie Videos hochladen können, die vom Cloud Run-Job verarbeitet werden sollen:
gsutil mb -l us-central1 gs://$BUCKET_ID/
[Optional] Sie können dieses Beispielvideo verwenden, indem Sie es lokal herunterladen.
gsutil cp gs://cloud-samples-data/video/visionapi.mp4 testvideo.mp4
Laden Sie jetzt Ihre Videodatei in Ihren Storage-Bucket hoch.
FILENAME=<YOUR-VIDEO-FILENAME> gsutil cp $FILENAME gs://$BUCKET_ID
6. Cloud Run-Job erstellen
Erstellen Sie zuerst ein Verzeichnis für den Quellcode und wechseln Sie in dieses Verzeichnis.
mkdir video-describer-job && cd $_
Erstellen Sie dann eine package.json-Datei mit folgendem Inhalt:
{
"name": "video-describer-job",
"version": "1.0.0",
"private": true,
"description": "describes the image in every scene for a given video",
"main": "app.js",
"author": "Google LLC",
"license": "Apache-2.0",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"@google-cloud/storage": "^7.7.0",
"@google-cloud/video-intelligence": "^5.0.1",
"axios": "^1.6.2",
"fluent-ffmpeg": "^2.1.2",
"google-auth-library": "^9.4.1"
}
}
Diese App besteht aus mehreren Quelldateien, um die Lesbarkeit zu verbessern. Erstellen Sie zuerst eine app.js-Quelldatei mit dem folgenden Inhalt. Diese Datei enthält den Einstiegspunkt für den Job und die Hauptlogik für die App.
const bucketName = "<YOUR_BUCKET_ID>";
const videoFilename = "<YOUR-VIDEO-FILENAME>";
const { captureImages } = require("./helpers/imageCapture.js");
const { detectSceneChanges } = require("./helpers/sceneDetector.js");
const { getImageCaption } = require("./helpers/imageCaptioning.js");
const storageHelper = require("./helpers/storage.js");
const authHelper = require("./helpers/auth.js");
const fs = require("fs").promises;
const path = require("path");
const main = async () => {
try {
// download the file to locally to the Cloud Run Job instance
let localFilename = await storageHelper.downloadVideoFile(
bucketName,
videoFilename
);
// PART 1 - Use Video Intelligence API
// detect all the scenes in the video & save timestamps to an array
// EXAMPLE OUTPUT
// Detected scene changes at the following timestamps:
// [1, 7, 11, 12]
let timestamps = await detectSceneChanges(localFilename);
console.log(
"Detected scene changes at the following timestamps: ",
timestamps
);
// PART 2 - Use ffmpeg via dockerfile install
// create an image of each scene change
// and save to a local directory called "output"
// returns the base filename for the generated images
// EXAMPLE OUTPUT
// creating screenshot for scene: 1 at output/video-filename-1.png
// creating screenshot for scene: 7 at output/video-filename-7.png
// creating screenshot for scene: 11 at output/video-filename-11.png
// creating screenshot for scene: 12 at output/video-filename-12.png
// returns the base filename for the generated images
let imageBaseName = await captureImages(localFilename, timestamps);
// PART 3a - get Access Token to call Vertex AI APIs via REST
// needed for the image captioning
// since we're calling the Vertex AI APIs directly
let accessToken = await authHelper.getAccessToken();
console.log("got an access token");
// PART 3b - use Image Captioning to describe each scene per screenshot
// EXAMPLE OUTPUT
/*
[
{
timestamp: 1,
description:
"an aerial view of a city with a bridge in the background"
},
{
timestamp: 7,
description:
"a man in a blue shirt sits in front of shelves of donuts"
},
{
timestamp: 11,
description:
"a black and white photo of people working in a bakery"
},
{
timestamp: 12,
description:
"a black and white photo of a man and woman working in a bakery"
}
]; */
// instantiate the data structure for storing the scene description and timestamp
// e.g. an array of json objects,
// [{ timestamp: 5, description: "..." }, ...]
let scenes = [];
// for each timestamp, send the image to Vertex AI
console.log("getting Vertex AI description for each timestamps");
scenes = await Promise.all(
timestamps.map(async (timestamp) => {
let filepath = path.join(
"./output",
imageBaseName + "-" + timestamp + ".png"
);
// get the base64 encoded image bc sending via REST
const encodedFile = await fs.readFile(filepath, "base64");
// send each screenshot to Vertex AI for description
let description = await getImageCaption(
accessToken,
encodedFile
);
return { timestamp: timestamp, description: description };
})
);
console.log("finished collecting all the scenes");
console.log(scenes);
} catch (error) {
//return an error
console.error("received error: ", error);
}
};
// Start script
main().catch((err) => {
console.error(err);
});
Erstellen Sie als Nächstes das Dockerfile.
# Copyright 2020 Google, LLC. # # 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 the official lightweight Node.js image. # https://hub.docker.com/_/node FROM node:20.10.0-slim # Create and change to the app directory. WORKDIR /usr/src/app RUN apt-get update && apt-get install -y ffmpeg # Copy application dependency manifests to the container image. # A wildcard is used to ensure both package.json AND package-lock.json are copied. # Copying this separately prevents re-running npm install on every code change. COPY package*.json ./ # Install dependencies. # If you add a package-lock.json speed your build by switching to 'npm ci'. # RUN npm ci --only=production RUN npm install --production # Copy local code to the container image. COPY . . # Run the job on container startup. CMD [ "npm", "start" ]
Erstellen Sie eine Datei mit dem Namen .dockerignore, um bestimmte Dateien vom Containerisieren auszuschließen.
Dockerfile .dockerignore node_modules npm-debug.log
Erstellen Sie jetzt einen Ordner mit dem Namen helpers. Dieser Ordner enthält fünf Hilfsdateien.
mkdir helpers cd helpers
Erstellen Sie als Nächstes eine sceneDetector.js-Datei mit folgendem Inhalt. In dieser Datei wird die Video Intelligence API verwendet, um Szenenwechsel im Video zu erkennen.
const fs = require("fs");
const util = require("util");
const readFile = util.promisify(fs.readFile);
const ffmpeg = require("fluent-ffmpeg");
const Video = require("@google-cloud/video-intelligence");
const client = new Video.VideoIntelligenceServiceClient();
module.exports = {
detectSceneChanges: async function (downloadedFile) {
// Reads a local video file and converts it to base64
const file = await readFile(downloadedFile);
const inputContent = file.toString("base64");
// setup request for shot change detection
const videoContext = {
speechTranscriptionConfig: {
languageCode: "en-US",
enableAutomaticPunctuation: true
}
};
const request = {
inputContent: inputContent,
features: ["SHOT_CHANGE_DETECTION"]
};
// Detects camera shot changes
const [operation] = await client.annotateVideo(request);
console.log("Shot (scene) detection in progress...");
const [operationResult] = await operation.promise();
// Gets shot changes
const shotChanges =
operationResult.annotationResults[0].shotAnnotations;
console.log(
"Shot (scene) changes detected: " + shotChanges.length
);
// data structure to be returned
let sceneChanges = [];
// for the initial scene
sceneChanges.push(1);
// if only one scene, keep at 1 second
if (shotChanges.length === 1) {
return sceneChanges;
}
// get length of video
const videoLength = await getVideoLength(downloadedFile);
shotChanges.forEach((shot, shotIndex) => {
if (shot.endTimeOffset === undefined) {
shot.endTimeOffset = {};
}
if (shot.endTimeOffset.seconds === undefined) {
shot.endTimeOffset.seconds = 0;
}
if (shot.endTimeOffset.nanos === undefined) {
shot.endTimeOffset.nanos = 0;
}
// convert to a number
let currentTimestampSecond = Number(
shot.endTimeOffset.seconds
);
let sceneChangeTime = 0;
// double-check no scenes were detected within the last second
if (currentTimestampSecond + 1 > videoLength) {
sceneChangeTime = currentTimestampSecond;
} else {
// otherwise, for simplicity, just round up to the next second
sceneChangeTime = currentTimestampSecond + 1;
}
sceneChanges.push(sceneChangeTime);
});
return sceneChanges;
}
};
async function getVideoLength(localFile) {
let getLength = util.promisify(ffmpeg.ffprobe);
let length = await getLength(localFile);
console.log("video length: ", length.format.duration);
return length.format.duration;
}
Erstellen Sie jetzt eine Datei mit dem Namen imageCapture.js mit folgendem Inhalt. In dieser Datei wird das Knotenpaket fluent-ffmpeg verwendet, um ffmpeg-Befehle aus einer Knoten-App auszuführen.
const ffmpeg = require("fluent-ffmpeg");
const path = require("path");
const util = require("util");
module.exports = {
captureImages: async function (localFile, scenes) {
let imageBaseName = path.parse(localFile).name;
try {
for (scene of scenes) {
console.log("creating screenshot for scene: ", +scene);
await createScreenshot(localFile, imageBaseName, scene);
}
} catch (error) {
console.log("error gathering screenshots: ", error);
}
console.log("finished gathering the screenshots");
return imageBaseName; // return the base filename for each image
}
};
async function createScreenshot(localFile, imageBaseName, scene) {
return new Promise((resolve, reject) => {
ffmpeg(localFile)
.screenshots({
timestamps: [scene],
filename: `${imageBaseName}-${scene}.png`,
folder: "output",
size: "320x240"
})
.on("error", () => {
console.log(
"Failed to create scene for timestamp: " + scene
);
return reject(
"Failed to create scene for timestamp: " + scene
);
})
.on("end", () => {
return resolve();
});
});
}
Erstellen Sie schließlich eine Datei mit dem Namen imageCaptioning.js mit folgendem Inhalt. In dieser Datei wird Vertex AI verwendet, um eine visuelle Beschreibung jedes Szenenbildes zu erhalten.
const axios = require("axios");
const { GoogleAuth } = require("google-auth-library");
const auth = new GoogleAuth({
scopes: "https://www.googleapis.com/auth/cloud-platform"
});
module.exports = {
getImageCaption: async function (token, encodedFile) {
// this example shows you how to call the Vertex REST APIs directly
// https://cloud.google.com/vertex-ai/generative-ai/docs/image/image-captioning#get-captions-short
// https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/image-captioning
let projectId = await auth.getProjectId();
let config = {
headers: {
"Authorization": "Bearer " + token,
"Content-Type": "application/json; charset=utf-8"
}
};
const json = {
"instances": [
{
"image": {
"bytesBase64Encoded": encodedFile
}
}
],
"parameters": {
"sampleCount": 1,
"language": "en"
}
};
let response = await axios.post(
"https://us-central1-aiplatform.googleapis.com/v1/projects/" +
projectId +
"/locations/us-central1/publishers/google/models/imagetext:predict",
json,
config
);
return response.data.predictions[0];
}
};
Erstellen Sie eine Datei mit dem Namen auth.js. In dieser Datei wird die Google-Authentifizierungs-Clientbibliothek verwendet, um ein Zugriffstoken abzurufen, das zum direkten Aufrufen der Vertex AI-Endpunkte erforderlich ist.
const { GoogleAuth } = require("google-auth-library");
const auth = new GoogleAuth({
scopes: "https://www.googleapis.com/auth/cloud-platform"
});
module.exports = {
getAccessToken: async function () {
return await auth.getAccessToken();
}
};
Erstellen Sie schließlich eine Datei mit dem Namen storage.js. In dieser Datei werden die Cloud Storage-Clientbibliotheken verwendet, um ein Video aus Cloud Storage herunterzuladen.
const { Storage } = require("@google-cloud/storage");
module.exports = {
downloadVideoFile: async function (bucketName, videoFilename) {
// Creates a client
const storage = new Storage();
// keep same name locally
let localFilename = videoFilename;
const options = {
destination: localFilename
};
// Download the file
await storage
.bucket(bucketName)
.file(videoFilename)
.download(options);
console.log(
`gs://${bucketName}/${videoFilename} downloaded locally to ${localFilename}.`
);
return localFilename;
}
};
7. Cloud Run-Job bereitstellen und ausführen
Prüfen Sie zuerst, ob Sie sich im Stammverzeichnis video-describer-job für Ihr Codelab befinden.
cd .. && pwd
Mit diesem Befehl können Sie den Cloud Run-Job bereitstellen.
gcloud run jobs deploy $JOB_NAME --source . --region $REGION
Jetzt können Sie den Cloud Run-Job mit dem folgenden Befehl ausführen:
gcloud run jobs execute $JOB_NAME
Nachdem der Job ausgeführt wurde, können Sie mit dem folgenden Befehl einen Link zum Log-URI abrufen. Alternativ können Sie in der Cloud Console direkt zu Cloud Run-Jobs wechseln, um die Logs aufzurufen.
gcloud run jobs executions describe <JOB_EXECUTION_ID>
In den Logs sollte die folgende Ausgabe angezeigt werden:
[{ timestamp: 1, description: 'what is google cloud vision api ? is written on a white background .'},
{ timestamp: 3, description: 'a woman wearing a google cloud vision api shirt sits at a table'},
{ timestamp: 18, description: 'a person holding a cell phone with the words what is cloud vision api on the bottom' }, ...]
8. Glückwunsch!
Sie haben das Codelab abgeschlossen.
Wir empfehlen Ihnen, die Dokumentation zu Video Intelligence API, zu Cloud Run und zur visuellen Beschriftung von Vertex AI zu lesen.
Behandelte Themen
- Erstellen eines Container-Images mit einem Dockerfile zum Installieren einer Drittanbieter-Binärdatei
- Anwenden des Grundsatzes der geringsten Berechtigung durch Erstellen eines Dienstkontos für den Cloud Run-Job, um andere Google Cloud-Dienste aufzurufen
- Verwenden der Video Intelligence-Clientbibliothek aus einem Cloud Run-Job
- Aufrufen von Google APIs, um die visuelle Beschreibung jeder Szene von Vertex AI abzurufen
9. Bereinigen
Um unbeabsichtigte Kosten zu vermeiden (z. B. wenn dieser Cloud Run-Job versehentlich häufiger aufgerufen wird als die monatliche Zuweisung für Cloud Run-Aufrufe im kostenlosen Kontingent), können Sie entweder den Cloud Run-Job oder das in Schritt 2 erstellte Projekt löschen.
Wenn Sie den Cloud Run-Job löschen möchten, rufen Sie die Cloud Run-Cloud Console unter https://console.cloud.google.com/run/ auf und löschen Sie die video-describer-job Funktion (oder $JOB_NAME, falls Sie einen anderen Namen verwendet haben).
Wenn Sie das gesamte Projekt löschen möchten, rufen Sie https://console.cloud.google.com/cloud-resource-manager auf, wählen Sie das in Schritt 2 erstellte Projekt aus und klicken Sie auf „Löschen“. Wenn Sie das Projekt löschen, müssen Sie die Projekte in Ihrem Cloud SDK ändern. Sie können die Liste aller verfügbaren Projekte mit dem Befehl gcloud projects list aufrufen.