Compare commits

..

2 Commits

Author SHA1 Message Date
Chenlei Hu
702458acbd Refactor + error handling 2025-05-07 14:52:37 -04:00
Chenlei Hu
fd01f13fbf Update link_fixer from rgthree 2025-05-07 14:44:28 -04:00
119 changed files with 1157 additions and 20971 deletions

View File

@@ -30,7 +30,7 @@ jobs:
with:
repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
ref: '9d2421fd3208a310e4d0f71fca2ea0c985759c33'
- uses: actions/setup-node@v4
with:

View File

@@ -33,11 +33,5 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -130,11 +130,6 @@
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -42,12 +42,12 @@
"config": {},
"extra": {
"ds": {
"scale": 1,
"scale": 2.1600300525920346,
"offset": [
0,
0
63.071794466403446,
75.18055335968394
]
}
},
"version": 0.4
}
}

View File

@@ -499,11 +499,6 @@
],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -1,85 +0,0 @@
{
"id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862",
"revision": 0,
"last_node_id": 4,
"last_link_id": 3,
"nodes": [
{
"id": 4,
"type": "PreviewAny",
"pos": [946.2566528320312, 598.4373168945312],
"size": [140, 76],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 3
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 1,
"type": "PreviewAny",
"pos": [951.0236206054688, 421.3861083984375],
"size": [140, 76],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 2
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": []
},
{
"id": 3,
"type": "PrimitiveString",
"pos": [575.1760864257812, 504.5214538574219],
"size": [270, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [2, 3]
}
],
"properties": {
"Node name for S&R": "PrimitiveString"
},
"widgets_values": ["foo"]
}
],
"links": [
[2, 3, 0, 1, 0, "*"],
[3, 3, 0, 4, 0, "*"]
],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.19.1",
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

View File

@@ -160,4 +160,4 @@
}
},
"version": 0.4
}
}

View File

@@ -43,10 +43,6 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"groupNodes": {
"group_node": {
"nodes": [
@@ -405,4 +401,4 @@
}
},
"version": 0.4
}
}

View File

@@ -110,10 +110,6 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"groupNodes": {
"hello": {
"nodes": [
@@ -253,4 +249,4 @@
}
},
"version": 0.4
}
}

View File

@@ -44,11 +44,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -61,11 +61,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -1,36 +0,0 @@
{
"id": "5635564e-189f-49e4-9b25-6b7634bcd595",
"revision": 0,
"last_node_id": 78,
"last_link_id": 53,
"nodes": [
{
"id": 78,
"type": "DevToolsNodeWithV2ComboInput",
"pos": [1320, 904],
"size": [270.3199157714844, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "COMBO",
"type": "COMBO",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsNodeWithV2ComboInput"
},
"widgets_values": ["A"]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.19.7"
},
"version": 0.4
}

View File

@@ -50,11 +50,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -38,11 +38,7 @@
"groups": [],
"config": {},
"extra": {
"groupNodes": {},
"ds": {
"offset": [0, 0],
"scale": 1
}
"groupNodes": {}
},
"version": 0.4
}
}

View File

@@ -54,11 +54,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -92,14 +92,10 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"VHS_latentpreview": true,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": false,
"VHS_KeepIntermediate": false
},
"version": 0.4
}
}

View File

@@ -84,10 +84,6 @@
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
},
"reroutes": [
{
"id": 1,

View File

@@ -106,7 +106,10 @@
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
"offset": {
"0": 0,
"1": 0
}
}
},
"version": 0.4

View File

@@ -368,10 +368,10 @@
"ds": {
"scale": 1,
"offset": [
0,
0
149.9747408641311,
383.8593224280729
]
}
},
"version": 0.4
}
}

View File

@@ -31,11 +31,5 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}
}

View File

@@ -8,4 +8,4 @@
"title": "Load Animated Image"
}
}
}
}

View File

@@ -27,11 +27,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}

View File

@@ -41,11 +41,6 @@
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"extra": {},
"version": 0.4
}
}

View File

@@ -54,11 +54,7 @@
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.17.0",
"ds": {
"offset": [0, 0],
"scale": 1
}
"frontendVersion": "1.17.0"
},
"version": 0.4
}

View File

@@ -18,27 +18,3 @@ test.describe('Execution', () => {
)
})
})
test.describe('Execute to selected output nodes', () => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
// @note: Wait for the execution to finish. We might want to move to a more
// reliable way to wait for the execution to finish. Workflow in this test
// is simple enough that this is fine for now.
await comfyPage.page.waitForTimeout(200)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
})
})

View File

@@ -666,12 +666,6 @@ test.describe('Load workflow', () => {
expect(activeWorkflowName).toEqual(workflowPathB)
})
})
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
})
})
test.describe('Load duplicate workflow', () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -53,26 +53,6 @@ test.describe('Combo text widget', () => {
const refreshedComboValues = await getComboValues()
expect(refreshedComboValues).not.toEqual(initialComboValues)
})
test('Should refresh combo values of nodes with v2 combo input spec', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('node_with_v2_combo_input')
// click canvas to focus
await comfyPage.page.mouse.click(400, 300)
// press R to trigger refresh
await comfyPage.page.keyboard.press('r')
// wait for nodes' widgets to be updated
await comfyPage.page.mouse.click(400, 300)
await comfyPage.nextFrame()
// get the combo widget's values
const comboValues = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes
.find((node) => node.title === 'Node With V2 Combo Input')
.widgets.find((widget) => widget.name === 'combo_input').options.values
})
expect(comboValues).toEqual(['A', 'B'])
})
})
test.describe('Boolean widget', () => {

12
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.19.9",
"version": "1.19.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.19.9",
"version": "1.19.4",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@comfyorg/litegraph": "^0.15.9",
"@comfyorg/litegraph": "^0.15.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -482,9 +482,9 @@
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.15.9",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.9.tgz",
"integrity": "sha512-uMKUZx3VPLnpTOAcdvnppbJPQ/BL7Yt4flU2YeF+835r/59il+iUXCfnObpYmJj5jA4o89atpxByS8Jl1PM9vQ==",
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.3.tgz",
"integrity": "sha512-YpHhNgV6a1bsi/NCuK/u5m9bgI3HK5qLXCLR2aKUJ4MPVfdILodqcbkKVBOb4li6KKUyg7plQ9GJpml6F2/fYQ==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.19.9",
"version": "1.19.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -72,7 +72,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@comfyorg/litegraph": "^0.15.9",
"@comfyorg/litegraph": "^0.15.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

View File

@@ -32,8 +32,6 @@ describe('TreeExplorerTreeNode', () => {
handleRename: () => {}
} as RenderedTreeExplorerNode
const mockHandleEditLabel = vi.fn()
beforeAll(() => {
// Create a Vue app instance for PrimeVuePrimeVue
const app = createApp({})
@@ -50,10 +48,7 @@ describe('TreeExplorerTreeNode', () => {
props: { node: mockNode },
global: {
components: { EditableText, Badge },
plugins: [createTestingPinia(), i18n],
provide: {
[InjectKeyHandleEditLabelFunction]: mockHandleEditLabel
}
plugins: [createTestingPinia(), i18n]
}
})
@@ -77,10 +72,7 @@ describe('TreeExplorerTreeNode', () => {
},
global: {
components: { EditableText, Badge, InputText },
plugins: [createTestingPinia(), i18n, PrimeVue],
provide: {
[InjectKeyHandleEditLabelFunction]: mockHandleEditLabel
}
plugins: [createTestingPinia(), i18n, PrimeVue]
}
})

View File

@@ -1,132 +1,95 @@
<template>
<div class="w-96 p-2 overflow-x-hidden">
<ApiKeyForm
v-if="showApiKeyForm"
@back="showApiKeyForm = false"
@success="onSuccess"
/>
<template v-else>
<!-- Header -->
<div class="flex flex-col gap-4 mb-8">
<h1 class="text-2xl font-medium leading-normal my-0">
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
</h1>
<p class="text-base my-0">
<span class="text-muted">{{
isSignIn
? t('auth.login.newUser')
: t('auth.signup.alreadyHaveAccount')
}}</span>
<span
class="ml-1 cursor-pointer text-blue-500"
@click="toggleState"
>{{
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
}}</span
>
</p>
</div>
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
{{ t('auth.login.insecureContextWarning') }}
</Message>
<!-- Form -->
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
<template v-else>
<Message v-if="userIsInChina" severity="warn" class="mb-4">
{{ t('auth.signup.regionRestrictionChina') }}
</Message>
<SignUpForm v-else @submit="signUpWithEmail" />
</template>
<!-- Divider -->
<Divider align="center" layout="horizontal" class="my-8">
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
</Divider>
<!-- Social Login Buttons -->
<div class="flex flex-col gap-6">
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGoogle')
: t('auth.signup.signUpWithGoogle')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGithub')
: t('auth.signup.signUpWithGithub')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="showApiKeyForm = true"
>
<img
src="/assets/images/comfy-logo-mono.svg"
class="w-5 h-5 mr-2"
alt="Comfy"
/>
{{ t('auth.login.useApiKey') }}
</Button>
<small class="text-muted text-center">
{{ t('auth.apiKey.helpText') }}
<a
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.apiKey.generateKey') }}
</a>
</small>
</div>
<!-- Terms & Contact -->
<p class="text-xs text-muted mt-8">
{{ t('auth.login.termsText') }}
<a
href="https://www.comfy.org/terms-of-service"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.login.termsLink') }}
</a>
{{ t('auth.login.andText') }}
<a
href="https://www.comfy.org/privacy"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.login.privacyLink') }} </a
>.
{{ t('auth.login.questionsContactPrefix') }}
<a href="mailto:hello@comfy.org" class="text-blue-500 cursor-pointer">
hello@comfy.org</a
>.
<div class="w-96 p-2">
<!-- Header -->
<div class="flex flex-col gap-4 mb-8">
<h1 class="text-2xl font-medium leading-normal my-0">
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
</h1>
<p class="text-base my-0">
<span class="text-muted">{{
isSignIn
? t('auth.login.newUser')
: t('auth.signup.alreadyHaveAccount')
}}</span>
<span class="ml-1 cursor-pointer text-blue-500" @click="toggleState">{{
isSignIn ? t('auth.login.signUp') : t('auth.signup.signIn')
}}</span>
</p>
</div>
<Message v-if="!isSecureContext" severity="warn" class="mb-4">
{{ t('auth.login.insecureContextWarning') }}
</Message>
<!-- Form -->
<SignInForm v-if="isSignIn" @submit="signInWithEmail" />
<template v-else>
<Message v-if="userIsInChina" severity="warn" class="mb-4">
{{ t('auth.signup.regionRestrictionChina') }}
</Message>
<SignUpForm v-else @submit="signUpWithEmail" />
</template>
<!-- Divider -->
<Divider align="center" layout="horizontal" class="my-8">
<span class="text-muted">{{ t('auth.login.orContinueWith') }}</span>
</Divider>
<!-- Social Login Buttons -->
<div class="flex flex-col gap-6">
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGoogle')
: t('auth.signup.signUpWithGoogle')
}}
</Button>
<Button
type="button"
class="h-10"
severity="secondary"
outlined
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
{{
isSignIn
? t('auth.login.loginWithGithub')
: t('auth.signup.signUpWithGithub')
}}
</Button>
</div>
<!-- Terms & Contact -->
<p class="text-xs text-muted mt-8">
{{ t('auth.login.termsText') }}
<a
href="https://www.comfy.org/terms-of-service"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.login.termsLink') }}
</a>
{{ t('auth.login.andText') }}
<a
href="https://www.comfy.org/privacy"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.login.privacyLink') }} </a
>.
{{ t('auth.login.questionsContactPrefix') }}
<a href="mailto:hello@comfy.org" class="text-blue-500 cursor-pointer">
hello@comfy.org</a
>.
</p>
</div>
</template>
@@ -137,12 +100,10 @@ import Message from 'primevue/message'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import { SignInData, SignUpData } from '@/schemas/signInSchema'
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
import { isInChina } from '@/utils/networkUtil'
import ApiKeyForm from './signin/ApiKeyForm.vue'
import SignInForm from './signin/SignInForm.vue'
import SignUpForm from './signin/SignUpForm.vue'
@@ -154,11 +115,8 @@ const { t } = useI18n()
const authService = useFirebaseAuthService()
const isSecureContext = window.isSecureContext
const isSignIn = ref(true)
const showApiKeyForm = ref(false)
const toggleState = () => {
isSignIn.value = !isSignIn.value
showApiKeyForm.value = false
}
const signInWithGoogle = async () => {

View File

@@ -1,8 +1,6 @@
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import PrimeVue from 'primevue/config'
import Tag from 'primevue/tag'
import Tooltip from 'primevue/tooltip'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
@@ -21,16 +19,7 @@ describe('SettingItem', () => {
const mountComponent = (props: any, options = {}): any => {
return mount(SettingItem, {
global: {
plugins: [PrimeVue, i18n, createPinia()],
components: {
Tag
},
directives: {
tooltip: Tooltip
},
stubs: {
'i-material-symbols:experiment-outline': true
}
plugins: [PrimeVue, i18n, createPinia()]
},
props,
...options

View File

@@ -4,11 +4,10 @@
<h2 class="text-2xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
<Divider class="mb-3" />
<!-- Normal User Panel -->
<div v-if="isLoggedIn" class="flex flex-col gap-2">
<div v-if="user" class="flex flex-col gap-2">
<UserAvatar
v-if="userPhotoUrl"
:photo-url="userPhotoUrl"
v-if="user.photoURL"
:photo-url="user.photoURL"
shape="circle"
size="large"
/>
@@ -18,7 +17,7 @@
{{ $t('userSettings.name') }}
</h3>
<div class="text-muted">
{{ userDisplayName || $t('userSettings.notSet') }}
{{ user.displayName || $t('userSettings.notSet') }}
</div>
</div>
@@ -26,9 +25,9 @@
<h3 class="font-medium">
{{ $t('userSettings.email') }}
</h3>
<span class="text-muted">
{{ userEmail }}
</span>
<a :href="'mailto:' + user.email" class="hover:underline">
{{ user.email }}
</a>
</div>
<div class="flex flex-col gap-0.5">
@@ -91,22 +90,51 @@ import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ProgressSpinner from 'primevue/progressspinner'
import TabPanel from 'primevue/tabpanel'
import { computed } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const dialogService = useDialogService()
const {
loading,
isLoggedIn,
isEmailProvider,
userDisplayName,
userEmail,
userPhotoUrl,
providerName,
providerIcon,
handleSignOut,
handleSignIn
} = useCurrentUser()
const authStore = useFirebaseAuthStore()
const commandStore = useCommandStore()
const user = computed(() => authStore.currentUser)
const loading = computed(() => authStore.loading)
const providerName = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'Google'
}
if (providerId?.includes('github')) {
return 'GitHub'
}
return providerId
})
const providerIcon = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'pi pi-google'
}
if (providerId?.includes('github')) {
return 'pi pi-github'
}
return 'pi pi-user'
})
const isEmailProvider = computed(() => {
const providerId = user.value?.providerData[0]?.providerId
return providerId === 'password'
})
const handleSignOut = async () => {
await commandStore.execute('Comfy.User.SignOut')
}
const handleSignIn = async () => {
await commandStore.execute('Comfy.User.OpenSignInDialog')
}
</script>

View File

@@ -1,117 +0,0 @@
import { Form } from '@primevue/forms'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import Button from 'primevue/button'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp } from 'vue'
import { createI18n } from 'vue-i18n'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import ApiKeyForm from './ApiKeyForm.vue'
const mockStoreApiKey = vi.fn()
const mockLoading = vi.fn(() => false)
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: vi.fn(() => ({
loading: mockLoading()
}))
}))
vi.mock('@/stores/apiKeyAuthStore', () => ({
useApiKeyAuthStore: vi.fn(() => ({
storeApiKey: mockStoreApiKey
}))
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
auth: {
apiKey: {
title: 'API Key',
label: 'API Key',
placeholder: 'Enter your API Key',
error: 'Invalid API Key',
helpText: 'Need an API key?',
generateKey: 'Get one here',
whitelistInfo: 'About non-whitelisted sites',
description: 'Use your Comfy API key to enable API Nodes'
}
},
g: {
back: 'Back',
save: 'Save',
learnMore: 'Learn more'
}
}
}
})
describe('ApiKeyForm', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)
vi.clearAllMocks()
mockStoreApiKey.mockReset()
mockLoading.mockReset()
})
const mountComponent = (props: any = {}) => {
return mount(ApiKeyForm, {
global: {
plugins: [PrimeVue, createPinia(), i18n],
components: { Button, Form, InputText, Message }
},
props
})
}
it('renders correctly with all required elements', () => {
const wrapper = mountComponent()
expect(wrapper.find('h1').text()).toBe('API Key')
expect(wrapper.find('label').text()).toBe('API Key')
expect(wrapper.findComponent(InputText).exists()).toBe(true)
expect(wrapper.findComponent(Button).exists()).toBe(true)
})
it('emits back event when back button is clicked', async () => {
const wrapper = mountComponent()
await wrapper.findComponent(Button).trigger('click')
expect(wrapper.emitted('back')).toBeTruthy()
})
it('shows loading state when submitting', async () => {
mockLoading.mockReturnValue(true)
const wrapper = mountComponent()
const input = wrapper.findComponent(InputText)
await input.setValue(
'comfyui-123456789012345678901234567890123456789012345678901234567890123456789012'
)
await wrapper.find('form').trigger('submit')
const submitButton = wrapper
.findAllComponents(Button)
.find((btn) => btn.text() === 'Save')
expect(submitButton?.props('loading')).toBe(true)
})
it('displays help text and links correctly', () => {
const wrapper = mountComponent()
const helpText = wrapper.find('small')
expect(helpText.text()).toContain('Need an API key?')
expect(helpText.find('a').attributes('href')).toBe(
`${COMFY_PLATFORM_BASE_URL}/login`
)
})
})

