Google 在 Gemini API、Gemini in Vertex AI 以及 Gemini AI Studio 中發佈了用於 AI 影片生成的 Veo 3.1 Lite 模型。此模型解決了開發者常見的痛點:快速且低成本地生成高品質影片。
在這篇部落格文章中,我將應用程式遷移至使用 Veo 3.1 Lite 模型,並實作一個新的 Firebase Cloud Function,使用 GenAI TypeScript SDK 來擴展影片。
該應用程式支援圖生影片(image-to-video)、使用首尾影格的影片插補(video interpolation),以及擴展 Veo 影片。
專案的技術堆疊:
http://localhost:5001 本地測試函式。公開的 Google AI Studio API 在我所在的地區(香港)受到限制。然而,Vertex AI (Google Cloud) 提供了可在這裡穩定運作的企業級存取,因此我選擇在本次展示中使用 Vertex AI。
npm i -g firebase-tools
使用 npm 全域安裝 firebase-tools。
firebase logout
firebase login
登出 Firebase 並重新登入以執行正確的 Firebase 驗證。
firebase init
執行 firebase init 並按照提示設定 Firebase Cloud Functions、Firebase Local Emulator Suite、Firebase Cloud Storage 和 Firebase Remote Config。
如果您有現有的專案或多個專案,可以在命令列中指定專案 ID。
firebase init --project <PROJECT_ID>
在上述兩種情況下,Firebase CLI 都會自動安裝 firebase-admin 和 firebase-functions 相依性。
完成設定步驟後,Firebase 工具會生成函式模擬器、函式、儲存規則檔案、遠端設定範本以及設定檔(如 .firebaserc 和 firebase.json)。
npm i firebase
Angular 應用程式需要 firebase 相依性來初始化 Firebase 應用程式、載入遠端設定並調用 Firebase Cloud Functions 來生成影片。
npm i @cfworker/json-schema @google/genai @modelcontextprotocol/sdk
安裝上述相依性以存取 Gemini in Vertex AI。@google/genai 依賴於 @cfworker/json-schema 和 @modelcontextprotocol/sdk。若缺少這些,Cloud Functions 將無法啟動。
專案配置完成後,讓我們看看前端和後端是如何通訊的。

