💬 Commit message: Update 2026-02-15 08:56:49, 5 files, 971 lines
📁 Files changed: 5 📝 Lines changed: 971 • package.json • comfy-client.ts • comfy.ts • tsconfig.json • tsconfig.node.json
This commit is contained in:
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<K extends keyof ComfyEventMap> = {
|
|
||||||
event: K;
|
|
||||||
handler: ComfyEventHandler<K>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ComfyClient {
|
|
||||||
private config: ComfyClientConfig;
|
|
||||||
private ws: WebSocket | null = null;
|
|
||||||
private listeners: Listener<keyof ComfyEventMap>[] = [];
|
|
||||||
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<void> {
|
|
||||||
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<K extends keyof ComfyEventMap>(
|
|
||||||
event: K,
|
|
||||||
handler: ComfyEventHandler<K>
|
|
||||||
): () => void {
|
|
||||||
const listener = { event, handler } as Listener<keyof ComfyEventMap>;
|
|
||||||
this.listeners.push(listener);
|
|
||||||
|
|
||||||
// Return unsubscribe function
|
|
||||||
return () => {
|
|
||||||
const index = this.listeners.indexOf(listener);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.listeners.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
off<K extends keyof ComfyEventMap>(
|
|
||||||
event: K,
|
|
||||||
handler?: ComfyEventHandler<K>
|
|
||||||
): void {
|
|
||||||
this.listeners = this.listeners.filter((listener) => {
|
|
||||||
if (listener.event !== event) return true;
|
|
||||||
if (handler && listener.handler !== handler) return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private emit<K extends keyof ComfyEventMap>(
|
|
||||||
event: K,
|
|
||||||
data: ComfyEventMap[K]
|
|
||||||
): void {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
if (listener.event === event) {
|
|
||||||
(listener.handler as ComfyEventHandler<K>)(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HTTP Helpers
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
private async fetch<T>(
|
|
||||||
endpoint: string,
|
|
||||||
options: RequestInit = {}
|
|
||||||
): Promise<T> {
|
|
||||||
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<SystemStats> {
|
|
||||||
return this.fetch<SystemStats>('/system_stats');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get node definitions (object_info)
|
|
||||||
*/
|
|
||||||
async getObjectInfo(): Promise<ObjectInfo> {
|
|
||||||
return this.fetch<ObjectInfo>('/object_info');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get specific node definition
|
|
||||||
*/
|
|
||||||
async getNodeInfo(nodeType: string): Promise<ObjectInfo> {
|
|
||||||
return this.fetch<ObjectInfo>(`/object_info/${encodeURIComponent(nodeType)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Queue API
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current queue status
|
|
||||||
*/
|
|
||||||
async getQueue(): Promise<QueueStatus> {
|
|
||||||
return this.fetch<QueueStatus>('/queue');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue a prompt/workflow for execution
|
|
||||||
*/
|
|
||||||
async queuePrompt(
|
|
||||||
prompt: WorkflowPrompt,
|
|
||||||
options: Partial<Omit<PromptRequest, 'prompt'>> = {}
|
|
||||||
): Promise<PromptResponse> {
|
|
||||||
const request: PromptRequest = {
|
|
||||||
prompt,
|
|
||||||
client_id: options.client_id ?? this.clientId,
|
|
||||||
extra_data: options.extra_data,
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.fetch<PromptResponse>('/prompt', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a queued item
|
|
||||||
*/
|
|
||||||
async deleteQueueItem(deleteType: 'queue' | 'history', promptId: string): Promise<void> {
|
|
||||||
await this.fetch(`/${deleteType}`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ delete: [promptId] }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the queue
|
|
||||||
*/
|
|
||||||
async clearQueue(): Promise<void> {
|
|
||||||
await this.fetch('/queue', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ clear: true }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interrupt the current generation
|
|
||||||
*/
|
|
||||||
async interrupt(): Promise<void> {
|
|
||||||
await this.fetch('/interrupt', {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// History API
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get generation history
|
|
||||||
*/
|
|
||||||
async getHistory(maxItems?: number): Promise<HistoryResponse> {
|
|
||||||
const endpoint = maxItems ? `/history?max_items=${maxItems}` : '/history';
|
|
||||||
return this.fetch<HistoryResponse>(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a specific history entry
|
|
||||||
*/
|
|
||||||
async getHistoryEntry(promptId: string): Promise<HistoryEntry | undefined> {
|
|
||||||
const history = await this.fetch<HistoryResponse>(
|
|
||||||
`/history/${encodeURIComponent(promptId)}`
|
|
||||||
);
|
|
||||||
return history[promptId];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear history
|
|
||||||
*/
|
|
||||||
async clearHistory(): Promise<void> {
|
|
||||||
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<UploadImageResponse> {
|
|
||||||
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<ModelsResponse> {
|
|
||||||
// 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<string[]> {
|
|
||||||
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<string[]> {
|
|
||||||
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<HistoryEntry> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timeout = options.timeout ?? 300000; // 5 minutes default
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout>;
|
|
||||||
|
|
||||||
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<HistoryEntry> {
|
|
||||||
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;
|
|
||||||
@@ -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<string, unknown>;
|
|
||||||
outputs_to_execute: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Workflow / Prompt Types
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export type WorkflowPrompt = Record<string, WorkflowNode>;
|
|
||||||
|
|
||||||
export interface WorkflowNode {
|
|
||||||
class_type: string;
|
|
||||||
inputs: Record<string, unknown>;
|
|
||||||
_meta?: {
|
|
||||||
title?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PromptRequest {
|
|
||||||
prompt: WorkflowPrompt;
|
|
||||||
client_id?: string;
|
|
||||||
extra_data?: {
|
|
||||||
extra_pnginfo?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PromptResponse {
|
|
||||||
prompt_id: string;
|
|
||||||
number: number;
|
|
||||||
node_errors?: Record<string, NodeError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeError {
|
|
||||||
type: string;
|
|
||||||
message: string;
|
|
||||||
details: string;
|
|
||||||
extra_info: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// History Types
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
export type HistoryResponse = Record<string, HistoryEntry>;
|
|
||||||
|
|
||||||
export interface HistoryEntry {
|
|
||||||
prompt: [number, string, WorkflowPrompt, Record<string, unknown>, string[]];
|
|
||||||
outputs: Record<string, NodeOutput>;
|
|
||||||
status: {
|
|
||||||
status_str: string;
|
|
||||||
completed: boolean;
|
|
||||||
messages: Array<[string, Record<string, unknown>]>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string, NodeDefinition>;
|
|
||||||
|
|
||||||
export interface NodeDefinition {
|
|
||||||
input: {
|
|
||||||
required?: Record<string, InputDefinition>;
|
|
||||||
optional?: Record<string, InputDefinition>;
|
|
||||||
hidden?: Record<string, InputDefinition>;
|
|
||||||
};
|
|
||||||
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<string, unknown>;
|
|
||||||
current_outputs?: Record<string, unknown>[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// 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<K extends keyof ComfyEventMap> = (
|
|
||||||
data: ComfyEventMap[K]
|
|
||||||
) => void;
|
|
||||||
@@ -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" }]
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user