171 lines
4.5 KiB
TypeScript
171 lines
4.5 KiB
TypeScript
// Image Generation API service
|
||
|
||
export interface ImageGenerationRequest {
|
||
style: 'cartoon' | 'realistic' | 'minimal'
|
||
priority: 'normal' | 'high' | 'low'
|
||
width: number
|
||
height: number
|
||
replicateModel: string
|
||
options: {
|
||
useGeminiCache: boolean
|
||
useImageCache: boolean
|
||
maxRetries: number
|
||
learnerLevel: string
|
||
scenario: string
|
||
visualPreferences: string[]
|
||
}
|
||
}
|
||
|
||
export interface GenerationStatus {
|
||
requestId: string
|
||
overallStatus: string
|
||
currentStage?: string
|
||
stages: {
|
||
gemini: {
|
||
status: string
|
||
startedAt?: string
|
||
completedAt?: string
|
||
processingTimeMs?: number
|
||
cost?: number
|
||
generatedDescription?: string
|
||
}
|
||
replicate: {
|
||
status: string
|
||
startedAt?: string
|
||
completedAt?: string
|
||
processingTimeMs?: number
|
||
cost?: number
|
||
model?: string
|
||
modelVersion?: string
|
||
progress?: string
|
||
}
|
||
}
|
||
totalCost?: number
|
||
completedAt?: string
|
||
result?: {
|
||
imageUrl: string
|
||
imageId: string
|
||
qualityScore?: number
|
||
dimensions?: {
|
||
width: number
|
||
height: number
|
||
}
|
||
fileSize?: number
|
||
}
|
||
}
|
||
|
||
export interface ApiResponse<T> {
|
||
success: boolean
|
||
data?: T
|
||
error?: string
|
||
details?: string
|
||
}
|
||
|
||
class ImageGenerationService {
|
||
private baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'
|
||
|
||
private async makeRequest<T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>> {
|
||
const token = localStorage.getItem('token')
|
||
|
||
const response = await fetch(`${this.baseUrl}${url}`, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
// 開發階段:不發送無效的token,讓後端使用測試用戶
|
||
// 'Authorization': token ? `Bearer ${token}` : '',
|
||
...options.headers,
|
||
},
|
||
...options,
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({ error: 'Network error' }))
|
||
throw new Error(errorData.error || errorData.details || `HTTP ${response.status}`)
|
||
}
|
||
|
||
return response.json()
|
||
}
|
||
|
||
// 啟動圖片生成
|
||
async generateImage(flashcardId: string, request?: Partial<ImageGenerationRequest>): Promise<ApiResponse<{ requestId: string }>> {
|
||
const defaultRequest: ImageGenerationRequest = {
|
||
style: 'cartoon',
|
||
priority: 'normal',
|
||
width: 512,
|
||
height: 512,
|
||
replicateModel: 'ideogram-v2a-turbo',
|
||
options: {
|
||
useGeminiCache: true,
|
||
useImageCache: true,
|
||
maxRetries: 3,
|
||
learnerLevel: 'B1',
|
||
scenario: 'daily',
|
||
visualPreferences: ['colorful', 'simple']
|
||
},
|
||
...request
|
||
}
|
||
|
||
return this.makeRequest(`/api/imagegeneration/flashcards/${flashcardId}/generate`, {
|
||
method: 'POST',
|
||
body: JSON.stringify(defaultRequest)
|
||
})
|
||
}
|
||
|
||
// 查詢生成狀態
|
||
async getGenerationStatus(requestId: string): Promise<ApiResponse<GenerationStatus>> {
|
||
return this.makeRequest(`/api/imagegeneration/requests/${requestId}/status`)
|
||
}
|
||
|
||
// 取消生成
|
||
async cancelGeneration(requestId: string): Promise<ApiResponse<{ message: string }>> {
|
||
return this.makeRequest(`/api/imagegeneration/requests/${requestId}/cancel`, {
|
||
method: 'POST'
|
||
})
|
||
}
|
||
|
||
// 輪詢直到完成
|
||
async pollUntilComplete(
|
||
requestId: string,
|
||
onProgress?: (status: GenerationStatus) => void,
|
||
timeoutMinutes = 5
|
||
): Promise<GenerationStatus> {
|
||
const startTime = Date.now()
|
||
const timeout = timeoutMinutes * 60 * 1000
|
||
|
||
while (Date.now() - startTime < timeout) {
|
||
try {
|
||
const result = await this.getGenerationStatus(requestId)
|
||
|
||
if (!result.success || !result.data) {
|
||
throw new Error(result.error || 'Failed to get status')
|
||
}
|
||
|
||
const status = result.data
|
||
|
||
// 呼叫進度回調
|
||
if (onProgress) {
|
||
onProgress(status)
|
||
}
|
||
|
||
// 檢查是否完成
|
||
if (status.overallStatus === 'completed') {
|
||
return status
|
||
}
|
||
|
||
// 檢查是否失敗
|
||
if (status.overallStatus === 'failed') {
|
||
throw new Error('圖片生成失敗')
|
||
}
|
||
|
||
// 等待 2 秒後再次檢查
|
||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||
} catch (error) {
|
||
console.error('輪詢狀態時發生錯誤:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
throw new Error('圖片生成超時')
|
||
}
|
||
}
|
||
|
||
export const imageGenerationService = new ImageGenerationService() |