1. Wprowadzenie
Przegląd
W tym ćwiczeniu utworzysz zadanie Cloud Run napisane w Node.js, które zawiera wizualny opis każdej sceny w filmie. Najpierw zadanie użyje interfejsu Video Intelligence API do wykrywania sygnatur czasowych, w których zmienia się scena. Następnie zadanie użyje binarnego pliku innej firmy o nazwie ffmpeg, aby zrobić zrzut ekranu dla każdej sygnatury czasowej zmiany sceny. Na koniec funkcja generowania opisów treści wizualnych w Vertex AI służy do tworzenia opisów wizualnych zrzutów ekranu.
W tym ćwiczeniu w Codelabs pokazujemy też, jak używać ffmpeg w zadaniu Cloud Run do przechwytywania obrazów z filmu w danej sygnaturze czasowej. Ponieważ ffmpeg musi być zainstalowany niezależnie, to ćwiczenie pokazuje, jak utworzyć plik Dockerfile, aby zainstalować ffmpeg w ramach zadania Cloud Run.
Oto ilustracja przedstawiająca działanie zadania Cloud Run:

Czego się nauczysz
- Jak utworzyć obraz kontenera za pomocą pliku Dockerfile, aby zainstalować binarny plik innej firmy
- Jak przestrzegać zasady najmniejszych uprawnień, tworząc konto usługi dla zadania Cloud Run, aby wywoływać inne usługi Google Cloud
- Jak używać biblioteki klienta Video Intelligence w zadaniu Cloud Run
- Jak wywołać interfejsy API Google, aby uzyskać opis wizualny każdej sceny z Vertex AI
2. Konfiguracja i wymagania
Wymagania wstępne
- Jesteś zalogowany(-a) w konsoli Google Cloud.
- Usługa Cloud Run została już wdrożona. Na początek możesz na przykład skorzystać z krótkiego wprowadzenia dotyczącego wdrażania usługi sieciowej z kodu źródłowego.
Aktywowanie Cloud Shell
- W konsoli Cloud kliknij Aktywuj Cloud Shell
.

Jeśli uruchamiasz Cloud Shell po raz pierwszy, zobaczysz ekran pośredni z opisem tego środowiska. Jeśli pojawił się ekran pośredni, kliknij Dalej.

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

