Compare commits
24 Commits
glary/oxli
...
update-ing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d4a70d62f | ||
|
|
fef19b1c52 | ||
|
|
f744a4f1f8 | ||
|
|
0588ca45b3 | ||
|
|
60ce0ee0c3 | ||
|
|
91d2df45a1 | ||
|
|
7b4fef5eca | ||
|
|
c703db5f6c | ||
|
|
3011d3a60c | ||
|
|
6e31ce77c6 | ||
|
|
551c595bbb | ||
|
|
ee286291d4 | ||
|
|
efb214efe7 | ||
|
|
9a2bea7283 | ||
|
|
0a07781a76 | ||
|
|
b3ba6c9344 | ||
|
|
a50b3d16da | ||
|
|
3ce0c07af2 | ||
|
|
52d77e6ee0 | ||
|
|
f1f65cff61 | ||
|
|
b0144db644 | ||
|
|
8ee8dd03c4 | ||
|
|
d472ca783b | ||
|
|
e80ec6e3d4 |
6
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -45,12 +45,8 @@ jobs:
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
# Build cloud distribution for @cloud tagged tests
|
||||
# NX_SKIP_NX_CACHE=true is required because `nx build` was already run
|
||||
# for the OSS distribution above. Without skipping cache, Nx returns
|
||||
# the cached OSS build since env vars aren't part of the cache key.
|
||||
- name: Build cloud frontend
|
||||
run: NX_SKIP_NX_CACHE=true pnpm build:cloud
|
||||
run: pnpm build:cloud
|
||||
|
||||
- name: Upload cloud frontend
|
||||
uses: actions/upload-artifact@v6
|
||||
|
||||
2
.github/workflows/pr-claude-review.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies for analysis tools
|
||||
run: |
|
||||
pnpm install -g typescript @vue/compiler-sfc
|
||||
pnpm add -g typescript @vue/compiler-sfc
|
||||
|
||||
- name: Run Claude PR Review
|
||||
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
|
||||
|
||||
2
.github/workflows/release-draft-create.yaml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
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
|
||||
|
||||
6
.github/workflows/weekly-docs-check.yaml
vendored
@@ -40,11 +40,11 @@ jobs:
|
||||
- name: Install dependencies for analysis tools
|
||||
run: |
|
||||
# Check if packages are already available locally
|
||||
if ! pnpm list typescript @vue/compiler-sfc >/dev/null 2>&1; then
|
||||
if ! pnpm list -g typescript @vue/compiler-sfc >/dev/null 2>&1; then
|
||||
echo "Installing TypeScript and Vue compiler globally..."
|
||||
pnpm install -g typescript @vue/compiler-sfc
|
||||
pnpm add -g typescript @vue/compiler-sfc
|
||||
else
|
||||
echo "TypeScript and Vue compiler already available locally"
|
||||
echo "TypeScript and Vue compiler already available globally"
|
||||
fi
|
||||
|
||||
- name: Run Claude Documentation Review
|
||||
|
||||
5
.gitignore
vendored
@@ -19,6 +19,7 @@ yarn.lock
|
||||
|
||||
node_modules
|
||||
.pnpm-store
|
||||
.nx
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
@@ -89,10 +90,6 @@ storybook-static
|
||||
# MCP Servers
|
||||
.playwright-mcp/*
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.cursor/rules/nx-rules.mdc
|
||||
.github/instructions/nx.instructions.md
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
|
||||
|
||||
3
.npmrc
@@ -1,3 +0,0 @@
|
||||
ignore-workspace-root-check=true
|
||||
catalog-mode=prefer
|
||||
public-hoist-pattern[]=@parcel/watcher
|
||||
@@ -2,7 +2,6 @@
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".i18nrc.cjs",
|
||||
".nx/*",
|
||||
"**/vite.config.*.timestamp*",
|
||||
"**/vitest.config.*.timestamp*",
|
||||
"components.d.ts",
|
||||
|
||||
@@ -35,7 +35,7 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
|
||||
|
||||
## Monorepo Architecture
|
||||
|
||||
The project uses **Nx** for build orchestration and task management
|
||||
The project uses **pnpm workspaces** for monorepo organization and native tool CLIs for task execution
|
||||
|
||||
## Package Manager
|
||||
|
||||
@@ -237,7 +237,6 @@ See @docs/testing/\*.md for detailed patterns.
|
||||
- ComfyUI: <https://docs.comfy.org>
|
||||
- Electron: <https://www.electronjs.org/docs/latest/>
|
||||
- Wiki: <https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview>
|
||||
- Nx: <https://nx.dev/docs/reference/nx-commands>
|
||||
- [Practical Test Pyramid](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
@@ -7,7 +7,7 @@ This guide helps you resolve common issues when developing ComfyUI Frontend.
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Having Issues?] --> B{What's the problem?}
|
||||
B -->|Dev server stuck| C[nx serve hangs]
|
||||
B -->|Dev server stuck| C[pnpm dev hangs]
|
||||
B -->|Build errors| D[Check build issues]
|
||||
B -->|Lint errors| Q[Check linting issues]
|
||||
B -->|Dependency issues| E[Package problems]
|
||||
@@ -23,7 +23,7 @@ flowchart TD
|
||||
G -->|No| H[Run: pnpm i]
|
||||
G -->|Still stuck| I[Run: pnpm clean]
|
||||
I --> J{Still stuck?}
|
||||
J -->|Yes| K[Nuclear option:<br/>pnpm dlx rimraf node_modules<br/>&& pnpm i]
|
||||
J -->|Yes| K[Nuclear option:<br/>pnpm clean:all<br/>&& pnpm i]
|
||||
J -->|No| L[Fixed!]
|
||||
H --> L
|
||||
|
||||
@@ -41,11 +41,11 @@ flowchart TD
|
||||
|
||||
### Development Server Issues
|
||||
|
||||
#### Q: `pnpm dev` or `nx serve` gets stuck and won't start
|
||||
#### Q: `pnpm dev` gets stuck and won't start
|
||||
|
||||
**Symptoms:**
|
||||
|
||||
- Command hangs on "nx serve"
|
||||
- Command hangs during Vite startup
|
||||
- Dev server doesn't respond
|
||||
- Terminal appears frozen
|
||||
|
||||
@@ -65,7 +65,7 @@ flowchart TD
|
||||
|
||||
3. **Last resort - Full node_modules reset:**
|
||||
```bash
|
||||
pnpm dlx rimraf node_modules && pnpm i
|
||||
pnpm clean:all && pnpm i
|
||||
```
|
||||
|
||||
**Why this happens:**
|
||||
@@ -73,7 +73,7 @@ flowchart TD
|
||||
- Corrupted dependency cache
|
||||
- Outdated lock files after branch switching
|
||||
- Incomplete previous installations
|
||||
- NX cache corruption
|
||||
- stale local build cache
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "nx run @comfyorg/desktop-ui:lint",
|
||||
"typecheck": "nx run @comfyorg/desktop-ui:typecheck",
|
||||
"dev": "pnpm -w exec vite --config apps/desktop-ui/vite.config.mts",
|
||||
"build": "pnpm -w exec vite build --config apps/desktop-ui/vite.config.mts",
|
||||
"preview": "pnpm -w exec vite preview --config apps/desktop-ui/vite.config.mts",
|
||||
"lint": "eslint src --cache",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.json",
|
||||
"test:unit": "vitest run --config vitest.config.mts",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build -o dist/storybook"
|
||||
@@ -33,88 +36,5 @@
|
||||
"vite-plugin-html": "catalog:",
|
||||
"vite-plugin-vue-devtools": "catalog:",
|
||||
"vue-tsc": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:desktop",
|
||||
"type:app"
|
||||
],
|
||||
"targets": {
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "vite --config vite.config.mts"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "vite --config vite.config.mts"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"options": {
|
||||
"command": "vite build --config apps/desktop-ui/vite.config.mts"
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/dist"
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "vite preview --config vite.config.mts"
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "storybook dev -p 6007"
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "storybook build -o dist/storybook"
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/dist/storybook"
|
||||
]
|
||||
},
|
||||
"lint": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "eslint src --cache"
|
||||
}
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/desktop-ui",
|
||||
"command": "vue-tsc --noEmit -p tsconfig.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,88 +45,5 @@
|
||||
"tsx": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:website",
|
||||
"type:app"
|
||||
],
|
||||
"targets": {
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "astro dev"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "astro dev"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "astro build"
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/dist"
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "astro preview"
|
||||
}
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "astro check"
|
||||
}
|
||||
},
|
||||
"test:unit": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "vitest run"
|
||||
}
|
||||
},
|
||||
"test:coverage": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "vitest run --coverage"
|
||||
}
|
||||
},
|
||||
"test:e2e": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/website",
|
||||
"command": "playwright test"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/website/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
apps/website/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/website/public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"fetchedAt": "2026-05-12T16:10:34.114Z",
|
||||
"fetchedAt": "2026-05-22T00:07:48.353Z",
|
||||
"departments": [
|
||||
{
|
||||
"name": "DESIGN",
|
||||
@@ -36,14 +36,14 @@
|
||||
"id": "6a6d865eeb3c10a8",
|
||||
"title": "Senior Software Engineer, Frontend",
|
||||
"department": "Engineering",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/c3e0584d-5490-491f-aae4-b5922ef63fd2"
|
||||
},
|
||||
{
|
||||
"id": "1b4f7f1da9616e14",
|
||||
"title": "Senior Software Engineer, Backend Generalist",
|
||||
"department": "Engineering",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/732f8b39-076d-4847-afe3-f54d4451607e"
|
||||
},
|
||||
{
|
||||
@@ -71,14 +71,14 @@
|
||||
"id": "91604c4182a1bc3c",
|
||||
"title": "Software Engineer, Core ComfyUI Contributor",
|
||||
"department": "Engineering",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/7d4062d6-d500-445a-9a5f-014971af259f"
|
||||
},
|
||||
{
|
||||
"id": "a1dbc0576ab14034",
|
||||
"title": "Software Engineer, ComfyUI Desktop",
|
||||
"department": "Engineering",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/ad2f76cb-a787-47d8-81c5-7e7f917747c0"
|
||||
},
|
||||
{
|
||||
@@ -105,21 +105,21 @@
|
||||
"id": "23dd98cab77ff459",
|
||||
"title": "Freelance Motion Designer",
|
||||
"department": "Marketing",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/a7ccc2b4-4d9d-4e04-b39c-28a711995b5b"
|
||||
},
|
||||
{
|
||||
"id": "a998b9fc973ff3c0",
|
||||
"title": "Creative Artist",
|
||||
"department": "Marketing",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/19ba10aa-4961-45e8-8473-66a8a7a8079d"
|
||||
},
|
||||
{
|
||||
"id": "3e730938026d6e70",
|
||||
"title": "Graphic Designer",
|
||||
"department": "Marketing",
|
||||
"location": "San Francisco",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/49fa0b07-3fa1-4a3a-b2c6-d2cc684ad63f"
|
||||
},
|
||||
{
|
||||
@@ -135,6 +135,20 @@
|
||||
"department": "Marketing",
|
||||
"location": "San Francisco",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/89d3ff75-2055-4e92-9c69-81feff55627c"
|
||||
},
|
||||
{
|
||||
"id": "e11f8b9e58dbea81",
|
||||
"title": "Creative Producer",
|
||||
"department": "Marketing",
|
||||
"location": "Remote",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/7be2d690-7a2b-4ebf-b1c4-6907b273d3d9"
|
||||
},
|
||||
{
|
||||
"id": "6eac654593208ec3",
|
||||
"title": "Forward Deployed Creative Technologist",
|
||||
"department": "Marketing",
|
||||
"location": "San Francisco",
|
||||
"jobUrl": "https://jobs.ashbyhq.com/comfy-org/af49c05f-dcd8-4c3d-a464-43eb3b1c6efc"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -72,6 +72,9 @@ const websiteJsonLd = {
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/favicon.png" type="image/png" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="canonical" href={canonicalURL.href} />
|
||||
<link rel="preconnect" href="https://www.googletagmanager.com" />
|
||||
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
|
||||
|
||||
@@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
DEFAULT_REGISTRY_BASE_URL,
|
||||
fetchRegistryPacks
|
||||
fetchRegistryPacks,
|
||||
fetchRegistryPacksWithNodes
|
||||
} from './cloudNodes.registry'
|
||||
|
||||
function jsonResponse(
|
||||
@@ -142,3 +143,315 @@ describe('fetchRegistryPacks', () => {
|
||||
expect(result.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchRegistryPacksWithNodes', () => {
|
||||
it('fetches pack metadata and comfy nodes for each pack', async () => {
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
// Pack metadata request
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'comfyui-impact-pack',
|
||||
name: 'ComfyUI Impact Pack',
|
||||
repository: 'https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
latest_version: { version: '8.0.0', createdAt: '2026-01-01' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// Comfy nodes request
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
return jsonResponse({
|
||||
comfy_nodes: [
|
||||
{ comfy_node_name: 'FaceDetailer', category: 'detailer' },
|
||||
{ comfy_node_name: 'DetailerForEach', category: 'detailer' }
|
||||
],
|
||||
totalNumberOfPages: 1
|
||||
})
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['comfyui-impact-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
expect(result.size).toBe(1)
|
||||
const packData = result.get('comfyui-impact-pack')
|
||||
expect(packData).not.toBeNull()
|
||||
expect(packData?.pack.name).toBe('ComfyUI Impact Pack')
|
||||
expect(packData?.nodes).toHaveLength(2)
|
||||
expect(packData?.nodes[0]?.comfy_node_name).toBe('FaceDetailer')
|
||||
})
|
||||
|
||||
it('handles pagination for comfy nodes', async () => {
|
||||
let comfyNodesCallCount = 0
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'big-pack',
|
||||
name: 'Big Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
comfyNodesCallCount++
|
||||
const page = Number(url.searchParams.get('page') ?? '1')
|
||||
|
||||
if (page === 1) {
|
||||
return jsonResponse({
|
||||
comfy_nodes: [
|
||||
{ comfy_node_name: 'Node1', category: 'cat1' },
|
||||
{ comfy_node_name: 'Node2', category: 'cat1' }
|
||||
],
|
||||
totalNumberOfPages: 2
|
||||
})
|
||||
} else {
|
||||
return jsonResponse({
|
||||
comfy_nodes: [{ comfy_node_name: 'Node3', category: 'cat2' }],
|
||||
totalNumberOfPages: 2
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['big-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
expect(comfyNodesCallCount).toBe(2)
|
||||
const packData = result.get('big-pack')
|
||||
expect(packData?.nodes).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('returns null for packs without latest_version', async () => {
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'no-version-pack',
|
||||
name: 'No Version Pack',
|
||||
latest_version: null
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['no-version-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
expect(result.get('no-version-pack')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns empty nodes array when comfy-nodes request fails', async () => {
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'failing-pack',
|
||||
name: 'Failing Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
return new Response('Server error', { status: 500 })
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['failing-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
const packData = result.get('failing-pack')
|
||||
expect(packData).not.toBeNull()
|
||||
expect(packData?.pack.name).toBe('Failing Pack')
|
||||
expect(packData?.nodes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles null comfy_nodes in response', async () => {
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'null-nodes-pack',
|
||||
name: 'Null Nodes Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
return jsonResponse({
|
||||
comfy_nodes: null,
|
||||
totalNumberOfPages: 1
|
||||
})
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['null-nodes-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
const packData = result.get('null-nodes-pack')
|
||||
expect(packData?.nodes).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('fetches nodes for multiple packs in parallel', async () => {
|
||||
const packIds = ['pack-a', 'pack-b', 'pack-c']
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
const requestedIds = url.searchParams.getAll('node_id')
|
||||
return jsonResponse({
|
||||
nodes: requestedIds.map((id) => ({
|
||||
id,
|
||||
name: id.toUpperCase(),
|
||||
latest_version: { version: '1.0.0' }
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
const packId = url.pathname.split('/nodes/')[1]?.split('/')[0]
|
||||
return jsonResponse({
|
||||
comfy_nodes: [
|
||||
{ comfy_node_name: `${packId}-node`, category: 'test' }
|
||||
],
|
||||
totalNumberOfPages: 1
|
||||
})
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(packIds, {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
expect(result.size).toBe(3)
|
||||
for (const packId of packIds) {
|
||||
const packData = result.get(packId)
|
||||
expect(packData).not.toBeNull()
|
||||
expect(packData?.nodes[0]?.comfy_node_name).toBe(`${packId}-node`)
|
||||
}
|
||||
})
|
||||
|
||||
it('retries comfy-nodes fetch once on failure', async () => {
|
||||
let comfyNodesAttempts = 0
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'retry-pack',
|
||||
name: 'Retry Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
comfyNodesAttempts++
|
||||
if (comfyNodesAttempts === 1) {
|
||||
return new Response('Server error', { status: 500 })
|
||||
}
|
||||
return jsonResponse({
|
||||
comfy_nodes: [{ comfy_node_name: 'RetryNode', category: 'test' }],
|
||||
totalNumberOfPages: 1
|
||||
})
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['retry-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
expect(comfyNodesAttempts).toBe(2)
|
||||
const packData = result.get('retry-pack')
|
||||
expect(packData?.nodes).toHaveLength(1)
|
||||
expect(packData?.nodes[0]?.comfy_node_name).toBe('RetryNode')
|
||||
})
|
||||
|
||||
it('normalizes null boolean fields in comfy nodes', async () => {
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = new URL(String(input))
|
||||
|
||||
if (url.pathname === '/nodes') {
|
||||
return jsonResponse({
|
||||
nodes: [
|
||||
{
|
||||
id: 'bool-pack',
|
||||
name: 'Bool Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/comfy-nodes')) {
|
||||
return jsonResponse({
|
||||
comfy_nodes: [
|
||||
{
|
||||
comfy_node_name: 'TestNode',
|
||||
category: 'test',
|
||||
deprecated: null,
|
||||
experimental: null
|
||||
}
|
||||
],
|
||||
totalNumberOfPages: 1
|
||||
})
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 })
|
||||
})
|
||||
|
||||
const result = await fetchRegistryPacksWithNodes(['bool-pack'], {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
|
||||
const packData = result.get('bool-pack')
|
||||
expect(packData?.nodes[0]?.deprecated).toBeUndefined()
|
||||
expect(packData?.nodes[0]?.experimental).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,8 +5,10 @@ import type { components } from '@comfyorg/registry-types'
|
||||
export const DEFAULT_REGISTRY_BASE_URL = 'https://api.comfy.org'
|
||||
const DEFAULT_TIMEOUT_MS = 5_000
|
||||
const BATCH_SIZE = 50
|
||||
const COMFY_NODES_PAGE_SIZE = 500
|
||||
|
||||
export type RegistryPack = components['schemas']['Node']
|
||||
export type RegistryComfyNode = components['schemas']['ComfyNode']
|
||||
|
||||
function nullToUndefined<T>(value: T | null | undefined): T | undefined {
|
||||
return value ?? undefined
|
||||
@@ -58,6 +60,29 @@ const RegistryListResponseSchema = z
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const RegistryComfyNodeSchema = z
|
||||
.object({
|
||||
comfy_node_name: optionalString,
|
||||
category: optionalString,
|
||||
description: optionalString,
|
||||
deprecated: z
|
||||
.boolean()
|
||||
.nullish()
|
||||
.transform((v) => v ?? undefined),
|
||||
experimental: z
|
||||
.boolean()
|
||||
.nullish()
|
||||
.transform((v) => v ?? undefined)
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
const RegistryComfyNodesResponseSchema = z
|
||||
.object({
|
||||
comfy_nodes: z.array(RegistryComfyNodeSchema).nullish(),
|
||||
totalNumberOfPages: z.number().nullish()
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
interface FetchRegistryOptions {
|
||||
baseUrl?: string
|
||||
timeoutMs?: number
|
||||
@@ -122,6 +147,142 @@ export async function fetchRegistryPacks(
|
||||
return resolved
|
||||
}
|
||||
|
||||
export interface RegistryPackWithNodes {
|
||||
pack: RegistryPack
|
||||
nodes: RegistryComfyNode[]
|
||||
}
|
||||
|
||||
export async function fetchRegistryPacksWithNodes(
|
||||
packIds: readonly string[],
|
||||
options: FetchRegistryOptions = {}
|
||||
): Promise<Map<string, RegistryPackWithNodes | null>> {
|
||||
const packs = await fetchRegistryPacks(packIds, options)
|
||||
|
||||
const baseUrl = options.baseUrl ?? DEFAULT_REGISTRY_BASE_URL
|
||||
const timeoutMs = clampTimeoutMs(options.timeoutMs)
|
||||
const fetchImpl = options.fetchImpl ?? fetch
|
||||
|
||||
const entries = await Promise.all(
|
||||
[...packs.entries()].map(
|
||||
async ([packId, pack]): Promise<
|
||||
[string, RegistryPackWithNodes | null]
|
||||
> => {
|
||||
if (!pack?.latest_version?.version) {
|
||||
return [packId, null]
|
||||
}
|
||||
|
||||
const nodes = await fetchComfyNodesForPack(
|
||||
fetchImpl,
|
||||
baseUrl,
|
||||
packId,
|
||||
pack.latest_version.version,
|
||||
timeoutMs
|
||||
)
|
||||
|
||||
return [packId, { pack, nodes }]
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return new Map(entries)
|
||||
}
|
||||
|
||||
async function fetchComfyNodesForPack(
|
||||
fetchImpl: typeof fetch,
|
||||
baseUrl: string,
|
||||
packId: string,
|
||||
version: string,
|
||||
timeoutMs: number
|
||||
): Promise<RegistryComfyNode[]> {
|
||||
const allNodes: RegistryComfyNode[] = []
|
||||
let page = 1
|
||||
let totalPages = 1
|
||||
|
||||
while (page <= totalPages) {
|
||||
const result = await fetchComfyNodesPageWithRetry(
|
||||
fetchImpl,
|
||||
baseUrl,
|
||||
packId,
|
||||
version,
|
||||
page,
|
||||
timeoutMs
|
||||
)
|
||||
|
||||
if (!result) break
|
||||
|
||||
allNodes.push(...result.nodes)
|
||||
totalPages = result.totalPages
|
||||
page++
|
||||
}
|
||||
|
||||
return allNodes
|
||||
}
|
||||
|
||||
async function fetchComfyNodesPageWithRetry(
|
||||
fetchImpl: typeof fetch,
|
||||
baseUrl: string,
|
||||
packId: string,
|
||||
version: string,
|
||||
page: number,
|
||||
timeoutMs: number
|
||||
): Promise<{ nodes: RegistryComfyNode[]; totalPages: number } | null> {
|
||||
const firstAttempt = await fetchComfyNodesPage(
|
||||
fetchImpl,
|
||||
baseUrl,
|
||||
packId,
|
||||
version,
|
||||
page,
|
||||
timeoutMs
|
||||
)
|
||||
if (firstAttempt) return firstAttempt
|
||||
|
||||
// Retry once on failure
|
||||
return fetchComfyNodesPage(
|
||||
fetchImpl,
|
||||
baseUrl,
|
||||
packId,
|
||||
version,
|
||||
page,
|
||||
timeoutMs
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchComfyNodesPage(
|
||||
fetchImpl: typeof fetch,
|
||||
baseUrl: string,
|
||||
packId: string,
|
||||
version: string,
|
||||
page: number,
|
||||
timeoutMs: number
|
||||
): Promise<{ nodes: RegistryComfyNode[]; totalPages: number } | null> {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
const url = `${baseUrl}/nodes/${encodeURIComponent(packId)}/versions/${encodeURIComponent(version)}/comfy-nodes?limit=${COMFY_NODES_PAGE_SIZE}&page=${page}`
|
||||
const res = await fetchImpl(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (!res.ok) return null
|
||||
|
||||
const rawBody: unknown = await res.json()
|
||||
const parsed = RegistryComfyNodesResponseSchema.safeParse(rawBody)
|
||||
if (!parsed.success) return null
|
||||
|
||||
return {
|
||||
nodes: (parsed.data.comfy_nodes ?? []) as RegistryComfyNode[],
|
||||
totalPages: parsed.data.totalNumberOfPages ?? 1
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBatchWithRetry(
|
||||
fetchImpl: typeof fetch,
|
||||
baseUrl: string,
|
||||
|
||||
@@ -8,12 +8,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { NodesSnapshot } from '../data/cloudNodes'
|
||||
import type * as ObjectInfoParser from '@comfyorg/object-info-parser'
|
||||
|
||||
const fetchRegistryPacksMock = vi.hoisted(() => vi.fn(async () => new Map()))
|
||||
import type { RegistryPackWithNodes } from './cloudNodes.registry'
|
||||
|
||||
const fetchRegistryPacksWithNodesMock = vi.hoisted(() =>
|
||||
vi.fn(async () => new Map<string, RegistryPackWithNodes | null>())
|
||||
)
|
||||
const sanitizeCallSpy = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('./cloudNodes.registry', () => ({
|
||||
DEFAULT_REGISTRY_BASE_URL: 'https://api.comfy.org',
|
||||
fetchRegistryPacks: fetchRegistryPacksMock
|
||||
fetchRegistryPacksWithNodes: fetchRegistryPacksWithNodesMock
|
||||
}))
|
||||
|
||||
vi.mock('@comfyorg/object-info-parser', async (importOriginal) => {
|
||||
@@ -90,8 +94,8 @@ describe('fetchCloudNodesForBuild', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
resetCloudNodesFetcherForTests()
|
||||
fetchRegistryPacksMock.mockReset()
|
||||
fetchRegistryPacksMock.mockResolvedValue(new Map())
|
||||
fetchRegistryPacksWithNodesMock.mockReset()
|
||||
fetchRegistryPacksWithNodesMock.mockResolvedValue(new Map())
|
||||
sanitizeCallSpy.mockReset()
|
||||
delete process.env.WEBSITE_CLOUD_API_KEY
|
||||
})
|
||||
@@ -102,14 +106,21 @@ describe('fetchCloudNodesForBuild', () => {
|
||||
})
|
||||
|
||||
it('returns fresh when API succeeds', async () => {
|
||||
fetchRegistryPacksMock.mockResolvedValue(
|
||||
new Map([
|
||||
fetchRegistryPacksWithNodesMock.mockResolvedValue(
|
||||
new Map<string, RegistryPackWithNodes | null>([
|
||||
[
|
||||
'comfyui-impact-pack',
|
||||
{
|
||||
id: 'comfyui-impact-pack',
|
||||
name: 'ComfyUI Impact Pack',
|
||||
repository: 'https://github.com/ltdrdata/ComfyUI-Impact-Pack'
|
||||
pack: {
|
||||
id: 'comfyui-impact-pack',
|
||||
name: 'ComfyUI Impact Pack',
|
||||
repository: 'https://github.com/ltdrdata/ComfyUI-Impact-Pack',
|
||||
latest_version: { version: '1.0.0' }
|
||||
},
|
||||
nodes: [
|
||||
{ comfy_node_name: 'FaceDetailer', category: 'detailer' },
|
||||
{ comfy_node_name: 'DetailerForEach', category: 'detailer' }
|
||||
]
|
||||
}
|
||||
]
|
||||
])
|
||||
@@ -129,6 +140,10 @@ describe('fetchCloudNodesForBuild', () => {
|
||||
expect(outcome.snapshot.packs[0]?.repoUrl).toBe(
|
||||
'https://github.com/ltdrdata/ComfyUI-Impact-Pack'
|
||||
)
|
||||
// Nodes should come from registry, not object_info
|
||||
expect(outcome.snapshot.packs[0]?.nodes).toHaveLength(2)
|
||||
expect(outcome.snapshot.packs[0]?.nodes[0]?.name).toBe('DetailerForEach')
|
||||
expect(outcome.snapshot.packs[0]?.nodes[1]?.name).toBe('FaceDetailer')
|
||||
})
|
||||
|
||||
it('drops invalid nodes individually and keeps valid nodes', async () => {
|
||||
@@ -297,7 +312,7 @@ describe('fetchCloudNodesForBuild', () => {
|
||||
})
|
||||
|
||||
it('returns fresh even when registry enrichment fails', async () => {
|
||||
fetchRegistryPacksMock.mockResolvedValue(new Map())
|
||||
fetchRegistryPacksWithNodesMock.mockResolvedValue(new Map())
|
||||
const fetchImpl = vi.fn(async () => response({ ImpactNode: validNode() }))
|
||||
const outcome = await fetchCloudNodesForBuild({
|
||||
apiKey: KEY,
|
||||
@@ -305,5 +320,8 @@ describe('fetchCloudNodesForBuild', () => {
|
||||
fetchImpl: fetchImpl as typeof fetch
|
||||
})
|
||||
expect(outcome.status).toBe('fresh')
|
||||
// Falls back to object_info nodes when registry fails
|
||||
if (outcome.status !== 'fresh') return
|
||||
expect(outcome.snapshot.packs[0]?.nodes[0]?.name).toBe('ImpactNode')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,12 +6,15 @@ import {
|
||||
validateComfyNodeDef
|
||||
} from '@comfyorg/object-info-parser'
|
||||
|
||||
import type { RegistryPack } from './cloudNodes.registry'
|
||||
import type {
|
||||
RegistryComfyNode,
|
||||
RegistryPackWithNodes
|
||||
} from './cloudNodes.registry'
|
||||
import type { NodesSnapshot, Pack, PackNode } from '../data/cloudNodes'
|
||||
|
||||
import bundledSnapshot from '../data/cloud-nodes.snapshot.json' with { type: 'json' }
|
||||
import { isNodesSnapshot } from '../data/cloudNodes'
|
||||
import { fetchRegistryPacks } from './cloudNodes.registry'
|
||||
import { fetchRegistryPacksWithNodes } from './cloudNodes.registry'
|
||||
import { CloudNodesEnvelopeSchema } from './cloudNodes.schema'
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://cloud.comfy.org'
|
||||
@@ -235,26 +238,28 @@ async function parseCloudNodes(
|
||||
const sanitizedDefs = sanitizeUserContent(
|
||||
validDefs as Record<string, NonNullable<(typeof validDefs)[string]>>
|
||||
)
|
||||
const grouped = groupNodesByPack(sanitizedDefs)
|
||||
|
||||
let registryMap = new Map<string, RegistryPack | null>()
|
||||
// Use object_info to determine which packs are cloud-supported
|
||||
const grouped = groupNodesByPack(sanitizedDefs)
|
||||
const packIds = grouped.map((pack) => pack.id)
|
||||
|
||||
// Fetch full pack metadata and node list from registry
|
||||
let registryMap = new Map<string, RegistryPackWithNodes | null>()
|
||||
try {
|
||||
registryMap = await fetchRegistryPacks(
|
||||
grouped.map((pack) => pack.id),
|
||||
{ fetchImpl: options.fetchImpl }
|
||||
)
|
||||
registryMap = await fetchRegistryPacksWithNodes(packIds, {
|
||||
fetchImpl: options.fetchImpl
|
||||
})
|
||||
} catch {
|
||||
registryMap = new Map()
|
||||
}
|
||||
|
||||
const packs = grouped.map((pack) =>
|
||||
toDomainPack(
|
||||
pack.id,
|
||||
pack.displayName,
|
||||
pack.nodes,
|
||||
registryMap.get(pack.id)
|
||||
)
|
||||
)
|
||||
const packs = grouped
|
||||
.map((pack) => {
|
||||
const registryData = registryMap.get(pack.id)
|
||||
// Use registry nodes if available, otherwise fall back to object_info nodes
|
||||
return toDomainPack(pack.id, pack.displayName, pack.nodes, registryData)
|
||||
})
|
||||
.filter((pack) => pack.nodes.length > 0)
|
||||
|
||||
return { kind: 'ok', packs, droppedNodes }
|
||||
}
|
||||
@@ -274,7 +279,7 @@ function safeExternalUrl(value: string | undefined): string | undefined {
|
||||
function toDomainPack(
|
||||
packId: string,
|
||||
fallbackDisplayName: string,
|
||||
nodes: Array<{
|
||||
objectInfoNodes: Array<{
|
||||
className: string
|
||||
def: {
|
||||
display_name: string
|
||||
@@ -284,8 +289,18 @@ function toDomainPack(
|
||||
experimental?: boolean
|
||||
}
|
||||
}>,
|
||||
registryPack: RegistryPack | null | undefined
|
||||
registryData: RegistryPackWithNodes | null | undefined
|
||||
): Pack {
|
||||
const registryPack = registryData?.pack
|
||||
|
||||
// Prefer registry nodes if available, fall back to object_info nodes
|
||||
const nodes =
|
||||
registryData?.nodes && registryData.nodes.length > 0
|
||||
? registryData.nodes
|
||||
.map((node) => toDomainNodeFromRegistry(node))
|
||||
.filter((n): n is PackNode => n !== null)
|
||||
: objectInfoNodes.map((node) => toDomainNode(node.className, node.def))
|
||||
|
||||
return {
|
||||
id: packId,
|
||||
registryId: registryPack?.id,
|
||||
@@ -308,9 +323,20 @@ function toDomainPack(
|
||||
registryPack?.latest_version?.createdAt ?? registryPack?.created_at,
|
||||
supportedOs: registryPack?.supported_os,
|
||||
supportedAccelerators: registryPack?.supported_accelerators,
|
||||
nodes: nodes
|
||||
.map((node) => toDomainNode(node.className, node.def))
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
||||
nodes: nodes.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
||||
}
|
||||
}
|
||||
|
||||
function toDomainNodeFromRegistry(node: RegistryComfyNode): PackNode | null {
|
||||
if (!node.comfy_node_name) return null
|
||||
|
||||
return {
|
||||
name: node.comfy_node_name,
|
||||
displayName: node.comfy_node_name,
|
||||
category: node.category || '',
|
||||
description: node.description || undefined,
|
||||
deprecated: node.deprecated,
|
||||
experimental: node.experimental
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
|
||||
import type { RootCategoryId } from '@/components/searchbox/v2/rootCategories'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import type { Position } from '@e2e/fixtures/types'
|
||||
|
||||
const { searchBoxV2 } = TestIds
|
||||
|
||||
@@ -84,11 +86,12 @@ export class ComfyNodeSearchBoxV2 {
|
||||
await this.input.waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
async openByDoubleClickCanvas(): Promise<void> {
|
||||
async openByDoubleClickCanvas(position?: Position) {
|
||||
const { x, y } = position ?? { x: 200, y: 200 }
|
||||
// Use page.mouse.dblclick (not canvas.dblclick) so the z-999 Vue overlay
|
||||
// does not intercept; coords target a viewport spot that is on the canvas
|
||||
// and clear of both the side toolbar and any default-graph nodes.
|
||||
await this.comfyPage.page.mouse.dblclick(200, 200, { delay: 5 })
|
||||
await this.comfyPage.page.mouse.dblclick(x, y, { delay: 5 })
|
||||
}
|
||||
|
||||
async ensureV2Search(): Promise<void> {
|
||||
@@ -109,4 +112,14 @@ export class ComfyNodeSearchBoxV2 {
|
||||
'search box'
|
||||
)
|
||||
}
|
||||
|
||||
async addNode(query: string, options: { position?: Position } = {}) {
|
||||
const position = options.position ?? { x: 200, y: 200 }
|
||||
await this.openByDoubleClickCanvas(position)
|
||||
await this.input.fill(query)
|
||||
await expect(this.results.first()).toContainText(query)
|
||||
await this.comfyPage.page.keyboard.press('Enter')
|
||||
await expect(this.dialog).toBeHidden()
|
||||
await this.comfyPage.page.mouse.click(position.x, position.y)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,24 @@ import type { Locator } from '@playwright/test'
|
||||
|
||||
export class WidgetSelectDropdownFixture {
|
||||
public readonly selection: Locator
|
||||
public readonly trigger: Locator
|
||||
|
||||
constructor(public readonly root: Locator) {
|
||||
this.trigger = root.locator('button:has(> span)').first()
|
||||
this.selection = root.locator('button span span')
|
||||
}
|
||||
|
||||
async open(): Promise<void> {
|
||||
await this.trigger.click()
|
||||
}
|
||||
|
||||
async searchAndSelectTop(popover: Locator, query: string): Promise<void> {
|
||||
await this.open()
|
||||
const searchInput = popover.getByRole('textbox')
|
||||
await searchInput.fill(query)
|
||||
await searchInput.press('Enter')
|
||||
}
|
||||
|
||||
async selectedItem(): Promise<string> {
|
||||
return await this.selection.innerText()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { WidgetSelectDropdownFixture } from '@e2e/fixtures/components/WidgetSelectDropdown'
|
||||
|
||||
/**
|
||||
* Helper for interacting with widgets rendered in app mode (linear view).
|
||||
@@ -24,6 +25,11 @@ export class AppModeWidgetHelper {
|
||||
return this.container.locator(`[data-widget-key="${key}"]`)
|
||||
}
|
||||
|
||||
/** Get a FormDropdown widget by its key (e.g. "10:image"). */
|
||||
getSelectDropdown(key: string): WidgetSelectDropdownFixture {
|
||||
return new WidgetSelectDropdownFixture(this.getWidgetItem(key))
|
||||
}
|
||||
|
||||
/** Fill a textarea widget (e.g. CLIP Text Encode prompt). */
|
||||
async fillTextarea(key: string, value: string) {
|
||||
const widget = this.getWidgetItem(key)
|
||||
|
||||
@@ -62,12 +62,39 @@ export class WorkflowHelper {
|
||||
|
||||
async waitForDraftPersisted() {
|
||||
await this.comfyPage.page.waitForFunction(() =>
|
||||
Object.keys(localStorage).some((k) =>
|
||||
k.startsWith('Comfy.Workflow.Draft.v2:')
|
||||
Object.keys(localStorage).some((key) =>
|
||||
key.startsWith('Comfy.Workflow.Draft.v2:')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** Waits for V2 draft index recency, not payload content freshness. */
|
||||
async waitForDraftIndexUpdatedSince(updatedSince: number) {
|
||||
await this.comfyPage.page.waitForFunction((indexUpdatedSince) => {
|
||||
for (let i = 0; i < window.localStorage.length; i++) {
|
||||
const key = window.localStorage.key(i)
|
||||
if (!key?.startsWith('Comfy.Workflow.DraftIndex.v2:')) continue
|
||||
|
||||
const json = window.localStorage.getItem(key)
|
||||
if (!json) continue
|
||||
|
||||
try {
|
||||
const index = JSON.parse(json)
|
||||
if (
|
||||
typeof index.updatedAt === 'number' &&
|
||||
index.updatedAt >= indexUpdatedSince
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed storage while waiting for persistence.
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}, updatedSince)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the current page and waits for the app to initialize.
|
||||
* Unlike ComfyPage.setup(), this preserves localStorage (drafts) and
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@comfyorg/ingest-types/zod'
|
||||
|
||||
import type {
|
||||
JobDetail,
|
||||
JobStatus,
|
||||
RawJobListItem,
|
||||
zJobsListResponse
|
||||
@@ -182,6 +183,24 @@ export class JobsRouteMocker {
|
||||
return await this.mockPostManageRoute('history', zHistoryManageRequest, {})
|
||||
}
|
||||
|
||||
async mockDeleteHistory(): Promise<HistoryManageRequest[]> {
|
||||
return await this.mockPostManageRoute('history', zHistoryManageRequest, {})
|
||||
}
|
||||
|
||||
async mockJobDetail(jobId: string, detail: JobDetail): Promise<void> {
|
||||
await this.page.route(
|
||||
(url) => url.pathname.endsWith(`/api/jobs/${encodeURIComponent(jobId)}`),
|
||||
async (requestRoute) => {
|
||||
if (requestRoute.request().method().toUpperCase() !== 'GET') {
|
||||
await requestRoute.fallback()
|
||||
return
|
||||
}
|
||||
|
||||
await requestRoute.fulfill({ json: detail })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private async mockPostManageRoute<TRequest>(
|
||||
type: 'queue' | 'history',
|
||||
requestSchema: z.ZodType<TRequest>,
|
||||
|
||||
@@ -2,16 +2,10 @@ import {
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import { WidgetSelectDropdownFixture } from '@e2e/fixtures/components/WidgetSelectDropdown'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
|
||||
test.describe('App mode usage', () => {
|
||||
test('Drag and Drop', async ({ comfyPage, comfyFiles }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
test('Drag and Drop @vue-nodes', async ({ comfyPage, comfyFiles }) => {
|
||||
const { centerPanel } = comfyPage.appMode
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['3', 'seed']])
|
||||
await expect(centerPanel, 'Enter app mode').toBeVisible()
|
||||
@@ -25,15 +19,12 @@ test.describe('App mode usage', () => {
|
||||
//prep a load image
|
||||
await test.step('Add a load image node', async () => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.page.mouse.dblclick(200, 200, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Image')
|
||||
await comfyPage.searchBoxV2.addNode('Load Image')
|
||||
const loadImage = await comfyPage.vueNodes.getNodeLocator('10')
|
||||
await expect(loadImage).toBeVisible()
|
||||
})
|
||||
|
||||
const imageInput = new WidgetSelectDropdownFixture(
|
||||
comfyPage.appMode.linearWidgets.locator('.lg-node-widget')
|
||||
)
|
||||
const imageInput = comfyPage.appMode.widgets.getSelectDropdown('10:image')
|
||||
|
||||
await test.step('Enter app mode with image input', async () => {
|
||||
await comfyPage.appMode.enterAppModeWithInputs([['10', 'image']])
|
||||
@@ -107,6 +98,45 @@ test.describe('App mode usage', () => {
|
||||
//verify values are consistent with litegraph
|
||||
})
|
||||
|
||||
test('FormDropdown search Enter selects the top filtered item', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.appMode.enableLinearMode()
|
||||
const loadImageNode = await comfyPage.nodeOps.addNode('LoadImage')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const fileComboWidget = await loadImageNode.getWidget(0)
|
||||
const targetImage = String(await fileComboWidget.getValue())
|
||||
const initialImage = 'not-selected.png'
|
||||
await comfyPage.page.evaluate(
|
||||
([nodeId, value]) => {
|
||||
const node = window.app!.graph!.getNodeById(nodeId)
|
||||
const widget = node?.widgets?.[0]
|
||||
if (!widget) throw new Error(`Image widget not found: ${nodeId}`)
|
||||
|
||||
widget.value = value
|
||||
},
|
||||
[loadImageNode.id, initialImage] as const
|
||||
)
|
||||
await expect.poll(() => fileComboWidget.getValue()).toBe(initialImage)
|
||||
|
||||
await comfyPage.appMode.enterAppModeWithInputs([
|
||||
[String(loadImageNode.id), 'image']
|
||||
])
|
||||
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
||||
const imageInput = comfyPage.appMode.widgets.getSelectDropdown(
|
||||
`${loadImageNode.id}:image`
|
||||
)
|
||||
const popover = comfyPage.appMode.imagePickerPopover
|
||||
|
||||
await expect(imageInput.root).toBeVisible()
|
||||
await imageInput.searchAndSelectTop(popover, targetImage)
|
||||
|
||||
await expect(popover).toBeHidden()
|
||||
await expect(imageInput.selection).toHaveText(targetImage)
|
||||
await expect.poll(() => fileComboWidget.getValue()).toBe(targetImage)
|
||||
})
|
||||
|
||||
test.describe('Mobile', { tag: ['@mobile'] }, () => {
|
||||
test('panel navigation', async ({ comfyPage }) => {
|
||||
const { mobile } = comfyPage.appMode
|
||||
|
||||
@@ -75,33 +75,28 @@ test.describe('App mode builder selection', () => {
|
||||
})
|
||||
|
||||
test('Marks canvas readOnly', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
|
||||
await comfyPage.page.mouse.dblclick(100, 100, { delay: 5 })
|
||||
await comfyPage.searchBoxV2.openByDoubleClickCanvas()
|
||||
await expect(
|
||||
comfyPage.searchBox.input,
|
||||
comfyPage.searchBoxV2.input,
|
||||
'Canvas is initially editable'
|
||||
).toHaveCount(1)
|
||||
).toBeVisible()
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
|
||||
await comfyPage.appMode.enterBuilder()
|
||||
await comfyPage.appMode.steps.goToInputs()
|
||||
|
||||
await comfyPage.page.mouse.dblclick(100, 100, { delay: 5 })
|
||||
await comfyPage.searchBoxV2.openByDoubleClickCanvas()
|
||||
await expect(
|
||||
comfyPage.searchBox.input,
|
||||
comfyPage.searchBoxV2.input,
|
||||
'Entering builder makes the canvas readonly'
|
||||
).toHaveCount(0)
|
||||
).toBeHidden()
|
||||
|
||||
await comfyPage.page.keyboard.press('Space')
|
||||
await comfyPage.page.mouse.dblclick(100, 100, { delay: 5 })
|
||||
await comfyPage.searchBoxV2.openByDoubleClickCanvas()
|
||||
await expect(
|
||||
comfyPage.searchBox.input,
|
||||
comfyPage.searchBoxV2.input,
|
||||
'Canvas remains readonly after pressing space'
|
||||
).toHaveCount(0)
|
||||
).toBeHidden()
|
||||
|
||||
const ksampler = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Node container has conditional pointer-events:none that blocks actionability
|
||||
@@ -112,10 +107,10 @@ test.describe('App mode builder selection', () => {
|
||||
).toBeHidden()
|
||||
|
||||
await comfyPage.page.keyboard.press('Escape')
|
||||
await comfyPage.page.mouse.dblclick(100, 100, { delay: 5 })
|
||||
await comfyPage.searchBoxV2.openByDoubleClickCanvas()
|
||||
await expect(
|
||||
comfyPage.searchBox.input,
|
||||
comfyPage.searchBoxV2.input,
|
||||
'Canvas is no longer readonly after exiting'
|
||||
).toHaveCount(1)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -131,13 +131,10 @@ test.describe('App mode dropdown clipping', { tag: '@ui' }, () => {
|
||||
el.scrollTo({ top: el.scrollHeight, behavior: 'instant' })
|
||||
)
|
||||
|
||||
// Click the FormDropdown trigger button for the image widget.
|
||||
// The button emits 'select-click' which toggles the Popover.
|
||||
const imageRow = widgetList.locator(
|
||||
'div:has(> div > span:text-is("image"))'
|
||||
const imageInput = comfyPage.appMode.widgets.getSelectDropdown(
|
||||
`${loadImageId}:image`
|
||||
)
|
||||
const dropdownButton = imageRow.locator('button:has(> span)').first()
|
||||
await dropdownButton.click()
|
||||
await imageInput.open()
|
||||
|
||||
// The unstyled PrimeVue Popover renders with role="dialog".
|
||||
// Locate the one containing the image grid (filter buttons like "All", "Inputs").
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
comfyExpect as expect
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
@@ -43,4 +44,45 @@ test.describe('Linear Mode', { tag: '@ui' }, () => {
|
||||
await expect(comfyPage.page.getByTestId('linear-widgets')).toBeVisible()
|
||||
await expect(comfyPage.canvas).toBeHidden()
|
||||
})
|
||||
|
||||
test('Spinner persists until workflow loaded', async ({
|
||||
page,
|
||||
request
|
||||
}, testInfo) => {
|
||||
const comfyPage = new ComfyPage(page, request)
|
||||
const { parallelIndex } = testInfo
|
||||
const username = `playwright-test-${parallelIndex}`
|
||||
const userId = await comfyPage.setupUser(username)
|
||||
comfyPage.userIds[parallelIndex] = userId
|
||||
|
||||
await page.goto(`${comfyPage.url}/api/users`)
|
||||
await page.evaluate((id) => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
localStorage.setItem('Comfy.userId', id)
|
||||
}, comfyPage.id)
|
||||
|
||||
const splash = page.locator('#splash-loader')
|
||||
|
||||
let notifyWorkflowRequested!: () => void
|
||||
const workflowRequested = new Promise<void>(
|
||||
(r) => (notifyWorkflowRequested = r)
|
||||
)
|
||||
let unblockRequest!: () => void
|
||||
const requestUnblocked = new Promise<void>((r) => (unblockRequest = r))
|
||||
|
||||
await page.route('**/templates/default.json', async (route) => {
|
||||
notifyWorkflowRequested()
|
||||
await requestUnblocked
|
||||
return route.continue()
|
||||
})
|
||||
|
||||
await comfyPage.goto({ url: `${comfyPage.url}/?template=default` })
|
||||
await workflowRequested
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
await expect(splash).toBeVisible()
|
||||
unblockRequest()
|
||||
await expect(splash).toBeHidden()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
|
||||
test('Price badge displays on subgraphs @vue-nodes', async ({ comfyPage }) => {
|
||||
const apiNodeName = 'Node With Price Badge'
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
|
||||
const priceBadge = comfyPage.page.locator('.lg-node-header i + span')
|
||||
const apiNode = comfyPage.vueNodes.getNodeByTitle(apiNodeName)
|
||||
@@ -13,9 +12,7 @@ test('Price badge displays on subgraphs @vue-nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.newWorkflowButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.mouse.dblclick(500, 500, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(apiNodeName)
|
||||
await expect(comfyPage.searchBox.input).toBeHidden()
|
||||
await comfyPage.searchBoxV2.addNode(apiNodeName)
|
||||
await expect(apiNode, 'Add partner node').toBeVisible()
|
||||
await expect(apiNode.locator(priceBadge), 'Has price badge').toBeVisible()
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ import type {
|
||||
RawJobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
// Legacy coverage backed by AssetsHelper's shadow backend. New assets-sidebar
|
||||
// browser coverage should use typed route mocks in assetsSidebarTab.spec.ts.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
278
browser_tests/tests/sidebar/assetsSidebarTab.spec.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
createRouteMockJob,
|
||||
jobsRouteFixture,
|
||||
routeMockJobTimestamp
|
||||
} from '@e2e/fixtures/jobsRouteFixture'
|
||||
import type {
|
||||
JobDetail,
|
||||
RawJobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, jobsRouteFixture)
|
||||
|
||||
interface ViewFile {
|
||||
body?: Buffer | string
|
||||
contentType?: string
|
||||
}
|
||||
|
||||
type ViewFilesByName = Readonly<Record<string, ViewFile>>
|
||||
|
||||
const transparentPng = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lwPIRwAAAABJRU5ErkJggg==',
|
||||
'base64'
|
||||
)
|
||||
|
||||
const alphaJob = createRouteMockJob({
|
||||
id: 'alpha',
|
||||
create_time: routeMockJobTimestamp - 1_000,
|
||||
execution_start_time: routeMockJobTimestamp - 1_000,
|
||||
execution_end_time: routeMockJobTimestamp,
|
||||
preview_output: {
|
||||
filename: 'alpha.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
}
|
||||
})
|
||||
|
||||
const betaJob = createRouteMockJob({
|
||||
id: 'beta',
|
||||
create_time: routeMockJobTimestamp - 2_000,
|
||||
execution_start_time: routeMockJobTimestamp - 2_000,
|
||||
execution_end_time: routeMockJobTimestamp,
|
||||
preview_output: {
|
||||
filename: 'beta.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '2',
|
||||
mediaType: 'images'
|
||||
}
|
||||
})
|
||||
|
||||
const multiOutputJob = createRouteMockJob({
|
||||
id: 'multi-output',
|
||||
create_time: routeMockJobTimestamp - 3_000,
|
||||
execution_start_time: routeMockJobTimestamp - 3_000,
|
||||
execution_end_time: routeMockJobTimestamp,
|
||||
preview_output: {
|
||||
filename: 'multi-output-a.png',
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '3',
|
||||
mediaType: 'images'
|
||||
},
|
||||
outputs_count: 2
|
||||
})
|
||||
|
||||
const multiOutputJobDetail: JobDetail = {
|
||||
...multiOutputJob,
|
||||
outputs: {
|
||||
'3': {
|
||||
images: [
|
||||
{
|
||||
filename: 'multi-output-a.png',
|
||||
subfolder: '',
|
||||
type: 'output'
|
||||
},
|
||||
{
|
||||
filename: 'multi-output-b.png',
|
||||
subfolder: '',
|
||||
type: 'output'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const generatedJobs: RawJobListItem[] = [alphaJob, betaJob]
|
||||
|
||||
const viewFiles = {
|
||||
'alpha.png': {},
|
||||
'beta.png': {},
|
||||
'imported.png': {},
|
||||
'multi-output-a.png': {},
|
||||
'multi-output-b.png': {}
|
||||
}
|
||||
|
||||
async function mockInputFiles(page: Page, files: readonly string[]) {
|
||||
await page.route('**/internal/files/input**', async (route) => {
|
||||
if (route.request().method().toUpperCase() !== 'GET') {
|
||||
await route.fallback()
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({ json: [...files] })
|
||||
})
|
||||
}
|
||||
|
||||
async function mockViewFiles(page: Page, filesByName: ViewFilesByName) {
|
||||
await page.route('**/api/view**', async (route) => {
|
||||
if (route.request().method().toUpperCase() !== 'GET') {
|
||||
await route.fallback()
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(route.request().url())
|
||||
const filename = url.searchParams.get('filename')
|
||||
if (!filename) {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
json: { error: 'Missing filename' } satisfies { error: string }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const file = filesByName[filename]
|
||||
if (!file) {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
json: {
|
||||
error: `Unknown filename: ${filename}`
|
||||
} satisfies { error: string }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
body: file.body ?? transparentPng,
|
||||
contentType: file.contentType ?? 'image/png'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('FE-130 assets sidebar route mocks', () => {
|
||||
test.beforeEach(async ({ jobsRoutes, page }) => {
|
||||
await jobsRoutes.mockJobsQueue([])
|
||||
await jobsRoutes.mockJobsHistory(generatedJobs)
|
||||
await mockInputFiles(page, ['imported.png'])
|
||||
await mockViewFiles(page, viewFiles)
|
||||
})
|
||||
|
||||
test('renders generated and imported assets with image previews', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
|
||||
await comfyPage.setup()
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.getAssetCardByName('alpha')).toBeVisible()
|
||||
await expect(tab.getAssetCardByName('beta')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.getByRole('img', { name: 'alpha.png' })
|
||||
).toHaveJSProperty('naturalWidth', 1)
|
||||
|
||||
await tab.switchToImported()
|
||||
|
||||
await expect(tab.getAssetCardByName('imported')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.getByRole('img', { name: 'imported.png' })
|
||||
).toHaveJSProperty('naturalWidth', 1)
|
||||
})
|
||||
|
||||
test('opens previews for generated and imported images', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
|
||||
await comfyPage.setup()
|
||||
await tab.open()
|
||||
|
||||
await comfyPage.page.getByRole('img', { name: 'alpha.png' }).dblclick()
|
||||
await expect(comfyPage.mediaLightbox.root).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.mediaLightbox.root.getByRole('img', {
|
||||
name: 'alpha.png'
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
await comfyPage.mediaLightbox.closeButton.click()
|
||||
await expect(comfyPage.mediaLightbox.root).toBeHidden()
|
||||
|
||||
await tab.switchToImported()
|
||||
|
||||
await comfyPage.page.getByRole('img', { name: 'imported.png' }).dblclick()
|
||||
await expect(comfyPage.mediaLightbox.root).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.mediaLightbox.root.getByRole('img', {
|
||||
name: 'imported.png'
|
||||
})
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows footer actions for single and multiple generated selections', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
|
||||
await comfyPage.setup()
|
||||
await tab.open()
|
||||
|
||||
await tab.getAssetCardByName('alpha').click()
|
||||
await expect(tab.selectionCountButton).toHaveText(/Assets Selected:\s*1\b/)
|
||||
await expect(tab.deleteSelectedButton).toBeVisible()
|
||||
await expect(tab.downloadSelectedButton).toBeVisible()
|
||||
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await tab.getAssetCardByName('beta').click()
|
||||
await comfyPage.page.keyboard.up('Control')
|
||||
|
||||
await expect(tab.selectionCountButton).toHaveText(/Assets Selected:\s*2\b/)
|
||||
await expect(tab.deleteSelectedButton).toBeVisible()
|
||||
await expect(tab.downloadSelectedButton).toBeVisible()
|
||||
})
|
||||
|
||||
test('loads full generated job outputs from job detail', async ({
|
||||
comfyPage,
|
||||
jobsRoutes
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
|
||||
await jobsRoutes.mockJobsHistory([multiOutputJob])
|
||||
await jobsRoutes.mockJobDetail('multi-output', multiOutputJobDetail)
|
||||
|
||||
await comfyPage.setup()
|
||||
await tab.open()
|
||||
|
||||
await tab
|
||||
.getAssetCardByName('multi-output-a')
|
||||
.getByRole('button', { name: 'See more outputs' })
|
||||
.click()
|
||||
|
||||
await expect(tab.backToAssetsButton).toBeVisible()
|
||||
await expect(tab.getAssetCardByName('multi-output-b')).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.page.getByRole('img', { name: 'multi-output-b.png' })
|
||||
).toHaveJSProperty('naturalWidth', 1)
|
||||
})
|
||||
|
||||
test('deletes a generated output asset through explicit history refresh', async ({
|
||||
comfyPage,
|
||||
jobsRoutes
|
||||
}) => {
|
||||
const tab = comfyPage.menu.assetsTab
|
||||
|
||||
await comfyPage.setup()
|
||||
await tab.open()
|
||||
await expect(tab.getAssetCardByName('alpha')).toBeVisible()
|
||||
|
||||
const deleteRequests = await jobsRoutes.mockDeleteHistory()
|
||||
await jobsRoutes.mockJobsHistory([betaJob])
|
||||
|
||||
await tab.getAssetCardByName('alpha').click({ button: 'right' })
|
||||
await tab.contextMenuItem('Delete').click()
|
||||
await comfyPage.confirmDialog.delete.click()
|
||||
|
||||
await expect.poll(() => deleteRequests).toHaveLength(1)
|
||||
expect(deleteRequests[0]).toEqual({ delete: ['alpha'] })
|
||||
await expect(tab.getAssetCardByName('alpha')).toHaveCount(0)
|
||||
await expect(comfyPage.toast.toastSuccesses).toContainText(
|
||||
'Asset deleted successfully'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -129,4 +129,26 @@ test.describe('Node library sidebar V2', () => {
|
||||
await expect(tab.nodePreview, 'Preview displays on hover').toBeVisible()
|
||||
await expect(tab.nodePreview).toContainText('Inverts the image')
|
||||
})
|
||||
|
||||
test('Click-to-place from sidebar selects the newly added node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.nodeLibraryTabV2
|
||||
await comfyPage.nodeOps.clearGraph()
|
||||
await tab.expandFolder('sampling')
|
||||
|
||||
const canvasBox = (await comfyPage.canvas.boundingBox())!
|
||||
const target = {
|
||||
x: canvasBox.width / 2,
|
||||
y: canvasBox.height / 2
|
||||
}
|
||||
|
||||
await tab.getNode('KSampler (Advanced)').click()
|
||||
await comfyPage.canvas.click({ position: target })
|
||||
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
||||
await expect
|
||||
.poll(() => comfyPage.nodeOps.getSelectedGraphNodesCount())
|
||||
.toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,10 +5,6 @@ import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Subgraph Clipboard Operations', () => {
|
||||
@@ -58,8 +54,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
|
||||
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
|
||||
await subgraphNode.navigateIntoSubgraph()
|
||||
|
||||
await comfyPage.canvasOps.doubleClick()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Note')
|
||||
await comfyPage.searchBoxV2.addNode('Note')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const initialCount = await comfyPage.subgraph.getNodeCount()
|
||||
|
||||
@@ -745,20 +745,19 @@ test('Link already promoted widget @vue-nodes', async ({ comfyPage }) => {
|
||||
})
|
||||
|
||||
test('Can promote multiple previews @vue-nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
await comfyPage.menu.topbar.newWorkflowButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await test.step('Add and rename a Load Image node', async () => {
|
||||
await comfyPage.page.mouse.dblclick(300, 300, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Image')
|
||||
const position = { x: 300, y: 300 }
|
||||
await comfyPage.searchBoxV2.addNode('Load Image', { position })
|
||||
const loadImage = await comfyPage.vueNodes.getFixtureByTitle('Load Image')
|
||||
await loadImage.setTitle('Character Reference')
|
||||
})
|
||||
|
||||
await test.step('Add a second Load Image node', async () => {
|
||||
await comfyPage.page.mouse.dblclick(600, 300, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Image')
|
||||
const position = { x: 600, y: 300 }
|
||||
await comfyPage.searchBoxV2.addNode('Load Image', { position })
|
||||
})
|
||||
|
||||
await test.step('Convert both nodes to subgraph', async () => {
|
||||
|
||||
@@ -1082,17 +1082,10 @@ test.describe(
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.NodeSearchBoxImpl',
|
||||
'v1 (legacy)'
|
||||
)
|
||||
|
||||
// Setup workflow with a KSampler node
|
||||
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.nodeOps.waitForGraphNodes(0)
|
||||
await comfyPage.command.executeCommand('Workspace.SearchBox.Toggle')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await comfyPage.searchBoxV2.addNode('KSampler')
|
||||
await comfyPage.nodeOps.waitForGraphNodes(1)
|
||||
|
||||
// Convert the KSampler node to a subgraph
|
||||
|
||||
@@ -19,3 +19,19 @@ test('Can display a slot mismatched from widget type', async ({
|
||||
await expect(width.locator('path[fill*="INT"]')).toBeVisible()
|
||||
await expect(width.locator('path[fill*="FLOAT"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('MatchType updates output color @vue-nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.newWorkflowButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.searchBoxV2.addNode('Load Image')
|
||||
const loadImage = await comfyPage.vueNodes.getFixtureByTitle('Load Image')
|
||||
await comfyPage.searchBoxV2.addNode('Switch', {
|
||||
position: { x: 600, y: 200 }
|
||||
})
|
||||
const switchNode = await comfyPage.vueNodes.getFixtureByTitle('switch')
|
||||
|
||||
await loadImage.getSlot('MASK').dragTo(switchNode.getSlot('on_false'))
|
||||
const slotEl = switchNode.getSlot('output').locator('.slot-dot')
|
||||
await expect.poll(() => slotEl.getAttribute('style')).toContain('MASK')
|
||||
})
|
||||
|
||||
@@ -9,8 +9,6 @@ const file1 = 'workflow.mp4' as const
|
||||
const file2 = 'workflow.webm' as const
|
||||
|
||||
test('@vue-nodes Load Video', async ({ comfyPage, comfyFiles }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
|
||||
const loadVideoNode = comfyPage.vueNodes.getNodeByTitle('Load Video')
|
||||
const loadVideo = new VideoPreview(loadVideoNode)
|
||||
|
||||
@@ -18,9 +16,7 @@ test('@vue-nodes Load Video', async ({ comfyPage, comfyFiles }) => {
|
||||
await comfyPage.menu.topbar.newWorkflowButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.mouse.dblclick(500, 300, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Video')
|
||||
|
||||
await comfyPage.searchBoxV2.addNode('Load Video')
|
||||
await expect(loadVideoNode).toHaveCount(1)
|
||||
await expect(loadVideoNode).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
test('@vue-nodes Audio Widget', async ({ comfyPage, comfyFiles }) => {
|
||||
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
|
||||
|
||||
const loadAudioNode = comfyPage.vueNodes.getNodeByTitle('Load Audio')
|
||||
const audioPreview = new AudioPreview(loadAudioNode)
|
||||
|
||||
@@ -14,9 +12,7 @@ test('@vue-nodes Audio Widget', async ({ comfyPage, comfyFiles }) => {
|
||||
await comfyPage.menu.topbar.newWorkflowButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
//await comfyPage.canvasOps.doubleClick()
|
||||
await comfyPage.page.mouse.dblclick(500, 500, { delay: 5 })
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('Load Audio')
|
||||
await comfyPage.searchBoxV2.addNode('Load Audio')
|
||||
await expect(loadAudioNode).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,103 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { fitToViewInstant } from '@e2e/fixtures/utils/fitToView'
|
||||
|
||||
const generateUniqueFilename = () =>
|
||||
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||
|
||||
const waitForWorkflowTabState = async (comfyPage: ComfyPage, minPaths = 2) => {
|
||||
await comfyPage.page.waitForFunction((expectedMinPaths) => {
|
||||
let hasActivePath = false
|
||||
let hasOpenPaths = false
|
||||
|
||||
for (let i = 0; i < window.sessionStorage.length; i++) {
|
||||
const key = window.sessionStorage.key(i)
|
||||
if (key?.startsWith('Comfy.Workflow.ActivePath:')) {
|
||||
hasActivePath = true
|
||||
}
|
||||
if (!key?.startsWith('Comfy.Workflow.OpenPaths:')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const raw = window.sessionStorage.getItem(key)
|
||||
if (!raw) continue
|
||||
|
||||
try {
|
||||
const state = JSON.parse(raw) as { paths?: unknown[] }
|
||||
hasOpenPaths =
|
||||
Array.isArray(state.paths) && state.paths.length >= expectedMinPaths
|
||||
if (hasActivePath && hasOpenPaths) return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return hasActivePath && hasOpenPaths
|
||||
}, minPaths)
|
||||
}
|
||||
|
||||
type NodeRef = NonNullable<
|
||||
Awaited<ReturnType<ComfyPage['nodeOps']['getFirstNodeRef']>>
|
||||
>
|
||||
|
||||
const getRequiredFirstNodeRef = async (
|
||||
comfyPage: ComfyPage,
|
||||
message: string
|
||||
): Promise<NodeRef> => {
|
||||
const node = await comfyPage.nodeOps.getFirstNodeRef()
|
||||
expect(node, message).toBeDefined()
|
||||
if (!node) throw new Error(message)
|
||||
return node
|
||||
}
|
||||
|
||||
const makeActivePathStale = async (
|
||||
comfyPage: ComfyPage,
|
||||
staleWorkflowName: string,
|
||||
activeWorkflowName: string
|
||||
) => {
|
||||
// Intentionally desync ActivePath from OpenPaths to exercise stale pointer recovery.
|
||||
await comfyPage.page.evaluate(
|
||||
([staleName, activeName]) => {
|
||||
const findStorageKey = (prefix: string) => {
|
||||
for (let i = 0; i < window.sessionStorage.length; i++) {
|
||||
const key = window.sessionStorage.key(i)
|
||||
if (key?.startsWith(prefix)) return key
|
||||
}
|
||||
throw new Error(`Missing ${prefix} persistence key`)
|
||||
}
|
||||
|
||||
const activePathKey = findStorageKey('Comfy.Workflow.ActivePath:')
|
||||
const openPathsKey = findStorageKey('Comfy.Workflow.OpenPaths:')
|
||||
const activePointer = JSON.parse(
|
||||
window.sessionStorage.getItem(activePathKey)!
|
||||
) as { path: string }
|
||||
const openPointer = JSON.parse(
|
||||
window.sessionStorage.getItem(openPathsKey)!
|
||||
) as { paths: string[]; activeIndex: number }
|
||||
const pathForName = (name: string) => {
|
||||
const path = openPointer.paths.find((candidate) =>
|
||||
candidate.endsWith(`${name}.json`)
|
||||
)
|
||||
if (!path) throw new Error(`Missing stored path for ${name}`)
|
||||
return path
|
||||
}
|
||||
|
||||
const stalePath = pathForName(staleName)
|
||||
const activePath = pathForName(activeName)
|
||||
activePointer.path = stalePath
|
||||
openPointer.paths = [stalePath, activePath]
|
||||
openPointer.activeIndex = 1
|
||||
|
||||
window.sessionStorage.setItem(
|
||||
activePathKey,
|
||||
JSON.stringify(activePointer)
|
||||
)
|
||||
window.sessionStorage.setItem(openPathsKey, JSON.stringify(openPointer))
|
||||
},
|
||||
[staleWorkflowName, activeWorkflowName]
|
||||
)
|
||||
}
|
||||
|
||||
async function getNodeOutputImageCount(
|
||||
comfyPage: ComfyPage,
|
||||
@@ -103,9 +200,11 @@ test.describe('Workflow Persistence', () => {
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('outputs-test')
|
||||
|
||||
const firstNode = await comfyPage.nodeOps.getFirstNodeRef()
|
||||
expect(firstNode).toBeTruthy()
|
||||
const nodeId = String(firstNode!.id)
|
||||
const firstNode = await getRequiredFirstNodeRef(
|
||||
comfyPage,
|
||||
'First node should be available after loading the default workflow'
|
||||
)
|
||||
const nodeId = String(firstNode.id)
|
||||
|
||||
// Simulate node outputs as if execution completed
|
||||
await comfyPage.page.evaluate((id) => {
|
||||
@@ -382,6 +481,59 @@ test.describe('Workflow Persistence', () => {
|
||||
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toBe(nodeCountB)
|
||||
})
|
||||
|
||||
test('Restores saved workflow drafts from inactive restored tabs', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.settings.setSetting('Comfy.Workflow.Persist', true)
|
||||
await comfyPage.settings.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Topbar'
|
||||
)
|
||||
|
||||
const workflowA = generateUniqueFilename()
|
||||
const workflowB = generateUniqueFilename()
|
||||
|
||||
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
|
||||
await fitToViewInstant(comfyPage)
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
||||
|
||||
const firstNode = await getRequiredFirstNodeRef(
|
||||
comfyPage,
|
||||
'First node should be available after loading single_ksampler'
|
||||
)
|
||||
await firstNode.centerOnNode()
|
||||
const draftSaveStartedAt = Date.now()
|
||||
await firstNode.toggleCollapse()
|
||||
expect(await firstNode.isCollapsed()).toBe(true)
|
||||
await comfyPage.workflow.waitForDraftIndexUpdatedSince(draftSaveStartedAt)
|
||||
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
||||
await waitForWorkflowTabState(comfyPage)
|
||||
await makeActivePathStale(comfyPage, workflowA, workflowB)
|
||||
|
||||
await comfyPage.workflow.reloadAndWaitForApp()
|
||||
await expect
|
||||
.poll(() => comfyPage.menu.topbar.getActiveTabName())
|
||||
.toBe(workflowB)
|
||||
|
||||
const tabs = await comfyPage.menu.topbar.getTabNames()
|
||||
expect(tabs).toEqual(expect.arrayContaining([workflowA, workflowB]))
|
||||
expect(tabs.indexOf(workflowA)).toBeLessThan(tabs.indexOf(workflowB))
|
||||
|
||||
await comfyPage.menu.topbar.getWorkflowTab(workflowA).click()
|
||||
await comfyPage.workflow.waitForWorkflowIdle()
|
||||
await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
|
||||
|
||||
const restoredNode = await getRequiredFirstNodeRef(
|
||||
comfyPage,
|
||||
'Restored node should be available after switching back to workflow A'
|
||||
)
|
||||
expect(await restoredNode.isCollapsed()).toBe(true)
|
||||
await expect(comfyPage.toast.toastErrors).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Closing an inactive tab with save preserves its own content', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ Date: 2025-08-25
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
Accepted (Nx tooling choice superseded by [ADR-0010](0010-remove-nx-orchestration.md))
|
||||
|
||||
<!-- [Proposed | Accepted | Rejected | Deprecated | Superseded by [ADR-NNNN](NNNN-title.md)] -->
|
||||
|
||||
@@ -31,6 +31,8 @@ For more information on Monorepos, check out [monorepo.tools](https://monorepo.t
|
||||
For monorepo management, I'd probably go with [Nx](https://nx.dev/), but I could be conviced otherwise.
|
||||
There's a [whole list here](https://monorepo.tools/#tools-review) if you're interested.
|
||||
|
||||
> **Update:** The Nx tooling choice has since been reversed. See [ADR-0010: Remove Nx Orchestration](0010-remove-nx-orchestration.md) for the migration to direct pnpm workspace scripts and native tool CLIs.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
92
docs/adr/0010-remove-nx-orchestration.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# 10. Remove Nx Orchestration
|
||||
|
||||
Date: 2026-05-19
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
<!-- [Proposed | Accepted | Rejected | Deprecated | Superseded by [ADR-NNNN](NNNN-title.md)] -->
|
||||
|
||||
## Context
|
||||
|
||||
[ADR-0002](0002-monorepo-conversion.md) adopted [Nx](https://nx.dev/) as a tooling option for managing the
|
||||
ComfyUI Frontend monorepo on top of pnpm workspaces. Nx was introduced as task
|
||||
orchestration to coordinate builds, tests, lints, and types across the apps and
|
||||
packages workspaces.
|
||||
|
||||
In practice, Nx provided little value beyond what pnpm workspaces and the
|
||||
underlying native tool CLIs (Vite, Vitest, Playwright, ESLint, oxlint, oxfmt,
|
||||
TypeScript) already offer:
|
||||
|
||||
- pnpm's `--filter` and `--recursive` flags already provide topological,
|
||||
parallel, and selective execution across workspaces.
|
||||
- Each underlying tool already has fast, well-supported caching (Vite, Vitest,
|
||||
ESLint, oxlint, TS incremental builds, etc.).
|
||||
- Nx added an extra configuration surface (`nx.json`, `.nxignore`, per-package
|
||||
`nx` blocks), an extra cache layer, an extra `node_modules/.cache/nx`
|
||||
artifact, and an extra CI dimension to debug.
|
||||
- Contributors and AI agents had to learn the Nx mental model in addition to
|
||||
pnpm and the individual tool CLIs.
|
||||
- The Nx daemon and remote-cache features were not in use, so the runtime
|
||||
benefit was limited to local task graph caching, which is largely redundant
|
||||
with the per-tool caches.
|
||||
|
||||
The cost (configuration, mental overhead, surprise behavior, occasional
|
||||
cache-related failures) exceeded the benefit.
|
||||
|
||||
## Decision
|
||||
|
||||
Remove Nx from the repository and run monorepo tasks using:
|
||||
|
||||
- pnpm workspace scripts (`pnpm -r run <script>`,
|
||||
`pnpm --filter <pkg> run <script>`).
|
||||
- Each tool's native CLI (Vite, Vitest, Playwright, ESLint, oxlint, oxfmt,
|
||||
`vue-tsc`, etc.) invoked directly from the relevant workspace.
|
||||
|
||||
Concretely, this change:
|
||||
|
||||
- Deletes `nx.json` and `.nxignore`.
|
||||
- Removes `nx` entries from root and per-package `package.json` files (the
|
||||
`nx` block on each `package.json`, the dev dependency, and Nx-specific
|
||||
scripts).
|
||||
- Removes `nx`-related entries from `pnpm-workspace.yaml`'s `allowBuilds`.
|
||||
- Rewrites the affected CI workflows (`.github/workflows/ci-tests-e2e.yaml`,
|
||||
`.github/workflows/release-draft-create.yaml`) to call pnpm/native CLIs
|
||||
directly.
|
||||
- Updates `AGENTS.md`, `TROUBLESHOOTING.md`, and
|
||||
[ADR-0002](0002-monorepo-conversion.md) to reflect the new tooling story.
|
||||
- Cleans up Nx-specific lint/format/ignore rules in `.oxlintrc.json`,
|
||||
`eslint.config.ts`, `vite.config.mts`, and `.gitignore`.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Fewer moving parts: no `nx.json`, no `.nx/` cache, no Nx daemon, no
|
||||
Nx-specific scripts to maintain.
|
||||
- Easier onboarding for contributors and AI agents: pnpm + each tool's CLI is
|
||||
the only required knowledge.
|
||||
- CI logs and failures are easier to read because tasks run directly under the
|
||||
tool that owns them, instead of being wrapped by Nx.
|
||||
- Faster, more predictable cache invalidation behavior — each tool owns its
|
||||
own cache and we no longer hit Nx-cache edge cases.
|
||||
- Smaller dependency tree (~2k fewer lines in `pnpm-lock.yaml`).
|
||||
|
||||
### Negative
|
||||
|
||||
- We lose Nx's unified task graph and project graph commands; coordination
|
||||
across workspaces now relies on pnpm filters and explicit script wiring.
|
||||
- We lose Nx's remote/distributed caching as a future option without
|
||||
re-adopting Nx (or a comparable tool like Turborepo).
|
||||
- Contributors who already knew Nx workflows need to relearn the equivalent
|
||||
pnpm invocations.
|
||||
|
||||
## Notes
|
||||
|
||||
- The migration is purely a tooling change; no application behavior, public
|
||||
API, or build output changes.
|
||||
- If we later need more sophisticated task orchestration (e.g. distributed
|
||||
remote cache, fine-grained affected-graph queries), revisit this decision and
|
||||
evaluate Nx, Turborepo, or Moon at that time, with concrete CI/perf data to
|
||||
justify the additional complexity.
|
||||
@@ -8,16 +8,18 @@ An Architecture Decision Record captures an important architectural decision mad
|
||||
|
||||
## ADR Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
| -------------------------------------------------------- | ---------------------------------------- | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
|
||||
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
|
||||
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
|
||||
| ADR | Title | Status | Date |
|
||||
| ----------------------------------------------------------- | ------------------------------------------- | -------- | ---------- |
|
||||
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
|
||||
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
|
||||
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
|
||||
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
|
||||
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
|
||||
| [0006](0006-primitive-node-copy-paste-lifecycle.md) | PrimitiveNode Copy/Paste Lifecycle | Proposed | 2026-02-22 |
|
||||
| [0007](0007-node-execution-output-passthrough-schema.md) | NodeExecutionOutput Passthrough Schema | Accepted | 2026-03-11 |
|
||||
| [0008](0008-entity-component-system.md) | Entity Component System | Proposed | 2026-03-23 |
|
||||
| [0009](0009-subgraph-promoted-widgets-use-linked-inputs.md) | Subgraph Promoted Widgets Use Linked Inputs | Proposed | 2026-05-05 |
|
||||
| [0010](0010-remove-nx-orchestration.md) | Remove Nx Orchestration | Accepted | 2026-05-19 |
|
||||
|
||||
## Creating a New ADR
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ export default defineConfig([
|
||||
{
|
||||
ignores: [
|
||||
'.i18nrc.cjs',
|
||||
'.nx/*',
|
||||
'**/vite.config.*.timestamp*',
|
||||
'**/vitest.config.*.timestamp*',
|
||||
'components.d.ts',
|
||||
|
||||
41
nx.json
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||
"plugins": [
|
||||
{
|
||||
"plugin": "@nx/eslint/plugin",
|
||||
"options": {
|
||||
"targetName": "lint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"plugin": "@nx/storybook/plugin",
|
||||
"options": {
|
||||
"serveStorybookTargetName": "storybook",
|
||||
"buildStorybookTargetName": "build-storybook",
|
||||
"testStorybookTargetName": "test-storybook",
|
||||
"staticStorybookTargetName": "static-storybook"
|
||||
}
|
||||
},
|
||||
{
|
||||
"plugin": "@nx/vite/plugin",
|
||||
"options": {
|
||||
"buildTargetName": "build",
|
||||
"testTargetName": "test",
|
||||
"serveTargetName": "serve",
|
||||
"devTargetName": "dev",
|
||||
"previewTargetName": "preview",
|
||||
"serveStaticTargetName": "serve-static",
|
||||
"typecheckTargetName": "typecheck",
|
||||
"buildDepsTargetName": "build-deps",
|
||||
"watchDepsTargetName": "watch-deps"
|
||||
}
|
||||
},
|
||||
{
|
||||
"plugin": "@nx/playwright/plugin",
|
||||
"options": {
|
||||
"targetName": "e2e"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analytics": false
|
||||
}
|
||||
67
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.45.11",
|
||||
"version": "1.45.13",
|
||||
"private": true,
|
||||
"description": "Official front-end implementation of ComfyUI",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -8,20 +8,22 @@
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build:cloud": "cross-env DISTRIBUTION=cloud NODE_OPTIONS='--max-old-space-size=8192' nx build",
|
||||
"build:desktop": "nx build @comfyorg/desktop-ui",
|
||||
"build:cloud": "cross-env DISTRIBUTION=cloud NODE_OPTIONS='--max-old-space-size=8192' vite build --config vite.config.mts",
|
||||
"build:desktop": "pnpm --filter @comfyorg/desktop-ui run build",
|
||||
"build-storybook": "storybook build",
|
||||
"build:types": "cross-env NODE_OPTIONS='--max-old-space-size=8192' nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build:types": "cross-env NODE_OPTIONS='--max-old-space-size=8192' vite build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && vite build --config vite.config.mts",
|
||||
"clean": "pnpm dlx rimraf dist dist-ssr coverage playwright-report blob-report test-results node_modules/.vite apps/desktop-ui/dist apps/website/dist",
|
||||
"clean:all": "pnpm clean && pnpm dlx rimraf node_modules",
|
||||
"size:collect": "node scripts/size-collect.js",
|
||||
"size:report": "node scripts/size-report.js",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve",
|
||||
"dev:desktop": "nx dev @comfyorg/desktop-ui",
|
||||
"dev:electron": "cross-env DISTRIBUTION=desktop nx serve --config vite.electron.config.mts",
|
||||
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
|
||||
"dev": "nx serve",
|
||||
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' vite --config vite.config.mts",
|
||||
"dev:desktop": "pnpm --filter @comfyorg/desktop-ui run dev",
|
||||
"dev:electron": "cross-env DISTRIBUTION=desktop vite --config vite.electron.config.mts",
|
||||
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true vite --config vite.config.mts",
|
||||
"dev": "vite --config vite.config.mts",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"format:check": "oxfmt --check",
|
||||
"format": "oxfmt --write",
|
||||
@@ -34,26 +36,25 @@
|
||||
"lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix",
|
||||
"lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache",
|
||||
"lint": "pnpm stylelint && oxlint src browser_tests --type-aware && eslint src --cache",
|
||||
"lint:desktop": "nx run @comfyorg/desktop-ui:lint",
|
||||
"lint:desktop": "pnpm --filter @comfyorg/desktop-ui run lint",
|
||||
"locale": "lobe-i18n locale",
|
||||
"oxlint": "oxlint src browser_tests --type-aware",
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
"storybook": "nx storybook",
|
||||
"storybook:desktop": "nx run @comfyorg/desktop-ui:storybook",
|
||||
"prepare": "pnpm exec husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "vite preview --config vite.config.mts",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:desktop": "pnpm --filter @comfyorg/desktop-ui run storybook",
|
||||
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
"test:browser": "pnpm exec playwright test",
|
||||
"test:browser:coverage": "cross-env COLLECT_COVERAGE=true pnpm test:browser",
|
||||
"test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173 pnpm test:browser",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:unit": "nx run test",
|
||||
"test:unit": "vitest run",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json",
|
||||
"typecheck:desktop": "nx run @comfyorg/desktop-ui:typecheck",
|
||||
"typecheck:website": "nx run @comfyorg/website:typecheck",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"clean": "nx reset"
|
||||
"typecheck:desktop": "pnpm --filter @comfyorg/desktop-ui run typecheck",
|
||||
"typecheck:website": "pnpm --filter @comfyorg/website run typecheck",
|
||||
"zipdist": "node scripts/zipdist.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "catalog:",
|
||||
@@ -113,7 +114,7 @@
|
||||
"primevue": "catalog:",
|
||||
"reka-ui": "catalog:",
|
||||
"semver": "^7.7.2",
|
||||
"three": "^0.170.0",
|
||||
"three": "catalog:",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"typegpu": "catalog:",
|
||||
"vee-validate": "catalog:",
|
||||
@@ -131,10 +132,6 @@
|
||||
"@eslint/js": "catalog:",
|
||||
"@intlify/eslint-plugin-vue-i18n": "catalog:",
|
||||
"@lobehub/i18n-cli": "catalog:",
|
||||
"@nx/eslint": "catalog:",
|
||||
"@nx/playwright": "catalog:",
|
||||
"@nx/storybook": "catalog:",
|
||||
"@nx/vite": "catalog:",
|
||||
"@pinia/testing": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
@@ -180,7 +177,6 @@
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"monocart-coverage-reports": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"oxfmt": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"oxlint-tsgolint": "catalog:",
|
||||
@@ -211,20 +207,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "24.x",
|
||||
"pnpm": ">=10"
|
||||
"pnpm": ">=11"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"@firebase/util",
|
||||
"core-js",
|
||||
"protobufjs",
|
||||
"sharp",
|
||||
"unrs-resolver",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
"packageManager": "pnpm@11.1.1"
|
||||
}
|
||||
|
||||
@@ -19,12 +19,5 @@
|
||||
"devDependencies": {
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1",
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:design"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.93.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1",
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:types"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,10 +356,7 @@ export type {
|
||||
GetModelFoldersResponse,
|
||||
GetModelFoldersResponses,
|
||||
GetModelPreviewData,
|
||||
GetModelPreviewError,
|
||||
GetModelPreviewErrors,
|
||||
GetModelPreviewResponse,
|
||||
GetModelPreviewResponses,
|
||||
GetModelsInFolderData,
|
||||
GetModelsInFolderError,
|
||||
GetModelsInFolderErrors,
|
||||
@@ -389,8 +386,21 @@ export type {
|
||||
GetNodeReplacementsErrors,
|
||||
GetNodeReplacementsResponse,
|
||||
GetNodeReplacementsResponses,
|
||||
GetOpenapiSpecData,
|
||||
GetOpenapiSpecResponses,
|
||||
GetOAuthAuthorizationServerData,
|
||||
GetOAuthAuthorizationServerError,
|
||||
GetOAuthAuthorizationServerErrors,
|
||||
GetOAuthAuthorizationServerResponse,
|
||||
GetOAuthAuthorizationServerResponses,
|
||||
GetOAuthAuthorizeData,
|
||||
GetOAuthAuthorizeError,
|
||||
GetOAuthAuthorizeErrors,
|
||||
GetOAuthAuthorizeResponse,
|
||||
GetOAuthAuthorizeResponses,
|
||||
GetOAuthProtectedResourceData,
|
||||
GetOAuthProtectedResourceError,
|
||||
GetOAuthProtectedResourceErrors,
|
||||
GetOAuthProtectedResourceResponse,
|
||||
GetOAuthProtectedResourceResponses,
|
||||
GetPaymentPortalData,
|
||||
GetPaymentPortalError,
|
||||
GetPaymentPortalErrors,
|
||||
@@ -427,11 +437,11 @@ export type {
|
||||
GetSecretErrors,
|
||||
GetSecretResponse,
|
||||
GetSecretResponses,
|
||||
GetSettingByKeyData,
|
||||
GetSettingByKeyError,
|
||||
GetSettingByKeyErrors,
|
||||
GetSettingByKeyResponse,
|
||||
GetSettingByKeyResponses,
|
||||
GetSettingByIdData,
|
||||
GetSettingByIdError,
|
||||
GetSettingByIdErrors,
|
||||
GetSettingByIdResponse,
|
||||
GetSettingByIdResponses,
|
||||
GetStaticExtensionsData,
|
||||
GetStaticExtensionsErrors,
|
||||
GetStaticExtensionsResponses,
|
||||
@@ -447,6 +457,7 @@ export type {
|
||||
GetTaskResponses,
|
||||
GetTemplateProxyData,
|
||||
GetTemplateProxyErrors,
|
||||
GetTemplateProxyResponses,
|
||||
GetUserData,
|
||||
GetUserdataData,
|
||||
GetUserdataError,
|
||||
@@ -642,6 +653,17 @@ export type {
|
||||
MoveUserdataFileResponse,
|
||||
MoveUserdataFileResponses,
|
||||
NodeInfo,
|
||||
OAuthAuthorizationServerMetadata,
|
||||
OAuthAuthorizeRedirectResponse,
|
||||
OAuthConsentChallenge,
|
||||
OAuthConsentChallengeWorkspace,
|
||||
OAuthProtectedResourceMetadata,
|
||||
OAuthRegisterBadRequestResponse,
|
||||
OAuthRegisterError,
|
||||
OAuthRegisterRequest,
|
||||
OAuthRegisterResponse,
|
||||
OAuthTokenError,
|
||||
OAuthTokenResponse,
|
||||
PaginationInfo,
|
||||
PartnerUsageRequest,
|
||||
PartnerUsageResponse,
|
||||
@@ -663,6 +685,21 @@ export type {
|
||||
PostMonitoringTasksSubpathData,
|
||||
PostMonitoringTasksSubpathErrors,
|
||||
PostMonitoringTasksSubpathResponses,
|
||||
PostOAuthAuthorizeData,
|
||||
PostOAuthAuthorizeError,
|
||||
PostOAuthAuthorizeErrors,
|
||||
PostOAuthAuthorizeResponse,
|
||||
PostOAuthAuthorizeResponses,
|
||||
PostOAuthRegisterData,
|
||||
PostOAuthRegisterError,
|
||||
PostOAuthRegisterErrors,
|
||||
PostOAuthRegisterResponse,
|
||||
PostOAuthRegisterResponses,
|
||||
PostOAuthTokenData,
|
||||
PostOAuthTokenError,
|
||||
PostOAuthTokenErrors,
|
||||
PostOAuthTokenResponse,
|
||||
PostOAuthTokenResponses,
|
||||
PostPprofSymbolData,
|
||||
PostPprofSymbolResponses,
|
||||
PostUserdataFileData,
|
||||
@@ -799,11 +836,11 @@ export type {
|
||||
UpdateSecretRequest,
|
||||
UpdateSecretResponse,
|
||||
UpdateSecretResponses,
|
||||
UpdateSettingByKeyData,
|
||||
UpdateSettingByKeyError,
|
||||
UpdateSettingByKeyErrors,
|
||||
UpdateSettingByKeyResponse,
|
||||
UpdateSettingByKeyResponses,
|
||||
UpdateSettingByIdData,
|
||||
UpdateSettingByIdError,
|
||||
UpdateSettingByIdErrors,
|
||||
UpdateSettingByIdResponse,
|
||||
UpdateSettingByIdResponses,
|
||||
UpdateSubscriptionCacheData,
|
||||
UpdateSubscriptionCacheError,
|
||||
UpdateSubscriptionCacheErrors,
|
||||
|
||||
709
packages/ingest-types/src/types.gen.ts
generated
@@ -1382,6 +1382,250 @@ export type JwkKey = {
|
||||
y: string
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 6749 §5.2 error response.
|
||||
*/
|
||||
export type OAuthTokenError = {
|
||||
/**
|
||||
* RFC 6749 §5.2 error code: invalid_request, invalid_client, invalid_grant, unauthorized_client, unsupported_grant_type, invalid_scope.
|
||||
*/
|
||||
error: string
|
||||
/**
|
||||
* Human-readable, no leak of internal storage state.
|
||||
*/
|
||||
error_description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 6749 §5.1 successful token response.
|
||||
*/
|
||||
export type OAuthTokenResponse = {
|
||||
/**
|
||||
* Resource-bound Cloud JWT (audience matches the protected resource).
|
||||
*/
|
||||
access_token: string
|
||||
token_type: 'Bearer'
|
||||
/**
|
||||
* Access token lifetime in seconds.
|
||||
*/
|
||||
expires_in: number
|
||||
/**
|
||||
* Opaque refresh token. Rotates on every successful refresh; presenting an already-rotated token revokes the entire family.
|
||||
*/
|
||||
refresh_token: string
|
||||
/**
|
||||
* Space-delimited scopes granted with this token.
|
||||
*/
|
||||
scope: string
|
||||
}
|
||||
|
||||
/**
|
||||
* One workspace option presented in the OAuth consent challenge. Promoted to a named schema so the generated Go type is referenceable in handlers and tests rather than re-declared as an anonymous struct at every callsite.
|
||||
*
|
||||
*/
|
||||
export type OAuthConsentChallengeWorkspace = {
|
||||
id: string
|
||||
name: string
|
||||
type: 'personal' | 'team'
|
||||
role: 'owner' | 'member'
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect target produced after a JSON consent submission. The frontend must navigate the browser to this URL so custom-scheme client callbacks work without relying on fetch-visible 302 headers.
|
||||
*/
|
||||
export type OAuthAuthorizeRedirectResponse = {
|
||||
/**
|
||||
* OAuth client redirect URI with either code+state for allow, or error+state for deny.
|
||||
*/
|
||||
redirect_url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side state describing the OAuth consent decision the user is being asked to make. Returned by GET /oauth/authorize when a valid Cloud session exists; the frontend renders the consent UI from this payload and POSTs the decision back. Browser never sees the original OAuth params on resume.
|
||||
*
|
||||
*/
|
||||
export type OAuthConsentChallenge = {
|
||||
/**
|
||||
* Opaque server-side identifier for the authorization-request row. Carried back unchanged in the consent submission.
|
||||
*/
|
||||
oauth_request_id: string
|
||||
/**
|
||||
* Per-row CSRF token bound to this authorization request (not to the session). Must be echoed back on POST.
|
||||
*/
|
||||
csrf_token: string
|
||||
/**
|
||||
* Human-readable name of the OAuth client requesting authorization, from oauth_clients.display_name.
|
||||
*/
|
||||
client_display_name: string
|
||||
/**
|
||||
* Human-readable name of the protected resource, from oauth_resources.display_name.
|
||||
*/
|
||||
resource_display_name: string
|
||||
/**
|
||||
* Scopes the client is requesting for this resource. The frontend should present these for the user to approve.
|
||||
*/
|
||||
scopes: Array<string>
|
||||
/**
|
||||
* Workspaces the user can select from. Membership is re-checked on POST.
|
||||
*/
|
||||
workspaces: Array<OAuthConsentChallengeWorkspace>
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.1 protected-resource metadata (RFC 9728).
|
||||
*/
|
||||
export type OAuthProtectedResourceMetadata = {
|
||||
resource: string
|
||||
authorization_servers: Array<string>
|
||||
scopes_supported: Array<string>
|
||||
bearer_methods_supported?: Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 7591 §3.2.2 error response.
|
||||
*/
|
||||
export type OAuthRegisterError = {
|
||||
error: 'invalid_redirect_uri' | 'invalid_client_metadata'
|
||||
error_description?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Union of the two 400 shapes /oauth/register can emit. `OAuthRegisterError` is the handler-shaped RFC 7591 §3.2.2 error; `BindingErrorResponse` is the strict-server binding-layer error fired when the request body fails OpenAPI-schema validation before the handler runs.
|
||||
*
|
||||
*/
|
||||
export type OAuthRegisterBadRequestResponse =
|
||||
| OAuthRegisterError
|
||||
| BindingErrorResponse
|
||||
|
||||
/**
|
||||
* Error shape returned when request binding or validation fails before the handler runs.
|
||||
*/
|
||||
export type BindingErrorResponse = {
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 7591 §3.2.1 successful registration response.
|
||||
*/
|
||||
export type OAuthRegisterResponse = {
|
||||
/**
|
||||
* Server-generated client_id. Always carries the `mcp-dyn-` prefix.
|
||||
*/
|
||||
client_id: string
|
||||
/**
|
||||
* Unix timestamp (seconds) when the client was registered.
|
||||
*/
|
||||
client_id_issued_at: number
|
||||
client_name?: string
|
||||
redirect_uris: Array<string>
|
||||
grant_types: Array<string>
|
||||
response_types: Array<string>
|
||||
token_endpoint_auth_method: 'none'
|
||||
application_type: 'native' | 'web'
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC 7591 §2 client metadata document. Only the fields the server honors are listed; presence of `scope` or `resource_grants` in the request is rejected (`invalid_client_metadata`) because those are server-owned for dynamic clients. `additionalProperties: false` mirrors the runtime middleware that rejects any unknown metadata key.
|
||||
*
|
||||
*/
|
||||
export type OAuthRegisterRequest = {
|
||||
/**
|
||||
* 1–5 redirect URIs. Validated against `application_type` policy.
|
||||
*/
|
||||
redirect_uris: Array<string>
|
||||
/**
|
||||
* Human-readable name shown in the consent UI. Reserved-name list rejects impersonation of major MCP clients.
|
||||
*/
|
||||
client_name?: string
|
||||
/**
|
||||
* RFC 7591 §2 application_type. **REQUIRED** — clients MUST declare intent; the server does not default this field. `native` for desktop / CLI / MCP-spec-strict clients (loopback redirects); `web` for hosted clients (HTTPS only, host must be allowlisted). A missing or explicitly empty `application_type` rejects with `invalid_client_metadata`. The realistic MCP-client population is overwhelmingly native/loopback — requiring explicit declaration avoids silently bouncing those clients off the web HTTPS policy.
|
||||
*
|
||||
*/
|
||||
application_type: 'native' | 'web'
|
||||
/**
|
||||
* Public clients only this phase — must be `none` if present. The server forces `none` regardless.
|
||||
*/
|
||||
token_endpoint_auth_method?: 'none'
|
||||
/**
|
||||
* Optional. Defaults to `["authorization_code","refresh_token"]`.
|
||||
*/
|
||||
grant_types?: Array<'authorization_code' | 'refresh_token'>
|
||||
/**
|
||||
* Optional. Defaults to `["code"]`.
|
||||
*/
|
||||
response_types?: Array<'code'>
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Dynamic clients do not pick scopes — the server assigns scopes from the active MCP resource's published list. Sending `scope` in the registration body is treated as a privilege-escalation attempt and returns `invalid_client_metadata`. The field is documented here so clients see a well-defined error rather than silent drop.
|
||||
*
|
||||
*/
|
||||
scope?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Same reason as `scope`. The set of resources and scopes a dynamic client may request is server-policy, not request-driven.
|
||||
*
|
||||
*/
|
||||
resource_grants?: {
|
||||
[key: string]: Array<string>
|
||||
} | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
client_uri?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
logo_uri?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
tos_uri?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
policy_uri?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
software_id?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
software_version?: string | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
contacts?: Array<string> | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
jwks?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
/**
|
||||
* **REJECTED IF PRESENT.** Unsupported RFC 7591 metadata for this public MCP-client phase.
|
||||
*/
|
||||
jwks_uri?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth 2.1 authorization-server metadata (RFC 8414).
|
||||
*/
|
||||
export type OAuthAuthorizationServerMetadata = {
|
||||
issuer: string
|
||||
authorization_endpoint: string
|
||||
token_endpoint: string
|
||||
jwks_uri: string
|
||||
/**
|
||||
* RFC 7591 §3.1 Dynamic Client Registration endpoint. Advertised so MCP-spec-compliant clients can auto-discover and self-register without operator involvement. Present only when DCR is enabled.
|
||||
*
|
||||
*/
|
||||
registration_endpoint?: string
|
||||
response_types_supported: Array<string>
|
||||
grant_types_supported: Array<string>
|
||||
code_challenge_methods_supported: Array<string>
|
||||
token_endpoint_auth_methods_supported: Array<string>
|
||||
scopes_supported?: Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Web Key Set containing the public keys used to verify Cloud JWTs.
|
||||
*/
|
||||
@@ -1531,6 +1775,10 @@ export type WorkspaceApiKeyInfo = {
|
||||
* User-provided label
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* User-provided description of the key's purpose. Limit is byte-based (UTF-8 encoding); 5000 bytes equals 5000 ASCII characters or fewer multi-byte characters.
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* First 8 chars after prefix for display
|
||||
*/
|
||||
@@ -1565,6 +1813,10 @@ export type CreateWorkspaceApiKeyResponse = {
|
||||
* User-provided label
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* User-provided description of the key's purpose. Limit is byte-based (UTF-8 encoding); 5000 bytes equals 5000 ASCII characters or fewer multi-byte characters.
|
||||
*/
|
||||
description: string
|
||||
/**
|
||||
* The full plaintext API key (only shown once)
|
||||
*/
|
||||
@@ -1591,6 +1843,10 @@ export type CreateWorkspaceApiKeyRequest = {
|
||||
* User-provided label for the key
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* User-provided description of the key's purpose. Limit is byte-based (UTF-8 encoding); 5000 bytes equals 5000 ASCII characters or fewer multi-byte characters.
|
||||
*/
|
||||
description?: string
|
||||
/**
|
||||
* Optional expiration timestamp
|
||||
*/
|
||||
@@ -2270,6 +2526,12 @@ export type ListAssetsResponse = {
|
||||
* Whether more assets are available beyond this page
|
||||
*/
|
||||
has_more: boolean
|
||||
/**
|
||||
* Opaque cursor to pass as the `after` query parameter to fetch the
|
||||
* next page. Omitted from the response when there are no more results.
|
||||
*
|
||||
*/
|
||||
next_cursor?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2284,6 +2546,10 @@ export type Asset = {
|
||||
* Name of the asset file
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* Display name of the asset. Mirrors name for backwards compatibility.
|
||||
*/
|
||||
display_name?: string | null
|
||||
/**
|
||||
* Blake3 hash of the asset content
|
||||
*/
|
||||
@@ -2360,6 +2626,10 @@ export type AssetUpdated = {
|
||||
* Updated name of the asset
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* Display name of the asset. Mirrors name for backwards compatibility.
|
||||
*/
|
||||
display_name?: string | null
|
||||
/**
|
||||
* Blake3 hash of the asset content
|
||||
*/
|
||||
@@ -3035,18 +3305,11 @@ export type ExportDownloadUrlResponse = {
|
||||
expires_at?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Error shape returned when request binding or validation fails before the handler runs.
|
||||
*/
|
||||
export type BindingErrorResponse = {
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard error response with a machine-readable code and human-readable message.
|
||||
*/
|
||||
export type ErrorResponse = {
|
||||
code: string
|
||||
code?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
@@ -3124,6 +3387,12 @@ export type ListAssetsResponseWritable = {
|
||||
* Whether more assets are available beyond this page
|
||||
*/
|
||||
has_more: boolean
|
||||
/**
|
||||
* Opaque cursor to pass as the `after` query parameter to fetch the
|
||||
* next page. Omitted from the response when there are no more results.
|
||||
*
|
||||
*/
|
||||
next_cursor?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3138,6 +3407,10 @@ export type AssetWritable = {
|
||||
* Name of the asset file
|
||||
*/
|
||||
name: string
|
||||
/**
|
||||
* Display name of the asset. Mirrors name for backwards compatibility.
|
||||
*/
|
||||
display_name?: string | null
|
||||
/**
|
||||
* Blake3 hash of the asset content
|
||||
*/
|
||||
@@ -3507,50 +3780,6 @@ export type GetModelsInFolderResponses = {
|
||||
export type GetModelsInFolderResponse =
|
||||
GetModelsInFolderResponses[keyof GetModelsInFolderResponses]
|
||||
|
||||
export type GetModelPreviewData = {
|
||||
body?: never
|
||||
path: {
|
||||
/**
|
||||
* The folder name containing the model
|
||||
*/
|
||||
folder: string
|
||||
/**
|
||||
* The path index (usually 0 for cloud service)
|
||||
*/
|
||||
path_index: number
|
||||
/**
|
||||
* The model filename (with or without .webp extension)
|
||||
*/
|
||||
filename: string
|
||||
}
|
||||
query?: never
|
||||
url: '/api/experiment/models/preview/{folder}/{path_index}/{filename}'
|
||||
}
|
||||
|
||||
export type GetModelPreviewErrors = {
|
||||
/**
|
||||
* Model not found or preview not available
|
||||
*/
|
||||
404: ErrorResponse
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ErrorResponse
|
||||
}
|
||||
|
||||
export type GetModelPreviewError =
|
||||
GetModelPreviewErrors[keyof GetModelPreviewErrors]
|
||||
|
||||
export type GetModelPreviewResponses = {
|
||||
/**
|
||||
* Success - Model preview image
|
||||
*/
|
||||
200: Blob | File
|
||||
}
|
||||
|
||||
export type GetModelPreviewResponse =
|
||||
GetModelPreviewResponses[keyof GetModelPreviewResponses]
|
||||
|
||||
export type GetLegacyHistoryData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -4012,10 +4241,6 @@ export type ListAssetsData = {
|
||||
* Sort order
|
||||
*/
|
||||
order?: 'asc' | 'desc'
|
||||
/**
|
||||
* Filter assets by job IDs (prompt IDs)
|
||||
*/
|
||||
job_ids?: Array<string>
|
||||
/**
|
||||
* Whether to include public/shared assets in results
|
||||
*/
|
||||
@@ -4024,6 +4249,17 @@ export type ListAssetsData = {
|
||||
* Filter assets by exact content hash
|
||||
*/
|
||||
asset_hash?: string
|
||||
/**
|
||||
* Opaque cursor for keyset pagination. Pass the `next_cursor` value
|
||||
* from the previous response to fetch the next page. When provided,
|
||||
* `offset` is ignored. Cursor pagination is only supported with
|
||||
* `sort` values `created_at`, `updated_at`, `name`, or `size`;
|
||||
* requests combining `after` with other sort fields return 400.
|
||||
* The cursor must have been minted under the same `sort` value used
|
||||
* in the follow-up request.
|
||||
*
|
||||
*/
|
||||
after?: string
|
||||
}
|
||||
url: '/api/assets'
|
||||
}
|
||||
@@ -4122,10 +4358,6 @@ export type UploadAssetErrors = {
|
||||
export type UploadAssetError = UploadAssetErrors[keyof UploadAssetErrors]
|
||||
|
||||
export type UploadAssetResponses = {
|
||||
/**
|
||||
* Asset already exists (returned existing asset)
|
||||
*/
|
||||
200: AssetCreated
|
||||
/**
|
||||
* Asset created successfully
|
||||
*/
|
||||
@@ -4188,10 +4420,6 @@ export type CreateAssetFromHashError =
|
||||
CreateAssetFromHashErrors[keyof CreateAssetFromHashErrors]
|
||||
|
||||
export type CreateAssetFromHashResponses = {
|
||||
/**
|
||||
* Asset reference already exists (returned existing)
|
||||
*/
|
||||
200: AssetCreated
|
||||
/**
|
||||
* Asset reference created successfully
|
||||
*/
|
||||
@@ -5345,19 +5573,19 @@ export type UpdateMultipleSettingsResponses = {
|
||||
export type UpdateMultipleSettingsResponse =
|
||||
UpdateMultipleSettingsResponses[keyof UpdateMultipleSettingsResponses]
|
||||
|
||||
export type GetSettingByKeyData = {
|
||||
export type GetSettingByIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
/**
|
||||
* Setting key to retrieve
|
||||
* Setting id to retrieve
|
||||
*/
|
||||
key: string
|
||||
id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/api/settings/{key}'
|
||||
url: '/api/settings/{id}'
|
||||
}
|
||||
|
||||
export type GetSettingByKeyErrors = {
|
||||
export type GetSettingByIdErrors = {
|
||||
/**
|
||||
* Unauthorized
|
||||
*/
|
||||
@@ -5368,10 +5596,10 @@ export type GetSettingByKeyErrors = {
|
||||
404: ErrorResponse
|
||||
}
|
||||
|
||||
export type GetSettingByKeyError =
|
||||
GetSettingByKeyErrors[keyof GetSettingByKeyErrors]
|
||||
export type GetSettingByIdError =
|
||||
GetSettingByIdErrors[keyof GetSettingByIdErrors]
|
||||
|
||||
export type GetSettingByKeyResponses = {
|
||||
export type GetSettingByIdResponses = {
|
||||
/**
|
||||
* Setting value response
|
||||
*/
|
||||
@@ -5383,25 +5611,25 @@ export type GetSettingByKeyResponses = {
|
||||
}
|
||||
}
|
||||
|
||||
export type GetSettingByKeyResponse =
|
||||
GetSettingByKeyResponses[keyof GetSettingByKeyResponses]
|
||||
export type GetSettingByIdResponse =
|
||||
GetSettingByIdResponses[keyof GetSettingByIdResponses]
|
||||
|
||||
export type UpdateSettingByKeyData = {
|
||||
export type UpdateSettingByIdData = {
|
||||
/**
|
||||
* New value for the setting
|
||||
*/
|
||||
body: unknown
|
||||
path: {
|
||||
/**
|
||||
* Setting key to update
|
||||
* Setting id to update
|
||||
*/
|
||||
key: string
|
||||
id: string
|
||||
}
|
||||
query?: never
|
||||
url: '/api/settings/{key}'
|
||||
url: '/api/settings/{id}'
|
||||
}
|
||||
|
||||
export type UpdateSettingByKeyErrors = {
|
||||
export type UpdateSettingByIdErrors = {
|
||||
/**
|
||||
* Invalid request
|
||||
*/
|
||||
@@ -5412,10 +5640,10 @@ export type UpdateSettingByKeyErrors = {
|
||||
401: ErrorResponse
|
||||
}
|
||||
|
||||
export type UpdateSettingByKeyError =
|
||||
UpdateSettingByKeyErrors[keyof UpdateSettingByKeyErrors]
|
||||
export type UpdateSettingByIdError =
|
||||
UpdateSettingByIdErrors[keyof UpdateSettingByIdErrors]
|
||||
|
||||
export type UpdateSettingByKeyResponses = {
|
||||
export type UpdateSettingByIdResponses = {
|
||||
/**
|
||||
* Updated setting value response
|
||||
*/
|
||||
@@ -5427,8 +5655,8 @@ export type UpdateSettingByKeyResponses = {
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateSettingByKeyResponse =
|
||||
UpdateSettingByKeyResponses[keyof UpdateSettingByKeyResponses]
|
||||
export type UpdateSettingByIdResponse =
|
||||
UpdateSettingByIdResponses[keyof UpdateSettingByIdResponses]
|
||||
|
||||
export type SubmitFeedbackData = {
|
||||
body: FeedbackRequest
|
||||
@@ -5916,40 +6144,6 @@ export type UploadMaskResponses = {
|
||||
* Type of upload (e.g., "output")
|
||||
*/
|
||||
type?: string
|
||||
/**
|
||||
* Additional metadata for mask detection and re-editing
|
||||
*/
|
||||
metadata?: {
|
||||
/**
|
||||
* Whether this file is a mask
|
||||
*/
|
||||
is_mask?: boolean
|
||||
/**
|
||||
* Hash of the original unmasked image
|
||||
*/
|
||||
original_hash?: string
|
||||
/**
|
||||
* Type of mask (e.g., "painted_masked")
|
||||
*/
|
||||
mask_type?: string
|
||||
/**
|
||||
* Related mask layer files (if available)
|
||||
*/
|
||||
related_files?: {
|
||||
/**
|
||||
* Hash of the mask layer
|
||||
*/
|
||||
mask?: string
|
||||
/**
|
||||
* Hash of the paint layer
|
||||
*/
|
||||
paint?: string
|
||||
/**
|
||||
* Hash of the painted image
|
||||
*/
|
||||
painted?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6117,6 +6311,229 @@ export type GetJwksResponses = {
|
||||
|
||||
export type GetJwksResponse = GetJwksResponses[keyof GetJwksResponses]
|
||||
|
||||
export type GetOAuthAuthorizationServerData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/.well-known/oauth-authorization-server'
|
||||
}
|
||||
|
||||
export type GetOAuthAuthorizationServerErrors = {
|
||||
/**
|
||||
* OAuth disabled
|
||||
*/
|
||||
404: ErrorResponse
|
||||
}
|
||||
|
||||
export type GetOAuthAuthorizationServerError =
|
||||
GetOAuthAuthorizationServerErrors[keyof GetOAuthAuthorizationServerErrors]
|
||||
|
||||
export type GetOAuthAuthorizationServerResponses = {
|
||||
/**
|
||||
* Authorization-server metadata
|
||||
*/
|
||||
200: OAuthAuthorizationServerMetadata
|
||||
}
|
||||
|
||||
export type GetOAuthAuthorizationServerResponse =
|
||||
GetOAuthAuthorizationServerResponses[keyof GetOAuthAuthorizationServerResponses]
|
||||
|
||||
export type GetOAuthProtectedResourceData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/.well-known/oauth-protected-resource'
|
||||
}
|
||||
|
||||
export type GetOAuthProtectedResourceErrors = {
|
||||
/**
|
||||
* OAuth disabled or no active resource configured
|
||||
*/
|
||||
404: ErrorResponse
|
||||
}
|
||||
|
||||
export type GetOAuthProtectedResourceError =
|
||||
GetOAuthProtectedResourceErrors[keyof GetOAuthProtectedResourceErrors]
|
||||
|
||||
export type GetOAuthProtectedResourceResponses = {
|
||||
/**
|
||||
* Protected-resource metadata
|
||||
*/
|
||||
200: OAuthProtectedResourceMetadata
|
||||
}
|
||||
|
||||
export type GetOAuthProtectedResourceResponse =
|
||||
GetOAuthProtectedResourceResponses[keyof GetOAuthProtectedResourceResponses]
|
||||
|
||||
export type GetOAuthAuthorizeData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
response_type?: string
|
||||
client_id?: string
|
||||
redirect_uri?: string
|
||||
scope?: string
|
||||
/**
|
||||
* RFC 6749 §10.12 marks `state` as RECOMMENDED. Our hardening makes
|
||||
* it REQUIRED on the initial-entry path (omitted only on the resume
|
||||
* path where `oauth_request_id` is supplied instead). This parameter
|
||||
* is `required: false` at the spec level only because the operation
|
||||
* is dual-mode (initial entry vs. resume); the runtime parser
|
||||
* (services/ingest/server/implementation/oauth/protocol/request.go)
|
||||
* rejects empty `state` on the initial-entry path with a stable
|
||||
* `invalid_request` 400.
|
||||
*
|
||||
*/
|
||||
state?: string
|
||||
code_challenge?: string
|
||||
code_challenge_method?: string
|
||||
resource?: string
|
||||
oauth_request_id?: string
|
||||
}
|
||||
url: '/oauth/authorize'
|
||||
}
|
||||
|
||||
export type GetOAuthAuthorizeErrors = {
|
||||
/**
|
||||
* Invalid authorize request (pre-redirect failure — unknown client, redirect mismatch, malformed params)
|
||||
*/
|
||||
400: ErrorResponse
|
||||
/**
|
||||
* OAuth disabled
|
||||
*/
|
||||
404: ErrorResponse
|
||||
}
|
||||
|
||||
export type GetOAuthAuthorizeError =
|
||||
GetOAuthAuthorizeErrors[keyof GetOAuthAuthorizeErrors]
|
||||
|
||||
export type GetOAuthAuthorizeResponses = {
|
||||
/**
|
||||
* Consent challenge payload (cookie present, email verified). Frontend renders the consent UI from this payload and POSTs back to /oauth/authorize.
|
||||
*
|
||||
*/
|
||||
200: OAuthConsentChallenge
|
||||
}
|
||||
|
||||
export type GetOAuthAuthorizeResponse =
|
||||
GetOAuthAuthorizeResponses[keyof GetOAuthAuthorizeResponses]
|
||||
|
||||
export type PostOAuthAuthorizeData = {
|
||||
body: {
|
||||
oauth_request_id: string
|
||||
csrf_token: string
|
||||
decision: 'allow' | 'deny'
|
||||
workspace_id: string
|
||||
}
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/oauth/authorize'
|
||||
}
|
||||
|
||||
export type PostOAuthAuthorizeErrors = {
|
||||
/**
|
||||
* Bad request (CSRF mismatch, expired/consumed request, inaccessible workspace)
|
||||
*/
|
||||
400: ErrorResponse
|
||||
/**
|
||||
* Scope broadening on consent re-grant — fresh consent flow required
|
||||
*/
|
||||
403: ErrorResponse
|
||||
/**
|
||||
* OAuth disabled
|
||||
*/
|
||||
404: ErrorResponse
|
||||
}
|
||||
|
||||
export type PostOAuthAuthorizeError =
|
||||
PostOAuthAuthorizeErrors[keyof PostOAuthAuthorizeErrors]
|
||||
|
||||
export type PostOAuthAuthorizeResponses = {
|
||||
/**
|
||||
* Redirect URL for the frontend to navigate to (allow → with code+state; deny → with error+state)
|
||||
*/
|
||||
200: OAuthAuthorizeRedirectResponse
|
||||
}
|
||||
|
||||
export type PostOAuthAuthorizeResponse =
|
||||
PostOAuthAuthorizeResponses[keyof PostOAuthAuthorizeResponses]
|
||||
|
||||
export type PostOAuthTokenData = {
|
||||
body: {
|
||||
grant_type: 'authorization_code' | 'refresh_token'
|
||||
client_id: string
|
||||
code?: string
|
||||
redirect_uri?: string
|
||||
code_verifier?: string
|
||||
refresh_token?: string
|
||||
scope?: string
|
||||
client_secret?: string
|
||||
}
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/oauth/token'
|
||||
}
|
||||
|
||||
export type PostOAuthTokenErrors = {
|
||||
/**
|
||||
* RFC 6749 §5.2 error
|
||||
*/
|
||||
400: OAuthTokenError
|
||||
/**
|
||||
* OAuth disabled
|
||||
*/
|
||||
404: ErrorResponse
|
||||
}
|
||||
|
||||
export type PostOAuthTokenError =
|
||||
PostOAuthTokenErrors[keyof PostOAuthTokenErrors]
|
||||
|
||||
export type PostOAuthTokenResponses = {
|
||||
/**
|
||||
* New token pair
|
||||
*/
|
||||
200: OAuthTokenResponse
|
||||
}
|
||||
|
||||
export type PostOAuthTokenResponse =
|
||||
PostOAuthTokenResponses[keyof PostOAuthTokenResponses]
|
||||
|
||||
export type PostOAuthRegisterData = {
|
||||
body: OAuthRegisterRequest
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/oauth/register'
|
||||
}
|
||||
|
||||
export type PostOAuthRegisterErrors = {
|
||||
/**
|
||||
* Bad request. Two shapes possible: `OAuthRegisterError` (RFC 7591 §3.2.2, emitted by the handler for invalid client metadata, missing application_type, reserved client_name, etc.) OR `BindingErrorResponse` (emitted by the strict-server binding layer when the request body fails OpenAPI-schema validation — malformed JSON, missing required fields, `additionalProperties: false` violations).
|
||||
*
|
||||
*/
|
||||
400: OAuthRegisterBadRequestResponse
|
||||
/**
|
||||
* OAuth disabled
|
||||
*/
|
||||
404: ErrorResponse
|
||||
/**
|
||||
* No active MCP resource is configured — DCR cannot mint a usable client until ops seeds an active oauth_resources row.
|
||||
*/
|
||||
503: ErrorResponse
|
||||
}
|
||||
|
||||
export type PostOAuthRegisterError =
|
||||
PostOAuthRegisterErrors[keyof PostOAuthRegisterErrors]
|
||||
|
||||
export type PostOAuthRegisterResponses = {
|
||||
/**
|
||||
* Registered. Body echoes the metadata RFC 7591 §3.2.1 requires.
|
||||
*/
|
||||
201: OAuthRegisterResponse
|
||||
}
|
||||
|
||||
export type PostOAuthRegisterResponse =
|
||||
PostOAuthRegisterResponses[keyof PostOAuthRegisterResponses]
|
||||
|
||||
export type ListWorkspacesData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -6679,7 +7096,7 @@ export type CreateWorkspaceApiKeyErrors = {
|
||||
*/
|
||||
401: ErrorResponse
|
||||
/**
|
||||
* Not a workspace member or personal workspace
|
||||
* Not a workspace member
|
||||
*/
|
||||
403: ErrorResponse
|
||||
/**
|
||||
@@ -8888,9 +9305,20 @@ export type GetTemplateProxyData = {
|
||||
|
||||
export type GetTemplateProxyErrors = {
|
||||
/**
|
||||
* Template not found
|
||||
* Template not found.
|
||||
*/
|
||||
404: unknown
|
||||
/**
|
||||
* Workflow templates version not available.
|
||||
*/
|
||||
503: unknown
|
||||
}
|
||||
|
||||
export type GetTemplateProxyResponses = {
|
||||
/**
|
||||
* Template file content streamed from GCS.
|
||||
*/
|
||||
200: unknown
|
||||
}
|
||||
|
||||
export type GetHealthData = {
|
||||
@@ -8918,20 +9346,6 @@ export type GetHealthResponses = {
|
||||
|
||||
export type GetHealthResponse = GetHealthResponses[keyof GetHealthResponses]
|
||||
|
||||
export type GetOpenapiSpecData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: never
|
||||
url: '/openapi'
|
||||
}
|
||||
|
||||
export type GetOpenapiSpecResponses = {
|
||||
/**
|
||||
* OpenAPI specification document
|
||||
*/
|
||||
200: unknown
|
||||
}
|
||||
|
||||
export type GetMonitoringTasksData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@@ -9194,6 +9608,33 @@ export type PostCustomNodeProxyResponses = {
|
||||
200: unknown
|
||||
}
|
||||
|
||||
export type GetModelPreviewData = {
|
||||
body?: never
|
||||
path: {
|
||||
/**
|
||||
* The folder name containing the model.
|
||||
*/
|
||||
folder: string
|
||||
/**
|
||||
* The path index (usually 0 for cloud service).
|
||||
*/
|
||||
path_index: number
|
||||
/**
|
||||
* The model filename (with or without .webp extension).
|
||||
*/
|
||||
filename: string
|
||||
}
|
||||
query?: never
|
||||
url: '/api/experiment/models/preview/{folder}/{path_index}/{filename}'
|
||||
}
|
||||
|
||||
export type GetModelPreviewErrors = {
|
||||
/**
|
||||
* Preview not available on Cloud
|
||||
*/
|
||||
404: unknown
|
||||
}
|
||||
|
||||
export type GetLegacyPromptByIdData = {
|
||||
body?: never
|
||||
path: {
|
||||
|
||||
330
packages/ingest-types/src/zod.gen.ts
generated
@@ -879,6 +879,153 @@ export const zJwkKey = z.object({
|
||||
y: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 6749 §5.2 error response.
|
||||
*/
|
||||
export const zOAuthTokenError = z.object({
|
||||
error: z.string(),
|
||||
error_description: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 6749 §5.1 successful token response.
|
||||
*/
|
||||
export const zOAuthTokenResponse = z.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.enum(['Bearer']),
|
||||
expires_in: z.number().int(),
|
||||
refresh_token: z.string(),
|
||||
scope: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* One workspace option presented in the OAuth consent challenge. Promoted to a named schema so the generated Go type is referenceable in handlers and tests rather than re-declared as an anonymous struct at every callsite.
|
||||
*
|
||||
*/
|
||||
export const zOAuthConsentChallengeWorkspace = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.enum(['personal', 'team']),
|
||||
role: z.enum(['owner', 'member'])
|
||||
})
|
||||
|
||||
/**
|
||||
* Redirect target produced after a JSON consent submission. The frontend must navigate the browser to this URL so custom-scheme client callbacks work without relying on fetch-visible 302 headers.
|
||||
*/
|
||||
export const zOAuthAuthorizeRedirectResponse = z.object({
|
||||
redirect_url: z.string().url()
|
||||
})
|
||||
|
||||
/**
|
||||
* Server-side state describing the OAuth consent decision the user is being asked to make. Returned by GET /oauth/authorize when a valid Cloud session exists; the frontend renders the consent UI from this payload and POSTs the decision back. Browser never sees the original OAuth params on resume.
|
||||
*
|
||||
*/
|
||||
export const zOAuthConsentChallenge = z.object({
|
||||
oauth_request_id: z.string().uuid(),
|
||||
csrf_token: z.string(),
|
||||
client_display_name: z.string(),
|
||||
resource_display_name: z.string(),
|
||||
scopes: z.array(z.string()),
|
||||
workspaces: z.array(zOAuthConsentChallengeWorkspace)
|
||||
})
|
||||
|
||||
/**
|
||||
* OAuth 2.1 protected-resource metadata (RFC 9728).
|
||||
*/
|
||||
export const zOAuthProtectedResourceMetadata = z.object({
|
||||
resource: z.string().url(),
|
||||
authorization_servers: z.array(z.string().url()),
|
||||
scopes_supported: z.array(z.string()),
|
||||
bearer_methods_supported: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 7591 §3.2.2 error response.
|
||||
*/
|
||||
export const zOAuthRegisterError = z.object({
|
||||
error: z.enum(['invalid_redirect_uri', 'invalid_client_metadata']),
|
||||
error_description: z.string().nullish()
|
||||
})
|
||||
|
||||
/**
|
||||
* Error shape returned when request binding or validation fails before the handler runs.
|
||||
*/
|
||||
export const zBindingErrorResponse = z.object({
|
||||
message: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* Union of the two 400 shapes /oauth/register can emit. `OAuthRegisterError` is the handler-shaped RFC 7591 §3.2.2 error; `BindingErrorResponse` is the strict-server binding-layer error fired when the request body fails OpenAPI-schema validation before the handler runs.
|
||||
*
|
||||
*/
|
||||
export const zOAuthRegisterBadRequestResponse = z.union([
|
||||
zOAuthRegisterError,
|
||||
zBindingErrorResponse
|
||||
])
|
||||
|
||||
/**
|
||||
* RFC 7591 §3.2.1 successful registration response.
|
||||
*/
|
||||
export const zOAuthRegisterResponse = z.object({
|
||||
client_id: z.string(),
|
||||
client_id_issued_at: z.coerce
|
||||
.bigint()
|
||||
.min(BigInt('-9223372036854775808'), {
|
||||
message: 'Invalid value: Expected int64 to be >= -9223372036854775808'
|
||||
})
|
||||
.max(BigInt('9223372036854775807'), {
|
||||
message: 'Invalid value: Expected int64 to be <= 9223372036854775807'
|
||||
}),
|
||||
client_name: z.string().optional(),
|
||||
redirect_uris: z.array(z.string()),
|
||||
grant_types: z.array(z.string()),
|
||||
response_types: z.array(z.string()),
|
||||
token_endpoint_auth_method: z.enum(['none']),
|
||||
application_type: z.enum(['native', 'web'])
|
||||
})
|
||||
|
||||
/**
|
||||
* RFC 7591 §2 client metadata document. Only the fields the server honors are listed; presence of `scope` or `resource_grants` in the request is rejected (`invalid_client_metadata`) because those are server-owned for dynamic clients. `additionalProperties: false` mirrors the runtime middleware that rejects any unknown metadata key.
|
||||
*
|
||||
*/
|
||||
export const zOAuthRegisterRequest = z.object({
|
||||
redirect_uris: z.array(z.string()).min(1).max(5),
|
||||
client_name: z.string().max(100).optional(),
|
||||
application_type: z.enum(['native', 'web']),
|
||||
token_endpoint_auth_method: z.enum(['none']).optional(),
|
||||
grant_types: z
|
||||
.array(z.enum(['authorization_code', 'refresh_token']))
|
||||
.optional(),
|
||||
response_types: z.array(z.enum(['code'])).optional(),
|
||||
scope: z.string().nullish(),
|
||||
resource_grants: z.record(z.array(z.string())).nullish(),
|
||||
client_uri: z.string().nullish(),
|
||||
logo_uri: z.string().nullish(),
|
||||
tos_uri: z.string().nullish(),
|
||||
policy_uri: z.string().nullish(),
|
||||
software_id: z.string().nullish(),
|
||||
software_version: z.string().nullish(),
|
||||
contacts: z.array(z.string()).nullish(),
|
||||
jwks: z.record(z.unknown()).nullish(),
|
||||
jwks_uri: z.string().nullish()
|
||||
})
|
||||
|
||||
/**
|
||||
* OAuth 2.1 authorization-server metadata (RFC 8414).
|
||||
*/
|
||||
export const zOAuthAuthorizationServerMetadata = z.object({
|
||||
issuer: z.string().url(),
|
||||
authorization_endpoint: z.string().url(),
|
||||
token_endpoint: z.string().url(),
|
||||
jwks_uri: z.string().url(),
|
||||
registration_endpoint: z.string().url().optional(),
|
||||
response_types_supported: z.array(z.string()),
|
||||
grant_types_supported: z.array(z.string()),
|
||||
code_challenge_methods_supported: z.array(z.string()),
|
||||
token_endpoint_auth_methods_supported: z.array(z.string()),
|
||||
scopes_supported: z.array(z.string()).optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON Web Key Set containing the public keys used to verify Cloud JWTs.
|
||||
*/
|
||||
@@ -940,6 +1087,7 @@ export const zWorkspaceApiKeyInfo = z.object({
|
||||
workspace_id: z.string(),
|
||||
user_id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().max(5000),
|
||||
key_prefix: z.string(),
|
||||
expires_at: z.string().datetime().optional(),
|
||||
last_used_at: z.string().datetime().optional(),
|
||||
@@ -960,6 +1108,7 @@ export const zListWorkspaceApiKeysResponse = z.object({
|
||||
export const zCreateWorkspaceApiKeyResponse = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
description: z.string().max(5000),
|
||||
key: z.string(),
|
||||
key_prefix: z.string(),
|
||||
expires_at: z.string().datetime().optional(),
|
||||
@@ -971,6 +1120,7 @@ export const zCreateWorkspaceApiKeyResponse = z.object({
|
||||
*/
|
||||
export const zCreateWorkspaceApiKeyRequest = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().max(5000).optional(),
|
||||
expires_at: z.string().datetime().optional()
|
||||
})
|
||||
|
||||
@@ -1353,6 +1503,7 @@ export const zListTagsResponse = z.object({
|
||||
export const zAsset = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
display_name: z.string().nullish(),
|
||||
asset_hash: z
|
||||
.string()
|
||||
.regex(/^blake3:[a-f0-9]{64}$/)
|
||||
@@ -1385,7 +1536,8 @@ export const zAsset = z.object({
|
||||
export const zListAssetsResponse = z.object({
|
||||
assets: z.array(zAsset),
|
||||
total: z.number().int(),
|
||||
has_more: z.boolean()
|
||||
has_more: z.boolean(),
|
||||
next_cursor: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -1394,6 +1546,7 @@ export const zListAssetsResponse = z.object({
|
||||
export const zAssetUpdated = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().optional(),
|
||||
display_name: z.string().nullish(),
|
||||
asset_hash: z
|
||||
.string()
|
||||
.regex(/^blake3:[a-f0-9]{64}$/)
|
||||
@@ -1753,18 +1906,11 @@ export const zExportDownloadUrlResponse = z.object({
|
||||
expires_at: z.string().datetime().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Error shape returned when request binding or validation fails before the handler runs.
|
||||
*/
|
||||
export const zBindingErrorResponse = z.object({
|
||||
message: z.string()
|
||||
})
|
||||
|
||||
/**
|
||||
* Standard error response with a machine-readable code and human-readable message.
|
||||
*/
|
||||
export const zErrorResponse = z.object({
|
||||
code: z.string(),
|
||||
code: z.string().optional(),
|
||||
message: z.string()
|
||||
})
|
||||
|
||||
@@ -1796,6 +1942,7 @@ export const zPromptRequest = z.object({
|
||||
export const zAssetWritable = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
display_name: z.string().nullish(),
|
||||
asset_hash: z
|
||||
.string()
|
||||
.regex(/^blake3:[a-f0-9]{64}$/)
|
||||
@@ -1827,7 +1974,8 @@ export const zAssetWritable = z.object({
|
||||
export const zListAssetsResponseWritable = z.object({
|
||||
assets: z.array(zAssetWritable),
|
||||
total: z.number().int(),
|
||||
has_more: z.boolean()
|
||||
has_more: z.boolean(),
|
||||
next_cursor: z.string().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -1961,21 +2109,6 @@ export const zGetModelsInFolderData = z.object({
|
||||
*/
|
||||
export const zGetModelsInFolderResponse = z.array(zModelFile)
|
||||
|
||||
export const zGetModelPreviewData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
folder: z.string(),
|
||||
path_index: z.number().int(),
|
||||
filename: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Success - Model preview image
|
||||
*/
|
||||
export const zGetModelPreviewResponse = z.string()
|
||||
|
||||
export const zGetLegacyHistoryData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -2132,9 +2265,9 @@ export const zListAssetsData = z.object({
|
||||
.enum(['name', 'created_at', 'updated_at', 'size', 'last_access_time'])
|
||||
.optional(),
|
||||
order: z.enum(['asc', 'desc']).optional(),
|
||||
job_ids: z.array(z.string().uuid()).optional(),
|
||||
include_public: z.boolean().optional().default(true),
|
||||
asset_hash: z.string().optional()
|
||||
asset_hash: z.string().optional(),
|
||||
after: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
@@ -2157,7 +2290,7 @@ export const zUploadAssetData = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset already exists (returned existing asset)
|
||||
* Asset created successfully
|
||||
*/
|
||||
export const zUploadAssetResponse = zAssetCreated
|
||||
|
||||
@@ -2174,7 +2307,7 @@ export const zCreateAssetFromHashData = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Asset reference already exists (returned existing)
|
||||
* Asset reference created successfully
|
||||
*/
|
||||
export const zCreateAssetFromHashResponse = zAssetCreated
|
||||
|
||||
@@ -2509,10 +2642,10 @@ export const zUpdateMultipleSettingsData = z.object({
|
||||
*/
|
||||
export const zUpdateMultipleSettingsResponse = z.record(z.unknown())
|
||||
|
||||
export const zGetSettingByKeyData = z.object({
|
||||
export const zGetSettingByIdData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
key: z.string()
|
||||
id: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
@@ -2520,14 +2653,14 @@ export const zGetSettingByKeyData = z.object({
|
||||
/**
|
||||
* Setting value response
|
||||
*/
|
||||
export const zGetSettingByKeyResponse = z.object({
|
||||
export const zGetSettingByIdResponse = z.object({
|
||||
value: z.unknown().optional()
|
||||
})
|
||||
|
||||
export const zUpdateSettingByKeyData = z.object({
|
||||
export const zUpdateSettingByIdData = z.object({
|
||||
body: z.unknown(),
|
||||
path: z.object({
|
||||
key: z.string()
|
||||
id: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
@@ -2535,7 +2668,7 @@ export const zUpdateSettingByKeyData = z.object({
|
||||
/**
|
||||
* Updated setting value response
|
||||
*/
|
||||
export const zUpdateSettingByKeyResponse = z.object({
|
||||
export const zUpdateSettingByIdResponse = z.object({
|
||||
value: z.unknown().optional()
|
||||
})
|
||||
|
||||
@@ -2691,21 +2824,7 @@ export const zUploadMaskData = z.object({
|
||||
export const zUploadMaskResponse = z.object({
|
||||
name: z.string().optional(),
|
||||
subfolder: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
metadata: z
|
||||
.object({
|
||||
is_mask: z.boolean().optional(),
|
||||
original_hash: z.string().optional(),
|
||||
mask_type: z.string().optional(),
|
||||
related_files: z
|
||||
.object({
|
||||
mask: z.string().optional(),
|
||||
paint: z.string().optional(),
|
||||
painted: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
type: z.string().optional()
|
||||
})
|
||||
|
||||
export const zGetLogsData = z.object({
|
||||
@@ -2774,6 +2893,101 @@ export const zGetJwksData = z.object({
|
||||
*/
|
||||
export const zGetJwksResponse = zJwksResponse
|
||||
|
||||
export const zGetOAuthAuthorizationServerData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Authorization-server metadata
|
||||
*/
|
||||
export const zGetOAuthAuthorizationServerResponse =
|
||||
zOAuthAuthorizationServerMetadata
|
||||
|
||||
export const zGetOAuthProtectedResourceData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Protected-resource metadata
|
||||
*/
|
||||
export const zGetOAuthProtectedResourceResponse =
|
||||
zOAuthProtectedResourceMetadata
|
||||
|
||||
export const zGetOAuthAuthorizeData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z
|
||||
.object({
|
||||
response_type: z.string().optional(),
|
||||
client_id: z.string().optional(),
|
||||
redirect_uri: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
state: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
code_challenge_method: z.string().optional(),
|
||||
resource: z.string().optional(),
|
||||
oauth_request_id: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Consent challenge payload (cookie present, email verified). Frontend renders the consent UI from this payload and POSTs back to /oauth/authorize.
|
||||
*
|
||||
*/
|
||||
export const zGetOAuthAuthorizeResponse = zOAuthConsentChallenge
|
||||
|
||||
export const zPostOAuthAuthorizeData = z.object({
|
||||
body: z.object({
|
||||
oauth_request_id: z.string().uuid(),
|
||||
csrf_token: z.string(),
|
||||
decision: z.enum(['allow', 'deny']),
|
||||
workspace_id: z.string()
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Redirect URL for the frontend to navigate to (allow → with code+state; deny → with error+state)
|
||||
*/
|
||||
export const zPostOAuthAuthorizeResponse = zOAuthAuthorizeRedirectResponse
|
||||
|
||||
export const zPostOAuthTokenData = z.object({
|
||||
body: z.object({
|
||||
grant_type: z.enum(['authorization_code', 'refresh_token']),
|
||||
client_id: z.string(),
|
||||
code: z.string().optional(),
|
||||
redirect_uri: z.string().optional(),
|
||||
code_verifier: z.string().optional(),
|
||||
refresh_token: z.string().optional(),
|
||||
scope: z.string().optional(),
|
||||
client_secret: z.string().optional()
|
||||
}),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* New token pair
|
||||
*/
|
||||
export const zPostOAuthTokenResponse = zOAuthTokenResponse
|
||||
|
||||
export const zPostOAuthRegisterData = z.object({
|
||||
body: zOAuthRegisterRequest,
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
/**
|
||||
* Registered. Body echoes the metadata RFC 7591 §3.2.1 requires.
|
||||
*/
|
||||
export const zPostOAuthRegisterResponse = zOAuthRegisterResponse
|
||||
|
||||
export const zListWorkspacesData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -3671,12 +3885,6 @@ export const zGetHealthData = z.object({
|
||||
*/
|
||||
export const zGetHealthResponse = z.string()
|
||||
|
||||
export const zGetOpenapiSpecData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetMonitoringTasksData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.never().optional(),
|
||||
@@ -3757,6 +3965,16 @@ export const zPostCustomNodeProxyData = z.object({
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetModelPreviewData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
folder: z.string(),
|
||||
path_index: z.number().int(),
|
||||
filename: z.string()
|
||||
}),
|
||||
query: z.never().optional()
|
||||
})
|
||||
|
||||
export const zGetLegacyPromptByIdData = z.object({
|
||||
body: z.never().optional(),
|
||||
path: z.object({
|
||||
|
||||
@@ -20,11 +20,5 @@
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:util"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,5 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/comfyRegistryTypes.ts"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1",
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:types"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"packageManager": "pnpm@10.17.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:util"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
3320
pnpm-lock.yaml
generated
@@ -2,6 +2,11 @@ packages:
|
||||
- apps/**
|
||||
- packages/**
|
||||
|
||||
ignoreWorkspaceRootCheck: true
|
||||
catalogMode: prefer
|
||||
publicHoistPattern:
|
||||
- '@parcel/watcher'
|
||||
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@astrojs/check': ^0.9.8
|
||||
@@ -16,10 +21,6 @@ catalog:
|
||||
'@iconify/utils': ^3.1.0
|
||||
'@intlify/eslint-plugin-vue-i18n': ^4.1.1
|
||||
'@lobehub/i18n-cli': ^1.26.1
|
||||
'@nx/eslint': 22.6.1
|
||||
'@nx/playwright': 22.6.1
|
||||
'@nx/storybook': 22.6.1
|
||||
'@nx/vite': 22.6.1
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@playwright/test': ^1.58.1
|
||||
'@primeuix/forms': 0.0.2
|
||||
@@ -31,7 +32,7 @@ catalog:
|
||||
'@primevue/themes': ^4.2.5
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
'@sentry/vue': ^10.32.1
|
||||
'@sparkjsdev/spark': ^0.1.10
|
||||
'@sparkjsdev/spark': ^2.1.0
|
||||
'@storybook/addon-docs': ^10.2.10
|
||||
'@storybook/addon-mcp': 0.1.6
|
||||
'@storybook/vue3': ^10.2.10
|
||||
@@ -54,7 +55,7 @@ catalog:
|
||||
'@types/jsdom': ^21.1.7
|
||||
'@types/node': ^24.1.0
|
||||
'@types/semver': ^7.7.0
|
||||
'@types/three': ^0.170.0
|
||||
'@types/three': ^0.184.1
|
||||
'@vee-validate/zod': ^4.15.1
|
||||
'@vercel/analytics': ^2.0.1
|
||||
'@vitejs/plugin-vue': ^6.0.0
|
||||
@@ -97,7 +98,6 @@ catalog:
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
monocart-coverage-reports: ^2.12.9
|
||||
nx: 22.6.1
|
||||
oxfmt: ^0.44.0
|
||||
oxlint: ^1.59.0
|
||||
oxlint-tsgolint: ^0.20.0
|
||||
@@ -113,7 +113,7 @@ catalog:
|
||||
storybook: ^10.2.10
|
||||
stylelint: ^16.26.1
|
||||
tailwindcss: ^4.3.0
|
||||
three: ^0.170.0
|
||||
three: ^0.184.0
|
||||
tailwindcss-primeui: ^0.6.1
|
||||
tsx: ^4.15.6
|
||||
tw-animate-css: ^1.3.8
|
||||
@@ -144,22 +144,19 @@ catalog:
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
- unrs-resolver
|
||||
- vue-demi
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@playwright/browser-chromium'
|
||||
- '@playwright/browser-firefox'
|
||||
- '@playwright/browser-webkit'
|
||||
- '@sentry/cli'
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
||||
- nx
|
||||
- oxc-resolver
|
||||
allowBuilds:
|
||||
'@firebase/util': false
|
||||
'@sentry/cli': true
|
||||
'@tailwindcss/oxide': true
|
||||
core-js: false
|
||||
esbuild: true
|
||||
oxc-resolver: true
|
||||
protobufjs: false
|
||||
sharp: false
|
||||
unrs-resolver: false
|
||||
vue-demi: false
|
||||
|
||||
overrides:
|
||||
vite: 'catalog:'
|
||||
'@tiptap/pm': 2.27.2
|
||||
'@types/eslint': '-'
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 274 B |
|
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 277 B |
|
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 269 B |
|
Before Width: | Height: | Size: 674 B After Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 281 B |
|
Before Width: | Height: | Size: 700 B After Width: | Height: | Size: 277 B |
|
Before Width: | Height: | Size: 702 B After Width: | Height: | Size: 280 B |
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 708 B After Width: | Height: | Size: 285 B |
|
Before Width: | Height: | Size: 705 B After Width: | Height: | Size: 296 B |
@@ -744,10 +744,6 @@ const sortOptions = computed(() => [
|
||||
value: 'popular'
|
||||
},
|
||||
{ name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },
|
||||
{
|
||||
name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Usage (Low to High)'),
|
||||
value: 'vram-low-to-high'
|
||||
},
|
||||
{
|
||||
name: t(
|
||||
'templateWorkflows.sort.modelSizeLowToHigh',
|
||||
|
||||
@@ -541,32 +541,26 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
vueNodeLifecycle.setupEmptyGraphListener()
|
||||
|
||||
// Load color palette
|
||||
colorPaletteStore.customPalettes = settingStore.get(
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
// Restore saved workflow and workflow tabs state
|
||||
await workflowPersistence.initializeWorkflow()
|
||||
await workflowPersistence.restoreWorkflowTabsState()
|
||||
await workflowPersistence.loadTemplateFromUrlIfPresent()
|
||||
} finally {
|
||||
workspaceStore.spinner = false
|
||||
}
|
||||
await workflowPersistence.loadSharedWorkflowFromUrlIfPresent()
|
||||
|
||||
comfyApp.canvas.onSelectionChange = useChainCallback(
|
||||
comfyApp.canvas.onSelectionChange,
|
||||
() => canvasStore.updateSelectedItems()
|
||||
)
|
||||
|
||||
// Load color palette
|
||||
colorPaletteStore.customPalettes = settingStore.get(
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
// Restore saved workflow and workflow tabs state
|
||||
await workflowPersistence.initializeWorkflow()
|
||||
await workflowPersistence.restoreWorkflowTabsState()
|
||||
|
||||
const sharedWorkflowLoadStatus =
|
||||
await workflowPersistence.loadSharedWorkflowFromUrlIfPresent()
|
||||
|
||||
// Load template from URL if present
|
||||
if (sharedWorkflowLoadStatus === 'not-present') {
|
||||
await workflowPersistence.loadTemplateFromUrlIfPresent()
|
||||
}
|
||||
|
||||
// Accept workspace invite from URL if present (e.g., ?invite=TOKEN)
|
||||
// WorkspaceAuthGate ensures flag state is resolved before GraphCanvas mounts
|
||||
if (inviteUrlLoader && flags.teamWorkspacesEnabled) {
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
>
|
||||
<Load3DScene
|
||||
v-if="node"
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
data-capture-wheel="true"
|
||||
tabindex="-1"
|
||||
@pointerdown.stop="focusContainer"
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
@mousedown.stop
|
||||
@mousemove.stop
|
||||
@mouseup.stop
|
||||
@contextmenu.stop.prevent
|
||||
@dragover.prevent.stop="handleDragOver"
|
||||
@dragleave.stop="handleDragLeave"
|
||||
|
||||
@@ -6,24 +6,6 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import AnimationControls from '@/components/load3d/controls/AnimationControls.vue'
|
||||
|
||||
vi.mock('primevue/select', () => ({
|
||||
default: {
|
||||
name: 'Select',
|
||||
props: ['modelValue', 'options', 'optionLabel', 'optionValue'],
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<select
|
||||
:value="modelValue"
|
||||
@change="$emit('update:modelValue', isNaN(Number($event.target.value)) ? $event.target.value : Number($event.target.value))"
|
||||
>
|
||||
<option v-for="opt in options" :key="opt[optionValue]" :value="opt[optionValue]">
|
||||
{{ opt[optionLabel] }}
|
||||
</option>
|
||||
</select>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/slider/Slider.vue', () => ({
|
||||
default: {
|
||||
name: 'UiSlider',
|
||||
|
||||
@@ -6,23 +6,6 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ViewerSceneControls from '@/components/load3d/controls/viewer/ViewerSceneControls.vue'
|
||||
|
||||
vi.mock('primevue/checkbox', () => ({
|
||||
default: {
|
||||
name: 'Checkbox',
|
||||
props: ['modelValue', 'inputId', 'binary', 'name'],
|
||||
emits: ['update:modelValue'],
|
||||
template: `
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="inputId"
|
||||
:name="name"
|
||||
:checked="modelValue"
|
||||
@change="$emit('update:modelValue', $event.target.checked)"
|
||||
/>
|
||||
`
|
||||
}
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
|
||||
@@ -3,10 +3,20 @@ import { ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { fireEvent, render, screen } from '@testing-library/vue'
|
||||
|
||||
import NodeLibrarySidebarTabV2 from './NodeLibrarySidebarTabV2.vue'
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
mockSearchNode: vi.fn<(query: string) => unknown[]>(() => [])
|
||||
}))
|
||||
|
||||
vi.mock('@/services/nodeSearchService', () => ({
|
||||
NodeSearchService: class {
|
||||
searchNode = hoisted.mockSearchNode
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
@@ -72,8 +82,10 @@ vi.mock('./nodeLibrary/NodeDragPreview.vue', () => ({
|
||||
vi.mock('@/components/ui/search-input/SearchInput.vue', () => ({
|
||||
default: {
|
||||
name: 'SearchBox',
|
||||
template: '<input data-testid="search-box" />',
|
||||
template:
|
||||
'<input data-testid="search-box" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
|
||||
props: ['modelValue', 'placeholder'],
|
||||
emits: ['update:modelValue', 'search'],
|
||||
setup() {
|
||||
return { focus: vi.fn() }
|
||||
},
|
||||
@@ -84,12 +96,22 @@ vi.mock('@/components/ui/search-input/SearchInput.vue', () => ({
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: {} }
|
||||
messages: {
|
||||
en: {
|
||||
sideToolbar: {
|
||||
nodeLibraryTab: {
|
||||
noMatchingNodes: 'No nodes match "{query}"'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('NodeLibrarySidebarTabV2', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
hoisted.mockSearchNode.mockReset()
|
||||
hoisted.mockSearchNode.mockReturnValue([])
|
||||
})
|
||||
|
||||
function renderComponent() {
|
||||
@@ -123,4 +145,49 @@ describe('NodeLibrarySidebarTabV2', () => {
|
||||
expect(screen.queryByTestId('all-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('blueprints-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('search empty state', () => {
|
||||
it('does not render the empty state when search query is empty', () => {
|
||||
renderComponent()
|
||||
|
||||
expect(screen.queryByText(/No nodes match/)).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('essential-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the empty state with the query when search has no matches', async () => {
|
||||
hoisted.mockSearchNode.mockReturnValue([])
|
||||
renderComponent()
|
||||
|
||||
await fireEvent.update(screen.getByTestId('search-box'), 'gibberish')
|
||||
|
||||
expect(screen.getByText('No nodes match "gibberish"')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('essential-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('all-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('blueprints-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the empty state when the search has matches', async () => {
|
||||
hoisted.mockSearchNode.mockReturnValue([{ name: 'KSampler' }])
|
||||
renderComponent()
|
||||
|
||||
await fireEvent.update(screen.getByTestId('search-box'), 'ksampler')
|
||||
|
||||
expect(screen.queryByText(/No nodes match/)).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('essential-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the empty state once the query is cleared', async () => {
|
||||
hoisted.mockSearchNode.mockReturnValue([])
|
||||
renderComponent()
|
||||
|
||||
const input = screen.getByTestId('search-box')
|
||||
await fireEvent.update(input, 'gibberish')
|
||||
expect(screen.getByText('No nodes match "gibberish"')).toBeInTheDocument()
|
||||
|
||||
await fireEvent.update(input, '')
|
||||
|
||||
expect(screen.queryByText(/No nodes match/)).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('essential-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -117,7 +117,17 @@
|
||||
<template #body>
|
||||
<NodeDragPreview />
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="min-h-0 flex-1 overflow-y-auto py-2">
|
||||
<div
|
||||
v-if="hasNoMatches"
|
||||
class="flex min-h-0 flex-1 items-center justify-center px-6 py-8 text-center text-sm text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
$t('sideToolbar.nodeLibraryTab.noMatchingNodes', {
|
||||
query: searchQuery
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div v-else class="min-h-0 flex-1 overflow-y-auto py-2">
|
||||
<TabPanel
|
||||
v-if="flags.nodeLibraryEssentialsEnabled"
|
||||
:model-value="selectedTab"
|
||||
@@ -274,9 +284,13 @@ const filteredNodeDefs = computed(() => {
|
||||
})
|
||||
|
||||
const activeNodes = computed(() =>
|
||||
filteredNodeDefs.value.length > 0
|
||||
? filteredNodeDefs.value
|
||||
: nodeDefStore.visibleNodeDefs
|
||||
searchQuery.value.length === 0
|
||||
? nodeDefStore.visibleNodeDefs
|
||||
: filteredNodeDefs.value
|
||||
)
|
||||
|
||||
const hasNoMatches = computed(
|
||||
() => searchQuery.value.length > 0 && filteredNodeDefs.value.length === 0
|
||||
)
|
||||
|
||||
const sections = computed(() => {
|
||||
|
||||
@@ -25,17 +25,19 @@ type Searcher = NonNullable<ComponentProps<typeof AsyncSearchInput>['searcher']>
|
||||
function renderSearch(
|
||||
initialQuery: string = '',
|
||||
searcher?: Searcher,
|
||||
updateKey?: { value: unknown }
|
||||
updateKey?: { value: unknown },
|
||||
onEnter?: (event: KeyboardEvent) => void
|
||||
) {
|
||||
const query = ref(initialQuery)
|
||||
const key = updateKey
|
||||
const Harness = defineComponent({
|
||||
components: { AsyncSearchInput },
|
||||
setup: () => ({ query, searcher, key }),
|
||||
setup: () => ({ query, searcher, key, onEnter }),
|
||||
template: `<AsyncSearchInput
|
||||
v-model="query"
|
||||
:searcher="searcher"
|
||||
:update-key="key"
|
||||
@enter="onEnter"
|
||||
/>`
|
||||
})
|
||||
const utils = render(Harness, { global: { plugins: [i18n] } })
|
||||
@@ -63,6 +65,14 @@ describe('AsyncSearchInput', () => {
|
||||
await user.type(screen.getByRole('textbox'), 'abc')
|
||||
expect(query.value).toBe('abc')
|
||||
})
|
||||
|
||||
it('emits enter when the user presses Enter in the textbox', async () => {
|
||||
const onEnter = vi.fn()
|
||||
renderSearch('', undefined, undefined, onEnter)
|
||||
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
|
||||
await user.type(screen.getByRole('textbox'), '{Enter}')
|
||||
expect(onEnter).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Clear button', () => {
|
||||
|
||||
@@ -23,6 +23,9 @@ const {
|
||||
debounceMaxWaitMs?: number
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
enter: [event: KeyboardEvent]
|
||||
}>()
|
||||
|
||||
const searchQuery = defineModel<string>({ default: '' })
|
||||
|
||||
@@ -62,6 +65,11 @@ function handleFocus(event: FocusEvent) {
|
||||
target.select()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydownEnter(event: KeyboardEvent) {
|
||||
if (event.isComposing) return
|
||||
emit('enter', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -97,6 +105,7 @@ function handleFocus(event: FocusEvent) {
|
||||
:placeholder="$t('g.searchPlaceholder', { subject: '' })"
|
||||
:autofocus
|
||||
@focus="handleFocus"
|
||||
@keydown.enter="handleKeydownEnter"
|
||||
/>
|
||||
<button
|
||||
v-if="searchQuery.trim().length > 0"
|
||||
|
||||
@@ -53,7 +53,6 @@ const sortOptions: SelectOption[] = [
|
||||
{ name: 'Recommended', value: 'recommended' },
|
||||
{ name: 'Popular', value: 'popular' },
|
||||
{ name: 'Newest', value: 'newest' },
|
||||
{ name: 'VRAM Usage (Low to High)', value: 'vram-low-to-high' },
|
||||
{ name: 'Model Size (Low to High)', value: 'model-size-low-to-high' },
|
||||
{ name: 'Alphabetical (A-Z)', value: 'alphabetical' }
|
||||
]
|
||||
|
||||
@@ -3,20 +3,27 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type { useNodeDragToCanvas as UseNodeDragToCanvasType } from './useNodeDragToCanvas'
|
||||
|
||||
const { mockAddNodeOnGraph, mockConvertEventToCanvasOffset, mockCanvas } =
|
||||
vi.hoisted(() => {
|
||||
const mockConvertEventToCanvasOffset = vi.fn()
|
||||
return {
|
||||
mockAddNodeOnGraph: vi.fn(),
|
||||
mockConvertEventToCanvasOffset,
|
||||
mockCanvas: {
|
||||
canvas: {
|
||||
getBoundingClientRect: vi.fn()
|
||||
},
|
||||
convertEventToCanvasOffset: mockConvertEventToCanvasOffset
|
||||
}
|
||||
const {
|
||||
mockAddNodeOnGraph,
|
||||
mockConvertEventToCanvasOffset,
|
||||
mockSelectItems,
|
||||
mockCanvas
|
||||
} = vi.hoisted(() => {
|
||||
const mockConvertEventToCanvasOffset = vi.fn()
|
||||
const mockSelectItems = vi.fn()
|
||||
return {
|
||||
mockAddNodeOnGraph: vi.fn(),
|
||||
mockConvertEventToCanvasOffset,
|
||||
mockSelectItems,
|
||||
mockCanvas: {
|
||||
canvas: {
|
||||
getBoundingClientRect: vi.fn()
|
||||
},
|
||||
convertEventToCanvasOffset: mockConvertEventToCanvasOffset,
|
||||
selectItems: mockSelectItems
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn(() => ({
|
||||
@@ -119,6 +126,11 @@ describe('useNodeDragToCanvas', () => {
|
||||
'pointermove',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'pointerdown',
|
||||
expect.any(Function),
|
||||
true
|
||||
)
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
||||
'pointerup',
|
||||
expect.any(Function),
|
||||
@@ -239,6 +251,57 @@ describe('useNodeDragToCanvas', () => {
|
||||
expect(isDragging.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should select the placed node when one is returned from the graph', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
const placedNode = { id: 1 }
|
||||
mockAddNodeOnGraph.mockReturnValue(placedNode)
|
||||
|
||||
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
|
||||
expect(mockSelectItems).toHaveBeenCalledWith([placedNode])
|
||||
})
|
||||
|
||||
it('should not call selectItems when graph returns no node', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
mockConvertEventToCanvasOffset.mockReturnValue([150, 150])
|
||||
mockAddNodeOnGraph.mockReturnValue(null)
|
||||
|
||||
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
document.dispatchEvent(
|
||||
new PointerEvent('pointerup', {
|
||||
clientX: 250,
|
||||
clientY: 250,
|
||||
bubbles: true
|
||||
})
|
||||
)
|
||||
|
||||
expect(mockSelectItems).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not add node on pointerup when in native drag mode', () => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
@@ -339,4 +402,58 @@ describe('useNodeDragToCanvas', () => {
|
||||
expect(dragMode.value).toBe('click')
|
||||
})
|
||||
})
|
||||
|
||||
describe('blockCommitPointerDown', () => {
|
||||
function dispatchPointerDown(x: number, y: number) {
|
||||
const event = new PointerEvent('pointerdown', {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
const stopSpy = vi.spyOn(event, 'stopImmediatePropagation')
|
||||
document.dispatchEvent(event)
|
||||
return stopSpy
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockCanvas.canvas.getBoundingClientRect.mockReturnValue({
|
||||
left: 0,
|
||||
right: 500,
|
||||
top: 0,
|
||||
bottom: 500
|
||||
})
|
||||
})
|
||||
|
||||
it('should stop propagation when in click-drag mode over canvas', () => {
|
||||
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
expect(dispatchPointerDown(250, 250)).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not stop propagation when not dragging', () => {
|
||||
const { setupGlobalListeners } = useNodeDragToCanvas()
|
||||
setupGlobalListeners()
|
||||
|
||||
expect(dispatchPointerDown(250, 250)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not stop propagation in native drag mode', () => {
|
||||
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef, 'native')
|
||||
|
||||
expect(dispatchPointerDown(250, 250)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not stop propagation when pointer is outside canvas', () => {
|
||||
const { startDrag, setupGlobalListeners } = useNodeDragToCanvas()
|
||||
setupGlobalListeners()
|
||||
startDrag(mockNodeDef)
|
||||
|
||||
expect(dispatchPointerDown(600, 250)).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,31 +22,33 @@ function cancelDrag() {
|
||||
dragMode.value = 'click'
|
||||
}
|
||||
|
||||
function addNodeAtPosition(clientX: number, clientY: number): boolean {
|
||||
if (!draggedNode.value) return false
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const canvas = canvasStore.canvas
|
||||
if (!canvas) return false
|
||||
|
||||
const canvasElement = canvas.canvas as HTMLCanvasElement
|
||||
function isOverCanvas(clientX: number, clientY: number): boolean {
|
||||
const canvasElement = useCanvasStore().canvas?.canvas as
|
||||
| HTMLCanvasElement
|
||||
| undefined
|
||||
if (!canvasElement) return false
|
||||
const rect = canvasElement.getBoundingClientRect()
|
||||
const isOverCanvas =
|
||||
return (
|
||||
clientX >= rect.left &&
|
||||
clientX <= rect.right &&
|
||||
clientY >= rect.top &&
|
||||
clientY <= rect.bottom
|
||||
)
|
||||
}
|
||||
|
||||
if (isOverCanvas) {
|
||||
const pos = canvas.convertEventToCanvasOffset({
|
||||
clientX,
|
||||
clientY
|
||||
} as PointerEvent)
|
||||
const litegraphService = useLitegraphService()
|
||||
litegraphService.addNodeOnGraph(draggedNode.value, { pos })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
function addNodeAtPosition(clientX: number, clientY: number): boolean {
|
||||
if (!draggedNode.value) return false
|
||||
const canvas = useCanvasStore().canvas
|
||||
if (!canvas) return false
|
||||
if (!isOverCanvas(clientX, clientY)) return false
|
||||
|
||||
const pos = canvas.convertEventToCanvasOffset({
|
||||
clientX,
|
||||
clientY
|
||||
} as PointerEvent)
|
||||
const node = useLitegraphService().addNodeOnGraph(draggedNode.value, { pos })
|
||||
if (node) canvas.selectItems([node])
|
||||
return true
|
||||
}
|
||||
|
||||
function endDrag(e: PointerEvent) {
|
||||
@@ -64,11 +66,19 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') cancelDrag()
|
||||
}
|
||||
|
||||
// Prevent LiteGraph's empty-canvas hit-test from deselecting the placed node on pointerup.
|
||||
function blockCommitPointerDown(e: PointerEvent) {
|
||||
if (!isDragging.value || dragMode.value !== 'click') return
|
||||
if (!isOverCanvas(e.clientX, e.clientY)) return
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
function setupGlobalListeners() {
|
||||
if (listenersSetup) return
|
||||
listenersSetup = true
|
||||
|
||||
document.addEventListener('pointermove', updatePosition)
|
||||
document.addEventListener('pointerdown', blockCommitPointerDown, true)
|
||||
document.addEventListener('pointerup', endDrag, true)
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
@@ -78,6 +88,7 @@ function cleanupGlobalListeners() {
|
||||
listenersSetup = false
|
||||
|
||||
document.removeEventListener('pointermove', updatePosition)
|
||||
document.removeEventListener('pointerdown', blockCommitPointerDown, true)
|
||||
document.removeEventListener('pointerup', endDrag, true)
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
|
||||
|
||||
@@ -78,59 +78,6 @@ describe('useTemplateFiltering', () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('sorts templates by VRAM from low to high and pushes missing values last', () => {
|
||||
const gb = (value: number) => value * 1024 ** 3
|
||||
|
||||
const templates = ref<TemplateInfo[]>([
|
||||
{
|
||||
name: 'missing-vram',
|
||||
description: 'no vram value',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png'
|
||||
},
|
||||
{
|
||||
name: 'highest-vram',
|
||||
description: 'high usage',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
vram: gb(12)
|
||||
},
|
||||
{
|
||||
name: 'mid-vram',
|
||||
description: 'medium usage',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
vram: gb(7.5)
|
||||
},
|
||||
{
|
||||
name: 'low-vram',
|
||||
description: 'low usage',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
vram: gb(5)
|
||||
},
|
||||
{
|
||||
name: 'zero-vram',
|
||||
description: 'unknown usage',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'png',
|
||||
vram: 0
|
||||
}
|
||||
])
|
||||
|
||||
const { sortBy, filteredTemplates } = useTemplateFiltering(templates)
|
||||
|
||||
sortBy.value = 'vram-low-to-high'
|
||||
|
||||
expect(filteredTemplates.value.map((template) => template.name)).toEqual([
|
||||
'low-vram',
|
||||
'mid-vram',
|
||||
'highest-vram',
|
||||
'missing-vram',
|
||||
'zero-vram'
|
||||
])
|
||||
})
|
||||
|
||||
it('filters by search text, models, tags, and license with debounce handling', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
|
||||
@@ -220,17 +220,6 @@ export function useTemplateFiltering(
|
||||
})
|
||||
})
|
||||
|
||||
const getVramMetric = (template: TemplateInfo) => {
|
||||
if (
|
||||
typeof template.vram === 'number' &&
|
||||
Number.isFinite(template.vram) &&
|
||||
template.vram > 0
|
||||
) {
|
||||
return template.vram
|
||||
}
|
||||
return Number.POSITIVE_INFINITY
|
||||
}
|
||||
|
||||
watch(
|
||||
filteredByRunsOn,
|
||||
(templates) => {
|
||||
@@ -279,22 +268,6 @@ export function useTemplateFiltering(
|
||||
const dateB = new Date(b.date || '1970-01-01')
|
||||
return dateB.getTime() - dateA.getTime()
|
||||
})
|
||||
case 'vram-low-to-high':
|
||||
return templates.sort((a, b) => {
|
||||
const vramA = getVramMetric(a)
|
||||
const vramB = getVramMetric(b)
|
||||
|
||||
if (vramA === vramB) {
|
||||
const nameA = a.title || a.name || ''
|
||||
const nameB = b.title || b.name || ''
|
||||
return nameA.localeCompare(nameB)
|
||||
}
|
||||
|
||||
if (vramA === Number.POSITIVE_INFINITY) return 1
|
||||
if (vramB === Number.POSITIVE_INFINITY) return -1
|
||||
|
||||
return vramA - vramB
|
||||
})
|
||||
case 'model-size-low-to-high':
|
||||
return templates.sort((a, b) => {
|
||||
const sizeA =
|
||||
|
||||
@@ -321,12 +321,9 @@ function withComfyMatchType(node: LGraphNode): asserts node is MatchTypeNode {
|
||||
if (!outputType) throw new Error('invalid connection')
|
||||
this.outputs.forEach((output, idx) => {
|
||||
if (!(outputGroups?.[idx] == matchKey)) return
|
||||
this.outputs[idx] = shallowReactive(this.outputs[idx])
|
||||
changeOutputType(this, output, outputType)
|
||||
})
|
||||
// Force Vue reactivity update for output slot types.
|
||||
// Outputs are wrapped in shallowReactive by useGraphNodeManager,
|
||||
// so mutating output.type alone doesn't trigger re-render.
|
||||
this.outputs = [...this.outputs]
|
||||
app.canvas?.setDirty(true, true)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { clearOAuthRequestId } from '@/platform/cloud/oauth/oauthState'
|
||||
import { useSessionCookie } from '@/platform/auth/session/useSessionCookie'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
@@ -19,6 +20,7 @@ useExtensionService().registerExtension({
|
||||
},
|
||||
|
||||
onAuthUserLogout: async () => {
|
||||
clearOAuthRequestId()
|
||||
const { deleteSession } = useSessionCookie()
|
||||
await deleteSession()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SparkRenderer } from '@sparkjsdev/spark'
|
||||
import * as THREE from 'three'
|
||||
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -137,6 +138,13 @@ describe('SceneManager', () => {
|
||||
expect(manager.scene.children).toContain(manager.gridHelper)
|
||||
})
|
||||
|
||||
it('adds a SparkRenderer to the scene so SplatMesh instances render', () => {
|
||||
const sparkRenderers = manager.scene.children.filter(
|
||||
(child) => child instanceof SparkRenderer
|
||||
)
|
||||
expect(sparkRenderers).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('builds a separate background scene with a tiled mesh', () => {
|
||||
expect(manager.backgroundScene).toBeInstanceOf(THREE.Scene)
|
||||
expect(manager.backgroundMesh).toBeInstanceOf(THREE.Mesh)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SparkRenderer } from '@sparkjsdev/spark'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
export class SceneManager implements SceneManagerInterface {
|
||||
scene!: THREE.Scene
|
||||
gridHelper: THREE.GridHelper
|
||||
private sparkRenderer: SparkRenderer
|
||||
|
||||
backgroundScene!: THREE.Scene
|
||||
backgroundCamera: THREE.OrthographicCamera
|
||||
@@ -42,6 +44,12 @@ export class SceneManager implements SceneManagerInterface {
|
||||
|
||||
this.getActiveCamera = getActiveCamera
|
||||
|
||||
// Spark 2.x requires a SparkRenderer in the scene tree to render SplatMesh
|
||||
// (Gaussian splat) instances; without it splats are silent no-ops. Kept
|
||||
// alive across model reloads by SceneModelManager.clearModel.
|
||||
this.sparkRenderer = new SparkRenderer({ renderer })
|
||||
this.scene.add(this.sparkRenderer)
|
||||
|
||||
this.gridHelper = new THREE.GridHelper(20, 20)
|
||||
this.gridHelper.position.set(0, 0, 0)
|
||||
this.scene.add(this.gridHelper)
|
||||
@@ -277,8 +285,8 @@ export class SceneManager implements SceneManagerInterface {
|
||||
|
||||
if (!material.map) return
|
||||
|
||||
const imageAspect =
|
||||
backgroundTexture.image.width / backgroundTexture.image.height
|
||||
const image = backgroundTexture.image as { width: number; height: number }
|
||||
const imageAspect = image.width / image.height
|
||||
const targetAspect = targetWidth / targetHeight
|
||||
|
||||
if (imageAspect > targetAspect) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SparkRenderer } from '@sparkjsdev/spark'
|
||||
import * as THREE from 'three'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -355,6 +356,20 @@ describe('SceneModelManager', () => {
|
||||
expect(geoDispose).toHaveBeenCalled()
|
||||
expect(matDispose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves SparkRenderer across model reloads', async () => {
|
||||
const { manager, scene } = createManager()
|
||||
const sparkRenderer = new SparkRenderer({
|
||||
renderer: {} as THREE.WebGLRenderer
|
||||
})
|
||||
scene.add(sparkRenderer)
|
||||
|
||||
const model = createMeshModel()
|
||||
await manager.setupModel(model)
|
||||
manager.clearModel()
|
||||
|
||||
expect(scene.children).toContain(sparkRenderer)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reset', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SparkRenderer } from '@sparkjsdev/spark'
|
||||
import * as THREE from 'three'
|
||||
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
|
||||
@@ -317,6 +318,7 @@ export class SceneModelManager implements ModelManagerInterface {
|
||||
object instanceof THREE.GridHelper ||
|
||||
object instanceof THREE.Light ||
|
||||
object instanceof THREE.Camera ||
|
||||
object instanceof SparkRenderer ||
|
||||
object.name === 'GizmoTransformControls'
|
||||
|
||||
if (!isEnvironmentObject) {
|
||||
|
||||
@@ -800,6 +800,8 @@
|
||||
"CONTROL_NET": "ControlNet",
|
||||
"CURVE": "منحنى",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "نموذج كشف الوجه",
|
||||
"FACE_LANDMARKS": "معالم الوجه",
|
||||
"FILE_3D": "ملف ثلاثي الأبعاد",
|
||||
"FILE_3D_FBX": "ملف FBX ثلاثي الأبعاد",
|
||||
"FILE_3D_GLB": "ملف GLB ثلاثي الأبعاد",
|
||||
@@ -2278,6 +2280,7 @@
|
||||
"Meshy": "Meshy",
|
||||
"MiniMax": "MiniMax",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenRouter": "OpenRouter",
|
||||
"PixVerse": "PixVerse",
|
||||
"Quiver": "Quiver",
|
||||
"Recraft": "Recraft",
|
||||
@@ -2294,6 +2297,7 @@
|
||||
"Vidu": "فيدو",
|
||||
"Wan": "وان",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"adjustments": "تعديلات",
|
||||
"advanced": "متقدم",
|
||||
"api node": "عقدة API",
|
||||
"attention_experiments": "تجارب الانتباه",
|
||||
@@ -2443,7 +2447,8 @@
|
||||
"nonPublicAssetsWarningLine1": "يأتي هذا سير العمل مع أصول غير عامة.",
|
||||
"nonPublicAssetsWarningLine2": "سيتم استيراد هذه الأصول إلى مكتبتك عند فتح سير العمل",
|
||||
"openWithoutImporting": "فتح بدون استيراد",
|
||||
"openWorkflow": "فتح سير العمل"
|
||||
"openWorkflow": "فتح سير العمل",
|
||||
"opening": "جارٍ فتح سير العمل المشترك..."
|
||||
},
|
||||
"painter": {
|
||||
"background": "الخلفية",
|
||||
@@ -3620,7 +3625,8 @@
|
||||
"placeholderMesh": "اختر شبكة...",
|
||||
"placeholderModel": "اختر نموذج...",
|
||||
"placeholderUnknown": "اختر وسائط...",
|
||||
"placeholderVideo": "اختر فيديو..."
|
||||
"placeholderVideo": "اختر فيديو...",
|
||||
"topResult": "أفضل نتيجة: {result}"
|
||||
},
|
||||
"valueControl": {
|
||||
"decrement": "إنقاص القيمة",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
}
|
||||
},
|
||||
"AdjustBrightness": {
|
||||
"description": "ضبط سطوع الصورة.",
|
||||
"display_name": "ضبط السطوع",
|
||||
"inputs": {
|
||||
"factor": {
|
||||
@@ -138,6 +139,7 @@
|
||||
}
|
||||
},
|
||||
"AdjustContrast": {
|
||||
"description": "ضبط تباين الصورة.",
|
||||
"display_name": "ضبط التباين",
|
||||
"inputs": {
|
||||
"factor": {
|
||||
@@ -176,6 +178,7 @@
|
||||
}
|
||||
},
|
||||
"AudioAdjustVolume": {
|
||||
"description": "ضبط مستوى صوت الملف الصوتي بمقدار محدد بوحدة ديسيبل (dB).",
|
||||
"display_name": "ضبط مستوى الصوت",
|
||||
"inputs": {
|
||||
"audio": {
|
||||
@@ -577,6 +580,9 @@
|
||||
"model_auto_downscale": {
|
||||
"name": "التقليل التلقائي للحجم"
|
||||
},
|
||||
"model_auto_upscale": {
|
||||
"name": "auto_upscale"
|
||||
},
|
||||
"model_duration": {
|
||||
"name": "المدة"
|
||||
},
|
||||
@@ -1547,6 +1553,7 @@
|
||||
}
|
||||
},
|
||||
"CenterCropImages": {
|
||||
"description": "قص الصورة من المنتصف إلى الأبعاد المحددة.",
|
||||
"display_name": "قص الصور من المنتصف",
|
||||
"inputs": {
|
||||
"height": {
|
||||
@@ -1666,6 +1673,9 @@
|
||||
"model_max_tokens": {
|
||||
"name": "max_tokens"
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"model_temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
@@ -5699,6 +5709,7 @@
|
||||
}
|
||||
},
|
||||
"ImageCropV2": {
|
||||
"description": "قص الصورة إلى الأبعاد المحددة.",
|
||||
"display_name": "قص الصورة",
|
||||
"inputs": {
|
||||
"crop_region": {
|
||||
@@ -5719,6 +5730,7 @@
|
||||
}
|
||||
},
|
||||
"ImageDeduplication": {
|
||||
"description": "إزالة الصور المكررة أو المتشابهة جداً من القائمة.",
|
||||
"display_name": "إزالة تكرار الصور",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -5773,6 +5785,7 @@
|
||||
}
|
||||
},
|
||||
"ImageGrid": {
|
||||
"description": "ترتيب عدة صور في شبكة.",
|
||||
"display_name": "شبكة الصور",
|
||||
"inputs": {
|
||||
"cell_height": {
|
||||
@@ -8366,6 +8379,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageDataSetFromFolder": {
|
||||
"description": "تحميل مجموعة بيانات من الصور من مجلد محدد وإرجاع قائمة بالصور. الصيغ المدعومة: PNG، JPG، JPEG، WEBP.",
|
||||
"display_name": "تحميل مجموعة بيانات الصور من مجلد",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8409,6 +8423,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageTextDataSetFromFolder": {
|
||||
"description": "تحميل مجموعة بيانات من أزواج الصور والتعليقات النصية من مجلد محدد وإرجاعها كقائمة. الصيغ المدعومة: PNG، JPG، JPEG، WEBP.",
|
||||
"display_name": "تحميل مجموعة بيانات الصور والنصوص من مجلد",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8435,6 +8450,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMediaPipeFaceLandmarker": {
|
||||
"display_name": "تحميل MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name",
|
||||
"tooltip": "ملف safetensors الخاص بـ Face Landmarker من models/mediapipe/."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMoGeModel": {
|
||||
"display_name": "تحميل نموذج MoGe",
|
||||
"inputs": {
|
||||
@@ -8449,6 +8478,7 @@
|
||||
}
|
||||
},
|
||||
"LoadTrainingDataset": {
|
||||
"description": "تحميل مجموعة بيانات التدريب المشفرة (الفضاء الكامن + الشروط) من القرص لاستخدامها في التدريب.",
|
||||
"display_name": "تحميل مجموعة بيانات التدريب",
|
||||
"inputs": {
|
||||
"folder_name": {
|
||||
@@ -9232,6 +9262,7 @@
|
||||
}
|
||||
},
|
||||
"MakeTrainingDataset": {
|
||||
"description": "ترميز الصور باستخدام VAE والنصوص باستخدام CLIP لإنشاء مجموعة بيانات تدريبية من الفضاء الكامن والشروط.",
|
||||
"display_name": "إنشاء مجموعة بيانات تدريبية",
|
||||
"inputs": {
|
||||
"clip": {
|
||||
@@ -9322,7 +9353,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceLandmarker": {
|
||||
"description": "اكتشاف معالم الوجه باستخدام نموذج MediaPipe.",
|
||||
"display_name": "MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"detector_variant": {
|
||||
"name": "detector_variant",
|
||||
"tooltip": "نطاق كاشف الوجه. 'short' مخصص للوجوه القريبة (ضمن ~٢ متر من الكاميرا)؛ 'full' يغطي الوجوه البعيدة/الأصغر (حتى ~٥ متر) لكنه أبطأ. 'both' يشغل كلا الكاشفين ويحتفظ بالكاشف الذي وجد وجوهًا أكثر في كل إطار (تكلفة كشف مضاعفة تقريبًا)."
|
||||
},
|
||||
"face_detection_model": {
|
||||
"name": "face_detection_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "الصورة"
|
||||
},
|
||||
"min_confidence": {
|
||||
"name": "min_confidence",
|
||||
"tooltip": "عتبة درجة BlazeFace. خفضها لالتقاط الوجوه الصغيرة/المحجوبة."
|
||||
},
|
||||
"missing_frame_fallback": {
|
||||
"name": "missing_frame_fallback",
|
||||
"tooltip": "سلوك كل إطار عند فشل الكشف في دفعة. 'empty' يترك الإطار بدون وجه. 'previous' ينسخ آخر كشف ناجح. 'interpolate' يقوم بتقريب المعالم/الصندوق/أشكال المزج بين الإطارات الناجحة المحيطة. في حالة تعدد الوجوه: يتم إقران الوجوه عبر الإطارات بأقرب مركز صندوق."
|
||||
},
|
||||
"num_faces": {
|
||||
"name": "num_faces",
|
||||
"tooltip": "أقصى عدد للوجوه التي يتم إرجاعها في كل إطار. ٠ = بدون حد (إرجاع جميع الوجوه المكتشفة)."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "face_landmarks",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "bboxes",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMask": {
|
||||
"description": "رسم قناع باستخدام معالم الوجه.",
|
||||
"display_name": "MediaPipe Face Mask",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"regions": {
|
||||
"name": "regions",
|
||||
"tooltip": "'all' = اتحاد face_oval+lips+eyes+irises (والذي يختصر إلى face_oval لأنه يحيط بالباقي). 'custom' = تفعيل كل منطقة بشكل فردي لتكوينات مثل lips+eyes."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMeshVisualize": {
|
||||
"description": "رسم شبكة معالم الوجه على الصورة المدخلة.",
|
||||
"display_name": "تصوير شبكة وجه MediaPipe",
|
||||
"inputs": {
|
||||
"color": {
|
||||
"name": "color"
|
||||
},
|
||||
"connections": {
|
||||
"name": "connections",
|
||||
"tooltip": "'all' = oval+eyes+brows+lips+irises+nose. 'fill' = مضلع face_oval صلب (قناع صورة ظلية). 'custom' = تفعيل كل ميزة بشكل فردي (بما في ذلك 'tesselation'، شبكة الأسلاك الكاملة ذات ٢٥٤٧ حافة)."
|
||||
},
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"image": {
|
||||
"name": "الصورة",
|
||||
"tooltip": "إذا لم يتم توصيلها، سيتم استخدام لوحة سوداء."
|
||||
},
|
||||
"point_size": {
|
||||
"name": "point_size",
|
||||
"tooltip": "نصف قطر نقطة المعلم بالبكسل. ٠ يعطل رسم النقاط."
|
||||
},
|
||||
"thickness": {
|
||||
"name": "thickness",
|
||||
"tooltip": "سُمك خط الحافة بالبكسل. ٠ يعطل رسم الحواف."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MergeImageLists": {
|
||||
"description": "دمج عدة قوائم صور في قائمة واحدة.",
|
||||
"display_name": "دمج قوائم الصور",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -9777,6 +9898,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeInference": {
|
||||
"description": "تشغيل MoGe على صورة واحدة لتقدير العمق والهندسة.",
|
||||
"display_name": "استدلال MoGe",
|
||||
"inputs": {
|
||||
"apply_mask": {
|
||||
@@ -9813,6 +9935,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePanoramaInference": {
|
||||
"description": "تشغيل MoGe على صورة بانورامية إكويركتانجولار عن طريق تقسيمها إلى ١٢ منظوراً، إجراء الاستدلال على كل منها، ودمج النتائج في خريطة عمق واحدة.",
|
||||
"display_name": "استدلال MoGe بانوراما",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
@@ -9847,6 +9970,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePointMapToMesh": {
|
||||
"description": "تحويل خريطة نقاط MoGe إلى شبكة ثلاثية الأبعاد.",
|
||||
"display_name": "MoGe تحويل خريطة النقاط إلى شبكة",
|
||||
"inputs": {
|
||||
"batch_index": {
|
||||
@@ -9876,6 +10000,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeRender": {
|
||||
"description": "عرض خريطة عمق أو خريطة عادية من بيانات الهندسة.",
|
||||
"display_name": "MoGe عرض",
|
||||
"inputs": {
|
||||
"moge_geometry": {
|
||||
@@ -12164,6 +12289,7 @@
|
||||
}
|
||||
},
|
||||
"NormalizeImages": {
|
||||
"description": "تطبيع الصور باستخدام المتوسط والانحراف المعياري.",
|
||||
"display_name": "تطبيع الصور",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -12493,6 +12619,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenRouterLLMNode": {
|
||||
"description": "توليد ردود نصية عبر OpenRouter. يوجه إلى مجموعة مختارة من النماذج الشهيرة من xAI، DeepSeek، Qwen، Mistral، Z.AI (GLM)، Moonshot (Kimi)، وPerplexity Sonar.",
|
||||
"display_name": "OpenRouter LLM",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "نموذج OpenRouter المستخدم لتوليد الرد."
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "إدخال نصي للنموذج."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "بذرة العينة. اضبطها على 0 للتجاهل. معظم النماذج تعتبرها مجرد إشارة."
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "system_prompt",
|
||||
"tooltip": "تعليمات أساسية تحدد سلوك النموذج."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpticalFlowLoader": {
|
||||
"display_name": "تحميل نموذج التدفق البصري",
|
||||
"inputs": {
|
||||
@@ -13306,6 +13465,7 @@
|
||||
}
|
||||
},
|
||||
"RandomCropImages": {
|
||||
"description": "قص الصورة بشكل عشوائي إلى الأبعاد المحددة.",
|
||||
"display_name": "قص عشوائي للصور",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -14168,6 +14328,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByLongerEdge": {
|
||||
"description": "تغيير حجم الصور بحيث يتطابق الحافة الأطول مع البعد المحدد مع الحفاظ على نسبة العرض إلى الارتفاع.",
|
||||
"display_name": "تغيير حجم الصور حسب الحافة الأطول",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14187,6 +14348,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByShorterEdge": {
|
||||
"description": "تغيير حجم الصور بحيث يتطابق الحافة الأقصر مع البُعد المحدد مع الحفاظ على نسبة العرض إلى الارتفاع.",
|
||||
"display_name": "تغيير حجم الصور حسب الحافة الأقصر",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14206,6 +14368,7 @@
|
||||
}
|
||||
},
|
||||
"ResolutionBucket": {
|
||||
"description": "تجميع الـ latent والتهيئات في مجموعات (buckets)",
|
||||
"display_name": "تجميع الدقة",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15550,6 +15713,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageDataSetToFolder": {
|
||||
"description": "حفظ مجموعة بيانات من الصور في مجلد محدد. الصيغ المدعومة: PNG.",
|
||||
"display_name": "حفظ مجموعة بيانات الصور في مجلد",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15567,6 +15731,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageTextDataSetToFolder": {
|
||||
"description": "حفظ مجموعة بيانات من أزواج الصور والتعليقات النصية في مجلد محدد. تُحفظ الصور كملفات PNG والتعليقات كملفات TXT بنفس بادئة اسم الملف.",
|
||||
"display_name": "حفظ مجموعة بيانات الصور والنصوص في مجلد",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15637,6 +15802,7 @@
|
||||
}
|
||||
},
|
||||
"SaveTrainingDataset": {
|
||||
"description": "حفظ مجموعة بيانات التدريب المشفرة (latents + التهيئة) على القرص لتحميلها بكفاءة أثناء التدريب.",
|
||||
"display_name": "حفظ مجموعة بيانات التدريب",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15823,6 +15989,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleDataset": {
|
||||
"description": "تبديل ترتيب الصور في القائمة بشكل عشوائي.",
|
||||
"display_name": "تبديل ترتيب مجموعة بيانات الصور",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -15845,6 +16012,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleImageTextDataset": {
|
||||
"description": "تبديل ترتيب أزواج الصورة والنص في القائمة بشكل عشوائي.",
|
||||
"display_name": "تبديل ترتيب مجموعة بيانات الصور والنصوص",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -16661,6 +16829,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringFormat": {
|
||||
"description": "مماثل لطريقة تنسيق السلاسل النصية في بايثون. يدعم جميع خيارات وميزات التنسيق في بايثون.",
|
||||
"display_name": "تنسيق النص",
|
||||
"inputs": {
|
||||
"f_string": {
|
||||
"name": "f_string"
|
||||
},
|
||||
"values": {
|
||||
"name": "values"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringLength": {
|
||||
"display_name": "الطول",
|
||||
"inputs": {
|
||||
@@ -19328,6 +19513,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMesh": {
|
||||
"description": "تحويل شبكة فوكسل إلى شبكة مضلعة (mesh).",
|
||||
"display_name": "تحويل الفوكسل إلى شبكة",
|
||||
"inputs": {
|
||||
"algorithm": {
|
||||
@@ -19347,6 +19533,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"description": "تحويل شبكة فوكسل إلى شبكة مضلعة (mesh).",
|
||||
"display_name": "تحويل الفوكسل إلى شبكة أساسية",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
|
||||
@@ -904,6 +904,7 @@
|
||||
"alphabetical": "A-Z",
|
||||
"alphabeticalDesc": "Sort alphabetically within groups"
|
||||
},
|
||||
"noMatchingNodes": "No nodes match \"{query}\"",
|
||||
"sections": {
|
||||
"favorites": "Bookmarks",
|
||||
"favoriteNode": "Bookmark Node",
|
||||
@@ -1112,7 +1113,6 @@
|
||||
"alphabetical": "A → Z",
|
||||
"newest": "Newest",
|
||||
"searchPlaceholder": "Search...",
|
||||
"vramLowToHigh": "VRAM Usage (Low to High)",
|
||||
"modelSizeLowToHigh": "Model Size (Low to High)",
|
||||
"default": "Default"
|
||||
},
|
||||
@@ -1661,6 +1661,7 @@
|
||||
"dataset": "dataset",
|
||||
"text": "text",
|
||||
"image": "image",
|
||||
"adjustments": "adjustments",
|
||||
"sampling": "sampling",
|
||||
"schedulers": "schedulers",
|
||||
"conditioning": "conditioning",
|
||||
@@ -1676,6 +1677,7 @@
|
||||
"video": "video",
|
||||
"ByteDance": "ByteDance",
|
||||
"filters": "filters",
|
||||
"transform": "transform",
|
||||
"advanced": "advanced",
|
||||
"guidance": "guidance",
|
||||
"model_merging": "model_merging",
|
||||
@@ -1694,7 +1696,6 @@
|
||||
"inpaint": "inpaint",
|
||||
"scheduling": "scheduling",
|
||||
"create": "create",
|
||||
"transform": "transform",
|
||||
"deprecated": "deprecated",
|
||||
"detection": "detection",
|
||||
"debug": "debug",
|
||||
@@ -1734,6 +1735,7 @@
|
||||
"geometry_estimation": "geometry_estimation",
|
||||
"OpenAI": "OpenAI",
|
||||
"Sora": "Sora",
|
||||
"OpenRouter": "OpenRouter",
|
||||
"cond pair": "cond pair",
|
||||
"photomaker": "photomaker",
|
||||
"PixVerse": "PixVerse",
|
||||
@@ -1783,6 +1785,8 @@
|
||||
"CONTROL_NET": "CONTROL_NET",
|
||||
"CURVE": "CURVE",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "FACE_DETECTION_MODEL",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
"FILE_3D": "FILE_3D",
|
||||
"FILE_3D_FBX": "FILE_3D_FBX",
|
||||
"FILE_3D_GLB": "FILE_3D_GLB",
|
||||
@@ -2135,6 +2139,43 @@
|
||||
"slots": "Node Slots Error",
|
||||
"widgets": "Node Widgets Error"
|
||||
},
|
||||
"oauth": {
|
||||
"consent": {
|
||||
"allow": "Continue",
|
||||
"deny": "Cancel",
|
||||
"genericError": "OAuth request failed. Please restart from the client app.",
|
||||
"loading": "Loading authorization request…",
|
||||
"missingRequest": "This authorization request is missing. Please restart from the client app.",
|
||||
"noWorkspaces": "No eligible workspaces are available for this request.",
|
||||
"title": "{client} wants access",
|
||||
"subtitle": "Sign in to {resource} to continue",
|
||||
"resourceFallback": "this app",
|
||||
"workspaceLabel": "Workspace",
|
||||
"permissionsHeader": "Permissions",
|
||||
"workspaceHelp": "Permissions apply to this workspace only.",
|
||||
"redirectNotice": "You'll be redirected to",
|
||||
"appTypeNative": "Native app",
|
||||
"appTypeWeb": "Web app",
|
||||
"errorExpired": "This consent request has expired or has already been used. Please restart from the client app.",
|
||||
"errorScopeBroadening": "The previously approved permissions don't cover this request. You'll need to re-authorize with the new permissions.",
|
||||
"errorUnavailable": "This feature isn't available right now. Please contact support if the problem persists.",
|
||||
"sessionError": "Failed to establish session. Please try again.",
|
||||
"sessionErrorToastSummary": "Couldn't continue OAuth sign-in"
|
||||
},
|
||||
"scopes": {
|
||||
"mcp:tools:read": {
|
||||
"label": "View available workflow tools"
|
||||
},
|
||||
"mcp:tools:call": {
|
||||
"label": "Run workflows on your behalf"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"personal": "Personal",
|
||||
"owner": "Owner",
|
||||
"member": "Member"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
"title": "API Key",
|
||||
@@ -2707,7 +2748,8 @@
|
||||
"placeholderMesh": "Select mesh...",
|
||||
"placeholderModel": "Select model...",
|
||||
"placeholderUnknown": "Select media...",
|
||||
"maxSelectionReached": "Maximum selection limit reached"
|
||||
"maxSelectionReached": "Maximum selection limit reached",
|
||||
"topResult": "Top result: {result}"
|
||||
},
|
||||
"valueControl": {
|
||||
"header": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
},
|
||||
"AddTextPrefix": {
|
||||
"display_name": "Add Text Prefix",
|
||||
"display_name": "Add Text Prefix (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
},
|
||||
"AddTextSuffix": {
|
||||
"display_name": "Add Text Suffix",
|
||||
"display_name": "Add Text Suffix (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -61,6 +61,7 @@
|
||||
},
|
||||
"AdjustBrightness": {
|
||||
"display_name": "Adjust Brightness",
|
||||
"description": "Adjust the brightness of an image.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -80,6 +81,7 @@
|
||||
},
|
||||
"AdjustContrast": {
|
||||
"display_name": "Adjust Contrast",
|
||||
"description": "Adjust the contrast of an image.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -176,7 +178,8 @@
|
||||
}
|
||||
},
|
||||
"AudioAdjustVolume": {
|
||||
"display_name": "Audio Adjust Volume",
|
||||
"display_name": "Adjust Audio Volume",
|
||||
"description": "Adjust the volume of the audio by a specified amount in decibels (dB).",
|
||||
"inputs": {
|
||||
"audio": {
|
||||
"name": "audio"
|
||||
@@ -193,7 +196,7 @@
|
||||
}
|
||||
},
|
||||
"AudioConcat": {
|
||||
"display_name": "Audio Concat",
|
||||
"display_name": "Concatenate Audio",
|
||||
"description": "Concatenates the audio1 to audio2 in the specified direction.",
|
||||
"inputs": {
|
||||
"audio1": {
|
||||
@@ -284,7 +287,7 @@
|
||||
}
|
||||
},
|
||||
"AudioMerge": {
|
||||
"display_name": "Audio Merge",
|
||||
"display_name": "Merge Audio",
|
||||
"description": "Combine two audio tracks by overlaying their waveforms.",
|
||||
"inputs": {
|
||||
"audio1": {
|
||||
@@ -585,6 +588,9 @@
|
||||
"model_auto_downscale": {
|
||||
"name": "auto_downscale"
|
||||
},
|
||||
"model_auto_upscale": {
|
||||
"name": "auto_upscale"
|
||||
},
|
||||
"model_duration": {
|
||||
"name": "duration"
|
||||
},
|
||||
@@ -1115,7 +1121,8 @@
|
||||
}
|
||||
},
|
||||
"CenterCropImages": {
|
||||
"display_name": "Center Crop Images",
|
||||
"display_name": "Crop Image (Center)",
|
||||
"description": "Center crop an image to the specified dimensions.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -1299,6 +1306,9 @@
|
||||
"model_max_tokens": {
|
||||
"name": "max_tokens"
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"model_temperature": {
|
||||
"name": "temperature"
|
||||
}
|
||||
@@ -5700,6 +5710,7 @@
|
||||
},
|
||||
"ImageCropV2": {
|
||||
"display_name": "Crop Image",
|
||||
"description": "Crop an image to the specified dimensions.",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "image"
|
||||
@@ -5719,7 +5730,8 @@
|
||||
}
|
||||
},
|
||||
"ImageDeduplication": {
|
||||
"display_name": "Image Deduplication",
|
||||
"display_name": "Deduplicate Images",
|
||||
"description": "Remove duplicate or very similar images from a list.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -5773,7 +5785,8 @@
|
||||
}
|
||||
},
|
||||
"ImageGrid": {
|
||||
"display_name": "Image Grid",
|
||||
"display_name": "Make Image Grid",
|
||||
"description": "Arrange multiple images into a grid layout.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -7945,7 +7958,8 @@
|
||||
}
|
||||
},
|
||||
"LoadImageDataSetFromFolder": {
|
||||
"display_name": "Load Image Dataset from Folder",
|
||||
"display_name": "Load Image (from Folder)",
|
||||
"description": "Load a dataset of images from a specified folder and return a list of images. Supported formats: PNG, JPG, JPEG, WEBP.",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
"name": "folder",
|
||||
@@ -7988,11 +8002,12 @@
|
||||
}
|
||||
},
|
||||
"LoadImageTextDataSetFromFolder": {
|
||||
"display_name": "Load Image and Text Dataset from Folder",
|
||||
"display_name": "Load Image-Text (from Folder)",
|
||||
"description": "Load a dataset of pairs of images and text captions from a specified folder and return them as a list. Supported formats: PNG, JPG, JPEG, WEBP.",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
"name": "folder",
|
||||
"tooltip": "The folder to load images from."
|
||||
"tooltip": "The folder to load images and text captions from."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -8014,6 +8029,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMediaPipeFaceLandmarker": {
|
||||
"display_name": "Load Face Detection Model (MediaPipe)",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name",
|
||||
"tooltip": "Face detection model from models/detection/."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMoGeModel": {
|
||||
"display_name": "Load MoGe Model",
|
||||
"inputs": {
|
||||
@@ -8029,6 +8058,7 @@
|
||||
},
|
||||
"LoadTrainingDataset": {
|
||||
"display_name": "Load Training Dataset",
|
||||
"description": "Load encoded training dataset (latents + conditioning) from disk for use in training.",
|
||||
"inputs": {
|
||||
"folder_name": {
|
||||
"name": "folder_name",
|
||||
@@ -8417,7 +8447,7 @@
|
||||
}
|
||||
},
|
||||
"LTXVAudioVAELoader": {
|
||||
"display_name": "LTXV Audio VAE Loader",
|
||||
"display_name": "Load LTXV Audio VAE",
|
||||
"inputs": {
|
||||
"ckpt_name": {
|
||||
"name": "ckpt_name",
|
||||
@@ -9233,6 +9263,7 @@
|
||||
},
|
||||
"MakeTrainingDataset": {
|
||||
"display_name": "Make Training Dataset",
|
||||
"description": "Encode images with VAE and texts with CLIP to create a training dataset of latents and conditionings.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -9322,8 +9353,98 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceLandmarker": {
|
||||
"display_name": "Detect Face Landmarks (MediaPipe)",
|
||||
"description": "Detects facial landmarks using MediaPipe model.",
|
||||
"inputs": {
|
||||
"face_detection_model": {
|
||||
"name": "face_detection_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"detector_variant": {
|
||||
"name": "detector_variant",
|
||||
"tooltip": "Face detector range. 'short' is tuned for close-up faces (within ~2 m of the camera); 'full' covers farther / smaller faces (up to ~5 m) but is slower. 'both' runs both detectors and keeps whichever found more faces per frame (~2× detection cost)."
|
||||
},
|
||||
"num_faces": {
|
||||
"name": "num_faces",
|
||||
"tooltip": "Maximum faces to return per frame. 0 = no cap (return all detected)."
|
||||
},
|
||||
"min_confidence": {
|
||||
"name": "min_confidence",
|
||||
"tooltip": "BlazeFace score threshold. Lower to catch small/occluded faces."
|
||||
},
|
||||
"missing_frame_fallback": {
|
||||
"name": "missing_frame_fallback",
|
||||
"tooltip": "Per-frame behaviour when detection fails in a batch. 'empty' leaves the frame faceless. 'previous' copies the most recent successful detection. 'interpolate' lerps landmarks/bbox/blendshapes between bracketing successful frames. Multi-face: pairs faces across frames by greedy bbox-centre NN."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "face_landmarks",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "bboxes",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMask": {
|
||||
"display_name": "Draw Face Mask (MediaPipe)",
|
||||
"description": "Draws a mask from face landmarks.",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"regions": {
|
||||
"name": "regions",
|
||||
"tooltip": "'all' = union of face_oval+lips+eyes+irises (which collapses to face_oval since it encloses the rest). 'custom' = toggle each region individually for combos like lips+eyes."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMeshVisualize": {
|
||||
"display_name": "Visualize Face Landmarks (MediaPipe)",
|
||||
"description": "Draws face landmarks mesh on the input image.",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"connections": {
|
||||
"name": "connections",
|
||||
"tooltip": "'all' = oval+eyes+brows+lips+irises+nose. 'fill' = solid face_oval polygon (silhouette mask). 'custom' = toggle each feature individually (including 'tesselation', the full 2547-edge wireframe)."
|
||||
},
|
||||
"color": {
|
||||
"name": "color"
|
||||
},
|
||||
"thickness": {
|
||||
"name": "thickness",
|
||||
"tooltip": "Edge line thickness in pixels. 0 disables edge drawing."
|
||||
},
|
||||
"point_size": {
|
||||
"name": "point_size",
|
||||
"tooltip": "Landmark dot radius in pixels. 0 disables point drawing."
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "If not connected, a black canvas will be used."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MergeImageLists": {
|
||||
"display_name": "Merge Image Lists",
|
||||
"display_name": "Merge Image Lists (DEPRECATED)",
|
||||
"description": "Concatenate multiple image lists into one.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -9338,7 +9459,7 @@
|
||||
}
|
||||
},
|
||||
"MergeTextLists": {
|
||||
"display_name": "Merge Text Lists",
|
||||
"display_name": "Merge Text Lists (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -12003,7 +12124,8 @@
|
||||
}
|
||||
},
|
||||
"MoGeInference": {
|
||||
"display_name": "MoGe Inference",
|
||||
"display_name": "Run MoGe Inference",
|
||||
"description": "Run MoGe on a single image to estimate depth and geometry.",
|
||||
"inputs": {
|
||||
"moge_model": {
|
||||
"name": "moge_model"
|
||||
@@ -12039,7 +12161,8 @@
|
||||
}
|
||||
},
|
||||
"MoGePanoramaInference": {
|
||||
"display_name": "MoGe Panorama Inference",
|
||||
"display_name": "Run MoGe Panorama Inference",
|
||||
"description": "Run MoGe on an equirectangular panorama by splitting it into 12 perspective views, running inference on each, and merging the results into a single depth map.",
|
||||
"inputs": {
|
||||
"moge_model": {
|
||||
"name": "moge_model"
|
||||
@@ -12073,7 +12196,8 @@
|
||||
}
|
||||
},
|
||||
"MoGePointMapToMesh": {
|
||||
"display_name": "MoGe Point Map to Mesh",
|
||||
"display_name": "Convert MoGe Point Map to Mesh",
|
||||
"description": "Convert a MoGe point map into a 3D mesh.",
|
||||
"inputs": {
|
||||
"moge_geometry": {
|
||||
"name": "moge_geometry"
|
||||
@@ -12102,7 +12226,8 @@
|
||||
}
|
||||
},
|
||||
"MoGeRender": {
|
||||
"display_name": "MoGe Render",
|
||||
"display_name": "Render MoGe Geometry",
|
||||
"description": "Render a depth map or normal map from geometry data",
|
||||
"inputs": {
|
||||
"moge_geometry": {
|
||||
"name": "moge_geometry"
|
||||
@@ -12164,7 +12289,8 @@
|
||||
}
|
||||
},
|
||||
"NormalizeImages": {
|
||||
"display_name": "Normalize Images",
|
||||
"display_name": "Normalize Image Colors",
|
||||
"description": "Normalize images using mean and standard deviation.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -12493,6 +12619,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenRouterLLMNode": {
|
||||
"display_name": "OpenRouter LLM",
|
||||
"description": "Generate text responses through OpenRouter. Routes to a curated set of popular models from xAI, DeepSeek, Qwen, Mistral, Z.AI (GLM), Moonshot (Kimi), and Perplexity Sonar.",
|
||||
"inputs": {
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Text input to the model."
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "The OpenRouter model used to generate the response."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Seed for sampling. Set to 0 to omit. Most models treat this as a hint only."
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "system_prompt",
|
||||
"tooltip": "Foundational instructions that dictate the model's behavior."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpticalFlowLoader": {
|
||||
"display_name": "Load Optical Flow Model",
|
||||
"inputs": {
|
||||
@@ -13278,7 +13437,8 @@
|
||||
}
|
||||
},
|
||||
"RandomCropImages": {
|
||||
"display_name": "Random Crop Images",
|
||||
"display_name": "Crop Image (Random)",
|
||||
"description": "Randomly crop an image to the specified dimensions.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -14027,7 +14187,7 @@
|
||||
}
|
||||
},
|
||||
"ReplaceText": {
|
||||
"display_name": "Replace Text",
|
||||
"display_name": "Replace Text (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -14140,7 +14300,8 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByLongerEdge": {
|
||||
"display_name": "Resize Images by Longer Edge",
|
||||
"display_name": "Resize Images by Longer Edge (DEPRECATED)",
|
||||
"description": "Resize images so that the longer edge matches the specified dimension while preserving aspect ratio.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -14148,7 +14309,7 @@
|
||||
},
|
||||
"longer_edge": {
|
||||
"name": "longer_edge",
|
||||
"tooltip": "Target length for the longer edge."
|
||||
"tooltip": "Target dimension for the longer edge."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14159,7 +14320,8 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByShorterEdge": {
|
||||
"display_name": "Resize Images by Shorter Edge",
|
||||
"display_name": "Resize Images by Shorter Edge (DEPRECATED)",
|
||||
"description": "Resize images so that the shorter edge matches the specified dimension while preserving aspect ratio.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -14167,7 +14329,7 @@
|
||||
},
|
||||
"shorter_edge": {
|
||||
"name": "shorter_edge",
|
||||
"tooltip": "Target length for the shorter edge."
|
||||
"tooltip": "Target dimension for the shorter edge."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14179,6 +14341,7 @@
|
||||
},
|
||||
"ResolutionBucket": {
|
||||
"display_name": "Resolution Bucket",
|
||||
"description": "Group latents and conditionings into buckets",
|
||||
"inputs": {
|
||||
"latents": {
|
||||
"name": "latents",
|
||||
@@ -15292,7 +15455,8 @@
|
||||
}
|
||||
},
|
||||
"SaveImageDataSetToFolder": {
|
||||
"display_name": "Save Image Dataset to Folder",
|
||||
"display_name": "Save Image (to Folder) (DEPRECATED)",
|
||||
"description": "Save a dataset of images to a specified folder. Supported formats: PNG.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -15309,16 +15473,13 @@
|
||||
}
|
||||
},
|
||||
"SaveImageTextDataSetToFolder": {
|
||||
"display_name": "Save Image and Text Dataset to Folder",
|
||||
"display_name": "Save Image-Text (to Folder)",
|
||||
"description": "Save a dataset of pairs of images and text captions to a specified folder. Images are saved as PNG files and captions are saved as TXT files with the same filename_prefix.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
"tooltip": "List of images to save."
|
||||
},
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
"tooltip": "List of text captions to save."
|
||||
},
|
||||
"folder_name": {
|
||||
"name": "folder_name",
|
||||
"tooltip": "Name of the folder to save images to (inside output directory)."
|
||||
@@ -15326,6 +15487,10 @@
|
||||
"filename_prefix": {
|
||||
"name": "filename_prefix",
|
||||
"tooltip": "Prefix for saved image filenames."
|
||||
},
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
"tooltip": "List of text captions to save."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15380,6 +15545,7 @@
|
||||
},
|
||||
"SaveTrainingDataset": {
|
||||
"display_name": "Save Training Dataset",
|
||||
"description": "Save encoded training dataset (latents + conditioning) to disk for efficient loading during training.",
|
||||
"inputs": {
|
||||
"latents": {
|
||||
"name": "latents",
|
||||
@@ -15702,7 +15868,8 @@
|
||||
}
|
||||
},
|
||||
"ShuffleDataset": {
|
||||
"display_name": "Shuffle Image Dataset",
|
||||
"display_name": "Shuffle Images List",
|
||||
"description": "Randomly shuffle the order of images in a list.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -15724,7 +15891,8 @@
|
||||
}
|
||||
},
|
||||
"ShuffleImageTextDataset": {
|
||||
"display_name": "Shuffle Image-Text Dataset",
|
||||
"display_name": "Shuffle Pairs of Image-Text",
|
||||
"description": "Randomly shuffle the order of pairs of image-text in a list.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -16540,6 +16708,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringFormat": {
|
||||
"display_name": "Format Text",
|
||||
"description": "Same as Python's string format method. Supports all of Python's format options and features.",
|
||||
"inputs": {
|
||||
"values": {
|
||||
"name": "values"
|
||||
},
|
||||
"f_string": {
|
||||
"name": "f_string"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringLength": {
|
||||
"display_name": "Text Length",
|
||||
"inputs": {
|
||||
@@ -16609,7 +16794,7 @@
|
||||
}
|
||||
},
|
||||
"StripWhitespace": {
|
||||
"display_name": "Strip Whitespace",
|
||||
"display_name": "Strip Whitespace (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -17376,7 +17561,7 @@
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "Text to Lowercase",
|
||||
"display_name": "Convert Text to Lowercase (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -17391,7 +17576,7 @@
|
||||
}
|
||||
},
|
||||
"TextToUppercase": {
|
||||
"display_name": "Text to Uppercase",
|
||||
"display_name": "Convert Text to Uppercase (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -19332,6 +19517,7 @@
|
||||
},
|
||||
"VoxelToMesh": {
|
||||
"display_name": "Voxel to Mesh",
|
||||
"description": "Converts a voxel grid to a mesh.",
|
||||
"inputs": {
|
||||
"voxel": {
|
||||
"name": "voxel"
|
||||
@@ -19350,7 +19536,8 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"display_name": "Voxel to Mesh (Basic)",
|
||||
"display_name": "Voxel to Mesh (Basic) (DEPRECATED)",
|
||||
"description": "Converts a voxel grid to a mesh.",
|
||||
"inputs": {
|
||||
"voxel": {
|
||||
"name": "voxel"
|
||||
|
||||
@@ -800,6 +800,8 @@
|
||||
"CONTROL_NET": "RED_DE_CONTROL",
|
||||
"CURVE": "CURVA",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "MODELO_DE_DETECCIÓN_DE_CARAS",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
"FILE_3D": "ARCHIVO_3D",
|
||||
"FILE_3D_FBX": "ARCHIVO_3D_FBX",
|
||||
"FILE_3D_GLB": "ARCHIVO_3D_GLB",
|
||||
@@ -2278,6 +2280,7 @@
|
||||
"Meshy": "Meshy",
|
||||
"MiniMax": "MiniMax",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenRouter": "OpenRouter",
|
||||
"PixVerse": "PixVerse",
|
||||
"Quiver": "Quiver",
|
||||
"Recraft": "Recraft",
|
||||
@@ -2294,6 +2297,7 @@
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"adjustments": "ajustes",
|
||||
"advanced": "avanzado",
|
||||
"api node": "nodo api",
|
||||
"attention_experiments": "experimentos_de_atención",
|
||||
@@ -2443,7 +2447,8 @@
|
||||
"nonPublicAssetsWarningLine1": "Este flujo de trabajo incluye recursos no públicos.",
|
||||
"nonPublicAssetsWarningLine2": "Estos se importarán a tu biblioteca al abrir el flujo de trabajo",
|
||||
"openWithoutImporting": "Abrir sin importar",
|
||||
"openWorkflow": "Abrir flujo de trabajo"
|
||||
"openWorkflow": "Abrir flujo de trabajo",
|
||||
"opening": "Abriendo flujo de trabajo compartido..."
|
||||
},
|
||||
"painter": {
|
||||
"background": "Fondo",
|
||||
@@ -3620,7 +3625,8 @@
|
||||
"placeholderMesh": "Seleccionar malla...",
|
||||
"placeholderModel": "Seleccionar modelo...",
|
||||
"placeholderUnknown": "Seleccionar medio...",
|
||||
"placeholderVideo": "Seleccionar video..."
|
||||
"placeholderVideo": "Seleccionar video...",
|
||||
"topResult": "Mejor resultado: {result}"
|
||||
},
|
||||
"valueControl": {
|
||||
"decrement": "Disminuir valor",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
}
|
||||
},
|
||||
"AdjustBrightness": {
|
||||
"description": "Ajusta el brillo de una imagen.",
|
||||
"display_name": "Ajustar brillo",
|
||||
"inputs": {
|
||||
"factor": {
|
||||
@@ -138,6 +139,7 @@
|
||||
}
|
||||
},
|
||||
"AdjustContrast": {
|
||||
"description": "Ajusta el contraste de una imagen.",
|
||||
"display_name": "Ajustar contraste",
|
||||
"inputs": {
|
||||
"factor": {
|
||||
@@ -176,6 +178,7 @@
|
||||
}
|
||||
},
|
||||
"AudioAdjustVolume": {
|
||||
"description": "Ajusta el volumen del audio en una cantidad especificada en decibelios (dB).",
|
||||
"display_name": "Ajustar Volumen de Audio",
|
||||
"inputs": {
|
||||
"audio": {
|
||||
@@ -577,6 +580,9 @@
|
||||
"model_auto_downscale": {
|
||||
"name": "auto_downscale"
|
||||
},
|
||||
"model_auto_upscale": {
|
||||
"name": "auto_upscale"
|
||||
},
|
||||
"model_duration": {
|
||||
"name": "duración"
|
||||
},
|
||||
@@ -1547,6 +1553,7 @@
|
||||
}
|
||||
},
|
||||
"CenterCropImages": {
|
||||
"description": "Recorta una imagen al centro con las dimensiones especificadas.",
|
||||
"display_name": "Recorte central de imágenes",
|
||||
"inputs": {
|
||||
"height": {
|
||||
@@ -1666,6 +1673,9 @@
|
||||
"model_max_tokens": {
|
||||
"name": "max_tokens"
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"model_temperature": {
|
||||
"name": "temperature"
|
||||
},
|
||||
@@ -5699,6 +5709,7 @@
|
||||
}
|
||||
},
|
||||
"ImageCropV2": {
|
||||
"description": "Recorta una imagen a las dimensiones especificadas.",
|
||||
"display_name": "Recorte de Imagen",
|
||||
"inputs": {
|
||||
"crop_region": {
|
||||
@@ -5719,6 +5730,7 @@
|
||||
}
|
||||
},
|
||||
"ImageDeduplication": {
|
||||
"description": "Elimina imágenes duplicadas o muy similares de una lista.",
|
||||
"display_name": "Eliminación de Imágenes Duplicadas",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -5773,6 +5785,7 @@
|
||||
}
|
||||
},
|
||||
"ImageGrid": {
|
||||
"description": "Organiza varias imágenes en una cuadrícula.",
|
||||
"display_name": "Cuadrícula de Imágenes",
|
||||
"inputs": {
|
||||
"cell_height": {
|
||||
@@ -8366,6 +8379,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageDataSetFromFolder": {
|
||||
"description": "Carga un conjunto de datos de imágenes desde una carpeta especificada y devuelve una lista de imágenes. Formatos soportados: PNG, JPG, JPEG, WEBP.",
|
||||
"display_name": "Cargar conjunto de imágenes desde carpeta",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8409,6 +8423,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageTextDataSetFromFolder": {
|
||||
"description": "Carga un conjunto de datos de pares de imágenes y textos desde una carpeta especificada y los devuelve como una lista. Formatos soportados: PNG, JPG, JPEG, WEBP.",
|
||||
"display_name": "Cargar conjunto de imágenes y texto desde carpeta",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8435,6 +8450,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMediaPipeFaceLandmarker": {
|
||||
"display_name": "Cargar MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name",
|
||||
"tooltip": "Face Landmarker safetensors de models/mediapipe/."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMoGeModel": {
|
||||
"display_name": "Cargar modelo MoGe",
|
||||
"inputs": {
|
||||
@@ -8449,6 +8478,7 @@
|
||||
}
|
||||
},
|
||||
"LoadTrainingDataset": {
|
||||
"description": "Carga un conjunto de datos de entrenamiento codificado (latentes + condicionamiento) desde el disco para su uso en entrenamiento.",
|
||||
"display_name": "Cargar conjunto de datos de entrenamiento",
|
||||
"inputs": {
|
||||
"folder_name": {
|
||||
@@ -9232,6 +9262,7 @@
|
||||
}
|
||||
},
|
||||
"MakeTrainingDataset": {
|
||||
"description": "Codifica imágenes con VAE y textos con CLIP para crear un conjunto de datos de latentes y condicionamientos para entrenamiento.",
|
||||
"display_name": "Crear Conjunto de Datos de Entrenamiento",
|
||||
"inputs": {
|
||||
"clip": {
|
||||
@@ -9322,7 +9353,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceLandmarker": {
|
||||
"description": "Detecta puntos de referencia faciales usando el modelo MediaPipe.",
|
||||
"display_name": "MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"detector_variant": {
|
||||
"name": "detector_variant",
|
||||
"tooltip": "Rango del detector facial. 'short' está ajustado para rostros cercanos (a menos de ~2 m de la cámara); 'full' cubre rostros más lejanos/pequeños (hasta ~5 m) pero es más lento. 'both' ejecuta ambos detectores y mantiene el que encontró más rostros por fotograma (costo de detección ~2×)."
|
||||
},
|
||||
"face_detection_model": {
|
||||
"name": "face_detection_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"min_confidence": {
|
||||
"name": "min_confidence",
|
||||
"tooltip": "Umbral de puntuación BlazeFace. Disminuye para captar rostros pequeños/ocultos."
|
||||
},
|
||||
"missing_frame_fallback": {
|
||||
"name": "missing_frame_fallback",
|
||||
"tooltip": "Comportamiento por fotograma cuando falla la detección en un lote. 'empty' deja el fotograma sin rostro. 'previous' copia la detección exitosa más reciente. 'interpolate' interpola puntos clave/bbox/blendshapes entre los fotogramas exitosos adyacentes. Multi-rostro: empareja rostros entre fotogramas por centro de bbox más cercano."
|
||||
},
|
||||
"num_faces": {
|
||||
"name": "num_faces",
|
||||
"tooltip": "Máximo de rostros a devolver por fotograma. 0 = sin límite (devuelve todos los detectados)."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "face_landmarks",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "bboxes",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMask": {
|
||||
"description": "Dibuja una máscara a partir de los puntos de referencia faciales.",
|
||||
"display_name": "MediaPipe Face Mask",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"regions": {
|
||||
"name": "regions",
|
||||
"tooltip": "'all' = unión de face_oval+lips+eyes+irises (que se reduce a face_oval ya que lo contiene todo). 'custom' = activa cada región individualmente para combinaciones como lips+eyes."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMeshVisualize": {
|
||||
"description": "Dibuja la malla de puntos de referencia faciales sobre la imagen de entrada.",
|
||||
"display_name": "Visualizar Malla Facial de MediaPipe",
|
||||
"inputs": {
|
||||
"color": {
|
||||
"name": "color"
|
||||
},
|
||||
"connections": {
|
||||
"name": "connections",
|
||||
"tooltip": "'all' = oval+eyes+brows+lips+irises+nose. 'fill' = polígono sólido face_oval (máscara de silueta). 'custom' = activa cada característica individualmente (incluyendo 'tesselation', el entramado completo de 2547 aristas)."
|
||||
},
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "Si no se conecta, se usará un lienzo negro."
|
||||
},
|
||||
"point_size": {
|
||||
"name": "point_size",
|
||||
"tooltip": "Radio de los puntos de referencia en píxeles. 0 desactiva el dibujo de puntos."
|
||||
},
|
||||
"thickness": {
|
||||
"name": "thickness",
|
||||
"tooltip": "Grosor de línea de los bordes en píxeles. 0 desactiva el dibujo de bordes."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MergeImageLists": {
|
||||
"description": "Concatena varias listas de imágenes en una sola.",
|
||||
"display_name": "Unir Listas de Imágenes",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -9777,6 +9898,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeInference": {
|
||||
"description": "Ejecuta MoGe en una sola imagen para estimar profundidad y geometría.",
|
||||
"display_name": "Inferencia MoGe",
|
||||
"inputs": {
|
||||
"apply_mask": {
|
||||
@@ -9813,6 +9935,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePanoramaInference": {
|
||||
"description": "Ejecuta MoGe en una panorámica equirectangular dividiéndola en 12 vistas en perspectiva, ejecutando inferencia en cada una y fusionando los resultados en un solo mapa de profundidad.",
|
||||
"display_name": "Inferencia panorámica MoGe",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
@@ -9847,6 +9970,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePointMapToMesh": {
|
||||
"description": "Convierte un mapa de puntos MoGe en una malla 3D.",
|
||||
"display_name": "MoGe Point Map a Malla",
|
||||
"inputs": {
|
||||
"batch_index": {
|
||||
@@ -9876,6 +10000,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeRender": {
|
||||
"description": "Renderiza un mapa de profundidad o un mapa normal a partir de datos de geometría",
|
||||
"display_name": "MoGe Renderizado",
|
||||
"inputs": {
|
||||
"moge_geometry": {
|
||||
@@ -12164,6 +12289,7 @@
|
||||
}
|
||||
},
|
||||
"NormalizeImages": {
|
||||
"description": "Normaliza imágenes usando la media y la desviación estándar.",
|
||||
"display_name": "Normalizar Imágenes",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -12493,6 +12619,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenRouterLLMNode": {
|
||||
"description": "Genera respuestas de texto a través de OpenRouter. Redirige a un conjunto seleccionado de modelos populares de xAI, DeepSeek, Qwen, Mistral, Z.AI (GLM), Moonshot (Kimi) y Perplexity Sonar.",
|
||||
"display_name": "OpenRouter LLM",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "El modelo de OpenRouter utilizado para generar la respuesta."
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Entrada de texto para el modelo."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "Semilla para muestreo. Establecer en 0 para omitir. La mayoría de los modelos lo toman solo como sugerencia."
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "system_prompt",
|
||||
"tooltip": "Instrucciones fundamentales que dictan el comportamiento del modelo."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpticalFlowLoader": {
|
||||
"display_name": "Cargar modelo de flujo óptico",
|
||||
"inputs": {
|
||||
@@ -13306,6 +13465,7 @@
|
||||
}
|
||||
},
|
||||
"RandomCropImages": {
|
||||
"description": "Recorta aleatoriamente una imagen a las dimensiones especificadas.",
|
||||
"display_name": "Recorte Aleatorio de Imágenes",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -14168,6 +14328,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByLongerEdge": {
|
||||
"description": "Redimensiona imágenes para que el borde más largo coincida con la dimensión especificada, manteniendo la relación de aspecto.",
|
||||
"display_name": "Redimensionar imágenes por el borde más largo",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14187,6 +14348,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByShorterEdge": {
|
||||
"description": "Redimensiona las imágenes para que el borde más corto coincida con la dimensión especificada, manteniendo la proporción de aspecto.",
|
||||
"display_name": "Redimensionar imágenes por el borde más corto",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14206,6 +14368,7 @@
|
||||
}
|
||||
},
|
||||
"ResolutionBucket": {
|
||||
"description": "Agrupa latents y condicionamientos en buckets",
|
||||
"display_name": "Agrupación por resolución",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15550,6 +15713,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageDataSetToFolder": {
|
||||
"description": "Guarda un conjunto de imágenes en una carpeta especificada. Formatos soportados: PNG.",
|
||||
"display_name": "Guardar conjunto de imágenes en carpeta",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15567,6 +15731,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageTextDataSetToFolder": {
|
||||
"description": "Guarda un conjunto de pares de imágenes y subtítulos de texto en una carpeta especificada. Las imágenes se guardan como archivos PNG y los subtítulos como archivos TXT con el mismo prefijo de nombre de archivo.",
|
||||
"display_name": "Guardar conjunto de imágenes y textos en carpeta",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15637,6 +15802,7 @@
|
||||
}
|
||||
},
|
||||
"SaveTrainingDataset": {
|
||||
"description": "Guarda el conjunto de datos de entrenamiento codificado (latents + conditioning) en disco para una carga eficiente durante el entrenamiento.",
|
||||
"display_name": "Guardar conjunto de datos de entrenamiento",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15823,6 +15989,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleDataset": {
|
||||
"description": "Mezcla aleatoriamente el orden de las imágenes en una lista.",
|
||||
"display_name": "Barajar conjunto de imágenes",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -15845,6 +16012,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleImageTextDataset": {
|
||||
"description": "Mezcla aleatoriamente el orden de los pares imagen-texto en una lista.",
|
||||
"display_name": "Barajar conjunto de imágenes y textos",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -16661,6 +16829,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringFormat": {
|
||||
"description": "Igual que el método de formato de cadenas de Python. Soporta todas las opciones y características de formato de Python.",
|
||||
"display_name": "Formatear Texto",
|
||||
"inputs": {
|
||||
"f_string": {
|
||||
"name": "f_string"
|
||||
},
|
||||
"values": {
|
||||
"name": "values"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringLength": {
|
||||
"display_name": "Longitud",
|
||||
"inputs": {
|
||||
@@ -19328,6 +19513,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMesh": {
|
||||
"description": "Convierte una cuadrícula de vóxeles en una malla.",
|
||||
"display_name": "VoxelToMesh",
|
||||
"inputs": {
|
||||
"algorithm": {
|
||||
@@ -19347,6 +19533,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"description": "Convierte una cuadrícula de vóxeles en una malla.",
|
||||
"display_name": "VoxelAMallaBásico",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
|
||||
@@ -800,6 +800,8 @@
|
||||
"CONTROL_NET": "controlnet",
|
||||
"CURVE": "CURVE",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "مدل تشخیص چهره",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
"FILE_3D": "FILE_3D",
|
||||
"FILE_3D_FBX": "FILE_3D_FBX",
|
||||
"FILE_3D_GLB": "FILE_3D_GLB",
|
||||
@@ -2278,6 +2280,7 @@
|
||||
"Meshy": "Meshy",
|
||||
"MiniMax": "MiniMax",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenRouter": "OpenRouter",
|
||||
"PixVerse": "PixVerse",
|
||||
"Quiver": "Quiver",
|
||||
"Recraft": "Recraft",
|
||||
@@ -2294,6 +2297,7 @@
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"adjustments": "تنظیمات",
|
||||
"advanced": "پیشرفته",
|
||||
"api node": "گره API",
|
||||
"attention_experiments": "آزمایشهای توجه",
|
||||
@@ -2443,7 +2447,8 @@
|
||||
"nonPublicAssetsWarningLine1": "این گردشکار دارای داراییهای غیرعمومی است.",
|
||||
"nonPublicAssetsWarningLine2": "این موارد هنگام باز کردن گردشکار به کتابخانه شما وارد میشوند",
|
||||
"openWithoutImporting": "باز کردن بدون وارد کردن",
|
||||
"openWorkflow": "باز کردن گردشکار"
|
||||
"openWorkflow": "باز کردن گردشکار",
|
||||
"opening": "در حال باز کردن گردشکار مشترک..."
|
||||
},
|
||||
"painter": {
|
||||
"background": "پسزمینه",
|
||||
@@ -3632,7 +3637,8 @@
|
||||
"placeholderMesh": "مش را انتخاب کنید...",
|
||||
"placeholderModel": "انتخاب مدل...",
|
||||
"placeholderUnknown": "انتخاب رسانه...",
|
||||
"placeholderVideo": "انتخاب ویدیو..."
|
||||
"placeholderVideo": "انتخاب ویدیو...",
|
||||
"topResult": "نتیجه برتر: {result}"
|
||||
},
|
||||
"valueControl": {
|
||||
"decrement": "کاهش مقدار",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
}
|
||||
},
|
||||
"AdjustBrightness": {
|
||||
"description": "تنظیم روشنایی یک تصویر.",
|
||||
"display_name": "تنظیم روشنایی",
|
||||
"inputs": {
|
||||
"factor": {
|
||||
@@ -138,6 +139,7 @@
|
||||
}
|
||||
},
|
||||
"AdjustContrast": {
|
||||
"description": "تنظیم کنتراست یک تصویر.",
|
||||
"display_name": "تنظیم کنتراست",
|
||||
"inputs": {
|
||||
"factor": {
|
||||
@@ -176,6 +178,7 @@
|
||||
}
|
||||
},
|
||||
"AudioAdjustVolume": {
|
||||
"description": "تنظیم حجم صدا به میزان مشخص بر حسب دسیبل (dB).",
|
||||
"display_name": "تنظیم حجم صدا",
|
||||
"inputs": {
|
||||
"audio": {
|
||||
@@ -577,6 +580,9 @@
|
||||
"model_auto_downscale": {
|
||||
"name": "auto_downscale"
|
||||
},
|
||||
"model_auto_upscale": {
|
||||
"name": "auto_upscale"
|
||||
},
|
||||
"model_duration": {
|
||||
"name": "مدت زمان"
|
||||
},
|
||||
@@ -1547,6 +1553,7 @@
|
||||
}
|
||||
},
|
||||
"CenterCropImages": {
|
||||
"description": "برش مرکزی تصویر به ابعاد مشخص شده.",
|
||||
"display_name": "برش مرکزی تصاویر",
|
||||
"inputs": {
|
||||
"height": {
|
||||
@@ -1666,6 +1673,9 @@
|
||||
"model_max_tokens": {
|
||||
"name": "حداکثر توکنها"
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"model_temperature": {
|
||||
"name": "دمای مدل"
|
||||
},
|
||||
@@ -5699,6 +5709,7 @@
|
||||
}
|
||||
},
|
||||
"ImageCropV2": {
|
||||
"description": "برش تصویر به ابعاد مشخص شده.",
|
||||
"display_name": "برش تصویر",
|
||||
"inputs": {
|
||||
"crop_region": {
|
||||
@@ -5719,6 +5730,7 @@
|
||||
}
|
||||
},
|
||||
"ImageDeduplication": {
|
||||
"description": "حذف تصاویر تکراری یا بسیار مشابه از یک لیست.",
|
||||
"display_name": "حذف تصاویر تکراری",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -5773,6 +5785,7 @@
|
||||
}
|
||||
},
|
||||
"ImageGrid": {
|
||||
"description": "چیدمان چندین تصویر در یک ساختار شبکهای.",
|
||||
"display_name": "شبکه تصاویر",
|
||||
"inputs": {
|
||||
"cell_height": {
|
||||
@@ -8366,6 +8379,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageDataSetFromFolder": {
|
||||
"description": "بارگذاری مجموعهای از تصاویر از یک پوشه مشخص و بازگرداندن لیستی از تصاویر. فرمتهای پشتیبانیشده: PNG، JPG، JPEG، WEBP.",
|
||||
"display_name": "بارگذاری مجموعه تصاویر از پوشه",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8409,6 +8423,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageTextDataSetFromFolder": {
|
||||
"description": "بارگذاری مجموعهای از جفتهای تصویر و کپشن متنی از یک پوشه مشخص و بازگرداندن آنها به صورت لیست. فرمتهای پشتیبانیشده: PNG، JPG، JPEG، WEBP.",
|
||||
"display_name": "بارگذاری مجموعه داده تصویر و متن از پوشه",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8435,6 +8450,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMediaPipeFaceLandmarker": {
|
||||
"display_name": "بارگذاری MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name",
|
||||
"tooltip": "safetensors مربوط به Face Landmarker از مسیر models/mediapipe/."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoadMoGeModel": {
|
||||
"display_name": "بارگذاری مدل MoGe",
|
||||
"inputs": {
|
||||
@@ -8449,6 +8478,7 @@
|
||||
}
|
||||
},
|
||||
"LoadTrainingDataset": {
|
||||
"description": "بارگذاری دیتاست آموزش رمزگذاریشده (latents + conditioning) از دیسک برای استفاده در آموزش.",
|
||||
"display_name": "بارگذاری مجموعه داده آموزشی",
|
||||
"inputs": {
|
||||
"folder_name": {
|
||||
@@ -9232,6 +9262,7 @@
|
||||
}
|
||||
},
|
||||
"MakeTrainingDataset": {
|
||||
"description": "رمزگذاری تصاویر با VAE و متون با CLIP برای ساخت دیتاست آموزشی از latents و conditioning.",
|
||||
"display_name": "ایجاد دیتاست آموزش",
|
||||
"inputs": {
|
||||
"clip": {
|
||||
@@ -9322,7 +9353,97 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceLandmarker": {
|
||||
"description": "شناسایی نقاط کلیدی صورت با استفاده از مدل MediaPipe.",
|
||||
"display_name": "MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"detector_variant": {
|
||||
"name": "detector_variant",
|
||||
"tooltip": "محدوده تشخیص چهره. 'short' برای چهرههای نزدیک (تا حدود ۲ متر از دوربین) تنظیم شده است؛ 'full' چهرههای دورتر/کوچکتر (تا حدود ۵ متر) را پوشش میدهد اما کندتر است. 'both' هر دو تشخیصدهنده را اجرا میکند و هر کدام که چهرههای بیشتری در هر فریم پیدا کند را نگه میدارد (هزینه تشخیص تقریباً ۲ برابر)."
|
||||
},
|
||||
"face_detection_model": {
|
||||
"name": "face_detection_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"min_confidence": {
|
||||
"name": "min_confidence",
|
||||
"tooltip": "آستانه امتیاز BlazeFace. برای شناسایی چهرههای کوچک یا پوشیده مقدار را کاهش دهید."
|
||||
},
|
||||
"missing_frame_fallback": {
|
||||
"name": "missing_frame_fallback",
|
||||
"tooltip": "رفتار هر فریم زمانی که تشخیص در یک دسته ناموفق باشد. 'empty' فریم را بدون چهره باقی میگذارد. 'previous' آخرین تشخیص موفق را کپی میکند. 'interpolate' نقاط/باکس/شکلهای ترکیبی را بین فریمهای موفق مجاور میانیابی میکند. چند چهره: جفتسازی چهرهها بین فریمها با نزدیکترین مرکز باکس به صورت حریصانه."
|
||||
},
|
||||
"num_faces": {
|
||||
"name": "num_faces",
|
||||
"tooltip": "حداکثر تعداد چهره برای بازگشت در هر فریم. ۰ = بدون محدودیت (همه چهرههای شناساییشده بازگردانده میشوند)."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "face_landmarks",
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "bboxes",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMask": {
|
||||
"description": "ترسیم ماسک از نقاط کلیدی صورت.",
|
||||
"display_name": "ماسک چهره MediaPipe",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"regions": {
|
||||
"name": "regions",
|
||||
"tooltip": "'all' = اجتماع face_oval+lips+eyes+irises (که به دلیل احاطه شدن توسط face_oval به همان face_oval تبدیل میشود). 'custom' = فعال/غیرفعال کردن هر ناحیه به صورت جداگانه برای ترکیبهایی مانند lips+eyes."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMeshVisualize": {
|
||||
"description": "ترسیم مش نقاط کلیدی صورت روی تصویر ورودی.",
|
||||
"display_name": "بصریسازی Face Mesh MediaPipe",
|
||||
"inputs": {
|
||||
"color": {
|
||||
"name": "color"
|
||||
},
|
||||
"connections": {
|
||||
"name": "connections",
|
||||
"tooltip": "'all' = بیضی+چشمها+ابروها+لبها+عنبیهها+بینی. 'fill' = چندضلعی face_oval پر (ماسک سیلوئت). 'custom' = فعال/غیرفعال کردن هر ویژگی به صورت جداگانه (شامل 'tesselation'، شبکه سیمی کامل با ۲۵۴۷ لبه)."
|
||||
},
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "در صورت عدم اتصال، یک بوم سیاه استفاده خواهد شد."
|
||||
},
|
||||
"point_size": {
|
||||
"name": "point_size",
|
||||
"tooltip": "شعاع نقطههای نشانهگذاری بر حسب پیکسل. ۰ یعنی رسم نقطه غیرفعال است."
|
||||
},
|
||||
"thickness": {
|
||||
"name": "thickness",
|
||||
"tooltip": "ضخامت خطوط لبه بر حسب پیکسل. ۰ یعنی رسم لبه غیرفعال است."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"MergeImageLists": {
|
||||
"description": "ادغام چندین لیست تصویر در یک لیست.",
|
||||
"display_name": "ادغام فهرست تصاویر",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -9777,6 +9898,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeInference": {
|
||||
"description": "اجرای MoGe روی یک تصویر برای تخمین عمق و هندسه.",
|
||||
"display_name": "استنتاج MoGe",
|
||||
"inputs": {
|
||||
"apply_mask": {
|
||||
@@ -9813,6 +9935,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePanoramaInference": {
|
||||
"description": "اجرای MoGe روی یک پانورامای equirectangular با تقسیم آن به ۱۲ نمای پرسپکتیو، اجرای inference روی هر کدام و ادغام نتایج در یک نقشه عمق واحد.",
|
||||
"display_name": "استنتاج پانورامای MoGe",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
@@ -9847,6 +9970,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePointMapToMesh": {
|
||||
"description": "تبدیل نقشه نقاط MoGe به یک مش سهبعدی.",
|
||||
"display_name": "MoGe Point Map to Mesh",
|
||||
"inputs": {
|
||||
"batch_index": {
|
||||
@@ -9876,6 +10000,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeRender": {
|
||||
"description": "رندر نقشه عمق یا نقشه نرمال از دادههای هندسی.",
|
||||
"display_name": "MoGe Render",
|
||||
"inputs": {
|
||||
"moge_geometry": {
|
||||
@@ -12164,6 +12289,7 @@
|
||||
}
|
||||
},
|
||||
"NormalizeImages": {
|
||||
"description": "نرمالسازی تصاویر با استفاده از میانگین و انحراف معیار.",
|
||||
"display_name": "نرمالسازی تصاویر",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -12493,6 +12619,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpenRouterLLMNode": {
|
||||
"description": "تولید پاسخ متنی از طریق OpenRouter. مسیریابی به مجموعهای منتخب از مدلهای محبوب xAI، DeepSeek، Qwen، Mistral، Z.AI (GLM)، Moonshot (Kimi) و Perplexity Sonar.",
|
||||
"display_name": "OpenRouter LLM",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"tooltip": "مدل OpenRouter مورد استفاده برای تولید پاسخ."
|
||||
},
|
||||
"model_reasoning_effort": {
|
||||
"name": "reasoning_effort"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "ورودی متنی برای مدل."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed",
|
||||
"tooltip": "بذر نمونهگیری. برای حذف، مقدار ۰ را قرار دهید. اکثر مدلها این مقدار را فقط به عنوان راهنما در نظر میگیرند."
|
||||
},
|
||||
"system_prompt": {
|
||||
"name": "system_prompt",
|
||||
"tooltip": "دستورالعملهای پایه که رفتار مدل را تعیین میکند."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpticalFlowLoader": {
|
||||
"display_name": "بارگذاری مدل Optical Flow",
|
||||
"inputs": {
|
||||
@@ -13306,6 +13465,7 @@
|
||||
}
|
||||
},
|
||||
"RandomCropImages": {
|
||||
"description": "برش تصادفی تصویر به ابعاد مشخص شده.",
|
||||
"display_name": "برش تصادفی تصاویر",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -14168,6 +14328,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByLongerEdge": {
|
||||
"description": "تغییر اندازه تصاویر به گونهای که لبه بلندتر با بعد مشخص شده مطابقت داشته باشد و نسبت ابعاد حفظ شود.",
|
||||
"display_name": "تغییر اندازه تصاویر بر اساس ضلع بلندتر",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14187,6 +14348,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByShorterEdge": {
|
||||
"description": "تغییر اندازه تصاویر به گونهای که لبه کوتاهتر با ابعاد مشخص شده مطابقت داشته باشد و نسبت ابعاد حفظ شود.",
|
||||
"display_name": "تغییر اندازه تصاویر بر اساس ضلع کوتاهتر",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14206,6 +14368,7 @@
|
||||
}
|
||||
},
|
||||
"ResolutionBucket": {
|
||||
"description": "گروهبندی latentها و conditioningها در سطلها",
|
||||
"display_name": "دستهبندی بر اساس وضوح",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15550,6 +15713,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageDataSetToFolder": {
|
||||
"description": "ذخیره یک مجموعه داده تصویر در پوشهای مشخص. فرمتهای پشتیبانیشده: PNG.",
|
||||
"display_name": "ذخیره مجموعه تصاویر در پوشه",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15567,6 +15731,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageTextDataSetToFolder": {
|
||||
"description": "ذخیره یک مجموعه داده شامل جفتهای تصویر و کپشن متنی در پوشهای مشخص. تصاویر به صورت فایل PNG و کپشنها به صورت فایل TXT با همان پیشوند نام فایل ذخیره میشوند.",
|
||||
"display_name": "ذخیره مجموعه تصویر و متن در پوشه",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15637,6 +15802,7 @@
|
||||
}
|
||||
},
|
||||
"SaveTrainingDataset": {
|
||||
"description": "ذخیره مجموعه داده آموزش کدگذاریشده (latentها + conditioning) روی دیسک برای بارگذاری سریعتر در زمان آموزش.",
|
||||
"display_name": "ذخیره مجموعه داده آموزشی",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15823,6 +15989,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleDataset": {
|
||||
"description": "ترتیب تصاویر را در یک لیست به صورت تصادفی جابجا میکند.",
|
||||
"display_name": "درهمریختن دیتاست تصویر",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -15845,6 +16012,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleImageTextDataset": {
|
||||
"description": "ترتیب جفتهای تصویر-متن را در یک لیست به صورت تصادفی جابجا میکند.",
|
||||
"display_name": "درهمریختن دیتاست تصویر-متن",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -16661,6 +16829,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringFormat": {
|
||||
"description": "همانند متد فرمت رشته در پایتون. از تمام گزینهها و ویژگیهای فرمت پایتون پشتیبانی میکند.",
|
||||
"display_name": "فرمت متن",
|
||||
"inputs": {
|
||||
"f_string": {
|
||||
"name": "f_string"
|
||||
},
|
||||
"values": {
|
||||
"name": "values"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"StringLength": {
|
||||
"display_name": "طول",
|
||||
"inputs": {
|
||||
@@ -19328,6 +19513,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMesh": {
|
||||
"description": "تبدیل شبکه voxel به mesh.",
|
||||
"display_name": "VoxelToMesh",
|
||||
"inputs": {
|
||||
"algorithm": {
|
||||
@@ -19347,6 +19533,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMeshBasic": {
|
||||
"description": "تبدیل شبکه voxel به mesh.",
|
||||
"display_name": "VoxelToMeshBasic",
|
||||
"inputs": {
|
||||
"threshold": {
|
||||
|
||||
@@ -800,6 +800,8 @@
|
||||
"CONTROL_NET": "RESEAU_DE_CONTROLE",
|
||||
"CURVE": "COURBE",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_DETECTION_MODEL": "MODÈLE_DE_DÉTECTION_DE_VISAGE",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
"FILE_3D": "FICHIER_3D",
|
||||
"FILE_3D_FBX": "FICHIER_3D_FBX",
|
||||
"FILE_3D_GLB": "FICHIER_3D_GLB",
|
||||
@@ -2278,6 +2280,7 @@
|
||||
"Meshy": "Meshy",
|
||||
"MiniMax": "MiniMax",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenRouter": "OpenRouter",
|
||||
"PixVerse": "PixVerse",
|
||||
"Quiver": "Quiver",
|
||||
"Recraft": "Recraft",
|
||||
@@ -2294,6 +2297,7 @@
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"adjustments": "ajustements",
|
||||
"advanced": "avancé",
|
||||
"api node": "nœud api",
|
||||
"attention_experiments": "expériences_d'attention",
|
||||
@@ -2443,7 +2447,8 @@
|
||||
"nonPublicAssetsWarningLine1": "Ce workflow contient des ressources non publiques.",
|
||||
"nonPublicAssetsWarningLine2": "Celles-ci seront importées dans votre bibliothèque lors de l'ouverture du workflow",
|
||||
"openWithoutImporting": "Ouvrir sans importer",
|
||||
"openWorkflow": "Ouvrir le workflow"
|
||||
"openWorkflow": "Ouvrir le workflow",
|
||||
"opening": "Ouverture du workflow partagé..."
|
||||
},
|
||||
"painter": {
|
||||
"background": "Arrière-plan",
|
||||
@@ -3620,7 +3625,8 @@
|
||||
"placeholderMesh": "Sélectionner un mesh...",
|
||||
"placeholderModel": "Sélectionner un modèle...",
|
||||
"placeholderUnknown": "Sélectionner un média...",
|
||||
"placeholderVideo": "Sélectionner une vidéo..."
|
||||
"placeholderVideo": "Sélectionner une vidéo...",
|
||||
"topResult": "Meilleur résultat : {result}"
|
||||
},
|
||||
"valueControl": {
|
||||
"decrement": "Décrémenter la valeur",
|
||||
|
||||