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 bietet. Zuerst verwendet Ihr Dienst die Video Intelligence API, um die Zeitstempel zu erkennen, wenn sich eine Szene ändert. Als Nächstes verwendet Ihr Dienst 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-Dienst 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-Dienstes zu installieren.

Hier sehen Sie ein Beispiel dafür, wie der Cloud Run-Dienst funktioniert:

Diagramm: Cloud Run Video Description Service

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-Dienst, um andere Google Cloud-Dienste aufzurufen
  • Video Intelligence-Clientbibliothek aus einem Cloud Run-Dienst 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)')
SERVICE_NAME=video-describer
export BUCKET_ID=$PROJECT_ID-video-describer

4. Cloud Storage-Bucket erstellen

Erstellen Sie einen Cloud Storage-Bucket, in den Sie mit dem folgenden Befehl Videos zur Verarbeitung durch den Cloud Run-Dienst 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

5. Node.js-Anwendung erstellen

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

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

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 viewDetector.js-Datei mit dem folgenden 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 namens imageCapture.js mit 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");
    }
}


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 abschließend eine Datei namens „imagedescriber.js“ mit 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 = {
    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 dieser Dienst ffmpeg verwendet, müssen Sie ein Dockerfile erstellen, das ffmpeg installiert.

Erstellen Sie eine Datei mit dem Namen Dockerfile, die folgenden Inhalt enthält:

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

Außerdem erstellen Sie eine Datei mit dem Namen „.dockerignore“, um die Containerisierung bestimmter Dateien zu ignorieren.

Dockerfile
.dockerignore
node_modules
npm-debug.log

6. Dienstkonto erstellen

Sie erstellen ein Dienstkonto für den Cloud Run-Dienst, um auf Cloud Storage, Vertex AI und die Video Intelligence API zuzugreifen.

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 Container zu verlagern.

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

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

Jetzt können Sie Ihren Dienst 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}

Die Ergebnisse sollten in etwa so aussehen:

[{"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 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-Dienst, um andere Google Cloud-Dienste aufzurufen
  • Video Intelligence-Clientbibliothek aus einem Cloud Run-Dienst verwenden
  • Google APIs aufrufen, um die visuelle Beschreibung jeder Szene aus Vertex AI zu erhalten

10. Bereinigen

Um versehentliche Gebühren zu vermeiden, z. B. wenn dieser Cloud Run-Dienst versehentlich häufiger aufgerufen wird als Ihre monatliche Zuweisung von Cloud Run-Aufrufen in der kostenlosen Stufe, 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 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.