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

132 lines
3.8 KiB
TypeScript
Raw Normal View History

2025-12-16 10:08:51 +00:00
/**
* 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)
}
}