在昨天的 [Day 14] 中,我們成功將 Google Stitch 產出的 UI 原型解耦重構,封裝成具備現代質感與良好狀態控制的 Next.js 15 儀表板元件。
現在,我們面臨全棧 AI 應用開發中最具挑戰性的一道關卡 —— 「檔案管線(File Pipeline)」。
在 OmniVibe AI 的實際場景中,用戶不可能只傳幾 KB 的小文字檔。典型的使用案例是上傳 100MB 以上的 MP4 演講影片、高音質 Podcast 音訊(MP3),或是數百頁的 PDF 白皮書。
如果在後端直接把大檔案轉成 Base64 字串硬丟給 Gemini API(Inline Data),將會面臨:
今天,我們將引進 Google 官方推薦的 Google AI File API,並結合 Next.js 15 的 Server Actions,搭建一條能穩定吞吐巨型檔案、支援異步狀態監控(Polling)的生產級檔案處理解決方案!
Google Gen AI SDK 提供兩種餵給模型多模態資產的方式:
| 方案 | 適用情境 | 檔案大小上限 | 優缺點分析 |
|---|---|---|---|
| Inline Data (Base64) | 小型圖片(JPEG/PNG)、短音訊片段 | < 20 MB | ⚡ 優點:無需額外 API 呼叫,直接隨著 Prompt 送出。 |
❌ 缺點:消耗大量 Payload 頻寬,不支援長影片與大檔案。 |
| Google AI File API | 巨型 MP4 影片、長 Podcast、百頁 PDF | 單檔最大 2 GB
(每專案上限 20 GB) | ⚡ 優點:上傳後返回可重複使用的 fileUri,支援 Context Caching,上傳一次即可無限次提問。
❌ 缺點:影片需要非同步轉碼,需實作狀態輪詢機制。 |
對於 OmniVibe AI 這種內容提煉 SaaS 來說,Google AI File API 是絕對的不二之選。
為了確保使用者介面不會死鎖,我們將檔案處理解耦為三個獨立階段:
sequenceDiagram
autonumber
participant Client as 前端 (Dashboard UI)
participant ServerAction as Next.js Server Action
participant FileManager as Google AI File API
participant Gemini as Gemini 1.5 大腦
Client->>ServerAction: 1. 提交 FormData (影音/PDF 檔案)
ServerAction->>FileManager: 2. 呼叫 fileManager.uploadFile() 暫存至 Google 雲端
FileManager-->>ServerAction: 3. 回傳 File metadata (狀態: PROCESSING)
loop 狀態監控 (State Polling)
ServerAction->>FileManager: 4. 輪詢 fileManager.getFile(fileName)
FileManager-->>ServerAction: 5. 回傳最新狀態 (PROCESSING / ACTIVE)
end
ServerAction-->>Client: 6. 返回就緒的 fileUri 與 mimeType
Client->>ServerAction: 7. 發起內容提煉請求 (附帶 fileUri)
ServerAction->>Gemini: 8. generateContent([fileUri, prompt])
Gemini-->>Client: 9. 回傳高價值摘要與社群貼文
我們將在 src/lib/gemini/file-manager.ts 封裝 SDK 的上傳與監控邏輯,並在 src/app/actions/upload-asset.ts 中暴露 Server Action。
src/lib/gemini/file-manager.ts)我們使用 @google/generative-ai/server 提供的 GoogleAIFileManager:
// src/lib/gemini/file-manager.ts
import { GoogleAIFileManager, FileState } from '@google/generative-ai/server';
const apiKey = process.env.GEMINI_API_KEY || '';
export const fileManager = new GoogleAIFileManager(apiKey);
export interface UploadedFileResult {
uri: string;
mimeType: string;
name: string;
displayName: string;
sizeBytes: string;
}
/**
* 上傳檔案至 Google AI File API 並等待其狀態轉為 ACTIVE (適用於影音後台轉碼)
*/
export async function uploadAndPollFile(
filePath: string,
mimeType: string,
displayName: string
): Promise<UploadedFileResult> {
try {
console.log(`[File API] 開始上傳檔案: ${displayName} (${mimeType})...`);
// 1. 上傳檔案至 Google 雲端暫存
const uploadResult = await fileManager.uploadFile(filePath, {
mimeType,
displayName,
});
const fileName = uploadResult.file.name;
console.log(`[File API] 上傳完成,檔案識別碼: ${fileName},等待轉碼就緒...`);
// 2. 針對影音等需要後台處理的檔案進行狀態輪詢 (Polling)
let file = await fileManager.getFile(fileName);
let attempts = 0;
const maxAttempts = 30; // 最多等待 90 秒 (3s * 30)
while (file.state === FileState.PROCESSING && attempts < maxAttempts) {
console.log(`[File API] 檔案轉碼中... (${attempts + 1}/${maxAttempts})`);
await new Promise((resolve) => setTimeout(resolve, 3000)); // 每 3 秒輪詢一次
file = await fileManager.getFile(fileName);
attempts++;
}
if (file.state === FileState.FAILED) {
throw new Error('Google AI File API 檔案轉碼失敗');
}
if (file.state !== FileState.ACTIVE) {
throw new Error('檔案轉碼逾時,請稍後重試');
}
console.log(`[File API] 檔案狀態已就緒 (ACTIVE)!URI: ${file.uri}`);
return {
uri: file.uri,
mimeType: file.mimeType,
name: file.name,
displayName: file.displayName || displayName,
sizeBytes: file.sizeBytes,
};
} catch (error: any) {
console.error('[File API Error]:', error);
throw new Error(`檔案處理解析失敗: ${error.message}`);
}
}
src/app/actions/upload-asset.ts)在 Next.js 15 中,Server Actions 允許前端以原生調用 Function 的方式發起伺服器端操作。我們需要處理傳入的 FormData,將上傳的檔案暫存至 Node.js 臨時目錄,傳送至 Gemini 後再乾淨地清理暫存檔:
// src/app/actions/upload-asset.ts
'use server';
import { uploadAndPollFile } from '@/lib/gemini/file-manager';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
export async function uploadAssetAction(formData: FormData) {
let tempFilePath: string | null = null;
try {
const file = formData.get('file') as File | null;
if (!file) {
return { success: false, error: '未檢測到上傳檔案' };
}
console.log(`[Server Action] 接收到上傳請求: ${file.name}, 大小: ${(file.size / 1024 / 1024).toFixed(2)} MB`);
// 1. 將前端傳入的 File 轉為 Buffer 並寫入系統臨時目錄 (/tmp)
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
tempFilePath = path.join(os.tmpdir(), `omnivibe_${Date.now()}_${file.name}`);
await fs.writeFile(tempFilePath, buffer);
// 2. 調用 File API 進行遠端上傳與狀態輪詢
const fileResult = await uploadAndPollFile(
tempFilePath,
file.type,
file.name
);
// 3. 回傳安全的 JSON 資訊給前端 Dashboard
return {
success: true,
data: fileResult,
};
} catch (error: any) {
console.error('[Upload Action Error]:', error);
return {
success: false,
error: error.message || '檔案上傳過程發生未知錯誤',
};
} finally {
// 4. 清理本機伺服器的臨時檔案,避免硬碟空間爆滿
if (tempFilePath) {
try {
await fs.unlink(tempFilePath);
console.log(`[Server Action] 臨時檔案已成功清理: ${tempFilePath}`);
} catch (cleanupErr) {
console.error('[Server Action] 清理臨時檔案失敗:', cleanupErr);
}
}
}
}
DropzoneArea 與 Server Action 連接 (src/components/dashboard/DropzoneArea.tsx)現在,我們可以在 Days 14 寫好的前端 UI 中,輕鬆觸發上傳動作並更新狀態:
// 在 src/app/dashboard/page.tsx 中整合 Actions
import { uploadAssetAction } from '@/app/actions/upload-asset';
// ... (內部觸發邏輯)
const handleStartDistill = async () => {
if (!selectedFile) return;
setIsLoading(true);
// 建立 FormData
const formData = new FormData();
formData.append('file', selectedFile);
// 呼叫 Server Action 處理上傳與轉碼
const uploadRes = await uploadAssetAction(formData);
if (!uploadRes.success || !uploadRes.data) {
alert(`上傳失敗: ${uploadRes.error}`);
setIsLoading(false);
return;
}
console.log('取得 Gemini File URI:', uploadRes.data.uri);
// 取得 fileUri 後發起 Gemini 推論 (Day 07/12 的管線)
// ...
};
我們試著上傳一份 85MB 的高畫質演講影音檔 ai_keynote_2026.mp4:
[Server Action] 接收到上傳請求: ai_keynote_2026.mp4, 大小: 85.40 MB
[File API] 開始上傳檔案: ai_keynote_2026.mp4 (video/mp4)...
[File API] 上傳完成,檔案識別碼: files/ab12cd34ef56,等待轉碼就緒...
[File API] 檔案轉碼中... (1/30)
[File API] 檔案轉碼中... (2/30)
[File API] 檔案狀態已就緒 (ACTIVE)!URI: https://generativelanguage.googleapis.com/v1beta/files/ab12cd34ef56
[Server Action] 臨時檔案已成功清理: /tmp/omnivibe_1759048000_ai_keynote_2026.mp4
fs.unlink 清理 tmp 檔案,保護 Node.js runtime 健全度。FileState.PROCESSING 輪詢,確保 Gemini 1.5 接收到影音 URI 時 100% 已可被讀取與分析,徹底告別 API 轉碼中的 Crash 錯誤。fileUri 可直接作為 Day 12 所介紹的 Context Caching 輸入,一次上傳,無限次極速追問!今天我們打通了 OmniVibe AI 多模態資產處理的「最後一公里」:
擁有了極致的前端介面與穩健的後端檔案上傳管線,但如果用戶點擊提煉後,需要乾等 10 秒才能一次看到所有結果,體驗依舊不夠流暢。
👉 明天(Day 16),我們將進入【流式傳輸與打字機篇】:實戰 Server-Sent Events (SSE) 與 Gemini Stream 串流打字機!看我們如何用代碼實現逐字噴發的打字機效果,帶給使用者極致流暢的 AI 互動體驗!
我們明天見!🔥