Mit Cloud Run, Video Intelligence API und Vertex AI einen Dienst für eine Szene für Szene-Bildbeschreibung erstellen

1. Einführung

Übersicht

In diesem Codelab erstellen Sie einen in Node.js geschriebenen Cloud Run-Dienst, der eine visuelle Beschreibung jeder Szene in einem Video liefert. Zuerst verwendet Ihr Dienst die Video Intelligence API, um die Zeitstempel für Szenenwechsel zu erkennen. Als Nächstes verwendet Ihr Dienst eine Drittanbieter-Binärdatei namens ffmpeg, um für jeden Zeitstempel für Szenenwechsel einen Screenshot zu erstellen. Schließlich wird die visuelle Untertitelung von Vertex AI verwendet, um eine visuelle Beschreibung der Screenshots zu liefern.

In diesem Codelab wird auch gezeigt, wie Sie ffmpeg in Ihrem Cloud Run-Dienst verwenden, um Bilder aus einem Video zu einem bestimmten Zeitstempel zu erfassen. Da ffmpeg unabhängig installiert werden muss, wird in diesem Codelab gezeigt, wie Sie ein Dockerfile erstellen, um ffmpeg als Teil Ihres Cloud Run-Dienstes zu installieren.

Hier sehen Sie eine Abbildung der Funktionsweise des Cloud Run-Dienstes:

Diagramm des Cloud Run Video Description Service

Lerninhalte

  • Container-Image mit einem Dockerfile erstellen, um eine Binärdatei eines Drittanbieters zu installieren
  • Grundsatz der geringsten Berechtigung anwenden, indem Sie ein Dienstkonto für den Cloud Run-Dienst erstellen, um andere Google Cloud-Dienste aufzurufen
  • Video Intelligence-Clientbibliothek in einem Cloud Run-Dienst verwenden
  • Google-APIs aufrufen, um die visuelle Beschreibung jeder Szene von Vertex AI abzurufen

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 Fenster mit einer Beschreibung eingeblendet. Klicken Sie in diesem Fall einfach auf Weiter.

d95252b003979716.png

Das Herstellen der Verbindung mit der Cloud Shell sollte nur wenige Augenblicke dauern.

7833d5e1c5d18f54.png

Auf dieser virtuellen Maschine sind alle erforderlichen Entwicklungstools installiert. Sie bietet ein Basisverzeichnis mit 5 GB nichtflüchtigem Speicher und läuft in Google Cloud, was die Netzwerkleistung und Authentifizierung erheblich verbessert. Die meisten, wenn nicht sogar alle Aufgaben in diesem Codelab können mit einem Browser erledigt werden.

Sobald die Verbindung mit der Cloud Shell hergestellt ist, sehen Sie, dass Sie authentifiziert sind und für das Projekt Ihre Projekt-ID eingestellt ist.

  1. Führen Sie in der 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 den folgenden Befehl in Cloud Shell aus, um zu bestätigen, dass 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 mit diesem Codelab beginnen können, müssen Sie mehrere APIs aktivieren. Für dieses Codelab müssen Sie die folgenden APIs verwenden. Sie können diese APIs mit dem folgenden Befehl aktivieren:

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

Anschließend 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)')
SERVICE_NAME=video-describer
export BUCKET_ID=$PROJECT_ID-video-describer

4. Cloud Storage-Bucket erstellen

Erstellen Sie mit dem folgenden Befehl einen Cloud Storage-Bucket, in den Sie Videos hochladen können, die vom Cloud Run-Dienst verarbeitet werden sollen:

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 Ihre Videodatei jetzt in Ihren Speicher-Bucket hoch.

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

5. Node.js-Anwendung erstellen

Erstellen Sie zuerst ein Verzeichnis für den Quellcode und wechseln Sie in dieses Verzeichnis.

mkdir video-describer && cd $_

Erstellen Sie dann eine package.json-Datei mit folgendem Inhalt:

{
  "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"
  }
}

Diese App besteht aus mehreren Quellcode-Dateien, um die Lesbarkeit zu verbessern. Erstellen Sie zuerst eine index.js-Quelldatei mit dem folgenden Inhalt. Diese Datei enthält den Einstiegspunkt für den Dienst und die Hauptlogik für die 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;
}

