Cherry pick API node fixes (#3839)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: Chenlei Hu <hcl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Chenlei Hu <huchenlei@proton.me>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
This commit is contained in:
Christian Byrne
2025-05-09 16:28:49 -07:00
committed by GitHub
parent b38b388674
commit 0a2f567d49
48 changed files with 20614 additions and 384 deletions

BIN
public/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,9 @@
<svg width="520" height="520" viewBox="0 0 520 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_227_285" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="520" height="520">
<path d="M0 184.335C0 119.812 0 87.5502 12.5571 62.9055C23.6026 41.2274 41.2274 23.6026 62.9055 12.5571C87.5502 0 119.812 0 184.335 0H335.665C400.188 0 432.45 0 457.094 12.5571C478.773 23.6026 496.397 41.2274 507.443 62.9055C520 87.5502 520 119.812 520 184.335V335.665C520 400.188 520 432.45 507.443 457.094C496.397 478.773 478.773 496.397 457.094 507.443C432.45 520 400.188 520 335.665 520H184.335C119.812 520 87.5502 520 62.9055 507.443C41.2274 496.397 23.6026 478.773 12.5571 457.094C0 432.45 0 400.188 0 335.665V184.335Z" fill="#FFFFFF"/>
</mask>
<g mask="url(#mask0_227_285)">
<rect y="0.751831" width="520" height="520" fill="#000000"/>
<path d="M176.484 428.831C168.649 428.831 162.327 425.919 158.204 420.412C153.966 414.755 152.861 406.857 155.171 398.749L164.447 366.178C165.187 363.585 164.672 360.794 163.059 358.636C161.446 356.483 158.921 355.216 156.241 355.216H129.571C121.731 355.216 115.409 352.308 111.289 346.802C107.051 341.14 105.946 333.242 108.258 325.134L140.124 213.748L143.642 201.51C148.371 184.904 165.62 171.407 182.097 171.407H214.009C217.817 171.407 221.167 168.868 222.215 165.183L232.769 128.135C237.494 111.545 254.742 98.048 271.219 98.048L339.468 97.9264L389.431 97.9221C397.268 97.9221 403.59 100.831 407.711 106.337C411.949 111.994 413.054 119.892 410.744 128L396.457 178.164C391.734 194.75 374.485 208.242 358.009 208.242L289.607 208.372H257.706C253.902 208.372 250.557 210.907 249.502 214.588L222.903 307.495C222.159 310.093 222.673 312.892 224.291 315.049C225.904 317.202 228.428 318.469 231.107 318.469C231.113 318.469 276.307 318.381 276.307 318.381H326.122C333.959 318.381 340.281 321.29 344.402 326.796C348.639 332.457 349.744 340.355 347.433 348.463L333.146 398.619C328.423 415.209 311.174 428.701 294.698 428.701L226.299 428.831H176.484Z" fill="#FFFFFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,9 @@
<svg width="520" height="520" viewBox="0 0 520 520" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_227_285" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="520" height="520">
<path d="M0 184.335C0 119.812 0 87.5502 12.5571 62.9055C23.6026 41.2274 41.2274 23.6026 62.9055 12.5571C87.5502 0 119.812 0 184.335 0H335.665C400.188 0 432.45 0 457.094 12.5571C478.773 23.6026 496.397 41.2274 507.443 62.9055C520 87.5502 520 119.812 520 184.335V335.665C520 400.188 520 432.45 507.443 457.094C496.397 478.773 478.773 496.397 457.094 507.443C432.45 520 400.188 520 335.665 520H184.335C119.812 520 87.5502 520 62.9055 507.443C41.2274 496.397 23.6026 478.773 12.5571 457.094C0 432.45 0 400.188 0 335.665V184.335Z" fill="#EEFF30"/>
</mask>
<g mask="url(#mask0_227_285)">
<rect y="0.751831" width="520" height="520" fill="#172DD7"/>
<path d="M176.484 428.831C168.649 428.831 162.327 425.919 158.204 420.412C153.966 414.755 152.861 406.857 155.171 398.749L164.447 366.178C165.187 363.585 164.672 360.794 163.059 358.636C161.446 356.483 158.921 355.216 156.241 355.216H129.571C121.731 355.216 115.409 352.308 111.289 346.802C107.051 341.14 105.946 333.242 108.258 325.134L140.124 213.748L143.642 201.51C148.371 184.904 165.62 171.407 182.097 171.407H214.009C217.817 171.407 221.167 168.868 222.215 165.183L232.769 128.135C237.494 111.545 254.742 98.048 271.219 98.048L339.468 97.9264L389.431 97.9221C397.268 97.9221 403.59 100.831 407.711 106.337C411.949 111.994 413.054 119.892 410.744 128L396.457 178.164C391.734 194.75 374.485 208.242 358.009 208.242L289.607 208.372H257.706C253.902 208.372 250.557 210.907 249.502 214.588L222.903 307.495C222.159 310.093 222.673 312.892 224.291 315.049C225.904 317.202 228.428 318.469 231.107 318.469C231.113 318.469 276.307 318.381 276.307 318.381H326.122C333.959 318.381 340.281 321.29 344.402 326.796C348.639 332.457 349.744 340.355 347.433 348.463L333.146 398.619C328.423 415.209 311.174 428.701 294.698 428.701L226.299 428.831H176.484Z" fill="#F0FF41"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -5,7 +5,7 @@
<div class="flex flex-col items-center">
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem" />
<h3>{{ title }}</h3>
<p class="whitespace-pre-line text-center">
<p :class="textClass" class="whitespace-pre-line text-center">
{{ message }}
</p>
<Button
@@ -29,6 +29,7 @@ const props = defineProps<{
icon?: string
title: string
message: string
textClass?: string
buttonLabel?: string
}>()

View File

@@ -0,0 +1,106 @@
import { mount } from '@vue/test-utils'
import Avatar from 'primevue/avatar'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import UserAvatar from './UserAvatar.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
auth: {
login: {
userAvatar: 'User Avatar'
}
}
}
}
})
describe('UserAvatar', () => {
beforeEach(() => {
const app = createApp({})
app.use(PrimeVue)
})
const mountComponent = (props: any = {}) => {
return mount(UserAvatar, {
global: {
plugins: [PrimeVue, i18n],
components: { Avatar }
},
props
})
}
it('renders correctly with photo Url', async () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/avatar.jpg'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBe('https://example.com/avatar.jpg')
expect(avatar.props('icon')).toBeNull()
})
it('renders with default icon when no photo Url is provided', () => {
const wrapper = mountComponent({
photoUrl: undefined
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('renders with default icon when provided photo Url is null', () => {
const wrapper = mountComponent({
photoUrl: null
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('falls back to icon when image fails to load', async () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/broken-image.jpg'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.props('icon')).toBeNull()
// Simulate image load error
avatar.vm.$emit('error')
await nextTick()
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('uses provided ariaLabel', () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/avatar.jpg',
ariaLabel: 'Custom Label'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.attributes('aria-label')).toBe('Custom Label')
})
it('falls back to i18n translation when no ariaLabel is provided', () => {
const wrapper = mountComponent({
photoUrl: 'https://example.com/avatar.jpg'
})
const avatar = wrapper.findComponent(Avatar)
expect(avatar.attributes('aria-label')).toBe('User Avatar')
})
})

View File

@@ -0,0 +1,25 @@
<template>
<Avatar
:image="photoUrl ?? undefined"
:icon="hasAvatar ? undefined : 'pi pi-user'"
shape="circle"
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
@error="handleImageError"
/>
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import { computed, ref } from 'vue'
const { photoUrl, ariaLabel } = defineProps<{
photoUrl?: string | null
ariaLabel?: string
}>()
const imageError = ref(false)
const handleImageError = () => {
imageError.value = true
}
const hasAvatar = computed(() => photoUrl && !imageError.value)
</script>

View File

@@ -5,6 +5,7 @@
icon="pi pi-exclamation-circle"
:title="title"
:message="error.exceptionMessage"
:text-class="'break-words max-w-[60vw]'"
/>
<template v-if="error.extensionFile">
<span>{{ t('errorDialog.extensionFileHint') }}:</span>

View File

@@ -1,95 +1,132 @@
<template>
<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" />
<div class="w-96 p-2 overflow-x-hidden">
<ApiKeyForm
v-if="showApiKeyForm"
@back="showApiKeyForm = false"
@success="onSuccess"
/>
<template v-else>
<Message v-if="userIsInChina" severity="warn" class="mb-4">
{{ t('auth.signup.regionRestrictionChina') }}
<!-- 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>
<SignUpForm v-else @submit="signUpWithEmail" />
<!-- 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="https://platform.comfy.org/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
>.
</p>
</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>
@@ -104,6 +141,7 @@ 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'
@@ -115,8 +153,11 @@ 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

@@ -4,13 +4,13 @@
<h2 class="text-2xl font-bold mb-2">{{ $t('userSettings.title') }}</h2>
<Divider class="mb-3" />
<div v-if="user" class="flex flex-col gap-2">
<Avatar
v-if="user.photoURL"
:image="user.photoURL"
<!-- Normal User Panel -->
<div v-if="isLoggedIn" class="flex flex-col gap-2">
<UserAvatar
v-if="userPhotoUrl"
:photo-url="userPhotoUrl"
shape="circle"
size="large"
aria-label="User Avatar"
/>
<div class="flex flex-col gap-0.5">
@@ -18,7 +18,7 @@
{{ $t('userSettings.name') }}
</h3>
<div class="text-muted">
{{ user.displayName || $t('userSettings.notSet') }}
{{ userDisplayName || $t('userSettings.notSet') }}
</div>
</div>
@@ -26,8 +26,8 @@
<h3 class="font-medium">
{{ $t('userSettings.email') }}
</h3>
<a :href="'mailto:' + user.email" class="hover:underline">
{{ user.email }}
<a :href="'mailto:' + userEmail" class="hover:underline">
{{ userEmail }}
</a>
</div>
@@ -87,55 +87,26 @@
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
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 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')
}
const {
loading,
isLoggedIn,
isEmailProvider,
userDisplayName,
userEmail,
userPhotoUrl,
providerName,
providerIcon,
handleSignOut,
handleSignIn
} = useCurrentUser()
</script>

View File

@@ -0,0 +1,114 @@
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 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'
}
},
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(
'https://platform.comfy.org/login'
)
})
})

View File

@@ -0,0 +1,111 @@
<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="https://platform.comfy.org/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 { 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

@@ -1,6 +1,11 @@
<!-- A dialog header with ComfyOrg logo -->
<template>
<div class="px-2 py-4">
<img src="/assets/images/Comfy_Logo_x32.png" alt="ComfyOrg Logo" />
<img
src="/assets/images/comfy-logo-single.svg"
alt="ComfyOrg Logo"
width="32"
height="32"
/>
</div>
</template>

View File

