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

143 lines
4.1 KiB
TypeScript

/**
* Grok Client
* 與 Gemini 相同介面
* 預設不啟用
*/
import type { AIProvider } from '~/types/ai'
import { getGrokToken, getTextModel, getVisionModel } from '~/utils/storage'
import { DEFAULT_GROK_TEXT_MODEL, DEFAULT_GROK_VISION_MODEL, getGrokModelForTask } from './grok-models'
const GROK_API_BASE = 'https://api.x.ai/v1'
/**
* Grok API Client
*/
export class GrokClient implements AIProvider {
/**
* 取得 API Token
*/
private getToken(): string {
const token = getGrokToken()
if (!token) {
throw new Error('Grok API Token 未設定,請至設定頁面輸入')
}
return token
}
/**
* 呼叫 Grok API
* @param prompt - 文字提示
* @param model - 模型名稱,預設使用 grok-2
* @param parts - 多模態內容(圖像等)
*/
private async callAPI(
prompt: string,
model: string = DEFAULT_GROK_TEXT_MODEL,
parts?: Array<{ text?: string; image_url?: { url: string } }>
): Promise<string> {
const token = this.getToken()
const url = `${GROK_API_BASE}/chat/completions`
// 組裝 messages
const messages: Array<{ role: string; content: Array<{ type: string; text?: string; image_url?: { url: string } }> }> = []
const contentParts: Array<{ type: string; text?: string; image_url?: { url: string } }> = []
if (prompt) {
contentParts.push({ type: 'text', text: prompt })
}
if (parts) {
parts.forEach(part => {
if (part.image_url) {
contentParts.push({ type: 'image_url', image_url: part.image_url })
}
})
}
messages.push({
role: 'user',
content: contentParts
})
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
model,
messages
})
})
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.choices?.[0]?.message?.content || ''
}
/**
* 生成分鏡表
*/
async generateStoryboard(input: unknown): Promise<string> {
// 如果選擇的模型是 Grok 模型,使用選擇的模型;否則使用預設
const selectedModel = getTextModel()
const model = selectedModel && selectedModel.includes('grok')
? selectedModel
: getGrokModelForTask('text')
const prompt = typeof input === 'string' ? input : JSON.stringify(input)
return this.callAPI(prompt, model)
}
/**
* 分析攝影機鏡位與運鏡
* 支援圖像輸入
*/
async analyzeCamera(input: unknown): Promise<string> {
// 如果選擇的模型是 Grok 模型,使用選擇的模型;否則使用預設
const selectedModel = getVisionModel()
const model = selectedModel && selectedModel.includes('grok')
? selectedModel
: getGrokModelForTask('vision')
// 如果 input 是物件且包含圖像
if (typeof input === 'object' && input !== null) {
const inputObj = input as { prompt?: string; image?: { url: string } }
const prompt = inputObj.prompt || '請分析這張圖片的攝影機鏡位與運鏡建議'
const parts: Array<{ text?: string; image_url?: { url: string } }> = []
if (inputObj.image) {
parts.push({ image_url: 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)
}
}