diff --git a/web/package.json b/web/package.json deleted file mode 100644 index fbf4335..0000000 --- a/web/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@tensors/web", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "lint": "eslint src --ext .ts,.tsx", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", - "typescript": "^5.6.0", - "vite": "^6.0.0" - } -} diff --git a/web/src/lib/comfy-client.ts b/web/src/lib/comfy-client.ts deleted file mode 100644 index 91e7216..0000000 --- a/web/src/lib/comfy-client.ts +++ /dev/null @@ -1,592 +0,0 @@ -/** - * ComfyUI WebSocket/REST Client - * - * A TypeScript client for interacting with the ComfyUI API. - * - * Example usage: - * const client = new ComfyClient({ baseUrl: 'http://localhost:8188' }); - * await client.connect(); - * - * client.on('progress', (data) => console.log(`Progress: ${data.value}/${data.max}`)); - * client.on('executed', (data) => console.log('Generated:', data.output)); - * - * const { prompt_id } = await client.queuePrompt(workflow); - * // Listen for events... - */ - -import type { - ComfyClientConfig, - ComfyEventHandler, - ComfyEventMap, - ComfyMessage, - HistoryEntry, - HistoryResponse, - ModelsResponse, - ObjectInfo, - PromptRequest, - PromptResponse, - QueueStatus, - SystemStats, - UploadImageResponse, - ViewImageParams, - WorkflowPrompt, -} from '../types/comfy'; - -type Listener = { - event: K; - handler: ComfyEventHandler; -}; - -export class ComfyClient { - private config: ComfyClientConfig; - private ws: WebSocket | null = null; - private listeners: Listener[] = []; - private reconnectAttempts = 0; - private maxReconnectAttempts = 5; - private reconnectDelay = 1000; - private shouldReconnect = true; - - constructor(config: ComfyClientConfig) { - this.config = { - ...config, - clientId: config.clientId ?? crypto.randomUUID(), - }; - } - - // ============================================================ - // Connection Management - // ============================================================ - - get clientId(): string { - return this.config.clientId!; - } - - get baseUrl(): string { - return this.config.baseUrl; - } - - get isConnected(): boolean { - return this.ws?.readyState === WebSocket.OPEN; - } - - /** - * Connect to ComfyUI WebSocket server - */ - connect(): Promise { - return new Promise((resolve, reject) => { - if (this.isConnected) { - resolve(); - return; - } - - const wsUrl = this.config.baseUrl - .replace(/^http/, 'ws') - .replace(/\/$/, ''); - - this.ws = new WebSocket(`${wsUrl}/ws?clientId=${this.clientId}`); - - this.ws.onopen = () => { - this.reconnectAttempts = 0; - this.emit('connected', undefined); - resolve(); - }; - - this.ws.onclose = () => { - this.emit('disconnected', undefined); - this.handleReconnect(); - }; - - this.ws.onerror = (event) => { - const error = new Error('WebSocket error'); - this.emit('error', error); - reject(error); - }; - - this.ws.onmessage = (event) => { - this.handleMessage(event.data); - }; - }); - } - - /** - * Disconnect from ComfyUI WebSocket server - */ - disconnect(): void { - this.shouldReconnect = false; - if (this.ws) { - this.ws.close(); - this.ws = null; - } - } - - private handleReconnect(): void { - if (!this.shouldReconnect) return; - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - this.emit('error', new Error('Max reconnection attempts reached')); - return; - } - - this.reconnectAttempts++; - const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); - - setTimeout(() => { - this.connect().catch(() => { - // Reconnect will be attempted again via onclose - }); - }, delay); - } - - private handleMessage(data: string): void { - try { - const message = JSON.parse(data) as ComfyMessage; - this.emit(message.type, message.data as ComfyEventMap[typeof message.type]); - } catch (error) { - console.error('Failed to parse WebSocket message:', error); - } - } - - // ============================================================ - // Event Handling - // ============================================================ - - on( - event: K, - handler: ComfyEventHandler - ): () => void { - const listener = { event, handler } as Listener; - this.listeners.push(listener); - - // Return unsubscribe function - return () => { - const index = this.listeners.indexOf(listener); - if (index !== -1) { - this.listeners.splice(index, 1); - } - }; - } - - off( - event: K, - handler?: ComfyEventHandler - ): void { - this.listeners = this.listeners.filter((listener) => { - if (listener.event !== event) return true; - if (handler && listener.handler !== handler) return true; - return false; - }); - } - - private emit( - event: K, - data: ComfyEventMap[K] - ): void { - for (const listener of this.listeners) { - if (listener.event === event) { - (listener.handler as ComfyEventHandler)(data); - } - } - } - - // ============================================================ - // HTTP Helpers - // ============================================================ - - private async fetch( - endpoint: string, - options: RequestInit = {} - ): Promise { - const url = `${this.config.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`HTTP ${response.status}: ${text}`); - } - - return response.json(); - } - - // ============================================================ - // System API - // ============================================================ - - /** - * Get system statistics (RAM, VRAM, versions) - */ - async getSystemStats(): Promise { - return this.fetch('/system_stats'); - } - - /** - * Get node definitions (object_info) - */ - async getObjectInfo(): Promise { - return this.fetch('/object_info'); - } - - /** - * Get specific node definition - */ - async getNodeInfo(nodeType: string): Promise { - return this.fetch(`/object_info/${encodeURIComponent(nodeType)}`); - } - - // ============================================================ - // Queue API - // ============================================================ - - /** - * Get current queue status - */ - async getQueue(): Promise { - return this.fetch('/queue'); - } - - /** - * Queue a prompt/workflow for execution - */ - async queuePrompt( - prompt: WorkflowPrompt, - options: Partial> = {} - ): Promise { - const request: PromptRequest = { - prompt, - client_id: options.client_id ?? this.clientId, - extra_data: options.extra_data, - }; - - return this.fetch('/prompt', { - method: 'POST', - body: JSON.stringify(request), - }); - } - - /** - * Delete a queued item - */ - async deleteQueueItem(deleteType: 'queue' | 'history', promptId: string): Promise { - await this.fetch(`/${deleteType}`, { - method: 'POST', - body: JSON.stringify({ delete: [promptId] }), - }); - } - - /** - * Clear the queue - */ - async clearQueue(): Promise { - await this.fetch('/queue', { - method: 'POST', - body: JSON.stringify({ clear: true }), - }); - } - - /** - * Interrupt the current generation - */ - async interrupt(): Promise { - await this.fetch('/interrupt', { - method: 'POST', - }); - } - - // ============================================================ - // History API - // ============================================================ - - /** - * Get generation history - */ - async getHistory(maxItems?: number): Promise { - const endpoint = maxItems ? `/history?max_items=${maxItems}` : '/history'; - return this.fetch(endpoint); - } - - /** - * Get a specific history entry - */ - async getHistoryEntry(promptId: string): Promise { - const history = await this.fetch( - `/history/${encodeURIComponent(promptId)}` - ); - return history[promptId]; - } - - /** - * Clear history - */ - async clearHistory(): Promise { - await this.fetch('/history', { - method: 'POST', - body: JSON.stringify({ clear: true }), - }); - } - - // ============================================================ - // Image API - // ============================================================ - - /** - * Get URL for viewing an image - */ - getImageUrl(params: ViewImageParams): string { - const searchParams = new URLSearchParams(); - searchParams.set('filename', params.filename); - if (params.subfolder) searchParams.set('subfolder', params.subfolder); - if (params.type) searchParams.set('type', params.type); - if (params.preview) searchParams.set('preview', params.preview); - if (params.channel) searchParams.set('channel', params.channel); - - return `${this.config.baseUrl}/view?${searchParams.toString()}`; - } - - /** - * Upload an image - */ - async uploadImage( - file: File | Blob, - filename: string, - options: { - overwrite?: boolean; - subfolder?: string; - type?: 'input' | 'temp'; - } = {} - ): Promise { - const formData = new FormData(); - formData.append('image', file, filename); - if (options.overwrite) formData.append('overwrite', 'true'); - if (options.subfolder) formData.append('subfolder', options.subfolder); - if (options.type) formData.append('type', options.type); - - const response = await fetch(`${this.config.baseUrl}/upload/image`, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - throw new Error(`Upload failed: ${response.status}`); - } - - return response.json(); - } - - // ============================================================ - // Models API (custom endpoints - may need ComfyUI-Manager) - // ============================================================ - - /** - * Get list of available models by category - * Note: Uses internal folder structure, not a standard endpoint - */ - async getModels(): Promise { - // ComfyUI doesn't have a direct /models endpoint. - // We get model lists from object_info for specific nodes - const objectInfo = await this.getObjectInfo(); - const models: ModelsResponse = {}; - - // Extract checkpoint list from CheckpointLoaderSimple - const checkpointLoader = objectInfo['CheckpointLoaderSimple']; - if (checkpointLoader?.input?.required?.['ckpt_name']) { - const input = checkpointLoader.input.required['ckpt_name']; - if (Array.isArray(input) && Array.isArray(input[0])) { - models.checkpoints = input[0] as string[]; - } - } - - // Extract LoRA list from LoraLoader - const loraLoader = objectInfo['LoraLoader']; - if (loraLoader?.input?.required?.['lora_name']) { - const input = loraLoader.input.required['lora_name']; - if (Array.isArray(input) && Array.isArray(input[0])) { - models.loras = input[0] as string[]; - } - } - - // Extract VAE list from VAELoader - const vaeLoader = objectInfo['VAELoader']; - if (vaeLoader?.input?.required?.['vae_name']) { - const input = vaeLoader.input.required['vae_name']; - if (Array.isArray(input) && Array.isArray(input[0])) { - models.vae = input[0] as string[]; - } - } - - // Extract upscaler list from UpscaleModelLoader - const upscaleLoader = objectInfo['UpscaleModelLoader']; - if (upscaleLoader?.input?.required?.['model_name']) { - const input = upscaleLoader.input.required['model_name']; - if (Array.isArray(input) && Array.isArray(input[0])) { - models.upscale_models = input[0] as string[]; - } - } - - // Extract ControlNet list from ControlNetLoader - const controlnetLoader = objectInfo['ControlNetLoader']; - if (controlnetLoader?.input?.required?.['control_net_name']) { - const input = controlnetLoader.input.required['control_net_name']; - if (Array.isArray(input) && Array.isArray(input[0])) { - models.controlnet = input[0] as string[]; - } - } - - // Extract embeddings from object_info - const clipLoader = objectInfo['CLIPTextEncode']; - if (clipLoader) { - // Embeddings are typically shown in the text field tooltips or separate endpoint - // This is a simplification - full embedding list requires different approach - } - - return models; - } - - /** - * Get list of samplers - */ - async getSamplers(): Promise { - const objectInfo = await this.getObjectInfo(); - const ksampler = objectInfo['KSampler']; - if (ksampler?.input?.required?.['sampler_name']) { - const input = ksampler.input.required['sampler_name']; - if (Array.isArray(input) && Array.isArray(input[0])) { - return input[0] as string[]; - } - } - return []; - } - - /** - * Get list of schedulers - */ - async getSchedulers(): Promise { - const objectInfo = await this.getObjectInfo(); - const ksampler = objectInfo['KSampler']; - if (ksampler?.input?.required?.['scheduler']) { - const input = ksampler.input.required['scheduler']; - if (Array.isArray(input) && Array.isArray(input[0])) { - return input[0] as string[]; - } - } - return []; - } - - // ============================================================ - // Utility Methods - // ============================================================ - - /** - * Wait for a prompt to complete - * Returns the history entry when done - */ - async waitForPrompt( - promptId: string, - options: { - onProgress?: (value: number, max: number) => void; - onExecuting?: (node: string | null) => void; - timeout?: number; - } = {} - ): Promise { - return new Promise((resolve, reject) => { - const timeout = options.timeout ?? 300000; // 5 minutes default - let timeoutId: ReturnType; - - const cleanup = () => { - clearTimeout(timeoutId); - unsubProgress(); - unsubExecuting(); - unsubExecuted(); - unsubError(); - }; - - timeoutId = setTimeout(() => { - cleanup(); - reject(new Error(`Prompt ${promptId} timed out after ${timeout}ms`)); - }, timeout); - - const unsubProgress = this.on('progress', (data) => { - if (data.prompt_id === promptId) { - options.onProgress?.(data.value, data.max); - } - }); - - const unsubExecuting = this.on('executing', (data) => { - if (data.prompt_id === promptId) { - options.onExecuting?.(data.node); - - // When node is null, execution is complete - if (data.node === null) { - cleanup(); - this.getHistoryEntry(promptId) - .then((entry) => { - if (entry) { - resolve(entry); - } else { - reject(new Error(`No history entry for prompt ${promptId}`)); - } - }) - .catch(reject); - } - } - }); - - const unsubExecuted = this.on('executed', (data) => { - // Individual node executed - could be useful for streaming results - // but main completion is signaled by executing with node=null - }); - - const unsubError = this.on('execution_error', (data) => { - if (data.prompt_id === promptId) { - cleanup(); - reject( - new Error( - `Execution error in ${data.node_type}: ${data.exception_message}` - ) - ); - } - }); - }); - } - - /** - * Queue a prompt and wait for completion - */ - async generate( - prompt: WorkflowPrompt, - options: { - onProgress?: (value: number, max: number) => void; - onExecuting?: (node: string | null) => void; - timeout?: number; - } = {} - ): Promise { - const { prompt_id } = await this.queuePrompt(prompt); - return this.waitForPrompt(prompt_id, options); - } -} - -// ============================================================ -// Singleton / Factory -// ============================================================ - -let defaultClient: ComfyClient | null = null; - -/** - * Get or create the default ComfyUI client - */ -export function getComfyClient(config?: ComfyClientConfig): ComfyClient { - if (!defaultClient && !config) { - throw new Error('ComfyClient not initialized. Provide config on first call.'); - } - - if (config) { - defaultClient = new ComfyClient(config); - } - - return defaultClient!; -} - -export default ComfyClient; diff --git a/web/src/types/comfy.ts b/web/src/types/comfy.ts deleted file mode 100644 index 9bbd638..0000000 --- a/web/src/types/comfy.ts +++ /dev/null @@ -1,318 +0,0 @@ -/** - * ComfyUI API Type Definitions - */ - -// ============================================================ -// System Types -// ============================================================ - -export interface SystemStats { - system: { - os: string; - ram_total: number; - ram_free: number; - comfyui_version: string; - required_frontend_version: string; - installed_templates_version: string; - required_templates_version: string; - python_version: string; - pytorch_version: string; - embedded_python: boolean; - argv: string[]; - }; - devices: DeviceInfo[]; -} - -export interface DeviceInfo { - name: string; - type: string; - index: number; - vram_total: number; - vram_free: number; - torch_vram_total: number; - torch_vram_free: number; -} - -// ============================================================ -// Queue Types -// ============================================================ - -export interface QueueStatus { - queue_running: QueueItem[]; - queue_pending: QueueItem[]; -} - -export interface QueueItem { - prompt_id: string; - number: number; - prompt: WorkflowPrompt; - extra_data: Record; - outputs_to_execute: string[]; -} - -// ============================================================ -// Workflow / Prompt Types -// ============================================================ - -export type WorkflowPrompt = Record; - -export interface WorkflowNode { - class_type: string; - inputs: Record; - _meta?: { - title?: string; - }; -} - -export interface PromptRequest { - prompt: WorkflowPrompt; - client_id?: string; - extra_data?: { - extra_pnginfo?: Record; - }; -} - -export interface PromptResponse { - prompt_id: string; - number: number; - node_errors?: Record; -} - -export interface NodeError { - type: string; - message: string; - details: string; - extra_info: Record; -} - -// ============================================================ -// History Types -// ============================================================ - -export type HistoryResponse = Record; - -export interface HistoryEntry { - prompt: [number, string, WorkflowPrompt, Record, string[]]; - outputs: Record; - status: { - status_str: string; - completed: boolean; - messages: Array<[string, Record]>; - }; -} - -export interface NodeOutput { - images?: ImageOutput[]; - [key: string]: unknown; -} - -export interface ImageOutput { - filename: string; - subfolder: string; - type: string; -} - -// ============================================================ -// Object Info Types (Node Definitions) -// ============================================================ - -export type ObjectInfo = Record; - -export interface NodeDefinition { - input: { - required?: Record; - optional?: Record; - hidden?: Record; - }; - input_order?: { - required?: string[]; - optional?: string[]; - }; - output: string[]; - output_is_list: boolean[]; - output_name: string[]; - name: string; - display_name: string; - description: string; - python_module: string; - category: string; - output_node: boolean; - deprecated: boolean; - experimental: boolean; -} - -export type InputDefinition = - | [string, InputOptions?] // Type reference with options - | [string[], InputOptions?]; // Enum choices with options - -export interface InputOptions { - default?: unknown; - min?: number; - max?: number; - step?: number; - round?: number; - tooltip?: string; - multiline?: boolean; - dynamicPrompts?: boolean; - control_after_generate?: boolean; - forceInput?: boolean; -} - -// ============================================================ -// WebSocket Message Types -// ============================================================ - -export type ComfyMessage = - | StatusMessage - | ProgressMessage - | ExecutingMessage - | ExecutedMessage - | ExecutionStartMessage - | ExecutionCachedMessage - | ExecutionErrorMessage; - -export interface StatusMessage { - type: 'status'; - data: { - status: { - exec_info: { - queue_remaining: number; - }; - }; - sid?: string; - }; -} - -export interface ProgressMessage { - type: 'progress'; - data: { - value: number; - max: number; - prompt_id: string; - node: string; - }; -} - -export interface ExecutingMessage { - type: 'executing'; - data: { - node: string | null; - prompt_id: string; - display_node?: string; - }; -} - -export interface ExecutedMessage { - type: 'executed'; - data: { - node: string; - display_node: string; - output: NodeOutput; - prompt_id: string; - }; -} - -export interface ExecutionStartMessage { - type: 'execution_start'; - data: { - prompt_id: string; - timestamp: number; - }; -} - -export interface ExecutionCachedMessage { - type: 'execution_cached'; - data: { - nodes: string[]; - prompt_id: string; - timestamp: number; - }; -} - -export interface ExecutionErrorMessage { - type: 'execution_error'; - data: { - prompt_id: string; - node_id: string; - node_type: string; - exception_message: string; - exception_type: string; - traceback: string[]; - current_inputs?: Record; - current_outputs?: Record[]; - }; -} - -// ============================================================ -// Image View Types -// ============================================================ - -export interface ViewImageParams { - filename: string; - subfolder?: string; - type?: 'output' | 'input' | 'temp'; - preview?: string; - channel?: string; -} - -// ============================================================ -// Models Types -// ============================================================ - -export interface ModelsResponse { - checkpoints?: string[]; - loras?: string[]; - vae?: string[]; - controlnet?: string[]; - upscale_models?: string[]; - embeddings?: string[]; - hypernetworks?: string[]; - clip?: string[]; - clip_vision?: string[]; - style_models?: string[]; - diffusers?: string[]; - gligen?: string[]; - diffusion_models?: string[]; - unet?: string[]; - photomaker?: string[]; -} - -// ============================================================ -// Upload Types -// ============================================================ - -export interface UploadImageResponse { - name: string; - subfolder: string; - type: string; -} - -// ============================================================ -// Client Configuration -// ============================================================ - -export interface ComfyClientConfig { - baseUrl: string; - clientId?: string; -} - -// ============================================================ -// Event Types -// ============================================================ - -export interface ComfyEventMap { - status: StatusMessage['data']; - progress: ProgressMessage['data']; - executing: ExecutingMessage['data']; - executed: ExecutedMessage['data']; - execution_start: ExecutionStartMessage['data']; - execution_cached: ExecutionCachedMessage['data']; - execution_error: ExecutionErrorMessage['data']; - connected: undefined; - disconnected: undefined; - error: Error; -} - -export type ComfyEventHandler = ( - data: ComfyEventMap[K] -) => void; diff --git a/web/tsconfig.json b/web/tsconfig.json deleted file mode 100644 index 4ee1df7..0000000 --- a/web/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true, - "paths": { - "@/*": ["./src/*"] - }, - "baseUrl": "." - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json deleted file mode 100644 index 97ede7e..0000000 --- a/web/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true - }, - "include": ["vite.config.ts"] -}