@@ -52,7 +52,7 @@
<template #content>
<div class="flex items-center px-4 py-3">
<div class="flex-1 flex flex-col">
<h3 class="line-clamp-2 text-lg font-normal mb-0 h-10" :title="title">
<h3 class="line-clamp-2 text-lg font-normal mb-0 h-12" :title="title">
{{ title }}
</h3>
<p class="line-clamp-2 text-sm text-muted grow" :title="description">
@@ -79,15 +79,13 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import { st } from '@/i18n'
import { api } from '@/scripts/api'
import { TemplateInfo } from '@/types/workflowTemplateTypes'
import { normalizeI18nKey } from '@/utils/formatUtil'
const UPSCALE_ZOOM_SCALE = 16 // for upscale templates, exaggerate the hover zoom
const DEFAULT_ZOOM_SCALE = 5
const { sourceModule, categoryTitle, loading, template } = defineProps<{
const { sourceModule, loading, template } = defineProps<{
sourceModule: string
categoryTitle: string
loading: boolean
@@ -116,17 +114,17 @@ const overlayThumbnailSrc = computed(() =>
getThumbnailUrl(sourceModule === 'default' ? '2' : '')
)
const title = computed(() => {
const fallback = template.title ?? template.name ?? `${sourceModule} Template`
const description = computed(() => {
return sourceModule === 'default'
? st(
`templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
fallback
)
: fallback
? template.localizedDescription ?? ''
: template.description.replace(/[-_]/g, ' ').trim()
})
const description = computed(() => template.description.replace(/[-_]/g, ' '))
const title = computed(() => {
return sourceModule === 'default'
? template.localizedTitle ?? ''
: template.name
})
defineEmits<{
loadWorkflow: [name: string]

View File

@@ -0,0 +1,70 @@
<template>
<DataTable
v-model:selection="selectedTemplate"
:value="templates"
striped-rows
selection-mode="single"
>
<Column field="title" :header="$t('g.title')">
<template #body="slotProps">
<span :title="getTemplateTitle(slotProps.data)">{{
getTemplateTitle(slotProps.data)
}}</span>
</template>
</Column>
<Column field="description" :header="$t('g.description')">
<template #body="slotProps">
<span :title="getTemplateDescription(slotProps.data)">
{{ getTemplateDescription(slotProps.data) }}
</span>
</template>
</Column>
<Column field="actions" header="" class="w-12">
<template #body="slotProps">
<Button
icon="pi pi-arrow-right"
text
rounded
size="small"
:loading="loading === slotProps.data.name"
@click="emit('loadWorkflow', slotProps.data.name)"
/>
</template>
</Column>
</DataTable>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import { ref } from 'vue'
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
const { sourceModule, loading, templates } = defineProps<{
sourceModule: string
categoryTitle: string
loading: string | null
templates: TemplateInfo[]
}>()
const selectedTemplate = ref(null)
const emit = defineEmits<{
loadWorkflow: [name: string]
}>()
const getTemplateTitle = (template: TemplateInfo) => {
const fallback = template.title ?? template.name ?? `${sourceModule} Template`
return sourceModule === 'default'
? template.localizedTitle ?? fallback
: fallback
}
const getTemplateDescription = (template: TemplateInfo) => {
return sourceModule === 'default'
? template.localizedDescription ?? ''
: template.description.replace(/[-_]/g, ' ').trim()
}
</script>

View File

@@ -0,0 +1,82 @@
<template>
<DataView
:value="templates"
:layout="layout"
data-key="name"
:lazy="true"
pt:root="h-full grid grid-rows-[auto_1fr]"
pt:content="p-2 overflow-auto"
>
<template #header>
<div class="flex justify-between items-center">
<h2 class="text-lg">{{ title }}</h2>
<SelectButton
v-model="layout"
:options="['grid', 'list']"
:allow-empty="false"
>
<template #option="{ option }">
<i :class="[option === 'list' ? 'pi pi-bars' : 'pi pi-table']" />
</template>
</SelectButton>
</div>
</template>
<template #list="{ items }">
<TemplateWorkflowList
:source-module="sourceModule"
:templates="items"
:loading="loading"
:category-title="categoryTitle"
@load-workflow="onLoadWorkflow"
/>
</template>
<template #grid="{ items }">
<div
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] auto-rows-fr gap-8 justify-items-center"
>
<TemplateWorkflowCard
v-for="template in items"
:key="template.name"
:source-module="sourceModule"
:template="template"
:loading="loading === template.name"
:category-title="categoryTitle"
@load-workflow="onLoadWorkflow"
/>
</div>
</template>
</DataView>
</template>
<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
import DataView from 'primevue/dataview'
import SelectButton from 'primevue/selectbutton'
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
import TemplateWorkflowList from '@/components/templates/TemplateWorkflowList.vue'
import type { TemplateInfo } from '@/types/workflowTemplateTypes'
defineProps<{
title: string
sourceModule: string
categoryTitle: string
loading: string | null
templates: TemplateInfo[]
}>()
const layout = useLocalStorage<'grid' | 'list'>(
'Comfy.TemplateWorkflow.Layout',
'grid'
)
const emit = defineEmits<{
loadWorkflow: [name: string]
}>()
const onLoadWorkflow = (name: string) => {
emit('loadWorkflow', name)
}
</script>

View File

@@ -30,36 +30,22 @@
/>
</aside>
<div
class="flex-1 overflow-auto transition-all duration-300"
class="flex-1 transition-all duration-300"
:class="{
'pl-80': isSideNavOpen || !isSmallScreen,
'pl-8': !isSideNavOpen && isSmallScreen
}"
>
<div v-if="isReady && selectedTab" class="flex flex-col px-12 pb-4">
<div class="py-3 text-left">
<h2 class="text-lg">
{{ selectedTab.title }}
</h2>
</div>
<div
class="grid grid-cols-[repeat(auto-fill,minmax(16rem,1fr))] auto-rows-fr gap-8 justify-items-center"
>
<div
v-for="template in selectedTab.templates"
:key="template.name"
class="h-full"
>
<TemplateWorkflowCard
:source-module="selectedTab.moduleName"
:template="template"
:loading="template.name === workflowLoading"
:category-title="selectedTab.title"
@load-workflow="loadWorkflow"
/>
</div>
</div>
</div>
<TemplateWorkflowView
v-if="isReady && selectedTab"
class="px-12 py-4"
:title="selectedTab.title"
:source-module="selectedTab.moduleName"
:templates="selectedTab.templates"
:loading="workflowLoading"
:category-title="selectedTab.title"
@load-workflow="loadWorkflow"
/>
</div>
</div>
</div>
@@ -73,7 +59,7 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import TemplateWorkflowCard from '@/components/templates/TemplateWorkflowCard.vue'
import TemplateWorkflowView from '@/components/templates/TemplateWorkflowView.vue'
import TemplateWorkflowsSideNav from '@/components/templates/TemplateWorkflowsSideNav.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { api } from '@/scripts/api'

View File

@@ -4,7 +4,7 @@
:model-value="selectedTab"
:options="tabs"
option-group-label="label"
option-label="title"
option-label="localizedTitle"
option-group-children="modules"
class="w-full border-0 bg-transparent shadow-none"
:pt="{

View File

@@ -2,7 +2,7 @@
<template>
<div>
<Button
v-if="isAuthenticated"
v-if="isLoggedIn"
class="user-profile-button p-1"
severity="secondary"
text
@@ -12,12 +12,7 @@
<div
class="flex items-center rounded-full bg-[var(--p-content-background)]"
>
<Avatar
:image="photoURL"
:icon="photoURL ? undefined : 'pi pi-user'"
shape="circle"
aria-label="User Avatar"
/>
<UserAvatar :photo-url="photoURL" />
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
</div>
@@ -30,20 +25,19 @@
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import UserAvatar from '@/components/common/UserAvatar.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import CurrentUserPopover from './CurrentUserPopover.vue'
const authStore = useFirebaseAuthStore()
const { isLoggedIn, userPhotoUrl } = useCurrentUser()
const popover = ref<InstanceType<typeof Popover> | null>(null)
const isAuthenticated = computed(() => authStore.isAuthenticated)
const photoURL = computed<string | undefined>(
() => authStore.currentUser?.photoURL ?? undefined
() => userPhotoUrl.value ?? undefined
)
</script>

View File

@@ -4,21 +4,21 @@
<!-- User Info Section -->
<div class="p-3">
<div class="flex flex-col items-center">
<Avatar
<UserAvatar
class="mb-3"
:image="user?.photoURL ?? undefined"
:icon="user?.photoURL ? undefined : 'pi pi-user !text-2xl'"
shape="circle"
:photo-url="userPhotoUrl"
:pt:icon:class="{
'!text-2xl': !userPhotoUrl
}"
size="large"
aria-label="User Avatar"
/>
<!-- User Details -->
<h3 class="text-lg font-semibold truncate my-0 mb-1">
{{ user?.displayName || $t('g.user') }}
{{ userDisplayName || $t('g.user') }}
</h3>
<p v-if="user?.email" class="text-sm text-muted truncate my-0">
{{ user.email }}
<p v-if="userEmail" class="text-sm text-muted truncate my-0">
{{ userEmail }}
</p>
</div>
</div>
@@ -37,6 +37,18 @@
<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') }}
@@ -50,22 +62,20 @@
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { computed, onMounted } from 'vue'
import { 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 authStore = useFirebaseAuthStore()
const { userDisplayName, userEmail, userPhotoUrl } = useCurrentUser()
const authService = useFirebaseAuthService()
const dialogService = useDialogService()
const user = computed(() => authStore.currentUser)
const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user')
}
@@ -74,6 +84,10 @@ const handleTopUp = () => {
dialogService.showTopUpCreditsDialog()
}
const handleOpenApiPricing = () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank')
}
onMounted(() => {
void authService.fetchBalance()
})

View File

@@ -5,7 +5,11 @@
class="comfyui-menu flex items-center"
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
>
<h1 class="comfyui-logo mx-2 app-drag">ComfyUI</h1>
<img
src="/assets/images/comfy-logo-mono.svg"
alt="ComfyUI Logo"
class="comfyui-logo ml-2 app-drag h-6"
/>
<CommandMenubar />
<div class="flex-grow min-w-0 app-drag h-full">
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
@@ -126,8 +130,12 @@ onMounted(() => {
}
.comfyui-logo {
font-size: 1.2em;
user-select: none;
cursor: default;
filter: invert(0);
}
.dark-theme .comfyui-logo {
filter: invert(1);
}
</style>

View File

@@ -0,0 +1,101 @@
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

@@ -7,13 +7,14 @@ 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
@@ -29,7 +30,7 @@ export function useSettingUI(
| 'credits'
) {
const { t } = useI18n()
const firebaseAuthStore = useFirebaseAuthStore()
const { isLoggedIn } = useCurrentUser()
const settingStore = useSettingStore()
const activeCategory = ref<SettingTreeNode | null>(null)
@@ -165,7 +166,7 @@ export function useSettingUI(
label: 'Account',
children: [
userPanel.node,
...(firebaseAuthStore.isAuthenticated ? [creditsPanel.node] : [])
...(isLoggedIn.value ? [creditsPanel.node] : [])
].map(translateCategory)
},
// Normal settings stored in the settingStore

View File

@@ -113,7 +113,8 @@
"login": "Login",
"learnMore": "Learn more",
"amount": "Amount",
"unknownError": "Unknown error"
"unknownError": "Unknown error",
"title": "Title"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -484,7 +485,111 @@
"Image": "Image",
"Area Composition": "Area Composition",
"3D": "3D",
"Audio": "Audio"
"Audio": "Audio",
"Image API": "Image API",
"Video API": "Video API"
},
"templateDescription": {
"Basics": {
"default": "Generate images from text descriptions.",
"image2image": "Transform existing images using text prompts.",
"lora": "Apply LoRA models for specialized styles or subjects.",
"inpaint_example": "Edit specific parts of images seamlessly.",
"inpain_model_outpainting": "Extend images beyond their original boundaries.",
"embedding_example": "Use textual inversion for consistent styles.",
"gligen_textbox_example": "Specify the location and size of objects.",
"lora_multiple": "Combine multiple LoRA models for unique results."
},
"Flux": {
"flux_dev_checkpoint_example": "Create images using Flux development models.",
"flux_schnell": "Generate images quickly with Flux Schnell.",
"flux_fill_inpaint_example": "Fill in missing parts of images.",
"flux_fill_outpaint_example": "Extend images using Flux outpainting.",
"flux_canny_model_example": "Generate images from edge detection.",
"flux_depth_lora_example": "Create images with depth-aware LoRA.",
"flux_redux_model_example": "Transfer style from a reference image to guide image generation with Flux."
},
"Image": {
"hidream_i1_dev": "Generate images with HiDream I1 Dev.",
"hidream_i1_fast": "Generate images quickly with HiDream I1.",
"hidream_i1_full": "Generate images with HiDream I1.",
"hidream_e1_full": "Edit images with HiDream E1.",
"sd3_5_simple_example": "Generate images with SD 3.5.",
"sd3_5_large_canny_controlnet_example": "Use edge detection to guide image generation with SD 3.5.",
"sd3_5_large_depth": "Create depth-aware images with SD 3.5.",
"sd3_5_large_blur": "Generate images from blurred reference images with SD 3.5.",
"sdxl_simple_example": "Create high-quality images with SDXL.",
"sdxl_refiner_prompt_example": "Enhance SDXL outputs with refiners.",
"sdxl_revision_text_prompts": "Transfer concepts from reference images to guide image generation with SDXL.",
"sdxl_revision_zero_positive": "Add text prompts alongside reference images to guide image generation with SDXL.",
"sdxlturbo_example": "Generate images in a single step with SDXL Turbo."
},
"Video": {
"text_to_video_wan": "Quickly Generate videos from text descriptions.",
"image_to_video_wan": "Quickly Generate videos from images.",
"wan2_1_fun_inp": "Create videos from start and end frames.",
"wan2_1_fun_control": "Guide video generation with pose, depth, edge controls and more.",
"wan2_1_flf2v_720_f16": "Generate video through controlling the first and last frames.",
"ltxv_text_to_video": "Generate videos from text descriptions.",
"ltxv_image_to_video": "Convert still images into videos.",
"mochi_text_to_video_example": "Create videos with Mochi model.",
"hunyuan_video_text_to_video": "Generate videos using Hunyuan model.",
"image_to_video": "Transform images into animated videos.",
"txt_to_image_to_video": "Generate images from text and then convert them into videos."
},
"Image API": {
"api_openai_image_1_t2i": "Use GPT Image 1 API to generate images from text descriptions.",
"api_openai_image_1_i2i": "Use GPT Image 1 API to generate images from images.",
"api_openai_image_1_inpaint": "Use GPT Image 1 API to inpaint images.",
"api_openai_image_1_multi_inputs": "Use GPT Image 1 API with multiple inputs to generate images.",
"api-openai-dall-e-2-t2i": "Use Dall-E 2 API to generate images from text descriptions.",
"api-openai-dall-e-2-inpaint": "Use Dall-E 2 API to inpaint images.",
"api-openai-dall-e-3-t2i": "Use Dall-E 3 API to generate images from text descriptions.",
"api_bfl_flux_pro_t2i": "Create images with FLUX.1 [pro]'s excellent prompt following, visual quality, image detail and output diversity.",
"api_stability_sd3_t2i": "Generate high quality images with excellent prompt adherence. Perfect for professional use cases at 1 megapixel resolution.",
"api_ideogram_v3_t2i": "Generate images with high-quality image-prompt alignment, photorealism, and text rendering. Create professional-quality logos, promotional posters, landing page concepts, product photography, and more. Effortlessly craft sophisticated spatial compositions with intricate backgrounds, precise and nuanced lighting and colors, and lifelike environmental detail.",
"api_luma_photon_i2i": "Guide image generation using a combination of images and prompt.",
"api_luma_photon_style_ref": "Apply and blend style references with exact control. Luma Photon captures the essence of each reference image, letting you combine distinct visual elements while maintaining professional quality.",
"api_recraft_image_gen_with_color_control": "Create a custom palette to reuse for multiple images or hand-pick colors for each photo. Match your brand's color palette and craft visuals that are distinctly yours.",
"api_recraft_image_gen_with_style_control": "Control style with visual examples, align positioning, and fine-tune objects. Store and share styles for perfect brand consistency.",
"api_recraft_vector_gen": "Go from a text prompt to vector image with Recraft's AI vector generator. Produce the best-quality vector art for logos, posters, icon sets, ads, banners and mockups. Perfect your designs with sharp, high-quality SVG files. Create branded vector illustrations for your app or website in seconds."
},
"Video API": {
"api_luma_i2v": "Take static images and instantly create magical high quality animations.",
"api_kling_i2v": "Create videos with great prompt adherence for actions, expressions, and camera movements. Now supporting complex prompts with sequential actions for you to be the director of your scene.",
"api_veo2_i2v": "Use Google Veo2 API to generate videos from images.",
"api_hailuo_minimax_i2v": "Create refined videos from images and text, including CGI integration and trendy photo effects like viral AI hugging. Choose from a variety of video styles and themes to match your creative vision.",
"api_pika_scene": "Use multiple images as ingredients and generate videos that incorporate all of them.",
"api_pixverse_template_i2v": "Transforms static images into dynamic videos with motion and effects.",
"api_pixverse_t2v": "Generate videos with accurate prompt interpretation and stunning video dynamics."
},
"Upscaling": {
"hiresfix_latent_workflow": "Enhance image quality in latent space.",
"esrgan_example": "Use upscale models to enhance image quality.",
"hiresfix_esrgan_workflow": "Use upscale models during intermediate steps.",
"latent_upscale_different_prompt_model": "Upscale and change prompt across passes."
},
"ControlNet": {
"controlnet_example": "Control image generation with reference images.",
"2_pass_pose_worship": "Generate images from pose references.",
"depth_controlnet": "Create images with depth-aware generation.",
"depth_t2i_adapter": "Quickly generate depth-aware images with a T2I adapter.",
"mixing_controlnets": "Combine multiple ControlNet models together."
},
"Area Composition": {
"area_composition": "Control image composition with areas.",
"area_composition_reversed": "Reverse area composition workflow.",
"area_composition_square_area_for_subject": "Create consistent subject placement."
},
"3D": {
"hunyuan3d-non-multiview-train": "Use Hunyuan3D 2.0 to generate models from a single view.",
"hunyuan-3d-multiview-elf": " Use Hunyuan3D 2mv to generate models from multiple views.",
"hunyuan-3d-turbo": "Use Hunyuan3D 2mv turbo to generate models from multiple views.",
"stable_zero123_example": "Generate 3D views from single images."
},
"Audio": {
"stable_audio_example": "Generate audio from text descriptions."
}
},
"template": {
"Flux": {
@@ -539,8 +644,8 @@
"api-openai-dall-e-2-t2i": "Dall-E 2 Text to Image",
"api-openai-dall-e-2-inpaint": "Dall-E 2 Inpaint",
"api-openai-dall-e-3-t2i": "Dall-E 3 Text to Image",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] Text to Image",
"api_stability_sd3_t2i": "Stability SD3 Text to Image",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra Text to Image",
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra Text to Image",
"api_ideogram_v3_t2i": "Ideogram V3 Text to Image",
"api_luma_photon_i2i": "Luma Photon Image to Image",
"api_luma_photon_style_ref": "Luma Photon Style Reference",
@@ -554,7 +659,7 @@
"api_veo2_i2v": "Veo2 Image to Video",
"api_hailuo_minimax_i2v": "MiniMax Image to Video",
"api_pika_scene": "Pika Scenes: Images to Video",
"api_pixverse_template_i2v": "PixVerse Templates: Image to Video",
"api_pixverse_template_i2v": "PixVerse Template Effects: Image to Video",
"api_pixverse_t2v": "PixVerse Text to Video"
},
"Image": {
@@ -916,6 +1021,7 @@
"image": "image",
"preprocessors": "preprocessors",
"advanced": "advanced",
"guidance": "guidance",
"loaders": "loaders",
"model_merging": "model_merging",
"attention_experiments": "attention_experiments",
@@ -928,44 +1034,64 @@
"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",
"guidance": "guidance",
"Stability AI": "Stability AI",
"stable_cascade": "stable_cascade",
"3d_models": "3d_models",
"style_model": "style_model"
"style_model": "style_model",
"sd": "sd",
"Veo": "Veo"
},
"dataTypes": {
"*": "*",
"AUDIO": "AUDIO",
"BOOLEAN": "BOOLEAN",
"CAMERA_CONTROL": "CAMERA_CONTROL",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
@@ -984,18 +1110,27 @@
"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"
},
@@ -1115,13 +1250,33 @@
"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",
"passwordResetSentDetail": "Please check your email for a link to reset your password.",
"newUser": "New here?",
"userAvatar": "User Avatar",
"signUp": "Sign up",
"emailLabel": "Email",
"emailPlaceholder": "Enter your email",
@@ -1141,7 +1296,8 @@
"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"
"questionsContactPrefix": "Questions? Contact us at",
"noAssociatedUser": "There is no Comfy user associated with the provided API key"
},
"signup": {
"title": "Create an account",
@@ -1172,6 +1328,8 @@
"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",
@@ -1188,6 +1346,7 @@
"yourCreditBalance": "Your credit balance",
"purchaseCredits": "Purchase Credits",
"invoiceHistory": "Invoice History",
"apiPricing": "API Pricing",
"faqs": "FAQs",
"messageSupport": "Message Support",
"lastUpdated": "Last updated",

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,24 @@
"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",
@@ -43,6 +61,7 @@
"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",
@@ -55,7 +74,9 @@
"success": "Inicio de sesión exitoso",
"termsLink": "Términos de uso",
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
"title": "Inicia sesión en tu cuenta"
"title": "Inicia sesión en tu cuenta",
"useApiKey": "Clave API de Comfy",
"userAvatar": "Avatar de usuario"
},
"passwordUpdate": {
"success": "Contraseña actualizada",
@@ -130,6 +151,7 @@
"Unpin": "Desanclar"
},
"credits": {
"apiPricing": "Precios de la API",
"credits": "Créditos",
"faqs": "Preguntas frecuentes",
"invoiceHistory": "Historial de facturas",
@@ -148,8 +170,10 @@
"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",
@@ -166,20 +190,29 @@
"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"
},
@@ -325,6 +358,7 @@
"success": "Éxito",
"systemInfo": "Información del sistema",
"terminal": "Terminal",
"title": "Título",
"unknownError": "Error desconocido",
"update": "Actualizar",
"updateAvailable": "Actualización Disponible",
@@ -720,10 +754,22 @@
"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",
@@ -748,6 +794,7 @@
"instructpix2pix": "instruirpix2pix",
"latent": "latent",
"loaders": "cargadores",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "mask",
"model": "modelo",
@@ -759,10 +806,12 @@
"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",
@@ -771,6 +820,10 @@
"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"
},
@@ -1052,8 +1105,10 @@
"Custom Nodes": "Nodos Personalizados",
"Flux": "Flux",
"Image": "Imagen",
"Image API": "API de Imagen",
"Upscaling": "Ampliación",
"Video": "Video"
"Video": "Video",
"Video API": "API de Video"
},
"template": {
"3D": {
@@ -1114,7 +1169,7 @@
"api-openai-dall-e-2-inpaint": "Dall-E 2 Rellenar",
"api-openai-dall-e-2-t2i": "Dall-E 2 Texto a Imagen",
"api-openai-dall-e-3-t2i": "Dall-E 3 Texto a Imagen",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] Texto a Imagen",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra Texto a Imagen",
"api_ideogram_v3_t2i": "Ideogram V3 Texto a Imagen",
"api_luma_photon_i2i": "Luma Photon Imagen a Imagen",
"api_luma_photon_style_ref": "Luma Photon Referencia de Estilo",
@@ -1125,7 +1180,7 @@
"api_recraft_image_gen_with_color_control": "Recraft Generación de Imagen con Control de Color",
"api_recraft_image_gen_with_style_control": "Recraft Generación de Imagen con Control de Estilo",
"api_recraft_vector_gen": "Recraft Generación de Vectores",
"api_stability_sd3_t2i": "Stability SD3 Texto a Imagen"
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra Texto a Imagen"
},
"Upscaling": {
"esrgan_example": "ESRGAN",
@@ -1151,10 +1206,112 @@
"api_luma_i2v": "Luma Imagen a Video",
"api_pika_scene": "Pika Escenas: Imágenes a Video",
"api_pixverse_t2v": "PixVerse Texto a Video",
"api_pixverse_template_i2v": "PixVerse Plantillas: Imagen a Video",
"api_pixverse_template_i2v": "PixVerse Template Effects: Imagen a Video",
"api_veo2_i2v": "Veo2 Imagen a Video"
}
},
"templateDescription": {
"3D": {
"hunyuan-3d-multiview-elf": "Usa Hunyuan3D 2mv para generar modelos desde múltiples vistas.",
"hunyuan-3d-turbo": "Usa Hunyuan3D 2mv turbo para generar modelos desde múltiples vistas.",
"hunyuan3d-non-multiview-train": "Usa Hunyuan3D 2.0 para generar modelos desde una sola vista.",
"stable_zero123_example": "Genera vistas 3D a partir de imágenes individuales."
},
"Area Composition": {
"area_composition": "Controla la composición de la imagen por áreas.",
"area_composition_reversed": "Invierte el flujo de composición por áreas.",
"area_composition_square_area_for_subject": "Crea una colocación consistente del sujeto."
},
"Audio": {
"stable_audio_example": "Genera audio a partir de descripciones de texto."
},
"Basics": {
"default": "Genera imágenes a partir de descripciones de texto.",
"embedding_example": "Utiliza inversión textual para estilos consistentes.",
"gligen_textbox_example": "Especifica la ubicación y el tamaño de los objetos.",
"image2image": "Transforma imágenes existentes usando indicaciones de texto.",
"inpain_model_outpainting": "Extiende imágenes más allá de sus límites originales.",
"inpaint_example": "Edita partes específicas de imágenes de manera fluida.",
"lora": "Aplica modelos LoRA para estilos o temas especializados.",
"lora_multiple": "Combina múltiples modelos LoRA para resultados únicos."
},
"ControlNet": {
"2_pass_pose_worship": "Genera imágenes a partir de referencias de pose.",
"controlnet_example": "Controla la generación de imágenes con imágenes de referencia.",
"depth_controlnet": "Crea imágenes con generación consciente de profundidad.",
"depth_t2i_adapter": "Genera rápidamente imágenes conscientes de profundidad con un adaptador T2I.",
"mixing_controlnets": "Combina múltiples modelos ControlNet juntos."
},
"Flux": {
"flux_canny_model_example": "Genera imágenes a partir de detección de bordes.",
"flux_depth_lora_example": "Crea imágenes con LoRA consciente de profundidad.",
"flux_dev_checkpoint_example": "Crea imágenes usando modelos de desarrollo de Flux.",
"flux_fill_inpaint_example": "Rellena partes faltantes de imágenes.",
"flux_fill_outpaint_example": "Extiende imágenes usando outpainting de Flux.",
"flux_redux_model_example": "Transfiere el estilo de una imagen de referencia para guiar la generación de imágenes con Flux.",
"flux_schnell": "Genera imágenes rápidamente con Flux Schnell."
},
"Image": {
"hidream_e1_full": "Edita imágenes con HiDream E1.",
"hidream_i1_dev": "Genera imágenes con HiDream I1 Dev.",
"hidream_i1_fast": "Genera imágenes rápidamente con HiDream I1.",
"hidream_i1_full": "Genera imágenes con HiDream I1.",
"sd3_5_large_blur": "Genera imágenes a partir de imágenes de referencia borrosas con SD 3.5.",
"sd3_5_large_canny_controlnet_example": "Usa detección de bordes para guiar la generación de imágenes con SD 3.5.",
"sd3_5_large_depth": "Crea imágenes conscientes de profundidad con SD 3.5.",
"sd3_5_simple_example": "Genera imágenes con SD 3.5.",
"sdxl_refiner_prompt_example": "Mejora los resultados de SDXL con refinadores.",
"sdxl_revision_text_prompts": "Transfiere conceptos de imágenes de referencia para guiar la generación de imágenes con SDXL.",
"sdxl_revision_zero_positive": "Agrega indicaciones de texto junto a imágenes de referencia para guiar la generación de imágenes con SDXL.",
"sdxl_simple_example": "Crea imágenes de alta calidad con SDXL.",
"sdxlturbo_example": "Genera imágenes en un solo paso con SDXL Turbo."
},
"Image API": {
"api-openai-dall-e-2-inpaint": "Usa la API Dall-E 2 para hacer inpainting en imágenes.",
"api-openai-dall-e-2-t2i": "Usa la API Dall-E 2 para generar imágenes a partir de descripciones de texto.",
"api-openai-dall-e-3-t2i": "Usa la API Dall-E 3 para generar imágenes a partir de descripciones de texto.",
"api_bfl_flux_pro_t2i": "Crea imágenes con FLUX.1 [pro] y su excelente seguimiento de indicaciones, calidad visual, detalle de imagen y diversidad de resultados.",
"api_ideogram_v3_t2i": "Genera imágenes con alineación de indicaciones de alta calidad, fotorrealismo y renderizado de texto. Crea logotipos de calidad profesional, carteles promocionales, conceptos de páginas de destino, fotografía de productos y más. Crea composiciones espaciales sofisticadas con fondos intrincados, iluminación y colores precisos y matizados, y detalles ambientales realistas.",
"api_luma_photon_i2i": "Guía la generación de imágenes usando una combinación de imágenes e indicaciones.",
"api_luma_photon_style_ref": "Aplica y combina referencias de estilo con control exacto. Luma Photon captura la esencia de cada imagen de referencia, permitiéndote combinar elementos visuales distintos manteniendo calidad profesional.",
"api_openai_image_1_i2i": "Usa la API GPT Image 1 para generar imágenes a partir de imágenes.",
"api_openai_image_1_inpaint": "Usa la API GPT Image 1 para hacer inpainting en imágenes.",
"api_openai_image_1_multi_inputs": "Usa la API GPT Image 1 con múltiples entradas para generar imágenes.",
"api_openai_image_1_t2i": "Usa la API GPT Image 1 para generar imágenes a partir de descripciones de texto.",
"api_recraft_image_gen_with_color_control": "Crea una paleta personalizada para reutilizar en múltiples imágenes o selecciona colores para cada foto. Haz coincidir la paleta de tu marca y crea imágenes visuales que sean distintivamente tuyas.",
"api_recraft_image_gen_with_style_control": "Controla el estilo con ejemplos visuales, alinea la posición y ajusta objetos. Guarda y comparte estilos para una consistencia perfecta de marca.",
"api_recraft_vector_gen": "Pasa de una indicación de texto a una imagen vectorial con el generador de vectores IA de Recraft. Produce arte vectorial de la mejor calidad para logotipos, carteles, conjuntos de iconos, anuncios, banners y maquetas. Perfecciona tus diseños con archivos SVG nítidos y de alta calidad. Crea ilustraciones vectoriales de marca para tu app o sitio web en segundos.",
"api_stability_sd3_t2i": "Genera imágenes de alta calidad con excelente adherencia a las indicaciones. Perfecto para casos de uso profesional a resolución de 1 megapíxel."
},
"Upscaling": {
"esrgan_example": "Usa modelos de escalado para mejorar la calidad de imagen.",
"hiresfix_esrgan_workflow": "Usa modelos de escalado durante pasos intermedios.",
"hiresfix_latent_workflow": "Mejora la calidad de imagen en el espacio latente.",
"latent_upscale_different_prompt_model": "Escala y cambia la indicación entre pasadas."
},
"Video": {
"hunyuan_video_text_to_video": "Genera videos usando el modelo Hunyuan.",
"image_to_video": "Transforma imágenes en videos animados.",
"image_to_video_wan": "Genera videos rápidamente a partir de imágenes.",
"ltxv_image_to_video": "Convierte imágenes fijas en videos.",
"ltxv_text_to_video": "Genera videos a partir de descripciones de texto.",
"mochi_text_to_video_example": "Crea videos con el modelo Mochi.",
"text_to_video_wan": "Genera videos rápidamente a partir de descripciones de texto.",
"txt_to_image_to_video": "Genera imágenes a partir de texto y luego conviértelas en videos.",
"wan2_1_flf2v_720_f16": "Genera video controlando el primer y último fotograma.",
"wan2_1_fun_control": "Guía la generación de video con pose, profundidad, controles de bordes y más.",
"wan2_1_fun_inp": "Crea videos a partir de fotogramas iniciales y finales."
},
"Video API": {
"api_hailuo_minimax_i2v": "Crea videos refinados a partir de imágenes y texto, incluyendo integración CGI y efectos fotográficos de tendencia como abrazos virales de IA. Elige entre una variedad de estilos y temas de video para que coincidan con tu visión creativa.",
"api_kling_i2v": "Crea videos con gran adherencia a las indicaciones para acciones, expresiones y movimientos de cámara. Ahora soporta indicaciones complejas con acciones secuenciales para que seas el director de tu escena.",
"api_luma_i2v": "Convierte imágenes estáticas en animaciones mágicas de alta calidad al instante.",
"api_pika_scene": "Usa múltiples imágenes como ingredientes y genera videos que las incorporen todas.",
"api_pixverse_t2v": "Genera videos con interpretación precisa de indicaciones y una dinámica visual impresionante.",
"api_pixverse_template_i2v": "Transforma imágenes estáticas en videos dinámicos con movimiento y efectos.",
"api_veo2_i2v": "Usa la API Google Veo2 para generar videos a partir de imágenes."
}
},
"title": "Comienza con una Plantilla"
},
"toastMessages": {
@@ -1206,6 +1363,7 @@
},
"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": {
@@ -1218,6 +1376,7 @@
"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

@@ -29,6 +29,24 @@
"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",
@@ -43,6 +61,7 @@
"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",
@@ -55,7 +74,9 @@
"success": "Connexion réussie",
"termsLink": "Conditions d'utilisation",
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
"title": "Connectez-vous à votre compte"
"title": "Connectez-vous à votre compte",
"useApiKey": "Clé API Comfy",
"userAvatar": "Avatar utilisateur"
},
"passwordUpdate": {
"success": "Mot de passe mis à jour",
@@ -130,6 +151,7 @@
"Unpin": "Désépingler"
},
"credits": {
"apiPricing": "Tarification de lAPI",
"credits": "Crédits",
"faqs": "FAQ",
"invoiceHistory": "Historique des factures",
@@ -148,8 +170,10 @@
"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",
@@ -166,20 +190,29 @@
"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"
},
@@ -325,6 +358,7 @@
"success": "Succès",
"systemInfo": "Informations système",
"terminal": "Terminal",
"title": "Titre",
"unknownError": "Erreur inconnue",
"update": "Mettre à jour",
"updateAvailable": "Mise à jour disponible",
@@ -720,10 +754,22 @@
"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",
@@ -748,6 +794,7 @@
"instructpix2pix": "instructpix2pix",
"latent": "latent",
"loaders": "chargeurs",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "masque",
"model": "modèle",
@@ -759,10 +806,12 @@
"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",
@@ -771,6 +820,10 @@
"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"
},
@@ -1052,8 +1105,10 @@
"Custom Nodes": "Nœuds personnalisés",
"Flux": "Flux",
"Image": "Image",
"Image API": "API d'image",
"Upscaling": "Mise à l'échelle",
"Video": "Vidéo"
"Video": "Vidéo",
"Video API": "API vidéo"
},
"template": {
"3D": {
@@ -1114,7 +1169,7 @@
"api-openai-dall-e-2-inpaint": "Dall-E 2 Inpainting",
"api-openai-dall-e-2-t2i": "Dall-E 2 Texte vers Image",
"api-openai-dall-e-3-t2i": "Dall-E 3 Texte vers Image",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] Texte vers Image",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra Texte vers Image",
"api_ideogram_v3_t2i": "Ideogram V3 Texte vers Image",
"api_luma_photon_i2i": "Luma Photon Image vers Image",
"api_luma_photon_style_ref": "Luma Photon Référence de Style",
@@ -1125,7 +1180,7 @@
"api_recraft_image_gen_with_color_control": "Recraft Génération dImage avec Contrôle des Couleurs",
"api_recraft_image_gen_with_style_control": "Recraft Génération dImage avec Contrôle du Style",
"api_recraft_vector_gen": "Recraft Génération de Vecteur",
"api_stability_sd3_t2i": "Stability SD3 Texte vers Image"
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra Texte vers Image"
},
"Upscaling": {
"esrgan_example": "ESRGAN",
@@ -1151,10 +1206,112 @@
"api_luma_i2v": "Luma Image vers Vidéo",
"api_pika_scene": "Pika Scènes : Images vers Vidéo",
"api_pixverse_t2v": "PixVerse Texte vers Vidéo",
"api_pixverse_template_i2v": "PixVerse Modèles : Image vers Vidéo",
"api_pixverse_template_i2v": "PixVerse Template Effects: Image vers Vidéo",
"api_veo2_i2v": "Veo2 Image vers Vidéo"
}
},
"templateDescription": {
"3D": {
"hunyuan-3d-multiview-elf": "Utilisez Hunyuan3D 2mv pour générer des modèles à partir de plusieurs vues.",
"hunyuan-3d-turbo": "Utilisez Hunyuan3D 2mv turbo pour générer des modèles à partir de plusieurs vues.",
"hunyuan3d-non-multiview-train": "Utilisez Hunyuan3D 2.0 pour générer des modèles à partir d'une seule vue.",
"stable_zero123_example": "Générez des vues 3D à partir d'images uniques."
},
"Area Composition": {
"area_composition": "Contrôlez la composition d'image avec des zones.",
"area_composition_reversed": "Inversez le workflow de composition de zones.",
"area_composition_square_area_for_subject": "Créez un placement cohérent du sujet."
},
"Audio": {
"stable_audio_example": "Générez de l'audio à partir de descriptions textuelles."
},
"Basics": {
"default": "Générez des images à partir de descriptions textuelles.",
"embedding_example": "Utilisez l'inversion textuelle pour des styles cohérents.",
"gligen_textbox_example": "Spécifiez l'emplacement et la taille des objets.",
"image2image": "Transformez des images existantes à l'aide de prompts textuels.",
"inpain_model_outpainting": "Étendez les images au-delà de leurs limites d'origine.",
"inpaint_example": "Modifiez de façon transparente des parties spécifiques d'une image.",
"lora": "Appliquez des modèles LoRA pour des styles ou sujets spécialisés.",
"lora_multiple": "Combinez plusieurs modèles LoRA pour des résultats uniques."
},
"ControlNet": {
"2_pass_pose_worship": "Générez des images à partir de références de pose.",
"controlnet_example": "Contrôlez la génération d'image avec des images de référence.",
"depth_controlnet": "Créez des images avec une génération sensible à la profondeur.",
"depth_t2i_adapter": "Générez rapidement des images sensibles à la profondeur avec un adaptateur T2I.",
"mixing_controlnets": "Combinez plusieurs modèles ControlNet ensemble."
},
"Flux": {
"flux_canny_model_example": "Générez des images à partir de la détection de contours.",
"flux_depth_lora_example": "Créez des images avec LoRA sensible à la profondeur.",
"flux_dev_checkpoint_example": "Créez des images avec les modèles de développement Flux.",
"flux_fill_inpaint_example": "Complétez les parties manquantes d'une image.",
"flux_fill_outpaint_example": "Étendez les images avec l'outpainting Flux.",
"flux_redux_model_example": "Transférez le style d'une image de référence pour guider la génération d'image avec Flux.",
"flux_schnell": "Générez des images rapidement avec Flux Schnell."
},
"Image": {
"hidream_e1_full": "Modifiez des images avec HiDream E1.",
"hidream_i1_dev": "Générez des images avec HiDream I1 Dev.",
"hidream_i1_fast": "Générez rapidement des images avec HiDream I1.",
"hidream_i1_full": "Générez des images avec HiDream I1.",
"sd3_5_large_blur": "Générez des images à partir d'images de référence floues avec SD 3.5.",
"sd3_5_large_canny_controlnet_example": "Utilisez la détection de contours pour guider la génération d'images avec SD 3.5.",
"sd3_5_large_depth": "Créez des images sensibles à la profondeur avec SD 3.5.",
"sd3_5_simple_example": "Générez des images avec SD 3.5.",
"sdxl_refiner_prompt_example": "Améliorez les résultats SDXL avec des refineurs.",
"sdxl_revision_text_prompts": "Transférez des concepts à partir d'images de référence pour guider la génération d'images avec SDXL.",
"sdxl_revision_zero_positive": "Ajoutez des prompts textuels en plus des images de référence pour guider la génération d'images avec SDXL.",
"sdxl_simple_example": "Créez des images de haute qualité avec SDXL.",
"sdxlturbo_example": "Générez des images en une seule étape avec SDXL Turbo."
},
"Image API": {
"api-openai-dall-e-2-inpaint": "Utilisez l'API Dall-E 2 pour faire de l'inpainting sur des images.",
"api-openai-dall-e-2-t2i": "Utilisez l'API Dall-E 2 pour générer des images à partir de descriptions textuelles.",
"api-openai-dall-e-3-t2i": "Utilisez l'API Dall-E 3 pour générer des images à partir de descriptions textuelles.",
"api_bfl_flux_pro_t2i": "Créez des images avec FLUX.1 [pro] pour un excellent suivi des prompts, une qualité visuelle, des détails d'image et une grande diversité de sorties.",
"api_ideogram_v3_t2i": "Générez des images avec un alignement prompt-image de haute qualité, du photoréalisme et du rendu de texte. Créez des logos professionnels, affiches promotionnelles, concepts de pages d'accueil, photographies de produits et plus. Composez facilement des compositions spatiales sophistiquées avec des arrière-plans complexes, un éclairage et des couleurs précis et nuancés, et des détails environnementaux réalistes.",
"api_luma_photon_i2i": "Guidez la génération d'image en combinant images et prompt.",
"api_luma_photon_style_ref": "Appliquez et mélangez des références de style avec un contrôle précis. Luma Photon capture l'essence de chaque image de référence, vous permettant de combiner des éléments visuels distincts tout en maintenant une qualité professionnelle.",
"api_openai_image_1_i2i": "Utilisez l'API GPT Image 1 pour générer des images à partir d'images.",
"api_openai_image_1_inpaint": "Utilisez l'API GPT Image 1 pour faire de l'inpainting sur des images.",
"api_openai_image_1_multi_inputs": "Utilisez l'API GPT Image 1 avec plusieurs entrées pour générer des images.",
"api_openai_image_1_t2i": "Utilisez l'API GPT Image 1 pour générer des images à partir de descriptions textuelles.",
"api_recraft_image_gen_with_color_control": "Créez une palette personnalisée à réutiliser pour plusieurs images ou choisissez les couleurs pour chaque photo. Adaptez la palette de couleurs de votre marque et créez des visuels qui vous ressemblent.",
"api_recraft_image_gen_with_style_control": "Contrôlez le style avec des exemples visuels, alignez le positionnement et affinez les objets. Stockez et partagez des styles pour une cohérence parfaite de la marque.",
"api_recraft_vector_gen": "Passez d'un prompt textuel à une image vectorielle avec le générateur vectoriel IA de Recraft. Produisez des illustrations vectorielles de la meilleure qualité pour des logos, affiches, icônes, publicités, bannières et maquettes. Perfectionnez vos designs avec des fichiers SVG nets et de haute qualité. Créez des illustrations vectorielles de marque pour votre application ou site web en quelques secondes.",
"api_stability_sd3_t2i": "Générez des images de haute qualité avec une excellente fidélité au prompt. Parfait pour les cas d'usage professionnels en résolution 1 mégapixel."
},
"Upscaling": {
"esrgan_example": "Utilisez des modèles d'upscaling pour améliorer la qualité d'image.",
"hiresfix_esrgan_workflow": "Utilisez des modèles d'upscaling lors des étapes intermédiaires.",
"hiresfix_latent_workflow": "Améliorez la qualité d'image dans l'espace latent.",
"latent_upscale_different_prompt_model": "Upscalez et changez le prompt à chaque passage."
},
"Video": {
"hunyuan_video_text_to_video": "Générez des vidéos avec le modèle Hunyuan.",
"image_to_video": "Transformez des images en vidéos animées.",
"image_to_video_wan": "Générez rapidement des vidéos à partir d'images.",
"ltxv_image_to_video": "Convertissez des images fixes en vidéos.",
"ltxv_text_to_video": "Générez des vidéos à partir de descriptions textuelles.",
"mochi_text_to_video_example": "Créez des vidéos avec le modèle Mochi.",
"text_to_video_wan": "Générez rapidement des vidéos à partir de descriptions textuelles.",
"txt_to_image_to_video": "Générez des images à partir de texte puis convertissez-les en vidéos.",
"wan2_1_flf2v_720_f16": "Générez une vidéo en contrôlant la première et la dernière image.",
"wan2_1_fun_control": "Guidez la génération vidéo avec le contrôle de pose, profondeur, contours et plus.",
"wan2_1_fun_inp": "Créez des vidéos à partir d'images de début et de fin."
},
"Video API": {
"api_hailuo_minimax_i2v": "Créez des vidéos raffinées à partir d'images et de texte, incluant l'intégration CGI et des effets photo tendance comme le hugging IA viral. Choisissez parmi une variété de styles et thèmes vidéo pour correspondre à votre vision créative.",
"api_kling_i2v": "Créez des vidéos avec une excellente fidélité au prompt pour les actions, expressions et mouvements de caméra. Prend désormais en charge des prompts complexes avec des actions séquentielles pour vous permettre de diriger votre scène.",
"api_luma_i2v": "Transformez des images statiques en animations magiques de haute qualité instantanément.",
"api_pika_scene": "Utilisez plusieurs images comme ingrédients et générez des vidéos qui les intègrent toutes.",
"api_pixverse_t2v": "Générez des vidéos avec une interprétation précise du prompt et des dynamiques vidéo impressionnantes.",
"api_pixverse_template_i2v": "Transformez des images statiques en vidéos dynamiques avec mouvement et effets.",
"api_veo2_i2v": "Utilisez l'API Google Veo2 pour générer des vidéos à partir d'images."
}
},
"title": "Commencez avec un modèle"
},
"toastMessages": {
@@ -1206,6 +1363,7 @@
},
"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": {
@@ -1218,6 +1376,7 @@
"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

@@ -29,6 +29,24 @@
"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": "パスワードの確認",
@@ -43,6 +61,7 @@
"loginWithGithub": "Githubでログイン",
"loginWithGoogle": "Googleでログイン",
"newUser": "新規ユーザーですか?",
"noAssociatedUser": "指定されたAPIキーに関連付けられたComfyユーザーが存在しません",
"orContinueWith": "または以下で続ける",
"passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力してください",
@@ -55,7 +74,9 @@
"success": "ログイン成功",
"termsLink": "利用規約",
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
"title": "アカウントにログインする"
"title": "アカウントにログインする",
"useApiKey": "Comfy APIキー",
"userAvatar": "ユーザーアバター"
},
"passwordUpdate": {
"success": "パスワードが更新されました",
@@ -130,6 +151,7 @@
"Unpin": "ピンを解除"
},
"credits": {
"apiPricing": "API料金",
"credits": "クレジット",
"faqs": "よくある質問",
"invoiceHistory": "請求履歴",
@@ -148,8 +170,10 @@
"yourCreditBalance": "あなたのクレジット残高"
},
"dataTypes": {
"*": "*",
"AUDIO": "オーディオ",
"BOOLEAN": "ブール",
"CAMERA_CONTROL": "カメラコントロール",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
@@ -166,20 +190,29 @@
"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": "ウェブカメラ"
},
@@ -325,6 +358,7 @@
"success": "成功",
"systemInfo": "システム情報",
"terminal": "ターミナル",
"title": "タイトル",
"unknownError": "不明なエラー",
"update": "更新",
"updateAvailable": "更新が利用可能",
@@ -720,10 +754,22 @@
"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": "バッチ",
@@ -748,6 +794,7 @@
"instructpix2pix": "インストラクションピクス2ピクス",
"latent": "潜在",
"loaders": "ローダー",
"lotus": "lotus",
"ltxv": "LTXV",
"mask": "マスク",
"model": "モデル",
@@ -759,10 +806,12 @@
"photomaker": "photomaker",
"postprocessing": "ポストプロセッシング",
"preprocessors": "前処理",
"primitive": "プリミティブ",
"samplers": "サンプラー",
"sampling": "サンプリング",
"schedulers": "スケジューラー",
"scheduling": "スケジューリング",
"sd": "sd",
"sd3": "SD3",
"sigmas": "シグマ",
"stable_cascade": "安定したカスケード",
@@ -771,6 +820,10 @@
"unet": "U-Net",
"upscale_diffusion": "アップスケール拡散",
"upscaling": "アップスケーリング",
"utils": "ユーティリティ",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "ビデオ",
"video_models": "ビデオモデル"
},
@@ -1052,8 +1105,10 @@
"Custom Nodes": "カスタムノード",
"Flux": "Flux",
"Image": "画像",
"Image API": "画像API",
"Upscaling": "アップスケーリング",
"Video": "ビデオ"
"Video": "ビデオ",
"Video API": "動画API"
},
"template": {
"3D": {
@@ -1114,7 +1169,7 @@
"api-openai-dall-e-2-inpaint": "Dall-E 2 インペイント",
"api-openai-dall-e-2-t2i": "Dall-E 2 テキストから画像へ",
"api-openai-dall-e-3-t2i": "Dall-E 3 テキストから画像へ",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] テキストから画像へ",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra テキストから画像へ",
"api_ideogram_v3_t2i": "Ideogram V3 テキストから画像へ",
"api_luma_photon_i2i": "Luma Photon 画像から画像へ",
"api_luma_photon_style_ref": "Luma Photon スタイル参照",
@@ -1125,7 +1180,7 @@
"api_recraft_image_gen_with_color_control": "Recraft カラーコントロール画像生成",
"api_recraft_image_gen_with_style_control": "Recraft スタイルコントロール画像生成",
"api_recraft_vector_gen": "Recraft ベクター生成",
"api_stability_sd3_t2i": "Stability SD3 テキストから画像へ"
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra テキストから画像へ"
},
"Upscaling": {
"esrgan_example": "ESRGAN",
@@ -1151,10 +1206,112 @@
"api_luma_i2v": "Luma 画像から動画へ",
"api_pika_scene": "Pika シーン: 画像から動画へ",
"api_pixverse_t2v": "PixVerse テキストから動画へ",
"api_pixverse_template_i2v": "PixVerse テンプレート: 画像から動画へ",
"api_pixverse_template_i2v": "PixVerse Template Effects: 画像から動画へ",
"api_veo2_i2v": "Veo2 画像から動画へ"
}
},
"templateDescription": {
"3D": {
"hunyuan-3d-multiview-elf": "Hunyuan3D 2mvで複数ビューからモデルを生成します。",
"hunyuan-3d-turbo": "Hunyuan3D 2mv turboで複数ビューからモデルを生成します。",
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0で単一ビューからモデルを生成します。",
"stable_zero123_example": "単一画像から3Dビューを生成します。"
},
"Area Composition": {
"area_composition": "エリアで画像構成をコントロールします。",
"area_composition_reversed": "エリア構成ワークフローを逆転します。",
"area_composition_square_area_for_subject": "被写体の配置を一貫させます。"
},
"Audio": {
"stable_audio_example": "テキストの説明から音声を生成します。"
},
"Basics": {
"default": "テキストの説明から画像を生成します。",
"embedding_example": "テキスト反転を使って一貫したスタイルを実現します。",
"gligen_textbox_example": "オブジェクトの位置とサイズを指定します。",
"image2image": "テキストプロンプトを使って既存の画像を変換します。",
"inpain_model_outpainting": "画像を元の境界を超えて拡張します。",
"inpaint_example": "画像の特定部分をシームレスに編集します。",
"lora": "LoRAモデルを適用して特定のスタイルや対象を表現します。",
"lora_multiple": "複数のLoRAモデルを組み合わせて独自の結果を得ます。"
},
"ControlNet": {
"2_pass_pose_worship": "ポーズ参照から画像を生成します。",
"controlnet_example": "参照画像で画像生成をコントロールします。",
"depth_controlnet": "深度認識生成で画像を作成します。",
"depth_t2i_adapter": "T2Iアダプターで素早く深度認識画像を生成します。",
"mixing_controlnets": "複数のControlNetモデルを組み合わせます。"
},
"Flux": {
"flux_canny_model_example": "エッジ検出から画像を生成します。",
"flux_depth_lora_example": "深度認識LoRAで画像を生成します。",
"flux_dev_checkpoint_example": "Flux開発モデルを使って画像を生成します。",
"flux_fill_inpaint_example": "画像の欠損部分を補完します。",
"flux_fill_outpaint_example": "Fluxのアウトペイントで画像を拡張します。",
"flux_redux_model_example": "参照画像のスタイルを転送し、Fluxで画像生成をガイドします。",
"flux_schnell": "Flux Schnellで素早く画像を生成します。"
},
"Image": {
"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でワンステップで画像を生成します。"
},
"Image API": {
"api-openai-dall-e-2-inpaint": "Dall-E 2 APIで画像のインペイントを行います。",
"api-openai-dall-e-2-t2i": "Dall-E 2 APIでテキストの説明から画像を生成します。",
"api-openai-dall-e-3-t2i": "Dall-E 3 APIでテキストの説明から画像を生成します。",
"api_bfl_flux_pro_t2i": "FLUX.1 [pro]で優れたプロンプト追従性、画質、ディテール、多様な出力の画像を生成します。",
"api_ideogram_v3_t2i": "高品質な画像・プロンプト整合性、フォトリアリズム、テキスト描画で画像を生成します。プロ品質のロゴ、ポスター、ランディングページ、商品写真などを作成。複雑な背景や精密なライティング、リアルな環境ディテールで洗練された空間構成を簡単に作成できます。",
"api_luma_photon_i2i": "画像とプロンプトを組み合わせて画像生成をガイドします。",
"api_luma_photon_style_ref": "スタイル参照を正確に適用・ブレンドします。Luma Photonは各参照画像の本質を捉え、異なるビジュアル要素を組み合わせつつプロ品質を維持します。",
"api_openai_image_1_i2i": "GPT Image 1 APIで画像から画像を生成します。",
"api_openai_image_1_inpaint": "GPT Image 1 APIで画像のインペイントを行います。",
"api_openai_image_1_multi_inputs": "GPT Image 1 APIで複数入力を使って画像を生成します。",
"api_openai_image_1_t2i": "GPT Image 1 APIでテキストの説明から画像を生成します。",
"api_recraft_image_gen_with_color_control": "カスタムパレットを作成して複数画像で再利用したり、各写真の色を手動で選択できます。ブランドのカラーパレットに合わせて独自のビジュアルを作成します。",
"api_recraft_image_gen_with_style_control": "ビジュアル例でスタイルを制御し、位置合わせやオブジェクトの微調整が可能です。スタイルを保存・共有してブランドの一貫性を保ちます。",
"api_recraft_vector_gen": "テキストプロンプトからRecraftのAIベクター生成でベクター画像を作成します。ロゴ、ポスター、アイコンセット、広告、バナー、モックアップに最適な高品質SVGファイルを生成。アプリやウェブサイト用のブランドベクターイラストを数秒で作成します。",
"api_stability_sd3_t2i": "高品質でプロンプト追従性の高い画像を生成します。1メガピクセル解像度でプロ用途に最適です。"
},
"Upscaling": {
"esrgan_example": "アップスケールモデルで画像品質を向上させます。",
"hiresfix_esrgan_workflow": "中間ステップでアップスケールモデルを使用します。",
"hiresfix_latent_workflow": "latent空間で画像品質を向上させます。",
"latent_upscale_different_prompt_model": "アップスケールしつつパスごとにプロンプトを変更します。"
},
"Video": {
"hunyuan_video_text_to_video": "Hunyuanモデルで動画を生成します。",
"image_to_video": "画像をアニメーション動画に変換します。",
"image_to_video_wan": "画像から素早く動画を生成します。",
"ltxv_image_to_video": "静止画像を動画に変換します。",
"ltxv_text_to_video": "テキストの説明から動画を生成します。",
"mochi_text_to_video_example": "Mochiモデルで動画を作成します。",
"text_to_video_wan": "テキストの説明から素早く動画を生成します。",
"txt_to_image_to_video": "テキストから画像を生成し、それを動画に変換します。",
"wan2_1_flf2v_720_f16": "最初と最後のフレームを制御して動画を生成します。",
"wan2_1_fun_control": "ポーズ、深度、エッジ制御などで動画生成をガイドします。",
"wan2_1_fun_inp": "開始フレームと終了フレームから動画を作成します。"
},
"Video API": {
"api_hailuo_minimax_i2v": "画像とテキストから洗練された動画を作成。CGI統合や流行のAIハグなどのエフェクトも。多彩なスタイルやテーマから選択可能です。",
"api_kling_i2v": "アクション、表情、カメラワークのプロンプト追従性に優れた動画を作成します。複雑なシーケンシャルアクションもサポートし、あなたがシーンの監督になれます。",
"api_luma_i2v": "静止画像から瞬時に高品質なアニメーションを作成します。",
"api_pika_scene": "複数の画像を素材として使い、それらを組み込んだ動画を生成します。",
"api_pixverse_t2v": "プロンプト解釈が正確で、ダイナミックな動画を生成します。",
"api_pixverse_template_i2v": "静止画像を動きやエフェクトのあるダイナミックな動画に変換します。",
"api_veo2_i2v": "Google Veo2 APIで画像から動画を生成します。"
}
},
"title": "テンプレートを利用して開始"
},
"toastMessages": {
@@ -1206,6 +1363,7 @@
},
"validation": {
"invalidEmail": "無効なメールアドレス",
"length": "{length}文字でなければなりません",
"maxLength": "{length}文字以下でなければなりません",
"minLength": "{length}文字以上でなければなりません",
"password": {
@@ -1218,6 +1376,7 @@
"uppercase": "少なくとも1つの大文字を含む必要があります"
},
"personalDataConsentRequired": "個人データの処理に同意する必要があります。",
"prefix": "{prefix}で始める必要があります",
"required": "必須"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,24 @@
"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": "비밀번호 확인",
@@ -43,6 +61,7 @@
"loginWithGithub": "Github로 로그인",
"loginWithGoogle": "구글로 로그인",
"newUser": "처음이신가요?",
"noAssociatedUser": "제공된 API 키와 연결된 Comfy 사용자가 없습니다",
"orContinueWith": "또는 다음으로 계속",
"passwordLabel": "비밀번호",
"passwordPlaceholder": "비밀번호를 입력하세요",
@@ -55,7 +74,9 @@
"success": "로그인 성공",
"termsLink": "이용 약관",
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
"title": "계정에 로그인"
"title": "계정에 로그인",
"useApiKey": "Comfy API 키",
"userAvatar": "사용자 아바타"
},
"passwordUpdate": {
"success": "비밀번호가 업데이트되었습니다",
@@ -130,6 +151,7 @@
"Unpin": "고정 해제"
},
"credits": {
"apiPricing": "API 가격",
"credits": "크레딧",
"faqs": "자주 묻는 질문",
"invoiceHistory": "청구서 내역",
@@ -148,8 +170,10 @@
"yourCreditBalance": "보유 크레딧 잔액"
},
"dataTypes": {
"*": "*",
"AUDIO": "오디오",
"BOOLEAN": "논리값",
"CAMERA_CONTROL": "카메라 제어",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION 출력",
@@ -166,20 +190,29 @@
"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": "웹캠"
},
@@ -325,6 +358,7 @@
"success": "성공",
"systemInfo": "시스템 정보",
"terminal": "터미널",
"title": "제목",
"unknownError": "알 수 없는 오류",
"update": "업데이트",
"updateAvailable": "업데이트 가능",
@@ -720,10 +754,22 @@
"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": "배치",
@@ -748,6 +794,7 @@
"instructpix2pix": "InstructPix2Pix",
"latent": "잠재 데이터",
"loaders": "로더",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "마스크",
"model": "모델",
@@ -759,10 +806,12 @@
"photomaker": "포토메이커",
"postprocessing": "후처리",
"preprocessors": "전처리기",
"primitive": "프리미티브",
"samplers": "샘플러",
"sampling": "샘플링",
"schedulers": "스케줄러",
"scheduling": "스케줄링",
"sd": "sd",
"sd3": "sd3",
"sigmas": "시그마",
"stable_cascade": "Stable Cascade",
@@ -771,6 +820,10 @@
"unet": "UNet",
"upscale_diffusion": "업스케일 확산",
"upscaling": "업스케일링",
"utils": "유틸리티",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "비디오",
"video_models": "비디오 모델"
},
@@ -1052,8 +1105,10 @@
"Custom Nodes": "사용자 정의 노드",
"Flux": "FLUX",
"Image": "이미지",
"Image API": "이미지 API",
"Upscaling": "업스케일링",
"Video": "비디오"
"Video": "비디오",
"Video API": "비디오 API"
},
"template": {
"3D": {
@@ -1114,7 +1169,7 @@
"api-openai-dall-e-2-inpaint": "Dall-E 2 인페인트",
"api-openai-dall-e-2-t2i": "Dall-E 2 텍스트 투 이미지",
"api-openai-dall-e-3-t2i": "Dall-E 3 텍스트 투 이미지",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] 텍스트 투 이미지",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra 텍스트 투 이미지",
"api_ideogram_v3_t2i": "Ideogram V3 텍스트 투 이미지",
"api_luma_photon_i2i": "Luma Photon 이미지 투 이미지",
"api_luma_photon_style_ref": "Luma Photon 스타일 참조",
@@ -1125,7 +1180,7 @@
"api_recraft_image_gen_with_color_control": "Recraft 색상 제어 이미지 생성",
"api_recraft_image_gen_with_style_control": "Recraft 스타일 제어 이미지 생성",
"api_recraft_vector_gen": "Recraft 벡터 생성",
"api_stability_sd3_t2i": "Stability SD3 텍스트 투 이미지"
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra 텍스트 투 이미지"
},
"Upscaling": {
"esrgan_example": "ESRGAN",
@@ -1151,10 +1206,112 @@
"api_luma_i2v": "Luma 이미지 투 비디오",
"api_pika_scene": "Pika 장면: 이미지 투 비디오",
"api_pixverse_t2v": "PixVerse 텍스트 투 비디오",
"api_pixverse_template_i2v": "PixVerse 템플릿: 이미지 투 비디오",
"api_pixverse_template_i2v": "PixVerse Template Effects: 이미지 투 비디오",
"api_veo2_i2v": "Veo2 이미지 투 비디오"
}
},
"templateDescription": {
"3D": {
"hunyuan-3d-multiview-elf": "Hunyuan3D 2mv로 여러 뷰에서 모델을 생성합니다.",
"hunyuan-3d-turbo": "Hunyuan3D 2mv turbo로 여러 뷰에서 모델을 생성합니다.",
"hunyuan3d-non-multiview-train": "Hunyuan3D 2.0으로 단일 뷰에서 모델을 생성합니다.",
"stable_zero123_example": "단일 이미지에서 3D 뷰를 생성합니다."
},
"Area Composition": {
"area_composition": "영역을 통해 이미지 구성을 제어합니다.",
"area_composition_reversed": "영역 구성 워크플로우를 반대로 적용합니다.",
"area_composition_square_area_for_subject": "일관된 피사체 배치를 만듭니다."
},
"Audio": {
"stable_audio_example": "텍스트 설명으로 오디오를 생성합니다."
},
"Basics": {
"default": "텍스트 설명으로 이미지를 생성합니다.",
"embedding_example": "일관된 스타일을 위해 텍스트 인버전을 사용합니다.",
"gligen_textbox_example": "객체의 위치와 크기를 지정합니다.",
"image2image": "텍스트 프롬프트를 사용하여 기존 이미지를 변환합니다.",
"inpain_model_outpainting": "이미지의 원래 경계를 넘어 확장합니다.",
"inpaint_example": "이미지의 특정 부분을 자연스럽게 편집합니다.",
"lora": "특정 스타일이나 주제를 위해 LoRA 모델을 적용합니다.",
"lora_multiple": "여러 LoRA 모델을 결합하여 독특한 결과를 만듭니다."
},
"ControlNet": {
"2_pass_pose_worship": "포즈 참조로 이미지를 생성합니다.",
"controlnet_example": "참조 이미지를 사용해 이미지 생성을 제어합니다.",
"depth_controlnet": "깊이 인식 이미지 생성을 합니다.",
"depth_t2i_adapter": "T2I 어댑터로 깊이 인식 이미지를 빠르게 생성합니다.",
"mixing_controlnets": "여러 ControlNet 모델을 결합합니다."
},
"Flux": {
"flux_canny_model_example": "에지 감지로부터 이미지를 생성합니다.",
"flux_depth_lora_example": "깊이 인식 LoRA로 이미지를 생성합니다.",
"flux_dev_checkpoint_example": "Flux 개발 모델로 이미지를 생성합니다.",
"flux_fill_inpaint_example": "이미지의 누락된 부분을 채웁니다.",
"flux_fill_outpaint_example": "Flux 아웃페인팅으로 이미지를 확장합니다.",
"flux_redux_model_example": "참조 이미지의 스타일을 가이드 이미지 생성에 적용합니다.",
"flux_schnell": "Flux Schnell로 이미지를 빠르게 생성합니다."
},
"Image": {
"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로 한 번에 이미지를 생성합니다."
},
"Image API": {
"api-openai-dall-e-2-inpaint": "Dall-E 2 API로 이미지를 인페인팅합니다.",
"api-openai-dall-e-2-t2i": "Dall-E 2 API로 텍스트 설명에서 이미지를 생성합니다.",
"api-openai-dall-e-3-t2i": "Dall-E 3 API로 텍스트 설명에서 이미지를 생성합니다.",
"api_bfl_flux_pro_t2i": "FLUX.1 [pro]의 뛰어난 프롬프트 반영, 시각적 품질, 이미지 디테일, 다양성으로 이미지를 생성합니다.",
"api_ideogram_v3_t2i": "고품질 이미지-프롬프트 일치, 포토리얼리즘, 텍스트 렌더링으로 이미지를 생성합니다. 전문가 수준의 로고, 홍보 포스터, 랜딩 페이지 컨셉, 제품 사진 등을 만드세요. 정교한 배경, 섬세한 조명과 색상, 사실적인 환경 디테일로 세련된 공간 구성을 손쉽게 제작할 수 있습니다.",
"api_luma_photon_i2i": "이미지와 프롬프트를 조합하여 이미지 생성을 가이드합니다.",
"api_luma_photon_style_ref": "정확한 제어로 스타일 참조를 적용하고 혼합합니다. Luma Photon은 각 참조 이미지의 본질을 포착하여, 전문적인 품질을 유지하면서 독특한 시각적 요소를 결합할 수 있습니다.",
"api_openai_image_1_i2i": "GPT Image 1 API로 이미지에서 이미지를 생성합니다.",
"api_openai_image_1_inpaint": "GPT Image 1 API로 이미지를 인페인팅합니다.",
"api_openai_image_1_multi_inputs": "GPT Image 1 API로 여러 입력을 사용해 이미지를 생성합니다.",
"api_openai_image_1_t2i": "GPT Image 1 API로 텍스트 설명에서 이미지를 생성합니다.",
"api_recraft_image_gen_with_color_control": "여러 이미지에 재사용할 맞춤 팔레트를 만들거나 각 사진마다 색상을 직접 선택하세요. 브랜드의 색상 팔레트에 맞추고, 독창적인 비주얼을 제작하세요.",
"api_recraft_image_gen_with_style_control": "시각적 예시로 스타일을 제어하고, 위치를 맞추며, 객체를 미세 조정하세요. 스타일을 저장 및 공유하여 브랜드 일관성을 유지할 수 있습니다.",
"api_recraft_vector_gen": "텍스트 프롬프트에서 Recraft의 AI 벡터 생성기로 벡터 이미지를 만드세요. 로고, 포스터, 아이콘 세트, 광고, 배너, 목업 등 최고의 품질의 벡터 아트를 제작할 수 있습니다. 선명하고 고품질의 SVG 파일로 디자인을 완성하세요. 앱이나 웹사이트를 위한 브랜드 벡터 일러스트를 몇 초 만에 만드세요.",
"api_stability_sd3_t2i": "1메가픽셀 해상도에서 전문가용 고품질 이미지를 생성합니다. 프롬프트 반영이 우수합니다."
},
"Upscaling": {
"esrgan_example": "업스케일 모델로 이미지 품질을 향상합니다.",
"hiresfix_esrgan_workflow": "중간 단계에서 업스케일 모델을 사용합니다.",
"hiresfix_latent_workflow": "latent 공간에서 이미지 품질을 향상합니다.",
"latent_upscale_different_prompt_model": "업스케일과 프롬프트 변경을 여러 번에 걸쳐 적용합니다."
},
"Video": {
"hunyuan_video_text_to_video": "Hunyuan 모델을 사용하여 비디오를 생성합니다.",
"image_to_video": "이미지를 애니메이션 비디오로 변환합니다.",
"image_to_video_wan": "이미지로부터 빠르게 비디오를 생성합니다.",
"ltxv_image_to_video": "정지 이미지를 비디오로 변환합니다.",
"ltxv_text_to_video": "텍스트 설명으로 비디오를 생성합니다.",
"mochi_text_to_video_example": "Mochi 모델로 비디오를 생성합니다.",
"text_to_video_wan": "텍스트 설명으로 빠르게 비디오를 생성합니다.",
"txt_to_image_to_video": "텍스트로 이미지를 생성한 후 비디오로 변환합니다.",
"wan2_1_flf2v_720_f16": "첫 프레임과 마지막 프레임을 제어하여 비디오를 생성합니다.",
"wan2_1_fun_control": "포즈, 깊이, 에지 등으로 비디오 생성을 가이드합니다.",
"wan2_1_fun_inp": "시작 및 종료 프레임으로 비디오를 생성합니다."
},
"Video API": {
"api_hailuo_minimax_i2v": "이미지와 텍스트로 정교한 비디오를 생성합니다. CGI 통합, 바이럴 AI 허깅 등 트렌디한 사진 효과도 포함됩니다. 다양한 비디오 스타일과 테마로 창의적인 비전을 실현하세요.",
"api_kling_i2v": "동작, 표정, 카메라 움직임에 대한 프롬프트 반영이 뛰어난 비디오를 생성합니다. 이제 복잡한 프롬프트와 연속 동작도 지원되어, 장면의 연출자가 될 수 있습니다.",
"api_luma_i2v": "정지 이미지를 즉시 고품질 애니메이션으로 만드세요.",
"api_pika_scene": "여러 이미지를 재료로 사용하여 모두를 포함하는 비디오를 생성합니다.",
"api_pixverse_t2v": "정확한 프롬프트 해석과 놀라운 비디오 다이내믹스로 비디오를 생성합니다.",
"api_pixverse_template_i2v": "정지 이미지를 동적 비디오로 변환하고 모션과 효과를 추가합니다.",
"api_veo2_i2v": "Google Veo2 API로 이미지에서 비디오를 생성합니다."
}
},
"title": "템플릿으로 시작하기"
},
"toastMessages": {
@@ -1206,6 +1363,7 @@
},
"validation": {
"invalidEmail": "유효하지 않은 이메일 주소",
"length": "{length}자여야 합니다",
"maxLength": "{length}자를 초과할 수 없습니다",
"minLength": "{length}자 이상이어야 합니다",
"password": {
@@ -1218,6 +1376,7 @@
"uppercase": "적어도 하나의 대문자를 포함해야 합니다"
},
"personalDataConsentRequired": "개인 데이터 처리에 동의해야 합니다.",
"prefix": "{prefix}(으)로 시작해야 합니다",
"required": "필수"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,24 @@
"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": "Подтвердите пароль",
@@ -43,6 +61,7 @@
"loginWithGithub": "Войти через Github",
"loginWithGoogle": "Войти через Google",
"newUser": "Вы здесь впервые?",
"noAssociatedUser": "С предоставленным API-ключом не связан ни один пользователь Comfy",
"orContinueWith": "Или продолжить с",
"passwordLabel": "Пароль",
"passwordPlaceholder": "Введите ваш пароль",
@@ -55,7 +74,9 @@
"success": "Вход выполнен успешно",
"termsLink": "Условиями использования",
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
"title": "Войдите в свой аккаунт"
"title": "Войдите в свой аккаунт",
"useApiKey": "Comfy API-ключ",
"userAvatar": "Аватар пользователя"
},
"passwordUpdate": {
"success": "Пароль обновлён",
@@ -130,6 +151,7 @@
"Unpin": "Открепить"
},
"credits": {
"apiPricing": "Цены на API",
"credits": "Кредиты",
"faqs": "Часто задаваемые вопросы",
"invoiceHistory": "История счетов",
@@ -148,8 +170,10 @@
"yourCreditBalance": "Ваш баланс кредитов"
},
"dataTypes": {
"*": "*",
"AUDIO": "АУДИО",
"BOOLEAN": "БУЛЕВО",
"CAMERA_CONTROL": "УПРАВЛЕНИЕ_КАМЕРОЙ",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP_VISION",
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
@@ -166,20 +190,29 @@
"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": "ВЕБ-КАМЕРА"
},
@@ -325,6 +358,7 @@
"success": "Успех",
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"title": "Заголовок",
"unknownError": "Неизвестная ошибка",
"update": "Обновить",
"updateAvailable": "Доступно обновление",
@@ -720,10 +754,22 @@
"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": "пакет",
@@ -748,6 +794,7 @@
"instructpix2pix": "instructpix2pix",
"latent": "латентный",
"loaders": "загрузчики",
"lotus": "lotus",
"ltxv": "ltxv",
"mask": "маска",
"model": "модель",
@@ -759,10 +806,12 @@
"photomaker": "photomaker",
"postprocessing": "постобработка",
"preprocessors": "предобработчики",
"primitive": "примитив",
"samplers": "семплеры",
"sampling": "выборка",
"schedulers": "schedulers",
"scheduling": "scheduling",
"sd": "sd",
"sd3": "sd3",
"sigmas": "сигмы",
"stable_cascade": "стабильная_каскадная",
@@ -771,6 +820,10 @@
"unet": "unet",
"upscale_diffusion": "диффузии_апскейла",
"upscaling": "апскейл",
"utils": "утилиты",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "видео",
"video_models": "видеомодели"
},
@@ -1052,8 +1105,10 @@
"Custom Nodes": "Пользовательские узлы",
"Flux": "Flux",
"Image": "Изображение",
"Image API": "Image API",
"Upscaling": "Увеличение разрешения",
"Video": "Видео"
"Video": "Видео",
"Video API": "Video API"
},
"template": {
"3D": {
@@ -1114,7 +1169,7 @@
"api-openai-dall-e-2-inpaint": "Dall-E 2: дорисовка",
"api-openai-dall-e-2-t2i": "Dall-E 2: текст в изображение",
"api-openai-dall-e-3-t2i": "Dall-E 3: текст в изображение",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro]: текст в изображение",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra текст в изображение",
"api_ideogram_v3_t2i": "Ideogram V3: текст в изображение",
"api_luma_photon_i2i": "Luma Photon: изображение в изображение",
"api_luma_photon_style_ref": "Luma Photon: стиль по образцу",
@@ -1125,7 +1180,7 @@
"api_recraft_image_gen_with_color_control": "Recraft: генерация изображения с управлением цветом",
"api_recraft_image_gen_with_style_control": "Recraft: генерация изображения с управлением стилем",
"api_recraft_vector_gen": "Recraft: генерация векторного изображения",
"api_stability_sd3_t2i": "Stability SD3: текст в изображение"
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra текст в изображение"
},
"Upscaling": {
"esrgan_example": "ESRGAN",
@@ -1151,10 +1206,112 @@
"api_luma_i2v": "Luma: изображение в видео",
"api_pika_scene": "Pika Scenes: изображения в видео",
"api_pixverse_t2v": "PixVerse: текст в видео",
"api_pixverse_template_i2v": "PixVerse Templates: изображение в видео",
"api_pixverse_template_i2v": "PixVerse Template Effects: изображение в видео",
"api_veo2_i2v": "Veo2: изображение в видео"
}
},
"templateDescription": {
"3D": {
"hunyuan-3d-multiview-elf": "Используйте Hunyuan3D 2mv для генерации моделей по нескольким видам.",
"hunyuan-3d-turbo": "Используйте Hunyuan3D 2mv turbo для генерации моделей по нескольким видам.",
"hunyuan3d-non-multiview-train": "Используйте Hunyuan3D 2.0 для генерации моделей по одному виду.",
"stable_zero123_example": "Генерируйте 3D-виды по одному изображению."
},
"Area Composition": {
"area_composition": "Управляйте композицией изображения с помощью областей.",
"area_composition_reversed": "Обратный рабочий процесс композиции областей.",
"area_composition_square_area_for_subject": "Создавайте стабильное размещение объекта."
},
"Audio": {
"stable_audio_example": "Генерируйте аудио по текстовым описаниям."
},
"Basics": {
"default": "Генерируйте изображения по текстовым описаниям.",
"embedding_example": "Используйте текстовую инверсию для единых стилей.",
"gligen_textbox_example": "Указывайте расположение и размер объектов.",
"image2image": "Преобразуйте существующие изображения с помощью текстовых подсказок.",
"inpain_model_outpainting": "Расширяйте изображения за пределы их исходных границ.",
"inpaint_example": "Редактируйте отдельные части изображений без швов.",
"lora": "Применяйте LoRA-модели для специализированных стилей или объектов.",
"lora_multiple": "Комбинируйте несколько LoRA-моделей для уникальных результатов."
},
"ControlNet": {
"2_pass_pose_worship": "Генерируйте изображения по референсам поз.",
"controlnet_example": "Управляйте генерацией изображений с помощью референсных изображений.",
"depth_controlnet": "Создавайте изображения с учетом глубины.",
"depth_t2i_adapter": "Быстро генерируйте изображения с глубиной с помощью T2I-адаптера.",
"mixing_controlnets": "Комбинируйте несколько моделей ControlNet вместе."
},
"Flux": {
"flux_canny_model_example": "Генерируйте изображения по детекции границ.",
"flux_depth_lora_example": "Создавайте изображения с глубиной с помощью LoRA.",
"flux_dev_checkpoint_example": "Создавайте изображения с помощью Flux development models.",
"flux_fill_inpaint_example": "Заполняйте отсутствующие части изображений.",
"flux_fill_outpaint_example": "Расширяйте изображения с помощью Flux outpainting.",
"flux_redux_model_example": "Передавайте стиль с референсного изображения для управления генерацией с помощью Flux.",
"flux_schnell": "Быстро генерируйте изображения с Flux Schnell."
},
"Image": {
"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 с помощью refiners.",
"sdxl_revision_text_prompts": "Передавайте концепции с референсных изображений для управления генерацией с SDXL.",
"sdxl_revision_zero_positive": "Добавляйте текстовые подсказки вместе с референсными изображениями для управления генерацией с SDXL.",
"sdxl_simple_example": "Создавайте высококачественные изображения с SDXL.",
"sdxlturbo_example": "Генерируйте изображения за один шаг с SDXL Turbo."
},
"Image API": {
"api-openai-dall-e-2-inpaint": "Используйте Dall-E 2 API для инпейнта изображений.",
"api-openai-dall-e-2-t2i": "Используйте Dall-E 2 API для генерации изображений по текстовым описаниям.",
"api-openai-dall-e-3-t2i": "Используйте Dall-E 3 API для генерации изображений по текстовым описаниям.",
"api_bfl_flux_pro_t2i": "Создавайте изображения с помощью FLUX.1 [pro] с отличным следованием подсказкам, высоким качеством, детализацией и разнообразием.",
"api_ideogram_v3_t2i": "Генерируйте изображения с высоким соответствием подсказкам, фотореализмом и рендерингом текста. Создавайте профессиональные логотипы, промо-постеры, концепты лендингов, продуктовые фото и многое другое. Легко создавайте сложные пространственные композиции с детализированным фоном, точным освещением и реалистичной средой.",
"api_luma_photon_i2i": "Управляйте генерацией изображений с помощью комбинации изображений и подсказки.",
"api_luma_photon_style_ref": "Применяйте и смешивайте стили с точным контролем. Luma Photon захватывает суть каждого референса, позволяя комбинировать уникальные визуальные элементы с профессиональным качеством.",
"api_openai_image_1_i2i": "Используйте GPT Image 1 API для генерации изображений по изображениям.",
"api_openai_image_1_inpaint": "Используйте GPT Image 1 API для инпейнта изображений.",
"api_openai_image_1_multi_inputs": "Используйте GPT Image 1 API с несколькими входами для генерации изображений.",
"api_openai_image_1_t2i": "Используйте GPT Image 1 API для генерации изображений по текстовым описаниям.",
"api_recraft_image_gen_with_color_control": "Создайте собственную палитру для повторного использования или подберите цвета для каждого фото. Совместите фирменную палитру и создайте уникальные визуалы.",
"api_recraft_image_gen_with_style_control": "Контролируйте стиль с помощью визуальных примеров, выравнивайте объекты и настраивайте детали. Сохраняйте и делитесь стилями для идеального брендирования.",
"api_recraft_vector_gen": "Преобразуйте текстовую подсказку в векторное изображение с помощью AI-генератора Recraft. Создавайте лучшие векторные арты для логотипов, постеров, иконок, баннеров и мокапов. Дорабатывайте дизайн с помощью качественных SVG-файлов. Создавайте фирменные векторные иллюстрации для приложений и сайтов за секунды.",
"api_stability_sd3_t2i": "Генерируйте высококачественные изображения с отличным следованием подсказкам. Идеально для профессионального использования при разрешении 1 мегапиксель."
},
"Upscaling": {
"esrgan_example": "Используйте модели апскейлинга для повышения качества изображений.",
"hiresfix_esrgan_workflow": "Используйте модели апскейлинга на промежуточных этапах.",
"hiresfix_latent_workflow": "Улучшайте качество изображений в latent space.",
"latent_upscale_different_prompt_model": "Увеличивайте и меняйте подсказку на разных проходах."
},
"Video": {
"hunyuan_video_text_to_video": "Генерируйте видео с помощью модели Hunyuan.",
"image_to_video": "Преобразуйте изображения в анимированные видео.",
"image_to_video_wan": "Быстро генерируйте видео из изображений.",
"ltxv_image_to_video": "Преобразуйте статичные изображения в видео.",
"ltxv_text_to_video": "Генерируйте видео по текстовым описаниям.",
"mochi_text_to_video_example": "Создавайте видео с помощью модели Mochi.",
"text_to_video_wan": "Быстро генерируйте видео по текстовым описаниям.",
"txt_to_image_to_video": "Генерируйте изображения по тексту, а затем преобразуйте их в видео.",
"wan2_1_flf2v_720_f16": "Генерируйте видео, контролируя первый и последний кадры.",
"wan2_1_fun_control": "Управляйте генерацией видео с помощью позы, глубины, границ и других параметров.",
"wan2_1_fun_inp": "Создавайте видео по начальному и конечному кадрам."
},
"Video API": {
"api_hailuo_minimax_i2v": "Создавайте изысканные видео из изображений и текста, включая CGI и трендовые эффекты, такие как viral AI hugging. Выбирайте стили и темы для вашего креатива.",
"api_kling_i2v": "Создавайте видео с отличным следованием подсказкам для действий, эмоций и движений камеры. Теперь поддерживаются сложные подсказки с последовательными действиями — вы режиссёр своей сцены.",
"api_luma_i2v": "Преобразуйте статичные изображения в волшебные анимации высокого качества.",
"api_pika_scene": "Используйте несколько изображений как ингредиенты и генерируйте видео, включающие их все.",
"api_pixverse_t2v": "Генерируйте видео с точной интерпретацией подсказок и впечатляющей динамикой.",
"api_pixverse_template_i2v": "Преобразует статичные изображения в динамичные видео с движением и эффектами.",
"api_veo2_i2v": "Используйте Google Veo2 API для генерации видео из изображений."
}
},
"title": "Начните с шаблона"
},
"toastMessages": {
@@ -1206,6 +1363,7 @@
},
"validation": {
"invalidEmail": "Недействительный адрес электронной почты",
"length": "Должно быть {length} символов",
"maxLength": "Должно быть не более {length} символов",
"minLength": "Должно быть не менее {length} символов",
"password": {
@@ -1218,6 +1376,7 @@
"uppercase": "Должен содержать хотя бы одну заглавную букву"
},
"personalDataConsentRequired": "Вы должны согласиться на обработку ваших персональных данных.",
"prefix": "Должно начинаться с {prefix}",
"required": "Обязательно"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,24 @@
"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": "确认密码",
@@ -43,6 +61,7 @@
"loginWithGithub": "使用Github登录",
"loginWithGoogle": "使用Google登录",
"newUser": "新来的?",
"noAssociatedUser": "所提供的 API 密钥未关联任何 Comfy 用户",
"orContinueWith": "或者继续使用",
"passwordLabel": "密码",
"passwordPlaceholder": "输入您的密码",
@@ -55,7 +74,9 @@
"success": "登录成功",
"termsLink": "使用条款",
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
"title": "登录您的账户"
"title": "登录您的账户",
"useApiKey": "Comfy API 密钥",
"userAvatar": "用户头像"
},
"passwordUpdate": {
"success": "密码已更新",
@@ -130,6 +151,7 @@
"Unpin": "取消固定"
},
"credits": {
"apiPricing": "API 价格",
"credits": "积分",
"faqs": "常见问题",
"invoiceHistory": "发票历史",
@@ -148,8 +170,10 @@
"yourCreditBalance": "您的积分余额"
},
"dataTypes": {
"*": "*",
"AUDIO": "音频",
"BOOLEAN": "布尔",
"CAMERA_CONTROL": "相机控制",
"CLIP": "CLIP",
"CLIP_VISION": "CLIP视觉",
"CLIP_VISION_OUTPUT": "CLIP视觉输出",
@@ -166,20 +190,29 @@
"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": "摄像头"
},
@@ -325,6 +358,7 @@
"success": "成功",
"systemInfo": "系统信息",
"terminal": "终端",
"title": "标题",
"unknownError": "未知错误",
"update": "更新",
"updateAvailable": "有更新可用",
@@ -720,10 +754,22 @@
"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": "批处理",
@@ -748,6 +794,7 @@
"instructpix2pix": "InstructPix2Pix",
"latent": "Latent",
"loaders": "加载器",
"lotus": "lotus",
"ltxv": "LTXV",
"mask": "遮罩",
"model": "模型",
@@ -759,10 +806,12 @@
"photomaker": "PhotoMaker",
"postprocessing": "后处理",
"preprocessors": "预处理器",
"primitive": "基础",
"samplers": "采样器",
"sampling": "采样",
"schedulers": "调度器",
"scheduling": "调度",
"sd": "sd",
"sd3": "SD3",
"sigmas": "Sigmas",
"stable_cascade": "StableCascade",
@@ -771,6 +820,10 @@
"unet": "U-Net",
"upscale_diffusion": "放大扩散",
"upscaling": "放大",
"utils": "工具",
"v1": "v1",
"v2": "v2",
"v3": "v3",
"video": "视频",
"video_models": "视频模型"
},
@@ -1052,8 +1105,10 @@
"Custom Nodes": "自定义节点",
"Flux": "Flux",
"Image": "图片",
"Image API": "图像 API",
"Upscaling": "放大",
"Video": "视频"
"Video": "视频",
"Video API": "视频 API"
},
"template": {
"3D": {
@@ -1114,7 +1169,7 @@
"api-openai-dall-e-2-inpaint": "Dall-E 2 局部修复",
"api-openai-dall-e-2-t2i": "Dall-E 2 文生图",
"api-openai-dall-e-3-t2i": "Dall-E 3 文生图",
"api_bfl_flux_pro_t2i": "BFL Flux[Pro] 文生图",
"api_bfl_flux_pro_t2i": "BFL Flux 1.1[pro] Ultra 文生图",
"api_ideogram_v3_t2i": "Ideogram V3 文生图",
"api_luma_photon_i2i": "Luma Photon 图生图",
"api_luma_photon_style_ref": "Luma Photon 风格参考",
@@ -1125,7 +1180,7 @@
"api_recraft_image_gen_with_color_control": "Recraft 颜色控制图像生成",
"api_recraft_image_gen_with_style_control": "Recraft 风格控制图像生成",
"api_recraft_vector_gen": "Recraft 矢量生成",
"api_stability_sd3_t2i": "Stability SD3 文生图"
"api_stability_sd3_t2i": "Stability AI Stable Image Ultra 文生图"
},
"Upscaling": {
"esrgan_example": "ESRGAN",
@@ -1149,12 +1204,114 @@
"api_hailuo_minimax_i2v": "MiniMax 图生视频",
"api_kling_i2v": "Kling 图生视频",
"api_luma_i2v": "Luma 图生视频",
"api_pika_scene": "Pika 场景:图视频",
"api_pixverse_t2v": "PixVerse 文视频",
"api_pixverse_template_i2v": "PixVerse 模板:图视频",
"api_pika_scene": "Pika 场景:图视频",
"api_pixverse_t2v": "PixVerse 文视频",
"api_pixverse_template_i2v": "PixVerse特效:图视频",
"api_veo2_i2v": "Veo2 图生视频"
}
},
"templateDescription": {
"3D": {
"hunyuan-3d-multiview-elf": "使用 Hunyuan3D 2mv 从多视角生成模型。",
"hunyuan-3d-turbo": "使用 Hunyuan3D 2mv turbo 从多视角生成模型。",
"hunyuan3d-non-multiview-train": "使用 Hunyuan3D 2.0 从单视角生成模型。",
"stable_zero123_example": "通过单张图像生成 3D 视图。"
},
"Area Composition": {
"area_composition": "通过区域控制图像构图。",
"area_composition_reversed": "反向区域构图流程。",
"area_composition_square_area_for_subject": "实现主体位置一致性。"
},
"Audio": {
"stable_audio_example": "根据文本描述生成音频。"
},
"Basics": {
"default": "根据文本描述生成图像。",
"embedding_example": "使用文本反演实现风格一致性。",
"gligen_textbox_example": "指定物体的位置和大小。",
"image2image": "使用文本提示转换现有图像。",
"inpain_model_outpainting": "将图像扩展到原始边界之外。",
"inpaint_example": "无缝编辑图像的特定部分。",
"lora": "应用 LoRA 模型以实现特定风格或主题。",
"lora_multiple": "组合多个 LoRA 模型以获得独特效果。"
},
"ControlNet": {
"2_pass_pose_worship": "通过姿态参考生成图像。",
"controlnet_example": "通过参考图像控制图像生成。",
"depth_controlnet": "生成深度感知图像。",
"depth_t2i_adapter": "使用 T2I 适配器快速生成深度感知图像。",
"mixing_controlnets": "组合多个 ControlNet 模型。"
},
"Flux": {
"flux_canny_model_example": "通过边缘检测生成图像。",
"flux_depth_lora_example": "使用深度感知 LoRA 生成图像。",
"flux_dev_checkpoint_example": "使用 flux 开发模型生成图像。",
"flux_fill_inpaint_example": "填充图像中缺失的部分。",
"flux_fill_outpaint_example": "使用 flux 外扩生成图像。",
"flux_redux_model_example": "将参考图像的风格迁移到引导图像生成flux。",
"flux_schnell": "使用 flux schnell 快速生成图像。"
},
"Image": {
"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 一步生成图像。"
},
"Image API": {
"api-openai-dall-e-2-inpaint": "使用 Dall-E 2 API 对图像进行修复。",
"api-openai-dall-e-2-t2i": "使用 Dall-E 2 API 根据文本描述生成图像。",
"api-openai-dall-e-3-t2i": "使用 Dall-E 3 API 根据文本描述生成图像。",
"api_bfl_flux_pro_t2i": "使用 FLUX.1 [pro] 生成高质量、细节丰富、提示遵循性强且多样化的图像。",
"api_ideogram_v3_t2i": "生成高质量图像与提示对齐、照片级真实感和文本渲染。可用于专业级 logo、宣传海报、落地页概念、产品摄影等。轻松打造复杂空间构图、精细背景、精准光影与色彩、逼真环境细节。",
"api_luma_photon_i2i": "结合图像和提示词引导图像生成。",
"api_luma_photon_style_ref": "精确控制并融合风格参考。Luma Photon 捕捉每个参考图像的精髓,让你在保持专业品质的同时融合不同视觉元素。",
"api_openai_image_1_i2i": "使用 GPT Image 1 API 通过图像生成图像。",
"api_openai_image_1_inpaint": "使用 GPT Image 1 API 对图像进行修复。",
"api_openai_image_1_multi_inputs": "使用 GPT Image 1 API 多输入生成图像。",
"api_openai_image_1_t2i": "使用 GPT Image 1 API 根据文本描述生成图像。",
"api_recraft_image_gen_with_color_control": "创建自定义调色板以复用或为每张照片手动选色。匹配品牌色彩,打造专属视觉风格。",
"api_recraft_image_gen_with_style_control": "通过视觉示例控制风格、对齐位置、微调物体。存储并分享风格,实现品牌一致性。",
"api_recraft_vector_gen": "通过文本提示生成矢量图像,使用 Recraft 的 AI 矢量生成器。可用于 logo、海报、图标集、广告、横幅和模型。生成高质量 SVG 文件,几秒内为你的应用或网站创建品牌矢量插画。",
"api_stability_sd3_t2i": "生成高质量、提示遵循性极佳的图像。适用于专业场景,分辨率达 1 兆像素。"
},
"Upscaling": {
"esrgan_example": "使用超分模型提升图像质量。",
"hiresfix_esrgan_workflow": "在中间步骤使用超分模型提升图像质量。",
"hiresfix_latent_workflow": "在 latent 空间中提升图像质量。",
"latent_upscale_different_prompt_model": "放大图像并在不同阶段更换提示词。"
},
"Video": {
"hunyuan_video_text_to_video": "使用 Hunyuan 模型生成视频。",
"image_to_video": "将图像转换为动画视频。",
"image_to_video_wan": "快速将图像生成视频。",
"ltxv_image_to_video": "将静态图像转换为视频。",
"ltxv_text_to_video": "根据文本描述生成视频。",
"mochi_text_to_video_example": "使用 Mochi 模型生成视频。",
"text_to_video_wan": "快速将文本描述生成视频。",
"txt_to_image_to_video": "先由文本生成图像,再转换为视频。",
"wan2_1_flf2v_720_f16": "通过控制首帧和尾帧生成视频。",
"wan2_1_fun_control": "通过姿态、深度、边缘等控制引导视频生成。",
"wan2_1_fun_inp": "通过起始帧和结束帧生成视频。"
},
"Video API": {
"api_hailuo_minimax_i2v": "通过图像和文本生成精致视频,支持 CGI 效果和流行 AI 拥抱等特效。多种视频风格和主题,满足你的创意需求。",
"api_kling_i2v": "生成动作、表情、镜头运动等提示遵循性强的视频。支持复杂提示和顺序动作,让你成为场景导演。",
"api_luma_i2v": "将静态图像瞬间转化为高质量动画。",
"api_pika_scene": "将多张图像作为素材,生成融合所有内容的视频。",
"api_pixverse_t2v": "根据提示生成高还原度、动态效果出色的视频。",
"api_pixverse_template_i2v": "将静态图像转化为带有动态和特效的视频。",
"api_veo2_i2v": "使用 Google Veo2 API 通过图像生成视频。"
}
},
"title": "从模板开始"
},
"toastMessages": {
@@ -1206,6 +1363,7 @@
},
"validation": {
"invalidEmail": "无效的电子邮件地址",
"length": "必须为{length}个字符",
"maxLength": "不能超过{length}个字符",
"minLength": "必须至少有{length}个字符",
"password": {
@@ -1218,6 +1376,7 @@
"uppercase": "必须包含至少一个大写字母"
},
"personalDataConsentRequired": "您必须同意处理您的个人数据。",
"prefix": "必须以 {prefix} 开头",
"required": "必填"
},
"welcome": {

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,16 @@ 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

@@ -58,6 +58,21 @@ 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
@@ -228,6 +243,10 @@ 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()
@@ -544,6 +563,7 @@ 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

@@ -32,6 +32,7 @@ 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'
@@ -1160,6 +1161,7 @@ export class ComfyApp {
let comfyOrgAuthToken =
(await useFirebaseAuthStore().getIdToken()) ?? undefined
let comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
try {
while (this.#queueItems.length) {
@@ -1173,8 +1175,10 @@ export class ComfyApp {
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)

View File

@@ -0,0 +1,110 @@
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { t } from '@/i18n'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useToastStore } from '@/stores/toastStore'
import { ApiKeyAuthHeader } from '@/types/authTypes'
import { operations } from '@/types/comfyRegistryTypes'
type ComfyApiUser =
operations['createCustomer']['responses']['201']['content']['application/json']
const STORAGE_KEY = 'comfy_api_key'
export const useApiKeyAuthStore = defineStore('apiKeyAuth', () => {
const firebaseAuthStore = useFirebaseAuthStore()
const apiKey = useLocalStorage<string | null>(STORAGE_KEY, null)
const toastStore = useToastStore()
const { wrapWithErrorHandlingAsync, toastErrorHandler } = useErrorHandling()
const currentUser = ref<ComfyApiUser | null>(null)
const isAuthenticated = computed(() => !!currentUser.value)
const initializeUserFromApiKey = async () => {
const createCustomerResponse = await firebaseAuthStore.createCustomer()
if (!createCustomerResponse) {
apiKey.value = null
throw new Error(t('auth.login.noAssociatedUser'))
}
currentUser.value = createCustomerResponse
}
watch(
apiKey,
() => {
if (apiKey.value) {
// IF API key is set, initialize user
void initializeUserFromApiKey()
} else {
// IF API key is cleared, clear user
currentUser.value = null
}
},
{ immediate: true }
)
const reportError = (error: unknown) => {
if (error instanceof Error && error.message === 'STORAGE_FAILED') {
toastStore.add({
severity: 'error',
summary: t('auth.apiKey.storageFailed'),
detail: t('auth.apiKey.storageFailedDetail')
})
} else {
toastErrorHandler(error)
}
}
const storeApiKey = wrapWithErrorHandlingAsync(async (newApiKey: string) => {
apiKey.value = newApiKey
toastStore.add({
severity: 'success',
summary: t('auth.apiKey.stored'),
detail: t('auth.apiKey.storedDetail'),
life: 5000
})
return true
}, reportError)
const clearStoredApiKey = wrapWithErrorHandlingAsync(async () => {
apiKey.value = null
toastStore.add({
severity: 'success',
summary: t('auth.apiKey.cleared'),
detail: t('auth.apiKey.clearedDetail'),
life: 5000
})
return true
}, reportError)
const getApiKey = () => apiKey.value
/**
* Retrieves the appropriate authentication header for API requests if an
* API key is available, otherwise returns null.
*/
const getAuthHeader = (): ApiKeyAuthHeader | null => {
const comfyOrgApiKey = getApiKey()
if (comfyOrgApiKey) {
return {
'X-API-KEY': comfyOrgApiKey
}
}
return null
}
return {
// State
currentUser,
isAuthenticated,
// Actions
storeApiKey,
clearStoredApiKey,
getAuthHeader,
getApiKey
}
})

View File

@@ -20,6 +20,8 @@ import { useFirebaseAuth } from 'vuefire'
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
import { t } from '@/i18n'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { type AuthHeader } from '@/types/authTypes'
import { operations } from '@/types/comfyRegistryTypes'
type CreditPurchaseResponse =
@@ -60,6 +62,9 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
prompt: 'select_account'
})
const githubProvider = new GithubAuthProvider()
githubProvider.setCustomParameters({
prompt: 'select_account'
})
// Getters
const isAuthenticated = computed(() => !!currentUser.value)
@@ -90,12 +95,35 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
return null
}
/**
* Retrieves the appropriate authentication header for API requests.
* Checks for authentication in the following order:
* 1. Firebase authentication token (if user is logged in)
* 2. API key (if stored in the browser's credential manager)
*
* @returns {Promise<AuthHeader | null>}
* - A LoggedInAuthHeader with Bearer token if Firebase authenticated
* - An ApiKeyAuthHeader with X-API-KEY if API key exists
* - null if neither authentication method is available
*/
const getAuthHeader = async (): Promise<AuthHeader | null> => {
// If available, set header with JWT used to identify the user to Firebase service
const token = await getIdToken()
if (token) {
return {
Authorization: `Bearer ${token}`
}
}
// If not authenticated with Firebase, try falling back to API key if available
return useApiKeyAuthStore().getAuthHeader()
}
const fetchBalance = async (): Promise<GetCustomerBalanceResponse | null> => {
isFetchingBalance.value = true
try {
const token = await getIdToken()
if (!token) {
isFetchingBalance.value = false
const authHeader = await getAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(
t('toastMessages.userNotAuthenticated')
)
@@ -103,7 +131,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, {
headers: {
Authorization: `Bearer ${token}`
...authHeader,
'Content-Type': 'application/json'
}
})
@@ -130,14 +159,17 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
}
}
const createCustomer = async (
token: string
): Promise<CreateCustomerResponse> => {
const createCustomer = async (): Promise<CreateCustomerResponse> => {
const authHeader = await getAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}
const createCustomerRes = await fetch(`${COMFY_API_BASE_URL}/customers`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
...authHeader,
'Content-Type': 'application/json'
}
})
if (!createCustomerRes.ok) {
@@ -178,7 +210,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
if (!token) {
throw new Error('Cannot create customer: User not authenticated')
}
await createCustomer(token)
await createCustomer()
}
return result
@@ -239,22 +271,22 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
const addCredits = async (
requestBodyContent: CreditPurchasePayload
): Promise<CreditPurchaseResponse> => {
const token = await getIdToken()
if (!token) {
const authHeader = await getAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}
// Ensure customer was created during login/registration
if (!customerCreated.value) {
await createCustomer(token)
await createCustomer()
customerCreated.value = true
}
const response = await fetch(`${COMFY_API_BASE_URL}/customers/credit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
...authHeader,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBodyContent)
})
@@ -279,16 +311,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
const accessBillingPortal = async (
requestBody?: AccessBillingPortalReqBody
): Promise<AccessBillingPortalResponse> => {
const token = await getIdToken()
if (!token) {
const authHeader = await getAuthHeader()
if (!authHeader) {
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
}
const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
...authHeader,
'Content-Type': 'application/json'
},
...(requestBody && {
body: JSON.stringify(requestBody)
@@ -325,6 +357,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
login,
register,
logout,
createCustomer,
getIdToken,
loginWithGoogle,
loginWithGithub,
@@ -332,6 +365,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
fetchBalance,
accessBillingPortal,
sendPasswordReset,
updatePassword: _updatePassword
updatePassword: _updatePassword,
getAuthHeader
}
})

View File

@@ -57,19 +57,63 @@ export const useWorkflowTemplatesStore = defineStore(
return category
})
/**
* Add localization fields to a template.
*/
const addLocalizedFieldsToTemplate = (
template: TemplateInfo,
categoryTitle: string
) => ({
...template,
localizedTitle: st(
`templateWorkflows.template.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
template.title ?? template.name
),
localizedDescription: st(
`templateWorkflows.templateDescription.${normalizeI18nKey(categoryTitle)}.${normalizeI18nKey(template.name)}`,
template.description
)
})
/**
* Add localization fields to all templates in a list of templates.
*/
const localizeTemplateList = (
templates: TemplateInfo[],
categoryTitle: string
) =>
templates.map((template) =>
addLocalizedFieldsToTemplate(template, categoryTitle)
)
/**
* Add localization fields to a template category and all its constituent templates.
*/
const localizeTemplateCategory = (templateCategory: WorkflowTemplates) => ({
...templateCategory,
localizedTitle: st(
`templateWorkflows.category.${normalizeI18nKey(templateCategory.title)}`,
templateCategory.title ?? templateCategory.moduleName
),
templates: localizeTemplateList(
templateCategory.templates,
templateCategory.title
)
})
const groupedTemplates = computed<TemplateGroup[]>(() => {
const allTemplates = [
...sortCategoryTemplates(coreTemplates.value).map((template) => ({
...template,
title: st(
`templateWorkflows.category.${normalizeI18nKey(template.title)}`,
template.title ?? template.moduleName
)
})),
...sortCategoryTemplates(coreTemplates.value).map(
localizeTemplateCategory
),
...Object.entries(customTemplates.value).map(
([moduleName, templates]) => ({
moduleName,
title: moduleName,
localizedTitle: st(
`templateWorkflows.category.${normalizeI18nKey(moduleName)}`,
moduleName
),
templates: templates.map((name) => ({
name,
mediaType: 'image',

9
src/types/authTypes.ts Normal file
View File

@@ -0,0 +1,9 @@
type LoggedInAuthHeader = {
Authorization: `Bearer ${string}`
}
export type ApiKeyAuthHeader = {
'X-API-KEY': string
}
export type AuthHeader = LoggedInAuthHeader | ApiKeyAuthHeader

View File

@@ -9,6 +9,8 @@ export interface TemplateInfo {
mediaSubtype: string
thumbnailVariant?: string
description: string
localizedTitle?: string
localizedDescription?: string
}
export interface WorkflowTemplates {

View File

@@ -3,3 +3,9 @@
<router-view />
</main>
</template>
<script setup lang="ts">
import { useFavicon } from '@vueuse/core'
useFavicon('/assets/favicon.ico')
</script>

View File

@@ -55,7 +55,9 @@ vi.mock('firebase/auth', () => ({
GoogleAuthProvider: class {
setCustomParameters = vi.fn()
},
GithubAuthProvider: vi.fn(),
GithubAuthProvider: class {
setCustomParameters = vi.fn()
},
browserLocalPersistence: 'browserLocalPersistence',
setPersistence: vi.fn().mockResolvedValue(undefined)
}))

View File

@@ -52,6 +52,10 @@ export default defineConfig({
target: DEV_SERVER_COMFYUI_URL
},
'/templates': {
target: DEV_SERVER_COMFYUI_URL
},
'/testsubrouteindex': {
target: 'http://localhost:5173',
rewrite: (path) => path.substring('/testsubrouteindex'.length)