diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue index f6b55999d..a4529ac6b 100644 --- a/src/components/dialog/content/SettingDialogContent.vue +++ b/src/components/dialog/content/SettingDialogContent.vue @@ -71,6 +71,14 @@ + + + + + Loading server config panel... + + + @@ -93,6 +101,7 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import { flattenTree } from '@/utils/treeUtil' import AboutPanel from './setting/AboutPanel.vue' import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue' +import { isElectron } from '@/utils/envUtil' const KeybindingPanel = defineAsyncComponent( () => import('./setting/KeybindingPanel.vue') @@ -100,6 +109,9 @@ const KeybindingPanel = defineAsyncComponent( const ExtensionPanel = defineAsyncComponent( () => import('./setting/ExtensionPanel.vue') ) +const ServerConfigPanel = defineAsyncComponent( + () => import('./setting/ServerConfigPanel.vue') +) interface ISettingGroup { label: string @@ -124,18 +136,33 @@ const extensionPanelNode: SettingTreeNode = { children: [] } +const serverConfigPanelNode: SettingTreeNode = { + key: 'server-config', + label: 'Server-Config', + children: [] +} + const extensionPanelNodeList = computed(() => { const settingStore = useSettingStore() const showExtensionPanel = settingStore.get('Comfy.Settings.ExtensionPanel') return showExtensionPanel ? [extensionPanelNode] : [] }) +/** + * Server config panel is only available in Electron. We might want to support + * it in the web version in the future. + */ +const serverConfigPanelNodeList = computed(() => { + return isElectron() ? [serverConfigPanelNode] : [] +}) + const settingStore = useSettingStore() const settingRoot = computed(() => settingStore.settingTree) const categories = computed(() => [ ...(settingRoot.value.children || []), keybindingPanelNode, ...extensionPanelNodeList.value, + ...serverConfigPanelNodeList.value, aboutPanelNode ]) const activeCategory = ref(null) diff --git a/src/components/dialog/content/setting/ServerConfigPanel.vue b/src/components/dialog/content/setting/ServerConfigPanel.vue new file mode 100644 index 000000000..06c1b349e --- /dev/null +++ b/src/components/dialog/content/setting/ServerConfigPanel.vue @@ -0,0 +1,43 @@ + + + + {{ formatCamelCase(label) }} + + + + + + + diff --git a/src/constants/serverConfig.ts b/src/constants/serverConfig.ts new file mode 100644 index 000000000..53940fb9d --- /dev/null +++ b/src/constants/serverConfig.ts @@ -0,0 +1,428 @@ +import { FormItem } from '@/types/settingTypes' +import { + LatentPreviewMethod, + LogLevel, + HashFunction, + AutoLaunch, + CudaMalloc, + FloatingPointPrecision, + CrossAttentionMethod, + VramManagement +} from '@/types/serverArgs' + +export interface ServerConfig extends FormItem { + id: string + defaultValue: T + category?: string[] + // Override the default value getter with a custom function. + getValue?: (value: T) => Record +} + +export const SERVER_CONFIG_ITEMS: ServerConfig[] = [ + // Network settings + { + id: 'listen', + name: 'Host: The IP address to listen on', + category: ['Network'], + type: 'text', + defaultValue: '127.0.0.1' + }, + { + id: 'port', + name: 'Port: The port to listen on', + category: ['Network'], + type: 'number', + defaultValue: 8188 + }, + { + id: 'tls-keyfile', + name: 'TLS Key File: Path to TLS key file for HTTPS', + category: ['Network'], + type: 'text', + defaultValue: undefined + }, + { + id: 'tls-certfile', + name: 'TLS Certificate File: Path to TLS certificate file for HTTPS', + category: ['Network'], + type: 'text', + defaultValue: undefined + }, + { + id: 'enable-cors-header', + name: 'Enable CORS header: Use "*" for all origins or specify domain', + category: ['Network'], + type: 'text', + defaultValue: undefined + }, + { + id: 'max-upload-size', + name: 'Maximum upload size (MB)', + category: ['Network'], + type: 'number', + defaultValue: 100 + }, + + // Launch behavior + { + id: 'auto-launch', + name: 'Automatically opens in the browser on startup', + category: ['Launch'], + type: 'combo', + options: Object.values(AutoLaunch), + defaultValue: AutoLaunch.Auto, + getValue: (value: AutoLaunch) => { + switch (value) { + case AutoLaunch.Auto: + return {} + case AutoLaunch.Enable: + return { + ['auto-launch']: true + } + case AutoLaunch.Disable: + return { + ['disable-auto-launch']: true + } + } + } + }, + + // CUDA settings + { + id: 'cuda-device', + name: 'CUDA device index to use', + category: ['CUDA'], + type: 'number', + defaultValue: undefined + }, + { + id: 'cuda-malloc', + name: 'Use CUDA malloc for memory allocation', + category: ['CUDA'], + type: 'combo', + options: Object.values(CudaMalloc), + defaultValue: CudaMalloc.Auto, + getValue: (value: CudaMalloc) => { + switch (value) { + case CudaMalloc.Auto: + return {} + case CudaMalloc.Enable: + return { + ['cuda-malloc']: true + } + case CudaMalloc.Disable: + return { + ['disable-cuda-malloc']: true + } + } + } + }, + + // Precision settings + { + id: 'global-precision', + name: 'Global floating point precision', + category: ['Inference'], + type: 'combo', + options: [ + FloatingPointPrecision.AUTO, + FloatingPointPrecision.FP32, + FloatingPointPrecision.FP16 + ], + defaultValue: FloatingPointPrecision.AUTO, + tooltip: 'Global floating point precision', + getValue: (value: FloatingPointPrecision) => { + switch (value) { + case FloatingPointPrecision.AUTO: + return {} + case FloatingPointPrecision.FP32: + return { + ['force-fp32']: true + } + case FloatingPointPrecision.FP16: + return { + ['force-fp16']: true + } + default: + return {} + } + } + }, + + // UNET precision + { + id: 'unet-precision', + name: 'UNET precision', + category: ['Inference'], + type: 'combo', + options: [ + FloatingPointPrecision.AUTO, + FloatingPointPrecision.FP16, + FloatingPointPrecision.BF16, + FloatingPointPrecision.FP8E4M3FN, + FloatingPointPrecision.FP8E5M2 + ], + defaultValue: FloatingPointPrecision.AUTO, + tooltip: 'UNET precision', + getValue: (value: FloatingPointPrecision) => { + switch (value) { + case FloatingPointPrecision.AUTO: + return {} + default: + return { + [`${value.toLowerCase()}-unet`]: true + } + } + } + }, + + // VAE settings + { + id: 'vae-precision', + name: 'VAE precision', + category: ['Inference'], + type: 'combo', + options: [ + FloatingPointPrecision.AUTO, + FloatingPointPrecision.FP16, + FloatingPointPrecision.FP32, + FloatingPointPrecision.BF16 + ], + defaultValue: FloatingPointPrecision.AUTO, + tooltip: 'VAE precision', + getValue: (value: FloatingPointPrecision) => { + switch (value) { + case FloatingPointPrecision.AUTO: + return {} + default: + return { + [`${value.toLowerCase()}-vae`]: true + } + } + } + }, + { + id: 'cpu-vae', + name: 'Run VAE on CPU', + category: ['Inference'], + type: 'boolean', + defaultValue: false + }, + + // Text Encoder settings + { + id: 'text-encoder-precision', + name: 'Text Encoder precision', + category: ['Inference'], + type: 'combo', + options: [ + FloatingPointPrecision.AUTO, + FloatingPointPrecision.FP8E4M3FN, + FloatingPointPrecision.FP8E5M2, + FloatingPointPrecision.FP16, + FloatingPointPrecision.FP32 + ], + defaultValue: FloatingPointPrecision.AUTO, + tooltip: 'Text Encoder precision', + getValue: (value: FloatingPointPrecision) => { + switch (value) { + case FloatingPointPrecision.AUTO: + return {} + default: + return { + [`${value.toLowerCase()}-text-enc`]: true + } + } + } + }, + + // Memory and performance settings + { + id: 'force-channels-last', + name: 'Force channels-last memory format', + category: ['Memory'], + type: 'boolean', + defaultValue: false + }, + { + id: 'directml', + name: 'DirectML device index', + category: ['Memory'], + type: 'number', + defaultValue: undefined + }, + { + id: 'disable-ipex-optimize', + name: 'Disable IPEX optimization', + category: ['Memory'], + type: 'boolean', + defaultValue: false + }, + + // Preview settings + { + id: 'preview-method', + name: 'Method used for latent previews', + category: ['Preview'], + type: 'combo', + options: Object.values(LatentPreviewMethod), + defaultValue: LatentPreviewMethod.NoPreviews + }, + { + id: 'preview-size', + name: 'Size of preview images', + category: ['Preview'], + type: 'slider', + defaultValue: 512, + attrs: { + min: 128, + max: 2048, + step: 128 + } + }, + + // Cache settings + { + id: 'cache-classic', + name: 'Use classic cache system', + category: ['Cache'], + type: 'boolean', + defaultValue: false + }, + { + id: 'cache-lru', + name: 'Use LRU caching with a maximum of N node results cached. (0 to disable).', + category: ['Cache'], + type: 'number', + defaultValue: 0, + tooltip: 'May use more RAM/VRAM.' + }, + + // Attention settings + { + id: 'cross-attention-method', + name: 'Cross attention method', + category: ['Attention'], + type: 'combo', + options: Object.values(CrossAttentionMethod), + defaultValue: CrossAttentionMethod.Auto, + getValue: (value: CrossAttentionMethod) => { + switch (value) { + case CrossAttentionMethod.Auto: + return {} + default: + return { + [`use-${value.toLowerCase()}-cross-attention`]: true + } + } + } + }, + { + id: 'disable-xformers', + name: 'Disable xFormers optimization', + type: 'boolean', + defaultValue: false + }, + { + id: 'force-upcast-attention', + name: 'Force attention upcast', + category: ['Attention'], + type: 'boolean', + defaultValue: false + }, + { + id: 'dont-upcast-attention', + name: 'Prevent attention upcast', + category: ['Attention'], + type: 'boolean', + defaultValue: false + }, + + // VRAM management + { + id: 'vram-management', + name: 'VRAM management mode', + category: ['Memory'], + type: 'combo', + options: Object.values(VramManagement), + defaultValue: VramManagement.Auto, + getValue: (value: VramManagement) => { + switch (value) { + case VramManagement.Auto: + return {} + default: + return { + [value]: true + } + } + } + }, + { + id: 'reserve-vram', + name: 'Reserved VRAM (GB)', + category: ['Memory'], + type: 'number', + defaultValue: undefined, + tooltip: + 'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reverved depending on your OS.' + }, + + // Misc settings + { + id: 'default-hashing-function', + name: 'Default hashing function for model files', + type: 'combo', + options: Object.values(HashFunction), + defaultValue: HashFunction.SHA256 + }, + { + id: 'disable-smart-memory', + name: 'Force ComfyUI to agressively offload to regular ram instead of keeping models in vram when it can.', + category: ['Memory'], + type: 'boolean', + defaultValue: false + }, + { + id: 'deterministic', + name: 'Make pytorch use slower deterministic algorithms when it can.', + type: 'boolean', + defaultValue: false, + tooltip: 'Note that this might not make images deterministic in all cases.' + }, + { + id: 'fast', + name: 'Enable some untested and potentially quality deteriorating optimizations.', + type: 'boolean', + defaultValue: false + }, + { + id: 'dont-print-server', + name: "Don't print server output to console.", + type: 'boolean', + defaultValue: false + }, + { + id: 'disable-metadata', + name: 'Disable saving prompt metadata in files.', + type: 'boolean', + defaultValue: false + }, + { + id: 'disable-all-custom-nodes', + name: 'Disable loading all custom nodes.', + type: 'boolean', + defaultValue: false + }, + { + id: 'log-level', + name: 'Logging verbosity level', + type: 'combo', + options: Object.values(LogLevel), + defaultValue: LogLevel.INFO, + getValue: (value: LogLevel) => { + return { + verbose: value + } + } + } +] diff --git a/src/extensions/core/electronAdapter.ts b/src/extensions/core/electronAdapter.ts index 56d96b59a..7d4b06f34 100644 --- a/src/extensions/core/electronAdapter.ts +++ b/src/extensions/core/electronAdapter.ts @@ -31,14 +31,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil' type: 'boolean', defaultValue: true, onChange: onChangeRestartApp - }, - { - id: 'Comfy-Desktop.ComfyServer.ExtraLaunchArgs', - category: ['Comfy-Desktop', 'ComfyUI Server'], - name: 'Extra launch arguments passed to the ComfyUI main.py script', - type: 'text', - defaultValue: '', - onChange: onChangeRestartApp } ], diff --git a/src/stores/coreSettings.ts b/src/stores/coreSettings.ts index 0fd811261..d0fdfbed4 100644 --- a/src/stores/coreSettings.ts +++ b/src/stores/coreSettings.ts @@ -624,5 +624,23 @@ export const CORE_SETTINGS: SettingParams[] = [ type: 'boolean', defaultValue: false, versionAdded: '1.3.13' + }, + { + id: 'Comfy.Server.ServerConfigValues', + name: 'Server config values for frontend display', + tooltip: 'Server config values used for frontend display only', + type: 'hidden', + // Mapping from server config id to value. + defaultValue: {} as Record, + versionAdded: '1.4.8' + }, + { + id: 'Comfy.Server.LaunchArgs', + name: 'Server launch arguments', + tooltip: + 'These are the actual arguments that are passed to the server when it is launched.', + type: 'hidden', + defaultValue: {} as Record, + versionAdded: '1.4.8' } ] diff --git a/src/stores/serverConfigStore.ts b/src/stores/serverConfigStore.ts new file mode 100644 index 000000000..0ffb279ca --- /dev/null +++ b/src/stores/serverConfigStore.ts @@ -0,0 +1,73 @@ +import { ServerConfig } from '@/constants/serverConfig' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +export type ServerConfigWithValue = ServerConfig & { + value: T +} + +export const useServerConfigStore = defineStore('serverConfig', () => { + const serverConfigById = ref>>({}) + const serverConfigs = computed(() => { + return Object.values(serverConfigById.value) + }) + const serverConfigsByCategory = computed< + Record[]> + >(() => { + return serverConfigs.value.reduce( + (acc, config) => { + const category = config.category?.[0] ?? 'General' + acc[category] = acc[category] || [] + acc[category].push(config) + return acc + }, + {} as Record[]> + ) + }) + const serverConfigValues = computed>(() => { + return Object.fromEntries( + serverConfigs.value.map((config) => { + return [ + config.id, + config.value === config.defaultValue || !config.value + ? undefined + : config.value + ] + }) + ) + }) + const launchArgs = computed>(() => { + return Object.assign( + {}, + ...serverConfigs.value.map((config) => { + if (config.value === config.defaultValue || !config.value) { + return {} + } + return config.getValue + ? config.getValue(config.value) + : { [config.id]: config.value } + }) + ) + }) + + function loadServerConfig( + configs: ServerConfig[], + values: Record + ) { + for (const config of configs) { + serverConfigById.value[config.id] = { + ...config, + value: values[config.id] ?? config.defaultValue + } + } + } + + return { + serverConfigById, + serverConfigs, + serverConfigsByCategory, + serverConfigValues, + launchArgs, + loadServerConfig + } +}) diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index 8ab6a8fb8..411bbe852 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -526,7 +526,9 @@ const zSettings = z.record(z.any()).and( 'Comfy.Settings.ExtensionPanel': z.boolean(), 'Comfy.LinkRenderMode': z.number(), 'Comfy.Node.AutoSnapLinkToSlot': z.boolean(), - 'Comfy.Node.SnapHighlightsNode': 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()) }) .optional() ) diff --git a/src/types/serverArgs.ts b/src/types/serverArgs.ts new file mode 100644 index 000000000..aeb43ea49 --- /dev/null +++ b/src/types/serverArgs.ts @@ -0,0 +1,65 @@ +export enum LatentPreviewMethod { + NoPreviews = 'none', + Auto = 'auto', + Latent2RGB = 'latent2rgb', + TAESD = 'taesd' +} + +export enum LogLevel { + DEBUG = 'DEBUG', + INFO = 'INFO', + WARNING = 'WARNING', + ERROR = 'ERROR', + CRITICAL = 'CRITICAL' +} + +export enum HashFunction { + MD5 = 'md5', + SHA1 = 'sha1', + SHA256 = 'sha256', + SHA512 = 'sha512' +} + +export enum AutoLaunch { + // Let server decide whether to auto launch based on the current environment + Auto = 'auto', + // Disable auto launch + Disable = 'disable', + // Enable auto launch + Enable = 'enable' +} + +export enum CudaMalloc { + // Let server decide whether to use CUDA malloc based on the current environment + Auto = 'auto', + // Disable CUDA malloc + Disable = 'disable', + // Enable CUDA malloc + Enable = 'enable' +} + +export enum FloatingPointPrecision { + AUTO = 'auto', + FP32 = 'fp32', + FP16 = 'fp16', + BF16 = 'bf16', + FP8E4M3FN = 'fp8_e4m3fn', + FP8E5M2 = 'fp8_e5m2' +} + +export enum CrossAttentionMethod { + Auto = 'auto', + Split = 'split', + Quad = 'quad', + Pytorch = 'pytorch' +} + +export enum VramManagement { + Auto = 'auto', + GPUOnly = 'gpu-only', + HighVram = 'highvram', + NormalVram = 'normalvram', + LowVram = 'lowvram', + NoVram = 'novram', + CPU = 'cpu' +} diff --git a/tests-ui/tests/fast/store/serverConfigStore.test.ts b/tests-ui/tests/fast/store/serverConfigStore.test.ts new file mode 100644 index 000000000..034db00bd --- /dev/null +++ b/tests-ui/tests/fast/store/serverConfigStore.test.ts @@ -0,0 +1,189 @@ +import { setActivePinia, createPinia } from 'pinia' +import { useServerConfigStore } from '@/stores/serverConfigStore' +import { ServerConfig } from '@/constants/serverConfig' +import type { FormItem } from '@/types/settingTypes' + +const dummyFormItem: FormItem = { + name: '', + type: 'text' +} + +describe('useServerConfigStore', () => { + let store: ReturnType + + beforeEach(() => { + setActivePinia(createPinia()) + store = useServerConfigStore() + }) + + it('should initialize with empty configs', () => { + expect(store.serverConfigs).toHaveLength(0) + expect(Object.keys(store.serverConfigById)).toHaveLength(0) + expect(Object.keys(store.serverConfigsByCategory)).toHaveLength(0) + expect(Object.keys(store.serverConfigValues)).toHaveLength(0) + expect(Object.keys(store.launchArgs)).toHaveLength(0) + }) + + it('should load server configs with default values', () => { + const configs: ServerConfig[] = [ + { + ...dummyFormItem, + id: 'test.config1', + defaultValue: 'default1', + category: ['Test'] + }, + { + ...dummyFormItem, + id: 'test.config2', + defaultValue: 'default2' + } + ] + + store.loadServerConfig(configs, {}) + + expect(store.serverConfigs).toHaveLength(2) + expect(store.serverConfigById['test.config1'].value).toBe('default1') + expect(store.serverConfigById['test.config2'].value).toBe('default2') + }) + + it('should load server configs with provided values', () => { + const configs: ServerConfig[] = [ + { + ...dummyFormItem, + id: 'test.config1', + defaultValue: 'default1', + category: ['Test'] + } + ] + + store.loadServerConfig(configs, { + 'test.config1': 'custom1' + }) + + expect(store.serverConfigs).toHaveLength(1) + expect(store.serverConfigById['test.config1'].value).toBe('custom1') + }) + + it('should organize configs by category', () => { + const configs: ServerConfig[] = [ + { + ...dummyFormItem, + id: 'test.config1', + defaultValue: 'default1', + category: ['Test'] + }, + { + ...dummyFormItem, + id: 'test.config2', + defaultValue: 'default2', + category: ['Other'] + }, + { + ...dummyFormItem, + id: 'test.config3', + defaultValue: 'default3' + } + ] + + store.loadServerConfig(configs, {}) + + expect(Object.keys(store.serverConfigsByCategory)).toHaveLength(3) + expect(store.serverConfigsByCategory['Test']).toHaveLength(1) + expect(store.serverConfigsByCategory['Other']).toHaveLength(1) + expect(store.serverConfigsByCategory['General']).toHaveLength(1) + }) + + it('should generate server config values excluding defaults', () => { + const configs: ServerConfig[] = [ + { + ...dummyFormItem, + id: 'test.config1', + defaultValue: 'default1' + }, + { + ...dummyFormItem, + id: 'test.config2', + defaultValue: 'default2' + } + ] + + store.loadServerConfig(configs, { + 'test.config1': 'custom1', + 'test.config2': 'default2' + }) + + expect(Object.keys(store.serverConfigValues)).toHaveLength(2) + expect(store.serverConfigValues['test.config1']).toBe('custom1') + expect(store.serverConfigValues['test.config2']).toBeUndefined() + }) + + it('should generate launch arguments with custom getValue function', () => { + const configs: ServerConfig[] = [ + { + ...dummyFormItem, + id: 'test.config1', + defaultValue: 'default1', + getValue: (value: string) => ({ customArg: value }) + }, + { + ...dummyFormItem, + id: 'test.config2', + defaultValue: 'default2' + } + ] + + store.loadServerConfig(configs, { + 'test.config1': 'custom1', + 'test.config2': 'custom2' + }) + + expect(Object.keys(store.launchArgs)).toHaveLength(2) + expect(store.launchArgs['customArg']).toBe('custom1') + expect(store.launchArgs['test.config2']).toBe('custom2') + }) + + it('should not include default values in launch arguments', () => { + const configs: ServerConfig[] = [ + { + ...dummyFormItem, + id: 'test.config1', + defaultValue: 'default1' + }, + { + ...dummyFormItem, + id: 'test.config2', + defaultValue: 'default2' + } + ] + + store.loadServerConfig(configs, { + 'test.config1': 'custom1', + 'test.config2': 'default2' + }) + + expect(Object.keys(store.launchArgs)).toHaveLength(1) + expect(store.launchArgs['test.config1']).toBe('custom1') + expect(store.launchArgs['test.config2']).toBeUndefined() + }) + + it('should not include nullish values in launch arguments', () => { + const configs: ServerConfig[] = [ + { ...dummyFormItem, id: 'test.config1', defaultValue: 'default1' }, + { ...dummyFormItem, id: 'test.config2', defaultValue: 'default2' }, + { ...dummyFormItem, id: 'test.config3', defaultValue: 'default3' } + ] + + store.loadServerConfig(configs, { + 'test.config1': undefined, + 'test.config2': null, + 'test.config3': '' + }) + + expect(Object.keys(store.launchArgs)).toHaveLength(0) + expect(Object.keys(store.serverConfigValues)).toEqual([ + 'test.config1', + 'test.config2', + 'test.config3' + ]) + }) +})