ai-cut/app/utils/clients/gemini.ts

132 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Gemini Client
* 單一責任:呼叫 Gemini API
* 從 localStorage 讀取 token
*/
import type { AIProvider } from '~/types/ai'
import { getGeminiToken, getTextModel, getVisionModel } from '~/utils/storage'
import { DEFAULT_TEXT_MODEL, DEFAULT_VISION_MODEL, getModelForTask } from './gemini-models'
const GEMINI_API_BASE = 'https://generativelanguage.googleapis.com/v1beta'
/**
* Gemini API Client
*/
export class GeminiClient implements AIProvider {
private token: string | null
constructor() {
this.token = getGeminiToken()
}
/**
* 取得 API Token
*/
private getToken(): string {
const token = getGeminiToken()
if (!token) {
throw new Error('Gemini API Token 未設定,請至設定頁面輸入')
}
return token
}
/**
* 呼叫 Gemini API
* @param prompt - 文字提示
* @param model - 模型名稱,預設使用儲存的模型或 DEFAULT_TEXT_MODEL
* @param parts - 多模態內容(圖像、音頻等)
*/
private async callAPI(
prompt: string,
model?: string,
parts?: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }>
): Promise<string> {
// 如果沒有指定模型,使用儲存的模型或預設模型
const selectedModel = model || getTextModel() || DEFAULT_TEXT_MODEL
const token = this.getToken()
const url = `${GEMINI_API_BASE}/models/${selectedModel}:generateContent?key=${token}`
// 組裝 parts
const contentParts: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }> = []
if (prompt) {
contentParts.push({ text: prompt })
}
if (parts) {
contentParts.push(...parts)
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
contents: [{
parts: contentParts
}]
})
})
if (!response.ok) {
const error = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
throw new Error(error.error?.message || `API 錯誤: ${response.status}`)
}
const data = await response.json()
return data.candidates?.[0]?.content?.parts?.[0]?.text || ''
}
/**
* 生成分鏡表
*/
async generateStoryboard(input: unknown): Promise<string> {
// Prompt 組裝將由 utils/ai 處理,這裡只負責呼叫 API
const prompt = typeof input === 'string' ? input : JSON.stringify(input)
return this.callAPI(prompt)
}
/**
* 分析攝影機鏡位與運鏡
* 支援圖像輸入base64 或 URL
*/
async analyzeCamera(input: unknown): Promise<string> {
const model = getVisionModel() || DEFAULT_VISION_MODEL
// 如果 input 是物件且包含圖像
if (typeof input === 'object' && input !== null) {
const inputObj = input as { prompt?: string; image?: { mimeType: string; data: string } }
const prompt = inputObj.prompt || '請分析這張圖片的攝影機鏡位與運鏡建議'
const parts: Array<{ text?: string; inlineData?: { mimeType: string; data: string } }> = []
if (inputObj.image) {
parts.push({ inlineData: inputObj.image })
}
return this.callAPI(prompt, model, parts)
}
const prompt = typeof input === 'string' ? input : JSON.stringify(input)
return this.callAPI(prompt, model)
}
/**
* 生成影片規劃
*/
async generateVideoPlan(input: unknown): Promise<string> {
const prompt = typeof input === 'string' ? input : JSON.stringify(input)
return this.callAPI(prompt)
}
/**
* 生成剪輯建議
*/
async generateEditSuggestion(input: unknown): Promise<string> {
const prompt = typeof input === 'string' ? input : JSON.stringify(input)
return this.callAPI(prompt)
}
}