mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-03 03:44:07 +00:00
Compare commits
3 Commits
core/1.39
...
feature/qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc731d50e5 | ||
|
|
a0a762898e | ||
|
|
d0c58b79e3 |
10
.github/workflows/release-draft-create.yaml
vendored
10
.github/workflows/release-draft-create.yaml
vendored
@@ -53,13 +53,7 @@ jobs:
|
||||
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Desktop-specific release artifact with desktop distribution flags.
|
||||
DISTRIBUTION=desktop pnpm build
|
||||
pnpm zipdist ./dist ./dist-desktop.zip
|
||||
|
||||
# Default release artifact for core/PyPI.
|
||||
NX_SKIP_NX_CACHE=true pnpm build
|
||||
pnpm build
|
||||
pnpm zipdist
|
||||
- name: Upload dist artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -68,7 +62,6 @@ jobs:
|
||||
path: |
|
||||
dist/
|
||||
dist.zip
|
||||
dist-desktop.zip
|
||||
|
||||
draft_release:
|
||||
needs: build
|
||||
@@ -86,7 +79,6 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
dist.zip
|
||||
dist-desktop.zip
|
||||
tag_name: v${{ needs.build.outputs.version }}
|
||||
target_commitish: ${{ github.event.pull_request.base.ref }}
|
||||
make_latest: >-
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
"^build"
|
||||
],
|
||||
"options": {
|
||||
"command": "vite build --config apps/desktop-ui/vite.config.mts"
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "vite build --config vite.config.mts"
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/dist"
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
{
|
||||
"last_node_id": 7,
|
||||
"last_link_id": 5,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "T2IAdapterLoader",
|
||||
"pos": [100, 100],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONTROL_NET",
|
||||
"type": "CONTROL_NET",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "T2IAdapterLoader"
|
||||
},
|
||||
"widgets_values": ["t2iadapter_model.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": [100, 300],
|
||||
"size": [315, 98],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "ResizeImagesByLongerEdge",
|
||||
"pos": [500, 100],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [1],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ResizeImagesByLongerEdge"
|
||||
},
|
||||
"widgets_values": [1024]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "ImageScaleBy",
|
||||
"pos": [500, 280],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [2, 3],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageScaleBy"
|
||||
},
|
||||
"widgets_values": ["lanczos", 1.5]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "ImageBatch",
|
||||
"pos": [900, 100],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image1",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "image2",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [4],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ImageBatch"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "SaveImage",
|
||||
"pos": [900, 300],
|
||||
"size": [300, 80],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SaveImage"
|
||||
},
|
||||
"widgets_values": ["ComfyUI"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": [1250, 100],
|
||||
"size": [300, 250],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 3, 0, 4, 0, "IMAGE"],
|
||||
[2, 4, 0, 5, 0, "IMAGE"],
|
||||
[3, 4, 0, 6, 0, "IMAGE"],
|
||||
[4, 5, 0, 7, 0, "IMAGE"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
{
|
||||
"last_node_id": 5,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "Load3DAnimation",
|
||||
"pos": [100, 100],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MESH",
|
||||
"type": "MESH",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Load3DAnimation"
|
||||
},
|
||||
"widgets_values": ["model.glb"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "Preview3DAnimation",
|
||||
"pos": [450, 100],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mesh",
|
||||
"type": "MESH",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "Preview3DAnimation"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "ConditioningAverage ",
|
||||
"pos": [100, 300],
|
||||
"size": [300, 100],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "conditioning_to",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "conditioning_from",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [1],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ConditioningAverage "
|
||||
},
|
||||
"widgets_values": [1]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "SDV_img2vid_Conditioning",
|
||||
"pos": [450, 300],
|
||||
"size": [300, 150],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip_vision",
|
||||
"type": "CLIP_VISION",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "init_image",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "latent",
|
||||
"type": "LATENT",
|
||||
"links": [2],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "SDV_img2vid_Conditioning"
|
||||
},
|
||||
"widgets_values": [1024, 576, 14, 127, 25, 0.02]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "KSampler",
|
||||
"pos": [800, 300],
|
||||
"size": [300, 262],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "latent_image",
|
||||
"type": "LATENT",
|
||||
"link": 2
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [42, "fixed", 20, 8, "euler", "normal", 1]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[1, 3, 0, 5, 1, "CONDITIONING"],
|
||||
[2, 4, 2, 5, 3, "LATENT"]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1,
|
||||
"offset": [0, 0]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -298,10 +298,7 @@ test.describe('Settings', () => {
|
||||
await input.press('Alt+n')
|
||||
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
(req) =>
|
||||
req.url().includes('/api/settings') &&
|
||||
!req.url().includes('/api/settings/') &&
|
||||
req.method() === 'POST'
|
||||
'**/api/settings/Comfy.Keybinding.NewBindings'
|
||||
)
|
||||
|
||||
// Save keybinding
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 99 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 74 KiB |
7
global.d.ts
vendored
7
global.d.ts
vendored
@@ -5,11 +5,6 @@ declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
|
||||
interface ImpactQueueFunction {
|
||||
(...args: unknown[]): void
|
||||
a?: unknown[][]
|
||||
}
|
||||
|
||||
interface Window {
|
||||
__CONFIG__: {
|
||||
gtm_container_id?: string
|
||||
@@ -42,8 +37,6 @@ interface Window {
|
||||
session_number?: string
|
||||
}
|
||||
dataLayer?: Array<Record<string, unknown>>
|
||||
ire_o?: string
|
||||
ire?: ImpactQueueFunction
|
||||
}
|
||||
|
||||
interface Navigator {
|
||||
|
||||
42
index.html
42
index.html
@@ -35,6 +35,18 @@
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#vue-app:has(#loading-logo) {
|
||||
display: contents;
|
||||
color: var(--fg-color);
|
||||
& #loading-logo {
|
||||
place-self: center;
|
||||
font-size: clamp(2px, 1vw, 6px);
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
max-width: 100vw;
|
||||
border-radius: 20ch;
|
||||
}
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
@@ -53,6 +65,36 @@
|
||||
<body class="litegraph grid">
|
||||
<div id="vue-app">
|
||||
<span class="visually-hidden" role="status">Loading ComfyUI...</span>
|
||||
<svg
|
||||
width="520"
|
||||
height="520"
|
||||
viewBox="0 0 520 520"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="loading-logo"
|
||||
>
|
||||
<mask
|
||||
id="mask0_227_285"
|
||||
style="mask-type: alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="520"
|
||||
height="520"
|
||||
>
|
||||
<path
|
||||
d="M0 184.335C0 119.812 0 87.5502 12.5571 62.9055C23.6026 41.2274 41.2274 23.6026 62.9055 12.5571C87.5502 0 119.812 0 184.335 0H335.665C400.188 0 432.45 0 457.094 12.5571C478.773 23.6026 496.397 41.2274 507.443 62.9055C520 87.5502 520 119.812 520 184.335V335.665C520 400.188 520 432.45 507.443 457.094C496.397 478.773 478.773 496.397 457.094 507.443C432.45 520 400.188 520 335.665 520H184.335C119.812 520 87.5502 520 62.9055 507.443C41.2274 496.397 23.6026 478.773 12.5571 457.094C0 432.45 0 400.188 0 335.665V184.335Z"
|
||||
fill="#EEFF30"
|
||||
/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_227_285)">
|
||||
<rect y="0.751831" width="520" height="520" fill="#172DD7" />
|
||||
<path
|
||||
d="M176.484 428.831C168.649 428.831 162.327 425.919 158.204 420.412C153.966 414.755 152.861 406.857 155.171 398.749L164.447 366.178C165.187 363.585 164.672 360.794 163.059 358.636C161.446 356.483 158.921 355.216 156.241 355.216H129.571C121.731 355.216 115.409 352.308 111.289 346.802C107.051 341.14 105.946 333.242 108.258 325.134L140.124 213.748L143.642 201.51C148.371 184.904 165.62 171.407 182.097 171.407H214.009C217.817 171.407 221.167 168.868 222.215 165.183L232.769 128.135C237.494 111.545 254.742 98.048 271.219 98.048L339.468 97.9264L389.431 97.9221C397.268 97.9221 403.59 100.831 407.711 106.337C411.949 111.994 413.054 119.892 410.744 128L396.457 178.164C391.734 194.75 374.485 208.242 358.009 208.242L289.607 208.372H257.706C253.902 208.372 250.557 210.907 249.502 214.588L222.903 307.495C222.159 310.093 222.673 312.892 224.291 315.049C225.904 317.202 228.428 318.469 231.107 318.469C231.113 318.469 276.307 318.381 276.307 318.381H326.122C333.959 318.381 340.281 321.29 344.402 326.796C348.639 332.457 349.744 340.355 347.433 348.463L333.146 398.619C328.423 415.209 311.174 428.701 294.698 428.701L226.299 428.831H176.484Z"
|
||||
fill="#F0FF41"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.39.19",
|
||||
"version": "1.39.10",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import zipdir from 'zip-dir'
|
||||
|
||||
const sourceDir = process.argv[2] || './dist'
|
||||
const outputPath = process.argv[3] || './dist.zip'
|
||||
|
||||
zipdir(sourceDir, { saveTo: outputPath }, function (err, buffer) {
|
||||
zipdir('./dist', { saveTo: './dist.zip' }, function (err, buffer) {
|
||||
if (err) {
|
||||
console.error(`Error zipping "${sourceDir}" directory:`, err)
|
||||
console.error('Error zipping "dist" directory:', err)
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`Successfully zipped "${sourceDir}" directory to "${outputPath}".\n`
|
||||
)
|
||||
console.log('Successfully zipped "dist" directory.')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
downloadFile,
|
||||
extractFilenameFromContentDisposition
|
||||
} from '@/base/common/downloadUtil'
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
|
||||
let mockIsCloud = false
|
||||
|
||||
@@ -158,14 +155,10 @@ describe('downloadUtil', () => {
|
||||
const testUrl = 'https://storage.googleapis.com/bucket/file.bin'
|
||||
const blob = new Blob(['test'])
|
||||
const blobFn = vi.fn().mockResolvedValue(blob)
|
||||
const headersMock = {
|
||||
get: vi.fn().mockReturnValue(null)
|
||||
}
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
blob: blobFn,
|
||||
headers: headersMock
|
||||
blob: blobFn
|
||||
} as unknown as Response)
|
||||
|
||||
downloadFile(testUrl)
|
||||
@@ -202,147 +195,5 @@ describe('downloadUtil', () => {
|
||||
expect(createObjectURLSpy).not.toHaveBeenCalled()
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('uses filename from Content-Disposition header in cloud mode', async () => {
|
||||
mockIsCloud = true
|
||||
const testUrl = 'https://storage.googleapis.com/bucket/abc123.png'
|
||||
const blob = new Blob(['test'])
|
||||
const blobFn = vi.fn().mockResolvedValue(blob)
|
||||
const headersMock = {
|
||||
get: vi.fn().mockReturnValue('attachment; filename="user-friendly.png"')
|
||||
}
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
blob: blobFn,
|
||||
headers: headersMock
|
||||
} as unknown as Response)
|
||||
|
||||
downloadFile(testUrl)
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(testUrl)
|
||||
const fetchPromise = fetchMock.mock.results[0].value as Promise<Response>
|
||||
await fetchPromise
|
||||
const blobPromise = blobFn.mock.results[0].value as Promise<Blob>
|
||||
await blobPromise
|
||||
await Promise.resolve()
|
||||
expect(headersMock.get).toHaveBeenCalledWith('Content-Disposition')
|
||||
expect(mockLink.download).toBe('user-friendly.png')
|
||||
})
|
||||
|
||||
it('uses RFC 5987 filename from Content-Disposition header', async () => {
|
||||
mockIsCloud = true
|
||||
const testUrl = 'https://storage.googleapis.com/bucket/abc123.png'
|
||||
const blob = new Blob(['test'])
|
||||
const blobFn = vi.fn().mockResolvedValue(blob)
|
||||
const headersMock = {
|
||||
get: vi
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
'attachment; filename="fallback.png"; filename*=UTF-8\'\'%E4%B8%AD%E6%96%87.png'
|
||||
)
|
||||
}
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
blob: blobFn,
|
||||
headers: headersMock
|
||||
} as unknown as Response)
|
||||
|
||||
downloadFile(testUrl)
|
||||
|
||||
const fetchPromise = fetchMock.mock.results[0].value as Promise<Response>
|
||||
await fetchPromise
|
||||
const blobPromise = blobFn.mock.results[0].value as Promise<Blob>
|
||||
await blobPromise
|
||||
await Promise.resolve()
|
||||
expect(mockLink.download).toBe('中文.png')
|
||||
})
|
||||
|
||||
it('falls back to provided filename when Content-Disposition is missing', async () => {
|
||||
mockIsCloud = true
|
||||
const testUrl = 'https://storage.googleapis.com/bucket/abc123.png'
|
||||
const blob = new Blob(['test'])
|
||||
const blobFn = vi.fn().mockResolvedValue(blob)
|
||||
const headersMock = {
|
||||
get: vi.fn().mockReturnValue(null)
|
||||
}
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
blob: blobFn,
|
||||
headers: headersMock
|
||||
} as unknown as Response)
|
||||
|
||||
downloadFile(testUrl, 'my-fallback.png')
|
||||
|
||||
const fetchPromise = fetchMock.mock.results[0].value as Promise<Response>
|
||||
await fetchPromise
|
||||
const blobPromise = blobFn.mock.results[0].value as Promise<Blob>
|
||||
await blobPromise
|
||||
await Promise.resolve()
|
||||
expect(mockLink.download).toBe('my-fallback.png')
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractFilenameFromContentDisposition', () => {
|
||||
it('returns null for null header', () => {
|
||||
expect(extractFilenameFromContentDisposition(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for empty header', () => {
|
||||
expect(extractFilenameFromContentDisposition('')).toBeNull()
|
||||
})
|
||||
|
||||
it('extracts filename from simple quoted format', () => {
|
||||
const header = 'attachment; filename="test-file.png"'
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe(
|
||||
'test-file.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('extracts filename from unquoted format', () => {
|
||||
const header = 'attachment; filename=test-file.png'
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe(
|
||||
'test-file.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('extracts filename from RFC 5987 format', () => {
|
||||
const header = "attachment; filename*=UTF-8''test%20file.png"
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe(
|
||||
'test file.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('prefers RFC 5987 format over simple format', () => {
|
||||
const header =
|
||||
'attachment; filename="fallback.png"; filename*=UTF-8\'\'preferred.png'
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe(
|
||||
'preferred.png'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles unicode characters in RFC 5987 format', () => {
|
||||
const header =
|
||||
"attachment; filename*=UTF-8''%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6.png"
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe('中文文件.png')
|
||||
})
|
||||
|
||||
it('falls back to simple format when RFC 5987 decoding fails', () => {
|
||||
const header =
|
||||
'attachment; filename="fallback.png"; filename*=UTF-8\'\'%invalid'
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe('fallback.png')
|
||||
})
|
||||
|
||||
it('handles header with only attachment disposition', () => {
|
||||
const header = 'attachment'
|
||||
expect(extractFilenameFromContentDisposition(header)).toBeNull()
|
||||
})
|
||||
|
||||
it('handles case-insensitive filename parameter', () => {
|
||||
const header = 'attachment; FILENAME="test.png"'
|
||||
expect(extractFilenameFromContentDisposition(header)).toBe('test.png')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -75,57 +75,14 @@ const extractFilenameFromUrl = (url: string): string | null => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract filename from Content-Disposition header
|
||||
* Handles both simple format: attachment; filename="name.png"
|
||||
* And RFC 5987 format: attachment; filename="fallback.png"; filename*=UTF-8''encoded%20name.png
|
||||
* @param header - The Content-Disposition header value
|
||||
* @returns The extracted filename or null if not found
|
||||
*/
|
||||
export function extractFilenameFromContentDisposition(
|
||||
header: string | null
|
||||
): string | null {
|
||||
if (!header) return null
|
||||
|
||||
// Try RFC 5987 extended format first (filename*=UTF-8''...)
|
||||
const extendedMatch = header.match(/filename\*=UTF-8''([^;]+)/i)
|
||||
if (extendedMatch?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(extendedMatch[1])
|
||||
} catch {
|
||||
// Fall through to simple format
|
||||
}
|
||||
}
|
||||
|
||||
// Try simple quoted format: filename="..."
|
||||
const quotedMatch = header.match(/filename="([^"]+)"/i)
|
||||
if (quotedMatch?.[1]) {
|
||||
return quotedMatch[1]
|
||||
}
|
||||
|
||||
// Try unquoted format: filename=...
|
||||
const unquotedMatch = header.match(/filename=([^;\s]+)/i)
|
||||
if (unquotedMatch?.[1]) {
|
||||
return unquotedMatch[1]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const downloadViaBlobFetch = async (
|
||||
href: string,
|
||||
fallbackFilename: string
|
||||
filename: string
|
||||
): Promise<void> => {
|
||||
const response = await fetch(href)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${href}: ${response.status}`)
|
||||
}
|
||||
|
||||
// Try to get filename from Content-Disposition header (set by backend)
|
||||
const contentDisposition = response.headers.get('Content-Disposition')
|
||||
const headerFilename =
|
||||
extractFilenameFromContentDisposition(contentDisposition)
|
||||
|
||||
const blob = await response.blob()
|
||||
downloadBlob(headerFilename ?? fallbackFilename, blob)
|
||||
downloadBlob(filename, blob)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="h-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-(--dialog-surface)"
|
||||
>
|
||||
<div ref="container" class="h-full scrollbar-custom">
|
||||
<div :style="topSpacerStyle" />
|
||||
<div :style="mergedGridStyle">
|
||||
<div
|
||||
|
||||
@@ -18,35 +18,17 @@
|
||||
<div class="flex justify-end gap-4">
|
||||
<div
|
||||
v-if="type === 'overwriteBlueprint'"
|
||||
class="flex flex-col justify-start gap-1"
|
||||
class="flex justify-start gap-4"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<input
|
||||
id="doNotAskAgain"
|
||||
v-model="doNotAskAgain"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
<label for="doNotAskAgain">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<i18n-t
|
||||
v-if="doNotAskAgain"
|
||||
keypath="missingModelsDialog.reEnableInSettings"
|
||||
tag="span"
|
||||
class="text-sm text-muted-foreground ml-8"
|
||||
>
|
||||
<template #link>
|
||||
<Button
|
||||
variant="textonly"
|
||||
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
||||
@click="openBlueprintOverwriteSetting"
|
||||
>
|
||||
{{ t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||
</Button>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<Checkbox
|
||||
v-model="doNotAskAgain"
|
||||
class="flex justify-start gap-4"
|
||||
input-id="doNotAskAgain"
|
||||
binary
|
||||
/>
|
||||
<label for="doNotAskAgain" severity="secondary">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -110,13 +92,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import Message from 'primevue/message'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import type { ConfirmationDialogType } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -132,14 +114,6 @@ const { t } = useI18n()
|
||||
|
||||
const onCancel = () => useDialogStore().closeDialog()
|
||||
|
||||
function openBlueprintOverwriteSetting() {
|
||||
useDialogStore().closeDialog()
|
||||
void useDialogService().showSettingsDialog(
|
||||
undefined,
|
||||
'Comfy.Workflow.WarnBlueprintOverwrite'
|
||||
)
|
||||
}
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
const onDeny = () => {
|
||||
|
||||
@@ -5,34 +5,11 @@
|
||||
:title="t('missingModelsDialog.missingModels')"
|
||||
:message="t('missingModelsDialog.missingModelsMessage')"
|
||||
/>
|
||||
<div class="mb-4 flex flex-col gap-1">
|
||||
<div class="flex gap-1">
|
||||
<input
|
||||
id="doNotAskAgain"
|
||||
v-model="doNotAskAgain"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
<label for="doNotAskAgain">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<i18n-t
|
||||
v-if="doNotAskAgain"
|
||||
keypath="missingModelsDialog.reEnableInSettings"
|
||||
tag="span"
|
||||
class="text-sm text-muted-foreground ml-6"
|
||||
>
|
||||
<template #link>
|
||||
<Button
|
||||
variant="textonly"
|
||||
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
||||
@click="openShowMissingModelsSetting"
|
||||
>
|
||||
{{ t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||
</Button>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<div class="mb-4 flex gap-1">
|
||||
<Checkbox v-model="doNotAskAgain" binary input-id="doNotAskAgain" />
|
||||
<label for="doNotAskAgain">{{
|
||||
t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<ListBox :options="missingModels" class="comfy-missing-models">
|
||||
<template #option="{ option }">
|
||||
@@ -54,18 +31,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import ElectronFileDownload from '@/components/common/ElectronFileDownload.vue'
|
||||
import FileDownload from '@/components/common/FileDownload.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import { isDesktop } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { isDesktop } from '@/platform/distribution/types'
|
||||
|
||||
// TODO: Read this from server internal API rather than hardcoding here
|
||||
// as some installations may wish to use custom sources
|
||||
@@ -103,14 +78,6 @@ const { t } = useI18n()
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
function openShowMissingModelsSetting() {
|
||||
useDialogStore().closeDialog({ key: 'global-missing-models-warning' })
|
||||
void useDialogService().showSettingsDialog(
|
||||
undefined,
|
||||
'Comfy.Workflow.ShowMissingModelsWarning'
|
||||
)
|
||||
}
|
||||
|
||||
const modelDownloads = ref<Record<string, ModelInfo>>({})
|
||||
const missingModels = computed(() => {
|
||||
return props.missingModels.map((model) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="comfy-missing-nodes flex w-[490px] flex-col border-t border-border-default"
|
||||
:class="isCloud ? 'border-b' : ''"
|
||||
class="flex w-[490px] flex-col border-t-1 border-border-default"
|
||||
:class="isCloud ? 'border-b-1' : ''"
|
||||
>
|
||||
<div class="flex h-full w-full flex-col gap-4 p-4">
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<p class="m-0 text-sm leading-5 text-muted-foreground">
|
||||
<p class="m-0 text-sm leading-4 text-muted-foreground">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.description')
|
||||
@@ -14,210 +14,32 @@
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<MissingCoreNodesMessage v-if="!isCloud" :missing-core-nodes />
|
||||
|
||||
<!-- QUICK FIX AVAILABLE Section -->
|
||||
<div v-if="replaceableNodes.length > 0" class="flex flex-col gap-2">
|
||||
<!-- Section header with Replace button -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold uppercase text-primary">
|
||||
{{ $t('nodeReplacement.quickFixAvailable') }}
|
||||
</span>
|
||||
<div class="h-2 w-2 rounded-full bg-primary" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip.top="$t('nodeReplacement.replaceWarning')"
|
||||
variant="primary"
|
||||
size="md"
|
||||
:disabled="selectedTypes.size === 0"
|
||||
@click="handleReplaceSelected"
|
||||
>
|
||||
<i class="icon-[lucide--refresh-cw] mr-1.5 h-4 w-4" />
|
||||
{{
|
||||
$t('nodeReplacement.replaceSelected', {
|
||||
count: selectedTypes.size
|
||||
})
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Replaceable nodes list -->
|
||||
<div
|
||||
class="flex max-h-[200px] flex-col overflow-y-auto rounded-lg bg-secondary-background scrollbar-custom"
|
||||
>
|
||||
<!-- Select All row (sticky header) -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'sticky top-0 z-10 flex items-center gap-3 border-b border-border-default bg-secondary-background px-3 py-2',
|
||||
pendingNodes.length > 0
|
||||
? 'cursor-pointer hover:bg-secondary-background-hover'
|
||||
: 'opacity-50 pointer-events-none'
|
||||
)
|
||||
"
|
||||
tabindex="0"
|
||||
role="checkbox"
|
||||
:aria-checked="
|
||||
isAllSelected ? 'true' : isSomeSelected ? 'mixed' : 'false'
|
||||
"
|
||||
@click="toggleSelectAll"
|
||||
@keydown.enter.prevent="toggleSelectAll"
|
||||
@keydown.space.prevent="toggleSelectAll"
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded p-0.5 transition-all duration-200"
|
||||
:class="
|
||||
isAllSelected || isSomeSelected
|
||||
? 'bg-primary-background'
|
||||
: 'bg-secondary-background'
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="isAllSelected"
|
||||
class="icon-[lucide--check] text-bold text-xs text-base-foreground"
|
||||
/>
|
||||
<i
|
||||
v-else-if="isSomeSelected"
|
||||
class="icon-[lucide--minus] text-bold text-xs text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xs font-medium uppercase text-muted-foreground">
|
||||
{{ $t('nodeReplacement.compatibleAlternatives') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Replaceable node items -->
|
||||
<div
|
||||
v-for="node in replaceableNodes"
|
||||
:key="node.label"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-3 px-3 py-2',
|
||||
replacedTypes.has(node.label)
|
||||
? 'opacity-50 pointer-events-none'
|
||||
: 'cursor-pointer hover:bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
tabindex="0"
|
||||
role="checkbox"
|
||||
:aria-checked="
|
||||
replacedTypes.has(node.label) || selectedTypes.has(node.label)
|
||||
? 'true'
|
||||
: 'false'
|
||||
"
|
||||
@click="toggleNode(node.label)"
|
||||
@keydown.enter.prevent="toggleNode(node.label)"
|
||||
@keydown.space.prevent="toggleNode(node.label)"
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded p-0.5 transition-all duration-200"
|
||||
:class="
|
||||
replacedTypes.has(node.label) || selectedTypes.has(node.label)
|
||||
? 'bg-primary-background'
|
||||
: 'bg-secondary-background'
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="
|
||||
replacedTypes.has(node.label) || selectedTypes.has(node.label)
|
||||
"
|
||||
class="icon-[lucide--check] text-bold text-xs text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="replacedTypes.has(node.label)"
|
||||
class="inline-flex h-4 items-center rounded-full border border-success bg-success/10 px-1.5 text-xxxs font-semibold uppercase text-success"
|
||||
>
|
||||
{{ $t('nodeReplacement.replaced') }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex h-4 items-center rounded-full border border-primary bg-primary/10 px-1.5 text-xxxs font-semibold uppercase text-primary"
|
||||
>
|
||||
{{ $t('nodeReplacement.replaceable') }}
|
||||
</span>
|
||||
<span class="text-sm text-foreground">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ node.replacement?.new_node_id ?? node.hint ?? '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MANUAL INSTALLATION REQUIRED Section -->
|
||||
<!-- Missing Nodes List Wrapper -->
|
||||
<div
|
||||
v-if="nonReplaceableNodes.length > 0"
|
||||
class="flex max-h-[200px] flex-col gap-2"
|
||||
class="comfy-missing-nodes flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-secondary-background"
|
||||
>
|
||||
<!-- Section header -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold uppercase text-error">
|
||||
{{ $t('nodeReplacement.installationRequired') }}
|
||||
<div
|
||||
v-for="(node, i) in uniqueNodes"
|
||||
:key="i"
|
||||
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
|
||||
>
|
||||
<span class="text-xs">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
<i class="icon-[lucide--info] text-xs text-error" />
|
||||
</div>
|
||||
|
||||
<!-- Non-replaceable nodes list -->
|
||||
<div
|
||||
class="flex flex-col overflow-y-auto rounded-lg bg-secondary-background scrollbar-custom"
|
||||
>
|
||||
<div
|
||||
v-for="node in nonReplaceableNodes"
|
||||
:key="node.label"
|
||||
class="flex items-center justify-between px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex h-4 items-center rounded-full border border-error bg-error/10 px-1.5 text-xxxs font-semibold uppercase text-error"
|
||||
>
|
||||
{{ $t('nodeReplacement.notReplaceable') }}
|
||||
</span>
|
||||
<span class="text-sm text-foreground">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="node.hint" class="text-xs text-muted-foreground">
|
||||
{{ node.hint }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="node.action"
|
||||
variant="destructive-textonly"
|
||||
size="sm"
|
||||
@click="node.action.callback"
|
||||
>
|
||||
{{ node.action.text }}
|
||||
</Button>
|
||||
</div>
|
||||
<span v-if="node.hint" class="text-xs">{{ node.hint }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom instruction box -->
|
||||
<div
|
||||
class="flex gap-3 rounded-lg border border-warning-background bg-warning-background/10 p-3"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--triangle-alert] mt-0.5 h-4 w-4 shrink-0 text-warning-background"
|
||||
/>
|
||||
<p class="m-0 text-xs leading-5 text-neutral-foreground">
|
||||
<i18n-t keypath="nodeReplacement.instructionMessage">
|
||||
<template #red>
|
||||
<span class="text-error">{{
|
||||
$t('nodeReplacement.redHighlight')
|
||||
}}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<!-- Bottom instruction -->
|
||||
<div>
|
||||
<p class="m-0 text-sm leading-4 text-muted-foreground">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.replacementInstruction')
|
||||
: $t('missingNodes.oss.replacementInstruction')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,39 +47,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
|
||||
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
const { missingNodeTypes } = defineProps<{
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
// Get missing core nodes for OSS mode
|
||||
const { missingCoreNodes } = useMissingNodes()
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
interface ProcessedNode {
|
||||
label: string
|
||||
hint?: string
|
||||
action?: { text: string; callback: () => void }
|
||||
isReplaceable: boolean
|
||||
replacement?: NodeReplacement
|
||||
}
|
||||
|
||||
const replacedTypes = ref<Set<string>>(new Set())
|
||||
|
||||
const uniqueNodes = computed<ProcessedNode[]>(() => {
|
||||
const seenTypes = new Set<string>()
|
||||
return missingNodeTypes
|
||||
const uniqueNodes = computed(() => {
|
||||
const seenTypes = new Set()
|
||||
return props.missingNodeTypes
|
||||
.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
if (seenTypes.has(type)) return false
|
||||
@@ -269,81 +75,10 @@ const uniqueNodes = computed<ProcessedNode[]>(() => {
|
||||
return {
|
||||
label: node.type,
|
||||
hint: node.hint,
|
||||
action: node.action,
|
||||
isReplaceable: node.isReplaceable ?? false,
|
||||
replacement: node.replacement
|
||||
action: node.action
|
||||
}
|
||||
}
|
||||
return { label: node, isReplaceable: false }
|
||||
return { label: node }
|
||||
})
|
||||
})
|
||||
|
||||
const replaceableNodes = computed(() =>
|
||||
uniqueNodes.value.filter((n) => n.isReplaceable)
|
||||
)
|
||||
|
||||
const pendingNodes = computed(() =>
|
||||
replaceableNodes.value.filter((n) => !replacedTypes.value.has(n.label))
|
||||
)
|
||||
|
||||
const nonReplaceableNodes = computed(() =>
|
||||
uniqueNodes.value.filter((n) => !n.isReplaceable)
|
||||
)
|
||||
|
||||
// Selection state - all pending nodes selected by default
|
||||
const selectedTypes = ref(new Set(pendingNodes.value.map((n) => n.label)))
|
||||
|
||||
const isAllSelected = computed(
|
||||
() =>
|
||||
pendingNodes.value.length > 0 &&
|
||||
pendingNodes.value.every((n) => selectedTypes.value.has(n.label))
|
||||
)
|
||||
|
||||
const isSomeSelected = computed(
|
||||
() => selectedTypes.value.size > 0 && !isAllSelected.value
|
||||
)
|
||||
|
||||
function toggleNode(label: string) {
|
||||
if (replacedTypes.value.has(label)) return
|
||||
const next = new Set(selectedTypes.value)
|
||||
if (next.has(label)) {
|
||||
next.delete(label)
|
||||
} else {
|
||||
next.add(label)
|
||||
}
|
||||
selectedTypes.value = next
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (isAllSelected.value) {
|
||||
selectedTypes.value = new Set()
|
||||
} else {
|
||||
selectedTypes.value = new Set(pendingNodes.value.map((n) => n.label))
|
||||
}
|
||||
}
|
||||
|
||||
function handleReplaceSelected() {
|
||||
const selected = missingNodeTypes.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
return selectedTypes.value.has(type)
|
||||
})
|
||||
|
||||
const result = replaceNodesInPlace(selected)
|
||||
const nextReplaced = new Set(replacedTypes.value)
|
||||
const nextSelected = new Set(selectedTypes.value)
|
||||
for (const type of result) {
|
||||
nextReplaced.add(type)
|
||||
nextSelected.delete(type)
|
||||
}
|
||||
replacedTypes.value = nextReplaced
|
||||
selectedTypes.value = nextSelected
|
||||
|
||||
// Auto-close when all replaceable nodes replaced and no non-replaceable remain
|
||||
const allReplaced = replaceableNodes.value.every((n) =>
|
||||
nextReplaced.has(n.label)
|
||||
)
|
||||
if (allReplaced && nonReplaceableNodes.value.length === 0) {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,138 +1,72 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-2 py-2 px-4">
|
||||
<div class="flex flex-col gap-1 text-sm text-muted-foreground">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
id="doNotAskAgainNodes"
|
||||
v-model="doNotAskAgain"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
<label for="doNotAskAgainNodes">{{
|
||||
$t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<i18n-t
|
||||
v-if="doNotAskAgain"
|
||||
keypath="missingModelsDialog.reEnableInSettings"
|
||||
tag="span"
|
||||
class="text-sm text-muted-foreground ml-6"
|
||||
>
|
||||
<template #link>
|
||||
<Button
|
||||
variant="textonly"
|
||||
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
||||
@click="openShowMissingNodesSetting"
|
||||
>
|
||||
{{ $t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||
</Button>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<!-- All nodes replaceable: Skip button (cloud + OSS) -->
|
||||
<div v-if="!hasNonReplaceableNodes" class="flex justify-end gap-1">
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">
|
||||
{{ $t('nodeReplacement.skipForNow') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
||||
<div
|
||||
v-else-if="isCloud"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
||||
>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
as="a"
|
||||
href="https://www.comfy.org/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
as="a"
|
||||
href="https://www.comfy.org/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||
</Button>
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||
$t('missingNodes.cloud.gotIt')
|
||||
}}</Button>
|
||||
</div>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||
</Button>
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||
$t('missingNodes.cloud.gotIt')
|
||||
}}</Button>
|
||||
</div>
|
||||
|
||||
<!-- OSS mode: Manager buttons -->
|
||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1">
|
||||
<Button variant="textonly" @click="handleOpenManager">{{
|
||||
$t('g.openManager')
|
||||
}}</Button>
|
||||
<PackInstallButton
|
||||
v-if="showInstallAllButton"
|
||||
type="secondary"
|
||||
size="md"
|
||||
:disabled="
|
||||
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
||||
"
|
||||
:is-loading="isLoading"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="
|
||||
isLoading
|
||||
? $t('manager.gettingInfo')
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<!-- OSS mode: Open Manager + Install All buttons -->
|
||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
||||
<Button variant="textonly" @click="openManager">{{
|
||||
$t('g.openManager')
|
||||
}}</Button>
|
||||
<PackInstallButton
|
||||
v-if="showInstallAllButton"
|
||||
type="secondary"
|
||||
size="md"
|
||||
:disabled="
|
||||
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
||||
"
|
||||
:is-loading="isLoading"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="
|
||||
isLoading
|
||||
? $t('manager.gettingInfo')
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { computed, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { missingNodeTypes } = defineProps<{
|
||||
missingNodeTypes?: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
watch(doNotAskAgain, (value) => {
|
||||
void useSettingStore().set('Comfy.Workflow.ShowMissingNodesWarning', !value)
|
||||
})
|
||||
|
||||
const handleGotItClick = () => {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
}
|
||||
|
||||
function openShowMissingNodesSetting() {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
void useDialogService().showSettingsDialog(
|
||||
undefined,
|
||||
'Comfy.Workflow.ShowMissingNodesWarning'
|
||||
)
|
||||
}
|
||||
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const managerState = useManagerState()
|
||||
function handleOpenManager() {
|
||||
managerState.openManager({
|
||||
initialTab: ManagerTab.Missing,
|
||||
showToastOnLegacyError: true
|
||||
})
|
||||
}
|
||||
|
||||
// Check if any of the missing packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
@@ -152,29 +86,15 @@ const showInstallAllButton = computed(() => {
|
||||
return managerState.shouldShowInstallButton.value
|
||||
})
|
||||
|
||||
const hasNonReplaceableNodes = computed(
|
||||
() =>
|
||||
missingNodeTypes?.some(
|
||||
(n) =>
|
||||
typeof n === 'string' || (typeof n === 'object' && !n.isReplaceable)
|
||||
) ?? false
|
||||
)
|
||||
const openManager = async () => {
|
||||
await managerState.openManager({
|
||||
initialTab: ManagerTab.Missing,
|
||||
showToastOnLegacyError: true
|
||||
})
|
||||
}
|
||||
|
||||
// Track whether missingNodePacks was ever non-empty (i.e. there were packs to install)
|
||||
const hadMissingPacks = ref(false)
|
||||
|
||||
watch(
|
||||
missingNodePacks,
|
||||
(packs) => {
|
||||
if (packs && packs.length > 0) hadMissingPacks.value = true
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Only consider "all installed" when packs transitioned from non-empty to empty
|
||||
// (actual installation happened). Replaceable-only case is handled by Content auto-close.
|
||||
// Computed to check if all missing nodes have been installed
|
||||
const allMissingNodesInstalled = computed(() => {
|
||||
if (!hadMissingPacks.value) return false
|
||||
return (
|
||||
!isLoading.value &&
|
||||
!isInstalling.value &&
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
<template v-if="comfyAppReady">
|
||||
<TitleEditor />
|
||||
<SelectionToolbox v-if="selectionToolboxEnabled" />
|
||||
<NodeContextMenu />
|
||||
<!-- Render legacy DOM widgets only when Vue nodes are disabled -->
|
||||
<DomWidgets v-if="!shouldRenderVueNodes" />
|
||||
</template>
|
||||
@@ -122,7 +121,6 @@ import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import LinkOverlayCanvas from '@/components/graph/LinkOverlayCanvas.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import NodeContextMenu from '@/components/graph/NodeContextMenu.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import NodePropertiesPanel from '@/components/rightSidePanel/RightSidePanel.vue'
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
</Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
<NodeContextMenu />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -68,6 +69,7 @@ import { useExtensionService } from '@/services/extensionService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
|
||||
import NodeContextMenu from './NodeContextMenu.vue'
|
||||
import FrameNodes from './selectionToolbox/FrameNodes.vue'
|
||||
import NodeOptionsButton from './selectionToolbox/NodeOptionsButton.vue'
|
||||
import VerticalDivider from './selectionToolbox/VerticalDivider.vue'
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Active Jobs Grid -->
|
||||
<div
|
||||
v-if="!isInFolderView && isQueuePanelV2Enabled && activeJobItems.length"
|
||||
class="grid max-h-[50%] scrollbar-custom overflow-y-auto"
|
||||
:style="gridStyle"
|
||||
>
|
||||
<ActiveMediaAssetCard
|
||||
v-for="job in activeJobItems"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Assets Header -->
|
||||
<div
|
||||
v-if="assets.length"
|
||||
:class="cn('px-2 2xl:px-4', activeJobItems.length && 'mt-2')"
|
||||
>
|
||||
<div v-if="assets.length" class="px-2 2xl:px-4">
|
||||
<div
|
||||
class="flex items-center py-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
>
|
||||
@@ -59,25 +43,18 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import ActiveMediaAssetCard from '@/platform/assets/components/ActiveMediaAssetCard.vue'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const {
|
||||
assets,
|
||||
isSelected,
|
||||
isInFolderView = false,
|
||||
assetType = 'output',
|
||||
showOutputCount,
|
||||
getOutputCount
|
||||
} = defineProps<{
|
||||
assets: AssetItem[]
|
||||
isSelected: (assetId: string) => boolean
|
||||
isInFolderView?: boolean
|
||||
assetType?: 'input' | 'output'
|
||||
showOutputCount: (asset: AssetItem) => boolean
|
||||
getOutputCount: (asset: AssetItem) => number
|
||||
@@ -92,19 +69,9 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
|
||||
type AssetGridItem = { key: string; asset: AssetItem }
|
||||
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
|
||||
)
|
||||
|
||||
const assetItems = computed<AssetGridItem[]>(() =>
|
||||
assets.map((asset) => ({
|
||||
key: `asset-${asset.id}`,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import AssetsSidebarListView from './AssetsSidebarListView.vue'
|
||||
|
||||
@@ -10,51 +9,12 @@ vi.mock('vue-i18n', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useJobActions', () => ({
|
||||
useJobActions: () => ({
|
||||
cancelAction: { variant: 'ghost', label: 'Cancel', icon: 'pi pi-times' },
|
||||
canCancelJob: ref(false),
|
||||
runCancelJob: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
const mockJobItems = ref<
|
||||
Array<{
|
||||
id: string
|
||||
title: string
|
||||
meta: string
|
||||
state: string
|
||||
createTime?: number
|
||||
}>
|
||||
>([])
|
||||
|
||||
vi.mock('@/composables/queue/useJobList', () => ({
|
||||
useJobList: () => ({
|
||||
jobItems: mockJobItems
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/assetsStore', () => ({
|
||||
useAssetsStore: () => ({
|
||||
isAssetDeleting: () => false
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: (key: string) => key === 'Comfy.Queue.QPOV2'
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/queueUtil', () => ({
|
||||
isActiveJobState: (state: string) =>
|
||||
state === 'pending' || state === 'running'
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/queueDisplay', () => ({
|
||||
iconForJobState: () => 'pi pi-spinner'
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/schemas/assetMetadataSchema', () => ({
|
||||
getOutputAssetMetadata: () => undefined
|
||||
}))
|
||||
@@ -73,7 +33,6 @@ vi.mock('@/utils/formatUtil', () => ({
|
||||
describe('AssetsSidebarListView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockJobItems.value = []
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
@@ -84,67 +43,14 @@ describe('AssetsSidebarListView', () => {
|
||||
toggleStack: async () => {}
|
||||
}
|
||||
|
||||
it('displays active jobs in oldest-first order (FIFO)', () => {
|
||||
mockJobItems.value = [
|
||||
{
|
||||
id: 'newest',
|
||||
title: 'Newest Job',
|
||||
meta: '',
|
||||
state: 'pending',
|
||||
createTime: 3000
|
||||
},
|
||||
{
|
||||
id: 'middle',
|
||||
title: 'Middle Job',
|
||||
meta: '',
|
||||
state: 'running',
|
||||
createTime: 2000
|
||||
},
|
||||
{
|
||||
id: 'oldest',
|
||||
title: 'Oldest Job',
|
||||
meta: '',
|
||||
state: 'pending',
|
||||
createTime: 1000
|
||||
}
|
||||
]
|
||||
|
||||
it('renders without errors with empty assets', () => {
|
||||
const wrapper = mount(AssetsSidebarListView, {
|
||||
props: defaultProps,
|
||||
shallow: true
|
||||
})
|
||||
|
||||
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(jobListItems).toHaveLength(3)
|
||||
|
||||
const displayedTitles = jobListItems.map((item) =>
|
||||
item.props('primaryText')
|
||||
)
|
||||
expect(displayedTitles).toEqual(['Oldest Job', 'Middle Job', 'Newest Job'])
|
||||
})
|
||||
|
||||
it('excludes completed and failed jobs from active jobs section', () => {
|
||||
mockJobItems.value = [
|
||||
{ id: 'pending', title: 'Pending', meta: '', state: 'pending' },
|
||||
{ id: 'completed', title: 'Completed', meta: '', state: 'completed' },
|
||||
{ id: 'failed', title: 'Failed', meta: '', state: 'failed' },
|
||||
{ id: 'running', title: 'Running', meta: '', state: 'running' }
|
||||
]
|
||||
|
||||
const wrapper = mount(AssetsSidebarListView, {
|
||||
props: defaultProps,
|
||||
shallow: true
|
||||
})
|
||||
|
||||
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(jobListItems).toHaveLength(2)
|
||||
|
||||
const displayedTitles = jobListItems.map((item) =>
|
||||
item.props('primaryText')
|
||||
)
|
||||
expect(displayedTitles).toContain('Running')
|
||||
expect(displayedTitles).toContain('Pending')
|
||||
expect(displayedTitles).not.toContain('Completed')
|
||||
expect(displayedTitles).not.toContain('Failed')
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(listItems).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,48 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<div
|
||||
v-if="isQueuePanelV2Enabled && activeJobItems.length"
|
||||
class="flex max-h-[50%] scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
|
||||
>
|
||||
<AssetsListItem
|
||||
v-for="job in activeJobItems"
|
||||
:key="job.id"
|
||||
:class="
|
||||
cn(
|
||||
'w-full shrink-0 text-text-primary transition-colors hover:bg-secondary-background-hover',
|
||||
'cursor-default'
|
||||
)
|
||||
"
|
||||
:preview-url="job.iconImageUrl"
|
||||
:preview-alt="job.title"
|
||||
:icon-name="job.iconName"
|
||||
:icon-class="getJobIconClass(job)"
|
||||
:primary-text="job.title"
|
||||
:secondary-text="job.meta"
|
||||
:progress-total-percent="job.progressTotalPercent"
|
||||
:progress-current-percent="job.progressCurrentPercent"
|
||||
@mouseenter="onJobEnter(job.id)"
|
||||
@mouseleave="onJobLeave(job.id)"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="hoveredJobId === job.id" #actions>
|
||||
<Button
|
||||
v-if="canCancelJob"
|
||||
:variant="cancelAction.variant"
|
||||
size="icon"
|
||||
:aria-label="cancelAction.label"
|
||||
@click.stop="runCancelJob()"
|
||||
>
|
||||
<i :class="cancelAction.icon" class="size-4" />
|
||||
</Button>
|
||||
</template>
|
||||
</AssetsListItem>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="assetItems.length"
|
||||
:class="cn('px-2', activeJobItems.length && 'mt-2')"
|
||||
>
|
||||
<div v-if="assetItems.length" class="px-2">
|
||||
<div
|
||||
class="flex items-center p-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
>
|
||||
@@ -119,31 +77,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useJobActions } from '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
|
||||
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import {
|
||||
formatDuration,
|
||||
formatSize,
|
||||
getMediaTypeFromFilename,
|
||||
truncateFilename
|
||||
} from '@/utils/formatUtil'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
const {
|
||||
assetItems,
|
||||
@@ -170,24 +122,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
const hoveredJobId = ref<string | null>(null)
|
||||
const hoveredAssetId = ref<string | null>(null)
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
|
||||
)
|
||||
const hoveredJob = computed(() =>
|
||||
hoveredJobId.value
|
||||
? (activeJobItems.value.find((job) => job.id === hoveredJobId.value) ??
|
||||
null)
|
||||
: null
|
||||
)
|
||||
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
|
||||
|
||||
const listGridStyle = {
|
||||
display: 'grid',
|
||||
@@ -240,16 +175,6 @@ function getAssetCardClass(selected: boolean): string {
|
||||
)
|
||||
}
|
||||
|
||||
function onJobEnter(jobId: string) {
|
||||
hoveredJobId.value = jobId
|
||||
}
|
||||
|
||||
function onJobLeave(jobId: string) {
|
||||
if (hoveredJobId.value === jobId) {
|
||||
hoveredJobId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function onAssetEnter(assetId: string) {
|
||||
hoveredAssetId.value = assetId
|
||||
}
|
||||
@@ -259,13 +184,4 @@ function onAssetLeave(assetId: string) {
|
||||
hoveredAssetId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function getJobIconClass(job: JobListItem): string | undefined {
|
||||
const classes = []
|
||||
const iconName = job.iconName ?? iconForJobState(job.state)
|
||||
if (!job.iconImageUrl && iconName === iconForJobState('pending')) {
|
||||
classes.push('animate-spin')
|
||||
}
|
||||
return classes.length ? classes.join(' ') : undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
<template #tool-buttons>
|
||||
<!-- Normal Tab View -->
|
||||
<TabList v-if="!isInFolderView" v-model="activeTab">
|
||||
<Tab v-if="isQueuePanelV2Enabled" class="font-inter" value="queue">
|
||||
{{ $t('sideToolbar.labels.queue') }}
|
||||
<span
|
||||
v-if="activeJobsCount > 0"
|
||||
class="ml-1 inline-flex items-center justify-center rounded-full bg-primary px-1.5 text-xs text-base-foreground font-medium h-5"
|
||||
>
|
||||
{{ activeJobsCount }}
|
||||
</span>
|
||||
</Tab>
|
||||
<Tab class="font-inter" value="output">{{
|
||||
$t('sideToolbar.labels.generated')
|
||||
}}</Tab>
|
||||
@@ -43,8 +52,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<!-- Filter Bar (hidden on queue tab) -->
|
||||
<MediaAssetFilterBar
|
||||
v-if="!isQueueTab"
|
||||
v-model:search-query="searchQuery"
|
||||
v-model:sort-by="sortBy"
|
||||
v-model:view-mode="viewMode"
|
||||
@@ -53,30 +63,29 @@
|
||||
:show-generation-time-sort="activeTab === 'output'"
|
||||
/>
|
||||
<div
|
||||
v-if="isQueuePanelV2Enabled && !isInFolderView"
|
||||
class="flex items-center justify-between px-2 py-2 2xl:px-4"
|
||||
v-if="isQueueTab && !isInFolderView"
|
||||
class="flex items-center justify-between px-4 2xl:px-6"
|
||||
>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ activeJobsLabel }}
|
||||
</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-base-foreground">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearQueueTooltip') }}
|
||||
</span>
|
||||
<MediaAssetViewModeToggle v-model:view-mode="viewMode" />
|
||||
<Button
|
||||
v-tooltip.top="clearQueueTooltip"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.clearQueueTooltip')
|
||||
"
|
||||
:disabled="queuedCount === 0"
|
||||
:disabled="queueStore.pendingTasks.length === 0"
|
||||
@click="handleClearQueue"
|
||||
>
|
||||
<i class="icon-[lucide--list-x] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Divider v-else type="dashed" class="my-2" />
|
||||
<Divider v-else-if="!isQueueTab" type="dashed" class="my-2" />
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="showLoadingState">
|
||||
@@ -87,23 +96,32 @@
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
$t(
|
||||
activeTab === 'input'
|
||||
? 'sideToolbar.noImportedFiles'
|
||||
: 'sideToolbar.noGeneratedFiles'
|
||||
isQueueTab
|
||||
? 'sideToolbar.noQueueItems'
|
||||
: activeTab === 'input'
|
||||
? 'sideToolbar.noImportedFiles'
|
||||
: 'sideToolbar.noGeneratedFiles'
|
||||
)
|
||||
"
|
||||
:message="
|
||||
$t(
|
||||
isQueueTab
|
||||
? 'sideToolbar.noQueueItemsMessage'
|
||||
: 'sideToolbar.noFilesFoundMessage'
|
||||
)
|
||||
"
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="relative size-full" @click="handleEmptySpaceClick">
|
||||
<QueueAssetView v-if="isQueueTab" :view-mode="viewMode" />
|
||||
<AssetsSidebarListView
|
||||
v-if="isListView"
|
||||
v-else-if="isListView"
|
||||
:asset-items="listViewAssetItems"
|
||||
:is-selected="isSelected"
|
||||
:selectable-assets="listViewSelectableAssets"
|
||||
:is-stack-expanded="isListViewStackExpanded"
|
||||
:toggle-stack="toggleListViewStack"
|
||||
:asset-type="activeTab"
|
||||
:asset-type="assetTabType"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
@@ -112,8 +130,7 @@
|
||||
v-else
|
||||
:assets="displayAssets"
|
||||
:is-selected="isSelected"
|
||||
:is-in-folder-view="isInFolderView"
|
||||
:asset-type="activeTab"
|
||||
:asset-type="assetTabType"
|
||||
:show-output-count="shouldShowOutputCount"
|
||||
:get-output-count="getOutputCount"
|
||||
@select-asset="handleAssetSelect"
|
||||
@@ -224,13 +241,16 @@ const Load3dViewerContent = () =>
|
||||
import('@/components/load3d/Load3dViewerContent.vue')
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
import QueueAssetView from '@/components/sidebar/tabs/QueueAssetView.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import MediaAssetContextMenu from '@/platform/assets/components/MediaAssetContextMenu.vue'
|
||||
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
|
||||
import MediaAssetViewModeToggle from '@/platform/assets/components/MediaAssetViewModeToggle.vue'
|
||||
import { getAssetType } from '@/platform/assets/composables/media/assetMappers'
|
||||
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
||||
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
|
||||
@@ -257,7 +277,7 @@ const { activeJobsCount } = storeToRefs(queueStore)
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('output')
|
||||
const activeTab = ref<'input' | 'output' | 'queue'>('output')
|
||||
const folderPromptId = ref<string | null>(null)
|
||||
const folderExecutionTime = ref<number | undefined>(undefined)
|
||||
const isInFolderView = computed(() => folderPromptId.value !== null)
|
||||
@@ -268,6 +288,10 @@ const viewMode = useStorage<'list' | 'grid'>(
|
||||
const isQueuePanelV2Enabled = computed(() =>
|
||||
settingStore.get('Comfy.Queue.QPOV2')
|
||||
)
|
||||
const isQueueTab = computed(() => activeTab.value === 'queue')
|
||||
const assetTabType = computed<'input' | 'output'>(() =>
|
||||
activeTab.value === 'input' ? 'input' : 'output'
|
||||
)
|
||||
const isListView = computed(
|
||||
() => isQueuePanelV2Enabled.value && viewMode.value === 'list'
|
||||
)
|
||||
@@ -302,7 +326,9 @@ const formattedExecutionTime = computed(() => {
|
||||
return formatDuration(folderExecutionTime.value * 1000)
|
||||
})
|
||||
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const clearQueueTooltip = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearQueueTooltip'))
|
||||
)
|
||||
const activeJobsLabel = computed(() => {
|
||||
const count = activeJobsCount.value
|
||||
return t(
|
||||
@@ -415,18 +441,15 @@ const isBulkMode = computed(
|
||||
)
|
||||
|
||||
const showLoadingState = computed(
|
||||
() =>
|
||||
loading.value &&
|
||||
displayAssets.value.length === 0 &&
|
||||
activeJobsCount.value === 0
|
||||
() => !isQueueTab.value && loading.value && displayAssets.value.length === 0
|
||||
)
|
||||
|
||||
const showEmptyState = computed(
|
||||
() =>
|
||||
!loading.value &&
|
||||
displayAssets.value.length === 0 &&
|
||||
activeJobsCount.value === 0
|
||||
)
|
||||
const showEmptyState = computed(() => {
|
||||
if (isQueueTab.value) {
|
||||
return activeJobsCount.value === 0
|
||||
}
|
||||
return !loading.value && displayAssets.value.length === 0
|
||||
})
|
||||
|
||||
watch(visibleAssets, (newAssets) => {
|
||||
// Alternative: keep hidden selections and surface them in UI; for now prune
|
||||
@@ -483,12 +506,21 @@ watch(
|
||||
clearSelection()
|
||||
// Clear search when switching tabs
|
||||
searchQuery.value = ''
|
||||
// Reset pagination state when tab changes
|
||||
void refreshAssets()
|
||||
// Skip asset fetch for queue tab
|
||||
if (activeTab.value !== 'queue') {
|
||||
void refreshAssets()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Reset to output tab if QPOV2 is disabled while on queue tab
|
||||
watch(isQueuePanelV2Enabled, (enabled) => {
|
||||
if (!enabled && activeTab.value === 'queue') {
|
||||
activeTab.value = 'output'
|
||||
}
|
||||
})
|
||||
|
||||
function handleAssetSelect(asset: AssetItem, assets?: AssetItem[]) {
|
||||
const assetList = assets ?? visibleAssets.value
|
||||
const index = assetList.findIndex((a) => a.id === asset.id)
|
||||
|
||||
98
src/components/sidebar/tabs/QueueAssetView.test.ts
Normal file
98
src/components/sidebar/tabs/QueueAssetView.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
|
||||
import QueueAssetView from './QueueAssetView.vue'
|
||||
|
||||
const { mockJobItems } = vi.hoisted<{
|
||||
mockJobItems: Ref<JobListItem[]>
|
||||
}>(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { ref: hoistedRef } = require('vue')
|
||||
return { mockJobItems: hoistedRef([]) }
|
||||
})
|
||||
|
||||
vi.mock('@/composables/queue/useJobList', () => ({
|
||||
useJobList: () => ({
|
||||
jobItems: mockJobItems
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/queue/useJobActions', () => ({
|
||||
useJobActions: () => ({
|
||||
cancelAction: { variant: 'ghost', label: 'Cancel', icon: 'pi pi-times' },
|
||||
canCancelJob: ref(false),
|
||||
runCancelJob: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/queueUtil', () => ({
|
||||
isActiveJobState: (state: string) =>
|
||||
state === 'pending' || state === 'running' || state === 'initialization'
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/queueDisplay', () => ({
|
||||
iconForJobState: () => 'pi pi-spinner'
|
||||
}))
|
||||
|
||||
function makeJob(overrides: Partial<JobListItem>): JobListItem {
|
||||
return {
|
||||
id: 'job-1',
|
||||
title: 'Job 1',
|
||||
meta: '',
|
||||
state: 'pending',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('QueueAssetView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockJobItems.value = []
|
||||
})
|
||||
|
||||
it('displays active jobs in oldest-first order (FIFO)', () => {
|
||||
mockJobItems.value = [
|
||||
makeJob({ id: 'newest', title: 'Newest Job', state: 'pending' }),
|
||||
makeJob({ id: 'middle', title: 'Middle Job', state: 'running' }),
|
||||
makeJob({ id: 'oldest', title: 'Oldest Job', state: 'pending' })
|
||||
]
|
||||
|
||||
const wrapper = mount(QueueAssetView, {
|
||||
props: { viewMode: 'list' },
|
||||
shallow: true
|
||||
})
|
||||
|
||||
const items = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(items).toHaveLength(3)
|
||||
|
||||
const titles = items.map((item) => item.props('primaryText'))
|
||||
expect(titles).toEqual(['Oldest Job', 'Middle Job', 'Newest Job'])
|
||||
})
|
||||
|
||||
it('excludes completed and failed jobs', () => {
|
||||
mockJobItems.value = [
|
||||
makeJob({ id: 'pending', title: 'Pending', state: 'pending' }),
|
||||
makeJob({ id: 'completed', title: 'Completed', state: 'completed' }),
|
||||
makeJob({ id: 'failed', title: 'Failed', state: 'failed' }),
|
||||
makeJob({ id: 'running', title: 'Running', state: 'running' })
|
||||
]
|
||||
|
||||
const wrapper = mount(QueueAssetView, {
|
||||
props: { viewMode: 'list' },
|
||||
shallow: true
|
||||
})
|
||||
|
||||
const items = wrapper.findAllComponents({ name: 'AssetsListItem' })
|
||||
expect(items).toHaveLength(2)
|
||||
|
||||
const titles = items.map((item) => item.props('primaryText'))
|
||||
expect(titles).toContain('Running')
|
||||
expect(titles).toContain('Pending')
|
||||
expect(titles).not.toContain('Completed')
|
||||
expect(titles).not.toContain('Failed')
|
||||
})
|
||||
})
|
||||
124
src/components/sidebar/tabs/QueueAssetView.vue
Normal file
124
src/components/sidebar/tabs/QueueAssetView.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Grid View -->
|
||||
<VirtualGrid
|
||||
v-if="viewMode === 'grid'"
|
||||
class="flex-1"
|
||||
:items="gridItems"
|
||||
:grid-style="gridStyle"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<ActiveMediaAssetCard :job="item.job" />
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
|
||||
<!-- List View -->
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-1 scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
|
||||
>
|
||||
<AssetsListItem
|
||||
v-for="job in activeJobItems"
|
||||
:key="job.id"
|
||||
:class="
|
||||
cn(
|
||||
'w-full shrink-0 text-text-primary transition-colors hover:bg-secondary-background-hover',
|
||||
'cursor-default'
|
||||
)
|
||||
"
|
||||
:preview-url="job.iconImageUrl"
|
||||
:preview-alt="job.title"
|
||||
:icon-name="job.iconName"
|
||||
:icon-class="getJobIconClass(job)"
|
||||
:primary-text="job.title"
|
||||
:secondary-text="job.meta"
|
||||
:progress-total-percent="job.progressTotalPercent"
|
||||
:progress-current-percent="job.progressCurrentPercent"
|
||||
@mouseenter="onJobEnter(job.id)"
|
||||
@mouseleave="onJobLeave(job.id)"
|
||||
@click.stop
|
||||
>
|
||||
<template v-if="hoveredJobId === job.id" #actions>
|
||||
<Button
|
||||
v-if="canCancelJob"
|
||||
:variant="cancelAction.variant"
|
||||
size="icon"
|
||||
:aria-label="cancelAction.label"
|
||||
@click.stop="runCancelJob()"
|
||||
>
|
||||
<i :class="cancelAction.icon" class="size-4" />
|
||||
</Button>
|
||||
</template>
|
||||
</AssetsListItem>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useJobActions } from '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import ActiveMediaAssetCard from '@/platform/assets/components/ActiveMediaAssetCard.vue'
|
||||
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { viewMode = 'grid' } = defineProps<{
|
||||
viewMode?: 'list' | 'grid'
|
||||
}>()
|
||||
|
||||
const { jobItems } = useJobList()
|
||||
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value
|
||||
.filter((item) => isActiveJobState(item.state))
|
||||
.slice()
|
||||
.reverse()
|
||||
)
|
||||
|
||||
const gridItems = computed(() =>
|
||||
activeJobItems.value.map((job) => ({
|
||||
key: `queue-${job.id}`,
|
||||
job
|
||||
}))
|
||||
)
|
||||
|
||||
const gridStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
padding: '0 0.5rem',
|
||||
gap: '0.5rem'
|
||||
}
|
||||
|
||||
const hoveredJobId = ref<string | null>(null)
|
||||
const hoveredJob = computed(() =>
|
||||
hoveredJobId.value
|
||||
? (activeJobItems.value.find((job) => job.id === hoveredJobId.value) ??
|
||||
null)
|
||||
: null
|
||||
)
|
||||
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
|
||||
|
||||
function onJobEnter(jobId: string) {
|
||||
hoveredJobId.value = jobId
|
||||
}
|
||||
|
||||
function onJobLeave(jobId: string) {
|
||||
if (hoveredJobId.value === jobId) {
|
||||
hoveredJobId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function getJobIconClass(job: JobListItem): string | undefined {
|
||||
const iconName = job.iconName ?? iconForJobState(job.state)
|
||||
if (!job.iconImageUrl && iconName === iconForJobState('pending')) {
|
||||
return 'animate-spin'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
</script>
|
||||
@@ -36,7 +36,7 @@ defineProps<{
|
||||
:side-offset="5"
|
||||
:collision-padding="10"
|
||||
v-bind="$attrs"
|
||||
class="z-1700 rounded-lg p-2 bg-base-background shadow-sm border border-border-subtle will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||
class="rounded-lg p-2 bg-base-background shadow-sm border border-border-subtle will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
|
||||
>
|
||||
<slot>
|
||||
<div class="flex flex-col p-1">
|
||||
|
||||
@@ -197,7 +197,6 @@ describe('useCoreCommands', () => {
|
||||
addSetting: vi.fn(),
|
||||
load: vi.fn(),
|
||||
set: vi.fn(),
|
||||
setMany: vi.fn(),
|
||||
exists: vi.fn(),
|
||||
getDefaultValue: vi.fn(),
|
||||
isReady: true,
|
||||
|
||||
@@ -20,8 +20,7 @@ export enum ServerFeatureFlag {
|
||||
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled',
|
||||
LINEAR_TOGGLE_ENABLED = 'linear_toggle_enabled',
|
||||
TEAM_WORKSPACES_ENABLED = 'team_workspaces_enabled',
|
||||
USER_SECRETS_ENABLED = 'user_secrets_enabled',
|
||||
NODE_REPLACEMENTS = 'node_replacements'
|
||||
USER_SECRETS_ENABLED = 'user_secrets_enabled'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,9 +96,6 @@ export function useFeatureFlags() {
|
||||
remoteConfig.value.user_secrets_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.USER_SECRETS_ENABLED, false)
|
||||
)
|
||||
},
|
||||
get nodeReplacementsEnabled() {
|
||||
return api.getServerFeature(ServerFeatureFlag.NODE_REPLACEMENTS, false)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -155,18 +155,11 @@ export function useImageCrop(nodeId: NodeId, options: UseImageCropOptions) {
|
||||
const getInputImageUrl = (): string | null => {
|
||||
if (!node.value) return null
|
||||
|
||||
let sourceNode = node.value.getInputNode(0)
|
||||
if (!sourceNode) return null
|
||||
const inputNode = node.value.getInputNode(0)
|
||||
|
||||
if (sourceNode.isSubgraphNode()) {
|
||||
const link = node.value.getInputLink(0)
|
||||
if (!link) return null
|
||||
const resolved = sourceNode.resolveSubgraphOutputLink(link.origin_slot)
|
||||
sourceNode = resolved?.outputNode ?? null
|
||||
if (!sourceNode) return null
|
||||
}
|
||||
if (!inputNode) return null
|
||||
|
||||
const urls = nodeOutputStore.getNodeImageUrls(sourceNode)
|
||||
const urls = nodeOutputStore.getNodeImageUrls(inputNode)
|
||||
|
||||
if (urls?.length) {
|
||||
return urls[0]
|
||||
@@ -569,10 +562,7 @@ export function useImageCrop(nodeId: NodeId, options: UseImageCropOptions) {
|
||||
|
||||
const initialize = () => {
|
||||
if (nodeId != null) {
|
||||
node.value =
|
||||
app.canvas?.graph?.getNodeById(nodeId) ||
|
||||
app.rootGraph?.getNodeById(nodeId) ||
|
||||
null
|
||||
node.value = app.rootGraph?.getNodeById(nodeId) || null
|
||||
}
|
||||
|
||||
updateImageUrl()
|
||||
|
||||
@@ -219,15 +219,14 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
return modelPath
|
||||
}
|
||||
|
||||
const trimmed = modelPath.trim()
|
||||
const hasOutputSuffix = trimmed.endsWith('[output]')
|
||||
const cleanPath = hasOutputSuffix
|
||||
? trimmed.replace(/\s*\[output\]$/, '')
|
||||
: trimmed
|
||||
const type = hasOutputSuffix || isPreview.value ? 'output' : 'input'
|
||||
|
||||
const [subfolder, filename] = Load3dUtils.splitFilePath(cleanPath)
|
||||
return api.apiURL(Load3dUtils.getResourceURL(subfolder, filename, type))
|
||||
const [subfolder, filename] = Load3dUtils.splitFilePath(modelPath)
|
||||
return api.apiURL(
|
||||
Load3dUtils.getResourceURL(
|
||||
subfolder,
|
||||
filename,
|
||||
isPreview.value ? 'output' : 'input'
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to construct model URL:', error)
|
||||
return null
|
||||
|
||||
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
|
||||
import { SUPPORTED_EXTENSIONS } from '@/extensions/core/load3d/constants'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { createMockFileList } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
@@ -200,7 +199,7 @@ describe('useLoad3dDrag', () => {
|
||||
onModelDrop: mockOnModelDrop
|
||||
})
|
||||
|
||||
const extensions = [...SUPPORTED_EXTENSIONS]
|
||||
const extensions = ['.gltf', '.glb', '.obj', '.fbx', '.stl']
|
||||
|
||||
for (const ext of extensions) {
|
||||
vi.mocked(mockOnModelDrop).mockClear()
|
||||
|
||||
@@ -3,8 +3,8 @@ import { describe, expect, test, vi } from 'vitest'
|
||||
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
||||
import { promoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import { LGraph, LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphCanvas, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createTestSubgraph,
|
||||
@@ -119,94 +119,6 @@ describe('Subgraph proxyWidgets', () => {
|
||||
subgraphNode.widgets[0].computedHeight = 10
|
||||
expect(subgraphNode.widgets[0].value).toBe('value')
|
||||
})
|
||||
test('Proxy widget label shows widgetName, not "nodeId: widgetName"', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
innerNodes[0].addWidget('text', 'seed', 'value', () => {})
|
||||
subgraphNode.properties.proxyWidgets = [['1', 'seed']]
|
||||
|
||||
const proxyWidget = subgraphNode.widgets[0]
|
||||
expect(proxyWidget.label).toBe('seed')
|
||||
expect(proxyWidget.name).toBe('1: seed')
|
||||
})
|
||||
|
||||
test('Proxy widget label reflects linked widget label', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
innerNodes[0].addWidget('text', 'seed', 'value', () => {})
|
||||
subgraphNode.properties.proxyWidgets = [['1', 'seed']]
|
||||
|
||||
const proxyWidget = subgraphNode.widgets[0]
|
||||
expect(proxyWidget.label).toBe('seed')
|
||||
|
||||
innerNodes[0].widgets![0].label = 'My Inner Label'
|
||||
// Trigger re-resolve of linked widget
|
||||
proxyWidget.computedHeight = 10
|
||||
expect(proxyWidget.label).toBe('My Inner Label')
|
||||
})
|
||||
|
||||
test('Proxy widget user rename takes priority over linked widget label', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
innerNodes[0].addWidget('text', 'seed', 'value', () => {})
|
||||
subgraphNode.properties.proxyWidgets = [['1', 'seed']]
|
||||
|
||||
const proxyWidget = subgraphNode.widgets[0]
|
||||
proxyWidget.label = 'My Custom Seed'
|
||||
expect(proxyWidget.label).toBe('My Custom Seed')
|
||||
|
||||
innerNodes[0].widgets![0].label = 'Inner Override'
|
||||
proxyWidget.computedHeight = 10
|
||||
expect(proxyWidget.label).toBe('My Custom Seed')
|
||||
})
|
||||
|
||||
test('Proxy widget label resets to linked widget on undefined', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
innerNodes[0].addWidget('text', 'seed', 'value', () => {})
|
||||
subgraphNode.properties.proxyWidgets = [['1', 'seed']]
|
||||
|
||||
const proxyWidget = subgraphNode.widgets[0]
|
||||
proxyWidget.label = 'Custom'
|
||||
expect(proxyWidget.label).toBe('Custom')
|
||||
|
||||
proxyWidget.label = undefined
|
||||
innerNodes[0].widgets![0].label = 'Inner Label'
|
||||
proxyWidget.computedHeight = 10
|
||||
expect(proxyWidget.label).toBe('Inner Label')
|
||||
})
|
||||
|
||||
test('Proxy widget labels are correct when loaded from serialized data', () => {
|
||||
// Intentionally constructs SubgraphNode via constructor (not setupSubgraph)
|
||||
// to exercise the deserialization/onConfigure path from blueprint JSON.
|
||||
const subgraph = createTestSubgraph()
|
||||
const innerNode = new LGraphNode('InnerNode')
|
||||
subgraph.add(innerNode)
|
||||
innerNode.addWidget('text', 'seed', 'value', () => {})
|
||||
innerNode.addWidget('text', 'steps', 'value', () => {})
|
||||
|
||||
const parentGraph = new LGraph()
|
||||
const subgraphNode = new SubgraphNode(parentGraph, subgraph, {
|
||||
id: 1,
|
||||
type: subgraph.id,
|
||||
pos: [100, 100],
|
||||
size: [200, 100],
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
properties: {
|
||||
proxyWidgets: [
|
||||
['1', 'seed'],
|
||||
['1', 'steps']
|
||||
]
|
||||
},
|
||||
flags: {},
|
||||
mode: 0,
|
||||
order: 0
|
||||
})
|
||||
|
||||
expect(subgraphNode.widgets).toHaveLength(2)
|
||||
expect(subgraphNode.widgets[0].label).toBe('seed')
|
||||
expect(subgraphNode.widgets[0].name).toBe('1: seed')
|
||||
expect(subgraphNode.widgets[1].label).toBe('steps')
|
||||
expect(subgraphNode.widgets[1].name).toBe('1: steps')
|
||||
})
|
||||
|
||||
test('Prevents duplicate promotion', () => {
|
||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||
innerNodes[0].addWidget('text', 'stringWidget', 'value', () => {})
|
||||
|
||||
@@ -199,15 +199,12 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
* and the value used as 'this' if property is a get/set method
|
||||
* @param {unknown} value - only used on set calls. The thing being assigned
|
||||
*/
|
||||
let userLabel: string | undefined
|
||||
const handler = {
|
||||
get(_t: IBaseWidget, property: string, receiver: object) {
|
||||
let redirectedTarget: object = backingWidget
|
||||
let redirectedReceiver = receiver
|
||||
if (property == '_overlay') return overlay
|
||||
else if (property == 'value') redirectedReceiver = backingWidget
|
||||
else if (property == 'label')
|
||||
return userLabel ?? linkedWidget?.label ?? overlay.widgetName
|
||||
if (Object.prototype.hasOwnProperty.call(overlay, property)) {
|
||||
redirectedTarget = overlay
|
||||
redirectedReceiver = overlay
|
||||
@@ -215,10 +212,6 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
return Reflect.get(redirectedTarget, property, redirectedReceiver)
|
||||
},
|
||||
set(_t: IBaseWidget, property: string, value: unknown) {
|
||||
if (property == 'label') {
|
||||
userLabel = value as string | undefined
|
||||
return true
|
||||
}
|
||||
let redirectedTarget: object = backingWidget
|
||||
if (property == 'computedHeight') {
|
||||
if (overlay.widgetName.startsWith('$$') && linkedNode) {
|
||||
|
||||
@@ -6,7 +6,6 @@ useExtensionService().registerExtension({
|
||||
async nodeCreated(node) {
|
||||
if (node.constructor.comfyClass !== 'ImageCropV2') return
|
||||
|
||||
node.hideOutputImages = true
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 450)])
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
CameraState
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import { SUPPORTED_EXTENSIONS_ACCEPT } from '@/extensions/core/load3d/constants'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
@@ -259,7 +258,10 @@ useExtensionService().registerExtension({
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
LOAD_3D(node) {
|
||||
const fileInput = createFileInput(SUPPORTED_EXTENSIONS_ACCEPT, false)
|
||||
const fileInput = createFileInput(
|
||||
'.gltf,.glb,.obj,.fbx,.stl,.ply,.spz,.splat,.ksplat',
|
||||
false
|
||||
)
|
||||
|
||||
node.properties['Resource Folder'] = ''
|
||||
|
||||
|
||||
@@ -14,5 +14,3 @@ export const SUPPORTED_EXTENSIONS = new Set([
|
||||
'.ply',
|
||||
'.ksplat'
|
||||
])
|
||||
|
||||
export const SUPPORTED_EXTENSIONS_ACCEPT = [...SUPPORTED_EXTENSIONS].join(',')
|
||||
|
||||
@@ -56,16 +56,6 @@ useExtensionService().registerExtension({
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (isLoad3dNodeType(nodeData.name)) {
|
||||
// Inject mesh_upload spec flags so WidgetSelect.vue can detect
|
||||
// Load3D's model_file as a mesh upload widget without hardcoding.
|
||||
if (nodeData.name === 'Load3D') {
|
||||
const modelFile = nodeData.input?.required?.model_file
|
||||
if (modelFile?.[1]) {
|
||||
modelFile[1].mesh_upload = true
|
||||
modelFile[1].upload_subfolder = '3d'
|
||||
}
|
||||
}
|
||||
|
||||
// Load the 3D extensions and replay their beforeRegisterNodeDef hooks,
|
||||
// since invokeExtensionsAsync already captured the extensions snapshot
|
||||
// before these new extensions were registered.
|
||||
|
||||
@@ -52,12 +52,14 @@ useExtensionService().registerExtension({
|
||||
showValueWidget.options.hidden = true
|
||||
showValueWidget.options.read_only = true
|
||||
showValueWidget.element.readOnly = true
|
||||
showValueWidget.element.disabled = true
|
||||
showValueWidget.serialize = false
|
||||
|
||||
showValueWidgetPlain.hidden = false
|
||||
showValueWidgetPlain.options.hidden = false
|
||||
showValueWidgetPlain.options.read_only = true
|
||||
showValueWidgetPlain.element.readOnly = true
|
||||
showValueWidgetPlain.element.disabled = true
|
||||
showValueWidgetPlain.serialize = false
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ async function uploadFile(
|
||||
api.apiURL(getResourceURL(...splitFilePath(path)))
|
||||
)
|
||||
|
||||
audioWidget.value = path
|
||||
// Manually trigger the callback to update VueNodes
|
||||
audioWidget.callback?.(path)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
import type {
|
||||
ContextMenuDivElement,
|
||||
IContextMenuOptions,
|
||||
@@ -7,38 +5,6 @@ import type {
|
||||
} from './interfaces'
|
||||
import { LiteGraph } from './litegraph'
|
||||
|
||||
const ALLOWED_TAGS = ['span', 'b', 'i', 'em', 'strong']
|
||||
const ALLOWED_STYLE_PROPS = new Set([
|
||||
'display',
|
||||
'color',
|
||||
'background-color',
|
||||
'padding-left',
|
||||
'border-left'
|
||||
])
|
||||
|
||||
DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => {
|
||||
if (data.attrName === 'style') {
|
||||
const sanitizedStyle = data.attrValue
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => {
|
||||
const colonIdx = s.indexOf(':')
|
||||
if (colonIdx === -1) return false
|
||||
const prop = s.slice(0, colonIdx).trim().toLowerCase()
|
||||
return ALLOWED_STYLE_PROPS.has(prop)
|
||||
})
|
||||
.join('; ')
|
||||
data.attrValue = sanitizedStyle
|
||||
}
|
||||
})
|
||||
|
||||
function sanitizeMenuHTML(html: string): string {
|
||||
return DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS,
|
||||
ALLOWED_ATTR: ['style']
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Replace this pattern with something more modern.
|
||||
export interface ContextMenu<TValue = unknown> {
|
||||
constructor: new (
|
||||
@@ -157,7 +123,7 @@ export class ContextMenu<TValue = unknown> {
|
||||
if (options.title) {
|
||||
const element = document.createElement('div')
|
||||
element.className = 'litemenu-title'
|
||||
element.textContent = options.title
|
||||
element.innerHTML = options.title
|
||||
root.append(element)
|
||||
}
|
||||
|
||||
@@ -252,18 +218,11 @@ export class ContextMenu<TValue = unknown> {
|
||||
if (value === null) {
|
||||
element.classList.add('separator')
|
||||
} else {
|
||||
const label = name === null ? '' : String(name)
|
||||
const innerHtml = name === null ? '' : String(name)
|
||||
if (typeof value === 'string') {
|
||||
element.textContent = label
|
||||
element.innerHTML = innerHtml
|
||||
} else {
|
||||
// Use innerHTML for content that contains HTML tags, textContent otherwise
|
||||
const hasHtmlContent =
|
||||
value?.content !== undefined && /<[a-z][\s\S]*>/i.test(value.content)
|
||||
if (hasHtmlContent) {
|
||||
element.innerHTML = sanitizeMenuHTML(value.content!)
|
||||
} else {
|
||||
element.textContent = value?.title ?? label
|
||||
}
|
||||
element.innerHTML = value?.title ?? innerHtml
|
||||
|
||||
if (value.disabled) {
|
||||
disabled = true
|
||||
|
||||
@@ -73,28 +73,6 @@ describe('LinkConnector SubgraphInput connection validation', () => {
|
||||
expect(toTargetNode.onConnectionsChange).toHaveBeenCalledTimes(1)
|
||||
expect(fromTargetNode.onConnectionsChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('should allow reconnection to same target', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const node = new LGraphNode('TargetNode')
|
||||
node.addInput('number_in', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
const link = subgraph.inputNode.slots[0].connect(node.inputs[0], node)
|
||||
|
||||
const renderLink = new ToInputFromIoNodeLink(
|
||||
subgraph,
|
||||
subgraph.inputNode,
|
||||
subgraph.inputNode.slots[0],
|
||||
undefined,
|
||||
LinkDirection.CENTER,
|
||||
link
|
||||
)
|
||||
renderLink.connectToInput(node, node.inputs[0], connector.events)
|
||||
expect(node.inputs[0].link).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('MovingOutputLink validation', () => {
|
||||
|
||||
@@ -58,12 +58,6 @@ export class ToInputFromIoNodeLink implements RenderLink {
|
||||
events: CustomEventTarget<LinkConnectorEventMap>
|
||||
) {
|
||||
const { fromSlot, fromReroute, existingLink } = this
|
||||
if (
|
||||
existingLink &&
|
||||
node.id === existingLink.target_id &&
|
||||
node.inputs[existingLink.target_slot] === input
|
||||
)
|
||||
return
|
||||
|
||||
const newLink = fromSlot.connect(input, node, fromReroute?.id)
|
||||
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "عدم العرض مرة أخرى",
|
||||
"missingModels": "نماذج مفقودة",
|
||||
"missingModelsMessage": "عند تحميل الرسم البياني، لم يتم العثور على النماذج التالية",
|
||||
"reEnableInSettings": "إعادة التفعيل في {link}",
|
||||
"reEnableInSettingsLink": "الإعدادات"
|
||||
"missingModelsMessage": "عند تحميل الرسم البياني، لم يتم العثور على النماذج التالية"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "يمكنك العودة إلى Nodes 2.0 في أي وقت من القائمة الرئيسية."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(تكراري)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "ابدأ الآن",
|
||||
"title": "مرحباً بك في ComfyUI"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "اختر...",
|
||||
"placeholderAudio": "اختر صوت...",
|
||||
"placeholderImage": "اختر صورة...",
|
||||
"placeholderMesh": "اختر شبكة...",
|
||||
"placeholderModel": "اختر نموذج...",
|
||||
"placeholderUnknown": "اختر وسائط...",
|
||||
"placeholderVideo": "اختر فيديو..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "إنشاء مقاطع فيديو باستخدام Kling V3 من خلال الإطارين الأول والأخير.",
|
||||
"display_name": "Kling 3.0 الإطار الأول والأخير إلى فيديو",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"duration": {
|
||||
"name": "المدة"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "الإطار الأخير"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "الإطار الأول"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "توليد الصوت"
|
||||
},
|
||||
"model": {
|
||||
"name": "النموذج",
|
||||
"tooltip": "إعدادات النموذج والتوليد."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "الدقة"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "الوصف"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "كليغ صورة إلى فيديو",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "نسبة العرض إلى الارتفاع"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "تشابه الموضوع",
|
||||
"tooltip": "تشابه المرجع للموضوع البشري"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "نص التوجيه الإيجابي",
|
||||
"tooltip": "نص التوجيه الإيجابي"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "حرر فيديو موجود باستخدام أحدث نموذج من Kling.",
|
||||
"display_name": "تحرير فيديو Kling Omni (احترافي)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "الاحتفاظ بالصوت الأصلي"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "الدقة"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"video": {
|
||||
"name": "فيديو",
|
||||
"tooltip": "الفيديو للتحرير. سيكون طول الفيديو الناتج هو نفسه."
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "استخدم إطار بداية، وإطار نهاية اختياري، أو صور مرجعية مع أحدث نموذج من Kling.",
|
||||
"display_name": "Kling Omni من الإطار الأول إلى الأخير إلى فيديو (احترافي)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "توليد الصوت",
|
||||
"tooltip": "توليد صوت للفيديو. مدعوم فقط لـ kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "لوحات القصة",
|
||||
"tooltip": "إنشاء سلسلة من مقاطع الفيديو مع أوصاف ومدد فردية. مدعوم فقط لـ kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "عدد السلاسل",
|
||||
"tooltip": "إنشاء سلسلة من الصور. غير مدعوم لـ kling-image-o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "توليد الصوت",
|
||||
"tooltip": "توليد صوت للفيديو. مدعوم فقط لـ kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "لوحات القصة",
|
||||
"tooltip": "إنشاء سلسلة من مقاطع الفيديو مع أوصاف ومدد فردية. مدعوم فقط لـ kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "توليد الصوت"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "البذرة تتحكم في ما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "لوحات القصة",
|
||||
"tooltip": "إنشاء سلسلة من مقاطع الفيديو مع أوصاف ومدد فردية. يتم تجاهلها لنموذج o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "تتحكم البذرة فيما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "إنشاء مقاطع فيديو باستخدام Kling V3. يدعم التحويل من نص إلى فيديو ومن صورة إلى فيديو مع إمكانية استخدام لوحة قصة متعددة التعليمات وخيار توليد الصوت.",
|
||||
"display_name": "Kling 3.0 فيديو",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "توليد الصوت"
|
||||
},
|
||||
"model": {
|
||||
"name": "النموذج",
|
||||
"tooltip": "إعدادات النموذج والتوليد."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "نسبة العرض إلى الارتفاع"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "الدقة"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "متعدد اللقطات",
|
||||
"tooltip": "إنشاء سلسلة من مقاطع الفيديو مع تعليمات ومدة زمنية منفصلة لكل مقطع."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "المدة"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "تعليمة سلبية"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "تعليمة"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة",
|
||||
"tooltip": "تتحكم البذرة فيما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "الإطار الابتدائي",
|
||||
"tooltip": "صورة الإطار الابتدائي (اختياري). عند التوصيل، يتحول إلى وضع صورة إلى فيديو."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "عقدة تجربة الملابس الافتراضية من كليينج. أدخل صورة إنسان وصورة ملابس لتجربة الملابس على الإنسان.",
|
||||
"display_name": "كليينج تجربة الملابس الافتراضية",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "تقطيع الفيديو",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "المدة",
|
||||
"tooltip": "المدة بالثواني، أو 0 لمدة غير محدودة"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "وقت البدء",
|
||||
"tooltip": "وقت البدء بالثواني"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "مدة صارمة",
|
||||
"tooltip": "إذا كانت القيمة صحيحة، سيتم رفع خطأ عند عدم إمكانية تحقيق المدة المحددة."
|
||||
},
|
||||
"video": {
|
||||
"name": "الفيديو"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "توجيه VideoLinearCFG",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "تمكين اختصار التكبير السريع (Ctrl + Shift + سحب)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "إزالة التكرار من معرفات العقد الفرعية",
|
||||
"tooltip": "إعادة تعيين معرفات العقد المكررة تلقائيًا في العقد الفرعية عند تحميل سير العمل."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "علامات منتصف الروابط",
|
||||
"options": {
|
||||
|
||||
@@ -755,6 +755,8 @@
|
||||
"noFilesFound": "No files found",
|
||||
"noImportedFiles": "No imported files found",
|
||||
"noGeneratedFiles": "No generated files found",
|
||||
"noQueueItems": "No active jobs",
|
||||
"noQueueItemsMessage": "Queue a prompt to see active jobs here",
|
||||
"generatedAssetsHeader": "Generated assets",
|
||||
"importedAssetsHeader": "Imported assets",
|
||||
"activeJobStatus": "Active job: {status}",
|
||||
@@ -1691,8 +1693,6 @@
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Don't show this again",
|
||||
"reEnableInSettings": "Re-enable in {link}",
|
||||
"reEnableInSettingsLink": "Settings",
|
||||
"missingModels": "Missing Models",
|
||||
"missingModelsMessage": "When loading the graph, the following models were not found"
|
||||
},
|
||||
@@ -1803,7 +1803,7 @@
|
||||
},
|
||||
"openIn3DViewer": "Open in 3D Viewer",
|
||||
"dropToLoad": "Drop 3D model to load",
|
||||
"unsupportedFileType": "Unsupported file type (supports .gltf, .glb, .obj, .fbx, .stl, .ply, .spz, .splat, .ksplat)",
|
||||
"unsupportedFileType": "Unsupported file type (supports .gltf, .glb, .obj, .fbx, .stl)",
|
||||
"uploadingModel": "Uploading 3D model..."
|
||||
},
|
||||
"imageCrop": {
|
||||
@@ -2389,7 +2389,6 @@
|
||||
"placeholderImage": "Select image...",
|
||||
"placeholderAudio": "Select audio...",
|
||||
"placeholderVideo": "Select video...",
|
||||
"placeholderMesh": "Select mesh...",
|
||||
"placeholderModel": "Select model...",
|
||||
"placeholderUnknown": "Select media..."
|
||||
},
|
||||
@@ -2834,9 +2833,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(Iterative)"
|
||||
},
|
||||
"vueNodesBanner": {
|
||||
"title": "Introducing Nodes 2.0",
|
||||
"desc": "– More flexible workflows, powerful new widgets, built for extensibility",
|
||||
@@ -2881,25 +2877,6 @@
|
||||
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
|
||||
}
|
||||
},
|
||||
"nodeReplacement": {
|
||||
"quickFixAvailable": "Quick Fix Available",
|
||||
"installationRequired": "Installation Required",
|
||||
"compatibleAlternatives": "Compatible Alternatives",
|
||||
"replaceable": "Replaceable",
|
||||
"replaced": "Replaced",
|
||||
"notReplaceable": "Install Required",
|
||||
"selectAll": "Select All",
|
||||
"replaceSelected": "Replace Selected ({count})",
|
||||
"replacedNode": "Replaced node: {nodeType}",
|
||||
"replacedAllNodes": "Replaced {count} node type(s)",
|
||||
"replaceFailed": "Failed to replace nodes",
|
||||
"instructionMessage": "You must install these nodes or replace them with installed alternatives to run the workflow. Missing nodes are highlighted in {red} on the canvas. Some nodes cannot be swapped and must be installed via Node Manager.",
|
||||
"redHighlight": "red",
|
||||
"openNodeManager": "Open Node Manager",
|
||||
"skipForNow": "Skip for Now",
|
||||
"installMissingNodes": "Install Missing Nodes",
|
||||
"replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure."
|
||||
},
|
||||
"rightSidePanel": {
|
||||
"togglePanel": "Toggle properties panel",
|
||||
"noSelection": "Select a node to see its properties and info.",
|
||||
|
||||
@@ -4921,46 +4921,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"display_name": "Kling 3.0 First-Last-Frame to Video",
|
||||
"description": "Generate videos with Kling V3 using first and last frames.",
|
||||
"inputs": {
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "end_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generate_audio"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "Model and generation settings."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "resolution"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling Image(First Frame) to Video",
|
||||
"inputs": {
|
||||
@@ -5007,7 +4967,7 @@
|
||||
}
|
||||
},
|
||||
"KlingImageGenerationNode": {
|
||||
"display_name": "Kling 3.0 Image",
|
||||
"display_name": "Kling Image Generation",
|
||||
"description": "Kling Image Generation Node. Generate an image from a text prompt with an optional reference image.",
|
||||
"inputs": {
|
||||
"prompt": {
|
||||
@@ -5041,13 +5001,6 @@
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5057,7 +5010,7 @@
|
||||
}
|
||||
},
|
||||
"KlingImageToVideoWithAudio": {
|
||||
"display_name": "Kling 2.6 Image(First Frame) to Video with Audio",
|
||||
"display_name": "Kling Image(First Frame) to Video with Audio",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
@@ -5177,7 +5130,7 @@
|
||||
}
|
||||
},
|
||||
"KlingOmniProEditVideoNode": {
|
||||
"display_name": "Kling 3.0 Omni Edit Video",
|
||||
"display_name": "Kling Omni Edit Video (Pro)",
|
||||
"description": "Edit an existing video with the latest model from Kling.",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
@@ -5200,13 +5153,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5216,7 +5162,7 @@
|
||||
}
|
||||
},
|
||||
"KlingOmniProFirstLastFrameNode": {
|
||||
"display_name": "Kling 3.0 Omni First-Last-Frame to Video",
|
||||
"display_name": "Kling Omni First-Last-Frame to Video (Pro)",
|
||||
"description": "Use a start frame, an optional end frame, or reference images with the latest Kling model.",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
@@ -5224,7 +5170,7 @@
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "A text prompt describing the video content. This can include both positive and negative descriptions. Ignored when storyboards are enabled."
|
||||
"tooltip": "A text prompt describing the video content. This can include both positive and negative descriptions."
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
@@ -5234,7 +5180,7 @@
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "end_frame",
|
||||
"tooltip": "An optional end frame for the video. This cannot be used simultaneously with 'reference_images'. Does not work with storyboards."
|
||||
"tooltip": "An optional end frame for the video. This cannot be used simultaneously with 'reference_images'."
|
||||
},
|
||||
"reference_images": {
|
||||
"name": "reference_images",
|
||||
@@ -5242,21 +5188,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Generate a series of video segments with individual prompts and durations. Only supported for kling-v3-omni."
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generate_audio",
|
||||
"tooltip": "Generate audio for the video. Only supported for kling-v3-omni."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5266,7 +5197,7 @@
|
||||
}
|
||||
},
|
||||
"KlingOmniProImageNode": {
|
||||
"display_name": "Kling 3.0 Omni Image",
|
||||
"display_name": "Kling Omni Image (Pro)",
|
||||
"description": "Create or edit images with the latest model from Kling.",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
@@ -5282,20 +5213,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "series_amount",
|
||||
"tooltip": "Generate a series of images. Not supported for kling-image-o1."
|
||||
},
|
||||
"reference_images": {
|
||||
"name": "reference_images",
|
||||
"tooltip": "Up to 10 additional reference images."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5305,7 +5225,7 @@
|
||||
}
|
||||
},
|
||||
"KlingOmniProImageToVideoNode": {
|
||||
"display_name": "Kling 3.0 Omni Image to Video",
|
||||
"display_name": "Kling Omni Image to Video (Pro)",
|
||||
"description": "Use up to 7 reference images to generate a video with the latest Kling model.",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
@@ -5313,7 +5233,7 @@
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "A text prompt describing the video content. This can include both positive and negative descriptions. Ignored when storyboards are enabled."
|
||||
"tooltip": "A text prompt describing the video content. This can include both positive and negative descriptions."
|
||||
},
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
@@ -5327,21 +5247,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Generate a series of video segments with individual prompts and durations. Only supported for kling-v3-omni."
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generate_audio",
|
||||
"tooltip": "Generate audio for the video. Only supported for kling-v3-omni."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5351,7 +5256,7 @@
|
||||
}
|
||||
},
|
||||
"KlingOmniProTextToVideoNode": {
|
||||
"display_name": "Kling 3.0 Omni Text to Video",
|
||||
"display_name": "Kling Omni Text to Video (Pro)",
|
||||
"description": "Use text prompts to generate videos with the latest Kling model.",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
@@ -5359,7 +5264,7 @@
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "A text prompt describing the video content. This can include both positive and negative descriptions. Ignored when storyboards are enabled."
|
||||
"tooltip": "A text prompt describing the video content. This can include both positive and negative descriptions."
|
||||
},
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
@@ -5369,20 +5274,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Generate a series of video segments with individual prompts and durations. Ignored for o1 model."
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generate_audio"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5392,7 +5283,7 @@
|
||||
}
|
||||
},
|
||||
"KlingOmniProVideoToVideoNode": {
|
||||
"display_name": "Kling 3.0 Omni Video to Video",
|
||||
"display_name": "Kling Omni Video to Video (Pro)",
|
||||
"description": "Use a video and up to 4 reference images to generate a video with the latest Kling model.",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
@@ -5421,13 +5312,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5551,7 +5435,7 @@
|
||||
}
|
||||
},
|
||||
"KlingTextToVideoWithAudio": {
|
||||
"display_name": "Kling 2.6 Text to Video with Audio",
|
||||
"display_name": "Kling Text to Video with Audio",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
@@ -5613,54 +5497,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"display_name": "Kling 3.0 Video",
|
||||
"description": "Generate videos with Kling V3. Supports text-to-video and image-to-video with optional storyboard multi-prompt and audio generation.",
|
||||
"inputs": {
|
||||
"multi_shot": {
|
||||
"name": "multi_shot",
|
||||
"tooltip": "Generate a series of video segments with individual prompts and durations."
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generate_audio"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "Model and generation settings."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "start_frame",
|
||||
"tooltip": "Optional start frame image. When connected, switches to image-to-video mode."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "negative_prompt"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "prompt"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"display_name": "Kling Virtual Try On",
|
||||
"description": "Kling Virtual Try On Node. Input a human image and a cloth image to try on the cloth on the human. You can merge multiple clothing item pictures into one image with a white background.",
|
||||
@@ -15573,31 +15409,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "Video Slice",
|
||||
"inputs": {
|
||||
"video": {
|
||||
"name": "video"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "start_time",
|
||||
"tooltip": "Start time in seconds"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"tooltip": "Duration in seconds, or 0 for unlimited duration"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "strict_duration",
|
||||
"tooltip": "If True, when the specified duration is not possible, an error will be raised."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "VideoLinearCFGGuidance",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Enable fast-zoom shortcut (Ctrl + Shift + Drag)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "Deduplicate subgraph node IDs",
|
||||
"tooltip": "Automatically reassign duplicate node IDs in subgraphs when loading a workflow."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Link midpoint markers",
|
||||
"options": {
|
||||
@@ -285,8 +281,8 @@
|
||||
"name": "Show API node pricing badge"
|
||||
},
|
||||
"Comfy_NodeReplacement_Enabled": {
|
||||
"name": "Enable node replacement suggestions",
|
||||
"tooltip": "When enabled, missing nodes with known replacements will be shown as replaceable in the missing nodes dialog, allowing you to review and apply replacements."
|
||||
"name": "Enable automatic node replacement",
|
||||
"tooltip": "When enabled, missing nodes can be automatically replaced with their newer equivalents if a replacement mapping exists."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Node search box implementation",
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "No mostrar esto de nuevo",
|
||||
"missingModels": "Modelos faltantes",
|
||||
"missingModelsMessage": "Al cargar el gráfico, no se encontraron los siguientes modelos",
|
||||
"reEnableInSettings": "Vuelve a habilitar en {link}",
|
||||
"reEnableInSettingsLink": "Configuración"
|
||||
"missingModelsMessage": "Al cargar el gráfico, no se encontraron los siguientes modelos"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "Cambia a Nodes 2.0 en cualquier momento desde el menú principal."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(Iterativo)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Empezar",
|
||||
"title": "Bienvenido a ComfyUI"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "Seleccionar...",
|
||||
"placeholderAudio": "Seleccionar audio...",
|
||||
"placeholderImage": "Seleccionar imagen...",
|
||||
"placeholderMesh": "Seleccionar malla...",
|
||||
"placeholderModel": "Seleccionar modelo...",
|
||||
"placeholderUnknown": "Seleccionar medio...",
|
||||
"placeholderVideo": "Seleccionar video..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Genera videos con Kling V3 usando el primer y último fotograma.",
|
||||
"display_name": "Kling 3.0 Primer-Último Fotograma a Video",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duración"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "último_fotograma"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "primer_fotograma"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generar_audio"
|
||||
},
|
||||
"model": {
|
||||
"name": "modelo",
|
||||
"tooltip": "Configuración del modelo y generación."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "resolución"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling Imagen a Video",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "human_fidelity",
|
||||
"tooltip": "Similitud de referencia del sujeto"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Prompt de texto positivo"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "Edita un video existente con el modelo más reciente de Kling.",
|
||||
"display_name": "Kling Omni Editar Video (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "mantener_sonido_original"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "resolución"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
},
|
||||
"video": {
|
||||
"name": "video",
|
||||
"tooltip": "Video para editar. La longitud del video de salida será la misma."
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "Utiliza un fotograma inicial, un fotograma final opcional o imágenes de referencia con el último modelo de Kling.",
|
||||
"display_name": "Kling Omni Primer-Último-Frame a Video (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generar_audio",
|
||||
"tooltip": "Genera audio para el video. Solo compatible con kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Genera una serie de segmentos de video con prompts y duraciones individuales. Solo compatible con kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "cantidad_de_series",
|
||||
"tooltip": "Genera una serie de imágenes. No compatible con kling-image-o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generar_audio",
|
||||
"tooltip": "Genera audio para el video. Solo compatible con kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Genera una serie de segmentos de video con prompts y duraciones individuales. Solo compatible con kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generar_audio"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Genera una serie de segmentos de video con prompts y duraciones individuales. Ignorado para el modelo o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe volver a ejecutarse; los resultados son no deterministas independientemente de la semilla."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Genera videos con Kling V3. Soporta texto a video e imagen a video con opción de storyboard multiprompt y generación de audio.",
|
||||
"display_name": "Kling 3.0 Video",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar después de generar"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "generar audio"
|
||||
},
|
||||
"model": {
|
||||
"name": "modelo",
|
||||
"tooltip": "Configuración del modelo y generación."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "relación de aspecto"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "resolución"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "multi_shot",
|
||||
"tooltip": "Genera una serie de segmentos de video con indicaciones y duraciones individuales."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "duración"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "indicaciones negativas"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "indicaciones"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla",
|
||||
"tooltip": "La semilla controla si el nodo debe volver a ejecutarse; los resultados son no deterministas independientemente de la semilla."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "fotograma inicial",
|
||||
"tooltip": "Imagen de fotograma inicial opcional. Al conectarse, cambia al modo imagen a video."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Nodo Kling Virtual Try On. Ingresa una imagen de una persona y una imagen de una prenda para probar la prenda en la persona.",
|
||||
"display_name": "Kling Virtual Try On",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "Corte de Video",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "duración",
|
||||
"tooltip": "Duración en segundos, o 0 para duración ilimitada"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "hora de inicio",
|
||||
"tooltip": "Hora de inicio en segundos"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "duración estricta",
|
||||
"tooltip": "Si es Verdadero, cuando la duración especificada no sea posible, se generará un error."
|
||||
},
|
||||
"video": {
|
||||
"name": "video"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "OrientaciónLinealCFGVideo",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Habilitar atajo de zoom rápido (Ctrl + Shift + Arrastrar)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "Eliminar duplicados de IDs de nodos en subgráficos",
|
||||
"tooltip": "Reasigna automáticamente los IDs de nodos duplicados en subgráficos al cargar un flujo de trabajo."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Marcadores de punto medio de enlace",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "دیگر نمایش داده نشود",
|
||||
"missingModels": "مدلهای مفقود",
|
||||
"missingModelsMessage": "هنگام بارگذاری گراف، مدلهای زیر یافت نشدند",
|
||||
"reEnableInSettings": "فعالسازی مجدد در {link}",
|
||||
"reEnableInSettingsLink": "تنظیمات"
|
||||
"missingModelsMessage": "هنگام بارگذاری گراف، مدلهای زیر یافت نشدند"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2820,9 +2818,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "در هر زمان میتوانید از منوی اصلی به Nodes 2.0 بازگردید."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(تکراری)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "شروع کنید",
|
||||
"title": "به ComfyUI خوش آمدید"
|
||||
@@ -2843,7 +2838,6 @@
|
||||
"placeholder": "انتخاب...",
|
||||
"placeholderAudio": "انتخاب صوت...",
|
||||
"placeholderImage": "انتخاب تصویر...",
|
||||
"placeholderMesh": "مش را انتخاب کنید...",
|
||||
"placeholderModel": "انتخاب مدل...",
|
||||
"placeholderUnknown": "انتخاب رسانه...",
|
||||
"placeholderVideo": "انتخاب ویدیو..."
|
||||
|
||||
@@ -5033,46 +5033,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "تولید ویدیو با Kling V3 با استفاده از اولین و آخرین فریم.",
|
||||
"display_name": "Kling 3.0 تبدیل اولین و آخرین فریم به ویدیو",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"duration": {
|
||||
"name": "مدت زمان"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "آخرین فریم"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "اولین فریم"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "تولید صدا"
|
||||
},
|
||||
"model": {
|
||||
"name": "مدل",
|
||||
"tooltip": "تنظیمات مدل و تولید."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "رزولوشن"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "پرامپت"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "تبدیل تصویر Kling (فریم اول) به ویدیو",
|
||||
"inputs": {
|
||||
@@ -5125,9 +5085,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "human_fidelity",
|
||||
"tooltip": "شباهت مرجع سوژه"
|
||||
@@ -5156,10 +5113,6 @@
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "پرامپت متنی مثبت"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5292,9 +5245,6 @@
|
||||
"description": "ویرایش یک ویدیوی موجود با جدیدترین مدل Kling.",
|
||||
"display_name": "ویرایش ویدیوی Omni Kling (حرفهای)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "حفظ صدای اصلی"
|
||||
},
|
||||
@@ -5312,10 +5262,6 @@
|
||||
"resolution": {
|
||||
"name": "وضوح"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
},
|
||||
"video": {
|
||||
"name": "ویدیو",
|
||||
"tooltip": "ویدیو برای ویرایش. طول ویدیوی خروجی همانند ورودی خواهد بود."
|
||||
@@ -5331,9 +5277,6 @@
|
||||
"description": "استفاده از یک فریم شروع، یک فریم پایانی اختیاری یا تصاویر مرجع با جدیدترین مدل Kling.",
|
||||
"display_name": "Kling Omni تبدیل اولین-آخرین فریم به ویدیو (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5344,10 +5287,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "تولید صدا",
|
||||
"tooltip": "تولید صدا برای ویدیو. فقط برای kling-v3-omni پشتیبانی میشود."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5361,14 +5300,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "استوریبوردها",
|
||||
"tooltip": "تولید مجموعهای از بخشهای ویدیویی با پرامپت و مدت زمان جداگانه. فقط برای kling-v3-omni پشتیبانی میشود."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5384,9 +5315,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5400,14 +5328,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "تعداد سری",
|
||||
"tooltip": "تولید مجموعهای از تصاویر. برای kling-image-o1 پشتیبانی نمیشود."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5423,16 +5343,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "تولید صدا",
|
||||
"tooltip": "تولید صدا برای ویدیو. فقط برای kling-v3-omni پشتیبانی میشود."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5446,14 +5359,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "استوریبوردها",
|
||||
"tooltip": "تولید مجموعهای از بخشهای ویدیویی با پرامپت و مدت زمان جداگانه. فقط برای kling-v3-omni پشتیبانی میشود."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5469,15 +5374,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "تولید صدا"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5487,14 +5386,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که node باید دوباره اجرا شود یا خیر؛ نتایج حتی با seed یکسان غیرقطعی هستند."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "استوریبوردها",
|
||||
"tooltip": "تولید مجموعهای از بخشهای ویدیویی با پرامپت و مدت زمان جداگانه. برای مدل o1 نادیده گرفته میشود."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5510,9 +5401,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5536,10 +5424,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که آیا node باید دوباره اجرا شود؛ نتایج صرفنظر از seed غیرقطعی هستند."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5725,54 +5609,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "تولید ویدیو با Kling V3. پشتیبانی از تبدیل متن به ویدیو و تصویر به ویدیو با امکان استفاده از استوریبورد چندپرومپتی و تولید صوت اختیاری.",
|
||||
"display_name": "Kling 3.0 ویدیو",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "تولید صوت"
|
||||
},
|
||||
"model": {
|
||||
"name": "مدل",
|
||||
"tooltip": "تنظیمات مدل و تولید."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "نسبت تصویر"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "رزولوشن"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "چند شات",
|
||||
"tooltip": "تولید مجموعهای از بخشهای ویدیویی با پرومپتها و مدت زمانهای جداگانه."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "مدت زمان"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "پرومپت منفی"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "پرومپت"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "seed تعیین میکند که آیا node باید دوباره اجرا شود؛ نتایج صرفنظر از seed غیرقطعی هستند."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "فریم شروع",
|
||||
"tooltip": "تصویر فریم شروع اختیاری. در صورت اتصال، به حالت تصویر به ویدیو تغییر میکند."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "گره Kling پرو لباس مجازی. یک تصویر انسان و یک تصویر لباس وارد کنید تا لباس را روی انسان امتحان کنید. میتوانید چندین تصویر لباس را در یک تصویر با پسزمینه سفید ادغام کنید.",
|
||||
"display_name": "Kling پرو لباس مجازی",
|
||||
@@ -15448,31 +15284,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "برش ویدیو",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "مدت زمان",
|
||||
"tooltip": "مدت زمان بر حسب ثانیه، یا ۰ برای مدت نامحدود"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "زمان شروع",
|
||||
"tooltip": "زمان شروع بر حسب ثانیه"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "مدت زمان دقیق",
|
||||
"tooltip": "اگر True باشد، در صورت عدم امکان مدت زمان مشخص شده، خطا نمایش داده میشود."
|
||||
},
|
||||
"video": {
|
||||
"name": "ویدیو"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "VideoLinearCFGGuidance",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "فعالسازی میانبر بزرگنمایی سریع (Ctrl + Shift + کشیدن)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "حذف شناسههای تکراری node در زیرگراف",
|
||||
"tooltip": "شناسههای تکراری node در زیرگرافها هنگام بارگذاری workflow بهصورت خودکار دوباره اختصاص داده میشوند."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "نشانگرهای میانهی پیوند",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Ne plus afficher ce message",
|
||||
"missingModels": "Modèles manquants",
|
||||
"missingModelsMessage": "Lors du chargement du graphique, les modèles suivants n'ont pas été trouvés",
|
||||
"reEnableInSettings": "Réactiver dans {link}",
|
||||
"reEnableInSettingsLink": "Paramètres"
|
||||
"missingModelsMessage": "Lors du chargement du graphique, les modèles suivants n'ont pas été trouvés"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "Revenez à Nodes 2.0 à tout moment depuis le menu principal."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(Itératif)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Commencer",
|
||||
"title": "Bienvenue sur ComfyUI"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "Sélectionner...",
|
||||
"placeholderAudio": "Sélectionner un audio...",
|
||||
"placeholderImage": "Sélectionner une image...",
|
||||
"placeholderMesh": "Sélectionner un mesh...",
|
||||
"placeholderModel": "Sélectionner un modèle...",
|
||||
"placeholderUnknown": "Sélectionner un média...",
|
||||
"placeholderVideo": "Sélectionner une vidéo..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Générez des vidéos avec Kling V3 en utilisant la première et la dernière image.",
|
||||
"display_name": "Kling 3.0 Première-Dernière-Image en Vidéo",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"duration": {
|
||||
"name": "durée"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "dernière image"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "première image"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "générer l'audio"
|
||||
},
|
||||
"model": {
|
||||
"name": "modèle",
|
||||
"tooltip": "Paramètres du modèle et de la génération."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "résolution"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling Image to Video",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "human_fidelity",
|
||||
"tooltip": "Similarité de référence du sujet"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Invite textuelle positive"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "Éditez une vidéo existante avec le dernier modèle de Kling.",
|
||||
"display_name": "Kling Omni Édition Vidéo (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "garder_son_original"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "résolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
},
|
||||
"video": {
|
||||
"name": "vidéo",
|
||||
"tooltip": "Vidéo à éditer. La longueur de la vidéo de sortie sera la même."
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "Utilisez une image de départ, une image de fin optionnelle ou des images de référence avec le dernier modèle Kling.",
|
||||
"display_name": "Kling Omni Première-Dernière-Image vers Vidéo (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "générer l'audio",
|
||||
"tooltip": "Générer l'audio pour la vidéo. Pris en charge uniquement pour kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Générez une série de segments vidéo avec des prompts et des durées individuelles. Pris en charge uniquement pour kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "nombre de séries",
|
||||
"tooltip": "Générez une série d'images. Non pris en charge pour kling-image-o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "générer l'audio",
|
||||
"tooltip": "Générer l'audio pour la vidéo. Pris en charge uniquement pour kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Générez une série de segments vidéo avec des prompts et des durées individuelles. Pris en charge uniquement pour kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "générer l'audio"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Générez une série de segments vidéo avec des prompts et des durées individuelles. Ignoré pour le modèle o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Générez des vidéos avec Kling V3. Prend en charge le texte-vers-vidéo et l’image-vers-vidéo avec storyboard multi-invite optionnel et génération audio.",
|
||||
"display_name": "Kling 3.0 Vidéo",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "générer audio"
|
||||
},
|
||||
"model": {
|
||||
"name": "modèle",
|
||||
"tooltip": "Paramètres du modèle et de la génération."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "ratio d’aspect"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "résolution"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "multi_shot",
|
||||
"tooltip": "Générez une série de segments vidéo avec des invites et des durées individuelles."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "durée"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "invite_négative"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "invite"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Le seed contrôle si le nœud doit être relancé ; les résultats sont non déterministes quel que soit le seed."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "image de départ",
|
||||
"tooltip": "Image de départ optionnelle. Lorsqu’elle est connectée, passe en mode image-vers-vidéo."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Nœud Kling Virtual Try On. Importez une image humaine et une image de vêtement pour essayer le vêtement sur la personne.",
|
||||
"display_name": "Kling Virtual Try On",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "Découpage Vidéo",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "durée",
|
||||
"tooltip": "Durée en secondes, ou 0 pour une durée illimitée"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "heure_de_début",
|
||||
"tooltip": "Heure de début en secondes"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "durée_stricte",
|
||||
"tooltip": "Si vrai, lorsqu’il n’est pas possible de respecter la durée spécifiée, une erreur sera générée."
|
||||
},
|
||||
"video": {
|
||||
"name": "vidéo"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "Guidance VideoLinearCFG",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Activer le raccourci de zoom rapide (Ctrl + Shift + Glisser)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "Dédupliquer les identifiants de nœuds de sous-graphe",
|
||||
"tooltip": "Réattribue automatiquement les identifiants de nœuds dupliqués dans les sous-graphes lors du chargement d’un flux de travail."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Marqueurs de point médian du lien",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "再度表示しない",
|
||||
"missingModels": "モデルが見つかりません",
|
||||
"missingModelsMessage": "グラフを読み込む際に、次のモデルが見つかりませんでした",
|
||||
"reEnableInSettings": "{link}で再有効化",
|
||||
"reEnableInSettingsLink": "設定"
|
||||
"missingModelsMessage": "グラフを読み込む際に、次のモデルが見つかりませんでした"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "メインメニューからいつでもNodes 2.0に戻せます。"
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(反復)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "はじめる",
|
||||
"title": "ComfyUIへようこそ"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "選択...",
|
||||
"placeholderAudio": "音声を選択...",
|
||||
"placeholderImage": "画像を選択...",
|
||||
"placeholderMesh": "メッシュを選択...",
|
||||
"placeholderModel": "モデルを選択...",
|
||||
"placeholderUnknown": "メディアを選択...",
|
||||
"placeholderVideo": "動画を選択..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Kling V3 を使用して最初と最後のフレームからビデオを生成します。",
|
||||
"display_name": "Kling 3.0 ファースト・ラストフレームからビデオ生成",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"duration": {
|
||||
"name": "継続時間"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "最後のフレーム"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "最初のフレーム"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "音声を生成"
|
||||
},
|
||||
"model": {
|
||||
"name": "モデル",
|
||||
"tooltip": "モデルと生成設定。"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "プロンプト"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling 画像から動画へ",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "アスペクト比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "人物忠実度",
|
||||
"tooltip": "被写体の参照類似度"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "プロンプト",
|
||||
"tooltip": "ポジティブテキストプロンプト"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "Klingの最新モデルで既存のビデオを編集します。",
|
||||
"display_name": "Kling Omniビデオ編集(Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "元の音声を保持"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
},
|
||||
"video": {
|
||||
"name": "ビデオ",
|
||||
"tooltip": "編集するビデオ。出力ビデオの長さは同じになります。"
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "開始フレーム、オプションの終了フレーム、またはリファレンス画像を使用して最新のKlingモデルでビデオを生成します。",
|
||||
"display_name": "Kling Omni ファースト・ラストフレームからビデオへ (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"duration": {
|
||||
"name": "継続時間"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "開始フレーム"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "音声を生成",
|
||||
"tooltip": "ビデオ用の音声を生成します。kling-v3-omni のみ対応。"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "ストーリーボード",
|
||||
"tooltip": "個別のプロンプトと継続時間で一連のビデオセグメントを生成します。kling-v3-omni のみ対応。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "アスペクト比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "シリーズ数",
|
||||
"tooltip": "一連の画像を生成します。kling-image-o1 では非対応。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "アスペクト比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"duration": {
|
||||
"name": "継続時間"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "音声を生成",
|
||||
"tooltip": "ビデオ用の音声を生成します。kling-v3-omni のみ対応。"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "ストーリーボード",
|
||||
"tooltip": "個別のプロンプトと継続時間で一連のビデオセグメントを生成します。kling-v3-omni のみ対応。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "アスペクト比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"duration": {
|
||||
"name": "継続時間"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "音声を生成"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードを再実行するかどうかを制御します。シードに関係なく結果は非決定的です。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "ストーリーボード",
|
||||
"tooltip": "個別のプロンプトと継続時間で一連のビデオセグメントを生成します。o1 モデルでは無視されます。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "アスペクト比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"duration": {
|
||||
"name": "長さ"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードの再実行を制御します。シードに関係なく結果は非決定的です。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Kling V3でビデオを生成します。テキストからビデオ、画像からビデオ、オプションでストーリーボードのマルチプロンプトやオーディオ生成に対応しています。",
|
||||
"display_name": "Kling 3.0 ビデオ",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後のコントロール"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "オーディオ生成"
|
||||
},
|
||||
"model": {
|
||||
"name": "モデル",
|
||||
"tooltip": "モデルと生成設定。"
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "アスペクト比"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "解像度"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "マルチショット",
|
||||
"tooltip": "個別のプロンプトと時間で一連のビデオセグメントを生成します。"
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "時間"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "ネガティブプロンプト"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "プロンプト"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード",
|
||||
"tooltip": "シードはノードの再実行を制御します。シードに関係なく結果は非決定的です。"
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "開始フレーム",
|
||||
"tooltip": "オプションの開始フレーム画像。接続すると画像からビデオモードに切り替わります。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Klingバーチャル試着ノード。人物画像と服画像を入力して、人物に服を試着させます。",
|
||||
"display_name": "Klingバーチャル試着",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "ビデオスライス",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "時間",
|
||||
"tooltip": "時間(秒)、0の場合は無制限"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "開始時間",
|
||||
"tooltip": "開始時間(秒)"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "厳密な時間",
|
||||
"tooltip": "Trueの場合、指定した時間が不可能な場合はエラーになります。"
|
||||
},
|
||||
"video": {
|
||||
"name": "ビデオ"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "ビデオ線形CFGガイダンス",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "ファストズームショートカットを有効にする(Ctrl + Shift + ドラッグ)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "サブグラフノードIDの重複排除",
|
||||
"tooltip": "ワークフローを読み込む際に、サブグラフ内の重複したノードIDを自動的に再割り当てします。"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "リンク中点マーカー",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "다시 보지 않기",
|
||||
"missingModels": "모델이 없습니다",
|
||||
"missingModelsMessage": "그래프를 로드할 때 다음 모델을 찾을 수 없었습니다",
|
||||
"reEnableInSettings": "{link}에서 다시 활성화",
|
||||
"reEnableInSettingsLink": "설정"
|
||||
"missingModelsMessage": "그래프를 로드할 때 다음 모델을 찾을 수 없었습니다"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "메인 메뉴에서 언제든지 Nodes 2.0으로 다시 전환할 수 있습니다."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(반복)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "시작하기",
|
||||
"title": "ComfyUI에 오신 것을 환영합니다"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "선택...",
|
||||
"placeholderAudio": "오디오 선택...",
|
||||
"placeholderImage": "이미지 선택...",
|
||||
"placeholderMesh": "메시 선택...",
|
||||
"placeholderModel": "모델 선택...",
|
||||
"placeholderUnknown": "미디어 선택...",
|
||||
"placeholderVideo": "비디오 선택..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Kling V3를 사용하여 첫 프레임과 마지막 프레임으로 비디오를 생성합니다.",
|
||||
"display_name": "Kling 3.0 첫-마지막 프레임에서 비디오 생성",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"duration": {
|
||||
"name": "지속 시간"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "마지막 프레임"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "첫 프레임"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "오디오 생성"
|
||||
},
|
||||
"model": {
|
||||
"name": "모델",
|
||||
"tooltip": "모델 및 생성 설정."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "프롬프트"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling 비디오 생성 (이미지 → 비디오)",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "종횡비"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "사람 충실도",
|
||||
"tooltip": "피사체 참조 유사도"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "프롬프트",
|
||||
"tooltip": "이미지 생성을 위한 프롬프트"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "Kling의 최신 모델로 기존 비디오를 편집합니다.",
|
||||
"display_name": "Kling 옴니 비디오 편집 (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "원본 사운드 유지"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
},
|
||||
"video": {
|
||||
"name": "비디오",
|
||||
"tooltip": "편집할 비디오입니다. 출력 비디오 길이는 동일합니다."
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "최신 Kling 모델을 사용하여 시작 프레임, 선택적 종료 프레임 또는 참조 이미지를 사용합니다.",
|
||||
"display_name": "Kling Omni 첫-마지막 프레임에서 비디오로 (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"duration": {
|
||||
"name": "지속 시간"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "시작 프레임"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "오디오 생성",
|
||||
"tooltip": "비디오용 오디오를 생성합니다. kling-v3-omni에서만 지원됩니다."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "스토리보드",
|
||||
"tooltip": "각각의 프롬프트와 지속 시간으로 비디오 세그먼트 시리즈를 생성합니다. kling-v3-omni에서만 지원됩니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "화면 비율"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "시리즈 개수",
|
||||
"tooltip": "이미지 시리즈를 생성합니다. kling-image-o1에서는 지원되지 않습니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "화면 비율"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"duration": {
|
||||
"name": "지속 시간"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "오디오 생성",
|
||||
"tooltip": "비디오용 오디오를 생성합니다. kling-v3-omni에서만 지원됩니다."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "스토리보드",
|
||||
"tooltip": "각각의 프롬프트와 지속 시간으로 비디오 세그먼트 시리즈를 생성합니다. kling-v3-omni에서만 지원됩니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "화면 비율"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"duration": {
|
||||
"name": "지속 시간"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "오디오 생성"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "스토리보드",
|
||||
"tooltip": "각각의 프롬프트와 지속 시간으로 비디오 세그먼트 시리즈를 생성합니다. o1 모델에서는 무시됩니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "종횡비"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"duration": {
|
||||
"name": "길이"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행되어야 하는지 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Kling V3로 비디오를 생성합니다. 텍스트-투-비디오, 이미지-투-비디오, 선택적 스토리보드 멀티 프롬프트 및 오디오 생성을 지원합니다.",
|
||||
"display_name": "Kling 3.0 비디오",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "생성 후 제어"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "오디오 생성"
|
||||
},
|
||||
"model": {
|
||||
"name": "모델",
|
||||
"tooltip": "모델 및 생성 설정."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "종횡비"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "해상도"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "멀티 샷",
|
||||
"tooltip": "각각의 프롬프트와 지속 시간을 가진 비디오 세그먼트 시리즈를 생성합니다."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "지속 시간"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "네거티브 프롬프트"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "프롬프트"
|
||||
},
|
||||
"seed": {
|
||||
"name": "시드",
|
||||
"tooltip": "시드는 노드가 다시 실행되어야 하는지 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "시작 프레임",
|
||||
"tooltip": "선택적 시작 프레임 이미지입니다. 연결 시 이미지-투-비디오 모드로 전환됩니다."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Kling 가상 착용 노드입니다. 인물 이미지와 의류 이미지를 입력하여 인물에게 의류를 착용시킵니다.",
|
||||
"display_name": "Kling 가상 착용",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "비디오 슬라이스",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "지속 시간",
|
||||
"tooltip": "초 단위 지속 시간, 0은 무제한 지속"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "시작 시간",
|
||||
"tooltip": "초 단위 시작 시간"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "엄격한 지속 시간",
|
||||
"tooltip": "True로 설정 시, 지정한 지속 시간이 불가능하면 오류가 발생합니다."
|
||||
},
|
||||
"video": {
|
||||
"name": "비디오"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "비디오 선형 CFG 가이드",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "빠른 확대/축소 단축키 활성화 (Ctrl + Shift + 드래그)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "서브그래프 노드 ID 중복 제거",
|
||||
"tooltip": "워크플로우를 불러올 때 서브그래프 내 중복된 노드 ID를 자동으로 재할당합니다."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "링크 중간점 마커",
|
||||
"options": {
|
||||
@@ -249,8 +245,8 @@
|
||||
"name": "API 노드 가격 배지 표시"
|
||||
},
|
||||
"Comfy_NodeReplacement_Enabled": {
|
||||
"name": "노드 교체 제안 활성화",
|
||||
"tooltip": "활성화하면, 교체 매핑이 존재하는 누락 노드가 교체 가능으로 표시되어 검토 후 교체할 수 있습니다."
|
||||
"name": "자동 노드 교체 활성화",
|
||||
"tooltip": "활성화하면, 누락된 노드를 교체 매핑이 존재할 경우 최신 버전의 노드로 자동 교체할 수 있습니다."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "노드 검색 상자 구현",
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Não mostrar novamente",
|
||||
"missingModels": "Modelos ausentes",
|
||||
"missingModelsMessage": "Ao carregar o grafo, os seguintes modelos não foram encontrados",
|
||||
"reEnableInSettings": "Reativar em {link}",
|
||||
"reEnableInSettingsLink": "Configurações"
|
||||
"missingModelsMessage": "Ao carregar o grafo, os seguintes modelos não foram encontrados"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2820,9 +2818,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "Volte para Nodes 2.0 a qualquer momento pelo menu principal."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(Iterativo)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Começar",
|
||||
"title": "Bem-vindo ao ComfyUI"
|
||||
@@ -2843,7 +2838,6 @@
|
||||
"placeholder": "Selecionar...",
|
||||
"placeholderAudio": "Selecionar áudio...",
|
||||
"placeholderImage": "Selecionar imagem...",
|
||||
"placeholderMesh": "Selecionar malha...",
|
||||
"placeholderModel": "Selecionar modelo...",
|
||||
"placeholderUnknown": "Selecionar mídia...",
|
||||
"placeholderVideo": "Selecionar vídeo..."
|
||||
|
||||
@@ -5033,46 +5033,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Gere vídeos com Kling V3 usando o primeiro e o último frame.",
|
||||
"display_name": "Kling 3.0 Primeiro-Último-Frame para Vídeo",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duração"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "último_frame"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "primeiro_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "gerar_áudio"
|
||||
},
|
||||
"model": {
|
||||
"name": "modelo",
|
||||
"tooltip": "Configurações de modelo e geração."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "resolução"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling Imagem (Primeiro Quadro) para Vídeo",
|
||||
"inputs": {
|
||||
@@ -5125,9 +5085,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "human_fidelity",
|
||||
"tooltip": "Similaridade de referência do sujeito"
|
||||
@@ -5156,10 +5113,6 @@
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Prompt de texto positivo"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5292,9 +5245,6 @@
|
||||
"description": "Edite um vídeo existente com o modelo mais recente da Kling.",
|
||||
"display_name": "Kling Omni Editar Vídeo (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "manter_som_original"
|
||||
},
|
||||
@@ -5312,10 +5262,6 @@
|
||||
"resolution": {
|
||||
"name": "resolução"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
},
|
||||
"video": {
|
||||
"name": "vídeo",
|
||||
"tooltip": "Vídeo para edição. O comprimento do vídeo de saída será o mesmo."
|
||||
@@ -5331,9 +5277,6 @@
|
||||
"description": "Use um quadro inicial, um quadro final opcional ou imagens de referência com o modelo mais recente da Kling.",
|
||||
"display_name": "Kling Omni Quadro Inicial-Final para Vídeo (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5344,10 +5287,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "gerar_áudio",
|
||||
"tooltip": "Gerar áudio para o vídeo. Suportado apenas para kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5361,14 +5300,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Gere uma série de segmentos de vídeo com prompts e durações individuais. Suportado apenas para kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5384,9 +5315,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5400,14 +5328,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "quantidade_de_séries",
|
||||
"tooltip": "Gere uma série de imagens. Não suportado para kling-image-o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5423,16 +5343,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "gerar_áudio",
|
||||
"tooltip": "Gerar áudio para o vídeo. Suportado apenas para kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5446,14 +5359,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Gere uma série de segmentos de vídeo com prompts e durações individuais. Suportado apenas para kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5469,15 +5374,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "gerar_áudio"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5487,14 +5386,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semente",
|
||||
"tooltip": "A semente controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente da semente."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "storyboards",
|
||||
"tooltip": "Gere uma série de segmentos de vídeo com prompts e durações individuais. Ignorado para o modelo o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5510,9 +5401,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5536,10 +5424,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "O seed controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente do seed."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5725,54 +5609,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Gere vídeos com Kling V3. Suporta texto-para-vídeo e imagem-para-vídeo com storyboard opcional, multi-prompt e geração de áudio.",
|
||||
"display_name": "Kling 3.0 Vídeo",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "controlar após gerar"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "gerar áudio"
|
||||
},
|
||||
"model": {
|
||||
"name": "modelo",
|
||||
"tooltip": "Configurações de modelo e geração."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "proporção"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "resolução"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "multi_shot",
|
||||
"tooltip": "Gere uma série de segmentos de vídeo com prompts e durações individuais."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "duração"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "prompt_negativo"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "O seed controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente do seed."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "quadro inicial",
|
||||
"tooltip": "Quadro inicial opcional. Quando conectado, alterna para o modo imagem-para-vídeo."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Nó Kling Prova Virtual. Insira uma imagem de uma pessoa e uma imagem de roupa para experimentar a roupa na pessoa. Você pode mesclar várias imagens de peças de roupa em uma única imagem com fundo branco.",
|
||||
"display_name": "Kling Prova Virtual",
|
||||
@@ -15448,31 +15284,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "Corte de Vídeo",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "duração",
|
||||
"tooltip": "Duração em segundos, ou 0 para duração ilimitada"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "início",
|
||||
"tooltip": "Tempo de início em segundos"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "duração_estrita",
|
||||
"tooltip": "Se Verdadeiro, quando a duração especificada não for possível, um erro será exibido."
|
||||
},
|
||||
"video": {
|
||||
"name": "vídeo"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "VideoLinearCFGGuidance",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Ativar atalho de zoom rápido (Ctrl + Shift + Arrastar)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "Desduplicar IDs de nós de subgrafos",
|
||||
"tooltip": "Reatribui automaticamente IDs de nós duplicados em subgrafos ao carregar um fluxo de trabalho."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Marcadores de meio do link",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Больше не показывать это",
|
||||
"missingModels": "Отсутствующие модели",
|
||||
"missingModelsMessage": "При загрузке графа следующие модели не были найдены",
|
||||
"reEnableInSettings": "Включить снова в {link}",
|
||||
"reEnableInSettingsLink": "Настройки"
|
||||
"missingModelsMessage": "При загрузке графа следующие модели не были найдены"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "Переключиться обратно на Nodes 2.0 можно в главном меню."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(Итеративно)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Начать",
|
||||
"title": "Добро пожаловать в ComfyUI"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "Выбрать...",
|
||||
"placeholderAudio": "Выбрать аудио...",
|
||||
"placeholderImage": "Выбрать изображение...",
|
||||
"placeholderMesh": "Выберите mesh...",
|
||||
"placeholderModel": "Выбрать модель...",
|
||||
"placeholderUnknown": "Выбрать медиа...",
|
||||
"placeholderVideo": "Выбрать видео..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Создавайте видео с помощью Kling V3, используя первый и последний кадры.",
|
||||
"display_name": "Kling 3.0: Видео по первому и последнему кадру",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"duration": {
|
||||
"name": "длительность"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "последний кадр"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "первый кадр"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "создать аудио"
|
||||
},
|
||||
"model": {
|
||||
"name": "модель",
|
||||
"tooltip": "Настройки модели и генерации."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "разрешение"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling Image to Video",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "human_fidelity",
|
||||
"tooltip": "Сходство с референсом субъекта"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Положительный текстовый запрос"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "Редактируйте существующее видео с помощью последней модели от Kling.",
|
||||
"display_name": "Kling: Omni редактирование видео (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "сохранить_оригинальный_звук"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "разрешение"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
},
|
||||
"video": {
|
||||
"name": "видео",
|
||||
"tooltip": "Видео для редактирования. Длина выходного видео будет такой же."
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "Используйте начальный кадр, необязательный конечный кадр или референсные изображения с новейшей моделью Kling.",
|
||||
"display_name": "Kling Omni Первый-Последний Кадр в Видео (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "создать аудио",
|
||||
"tooltip": "Создать аудио для видео. Поддерживается только для kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "раскадровки",
|
||||
"tooltip": "Создайте серию видеосегментов с индивидуальными подсказками и длительностью. Поддерживается только для kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "количество серий",
|
||||
"tooltip": "Создать серию изображений. Не поддерживается для kling-image-o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "создать аудио",
|
||||
"tooltip": "Создать аудио для видео. Поддерживается только для kling-v3-omni."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "раскадровки",
|
||||
"tooltip": "Создайте серию видеосегментов с индивидуальными подсказками и длительностью. Поддерживается только для kling-v3-omni."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "создать аудио"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed определяет, нужно ли повторно запускать узел; результаты всегда недетерминированы, независимо от seed."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "раскадровки",
|
||||
"tooltip": "Создайте серию видеосегментов с индивидуальными подсказками и длительностью. Игнорируется для модели o1."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Сид управляет тем, должен ли узел выполняться повторно; результаты остаются недетерминированными независимо от значения сида."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Генерируйте видео с помощью Kling V3. Поддерживает текст-видео и изображение-видео с дополнительной раскадровкой, мультипромптом и генерацией аудио.",
|
||||
"display_name": "Kling 3.0 Видео",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "контроль после генерации"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "генерировать аудио"
|
||||
},
|
||||
"model": {
|
||||
"name": "модель",
|
||||
"tooltip": "Настройки модели и генерации."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "соотношение сторон"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "разрешение"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "мульти-шот",
|
||||
"tooltip": "Создайте серию видеосегментов с индивидуальными промптами и длительностью."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "длительность"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "негативный промпт"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "промпт"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Сид управляет тем, должен ли узел выполняться повторно; результаты остаются недетерминированными независимо от значения сида."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "стартовый кадр",
|
||||
"tooltip": "Необязательное стартовое изображение кадра. При подключении переключается в режим изображение-видео."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Узел Kling Виртуальная Примерка. Введите изображение человека и изображение одежды, чтобы примерить одежду на человеке.",
|
||||
"display_name": "Kling Виртуальная Примерка",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "Видеофрагмент",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "длительность",
|
||||
"tooltip": "Длительность в секундах, или 0 для неограниченной длительности"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "время начала",
|
||||
"tooltip": "Время начала в секундах"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "строгая длительность",
|
||||
"tooltip": "Если Истина, при невозможности указанной длительности будет вызвана ошибка."
|
||||
},
|
||||
"video": {
|
||||
"name": "видео"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "Направление Video Linear CFG",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Колёсико мыши)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "Удалить дубликаты идентификаторов узлов подграфа",
|
||||
"tooltip": "Автоматически переназначать повторяющиеся идентификаторы узлов в подграфах при загрузке рабочего процесса."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Маркер середины ссылки",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "Bunu bir daha gösterme",
|
||||
"missingModels": "Eksik Modeller",
|
||||
"missingModelsMessage": "Grafik yüklenirken aşağıdaki modeller bulunamadı",
|
||||
"reEnableInSettings": "{link} içinde tekrar etkinleştir",
|
||||
"reEnableInSettingsLink": "Ayarlar"
|
||||
"missingModelsMessage": "Grafik yüklenirken aşağıdaki modeller bulunamadı"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "Ana menüden istediğiniz zaman Nodes 2.0'a geri dönebilirsiniz."
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(Yinelemeli)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "Başlayın",
|
||||
"title": "ComfyUI'ye Hoş Geldiniz"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "Seç...",
|
||||
"placeholderAudio": "Ses seç...",
|
||||
"placeholderImage": "Görsel seç...",
|
||||
"placeholderMesh": "Ağ seç...",
|
||||
"placeholderModel": "Model seç...",
|
||||
"placeholderUnknown": "Medya seç...",
|
||||
"placeholderVideo": "Video seç..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "Kling V3 ile ilk ve son kareleri kullanarak video oluşturun.",
|
||||
"display_name": "Kling 3.0 İlk-Son-Kareden Videoya",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"duration": {
|
||||
"name": "süre"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "son_kare"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "ilk_kare"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "ses_oluştur"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "Model ve oluşturma ayarları."
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "çözünürlük"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling Görüntüden Videoya",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "en_boy_oranı"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "insan_sadakati",
|
||||
"tooltip": "Konu referans benzerliği"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "istem",
|
||||
"tooltip": "Pozitif metin istemi"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "Mevcut bir videoyu Kling'in en son modeliyle düzenleyin.",
|
||||
"display_name": "Kling Omni Video Düzenle (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "orijinal_sesi_koru"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "çözünürlük"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
},
|
||||
"video": {
|
||||
"name": "video",
|
||||
"tooltip": "Düzenlenecek video. Çıktı video uzunluğu aynı olacaktır."
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "Başlangıç karesi, isteğe bağlı bir bitiş karesi veya referans görselleri ile en yeni Kling modeli kullanılır.",
|
||||
"display_name": "Kling Omni İlk-Son-Kare'den Videoya (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "first_frame"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "ses_oluştur",
|
||||
"tooltip": "Video için ses oluşturun. Sadece kling-v3-omni için desteklenir."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "hikaye_tahtaları",
|
||||
"tooltip": "Bireysel promptlar ve sürelerle bir dizi video segmenti oluşturun. Sadece kling-v3-omni için desteklenir."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "seri_sayısı",
|
||||
"tooltip": "Bir dizi görsel oluşturun. kling-image-o1 için desteklenmez."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "ses_oluştur",
|
||||
"tooltip": "Video için ses oluşturun. Sadece kling-v3-omni için desteklenir."
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "hikaye_tahtaları",
|
||||
"tooltip": "Bireysel promptlar ve sürelerle bir dizi video segmenti oluşturun. Sadece kling-v3-omni için desteklenir."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "ses_oluştur"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "hikaye_tahtaları",
|
||||
"tooltip": "Bireysel promptlar ve sürelerle bir dizi video segmenti oluşturun. o1 modeli için dikkate alınmaz."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "resolution"
|
||||
},
|
||||
"seed": {
|
||||
"name": "tohum",
|
||||
"tooltip": "Tohum, düğümün yeniden çalıştırılıp çalıştırılmayacağını kontrol eder; sonuçlar tohumdan bağımsız olarak belirlenemezdir."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "Kling V3 ile videolar oluşturun. Metinden videoya ve isteğe bağlı storyboard çoklu istem ve ses üretimiyle görüntüden videoya destekler.",
|
||||
"display_name": "Kling 3.0 Video",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "oluşturduktan sonra kontrol et"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "ses oluştur"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "Model ve üretim ayarları."
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "en boy oranı"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "çözünürlük"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "çoklu çekim",
|
||||
"tooltip": "Her biri ayrı istem ve süreye sahip bir dizi video segmenti oluşturun."
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "süre"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "negatif istem"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "istem"
|
||||
},
|
||||
"seed": {
|
||||
"name": "tohum",
|
||||
"tooltip": "Tohum, düğümün yeniden çalıştırılıp çalıştırılmayacağını kontrol eder; sonuçlar tohumdan bağımsız olarak belirlenemezdir."
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "başlangıç karesi",
|
||||
"tooltip": "İsteğe bağlı başlangıç kare görüntüsü. Bağlandığında, görüntüden videoya moduna geçer."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Kling Sanal Deneme Düğümü. İnsan üzerine kıyafet denemek için bir insan resmi ve bir kıyafet resmi girin.",
|
||||
"display_name": "Kling Sanal Deneme",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "Video Dilimle",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "süre",
|
||||
"tooltip": "Süre (saniye cinsinden) veya sınırsız süre için 0"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "başlangıç zamanı",
|
||||
"tooltip": "Başlangıç zamanı (saniye cinsinden)"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "kesin süre",
|
||||
"tooltip": "Doğruysa, belirtilen süre mümkün değilse bir hata oluşur."
|
||||
},
|
||||
"video": {
|
||||
"name": "video"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "VideoDoğrusalCFGRehberliği",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Hızlı yakınlaştırma kısayolunu etkinleştir (Ctrl + Shift + Sürükle)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "Alt grafik düğüm kimliklerini çoğaltmayı önle",
|
||||
"tooltip": "Bir iş akışı yüklenirken alt grafiklerdeki yinelenen düğüm kimliklerini otomatik olarak yeniden ata."
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Bağlantı orta nokta işaretçileri",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不要再顯示此訊息",
|
||||
"missingModels": "缺少模型",
|
||||
"missingModelsMessage": "載入圖形時,找不到以下模型",
|
||||
"reEnableInSettings": "請在{link}中重新啟用",
|
||||
"reEnableInSettingsLink": "設定"
|
||||
"missingModelsMessage": "載入圖形時,找不到以下模型"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2809,9 +2807,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "隨時可從主選單切換回 Nodes 2.0。"
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(反覆運算)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "開始使用",
|
||||
"title": "歡迎使用 ComfyUI"
|
||||
@@ -2832,7 +2827,6 @@
|
||||
"placeholder": "選擇...",
|
||||
"placeholderAudio": "選擇音訊...",
|
||||
"placeholderImage": "選擇圖片...",
|
||||
"placeholderMesh": "選擇網格...",
|
||||
"placeholderModel": "選擇模型...",
|
||||
"placeholderUnknown": "選擇媒體...",
|
||||
"placeholderVideo": "選擇影片..."
|
||||
|
||||
@@ -5024,46 +5024,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "使用 Kling V3 以首尾影格產生影片。",
|
||||
"display_name": "Kling 3.0 首尾影格轉影片",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "尾影格"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "首影格"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "產生音訊"
|
||||
},
|
||||
"model": {
|
||||
"name": "模型",
|
||||
"tooltip": "模型與生成設定。"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "提示詞"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling 圖像轉影片",
|
||||
"inputs": {
|
||||
@@ -5116,9 +5076,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "aspect_ratio"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "human_fidelity",
|
||||
"tooltip": "主體參考相似度"
|
||||
@@ -5147,10 +5104,6 @@
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "正向文字提示"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5283,9 +5236,6 @@
|
||||
"description": "使用 Kling 最新模型編輯現有影片。",
|
||||
"display_name": "Kling Omni 編輯影片(專業版)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "保留原始音訊"
|
||||
},
|
||||
@@ -5303,10 +5253,6 @@
|
||||
"resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
},
|
||||
"video": {
|
||||
"name": "影片",
|
||||
"tooltip": "要編輯的影片。輸出影片長度將與原影片相同。"
|
||||
@@ -5322,9 +5268,6 @@
|
||||
"description": "使用起始影格、可選的結束影格,或參考圖片,搭配最新 Kling 模型。",
|
||||
"display_name": "Kling Omni 首末影格轉影片 (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
@@ -5335,10 +5278,6 @@
|
||||
"first_frame": {
|
||||
"name": "起始影格"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "產生音訊",
|
||||
"tooltip": "為影片產生音訊。僅支援 kling-v3-omni。"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5352,14 +5291,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "分鏡腳本",
|
||||
"tooltip": "以個別提示詞與時長產生一系列影片片段。僅支援 kling-v3-omni。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5375,9 +5306,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "長寬比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5391,14 +5319,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "系列數量",
|
||||
"tooltip": "產生一系列影像。不支援 kling-image-o1。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5414,16 +5334,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "長寬比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "產生音訊",
|
||||
"tooltip": "為影片產生音訊。僅支援 kling-v3-omni。"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5437,14 +5350,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "分鏡腳本",
|
||||
"tooltip": "以個別提示詞與時長產生一系列影片片段。僅支援 kling-v3-omni。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5460,15 +5365,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "長寬比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "產生音訊"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "model_name"
|
||||
},
|
||||
@@ -5478,14 +5377,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "種子",
|
||||
"tooltip": "種子決定此節點是否需重新執行;無論種子為何,結果皆為非決定性。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "分鏡腳本",
|
||||
"tooltip": "以個別提示詞與時長產生一系列影片片段。o1 模型將忽略此設定。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5501,9 +5392,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "長寬比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
@@ -5527,10 +5415,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed 控制此節點是否重新執行;無論 seed 為何,結果皆為非確定性。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5716,54 +5600,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "使用 Kling V3 產生影片。支援文字轉影片與圖片轉影片,可選擇分鏡多提示與音訊生成。",
|
||||
"display_name": "Kling 3.0 影片",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後控制"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "生成音訊"
|
||||
},
|
||||
"model": {
|
||||
"name": "模型",
|
||||
"tooltip": "模型與生成設定。"
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "長寬比"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "解析度"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "多段生成",
|
||||
"tooltip": "以個別提示與時長產生一系列影片片段。"
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "時長"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "負面提示"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "提示"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed 控制此節點是否重新執行;無論 seed 為何,結果皆為非確定性。"
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "起始畫格",
|
||||
"tooltip": "可選的起始畫格圖片。連接後將切換為圖片轉影片模式。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Kling 虛擬試穿節點。輸入一張人物圖片和一張服裝圖片,將服裝試穿在人物身上。",
|
||||
"display_name": "Kling 虛擬試穿",
|
||||
@@ -15435,31 +15271,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "影片切片",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "時長",
|
||||
"tooltip": "以秒為單位的時長,0 代表無限時長"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "起始時間",
|
||||
"tooltip": "以秒為單位的起始時間"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "嚴格時長",
|
||||
"tooltip": "若為 True,當指定時長無法達成時,將產生錯誤。"
|
||||
},
|
||||
"video": {
|
||||
"name": "影片"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "影片線性 CFG 引導",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "啟用快速縮放快捷鍵(Ctrl + Shift + 拖曳)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "去重子圖節點 ID",
|
||||
"tooltip": "載入工作流程時,自動重新分配子圖中重複的節點 ID。"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "連結中點標記",
|
||||
"options": {
|
||||
|
||||
@@ -1740,9 +1740,7 @@
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
"missingModels": "缺少模型",
|
||||
"missingModelsMessage": "加载工作流时,未找到以下模型",
|
||||
"reEnableInSettings": "可在{link}中重新启用",
|
||||
"reEnableInSettingsLink": "设置"
|
||||
"missingModelsMessage": "加载工作流时,未找到以下模型"
|
||||
},
|
||||
"missingNodes": {
|
||||
"cloud": {
|
||||
@@ -2820,9 +2818,6 @@
|
||||
"vueNodesMigrationMainMenu": {
|
||||
"message": "在主菜单中随时切换回 Nodes 2.0"
|
||||
},
|
||||
"vueNodesSlot": {
|
||||
"iterative": "(迭代)"
|
||||
},
|
||||
"welcome": {
|
||||
"getStarted": "开始使用",
|
||||
"title": "欢迎使用 ComfyUI"
|
||||
@@ -2843,7 +2838,6 @@
|
||||
"placeholder": "请选择...",
|
||||
"placeholderAudio": "请选择音频...",
|
||||
"placeholderImage": "请选择图片...",
|
||||
"placeholderMesh": "选择网格...",
|
||||
"placeholderModel": "请选择模型...",
|
||||
"placeholderUnknown": "请选择媒体...",
|
||||
"placeholderVideo": "请选择视频..."
|
||||
|
||||
@@ -5033,46 +5033,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingFirstLastFrameNode": {
|
||||
"description": "使用 Kling V3 通过首帧和末帧生成视频。",
|
||||
"display_name": "Kling 3.0 首帧-末帧生成视频",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
"end_frame": {
|
||||
"name": "末帧"
|
||||
},
|
||||
"first_frame": {
|
||||
"name": "首帧"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "生成音频"
|
||||
},
|
||||
"model": {
|
||||
"name": "模型",
|
||||
"tooltip": "模型与生成设置。"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "提示词"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingImage2VideoNode": {
|
||||
"display_name": "Kling 图像转视频",
|
||||
"inputs": {
|
||||
@@ -5125,9 +5085,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "宽高比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"human_fidelity": {
|
||||
"name": "主体参考强度",
|
||||
"tooltip": "主体参考相似度"
|
||||
@@ -5156,10 +5113,6 @@
|
||||
"prompt": {
|
||||
"name": "提示词",
|
||||
"tooltip": "正向文本提示"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5292,9 +5245,6 @@
|
||||
"description": "使用 Kling 最新模型编辑视频。",
|
||||
"display_name": "Kling Omni 编辑视频 (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"keep_original_sound": {
|
||||
"name": "保留原音频"
|
||||
},
|
||||
@@ -5312,10 +5262,6 @@
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
},
|
||||
"video": {
|
||||
"name": "视频",
|
||||
"tooltip": "需要编辑的视频,输出视频的时长和输入视频相同。"
|
||||
@@ -5331,9 +5277,6 @@
|
||||
"description": "使用 Kling 最新模型和起始帧、可选的结束帧或参考图像。",
|
||||
"display_name": "Kling Omni 首尾帧到视频 (Pro)",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
@@ -5344,10 +5287,6 @@
|
||||
"first_frame": {
|
||||
"name": "起始帧"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "生成音频",
|
||||
"tooltip": "为视频生成音频。仅支持 kling-v3-omni。"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "模型"
|
||||
},
|
||||
@@ -5361,14 +5300,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "分镜",
|
||||
"tooltip": "生成一系列带有独立提示词和时长的视频片段。仅支持 kling-v3-omni。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5384,9 +5315,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "宽高比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "模型"
|
||||
},
|
||||
@@ -5400,14 +5328,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
},
|
||||
"series_amount": {
|
||||
"name": "系列数量",
|
||||
"tooltip": "生成一系列图像。不支持 kling-image-o1。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5423,16 +5343,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "宽高比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "生成音频",
|
||||
"tooltip": "为视频生成音频。仅支持 kling-v3-omni。"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "模型"
|
||||
},
|
||||
@@ -5446,14 +5359,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "分镜",
|
||||
"tooltip": "生成一系列带有独立提示词和时长的视频片段。仅支持 kling-v3-omni。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5469,15 +5374,9 @@
|
||||
"aspect_ratio": {
|
||||
"name": "宽高比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "生成音频"
|
||||
},
|
||||
"model_name": {
|
||||
"name": "模型"
|
||||
},
|
||||
@@ -5487,14 +5386,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"seed": {
|
||||
"name": "种子",
|
||||
"tooltip": "种子控制节点是否重新运行;无论种子如何,结果都是非确定性的。"
|
||||
},
|
||||
"storyboards": {
|
||||
"name": "分镜",
|
||||
"tooltip": "生成一系列带有独立提示词和时长的视频片段。o1 模型将忽略此项。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5510,9 +5401,6 @@
|
||||
"aspect_ratio": {
|
||||
"name": "宽高比"
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
@@ -5536,10 +5424,6 @@
|
||||
},
|
||||
"resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed 控制节点是否重新运行;无论 seed 如何,结果都是非确定性的。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -5725,54 +5609,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVideoNode": {
|
||||
"description": "使用 Kling V3 生成视频。支持文生视频和图生视频,可选分镜多提示词和音频生成。",
|
||||
"display_name": "Kling 3.0 视频",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成后控制"
|
||||
},
|
||||
"generate_audio": {
|
||||
"name": "生成音频"
|
||||
},
|
||||
"model": {
|
||||
"name": "模型",
|
||||
"tooltip": "模型和生成设置。"
|
||||
},
|
||||
"model_aspect_ratio": {
|
||||
"name": "宽高比"
|
||||
},
|
||||
"model_resolution": {
|
||||
"name": "分辨率"
|
||||
},
|
||||
"multi_shot": {
|
||||
"name": "多镜头",
|
||||
"tooltip": "使用单独的提示词和时长生成一系列视频片段。"
|
||||
},
|
||||
"multi_shot_duration": {
|
||||
"name": "时长"
|
||||
},
|
||||
"multi_shot_negative_prompt": {
|
||||
"name": "反向提示词"
|
||||
},
|
||||
"multi_shot_prompt": {
|
||||
"name": "提示词"
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed 控制节点是否重新运行;无论 seed 如何,结果都是非确定性的。"
|
||||
},
|
||||
"start_frame": {
|
||||
"name": "起始帧",
|
||||
"tooltip": "可选的起始帧图像。连接后切换为图生视频模式。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"KlingVirtualTryOnNode": {
|
||||
"description": "Kling 虚拟试穿节点。输入一张人物图片和一张服装图片,将服装试穿到人物身上。",
|
||||
"display_name": "Kling 虚拟试穿",
|
||||
@@ -15448,31 +15284,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Video Slice": {
|
||||
"display_name": "视频切片",
|
||||
"inputs": {
|
||||
"duration": {
|
||||
"name": "时长",
|
||||
"tooltip": "以秒为单位的时长,0 表示不限时长"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "起始时间",
|
||||
"tooltip": "以秒为单位的起始时间"
|
||||
},
|
||||
"strict_duration": {
|
||||
"name": "严格时长",
|
||||
"tooltip": "如为 True,当指定时长无法实现时将报错。"
|
||||
},
|
||||
"video": {
|
||||
"name": "视频"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"VideoLinearCFGGuidance": {
|
||||
"display_name": "视频线性CFG引导",
|
||||
"inputs": {
|
||||
|
||||
@@ -104,10 +104,6 @@
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "启用快速缩放快捷键(Ctrl + Shift + 拖动)"
|
||||
},
|
||||
"Comfy_Graph_DeduplicateSubgraphNodeIds": {
|
||||
"name": "去重子图节点ID",
|
||||
"tooltip": "在加载工作流时,自动重新分配子图中重复的节点ID。"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "连线中点标记",
|
||||
"options": {
|
||||
|
||||
@@ -53,9 +53,7 @@ describe('useWorkspaceSwitch', () => {
|
||||
id: 'workspace-1',
|
||||
name: 'Test Workspace',
|
||||
type: 'personal',
|
||||
role: 'owner',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
joined_at: '2026-01-01T00:00:00Z'
|
||||
role: 'owner'
|
||||
}
|
||||
mockModifiedWorkflows.length = 0
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ const mockAccessBillingPortal = vi.fn()
|
||||
const mockReportError = vi.fn()
|
||||
const mockTrackBeginCheckout = vi.fn()
|
||||
const mockUserId = ref<string | undefined>('user-123')
|
||||
const mockGetAuthHeader = vi.fn(() =>
|
||||
const mockGetFirebaseAuthHeader = vi.fn(() =>
|
||||
Promise.resolve({ Authorization: 'Bearer test-token' })
|
||||
)
|
||||
const mockGetCheckoutAttribution = vi.hoisted(() => vi.fn(() => ({})))
|
||||
@@ -58,7 +58,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: () =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
getFirebaseAuthHeader: mockGetFirebaseAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
}),
|
||||
FirebaseAuthStoreError: class extends Error {}
|
||||
|
||||
@@ -267,7 +267,7 @@ import { isPlanDowngrade } from '@/platform/cloud/subscription/utils/subscriptio
|
||||
import type { BillingCycle } from '@/platform/cloud/subscription/utils/subscriptionTierRank'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import { getCheckoutAttribution } from '@/platform/telemetry/utils/checkoutAttribution'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
@@ -280,19 +280,6 @@ const getCheckoutTier = (
|
||||
billingCycle: BillingCycle
|
||||
): CheckoutTier => (billingCycle === 'yearly' ? `${tierKey}-yearly` : tierKey)
|
||||
|
||||
const getCheckoutAttributionForCloud =
|
||||
async (): Promise<CheckoutAttributionMetadata> => {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (__DISTRIBUTION__ !== 'cloud') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { getCheckoutAttribution } =
|
||||
await import('@/platform/telemetry/utils/checkoutAttribution')
|
||||
|
||||
return getCheckoutAttribution()
|
||||
}
|
||||
|
||||
interface BillingCycleOption {
|
||||
label: string
|
||||
value: BillingCycle
|
||||
@@ -428,7 +415,7 @@ const handleSubscribe = wrapWithErrorHandlingAsync(
|
||||
|
||||
try {
|
||||
if (isActiveSubscription.value) {
|
||||
const checkoutAttribution = await getCheckoutAttributionForCloud()
|
||||
const checkoutAttribution = getCheckoutAttribution()
|
||||
if (userId.value) {
|
||||
telemetry?.trackBeginCheckout({
|
||||
user_id: userId.value,
|
||||
|
||||
@@ -9,7 +9,6 @@ const {
|
||||
mockAccessBillingPortal,
|
||||
mockShowSubscriptionRequiredDialog,
|
||||
mockGetAuthHeader,
|
||||
mockGetCheckoutAttribution,
|
||||
mockTelemetry,
|
||||
mockUserId,
|
||||
mockIsCloud
|
||||
@@ -22,10 +21,6 @@ const {
|
||||
mockGetAuthHeader: vi.fn(() =>
|
||||
Promise.resolve({ Authorization: 'Bearer test-token' })
|
||||
),
|
||||
mockGetCheckoutAttribution: vi.fn(() => ({
|
||||
im_ref: 'impact-click-001',
|
||||
utm_source: 'impact'
|
||||
})),
|
||||
mockTelemetry: {
|
||||
trackSubscription: vi.fn(),
|
||||
trackMonthlySubscriptionCancelled: vi.fn()
|
||||
@@ -34,13 +29,6 @@ const {
|
||||
}))
|
||||
|
||||
let scope: ReturnType<typeof effectScope> | undefined
|
||||
type Distribution = 'desktop' | 'localhost' | 'cloud'
|
||||
|
||||
const setDistribution = (distribution: Distribution) => {
|
||||
;(
|
||||
globalThis as typeof globalThis & { __DISTRIBUTION__: Distribution }
|
||||
).__DISTRIBUTION__ = distribution
|
||||
}
|
||||
|
||||
function useSubscriptionWithScope() {
|
||||
if (!scope) {
|
||||
@@ -96,10 +84,6 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry/utils/checkoutAttribution', () => ({
|
||||
getCheckoutAttribution: mockGetCheckoutAttribution
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: vi.fn(() => ({
|
||||
showSubscriptionRequiredDialog: mockShowSubscriptionRequiredDialog
|
||||
@@ -108,7 +92,7 @@ vi.mock('@/services/dialogService', () => ({
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
getFirebaseAuthHeader: mockGetAuthHeader,
|
||||
get userId() {
|
||||
return mockUserId.value
|
||||
}
|
||||
@@ -123,13 +107,11 @@ describe('useSubscription', () => {
|
||||
afterEach(() => {
|
||||
scope?.stop()
|
||||
scope = undefined
|
||||
setDistribution('localhost')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
scope?.stop()
|
||||
scope = effectScope()
|
||||
setDistribution('cloud')
|
||||
|
||||
vi.clearAllMocks()
|
||||
mockIsLoggedIn.value = false
|
||||
@@ -302,10 +284,6 @@ describe('useSubscription', () => {
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer test-token',
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
body: JSON.stringify({
|
||||
im_ref: 'impact-click-001',
|
||||
utm_source: 'impact'
|
||||
})
|
||||
})
|
||||
)
|
||||
@@ -363,27 +341,6 @@ describe('useSubscription', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('non-cloud environments', () => {
|
||||
it('should not fetch subscription status when not on cloud', async () => {
|
||||
mockIsCloud.value = false
|
||||
mockIsLoggedIn.value = true
|
||||
|
||||
useSubscriptionWithScope()
|
||||
|
||||
await vi.dynamicImportSettled()
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should report isActiveSubscription as true when not on cloud', () => {
|
||||
mockIsCloud.value = false
|
||||
|
||||
const { isActiveSubscription } = useSubscriptionWithScope()
|
||||
|
||||
expect(isActiveSubscription.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('action handlers', () => {
|
||||
it('should open usage history URL', () => {
|
||||
const windowOpenSpy = vi
|
||||
|
||||
@@ -8,7 +8,6 @@ import { getComfyApiBaseUrl, getComfyPlatformBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
@@ -40,7 +39,7 @@ function useSubscriptionInternal() {
|
||||
const { showSubscriptionRequiredDialog } = useDialogService()
|
||||
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { getAuthHeader } = firebaseAuthStore
|
||||
const { getFirebaseAuthHeader } = firebaseAuthStore
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -99,18 +98,6 @@ function useSubscriptionInternal() {
|
||||
return `${getComfyApiBaseUrl()}${path}`
|
||||
}
|
||||
|
||||
const getCheckoutAttributionForCloud =
|
||||
async (): Promise<CheckoutAttributionMetadata> => {
|
||||
if (__DISTRIBUTION__ !== 'cloud') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { getCheckoutAttribution } =
|
||||
await import('@/platform/telemetry/utils/checkoutAttribution')
|
||||
|
||||
return getCheckoutAttribution()
|
||||
}
|
||||
|
||||
const fetchStatus = wrapWithErrorHandlingAsync(
|
||||
fetchSubscriptionStatus,
|
||||
reportError
|
||||
@@ -184,7 +171,7 @@ function useSubscriptionInternal() {
|
||||
* @returns Subscription status or null if no subscription exists
|
||||
*/
|
||||
async function fetchSubscriptionStatus(): Promise<CloudSubscriptionStatusResponse | null> {
|
||||
const authHeader = await getAuthHeader()
|
||||
const authHeader = await getFirebaseAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
@@ -217,7 +204,7 @@ function useSubscriptionInternal() {
|
||||
watch(
|
||||
() => isLoggedIn.value,
|
||||
async (loggedIn) => {
|
||||
if (loggedIn && isCloud) {
|
||||
if (loggedIn) {
|
||||
try {
|
||||
await fetchSubscriptionStatus()
|
||||
} catch (error) {
|
||||
@@ -238,13 +225,12 @@ function useSubscriptionInternal() {
|
||||
|
||||
const initiateSubscriptionCheckout =
|
||||
async (): Promise<CloudSubscriptionCheckoutResponse> => {
|
||||
const authHeader = await getAuthHeader()
|
||||
const authHeader = await getFirebaseAuthHeader()
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.userNotAuthenticated')
|
||||
)
|
||||
}
|
||||
const checkoutAttribution = await getCheckoutAttributionForCloud()
|
||||
|
||||
const response = await fetch(
|
||||
buildApiUrl('/customers/cloud-subscription-checkout'),
|
||||
@@ -253,8 +239,7 @@ function useSubscriptionInternal() {
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(checkoutAttribution)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ const {
|
||||
ga_client_id: 'ga-client-id',
|
||||
ga_session_id: 'ga-session-id',
|
||||
ga_session_number: 'ga-session-number',
|
||||
im_ref: 'impact-click-123',
|
||||
utm_source: 'impact',
|
||||
utm_medium: 'affiliate',
|
||||
utm_campaign: 'spring-launch',
|
||||
gclid: 'gclid-123',
|
||||
gbraid: 'gbraid-456',
|
||||
wbraid: 'wbraid-789'
|
||||
@@ -39,7 +35,7 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() =>
|
||||
reactive({
|
||||
getAuthHeader: mockGetAuthHeader,
|
||||
getFirebaseAuthHeader: mockGetAuthHeader,
|
||||
userId: computed(() => mockUserId.value)
|
||||
})
|
||||
),
|
||||
@@ -58,14 +54,6 @@ vi.mock('@/platform/telemetry/utils/checkoutAttribution', () => ({
|
||||
|
||||
global.fetch = vi.fn()
|
||||
|
||||
type Distribution = 'desktop' | 'localhost' | 'cloud'
|
||||
|
||||
const setDistribution = (distribution: Distribution) => {
|
||||
;(
|
||||
globalThis as typeof globalThis & { __DISTRIBUTION__: Distribution }
|
||||
).__DISTRIBUTION__ = distribution
|
||||
}
|
||||
|
||||
function createDeferred<T>() {
|
||||
let resolve: (value: T) => void = () => {}
|
||||
const promise = new Promise<T>((res) => {
|
||||
@@ -77,7 +65,6 @@ function createDeferred<T>() {
|
||||
|
||||
describe('performSubscriptionCheckout', () => {
|
||||
beforeEach(() => {
|
||||
setDistribution('cloud')
|
||||
vi.clearAllMocks()
|
||||
mockIsCloud.value = true
|
||||
mockUserId.value = 'user-123'
|
||||
@@ -85,7 +72,6 @@ describe('performSubscriptionCheckout', () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
setDistribution('localhost')
|
||||
})
|
||||
|
||||
it('tracks begin_checkout with user id and tier metadata', async () => {
|
||||
@@ -107,10 +93,6 @@ describe('performSubscriptionCheckout', () => {
|
||||
ga_client_id: 'ga-client-id',
|
||||
ga_session_id: 'ga-session-id',
|
||||
ga_session_number: 'ga-session-number',
|
||||
im_ref: 'impact-click-123',
|
||||
utm_source: 'impact',
|
||||
utm_medium: 'affiliate',
|
||||
utm_campaign: 'spring-launch',
|
||||
gclid: 'gclid-123',
|
||||
gbraid: 'gbraid-456',
|
||||
wbraid: 'wbraid-789'
|
||||
@@ -125,10 +107,6 @@ describe('performSubscriptionCheckout', () => {
|
||||
ga_client_id: 'ga-client-id',
|
||||
ga_session_id: 'ga-session-id',
|
||||
ga_session_number: 'ga-session-number',
|
||||
im_ref: 'impact-click-123',
|
||||
utm_source: 'impact',
|
||||
utm_medium: 'affiliate',
|
||||
utm_campaign: 'spring-launch',
|
||||
gclid: 'gclid-123',
|
||||
gbraid: 'gbraid-456',
|
||||
wbraid: 'wbraid-789'
|
||||
@@ -138,41 +116,6 @@ describe('performSubscriptionCheckout', () => {
|
||||
expect(openSpy).toHaveBeenCalledWith(checkoutUrl, '_blank')
|
||||
})
|
||||
|
||||
it('continues checkout when attribution collection fails', async () => {
|
||||
const checkoutUrl = 'https://checkout.stripe.com/test'
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
mockGetCheckoutAttribution.mockRejectedValueOnce(
|
||||
new Error('Attribution failed')
|
||||
)
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ checkout_url: checkoutUrl })
|
||||
} as Response)
|
||||
|
||||
await performSubscriptionCheckout('pro', 'monthly', true)
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[SubscriptionCheckout] Failed to collect checkout attribution',
|
||||
expect.any(Error)
|
||||
)
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/customers/cloud-subscription-checkout/pro'),
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
)
|
||||
expect(mockTelemetry.trackBeginCheckout).toHaveBeenCalledWith({
|
||||
user_id: 'user-123',
|
||||
tier: 'pro',
|
||||
cycle: 'monthly',
|
||||
checkout_type: 'new'
|
||||
})
|
||||
expect(openSpy).toHaveBeenCalledWith(checkoutUrl, '_blank')
|
||||
})
|
||||
|
||||
it('uses the latest userId when it changes after checkout starts', async () => {
|
||||
const checkoutUrl = 'https://checkout.stripe.com/test'
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
|
||||
@@ -4,11 +4,11 @@ import { getComfyApiBaseUrl } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { getCheckoutAttribution } from '@/platform/telemetry/utils/checkoutAttribution'
|
||||
import {
|
||||
FirebaseAuthStoreError,
|
||||
useFirebaseAuthStore
|
||||
} from '@/stores/firebaseAuthStore'
|
||||
import type { CheckoutAttributionMetadata } from '@/platform/telemetry/types'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import type { BillingCycle } from './subscriptionTierRank'
|
||||
|
||||
@@ -19,18 +19,6 @@ const getCheckoutTier = (
|
||||
billingCycle: BillingCycle
|
||||
): CheckoutTier => (billingCycle === 'yearly' ? `${tierKey}-yearly` : tierKey)
|
||||
|
||||
const getCheckoutAttributionForCloud =
|
||||
async (): Promise<CheckoutAttributionMetadata> => {
|
||||
if (__DISTRIBUTION__ !== 'cloud') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { getCheckoutAttribution } =
|
||||
await import('@/platform/telemetry/utils/checkoutAttribution')
|
||||
|
||||
return getCheckoutAttribution()
|
||||
}
|
||||
|
||||
/**
|
||||
* Core subscription checkout logic shared between PricingTable and
|
||||
* SubscriptionRedirectView. Handles:
|
||||
@@ -54,22 +42,14 @@ export async function performSubscriptionCheckout(
|
||||
const firebaseAuthStore = useFirebaseAuthStore()
|
||||
const { userId } = storeToRefs(firebaseAuthStore)
|
||||
const telemetry = useTelemetry()
|
||||
const authHeader = await firebaseAuthStore.getAuthHeader()
|
||||
const authHeader = await firebaseAuthStore.getFirebaseAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle)
|
||||
let checkoutAttribution: CheckoutAttributionMetadata = {}
|
||||
try {
|
||||
checkoutAttribution = await getCheckoutAttributionForCloud()
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
'[SubscriptionCheckout] Failed to collect checkout attribution',
|
||||
error
|
||||
)
|
||||
}
|
||||
const checkoutAttribution = getCheckoutAttribution()
|
||||
const checkoutPayload = { ...checkoutAttribution }
|
||||
|
||||
const response = await fetch(
|
||||
|
||||
@@ -137,14 +137,14 @@ export function useKeybindingService() {
|
||||
}
|
||||
|
||||
async function persistUserKeybindings() {
|
||||
await settingStore.setMany({
|
||||
'Comfy.Keybinding.NewBindings': Object.values(
|
||||
keybindingStore.getUserKeybindings()
|
||||
),
|
||||
'Comfy.Keybinding.UnsetBindings': Object.values(
|
||||
keybindingStore.getUserUnsetKeybindings()
|
||||
)
|
||||
})
|
||||
await settingStore.set(
|
||||
'Comfy.Keybinding.NewBindings',
|
||||
Object.values(keybindingStore.getUserKeybindings())
|
||||
)
|
||||
await settingStore.set(
|
||||
'Comfy.Keybinding.UnsetBindings',
|
||||
Object.values(keybindingStore.getUserUnsetKeybindings())
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { NodeReplacementResponse } from './types'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { ServerFeatureFlag } from '@/composables/useFeatureFlags'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { fetchNodeReplacements } from './nodeReplacementService'
|
||||
import { useNodeReplacementStore } from './nodeReplacementStore'
|
||||
|
||||
@@ -17,12 +15,6 @@ vi.mock('./nodeReplacementService', () => ({
|
||||
fetchNodeReplacements: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getServerFeature: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
function mockSettingStore(enabled: boolean) {
|
||||
vi.mocked(useSettingStore, { partial: true }).mockReturnValue({
|
||||
get: vi.fn().mockImplementation((key: string) => {
|
||||
@@ -35,17 +27,9 @@ function mockSettingStore(enabled: boolean) {
|
||||
})
|
||||
}
|
||||
|
||||
function createStore(settingEnabled = true, serverFeatureEnabled = true) {
|
||||
function createStore(enabled = true) {
|
||||
setActivePinia(createPinia())
|
||||
mockSettingStore(settingEnabled)
|
||||
vi.mocked(api.getServerFeature).mockImplementation(
|
||||
(flag: string, defaultValue?: unknown) => {
|
||||
if (flag === ServerFeatureFlag.NODE_REPLACEMENTS) {
|
||||
return serverFeatureEnabled
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
)
|
||||
mockSettingStore(enabled)
|
||||
return useNodeReplacementStore()
|
||||
}
|
||||
|
||||
@@ -54,7 +38,7 @@ describe('useNodeReplacementStore', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
store = createStore()
|
||||
store = createStore(true)
|
||||
})
|
||||
|
||||
it('should initialize with empty replacements', () => {
|
||||
@@ -244,7 +228,7 @@ describe('useNodeReplacementStore', () => {
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should not fetch when setting is disabled', async () => {
|
||||
it('should not fetch when feature is disabled', async () => {
|
||||
vi.mocked(fetchNodeReplacements).mockResolvedValue({})
|
||||
store = createStore(false)
|
||||
|
||||
@@ -254,16 +238,6 @@ describe('useNodeReplacementStore', () => {
|
||||
expect(store.isLoaded).toBe(false)
|
||||
})
|
||||
|
||||
it('should not fetch when server feature flag is disabled', async () => {
|
||||
vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements)
|
||||
store = createStore(true, false)
|
||||
|
||||
await store.load()
|
||||
|
||||
expect(fetchNodeReplacements).not.toHaveBeenCalled()
|
||||
expect(store.isLoaded).toBe(false)
|
||||
})
|
||||
|
||||
it('should not re-fetch when called twice', async () => {
|
||||
vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements)
|
||||
store = createStore()
|
||||
|
||||
@@ -3,9 +3,7 @@ import type { NodeReplacement, NodeReplacementResponse } from './types'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { ServerFeatureFlag } from '@/composables/useFeatureFlags'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { fetchNodeReplacements } from './nodeReplacementService'
|
||||
|
||||
export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
||||
@@ -17,9 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
||||
)
|
||||
|
||||
async function load() {
|
||||
if (!isEnabled.value || isLoaded.value) return
|
||||
if (!api.getServerFeature(ServerFeatureFlag.NODE_REPLACEMENTS, false))
|
||||
return
|
||||
if (isLoaded.value || !isEnabled.value) return
|
||||
|
||||
try {
|
||||
replacements.value = await fetchNodeReplacements()
|
||||
@@ -42,8 +38,8 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
||||
return {
|
||||
replacements,
|
||||
isLoaded,
|
||||
isEnabled,
|
||||
load,
|
||||
isEnabled,
|
||||
getReplacementFor,
|
||||
hasReplacement
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
interface InputMapOldId {
|
||||
new_id: string
|
||||
interface InputAssignOldId {
|
||||
assign_type: 'old_id'
|
||||
old_id: string
|
||||
}
|
||||
|
||||
interface InputMapSetValue {
|
||||
new_id: string
|
||||
set_value: unknown
|
||||
interface InputAssignSetValue {
|
||||
assign_type: 'set_value'
|
||||
value: unknown
|
||||
}
|
||||
|
||||
type InputMap = InputMapOldId | InputMapSetValue
|
||||
interface InputMap {
|
||||
new_id: string
|
||||
assign: InputAssignOldId | InputAssignSetValue
|
||||
}
|
||||
|
||||
interface OutputMap {
|
||||
new_idx: number
|
||||
|
||||
@@ -1,654 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeReplacement } from './types'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
vi.mock('@/lib/litegraph/src/litegraph', () => ({
|
||||
LiteGraph: {
|
||||
createNode: vi.fn(),
|
||||
registered_node_types: {}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: { rootGraph: null },
|
||||
sanitizeNodeName: (name: string) => name.replace(/[&<>"'`=]/g, '')
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/graphTraversalUtil', () => ({
|
||||
collectAllNodes: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => ({
|
||||
add: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
activeWorkflow: {
|
||||
changeTracker: {
|
||||
beforeChange: vi.fn(),
|
||||
afterChange: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, params?: Record<string, unknown>) =>
|
||||
params ? `${key}:${JSON.stringify(params)}` : key
|
||||
}))
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
import { useNodeReplacement } from './useNodeReplacement'
|
||||
|
||||
function createMockLink(
|
||||
id: number,
|
||||
originId: number,
|
||||
originSlot: number,
|
||||
targetId: number,
|
||||
targetSlot: number
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
origin_id: originId,
|
||||
origin_slot: originSlot,
|
||||
target_id: targetId,
|
||||
target_slot: targetSlot,
|
||||
type: 'IMAGE'
|
||||
}
|
||||
}
|
||||
|
||||
function createMockGraph(
|
||||
nodes: LGraphNode[],
|
||||
links: ReturnType<typeof createMockLink>[] = []
|
||||
): LGraph {
|
||||
const linksMap = new Map(links.map((l) => [l.id, l]))
|
||||
return {
|
||||
_nodes: nodes,
|
||||
_nodes_by_id: Object.fromEntries(nodes.map((n) => [n.id, n])),
|
||||
links: linksMap,
|
||||
updateExecutionOrder: vi.fn(),
|
||||
setDirtyCanvas: vi.fn()
|
||||
} as unknown as LGraph
|
||||
}
|
||||
|
||||
function createPlaceholderNode(
|
||||
id: number,
|
||||
type: string,
|
||||
inputs: { name: string; link: number | null }[] = [],
|
||||
outputs: { name: string; links: number[] | null }[] = [],
|
||||
graph?: LGraph
|
||||
): LGraphNode {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
pos: [100, 200],
|
||||
size: [200, 100],
|
||||
order: 0,
|
||||
mode: 0,
|
||||
flags: {},
|
||||
has_errors: true,
|
||||
last_serialization: {
|
||||
id,
|
||||
type,
|
||||
pos: [100, 200],
|
||||
size: [200, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: inputs.map((i) => ({ ...i, type: 'IMAGE' })),
|
||||
outputs: outputs.map((o) => ({ ...o, type: 'IMAGE' })),
|
||||
widgets_values: []
|
||||
},
|
||||
inputs: inputs.map((i) => ({ ...i, type: 'IMAGE' })),
|
||||
outputs: outputs.map((o) => ({ ...o, type: 'IMAGE' })),
|
||||
graph: graph ?? null,
|
||||
serialize: vi.fn(() => ({
|
||||
id,
|
||||
type,
|
||||
pos: [100, 200],
|
||||
size: [200, 100],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: inputs.map((i) => ({ ...i, type: 'IMAGE' })),
|
||||
outputs: outputs.map((o) => ({ ...o, type: 'IMAGE' })),
|
||||
widgets_values: []
|
||||
}))
|
||||
} as unknown as LGraphNode
|
||||
}
|
||||
|
||||
function createNewNode(
|
||||
inputs: { name: string; link: number | null }[] = [],
|
||||
outputs: { name: string; links: number[] | null }[] = [],
|
||||
widgets: { name: string; value: unknown }[] = []
|
||||
): LGraphNode {
|
||||
return {
|
||||
id: 0,
|
||||
type: '',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
order: 0,
|
||||
mode: 0,
|
||||
flags: {},
|
||||
has_errors: false,
|
||||
inputs: inputs.map((i) => ({ ...i, type: 'IMAGE' })),
|
||||
outputs: outputs.map((o) => ({ ...o, type: 'IMAGE' })),
|
||||
widgets: widgets.map((w) => ({ ...w, type: 'combo', options: {} })),
|
||||
configure: vi.fn(),
|
||||
serialize: vi.fn()
|
||||
} as unknown as LGraphNode
|
||||
}
|
||||
|
||||
function makeMissingNodeType(
|
||||
type: string,
|
||||
replacement: NodeReplacement
|
||||
): MissingNodeType {
|
||||
return {
|
||||
type,
|
||||
isReplaceable: true,
|
||||
replacement
|
||||
}
|
||||
}
|
||||
|
||||
describe('useNodeReplacement', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('replaceNodesInPlace', () => {
|
||||
it('should return empty array when no placeholders exist', () => {
|
||||
const graph = createMockGraph([])
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
vi.mocked(collectAllNodes).mockReturnValue([])
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const result = replaceNodesInPlace([])
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should use default mapping when no explicit mapping exists', () => {
|
||||
const placeholder = createPlaceholderNode(1, 'Load3DAnimation')
|
||||
const graph = createMockGraph([placeholder])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode()
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const result = replaceNodesInPlace([
|
||||
makeMissingNodeType('Load3DAnimation', {
|
||||
new_node_id: 'Load3D',
|
||||
old_node_id: 'Load3DAnimation',
|
||||
old_widget_ids: null,
|
||||
input_mapping: null,
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toEqual(['Load3DAnimation'])
|
||||
expect(newNode.configure).not.toHaveBeenCalled()
|
||||
expect(newNode.id).toBe(1)
|
||||
expect(newNode.has_errors).toBe(false)
|
||||
})
|
||||
|
||||
it('should transfer input connections using input_mapping', () => {
|
||||
const link = createMockLink(10, 5, 0, 1, 0)
|
||||
const placeholder = createPlaceholderNode(
|
||||
1,
|
||||
'T2IAdapterLoader',
|
||||
[{ name: 't2i_adapter_name', link: 10 }],
|
||||
[]
|
||||
)
|
||||
const graph = createMockGraph([placeholder], [link])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[{ name: 'control_net_name', link: null }],
|
||||
[]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const result = replaceNodesInPlace([
|
||||
makeMissingNodeType('T2IAdapterLoader', {
|
||||
new_node_id: 'ControlNetLoader',
|
||||
old_node_id: 'T2IAdapterLoader',
|
||||
old_widget_ids: null,
|
||||
input_mapping: [
|
||||
{ new_id: 'control_net_name', old_id: 't2i_adapter_name' }
|
||||
],
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toEqual(['T2IAdapterLoader'])
|
||||
// Link should be updated to point at new node's input
|
||||
expect(link.target_id).toBe(1)
|
||||
expect(link.target_slot).toBe(0)
|
||||
expect(newNode.inputs[0].link).toBe(10)
|
||||
})
|
||||
|
||||
it('should transfer output connections using output_mapping', () => {
|
||||
const link = createMockLink(20, 1, 0, 5, 0)
|
||||
const placeholder = createPlaceholderNode(
|
||||
1,
|
||||
'ResizeImagesByLongerEdge',
|
||||
[],
|
||||
[{ name: 'IMAGE', links: [20] }]
|
||||
)
|
||||
const graph = createMockGraph([placeholder], [link])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[{ name: 'image', link: null }],
|
||||
[{ name: 'IMAGE', links: null }]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('ResizeImagesByLongerEdge', {
|
||||
new_node_id: 'ImageScaleToMaxDimension',
|
||||
old_node_id: 'ResizeImagesByLongerEdge',
|
||||
old_widget_ids: ['longer_edge'],
|
||||
input_mapping: [
|
||||
{ new_id: 'image', old_id: 'images' },
|
||||
{ new_id: 'largest_size', old_id: 'longer_edge' },
|
||||
{ new_id: 'upscale_method', set_value: 'lanczos' }
|
||||
],
|
||||
output_mapping: [{ new_idx: 0, old_idx: 0 }]
|
||||
})
|
||||
])
|
||||
|
||||
// Output link should be remapped
|
||||
expect(link.origin_id).toBe(1)
|
||||
expect(link.origin_slot).toBe(0)
|
||||
expect(newNode.outputs[0].links).toEqual([20])
|
||||
})
|
||||
|
||||
it('should apply set_value to widget', () => {
|
||||
const placeholder = createPlaceholderNode(1, 'ImageScaleBy')
|
||||
const graph = createMockGraph([placeholder])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[{ name: 'input', link: null }],
|
||||
[],
|
||||
[
|
||||
{ name: 'resize_type', value: '' },
|
||||
{ name: 'scale_method', value: '' }
|
||||
]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('ImageScaleBy', {
|
||||
new_node_id: 'ResizeImageMaskNode',
|
||||
old_node_id: 'ImageScaleBy',
|
||||
old_widget_ids: ['upscale_method', 'scale_by'],
|
||||
input_mapping: [
|
||||
{ new_id: 'input', old_id: 'image' },
|
||||
{ new_id: 'resize_type', set_value: 'scale by multiplier' },
|
||||
{ new_id: 'resize_type.multiplier', old_id: 'scale_by' },
|
||||
{ new_id: 'scale_method', old_id: 'upscale_method' }
|
||||
],
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
// set_value should be applied to the widget
|
||||
expect(newNode.widgets![0].value).toBe('scale by multiplier')
|
||||
})
|
||||
|
||||
it('should transfer widget values using old_widget_ids', () => {
|
||||
const placeholder = createPlaceholderNode(1, 'ResizeImagesByLongerEdge')
|
||||
// Set widget values in serialized data
|
||||
placeholder.last_serialization!.widgets_values = [512]
|
||||
|
||||
const graph = createMockGraph([placeholder])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[
|
||||
{ name: 'image', link: null },
|
||||
{ name: 'largest_size', link: null }
|
||||
],
|
||||
[{ name: 'IMAGE', links: null }],
|
||||
[{ name: 'largest_size', value: 0 }]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('ResizeImagesByLongerEdge', {
|
||||
new_node_id: 'ImageScaleToMaxDimension',
|
||||
old_node_id: 'ResizeImagesByLongerEdge',
|
||||
old_widget_ids: ['longer_edge'],
|
||||
input_mapping: [
|
||||
{ new_id: 'image', old_id: 'images' },
|
||||
{ new_id: 'largest_size', old_id: 'longer_edge' },
|
||||
{ new_id: 'upscale_method', set_value: 'lanczos' }
|
||||
],
|
||||
output_mapping: [{ new_idx: 0, old_idx: 0 }]
|
||||
})
|
||||
])
|
||||
|
||||
// Widget value should be transferred: old "longer_edge" (idx 0, value 512) → new "largest_size"
|
||||
expect(newNode.widgets![0].value).toBe(512)
|
||||
})
|
||||
|
||||
it('should skip replacement when new node type is not registered', () => {
|
||||
const placeholder = createPlaceholderNode(1, 'UnknownNode')
|
||||
const graph = createMockGraph([placeholder])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(null)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const result = replaceNodesInPlace([
|
||||
makeMissingNodeType('UnknownNode', {
|
||||
new_node_id: 'NonExistentNode',
|
||||
old_node_id: 'UnknownNode',
|
||||
old_widget_ids: null,
|
||||
input_mapping: null,
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should replace multiple different node types at once', () => {
|
||||
const placeholder1 = createPlaceholderNode(1, 'Load3DAnimation')
|
||||
const placeholder2 = createPlaceholderNode(
|
||||
2,
|
||||
'ConditioningAverage',
|
||||
[],
|
||||
[]
|
||||
)
|
||||
// sanitizeNodeName strips & from type names (HTML entity chars)
|
||||
placeholder2.type = 'ConditioningAverage'
|
||||
|
||||
const graph = createMockGraph([placeholder1, placeholder2])
|
||||
placeholder1.graph = graph
|
||||
placeholder2.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder1, placeholder2])
|
||||
|
||||
const newNode1 = createNewNode()
|
||||
const newNode2 = createNewNode()
|
||||
vi.mocked(LiteGraph.createNode)
|
||||
.mockReturnValueOnce(newNode1)
|
||||
.mockReturnValueOnce(newNode2)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const result = replaceNodesInPlace([
|
||||
makeMissingNodeType('Load3DAnimation', {
|
||||
new_node_id: 'Load3D',
|
||||
old_node_id: 'Load3DAnimation',
|
||||
old_widget_ids: null,
|
||||
input_mapping: null,
|
||||
output_mapping: null
|
||||
}),
|
||||
makeMissingNodeType('ConditioningAverage&', {
|
||||
new_node_id: 'ConditioningAverage',
|
||||
old_node_id: 'ConditioningAverage&',
|
||||
old_widget_ids: null,
|
||||
input_mapping: null,
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result).toContain('Load3DAnimation')
|
||||
expect(result).toContain('ConditioningAverage&')
|
||||
})
|
||||
|
||||
it('should copy position and identity for mapped replacements', () => {
|
||||
const link = createMockLink(10, 5, 0, 1, 0)
|
||||
const placeholder = createPlaceholderNode(
|
||||
42,
|
||||
'T2IAdapterLoader',
|
||||
[{ name: 't2i_adapter_name', link: 10 }],
|
||||
[]
|
||||
)
|
||||
placeholder.pos = [300, 400]
|
||||
placeholder.size = [250, 150]
|
||||
|
||||
const graph = createMockGraph([placeholder], [link])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[{ name: 'control_net_name', link: null }],
|
||||
[]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('T2IAdapterLoader', {
|
||||
new_node_id: 'ControlNetLoader',
|
||||
old_node_id: 'T2IAdapterLoader',
|
||||
old_widget_ids: null,
|
||||
input_mapping: [
|
||||
{ new_id: 'control_net_name', old_id: 't2i_adapter_name' }
|
||||
],
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
expect(newNode.id).toBe(42)
|
||||
expect(newNode.pos).toEqual([300, 400])
|
||||
expect(newNode.size).toEqual([250, 150])
|
||||
expect(graph._nodes[0]).toBe(newNode)
|
||||
})
|
||||
|
||||
it('should transfer all widget values for ImageScaleBy with real workflow data', () => {
|
||||
const placeholder = createPlaceholderNode(
|
||||
12,
|
||||
'ImageScaleBy',
|
||||
[{ name: 'image', link: 2 }],
|
||||
[{ name: 'IMAGE', links: [3, 4] }]
|
||||
)
|
||||
// Real workflow data: widgets_values: ["lanczos", 2.0]
|
||||
placeholder.last_serialization!.widgets_values = ['lanczos', 2.0]
|
||||
|
||||
const graph = createMockGraph([placeholder])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[{ name: 'input', link: null }],
|
||||
[],
|
||||
[
|
||||
{ name: 'resize_type', value: '' },
|
||||
{ name: 'scale_method', value: '' }
|
||||
]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('ImageScaleBy', {
|
||||
new_node_id: 'ResizeImageMaskNode',
|
||||
old_node_id: 'ImageScaleBy',
|
||||
old_widget_ids: ['upscale_method', 'scale_by'],
|
||||
input_mapping: [
|
||||
{ new_id: 'input', old_id: 'image' },
|
||||
{ new_id: 'resize_type', set_value: 'scale by multiplier' },
|
||||
{ new_id: 'resize_type.multiplier', old_id: 'scale_by' },
|
||||
{ new_id: 'scale_method', old_id: 'upscale_method' }
|
||||
],
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
// set_value should be applied
|
||||
expect(newNode.widgets![0].value).toBe('scale by multiplier')
|
||||
// upscale_method (idx 0, value "lanczos") → scale_method widget
|
||||
expect(newNode.widgets![1].value).toBe('lanczos')
|
||||
})
|
||||
|
||||
it('should transfer widget value for ResizeImagesByLongerEdge with real workflow data', () => {
|
||||
const link = createMockLink(1, 5, 0, 8, 0)
|
||||
const placeholder = createPlaceholderNode(
|
||||
8,
|
||||
'ResizeImagesByLongerEdge',
|
||||
[{ name: 'images', link: 1 }],
|
||||
[{ name: 'IMAGE', links: [2] }]
|
||||
)
|
||||
// Real workflow data: widgets_values: [1024]
|
||||
placeholder.last_serialization!.widgets_values = [1024]
|
||||
|
||||
const graph = createMockGraph([placeholder], [link])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[
|
||||
{ name: 'image', link: null },
|
||||
{ name: 'largest_size', link: null }
|
||||
],
|
||||
[{ name: 'IMAGE', links: null }],
|
||||
[
|
||||
{ name: 'largest_size', value: 0 },
|
||||
{ name: 'upscale_method', value: '' }
|
||||
]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('ResizeImagesByLongerEdge', {
|
||||
new_node_id: 'ImageScaleToMaxDimension',
|
||||
old_node_id: 'ResizeImagesByLongerEdge',
|
||||
old_widget_ids: ['longer_edge'],
|
||||
input_mapping: [
|
||||
{ new_id: 'image', old_id: 'images' },
|
||||
{ new_id: 'largest_size', old_id: 'longer_edge' },
|
||||
{ new_id: 'upscale_method', set_value: 'lanczos' }
|
||||
],
|
||||
output_mapping: [{ new_idx: 0, old_idx: 0 }]
|
||||
})
|
||||
])
|
||||
|
||||
// longer_edge (idx 0, value 1024) → largest_size widget
|
||||
expect(newNode.widgets![0].value).toBe(1024)
|
||||
// set_value "lanczos" → upscale_method widget
|
||||
expect(newNode.widgets![1].value).toBe('lanczos')
|
||||
})
|
||||
|
||||
it('should transfer ConditioningAverage widget value with real workflow data', () => {
|
||||
const link = createMockLink(4, 7, 0, 13, 0)
|
||||
// sanitizeNodeName doesn't strip spaces, so placeholder keeps trailing space
|
||||
const placeholder = createPlaceholderNode(
|
||||
13,
|
||||
'ConditioningAverage ',
|
||||
[
|
||||
{ name: 'conditioning_to', link: 4 },
|
||||
{ name: 'conditioning_from', link: null }
|
||||
],
|
||||
[{ name: 'CONDITIONING', links: [6] }]
|
||||
)
|
||||
placeholder.last_serialization!.widgets_values = [0.75]
|
||||
|
||||
const graph = createMockGraph([placeholder], [link])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode(
|
||||
[
|
||||
{ name: 'conditioning_to', link: null },
|
||||
{ name: 'conditioning_from', link: null }
|
||||
],
|
||||
[{ name: 'CONDITIONING', links: null }],
|
||||
[{ name: 'conditioning_average', value: 0 }]
|
||||
)
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
replaceNodesInPlace([
|
||||
makeMissingNodeType('ConditioningAverage ', {
|
||||
new_node_id: 'ConditioningAverage',
|
||||
old_node_id: 'ConditioningAverage ',
|
||||
old_widget_ids: null,
|
||||
input_mapping: null,
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
// Default mapping transfers connections and widget values by name
|
||||
expect(newNode.id).toBe(13)
|
||||
expect(newNode.inputs[0].link).toBe(4)
|
||||
expect(newNode.outputs[0].links).toEqual([6])
|
||||
expect(newNode.widgets![0].value).toBe(0.75)
|
||||
})
|
||||
|
||||
it('should skip dot-notation input connections but still transfer widget values', () => {
|
||||
const placeholder = createPlaceholderNode(1, 'ImageBatch')
|
||||
const graph = createMockGraph([placeholder])
|
||||
placeholder.graph = graph
|
||||
Object.assign(app, { rootGraph: graph })
|
||||
|
||||
vi.mocked(collectAllNodes).mockReturnValue([placeholder])
|
||||
|
||||
const newNode = createNewNode([], [])
|
||||
vi.mocked(LiteGraph.createNode).mockReturnValue(newNode)
|
||||
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const result = replaceNodesInPlace([
|
||||
makeMissingNodeType('ImageBatch', {
|
||||
new_node_id: 'BatchImagesNode',
|
||||
old_node_id: 'ImageBatch',
|
||||
old_widget_ids: null,
|
||||
input_mapping: [
|
||||
{ new_id: 'images.image0', old_id: 'image1' },
|
||||
{ new_id: 'images.image1', old_id: 'image2' }
|
||||
],
|
||||
output_mapping: null
|
||||
})
|
||||
])
|
||||
|
||||
// Should still succeed (dot-notation skipped gracefully)
|
||||
expect(result).toEqual(['ImageBatch'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,292 +0,0 @@
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
import { t } from '@/i18n'
|
||||
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app, sanitizeNodeName } from '@/scripts/app'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { collectAllNodes } from '@/utils/graphTraversalUtil'
|
||||
|
||||
/** Compares sanitized type strings to match placeholder → missing node type. */
|
||||
function findMatchingType(
|
||||
node: LGraphNode,
|
||||
selectedTypes: MissingNodeType[]
|
||||
): Extract<MissingNodeType, { type: string }> | undefined {
|
||||
const nodeType = node.type
|
||||
for (const selected of selectedTypes) {
|
||||
if (typeof selected !== 'object' || !selected.isReplaceable) continue
|
||||
if (sanitizeNodeName(selected.type) === nodeType) return selected
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function transferInputConnection(
|
||||
oldNode: LGraphNode,
|
||||
oldInputName: string,
|
||||
newNode: LGraphNode,
|
||||
newInputName: string,
|
||||
graph: LGraph
|
||||
): void {
|
||||
const oldSlotIdx = oldNode.inputs?.findIndex((i) => i.name === oldInputName)
|
||||
const newSlotIdx = newNode.inputs?.findIndex((i) => i.name === newInputName)
|
||||
if (oldSlotIdx == null || oldSlotIdx === -1) return
|
||||
if (newSlotIdx == null || newSlotIdx === -1) return
|
||||
|
||||
const linkId = oldNode.inputs[oldSlotIdx].link
|
||||
if (linkId == null) return
|
||||
|
||||
const link = graph.links.get(linkId)
|
||||
if (!link) return
|
||||
|
||||
link.target_id = newNode.id
|
||||
link.target_slot = newSlotIdx
|
||||
newNode.inputs[newSlotIdx].link = linkId
|
||||
oldNode.inputs[oldSlotIdx].link = null
|
||||
}
|
||||
|
||||
function transferOutputConnections(
|
||||
oldNode: LGraphNode,
|
||||
oldOutputIdx: number,
|
||||
newNode: LGraphNode,
|
||||
newOutputIdx: number,
|
||||
graph: LGraph
|
||||
): void {
|
||||
const oldLinks = oldNode.outputs?.[oldOutputIdx]?.links
|
||||
if (!oldLinks?.length) return
|
||||
if (!newNode.outputs?.[newOutputIdx]) return
|
||||
|
||||
for (const linkId of oldLinks) {
|
||||
const link = graph.links.get(linkId)
|
||||
if (!link) continue
|
||||
link.origin_id = newNode.id
|
||||
link.origin_slot = newOutputIdx
|
||||
}
|
||||
newNode.outputs[newOutputIdx].links = [...oldLinks]
|
||||
oldNode.outputs[oldOutputIdx].links = []
|
||||
}
|
||||
|
||||
/** Uses old_widget_ids as name→index lookup into widgets_values. */
|
||||
function transferWidgetValue(
|
||||
serialized: ISerialisedNode,
|
||||
oldWidgetIds: string[] | null,
|
||||
oldInputName: string,
|
||||
newNode: LGraphNode,
|
||||
newInputName: string
|
||||
): void {
|
||||
if (!oldWidgetIds || !serialized.widgets_values) return
|
||||
|
||||
const oldWidgetIdx = oldWidgetIds.indexOf(oldInputName)
|
||||
if (oldWidgetIdx === -1) return
|
||||
|
||||
const oldValue = serialized.widgets_values[oldWidgetIdx]
|
||||
if (oldValue === undefined) return
|
||||
|
||||
const newWidget = newNode.widgets?.find((w) => w.name === newInputName)
|
||||
if (newWidget) {
|
||||
newWidget.value = oldValue
|
||||
newWidget.callback?.(oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
function applySetValue(
|
||||
newNode: LGraphNode,
|
||||
inputName: string,
|
||||
value: unknown
|
||||
): void {
|
||||
const widget = newNode.widgets?.find((w) => w.name === inputName)
|
||||
if (widget) {
|
||||
widget.value = value as TWidgetValue
|
||||
widget.callback?.(widget.value)
|
||||
}
|
||||
}
|
||||
|
||||
function isDotNotation(id: string): boolean {
|
||||
return id.includes('.')
|
||||
}
|
||||
|
||||
/** Auto-generates identity mapping by name for same-structure replacements without backend mapping. */
|
||||
function generateDefaultMapping(
|
||||
serialized: ISerialisedNode,
|
||||
newNode: LGraphNode
|
||||
): Pick<
|
||||
NodeReplacement,
|
||||
'input_mapping' | 'output_mapping' | 'old_widget_ids'
|
||||
> {
|
||||
const oldInputNames = new Set(serialized.inputs?.map((i) => i.name) ?? [])
|
||||
|
||||
const inputMapping: { old_id: string; new_id: string }[] = []
|
||||
for (const newInput of newNode.inputs ?? []) {
|
||||
if (oldInputNames.has(newInput.name)) {
|
||||
inputMapping.push({ old_id: newInput.name, new_id: newInput.name })
|
||||
}
|
||||
}
|
||||
|
||||
const oldWidgetIds = (newNode.widgets ?? []).map((w) => w.name)
|
||||
for (const widget of newNode.widgets ?? []) {
|
||||
if (!oldInputNames.has(widget.name)) {
|
||||
inputMapping.push({ old_id: widget.name, new_id: widget.name })
|
||||
}
|
||||
}
|
||||
|
||||
const outputMapping: { old_idx: number; new_idx: number }[] = []
|
||||
for (const [oldIdx, oldOutput] of (serialized.outputs ?? []).entries()) {
|
||||
const newIdx = newNode.outputs?.findIndex((o) => o.name === oldOutput.name)
|
||||
if (newIdx != null && newIdx !== -1) {
|
||||
outputMapping.push({ old_idx: oldIdx, new_idx: newIdx })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
input_mapping: inputMapping.length > 0 ? inputMapping : null,
|
||||
output_mapping: outputMapping.length > 0 ? outputMapping : null,
|
||||
old_widget_ids: oldWidgetIds.length > 0 ? oldWidgetIds : null
|
||||
}
|
||||
}
|
||||
|
||||
function replaceWithMapping(
|
||||
node: LGraphNode,
|
||||
newNode: LGraphNode,
|
||||
replacement: NodeReplacement,
|
||||
nodeGraph: LGraph,
|
||||
idx: number
|
||||
): void {
|
||||
newNode.id = node.id
|
||||
newNode.pos = [...node.pos]
|
||||
newNode.size = [...node.size]
|
||||
newNode.order = node.order
|
||||
newNode.mode = node.mode
|
||||
if (node.flags) newNode.flags = { ...node.flags }
|
||||
|
||||
nodeGraph._nodes[idx] = newNode
|
||||
newNode.graph = nodeGraph
|
||||
nodeGraph._nodes_by_id[newNode.id] = newNode
|
||||
|
||||
const serialized = node.last_serialization ?? node.serialize()
|
||||
|
||||
if (serialized.title != null) newNode.title = serialized.title
|
||||
if (serialized.properties) {
|
||||
newNode.properties = { ...serialized.properties }
|
||||
if ('Node name for S&R' in newNode.properties) {
|
||||
newNode.properties['Node name for S&R'] = replacement.new_node_id
|
||||
}
|
||||
}
|
||||
|
||||
if (replacement.input_mapping) {
|
||||
for (const inputMap of replacement.input_mapping) {
|
||||
if ('old_id' in inputMap) {
|
||||
if (isDotNotation(inputMap.new_id)) continue // Autogrow/DynamicCombo
|
||||
transferInputConnection(
|
||||
node,
|
||||
inputMap.old_id,
|
||||
newNode,
|
||||
inputMap.new_id,
|
||||
nodeGraph
|
||||
)
|
||||
transferWidgetValue(
|
||||
serialized,
|
||||
replacement.old_widget_ids,
|
||||
inputMap.old_id,
|
||||
newNode,
|
||||
inputMap.new_id
|
||||
)
|
||||
} else {
|
||||
if (!isDotNotation(inputMap.new_id)) {
|
||||
applySetValue(newNode, inputMap.new_id, inputMap.set_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (replacement.output_mapping) {
|
||||
for (const outMap of replacement.output_mapping) {
|
||||
transferOutputConnections(
|
||||
node,
|
||||
outMap.old_idx,
|
||||
newNode,
|
||||
outMap.new_idx,
|
||||
nodeGraph
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
newNode.has_errors = false
|
||||
}
|
||||
|
||||
export function useNodeReplacement() {
|
||||
const toastStore = useToastStore()
|
||||
|
||||
function replaceNodesInPlace(selectedTypes: MissingNodeType[]): string[] {
|
||||
const replacedTypes: string[] = []
|
||||
const graph = app.rootGraph
|
||||
|
||||
const changeTracker =
|
||||
useWorkflowStore().activeWorkflow?.changeTracker ?? null
|
||||
changeTracker?.beforeChange()
|
||||
|
||||
try {
|
||||
const placeholders = collectAllNodes(
|
||||
graph,
|
||||
(n) => !!n.has_errors && !!n.last_serialization
|
||||
)
|
||||
|
||||
for (const node of placeholders) {
|
||||
const match = findMatchingType(node, selectedTypes)
|
||||
if (!match?.replacement) continue
|
||||
|
||||
const replacement = match.replacement
|
||||
const nodeGraph = node.graph
|
||||
if (!nodeGraph) continue
|
||||
|
||||
const idx = nodeGraph._nodes.indexOf(node)
|
||||
if (idx === -1) continue
|
||||
|
||||
const newNode = LiteGraph.createNode(replacement.new_node_id)
|
||||
if (!newNode) continue
|
||||
|
||||
const hasMapping =
|
||||
replacement.input_mapping != null ||
|
||||
replacement.output_mapping != null
|
||||
|
||||
const effectiveReplacement = hasMapping
|
||||
? replacement
|
||||
: {
|
||||
...replacement,
|
||||
...generateDefaultMapping(
|
||||
node.last_serialization ?? node.serialize(),
|
||||
newNode
|
||||
)
|
||||
}
|
||||
replaceWithMapping(node, newNode, effectiveReplacement, nodeGraph, idx)
|
||||
|
||||
if (!replacedTypes.includes(match.type)) {
|
||||
replacedTypes.push(match.type)
|
||||
}
|
||||
}
|
||||
|
||||
if (replacedTypes.length > 0) {
|
||||
graph.updateExecutionOrder()
|
||||
graph.setDirtyCanvas(true, true)
|
||||
|
||||
toastStore.add({
|
||||
severity: 'success',
|
||||
summary: t('g.success'),
|
||||
detail: t('nodeReplacement.replacedAllNodes', {
|
||||
count: replacedTypes.length
|
||||
}),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
changeTracker?.afterChange()
|
||||
}
|
||||
|
||||
return replacedTypes
|
||||
}
|
||||
|
||||
return {
|
||||
replaceNodesInPlace
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ import Listbox from 'primevue/listbox'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import TabPanels from 'primevue/tabpanels'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed, nextTick, onBeforeUnmount, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import CurrentUserMessage from '@/components/dialog/content/setting/CurrentUserMessage.vue'
|
||||
@@ -129,7 +129,7 @@ import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
import type { ISettingGroup, SettingParams } from '@/platform/settings/types'
|
||||
import { flattenTree } from '@/utils/treeUtil'
|
||||
|
||||
const { defaultPanel, scrollToSettingId } = defineProps<{
|
||||
const { defaultPanel } = defineProps<{
|
||||
defaultPanel?:
|
||||
| 'about'
|
||||
| 'keybinding'
|
||||
@@ -140,7 +140,6 @@ const { defaultPanel, scrollToSettingId } = defineProps<{
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
| 'secrets'
|
||||
scrollToSettingId?: string
|
||||
}>()
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
@@ -154,7 +153,7 @@ const {
|
||||
settingCategories,
|
||||
groupedMenuTreeNodes,
|
||||
panels
|
||||
} = useSettingUI(defaultPanel, scrollToSettingId)
|
||||
} = useSettingUI(defaultPanel)
|
||||
|
||||
const {
|
||||
searchQuery,
|
||||
@@ -203,31 +202,6 @@ const tabValue = computed<string>(() =>
|
||||
inSearch.value ? 'Search Results' : (activeCategory.value?.label ?? '')
|
||||
)
|
||||
|
||||
// Scroll to and highlight the target setting once the correct tab renders.
|
||||
if (scrollToSettingId) {
|
||||
const stopScrollWatch = watch(
|
||||
tabValue,
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
const el = document.querySelector(
|
||||
`[data-setting-id="${CSS.escape(scrollToSettingId)}"]`
|
||||
)
|
||||
if (!el) return
|
||||
stopScrollWatch()
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
el.classList.add('setting-highlight')
|
||||
el.addEventListener(
|
||||
'animationend',
|
||||
() => el.classList.remove('setting-highlight'),
|
||||
{ once: true }
|
||||
)
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
onBeforeUnmount(stopScrollWatch)
|
||||
}
|
||||
|
||||
// Don't allow null category to be set outside of search.
|
||||
// In search mode, the active category can be null to show all search results.
|
||||
watch(activeCategory, (_, oldValue) => {
|
||||
@@ -244,26 +218,6 @@ watch(activeCategory, (_, oldValue) => {
|
||||
.settings-tab-panels {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.setting-highlight {
|
||||
animation: setting-highlight-pulse 1.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes setting-highlight-pulse {
|
||||
0%,
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
30% {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--p-primary-color) 15%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<div
|
||||
v-for="setting in group.settings.filter((s) => !s.deprecated)"
|
||||
:key="setting.id"
|
||||
:data-setting-id="setting.id"
|
||||
class="setting-item mb-4"
|
||||
>
|
||||
<SettingItem :setting="setting" />
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
getSettingInfo,
|
||||
useSettingStore
|
||||
} from '@/platform/settings/settingStore'
|
||||
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
|
||||
import { useSettingUI } from './useSettingUI'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (_: string, fallback: string) => fallback })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
useCurrentUser: () => ({ isLoggedIn: ref(false) })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({ isActiveSubscription: ref(false) })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||
useFeatureFlags: () => ({
|
||||
flags: { teamWorkspacesEnabled: false, userSecretsEnabled: false }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useVueFeatureFlags', () => ({
|
||||
useVueFeatureFlags: () => ({ shouldRenderVueNodes: ref(false) })
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: false,
|
||||
isDesktop: false
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(),
|
||||
getSettingInfo: vi.fn()
|
||||
}))
|
||||
|
||||
interface MockSettingParams {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
defaultValue: unknown
|
||||
category?: string[]
|
||||
}
|
||||
|
||||
describe('useSettingUI', () => {
|
||||
const mockSettings: Record<string, MockSettingParams> = {
|
||||
'Comfy.Locale': {
|
||||
id: 'Comfy.Locale',
|
||||
name: 'Locale',
|
||||
type: 'combo',
|
||||
defaultValue: 'en'
|
||||
},
|
||||
'LiteGraph.Zoom': {
|
||||
id: 'LiteGraph.Zoom',
|
||||
name: 'Zoom',
|
||||
type: 'slider',
|
||||
defaultValue: 1
|
||||
},
|
||||
'Appearance.Theme': {
|
||||
id: 'Appearance.Theme',
|
||||
name: 'Theme',
|
||||
type: 'combo',
|
||||
defaultValue: 'dark'
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia())
|
||||
vi.clearAllMocks()
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({
|
||||
settingsById: mockSettings
|
||||
} as ReturnType<typeof useSettingStore>)
|
||||
|
||||
vi.mocked(getSettingInfo).mockImplementation((setting) => {
|
||||
const parts = setting.category || setting.id.split('.')
|
||||
return {
|
||||
category: parts[0] ?? 'Other',
|
||||
subCategory: parts[1] ?? 'Other'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function findCategory(
|
||||
categories: SettingTreeNode[],
|
||||
label: string
|
||||
): SettingTreeNode | undefined {
|
||||
return categories.find((c) => c.label === label)
|
||||
}
|
||||
|
||||
it('defaults to first category when no params are given', () => {
|
||||
const { defaultCategory, settingCategories } = useSettingUI()
|
||||
expect(defaultCategory.value).toBe(settingCategories.value[0])
|
||||
})
|
||||
|
||||
it('resolves category from scrollToSettingId', () => {
|
||||
const { defaultCategory, settingCategories } = useSettingUI(
|
||||
undefined,
|
||||
'Comfy.Locale'
|
||||
)
|
||||
const comfyCategory = findCategory(settingCategories.value, 'Comfy')
|
||||
expect(comfyCategory).toBeDefined()
|
||||
expect(defaultCategory.value).toBe(comfyCategory)
|
||||
})
|
||||
|
||||
it('resolves different category from scrollToSettingId', () => {
|
||||
const { defaultCategory, settingCategories } = useSettingUI(
|
||||
undefined,
|
||||
'Appearance.Theme'
|
||||
)
|
||||
const appearanceCategory = findCategory(
|
||||
settingCategories.value,
|
||||
'Appearance'
|
||||
)
|
||||
expect(appearanceCategory).toBeDefined()
|
||||
expect(defaultCategory.value).toBe(appearanceCategory)
|
||||
})
|
||||
|
||||
it('falls back to first category for unknown scrollToSettingId', () => {
|
||||
const { defaultCategory, settingCategories } = useSettingUI(
|
||||
undefined,
|
||||
'NonExistent.Setting'
|
||||
)
|
||||
expect(defaultCategory.value).toBe(settingCategories.value[0])
|
||||
})
|
||||
|
||||
it('gives defaultPanel precedence over scrollToSettingId', () => {
|
||||
const { defaultCategory } = useSettingUI('about', 'Comfy.Locale')
|
||||
expect(defaultCategory.value.key).toBe('about')
|
||||
})
|
||||
})
|
||||
@@ -7,12 +7,10 @@ import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { isCloud, isDesktop } from '@/platform/distribution/types'
|
||||
import {
|
||||
getSettingInfo,
|
||||
useSettingStore
|
||||
} from '@/platform/settings/settingStore'
|
||||
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { buildTree } from '@/utils/treeUtil'
|
||||
|
||||
@@ -32,8 +30,7 @@ export function useSettingUI(
|
||||
| 'credits'
|
||||
| 'subscription'
|
||||
| 'workspace'
|
||||
| 'secrets',
|
||||
scrollToSettingId?: string
|
||||
| 'secrets'
|
||||
) {
|
||||
const { t } = useI18n()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -241,23 +238,12 @@ export function useSettingUI(
|
||||
* The default category to show when the dialog is opened.
|
||||
*/
|
||||
const defaultCategory = computed<SettingTreeNode>(() => {
|
||||
if (defaultPanel) {
|
||||
for (const group of groupedMenuTreeNodes.value) {
|
||||
const found = group.children?.find((node) => node.key === defaultPanel)
|
||||
if (found) return found
|
||||
}
|
||||
return settingCategories.value[0]
|
||||
if (!defaultPanel) return settingCategories.value[0]
|
||||
// Search through all groups in groupedMenuTreeNodes
|
||||
for (const group of groupedMenuTreeNodes.value) {
|
||||
const found = group.children?.find((node) => node.key === defaultPanel)
|
||||
if (found) return found
|
||||
}
|
||||
|
||||
if (scrollToSettingId) {
|
||||
const setting = settingStore.settingsById[scrollToSettingId]
|
||||
if (setting) {
|
||||
const { category } = getSettingInfo(setting)
|
||||
const found = settingCategories.value.find((c) => c.label === category)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
|
||||
return settingCategories.value[0]
|
||||
})
|
||||
|
||||
|
||||
@@ -170,15 +170,13 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
if (newValue === 'standard') {
|
||||
await settingStore.setMany({
|
||||
'Comfy.Canvas.LeftMouseClickBehavior': 'select',
|
||||
'Comfy.Canvas.MouseWheelScroll': 'panning'
|
||||
})
|
||||
// Update related settings to match standard mode - select + panning
|
||||
await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'select')
|
||||
await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'panning')
|
||||
} else if (newValue === 'legacy') {
|
||||
await settingStore.setMany({
|
||||
'Comfy.Canvas.LeftMouseClickBehavior': 'panning',
|
||||
'Comfy.Canvas.MouseWheelScroll': 'zoom'
|
||||
})
|
||||
// Update related settings to match legacy mode - panning + zoom
|
||||
await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'panning')
|
||||
await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'zoom')
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1196,9 +1194,9 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'Comfy.NodeReplacement.Enabled',
|
||||
category: ['Comfy', 'Workflow', 'NodeReplacement'],
|
||||
name: 'Enable node replacement suggestions',
|
||||
name: 'Enable automatic node replacement',
|
||||
tooltip:
|
||||
'When enabled, missing nodes with known replacements will be shown as replaceable in the missing nodes dialog, allowing you to review and apply replacements.',
|
||||
'When enabled, missing nodes can be automatically replaced with their newer equivalents if a replacement mapping exists.',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
experimental: true,
|
||||
|
||||
@@ -15,8 +15,7 @@ import { app } from '@/scripts/app'
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getSettings: vi.fn(),
|
||||
storeSetting: vi.fn(),
|
||||
storeSettings: vi.fn()
|
||||
storeSetting: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -504,85 +503,6 @@ describe('useSettingStore', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMany', () => {
|
||||
it('should set multiple values and make a single API call', async () => {
|
||||
const onChange1 = vi.fn()
|
||||
const onChange2 = vi.fn()
|
||||
store.addSetting({
|
||||
id: 'Comfy.Release.Version',
|
||||
name: 'Release Version',
|
||||
type: 'hidden',
|
||||
defaultValue: '',
|
||||
onChange: onChange1
|
||||
})
|
||||
store.addSetting({
|
||||
id: 'Comfy.Release.Status',
|
||||
name: 'Release Status',
|
||||
type: 'hidden',
|
||||
defaultValue: 'skipped',
|
||||
onChange: onChange2
|
||||
})
|
||||
vi.clearAllMocks()
|
||||
|
||||
await store.setMany({
|
||||
'Comfy.Release.Version': '1.0.0',
|
||||
'Comfy.Release.Status': 'changelog seen'
|
||||
})
|
||||
|
||||
expect(store.get('Comfy.Release.Version')).toBe('1.0.0')
|
||||
expect(store.get('Comfy.Release.Status')).toBe('changelog seen')
|
||||
expect(onChange1).toHaveBeenCalledWith('1.0.0', '')
|
||||
expect(onChange2).toHaveBeenCalledWith('changelog seen', 'skipped')
|
||||
expect(api.storeSettings).toHaveBeenCalledTimes(1)
|
||||
expect(api.storeSettings).toHaveBeenCalledWith({
|
||||
'Comfy.Release.Version': '1.0.0',
|
||||
'Comfy.Release.Status': 'changelog seen'
|
||||
})
|
||||
expect(api.storeSetting).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should skip unchanged values', async () => {
|
||||
store.addSetting({
|
||||
id: 'Comfy.Release.Version',
|
||||
name: 'Release Version',
|
||||
type: 'hidden',
|
||||
defaultValue: ''
|
||||
})
|
||||
store.addSetting({
|
||||
id: 'Comfy.Release.Status',
|
||||
name: 'Release Status',
|
||||
type: 'hidden',
|
||||
defaultValue: 'skipped'
|
||||
})
|
||||
await store.set('Comfy.Release.Version', 'existing')
|
||||
vi.clearAllMocks()
|
||||
|
||||
await store.setMany({
|
||||
'Comfy.Release.Version': 'existing',
|
||||
'Comfy.Release.Status': 'changelog seen'
|
||||
})
|
||||
|
||||
expect(api.storeSettings).toHaveBeenCalledWith({
|
||||
'Comfy.Release.Status': 'changelog seen'
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call API when all values are unchanged', async () => {
|
||||
store.addSetting({
|
||||
id: 'Comfy.Release.Version',
|
||||
name: 'Release Version',
|
||||
type: 'hidden',
|
||||
defaultValue: ''
|
||||
})
|
||||
await store.set('Comfy.Release.Version', 'existing')
|
||||
vi.clearAllMocks()
|
||||
|
||||
await store.setMany({ 'Comfy.Release.Version': 'existing' })
|
||||
|
||||
expect(api.storeSettings).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSettingInfo', () => {
|
||||
|
||||
@@ -92,58 +92,23 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a setting value locally: clone, migrate, fire onChange, and
|
||||
* update the in-memory store. Returns the migrated value, or
|
||||
* `undefined` when the value is unchanged and was skipped.
|
||||
* Set a setting value.
|
||||
* @param key - The key of the setting to set.
|
||||
* @param value - The value to set.
|
||||
*/
|
||||
function applySettingLocally<K extends keyof Settings>(
|
||||
key: K,
|
||||
value: Settings[K]
|
||||
): Settings[K] | undefined {
|
||||
async function set<K extends keyof Settings>(key: K, value: Settings[K]) {
|
||||
// Clone the incoming value to prevent external mutations
|
||||
const clonedValue = _.cloneDeep(value)
|
||||
const newValue = tryMigrateDeprecatedValue(
|
||||
settingsById.value[key],
|
||||
clonedValue
|
||||
)
|
||||
const oldValue = get(key)
|
||||
if (newValue === oldValue) return undefined
|
||||
if (newValue === oldValue) return
|
||||
|
||||
onChange(settingsById.value[key], newValue, oldValue)
|
||||
settingValues.value[key] = newValue
|
||||
return newValue as Settings[K]
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a setting value.
|
||||
* @param key - The key of the setting to set.
|
||||
* @param value - The value to set.
|
||||
*/
|
||||
async function set<K extends keyof Settings>(key: K, value: Settings[K]) {
|
||||
const applied = applySettingLocally(key, value)
|
||||
if (applied === undefined) return
|
||||
await api.storeSetting(key, applied)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set multiple setting values in a single API call.
|
||||
* @param settings - A partial settings object with key-value pairs to set.
|
||||
*/
|
||||
async function setMany(settings: Partial<Settings>) {
|
||||
const updatedSettings: Partial<Settings> = {}
|
||||
|
||||
for (const key of Object.keys(settings) as (keyof Settings)[]) {
|
||||
const applied = applySettingLocally(
|
||||
key,
|
||||
settings[key] as Settings[typeof key]
|
||||
)
|
||||
if (applied !== undefined) {
|
||||
updatedSettings[key] = applied
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updatedSettings).length > 0) {
|
||||
await api.storeSettings(updatedSettings)
|
||||
}
|
||||
await api.storeSetting(key, newValue)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,7 +271,6 @@ export const useSettingStore = defineStore('setting', () => {
|
||||
load,
|
||||
addSetting,
|
||||
set,
|
||||
setMany,
|
||||
get,
|
||||
exists,
|
||||
getDefaultValue
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user