Ta maszyna wirtualna zawiera wszystkie potrzebne narzędzia dla programistów. Zawiera również stały katalog domowy o pojemności 5 GB i działa w Google Cloud, co znacznie zwiększa wydajność sieci i usprawnia proces uwierzytelniania. Większość zadań w tym ćwiczeniu, a być może wszystkie, możesz wykonać w przeglądarce.
Po połączeniu z Cloud Shell zobaczysz, że uwierzytelnianie zostało już przeprowadzone, a projekt jest już ustawiony na Twój identyfikator projektu.
- Aby potwierdzić, że uwierzytelnianie zostało przeprowadzone, uruchom w Cloud Shell to polecenie:
gcloud auth list
Wynik polecenia
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Aby potwierdzić, że polecenie gcloud zna Twój projekt, uruchom w Cloud Shell to polecenie:
gcloud config list project
Wynik polecenia
[core] project = <PROJECT_ID>
Jeśli nie, możesz go ustawić za pomocą tego polecenia:
gcloud config set project <PROJECT_ID>
Wynik polecenia
Updated property [core/project].
3. Włączanie interfejsów API i ustawianie zmiennych środowiskowych
Zanim zaczniesz korzystać z tego ćwiczenia w Codelabs, musisz włączyć kilka interfejsów API. W tym ćwiczeniu musisz użyć tych 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órych będziesz używać podczas naszych ćwiczeń z programowania.
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. Tworzenie konta usługi
Utworzysz konto usługi, którego zadanie Cloud Run będzie używać do uzyskiwania dostępu do Cloud Storage, Vertex AI i interfejsu Video Intelligence API.
Najpierw utwórz konto usługi.
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run Video Scene Image Describer service account"
Następnie przyznaj kontu usługi dostęp do zasobnika Cloud Storage i interfejsów API Vertex AI.
# 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. Tworzenie zasobnika Cloud Storage
Utwórz zasobnik Cloud Storage, do którego możesz przesyłać filmy do przetworzenia przez zadanie Cloud Run, za pomocą tego polecenia:
gsutil mb -l us-central1 gs://$BUCKET_ID/
[Opcjonalnie] Możesz użyć tego przykładowego filmu, pobierając 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
6. Tworzenie zadania Cloud Run
Najpierw utwórz katalog kodu źródłowego i przejdź do niego.
mkdir video-describer-job && cd $_
Następnie utwórz plik package.json o tej treści:
{
"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"
}
}
Ta aplikacja składa się z kilku plików źródłowych, co poprawia czytelność. Najpierw utwórz plik źródłowy app.js z treścią podaną poniżej. Ten plik zawiera punkt wejścia zadania i główną logikę aplikacji.
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);
});
Następnie utwórz 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" ]
Utwórz też plik o nazwie .dockerignore, aby zignorować konteneryzację niektórych plików.
Dockerfile .dockerignore node_modules npm-debug.log
Teraz utwórz folder o nazwie helpers. Ten folder będzie zawierać 5 plików pomocniczych.
mkdir helpers cd helpers
Następnie utwórz plik sceneDetector.js o tej treś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 tą treścią: Ten plik używa pakietu węzła fluent-ffmpeg do uruchamiania poleceń ffmpeg w 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");
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();
});
});
}
Na koniec utwórz plik o nazwie imageCaptioning.js z tą zawartością: Ten plik korzysta z Vertex AI, aby uzyskać opis wizualny 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 = {
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];
}
};
Utwórz plik o nazwie auth.js. Ten plik będzie używać biblioteki klienta uwierzytelniania Google do uzyskiwania tokena dostępu potrzebnego do bezpośredniego wywoływania punktów końcowych Vertex AI.
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();
}
};
Na koniec utwórz plik o nazwie storage.js. Ten plik będzie używać bibliotek klienta Cloud Storage do pobierania filmu z Cloud Storage.
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. Wdrażanie i wykonywanie zadania Cloud Run
Najpierw upewnij się, że znajdujesz się w katalogu głównym video-describer-job dla tego ćwiczenia.
cd .. && pwd
Następnie możesz użyć tego polecenia, aby wdrożyć zadanie Cloud Run.
gcloud run jobs deploy $JOB_NAME --source . --region $REGION
Teraz możesz uruchomić zadanie Cloud Run, wpisując to polecenie:
gcloud run jobs execute $JOB_NAME
Po zakończeniu zadania możesz uruchomić to polecenie, aby uzyskać link do identyfikatora URI logu. (Możesz też użyć Cloud Console i przejść bezpośrednio do zadań Cloud Run, aby wyświetlić logi).
gcloud run jobs executions describe <JOB_EXECUTION_ID>
W logach powinny pojawić się te dane wyjściowe:
[{ 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. Gratulacje!
Gratulujemy ukończenia ćwiczenia!
Zalecamy zapoznanie się z dokumentacją Video Intelligence API, Cloud Run i Vertex AI do generowania opisów treści wizualnych.
Omówione zagadnienia
- Jak utworzyć obraz kontenera za pomocą pliku Dockerfile, aby zainstalować binarny plik innej firmy
- Jak przestrzegać zasady najmniejszych uprawnień, tworząc konto usługi dla zadania Cloud Run, aby wywoływać inne usługi Google Cloud
- Jak używać biblioteki klienta Video Intelligence w zadaniu Cloud Run
- Jak wywołać interfejsy API Google, aby uzyskać opis wizualny każdej sceny z Vertex AI
9. Czyszczenie danych
Aby uniknąć przypadkowych opłat (np. jeśli to zadanie Cloud Run zostanie przypadkowo wywołane więcej razy niż miesięczny limit wywołań Cloud Run w warstwie bezpłatnej), możesz usunąć zadanie Cloud Run lub projekt utworzony w kroku 2.
Aby usunąć zadanie Cloud Run, otwórz konsolę Cloud Run w Google Cloud pod adresem https://console.cloud.google.com/run/ i usuń funkcję video-describer-job (lub $JOB_NAME, jeśli używasz innej nazwy).
Jeśli zdecydujesz się usunąć cały projekt, otwórz stronę https://console.cloud.google.com/cloud-resource-manager, wybierz projekt utworzony w kroku 2 i kliknij Usuń. Jeśli usuniesz projekt, musisz zmienić projekty w Cloud SDK. Listę wszystkich dostępnych projektów możesz wyświetlić, uruchamiając polecenie gcloud projects list.