Utwórz usługę opisów obrazów scen po scenie w filmach za pomocą Cloud Run, interfejsu Video Intelligence API i Vertex AI

1. Wprowadzenie

Omówienie

W ramach tego ćwiczenia w programie utworzysz usługę Cloud Run napisaną w Node.js, która zapewnia wizualny opis każdej sceny w filmie. Po pierwsze, usługa używa interfejsu Video Intelligence API do wykrywania sygnatur czasowych po każdej zmianie sceny. Następnie usługa użyje zewnętrznego pliku binarnego o nazwie ffmpeg, aby zrobić zrzut ekranu dla każdej sygnatury czasowej zmiany sceny. Do wizualnego opisu zrzutów ekranu używane są napisy wizualne w Vertex AI.

To ćwiczenie w Codelabs pokazuje również, jak używać plików ffmpeg w usłudze Cloud Run do przechwytywania obrazów z filmu w określonej sygnaturze czasowej. Plik FFmpeg musi być zainstalowany niezależnie, dlatego z tego ćwiczenia w Codelabs dowiesz się, jak utworzyć plik Dockerfile, aby zainstalować FFmpeg w ramach usługi Cloud Run.

Oto ilustracja działania usługi Cloud Run:

Diagram usługi opisu filmu w Cloud Run

Czego się nauczysz

  • Jak utworzyć obraz kontenera za pomocą Dockerfile w celu zainstalowania pliku binarnego innej firmy
  • Jak zachować zgodność z zasadą jak najmniejszych uprawnień przez utworzenie konta usługi, które umożliwi usłudze Cloud Run wywoływanie innych usług Google Cloud
  • Jak używać biblioteki klienta Video Intelligence z usługi Cloud Run
  • Jak wywołać interfejsy API Google w celu uzyskania wizualnego opisu każdej sceny z Vertex AI

2. Konfiguracja i wymagania

Wymagania wstępne

Aktywowanie Cloud Shell

  1. W konsoli Cloud kliknij Aktywuj Cloud Shell d1264ca30785e435.png.

cb81e7c8e34bc8d.png

Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni z opisem tej usługi. Jeśli wyświetlił się ekran pośredni, kliknij Dalej.

d95252b003979716.png

Uzyskanie dostępu do Cloud Shell i połączenie się z nim powinno zająć tylko kilka chwil.

7833d5e1c5d18f54.png

Ta maszyna wirtualna ma wszystkie potrzebne narzędzia dla programistów. Zawiera stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i uwierzytelnianie. Większość zadań w ramach tego ćwiczenia z programowania można wykonać w przeglądarce.

Po nawiązaniu połączenia z Cloud Shell powinno pojawić się potwierdzenie, że użytkownik jest uwierzytelniony, a projekt jest ustawiony na identyfikator Twojego projektu.

  1. Uruchom to polecenie w Cloud Shell, aby potwierdzić, że jesteś uwierzytelniony:
gcloud auth list

Dane wyjściowe polecenia

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Uruchom to polecenie w Cloud Shell, aby sprawdzić, czy polecenie gcloud zna Twój projekt:
gcloud config list project

Dane wyjściowe polecenia

[core]
project = <PROJECT_ID>

Jeśli tak nie jest, możesz go ustawić za pomocą tego polecenia:

gcloud config set project <PROJECT_ID>

Dane wyjściowe polecenia

Updated property [core/project].

3. Włączanie interfejsów API i ustawianie zmiennych środowiskowych

Zanim zaczniesz korzystać z tego ćwiczenia z programowania, musisz włączyć kilka interfejsów API. To ćwiczenie w Codelabs wymaga używania poniższych interfejsów API. Możesz włączyć te interfejsy API, uruchamiając to polecenie:

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

Następnie możesz ustawić zmienne środowiskowe, które będą używane w tym ćwiczeniu z programowania.

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. Tworzenie zasobnika Cloud Storage

Utwórz zasobnik Cloud Storage, do którego możesz przesyłać filmy w celu przetworzenia przez usługę Cloud Run za pomocą tego polecenia:

gsutil mb -l us-central1 gs://$BUCKET_ID/

[Opcjonalnie] Aby użyć tego przykładowego filmu, pobierz go lokalnie.

gsutil cp gs://cloud-samples-data/video/visionapi.mp4 testvideo.mp4

Teraz prześlij plik wideo do zasobnika na dane.

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

