Cloud Run, Video Intelligence API ve Vertex AI'ı kullanarak tek tek video görüntüsü açıklama hizmeti oluşturma

1. Giriş

Genel Bakış

Bu codelab'de, Node.js'de yazılmış ve videodaki her sahnenin görsel açıklamasını sağlayan bir Cloud Run hizmeti oluşturacaksınız. İlk olarak, hizmetiniz bir sahne değişikliği olduğunda zaman damgalarını algılamak için Video Intelligence API'yi kullanır. Ardından, hizmetiniz her sahne değişikliği zaman damgası için ekran görüntüsü almak üzere ffmpeg adlı bir üçüncü taraf ikili programını kullanır. Son olarak, ekran görüntülerinin görsel açıklamasını sağlamak için Vertex AI görsel altyazı özelliği kullanıldı.

Bu codelab'de, belirli bir zaman damgasında videodan görüntü yakalamak için Cloud Run hizmetinizde ffmpeg'in nasıl kullanılacağı da gösterilmektedir. ffmpeg'in bağımsız olarak yüklenmesi gerektiğinden bu codelab'de, Cloud Run hizmetinizin bir parçası olarak ffmpeg'i yüklemek için nasıl Dockerfile oluşturacağınızı öğreneceksiniz.

Aşağıda, Cloud Run hizmetinin işleyiş şekli gösterilmiştir:

Cloud Run Video Açıklama Hizmeti şeması

Neler öğreneceksiniz?

  • Üçüncü taraf ikili programı yüklemek için Dockerfile kullanarak container görüntüsü oluşturma
  • Diğer Google Cloud hizmetlerini çağırmak amacıyla Cloud Run hizmeti için bir hizmet hesabı oluşturarak en az ayrıcalık ilkesini uygulama
  • Cloud Run hizmetinden Video Intelligence istemci kitaplığını kullanma
  • Vertex AI'dan her sahnenin görsel açıklamasını almak için Google API'lerine çağrı yapma

2. Kurulum ve Gereksinimler

Ön koşullar

  • Cloud Console'a giriş yaptınız.
  • Daha önce bir Cloud Run hizmeti dağıttınız. Örneğin, başlamak için kaynak kodundan web hizmeti dağıtma başlıklı makaledeki adımları uygulayabilirsiniz.

Cloud Shell'i etkinleştirme

  1. Cloud Console'da, Cloud Shell'i etkinleştir d1264ca30785e435.png simgesini tıklayın.

cb81e7c8e34bc8d.png

Cloud Shell'i ilk kez başlatıyorsanız ne olduğunu açıklayan bir ara ekran gösterilir. Ara bir ekran görüntülendiyse Devam'ı tıklayın.

d95252b003979716.png

Temel hazırlık ve Cloud Shell'e bağlanmak yalnızca birkaç dakika sürer.

7833d5e1c5d18f54.png

Gereken tüm geliştirme araçları bu sanal makinede yüklüdür. 5 GB boyutunda kalıcı bir ana dizin sunar ve Google Cloud'da çalışarak ağ performansını ve kimlik doğrulamasını büyük ölçüde iyileştirir. Bu codelab'deki çalışmalarınızın tamamı olmasa bile büyük bir kısmı tarayıcıyla yapılabilir.

Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin proje kimliğinize ayarlandığını göreceksiniz.

  1. Kimlik doğrulamanızın tamamlandığını onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın:
gcloud auth list

Komut çıkışı

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

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. gcloud komutunun projenizi bildiğini onaylamak için Cloud Shell'de aşağıdaki komutu çalıştırın:
gcloud config list project

Komut çıkışı

[core]
project = <PROJECT_ID>

Doğru değilse aşağıdaki komutla ayarlayabilirsiniz:

gcloud config set project <PROJECT_ID>

Komut çıkışı

Updated property [core/project].

3. API'leri Etkinleştirin ve Ortam Değişkenlerini Ayarlayın

Bu codelab'i kullanmaya başlamadan önce etkinleştirmeniz gereken birkaç API vardır. Bu codelab'de aşağıdaki API'lerin kullanılması gerekir. Bu API'leri şu komutu çalıştırarak etkinleştirebilirsiniz:

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

Ardından, bu codelab boyunca kullanılacak ortam değişkenlerini ayarlayabilirsiniz.

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 paketi oluşturma

Aşağıdaki komutla Cloud Run hizmeti tarafından işlenmek üzere video yükleyebileceğiniz bir Cloud Storage paketi oluşturun:

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

[İsteğe bağlı] Bu örnek videoyu yerel olarak indirerek kullanabilirsiniz.

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

Şimdi video dosyanızı depolama paketinize yükleyin.

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

