dramaling-vocab-learning/frontend/lib/services/imageGeneration.ts

171 lines
4.5 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.

// 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()