1. Introduzione
Panoramica
In questo codelab, creerai un servizio Cloud Run scritto in Node.js che fornisce una descrizione visiva di ogni scena in un video. In primo luogo, il servizio utilizzerà l'API Video Intelligence per rilevare i timestamp relativi al cambio di scena. Il tuo servizio userà quindi un programma binario di terze parti chiamato ffmpeg per acquisire uno screenshot per ogni timestamp del cambio di scena. Infine, i sottotitoli visivi di Vertex AI vengono utilizzati per fornire una descrizione visiva degli screenshot.
Questo codelab mostra anche come utilizzare ffmpeg all'interno del tuo servizio Cloud Run per acquisire immagini da un video in un determinato timestamp. Poiché ffmpeg deve essere installato in modo indipendente, questo codelab ti mostra come creare un Dockerfile per installare ffmpeg come parte del tuo servizio Cloud Run.
Ecco un'illustrazione di come funziona il servizio Cloud Run:
Cosa imparerai a fare
- Creare un'immagine container utilizzando un Dockerfile per installare un programma 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
- 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.
- Hai già eseguito il deployment di un servizio Cloud Run. Ad esempio, per iniziare, puoi seguire la guida rapida al deployment di un servizio web dal codice sorgente.
Attiva Cloud Shell
- Dalla console Cloud, fai clic su Attiva Cloud Shell
.
Se è la prima volta che avvii Cloud Shell, ti verrà mostrata una schermata intermedia che descrive di cosa si tratta. Se ti è stata presentata una schermata intermedia, fai clic su Continua.
Il provisioning e la connessione a Cloud Shell dovrebbero richiedere solo qualche istante.
Questa macchina virtuale viene 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 di rete e l'autenticazione. Gran parte, se non tutto, del lavoro in questo codelab può essere svolto con un browser.
Una volta stabilita la connessione a Cloud Shell, dovresti vedere che hai eseguito l'autenticazione e che 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 confermare che il comando gcloud è a conoscenza del 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, ci sono diverse API che dovrai abilitare. Questo codelab richiede l'utilizzo delle API seguenti. Puoi abilitare queste API eseguendo questo 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 caricare 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 tuo bucket di archiviazione.
FILENAME=<YOUR-VIDEO-FILENAME> gsutil cp $FILENAME gs://$BUCKET_ID
5. Crea l'app Node.js
Per prima cosa, crea una directory per il codice sorgente e accedi a quella directory.
mkdir video-describer && cd $_
Quindi, 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 seguenti. Questo file contiene il punto di ingresso del 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 il contenuto seguente. Questo file utilizza l'API Video Intelligence per rilevare il cambio di scene 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 image Capture.js con i contenuti riportati di seguito. Questo file utilizza il pacchetto di nodi fluent-ffmpeg per eseguire i comandi ffmpeg dall'interno di un'app del nodo.
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 contenuti seguenti. 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 include 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" ]
Creare un file denominato .dockerignore per ignorare la containerizzazione di determinati file.
Dockerfile .dockerignore node_modules npm-debug.log
6. Creare un account di servizio
Dovrai creare un account di servizio per il servizio Cloud Run da utilizzare 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 sull'origine per containerizzare automaticamente il tuo 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 prova suggerito dura 2 minuti. Se stai utilizzando un video di durata maggiore, potrebbe essere necessario modificare l'ora.
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 tuo 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 seguente:
[{"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 sull'API Video Intelligence, su Cloud Run e sui sottotitoli codificati visivi di Vertex AI.
Argomenti trattati
- Come creare un'immagine container utilizzando un Dockerfile per installare un programma 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
- 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. Esegui la pulizia
Per evitare addebiti involontari (ad esempio, se questo servizio Cloud Run viene inavvertitamente richiamato più volte rispetto all'allocazione mensile dei callout Cloud Run nel livello senza costi), puoi eliminare il servizio Cloud Run o eliminare il progetto che hai creato nel passaggio 2.
Per eliminare il servizio Cloud Run, vai alla console Cloud di Cloud Run all'indirizzo https://console.cloud.google.com/run/ ed elimina la funzione video-describer
(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, dovrai modificarli in Cloud SDK. Puoi visualizzare l'elenco di tutti i progetti disponibili eseguendo gcloud projects list
.