Compare commits
17 Commits
jaewon/m1-
...
v1.46.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1049a99a3 | ||
|
|
3da6e1766e | ||
|
|
52830a9e73 | ||
|
|
1cd98e8ab2 | ||
|
|
654a7d6904 | ||
|
|
eeeacc9b03 | ||
|
|
f744a4f1f8 | ||
|
|
0588ca45b3 | ||
|
|
60ce0ee0c3 | ||
|
|
91d2df45a1 | ||
|
|
7b4fef5eca | ||
|
|
c703db5f6c | ||
|
|
3011d3a60c | ||
|
|
6e31ce77c6 | ||
|
|
551c595bbb | ||
|
|
ee286291d4 | ||
|
|
efb214efe7 |
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/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
|
||||
|
||||
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*
|
||||
|
||||
|
||||
@@ -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 |
@@ -10,7 +10,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-6 py-24 lg:px-20 lg:py-32">
|
||||
<section class="max-w-9xl mx-auto px-6 py-24 lg:px-20 lg:py-32">
|
||||
<GlassCard
|
||||
class="mx-auto mt-20 flex flex-col gap-12 lg:flex-row lg:items-stretch lg:gap-8"
|
||||
>
|
||||
|
||||
@@ -74,7 +74,7 @@ useHeroAnimation({
|
||||
</div>
|
||||
|
||||
<!-- Video -->
|
||||
<div ref="videoRef" class="px-4 pb-20 lg:px-20 lg:pb-40">
|
||||
<div ref="videoRef" class="max-w-9xl mx-auto px-4 pb-20 lg:px-20 lg:pb-40">
|
||||
<VideoPlayer
|
||||
src="https://media.comfy.org/website/about/co-founders.webm"
|
||||
poster="https://media.comfy.org/website/about/co-founders-poster.webp"
|
||||
|
||||
@@ -33,7 +33,7 @@ const values: {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-6 py-24 lg:px-20 lg:py-32">
|
||||
<section class="max-w-9xl mx-auto px-6 py-24 lg:px-20 lg:py-32">
|
||||
<div class="mx-auto max-w-5xl text-center">
|
||||
<SectionLabel>
|
||||
{{ t('about.values.label', locale) }}
|
||||
|
||||
@@ -16,7 +16,7 @@ const investors = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-6 py-24 lg:px-20 lg:py-32">
|
||||
<section class="max-w-9xl mx-auto px-6 py-24 lg:px-20 lg:py-32">
|
||||
<div class="mx-auto text-center">
|
||||
<span
|
||||
class="text-primary-comfy-yellow text-xs font-semibold tracking-widest uppercase"
|
||||
|
||||
@@ -14,7 +14,7 @@ const reasons: TranslationKey[] = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-6 py-24 lg:px-20 lg:py-32">
|
||||
<section class="max-w-9xl mx-auto px-6 py-24 lg:px-20 lg:py-32">
|
||||
<WireNodeLayout :reasons right-card-padding="p-6" :locale="locale">
|
||||
<template #right-card>
|
||||
<img
|
||||
|
||||
@@ -41,7 +41,7 @@ function toggle(index: number) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-24 md:px-20 md:py-40">
|
||||
<section class="max-w-9xl mx-auto px-4 py-24 md:px-20 md:py-40">
|
||||
<div class="flex flex-col gap-6 md:flex-row md:gap-16">
|
||||
<!-- Left heading -->
|
||||
<div
|
||||
|
||||
@@ -46,7 +46,9 @@ const cards = excludeProduct
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-primary-comfy-ink px-0 py-20 lg:px-20 lg:py-24">
|
||||
<section
|
||||
class="bg-primary-comfy-ink max-w-9xl mx-auto px-0 py-20 lg:px-20 lg:py-24"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col items-center px-4 text-center">
|
||||
<SectionLabel v-if="labelKey">
|
||||
|
||||
@@ -45,11 +45,11 @@ const progressPercent = computed(() => `${progress.value * 100}%`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-6 py-16 lg:px-16 lg:py-24">
|
||||
<section class="max-w-9xl mx-auto px-6 py-16 lg:px-16 lg:py-24">
|
||||
<!-- Scrollable track -->
|
||||
<div
|
||||
ref="trackRef"
|
||||
class="scrollbar-none flex snap-x snap-mandatory gap-12 overflow-x-auto lg:gap-20"
|
||||
class="flex snap-x snap-mandatory scrollbar-none gap-12 overflow-x-auto lg:gap-20"
|
||||
>
|
||||
<div
|
||||
v-for="(fb, i) in feedbacks"
|
||||
|
||||
@@ -72,7 +72,7 @@ function handleLogoLoad() {
|
||||
</div>
|
||||
|
||||
<!-- Video -->
|
||||
<div ref="videoRef" class="px-4 pb-20 lg:px-20 lg:pb-40">
|
||||
<div ref="videoRef" class="max-w-9xl mx-auto px-4 pb-20 lg:px-20 lg:pb-40">
|
||||
<VideoPlayer
|
||||
src="https://media.comfy.org/website/customers/blackmath/video.webm"
|
||||
poster="https://media.comfy.org/website/customers/blackmath/poster.webp"
|
||||
|
||||
@@ -10,7 +10,7 @@ const prefix = locale === 'zh-CN' ? '/zh-CN' : ''
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="grid grid-cols-1 gap-6 px-6 py-16 lg:grid-cols-2 lg:px-16 lg:py-24"
|
||||
class="max-w-9xl mx-auto grid grid-cols-1 gap-6 px-6 py-16 lg:grid-cols-2 lg:px-16 lg:py-24"
|
||||
>
|
||||
<a
|
||||
v-for="story in customerStories"
|
||||
|
||||
@@ -7,7 +7,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-6 py-16 lg:px-20 lg:py-40">
|
||||
<section class="max-w-9xl mx-auto px-6 py-16 lg:px-20 lg:py-40">
|
||||
<VideoPlayer
|
||||
src="https://media.comfy.org/website/customers/silverside/video.webm"
|
||||
poster="https://media.comfy.org/website/customers/silverside/poster.webp"
|
||||
|
||||
@@ -7,7 +7,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col items-center px-4 pt-16 pb-24 text-center lg:px-20 lg:pt-20 lg:pb-40"
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-4 pt-16 pb-24 text-center lg:px-20 lg:pt-20 lg:pb-40"
|
||||
>
|
||||
<span
|
||||
class="text-primary-comfy-yellow text-sm font-bold tracking-widest uppercase"
|
||||
|
||||
@@ -223,7 +223,10 @@ while (idx < items.length) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section data-testid="gallery-grid" class="px-4 pb-20 lg:px-20">
|
||||
<section
|
||||
data-testid="gallery-grid"
|
||||
class="max-w-9xl mx-auto px-4 pb-20 lg:px-20"
|
||||
>
|
||||
<!-- Desktop grid -->
|
||||
<div
|
||||
class="rounded-5xl bg-transparency-white-t4 hidden flex-col gap-2 p-2 lg:flex"
|
||||
|
||||
@@ -8,7 +8,9 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex flex-col items-center px-6 pt-36 pb-16 text-center">
|
||||
<section
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-6 pt-36 pb-16 text-center"
|
||||
>
|
||||
<SectionLabel>
|
||||
{{ t('gallery.label', locale) }}
|
||||
</SectionLabel>
|
||||
|
||||
@@ -15,7 +15,7 @@ const row2 = [
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="bg-primary-comfy-ink flex flex-col items-center px-4 py-24 lg:px-6 lg:py-32"
|
||||
class="bg-primary-comfy-ink max-w-9xl mx-auto flex flex-col items-center px-4 py-24 lg:px-6 lg:py-32"
|
||||
>
|
||||
<!-- Node rows -->
|
||||
<div
|
||||
|
||||
@@ -12,7 +12,9 @@ const routes = getRoutes(locale)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-primary-comfy-ink px-4 py-20 lg:px-20 lg:py-24">
|
||||
<section
|
||||
class="bg-primary-comfy-ink max-w-9xl mx-auto px-4 py-20 lg:px-20 lg:py-24"
|
||||
>
|
||||
<GlassCard
|
||||
class="flex flex-col gap-12 lg:flex-row lg:items-stretch lg:gap-8"
|
||||
>
|
||||
|
||||
@@ -36,7 +36,9 @@ const steps = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-primary-comfy-ink px-4 py-20 lg:px-20 lg:py-24">
|
||||
<section
|
||||
class="bg-primary-comfy-ink max-w-9xl mx-auto px-4 py-20 lg:px-20 lg:py-24"
|
||||
>
|
||||
<div class="flex flex-col gap-12 lg:flex-row lg:gap-8">
|
||||
<!-- Left heading -->
|
||||
<div
|
||||
|
||||
@@ -15,7 +15,7 @@ const { loaded: logoLoaded } = useHeroLogo(logoContainer)
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="relative flex min-h-auto flex-col lg:flex-row lg:items-center"
|
||||
class="max-w-9xl relative mx-auto flex min-h-auto flex-col lg:flex-row lg:items-center"
|
||||
>
|
||||
<div
|
||||
ref="logoContainer"
|
||||
|
||||
@@ -55,7 +55,10 @@ watch(activeIndex, (current, previous) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section ref="sectionRef" class="px-4 py-20 lg:px-20 lg:py-24">
|
||||
<section
|
||||
ref="sectionRef"
|
||||
class="max-w-9xl mx-auto px-4 py-20 lg:px-20 lg:py-24"
|
||||
>
|
||||
<!-- Section header -->
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<NodeBadge :segments="badgeSegments" segment-class="" />
|
||||
|
||||
@@ -121,7 +121,7 @@ const activePlanIndex = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-16 lg:px-20 lg:py-14">
|
||||
<section class="max-w-9xl mx-auto px-4 py-16 lg:px-20 lg:py-14">
|
||||
<!-- Header -->
|
||||
<div class="mx-auto mb-8 max-w-3xl text-center lg:mb-10">
|
||||
<h1
|
||||
@@ -135,7 +135,7 @@ const activePlanIndex = ref(0)
|
||||
</div>
|
||||
|
||||
<!-- Mobile plan tabs -->
|
||||
<div class="scrollbar-none mb-6 flex gap-2 overflow-x-auto lg:hidden">
|
||||
<div class="mb-6 flex scrollbar-none gap-2 overflow-x-auto lg:hidden">
|
||||
<button
|
||||
v-for="(plan, index) in plans"
|
||||
:key="plan.id"
|
||||
|
||||
@@ -60,7 +60,7 @@ const features: IncludedFeature[] = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-16 lg:px-20 lg:py-24">
|
||||
<section class="max-w-9xl mx-auto px-4 py-16 lg:px-20 lg:py-24">
|
||||
<div class="mx-auto w-full lg:grid lg:grid-cols-[280px_1fr] lg:gap-x-16">
|
||||
<!-- Heading -->
|
||||
<div
|
||||
|
||||
@@ -25,7 +25,7 @@ const cards = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 pt-24 lg:px-20 lg:pt-40">
|
||||
<section class="max-w-9xl mx-auto px-4 pt-24 lg:px-20 lg:pt-40">
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-3.5xl/tight mx-auto max-w-3xl text-center font-light lg:text-5xl/tight"
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="bg-transparency-white-t4 rounded-5xl mx-4 mt-4 mb-24 p-2 lg:mx-20 lg:mt-8 lg:mb-40"
|
||||
class="bg-transparency-white-t4 rounded-5xl max-w-9xl mx-auto mt-4 mb-24 p-2 px-4 lg:mt-8 lg:mb-40 lg:px-20"
|
||||
>
|
||||
<div
|
||||
class="bg-primary-comfy-yellow flex flex-col gap-24 rounded-4xl p-8 lg:flex-row lg:items-end lg:justify-between"
|
||||
|
||||
@@ -442,7 +442,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-24 lg:px-20">
|
||||
<section class="max-w-9xl mx-auto px-4 py-24 lg:px-20">
|
||||
<GlassCard
|
||||
class="flex flex-col gap-8 lg:flex-row lg:items-stretch lg:gap-16"
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-24 lg:px-20 lg:py-40">
|
||||
<section class="max-w-9xl mx-auto px-4 py-24 lg:px-20 lg:py-40">
|
||||
<div
|
||||
class="bg-transparency-white-t4 rounded-5xl flex flex-col-reverse items-stretch gap-10 p-2 lg:flex-row lg:gap-8"
|
||||
>
|
||||
|
||||
@@ -77,7 +77,9 @@ function getCardClass(layoutClass: string): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="bg-primary-comfy-ink px-4 py-24 lg:px-20 lg:py-40">
|
||||
<section
|
||||
class="bg-primary-comfy-ink max-w-9xl mx-auto px-4 py-24 lg:px-20 lg:py-40"
|
||||
>
|
||||
<div class="mx-auto flex w-full max-w-7xl flex-col items-center">
|
||||
<p
|
||||
class="text-primary-comfy-yellow text-center text-sm font-bold tracking-widest uppercase"
|
||||
|
||||
@@ -11,7 +11,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-24 lg:px-20">
|
||||
<section class="max-w-9xl mx-auto px-4 py-24 lg:px-20">
|
||||
<SectionHeader>
|
||||
{{ heading }}
|
||||
<template v-if="subtitle" #subtitle>
|
||||
|
||||
@@ -22,7 +22,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="px-4 py-24 lg:px-20">
|
||||
<section class="max-w-9xl mx-auto px-4 py-24 lg:px-20">
|
||||
<SectionHeader>
|
||||
{{ heading }}
|
||||
<template #subtitle>
|
||||
|
||||
@@ -29,7 +29,7 @@ const {
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex flex-col gap-4 px-4 py-24 lg:flex-row lg:gap-16 lg:px-20 lg:py-40"
|
||||
class="max-w-9xl mx-auto flex flex-col gap-4 px-4 py-24 lg:flex-row lg:gap-16 lg:px-20 lg:py-40"
|
||||
>
|
||||
<!-- Left heading -->
|
||||
<div
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
46
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.45.12",
|
||||
"version": "1.46.1",
|
||||
"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": "pnpm exec 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",
|
||||
"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:",
|
||||
@@ -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:",
|
||||
|
||||
@@ -19,11 +19,5 @@
|
||||
"devDependencies": {
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:design"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.93.0"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:types"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,5 @@
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:util"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,5 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/comfyRegistryTypes.ts"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:types"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,5 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:util"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
2246
pnpm-lock.yaml
generated
@@ -21,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
|
||||
@@ -102,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
|
||||
@@ -155,7 +150,6 @@ allowBuilds:
|
||||
'@tailwindcss/oxide': true
|
||||
core-js: false
|
||||
esbuild: true
|
||||
nx: true
|
||||
oxc-resolver: true
|
||||
protobufjs: false
|
||||
sharp: false
|
||||
|
||||
|
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',
|
||||
|
||||
@@ -87,42 +87,26 @@ const rightSidePanelStore = useRightSidePanelStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const { totalErrorCount, isErrorOverlayOpen } = storeToRefs(executionErrorStore)
|
||||
const { allErrorGroups, missingModelGroups } = useErrorGroups(ref(''), t)
|
||||
const { allErrorGroups } = useErrorGroups(ref(''))
|
||||
|
||||
const singleErrorType = computed(() => {
|
||||
const types = new Set(allErrorGroups.value.map((g) => g.type))
|
||||
return types.size === 1 ? [...types][0] : null
|
||||
})
|
||||
|
||||
const friendlyMessageMap: Record<string, () => string> = {
|
||||
missing_node: () => t('errorOverlay.missingNodes'),
|
||||
swap_nodes: () => t('errorOverlay.swapNodes'),
|
||||
missing_media: () => t('errorOverlay.missingMedia'),
|
||||
missing_model: () => {
|
||||
const modelCount = missingModelGroups.value.reduce(
|
||||
(count, g) => count + g.models.length,
|
||||
0
|
||||
)
|
||||
return t('errorOverlay.missingModels', { count: modelCount }, modelCount)
|
||||
}
|
||||
}
|
||||
|
||||
function toFriendlyMessage(group: (typeof allErrorGroups.value)[number]) {
|
||||
return friendlyMessageMap[group.type]?.() ?? null
|
||||
}
|
||||
|
||||
const overlayMessages = computed<string[]>(() => {
|
||||
const messages = new Set<string>()
|
||||
for (const group of allErrorGroups.value) {
|
||||
const friendly = toFriendlyMessage(group)
|
||||
if (friendly) {
|
||||
messages.add(friendly)
|
||||
} else if (group.type === 'execution') {
|
||||
if (group.type === 'execution') {
|
||||
// TODO(FE-816 overlay-redesign): Keep runtime overlay copy raw until the
|
||||
// overlay redesign decides how to use catalog toast fields.
|
||||
for (const card of group.cards) {
|
||||
for (const err of card.errors) {
|
||||
messages.add(err.message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messages.add(group.displayMessage ?? group.displayTitle)
|
||||
}
|
||||
}
|
||||
return Array.from(messages)
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
v-if="showCameraControls"
|
||||
v-model:camera-type="cameraConfig!.cameraType"
|
||||
v-model:fov="cameraConfig!.fov"
|
||||
v-model:retain-view-on-reload="cameraConfig!.retainViewOnReload"
|
||||
/>
|
||||
|
||||
<div v-if="showLightControls" class="flex flex-col">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -11,17 +11,39 @@
|
||||
:aria-label="$t('load3d.switchCamera')"
|
||||
@click="switchCamera"
|
||||
>
|
||||
<i :class="['pi', 'pi-camera', 'text-lg text-base-foreground']" />
|
||||
<i :class="cn('pi pi-camera text-lg text-base-foreground')" />
|
||||
</Button>
|
||||
<PopupSlider
|
||||
v-if="showFOVButton"
|
||||
v-model="fov"
|
||||
:tooltip-text="$t('load3d.fov')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: $t('load3d.retainViewOnReload'),
|
||||
showDelay: 300
|
||||
}"
|
||||
size="icon"
|
||||
variant="textonly"
|
||||
class="rounded-full"
|
||||
:aria-label="$t('load3d.retainViewOnReload')"
|
||||
:aria-pressed="retainViewOnReload"
|
||||
@click="retainViewOnReload = !retainViewOnReload"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
'pi text-lg text-base-foreground',
|
||||
retainViewOnReload ? 'pi-lock' : 'pi-lock-open'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PopupSlider from '@/components/load3d/controls/PopupSlider.vue'
|
||||
@@ -30,6 +52,9 @@ import type { CameraType } from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const cameraType = defineModel<CameraType>('cameraType')
|
||||
const fov = defineModel<number>('fov')
|
||||
const retainViewOnReload = defineModel<boolean>('retainViewOnReload', {
|
||||
default: false
|
||||
})
|
||||
const showFOVButton = computed(() => cameraType.value === 'perspective')
|
||||
|
||||
const switchCamera = () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -209,6 +209,40 @@ describe('ErrorNodeCard.vue', () => {
|
||||
expect(screen.queryByText(/ComfyUI Error Report/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('displays catalog-resolved copy when available', async () => {
|
||||
renderCard({
|
||||
id: `node-${++cardIdCounter}`,
|
||||
title: 'KSampler',
|
||||
nodeId: '10',
|
||||
nodeTitle: 'KSampler',
|
||||
errors: [
|
||||
{
|
||||
message: 'Required input is missing',
|
||||
details: 'model',
|
||||
displayTitle: 'Missing connection',
|
||||
displayMessage:
|
||||
'Required input slots have no connection feeding them.',
|
||||
displayDetails: 'KSampler is missing a required input: model',
|
||||
displayItemLabel: 'KSampler - model'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Missing connection')).toBeInTheDocument()
|
||||
})
|
||||
expect(
|
||||
screen.getByText('Required input slots have no connection feeding them.')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('KSampler is missing a required input: model')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByText('KSampler - model')).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('Required input is missing')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('copies enriched report when copy button is clicked for runtime error', async () => {
|
||||
const reportText = '# Full Report Content'
|
||||
mockGenerateErrorReport.mockReturnValue(reportText)
|
||||
|
||||
@@ -54,12 +54,20 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- Human-friendly category/title when resolved by the error catalog. -->
|
||||
<p
|
||||
v-if="error.displayTitle"
|
||||
class="m-0 px-0.5 text-sm font-semibold text-destructive-background-hover"
|
||||
>
|
||||
{{ error.displayTitle }}
|
||||
</p>
|
||||
|
||||
<!-- Error Message -->
|
||||
<p
|
||||
v-if="error.message"
|
||||
v-if="getDisplayMessage(error)"
|
||||
class="m-0 max-h-[4lh] overflow-y-auto px-0.5 text-sm/relaxed wrap-break-word whitespace-pre-wrap"
|
||||
>
|
||||
{{ error.message }}
|
||||
{{ getDisplayMessage(error) }}
|
||||
</p>
|
||||
|
||||
<!-- Traceback / Details (enriched with full report for runtime errors) -->
|
||||
@@ -171,11 +179,15 @@ function handleEnterSubgraph() {
|
||||
|
||||
function handleCopyError(idx: number) {
|
||||
const details = displayedDetailsMap.value[idx]
|
||||
const message = card.errors[idx]?.message
|
||||
const message = getDisplayMessage(card.errors[idx])
|
||||
emit('copyToClipboard', [message, details].filter(Boolean).join('\n\n'))
|
||||
}
|
||||
|
||||
function handleCheckGithub(error: ErrorItem) {
|
||||
findOnGitHub(error.message)
|
||||
}
|
||||
|
||||
function getDisplayMessage(error: ErrorItem | undefined) {
|
||||
return error?.displayMessage ?? error?.message
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -57,11 +57,6 @@ describe('TabErrors.vue', () => {
|
||||
downloadAll: 'Download all',
|
||||
refresh: 'Refresh',
|
||||
refreshing: 'Refreshing missing models.'
|
||||
},
|
||||
promptErrors: {
|
||||
prompt_no_outputs: {
|
||||
desc: 'Prompt has no outputs'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +98,7 @@ describe('TabErrors.vue', () => {
|
||||
expect(screen.getByText('No errors')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders prompt-level errors (Group title = error message)', async () => {
|
||||
it('renders prompt-level errors with resolved display message', async () => {
|
||||
renderComponent({
|
||||
executionError: {
|
||||
lastPromptError: {
|
||||
@@ -115,7 +110,11 @@ describe('TabErrors.vue', () => {
|
||||
})
|
||||
|
||||
expect(screen.getByText('Server Error: No outputs')).toBeInTheDocument()
|
||||
expect(screen.getByText('Prompt has no outputs')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(
|
||||
'The workflow does not contain any output nodes (e.g. Save Image, Preview Image) to produce a result.'
|
||||
)
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByText('Error details')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div
|
||||
class="shrink-0 pb-2 text-sm font-semibold text-destructive-background-hover"
|
||||
>
|
||||
{{ singleRuntimeErrorGroup?.title }}
|
||||
{{ singleRuntimeErrorGroup?.displayTitle }}
|
||||
</div>
|
||||
<ErrorNodeCard
|
||||
:key="singleRuntimeErrorCard.id"
|
||||
@@ -53,12 +53,12 @@
|
||||
<!-- Group by Class Type -->
|
||||
<PropertiesAccordionItem
|
||||
v-for="group in filteredGroups"
|
||||
:key="group.title"
|
||||
:key="group.groupKey"
|
||||
:data-testid="'error-group-' + group.type.replaceAll('_', '-')"
|
||||
:collapse="isSectionCollapsed(group.title) && !isSearching"
|
||||
:collapse="isSectionCollapsed(group.groupKey) && !isSearching"
|
||||
class="border-b border-interface-stroke"
|
||||
:size="getGroupSize(group)"
|
||||
@update:collapse="setSectionCollapsed(group.title, $event)"
|
||||
@update:collapse="setSectionCollapsed(group.groupKey, $event)"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
||||
@@ -67,13 +67,7 @@
|
||||
class="icon-[lucide--octagon-alert] size-4 shrink-0 text-destructive-background-hover"
|
||||
/>
|
||||
<span class="truncate text-destructive-background-hover">
|
||||
{{
|
||||
group.type === 'missing_node'
|
||||
? `${group.title} (${missingPackGroups.length})`
|
||||
: group.type === 'swap_nodes'
|
||||
? `${group.title} (${swapNodeGroups.length})`
|
||||
: group.title
|
||||
}}
|
||||
{{ group.displayTitle }}
|
||||
</span>
|
||||
<span
|
||||
v-if="group.type === 'execution' && group.cards.length > 1"
|
||||
@@ -331,7 +325,7 @@ const {
|
||||
filteredMissingModelGroups: missingModelGroups,
|
||||
filteredMissingMediaGroups: missingMediaGroups,
|
||||
swapNodeGroups
|
||||
} = useErrorGroups(searchQuery, t)
|
||||
} = useErrorGroups(searchQuery)
|
||||
|
||||
const missingModelDownloadableModels = computed(() => {
|
||||
if (isCloud) return []
|
||||
@@ -366,22 +360,22 @@ const singleRuntimeErrorCard = computed(
|
||||
|
||||
const isAllCollapsed = computed({
|
||||
get() {
|
||||
return filteredGroups.value.every((g) => isSectionCollapsed(g.title))
|
||||
return filteredGroups.value.every((g) => isSectionCollapsed(g.groupKey))
|
||||
},
|
||||
set(collapse: boolean) {
|
||||
for (const group of tabErrorGroups.value) {
|
||||
setSectionCollapsed(group.title, collapse)
|
||||
setSectionCollapsed(group.groupKey, collapse)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function isSectionCollapsed(title: string): boolean {
|
||||
function isSectionCollapsed(groupKey: string): boolean {
|
||||
// Defaults to expanded when not explicitly set by the user
|
||||
return collapseState[title] ?? false
|
||||
return collapseState[groupKey] ?? false
|
||||
}
|
||||
|
||||
function setSectionCollapsed(title: string, collapsed: boolean) {
|
||||
collapseState[title] = collapsed
|
||||
function setSectionCollapsed(groupKey: string, collapsed: boolean) {
|
||||
collapseState[groupKey] = collapsed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,7 +397,7 @@ watch(
|
||||
card.graphNodeId === graphNodeId ||
|
||||
(card.nodeId?.startsWith(prefix) ?? false)
|
||||
)
|
||||
setSectionCollapsed(group.title, !hasMatch)
|
||||
setSectionCollapsed(group.groupKey, !hasMatch)
|
||||
}
|
||||
rightSidePanelStore.focusedErrorNodeId = null
|
||||
},
|
||||
|
||||
@@ -27,6 +27,8 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
te: vi.fn(() => false),
|
||||
t: vi.fn((key: string) => key),
|
||||
st: vi.fn((_key: string, fallback: string) => fallback)
|
||||
}))
|
||||
|
||||
@@ -84,8 +86,7 @@ describe('swapNodeGroups computed', () => {
|
||||
useMissingNodesErrorStore().surfaceMissingNodes(nodeTypes)
|
||||
|
||||
const searchQuery = ref('')
|
||||
const t = (key: string) => key
|
||||
const { swapNodeGroups } = useErrorGroups(searchQuery, t)
|
||||
const { swapNodeGroups } = useErrorGroups(searchQuery)
|
||||
return swapNodeGroups
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export interface ErrorItem {
|
||||
import type { ResolvedErrorMessage } from '@/platform/errorCatalog/types'
|
||||
|
||||
export interface ErrorItem extends ResolvedErrorMessage {
|
||||
/** Raw source/API-compatible message. */
|
||||
message: string
|
||||
/** Raw source/API-compatible details. */
|
||||
details?: string
|
||||
isRuntimeError?: boolean
|
||||
exceptionType?: string
|
||||
@@ -15,14 +19,28 @@ export interface ErrorCardData {
|
||||
errors: ErrorItem[]
|
||||
}
|
||||
|
||||
interface ErrorGroupBase extends Omit<ResolvedErrorMessage, 'displayTitle'> {
|
||||
/** Stable structural key used for rendering, collapse state, and cache identity. */
|
||||
groupKey: string
|
||||
/** Human-friendly title resolved for UI display. */
|
||||
displayTitle: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
export type ErrorGroup =
|
||||
| {
|
||||
| (ErrorGroupBase & {
|
||||
type: 'execution'
|
||||
title: string
|
||||
cards: ErrorCardData[]
|
||||
priority: number
|
||||
}
|
||||
| { type: 'missing_node'; title: string; priority: number }
|
||||
| { type: 'swap_nodes'; title: string; priority: number }
|
||||
| { type: 'missing_model'; title: string; priority: number }
|
||||
| { type: 'missing_media'; title: string; priority: number }
|
||||
})
|
||||
| (ErrorGroupBase & {
|
||||
type: 'missing_node'
|
||||
})
|
||||
| (ErrorGroupBase & {
|
||||
type: 'swap_nodes'
|
||||
})
|
||||
| (ErrorGroupBase & {
|
||||
type: 'missing_model'
|
||||
})
|
||||
| (ErrorGroupBase & {
|
||||
type: 'missing_media'
|
||||
})
|
||||
|
||||
@@ -30,7 +30,15 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
st: vi.fn((_key: string, fallback: string) => fallback)
|
||||
te: vi.fn(() => false),
|
||||
st: vi.fn((_key: string, fallback: string) => fallback),
|
||||
t: vi.fn((key: string, params?: { count?: number }) => {
|
||||
if (key === 'errorOverlay.missingModels') {
|
||||
const count = params?.count ?? 0
|
||||
return `${count} required ${count === 1 ? 'model is' : 'models are'} missing`
|
||||
}
|
||||
return key
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/comfyRegistryStore', () => ({
|
||||
@@ -113,8 +121,7 @@ function makeModel(
|
||||
function createErrorGroups() {
|
||||
const store = useExecutionErrorStore()
|
||||
const searchQuery = ref('')
|
||||
const t = (key: string) => key
|
||||
const groups = useErrorGroups(searchQuery, t)
|
||||
const groups = useErrorGroups(searchQuery)
|
||||
return { store, searchQuery, groups }
|
||||
}
|
||||
|
||||
@@ -245,6 +252,11 @@ describe('useErrorGroups', () => {
|
||||
(g) => g.type === 'missing_node'
|
||||
)
|
||||
expect(missingGroup).toBeDefined()
|
||||
expect(missingGroup?.groupKey).toBe('missing_node')
|
||||
expect(missingGroup?.displayTitle).toBe('Missing Node Packs (1)')
|
||||
expect(missingGroup?.displayMessage).toBe(
|
||||
'Some nodes are missing and need to be installed'
|
||||
)
|
||||
})
|
||||
|
||||
it('includes swap_nodes group when replaceable nodes exist', async () => {
|
||||
@@ -329,6 +341,54 @@ describe('useErrorGroups', () => {
|
||||
(g) => g.type === 'execution'
|
||||
)
|
||||
expect(execGroups.length).toBeGreaterThan(0)
|
||||
expect(execGroups[0].groupKey).toBe('execution:KSampler')
|
||||
expect(execGroups[0].displayTitle).toBe('KSampler')
|
||||
})
|
||||
|
||||
it('resolves required_input_missing item display copy', async () => {
|
||||
const { store, groups } = createErrorGroups()
|
||||
store.lastNodeErrors = {
|
||||
'1': {
|
||||
class_type: 'KSampler',
|
||||
dependent_outputs: [],
|
||||
errors: [
|
||||
{
|
||||
type: 'required_input_missing',
|
||||
message: 'Required input is missing',
|
||||
details: 'model',
|
||||
extra_info: {
|
||||
input_name: 'model'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
await nextTick()
|
||||
|
||||
const execGroup = groups.allErrorGroups.value.find(
|
||||
(g) => g.type === 'execution'
|
||||
)
|
||||
expect(execGroup?.type).toBe('execution')
|
||||
if (execGroup?.type !== 'execution') return
|
||||
|
||||
const card = execGroup.cards[0]
|
||||
const error = card.errors[0]
|
||||
|
||||
expect(error.message).toBe('Required input is missing')
|
||||
expect(error.details).toBe('model')
|
||||
expect(error.catalogId).toBe('missing_connection')
|
||||
expect(error.displayTitle).toBe('Missing connection')
|
||||
expect(error.displayMessage).toBe(
|
||||
'Required input slots have no connection feeding them.'
|
||||
)
|
||||
expect(error.displayDetails).toBe(
|
||||
'KSampler is missing a required input: model'
|
||||
)
|
||||
expect(error.displayItemLabel).toBe('KSampler - model')
|
||||
expect(error.toastTitle).toBe('Required input missing')
|
||||
expect(error.toastMessage).toBe(
|
||||
'KSampler is missing a required input: model'
|
||||
)
|
||||
})
|
||||
|
||||
it('includes execution error from runtime errors', async () => {
|
||||
@@ -351,6 +411,11 @@ describe('useErrorGroups', () => {
|
||||
(g) => g.type === 'execution'
|
||||
)
|
||||
expect(execGroups.length).toBeGreaterThan(0)
|
||||
if (execGroups[0].type !== 'execution') return
|
||||
expect(execGroups[0].cards[0].errors[0].displayItemLabel).toBe('KSampler')
|
||||
expect(execGroups[0].cards[0].errors[0].toastTitle).toBe(
|
||||
'KSampler failed'
|
||||
)
|
||||
})
|
||||
|
||||
it('includes prompt error when present', async () => {
|
||||
@@ -363,7 +428,7 @@ describe('useErrorGroups', () => {
|
||||
await nextTick()
|
||||
|
||||
const promptGroup = groups.allErrorGroups.value.find(
|
||||
(g) => g.type === 'execution' && g.title === 'No outputs'
|
||||
(g) => g.type === 'execution' && g.displayTitle === 'No outputs'
|
||||
)
|
||||
expect(promptGroup).toBeDefined()
|
||||
})
|
||||
@@ -546,7 +611,7 @@ describe('useErrorGroups', () => {
|
||||
expect(messages.filter((m) => m === 'Error A')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('includes missing node group title as message', async () => {
|
||||
it('includes missing node group display message', async () => {
|
||||
const { groups } = createErrorGroups()
|
||||
const missingNodesStore = useMissingNodesErrorStore()
|
||||
missingNodesStore.setMissingNodeTypes([
|
||||
@@ -558,7 +623,9 @@ describe('useErrorGroups', () => {
|
||||
(g) => g.type === 'missing_node'
|
||||
)
|
||||
expect(missingGroup).toBeDefined()
|
||||
expect(groups.groupedErrorMessages.value).toContain(missingGroup!.title)
|
||||
expect(groups.groupedErrorMessages.value).toContain(
|
||||
missingGroup!.displayMessage
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -703,6 +770,8 @@ describe('useErrorGroups', () => {
|
||||
(g) => g.type === 'missing_model'
|
||||
)
|
||||
expect(modelGroup).toBeDefined()
|
||||
expect(modelGroup?.groupKey).toBe('missing_model')
|
||||
expect(modelGroup?.displayTitle).toBe('Missing Models (1)')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ import type {
|
||||
import type { MissingMediaGroup } from '@/platform/missingMedia/types'
|
||||
import { groupCandidatesByName } from '@/platform/missingModel/missingModelScan'
|
||||
import { groupCandidatesByMediaType } from '@/platform/missingMedia/missingMediaScan'
|
||||
import {
|
||||
resolveMissingErrorMessage,
|
||||
resolveRunErrorMessage
|
||||
} from '@/platform/errorCatalog/errorMessageResolver'
|
||||
import {
|
||||
isNodeExecutionId,
|
||||
compareExecutionId
|
||||
@@ -40,11 +44,6 @@ import {
|
||||
|
||||
const PROMPT_CARD_ID = '__prompt__'
|
||||
const SINGLE_GROUP_KEY = '__single__'
|
||||
const KNOWN_PROMPT_ERROR_TYPES = new Set([
|
||||
'prompt_no_outputs',
|
||||
'no_prompt',
|
||||
'server_error'
|
||||
])
|
||||
|
||||
/** Sentinel: distinguishes "fetch in-flight" from "fetch done, pack not found (null)". */
|
||||
const RESOLVING = '__RESOLVING__'
|
||||
@@ -66,6 +65,7 @@ export interface SwapNodeGroup {
|
||||
|
||||
interface GroupEntry {
|
||||
type: 'execution'
|
||||
displayTitle: string
|
||||
priority: number
|
||||
cards: Map<string, ErrorCardData>
|
||||
}
|
||||
@@ -104,13 +104,19 @@ function resolveNodeInfo(nodeId: string) {
|
||||
|
||||
function getOrCreateGroup(
|
||||
groupsMap: Map<string, GroupEntry>,
|
||||
title: string,
|
||||
groupKey: string,
|
||||
displayTitle = groupKey,
|
||||
priority = 1
|
||||
): Map<string, ErrorCardData> {
|
||||
let entry = groupsMap.get(title)
|
||||
let entry = groupsMap.get(groupKey)
|
||||
if (!entry) {
|
||||
entry = { type: 'execution', priority, cards: new Map() }
|
||||
groupsMap.set(title, entry)
|
||||
entry = {
|
||||
type: 'execution',
|
||||
displayTitle,
|
||||
priority,
|
||||
cards: new Map()
|
||||
}
|
||||
groupsMap.set(groupKey, entry)
|
||||
}
|
||||
return entry.cards
|
||||
}
|
||||
@@ -160,7 +166,10 @@ function addCardErrorToGroup(
|
||||
card: ErrorCardData,
|
||||
error: ErrorItem
|
||||
) {
|
||||
const group = getOrCreateGroup(messageMap, error.message, 1)
|
||||
const displayTitle =
|
||||
error.displayTitle ?? error.displayMessage ?? error.message
|
||||
const groupKey = error.catalogId ?? displayTitle
|
||||
const group = getOrCreateGroup(messageMap, groupKey, displayTitle, 1)
|
||||
if (!group.has(card.id)) {
|
||||
group.set(card.id, { ...card, errors: [] })
|
||||
}
|
||||
@@ -173,15 +182,16 @@ function compareNodeId(a: ErrorCardData, b: ErrorCardData): number {
|
||||
|
||||
function toSortedGroups(groupsMap: Map<string, GroupEntry>): ErrorGroup[] {
|
||||
return Array.from(groupsMap.entries())
|
||||
.map(([title, groupData]) => ({
|
||||
.map(([rawGroupKey, groupData]) => ({
|
||||
type: 'execution' as const,
|
||||
title,
|
||||
groupKey: `execution:${rawGroupKey}`,
|
||||
displayTitle: groupData.displayTitle,
|
||||
cards: Array.from(groupData.cards.values()).sort(compareNodeId),
|
||||
priority: groupData.priority
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority
|
||||
return a.title.localeCompare(b.title)
|
||||
return a.displayTitle.localeCompare(b.displayTitle)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -199,8 +209,16 @@ function searchErrorGroups(groups: ErrorGroup[], query: string) {
|
||||
cardIndex: ci,
|
||||
searchableNodeId: card.nodeId ?? '',
|
||||
searchableNodeTitle: card.nodeTitle ?? '',
|
||||
searchableMessage: card.errors.map((e) => e.message).join(' '),
|
||||
searchableDetails: card.errors.map((e) => e.details ?? '').join(' ')
|
||||
searchableMessage: card.errors
|
||||
.map((e) =>
|
||||
[e.displayTitle, e.displayMessage, e.message]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
)
|
||||
.join(' '),
|
||||
searchableDetails: card.errors
|
||||
.map((e) => [e.displayDetails, e.details].filter(Boolean).join(' '))
|
||||
.join(' ')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -235,10 +253,7 @@ function searchErrorGroups(groups: ErrorGroup[], query: string) {
|
||||
.filter((group) => group.type !== 'execution' || group.cards.length > 0)
|
||||
}
|
||||
|
||||
export function useErrorGroups(
|
||||
searchQuery: MaybeRefOrGetter<string>,
|
||||
t: (key: string) => string
|
||||
) {
|
||||
export function useErrorGroups(searchQuery: MaybeRefOrGetter<string>) {
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const missingNodesStore = useMissingNodesErrorStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
@@ -323,11 +338,13 @@ export function useErrorGroups(
|
||||
) {
|
||||
if (filterBySelection && !isErrorInSelection(nodeId)) return
|
||||
const groupKey = isSingleNodeSelected.value ? SINGLE_GROUP_KEY : classType
|
||||
const cards = getOrCreateGroup(groupsMap, groupKey, 1)
|
||||
const cards = getOrCreateGroup(groupsMap, groupKey, classType, 1)
|
||||
if (!cards.has(nodeId)) {
|
||||
cards.set(nodeId, createErrorCard(nodeId, classType, idPrefix))
|
||||
}
|
||||
cards.get(nodeId)?.errors.push(...errors)
|
||||
const card = cards.get(nodeId)
|
||||
if (!card) return
|
||||
card.errors.push(...errors)
|
||||
}
|
||||
|
||||
function processPromptError(groupsMap: Map<string, GroupEntry>) {
|
||||
@@ -335,24 +352,27 @@ export function useErrorGroups(
|
||||
return
|
||||
|
||||
const error = executionErrorStore.lastPromptError
|
||||
const groupTitle = error.message
|
||||
const cards = getOrCreateGroup(groupsMap, groupTitle, 0)
|
||||
const isKnown = KNOWN_PROMPT_ERROR_TYPES.has(error.type)
|
||||
|
||||
// For server_error, resolve the i18n key based on the environment
|
||||
let errorTypeKey = error.type
|
||||
if (error.type === 'server_error') {
|
||||
errorTypeKey = isCloud ? 'server_error_cloud' : 'server_error_local'
|
||||
}
|
||||
const i18nKey = `rightSidePanel.promptErrors.${errorTypeKey}.desc`
|
||||
const resolvedDisplay = resolveRunErrorMessage({
|
||||
kind: 'prompt',
|
||||
error,
|
||||
isCloud
|
||||
})
|
||||
const groupDisplayTitle = resolvedDisplay.displayTitle ?? error.message
|
||||
const cards = getOrCreateGroup(
|
||||
groupsMap,
|
||||
`prompt:${error.type}`,
|
||||
groupDisplayTitle,
|
||||
0
|
||||
)
|
||||
|
||||
// Prompt errors are not tied to a node, so they bypass addNodeErrorToGroup.
|
||||
cards.set(PROMPT_CARD_ID, {
|
||||
id: PROMPT_CARD_ID,
|
||||
title: groupTitle,
|
||||
title: groupDisplayTitle,
|
||||
errors: [
|
||||
{
|
||||
message: isKnown ? t(i18nKey) : error.message
|
||||
message: error.message,
|
||||
...resolvedDisplay
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -367,15 +387,24 @@ export function useErrorGroups(
|
||||
for (const [nodeId, nodeError] of Object.entries(
|
||||
executionErrorStore.lastNodeErrors
|
||||
)) {
|
||||
const nodeDisplayName =
|
||||
resolveNodeInfo(nodeId).title || nodeError.class_type
|
||||
addNodeErrorToGroup(
|
||||
groupsMap,
|
||||
nodeId,
|
||||
nodeError.class_type,
|
||||
'node',
|
||||
nodeError.errors.map((e) => ({
|
||||
message: e.message,
|
||||
details: e.details ?? undefined
|
||||
})),
|
||||
nodeError.errors.map((e) => {
|
||||
return {
|
||||
message: e.message,
|
||||
details: e.details ?? undefined,
|
||||
...resolveRunErrorMessage({
|
||||
kind: 'node_validation',
|
||||
error: e,
|
||||
nodeDisplayName
|
||||
})
|
||||
}
|
||||
}),
|
||||
filterBySelection
|
||||
)
|
||||
}
|
||||
@@ -388,6 +417,12 @@ export function useErrorGroups(
|
||||
if (!executionErrorStore.lastExecutionError) return
|
||||
|
||||
const e = executionErrorStore.lastExecutionError
|
||||
const resolvedDisplay = resolveRunErrorMessage({
|
||||
kind: 'execution',
|
||||
error: e,
|
||||
nodeDisplayName: e.node_type,
|
||||
isCloud
|
||||
})
|
||||
addNodeErrorToGroup(
|
||||
groupsMap,
|
||||
String(e.node_id),
|
||||
@@ -398,7 +433,8 @@ export function useErrorGroups(
|
||||
message: `${e.exception_type}: ${e.exception_message}`,
|
||||
details: e.traceback.join('\n'),
|
||||
isRuntimeError: true,
|
||||
exceptionType: e.exception_type
|
||||
exceptionType: e.exception_type,
|
||||
...resolvedDisplay
|
||||
}
|
||||
],
|
||||
filterBySelection
|
||||
@@ -568,16 +604,28 @@ export function useErrorGroups(
|
||||
if (swapNodeGroups.value.length > 0) {
|
||||
groups.push({
|
||||
type: 'swap_nodes' as const,
|
||||
title: st('nodeReplacement.swapNodesTitle', 'Swap Nodes'),
|
||||
priority: 0
|
||||
groupKey: 'swap_nodes',
|
||||
priority: 0,
|
||||
...resolveMissingErrorMessage({
|
||||
kind: 'swap_nodes',
|
||||
nodeTypes: missingNodesStore.missingNodesError?.nodeTypes ?? [],
|
||||
count: swapNodeGroups.value.length,
|
||||
isCloud
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (missingPackGroups.value.length > 0) {
|
||||
groups.push({
|
||||
type: 'missing_node' as const,
|
||||
title: error.message,
|
||||
priority: 1
|
||||
groupKey: 'missing_node',
|
||||
priority: 1,
|
||||
...resolveMissingErrorMessage({
|
||||
kind: 'missing_node',
|
||||
nodeTypes: error.nodeTypes,
|
||||
count: missingPackGroups.value.length,
|
||||
isCloud
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -630,11 +678,21 @@ export function useErrorGroups(
|
||||
|
||||
function buildMissingModelGroups(): ErrorGroup[] {
|
||||
if (!missingModelGroups.value.length) return []
|
||||
const count = missingModelGroups.value.reduce(
|
||||
(total, group) => total + group.models.length,
|
||||
0
|
||||
)
|
||||
return [
|
||||
{
|
||||
type: 'missing_model' as const,
|
||||
title: `${t('rightSidePanel.missingModels.missingModelsTitle')} (${missingModelGroups.value.reduce((count, group) => count + group.models.length, 0)})`,
|
||||
priority: 2
|
||||
groupKey: 'missing_model',
|
||||
priority: 2,
|
||||
...resolveMissingErrorMessage({
|
||||
kind: 'missing_model',
|
||||
groups: missingModelGroups.value,
|
||||
count,
|
||||
isCloud
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -654,8 +712,15 @@ export function useErrorGroups(
|
||||
return [
|
||||
{
|
||||
type: 'missing_media' as const,
|
||||
title: `${t('rightSidePanel.missingMedia.missingMediaTitle')} (${totalItems})`,
|
||||
priority: 3
|
||||
groupKey: 'missing_media',
|
||||
priority: 3,
|
||||
...resolveMissingErrorMessage({
|
||||
kind: 'missing_media',
|
||||
groups: missingMediaGroups.value,
|
||||
count: totalItems,
|
||||
mediaTypes: missingMediaGroups.value.map((group) => group.mediaType),
|
||||
isCloud
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -736,11 +801,21 @@ export function useErrorGroups(
|
||||
|
||||
function buildMissingModelGroupsFiltered(): ErrorGroup[] {
|
||||
if (!filteredMissingModelGroups.value.length) return []
|
||||
const count = filteredMissingModelGroups.value.reduce(
|
||||
(total, group) => total + group.models.length,
|
||||
0
|
||||
)
|
||||
return [
|
||||
{
|
||||
type: 'missing_model' as const,
|
||||
title: `${t('rightSidePanel.missingModels.missingModelsTitle')} (${filteredMissingModelGroups.value.reduce((count, group) => count + group.models.length, 0)})`,
|
||||
priority: 2
|
||||
groupKey: 'missing_model',
|
||||
priority: 2,
|
||||
...resolveMissingErrorMessage({
|
||||
kind: 'missing_model',
|
||||
groups: filteredMissingModelGroups.value,
|
||||
count,
|
||||
isCloud
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -754,8 +829,17 @@ export function useErrorGroups(
|
||||
return [
|
||||
{
|
||||
type: 'missing_media' as const,
|
||||
title: `${t('rightSidePanel.missingMedia.missingMediaTitle')} (${totalItems})`,
|
||||
priority: 3
|
||||
groupKey: 'missing_media',
|
||||
priority: 3,
|
||||
...resolveMissingErrorMessage({
|
||||
kind: 'missing_media',
|
||||
groups: filteredMissingMediaGroups.value,
|
||||
count: totalItems,
|
||||
mediaTypes: filteredMissingMediaGroups.value.map(
|
||||
(group) => group.mediaType
|
||||
),
|
||||
isCloud
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -813,11 +897,11 @@ export function useErrorGroups(
|
||||
if (group.type === 'execution') {
|
||||
for (const card of group.cards) {
|
||||
for (const err of card.errors) {
|
||||
messages.add(err.message)
|
||||
messages.add(err.displayMessage ?? err.message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messages.add(group.title)
|
||||
messages.add(group.displayMessage ?? group.displayTitle)
|
||||
}
|
||||
}
|
||||
return Array.from(messages)
|
||||
|
||||
@@ -23,7 +23,7 @@ export function useErrorReport(cardSource: MaybeRefOrGetter<ErrorCardData>) {
|
||||
return Object.fromEntries(
|
||||
card.errors.map((error, idx) => [
|
||||
idx,
|
||||
enrichedDetails[idx] ?? error.details
|
||||
enrichedDetails[idx] ?? error.displayDetails ?? error.details
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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' }
|
||||
]
|
||||
|
||||
@@ -144,6 +144,7 @@ describe('useLoad3d', () => {
|
||||
setMaterialMode: vi.fn(),
|
||||
toggleCamera: vi.fn(),
|
||||
setFOV: vi.fn(),
|
||||
setRetainViewOnReload: vi.fn(),
|
||||
setLightIntensity: vi.fn(),
|
||||
setCameraState: vi.fn(),
|
||||
loadModel: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -568,17 +569,21 @@ describe('useLoad3d', () => {
|
||||
|
||||
vi.mocked(mockLoad3d.toggleCamera!).mockClear()
|
||||
vi.mocked(mockLoad3d.setFOV!).mockClear()
|
||||
vi.mocked(mockLoad3d.setRetainViewOnReload!).mockClear()
|
||||
|
||||
composable.cameraConfig.value.cameraType = 'orthographic'
|
||||
composable.cameraConfig.value.fov = 90
|
||||
composable.cameraConfig.value.retainViewOnReload = true
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad3d.toggleCamera).toHaveBeenCalledWith('orthographic')
|
||||
expect(mockLoad3d.setFOV).toHaveBeenCalledWith(90)
|
||||
expect(mockLoad3d.setRetainViewOnReload).toHaveBeenCalledWith(true)
|
||||
expect(mockNode.properties['Camera Config']).toEqual({
|
||||
cameraType: 'orthographic',
|
||||
fov: 90,
|
||||
state: null
|
||||
state: null,
|
||||
retainViewOnReload: true
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -483,6 +483,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
nodeRef.value.properties['Camera Config'] = newValue
|
||||
load3d.toggleCamera(newValue.cameraType)
|
||||
load3d.setFOV(newValue.fov)
|
||||
load3d.setRetainViewOnReload(newValue.retainViewOnReload ?? false)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import * as THREE from 'three'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import type { GizmoMode } from '@/extensions/core/load3d/interfaces'
|
||||
import type {
|
||||
CameraState,
|
||||
GizmoMode
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const {
|
||||
cloneSkinnedMock,
|
||||
@@ -769,6 +772,133 @@ describe('Load3d', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('retainViewOnReload', () => {
|
||||
function setupLoadInternal(initialFlag: boolean) {
|
||||
const getCameraState = vi.fn<() => CameraState>(() => ({
|
||||
position: new THREE.Vector3(1, 2, 3),
|
||||
target: new THREE.Vector3(),
|
||||
zoom: 1,
|
||||
cameraType: 'perspective'
|
||||
}))
|
||||
const setCameraState = vi.fn()
|
||||
const getCurrentCameraType = vi.fn(() => 'perspective' as const)
|
||||
const loaderLoadModel = vi.fn().mockResolvedValue(undefined)
|
||||
Object.assign(ctx.load3d, {
|
||||
cameraManager: {
|
||||
...ctx.cameraManager,
|
||||
getCameraState,
|
||||
setCameraState,
|
||||
getCurrentCameraType
|
||||
},
|
||||
controlsManager: { ...ctx.controlsManager, reset: vi.fn() },
|
||||
loaderManager: { loadModel: loaderLoadModel },
|
||||
modelManager: {
|
||||
...ctx.modelManager,
|
||||
currentModel: new THREE.Group(),
|
||||
originalModel: null
|
||||
},
|
||||
animationManager: {
|
||||
...ctx.animationManager,
|
||||
setupModelAnimations: vi.fn()
|
||||
},
|
||||
handleResize: vi.fn(),
|
||||
retainViewOnReload: initialFlag,
|
||||
hasLoadedModel: false
|
||||
})
|
||||
return { getCameraState, setCameraState, getCurrentCameraType }
|
||||
}
|
||||
|
||||
it('first load uses default framing even with retain enabled', async () => {
|
||||
const mocks = setupLoadInternal(true)
|
||||
|
||||
await ctx.load3d.loadModel('a.glb')
|
||||
|
||||
// hasLoadedModel started false, so retain shouldn't kick in yet.
|
||||
expect(ctx.cameraManager.reset).toHaveBeenCalledOnce()
|
||||
expect(mocks.getCameraState).not.toHaveBeenCalled()
|
||||
expect(mocks.setCameraState).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('subsequent load captures camera state, skips reset, and restores it', async () => {
|
||||
const mocks = setupLoadInternal(true)
|
||||
|
||||
await ctx.load3d.loadModel('a.glb')
|
||||
;(ctx.cameraManager.reset as ReturnType<typeof vi.fn>).mockClear()
|
||||
mocks.getCameraState.mockClear()
|
||||
mocks.setCameraState.mockClear()
|
||||
|
||||
await ctx.load3d.loadModel('b.glb')
|
||||
|
||||
expect(ctx.cameraManager.reset).not.toHaveBeenCalled()
|
||||
expect(mocks.getCameraState).toHaveBeenCalledOnce()
|
||||
expect(mocks.setCameraState).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not retain when the flag is off, even after a prior load', async () => {
|
||||
const mocks = setupLoadInternal(false)
|
||||
|
||||
await ctx.load3d.loadModel('a.glb')
|
||||
;(ctx.cameraManager.reset as ReturnType<typeof vi.fn>).mockClear()
|
||||
mocks.getCameraState.mockClear()
|
||||
mocks.setCameraState.mockClear()
|
||||
|
||||
await ctx.load3d.loadModel('b.glb')
|
||||
|
||||
expect(ctx.cameraManager.reset).toHaveBeenCalledOnce()
|
||||
expect(mocks.getCameraState).not.toHaveBeenCalled()
|
||||
expect(mocks.setCameraState).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('toggles to the saved camera type before restoring state when types differ', async () => {
|
||||
const mocks = setupLoadInternal(true)
|
||||
mocks.getCameraState.mockImplementation(() => ({
|
||||
position: new THREE.Vector3(0, 0, 5),
|
||||
target: new THREE.Vector3(),
|
||||
zoom: 1,
|
||||
cameraType: 'orthographic'
|
||||
}))
|
||||
// First load (active type stays perspective per the default mock).
|
||||
await ctx.load3d.loadModel('a.glb')
|
||||
;(ctx.cameraManager.toggleCamera as ReturnType<typeof vi.fn>).mockClear()
|
||||
|
||||
await ctx.load3d.loadModel('b.glb')
|
||||
|
||||
expect(ctx.cameraManager.toggleCamera).toHaveBeenCalledWith(
|
||||
'orthographic'
|
||||
)
|
||||
expect(mocks.setCameraState).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('resets hasLoadedModel on clearModel so the next load uses default framing', async () => {
|
||||
const mocks = setupLoadInternal(true)
|
||||
await ctx.load3d.loadModel('a.glb')
|
||||
ctx.load3d.clearModel()
|
||||
;(ctx.cameraManager.reset as ReturnType<typeof vi.fn>).mockClear()
|
||||
mocks.getCameraState.mockClear()
|
||||
|
||||
await ctx.load3d.loadModel('b.glb')
|
||||
|
||||
expect(ctx.cameraManager.reset).toHaveBeenCalledOnce()
|
||||
expect(mocks.getCameraState).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('setRetainViewOnReload flips the runtime behavior between loads', async () => {
|
||||
const mocks = setupLoadInternal(false)
|
||||
|
||||
await ctx.load3d.loadModel('a.glb')
|
||||
ctx.load3d.setRetainViewOnReload(true)
|
||||
;(ctx.cameraManager.reset as ReturnType<typeof vi.fn>).mockClear()
|
||||
mocks.getCameraState.mockClear()
|
||||
mocks.setCameraState.mockClear()
|
||||
|
||||
await ctx.load3d.loadModel('b.glb')
|
||||
|
||||
expect(ctx.cameraManager.reset).not.toHaveBeenCalled()
|
||||
expect(mocks.getCameraState).toHaveBeenCalledOnce()
|
||||
expect(mocks.setCameraState).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('captureScene', () => {
|
||||
it('hides the gizmo helper during capture and restores it after success', async () => {
|
||||
const captureResult = { scene: 'a', mask: 'b', normal: 'c' }
|
||||
|
||||
@@ -104,6 +104,8 @@ class Load3d {
|
||||
private disposeContextMenuGuard: (() => void) | null = null
|
||||
private resizeObserver: ResizeObserver | null = null
|
||||
private getZoomScaleCallback: (() => number) | undefined
|
||||
private retainViewOnReload: boolean = false
|
||||
private hasLoadedModel: boolean = false
|
||||
|
||||
constructor(
|
||||
container: Element | HTMLElement,
|
||||
@@ -564,13 +566,33 @@ class Load3d {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles whether `_loadModelInternal` preserves the current camera state
|
||||
* across model loads. When enabled and a model has previously loaded, the
|
||||
* camera position/target/zoom (and camera type) are captured before the
|
||||
* scene clears and restored after the new model is in place.
|
||||
*/
|
||||
public setRetainViewOnReload(value: boolean): void {
|
||||
this.retainViewOnReload = value
|
||||
}
|
||||
|
||||
private async _loadModelInternal(
|
||||
url: string,
|
||||
originalFileName?: string,
|
||||
options?: LoadModelOptions
|
||||
): Promise<void> {
|
||||
this.cameraManager.reset()
|
||||
this.controlsManager.reset()
|
||||
// Retain view only kicks in after a successful first load — on the very
|
||||
// first load there's no meaningful "current" framing to preserve, so the
|
||||
// default `setupForModel` framing wins.
|
||||
const shouldRetainView = this.retainViewOnReload && this.hasLoadedModel
|
||||
const savedCameraState = shouldRetainView
|
||||
? this.cameraManager.getCameraState()
|
||||
: null
|
||||
|
||||
if (!shouldRetainView) {
|
||||
this.cameraManager.reset()
|
||||
this.controlsManager.reset()
|
||||
}
|
||||
this.gizmoManager.detach()
|
||||
this.modelManager.clearModel()
|
||||
this.animationManager.dispose()
|
||||
@@ -583,6 +605,19 @@ class Load3d {
|
||||
this.modelManager.currentModel,
|
||||
this.modelManager.originalModel
|
||||
)
|
||||
this.hasLoadedModel = true
|
||||
}
|
||||
|
||||
if (savedCameraState) {
|
||||
// SceneModelManager.setupModel called setupForModel which clobbered the
|
||||
// camera. Restore the captured state on top of that.
|
||||
if (
|
||||
savedCameraState.cameraType !==
|
||||
this.cameraManager.getCurrentCameraType()
|
||||
) {
|
||||
this.toggleCamera(savedCameraState.cameraType)
|
||||
}
|
||||
this.cameraManager.setCameraState(savedCameraState)
|
||||
}
|
||||
|
||||
this.handleResize()
|
||||
@@ -607,6 +642,7 @@ class Load3d {
|
||||
this.gizmoManager.detach()
|
||||
this.modelManager.clearModel()
|
||||
this.adapterRef.current = null
|
||||
this.hasLoadedModel = false
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface CameraConfig {
|
||||
cameraType: CameraType
|
||||
fov: number
|
||||
state?: CameraState
|
||||
retainViewOnReload?: boolean
|
||||
}
|
||||
|
||||
export interface LightConfig {
|
||||
|
||||
@@ -800,7 +800,7 @@
|
||||
"CONTROL_NET": "ControlNet",
|
||||
"CURVE": "منحنى",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_LANDMARKER": "FACE_LANDMARKER",
|
||||
"FACE_DETECTION_MODEL": "نموذج كشف الوجه",
|
||||
"FACE_LANDMARKS": "معالم الوجه",
|
||||
"FILE_3D": "ملف ثلاثي الأبعاد",
|
||||
"FILE_3D_FBX": "ملف FBX ثلاثي الأبعاد",
|
||||
@@ -911,6 +911,44 @@
|
||||
"paused": "تم الإيقاف مؤقتًا",
|
||||
"resume": "استئناف التنزيل"
|
||||
},
|
||||
"errorCatalog": {
|
||||
"fallbacks": {
|
||||
"inputName": "مدخل غير معروف",
|
||||
"nodeName": "هذه العقدة"
|
||||
},
|
||||
"promptErrors": {
|
||||
"no_prompt": {
|
||||
"desc": "بيانات سير العمل المرسلة إلى الخادم فارغة. قد يكون هذا خطأ غير متوقع في النظام."
|
||||
},
|
||||
"prompt_no_outputs": {
|
||||
"desc": "سير العمل لا يحتوي على أي عقدة إخراج (مثل حفظ الصورة أو معاينة الصورة) لإنتاج نتيجة."
|
||||
},
|
||||
"server_error_cloud": {
|
||||
"desc": "واجه الخادم خطأ غير متوقع. يرجى المحاولة لاحقاً."
|
||||
},
|
||||
"server_error_local": {
|
||||
"desc": "واجه الخادم خطأ غير متوقع. يرجى مراجعة سجلات الخادم."
|
||||
}
|
||||
},
|
||||
"runtimeErrors": {
|
||||
"execution_failed": {
|
||||
"itemLabel": "{nodeName}",
|
||||
"toastMessageCloud": "حدث خطأ أثناء تنفيذ هذه العقدة. تحقق من المدخلات أو جرّب إعداداً مختلفاً. لم يتم خصم أي رصيد.",
|
||||
"toastMessageLocal": "حدث خطأ أثناء تنفيذ هذه العقدة. تحقق من المدخلات أو جرّب إعداداً مختلفاً.",
|
||||
"toastTitle": "فشل {nodeName}"
|
||||
}
|
||||
},
|
||||
"validationErrors": {
|
||||
"required_input_missing": {
|
||||
"details": "{nodeName} تفتقد إلى مدخل مطلوب: {inputName}",
|
||||
"itemLabel": "{nodeName} - {inputName}",
|
||||
"message": "بعض منافذ الإدخال المطلوبة غير متصلة.",
|
||||
"title": "الاتصال مفقود",
|
||||
"toastMessage": "{nodeName} تفتقد إلى مدخل مطلوب: {inputName}",
|
||||
"toastTitle": "مدخل مطلوب مفقود"
|
||||
}
|
||||
}
|
||||
},
|
||||
"errorDialog": {
|
||||
"accessRestrictedMessage": "Your account is not authorized for this feature.",
|
||||
"accessRestrictedTitle": "Access Restricted",
|
||||
@@ -1713,6 +1751,7 @@
|
||||
"reloadingModel": "جاري إعادة تحميل النموذج...",
|
||||
"removeBackgroundImage": "إزالة صورة الخلفية",
|
||||
"resizeNodeMatchOutput": "تغيير حجم العقدة لتتناسب مع المخرج",
|
||||
"retainViewOnReload": "تثبيت عرض الكاميرا عند إعادة تحميل النموذج",
|
||||
"scene": "المشهد",
|
||||
"showGrid": "عرض الشبكة",
|
||||
"showSkeleton": "إظهار الهيكل العظمي",
|
||||
@@ -2280,6 +2319,7 @@
|
||||
"Meshy": "Meshy",
|
||||
"MiniMax": "MiniMax",
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenRouter": "OpenRouter",
|
||||
"PixVerse": "PixVerse",
|
||||
"Quiver": "Quiver",
|
||||
"Recraft": "Recraft",
|
||||
@@ -2296,6 +2336,7 @@
|
||||
"Vidu": "فيدو",
|
||||
"Wan": "وان",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"adjustments": "تعديلات",
|
||||
"advanced": "متقدم",
|
||||
"api node": "عقدة API",
|
||||
"attention_experiments": "تجارب الانتباه",
|
||||
@@ -2435,6 +2476,43 @@
|
||||
},
|
||||
"title": "جهازك غير مدعوم"
|
||||
},
|
||||
"oauth": {
|
||||
"consent": {
|
||||
"allow": "متابعة",
|
||||
"appTypeNative": "تطبيق أصلي",
|
||||
"appTypeWeb": "تطبيق ويب",
|
||||
"deny": "إلغاء",
|
||||
"errorExpired": "انتهت صلاحية طلب الموافقة هذا أو تم استخدامه بالفعل. يرجى إعادة البدء من تطبيق العميل.",
|
||||
"errorScopeBroadening": "الأذونات التي تمت الموافقة عليها سابقًا لا تغطي هذا الطلب. ستحتاج إلى إعادة التفويض بالأذونات الجديدة.",
|
||||
"errorUnavailable": "هذه الميزة غير متوفرة حاليًا. يرجى التواصل مع الدعم إذا استمرت المشكلة.",
|
||||
"genericError": "فشل طلب OAuth. يرجى إعادة البدء من تطبيق العميل.",
|
||||
"loading": "جارٍ تحميل طلب التفويض…",
|
||||
"missingRequest": "هذا الطلب التفويضي غير موجود. يرجى إعادة البدء من تطبيق العميل.",
|
||||
"noWorkspaces": "لا توجد مساحات عمل مؤهلة لهذا الطلب.",
|
||||
"permissionsHeader": "الأذونات",
|
||||
"redirectNotice": "سيتم إعادة توجيهك إلى",
|
||||
"resourceFallback": "هذا التطبيق",
|
||||
"sessionError": "فشل في إنشاء الجلسة. يرجى المحاولة مرة أخرى.",
|
||||
"sessionErrorToastSummary": "تعذر متابعة تسجيل الدخول عبر OAuth",
|
||||
"subtitle": "سجّل الدخول إلى {resource} للمتابعة",
|
||||
"title": "{client} يطلب الوصول",
|
||||
"workspaceHelp": "تنطبق الأذونات على مساحة العمل هذه فقط.",
|
||||
"workspaceLabel": "مساحة العمل"
|
||||
},
|
||||
"scopes": {
|
||||
"mcp:tools:call": {
|
||||
"label": "تشغيل سير العمل نيابةً عنك"
|
||||
},
|
||||
"mcp:tools:read": {
|
||||
"label": "عرض أدوات سير العمل المتاحة"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"member": "عضو",
|
||||
"owner": "المالك",
|
||||
"personal": "شخصي"
|
||||
}
|
||||
},
|
||||
"openSharedWorkflow": {
|
||||
"author": "المؤلف:",
|
||||
"copyAssetsAndOpen": "استيراد الأصول وفتح سير العمل",
|
||||
@@ -2662,20 +2740,6 @@
|
||||
"normal": "عادي",
|
||||
"parameters": "المعلمات",
|
||||
"pinned": "مثبت",
|
||||
"promptErrors": {
|
||||
"no_prompt": {
|
||||
"desc": "بيانات سير العمل المرسلة إلى الخادم فارغة. قد يكون هذا خطأ غير متوقع في النظام."
|
||||
},
|
||||
"prompt_no_outputs": {
|
||||
"desc": "سير العمل لا يحتوي على أي عقدة إخراج (مثل حفظ الصورة، معاينة الصورة) لإنتاج نتيجة."
|
||||
},
|
||||
"server_error_cloud": {
|
||||
"desc": "واجه الخادم خطأ غير متوقع. يرجى المحاولة لاحقاً."
|
||||
},
|
||||
"server_error_local": {
|
||||
"desc": "واجه الخادم خطأ غير متوقع. يرجى مراجعة سجلات الخادم."
|
||||
}
|
||||
},
|
||||
"properties": "الخصائص",
|
||||
"removeFavorite": "إزالة من المفضلة",
|
||||
"resetAllParameters": "إعادة تعيين جميع المعلمات",
|
||||
@@ -3073,6 +3137,7 @@
|
||||
"sourceDesc": "التجميع حسب نوع المصدر (أساسي، مخصص، API)"
|
||||
},
|
||||
"noBookmarkedNodes": "لا توجد مفضلات بعد",
|
||||
"noMatchingNodes": "لا توجد عقد تطابق \"{query}\"",
|
||||
"resetView": "إعادة تعيين العرض إلى الافتراضي",
|
||||
"sections": {
|
||||
"bookmarked": "المفضلة",
|
||||
@@ -3458,8 +3523,7 @@
|
||||
"newest": "الأحدث",
|
||||
"popular": "الأكثر شيوعًا",
|
||||
"recommended": "موصى به",
|
||||
"searchPlaceholder": "بحث...",
|
||||
"vramLowToHigh": "استخدام VRAM (من الأقل إلى الأعلى)"
|
||||
"searchPlaceholder": "بحث..."
|
||||
},
|
||||
"sorting": "ترتيب حسب",
|
||||
"title": "ابدأ باستخدام قالب",
|
||||
@@ -3623,7 +3687,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"
|
||||
},
|
||||
@@ -1790,6 +1800,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyAndNode": {
|
||||
"description": "عملية AND المنطقية. تُرجع صحيح إذا كانت جميع القيم صحيحة حسب قواعد الحقيقة في بايثون.",
|
||||
"display_name": "و",
|
||||
"inputs": {
|
||||
"values": {
|
||||
"name": "القيم"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyMathExpression": {
|
||||
"display_name": "تعبير رياضي",
|
||||
"inputs": {
|
||||
@@ -1813,6 +1837,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyNotNode": {
|
||||
"description": "عملية NOT المنطقية. تُرجع صحيح إذا كانت القيمة غير صحيحة حسب قواعد الحقيقة في بايثون.",
|
||||
"display_name": "ليس",
|
||||
"inputs": {
|
||||
"value": {
|
||||
"name": "القيمة"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyNumberConvert": {
|
||||
"display_name": "تحويل الرقم",
|
||||
"inputs": {
|
||||
@@ -1829,6 +1867,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyOrNode": {
|
||||
"description": "عملية OR المنطقية. تُرجع صحيح إذا كانت أي من القيم صحيحة حسب قواعد الحقيقة في بايثون.",
|
||||
"display_name": "أو",
|
||||
"inputs": {
|
||||
"values": {
|
||||
"name": "القيم"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfySwitchNode": {
|
||||
"display_name": "مفتاح التحويل",
|
||||
"inputs": {
|
||||
@@ -5699,6 +5751,7 @@
|
||||
}
|
||||
},
|
||||
"ImageCropV2": {
|
||||
"description": "قص الصورة إلى الأبعاد المحددة.",
|
||||
"display_name": "قص الصورة",
|
||||
"inputs": {
|
||||
"crop_region": {
|
||||
@@ -5719,6 +5772,7 @@
|
||||
}
|
||||
},
|
||||
"ImageDeduplication": {
|
||||
"description": "إزالة الصور المكررة أو المتشابهة جداً من القائمة.",
|
||||
"display_name": "إزالة تكرار الصور",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -5773,6 +5827,7 @@
|
||||
}
|
||||
},
|
||||
"ImageGrid": {
|
||||
"description": "ترتيب عدة صور في شبكة.",
|
||||
"display_name": "شبكة الصور",
|
||||
"inputs": {
|
||||
"cell_height": {
|
||||
@@ -8366,6 +8421,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageDataSetFromFolder": {
|
||||
"description": "تحميل مجموعة بيانات من الصور من مجلد محدد وإرجاع قائمة بالصور. الصيغ المدعومة: PNG، JPG، JPEG، WEBP.",
|
||||
"display_name": "تحميل مجموعة بيانات الصور من مجلد",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8409,6 +8465,7 @@
|
||||
}
|
||||
},
|
||||
"LoadImageTextDataSetFromFolder": {
|
||||
"description": "تحميل مجموعة بيانات من أزواج الصور والتعليقات النصية من مجلد محدد وإرجاعها كقائمة. الصيغ المدعومة: PNG، JPG، JPEG، WEBP.",
|
||||
"display_name": "تحميل مجموعة بيانات الصور والنصوص من مجلد",
|
||||
"inputs": {
|
||||
"folder": {
|
||||
@@ -8463,6 +8520,7 @@
|
||||
}
|
||||
},
|
||||
"LoadTrainingDataset": {
|
||||
"description": "تحميل مجموعة بيانات التدريب المشفرة (الفضاء الكامن + الشروط) من القرص لاستخدامها في التدريب.",
|
||||
"display_name": "تحميل مجموعة بيانات التدريب",
|
||||
"inputs": {
|
||||
"folder_name": {
|
||||
@@ -9246,6 +9304,7 @@
|
||||
}
|
||||
},
|
||||
"MakeTrainingDataset": {
|
||||
"description": "ترميز الصور باستخدام VAE والنصوص باستخدام CLIP لإنشاء مجموعة بيانات تدريبية من الفضاء الكامن والشروط.",
|
||||
"display_name": "إنشاء مجموعة بيانات تدريبية",
|
||||
"inputs": {
|
||||
"clip": {
|
||||
@@ -9337,14 +9396,15 @@
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceLandmarker": {
|
||||
"description": "اكتشاف معالم الوجه باستخدام نموذج MediaPipe.",
|
||||
"display_name": "MediaPipe Face Landmarker",
|
||||
"inputs": {
|
||||
"detector_variant": {
|
||||
"name": "detector_variant",
|
||||
"tooltip": "نطاق كاشف الوجه. 'short' مخصص للوجوه القريبة (ضمن ~٢ متر من الكاميرا)؛ 'full' يغطي الوجوه البعيدة/الأصغر (حتى ~٥ متر) لكنه أبطأ. 'both' يشغل كلا الكاشفين ويحتفظ بالكاشف الذي وجد وجوهًا أكثر في كل إطار (تكلفة كشف مضاعفة تقريبًا)."
|
||||
},
|
||||
"face_landmarker": {
|
||||
"name": "face_landmarker"
|
||||
"face_detection_model": {
|
||||
"name": "face_detection_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "الصورة"
|
||||
@@ -9374,6 +9434,7 @@
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMask": {
|
||||
"description": "رسم قناع باستخدام معالم الوجه.",
|
||||
"display_name": "MediaPipe Face Mask",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
@@ -9391,6 +9452,7 @@
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMeshVisualize": {
|
||||
"description": "رسم شبكة معالم الوجه على الصورة المدخلة.",
|
||||
"display_name": "تصوير شبكة وجه MediaPipe",
|
||||
"inputs": {
|
||||
"color": {
|
||||
@@ -9423,6 +9485,7 @@
|
||||
}
|
||||
},
|
||||
"MergeImageLists": {
|
||||
"description": "دمج عدة قوائم صور في قائمة واحدة.",
|
||||
"display_name": "دمج قوائم الصور",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -9877,6 +9940,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeInference": {
|
||||
"description": "تشغيل MoGe على صورة واحدة لتقدير العمق والهندسة.",
|
||||
"display_name": "استدلال MoGe",
|
||||
"inputs": {
|
||||
"apply_mask": {
|
||||
@@ -9913,6 +9977,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePanoramaInference": {
|
||||
"description": "تشغيل MoGe على صورة بانورامية إكويركتانجولار عن طريق تقسيمها إلى ١٢ منظوراً، إجراء الاستدلال على كل منها، ودمج النتائج في خريطة عمق واحدة.",
|
||||
"display_name": "استدلال MoGe بانوراما",
|
||||
"inputs": {
|
||||
"batch_size": {
|
||||
@@ -9947,6 +10012,7 @@
|
||||
}
|
||||
},
|
||||
"MoGePointMapToMesh": {
|
||||
"description": "تحويل خريطة نقاط MoGe إلى شبكة ثلاثية الأبعاد.",
|
||||
"display_name": "MoGe تحويل خريطة النقاط إلى شبكة",
|
||||
"inputs": {
|
||||
"batch_index": {
|
||||
@@ -9976,6 +10042,7 @@
|
||||
}
|
||||
},
|
||||
"MoGeRender": {
|
||||
"description": "عرض خريطة عمق أو خريطة عادية من بيانات الهندسة.",
|
||||
"display_name": "MoGe عرض",
|
||||
"inputs": {
|
||||
"moge_geometry": {
|
||||
@@ -12264,6 +12331,7 @@
|
||||
}
|
||||
},
|
||||
"NormalizeImages": {
|
||||
"description": "تطبيع الصور باستخدام المتوسط والانحراف المعياري.",
|
||||
"display_name": "تطبيع الصور",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -12593,6 +12661,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": {
|
||||
@@ -13406,6 +13507,7 @@
|
||||
}
|
||||
},
|
||||
"RandomCropImages": {
|
||||
"description": "قص الصورة بشكل عشوائي إلى الأبعاد المحددة.",
|
||||
"display_name": "قص عشوائي للصور",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -14268,6 +14370,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByLongerEdge": {
|
||||
"description": "تغيير حجم الصور بحيث يتطابق الحافة الأطول مع البعد المحدد مع الحفاظ على نسبة العرض إلى الارتفاع.",
|
||||
"display_name": "تغيير حجم الصور حسب الحافة الأطول",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14287,6 +14390,7 @@
|
||||
}
|
||||
},
|
||||
"ResizeImagesByShorterEdge": {
|
||||
"description": "تغيير حجم الصور بحيث يتطابق الحافة الأقصر مع البُعد المحدد مع الحفاظ على نسبة العرض إلى الارتفاع.",
|
||||
"display_name": "تغيير حجم الصور حسب الحافة الأقصر",
|
||||
"inputs": {
|
||||
"images": {
|
||||
@@ -14306,6 +14410,7 @@
|
||||
}
|
||||
},
|
||||
"ResolutionBucket": {
|
||||
"description": "تجميع الـ latent والتهيئات في مجموعات (buckets)",
|
||||
"display_name": "تجميع الدقة",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -14538,6 +14643,164 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rodin3D_Gen25_Image": {
|
||||
"description": "إنشاء نموذج ثلاثي الأبعاد من ١ إلى ٥ صور مرجعية عبر Rodin Gen-2.5. اختر وضع (سريع / عادي / عالي جدًا) لضبط الجودة مقابل التكلفة.",
|
||||
"display_name": "Rodin 3D Gen-2.5 - من صورة إلى ثلاثي الأبعاد",
|
||||
"inputs": {
|
||||
"TAPose": {
|
||||
"name": "T/A Pose",
|
||||
"tooltip": "وضعية T/A للنماذج البشرية."
|
||||
},
|
||||
"addon_highpack": {
|
||||
"name": "إضافة HighPack",
|
||||
"tooltip": "إضافة HighPack: خامات ٤K وزيادة عدد الأوجه ~١٦ مرة في وضع Quad."
|
||||
},
|
||||
"bbox_height": {
|
||||
"name": "ارتفاع الصندوق المحيط",
|
||||
"tooltip": "ارتفاع الصندوق المحيط (محور Z)."
|
||||
},
|
||||
"bbox_length": {
|
||||
"name": "طول الصندوق المحيط",
|
||||
"tooltip": "طول الصندوق المحيط (محور X)."
|
||||
},
|
||||
"bbox_width": {
|
||||
"name": "عرض الصندوق المحيط",
|
||||
"tooltip": "عرض الصندوق المحيط (محور Y). ضع القيمة ٠ مع البقية لتخطي الصندوق."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"geometry_file_format": {
|
||||
"name": "تنسيق ملف الهندسة"
|
||||
},
|
||||
"hd_texture": {
|
||||
"name": "خامة عالية الجودة",
|
||||
"tooltip": "تعزيز جودة الخامة."
|
||||
},
|
||||
"height_cm": {
|
||||
"name": "الارتفاع (سم)",
|
||||
"tooltip": "ارتفاع النموذج التقريبي بالسنتيمتر (٠ للتخطي)."
|
||||
},
|
||||
"images": {
|
||||
"name": "الصور",
|
||||
"tooltip": "١-٥ صور. تُستخدم الصورة الأولى للمواد عند العرض من عدة زوايا."
|
||||
},
|
||||
"material": {
|
||||
"name": "المادة"
|
||||
},
|
||||
"mode": {
|
||||
"name": "الوضع",
|
||||
"tooltip": "وضع التوليد. عادي = متوازن. سريع = ١٬٠٠٠-٢٠٬٠٠٠ وجه للنماذج السريعة. عالي جدًا = ٢٠٬٠٠٠-٢٬٠٠٠٬٠٠٠ وجه مع تفاصيل دقيقة اختيارية."
|
||||
},
|
||||
"mode_creative": {
|
||||
"name": "إبداعي"
|
||||
},
|
||||
"mode_polygon_count": {
|
||||
"name": "عدد الأوجه"
|
||||
},
|
||||
"mode_tier": {
|
||||
"name": "الطبقة"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة"
|
||||
},
|
||||
"texture_delight": {
|
||||
"name": "إزالة الإضاءة من الخامة",
|
||||
"tooltip": "إزالة الإضاءة المخبوزة من الخامات."
|
||||
},
|
||||
"texture_mode": {
|
||||
"name": "وضع الخامة",
|
||||
"tooltip": "إعداد جودة الخامة. 'افتراضي' يستخدم إعداد الخادم الافتراضي للطبقة المختارة."
|
||||
},
|
||||
"use_original_alpha": {
|
||||
"name": "الحفاظ على الشفافية الأصلية",
|
||||
"tooltip": "الحفاظ على شفافية الصورة."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "ملف النموذج",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rodin3D_Gen25_Text": {
|
||||
"description": "إنشاء نموذج ثلاثي الأبعاد من وصف نصي باستخدام Rodin Gen-2.5. اختر وضع التشغيل (سريع / عادي / عالي للغاية) لضبط الجودة مقابل التكلفة.",
|
||||
"display_name": "Rodin 3D Gen-2.5 - تحويل النص إلى نموذج ثلاثي الأبعاد",
|
||||
"inputs": {
|
||||
"TAPose": {
|
||||
"name": "وضعية T/A",
|
||||
"tooltip": "وضعية T/A للنماذج البشرية."
|
||||
},
|
||||
"addon_highpack": {
|
||||
"name": "إضافة HighPack",
|
||||
"tooltip": "إضافة HighPack: خامات 4K وزيادة عدد الأوجه ~١٦ مرة في وضع Quad."
|
||||
},
|
||||
"bbox_height": {
|
||||
"name": "ارتفاع الصندوق المحيط",
|
||||
"tooltip": "ارتفاع الصندوق المحيط (محور Z)."
|
||||
},
|
||||
"bbox_length": {
|
||||
"name": "طول الصندوق المحيط",
|
||||
"tooltip": "طول الصندوق المحيط (محور X)."
|
||||
},
|
||||
"bbox_width": {
|
||||
"name": "عرض الصندوق المحيط",
|
||||
"tooltip": "عرض الصندوق المحيط (محور Y). ضع القيمة ٠ مع القيم الأخرى لتخطي الصندوق."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"geometry_file_format": {
|
||||
"name": "صيغة ملف الهندسة"
|
||||
},
|
||||
"hd_texture": {
|
||||
"name": "خامة عالية الجودة",
|
||||
"tooltip": "تعزيز جودة الخامة."
|
||||
},
|
||||
"height_cm": {
|
||||
"name": "الارتفاع (سم)",
|
||||
"tooltip": "الارتفاع التقريبي للنموذج بالسنتيمتر (٠ للتخطي)."
|
||||
},
|
||||
"material": {
|
||||
"name": "الخامة"
|
||||
},
|
||||
"mode": {
|
||||
"name": "الوضع",
|
||||
"tooltip": "وضع التوليد. عادي = توازن بين الجودة والسرعة. سريع = ١٬٠٠٠-٢٠٬٠٠٠ وجه للنماذج الأولية السريعة. عالي للغاية = ٢٠٬٠٠٠-٢٬٠٠٠٬٠٠٠ وجه مع تفاصيل دقيقة اختيارية."
|
||||
},
|
||||
"mode_creative": {
|
||||
"name": "إبداعي"
|
||||
},
|
||||
"mode_polygon_count": {
|
||||
"name": "عدد الأوجه"
|
||||
},
|
||||
"mode_tier": {
|
||||
"name": "الفئة"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "الوصف النصي",
|
||||
"tooltip": "الوصف النصي للنموذج ثلاثي الأبعاد."
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة"
|
||||
},
|
||||
"texture_delight": {
|
||||
"name": "إزالة الإضاءة من الخامة",
|
||||
"tooltip": "إزالة الإضاءة المخبوزة من الخامات."
|
||||
},
|
||||
"texture_mode": {
|
||||
"name": "وضع الخامة",
|
||||
"tooltip": "إعداد جودة الخامة. 'افتراضي' يستخدم الإعداد الافتراضي للخادم حسب الفئة المختارة."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "ملف النموذج",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rodin3D_Regular": {
|
||||
"description": "توليد أصول ثلاثية الأبعاد باستخدام واجهة برمجة تطبيقات رودين",
|
||||
"display_name": "رودين 3D توليد - توليد عادي",
|
||||
@@ -15650,6 +15913,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageDataSetToFolder": {
|
||||
"description": "حفظ مجموعة بيانات من الصور في مجلد محدد. الصيغ المدعومة: PNG.",
|
||||
"display_name": "حفظ مجموعة بيانات الصور في مجلد",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15667,6 +15931,7 @@
|
||||
}
|
||||
},
|
||||
"SaveImageTextDataSetToFolder": {
|
||||
"description": "حفظ مجموعة بيانات من أزواج الصور والتعليقات النصية في مجلد محدد. تُحفظ الصور كملفات PNG والتعليقات كملفات TXT بنفس بادئة اسم الملف.",
|
||||
"display_name": "حفظ مجموعة بيانات الصور والنصوص في مجلد",
|
||||
"inputs": {
|
||||
"filename_prefix": {
|
||||
@@ -15737,6 +16002,7 @@
|
||||
}
|
||||
},
|
||||
"SaveTrainingDataset": {
|
||||
"description": "حفظ مجموعة بيانات التدريب المشفرة (latents + التهيئة) على القرص لتحميلها بكفاءة أثناء التدريب.",
|
||||
"display_name": "حفظ مجموعة بيانات التدريب",
|
||||
"inputs": {
|
||||
"conditioning": {
|
||||
@@ -15923,6 +16189,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleDataset": {
|
||||
"description": "تبديل ترتيب الصور في القائمة بشكل عشوائي.",
|
||||
"display_name": "تبديل ترتيب مجموعة بيانات الصور",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -15945,6 +16212,7 @@
|
||||
}
|
||||
},
|
||||
"ShuffleImageTextDataset": {
|
||||
"description": "تبديل ترتيب أزواج الصورة والنص في القائمة بشكل عشوائي.",
|
||||
"display_name": "تبديل ترتيب مجموعة بيانات الصور والنصوص",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
@@ -19445,6 +19713,7 @@
|
||||
}
|
||||
},
|
||||
"VoxelToMesh": {
|
||||
"description": "تحويل شبكة فوكسل إلى شبكة مضلعة (mesh).",
|
||||
"display_name": "تحويل الفوكسل إلى شبكة",
|
||||
"inputs": {
|
||||
"algorithm": {
|
||||
@@ -19464,6 +19733,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,7 +1785,7 @@
|
||||
"CONTROL_NET": "CONTROL_NET",
|
||||
"CURVE": "CURVE",
|
||||
"ELEVENLABS_VOICE": "ELEVENLABS_VOICE",
|
||||
"FACE_LANDMARKER": "FACE_LANDMARKER",
|
||||
"FACE_DETECTION_MODEL": "FACE_DETECTION_MODEL",
|
||||
"FACE_LANDMARKS": "FACE_LANDMARKS",
|
||||
"FILE_3D": "FILE_3D",
|
||||
"FILE_3D_FBX": "FILE_3D_FBX",
|
||||
@@ -1950,6 +1952,7 @@
|
||||
},
|
||||
"load3d": {
|
||||
"switchCamera": "Switch Camera",
|
||||
"retainViewOnReload": "Lock camera view across model reloads",
|
||||
"showGrid": "Show Grid",
|
||||
"backgroundColor": "Background Color",
|
||||
"lightIntensity": "Light Intensity",
|
||||
@@ -2137,6 +2140,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",
|
||||
@@ -3531,20 +3571,6 @@
|
||||
"getHelpTooltip": "Report this error and we'll help you resolve it",
|
||||
"enterSubgraph": "Enter subgraph",
|
||||
"seeError": "See Error",
|
||||
"promptErrors": {
|
||||
"prompt_no_outputs": {
|
||||
"desc": "The workflow does not contain any output nodes (e.g. Save Image, Preview Image) to produce a result."
|
||||
},
|
||||
"no_prompt": {
|
||||
"desc": "The workflow data sent to the server is empty. This may be an unexpected system error."
|
||||
},
|
||||
"server_error_local": {
|
||||
"desc": "The server encountered an unexpected error. Please check the server logs."
|
||||
},
|
||||
"server_error_cloud": {
|
||||
"desc": "The server encountered an unexpected error. Please try again later."
|
||||
}
|
||||
},
|
||||
"errorHelp": "For more help, {github} or {support}",
|
||||
"errorHelpGithub": "submit a GitHub issue",
|
||||
"errorHelpSupport": "contact our support",
|
||||
@@ -3629,6 +3655,44 @@
|
||||
"swapNodes": "Some nodes can be replaced with alternatives",
|
||||
"missingMedia": "Some nodes are missing required inputs"
|
||||
},
|
||||
"errorCatalog": {
|
||||
"fallbacks": {
|
||||
"nodeName": "This node",
|
||||
"inputName": "unknown input"
|
||||
},
|
||||
"validationErrors": {
|
||||
"required_input_missing": {
|
||||
"title": "Missing connection",
|
||||
"message": "Required input slots have no connection feeding them.",
|
||||
"details": "{nodeName} is missing a required input: {inputName}",
|
||||
"itemLabel": "{nodeName} - {inputName}",
|
||||
"toastTitle": "Required input missing",
|
||||
"toastMessage": "{nodeName} is missing a required input: {inputName}"
|
||||
}
|
||||
},
|
||||
"runtimeErrors": {
|
||||
"execution_failed": {
|
||||
"itemLabel": "{nodeName}",
|
||||
"toastTitle": "{nodeName} failed",
|
||||
"toastMessageLocal": "This node threw an error during execution. Check its inputs or try a different configuration.",
|
||||
"toastMessageCloud": "This node threw an error during execution. Check its inputs or try a different configuration. No credits charged."
|
||||
}
|
||||
},
|
||||
"promptErrors": {
|
||||
"prompt_no_outputs": {
|
||||
"desc": "The workflow does not contain any output nodes (e.g. Save Image, Preview Image) to produce a result."
|
||||
},
|
||||
"no_prompt": {
|
||||
"desc": "The workflow data sent to the server is empty. This may be an unexpected system error."
|
||||
},
|
||||
"server_error_local": {
|
||||
"desc": "The server encountered an unexpected error. Please check the server logs."
|
||||
},
|
||||
"server_error_cloud": {
|
||||
"desc": "The server encountered an unexpected error. Please try again later."
|
||||
}
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"recentReleases": "Recent releases",
|
||||
"helpCenterMenu": "Help Center Menu"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1790,6 +1800,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyAndNode": {
|
||||
"display_name": "And",
|
||||
"description": "Logical AND operation. Returns true if all of the values are truthy. Uses Python's rules for truthiness.",
|
||||
"inputs": {
|
||||
"values": {
|
||||
"name": "values"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyMathExpression": {
|
||||
"display_name": "Math Expression",
|
||||
"inputs": {
|
||||
@@ -1813,6 +1837,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyNotNode": {
|
||||
"display_name": "Not",
|
||||
"description": "Logical NOT operation. Returns true if the value is falsy. Uses Python's rules for truthiness.",
|
||||
"inputs": {
|
||||
"value": {
|
||||
"name": "value"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyNumberConvert": {
|
||||
"display_name": "Convert Number",
|
||||
"inputs": {
|
||||
@@ -1829,6 +1867,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfyOrNode": {
|
||||
"display_name": "Or",
|
||||
"description": "Logical OR operation. Returns true if any of the values are truthy. Uses Python's rules for truthiness.",
|
||||
"inputs": {
|
||||
"values": {
|
||||
"name": "values"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ComfySwitchNode": {
|
||||
"display_name": "Switch",
|
||||
"inputs": {
|
||||
@@ -5700,6 +5752,7 @@
|
||||
},
|
||||
"ImageCropV2": {
|
||||
"display_name": "Crop Image",
|
||||
"description": "Crop an image to the specified dimensions.",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "image"
|
||||
@@ -5719,7 +5772,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 +5827,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 +8000,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 +8044,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": {
|
||||
@@ -8015,11 +8072,11 @@
|
||||
}
|
||||
},
|
||||
"LoadMediaPipeFaceLandmarker": {
|
||||
"display_name": "Load MediaPipe Face Landmarker",
|
||||
"display_name": "Load Face Detection Model (MediaPipe)",
|
||||
"inputs": {
|
||||
"model_name": {
|
||||
"name": "model_name",
|
||||
"tooltip": "Face Landmarker safetensors from models/mediapipe/."
|
||||
"tooltip": "Face detection model from models/detection/."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -8043,6 +8100,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",
|
||||
@@ -8431,7 +8489,7 @@
|
||||
}
|
||||
},
|
||||
"LTXVAudioVAELoader": {
|
||||
"display_name": "LTXV Audio VAE Loader",
|
||||
"display_name": "Load LTXV Audio VAE",
|
||||
"inputs": {
|
||||
"ckpt_name": {
|
||||
"name": "ckpt_name",
|
||||
@@ -9247,6 +9305,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",
|
||||
@@ -9337,10 +9396,11 @@
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceLandmarker": {
|
||||
"display_name": "MediaPipe Face Landmarker",
|
||||
"display_name": "Detect Face Landmarks (MediaPipe)",
|
||||
"description": "Detects facial landmarks using MediaPipe model.",
|
||||
"inputs": {
|
||||
"face_landmarker": {
|
||||
"name": "face_landmarker"
|
||||
"face_detection_model": {
|
||||
"name": "face_detection_model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
@@ -9374,7 +9434,8 @@
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMask": {
|
||||
"display_name": "MediaPipe Face Mask",
|
||||
"display_name": "Draw Face Mask (MediaPipe)",
|
||||
"description": "Draws a mask from face landmarks.",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
@@ -9391,7 +9452,8 @@
|
||||
}
|
||||
},
|
||||
"MediaPipeFaceMeshVisualize": {
|
||||
"display_name": "MediaPipe Face Mesh Visualize",
|
||||
"display_name": "Visualize Face Landmarks (MediaPipe)",
|
||||
"description": "Draws face landmarks mesh on the input image.",
|
||||
"inputs": {
|
||||
"face_landmarks": {
|
||||
"name": "face_landmarks"
|
||||
@@ -9423,7 +9485,8 @@
|
||||
}
|
||||
},
|
||||
"MergeImageLists": {
|
||||
"display_name": "Merge Image Lists",
|
||||
"display_name": "Merge Image Lists (DEPRECATED)",
|
||||
"description": "Concatenate multiple image lists into one.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -9438,7 +9501,7 @@
|
||||
}
|
||||
},
|
||||
"MergeTextLists": {
|
||||
"display_name": "Merge Text Lists",
|
||||
"display_name": "Merge Text Lists (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -12103,7 +12166,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"
|
||||
@@ -12139,7 +12203,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"
|
||||
@@ -12173,7 +12238,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"
|
||||
@@ -12202,7 +12268,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"
|
||||
@@ -12264,7 +12331,8 @@
|
||||
}
|
||||
},
|
||||
"NormalizeImages": {
|
||||
"display_name": "Normalize Images",
|
||||
"display_name": "Normalize Image Colors",
|
||||
"description": "Normalize images using mean and standard deviation.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
@@ -12593,6 +12661,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": {
|
||||
@@ -13378,7 +13479,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",
|
||||
@@ -14127,7 +14229,7 @@
|
||||
}
|
||||
},
|
||||
"ReplaceText": {
|
||||
"display_name": "Replace Text",
|
||||
"display_name": "Replace Text (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -14240,7 +14342,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",
|
||||
@@ -14248,7 +14351,7 @@
|
||||
},
|
||||
"longer_edge": {
|
||||
"name": "longer_edge",
|
||||
"tooltip": "Target length for the longer edge."
|
||||
"tooltip": "Target dimension for the longer edge."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14259,7 +14362,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",
|
||||
@@ -14267,7 +14371,7 @@
|
||||
},
|
||||
"shorter_edge": {
|
||||
"name": "shorter_edge",
|
||||
"tooltip": "Target length for the shorter edge."
|
||||
"tooltip": "Target dimension for the shorter edge."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -14279,6 +14383,7 @@
|
||||
},
|
||||
"ResolutionBucket": {
|
||||
"display_name": "Resolution Bucket",
|
||||
"description": "Group latents and conditionings into buckets",
|
||||
"inputs": {
|
||||
"latents": {
|
||||
"name": "latents",
|
||||
@@ -14510,6 +14615,164 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rodin3D_Gen25_Image": {
|
||||
"display_name": "Rodin 3D Gen-2.5 - Image to 3D",
|
||||
"description": "Generate a 3D model from 1-5 reference images via Rodin Gen-2.5. Pick a mode (Fast / Regular / Extreme-High) to tune quality vs. cost.",
|
||||
"inputs": {
|
||||
"images": {
|
||||
"name": "images",
|
||||
"tooltip": "1-5 images. The first image is used for materials when multi-view."
|
||||
},
|
||||
"mode": {
|
||||
"name": "mode",
|
||||
"tooltip": "Generation mode. Regular = balanced. Fast = 1K-20K faces for rapid prototyping. Extreme-High = 20K-2M faces with optional micro details."
|
||||
},
|
||||
"material": {
|
||||
"name": "material"
|
||||
},
|
||||
"geometry_file_format": {
|
||||
"name": "geometry_file_format"
|
||||
},
|
||||
"texture_mode": {
|
||||
"name": "texture_mode",
|
||||
"tooltip": "Texture quality preset. 'Default' uses the server's default for the selected tier."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed"
|
||||
},
|
||||
"TAPose": {
|
||||
"name": "TAPose",
|
||||
"tooltip": "T/A pose for human-like models."
|
||||
},
|
||||
"hd_texture": {
|
||||
"name": "hd_texture",
|
||||
"tooltip": "High-quality texture enhancement."
|
||||
},
|
||||
"texture_delight": {
|
||||
"name": "texture_delight",
|
||||
"tooltip": "Remove baked lighting from textures."
|
||||
},
|
||||
"use_original_alpha": {
|
||||
"name": "use_original_alpha",
|
||||
"tooltip": "Preserve image transparency."
|
||||
},
|
||||
"addon_highpack": {
|
||||
"name": "addon_highpack",
|
||||
"tooltip": "HighPack addon: 4K textures and ~16x faces in Quad mode."
|
||||
},
|
||||
"bbox_width": {
|
||||
"name": "bbox_width",
|
||||
"tooltip": "Bounding-box width (Y axis). Set to 0 with the others to skip bbox."
|
||||
},
|
||||
"bbox_height": {
|
||||
"name": "bbox_height",
|
||||
"tooltip": "Bounding-box height (Z axis)."
|
||||
},
|
||||
"bbox_length": {
|
||||
"name": "bbox_length",
|
||||
"tooltip": "Bounding-box length (X axis)."
|
||||
},
|
||||
"height_cm": {
|
||||
"name": "height_cm",
|
||||
"tooltip": "Approximate model height in centimeters (0 to skip)."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"mode_creative": {
|
||||
"name": "creative"
|
||||
},
|
||||
"mode_polygon_count": {
|
||||
"name": "polygon_count"
|
||||
},
|
||||
"mode_tier": {
|
||||
"name": "tier"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "model_file",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rodin3D_Gen25_Text": {
|
||||
"display_name": "Rodin 3D Gen-2.5 - Text to 3D",
|
||||
"description": "Generate a 3D model from a text prompt via Rodin Gen-2.5. Pick a mode (Fast / Regular / Extreme-High) to tune quality vs. cost.",
|
||||
"inputs": {
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Text prompt for the 3D model."
|
||||
},
|
||||
"mode": {
|
||||
"name": "mode",
|
||||
"tooltip": "Generation mode. Regular = balanced. Fast = 1K-20K faces for rapid prototyping. Extreme-High = 20K-2M faces with optional micro details."
|
||||
},
|
||||
"material": {
|
||||
"name": "material"
|
||||
},
|
||||
"geometry_file_format": {
|
||||
"name": "geometry_file_format"
|
||||
},
|
||||
"texture_mode": {
|
||||
"name": "texture_mode",
|
||||
"tooltip": "Texture quality preset. 'Default' uses the server's default for the selected tier."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed"
|
||||
},
|
||||
"TAPose": {
|
||||
"name": "TAPose",
|
||||
"tooltip": "T/A pose for human-like models."
|
||||
},
|
||||
"hd_texture": {
|
||||
"name": "hd_texture",
|
||||
"tooltip": "High-quality texture enhancement."
|
||||
},
|
||||
"texture_delight": {
|
||||
"name": "texture_delight",
|
||||
"tooltip": "Remove baked lighting from textures."
|
||||
},
|
||||
"addon_highpack": {
|
||||
"name": "addon_highpack",
|
||||
"tooltip": "HighPack addon: 4K textures and ~16x faces in Quad mode."
|
||||
},
|
||||
"bbox_width": {
|
||||
"name": "bbox_width",
|
||||
"tooltip": "Bounding-box width (Y axis). Set to 0 with the others to skip bbox."
|
||||
},
|
||||
"bbox_height": {
|
||||
"name": "bbox_height",
|
||||
"tooltip": "Bounding-box height (Z axis)."
|
||||
},
|
||||
"bbox_length": {
|
||||
"name": "bbox_length",
|
||||
"tooltip": "Bounding-box length (X axis)."
|
||||
},
|
||||
"height_cm": {
|
||||
"name": "height_cm",
|
||||
"tooltip": "Approximate model height in centimeters (0 to skip)."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"mode_creative": {
|
||||
"name": "creative"
|
||||
},
|
||||
"mode_polygon_count": {
|
||||
"name": "polygon_count"
|
||||
},
|
||||
"mode_tier": {
|
||||
"name": "tier"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "model_file",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"Rodin3D_Regular": {
|
||||
"display_name": "Rodin 3D Generate - Regular Generate",
|
||||
"description": "Generate 3D Assets using Rodin API",
|
||||
@@ -15392,7 +15655,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",
|
||||
@@ -15409,16 +15673,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)."
|
||||
@@ -15426,6 +15687,10 @@
|
||||
"filename_prefix": {
|
||||
"name": "filename_prefix",
|
||||
"tooltip": "Prefix for saved image filenames."
|
||||
},
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
"tooltip": "List of text captions to save."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -15480,6 +15745,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",
|
||||
@@ -15802,7 +16068,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",
|
||||
@@ -15824,7 +16091,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",
|
||||
@@ -16726,7 +16994,7 @@
|
||||
}
|
||||
},
|
||||
"StripWhitespace": {
|
||||
"display_name": "Strip Whitespace",
|
||||
"display_name": "Strip Whitespace (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -17493,7 +17761,7 @@
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "Text to Lowercase",
|
||||
"display_name": "Convert Text to Lowercase (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -17508,7 +17776,7 @@
|
||||
}
|
||||
},
|
||||
"TextToUppercase": {
|
||||
"display_name": "Text to Uppercase",
|
||||
"display_name": "Convert Text to Uppercase (DEPRECATED)",
|
||||
"inputs": {
|
||||
"texts": {
|
||||
"name": "texts",
|
||||
@@ -19449,6 +19717,7 @@
|
||||
},
|
||||
"VoxelToMesh": {
|
||||
"display_name": "Voxel to Mesh",
|
||||
"description": "Converts a voxel grid to a mesh.",
|
||||
"inputs": {
|
||||
"voxel": {
|
||||
"name": "voxel"
|
||||
@@ -19467,7 +19736,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"
|
||||
|
||||