diff --git a/package.json b/package.json index 75a7349abe..b1d58df5bb 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "@atlaskit/pragmatic-drag-and-drop": "^1.3.1", "@comfyorg/comfyui-electron-types": "0.4.73-0", "@comfyorg/design-system": "workspace:*", + "@comfyorg/schemas": "workspace:*", "@comfyorg/tailwind-utils": "workspace:*", "@iconify/json": "^2.2.380", "@primeuix/forms": "0.0.2", diff --git a/packages/schemas/package.json b/packages/schemas/package.json new file mode 100644 index 0000000000..1174ad92c4 --- /dev/null +++ b/packages/schemas/package.json @@ -0,0 +1,33 @@ +{ + "name": "@comfyorg/schemas", + "version": "1.0.0", + "type": "module", + "description": "Shared Zod schemas for ComfyUI Frontend", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + "./apiSchema": "./src/apiSchema.ts", + "./colorPaletteSchema": "./src/colorPaletteSchema.ts", + "./keyBindingSchema": "./src/keyBindingSchema.ts", + "./nodeDefSchema": "./src/nodeDefSchema.ts", + "./nodeDef/nodeDefSchemaV2": "./src/nodeDef/nodeDefSchemaV2.ts", + "./nodeDef/migration": "./src/nodeDef/migration.ts", + "./signInSchema": "./src/signInSchema.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "nx": { + "tags": [ + "scope:shared", + "type:schema" + ] + }, + "dependencies": { + "zod": "^3.23.8", + "zod-validation-error": "^3.3.0" + }, + "devDependencies": { + "typescript": "^5.4.5" + } +} diff --git a/packages/schemas/src/apiSchema.ts b/packages/schemas/src/apiSchema.ts new file mode 100644 index 0000000000..007b3e58dd --- /dev/null +++ b/packages/schemas/src/apiSchema.ts @@ -0,0 +1,526 @@ +import { z } from 'zod' + +import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph' +import { + zComfyWorkflow, + zNodeId +} from '@/platform/workflow/validation/schemas/workflowSchema' +import { colorPalettesSchema } from '@/schemas/colorPaletteSchema' +import { zKeybinding } from '@/schemas/keyBindingSchema' +import { NodeBadgeMode } from '@/types/nodeSource' +import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes' + +const zNodeType = z.string() +const zQueueIndex = z.number() +const zPromptId = z.string() +export const resultItemType = z.enum(['input', 'output', 'temp']) +export type ResultItemType = z.infer + +const zResultItem = z.object({ + filename: z.string().optional(), + subfolder: z.string().optional(), + type: resultItemType.optional() +}) +export type ResultItem = z.infer +const zOutputs = z + .object({ + audio: z.array(zResultItem).optional(), + images: z.array(zResultItem).optional(), + video: z.array(zResultItem).optional(), + animated: z.array(z.boolean()).optional() + }) + .passthrough() + +// WS messages +const zStatusWsMessageStatus = z.object({ + exec_info: z.object({ + queue_remaining: z.number().int() + }) +}) + +const zStatusWsMessage = z.object({ + status: zStatusWsMessageStatus.nullish(), + sid: z.string().nullish() +}) + +const zProgressWsMessage = z.object({ + value: z.number().int(), + max: z.number().int(), + prompt_id: zPromptId, + node: zNodeId +}) + +const zNodeProgressState = z.object({ + value: z.number(), + max: z.number(), + state: z.enum(['pending', 'running', 'finished', 'error']), + node_id: zNodeId, + prompt_id: zPromptId, + display_node_id: zNodeId.optional(), + parent_node_id: zNodeId.optional(), + real_node_id: zNodeId.optional() +}) + +const zProgressStateWsMessage = z.object({ + prompt_id: zPromptId, + nodes: z.record(zNodeId, zNodeProgressState) +}) + +const zExecutingWsMessage = z.object({ + node: zNodeId, + display_node: zNodeId, + prompt_id: zPromptId +}) + +const zExecutedWsMessage = zExecutingWsMessage.extend({ + output: zOutputs, + merge: z.boolean().optional() +}) + +const zExecutionWsMessageBase = z.object({ + prompt_id: zPromptId, + timestamp: z.number().int() +}) + +const zExecutionStartWsMessage = zExecutionWsMessageBase +const zExecutionSuccessWsMessage = zExecutionWsMessageBase +const zExecutionCachedWsMessage = zExecutionWsMessageBase.extend({ + nodes: z.array(zNodeId) +}) +const zExecutionInterruptedWsMessage = zExecutionWsMessageBase.extend({ + node_id: zNodeId, + node_type: zNodeType, + executed: z.array(zNodeId) +}) +const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({ + node_id: zNodeId, + node_type: zNodeType, + executed: z.array(zNodeId), + exception_message: z.string(), + exception_type: z.string(), + traceback: z.array(z.string()), + current_inputs: z.any(), + current_outputs: z.any() +}) + +const zProgressTextWsMessage = z.object({ + nodeId: zNodeId, + text: z.string() +}) + +const zDisplayComponentWsMessage = z.object({ + node_id: zNodeId, + component: z.enum(['ChatHistoryWidget']), + props: z.record(z.string(), z.any()).optional() +}) + +const zTerminalSize = z.object({ + cols: z.number(), + row: z.number() +}) +const zLogEntry = z.object({ + t: z.string(), + m: z.string() +}) +const zLogsWsMessage = z.object({ + size: zTerminalSize.optional(), + entries: z.array(zLogEntry) +}) +const zLogRawResponse = z.object({ + size: zTerminalSize, + entries: z.array(zLogEntry) +}) + +const zFeatureFlagsWsMessage = z.record(z.string(), z.any()) + +export type StatusWsMessageStatus = z.infer +export type StatusWsMessage = z.infer +export type ProgressWsMessage = z.infer +export type ExecutingWsMessage = z.infer +export type ExecutedWsMessage = z.infer +export type ExecutionStartWsMessage = z.infer +export type ExecutionSuccessWsMessage = z.infer< + typeof zExecutionSuccessWsMessage +> +export type ExecutionCachedWsMessage = z.infer +export type ExecutionInterruptedWsMessage = z.infer< + typeof zExecutionInterruptedWsMessage +> +export type ExecutionErrorWsMessage = z.infer +export type LogsWsMessage = z.infer +export type ProgressTextWsMessage = z.infer +export type DisplayComponentWsMessage = z.infer< + typeof zDisplayComponentWsMessage +> +export type NodeProgressState = z.infer +export type ProgressStateWsMessage = z.infer +export type FeatureFlagsWsMessage = z.infer +// End of ws messages + +const zPromptInputItem = z.object({ + inputs: z.record(z.string(), z.any()), + class_type: zNodeType +}) + +const zPromptInputs = z.record(zPromptInputItem) + +const zExtraPngInfo = z + .object({ + workflow: zComfyWorkflow + }) + .passthrough() + +const zExtraData = z.object({ + /** extra_pnginfo can be missing is backend execution gets a validation error. */ + extra_pnginfo: zExtraPngInfo.optional(), + client_id: z.string() +}) +const zOutputsToExecute = z.array(zNodeId) + +const zExecutionStartMessage = z.tuple([ + z.literal('execution_start'), + zExecutionStartWsMessage +]) + +const zExecutionSuccessMessage = z.tuple([ + z.literal('execution_success'), + zExecutionSuccessWsMessage +]) + +const zExecutionCachedMessage = z.tuple([ + z.literal('execution_cached'), + zExecutionCachedWsMessage +]) + +const zExecutionInterruptedMessage = z.tuple([ + z.literal('execution_interrupted'), + zExecutionInterruptedWsMessage +]) + +const zExecutionErrorMessage = z.tuple([ + z.literal('execution_error'), + zExecutionErrorWsMessage +]) + +const zStatusMessage = z.union([ + zExecutionStartMessage, + zExecutionSuccessMessage, + zExecutionCachedMessage, + zExecutionInterruptedMessage, + zExecutionErrorMessage +]) + +const zStatus = z.object({ + status_str: z.enum(['success', 'error']), + completed: z.boolean(), + messages: z.array(zStatusMessage) +}) + +const zTaskPrompt = z.tuple([ + zQueueIndex, + zPromptId, + zPromptInputs, + zExtraData, + zOutputsToExecute +]) + +const zRunningTaskItem = z.object({ + taskType: z.literal('Running'), + prompt: zTaskPrompt, + // @Deprecated + remove: z.object({ + name: z.literal('Cancel'), + cb: z.function() + }) +}) + +const zPendingTaskItem = z.object({ + taskType: z.literal('Pending'), + prompt: zTaskPrompt +}) + +const zTaskOutput = z.record(zNodeId, zOutputs) + +const zNodeOutputsMeta = z.object({ + node_id: zNodeId, + display_node: zNodeId, + prompt_id: zPromptId.optional(), + read_node_id: zNodeId.optional() +}) + +const zTaskMeta = z.record(zNodeId, zNodeOutputsMeta) + +const zHistoryTaskItem = z.object({ + taskType: z.literal('History'), + prompt: zTaskPrompt, + status: zStatus.optional(), + outputs: zTaskOutput, + meta: zTaskMeta.optional() +}) + +const zTaskItem = z.union([ + zRunningTaskItem, + zPendingTaskItem, + zHistoryTaskItem +]) + +const zTaskType = z.union([ + z.literal('Running'), + z.literal('Pending'), + z.literal('History') +]) + +export type TaskType = z.infer +export type TaskPrompt = z.infer +export type TaskStatus = z.infer +export type TaskOutput = z.infer + +// `/queue` +export type RunningTaskItem = z.infer +export type PendingTaskItem = z.infer +// `/history` +export type HistoryTaskItem = z.infer +export type TaskItem = z.infer + +const zEmbeddingsResponse = z.array(z.string()) +const zExtensionsResponse = z.array(z.string()) +const zError = z.object({ + type: z.string(), + message: z.string(), + details: z.string(), + extra_info: z + .object({ + input_name: z.string().optional() + }) + .passthrough() + .optional() +}) +const zNodeError = z.object({ + errors: z.array(zError), + class_type: z.string(), + dependent_outputs: z.array(z.any()) +}) +const zPromptResponse = z.object({ + node_errors: z.record(zNodeId, zNodeError).optional(), + prompt_id: z.string().optional(), + exec_info: z + .object({ + queue_remaining: z.number().optional() + }) + .optional(), + error: z.union([z.string(), zError]) +}) + +const zDeviceStats = z.object({ + name: z.string(), + type: z.string(), + index: z.number(), + vram_total: z.number(), + vram_free: z.number(), + torch_vram_total: z.number(), + torch_vram_free: z.number() +}) + +const zSystemStats = z.object({ + system: z.object({ + os: z.string(), + python_version: z.string(), + embedded_python: z.boolean(), + comfyui_version: z.string(), + pytorch_version: z.string(), + required_frontend_version: z.string().optional(), + argv: z.array(z.string()), + ram_total: z.number(), + ram_free: z.number() + }), + devices: z.array(zDeviceStats) +}) +const zUser = z.object({ + storage: z.enum(['server']), + // `migrated` is only available in single-user mode. + migrated: z.boolean().optional(), + // `users` is only available in multi-user server mode. + users: z.record(z.string(), z.string()).optional() +}) +const zUserData = z.array(z.array(z.string(), z.string())) +const zUserDataFullInfo = z.object({ + path: z.string(), + size: z.number(), + modified: z.number() +}) +const zBookmarkCustomization = z.object({ + icon: z.string().optional(), + color: z.string().optional() +}) +export type BookmarkCustomization = z.infer + +const zLinkReleaseTriggerAction = z.enum( + Object.values(LinkReleaseTriggerAction) as [string, ...string[]] +) + +const zNodeBadgeMode = z.enum( + Object.values(NodeBadgeMode) as [string, ...string[]] +) + +const zSettings = z.object({ + 'Comfy.ColorPalette': z.string(), + 'Comfy.CustomColorPalettes': colorPalettesSchema, + 'Comfy.Canvas.BackgroundImage': z.string().optional(), + 'Comfy.ConfirmClear': z.boolean(), + 'Comfy.DevMode': z.boolean(), + 'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(), + 'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(), + 'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(), + 'Comfy.DisableFloatRounding': z.boolean(), + 'Comfy.DisableSliders': z.boolean(), + 'Comfy.DOMClippingEnabled': z.boolean(), + 'Comfy.EditAttention.Delta': z.number(), + 'Comfy.EnableTooltips': z.boolean(), + 'Comfy.EnableWorkflowViewRestore': z.boolean(), + 'Comfy.FloatRoundingPrecision': z.number(), + 'Comfy.Graph.CanvasInfo': z.boolean(), + 'Comfy.Graph.CanvasMenu': z.boolean(), + 'Comfy.Graph.CtrlShiftZoom': z.boolean(), + 'Comfy.Graph.LinkMarkers': z.nativeEnum(LinkMarkerShape), + 'Comfy.Graph.ZoomSpeed': z.number(), + 'Comfy.Group.DoubleClickTitleToEdit': z.boolean(), + 'Comfy.GroupSelectedNodes.Padding': z.number(), + 'Comfy.Locale': z.string(), + 'Comfy.NodeLibrary.Bookmarks': z.array(z.string()), + 'Comfy.NodeLibrary.Bookmarks.V2': z.array(z.string()), + 'Comfy.NodeLibrary.BookmarksCustomization': z.record( + z.string(), + zBookmarkCustomization + ), + 'Comfy.LinkRelease.Action': zLinkReleaseTriggerAction, + 'Comfy.LinkRelease.ActionShift': zLinkReleaseTriggerAction, + 'Comfy.ModelLibrary.AutoLoadAll': z.boolean(), + 'Comfy.ModelLibrary.NameFormat': z.enum(['filename', 'title']), + 'Comfy.NodeSearchBoxImpl.NodePreview': z.boolean(), + 'Comfy.NodeSearchBoxImpl': z.enum(['default', 'simple']), + 'Comfy.NodeSearchBoxImpl.ShowCategory': z.boolean(), + 'Comfy.NodeSearchBoxImpl.ShowIdName': z.boolean(), + 'Comfy.NodeSearchBoxImpl.ShowNodeFrequency': z.boolean(), + 'Comfy.NodeSuggestions.number': z.number(), + 'Comfy.Node.BypassAllLinksOnDelete': z.boolean(), + 'Comfy.Node.Opacity': z.number(), + 'Comfy.Node.MiddleClickRerouteNode': z.boolean(), + 'Comfy.Node.ShowDeprecated': z.boolean(), + 'Comfy.Node.ShowExperimental': z.boolean(), + 'Comfy.Pointer.ClickBufferTime': z.number(), + 'Comfy.Pointer.ClickDrift': z.number(), + 'Comfy.Pointer.DoubleClickTime': z.number(), + 'Comfy.PreviewFormat': z.string(), + 'Comfy.PromptFilename': z.boolean(), + 'Comfy.Sidebar.Location': z.enum(['left', 'right']), + 'Comfy.Sidebar.Size': z.enum(['small', 'normal']), + 'Comfy.Sidebar.UnifiedWidth': z.boolean(), + 'Comfy.SnapToGrid.GridSize': z.number(), + 'Comfy.TextareaWidget.FontSize': z.number(), + 'Comfy.TextareaWidget.Spellcheck': z.boolean(), + 'Comfy.UseNewMenu': z.enum(['Disabled', 'Top', 'Bottom']), + 'Comfy.TreeExplorer.ItemPadding': z.number(), + 'Comfy.Validation.Workflows': z.boolean(), + 'Comfy.Workflow.SortNodeIdOnSave': z.boolean(), + 'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']), + 'Comfy.Workflow.WorkflowTabsPosition': z.enum([ + 'Sidebar', + 'Topbar', + 'Topbar (2nd-row)' + ]), + 'Comfy.Node.DoubleClickTitleToEdit': z.boolean(), + 'Comfy.WidgetControlMode': z.enum(['before', 'after']), + 'Comfy.Window.UnloadConfirmation': z.boolean(), + 'Comfy.NodeBadge.NodeSourceBadgeMode': zNodeBadgeMode, + 'Comfy.NodeBadge.NodeIdBadgeMode': zNodeBadgeMode, + 'Comfy.NodeBadge.NodeLifeCycleBadgeMode': zNodeBadgeMode, + 'Comfy.NodeBadge.ShowApiPricing': z.boolean(), + 'Comfy.Notification.ShowVersionUpdates': z.boolean(), + 'Comfy.QueueButton.BatchCountLimit': z.number(), + 'Comfy.Queue.MaxHistoryItems': z.number(), + 'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding), + 'Comfy.Keybinding.NewBindings': z.array(zKeybinding), + 'Comfy.Extension.Disabled': z.array(z.string()), + 'Comfy.LinkRenderMode': z.number(), + 'Comfy.Node.AutoSnapLinkToSlot': z.boolean(), + 'Comfy.Node.SnapHighlightsNode': z.boolean(), + 'Comfy.Server.ServerConfigValues': z.record(z.string(), z.any()), + 'Comfy.Server.LaunchArgs': z.record(z.string(), z.string()), + 'LiteGraph.Canvas.MaximumFps': z.number(), + 'Comfy.Workflow.ConfirmDelete': z.boolean(), + 'Comfy.Workflow.AutoSaveDelay': z.number(), + 'Comfy.Workflow.AutoSave': z.enum(['off', 'after delay']), + 'Comfy.RerouteBeta': z.boolean(), + 'LiteGraph.Canvas.MinFontSizeForLOD': z.number(), + 'Comfy.Canvas.SelectionToolbox': z.boolean(), + 'LiteGraph.Node.TooltipDelay': z.number(), + 'LiteGraph.ContextMenu.Scaling': z.boolean(), + 'LiteGraph.Reroute.SplineOffset': z.number(), + 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number(), + 'Comfy.Toast.DisableReconnectingToast': z.boolean(), + 'Comfy.Workflow.Persist': z.boolean(), + 'Comfy.TutorialCompleted': z.boolean(), + 'Comfy.InstalledVersion': z.string().nullable(), + 'Comfy.Node.AllowImageSizeDraw': z.boolean(), + 'Comfy.Minimap.Visible': z.boolean(), + 'Comfy.Minimap.NodeColors': z.boolean(), + 'Comfy.Minimap.ShowLinks': z.boolean(), + 'Comfy.Minimap.ShowGroups': z.boolean(), + 'Comfy.Minimap.RenderBypassState': z.boolean(), + 'Comfy.Minimap.RenderErrorState': z.boolean(), + 'Comfy.Canvas.NavigationMode': z.string(), + 'Comfy.Canvas.LeftMouseClickBehavior': z.string(), + 'Comfy.Canvas.MouseWheelScroll': z.string(), + 'Comfy.VueNodes.Enabled': z.boolean(), + 'Comfy.Assets.UseAssetAPI': z.boolean(), + 'Comfy-Desktop.AutoUpdate': z.boolean(), + 'Comfy-Desktop.SendStatistics': z.boolean(), + 'Comfy-Desktop.WindowStyle': z.string(), + 'Comfy-Desktop.UV.PythonInstallMirror': z.string(), + 'Comfy-Desktop.UV.PypiInstallMirror': z.string(), + 'Comfy-Desktop.UV.TorchInstallMirror': z.string(), + 'Comfy.MaskEditor.UseNewEditor': z.boolean(), + 'Comfy.MaskEditor.BrushAdjustmentSpeed': z.number(), + 'Comfy.MaskEditor.UseDominantAxis': z.boolean(), + 'Comfy.Load3D.ShowGrid': z.boolean(), + 'Comfy.Load3D.ShowPreview': z.boolean(), + 'Comfy.Load3D.BackgroundColor': z.string(), + 'Comfy.Load3D.LightIntensity': z.number(), + 'Comfy.Load3D.LightIntensityMaximum': z.number(), + 'Comfy.Load3D.LightIntensityMinimum': z.number(), + 'Comfy.Load3D.LightAdjustmentIncrement': z.number(), + 'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']), + 'Comfy.Load3D.3DViewerEnable': z.boolean(), + 'Comfy.Memory.AllowManualUnload': z.boolean(), + 'pysssss.SnapToGrid': z.boolean(), + /** VHS setting is used for queue video preview support. */ + 'VHS.AdvancedPreviews': z.string(), + /** Release data settings */ + 'Comfy.Release.Version': z.string(), + 'Comfy.Release.Status': z.enum([ + 'skipped', + 'changelog seen', + "what's new seen" + ]), + 'Comfy.Release.Timestamp': z.number(), + /** Settings used for testing */ + 'test.setting': z.any(), + 'main.sub.setting.name': z.any(), + 'single.setting': z.any(), + 'LiteGraph.Node.DefaultPadding': z.boolean(), + 'LiteGraph.Pointer.TrackpadGestures': z.boolean() +}) + +export type EmbeddingsResponse = z.infer +export type ExtensionsResponse = z.infer +export type PromptResponse = z.infer +export type NodeError = z.infer +export type Settings = z.infer +export type DeviceStats = z.infer +export type SystemStats = z.infer +export type User = z.infer +export type UserData = z.infer +export type UserDataFullInfo = z.infer +export type TerminalSize = z.infer +export type LogEntry = z.infer +export type LogsRawResponse = z.infer diff --git a/packages/schemas/src/colorPaletteSchema.ts b/packages/schemas/src/colorPaletteSchema.ts new file mode 100644 index 0000000000..88b7974866 --- /dev/null +++ b/packages/schemas/src/colorPaletteSchema.ts @@ -0,0 +1,116 @@ +import { z } from 'zod' + +import { LiteGraph } from '@/lib/litegraph/src/litegraph' + +const nodeSlotSchema = z.object({ + CLIP: z.string(), + CLIP_VISION: z.string(), + CLIP_VISION_OUTPUT: z.string(), + CONDITIONING: z.string(), + CONTROL_NET: z.string(), + IMAGE: z.string(), + LATENT: z.string(), + MASK: z.string(), + MODEL: z.string(), + STYLE_MODEL: z.string(), + VAE: z.string(), + NOISE: z.string(), + GUIDER: z.string(), + SAMPLER: z.string(), + SIGMAS: z.string(), + TAESD: z.string() +}) + +const litegraphBaseSchema = z.object({ + BACKGROUND_IMAGE: z.string(), + CLEAR_BACKGROUND_COLOR: z.string(), + NODE_TITLE_COLOR: z.string(), + NODE_SELECTED_TITLE_COLOR: z.string(), + NODE_TEXT_SIZE: z.number(), + NODE_TEXT_COLOR: z.string(), + NODE_TEXT_HIGHLIGHT_COLOR: z.string(), + NODE_SUBTEXT_SIZE: z.number(), + NODE_DEFAULT_COLOR: z.string(), + NODE_DEFAULT_BGCOLOR: z.string(), + NODE_DEFAULT_BOXCOLOR: z.string(), + NODE_DEFAULT_SHAPE: z.union([ + z.literal(LiteGraph.BOX_SHAPE), + z.literal(LiteGraph.ROUND_SHAPE), + z.literal(LiteGraph.CARD_SHAPE), + // Legacy palettes have string field for NODE_DEFAULT_SHAPE. + z.string() + ]), + NODE_BOX_OUTLINE_COLOR: z.string(), + NODE_BYPASS_BGCOLOR: z.string(), + NODE_ERROR_COLOUR: z.string(), + DEFAULT_SHADOW_COLOR: z.string(), + DEFAULT_GROUP_FONT: z.number(), + WIDGET_BGCOLOR: z.string(), + WIDGET_OUTLINE_COLOR: z.string(), + WIDGET_TEXT_COLOR: z.string(), + WIDGET_SECONDARY_TEXT_COLOR: z.string(), + WIDGET_DISABLED_TEXT_COLOR: z.string(), + LINK_COLOR: z.string(), + EVENT_LINK_COLOR: z.string(), + CONNECTING_LINK_COLOR: z.string(), + BADGE_FG_COLOR: z.string(), + BADGE_BG_COLOR: z.string() +}) + +const comfyBaseSchema = z.object({ + ['fg-color']: z.string(), + ['bg-color']: z.string(), + ['bg-img']: z.string().optional(), + ['comfy-menu-bg']: z.string(), + ['comfy-menu-secondary-bg']: z.string(), + ['comfy-input-bg']: z.string(), + ['input-text']: z.string(), + ['descrip-text']: z.string(), + ['drag-text']: z.string(), + ['error-text']: z.string(), + ['border-color']: z.string(), + ['tr-even-bg-color']: z.string(), + ['tr-odd-bg-color']: z.string(), + ['content-bg']: z.string(), + ['content-fg']: z.string(), + ['content-hover-bg']: z.string(), + ['content-hover-fg']: z.string(), + ['bar-shadow']: z.string() +}) + +const colorsSchema = z.object({ + node_slot: nodeSlotSchema, + litegraph_base: litegraphBaseSchema, + comfy_base: comfyBaseSchema +}) + +const partialColorsSchema = z.object({ + node_slot: nodeSlotSchema.partial(), + litegraph_base: litegraphBaseSchema.partial(), + comfy_base: comfyBaseSchema.partial() +}) + +// Palette in the wild can have custom metadata fields such as 'version'. +export const paletteSchema = z + .object({ + id: z.string(), + name: z.string(), + colors: partialColorsSchema, + light_theme: z.boolean().optional() + }) + .passthrough() + +const completedPaletteSchema = z + .object({ + id: z.string(), + name: z.string(), + colors: colorsSchema + }) + .passthrough() + +export const colorPalettesSchema = z.record(paletteSchema) + +export type Colors = z.infer +export type Palette = z.infer +export type CompletedPalette = z.infer +export type ColorPalettes = z.infer diff --git a/packages/schemas/src/keyBindingSchema.ts b/packages/schemas/src/keyBindingSchema.ts new file mode 100644 index 0000000000..04cbe319a1 --- /dev/null +++ b/packages/schemas/src/keyBindingSchema.ts @@ -0,0 +1,25 @@ +import { z } from 'zod' + +// KeyCombo schema +const zKeyCombo = z.object({ + key: z.string(), + ctrl: z.boolean().optional(), + alt: z.boolean().optional(), + shift: z.boolean().optional(), + meta: z.boolean().optional() +}) + +// Keybinding schema +export const zKeybinding = z.object({ + commandId: z.string(), + combo: zKeyCombo, + // Optional target element ID to limit keybinding to. + // Note: Currently only used to distinguish between global keybindings + // and litegraph canvas keybindings. + // Do NOT use this field in extensions as it has no effect. + targetElementId: z.string().optional() +}) + +// Infer types from schemas +export type KeyCombo = z.infer +export type Keybinding = z.infer diff --git a/packages/schemas/src/nodeDef/migration.ts b/packages/schemas/src/nodeDef/migration.ts new file mode 100644 index 0000000000..22d91c16e7 --- /dev/null +++ b/packages/schemas/src/nodeDef/migration.ts @@ -0,0 +1,143 @@ +import type { + ComfyNodeDef as ComfyNodeDefV2, + InputSpec as InputSpecV2, + OutputSpec as OutputSpecV2 +} from '@/schemas/nodeDef/nodeDefSchemaV2' +import type { + ComfyNodeDef as ComfyNodeDefV1, + InputSpec as InputSpecV1 +} from '@/schemas/nodeDefSchema' +import { + getComboSpecComboOptions, + isComboInputSpec, + isComboInputSpecV1 +} from '@/schemas/nodeDefSchema' + +/** + * Transforms a V1 node definition to V2 format + * @param nodeDefV1 The V1 node definition to transform + * @returns The transformed V2 node definition + */ +export function transformNodeDefV1ToV2( + nodeDefV1: ComfyNodeDefV1 +): ComfyNodeDefV2 { + // Transform inputs + const inputs: Record = {} + + // Process required inputs + if (nodeDefV1.input?.required) { + Object.entries(nodeDefV1.input.required).forEach(([name, inputSpecV1]) => { + inputs[name] = transformInputSpecV1ToV2(inputSpecV1, { + name, + isOptional: false + }) + }) + } + + // Process optional inputs + if (nodeDefV1.input?.optional) { + Object.entries(nodeDefV1.input.optional).forEach(([name, inputSpecV1]) => { + inputs[name] = transformInputSpecV1ToV2(inputSpecV1, { + name, + isOptional: true + }) + }) + } + + // Transform outputs + const outputs: OutputSpecV2[] = [] + + if (nodeDefV1.output) { + if (Array.isArray(nodeDefV1.output)) { + nodeDefV1.output.forEach((outputType, index) => { + const outputSpec: OutputSpecV2 = { + index, + name: nodeDefV1.output_name?.[index] || `output_${index}`, + type: Array.isArray(outputType) ? 'COMBO' : outputType, + is_list: nodeDefV1.output_is_list?.[index] || false, + tooltip: nodeDefV1.output_tooltips?.[index] + } + + // Add options for combo outputs + if (Array.isArray(outputType)) { + outputSpec.options = outputType + } + + outputs.push(outputSpec) + }) + } else { + console.warn('nodeDefV1.output is not an array:', nodeDefV1.output) + } + } + + // Create the V2 node definition + return { + inputs, + outputs, + hidden: nodeDefV1.input?.hidden, + name: nodeDefV1.name, + display_name: nodeDefV1.display_name, + description: nodeDefV1.description, + category: nodeDefV1.category, + output_node: nodeDefV1.output_node, + python_module: nodeDefV1.python_module, + deprecated: nodeDefV1.deprecated, + experimental: nodeDefV1.experimental + } +} + +/** + * Transforms a V1 input specification to V2 format + * @param inputSpecV1 The V1 input specification to transform + * @param name The name of the input + * @param isOptional Whether the input is optional + * @returns The transformed V2 input specification + */ +export function transformInputSpecV1ToV2( + inputSpecV1: InputSpecV1, + kwargs: { + name: string + isOptional?: boolean + } +): InputSpecV2 { + const { name, isOptional = false } = kwargs + + // Extract options from the input spec + const options = inputSpecV1[1] || {} + + // Base properties for all input types + const baseProps = { + name, + isOptional, + ...options + } + + // Handle different input types + if (isComboInputSpec(inputSpecV1)) { + return { + type: 'COMBO', + ...baseProps, + options: isComboInputSpecV1(inputSpecV1) + ? inputSpecV1[0] + : getComboSpecComboOptions(inputSpecV1) + } + } else if (typeof inputSpecV1[0] === 'string') { + // Handle standard types (INT, FLOAT, BOOLEAN, STRING) and custom types + return { + type: inputSpecV1[0], + ...baseProps + } + } + + // Fallback for any unhandled cases + return { + type: 'UNKNOWN', + ...baseProps + } +} + +export function transformInputSpecV2ToV1( + inputSpecV2: InputSpecV2 +): InputSpecV1 { + return [inputSpecV2.type, inputSpecV2] +} diff --git a/packages/schemas/src/nodeDef/nodeDefSchemaV2.ts b/packages/schemas/src/nodeDef/nodeDefSchemaV2.ts new file mode 100644 index 0000000000..09983d115e --- /dev/null +++ b/packages/schemas/src/nodeDef/nodeDefSchemaV2.ts @@ -0,0 +1,264 @@ +import { z } from 'zod' + +import { + zBaseInputOptions, + zBooleanInputOptions, + zComboInputOptions, + zFloatInputOptions, + zIntInputOptions, + zStringInputOptions +} from '@/schemas/nodeDefSchema' + +const zIntInputSpec = zIntInputOptions.extend({ + type: z.literal('INT'), + name: z.string(), + isOptional: z.boolean().optional() +}) + +const zFloatInputSpec = zFloatInputOptions.extend({ + type: z.literal('FLOAT'), + name: z.string(), + isOptional: z.boolean().optional() +}) + +const zBooleanInputSpec = zBooleanInputOptions.extend({ + type: z.literal('BOOLEAN'), + name: z.string(), + isOptional: z.boolean().optional() +}) + +const zStringInputSpec = zStringInputOptions.extend({ + type: z.literal('STRING'), + name: z.string(), + isOptional: z.boolean().optional() +}) + +const zComboInputSpec = zComboInputOptions.extend({ + type: z.literal('COMBO'), + name: z.string(), + isOptional: z.boolean().optional() +}) + +const zColorInputSpec = zBaseInputOptions.extend({ + type: z.literal('COLOR'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + default: z.string().optional() + }) + .optional() +}) + +const zFileUploadInputSpec = zBaseInputOptions.extend({ + type: z.literal('FILEUPLOAD'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z.record(z.unknown()).optional() +}) + +const zImageInputSpec = zBaseInputOptions.extend({ + type: z.literal('IMAGE'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z.record(z.unknown()).optional() +}) + +const zImageCompareInputSpec = zBaseInputOptions.extend({ + type: z.literal('IMAGECOMPARE'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z.record(z.unknown()).optional() +}) + +const zMarkdownInputSpec = zBaseInputOptions.extend({ + type: z.literal('MARKDOWN'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + content: z.string().optional() + }) + .optional() +}) + +const zTreeSelectInputSpec = zBaseInputOptions.extend({ + type: z.literal('TREESELECT'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + multiple: z.boolean().optional(), + values: z.array(z.unknown()).optional() + }) + .optional() +}) + +const zMultiSelectInputSpec = zBaseInputOptions.extend({ + type: z.literal('MULTISELECT'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + values: z.array(z.string()).optional() + }) + .optional() +}) + +const zChartInputSpec = zBaseInputOptions.extend({ + type: z.literal('CHART'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + type: z.enum(['bar', 'line']).optional(), + data: z.object({}).optional() + }) + .optional() +}) + +const zGalleriaInputSpec = zBaseInputOptions.extend({ + type: z.literal('GALLERIA'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + images: z.array(z.string()).optional() + }) + .optional() +}) + +const zSelectButtonInputSpec = zBaseInputOptions.extend({ + type: z.literal('SELECTBUTTON'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + values: z.array(z.string()).optional() + }) + .optional() +}) + +const zTextareaInputSpec = zBaseInputOptions.extend({ + type: z.literal('TEXTAREA'), + name: z.string(), + isOptional: z.boolean().optional(), + options: z + .object({ + rows: z.number().optional(), + cols: z.number().optional(), + default: z.string().optional() + }) + .optional() +}) + +const zCustomInputSpec = zBaseInputOptions.extend({ + type: z.string(), + name: z.string(), + isOptional: z.boolean().optional() +}) + +const zInputSpec = z.union([ + zIntInputSpec, + zFloatInputSpec, + zBooleanInputSpec, + zStringInputSpec, + zComboInputSpec, + zColorInputSpec, + zFileUploadInputSpec, + zImageInputSpec, + zImageCompareInputSpec, + zMarkdownInputSpec, + zTreeSelectInputSpec, + zMultiSelectInputSpec, + zChartInputSpec, + zGalleriaInputSpec, + zSelectButtonInputSpec, + zTextareaInputSpec, + zCustomInputSpec +]) + +// Output specs +const zOutputSpec = z.object({ + index: z.number(), + name: z.string(), + type: z.string(), + is_list: z.boolean(), + options: z.array(z.any()).optional(), + tooltip: z.string().optional() +}) + +// Main node definition schema +export const zComfyNodeDef = z.object({ + inputs: z.record(zInputSpec), + outputs: z.array(zOutputSpec), + hidden: z.record(z.any()).optional(), + + name: z.string(), + display_name: z.string(), + description: z.string(), + help: z.string().optional(), + category: z.string(), + output_node: z.boolean(), + python_module: z.string(), + deprecated: z.boolean().optional(), + experimental: z.boolean().optional(), + api_node: z.boolean().optional() +}) + +// Export types +type IntInputSpec = z.infer +type FloatInputSpec = z.infer +type BooleanInputSpec = z.infer +type StringInputSpec = z.infer +export type ComboInputSpec = z.infer +export type ColorInputSpec = z.infer +export type FileUploadInputSpec = z.infer +export type ImageCompareInputSpec = z.infer +export type TreeSelectInputSpec = z.infer +export type MultiSelectInputSpec = z.infer +export type ChartInputSpec = z.infer +export type GalleriaInputSpec = z.infer +export type SelectButtonInputSpec = z.infer +export type TextareaInputSpec = z.infer +export type CustomInputSpec = z.infer + +export type InputSpec = z.infer +export type OutputSpec = z.infer +export type ComfyNodeDef = z.infer + +export const isIntInputSpec = ( + inputSpec: InputSpec +): inputSpec is IntInputSpec => { + return inputSpec.type === 'INT' +} + +export const isFloatInputSpec = ( + inputSpec: InputSpec +): inputSpec is FloatInputSpec => { + return inputSpec.type === 'FLOAT' +} + +export const isBooleanInputSpec = ( + inputSpec: InputSpec +): inputSpec is BooleanInputSpec => { + return inputSpec.type === 'BOOLEAN' +} + +export const isStringInputSpec = ( + inputSpec: InputSpec +): inputSpec is StringInputSpec => { + return inputSpec.type === 'STRING' +} + +export const isComboInputSpec = ( + inputSpec: InputSpec +): inputSpec is ComboInputSpec => { + return inputSpec.type === 'COMBO' +} + +export const isChartInputSpec = ( + inputSpec: InputSpec +): inputSpec is ChartInputSpec => { + return inputSpec.type === 'CHART' +} diff --git a/packages/schemas/src/nodeDefSchema.ts b/packages/schemas/src/nodeDefSchema.ts new file mode 100644 index 0000000000..75890d6096 --- /dev/null +++ b/packages/schemas/src/nodeDefSchema.ts @@ -0,0 +1,255 @@ +import { z } from 'zod' +import { fromZodError } from 'zod-validation-error' + +import { resultItemType } from '@/schemas/apiSchema' + +const zComboOption = z.union([z.string(), z.number()]) +const zRemoteWidgetConfig = z.object({ + route: z.string().url().or(z.string().startsWith('/')), + refresh: z.number().gte(128).safe().or(z.number().lte(0).safe()).optional(), + response_key: z.string().optional(), + query_params: z.record(z.string(), z.string()).optional(), + refresh_button: z.boolean().optional(), + control_after_refresh: z.enum(['first', 'last']).optional(), + timeout: z.number().gte(0).optional(), + max_retries: z.number().gte(0).optional() +}) +const zMultiSelectOption = z.object({ + placeholder: z.string().optional(), + chip: z.boolean().optional() +}) + +export const zBaseInputOptions = z + .object({ + default: z.any().optional(), + defaultInput: z.boolean().optional(), + forceInput: z.boolean().optional(), + tooltip: z.string().optional(), + hidden: z.boolean().optional(), + advanced: z.boolean().optional(), + widgetType: z.string().optional(), + /** Backend-only properties. */ + rawLink: z.boolean().optional(), + lazy: z.boolean().optional() + }) + .passthrough() + +const zNumericInputOptions = zBaseInputOptions.extend({ + min: z.number().optional(), + max: z.number().optional(), + step: z.number().optional(), + /** Note: Many node authors are using INT/FLOAT to pass list of INT/FLOAT. */ + default: z.union([z.number(), z.array(z.number())]).optional(), + display: z.enum(['slider', 'number', 'knob']).optional() +}) + +export const zIntInputOptions = zNumericInputOptions.extend({ + /** + * If true, a linked widget will be added to the node to select the mode + * of `control_after_generate`. + */ + control_after_generate: z.boolean().optional() +}) + +export const zFloatInputOptions = zNumericInputOptions.extend({ + round: z.union([z.number(), z.literal(false)]).optional() +}) + +export const zBooleanInputOptions = zBaseInputOptions.extend({ + label_on: z.string().optional(), + label_off: z.string().optional(), + default: z.boolean().optional() +}) + +export const zStringInputOptions = zBaseInputOptions.extend({ + default: z.string().optional(), + multiline: z.boolean().optional(), + dynamicPrompts: z.boolean().optional(), + + // Multiline-only fields + defaultVal: z.string().optional(), + placeholder: z.string().optional() +}) + +export const zComboInputOptions = zBaseInputOptions.extend({ + control_after_generate: z.boolean().optional(), + image_upload: z.boolean().optional(), + image_folder: resultItemType.optional(), + allow_batch: z.boolean().optional(), + video_upload: z.boolean().optional(), + audio_upload: z.boolean().optional(), + animated_image_upload: z.boolean().optional(), + options: z.array(zComboOption).optional(), + remote: zRemoteWidgetConfig.optional(), + /** Whether the widget is a multi-select widget. */ + multi_select: zMultiSelectOption.optional() +}) + +const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()]) +const zFloatInputSpec = z.tuple([ + z.literal('FLOAT'), + zFloatInputOptions.optional() +]) +const zBooleanInputSpec = z.tuple([ + z.literal('BOOLEAN'), + zBooleanInputOptions.optional() +]) +const zStringInputSpec = z.tuple([ + z.literal('STRING'), + zStringInputOptions.optional() +]) +/** + * Legacy combo syntax. + * @deprecated Use `zComboInputSpecV2` instead. + */ +const zComboInputSpec = z.tuple([ + z.array(zComboOption), + zComboInputOptions.optional() +]) +const zComboInputSpecV2 = z.tuple([ + z.literal('COMBO'), + zComboInputOptions.optional() +]) + +export function isComboInputSpecV1( + inputSpec: InputSpec +): inputSpec is ComboInputSpec { + return Array.isArray(inputSpec[0]) +} + +export function isIntInputSpec( + inputSpec: InputSpec +): inputSpec is IntInputSpec { + return inputSpec[0] === 'INT' +} + +export function isFloatInputSpec( + inputSpec: InputSpec +): inputSpec is FloatInputSpec { + return inputSpec[0] === 'FLOAT' +} + +export function isComboInputSpecV2( + inputSpec: InputSpec +): inputSpec is ComboInputSpecV2 { + return inputSpec[0] === 'COMBO' +} + +export function isComboInputSpec( + inputSpec: InputSpec +): inputSpec is ComboInputSpec | ComboInputSpecV2 { + return isComboInputSpecV1(inputSpec) || isComboInputSpecV2(inputSpec) +} + +/** + * Get the type of an input spec. + * + * @param inputSpec - The input spec to get the type of. + * @returns The type of the input spec. + */ +export function getInputSpecType(inputSpec: InputSpec): string { + return isComboInputSpec(inputSpec) ? 'COMBO' : inputSpec[0] +} + +/** + * Get the combo options from a combo input spec. + * + * @param inputSpec - The input spec to get the combo options from. + * @returns The combo options. + */ +export function getComboSpecComboOptions( + inputSpec: ComboInputSpec | ComboInputSpecV2 +): (number | string)[] { + return ( + (isComboInputSpecV2(inputSpec) ? inputSpec[1]?.options : inputSpec[0]) ?? [] + ) +} + +const excludedLiterals = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO']) +const zCustomInputSpec = z.tuple([ + z.string().refine((value) => !excludedLiterals.has(value)), + zBaseInputOptions.optional() +]) + +const zInputSpec = z.union([ + zIntInputSpec, + zFloatInputSpec, + zBooleanInputSpec, + zStringInputSpec, + zComboInputSpec, + zComboInputSpecV2, + zCustomInputSpec +]) + +const zComfyInputsSpec = z.object({ + required: z.record(zInputSpec).optional(), + optional: z.record(zInputSpec).optional(), + // Frontend repo is not using it, but some custom nodes are using the + // hidden field to pass various values. + hidden: z.record(z.any()).optional() +}) + +const zComfyNodeDataType = z.string() +const zComfyComboOutput = z.array(zComboOption) +const zComfyOutputTypesSpec = z.array( + z.union([zComfyNodeDataType, zComfyComboOutput]) +) + +export const zComfyNodeDef = z.object({ + input: zComfyInputsSpec.optional(), + output: zComfyOutputTypesSpec.optional(), + output_is_list: z.array(z.boolean()).optional(), + output_name: z.array(z.string()).optional(), + output_tooltips: z.array(z.string()).optional(), + name: z.string(), + display_name: z.string(), + description: z.string(), + help: z.string().optional(), + category: z.string(), + output_node: z.boolean(), + python_module: z.string(), + deprecated: z.boolean().optional(), + experimental: z.boolean().optional(), + /** + * Whether the node is an API node. Running API nodes requires login to + * Comfy Org account. + * https://docs.comfy.org/tutorials/api-nodes/overview + */ + api_node: z.boolean().optional(), + /** + * Specifies the order of inputs for each input category. + * Used to ensure consistent widget ordering regardless of JSON serialization. + * Keys are 'required', 'optional', etc., values are arrays of input names. + */ + input_order: z.record(z.array(z.string())).optional() +}) + +// `/object_info` +export type ComfyInputsSpec = z.infer +export type ComfyOutputTypesSpec = z.infer +export type ComfyNodeDef = z.infer +export type RemoteWidgetConfig = z.infer + +export type ComboInputOptions = z.infer +export type NumericInputOptions = z.infer + +export type IntInputSpec = z.infer +export type FloatInputSpec = z.infer +export type ComboInputSpec = z.infer +export type ComboInputSpecV2 = z.infer +export type InputSpec = z.infer + +export function validateComfyNodeDef( + data: any, + onError: (error: string) => void = console.warn +): ComfyNodeDef | null { + const result = zComfyNodeDef.safeParse(data) + if (!result.success) { + const zodError = fromZodError(result.error) + onError( + `Invalid ComfyNodeDef: ${JSON.stringify(data)}\n${zodError.message}` + ) + return null + } + return result.data +} diff --git a/packages/schemas/src/signInSchema.ts b/packages/schemas/src/signInSchema.ts new file mode 100644 index 0000000000..1e5a44198a --- /dev/null +++ b/packages/schemas/src/signInSchema.ts @@ -0,0 +1,60 @@ +import { z } from 'zod' + +import { t } from '@/i18n' + +export const apiKeySchema = z.object({ + apiKey: z + .string() + .trim() + .startsWith('comfyui-', t('validation.prefix', { prefix: 'comfyui-' })) + .length(72, t('validation.length', { length: 72 })) +}) + +export const signInSchema = z.object({ + email: z + .string() + .email(t('validation.invalidEmail')) + .min(1, t('validation.required')), + password: z.string().min(1, t('validation.required')) +}) + +export type SignInData = z.infer + +const passwordSchema = z.object({ + password: z + .string() + .min(8, t('validation.minLength', { length: 8 })) + .max(32, t('validation.maxLength', { length: 32 })) + .regex(/[A-Z]/, t('validation.password.uppercase')) + .regex(/[a-z]/, t('validation.password.lowercase')) + .regex(/\d/, t('validation.password.number')) + .regex(/[^A-Za-z0-9]/, t('validation.password.special')), + confirmPassword: z.string().min(1, t('validation.required')) +}) + +export const updatePasswordSchema = passwordSchema.refine( + (data) => data.password === data.confirmPassword, + { + message: t('validation.password.match'), + path: ['confirmPassword'] + } +) + +export const signUpSchema = passwordSchema + .extend({ + email: z + .string() + .email(t('validation.invalidEmail')) + .min(1, t('validation.required')), + personalDataConsent: z.boolean() + }) + .refine((data) => data.password === data.confirmPassword, { + message: t('validation.password.match'), + path: ['confirmPassword'] + }) + .refine((data) => data.personalDataConsent === true, { + message: t('validation.personalDataConsentRequired'), + path: ['personalDataConsent'] + }) + +export type SignUpData = z.infer diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json new file mode 100644 index 0000000000..60c7df1811 --- /dev/null +++ b/packages/schemas/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fb5c1a7dd..9fff958b44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@comfyorg/design-system': specifier: workspace:* version: link:packages/design-system + '@comfyorg/schemas': + specifier: workspace:* + version: link:packages/schemas '@comfyorg/tailwind-utils': specifier: workspace:* version: link:packages/tailwind-utils @@ -368,6 +371,19 @@ importers: specifier: ^5.4.5 version: 5.9.2 + packages/schemas: + dependencies: + zod: + specifier: ^3.23.8 + version: 3.24.1 + zod-validation-error: + specifier: ^3.3.0 + version: 3.3.0(zod@3.24.1) + devDependencies: + typescript: + specifier: ^5.4.5 + version: 5.9.2 + packages/tailwind-utils: dependencies: clsx: diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 007b3e58dd..b83c15a79c 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -1,526 +1 @@ -import { z } from 'zod' - -import { LinkMarkerShape } from '@/lib/litegraph/src/litegraph' -import { - zComfyWorkflow, - zNodeId -} from '@/platform/workflow/validation/schemas/workflowSchema' -import { colorPalettesSchema } from '@/schemas/colorPaletteSchema' -import { zKeybinding } from '@/schemas/keyBindingSchema' -import { NodeBadgeMode } from '@/types/nodeSource' -import { LinkReleaseTriggerAction } from '@/types/searchBoxTypes' - -const zNodeType = z.string() -const zQueueIndex = z.number() -const zPromptId = z.string() -export const resultItemType = z.enum(['input', 'output', 'temp']) -export type ResultItemType = z.infer - -const zResultItem = z.object({ - filename: z.string().optional(), - subfolder: z.string().optional(), - type: resultItemType.optional() -}) -export type ResultItem = z.infer -const zOutputs = z - .object({ - audio: z.array(zResultItem).optional(), - images: z.array(zResultItem).optional(), - video: z.array(zResultItem).optional(), - animated: z.array(z.boolean()).optional() - }) - .passthrough() - -// WS messages -const zStatusWsMessageStatus = z.object({ - exec_info: z.object({ - queue_remaining: z.number().int() - }) -}) - -const zStatusWsMessage = z.object({ - status: zStatusWsMessageStatus.nullish(), - sid: z.string().nullish() -}) - -const zProgressWsMessage = z.object({ - value: z.number().int(), - max: z.number().int(), - prompt_id: zPromptId, - node: zNodeId -}) - -const zNodeProgressState = z.object({ - value: z.number(), - max: z.number(), - state: z.enum(['pending', 'running', 'finished', 'error']), - node_id: zNodeId, - prompt_id: zPromptId, - display_node_id: zNodeId.optional(), - parent_node_id: zNodeId.optional(), - real_node_id: zNodeId.optional() -}) - -const zProgressStateWsMessage = z.object({ - prompt_id: zPromptId, - nodes: z.record(zNodeId, zNodeProgressState) -}) - -const zExecutingWsMessage = z.object({ - node: zNodeId, - display_node: zNodeId, - prompt_id: zPromptId -}) - -const zExecutedWsMessage = zExecutingWsMessage.extend({ - output: zOutputs, - merge: z.boolean().optional() -}) - -const zExecutionWsMessageBase = z.object({ - prompt_id: zPromptId, - timestamp: z.number().int() -}) - -const zExecutionStartWsMessage = zExecutionWsMessageBase -const zExecutionSuccessWsMessage = zExecutionWsMessageBase -const zExecutionCachedWsMessage = zExecutionWsMessageBase.extend({ - nodes: z.array(zNodeId) -}) -const zExecutionInterruptedWsMessage = zExecutionWsMessageBase.extend({ - node_id: zNodeId, - node_type: zNodeType, - executed: z.array(zNodeId) -}) -const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({ - node_id: zNodeId, - node_type: zNodeType, - executed: z.array(zNodeId), - exception_message: z.string(), - exception_type: z.string(), - traceback: z.array(z.string()), - current_inputs: z.any(), - current_outputs: z.any() -}) - -const zProgressTextWsMessage = z.object({ - nodeId: zNodeId, - text: z.string() -}) - -const zDisplayComponentWsMessage = z.object({ - node_id: zNodeId, - component: z.enum(['ChatHistoryWidget']), - props: z.record(z.string(), z.any()).optional() -}) - -const zTerminalSize = z.object({ - cols: z.number(), - row: z.number() -}) -const zLogEntry = z.object({ - t: z.string(), - m: z.string() -}) -const zLogsWsMessage = z.object({ - size: zTerminalSize.optional(), - entries: z.array(zLogEntry) -}) -const zLogRawResponse = z.object({ - size: zTerminalSize, - entries: z.array(zLogEntry) -}) - -const zFeatureFlagsWsMessage = z.record(z.string(), z.any()) - -export type StatusWsMessageStatus = z.infer -export type StatusWsMessage = z.infer -export type ProgressWsMessage = z.infer -export type ExecutingWsMessage = z.infer -export type ExecutedWsMessage = z.infer -export type ExecutionStartWsMessage = z.infer -export type ExecutionSuccessWsMessage = z.infer< - typeof zExecutionSuccessWsMessage -> -export type ExecutionCachedWsMessage = z.infer -export type ExecutionInterruptedWsMessage = z.infer< - typeof zExecutionInterruptedWsMessage -> -export type ExecutionErrorWsMessage = z.infer -export type LogsWsMessage = z.infer -export type ProgressTextWsMessage = z.infer -export type DisplayComponentWsMessage = z.infer< - typeof zDisplayComponentWsMessage -> -export type NodeProgressState = z.infer -export type ProgressStateWsMessage = z.infer -export type FeatureFlagsWsMessage = z.infer -// End of ws messages - -const zPromptInputItem = z.object({ - inputs: z.record(z.string(), z.any()), - class_type: zNodeType -}) - -const zPromptInputs = z.record(zPromptInputItem) - -const zExtraPngInfo = z - .object({ - workflow: zComfyWorkflow - }) - .passthrough() - -const zExtraData = z.object({ - /** extra_pnginfo can be missing is backend execution gets a validation error. */ - extra_pnginfo: zExtraPngInfo.optional(), - client_id: z.string() -}) -const zOutputsToExecute = z.array(zNodeId) - -const zExecutionStartMessage = z.tuple([ - z.literal('execution_start'), - zExecutionStartWsMessage -]) - -const zExecutionSuccessMessage = z.tuple([ - z.literal('execution_success'), - zExecutionSuccessWsMessage -]) - -const zExecutionCachedMessage = z.tuple([ - z.literal('execution_cached'), - zExecutionCachedWsMessage -]) - -const zExecutionInterruptedMessage = z.tuple([ - z.literal('execution_interrupted'), - zExecutionInterruptedWsMessage -]) - -const zExecutionErrorMessage = z.tuple([ - z.literal('execution_error'), - zExecutionErrorWsMessage -]) - -const zStatusMessage = z.union([ - zExecutionStartMessage, - zExecutionSuccessMessage, - zExecutionCachedMessage, - zExecutionInterruptedMessage, - zExecutionErrorMessage -]) - -const zStatus = z.object({ - status_str: z.enum(['success', 'error']), - completed: z.boolean(), - messages: z.array(zStatusMessage) -}) - -const zTaskPrompt = z.tuple([ - zQueueIndex, - zPromptId, - zPromptInputs, - zExtraData, - zOutputsToExecute -]) - -const zRunningTaskItem = z.object({ - taskType: z.literal('Running'), - prompt: zTaskPrompt, - // @Deprecated - remove: z.object({ - name: z.literal('Cancel'), - cb: z.function() - }) -}) - -const zPendingTaskItem = z.object({ - taskType: z.literal('Pending'), - prompt: zTaskPrompt -}) - -const zTaskOutput = z.record(zNodeId, zOutputs) - -const zNodeOutputsMeta = z.object({ - node_id: zNodeId, - display_node: zNodeId, - prompt_id: zPromptId.optional(), - read_node_id: zNodeId.optional() -}) - -const zTaskMeta = z.record(zNodeId, zNodeOutputsMeta) - -const zHistoryTaskItem = z.object({ - taskType: z.literal('History'), - prompt: zTaskPrompt, - status: zStatus.optional(), - outputs: zTaskOutput, - meta: zTaskMeta.optional() -}) - -const zTaskItem = z.union([ - zRunningTaskItem, - zPendingTaskItem, - zHistoryTaskItem -]) - -const zTaskType = z.union([ - z.literal('Running'), - z.literal('Pending'), - z.literal('History') -]) - -export type TaskType = z.infer -export type TaskPrompt = z.infer -export type TaskStatus = z.infer -export type TaskOutput = z.infer - -// `/queue` -export type RunningTaskItem = z.infer -export type PendingTaskItem = z.infer -// `/history` -export type HistoryTaskItem = z.infer -export type TaskItem = z.infer - -const zEmbeddingsResponse = z.array(z.string()) -const zExtensionsResponse = z.array(z.string()) -const zError = z.object({ - type: z.string(), - message: z.string(), - details: z.string(), - extra_info: z - .object({ - input_name: z.string().optional() - }) - .passthrough() - .optional() -}) -const zNodeError = z.object({ - errors: z.array(zError), - class_type: z.string(), - dependent_outputs: z.array(z.any()) -}) -const zPromptResponse = z.object({ - node_errors: z.record(zNodeId, zNodeError).optional(), - prompt_id: z.string().optional(), - exec_info: z - .object({ - queue_remaining: z.number().optional() - }) - .optional(), - error: z.union([z.string(), zError]) -}) - -const zDeviceStats = z.object({ - name: z.string(), - type: z.string(), - index: z.number(), - vram_total: z.number(), - vram_free: z.number(), - torch_vram_total: z.number(), - torch_vram_free: z.number() -}) - -const zSystemStats = z.object({ - system: z.object({ - os: z.string(), - python_version: z.string(), - embedded_python: z.boolean(), - comfyui_version: z.string(), - pytorch_version: z.string(), - required_frontend_version: z.string().optional(), - argv: z.array(z.string()), - ram_total: z.number(), - ram_free: z.number() - }), - devices: z.array(zDeviceStats) -}) -const zUser = z.object({ - storage: z.enum(['server']), - // `migrated` is only available in single-user mode. - migrated: z.boolean().optional(), - // `users` is only available in multi-user server mode. - users: z.record(z.string(), z.string()).optional() -}) -const zUserData = z.array(z.array(z.string(), z.string())) -const zUserDataFullInfo = z.object({ - path: z.string(), - size: z.number(), - modified: z.number() -}) -const zBookmarkCustomization = z.object({ - icon: z.string().optional(), - color: z.string().optional() -}) -export type BookmarkCustomization = z.infer - -const zLinkReleaseTriggerAction = z.enum( - Object.values(LinkReleaseTriggerAction) as [string, ...string[]] -) - -const zNodeBadgeMode = z.enum( - Object.values(NodeBadgeMode) as [string, ...string[]] -) - -const zSettings = z.object({ - 'Comfy.ColorPalette': z.string(), - 'Comfy.CustomColorPalettes': colorPalettesSchema, - 'Comfy.Canvas.BackgroundImage': z.string().optional(), - 'Comfy.ConfirmClear': z.boolean(), - 'Comfy.DevMode': z.boolean(), - 'Comfy.Workflow.ShowMissingNodesWarning': z.boolean(), - 'Comfy.Workflow.ShowMissingModelsWarning': z.boolean(), - 'Comfy.Workflow.WarnBlueprintOverwrite': z.boolean(), - 'Comfy.DisableFloatRounding': z.boolean(), - 'Comfy.DisableSliders': z.boolean(), - 'Comfy.DOMClippingEnabled': z.boolean(), - 'Comfy.EditAttention.Delta': z.number(), - 'Comfy.EnableTooltips': z.boolean(), - 'Comfy.EnableWorkflowViewRestore': z.boolean(), - 'Comfy.FloatRoundingPrecision': z.number(), - 'Comfy.Graph.CanvasInfo': z.boolean(), - 'Comfy.Graph.CanvasMenu': z.boolean(), - 'Comfy.Graph.CtrlShiftZoom': z.boolean(), - 'Comfy.Graph.LinkMarkers': z.nativeEnum(LinkMarkerShape), - 'Comfy.Graph.ZoomSpeed': z.number(), - 'Comfy.Group.DoubleClickTitleToEdit': z.boolean(), - 'Comfy.GroupSelectedNodes.Padding': z.number(), - 'Comfy.Locale': z.string(), - 'Comfy.NodeLibrary.Bookmarks': z.array(z.string()), - 'Comfy.NodeLibrary.Bookmarks.V2': z.array(z.string()), - 'Comfy.NodeLibrary.BookmarksCustomization': z.record( - z.string(), - zBookmarkCustomization - ), - 'Comfy.LinkRelease.Action': zLinkReleaseTriggerAction, - 'Comfy.LinkRelease.ActionShift': zLinkReleaseTriggerAction, - 'Comfy.ModelLibrary.AutoLoadAll': z.boolean(), - 'Comfy.ModelLibrary.NameFormat': z.enum(['filename', 'title']), - 'Comfy.NodeSearchBoxImpl.NodePreview': z.boolean(), - 'Comfy.NodeSearchBoxImpl': z.enum(['default', 'simple']), - 'Comfy.NodeSearchBoxImpl.ShowCategory': z.boolean(), - 'Comfy.NodeSearchBoxImpl.ShowIdName': z.boolean(), - 'Comfy.NodeSearchBoxImpl.ShowNodeFrequency': z.boolean(), - 'Comfy.NodeSuggestions.number': z.number(), - 'Comfy.Node.BypassAllLinksOnDelete': z.boolean(), - 'Comfy.Node.Opacity': z.number(), - 'Comfy.Node.MiddleClickRerouteNode': z.boolean(), - 'Comfy.Node.ShowDeprecated': z.boolean(), - 'Comfy.Node.ShowExperimental': z.boolean(), - 'Comfy.Pointer.ClickBufferTime': z.number(), - 'Comfy.Pointer.ClickDrift': z.number(), - 'Comfy.Pointer.DoubleClickTime': z.number(), - 'Comfy.PreviewFormat': z.string(), - 'Comfy.PromptFilename': z.boolean(), - 'Comfy.Sidebar.Location': z.enum(['left', 'right']), - 'Comfy.Sidebar.Size': z.enum(['small', 'normal']), - 'Comfy.Sidebar.UnifiedWidth': z.boolean(), - 'Comfy.SnapToGrid.GridSize': z.number(), - 'Comfy.TextareaWidget.FontSize': z.number(), - 'Comfy.TextareaWidget.Spellcheck': z.boolean(), - 'Comfy.UseNewMenu': z.enum(['Disabled', 'Top', 'Bottom']), - 'Comfy.TreeExplorer.ItemPadding': z.number(), - 'Comfy.Validation.Workflows': z.boolean(), - 'Comfy.Workflow.SortNodeIdOnSave': z.boolean(), - 'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']), - 'Comfy.Workflow.WorkflowTabsPosition': z.enum([ - 'Sidebar', - 'Topbar', - 'Topbar (2nd-row)' - ]), - 'Comfy.Node.DoubleClickTitleToEdit': z.boolean(), - 'Comfy.WidgetControlMode': z.enum(['before', 'after']), - 'Comfy.Window.UnloadConfirmation': z.boolean(), - 'Comfy.NodeBadge.NodeSourceBadgeMode': zNodeBadgeMode, - 'Comfy.NodeBadge.NodeIdBadgeMode': zNodeBadgeMode, - 'Comfy.NodeBadge.NodeLifeCycleBadgeMode': zNodeBadgeMode, - 'Comfy.NodeBadge.ShowApiPricing': z.boolean(), - 'Comfy.Notification.ShowVersionUpdates': z.boolean(), - 'Comfy.QueueButton.BatchCountLimit': z.number(), - 'Comfy.Queue.MaxHistoryItems': z.number(), - 'Comfy.Keybinding.UnsetBindings': z.array(zKeybinding), - 'Comfy.Keybinding.NewBindings': z.array(zKeybinding), - 'Comfy.Extension.Disabled': z.array(z.string()), - 'Comfy.LinkRenderMode': z.number(), - 'Comfy.Node.AutoSnapLinkToSlot': z.boolean(), - 'Comfy.Node.SnapHighlightsNode': z.boolean(), - 'Comfy.Server.ServerConfigValues': z.record(z.string(), z.any()), - 'Comfy.Server.LaunchArgs': z.record(z.string(), z.string()), - 'LiteGraph.Canvas.MaximumFps': z.number(), - 'Comfy.Workflow.ConfirmDelete': z.boolean(), - 'Comfy.Workflow.AutoSaveDelay': z.number(), - 'Comfy.Workflow.AutoSave': z.enum(['off', 'after delay']), - 'Comfy.RerouteBeta': z.boolean(), - 'LiteGraph.Canvas.MinFontSizeForLOD': z.number(), - 'Comfy.Canvas.SelectionToolbox': z.boolean(), - 'LiteGraph.Node.TooltipDelay': z.number(), - 'LiteGraph.ContextMenu.Scaling': z.boolean(), - 'LiteGraph.Reroute.SplineOffset': z.number(), - 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number(), - 'Comfy.Toast.DisableReconnectingToast': z.boolean(), - 'Comfy.Workflow.Persist': z.boolean(), - 'Comfy.TutorialCompleted': z.boolean(), - 'Comfy.InstalledVersion': z.string().nullable(), - 'Comfy.Node.AllowImageSizeDraw': z.boolean(), - 'Comfy.Minimap.Visible': z.boolean(), - 'Comfy.Minimap.NodeColors': z.boolean(), - 'Comfy.Minimap.ShowLinks': z.boolean(), - 'Comfy.Minimap.ShowGroups': z.boolean(), - 'Comfy.Minimap.RenderBypassState': z.boolean(), - 'Comfy.Minimap.RenderErrorState': z.boolean(), - 'Comfy.Canvas.NavigationMode': z.string(), - 'Comfy.Canvas.LeftMouseClickBehavior': z.string(), - 'Comfy.Canvas.MouseWheelScroll': z.string(), - 'Comfy.VueNodes.Enabled': z.boolean(), - 'Comfy.Assets.UseAssetAPI': z.boolean(), - 'Comfy-Desktop.AutoUpdate': z.boolean(), - 'Comfy-Desktop.SendStatistics': z.boolean(), - 'Comfy-Desktop.WindowStyle': z.string(), - 'Comfy-Desktop.UV.PythonInstallMirror': z.string(), - 'Comfy-Desktop.UV.PypiInstallMirror': z.string(), - 'Comfy-Desktop.UV.TorchInstallMirror': z.string(), - 'Comfy.MaskEditor.UseNewEditor': z.boolean(), - 'Comfy.MaskEditor.BrushAdjustmentSpeed': z.number(), - 'Comfy.MaskEditor.UseDominantAxis': z.boolean(), - 'Comfy.Load3D.ShowGrid': z.boolean(), - 'Comfy.Load3D.ShowPreview': z.boolean(), - 'Comfy.Load3D.BackgroundColor': z.string(), - 'Comfy.Load3D.LightIntensity': z.number(), - 'Comfy.Load3D.LightIntensityMaximum': z.number(), - 'Comfy.Load3D.LightIntensityMinimum': z.number(), - 'Comfy.Load3D.LightAdjustmentIncrement': z.number(), - 'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']), - 'Comfy.Load3D.3DViewerEnable': z.boolean(), - 'Comfy.Memory.AllowManualUnload': z.boolean(), - 'pysssss.SnapToGrid': z.boolean(), - /** VHS setting is used for queue video preview support. */ - 'VHS.AdvancedPreviews': z.string(), - /** Release data settings */ - 'Comfy.Release.Version': z.string(), - 'Comfy.Release.Status': z.enum([ - 'skipped', - 'changelog seen', - "what's new seen" - ]), - 'Comfy.Release.Timestamp': z.number(), - /** Settings used for testing */ - 'test.setting': z.any(), - 'main.sub.setting.name': z.any(), - 'single.setting': z.any(), - 'LiteGraph.Node.DefaultPadding': z.boolean(), - 'LiteGraph.Pointer.TrackpadGestures': z.boolean() -}) - -export type EmbeddingsResponse = z.infer -export type ExtensionsResponse = z.infer -export type PromptResponse = z.infer -export type NodeError = z.infer -export type Settings = z.infer -export type DeviceStats = z.infer -export type SystemStats = z.infer -export type User = z.infer -export type UserData = z.infer -export type UserDataFullInfo = z.infer -export type TerminalSize = z.infer -export type LogEntry = z.infer -export type LogsRawResponse = z.infer +export * from '@comfyorg/schemas/apiSchema' diff --git a/src/schemas/colorPaletteSchema.ts b/src/schemas/colorPaletteSchema.ts index 88b7974866..d54059b9ed 100644 --- a/src/schemas/colorPaletteSchema.ts +++ b/src/schemas/colorPaletteSchema.ts @@ -1,116 +1 @@ -import { z } from 'zod' - -import { LiteGraph } from '@/lib/litegraph/src/litegraph' - -const nodeSlotSchema = z.object({ - CLIP: z.string(), - CLIP_VISION: z.string(), - CLIP_VISION_OUTPUT: z.string(), - CONDITIONING: z.string(), - CONTROL_NET: z.string(), - IMAGE: z.string(), - LATENT: z.string(), - MASK: z.string(), - MODEL: z.string(), - STYLE_MODEL: z.string(), - VAE: z.string(), - NOISE: z.string(), - GUIDER: z.string(), - SAMPLER: z.string(), - SIGMAS: z.string(), - TAESD: z.string() -}) - -const litegraphBaseSchema = z.object({ - BACKGROUND_IMAGE: z.string(), - CLEAR_BACKGROUND_COLOR: z.string(), - NODE_TITLE_COLOR: z.string(), - NODE_SELECTED_TITLE_COLOR: z.string(), - NODE_TEXT_SIZE: z.number(), - NODE_TEXT_COLOR: z.string(), - NODE_TEXT_HIGHLIGHT_COLOR: z.string(), - NODE_SUBTEXT_SIZE: z.number(), - NODE_DEFAULT_COLOR: z.string(), - NODE_DEFAULT_BGCOLOR: z.string(), - NODE_DEFAULT_BOXCOLOR: z.string(), - NODE_DEFAULT_SHAPE: z.union([ - z.literal(LiteGraph.BOX_SHAPE), - z.literal(LiteGraph.ROUND_SHAPE), - z.literal(LiteGraph.CARD_SHAPE), - // Legacy palettes have string field for NODE_DEFAULT_SHAPE. - z.string() - ]), - NODE_BOX_OUTLINE_COLOR: z.string(), - NODE_BYPASS_BGCOLOR: z.string(), - NODE_ERROR_COLOUR: z.string(), - DEFAULT_SHADOW_COLOR: z.string(), - DEFAULT_GROUP_FONT: z.number(), - WIDGET_BGCOLOR: z.string(), - WIDGET_OUTLINE_COLOR: z.string(), - WIDGET_TEXT_COLOR: z.string(), - WIDGET_SECONDARY_TEXT_COLOR: z.string(), - WIDGET_DISABLED_TEXT_COLOR: z.string(), - LINK_COLOR: z.string(), - EVENT_LINK_COLOR: z.string(), - CONNECTING_LINK_COLOR: z.string(), - BADGE_FG_COLOR: z.string(), - BADGE_BG_COLOR: z.string() -}) - -const comfyBaseSchema = z.object({ - ['fg-color']: z.string(), - ['bg-color']: z.string(), - ['bg-img']: z.string().optional(), - ['comfy-menu-bg']: z.string(), - ['comfy-menu-secondary-bg']: z.string(), - ['comfy-input-bg']: z.string(), - ['input-text']: z.string(), - ['descrip-text']: z.string(), - ['drag-text']: z.string(), - ['error-text']: z.string(), - ['border-color']: z.string(), - ['tr-even-bg-color']: z.string(), - ['tr-odd-bg-color']: z.string(), - ['content-bg']: z.string(), - ['content-fg']: z.string(), - ['content-hover-bg']: z.string(), - ['content-hover-fg']: z.string(), - ['bar-shadow']: z.string() -}) - -const colorsSchema = z.object({ - node_slot: nodeSlotSchema, - litegraph_base: litegraphBaseSchema, - comfy_base: comfyBaseSchema -}) - -const partialColorsSchema = z.object({ - node_slot: nodeSlotSchema.partial(), - litegraph_base: litegraphBaseSchema.partial(), - comfy_base: comfyBaseSchema.partial() -}) - -// Palette in the wild can have custom metadata fields such as 'version'. -export const paletteSchema = z - .object({ - id: z.string(), - name: z.string(), - colors: partialColorsSchema, - light_theme: z.boolean().optional() - }) - .passthrough() - -const completedPaletteSchema = z - .object({ - id: z.string(), - name: z.string(), - colors: colorsSchema - }) - .passthrough() - -export const colorPalettesSchema = z.record(paletteSchema) - -export type Colors = z.infer -export type Palette = z.infer -export type CompletedPalette = z.infer -export type ColorPalettes = z.infer +export * from '@comfyorg/schemas/colorPaletteSchema' diff --git a/src/schemas/keyBindingSchema.ts b/src/schemas/keyBindingSchema.ts index 04cbe319a1..8edd26a27c 100644 --- a/src/schemas/keyBindingSchema.ts +++ b/src/schemas/keyBindingSchema.ts @@ -1,25 +1 @@ -import { z } from 'zod' - -// KeyCombo schema -const zKeyCombo = z.object({ - key: z.string(), - ctrl: z.boolean().optional(), - alt: z.boolean().optional(), - shift: z.boolean().optional(), - meta: z.boolean().optional() -}) - -// Keybinding schema -export const zKeybinding = z.object({ - commandId: z.string(), - combo: zKeyCombo, - // Optional target element ID to limit keybinding to. - // Note: Currently only used to distinguish between global keybindings - // and litegraph canvas keybindings. - // Do NOT use this field in extensions as it has no effect. - targetElementId: z.string().optional() -}) - -// Infer types from schemas -export type KeyCombo = z.infer -export type Keybinding = z.infer +export * from '@comfyorg/schemas/keyBindingSchema' diff --git a/src/schemas/nodeDef/migration.ts b/src/schemas/nodeDef/migration.ts index 22d91c16e7..3b99e6d668 100644 --- a/src/schemas/nodeDef/migration.ts +++ b/src/schemas/nodeDef/migration.ts @@ -1,143 +1 @@ -import type { - ComfyNodeDef as ComfyNodeDefV2, - InputSpec as InputSpecV2, - OutputSpec as OutputSpecV2 -} from '@/schemas/nodeDef/nodeDefSchemaV2' -import type { - ComfyNodeDef as ComfyNodeDefV1, - InputSpec as InputSpecV1 -} from '@/schemas/nodeDefSchema' -import { - getComboSpecComboOptions, - isComboInputSpec, - isComboInputSpecV1 -} from '@/schemas/nodeDefSchema' - -/** - * Transforms a V1 node definition to V2 format - * @param nodeDefV1 The V1 node definition to transform - * @returns The transformed V2 node definition - */ -export function transformNodeDefV1ToV2( - nodeDefV1: ComfyNodeDefV1 -): ComfyNodeDefV2 { - // Transform inputs - const inputs: Record = {} - - // Process required inputs - if (nodeDefV1.input?.required) { - Object.entries(nodeDefV1.input.required).forEach(([name, inputSpecV1]) => { - inputs[name] = transformInputSpecV1ToV2(inputSpecV1, { - name, - isOptional: false - }) - }) - } - - // Process optional inputs - if (nodeDefV1.input?.optional) { - Object.entries(nodeDefV1.input.optional).forEach(([name, inputSpecV1]) => { - inputs[name] = transformInputSpecV1ToV2(inputSpecV1, { - name, - isOptional: true - }) - }) - } - - // Transform outputs - const outputs: OutputSpecV2[] = [] - - if (nodeDefV1.output) { - if (Array.isArray(nodeDefV1.output)) { - nodeDefV1.output.forEach((outputType, index) => { - const outputSpec: OutputSpecV2 = { - index, - name: nodeDefV1.output_name?.[index] || `output_${index}`, - type: Array.isArray(outputType) ? 'COMBO' : outputType, - is_list: nodeDefV1.output_is_list?.[index] || false, - tooltip: nodeDefV1.output_tooltips?.[index] - } - - // Add options for combo outputs - if (Array.isArray(outputType)) { - outputSpec.options = outputType - } - - outputs.push(outputSpec) - }) - } else { - console.warn('nodeDefV1.output is not an array:', nodeDefV1.output) - } - } - - // Create the V2 node definition - return { - inputs, - outputs, - hidden: nodeDefV1.input?.hidden, - name: nodeDefV1.name, - display_name: nodeDefV1.display_name, - description: nodeDefV1.description, - category: nodeDefV1.category, - output_node: nodeDefV1.output_node, - python_module: nodeDefV1.python_module, - deprecated: nodeDefV1.deprecated, - experimental: nodeDefV1.experimental - } -} - -/** - * Transforms a V1 input specification to V2 format - * @param inputSpecV1 The V1 input specification to transform - * @param name The name of the input - * @param isOptional Whether the input is optional - * @returns The transformed V2 input specification - */ -export function transformInputSpecV1ToV2( - inputSpecV1: InputSpecV1, - kwargs: { - name: string - isOptional?: boolean - } -): InputSpecV2 { - const { name, isOptional = false } = kwargs - - // Extract options from the input spec - const options = inputSpecV1[1] || {} - - // Base properties for all input types - const baseProps = { - name, - isOptional, - ...options - } - - // Handle different input types - if (isComboInputSpec(inputSpecV1)) { - return { - type: 'COMBO', - ...baseProps, - options: isComboInputSpecV1(inputSpecV1) - ? inputSpecV1[0] - : getComboSpecComboOptions(inputSpecV1) - } - } else if (typeof inputSpecV1[0] === 'string') { - // Handle standard types (INT, FLOAT, BOOLEAN, STRING) and custom types - return { - type: inputSpecV1[0], - ...baseProps - } - } - - // Fallback for any unhandled cases - return { - type: 'UNKNOWN', - ...baseProps - } -} - -export function transformInputSpecV2ToV1( - inputSpecV2: InputSpecV2 -): InputSpecV1 { - return [inputSpecV2.type, inputSpecV2] -} +export * from '@comfyorg/schemas/nodeDef/migration' diff --git a/src/schemas/nodeDef/nodeDefSchemaV2.ts b/src/schemas/nodeDef/nodeDefSchemaV2.ts index 09983d115e..f808b05dae 100644 --- a/src/schemas/nodeDef/nodeDefSchemaV2.ts +++ b/src/schemas/nodeDef/nodeDefSchemaV2.ts @@ -1,264 +1 @@ -import { z } from 'zod' - -import { - zBaseInputOptions, - zBooleanInputOptions, - zComboInputOptions, - zFloatInputOptions, - zIntInputOptions, - zStringInputOptions -} from '@/schemas/nodeDefSchema' - -const zIntInputSpec = zIntInputOptions.extend({ - type: z.literal('INT'), - name: z.string(), - isOptional: z.boolean().optional() -}) - -const zFloatInputSpec = zFloatInputOptions.extend({ - type: z.literal('FLOAT'), - name: z.string(), - isOptional: z.boolean().optional() -}) - -const zBooleanInputSpec = zBooleanInputOptions.extend({ - type: z.literal('BOOLEAN'), - name: z.string(), - isOptional: z.boolean().optional() -}) - -const zStringInputSpec = zStringInputOptions.extend({ - type: z.literal('STRING'), - name: z.string(), - isOptional: z.boolean().optional() -}) - -const zComboInputSpec = zComboInputOptions.extend({ - type: z.literal('COMBO'), - name: z.string(), - isOptional: z.boolean().optional() -}) - -const zColorInputSpec = zBaseInputOptions.extend({ - type: z.literal('COLOR'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - default: z.string().optional() - }) - .optional() -}) - -const zFileUploadInputSpec = zBaseInputOptions.extend({ - type: z.literal('FILEUPLOAD'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z.record(z.unknown()).optional() -}) - -const zImageInputSpec = zBaseInputOptions.extend({ - type: z.literal('IMAGE'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z.record(z.unknown()).optional() -}) - -const zImageCompareInputSpec = zBaseInputOptions.extend({ - type: z.literal('IMAGECOMPARE'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z.record(z.unknown()).optional() -}) - -const zMarkdownInputSpec = zBaseInputOptions.extend({ - type: z.literal('MARKDOWN'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - content: z.string().optional() - }) - .optional() -}) - -const zTreeSelectInputSpec = zBaseInputOptions.extend({ - type: z.literal('TREESELECT'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - multiple: z.boolean().optional(), - values: z.array(z.unknown()).optional() - }) - .optional() -}) - -const zMultiSelectInputSpec = zBaseInputOptions.extend({ - type: z.literal('MULTISELECT'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - values: z.array(z.string()).optional() - }) - .optional() -}) - -const zChartInputSpec = zBaseInputOptions.extend({ - type: z.literal('CHART'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - type: z.enum(['bar', 'line']).optional(), - data: z.object({}).optional() - }) - .optional() -}) - -const zGalleriaInputSpec = zBaseInputOptions.extend({ - type: z.literal('GALLERIA'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - images: z.array(z.string()).optional() - }) - .optional() -}) - -const zSelectButtonInputSpec = zBaseInputOptions.extend({ - type: z.literal('SELECTBUTTON'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - values: z.array(z.string()).optional() - }) - .optional() -}) - -const zTextareaInputSpec = zBaseInputOptions.extend({ - type: z.literal('TEXTAREA'), - name: z.string(), - isOptional: z.boolean().optional(), - options: z - .object({ - rows: z.number().optional(), - cols: z.number().optional(), - default: z.string().optional() - }) - .optional() -}) - -const zCustomInputSpec = zBaseInputOptions.extend({ - type: z.string(), - name: z.string(), - isOptional: z.boolean().optional() -}) - -const zInputSpec = z.union([ - zIntInputSpec, - zFloatInputSpec, - zBooleanInputSpec, - zStringInputSpec, - zComboInputSpec, - zColorInputSpec, - zFileUploadInputSpec, - zImageInputSpec, - zImageCompareInputSpec, - zMarkdownInputSpec, - zTreeSelectInputSpec, - zMultiSelectInputSpec, - zChartInputSpec, - zGalleriaInputSpec, - zSelectButtonInputSpec, - zTextareaInputSpec, - zCustomInputSpec -]) - -// Output specs -const zOutputSpec = z.object({ - index: z.number(), - name: z.string(), - type: z.string(), - is_list: z.boolean(), - options: z.array(z.any()).optional(), - tooltip: z.string().optional() -}) - -// Main node definition schema -export const zComfyNodeDef = z.object({ - inputs: z.record(zInputSpec), - outputs: z.array(zOutputSpec), - hidden: z.record(z.any()).optional(), - - name: z.string(), - display_name: z.string(), - description: z.string(), - help: z.string().optional(), - category: z.string(), - output_node: z.boolean(), - python_module: z.string(), - deprecated: z.boolean().optional(), - experimental: z.boolean().optional(), - api_node: z.boolean().optional() -}) - -// Export types -type IntInputSpec = z.infer -type FloatInputSpec = z.infer -type BooleanInputSpec = z.infer -type StringInputSpec = z.infer -export type ComboInputSpec = z.infer -export type ColorInputSpec = z.infer -export type FileUploadInputSpec = z.infer -export type ImageCompareInputSpec = z.infer -export type TreeSelectInputSpec = z.infer -export type MultiSelectInputSpec = z.infer -export type ChartInputSpec = z.infer -export type GalleriaInputSpec = z.infer -export type SelectButtonInputSpec = z.infer -export type TextareaInputSpec = z.infer -export type CustomInputSpec = z.infer - -export type InputSpec = z.infer -export type OutputSpec = z.infer -export type ComfyNodeDef = z.infer - -export const isIntInputSpec = ( - inputSpec: InputSpec -): inputSpec is IntInputSpec => { - return inputSpec.type === 'INT' -} - -export const isFloatInputSpec = ( - inputSpec: InputSpec -): inputSpec is FloatInputSpec => { - return inputSpec.type === 'FLOAT' -} - -export const isBooleanInputSpec = ( - inputSpec: InputSpec -): inputSpec is BooleanInputSpec => { - return inputSpec.type === 'BOOLEAN' -} - -export const isStringInputSpec = ( - inputSpec: InputSpec -): inputSpec is StringInputSpec => { - return inputSpec.type === 'STRING' -} - -export const isComboInputSpec = ( - inputSpec: InputSpec -): inputSpec is ComboInputSpec => { - return inputSpec.type === 'COMBO' -} - -export const isChartInputSpec = ( - inputSpec: InputSpec -): inputSpec is ChartInputSpec => { - return inputSpec.type === 'CHART' -} +export * from '@comfyorg/schemas/nodeDef/nodeDefSchemaV2' diff --git a/src/schemas/nodeDefSchema.ts b/src/schemas/nodeDefSchema.ts index 75890d6096..b9a458fcfb 100644 --- a/src/schemas/nodeDefSchema.ts +++ b/src/schemas/nodeDefSchema.ts @@ -1,255 +1 @@ -import { z } from 'zod' -import { fromZodError } from 'zod-validation-error' - -import { resultItemType } from '@/schemas/apiSchema' - -const zComboOption = z.union([z.string(), z.number()]) -const zRemoteWidgetConfig = z.object({ - route: z.string().url().or(z.string().startsWith('/')), - refresh: z.number().gte(128).safe().or(z.number().lte(0).safe()).optional(), - response_key: z.string().optional(), - query_params: z.record(z.string(), z.string()).optional(), - refresh_button: z.boolean().optional(), - control_after_refresh: z.enum(['first', 'last']).optional(), - timeout: z.number().gte(0).optional(), - max_retries: z.number().gte(0).optional() -}) -const zMultiSelectOption = z.object({ - placeholder: z.string().optional(), - chip: z.boolean().optional() -}) - -export const zBaseInputOptions = z - .object({ - default: z.any().optional(), - defaultInput: z.boolean().optional(), - forceInput: z.boolean().optional(), - tooltip: z.string().optional(), - hidden: z.boolean().optional(), - advanced: z.boolean().optional(), - widgetType: z.string().optional(), - /** Backend-only properties. */ - rawLink: z.boolean().optional(), - lazy: z.boolean().optional() - }) - .passthrough() - -const zNumericInputOptions = zBaseInputOptions.extend({ - min: z.number().optional(), - max: z.number().optional(), - step: z.number().optional(), - /** Note: Many node authors are using INT/FLOAT to pass list of INT/FLOAT. */ - default: z.union([z.number(), z.array(z.number())]).optional(), - display: z.enum(['slider', 'number', 'knob']).optional() -}) - -export const zIntInputOptions = zNumericInputOptions.extend({ - /** - * If true, a linked widget will be added to the node to select the mode - * of `control_after_generate`. - */ - control_after_generate: z.boolean().optional() -}) - -export const zFloatInputOptions = zNumericInputOptions.extend({ - round: z.union([z.number(), z.literal(false)]).optional() -}) - -export const zBooleanInputOptions = zBaseInputOptions.extend({ - label_on: z.string().optional(), - label_off: z.string().optional(), - default: z.boolean().optional() -}) - -export const zStringInputOptions = zBaseInputOptions.extend({ - default: z.string().optional(), - multiline: z.boolean().optional(), - dynamicPrompts: z.boolean().optional(), - - // Multiline-only fields - defaultVal: z.string().optional(), - placeholder: z.string().optional() -}) - -export const zComboInputOptions = zBaseInputOptions.extend({ - control_after_generate: z.boolean().optional(), - image_upload: z.boolean().optional(), - image_folder: resultItemType.optional(), - allow_batch: z.boolean().optional(), - video_upload: z.boolean().optional(), - audio_upload: z.boolean().optional(), - animated_image_upload: z.boolean().optional(), - options: z.array(zComboOption).optional(), - remote: zRemoteWidgetConfig.optional(), - /** Whether the widget is a multi-select widget. */ - multi_select: zMultiSelectOption.optional() -}) - -const zIntInputSpec = z.tuple([z.literal('INT'), zIntInputOptions.optional()]) -const zFloatInputSpec = z.tuple([ - z.literal('FLOAT'), - zFloatInputOptions.optional() -]) -const zBooleanInputSpec = z.tuple([ - z.literal('BOOLEAN'), - zBooleanInputOptions.optional() -]) -const zStringInputSpec = z.tuple([ - z.literal('STRING'), - zStringInputOptions.optional() -]) -/** - * Legacy combo syntax. - * @deprecated Use `zComboInputSpecV2` instead. - */ -const zComboInputSpec = z.tuple([ - z.array(zComboOption), - zComboInputOptions.optional() -]) -const zComboInputSpecV2 = z.tuple([ - z.literal('COMBO'), - zComboInputOptions.optional() -]) - -export function isComboInputSpecV1( - inputSpec: InputSpec -): inputSpec is ComboInputSpec { - return Array.isArray(inputSpec[0]) -} - -export function isIntInputSpec( - inputSpec: InputSpec -): inputSpec is IntInputSpec { - return inputSpec[0] === 'INT' -} - -export function isFloatInputSpec( - inputSpec: InputSpec -): inputSpec is FloatInputSpec { - return inputSpec[0] === 'FLOAT' -} - -export function isComboInputSpecV2( - inputSpec: InputSpec -): inputSpec is ComboInputSpecV2 { - return inputSpec[0] === 'COMBO' -} - -export function isComboInputSpec( - inputSpec: InputSpec -): inputSpec is ComboInputSpec | ComboInputSpecV2 { - return isComboInputSpecV1(inputSpec) || isComboInputSpecV2(inputSpec) -} - -/** - * Get the type of an input spec. - * - * @param inputSpec - The input spec to get the type of. - * @returns The type of the input spec. - */ -export function getInputSpecType(inputSpec: InputSpec): string { - return isComboInputSpec(inputSpec) ? 'COMBO' : inputSpec[0] -} - -/** - * Get the combo options from a combo input spec. - * - * @param inputSpec - The input spec to get the combo options from. - * @returns The combo options. - */ -export function getComboSpecComboOptions( - inputSpec: ComboInputSpec | ComboInputSpecV2 -): (number | string)[] { - return ( - (isComboInputSpecV2(inputSpec) ? inputSpec[1]?.options : inputSpec[0]) ?? [] - ) -} - -const excludedLiterals = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO']) -const zCustomInputSpec = z.tuple([ - z.string().refine((value) => !excludedLiterals.has(value)), - zBaseInputOptions.optional() -]) - -const zInputSpec = z.union([ - zIntInputSpec, - zFloatInputSpec, - zBooleanInputSpec, - zStringInputSpec, - zComboInputSpec, - zComboInputSpecV2, - zCustomInputSpec -]) - -const zComfyInputsSpec = z.object({ - required: z.record(zInputSpec).optional(), - optional: z.record(zInputSpec).optional(), - // Frontend repo is not using it, but some custom nodes are using the - // hidden field to pass various values. - hidden: z.record(z.any()).optional() -}) - -const zComfyNodeDataType = z.string() -const zComfyComboOutput = z.array(zComboOption) -const zComfyOutputTypesSpec = z.array( - z.union([zComfyNodeDataType, zComfyComboOutput]) -) - -export const zComfyNodeDef = z.object({ - input: zComfyInputsSpec.optional(), - output: zComfyOutputTypesSpec.optional(), - output_is_list: z.array(z.boolean()).optional(), - output_name: z.array(z.string()).optional(), - output_tooltips: z.array(z.string()).optional(), - name: z.string(), - display_name: z.string(), - description: z.string(), - help: z.string().optional(), - category: z.string(), - output_node: z.boolean(), - python_module: z.string(), - deprecated: z.boolean().optional(), - experimental: z.boolean().optional(), - /** - * Whether the node is an API node. Running API nodes requires login to - * Comfy Org account. - * https://docs.comfy.org/tutorials/api-nodes/overview - */ - api_node: z.boolean().optional(), - /** - * Specifies the order of inputs for each input category. - * Used to ensure consistent widget ordering regardless of JSON serialization. - * Keys are 'required', 'optional', etc., values are arrays of input names. - */ - input_order: z.record(z.array(z.string())).optional() -}) - -// `/object_info` -export type ComfyInputsSpec = z.infer -export type ComfyOutputTypesSpec = z.infer -export type ComfyNodeDef = z.infer -export type RemoteWidgetConfig = z.infer - -export type ComboInputOptions = z.infer -export type NumericInputOptions = z.infer - -export type IntInputSpec = z.infer -export type FloatInputSpec = z.infer -export type ComboInputSpec = z.infer -export type ComboInputSpecV2 = z.infer -export type InputSpec = z.infer - -export function validateComfyNodeDef( - data: any, - onError: (error: string) => void = console.warn -): ComfyNodeDef | null { - const result = zComfyNodeDef.safeParse(data) - if (!result.success) { - const zodError = fromZodError(result.error) - onError( - `Invalid ComfyNodeDef: ${JSON.stringify(data)}\n${zodError.message}` - ) - return null - } - return result.data -} +export * from '@comfyorg/schemas/nodeDefSchema' diff --git a/src/schemas/signInSchema.ts b/src/schemas/signInSchema.ts index 1e5a44198a..2d821e9cc2 100644 --- a/src/schemas/signInSchema.ts +++ b/src/schemas/signInSchema.ts @@ -1,60 +1 @@ -import { z } from 'zod' - -import { t } from '@/i18n' - -export const apiKeySchema = z.object({ - apiKey: z - .string() - .trim() - .startsWith('comfyui-', t('validation.prefix', { prefix: 'comfyui-' })) - .length(72, t('validation.length', { length: 72 })) -}) - -export const signInSchema = z.object({ - email: z - .string() - .email(t('validation.invalidEmail')) - .min(1, t('validation.required')), - password: z.string().min(1, t('validation.required')) -}) - -export type SignInData = z.infer - -const passwordSchema = z.object({ - password: z - .string() - .min(8, t('validation.minLength', { length: 8 })) - .max(32, t('validation.maxLength', { length: 32 })) - .regex(/[A-Z]/, t('validation.password.uppercase')) - .regex(/[a-z]/, t('validation.password.lowercase')) - .regex(/\d/, t('validation.password.number')) - .regex(/[^A-Za-z0-9]/, t('validation.password.special')), - confirmPassword: z.string().min(1, t('validation.required')) -}) - -export const updatePasswordSchema = passwordSchema.refine( - (data) => data.password === data.confirmPassword, - { - message: t('validation.password.match'), - path: ['confirmPassword'] - } -) - -export const signUpSchema = passwordSchema - .extend({ - email: z - .string() - .email(t('validation.invalidEmail')) - .min(1, t('validation.required')), - personalDataConsent: z.boolean() - }) - .refine((data) => data.password === data.confirmPassword, { - message: t('validation.password.match'), - path: ['confirmPassword'] - }) - .refine((data) => data.personalDataConsent === true, { - message: t('validation.personalDataConsentRequired'), - path: ['personalDataConsent'] - }) - -export type SignUpData = z.infer +export * from '@comfyorg/schemas/signInSchema'