前端應用程式是使用 Angular 建置的。它依賴 Firebase AI Logic,使用 Gemini 3.1 Flash Image Preview 模型生成圖像。接著,將文字提示詞(prompt)和圖像提交給 Firebase Cloud Function 以生成影片,將其儲存在 Firebase Cloud Storage 儲存桶中,並向用戶端傳回 GCS URI、MIME 類型和 HTTP URL。
當用戶端擴展 Veo 影片時,它會將提示詞、GCS URI 和 MIME 類型提供給另一個 Firebase Cloud Function,以生成擴展後的影片,將其儲存在 Firebase Cloud Storage 儲存桶中,並向用戶端傳回 GCS URI、MIME 類型和 HTTP URL。
同樣地,HTML 影片播放器元件會在用戶端應用程式中播放該 HTTP URL。
| 差異項目 | Gemini AI Studio | Gemini in Vertex AI |
|---|---|---|
| 模型名稱 | veo-3.1-lite-generate-preview | veo-3.1-lite-generate-001 |
| 擴展次數 | 每次擴展 7 秒,最多可擴展 20 次 | 每次擴展 7 秒,最多可擴展 4 次 |
| 影片長度 | 可擴展至 141 秒,總長度為 148 秒 | 可擴展至 28 秒,總長度為 36 秒 |
這項限制不會影響我們的應用程式,因為展示僅專注於影片擴展,且生成的影片解析度已硬編碼(hardcoded)為 720p。
我在 Firebase 專案中定義了環境變數。這確保了函式知道用於儲存、函式託管的地區,以及用於影片生成的 Veo 模型。
.env.example
GOOGLE_CLOUD_LOCATION="us-central1"
GEMINI_VIDEO_MODEL_NAME="veo-3.1-lite-generate-001"
IS_VEO31_USED="true"
POLLING_PERIOD_MS="10000"
GOOGLE_FUNCTION_LOCATION="us-central1"
WHITELIST="http://localhost:4200"
REFERER="http://localhost:4200/"
| 變數 | 描述 |
|---|---|
| GOOGLE_CLOUD_LOCATION | 儲存桶的位置。我選擇 us-central1,因為該地區的儲存桶始終免費。 |
| GEMINI_VIDEO_MODEL_NAME | Gemini 影片模型的名稱。 |
| IS_VEO31_USED | 是否使用 Veo 3.1。如果為 false,則會退而生成影片而非插補。 |
| POLLING_PERIOD_MS | 影片操作的輪詢週期(以毫秒為單位)。 |
| GOOGLE_FUNCTION_LOCATION | Cloud Functions 的區域。我選擇 us-central1 以使函式和儲存桶位於同一區域。 |
| WHITELIST | 請求必須來自 http://localhost:4200。 |
| REFERER | 請求源自 http://localhost:4200/。 |
在 Cloud Function 進行任何 AI 呼叫之前,確保所有必要的環境變數都存在至關重要。我實作了一個 VIDEO_CONFIG IIFE(立即調用函式運算式),使其執行一次以驗證輪詢週期、是否使用 Veo 3.1、Veo 模型名稱、專案 ID 和位置等環境變數。
import logger from "firebase-functions/logger";
export function validate(value: string | undefined, fieldName: string, missingKeys: string[]) {
const err = `${fieldName} is missing.`;
if (!value) {
logger.error(err);
missingKeys.push(fieldName);
return "";
}
return value;
}
export const VIDEO_CONFIG = (() => {
process.loadEnvFile();
const env = process.env;
const isVeo31Used = (env.IS_VEO31_USED || "false") === "true";
const pollingPeriod = Number(env.POLLING_PERIOD_MS || "10000");
const missingKeys: string[] = [];
const model = validate(env.GEMINI_VIDEO_MODEL_NAME, "Gemini Video Model Name", missingKeys);
const project = validate(env.GCLOUD_PROJECT, "Project ID", missingKeys);
const location = validate(env.GOOGLE_CLOUD_LOCATION, "Google Cloud Location", missingKeys);
if (missingKeys.length > 0) {
throw new Error(`Missing environment variables: ${missingKeys.join(", ")}`);
}
return {
genAIOptions: {
project,
location,
vertexai: true,
},
aiVideoOptions: {
model,
storageBucket: `${project}.firebasestorage.app`,
isVeo31Used,
pollingPeriod,
},
};
})();
截至 2026 年 4 月,我使用的是 Node 24。自 Node 20 起,我們可以使用內建的 process.loadEnvFile 函式,從 .env 檔案載入環境變數。
如果您使用的 Node 版本不支援 process.loadEnvfile,替代方案是安裝 dotenv 來載入環境變數。
npm i dotenv
import dotenv from "dotenv";
dotenv.config();
Firebase 提供了 GCLOUD_PROJECT 變數,因此它未在 .env 檔案中定義。
當 missingKeys 陣列不為空時,VIDEO_CONFIG 會拋出列出所有缺失變數名稱的錯誤。如果驗證成功,則會傳回 genAIOptions 和 aiVideoOptions。genAIOptions 用於初始化 GoogleGenAI,而 aiVideoOptions 包含擴展 Veo 影片的參數。
extendVideo Cloud Function 將酬載(payload)傳遞給 extendVideoFunction 函式。
所有的 Cloud Functions 都強制執行 App Check、CORS 以及 600 秒的逾時期間。如果未指定 WHITELIST,CORS 預設為 true。這在展示中是可以接受的,但在生產環境中,預設為 false 或特定網域會更安全。
const cors = process.env.WHITELIST ? process.env.WHITELIST.split(",").map((origin) => origin.trim()): true;
const options = {
cors,
enforceAppCheck: true,
timeoutSeconds: 600,
};
export const extendVideo = onCall( options,
({ data }) => extendVideoFunction(data)
);
extendVideoFunction 委派給 extendVideoByPolling 函式來構建影片引數(arguments),並對影片操作進行輪詢直到完成。當函式成功完成時,它會傳回 GCS URI 和 MIME 類型。在錯誤情況下,函式會拋出錯誤。
import { GoogleGenAI } from "@google/genai";
import { ExtendVideoRequest } from "./types/video.type";
import { extendVideoByPolling, VIDEO_CONFIG } from "./video.util";
export async function extendVideoFunction(data: ExtendVideoRequest) {
const { genAIOptions, aiVideoOptions } = VIDEO_CONFIG;
try {
if (!aiVideoOptions.isVeo31Used) {
throw new Error("Video extension is only supported for Veo 3.1 model");
}
const ai = new GoogleGenAI(genAIOptions);
return await extendVideoByPolling({ ai, ...aiVideoOptions }, {
prompt: data.prompt,
video: data.video,
config: data.config,
});
} catch (error) {
console.error("Error generating video:", error);
throw new Error("Error generating video");
}
}
import { GenerateVideosConfig, GoogleGenAI, Video } from "@google/genai";
export type AIVideoBucket = {
ai: GoogleGenAI;
model: string;
storageBucket: string;
isVeo31Used: boolean;
pollingPeriod: number;
}
export type ExtendVideoRequest = {
prompt: string;
video: Video;
config?: GenerateVideosConfig;
}
export async function extendVideoByPolling(
aiVideo: AIVideoBucket,
request: ExtendVideoRequest,
) {
return processVideoPolling(aiVideo, {
prompt: request.prompt,
config: request.config,
video: request.video,
});
}
async function processVideoPolling(
{ ai, model, storageBucket, pollingPeriod }: AIVideoBucket,
mediaParams: VideoMediaParams
) {
const genVideosParams: GenerateVideosParameters = {
model,
...mediaParams,
config: {
...mediaParams.config,
numberOfVideos: 1,
outputGcsUri: `gs://${storageBucket}`,
},
};
return getVideoUri(ai, genVideosParams, pollingPeriod);
}
extendVideoByPolling 函式調用 processVideoPolling 函式來輪詢影片操作直到完成。
processVideoPolling 函式構建影片參數並調用 getVideoUri 函式來傳回 GCS URI 和 MIME 類型。最重要的是,config 屬性指定生成的影片數量為 1,且 outputGcsUri 將生成的影片保留在儲存桶中。
影片擴展是一項長時間運行的任務。由於 Vertex AI 是非同步處理這些任務的,函式必須在 while 迴圈中輪詢操作狀態,直到 done 旗標為 true。
Gemini API 無法識別 Firebase Local Emulator 的 Cloud Storage,因此它需要一個真實的輸出 GCS URI,即 gs://${storageBucket}。
當 done 旗標為 true 時,操作結束,並會出現以下三種結果之一:
error 為 true,影片生成失敗。因此,函式拋出錯誤。import { GenerateVideosParameters, GoogleGenAI } from "@google/genai";
async function getVideoUri(
ai: GoogleGenAI,
genVideosParams: GenerateVideosParameters,
pollingPeriod: number,
): Promise<{ uri: string, mimeType: string }> {
let operation = await ai.models.generateVideos(genVideosParams);
while (!operation.done) {
await new Promise((resolve) => setTimeout(resolve, pollingPeriod));
operation = await ai.operations.getVideosOperation({ operation });
}
if (operation.error) {
const strError = `Video generation failed: ${operation.error.message}`;
console.error(strError);
throw new Error(strError);
}
const video = operation.response?.generatedVideos?.[0]?.video || {};
const { uri, mimeType } = video;
if (uri && mimeType) {
console.log("video uri", uri, mimeType);
return { uri, mimeType };
}
const strError = "Video generation finished but no uri was provided.";
console.error(strError);
throw new Error(strError);
}
環境變數 POLLING_PERIOD_MS 將輪詢週期設定為 10 秒。如果等待時間太長,可以縮短輪詢週期。如果輪詢成本昂貴或過於頻繁,可以增加輪詢週期。
注意:出於展示目的,輪詢是處理非同步影片生成的一種不錯的解決方案。然而,它的成本很高,且會產生不必要的負載和延遲。對於生產環境使用,您可以考慮推播通知(如 WebSockets 和伺服器傳送事件)。
getFirebaseConfig 是一個 Firebase Cloud Function,會傳回 Firebase 應用程式設定以及 reCAPTCHA 網站金鑰。
import logger from "firebase-functions/logger";
import { validate } from "./validate";
function validateFirebaseConfigFields(env: NodeJS.ProcessEnv) {
const missingKeys: string[] = [];
const apiKey = validate(env.APP_API_KEY, "API Key", missingKeys);
const appId = validate(env.APP_ID, "App Id", missingKeys);
const messagingSenderId = validate(env.APP_MESSAGING_SENDER_ID, "Messaging Sender ID", missingKeys);
const recaptchaSiteKey = validate(env.RECAPTCHA_ENTERPRISE_SITE_KEY, "Recaptcha site key", missingKeys);
const projectId = validate(env.GCLOUD_PROJECT, "Project ID", missingKeys);
if (missingKeys.length > 0) {
throw new Error(`Missing environment variables: ${missingKeys.join(", ")}`);
}
return {
app: {
apiKey,
appId,
projectId,
messagingSenderId,
authDomain: `${projectId}.firebaseapp.com`,
storageBucket: `${projectId}.firebasestorage.app`,
},
recaptchaSiteKey,
};
}
export const getFirebaseConfigFunction = () => {
logger.info("getFirebaseConfig called");
process.loadEnvFile();
const variables = validateFirebaseConfigFields(process.env);
if (!variables) {
return undefined;
}
return variables;
};
Angular 應用程式從 Cloud Function 接收 Firebase 應用程式設定和 reCAPTCHA 網站金鑰,以初始化 Firebase AI Logic 並保護資源免受未經授權的存取和濫用。
對於本地開發,我使用了 Firebase Local Emulator Suite。在 bootstrapFirebase 程序中,應用程式會呼叫 connectFunctionsEmulator 以連結到運行在 http://localhost:5001 的 Cloud Functions。
當執行 firebase init 時,通訊埠號碼預設為 5001。
注意: 雖然 Cloud Function 在本地運行(零成本),但並未使用 Storage 模擬器。這是因為 Gemini API 需要一個實際可存取的 GCS 儲存桶來儲存生成的影片。
export function connectEmulators({ remoteConfig, functions }: FirebaseObjects) {
const useEmulators = getValue(remoteConfig, 'useEmulators').asBoolean();
if (useEmulators) {
console.log('Connecting to emulators...');
const host = getValue(remoteConfig, 'functionEmulatorHost').asString();
const port = getValue(remoteConfig, 'functionEmulatorPort').asNumber();
console.log('functionEmulator', `${host}:${port}`);
connectFunctionsEmulator(functions, host, port);
}
}
loadFirebaseConfig 是一個輔助函式,會向 Cloud function 發送請求以獲取 Firebase 應用程式設定和 reCAPTCHA 網站金鑰。
{
"getFirebaseConfigUrl": "http://127.0.0.1:5001/vertexai-firebase-6a64f/us-central1/getFirebaseConfig"
}
import { HttpClient } from '@angular/common/http';
import { inject } from '@angular/core';
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check';
import { catchError, lastValueFrom, throwError } from 'rxjs';
import config from '../../public/config.json';
import { ConfigService } from './ai/services/config.service';
import { FirebaseConfigResponse } from './ai/types/firebase-config.type';
import { connectEmulators, initFirebaseApp } from './firebase.util';
async function loadFirebaseConfig() {
const httpService = inject(HttpClient);
const firebaseConfig$ =
httpService.get<FirebaseConfigResponse>(`${config.getFirebaseConfigUrl}`)
.pipe(catchError((e) => throwError(() => e)));
return lastValueFrom(firebaseConfig$);
}
export async function bootstrapFirebase() {
try {
const configService = inject(ConfigService);
const firebaseConfig = await loadFirebaseConfig();
const { app, recaptchaSiteKey } = firebaseConfig;
const firebaseObjects = await initFirebaseApp(app);
const { firebaseApp } = firebaseObjects;
initializeAppCheck(firebaseApp, {
provider: new ReCaptchaEnterpriseProvider(recaptchaSiteKey),
isTokenAutoRefreshEnabled: true,
});
connectEmulators(firebaseObjects);
configService.loadConfig(firebaseObjects);
} catch (err) {
console.error(err);
}
}
AppConfig 保持不變。
import { ApplicationConfig, provideAppInitializer } from '@angular/core';
import { bootstrapFirebase } from './app.bootstrap';
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(async () => bootstrapFirebase()),
]
};
在 Firebase Remote Config 中引入了用於擴展 Veo 影片的新變數。對於 Gemini API,一段影片最多可擴展 20 次。對於 Gemini in Vertex AI,由於影片長度超過 30 秒,一段影片最多可擴展 4 次。因此,這被外部化為 maxVideoExtendAllowed 變數。
| 變數 | 描述 | 值 |
|---|---|---|
| maxVideoExtendAllowed | 允許的最大擴展次數 | 4 |
Angular 前端使用 httpsCallable 觸發程序。一旦函式傳回 Cloud Storage 路徑和 MIME 類型,應用程式就會獲取下載 URL 以進行播放。
ConfigService 儲存 Firebase 應用程式、Remote Config 和函式,供整個應用程式使用。
import { FirebaseApp } from 'firebase/app';
import { Functions } from 'firebase/functions';
import { RemoteConfig } from 'firebase/remote-config';
export type FirebaseObjects = {
firebaseApp: FirebaseApp;
remoteConfig: RemoteConfig;
functions: Functions;
}
import { Injectable } from '@angular/core';
import { FirebaseObjects } from '../types/firebase-objects';
@Injectable({
providedIn: 'root'
})
export class ConfigService {
firebaseObjects: FirebaseObjects | undefined = undefined;
loadConfig(firebaseObjects: FirebaseObjects) {
this.firebaseObjects = firebaseObjects;
}
}
retrieveVideoUri 方法直接呼叫 Cloud Function 以檢索 GCS URI 和 MIME 類型。
downloadVideoUriAndUrl 方法將 URI 解析為 HTTP URL,以便 HTML 影片播放器可以立即播放。
export type VideoGenerationResponse = {
uri: string;
url: string;
mimeType: string;
}
export type DownloadVideoResponse = {
uri: string;
mimeType: string;
}
import { inject, Injectable } from '@angular/core';
import { httpsCallable } from 'firebase/functions';
import { getDownloadURL, getStorage, ref } from 'firebase/storage';
import { GenerateVideoRequest } from '../types/video.type';
import { ConfigService } from './config.service';
@Injectable({
providedIn: 'root'
})
export class VeoService {
private readonly storage = getStorage();
private readonly configService = inject(ConfigService);
private async retrieveVideoUri<T = GenerateVideoRequest>(request: T, methodName: string): Promise<DownloadVideoResponse> {
try {
const { functions } = this.configService.firebaseObjects || {};
if (!functions) {
throw new Error('Functions does not exist.');
}
const downloadGcsUri = httpsCallable<T, DownloadVideoResponse>(
functions, methodName, { timeout: 600000 }
);
const { data } = await downloadGcsUri(request);
return data;
} catch (err) {
console.error(err);
throw err;
}
}
async downloadVideoUriAndUrl<T = GenerateVideoRequest>(request: T, methodName: CallableNames = 'videos-generateVideo'): Promise<VideoGenerationResponse> {
const { uri, mimeType } = await this.retrieveVideoUri(request, methodName);
if (!uri) {
throw new Error('Video operation completed but no URI was returned.');
}
return getDownloadURL(ref(this.storage, uri))
.then((url) => {
console.log("download url", url);
return { uri, url, mimeType };
})
.catch((error) => {
switch (error.code) {
case 'storage/object-not-found':
throw new Error("File doesn't exist");
case 'storage/unauthorized':
throw new Error("User doesn't have permission to access the object");
case 'storage/canceled':
throw new Error("User canceled the upload");
case 'storage/unknown':
throw new Error("Unknown storage error occurred, inspect the server response");
}
throw new Error("Unknown error occurred");
});
}
}
VideoPlayerComponent 有一個必要的 videoUrl signal 輸入,它被分配給影片播放器的來源。它有一個「擴展影片」(Extend Video)按鈕,點擊時會發送一個 extendVideo 自定義事件。此事件會通知父元件擴展 videoUrl signal 輸入中的影片。
import { LoaderComponent } from '@/shared/loader/loader.component';
import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { ExtendVideoIconComponent } from '../../icons/extend-video-icon.component';
@Component({
selector: 'app-video-player',
imports: [LoaderComponent, ExtendVideoIconComponent],
template: `
@if (isGeneratingVideo()) {
<div class="mt-6">
<app-loader [loadingText]="loadingText()">
<p class="text-sm">This can take several minutes. Please be patient.</p>
</app-loader>
</div>
} @else if (videoUrl()) {
<div class="mt-6 flex flex-col gap-4">
<div class="video-container">
<video [src]="videoUrl()" controls autoplay loop class="w-full rounded-md"></video>
</div>
<div class="flex justify-center">
<button
(click)="extendVideo.emit()"
aria-label="Extend video"
title="Extend video"
class="extend-btn"
>
<app-extend-video-icon />
<span>Extend Video</span>
</button>
</div>
</div>
}
`,
styleUrl: './video-player.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VideoPlayerComponent {
isGeneratingVideo = input(false);
videoUrl = input.required<string>();
extendVideo = output<void>();
loadingText = input('Generating your video...');
}
import { VideoPlayerComponent } from './video-player/video-player.component';
@Component({
selector: 'app-gen-media',
imports: [
VideoPlayerComponent,
],
template: `
<app-video-player
[isGeneratingVideo]="isGeneratingVideo()"
[videoUrl]="videoUrl()"
(extendVideo)="extendVideo()"
[loadingText]="loadingVideoText()"
/>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GenMediaComponent {
private readonly genMediaService = inject(GenMediaService);
private readonly genVideoService = inject(GenVideoService);
genMediaInput = input<GenMediaInput>();
videoUrl = this.genVideoService.videoUrl;
isGeneratingVideo = this.genVideoService.isGeneratingVideo.asReadonly();
loadingVideoText = signal('Generating your video...');
trimmedUserPrompt = computed(() => this.genMediaInput()?.userPrompt.trim() || '');
async extendVideo() {
if (this.trimmedUserPrompt()) {
try {
this.loadingVideoText.set('Extending your video...');
await this.genVideoService.extendVideo(this.trimmedUserPrompt());
} finally {
this.loadingVideoText.set('Generating your video...');
}
}
}
}
GenMediaComponent 呼叫 extendVideo 並帶入提示詞和 Veo 影片 URL 來建立擴展後的影片。
import { ConfigService } from '@/ai/services/config.service';
import { VeoService } from '@/ai/services/veo.service';
import { ExtendVideoRequest, GenerateVideoFromFramesRequest, GenerateVideoRequest, VideoGenerationResponse } from '@/ai/types/video.type';
import { computed, inject, Injectable, signal, WritableSignal } from '@angular/core';
import { getValue } from 'firebase/remote-config';
@Injectable({
providedIn: 'root'
})
export class GenVideoService {
private readonly veoService = inject(VeoService);
private readonly configService = inject(ConfigService);
videoError = signal('');
videoResponse = signal<VideoGenerationResponse>({ uri: '', url: '', mimeType: '' });
#extendVideoCounter = signal(0);
isGeneratingVideo = signal(false);
videoUrl = computed(() => this.videoResponse().url);
isVideoExtensionAllowed(counter: number) {
const remoteConfig = this.configService.firebaseObjects?.remoteConfig;
if (!remoteConfig) {
console.warn('Remote config does not exist.');
return false;
}
const max_extend_allowed = getValue(remoteConfig,'maxVideoExtendAllowed').asNumber();
if (counter >= max_extend_allowed) {
console.warn('Maximum extension limit reached.');
return false;
}
return true;
}
async extendVideo(prompt: string): Promise<void> {
try {
const result = await this.extendInterpolatedVideo(
prompt,
this.#extendVideoCounter(),
this.videoResponse()
);
if (!result) {
return;
}
this.videoResponse.set(result);
this.#extendVideoCounter.update(count => count + 1);
console.log(`Video extended successfully. Current extension count: ${this.#extendVideoCounter()}`);
} catch (e) {
console.error(e);
const errMsg = e instanceof Error ?
e.message :
'An unexpected error occurred in video generation.'
this.videoError.set(errMsg);
} finally {
this.isGeneratingVideo.set(false);
}
}
async extendInterpolatedVideo(
prompt: string,
counter: number,
customVideo: Pick<VideoGenerationResponse, 'uri' | 'mimeType'>,
generatingSignal?: WritableSignal<boolean>,
error?: WritableSignal<string>
) {
const { uri, mimeType } = customVideo;
if (!mimeType || !uri) {
console.warn('No video to extend. Please generate a video first.');
return null;
}
if (!prompt) {
console.warn('Prompt is required to extend the video.');
return null;
}
if (!this.isVideoExtensionAllowed(counter)) {
return null;
}
const actualErrorSignal = error || this.videoError;
const actualGeneratingSignal = generatingSignal || this.isGeneratingVideo;
try {
actualErrorSignal.set('');
actualGeneratingSignal.set(true);
const extendVideoParams: ExtendVideoRequest = {
prompt,
video: { uri, mimeType },
}
return await this.veoService.downloadVideoUriAndUrl(extendVideoParams, 'videos-extendVideo');
} catch (e) {
console.error(e);
throw e;
} finally {
actualGeneratingSignal.set(false);
}
}
}
extendVideo 和 extendInterpolatedVideo 方法會通過比較 #extendVideoCounter signal 值與 maxVideoExtendAllowed Remote Config 值來驗證是否允許擴展。如果檢查失敗,則不執行任何操作。如果檢查成功,則會呼叫 videos-extendVideo Cloud Function 來擴展影片。接著,#extendVideoCounter 會增加 1,直到達到擴展限制。當擴展的 Veo 影片過長時,Gemini 會拋出錯誤,我希望在用戶端封鎖這種情況發生。
以上是本次展示的逐步引導。您現在應該能夠在 Cloud Function 中擴展生成的影片,將其安全地儲存在儲存桶中,並在使用者介面的影片播放器中播放。
將 Veo 3.1 與 Firebase 的無伺服器擴展性相結合是一個強大的工作流程。
首先,Angular 應用程式既不需要安裝 genai 相依性,也不需要在 .env 檔案中維護 Vertex AI 環境變數。用戶端應用程式呼叫 Cloud Functions 來執行密集型任務並等待結果。
Cloud Functions 接收來自用戶端的引數,執行複雜的 AI 操作(如生成、插補和影片擴展),並將影片安全地寫入專用儲存桶。在本地開發期間,Firebase Emulator 會呼叫運行在 http://localhost:5001 的函式,而不是部署在 Cloud Run 平台上的函式。
嘗試複製 GitHub 存放庫,生成圖像,並使用這些圖像來生成和插補影片。接著,您可以擴展這些 Veo 影片,建立長度超過 7 秒的擴展影片。