Come utilizzare i job Cloud Run e API Video Intelligence per elaborare i video

1. Introduzione

Panoramica

In questo codelab, creerai un job Cloud Run scritto in Node.js che fornisce una descrizione visiva di ogni scena in un video. In primo luogo, il tuo job utilizzerà l'API Video Intelligence per rilevare i timestamp ogni volta che una scena cambia. Il tuo job userà 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 job 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 job Cloud Run.

Ecco un'illustrazione di come funziona il job Cloud Run:

Illustrazione della descrizione del video del job 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 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

Attiva Cloud Shell

  1. Dalla console Cloud, fai clic su Attiva Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

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.

d95252b003979716.png

Il provisioning e la connessione a Cloud Shell dovrebbero richiedere solo qualche istante.

7833d5e1c5d18f54.png

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.

  1. 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`
  1. 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)')
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 account di servizio

Dovrai creare un account di servizio per il job Cloud Run da utilizzare per accedere a Cloud Storage, Vertex AI e all'API Video Intelligence.

Innanzitutto, crea l'account di servizio.

gcloud iam service-accounts create $SERVICE_ACCOUNT \
  --display-name="Cloud Run Video Scene Image Describer service account"

Concedi quindi all'account di servizio 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 caricare video per l'elaborazione da parte del job 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

6. Crea il job Cloud Run

Per prima cosa, crea una directory per il codice sorgente e accedi a quella directory.

mkdir video-describer-job && cd $_

Quindi, crea un file package.json con i seguenti contenuti:

{
  "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 è composta da diversi file di origine per una migliore leggibilità. Innanzitutto, crea un file sorgente app.js con i contenuti seguenti. 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 l'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à cinque file helper.

mkdir helpers
cd helpers

A questo punto, crea un file sceneDetector.js con i seguenti contenuti. 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 imageCapture.js con i seguenti contenuti. 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");
        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 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 = {
    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 di Google per ottenere il 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 del job Cloud Run ed eseguilo

Innanzitutto, assicurati di essere 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

A questo punto, puoi eseguire il job Cloud Run eseguendo questo comando:

gcloud run jobs execute $JOB_NAME

Al termine dell'esecuzione del job, puoi eseguire il comando seguente per ottenere un link all'URI di log. In alternativa, puoi utilizzare la console Cloud e andare direttamente ai job Cloud Run per visualizzare i log.

gcloud run jobs executions describe <JOB_EXECUTION_ID>

Nei log dovrebbe essere visualizzato il seguente output:

[{ 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!

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

  • 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 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 inavvertitamente richiamato più volte rispetto all'allocazione mensile dei callout di Cloud Run nel livello senza costi), puoi eliminare il job Cloud Run o eliminare il progetto che hai creato nel passaggio 2.

Per eliminare il job Cloud Run, vai alla console Cloud di Cloud Run all'indirizzo https://console.cloud.google.com/run/ ed elimina la funzione video-describer-job (o $JOB_NAME nel caso in cui tu abbia 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.