Erstellen Sie als Nächstes eine Datei namens „sceneDetector.js“ mit folgendem Inhalt. In dieser Datei wird die Video Intelligence API verwendet, um Szenenwechsel im Video zu erkennen.

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 dem folgenden Inhalt. In dieser Datei wird das Knotenpaket fluent-ffmpeg verwendet, um ffmpeg-Befehle aus einer Knoten-App heraus 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");
    }
}


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 schließlich eine Datei mit dem Namen „imageDescriber.js“ mit folgendem Inhalt. In dieser Datei wird Vertex AI verwendet, um eine visuelle Beschreibung der einzelnen Szenenbilder 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 = {
    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];
    }
}

Dockerfile und .dockerignore-Datei erstellen

Da für diesen Dienst ffmpeg verwendet wird, müssen Sie ein Dockerfile erstellen, in dem ffmpeg installiert wird.

Erstellen Sie eine Datei mit dem Namen Dockerfile und mit folgendem Inhalt:

# 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" ]

Erstellen Sie eine Datei mit dem Namen „.dockerignore“, um bestimmte Dateien beim Containerisieren zu ignorieren.

Dockerfile
.dockerignore
node_modules
npm-debug.log

6. Dienstkonto erstellen

Sie erstellen ein Dienstkonto für den Cloud Run-Dienst, mit dem auf Cloud Storage, Vertex AI und die Video Intelligence API zugegriffen wird.

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. Cloud Run-Dienst bereitstellen

Jetzt können Sie eine quellbasierte Bereitstellung verwenden, um Ihren Cloud Run-Dienst automatisch in einen Container zu packen.

Hinweis: Die Standardverarbeitungszeit für einen Cloud Run-Dienst beträgt 60 Sekunden. In diesem Codelab wird ein 5-Minuten-Zeitlimit verwendet, da das empfohlene Testvideo 2 Minuten lang ist. Wenn Sie ein Video mit einer längeren Dauer verwenden, müssen Sie die Zeit möglicherweise anpassen.

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=.

Speichern Sie nach der Bereitstellung die Dienst-URL in einer Umgebungsvariablen.

SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --platform managed --region $REGION --format 'value(status.url)')

8. Cloud Run-Dienst aufrufen

Sie können den Dienst jetzt aufrufen, indem Sie den Namen des Videos angeben, das Sie in Cloud Storage hochgeladen haben.

curl -X GET -H "Authorization: Bearer $(gcloud auth print-identity-token)" ${SERVICE_URL}?filename=${FILENAME}

Ihre Ergebnisse sollten der Beispielausgabe unten ähneln:

[{"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. Glückwunsch!

Herzlichen Glückwunsch zum Abschluss des Codelabs!

Wir empfehlen, die Dokumentation zur Video Intelligence API, zu Cloud Run und zur Vertex AI-Funktion zum Erstellen von Bildunterschriften zu lesen.

Behandelte Themen

  • Container-Image mit einem Dockerfile erstellen, um eine Binärdatei eines Drittanbieters zu installieren
  • Grundsatz der geringsten Berechtigung anwenden, indem Sie ein Dienstkonto für den Cloud Run-Dienst erstellen, um andere Google Cloud-Dienste aufzurufen
  • Video Intelligence-Clientbibliothek in einem Cloud Run-Dienst verwenden
  • Google-APIs aufrufen, um die visuelle Beschreibung jeder Szene von Vertex AI abzurufen

10. Bereinigen

Um unbeabsichtigte Gebühren zu vermeiden (z. B. wenn dieser Cloud Run-Dienst versehentlich öfter aufgerufen wird als Ihre monatliche Cloud Run-Aufrufkontingent im kostenlosen Kontingent), können Sie entweder den Cloud Run-Dienst oder das in Schritt 2 erstellte Projekt löschen.

Wenn Sie den Cloud Run-Dienst 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 (oder $SERVICE_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 Projekt aus, das Sie in Schritt 2 erstellt haben, und klicken Sie auf „Löschen“. Wenn Sie das Projekt löschen, müssen Sie das Projekt in Ihrem Cloud SDK ändern. Sie können die Liste aller verfügbaren Projekte mit gcloud projects list aufrufen.