5. Node.js uygulamasını oluşturma

İlk olarak, kaynak kodu için bir dizin oluşturun ve bu dizin için cd'yi kullanın.

mkdir video-describer && cd $_

Ardından, aşağıdaki içeriğe sahip bir package.json dosyası oluşturun:

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

Bu uygulama, okunabilirliği artırmak için çeşitli kaynak dosyalardan oluşur. İlk olarak, aşağıdaki içeriğe sahip bir index.js kaynak dosyası oluşturun. Bu dosya, hizmetin giriş noktasını ve uygulamanın ana mantığını içerir.

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

Sonra, aşağıdaki içeriğe sahip bir schemaDetector.js dosyası oluşturun. Bu dosya, videodaki sahnelerin değiştiğini algılamak için Video Intelligence API'yi kullanır.

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

Şimdi, aşağıdaki içeriğe sahip imageCapture.js adlı bir dosya oluşturun. Bu dosya, ffmpeg komutlarını bir düğüm uygulaması içinden çalıştırmak için fluent-ffmpeg düğüm paketini kullanır.

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

Son olarak, aşağıdaki içeriğe sahip "imageDescriptionr.js" adlı bir dosya oluşturun. Bu dosyada, her sahne görüntüsünün görsel açıklamasını almak için Vertex AI kullanılır.

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 ve .dockerignore dosyası oluşturma

Bu hizmet ffmpeg kullandığından ffmpeg yükleyen bir Dockerfile oluşturmanız gerekir.

Dockerfile adında, aşağıdaki içeriğe sahip bir dosya oluşturun:

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

Belirli dosyaları kapsayıcıya alma işlemini göz ardı etmek için .dockerignore adında bir dosya da oluşturabilirsiniz.

Dockerfile
.dockerignore
node_modules
npm-debug.log

6. Hizmet Hesabı oluşturma

Cloud Storage, Vertex AI ve Video Intelligence API'ye erişmek üzere kullanmak üzere Cloud Run hizmeti için bir hizmet hesabı oluşturacaksınız.

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 hizmetini dağıtma

Artık Cloud Run hizmetinizi otomatik olarak container mimarisine almak için kaynak tabanlı bir dağıtım kullanabilirsiniz.

Not: Cloud Run hizmetleri için varsayılan işleme süresi 60 saniyedir. Önerilen test videosu 2 dakika uzunluğunda olduğu için bu codelab'de 5 dakikalık zaman aşımı kullanılmıştır. Daha uzun süreli bir video kullanıyorsanız zamanı değiştirmeniz gerekebilir.

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

Dağıtımdan sonra hizmet URL'sini bir ortam değişkenine kaydedin.

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

8. Cloud Run hizmetini çağırma

Artık Cloud Storage'a yüklediğiniz videonun adını sağlayarak hizmetinizi çağırabilirsiniz.

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

Sonuçlarınız, aşağıdaki örnek çıkışa benzer bir görünümde olacaktır:

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

Tebrikler, codelab'i tamamladınız.

Video Intelligence API, Cloud Run ve Vertex AI görsel altyazısı ile ilgili belgeleri incelemenizi öneririz.

İşlediklerimiz

  • Üçüncü taraf ikili programı yüklemek için Dockerfile kullanarak container görüntüsü oluşturma
  • Diğer Google Cloud hizmetlerini çağırmak amacıyla Cloud Run hizmeti için bir hizmet hesabı oluşturarak en az ayrıcalık ilkesini uygulama
  • Cloud Run hizmetinden Video Intelligence istemci kitaplığını kullanma
  • Vertex AI'dan her sahnenin görsel açıklamasını almak için Google API'lerine çağrı yapma

10. Temizleme

Yanlışlıkla yapılan ücretleri önlemek için (örneğin, bu Cloud Run hizmeti yanlışlıkla ücretsiz katmandaki aylık Cloud Run çağırma tahsisinizden daha fazla kez çağrıldıysa) Cloud Run hizmetini silebilir veya 2. adımda oluşturduğunuz projeyi silebilirsiniz.

Cloud Run hizmetini silmek için https://console.cloud.google.com/run/ adresinden Cloud Run Cloud Console'a gidip video-describer işlevini (veya farklı bir ad kullandıysanız $SERVICE_NAME) silin.

Projenin tamamını silmeyi tercih ederseniz https://console.cloud.google.com/cloud-resource-manager adresine gidip 2. adımda oluşturduğunuz projeyi, ardından Sil'i seçebilirsiniz. Projeyi silerseniz Cloud SDK'nızdaki projeleri değiştirmeniz gerekir. gcloud projects list komutunu çalıştırarak mevcut tüm projelerin listesini görüntüleyebilirsiniz.