1. Wprowadzenie
Omówienie
W ramach tego ćwiczenia w programie utworzysz zadanie Cloud Run napisane w środowisku Node.js, które zawiera wizualny opis każdej sceny w filmie. Najpierw Twoje zadanie będzie korzystać z interfejsu Video Intelligence API do wykrywania sygnatur czasowych po każdej zmianie sceny. Następnie Twoje zadanie 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 zadaniu 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ć plik ffmpeg w ramach zadania Cloud Run.
Oto ilustracja działania zadania 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 dla zadania Cloud Run do wywoływania innych usług Google Cloud
- Jak używać biblioteki klienta Video Intelligence z zadania 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
- Jesteś zalogowany w konsoli Google Cloud.
- Masz już wdrożoną usługę Cloud Run. Możesz na przykład zapoznać się z krótkim wprowadzeniem do 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 tej usługi. Jeśli wyświetlił się ekran pośredni, kliknij Dalej.
Uzyskanie dostępu do Cloud Shell i połączenie się z nim powinno zająć tylko kilka chwil.
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.
- 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`
- 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)') 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. Utwórz konto usługi
Utworzysz konto usługi dla zadania Cloud Run, które będzie używane 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 przesłać filmy do przetworzenia przez zadanie 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
6. Tworzenie zadania Cloud Run
Najpierw utwórz katalog na kod źródłowy i cd w tym katalogu.
mkdir video-describer-job && cd $_
Następnie utwórz plik package.json
z tą zawartoś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, które zwiększają czytelność. Najpierw utwórz plik źródłowy app.js
z poniższą zawartością. Ten plik zawiera punkt wejścia zadania i główną funkcję logiczną 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 plik o nazwie .dockerignore
, aby ignorować 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 podanej niżej 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"); 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 poniższą 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 = { 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 uzyskania 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 użyje bibliotek klienta Cloud Storage do pobrania 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 uruchamianie zadania Cloud Run
Najpierw sprawdź, czy jesteś w katalogu głównym video-describer-job
, w którym wykonujesz ćwiczenia z programowania.
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, uruchamiając to polecenie:
gcloud run jobs execute $JOB_NAME
Po zakończeniu zadania możesz uruchomić poniższe polecenie, aby uzyskać link do identyfikatora URI logu. (Możesz też użyć konsoli Cloud i bezpośrednio otworzyć zadania Cloud Run, aby wyświetlić logi).
gcloud run jobs executions describe <JOB_EXECUTION_ID>
W dziennikach 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 ć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 dla zadania Cloud Run do wywoływania innych usług Google Cloud
- Jak używać biblioteki klienta Video Intelligence z zadania Cloud Run
- Jak wywołać interfejsy API Google w celu uzyskania wizualnego opisu każdej sceny z Vertex AI
9. Czyszczenie danych
Aby uniknąć niezamierzonych opłat (na przykład jeśli to zadanie Cloud Run zostało nieumyślnie wywołane więcej razy niż miesięczny przydział wywołań Cloud Run na poziomie bezpłatnym), możesz usunąć zadanie Cloud Run lub projekt utworzony w kroku 2.
Aby usunąć zadanie Cloud Run, otwórz konsolę Cloud Run na stronie https://console.cloud.google.com/run/ i usuń funkcję video-describer-job
(lub nazwę zadania $JOB_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
.