132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|