Compare commits
25 Commits
update-reg
...
model_file
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d61221ad3 | ||
|
|
fec5dbcf70 | ||
|
|
c72ba664ee | ||
|
|
6ed870d431 | ||
|
|
4e25a78d2d | ||
|
|
6408623b71 | ||
|
|
fdad2475ce | ||
|
|
5486fb94a0 | ||
|
|
34b1fd5a72 | ||
|
|
aa46524829 | ||
|
|
3bd87820eb | ||
|
|
0f95ed852e | ||
|
|
3501b480d4 | ||
|
|
5fa0401acd | ||
|
|
4d39dc28e0 | ||
|
|
2a297e512d | ||
|
|
2019c1d877 | ||
|
|
2425e32d51 | ||
|
|
b6466c44e5 | ||
|
|
13441add24 | ||
|
|
64ce8ce5ed | ||
|
|
841ff0b46c | ||
|
|
df86da3d67 | ||
|
|
3aea2c120a | ||
|
|
6fdef0308b |
@@ -33,5 +33,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -130,6 +130,11 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 2.1600300525920346,
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
63.071794466403446,
|
||||
75.18055335968394
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,6 +499,11 @@
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
85
browser_tests/assets/execution/partial_execution.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 3,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PreviewAny",
|
||||
"pos": [946.2566528320312, 598.4373168945312],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PreviewAny",
|
||||
"pos": [951.0236206054688, 421.3861083984375],
|
||||
"size": [140, 76],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "source",
|
||||
"type": "*",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewAny"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [575.1760864257812, 504.5214538574219],
|
||||
"size": [270, 58],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [2, 3]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": ["foo"]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[2, 3, 0, 1, 0, "*"],
|
||||
[3, 3, 0, 4, 0, "*"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.19.1",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -160,4 +160,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
@@ -401,4 +405,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"groupNodes": {
|
||||
"hello": {
|
||||
"nodes": [
|
||||
@@ -249,4 +253,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {}
|
||||
"groupNodes": {},
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -92,10 +92,14 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"VHS_latentpreview": true,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": false,
|
||||
"VHS_KeepIntermediate": false
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
},
|
||||
"reroutes": [
|
||||
{
|
||||
"id": 1,
|
||||
|
||||
@@ -106,10 +106,7 @@
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": {
|
||||
"0": 0,
|
||||
"1": 0
|
||||
}
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
|
||||
@@ -368,10 +368,10 @@
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [
|
||||
149.9747408641311,
|
||||
383.8593224280729
|
||||
0,
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
"title": "Load Animated Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"frontendVersion": "1.17.0"
|
||||
"frontendVersion": "1.17.0",
|
||||
"ds": {
|
||||
"offset": [0, 0],
|
||||
"scale": 1
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
|
||||
124
browser_tests/desktop/fixtures/electron.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
type ElectronFixtureOptions = {
|
||||
registerDefaults?: {
|
||||
downloadManager?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type MockFunction = {
|
||||
calls: unknown[][]
|
||||
called: () => Promise<void>
|
||||
handler?: (args: unknown[]) => unknown
|
||||
}
|
||||
|
||||
export type MockElectronAPI = {
|
||||
setup: (method: string, handler: (args: unknown[]) => unknown) => MockFunction
|
||||
}
|
||||
|
||||
export const electronFixture = base.extend<{
|
||||
electronAPI: MockElectronAPI
|
||||
electronOptions: ElectronFixtureOptions
|
||||
}>({
|
||||
electronOptions: [
|
||||
{
|
||||
registerDefaults: {
|
||||
downloadManager: true
|
||||
}
|
||||
},
|
||||
{ option: true }
|
||||
],
|
||||
|
||||
electronAPI: [
|
||||
async ({ page, electronOptions }, use) => {
|
||||
const mocks = new Map<string, MockFunction>()
|
||||
|
||||
await page.exposeFunction(
|
||||
'__handleMockCall',
|
||||
async (method: string, args: unknown[]) => {
|
||||
const mock = mocks.get(method)
|
||||
|
||||
if (electronOptions.registerDefaults?.downloadManager) {
|
||||
if (method === 'DownloadManager.getAllDownloads') {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
if (!mock) return null
|
||||
mock.calls.push(args)
|
||||
return mock.handler ? mock.handler(args) : null
|
||||
}
|
||||
)
|
||||
|
||||
const createMockFunction = (
|
||||
method: string,
|
||||
handler: (args: unknown[]) => unknown
|
||||
): MockFunction => {
|
||||
let resolveNextCall: (() => void) | null = null
|
||||
|
||||
const mockFn: MockFunction = {
|
||||
calls: [],
|
||||
async called() {
|
||||
if (this.calls.length > 0) return
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
resolveNextCall = resolve
|
||||
})
|
||||
},
|
||||
handler: (args: unknown[]) => {
|
||||
const result = handler(args)
|
||||
resolveNextCall?.()
|
||||
resolveNextCall = null
|
||||
return result
|
||||
}
|
||||
}
|
||||
mocks.set(method, mockFn)
|
||||
|
||||
// Add the method to the window.electronAPI object
|
||||
page.evaluate((methodName) => {
|
||||
const w = window as typeof window & {
|
||||
electronAPI: Record<string, any>
|
||||
}
|
||||
|
||||
w.electronAPI[methodName] = async (...args: unknown[]) => {
|
||||
return window['__handleMockCall'](methodName, args)
|
||||
}
|
||||
}, method)
|
||||
|
||||
return mockFn
|
||||
}
|
||||
|
||||
const testAPI: MockElectronAPI = {
|
||||
setup(method, handler) {
|
||||
console.log('adding handler for', method)
|
||||
return createMockFunction(method, handler)
|
||||
}
|
||||
}
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
const getProxy = (...path: string[]) => {
|
||||
return new Proxy(() => {}, {
|
||||
// Handle the proxy itself being called as a function
|
||||
apply: async (target, thisArg, argArray) => {
|
||||
return window['__handleMockCall'](path.join('.'), argArray)
|
||||
},
|
||||
// Handle property access
|
||||
get: (target, prop: string) => {
|
||||
return getProxy(...path, prop)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const w = window as typeof window & {
|
||||
electronAPI: any
|
||||
}
|
||||
|
||||
w.electronAPI = getProxy()
|
||||
console.log('registered electron api')
|
||||
})
|
||||
|
||||
await use(testAPI)
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
172
browser_tests/desktop/importModel.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
import { MockElectronAPI, electronFixture } from './fixtures/electron'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, electronFixture)
|
||||
|
||||
comfyPageFixture.describe('Import Model (web)', () => {
|
||||
comfyPageFixture(
|
||||
'Import dialog does not show when electron api is not available',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.dragAndDropExternalResource({
|
||||
fileName: 'test.bin',
|
||||
buffer: Buffer.from('')
|
||||
})
|
||||
|
||||
// Normal unable to find workflow in file error
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-warn')
|
||||
).toHaveCount(1)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Import Model (electron)', () => {
|
||||
const dropFile = async (
|
||||
comfyPage: ComfyPage,
|
||||
electronAPI: MockElectronAPI,
|
||||
fileName: string,
|
||||
metadata: string
|
||||
) => {
|
||||
const getFilePathMock = electronAPI.setup('getFilePath', () =>
|
||||
Promise.resolve('some/file/path/' + fileName)
|
||||
)
|
||||
|
||||
let buffer: Buffer | undefined
|
||||
if (metadata) {
|
||||
const contentBuffer = Buffer.from(metadata, 'utf-8')
|
||||
|
||||
const headerSizeBuffer = Buffer.alloc(8)
|
||||
headerSizeBuffer.writeBigUInt64LE(BigInt(contentBuffer.length))
|
||||
|
||||
buffer = Buffer.concat([headerSizeBuffer, contentBuffer])
|
||||
}
|
||||
|
||||
await comfyPage.dragAndDropExternalResource({
|
||||
fileName,
|
||||
buffer
|
||||
})
|
||||
await getFilePathMock.called()
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-warn')
|
||||
).toHaveCount(0)
|
||||
await expect(comfyPage.importModelDialog.rootEl).toBeVisible()
|
||||
}
|
||||
|
||||
test('Can show import file dialog by dropping file onto the app', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(comfyPage, electronAPI, 'test.bin', '{}')
|
||||
})
|
||||
|
||||
test('Can autodetect checkpoint model type from modelspec', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
__metadata__: {
|
||||
'modelspec.sai_model_spec': 'test',
|
||||
'modelspec.architecture': 'stable-diffusion-v1'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect lora model type from modelspec', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
__metadata__: {
|
||||
'modelspec.sai_model_spec': 'test',
|
||||
'modelspec.architecture': 'Flux.1-AE/lora'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'loras'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect checkpoint model type from header keys', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
'model.diffusion_model.input_blocks.0.0.bias': {}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can autodetect lora model type from header keys', async ({
|
||||
comfyPage,
|
||||
electronAPI
|
||||
}) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'file.safetensors',
|
||||
JSON.stringify({
|
||||
'lora_unet_down_blocks_0_attentions_0_proj_in.alpha': {}
|
||||
})
|
||||
)
|
||||
|
||||
await expect(comfyPage.importModelDialog.modelTypeInput).toHaveValue(
|
||||
'loras'
|
||||
)
|
||||
})
|
||||
|
||||
test('Can import file', async ({ comfyPage, electronAPI }) => {
|
||||
await dropFile(
|
||||
comfyPage,
|
||||
electronAPI,
|
||||
'checkpoint_modelspec.safetensors',
|
||||
'{}'
|
||||
)
|
||||
|
||||
const importModelMock = electronAPI.setup(
|
||||
'importModel',
|
||||
() => new Promise((resolve) => setTimeout(resolve, 100))
|
||||
)
|
||||
|
||||
// Model type is required so select one
|
||||
await expect(comfyPage.importModelDialog.importButton).toBeDisabled()
|
||||
await comfyPage.importModelDialog.modelTypeInput.fill('checkpoints')
|
||||
await expect(comfyPage.importModelDialog.importButton).toBeEnabled()
|
||||
|
||||
// Click import, ensure API is called
|
||||
await comfyPage.importModelDialog.importButton.click()
|
||||
await importModelMock.called()
|
||||
|
||||
// Toast should be shown and dialog closes
|
||||
await expect(
|
||||
comfyPage.page.locator('.p-toast-message.p-toast-message-success')
|
||||
).toHaveCount(1)
|
||||
|
||||
await expect(comfyPage.importModelDialog.rootEl).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { ImportModelDialog } from './components/ImportModelDialog'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
NodeLibrarySidebarTab,
|
||||
@@ -140,6 +141,7 @@ export class ComfyPage {
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
public readonly importModelDialog: ImportModelDialog
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -165,6 +167,7 @@ export class ComfyPage {
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
this.importModelDialog = new ImportModelDialog(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -469,6 +472,7 @@ export class ComfyPage {
|
||||
fileName?: string
|
||||
url?: string
|
||||
dropPosition?: Position
|
||||
buffer?: Buffer
|
||||
} = {}
|
||||
) {
|
||||
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
|
||||
@@ -487,7 +491,7 @@ export class ComfyPage {
|
||||
// Dropping a file from the filesystem
|
||||
if (fileName) {
|
||||
const filePath = this.assetPath(fileName)
|
||||
const buffer = fs.readFileSync(filePath)
|
||||
const buffer = options.buffer ?? fs.readFileSync(filePath)
|
||||
|
||||
const getFileType = (fileName: string) => {
|
||||
if (fileName.endsWith('.png')) return 'image/png'
|
||||
|
||||
17
browser_tests/fixtures/components/ImportModelDialog.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Page } from '@playwright/test'
|
||||
|
||||
export class ImportModelDialog {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
get rootEl() {
|
||||
return this.page.locator('div[aria-labelledby="global-import-model"]')
|
||||
}
|
||||
|
||||
get modelTypeInput() {
|
||||
return this.rootEl.locator('#model-type')
|
||||
}
|
||||
|
||||
get importButton() {
|
||||
return this.rootEl.getByLabel('Import')
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,27 @@ test.describe('Execution', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Execute to selected output nodes', () => {
|
||||
test('Execute to selected output nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution/partial_execution')
|
||||
const input = await comfyPage.getNodeRefById(3)
|
||||
const output1 = await comfyPage.getNodeRefById(1)
|
||||
const output2 = await comfyPage.getNodeRefById(4)
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
|
||||
await output1.click('title')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
|
||||
// @note: Wait for the execution to finish. We might want to move to a more
|
||||
// reliable way to wait for the execution to finish. Workflow in this test
|
||||
// is simple enough that this is fine for now.
|
||||
await comfyPage.page.waitForTimeout(200)
|
||||
|
||||
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
|
||||
expect(await (await output2.getWidget(0)).getValue()).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -666,6 +666,12 @@ test.describe('Load workflow', () => {
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
|
||||
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
|
||||
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 57 KiB |
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.19.4",
|
||||
"version": "1.19.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.19.4",
|
||||
"version": "1.19.7",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.15.3",
|
||||
"@comfyorg/litegraph": "^0.15.8",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -482,9 +482,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.3.tgz",
|
||||
"integrity": "sha512-YpHhNgV6a1bsi/NCuK/u5m9bgI3HK5qLXCLR2aKUJ4MPVfdILodqcbkKVBOb4li6KKUyg7plQ9GJpml6F2/fYQ==",
|
||||
"version": "0.15.8",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.15.8.tgz",
|
||||
"integrity": "sha512-pW3W9c9wQsSOultjzRZqSxcXv07sppFybGsEdfnPqLYXqlJcrsysU0NbPTSaZqOAJdFHMu2d0J6P02Id4oFB/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.19.4",
|
||||
"version": "1.19.7",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -72,7 +72,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.43",
|
||||
"@comfyorg/litegraph": "^0.15.3",
|
||||
"@comfyorg/litegraph": "^0.15.8",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
16
src/App.vue
@@ -16,8 +16,10 @@ import { computed, onMounted } from 'vue'
|
||||
|
||||
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { useDialogService } from './services/dialogService'
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -46,6 +48,20 @@ onMounted(() => {
|
||||
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
|
||||
// Handle file drops to import models via electron
|
||||
api.addEventListener('unhandledFileDrop', async (e) => {
|
||||
e.preventDefault() // Prevent unable to find workflow in file error
|
||||
|
||||
const filePath = await electronAPI()['getFilePath'](e.detail.file)
|
||||
|
||||
if (filePath) {
|
||||
useDialogService().showImportModelDialog({
|
||||
path: filePath,
|
||||
file: e.detail.file
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
123
src/components/dialog/content/ImportModelDialogContent.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="px-4 py-2 h-full gap-2 flex flex-col">
|
||||
<h2 class="text-4xl font-normal my-0">
|
||||
{{ t('importModelDialog.title') }}
|
||||
</h2>
|
||||
<span class="text-muted">{{ path }}</span>
|
||||
<div class="flex flex-col gap-2 mt-4">
|
||||
<IftaLabel>
|
||||
<Select
|
||||
v-model="selectedType"
|
||||
:options="modelFolders"
|
||||
editable
|
||||
filter
|
||||
labelId="model-type"
|
||||
:disabled="importing"
|
||||
/>
|
||||
<label for="model-type">Type</label>
|
||||
</IftaLabel>
|
||||
</div>
|
||||
<Message severity="error" v-if="importError">{{ importError }}</Message>
|
||||
</div>
|
||||
<footer>
|
||||
<div class="flex justify-between gap-2 p-4">
|
||||
<SelectButton
|
||||
v-model="selectedImportMode"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
:options="importModes"
|
||||
:disabled="importing"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
label="Cancel"
|
||||
severity="secondary"
|
||||
@click="dialogStore.closeDialog()"
|
||||
:disabled="importing"
|
||||
></Button>
|
||||
<Button
|
||||
type="button"
|
||||
label="Import"
|
||||
@click="importModel()"
|
||||
:icon="importIcon"
|
||||
:loading="importing"
|
||||
:disabled="!selectedType"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import IftaLabel from 'primevue/iftalabel'
|
||||
import Message from 'primevue/message'
|
||||
import Select from 'primevue/select'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { guessModelType } from '@/utils/safetensorsUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const { path, file } = defineProps<{
|
||||
path: string
|
||||
file: File
|
||||
}>()
|
||||
|
||||
const importModes = ref([
|
||||
{ label: t('importModelDialog.move'), value: 'move' },
|
||||
{ label: t('importModelDialog.copy'), value: 'copy' }
|
||||
])
|
||||
const modelStore = useModelStore()
|
||||
const modelFolders = ref<string[]>()
|
||||
const selectedType = ref<string>()
|
||||
const selectedImportMode = ref<string>('move')
|
||||
const importing = ref<boolean>(false)
|
||||
const importError = ref<string>()
|
||||
|
||||
const importIcon = computed(() => {
|
||||
return selectedImportMode.value === 'move'
|
||||
? 'pi pi-file-import'
|
||||
: 'pi pi-copy'
|
||||
})
|
||||
|
||||
const importModel = async () => {
|
||||
importing.value = true
|
||||
try {
|
||||
await electronAPI()?.['importModel'](
|
||||
file,
|
||||
selectedType.value,
|
||||
selectedImportMode.value
|
||||
)
|
||||
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
dialogStore.closeDialog()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
importError.value = error.message
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
if (!modelStore.modelFolders.length) {
|
||||
await modelStore.loadModelFolders()
|
||||
}
|
||||
|
||||
modelFolders.value = modelStore.modelFolders.map((folder) => folder.directory)
|
||||
const type = await guessModelType(file)
|
||||
|
||||
if (!selectedType.value) {
|
||||
selectedType.value = type
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
<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">
|
||||
<!-- Normal User Panel -->
|
||||
<div v-if="isLoggedIn" class="flex flex-col gap-2">
|
||||
<UserAvatar
|
||||
v-if="user.photoURL"
|
||||
:photo-url="user.photoURL"
|
||||
v-if="userPhotoUrl"
|
||||
:photo-url="userPhotoUrl"
|
||||
shape="circle"
|
||||
size="large"
|
||||
/>
|
||||
@@ -17,7 +18,7 @@
|
||||
{{ $t('userSettings.name') }}
|
||||
</h3>
|
||||
<div class="text-muted">
|
||||
{{ user.displayName || $t('userSettings.notSet') }}
|
||||
{{ userDisplayName || $t('userSettings.notSet') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,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>
|
||||
|
||||
@@ -90,51 +91,22 @@ 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>
|
||||
|
||||
114
src/components/dialog/content/signin/ApiKeyForm.test.ts
Normal 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'
|
||||
)
|
||||
})
|
||||
})
|
||||
111
src/components/dialog/content/signin/ApiKeyForm.vue
Normal 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>
|
||||
@@ -6,6 +6,7 @@
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
>
|
||||
<ExecuteButton v-show="nodeSelected" />
|
||||
<ColorPickerButton v-show="nodeSelected || groupSelected" />
|
||||
<Button
|
||||
v-show="nodeSelected"
|
||||
@@ -74,6 +75,7 @@ import Panel from 'primevue/panel'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
import { useRefreshableSelection } from '@/composables/useRefreshableSelection'
|
||||
import { st, t } from '@/i18n'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
71
src/components/graph/selectionToolbox/ExecuteButton.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: isDisabled
|
||||
? t('selectionToolbox.executeButton.disabledTooltip')
|
||||
: t('selectionToolbox.executeButton.tooltip'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
:severity="isDisabled ? 'secondary' : 'success'"
|
||||
text
|
||||
:disabled="isDisabled"
|
||||
@mouseenter="() => handleMouseEnter()"
|
||||
@mouseleave="() => handleMouseLeave()"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i-lucide:play />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const buttonHovered = ref(false)
|
||||
const selectedOutputNodes = computed(
|
||||
() =>
|
||||
canvasStore.selectedItems.filter(
|
||||
(item) => isLGraphNode(item) && item.constructor.nodeData.output_node
|
||||
) as LGraphNode[]
|
||||
)
|
||||
|
||||
const isDisabled = computed(() => selectedOutputNodes.value.length === 0)
|
||||
|
||||
function outputNodeStokeStyle(this: LGraphNode) {
|
||||
if (
|
||||
this.selected &&
|
||||
this.constructor.nodeData.output_node &&
|
||||
buttonHovered.value
|
||||
) {
|
||||
return { color: 'orange', lineWidth: 2, padding: 10 }
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
buttonHovered.value = true
|
||||
for (const node of selectedOutputNodes.value) {
|
||||
node.strokeStyles['outputNode'] = outputNodeStokeStyle
|
||||
}
|
||||
canvas.setDirty(true)
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
buttonHovered.value = false
|
||||
canvas.setDirty(true)
|
||||
}
|
||||
|
||||
const handleClick = async () => {
|
||||
await commandStore.execute('Comfy.QueueSelectedOutputNodes')
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<Button
|
||||
v-if="isAuthenticated"
|
||||
v-if="isLoggedIn"
|
||||
class="user-profile-button p-1"
|
||||
severity="secondary"
|
||||
text
|
||||
@@ -30,15 +30,14 @@ import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
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>
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
<div class="flex flex-col items-center">
|
||||
<UserAvatar
|
||||
class="mb-3"
|
||||
:photo-url="user?.photoURL"
|
||||
:photo-url="userPhotoUrl"
|
||||
:pt:icon:class="{
|
||||
'!text-2xl': !user?.photoURL
|
||||
'!text-2xl': !userPhotoUrl
|
||||
}"
|
||||
size="large"
|
||||
/>
|
||||
|
||||
<!-- 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') }}
|
||||
@@ -52,20 +64,18 @@
|
||||
<script setup lang="ts">
|
||||
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()
|
||||
})
|
||||
|
||||
101
src/composables/auth/useCurrentUser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -312,6 +312,28 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
await app.queuePrompt(-1, batchCount)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.QueueSelectedOutputNodes',
|
||||
icon: 'pi pi-play',
|
||||
label: 'Queue Selected Output Nodes',
|
||||
versionAdded: '1.19.6',
|
||||
function: async () => {
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
const queueNodeIds = getSelectedNodes()
|
||||
.filter((node) => node.constructor.nodeData.output_node)
|
||||
.map((node) => node.id)
|
||||
if (queueNodeIds.length === 0) {
|
||||
toastStore.add({
|
||||
severity: 'error',
|
||||
summary: t('toastMessages.nothingToQueue'),
|
||||
detail: t('toastMessages.pleaseSelectOutputNodes'),
|
||||
life: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
await app.queuePrompt(0, batchCount, queueNodeIds)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ShowSettingsDialog',
|
||||
icon: 'pi pi-cog',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { computed, ref, watchEffect } from 'vue'
|
||||
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
@@ -8,9 +9,11 @@ interface RefreshableItem {
|
||||
refresh: () => Promise<void> | void
|
||||
}
|
||||
|
||||
type RefreshableWidget = IWidget & RefreshableItem
|
||||
type RefreshableWidget = IBaseWidget & RefreshableItem
|
||||
|
||||
const isRefreshableWidget = (widget: IWidget): widget is RefreshableWidget =>
|
||||
const isRefreshableWidget = (
|
||||
widget: IBaseWidget
|
||||
): widget is RefreshableWidget =>
|
||||
'refresh' in widget && typeof widget.refresh === 'function'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph, isComboWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
@@ -33,7 +33,7 @@ const ext = {
|
||||
const clickedComboValue = currentNode?.widgets
|
||||
?.filter(
|
||||
(w) =>
|
||||
w.type === 'combo' && w.options.values?.length === values.length
|
||||
isComboWidget(w) && w.options.values?.length === values.length
|
||||
)
|
||||
.find((w) =>
|
||||
// @ts-expect-error Poorly typed; filter above "should" mitigate exceptions
|
||||
|
||||
@@ -1231,7 +1231,6 @@ export class GroupNodeHandler {
|
||||
const widgetName = self.groupData.oldToNewWidgetMap[n][w]
|
||||
const widget = this.widgets.find((w) => w.name === widgetName)
|
||||
if (widget) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widget.type = 'hidden'
|
||||
widget.computeSize = () => [0, -4]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { IStringWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import type {
|
||||
IComboWidget,
|
||||
IStringWidget
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
@@ -116,7 +119,7 @@ useExtensionService().registerExtension({
|
||||
if (fileInput.files?.length) {
|
||||
const modelWidget = node.widgets?.find(
|
||||
(w) => w.name === 'model_file'
|
||||
) as IStringWidget
|
||||
) as IComboWidget & { options: { values: string[] } }
|
||||
|
||||
node.properties['Texture'] = undefined
|
||||
|
||||
@@ -138,7 +141,6 @@ useExtensionService().registerExtension({
|
||||
|
||||
if (uploadPath && modelWidget) {
|
||||
if (!modelWidget.options?.values?.includes(uploadPath)) {
|
||||
// @ts-expect-error Fails due to earlier type-assertion of IStringWidget
|
||||
modelWidget.options?.values?.push(uploadPath)
|
||||
}
|
||||
|
||||
@@ -292,7 +294,6 @@ useExtensionService().registerExtension({
|
||||
|
||||
if (uploadPath && modelWidget) {
|
||||
if (!modelWidget.options?.values?.includes(uploadPath)) {
|
||||
// @ts-expect-error Fails due to earlier type-assertion of IStringWidget
|
||||
modelWidget.options?.values?.push(uploadPath)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
@@ -15,17 +15,20 @@ class Load3DConfiguration {
|
||||
|
||||
configure(
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
modelWidget: IBaseWidget,
|
||||
cameraState?: any,
|
||||
width: IWidget | null = null,
|
||||
height: IWidget | null = null
|
||||
width: IBaseWidget | null = null,
|
||||
height: IBaseWidget | null = null
|
||||
) {
|
||||
this.setupModelHandling(modelWidget, loadFolder, cameraState)
|
||||
this.setupTargetSize(width, height)
|
||||
this.setupDefaultProperties()
|
||||
}
|
||||
|
||||
private setupTargetSize(width: IWidget | null, height: IWidget | null) {
|
||||
private setupTargetSize(
|
||||
width: IBaseWidget | null,
|
||||
height: IBaseWidget | null
|
||||
) {
|
||||
if (width && height) {
|
||||
this.load3d.setTargetSize(width.value as number, height.value as number)
|
||||
|
||||
@@ -51,7 +54,7 @@ class Load3DConfiguration {
|
||||
}
|
||||
|
||||
private setupModelHandling(
|
||||
modelWidget: IWidget,
|
||||
modelWidget: IBaseWidget,
|
||||
loadFolder: 'input' | 'output',
|
||||
cameraState?: any
|
||||
) {
|
||||
|
||||
@@ -90,7 +90,13 @@ app.registerExtension({
|
||||
name: 'Comfy.AudioWidget',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (
|
||||
['LoadAudio', 'SaveAudio', 'PreviewAudio'].includes(
|
||||
[
|
||||
'LoadAudio',
|
||||
'SaveAudio',
|
||||
'PreviewAudio',
|
||||
'SaveAudioMP3',
|
||||
'SaveAudioOpus'
|
||||
].includes(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodeType.prototype.comfyClass
|
||||
)
|
||||
|
||||
@@ -3,11 +3,11 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
IWidget,
|
||||
LLink,
|
||||
Vector2
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { CanvasMouseEvent } from '@comfyorg/litegraph/dist/types/events'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import {
|
||||
type CallbackParams,
|
||||
@@ -226,7 +226,7 @@ export class PrimitiveNode extends LGraphNode {
|
||||
|
||||
// Store current size as addWidget resizes the node
|
||||
const [oldWidth, oldHeight] = this.size
|
||||
let widget: IWidget | undefined
|
||||
let widget: IBaseWidget | undefined
|
||||
if (type in ComfyWidgets) {
|
||||
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
|
||||
} else {
|
||||
@@ -426,7 +426,7 @@ function getConfig(this: LGraphNode, widgetName: string) {
|
||||
*/
|
||||
export function convertToInput(
|
||||
node: LGraphNode,
|
||||
widget: IWidget
|
||||
widget: IBaseWidget
|
||||
): INodeInputSlot | undefined {
|
||||
console.warn(
|
||||
'Please remove call to convertToInput. Widget to socket conversion is no longer necessary, as they co-exist now.'
|
||||
@@ -506,8 +506,7 @@ export function mergeIfValid(
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return { customConfig: customSpec[1] }
|
||||
return { customConfig: customSpec?.[1] ?? {} }
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "Queue Prompt (Front)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "Queue Selected Output Nodes"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "Redo"
|
||||
},
|
||||
|
||||
@@ -803,6 +803,7 @@
|
||||
"Open": "Open",
|
||||
"Queue Prompt": "Queue Prompt",
|
||||
"Queue Prompt (Front)": "Queue Prompt (Front)",
|
||||
"Queue Selected Output Nodes": "Queue Selected Output Nodes",
|
||||
"Redo": "Redo",
|
||||
"Refresh Node Definitions": "Refresh Node Definitions",
|
||||
"Save": "Save",
|
||||
@@ -1021,6 +1022,7 @@
|
||||
"image": "image",
|
||||
"preprocessors": "preprocessors",
|
||||
"advanced": "advanced",
|
||||
"guidance": "guidance",
|
||||
"loaders": "loaders",
|
||||
"model_merging": "model_merging",
|
||||
"attention_experiments": "attention_experiments",
|
||||
@@ -1033,44 +1035,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",
|
||||
@@ -1089,18 +1111,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"
|
||||
},
|
||||
@@ -1197,6 +1228,8 @@
|
||||
"resizeNodeMatchOutput": "Resize Node to match output"
|
||||
},
|
||||
"toastMessages": {
|
||||
"nothingToQueue": "Nothing to queue",
|
||||
"pleaseSelectOutputNodes": "Please select output nodes",
|
||||
"no3dScene": "No 3D scene to apply texture",
|
||||
"failedToApplyTexture": "Failed to apply texture",
|
||||
"no3dSceneToExport": "No 3D scene to export",
|
||||
@@ -1229,8 +1262,27 @@
|
||||
"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",
|
||||
@@ -1256,7 +1308,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",
|
||||
@@ -1287,6 +1340,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",
|
||||
@@ -1303,6 +1358,7 @@
|
||||
"yourCreditBalance": "Your credit balance",
|
||||
"purchaseCredits": "Purchase Credits",
|
||||
"invoiceHistory": "Invoice History",
|
||||
"apiPricing": "API Pricing",
|
||||
"faqs": "FAQs",
|
||||
"messageSupport": "Message Support",
|
||||
"lastUpdated": "Last updated",
|
||||
@@ -1343,5 +1399,17 @@
|
||||
"title": "Run!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"tooltip": "Execute to selected output nodes (Highlighted with orange border)",
|
||||
"disabledTooltip": "No output nodes selected"
|
||||
}
|
||||
},
|
||||
"importModelDialog": {
|
||||
"title": "Import Model",
|
||||
"type": "Type",
|
||||
"move": "Move",
|
||||
"copy": "Copy"
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "Prompt de Cola (Frente)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "Encolar nodos de salida seleccionados"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "Rehacer"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
@@ -56,6 +75,7 @@
|
||||
"termsLink": "Términos de uso",
|
||||
"termsText": "Al hacer clic en \"Siguiente\" o \"Registrarse\", aceptas nuestros",
|
||||
"title": "Inicia sesión en tu cuenta",
|
||||
"useApiKey": "Clave API de Comfy",
|
||||
"userAvatar": "Avatar de usuario"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -131,6 +151,7 @@
|
||||
"Unpin": "Desanclar"
|
||||
},
|
||||
"credits": {
|
||||
"apiPricing": "Precios de la API",
|
||||
"credits": "Créditos",
|
||||
"faqs": "Preguntas frecuentes",
|
||||
"invoiceHistory": "Historial de facturas",
|
||||
@@ -149,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",
|
||||
@@ -167,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"
|
||||
},
|
||||
@@ -695,6 +727,7 @@
|
||||
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
|
||||
"Queue Prompt": "Indicador de cola",
|
||||
"Queue Prompt (Front)": "Indicador de cola (Frente)",
|
||||
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
|
||||
"Quit": "Salir",
|
||||
"Redo": "Rehacer",
|
||||
"Refresh Node Definitions": "Actualizar definiciones de nodo",
|
||||
@@ -731,10 +764,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",
|
||||
@@ -759,6 +804,7 @@
|
||||
"instructpix2pix": "instruirpix2pix",
|
||||
"latent": "latent",
|
||||
"loaders": "cargadores",
|
||||
"lotus": "lotus",
|
||||
"ltxv": "ltxv",
|
||||
"mask": "mask",
|
||||
"model": "modelo",
|
||||
@@ -770,10 +816,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",
|
||||
@@ -782,6 +830,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"
|
||||
},
|
||||
@@ -801,6 +853,12 @@
|
||||
},
|
||||
"title": "Tu dispositivo no es compatible"
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"disabledTooltip": "No hay nodos de salida seleccionados",
|
||||
"tooltip": "Ejecutar en los nodos de salida seleccionados (resaltados con borde naranja)"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "Has modificado las siguientes configuraciones del servidor. Reinicia para aplicar los cambios.",
|
||||
"restart": "Reiniciar",
|
||||
@@ -1297,8 +1355,10 @@
|
||||
"noTemplatesToExport": "No hay plantillas para exportar",
|
||||
"nodeDefinitionsUpdated": "Definiciones de nodos actualizadas",
|
||||
"nothingToGroup": "Nada para agrupar",
|
||||
"nothingToQueue": "Nada para poner en cola",
|
||||
"pendingTasksDeleted": "Tareas pendientes eliminadas",
|
||||
"pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para",
|
||||
"pleaseSelectOutputNodes": "Por favor, selecciona los nodos de salida",
|
||||
"unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo",
|
||||
"unauthorizedDomain": "Tu dominio {domain} no está autorizado para usar este servicio. Por favor, contacta a {email} para agregar tu dominio a la lista blanca.",
|
||||
"updateRequested": "Actualización solicitada",
|
||||
@@ -1321,6 +1381,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": {
|
||||
@@ -1333,6 +1394,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": {
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "Invite de file d'attente (avant)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "Mettre en file d’attente les nœuds de sortie sélectionnés"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "Refaire"
|
||||
},
|
||||
|
||||
@@ -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 l’enregistrement 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",
|
||||
@@ -56,6 +75,7 @@
|
||||
"termsLink": "Conditions d'utilisation",
|
||||
"termsText": "En cliquant sur \"Suivant\" ou \"S'inscrire\", vous acceptez nos",
|
||||
"title": "Connectez-vous à votre compte",
|
||||
"useApiKey": "Clé API Comfy",
|
||||
"userAvatar": "Avatar utilisateur"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -131,6 +151,7 @@
|
||||
"Unpin": "Désépingler"
|
||||
},
|
||||
"credits": {
|
||||
"apiPricing": "Tarification de l’API",
|
||||
"credits": "Crédits",
|
||||
"faqs": "FAQ",
|
||||
"invoiceHistory": "Historique des factures",
|
||||
@@ -149,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",
|
||||
@@ -167,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"
|
||||
},
|
||||
@@ -364,6 +396,12 @@
|
||||
"inbox": "Boîte de réception",
|
||||
"star": "Étoile"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "Copier",
|
||||
"move": "Déplacer",
|
||||
"title": "Importer le modèle",
|
||||
"type": "Type"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "Répertoire des données de l'application ComfyUI. Stocke :\n- Logs\n- Configurations du serveur",
|
||||
"appPathLocationTooltip": "Répertoire des ressources de l'application ComfyUI. Stocke le code et les ressources de ComfyUI",
|
||||
@@ -695,6 +733,7 @@
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Queue Selected Output Nodes": "Mettre en file d’attente les nœuds de sortie sélectionnés",
|
||||
"Quit": "Quitter",
|
||||
"Redo": "Refaire",
|
||||
"Refresh Node Definitions": "Actualiser les définitions de nœud",
|
||||
@@ -731,10 +770,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",
|
||||
@@ -759,6 +810,7 @@
|
||||
"instructpix2pix": "instructpix2pix",
|
||||
"latent": "latent",
|
||||
"loaders": "chargeurs",
|
||||
"lotus": "lotus",
|
||||
"ltxv": "ltxv",
|
||||
"mask": "masque",
|
||||
"model": "modèle",
|
||||
@@ -770,10 +822,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",
|
||||
@@ -782,6 +836,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"
|
||||
},
|
||||
@@ -801,6 +859,12 @@
|
||||
},
|
||||
"title": "Votre appareil n'est pas pris en charge"
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"disabledTooltip": "Aucun nœud de sortie sélectionné",
|
||||
"tooltip": "Exécuter vers les nœuds de sortie sélectionnés (surlignés avec une bordure orange)"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "Vous avez modifié les configurations suivantes du serveur. Redémarrez pour appliquer les modifications.",
|
||||
"restart": "Redémarrer",
|
||||
@@ -1297,8 +1361,10 @@
|
||||
"noTemplatesToExport": "Aucun modèle à exporter",
|
||||
"nodeDefinitionsUpdated": "Définitions de nœuds mises à jour",
|
||||
"nothingToGroup": "Rien à regrouper",
|
||||
"nothingToQueue": "Rien à ajouter à la file d’attente",
|
||||
"pendingTasksDeleted": "Tâches en attente supprimées",
|
||||
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
|
||||
"pleaseSelectOutputNodes": "Veuillez sélectionner les nœuds de sortie",
|
||||
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
|
||||
"unauthorizedDomain": "Votre domaine {domain} n'est pas autorisé à utiliser ce service. Veuillez contacter {email} pour ajouter votre domaine à la liste blanche.",
|
||||
"updateRequested": "Mise à jour demandée",
|
||||
@@ -1321,6 +1387,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": {
|
||||
@@ -1333,6 +1400,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": {
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "キュープロンプト(フロント)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "選択した出力ノードをキューに追加"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "やり直す"
|
||||
},
|
||||
|
||||
@@ -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": "パスワードを入力してください",
|
||||
@@ -56,6 +75,7 @@
|
||||
"termsLink": "利用規約",
|
||||
"termsText": "「次へ」または「サインアップ」をクリックすると、私たちの",
|
||||
"title": "アカウントにログインする",
|
||||
"useApiKey": "Comfy APIキー",
|
||||
"userAvatar": "ユーザーアバター"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -131,6 +151,7 @@
|
||||
"Unpin": "ピンを解除"
|
||||
},
|
||||
"credits": {
|
||||
"apiPricing": "API料金",
|
||||
"credits": "クレジット",
|
||||
"faqs": "よくある質問",
|
||||
"invoiceHistory": "請求履歴",
|
||||
@@ -149,8 +170,10 @@
|
||||
"yourCreditBalance": "あなたのクレジット残高"
|
||||
},
|
||||
"dataTypes": {
|
||||
"*": "*",
|
||||
"AUDIO": "オーディオ",
|
||||
"BOOLEAN": "ブール",
|
||||
"CAMERA_CONTROL": "カメラコントロール",
|
||||
"CLIP": "CLIP",
|
||||
"CLIP_VISION": "CLIP_VISION",
|
||||
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
|
||||
@@ -167,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": "ウェブカメラ"
|
||||
},
|
||||
@@ -364,6 +396,12 @@
|
||||
"inbox": "受信トレイ",
|
||||
"star": "星"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "コピー",
|
||||
"move": "移動",
|
||||
"title": "モデルをインポート",
|
||||
"type": "タイプ"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "ComfyUIのアプリデータディレクトリ。保存内容:\n- ログ\n- サーバー設定",
|
||||
"appPathLocationTooltip": "ComfyUIのアプリ資産ディレクトリ。ComfyUIのコードとアセットを保存します",
|
||||
@@ -695,6 +733,7 @@
|
||||
"Previous Opened Workflow": "前に開いたワークフロー",
|
||||
"Queue Prompt": "キューのプロンプト",
|
||||
"Queue Prompt (Front)": "キューのプロンプト (前面)",
|
||||
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
|
||||
"Quit": "終了",
|
||||
"Redo": "やり直す",
|
||||
"Refresh Node Definitions": "ノード定義を更新",
|
||||
@@ -731,10 +770,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": "バッチ",
|
||||
@@ -759,6 +810,7 @@
|
||||
"instructpix2pix": "インストラクションピクス2ピクス",
|
||||
"latent": "潜在",
|
||||
"loaders": "ローダー",
|
||||
"lotus": "lotus",
|
||||
"ltxv": "LTXV",
|
||||
"mask": "マスク",
|
||||
"model": "モデル",
|
||||
@@ -770,10 +822,12 @@
|
||||
"photomaker": "photomaker",
|
||||
"postprocessing": "ポストプロセッシング",
|
||||
"preprocessors": "前処理",
|
||||
"primitive": "プリミティブ",
|
||||
"samplers": "サンプラー",
|
||||
"sampling": "サンプリング",
|
||||
"schedulers": "スケジューラー",
|
||||
"scheduling": "スケジューリング",
|
||||
"sd": "sd",
|
||||
"sd3": "SD3",
|
||||
"sigmas": "シグマ",
|
||||
"stable_cascade": "安定したカスケード",
|
||||
@@ -782,6 +836,10 @@
|
||||
"unet": "U-Net",
|
||||
"upscale_diffusion": "アップスケール拡散",
|
||||
"upscaling": "アップスケーリング",
|
||||
"utils": "ユーティリティ",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "ビデオ",
|
||||
"video_models": "ビデオモデル"
|
||||
},
|
||||
@@ -801,6 +859,12 @@
|
||||
},
|
||||
"title": "お使いのデバイスはサポートされていません"
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"disabledTooltip": "出力ノードが選択されていません",
|
||||
"tooltip": "選択した出力ノードに実行(オレンジ色の枠でハイライト表示)"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "以下のサーバー設定を変更しました。変更を適用するには再起動してください。",
|
||||
"restart": "再起動",
|
||||
@@ -1297,8 +1361,10 @@
|
||||
"noTemplatesToExport": "エクスポートするテンプレートがありません",
|
||||
"nodeDefinitionsUpdated": "ノード定義が更新されました",
|
||||
"nothingToGroup": "グループ化するものがありません",
|
||||
"nothingToQueue": "キューに追加する項目がありません",
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
|
||||
"pleaseSelectOutputNodes": "出力ノードを選択してください",
|
||||
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
|
||||
"unauthorizedDomain": "あなたのドメイン {domain} はこのサービスを利用する権限がありません。ご利用のドメインをホワイトリストに追加するには、{email} までご連絡ください。",
|
||||
"updateRequested": "更新が要求されました",
|
||||
@@ -1321,6 +1387,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "無効なメールアドレス",
|
||||
"length": "{length}文字でなければなりません",
|
||||
"maxLength": "{length}文字以下でなければなりません",
|
||||
"minLength": "{length}文字以上でなければなりません",
|
||||
"password": {
|
||||
@@ -1333,6 +1400,7 @@
|
||||
"uppercase": "少なくとも1つの大文字を含む必要があります"
|
||||
},
|
||||
"personalDataConsentRequired": "個人データの処理に同意する必要があります。",
|
||||
"prefix": "{prefix}で始める必要があります",
|
||||
"required": "必須"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "실행 큐 맨 앞에 프롬프트 추가"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "선택한 출력 노드 대기열에 추가"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "다시 실행"
|
||||
},
|
||||
|
||||
@@ -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": "비밀번호를 입력하세요",
|
||||
@@ -56,6 +75,7 @@
|
||||
"termsLink": "이용 약관",
|
||||
"termsText": "\"다음\" 또는 \"가입하기\"를 클릭하면 우리의",
|
||||
"title": "계정에 로그인",
|
||||
"useApiKey": "Comfy API 키",
|
||||
"userAvatar": "사용자 아바타"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -131,6 +151,7 @@
|
||||
"Unpin": "고정 해제"
|
||||
},
|
||||
"credits": {
|
||||
"apiPricing": "API 가격",
|
||||
"credits": "크레딧",
|
||||
"faqs": "자주 묻는 질문",
|
||||
"invoiceHistory": "청구서 내역",
|
||||
@@ -149,8 +170,10 @@
|
||||
"yourCreditBalance": "보유 크레딧 잔액"
|
||||
},
|
||||
"dataTypes": {
|
||||
"*": "*",
|
||||
"AUDIO": "오디오",
|
||||
"BOOLEAN": "논리값",
|
||||
"CAMERA_CONTROL": "카메라 제어",
|
||||
"CLIP": "CLIP",
|
||||
"CLIP_VISION": "CLIP_VISION",
|
||||
"CLIP_VISION_OUTPUT": "CLIP_VISION 출력",
|
||||
@@ -167,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": "웹캠"
|
||||
},
|
||||
@@ -364,6 +396,12 @@
|
||||
"inbox": "받은 편지함",
|
||||
"star": "별"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "복사",
|
||||
"move": "이동",
|
||||
"title": "모델 가져오기",
|
||||
"type": "유형"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "ComfyUI의 앱 데이터 디렉토리. 저장소:\n- 로그\n- 서버 구성",
|
||||
"appPathLocationTooltip": "ComfyUI의 앱 에셋 디렉토리. ComfyUI 코드 및 에셋을 저장합니다.",
|
||||
@@ -695,6 +733,7 @@
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Queue Prompt": "실행 큐에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 큐 맨 앞에 프롬프트 추가",
|
||||
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
|
||||
"Quit": "종료",
|
||||
"Redo": "다시 실행",
|
||||
"Refresh Node Definitions": "노드 정의 새로 고침",
|
||||
@@ -731,10 +770,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": "배치",
|
||||
@@ -759,6 +810,7 @@
|
||||
"instructpix2pix": "InstructPix2Pix",
|
||||
"latent": "잠재 데이터",
|
||||
"loaders": "로더",
|
||||
"lotus": "lotus",
|
||||
"ltxv": "ltxv",
|
||||
"mask": "마스크",
|
||||
"model": "모델",
|
||||
@@ -770,10 +822,12 @@
|
||||
"photomaker": "포토메이커",
|
||||
"postprocessing": "후처리",
|
||||
"preprocessors": "전처리기",
|
||||
"primitive": "프리미티브",
|
||||
"samplers": "샘플러",
|
||||
"sampling": "샘플링",
|
||||
"schedulers": "스케줄러",
|
||||
"scheduling": "스케줄링",
|
||||
"sd": "sd",
|
||||
"sd3": "sd3",
|
||||
"sigmas": "시그마",
|
||||
"stable_cascade": "Stable Cascade",
|
||||
@@ -782,6 +836,10 @@
|
||||
"unet": "UNet",
|
||||
"upscale_diffusion": "업스케일 확산",
|
||||
"upscaling": "업스케일링",
|
||||
"utils": "유틸리티",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "비디오",
|
||||
"video_models": "비디오 모델"
|
||||
},
|
||||
@@ -801,6 +859,12 @@
|
||||
},
|
||||
"title": "이 장치는 지원되지 않습니다."
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"disabledTooltip": "선택된 출력 노드가 없습니다",
|
||||
"tooltip": "선택한 출력 노드에 실행 (주황색 테두리로 강조 표시됨)"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "다음 서버 구성을 수정했습니다. 변경 사항을 적용하려면 다시 시작하세오.",
|
||||
"restart": "다시 시작",
|
||||
@@ -1297,8 +1361,10 @@
|
||||
"noTemplatesToExport": "내보낼 템플릿이 없습니다",
|
||||
"nodeDefinitionsUpdated": "노드 정의가 업데이트되었습니다",
|
||||
"nothingToGroup": "그룹화할 항목이 없습니다",
|
||||
"nothingToQueue": "대기열에 추가할 항목이 없습니다",
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
|
||||
"pleaseSelectOutputNodes": "출력 노드를 선택해 주세요",
|
||||
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
|
||||
"unauthorizedDomain": "귀하의 도메인 {domain}은(는) 이 서비스를 사용할 수 있는 권한이 없습니다. 도메인을 허용 목록에 추가하려면 {email}로 문의해 주세요.",
|
||||
"updateRequested": "업데이트 요청됨",
|
||||
@@ -1321,6 +1387,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "유효하지 않은 이메일 주소",
|
||||
"length": "{length}자여야 합니다",
|
||||
"maxLength": "{length}자를 초과할 수 없습니다",
|
||||
"minLength": "{length}자 이상이어야 합니다",
|
||||
"password": {
|
||||
@@ -1333,6 +1400,7 @@
|
||||
"uppercase": "적어도 하나의 대문자를 포함해야 합니다"
|
||||
},
|
||||
"personalDataConsentRequired": "개인 데이터 처리에 동의해야 합니다.",
|
||||
"prefix": "{prefix}(으)로 시작해야 합니다",
|
||||
"required": "필수"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "Очередь запросов (передняя)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "Добавить выбранные выходные узлы в очередь"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "Повторить"
|
||||
},
|
||||
|
||||
@@ -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": "Введите ваш пароль",
|
||||
@@ -56,6 +75,7 @@
|
||||
"termsLink": "Условиями использования",
|
||||
"termsText": "Нажимая \"Далее\" или \"Зарегистрироваться\", вы соглашаетесь с нашими",
|
||||
"title": "Войдите в свой аккаунт",
|
||||
"useApiKey": "Comfy API-ключ",
|
||||
"userAvatar": "Аватар пользователя"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -131,6 +151,7 @@
|
||||
"Unpin": "Открепить"
|
||||
},
|
||||
"credits": {
|
||||
"apiPricing": "Цены на API",
|
||||
"credits": "Кредиты",
|
||||
"faqs": "Часто задаваемые вопросы",
|
||||
"invoiceHistory": "История счетов",
|
||||
@@ -149,8 +170,10 @@
|
||||
"yourCreditBalance": "Ваш баланс кредитов"
|
||||
},
|
||||
"dataTypes": {
|
||||
"*": "*",
|
||||
"AUDIO": "АУДИО",
|
||||
"BOOLEAN": "БУЛЕВО",
|
||||
"CAMERA_CONTROL": "УПРАВЛЕНИЕ_КАМЕРОЙ",
|
||||
"CLIP": "CLIP",
|
||||
"CLIP_VISION": "CLIP_VISION",
|
||||
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
|
||||
@@ -167,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": "ВЕБ-КАМЕРА"
|
||||
},
|
||||
@@ -364,6 +396,12 @@
|
||||
"inbox": "Входящие",
|
||||
"star": "Звезда"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "Копировать",
|
||||
"move": "Переместить",
|
||||
"title": "Импорт модели",
|
||||
"type": "Тип"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "Директория данных приложения ComfyUI. Хранит:\n- Логи\n- Конфигурации сервера",
|
||||
"appPathLocationTooltip": "Директория активов приложения ComfyUI. Хранит код и активы ComfyUI",
|
||||
@@ -695,6 +733,7 @@
|
||||
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
|
||||
"Queue Prompt": "Запрос в очереди",
|
||||
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
|
||||
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
|
||||
"Quit": "Выйти",
|
||||
"Redo": "Повторить",
|
||||
"Refresh Node Definitions": "Обновить определения нод",
|
||||
@@ -731,10 +770,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": "пакет",
|
||||
@@ -759,6 +810,7 @@
|
||||
"instructpix2pix": "instructpix2pix",
|
||||
"latent": "латентный",
|
||||
"loaders": "загрузчики",
|
||||
"lotus": "lotus",
|
||||
"ltxv": "ltxv",
|
||||
"mask": "маска",
|
||||
"model": "модель",
|
||||
@@ -770,10 +822,12 @@
|
||||
"photomaker": "photomaker",
|
||||
"postprocessing": "постобработка",
|
||||
"preprocessors": "предобработчики",
|
||||
"primitive": "примитив",
|
||||
"samplers": "семплеры",
|
||||
"sampling": "выборка",
|
||||
"schedulers": "schedulers",
|
||||
"scheduling": "scheduling",
|
||||
"sd": "sd",
|
||||
"sd3": "sd3",
|
||||
"sigmas": "сигмы",
|
||||
"stable_cascade": "стабильная_каскадная",
|
||||
@@ -782,6 +836,10 @@
|
||||
"unet": "unet",
|
||||
"upscale_diffusion": "диффузии_апскейла",
|
||||
"upscaling": "апскейл",
|
||||
"utils": "утилиты",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "видео",
|
||||
"video_models": "видеомодели"
|
||||
},
|
||||
@@ -801,6 +859,12 @@
|
||||
},
|
||||
"title": "Ваше устройство не поддерживается"
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"disabledTooltip": "Выходные узлы не выбраны",
|
||||
"tooltip": "Выполнить для выбранных выходных узлов (выделены оранжевой рамкой)"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "Вы изменили следующие конфигурации сервера. Перезапустите, чтобы применить изменения.",
|
||||
"restart": "Перезапустить",
|
||||
@@ -1297,8 +1361,10 @@
|
||||
"noTemplatesToExport": "Нет шаблонов для экспорта",
|
||||
"nodeDefinitionsUpdated": "Определения узлов обновлены",
|
||||
"nothingToGroup": "Нечего группировать",
|
||||
"nothingToQueue": "Нет заданий в очереди",
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
|
||||
"pleaseSelectOutputNodes": "Пожалуйста, выберите выходные узлы",
|
||||
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
|
||||
"unauthorizedDomain": "Ваш домен {domain} не авторизован для использования этого сервиса. Пожалуйста, свяжитесь с {email}, чтобы добавить ваш домен в белый список.",
|
||||
"updateRequested": "Запрошено обновление",
|
||||
@@ -1321,6 +1387,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "Недействительный адрес электронной почты",
|
||||
"length": "Должно быть {length} символов",
|
||||
"maxLength": "Должно быть не более {length} символов",
|
||||
"minLength": "Должно быть не менее {length} символов",
|
||||
"password": {
|
||||
@@ -1333,6 +1400,7 @@
|
||||
"uppercase": "Должен содержать хотя бы одну заглавную букву"
|
||||
},
|
||||
"personalDataConsentRequired": "Вы должны согласиться на обработку ваших персональных данных.",
|
||||
"prefix": "Должно начинаться с {prefix}",
|
||||
"required": "Обязательно"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -155,6 +155,9 @@
|
||||
"Comfy_QueuePromptFront": {
|
||||
"label": "执行提示词(前端)"
|
||||
},
|
||||
"Comfy_QueueSelectedOutputNodes": {
|
||||
"label": "队列所选输出节点"
|
||||
},
|
||||
"Comfy_Redo": {
|
||||
"label": "重做"
|
||||
},
|
||||
|
||||
@@ -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": "输入您的密码",
|
||||
@@ -56,6 +75,7 @@
|
||||
"termsLink": "使用条款",
|
||||
"termsText": "点击“下一步”或“注册”即表示您同意我们的",
|
||||
"title": "登录您的账户",
|
||||
"useApiKey": "Comfy API 密钥",
|
||||
"userAvatar": "用户头像"
|
||||
},
|
||||
"passwordUpdate": {
|
||||
@@ -131,6 +151,7 @@
|
||||
"Unpin": "取消固定"
|
||||
},
|
||||
"credits": {
|
||||
"apiPricing": "API 价格",
|
||||
"credits": "积分",
|
||||
"faqs": "常见问题",
|
||||
"invoiceHistory": "发票历史",
|
||||
@@ -149,8 +170,10 @@
|
||||
"yourCreditBalance": "您的积分余额"
|
||||
},
|
||||
"dataTypes": {
|
||||
"*": "*",
|
||||
"AUDIO": "音频",
|
||||
"BOOLEAN": "布尔",
|
||||
"CAMERA_CONTROL": "相机控制",
|
||||
"CLIP": "CLIP",
|
||||
"CLIP_VISION": "CLIP视觉",
|
||||
"CLIP_VISION_OUTPUT": "CLIP视觉输出",
|
||||
@@ -167,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": "摄像头"
|
||||
},
|
||||
@@ -364,6 +396,12 @@
|
||||
"inbox": "收件箱",
|
||||
"star": "星星"
|
||||
},
|
||||
"importModelDialog": {
|
||||
"copy": "复制",
|
||||
"move": "移动",
|
||||
"title": "导入模型",
|
||||
"type": "类型"
|
||||
},
|
||||
"install": {
|
||||
"appDataLocationTooltip": "ComfyUI 的应用数据目录。存储:\n- 日志\n- 服务器配置",
|
||||
"appPathLocationTooltip": "ComfyUI 的应用资产目录。存储 ComfyUI 代码和资产",
|
||||
@@ -695,6 +733,7 @@
|
||||
"Previous Opened Workflow": "上一个打开的工作流",
|
||||
"Queue Prompt": "执行提示词",
|
||||
"Queue Prompt (Front)": "执行提示词 (优先执行)",
|
||||
"Queue Selected Output Nodes": "将所选输出节点加入队列",
|
||||
"Quit": "退出",
|
||||
"Redo": "重做",
|
||||
"Refresh Node Definitions": "刷新节点定义",
|
||||
@@ -731,10 +770,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": "批处理",
|
||||
@@ -759,6 +810,7 @@
|
||||
"instructpix2pix": "InstructPix2Pix",
|
||||
"latent": "Latent",
|
||||
"loaders": "加载器",
|
||||
"lotus": "lotus",
|
||||
"ltxv": "LTXV",
|
||||
"mask": "遮罩",
|
||||
"model": "模型",
|
||||
@@ -770,10 +822,12 @@
|
||||
"photomaker": "PhotoMaker",
|
||||
"postprocessing": "后处理",
|
||||
"preprocessors": "预处理器",
|
||||
"primitive": "基础",
|
||||
"samplers": "采样器",
|
||||
"sampling": "采样",
|
||||
"schedulers": "调度器",
|
||||
"scheduling": "调度",
|
||||
"sd": "sd",
|
||||
"sd3": "SD3",
|
||||
"sigmas": "Sigmas",
|
||||
"stable_cascade": "StableCascade",
|
||||
@@ -782,6 +836,10 @@
|
||||
"unet": "U-Net",
|
||||
"upscale_diffusion": "放大扩散",
|
||||
"upscaling": "放大",
|
||||
"utils": "工具",
|
||||
"v1": "v1",
|
||||
"v2": "v2",
|
||||
"v3": "v3",
|
||||
"video": "视频",
|
||||
"video_models": "视频模型"
|
||||
},
|
||||
@@ -801,6 +859,12 @@
|
||||
},
|
||||
"title": "您的设备不受支持"
|
||||
},
|
||||
"selectionToolbox": {
|
||||
"executeButton": {
|
||||
"disabledTooltip": "未选择输出节点",
|
||||
"tooltip": "执行到选定的输出节点(用橙色边框高亮显示)"
|
||||
}
|
||||
},
|
||||
"serverConfig": {
|
||||
"modifiedConfigs": "您已修改以下服务器配置。重启以应用更改。",
|
||||
"restart": "重启",
|
||||
@@ -1297,8 +1361,10 @@
|
||||
"noTemplatesToExport": "没有模板可以导出",
|
||||
"nodeDefinitionsUpdated": "节点定义已更新",
|
||||
"nothingToGroup": "没有可分组的内容",
|
||||
"nothingToQueue": "没有可加入队列的内容",
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
|
||||
"pleaseSelectOutputNodes": "请选择输出节点",
|
||||
"unableToGetModelFilePath": "无法获取模型文件路径",
|
||||
"unauthorizedDomain": "您的域名 {domain} 未被授权使用此服务。请联系 {email} 将您的域名添加到白名单。",
|
||||
"updateRequested": "已请求更新",
|
||||
@@ -1321,6 +1387,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"invalidEmail": "无效的电子邮件地址",
|
||||
"length": "必须为{length}个字符",
|
||||
"maxLength": "不能超过{length}个字符",
|
||||
"minLength": "必须至少有{length}个字符",
|
||||
"password": {
|
||||
@@ -1333,6 +1400,7 @@
|
||||
"uppercase": "必须包含至少一个大写字母"
|
||||
},
|
||||
"personalDataConsentRequired": "您必须同意处理您的个人数据。",
|
||||
"prefix": "必须以 {prefix} 开头",
|
||||
"required": "必填"
|
||||
},
|
||||
"welcome": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -67,6 +82,7 @@ interface QueuePromptRequestBody {
|
||||
interface FrontendApiCalls {
|
||||
graphChanged: ComfyWorkflowJSON
|
||||
promptQueued: { number: number; batchCount: number }
|
||||
unhandledFileDrop: { file: File }
|
||||
graphCleared: never
|
||||
reconnecting: never
|
||||
reconnected: never
|
||||
@@ -228,6 +244,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()
|
||||
@@ -294,20 +314,23 @@ export class ComfyApi extends EventTarget {
|
||||
* Provides type safety for the contravariance issue with EventTarget (last checked TS 5.6).
|
||||
* @param type The type of event to emit
|
||||
* @param detail The detail property used for a custom event ({@link CustomEventInit.detail})
|
||||
* @param init The event config used for a custom event ({@link CustomEventInit})
|
||||
*/
|
||||
dispatchCustomEvent<T extends SimpleApiEvents>(type: T): boolean
|
||||
dispatchCustomEvent<T extends ComplexApiEvents>(
|
||||
type: T,
|
||||
detail: ApiEventTypes[T] | null
|
||||
detail: ApiEventTypes[T] | null,
|
||||
init?: EventInit
|
||||
): boolean
|
||||
dispatchCustomEvent<T extends keyof ApiEventTypes>(
|
||||
type: T,
|
||||
detail?: ApiEventTypes[T]
|
||||
detail?: ApiEventTypes[T],
|
||||
init?: EventInit
|
||||
): boolean {
|
||||
const event =
|
||||
detail === undefined
|
||||
? new CustomEvent(type)
|
||||
: new CustomEvent(type, { detail })
|
||||
? new CustomEvent(type, { ...init })
|
||||
: new CustomEvent(type, { detail, ...init })
|
||||
return super.dispatchEvent(event)
|
||||
}
|
||||
|
||||
@@ -545,6 +568,7 @@ export class ComfyApi extends EventTarget {
|
||||
prompt,
|
||||
extra_data: {
|
||||
auth_token_comfy_org: this.authToken,
|
||||
api_key_comfy_org: this.apiKey,
|
||||
extra_pnginfo: { workflow }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { IWidget, Vector2 } from '@comfyorg/litegraph'
|
||||
import type { Vector2 } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { reactive } from 'vue'
|
||||
@@ -28,10 +29,14 @@ import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
||||
import { getFromWebmFile } from '@/scripts/metadata/ebml'
|
||||
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
|
||||
import { getFromIsobmffFile } from '@/scripts/metadata/isobmff'
|
||||
import { getMp3Metadata } from '@/scripts/metadata/mp3'
|
||||
import { getOggMetadata } from '@/scripts/metadata/ogg'
|
||||
import { getSvgMetadata } from '@/scripts/metadata/svg'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
@@ -63,7 +68,6 @@ import { deserialiseAndCreate } from '@/utils/vintageClipboard'
|
||||
import { type ComfyApi, PromptExecutionError, api } from './api'
|
||||
import { defaultGraph } from './defaultGraph'
|
||||
import { pruneWidgets } from './domWidget'
|
||||
import { getSvgMetadata } from './metadata/svg'
|
||||
import {
|
||||
getFlacMetadata,
|
||||
getLatentMetadata,
|
||||
@@ -94,7 +98,7 @@ function sanitizeNodeName(string: string) {
|
||||
}
|
||||
|
||||
type Clipspace = {
|
||||
widgets?: Pick<IWidget, 'type' | 'name' | 'value'>[] | null
|
||||
widgets?: Pick<IBaseWidget, 'type' | 'name' | 'value'>[] | null
|
||||
imgs?: HTMLImageElement[] | null
|
||||
original_imgs?: HTMLImageElement[] | null
|
||||
images?: any[] | null
|
||||
@@ -106,7 +110,11 @@ export class ComfyApp {
|
||||
/**
|
||||
* List of entries to queue
|
||||
*/
|
||||
#queueItems: { number: number; batchCount: number }[] = []
|
||||
#queueItems: {
|
||||
number: number
|
||||
batchCount: number
|
||||
queueNodeIds?: NodeId[]
|
||||
}[] = []
|
||||
/**
|
||||
* If the queue is currently being processed
|
||||
*/
|
||||
@@ -387,7 +395,6 @@ export class ComfyApp {
|
||||
const index = node.widgets.findIndex((obj) => obj.name === 'image')
|
||||
if (index >= 0) {
|
||||
if (
|
||||
// @ts-expect-error custom widget type
|
||||
node.widgets[index].type != 'image' &&
|
||||
typeof node.widgets[index].value == 'string' &&
|
||||
clip_image.filename
|
||||
@@ -409,7 +416,6 @@ export class ComfyApp {
|
||||
)
|
||||
if (prop && prop.type != 'button') {
|
||||
if (
|
||||
// @ts-expect-error Custom widget type
|
||||
prop.type != 'image' &&
|
||||
typeof prop.value == 'string' &&
|
||||
// @ts-expect-error Custom widget value
|
||||
@@ -421,7 +427,6 @@ export class ComfyApp {
|
||||
resultItem.filename +
|
||||
(resultItem.type ? ` [${resultItem.type}]` : '')
|
||||
} else {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
prop.value = value
|
||||
prop.callback?.(value)
|
||||
}
|
||||
@@ -1060,13 +1065,21 @@ export class ComfyApp {
|
||||
try {
|
||||
// @ts-expect-error Discrepancies between zod and litegraph - in progress
|
||||
this.graph.configure(graphData)
|
||||
if (
|
||||
restore_view &&
|
||||
useSettingStore().get('Comfy.EnableWorkflowViewRestore') &&
|
||||
graphData.extra?.ds
|
||||
) {
|
||||
this.canvas.ds.offset = graphData.extra.ds.offset
|
||||
this.canvas.ds.scale = graphData.extra.ds.scale
|
||||
if (restore_view) {
|
||||
if (
|
||||
useSettingStore().get('Comfy.EnableWorkflowViewRestore') &&
|
||||
graphData.extra?.ds
|
||||
) {
|
||||
this.canvas.ds.offset = graphData.extra.ds.offset
|
||||
this.canvas.ds.scale = graphData.extra.ds.scale
|
||||
} else {
|
||||
// @note: Set view after the graph has been rendered once. fitView uses
|
||||
// boundingRect on nodes to calculate the view bounds, which only become
|
||||
// available after the first render.
|
||||
requestAnimationFrame(() => {
|
||||
useLitegraphService().fitView()
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
useDialogService().showErrorDialog(error, {
|
||||
@@ -1101,10 +1114,8 @@ export class ComfyApp {
|
||||
) {
|
||||
if (widget.name == 'control_after_generate') {
|
||||
if (widget.value === true) {
|
||||
// @ts-expect-error string is not assignable to boolean
|
||||
widget.value = 'randomize'
|
||||
} else if (widget.value === false) {
|
||||
// @ts-expect-error string is not assignable to boolean
|
||||
widget.value = 'fixed'
|
||||
}
|
||||
}
|
||||
@@ -1148,14 +1159,22 @@ export class ComfyApp {
|
||||
})
|
||||
}
|
||||
|
||||
async graphToPrompt(graph = this.graph) {
|
||||
async graphToPrompt(
|
||||
graph = this.graph,
|
||||
options: { queueNodeIds?: NodeId[] } = {}
|
||||
) {
|
||||
return graphToPrompt(graph, {
|
||||
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave')
|
||||
sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave'),
|
||||
queueNodeIds: options.queueNodeIds
|
||||
})
|
||||
}
|
||||
|
||||
async queuePrompt(number: number, batchCount: number = 1): Promise<boolean> {
|
||||
this.#queueItems.push({ number, batchCount })
|
||||
async queuePrompt(
|
||||
number: number,
|
||||
batchCount: number = 1,
|
||||
queueNodeIds?: NodeId[]
|
||||
): Promise<boolean> {
|
||||
this.#queueItems.push({ number, batchCount, queueNodeIds })
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
if (this.#processingQueue) {
|
||||
@@ -1168,21 +1187,24 @@ export class ComfyApp {
|
||||
|
||||
let comfyOrgAuthToken =
|
||||
(await useFirebaseAuthStore().getIdToken()) ?? undefined
|
||||
let comfyOrgApiKey = useApiKeyAuthStore().getApiKey()
|
||||
|
||||
try {
|
||||
while (this.#queueItems.length) {
|
||||
const { number, batchCount } = this.#queueItems.pop()!
|
||||
const { number, batchCount, queueNodeIds } = this.#queueItems.pop()!
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
// Allow widgets to run callbacks before a prompt has been queued
|
||||
// e.g. random seed before every gen
|
||||
executeWidgetsCallback(this.graph.nodes, 'beforeQueued')
|
||||
|
||||
const p = await this.graphToPrompt()
|
||||
const p = await this.graphToPrompt(this.graph, { queueNodeIds })
|
||||
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)
|
||||
@@ -1230,10 +1252,22 @@ export class ComfyApp {
|
||||
return !executionStore.lastNodeErrors
|
||||
}
|
||||
|
||||
showErrorOnFileLoad(file: File) {
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.fileLoadError', { fileName: file.name })
|
||||
onUnhandledFile(file: File) {
|
||||
// Fire custom event to allow other parts of the app to handle the file
|
||||
const unhandled = api.dispatchCustomEvent(
|
||||
'unhandledFileDrop',
|
||||
{ file },
|
||||
{
|
||||
cancelable: true
|
||||
}
|
||||
)
|
||||
|
||||
if (unhandled) {
|
||||
// Nothing handled the event, so show the error dialog
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.fileLoadError', { fileName: file.name })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1269,7 +1303,7 @@ export class ComfyApp {
|
||||
this.graph.serialize() as unknown as ComfyWorkflowJSON
|
||||
)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'image/webp') {
|
||||
const pngInfo = await getWebpMetadata(file)
|
||||
@@ -1282,7 +1316,25 @@ export class ComfyApp {
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(JSON.parse(prompt), fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'audio/mpeg') {
|
||||
const { workflow, prompt } = await getMp3Metadata(file)
|
||||
if (workflow) {
|
||||
this.loadGraphData(workflow, true, true, fileName)
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(prompt, fileName)
|
||||
} else {
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'audio/ogg') {
|
||||
const { workflow, prompt } = await getOggMetadata(file)
|
||||
if (workflow) {
|
||||
this.loadGraphData(workflow, true, true, fileName)
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(prompt, fileName)
|
||||
} else {
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'audio/flac' || file.type === 'audio/x-flac') {
|
||||
const pngInfo = await getFlacMetadata(file)
|
||||
@@ -1294,7 +1346,7 @@ export class ComfyApp {
|
||||
} else if (prompt) {
|
||||
this.loadApiJson(JSON.parse(prompt), fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (file.type === 'video/webm') {
|
||||
const webmInfo = await getFromWebmFile(file)
|
||||
@@ -1303,7 +1355,7 @@ export class ComfyApp {
|
||||
} else if (webmInfo.prompt) {
|
||||
this.loadApiJson(webmInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'video/mp4' ||
|
||||
@@ -1326,7 +1378,7 @@ export class ComfyApp {
|
||||
} else if (svgInfo.prompt) {
|
||||
this.loadApiJson(svgInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'model/gltf-binary' ||
|
||||
@@ -1338,7 +1390,7 @@ export class ComfyApp {
|
||||
} else if (gltfInfo.prompt) {
|
||||
this.loadApiJson(gltfInfo.prompt, fileName)
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else if (
|
||||
file.type === 'application/json' ||
|
||||
@@ -1356,7 +1408,7 @@ export class ComfyApp {
|
||||
await this.loadGraphData(
|
||||
JSON.parse(readerResult),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
fileName
|
||||
)
|
||||
}
|
||||
@@ -1369,7 +1421,7 @@ export class ComfyApp {
|
||||
const info = await getLatentMetadata(file)
|
||||
// TODO define schema to LatentMetadata
|
||||
// @ts-expect-error
|
||||
if (info.workflow) {
|
||||
if (info?.workflow) {
|
||||
await this.loadGraphData(
|
||||
// @ts-expect-error
|
||||
JSON.parse(info.workflow),
|
||||
@@ -1378,14 +1430,14 @@ export class ComfyApp {
|
||||
fileName
|
||||
)
|
||||
// @ts-expect-error
|
||||
} else if (info.prompt) {
|
||||
} else if (info?.prompt) {
|
||||
// @ts-expect-error
|
||||
this.loadApiJson(JSON.parse(info.prompt))
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
} else {
|
||||
this.showErrorOnFileLoad(file)
|
||||
this.onUnhandledFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1535,10 +1587,8 @@ export class ComfyApp {
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.type === 'combo') {
|
||||
if (def['input'].required?.[widget.name] !== undefined) {
|
||||
// @ts-expect-error Requires discriminated union
|
||||
widget.options.values = def['input'].required[widget.name][0]
|
||||
} else if (def['input'].optional?.[widget.name] !== undefined) {
|
||||
// @ts-expect-error Requires discriminated union
|
||||
widget.options.values = def['input'].optional[widget.name][0]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,12 @@ export const defaultGraph: ComfyWorkflowJSON = {
|
||||
],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {},
|
||||
extra: {
|
||||
ds: {
|
||||
offset: [0, 0],
|
||||
scale: 1
|
||||
}
|
||||
},
|
||||
version: 0.4
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphNode, LegacyWidget, LiteGraph } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
ICustomWidget,
|
||||
IWidget,
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
@@ -13,9 +12,9 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
export interface BaseDOMWidget<V extends object | string>
|
||||
extends ICustomWidget {
|
||||
extends IBaseWidget<V, string, DOMWidgetOptions<V>> {
|
||||
// ICustomWidget properties
|
||||
type: 'custom'
|
||||
type: string
|
||||
options: DOMWidgetOptions<V>
|
||||
value: V
|
||||
callback?: (value: V) => void
|
||||
@@ -81,26 +80,23 @@ export interface DOMWidgetOptions<V extends object | string>
|
||||
}
|
||||
|
||||
export const isDOMWidget = <T extends HTMLElement, V extends object | string>(
|
||||
widget: IWidget
|
||||
widget: IBaseWidget
|
||||
): widget is DOMWidget<T, V> => 'element' in widget && !!widget.element
|
||||
|
||||
export const isComponentWidget = <V extends object | string>(
|
||||
widget: IWidget
|
||||
widget: IBaseWidget
|
||||
): widget is ComponentWidget<V> => 'component' in widget && !!widget.component
|
||||
|
||||
abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
extends LegacyWidget<IBaseWidget<V, string, DOMWidgetOptions<V>>>
|
||||
implements BaseDOMWidget<V>
|
||||
{
|
||||
static readonly DEFAULT_MARGIN = 10
|
||||
readonly type: 'custom'
|
||||
readonly name: string
|
||||
readonly options: DOMWidgetOptions<V>
|
||||
computedHeight?: number
|
||||
y: number = 0
|
||||
callback?: (value: V) => void
|
||||
declare readonly name: string
|
||||
declare readonly options: DOMWidgetOptions<V>
|
||||
declare callback?: (value: V) => void
|
||||
|
||||
readonly id: string
|
||||
readonly node: LGraphNode
|
||||
|
||||
constructor(obj: {
|
||||
node: LGraphNode
|
||||
@@ -108,20 +104,17 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
type: string
|
||||
options: DOMWidgetOptions<V>
|
||||
}) {
|
||||
// @ts-expect-error custom widget type
|
||||
this.type = obj.type
|
||||
this.name = obj.name
|
||||
this.options = obj.options
|
||||
const { node, name, type, options } = obj
|
||||
super({ y: 0, name, type, options }, node)
|
||||
|
||||
this.id = generateUUID()
|
||||
this.node = obj.node
|
||||
}
|
||||
|
||||
get value(): V {
|
||||
override get value(): V {
|
||||
return this.options.getValue?.() ?? ('' as V)
|
||||
}
|
||||
|
||||
set value(v: V) {
|
||||
override set value(v: V) {
|
||||
this.options.setValue?.(v)
|
||||
this.callback?.(this.value)
|
||||
}
|
||||
@@ -134,7 +127,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
return !['hidden'].includes(this.type) && this.node.isWidgetVisible(this)
|
||||
}
|
||||
|
||||
draw(
|
||||
override draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
_node: LGraphNode,
|
||||
widget_width: number,
|
||||
@@ -159,7 +152,7 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
this.options.onDraw?.(this)
|
||||
}
|
||||
|
||||
onRemove(): void {
|
||||
override onRemove(): void {
|
||||
useDomWidgetStore().unregisterWidget(this.id)
|
||||
}
|
||||
}
|
||||
@@ -168,7 +161,7 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
extends BaseDOMWidgetImpl<V>
|
||||
implements DOMWidget<T, V>
|
||||
{
|
||||
readonly element: T
|
||||
override readonly element: T
|
||||
|
||||
constructor(obj: {
|
||||
node: LGraphNode
|
||||
@@ -182,8 +175,7 @@ export class DOMWidgetImpl<T extends HTMLElement, V extends object | string>
|
||||
}
|
||||
|
||||
/** Extract DOM widget size info */
|
||||
computeLayoutSize(node: LGraphNode) {
|
||||
// @ts-expect-error custom widget type
|
||||
override computeLayoutSize(node: LGraphNode) {
|
||||
if (this.type === 'hidden') {
|
||||
return {
|
||||
minHeight: 0,
|
||||
@@ -247,7 +239,7 @@ export class ComponentWidgetImpl<V extends object | string>
|
||||
this.inputSpec = obj.inputSpec
|
||||
}
|
||||
|
||||
computeLayoutSize() {
|
||||
override computeLayoutSize() {
|
||||
const minHeight = this.options.getMinHeight?.() ?? 50
|
||||
const maxHeight = this.options.getMaxHeight?.()
|
||||
return {
|
||||
@@ -257,7 +249,7 @@ export class ComponentWidgetImpl<V extends object | string>
|
||||
}
|
||||
}
|
||||
|
||||
serializeValue(): V {
|
||||
override serializeValue(): V {
|
||||
return toRaw(this.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IWidget, LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useBooleanWidget } from '@/composables/widgets/useBooleanWidget'
|
||||
@@ -10,7 +11,7 @@ const FloatWidget = useFloatWidget()
|
||||
const BooleanWidget = useBooleanWidget()
|
||||
|
||||
function addWidgetFromValue(node: LGraphNode, value: unknown) {
|
||||
let widget: IWidget
|
||||
let widget: IBaseWidget
|
||||
|
||||
if (typeof value === 'string') {
|
||||
widget = StringWidget(node, {
|
||||
|
||||
29
src/scripts/metadata/mp3.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export async function getMp3Metadata(file: File) {
|
||||
const reader = new FileReader()
|
||||
const read_process = new Promise(
|
||||
(r) => (reader.onload = (event) => r(event?.target?.result))
|
||||
)
|
||||
reader.readAsArrayBuffer(file)
|
||||
const arrayBuffer = (await read_process) as ArrayBuffer
|
||||
//https://stackoverflow.com/questions/7302439/how-can-i-determine-that-a-particular-file-is-in-fact-an-mp3-file#7302482
|
||||
const sig_bytes = new Uint8Array(arrayBuffer, 0, 3)
|
||||
if (
|
||||
(sig_bytes[0] != 0xff && sig_bytes[1] != 0xfb) ||
|
||||
(sig_bytes[0] != 0x49 && sig_bytes[1] != 0x44 && sig_bytes[2] != 0x33)
|
||||
)
|
||||
console.error('Invalid file signature.')
|
||||
let header = ''
|
||||
while (header.length < arrayBuffer.byteLength) {
|
||||
const page = String.fromCharCode(
|
||||
...new Uint8Array(arrayBuffer, header.length, header.length + 4096)
|
||||
)
|
||||
header += page
|
||||
if (page.match('\u00ff\u00fb')) break
|
||||
}
|
||||
let workflow, prompt
|
||||
let prompt_s = header.match(/prompt\u0000(\{.*?\})\u0000/s)?.[1]
|
||||
if (prompt_s) prompt = JSON.parse(prompt_s)
|
||||
let workflow_s = header.match(/workflow\u0000(\{.*?\})\u0000/s)?.[1]
|
||||
if (workflow_s) workflow = JSON.parse(workflow_s)
|
||||
return { prompt, workflow }
|
||||
}
|
||||
30
src/scripts/metadata/ogg.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export async function getOggMetadata(file: File) {
|
||||
const reader = new FileReader()
|
||||
const read_process = new Promise(
|
||||
(r) => (reader.onload = (event) => r(event?.target?.result))
|
||||
)
|
||||
reader.readAsArrayBuffer(file)
|
||||
const arrayBuffer = (await read_process) as ArrayBuffer
|
||||
const signature = String.fromCharCode(...new Uint8Array(arrayBuffer, 0, 4))
|
||||
if (signature !== 'OggS') console.error('Invalid file signature.')
|
||||
let oggs = 0
|
||||
let header = ''
|
||||
while (header.length < arrayBuffer.byteLength) {
|
||||
const page = String.fromCharCode(
|
||||
...new Uint8Array(arrayBuffer, header.length, header.length + 4096)
|
||||
)
|
||||
if (page.match('OggS\u0000')) oggs++
|
||||
header += page
|
||||
if (oggs > 1) break
|
||||
}
|
||||
let workflow, prompt
|
||||
let prompt_s = header
|
||||
.match(/prompt=(\{.*?(\}.*?\u0000))/s)?.[1]
|
||||
?.match(/\{.*\}/)?.[0]
|
||||
if (prompt_s) prompt = JSON.parse(prompt_s)
|
||||
let workflow_s = header
|
||||
.match(/workflow=(\{.*?(\}.*?\u0000))/s)?.[1]
|
||||
?.match(/\{.*\}/)?.[0]
|
||||
if (workflow_s) workflow = JSON.parse(workflow_s)
|
||||
return { prompt, workflow }
|
||||
}
|
||||
@@ -143,12 +143,17 @@ export function getLatentMetadata(file) {
|
||||
const dataView = new DataView(safetensorsData.buffer)
|
||||
let header_size = dataView.getUint32(0, true)
|
||||
let offset = 8
|
||||
let header = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
safetensorsData.slice(offset, offset + header_size)
|
||||
try {
|
||||
let header = JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
safetensorsData.slice(offset, offset + header_size)
|
||||
)
|
||||
)
|
||||
)
|
||||
r(header.__metadata__)
|
||||
r(header.__metadata__)
|
||||
} catch (e) {
|
||||
// Invalid header
|
||||
r(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
var slice = file.slice(0, 1024 * 1024 * 4)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import type { IWidget } from '@comfyorg/litegraph'
|
||||
import { type LGraphNode, isComboWidget } from '@comfyorg/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IComboWidget,
|
||||
IStringWidget
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
@@ -25,7 +25,7 @@ import './errorNodeWidgets'
|
||||
export type ComfyWidgetConstructorV2 = (
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpecV2
|
||||
) => IWidget
|
||||
) => IBaseWidget
|
||||
|
||||
export type ComfyWidgetConstructor = (
|
||||
node: LGraphNode,
|
||||
@@ -33,7 +33,7 @@ export type ComfyWidgetConstructor = (
|
||||
inputData: InputSpec,
|
||||
app: ComfyApp,
|
||||
widgetName?: string
|
||||
) => { widget: IWidget; minWidth?: number; minHeight?: number }
|
||||
) => { widget: IBaseWidget; minWidth?: number; minHeight?: number }
|
||||
|
||||
/**
|
||||
* Transforms a V2 widget constructor to a V1 widget constructor.
|
||||
@@ -60,7 +60,7 @@ function controlValueRunBefore() {
|
||||
return useSettingStore().get('Comfy.WidgetControlMode') === 'before'
|
||||
}
|
||||
|
||||
export function updateControlWidgetLabel(widget: IWidget) {
|
||||
export function updateControlWidgetLabel(widget: IBaseWidget) {
|
||||
if (controlValueRunBefore()) {
|
||||
widget.label = t('g.control_before_generate')
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ const HAS_EXECUTED = Symbol()
|
||||
|
||||
export function addValueControlWidget(
|
||||
node: LGraphNode,
|
||||
targetWidget: IWidget,
|
||||
targetWidget: IBaseWidget,
|
||||
defaultValue?: string,
|
||||
_values?: unknown,
|
||||
widgetName?: string,
|
||||
@@ -98,7 +98,7 @@ export function addValueControlWidget(
|
||||
|
||||
export function addValueControlWidgets(
|
||||
node: LGraphNode,
|
||||
targetWidget: IWidget,
|
||||
targetWidget: IBaseWidget,
|
||||
defaultValue?: string,
|
||||
options?: Record<string, any>,
|
||||
inputData?: InputSpec
|
||||
@@ -136,7 +136,7 @@ export function addValueControlWidgets(
|
||||
updateControlWidgetLabel(valueControl)
|
||||
const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl]
|
||||
|
||||
const isCombo = targetWidget.type === 'combo'
|
||||
const isCombo = isComboWidget(targetWidget)
|
||||
let comboFilter: IStringWidget
|
||||
if (isCombo && valueControl.options.values) {
|
||||
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
|
||||
|
||||
@@ -2,6 +2,7 @@ import ApiNodesNewsContent from '@/components/dialog/content/ApiNodesNewsContent
|
||||
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
|
||||
import ImportModelDialogContent from '@/components/dialog/content/ImportModelDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
|
||||
@@ -406,6 +407,16 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showImportModelDialog(
|
||||
props: InstanceType<typeof ImportModelDialogContent>['$props']
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-import-model',
|
||||
component: ImportModelDialogContent,
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showLoadWorkflowWarning,
|
||||
showMissingModelsWarning,
|
||||
@@ -422,6 +433,7 @@ export const useDialogService = () => {
|
||||
showTopUpCreditsDialog,
|
||||
showUpdatePasswordDialog,
|
||||
showApiNodesNewsDialog,
|
||||
showImportModelDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
RenderShape,
|
||||
type Vector2
|
||||
type Vector2,
|
||||
createBounds
|
||||
} from '@comfyorg/litegraph'
|
||||
import type {
|
||||
ISerialisableNodeInput,
|
||||
@@ -651,11 +652,23 @@ export const useLitegraphService = () => {
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
function fitView() {
|
||||
const canvas = canvasStore.canvas
|
||||
if (!canvas) return
|
||||
|
||||
const bounds = createBounds(app.graph.nodes)
|
||||
if (!bounds) return
|
||||
|
||||
canvas.ds.fitToBounds(bounds)
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
return {
|
||||
registerNodeDef,
|
||||
addNodeOnGraph,
|
||||
getCanvasCenter,
|
||||
goToNode,
|
||||
resetView
|
||||
resetView,
|
||||
fitView
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,21 @@ export const useWorkflowService = () => {
|
||||
}
|
||||
return defaultName
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds scale and offset from litegraph canvas to the workflow JSON.
|
||||
* @param workflow The workflow to add the view restore data to
|
||||
*/
|
||||
function addViewRestore(workflow: ComfyWorkflowJSON) {
|
||||
if (!settingStore.get('Comfy.EnableWorkflowViewRestore')) return
|
||||
|
||||
const { offset, scale } = app.canvas.ds
|
||||
const [x, y] = offset
|
||||
|
||||
workflow.extra ??= {}
|
||||
workflow.extra.ds = { scale, offset: [x, y] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the current workflow as a JSON file
|
||||
* @param filename The filename to save the workflow as
|
||||
@@ -50,6 +65,8 @@ export const useWorkflowService = () => {
|
||||
filename = workflow.filename
|
||||
}
|
||||
const p = await app.graphToPrompt()
|
||||
|
||||
addViewRestore(p.workflow)
|
||||
const json = JSON.stringify(p[promptProperty], null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const file = await getFilename(filename)
|
||||
|
||||
110
src/stores/apiKeyAuthStore.ts
Normal 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
|
||||
}
|
||||
})
|
||||
@@ -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 =
|
||||
@@ -93,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')
|
||||
)
|
||||
@@ -106,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'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -133,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) {
|
||||
@@ -181,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
|
||||
@@ -242,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)
|
||||
})
|
||||
@@ -282,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)
|
||||
@@ -328,6 +357,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
createCustomer,
|
||||
getIdToken,
|
||||
loginWithGoogle,
|
||||
loginWithGithub,
|
||||
@@ -335,6 +365,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
fetchBalance,
|
||||
accessBillingPortal,
|
||||
sendPasswordReset,
|
||||
updatePassword: _updatePassword
|
||||
updatePassword: _updatePassword,
|
||||
getAuthHeader
|
||||
}
|
||||
})
|
||||
|
||||
9
src/types/authTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
type LoggedInAuthHeader = {
|
||||
Authorization: `Bearer ${string}`
|
||||
}
|
||||
|
||||
export type ApiKeyAuthHeader = {
|
||||
'X-API-KEY': string
|
||||
}
|
||||
|
||||
export type AuthHeader = LoggedInAuthHeader | ApiKeyAuthHeader
|
||||
20
src/types/litegraph-augmentation.d.ts
vendored
@@ -32,15 +32,15 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
|
||||
}
|
||||
|
||||
interface IBaseWidget {
|
||||
onRemove?: () => void
|
||||
beforeQueued?: () => unknown
|
||||
afterQueued?: () => unknown
|
||||
onRemove?(): void
|
||||
beforeQueued?(): unknown
|
||||
afterQueued?(): unknown
|
||||
serializeValue?(node: LGraphNode, index: number): Promise<unknown> | unknown
|
||||
|
||||
/**
|
||||
* Refreshes the widget's value or options from its remote source.
|
||||
*/
|
||||
refresh?: () => unknown
|
||||
refresh?(): unknown
|
||||
|
||||
/**
|
||||
* If the widget supports dynamic prompts, this will be set to true.
|
||||
@@ -54,6 +54,8 @@ declare module '@comfyorg/litegraph/dist/types/widgets' {
|
||||
* ComfyUI extensions of litegraph
|
||||
*/
|
||||
declare module '@comfyorg/litegraph' {
|
||||
import type { IBaseWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||
type?: string
|
||||
comfyClass: string
|
||||
@@ -63,13 +65,9 @@ declare module '@comfyorg/litegraph' {
|
||||
new (): T
|
||||
}
|
||||
|
||||
interface TextWidget {
|
||||
dynamicPrompts?: boolean
|
||||
}
|
||||
|
||||
interface BaseWidget {
|
||||
serializeValue?(node: LGraphNode, index: number): Promise<unknown> | unknown
|
||||
}
|
||||
// Add interface augmentations into the class itself
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface BaseWidget extends IBaseWidget {}
|
||||
|
||||
interface LGraphNode {
|
||||
constructor: LGraphNodeConstructor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraph } from '@comfyorg/litegraph'
|
||||
import type { LGraph, NodeId } from '@comfyorg/litegraph'
|
||||
import { LGraphEventMode } from '@comfyorg/litegraph'
|
||||
|
||||
import type {
|
||||
@@ -8,16 +8,46 @@ import type {
|
||||
|
||||
import { compressWidgetInputSlots } from './litegraphUtil'
|
||||
|
||||
/**
|
||||
* Recursively target node's parent nodes to the new output.
|
||||
* @param nodeId The node id to add.
|
||||
* @param oldOutput The old output.
|
||||
* @param newOutput The new output.
|
||||
* @returns The new output.
|
||||
*/
|
||||
function recursiveAddNodes(
|
||||
nodeId: NodeId,
|
||||
oldOutput: ComfyApiWorkflow,
|
||||
newOutput: ComfyApiWorkflow
|
||||
) {
|
||||
const currentId = String(nodeId)
|
||||
const currentNode = oldOutput[currentId]!
|
||||
if (newOutput[currentId] == null) {
|
||||
newOutput[currentId] = currentNode
|
||||
for (const inputValue of Object.values(currentNode.inputs || [])) {
|
||||
if (Array.isArray(inputValue)) {
|
||||
recursiveAddNodes(inputValue[0], oldOutput, newOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newOutput
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the current graph workflow for sending to the API.
|
||||
* Note: Node widgets are updated before serialization to prepare queueing.
|
||||
* @note Node widgets are updated before serialization to prepare queueing.
|
||||
*
|
||||
* @param graph The graph to convert.
|
||||
* @param options The options for the conversion.
|
||||
* - `sortNodes`: Whether to sort the nodes by execution order.
|
||||
* - `queueNodeIds`: The output nodes to execute. Execute all output nodes if not provided.
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
export const graphToPrompt = async (
|
||||
graph: LGraph,
|
||||
options: { sortNodes?: boolean } = {}
|
||||
options: { sortNodes?: boolean; queueNodeIds?: NodeId[] } = {}
|
||||
): Promise<{ workflow: ComfyWorkflowJSON; output: ComfyApiWorkflow }> => {
|
||||
const { sortNodes = false } = options
|
||||
const { sortNodes = false, queueNodeIds } = options
|
||||
|
||||
for (const node of graph.computeExecutionOrder(false)) {
|
||||
const innerNodes = node.getInnerNodes ? node.getInnerNodes() : [node]
|
||||
@@ -44,7 +74,7 @@ export const graphToPrompt = async (
|
||||
workflow.extra ??= {}
|
||||
workflow.extra.frontendVersion = __COMFYUI_FRONTEND_VERSION__
|
||||
|
||||
const output: ComfyApiWorkflow = {}
|
||||
let output: ComfyApiWorkflow = {}
|
||||
// Process nodes in order of execution
|
||||
for (const outerNode of graph.computeExecutionOrder(false)) {
|
||||
const skipNode =
|
||||
@@ -163,6 +193,15 @@ export const graphToPrompt = async (
|
||||
}
|
||||
}
|
||||
|
||||
// Partial execution
|
||||
if (queueNodeIds?.length) {
|
||||
const newOutput = {}
|
||||
for (const queueNodeId of queueNodeIds) {
|
||||
recursiveAddNodes(queueNodeId, output, newOutput)
|
||||
}
|
||||
output = newOutput
|
||||
}
|
||||
|
||||
// @ts-expect-error Convert ISerializedGraph to ComfyWorkflowJSON
|
||||
return { workflow: workflow as ComfyWorkflowJSON, output }
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { ColorOption, LGraph } from '@comfyorg/litegraph'
|
||||
import { LGraphGroup, LGraphNode, isColorable } from '@comfyorg/litegraph'
|
||||
import type { ISerialisedGraph } from '@comfyorg/litegraph/dist/types/serialisation'
|
||||
import type {
|
||||
IComboWidget,
|
||||
IWidget
|
||||
IBaseWidget,
|
||||
IComboWidget
|
||||
} from '@comfyorg/litegraph/dist/types/widgets'
|
||||
import _ from 'lodash'
|
||||
|
||||
@@ -92,7 +92,7 @@ export function executeWidgetsCallback(
|
||||
*/
|
||||
export function migrateWidgetsValues<TWidgetValue>(
|
||||
inputDefs: Record<string, InputSpec>,
|
||||
widgets: IWidget[],
|
||||
widgets: IBaseWidget[],
|
||||
widgetsValues: TWidgetValue[]
|
||||
): TWidgetValue[] {
|
||||
const widgetNames = new Set(widgets.map((w) => w.name))
|
||||
|
||||
93
src/utils/safetensorsUtil.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
export interface ModelSpec {
|
||||
'modelspec.sai_model_spec': string
|
||||
'modelspec.architecture': string
|
||||
'modelspec.title': string
|
||||
'modelspec.description': string
|
||||
}
|
||||
|
||||
const architectureToType: Record<string, string> = {
|
||||
'stable-diffusion-v1': 'checkpoints',
|
||||
'stable-diffusion-xl-v1-base': 'checkpoints',
|
||||
'Flux.1-schnell': 'checkpoints',
|
||||
'Flux.1-dev': 'checkpoints',
|
||||
'Flux.1-AE': 'vae'
|
||||
}
|
||||
|
||||
interface SafetensorsHeader<TMetadata = Record<string, string> | ModelSpec> {
|
||||
[k: string]: unknown
|
||||
__metadata__?: TMetadata
|
||||
}
|
||||
|
||||
export async function guessModelType(file: File): Promise<string | null> {
|
||||
const header = await getHeader(file)
|
||||
if (!header) return null
|
||||
|
||||
let suggestedType: string | null
|
||||
if (isModelSpec(header)) {
|
||||
suggestedType = guessFromModelSpec(header)
|
||||
}
|
||||
|
||||
suggestedType ??= guessFromHeaderKeys(header)
|
||||
|
||||
return suggestedType
|
||||
}
|
||||
|
||||
async function getHeader(file: File): Promise<SafetensorsHeader | null> {
|
||||
try {
|
||||
// 8 bytes: an unsigned little-endian 64-bit integer, containing the size of the header
|
||||
// Slice the first 8 bytes so we don't read the whole file
|
||||
const headerSizeBlob = file.slice(0, 8)
|
||||
const headerSizeView = new DataView(await headerSizeBlob.arrayBuffer())
|
||||
const headerSize = headerSizeView.getBigUint64(0, true)
|
||||
|
||||
if (
|
||||
headerSize < 0 ||
|
||||
headerSize > file.size ||
|
||||
headerSize > Number.MAX_SAFE_INTEGER
|
||||
) {
|
||||
// Invalid header, probably not a safetensors file
|
||||
console.log(`Invalid header size ${headerSize} for file '${file.name}'`)
|
||||
return null
|
||||
}
|
||||
|
||||
// N bytes: a JSON UTF-8 string representing the header.
|
||||
const header = file.slice(8, Number(headerSize) + 8)
|
||||
const content = await header.text()
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
// Error reading the file, probably not a safetensors file
|
||||
console.error(`Error reading safetensors header '${file.name}'`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function guessFromModelSpec(header: SafetensorsHeader<ModelSpec>) {
|
||||
const architecture = header.__metadata__?.['modelspec.architecture']
|
||||
if (!architecture) return null
|
||||
let suggestedType = architectureToType[architecture]
|
||||
if (!suggestedType) {
|
||||
if (architecture?.endsWith('/lora')) {
|
||||
suggestedType = 'loras'
|
||||
}
|
||||
}
|
||||
|
||||
return suggestedType
|
||||
}
|
||||
|
||||
function guessFromHeaderKeys(header: SafetensorsHeader) {
|
||||
let suggestedType: string | null = null
|
||||
const keys = Object.keys(header)
|
||||
if (keys.find((k) => k.startsWith('lora_unet_'))) {
|
||||
suggestedType = 'loras'
|
||||
} else if (keys.find((k) => k.startsWith('model.diffusion_model.'))) {
|
||||
suggestedType = 'checkpoints'
|
||||
}
|
||||
|
||||
return suggestedType
|
||||
}
|
||||
|
||||
function isModelSpec(
|
||||
header: SafetensorsHeader
|
||||
): header is SafetensorsHeader<ModelSpec> {
|
||||
return !!header.__metadata__?.['modelspec.sai_model_spec']
|
||||
}
|
||||
@@ -373,4 +373,4 @@
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
|
||||