View File

@@ -1,112 +0,0 @@
<template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-4 mb-8">
<h1 class="text-2xl font-medium leading-normal my-0">
{{ t('auth.apiKey.title') }}
</h1>
<div class="flex flex-col gap-2">
<p class="text-base my-0 text-muted">
{{ t('auth.apiKey.description') }}
</p>
<a
href="https://docs.comfy.org/interface/user#logging-in-with-an-api-key"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('g.learnMore') }}
</a>
</div>
</div>
<Form
v-slot="$form"
class="flex flex-col gap-6"
:resolver="zodResolver(apiKeySchema)"
@submit="onSubmit"
>
<Message v-if="$form.apiKey?.invalid" severity="error" class="mb-4">
{{ $form.apiKey.error.message }}
</Message>
<div class="flex flex-col gap-2">
<label
class="opacity-80 text-base font-medium mb-2"
for="comfy-org-api-key"
>
{{ t('auth.apiKey.label') }}
</label>
<div class="flex flex-col gap-2">
<InputText
pt:root:id="comfy-org-api-key"
pt:root:autocomplete="off"
class="h-10"
name="apiKey"
type="password"
:placeholder="t('auth.apiKey.placeholder')"
:invalid="$form.apiKey?.invalid"
/>
<small class="text-muted">
{{ t('auth.apiKey.helpText') }}
<a
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.apiKey.generateKey') }}
</a>
<span class="mx-1"></span>
<a
href="https://docs.comfy.org/tutorials/api-nodes/overview#log-in-with-api-key-on-non-whitelisted-websites"
target="_blank"
class="text-blue-500 cursor-pointer"
>
{{ t('auth.apiKey.whitelistInfo') }}
</a>
</small>
</div>
</div>
<div class="flex justify-between items-center mt-4">
<Button type="button" link @click="$emit('back')">
{{ t('g.back') }}
</Button>
<Button type="submit" :loading="loading" :disabled="loading">
{{ t('g.save') }}
</Button>
</div>
</Form>
</div>
</template>
<script setup lang="ts">
import { Form, FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { COMFY_PLATFORM_BASE_URL } from '@/config/comfyApi'
import { apiKeySchema } from '@/schemas/signInSchema'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const apiKeyStore = useApiKeyAuthStore()
const loading = computed(() => authStore.loading)
const { t } = useI18n()
const emit = defineEmits<{
(e: 'back'): void
(e: 'success'): void
}>()
const onSubmit = async (event: FormSubmitEvent) => {
if (event.valid) {
await apiKeyStore.storeApiKey(event.values.apiKey)
emit('success')
}
}
</script>

View File

@@ -16,12 +16,12 @@ import { computed, watch } from 'vue'
import DomWidget from '@/components/graph/widgets/DomWidget.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { type DomWidgetState, useDomWidgetStore } from '@/stores/domWidgetStore'
import { useCanvasStore } from '@/stores/graphStore'
const domWidgetStore = useDomWidgetStore()
const widgetStates = computed(() =>
Array.from(domWidgetStore.widgetStates.values())
const widgetStates = computed(
() => Array.from(domWidgetStore.widgetStates.values()) as DomWidgetState[]
)
const updateWidgets = () => {

View File

@@ -225,6 +225,32 @@ watch(
}
)
// Save the drag & scale info in the serialized workflow if the setting is enabled
watch(
[
() => canvasStore.canvas,
() => settingStore.get('Comfy.EnableWorkflowViewRestore')
],
([canvas, enableWorkflowViewRestore]) => {
const extra = canvas?.graph?.extra
if (!extra) return
if (enableWorkflowViewRestore) {
extra.ds = {
get scale() {
return canvas.ds.scale
},
get offset() {
const [x, y] = canvas.ds.offset
return [x, y]
}
}
} else {
delete extra.ds
}
}
)
useEventListener(
canvasRef,
'litegraph:no-items-selected',
@@ -267,7 +293,7 @@ onMounted(async () => {
workspaceStore.spinner = true
// ChangeTracker needs to be initialized before setup, as it will overwrite
// some listeners of litegraph canvas.
ChangeTracker.init()
ChangeTracker.init(comfyApp)
await loadCustomNodesI18n()
try {
await settingStore.loadSettingValues()

View File

@@ -6,7 +6,6 @@
content: 'p-0 flex flex-row'
}"
>
<ExecuteButton v-show="nodeSelected" />
<ColorPickerButton v-show="nodeSelected || groupSelected" />
<Button
v-show="nodeSelected"
@@ -75,7 +74,6 @@ import Panel from 'primevue/panel'
import { computed } from 'vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
import { st, t } from '@/i18n'
import { useExtensionService } from '@/services/extensionService'

View File

@@ -1,71 +0,0 @@
<template>
<Button
v-tooltip.top="{
value: isDisabled
? t('selectionToolbox.executeButton.disabledTooltip')
: t('selectionToolbox.executeButton.tooltip'),
showDelay: 1000
}"
:severity="isDisabled ? 'secondary' : 'success'"
text
:disabled="isDisabled"
@mouseenter="() => handleMouseEnter()"
@mouseleave="() => handleMouseLeave()"
@click="handleClick"
>
<i-lucide:play />
</Button>
</template>
<script setup lang="ts">
import type { LGraphNode } from '@comfyorg/litegraph'
import Button from 'primevue/button'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { isLGraphNode } from '@/utils/litegraphUtil'
const { t } = useI18n()
const canvasStore = useCanvasStore()
const commandStore = useCommandStore()
const canvas = canvasStore.getCanvas()
const buttonHovered = ref(false)
const selectedOutputNodes = computed(
() =>
canvasStore.selectedItems.filter(
(item) => isLGraphNode(item) && item.constructor.nodeData?.output_node
) as LGraphNode[]
)
const isDisabled = computed(() => selectedOutputNodes.value.length === 0)
function outputNodeStokeStyle(this: LGraphNode) {
if (
this.selected &&
this.constructor.nodeData?.output_node &&
buttonHovered.value
) {
return { color: 'orange', lineWidth: 2, padding: 10 }
}
}
const handleMouseEnter = () => {
buttonHovered.value = true
for (const node of selectedOutputNodes.value) {
node.strokeStyles['outputNode'] = outputNodeStokeStyle
}
canvas.setDirty(true)
}
const handleMouseLeave = () => {
buttonHovered.value = false
canvas.setDirty(true)
}
const handleClick = async () => {
await commandStore.execute('Comfy.QueueSelectedOutputNodes')
}
</script>

View File

@@ -1,53 +0,0 @@
<template>
<div
class="relative w-full text-xs min-h-[28px] max-h-[200px] rounded-lg px-4 py-2 overflow-y-auto"
>
<div class="flex items-center gap-2">
<div class="flex-1 break-all flex items-center gap-2">
<span v-html="formattedText"></span>
<Skeleton v-if="isParentNodeExecuting" class="!flex-1 !h-4" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { NodeId } from '@comfyorg/litegraph'
import Skeleton from 'primevue/skeleton'
import { computed, onMounted, ref, watch } from 'vue'
import { useExecutionStore } from '@/stores/executionStore'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const modelValue = defineModel<string>({ required: true })
defineProps<{
widget?: object
}>()
const executionStore = useExecutionStore()
const isParentNodeExecuting = ref(true)
const formattedText = computed(() => nl2br(linkifyHtml(modelValue.value)))
let executingNodeId: NodeId | null = null
onMounted(() => {
executingNodeId = executionStore.executingNodeId
})
// Watch for either a new node has starting execution or overall execution ending
const stopWatching = watch(
[() => executionStore.executingNode, () => executionStore.isIdle],
() => {
if (
executionStore.isIdle ||
(executionStore.executingNode &&
executionStore.executingNode.id !== executingNodeId)
) {
isParentNodeExecuting.value = false
stopWatching()
}
if (!executingNodeId) {
executingNodeId = executionStore.executingNodeId
}
}
)
</script>

View File

@@ -75,7 +75,7 @@
</template>
<script setup lang="ts">
import { LGraphNode } from '@comfyorg/litegraph'
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
import { Tooltip } from 'primevue'
import Button from 'primevue/button'
@@ -101,13 +101,13 @@ const emit = defineEmits<{
const resizeNodeMatchOutput = () => {
console.log('resizeNodeMatchOutput')
const outputWidth = node.widgets?.find((w) => w.name === 'width')
const outputHeight = node.widgets?.find((w) => w.name === 'height')
const outputWidth = node.widgets?.find((w: IWidget) => w.name === 'width')
const outputHeight = node.widgets?.find((w: IWidget) => w.name === 'height')
if (outputWidth && outputHeight && outputHeight.value && outputWidth.value) {
const [oldWidth, oldHeight] = node.size
const scene = node.widgets?.find((w) => w.name === 'image')
const scene = node.widgets?.find((w: IWidget) => w.name === 'image')
const sceneHeight = scene?.computedHeight

View File

@@ -8,7 +8,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useExtensionStore } from '@/stores/extensionStore'
import { ResultItemImpl } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
@@ -17,16 +16,9 @@ const props = defineProps<{
}>()
const settingStore = useSettingStore()
const { isExtensionInstalled, isExtensionEnabled } = useExtensionStore()
const vhsAdvancedPreviews = computed(() => {
return (
isExtensionInstalled('VideoHelperSuite.Core') &&
isExtensionEnabled('VideoHelperSuite.Core') &&
settingStore.get('VHS.AdvancedPreviews') &&
settingStore.get('VHS.AdvancedPreviews') !== 'Never'
)
})
const vhsAdvancedPreviews = computed(() =>
settingStore.get('VHS.AdvancedPreviews')
)
const url = computed(() =>
vhsAdvancedPreviews.value

View File

@@ -2,7 +2,7 @@
<template>
<div>
<Button
v-if="isLoggedIn"
v-if="isAuthenticated"
class="user-profile-button p-1"
severity="secondary"
text
@@ -30,14 +30,15 @@ import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import CurrentUserPopover from './CurrentUserPopover.vue'
const { isLoggedIn, userPhotoUrl } = useCurrentUser()
const authStore = useFirebaseAuthStore()
const popover = ref<InstanceType<typeof Popover> | null>(null)
const isAuthenticated = computed(() => authStore.isAuthenticated)
const photoURL = computed<string | undefined>(
() => userPhotoUrl.value ?? undefined
() => authStore.currentUser?.photoURL ?? undefined
)
</script>

View File

@@ -6,19 +6,19 @@
<div class="flex flex-col items-center">
<UserAvatar
class="mb-3"
:photo-url="userPhotoUrl"
:photo-url="user?.photoURL"
:pt:icon:class="{
'!text-2xl': !userPhotoUrl
'!text-2xl': !user?.photoURL
}"
size="large"
/>
<!-- User Details -->
<h3 class="text-lg font-semibold truncate my-0 mb-1">
{{ userDisplayName || $t('g.user') }}
{{ user?.displayName || $t('g.user') }}
</h3>
<p v-if="userEmail" class="text-sm text-muted truncate my-0">
{{ userEmail }}
<p v-if="user?.email" class="text-sm text-muted truncate my-0">
{{ user.email }}
</p>
</div>
</div>
@@ -37,18 +37,6 @@
<Divider class="my-2" />
<Button
class="justify-start"
:label="$t('credits.apiPricing')"
icon="pi pi-external-link"
text
fluid
severity="secondary"
@click="handleOpenApiPricing"
/>
<Divider class="my-2" />
<div class="w-full flex flex-col gap-2 p-2">
<div class="text-muted text-sm">
{{ $t('credits.yourCreditBalance') }}
@@ -64,18 +52,20 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { onMounted } from 'vue'
import { computed, onMounted } from 'vue'
import UserAvatar from '@/components/common/UserAvatar.vue'
import UserCredit from '@/components/common/UserCredit.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
const authStore = useFirebaseAuthStore()
const authService = useFirebaseAuthService()
const dialogService = useDialogService()
const user = computed(() => authStore.currentUser)
const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user')
}
@@ -84,10 +74,6 @@ const handleTopUp = () => {
dialogService.showTopUpCreditsDialog()
}
const handleOpenApiPricing = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank')
}
onMounted(() => {
void authService.fetchBalance()
})

View File

@@ -1,101 +0,0 @@
import { computed } from 'vue'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
export const useCurrentUser = () => {
const authStore = useFirebaseAuthStore()
const commandStore = useCommandStore()
const apiKeyStore = useApiKeyAuthStore()
const firebaseUser = computed(() => authStore.currentUser)
const isApiKeyLogin = computed(() => apiKeyStore.isAuthenticated)
const isLoggedIn = computed(
() => !!isApiKeyLogin.value || firebaseUser.value !== null
)
const userDisplayName = computed(() => {
if (isApiKeyLogin.value) {
return apiKeyStore.currentUser?.name
}
return firebaseUser.value?.displayName
})
const userEmail = computed(() => {
if (isApiKeyLogin.value) {
return apiKeyStore.currentUser?.email
}
return firebaseUser.value?.email
})
const providerName = computed(() => {
if (isApiKeyLogin.value) {
return 'Comfy API Key'
}
const providerId = firebaseUser.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'Google'
}
if (providerId?.includes('github')) {
return 'GitHub'
}
return providerId
})
const providerIcon = computed(() => {
if (isApiKeyLogin.value) {
return 'pi pi-key'
}
const providerId = firebaseUser.value?.providerData[0]?.providerId
if (providerId?.includes('google')) {
return 'pi pi-google'
}
if (providerId?.includes('github')) {
return 'pi pi-github'
}
return 'pi pi-user'
})
const isEmailProvider = computed(() => {
if (isApiKeyLogin.value) {
return false
}
const providerId = firebaseUser.value?.providerData[0]?.providerId
return providerId === 'password'
})
const userPhotoUrl = computed(() => {
if (isApiKeyLogin.value) return null
return firebaseUser.value?.photoURL
})
const handleSignOut = async () => {
if (isApiKeyLogin.value) {
await apiKeyStore.clearStoredApiKey()
} else {
await commandStore.execute('Comfy.User.SignOut')
}
}
const handleSignIn = async () => {
await commandStore.execute('Comfy.User.OpenSignInDialog')
}
return {
loading: authStore.loading,
isLoggedIn,
isApiKeyLogin,
isEmailProvider,
userDisplayName,
userEmail,
userPhotoUrl,
providerName,
providerIcon,
handleSignOut,
handleSignIn
}
}

View File

@@ -1,53 +0,0 @@
import { LGraphNode } from '@comfyorg/litegraph'
import { useTextPreviewWidget } from '@/composables/widgets/useProgressTextWidget'
const TEXT_PREVIEW_WIDGET_NAME = '$$node-text-preview'
/**
* Composable for handling node text previews
*/
export function useNodeProgressText() {
const textPreviewWidget = useTextPreviewWidget()
const findTextPreviewWidget = (node: LGraphNode) =>
node.widgets?.find((w) => w.name === TEXT_PREVIEW_WIDGET_NAME)
const addTextPreviewWidget = (node: LGraphNode) =>
textPreviewWidget(node, {
name: TEXT_PREVIEW_WIDGET_NAME,
type: 'progressText'
})
/**
* Shows text preview for a node
* @param node The graph node to show the preview for
*/
function showTextPreview(node: LGraphNode, text: string) {
const widget = findTextPreviewWidget(node) ?? addTextPreviewWidget(node)
widget.value = text
node.setDirtyCanvas?.(true)
}
/**
* Removes text preview from a node
* @param node The graph node to remove the preview from
*/
function removeTextPreview(node: LGraphNode) {
if (!node.widgets) return
const widgetIdx = node.widgets.findIndex(
(w) => w.name === TEXT_PREVIEW_WIDGET_NAME
)
if (widgetIdx > -1) {
node.widgets[widgetIdx].onRemove?.()
node.widgets.splice(widgetIdx, 1)
}
}
return {
showTextPreview,
removeTextPreview
}
}

View File

@@ -7,14 +7,13 @@ import {
} from 'vue'
import { useI18n } from 'vue-i18n'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { SettingTreeNode, useSettingStore } from '@/stores/settingStore'
import type { SettingParams } from '@/types/settingTypes'
import { isElectron } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { buildTree } from '@/utils/treeUtil'
import { useCurrentUser } from '../auth/useCurrentUser'
interface SettingPanelItem {
node: SettingTreeNode
component: Component
@@ -30,7 +29,7 @@ export function useSettingUI(
| 'credits'
) {
const { t } = useI18n()
const { isLoggedIn } = useCurrentUser()
const firebaseAuthStore = useFirebaseAuthStore()
const settingStore = useSettingStore()
const activeCategory = ref<SettingTreeNode | null>(null)
@@ -166,7 +165,7 @@ export function useSettingUI(
label: 'Account',
children: [
userPanel.node,
...(isLoggedIn.value ? [creditsPanel.node] : [])
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : [])
].map(translateCategory)
},
// Normal settings stored in the settingStore

View File

@@ -312,28 +312,6 @@ export function useCoreCommands(): ComfyCommand[] {
await app.queuePrompt(-1, batchCount)
}
},
{
id: 'Comfy.QueueSelectedOutputNodes',
icon: 'pi pi-play',
label: 'Queue Selected Output Nodes',
versionAdded: '1.19.6',
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
const queueNodeIds = getSelectedNodes()
.filter((node) => node.constructor.nodeData?.output_node)
.map((node) => node.id)
if (queueNodeIds.length === 0) {
toastStore.add({
severity: 'error',
summary: t('toastMessages.nothingToQueue'),
detail: t('toastMessages.pleaseSelectOutputNodes'),
life: 3000
})
return
}
await app.queuePrompt(0, batchCount, queueNodeIds)
}
},
{
id: 'Comfy.ShowSettingsDialog',
icon: 'pi pi-cog',

View File

@@ -128,10 +128,4 @@ export const useLitegraphSettings = () => {
'LiteGraph.Pointer.TrackpadGestures'
)
})
watchEffect(() => {
LiteGraph.saveViewportWithGraph = settingStore.get(
'Comfy.EnableWorkflowViewRestore'
)
})
}

