1. Introduzione
Panoramica
In questo codelab, creerai un job Cloud Run scritto in Node.js che fornisce una descrizione visiva di ogni scena di un video. Innanzitutto, il job utilizzerà l'API Video Intelligence per rilevare i timestamp ogni volta che cambia una scena. Successivamente, il job utilizzerà un binario di terze parti chiamato ffmpeg per acquisire uno screenshot per ogni timestamp di cambio scena. Infine, le didascalie visive di Vertex AI vengono utilizzate per fornire una descrizione visiva degli screenshot.
Questo codelab mostra anche come utilizzare ffmpeg all'interno del job Cloud Run per acquisire immagini da un video in un determinato timestamp. Poiché ffmpeg deve essere installato in modo indipendente, questo codelab mostra come creare un Dockerfile per installare ffmpeg come parte del tuo job Cloud Run.
Ecco un'illustrazione del funzionamento del job Cloud Run:

Cosa imparerai a fare
- Creare un'immagine container utilizzando un Dockerfile per installare un binario di terze parti
- Come seguire il principio del privilegio minimo creando un service account per il job Cloud Run per chiamare altri servizi Google Cloud
- Come utilizzare la libreria client Video Intelligence da un job Cloud Run
- Come effettuare una chiamata alle API di Google per ottenere la descrizione visiva di ogni scena da Vertex AI
2. Configurazione e requisiti
Prerequisiti
- Hai eseguito l'accesso a Cloud Console.
- Hai già eseguito il deployment di un servizio Cloud Run. Ad esempio, puoi seguire la guida rapida per il deployment di un servizio web dal codice sorgente per iniziare.
Attiva Cloud Shell
- Nella console Cloud, fai clic su Attiva Cloud Shell
.

Se è la prima volta che avvii Cloud Shell, viene visualizzata una schermata intermedia che ne descrive le funzionalità. Se è stata visualizzata una schermata intermedia, fai clic su Continua.

Bastano pochi istanti per eseguire il provisioning e connettersi a Cloud Shell.