5. Tworzenie aplikacji Node.js

Najpierw utwórz katalog na kod źródłowy i cd w tym katalogu.

mkdir video-describer && cd $_

Następnie utwórz plik package.json z tą zawartością:

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

Ta aplikacja składa się z kilku plików źródłowych, które zwiększają czytelność. Najpierw utwórz plik źródłowy index.js z poniższą zawartością. Ten plik zawiera punkt wejścia usługi i główną logikę aplikacji.

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;
}

Następnie utwórz plik sceneDetector.js o następującej zawartości. Ten plik używa interfejsu Video Intelligence API do wykrywania zmian scen w filmie.

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;
}

Teraz utwórz plik o nazwie imageCapture.js z poniższą zawartością. Ten plik używa pakietu węzłów fluent-ffmpeg do uruchamiania poleceń ffmpeg z aplikacji węzła.

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();
            });
    })
}

Na koniec utwórz plik o nazwie „imageDescriber.js” z podaną niżej zawartością. Ten plik korzysta z Vertex AI w celu uzyskania wizualnego opisu każdego obrazu sceny.

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];
    }
}

Tworzenie pliku Dockerfile i plików .dockerignore

Ponieważ ta usługa korzysta z pliku ffmpeg, musisz utworzyć plik Dockerfile, który zainstaluje plik FFmpeg.

Utwórz plik o nazwie Dockerfile zawierający te treści:

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

Utwórz też plik o nazwie .dockerignore, aby ignorować konteneryzację niektórych plików.

Dockerfile
.dockerignore
node_modules
npm-debug.log

6. Utwórz konto usługi

Utworzysz konto usługi Cloud Run, za pomocą którego będziesz mieć dostęp do Cloud Storage, Vertex AI i interfejsu Video Intelligence API.

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. Wdrażanie usługi Cloud Run

Teraz możesz użyć wdrożenia opartego na źródle, aby automatycznie konteneryzować usługę Cloud Run.

Uwaga: domyślny czas przetwarzania w usłudze Cloud Run to 60 sekund. W ramach tego ćwiczenia w Codelabs stosowany jest limit czasu wynoszący 5 minut, ponieważ sugerowany film testowy ma długość 2 minut. Jeśli korzystasz z filmu o dłuższym czasie trwania, być może trzeba będzie zmienić czas.

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

Po wdrożeniu zapisz adres URL usługi w zmiennej środowiskowej.

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

8. Wywoływanie usługi Cloud Run

Teraz możesz wywołać usługę, podając nazwę filmu przesłanego do Cloud Storage.

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

Twoje wyniki powinny wyglądać podobnie do tych poniżej:

[{"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. Gratulacje!

Gratulujemy ukończenia ćwiczeń z programowania.

Zalecamy zapoznanie się z dokumentacją Video Intelligence API, Cloud Run i napisów wizualnych Vertex AI.

Omówione zagadnienia

  • Jak utworzyć obraz kontenera za pomocą Dockerfile w celu zainstalowania pliku binarnego innej firmy
  • Jak zachować zgodność z zasadą jak najmniejszych uprawnień przez utworzenie konta usługi, które umożliwi usłudze Cloud Run wywoływanie innych usług Google Cloud
  • Jak używać biblioteki klienta Video Intelligence z usługi Cloud Run
  • Jak wywołać interfejsy API Google w celu uzyskania wizualnego opisu każdej sceny z Vertex AI

10. Czyszczenie danych

Aby uniknąć niezamierzonych opłat (na przykład jeśli ta usługa Cloud Run została przypadkowo wywołana więcej razy niż miesięczny przydział wywołań Cloud Run na poziomie bezpłatnym), możesz usunąć usługę Cloud Run lub projekt utworzony w kroku 2.

Aby usunąć usługę Cloud Run, otwórz konsolę Cloud Run na stronie https://console.cloud.google.com/run/ i usuń funkcję video-describer (lub nazwę usługi $SERVICE_NAME w przypadku użycia innej nazwy).

Jeśli zdecydujesz się usunąć cały projekt, możesz otworzyć stronę https://console.cloud.google.com/cloud-resource-manager, wybrać projekt utworzony w kroku 2 i kliknąć Usuń. Jeśli usuniesz projekt, musisz zmienić projekty w pakiecie SDK Cloud. Aby wyświetlić listę wszystkich dostępnych projektów, uruchom polecenie gcloud projects list.