View File

@@ -1,5 +1,4 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
import { computed, ref, watchEffect } from 'vue'
import { useCanvasStore } from '@/stores/graphStore'
@@ -9,11 +8,9 @@ interface RefreshableItem {
refresh: () => Promise<void> | void
}
type RefreshableWidget = IBaseWidget & RefreshableItem
type RefreshableWidget = IWidget & RefreshableItem
const isRefreshableWidget = (
widget: IBaseWidget
): widget is RefreshableWidget =>
const isRefreshableWidget = (widget: IWidget): widget is RefreshableWidget =>
'refresh' in widget && typeof widget.refresh === 'function'
/**

View File

@@ -3,10 +3,14 @@ import type { ISerialisedGraph } from '@comfyorg/litegraph/dist/types/serialisat
import type { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
import { validateComfyWorkflow } from '@/schemas/comfyWorkflowSchema'
import { useToastStore } from '@/stores/toastStore'
import { fixBadLinks } from '@/utils/linkFixer'
import { WorkflowLinkFixer } from '@/utils/linkFixer'
export interface ValidationResult {
graphData: ComfyWorkflowJSON | null
linksFixes?: {
patched: number
deleted: number
}
}
export function useWorkflowValidation() {
@@ -15,26 +19,26 @@ export function useWorkflowValidation() {
function tryFixLinks(
graphData: ComfyWorkflowJSON,
options: { silent?: boolean } = {}
) {
): ComfyWorkflowJSON | null {
const { silent = false } = options
// Collect all logs in an array
const logs: string[] = []
// Then validate and fix links if schema validation passed
const linkValidation = fixBadLinks(
graphData as unknown as ISerialisedGraph,
{
fix: true,
silent,
logger: {
log: (message: string) => {
logs.push(message)
}
}
}
const fixer = WorkflowLinkFixer.create(
graphData as unknown as ISerialisedGraph
)
fixer.logger = {
log: (...args: any[]) => {
logs.push(...args)
}
}
const checkBadLinksResult = fixer.check()
// Graph has no bad links, so we can return it as is
if (!checkBadLinksResult.hasBadLinks) {
return graphData
}
if (!silent && logs.length > 0) {
if (!silent) {
toastStore.add({
severity: 'warn',
summary: 'Workflow Validation',
@@ -42,18 +46,17 @@ export function useWorkflowValidation() {
})
}
// If links were fixed, notify the user
if (linkValidation.fixed) {
const fixBadLinksResult = fixer.fix()
if (!fixBadLinksResult.hasBadLinks) {
if (!silent) {
toastStore.add({
severity: 'success',
summary: 'Workflow Links Fixed',
detail: `Fixed ${linkValidation.patched} node connections and removed ${linkValidation.deleted} invalid links.`
summary: 'Workflow Links Fixed'
})
}
return fixBadLinksResult.graph as unknown as ComfyWorkflowJSON
}
return linkValidation.graph as unknown as ComfyWorkflowJSON
return null
}
/**
@@ -83,7 +86,7 @@ export function useWorkflowValidation() {
try {
validatedData = tryFixLinks(validatedGraphData, { silent })
} catch (err) {
// Link fixer itself is throwing an error
// Link fixer itself is throwing an error. Log it and return the original graph
console.error(err)
}
}

View File

@@ -50,8 +50,7 @@ export const useFloatWidget = () => {
Math.max(0, -Math.floor(Math.log10(step)))
const enableRounding = !settingStore.get('Comfy.DisableFloatRounding')
/** Assertion {@link inputSpec.default} */
const defaultValue = (inputSpec.default as number | undefined) ?? 0
const defaultValue = inputSpec.default ?? 0
return node.addWidget(
widgetType,
inputSpec.name,

View File

@@ -55,8 +55,7 @@ export const useIntWidget = () => {
: 'number'
const step = inputSpec.step ?? 1
/** Assertion {@link inputSpec.default} */
const defaultValue = (inputSpec.default as number | undefined) ?? 0
const defaultValue = inputSpec.default ?? 0
const widget = node.addWidget(
widgetType,
inputSpec.name,

View File

@@ -1,35 +0,0 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import { ref } from 'vue'
import TextPreviewWidget from '@/components/graph/widgets/TextPreviewWidget.vue'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
const PADDING = 16
export const useTextPreviewWidget = () => {
const widgetConstructor: ComfyWidgetConstructorV2 = (
node: LGraphNode,
inputSpec: InputSpec
) => {
const widgetValue = ref<string>('')
const widget = new ComponentWidgetImpl<string | object>({
node,
name: inputSpec.name,
component: TextPreviewWidget,
inputSpec,
options: {
getValue: () => widgetValue.value,
setValue: (value: string | object) => {
widgetValue.value = typeof value === 'string' ? value : String(value)
},
getMinHeight: () => 42 + PADDING
}
})
addWidget(node, widget)
return widget
}
return widgetConstructor
}

View File

@@ -1,7 +1,3 @@
export const COMFY_API_BASE_URL = __USE_PROD_CONFIG__
? 'https://api.comfy.org'
: 'https://stagingapi.comfy.org'
export const COMFY_PLATFORM_BASE_URL = __USE_PROD_CONFIG__
? 'https://platform.comfy.org'
: 'https://stagingplatform.comfy.org'

View File

@@ -1,4 +1,4 @@
import { LGraphCanvas, LiteGraph, isComboWidget } from '@comfyorg/litegraph'
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
import { app } from '../../scripts/app'
@@ -33,10 +33,10 @@ const ext = {
const clickedComboValue = currentNode?.widgets
?.filter(
(w) =>
isComboWidget(w) && w.options.values?.length === values.length
w.type === 'combo' && w.options.values?.length === values.length
)
.find((w) =>
// @ts-expect-error Poorly typed; filter above "should" mitigate exceptions
// @ts-ignore Poorly typed; filter above "should" mitigate exceptions
w.options.values?.every((v, i) => v === values[i])
)?.value

View File

@@ -797,7 +797,7 @@ export class GroupNodeConfig {
export class GroupNodeHandler {
node: LGraphNode
groupData: any
groupData
innerNodes: any
constructor(node: LGraphNode) {
@@ -1231,6 +1231,7 @@ export class GroupNodeHandler {
const widgetName = self.groupData.oldToNewWidgetMap[n][w]
const widget = this.widgets.find((w) => w.name === widgetName)
if (widget) {
// @ts-expect-error fixme ts strict error
widget.type = 'hidden'
widget.computeSize = () => [0, -4]
}

View File

@@ -1,7 +1,5 @@
import type {
IComboWidget,
IStringWidget
} from '@comfyorg/litegraph/dist/types/widgets'
import { IWidget } from '@comfyorg/litegraph'
import { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
@@ -118,8 +116,8 @@ useExtensionService().registerExtension({
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_file'
) as IComboWidget & { options: { values: string[] } }
(w: IWidget) => w.name === 'model_file'
) as IStringWidget
node.properties['Texture'] = undefined
@@ -141,6 +139,7 @@ useExtensionService().registerExtension({
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
// @ts-ignore Fails due to earlier type-assertion of IStringWidget
modelWidget.options?.values?.push(uploadPath)
}
@@ -156,7 +155,9 @@ useExtensionService().registerExtension({
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
if (modelWidget) {
modelWidget.value = ''
@@ -202,10 +203,12 @@ useExtensionService().registerExtension({
const config = new Load3DConfiguration(load3d)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
const width = node.widgets?.find((w: IWidget) => w.name === 'width')
const height = node.widgets?.find((w: IWidget) => w.name === 'height')
const sceneWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
if (modelWidget && width && height && sceneWidget) {
config.configure('input', modelWidget, cameraState, width, height)
@@ -273,7 +276,7 @@ useExtensionService().registerExtension({
fileInput.onchange = async () => {
if (fileInput.files?.length) {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model_file'
(w: IWidget) => w.name === 'model_file'
) as IStringWidget
const uploadPath = await Load3dUtils.uploadFile(
@@ -294,6 +297,7 @@ useExtensionService().registerExtension({
if (uploadPath && modelWidget) {
if (!modelWidget.options?.values?.includes(uploadPath)) {
// @ts-ignore Fails due to earlier type-assertion of IStringWidget
modelWidget.options?.values?.push(uploadPath)
}
@@ -309,7 +313,9 @@ useExtensionService().registerExtension({
node.addWidget('button', 'clear', 'clear', () => {
useLoad3dService().getLoad3d(node)?.clearModel()
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
if (modelWidget) {
modelWidget.value = ''
}
@@ -346,16 +352,18 @@ useExtensionService().registerExtension({
await nextTick()
const sceneWidget = node.widgets?.find((w) => w.name === 'image')
const sceneWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
const load3d = useLoad3dService().getLoad3d(node) as Load3dAnimation
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
let cameraState = node.properties['Camera Info']
const width = node.widgets?.find((w) => w.name === 'width')
const height = node.widgets?.find((w) => w.name === 'height')
const width = node.widgets?.find((w: IWidget) => w.name === 'width')
const height = node.widgets?.find((w: IWidget) => w.name === 'height')
if (modelWidget && width && height && sceneWidget && load3d) {
const config = new Load3DConfiguration(load3d)
@@ -471,7 +479,9 @@ useExtensionService().registerExtension({
let cameraState = message.result[1]
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
if (load3d && modelWidget) {
modelWidget.value = filePath.replaceAll('\\', '/')
@@ -545,7 +555,9 @@ useExtensionService().registerExtension({
const load3d = useLoad3dService().getLoad3d(node)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')
const modelWidget = node.widgets?.find(
(w: IWidget) => w.name === 'model_file'
)
if (load3d && modelWidget) {
modelWidget.value = filePath.replaceAll('\\', '/')

View File

@@ -1,4 +1,4 @@
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
import type { IWidget } from '@comfyorg/litegraph'
import Load3d from '@/extensions/core/load3d/Load3d'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
@@ -15,20 +15,17 @@ class Load3DConfiguration {
configure(
loadFolder: 'input' | 'output',
modelWidget: IBaseWidget,
modelWidget: IWidget,
cameraState?: any,
width: IBaseWidget | null = null,
height: IBaseWidget | null = null
width: IWidget | null = null,
height: IWidget | null = null
) {
this.setupModelHandling(modelWidget, loadFolder, cameraState)
this.setupTargetSize(width, height)
this.setupDefaultProperties()
}
private setupTargetSize(
width: IBaseWidget | null,
height: IBaseWidget | null
) {
private setupTargetSize(width: IWidget | null, height: IWidget | null) {
if (width && height) {
this.load3d.setTargetSize(width.value as number, height.value as number)
@@ -54,7 +51,7 @@ class Load3DConfiguration {
}
private setupModelHandling(
modelWidget: IBaseWidget,
modelWidget: IWidget,
loadFolder: 'input' | 'output',
cameraState?: any
) {

View File

@@ -401,7 +401,6 @@ app.registerExtension({
// @ts-expect-error
data.groupNodes = {}
}
if (nodeData == null) throw new TypeError('nodeData is not set')
// @ts-expect-error
data.groupNodes[nodeData.name] = groupData
// @ts-expect-error

View File

@@ -3,6 +3,8 @@ Preview Any - original implement from
https://github.com/rgthree/rgthree-comfy/blob/main/py/display_any.py
upstream requested in https://github.com/Kosinkadink/rfcs/blob/main/rfcs/0000-corenodes.md#preview-nodes
*/
import { IWidget } from '@comfyorg/litegraph'
import { DOMWidget } from '@/scripts/domWidget'
import { ComfyWidgets } from '@/scripts/widgets'
import { useExtensionService } from '@/services/extensionService'
@@ -35,7 +37,9 @@ useExtensionService().registerExtension({
? void 0
: onExecuted.apply(this, [message])
const previewWidget = this.widgets?.find((w) => w.name === 'preview')
const previewWidget = this.widgets?.find(
(w: IWidget) => w.name === 'preview'
)
if (previewWidget) {
previewWidget.value = message.text[0]

View File

@@ -1,3 +1,4 @@
import { IWidget } from '@comfyorg/litegraph'
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
@@ -60,7 +61,7 @@ useExtensionService().registerExtension({
const load3d = useLoad3dService().getLoad3d(node)
const modelWidget = node.widgets?.find((w) => w.name === 'image')
const modelWidget = node.widgets?.find((w: IWidget) => w.name === 'image')
if (load3d && modelWidget) {
const filePath = fileInfo['subfolder'] + '/' + fileInfo['filename']

View File

@@ -1,4 +1,4 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
@@ -90,13 +90,7 @@ app.registerExtension({
name: 'Comfy.AudioWidget',
async beforeRegisterNodeDef(nodeType, nodeData) {
if (
[
'LoadAudio',
'SaveAudio',
'PreviewAudio',
'SaveAudioMP3',
'SaveAudioOpus'
].includes(
['LoadAudio', 'SaveAudio', 'PreviewAudio'].includes(
// @ts-expect-error fixme ts strict error
nodeType.prototype.comfyClass
)
@@ -117,10 +111,7 @@ app.registerExtension({
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
audioUIWidget.serialize = false
const { nodeData } = node.constructor
if (nodeData == null) throw new TypeError('nodeData is null')
const isOutputNode = nodeData.output_node
const isOutputNode = node.constructor.nodeData.output_node
if (isOutputNode) {
// Hide the audio widget when there is no audio initially.
audioUIWidget.element.classList.add('empty-audio-widget')
@@ -173,11 +164,11 @@ app.registerExtension({
// The widget that allows user to select file.
// @ts-expect-error fixme ts strict error
const audioWidget = node.widgets.find(
(w) => w.name === 'audio'
(w: IWidget) => w.name === 'audio'
) as IStringWidget
// @ts-expect-error fixme ts strict error
const audioUIWidget = node.widgets.find(
(w) => w.name === 'audioUI'
(w: IWidget) => w.name === 'audioUI'
) as unknown as DOMWidget<HTMLAudioElement, string>
const onAudioWidgetUpdate = () => {

View File

@@ -3,11 +3,11 @@ import type {
INodeInputSlot,
INodeOutputSlot,
ISlotType,
IWidget,
LLink,
Vector2
} from '@comfyorg/litegraph'
import type { CanvasMouseEvent } from '@comfyorg/litegraph/dist/types/events'
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
import {
type CallbackParams,
@@ -226,7 +226,7 @@ export class PrimitiveNode extends LGraphNode {
// Store current size as addWidget resizes the node
const [oldWidth, oldHeight] = this.size
let widget: IBaseWidget | undefined
let widget: IWidget | undefined
if (type in ComfyWidgets) {
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
} else {
@@ -426,7 +426,7 @@ function getConfig(this: LGraphNode, widgetName: string) {
*/
export function convertToInput(
node: LGraphNode,
widget: IBaseWidget
widget: IWidget
): INodeInputSlot | undefined {
console.warn(
'Please remove call to convertToInput. Widget to socket conversion is no longer necessary, as they co-exist now.'
@@ -506,7 +506,8 @@ export function mergeIfValid(
}
}
return { customConfig: customSpec?.[1] ?? {} }
// @ts-expect-error fixme ts strict error
return { customConfig: customSpec[1] }
}
app.registerExtension({

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "Queue Prompt (Front)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Queue Selected Output Nodes"
},
"Comfy_Redo": {
"label": "Redo"
},

View File

@@ -803,7 +803,6 @@
"Open": "Open",
"Queue Prompt": "Queue Prompt",
"Queue Prompt (Front)": "Queue Prompt (Front)",
"Queue Selected Output Nodes": "Queue Selected Output Nodes",
"Redo": "Redo",
"Refresh Node Definitions": "Refresh Node Definitions",
"Save": "Save",
@@ -1022,7 +1021,6 @@
"image": "image",
"preprocessors": "preprocessors",
"advanced": "advanced",
"guidance": "guidance",
"loaders": "loaders",
"model_merging": "model_merging",
"attention_experiments": "attention_experiments",
@@ -1035,64 +1033,44 @@
"inpaint": "inpaint",
"scheduling": "scheduling",
"create": "create",
"video": "video",
"mask": "mask",
"deprecated": "deprecated",
"latent": "latent",
"video": "video",
"audio": "audio",
"3d": "3d",
"ltxv": "ltxv",
"sd3": "sd3",
"sigmas": "sigmas",
"api node": "api node",
"BFL": "BFL",
"model_patches": "model_patches",
"unet": "unet",
"gligen": "gligen",
"video_models": "video_models",
"Ideogram": "Ideogram",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"postprocessing": "postprocessing",
"transform": "transform",
"batch": "batch",
"upscaling": "upscaling",
"instructpix2pix": "instructpix2pix",
"compositing": "compositing",
"Kling": "Kling",
"samplers": "samplers",
"operations": "operations",
"lotus": "lotus",
"Luma": "Luma",
"MiniMax": "MiniMax",
"debug": "debug",
"model": "model",
"model_specific": "model_specific",
"OpenAI": "OpenAI",
"cond pair": "cond pair",
"photomaker": "photomaker",
"Pika": "Pika",
"PixVerse": "PixVerse",
"utils": "utils",
"primitive": "primitive",
"Recraft": "Recraft",
"animation": "animation",
"api": "api",
"upscale_diffusion": "upscale_diffusion",
"clip": "clip",
"Stability AI": "Stability AI",
"guidance": "guidance",
"stable_cascade": "stable_cascade",
"3d_models": "3d_models",
"style_model": "style_model",
"sd": "sd",
"Veo": "Veo"
"style_model": "style_model"
},
"dataTypes": {
"*": "*",
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEAN",
"CAMERA_CONTROL": "CAMERA_CONTROL",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
@@ -1111,27 +1089,18 @@
"LATENT_OPERATION": "LATENT_OPERATION",
"LOAD_3D": "LOAD_3D",
"LOAD_3D_ANIMATION": "LOAD_3D_ANIMATION",
"LOAD3D_CAMERA": "LOAD3D_CAMERA",
"LUMA_CONCEPTS": "LUMA_CONCEPTS",
"LUMA_REF": "LUMA_REF",
"MASK": "MASK",
"MESH": "MESH",
"MODEL": "MODEL",
"NOISE": "NOISE",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "PIXVERSE_TEMPLATE",
"RECRAFT_COLOR": "RECRAFT_COLOR",
"RECRAFT_CONTROLS": "RECRAFT_CONTROLS",
"RECRAFT_V3_STYLE": "RECRAFT_V3_STYLE",
"SAMPLER": "SAMPLER",
"SIGMAS": "SIGMAS",
"STRING": "STRING",
"STYLE_MODEL": "STYLE_MODEL",
"SVG": "SVG",
"TIMESTEPS_RANGE": "TIMESTEPS_RANGE",
"UPSCALE_MODEL": "UPSCALE_MODEL",
"VAE": "VAE",
"VIDEO": "VIDEO",
"VOXEL": "VOXEL",
"WEBCAM": "WEBCAM"
},
@@ -1228,8 +1197,6 @@
"resizeNodeMatchOutput": "Resize Node to match output"
},
"toastMessages": {
"nothingToQueue": "Nothing to queue",
"pleaseSelectOutputNodes": "Please select output nodes",
"no3dScene": "No 3D scene to apply texture",
"failedToApplyTexture": "Failed to apply texture",
"no3dSceneToExport": "No 3D scene to export",
@@ -1262,27 +1229,8 @@
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist."
},
"auth": {
"apiKey": {
"title": "API Key",
"label": "API Key",
"description": "Use your Comfy API key to enable API Nodes",
"placeholder": "Enter your API Key",
"error": "Invalid API Key",
"storageFailed": "Failed to store API Key",
"storageFailedDetail": "Please try again.",
"stored": "API Key stored",
"storedDetail": "Your API Key has been stored successfully",
"cleared": "API Key cleared",
"clearedDetail": "Your API Key has been cleared successfully",
"invalid": "Invalid API Key",
"invalidDetail": "Please enter a valid API Key",
"helpText": "Need an API key?",
"generateKey": "Get one here",
"whitelistInfo": "About non-whitelisted sites"
},
"login": {
"title": "Log in to your account",
"useApiKey": "Comfy API Key",
"signInOrSignUp": "Sign In / Sign Up",
"forgotPasswordError": "Failed to send password reset email",
"passwordResetSent": "Password reset email sent",
@@ -1308,8 +1256,7 @@
"success": "Login successful",
"failed": "Login failed",
"insecureContextWarning": "This connection is insecure (HTTP) - your credentials may be intercepted by attackers if you proceed to login.",
"questionsContactPrefix": "Questions? Contact us at",
"noAssociatedUser": "There is no Comfy user associated with the provided API key"
"questionsContactPrefix": "Questions? Contact us at"
},
"signup": {
"title": "Create an account",
@@ -1340,8 +1287,6 @@
"required": "Required",
"minLength": "Must be at least {length} characters",
"maxLength": "Must be no more than {length} characters",
"prefix": "Must start with {prefix}",
"length": "Must be {length} characters",
"password": {
"requirements": "Password requirements",
"minLength": "Must be between 8 and 32 characters",
@@ -1358,7 +1303,6 @@
"yourCreditBalance": "Your credit balance",
"purchaseCredits": "Purchase Credits",
"invoiceHistory": "Invoice History",
"apiPricing": "API Pricing",
"faqs": "FAQs",
"messageSupport": "Message Support",
"lastUpdated": "Last updated",
@@ -1399,11 +1343,5 @@
"title": "Run!"
}
}
},
"selectionToolbox": {
"executeButton": {
"tooltip": "Execute to selected output nodes (Highlighted with orange border)",
"disabledTooltip": "No output nodes selected"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "Prompt de Cola (Frente)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Encolar nodos de salida seleccionados"
},
"Comfy_Redo": {
"label": "Rehacer"
},

View File

@@ -29,24 +29,6 @@
"title": "Se requiere iniciar sesión para usar los nodos de API"
},
"auth": {
"apiKey": {
"cleared": "Clave API eliminada",
"clearedDetail": "Tu clave API se ha eliminado correctamente",
"description": "Usa tu clave API de Comfy para habilitar los nodos de API",
"error": "Clave API no válida",
"generateKey": "Consíguela aquí",
"helpText": "¿Necesitas una clave API?",
"invalid": "Clave API no válida",
"invalidDetail": "Por favor, introduce una clave API válida",
"label": "Clave API",
"placeholder": "Introduce tu clave API",
"storageFailed": "No se pudo guardar la clave API",
"storageFailedDetail": "Por favor, inténtalo de nuevo.",
"stored": "Clave API guardada",
"storedDetail": "Tu clave API se ha guardado correctamente",
"title": "Clave API",
"whitelistInfo": "Acerca de los sitios no incluidos en la lista blanca"
},
"login": {
"andText": "y",
"confirmPasswordLabel": "Confirmar contraseña",
@@ -61,7 +43,6 @@
"loginWithGithub": "Iniciar sesión con Github",
"loginWithGoogle": "Iniciar sesión con Google",
"newUser": "¿Eres nuevo aquí?",
"noAssociatedUser": "No hay ningún usuario de Comfy asociado con la clave API proporcionada",
"orContinueWith": "O continuar con",
"passwordLabel": "Contraseña",
"passwordPlaceholder": "Ingresa tu contraseña",
@@ -75,7 +56,6 @@
"termsLink": "Términos de uso",
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
"title": "Inicia sesión en tu cuenta",
"useApiKey": "Clave API de Comfy",
"userAvatar": "Avatar de usuario"
},
"passwordUpdate": {
@@ -151,7 +131,6 @@
"Unpin": "Desanclar"
},
"credits": {
"apiPricing": "Precios de la API",
"credits": "Créditos",
"faqs": "Preguntas frecuentes",
"invoiceHistory": "Historial de facturas",
@@ -170,10 +149,8 @@
"yourCreditBalance": "Tu saldo de créditos"
},
"dataTypes": {
"*": "*",
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEANO",
"CAMERA_CONTROL": "CONTROL DE CÁMARA",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "SALIDA_CLIP_VISION",
@@ -190,29 +167,20 @@
"INT": "ENTERO",
"LATENT": "LATENTE",
"LATENT_OPERATION": "OPERACIÓN_LATENTE",
"LOAD3D_CAMERA": "CARGAR CÁMARA 3D",
"LOAD_3D": "CARGAR_3D",
"LOAD_3D_ANIMATION": "CARGAR_ANIMACIÓN_3D",
"LUMA_CONCEPTS": "CONCEPTOS LUMA",
"LUMA_REF": "REFERENCIA LUMA",
"MASK": "MASK",
"MESH": "MALLA",
"MODEL": "MODELO",
"NOISE": "RUIDO",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "PLANTILLA PIXVERSE",
"RECRAFT_COLOR": "COLOR RECRAFT",
"RECRAFT_CONTROLS": "CONTROLES RECRAFT",
"RECRAFT_V3_STYLE": "ESTILO RECRAFT V3",
"SAMPLER": "MUESTREADOR",
"SIGMAS": "SIGMAS",
"STRING": "CADENA",
"STYLE_MODEL": "MODELO_DE_ESTILO",
"SVG": "SVG",
"TIMESTEPS_RANGE": "RANGO_DE_PASOS_DE_TIEMPO",
"UPSCALE_MODEL": "MODELO_DE_ESCALADO",
"VAE": "VAE",
"VIDEO": "VÍDEO",
"VOXEL": "VOXEL",
"WEBCAM": "WEBCAM"
},
@@ -727,7 +695,6 @@
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
"Queue Prompt": "Indicador de cola",
"Queue Prompt (Front)": "Indicador de cola (Frente)",
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
"Quit": "Salir",
"Redo": "Rehacer",
"Refresh Node Definitions": "Actualizar definiciones de nodo",
@@ -764,22 +731,10 @@
"nodeCategories": {
"3d": "3d",
"3d_models": "modelos_3d",
"BFL": "BFL",
"Ideogram": "Ideogram",
"Kling": "Kling",
"Luma": "Luma",
"MiniMax": "MiniMax",
"OpenAI": "OpenAI",
"Pika": "Pika",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Stability AI": "Stability AI",
"Veo": "Veo",
"_for_testing": "_para_pruebas",
"advanced": "avanzado",
"animation": "animación",
"api": "api",
"api node": "nodo api",
"attention_experiments": "experimentos_de_atención",
"audio": "audio",
"batch": "lote",
@@ -804,7 +759,6 @@
"instructpix2pix": "instruirpix2pix",
"latent": "latent",
"loaders": "cargadores",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "mask",
"model": "modelo",
@@ -816,12 +770,10 @@
"photomaker": "photomaker",
"postprocessing": "postprocesamiento",
"preprocessors": "preprocesadores",
"primitive": "primitivo",
"samplers": "muestreadores",
"sampling": "muestreo",
"schedulers": "programadores",
"scheduling": "programación",
"sd": "sd",
"sd3": "sd3",
"sigmas": "sigmas",
"stable_cascade": "stable_cascade",
@@ -830,10 +782,6 @@
"unet": "unet",
"upscale_diffusion": "difusión_de_escalado",
"upscaling": "escalado",
"utils": "utilidades",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "video",
"video_models": "modelos_de_video"
},
@@ -853,12 +801,6 @@
},
"title": "Tu dispositivo no es compatible"
},
"selectionToolbox": {
"executeButton": {
"disabledTooltip": "No hay nodos de salida seleccionados",
"tooltip": "Ejecutar en los nodos de salida seleccionados (resaltados con borde naranja)"
}
},
"serverConfig": {
"modifiedConfigs": "Has modificado las siguientes configuraciones del servidor. Reinicia para aplicar los cambios.",
"restart": "Reiniciar",
@@ -1355,10 +1297,8 @@
"noTemplatesToExport": "No hay plantillas para exportar",
"nodeDefinitionsUpdated": "Definiciones de nodos actualizadas",
"nothingToGroup": "Nada para agrupar",
"nothingToQueue": "Nada para poner en cola",
"pendingTasksDeleted": "Tareas pendientes eliminadas",
"pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para",
"pleaseSelectOutputNodes": "Por favor, selecciona los nodos de salida",
"unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo",
"unauthorizedDomain": "Tu dominio {domain} no está autorizado para usar este servicio. Por favor, contacta a {email} para agregar tu dominio a la lista blanca.",
"updateRequested": "Actualización solicitada",
@@ -1381,7 +1321,6 @@
},
"validation": {
"invalidEmail": "Dirección de correo electrónico inválida",
"length": "Debe tener {length} caracteres",
"maxLength": "No debe tener más de {length} caracteres",
"minLength": "Debe tener al menos {length} caracteres",
"password": {
@@ -1394,7 +1333,6 @@
"uppercase": "Debe contener al menos una letra mayúscula"
},
"personalDataConsentRequired": "Debes aceptar el procesamiento de tus datos personales.",
"prefix": "Debe comenzar con {prefix}",
"required": "Requerido"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "Invite de file d'attente (avant)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Mettre en file dattente les nœuds de sortie sélectionnés"
},
"Comfy_Redo": {
"label": "Refaire"
},

View File

@@ -29,24 +29,6 @@
"title": "Connexion requise pour utiliser les nœuds API"
},
"auth": {
"apiKey": {
"cleared": "Clé API supprimée",
"clearedDetail": "Votre clé API a été supprimée avec succès",
"description": "Utilisez votre clé API Comfy pour activer les nœuds API",
"error": "Clé API invalide",
"generateKey": "Obtenez-en une ici",
"helpText": "Besoin d'une clé API ?",
"invalid": "Clé API invalide",
"invalidDetail": "Veuillez entrer une clé API valide",
"label": "Clé API",
"placeholder": "Entrez votre clé API",
"storageFailed": "Échec de lenregistrement de la clé API",
"storageFailedDetail": "Veuillez réessayer.",
"stored": "Clé API enregistrée",
"storedDetail": "Votre clé API a été enregistrée avec succès",
"title": "Clé API",
"whitelistInfo": "À propos des sites non autorisés"
},
"login": {
"andText": "et",
"confirmPasswordLabel": "Confirmer le mot de passe",
@@ -61,7 +43,6 @@
"loginWithGithub": "Se connecter avec Github",
"loginWithGoogle": "Se connecter avec Google",
"newUser": "Nouveau ici?",
"noAssociatedUser": "Aucun utilisateur Comfy n'est associé à la clé API fournie",
"orContinueWith": "Ou continuer avec",
"passwordLabel": "Mot de passe",
"passwordPlaceholder": "Entrez votre mot de passe",
@@ -75,7 +56,6 @@
"termsLink": "Conditions d'utilisation",
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
"title": "Connectez-vous à votre compte",
"useApiKey": "Clé API Comfy",
"userAvatar": "Avatar utilisateur"
},
"passwordUpdate": {
@@ -151,7 +131,6 @@
"Unpin": "Désépingler"
},
"credits": {
"apiPricing": "Tarification de lAPI",
"credits": "Crédits",
"faqs": "FAQ",
"invoiceHistory": "Historique des factures",
@@ -170,10 +149,8 @@
"yourCreditBalance": "Votre solde de crédits"
},
"dataTypes": {
"*": "*",
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEAN",
"CAMERA_CONTROL": "Contrôle de la caméra",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "SORTIE_CLIP_VISION",
@@ -190,29 +167,20 @@
"INT": "ENTIER",
"LATENT": "LATENT",
"LATENT_OPERATION": "OPERATION_LATENTE",
"LOAD3D_CAMERA": "Charger la caméra 3D",
"LOAD_3D": "CHARGER_3D",
"LOAD_3D_ANIMATION": "CHARGER_ANIMATION_3D",
"LUMA_CONCEPTS": "Concepts Luma",
"LUMA_REF": "Référence Luma",
"MASK": "MASQUE",
"MESH": "MAILLAGE",
"MODEL": "MODÈLE",
"NOISE": "BRUIT",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "Modèle Pixverse",
"RECRAFT_COLOR": "Couleur Recraft",
"RECRAFT_CONTROLS": "Contrôles Recraft",
"RECRAFT_V3_STYLE": "Style Recraft V3",
"SAMPLER": "ÉCHANTILLONNEUR",
"SIGMAS": "SIGMAS",
"STRING": "CHAÎNE",
"STYLE_MODEL": "MODÈLE_DE_STYLE",
"SVG": "SVG",
"TIMESTEPS_RANGE": "PLAGE_DES_ÉTAPES_TEMPORELLES",
"UPSCALE_MODEL": "MODÈLE_DE_MISE_À_L'ÉCHELLE",
"VAE": "VAE",
"VIDEO": "Vidéo",
"VOXEL": "VOXEL",
"WEBCAM": "WEBCAM"
},
@@ -727,7 +695,6 @@
"Previous Opened Workflow": "Flux de travail ouvert précédent",
"Queue Prompt": "Invite de file d'attente",
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
"Queue Selected Output Nodes": "Mettre en file dattente les nœuds de sortie sélectionnés",
"Quit": "Quitter",
"Redo": "Refaire",
"Refresh Node Definitions": "Actualiser les définitions de nœud",
@@ -764,22 +731,10 @@
"nodeCategories": {
"3d": "3d",
"3d_models": "modèles_3d",
"BFL": "BFL",
"Ideogram": "Ideogram",
"Kling": "Kling",
"Luma": "Luma",
"MiniMax": "MiniMax",
"OpenAI": "OpenAI",
"Pika": "Pika",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Stability AI": "Stability AI",
"Veo": "Veo",
"_for_testing": "_pour_test",
"advanced": "avancé",
"animation": "animation",
"api": "api",
"api node": "nœud api",
"attention_experiments": "expériences_d'attention",
"audio": "audio",
"batch": "lot",
@@ -804,7 +759,6 @@
"instructpix2pix": "instructpix2pix",
"latent": "latent",
"loaders": "chargeurs",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "masque",
"model": "modèle",
@@ -816,12 +770,10 @@
"photomaker": "photomaker",
"postprocessing": "post-traitement",
"preprocessors": "préprocesseurs",
"primitive": "primitif",
"samplers": "échantillonneurs",
"sampling": "échantillonnage",
"schedulers": "planificateurs",
"scheduling": "planification",
"sd": "sd",
"sd3": "sd3",
"sigmas": "sigmas",
"stable_cascade": "stable_cascade",
@@ -830,10 +782,6 @@
"unet": "unet",
"upscale_diffusion": "diffusion_de_mise_à_l'échelle",
"upscaling": "mise_à_l'échelle",
"utils": "utilitaires",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "vidéo",
"video_models": "modèles_vidéo"
},
@@ -853,12 +801,6 @@
},
"title": "Votre appareil n'est pas pris en charge"
},
"selectionToolbox": {
"executeButton": {
"disabledTooltip": "Aucun nœud de sortie sélectionné",
"tooltip": "Exécuter vers les nœuds de sortie sélectionnés (surlignés avec une bordure orange)"
}
},
"serverConfig": {
"modifiedConfigs": "Vous avez modifié les configurations suivantes du serveur. Redémarrez pour appliquer les modifications.",
"restart": "Redémarrer",
@@ -1355,10 +1297,8 @@
"noTemplatesToExport": "Aucun modèle à exporter",
"nodeDefinitionsUpdated": "Définitions de nœuds mises à jour",
"nothingToGroup": "Rien à regrouper",
"nothingToQueue": "Rien à ajouter à la file dattente",
"pendingTasksDeleted": "Tâches en attente supprimées",
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
"pleaseSelectOutputNodes": "Veuillez sélectionner les nœuds de sortie",
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
"unauthorizedDomain": "Votre domaine {domain} n'est pas autorisé à utiliser ce service. Veuillez contacter {email} pour ajouter votre domaine à la liste blanche.",
"updateRequested": "Mise à jour demandée",
@@ -1381,7 +1321,6 @@
},
"validation": {
"invalidEmail": "Adresse e-mail invalide",
"length": "Doit comporter {length} caractères",
"maxLength": "Ne doit pas dépasser {length} caractères",
"minLength": "Doit contenir au moins {length} caractères",
"password": {
@@ -1394,7 +1333,6 @@
"uppercase": "Doit contenir au moins une lettre majuscule"
},
"personalDataConsentRequired": "Vous devez accepter le traitement de vos données personnelles.",
"prefix": "Doit commencer par {prefix}",
"required": "Requis"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "キュープロンプト(フロント)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "選択した出力ノードをキューに追加"
},
"Comfy_Redo": {
"label": "やり直す"
},

View File

@@ -29,24 +29,6 @@
"title": "APIードを使用するためにはサインインが必要です"
},
"auth": {
"apiKey": {
"cleared": "APIキーが削除されました",
"clearedDetail": "APIキーが正常に削除されました",
"description": "Comfy APIキーを使用してAPIードを有効にします",
"error": "無効なAPIキーです",
"generateKey": "こちらから取得",
"helpText": "APIキーが必要ですか",
"invalid": "無効なAPIキーです",
"invalidDetail": "有効なAPIキーを入力してください",
"label": "APIキー",
"placeholder": "APIキーを入力してください",
"storageFailed": "APIキーの保存に失敗しました",
"storageFailedDetail": "もう一度お試しください。",
"stored": "APIキーが保存されました",
"storedDetail": "APIキーが正常に保存されました",
"title": "APIキー",
"whitelistInfo": "ホワイトリストに登録されていないサイトについて"
},
"login": {
"andText": "および",
"confirmPasswordLabel": "パスワードの確認",
@@ -61,7 +43,6 @@
"loginWithGithub": "Githubでログイン",
"loginWithGoogle": "Googleでログイン",
"newUser": "新規ユーザーですか?",
"noAssociatedUser": "指定されたAPIキーに関連付けられたComfyユーザーが存在しません",
"orContinueWith": "または以下で続ける",
"passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力してください",
@@ -75,7 +56,6 @@
"termsLink": "利用規約",
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
"title": "アカウントにログインする",
"useApiKey": "Comfy APIキー",
"userAvatar": "ユーザーアバター"
},
"passwordUpdate": {
@@ -151,7 +131,6 @@
"Unpin": "ピンを解除"
},
"credits": {
"apiPricing": "API料金",
"credits": "クレジット",
"faqs": "よくある質問",
"invoiceHistory": "請求履歴",
@@ -170,10 +149,8 @@
"yourCreditBalance": "あなたのクレジット残高"
},
"dataTypes": {
"*": "*",
"AUDIO": "オーディオ",
"BOOLEAN": "ブール",
"CAMERA_CONTROL": "カメラコントロール",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
@@ -190,29 +167,20 @@
"INT": "整数",
"LATENT": "潜在",
"LATENT_OPERATION": "潜在操作",
"LOAD3D_CAMERA": "3Dカメラの読み込み",
"LOAD_3D": "3Dをロード",
"LOAD_3D_ANIMATION": "3Dアニメーションをロード",
"LUMA_CONCEPTS": "Lumaコンセプト",
"LUMA_REF": "Luma参照",
"MASK": "マスク",
"MESH": "メッシュ",
"MODEL": "モデル",
"NOISE": "ノイズ",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "Pixverseテンプレート",
"RECRAFT_COLOR": "Recraftカラー",
"RECRAFT_CONTROLS": "Recraftコントロール",
"RECRAFT_V3_STYLE": "Recraft V3スタイル",
"SAMPLER": "サンプラー",
"SIGMAS": "シグマ",
"STRING": "文字列",
"STYLE_MODEL": "スタイルモデル",
"SVG": "SVG",
"TIMESTEPS_RANGE": "タイムステップの範囲",
"UPSCALE_MODEL": "アップスケールモデル",
"VAE": "VAE",
"VIDEO": "ビデオ",
"VOXEL": "ボクセル",
"WEBCAM": "ウェブカメラ"
},
@@ -727,7 +695,6 @@
"Previous Opened Workflow": "前に開いたワークフロー",
"Queue Prompt": "キューのプロンプト",
"Queue Prompt (Front)": "キューのプロンプト (前面)",
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
"Quit": "終了",
"Redo": "やり直す",
"Refresh Node Definitions": "ノード定義を更新",
@@ -764,22 +731,10 @@
"nodeCategories": {
"3d": "3d",
"3d_models": "3Dモデル",
"BFL": "BFL",
"Ideogram": "Ideogram",
"Kling": "Kling",
"Luma": "Luma",
"MiniMax": "MiniMax",
"OpenAI": "OpenAI",
"Pika": "Pika",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Stability AI": "Stability AI",
"Veo": "Veo",
"_for_testing": "_テスト用",
"advanced": "高度な機能",
"animation": "アニメーション",
"api": "API",
"api node": "apiード",
"attention_experiments": "アテンション実験",
"audio": "オーディオ",
"batch": "バッチ",
@@ -804,7 +759,6 @@
"instructpix2pix": "インストラクションピクス2ピクス",
"latent": "潜在",
"loaders": "ローダー",
"lotus": "lotus",
"ltxv": "LTXV",
"mask": "マスク",
"model": "モデル",
@@ -816,12 +770,10 @@
"photomaker": "photomaker",
"postprocessing": "ポストプロセッシング",
"preprocessors": "前処理",
"primitive": "プリミティブ",
"samplers": "サンプラー",
"sampling": "サンプリング",
"schedulers": "スケジューラー",
"scheduling": "スケジューリング",
"sd": "sd",
"sd3": "SD3",
"sigmas": "シグマ",
"stable_cascade": "安定したカスケード",
@@ -830,10 +782,6 @@
"unet": "U-Net",
"upscale_diffusion": "アップスケール拡散",
"upscaling": "アップスケーリング",
"utils": "ユーティリティ",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "ビデオ",
"video_models": "ビデオモデル"
},
@@ -853,12 +801,6 @@
},
"title": "お使いのデバイスはサポートされていません"
},
"selectionToolbox": {
"executeButton": {
"disabledTooltip": "出力ノードが選択されていません",
"tooltip": "選択した出力ノードに実行(オレンジ色の枠でハイライト表示)"
}
},
"serverConfig": {
"modifiedConfigs": "以下のサーバー設定を変更しました。変更を適用するには再起動してください。",
"restart": "再起動",
@@ -1355,10 +1297,8 @@
"noTemplatesToExport": "エクスポートするテンプレートがありません",
"nodeDefinitionsUpdated": "ノード定義が更新されました",
"nothingToGroup": "グループ化するものがありません",
"nothingToQueue": "キューに追加する項目がありません",
"pendingTasksDeleted": "保留中のタスクが削除されました",
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
"pleaseSelectOutputNodes": "出力ノードを選択してください",
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
"unauthorizedDomain": "あなたのドメイン {domain} はこのサービスを利用する権限がありません。ご利用のドメインをホワイトリストに追加するには、{email} までご連絡ください。",
"updateRequested": "更新が要求されました",
@@ -1381,7 +1321,6 @@
},
"validation": {
"invalidEmail": "無効なメールアドレス",
"length": "{length}文字でなければなりません",
"maxLength": "{length}文字以下でなければなりません",
"minLength": "{length}文字以上でなければなりません",
"password": {
@@ -1394,7 +1333,6 @@
"uppercase": "少なくとも1つの大文字を含む必要があります"
},
"personalDataConsentRequired": "個人データの処理に同意する必要があります。",
"prefix": "{prefix}で始める必要があります",
"required": "必須"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "실행 큐 맨 앞에 프롬프트 추가"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "선택한 출력 노드 대기열에 추가"
},
"Comfy_Redo": {
"label": "다시 실행"
},

View File

@@ -29,24 +29,6 @@
"title": "API 노드 사용에 필요한 로그인"
},
"auth": {
"apiKey": {
"cleared": "API 키 삭제됨",
"clearedDetail": "API 키가 성공적으로 삭제되었습니다",
"description": "Comfy API 키를 사용하여 API 노드를 활성화하세요",
"error": "유효하지 않은 API 키",
"generateKey": "여기에서 받기",
"helpText": "API 키가 필요하신가요?",
"invalid": "유효하지 않은 API 키",
"invalidDetail": "유효한 API 키를 입력해 주세요",
"label": "API 키",
"placeholder": "API 키를 입력하세요",
"storageFailed": "API 키 저장 실패",
"storageFailedDetail": "다시 시도해 주세요.",
"stored": "API 키 저장됨",
"storedDetail": "API 키가 성공적으로 저장되었습니다",
"title": "API 키",
"whitelistInfo": "비허용 사이트에 대하여"
},
"login": {
"andText": "및",
"confirmPasswordLabel": "비밀번호 확인",
@@ -61,7 +43,6 @@
"loginWithGithub": "Github로 로그인",
"loginWithGoogle": "구글로 로그인",
"newUser": "처음이신가요?",
"noAssociatedUser": "제공된 API 키와 연결된 Comfy 사용자가 없습니다",
"orContinueWith": "또는 다음으로 계속",
"passwordLabel": "비밀번호",
"passwordPlaceholder": "비밀번호를 입력하세요",
@@ -75,7 +56,6 @@
"termsLink": "이용 약관",
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
"title": "계정에 로그인",
"useApiKey": "Comfy API 키",
"userAvatar": "사용자 아바타"
},
"passwordUpdate": {
@@ -151,7 +131,6 @@
"Unpin": "고정 해제"
},
"credits": {
"apiPricing": "API 가격",
"credits": "크레딧",
"faqs": "자주 묻는 질문",
"invoiceHistory": "청구서 내역",
@@ -170,10 +149,8 @@
"yourCreditBalance": "보유 크레딧 잔액"
},
"dataTypes": {
"*": "*",
"AUDIO": "오디오",
"BOOLEAN": "논리값",
"CAMERA_CONTROL": "카메라 제어",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION 출력",
@@ -190,29 +167,20 @@
"INT": "정수",
"LATENT": "잠재 데이터",
"LATENT_OPERATION": "잠재 연산",
"LOAD3D_CAMERA": "3D 카메라 불러오기",
"LOAD_3D": "3D 로드",
"LOAD_3D_ANIMATION": "3D 애니메이션 로드",
"LUMA_CONCEPTS": "Luma 컨셉",
"LUMA_REF": "Luma 참조",
"MASK": "마스크",
"MESH": "메시",
"MODEL": "모델",
"NOISE": "노이즈",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": "Pixverse 템플릿",
"RECRAFT_COLOR": "Recraft 색상",
"RECRAFT_CONTROLS": "Recraft 컨트롤",
"RECRAFT_V3_STYLE": "Recraft V3 스타일",
"SAMPLER": "샘플러",
"SIGMAS": "시그마",
"STRING": "문자열",
"STYLE_MODEL": "스타일 모델",
"SVG": "SVG",
"TIMESTEPS_RANGE": "타임스텝 범위",
"UPSCALE_MODEL": "업스케일 모델",
"VAE": "VAE",
"VIDEO": "비디오",
"VOXEL": "복셀",
"WEBCAM": "웹캠"
},
@@ -655,20 +623,20 @@
"Tolerance": "허용 오차"
},
"menu": {
"autoQueue": "자동 실행 대기열",
"autoQueue": "자동 실행 ",
"batchCount": "배치 수",
"batchCountTooltip": "워크플로 작업을 실행 대기열에 반복 추가할 횟수",
"batchCountTooltip": "워크플로 작업을 실행 에 반복 추가할 횟수",
"clear": "워크플로 비우기",
"clipspace": "클립스페이스 열기",
"disabled": "비활성화됨",
"disabledTooltip": "워크플로 작업을 자동으로 실행 대기열에 추가하지 않습니다.",
"disabledTooltip": "워크플로 작업을 자동으로 실행 에 추가하지 않습니다.",
"execute": "실행",
"hideMenu": "메뉴 숨기기",
"instant": "즉시",
"instantTooltip": "워크플로 실행이 완료되면 즉시 실행 대기열에 추가합니다.",
"instantTooltip": "워크플로 실행이 완료되면 즉시 실행 에 추가합니다.",
"interrupt": "현재 실행 취소",
"onChange": "변경 시",
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 에 추가합니다.",
"refresh": "노드 정의 새로 고침",
"resetView": "캔버스 보기 재설정",
"run": "실행",
@@ -725,9 +693,8 @@
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
"Previous Opened Workflow": "이전 열린 워크플로",
"Queue Prompt": "실행 대기열에 프롬프트 추가",
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
"Queue Prompt": "실행 에 프롬프트 추가",
"Queue Prompt (Front)": "실행 맨 앞에 프롬프트 추가",
"Quit": "종료",
"Redo": "다시 실행",
"Refresh Node Definitions": "노드 정의 새로 고침",
@@ -745,7 +712,7 @@
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
"Toggle Progress Dialog": "진행 상황 대화 상자 전환",
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
"Toggle Queue Sidebar": "실행 사이드바 전환",
"Toggle Search Box": "검색 상자 전환",
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
@@ -764,22 +731,10 @@
"nodeCategories": {
"3d": "3d",
"3d_models": "3D 모델",
"BFL": "BFL",
"Ideogram": "Ideogram",
"Kling": "Kling",
"Luma": "Luma",
"MiniMax": "MiniMax",
"OpenAI": "OpenAI",
"Pika": "Pika",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Stability AI": "Stability AI",
"Veo": "Veo",
"_for_testing": "_테스트용",
"advanced": "고급",
"animation": "애니메이션",
"api": "API",
"api node": "api 노드",
"attention_experiments": "어텐션 실험",
"audio": "오디오",
"batch": "배치",
@@ -804,7 +759,6 @@
"instructpix2pix": "InstructPix2Pix",
"latent": "잠재 데이터",
"loaders": "로더",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "마스크",
"model": "모델",
@@ -816,12 +770,10 @@
"photomaker": "포토메이커",
"postprocessing": "후처리",
"preprocessors": "전처리기",
"primitive": "기본 입력",
"samplers": "샘플러",
"sampling": "샘플링",
"schedulers": "스케줄러",
"scheduling": "스케줄링",
"sd": "sd",
"sd3": "sd3",
"sigmas": "시그마",
"stable_cascade": "Stable Cascade",
@@ -830,10 +782,6 @@
"unet": "UNet",
"upscale_diffusion": "업스케일 확산",
"upscaling": "업스케일링",
"utils": "유틸리티",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "비디오",
"video_models": "비디오 모델"
},
@@ -853,12 +801,6 @@
},
"title": "이 장치는 지원되지 않습니다."
},
"selectionToolbox": {
"executeButton": {
"disabledTooltip": "선택된 출력 노드가 없습니다",
"tooltip": "선택한 출력 노드에 실행 (주황색 테두리로 강조 표시됨)"
}
},
"serverConfig": {
"modifiedConfigs": "다음 서버 구성을 수정했습니다. 변경 사항을 적용하려면 다시 시작하세오.",
"restart": "다시 시작",
@@ -1041,8 +983,8 @@
"Node Widget": "노드 위젯",
"NodeLibrary": "노드 라이브러리",
"Pointer": "포인터",
"Queue": "실행 대기열",
"QueueButton": "실행 대기열 버튼",
"Queue": "실행 ",
"QueueButton": "실행 버튼",
"Reroute": "경유점",
"RerouteBeta": "경유점 (베타)",
"Scene": "장면",
@@ -1068,7 +1010,7 @@
"sortOrder": "정렬 순서"
},
"openWorkflow": "로컬 파일 시스템에서 워크플로 열기",
"queue": "실행 대기열",
"queue": "실행 ",
"queueTab": {
"backToAllTasks": "모든 작업으로 돌아가기",
"clearPendingTasks": "보류 중인 작업 지우기",
@@ -1259,28 +1201,28 @@
"mixing_controlnets": "여러 ControlNet 모델을 결합합니다."
},
"Flux": {
"flux_canny_model_example": "검출된 경계선으로 이미지를 생성합니다.",
"flux_depth_lora_example": "깊이 인식 LoRA 를 이용해 이미지를 생성합니다.",
"flux_dev_checkpoint_example": "FLUX Dev 모델로 이미지를 생성합니다.",
"flux_canny_model_example": "에지 감지로부터 이미지를 생성합니다.",
"flux_depth_lora_example": "깊이 인식 LoRA 이미지를 생성합니다.",
"flux_dev_checkpoint_example": "Flux 개발 모델로 이미지를 생성합니다.",
"flux_fill_inpaint_example": "이미지의 누락된 부분을 채웁니다.",
"flux_fill_outpaint_example": "FLUX 아웃페인팅으로 이미지를 확장합니다.",
"flux_fill_outpaint_example": "Flux 아웃페인팅으로 이미지를 확장합니다.",
"flux_redux_model_example": "참조 이미지의 스타일을 가이드 이미지 생성에 적용합니다.",
"flux_schnell": "FLUX Schnell 모델로 이미지를 빠르게 생성합니다."
"flux_schnell": "Flux Schnell로 이미지를 빠르게 생성합니다."
},
"Image": {
"hidream_e1_full": "HiDream E1 모델로 이미지를 편집합니다.",
"hidream_i1_dev": "HiDream I1 Dev 모델로 이미지를 생성합니다.",
"hidream_i1_fast": "HiDream I1 Fast 모델로 이미지를 빠르게 생성합니다.",
"hidream_i1_full": "HiDream I1 Full 모델로 이미지를 생성합니다.",
"sd3_5_large_blur": "SD 3.5 모델로 흐릿한 참조 이미지에서 이미지를 생성합니다.",
"sd3_5_large_canny_controlnet_example": "Canny 에지 이미지를 통해 SD 3.5 모델 이미지 생성을 가이드합니다.",
"sd3_5_large_depth": "깊이 인식 이미지를 통해 SD 3.5 모델 이미지 생성을 가이드합니다.",
"sd3_5_simple_example": "SD 3.5 모델로 이미지를 생성합니다.",
"hidream_e1_full": "HiDream E1로 이미지를 편집합니다.",
"hidream_i1_dev": "HiDream I1 Dev로 이미지를 생성합니다.",
"hidream_i1_fast": "HiDream I1로 이미지를 빠르게 생성합니다.",
"hidream_i1_full": "HiDream I1로 이미지를 생성합니다.",
"sd3_5_large_blur": "SD 3.5로 흐릿한 참조 이미지에서 이미지를 생성합니다.",
"sd3_5_large_canny_controlnet_example": "SD 3.5에서 에지 감지로 이미지 생성을 가이드합니다.",
"sd3_5_large_depth": "SD 3.5로 깊이 인식 이미지 생성합니다.",
"sd3_5_simple_example": "SD 3.5로 이미지를 생성합니다.",
"sdxl_refiner_prompt_example": "SDXL 결과물을 리파이너로 향상시킵니다.",
"sdxl_revision_text_prompts": "참조 이미지의 개념을 SDXL 이미지 생성에 적용합니다.",
"sdxl_revision_zero_positive": "참조 이미지와 함께 텍스트 프롬프트를 추가하여 SDXL 이미지 생성을 가이드합니다.",
"sdxl_simple_example": "SDXL 모델로 고품질 이미지를 생성합니다.",
"sdxlturbo_example": "SDXL Turbo 모델로 1 스텝으로 이미지를 생성합니다."
"sdxl_simple_example": "SDXL로 고품질 이미지를 생성합니다.",
"sdxlturbo_example": "SDXL Turbo로 한 번에 이미지를 생성합니다."
},
"Image API": {
"api-openai-dall-e-2-inpaint": "Dall-E 2 API로 이미지를 인페인팅합니다.",
@@ -1355,10 +1297,8 @@
"noTemplatesToExport": "내보낼 템플릿이 없습니다",
"nodeDefinitionsUpdated": "노드 정의가 업데이트되었습니다",
"nothingToGroup": "그룹화할 항목이 없습니다",
"nothingToQueue": "대기열에 추가할 항목이 없습니다",
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
"pleaseSelectOutputNodes": "출력 노드를 선택해 주세요",
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
"unauthorizedDomain": "귀하의 도메인 {domain}은(는) 이 서비스를 사용할 수 있는 권한이 없습니다. 도메인을 허용 목록에 추가하려면 {email}로 문의해 주세요.",
"updateRequested": "업데이트 요청됨",
@@ -1381,7 +1321,6 @@
},
"validation": {
"invalidEmail": "유효하지 않은 이메일 주소",
"length": "{length}자여야 합니다",
"maxLength": "{length}자를 초과할 수 없습니다",
"minLength": "{length}자 이상이어야 합니다",
"password": {
@@ -1394,7 +1333,6 @@
"uppercase": "적어도 하나의 대문자를 포함해야 합니다"
},
"personalDataConsentRequired": "개인 데이터 처리에 동의해야 합니다.",
"prefix": "{prefix}(으)로 시작해야 합니다",
"required": "필수"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "Очередь запросов (передняя)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "Добавить выбранные выходные узлы в очередь"
},
"Comfy_Redo": {
"label": "Повторить"
},

View File

@@ -29,24 +29,6 @@
"title": "Требуется вход для использования API Nodes"
},
"auth": {
"apiKey": {
"cleared": "API-ключ удалён",
"clearedDetail": "Ваш API-ключ был успешно удалён",
"description": "Используйте ваш Comfy API-ключ для активации API-узлов",
"error": "Недействительный API-ключ",
"generateKey": "Получить здесь",
"helpText": "Нужен API-ключ?",
"invalid": "Недействительный API-ключ",
"invalidDetail": "Пожалуйста, введите действительный API-ключ",
"label": "API-ключ",
"placeholder": "Введите ваш API-ключ",
"storageFailed": "Не удалось сохранить API-ключ",
"storageFailedDetail": "Пожалуйста, попробуйте еще раз.",
"stored": "API-ключ сохранён",
"storedDetail": "Ваш API-ключ был успешно сохранён",
"title": "API-ключ",
"whitelistInfo": "О не включённых в белый список сайтах"
},
"login": {
"andText": "и",
"confirmPasswordLabel": "Подтвердите пароль",
@@ -61,7 +43,6 @@
"loginWithGithub": "Войти через Github",
"loginWithGoogle": "Войти через Google",
"newUser": "Вы здесь впервые?",
"noAssociatedUser": "С предоставленным API-ключом не связан ни один пользователь Comfy",
"orContinueWith": "Или продолжить с",
"passwordLabel": "Пароль",
"passwordPlaceholder": "Введите ваш пароль",
@@ -75,7 +56,6 @@
"termsLink": "Условиями использования",
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
"title": "Войдите в свой аккаунт",
"useApiKey": "Comfy API-ключ",
"userAvatar": "Аватар пользователя"
},
"passwordUpdate": {
@@ -151,7 +131,6 @@
"Unpin": "Открепить"
},
"credits": {
"apiPricing": "Цены на API",
"credits": "Кредиты",
"faqs": "Часто задаваемые вопросы",
"invoiceHistory": "История счетов",
@@ -170,10 +149,8 @@
"yourCreditBalance": "Ваш баланс кредитов"
},
"dataTypes": {
"*": "*",
"AUDIO": "АУДИО",
"BOOLEAN": "БУЛЕВО",
"CAMERA_CONTROL": "УПРАВЛЕНИЕ_КАМЕРОЙ",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
@@ -190,29 +167,20 @@
"INT": "ЦЕЛОЕ",
"LATENT": "ЛАТЕНТНЫЙ",
"LATENT_OPERATION": АТЕНТНАЯ_ОПЕРАЦИЯ",
"LOAD3D_CAMERA": "ЗАГРУЗИТЬ3D_КАМЕРУ",
"LOAD_3D": "ЗАГРУЗИТЬ_3D",
"LOAD_3D_ANIMATION": "ЗАГРУЗИТЬ_3D_АНИМАЦИЮ",
"LUMA_CONCEPTS": "LUMA_CONCEPTS",
"LUMA_REF": "LUMA_REF",
"MASK": "МАСКА",
"MESH": "СЕТКА",
"MODEL": "МОДЕЛЬ",
"NOISE": "ШУМ",
"PHOTOMAKER": "PHOTOMAKER",
"PIXVERSE_TEMPLATE": АБЛОН_PIXVERSE",
"RECRAFT_COLOR": "RECRAFT_ЦВЕТ",
"RECRAFT_CONTROLS": "RECRAFT_УПРАВЛЕНИЯ",
"RECRAFT_V3_STYLE": "RECRAFT_V3_СТИЛЬ",
"SAMPLER": "СЭМПЛЕР",
"SIGMAS": "СИГМЫ",
"STRING": "СТРОКА",
"STYLE_MODEL": "МОДЕЛЬ_СТИЛЯ",
"SVG": "SVG",
"TIMESTEPS_RANGE": "ДИАПАЗОН_ВРЕМЕННЫХАГОВ",
"UPSCALE_MODEL": "МОДЕЛЬ_АПСКЕЙЛА",
"VAE": "VAE",
"VIDEO": "ВИДЕО",
"VOXEL": "ВОКСЕЛ",
"WEBCAM": "ВЕБ-КАМЕРА"
},
@@ -727,7 +695,6 @@
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
"Queue Prompt": "Запрос в очереди",
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
"Quit": "Выйти",
"Redo": "Повторить",
"Refresh Node Definitions": "Обновить определения нод",
@@ -764,22 +731,10 @@
"nodeCategories": {
"3d": "3d",
"3d_models": "3d_модели",
"BFL": "BFL",
"Ideogram": "Ideogram",
"Kling": "Kling",
"Luma": "Luma",
"MiniMax": "MiniMax",
"OpenAI": "OpenAI",
"Pika": "Pika",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Stability AI": "Stability AI",
"Veo": "Veo",
"_for_testing": "_для_тестирования",
"advanced": "расширенный",
"animation": "анимация",
"api": "api",
"api node": "api-узел",
"attention_experiments": "эксперименты_внимания",
"audio": "аудио",
"batch": "пакет",
@@ -804,7 +759,6 @@
"instructpix2pix": "instructpix2pix",
"latent": "латентный",
"loaders": "загрузчики",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "маска",
"model": "модель",
@@ -816,12 +770,10 @@
"photomaker": "photomaker",
"postprocessing": "постобработка",
"preprocessors": "предобработчики",
"primitive": "примитив",
"samplers": "семплеры",
"sampling": "выборка",
"schedulers": "schedulers",
"scheduling": "scheduling",
"sd": "sd",
"sd3": "sd3",
"sigmas": "сигмы",
"stable_cascade": "стабильная_каскадная",
@@ -830,10 +782,6 @@
"unet": "unet",
"upscale_diffusion": "диффузии_апскейла",
"upscaling": "апскейл",
"utils": "утилиты",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "видео",
"video_models": "видеомодели"
},
@@ -853,12 +801,6 @@
},
"title": "Ваше устройство не поддерживается"
},
"selectionToolbox": {
"executeButton": {
"disabledTooltip": "Выходные узлы не выбраны",
"tooltip": "Выполнить для выбранных выходных узлов (выделены оранжевой рамкой)"
}
},
"serverConfig": {
"modifiedConfigs": "Вы изменили следующие конфигурации сервера. Перезапустите, чтобы применить изменения.",
"restart": "Перезапустить",
@@ -1355,10 +1297,8 @@
"noTemplatesToExport": "Нет шаблонов для экспорта",
"nodeDefinitionsUpdated": "Определения узлов обновлены",
"nothingToGroup": "Нечего группировать",
"nothingToQueue": "Нет заданий в очереди",
"pendingTasksDeleted": "Ожидающие задачи удалены",
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
"pleaseSelectOutputNodes": "Пожалуйста, выберите выходные узлы",
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
"unauthorizedDomain": "Ваш домен {domain} не авторизован для использования этого сервиса. Пожалуйста, свяжитесь с {email}, чтобы добавить ваш домен в белый список.",
"updateRequested": "Запрошено обновление",
@@ -1381,7 +1321,6 @@
},
"validation": {
"invalidEmail": "Недействительный адрес электронной почты",
"length": "Должно быть {length} символов",
"maxLength": "Должно быть не более {length} символов",
"minLength": "Должно быть не менее {length} символов",
"password": {
@@ -1394,7 +1333,6 @@
"uppercase": "Должен содержать хотя бы одну заглавную букву"
},
"personalDataConsentRequired": "Вы должны согласиться на обработку ваших персональных данных.",
"prefix": "Должно начинаться с {prefix}",
"required": "Обязательно"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -155,9 +155,6 @@
"Comfy_QueuePromptFront": {
"label": "执行提示词(前端)"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "队列所选输出节点"
},
"Comfy_Redo": {
"label": "重做"
},

View File

@@ -29,24 +29,6 @@
"title": "使用API节点需要登录"
},
"auth": {
"apiKey": {
"cleared": "API 密钥已清除",
"clearedDetail": "您的 API 密钥已成功清除",
"description": "使用您的 Comfy API 密钥以启用 API 节点",
"error": "无效的 API 密钥",
"generateKey": "在这里获取",
"helpText": "需要 API 密钥?",
"invalid": "无效的 API 密钥",
"invalidDetail": "请输入有效的 API 密钥",
"label": "API 密钥",
"placeholder": "请输入您的 API 密钥",
"storageFailed": "API 密钥存储失败",
"storageFailedDetail": "请重试。",
"stored": "API 密钥已存储",
"storedDetail": "您的 API 密钥已成功存储",
"title": "API 密钥",
"whitelistInfo": "关于非白名单网站"
},
"login": {
"andText": "和",
"confirmPasswordLabel": "确认密码",
@@ -61,7 +43,6 @@
"loginWithGithub": "使用Github登录",
"loginWithGoogle": "使用Google登录",
"newUser": "新来的?",
"noAssociatedUser": "所提供的 API 密钥未关联任何 Comfy 用户",
"orContinueWith": "或者继续使用",
"passwordLabel": "密码",
"passwordPlaceholder": "输入您的密码",
@@ -75,7 +56,6 @@
"termsLink": "使用条款",
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
"title": "登录您的账户",
"useApiKey": "Comfy API 密钥",
"userAvatar": "用户头像"
},
"passwordUpdate": {
@@ -151,7 +131,6 @@
"Unpin": "取消固定"
},
"credits": {
"apiPricing": "API 价格",
"credits": "积分",
"faqs": "常见问题",
"invoiceHistory": "发票历史",
@@ -170,10 +149,8 @@
"yourCreditBalance": "您的积分余额"
},
"dataTypes": {
"*": "*",
"AUDIO": "音频",
"BOOLEAN": "布尔",
"CAMERA_CONTROL": "相机控制",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP视觉",
"CLIP_VISION_OUTPUT": "CLIP视觉输出",
@@ -190,29 +167,20 @@
"INT": "整数",
"LATENT": "Latent",
"LATENT_OPERATION": "Latent操作",
"LOAD3D_CAMERA": "加载3D相机",
"LOAD_3D": "加载3D",
"LOAD_3D_ANIMATION": "加载3D动画",
"LUMA_CONCEPTS": "Luma 概念",
"LUMA_REF": "Luma 参考",
"MASK": "遮罩",
"MESH": "网格",
"MODEL": "模型",
"NOISE": "噪波",
"PHOTOMAKER": "PhotoMaker",
"PIXVERSE_TEMPLATE": "Pixverse 模板",
"RECRAFT_COLOR": "Recraft 颜色",
"RECRAFT_CONTROLS": "Recraft 控件",
"RECRAFT_V3_STYLE": "Recraft V3 风格",
"SAMPLER": "采样器",
"SIGMAS": "Sigmas",
"STRING": "字符串",
"STYLE_MODEL": "风格模型",
"SVG": "SVG",
"TIMESTEPS_RANGE": "时间间隔范围",
"UPSCALE_MODEL": "放大模型",
"VAE": "VAE",
"VIDEO": "视频",
"VOXEL": "体素",
"WEBCAM": "摄像头"
},
@@ -727,7 +695,6 @@
"Previous Opened Workflow": "上一个打开的工作流",
"Queue Prompt": "执行提示词",
"Queue Prompt (Front)": "执行提示词 (优先执行)",
"Queue Selected Output Nodes": "将所选输出节点加入队列",
"Quit": "退出",
"Redo": "重做",
"Refresh Node Definitions": "刷新节点定义",
@@ -764,22 +731,10 @@
"nodeCategories": {
"3d": "3d",
"3d_models": "3D模型",
"BFL": "BFL",
"Ideogram": "Ideogram",
"Kling": "Kling",
"Luma": "Luma",
"MiniMax": "MiniMax",
"OpenAI": "OpenAI",
"Pika": "Pika",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Stability AI": "Stability AI",
"Veo": "Veo",
"_for_testing": "_用于测试",
"advanced": "高级",
"animation": "动画",
"api": "API",
"api node": "api 节点",
"attention_experiments": "注意力实验",
"audio": "音频",
"batch": "批处理",
@@ -804,7 +759,6 @@
"instructpix2pix": "InstructPix2Pix",
"latent": "Latent",
"loaders": "加载器",
"lotus": "lotus",
"ltxv": "LTXV",
"mask": "遮罩",
"model": "模型",
@@ -816,12 +770,10 @@
"photomaker": "PhotoMaker",
"postprocessing": "后处理",
"preprocessors": "预处理器",
"primitive": "基础",
"samplers": "采样器",
"sampling": "采样",
"schedulers": "调度器",
"scheduling": "调度",
"sd": "sd",
"sd3": "SD3",
"sigmas": "Sigmas",
"stable_cascade": "StableCascade",
@@ -830,10 +782,6 @@
"unet": "U-Net",
"upscale_diffusion": "放大扩散",
"upscaling": "放大",
"utils": "工具",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "视频",
"video_models": "视频模型"
},
@@ -853,12 +801,6 @@
},
"title": "您的设备不受支持"
},
"selectionToolbox": {
"executeButton": {
"disabledTooltip": "未选择输出节点",
"tooltip": "执行到选定的输出节点(用橙色边框高亮显示)"
}
},
"serverConfig": {
"modifiedConfigs": "您已修改以下服务器配置。重启以应用更改。",
"restart": "重启",
@@ -1355,10 +1297,8 @@
"noTemplatesToExport": "没有模板可以导出",
"nodeDefinitionsUpdated": "节点定义已更新",
"nothingToGroup": "没有可分组的内容",
"nothingToQueue": "没有可加入队列的内容",
"pendingTasksDeleted": "待处理任务已删除",
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
"pleaseSelectOutputNodes": "请选择输出节点",
"unableToGetModelFilePath": "无法获取模型文件路径",
"unauthorizedDomain": "您的域名 {domain} 未被授权使用此服务。请联系 {email} 将您的域名添加到白名单。",
"updateRequested": "已请求更新",
@@ -1381,7 +1321,6 @@
},
"validation": {
"invalidEmail": "无效的电子邮件地址",
"length": "必须为{length}个字符",
"maxLength": "不能超过{length}个字符",
"minLength": "必须至少有{length}个字符",
"password": {
@@ -1394,7 +1333,6 @@
"uppercase": "必须包含至少一个大写字母"
},
"personalDataConsentRequired": "您必须同意处理您的个人数据。",
"prefix": "必须以 {prefix} 开头",
"required": "必填"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -82,11 +82,6 @@ const zExecutionErrorWsMessage = zExecutionWsMessageBase.extend({
current_outputs: z.any()
})
const zProgressTextWsMessage = z.object({
nodeId: zNodeId,
text: z.string()
})
const zTerminalSize = z.object({
cols: z.number(),
row: z.number()
@@ -119,7 +114,6 @@ export type ExecutionInterruptedWsMessage = z.infer<
>
export type ExecutionErrorWsMessage = z.infer<typeof zExecutionErrorWsMessage>
export type LogsWsMessage = z.infer<typeof zLogsWsMessage>
export type ProgressTextWsMessage = z.infer<typeof zProgressTextWsMessage>
// End of ws messages
const zPromptInputItem = z.object({
@@ -457,7 +451,7 @@ const zSettings = z.object({
'Comfy.Load3D.CameraType': z.enum(['perspective', 'orthographic']),
'pysssss.SnapToGrid': z.boolean(),
/** VHS setting is used for queue video preview support. */
'VHS.AdvancedPreviews': z.string(),
'VHS.AdvancedPreviews': z.boolean(),
/** Settings used for testing */
'test.setting': z.any(),
'main.sub.setting.name': z.any(),

View File

@@ -216,7 +216,6 @@ const zComfyNode = z
const zGroup = z
.object({
id: z.number().optional(),
title: z.string(),
bounding: z.tuple([z.number(), z.number(), z.number(), z.number()]),
color: z.string().optional(),

View File

@@ -36,7 +36,7 @@ export const zNumericInputOptions = zBaseInputOptions.extend({
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
/** Note: Many node authors are using INT/FLOAT to pass list of INT/FLOAT. */
// Note: Many node authors are using INT/FLOAT to pass list of INT/FLOAT.
default: z.union([z.number(), z.array(z.number())]).optional(),
display: z.enum(['slider', 'number', 'knob']).optional()
})

View File

@@ -2,16 +2,6 @@ import { z } from 'zod'
import { t } from '@/i18n'
export const apiKeySchema = z.object({
apiKey: z
.string()
.trim()
.startsWith('comfyui-', t('validation.prefix', { prefix: 'comfyui-' }))
.length(72, t('validation.length', { length: 72 }))
})
export type ApiKeyData = z.infer<typeof apiKeySchema>
export const signInSchema = z.object({
email: z
.string()

View File

@@ -14,7 +14,6 @@ import type {
LogsRawResponse,
LogsWsMessage,
PendingTaskItem,
ProgressTextWsMessage,
ProgressWsMessage,
PromptResponse,
RunningTaskItem,
@@ -59,21 +58,6 @@ interface QueuePromptRequestBody {
* ```
*/
auth_token_comfy_org?: string
/**
* The auth token for the comfy org account if the user is logged in.
*
* Backend node can access this token by specifying following input:
* ```python
* def INPUT_TYPES(s):
* return {
* "hidden": { "api_key": "API_KEY_COMFY_ORG" }
* }
*
* def execute(self, api_key: str):
* print(f"API Key: {api_key}")
* ```
*/
api_key_comfy_org?: string
}
front?: boolean
number?: number
@@ -102,7 +86,6 @@ interface BackendApiCalls {
logs: LogsWsMessage
/** Binary preview/progress data */
b_preview: Blob
progress_text: ProgressTextWsMessage
}
/** Dictionary of all api calls */
@@ -245,10 +228,6 @@ export class ComfyApi extends EventTarget {
* custom nodes are patched.
*/
authToken?: string
/**
* The API key for the comfy org account if the user logged in via API key.
*/
apiKey?: string
constructor() {
super()
@@ -401,21 +380,12 @@ export class ComfyApi extends EventTarget {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data)
const eventType = view.getUint32(0)
const imageType = view.getUint32(4)
const imageData = event.data.slice(8)
let imageMime
switch (eventType) {
case 3:
const decoder = new TextDecoder()
const data = event.data.slice(4)
const nodeIdLength = view.getUint32(4)
this.dispatchCustomEvent('progress_text', {
nodeId: decoder.decode(data.slice(4, 4 + nodeIdLength)),
text: decoder.decode(data.slice(4 + nodeIdLength))
})
break
case 1:
const imageType = view.getUint32(4)
const imageData = event.data.slice(8)
switch (imageType) {
case 2:
imageMime = 'image/png'
@@ -575,7 +545,6 @@ export class ComfyApi extends EventTarget {
prompt,
extra_data: {
auth_token_comfy_org: this.authToken,
api_key_comfy_org: this.apiKey,
extra_pnginfo: { workflow }
}
}

View File

@@ -5,8 +5,7 @@ import {
LGraphNode,
LiteGraph
} from '@comfyorg/litegraph'
import type { Vector2 } from '@comfyorg/litegraph'
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
import type { IWidget, Vector2 } from '@comfyorg/litegraph'
import _ from 'lodash'
import type { ToastMessageOptions } from 'primevue/toast'
import { reactive } from 'vue'
@@ -25,22 +24,14 @@ import {
type ModelFile,
type NodeId
} from '@/schemas/comfyWorkflowSchema'
import {
type ComfyNodeDef as ComfyNodeDefV1,
isComboInputSpecV1,
isComboInputSpecV2
} from '@/schemas/nodeDefSchema'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
import { getMp3Metadata } from '@/scripts/metadata/mp3'
import { getOggMetadata } from '@/scripts/metadata/ogg'
import { getSvgMetadata } from '@/scripts/metadata/svg'
import { useDialogService } from '@/services/dialogService'
import { useExtensionService } from '@/services/extensionService'
import { useLitegraphService } from '@/services/litegraphService'
import { useWorkflowService } from '@/services/workflowService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useExtensionStore } from '@/stores/extensionStore'
@@ -72,6 +63,7 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard'
import { type ComfyApi, PromptExecutionError, api } from './api'
import { defaultGraph } from './defaultGraph'
import { pruneWidgets } from './domWidget'
import { getSvgMetadata } from './metadata/svg'
import {
getFlacMetadata,
getLatentMetadata,
@@ -102,7 +94,7 @@ function sanitizeNodeName(string: string) {
}
type Clipspace = {
widgets?: Pick<IBaseWidget, 'type' | 'name' | 'value'>[] | null
widgets?: Pick<IWidget, 'type' | 'name' | 'value'>[] | null
imgs?: HTMLImageElement[] | null
original_imgs?: HTMLImageElement[] | null
images?: any[] | null
@@ -114,11 +106,7 @@ export class ComfyApp {
/**
* List of entries to queue
*/
#queueItems: {
number: number
batchCount: number
queueNodeIds?: NodeId[]
}[] = []
#queueItems: { number: number; batchCount: number }[] = []
/**
* If the queue is currently being processed
*/
@@ -399,6 +387,7 @@ export class ComfyApp {
const index = node.widgets.findIndex((obj) => obj.name === 'image')
if (index >= 0) {
if (
// @ts-expect-error custom widget type
node.widgets[index].type != 'image' &&
typeof node.widgets[index].value == 'string' &&
clip_image.filename
@@ -420,6 +409,7 @@ export class ComfyApp {
)
if (prop && prop.type != 'button') {
if (
// @ts-expect-error Custom widget type
prop.type != 'image' &&
typeof prop.value == 'string' &&
// @ts-expect-error Custom widget value
@@ -431,6 +421,7 @@ export class ComfyApp {
resultItem.filename +
(resultItem.type ? ` [${resultItem.type}]` : '')
} else {
// @ts-expect-error fixme ts strict error
prop.value = value
prop.callback?.(value)
}
@@ -1069,21 +1060,13 @@ export class ComfyApp {
try {
// @ts-expect-error Discrepancies between zod and litegraph - in progress
this.graph.configure(graphData)
if (restore_view) {
if (
useSettingStore().get('Comfy.EnableWorkflowViewRestore') &&
graphData.extra?.ds
) {
this.canvas.ds.offset = graphData.extra.ds.offset
this.canvas.ds.scale = graphData.extra.ds.scale
} else {
// @note: Set view after the graph has been rendered once. fitView uses
// boundingRect on nodes to calculate the view bounds, which only become
// available after the first render.
requestAnimationFrame(() => {
useLitegraphService().fitView()
})
}
if (
restore_view &&
useSettingStore().get('Comfy.EnableWorkflowViewRestore') &&
graphData.extra?.ds
) {
this.canvas.ds.offset = graphData.extra.ds.offset
this.canvas.ds.scale = graphData.extra.ds.scale
}
} catch (error) {
useDialogService().showErrorDialog(error, {
@@ -1118,8 +1101,10 @@ export class ComfyApp {
) {
if (widget.name == 'control_after_generate') {
if (widget.value === true) {
// @ts-expect-error string is not assignable to boolean
widget.value = 'randomize'
} else if (widget.value === false) {
// @ts-expect-error string is not assignable to boolean
widget.value = 'fixed'
}
}
@@ -1163,22 +1148,14 @@ export class ComfyApp {
})
}
async graphToPrompt(
graph = this.graph,
options: { queueNodeIds?: NodeId[] } = {}
) {
async graphToPrompt(graph = this.graph) {
return graphToPrompt(graph, {
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave'),
queueNodeIds: options.queueNodeIds
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
})
}
async queuePrompt(
number: number,
batchCount: number = 1,
queueNodeIds?: NodeId[]
): Promise<boolean> {
this.#queueItems.push({ number, batchCount, queueNodeIds })
async queuePrompt(number: number, batchCount: number = 1): Promise<boolean> {
this.#queueItems.push({ number, batchCount })
// Only have one action process the items so each one gets a unique seed correctly
if (this.#processingQueue) {
@@ -1191,24 +1168,21 @@ export class ComfyApp {
let comfyOrgAuthToken =
(await useFirebaseAuthStore().getIdToken()) ?? undefined
let comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
try {
while (this.#queueItems.length) {
const { number, batchCount, queueNodeIds } = this.#queueItems.pop()!
const { number, batchCount } = this.#queueItems.pop()!
for (let i = 0; i < batchCount; i++) {
// Allow widgets to run callbacks before a prompt has been queued
// e.g. random seed before every gen
executeWidgetsCallback(this.graph.nodes, 'beforeQueued')
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
const p = await this.graphToPrompt()
try {
api.authToken = comfyOrgAuthToken
api.apiKey = comfyOrgApiKey ?? undefined
const res = await api.queuePrompt(number, p)
delete api.authToken
delete api.apiKey
executionStore.lastNodeErrors = res.node_errors ?? null
if (executionStore.lastNodeErrors?.length) {
this.canvas.draw(true, true)
@@ -1310,24 +1284,6 @@ export class ComfyApp {
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/mpeg') {
const { workflow, prompt } = await getMp3Metadata(file)
if (workflow) {
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/ogg') {
const { workflow, prompt } = await getOggMetadata(file)
if (workflow) {
this.loadGraphData(workflow, true, true, fileName)
} else if (prompt) {
this.loadApiJson(prompt, fileName)
} else {
this.showErrorOnFileLoad(file)
}
} else if (file.type === 'audio/flac' || file.type === 'audio/x-flac') {
const pngInfo = await getFlacMetadata(file)
const workflow = pngInfo?.workflow || pngInfo?.Workflow
@@ -1400,7 +1356,7 @@ export class ComfyApp {
await this.loadGraphData(
JSON.parse(readerResult),
true,
true,
false,
fileName
)
}
@@ -1576,26 +1532,14 @@ export class ComfyApp {
if (!def?.input) continue
if (node.widgets) {
const nodeInputs = def.input
for (const widget of node.widgets) {
if (widget.type === 'combo') {
let inputType: 'required' | 'optional' | undefined
if (nodeInputs.required?.[widget.name] !== undefined) {
inputType = 'required'
} else if (nodeInputs.optional?.[widget.name] !== undefined) {
inputType = 'optional'
}
if (inputType !== undefined) {
// Get the input spec associated with the widget
const inputSpec = nodeInputs[inputType]?.[widget.name]
if (inputSpec) {
// Refresh the combo widget's options with the values from the input spec
if (isComboInputSpecV2(inputSpec)) {
widget.options.values = inputSpec[1]?.options
} else if (isComboInputSpecV1(inputSpec)) {
widget.options.values = inputSpec[0]
}
}
if (def['input'].required?.[widget.name] !== undefined) {
// @ts-expect-error Requires discriminated union
widget.options.values = def['input'].required[widget.name][0]
} else if (def['input'].optional?.[widget.name] !== undefined) {
// @ts-expect-error Requires discriminated union
widget.options.values = def['input'].optional[widget.name][0]
}
}
}

View File

@@ -10,7 +10,6 @@ import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
import { api } from './api'
import type { ComfyApp } from './app'
import { app } from './app'
function clone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj))
@@ -37,6 +36,11 @@ export class ChangeTracker {
ds?: { scale: number; offset: [number, number] }
nodeOutputs?: Record<string, any>
static app?: ComfyApp
get app(): ComfyApp {
return ChangeTracker.app!
}
constructor(
/**
* The workflow that this change tracker is tracking
@@ -64,18 +68,18 @@ export class ChangeTracker {
store() {
this.ds = {
scale: app.canvas.ds.scale,
offset: [app.canvas.ds.offset[0], app.canvas.ds.offset[1]]
scale: this.app.canvas.ds.scale,
offset: [this.app.canvas.ds.offset[0], this.app.canvas.ds.offset[1]]
}
}
restore() {
if (this.ds) {
app.canvas.ds.scale = this.ds.scale
app.canvas.ds.offset = this.ds.offset
this.app.canvas.ds.scale = this.ds.scale
this.app.canvas.ds.offset = this.ds.offset
}
if (this.nodeOutputs) {
app.nodeOutputs = this.nodeOutputs
this.app.nodeOutputs = this.nodeOutputs
}
}
@@ -101,8 +105,10 @@ export class ChangeTracker {
}
checkState() {
if (!app.graph || this.changeCount) return
const currentState = clone(app.graph.serialize()) as ComfyWorkflowJSON
if (!this.app.graph || this.changeCount) return
// @ts-expect-error zod type issue on ComfyWorkflowJSON. ComfyWorkflowJSON
// is stricter than LiteGraph's serialisation schema.
const currentState = clone(this.app.graph.serialize()) as ComfyWorkflowJSON
if (!this.activeState) {
this.activeState = currentState
return
@@ -126,7 +132,7 @@ export class ChangeTracker {
target.push(this.activeState)
this.restoringState = true
try {
await app.loadGraphData(prevState, false, false, this.workflow, {
await this.app.loadGraphData(prevState, false, false, this.workflow, {
showMissingModelsDialog: false,
showMissingNodesDialog: false,
checkForRerouteMigration: false
@@ -183,11 +189,13 @@ export class ChangeTracker {
}
}
static init() {
static init(app: ComfyApp) {
const getCurrentChangeTracker = () =>
useWorkflowStore().activeWorkflow?.changeTracker
const checkState = () => getCurrentChangeTracker()?.checkState()
ChangeTracker.app = app
let keyIgnored = false
window.addEventListener(
'keydown',
@@ -229,7 +237,7 @@ export class ChangeTracker {
if (await changeTracker.undoRedo(e)) return
// If our active element is some type of input then handle changes after they're done
if (ChangeTracker.bindInput(bindInputEl)) return
if (ChangeTracker.bindInput(app, bindInputEl)) return
logger.debug('checkState on keydown')
changeTracker.checkState()
})
@@ -331,7 +339,7 @@ export class ChangeTracker {
})
}
static bindInput(activeEl: Element | null): boolean {
static bindInput(_app: ComfyApp, activeEl: Element | null): boolean {
if (
!activeEl ||
activeEl.tagName === 'CANVAS' ||

View File

@@ -132,12 +132,7 @@ export const defaultGraph: ComfyWorkflowJSON = {
],
groups: [],
config: {},
extra: {
ds: {
offset: [0, 0],
scale: 1
}
},
extra: {},
version: 0.4
}

View File

@@ -1,6 +1,7 @@
import { LGraphNode, LegacyWidget, LiteGraph } from '@comfyorg/litegraph'
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
import type {
IBaseWidget,
ICustomWidget,
IWidget,
IWidgetOptions
} from '@comfyorg/litegraph/dist/types/widgets'
import _ from 'lodash'
@@ -12,9 +13,9 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { generateUUID } from '@/utils/formatUtil'
export interface BaseDOMWidget<V extends object | string>
extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
extends ICustomWidget {
// ICustomWidget properties
type: string
type: 'custom'
options: DOMWidgetOptions<V>
value: V
callback?: (value: V) => void
@@ -80,23 +81,26 @@ export interface DOMWidgetOptions<V extends object | string>
}
export const isDOMWidget = <T extends HTMLElement, V extends object | string>(
widget: IBaseWidget
widget: IWidget
): widget is DOMWidget<T, V> => 'element' in widget && !!widget.element
export const isComponentWidget = <V extends object | string>(
widget: IBaseWidget
widget: IWidget
): widget is ComponentWidget<V> => 'component' in widget && !!widget.component
abstract class BaseDOMWidgetImpl<V extends object | string>
extends LegacyWidget<IBaseWidget<V, string, DOMWidgetOptions<V>>>
implements BaseDOMWidget<V>
{
static readonly DEFAULT_MARGIN = 10
declare readonly name: string
declare readonly options: DOMWidgetOptions<V>
declare callback?: (value: V) => void
readonly type: 'custom'
readonly name: string
readonly options: DOMWidgetOptions<V>
computedHeight?: number
y: number = 0
callback?: (value: V) => void
readonly id: string
readonly node: LGraphNode
constructor(obj: {
node: LGraphNode
@@ -104,17 +108,20 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
type: string
options: DOMWidgetOptions<V>
}) {
const { node, name, type, options } = obj
super({ y: 0, name, type, options }, node)
// @ts-expect-error custom widget type
this.type = obj.type
this.name = obj.name
this.options = obj.options
this.id = generateUUID()
this.node = obj.node
}
override get value(): V {
get value(): V {
return this.options.getValue?.() ?? ('' as V)
}
override set value(v: V) {
set value(v: V) {
this.options.setValue?.(v)
this.callback?.(this.value)
}
@@ -127,7 +134,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
return !['hidden'].includes(this.type) && this.node.isWidgetVisible(this)
}
override draw(
draw(
ctx: CanvasRenderingContext2D,
_node: LGraphNode,
widget_width: number,
@@ -152,7 +159,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
this.options.onDraw?.(this)
}
override onRemove(): void {
onRemove(): void {
useDomWidgetStore().unregisterWidget(this.id)
}
}
@@ -161,7 +168,7 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
extends BaseDOMWidgetImpl<V>
implements DOMWidget<T, V>
{
override readonly element: T
readonly element: T
constructor(obj: {
node: LGraphNode
@@ -175,7 +182,8 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
}
/** Extract DOM widget size info */
override computeLayoutSize(node: LGraphNode) {
computeLayoutSize(node: LGraphNode) {
// @ts-expect-error custom widget type
if (this.type === 'hidden') {
return {
minHeight: 0,
@@ -239,7 +247,7 @@ export class ComponentWidgetImpl<V extends object | string>
this.inputSpec = obj.inputSpec
}
override computeLayoutSize() {
computeLayoutSize() {
const minHeight = this.options.getMinHeight?.() ?? 50
const maxHeight = this.options.getMaxHeight?.()
return {
@@ -249,7 +257,7 @@ export class ComponentWidgetImpl<V extends object | string>
}
}
override serializeValue(): V {
serializeValue(): V {
return toRaw(this.value)
}
}

View File

@@ -1,5 +1,4 @@
import { LGraphNode } from '@comfyorg/litegraph'
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useBooleanWidget } from '@/composables/widgets/useBooleanWidget'
@@ -11,7 +10,7 @@ const FloatWidget = useFloatWidget()
const BooleanWidget = useBooleanWidget()
function addWidgetFromValue(node: LGraphNode, value: unknown) {
let widget: IBaseWidget
let widget: IWidget
if (typeof value === 'string') {
widget = StringWidget(node, {

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