Questa macchina virtuale è caricata con tutti gli strumenti di sviluppo necessari. Offre una home directory permanente da 5 GB e viene eseguita in Google Cloud, migliorando notevolmente le prestazioni e l'autenticazione della rete. Gran parte del lavoro per questo codelab, se non tutto, può essere svolto con un browser.
Una volta eseguita la connessione a Cloud Shell, dovresti vedere che il tuo account è autenticato e il progetto è impostato sul tuo ID progetto.
- Esegui questo comando in Cloud Shell per verificare che l'account sia autenticato:
gcloud auth list
Output comando
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Esegui questo comando in Cloud Shell per verificare che il comando gcloud conosca il tuo progetto:
gcloud config list project
Output comando
[core] project = <PROJECT_ID>
In caso contrario, puoi impostarlo con questo comando:
gcloud config set project <PROJECT_ID>
Output comando
Updated property [core/project].
3. Abilita le API e imposta le variabili di ambiente
Prima di poter iniziare a utilizzare questo codelab, devi abilitare diverse API. Questo codelab richiede l'utilizzo delle seguenti API. Puoi abilitare queste API eseguendo il seguente comando:
gcloud services enable run.googleapis.com \
storage.googleapis.com \
cloudbuild.googleapis.com \
videointelligence.googleapis.com \
aiplatform.googleapis.com
Poi puoi impostare le variabili di ambiente che verranno utilizzate durante questo codelab.
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. Creare un service account
Creerai un service account da utilizzare per accedere a Cloud Storage, Vertex AI e all'API Video Intelligence.
Innanzitutto, crea il service account.
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run Video Scene Image Describer service account"
Poi concedi al service account l'accesso al bucket Cloud Storage e alle API Vertex AI.
# 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. Crea un bucket Cloud Storage
Crea un bucket Cloud Storage in cui puoi caricare i video da elaborare con il job Cloud Run con il comando seguente:
gsutil mb -l us-central1 gs://$BUCKET_ID/
[Facoltativo] Puoi utilizzare questo video di esempio scaricandolo in locale.
gsutil cp gs://cloud-samples-data/video/visionapi.mp4 testvideo.mp4
Ora carica il file video nel bucket di archiviazione.
FILENAME=<YOUR-VIDEO-FILENAME> gsutil cp $FILENAME gs://$BUCKET_ID
6. Crea il job Cloud Run
Innanzitutto, crea una directory per il codice sorgente e accedi tramite cd.
mkdir video-describer-job && cd $_
Quindi, crea un file package.json con il seguente contenuto:
{
"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"
}
}
Questa app è costituita da diversi file di origine per una migliore leggibilità. Innanzitutto, crea un file di origine app.js con il seguente contenuto. Questo file contiene il punto di ingresso del job e la logica principale dell'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);
});
A questo punto, crea 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" ]
Crea un file denominato .dockerignore per ignorare la containerizzazione di determinati file.
Dockerfile .dockerignore node_modules npm-debug.log
Ora crea una cartella denominata helpers. Questa cartella conterrà 5 file di supporto.
mkdir helpers cd helpers
Poi crea un file sceneDetector.js con il seguente contenuto. Questo file utilizza l'API Video Intelligence per rilevare i cambi di scena nel video.
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;
}
Ora crea un file denominato imageCapture.js con il seguente contenuto. Questo file utilizza il pacchetto di nodi fluent-ffmpeg per eseguire i comandi ffmpeg dall'interno di un'app di nodi.
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();
});
});
}
Infine, crea un file denominato imageCaptioning.js con il seguente contenuto. Questo file utilizza Vertex AI per ottenere una descrizione visiva di ogni immagine della scena.
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];
}
};
Crea un file denominato auth.js. Questo file utilizzerà la libreria client di autenticazione Google per ottenere un token di accesso necessario per chiamare direttamente gli endpoint Vertex AI.
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();
}
};
Infine, crea un file denominato storage.js. Questo file utilizzerà le librerie client di Cloud Storage per scaricare un video da Cloud Storage.
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. Esegui il deployment ed esegui il job Cloud Run
Innanzitutto, assicurati di trovarti nella directory principale video-describer-job del codelab.
cd .. && pwd
Quindi, puoi utilizzare questo comando per eseguire il deployment del job Cloud Run.
gcloud run jobs deploy $JOB_NAME --source . --region $REGION
Ora puoi eseguire il job Cloud Run eseguendo questo comando:
gcloud run jobs execute $JOB_NAME
Una volta terminata l'esecuzione del job, puoi eseguire il seguente comando per ottenere un link all'URI del log. In alternativa, puoi utilizzare la console Cloud e andare direttamente a Cloud Run Jobs per visualizzare i log.
gcloud run jobs executions describe <JOB_EXECUTION_ID>
Nei log dovrebbe essere visualizzato l'output seguente:
[{ 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. Complimenti!
Congratulazioni per aver completato il codelab.
Ti consigliamo di consultare la documentazione relativa all'API Video Intelligence, a Cloud Run e alla didascalia visiva di Vertex AI.
Argomenti trattati
- Creare un'immagine container utilizzando un Dockerfile per installare un binario di terze parti
- Come seguire il principio del privilegio minimo creando un service account per il job Cloud Run per chiamare altri servizi Google Cloud
- Come utilizzare la libreria client Video Intelligence da un job Cloud Run
- Come effettuare una chiamata alle API di Google per ottenere la descrizione visiva di ogni scena da Vertex AI
9. Esegui la pulizia
Per evitare addebiti involontari (ad esempio, se questo job Cloud Run viene richiamato inavvertitamente più volte rispetto all'allocazione mensile di chiamate Cloud Run nel livello senza costi), puoi eliminare il job Cloud Run o il progetto che hai creato nel passaggio 2.
Per eliminare il job Cloud Run, vai alla console Cloud Run all'indirizzo https://console.cloud.google.com/run/ ed elimina la funzione video-describer-job (o $JOB_NAME se hai utilizzato un nome diverso).
Se scegli di eliminare l'intero progetto, puoi andare alla pagina https://console.cloud.google.com/cloud-resource-manager, selezionare il progetto che hai creato nel passaggio 2 e scegliere Elimina. Se elimini il progetto, dovrai cambiare progetto in Cloud SDK. Puoi visualizzare l'elenco di tutti i progetti disponibili eseguendo gcloud projects list.