1. Introduzione
Panoramica
In questo codelab, creerai un servizio Cloud Run scritto in Node.js che fornisce una descrizione visiva di ogni scena di un video. Innanzitutto, il servizio utilizzerà l'API Video Intelligence per rilevare i timestamp ogni volta che una scena cambia. Successivamente, il servizio utilizzerà un file binario di terze parti chiamato ffmpeg per acquisire uno screenshot per ogni timestamp di cambio di scena. Infine, viene utilizzata la didascalia visiva di Vertex AI per fornire una descrizione visiva degli screenshot.
Questo codelab mostra anche come utilizzare ffmpeg all'interno del servizio 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 servizio Cloud Run.
Di seguito è riportata un'illustrazione del funzionamento del servizio Cloud Run:

Cosa imparerai a fare
- Come creare un'immagine container utilizzando un Dockerfile per installare un file binario di terze parti
- Come seguire il principio del privilegio minimo creando un account di servizio per il servizio Cloud Run per chiamare altri servizi Google Cloud
- Come utilizzare la libreria client Video Intelligence da un servizio 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 alla console Cloud.
- In precedenza hai eseguito il deployment di un servizio Cloud Run. Ad esempio, puoi seguire la guida rapida Eseguire 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 hai visualizzato 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 persistente di 5 GB ed è in esecuzione in Google Cloud, migliorando notevolmente le prestazioni di rete e l'autenticazione. 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 in questo codelab.
REGION=<YOUR-REGION> PROJECT_ID=<YOUR-PROJECT-ID> PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') SERVICE_NAME=video-describer export BUCKET_ID=$PROJECT_ID-video-describer
4. Crea un bucket Cloud Storage
Crea un bucket Cloud Storage in cui puoi caricare i video per l'elaborazione da parte del servizio Cloud Run con il seguente comando:
gsutil mb -l us-central1 gs://$BUCKET_ID/
[Facoltativo] Puoi utilizzare questo video di esempio scaricandolo localmente.
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
5. Crea l'app Node.js
Innanzitutto, crea una directory per il codice sorgente e passa a questa directory.
mkdir video-describer && cd $_
Poi, crea un file package.json con i seguenti contenuti:
{
"name": "video-describer",
"version": "1.0.0",
"private": true,
"description": "describes the image in every scene for a given video",
"main": "index.js",
"author": "Google LLC",
"license": "Apache-2.0",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"@google-cloud/storage": "^7.7.0",
"@google-cloud/video-intelligence": "^5.0.1",
"axios": "^1.6.2",
"express": "^4.18.2",
"fluent-ffmpeg": "^2.1.2",
"google-auth-library": "^9.4.1"
}
}
Questa app è composta da diversi file di origine per una migliore leggibilità. Innanzitutto, crea un file di origine index.js con i contenuti riportati di seguito. Questo file contiene il punto di ingresso per il servizio e la logica principale dell'app.
const { captureImages } = require('./imageCapture.js');
const { detectSceneChanges } = require('./sceneDetector.js');
const transcribeScene = require('./imageDescriber.js');
const { Storage } = require('@google-cloud/storage');
const fs = require('fs').promises;
const path = require('path');
const express = require('express');
const app = express();
const bucketName = process.env.BUCKET_ID;
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, () => {
console.log(`video describer service ready: listening on port ${port}`);
});
// entry point for the service
app.get('/', async (req, res) => {
try {
// download the requested video from Cloud Storage
let videoFilename = req.query.filename;
console.log("processing file: " + videoFilename);
// download the file to locally to the Cloud Run instance
let localFilename = await downloadVideoFile(videoFilename);
// detect all the scenes in the video & save timestamps to an array
let timestamps = await detectSceneChanges(localFilename);
console.log("Detected scene changes at the following timestamps: ", timestamps);
// create an image of each scene change
// and save to a local directory called "output"
await captureImages(localFilename, timestamps);
// get an access token for the Service Account to call the Google APIs
let accessToken = await transcribeScene.getAccessToken();
console.log("got an access token");
let imageBaseName = path.parse(localFilename).name;
// the data structure for storing the scene description and timestamp
// e.g. an array of json objects {timestamp: 1, description: "..."}, etc.
let scenes = []
// for each timestamp, send the image to Vertex AI
console.log("getting Vertex AI description all the timestamps");
scenes = await Promise.all(
timestamps.map(async (timestamp) => {
let filepath = path.join("./output", imageBaseName + "-" + timestamp + ".png");
// get the base64 encoded image
const encodedFile = await fs.readFile(filepath, 'base64');
// send each screenshot to Vertex AI for description
let description = await transcribeScene.transcribeScene(accessToken, encodedFile)
return { timestamp: timestamp, description: description };
}));
console.log("finished collecting all the scenes");
//console.log(scenes);
return res.json(scenes);
} catch (error) {
//return an error
console.log("received error: ", error);
return res.status(500).json("an internal error occurred");
}
});
async function downloadVideoFile(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;
}
Quindi, crea un file sceneDetector.js con i seguenti contenuti. Questo file utilizza l'API Video Intelligence per rilevare quando le scene cambiano 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 i seguenti contenuti. Questo file utilizza il pacchetto node fluent-ffmpeg per eseguire i comandi ffmpeg da un'app node.
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");
}
}
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 `imageDescriber.js`` con i seguenti contenuti. 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 = {
getAccessToken: async function () {
return await auth.getAccessToken();
},
transcribeScene: async function(token, encodedFile) {
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 Dockerfile e un file .dockerignore
Poiché questo servizio utilizza ffmpeg, devi creare un Dockerfile che installi ffmpeg.
Crea un file denominato Dockerfile che contenga i seguenti contenuti:
# 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 web service 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
6. Creare un account di servizio
Creerai un account di servizio che il servizio Cloud Run utilizzerà per accedere a Cloud Storage, Vertex AI e all'API Video Intelligence.
SERVICE_ACCOUNT="cloud-run-video-description" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run Video Scene Image Describer service account" # 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
7. Esegui il deployment del servizio Cloud Run
Ora puoi utilizzare un deployment basato sul codice sorgente per containerizzare automaticamente il servizio Cloud Run.
Nota: il tempo di elaborazione predefinito per un servizio Cloud Run è di 60 secondi. Questo codelab utilizza un timeout di 5 minuti perché il video di test suggerito dura 2 minuti. Potresti dover modificare il tempo se utilizzi un video di durata maggiore.
gcloud run deploy $SERVICE_NAME \ --region=$REGION \ --set-env-vars BUCKET_ID=$BUCKET_ID \ --no-allow-unauthenticated \ --service-account $SERVICE_ACCOUNT_ADDRESS \ --timeout=5m \ --source=.
Una volta eseguito il deployment, salva l'URL del servizio in una variabile di ambiente.
SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --platform managed --region $REGION --format 'value(status.url)')
8. Chiama il servizio Cloud Run
Ora puoi chiamare il servizio fornendo il nome del video che hai caricato in Cloud Storage.
curl -X GET -H "Authorization: Bearer $(gcloud auth print-identity-token)" ${SERVICE_URL}?filename=${FILENAME}
I risultati dovrebbero essere simili all'output di esempio riportato di seguito:
[{"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"}]
9. Complimenti!
Complimenti 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
- Come creare un'immagine container utilizzando un Dockerfile per installare un file binario di terze parti
- Come seguire il principio del privilegio minimo creando un account di servizio per il servizio Cloud Run per chiamare altri servizi Google Cloud
- Come utilizzare la libreria client Video Intelligence da un servizio Cloud Run
- Come effettuare una chiamata alle API di Google per ottenere la descrizione visiva di ogni scena da Vertex AI
10. Libera spazio
Per evitare addebiti involontari (ad esempio, se questo servizio Cloud Run viene richiamato inavvertitamente più volte di quanto consentito dalla quota mensile di richiami di Cloud Run nel livello senza costi), puoi eliminare il servizio Cloud Run o il progetto che hai creato nel passaggio 2.
Per eliminare il servizio Cloud Run, vai alla console Cloud Run all'indirizzo https://console.cloud.google.com/run/ ed elimina la video-describer funzione (o $SERVICE_NAME se hai utilizzato un nome diverso).
Se scegli di eliminare l'intero progetto, puoi andare all'indirizzo https://console.cloud.google.com/cloud-resource-manager, selezionare il progetto che hai creato nel passaggio 2 e scegliere Elimina. Se elimini il progetto, devi cambiare progetto in Cloud SDK. Puoi visualizzare l'elenco di tutti i progetti disponibili eseguendo gcloud projects list.