Compare commits

..

1 Commits

Author SHA1 Message Date
John Haugeland
bcc2c51df1 feat: add executionIdToNodeLocatorId call counter behind feature flag
Add dev-only instrumentation to count how many times
executionIdToNodeLocatorId is called per execution run. This
provides a baseline measurement for evaluating the impact of
caching this function's results, in a later PR.

Gated behind the feature flag
"ff:expose_executionId_to_node_locator_id_cache_counters"
(localStorage). Uses the existing getDevOverride utility which
is tree-shaken from production builds via import.meta.env.DEV.

Enable in browser console:
  localStorage.setItem(
    'ff:expose_executionId_to_node_locator_id_cache_counters',
    'true'
  )

On execution completion, logs the total call count to
console.warn. No reload needed to toggle.

**feat**: Allow measurement of caching impact, behind a flag

**impact**: None unless activated, then just some logging

There isn't one.  I don't have access to AmpCode or Unito.
2026-02-25 23:10:11 -08:00
140 changed files with 1037 additions and 5142 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -22,9 +22,7 @@ const extraFileExtensions = ['.vue']
const commonGlobals = {
...globals.browser,
__COMFYUI_FRONTEND_VERSION__: 'readonly',
__DISTRIBUTION__: 'readonly',
__IS_NIGHTLY__: 'readonly'
__COMFYUI_FRONTEND_VERSION__: 'readonly'
} as const
const settings = {

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.41.7",
"version": "1.42.0",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -51,6 +51,7 @@ onMounted(() => {
// See: https://vite.dev/guide/build#load-error-handling
window.addEventListener('vite:preloadError', (event) => {
event.preventDefault()
// eslint-disable-next-line no-undef
if (__DISTRIBUTION__ === 'cloud') {
captureException(event.payload, {
tags: { error_type: 'vite_preload_error' }

View File

@@ -18,7 +18,7 @@
<Splitter
:key="splitterRefreshKey"
class="bg-transparent pointer-events-none border-none flex-1 overflow-hidden"
:state-key="isSelectMode ? 'builder-splitter' : sidebarStateKey"
:state-key="sidebarStateKey"
state-storage="local"
@resizestart="onResizestart"
>
@@ -35,10 +35,8 @@
)
: 'bg-comfy-menu-bg pointer-events-auto'
"
:min-size="
sidebarLocation === 'left' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE
"
:size="SIDE_PANEL_SIZE"
:min-size="sidebarLocation === 'left' ? 10 : 15"
:size="20"
:style="firstPanelStyle"
:role="sidebarLocation === 'left' ? 'complementary' : undefined"
:aria-label="
@@ -56,7 +54,7 @@
</SplitterPanel>
<!-- Main panel (always present) -->
<SplitterPanel :size="CENTER_PANEL_SIZE" class="flex flex-col">
<SplitterPanel :size="80" class="flex flex-col">
<slot name="topmenu" :sidebar-panel-visible />
<Splitter
@@ -97,10 +95,8 @@
)
: 'bg-comfy-menu-bg pointer-events-auto'
"
:min-size="
sidebarLocation === 'right' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE
"
:size="SIDE_PANEL_SIZE"
:min-size="sidebarLocation === 'right' ? 10 : 15"
:size="20"
:style="lastPanelStyle"
:role="sidebarLocation === 'right' ? 'complementary' : undefined"
:aria-label="
@@ -127,14 +123,8 @@ import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppMode } from '@/composables/useAppMode'
import {
BUILDER_MIN_SIZE,
CENTER_PANEL_SIZE,
SIDEBAR_MIN_SIZE,
SIDE_PANEL_SIZE
} from '@/constants/splitterConstants'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useAppModeStore } from '@/stores/appModeStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
@@ -155,12 +145,12 @@ const unifiedWidth = computed(() =>
const { focusMode } = storeToRefs(workspaceStore)
const { isSelectMode } = useAppMode()
const appModeStore = useAppModeStore()
const { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)
const { bottomPanelVisible } = storeToRefs(useBottomPanelStore())
const { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)
const showOffsideSplitter = computed(
() => rightSidePanelVisible.value || isSelectMode.value
() => rightSidePanelVisible.value || appModeStore.mode === 'builder:select'
)
const sidebarPanelVisible = computed(() => activeSidebarTab.value !== null)
@@ -184,7 +174,7 @@ function onResizestart({ originalEvent: event }: SplitterResizeStartEvent) {
* to recalculate the width and panel order
*/
const splitterRefreshKey = computed(() => {
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}${isSelectMode.value ? '-builder' : ''}-${sidebarLocation.value}`
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`
})
const firstPanelStyle = computed(() => {

View File

@@ -56,6 +56,43 @@
:queue-overlay-expanded="isQueueOverlayExpanded"
@update:progress-target="updateProgressTarget"
/>
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
type="destructive"
size="md"
:aria-pressed="
isQueuePanelV2Enabled
? activeSidebarTabId === 'job-history'
: isQueueProgressOverlayEnabled
? isQueueOverlayExpanded
: undefined
"
class="relative px-3"
data-testid="queue-overlay-toggle"
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<StatusBadge
v-if="activeJobsCount > 0"
data-testid="active-jobs-indicator"
variant="dot"
class="pointer-events-none absolute -top-0.5 -right-0.5 animate-pulse"
/>
<span class="sr-only">
{{
isQueuePanelV2Enabled
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
}}
</span>
</Button>
<ContextMenu
ref="queueContextMenu"
:model="queueContextMenuItems"
/>
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
@@ -111,11 +148,14 @@
<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
import QueueNotificationBannerHost from '@/components/queue/QueueNotificationBannerHost.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
@@ -129,9 +169,12 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useQueueUIStore } from '@/stores/queueStore'
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isDesktop } from '@/platform/distribution/types'
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
@@ -144,11 +187,17 @@ const workspaceStore = useWorkspaceStore()
const rightSidePanelStore = useRightSidePanelStore()
const managerState = useManagerState()
const { isLoggedIn } = useCurrentUser()
const { t } = useI18n()
const { t, n } = useI18n()
const { toastErrorHandler } = useErrorHandling()
const commandStore = useCommandStore()
const queueStore = useQueueStore()
const executionStore = useExecutionStore()
const executionErrorStore = useExecutionErrorStore()
const queueUIStore = useQueueUIStore()
const sidebarTabStore = useSidebarTabStore()
const { activeJobsCount } = storeToRefs(queueStore)
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
const { shouldShowRedDot: shouldShowConflictRedDot } =
useConflictAcknowledgment()
const isTopMenuHovered = ref(false)
@@ -161,6 +210,14 @@ const isActionbarEnabled = computed(
const isActionbarFloating = computed(
() => isActionbarEnabled.value && !isActionbarDocked.value
)
const activeJobsLabel = computed(() => {
const count = activeJobsCount.value
return t(
'sideToolbar.queueProgressOverlay.activeJobsShort',
{ count: n(count) },
count
)
})
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
)
@@ -189,9 +246,24 @@ const inlineProgressSummaryTarget = computed(() => {
const shouldHideInlineProgressSummary = computed(
() => isQueueProgressOverlayEnabled.value && isQueueOverlayExpanded.value
)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
)
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.manageExtensions'))
)
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
const queueContextMenuItems = computed<MenuItem[]>(() => [
{
label: t('sideToolbar.queueProgressOverlay.clearQueueTooltip'),
icon: 'icon-[lucide--list-x] text-destructive-background',
class: '*:text-destructive-background',
disabled: queueStore.pendingTasks.length === 0,
command: () => {
void handleClearQueue()
}
}
])
const shouldShowRedDot = computed((): boolean => {
return shouldShowConflictRedDot.value
@@ -214,6 +286,27 @@ onMounted(() => {
}
})
const toggleQueueOverlay = () => {
if (isQueuePanelV2Enabled.value) {
sidebarTabStore.toggleSidebarTab('job-history')
return
}
commandStore.execute('Comfy.Queue.ToggleOverlay')
}
const showQueueContextMenu = (event: MouseEvent) => {
queueContextMenu.value?.show(event)
}
const handleClearQueue = async () => {
const pendingJobIds = queueStore.pendingTasks
.map((task) => task.jobId)
.filter((id): id is string => typeof id === 'string' && id.length > 0)
await commandStore.execute('Comfy.ClearPendingTasks')
executionStore.clearInitializationByJobIds(pendingJobIds)
}
const openCustomNodeManager = async () => {
try {
await managerState.openManager({

View File

@@ -42,38 +42,6 @@
>
<i class="icon-[lucide--x] size-4" />
</Button>
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
variant="secondary"
size="md"
:aria-pressed="
isQueuePanelV2Enabled
? activeSidebarTabId === 'job-history'
: queueOverlayExpanded
"
class="relative px-3"
data-testid="queue-overlay-toggle"
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<StatusBadge
v-if="activeJobsCount > 0"
data-testid="active-jobs-indicator"
variant="dot"
class="pointer-events-none absolute -top-0.5 -right-0.5 animate-pulse"
/>
<span class="sr-only">
{{
isQueuePanelV2Enabled
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
}}
</span>
</Button>
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" />
</div>
</Panel>
@@ -97,14 +65,11 @@ import {
} from '@vueuse/core'
import { clamp } from 'es-toolkit/compat'
import { storeToRefs } from 'pinia'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import Panel from 'primevue/panel'
import { computed, nextTick, ref, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import StatusBadge from '@/components/common/StatusBadge.vue'
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
@@ -112,8 +77,6 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton'
@@ -129,13 +92,8 @@ const emit = defineEmits<{
const settingsStore = useSettingStore()
const commandStore = useCommandStore()
const executionStore = useExecutionStore()
const queueStore = useQueueStore()
const sidebarTabStore = useSidebarTabStore()
const { t, n } = useI18n()
const { isIdle: isExecutionIdle } = storeToRefs(executionStore)
const { activeJobsCount } = storeToRefs(queueStore)
const { activeSidebarTabId } = storeToRefs(sidebarTabStore)
const { t } = useI18n()
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
const visible = computed(() => position.value !== 'Disabled')
@@ -360,52 +318,11 @@ watch(isDragging, (dragging) => {
const cancelJobTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.interrupt'))
)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
)
const activeJobsLabel = computed(() => {
const count = activeJobsCount.value
return t(
'sideToolbar.queueProgressOverlay.activeJobsShort',
{ count: n(count) },
count
)
})
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
const queueContextMenuItems = computed<MenuItem[]>(() => [
{
label: t('sideToolbar.queueProgressOverlay.clearQueueTooltip'),
icon: 'icon-[lucide--list-x] text-destructive-background',
class: '*:text-destructive-background',
disabled: queueStore.pendingTasks.length === 0,
command: () => {
void handleClearQueue()
}
}
])
const cancelCurrentJob = async () => {
if (isExecutionIdle.value) return
await commandStore.execute('Comfy.Interrupt')
}
const toggleQueueOverlay = () => {
if (isQueuePanelV2Enabled.value) {
sidebarTabStore.toggleSidebarTab('job-history')
return
}
commandStore.execute('Comfy.Queue.ToggleOverlay')
}
const showQueueContextMenu = (event: MouseEvent) => {
queueContextMenu.value?.show(event)
}
const handleClearQueue = async () => {
const pendingJobIds = queueStore.pendingTasks
.map((task) => task.jobId)
.filter((id): id is string => typeof id === 'string' && id.length > 0)
await commandStore.execute('Comfy.ClearPendingTasks')
executionStore.clearInitializationByJobIds(pendingJobIds)
}
const actionbarClass = computed(() =>
cn(

View File

@@ -1,16 +1,19 @@
<template>
<div class="relative">
<ComfyQueueButton />
<SubscribeToRunButton
v-if="!isActiveSubscription"
class="absolute inset-0 z-10"
/>
</div>
<component
:is="currentButton"
:key="isActiveSubscription ? 'queue' : 'subscribe'"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
const { isActiveSubscription } = useBillingContext()
const currentButton = computed(() =>
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
)
</script>

View File

@@ -8,12 +8,12 @@ import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemp
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { cn } from '@/utils/tailwindUtil'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
const { t } = useI18n()
const commandStore = useCommandStore()
const workspaceStore = useWorkspaceStore()
const { enableAppBuilder, setMode } = useAppMode()
const appModeStore = useAppModeStore()
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
const isAssetsActive = computed(
@@ -24,7 +24,7 @@ const isWorkflowsActive = computed(
)
function enterBuilderMode() {
setMode('builder:select')
appModeStore.setMode('builder:select')
}
function openAssets() {
@@ -61,7 +61,7 @@ function openTemplates() {
</WorkflowActionsDropdown>
<Button
v-if="enableAppBuilder"
v-if="appModeStore.enableAppBuilder"
v-tooltip.right="{
value: t('linearMode.appModeToolbar.appBuilder'),
...tooltipOptions

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import { remove } from 'es-toolkit'
import { computed, provide, ref, toValue, watchEffect } from 'vue'
import { computed, ref, toValue } from 'vue'
import type { MaybeRef } from 'vue'
import { useI18n } from 'vue-i18n'
import DraggableList from '@/components/common/DraggableList.vue'
import IoItem from '@/components/builder/IoItem.vue'
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
import Button from '@/components/ui/button/Button.vue'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
@@ -24,10 +23,8 @@ import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import { app } from '@/scripts/app'
import { DOMWidgetImpl } from '@/scripts/domWidget'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { cn } from '@/utils/tailwindUtil'
import { HideLayoutFieldKey } from '@/types/widgetTypes'
type BoundStyle = { top: string; left: string; width: string; height: string }
@@ -39,36 +36,10 @@ const workflowStore = useWorkflowStore()
const { t } = useI18n()
const canvas: LGraphCanvas = canvasStore.getCanvas()
const { mode, isArrangeMode } = useAppMode()
const hoveringSelectable = ref(false)
provide(HideLayoutFieldKey, true)
workflowStore.activeWorkflow?.changeTracker?.reset()
// Prune stale entries whose node/widget no longer exists, so the
// DraggableList model always matches the rendered items.
watchEffect(() => {
const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) => {
const node = app.rootGraph.getNodeById(nodeId)
return node?.widgets?.some((w) => w.name === widgetName)
})
if (valid.length < appModeStore.selectedInputs.length) {
appModeStore.selectedInputs = valid
}
})
const arrangeInputs = computed(() =>
appModeStore.selectedInputs
.map(([nodeId, widgetName]) => {
const node = app.rootGraph.getNodeById(nodeId)
const widget = node?.widgets?.find((w) => w.name === widgetName)
if (!node || !widget) return null
return { nodeId, widgetName, node, widget }
})
.filter((item): item is NonNullable<typeof item> => item !== null)
)
const inputsWithState = computed(() =>
appModeStore.selectedInputs.map(([nodeId, widgetName]) => {
const node = app.rootGraph.getNodeById(nodeId)
@@ -208,36 +179,12 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
</script>
<template>
<div class="flex font-bold p-2 border-border-subtle border-b items-center">
{{
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
}}
{{ t('linearMode.builder.title') }}
<Button class="ml-auto" @click="appModeStore.exitBuilder">
{{ t('linearMode.builder.exit') }}
</Button>
</div>
<DraggableList
v-if="isArrangeMode"
v-slot="{ dragClass }"
v-model="appModeStore.selectedInputs"
>
<div
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
:key="`${nodeId}: ${widgetName}`"
:class="cn(dragClass, 'p-2 my-2 pointer-events-auto')"
:aria-label="`${widget.label ?? widgetName} ${node.title}`"
>
<div class="pointer-events-none" inert>
<WidgetItem
:widget="widget"
:node="node"
show-node-name
hidden-widget-actions
/>
</div>
</div>
</DraggableList>
<PropertiesAccordionItem
v-else
:label="t('nodeHelpPage.inputs')"
enable-empty-state
:disabled="!appModeStore.selectedInputs.length"
@@ -285,7 +232,6 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
</DraggableList>
</PropertiesAccordionItem>
<PropertiesAccordionItem
v-if="!isArrangeMode"
:label="t('nodeHelpPage.outputs')"
enable-empty-state
:disabled="!appModeStore.selectedOutputs.length"
@@ -328,7 +274,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
</DraggableList>
</PropertiesAccordionItem>
<Teleport v-if="mode === 'builder:select'" to="body">
<Teleport to="body">
<div
:class="
cn(

View File

@@ -20,7 +20,7 @@
)
"
:aria-current="activeStep === step.id ? 'step' : undefined"
@click="setMode(step.id)"
@click="appModeStore.setMode(step.id)"
>
<StepBadge :step :index :model-value="activeStep" />
<StepLabel :step />
@@ -31,9 +31,9 @@
<!-- Save -->
<ConnectOutputPopover
v-if="!hasOutputs"
v-if="!appModeStore.hasOutputs"
:is-select-active="activeStep === 'builder:select'"
@switch="setMode('builder:select')"
@switch="appModeStore.setMode('builder:select')"
>
<button :class="cn(stepClasses, 'opacity-30 bg-transparent')">
<StepBadge :step="saveStep" :index="2" :model-value="activeStep" />
@@ -50,7 +50,7 @@
: 'hover:bg-secondary-background bg-transparent'
)
"
@click="setSaving(true)"
@click="appModeStore.setBuilderSaving(true)"
>
<StepBadge :step="saveStep" :index="2" :model-value="activeStep" />
<StepLabel :step="saveStep" />
@@ -62,25 +62,31 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useEventListener } from '@vueuse/core'
import { useAppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import type { AppMode } from '@/stores/appModeStore'
import { cn } from '@/utils/tailwindUtil'
import { useBuilderSave } from './useBuilderSave'
import ConnectOutputPopover from './ConnectOutputPopover.vue'
import StepBadge from './StepBadge.vue'
import StepLabel from './StepLabel.vue'
import type { BuilderToolbarStep } from './types'
import { storeToRefs } from 'pinia'
const { t } = useI18n()
const { mode, setMode } = useAppMode()
const { hasOutputs } = storeToRefs(useAppModeStore())
const { saving, setSaving } = useBuilderSave()
const appModeStore = useAppModeStore()
const activeStep = computed(() => (saving.value ? 'save' : mode.value))
useEventListener(document, 'keydown', (e: KeyboardEvent) => {
if (e.key !== 'Escape') return
e.preventDefault()
e.stopPropagation()
void appModeStore.exitBuilder()
})
const activeStep = computed(() =>
appModeStore.isBuilderSaving ? 'save' : appModeStore.mode
)
const stepClasses =
'inline-flex h-14 min-h-8 cursor-pointer items-center gap-3 rounded-lg py-2 pr-4 pl-2 transition-colors border-none'

View File

@@ -1,36 +1,30 @@
import { ref } from 'vue'
import { watch } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { useDialogStore } from '@/stores/dialogStore'
import BuilderSaveDialogContent from './BuilderSaveDialogContent.vue'
import BuilderSaveSuccessDialogContent from './BuilderSaveSuccessDialogContent.vue'
import { whenever } from '@vueuse/core'
const SAVE_DIALOG_KEY = 'builder-save'
const SUCCESS_DIALOG_KEY = 'builder-save-success'
export function useBuilderSave() {
const { setMode } = useAppMode()
const { toastErrorHandler } = useErrorHandling()
const appModeStore = useAppModeStore()
const workflowStore = useWorkflowStore()
const workflowService = useWorkflowService()
const dialogService = useDialogService()
const appModeStore = useAppModeStore()
const dialogStore = useDialogStore()
const saving = ref(false)
whenever(saving, onBuilderSave)
function setSaving(value: boolean) {
saving.value = value
}
watch(
() => appModeStore.isBuilderSaving,
(saving) => {
if (saving) void onBuilderSave()
}
)
async function onBuilderSave() {
const workflow = workflowStore.activeWorkflow
@@ -39,14 +33,13 @@ export function useBuilderSave() {
return
}
if (!workflow.isTemporary && workflow.initialMode != null) {
// Re-save with the previously chosen mode — no dialog needed.
if (!workflow.isTemporary && workflow.activeState.extra?.linearMode) {
try {
appModeStore.flushSelections()
workflow.changeTracker?.checkState()
appModeStore.saveSelectedToWorkflow()
await workflowService.saveWorkflow(workflow)
showSuccessDialog(workflow.filename, workflow.initialMode === 'app')
} catch (e) {
toastErrorHandler(e)
showSuccessDialog(workflow.filename, appModeStore.isAppMode)
} catch {
resetSaving()
}
return
@@ -80,19 +73,17 @@ export function useBuilderSave() {
const workflow = workflowStore.activeWorkflow
if (!workflow) return
appModeStore.flushSelections()
const mode = openAsApp ? 'app' : 'graph'
appModeStore.saveSelectedToWorkflow()
const saved = await workflowService.saveWorkflowAs(workflow, {
filename,
initialMode: mode
openAsApp
})
if (!saved) return
closeSaveDialog()
showSuccessDialog(filename, openAsApp)
} catch (e) {
toastErrorHandler(e)
} catch {
closeSaveDialog()
resetSaving()
}
@@ -106,7 +97,7 @@ export function useBuilderSave() {
workflowName,
savedAsApp,
onViewApp: () => {
setMode('app')
appModeStore.setMode('app')
closeSuccessDialog()
},
onClose: closeSuccessDialog
@@ -127,8 +118,6 @@ export function useBuilderSave() {
}
function resetSaving() {
saving.value = false
appModeStore.setBuilderSaving(false)
}
return { saving, setSaving }
}

View File

@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import type { CurvePoint } from './types'
import type { CurvePoint } from '@/lib/litegraph/src/types/widgets'
import CurveEditor from './CurveEditor.vue'

View File

@@ -77,8 +77,7 @@
import { computed, useTemplateRef } from 'vue'
import { useCurveEditor } from '@/composables/useCurveEditor'
import type { CurvePoint } from './types'
import type { CurvePoint } from '@/lib/litegraph/src/types/widgets'
import { histogramToPath } from './curveUtils'

View File

@@ -3,7 +3,7 @@
</template>
<script setup lang="ts">
import type { CurvePoint } from './types'
import type { CurvePoint } from '@/lib/litegraph/src/types/widgets'
import CurveEditor from './CurveEditor.vue'

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { CurvePoint } from './types'
import type { CurvePoint } from '@/lib/litegraph/src/types/widgets'
import {
createMonotoneInterpolator,

View File

@@ -1,4 +1,4 @@
import type { CurvePoint } from './types'
import type { CurvePoint } from '@/lib/litegraph/src/types/widgets'
/**
* Monotone cubic Hermite interpolation.
@@ -95,15 +95,15 @@ export function histogramToPath(histogram: Uint32Array): string {
const max = sorted[Math.floor(255 * 0.995)]
if (max === 0) return ''
const invMax = 1 / max
const parts: string[] = ['M0,1']
const step = 1 / 255
let d = 'M0,1'
for (let i = 0; i < 256; i++) {
const x = i / 255
const y = 1 - Math.min(1, histogram[i] * invMax)
parts.push(`L${x},${y}`)
const x = i * step
const y = 1 - Math.min(1, histogram[i] / max)
d += ` L${x.toFixed(4)},${y.toFixed(4)}`
}
parts.push('L1,1 Z')
return parts.join(' ')
d += ' L1,1 Z'
return d
}
export function curvesToLUT(points: CurvePoint[]): Uint8Array {

View File

@@ -1 +0,0 @@
export type CurvePoint = [x: number, y: number]

View File

@@ -438,6 +438,7 @@ onMounted(() => {
const systemStatsStore = useSystemStatsStore()
const distributions = computed(() => {
// eslint-disable-next-line no-undef
switch (__DISTRIBUTION__) {
case 'cloud':
return [TemplateIncludeOnDistributionEnum.Cloud]

View File

@@ -137,7 +137,7 @@ const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { isActiveSubscription, subscription } = useBillingContext()
const { isActiveSubscription } = useBillingContext()
const loading = computed(() => authStore.loading)
const balanceLoading = computed(() => authStore.isFetchingBalance)
@@ -160,9 +160,7 @@ watch(
const handlePurchaseCreditsClick = () => {
// Track purchase credits entry from Settings > Credits panel
useTelemetry()?.trackAddApiCreditButtonClicked({
current_tier: subscription.value?.tier?.toLowerCase()
})
useTelemetry()?.trackAddApiCreditButtonClicked()
dialogService.showTopUpCreditsDialog()
}

View File

@@ -18,11 +18,10 @@
>
<WorkflowTabs />
<TopbarBadges />
<TopbarSubscribeButton />
</div>
</div>
</template>
<template v-if="showUI && !isBuilderMode" #side-toolbar>
<template v-if="showUI && !appModeStore.isBuilderMode" #side-toolbar>
<SideToolbar />
</template>
<template v-if="showUI" #side-bar-panel>
@@ -32,24 +31,27 @@
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
</div>
</template>
<template v-if="showUI && !isBuilderMode" #topmenu>
<template v-if="showUI && !appModeStore.isBuilderMode" #topmenu>
<TopMenuSection />
</template>
<template v-if="showUI" #bottom-panel>
<BottomPanel />
</template>
<template v-if="showUI" #right-side-panel>
<AppBuilder v-if="mode === 'builder:select'" />
<NodePropertiesPanel v-else-if="!isBuilderMode" />
<AppBuilder v-if="appModeStore.mode === 'builder:select'" />
<NodePropertiesPanel v-else-if="!appModeStore.isBuilderMode" />
</template>
<template #graph-canvas-panel>
<GraphCanvasMenu
v-if="canvasMenuEnabled && !isBuilderMode"
v-if="canvasMenuEnabled && !appModeStore.isBuilderMode"
class="pointer-events-auto"
/>
<MiniMap
v-if="
comfyAppReady && minimapEnabled && betaMenuEnabled && !isBuilderMode
comfyAppReady &&
minimapEnabled &&
betaMenuEnabled &&
!appModeStore.isBuilderMode
"
class="pointer-events-auto"
/>
@@ -125,10 +127,10 @@ import {
import { useI18n } from 'vue-i18n'
import { isMiddlePointerInput } from '@/base/pointerUtils'
import AppBuilder from '@/components/builder/AppBuilder.vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import TopMenuSection from '@/components/TopMenuSection.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import AppBuilder from '@/components/builder/AppBuilder.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
@@ -141,7 +143,6 @@ import NodePropertiesPanel from '@/components/rightSidePanel/RightSidePanel.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
@@ -183,7 +184,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
import { forEachNode } from '@/utils/graphTraversalUtil'
@@ -204,7 +205,7 @@ const nodeSearchboxPopoverRef = shallowRef<InstanceType<
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore()
const { mode, isBuilderMode } = useAppMode()
const appModeStore = useAppModeStore()
const canvasStore = useCanvasStore()
const workflowStore = useWorkflowStore()
const executionStore = useExecutionStore()

View File

@@ -20,7 +20,7 @@
class="w-full justify-between text-sm font-light"
variant="textonly"
size="sm"
@click="onToggleDockedJobHistory(close)"
@click="onToggleDockedJobHistory"
>
<span class="flex items-center gap-2">
<i
@@ -79,7 +79,6 @@ import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
const emit = defineEmits<{
(e: 'clearHistory'): void
@@ -87,7 +86,6 @@ const emit = defineEmits<{
const { t } = useI18n()
const settingStore = useSettingStore()
const sidebarTabStore = useSidebarTabStore()
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
const isQueuePanelV2Enabled = computed(() =>
@@ -100,22 +98,7 @@ const onClearHistoryFromMenu = (close: () => void) => {
emit('clearHistory')
}
const onToggleDockedJobHistory = async (close: () => void) => {
close()
try {
if (isQueuePanelV2Enabled.value) {
await settingStore.setMany({
'Comfy.Queue.QPOV2': false,
'Comfy.Queue.History.Expanded': true
})
return
}
sidebarTabStore.activeSidebarTabId = 'job-history'
await settingStore.set('Comfy.Queue.QPOV2', true)
} catch {
return
}
const onToggleDockedJobHistory = async () => {
await settingStore.set('Comfy.Queue.QPOV2', !isQueuePanelV2Enabled.value)
}
</script>

View File

@@ -23,27 +23,18 @@ vi.mock('@/components/ui/Popover.vue', () => {
return { default: PopoverStub }
})
const mockGetSetting = vi.fn<(key: string) => boolean | undefined>((key) =>
const mockGetSetting = vi.fn((key: string) =>
key === 'Comfy.Queue.QPOV2' ? true : undefined
)
const mockSetSetting = vi.fn()
const mockSetMany = vi.fn()
const mockSidebarTabStore = {
activeSidebarTabId: null as string | null
}
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: mockGetSetting,
set: mockSetSetting,
setMany: mockSetMany
set: mockSetSetting
})
}))
vi.mock('@/stores/workspace/sidebarTabStore', () => ({
useSidebarTabStore: () => mockSidebarTabStore
}))
import QueueOverlayHeader from './QueueOverlayHeader.vue'
import * as tooltipConfig from '@/composables/useTooltipConfig'
@@ -90,11 +81,6 @@ describe('QueueOverlayHeader', () => {
beforeEach(() => {
popoverCloseSpy.mockClear()
mockSetSetting.mockClear()
mockSetMany.mockClear()
mockSidebarTabStore.activeSidebarTabId = null
mockGetSetting.mockImplementation((key: string) =>
key === 'Comfy.Queue.QPOV2' ? true : undefined
)
})
it('renders header title', () => {
@@ -139,7 +125,7 @@ describe('QueueOverlayHeader', () => {
expect(wrapper.emitted('clearHistory')).toHaveLength(1)
})
it('opens floating queue progress overlay when disabling from the menu', async () => {
it('toggles docked job history setting from the menu', async () => {
const wrapper = mountHeader()
const dockedJobHistoryButton = wrapper.get(
@@ -147,64 +133,7 @@ describe('QueueOverlayHeader', () => {
)
await dockedJobHistoryButton.trigger('click')
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
expect(mockSetMany).toHaveBeenCalledTimes(1)
expect(mockSetMany).toHaveBeenCalledWith({
'Comfy.Queue.QPOV2': false,
'Comfy.Queue.History.Expanded': true
})
expect(mockSetSetting).not.toHaveBeenCalled()
expect(mockSidebarTabStore.activeSidebarTabId).toBe(null)
})
it('opens docked job history sidebar when enabling from the menu', async () => {
mockGetSetting.mockImplementation((key: string) =>
key === 'Comfy.Queue.QPOV2' ? false : undefined
)
const wrapper = mountHeader()
const dockedJobHistoryButton = wrapper.get(
'[data-testid="docked-job-history-action"]'
)
await dockedJobHistoryButton.trigger('click')
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledWith('Comfy.Queue.QPOV2', true)
expect(mockSetMany).not.toHaveBeenCalled()
expect(mockSidebarTabStore.activeSidebarTabId).toBe('job-history')
})
it('keeps docked target open even when enabling persistence fails', async () => {
mockGetSetting.mockImplementation((key: string) =>
key === 'Comfy.Queue.QPOV2' ? false : undefined
)
mockSetSetting.mockRejectedValueOnce(new Error('persistence failed'))
const wrapper = mountHeader()
const dockedJobHistoryButton = wrapper.get(
'[data-testid="docked-job-history-action"]'
)
await dockedJobHistoryButton.trigger('click')
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
expect(mockSetSetting).toHaveBeenCalledWith('Comfy.Queue.QPOV2', true)
expect(mockSidebarTabStore.activeSidebarTabId).toBe('job-history')
})
it('closes the menu when disabling persistence fails', async () => {
mockSetMany.mockRejectedValueOnce(new Error('persistence failed'))
const wrapper = mountHeader()
const dockedJobHistoryButton = wrapper.get(
'[data-testid="docked-job-history-action"]'
)
await dockedJobHistoryButton.trigger('click')
expect(popoverCloseSpy).toHaveBeenCalledTimes(1)
expect(mockSetMany).toHaveBeenCalledWith({
'Comfy.Queue.QPOV2': false,
'Comfy.Queue.History.Expanded': true
})
expect(mockSetSetting).toHaveBeenCalledWith('Comfy.Queue.QPOV2', false)
})
})

View File

@@ -131,11 +131,11 @@ export const Queued: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-1'
const priority = 104
const queueIndex = 104
// Current job in pending
queue.pendingTasks = [
makePendingTask(jobId, priority, Date.now() - 90_000)
makePendingTask(jobId, queueIndex, Date.now() - 90_000)
]
// Add some other pending jobs to give context
queue.pendingTasks.push(
@@ -179,13 +179,13 @@ export const QueuedParallel: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-parallel'
const priority = 210
const queueIndex = 210
// Current job in pending with some ahead
queue.pendingTasks = [
makePendingTask('job-ahead-1', 200, Date.now() - 180_000),
makePendingTask('job-ahead-2', 205, Date.now() - 150_000),
makePendingTask(jobId, priority, Date.now() - 120_000)
makePendingTask(jobId, queueIndex, Date.now() - 120_000)
]
// Seen 2 minutes ago - set via prompt metadata above
@@ -238,9 +238,9 @@ export const Running: Story = {
const exec = useExecutionStore()
const jobId = 'job-running-1'
const priority = 300
const queueIndex = 300
queue.runningTasks = [
makeRunningTask(jobId, priority, Date.now() - 65_000)
makeRunningTask(jobId, queueIndex, Date.now() - 65_000)
]
queue.historyTasks = [
makeHistoryTask('hist-r1', 250, 30, true),
@@ -279,10 +279,10 @@ export const QueuedZeroAheadSingleRunning: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-zero-ahead-single'
const priority = 510
const queueIndex = 510
queue.pendingTasks = [
makePendingTask(jobId, priority, Date.now() - 45_000)
makePendingTask(jobId, queueIndex, Date.now() - 45_000)
]
queue.historyTasks = [
@@ -324,10 +324,10 @@ export const QueuedZeroAheadMultiRunning: Story = {
const exec = useExecutionStore()
const jobId = 'job-queued-zero-ahead-multi'
const priority = 520
const queueIndex = 520
queue.pendingTasks = [
makePendingTask(jobId, priority, Date.now() - 20_000)
makePendingTask(jobId, queueIndex, Date.now() - 20_000)
]
queue.historyTasks = [
@@ -380,8 +380,8 @@ export const Completed: Story = {
const queue = useQueueStore()
const jobId = 'job-completed-1'
const priority = 400
queue.historyTasks = [makeHistoryTask(jobId, priority, 37, true)]
const queueIndex = 400
queue.historyTasks = [makeHistoryTask(jobId, queueIndex, 37, true)]
return { args: { ...args, jobId } }
},
@@ -401,11 +401,11 @@ export const Failed: Story = {
const queue = useQueueStore()
const jobId = 'job-failed-1'
const priority = 410
const queueIndex = 410
queue.historyTasks = [
makeHistoryTask(
jobId,
priority,
queueIndex,
12,
false,
'Example error: invalid inputs for node X'

View File

@@ -166,16 +166,16 @@ const queuedAtValue = computed(() =>
: ''
)
const currentJobPriority = computed<number | null>(() => {
const currentQueueIndex = computed<number | null>(() => {
const task = taskForJob.value
return task ? Number(task.job.priority) : null
return task ? Number(task.queueIndex) : null
})
const jobsAhead = computed<number | null>(() => {
const idx = currentJobPriority.value
const idx = currentQueueIndex.value
if (idx == null) return null
const ahead = queueStore.pendingTasks.filter(
(t: TaskItemImpl) => Number(t.job.priority) < idx
(t: TaskItemImpl) => Number(t.queueIndex) < idx
)
return ahead.length
})

View File

@@ -5,7 +5,7 @@
v-for="tab in visibleJobTabs"
:key="tab"
:variant="selectedJobTab === tab ? 'secondary' : 'muted-textonly'"
size="md"
size="sm"
@click="$emit('update:selectedJobTab', tab)"
>
{{ tabLabel(tab) }}

View File

@@ -1,150 +0,0 @@
<template>
<div class="flex flex-col w-full mb-4">
<!-- Type header row: type name + chevron -->
<div class="flex h-8 items-center w-full">
<p
class="flex-1 min-w-0 text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap text-foreground"
>
{{ `${group.type} (${group.nodeTypes.length})` }}
</p>
<Button
variant="textonly"
size="icon-sm"
:class="
cn(
'size-8 shrink-0 transition-transform duration-200 hover:bg-transparent',
{ 'rotate-180': expanded }
)
"
:aria-label="
expanded
? t('rightSidePanel.missingNodePacks.collapse', 'Collapse')
: t('rightSidePanel.missingNodePacks.expand', 'Expand')
"
@click="toggleExpand"
>
<i
class="icon-[lucide--chevron-down] size-4 text-muted-foreground group-hover:text-base-foreground"
/>
</Button>
</div>
<!-- Sub-labels: individual node instances, each with their own Locate button -->
<TransitionCollapse>
<div
v-if="expanded"
class="flex flex-col gap-0.5 pl-2 mb-2 overflow-hidden"
>
<div
v-for="nodeType in group.nodeTypes"
:key="getKey(nodeType)"
class="flex h-7 items-center"
>
<span
v-if="
showNodeIdBadge &&
typeof nodeType !== 'string' &&
nodeType.nodeId != null
"
class="shrink-0 rounded-md bg-secondary-background-selected px-2 py-0.5 text-xs font-mono text-muted-foreground font-bold mr-1"
>
#{{ nodeType.nodeId }}
</span>
<p class="flex-1 min-w-0 text-xs text-muted-foreground truncate">
{{ getLabel(nodeType) }}
</p>
<Button
v-if="typeof nodeType !== 'string' && nodeType.nodeId != null"
variant="textonly"
size="icon-sm"
class="size-6 text-muted-foreground hover:text-base-foreground shrink-0 mr-1"
:aria-label="t('rightSidePanel.locateNode', 'Locate Node')"
@click="handleLocateNode(nodeType)"
>
<i class="icon-[lucide--locate] size-3" />
</Button>
</div>
</div>
</TransitionCollapse>
<!-- Description rows: what it is replaced by -->
<div class="flex flex-col text-[13px] mb-2 mt-1 px-1 gap-0.5">
<span class="text-muted-foreground">{{
t('nodeReplacement.willBeReplacedBy', 'This node will be replaced by:')
}}</span>
<span class="font-bold text-foreground">{{
group.newNodeId ?? t('nodeReplacement.unknownNode', 'Unknown')
}}</span>
</div>
<!-- Replace Action Button -->
<div class="flex items-start w-full pt-1 pb-1">
<Button
variant="secondary"
size="md"
class="flex flex-1 w-full"
@click="handleReplaceNode"
>
<i class="icon-[lucide--repeat] size-4 text-foreground shrink-0 mr-1" />
<span class="text-sm text-foreground truncate min-w-0">
{{ t('nodeReplacement.replaceNode', 'Replace Node') }}
</span>
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import TransitionCollapse from '@/components/rightSidePanel/layout/TransitionCollapse.vue'
import type { MissingNodeType } from '@/types/comfy'
import type { SwapNodeGroup } from './useErrorGroups'
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
const props = defineProps<{
group: SwapNodeGroup
showNodeIdBadge: boolean
}>()
const emit = defineEmits<{
'locate-node': [nodeId: string]
}>()
const { t } = useI18n()
const { replaceNodesInPlace } = useNodeReplacement()
const executionErrorStore = useExecutionErrorStore()
const expanded = ref(false)
function toggleExpand() {
expanded.value = !expanded.value
}
function getKey(nodeType: MissingNodeType): string {
if (typeof nodeType === 'string') return nodeType
return nodeType.nodeId != null ? String(nodeType.nodeId) : nodeType.type
}
function getLabel(nodeType: MissingNodeType): string {
return typeof nodeType === 'string' ? nodeType : nodeType.type
}
function handleLocateNode(nodeType: MissingNodeType) {
if (typeof nodeType === 'string') return
if (nodeType.nodeId != null) {
emit('locate-node', String(nodeType.nodeId))
}
}
function handleReplaceNode() {
const replaced = replaceNodesInPlace(props.group.nodeTypes)
if (replaced.length > 0) {
executionErrorStore.removeMissingNodesByType([props.group.type])
}
}
</script>

View File

@@ -1,38 +0,0 @@
<template>
<div class="px-4 pb-2 mt-2">
<!-- Sub-label: guidance message shown above all swap groups -->
<p class="m-0 pb-5 text-sm text-muted-foreground leading-relaxed">
{{
t(
'nodeReplacement.swapNodesGuide',
'The following nodes can be automatically replaced with compatible alternatives.'
)
}}
</p>
<!-- Group Rows -->
<SwapNodeGroupRow
v-for="group in swapNodeGroups"
:key="group.type"
:group="group"
:show-node-id-badge="showNodeIdBadge"
@locate-node="emit('locate-node', $event)"
/>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { SwapNodeGroup } from './useErrorGroups'
import SwapNodeGroupRow from './SwapNodeGroupRow.vue'
const { t } = useI18n()
const { swapNodeGroups, showNodeIdBadge } = defineProps<{
swapNodeGroups: SwapNodeGroup[]
showNodeIdBadge: boolean
}>()
const emit = defineEmits<{
'locate-node': [nodeId: string]
}>()
</script>

View File

@@ -27,11 +27,7 @@
:key="group.title"
:collapse="collapseState[group.title] ?? false"
class="border-b border-interface-stroke"
:size="
group.type === 'missing_node' || group.type === 'swap_nodes'
? 'lg'
: 'default'
"
:size="group.type === 'missing_node' ? 'lg' : 'default'"
@update:collapse="collapseState[group.title] = $event"
>
<template #label>
@@ -44,9 +40,7 @@
{{
group.type === 'missing_node'
? `${group.title} (${missingPackGroups.length})`
: group.type === 'swap_nodes'
? `${group.title} (${swapNodeGroups.length})`
: group.title
: group.title
}}
</span>
<span
@@ -75,21 +69,6 @@
: t('rightSidePanel.missingNodePacks.installAll')
}}
</Button>
<Button
v-else-if="group.type === 'swap_nodes'"
v-tooltip.top="
t(
'nodeReplacement.replaceAllWarning',
'Replaces all available nodes in this group.'
)
"
variant="secondary"
size="sm"
class="shrink-0 mr-2 h-8 rounded-lg text-sm"
@click.stop="handleReplaceAll()"
>
{{ t('nodeReplacement.replaceAll', 'Replace All') }}
</Button>
</div>
</template>
@@ -103,16 +82,8 @@
@open-manager-info="handleOpenManagerInfo"
/>
<!-- Swap Nodes -->
<SwapNodesCard
v-else-if="group.type === 'swap_nodes'"
:swap-node-groups="swapNodeGroups"
:show-node-id-badge="showNodeIdBadge"
@locate-node="handleLocateMissingNode"
/>
<!-- Execution Errors -->
<div v-else-if="group.type === 'execution'" class="px-4 space-y-3">
<div v-else class="px-4 space-y-3">
<ErrorNodeCard
v-for="card in group.cards"
:key="card.id"
@@ -179,14 +150,11 @@ import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
import ErrorNodeCard from './ErrorNodeCard.vue'
import MissingNodeCard from './MissingNodeCard.vue'
import SwapNodesCard from './SwapNodesCard.vue'
import Button from '@/components/ui/button/Button.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { useErrorGroups } from './useErrorGroups'
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
const { t } = useI18n()
const { copyToClipboard } = useCopyToClipboard()
@@ -199,8 +167,6 @@ const { shouldShowManagerButtons, shouldShowInstallButton, openManager } =
const { missingNodePacks } = useMissingNodes()
const { isInstalling: isInstallingAll, installAllPacks: installAll } =
usePackInstall(() => missingNodePacks.value)
const { replaceNodesInPlace } = useNodeReplacement()
const executionErrorStore = useExecutionErrorStore()
const searchQuery = ref('')
@@ -217,8 +183,7 @@ const {
isSingleNodeSelected,
errorNodeCache,
missingNodeCache,
missingPackGroups,
swapNodeGroups
missingPackGroups
} = useErrorGroups(searchQuery, t)
/**
@@ -264,14 +229,6 @@ function handleOpenManagerInfo(packId: string) {
}
}
function handleReplaceAll() {
const allNodeTypes = swapNodeGroups.value.flatMap((g) => g.nodeTypes)
const replaced = replaceNodesInPlace(allNodeTypes)
if (replaced.length > 0) {
executionErrorStore.removeMissingNodesByType(replaced)
}
}
function handleEnterSubgraph(nodeId: string) {
enterSubgraph(nodeId, errorNodeCache.value)
}

View File

@@ -22,4 +22,3 @@ export type ErrorGroup =
priority: number
}
| { type: 'missing_node'; title: string; priority: number }
| { type: 'swap_nodes'; title: string; priority: number }

View File

@@ -42,12 +42,6 @@ export interface MissingPackGroup {
isResolving: boolean
}
export interface SwapNodeGroup {
type: string
newNodeId: string | undefined
nodeTypes: MissingNodeType[]
}
interface GroupEntry {
type: 'execution'
priority: number
@@ -450,8 +444,6 @@ export function useErrorGroups(
const resolvingKeys = new Set<string | null>()
for (const nodeType of nodeTypes) {
if (typeof nodeType !== 'string' && nodeType.isReplaceable) continue
let packId: string | null
if (typeof nodeType === 'string') {
@@ -503,53 +495,18 @@ export function useErrorGroups(
}))
})
const swapNodeGroups = computed<SwapNodeGroup[]>(() => {
const nodeTypes = executionErrorStore.missingNodesError?.nodeTypes ?? []
const map = new Map<string, SwapNodeGroup>()
for (const nodeType of nodeTypes) {
if (typeof nodeType === 'string' || !nodeType.isReplaceable) continue
const typeName = nodeType.type
const existing = map.get(typeName)
if (existing) {
existing.nodeTypes.push(nodeType)
} else {
map.set(typeName, {
type: typeName,
newNodeId: nodeType.replacement?.new_node_id,
nodeTypes: [nodeType]
})
}
}
return Array.from(map.values()).sort((a, b) => a.type.localeCompare(b.type))
})
/** Builds an ErrorGroup from missingNodesError. Returns [] when none present. */
function buildMissingNodeGroups(): ErrorGroup[] {
const error = executionErrorStore.missingNodesError
if (!error) return []
const groups: ErrorGroup[] = []
if (swapNodeGroups.value.length > 0) {
groups.push({
type: 'swap_nodes' as const,
title: st('nodeReplacement.swapNodesTitle', 'Swap Nodes'),
priority: 0
})
}
if (missingPackGroups.value.length > 0) {
groups.push({
return [
{
type: 'missing_node' as const,
title: error.message,
priority: 1
})
}
return groups.sort((a, b) => a.priority - b.priority)
priority: 0
}
]
}
const allErrorGroups = computed<ErrorGroup[]>(() => {
@@ -607,7 +564,6 @@ export function useErrorGroups(
errorNodeCache,
missingNodeCache,
groupedErrorMessages,
missingPackGroups,
swapNodeGroups
missingPackGroups
}
}

View File

@@ -34,7 +34,6 @@ const {
node,
isDraggable = false,
hiddenFavoriteIndicator = false,
hiddenWidgetActions = false,
showNodeName = false,
parents = [],
isShownOnParents = false
@@ -43,7 +42,6 @@ const {
node: LGraphNode
isDraggable?: boolean
hiddenFavoriteIndicator?: boolean
hiddenWidgetActions?: boolean
showNodeName?: boolean
parents?: SubgraphNode[]
isShownOnParents?: boolean
@@ -172,10 +170,7 @@ const displayLabel = customRef((track, trigger) => {
>
{{ sourceNodeName }}
</span>
<div
v-if="!hiddenWidgetActions"
class="flex items-center gap-1 shrink-0 pointer-events-auto"
>
<div class="flex items-center gap-1 shrink-0 pointer-events-auto">
<WidgetActions
v-model:label="displayLabel"
:widget="widget"

View File

@@ -3,7 +3,7 @@
ref="containerRef"
:class="
cn(
'comfy-vue-side-bar-container group/sidebar-tab flex h-full flex-col w-full',
'comfy-vue-side-bar-container group/sidebar-tab flex h-full flex-col',
props.class
)
"

View File

@@ -10,6 +10,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { TopbarBadge as TopbarBadgeType } from '@/types/comfy'
@@ -27,8 +28,10 @@ const {
backgroundColor?: string
}>()
const { t } = useI18n()
const cloudBadge = computed<TopbarBadgeType>(() => ({
icon: 'icon-[lucide--cloud]',
label: t('g.beta'),
text: 'Comfy Cloud'
}))
</script>

View File

@@ -27,20 +27,6 @@
>
{{ subscriptionTierName }}
</span>
<Button
v-if="isFreeTier"
variant="primary"
size="sm"
class="mt-2 whitespace-nowrap"
:style="{
background: 'var(--color-subscription-button-gradient)',
color: 'var(--color-white)',
borderColor: 'transparent'
}"
@click="handleSubscribeForMore"
>
{{ $t('subscription.subscribeForMore') }}
</Button>
</div>
<!-- Credits Section -->
@@ -184,7 +170,6 @@ const settingsDialog = useSettingsDialog()
const dialogService = useDialogService()
const {
isActiveSubscription,
isFreeTier,
subscriptionTierName,
subscriptionTier,
fetchStatus
@@ -223,9 +208,7 @@ const handleOpenUserSettings = () => {
}
const handleOpenPlansAndPricing = () => {
subscriptionDialog.showPricingTable({
entry_point: 'popover_plans_and_pricing'
})
subscriptionDialog.showPricingTable()
emit('close')
}
@@ -241,9 +224,7 @@ const handleOpenPlanAndCreditsSettings = () => {
const handleTopUp = () => {
// Track purchase credits entry from avatar popover
useTelemetry()?.trackAddApiCreditButtonClicked({
current_tier: subscriptionTier.value?.toLowerCase()
})
useTelemetry()?.trackAddApiCreditButtonClicked()
dialogService.showTopUpCreditsDialog()
emit('close')
}
@@ -261,11 +242,6 @@ const handleLogout = async () => {
emit('close')
}
const handleSubscribeForMore = () => {
subscriptionDialog.show({ entry_point: 'popover_upgrade' })
emit('close')
}
const handleSubscribed = async () => {
await fetchStatus()
}

View File

@@ -1,35 +0,0 @@
<template>
<Button
v-if="isFreeTier"
class="mr-2 shrink-0 whitespace-nowrap"
variant="primary"
size="sm"
:style="{
background: 'var(--color-subscription-button-gradient)',
color: 'var(--color-white)',
borderColor: 'transparent'
}"
data-testid="topbar-subscribe-button"
@click="handleClick"
>
{{ $t('subscription.subscribeForMore') }}
</Button>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
const { isFreeTier, showSubscriptionDialog } = useBillingContext()
function handleClick() {
if (isCloud) {
useTelemetry()?.trackSubscription('subscribe_clicked', {
current_tier: 'free'
})
}
showSubscriptionDialog()
}
</script>

View File

@@ -10,7 +10,7 @@
@click="handleClick"
>
<i
v-if="workflowOption.workflow.initialMode === 'app'"
v-if="workflowOption.workflow.activeState?.extra?.linearMode"
class="icon-[lucide--panels-top-left] bg-primary-background"
/>
<span

View File

@@ -25,19 +25,15 @@ whenever(feedbackRef, () => {
:href="`https://form.typeform.com/to/${dataTfWidget}`"
target="_blank"
variant="inverted"
class="flex h-10 items-center justify-center gap-2.5 px-3 py-2"
class="rounded-full size-12"
v-bind="$attrs"
>
<i class="icon-[lucide--circle-help] size-4" />
<i class="icon-[lucide--circle-question-mark] size-6" />
</Button>
<Popover v-else>
<template #button>
<Button
variant="inverted"
class="flex h-10 items-center justify-center gap-2.5 px-3 py-2"
v-bind="$attrs"
>
<i class="icon-[lucide--circle-help] size-4" />
<Button variant="inverted" class="rounded-full size-12" v-bind="$attrs">
<i class="icon-[lucide--circle-question-mark] size-6" />
</Button>
</template>
<div ref="feedbackRef" data-tf-auto-resize :data-tf-widget />

View File

@@ -1,224 +0,0 @@
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import { useDomClipping } from './useDomClipping'
function createMockElement(rect: {
left: number
top: number
width: number
height: number
}): HTMLElement {
return {
getBoundingClientRect: vi.fn(
() =>
({
...rect,
x: rect.left,
y: rect.top,
right: rect.left + rect.width,
bottom: rect.top + rect.height,
toJSON: () => ({})
}) as DOMRect
)
} as unknown as HTMLElement
}
function createMockCanvas(rect: {
left: number
top: number
width: number
height: number
}): HTMLCanvasElement {
return {
getBoundingClientRect: vi.fn(
() =>
({
...rect,
x: rect.left,
y: rect.top,
right: rect.left + rect.width,
bottom: rect.top + rect.height,
toJSON: () => ({})
}) as DOMRect
)
} as unknown as HTMLCanvasElement
}
describe('useDomClipping', () => {
let rafCallbacks: Map<number, FrameRequestCallback>
let nextRafId: number
beforeEach(() => {
rafCallbacks = new Map()
nextRafId = 1
vi.stubGlobal(
'requestAnimationFrame',
vi.fn((cb: FrameRequestCallback) => {
const id = nextRafId++
rafCallbacks.set(id, cb)
return id
})
)
vi.stubGlobal(
'cancelAnimationFrame',
vi.fn((id: number) => {
rafCallbacks.delete(id)
})
)
})
afterEach(() => {
vi.restoreAllMocks()
})
function flushRaf() {
const callbacks = [...rafCallbacks.values()]
rafCallbacks.clear()
for (const cb of callbacks) {
cb(performance.now())
}
}
it('coalesces multiple rapid calls into a single getBoundingClientRect read', () => {
const { updateClipPath } = useDomClipping()
const element = createMockElement({
left: 10,
top: 10,
width: 100,
height: 50
})
const canvas = createMockCanvas({
left: 0,
top: 0,
width: 800,
height: 600
})
updateClipPath(element, canvas, true)
updateClipPath(element, canvas, true)
updateClipPath(element, canvas, true)
expect(element.getBoundingClientRect).not.toHaveBeenCalled()
flushRaf()
expect(element.getBoundingClientRect).toHaveBeenCalledTimes(1)
expect(canvas.getBoundingClientRect).toHaveBeenCalledTimes(1)
})
it('updates style ref after RAF fires', () => {
const { style, updateClipPath } = useDomClipping()
const element = createMockElement({
left: 10,
top: 10,
width: 100,
height: 50
})
const canvas = createMockCanvas({
left: 0,
top: 0,
width: 800,
height: 600
})
updateClipPath(element, canvas, true)
expect(style.value).toEqual({})
flushRaf()
expect(style.value).toEqual({
clipPath: 'none',
willChange: 'clip-path'
})
})
it('cancels previous RAF when called again before it fires', () => {
const { style, updateClipPath } = useDomClipping()
const element1 = createMockElement({
left: 10,
top: 10,
width: 100,
height: 50
})
const element2 = createMockElement({
left: 20,
top: 20,
width: 200,
height: 100
})
const canvas = createMockCanvas({
left: 0,
top: 0,
width: 800,
height: 600
})
updateClipPath(element1, canvas, true)
updateClipPath(element2, canvas, true)
expect(cancelAnimationFrame).toHaveBeenCalledTimes(1)
flushRaf()
expect(element1.getBoundingClientRect).not.toHaveBeenCalled()
expect(element2.getBoundingClientRect).toHaveBeenCalledTimes(1)
expect(style.value).toEqual({
clipPath: 'none',
willChange: 'clip-path'
})
})
it('generates clip-path polygon when element intersects unselected area', () => {
const { style, updateClipPath } = useDomClipping()
const element = createMockElement({
left: 50,
top: 50,
width: 100,
height: 100
})
const canvas = createMockCanvas({
left: 0,
top: 0,
width: 800,
height: 600
})
const selectedArea = {
x: 40,
y: 40,
width: 200,
height: 200,
scale: 1,
offset: [0, 0] as [number, number]
}
updateClipPath(element, canvas, false, selectedArea)
flushRaf()
expect(style.value.clipPath).toContain('polygon')
expect(style.value.willChange).toBe('clip-path')
})
it('does not read layout before RAF fires', () => {
const { updateClipPath } = useDomClipping()
const element = createMockElement({
left: 0,
top: 0,
width: 50,
height: 50
})
const canvas = createMockCanvas({
left: 0,
top: 0,
width: 800,
height: 600
})
updateClipPath(element, canvas, true)
expect(element.getBoundingClientRect).not.toHaveBeenCalled()
expect(canvas.getBoundingClientRect).not.toHaveBeenCalled()
})
})

View File

@@ -85,12 +85,8 @@ export const useDomClipping = (options: ClippingOptions = {}) => {
return ''
}
let pendingRaf = 0
/**
* Updates the clip-path style based on element and selection information.
* Batched via requestAnimationFrame to avoid forcing synchronous layout
* from getBoundingClientRect() on every reactive state change.
* Updates the clip-path style based on element and selection information
*/
const updateClipPath = (
element: HTMLElement,
@@ -105,24 +101,20 @@ export const useDomClipping = (options: ClippingOptions = {}) => {
offset: [number, number]
}
) => {
if (pendingRaf) cancelAnimationFrame(pendingRaf)
pendingRaf = requestAnimationFrame(() => {
pendingRaf = 0
const elementRect = element.getBoundingClientRect()
const canvasRect = canvasElement.getBoundingClientRect()
const elementRect = element.getBoundingClientRect()
const canvasRect = canvasElement.getBoundingClientRect()
const clipPath = calculateClipPath(
elementRect,
canvasRect,
isSelected,
selectedArea
)
const clipPath = calculateClipPath(
elementRect,
canvasRect,
isSelected,
selectedArea
)
style.value = {
clipPath: clipPath || 'none',
willChange: 'clip-path'
}
})
style.value = {
clipPath: clipPath || 'none',
willChange: 'clip-path'
}
}
return {

View File

@@ -14,7 +14,6 @@ import type {
} from '@/lib/litegraph/src/interfaces'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeId } from '@/renderer/core/layout/types'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -443,11 +442,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
const nodePosition = { x: node.pos[0], y: node.pos[1] }
const nodeSize = { width: node.size[0], height: node.size[1] }
// Skip layout creation if it already exists
// (e.g. in-place node replacement where the old node's layout is reused for the new node with the same ID).
const existingLayout = layoutStore.getNodeLayoutRef(id).value
if (existingLayout) return
// Add node to layout store with final positions
setSource(LayoutSource.Canvas)
void createNode(id, {

View File

@@ -2,11 +2,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useImagePreviewWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget'
export const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview'
const CANVAS_IMAGE_PREVIEW_NODE_TYPES = new Set([
'PreviewImage',
'SaveImage',
'GLSLShader'
])
const CANVAS_IMAGE_PREVIEW_NODE_TYPES = new Set(['PreviewImage', 'SaveImage'])
export function supportsVirtualCanvasImagePreview(node: LGraphNode): boolean {
return CANVAS_IMAGE_PREVIEW_NODE_TYPES.has(node.type)

View File

@@ -11,7 +11,7 @@ import type { TaskItemImpl } from '@/stores/queueStore'
type TestTask = {
jobId: string
job: { priority: number }
queueIndex: number
mockState: JobState
executionTime?: number
executionEndTimestamp?: number
@@ -174,7 +174,7 @@ const createTask = (
overrides: Partial<TestTask> & { mockState?: JobState } = {}
): TestTask => ({
jobId: overrides.jobId ?? `task-${Math.random().toString(36).slice(2, 7)}`,
job: overrides.job ?? { priority: 0 },
queueIndex: overrides.queueIndex ?? 0,
mockState: overrides.mockState ?? 'pending',
executionTime: overrides.executionTime,
executionEndTimestamp: overrides.executionEndTimestamp,
@@ -258,7 +258,7 @@ describe('useJobList', () => {
it('tracks recently added pending jobs and clears the hint after expiry', async () => {
vi.useFakeTimers()
queueStoreMock.pendingTasks = [
createTask({ jobId: '1', job: { priority: 1 }, mockState: 'pending' })
createTask({ jobId: '1', queueIndex: 1, mockState: 'pending' })
]
const { jobItems } = initComposable()
@@ -287,7 +287,7 @@ describe('useJobList', () => {
vi.useFakeTimers()
const taskId = '2'
queueStoreMock.pendingTasks = [
createTask({ jobId: taskId, job: { priority: 1 }, mockState: 'pending' })
createTask({ jobId: taskId, queueIndex: 1, mockState: 'pending' })
]
const { jobItems } = initComposable()
@@ -300,7 +300,7 @@ describe('useJobList', () => {
vi.mocked(buildJobDisplay).mockClear()
queueStoreMock.pendingTasks = [
createTask({ jobId: taskId, job: { priority: 2 }, mockState: 'pending' })
createTask({ jobId: taskId, queueIndex: 2, mockState: 'pending' })
]
await flush()
jobItems.value
@@ -314,7 +314,7 @@ describe('useJobList', () => {
it('cleans up timeouts on unmount', async () => {
vi.useFakeTimers()
queueStoreMock.pendingTasks = [
createTask({ jobId: '3', job: { priority: 1 }, mockState: 'pending' })
createTask({ jobId: '3', queueIndex: 1, mockState: 'pending' })
]
initComposable()
@@ -331,7 +331,7 @@ describe('useJobList', () => {
queueStoreMock.pendingTasks = [
createTask({
jobId: 'p',
job: { priority: 1 },
queueIndex: 1,
mockState: 'pending',
createTime: 3000
})
@@ -339,7 +339,7 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'r',
job: { priority: 5 },
queueIndex: 5,
mockState: 'running',
createTime: 2000
})
@@ -347,7 +347,7 @@ describe('useJobList', () => {
queueStoreMock.historyTasks = [
createTask({
jobId: 'h',
job: { priority: 3 },
queueIndex: 3,
mockState: 'completed',
createTime: 1000,
executionEndTimestamp: 5000
@@ -366,9 +366,9 @@ describe('useJobList', () => {
it('filters by job tab and resets failed tab when failures disappear', async () => {
queueStoreMock.historyTasks = [
createTask({ jobId: 'c', job: { priority: 3 }, mockState: 'completed' }),
createTask({ jobId: 'f', job: { priority: 2 }, mockState: 'failed' }),
createTask({ jobId: 'p', job: { priority: 1 }, mockState: 'pending' })
createTask({ jobId: 'c', queueIndex: 3, mockState: 'completed' }),
createTask({ jobId: 'f', queueIndex: 2, mockState: 'failed' }),
createTask({ jobId: 'p', queueIndex: 1, mockState: 'pending' })
]
const instance = initComposable()
@@ -384,7 +384,7 @@ describe('useJobList', () => {
expect(instance.hasFailedJobs.value).toBe(true)
queueStoreMock.historyTasks = [
createTask({ jobId: 'c', job: { priority: 3 }, mockState: 'completed' })
createTask({ jobId: 'c', queueIndex: 3, mockState: 'completed' })
]
await flush()
@@ -396,13 +396,13 @@ describe('useJobList', () => {
queueStoreMock.pendingTasks = [
createTask({
jobId: 'wf-1',
job: { priority: 2 },
queueIndex: 2,
mockState: 'pending',
workflowId: 'workflow-1'
}),
createTask({
jobId: 'wf-2',
job: { priority: 1 },
queueIndex: 1,
mockState: 'pending',
workflowId: 'workflow-2'
})
@@ -426,14 +426,14 @@ describe('useJobList', () => {
queueStoreMock.historyTasks = [
createTask({
jobId: 'alpha',
job: { priority: 2 },
queueIndex: 2,
mockState: 'completed',
createTime: 2000,
executionEndTimestamp: 2000
}),
createTask({
jobId: 'beta',
job: { priority: 1 },
queueIndex: 1,
mockState: 'failed',
createTime: 1000,
executionEndTimestamp: 1000
@@ -471,13 +471,13 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'active',
job: { priority: 3 },
queueIndex: 3,
mockState: 'running',
executionTime: 7_200_000
}),
createTask({
jobId: 'other',
job: { priority: 2 },
queueIndex: 2,
mockState: 'running',
executionTime: 3_600_000
})
@@ -507,7 +507,7 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'live-preview',
job: { priority: 1 },
queueIndex: 1,
mockState: 'running'
})
]
@@ -526,7 +526,7 @@ describe('useJobList', () => {
queueStoreMock.runningTasks = [
createTask({
jobId: 'disabled-preview',
job: { priority: 1 },
queueIndex: 1,
mockState: 'running'
})
]
@@ -567,28 +567,28 @@ describe('useJobList', () => {
queueStoreMock.historyTasks = [
createTask({
jobId: 'today-small',
job: { priority: 4 },
queueIndex: 4,
mockState: 'completed',
executionEndTimestamp: Date.now(),
executionTime: 2_000
}),
createTask({
jobId: 'today-large',
job: { priority: 3 },
queueIndex: 3,
mockState: 'completed',
executionEndTimestamp: Date.now(),
executionTime: 5_000
}),
createTask({
jobId: 'yesterday',
job: { priority: 2 },
queueIndex: 2,
mockState: 'failed',
executionEndTimestamp: Date.now() - 86_400_000,
executionTime: 1_000
}),
createTask({
jobId: 'undated',
job: { priority: 1 },
queueIndex: 1,
mockState: 'pending'
})
]

View File

@@ -1,47 +0,0 @@
import { computed, ref } from 'vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
export type AppMode = 'graph' | 'app' | 'builder:select' | 'builder:arrange'
const enableAppBuilder = ref(true)
export function useAppMode() {
const workflowStore = useWorkflowStore()
const mode = computed(
() =>
workflowStore.activeWorkflow?.activeMode ??
workflowStore.activeWorkflow?.initialMode ??
'graph'
)
const isBuilderMode = computed(
() => isSelectMode.value || isArrangeMode.value
)
const isSelectMode = computed(() => mode.value === 'builder:select')
const isArrangeMode = computed(() => mode.value === 'builder:arrange')
const isAppMode = computed(
() => mode.value === 'app' || mode.value === 'builder:arrange'
)
const isGraphMode = computed(
() => mode.value === 'graph' || mode.value === 'builder:select'
)
function setMode(newMode: AppMode) {
if (newMode === mode.value) return
const workflow = workflowStore.activeWorkflow
if (workflow) workflow.activeMode = newMode
}
return {
mode,
enableAppBuilder,
isBuilderMode,
isSelectMode,
isArrangeMode,
isAppMode,
isGraphMode,
setMode
}
}

View File

@@ -4,32 +4,64 @@ import { useToast } from 'primevue/usetoast'
import { t } from '@/i18n'
export function useCopyToClipboard() {
const { copy, copied } = useClipboard({ legacy: true })
const { copy, copied } = useClipboard()
const toast = useToast()
const showSuccessToast = () => {
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('clipboard.successMessage'),
life: 3000
})
}
const showErrorToast = () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('clipboard.errorMessage')
})
}
async function copyToClipboard(text: string) {
function fallbackCopy(text: string) {
const textarea = document.createElement('textarea')
textarea.setAttribute('readonly', '')
textarea.value = text
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
textarea.setAttribute('aria-hidden', 'true')
textarea.setAttribute('tabindex', '-1')
textarea.style.width = '1px'
textarea.style.height = '1px'
document.body.appendChild(textarea)
textarea.select()
try {
// using legacy document.execCommand for fallback for old and linux browsers
const successful = document.execCommand('copy')
if (successful) {
showSuccessToast()
} else {
showErrorToast()
}
} catch (err) {
showErrorToast()
} finally {
textarea.remove()
}
}
const copyToClipboard = async (text: string) => {
try {
await copy(text)
if (copied.value) {
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('clipboard.successMessage'),
life: 3000
})
showSuccessToast()
} else {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('clipboard.errorMessage')
})
// If VueUse copy failed, try fallback
fallbackCopy(text)
}
} catch {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('clipboard.errorMessage')
})
} catch (err) {
// VueUse copy failed, try fallback
fallbackCopy(text)
}
}

View File

@@ -71,8 +71,7 @@ import { useDialogStore } from '@/stores/dialogStore'
const moveSelectedNodesVersionAdded = '1.22.2'
export function useCoreCommands(): ComfyCommand[] {
const { isActiveSubscription, showSubscriptionDialog, subscription } =
useBillingContext()
const { isActiveSubscription, showSubscriptionDialog } = useBillingContext()
const workflowService = useWorkflowService()
const workflowStore = useWorkflowStore()
const settingsDialog = useSettingsDialog()
@@ -496,10 +495,7 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton({
...metadata,
current_tier: subscription.value?.tier?.toLowerCase()
})
useTelemetry()?.trackRunButton(metadata)
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -522,10 +518,7 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton({
...metadata,
current_tier: subscription.value?.tier?.toLowerCase()
})
useTelemetry()?.trackRunButton(metadata)
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -547,10 +540,7 @@ export function useCoreCommands(): ComfyCommand[] {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
}) => {
useTelemetry()?.trackRunButton({
...metadata,
current_tier: subscription.value?.tier?.toLowerCase()
})
useTelemetry()?.trackRunButton(metadata)
if (!isActiveSubscription.value) {
showSubscriptionDialog()
return
@@ -1348,6 +1338,8 @@ export function useCoreCommands(): ComfyCommand[] {
typeof metadata?.source === 'string' ? metadata.source : 'keybind'
const newMode = !canvasStore.linearMode
if (newMode) useTelemetry()?.trackEnterLinear({ source })
app.rootGraph.extra.linearMode = newMode
workflowStore.activeWorkflow?.changeTracker?.checkState()
canvasStore.linearMode = newMode
}
}

View File

@@ -2,7 +2,7 @@ import { computed, onBeforeUnmount, ref } from 'vue'
import type { Ref } from 'vue'
import { createMonotoneInterpolator } from '@/components/curve/curveUtils'
import type { CurvePoint } from '@/components/curve/types'
import type { CurvePoint } from '@/lib/litegraph/src/types/widgets'
interface UseCurveEditorOptions {
svgRef: Ref<SVGSVGElement | null>
@@ -21,12 +21,11 @@ export function useCurveEditor({ svgRef, modelValue }: UseCurveEditorOptions) {
const xMin = points[0][0]
const xMax = points[points.length - 1][0]
const segments = 128
const range = xMax - xMin
const parts: string[] = []
for (let i = 0; i <= segments; i++) {
const x = xMin + range * (i / segments)
const x = xMin + (xMax - xMin) * (i / segments)
const y = 1 - interpolate(x)
parts.push(`${i === 0 ? 'M' : 'L'}${x},${y}`)
parts.push(`${i === 0 ? 'M' : 'L'}${x.toFixed(4)},${y.toFixed(4)}`)
}
return parts.join('')
})

View File

@@ -1,44 +0,0 @@
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { useNodeReplacementStore } from '@/platform/nodeReplacement/nodeReplacementStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import type { MissingNodeType } from '@/types/comfy'
import {
collectAllNodes,
getExecutionIdByNode
} from '@/utils/graphTraversalUtil'
import { getCnrIdFromNode } from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
/** Scan the live graph for unregistered node types and build a full MissingNodeType list. */
function scanMissingNodes(rootGraph: LGraph): MissingNodeType[] {
const nodeReplacementStore = useNodeReplacementStore()
const missingNodeTypes: MissingNodeType[] = []
const allNodes = collectAllNodes(rootGraph)
for (const node of allNodes) {
const originalType = node.last_serialization?.type ?? node.type ?? 'Unknown'
if (originalType in LiteGraph.registered_node_types) continue
const cnrId = getCnrIdFromNode(node)
const replacement = nodeReplacementStore.getReplacementFor(originalType)
const executionId = getExecutionIdByNode(rootGraph, node)
missingNodeTypes.push({
type: originalType,
nodeId: executionId ?? String(node.id),
cnrId,
isReplaceable: replacement !== null,
replacement: replacement ?? undefined
})
}
return missingNodeTypes
}
/** Re-scan the graph for missing nodes and update the error store. */
export function rescanAndSurfaceMissingNodes(rootGraph: LGraph): void {
const types = scanMissingNodes(rootGraph)
useExecutionErrorStore().surfaceMissingNodes(types)
}

View File

@@ -1,62 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
ESSENTIALS_CATEGORIES,
ESSENTIALS_CATEGORY_CANONICAL,
ESSENTIALS_CATEGORY_MAP,
ESSENTIALS_NODES,
TOOLKIT_BLUEPRINT_MODULES,
TOOLKIT_NOVEL_NODE_NAMES
} from './essentialsNodes'
describe('essentialsNodes', () => {
it('has no duplicate node names across categories', () => {
const seen = new Map<string, string>()
for (const [category, nodes] of Object.entries(ESSENTIALS_NODES)) {
for (const node of nodes) {
expect(
seen.has(node),
`"${node}" duplicated in "${category}" and "${seen.get(node)}"`
).toBe(false)
seen.set(node, category)
}
}
})
it('ESSENTIALS_CATEGORY_MAP covers every node in ESSENTIALS_NODES', () => {
for (const [category, nodes] of Object.entries(ESSENTIALS_NODES)) {
for (const node of nodes) {
expect(ESSENTIALS_CATEGORY_MAP[node]).toBe(category)
}
}
})
it('TOOLKIT_NOVEL_NODE_NAMES excludes basics nodes', () => {
for (const basicNode of ESSENTIALS_NODES.basics) {
expect(TOOLKIT_NOVEL_NODE_NAMES.has(basicNode)).toBe(false)
}
})
it('TOOLKIT_NOVEL_NODE_NAMES excludes SubgraphBlueprint-prefixed nodes', () => {
for (const name of TOOLKIT_NOVEL_NODE_NAMES) {
expect(name.startsWith('SubgraphBlueprint.')).toBe(false)
}
})
it('ESSENTIALS_NODES keys match ESSENTIALS_CATEGORIES', () => {
const nodeKeys = Object.keys(ESSENTIALS_NODES)
expect(nodeKeys).toEqual([...ESSENTIALS_CATEGORIES])
})
it('TOOLKIT_BLUEPRINT_MODULES contains comfy_essentials', () => {
expect(TOOLKIT_BLUEPRINT_MODULES.has('comfy_essentials')).toBe(true)
})
it('ESSENTIALS_CATEGORY_CANONICAL maps every category case-insensitively', () => {
for (const category of ESSENTIALS_CATEGORIES) {
expect(ESSENTIALS_CATEGORY_CANONICAL.get(category.toLowerCase())).toBe(
category
)
}
})
})

View File

@@ -1,115 +0,0 @@
/**
* Single source of truth for Essentials tab node categorization and ordering.
*
* Adding a new node to the Essentials tab? Add it here and nowhere else.
*
* Source: https://www.notion.so/comfy-org/2fe6d73d365080d0a951d14cdf540778
*/
export const ESSENTIALS_CATEGORIES = [
'basics',
'text generation',
'image generation',
'video generation',
'image tools',
'video tools',
'audio',
'3D'
] as const
export type EssentialsCategory = (typeof ESSENTIALS_CATEGORIES)[number]
/**
* Ordered list of nodes per category.
* Array order = display order in the Essentials tab.
* Presence in a category = the node's essentials_category mock fallback.
*/
export const ESSENTIALS_NODES: Record<EssentialsCategory, readonly string[]> = {
basics: [
'LoadImage',
'LoadVideo',
'Load3D',
'SaveImage',
'SaveVideo',
'SaveGLB',
'PrimitiveStringMultiline',
'PreviewImage'
],
'text generation': ['OpenAIChatNode'],
'image generation': [
'LoraLoader',
'LoraLoaderModelOnly',
'ConditioningCombine'
],
'video generation': [
'SubgraphBlueprint.pose_to_video_ltx_2_0',
'SubgraphBlueprint.canny_to_video_ltx_2_0',
'KlingLipSyncAudioToVideoNode',
'KlingOmniProEditVideoNode'
],
'image tools': [
'ImageBatch',
'ImageCrop',
'ImageCropV2',
'ImageScale',
'ImageScaleBy',
'ImageRotate',
'ImageBlur',
'ImageBlend',
'ImageInvert',
'ImageCompare',
'Canny',
'RecraftRemoveBackgroundNode',
'RecraftVectorizeImageNode',
'LoadImageMask',
'GLSLShader'
],
'video tools': ['GetVideoComponents', 'CreateVideo', 'Video Slice'],
audio: [
'LoadAudio',
'SaveAudio',
'SaveAudioMP3',
'StabilityTextToAudio',
'EmptyLatentAudio'
],
'3D': ['TencentTextToModelNode', 'TencentImageToModelNode']
}
/**
* Flat map: node name → category (derived from ESSENTIALS_NODES).
* Used as mock/fallback when backend doesn't provide essentials_category.
*/
export const ESSENTIALS_CATEGORY_MAP: Record<string, EssentialsCategory> =
Object.fromEntries(
Object.entries(ESSENTIALS_NODES).flatMap(([category, nodes]) =>
nodes.map((node) => [node, category])
)
) as Record<string, EssentialsCategory>
/**
* Case-insensitive lookup: lowercase category → canonical category.
* Used to normalize backend categories (which may be title-cased) to the
* canonical form used in ESSENTIALS_CATEGORIES.
*/
export const ESSENTIALS_CATEGORY_CANONICAL: ReadonlyMap<
string,
EssentialsCategory
> = new Map(ESSENTIALS_CATEGORIES.map((c) => [c.toLowerCase(), c]))
/**
* "Novel" toolkit nodes for telemetry — basics excluded.
* Derived from ESSENTIALS_NODES minus the 'basics' category.
*/
export const TOOLKIT_NOVEL_NODE_NAMES: ReadonlySet<string> = new Set(
Object.entries(ESSENTIALS_NODES)
.filter(([cat]) => cat !== 'basics')
.flatMap(([, nodes]) => nodes)
.filter((n) => !n.startsWith('SubgraphBlueprint.'))
)
/**
* python_module values that identify toolkit blueprint nodes.
*/
export const TOOLKIT_BLUEPRINT_MODULES: ReadonlySet<string> = new Set([
'comfy_essentials'
])

View File

@@ -1,11 +0,0 @@
/** Default panel size (%) for sidebar and builder panels */
export const SIDE_PANEL_SIZE = 20
/** Default panel size (%) for the center/main panel */
export const CENTER_PANEL_SIZE = 80
/** Minimum panel size (%) for the sidebar */
export const SIDEBAR_MIN_SIZE = 10
/** Minimum panel size (%) for the builder panel */
export const BUILDER_MIN_SIZE = 15

View File

@@ -1,10 +1,38 @@
/**
* Toolkit (Essentials) node detection constants.
*
* Re-exported from essentialsNodes.ts — the single source of truth.
* Used by telemetry to track toolkit node adoption and popularity.
* Only novel nodes — basic nodes (LoadImage, SaveImage, etc.) are excluded.
*
* Source: https://www.notion.so/comfy-org/2fe6d73d365080d0a951d14cdf540778
*/
export {
TOOLKIT_NOVEL_NODE_NAMES as TOOLKIT_NODE_NAMES,
TOOLKIT_BLUEPRINT_MODULES
} from './essentialsNodes'
/**
* Canonical node type names for individual toolkit nodes.
*/
export const TOOLKIT_NODE_NAMES: ReadonlySet<string> = new Set([
// Image Tools
'ImageCrop',
'ImageRotate',
'ImageBlur',
'ImageInvert',
'ImageCompare',
'Canny',
// Video Tools
'Video Slice',
// API Nodes
'RecraftRemoveBackgroundNode',
'RecraftVectorizeImageNode',
'KlingOmniProEditVideoNode'
])
/**
* python_module values that identify toolkit blueprint nodes.
* Essentials blueprints are registered with node_pack 'comfy_essentials',
* which maps to python_module on the node def.
*/
export const TOOLKIT_BLUEPRINT_MODULES: ReadonlySet<string> = new Set([
'comfy_essentials'
])

View File

@@ -201,8 +201,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
projected.drawWidget(ctx, {
width: widgetWidth,
showText: !lowQuality,
suppressPromotedOutline: true,
previewImages: resolved.node.imgs
suppressPromotedOutline: true
})
projected.y = originalY

View File

@@ -184,17 +184,6 @@ describe('getPromotableWidgets', () => {
).toBe(true)
})
it('adds virtual canvas preview widget for GLSLShader nodes', () => {
const node = new LGraphNode('GLSLShader')
node.type = 'GLSLShader'
const widgets = getPromotableWidgets(node)
expect(
widgets.some((widget) => widget.name === CANVAS_IMAGE_PREVIEW_WIDGET)
).toBe(true)
})
it('does not add virtual canvas preview widget for non-image nodes', () => {
const node = new LGraphNode('TextNode')
node.addOutput('TEXT', 'STRING')
@@ -243,25 +232,4 @@ describe('promoteRecommendedWidgets', () => {
expect(updatePreviewsMock).not.toHaveBeenCalled()
})
it('eagerly promotes virtual preview widget for CANVAS_IMAGE_PREVIEW nodes', () => {
const subgraph = createTestSubgraph()
const subgraphNode = createTestSubgraphNode(subgraph)
const glslNode = new LGraphNode('GLSLShader')
glslNode.type = 'GLSLShader'
subgraph.add(glslNode)
promoteRecommendedWidgets(subgraphNode)
const store = usePromotionStore()
expect(
store.isPromoted(
subgraphNode.rootGraph.id,
subgraphNode.id,
String(glslNode.id),
CANVAS_IMAGE_PREVIEW_WIDGET
)
).toBe(true)
expect(updatePreviewsMock).not.toHaveBeenCalled()
})
})

View File

@@ -227,29 +227,6 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
// defer. Core $$ preview widgets are the lazy path that needs updatePreviews.
if (hasPreviewWidget()) continue
// Nodes in CANVAS_IMAGE_PREVIEW_NODE_TYPES support a virtual $$
// preview widget. Eagerly promote it so getPseudoWidgetPreviewTargets
// includes this node and onDrawBackground can call updatePreviews on it
// once execution outputs arrive.
if (supportsVirtualCanvasImagePreview(node)) {
if (
!store.isPromoted(
subgraphNode.rootGraph.id,
subgraphNode.id,
String(node.id),
CANVAS_IMAGE_PREVIEW_WIDGET
)
) {
store.promote(
subgraphNode.rootGraph.id,
subgraphNode.id,
String(node.id),
CANVAS_IMAGE_PREVIEW_WIDGET
)
}
continue
}
// Also schedule a deferred check: core $$ widgets are created lazily by
// updatePreviews when node outputs are first loaded.
requestAnimationFrame(() => updatePreviews(node, promotePreviewWidget))

View File

@@ -1,6 +1,7 @@
import { computed } from 'vue'
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
import { t } from '@/i18n'
import { useExtensionService } from '@/services/extensionService'
import type { TopbarBadge } from '@/types/comfy'
@@ -20,7 +21,7 @@ const badges = computed<TopbarBadge[]>(() => {
// Always add cloud badge last (furthest right)
result.push({
icon: 'icon-[lucide--cloud]',
label: t('g.beta'),
text: 'Comfy Cloud'
})

View File

@@ -1,5 +1,4 @@
import type { Bounds } from '@/renderer/core/layout/types'
import type { CurvePoint } from '@/components/curve/types'
import type {
CanvasColour,
@@ -331,6 +330,8 @@ export interface IBoundingBoxWidget extends IBaseWidget<Bounds, 'boundingbox'> {
value: Bounds
}
export type CurvePoint = [x: number, y: number]
export interface ICurveWidget extends IBaseWidget<CurvePoint[], 'curve'> {
type: 'curve'
value: CurvePoint[]

View File

@@ -27,8 +27,6 @@ export interface DrawWidgetOptions {
showText?: boolean
/** When true, suppresses the promoted outline color (e.g. for projected copies on SubgraphNode). */
suppressPromotedOutline?: boolean
/** Transient image source for preview widgets rendered on behalf of another node (e.g. subgraph promotion). */
previewImages?: HTMLImageElement[]
}
interface DrawTruncatingTextOptions extends DrawWidgetOptions {
@@ -142,9 +140,6 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
this._state = useWidgetValueStore().registerWidget(graphId, {
...this._state,
// BaseWidget: this.value getter returns this._state.value. So value: this.value === value: this._state.value.
// BaseDOMWidgetImpl: this.value getter returns options.getValue?.() ?? ''. Resolves the correct initial value instead of undefined.
// I.e., calls overriden getter -> options.getValue() -> correct value (https://github.com/Comfy-Org/ComfyUI_frontend/issues/9194).
value: this.value,
nodeId
})

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "ملفات إدخال أوبن إيه آي",
"PHOTOMAKER": "صانع الصور",
"PIXVERSE_TEMPLATE": "قالب PixVerse",
"POSE_KEYPOINT": "نقطة مفتاحية للوضعية",
"RECRAFT_COLOR": "لون Recraft",
"RECRAFT_CONTROLS": "عناصر تحكم Recraft",
"RECRAFT_V3_STYLE": "نمط Recraft V3",
@@ -959,7 +958,6 @@
"imageUrl": "رابط الصورة",
"import": "استيراد",
"inProgress": "جارٍ التنفيذ",
"inSubgraph": "في الرسم البياني الفرعي '{name}'",
"increment": "زيادة",
"info": "معلومات العقدة",
"input": "إدخال",
@@ -1117,7 +1115,6 @@
"updated": "تم التحديث",
"updating": "جارٍ التحديث",
"upload": "رفع",
"uploadAlreadyInProgress": "الرفع جارٍ بالفعل",
"usageHint": "تلميح الاستخدام",
"use": "استخدم",
"user": "المستخدم",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "تنزيل الكل",
"dragAndDropImage": "اسحب وأسقط صورة",
"giveFeedback": "إعطاء ملاحظات",
"graphMode": "وضع الرسم البياني",
"linearMode": "وضع التطبيق",
"queue": {
"clear": "مسح قائمة الانتظار",
"clickToClear": "انقر لمسح قائمة الانتظار"
},
"rerun": "تشغيل مجدد",
"reuseParameters": "إعادة استخدام المعلمات",
"runCount": "عدد مرات التشغيل:",
@@ -1583,9 +1575,6 @@
"nodePack": "حزمة العقد",
"nodePackInfo": "معلومات حزمة العقد",
"notAvailable": "غير متوفر",
"packInstall": {
"nodeIdRequired": "معرّف العقدة مطلوب للتثبيت"
},
"packsSelected": "الحزم المحددة",
"repository": "المستودع",
"restartToApplyChanges": "لـتطبيق التغييرات، يرجى إعادة تشغيل ComfyUI",
@@ -2083,10 +2072,7 @@
"openNodeManager": "فتح مدير العقد",
"quickFixAvailable": "إصلاح سريع متاح",
"redHighlight": "أحمر",
"replaceAll": "استبدال الكل",
"replaceAllWarning": "سيتم استبدال جميع العقد المتاحة في هذه المجموعة.",
"replaceFailed": "فشل في استبدال العقد",
"replaceNode": "استبدال العقدة",
"replaceSelected": "استبدال المحدد ({count})",
"replaceWarning": "سيؤدي هذا إلى تعديل سير العمل بشكل دائم. احفظ نسخة أولاً إذا لم تكن متأكدًا.",
"replaceable": "قابل للاستبدال",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "تم استبدال {count} نوع/أنواع من العقد",
"replacedNode": "تم استبدال العقدة: {nodeType}",
"selectAll": "تحديد الكل",
"skipForNow": "تخطي الآن",
"swapNodesGuide": "يمكن استبدال العقد التالية تلقائيًا ببدائل متوافقة.",
"swapNodesTitle": "تبديل العقد",
"unknownNode": "غير معروف",
"willBeReplacedBy": "سيتم استبدال هذه العقدة بـ:"
"skipForNow": "تخطي الآن"
},
"nodeTemplates": {
"enterName": "أدخل الاسم",
@@ -2117,19 +2099,6 @@
},
"title": "جهازك غير مدعوم"
},
"painter": {
"background": "الخلفية",
"brush": "فرشاة",
"clear": "مسح",
"color": "منتقي اللون",
"eraser": "ممحاة",
"hardness": "الصلابة",
"height": "الارتفاع",
"size": "حجم المؤشر",
"tool": "أداة",
"uploadError": "فشل في رفع صورة الرسام: {status} - {statusText}",
"width": "العرض"
},
"progressToast": {
"allDownloadsCompleted": "اكتملت جميع التنزيلات",
"downloadingModel": "جاري تنزيل النموذج...",
@@ -2250,22 +2219,6 @@
"inputsNone": "لا توجد مدخلات",
"inputsNoneTooltip": "العقدة ليس لديها مدخلات",
"locateNode": "تحديد موقع العقدة على اللوحة",
"missingNodePacks": {
"applyChanges": "تطبيق التغييرات",
"cloudMessage": "يتطلب سير العمل هذا عقدًا مخصصة غير متوفرة بعد على Comfy Cloud.",
"collapse": "طي",
"expand": "توسيع",
"installAll": "تثبيت الكل",
"installNodePack": "تثبيت حزمة العقد",
"installed": "تم التثبيت",
"installing": "جارٍ التثبيت...",
"ossMessage": "يستخدم سير العمل هذا عقدًا مخصصة لم تقم بتثبيتها بعد.",
"searchInManager": "البحث في مدير العقد",
"title": "حزم العقد المفقودة",
"unknownPack": "حزمة غير معروفة",
"unsupportedTitle": "حزم العقد غير المدعومة",
"viewInManager": "عرض في المدير"
},
"mute": "كتم",
"noErrors": "لا توجد أخطاء",
"noSelection": "حدد عقدة لعرض خصائصها ومعلوماتها.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "قص وتغيير حجم المناطق من دفعة الصور المدخلة بناءً على مربعات التحديد المقدمة.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "مربعات التحديد"
},
"image": {
"name": "الصورة"
},
"output_height": {
"name": "ارتفاع الناتج",
"tooltip": "الارتفاع الذي يتم تغيير حجم كل قص إليه."
},
"output_width": {
"name": "عرض الناتج",
"tooltip": "العرض الذي يتم تغيير حجم كل قص إليه."
},
"padding": {
"name": "هامش إضافي",
"tooltip": "هامش إضافي بالبكسل يُضاف على كل جانب من مربع التحديد قبل القص."
}
},
"outputs": {
"0": {
"tooltip": "جميع القصاصات مكدسة في دفعة صور واحدة."
}
}
},
"CropMask": {
"display_name": "قص القناع",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "إنشاء أو تعديل الصور بشكل متزامن عبر Google Vertex API.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "إذا تم تعيينها إلى 'auto'، سيتم مطابقة نسبة العرض إلى الارتفاع لصورتك المدخلة؛ إذا لم يتم توفير صورة، يتم عادةً إنشاء صورة مربعة بنسبة 16:9."
},
"control_after_generate": {
"name": "control after generate"
},
"files": {
"name": "files",
"tooltip": "ملف (ملفات) اختيارية لاستخدامها كسياق للنموذج. يقبل المدخلات من عقدة Gemini Generate Content Input Files."
},
"images": {
"name": "images",
"tooltip": "صورة (صور) مرجعية اختيارية. لإضافة عدة صور، استخدم عقدة Batch Images (حتى ١٤ صورة)."
},
"model": {
"name": "model"
},
"prompt": {
"name": "prompt",
"tooltip": "وصف نصي للصورة المراد إنشاؤها أو التعديلات المطلوب تطبيقها. أدرج أي قيود أو أنماط أو تفاصيل يجب على النموذج اتباعها."
},
"resolution": {
"name": "resolution",
"tooltip": "دقة الإخراج المستهدفة. بالنسبة لـ 2K/4K يتم استخدام أداة التكبير الأصلية لـ Gemini."
},
"response_modalities": {
"name": "response_modalities"
},
"seed": {
"name": "seed",
"tooltip": "عند تثبيت قيمة seed على رقم محدد، يحاول النموذج تقديم نفس الاستجابة للطلبات المتكررة قدر الإمكان. لا يمكن ضمان نتائج حتمية. كما أن تغيير النموذج أو إعدادات المعلمات مثل درجة العشوائية قد يؤدي إلى اختلاف النتائج حتى مع نفس قيمة seed. بشكل افتراضي، يتم استخدام قيمة seed عشوائية."
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "تعليمات أساسية تحدد سلوك الذكاء الاصطناعي."
},
"thinking_level": {
"name": "thinking_level"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "إنشاء استجابات نصية باستخدام نموذج الذكاء الاصطناعي Gemini من Google. يمكنك تقديم أنواع متعددة من المدخلات (نص، صور، صوت، فيديو) كسياق لإنشاء استجابات أكثر صلة ومعنى.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "رسم الجسم"
},
"draw_face": {
"name": "رسم الوجه"
},
"draw_feet": {
"name": "رسم القدمين"
},
"draw_hands": {
"name": "رسم اليدين"
},
"face_point_size": {
"name": "حجم نقطة الوجه"
},
"keypoints": {
"name": "النقاط الرئيسية"
},
"score_threshold": {
"name": "عتبة الدرجات"
},
"stick_width": {
"name": "عرض الخط"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "إجبار الشكل المربع",
"tooltip": "توسيع المحور الأقصر لمربع التحديد بحيث تكون منطقة القص دائماً مربعة."
},
"keypoints": {
"name": "النقاط الرئيسية"
},
"scale": {
"name": "المقياس",
"tooltip": "معامل ضرب لمساحة مربع التحديد حول كل وجه مكتشف."
}
},
"outputs": {
"0": {
"name": "مربعات التحديد",
"tooltip": "مربعات تحديد الوجه لكل إطار، متوافقة مع مدخل مربعات التحديد في SDPoseKeypointExtractor."
}
}
},
"SDPoseKeypointExtractor": {
"description": "استخراج النقاط الرئيسية للوضعية من الصور باستخدام نموذج SDPose: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "حجم الدفعة"
},
"bboxes": {
"name": "مربعات التحديد",
"tooltip": "مربعات التحديد الاختيارية للحصول على كشف أدق. مطلوبة لاكتشاف عدة أشخاص."
},
"image": {
"name": "الصورة"
},
"model": {
"name": "النموذج"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "النقاط الرئيسية",
"tooltip": "النقاط الرئيسية بتنسيق OpenPose (عرض اللوحة، ارتفاع اللوحة، الأشخاص)"
}
}
},
"SDTurboScheduler": {
"display_name": "جدول SD Turbo",
"inputs": {

View File

@@ -1159,8 +1159,8 @@
"queue": {
"initializingAlmostReady": "Initializing - Almost ready",
"inQueue": "In queue...",
"jobAddedToQueue": "Job queued",
"jobQueueing": "Job queuing",
"jobAddedToQueue": "Job added to queue",
"jobQueueing": "Job queueing",
"completedIn": "Finished in {duration}",
"jobMenu": {
"openAsWorkflowNewTab": "Open as workflow in new tab",
@@ -1703,7 +1703,6 @@
"OPENAI_INPUT_FILES": "OPENAI_INPUT_FILES",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "PIXVERSE_TEMPLATE",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "RECRAFT_COLOR",
"RECRAFT_CONTROLS": "RECRAFT_CONTROLS",
"RECRAFT_V3_STYLE": "RECRAFT_V3_STYLE",
@@ -2274,7 +2273,6 @@
},
"subscribeToRun": "Subscribe",
"subscribeToRunFull": "Subscribe to Run",
"subscribeForMore": "Upgrade",
"subscribeNow": "Subscribe Now",
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
"workspaceNotSubscribed": "This workspace is not on a subscription",
@@ -2998,8 +2996,7 @@
},
"linearMode": {
"linearMode": "App Mode",
"beta": "App mode in beta",
"giveFeedback": "Give feedback",
"beta": "App Mode in Beta - Feedback",
"graphMode": "Graph Mode",
"dragAndDropImage": "Click to browse or drag an image",
"runCount": "Number of runs",
@@ -3042,10 +3039,6 @@
"noOutputs": "No output nodes added yet",
"outputsDesc": "Connect at least one output node so users can see results after running.",
"outputsExample": "Examples: “Save Image” or “Save Video”"
},
"queue": {
"clickToClear": "Click to clear queue",
"clear": "Clear queue"
}
},
"missingNodes": {
@@ -3080,14 +3073,7 @@
"openNodeManager": "Open Node Manager",
"skipForNow": "Skip for Now",
"installMissingNodes": "Install Missing Nodes",
"replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure.",
"swapNodesGuide": "The following nodes can be automatically replaced with compatible alternatives.",
"willBeReplacedBy": "This node will be replaced by:",
"replaceNode": "Replace Node",
"replaceAll": "Replace All",
"unknownNode": "Unknown",
"replaceAllWarning": "Replaces all available nodes in this group.",
"swapNodesTitle": "Swap Nodes"
"replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure."
},
"rightSidePanel": {
"togglePanel": "Toggle properties panel",

View File

@@ -673,7 +673,7 @@
}
},
"ByteDanceSeedreamNode": {
"display_name": "ByteDance Seedream 4.5 & 5.0",
"display_name": "ByteDance Seedream 5.0",
"description": "Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.",
"inputs": {
"model": {
@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"display_name": "CropByBBoxes",
"description": "Crop and resize regions from the input image batch based on provided bounding boxes.",
"inputs": {
"image": {
"name": "image"
},
"bboxes": {
"name": "bboxes"
},
"output_width": {
"name": "output_width",
"tooltip": "Width each crop is resized to."
},
"output_height": {
"name": "output_height",
"tooltip": "Height each crop is resized to."
},
"padding": {
"name": "padding",
"tooltip": "Extra padding in pixels added on each side of the bbox before cropping."
}
},
"outputs": {
"0": {
"tooltip": "All crops stacked into a single image batch."
}
}
},
"CropMask": {
"display_name": "CropMask",
"inputs": {
@@ -3630,57 +3601,6 @@
}
}
},
"GeminiNanoBanana2": {
"display_name": "Nano Banana 2",
"description": "Generate or edit images synchronously via Google Vertex API.",
"inputs": {
"prompt": {
"name": "prompt",
"tooltip": "Text prompt describing the image to generate or the edits to apply. Include any constraints, styles, or details the model should follow."
},
"model": {
"name": "model"
},
"seed": {
"name": "seed",
"tooltip": "When the seed is fixed to a specific value, the model makes a best effort to provide the same response for repeated requests. Deterministic output isn't guaranteed. Also, changing the model or parameter settings, such as the temperature, can cause variations in the response even when you use the same seed value. By default, a random seed value is used."
},
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "If set to 'auto', matches your input image's aspect ratio; if no image is provided, a 16:9 square is usually generated."
},
"resolution": {
"name": "resolution",
"tooltip": "Target output resolution. For 2K/4K the native Gemini upscaler is used."
},
"response_modalities": {
"name": "response_modalities"
},
"thinking_level": {
"name": "thinking_level"
},
"images": {
"name": "images",
"tooltip": "Optional reference image(s). To include multiple images, use the Batch Images node (up to 14)."
},
"files": {
"name": "files",
"tooltip": "Optional file(s) to use as context for the model. Accepts inputs from the Gemini Generate Content Input Files node."
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "Foundational instructions that dictate an AI's behavior."
},
"control_after_generate": {
"name": "control after generate"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"display_name": "Google Gemini",
"description": "Generate text responses with Google's Gemini AI model. You can provide multiple types of inputs (text, images, audio, video) as context for generating more relevant and meaningful responses.",
@@ -13770,90 +13690,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"keypoints": {
"name": "keypoints"
},
"draw_body": {
"name": "draw_body"
},
"draw_hands": {
"name": "draw_hands"
},
"draw_face": {
"name": "draw_face"
},
"draw_feet": {
"name": "draw_feet"
},
"stick_width": {
"name": "stick_width"
},
"face_point_size": {
"name": "face_point_size"
},
"score_threshold": {
"name": "score_threshold"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"keypoints": {
"name": "keypoints"
},
"scale": {
"name": "scale",
"tooltip": "Multiplier for the bounding box area around each detected face."
},
"force_square": {
"name": "force_square",
"tooltip": "Expand the shorter bbox axis so the crop region is always square."
}
},
"outputs": {
"0": {
"name": "bboxes",
"tooltip": "Face bounding boxes per frame, compatible with SDPoseKeypointExtractor bboxes input."
}
}
},
"SDPoseKeypointExtractor": {
"display_name": "SDPoseKeypointExtractor",
"description": "Extract pose keypoints from images using the SDPose model: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"inputs": {
"model": {
"name": "model"
},
"vae": {
"name": "vae"
},
"image": {
"name": "image"
},
"batch_size": {
"name": "batch_size"
},
"bboxes": {
"name": "bboxes",
"tooltip": "Optional bounding boxes for more accurate detections. Required for multi-person detection."
}
},
"outputs": {
"0": {
"name": "keypoints",
"tooltip": "Keypoints in OpenPose frame format (canvas_width, canvas_height, people)"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurboScheduler",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "ARCHIVOS_ENTRADA_OPENAI",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "PLANTILLA PIXVERSE",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "COLOR RECRAFT",
"RECRAFT_CONTROLS": "CONTROLES RECRAFT",
"RECRAFT_V3_STYLE": "ESTILO RECRAFT V3",
@@ -959,7 +958,6 @@
"imageUrl": "URL de la imagen",
"import": "Importar",
"inProgress": "En progreso",
"inSubgraph": "en subgrafo '{name}'",
"increment": "Incrementar",
"info": "Información del Nodo",
"input": "Entrada",
@@ -1117,7 +1115,6 @@
"updated": "Actualizado",
"updating": "Actualizando",
"upload": "Subir",
"uploadAlreadyInProgress": "La carga ya está en curso",
"usageHint": "Sugerencia de uso",
"use": "Usar",
"user": "Usuario",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "Descargar todo",
"dragAndDropImage": "Arrastra y suelta una imagen",
"giveFeedback": "Enviar comentarios",
"graphMode": "Modo gráfico",
"linearMode": "Modo App",
"queue": {
"clear": "Limpiar cola",
"clickToClear": "Haz clic para limpiar la cola"
},
"rerun": "Volver a ejecutar",
"reuseParameters": "Reutilizar parámetros",
"runCount": "Número de ejecuciones:",
@@ -1583,9 +1575,6 @@
"nodePack": "Paquete de Nodos",
"nodePackInfo": "Información del paquete de nodos",
"notAvailable": "No Disponible",
"packInstall": {
"nodeIdRequired": "Se requiere el ID del nodo para la instalación"
},
"packsSelected": "Paquetes Seleccionados",
"repository": "Repositorio",
"restartToApplyChanges": "Para aplicar los cambios, por favor reinicia ComfyUI",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Abrir Administrador de Nodos",
"quickFixAvailable": "Solución rápida disponible",
"redHighlight": "rojo",
"replaceAll": "Reemplazar todo",
"replaceAllWarning": "Reemplaza todos los nodos disponibles en este grupo.",
"replaceFailed": "Error al reemplazar nodos",
"replaceNode": "Reemplazar nodo",
"replaceSelected": "Reemplazar seleccionados ({count})",
"replaceWarning": "Esto modificará permanentemente el flujo de trabajo. Guarda una copia primero si no estás seguro.",
"replaceable": "Reemplazable",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "Reemplazados {count} tipo(s) de nodo",
"replacedNode": "Nodo reemplazado: {nodeType}",
"selectAll": "Seleccionar todo",
"skipForNow": "Omitir por ahora",
"swapNodesGuide": "Los siguientes nodos pueden ser reemplazados automáticamente por alternativas compatibles.",
"swapNodesTitle": "Intercambiar nodos",
"unknownNode": "Desconocido",
"willBeReplacedBy": "Este nodo será reemplazado por:"
"skipForNow": "Omitir por ahora"
},
"nodeTemplates": {
"enterName": "Introduzca el nombre",
@@ -2117,19 +2099,6 @@
},
"title": "Tu dispositivo no es compatible"
},
"painter": {
"background": "Fondo",
"brush": "Pincel",
"clear": "Limpiar",
"color": "Selector de color",
"eraser": "Borrador",
"hardness": "Dureza",
"height": "Alto",
"size": "Tamaño del cursor",
"tool": "Herramienta",
"uploadError": "Error al cargar la imagen del pintor: {status} - {statusText}",
"width": "Ancho"
},
"progressToast": {
"allDownloadsCompleted": "Todas las descargas completadas",
"downloadingModel": "Descargando modelo...",
@@ -2250,22 +2219,6 @@
"inputsNone": "SIN ENTRADAS",
"inputsNoneTooltip": "El nodo no tiene entradas",
"locateNode": "Localizar nodo en el lienzo",
"missingNodePacks": {
"applyChanges": "Aplicar cambios",
"cloudMessage": "Este flujo de trabajo requiere nodos personalizados que aún no están disponibles en Comfy Cloud.",
"collapse": "Colapsar",
"expand": "Expandir",
"installAll": "Instalar todo",
"installNodePack": "Instalar paquete de nodos",
"installed": "Instalado",
"installing": "Instalando...",
"ossMessage": "Este flujo de trabajo utiliza nodos personalizados que aún no has instalado.",
"searchInManager": "Buscar en el Gestor de Nodos",
"title": "Paquetes de nodos faltantes",
"unknownPack": "Paquete desconocido",
"unsupportedTitle": "Paquetes de nodos no compatibles",
"viewInManager": "Ver en el Gestor"
},
"mute": "Silenciar",
"noErrors": "Sin errores",
"noSelection": "Selecciona un nodo para ver sus propiedades e información.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "Recorta y redimensiona regiones del lote de imágenes de entrada según las cajas delimitadoras proporcionadas.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "cajas delimitadoras"
},
"image": {
"name": "imagen"
},
"output_height": {
"name": "alto de salida",
"tooltip": "Alto al que se redimensiona cada recorte."
},
"output_width": {
"name": "ancho de salida",
"tooltip": "Ancho al que se redimensiona cada recorte."
},
"padding": {
"name": "relleno",
"tooltip": "Relleno extra en píxeles añadido a cada lado de la caja antes de recortar."
}
},
"outputs": {
"0": {
"tooltip": "Todos los recortes apilados en un solo lote de imágenes."
}
}
},
"CropMask": {
"display_name": "CropMask",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Genera o edita imágenes de forma síncrona a través de la API de Google Vertex.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "Si se establece en 'auto', coincide con la relación de aspecto de tu imagen de entrada; si no se proporciona imagen, normalmente se genera un cuadrado 16:9."
},
"control_after_generate": {
"name": "control after generate"
},
"files": {
"name": "files",
"tooltip": "Archivo(s) opcional(es) para usar como contexto para el modelo. Acepta entradas del nodo Gemini Generate Content Input Files."
},
"images": {
"name": "images",
"tooltip": "Imagen(es) de referencia opcional(es). Para incluir varias imágenes, utiliza el nodo Batch Images (hasta 14)."
},
"model": {
"name": "model"
},
"prompt": {
"name": "prompt",
"tooltip": "Texto descriptivo de la imagen a generar o de las ediciones a aplicar. Incluye cualquier restricción, estilo o detalle que el modelo deba seguir."
},
"resolution": {
"name": "resolution",
"tooltip": "Resolución de salida objetivo. Para 2K/4K se utiliza el escalador nativo de Gemini."
},
"response_modalities": {
"name": "response_modalities"
},
"seed": {
"name": "seed",
"tooltip": "Cuando la semilla se fija a un valor específico, el modelo intenta proporcionar la misma respuesta en solicitudes repetidas. No se garantiza una salida determinista. Además, cambiar el modelo o los parámetros, como la temperatura, puede causar variaciones en la respuesta incluso usando la misma semilla. Por defecto, se utiliza un valor de semilla aleatorio."
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "Instrucciones fundamentales que dictan el comportamiento de la IA."
},
"thinking_level": {
"name": "thinking_level"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "Genera respuestas de texto con el modelo de IA Gemini de Google. Puede proporcionar múltiples tipos de entradas (texto, imágenes, audio, video) como contexto para generar respuestas más relevantes y significativas.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "dibujar cuerpo"
},
"draw_face": {
"name": "dibujar rostro"
},
"draw_feet": {
"name": "dibujar pies"
},
"draw_hands": {
"name": "dibujar manos"
},
"face_point_size": {
"name": "tamaño de punto facial"
},
"keypoints": {
"name": "puntos clave"
},
"score_threshold": {
"name": "umbral de puntuación"
},
"stick_width": {
"name": "ancho de línea"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "forzar cuadrado",
"tooltip": "Expande el eje más corto de la caja para que la región recortada sea siempre cuadrada."
},
"keypoints": {
"name": "puntos clave"
},
"scale": {
"name": "escala",
"tooltip": "Multiplicador para el área de la caja delimitadora alrededor de cada rostro detectado."
}
},
"outputs": {
"0": {
"name": "cajas delimitadoras",
"tooltip": "Cajas delimitadoras de rostro por fotograma, compatibles con la entrada de cajas delimitadoras de SDPoseKeypointExtractor."
}
}
},
"SDPoseKeypointExtractor": {
"description": "Extrae puntos clave de pose de imágenes usando el modelo SDPose: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "tamaño de lote"
},
"bboxes": {
"name": "cajas delimitadoras",
"tooltip": "Cajas delimitadoras opcionales para detecciones más precisas. Requerido para la detección de múltiples personas."
},
"image": {
"name": "imagen"
},
"model": {
"name": "modelo"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "puntos clave",
"tooltip": "Puntos clave en formato de marco OpenPose (ancho_lienzo, alto_lienzo, personas)"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurboScheduler",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "فایل‌های ورودی OpenAI",
"PHOTOMAKER": "photomaker",
"PIXVERSE_TEMPLATE": "قالب Pixverse",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "رنگ Recraft",
"RECRAFT_CONTROLS": "کنترل‌های Recraft",
"RECRAFT_V3_STYLE": "سبک Recraft V3",
@@ -959,7 +958,6 @@
"imageUrl": "آدرس تصویر",
"import": "وارد کردن",
"inProgress": "در حال انجام",
"inSubgraph": "در زیرگراف «{name}»",
"increment": "افزایش",
"info": "اطلاعات node",
"input": "ورودی",
@@ -1117,7 +1115,6 @@
"updated": "به‌روزرسانی شد",
"updating": "در حال به‌روزرسانی {id}",
"upload": "بارگذاری",
"uploadAlreadyInProgress": "بارگذاری در حال انجام است",
"usageHint": "راهنمای استفاده",
"use": "استفاده",
"user": "کاربر",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "دانلود همه",
"dragAndDropImage": "تصویر را بکشید و رها کنید",
"giveFeedback": "ارسال بازخورد",
"graphMode": "حالت گراف",
"linearMode": "حالت برنامه",
"queue": {
"clear": "پاک‌سازی صف",
"clickToClear": "برای پاک‌سازی صف کلیک کنید"
},
"rerun": "اجرای مجدد",
"reuseParameters": "استفاده مجدد از پارامترها",
"runCount": "تعداد اجرا: ",
@@ -1583,9 +1575,6 @@
"nodePack": "بسته نود",
"nodePackInfo": "اطلاعات Node Pack",
"notAvailable": "در دسترس نیست",
"packInstall": {
"nodeIdRequired": "شناسه node برای نصب الزامی است"
},
"packsSelected": "بسته انتخاب شد",
"repository": "مخزن",
"restartToApplyChanges": "برای اعمال تغییرات، لطفاً ComfyUI را مجدداً راه‌اندازی کنید",
@@ -2083,10 +2072,7 @@
"openNodeManager": "باز کردن Node Manager",
"quickFixAvailable": "رفع سریع در دسترس است",
"redHighlight": "قرمز",
"replaceAll": "جایگزینی همه",
"replaceAllWarning": "همه گره‌های موجود در این گروه جایگزین خواهند شد.",
"replaceFailed": "جایگزینی نودها ناموفق بود",
"replaceNode": "جایگزینی گره",
"replaceSelected": "جایگزینی انتخاب‌شده‌ها ({count})",
"replaceWarning": "این کار workflow را به طور دائمی تغییر می‌دهد. اگر مطمئن نیستید، ابتدا یک نسخه ذخیره کنید.",
"replaceable": "قابل جایگزینی",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "{count} نوع نود جایگزین شد",
"replacedNode": "نود جایگزین شد: {nodeType}",
"selectAll": "انتخاب همه",
"skipForNow": "فعلاً رد شود",
"swapNodesGuide": "گره‌های زیر می‌توانند به‌صورت خودکار با گزینه‌های سازگار جایگزین شوند.",
"swapNodesTitle": "جایگزینی گره‌ها",
"unknownNode": "ناشناخته",
"willBeReplacedBy": "این گره جایگزین خواهد شد با:"
"skipForNow": "فعلاً رد شود"
},
"nodeTemplates": {
"enterName": "نام را وارد کنید",
@@ -2117,19 +2099,6 @@
},
"title": "دستگاه شما پشتیبانی نمی‌شود"
},
"painter": {
"background": "پس‌زمینه",
"brush": "براش",
"clear": "پاک‌سازی",
"color": "انتخاب رنگ",
"eraser": "پاک‌کن",
"hardness": "سختی",
"height": "ارتفاع",
"size": "اندازه نشانگر",
"tool": "ابزار",
"uploadError": "بارگذاری تصویر painter ناموفق بود: {status} - {statusText}",
"width": "عرض"
},
"progressToast": {
"allDownloadsCompleted": "همه دانلودها تکمیل شدند",
"downloadingModel": "در حال دانلود مدل...",
@@ -2250,22 +2219,6 @@
"inputsNone": "بدون ورودی",
"inputsNoneTooltip": "این نود ورودی ندارد",
"locateNode": "یافتن node در canvas",
"missingNodePacks": {
"applyChanges": "اعمال تغییرات",
"cloudMessage": "این workflow به nodeهای سفارشی نیاز دارد که هنوز در Comfy Cloud موجود نیستند.",
"collapse": "جمع کردن",
"expand": "باز کردن",
"installAll": "نصب همه",
"installNodePack": "نصب پک node",
"installed": "نصب شد",
"installing": "در حال نصب...",
"ossMessage": "این workflow از nodeهای سفارشی استفاده می‌کند که هنوز نصب نکرده‌اید.",
"searchInManager": "جستجو در Node Manager",
"title": "پک‌های node مفقود",
"unknownPack": "پک ناشناخته",
"unsupportedTitle": "پک‌های node پشتیبانی‌نشده",
"viewInManager": "مشاهده در Manager"
},
"mute": "بی‌صدا",
"noErrors": "بدون خطا",
"noSelection": "یک نود را انتخاب کنید تا ویژگی‌ها و اطلاعات آن نمایش داده شود.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "برش و تغییر اندازه نواحی از دسته تصویر ورودی بر اساس جعبه‌های مرزی ارائه‌شده.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "جعبه‌های مرزی"
},
"image": {
"name": "تصویر"
},
"output_height": {
"name": "ارتفاع خروجی",
"tooltip": "ارتفاعی که هر برش به آن تغییر اندازه داده می‌شود."
},
"output_width": {
"name": "عرض خروجی",
"tooltip": "عرضی که هر برش به آن تغییر اندازه داده می‌شود."
},
"padding": {
"name": "حاشیه",
"tooltip": "حاشیه اضافی (بر حسب پیکسل) که به هر طرف جعبه مرزی قبل از برش اضافه می‌شود."
}
},
"outputs": {
"0": {
"tooltip": "تمام برش‌ها به صورت یک دسته تصویر واحد جمع‌آوری شده‌اند."
}
}
},
"CropMask": {
"display_name": "برش Mask",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "تولید یا ویرایش تصاویر به صورت همزمان از طریق Google Vertex API.",
"display_name": "Nano Banana ۲",
"inputs": {
"aspect_ratio": {
"name": "نسبت تصویر",
"tooltip": "اگر روی 'auto' تنظیم شود، با نسبت تصویر ورودی شما مطابقت دارد؛ اگر تصویری ارائه نشود، معمولاً یک مربع با نسبت ۱۶:۹ تولید می‌شود."
},
"control_after_generate": {
"name": "کنترل پس از تولید"
},
"files": {
"name": "فایل‌ها",
"tooltip": "فایل(های) اختیاری برای استفاده به عنوان زمینه برای مدل. ورودی‌ها را از node Gemini Generate Content Input Files می‌پذیرد."
},
"images": {
"name": "تصاویر",
"tooltip": "تصویر(های) مرجع اختیاری. برای افزودن چند تصویر، از node Batch Images استفاده کنید (تا ۱۴ تصویر)."
},
"model": {
"name": "مدل"
},
"prompt": {
"name": "پرامپت",
"tooltip": "توضیح متنی درباره تصویری که باید تولید شود یا ویرایش‌هایی که باید اعمال گردد. هرگونه محدودیت، سبک یا جزئیاتی که مدل باید رعایت کند را وارد کنید."
},
"resolution": {
"name": "وضوح تصویر",
"tooltip": "وضوح خروجی هدف. برای ۲K/۴K از upscaler بومی Gemini استفاده می‌شود."
},
"response_modalities": {
"name": "حالت‌های پاسخ"
},
"seed": {
"name": "seed",
"tooltip": "زمانی که مقدار seed ثابت باشد، مدل تلاش می‌کند تا پاسخ مشابهی برای درخواست‌های تکراری ارائه دهد. خروجی قطعی تضمین نمی‌شود. همچنین، تغییر مدل یا تنظیمات پارامترها مانند دما (temperature) می‌تواند باعث تغییر در پاسخ حتی با همان مقدار seed شود. به طور پیش‌فرض، مقدار seed به صورت تصادفی انتخاب می‌شود."
},
"system_prompt": {
"name": "پرامپت سیستمی",
"tooltip": "دستورالعمل‌های پایه‌ای که رفتار هوش مصنوعی را تعیین می‌کند."
},
"thinking_level": {
"name": "سطح تفکر"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "تولید پاسخ متنی با مدل هوش مصنوعی Gemini گوگل. می‌توانید انواع مختلفی از ورودی‌ها (متن، تصویر، صوت، ویدئو) را به عنوان زمینه برای تولید پاسخ‌های مرتبط‌تر و معنادارتر ارائه دهید.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "ترسیم بدن"
},
"draw_face": {
"name": "ترسیم صورت"
},
"draw_feet": {
"name": "ترسیم پاها"
},
"draw_hands": {
"name": "ترسیم دست‌ها"
},
"face_point_size": {
"name": "اندازه نقطه صورت"
},
"keypoints": {
"name": "نقاط کلیدی"
},
"score_threshold": {
"name": "آستانه امتیاز"
},
"stick_width": {
"name": "ضخامت خط"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "اجبار به مربع بودن",
"tooltip": "محور کوتاه‌تر جعبه مرزی را گسترش می‌دهد تا ناحیه برش همیشه مربع باشد."
},
"keypoints": {
"name": "نقاط کلیدی"
},
"scale": {
"name": "مقیاس",
"tooltip": "ضریب بزرگنمایی برای ناحیه جعبه مرزی اطراف هر صورت شناسایی‌شده."
}
},
"outputs": {
"0": {
"name": "جعبه‌های مرزی",
"tooltip": "جعبه‌های مرزی صورت برای هر فریم، سازگار با ورودی bboxes در SDPoseKeypointExtractor."
}
}
},
"SDPoseKeypointExtractor": {
"description": "استخراج نقاط کلیدی ژست از تصاویر با استفاده از مدل SDPose: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "اندازه دسته"
},
"bboxes": {
"name": "جعبه‌های مرزی",
"tooltip": "جعبه‌های مرزی اختیاری برای تشخیص دقیق‌تر. برای تشخیص چند نفره الزامی است."
},
"image": {
"name": "تصویر"
},
"model": {
"name": "مدل"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "نقاط کلیدی",
"tooltip": "نقاط کلیدی در قالب قاب OpenPose (عرض بوم، ارتفاع بوم، افراد)"
}
}
},
"SDTurboScheduler": {
"display_name": "زمان‌بندی SDTurbo",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "FICHIERS_ENTRÉE_OPENAI",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "Modèle Pixverse",
"POSE_KEYPOINT": "POINT CLÉ DE POSE",
"RECRAFT_COLOR": "Couleur Recraft",
"RECRAFT_CONTROLS": "Contrôles Recraft",
"RECRAFT_V3_STYLE": "Style Recraft V3",
@@ -959,7 +958,6 @@
"imageUrl": "URL de l'image",
"import": "Importer",
"inProgress": "En cours",
"inSubgraph": "dans le sous-graphe « {name} »",
"increment": "Augmenter",
"info": "Informations du nœud",
"input": "Entrée",
@@ -1117,7 +1115,6 @@
"updated": "Mis à jour",
"updating": "Mise à jour",
"upload": "Téléverser",
"uploadAlreadyInProgress": "Téléversement déjà en cours",
"usageHint": "Conseil d'utilisation",
"use": "Utiliser",
"user": "Utilisateur",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "Tout télécharger",
"dragAndDropImage": "Glissez-déposez une image",
"giveFeedback": "Donner un avis",
"graphMode": "Mode graphique",
"linearMode": "Mode App",
"queue": {
"clear": "Vider la file d'attente",
"clickToClear": "Cliquez pour vider la file d'attente"
},
"rerun": "Relancer",
"reuseParameters": "Réutiliser les paramètres",
"runCount": "Nombre dexécutions :",
@@ -1583,9 +1575,6 @@
"nodePack": "Pack de Nœuds",
"nodePackInfo": "Informations sur le pack de nœuds",
"notAvailable": "Non disponible",
"packInstall": {
"nodeIdRequired": "L'identifiant du nœud est requis pour l'installation"
},
"packsSelected": "Packs sélectionnés",
"repository": "Référentiel",
"restartToApplyChanges": "Pour appliquer les modifications, veuillez redémarrer ComfyUI",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Ouvrir le Gestionnaire de nœuds",
"quickFixAvailable": "Correction rapide disponible",
"redHighlight": "rouge",
"replaceAll": "Tout remplacer",
"replaceAllWarning": "Remplace tous les nœuds disponibles dans ce groupe.",
"replaceFailed": "Échec du remplacement des nœuds",
"replaceNode": "Remplacer le nœud",
"replaceSelected": "Remplacer la sélection ({count})",
"replaceWarning": "Cela modifiera définitivement le workflow. Sauvegardez une copie si vous nêtes pas sûr.",
"replaceable": "Remplaçable",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "{count} type(s) de nœud remplacé(s)",
"replacedNode": "Nœud remplacé : {nodeType}",
"selectAll": "Tout sélectionner",
"skipForNow": "Ignorer pour linstant",
"swapNodesGuide": "Les nœuds suivants peuvent être automatiquement remplacés par des alternatives compatibles.",
"swapNodesTitle": "Échanger les nœuds",
"unknownNode": "Inconnu",
"willBeReplacedBy": "Ce nœud sera remplacé par :"
"skipForNow": "Ignorer pour linstant"
},
"nodeTemplates": {
"enterName": "Entrez le nom",
@@ -2117,19 +2099,6 @@
},
"title": "Votre appareil n'est pas pris en charge"
},
"painter": {
"background": "Arrière-plan",
"brush": "Pinceau",
"clear": "Effacer",
"color": "Sélecteur de couleur",
"eraser": "Gomme",
"hardness": "Dureté",
"height": "Hauteur",
"size": "Taille du curseur",
"tool": "Outil",
"uploadError": "Échec du téléversement de l'image du peintre : {status} - {statusText}",
"width": "Largeur"
},
"progressToast": {
"allDownloadsCompleted": "Tous les téléchargements sont terminés",
"downloadingModel": "Téléchargement du modèle...",
@@ -2250,22 +2219,6 @@
"inputsNone": "AUCUNE ENTRÉE",
"inputsNoneTooltip": "Le nœud na pas dentrées",
"locateNode": "Localiser le nœud sur le canevas",
"missingNodePacks": {
"applyChanges": "Appliquer les modifications",
"cloudMessage": "Ce workflow nécessite des nœuds personnalisés qui ne sont pas encore disponibles sur Comfy Cloud.",
"collapse": "Réduire",
"expand": "Développer",
"installAll": "Tout installer",
"installNodePack": "Installer le pack de nœuds",
"installed": "Installé",
"installing": "Installation en cours...",
"ossMessage": "Ce workflow utilise des nœuds personnalisés que vous n'avez pas encore installés.",
"searchInManager": "Rechercher dans le gestionnaire de nœuds",
"title": "Packs de nœuds manquants",
"unknownPack": "Pack inconnu",
"unsupportedTitle": "Packs de nœuds non pris en charge",
"viewInManager": "Voir dans le gestionnaire"
},
"mute": "Muet",
"noErrors": "Aucune erreur",
"noSelection": "Sélectionnez un nœud pour voir ses propriétés et informations.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "Rogner et redimensionner des régions du lot dimages dentrée selon les boîtes englobantes fournies.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "bboxes"
},
"image": {
"name": "image"
},
"output_height": {
"name": "output_height",
"tooltip": "Hauteur à laquelle chaque découpe est redimensionnée."
},
"output_width": {
"name": "output_width",
"tooltip": "Largeur à laquelle chaque découpe est redimensionnée."
},
"padding": {
"name": "padding",
"tooltip": "Marge supplémentaire en pixels ajoutée de chaque côté de la boîte avant le rognage."
}
},
"outputs": {
"0": {
"tooltip": "Toutes les découpes empilées dans un seul lot dimages."
}
}
},
"CropMask": {
"display_name": "CropMask",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Générez ou modifiez des images de manière synchrone via lAPI Google Vertex.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "rapport daspect",
"tooltip": "Si défini sur « auto », correspond au rapport daspect de votre image dentrée ; si aucune image nest fournie, un carré 16:9 est généralement généré."
},
"control_after_generate": {
"name": "contrôle après génération"
},
"files": {
"name": "fichiers",
"tooltip": "Fichier(s) optionnel(s) à utiliser comme contexte pour le modèle. Accepte les entrées du nœud Gemini Generate Content Input Files."
},
"images": {
"name": "images",
"tooltip": "Image(s) de référence optionnelle(s). Pour inclure plusieurs images, utilisez le nœud Batch Images (jusquà 14)."
},
"model": {
"name": "modèle"
},
"prompt": {
"name": "prompt",
"tooltip": "Invite textuelle décrivant limage à générer ou les modifications à appliquer. Incluez toutes contraintes, styles ou détails que le modèle doit respecter."
},
"resolution": {
"name": "résolution",
"tooltip": "Résolution de sortie cible. Pour le 2K/4K, lupscaler natif Gemini est utilisé."
},
"response_modalities": {
"name": "modalités de réponse"
},
"seed": {
"name": "graine",
"tooltip": "Lorsque la graine est fixée à une valeur spécifique, le modèle sefforce de fournir la même réponse pour des requêtes répétées. Un résultat déterministe nest pas garanti. De plus, changer le modèle ou les paramètres, comme la température, peut entraîner des variations de la réponse même avec la même valeur de graine. Par défaut, une graine aléatoire est utilisée."
},
"system_prompt": {
"name": "invite système",
"tooltip": "Instructions fondamentales qui dictent le comportement de lIA."
},
"thinking_level": {
"name": "niveau de réflexion"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "Génère des réponses textuelles avec le modèle d'IA Gemini de Google. Vous pouvez fournir plusieurs types d'entrées (texte, images, audio, vidéo) comme contexte pour générer des réponses plus pertinentes et significatives.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "draw_body"
},
"draw_face": {
"name": "draw_face"
},
"draw_feet": {
"name": "draw_feet"
},
"draw_hands": {
"name": "draw_hands"
},
"face_point_size": {
"name": "face_point_size"
},
"keypoints": {
"name": "keypoints"
},
"score_threshold": {
"name": "score_threshold"
},
"stick_width": {
"name": "stick_width"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "force_square",
"tooltip": "Agrandir laxe le plus court de la boîte pour que la région découpée soit toujours carrée."
},
"keypoints": {
"name": "keypoints"
},
"scale": {
"name": "scale",
"tooltip": "Multiplicateur pour la zone de la boîte englobante autour de chaque visage détecté."
}
},
"outputs": {
"0": {
"name": "bboxes",
"tooltip": "Boîtes englobantes des visages par image, compatibles avec lentrée bboxes de SDPoseKeypointExtractor."
}
}
},
"SDPoseKeypointExtractor": {
"description": "Extraire les points clés de pose à partir dimages en utilisant le modèle SDPose : https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "batch_size"
},
"bboxes": {
"name": "bboxes",
"tooltip": "Boîtes englobantes optionnelles pour des détections plus précises. Requis pour la détection multi-personnes."
},
"image": {
"name": "image"
},
"model": {
"name": "model"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "keypoints",
"tooltip": "Points clés au format OpenPose (canvas_width, canvas_height, people)"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurboScheduler",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "OpenAI入力ファイル",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "Pixverseテンプレート",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "Recraftカラー",
"RECRAFT_CONTROLS": "Recraftコントロール",
"RECRAFT_V3_STYLE": "Recraft V3スタイル",
@@ -959,7 +958,6 @@
"imageUrl": "画像URL",
"import": "インポート",
"inProgress": "進行中",
"inSubgraph": "サブグラフ「{name}」内",
"increment": "増加",
"info": "ノード情報",
"input": "入力",
@@ -1117,7 +1115,6 @@
"updated": "更新済み",
"updating": "更新中",
"upload": "アップロード",
"uploadAlreadyInProgress": "アップロードはすでに進行中です",
"usageHint": "使用ヒント",
"use": "使用",
"user": "ユーザー",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "すべてダウンロード",
"dragAndDropImage": "画像をドラッグ&ドロップ",
"giveFeedback": "フィードバックを送る",
"graphMode": "グラフモード",
"linearMode": "アプリモード",
"queue": {
"clear": "キューをクリア",
"clickToClear": "クリックしてキューをクリア"
},
"rerun": "再実行",
"reuseParameters": "パラメータを再利用",
"runCount": "実行回数:",
@@ -1583,9 +1575,6 @@
"nodePack": "ノードパック",
"nodePackInfo": "ノードパック情報",
"notAvailable": "利用不可",
"packInstall": {
"nodeIdRequired": "インストールにはードIDが必要です"
},
"packsSelected": "選択したパック",
"repository": "リポジトリ",
"restartToApplyChanges": "変更を適用するには、ComfyUIを再起動してください",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Node Managerを開く",
"quickFixAvailable": "クイック修正可能",
"redHighlight": "赤",
"replaceAll": "すべて置き換える",
"replaceAllWarning": "このグループ内の利用可能なすべてのノードを置き換えます。",
"replaceFailed": "ノードの置き換えに失敗しました",
"replaceNode": "ノードを置き換える",
"replaceSelected": "選択したものを置き換え ({count})",
"replaceWarning": "この操作はワークフローを永久に変更します。心配な場合は、先にコピーを保存してください。",
"replaceable": "置き換え可能",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "{count} 種類のノードを置き換えました",
"replacedNode": "置き換えたノード: {nodeType}",
"selectAll": "すべて選択",
"skipForNow": "今はスキップ",
"swapNodesGuide": "以下のノードは互換性のある代替品に自動的に置き換えることができます。",
"swapNodesTitle": "ノードの入れ替え",
"unknownNode": "不明",
"willBeReplacedBy": "このノードは次で置き換えられます:"
"skipForNow": "今はスキップ"
},
"nodeTemplates": {
"enterName": "名前を入力",
@@ -2117,19 +2099,6 @@
},
"title": "お使いのデバイスはサポートされていません"
},
"painter": {
"background": "背景",
"brush": "ブラシ",
"clear": "クリア",
"color": "カラーピッカー",
"eraser": "消しゴム",
"hardness": "硬さ",
"height": "高さ",
"size": "カーソルサイズ",
"tool": "ツール",
"uploadError": "ペインター画像のアップロードに失敗しました: {status} - {statusText}",
"width": "幅"
},
"progressToast": {
"allDownloadsCompleted": "すべてのダウンロードが完了しました",
"downloadingModel": "モデルをダウンロード中...",
@@ -2250,22 +2219,6 @@
"inputsNone": "入力なし",
"inputsNoneTooltip": "このノードには入力がありません",
"locateNode": "キャンバス上でノードを探す",
"missingNodePacks": {
"applyChanges": "変更を適用",
"cloudMessage": "このワークフローにはComfy Cloudでまだ利用できないカスタムードが必要です。",
"collapse": "折りたたむ",
"expand": "展開",
"installAll": "すべてインストール",
"installNodePack": "ノードパックをインストール",
"installed": "インストール済み",
"installing": "インストール中...",
"ossMessage": "このワークフローにはまだインストールしていないカスタムノードが使われています。",
"searchInManager": "ノードマネージャーで検索",
"title": "不足しているノードパック",
"unknownPack": "不明なパック",
"unsupportedTitle": "サポートされていないノードパック",
"viewInManager": "マネージャーで表示"
},
"mute": "ミュート",
"noErrors": "エラーなし",
"noSelection": "ノードを選択すると、そのプロパティと情報が表示されます。",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "指定されたバウンディングボックスに基づいて、入力画像バッチから領域を切り抜きリサイズします。",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "バウンディングボックス"
},
"image": {
"name": "画像"
},
"output_height": {
"name": "出力高さ",
"tooltip": "各切り抜き画像がリサイズされる高さ。"
},
"output_width": {
"name": "出力幅",
"tooltip": "各切り抜き画像がリサイズされる幅。"
},
"padding": {
"name": "パディング",
"tooltip": "切り抜き前にバウンディングボックスの各辺に追加される余白(ピクセル単位)。"
}
},
"outputs": {
"0": {
"tooltip": "すべての切り抜き画像が1つの画像バッチとしてまとめられます。"
}
}
},
"CropMask": {
"display_name": "マスクをトリミング",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Google Vertex API を使って画像を同期的に生成または編集します。",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "アスペクト比",
"tooltip": "「auto」に設定すると入力画像のアスペクト比に合わせます。画像が指定されていない場合は通常16:9の正方形が生成されます。"
},
"control_after_generate": {
"name": "生成後のコントロール"
},
"files": {
"name": "ファイル",
"tooltip": "モデルのコンテキストとして使用する任意のファイル。Gemini Generate Content Input Filesードからの入力を受け付けます。"
},
"images": {
"name": "画像",
"tooltip": "任意の参照画像。複数画像を含める場合はBatch Imagesードを使用してください最大14枚まで。"
},
"model": {
"name": "モデル"
},
"prompt": {
"name": "プロンプト",
"tooltip": "生成する画像や適用する編集内容を説明するテキストプロンプトです。モデルが従うべき制約、スタイル、詳細なども含めてください。"
},
"resolution": {
"name": "解像度",
"tooltip": "出力画像の目標解像度です。2K/4Kの場合はGeminiのネイティブアップスケーラーが使用されます。"
},
"response_modalities": {
"name": "レスポンスモダリティ"
},
"seed": {
"name": "シード",
"tooltip": "シード値を特定の値に固定すると、モデルは繰り返しリクエストに対して同じ応答を返すよう最善を尽くしますが、完全な決定論的出力は保証されません。また、モデルやパラメータ設定temperatureを変更すると、同じシード値でも応答が変化する場合があります。デフォルトではランダムなシード値が使用されます。"
},
"system_prompt": {
"name": "システムプロンプト",
"tooltip": "AIの挙動を決定する基本的な指示です。"
},
"thinking_level": {
"name": "思考レベル"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "GoogleのGemini AIモデルでテキスト応答を生成します。テキスト、画像、音声、動画など複数の種類の入力をコンテキストとして提供し、より関連性の高い意味のある応答を生成できます。",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "ボディを描画"
},
"draw_face": {
"name": "顔を描画"
},
"draw_feet": {
"name": "足を描画"
},
"draw_hands": {
"name": "手を描画"
},
"face_point_size": {
"name": "顔ポイントサイズ"
},
"keypoints": {
"name": "キーポイント"
},
"score_threshold": {
"name": "スコア閾値"
},
"stick_width": {
"name": "スティック幅"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "正方形に強制",
"tooltip": "短い辺を拡張して、切り抜き領域が常に正方形になるようにします。"
},
"keypoints": {
"name": "キーポイント"
},
"scale": {
"name": "スケール",
"tooltip": "各検出された顔のバウンディングボックス領域の倍率。"
}
},
"outputs": {
"0": {
"name": "バウンディングボックス",
"tooltip": "各フレームごとの顔バウンディングボックス。SDPoseKeypointExtractorのbboxes入力と互換性があります。"
}
}
},
"SDPoseKeypointExtractor": {
"description": "SDPoseモデルhttps://huggingface.co/Comfy-Org/SDPose/tree/main/checkpointsを使用して画像からポーズキーポイントを抽出します。",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "バッチサイズ"
},
"bboxes": {
"name": "バウンディングボックス",
"tooltip": "より正確な検出のためのオプションのバウンディングボックス。複数人検出時は必須です。"
},
"image": {
"name": "画像"
},
"model": {
"name": "モデル"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "キーポイント",
"tooltip": "OpenPoseフレーム形式canvas_width, canvas_height, peopleのキーポイント"
}
}
},
"SDTurboScheduler": {
"display_name": "SDターボスケジューラー",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "OpenAI 입력 파일",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "Pixverse 템플릿",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "Recraft 색상",
"RECRAFT_CONTROLS": "Recraft 컨트롤",
"RECRAFT_V3_STYLE": "Recraft V3 스타일",
@@ -959,7 +958,6 @@
"imageUrl": "이미지 URL",
"import": "가져오기",
"inProgress": "진행 중",
"inSubgraph": "서브그래프 '{name}'에 있음",
"increment": "증가",
"info": "노드 정보",
"input": "입력",
@@ -1117,7 +1115,6 @@
"updated": "업데이트 됨",
"updating": "업데이트 중",
"upload": "업로드",
"uploadAlreadyInProgress": "업로드가 이미 진행 중입니다",
"usageHint": "사용 힌트",
"use": "사용",
"user": "사용자",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "모두 다운로드",
"dragAndDropImage": "이미지를 드래그 앤 드롭하세요",
"giveFeedback": "피드백 보내기",
"graphMode": "그래프 모드",
"linearMode": "앱 모드",
"queue": {
"clear": "대기열 비우기",
"clickToClear": "클릭하여 대기열 비우기"
},
"rerun": "다시 실행",
"reuseParameters": "파라미터 재사용",
"runCount": "실행 횟수:",
@@ -1583,9 +1575,6 @@
"nodePack": "노드 팩",
"nodePackInfo": "노드 팩 정보",
"notAvailable": "사용 불가",
"packInstall": {
"nodeIdRequired": "설치를 위해 노드 ID가 필요합니다"
},
"packsSelected": "선택한 노드 팩",
"repository": "저장소",
"restartToApplyChanges": "변경 사항을 적용하려면 ComfyUI를 재시작해 주세요",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Node Manager 열기",
"quickFixAvailable": "빠른 수정 가능",
"redHighlight": "빨간색",
"replaceAll": "모두 교체",
"replaceAllWarning": "이 그룹의 사용 가능한 모든 노드를 교체합니다.",
"replaceFailed": "노드 교체 실패",
"replaceNode": "노드 교체",
"replaceSelected": "선택한 항목 교체 ({count})",
"replaceWarning": "이 작업은 워크플로를 영구적으로 수정합니다. 확실하지 않으면 먼저 복사본을 저장하세요.",
"replaceable": "교체 가능",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "{count}개 노드 유형 교체됨",
"replacedNode": "교체된 노드: {nodeType}",
"selectAll": "전체 선택",
"skipForNow": "일단 건너뛰기",
"swapNodesGuide": "다음 노드는 호환 가능한 대체 노드로 자동 교체할 수 있습니다.",
"swapNodesTitle": "노드 교체",
"unknownNode": "알 수 없음",
"willBeReplacedBy": "이 노드는 다음으로 교체됩니다:"
"skipForNow": "일단 건너뛰기"
},
"nodeTemplates": {
"enterName": "이름 입력",
@@ -2117,19 +2099,6 @@
},
"title": "이 장치는 지원되지 않습니다."
},
"painter": {
"background": "배경",
"brush": "브러시",
"clear": "지우기",
"color": "색상 선택기",
"eraser": "지우개",
"hardness": "경도",
"height": "높이",
"size": "커서 크기",
"tool": "도구",
"uploadError": "페인터 이미지를 업로드하지 못했습니다: {status} - {statusText}",
"width": "너비"
},
"progressToast": {
"allDownloadsCompleted": "모든 다운로드가 완료되었습니다",
"downloadingModel": "모델 다운로드 중...",
@@ -2250,22 +2219,6 @@
"inputsNone": "입력 없음",
"inputsNoneTooltip": "노드에 입력이 없습니다",
"locateNode": "캔버스에서 노드 찾기",
"missingNodePacks": {
"applyChanges": "변경 사항 적용",
"cloudMessage": "이 워크플로우에는 Comfy Cloud에서 아직 사용할 수 없는 커스텀 노드가 필요합니다.",
"collapse": "접기",
"expand": "펼치기",
"installAll": "모두 설치",
"installNodePack": "노드 팩 설치",
"installed": "설치됨",
"installing": "설치 중...",
"ossMessage": "이 워크플로우에는 아직 설치하지 않은 커스텀 노드를 사용합니다.",
"searchInManager": "노드 매니저에서 검색",
"title": "누락된 노드 팩",
"unknownPack": "알 수 없는 팩",
"unsupportedTitle": "지원되지 않는 노드 팩",
"viewInManager": "매니저에서 보기"
},
"mute": "음소거",
"noErrors": "오류 없음",
"noSelection": "노드를 선택하면 속성과 정보를 볼 수 있습니다.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "제공된 바운딩 박스를 기반으로 입력 이미지 배치에서 영역을 자르고 크기를 조정합니다.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "bboxes"
},
"image": {
"name": "image"
},
"output_height": {
"name": "output_height",
"tooltip": "각 잘라낸 이미지의 높이로 크기가 조정됩니다."
},
"output_width": {
"name": "output_width",
"tooltip": "각 잘라낸 이미지의 너비로 크기가 조정됩니다."
},
"padding": {
"name": "padding",
"tooltip": "자르기 전에 바운딩 박스의 각 면에 추가되는 픽셀 단위의 여백입니다."
}
},
"outputs": {
"0": {
"tooltip": "모든 잘라낸 이미지가 하나의 이미지 배치로 쌓입니다."
}
}
},
"CropMask": {
"display_name": "마스크 자르기",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Google Vertex API를 통해 이미지를 동기적으로 생성하거나 편집합니다.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "종횡비",
"tooltip": "'auto'로 설정하면 입력 이미지의 종횡비와 일치합니다. 이미지가 제공되지 않으면 일반적으로 16:9 정사각형이 생성됩니다."
},
"control_after_generate": {
"name": "생성 후 제어"
},
"files": {
"name": "파일",
"tooltip": "모델의 컨텍스트로 사용할 선택적 파일(들)입니다. Gemini Generate Content Input Files 노드에서 입력을 받을 수 있습니다."
},
"images": {
"name": "이미지",
"tooltip": "선택적 참조 이미지(들)입니다. 여러 이미지를 포함하려면 Batch Images 노드를 사용하세요(최대 14개)."
},
"model": {
"name": "모델"
},
"prompt": {
"name": "프롬프트",
"tooltip": "생성할 이미지 또는 적용할 편집을 설명하는 텍스트 프롬프트입니다. 모델이 따라야 할 제약 조건, 스타일 또는 세부 정보를 포함하세요."
},
"resolution": {
"name": "해상도",
"tooltip": "목표 출력 해상도입니다. 2K/4K의 경우 Gemini 기본 업스케일러가 사용됩니다."
},
"response_modalities": {
"name": "응답 모달리티"
},
"seed": {
"name": "시드",
"tooltip": "시드를 특정 값으로 고정하면, 모델은 반복 요청에 대해 동일한 응답을 제공하려고 최선을 다합니다. 결정적 결과는 보장되지 않습니다. 또한, 모델이나 파라미터 설정(예: temperature)을 변경하면 동일한 시드 값을 사용해도 응답이 달라질 수 있습니다. 기본적으로 무작위 시드 값이 사용됩니다."
},
"system_prompt": {
"name": "시스템 프롬프트",
"tooltip": "AI의 동작을 지시하는 기본 지침입니다."
},
"thinking_level": {
"name": "사고 수준"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "Google의 Gemini AI 모델로 텍스트 응답을 생성합니다. 더 관련성 있고 의미 있는 응답을 생성하기 위해 컨텍스트로 여러 유형의 입력(텍스트, 이미지, 오디오, 비디오)을 제공할 수 있습니다.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "draw_body"
},
"draw_face": {
"name": "draw_face"
},
"draw_feet": {
"name": "draw_feet"
},
"draw_hands": {
"name": "draw_hands"
},
"face_point_size": {
"name": "face_point_size"
},
"keypoints": {
"name": "keypoints"
},
"score_threshold": {
"name": "score_threshold"
},
"stick_width": {
"name": "stick_width"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "force_square",
"tooltip": "자르기 영역이 항상 정사각형이 되도록 더 짧은 바운딩 박스 축을 확장합니다."
},
"keypoints": {
"name": "keypoints"
},
"scale": {
"name": "scale",
"tooltip": "각 감지된 얼굴 주위의 바운딩 박스 영역에 곱해지는 배수입니다."
}
},
"outputs": {
"0": {
"name": "bboxes",
"tooltip": "프레임별 얼굴 바운딩 박스이며, SDPoseKeypointExtractor의 bboxes 입력과 호환됩니다."
}
}
},
"SDPoseKeypointExtractor": {
"description": "SDPose 모델을 사용하여 이미지에서 포즈 키포인트를 추출합니다: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "batch_size"
},
"bboxes": {
"name": "bboxes",
"tooltip": "더 정확한 감지를 위한 선택적 바운딩 박스입니다. 다중 인물 감지 시 필수입니다."
},
"image": {
"name": "image"
},
"model": {
"name": "model"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "keypoints",
"tooltip": "OpenPose 프레임 형식의 키포인트 (canvas_width, canvas_height, people)"
}
}
},
"SDTurboScheduler": {
"display_name": "SD-Turbo 스케줄러",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "ARQUIVOS DE ENTRADA OPENAI",
"PHOTOMAKER": "photomaker",
"PIXVERSE_TEMPLATE": "MODELO PIXVERSE",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "COR RECRAFT",
"RECRAFT_CONTROLS": "CONTROLES RECRAFT",
"RECRAFT_V3_STYLE": "ESTILO RECRAFT V3",
@@ -959,7 +958,6 @@
"imageUrl": "URL da imagem",
"import": "Importar",
"inProgress": "Em andamento",
"inSubgraph": "no subgrafo '{name}'",
"increment": "Aumentar",
"info": "Informações do nó",
"input": "Entrada",
@@ -1117,7 +1115,6 @@
"updated": "Atualizado",
"updating": "Atualizando {id}",
"upload": "Enviar",
"uploadAlreadyInProgress": "O upload já está em andamento",
"usageHint": "Dica de uso",
"use": "Usar",
"user": "Usuário",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "Baixar tudo",
"dragAndDropImage": "Arraste e solte uma imagem",
"giveFeedback": "Enviar feedback",
"graphMode": "Modo Gráfico",
"linearMode": "Modo App",
"queue": {
"clear": "Limpar fila",
"clickToClear": "Clique para limpar a fila"
},
"rerun": "Executar novamente",
"reuseParameters": "Reutilizar parâmetros",
"runCount": "Número de execuções:",
@@ -1583,9 +1575,6 @@
"nodePack": "Node Pack",
"nodePackInfo": "Informações do Pacote de Nós",
"notAvailable": "Não Disponível",
"packInstall": {
"nodeIdRequired": "ID do nó é obrigatório para instalação"
},
"packsSelected": "pacotes selecionados",
"repository": "Repositório",
"restartToApplyChanges": "Para aplicar as alterações, reinicie o ComfyUI",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Abrir Gerenciador de Nós",
"quickFixAvailable": "Correção Rápida Disponível",
"redHighlight": "vermelho",
"replaceAll": "Substituir Todos",
"replaceAllWarning": "Substitui todos os nós disponíveis neste grupo.",
"replaceFailed": "Falha ao substituir nós",
"replaceNode": "Substituir Nó",
"replaceSelected": "Substituir Selecionados ({count})",
"replaceWarning": "Isso modificará permanentemente o fluxo de trabalho. Salve uma cópia antes se não tiver certeza.",
"replaceable": "Substituível",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "Substituídos {count} tipo(s) de nó",
"replacedNode": "Nó substituído: {nodeType}",
"selectAll": "Selecionar Tudo",
"skipForNow": "Pular por enquanto",
"swapNodesGuide": "Os seguintes nós podem ser automaticamente substituídos por alternativas compatíveis.",
"swapNodesTitle": "Trocar Nós",
"unknownNode": "Desconhecido",
"willBeReplacedBy": "Este nó será substituído por:"
"skipForNow": "Pular por enquanto"
},
"nodeTemplates": {
"enterName": "Digite o nome",
@@ -2117,19 +2099,6 @@
},
"title": "Seu dispositivo não é suportado"
},
"painter": {
"background": "Fundo",
"brush": "Pincel",
"clear": "Limpar",
"color": "Seletor de cor",
"eraser": "Borracha",
"hardness": "Dureza",
"height": "Altura",
"size": "Tamanho do cursor",
"tool": "Ferramenta",
"uploadError": "Falha ao enviar imagem do painter: {status} - {statusText}",
"width": "Largura"
},
"progressToast": {
"allDownloadsCompleted": "Todos os downloads concluídos",
"downloadingModel": "Baixando modelo...",
@@ -2250,22 +2219,6 @@
"inputsNone": "SEM ENTRADAS",
"inputsNoneTooltip": "O nó não possui entradas",
"locateNode": "Localizar nó no canvas",
"missingNodePacks": {
"applyChanges": "Aplicar alterações",
"cloudMessage": "Este fluxo de trabalho requer nós personalizados que ainda não estão disponíveis no Comfy Cloud.",
"collapse": "Recolher",
"expand": "Expandir",
"installAll": "Instalar todos",
"installNodePack": "Instalar pacote de nós",
"installed": "Instalado",
"installing": "Instalando...",
"ossMessage": "Este fluxo de trabalho usa nós personalizados que você ainda não instalou.",
"searchInManager": "Buscar no Gerenciador de Nós",
"title": "Pacotes de nós ausentes",
"unknownPack": "Pacote desconhecido",
"unsupportedTitle": "Pacotes de nós não suportados",
"viewInManager": "Ver no Gerenciador"
},
"mute": "Silenciar",
"noErrors": "Sem erros",
"noSelection": "Selecione um nó para ver suas propriedades e informações.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "Recorta e redimensiona regiões do lote de imagens de entrada com base nas caixas delimitadoras fornecidas.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "caixas delimitadoras"
},
"image": {
"name": "imagem"
},
"output_height": {
"name": "altura_de_saida",
"tooltip": "Altura para a qual cada recorte será redimensionado."
},
"output_width": {
"name": "largura_de_saida",
"tooltip": "Largura para a qual cada recorte será redimensionado."
},
"padding": {
"name": "preenchimento",
"tooltip": "Preenchimento extra em pixels adicionado em cada lado da caixa antes do recorte."
}
},
"outputs": {
"0": {
"tooltip": "Todos os recortes empilhados em um único lote de imagens."
}
}
},
"CropMask": {
"display_name": "Cortar Máscara",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Gere ou edite imagens de forma síncrona via Google Vertex API.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "Se definido como 'auto', corresponde à proporção da imagem de entrada; se nenhuma imagem for fornecida, normalmente é gerado um quadrado 16:9."
},
"control_after_generate": {
"name": "control after generate"
},
"files": {
"name": "files",
"tooltip": "Arquivo(s) opcional(is) para usar como contexto para o modelo. Aceita entradas do nó Gemini Generate Content Input Files."
},
"images": {
"name": "images",
"tooltip": "Imagem(ns) de referência opcional(is). Para incluir várias imagens, use o nó Batch Images (até 14)."
},
"model": {
"name": "model"
},
"prompt": {
"name": "prompt",
"tooltip": "Prompt de texto descrevendo a imagem a ser gerada ou as edições a serem aplicadas. Inclua quaisquer restrições, estilos ou detalhes que o modelo deve seguir."
},
"resolution": {
"name": "resolution",
"tooltip": "Resolução de saída desejada. Para 2K/4K, o upscaler nativo do Gemini é utilizado."
},
"response_modalities": {
"name": "response_modalities"
},
"seed": {
"name": "seed",
"tooltip": "Quando a seed é fixada em um valor específico, o modelo faz o melhor esforço para fornecer a mesma resposta em solicitações repetidas. A saída determinística não é garantida. Além disso, alterar o modelo ou configurações de parâmetros, como a temperatura, pode causar variações na resposta mesmo usando o mesmo valor de seed. Por padrão, um valor de seed aleatório é usado."
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "Instruções fundamentais que ditam o comportamento da IA."
},
"thinking_level": {
"name": "thinking_level"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "Gere respostas em texto com o modelo Gemini AI do Google. Você pode fornecer múltiplos tipos de entrada (texto, imagens, áudio, vídeo) como contexto para gerar respostas mais relevantes e significativas.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "desenhar_corpo"
},
"draw_face": {
"name": "desenhar_rosto"
},
"draw_feet": {
"name": "desenhar_pés"
},
"draw_hands": {
"name": "desenhar_mãos"
},
"face_point_size": {
"name": "tamanho_do_ponto_do_rosto"
},
"keypoints": {
"name": "pontos-chave"
},
"score_threshold": {
"name": "limite_de_pontuação"
},
"stick_width": {
"name": "largura_da_linha"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "forçar_quadrado",
"tooltip": "Expande o eixo menor da caixa para que a região recortada seja sempre quadrada."
},
"keypoints": {
"name": "pontos-chave"
},
"scale": {
"name": "escala",
"tooltip": "Multiplicador para a área da caixa delimitadora ao redor de cada rosto detectado."
}
},
"outputs": {
"0": {
"name": "caixas_delimitadoras",
"tooltip": "Caixas delimitadoras de rosto por quadro, compatíveis com a entrada de caixas do SDPoseKeypointExtractor."
}
}
},
"SDPoseKeypointExtractor": {
"description": "Extrai pontos-chave de pose das imagens usando o modelo SDPose: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "tamanho_do_lote"
},
"bboxes": {
"name": "caixas_delimitadoras",
"tooltip": "Caixas delimitadoras opcionais para detecções mais precisas. Necessário para detecção de múltiplas pessoas."
},
"image": {
"name": "imagem"
},
"model": {
"name": "modelo"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "pontos-chave",
"tooltip": "Pontos-chave no formato de quadro OpenPose (canvas_width, canvas_height, pessoas)"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurboScheduler",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "ВХОДНЫЕАЙЛЫ_OPENAI",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": АБЛОН_PIXVERSE",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "RECRAFT_ЦВЕТ",
"RECRAFT_CONTROLS": "RECRAFT_УПРАВЛЕНИЯ",
"RECRAFT_V3_STYLE": "RECRAFT_V3_СТИЛЬ",
@@ -959,7 +958,6 @@
"imageUrl": "URL изображения",
"import": "Импорт",
"inProgress": "В процессе",
"inSubgraph": "в подграфе «{name}»",
"increment": "Увеличить",
"info": "Информация о ноде",
"input": "Вход",
@@ -1117,7 +1115,6 @@
"updated": "Обновлено",
"updating": "Обновление",
"upload": "Загрузить",
"uploadAlreadyInProgress": "Загрузка уже выполняется",
"usageHint": "Подсказка по использованию",
"use": "Использовать",
"user": "Пользователь",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "Скачать всё",
"dragAndDropImage": "Перетащите изображение",
"giveFeedback": "Оставить отзыв",
"graphMode": "Графовый режим",
"linearMode": "Режим приложения",
"queue": {
"clear": "Очистить очередь",
"clickToClear": "Нажмите, чтобы очистить очередь"
},
"rerun": "Перезапустить",
"reuseParameters": "Повторно использовать параметры",
"runCount": "Количество запусков:",
@@ -1583,9 +1575,6 @@
"nodePack": "Пакет Узлов",
"nodePackInfo": "Информация о пакете узлов",
"notAvailable": "Недоступно",
"packInstall": {
"nodeIdRequired": "Для установки требуется идентификатор узла"
},
"packsSelected": "Выбрано пакетов",
"repository": "Репозиторий",
"restartToApplyChanges": "Чтобы применить изменения, пожалуйста, перезапустите ComfyUI",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Открыть Менеджер узлов",
"quickFixAvailable": "Доступно быстрое исправление",
"redHighlight": "красным",
"replaceAll": "Заменить все",
"replaceAllWarning": "Заменяет все доступные узлы в этой группе.",
"replaceFailed": "Не удалось заменить узлы",
"replaceNode": "Заменить узел",
"replaceSelected": "Заменить выбранные ({count})",
"replaceWarning": "Это действие навсегда изменит рабочий процесс. Сохраните копию, если не уверены.",
"replaceable": "Можно заменить",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "Заменено {count} типов(а) узлов",
"replacedNode": "Заменённый узел: {nodeType}",
"selectAll": "Выбрать все",
"skipForNow": "Пропустить сейчас",
"swapNodesGuide": "Следующие узлы могут быть автоматически заменены совместимыми альтернативами.",
"swapNodesTitle": "Заменить узлы",
"unknownNode": "Неизвестно",
"willBeReplacedBy": "Этот узел будет заменён на:"
"skipForNow": "Пропустить сейчас"
},
"nodeTemplates": {
"enterName": "Введите название",
@@ -2117,19 +2099,6 @@
},
"title": "Ваше устройство не поддерживается"
},
"painter": {
"background": "Фон",
"brush": "Кисть",
"clear": "Очистить",
"color": "Палитра цветов",
"eraser": "Ластик",
"hardness": "Жёсткость",
"height": "Высота",
"size": "Размер курсора",
"tool": "Инструмент",
"uploadError": "Не удалось загрузить изображение: {status} - {statusText}",
"width": "Ширина"
},
"progressToast": {
"allDownloadsCompleted": "Все загрузки завершены",
"downloadingModel": "Загрузка модели...",
@@ -2250,22 +2219,6 @@
"inputsNone": "НЕТ ВХОДОВ",
"inputsNoneTooltip": "Узел не имеет входов",
"locateNode": "Найти узел на холсте",
"missingNodePacks": {
"applyChanges": "Применить изменения",
"cloudMessage": "Для этого рабочего процесса требуются пользовательские узлы, которые ещё недоступны в Comfy Cloud.",
"collapse": "Свернуть",
"expand": "Развернуть",
"installAll": "Установить все",
"installNodePack": "Установить пакет узлов",
"installed": "Установлено",
"installing": "Установка...",
"ossMessage": "В этом рабочем процессе используются пользовательские узлы, которые вы ещё не установили.",
"searchInManager": "Поиск в менеджере узлов",
"title": "Отсутствующие пакеты узлов",
"unknownPack": "Неизвестный пакет",
"unsupportedTitle": "Неподдерживаемые пакеты узлов",
"viewInManager": "Просмотреть в менеджере"
},
"mute": "Отключить",
"noErrors": "Ошибок нет",
"noSelection": "Выберите узел, чтобы увидеть его свойства и информацию.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "Обрезать и изменить размер областей из входного пакета изображений на основе предоставленных ограничивающих рамок.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "bboxes"
},
"image": {
"name": "image"
},
"output_height": {
"name": "output_height",
"tooltip": "Высота, до которой изменяется каждый обрезанный фрагмент."
},
"output_width": {
"name": "output_width",
"tooltip": "Ширина, до которой изменяется каждый обрезанный фрагмент."
},
"padding": {
"name": "padding",
"tooltip": "Дополнительный отступ в пикселях, добавляемый с каждой стороны ограничивающей рамки перед обрезкой."
}
},
"outputs": {
"0": {
"tooltip": "Все обрезанные фрагменты объединены в один пакет изображений."
}
}
},
"CropMask": {
"display_name": "Обрезать маску",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Генерируйте или редактируйте изображения синхронно через Google Vertex API.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "Если установлено значение 'auto', используется соотношение сторон вашего входного изображения; если изображение не предоставлено, обычно генерируется квадрат 16:9."
},
"control_after_generate": {
"name": "control after generate"
},
"files": {
"name": "files",
"tooltip": "Необязательные файлы для использования в качестве контекста для модели. Принимает входные данные из узла Gemini Generate Content Input Files."
},
"images": {
"name": "images",
"tooltip": "Необязательное изображение(я) для ссылки. Чтобы добавить несколько изображений, используйте узел Batch Images (до 14)."
},
"model": {
"name": "model"
},
"prompt": {
"name": "prompt",
"tooltip": "Текстовый запрос, описывающий изображение для генерации или редактирования. Укажите любые ограничения, стили или детали, которым должна следовать модель."
},
"resolution": {
"name": "resolution",
"tooltip": "Целевое разрешение вывода. Для 2K/4K используется собственный Gemini upscaler."
},
"response_modalities": {
"name": "response_modalities"
},
"seed": {
"name": "seed",
"tooltip": "Когда seed фиксирован на определённом значении, модель старается выдавать одинаковый результат при повторных запросах. Детерминированный результат не гарантируется. Также изменение модели или параметров, например, температуры, может привести к различиям в ответе даже при одинаковом seed. По умолчанию используется случайное значение seed."
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "Базовые инструкции, определяющие поведение ИИ."
},
"thinking_level": {
"name": "thinking_level"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "Генерируйте текстовые ответы с помощью модели ИИ Google Gemini. Вы можете предоставить несколько типов входных данных (текст, изображения, аудио, видео) в качестве контекста для генерации более релевантных и осмысленных ответов.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "draw_body"
},
"draw_face": {
"name": "draw_face"
},
"draw_feet": {
"name": "draw_feet"
},
"draw_hands": {
"name": "draw_hands"
},
"face_point_size": {
"name": "face_point_size"
},
"keypoints": {
"name": "keypoints"
},
"score_threshold": {
"name": "score_threshold"
},
"stick_width": {
"name": "stick_width"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "force_square",
"tooltip": "Увеличить меньшую сторону рамки, чтобы область обрезки всегда была квадратной."
},
"keypoints": {
"name": "keypoints"
},
"scale": {
"name": "scale",
"tooltip": "Множитель для области ограничивающей рамки вокруг каждого обнаруженного лица."
}
},
"outputs": {
"0": {
"name": "bboxes",
"tooltip": "Ограничивающие рамки лиц для каждого кадра, совместимы с входом bboxes SDPoseKeypointExtractor."
}
}
},
"SDPoseKeypointExtractor": {
"description": "Извлекает ключевые точки позы из изображений с помощью модели SDPose: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "batch_size"
},
"bboxes": {
"name": "bboxes",
"tooltip": "Необязательные ограничивающие рамки для более точного обнаружения. Требуется для обнаружения нескольких людей."
},
"image": {
"name": "image"
},
"model": {
"name": "model"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "keypoints",
"tooltip": "Ключевые точки в формате OpenPose (canvas_width, canvas_height, people)"
}
}
},
"SDTurboScheduler": {
"display_name": "Scheduler SDTurbo",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "OPENAI_GİRİŞ_DOSYALARI",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "PIXVERSE_ŞABLONU",
"POSE_KEYPOINT": "POZ ANAHTAR NOKTASI",
"RECRAFT_COLOR": "RECRAFT_RENK",
"RECRAFT_CONTROLS": "RECRAFT_KONTROLLERİ",
"RECRAFT_V3_STYLE": "RECRAFT_V3_STİLİ",
@@ -959,7 +958,6 @@
"imageUrl": "Görsel URL'si",
"import": "İçe Aktar",
"inProgress": "Devam ediyor",
"inSubgraph": "'{name}' alt grafında",
"increment": "Arttır",
"info": "Düğüm Bilgisi",
"input": "Girdi",
@@ -1117,7 +1115,6 @@
"updated": "Güncellendi",
"updating": "{id} güncelleniyor",
"upload": "Yükle",
"uploadAlreadyInProgress": "Yükleme zaten devam ediyor",
"usageHint": "Kullanım ipucu",
"use": "Kullan",
"user": "Kullanıcı",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "Tümünü İndir",
"dragAndDropImage": "Bir görseli sürükleyip bırakın",
"giveFeedback": "Geri bildirim ver",
"graphMode": "Grafik Modu",
"linearMode": "Uygulama Modu",
"queue": {
"clear": "Kuyruğu temizle",
"clickToClear": "Kuyruğu temizlemek için tıklayın"
},
"rerun": "Tekrar Çalıştır",
"reuseParameters": "Parametreleri Yeniden Kullan",
"runCount": "Çalıştırma sayısı:",
@@ -1583,9 +1575,6 @@
"nodePack": "Düğüm Paketi",
"nodePackInfo": "Düğüm Paketi Bilgisi",
"notAvailable": "Mevcut Değil",
"packInstall": {
"nodeIdRequired": "Kurulum için Düğüm Kimliği gereklidir"
},
"packsSelected": "paket seçildi",
"repository": "Depo",
"restartToApplyChanges": "Değişiklikleri uygulamak için lütfen ComfyUI'yi yeniden başlatın",
@@ -2083,10 +2072,7 @@
"openNodeManager": "Node Manager'ı Aç",
"quickFixAvailable": "Hızlı Düzeltme Mevcut",
"redHighlight": "kırmızı",
"replaceAll": "Hepsini Değiştir",
"replaceAllWarning": "Bu gruptaki mevcut tüm düğümleri değiştirir.",
"replaceFailed": "Düğümler değiştirilemedi",
"replaceNode": "Düğümü Değiştir",
"replaceSelected": "Seçilenleri Değiştir ({count})",
"replaceWarning": "Bu işlem iş akışını kalıcı olarak değiştirecek. Emin değilseniz önce bir kopyasını kaydedin.",
"replaceable": "Değiştirilebilir",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "{count} düğüm türü değiştirildi",
"replacedNode": "Değiştirilen düğüm: {nodeType}",
"selectAll": "Tümünü Seç",
"skipForNow": "Şimdilik Atla",
"swapNodesGuide": "Aşağıdaki düğümler, uyumlu alternatiflerle otomatik olarak değiştirilebilir.",
"swapNodesTitle": "Düğümleri Değiştir",
"unknownNode": "Bilinmeyen",
"willBeReplacedBy": "Bu düğüm şununla değiştirilecek:"
"skipForNow": "Şimdilik Atla"
},
"nodeTemplates": {
"enterName": "İsim girin",
@@ -2117,19 +2099,6 @@
},
"title": "Cihazınız desteklenmiyor"
},
"painter": {
"background": "Arka Plan",
"brush": "Fırça",
"clear": "Temizle",
"color": "Renk Seçici",
"eraser": "Silgi",
"hardness": "Sertlik",
"height": "Yükseklik",
"size": "İmleç Boyutu",
"tool": "Araç",
"uploadError": "Ressam görseli yüklenemedi: {status} - {statusText}",
"width": "Genişlik"
},
"progressToast": {
"allDownloadsCompleted": "Tüm indirmeler tamamlandı",
"downloadingModel": "Model indiriliyor...",
@@ -2250,22 +2219,6 @@
"inputsNone": "GİRİŞ YOK",
"inputsNoneTooltip": "Düğümün girişi yok",
"locateNode": "Düğümü tuvalde bul",
"missingNodePacks": {
"applyChanges": "Değişiklikleri Uygula",
"cloudMessage": "Bu iş akışı, Comfy Cloud'da henüz mevcut olmayan özel düğümler gerektiriyor.",
"collapse": "Daralt",
"expand": "Genişlet",
"installAll": "Tümünü Yükle",
"installNodePack": "Düğüm paketini yükle",
"installed": "Yüklendi",
"installing": "Yükleniyor...",
"ossMessage": "Bu iş akışı, henüz yüklemediğiniz özel düğümler kullanıyor.",
"searchInManager": "Düğüm Yöneticisinde Ara",
"title": "Eksik Düğüm Paketleri",
"unknownPack": "Bilinmeyen paket",
"unsupportedTitle": "Desteklenmeyen Düğüm Paketleri",
"viewInManager": "Yöneticide Görüntüle"
},
"mute": "Sessiz",
"noErrors": "Hata yok",
"noSelection": "Bir düğüm seçerek özelliklerini ve bilgisini görebilirsiniz.",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "Sağlanan sınırlayıcı kutulara göre giriş görüntü grubundan bölgeleri kırp ve yeniden boyutlandır.",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "sınırlayıcı_kutular"
},
"image": {
"name": "görüntü"
},
"output_height": {
"name": ıktı_yüksekliği",
"tooltip": "Her kırpmanın yeniden boyutlandırılacağı yükseklik."
},
"output_width": {
"name": ıktı_genişliği",
"tooltip": "Her kırpmanın yeniden boyutlandırılacağı genişlik."
},
"padding": {
"name": "dolgu",
"tooltip": "Kırpmadan önce sınırlayıcı kutunun her bir kenarına piksel cinsinden eklenen ekstra dolgu."
}
},
"outputs": {
"0": {
"tooltip": "Tüm kırpmalar tek bir görüntü grubunda birleştirilir."
}
}
},
"CropMask": {
"display_name": "Maskeyi Kırp",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "Google Vertex API üzerinden senkron olarak görseller oluşturun veya düzenleyin.",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "'auto' olarak ayarlanırsa, giriş görselinizin en-boy oranı kullanılır; görsel yoksa genellikle 16:9 kare oluşturulur."
},
"control_after_generate": {
"name": "control after generate"
},
"files": {
"name": "files",
"tooltip": "Model için bağlam olarak kullanılacak isteğe bağlı dosya(lar). Gemini Generate Content Input Files düğümünden gelen girdileri kabul eder."
},
"images": {
"name": "images",
"tooltip": "İsteğe bağlı referans görsel(ler)i. Birden fazla görsel eklemek için Batch Images düğümünü kullanın (en fazla 14)."
},
"model": {
"name": "model"
},
"prompt": {
"name": "prompt",
"tooltip": "Oluşturulacak görseli veya uygulanacak düzenlemeleri tanımlayan metin istemi. Modelin uyması gereken kısıtlamalar, stiller veya detayları ekleyin."
},
"resolution": {
"name": "resolution",
"tooltip": "Hedef çıktı çözünürlüğü. 2K/4K için yerel Gemini yükselticisi kullanılır."
},
"response_modalities": {
"name": "response_modalities"
},
"seed": {
"name": "seed",
"tooltip": "Seed belirli bir değere sabitlendiğinde, model tekrarlanan isteklerde aynı yanıtı vermeye çalışır. Deterministik çıktı garanti edilmez. Ayrıca, model veya parametre ayarlarını (ör. sıcaklık) değiştirmek, aynı seed değeriyle bile yanıtın farklı olmasına neden olabilir. Varsayılan olarak rastgele bir seed değeri kullanılır."
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "Bir yapay zekanın davranışını belirleyen temel talimatlar."
},
"thinking_level": {
"name": "thinking_level"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "Google'ın Gemini AI modeli ile metin yanıtları oluşturun. Daha alakalı ve anlamlı yanıtlar oluşturmak için bağlam olarak birden fazla girdi türü (metin, görseller, ses, video) sağlayabilirsiniz.",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "gövde_çiz"
},
"draw_face": {
"name": "yüzü_çiz"
},
"draw_feet": {
"name": "ayakları_çiz"
},
"draw_hands": {
"name": "elleri_çiz"
},
"face_point_size": {
"name": "yüz_nokta_boyutu"
},
"keypoints": {
"name": "anahtar_noktalar"
},
"score_threshold": {
"name": "puan_eşiği"
},
"stick_width": {
"name": "çizgi_genişliği"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "kare_zorla",
"tooltip": "Kırpma bölgesinin her zaman kare olması için kısa olan kutu eksenini genişlet."
},
"keypoints": {
"name": "anahtar_noktalar"
},
"scale": {
"name": "ölçek",
"tooltip": "Her tespit edilen yüzün etrafındaki sınırlayıcı kutu alanı için çarpan."
}
},
"outputs": {
"0": {
"name": "sınırlayıcı_kutular",
"tooltip": "Her kare için yüz sınırlayıcı kutuları, SDPoseKeypointExtractor bboxes girişiyle uyumlu."
}
}
},
"SDPoseKeypointExtractor": {
"description": "SDPose modeli ile görüntülerden poz anahtar noktalarını çıkar: https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "toplu_boyut"
},
"bboxes": {
"name": "sınırlayıcı_kutular",
"tooltip": "Daha doğru tespitler için isteğe bağlı sınırlayıcı kutular. Çoklu kişi tespiti için gereklidir."
},
"image": {
"name": "görüntü"
},
"model": {
"name": "model"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "anahtar_noktalar",
"tooltip": "OpenPose çerçeve formatında anahtar noktalar (canvas_genişliği, canvas_yüksekliği, kişiler)"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurboZamanlayıcı",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "OpenAI輸入檔案",
"PHOTOMAKER": "PhotoMaker",
"PIXVERSE_TEMPLATE": "PIXVERSE 範本",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "RECRAFT 顏色",
"RECRAFT_CONTROLS": "RECRAFT 控制",
"RECRAFT_V3_STYLE": "RECRAFT V3 風格",
@@ -959,7 +958,6 @@
"imageUrl": "圖片網址",
"import": "匯入",
"inProgress": "進行中",
"inSubgraph": "於子圖「{name}」中",
"increment": "增加",
"info": "節點資訊",
"input": "輸入",
@@ -1117,7 +1115,6 @@
"updated": "已更新",
"updating": "更新中",
"upload": "上傳",
"uploadAlreadyInProgress": "上傳已在進行中",
"usageHint": "使用提示",
"use": "使用",
"user": "使用者",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "全部下載",
"dragAndDropImage": "拖曳圖片到此",
"giveFeedback": "提供回饋",
"graphMode": "圖形模式",
"linearMode": "App 模式",
"queue": {
"clear": "清除佇列",
"clickToClear": "點擊以清除佇列"
},
"rerun": "重新執行",
"reuseParameters": "重用參數",
"runCount": "執行次數:",
@@ -1583,9 +1575,6 @@
"nodePack": "節點包",
"nodePackInfo": "節點包資訊",
"notAvailable": "不可用",
"packInstall": {
"nodeIdRequired": "安裝需要節點 ID"
},
"packsSelected": "已選擇套件",
"repository": "儲存庫",
"restartToApplyChanges": "請重新啟動 ComfyUI 以套用變更",
@@ -2083,10 +2072,7 @@
"openNodeManager": "開啟節點管理器",
"quickFixAvailable": "可用快速修復",
"redHighlight": "紅色",
"replaceAll": "全部替換",
"replaceAllWarning": "將替換此群組中所有可用的節點。",
"replaceFailed": "替換節點失敗",
"replaceNode": "替換節點",
"replaceSelected": "替換所選 ({count})",
"replaceWarning": "這將永久修改工作流程。如有疑慮,請先儲存副本。",
"replaceable": "可替換",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "已替換 {count} 種節點類型",
"replacedNode": "已替換節點:{nodeType}",
"selectAll": "全選",
"skipForNow": "暫時略過",
"swapNodesGuide": "以下節點可以自動替換為相容的替代品。",
"swapNodesTitle": "交換節點",
"unknownNode": "未知",
"willBeReplacedBy": "此節點將被替換為:"
"skipForNow": "暫時略過"
},
"nodeTemplates": {
"enterName": "輸入名稱",
@@ -2117,19 +2099,6 @@
},
"title": "您的裝置不受支援"
},
"painter": {
"background": "背景",
"brush": "畫筆",
"clear": "清除",
"color": "顏色選擇器",
"eraser": "橡皮擦",
"hardness": "硬度",
"height": "高度",
"size": "游標大小",
"tool": "工具",
"uploadError": "上傳繪圖圖像失敗:{status} - {statusText}",
"width": "寬度"
},
"progressToast": {
"allDownloadsCompleted": "所有下載已完成",
"downloadingModel": "正在下載模型...",
@@ -2250,22 +2219,6 @@
"inputsNone": "無輸入",
"inputsNoneTooltip": "此節點沒有輸入",
"locateNode": "在畫布上定位節點",
"missingNodePacks": {
"applyChanges": "套用變更",
"cloudMessage": "此工作流程需要 Comfy Cloud 尚未提供的自訂節點。",
"collapse": "收合",
"expand": "展開",
"installAll": "全部安裝",
"installNodePack": "安裝節點包",
"installed": "已安裝",
"installing": "安裝中⋯⋯",
"ossMessage": "此工作流程使用了你尚未安裝的自訂節點。",
"searchInManager": "在節點管理器中搜尋",
"title": "缺少節點包",
"unknownPack": "未知的節點包",
"unsupportedTitle": "不支援的節點包",
"viewInManager": "在管理器中檢視"
},
"mute": "靜音",
"noErrors": "沒有錯誤",
"noSelection": "請選擇節點以檢視其屬性與資訊。",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "根據提供的邊界框,從輸入影像批次中裁切並調整區域大小。",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "bboxes"
},
"image": {
"name": "image"
},
"output_height": {
"name": "output_height",
"tooltip": "每個裁切區域調整後的高度。"
},
"output_width": {
"name": "output_width",
"tooltip": "每個裁切區域調整後的寬度。"
},
"padding": {
"name": "padding",
"tooltip": "在裁切前,於邊界框每側額外增加的像素邊距。"
}
},
"outputs": {
"0": {
"tooltip": "所有裁切區域堆疊為單一影像批次。"
}
}
},
"CropMask": {
"display_name": "裁剪遮罩",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "透過 Google Vertex API 同步產生或編輯影像。",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "aspect_ratio",
"tooltip": "若設為「auto」則會配合輸入影像的長寬比若未提供影像通常會產生 16:9 的正方形。"
},
"control_after_generate": {
"name": "control after generate"
},
"files": {
"name": "files",
"tooltip": "可選的檔案,作為模型的參考內容。可接受來自 Gemini Generate Content Input Files 節點的輸入。"
},
"images": {
"name": "images",
"tooltip": "可選的參考影像。若要加入多張影像,請使用 Batch Images 節點(最多 14 張)。"
},
"model": {
"name": "model"
},
"prompt": {
"name": "prompt",
"tooltip": "描述要產生影像或要套用編輯的文字提示。請包含模型應遵循的任何限制、風格或細節。"
},
"resolution": {
"name": "resolution",
"tooltip": "目標輸出解析度。若選擇 2K/4K將使用原生 Gemini 放大器。"
},
"response_modalities": {
"name": "response_modalities"
},
"seed": {
"name": "seed",
"tooltip": "當 seed 設定為特定值時,模型會盡力在重複請求時提供相同的回應,但不保證完全一致。更改模型或參數(如 temperature即使使用相同 seed 也可能導致回應不同。預設會使用隨機 seed 值。"
},
"system_prompt": {
"name": "system_prompt",
"tooltip": "決定 AI 行為的基礎指令。"
},
"thinking_level": {
"name": "thinking_level"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "使用 Google 的 Gemini AI 模型生成文字回應。您可以提供多種類型的輸入(文字、圖片、音訊、影片)作為上下文,以生成更相關且有意義的回應。",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "draw_body"
},
"draw_face": {
"name": "draw_face"
},
"draw_feet": {
"name": "draw_feet"
},
"draw_hands": {
"name": "draw_hands"
},
"face_point_size": {
"name": "face_point_size"
},
"keypoints": {
"name": "keypoints"
},
"score_threshold": {
"name": "score_threshold"
},
"stick_width": {
"name": "stick_width"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "force_square",
"tooltip": "將較短的邊界框軸向擴展,使裁切區域始終為正方形。"
},
"keypoints": {
"name": "keypoints"
},
"scale": {
"name": "scale",
"tooltip": "每個偵測到的臉部邊界框區域的倍率。"
}
},
"outputs": {
"0": {
"name": "bboxes",
"tooltip": "每幀的臉部邊界框,與 SDPoseKeypointExtractor 的 bboxes 輸入相容。"
}
}
},
"SDPoseKeypointExtractor": {
"description": "使用 SDPose 模型從影像中提取姿勢關鍵點https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "batch_size"
},
"bboxes": {
"name": "bboxes",
"tooltip": "可選的邊界框以提升偵測準確度。多人偵測時必須提供。"
},
"image": {
"name": "image"
},
"model": {
"name": "model"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "keypoints",
"tooltip": "以 OpenPose 幀格式canvas_width, canvas_height, people表示的關鍵點"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurboScheduler",
"inputs": {

View File

@@ -699,7 +699,6 @@
"OPENAI_INPUT_FILES": "OpenAI输入文件",
"PHOTOMAKER": "PhotoMaker",
"PIXVERSE_TEMPLATE": "Pixverse 模板",
"POSE_KEYPOINT": "POSE_KEYPOINT",
"RECRAFT_COLOR": "Recraft 颜色",
"RECRAFT_CONTROLS": "Recraft 控件",
"RECRAFT_V3_STYLE": "Recraft V3 风格",
@@ -959,7 +958,6 @@
"imageUrl": "图片网址",
"import": "导入",
"inProgress": "进行中",
"inSubgraph": "在子图 '{name}' 中",
"increment": "增加",
"info": "节点信息",
"input": "输入",
@@ -1117,7 +1115,6 @@
"updated": "已更新",
"updating": "更新中",
"upload": "上传",
"uploadAlreadyInProgress": "上传已在进行中",
"usageHint": "使用提示",
"use": "使用",
"user": "用户",
@@ -1357,13 +1354,8 @@
},
"downloadAll": "全部下载",
"dragAndDropImage": "拖拽图片到此处",
"giveFeedback": "提供反馈",
"graphMode": "图形模式",
"linearMode": "App 模式",
"queue": {
"clear": "清空队列",
"clickToClear": "点击清空队列"
},
"rerun": "重新运行",
"reuseParameters": "复用参数",
"runCount": "运行次数:",
@@ -1583,9 +1575,6 @@
"nodePack": "节点包",
"nodePackInfo": "节点包信息",
"notAvailable": "不可用",
"packInstall": {
"nodeIdRequired": "安装需要节点 ID"
},
"packsSelected": "选定的包",
"repository": "仓库",
"restartToApplyChanges": "要应用更改请重新启动ComfyUI",
@@ -2083,10 +2072,7 @@
"openNodeManager": "打开节点管理器",
"quickFixAvailable": "可用快速修复",
"redHighlight": "红色",
"replaceAll": "全部替换",
"replaceAllWarning": "将替换此组中所有可用节点。",
"replaceFailed": "替换节点失败",
"replaceNode": "替换节点",
"replaceSelected": "替换已选({count}",
"replaceWarning": "此操作将永久修改工作流。如不确定,请先保存副本。",
"replaceable": "可替换",
@@ -2094,11 +2080,7 @@
"replacedAllNodes": "已替换 {count} 种节点类型",
"replacedNode": "已替换节点:{nodeType}",
"selectAll": "全选",
"skipForNow": "暂时跳过",
"swapNodesGuide": "以下节点可以自动替换为兼容的替代项。",
"swapNodesTitle": "交换节点",
"unknownNode": "未知",
"willBeReplacedBy": "此节点将被替换为:"
"skipForNow": "暂时跳过"
},
"nodeTemplates": {
"enterName": "输入名称",
@@ -2117,19 +2099,6 @@
},
"title": "您的设备不受支持"
},
"painter": {
"background": "背景",
"brush": "画笔",
"clear": "清除",
"color": "取色器",
"eraser": "橡皮擦",
"hardness": "硬度",
"height": "高度",
"size": "光标大小",
"tool": "工具",
"uploadError": "上传绘图图像失败:{status} - {statusText}",
"width": "宽度"
},
"progressToast": {
"allDownloadsCompleted": "所有下载已完成",
"downloadingModel": "正在下载模型...",
@@ -2250,22 +2219,6 @@
"inputsNone": "无输入",
"inputsNoneTooltip": "节点没有输入",
"locateNode": "在画布上定位节点",
"missingNodePacks": {
"applyChanges": "应用更改",
"cloudMessage": "此工作流需要 Comfy Cloud 上尚未提供的自定义节点。",
"collapse": "收起",
"expand": "展开",
"installAll": "全部安装",
"installNodePack": "安装节点包",
"installed": "已安装",
"installing": "正在安装...",
"ossMessage": "此工作流使用了你尚未安装的自定义节点。",
"searchInManager": "在节点管理器中搜索",
"title": "缺失节点包",
"unknownPack": "未知包",
"unsupportedTitle": "不支持的节点包",
"viewInManager": "在管理器中查看"
},
"mute": "禁用",
"noErrors": "无错误",
"noSelection": "选择一个节点查看其属性信息。",

View File

@@ -2137,35 +2137,6 @@
}
}
},
"CropByBBoxes": {
"description": "根据提供的边界框,从输入图像批量中裁剪并调整区域大小。",
"display_name": "CropByBBoxes",
"inputs": {
"bboxes": {
"name": "bboxes"
},
"image": {
"name": "image"
},
"output_height": {
"name": "output_height",
"tooltip": "每个裁剪区域调整后的高度。"
},
"output_width": {
"name": "output_width",
"tooltip": "每个裁剪区域调整后的宽度。"
},
"padding": {
"name": "padding",
"tooltip": "在裁剪前,每个边界框四周额外添加的像素填充。"
}
},
"outputs": {
"0": {
"tooltip": "所有裁剪区域堆叠为一个图像批量。"
}
}
},
"CropMask": {
"display_name": "裁剪遮罩",
"inputs": {
@@ -3730,57 +3701,6 @@
}
}
},
"GeminiNanoBanana2": {
"description": "通过 Google Vertex API 同步生成或编辑图像。",
"display_name": "Nano Banana 2",
"inputs": {
"aspect_ratio": {
"name": "宽高比",
"tooltip": "如果设置为“自动”,则匹配输入图像的宽高比;如果未提供图像,通常会生成 16:9 的正方形。"
},
"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": "响应模式"
},
"seed": {
"name": "种子",
"tooltip": "当种子值固定时,模型会尽力为重复请求提供相同的响应,但不能保证输出完全一致。此外,更改模型或参数设置(如温度)即使使用相同的种子值,也可能导致响应不同。默认情况下使用随机种子值。"
},
"system_prompt": {
"name": "系统提示词",
"tooltip": "决定 AI 行为的基础性指令。"
},
"thinking_level": {
"name": "思考层级"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"GeminiNode": {
"description": "使用 Google 的 Gemini AI 模型生成文本响应。您可以提供多种类型的输入(文本、图像、音频、视频)作为上下文,以生成更相关和有意义的响应。",
"display_name": "Google Gemini",
@@ -13058,90 +12978,6 @@
}
}
},
"SDPoseDrawKeypoints": {
"display_name": "SDPoseDrawKeypoints",
"inputs": {
"draw_body": {
"name": "draw_body"
},
"draw_face": {
"name": "draw_face"
},
"draw_feet": {
"name": "draw_feet"
},
"draw_hands": {
"name": "draw_hands"
},
"face_point_size": {
"name": "face_point_size"
},
"keypoints": {
"name": "keypoints"
},
"score_threshold": {
"name": "score_threshold"
},
"stick_width": {
"name": "stick_width"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"SDPoseFaceBBoxes": {
"display_name": "SDPoseFaceBBoxes",
"inputs": {
"force_square": {
"name": "force_square",
"tooltip": "扩展较短的边界框轴,使裁剪区域始终为正方形。"
},
"keypoints": {
"name": "keypoints"
},
"scale": {
"name": "scale",
"tooltip": "每个检测到的人脸边界框区域的缩放倍数。"
}
},
"outputs": {
"0": {
"name": "bboxes",
"tooltip": "每帧的人脸边界框可与SDPoseKeypointExtractor的bboxes输入兼容。"
}
}
},
"SDPoseKeypointExtractor": {
"description": "使用SDPose模型从图像中提取姿态关键点https://huggingface.co/Comfy-Org/SDPose/tree/main/checkpoints",
"display_name": "SDPoseKeypointExtractor",
"inputs": {
"batch_size": {
"name": "batch_size"
},
"bboxes": {
"name": "bboxes",
"tooltip": "可选的边界框,用于更精确的检测。多人检测时必需。"
},
"image": {
"name": "image"
},
"model": {
"name": "model"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"name": "keypoints",
"tooltip": "OpenPose帧格式的关键点canvas_width, canvas_height, people"
}
}
},
"SDTurboScheduler": {
"display_name": "SDTurbo调度器",
"inputs": {

View File

@@ -282,6 +282,7 @@ const getCheckoutTier = (
const getCheckoutAttributionForCloud =
async (): Promise<CheckoutAttributionMetadata> => {
// eslint-disable-next-line no-undef
if (__DISTRIBUTION__ !== 'cloud') {
return {}
}

View File

@@ -40,14 +40,11 @@ const buttonLabel = computed(() =>
: t('subscription.subscribeToRun')
)
const { showSubscriptionDialog, subscription } = useBillingContext()
const { showSubscriptionDialog } = useBillingContext()
const handleSubscribeToRun = () => {
if (isCloud) {
useTelemetry()?.trackRunButton({
subscribe_to_run: true,
current_tier: subscription.value?.tier?.toLowerCase()
})
useTelemetry()?.trackRunButton({ subscribe_to_run: true })
}
showSubscriptionDialog()

View File

@@ -101,7 +101,6 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
const i18n = createI18n({
legacy: false,
locale: 'en',
escapeParameter: true,
messages: {
en: {
subscription: {
@@ -113,8 +112,7 @@ const i18n = createI18n({
partnerNodesBalance: 'Partner Nodes Balance',
partnerNodesDescription: 'Credits for partner nodes',
totalCredits: 'Total Credits',
creditsRemainingThisMonth: 'Included (Refills {date})',
creditsRemainingThisYear: 'Included (Refills {date})',
creditsRemainingThisMonth: 'Credits remaining this month',
creditsYouveAdded: "Credits you've added",
monthlyBonusDescription: 'Monthly bonus',
prepaidDescription: 'Prepaid credits',
@@ -288,13 +286,6 @@ describe('SubscriptionPanel', () => {
const wrapper = createWrapper()
expect(wrapper.findAll('.skeleton').length).toBe(0)
})
it('renders refill date with literal slashes', () => {
mockIsActiveSubscription.value = true
const wrapper = createWrapper()
expect(wrapper.text()).toContain('Included (Refills 12/31/24)')
expect(wrapper.text()).not.toContain('&#x2F;')
})
})
// TODO: Re-enable when migrating to VTL so we can find by user visible content.

View File

@@ -48,7 +48,7 @@
v-if="isActiveSubscription"
variant="primary"
class="rounded-lg px-4 py-2 text-sm font-normal text-text-primary"
@click="handleUpgradePlan"
@click="showSubscriptionDialog"
>
{{ $t('subscription.upgradePlan') }}
</Button>
@@ -234,11 +234,7 @@ const {
isYearlySubscription
} = useSubscription()
const { showPricingTable } = useSubscriptionDialog()
function handleUpgradePlan() {
showPricingTable({ entry_point: 'settings_upgrade_plan' })
}
const { show: showSubscriptionDialog } = useSubscriptionDialog()
const tierKey = computed(() => {
const tier = subscriptionTier.value
@@ -260,24 +256,12 @@ const refillsDate = computed(() => {
const creditsRemainingLabel = computed(() =>
isYearlySubscription.value
? t(
'subscription.creditsRemainingThisYear',
{
date: refillsDate.value
},
{
escapeParameter: false
}
)
: t(
'subscription.creditsRemainingThisMonth',
{
date: refillsDate.value
},
{
escapeParameter: false
}
)
? t('subscription.creditsRemainingThisYear', {
date: refillsDate.value
})
: t('subscription.creditsRemainingThisMonth', {
date: refillsDate.value
})
)
const planTotalCredits = computed(() => {

View File

@@ -153,7 +153,7 @@ const emit = defineEmits<{
close: [subscribed: boolean]
}>()
const { fetchStatus, isActiveSubscription, subscription } = useBillingContext()
const { fetchStatus, isActiveSubscription } = useBillingContext()
const isSubscriptionEnabled = (): boolean =>
Boolean(isCloud && window.__CONFIG__?.subscription_required)
@@ -236,10 +236,6 @@ watch(
() => isActiveSubscription.value,
(isActive) => {
if (isActive && showCustomPricingTable.value) {
telemetry?.trackSubscriptionSucceeded({
tier: subscription.value?.tier?.toLowerCase(),
duration: subscription.value?.duration?.toLowerCase()
})
emit('close', true)
}
}

View File

@@ -136,7 +136,13 @@ function useSubscriptionInternal() {
const showSubscriptionDialog = (options?: {
reason?: SubscriptionDialogReason
}) => {
// modal_opened tracking is handled by useSubscriptionDialog.show()/showPricingTable()
if (isCloud) {
useTelemetry()?.trackSubscription('modal_opened', {
current_tier: subscriptionTier.value?.toLowerCase(),
reason: options?.reason
})
}
void showSubscriptionRequiredDialog(options)
}

View File

@@ -15,7 +15,7 @@ export function useSubscriptionActions() {
const authActions = useFirebaseAuthActions()
const commandStore = useCommandStore()
const telemetry = useTelemetry()
const { fetchStatus, subscription } = useBillingContext()
const { fetchStatus } = useBillingContext()
const isLoadingSupport = ref(false)
@@ -24,11 +24,6 @@ export function useSubscriptionActions() {
})
const handleAddApiCredits = () => {
if (isCloud) {
telemetry?.trackAddApiCreditButtonClicked({
current_tier: subscription.value?.tier?.toLowerCase()
})
}
void dialogService.showTopUpCreditsDialog()
}

View File

@@ -2,8 +2,6 @@ import { defineAsyncComponent } from 'vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTeamWorkspaceStore } from '@/platform/workspace/stores/teamWorkspaceStore'
@@ -15,38 +13,19 @@ export type SubscriptionDialogReason =
| 'out_of_credits'
| 'top_up_blocked'
interface SubscriptionDialogOptions {
reason?: SubscriptionDialogReason
entry_point?: string
}
export const useSubscriptionDialog = () => {
const { flags } = useFeatureFlags()
const dialogService = useDialogService()
const dialogStore = useDialogStore()
const workspaceStore = useTeamWorkspaceStore()
const { isFreeTier, subscriptionTier } = useSubscription()
const { isFreeTier } = useSubscription()
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
dialogStore.closeDialog({ key: FREE_TIER_DIALOG_KEY })
}
function trackModalOpened(
modalName: 'free_tier_upsell' | 'pricing_table',
options?: SubscriptionDialogOptions
) {
if (isCloud) {
useTelemetry()?.trackSubscription('modal_opened', {
current_tier: subscriptionTier.value?.toLowerCase(),
reason: options?.reason,
entry_point: options?.entry_point,
modal_name: modalName
})
}
}
function openPricingDialog(options?: SubscriptionDialogOptions) {
function showPricingTable(options?: { reason?: SubscriptionDialogReason }) {
const useWorkspaceVariant =
flags.teamWorkspacesEnabled && !workspaceStore.isInPersonalWorkspace
@@ -82,17 +61,8 @@ export const useSubscriptionDialog = () => {
})
}
function showPricingTable(options?: SubscriptionDialogOptions) {
trackModalOpened('pricing_table', options)
openPricingDialog(options)
}
function show(options?: SubscriptionDialogOptions) {
const isPersonalContext =
workspaceStore.isInPersonalWorkspace || !flags.teamWorkspacesEnabled
if (isFreeTier.value && isPersonalContext) {
trackModalOpened('free_tier_upsell', options)
function show(options?: { reason?: SubscriptionDialogReason }) {
if (isFreeTier.value && workspaceStore.isInPersonalWorkspace) {
const component = defineAsyncComponent(
() =>
import('@/platform/cloud/subscription/components/FreeTierDialogContent.vue')
@@ -105,13 +75,6 @@ export const useSubscriptionDialog = () => {
reason: options?.reason,
onClose: hide,
onUpgrade: () => {
if (isCloud) {
useTelemetry()?.trackSubscription('subscribe_clicked', {
current_tier: subscriptionTier.value?.toLowerCase(),
reason: options?.reason,
entry_point: options?.entry_point
})
}
hide()
showPricingTable(options)
}

View File

@@ -261,10 +261,6 @@ export function useNodeReplacement() {
}
replaceWithMapping(node, newNode, effectiveReplacement, nodeGraph, idx)
// Refresh Vue node data — replaceWithMapping bypasses graph.add()
// so onNodeAdded must be called explicitly to update VueNodeData.
nodeGraph.onNodeAdded?.(newNode)
if (!replacedTypes.includes(match.type)) {
replacedTypes.push(match.type)
}
@@ -283,19 +279,6 @@ export function useNodeReplacement() {
life: 3000
})
}
} catch (error) {
console.error('Failed to replace nodes:', error)
if (replacedTypes.length > 0) {
graph.updateExecutionOrder()
graph.setDirtyCanvas(true, true)
}
toastStore.add({
severity: 'error',
summary: t('g.error', 'Error'),
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes'),
life: 5000
})
return replacedTypes
} finally {
changeTracker?.afterChange()
}

View File

@@ -16,7 +16,6 @@ import type {
PageVisibilityMetadata,
SettingChangedMetadata,
SubscriptionMetadata,
SubscriptionSucceededMetadata,
SurveyResponses,
TabCountMetadata,
TelemetryDispatcher,
@@ -78,18 +77,16 @@ export class TelemetryRegistry implements TelemetryDispatcher {
this.dispatch((provider) => provider.trackBeginCheckout?.(metadata))
}
trackSubscriptionSucceeded(metadata?: SubscriptionSucceededMetadata): void {
this.dispatch((provider) => provider.trackSubscriptionSucceeded?.(metadata))
trackMonthlySubscriptionSucceeded(): void {
this.dispatch((provider) => provider.trackMonthlySubscriptionSucceeded?.())
}
trackMonthlySubscriptionCancelled(): void {
this.dispatch((provider) => provider.trackMonthlySubscriptionCancelled?.())
}
trackAddApiCreditButtonClicked(metadata?: { current_tier?: string }): void {
this.dispatch((provider) =>
provider.trackAddApiCreditButtonClicked?.(metadata)
)
trackAddApiCreditButtonClicked(): void {
this.dispatch((provider) => provider.trackAddApiCreditButtonClicked?.())
}
trackApiCreditTopupButtonPurchaseClicked(amount: number): void {
@@ -105,7 +102,6 @@ export class TelemetryRegistry implements TelemetryDispatcher {
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
current_tier?: string
}): void {
this.dispatch((provider) => provider.trackRunButton?.(options))
}

View File

@@ -36,7 +36,6 @@ import type {
RunButtonProperties,
SettingChangedMetadata,
SubscriptionMetadata,
SubscriptionSucceededMetadata,
SurveyResponses,
TabCountMetadata,
TelemetryEventName,
@@ -236,12 +235,12 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
this.trackEvent(eventName, metadata)
}
trackAddApiCreditButtonClicked(metadata?: { current_tier?: string }): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED, metadata)
trackAddApiCreditButtonClicked(): void {
this.trackEvent(TelemetryEvents.ADD_API_CREDIT_BUTTON_CLICKED)
}
trackSubscriptionSucceeded(metadata?: SubscriptionSucceededMetadata): void {
this.trackEvent(TelemetryEvents.SUBSCRIPTION_SUCCEEDED, metadata)
trackMonthlySubscriptionSucceeded(): void {
this.trackEvent(TelemetryEvents.MONTHLY_SUBSCRIPTION_SUCCEEDED)
}
/**
@@ -282,7 +281,6 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
trackRunButton(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
current_tier?: string
}): void {
const executionContext = this.getExecutionContext()
@@ -297,8 +295,7 @@ export class MixpanelTelemetryProvider implements TelemetryProvider {
api_node_names: executionContext.api_node_names,
has_toolkit_nodes: executionContext.has_toolkit_nodes,
toolkit_node_names: executionContext.toolkit_node_names,
trigger_source: options?.trigger_source,
current_tier: options?.current_tier
trigger_source: options?.trigger_source
}
this.lastTriggerSource = options?.trigger_source

View File

@@ -63,7 +63,6 @@ export interface RunButtonProperties {
has_toolkit_nodes: boolean
toolkit_node_names: string[]
trigger_source?: ExecutionTriggerSource
current_tier?: string
}
/**
@@ -306,13 +305,6 @@ export interface CheckoutAttributionMetadata {
export interface SubscriptionMetadata {
current_tier?: string
reason?: SubscriptionDialogReason
entry_point?: string
modal_name?: 'free_tier_upsell' | 'pricing_table'
}
export interface SubscriptionSucceededMetadata {
tier?: string
duration?: string
}
export interface BeginCheckoutMetadata
@@ -340,15 +332,14 @@ export interface TelemetryProvider {
metadata?: SubscriptionMetadata
): void
trackBeginCheckout?(metadata: BeginCheckoutMetadata): void
trackSubscriptionSucceeded?(metadata?: SubscriptionSucceededMetadata): void
trackMonthlySubscriptionSucceeded?(): void
trackMonthlySubscriptionCancelled?(): void
trackAddApiCreditButtonClicked?(metadata?: { current_tier?: string }): void
trackAddApiCreditButtonClicked?(): void
trackApiCreditTopupButtonPurchaseClicked?(amount: number): void
trackApiCreditTopupSucceeded?(): void
trackRunButton?(options?: {
subscribe_to_run?: boolean
trigger_source?: ExecutionTriggerSource
current_tier?: string
}): void
// Credit top-up tracking (composition with internal utilities)
@@ -432,7 +423,7 @@ export const TelemetryEvents = {
RUN_BUTTON_CLICKED: 'app:run_button_click',
SUBSCRIPTION_REQUIRED_MODAL_OPENED: 'app:subscription_required_modal_opened',
SUBSCRIBE_NOW_BUTTON_CLICKED: 'app:subscribe_now_button_clicked',
SUBSCRIPTION_SUCCEEDED: 'app:subscription_succeeded',
MONTHLY_SUBSCRIPTION_SUCCEEDED: 'app:monthly_subscription_succeeded',
MONTHLY_SUBSCRIPTION_CANCELLED: 'app:monthly_subscription_cancelled',
ADD_API_CREDIT_BUTTON_CLICKED: 'app:add_api_credit_button_clicked',
API_CREDIT_TOPUP_BUTTON_PURCHASE_CLICKED:
@@ -531,4 +522,3 @@ export type TelemetryEventProperties =
| WorkflowCreatedMetadata
| EnterLinearMetadata
| SubscriptionMetadata
| SubscriptionSucceededMetadata

View File

@@ -2,58 +2,12 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
LoadedComfyWorkflow,
PendingWarnings
} from '@/platform/workflow/management/stores/comfyWorkflow'
import { ComfyWorkflow as ComfyWorkflowClass } from '@/platform/workflow/management/stores/comfyWorkflow'
import type { PendingWarnings } from '@/platform/workflow/management/stores/comfyWorkflow'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { app } from '@/scripts/app'
import { useAppMode } from '@/composables/useAppMode'
import type { AppMode } from '@/composables/useAppMode'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
function createModeTestWorkflow(
options: {
path?: string
initialMode?: AppMode | null
activeMode?: AppMode | null
loaded?: boolean
} = {}
): LoadedComfyWorkflow {
const workflow = new ComfyWorkflowClass({
path: options.path ?? 'workflows/test.json',
modified: Date.now(),
size: 100
})
if ('initialMode' in options) workflow.initialMode = options.initialMode
workflow.activeMode = options.activeMode ?? null
if (options.loaded !== false) {
workflow.changeTracker = createMockChangeTracker()
workflow.content = '{}'
workflow.originalContent = '{}'
}
return workflow as LoadedComfyWorkflow
}
function makeWorkflowData(
extra: Record<string, unknown> = {}
): ComfyWorkflowJSON {
return {
last_node_id: 5,
last_link_id: 3,
nodes: [],
links: [],
groups: [],
config: {},
version: 0.4,
extra
}
}
const { mockShowMissingNodes, mockShowMissingModels } = vi.hoisted(() => ({
mockShowMissingNodes: vi.fn(),
@@ -118,14 +72,6 @@ vi.mock('@/stores/domWidgetStore', () => ({
})
}))
vi.mock('@/stores/workspaceStore', () => ({
useWorkspaceStore: () => ({
get workflow() {
return useWorkflowStore()
}
})
}))
const MISSING_MODELS: PendingWarnings['missingModels'] = {
missingModels: [
{ name: 'model.safetensors', url: '', directory: 'checkpoints' }
@@ -137,7 +83,7 @@ function createWorkflow(
warnings: PendingWarnings | null = null,
options: { loadable?: boolean; path?: string } = {}
): ComfyWorkflow {
const wf = {
return {
pendingWarnings: warnings,
...(options.loadable && {
path: options.path ?? 'workflows/test.json',
@@ -145,8 +91,7 @@ function createWorkflow(
activeState: { nodes: [], links: [] },
changeTracker: { reset: vi.fn(), restore: vi.fn() }
})
} as Partial<ComfyWorkflow>
return wf as ComfyWorkflow
} as Partial<ComfyWorkflow> as ComfyWorkflow
}
function enableWarningSettings() {
@@ -235,7 +180,12 @@ describe('useWorkflowService', () => {
workflowStore = useWorkflowStore()
vi.mocked(app.loadGraphData).mockImplementation(
async (_data, _clean, _restore, wf) => {
workflowStore.activeWorkflow = wf as LoadedComfyWorkflow
;(
workflowStore as Partial<Record<string, unknown>> as Record<
string,
unknown
>
).activeWorkflow = wf
}
)
})
@@ -306,231 +256,4 @@ describe('useWorkflowService', () => {
expect(mockShowMissingNodes).toHaveBeenCalledTimes(1)
})
})
describe('per-workflow mode switching', () => {
let appMode: ReturnType<typeof useAppMode>
let workflowStore: ReturnType<typeof useWorkflowStore>
let service: ReturnType<typeof useWorkflowService>
function mockOpenWorkflow() {
vi.spyOn(workflowStore, 'openWorkflow').mockImplementation(async (wf) => {
// Simulate load() setting changeTracker on first open
if (!wf.changeTracker) {
wf.changeTracker = createMockChangeTracker()
wf.content = '{}'
wf.originalContent = '{}'
}
const loaded = wf as LoadedComfyWorkflow
workflowStore.activeWorkflow = loaded
return loaded
})
}
beforeEach(() => {
appMode = useAppMode()
workflowStore = useWorkflowStore()
service = useWorkflowService()
})
describe('mode derivation from active workflow', () => {
it('reflects initialMode of the active workflow', () => {
const workflow = createModeTestWorkflow({ initialMode: 'app' })
workflowStore.activeWorkflow = workflow
expect(appMode.mode.value).toBe('app')
})
it('activeMode takes precedence over initialMode', () => {
const workflow = createModeTestWorkflow({
initialMode: 'app',
activeMode: 'graph'
})
workflowStore.activeWorkflow = workflow
expect(appMode.mode.value).toBe('graph')
})
it('defaults to graph when no active workflow', () => {
expect(appMode.mode.value).toBe('graph')
})
it('updates when activeWorkflow changes', () => {
const workflow1 = createModeTestWorkflow({
path: 'workflows/one.json',
initialMode: 'app'
})
const workflow2 = createModeTestWorkflow({
path: 'workflows/two.json',
activeMode: 'builder:select'
})
workflowStore.activeWorkflow = workflow1
expect(appMode.mode.value).toBe('app')
workflowStore.activeWorkflow = workflow2
expect(appMode.mode.value).toBe('builder:select')
})
})
describe('setMode writes to active workflow', () => {
it('writes activeMode without changing initialMode', () => {
const workflow = createModeTestWorkflow({ initialMode: 'graph' })
workflowStore.activeWorkflow = workflow
appMode.setMode('builder:arrange')
expect(workflow.activeMode).toBe('builder:arrange')
expect(workflow.initialMode).toBe('graph')
expect(appMode.mode.value).toBe('builder:arrange')
})
})
describe('afterLoadNewGraph initializes initialMode', () => {
beforeEach(() => {
mockOpenWorkflow()
})
it('sets initialMode from extra.linearMode on first load', async () => {
const workflow = createModeTestWorkflow({ loaded: false })
await service.afterLoadNewGraph(
workflow,
makeWorkflowData({ linearMode: true })
)
expect(workflow.initialMode).toBe('app')
})
it('leaves initialMode null when extra.linearMode is absent', async () => {
const workflow = createModeTestWorkflow({ loaded: false })
await service.afterLoadNewGraph(workflow, makeWorkflowData())
expect(workflow.initialMode).toBeNull()
})
it('sets initialMode to graph when extra.linearMode is false', async () => {
const workflow = createModeTestWorkflow({ loaded: false })
await service.afterLoadNewGraph(
workflow,
makeWorkflowData({ linearMode: false })
)
expect(workflow.initialMode).toBe('graph')
})
it('does not set initialMode on tab switch even if data has linearMode', async () => {
const workflow = createModeTestWorkflow({ loaded: false })
// First load — no linearMode in data
await service.afterLoadNewGraph(workflow, makeWorkflowData())
expect(workflow.initialMode).toBeNull()
// User switches to app mode at runtime
workflow.activeMode = 'app'
// Tab switch / reload — data now has linearMode (leaked from graph)
await service.afterLoadNewGraph(
workflow,
makeWorkflowData({ linearMode: true })
)
// initialMode should NOT have been updated — only builder save sets it
expect(workflow.initialMode).toBeNull()
})
it('preserves existing initialMode on tab switch', async () => {
const workflow = createModeTestWorkflow({
initialMode: 'app'
})
await service.afterLoadNewGraph(workflow, makeWorkflowData())
expect(workflow.initialMode).toBe('app')
})
it('sets initialMode to app for fresh string-based loads with linearMode', async () => {
vi.spyOn(workflowStore, 'createNewTemporary').mockReturnValue(
createModeTestWorkflow()
)
await service.afterLoadNewGraph(
'test.json',
makeWorkflowData({ linearMode: true })
)
expect(appMode.mode.value).toBe('app')
})
it('syncs linearMode to rootGraph.extra for draft persistence', async () => {
const workflow = createModeTestWorkflow({ loaded: false })
await service.afterLoadNewGraph(
workflow,
makeWorkflowData({ linearMode: true })
)
expect(app.rootGraph.extra.linearMode).toBe(true)
})
it('reads initialMode from file when draft lacks linearMode (restoration)', async () => {
const filePath = 'workflows/saved-app.json'
const fileInitialState = makeWorkflowData({ linearMode: true })
const mockTracker = createMockChangeTracker()
mockTracker.initialState = fileInitialState
// Persisted, not-loaded workflow in the store
const persistedWorkflow = new ComfyWorkflowClass({
path: filePath,
modified: Date.now(),
size: 100
})
vi.spyOn(workflowStore, 'getWorkflowByPath').mockReturnValue(
persistedWorkflow
)
vi.spyOn(workflowStore, 'openWorkflow').mockImplementation(
async (wf) => {
wf.changeTracker = mockTracker
wf.content = JSON.stringify(fileInitialState)
wf.originalContent = wf.content
workflowStore.activeWorkflow = wf as LoadedComfyWorkflow
return wf as LoadedComfyWorkflow
}
)
// Draft data has NO linearMode (simulates rootGraph serialization)
const draftData = makeWorkflowData()
await service.afterLoadNewGraph('saved-app.json', draftData)
// initialMode should come from the file, not the draft
expect(persistedWorkflow.initialMode).toBe('app')
expect(app.rootGraph.extra.linearMode).toBe(true)
})
})
describe('round-trip mode preservation', () => {
it('each workflow retains its own mode across tab switches', () => {
const workflow1 = createModeTestWorkflow({
path: 'workflows/one.json',
activeMode: 'builder:select'
})
const workflow2 = createModeTestWorkflow({
path: 'workflows/two.json',
initialMode: 'app'
})
workflowStore.activeWorkflow = workflow1
expect(appMode.mode.value).toBe('builder:select')
workflowStore.activeWorkflow = workflow2
expect(appMode.mode.value).toBe('app')
workflowStore.activeWorkflow = workflow1
expect(appMode.mode.value).toBe('builder:select')
})
})
})
})

Some files were not shown because too many files have changed in this diff Show More