Cloud Run-Jobs verwenden Video Intelligence APIs zur Videoverarbeitung

1. Einführung

Übersicht

In diesem Codelab erstellen Sie einen Cloud Run-Job, der in Node.js geschrieben ist und eine visuelle Beschreibung jeder Szene in einem Video bietet. Zuerst verwendet Ihr Job die Video Intelligence API, um die Zeitstempel für den Zeitpunkt eines Szenenwechsels zu erkennen. Als Nächstes verwendet Ihr Job ein Drittanbieter-Binärprogramm namens ffmpeg, um für jeden Szenenwechselzeitstempel einen Screenshot zu erstellen. Schließlich werden visuelle Untertitel in Vertex AI verwendet, um eine visuelle Beschreibung der Screenshots bereitzustellen.

In diesem Codelab wird außerdem gezeigt, wie Sie ffmpeg in Ihrem Cloud Run-Job verwenden, um Bilder aus einem Video mit einem bestimmten Zeitstempel zu erfassen. Da ffmpeg unabhängig installiert werden muss, erfahren Sie in diesem Codelab, wie Sie ein Dockerfile erstellen, um ffmpeg als Teil Ihres Cloud Run-Jobs zu installieren.

Hier ist ein Beispiel dafür, wie der Cloud Run-Job funktioniert:

Abbildung der Videobeschreibung für Cloud Run-Job

Aufgaben in diesem Lab

  • Container-Image mit einem Dockerfile erstellen und ein Drittanbieter-Binärprogramm installieren
  • Anleitung zum Prinzip der geringsten Berechtigung durch Erstellen eines Dienstkontos für den Cloud Run-Job, um andere Google Cloud-Dienste aufzurufen
  • Video Intelligence-Clientbibliothek aus einem Cloud Run-Job verwenden
  • Google APIs aufrufen, um die visuelle Beschreibung jeder Szene aus Vertex AI zu erhalten

2. Einrichtung und Anforderungen

Voraussetzungen

Cloud Shell aktivieren

  1. Klicken Sie in der Cloud Console auf Cloud Shell aktivieren d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Wenn Sie Cloud Shell zum ersten Mal starten, wird ein Zwischenbildschirm mit einer Beschreibung der Funktion angezeigt. Wenn ein Zwischenbildschirm angezeigt wird, klicken Sie auf Weiter.

d95252b003979716.png

Die Bereitstellung und Verbindung mit Cloud Shell dauert nur einen Moment.

7833d5e1c5d18f54.png

Diese virtuelle Maschine verfügt über alle erforderlichen Entwicklertools. Es bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und wird in Google Cloud ausgeführt. Dadurch werden die Netzwerkleistung und die Authentifizierung erheblich verbessert. Viele, wenn nicht sogar alle Arbeiten in diesem Codelab können mit einem Browser erledigt werden.

Sobald Sie mit Cloud Shell verbunden sind, sollten Sie sehen, dass Sie authentifiziert sind und das Projekt auf Ihre Projekt-ID eingestellt ist.

  1. Führen Sie in 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`
  1. Führen Sie in 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 dieses Codelab verwenden können, müssen Sie mehrere APIs aktivieren. Für dieses Codelab müssen die folgenden APIs verwendet werden. Sie können diese APIs aktivieren, indem Sie den folgenden Befehl ausführen:

gcloud services enable run.googleapis.com \
    storage.googleapis.com \
    cloudbuild.googleapis.com \
    videointelligence.googleapis.com \
    aiplatform.googleapis.com

Dann 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 Sie auf Cloud Storage, Vertex AI und die Video Intelligence API zugreifen können.

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 zur Verarbeitung durch den Cloud Run-Job hochladen können:

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 nun die Videodatei in Ihren Storage-Bucket hoch.

FILENAME=<YOUR-VIDEO-FILENAME>
gsutil cp $FILENAME gs://$BUCKET_ID

6. Cloud Run-Job erstellen

Erstellen Sie zunächst ein Verzeichnis für den Quellcode und speichern Sie das Verzeichnis mit cd.

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 Anwendung besteht aus mehreren Quelldateien, um die Lesbarkeit zu verbessern. Erstellen Sie zuerst eine app.js-Quelldatei mit folgendem Inhalt. Diese Datei enthält den Einstiegspunkt für den Job und die Hauptlogik für die Anwendung.

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 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 außerdem eine Datei mit dem Namen .dockerignore, um die Containerisierung bestimmter Dateien zu ignorieren.

Dockerfile
.dockerignore
node_modules
npm-debug.log

Erstellen Sie jetzt einen Ordner mit dem Namen helpers. Dieser Ordner enthält 5 Hilfsdateien.

mkdir helpers
cd helpers

Erstellen Sie als Nächstes eine sceneDetector.js-Datei mit folgendem Inhalt. Für diese Datei wird die Video Intelligence API verwendet, um zu erkennen, wenn sich Szenen im Video ändern.

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 nun eine Datei mit dem Namen imageCapture.js und folgendem Inhalt. Diese Datei verwendet das Knotenpaket fluent-ffmpeg, um ffmpeg-Befehle in einer Knotenanwendung 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 zuletzt eine Datei mit dem Namen imageCaptioning.js und folgendem Inhalt. Diese Datei verwendet Vertex AI, um eine visuelle Beschreibung jedes Szenenbilds 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. Diese Datei verwendet die Authentifizierungs-Clientbibliothek von Google, um ein Zugriffstoken abzurufen, das für den direkten Aufruf 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 zuletzt eine Datei mit dem Namen storage.js. Diese Datei verwendet die Cloud Storage-Clientbibliotheken, 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

Anschließend können Sie diesen Befehl verwenden, um den Cloud Run-Job bereitzustellen.

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 die Cloud Console verwenden und direkt zu Cloud Run-Jobs wechseln, um die Logs aufzurufen.)

gcloud run jobs executions describe <JOB_EXECUTION_ID>

In den Logs sollten Sie die folgende Ausgabe sehen:

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

Herzlichen Glückwunsch zum Abschluss des Codelabs!

Wir empfehlen Ihnen, die Dokumentation zur Video Intelligence API, zu Cloud Run und zur visuellen Untertitelung mit Vertex AI zu lesen.

Behandelte Themen

  • Container-Image mit einem Dockerfile erstellen und ein Drittanbieter-Binärprogramm installieren
  • Anleitung zum Prinzip der geringsten Berechtigung durch Erstellen eines Dienstkontos für den Cloud Run-Job, um andere Google Cloud-Dienste aufzurufen
  • Video Intelligence-Clientbibliothek aus einem Cloud Run-Job verwenden
  • Google APIs aufrufen, um die visuelle Beschreibung jeder Szene aus Vertex AI zu erhalten

9. Bereinigen

Um versehentliche Kosten zu vermeiden, z. B. wenn dieser Cloud Run-Job versehentlich häufiger aufgerufen wird als Ihre monatliche Zuweisung von Cloud Run-Aufrufen in der kostenlosen Stufe, 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 Funktion video-describer-job (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.