diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 53acf0546..14c958591 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -10,7 +10,7 @@ module.exports = defineConfig({ entryLocale: 'en', output: 'src/locales', outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'], - reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. + reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png index 4cdf53e1c..cd3d56d85 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts new file mode 100644 index 000000000..ee2c85eda --- /dev/null +++ b/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts @@ -0,0 +1,86 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../../fixtures/ComfyPage' + +test.describe('Properties panel position', () => { + test.beforeEach(async ({ comfyPage }) => { + // Open a sidebar tab to ensure sidebar is visible + await comfyPage.menu.nodeLibraryTab.open() + await comfyPage.actionbar.propertiesButton.click() + }) + + test('positions on the right when sidebar is on the left', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.nextFrame() + + const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const sidebar = comfyPage.page.locator('.side-bar-panel').first() + + await expect(propertiesPanel).toBeVisible() + await expect(sidebar).toBeVisible() + + const propsBoundingBox = await propertiesPanel.boundingBox() + const sidebarBoundingBox = await sidebar.boundingBox() + + expect(propsBoundingBox).not.toBeNull() + expect(sidebarBoundingBox).not.toBeNull() + + // Properties panel should be to the right of the sidebar + expect(propsBoundingBox!.x).toBeGreaterThan( + sidebarBoundingBox!.x + sidebarBoundingBox!.width + ) + }) + + test('positions on the left when sidebar is on the right', async ({ + comfyPage + }) => { + await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.nextFrame() + + const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const sidebar = comfyPage.page.locator('.side-bar-panel').first() + + await expect(propertiesPanel).toBeVisible() + await expect(sidebar).toBeVisible() + + const propsBoundingBox = await propertiesPanel.boundingBox() + const sidebarBoundingBox = await sidebar.boundingBox() + + expect(propsBoundingBox).not.toBeNull() + expect(sidebarBoundingBox).not.toBeNull() + + // Properties panel should be to the left of the sidebar + expect(propsBoundingBox!.x + propsBoundingBox!.width).toBeLessThan( + sidebarBoundingBox!.x + ) + }) + + test('close button icon updates based on sidebar location', async ({ + comfyPage + }) => { + const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + + // When sidebar is on the left, panel is on the right + await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.nextFrame() + + await expect(propertiesPanel).toBeVisible() + const closeButtonLeft = propertiesPanel + .locator('button[aria-pressed]') + .locator('i') + await expect(closeButtonLeft).toBeVisible() + await expect(closeButtonLeft).toHaveClass(/lucide--panel-right/) + + // When sidebar is on the right, panel is on the left + await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.nextFrame() + + const closeButtonRight = propertiesPanel + .locator('button[aria-pressed]') + .locator('i') + await expect(closeButtonRight).toBeVisible() + await expect(closeButtonRight).toHaveClass(/lucide--panel-left/) + }) +}) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 730acc749..5e66054a0 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -109,22 +109,27 @@ test.describe('Templates', () => { }) test('Uses proper locale files for templates', async ({ comfyPage }) => { - // Load the templates dialog and wait for the French index file request - const requestPromise = comfyPage.page.waitForRequest( - '**/templates/index.fr.json' - ) - - // Set locale to French before opening templates await comfyPage.setSetting('Comfy.Locale', 'fr') await comfyPage.executeCommand('Comfy.BrowseTemplates') - const request = await requestPromise + const dialog = comfyPage.page.getByRole('dialog').filter({ + has: comfyPage.page.getByRole('heading', { name: 'Modèles', exact: true }) + }) + await expect(dialog).toBeVisible() - // Verify French index was requested - expect(request.url()).toContain('templates/index.fr.json') + // Validate that French-localized strings from the templates index are rendered + await expect( + dialog.getByRole('heading', { name: 'Modèles', exact: true }) + ).toBeVisible() + await expect( + dialog.getByRole('button', { name: 'Tous les modèles', exact: true }) + ).toBeVisible() - await expect(comfyPage.templates.content).toBeVisible() + // Ensure the English fallback copy is not shown anywhere + await expect( + comfyPage.page.getByText('All Templates', { exact: true }) + ).toHaveCount(0) }) test('Falls back to English templates when locale file not found', async ({ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index c929144a7..83f50b037 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index e2adb6941..91cf132e5 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 042ae1f5f..c224f4766 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index c5ad54c6c..21d56b070 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 3caea79f2..e53c7959b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index 98c2ad599..b7174ca64 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 2383a7868..5ebd219fc 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index cd9458859..d660caf64 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 49f001a12..379b91460 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 8e1b42fcb..a073f0fcc 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png index 06f4d490a..783f0dc0c 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png index 8b5c63a41..8610dab8b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png index b69e96dac..c4d5979d0 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png index 2f7b72e1a..d43f10dde 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png differ diff --git a/package.json b/package.json index 136eed519..cc2872d02 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.36.8", + "version": "1.36.12", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", diff --git a/packages/registry-types/src/comfyRegistryTypes.ts b/packages/registry-types/src/comfyRegistryTypes.ts index f814e64e9..f2d69fd29 100644 --- a/packages/registry-types/src/comfyRegistryTypes.ts +++ b/packages/registry-types/src/comfyRegistryTypes.ts @@ -11910,6 +11910,8 @@ export interface operations { "application/json": { /** @description Optional URL to redirect the customer after they're done with the billing portal */ return_url?: string; + /** @description Optional target subscription tier. When provided, creates a deep link directly to the subscription update confirmation screen with this tier pre-selected. */ + target_tier?: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly"; }; }; }; diff --git a/public/assets/images/hf-logo.svg b/public/assets/images/hf-logo.svg new file mode 100644 index 000000000..ab959d165 --- /dev/null +++ b/public/assets/images/hf-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/components/LiteGraphCanvasSplitterOverlay.vue b/src/components/LiteGraphCanvasSplitterOverlay.vue index 4d56e623b..4a7695d18 100644 --- a/src/components/LiteGraphCanvasSplitterOverlay.vue +++ b/src/components/LiteGraphCanvasSplitterOverlay.vue @@ -22,29 +22,38 @@ state-storage="local" @resizestart="onResizestart" > + + + @@ -73,38 +82,33 @@ + + - - - - - @@ -117,6 +121,7 @@ import Splitter from 'primevue/splitter' import type { SplitterResizeStartEvent } from 'primevue/splitter' import SplitterPanel from 'primevue/splitterpanel' import { computed } from 'vue' +import { useI18n } from 'vue-i18n' import { useSettingStore } from '@/platform/settings/settingStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' @@ -128,6 +133,7 @@ const workspaceStore = useWorkspaceStore() const settingStore = useSettingStore() const rightSidePanelStore = useRightSidePanelStore() const sidebarTabStore = useSidebarTabStore() +const { t } = useI18n() const sidebarLocation = computed<'left' | 'right'>(() => settingStore.get('Comfy.Sidebar.Location') ) @@ -159,12 +165,25 @@ function onResizestart({ originalEvent: event }: SplitterResizeStartEvent) { } /* - * Force refresh the splitter when right panel visibility changes to recalculate the width + * Force refresh the splitter when right panel visibility or sidebar location changes + * to recalculate the width and panel order */ const splitterRefreshKey = computed(() => { - return rightSidePanelVisible.value - ? 'main-splitter-with-right-panel' - : 'main-splitter' + return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}` +}) + +const firstPanelStyle = computed(() => { + if (sidebarLocation.value === 'left') { + return { display: sidebarPanelVisible.value ? 'flex' : 'none' } + } + return undefined +}) + +const lastPanelStyle = computed(() => { + if (sidebarLocation.value === 'right') { + return { display: sidebarPanelVisible.value ? 'flex' : 'none' } + } + return undefined }) diff --git a/src/components/queue/QueueProgressOverlay.vue b/src/components/queue/QueueProgressOverlay.vue index a941af55a..78efb7a78 100644 --- a/src/components/queue/QueueProgressOverlay.vue +++ b/src/components/queue/QueueProgressOverlay.vue @@ -197,7 +197,16 @@ const displayedJobGroups = computed(() => groupedJobItems.value) const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => { const promptId = item.taskRef?.promptId if (!promptId) return - await api.interrupt(promptId) + + if (item.state === 'running' || item.state === 'initialization') { + // Running/initializing jobs: interrupt execution + await api.interrupt(promptId) + await queueStore.update() + } else if (item.state === 'pending') { + // Pending jobs: remove from queue + await api.deleteItem('queue', promptId) + await queueStore.update() + } }) const onDeleteItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => { diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index 50dc9de33..4eb2ae31b 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -9,6 +9,7 @@ import TabList from '@/components/tab/TabList.vue' import Button from '@/components/ui/button/Button.vue' import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore' @@ -22,11 +23,23 @@ import SubgraphEditor from './subgraph/SubgraphEditor.vue' const canvasStore = useCanvasStore() const rightSidePanelStore = useRightSidePanelStore() +const settingStore = useSettingStore() const { t } = useI18n() const { selectedItems } = storeToRefs(canvasStore) const { activeTab, isEditingSubgraph } = storeToRefs(rightSidePanelStore) +const sidebarLocation = computed<'left' | 'right'>(() => + settingStore.get('Comfy.Sidebar.Location') +) + +// Panel is on the left when sidebar is on the right, and vice versa +const panelIcon = computed(() => + sidebarLocation.value === 'right' + ? 'icon-[lucide--panel-left]' + : 'icon-[lucide--panel-right]' +) + const hasSelection = computed(() => selectedItems.value.length > 0) const selectedNodes = computed((): LGraphNode[] => { @@ -160,7 +173,7 @@ function handleTitleCancel() { :aria-label="t('rightSidePanel.togglePanel')" @click="closePanel" > - + diff --git a/src/components/rightSidePanel/parameters/SectionWidgets.vue b/src/components/rightSidePanel/parameters/SectionWidgets.vue index fc9b71d10..71ee6194f 100644 --- a/src/components/rightSidePanel/parameters/SectionWidgets.vue +++ b/src/components/rightSidePanel/parameters/SectionWidgets.vue @@ -2,6 +2,7 @@ import { computed, provide } from 'vue' import { useI18n } from 'vue-i18n' +import { useReactiveWidgetValue } from '@/composables/graph/useGraphNodeManager' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' @@ -71,7 +72,7 @@ const displayLabel = computed( { window.open(response.checkout_url, '_blank') }, reportError) - const accessBillingPortal = wrapWithErrorHandlingAsync(async () => { - const response = await authStore.accessBillingPortal() + const accessBillingPortal = wrapWithErrorHandlingAsync< + [targetTier?: BillingPortalTargetTier], + void + >(async (targetTier) => { + const response = await authStore.accessBillingPortal(targetTier) if (!response.billing_portal_url) { throw new Error( t('toastMessages.failedToAccessBillingPortal', { diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index 3a8df1ce3..4482f9433 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -3,7 +3,7 @@ * Provides event-driven reactivity with performance optimizations */ import { reactiveComputed } from '@vueuse/core' -import { reactive, shallowReactive } from 'vue' +import { customRef, reactive, shallowReactive } from 'vue' import { useChainCallback } from '@/composables/functional/useChainCallback' import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget' @@ -90,6 +90,23 @@ export interface GraphNodeManager { cleanup(): void } +function widgetWithVueTrack( + widget: IBaseWidget +): asserts widget is IBaseWidget & { vueTrack: () => void } { + if (widget.vueTrack) return + + customRef((track, trigger) => { + widget.callback = useChainCallback(widget.callback, trigger) + widget.vueTrack = track + return { get() {}, set() {} } + }) +} +export function useReactiveWidgetValue(widget: IBaseWidget) { + widgetWithVueTrack(widget) + widget.vueTrack() + return widget.value +} + function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined { const cagWidget = widget.linkedWidgets?.find( (w) => w.name == 'control_after_generate' @@ -106,6 +123,37 @@ function getNodeType(node: LGraphNode, widget: IBaseWidget) { return subNode?.type } +/** + * Validates that a value is a valid WidgetValue type + */ +const normalizeWidgetValue = (value: unknown): WidgetValue => { + if (value === null || value === undefined || value === void 0) { + return undefined + } + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value + } + if (typeof value === 'object') { + // Check if it's a File array + if ( + Array.isArray(value) && + value.length > 0 && + value.every((item): item is File => item instanceof File) + ) { + return value + } + // Otherwise it's a generic object + return value + } + // If none of the above, return undefined + console.warn(`Invalid widget value type: ${typeof value}`, value) + return undefined +} + export function safeWidgetMapper( node: LGraphNode, slotMetadata: Map @@ -113,19 +161,6 @@ export function safeWidgetMapper( const nodeDefStore = useNodeDefStore() return function (widget) { try { - // TODO: Use widget.getReactiveData() once TypeScript types are updated - let value = widget.value - - // For combo widgets, if value is undefined, use the first option as default - if ( - value === undefined && - widget.type === 'combo' && - widget.options?.values && - Array.isArray(widget.options.values) && - widget.options.values.length > 0 - ) { - value = widget.options.values[0] - } const spec = nodeDefStore.getInputSpecForWidget(node, widget.name) const slotInfo = slotMetadata.get(widget.name) const borderStyle = widget.promoted @@ -133,13 +168,18 @@ export function safeWidgetMapper( : widget.advanced ? 'ring ring-component-node-widget-advanced' : undefined + const callback = (v: unknown) => { + const value = normalizeWidgetValue(v) + widget.value = value ?? undefined + widget.callback?.(value) + } return { name: widget.name, type: widget.type, - value: value, + value: useReactiveWidgetValue(widget), borderStyle, - callback: widget.callback, + callback, controlWidget: getControlWidget(widget), isDOMWidget: isDOMWidget(widget), label: widget.label, @@ -286,128 +326,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { return nodeRefs.get(id) } - /** - * Validates that a value is a valid WidgetValue type - */ - const validateWidgetValue = (value: unknown): WidgetValue => { - if (value === null || value === undefined || value === void 0) { - return undefined - } - if ( - typeof value === 'string' || - typeof value === 'number' || - typeof value === 'boolean' - ) { - return value - } - if (typeof value === 'object') { - // Check if it's a File array - if ( - Array.isArray(value) && - value.length > 0 && - value.every((item): item is File => item instanceof File) - ) { - return value - } - // Otherwise it's a generic object - return value - } - // If none of the above, return undefined - console.warn(`Invalid widget value type: ${typeof value}`, value) - return undefined - } - - /** - * Updates Vue state when widget values change - */ - const updateVueWidgetState = ( - nodeId: string, - widgetName: string, - value: unknown - ): void => { - try { - const currentData = vueNodeData.get(nodeId) - if (!currentData?.widgets) return - - const updatedWidgets = currentData.widgets.map((w) => - w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w - ) - // Create a completely new object to ensure Vue reactivity triggers - const updatedData = { - ...currentData, - widgets: updatedWidgets - } - - vueNodeData.set(nodeId, updatedData) - } catch (error) { - // Ignore widget update errors to prevent cascade failures - } - } - - /** - * Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync - */ - const createWrappedWidgetCallback = ( - widget: IBaseWidget, // LiteGraph widget with minimal typing - originalCallback: ((value: unknown) => void) | undefined, - nodeId: string - ) => { - let updateInProgress = false - - return (value: unknown) => { - if (updateInProgress) return - updateInProgress = true - - try { - // 1. Update the widget value in LiteGraph (critical for LiteGraph state) - // Validate that the value is of an acceptable type - if ( - value !== null && - value !== undefined && - typeof value !== 'string' && - typeof value !== 'number' && - typeof value !== 'boolean' && - typeof value !== 'object' - ) { - console.warn(`Invalid widget value type: ${typeof value}`) - updateInProgress = false - return - } - - // Always update widget.value to ensure sync - widget.value = value ?? undefined - - // 2. Call the original callback if it exists - if (originalCallback && widget.type !== 'asset') { - originalCallback.call(widget, value) - } - - // 3. Update Vue state to maintain synchronization - updateVueWidgetState(nodeId, widget.name, value) - } finally { - updateInProgress = false - } - } - } - - /** - * Sets up widget callbacks for a node - */ - const setupNodeWidgetCallbacks = (node: LGraphNode) => { - if (!node.widgets) return - - const nodeId = String(node.id) - - node.widgets.forEach((widget) => { - const originalCallback = widget.callback - widget.callback = createWrappedWidgetCallback( - widget, - originalCallback, - nodeId - ) - }) - } - const syncWithGraph = () => { if (!graph?._nodes) return @@ -428,9 +346,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { // Store non-reactive reference nodeRefs.set(id, node) - // Set up widget callbacks BEFORE extracting data (critical order) - setupNodeWidgetCallbacks(node) - // Extract and store safe data for Vue vueNodeData.set(id, extractVueNodeData(node)) }) @@ -449,9 +364,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager { // Store non-reactive reference to original node nodeRefs.set(id, node) - // Set up widget callbacks BEFORE extracting data (critical order) - setupNodeWidgetCallbacks(node) - // Extract initial data for Vue (may be incomplete during graph configure) vueNodeData.set(id, extractVueNodeData(node)) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index c6ad985c8..cf627b6f9 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -116,6 +116,31 @@ const makeOmniProDurationCalculator = return formatCreditsLabel(cost) } +const klingMotionControlPricingCalculator: PricingFunction = ( + node: LGraphNode +): string => { + const modeWidget = node.widgets?.find( + (w) => w.name === 'mode' + ) as IComboWidget + + if (!modeWidget) { + return formatCreditsListLabel([0.07, 0.112], { + suffix: '/second', + note: '(std/pro)' + }) + } + + const mode = String(modeWidget.value).toLowerCase() + + if (mode === 'pro') return formatCreditsLabel(0.112, { suffix: '/second' }) + if (mode === 'std') return formatCreditsLabel(0.07, { suffix: '/second' }) + + return formatCreditsListLabel([0.07, 0.112], { + suffix: '/second', + note: '(std/pro)' + }) +} + const pixversePricingCalculator = (node: LGraphNode): string => { const durationWidget = node.widgets?.find( (w) => w.name === 'duration_seconds' @@ -1034,6 +1059,9 @@ const apiNodeCosts: Record = KlingOmniProVideoToVideoNode: { displayPrice: makeOmniProDurationCalculator(0.168) }, + KlingMotionControl: { + displayPrice: klingMotionControlPricingCalculator + }, KlingOmniProEditVideoNode: { displayPrice: formatCreditsLabel(0.168, { suffix: '/second' }) }, @@ -2117,6 +2145,7 @@ export const useNodePricing = () => { KlingOmniProFirstLastFrameNode: ['duration'], KlingOmniProImageToVideoNode: ['duration'], KlingOmniProVideoToVideoNode: ['duration'], + KlingMotionControl: ['mode'], MinimaxHailuoVideoNode: ['resolution', 'duration'], OpenAIDalle3: ['size', 'quality'], OpenAIDalle2: ['size', 'n'], diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 4f2e65abd..697818c4e 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -13,7 +13,8 @@ export enum ServerFeatureFlag { MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled', ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled', PRIVATE_MODELS_ENABLED = 'private_models_enabled', - ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled' + ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled', + HUGGINGFACE_MODEL_IMPORT_ENABLED = 'huggingface_model_import_enabled' } /** @@ -62,6 +63,16 @@ export function useFeatureFlags() { remoteConfig.value.onboarding_survey_enabled ?? api.getServerFeature(ServerFeatureFlag.ONBOARDING_SURVEY_ENABLED, true) ) + }, + get huggingfaceModelImportEnabled() { + // Check remote config first (from /api/features), fall back to websocket feature flags + return ( + remoteConfig.value.huggingface_model_import_enabled ?? + api.getServerFeature( + ServerFeatureFlag.HUGGINGFACE_MODEL_IMPORT_ENABLED, + false + ) + ) } }) diff --git a/src/core/graph/subgraph/proxyWidget.ts b/src/core/graph/subgraph/proxyWidget.ts index ef04bd76b..60f6d456a 100644 --- a/src/core/graph/subgraph/proxyWidget.ts +++ b/src/core/graph/subgraph/proxyWidget.ts @@ -211,11 +211,9 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) { } return Reflect.get(redirectedTarget, property, redirectedReceiver) }, - set(_t: IBaseWidget, property: string, value: unknown, receiver: object) { + set(_t: IBaseWidget, property: string, value: unknown) { let redirectedTarget: object = backingWidget - let redirectedReceiver = receiver - if (property == 'value') redirectedReceiver = backingWidget - else if (property == 'computedHeight') { + if (property == 'computedHeight') { if (overlay.widgetName.startsWith('$$') && linkedNode) { updatePreviews(linkedNode) } @@ -228,9 +226,8 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) { } if (Object.prototype.hasOwnProperty.call(overlay, property)) { redirectedTarget = overlay - redirectedReceiver = overlay } - return Reflect.set(redirectedTarget, property, value, redirectedReceiver) + return Reflect.set(redirectedTarget, property, value, redirectedTarget) }, getPrototypeOf() { return Reflect.getPrototypeOf(backingWidget) diff --git a/src/extensions/core/imageCompare.ts b/src/extensions/core/imageCompare.ts new file mode 100644 index 000000000..608590f1b --- /dev/null +++ b/src/extensions/core/imageCompare.ts @@ -0,0 +1,48 @@ +import type { NodeExecutionOutput } from '@/schemas/apiSchema' +import { api } from '@/scripts/api' +import { app } from '@/scripts/app' +import { useExtensionService } from '@/services/extensionService' + +useExtensionService().registerExtension({ + name: 'Comfy.ImageCompare', + + async nodeCreated(node) { + if (node.constructor.comfyClass !== 'ImageCompare') return + + const [oldWidth, oldHeight] = node.size + node.setSize([Math.max(oldWidth, 400), Math.max(oldHeight, 350)]) + + const onExecuted = node.onExecuted + + node.onExecuted = function (output: NodeExecutionOutput) { + onExecuted?.call(this, output) + + const aImages = (output as Record).a_images as + | Record[] + | undefined + const bImages = (output as Record).b_images as + | Record[] + | undefined + const rand = app.getRandParam() + + const beforeUrl = + aImages && aImages.length > 0 + ? api.apiURL(`/view?${new URLSearchParams(aImages[0])}${rand}`) + : '' + const afterUrl = + bImages && bImages.length > 0 + ? api.apiURL(`/view?${new URLSearchParams(bImages[0])}${rand}`) + : '' + + const widget = node.widgets?.find((w) => w.type === 'imagecompare') + + if (widget) { + widget.value = { + before: beforeUrl, + after: afterUrl + } + widget.callback?.(widget.value) + } + } + } +}) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index a007a365d..ba36f847a 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -9,6 +9,7 @@ import './electronAdapter' import './groupNode' import './groupNodeManage' import './groupOptions' +import './imageCompare' import './load3d' import './maskeditor' import './nodeTemplates' diff --git a/src/lib/litegraph/src/DragAndScale.ts b/src/lib/litegraph/src/DragAndScale.ts index c71fca2d2..d2e35252d 100644 --- a/src/lib/litegraph/src/DragAndScale.ts +++ b/src/lib/litegraph/src/DragAndScale.ts @@ -192,8 +192,14 @@ export class DragAndScale { bounds: ReadOnlyRect, { zoom = 0.75 }: { zoom?: number } = {} ): void { - const cw = this.element.width / window.devicePixelRatio - const ch = this.element.height / window.devicePixelRatio + //If element hasn't initialized (browser tab is in background) + //it has a size of 300x150 and a more reasonable default is used instead. + const [width, height] = + this.element.width === 300 && this.element.height === 150 + ? [1920, 1080] + : [this.element.width, this.element.height] + const cw = width / window.devicePixelRatio + const ch = height / window.devicePixelRatio let targetScale = this.scale if (zoom > 0) { diff --git a/src/lib/litegraph/src/types/widgets.ts b/src/lib/litegraph/src/types/widgets.ts index f383f9404..1cfb323d8 100644 --- a/src/lib/litegraph/src/types/widgets.ts +++ b/src/lib/litegraph/src/types/widgets.ts @@ -284,6 +284,7 @@ export interface IBaseWidget< /** Widget type (see {@link TWidgetType}) */ type: TType value?: TValue + vueTrack?: () => void /** * Whether the widget value should be serialized on node serialization. diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 665a9ba57..a1f15efe8 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -648,6 +648,7 @@ "box": "Box" }, "sideToolbar": { + "sidebar": "Sidebar", "themeToggle": "Toggle Theme", "helpCenter": "Help Center", "logout": "Logout", @@ -1416,6 +1417,7 @@ "latent": "latent", "3d": "3d", "ltxv": "ltxv", + "qwen": "qwen", "sd3": "sd3", "unet": "unet", "sigmas": "sigmas", @@ -1447,7 +1449,6 @@ "photomaker": "photomaker", "PixVerse": "PixVerse", "primitive": "primitive", - "qwen": "qwen", "Recraft": "Recraft", "edit_models": "edit_models", "Rodin": "Rodin", @@ -1607,6 +1608,9 @@ "errorMessage": "Failed to copy to clipboard", "errorNotSupported": "Clipboard API not supported in your browser" }, + "imageCompare": { + "noImages": "No images to compare" + }, "load3d": { "switchCamera": "Switch Camera", "showGrid": "Show Grid", @@ -2233,8 +2237,11 @@ "baseModels": "Base models", "browseAssets": "Browse Assets", "checkpoints": "Checkpoints", - "civitaiLinkExample": "Example: https://civitai.com/models/10706/luisap-z-image-and-qwen-pixel-art-refiner?modelVersionId=2225295", - "civitaiLinkLabel": "Civitai model download link", + "civitaiLinkExample": "{example} {link}", + "civitaiLinkExampleStrong": "Example:", + "civitaiLinkExampleUrl": "https://civitai.com/models/10706/luisap-z-image-and-qwen-pixel-art-refiner?modelVersionId=2225295", + "civitaiLinkLabel": "Civitai model {download} link", + "civitaiLinkLabelDownload": "download", "civitaiLinkPlaceholder": "Paste link here", "confirmModelDetails": "Confirm Model Details", "connectionError": "Please check your connection and try again", @@ -2252,8 +2259,11 @@ "filterBy": "Filter by", "findInLibrary": "Find it in the {type} section of the models library.", "finish": "Finish", + "genericLinkPlaceholder": "Paste link here", "jobId": "Job ID", "loadingModels": "Loading {type}...", + "maxFileSize": "Max file size: {size}", + "maxFileSizeValue": "1 GB", "modelAssociatedWithLink": "The model associated with the link you provided:", "modelName": "Model Name", "modelNamePlaceholder": "Enter a name for this model", @@ -2268,20 +2278,24 @@ "ownershipAll": "All", "ownershipMyModels": "My models", "ownershipPublicModels": "Public models", + "providerCivitai": "Civitai", + "providerHuggingFace": "Hugging Face", + "noValidSourceDetected": "No valid import source detected", "selectFrameworks": "Select Frameworks", "selectModelType": "Select model type", "selectProjects": "Select Projects", "sortAZ": "A-Z", "sortBy": "Sort by", + "sortingType": "Sorting Type", "sortPopular": "Popular", "sortRecent": "Recent", "sortZA": "Z-A", - "sortingType": "Sorting Type", "tags": "Tags", "tagsHelp": "Separate tags with commas", "tagsPlaceholder": "e.g., models, checkpoint", "tryAdjustingFilters": "Try adjusting your search or filters", "unknown": "Unknown", + "unsupportedUrlSource": "Only URLs from {sources} are supported", "upgradeFeatureDescription": "This feature is only available with Creator or Pro plans.", "upgradeToUnlockFeature": "Upgrade to unlock this feature", "upload": "Import", @@ -2289,10 +2303,15 @@ "uploadingModel": "Importing model...", "uploadModel": "Import", "uploadModelDescription1": "Paste a Civitai model download link to add it to your library.", - "uploadModelDescription2": "Only links from https://civitai.com/models are supported at the moment", - "uploadModelDescription3": "Max file size: 1 GB", + "uploadModelDescription1Generic": "Paste a model download link to add it to your library.", + "uploadModelDescription2": "Only links from {link} are supported at the moment", + "uploadModelDescription2Link": "https://civitai.com/models", + "uploadModelDescription2Generic": "Only URLs from the following providers are supported:", + "uploadModelDescription3": "Max file size: {size}", "uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.", "uploadModelFromCivitai": "Import a model from Civitai", + "uploadModelGeneric": "Import a model", + "uploadModelHelpFooterText": "Need help finding the URLs? Click on a provider below to see a how-to video.", "uploadModelHelpVideo": "Upload Model Help Video", "uploadModelHowDoIFindThis": "How do I find this?", "uploadSuccess": "Model imported successfully!", @@ -2428,4 +2447,4 @@ "recentReleases": "Recent releases", "helpCenterMenu": "Help Center Menu" } -} +} \ No newline at end of file diff --git a/src/locales/en/nodeDefs.json b/src/locales/en/nodeDefs.json index 5d132a2cb..9de05e37a 100644 --- a/src/locales/en/nodeDefs.json +++ b/src/locales/en/nodeDefs.json @@ -2359,6 +2359,28 @@ } } }, + "EmptyQwenImageLayeredLatentImage": { + "display_name": "Empty Qwen Image Layered Latent", + "inputs": { + "width": { + "name": "width" + }, + "height": { + "name": "height" + }, + "layers": { + "name": "layers" + }, + "batch_size": { + "name": "batch_size" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, "EmptySD3LatentImage": { "display_name": "EmptySD3LatentImage", "inputs": { @@ -3161,13 +3183,16 @@ }, "outputs": { "0": { - "name": "width" + "name": "width", + "tooltip": null }, "1": { - "name": "height" + "name": "height", + "tooltip": null }, "2": { - "name": "batch_size" + "name": "batch_size", + "tooltip": null } } }, @@ -3703,6 +3728,11 @@ "control_after_generate": { "name": "control after generate" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageBatch": { @@ -3819,6 +3849,11 @@ "y": { "name": "y" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageDeduplication": { @@ -3849,6 +3884,11 @@ "flip_method": { "name": "flip_method" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageFromBatch": { @@ -3863,6 +3903,11 @@ "length": { "name": "length" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageGrid": { @@ -4002,6 +4047,11 @@ "rotation": { "name": "rotation" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageScale": { @@ -4050,6 +4100,11 @@ "largest_size": { "name": "largest_size" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageScaleToTotalPixels": { @@ -4098,7 +4153,7 @@ }, "ImageStitch": { "display_name": "Image Stitch", - "description": "\nStitches image2 to image1 in the specified direction.\nIf image2 is not provided, returns image1 unchanged.\nOptional spacing can be added between images.\n", + "description": "Stitches image2 to image1 in the specified direction.\nIf image2 is not provided, returns image1 unchanged.\nOptional spacing can be added between images.", "inputs": { "image1": { "name": "image1" @@ -4118,6 +4173,11 @@ "image2": { "name": "image2" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ImageToMask": { @@ -4665,6 +4725,36 @@ } } }, + "KlingMotionControl": { + "display_name": "Kling Motion Control", + "inputs": { + "prompt": { + "name": "prompt" + }, + "reference_image": { + "name": "reference_image" + }, + "reference_video": { + "name": "reference_video", + "tooltip": "Motion reference video used to drive movement/expression.\nDuration limits depend on character_orientation:\n - image: 3–10s (max 10s)\n - video: 3–30s (max 30s)" + }, + "keep_original_sound": { + "name": "keep_original_sound" + }, + "character_orientation": { + "name": "character_orientation", + "tooltip": "Controls where the character's facing/orientation comes from.\nvideo: movements, expressions, camera moves, and orientation follow the motion reference video (other details via prompt).\nimage: movements and expressions still follow the motion reference video, but the character orientation matches the reference image (camera/other details via prompt)." + }, + "mode": { + "name": "mode" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, "KlingOmniProEditVideoNode": { "display_name": "Kling Omni Edit Video (Pro)", "description": "Edit an existing video with the latest model from Kling.", @@ -6413,6 +6503,19 @@ } } }, + "ManualSigmas": { + "display_name": "ManualSigmas", + "inputs": { + "sigmas": { + "name": "sigmas" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, "MaskComposite": { "display_name": "MaskComposite", "inputs": { @@ -10430,6 +10533,11 @@ "amount": { "name": "amount" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "RepeatLatentBatch": { @@ -10517,6 +10625,11 @@ "interpolation": { "name": "interpolation" } + }, + "outputs": { + "0": { + "tooltip": null + } } }, "ResizeImagesByLongerEdge": { @@ -13816,7 +13929,7 @@ }, "enhance_prompt": { "name": "enhance_prompt", - "tooltip": "Whether to enhance the prompt with AI assistance" + "tooltip": "This parameter is deprecated and ignored." }, "person_generation": { "name": "person_generation", diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 0a3b21623..77044d99f 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -460,4 +460,4 @@ "pysssss_SnapToGrid": { "name": "Always snap to grid" } -} +} \ No newline at end of file diff --git a/src/locales/zh/commands.json b/src/locales/zh/commands.json index ff4773565..6255e1aef 100644 --- a/src/locales/zh/commands.json +++ b/src/locales/zh/commands.json @@ -1,4 +1,40 @@ { + "Comfy-Desktop_CheckForUpdates": { + "label": "检查更新" + }, + "Comfy-Desktop_Folders_OpenCustomNodesFolder": { + "label": "打开自定义节点文件夹" + }, + "Comfy-Desktop_Folders_OpenInputsFolder": { + "label": "打开输入文件夹" + }, + "Comfy-Desktop_Folders_OpenLogsFolder": { + "label": "打开日志文件夹" + }, + "Comfy-Desktop_Folders_OpenModelConfig": { + "label": "打开 extra_model_paths.yaml" + }, + "Comfy-Desktop_Folders_OpenModelsFolder": { + "label": "打开模型文件夹" + }, + "Comfy-Desktop_Folders_OpenOutputsFolder": { + "label": "打开输出文件夹" + }, + "Comfy-Desktop_OpenDevTools": { + "label": "打开开发者工具" + }, + "Comfy-Desktop_OpenUserGuide": { + "label": "桌面端用户手册" + }, + "Comfy-Desktop_Quit": { + "label": "退出" + }, + "Comfy-Desktop_Reinstall": { + "label": "重装" + }, + "Comfy-Desktop_Restart": { + "label": "重启" + }, "Comfy_3DViewer_Open3DViewer": { "label": "为所选节点开启 3D 浏览器(Beta 版)" }, @@ -221,6 +257,9 @@ "Comfy_ToggleHelpCenter": { "label": "说明中心" }, + "Comfy_ToggleLinear": { + "label": "切换线性模式" + }, "Comfy_ToggleTheme": { "label": "切换主题" }, diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index c736137ca..37d83187f 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -1,6 +1,8 @@ { "actionbar": { - "dockToTop": "停靠到顶部" + "dockToTop": "停靠到顶部", + "feedback": "反馈", + "feedbackTooltip": "反馈" }, "apiNodesCostBreakdown": { "costPerRun": "每次运行的成本", @@ -16,24 +18,105 @@ "allModels": "全部模型", "ariaLabel": { "assetCard": "{name} - {type}资产", - "loadingAsset": "正在加载资源" + "loadingAsset": "正在加载资产" }, - "assets": "资源", + "assetCollection": "资产合集", + "assets": "资产", "baseModels": "基础模型", - "browseAssets": "浏览资源", + "browseAssets": "浏览资产", + "checkpoints": "模型", + "civitaiLinkExample": "案例: https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor", + "civitaiLinkExampleStrong": "案例:", + "civitaiLinkExampleUrl": "https://civitai.com/models/10706/luisap-z-image-and-qwen-pixel-art-refiner?modelVersionId=2225295", + "civitaiLinkLabel": "Civitai 模型 download link", + "civitaiLinkLabelDownload": "下载", + "civitaiLinkPlaceholder": "粘贴链接到此", + "confirmModelDetails": "确认模型信息", "connectionError": "请检查您的网络连接后重试", + "deletion": { + "body": "从资产库永久移除这个模型。", + "complete": "{assetName} 已经删除。", + "failed": "{assetName} 无法删除。", + "header": "删除该模型?", + "inProgress": "正在删除 {assetName}..." + }, + "errorFileTooLarge": "允许执行文件的文件大小限制", + "errorFormatNotAllowed": "仅允许 SafeTensor 格式", + "errorModelTypeNotSupported": "不支持该类型的模型", + "errorUnknown": "发生了意外错误", + "errorUnsafePickleScan": "CivitAI 在该文件中检测到不安全代码", + "errorUnsafeVirusScan": "CivitAI 在该文件中检测到木马或可疑文件", + "errorUploadFailed": "导入资产失败,请重试。", "failedToCreateNode": "创建节点失败。请重试或查看控制台获取详细信息。", "fileFormats": "文件格式", + "fileName": "文件名", + "fileSize": "文件大小", + "filterBy": "筛选方式", + "findInLibrary": "模型在模型库的{type}区里", + "finish": "完成", + "genericLinkPlaceholder": "粘贴链接到这", + "jobId": "任务ID", "loadingModels": "正在加载{type}...", - "noAssetsFound": "未找到资源", + "maxFileSize": "最大文件大小:{size}", + "maxFileSizeValue": "1 GB", + "media": { + "audioPlaceholder": "音频", + "threeDModelPlaceholder": "3D 模型" + }, + "modelAssociatedWithLink": "您提供的链接的模型:", + "modelName": "模型名", + "modelNamePlaceholder": "输入该模型的名称", + "modelTypeSelectorLabel": "这是什么类型的模型?", + "modelTypeSelectorPlaceholder": "选择模型类型", + "modelUploaded": "模型导入成功!🎉", + "noAssetsFound": "未找到资产", "noModelsInFolder": "此文件夹中没有可用的{type}", + "noValidSourceDetected": "检测不到有效的导入源", + "notSureLeaveAsIs": "不确定?那就放着不管吧", + "onlyCivitaiUrlsSupported": "仅支持 Civitai 链接", + "ownership": "所属", + "ownershipAll": "全部", + "ownershipMyModels": "我的模型", + "ownershipPublicModels": "公共模型", + "providerCivitai": "Civitai", + "providerHuggingFace": "Hugging Face", + "rename": { + "failed": "无法重命名资产。" + }, + "selectFrameworks": "选择框架", + "selectModelType": "选择模型类型", + "selectProjects": "选择项目", "sortAZ": "A-Z", "sortBy": "排序方式", "sortPopular": "最受欢迎", "sortRecent": "最近", "sortZA": "Z-A", + "sortingType": "排序类型", + "tags": "标签", + "tagsHelp": "按逗号区分标签", + "tagsPlaceholder": "例如:模型, checkpoint", "tryAdjustingFilters": "请尝试调整搜索或筛选条件", - "unknown": "未知" + "unknown": "未知", + "unsupportedUrlSource": "仅支持来自 {sources} URL 的模型", + "upgradeFeatureDescription": "该功能仅 Creator 和 Pro 订阅计划可用", + "upgradeToUnlockFeature": "升级订阅解锁该功能", + "upload": "导入", + "uploadFailed": "导入失败", + "uploadModel": "导入模型", + "uploadModelDescription1": "粘贴 Civitai 模型下载链接,将其添加到库中。", + "uploadModelDescription1Generic": "站贴模型下载链接,将其添加到模型库中。", + "uploadModelDescription2": "目前仅支持 https://civitai.com 链接。", + "uploadModelDescription2Generic": "仅支持来自以下提供服务的 URL :", + "uploadModelDescription2Link": "https://civitai.com/models", + "uploadModelDescription3": "最大文件大小:1 GB", + "uploadModelFailedToRetrieveMetadata": "检索元数据失败。请检查连接并重试。", + "uploadModelFromCivitai": "从 Civitai 导入模型", + "uploadModelGeneric": "导入模型", + "uploadModelHelpFooterText": "需要查找 URL 的帮助吗?点击下面的教程视频。", + "uploadModelHelpVideo": "上传模型视频", + "uploadModelHowDoIFindThis": "如何找到这个?", + "uploadSuccess": "模型导入成功!", + "uploadingModel": "正在导入模型..." }, "auth": { "apiKey": { @@ -152,7 +235,8 @@ "deleteBlueprint": "删除蓝图", "deleteWorkflow": "删除工作流", "duplicate": "复制", - "enterNewName": "输入新名称" + "enterNewName": "输入新名称", + "missingNodesWarning": "工作流包含不支持的节点(红色突出显示)。" }, "clipboard": { "errorMessage": "复制到剪贴板失败", @@ -236,7 +320,7 @@ "software": "软件与技术" }, "making": { - "3d": "3D 资源", + "3d": "3D 资产", "audio": "音频 / 音乐", "custom_nodes": "自定义节点和工作流", "images": "图片", @@ -319,60 +403,62 @@ "Add Group": "添加组", "Add Group For Selected Nodes": "为选定节点添加组", "Add Node": "添加节点", - "Add Subgraph to Library": "Add Subgraph to Library", - "Adjust Size": "Adjust Size", - "Align Selected To": "Align Selected To", - "Bottom": "Bottom", + "Add Subgraph to Library": "添加子工作流到节点库", + "Adjust Size": "调整尺寸", + "Align Selected To": "对齐选中项到", + "Bottom": "底部", "Bypass": "绕过", "Clone": "克隆", "Collapse": "折叠", - "Color": "Color", + "Color": "颜色", "Colors": "颜色", "Convert to Group Node": "转换为组节点", - "Convert to Subgraph": "Convert to Subgraph", - "Copy": "Copy", + "Convert to Subgraph": "转换为子工作流", + "Copy": "复制", "Copy (Clipspace)": "复制 (Clipspace)", - "Copy Image": "Copy Image", - "Delete": "Delete", - "Distribute Nodes": "Distribute Nodes", - "Duplicate": "Duplicate", - "Edit Subgraph Widgets": "Edit Subgraph Widgets", + "Copy Image": "复制图像", + "Delete": "删除", + "Distribute Nodes": "分布节点", + "Duplicate": "复制", + "Edit Subgraph Widgets": "编辑子工作流组件", "Expand": "展开", - "Expand Node": "Expand Node", - "Horizontal": "Horizontal", + "Expand Node": "展开节点", + "Extensions": "扩展", + "Horizontal": "水平", "Inputs": "输入", - "Left": "Left", + "Left": "左侧", "Manage": "管理", "Manage Group Nodes": "管理组节点", - "Minimize Node": "Minimize Node", + "Minimize Node": "最小化节点", "Mode": "模式", - "Node Info": "Node Info", + "Node Info": "节点信息", "Node Templates": "节点模板", - "Open Image": "Open Image", - "Open in Mask Editor": "Open in Mask Editor", + "Open Image": "打开图像", + "Open in Mask Editor": "用遮罩编辑器打开", "Outputs": "输出", - "Paste": "Paste", + "Paste": "站贴", "Pin": "固定", "Properties": "属性", "Properties Panel": "属性面板", "Remove": "删除", - "Remove Bypass": "Remove Bypass", - "Rename": "Rename", + "Remove Bypass": "删除所有忽略节点", + "Rename": "重命名", + "RenameWidget": "重命名组件", "Resize": "调整大小", - "Right": "Right", - "Run Branch": "Run Branch", - "Save Image": "Save Image", + "Right": "右侧", + "Run Branch": "运行支流节点", + "Save Image": "保存图像", "Save Selected as Template": "将选定节点另存为模板", "Search": "搜索", - "Shape": "Shape", + "Shape": "形状", "Shapes": "形状", "Title": "标题", - "Top": "Top", - "Unpack Subgraph": "Unpack Subgraph", + "Top": "顶部", + "Unpack Subgraph": "解包子工作流", "Unpin": "取消固定", - "Vertical": "Vertical", - "deprecated": "deprecated", - "new": "new" + "Vertical": "竖直", + "deprecated": "弃用", + "new": "新" }, "credits": { "accountInitialized": "账户已初始化", @@ -381,6 +467,7 @@ "additionalInfo": "附加信息", "apiPricing": "API 价格", "credits": "积分", + "creditsAvailable": "积分可用", "details": "详情", "eventType": "事件类型", "faqs": "常见问题", @@ -389,15 +476,31 @@ "messageSupport": "联系客服", "model": "模型", "purchaseCredits": "购买积分", + "refreshes": "于 {date} 刷新", "time": "时间", "topUp": { + "addMoreCredits": "获取更多积分", + "addMoreCreditsToRun": "获取更多积分来运行", + "buy": "购买", "buyNow": "立即购买", + "creditsDescription": "该工作流需要积分运行。", + "howManyCredits": "您想要多少积分?", "insufficientMessage": "您的积分不足,无法运行此工作流。", "insufficientTitle": "积分不足", + "insufficientWorkflowMessage": "您的积分不足以运行该工作流。", "maxAmount": "(最高 $1,000 美元)", + "purchaseError": "购买失败", + "purchaseErrorDetail": "购买积分失败:{error}", "quickPurchase": "快速购买", "seeDetails": "查看详情", - "topUp": "充值" + "templateNote": "*使用 Wan Fun Control 模板生成", + "topUp": "充值", + "unknownError": "发生未知错误", + "videosEstimate": "~{count} 个视频*" + }, + "unified": { + "message": "积分已合并", + "tooltip": "我们统一了 Comfy 的支付方式。现在全部使用 Comfy 积分 :\n- 合作节点(API节点)\n- Cloud 工作流\n\n您现有的合作伙伴节点积分将自动转换为 Comfy 积分。" }, "yourCreditBalance": "您的积分余额" }, @@ -426,6 +529,7 @@ "INT": "整数", "LATENT": "Latent", "LATENT_OPERATION": "Latent操作", + "LATENT_UPSCALE_MODEL": "LATENT_UPSCALE_MODEL", "LOAD3D_CAMERA": "加载3D相机", "LOAD_3D": "加载3D", "LORA_MODEL": "LORA模型", @@ -435,11 +539,11 @@ "MASK": "遮罩", "MESH": "网格", "MODEL": "模型", - "MODEL_PATCH": "MODEL_PATCH", + "MODEL_PATCH": "模型补丁", "MODEL_TASK_ID": "模型任务ID", "NOISE": "噪波", - "OPENAI_CHAT_CONFIG": "OPENAI_CHAT_CONFIG", - "OPENAI_INPUT_FILES": "OPENAI输入文件", + "OPENAI_CHAT_CONFIG": "OpenAI对话文件", + "OPENAI_INPUT_FILES": "OpenAI输入文件", "PHOTOMAKER": "PhotoMaker", "PIXVERSE_TEMPLATE": "Pixverse 模板", "RECRAFT_COLOR": "Recraft 颜色", @@ -453,6 +557,7 @@ "STYLE_MODEL": "风格模型", "SVG": "SVG", "TIMESTEPS_RANGE": "时间间隔范围", + "TRACKS": "TRACKS", "UPSCALE_MODEL": "放大模型", "VAE": "VAE", "VIDEO": "视频", @@ -525,10 +630,12 @@ "audioProgress": "音频进度", "author": "作者", "back": "返回", + "batchRename": "批量重命名", "beta": "测试版", "bookmark": "保存到库", "calculatingDimensions": "正在计算尺寸", "cancel": "取消", + "cancelled": "已取消", "capture": "捕获", "category": "类别", "chart": "图表", @@ -557,7 +664,9 @@ "copyJobId": "复制队列 ID", "copyToClipboard": "复制到剪贴板", "copyURL": "复制链接", + "core": "核心", "currentUser": "当前用户", + "custom": "自定义", "customBackground": "自定义背景", "customize": "自定义", "customizeFolder": "自定义文件夹", @@ -569,6 +678,8 @@ "description": "描述", "devices": "设备", "disableAll": "禁用全部", + "disableSelected": "禁用选中项", + "disableThirdParty": "禁用第三方", "disabling": "禁用中", "dismiss": "关闭", "download": "下载", @@ -579,17 +690,22 @@ "edit": "编辑", "editImage": "编辑图片", "editOrMaskImage": "编辑或遮罩图片", + "emDash": "-", "empty": "空", "enableAll": "启用全部", "enableOrDisablePack": "启用或禁用包", + "enableSelected": "启用选中项", "enabled": "已启用", "enabling": "启用中", + "enterBaseName": "输入基础名称", + "enterNewName": "输入新名称", "error": "错误", "errorLoadingImage": "图片加载出错", "errorLoadingVideo": "视频加载出错", "experimental": "测试版", "export": "导出", "extensionName": "扩展名称", + "failed": "失败", "failedToCopyJobId": "未能复制队列 ID", "failedToDownloadImage": "图片下载失败", "failedToDownloadVideo": "视频下载失败", @@ -618,12 +734,14 @@ "installing": "正在安装", "interrupted": "已中断", "itemSelected": "已选择 {selectedCount} 项", + "itemsCopiedToClipboard": "已复制到剪贴板", "itemsSelected": "已选择 {selectedCount} 项", + "job": "任务", "jobIdCopied": "队列 ID 已复制到剪贴板", "keybinding": "按键绑定", "keybindingAlreadyExists": "快捷键已存在", "learnMore": "了解更多", - "listening": "正在聆听...", + "listening": "正在监听...", "liveSamplingPreview": "实时采样预览", "loadAllFolders": "加载所有文件夹", "loadWorkflow": "加载工作流", @@ -636,6 +754,7 @@ "micPermissionDenied": "麦克风权限被拒绝", "migrate": "迁移", "missing": "缺失", + "more": "更多", "moreOptions": "更多选项", "moreWorkflows": "更多工作流", "multiSelectDropdown": "多选下拉框", @@ -644,6 +763,8 @@ "next": "下一个", "no": "否", "noAudioRecorded": "未录制音频", + "noItems": "无项目", + "noResults": "无结果", "noResultsFound": "未找到结果", "noTasksFound": "未找到任务", "noTasksFoundMessage": "队列中没有任务。", @@ -656,21 +777,38 @@ "nodes": "节点", "nodesRunning": "节点正在运行", "none": "无", + "nothingToCopy": "没有可以复制的内容", + "nothingToDelete": "没有可以删除的内容", + "nothingToDuplicate": "Nothing to duplicate", + "nothingToRename": "没有可以重命名的内容", "ok": "确定", "openManager": "打开管理器", "openNewIssue": "打开新问题", + "or": "或", "overwrite": "覆盖", + "playPause": "开启/暂停", "playRecording": "播放录音", "playbackSpeed": "播放速度", "playing": "播放中", "pressKeysForNewBinding": "按下按键设置新绑定", "preview": "预览", + "profile": "档案", "progressCountOf": "共", + "queued": "已执行", "ready": "就绪", "reconnected": "已重新连接", "reconnecting": "重新连接中", "refresh": "刷新", "refreshNode": "刷新节点", + "relativeTime": { + "daysAgo": "{count}天 前", + "hoursAgo": "{count}时 前", + "minutesAgo": "{count}分 前", + "monthsAgo": "{count}月 前", + "now": "刚才", + "weeksAgo": "{count}周 前", + "yearsAgo": "{count}年 前" + }, "releaseTitle": "{package} {version} 发布", "reloadToApplyChanges": "重新加载以应用更改", "removeImage": "移除图片", @@ -688,6 +826,7 @@ "resizeFromTopRight": "从右上角调整大小", "restart": "重新启动", "resultsCount": "找到 {count} 个结果", + "running": "正在运行", "save": "保存", "saving": "正在保存", "search": "搜索", @@ -696,9 +835,14 @@ "searchKeybindings": "搜索快捷键", "searchModels": "搜索模型", "searchNodes": "搜索节点", + "searchPlaceholder": "搜索占位符", "searchSettings": "搜索设置", "searchWorkflows": "搜索工作流", "seeTutorial": "查看教程", + "selectItemsToCopy": "选择复制", + "selectItemsToDelete": "选择删除", + "selectItemsToDuplicate": "选择复制", + "selectItemsToRename": "选择重命名", "selectedFile": "已选文件", "setAsBackground": "设为背景", "settings": "设置", @@ -710,12 +854,14 @@ "status": "状态", "stopPlayback": "停止播放", "stopRecording": "停止录音", + "submit": "提交", "success": "成功", "systemInfo": "系统信息", "terminal": "终端", "title": "标题", "triggerPhrase": "触发短语", "unknownError": "未知错误", + "untitled": "无标题", "update": "更新", "updateAvailable": "有更新可用", "updateFrontend": "更新前端", @@ -756,11 +902,17 @@ "create": "创建组节点", "enterName": "输入名称" }, + "help": { + "helpCenterMenu": "帮助中心菜单", + "recentReleases": "最近发布" + }, "helpCenter": { "clickToLearnMore": "点击了解更多 →", "desktopUserGuide": "桌面端用户指南", "docs": "文档", + "feedback": "提交反馈", "github": "Github", + "help": "帮助和支持", "loadingReleases": "加载发布信息...", "managerExtension": "管理扩展", "more": "更多...", @@ -769,6 +921,12 @@ "recentReleases": "最近发布", "reinstall": "重新安装", "updateAvailable": "更新", + "updateComfyUI": "更新 ComfyUI", + "updateComfyUIFailed": "更新 ComfyUI 失败,请重试。", + "updateComfyUIStarted": "更新开始", + "updateComfyUIStartedDetail": "正在更新 ComfyUI,请稍等...", + "updateComfyUISuccess": "更新完成", + "updateComfyUISuccessDetail": "ComfyUI 已更新,正在重启...", "whatsNew": "新功能?" }, "icon": { @@ -821,6 +979,8 @@ "selectGpuDescription": "选择你拥有的 GPU 类型" }, "helpImprove": "请帮助我们改进ComfyUI", + "insideAppInstallDir": "该文件夹位于 ComfyUI Desktop 应用程序包中,程序更新时会删除该文件夹。选择安装文件夹之外的目录,例如 Documents/ComfyUI。", + "insideUpdaterCache": "该文件夹位于 ComfyUI 更新缓存中,程序更新时会清除该缓存。为您的数据选择一个不同的位置。", "installLocation": "安装位置", "installLocationDescription": "选择 ComfyUI 用户数据的存放目录。将安装一个 Python 环境到所选位置。请确保所选磁盘有足够的空间(约 15GB)。", "installLocationTooltip": "ComfyUI 的用户数据目录。存储:\n- Python 环境\n- 模型\n- 自定义节点\n", @@ -895,6 +1055,10 @@ "issueReport": { "helpFix": "帮助修复这个" }, + "linearMode": { + "openWorkflow": "打开工作流", + "share": "分享" + }, "load3d": { "applyingTexture": "应用纹理中...", "backgroundColor": "背景颜色", @@ -921,10 +1085,12 @@ "lineart": "线稿", "normal": "法线", "original": "原始", + "pointCloud": "点云", "wireframe": "线框" }, "model": "模型", "openIn3DViewer": "在 3D 查看器中打开", + "panoramaMode": "全景", "previewOutput": "预览输出", "reloadingModel": "正在重新加载模型...", "removeBackgroundImage": "移除背景图片", @@ -935,6 +1101,7 @@ "stopRecording": "停止录制", "switchCamera": "切换摄影机类型", "switchingMaterialMode": "切换材质模式中...", + "tiledMode": "分快", "unsupportedFileType": "不支持的文件类型(支持 .gltf、.glb、.obj、.fbx、.stl)", "upDirection": "上方向", "upDirections": { @@ -955,6 +1122,11 @@ "title": "3D 查看器(测试版)" } }, + "loadWorkflowWarning": { + "coreNodesFromVersion": "核心节点来源于 {version} 版本。", + "outdatedVersion": "这个工作流由新版 ComfyUI({version})创建,部分节点可能无法正常运行。", + "outdatedVersionGeneric": "这个工作流由新版 ComfyUI 创建,部分节点可能无法正常运行。" + }, "maintenance": { "None": "无", "OK": "确定", @@ -973,7 +1145,15 @@ "showManual": "显示维护任务", "status": "状态", "terminalDefaultMessage": "当你运行一个故障排除命令时,任何输出都会在这里显示。", - "title": "维护" + "title": "维护", + "unsafeMigration": { + "action": "使用下方的 \"基本路径\" 维护将路径移动到安全位置。", + "appInstallDir": "您当前的 ComfyUI 基本路径位于 ComfyUI Desktop 应用程序包中,程序更新时会删除该文件夹。选择安装文件夹之外的目录,例如 Documents/ComfyUI。", + "generic": "您当前的 ComfyUI 基本路径所在的位置可能会在更新期间被删除或修改。为避免数据丢失,请将其移至安全文件夹。", + "oneDrive": "您当前的 ComfyUI 基本路径在 OneDrive上,这可能会导致同步问题和意外的数据丢失。选择一个不受 OneDrive 管理的本地文件夹。", + "title": "检测到安装路径不安全", + "updaterCache": "您当前的 ComfyUI 基本路径位于 ComfyUI 更新缓存中,程序更新时会清除该缓存。为您的数据选择一个不同的位置。" + } }, "manager": { "allMissingNodesInstalled": "所有缺失节点已成功安装", @@ -1074,6 +1254,8 @@ "totalNodes": "节点总数", "tryAgainLater": "请稍后再试。", "tryDifferentSearch": "请尝试不同的搜索查询。", + "tryUpdate": "尝试更新", + "tryUpdateTooltip": "从库中拉取更新。测试版本可能有无法自动检测到更新。", "uninstall": "卸载", "uninstallSelected": "卸载所选", "uninstalling": "正在卸载", @@ -1084,31 +1266,89 @@ "version": "版本" }, "maskEditor": { + "activateLayer": "活跃层", + "applyToWholeImage": "应用到图像整体", + "baseImageLayer": "基础图像层", + "baseLayerPreview": "基础图像层预览", + "black": "黑", + "brushSettings": "笔刷设置", + "brushShape": "笔刷形状", + "clear": "清除", + "clickToResetZoom": "点击重置缩放", + "colorSelectSettings": "色彩选取设置", + "colorSelector": "色彩选取", + "fillOpacity": "填充不透明度", + "hardness": "硬度", + "imageLayer": "图像层", + "invert": "反转", + "layers": "图层", + "livePreview": "实时预览", + "maskBlendingOptions": "遮罩混合设置", + "maskLayer": "遮罩层", + "maskOpacity": "遮罩不透明度", + "maskTolerance": "遮罩阈值", + "method": "方法", + "negative": "负面", + "opacity": "不透明度", + "paintBucketSettings": "填充设置", + "paintLayer": "绘画层", + "redo": "重做", + "resetToDefault": "重置为默认", + "selectionOpacity": "选取不透明度", + "smoothingPrecision": "预测平滑", + "stepSize": "间距", + "stopAtMask": "遇到遮罩时停止", + "thickness": "厚度", + "title": "遮罩编辑器", + "tolerance": "阈值", + "undo": "撤回", + "white": "白" }, "mediaAsset": { + "actions": { + "addToWorkflow": "添加到当前工作流", + "copyJobId": "复制任务ID", + "delete": "删除", + "download": "下载", + "exportWorkflow": "导出工作流", + "inspect": "查看资产", + "more": "更多设置", + "openWorkflow": "在新标签页中读取工作流", + "seeMoreOutputs": "查看更多输出" + }, "assetDeletedSuccessfully": "资产删除成功", "deleteAssetDescription": "此资产将被永久删除。", "deleteAssetTitle": "删除此资产?", "deleteSelectedDescription": "{count} 项资产将被永久删除。", "deleteSelectedTitle": "删除所选资产?", "deletingImportedFilesCloudOnly": "删除导入文件仅支持云版本", + "failedToCreateNode": "创建节点失败", "failedToDeleteAsset": "删除资产失败", + "failedToExportWorkflow": "工作流导出失败", "jobIdToast": { "copied": "已复制", "error": "错误", "jobIdCopied": "任务 ID 已复制到剪贴板", "jobIdCopyFailed": "复制队列 ID 失败" }, + "noJobIdFound": "该资产不包含任务ID", + "noWorkflowDataFound": "该资产不包含工作流数据", + "nodeAddedToWorkflow": "{nodeType} 节点已添加到工作流", + "nodeTypeNotFound": "节点类型 {nodeType} 不存在", "selection": { - "assetsDeletedSuccessfully": "已成功删除 {count} 个资源", + "assetsDeletedSuccessfully": "已成功删除 {count} 个资产", "deleteSelected": "删除", "deselectAll": "取消全选", "downloadSelected": "下载", "downloadStarted": "正在下载 {count} 个文件...", "downloadsStarted": "开始下载 {count} 个文件", "failedToDeleteAssets": "未能删除所选资产", + "partialDeleteSuccess": "{succeeded} 删除成功, {failed} 失败", "selectedCount": "已选择资产:{count}" - } + }, + "unsupportedFileType": "加载节点不支持该类型文件", + "workflowExportedSuccessfully": "工作流导出成功!", + "workflowOpenedInNewTab": "工作流已在新标签页中打开" }, "menu": { "autoQueue": "自动执行", @@ -1116,6 +1356,7 @@ "batchCountTooltip": "工作流生成次数", "clear": "清空工作流", "clipspace": "打开剪贴板", + "customNodesManager": "自定义节点管理器", "dark": "深色", "disabled": "禁用", "disabledTooltip": "工作流将不会自动执行", @@ -1134,6 +1375,7 @@ "resetView": "重置视图", "run": "运行", "runWorkflow": "运行工作流(Shift排在前面)", + "runWorkflowDisabled": "工作流包含不支持的节点(已用红色突出显示)。删除这些节点以运行工作流。", "runWorkflowFront": "运行工作流(排在前面)", "settings": "设定", "showMenu": "显示菜单", @@ -1149,6 +1391,7 @@ "Canvas Performance": "画布性能", "Canvas Toggle Lock": "切换视图锁定", "Check for Custom Node Updates": "检查自定义节点更新", + "Check for Updates": "检查更新", "Clear Pending Tasks": "清除待处理任务", "Clear Workflow": "清除工作流", "Clipspace": "剪贴空间", @@ -1159,18 +1402,20 @@ "ComfyUI Forum": "ComfyUI 论坛", "ComfyUI Issues": "ComfyUI 问题", "Contact Support": "联系支持", - "Convert Selection to Subgraph": "将选中内容转换为子图", + "Convert Selection to Subgraph": "将选中内容转换为子工作流", "Convert selected nodes to group node": "将选中节点转换为组节点", "Custom Nodes (Legacy)": "自定义节点(旧版)", "Custom Nodes Manager": "自定义节点管理器", "Decrease Brush Size in MaskEditor": "在 MaskEditor 中减小笔刷大小", "Delete Selected Items": "删除选定的项目", + "Desktop User Guide": "桌面端用户手册", "Duplicate Current Workflow": "复制当前工作流", "Edit": "编辑", - "Edit Subgraph Widgets": "编辑子图组件", - "Exit Subgraph": "退出子图", - "Experimental: Browse Model Assets": "实验性:浏览模型资源", + "Edit Subgraph Widgets": "编辑子工作流组件", + "Exit Subgraph": "退出子工作流", + "Experimental: Browse Model Assets": "实验性:浏览模型资产", "Experimental: Enable AssetAPI": "实验性:启用 AssetAPI", + "Experimental: Enable Nodes 2_0": "实验性:启用 Nodes 2.0", "Export": "导出", "Export (API)": "导出 (API)", "File": "文件", @@ -1200,8 +1445,15 @@ "Node Links": "节点连接", "Open": "打开", "Open 3D Viewer (Beta) for Selected Node": "为选中节点打开3D查看器(测试版)", + "Open Custom Nodes Folder": "打开自定义节点文件夹", + "Open DevTools": "打开开发者工具", + "Open Inputs Folder": "打开输入文件夹", + "Open Logs Folder": "打开日志文件夹", "Open Mask Editor for Selected Node": "为选中节点打开 Mask 编辑器", + "Open Models Folder": "打开模型文件夹", + "Open Outputs Folder": "打开输出文件夹", "Open Sign In Dialog": "打开登录对话框", + "Open extra_model_paths_yaml": "打开 extra_model_paths.yaml", "Pin/Unpin Selected Items": "固定/取消固定选定项目", "Pin/Unpin Selected Nodes": "固定/取消固定选定节点", "Previous Opened Workflow": "上一个打开的工作流", @@ -1209,10 +1461,13 @@ "Queue Prompt": "执行提示词", "Queue Prompt (Front)": "执行提示词 (优先执行)", "Queue Selected Output Nodes": "将所选输出节点加入队列", + "Quit": "退出", "Redo": "重做", "Refresh Node Definitions": "刷新节点定义", + "Reinstall": "重装", "Reset View": "重置视图", "Resize Selected Nodes": "调整选定节点的大小", + "Restart": "重启", "Save": "保存", "Save As": "另存为", "Show Keybindings Dialog": "显示快捷键对话框", @@ -1232,12 +1487,13 @@ "Unload Models": "卸载模型", "Unload Models and Execution Cache": "卸载模型和执行缓存", "Unlock Canvas": "解除锁定画布", - "Unpack the selected Subgraph": "解包选中子图", + "Unpack the selected Subgraph": "解包选中子工作流", "View": "视图", "Workflows": "工作流", "Zoom In": "放大画面", "Zoom Out": "缩小画面", - "Zoom to fit": "缩放以适应" + "Zoom to fit": "缩放以适应", + "toggle linear mode": "切换线性模式" }, "minimap": { "nodeColors": "节点颜色", @@ -1251,7 +1507,23 @@ "missingModels": "缺少模型", "missingModelsMessage": "加载工作流时,未找到以下模型" }, + "missingNodes": { + "cloud": { + "description": "该工作流包含 Comfy Cloud 目前不支持的节点", + "gotIt": "好的", + "learnMore": "查看更多", + "priorityMessage": "我们已经标记这些节点,将会优先支持它们。", + "replacementInstruction": "同时,如果可能的话,将这些节点(红色突出显示)替换为已经支持的节点,或者尝试不同的工作流。", + "title": "Comfy Cloud 目前不支持这些节点" + }, + "oss": { + "description": "该工作流包含您未安装的自定义节点", + "replacementInstruction": "安装这些节点后运行此工作流,或者用已安装的节点替换它们。缺失的节点在画布上以红色突出显示。", + "title": "该工作流含有缺失节点" + } + }, "nodeCategories": { + "": "", "3d": "3d", "3d_models": "3D模型", "BFL": "BFL", @@ -1266,14 +1538,15 @@ "OpenAI": "OpenAI", "PixVerse": "PixVerse", "Recraft": "Recraft", - "Rodin": "罗丹", - "Runway": "跑道", + "Rodin": "Rodin", + "Runway": "Runway", "Sora": "Sora", "Stability AI": "Stability AI", + "Topaz": "Topaz", "Tripo": "Tripo", "Veo": "Veo", "Vidu": "Vidu", - "Wan": "万相", + "Wan": "Wan万相", "_for_testing": "_用于测试", "advanced": "高级", "animation": "动画", @@ -1294,6 +1567,7 @@ "controlnet": "ControlNet", "create": "创建", "custom_sampling": "自定义采样", + "dataset": "dataset", "debug": "调试", "deprecated": "已弃用", "edit_models": "编辑模型", @@ -1305,6 +1579,7 @@ "image": "图像", "inpaint": "局部重绘", "instructpix2pix": "InstructPix2Pix", + "kandinsky5": "kandinsky5", "latent": "Latent", "loaders": "加载器", "lotus": "lotus", @@ -1320,7 +1595,7 @@ "postprocessing": "后处理", "preprocessors": "预处理器", "primitive": "基础", - "qwen": "千问", + "qwen": "Qwen千问", "samplers": "采样器", "sampling": "采样", "save": "保存", @@ -1340,7 +1615,15 @@ "upscaling": "放大", "utils": "工具", "video": "视频", - "video_models": "视频模型" + "video_models": "视频模型", + "zimage": "zimage" + }, + "nodeErrors": { + "content": "节点内容错误", + "header": "节点头错误", + "render": "节点渲染错误", + "slots": "节点接口错误", + "widgets": "节点组件错误" }, "nodeHelpPage": { "documentationPage": "文档页面", @@ -1357,6 +1640,7 @@ "notSupported": { "continue": "继续", "continueTooltip": "我确定我的设备是受支持的", + "illustrationAlt": "Sad girl illustration", "learnMore": "了解更多", "message": "仅支持以下设备:", "reportIssue": "报告问题", @@ -1366,12 +1650,81 @@ }, "title": "您的设备不受支持" }, + "queue": { + "completedIn": "{duration} 后完成", + "inQueue": "正在执行...", + "initializingAlmostReady": "初始化中 - 即将完成", + "jobAddedToQueue": "任务添加到队列", + "jobDetails": { + "computeHoursUsed": "计算耗时", + "errorMessage": "报错信息", + "estimatedFinishIn": "预计完成于", + "estimatedStartIn": "预计开始于", + "eta": { + "minutes": "~{count} 分钟 | ~{count} 分钟", + "minutesRange": "~{lo}-{hi} 分钟", + "seconds": "~{count} 秒 | ~{count} 秒", + "secondsRange": "~{lo}-{hi} 秒" + }, + "failedAfter": "失败于", + "generatedOn": "生成于", + "header": "任务细节", + "jobId": "任务ID", + "queuePosition": "队列位置", + "queuePositionValue": "在您前方有 ~{count} 个任务 | 在您前方有 ~{count} 个任务", + "queuedAt": "执行于", + "report": "反馈", + "timeElapsed": "耗时", + "totalGenerationTime": "总生成时间", + "workflow": "工作流" + }, + "jobList": { + "sortComputeHoursUsed": "计算用时(最先)", + "sortMostRecent": "最新", + "sortTotalGenerationTime": "生成时间(最慢)", + "undated": "无日期" + }, + "jobMenu": { + "addToCurrentWorkflow": "添加到当前工作流", + "cancelJob": "取消任务", + "copyErrorMessage": "复制报错信息", + "copyJobId": "复制任务ID", + "delete": "删除", + "deleteAsset": "删除资产", + "download": "下载", + "exportWorkflow": "导出工作流", + "inspectAsset": "查看资产", + "openAsWorkflowNewTab": "在新标签页中读取工作流", + "openWorkflowNewTab": "在新标签页中打开工作流", + "removeJob": "移除任务", + "reportError": "反馈报错" + } + }, "releaseToast": { + "description": "在此更新中尝试最新的改进和功能。", "newVersionAvailable": "新版本可用!", "skip": "跳过", "update": "更新", "whatsNew": "新功能?" }, + "rightSidePanel": { + "bypass": "忽略", + "color": "节点颜色", + "info": "信息", + "inputs": "输入", + "inputsNone": "无输入", + "inputsNoneTooltip": "节点没有输入", + "mute": "禁用", + "noSelection": "选择一个节点查看其属性信息。", + "nodeState": "节点状态", + "normal": "正常", + "parameters": "参数", + "pinned": "顶固", + "properties": "属性", + "settings": "设置", + "title": "无选中节点 | 选中了 1 个节点 | 选中了 {count} 个节点", + "togglePanel": "开关属性面板" + }, "selectionToolbox": { "Bypass Group Nodes": "绕过分组节点", "Set Group Nodes to Always": "将分组节点设置为始终", @@ -1384,6 +1737,8 @@ "serverConfig": { "modifiedConfigs": "您已修改以下服务器配置。重启以应用更改。", "restart": "重启", + "restartRequiredToastDetail": "重启软件应用设置。", + "restartRequiredToastSummary": "需要重启", "revertChanges": "撤销更改" }, "serverConfigCategories": { @@ -1452,6 +1807,10 @@ "enable-cors-header": { "name": "启用 CORS header:使用 \"*\" 代表所有来源或指定域名" }, + "enable-manager-legacy-ui": { + "name": "使用旧版管理器UI", + "tooltip": "是用旧版 ComfyUI-Manager UI。" + }, "fast": { "name": "启用一些未经测试且可能降低质量的优化。" }, @@ -1553,6 +1912,7 @@ "CustomColorPalettes": "自定义色彩主题", "DevMode": "开发模式", "EditTokenWeight": "编辑令牌权重", + "Execution": "执行", "Extension": "扩展", "General": "常规", "Graph": "画面", @@ -1571,7 +1931,9 @@ "Node Search Box": "节点搜索框", "Node Widget": "节点组件", "NodeLibrary": "节点库", + "Nodes 2_0": "Nodes 2.0", "Notification Preferences": "通知偏好", + "PLY": "PLY", "PlanCredits": "计划与积分", "Pointer": "指针", "Queue": "队列", @@ -1587,8 +1949,8 @@ "UV": "UV", "User": "用户", "Validation": "验证", - "Vue Nodes": "Vue 节点", - "VueNodes": "Vue 节点", + "Vue Nodes": "Nodes 2.0", + "VueNodes": "Nodes 2.0", "Window": "窗口", "Workflow": "工作流" }, @@ -1634,7 +1996,17 @@ "workflows": "工作流" }, "logout": "登出", - "mediaAssets": "媒体资源", + "mediaAssets": { + "filter3D": "3D", + "filterAudio": "音频", + "filterImage": "图像", + "filterVideo": "视频", + "sortFastestFirst": "生成时间(最快)", + "sortLongestFirst": "生成时间(最慢)", + "sortNewestFirst": "最新", + "sortOldestFirst": "最旧", + "title": "媒体资产" + }, "modelLibrary": "模型库", "newBlankWorkflow": "创建空白工作流", "noFilesFound": "未找到文件", @@ -1663,6 +2035,42 @@ }, "openWorkflow": "在本地文件系统中打开工作流", "queue": "队列", + "queueProgressOverlay": { + "activeJobsSuffix": "活跃任务", + "cancelJobTooltip": "取消任务", + "clearHistory": "清除任务记录", + "clearHistoryDialogAssetsNote": "这些任务生成的资产不会被删除,并且始终可以在资产面板中查看。", + "clearHistoryDialogDescription": "以下所有已完成或失败的任务将从队列面板中删除。", + "clearHistoryDialogTitle": "确定要清除任务记录?", + "clearQueueTooltip": "清理队列", + "clearQueued": "清除已执行", + "colonPercent": ":{percent}", + "currentNode": "当前节点:", + "expandCollapsedQueue": "展开任务队列", + "filterAllWorkflows": "全部工作流", + "filterBy": "筛选方式", + "filterCurrentWorkflow": "当前工作流", + "filterJobs": "筛选任务", + "interruptAll": "中断全部正在运行的任务", + "jobQueue": "任务队列", + "jobsCompleted": "{count} 任务完成 | {count} 任务完成", + "jobsFailed": "{count} 任务失败 | {count} 任务失败", + "moreOptions": "更多设置", + "noActiveJobs": "无活跃任务", + "preview": "预览", + "queuedSuffix": "已执行", + "running": "运行中", + "showAssets": "显示资产", + "showAssetsPanel": "显示资产面板", + "sortBy": "排序方式", + "sortJobs": "排序任务", + "stubClipTextEncode": "CLIP文本编码:", + "title": "队列进度", + "total": "全部:{percent}", + "viewAllJobs": "查看全部任务", + "viewJobHistory": "查看任务记录" + }, + "searchAssets": "搜索资产", "templates": "模板", "themeToggle": "切换主题", "workflowTab": { @@ -1685,19 +2093,19 @@ "workflows": "工作流" }, "subgraphStore": { - "blueprintName": "子图名称", - "confirmDelete": "此操作将永久从您的库中移除该子图", - "confirmDeleteTitle": "删除子图?", + "blueprintName": "子工作流名称", + "confirmDelete": "此操作将永久从您的库中移除该子工作流", + "confirmDeleteTitle": "删除子工作流?", "hidden": "隐藏/嵌套参数", "hideAll": "全部隐藏", - "loadFailure": "加载子图蓝图失败", - "overwriteBlueprint": "保存将用您的更改覆盖当前子图", - "overwriteBlueprintTitle": "覆盖现有子图?", - "promoteOutsideSubgraph": "不在子图中时无法提升小部件", - "publish": "发布子图", + "loadFailure": "加载子工作流蓝图失败", + "overwriteBlueprint": "保存将用您的更改覆盖当前子工作流", + "overwriteBlueprintTitle": "覆盖现有子工作流?", + "promoteOutsideSubgraph": "不在子工作流中时无法提升小部件", + "publish": "发布子工作流", "publishSuccess": "已保存到节点库", - "publishSuccessMessage": "您可以在节点库的“子图蓝图”下找到您的子图蓝图", - "saveBlueprint": "保存子图到节点库", + "publishSuccessMessage": "您可以在节点库的“子工作流蓝图”下找到您的子工作流蓝图", + "saveBlueprint": "保存子工作流到节点库", "showAll": "全部显示", "showRecommended": "显示推荐控件", "shown": "节点上显示" @@ -1705,24 +2113,51 @@ "subscription": { "addApiCredits": "添加API额度", "addCredits": "添加积分", + "addCreditsLabel": "随时获取更多积分", "benefits": { "benefit1": "合作伙伴节点的月度积分 — 按需充值", "benefit2": "每个队列最长运行 30 分钟" }, "beta": "测试版", + "billedMonthly": "每月付款", + "billedYearly": "{total} 每年付款", + "changeTo": "更改为 {plan}", "comfyCloud": "Comfy 云", + "comfyCloudLogo": "Comfy Cloud Logo", + "contactUs": "联系我们", + "creditsRemainingThisMonth": "本月剩余积分", + "creditsRemainingThisYear": "今年剩余积分", + "creditsYouveAdded": "你获得的积分", + "currentPlan": "当前订阅计划", + "customLoRAsLabel": "导入您的 Lora", + "description": "选择最适合您的订阅计划", "expiresDate": "于 {date} 过期", + "gpuLabel": "RTX 6000 Pro (96GB VRAM)", + "haveQuestions": "对企业级有疑问?", "invoiceHistory": "发票历史", "learnMore": "了解更多", + "managePlan": "管理订阅", "manageSubscription": "管理订阅", + "maxDuration": { + "creator": "30 分钟", + "founder": "30 分钟", + "pro": "1 小时", + "standard": "30 分钟" + }, + "maxDurationLabel": "运行单个工作流的最大时长", "messageSupport": "消息支持", + "monthly": "月度", "monthlyBonusDescription": "每月积分奖励", + "monthlyCreditsInfo": "积分每月刷新,不会保留", + "monthlyCreditsLabel": "每月积分", "monthlyCreditsRollover": "这些积分将结转到下个月", + "mostPopular": "最受欢迎", "nextBillingCycle": "下一个计费周期", "partnerNodesBalance": "\"合作伙伴节点\"积分余额", "partnerNodesCredits": "合作伙伴节点积分", "partnerNodesDescription": "用于运行商业/专有模型", "perMonth": "美元 / 月", + "plansAndPricing": "订阅和定价", "prepaidCreditsInfo": "单独购买且不会过期的积分", "prepaidDescription": "预付款额度", "renewsDate": "将于 {date} 续订", @@ -1732,14 +2167,43 @@ "waitingForSubscription": "请在新标签页中完成订阅。我们会自动检测到您已完成!" }, "subscribeNow": "立即订阅", + "subscribeTo": "订阅 {plan}", "subscribeToComfyCloud": "订阅 Comfy Cloud", "subscribeToRun": "订阅", "subscribeToRunFull": "订阅 Run", + "tierNameYearly": "{name} 年度", + "tiers": { + "creator": { + "name": "Creator" + }, + "founder": { + "name": "Founder's Edition" + }, + "pro": { + "name": "Pro" + }, + "standard": { + "name": "Standard" + } + }, "title": "订阅", "titleUnsubscribed": "订阅 Comfy Cloud", "totalCredits": "总积分", + "upgrade": "升级", + "upgradePlan": "升级订阅", + "upgradeTo": "升级到 {plan}", + "usdPerMonth": "USD / mo", + "videoEstimateExplanation": "这些预估基于 Wan Fun Control 生成 5 秒视频。", + "videoEstimateHelp": "这是什么?", + "videoEstimateLabel": "可使用 Wan Fun Control 模板生成 5 秒视频的数量", + "videoEstimateTryTemplate": "试试 Wan Fun Control template →", + "viewEnterprise": "查看企业", "viewMoreDetails": "查看更多详情", + "viewMoreDetailsPlans": "查看有关订阅和定价的更多信息", "viewUsageHistory": "查看使用历史", + "yearly": "年度", + "yearlyCreditsLabel": "总共年度积分", + "yearlyDiscount": "20% 减免", "yourPlanIncludes": "您的计划包括:" }, "tabMenu": { @@ -1751,8 +2215,14 @@ "duplicateTab": "复制标签", "removeFromBookmarks": "从书签中移除" }, + "templateWidgets": { + "sort": { + "searchPlaceholder": "搜索中..." + } + }, "templateWorkflows": { "activeFilters": "筛选条件:", + "allTemplates": "全部模板", "categories": "分类", "category": { "3D": "3D", @@ -1779,6 +2249,7 @@ "error": { "templateNotFound": "未找到模板 \"{templateName}\"" }, + "licenseFilter": "许可证", "loading": "正在加载模板...", "loadingMore": "正在加载更多模板...", "modelFilter": "模型筛选", @@ -1801,10 +2272,11 @@ }, "sorting": "排序方式", "title": "从模板开始", + "useCaseFilter": "使用例", "useCasesSelected": "已选 {count} 个用例" }, "toastMessages": { - "cannotCreateSubgraph": "无法创建子图", + "cannotCreateSubgraph": "无法创建子工作流", "couldNotDetermineFileType": "无法确定文件类型", "dropFileError": "无法处理掉落的项目:{error}", "emptyCanvas": "画布为空", @@ -1815,7 +2287,7 @@ "failedExecutionPathResolution": "无法解析所选节点的路径", "failedToAccessBillingPortal": "访问账单门户失败:{error}", "failedToApplyTexture": "应用纹理失败", - "failedToConvertToSubgraph": "无法将项目转换为子图", + "failedToConvertToSubgraph": "无法将项目转换为子工作流", "failedToCreateCustomer": "创建客户失败:{error}", "failedToDownloadFile": "文件下载失败", "failedToExportModel": "无法将模型导出为 {format}", @@ -1829,9 +2301,21 @@ "failedToLoadModel": "无法加载3D模型", "failedToPurchaseCredits": "购买积分失败:{error}", "failedToQueue": "排队失败", + "failedToToggleCamera": "切换镜头失败", + "failedToToggleGrid": "切换网格失败", + "failedToUpdateBackgroundColor": "更新背景色失败", + "failedToUpdateBackgroundImage": "更新背景图像失败", + "failedToUpdateBackgroundRenderMode": "切换背景模式到 {mode} 失败", + "failedToUpdateEdgeThreshold": "更新边缘阈值失败", + "failedToUpdateFOV": "更新FOV失败", + "failedToUpdateLightIntensity": "更新光照强度失败", + "failedToUpdateMaterialMode": "更新材质模式失败", + "failedToUpdateUpDirection": "更新向上轴失败", + "failedToUploadBackgroundImage": "上传背景图像失败", "fileLoadError": "无法在 {fileName} 中找到工作流", "fileUploadFailed": "文件上传失败", "interrupted": "执行已被中断", + "legacyMaskEditorDeprecated": "旧版遮罩编辑器已弃用,即将删除。", "migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。", "modelLoadedSuccessfully": "3D模型加载成功", "no3dScene": "没有3D场景可以应用纹理", @@ -1858,11 +2342,12 @@ "selectUser": "选择用户" }, "userSettings": { + "accountSettings": "用户设置", "email": "电子邮件", "name": "名称", "notSet": "未设置", "provider": "登录方式", - "title": "用户设置", + "title": "我的用户设置", "updatePassword": "更新密码" }, "validation": { @@ -1892,20 +2377,29 @@ "updateFrontend": "更新前端" }, "vueNodesBanner": { + "title": "介绍 Nodes 2.0", "tryItOut": "试试看" }, "vueNodesMigration": { "button": "打开设置", "message": "是否偏好经典节点设计?" }, + "vueNodesMigrationMainMenu": { + "message": "在主菜单中随时切换回 Nodes 2.0" + }, "welcome": { "getStarted": "开始使用", "title": "欢迎使用 ComfyUI" }, "whatsNewPopup": { + "later": "Later", "learnMore": "了解更多", "noReleaseNotes": "暂无更新说明。" }, + "widgetFileUpload": { + "browseFiles": "浏览文件", + "dropPrompt": "将文件拖到此处或" + }, "widgets": { "selectModel": "选择模型", "uploadSelect": { @@ -1915,6 +2409,26 @@ "placeholderModel": "请选择模型...", "placeholderUnknown": "请选择媒体...", "placeholderVideo": "请选择视频..." + }, + "valueControl": { + "decrement": "递减值", + "decrementDesc": "数值-1或切换到上一个选项", + "editSettings": "改变控制设置", + "fixed": "固定值", + "fixedDesc": "数值不变", + "header": { + "after": "之后", + "before": "之前", + "postfix": "正在运行工作流:", + "prefix": "自动更新该值" + }, + "increment": "递增值", + "incrementDesc": "数值+1或切换到下一个选项", + "linkToGlobal": "连接到", + "linkToGlobalDesc": "唯一值连接到全局值控制设置", + "linkToGlobalSeed": "全局值", + "randomize": "随机值", + "randomizeDesc": "每次运行后随机化该值" } }, "workflowService": { diff --git a/src/locales/zh/nodeDefs.json b/src/locales/zh/nodeDefs.json index a1cd59ab1..59e022856 100644 --- a/src/locales/zh/nodeDefs.json +++ b/src/locales/zh/nodeDefs.json @@ -14,7 +14,7 @@ "tooltip": "控制扩散过程中的引导运行平均值,设置为0时禁用。" }, "norm_threshold": { - "name": "norm_threshold", + "name": "向量归一化", "tooltip": "将引导向量归一化到此值,设置为 0 时禁用归一化。" } }, @@ -73,18 +73,18 @@ } }, "AudioConcat": { - "description": "将 audio1 与 audio2 按指定方向连接。", + "description": "将 音频1 与 音频2 按指定方向连接。", "display_name": "音频拼接", "inputs": { "audio1": { - "name": "audio1" + "name": "音频1" }, "audio2": { "name": "音频2" }, "direction": { "name": "方向", - "tooltip": "是否在 audio1 之后或之前追加 audio2。" + "tooltip": "音频2 在 音频1 之前或之后。" } } }, @@ -105,10 +105,10 @@ } }, "AudioEncoderLoader": { - "display_name": "音频编码器加载器", + "display_name": "加载音频编码器", "inputs": { "audio_encoder_name": { - "name": "音频编码器名称" + "name": "音频编码器" } }, "outputs": { @@ -118,11 +118,11 @@ } }, "AudioMerge": { - "description": "通过叠加两个音频轨道的波形来合并它们。", + "description": "叠加 音频1 和 音频2 轨道的波形。", "display_name": "音频合并", "inputs": { "audio1": { - "name": "audio1" + "name": "音频1" }, "audio2": { "name": "音频2" @@ -165,10 +165,10 @@ "display_name": "Beta采样调度器", "inputs": { "alpha": { - "name": "阿尔法" + "name": "alpha" }, "beta": { - "name": "贝塔" + "name": "beta" }, "model": { "name": "模型" @@ -539,7 +539,7 @@ }, "outputs": { "0": { - "name": "修补模型", + "name": "模型", "tooltip": null } } @@ -548,12 +548,12 @@ "display_name": "CFGZeroStar", "inputs": { "model": { - "name": "model" + "name": "模型" } }, "outputs": { "0": { - "name": "patched_model", + "name": "模型", "tooltip": null } } @@ -562,7 +562,7 @@ "display_name": "CLIP注意力相乘", "inputs": { "clip": { - "name": "clip" + "name": "CLIP" }, "k": { "name": "k" @@ -720,7 +720,7 @@ } }, "CLIPTextEncodeHiDream": { - "display_name": "CLIPTextEncodeHiDream", + "display_name": "CLIP文本编码HiDream", "inputs": { "clip": { "name": "clip" @@ -765,18 +765,18 @@ }, "CLIPTextEncodeLumina2": { "description": "使用CLIP模型将系统提示和用户提示编码成可以用来引导扩散模型生成特定图像的嵌入。", - "display_name": "Lumina2的CLIP文本编码", + "display_name": "CLIP文本编码Lumina2", "inputs": { "clip": { "name": "clip", "tooltip": "用于编码文本的CLIP模型。" }, "system_prompt": { - "name": "system_prompt", + "name": "系统提示词", "tooltip": "Lumina2提供两种类型的系统提示:优越:你是一个设计来根据文本提示或用户提示生成优越图像的助手,这些图像具有基于文本提示的优越度的图像-文本对齐。对齐:你是一个设计来根据文本提示生成高质量图像的助手,这些图像具有基于文本提示的最高度的图像-文本对齐。" }, "user_prompt": { - "name": "user_prompt", + "name": "提示词", "tooltip": "需要编码的文本。" } }, @@ -788,7 +788,7 @@ }, "CLIPTextEncodePixArtAlpha": { "description": "编码文本并设置PixArt Alpha的分辨率条件。不适用于PixArt Sigma。", - "display_name": "CLIPTextEncodePixArtAlpha", + "display_name": "CLIP文本编码PixArtAlpha", "inputs": { "clip": { "name": "剪辑" @@ -1575,10 +1575,10 @@ } }, "CosmosPredict2ImageToVideoLatent": { - "display_name": "CosmosPredict2ImageToVideoLatent", + "display_name": "CosmosPredict2图像到视频Latent", "inputs": { "batch_size": { - "name": "batch_size" + "name": "批次大小" }, "end_image": { "name": "结束图像" @@ -1874,7 +1874,7 @@ "display_name": "EasyCache", "inputs": { "end_percent": { - "name": "end_percent", + "name": "结束位置", "tooltip": "结束使用 EasyCache 的相对采样步数。" }, "model": { @@ -1882,15 +1882,15 @@ "tooltip": "要添加 EasyCache 的模型。" }, "reuse_threshold": { - "name": "reuse_threshold", + "name": "重用阈值", "tooltip": "重用缓存步骤的阈值。" }, "start_percent": { - "name": "start_percent", + "name": "开始位置", "tooltip": "开始使用 EasyCache 的相对采样步数。" }, "verbose": { - "name": "verbose", + "name": "调试信息", "tooltip": "是否记录详细信息。" } }, @@ -1901,11 +1901,11 @@ } }, "EmptyAceStepLatentAudio": { - "display_name": "EmptyAceStepLatentAudio", + "display_name": "空Latent音频(AceStep)", "inputs": { "batch_size": { - "name": "batch_size", - "tooltip": "批次中的潜在图像数量。" + "name": "批次大小", + "tooltip": "批次中的 Latent 数量。" }, "seconds": { "name": "秒数" @@ -1921,24 +1921,24 @@ "display_name": "空音频", "inputs": { "channels": { - "name": "channels", + "name": "通道", "tooltip": "音频通道数(1 为单声道,2 为立体声)。" }, "duration": { - "name": "duration", + "name": "长度", "tooltip": "空音频片段的持续时间(秒)" }, "sample_rate": { - "name": "sample_rate", + "name": "采样率", "tooltip": "空音频片段的采样率。" } } }, "EmptyChromaRadianceLatentImage": { - "display_name": "EmptyChromaRadianceLatentImage", + "display_name": "空Latent图像(ChromaRadiance)", "inputs": { "batch_size": { - "name": "batch_size" + "name": "批次大小" }, "height": { "name": "高度" @@ -1976,10 +1976,10 @@ } }, "EmptyHunyuanImageLatent": { - "display_name": "EmptyHunyuanImageLatent", + "display_name": "空Latent图像", "inputs": { "batch_size": { - "name": "batch_size" + "name": "批次大小" }, "height": { "name": "高度" @@ -1995,7 +1995,7 @@ } }, "EmptyHunyuanLatentVideo": { - "display_name": "空Latent视频(混元)", + "display_name": "空Latent视频(Hunyuan)", "inputs": { "batch_size": { "name": "批量大小" @@ -2016,6 +2016,28 @@ } } }, + "EmptyHunyuanVideo15Latent": { + "display_name": "空Latent视频(Hunyuan1.5)", + "inputs": { + "batch_size": { + "name": "批次大小" + }, + "height": { + "name": "高度" + }, + "length": { + "name": "时长" + }, + "width": { + "name": "宽度" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, "EmptyImage": { "display_name": "空图像", "inputs": { @@ -2068,11 +2090,11 @@ } }, "EmptyLatentHunyuan3Dv2": { - "display_name": "EmptyLatentHunyuan3Dv2", + "display_name": "空Latent图像(Hunyuan3Dv2)", "inputs": { "batch_size": { - "name": "批量大小", - "tooltip": "批量中的潜在图像数量。" + "name": "批次大小", + "tooltip": "批次中的Latent数量。" }, "resolution": { "name": "分辨率" @@ -2144,13 +2166,13 @@ } }, "Epsilon Scaling": { - "display_name": "Epsilon Scaling", + "display_name": "Epsilon缩放", "inputs": { "model": { "name": "模型" }, "scaling_factor": { - "name": "scaling_factor" + "name": "系数" } }, "outputs": { @@ -2174,7 +2196,7 @@ } }, "ExtendIntermediateSigmas": { - "display_name": "ExtendIntermediateSigmas", + "display_name": "插值扩展Sigmas", "inputs": { "end_at_sigma": { "name": "结束 sigma" @@ -2183,7 +2205,7 @@ "name": "sigmas" }, "spacing": { - "name": "间距" + "name": "间距方式" }, "start_at_sigma": { "name": "起始 sigma" @@ -2226,7 +2248,7 @@ "display_name": "Flux禁用指导", "inputs": { "conditioning": { - "name": "conditioning" + "name": "条件" } }, "outputs": { @@ -2253,10 +2275,10 @@ }, "FluxKontextImageScale": { "description": "此节点将图像调整为更适合 flux kontext 的尺寸。", - "display_name": "FluxKontextImageScale", + "display_name": "图像缩放为FluxKontext", "inputs": { "image": { - "name": "image" + "name": "图像" } }, "outputs": { @@ -2267,11 +2289,11 @@ }, "FluxKontextMaxImageNode": { "description": "使用Flux.1 Kontext [max]通过API基于提示词和宽高比编辑图像。", - "display_name": "Flux.1 Kontext [max] Image", + "display_name": "Flux.1 Kontext [max] 图像", "inputs": { "aspect_ratio": { "name": "宽高比", - "tooltip": "图像宽高比;必须在1:4到4:1之间。" + "tooltip": "图像宽高比;必须在1:4和4:1之间。" }, "control_after_generate": { "name": "生成后控制" @@ -2431,7 +2453,7 @@ "name": "图像" }, "mask": { - "name": "mask" + "name": "遮罩" }, "prompt": { "name": "提示词", @@ -2502,18 +2524,18 @@ "display_name": "FreSca", "inputs": { "freq_cutoff": { - "name": "freq_cutoff", + "name": "低频阈值", "tooltip": "围绕中心被视为低频的频率索引数量" }, "model": { - "name": "model" + "name": "模型" }, "scale_high": { - "name": "scale_high", + "name": "高频缩放", "tooltip": "高频分量的缩放因子" }, "scale_low": { - "name": "scale_low", + "name": "低频缩放", "tooltip": "低频分量的缩放因子" } }, @@ -2594,7 +2616,7 @@ "display_name": "GLIGEN文本框应用", "inputs": { "clip": { - "name": "CLIPCLIP" + "name": "CLIP" }, "conditioning_to": { "name": "条件到" @@ -2619,9 +2641,57 @@ } } }, + "GeminiImage2Node": { + "description": "通过Google API编辑图像。", + "display_name": "Nano Banana Pro(Google Gemini 图像)", + "inputs": { + "aspect_ratio": { + "name": "宽高比", + "tooltip": "自动:会自动匹配输入图像的宽高比,或如果没有提供图像,则生成1:1的正方形。" + }, + "control_after_generate": { + "name": "生成后控制" + }, + "files": { + "name": "文件", + "tooltip": "模型上下文文件。接受来自 Gemini输入文件 节点的输入。" + }, + "images": { + "name": "图像", + "tooltip": "参考图像。如果要使用多个参考图,使用 图像批次 节点(最多14张)。" + }, + "model": { + "name": "模型" + }, + "prompt": { + "name": "提示词", + "tooltip": "使用文本描述要生成的图像或要应用的编辑的内容。包括模型应该遵循的任何约束、样式或细节。" + }, + "resolution": { + "name": "分辨率", + "tooltip": "目标分辨率。2K/4K 会使用 Gemini 放大生成。" + }, + "response_modalities": { + "name": "响应模态", + "tooltip": "选择 图像 仅生成图像,选择 图像+文本 会生成图像和文本。" + }, + "seed": { + "name": "随机种", + "tooltip": "为特定值时,模型将尽可能提供相同的结果。但不保证确定一致的输出。此外,即使使用相同的种子值,更改模型或参数设置(例如温度)也会导致结果变化。默认使用随机值。" + } + }, + "outputs": { + "0": { + "tooltip": null + }, + "1": { + "tooltip": null + } + } + }, "GeminiImageNode": { "description": "通过Google API同步编辑图像。", - "display_name": "Google Gemini 图像", + "display_name": "Nano Banana (Google Gemini 图像)", "inputs": { "aspect_ratio": { "name": "宽高比", @@ -2665,7 +2735,7 @@ "display_name": "Gemini 输入文件", "inputs": { "GEMINI_INPUT_FILES": { - "name": "GEMINI_INPUT_FILES", + "name": "Gemini 输入文件", "tooltip": "与此节点加载的文件批量组合的可选附加文件。允许链式连接输入文件,以便单个消息可包含多个输入文件。" }, "file": { @@ -2780,7 +2850,7 @@ } }, "Hunyuan3Dv2Conditioning": { - "display_name": "Hunyuan3Dv2Conditioning", + "display_name": "Hunyuan3Dv2条件", "inputs": { "clip_vision_output": { "name": "clip视觉输出" @@ -2796,7 +2866,7 @@ } }, "Hunyuan3Dv2ConditioningMultiView": { - "display_name": "Hunyuan3Dv2ConditioningMultiView", + "display_name": "Hunyuan3Dv2条件多视角", "inputs": { "back": { "name": "后" @@ -2854,7 +2924,7 @@ "tooltip": null }, "1": { - "name": "潜在空间", + "name": "Latent", "tooltip": null } } @@ -2863,7 +2933,7 @@ "display_name": "HunyuanRefinerLatent", "inputs": { "latent": { - "name": "潜在" + "name": "Latent" }, "negative": { "name": "负面" @@ -2890,6 +2960,120 @@ } } }, + "HunyuanVideo15ImageToVideo": { + "display_name": "Hunyuan Video 15 图像到视频", + "inputs": { + "batch_size": { + "name": "批次大小" + }, + "clip_vision_output": { + "name": "CLIP视觉输出" + }, + "height": { + "name": "高度" + }, + "length": { + "name": "帧数" + }, + "negative": { + "name": "负面条件" + }, + "positive": { + "name": "正面条件" + }, + "start_image": { + "name": "图像" + }, + "vae": { + "name": "vae" + }, + "width": { + "name": "宽度" + } + }, + "outputs": { + "0": { + "name": "正面条件", + "tooltip": null + }, + "1": { + "name": "负面条件", + "tooltip": null + }, + "2": { + "name": "latent", + "tooltip": null + } + } + }, + "HunyuanVideo15LatentUpscaleWithModel": { + "display_name": "Hunyuan Video 15 Latent 使用模型放大", + "inputs": { + "crop": { + "name": "裁剪" + }, + "height": { + "name": "高度" + }, + "model": { + "name": "模型" + }, + "samples": { + "name": "Latent" + }, + "upscale_method": { + "name": "放大方法" + }, + "width": { + "name": "宽度" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, + "HunyuanVideo15SuperResolution": { + "display_name": "Hunyuan Video 15超分辨率", + "inputs": { + "clip_vision_output": { + "name": "CLIP视觉输出" + }, + "latent": { + "name": "latent" + }, + "negative": { + "name": "负面条件" + }, + "noise_augmentation": { + "name": "噪波增强" + }, + "positive": { + "name": "正面条件" + }, + "start_image": { + "name": "图像" + }, + "vae": { + "name": "vae" + } + }, + "outputs": { + "0": { + "name": "正面条件", + "tooltip": null + }, + "1": { + "name": "负面条件", + "tooltip": null + }, + "2": { + "name": "Latent", + "tooltip": null + } + } + }, "HyperTile": { "display_name": "超分块HyperTile", "inputs": { @@ -2939,29 +3123,29 @@ "display_name": "Ideogram V1", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "图像生成的宽高比。" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "magic_prompt_option": { - "name": "magic_prompt_option", + "name": "Magic Prompt", "tooltip": "确定生成时是否使用 MagicPrompt" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "描述需要从图像中排除的内容" }, "num_images": { - "name": "num_images" + "name": "图像数量" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于图像生成的提示词" }, "seed": { - "name": "seed" + "name": "随机种" }, "turbo": { "name": "turbo", @@ -2979,36 +3163,36 @@ "display_name": "Ideogram V2", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "图像生成的宽高比。如果分辨率未设置为 AUTO,则此项无效。" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "magic_prompt_option": { - "name": "magic_prompt_option", + "name": "Magic Prompt", "tooltip": "确定生成时是否使用 MagicPrompt" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "描述图像中需要排除的内容" }, "num_images": { - "name": "num_images" + "name": "图像数量" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于图像生成的提示词" }, "resolution": { - "name": "resolution", + "name": "分辨率", "tooltip": "图像生成的分辨率。如果未设置为 AUTO,则会覆盖 aspect_ratio 设置。" }, "seed": { - "name": "seed" + "name": "随机种" }, "style_type": { - "name": "style_type", + "name": "风格", "tooltip": "生成的风格类型(仅限 V2)" }, "turbo": { @@ -3027,7 +3211,7 @@ "display_name": "Ideogram V3", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "图像生成的宽高比。如果分辨率未设置为自动,则忽略此项。" }, "character_image": { @@ -3039,37 +3223,37 @@ "tooltip": "角色参考图像的可选遮罩。" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "image": { - "name": "image", + "name": "图像", "tooltip": "用于图像编辑的可选参考图片。" }, "magic_prompt_option": { - "name": "magic_prompt_option", + "name": "Magic Prompt", "tooltip": "决定生成时是否使用 MagicPrompt" }, "mask": { - "name": "mask", + "name": "遮罩", "tooltip": "用于修复的可选 mask(白色区域将被替换)" }, "num_images": { - "name": "num_images" + "name": "图像数量" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于图像生成或编辑的提示词" }, "rendering_speed": { - "name": "rendering_speed", + "name": "生成速度", "tooltip": "控制生成速度与质量之间的权衡" }, "resolution": { - "name": "resolution", + "name": "分辨率", "tooltip": "图像生成的分辨率。如果未设置为自动,则覆盖 aspect_ratio 设置。" }, "seed": { - "name": "seed" + "name": "随机种" } }, "outputs": { @@ -3303,7 +3487,7 @@ } }, "ImageRGBToYUV": { - "display_name": "ImageRGBToYUV", + "display_name": "图像RGB到YUV", "inputs": { "image": { "name": "图像" @@ -3476,7 +3660,7 @@ } }, "ImageYUVToRGB": { - "display_name": "ImageYUVToRGB", + "display_name": "图像YUV到RGB", "inputs": { "U": { "name": "U" @@ -3700,10 +3884,10 @@ "name": "rho" }, "sigma_max": { - "name": "sigma_max" + "name": "最大Sigma" }, "sigma_min": { - "name": "sigma_min" + "name": "最小Sigma" }, "steps": { "name": "步数" @@ -3715,25 +3899,25 @@ "display_name": "Kling 图像转视频(摄像机控制)", "inputs": { "aspect_ratio": { - "name": "aspect_ratio" + "name": "宽高比" }, "camera_control": { - "name": "camera_control", + "name": "镜头控制", "tooltip": "可通过 Kling Camera Controls 节点创建。控制视频生成过程中的摄像机运动和动作。" }, "cfg_scale": { - "name": "cfg_scale" + "name": "CFG" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "反向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "正向文本提示" }, "start_frame": { - "name": "start_frame", + "name": "起始帧", "tooltip": "参考图像 - URL 或 Base64 编码字符串,不能超过 10MB,分辨率不低于 300*300 像素,宽高比在 1:2.5 ~ 2.5:1 之间。Base64 不应包含 data:image 前缀。" } }, @@ -3742,11 +3926,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -3756,21 +3940,21 @@ "display_name": "Kling 文本转视频(摄像机控制)", "inputs": { "aspect_ratio": { - "name": "aspect_ratio" + "name": "宽高比" }, "camera_control": { - "name": "camera_control", + "name": "镜头控制", "tooltip": "可通过 Kling Camera Controls 节点创建。控制视频生成过程中的摄像机运动和移动。" }, "cfg_scale": { - "name": "cfg_scale" + "name": "CFG" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "反向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "正向文本提示" } }, @@ -3779,11 +3963,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -3793,63 +3977,63 @@ "display_name": "Kling 相机控制", "inputs": { "camera_control_type": { - "name": "camera_control_type" + "name": "镜头类型" }, "horizontal_movement": { - "name": "horizontal_movement", + "name": "水平移动", "tooltip": "控制相机在水平轴(x 轴)上的移动。负值表示向左,正值表示向右。" }, "pan": { - "name": "pan", + "name": "垂直旋转", "tooltip": "控制相机在垂直平面(x 轴)上的旋转。负值表示向下旋转,正值表示向上旋转。" }, "roll": { - "name": "roll", + "name": "滚转", "tooltip": "控制相机的滚转量(z 轴)。负值表示逆时针,正值表示顺时针。" }, "tilt": { - "name": "tilt", + "name": "水平旋转", "tooltip": "控制相机在水平平面(y 轴)上的旋转。负值表示向左旋转,正值表示向右旋转。" }, "vertical_movement": { - "name": "vertical_movement", + "name": "垂直移动", "tooltip": "控制相机在垂直轴(y 轴)上的移动。负值表示向下,正值表示向上。" }, "zoom": { - "name": "zoom", + "name": "变焦", "tooltip": "控制相机焦距的变化。负值表示视野变窄,正值表示视野变宽。" } }, "outputs": { "0": { - "name": "camera_control", + "name": "镜头控制", "tooltip": null } } }, "KlingDualCharacterVideoEffectNode": { - "description": "根据 effect_scene 在生成视频时实现不同的特效。第一张图片将被放置在合成画面的左侧,第二张图片在右侧。", + "description": "根据 效果 在生成视频时实现不同的特效。第一张图片将被放置在合成画面的左侧,第二张图片在右侧。", "display_name": "Kling 双角色视频特效", "inputs": { "duration": { - "name": "duration" + "name": "时长" }, "effect_scene": { - "name": "effect_scene" + "name": "效果" }, "image_left": { - "name": "image_left", + "name": "左侧图像", "tooltip": "左侧图片" }, "image_right": { - "name": "image_right", + "name": "右侧图像", "tooltip": "右侧图片" }, "mode": { - "name": "mode" + "name": "模式" }, "model_name": { - "name": "model_name" + "name": "模型" } }, "outputs": { @@ -3857,7 +4041,7 @@ "tooltip": null }, "1": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -3866,30 +4050,30 @@ "display_name": "Kling 图像转视频", "inputs": { "aspect_ratio": { - "name": "aspect_ratio" + "name": "宽高比" }, "cfg_scale": { - "name": "cfg_scale" + "name": "CFG" }, "duration": { - "name": "duration" + "name": "时长" }, "mode": { - "name": "mode" + "name": "模式" }, "model_name": { - "name": "model_name" + "name": "模型" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "反向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "正向文本提示" }, "start_frame": { - "name": "start_frame", + "name": "起始帧", "tooltip": "参考图像 - URL 或 Base64 编码字符串,不能超过 10MB,分辨率不少于 300*300 像素,宽高比在 1:2.5 ~ 2.5:1 之间。Base64 不应包含 data:image 前缀。" } }, @@ -3898,11 +4082,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -3912,35 +4096,35 @@ "display_name": "Kling 图像生成", "inputs": { "aspect_ratio": { - "name": "aspect_ratio" + "name": "宽高比" }, "human_fidelity": { - "name": "human_fidelity", + "name": "主体参考强度", "tooltip": "主体参考相似度" }, "image": { - "name": "image" + "name": "图像" }, "image_fidelity": { - "name": "image_fidelity", + "name": "图像参考强度", "tooltip": "用户上传图像的参考强度" }, "image_type": { - "name": "image_type" + "name": "图像类型" }, "model_name": { - "name": "model_name" + "name": "模型" }, "n": { - "name": "n", + "name": "图像数量", "tooltip": "生成图像数量" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "反向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "正向文本提示" } }, @@ -4012,21 +4196,21 @@ } }, "KlingSingleImageVideoEffectNode": { - "description": "根据 effect_scene 在生成视频时实现不同的特效。", + "description": "根据 效果 在生成视频时实现不同的特效。", "display_name": "Kling 视频特效", "inputs": { "duration": { - "name": "duration" + "name": "时长" }, "effect_scene": { - "name": "effect_scene" + "name": "效果" }, "image": { - "name": "image", + "name": "图像3", "tooltip": "参考图片。URL 或 Base64 编码字符串(不含 data:image 前缀)。文件大小不得超过 10MB,分辨率不低于 300*300 像素,宽高比在 1:2.5 ~ 2.5:1 之间。" }, "model_name": { - "name": "model_name" + "name": "模型" } }, "outputs": { @@ -4034,11 +4218,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -4048,29 +4232,29 @@ "display_name": "Kling 起止帧生成视频", "inputs": { "aspect_ratio": { - "name": "aspect_ratio" + "name": "宽高比" }, "cfg_scale": { - "name": "cfg_scale" + "name": "CFG" }, "end_frame": { "name": "end_frame", "tooltip": "参考图像 - 结束帧控制。URL 或 Base64 编码字符串,不能超过 10MB,分辨率不低于 300*300 像素。Base64 不应包含 data:image 前缀。" }, "mode": { - "name": "mode", + "name": "模式", "tooltip": "用于视频生成的配置,格式为:mode / duration / model_name。" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "反向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "正向文本提示" }, "start_frame": { - "name": "start_frame", + "name": "起始帧", "tooltip": "参考图像 - URL 或 Base64 编码字符串,不能超过 10MB,分辨率不低于 300*300 像素,宽高比在 1:2.5 ~ 2.5:1 之间。Base64 不应包含 data:image 前缀。" } }, @@ -4079,11 +4263,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -4093,21 +4277,21 @@ "display_name": "Kling 文本转视频", "inputs": { "aspect_ratio": { - "name": "aspect_ratio" + "name": "宽高比" }, "cfg_scale": { - "name": "cfg_scale" + "name": "CFG" }, "mode": { - "name": "mode", + "name": "模式", "tooltip": "用于视频生成的配置,格式为:mode / duration / model_name。" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "反向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "正向文本提示" } }, @@ -4116,11 +4300,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -4130,18 +4314,18 @@ "display_name": "Kling 视频扩展", "inputs": { "cfg_scale": { - "name": "cfg_scale" + "name": "CFG" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "用于在扩展视频中避免出现的元素的负向文本提示" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于引导视频扩展的正向文本提示" }, "video_id": { - "name": "video_id", + "name": "视频ID", "tooltip": "要扩展的视频 ID。支持由文本转视频、图像转视频以及之前的视频扩展操作生成的视频。扩展后总时长不能超过 3 分钟。" } }, @@ -4150,11 +4334,11 @@ "tooltip": null }, "1": { - "name": "video_id", + "name": "视频ID", "tooltip": null }, "2": { - "name": "duration", + "name": "时长", "tooltip": null } } @@ -4164,13 +4348,13 @@ "display_name": "Kling 虚拟试穿", "inputs": { "cloth_image": { - "name": "cloth_image" + "name": "服装" }, "human_image": { - "name": "human_image" + "name": "主体" }, "model_name": { - "name": "model_name" + "name": "模型" } }, "outputs": { @@ -4738,6 +4922,19 @@ } } }, + "LatentUpscaleModelLoader": { + "display_name": "加载Latent放大模型", + "inputs": { + "model_name": { + "name": "模型" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, "LazyCache": { "description": "EasyCache 的自制版本 - 更'简单'的 EasyCache 实现。总体效果不如 EasyCache,但在某些罕见情况下表现更好,并且与 ComfyUI 中的所有内容具有通用兼容性。", "display_name": "惰性缓存", @@ -4791,22 +4988,22 @@ }, "outputs": { "0": { - "name": "image" + "name": "图像" }, "1": { - "name": "mask" + "name": "遮罩" }, "2": { - "name": "mesh_path" + "name": "网格路径" }, "3": { - "name": "normal" + "name": "法向" }, "4": { - "name": "lineart" + "name": "线条" }, "5": { - "name": "camera_info" + "name": "相机信息" } } }, @@ -4892,7 +5089,7 @@ "display_name": "加载LoRA", "inputs": { "clip": { - "name": "CLIPCLIP", + "name": "CLIP", "tooltip": "LoRA 将应用于的 CLIP 模型。" }, "lora_name": { @@ -5000,7 +5197,7 @@ } }, "LotusConditioning": { - "display_name": "LotusConditioning", + "display_name": "Lotus条件", "outputs": { "0": { "name": "条件", @@ -5077,25 +5274,25 @@ "display_name": "Luma 概念", "inputs": { "concept1": { - "name": "concept1" + "name": "概念1" }, "concept2": { - "name": "concept2" + "name": "概念2" }, "concept3": { - "name": "concept3" + "name": "概念3" }, "concept4": { - "name": "concept4" + "name": "概念4" }, "luma_concepts": { - "name": "luma_concepts", + "name": "Luma概念", "tooltip": "可选的相机概念,将添加到此处选择的概念中。" } }, "outputs": { "0": { - "name": "luma_concepts", + "name": "Luma概念", "tooltip": null } } @@ -5105,24 +5302,24 @@ "display_name": "Luma 图像到图像", "inputs": { "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "image": { - "name": "image" + "name": "图像" }, "image_weight": { - "name": "image_weight", + "name": "图像权重", "tooltip": "图像权重;越接近 1.0,图像被修改的程度越小。" }, "model": { - "name": "model" + "name": "模型" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于图像生成的提示词" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于决定节点是否重新运行的种子;无论种子如何,实际结果都是非确定性的。" } }, @@ -5198,7 +5395,7 @@ "name": "循环" }, "luma_concepts": { - "name": "luma_concepts", + "name": "Luma概念", "tooltip": "可选的 Camera Concepts,通过 Luma Concepts 节点控制相机运动。" }, "model": { @@ -5231,7 +5428,7 @@ "tooltip": "用作参考的图片。" }, "luma_ref": { - "name": "luma_ref" + "name": "Luma参考" }, "weight": { "name": "权重", @@ -5240,7 +5437,7 @@ }, "outputs": { "0": { - "name": "luma_ref", + "name": "Luma参考", "tooltip": null } } @@ -5262,7 +5459,7 @@ "name": "循环" }, "luma_concepts": { - "name": "luma_concepts", + "name": "Luma概念", "tooltip": "可选的相机概念,通过 Luma Concepts 节点控制相机运动。" }, "model": { @@ -5323,10 +5520,10 @@ }, "MaskPreview": { "description": "将输入图像保存到您的 ComfyUI 输出目录。", - "display_name": "MaskPreview", + "display_name": "预览遮罩", "inputs": { "mask": { - "name": "mask" + "name": "遮罩" } } }, @@ -5381,22 +5578,22 @@ "display_name": "MiniMax 图像转视频", "inputs": { "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "image": { - "name": "image", + "name": "图像", "tooltip": "用于视频生成首帧的图像" }, "model": { - "name": "model", + "name": "模型", "tooltip": "用于视频生成的模型" }, "prompt_text": { - "name": "prompt_text", + "name": "提示词", "tooltip": "用于引导视频生成的文本提示" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成噪声的随机种子。" } }, @@ -5411,18 +5608,18 @@ "display_name": "MiniMax 文本转视频", "inputs": { "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "model": { - "name": "model", + "name": "模型", "tooltip": "用于视频生成的模型" }, "prompt_text": { - "name": "prompt_text", + "name": "提示词", "tooltip": "用于引导视频生成的文本提示" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成噪声的随机种子。" } }, @@ -5615,7 +5812,7 @@ } }, "ModelMergeCosmos14B": { - "display_name": "ModelMergeCosmos14B", + "display_name": "模型融合(Cosmos14B)", "inputs": { "affline_norm_": { "name": "仿射规范化." @@ -5752,7 +5949,7 @@ } }, "ModelMergeCosmos7B": { - "display_name": "ModelMergeCosmos7B", + "display_name": "模型融合(COsmos7B)", "inputs": { "affline_norm_": { "name": "仿射规范化." @@ -5865,7 +6062,7 @@ } }, "ModelMergeCosmosPredict2_14B": { - "display_name": "ModelMergeCosmosPredict2_14B", + "display_name": "模型融合(CosmosPredict2_14B)", "inputs": { "blocks_0_": { "name": "块0。" @@ -5999,7 +6196,7 @@ } }, "ModelMergeCosmosPredict2_2B": { - "display_name": "ModelMergeCosmosPredict2_2B", + "display_name": "模型融合(CosmosPredict2_2B)", "inputs": { "blocks_0_": { "name": "块.0." @@ -7356,7 +7553,7 @@ }, "ModelMergeWAN2_1": { "description": "1.3B模型有30个模块,14B模型有40个模块。图像转视频模型有额外的img_emb。", - "display_name": "ModelMergeWAN2_1", + "display_name": "模型融合(WAN2.1)", "inputs": { "blocks_0_": { "name": "blocks.0." @@ -7485,10 +7682,10 @@ "name": "img_emb." }, "model1": { - "name": "model1" + "name": "模型1" }, "model2": { - "name": "model2" + "name": "模型2" }, "patch_embedding_": { "name": "patch_embedding." @@ -7505,7 +7702,7 @@ } }, "ModelPatchLoader": { - "display_name": "ModelPatchLoader", + "display_name": "加载模型补丁", "inputs": { "name": { "name": "名称" @@ -7784,11 +7981,11 @@ "display_name": "OpenAI ChatGPT 高级选项", "inputs": { "instructions": { - "name": "instructions", + "name": "指令", "tooltip": "指导模型如何生成响应的指令" }, "max_output_tokens": { - "name": "max_output_tokens", + "name": "Token输出上限", "tooltip": "生成响应时可生成token数量的上限,包括可见输出token" }, "truncation": { @@ -7807,27 +8004,27 @@ "display_name": "OpenAI ChatGPT", "inputs": { "advanced_options": { - "name": "advanced_options", + "name": "高级设置", "tooltip": "模型的可选配置。接受来自OpenAI聊天高级选项节点的输入。" }, "files": { - "name": "files", + "name": "文件", "tooltip": "可选文件,用作模型的上下文。接受来自OpenAI聊天输入文件节点的输入。" }, "images": { - "name": "images", + "name": "图像", "tooltip": "可选图像,用作模型的上下文。要包含多张图像,可使用批处理图像节点。" }, "model": { - "name": "model", + "name": "模型", "tooltip": "用于生成响应的模型" }, "persist_context": { - "name": "persist_context", + "name": "保持上下文", "tooltip": "此参数已弃用,无任何效果。" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "模型的文本输入,用于生成响应。" } }, @@ -7925,7 +8122,7 @@ "tooltip": "用于图像编辑的可选参考图像。" }, "mask": { - "name": "mask", + "name": "遮罩", "tooltip": "用于修复的可选 mask(白色区域将被替换)" }, "n": { @@ -7960,11 +8157,11 @@ "display_name": "OpenAI ChatGPT Input Files", "inputs": { "OPENAI_INPUT_FILES": { - "name": "OPENAI_INPUT_FILES", + "name": "OpenAI输入文件", "tooltip": "可选的附加文件,与此节点加载的文件一起批处理。允许链式连接输入文件,以便单个消息可包含多个输入文件。" }, "file": { - "name": "file", + "name": "文件", "tooltip": "作为模型上下文的输入文件。目前仅接受文本(.txt)和PDF(.pdf)文件。" } }, @@ -7979,27 +8176,27 @@ "display_name": "OpenAI Sora - Video", "inputs": { "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "duration": { - "name": "duration" + "name": "时长" }, "image": { - "name": "image" + "name": "图像" }, "model": { - "name": "model" + "name": "模型" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "引导文本;如果存在输入图像,可为空。" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "确定节点是否应重新运行的种子;无论种子如何,实际结果都是非确定性的。" }, "size": { - "name": "size" + "name": "尺寸" } }, "outputs": { @@ -8009,13 +8206,13 @@ } }, "OptimalStepsScheduler": { - "display_name": "OptimalStepsScheduler", + "display_name": "OptimalSteps调度器", "inputs": { "denoise": { "name": "去噪" }, "model_type": { - "name": "model_type" + "name": "模型" }, "steps": { "name": "步数" @@ -8337,7 +8534,7 @@ }, "outputs": { "0": { - "name": "pixverse_template", + "name": "Pixverse模板", "tooltip": null } } @@ -8584,7 +8781,7 @@ }, "QuadrupleCLIPLoader": { "description": "[配方]\n\nhidream: long clip-l, long clip-g, t5xxl, llama_8b_3.1_instruct", - "display_name": "QuadrupleCLIPLoader", + "display_name": "四重CLIP加载器", "inputs": { "clip_name1": { "name": "clip_name1" @@ -8609,19 +8806,19 @@ "display_name": "QwenImageDiffsynthControlnet", "inputs": { "image": { - "name": "image" + "name": "图像" }, "mask": { - "name": "mask" + "name": "遮罩" }, "model": { - "name": "model" + "name": "模型" }, "model_patch": { - "name": "model_patch" + "name": "模型补丁" }, "strength": { - "name": "strength" + "name": "强度" }, "vae": { "name": "vae" @@ -8672,10 +8869,10 @@ } }, "RecordAudio": { - "display_name": "Record Audio", + "display_name": "录制音频", "inputs": { "audio": { - "name": "audio" + "name": "音频" } } }, @@ -8696,12 +8893,12 @@ "tooltip": "颜色的红色值。" }, "recraft_color": { - "name": "recraft_color" + "name": "Recraft色彩" } }, "outputs": { "0": { - "name": "recraft_color", + "name": "Recraft色彩", "tooltip": null } } @@ -8711,15 +8908,15 @@ "display_name": "Recraft 控件", "inputs": { "background_color": { - "name": "background_color" + "name": "背景色" }, "colors": { - "name": "colors" + "name": "色彩" } }, "outputs": { "0": { - "name": "recraft_controls", + "name": "Recraft控制", "tooltip": null } } @@ -8760,10 +8957,10 @@ "name": "生成后控制" }, "image": { - "name": "image" + "name": "图像" }, "mask": { - "name": "mask" + "name": "遮罩" }, "n": { "name": "数量", @@ -8778,7 +8975,7 @@ "tooltip": "用于图像生成的提示词。" }, "recraft_style": { - "name": "recraft_style" + "name": "Recraft风格" }, "seed": { "name": "种子", @@ -8799,7 +8996,7 @@ "name": "生成后控制" }, "image": { - "name": "image" + "name": "图像" }, "n": { "name": "数量", @@ -8814,11 +9011,11 @@ "tooltip": "用于生成图像的提示词。" }, "recraft_controls": { - "name": "recraft_controls", + "name": "Recraft控制", "tooltip": "通过 Recraft Controls 节点对生成过程进行可选的附加控制。" }, "recraft_style": { - "name": "recraft_style" + "name": "Recraft风格" }, "seed": { "name": "种子", @@ -8898,7 +9095,7 @@ }, "outputs": { "0": { - "name": "recraft_style", + "name": "Recraft风格", "tooltip": null } } @@ -8908,13 +9105,13 @@ "display_name": "Recraft 风格 - 无限风格库", "inputs": { "style_id": { - "name": "style_id", + "name": "风格ID", "tooltip": "来自无限风格库的风格 UUID。" } }, "outputs": { "0": { - "name": "recraft_style", + "name": "Recraft风格", "tooltip": null } } @@ -8929,7 +9126,7 @@ }, "outputs": { "0": { - "name": "recraft_style", + "name": "Recraft风格", "tooltip": null } } @@ -8944,7 +9141,7 @@ }, "outputs": { "0": { - "name": "recraft_style", + "name": "Recraft风格", "tooltip": null } } @@ -8954,33 +9151,33 @@ "display_name": "Recraft 文本转图像", "inputs": { "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "n": { "name": "n", "tooltip": "要生成的图像数量。" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "对图像中不希望出现元素的可选文本描述。" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于图像生成的提示词。" }, "recraft_controls": { - "name": "recraft_controls", + "name": "Recraft控制", "tooltip": "通过 Recraft Controls 节点对生成过程的可选附加控制。" }, "recraft_style": { - "name": "recraft_style" + "name": "Recraft风格" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于决定节点是否重新运行的种子;无论种子如何,实际结果都是非确定性的。" }, "size": { - "name": "size", + "name": "尺寸", "tooltip": "生成图像的尺寸。" } }, @@ -9047,13 +9244,13 @@ }, "ReferenceLatent": { "description": "此节点为编辑模型设置引导潜在空间。如果模型支持,您可以链式连接多个以设置多个参考图像。", - "display_name": "ReferenceLatent", + "display_name": "参考Latent", "inputs": { "conditioning": { - "name": "conditioning" + "name": "条件" }, "latent": { - "name": "latent" + "name": "Latent" } }, "outputs": { @@ -9708,7 +9905,7 @@ "name": "eta" }, "noise_device": { - "name": "噪波设备" + "name": "设备" }, "s_noise": { "name": "s_noise" @@ -9736,7 +9933,7 @@ "name": "eta" }, "noise_device": { - "name": "噪波设备" + "name": "设备" }, "s_noise": { "name": "s_noise" @@ -9750,7 +9947,7 @@ "name": "eta" }, "noise_device": { - "name": "噪波设备" + "name": "设备" }, "r": { "name": "r" @@ -9841,7 +10038,7 @@ } }, "SamplerSASolver": { - "display_name": "SamplerSASolver", + "display_name": "SASolver采样器", "inputs": { "corrector_order": { "name": "校正器阶数" @@ -9873,7 +10070,7 @@ } }, "SamplingPercentToSigma": { - "display_name": "SamplingPercentToSigma", + "display_name": "采样比到Sigma", "inputs": { "model": { "name": "模型" @@ -9981,7 +10178,7 @@ } }, "SaveGLB": { - "display_name": "SaveGLB", + "display_name": "保存GLB", "inputs": { "filename_prefix": { "name": "文件名前缀" @@ -10029,10 +10226,10 @@ }, "SaveSVGNode": { "description": "在磁盘上保存 SVG 文件。", - "display_name": "SaveSVGNode", + "display_name": "保存SVG", "inputs": { "filename_prefix": { - "name": "filename_prefix", + "name": "文件名前缀", "tooltip": "保存文件的前缀。可包含格式化信息,如 %date:yyyy-MM-dd% 或 %Empty Latent Image.width% 以包含节点中的值。" }, "svg": { @@ -10085,10 +10282,10 @@ }, "ScaleROPE": { "description": "缩放和偏移模型的ROPE。", - "display_name": "ScaleROPE", + "display_name": "缩放ROPE", "inputs": { "model": { - "name": "model" + "name": "模型" }, "scale_t": { "name": "scale_t" @@ -10233,23 +10430,23 @@ } }, "SkipLayerGuidanceDiTSimple": { - "description": "SkipLayerGuidanceDiT节点的简化版本,仅修改无条件传递。", - "display_name": "SkipLayerGuidanceDiTSimple", + "description": "跳过层引导(DiT)节点的简化版本,仅修改无条件传递。", + "display_name": "跳过层引导(DiT简化)", "inputs": { "double_layers": { - "name": "double_layers" + "name": "双层" }, "end_percent": { - "name": "end_percent" + "name": "结束百分比" }, "model": { - "name": "model" + "name": "模型" }, "single_layers": { - "name": "single_layers" + "name": "单层" }, "start_percent": { - "name": "start_percent" + "name": "开始百分比" } }, "outputs": { @@ -10300,18 +10497,18 @@ }, "SplitAudioChannels": { "description": "将音频分离为左右声道。", - "display_name": "Split Audio Channels", + "display_name": "分离音频通道", "inputs": { "audio": { - "name": "audio" + "name": "音频" } }, "outputs": { "0": { - "name": "left" + "name": "左声道" }, "1": { - "name": "right" + "name": "右声道" } } }, @@ -10371,37 +10568,37 @@ }, "StabilityAudioInpaint": { "description": "使用文本指令转换现有音频样本的部分内容。", - "display_name": "Stability AI Audio Inpaint", + "display_name": "Stability AI 音频重绘", "inputs": { "audio": { - "name": "audio", + "name": "音频", "tooltip": "音频长度必须在6到190秒之间。" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "duration": { - "name": "duration", + "name": "时长", "tooltip": "控制生成音频的时长(秒)。" }, "mask_end": { - "name": "mask_end" + "name": "结束遮罩" }, "mask_start": { - "name": "mask_start" + "name": "开始遮罩" }, "model": { - "name": "model" + "name": "模型" }, "prompt": { - "name": "prompt" + "name": "提示词" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成的随机种子。" }, "steps": { - "name": "steps", + "name": "步数", "tooltip": "控制采样步数。" } }, @@ -10413,31 +10610,31 @@ }, "StabilityAudioToAudio": { "description": "使用文本指令将现有音频样本转换为新的高质量作品。", - "display_name": "Stability AI Audio To Audio", + "display_name": "Stability AI 音频到音频", "inputs": { "audio": { - "name": "audio", + "name": "音频", "tooltip": "音频长度必须在6到190秒之间。" }, "control_after_generate": { "name": "生成后控制" }, "duration": { - "name": "duration", + "name": "时长", "tooltip": "控制生成音频的时长(秒)。" }, "model": { - "name": "model" + "name": "模型" }, "prompt": { - "name": "prompt" + "name": "提示词" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成的随机种子。" }, "steps": { - "name": "steps", + "name": "步数", "tooltip": "控制采样步数。" }, "strength": { @@ -10456,40 +10653,40 @@ "display_name": "Stability AI Stable Diffusion 3.5 图像", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "生成图像的宽高比。" }, "cfg_scale": { - "name": "cfg_scale", + "name": "CFG", "tooltip": "扩散过程对提示词文本的遵循程度(数值越高,生成的图像越接近你的提示词)" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "image": { - "name": "image" + "name": "图像" }, "image_denoise": { - "name": "image_denoise", + "name": "图像降噪", "tooltip": "输入图像的去噪程度;0.0 表示与输入图像完全相同,1.0 表示完全不使用输入图像。" }, "model": { - "name": "model" + "name": "模型" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "你不希望在输出图像中出现的关键词。此为高级功能。" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "你希望在输出图像中看到的内容。强有力且描述清晰的提示词,能够明确定义元素、颜色和主题,将带来更好的结果。" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成噪声的随机种子。" }, "style_preset": { - "name": "style_preset", + "name": "风格预设", "tooltip": "可选,生成图像的期望风格。" } }, @@ -10501,36 +10698,36 @@ }, "StabilityStableImageUltraNode": { "description": "根据提示词和分辨率同步生成图像。", - "display_name": "Stability AI Stable Image Ultra", + "display_name": "Stability AI Stable 图像 Ultra", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "生成图像的宽高比。" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "image": { - "name": "image" + "name": "图像" }, "image_denoise": { - "name": "image_denoise", + "name": "降噪", "tooltip": "输入图像的去噪强度;0.0 表示与输入图像完全相同,1.0 表示完全不参考输入图像。" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "描述你不希望在输出图像中出现内容的文本。这是一个高级功能。" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "你希望在输出图像中看到的内容。一个强有力且描述清晰的提示词,能够明确界定元素、颜色和主题,将带来更好的结果。要控制某个词的权重,请使用格式 `(word:weight)`,其中 `word` 是你想要控制权重的词,`weight` 是介于 0 到 1 之间的数值。例如:`The sky was a crisp (blue:0.3) and (green:0.8)` 表示天空是蓝色和绿色,但绿色多于蓝色。" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成噪声的随机种子。" }, "style_preset": { - "name": "style_preset", + "name": "风格预设", "tooltip": "可选的生成图像风格。" } }, @@ -10584,7 +10781,7 @@ "tooltip": "控制生成与初始图像不强相关的额外细节的可能性。" }, "image": { - "name": "image" + "name": "图像" }, "negative_prompt": { "name": "反向提示词", @@ -10998,7 +11195,7 @@ } }, "T5TokenizerOptions": { - "display_name": "T5TokenizerOptions", + "display_name": "T5Tokenizer设置", "inputs": { "clip": { "name": "clip" @@ -11026,7 +11223,7 @@ }, "outputs": { "0": { - "name": "修补模型", + "name": "模型", "tooltip": null } } @@ -11049,13 +11246,13 @@ }, "outputs": { "0": { - "name": "patched_model", + "name": "模型", "tooltip": null } } }, "TextEncodeAceStepAudio": { - "display_name": "TextEncodeAceStepAudio", + "display_name": "文本音频编码(AceStep)", "inputs": { "clip": { "name": "clip" @@ -11077,7 +11274,7 @@ } }, "TextEncodeHunyuanVideo_ImageToVideo": { - "display_name": "文本编码Hunyuan视频_图像到视频", + "display_name": "文本编码(Hunyuan视频_图像到视频)", "inputs": { "clip": { "name": "clip" @@ -11100,7 +11297,7 @@ } }, "TextEncodeQwenImageEdit": { - "display_name": "TextEncodeQwenImageEdit", + "display_name": "文本编码(QwenImageEdit)", "inputs": { "clip": { "name": "clip" @@ -11122,7 +11319,7 @@ } }, "TextEncodeQwenImageEditPlus": { - "display_name": "TextEncodeQwenImageEditPlus", + "display_name": "文本编码(QwenImageEditPlus)", "inputs": { "clip": { "name": "clip" @@ -11176,6 +11373,118 @@ } } }, + "TopazImageEnhance": { + "description": "专业级放大和图像增强。", + "display_name": "Topaz 图像增强", + "inputs": { + "color_preservation": { + "name": "固定色彩", + "tooltip": "保证色彩一致性。" + }, + "creativity": { + "name": "多样性" + }, + "crop_to_fill": { + "name": "裁剪", + "tooltip": "默认情况下,当输出宽高比不同时,图像会带有边框。启用后会裁剪图像填充输出尺寸。" + }, + "face_enhancement": { + "name": "面部增强", + "tooltip": "优化面部(如果存在)。" + }, + "face_enhancement_creativity": { + "name": "面部增强多样性", + "tooltip": "设置面部增强时的多样性。" + }, + "face_enhancement_strength": { + "name": "面部增强强度", + "tooltip": "控制面部相对于背景的锐度。" + }, + "face_preservation": { + "name": "固定面部", + "tooltip": "保证主体面部一致性。" + }, + "image": { + "name": "图像" + }, + "model": { + "name": "模型" + }, + "output_height": { + "name": "输出高度", + "tooltip": "0为自动(一般情况下和输入图像相同)。" + }, + "output_width": { + "name": "输出宽度", + "tooltip": "0为自动(一般情况下和输入图像相同)。" + }, + "prompt": { + "name": "提示词", + "tooltip": "用于引导放大" + }, + "subject_detection": { + "name": "物体检测" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, + "TopazVideoEnhance": { + "description": "通过强大的放大和修复技术,为视频注入新的生命。", + "display_name": "Topaz 视频增强", + "inputs": { + "dynamic_compression_level": { + "name": "动态压缩", + "tooltip": "CQP 等级." + }, + "interpolation_duplicate": { + "name": "补帧重复帧", + "tooltip": "分析原视频,移除重复的帧。" + }, + "interpolation_duplicate_threshold": { + "name": "补帧重复帧阈值", + "tooltip": "判断为重复帧的阈值。" + }, + "interpolation_enabled": { + "name": "补帧" + }, + "interpolation_frame_rate": { + "name": "补帧帧率", + "tooltip": "输出帧率" + }, + "interpolation_model": { + "name": "补帧模型" + }, + "interpolation_slowmo": { + "name": "补帧慢动作", + "tooltip": "使用慢动作补帧。例如,设为 2 会让视频变慢到 0.5 倍,持续时间增加到 2 倍。" + }, + "upscaler_creativity": { + "name": "放大多样性", + "tooltip": "多样等级(仅适用 Starlight (Astra) Creative 模型)." + }, + "upscaler_enabled": { + "name": "放大" + }, + "upscaler_model": { + "name": "放大模型" + }, + "upscaler_resolution": { + "name": "放大分辨率" + }, + "video": { + "name": "视频" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } + }, "TorchCompileModel": { "display_name": "Torch编译模型", "inputs": { @@ -11296,13 +11605,13 @@ } }, "TrimVideoLatent": { - "display_name": "TrimVideoLatent", + "display_name": "修剪视频Latent", "inputs": { "samples": { - "name": "samples" + "name": "Latent" }, "trim_amount": { - "name": "trim_amount" + "name": "修剪数量" } }, "outputs": { @@ -11751,7 +12060,7 @@ } }, "VAEDecodeHunyuan3D": { - "display_name": "VAEDecodeHunyuan3D", + "display_name": "VAE解码(Hunyuan3D)", "inputs": { "num_chunks": { "name": "块数" @@ -11897,46 +12206,46 @@ "display_name": "Google Veo 3 视频生成", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "输出视频的宽高比" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "duration_seconds": { - "name": "duration_seconds", + "name": "时长", "tooltip": "输出视频的时长(秒)(Veo 3 仅支持 8 秒)" }, "enhance_prompt": { - "name": "enhance_prompt", + "name": "优化提示词", "tooltip": "是否使用 AI 辅助增强提示" }, "generate_audio": { - "name": "generate_audio", + "name": "生成音频", "tooltip": "为视频生成音频。所有 Veo 3 模型均支持此功能。" }, "image": { - "name": "image", + "name": "图像", "tooltip": "用于指导视频生成的可选参考图像" }, "model": { - "name": "model", + "name": "模型", "tooltip": "用于视频生成的 Veo 3 模型" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "负面文本提示,指导视频中应避免的内容" }, "person_generation": { - "name": "person_generation", + "name": "生成人类", "tooltip": "是否允许在视频中生成人物" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "视频的文本描述" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "视频生成的种子值(0 表示随机)" } }, @@ -11951,42 +12260,42 @@ "display_name": "Google Veo2 视频生成", "inputs": { "aspect_ratio": { - "name": "aspect_ratio", + "name": "宽高比", "tooltip": "输出视频的宽高比" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "duration_seconds": { - "name": "duration_seconds", + "name": "时长", "tooltip": "输出视频的时长(秒)" }, "enhance_prompt": { - "name": "enhance_prompt", + "name": "优化提示词", "tooltip": "是否使用 AI 辅助增强提示词" }, "image": { - "name": "image", + "name": "图像", "tooltip": "可选的参考图像,用于引导视频生成" }, "model": { - "name": "model", + "name": "模型", "tooltip": "用于视频生成的 Veo 2 模型" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "用于指导视频中应避免内容的负面文本提示" }, "person_generation": { - "name": "person_generation", + "name": "生成人类", "tooltip": "是否允许在视频中生成人物" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "视频的文本描述" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "视频生成的种子(0 表示随机)" } }, @@ -12023,34 +12332,34 @@ "display_name": "Vidu 图像转视频生成", "inputs": { "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "duration": { - "name": "duration", + "name": "时长", "tooltip": "输出视频的时长(秒)" }, "image": { - "name": "image", + "name": "图像", "tooltip": "用作生成视频起始帧的图像" }, "model": { - "name": "model", + "name": "模型", "tooltip": "模型名称" }, "movement_amplitude": { - "name": "movement_amplitude", + "name": "运动幅度", "tooltip": "画面中对象的运动幅度" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于视频生成的文本描述" }, "resolution": { - "name": "resolution", + "name": "分辨率", "tooltip": "支持的值可能因模型和时长而异" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "视频生成的种子值(0 表示随机)" } }, @@ -12076,11 +12385,11 @@ "tooltip": "输出视频的时长(秒)" }, "images": { - "name": "images", + "name": "图像", "tooltip": "用作参考以生成具有一致主体的图像(最多 7 张图像)" }, "model": { - "name": "model", + "name": "模型", "tooltip": "模型名称" }, "movement_amplitude": { @@ -12088,7 +12397,7 @@ "tooltip": "画面中物体的运动幅度" }, "prompt": { - "name": "prompt", + "name": "提示词", "tooltip": "用于视频生成的文本描述" }, "resolution": { @@ -12195,7 +12504,7 @@ } }, "VoxelToMesh": { - "display_name": "VoxelToMesh", + "display_name": "体素到网格", "inputs": { "algorithm": { "name": "算法" @@ -12204,12 +12513,12 @@ "name": "阈值" }, "voxel": { - "name": "voxel" + "name": "体素" } } }, "VoxelToMeshBasic": { - "display_name": "VoxelToMeshBasic", + "display_name": "体素到网格(基础)", "inputs": { "threshold": { "name": "阈值" @@ -12220,7 +12529,7 @@ } }, "Wan22FunControlToVideo": { - "display_name": "Wan22FunControlToVideo", + "display_name": "Wan22FunControl视频", "inputs": { "batch_size": { "name": "批次大小" @@ -12512,7 +12821,7 @@ } }, "WanFirstLastFrameToVideo": { - "display_name": "WanFirstLastFrameToVideo", + "display_name": "Wan首尾帧视频", "inputs": { "batch_size": { "name": "批量大小" @@ -12564,13 +12873,13 @@ } }, "WanFunControlToVideo": { - "display_name": "WanFunControlToVideo", + "display_name": "WanFunControl视频", "inputs": { "batch_size": { "name": "批量大小" }, "clip_vision_output": { - "name": "clip_vision_output" + "name": "CLIP视觉输出" }, "control_video": { "name": "控制视频" @@ -12613,13 +12922,13 @@ } }, "WanFunInpaintToVideo": { - "display_name": "WanFunInpaintToVideo", + "display_name": "WanFunInpaint视频", "inputs": { "batch_size": { "name": "批量大小" }, "clip_vision_output": { - "name": "clip_vision_output" + "name": "CLIP视觉输出" }, "end_image": { "name": "结束图像" @@ -12895,7 +13204,7 @@ } }, "WanSoundImageToVideo": { - "display_name": "WanSoundImageToVideo", + "display_name": "WanSound图像到视频", "inputs": { "audio_encoder_output": { "name": "音频编码器输出" @@ -12947,7 +13256,7 @@ } }, "WanSoundImageToVideoExtend": { - "display_name": "WanSoundImageToVideoExtend", + "display_name": "WanSound图像到视频扩展", "inputs": { "audio_encoder_output": { "name": "音频编码器输出" @@ -13038,18 +13347,18 @@ "display_name": "Wan文生视频", "inputs": { "audio": { - "name": "audio", + "name": "音频", "tooltip": "音频必须包含清晰、响亮的人声,无杂音和背景音乐。" }, "control_after_generate": { - "name": "control after generate" + "name": "生成后控制" }, "duration": { - "name": "duration", + "name": "时长", "tooltip": "可用时长:5秒和10秒" }, "generate_audio": { - "name": "generate_audio", + "name": "生成音频", "tooltip": "若无音频输入,则自动生成音频。" }, "model": { @@ -13057,7 +13366,7 @@ "tooltip": "要使用的模型。" }, "negative_prompt": { - "name": "negative_prompt", + "name": "负面提示词", "tooltip": "用于引导避免内容的负面文本提示。" }, "prompt": { @@ -13065,18 +13374,18 @@ "tooltip": "用于描述元素和视觉特征的提示词,支持英文/中文。" }, "prompt_extend": { - "name": "prompt_extend", + "name": "优化提示词", "tooltip": "是否通过AI辅助增强提示词。" }, "seed": { - "name": "seed", + "name": "随机种", "tooltip": "用于生成的种子值。" }, "size": { - "name": "size" + "name": "尺寸" }, "watermark": { - "name": "watermark", + "name": "水印", "tooltip": "是否在结果中添加“AI生成”水印。" } }, @@ -13087,31 +13396,31 @@ } }, "WanTrackToVideo": { - "display_name": "WanTrackToVideo", + "display_name": "WanTrack视频", "inputs": { "batch_size": { - "name": "batch_size" + "name": "批次大小" }, "clip_vision_output": { - "name": "clip_vision_output" + "name": "CLIP视觉输出" }, "height": { - "name": "height" + "name": "高度" }, "length": { - "name": "length" + "name": "长度" }, "negative": { - "name": "negative" + "name": "负面条件" }, "positive": { - "name": "positive" + "name": "正面条件" }, "start_image": { - "name": "start_image" + "name": "图像" }, "temperature": { - "name": "temperature" + "name": "温度" }, "topk": { "name": "topk" @@ -13123,16 +13432,16 @@ "name": "vae" }, "width": { - "name": "width" + "name": "宽度" } }, "outputs": { "0": { - "name": "positive", + "name": "正面提示词", "tooltip": null }, "1": { - "name": "negative", + "name": "负面提示词", "tooltip": null }, "2": { @@ -13142,13 +13451,13 @@ } }, "WanVaceToVideo": { - "display_name": "WanVaceToVideo", + "display_name": "WanVace视频", "inputs": { "batch_size": { "name": "批量大小" }, "control_masks": { - "name": "控制mask" + "name": "控制遮罩" }, "control_video": { "name": "控制视频" @@ -13160,10 +13469,10 @@ "name": "长度" }, "negative": { - "name": "负向" + "name": "负面条件" }, "positive": { - "name": "正向" + "name": "正面条件" }, "reference_image": { "name": "参考图像" @@ -13240,5 +13549,19 @@ "name": "强度" } } + }, + "wanBlockSwap": { + "description": "NOP", + "display_name": "Wan块置换", + "inputs": { + "model": { + "name": "模型" + } + }, + "outputs": { + "0": { + "tooltip": null + } + } } } diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index 90934b95a..8098393e3 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -57,9 +57,6 @@ "Comfy_ConfirmClear": { "name": "清除工作流时需要确认" }, - "Comfy_DOMClippingEnabled": { - "name": "启用DOM元素裁剪(启用可能会降低性能)" - }, "Comfy_DevMode": { "name": "启用开发模式选项(API保存等)" }, @@ -70,6 +67,9 @@ "Comfy_DisableSliders": { "name": "禁用节点组件滑块" }, + "Comfy_DOMClippingEnabled": { + "name": "启用DOM元素裁剪(启用可能会降低性能)" + }, "Comfy_EditAttention_Delta": { "name": "Ctrl+上/下 精度" }, @@ -79,6 +79,17 @@ "Comfy_EnableWorkflowViewRestore": { "name": "在工作流中保存和恢复视图位置及缩放" }, + "Comfy_Execution_PreviewMethod": { + "name": "实时预览", + "options": { + "auto": "自动", + "default": "默认", + "latent2rgb": "latent2rgb", + "none": "无", + "taesd": "taesd" + }, + "tooltip": "图像生成过程中实时预览。 \"默认\" 使用服务器 CLI 设置。" + }, "Comfy_FloatRoundingPrecision": { "name": "浮点组件四舍五入的小数位数 [0 = 自动]。", "tooltip": "(需要重新加载页面)" @@ -100,15 +111,19 @@ "None": "无" } }, + "Comfy_Graph_LiveSelection": { + "name": "实时选择", + "tooltip": "启用后,在框选拖动时实时选择/取消选择节点,类似于其他设计工具。" + }, "Comfy_Graph_ZoomSpeed": { "name": "画布缩放速度" }, - "Comfy_GroupSelectedNodes_Padding": { - "name": "选定节点的组内边距" - }, "Comfy_Group_DoubleClickTitleToEdit": { "name": "双击组标题以编辑" }, + "Comfy_GroupSelectedNodes_Padding": { + "name": "选定节点的组内边距" + }, "Comfy_LinkRelease_Action": { "name": "释放连线时的操作", "options": { @@ -166,6 +181,15 @@ "name": "光照强度下限", "tooltip": "设置3D场景允许的最小光照强度值。此项定义在调整任何3D控件照明时可设定的最低亮度。" }, + "Comfy_Load3D_PLYEngine": { + "name": "PLY 引擎", + "options": { + "fastply": "fastply", + "sparkjs": "sparkjs", + "threejs": "threejs" + }, + "tooltip": "选择加载 PLY 文件的引擎。 \"threejs\" 使用原生 Three.js PLY 加载器(最适合网格 PLY)。 \"fastply\" 使用专用于 ASCII 点云的 PLY 文件加载器。 \"sparkjs\" 使用 Spark.js 加载 3D 高斯泼溅 PLY 文件。" + }, "Comfy_Load3D_ShowGrid": { "name": "显示网格", "tooltip": "默认显示网格开关" @@ -193,6 +217,38 @@ }, "tooltip": "选择“文件名”以在模型列表中显示原始文件名的简化视图(不带目录和“.safetensors”后缀名)。选择“标题”以显示可配置的模型元数据标题。" }, + "Comfy_Node_AllowImageSizeDraw": { + "name": "在图像预览下方显示宽度×高度" + }, + "Comfy_Node_AutoSnapLinkToSlot": { + "name": "连线自动吸附到节点接口", + "tooltip": "在节点上拖动连线时,连线会自动吸附到节点的可用输入接口。" + }, + "Comfy_Node_BypassAllLinksOnDelete": { + "name": "删除节点时保留连线", + "tooltip": "删除节点时,尝试重新连接其所有输入和输出连线(类似于忽略节点)。" + }, + "Comfy_Node_DoubleClickTitleToEdit": { + "name": "双击节点标题以编辑" + }, + "Comfy_Node_MiddleClickRerouteNode": { + "name": "中键单击创建新的转接点" + }, + "Comfy_Node_Opacity": { + "name": "节点不透明度" + }, + "Comfy_Node_ShowDeprecated": { + "name": "在搜索中显示已弃用的节点", + "tooltip": "弃用节点在UI中默认隐藏,但在工作流中仍然有效。" + }, + "Comfy_Node_ShowExperimental": { + "name": "在搜索中显示实验性节点", + "tooltip": "实验节点在UI中标记为实验性,可能在未来版本中发生重大变化或被移除。在生产工作流中谨慎使用。" + }, + "Comfy_Node_SnapHighlightsNode": { + "name": "吸附高亮节点", + "tooltip": "在拖动连线经过具有可用输入接口的节点时,高亮显示该节点。" + }, "Comfy_NodeBadge_NodeIdBadgeMode": { "name": "节点ID标签", "options": { @@ -245,38 +301,6 @@ "name": "节点建议数量", "tooltip": "仅适用于 litegraph" }, - "Comfy_Node_AllowImageSizeDraw": { - "name": "在图像预览下方显示宽度×高度" - }, - "Comfy_Node_AutoSnapLinkToSlot": { - "name": "连线自动吸附到节点接口", - "tooltip": "在节点上拖动连线时,连线会自动吸附到节点的可用输入接口。" - }, - "Comfy_Node_BypassAllLinksOnDelete": { - "name": "删除节点时保留连线", - "tooltip": "删除节点时,尝试重新连接其所有输入和输出连线(类似于忽略节点)。" - }, - "Comfy_Node_DoubleClickTitleToEdit": { - "name": "双击节点标题以编辑" - }, - "Comfy_Node_MiddleClickRerouteNode": { - "name": "中键单击创建新的转接点" - }, - "Comfy_Node_Opacity": { - "name": "节点不透明度" - }, - "Comfy_Node_ShowDeprecated": { - "name": "在搜索中显示已弃用的节点", - "tooltip": "弃用节点在UI中默认隐藏,但在工作流中仍然有效。" - }, - "Comfy_Node_ShowExperimental": { - "name": "在搜索中显示实验性节点", - "tooltip": "实验节点在UI中标记为实验性,可能在未来版本中发生重大变化或被移除。在生产工作流中谨慎使用。" - }, - "Comfy_Node_SnapHighlightsNode": { - "name": "吸附高亮节点", - "tooltip": "在拖动连线经过具有可用输入接口的节点时,高亮显示该节点。" - }, "Comfy_Notification_ShowVersionUpdates": { "name": "显示版本更新", "tooltip": "显示新模型和主要新功能的更新。" @@ -300,14 +324,14 @@ "Comfy_PromptFilename": { "name": "保存工作流时提示文件名" }, - "Comfy_QueueButton_BatchCountLimit": { - "name": "批处理计数限制", - "tooltip": "单次添加到队列的最大任务数量" - }, "Comfy_Queue_MaxHistoryItems": { "name": "队列历史大小", "tooltip": "队列历史中显示的最大任务数量。" }, + "Comfy_QueueButton_BatchCountLimit": { + "name": "批处理计数限制", + "tooltip": "单次添加到队列的最大任务数量" + }, "Comfy_Sidebar_Location": { "name": "侧边栏位置", "options": { @@ -436,4 +460,4 @@ "pysssss_SnapToGrid": { "name": "始终吸附到网格" } -} +} \ No newline at end of file diff --git a/src/platform/assets/components/UploadModelDialog.vue b/src/platform/assets/components/UploadModelDialog.vue index 5d1e6cdde..d6be9e97e 100644 --- a/src/platform/assets/components/UploadModelDialog.vue +++ b/src/platform/assets/components/UploadModelDialog.vue @@ -4,7 +4,13 @@ > + @@ -46,14 +52,17 @@ diff --git a/src/platform/assets/components/UploadModelFooter.vue b/src/platform/assets/components/UploadModelFooter.vue index 7fadaff1f..607484d7c 100644 --- a/src/platform/assets/components/UploadModelFooter.vue +++ b/src/platform/assets/components/UploadModelFooter.vue @@ -1,12 +1,34 @@ @@ -78,9 +105,13 @@ import { ref } from 'vue' import Button from '@/components/ui/button/Button.vue' +import { useFeatureFlags } from '@/composables/useFeatureFlags' import VideoHelpDialog from '@/platform/assets/components/VideoHelpDialog.vue' -const showVideoHelp = ref(false) +const { flags } = useFeatureFlags() + +const showCivitaiHelp = ref(false) +const showHuggingFaceHelp = ref(false) defineProps<{ currentStep: number diff --git a/src/platform/assets/components/UploadModelUrlInput.vue b/src/platform/assets/components/UploadModelUrlInput.vue index c7e94a8be..635d28c02 100644 --- a/src/platform/assets/components/UploadModelUrlInput.vue +++ b/src/platform/assets/components/UploadModelUrlInput.vue @@ -1,28 +1,74 @@ @@ -44,4 +90,9 @@ const url = computed({ get: () => props.modelValue, set: (value: string) => emit('update:modelValue', value) }) + +const civitaiIcon = '/assets/images/civitai.svg' +const civitaiUrl = 'https://civitai.com/models' +const huggingFaceIcon = '/assets/images/hf-logo.svg' +const huggingFaceUrl = 'https://huggingface.co' diff --git a/src/platform/assets/components/UploadModelUrlInputCivitai.vue b/src/platform/assets/components/UploadModelUrlInputCivitai.vue new file mode 100644 index 000000000..b89ee5329 --- /dev/null +++ b/src/platform/assets/components/UploadModelUrlInputCivitai.vue @@ -0,0 +1,82 @@ + + + diff --git a/src/platform/assets/composables/useModelTypes.ts b/src/platform/assets/composables/useModelTypes.ts index 12aa8d419..8f578c926 100644 --- a/src/platform/assets/composables/useModelTypes.ts +++ b/src/platform/assets/composables/useModelTypes.ts @@ -50,10 +50,12 @@ export const useModelTypes = createSharedComposable(() => { } = useAsyncState( async (): Promise => { const response = await api.getModelFolders() - return response.map((folder) => ({ - name: formatDisplayName(folder.name), - value: folder.name - })) + return response + .map((folder) => ({ + name: formatDisplayName(folder.name), + value: folder.name + })) + .sort((a, b) => a.name.localeCompare(b.name)) }, [] as ModelTypeOption[], { diff --git a/src/platform/assets/composables/useUploadModelWizard.ts b/src/platform/assets/composables/useUploadModelWizard.ts index 73341c0f2..2d97efd9c 100644 --- a/src/platform/assets/composables/useUploadModelWizard.ts +++ b/src/platform/assets/composables/useUploadModelWizard.ts @@ -1,9 +1,15 @@ import type { Ref } from 'vue' import { computed, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' +import { useFeatureFlags } from '@/composables/useFeatureFlags' import { st } from '@/i18n' +import { civitaiImportSource } from '@/platform/assets/importSources/civitaiImportSource' +import { huggingfaceImportSource } from '@/platform/assets/importSources/huggingfaceImportSource' import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema' import { assetService } from '@/platform/assets/services/assetService' +import type { ImportSource } from '@/platform/assets/types/importSource' +import { validateSourceUrl } from '@/platform/assets/utils/importSourceUtil' import { useAssetsStore } from '@/stores/assetsStore' import { useModelToNodeStore } from '@/stores/modelToNodeStore' @@ -21,8 +27,10 @@ interface ModelTypeOption { } export function useUploadModelWizard(modelTypes: Ref) { + const { t } = useI18n() const assetsStore = useAssetsStore() const modelToNodeStore = useModelToNodeStore() + const { flags } = useFeatureFlags() const currentStep = ref(1) const isFetchingMetadata = ref(false) const isUploading = ref(false) @@ -37,6 +45,20 @@ export function useUploadModelWizard(modelTypes: Ref) { const selectedModelType = ref() + // Available import sources + const importSources: ImportSource[] = flags.huggingfaceModelImportEnabled + ? [civitaiImportSource, huggingfaceImportSource] + : [civitaiImportSource] + + // Detected import source based on URL + const detectedSource = computed(() => { + const url = wizardData.value.url.trim() + if (!url) return null + return ( + importSources.find((source) => validateSourceUrl(url, source)) ?? null + ) + }) + // Clear error when URL changes watch( () => wizardData.value.url, @@ -54,15 +76,6 @@ export function useUploadModelWizard(modelTypes: Ref) { return !!selectedModelType.value }) - function isCivitaiUrl(url: string): boolean { - try { - const hostname = new URL(url).hostname.toLowerCase() - return hostname === 'civitai.com' || hostname.endsWith('.civitai.com') - } catch { - return false - } - } - async function fetchMetadata() { if (!canFetchMetadata.value) return @@ -75,17 +88,36 @@ export function useUploadModelWizard(modelTypes: Ref) { } wizardData.value.url = cleanedUrl - if (!isCivitaiUrl(wizardData.value.url)) { - uploadError.value = st( - 'assetBrowser.onlyCivitaiUrlsSupported', - 'Only Civitai URLs are supported' - ) + // Validate URL belongs to a supported import source + const source = detectedSource.value + if (!source) { + const supportedSources = importSources.map((s) => s.name).join(', ') + uploadError.value = t('assetBrowser.unsupportedUrlSource', { + sources: supportedSources + }) return } isFetchingMetadata.value = true try { const metadata = await assetService.getAssetMetadata(wizardData.value.url) + + // Decode URL-encoded filenames (e.g., Chinese characters) + if (metadata.filename) { + try { + metadata.filename = decodeURIComponent(metadata.filename) + } catch { + // Keep original if decoding fails + } + } + if (metadata.name) { + try { + metadata.name = decodeURIComponent(metadata.name) + } catch { + // Keep original if decoding fails + } + } + wizardData.value.metadata = metadata // Pre-fill name from metadata @@ -125,6 +157,14 @@ export function useUploadModelWizard(modelTypes: Ref) { async function uploadModel() { if (!canUploadModel.value) return + // Defensive check: detectedSource should be valid after fetchMetadata validation, + // but guard against edge cases (e.g., URL modified between steps) + const source = detectedSource.value + if (!source) { + uploadError.value = t('assetBrowser.noValidSourceDetected') + return false + } + isUploading.value = true uploadStatus.value = 'uploading' @@ -170,7 +210,7 @@ export function useUploadModelWizard(modelTypes: Ref) { name: filename, tags, user_metadata: { - source: 'civitai', + source: source.type, source_url: wizardData.value.url, model_type: selectedModelType.value }, @@ -224,6 +264,7 @@ export function useUploadModelWizard(modelTypes: Ref) { // Computed canFetchMetadata, canUploadModel, + detectedSource, // Actions fetchMetadata, diff --git a/src/platform/assets/importSources/civitaiImportSource.ts b/src/platform/assets/importSources/civitaiImportSource.ts new file mode 100644 index 000000000..5ff324d00 --- /dev/null +++ b/src/platform/assets/importSources/civitaiImportSource.ts @@ -0,0 +1,10 @@ +import type { ImportSource } from '@/platform/assets/types/importSource' + +/** + * Civitai model import source configuration + */ +export const civitaiImportSource: ImportSource = { + type: 'civitai', + name: 'Civitai', + hostnames: ['civitai.com'] +} diff --git a/src/platform/assets/importSources/huggingfaceImportSource.ts b/src/platform/assets/importSources/huggingfaceImportSource.ts new file mode 100644 index 000000000..310e170af --- /dev/null +++ b/src/platform/assets/importSources/huggingfaceImportSource.ts @@ -0,0 +1,10 @@ +import type { ImportSource } from '@/platform/assets/types/importSource' + +/** + * Hugging Face model import source configuration + */ +export const huggingfaceImportSource: ImportSource = { + type: 'huggingface', + name: 'Hugging Face', + hostnames: ['huggingface.co'] +} diff --git a/src/platform/assets/types/importSource.ts b/src/platform/assets/types/importSource.ts new file mode 100644 index 000000000..12aa5e3db --- /dev/null +++ b/src/platform/assets/types/importSource.ts @@ -0,0 +1,24 @@ +/** + * Supported model import sources + */ +type ImportSourceType = 'civitai' | 'huggingface' + +/** + * Configuration for a model import source + */ +export interface ImportSource { + /** + * Unique identifier for this import source + */ + readonly type: ImportSourceType + + /** + * Display name for the source + */ + readonly name: string + + /** + * Hostname(s) that identify this source + */ + readonly hostnames: readonly string[] +} diff --git a/src/platform/assets/utils/importSourceUtil.ts b/src/platform/assets/utils/importSourceUtil.ts new file mode 100644 index 000000000..2628593cc --- /dev/null +++ b/src/platform/assets/utils/importSourceUtil.ts @@ -0,0 +1,15 @@ +import type { ImportSource } from '@/platform/assets/types/importSource' + +/** + * Check if a URL belongs to a specific import source + */ +export function validateSourceUrl(url: string, source: ImportSource): boolean { + try { + const hostname = new URL(url).hostname.toLowerCase() + return source.hostnames.some( + (h) => hostname === h || hostname.endsWith(`.${h}`) + ) + } catch { + return false + } +} diff --git a/src/platform/cloud/subscription/components/PricingTable.vue b/src/platform/cloud/subscription/components/PricingTable.vue index ea9266304..82ec83c97 100644 --- a/src/platform/cloud/subscription/components/PricingTable.vue +++ b/src/platform/cloud/subscription/components/PricingTable.vue @@ -333,7 +333,7 @@ const { n } = useI18n() const { getAuthHeader } = useFirebaseAuthStore() const { isActiveSubscription, subscriptionTier, isYearlySubscription } = useSubscription() -const { reportError } = useFirebaseAuthActions() +const { accessBillingPortal, reportError } = useFirebaseAuthActions() const { wrapWithErrorHandlingAsync } = useErrorHandling() const isLoading = ref(false) @@ -443,9 +443,15 @@ const handleSubscribe = wrapWithErrorHandlingAsync( loadingTier.value = tierKey try { - const response = await initiateCheckout(tierKey) - if (response.checkout_url) { - window.open(response.checkout_url, '_blank') + if (isActiveSubscription.value) { + // Pass the target tier to create a deep link to subscription update confirmation + const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle.value) + await accessBillingPortal(checkoutTier) + } else { + const response = await initiateCheckout(tierKey) + if (response.checkout_url) { + window.open(response.checkout_url, '_blank') + } } } finally { isLoading.value = false diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index 1a4ef1261..cbca526bf 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -38,4 +38,5 @@ export type RemoteConfig = { asset_update_options_enabled?: boolean private_models_enabled?: boolean onboarding_survey_enabled?: boolean + huggingface_model_import_enabled?: boolean } diff --git a/src/renderer/core/layout/sync/useLayoutSync.ts b/src/renderer/core/layout/sync/useLayoutSync.ts index b51aeafee..221fe64eb 100644 --- a/src/renderer/core/layout/sync/useLayoutSync.ts +++ b/src/renderer/core/layout/sync/useLayoutSync.ts @@ -8,7 +8,6 @@ import { onUnmounted, ref } from 'vue' import type { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import { addNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil' /** * Composable for syncing LiteGraph with the Layout system @@ -44,13 +43,15 @@ export function useLayoutSync() { liteNode.pos[1] = layout.position.y } - const targetHeight = addNodeTitleHeight(layout.size.height) + // Note: layout.size.height is the content height without title. + // LiteGraph's measure() will add titleHeight to get boundingRect. + // Do NOT use addNodeTitleHeight here - that would double-count the title. if ( liteNode.size[0] !== layout.size.width || - liteNode.size[1] !== targetHeight + liteNode.size[1] !== layout.size.height ) { // Use setSize() to trigger onResize callback - liteNode.setSize([layout.size.width, targetHeight]) + liteNode.setSize([layout.size.width, layout.size.height]) } } diff --git a/src/renderer/core/layout/utils/nodeSizeUtil.ts b/src/renderer/core/layout/utils/nodeSizeUtil.ts index 811240cd0..ce2691a92 100644 --- a/src/renderer/core/layout/utils/nodeSizeUtil.ts +++ b/src/renderer/core/layout/utils/nodeSizeUtil.ts @@ -2,6 +2,3 @@ import { LiteGraph } from '@/lib/litegraph/src/litegraph' export const removeNodeTitleHeight = (height: number) => Math.max(0, height - (LiteGraph.NODE_TITLE_HEIGHT || 0)) - -export const addNodeTitleHeight = (height: number) => - height + LiteGraph.NODE_TITLE_HEIGHT diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue index 9205768e6..43aa36ad6 100644 --- a/src/renderer/extensions/vueNodes/components/InputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue @@ -43,7 +43,12 @@ ) " > - {{ slotData.localized_name || slotData.name || `Input ${index}` }} + {{ + slotData.label || + slotData.localized_name || + slotData.name || + `Input ${index}` + }} diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts index 847ffc657..e1eb34e1b 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts @@ -1,6 +1,4 @@ import { mount } from '@vue/test-utils' -import PrimeVue from 'primevue/config' -import ImageCompare from 'primevue/imagecompare' import { describe, expect, it } from 'vitest' import type { SimplifiedWidget } from '@/types/simplifiedWidget' @@ -25,8 +23,9 @@ describe('WidgetImageCompare Display', () => { ) => { return mount(WidgetImageCompare, { global: { - plugins: [PrimeVue], - components: { ImageCompare } + mocks: { + $t: (key: string) => key + } }, props: { widget, @@ -36,7 +35,7 @@ describe('WidgetImageCompare Display', () => { } describe('Component Rendering', () => { - it('renders imagecompare component with proper structure and styling', () => { + it('renders with proper structure and styling when images are provided', () => { const value: ImageCompareValue = { before: 'https://example.com/before.jpg', after: 'https://example.com/after.jpg' @@ -44,21 +43,15 @@ describe('WidgetImageCompare Display', () => { const widget = createMockWidget(value) const wrapper = mountComponent(widget) - // Component exists - const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) - expect(imageCompare.exists()).toBe(true) - - // Renders both images with correct URLs const images = wrapper.findAll('img') expect(images).toHaveLength(2) - expect(images[0].attributes('src')).toBe('https://example.com/before.jpg') - expect(images[1].attributes('src')).toBe('https://example.com/after.jpg') - // Images have proper styling classes + // In the new implementation: after image is first (background), before image is second (overlay) + expect(images[0].attributes('src')).toBe('https://example.com/after.jpg') + expect(images[1].attributes('src')).toBe('https://example.com/before.jpg') + images.forEach((img) => { - expect(img.classes()).toContain('object-cover') - expect(img.classes()).toContain('w-full') - expect(img.classes()).toContain('h-full') + expect(img.classes()).toContain('object-contain') }) }) }) @@ -74,8 +67,9 @@ describe('WidgetImageCompare Display', () => { } const customWrapper = mountComponent(createMockWidget(customAltValue)) const customImages = customWrapper.findAll('img') - expect(customImages[0].attributes('alt')).toBe('Original design') - expect(customImages[1].attributes('alt')).toBe('Updated design') + // DOM order: [after, before] + expect(customImages[0].attributes('alt')).toBe('Updated design') + expect(customImages[1].attributes('alt')).toBe('Original design') // Test default alt text const defaultAltValue: ImageCompareValue = { @@ -84,8 +78,8 @@ describe('WidgetImageCompare Display', () => { } const defaultWrapper = mountComponent(createMockWidget(defaultAltValue)) const defaultImages = defaultWrapper.findAll('img') - expect(defaultImages[0].attributes('alt')).toBe('Before image') - expect(defaultImages[1].attributes('alt')).toBe('After image') + expect(defaultImages[0].attributes('alt')).toBe('After image') + expect(defaultImages[1].attributes('alt')).toBe('Before image') // Test empty string alt text (falls back to default) const emptyAltValue: ImageCompareValue = { @@ -96,29 +90,36 @@ describe('WidgetImageCompare Display', () => { } const emptyWrapper = mountComponent(createMockWidget(emptyAltValue)) const emptyImages = emptyWrapper.findAll('img') - expect(emptyImages[0].attributes('alt')).toBe('Before image') - expect(emptyImages[1].attributes('alt')).toBe('After image') + expect(emptyImages[0].attributes('alt')).toBe('After image') + expect(emptyImages[1].attributes('alt')).toBe('Before image') }) - it('handles missing and partial image URLs gracefully', () => { - // Missing URLs - const missingValue: ImageCompareValue = { before: '', after: '' } - const missingWrapper = mountComponent(createMockWidget(missingValue)) - const missingImages = missingWrapper.findAll('img') - expect(missingImages[0].attributes('src')).toBe('') - expect(missingImages[1].attributes('src')).toBe('') - - // Partial URLs - const partialValue: ImageCompareValue = { + it('handles partial image URLs gracefully', () => { + // Only before image provided + const beforeOnlyValue: ImageCompareValue = { before: 'https://example.com/before.jpg', after: '' } - const partialWrapper = mountComponent(createMockWidget(partialValue)) - const partialImages = partialWrapper.findAll('img') - expect(partialImages[0].attributes('src')).toBe( + const beforeOnlyWrapper = mountComponent( + createMockWidget(beforeOnlyValue) + ) + const beforeOnlyImages = beforeOnlyWrapper.findAll('img') + expect(beforeOnlyImages).toHaveLength(1) + expect(beforeOnlyImages[0].attributes('src')).toBe( 'https://example.com/before.jpg' ) - expect(partialImages[1].attributes('src')).toBe('') + + // Only after image provided + const afterOnlyValue: ImageCompareValue = { + before: '', + after: 'https://example.com/after.jpg' + } + const afterOnlyWrapper = mountComponent(createMockWidget(afterOnlyValue)) + const afterOnlyImages = afterOnlyWrapper.findAll('img') + expect(afterOnlyImages).toHaveLength(1) + expect(afterOnlyImages[0].attributes('src')).toBe( + 'https://example.com/after.jpg' + ) }) }) @@ -129,75 +130,14 @@ describe('WidgetImageCompare Display', () => { const wrapper = mountComponent(widget) const images = wrapper.findAll('img') + expect(images).toHaveLength(1) expect(images[0].attributes('src')).toBe('https://example.com/single.jpg') - expect(images[1].attributes('src')).toBe('') - }) - - it('uses default alt text for string values', () => { - const value = 'https://example.com/single.jpg' - const widget = createMockWidget(value) - const wrapper = mountComponent(widget) - - const images = wrapper.findAll('img') expect(images[0].attributes('alt')).toBe('Before image') - expect(images[1].attributes('alt')).toBe('After image') - }) - }) - - describe('Widget Options Handling', () => { - it('passes through accessibility options', () => { - const value: ImageCompareValue = { - before: 'https://example.com/before.jpg', - after: 'https://example.com/after.jpg' - } - const widget = createMockWidget(value, { - tabindex: 1, - ariaLabel: 'Compare images', - ariaLabelledby: 'compare-label' - }) - const wrapper = mountComponent(widget) - - const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) - expect(imageCompare.props('tabindex')).toBe(1) - expect(imageCompare.props('ariaLabel')).toBe('Compare images') - expect(imageCompare.props('ariaLabelledby')).toBe('compare-label') - }) - - it('uses default tabindex when not provided', () => { - const value: ImageCompareValue = { - before: 'https://example.com/before.jpg', - after: 'https://example.com/after.jpg' - } - const widget = createMockWidget(value) - const wrapper = mountComponent(widget) - - const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) - expect(imageCompare.props('tabindex')).toBe(0) - }) - - it('passes through PrimeVue specific options', () => { - const value: ImageCompareValue = { - before: 'https://example.com/before.jpg', - after: 'https://example.com/after.jpg' - } - const widget = createMockWidget(value, { - unstyled: true, - pt: { root: { class: 'custom-class' } }, - ptOptions: { mergeSections: true } - }) - const wrapper = mountComponent(widget) - - const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) - expect(imageCompare.props('unstyled')).toBe(true) - expect(imageCompare.props('pt')).toEqual({ - root: { class: 'custom-class' } - }) - expect(imageCompare.props('ptOptions')).toEqual({ mergeSections: true }) }) }) describe('Readonly Mode', () => { - it('renders normally in readonly mode (no interaction restrictions)', () => { + it('renders normally in readonly mode', () => { const value: ImageCompareValue = { before: 'https://example.com/before.jpg', after: 'https://example.com/after.jpg' @@ -205,45 +145,39 @@ describe('WidgetImageCompare Display', () => { const widget = createMockWidget(value) const wrapper = mountComponent(widget, true) - // ImageCompare is display-only, readonly doesn't affect rendering - const imageCompare = wrapper.findComponent({ name: 'ImageCompare' }) - expect(imageCompare.exists()).toBe(true) - const images = wrapper.findAll('img') expect(images).toHaveLength(2) }) }) describe('Edge Cases', () => { - it('handles null or undefined widget value', () => { + it('shows no images message when widget value is empty string', () => { const widget = createMockWidget('') const wrapper = mountComponent(widget) const images = wrapper.findAll('img') - expect(images[0].attributes('src')).toBe('') - expect(images[1].attributes('src')).toBe('') - expect(images[0].attributes('alt')).toBe('Before image') - expect(images[1].attributes('alt')).toBe('After image') + expect(images).toHaveLength(0) + expect(wrapper.text()).toContain('imageCompare.noImages') }) - it('handles empty object value', () => { + it('shows no images message when both URLs are empty', () => { + const value: ImageCompareValue = { before: '', after: '' } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const images = wrapper.findAll('img') + expect(images).toHaveLength(0) + expect(wrapper.text()).toContain('imageCompare.noImages') + }) + + it('shows no images message for empty object value', () => { const value: ImageCompareValue = {} as ImageCompareValue const widget = createMockWidget(value) const wrapper = mountComponent(widget) const images = wrapper.findAll('img') - expect(images[0].attributes('src')).toBe('') - expect(images[1].attributes('src')).toBe('') - }) - - it('handles malformed object value', () => { - const value = { randomProp: 'test', before: '', after: '' } - const widget = createMockWidget(value) - const wrapper = mountComponent(widget) - - const images = wrapper.findAll('img') - expect(images[0].attributes('src')).toBe('') - expect(images[1].attributes('src')).toBe('') + expect(images).toHaveLength(0) + expect(wrapper.text()).toContain('imageCompare.noImages') }) it('handles special content - long URLs, special characters, and long alt text', () => { @@ -290,7 +224,7 @@ describe('WidgetImageCompare Display', () => { }) describe('Template Structure', () => { - it('correctly assigns images to left and right template slots', () => { + it('correctly renders after image as background and before image as overlay', () => { const value: ImageCompareValue = { before: 'https://example.com/before.jpg', after: 'https://example.com/after.jpg' @@ -299,10 +233,11 @@ describe('WidgetImageCompare Display', () => { const wrapper = mountComponent(widget) const images = wrapper.findAll('img') - // First image (before) should be in left template slot - expect(images[0].attributes('src')).toBe('https://example.com/before.jpg') - // Second image (after) should be in right template slot - expect(images[1].attributes('src')).toBe('https://example.com/after.jpg') + // After image is rendered first as background + expect(images[0].attributes('src')).toBe('https://example.com/after.jpg') + // Before image is rendered second as overlay with clipPath + expect(images[1].attributes('src')).toBe('https://example.com/before.jpg') + expect(images[1].classes()).toContain('absolute') }) }) @@ -333,4 +268,27 @@ describe('WidgetImageCompare Display', () => { expect(blobUrlImages[1].attributes('src')).toBe(blobUrl) }) }) + + describe('Slider Element', () => { + it('renders slider divider when images are present', () => { + const value: ImageCompareValue = { + before: 'https://example.com/before.jpg', + after: 'https://example.com/after.jpg' + } + const widget = createMockWidget(value) + const wrapper = mountComponent(widget) + + const slider = wrapper.find('[role="presentation"]') + expect(slider.exists()).toBe(true) + expect(slider.classes()).toContain('bg-white') + }) + + it('does not render slider when no images', () => { + const widget = createMockWidget('') + const wrapper = mountComponent(widget) + + const slider = wrapper.find('[role="presentation"]') + expect(slider.exists()).toBe(false) + }) + }) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue index 34516a120..8cb80341a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue @@ -1,32 +1,39 @@