|
|
import { apiClient } from './client' |
|
|
import { io, Socket } from 'socket.io-client' |
|
|
|
|
|
export interface ProcessingOptions { |
|
|
model?: 'rembg' | 'u2net' | 'deeplab' | 'custom' |
|
|
quality?: 'low' | 'medium' | 'high' | 'ultra' |
|
|
returnMask?: boolean |
|
|
edgeRefinement?: number |
|
|
feather?: number |
|
|
tolerance?: number |
|
|
preserveDetails?: boolean |
|
|
} |
|
|
|
|
|
export interface ProcessingResult { |
|
|
id: string |
|
|
image: string |
|
|
mask?: string |
|
|
metadata: { |
|
|
width: number |
|
|
height: number |
|
|
format: string |
|
|
processingTime: number |
|
|
} |
|
|
} |
|
|
|
|
|
export interface BatchJob { |
|
|
id: string |
|
|
status: 'pending' | 'processing' | 'completed' | 'failed' |
|
|
progress: number |
|
|
totalFiles: number |
|
|
processedFiles: number |
|
|
results?: ProcessingResult[] |
|
|
error?: string |
|
|
createdAt: string |
|
|
completedAt?: string |
|
|
} |
|
|
|
|
|
class ProcessingAPI { |
|
|
private socket: Socket | null = null |
|
|
private listeners: Map<string, Set<Function>> = new Map() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async processImage( |
|
|
file: File, |
|
|
options: ProcessingOptions = {}, |
|
|
onProgress?: (progress: number) => void |
|
|
): Promise<ProcessingResult> { |
|
|
const formData = new FormData() |
|
|
formData.append('file', file) |
|
|
formData.append('options', JSON.stringify(options)) |
|
|
|
|
|
return apiClient.upload<ProcessingResult>( |
|
|
'/api/v1/process/remove-background', |
|
|
file, |
|
|
onProgress |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async processBatch( |
|
|
files: File[], |
|
|
options: ProcessingOptions = {} |
|
|
): Promise<BatchJob> { |
|
|
const formData = new FormData() |
|
|
files.forEach((file) => formData.append('files', file)) |
|
|
formData.append('options', JSON.stringify(options)) |
|
|
|
|
|
const response = await apiClient.post<BatchJob>('/api/v1/process/batch', formData, { |
|
|
headers: { 'Content-Type': 'multipart/form-data' }, |
|
|
}) |
|
|
|
|
|
|
|
|
this.connectWebSocket(response.id) |
|
|
|
|
|
return response |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getJobStatus(jobId: string): Promise<BatchJob> { |
|
|
return apiClient.get<BatchJob>(`/api/v1/process/jobs/${jobId}`) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async cancelJob(jobId: string): Promise<void> { |
|
|
await apiClient.post(`/api/v1/process/jobs/${jobId}/cancel`) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async replaceBackground( |
|
|
imageId: string, |
|
|
background: string | File |
|
|
): Promise<ProcessingResult> { |
|
|
const formData = new FormData() |
|
|
formData.append('imageId', imageId) |
|
|
|
|
|
if (background instanceof File) { |
|
|
formData.append('background', background) |
|
|
} else { |
|
|
formData.append('backgroundUrl', background) |
|
|
} |
|
|
|
|
|
return apiClient.post<ProcessingResult>( |
|
|
'/api/v1/process/replace-background', |
|
|
formData, |
|
|
{ headers: { 'Content-Type': 'multipart/form-data' } } |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async enhanceImage( |
|
|
imageId: string, |
|
|
enhancements: { |
|
|
sharpness?: number |
|
|
contrast?: number |
|
|
brightness?: number |
|
|
saturation?: number |
|
|
denoise?: boolean |
|
|
upscale?: 2 | 4 |
|
|
} |
|
|
): Promise<ProcessingResult> { |
|
|
return apiClient.post<ProcessingResult>('/api/v1/process/enhance', { |
|
|
imageId, |
|
|
enhancements, |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async generateBackground( |
|
|
prompt: string, |
|
|
style?: 'realistic' | 'artistic' | 'abstract' | 'gradient' |
|
|
): Promise<{ id: string; url: string }> { |
|
|
return apiClient.post('/api/v1/backgrounds/generate', { |
|
|
prompt, |
|
|
style, |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async refineEdges( |
|
|
imageId: string, |
|
|
maskId: string, |
|
|
options: { |
|
|
mode: 'hair' | 'fur' | 'smooth' | 'detailed' |
|
|
strength: number |
|
|
} |
|
|
): Promise<ProcessingResult> { |
|
|
return apiClient.post<ProcessingResult>('/api/v1/process/refine-edges', { |
|
|
imageId, |
|
|
maskId, |
|
|
...options, |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private connectWebSocket(jobId: string) { |
|
|
if (this.socket?.connected) { |
|
|
this.socket.emit('subscribe', { jobId }) |
|
|
return |
|
|
} |
|
|
|
|
|
const wsUrl = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000' |
|
|
this.socket = io(wsUrl, { |
|
|
transports: ['websocket'], |
|
|
query: { jobId }, |
|
|
}) |
|
|
|
|
|
this.socket.on('connect', () => { |
|
|
console.log('WebSocket connected') |
|
|
this.socket?.emit('subscribe', { jobId }) |
|
|
}) |
|
|
|
|
|
this.socket.on('job:progress', (data) => { |
|
|
this.emit('progress', data) |
|
|
}) |
|
|
|
|
|
this.socket.on('job:complete', (data) => { |
|
|
this.emit('complete', data) |
|
|
this.disconnectWebSocket() |
|
|
}) |
|
|
|
|
|
this.socket.on('job:error', (data) => { |
|
|
this.emit('error', data) |
|
|
this.disconnectWebSocket() |
|
|
}) |
|
|
|
|
|
this.socket.on('disconnect', () => { |
|
|
console.log('WebSocket disconnected') |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private disconnectWebSocket() { |
|
|
if (this.socket) { |
|
|
this.socket.disconnect() |
|
|
this.socket = null |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
on(event: string, callback: Function) { |
|
|
if (!this.listeners.has(event)) { |
|
|
this.listeners.set(event, new Set()) |
|
|
} |
|
|
this.listeners.get(event)?.add(callback) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
off(event: string, callback: Function) { |
|
|
this.listeners.get(event)?.delete(callback) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private emit(event: string, data: any) { |
|
|
this.listeners.get(event)?.forEach((callback) => callback(data)) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
estimateProcessingTime( |
|
|
fileSize: number, |
|
|
options: ProcessingOptions |
|
|
): number { |
|
|
const basetime = 1000 |
|
|
const sizeMultiplier = fileSize / (1024 * 1024) |
|
|
const qualityMultiplier = { |
|
|
low: 0.5, |
|
|
medium: 1, |
|
|
high: 1.5, |
|
|
ultra: 2, |
|
|
}[options.quality || 'medium'] |
|
|
|
|
|
return Math.round(basetime * sizeMultiplier * qualityMultiplier) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validateImage(file: File): { valid: boolean; error?: string } { |
|
|
const maxSize = 50 * 1024 * 1024 |
|
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] |
|
|
|
|
|
if (file.size > maxSize) { |
|
|
return { valid: false, error: 'File size exceeds 50MB limit' } |
|
|
} |
|
|
|
|
|
if (!allowedTypes.includes(file.type)) { |
|
|
return { valid: false, error: 'Unsupported file type' } |
|
|
} |
|
|
|
|
|
return { valid: true } |
|
|
} |
|
|
} |
|
|
|
|
|
export const processingAPI = new ProcessingAPI() |
|
|
|
|
|
|
|
|
export async function processImage( |
|
|
file: File, |
|
|
options?: ProcessingOptions, |
|
|
onProgress?: (progress: number) => void |
|
|
): Promise<ProcessingResult> { |
|
|
return processingAPI.processImage(file, options, onProgress) |
|
|
} |
|
|
|
|
|
export async function processBatch( |
|
|
files: File[], |
|
|
options?: ProcessingOptions |
|
|
): Promise<BatchJob> { |
|
|
return processingAPI.processBatch(files, options) |
|
|
} |
|
|
|
|
|
export default processingAPI |