💬 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