[Type] Disallow type upcasting for node input spec (#2790)

This commit is contained in:
Chenlei Hu
2025-03-01 16:58:45 -05:00
committed by GitHub
parent bca0af82a3
commit 09ab14ac81
20 changed files with 136 additions and 109 deletions

View File

@@ -17,6 +17,7 @@ export const useBooleanWidget = () => {
}
return {
// @ts-expect-error InputSpec is not typed correctly
widget: node.addWidget('toggle', inputName, defaultVal, () => {}, options)
}
}

View File

@@ -20,6 +20,7 @@ export const useComboWidget = () => {
const res = {
widget: node.addWidget('combo', inputName, defaultValue, () => {}, {
// @ts-expect-error InputSpec is not typed correctly
values: options ?? inputData[0]
}) as IComboWidget
}
@@ -31,6 +32,7 @@ export const useComboWidget = () => {
node,
widget: res.widget
})
// @ts-expect-error InputSpec is not typed correctly
if (remote.refresh_button) remoteWidget.addRefreshButton()
const origOptions = res.widget.options

View File

@@ -2,7 +2,7 @@ import type { LGraphNode } from '@comfyorg/litegraph'
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
import _ from 'lodash'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import type { FloatInputOptions, InputSpec } from '@/schemas/nodeDefSchema'
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
import { useSettingStore } from '@/stores/settingStore'
import { getNumberDefaults } from '@/utils/mathUtil'
@@ -45,11 +45,14 @@ export const useFloatWidget = () => {
settingStore.get('Comfy.FloatRoundingPrecision') || undefined
const enableRounding = !settingStore.get('Comfy.DisableFloatRounding')
const { val, config } = getNumberDefaults(inputOptions, {
defaultStep: 0.5,
precision,
enableRounding
})
const { val, config } = getNumberDefaults(
inputOptions as FloatInputOptions,
{
defaultStep: 0.5,
precision,
enableRounding
}
)
return {
widget: node.addWidget(
@@ -57,6 +60,7 @@ export const useFloatWidget = () => {
inputName,
val,
onFloatValueChange,
// @ts-expect-error InputSpec is not typed correctly
config
)
}

View File

@@ -44,9 +44,11 @@ export const useImageUploadWidget = () => {
const { showPreview } = isVideo ? useNodeVideo(node) : useNodeImage(node)
const fileFilter = isVideo ? isVideoFile : isImageFile
// @ts-expect-error InputSpec is not typed correctly
const fileComboWidget = findFileComboWidget(node, imageInputName)
const initialFile = `${fileComboWidget.value}`
const formatPath = (value: InternalFile) =>
// @ts-expect-error InputSpec is not typed correctly
createAnnotatedPath(value, { rootFolder: image_folder })
const transform = (internalValue: InternalValue): ExposedValue => {
@@ -66,6 +68,7 @@ export const useImageUploadWidget = () => {
// Setup file upload handling
const { openFileSelection } = useNodeImageUpload(node, {
// @ts-expect-error InputSpec is not typed correctly
allow_batch,
fileFilter,
accept,

View File

@@ -1,7 +1,7 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { INumericWidget } from '@comfyorg/litegraph/dist/types/widgets'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec, IntInputOptions } from '@/schemas/nodeDefSchema'
import type { ComfyApp } from '@/scripts/app'
import {
type ComfyWidgetConstructor,
@@ -50,7 +50,7 @@ export const useIntWidget = () => {
: 'number'
: 'number'
const { val, config } = getNumberDefaults(inputOptions, {
const { val, config } = getNumberDefaults(inputOptions as IntInputOptions, {
defaultStep: 1,
precision: 0,
enableRounding: true
@@ -58,6 +58,7 @@ export const useIntWidget = () => {
config.precision = 0
const result = {
// @ts-expect-error InputSpec is not typed correctly
widget: node.addWidget(widgetType, inputName, val, onValueChange, config)
}

View File

@@ -73,7 +73,7 @@ export function useRemoteWidget<
widget: IWidget
}) {
const { inputData, defaultValue, node, widget } = options
const config: RemoteWidgetConfig = inputData[1].remote
const config = (inputData[1]?.remote ?? {}) as RemoteWidgetConfig
const { refresh = 0, max_retries = MAX_RETRIES } = config
const isPermanent = refresh <= 0
const cacheKey = createCacheKey(config)

View File

@@ -82,6 +82,7 @@ export const useStringWidget = () => {
}
if (inputData[1]?.dynamicPrompts != undefined)
// @ts-expect-error InputSpec is not typed correctly
res.widget.dynamicPrompts = inputData[1].dynamicPrompts
return res

View File

@@ -1162,6 +1162,7 @@ export class GroupNodeHandler {
def?.input?.optional?.[old.inputName]
if (!input) continue
// @ts-expect-error InputSpec is not typed correctly
widget.options.values = input[0]
if (

View File

@@ -337,6 +337,7 @@ app.registerExtension({
// @ts-expect-error ComfyNode
['Preview3D'].includes(nodeType.comfyClass)
) {
// @ts-expect-error InputSpec is not typed correctly
nodeData.input.required.image = ['PREVIEW_3D']
}
},
@@ -439,6 +440,7 @@ app.registerExtension({
// @ts-expect-error ComfyNode
['Preview3DAnimation'].includes(nodeType.comfyClass)
) {
// @ts-expect-error InputSpec is not typed correctly
nodeData.input.required.image = ['PREVIEW_3D_ANIMATION']
}
},

View File

@@ -91,6 +91,7 @@ app.registerExtension({
// @ts-expect-error ComfyNode
['LoadAudio', 'SaveAudio', 'PreviewAudio'].includes(nodeType.comfyClass)
) {
// @ts-expect-error InputSpec is not typed correctly
nodeData.input.required.audioUI = ['AUDIO_UI']
}
},
@@ -149,6 +150,7 @@ app.registerExtension({
name: 'Comfy.UploadAudio',
async beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDef) {
if (nodeData?.input?.required?.audio?.[1]?.audio_upload === true) {
// @ts-expect-error InputSpec is not typed correctly
nodeData.input.required.upload = ['AUDIOUPLOAD']
}
},

View File

@@ -245,6 +245,7 @@ export class PrimitiveNode extends LGraphNode {
if (type in ComfyWidgets) {
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
} else {
// @ts-expect-error InputSpec is not typed correctly
widget = this.addWidget(type, 'value', null, () => {}, {})
}
@@ -452,6 +453,7 @@ function getConfig(widgetName: string) {
function isConvertibleWidget(widget: IWidget, config: InputSpec): boolean {
return (
// @ts-expect-error InputSpec is not typed correctly
(VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) &&
!widget.options?.forceInput
)
@@ -677,6 +679,7 @@ export function mergeIfValid(
return
}
getCustomConfig()[k] =
// @ts-expect-error InputSpec is not typed correctly
v1 == null ? v2 : v2 == null ? v1 : Math.max(v1, v2)
continue
} else if (k === 'max') {
@@ -686,6 +689,7 @@ export function mergeIfValid(
return
}
getCustomConfig()[k] =
// @ts-expect-error InputSpec is not typed correctly
v1 == null ? v2 : v2 == null ? v1 : Math.min(v1, v2)
continue
} else if (k === 'step') {
@@ -703,6 +707,7 @@ export function mergeIfValid(
v2 = v1
v1 = a
}
// @ts-expect-error InputSpec is not typed correctly
if (v1 % v2) {
console.log(
'connection rejected: steps not divisible',

View File

@@ -1,24 +1,6 @@
import type { ZodType } from 'zod'
import { z } from 'zod'
import { fromZodError } from 'zod-validation-error'
function inputSpec<TType extends ZodType, TSpec extends ZodType>(
spec: [TType, TSpec],
allowUpcast: boolean = true
) {
const [inputType, inputSpec] = spec
// e.g. "INT" => ["INT", {}]
const upcastTypes = allowUpcast
? [inputType.transform((type) => [type, {}])]
: []
return z.union([
z.tuple([inputType, inputSpec]),
z.tuple([inputType]).transform(([type]) => [type, {}]),
...upcastTypes
])
}
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(),
@@ -89,21 +71,27 @@ const zComboInputOptions = zBaseInputOptions.extend({
remote: zRemoteWidgetConfig.optional()
})
const zIntInputSpec = inputSpec([z.literal('INT'), zIntInputOptions])
const zFloatInputSpec = inputSpec([z.literal('FLOAT'), zFloatInputOptions])
const zBooleanInputSpec = inputSpec([
z.literal('BOOLEAN'),
zBooleanInputOptions
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()
])
const zComboInputSpec = z.tuple([
z.array(z.union([z.string(), z.number()])),
zComboInputOptions.optional()
])
const zComboInputSpecV2 = z.tuple([
z.literal('COMBO'),
zComboInputOptions.optional()
])
const zStringInputSpec = inputSpec([z.literal('STRING'), zStringInputOptions])
const zComboInputSpec = inputSpec(
[z.array(z.any()), zComboInputOptions],
/* allowUpcast=*/ false
)
const zComboInputSpecV2 = inputSpec(
[z.literal('COMBO'), zComboInputOptions],
/* allowUpcast=*/ false
)
export function isComboInputSpecV1(
inputSpec: InputSpec
@@ -154,9 +142,9 @@ export function isComboInputSpec(
}
const excludedLiterals = new Set(['INT', 'FLOAT', 'BOOLEAN', 'STRING', 'COMBO'])
const zCustomInputSpec = inputSpec([
const zCustomInputSpec = z.tuple([
z.string().refine((value) => !excludedLiterals.has(value)),
zBaseInputOptions
zBaseInputOptions.optional()
])
const zInputSpec = z.union([

View File

@@ -1606,8 +1606,10 @@ export class ComfyApp {
const widget = node.widgets[widgetNum]
if (widget.type === 'combo') {
if (def['input'].required?.[widget.name] !== undefined) {
// @ts-expect-error InputSpec is not typed correctly
widget.options.values = def['input'].required[widget.name][0]
} else if (def['input'].optional?.[widget.name] !== undefined) {
// @ts-expect-error InputSpec is not typed correctly
widget.options.values = def['input'].optional[widget.name][0]
}
}

View File

@@ -74,11 +74,13 @@ export const useLitegraphService = () => {
if (widgetType === 'COMBO') {
Object.assign(
config,
// @ts-expect-error InputSpec is not typed correctly
app.widgets.COMBO(this, inputName, inputData, app) || {}
)
} else {
Object.assign(
config,
// @ts-expect-error InputSpec is not typed correctly
app.widgets[widgetType](this, inputName, inputData, app) || {}
)
}
@@ -95,6 +97,7 @@ export const useLitegraphService = () => {
...shapeOptions,
localized_name: st(nameKey, inputName)
}
// @ts-expect-error InputSpec is not typed correctly
this.addInput(inputName, type, inputOptions)
widgetCreated = false
}
@@ -104,15 +107,19 @@ export const useLitegraphService = () => {
if (!inputIsRequired) {
config.widget.options.inputIsOptional = true
}
// @ts-expect-error InputSpec is not typed correctly
if (inputData[1]?.forceInput) {
config.widget.options.forceInput = true
}
// @ts-expect-error InputSpec is not typed correctly
if (inputData[1]?.defaultInput) {
config.widget.options.defaultInput = true
}
// @ts-expect-error InputSpec is not typed correctly
if (inputData[1]?.advanced) {
config.widget.advanced = true
}
// @ts-expect-error InputSpec is not typed correctly
if (inputData[1]?.hidden) {
config.widget.hidden = true
}

View File

@@ -270,7 +270,7 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDef> = {
name: 'Reroute',
display_name: 'Reroute',
category: 'utils',
input: { required: { '': ['*'] }, optional: {} },
input: { required: { '': ['*', {}] }, optional: {} },
output: ['*'],
output_name: [''],
output_is_list: [false],

View File

@@ -47,6 +47,7 @@ export const useWidgetStore = defineStore('widget', () => {
if (Array.isArray(inputData[0]))
return getDefaultValue(transformComboInput(inputData))
// @ts-expect-error InputSpec is not typed correctly
const widgetType = getWidgetType(inputData[0], inputData[1]?.name)
const [_, props] = inputData
@@ -54,12 +55,14 @@ export const useWidgetStore = defineStore('widget', () => {
if (!props) return undefined
if (props.default) return props.default
// @ts-expect-error InputSpec is not typed correctly
if (widgetType === 'COMBO' && props.options?.length) return props.options[0]
if (props.remote) return 'Loading...'
return undefined
}
const transformComboInput = (inputData: InputSpec): ComboInputSpecV2 => {
// @ts-expect-error InputSpec is not typed correctly
return isComboInputSpecV1(inputData)
? [
'COMBO',

View File

@@ -1,7 +1,10 @@
import type { InputSpec } from '@/schemas/nodeDefSchema'
import type {
FloatInputOptions,
IntInputOptions
} from '@/schemas/nodeDefSchema'
export function getNumberDefaults(
inputOptions: InputSpec[1],
inputOptions: IntInputOptions | FloatInputOptions,
options: {
defaultStep: number
precision?: number

View File

@@ -9,7 +9,7 @@ import {
const EXAMPLE_NODE_DEF: ComfyNodeDef = {
input: {
required: {
ckpt_name: [['model1.safetensors', 'model2.ckpt']]
ckpt_name: [['model1.safetensors', 'model2.ckpt'], {}]
}
},
output: ['MODEL', 'CLIP', 'VAE'],
@@ -31,8 +31,6 @@ describe('validateNodeDef', () => {
})
describe.each([
[{ ckpt_name: 'foo' }, ['foo', {}]],
[{ ckpt_name: ['foo'] }, ['foo', {}]],
[{ ckpt_name: ['foo', { default: 1 }] }, ['foo', { default: 1 }]],
// Extra input spec should be preserved
[{ ckpt_name: ['foo', { bar: 1 }] }, ['foo', { bar: 1 }]],

View File

@@ -1,6 +1,7 @@
// @ts-strict-ignore
import { describe, expect, it } from 'vitest'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import {
BooleanInputSpec,
ComfyInputsSpec,
@@ -27,7 +28,7 @@ describe('ComfyInputsSpec', () => {
hidden: {
someHiddenValue: 42
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
@@ -43,7 +44,7 @@ describe('ComfyInputsSpec', () => {
intInput: ['INT', { min: 0, max: 100, default: 50 }],
stringInput: ['STRING', { default: 'Hello', multiline: true }]
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
@@ -68,7 +69,7 @@ describe('ComfyInputsSpec', () => {
],
floatInput: ['FLOAT', { min: 0, max: 1, step: 0.1 }]
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
@@ -88,7 +89,7 @@ describe('ComfyInputsSpec', () => {
optional: {
comboInput: [[1, 2, 3], { default: 2 }]
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
expect(result.optional.comboInput.type).toBe('COMBO')
@@ -100,7 +101,7 @@ describe('ComfyInputsSpec', () => {
optional: {
comboInput: [[1, 2, 3], {}]
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
expect(result.optional.comboInput.type).toBe('COMBO')
@@ -113,7 +114,7 @@ describe('ComfyInputsSpec', () => {
optional: {
customInput: ['CUSTOM_TYPE', { default: 'custom value' }]
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
expect(result.optional.customInput.type).toBe('CUSTOM_TYPE')
@@ -126,7 +127,7 @@ describe('ComfyInputsSpec', () => {
someHiddenValue: 42,
anotherHiddenValue: { nested: 'object' }
}
}
} as ComfyNodeDef['input']
const result = new ComfyInputsSpec(plainObject)
@@ -163,7 +164,7 @@ describe('ComfyNodeDefImpl', () => {
output: ['INT'],
output_is_list: [false],
output_name: ['intOutput']
}
} as ComfyNodeDef
const result = new ComfyNodeDefImpl(plainObject)
@@ -201,7 +202,7 @@ describe('ComfyNodeDefImpl', () => {
output_is_list: [false],
output_name: ['intOutput'],
deprecated: true
}
} as ComfyNodeDef
const result = new ComfyNodeDefImpl(plainObject)
expect(result.deprecated).toBe(true)
@@ -224,7 +225,7 @@ describe('ComfyNodeDefImpl', () => {
output: ['INT'],
output_is_list: [false],
output_name: ['intOutput']
}
} as ComfyNodeDef
const result = new ComfyNodeDefImpl(plainObject)
expect(result.deprecated).toBe(true)
@@ -393,7 +394,7 @@ describe('ComfyNodeDefImpl', () => {
output: ['INT'],
output_is_list: [false],
output_name: ['result']
}
} as ComfyNodeDef
const result = new ComfyNodeDefImpl(plainObject)

View File

@@ -1,59 +1,62 @@
// @ts-strict-ignore
import { describe, expect, it } from 'vitest'
import { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { NodeSearchService } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
const EXAMPLE_NODE_DEFS: ComfyNodeDefImpl[] = [
{
input: {
required: {
ckpt_name: [['model1.safetensors', 'model2.ckpt'], {}]
}
const EXAMPLE_NODE_DEFS: ComfyNodeDefImpl[] = (
[
{
input: {
required: {
ckpt_name: [['model1.safetensors', 'model2.ckpt'], {}]
}
},
output: ['MODEL', 'CLIP', 'VAE'],
output_is_list: [false, false, false],
output_name: ['MODEL', 'CLIP', 'VAE'],
name: 'CheckpointLoaderSimple',
display_name: 'Load Checkpoint',
description: '',
python_module: 'nodes',
category: 'loaders',
output_node: false
},
output: ['MODEL', 'CLIP', 'VAE'],
output_is_list: [false, false, false],
output_name: ['MODEL', 'CLIP', 'VAE'],
name: 'CheckpointLoaderSimple',
display_name: 'Load Checkpoint',
description: '',
python_module: 'nodes',
category: 'loaders',
output_node: false
},
{
input: {
required: {
samples: ['LATENT'],
batch_index: [
'INT',
{
default: 0,
min: 0,
max: 63
}
],
length: [
'INT',
{
default: 1,
min: 1,
max: 64
}
]
}
},
output: ['LATENT'],
output_is_list: [false],
output_name: ['LATENT'],
name: 'LatentFromBatch',
display_name: 'Latent From Batch',
description: '',
python_module: 'nodes',
category: 'latent/batch',
output_node: false
}
].map((nodeDef) => {
{
input: {
required: {
samples: ['LATENT'],
batch_index: [
'INT',
{
default: 0,
min: 0,
max: 63
}
],
length: [
'INT',
{
default: 1,
min: 1,
max: 64
}
]
}
},
output: ['LATENT'],
output_is_list: [false],
output_name: ['LATENT'],
name: 'LatentFromBatch',
display_name: 'Latent From Batch',
description: '',
python_module: 'nodes',
category: 'latent/batch',
output_node: false
}
] as ComfyNodeDef[]
).map((nodeDef: ComfyNodeDef) => {
const def = new ComfyNodeDefImpl(nodeDef)
def['postProcessSearchScores'] = (s) => s
return def