Compare commits

..

3 Commits

Author SHA1 Message Date
Hunter Senft-Grupp
5bbd5103cd fix: default frontend preview variant to cpu
Frontend previews don't need GPU resources. Default to cpu variant
and only use gpu when the preview-gpu label is explicitly added.
2026-03-10 14:53:54 -04:00
Hunter Senft-Grupp
a3d47fed8c test: change title to Preview v2 for update timing 2026-03-10 13:58:14 -04:00
Hunter Senft-Grupp
eda40b02c4 test: change page title for preview env smoke test 2026-03-10 13:36:25 -04:00
213 changed files with 822 additions and 12246 deletions

View File

@@ -1,150 +0,0 @@
---
name: backport-management
description: Manages cherry-pick backports across stable release branches. Discovers candidates from Slack/git, analyzes dependencies, resolves conflicts via worktree, and logs results. Use when asked to backport, cherry-pick to stable, manage release branches, do stable branch maintenance, or run a backport session.
---
# Backport Management
Cherry-pick backport management for Comfy-Org/ComfyUI_frontend stable release branches.
## Quick Start
1. **Discover** — Collect candidates from Slack bot + git log gap (`reference/discovery.md`)
2. **Analyze** — Categorize MUST/SHOULD/SKIP, check deps (`reference/analysis.md`)
3. **Plan** — Order by dependency (leaf fixes first), group into waves per branch
4. **Execute** — Label-driven automation → worktree fallback for conflicts (`reference/execution.md`)
5. **Verify** — After each wave, verify branch integrity before proceeding
6. **Log & Report** — Generate session report with mermaid diagram (`reference/logging.md`)
## System Context
| Item | Value |
| -------------- | ------------------------------------------------- |
| Repo | `~/ComfyUI_frontend` (Comfy-Org/ComfyUI_frontend) |
| Merge strategy | Squash merge (`gh pr merge --squash --admin`) |
| Automation | `pr-backport.yaml` GitHub Action (label-driven) |
| Tracking dir | `~/temp/backport-session/` |
## Branch Scope Rules
**Critical: Match PRs to the correct target branches.**
| Branch prefix | Scope | Example |
| ------------- | ------------------------------ | ----------------------------------------- |
| `cloud/*` | Cloud-hosted ComfyUI only | App mode, cloud auth, cloud-specific UI |
| `core/*` | Local/self-hosted ComfyUI only | Core editor, local workflows, node system |
**⚠️ NEVER backport cloud-only PRs to `core/*` branches.** Cloud-only changes (app mode, cloud auth, cloud billing UI, cloud-specific API calls) are irrelevant to local users and waste effort. Before backporting any PR to a `core/*` branch, check:
- Does the PR title/description mention "app mode", "cloud", or cloud-specific features?
- Does the PR only touch files like `appModeStore.ts`, cloud auth, or cloud-specific components?
- If yes → skip for `core/*` branches (may still apply to `cloud/*` branches)
## ⚠️ Gotchas (Learn from Past Sessions)
### Use `gh api` for Labels — NOT `gh pr edit`
`gh pr edit --add-label` triggers Projects Classic deprecation errors. Always use:
```bash
gh api repos/Comfy-Org/ComfyUI_frontend/issues/$PR/labels \
-f "labels[]=needs-backport" -f "labels[]=TARGET_BRANCH"
```
### Automation Over-Reports Conflicts
The `pr-backport.yaml` action reports more conflicts than reality. `git cherry-pick -m 1` with git auto-merge handles many cases the automation can't. Always attempt manual cherry-pick before skipping.
### Never Skip Based on Conflict File Count
12 or 27 conflicting files can be trivial (snapshots, new files). **Categorize conflicts first**, then decide. See Conflict Triage below.
## Conflict Triage
**Always categorize before deciding to skip. High conflict count ≠ hard conflicts.**
| Type | Symptom | Resolution |
| ---------------------------- | ------------------------------------ | --------------------------------------------------------------- |
| **Binary snapshots (PNGs)** | `.png` files in conflict list | `git checkout --theirs $FILE && git add $FILE` — always trivial |
| **Modify/delete (new file)** | PR introduces files not on target | `git add $FILE` — keep the new file |
| **Modify/delete (removed)** | Target removed files the PR modifies | `git rm $FILE` — file no longer relevant |
| **Content conflicts** | Marker-based (`<<<<<<<`) | Accept theirs via python regex (see below) |
| **Add/add** | Both sides added same file | Accept theirs, verify no logic conflict |
| **Locale/JSON files** | i18n key additions | Accept theirs, validate JSON after |
```python
# Accept theirs for content conflicts
import re
pattern = r'<<<<<<< HEAD\n(.*?)=======\n(.*?)>>>>>>> [^\n]+\n?'
content = re.sub(pattern, r'\2', content, flags=re.DOTALL)
```
### Escalation Triggers (Flag for Human)
- **Package.json/lockfile changes** → skip on stable (transitive dep regression risk)
- **Core type definition changes** → requires human judgment
- **Business logic conflicts** (not just imports/exports) → requires domain knowledge
- **Admin-merged conflict resolutions** → get human review of the resolution before continuing the wave
## Auto-Skip Categories
Skip these without discussion:
- **Dep refresh PRs** — Risk of transitive dep regressions on stable. Cherry-pick individual CVE fixes instead.
- **CI/tooling changes** — Not user-facing
- **Test-only / lint rule changes** — Not user-facing
- **Revert pairs** — If PR A reverted by PR B, skip both. If fixed version (PR C) exists, backport only C.
- **Features not on target branch** — e.g., Painter, GLSLShader, appModeStore on core/1.40
- **Cloud-only PRs on core/\* branches** — App mode, cloud auth, cloud billing. These only affect cloud-hosted ComfyUI.
## Wave Verification
After merging each wave of PRs to a target branch, verify branch integrity before proceeding:
```bash
# Fetch latest state of target branch
git fetch origin TARGET_BRANCH
# Quick smoke check: does the branch build?
git worktree add /tmp/verify-TARGET origin/TARGET_BRANCH
cd /tmp/verify-TARGET
source ~/.nvm/nvm.sh && nvm use 24 && pnpm install && pnpm typecheck
git worktree remove /tmp/verify-TARGET --force
```
If typecheck fails, stop and investigate before continuing. A broken branch after wave N means all subsequent waves will compound the problem.
## Continuous Backporting Recommendation
Large backport sessions (50+ PRs) are expensive and error-prone. Prefer continuous backporting:
- Backport bug fixes as they merge to main (same day or next day)
- Use the automation labels immediately after merge
- Reserve session-style bulk backporting for catching up after gaps
- When a release branch is created, immediately start the continuous process
## Quick Reference
### Label-Driven Automation (default path)
```bash
gh api repos/Comfy-Org/ComfyUI_frontend/issues/$PR/labels \
-f "labels[]=needs-backport" -f "labels[]=TARGET_BRANCH"
# Wait 3 min, check: gh pr list --base TARGET_BRANCH --state open
```
### Manual Worktree Cherry-Pick (conflict fallback)
```bash
git worktree add /tmp/backport-$BRANCH origin/$BRANCH
cd /tmp/backport-$BRANCH
git checkout -b backport-$PR-to-$BRANCH origin/$BRANCH
git cherry-pick -m 1 $MERGE_SHA
# Resolve conflicts, push, create PR, merge
```
### PR Title Convention
```
[backport TARGET_BRANCH] Original Title (#ORIGINAL_PR)
```

View File

@@ -1,68 +0,0 @@
# Analysis & Decision Framework
## Categorization
| Category | Criteria | Action |
| -------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| **MUST** | User-facing bug, crash, data corruption, security. Clear breakage that users will hit. | Backport (with deps if needed) |
| **SHOULD** | UX improvement, minor bug, small dep chain. No user-visible breakage if skipped, but improves experience. | Backport if clean cherry-pick; defer if conflict resolution is non-trivial |
| **SKIP** | CI/tooling, test-only, lint rules, cosmetic, dep refresh | Skip with documented reason |
| **NEEDS DISCUSSION** | Large dep chain, unclear risk/benefit, touches core types | Flag for human |
### MUST vs SHOULD Decision Guide
When unsure, ask: "If a user on this stable branch reports this issue, would we consider it a bug?"
- **Yes** → MUST. The fix addresses broken behavior.
- **No, but it's noticeably better** → SHOULD. The fix is a quality-of-life improvement.
- **No, and it's cosmetic or internal** → SKIP.
For SHOULD items with conflicts: if conflict resolution requires more than trivial accept-theirs patterns (content conflicts in business logic, not just imports), downgrade to SKIP or escalate to NEEDS DISCUSSION.
## Branch Scope Filtering
**Before categorizing, filter by branch scope:**
| Target branch | Skip if PR is... |
| ------------- | ------------------------------------------------------------------- |
| `core/*` | Cloud-only (app mode, cloud auth, cloud billing, cloud-specific UI) |
| `cloud/*` | Local-only features not present on cloud branch |
Cloud-only PRs backported to `core/*` are wasted effort — `core/*` branches serve local/self-hosted users who never see cloud features. Check PR titles, descriptions, and files changed for cloud-specific indicators.
## Features Not on Stable Branches
Check before backporting — these don't exist on older branches:
- **Painter** (`src/extensions/core/painter.ts`) — not on core/1.40
- **GLSLShader** — not on core/1.40
- **App builder** — check per branch
- **appModeStore.ts** — not on core/1.40
## Dep Refresh PRs
Always SKIP on stable branches. Risk of transitive dependency regressions outweighs audit cleanup benefit. If a specific CVE fix is needed, cherry-pick that individual fix instead.
## Revert Pairs
If PR A is reverted by PR B:
- Skip BOTH A and B
- If a fixed version exists (PR C), backport only C
## Dependency Analysis
```bash
# Find other PRs that touched the same files
gh pr view $PR --json files --jq '.files[].path' | while read f; do
git log --oneline origin/TARGET..$MERGE_SHA -- "$f"
done
```
## Human Review Checkpoint
Present decisions.md before execution. Include:
1. All MUST/SHOULD/SKIP categorizations with rationale
2. Questions for human (feature existence, scope, deps)
3. Estimated effort per branch

View File

@@ -1,42 +0,0 @@
# Discovery — Candidate Collection
## Source 1: Slack Backport-Checker Bot
Use `slackdump` skill to export `#frontend-releases` channel (C09K9TPU2G7):
```bash
slackdump export -o ~/slack-exports/frontend-releases.zip C09K9TPU2G7
```
Parse bot messages for PRs flagged "Might need backport" per release version.
## Source 2: Git Log Gap Analysis
```bash
# Count gap
git log --oneline origin/TARGET..origin/main | wc -l
# List gap commits
git log --oneline origin/TARGET..origin/main
# Check if a PR is already on target
git log --oneline origin/TARGET --grep="#PR_NUMBER"
# Check for existing backport PRs
gh pr list --base TARGET --state all --search "backport PR_NUMBER"
```
## Source 3: GitHub PR Details
```bash
# Get merge commit SHA
gh pr view $PR --json mergeCommit,title --jq '"Title: \(.title)\nMerge: \(.mergeCommit.oid)"'
# Get files changed
gh pr view $PR --json files --jq '.files[].path'
```
## Output: candidate_list.md
Table per target branch:
| PR# | Title | Category | Flagged by Bot? | Decision |

View File

@@ -1,150 +0,0 @@
# Execution Workflow
## Per-Branch Execution Order
1. Smallest gap first (validation run)
2. Medium gap next (quick win)
3. Largest gap last (main effort)
## Step 1: Label-Driven Automation (Batch)
```bash
# Add labels to all candidates for a target branch
for pr in $PR_LIST; do
gh api repos/Comfy-Org/ComfyUI_frontend/issues/$pr/labels \
-f "labels[]=needs-backport" -f "labels[]=TARGET_BRANCH" --silent
sleep 2
done
# Wait 3 minutes for automation
sleep 180
# Check which got auto-PRs
gh pr list --base TARGET_BRANCH --state open --limit 50 --json number,title
```
## Step 2: Review & Merge Clean Auto-PRs
```bash
for pr in $AUTO_PRS; do
# Check size
gh pr view $pr --json title,additions,deletions,changedFiles \
--jq '"Files: \(.changedFiles), +\(.additions)/-\(.deletions)"'
# Admin merge
gh pr merge $pr --squash --admin
sleep 3
done
```
## Step 3: Manual Worktree for Conflicts
```bash
git fetch origin TARGET_BRANCH
git worktree add /tmp/backport-TARGET origin/TARGET_BRANCH
cd /tmp/backport-TARGET
for PR in ${CONFLICT_PRS[@]}; do
# Refresh target ref so each branch is based on current HEAD
git fetch origin TARGET_BRANCH
git checkout origin/TARGET_BRANCH
git checkout -b backport-$PR-to-TARGET origin/TARGET_BRANCH
git cherry-pick -m 1 $MERGE_SHA
# If conflict — NEVER skip based on file count alone!
# Categorize conflicts first: binary PNGs, modify/delete, content, add/add
# See SKILL.md Conflict Triage table for resolution per type.
# Resolve all conflicts, then:
git add .
GIT_EDITOR=true git cherry-pick --continue
git push origin backport-$PR-to-TARGET
NEW_PR=$(gh pr create --base TARGET_BRANCH --head backport-$PR-to-TARGET \
--title "[backport TARGET] TITLE (#$PR)" \
--body "Backport of #$PR..." | grep -oP '\d+$')
gh pr merge $NEW_PR --squash --admin
sleep 3
done
# Cleanup
cd -
git worktree remove /tmp/backport-TARGET --force
```
**⚠️ Human review for conflict resolutions:** When admin-merging a PR where you manually resolved conflicts (especially content conflicts beyond trivial accept-theirs), pause and present the resolution diff to the human for review before merging. Trivial resolutions (binary snapshots, modify/delete, locale key additions) can proceed without review.
## Step 4: Wave Verification
After completing all PRs in a wave for a target branch:
```bash
git fetch origin TARGET_BRANCH
git worktree add /tmp/verify-TARGET origin/TARGET_BRANCH
cd /tmp/verify-TARGET
source ~/.nvm/nvm.sh && nvm use 24 && pnpm install && pnpm typecheck
git worktree remove /tmp/verify-TARGET --force
```
If verification fails, stop and fix before proceeding to the next wave. Do not compound problems across waves.
## Conflict Resolution Patterns
### 1. Content Conflicts (accept theirs)
```python
import re
pattern = r'<<<<<<< HEAD\n(.*?)=======\n(.*?)>>>>>>> [^\n]+\n?'
content = re.sub(pattern, r'\2', content, flags=re.DOTALL)
```
### 2. Modify/Delete (two cases!)
```bash
# Case A: PR introduces NEW files not on target → keep them
git add $FILE
# Case B: Target REMOVED files the PR modifies → drop them
git rm $FILE
```
### 3. Binary Files (snapshots)
```bash
git checkout --theirs $FILE && git add $FILE
```
### 4. Locale Files
Usually adding new i18n keys — accept theirs, validate JSON:
```bash
python3 -c "import json; json.load(open('src/locales/en/main.json'))" && echo "Valid"
```
## Merge Conflicts After Other Merges
When merging multiple PRs to the same branch, later PRs may conflict with earlier merges:
```bash
git fetch origin TARGET_BRANCH
git rebase origin/TARGET_BRANCH
# Resolve new conflicts
git push --force origin backport-$PR-to-TARGET
sleep 20 # Wait for GitHub to recompute merge state
gh pr merge $PR --squash --admin
```
## Lessons Learned
1. **Automation reports more conflicts than reality**`cherry-pick -m 1` with git auto-merge handles many "conflicts" the automation can't
2. **Never skip based on conflict file count** — 12 or 27 conflicts can be trivial (snapshots, new files). Categorize first: binary PNGs, modify/delete, content, add/add.
3. **Modify/delete goes BOTH ways** — if the PR introduces new files (not on target), `git add` them. If target deleted files the PR modifies, `git rm`.
4. **Binary snapshot PNGs** — always `git checkout --theirs && git add`. Never skip a PR just because it has many snapshot conflicts.
5. **Batch label additions need 2s delay** between API calls to avoid rate limits
6. **Merging 6+ PRs rapidly** can cause later PRs to become unmergeable — wait 20-30s for GitHub to recompute merge state
7. **appModeStore.ts, painter files, GLSLShader files** don't exist on core/1.40 — `git rm` these
8. **Always validate JSON** after resolving locale file conflicts
9. **Dep refresh PRs** — skip on stable branches. Risk of transitive dep regressions outweighs audit cleanup. Cherry-pick individual CVE fixes instead.
10. **Verify after each wave** — run `pnpm typecheck` on the target branch after merging a batch. Catching breakage early prevents compounding errors.
11. **Cloud-only PRs don't belong on core/\* branches** — app mode, cloud auth, and cloud-specific UI changes are irrelevant to local users. Always check PR scope against branch scope before backporting.

View File

@@ -1,96 +0,0 @@
# Logging & Session Reports
## During Execution
Maintain `execution-log.md` with per-branch tables:
```markdown
| PR# | Title | Status | Backport PR | Notes |
| ----- | ----- | --------------------------------- | ----------- | ------- |
| #XXXX | Title | ✅ Merged / ⏭️ Skip / ⏸️ Deferred | #YYYY | Details |
```
## Wave Verification Log
Track verification results per wave:
```markdown
## Wave N Verification — TARGET_BRANCH
- PRs merged: #A, #B, #C
- Typecheck: ✅ Pass / ❌ Fail
- Issues found: (if any)
- Human review needed: (list any non-trivial conflict resolutions)
```
## Session Report Template
```markdown
# Backport Session Report
## Summary
| Branch | Candidates | Merged | Skipped | Deferred | Rate |
| ------ | ---------- | ------ | ------- | -------- | ---- |
## Deferred Items (Needs Human)
| PR# | Title | Branch | Issue |
## Conflict Resolutions Requiring Review
| PR# | Branch | Conflict Type | Resolution Summary |
## Automation Performance
| Metric | Value |
| --------------------------- | ----- |
| Auto success rate | X% |
| Manual resolution rate | X% |
| Overall clean rate | X% |
| Wave verification pass rate | X% |
## Process Recommendations
- Were there clusters of related PRs that should have been backported together?
- Any PRs that should have been backported sooner (continuous backporting candidates)?
- Feature branches that need tracking for future sessions?
```
## Final Deliverable: Visual Summary
At session end, generate a **mermaid diagram** showing all backported PRs organized by target branch and category (MUST/SHOULD), plus a summary table. Present this to the user as the final output.
```mermaid
graph TD
subgraph branch1["☁️ cloud/X.XX — N PRs"]
C1["#XXXX title"]
C2["#XXXX title"]
end
subgraph branch2must["🔴 core/X.XX MUST — N PRs"]
M1["#XXXX title"]
end
subgraph branch2should["🟡 core/X.XX SHOULD — N PRs"]
S1["#XXXX-#XXXX N auto-merged"]
S2["#XXXX-#XXXX N manual picks"]
end
classDef cloudStyle fill:#1a3a5c,stroke:#4da6ff,color:#e0f0ff
classDef coreStyle fill:#1a4a2e,stroke:#4dff88,color:#e0ffe8
classDef mustStyle fill:#5c1a1a,stroke:#ff4d4d,color:#ffe0e0
classDef shouldStyle fill:#4a3a1a,stroke:#ffcc4d,color:#fff5e0
```
Use the `mermaid` tool to render this diagram and present it alongside the summary table as the session's final deliverable.
## Files to Track
- `candidate_list.md` — all candidates per branch
- `decisions.md` — MUST/SHOULD/SKIP with rationale
- `wave-plan.md` — execution order
- `execution-log.md` — real-time status
- `backport-session-report.md` — final summary
All in `~/temp/backport-session/`.

View File

@@ -38,9 +38,6 @@ TEST_COMFYUI_DIR=/home/ComfyUI
ALGOLIA_APP_ID=4E0RO38HS8
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
# Enable PostHog debug logging in the browser console.
# VITE_POSTHOG_DEBUG=true
# Sentry ENV vars replace with real ones for debugging
# SENTRY_AUTH_TOKEN=private-token # get from sentry
# SENTRY_ORG=comfy-org

View File

@@ -4,8 +4,6 @@ name: 'CI: Tests Storybook'
on:
workflow_dispatch: # Allow manual triggering
pull_request:
push:
branches: [main]
jobs:
# Post starting comment for non-forked PRs
@@ -140,29 +138,6 @@ jobs:
"${{ github.head_ref }}" \
"completed"
# Deploy Storybook to production URL on main branch push
deploy-production:
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook
run: pnpm build-storybook
- name: Deploy to Cloudflare Pages (production)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
npx wrangler@^4.0.0 pages deploy storybook-static \
--project-name=comfy-storybook \
--branch=main
# Update comment with Chromatic URLs for version-bump branches
update-comment-with-chromatic:
needs: [chromatic-deployment, deploy-and-comment]

View File

@@ -55,9 +55,14 @@ jobs:
REF="${PR_HEAD_SHA}"
BRANCH="${PR_HEAD_REF}"
# Derive variant from all PR labels (default to cpu for frontend-only previews)
# Derive variant from labels (default to cpu for frontend-only previews)
VARIANT="cpu"
echo "${PR_LABELS}" | grep -q '"preview-gpu"' && VARIANT="gpu"
if [ "${ACTION}" = "labeled" ]; then
[ "${LABEL_NAME}" = "preview-gpu" ] && VARIANT="gpu"
else
# synchronize — check existing labels on the PR
echo "${PR_LABELS}" | grep -q '"preview-gpu"' && VARIANT="gpu"
fi
else
REF="${GITHUB_SHA}"
BRANCH="${GITHUB_REF_NAME}"

1
.gitignore vendored
View File

@@ -26,7 +26,6 @@ dist-ssr
.claude/*.local.json
.claude/*.local.md
.claude/*.local.txt
.claude/worktrees
CLAUDE.local.md
# Editor directories and files

View File

@@ -58,7 +58,7 @@ export const withTheme = (Story: StoryFn, context: StoryContext) => {
document.documentElement.classList.remove('dark-theme')
document.body.classList.remove('dark-theme')
}
document.body.classList.add('font-inter')
document.body.classList.add('[&_*]:!font-inter')
return Story(context.args, context)
}

View File

@@ -1,47 +0,0 @@
{
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [50, 50],
"size": [315, 314],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{ "name": "IMAGE", "type": "IMAGE", "links": null },
{ "name": "MASK", "type": "MASK", "links": null }
],
"properties": { "Node name for S&R": "LoadImage" },
"widgets_values": ["example.png", "image"]
},
{
"id": 2,
"type": "KSampler",
"pos": [500, 50],
"size": [315, 262],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{ "name": "model", "type": "MODEL", "link": null },
{ "name": "positive", "type": "CONDITIONING", "link": null },
{ "name": "negative", "type": "CONDITIONING", "link": null },
{ "name": "latent_image", "type": "LATENT", "link": null }
],
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": null }],
"properties": { "Node name for S&R": "KSampler" },
"widgets_values": [0, "randomize", 20, 8, "euler", "normal", 1]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": { "offset": [0, 0], "scale": 1 }
},
"version": 0.4
}

View File

@@ -333,7 +333,7 @@ test.describe('Settings', () => {
await editKeybindingButton.click()
// Set new keybinding
const input = comfyPage.page.getByPlaceholder('Enter your keybind')
const input = comfyPage.page.getByPlaceholder('Press keys for new binding')
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
@@ -345,7 +345,7 @@ test.describe('Settings', () => {
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('Modify keybinding')
.getByLabel('New Blank Workflow')
.getByText('Save')
await saveButton.click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -1,58 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe(
'Image paste priority over stale node metadata',
{ tag: ['@node'] },
() => {
test('Should not paste copied node when a LoadImage node is selected and clipboard has stale node metadata', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('nodes/load_image_with_ksampler')
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(initialCount).toBe(2)
// Copy the KSampler node (puts data-metadata in clipboard)
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await ksamplerNodes[0].copy()
// Select the LoadImage node
const loadImageNodes =
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
await loadImageNodes[0].click('title')
// Simulate pasting when clipboard has stale node metadata (text/html
// with data-metadata) but no image file items. This replicates the bug
// scenario: user copied a node, then copied a web image (which replaces
// clipboard files but may leave stale text/html with node metadata).
await comfyPage.page.evaluate(() => {
const nodeData = { nodes: [{ type: 'KSampler', id: 99 }] }
const base64 = btoa(JSON.stringify(nodeData))
const html =
'<meta charset="utf-8"><div><span data-metadata="' +
base64 +
'"></span></div><span style="white-space:pre-wrap;">Text</span>'
const dataTransfer = new DataTransfer()
dataTransfer.setData('text/html', html)
const event = new ClipboardEvent('paste', {
clipboardData: dataTransfer,
bubbles: true,
cancelable: true
})
document.dispatchEvent(event)
})
await comfyPage.nextFrame()
// Node count should remain the same — stale node metadata should NOT
// be deserialized when a media node is selected.
const finalCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(finalCount).toBe(initialCount)
})
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -1,91 +0,0 @@
# Change Tracker (Undo/Redo System)
The `ChangeTracker` class (`src/scripts/changeTracker.ts`) manages undo/redo
history by comparing serialized graph snapshots.
## How It Works
`checkState()` is the core method. It:
1. Serializes the current graph via `app.rootGraph.serialize()`
2. Deep-compares the result against the last known `activeState`
3. If different, pushes `activeState` onto `undoQueue` and replaces it
**It is not reactive.** Changes to the graph (widget values, node positions,
links, etc.) are only captured when `checkState()` is explicitly triggered.
## Automatic Triggers
These are set up once in `ChangeTracker.init()`:
| Trigger | Event / Hook | What It Catches |
| ----------------------------------- | -------------------------------------------------- | --------------------------------------------------- |
| Keyboard (non-modifier, non-repeat) | `window` `keydown` | Shortcuts, typing in canvas |
| Modifier key release | `window` `keyup` | Releasing Ctrl/Shift/Alt/Meta |
| Mouse click | `window` `mouseup` | General clicks on native DOM |
| Canvas mouse up | `LGraphCanvas.processMouseUp` override | LiteGraph canvas interactions |
| Number/string dialog | `LGraphCanvas.prompt` override | Dialog popups for editing widgets |
| Context menu close | `LiteGraph.ContextMenu.close` override | COMBO widget menus in LiteGraph |
| Active input element | `bindInput` (change/input/blur on focused element) | Native HTML input edits |
| Prompt queued | `api` `promptQueued` event | Dynamic widget changes on queue |
| Graph cleared | `api` `graphCleared` event | Full graph clear |
| Transaction end | `litegraph:canvas` `after-change` event | Batched operations via `beforeChange`/`afterChange` |
## When You Must Call `checkState()` Manually
The automatic triggers above are designed around LiteGraph's native DOM
rendering. They **do not cover**:
- **Vue-rendered widgets** — Vue handles events internally without triggering
native DOM events that the tracker listens to (e.g., `mouseup` on a Vue
dropdown doesn't bubble the same way as a native LiteGraph widget click)
- **Programmatic graph mutations** — Any code that modifies the graph outside
of user interaction (e.g., applying a template, pasting nodes, aligning)
- **Async operations** — File uploads, API calls that change widget values
after the initial user gesture
### Pattern for Manual Calls
```typescript
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
// After mutating the graph:
useWorkflowStore().activeWorkflow?.changeTracker?.checkState()
```
### Existing Manual Call Sites
These locations already call `checkState()` explicitly:
- `WidgetSelectDropdown.vue` — After dropdown selection and file upload
- `ColorPickerButton.vue` — After changing node colors
- `NodeSearchBoxPopover.vue` — After adding a node from search
- `useAppSetDefaultView.ts` — After setting default view
- `useSelectionOperations.ts` — After align, copy, paste, duplicate, group
- `useSelectedNodeActions.ts` — After pin, bypass, collapse
- `useGroupMenuOptions.ts` — After group operations
- `useSubgraphOperations.ts` — After subgraph enter/exit
- `useCanvasRefresh.ts` — After canvas refresh
- `useCoreCommands.ts` — After metadata/subgraph commands
- `workflowService.ts` — After workflow service operations
## Transaction Guards
For operations that make multiple changes that should be a single undo entry:
```typescript
changeTracker.beforeChange()
// ... multiple graph mutations ...
changeTracker.afterChange() // calls checkState() when nesting count hits 0
```
The `litegraph:canvas` custom event also supports this with `before-change` /
`after-change` sub-types.
## Key Invariants
- `checkState()` is a no-op during `loadGraphData` (guarded by
`isLoadingGraph`) to prevent cross-workflow corruption
- `checkState()` is a no-op when `changeCount > 0` (inside a transaction)
- `undoQueue` is capped at 50 entries (`MAX_HISTORY`)
- `graphEqual` ignores node order and `ds` (pan/zoom) when comparing

1
global.d.ts vendored
View File

@@ -35,7 +35,6 @@ interface Window {
mixpanel_token?: string
posthog_project_token?: string
posthog_api_host?: string
posthog_config?: Record<string, unknown>
require_whitelist?: boolean
subscription_required?: boolean
max_upload_size?: number

File diff suppressed because one or more lines are too long

View File

@@ -20,10 +20,6 @@ const config: KnipConfig = {
'packages/tailwind-utils': {
project: ['src/**/*.{js,ts}']
},
'packages/shared-frontend-utils': {
project: ['src/**/*.{js,ts}'],
entry: ['src/formatUtil.ts', 'src/networkUtil.ts']
},
'packages/registry-types': {
project: ['src/**/*.{js,ts}']
}
@@ -36,9 +32,7 @@ const config: KnipConfig = {
'@primeuix/forms',
'@primeuix/styled',
'@primeuix/utils',
'@primevue/icons',
// Used by lucideStrokePlugin.js (CSS @plugin)
'@iconify/utils'
'@primevue/icons'
],
ignore: [
// Auto generated manager types
@@ -53,9 +47,7 @@ const config: KnipConfig = {
// Pending integration in stacked PR
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
// Agent review check config, not part of the build
'.agents/checks/eslint.strict.config.js',
// Loaded via @plugin directive in CSS, not detected by knip
'packages/design-system/src/css/lucideStrokePlugin.js'
'.agents/checks/eslint.strict.config.js'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.42.3",
"version": "1.42.2",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -1,51 +0,0 @@
/* Pre-Vue splash loader — colors set by inline script */
#splash-loader {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
contain: strict;
}
#splash-loader svg {
width: min(200px, 50vw);
height: auto;
transform: translateZ(0);
}
#splash-loader .wave-group {
animation: splash-rise 4s ease-in-out infinite alternate;
will-change: transform;
transform: translateZ(0);
}
#splash-loader .wave-path {
animation: splash-wave 1.2s linear infinite;
will-change: transform;
transform: translateZ(0);
}
@keyframes splash-rise {
from {
transform: translateY(280px);
}
to {
transform: translateY(-80px);
}
}
@keyframes splash-wave {
from {
transform: translateX(0);
}
to {
transform: translateX(-880px);
}
}
@media (prefers-reduced-motion: reduce) {
#splash-loader .wave-group,
#splash-loader .wave-path {
animation: none;
}
#splash-loader .wave-group {
transform: translateY(-80px);
}
}

View File

@@ -2,13 +2,20 @@
<router-view />
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
<div
v-if="isLoading"
class="pointer-events-none fixed inset-0 z-1200 flex items-center justify-center"
>
<LogoComfyWaveLoader size="xl" color="yellow" />
</div>
</template>
<script setup lang="ts">
import { captureException } from '@sentry/vue'
import BlockUI from 'primevue/blockui'
import { computed, onMounted, watch } from 'vue'
import { computed, onMounted } from 'vue'
import LogoComfyWaveLoader from '@/components/loader/LogoComfyWaveLoader.vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -24,16 +31,6 @@ app.extensionManager = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
watch(
isLoading,
(loading, prevLoading) => {
if (prevLoading && !loading) {
document.getElementById('splash-loader')?.remove()
}
},
{ flush: 'post' }
)
const showContextMenu = (event: MouseEvent) => {
const { target } = event
switch (true) {

View File

@@ -25,13 +25,15 @@
class: {
'p-3 rounded-lg': true,
'pointer-events-none':
bottomPanelStore.bottomPanelTabs.length === 1,
'bg-secondary-background text-secondary-foreground':
x.context.active &&
bottomPanelStore.bottomPanelTabs.length > 1,
'text-muted-foreground':
bottomPanelStore.bottomPanelTabs.length === 1
},
style: {
color: 'var(--fg-color)',
backgroundColor:
!x.context.active ||
bottomPanelStore.bottomPanelTabs.length <= 1
bottomPanelStore.bottomPanelTabs.length === 1
? ''
: 'var(--bg-color)'
}
})
"
@@ -125,8 +127,4 @@ const closeBottomPanel = () => {
:deep(.p-tablist-active-bar) {
display: none;
}
:deep(.p-tab-active) {
color: inherit;
}
</style>

View File

@@ -1,7 +1,6 @@
import { computed } from 'vue'
import { useAppMode } from '@/composables/useAppMode'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
@@ -43,9 +42,6 @@ export function useAppSetDefaultView() {
const extra = (app.rootGraph.extra ??= {})
extra.linearMode = openAsApp
workflow.changeTracker?.checkState()
useTelemetry()?.trackDefaultViewSet({
default_view: openAsApp ? 'app' : 'graph'
})
closeDialog()
showAppliedDialog(openAsApp)
}
@@ -58,7 +54,6 @@ export function useAppSetDefaultView() {
appliedAsApp,
onViewApp: () => {
closeAppliedDialog()
useTelemetry()?.trackEnterLinear({ source: 'app_builder' })
setMode('app')
},
onExitToWorkflow: () => {

View File

@@ -1,160 +0,0 @@
import type {
ComponentPropsAndSlots,
Meta,
StoryObj
} from '@storybook/vue3-vite'
import { ref, toRefs } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import Popover from '@/components/ui/Popover.vue'
import ScrubableNumberInput from './ScrubableNumberInput.vue'
type StoryArgs = ComponentPropsAndSlots<typeof ScrubableNumberInput>
const meta: Meta<StoryArgs> = {
title: 'Components/Input/Number',
component: ScrubableNumberInput,
tags: ['autodocs'],
parameters: { layout: 'centered' },
argTypes: {
min: { control: 'number' },
max: { control: 'number' },
step: { control: 'number' },
disabled: { control: 'boolean' },
hideButtons: { control: 'boolean' }
},
args: {
min: 0,
max: 100,
step: 1,
disabled: false,
hideButtons: false
},
decorators: [
(story) => ({
components: { story },
template: '<div class="w-60"><story /></div>'
})
]
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: (args) => ({
components: { ScrubableNumberInput },
setup() {
const { min, max, step, disabled, hideButtons } = toRefs(args)
const value = ref(42)
return { value, min, max, step, disabled, hideButtons }
},
template:
'<ScrubableNumberInput v-model="value" :min :max :step :disabled :hideButtons />'
})
}
export const Disabled: Story = {
args: { disabled: true },
render: (args) => ({
components: { ScrubableNumberInput },
setup() {
const { disabled } = toRefs(args)
const value = ref(50)
return { value, disabled }
},
template:
'<ScrubableNumberInput v-model="value" :min="0" :max="100" :step="1" :disabled />'
})
}
export const AtMinimum: Story = {
render: () => ({
components: { ScrubableNumberInput },
setup() {
const value = ref(0)
return { value }
},
template:
'<ScrubableNumberInput v-model="value" :min="0" :max="100" :step="1" />'
})
}
export const AtMaximum: Story = {
render: () => ({
components: { ScrubableNumberInput },
setup() {
const value = ref(100)
return { value }
},
template:
'<ScrubableNumberInput v-model="value" :min="0" :max="100" :step="1" />'
})
}
export const FloatPrecision: Story = {
args: { min: 0, max: 1, step: 0.01 },
render: (args) => ({
components: { ScrubableNumberInput },
setup() {
const { min, max, step } = toRefs(args)
const value = ref(0.75)
return { value, min, max, step }
},
template:
'<ScrubableNumberInput v-model="value" :min :max :step display-value="0.75" />'
})
}
export const LargeNumber: Story = {
render: () => ({
components: { ScrubableNumberInput },
setup() {
const value = ref(1809000312992)
return { value }
},
template:
'<ScrubableNumberInput v-model="value" :min="0" :max="Number.MAX_SAFE_INTEGER" :step="1" />'
})
}
export const HiddenButtons: Story = {
args: { hideButtons: true },
render: (args) => ({
components: { ScrubableNumberInput },
setup() {
const { hideButtons } = toRefs(args)
const value = ref(42)
return { value, hideButtons }
},
template:
'<ScrubableNumberInput v-model="value" :min="0" :max="100" :step="1" :hideButtons />'
})
}
export const WithControlButton: Story = {
render: () => ({
components: { ScrubableNumberInput, Button, Popover },
setup() {
const value = ref(1809000312992)
return { value }
},
template: `
<ScrubableNumberInput v-model="value" :min="0" :max="Number.MAX_SAFE_INTEGER" :step="1">
<Popover>
<template #button>
<Button
variant="textonly"
size="sm"
class="h-4 w-7 self-center rounded-xl bg-primary-background/30 p-0 hover:bg-primary-background-hover/30"
>
<i class="icon-[lucide--shuffle] w-full text-xs text-primary-background" />
</Button>
</template>
<div class="p-4 text-sm">Control popover content</div>
</Popover>
</ScrubableNumberInput>
`
})
}

View File

@@ -1,15 +1,9 @@
<template>
<div class="system-stats">
<div class="mb-6">
<div class="mb-4 flex items-center gap-2">
<h2 class="text-2xl font-semibold">
{{ $t('g.systemInfo') }}
</h2>
<Button variant="secondary" @click="copySystemInfo">
<i class="pi pi-copy" />
{{ $t('g.copySystemInfo') }}
</Button>
</div>
<h2 class="mb-4 text-2xl font-semibold">
{{ $t('g.systemInfo') }}
</h2>
<div class="grid grid-cols-2 gap-2">
<template v-for="col in systemColumns" :key="col.field">
<div :class="cn('font-medium', isOutdated(col) && 'text-danger-100')">
@@ -52,8 +46,6 @@ import TabView from 'primevue/tabview'
import { computed } from 'vue'
import DeviceInfo from '@/components/common/DeviceInfo.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { isCloud } from '@/platform/distribution/types'
import type { SystemStats } from '@/schemas/apiSchema'
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
@@ -63,8 +55,6 @@ const props = defineProps<{
stats: SystemStats
}>()
const { copyToClipboard } = useCopyToClipboard()
const systemInfo = computed(() => ({
...props.stats.system,
argv: props.stats.system.argv.join(' ')
@@ -118,7 +108,7 @@ function isOutdated(column: ColumnDef): boolean {
return !!installed && !!required && installed !== required
}
function getDisplayValue(column: ColumnDef) {
const getDisplayValue = (column: ColumnDef) => {
const value = systemInfo.value[column.field]
if (column.formatNumber && typeof value === 'number') {
return column.formatNumber(value)
@@ -128,33 +118,4 @@ function getDisplayValue(column: ColumnDef) {
}
return value
}
function formatSystemInfoText(): string {
const lines: string[] = ['## System Info']
for (const col of systemColumns.value) {
const display = getDisplayValue(col)
if (display !== undefined && display !== '') {
lines.push(`${col.header}: ${display}`)
}
}
if (hasDevices.value) {
lines.push('')
lines.push('## Devices')
for (const device of props.stats.devices) {
lines.push(`- ${device.name} (${device.type})`)
lines.push(` VRAM Total: ${formatSize(device.vram_total)}`)
lines.push(` VRAM Free: ${formatSize(device.vram_free)}`)
lines.push(` Torch VRAM Total: ${formatSize(device.torch_vram_total)}`)
lines.push(` Torch VRAM Free: ${formatSize(device.torch_vram_free)}`)
}
}
return lines.join('\n')
}
function copySystemInfo() {
copyToClipboard(formatSystemInfoText())
}
</script>

View File

@@ -8,7 +8,6 @@
<!-- Node -->
<div
v-if="item.value.type === 'node'"
v-bind="$attrs"
:class="cn(ROW_CLASS, isSelected && 'bg-comfy-input')"
:style="rowStyle"
draggable="true"
@@ -49,7 +48,6 @@
<!-- Folder -->
<div
v-else
v-bind="$attrs"
:class="cn(ROW_CLASS, isSelected && 'bg-comfy-input')"
:style="rowStyle"
@click.stop="handleClick($event, handleToggle, handleSelect)"
@@ -100,10 +98,6 @@ import { InjectKeyContextMenuNode } from '@/types/treeExplorerTypes'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { cn } from '@/utils/tailwindUtil'
defineOptions({
inheritAttrs: false
})
const ROW_CLASS =
'group/tree-node flex cursor-pointer select-none items-center gap-3 overflow-hidden py-2 outline-none hover:bg-comfy-input mx-2 rounded'

View File

@@ -59,15 +59,7 @@
:pt="{ bodyCell: 'p-1 min-h-8' }"
>
<template #body="slotProps">
<div
class="flex items-center gap-1.5 truncate"
:title="slotProps.data.id"
>
<i
v-if="slotProps.data.keybinding?.combo.isBrowserReserved"
v-tooltip="$t('g.browserReservedKeybindingTooltip')"
class="icon-[lucide--triangle-alert] shrink-0 text-warning-background"
/>
<div class="truncate" :title="slotProps.data.id">
{{ slotProps.data.label }}
</div>
</template>
@@ -101,6 +93,44 @@
</Column>
</DataTable>
<Dialog
v-model:visible="editDialogVisible"
class="min-w-96"
modal
:header="currentEditingCommand?.label"
@hide="cancelEdit"
>
<div>
<InputText
ref="keybindingInput"
class="mb-2 text-center"
:model-value="newBindingKeyCombo?.toString() ?? ''"
:placeholder="$t('g.pressKeysForNewBinding')"
autocomplete="off"
fluid
@keydown.stop.prevent="captureKeybinding"
/>
<Message v-if="existingKeybindingOnCombo" severity="warn">
{{ $t('g.keybindingAlreadyExists') }}
<Tag
severity="secondary"
:value="existingKeybindingOnCombo.commandId"
/>
</Message>
</div>
<template #footer>
<Button
:variant="existingKeybindingOnCombo ? 'destructive' : 'primary'"
autofocus
@click="saveKeybinding"
>
<i
:class="existingKeybindingOnCombo ? 'pi pi-pencil' : 'pi pi-check'"
/>
{{ existingKeybindingOnCombo ? $t('g.overwrite') : $t('g.save') }}
</Button>
</template>
</Dialog>
<Button
v-tooltip="$t('g.resetAllKeybindingsTooltip')"
class="mt-4 w-full"
@@ -117,14 +147,18 @@
import { FilterMatchMode } from '@primevue/core/api'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import Message from 'primevue/message'
import Tag from 'primevue/tag'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { computed, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import SearchInput from '@/components/ui/search-input/SearchInput.vue'
import Button from '@/components/ui/button/Button.vue'
import { useEditKeybindingDialog } from '@/composables/useEditKeybindingDialog'
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useCommandStore } from '@/stores/commandStore'
@@ -161,16 +195,50 @@ const commandsData = computed<ICommandData[]>(() => {
})
const selectedCommandData = ref<ICommandData | null>(null)
const editKeybindingDialog = useEditKeybindingDialog()
const editDialogVisible = ref(false)
const newBindingKeyCombo = ref<KeyComboImpl | null>(null)
const currentEditingCommand = ref<ICommandData | null>(null)
const keybindingInput = ref<InstanceType<typeof InputText> | null>(null)
const existingKeybindingOnCombo = computed<KeybindingImpl | null>(() => {
if (!currentEditingCommand.value) {
return null
}
// If the new keybinding is the same as the current editing command, then don't show the error
if (
currentEditingCommand.value.keybinding?.combo?.equals(
newBindingKeyCombo.value
)
) {
return null
}
if (!newBindingKeyCombo.value) {
return null
}
return keybindingStore.getKeybinding(newBindingKeyCombo.value)
})
function editKeybinding(commandData: ICommandData) {
editKeybindingDialog.show({
commandId: commandData.id,
commandLabel: commandData.label,
currentCombo: commandData.keybinding?.combo ?? null
})
currentEditingCommand.value = commandData
newBindingKeyCombo.value = commandData.keybinding
? commandData.keybinding.combo
: null
editDialogVisible.value = true
}
watchEffect(() => {
if (editDialogVisible.value) {
// nextTick doesn't work here, so we use a timeout instead
setTimeout(() => {
// @ts-expect-error - $el is an internal property of the InputText component
keybindingInput.value?.$el?.focus()
}, 300)
}
})
async function removeKeybinding(commandData: ICommandData) {
if (commandData.keybinding) {
keybindingStore.unsetKeybinding(commandData.keybinding)
@@ -178,6 +246,40 @@ async function removeKeybinding(commandData: ICommandData) {
}
}
async function captureKeybinding(event: KeyboardEvent) {
// Allow the use of keyboard shortcuts when adding keyboard shortcuts
if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
switch (event.key) {
case 'Escape':
cancelEdit()
return
case 'Enter':
await saveKeybinding()
return
}
}
const keyCombo = KeyComboImpl.fromEvent(event)
newBindingKeyCombo.value = keyCombo
}
function cancelEdit() {
editDialogVisible.value = false
currentEditingCommand.value = null
newBindingKeyCombo.value = null
}
async function saveKeybinding() {
const commandId = currentEditingCommand.value?.id
const combo = newBindingKeyCombo.value
cancelEdit()
if (!combo || commandId == undefined) return
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({ commandId, combo })
)
if (updated) await keybindingService.persistUserKeybindings()
}
async function resetKeybinding(commandData: ICommandData) {
if (keybindingStore.resetKeybindingForCommand(commandData.id)) {
await keybindingService.persistUserKeybindings()

View File

@@ -1,56 +0,0 @@
<template>
<div class="flex w-96 flex-col border-t border-border-default px-4">
<p class="mb-4 text-sm text-muted-foreground">
{{ $t('g.setAKeybindingForTheFollowing') }}
</p>
<div class="mb-4 text-sm text-base-foreground">
{{ commandLabel }}
</div>
<input
class="text-foreground mb-4 w-full rounded-sm border border-border-default bg-secondary-background px-3 py-2 text-center shadow-none focus:outline-none"
:value="dialogState.newCombo?.toString() ?? ''"
:placeholder="$t('g.enterYourKeybind')"
:aria-label="$t('g.enterYourKeybind')"
autocomplete="off"
autofocus
@keydown.stop.prevent="captureKeybinding"
/>
<div class="min-h-12">
<p
v-if="dialogState.newCombo?.isBrowserReserved"
class="m-0 text-sm text-destructive-background"
>
{{ $t('g.browserReservedKeybinding') }}
</p>
<p
v-else-if="existingKeybindingOnCombo"
class="m-0 text-sm text-destructive-background"
>
{{ $t('g.keybindingAlreadyExists') }}
{{ existingKeybindingOnCombo.commandId }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
import type { EditKeybindingDialogState } from '@/composables/useEditKeybindingDialog'
const { dialogState, onUpdateCombo, existingKeybindingOnCombo } = defineProps<{
dialogState: EditKeybindingDialogState
commandLabel: string
onUpdateCombo: (combo: KeyComboImpl) => void
existingKeybindingOnCombo: KeybindingImpl | null
}>()
function captureKeybinding(event: KeyboardEvent) {
if (!event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) {
if (event.key === 'Escape') return
}
onUpdateCombo(KeyComboImpl.fromEvent(event))
}
</script>

View File

@@ -1,72 +0,0 @@
<template>
<div class="flex w-full justify-end gap-2 px-4 py-2">
<Button
variant="textonly"
size="md"
class="text-muted-foreground"
@click="handleCancel"
>
{{ $t('g.cancel') }}
</Button>
<Button
:variant="
existingKeybindingOnCombo
? 'destructive'
: dialogState.newCombo?.isBrowserReserved
? 'secondary'
: 'primary'
"
size="md"
:disabled="!dialogState.newCombo"
class="px-4 py-2"
@click="handleSave"
>
{{
existingKeybindingOnCombo
? $t('g.overwrite')
: dialogState.newCombo?.isBrowserReserved
? $t('g.saveAnyway')
: $t('g.save')
}}
</Button>
</div>
</template>
<script setup lang="ts">
import type { Reactive } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useDialogStore } from '@/stores/dialogStore'
import type { EditKeybindingDialogState } from '@/composables/useEditKeybindingDialog'
import { DIALOG_KEY } from '@/composables/useEditKeybindingDialog'
const { dialogState, existingKeybindingOnCombo } = defineProps<{
dialogState: Reactive<EditKeybindingDialogState>
existingKeybindingOnCombo: KeybindingImpl | null
}>()
const keybindingStore = useKeybindingStore()
const keybindingService = useKeybindingService()
const dialogStore = useDialogStore()
function handleCancel() {
dialogStore.closeDialog({ key: DIALOG_KEY })
}
async function handleSave() {
const combo = dialogState.newCombo
const commandId = dialogState.commandId
if (!combo || !commandId) return
dialogStore.closeDialog({ key: DIALOG_KEY })
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({ commandId, combo })
)
if (updated) await keybindingService.persistUserKeybindings()
}
</script>

View File

@@ -1,5 +0,0 @@
<template>
<div class="flex w-full items-center gap-2 p-4">
<p class="m-0 font-semibold">{{ $t('g.modifyKeybinding') }}</p>
</div>
</template>

View File

@@ -1,153 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import CanvasModeSelector from '@/components/graph/CanvasModeSelector.vue'
const mockExecute = vi.fn()
const mockGetCommand = vi.fn().mockReturnValue({
keybinding: {
combo: {
getKeySequences: () => ['V']
}
}
})
const mockFormatKeySequence = vi.fn().mockReturnValue('V')
vi.mock('@/stores/commandStore', () => ({
useCommandStore: () => ({
execute: mockExecute,
getCommand: mockGetCommand,
formatKeySequence: mockFormatKeySequence
})
}))
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
canvas: { read_only: false }
})
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
graphCanvasMenu: {
select: 'Select',
hand: 'Hand',
canvasMode: 'Canvas Mode'
}
}
}
})
const mockPopoverHide = vi.fn()
function createWrapper() {
return mount(CanvasModeSelector, {
global: {
plugins: [i18n],
stubs: {
Popover: {
template: '<div><slot /></div>',
methods: {
toggle: vi.fn(),
hide: mockPopoverHide
}
}
}
}
})
}
describe('CanvasModeSelector', () => {
it('should render menu with menuitemradio roles and aria-checked', () => {
const wrapper = createWrapper()
const menu = wrapper.find('[role="menu"]')
expect(menu.exists()).toBe(true)
const menuItems = wrapper.findAll('[role="menuitemradio"]')
expect(menuItems).toHaveLength(2)
// Select mode is active (read_only: false), so select is checked
expect(menuItems[0].attributes('aria-checked')).toBe('true')
expect(menuItems[1].attributes('aria-checked')).toBe('false')
})
it('should render menu items as buttons with aria-labels', () => {
const wrapper = createWrapper()
const menuItems = wrapper.findAll('[role="menuitemradio"]')
menuItems.forEach((btn) => {
expect(btn.element.tagName).toBe('BUTTON')
expect(btn.attributes('type')).toBe('button')
})
expect(menuItems[0].attributes('aria-label')).toBe('Select')
expect(menuItems[1].attributes('aria-label')).toBe('Hand')
})
it('should use roving tabindex based on active mode', () => {
const wrapper = createWrapper()
const menuItems = wrapper.findAll('[role="menuitemradio"]')
// Select is active (read_only: false) → tabindex 0
expect(menuItems[0].attributes('tabindex')).toBe('0')
// Hand is inactive → tabindex -1
expect(menuItems[1].attributes('tabindex')).toBe('-1')
})
it('should mark icons as aria-hidden', () => {
const wrapper = createWrapper()
const icons = wrapper.findAll('[role="menuitemradio"] i')
icons.forEach((icon) => {
expect(icon.attributes('aria-hidden')).toBe('true')
})
})
it('should expose trigger button with aria-haspopup and aria-expanded', () => {
const wrapper = createWrapper()
const trigger = wrapper.find('[aria-haspopup="menu"]')
expect(trigger.exists()).toBe(true)
expect(trigger.attributes('aria-label')).toBe('Canvas Mode')
expect(trigger.attributes('aria-expanded')).toBe('false')
})
it('should call focus on next item when ArrowDown is pressed', async () => {
const wrapper = createWrapper()
const menuItems = wrapper.findAll('[role="menuitemradio"]')
const secondItemEl = menuItems[1].element as HTMLElement
const focusSpy = vi.spyOn(secondItemEl, 'focus')
await menuItems[0].trigger('keydown', { key: 'ArrowDown' })
expect(focusSpy).toHaveBeenCalled()
})
it('should call focus on previous item when ArrowUp is pressed', async () => {
const wrapper = createWrapper()
const menuItems = wrapper.findAll('[role="menuitemradio"]')
const firstItemEl = menuItems[0].element as HTMLElement
const focusSpy = vi.spyOn(firstItemEl, 'focus')
await menuItems[1].trigger('keydown', { key: 'ArrowUp' })
expect(focusSpy).toHaveBeenCalled()
})
it('should close popover on Escape and restore focus to trigger', async () => {
const wrapper = createWrapper()
const menuItems = wrapper.findAll('[role="menuitemradio"]')
const trigger = wrapper.find('[aria-haspopup="menu"]')
const triggerEl = trigger.element as HTMLElement
const focusSpy = vi.spyOn(triggerEl, 'focus')
await menuItems[0].trigger('keydown', { key: 'Escape' })
expect(mockPopoverHide).toHaveBeenCalled()
expect(focusSpy).toHaveBeenCalled()
})
})

View File

@@ -4,21 +4,15 @@
variant="secondary"
class="group h-8 rounded-none! bg-comfy-menu-bg p-0 transition-none! hover:rounded-lg! hover:bg-interface-button-hover-surface!"
:style="buttonStyles"
:aria-label="$t('graphCanvasMenu.canvasMode')"
aria-haspopup="menu"
:aria-expanded="isOpen"
@click="toggle"
>
<div class="flex items-center gap-1 pr-0.5">
<div
class="rounded-lg bg-interface-panel-selected-surface p-2 group-hover:bg-interface-button-hover-surface"
>
<i :class="currentModeIcon" class="block size-4" aria-hidden="true" />
<i :class="currentModeIcon" class="block size-4" />
</div>
<i
class="icon-[lucide--chevron-down] block size-4 pr-1.5"
aria-hidden="true"
/>
<i class="icon-[lucide--chevron-down] block size-4 pr-1.5" />
</div>
</Button>
@@ -30,54 +24,31 @@
:close-on-escape="true"
unstyled
:pt="popoverPt"
@show="onPopoverShow"
@hide="onPopoverHide"
>
<div
ref="menuRef"
class="flex flex-col gap-1"
role="menu"
:aria-label="$t('graphCanvasMenu.canvasMode')"
>
<button
type="button"
role="menuitemradio"
:aria-checked="!isCanvasReadOnly"
:tabindex="!isCanvasReadOnly ? 0 : -1"
class="flex w-full cursor-pointer items-center justify-between rounded-sm border-none bg-transparent px-3 py-2 text-sm text-text-primary outline-none hover:bg-node-component-surface-hovered focus-visible:bg-node-component-surface-hovered"
:aria-label="$t('graphCanvasMenu.select')"
<div class="flex flex-col gap-1">
<div
class="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="setMode('select')"
@keydown.arrow-down.prevent="focusNextItem"
@keydown.arrow-up.prevent="focusPrevItem"
@keydown.escape.prevent="closeAndRestoreFocus"
>
<div class="flex items-center gap-2">
<i class="icon-[lucide--mouse-pointer-2] size-4" aria-hidden="true" />
<i class="icon-[lucide--mouse-pointer-2] size-4" />
<span>{{ $t('graphCanvasMenu.select') }}</span>
</div>
<span class="text-[9px] text-text-primary">{{
unlockCommandText
}}</span>
</button>
</div>
<button
type="button"
role="menuitemradio"
:aria-checked="isCanvasReadOnly"
:tabindex="isCanvasReadOnly ? 0 : -1"
class="flex w-full cursor-pointer items-center justify-between rounded-sm border-none bg-transparent px-3 py-2 text-sm text-text-primary outline-none hover:bg-node-component-surface-hovered focus-visible:bg-node-component-surface-hovered"
:aria-label="$t('graphCanvasMenu.hand')"
<div
class="flex cursor-pointer items-center justify-between rounded-sm px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="setMode('hand')"
@keydown.arrow-down.prevent="focusNextItem"
@keydown.arrow-up.prevent="focusPrevItem"
@keydown.escape.prevent="closeAndRestoreFocus"
>
<div class="flex items-center gap-2">
<i class="icon-[lucide--hand] size-4" aria-hidden="true" />
<i class="icon-[lucide--hand] size-4" />
<span>{{ $t('graphCanvasMenu.hand') }}</span>
</div>
<span class="text-[9px] text-text-primary">{{ lockCommandText }}</span>
</button>
</div>
</div>
</Popover>
</template>
@@ -85,7 +56,7 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import type { ComponentPublicInstance } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { computed, ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -98,8 +69,6 @@ interface Props {
defineProps<Props>()
const buttonRef = ref<ComponentPublicInstance | null>(null)
const popover = ref<InstanceType<typeof Popover>>()
const menuRef = ref<HTMLElement | null>(null)
const isOpen = ref(false)
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
@@ -137,43 +106,6 @@ const setMode = (mode: 'select' | 'hand') => {
popover.value?.hide()
}
async function onPopoverShow() {
isOpen.value = true
await nextTick()
const checkedItem = menuRef.value?.querySelector<HTMLElement>(
'[aria-checked="true"]'
)
checkedItem?.focus()
}
function onPopoverHide() {
isOpen.value = false
}
function closeAndRestoreFocus() {
popover.value?.hide()
const el = buttonRef.value?.$el || buttonRef.value
;(el as HTMLElement)?.focus()
}
function focusNextItem(event: KeyboardEvent) {
const items = getMenuItems(event)
const index = items.indexOf(event.target as HTMLElement)
items[(index + 1) % items.length]?.focus()
}
function focusPrevItem(event: KeyboardEvent) {
const items = getMenuItems(event)
const index = items.indexOf(event.target as HTMLElement)
items[(index - 1 + items.length) % items.length]?.focus()
}
function getMenuItems(event: KeyboardEvent): HTMLElement[] {
const menu = (event.target as HTMLElement).closest('[role="menu"]')
if (!menu) return []
return Array.from(menu.querySelectorAll('[role="menuitemradio"]'))
}
const popoverPt = computed(() => ({
root: {
class: 'absolute z-50 -translate-y-2'

View File

@@ -479,53 +479,50 @@ useEventListener(
onMounted(async () => {
comfyApp.vueAppReady = true
workspaceStore.spinner = true
try {
// ChangeTracker needs to be initialized before setup, as it will overwrite
// some listeners of litegraph canvas.
ChangeTracker.init()
// ChangeTracker needs to be initialized before setup, as it will overwrite
// some listeners of litegraph canvas.
ChangeTracker.init()
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
if (settingsError.value) {
if (settingsError.value instanceof UnauthorizedError) {
localStorage.removeItem('Comfy.userId')
localStorage.removeItem('Comfy.userName')
window.location.reload()
return
}
throw settingsError.value
if (settingsError.value) {
if (settingsError.value instanceof UnauthorizedError) {
localStorage.removeItem('Comfy.userId')
localStorage.removeItem('Comfy.userName')
window.location.reload()
return
}
// Register core settings immediately after settings are ready
CORE_SETTINGS.forEach(settingStore.addSetting)
await Promise.all([
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
useNewUserService().initializeIfNewUser()
])
if (i18nError.value) {
console.warn(
'[GraphCanvas] Failed to load custom nodes i18n:',
i18nError.value
)
}
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
useSearchBoxStore().setPopoverRef(nodeSearchboxPopoverRef.value)
window.app = comfyApp
window.graph = comfyApp.graph
comfyAppReady.value = true
vueNodeLifecycle.setupEmptyGraphListener()
} finally {
workspaceStore.spinner = false
throw settingsError.value
}
// Register core settings immediately after settings are ready
CORE_SETTINGS.forEach(settingStore.addSetting)
await Promise.all([
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
useNewUserService().initializeIfNewUser()
])
if (i18nError.value) {
console.warn(
'[GraphCanvas] Failed to load custom nodes i18n:',
i18nError.value
)
}
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false
useSearchBoxStore().setPopoverRef(nodeSearchboxPopoverRef.value)
window.app = comfyApp
window.graph = comfyApp.graph
comfyAppReady.value = true
vueNodeLifecycle.setupEmptyGraphListener()
comfyApp.canvas.onSelectionChange = useChainCallback(
comfyApp.canvas.onSelectionChange,
() => canvasStore.updateSelectedItems()
@@ -538,7 +535,7 @@ onMounted(async () => {
// Restore saved workflow and workflow tabs state
await workflowPersistence.initializeWorkflow()
await workflowPersistence.restoreWorkflowTabsState()
workflowPersistence.restoreWorkflowTabsState()
const sharedWorkflowLoadStatus =
await workflowPersistence.loadSharedWorkflowFromUrlIfPresent()

View File

@@ -10,8 +10,6 @@
></div>
<ButtonGroup
role="toolbar"
:aria-label="t('graphCanvasMenu.canvasToolbar')"
class="absolute right-0 bottom-0 z-1200 flex-row gap-1 border border-interface-stroke bg-comfy-menu-bg p-2"
:style="{
...stringifiedMinimapStyles.buttonGroupStyles
@@ -32,7 +30,7 @@
class="size-8 bg-comfy-menu-bg p-0 hover:bg-interface-button-hover-surface!"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<i class="icon-[lucide--focus] size-4" aria-hidden="true" />
<i class="icon-[lucide--focus] size-4" />
</Button>
<Button
@@ -46,7 +44,7 @@
>
<span class="inline-flex items-center gap-1 px-2 text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i class="icon-[lucide--chevron-down] size-4" aria-hidden="true" />
<i class="icon-[lucide--chevron-down] size-4" />
</span>
</Button>
@@ -61,7 +59,7 @@
:class="minimapButtonClass"
@click="onMinimapToggleClick"
>
<i class="icon-[lucide--map] size-4" aria-hidden="true" />
<i class="icon-[lucide--map] size-4" />
</Button>
<Button
@@ -80,7 +78,7 @@
:style="stringifiedMinimapStyles.buttonStyles"
@click="onLinkVisibilityToggleClick"
>
<i class="icon-[lucide--route-off] size-4" aria-hidden="true" />
<i class="icon-[lucide--route-off] size-4" />
</Button>
</ButtonGroup>
</div>

View File

@@ -0,0 +1,96 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import LogoCFillLoader from './LogoCFillLoader.vue'
const meta: Meta<typeof LogoCFillLoader> = {
title: 'Components/Loader/LogoCFillLoader',
component: LogoCFillLoader,
tags: ['autodocs'],
parameters: {
layout: 'centered',
backgrounds: { default: 'dark' }
},
argTypes: {
size: {
control: 'select',
options: ['sm', 'md', 'lg', 'xl']
},
color: {
control: 'select',
options: ['yellow', 'blue', 'white', 'black']
},
bordered: {
control: 'boolean'
},
disableAnimation: {
control: 'boolean'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const Small: Story = {
args: { size: 'sm' }
}
export const Large: Story = {
args: { size: 'lg' }
}
export const ExtraLarge: Story = {
args: { size: 'xl' }
}
export const NoBorder: Story = {
args: { bordered: false }
}
export const Static: Story = {
args: { disableAnimation: true }
}
export const BrandColors: Story = {
render: () => ({
components: { LogoCFillLoader },
template: `
<div class="flex items-end gap-12">
<div class="flex flex-col items-center gap-2">
<span class="text-xs text-neutral-400">Yellow</span>
<LogoCFillLoader size="lg" color="yellow" />
</div>
<div class="flex flex-col items-center gap-2">
<span class="text-xs text-neutral-400">Blue</span>
<LogoCFillLoader size="lg" color="blue" />
</div>
<div class="flex flex-col items-center gap-2">
<span class="text-xs text-neutral-400">White</span>
<LogoCFillLoader size="lg" color="white" />
</div>
<div class="p-4 bg-white rounded" style="background: white">
<div class="flex flex-col items-center gap-2">
<span class="text-xs text-neutral-600">Black</span>
<LogoCFillLoader size="lg" color="black" />
</div>
</div>
</div>
`
})
}
export const AllSizes: Story = {
render: () => ({
components: { LogoCFillLoader },
template: `
<div class="flex items-end gap-8">
<LogoCFillLoader size="sm" color="yellow" />
<LogoCFillLoader size="md" color="yellow" />
<LogoCFillLoader size="lg" color="yellow" />
<LogoCFillLoader size="xl" color="yellow" />
</div>
`
})
}

View File

@@ -0,0 +1,100 @@
<template>
<span role="status" :class="cn('inline-flex', colorClass)">
<svg
:width="Math.round(heightMap[size] * (VB_W / VB_H))"
:height="heightMap[size]"
:viewBox="`0 0 ${VB_W} ${VB_H}`"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<mask :id="maskId">
<path :d="C_PATH" fill="white" />
</mask>
</defs>
<path
v-if="bordered"
:d="C_PATH"
stroke="currentColor"
stroke-width="2"
fill="none"
opacity="0.4"
/>
<g :mask="`url(#${maskId})`">
<rect
:class="disableAnimation ? undefined : 'c-fill-rect'"
:x="-BLEED"
:y="-BLEED"
:width="VB_W + BLEED * 2"
:height="VB_H + BLEED * 2"
fill="currentColor"
/>
</g>
</svg>
<span class="sr-only">{{ t('g.loading') }}</span>
</span>
</template>
<script setup lang="ts">
import { useId, computed } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import { useI18n } from 'vue-i18n'
const {
size = 'md',
color = 'black',
bordered = true,
disableAnimation = false
} = defineProps<{
size?: 'sm' | 'md' | 'lg' | 'xl'
color?: 'yellow' | 'blue' | 'white' | 'black'
bordered?: boolean
disableAnimation?: boolean
}>()
const { t } = useI18n()
const maskId = `c-mask-${useId()}`
const VB_W = 185
const VB_H = 201
const BLEED = 1
// Larger than LogoComfyWaveLoader because the C logo is near-square (185×201)
// while the COMFY wordmark is wide (879×284), so larger heights are needed
// for visually comparable perceived size.
const heightMap = { sm: 48, md: 80, lg: 120, xl: 200 } as const
const colorMap = {
yellow: 'text-brand-yellow',
blue: 'text-brand-blue',
white: 'text-white',
black: 'text-black'
} as const
const colorClass = computed(() => colorMap[color])
const C_PATH =
'M42.1217 200.812C37.367 200.812 33.5304 199.045 31.0285 195.703C28.4569 192.27 27.7864 187.477 29.1882 182.557L34.8172 162.791C35.2661 161.217 34.9537 159.523 33.9747 158.214C32.9958 156.908 31.464 156.139 29.8371 156.139L13.6525 156.139C8.89521 156.139 5.05862 154.374 2.55797 151.032C-0.0136533 147.597-0.684085 142.804 0.71869 137.883L20.0565 70.289L22.1916 62.8625C25.0617 52.7847 35.5288 44.5943 45.528 44.5943L64.8938 44.5943C67.2048 44.5943 69.2376 43.0535 69.8738 40.8175L76.2782 18.3344C79.1454 8.26681 89.6127 0.0763962 99.6117 0.0763945L141.029 0.00258328L171.349-2.99253e-05C176.104-3.0756e-05 179.941 1.765 182.442 5.10626C185.013 8.53932 185.684 13.3324 184.282 18.2528L175.612 48.6947C172.746 58.7597 162.279 66.9475 152.28 66.9475L110.771 67.0265L91.4113 67.0265C89.1029 67.0265 87.0727 68.5647 86.4326 70.7983L70.2909 127.179C69.8394 128.756 70.1518 130.454 71.1334 131.763C72.1123 133.07 73.6441 133.839 75.2697 133.839C75.2736 133.839 102.699 133.785 102.699 133.785L132.929 133.785C137.685 133.785 141.522 135.55 144.023 138.892C146.594 142.327 147.265 147.12 145.862 152.041L137.192 182.478C134.326 192.545 123.859 200.733 113.86 200.733L72.3517 200.812L42.1217 200.812Z'
</script>
<style scoped>
.c-fill-rect {
animation: c-fill-up 2.5s cubic-bezier(0.25, 0, 0.3, 1) forwards;
will-change: transform;
}
@keyframes c-fill-up {
0% {
transform: translateY(calc(v-bind(VB_H) * 1px + v-bind(BLEED) * 1px));
}
100% {
transform: translateY(calc(v-bind(BLEED) * -1px));
}
}
@media (prefers-reduced-motion: reduce) {
.c-fill-rect {
animation: none;
}
}
</style>

View File

@@ -13,7 +13,6 @@ import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
@@ -37,7 +36,6 @@ import TabErrors from './errors/TabErrors.vue'
const canvasStore = useCanvasStore()
const executionErrorStore = useExecutionErrorStore()
const missingModelStore = useMissingModelStore()
const rightSidePanelStore = useRightSidePanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()
@@ -45,8 +43,6 @@ const { t } = useI18n()
const { hasAnyError, allErrorExecutionIds, activeMissingNodeGraphIds } =
storeToRefs(executionErrorStore)
const { activeMissingModelGraphIds } = storeToRefs(missingModelStore)
const { findParentGroup } = useGraphHierarchy()
const { selectedItems: directlySelectedItems } = storeToRefs(canvasStore)
@@ -122,21 +118,12 @@ const hasMissingNodeSelected = computed(
)
)
const hasMissingModelSelected = computed(
() =>
hasSelection.value &&
selectedNodes.value.some((node) =>
activeMissingModelGraphIds.value.has(String(node.id))
)
)
const hasRelevantErrors = computed(() => {
if (!hasSelection.value) return hasAnyError.value
return (
hasDirectNodeError.value ||
hasContainerInternalError.value ||
hasMissingNodeSelected.value ||
hasMissingModelSelected.value
hasMissingNodeSelected.value
)
})
@@ -327,11 +314,7 @@ function handleTitleCancel() {
:value="tab.value"
>
{{ tab.label() }}
<i
v-if="tab.icon"
aria-hidden="true"
:class="cn(tab.icon, 'size-4')"
/>
<i v-if="tab.icon" :class="cn(tab.icon, 'size-4')" />
</Tab>
</TabList>
</nav>

View File

@@ -12,7 +12,7 @@
</div>
<!-- Scrollable content -->
<div class="min-w-0 flex-1 overflow-y-auto" aria-live="polite">
<div class="min-w-0 flex-1 overflow-y-auto">
<TransitionGroup tag="div" name="list-scale" class="relative">
<div
v-if="filteredGroups.length === 0"
@@ -32,7 +32,11 @@
:key="group.title"
:collapse="isSectionCollapsed(group.title) && !isSearching"
class="border-b border-interface-stroke"
:size="getGroupSize(group)"
:size="
group.type === 'missing_node' || group.type === 'swap_nodes'
? 'lg'
: 'default'
"
@update:collapse="setSectionCollapsed(group.title, $event)"
>
<template #label>
@@ -126,14 +130,6 @@
@copy-to-clipboard="copyToClipboard"
/>
</div>
<!-- Missing Models -->
<MissingModelCard
v-else-if="group.type === 'missing_model'"
:missing-model-groups="missingModelGroups"
:show-node-id-badge="showNodeIdBadge"
@locate-model="handleLocateModel"
/>
</PropertiesAccordionItem>
</TransitionGroup>
</div>
@@ -191,14 +187,12 @@ import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/f
import ErrorNodeCard from './ErrorNodeCard.vue'
import MissingNodeCard from './MissingNodeCard.vue'
import SwapNodesCard from '@/platform/nodeReplacement/components/SwapNodesCard.vue'
import MissingModelCard from '@/platform/missingModel/components/MissingModelCard.vue'
import Button from '@/components/ui/button/Button.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { useErrorGroups } from './useErrorGroups'
import type { SwapNodeGroup } from './useErrorGroups'
import type { ErrorGroup } from './types'
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
const { t } = useI18n()
@@ -217,15 +211,6 @@ const { replaceGroup, replaceAllGroups } = useNodeReplacement()
const searchQuery = ref('')
const isSearching = computed(() => searchQuery.value.trim() !== '')
const fullSizeGroupTypes = new Set([
'missing_node',
'swap_nodes',
'missing_model'
])
function getGroupSize(group: ErrorGroup) {
return fullSizeGroupTypes.has(group.type) ? 'lg' : 'default'
}
const showNodeIdBadge = computed(
() =>
(settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode) !==
@@ -241,7 +226,6 @@ const {
errorNodeCache,
missingNodeCache,
missingPackGroups,
missingModelGroups,
swapNodeGroups
} = useErrorGroups(searchQuery, t)
@@ -299,10 +283,6 @@ function handleLocateMissingNode(nodeId: string) {
focusNode(nodeId, missingNodeCache.value)
}
function handleLocateModel(nodeId: string) {
focusNode(nodeId)
}
function handleOpenManagerInfo(packId: string) {
const isKnownToRegistry = missingNodePacks.value.some((p) => p.id === packId)
if (isKnownToRegistry) {

View File

@@ -23,4 +23,3 @@ export type ErrorGroup =
}
| { type: 'missing_node'; title: string; priority: number }
| { type: 'swap_nodes'; title: string; priority: number }
| { type: 'missing_model'; title: string; priority: number }

View File

@@ -47,13 +47,6 @@ vi.mock('@/utils/executableGroupNodeDto', () => ({
isGroupNode: vi.fn(() => false)
}))
vi.mock(
'@/platform/missingModel/composables/useMissingModelInteractions',
() => ({
clearMissingModelState: vi.fn()
})
)
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useErrorGroups } from './useErrorGroups'
@@ -527,115 +520,4 @@ describe('useErrorGroups', () => {
expect(typeof groups.collapseState).toBe('object')
})
})
describe('missingModelGroups', () => {
function makeModel(
name: string,
opts: {
nodeId?: string | number
widgetName?: string
directory?: string
isAssetSupported?: boolean
} = {}
) {
return {
name,
nodeId: opts.nodeId ?? '1',
nodeType: 'CheckpointLoaderSimple',
widgetName: opts.widgetName ?? 'ckpt_name',
isAssetSupported: opts.isAssetSupported ?? false,
isMissing: true as const,
directory: opts.directory
}
}
it('returns empty array when no missing models', () => {
const { groups } = createErrorGroups()
expect(groups.missingModelGroups.value).toEqual([])
})
it('groups asset-supported models by directory', async () => {
const { store, groups } = createErrorGroups()
store.surfaceMissingModels([
makeModel('model_a.safetensors', {
directory: 'checkpoints',
isAssetSupported: true
}),
makeModel('model_b.safetensors', {
nodeId: '2',
directory: 'checkpoints',
isAssetSupported: true
}),
makeModel('lora_a.safetensors', {
nodeId: '3',
directory: 'loras',
isAssetSupported: true
})
])
await nextTick()
expect(groups.missingModelGroups.value).toHaveLength(2)
const ckptGroup = groups.missingModelGroups.value.find(
(g) => g.directory === 'checkpoints'
)
expect(ckptGroup?.models).toHaveLength(2)
expect(ckptGroup?.isAssetSupported).toBe(true)
})
it('puts unsupported models in a separate group', async () => {
const { store, groups } = createErrorGroups()
store.surfaceMissingModels([
makeModel('model_a.safetensors', {
directory: 'checkpoints',
isAssetSupported: true
}),
makeModel('custom_model.safetensors', {
nodeId: '2',
isAssetSupported: false
})
])
await nextTick()
expect(groups.missingModelGroups.value).toHaveLength(2)
const unsupported = groups.missingModelGroups.value.find(
(g) => !g.isAssetSupported
)
expect(unsupported?.models).toHaveLength(1)
})
it('merges same-named models into one view model with multiple referencingNodes', async () => {
const { store, groups } = createErrorGroups()
store.surfaceMissingModels([
makeModel('shared_model.safetensors', {
nodeId: '1',
widgetName: 'ckpt_name',
directory: 'checkpoints',
isAssetSupported: true
}),
makeModel('shared_model.safetensors', {
nodeId: '2',
widgetName: 'ckpt_name',
directory: 'checkpoints',
isAssetSupported: true
})
])
await nextTick()
expect(groups.missingModelGroups.value).toHaveLength(1)
const model = groups.missingModelGroups.value[0].models[0]
expect(model.name).toBe('shared_model.safetensors')
expect(model.referencingNodes).toHaveLength(2)
})
it('includes missing_model group in allErrorGroups', async () => {
const { store, groups } = createErrorGroups()
store.surfaceMissingModels([makeModel('model_a.safetensors')])
await nextTick()
const modelGroup = groups.allErrorGroups.value.find(
(g) => g.type === 'missing_model'
)
expect(modelGroup).toBeDefined()
})
})
})

View File

@@ -3,7 +3,6 @@ import type { MaybeRefOrGetter } from 'vue'
import Fuse from 'fuse.js'
import type { IFuseOptions } from 'fuse.js'
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -24,11 +23,6 @@ import { st } from '@/i18n'
import type { MissingNodeType } from '@/types/comfy'
import type { ErrorCardData, ErrorGroup, ErrorItem } from './types'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import type {
MissingModelCandidate,
MissingModelGroup
} from '@/platform/missingModel/types'
import { groupCandidatesByName } from '@/platform/missingModel/missingModelScan'
import {
isNodeExecutionId,
compareExecutionId
@@ -45,9 +39,6 @@ const KNOWN_PROMPT_ERROR_TYPES = new Set([
/** Sentinel: distinguishes "fetch in-flight" from "fetch done, pack not found (null)". */
const RESOLVING = '__RESOLVING__'
/** Sentinel key for grouping non-asset-supported missing models. */
const UNSUPPORTED = Symbol('unsupported')
export interface MissingPackGroup {
packId: string | null
nodeTypes: MissingNodeType[]
@@ -240,7 +231,6 @@ export function useErrorGroups(
t: (key: string) => string
) {
const executionErrorStore = useExecutionErrorStore()
const missingModelStore = useMissingModelStore()
const canvasStore = useCanvasStore()
const { inferPackFromNodeName } = useComfyRegistryStore()
const collapseState = reactive<Record<string, boolean>>({})
@@ -569,60 +559,6 @@ export function useErrorGroups(
return groups.sort((a, b) => a.priority - b.priority)
}
/** Groups missing models. Asset-supported models group by directory; others go into a separate group.
* Within each group, candidates with the same model name are merged into a single view model. */
const missingModelGroups = computed<MissingModelGroup[]>(() => {
const candidates = missingModelStore.missingModelCandidates
if (!candidates?.length) return []
type GroupKey = string | null | typeof UNSUPPORTED
const map = new Map<
GroupKey,
{ candidates: MissingModelCandidate[]; isAssetSupported: boolean }
>()
for (const c of candidates) {
const groupKey: GroupKey = c.isAssetSupported
? c.directory || null
: UNSUPPORTED
const existing = map.get(groupKey)
if (existing) {
existing.candidates.push(c)
} else {
map.set(groupKey, {
candidates: [c],
isAssetSupported: c.isAssetSupported
})
}
}
return Array.from(map.entries())
.sort(([dirA], [dirB]) => {
if (dirA === UNSUPPORTED) return 1
if (dirB === UNSUPPORTED) return -1
if (dirA === null) return 1
if (dirB === null) return -1
return dirA.localeCompare(dirB)
})
.map(([key, { candidates: groupCandidates, isAssetSupported }]) => ({
directory: typeof key === 'string' ? key : null,
models: groupCandidatesByName(groupCandidates),
isAssetSupported
}))
})
function buildMissingModelGroups(): ErrorGroup[] {
if (!missingModelGroups.value.length) return []
return [
{
type: 'missing_model' as const,
title: `${t('rightSidePanel.missingModels.missingModelsTitle')} (${missingModelGroups.value.reduce((count, group) => count + group.models.length, 0)})`,
priority: 2
}
]
}
const allErrorGroups = computed<ErrorGroup[]>(() => {
const groupsMap = new Map<string, GroupEntry>()
@@ -630,11 +566,7 @@ export function useErrorGroups(
processNodeErrors(groupsMap)
processExecutionError(groupsMap)
return [
...buildMissingNodeGroups(),
...buildMissingModelGroups(),
...toSortedGroups(groupsMap)
]
return [...buildMissingNodeGroups(), ...toSortedGroups(groupsMap)]
})
const tabErrorGroups = computed<ErrorGroup[]>(() => {
@@ -648,11 +580,7 @@ export function useErrorGroups(
? toSortedGroups(regroupByErrorMessage(groupsMap))
: toSortedGroups(groupsMap)
return [
...buildMissingNodeGroups(),
...buildMissingModelGroups(),
...executionGroups
]
return [...buildMissingNodeGroups(), ...executionGroups]
})
const filteredGroups = computed<ErrorGroup[]>(() => {
@@ -687,7 +615,6 @@ export function useErrorGroups(
missingNodeCache,
groupedErrorMessages,
missingPackGroups,
missingModelGroups,
swapNodeGroups
}
}

View File

@@ -11,15 +11,13 @@
}"
@click="onLogoMenuClick($event)"
>
<div class="grid place-items-center-safe gap-0.5">
<i
class="col-span-full row-span-full icon-[lucide--chevron-down] size-3 translate-x-4 text-muted-foreground"
/>
<div class="flex items-center gap-0.5">
<ComfyLogo
alt="ComfyUI Logo"
class="comfyui-logo col-span-full row-span-full size-4.5"
class="comfyui-logo h-[18px] w-[18px]"
mode="fill"
/>
<i class="icon-[lucide--chevron-down] size-3 text-muted-foreground" />
</div>
</div>

View File

@@ -48,24 +48,13 @@
</div>
</div>
<HelpCenterPopups :is-small="isSmall" />
<Suspense v-if="NightlySurveyController">
<component :is="NightlySurveyController" />
</Suspense>
</nav>
</template>
<script setup lang="ts">
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'es-toolkit/compat'
import {
computed,
defineAsyncComponent,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch
} from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
@@ -73,7 +62,7 @@ import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { isCloud, isDesktop, isNightly } from '@/platform/distribution/types'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -89,13 +78,6 @@ import SidebarIcon from './SidebarIcon.vue'
import SidebarLogoutIcon from './SidebarLogoutIcon.vue'
import SidebarTemplatesButton from './SidebarTemplatesButton.vue'
const NightlySurveyController =
isNightly && !isCloud && !isDesktop
? defineAsyncComponent(
() => import('@/platform/surveys/NightlySurveyController.vue')
)
: undefined
const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()

View File

@@ -90,7 +90,6 @@ import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { getAssetDisplayName } from '@/platform/assets/utils/assetMetadataUtils'
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
import { useAssetsStore } from '@/stores/assetsStore'
import {
@@ -136,6 +135,10 @@ const listGridStyle = {
gap: '0.5rem'
}
function getAssetDisplayName(asset: AssetItem): string {
return asset.display_name || asset.name
}
function getAssetPrimaryText(asset: AssetItem): string {
return truncateFilename(getAssetDisplayName(asset))
}

View File

@@ -236,7 +236,6 @@ import { useOutputStacks } from '@/platform/assets/composables/useOutputStacks'
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { getAssetDisplayName } from '@/platform/assets/utils/assetMetadataUtils'
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
import { resolveOutputAssetItems } from '@/platform/assets/utils/outputAssetUtil'
import { isCloud } from '@/platform/distribution/types'
@@ -570,7 +569,7 @@ const handleZoomClick = (asset: AssetItem) => {
const dialogStore = useDialogStore()
dialogStore.showDialog({
key: 'asset-3d-viewer',
title: getAssetDisplayName(asset),
title: asset.display_name || asset.name,
component: Load3dViewerContent,
props: {
modelUrl: asset.preview_url || ''

View File

@@ -1,213 +0,0 @@
<template>
<ComboboxRoot
v-model="modelValue"
v-model:open="isOpen"
ignore-filter
:disabled
:class="className"
>
<ComboboxAnchor
:class="
cn(
searchInputVariants({ size }),
disabled && 'pointer-events-none opacity-50'
)
"
@click="focus"
>
<Button
v-if="modelValue"
:class="cn('absolute', sizeConfig.clearPos)"
variant="textonly"
size="icon-sm"
:aria-label="$t('g.clear')"
@click.stop="clearSearch"
>
<i :class="cn('icon-[lucide--x]', sizeConfig.icon)" />
</Button>
<i
v-else-if="loading"
:class="
cn(
'pointer-events-none absolute icon-[lucide--loader-circle] animate-spin',
sizeConfig.iconPos,
sizeConfig.icon
)
"
/>
<i
v-else
:class="
cn(
'pointer-events-none absolute',
sizeConfig.iconPos,
sizeConfig.icon,
icon
)
"
/>
<ComboboxInput
ref="inputRef"
v-model="modelValue"
:class="
cn(
'size-full border-none bg-transparent outline-none',
sizeConfig.inputPl,
sizeConfig.inputText
)
"
:placeholder="placeholderText"
:auto-focus="autofocus"
@compositionstart="isComposing = true"
@compositionend="isComposing = false"
@keydown.enter="onEnterKey"
/>
</ComboboxAnchor>
<ComboboxContent
v-if="suggestions.length > 0"
position="popper"
:side-offset="4"
:class="
cn(
'z-50 max-h-60 w-(--reka-combobox-trigger-width) overflow-y-auto',
'rounded-lg border border-border-default bg-base-background p-1 shadow-lg'
)
"
>
<ComboboxItem
v-for="(suggestion, index) in suggestions"
:key="suggestionKey(suggestion, index)"
:value="suggestionValue(suggestion)"
:class="
cn(
'cursor-pointer rounded-sm px-3 py-2 text-sm outline-none',
'data-highlighted:bg-secondary-background-hover'
)
"
@select.prevent="onSelectSuggestion(suggestion)"
>
<slot name="suggestion" :suggestion>
{{ suggestionLabel(suggestion) }}
</slot>
</ComboboxItem>
</ComboboxContent>
</ComboboxRoot>
</template>
<script setup lang="ts" generic="T">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import {
ComboboxAnchor,
ComboboxContent,
ComboboxInput,
ComboboxItem,
ComboboxRoot
} from 'reka-ui'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import type { SearchInputVariants } from './searchInput.variants'
import {
searchInputSizeConfig,
searchInputVariants
} from './searchInput.variants'
const { t } = useI18n()
const {
placeholder,
icon = 'icon-[lucide--search]',
autofocus = false,
loading = false,
disabled = false,
size = 'md',
suggestions = [],
optionLabel,
optionKey,
class: className
} = defineProps<{
placeholder?: string
icon?: string
autofocus?: boolean
loading?: boolean
disabled?: boolean
size?: SearchInputVariants['size']
suggestions?: T[]
optionLabel?: keyof T & string
optionKey?: keyof T & string
class?: HTMLAttributes['class']
}>()
const emit = defineEmits<{
select: [item: T]
}>()
const sizeConfig = computed(() => searchInputSizeConfig[size])
const modelValue = defineModel<string>({ required: true })
const inputRef = ref<InstanceType<typeof ComboboxInput> | null>(null)
const isOpen = ref(false)
const isComposing = ref(false)
function focus() {
inputRef.value?.$el?.focus()
}
defineExpose({ focus })
const placeholderText = computed(
() => placeholder ?? t('g.searchPlaceholder', { subject: '' })
)
function clearSearch() {
modelValue.value = ''
focus()
}
function getItemProperty(item: T, key: keyof T & string): string {
if (typeof item === 'object' && item !== null) {
return String(item[key])
}
return String(item)
}
function suggestionLabel(item: T): string {
if (optionLabel) return getItemProperty(item, optionLabel)
return String(item)
}
function suggestionKey(item: T, index: number): string {
if (optionKey) return getItemProperty(item, optionKey)
return `${suggestionLabel(item)}-${index}`
}
function suggestionValue(item: T): string {
return suggestionLabel(item)
}
function onSelectSuggestion(item: T) {
modelValue.value = suggestionLabel(item)
isOpen.value = false
emit('select', item)
}
function onEnterKey(e: KeyboardEvent) {
if (isComposing.value) {
e.preventDefault()
e.stopPropagation()
}
}
watch(
() => suggestions,
(items) => {
isOpen.value = items.length > 0 && !!modelValue.value
}
)
</script>

View File

@@ -1,5 +1,10 @@
<template>
<ComboboxRoot :open="false" ignore-filter :disabled :class="className">
<ComboboxRoot
:ignore-filter="true"
:open="false"
:disabled="disabled"
:class="className"
>
<ComboboxAnchor
:class="
cn(

View File

@@ -64,7 +64,6 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
)
"
>
<slot name="prepend" />
<SelectScrollUpButton />
<SelectViewport
:class="

View File

@@ -25,7 +25,7 @@
:label="String(badge)"
severity="contrast"
variant="circle"
class="ml-auto min-h-5 min-w-5 px-1 text-base-background"
class="ml-auto"
/>
</div>
</template>

View File

@@ -324,8 +324,7 @@ function safeWidgetMapper(
}
: (extractWidgetDisplayOptions(effectiveWidget) ?? options),
slotMetadata: slotInfo,
slotName: name !== widget.name ? widget.name : undefined,
tooltip: widget.tooltip
slotName: name !== widget.name ? widget.name : undefined
}
} catch (error) {
return {

View File

@@ -924,8 +924,7 @@ export function useBrushDrawing(initialSettings?: {
}
// Calculate target spacing based on step size percentage
const stepPercentage =
Math.pow(100, store.brushSettings.stepSize / 100) / 100
const stepPercentage = store.brushSettings.stepSize / 100
const targetSpacing = Math.max(
1.0,
store.brushSettings.size * stepPercentage
@@ -1484,8 +1483,7 @@ export function useBrushDrawing(initialSettings?: {
const dist = Math.hypot(p2.x - p1.x, p2.y - p1.y)
// Calculate target spacing based on stepSize
const stepPercentage =
Math.pow(100, store.brushSettings.stepSize / 100) / 100
const stepPercentage = store.brushSettings.stepSize / 100
const stepSize = Math.max(
1.0,
store.brushSettings.size * stepPercentage

View File

@@ -1,60 +0,0 @@
import { computed, reactive } from 'vue'
import EditKeybindingContent from '@/components/dialog/content/setting/keybinding/EditKeybindingContent.vue'
import EditKeybindingFooter from '@/components/dialog/content/setting/keybinding/EditKeybindingFooter.vue'
import EditKeybindingHeader from '@/components/dialog/content/setting/keybinding/EditKeybindingHeader.vue'
import type { KeyComboImpl } from '@/platform/keybindings/keyCombo'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useDialogService } from '@/services/dialogService'
export const DIALOG_KEY = 'edit-keybinding'
export interface EditKeybindingDialogState {
commandId: string
newCombo: KeyComboImpl | null
currentCombo: KeyComboImpl | null
}
export function useEditKeybindingDialog() {
const { showSmallLayoutDialog } = useDialogService()
const keybindingStore = useKeybindingStore()
function show(options: {
commandId: string
commandLabel: string
currentCombo: KeyComboImpl | null
}) {
const dialogState = reactive<EditKeybindingDialogState>({
commandId: options.commandId,
newCombo: options.currentCombo,
currentCombo: options.currentCombo
})
const existingKeybindingOnCombo = computed(() => {
if (!dialogState.newCombo) return null
if (dialogState.currentCombo?.equals(dialogState.newCombo)) return null
return keybindingStore.getKeybinding(dialogState.newCombo)
})
function onUpdateCombo(combo: KeyComboImpl) {
dialogState.newCombo = combo
}
showSmallLayoutDialog({
key: DIALOG_KEY,
headerComponent: EditKeybindingHeader,
footerComponent: EditKeybindingFooter,
component: EditKeybindingContent,
props: {
dialogState,
onUpdateCombo,
commandLabel: options.commandLabel,
existingKeybindingOnCombo
},
headerProps: {},
footerProps: { dialogState, existingKeybindingOnCombo }
})
}
return { show }
}

View File

@@ -357,8 +357,7 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
}
/**
* Initialize viewer in standalone mode (for asset preview).
* Creates the Load3d instance once; subsequent calls reuse it.
* Initialize viewer in standalone mode (for asset preview)
*/
const initializeStandaloneViewer = async (
containerRef: HTMLElement,
@@ -367,11 +366,6 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
if (!containerRef) return
try {
if (load3d) {
await loadStandaloneModel(modelUrl)
return
}
isStandaloneMode.value = true
load3d = new Load3d(containerRef, {
@@ -398,23 +392,6 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
setupAnimationEvents()
} catch (error) {
console.error('Error initializing standalone 3D viewer:', error)
useToastStore().addAlert(t('toastMessages.failedToLoadModel'))
}
}
/**
* Load a new model into an existing standalone viewer,
* reusing the same WebGLRenderer.
*/
const loadStandaloneModel = async (modelUrl: string) => {
if (!load3d) return
try {
await load3d.loadModel(modelUrl)
isSplatModel.value = load3d.isSplatModel()
isPlyModel.value = load3d.isPlyModel()
} catch (error) {
console.error('Error loading model in standalone viewer:', error)
useToastStore().addAlert('Failed to load 3D model')
}
}

View File

@@ -595,34 +595,6 @@ describe('usePaste', () => {
)
})
})
it('should skip node metadata paste when a media node is selected', async () => {
const mockNode = createMockLGraphNode({
is_selected: true,
pasteFile: vi.fn(),
pasteFiles: vi.fn()
})
mockCanvas.current_node = mockNode
vi.mocked(isImageNode).mockReturnValue(true)
usePaste()
const nodeData = { nodes: [{ type: 'KSampler' }] }
const encoded = btoa(JSON.stringify(nodeData))
const html = `<div data-metadata="${encoded}"></div>`
const dataTransfer = new DataTransfer()
dataTransfer.setData('text/html', html)
dataTransfer.setData('text/plain', 'some text')
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
document.dispatchEvent(event)
await vi.waitFor(() => {
expect(mockCanvas._deserializeItems).not.toHaveBeenCalled()
expect(mockCanvas.pasteFromClipboard).toHaveBeenCalled()
})
})
})
describe('cloneDataTransfer', () => {

View File

@@ -229,10 +229,7 @@ export const usePaste = () => {
return
}
}
const isMediaNodeSelected =
isImageNodeSelected || isVideoNodeSelected || isAudioNodeSelected
if (!isMediaNodeSelected && pasteClipboardItems(data)) return
if (pasteClipboardItems(data)) return
// No image found. Look for node data
data = data.getData('text/plain')

View File

@@ -1,35 +0,0 @@
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import { zAutogrowOptions, zMatchTypeOptions } from '@/schemas/nodeDefSchema'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
const dynamicTypeResolvers: Record<
string,
(inputSpec: InputSpecV2) => string[]
> = {
COMFY_AUTOGROW_V3: resolveAutogrowType,
COMFY_MATCHTYPE_V3: (input) =>
zMatchTypeOptions
.safeParse(input)
.data?.template?.allowed_types?.split(',') ?? []
}
export function resolveInputType(input: InputSpecV2): string[] {
return input.type in dynamicTypeResolvers
? dynamicTypeResolvers[input.type](input)
: input.type.split(',')
}
function resolveAutogrowType(rawSpec: InputSpecV2): string[] {
const { input } = zAutogrowOptions.safeParse(rawSpec).data?.template ?? {}
const inputTypes: (Record<string, InputSpec> | undefined)[] = [
input?.required,
input?.optional
]
return inputTypes.flatMap((inputType) =>
Object.entries(inputType ?? {}).flatMap(([name, v]) =>
resolveInputType(transformInputSpecV1ToV2(v, { name }))
)
)
}

View File

@@ -27,7 +27,7 @@ function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
`${namePrefix}.${depth}.${inputIndex}`,
Array.isArray(input)
? ['COMFY_DYNAMICCOMBO_V3', { options: getSpec(input, depth + 1) }]
: [input, { tooltip: `${groupIndex}` }]
: [input, {}]
])
return {
key: `${groupIndex}`,
@@ -106,13 +106,6 @@ describe('Dynamic Combos', () => {
expect(node.inputs[1].name).toBe('0.0.0.0')
expect(node.inputs[3].name).toBe('2.2.0.0')
})
test('Dynamically added widgets have tooltips', () => {
const node = testNode()
addDynamicCombo(node, [['INT'], ['STRING']])
expect.soft(node.widgets[1].tooltip).toBe('0')
node.widgets[0].value = '1'
expect.soft(node.widgets[1].tooltip).toBe('1')
})
})
describe('Autogrow', () => {
const inputsSpec = { required: { image: ['IMAGE', {}] } }

View File

@@ -16,8 +16,7 @@ import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import {
zAutogrowOptions,
zDynamicComboInputSpec,
zMatchTypeOptions
zDynamicComboInputSpec
} from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
import { app } from '@/scripts/app'
@@ -216,7 +215,6 @@ export function applyDynamicInputs(
dynamicInputs[inputSpec.type](node, inputSpec)
return true
}
function spliceInputs(
node: LGraphNode,
startIndex: number,
@@ -331,10 +329,11 @@ function withComfyMatchType(node: LGraphNode): asserts node is MatchTypeNode {
function applyMatchType(node: LGraphNode, inputSpec: InputSpecV2) {
const { addNodeInput } = useLitegraphService()
const name = inputSpec.name
const matchTypeSpec = zMatchTypeOptions.safeParse(inputSpec).data
if (!matchTypeSpec) return
const { allowed_types, template_id } = matchTypeSpec.template
const { allowed_types, template_id } = (
inputSpec as InputSpecV2 & {
template: { allowed_types: string; template_id: string }
}
).template
const typedSpec = { ...inputSpec, type: allowed_types }
addNodeInput(node, typedSpec)
withComfyMatchType(node)

View File

@@ -936,8 +936,6 @@
"batchRename": "إعادة تسمية جماعية",
"beta": "نسخة تجريبية",
"bookmark": "حفظ في المكتبة",
"browserReservedKeybinding": "هذا الاختصار محجوز من قبل بعض المتصفحات وقد يؤدي إلى نتائج غير متوقعة.",
"browserReservedKeybindingTooltip": "هذا الاختصار يتعارض مع اختصارات المتصفح المحجوزة",
"calculatingDimensions": "جارٍ حساب الأبعاد",
"cancel": "إلغاء",
"cancelled": "أُلغي",
@@ -972,7 +970,6 @@
"copy": "نسخ",
"copyAll": "نسخ الكل",
"copyJobId": "نسخ معرف المهمة",
"copySystemInfo": "نسخ معلومات النظام",
"copyToClipboard": "نسخ إلى الحافظة",
"copyURL": "نسخ الرابط",
"core": "النواة",
@@ -1019,7 +1016,6 @@
"enterNewName": "أدخل الاسم الجديد",
"enterNewNamePrompt": "أدخل اسمًا جديدًا:",
"enterSubgraph": "دخول الرسم البياني الفرعي",
"enterYourKeybind": "أدخل اختصارك",
"error": "خطأ",
"errorLoadingImage": "حدث خطأ أثناء تحميل الصورة",
"errorLoadingVideo": "حدث خطأ أثناء تحميل الفيديو",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "تم رفض إذن الميكروفون",
"migrate": "ترحيل",
"missing": "مفقود",
"modifyKeybinding": "تعديل اختصار لوحة المفاتيح",
"more": "المزيد",
"moreOptions": "خيارات إضافية",
"moreWorkflows": "المزيد من سير العمل",
@@ -1098,7 +1093,6 @@
"name": "الاسم",
"newFolder": "مجلد جديد",
"next": "التالي",
"nextImage": "الصورة التالية",
"nightly": "NIGHTLY",
"no": "لا",
"noAudioRecorded": "لم يتم تسجيل أي صوت",
@@ -1132,8 +1126,8 @@
"playRecording": "تشغيل التسجيل",
"playbackSpeed": "سرعة التشغيل",
"playing": "جاري التشغيل",
"pressKeysForNewBinding": "اضغط على المفاتيح لربط جديد",
"preview": "معاينة",
"previousImage": "الصورة السابقة",
"profile": "الملف الشخصي",
"progressCountOf": "من",
"queued": "في قائمة الانتظار",
@@ -1173,7 +1167,6 @@
"resultsCount": "تم العثور على {count} نتيجة",
"running": "يعمل",
"save": "حفظ",
"saveAnyway": "احفظ على أي حال",
"saving": "جارٍ الحفظ",
"scrollLeft": "التمرير لليسار",
"scrollRight": "التمرير لليمين",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "حدد العناصر لعمل نسخة",
"selectItemsToRename": "حدد العناصر لإعادة التسمية",
"selectedFile": "الملف المحدد",
"setAKeybindingForTheFollowing": "تعيين اختصار لوحة المفاتيح لما يلي:",
"setAsBackground": "تعيين كخلفية",
"settings": "الإعدادات",
"shortcutSuffix": " ({shortcut})",
@@ -1208,8 +1200,6 @@
"stopRecording": "إيقاف التسجيل",
"submit": "إرسال",
"success": "نجاح",
"switchToGridView": "التبديل إلى عرض الشبكة",
"switchToSingleView": "التبديل إلى العرض الفردي",
"systemInfo": "معلومات النظام",
"terminal": "الطرفية",
"title": "العنوان",
@@ -1239,8 +1229,6 @@
"you": "أنت"
},
"graphCanvasMenu": {
"canvasMode": "وضع اللوحة",
"canvasToolbar": "شريط أدوات اللوحة",
"fitView": "ملائمة العرض",
"focusMode": "وضع التركيز",
"hand": "يد",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "اسحب وأسقط صورة",
"emptyWorkflowExplanation": "سير العمل الخاص بك فارغ. تحتاج إلى بعض العقد أولاً لبدء بناء التطبيق.",
"enterNodeGraph": "دخول مخطط العقد",
"error": {
"getHelp": "للمساعدة، راجع {0} أو {1} أو {2} مع تقرير الخطأ المنسوخ.",
"github": "إرسال مشكلة على GitHub",
"goto": "عرض الأخطاء في المخطط",
"guide": "دليل استكشاف الأخطاء",
"header": "حدث خطأ في هذا التطبيق",
"log": "سجلات الأخطاء",
"mobileFixable": "تحقق من {0} للأخطاء",
"promptShow": "عرض تقرير الخطأ",
"promptVisitGraph": "اعرض مخطط العُقد لرؤية الخطأ بالكامل.",
"requiresGraph": "حدث خطأ أثناء التوليد. قد يكون ذلك بسبب مدخلات مخفية غير صالحة، أو موارد مفقودة، أو مشاكل في إعداد سير العمل.",
"support": "الاتصال بالدعم"
},
"giveFeedback": "إعطاء ملاحظات",
"graphMode": "وضع الرسم البياني",
"hasCreditCost": "يتطلب أرصدة إضافية",
"linearMode": "وضع التطبيق",
"loadTemplate": "تحميل قالب",
"mobileControls": "تعديل وتشغيل",
"mobileNoWorkflow": "لم يتم بناء سير العمل هذا لوضع التطبيق. جرب سير عمل آخر.",
"queue": {
"clear": "مسح قائمة الانتظار",
"clickToClear": "انقر لمسح قائمة الانتظار"
@@ -1504,7 +1478,6 @@
"rerun": "تشغيل مجدد",
"reuseParameters": "إعادة استخدام المعلمات",
"runCount": "عدد مرات التشغيل:",
"viewGraph": "عرض مخطط العُقد",
"viewJob": "عرض المهمة",
"welcome": {
"buildApp": "إنشاء تطبيق",
@@ -1713,7 +1686,6 @@
"installedSection": "المثبتة",
"missingNodes": "عقد مفقودة",
"notInstalled": "غير مثبت",
"unresolvedNodes": "العُقد غير المحلولة",
"updatesAvailable": "تحديثات متوفرة"
},
"nightlyVersion": "ليلي",
@@ -1760,10 +1732,6 @@
"uninstall": "إلغاء التثبيت",
"uninstallSelected": "إلغاء تثبيت المحدد",
"uninstalling": "جاري إلغاء التثبيت",
"unresolvedNodes": {
"message": "العُقد التالية غير مثبتة ولم يتم العثور عليها في السجل.",
"title": "العُقد المفقودة غير المحلولة"
},
"update": "تحديث",
"updateAll": "تحديث الكل",
"updateSelected": "تحديث المحدد",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "رودان",
"Runway": "رن واي",
"Sora": "سورا",
@@ -2412,33 +2379,6 @@
"inputsNone": "لا توجد مدخلات",
"inputsNoneTooltip": "العقدة ليس لديها مدخلات",
"locateNode": "تحديد موقع العقدة على اللوحة",
"missingModels": {
"alreadyExistsInCategory": "هذا النموذج موجود بالفعل في \"{category}\"",
"assetLoadTimeout": "انتهت مهلة اكتشاف النموذج. حاول إعادة تحميل سير العمل.",
"cancelSelection": "إلغاء الاختيار",
"clearUrl": "مسح الرابط",
"collapseNodes": "إخفاء العُقد المرجعية",
"confirmSelection": "تأكيد الاختيار",
"copyModelName": "نسخ اسم النموذج",
"customNodeDownloadDisabled": "بيئة السحابة لا تدعم استيراد النماذج للعُقد المخصصة في هذا القسم. يرجى استخدام عُقد التحميل القياسية أو استبدالها بنموذج من المكتبة أدناه.",
"expandNodes": "عرض العُقد المرجعية",
"import": "استيراد",
"importAnyway": "استيراد على أي حال",
"importFailed": "فشل الاستيراد",
"importNotSupported": "الاستيراد غير مدعوم",
"imported": "تم الاستيراد",
"importing": "جارٍ الاستيراد...",
"locateNode": "تحديد موقع العُقدة على اللوحة",
"metadataFetchFailed": "فشل في جلب البيانات الوصفية. يرجى التحقق من الرابط والمحاولة مرة أخرى.",
"missingModelsTitle": "النماذج المفقودة",
"or": "أو",
"typeMismatch": "يبدو أن هذا النموذج هو \"{detectedType}\". هل أنت متأكد؟",
"unknownCategory": "غير معروف",
"unsupportedUrl": "يتم دعم روابط Civitai و Hugging Face فقط.",
"urlPlaceholder": "الصق رابط النموذج (Civitai أو Hugging Face)",
"useFromLibrary": "استخدم من المكتبة",
"usingFromLibrary": "يتم الاستخدام من المكتبة"
},
"missingNodePacks": {
"applyChanges": "تطبيق التغييرات",
"cloudMessage": "يتطلب سير العمل هذا عقدًا مخصصة غير متوفرة بعد على Comfy Cloud.",
@@ -3298,7 +3238,6 @@
"interrupted": "تم إيقاف التنفيذ",
"legacyMaskEditorDeprecated": "محرر القناع القديم لم يعد مدعوماً وسيتم إزالته قريباً.",
"migrateToLitegraphReroute": "سيتم إزالة عقد إعادة التوجيه في الإصدارات المستقبلية. انقر للترحيل إلى إعادة التوجيه الأصلية في Litegraph.",
"missingModelVerificationFailed": "فشل التحقق من النماذج المفقودة. قد لا تظهر بعض النماذج في علامة تبويب الأخطاء.",
"modelLoadedSuccessfully": "تم تحميل النموذج ثلاثي الأبعاد بنجاح",
"no3dScene": "لا يوجد مشهد ثلاثي الأبعاد لتطبيق الخامة",
"no3dSceneToExport": "لا يوجد مشهد ثلاثي الأبعاد للتصدير",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "يُمكّن تحسين KV Cache لصور المرجع على نماذج عائلة Flux.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "النموذج",
"tooltip": "النموذج الذي سيتم تفعيل KV Cache عليه."
}
},
"outputs": {
"0": {
"tooltip": "النموذج المعدّل مع تفعيل KV Cache."
}
}
},
"FluxKontextImageScale": {
"description": "تعيد هذه العقدة ضبط حجم الصورة إلى حجم أكثر ملاءمة لـ flux kontext.",
"display_name": "FluxKontextImageScale",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "توليد الصور من أوصاف نصية باستخدام Reve.",
"display_name": "إنشاء صورة Reve",
"inputs": {
"control_after_generate": {
"name": "التحكم بعد التوليد"
},
"model": {
"name": "النموذج",
"tooltip": "إصدار النموذج المستخدم في التوليد."
},
"model_aspect_ratio": {
"name": "نسبة الأبعاد"
},
"model_test_time_scaling": {
"name": "اختبار التحجيم الزمني"
},
"prompt": {
"name": "الوصف النصي",
"tooltip": "الوصف النصي للصورة المطلوبة. الحد الأقصى ٢٥٦٠ حرفًا."
},
"remove_background": {
"name": "إزالة الخلفية",
"tooltip": "إزالة الخلفية من الصورة المولدة. قد يضيف تكلفة إضافية."
},
"seed": {
"name": "البذرة",
"tooltip": "تتحكم البذرة فيما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
},
"upscale": {
"name": "تكبير",
"tooltip": "تكبير الصورة المولدة. قد يضيف تكلفة إضافية."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "تعديل الصور باستخدام تعليمات لغة طبيعية مع Reve.",
"display_name": "تعديل صورة Reve",
"inputs": {
"control_after_generate": {
"name": "التحكم بعد التوليد"
},
"edit_instruction": {
"name": "تعليمات التعديل",
"tooltip": "الوصف النصي لكيفية تعديل الصورة. الحد الأقصى ٢٥٦٠ حرفًا."
},
"image": {
"name": "الصورة",
"tooltip": "الصورة المراد تعديلها."
},
"model": {
"name": "النموذج",
"tooltip": "إصدار النموذج المستخدم في التعديل."
},
"model_aspect_ratio": {
"name": "نسبة الأبعاد"
},
"model_test_time_scaling": {
"name": "اختبار التحجيم الزمني"
},
"remove_background": {
"name": "إزالة الخلفية",
"tooltip": "إزالة الخلفية من الصورة المولدة. قد يضيف تكلفة إضافية."
},
"seed": {
"name": "البذرة",
"tooltip": "تتحكم البذرة فيما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
},
"upscale": {
"name": "تكبير",
"tooltip": "تكبير الصورة المولدة. قد يضيف تكلفة إضافية."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "دمج الصور المرجعية مع الأوصاف النصية لإنشاء صور جديدة باستخدام Reve.",
"display_name": "ريمكس صورة Reve",
"inputs": {
"control_after_generate": {
"name": "التحكم بعد التوليد"
},
"model": {
"name": "النموذج",
"tooltip": "إصدار النموذج المستخدم في الريمكس."
},
"model_aspect_ratio": {
"name": "نسبة الأبعاد"
},
"model_test_time_scaling": {
"name": "اختبار التحجيم الزمني"
},
"prompt": {
"name": "الوصف النصي",
"tooltip": "الوصف النصي للصورة المطلوبة. يمكن أن يتضمن وسوم XML للصور للإشارة إلى صور محددة حسب الفهرس، مثل <img>0</img>، <img>1</img>، إلخ."
},
"reference_images": {
"name": "الصور المرجعية"
},
"remove_background": {
"name": "إزالة الخلفية",
"tooltip": "إزالة الخلفية من الصورة المولدة. قد يضيف تكلفة إضافية."
},
"seed": {
"name": "البذرة",
"tooltip": "تتحكم البذرة فيما إذا كان يجب إعادة تشغيل العقدة؛ النتائج غير حتمية بغض النظر عن البذرة."
},
"upscale": {
"name": "تكبير",
"tooltip": "تكبير الصورة المولدة. قد يضيف تكلفة إضافية."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "توليد أصول ثلاثية الأبعاد باستخدام واجهة برمجة تطبيقات رودين",
"display_name": "رودين 3D توليد - توليد التفاصيل",

View File

@@ -35,10 +35,6 @@
"videoPreview": "Video preview - Use arrow keys to navigate between videos",
"galleryImage": "Gallery image",
"galleryThumbnail": "Gallery thumbnail",
"previousImage": "Previous image",
"nextImage": "Next image",
"switchToGridView": "Switch to grid view",
"switchToSingleView": "Switch to single view",
"errorLoadingImage": "Error loading image",
"errorLoadingVideo": "Error loading video",
"failedToDownloadImage": "Failed to download image",
@@ -97,7 +93,6 @@
"reportIssueTooltip": "Submit the error report to Comfy Org",
"reportSent": "Report Submitted",
"copyToClipboard": "Copy to Clipboard",
"copySystemInfo": "Copy System Info",
"copyAll": "Copy All",
"openNewIssue": "Open New Issue",
"showReport": "Show Report",
@@ -110,7 +105,6 @@
"delete": "Delete",
"rename": "Rename",
"save": "Save",
"saveAnyway": "Save Anyway",
"saving": "Saving",
"no": "No",
"cancel": "Cancel",
@@ -121,6 +115,7 @@
"showRightPanel": "Show right panel",
"hideRightPanel": "Hide right panel",
"or": "or",
"pressKeysForNewBinding": "Press keys for new binding",
"defaultBanner": "default banner",
"enableOrDisablePack": "Enable or disable pack",
"openManager": "Open Manager",
@@ -265,11 +260,6 @@
"multiSelectDropdown": "Multi-select dropdown",
"singleSelectDropdown": "Single-select dropdown",
"progressCountOf": "of",
"modifyKeybinding": "Modify keybinding",
"setAKeybindingForTheFollowing": "Set a keybinding for the following:",
"enterYourKeybind": "Enter your keybind",
"browserReservedKeybinding": "This shortcut is reserved by some browsers and may have unexpected results.",
"browserReservedKeybindingTooltip": "This shortcut conflicts with browser-reserved shortcuts",
"keybindingAlreadyExists": "Keybinding already exists on",
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information.",
"startRecording": "Start Recording",
@@ -349,8 +339,7 @@
"conflicting": "Conflicting",
"inWorkflowSection": "IN WORKFLOW",
"allInWorkflow": "All in: {workflowName}",
"missingNodes": "Missing Nodes",
"unresolvedNodes": "Unresolved Nodes"
"missingNodes": "Missing Nodes"
},
"infoPanelEmpty": "Click an item to see the info",
"applyChanges": "Apply Changes",
@@ -415,10 +404,6 @@
"noDescription": "No description available",
"installSelected": "Install Selected",
"installAllMissingNodes": "Install All",
"unresolvedNodes": {
"title": "Unresolved Missing Nodes",
"message": "The following nodes are not installed and could not be found in the registry."
},
"allMissingNodesInstalled": "All missing nodes have been successfully installed",
"packsSelected": "packs selected",
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
@@ -1061,8 +1046,6 @@
"logoProviderSeparator": " & "
},
"graphCanvasMenu": {
"canvasMode": "Canvas Mode",
"canvasToolbar": "Canvas Toolbar",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"resetView": "Reset View",
@@ -1632,7 +1615,6 @@
"unet": "unet",
"sigmas": "sigmas",
"BFL": "BFL",
"": "",
"Gemini": "Gemini",
"video_models": "video_models",
"gligen": "gligen",
@@ -1667,7 +1649,6 @@
"primitive": "primitive",
"Recraft": "Recraft",
"edit_models": "edit_models",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"animation": "animation",
@@ -1681,6 +1662,7 @@
"style_model": "style_model",
"Tencent": "Tencent",
"textgen": "textgen",
"": "",
"Topaz": "Topaz",
"Tripo": "Tripo",
"Veo": "Veo",
@@ -1967,7 +1949,6 @@
"exportSuccess": "Successfully exported model as {format}",
"fileLoadError": "Unable to find workflow in {fileName}",
"dropFileError": "Unable to process dropped item: {error}",
"missingModelVerificationFailed": "Failed to verify missing models. Some models may not be shown in the Errors tab.",
"interrupted": "Execution has been interrupted",
"pendingTasksDeleted": "Pending tasks deleted",
"nothingToGroup": "Nothing to group",
@@ -3399,33 +3380,6 @@
"viewInManager": "View in Manager",
"collapse": "Collapse",
"expand": "Expand"
},
"missingModels": {
"urlPlaceholder": "Paste Model URL (Civitai or Hugging Face)",
"or": "OR",
"useFromLibrary": "Use from Library",
"usingFromLibrary": "Using from Library",
"unsupportedUrl": "Only Civitai and Hugging Face URLs are supported.",
"metadataFetchFailed": "Failed to retrieve metadata. Please check the link and try again.",
"import": "Import",
"importing": "Importing...",
"imported": "Imported",
"importFailed": "Import failed",
"typeMismatch": "This model seems to be a \"{detectedType}\". Are you sure?",
"importAnyway": "Import Anyway",
"alreadyExistsInCategory": "This model already exists in \"{category}\"",
"customNodeDownloadDisabled": "Cloud environment does not support model imports for custom nodes in this section. Please use standard loader nodes or substitute with a model from the library below.",
"importNotSupported": "Import Not Supported",
"copyModelName": "Copy model name",
"confirmSelection": "Confirm selection",
"locateNode": "Locate node on canvas",
"cancelSelection": "Cancel selection",
"clearUrl": "Clear URL",
"expandNodes": "Show referencing nodes",
"collapseNodes": "Hide referencing nodes",
"unknownCategory": "Unknown",
"missingModelsTitle": "Missing Models",
"assetLoadTimeout": "Model detection timed out. Try reloading the workflow."
}
},
"errorOverlay": {

View File

@@ -3319,21 +3319,6 @@
}
}
},
"FluxKVCache": {
"display_name": "Flux KV Cache",
"description": "Enables KV Cache optimization for reference images on Flux family models.",
"inputs": {
"model": {
"name": "model",
"tooltip": "The model to use KV Cache on."
}
},
"outputs": {
"0": {
"tooltip": "The patched model with KV Cache enabled."
}
}
},
"FluxProExpandNode": {
"display_name": "Flux.1 Expand Image",
"description": "Outpaints image based on prompt.",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"display_name": "Reve Image Create",
"description": "Generate images from text descriptions using Reve.",
"inputs": {
"prompt": {
"name": "prompt",
"tooltip": "Text description of the desired image. Maximum 2560 characters."
},
"model": {
"name": "model",
"tooltip": "Model version to use for generation."
},
"upscale": {
"name": "upscale",
"tooltip": "Upscale the generated image. May add additional cost."
},
"remove_background": {
"name": "remove_background",
"tooltip": "Remove the background from the generated image. May add additional cost."
},
"seed": {
"name": "seed",
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
},
"control_after_generate": {
"name": "control after generate"
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"display_name": "Reve Image Edit",
"description": "Edit images using natural language instructions with Reve.",
"inputs": {
"image": {
"name": "image",
"tooltip": "The image to edit."
},
"edit_instruction": {
"name": "edit_instruction",
"tooltip": "Text description of how to edit the image. Maximum 2560 characters."
},
"model": {
"name": "model",
"tooltip": "Model version to use for editing."
},
"upscale": {
"name": "upscale",
"tooltip": "Upscale the generated image. May add additional cost."
},
"remove_background": {
"name": "remove_background",
"tooltip": "Remove the background from the generated image. May add additional cost."
},
"seed": {
"name": "seed",
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
},
"control_after_generate": {
"name": "control after generate"
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"display_name": "Reve Image Remix",
"description": "Combine reference images with text prompts to create new images using Reve.",
"inputs": {
"reference_images": {
"name": "reference_images"
},
"prompt": {
"name": "prompt",
"tooltip": "Text description of the desired image. May include XML img tags to reference specific images by index, e.g. <img>0</img>, <img>1</img>, etc."
},
"model": {
"name": "model",
"tooltip": "Model version to use for remixing."
},
"upscale": {
"name": "upscale",
"tooltip": "Upscale the generated image. May add additional cost."
},
"remove_background": {
"name": "remove_background",
"tooltip": "Remove the background from the generated image. May add additional cost."
},
"seed": {
"name": "seed",
"tooltip": "Seed controls whether the node should re-run; results are non-deterministic regardless of seed."
},
"control_after_generate": {
"name": "control after generate"
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"display_name": "Rodin 3D Generate - Detail Generate",
"description": "Generate 3D Assets using Rodin API",

View File

@@ -348,8 +348,8 @@
"tooltip": "The maximum number of tasks that show in the queue history."
},
"Comfy_Queue_QPOV2": {
"name": "Docked job history/queue panel",
"tooltip": "Replaces the floating job queue panel with an equivalent job queue embedded in the job history side panel. You can disable this to return to the floating panel layout."
"name": "Use the unified job queue in the Assets side panel",
"tooltip": "Replaces the floating job queue panel with an equivalent job queue embedded in the Assets side panel. You can disable this to return to the floating panel layout."
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Batch count limit",

View File

@@ -936,8 +936,6 @@
"batchRename": "Renombrar en lote",
"beta": "BETA",
"bookmark": "Guardar en Biblioteca",
"browserReservedKeybinding": "Este atajo está reservado por algunos navegadores y puede tener resultados inesperados.",
"browserReservedKeybindingTooltip": "Este atajo entra en conflicto con los atajos reservados del navegador",
"calculatingDimensions": "Calculando dimensiones",
"cancel": "Cancelar",
"cancelled": "Cancelado",
@@ -972,7 +970,6 @@
"copy": "Copiar",
"copyAll": "Copiar todo",
"copyJobId": "Copiar ID de trabajo",
"copySystemInfo": "Copiar información del sistema",
"copyToClipboard": "Copiar al portapapeles",
"copyURL": "Copiar URL",
"core": "Núcleo",
@@ -1019,7 +1016,6 @@
"enterNewName": "Introduce el nuevo nombre",
"enterNewNamePrompt": "Introduce un nuevo nombre:",
"enterSubgraph": "Entrar en subgrafo",
"enterYourKeybind": "Introduce tu atajo de teclado",
"error": "Error",
"errorLoadingImage": "Error al cargar imagen",
"errorLoadingVideo": "Error al cargar video",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "Permiso de micrófono denegado",
"migrate": "Migrar",
"missing": "Faltante",
"modifyKeybinding": "Modificar atajo de teclado",
"more": "Más",
"moreOptions": "Más Opciones",
"moreWorkflows": "Más flujos de trabajo",
@@ -1098,7 +1093,6 @@
"name": "Nombre",
"newFolder": "Nueva carpeta",
"next": "Siguiente",
"nextImage": "Imagen siguiente",
"nightly": "NIGHTLY",
"no": "No",
"noAudioRecorded": "No se grabó audio",
@@ -1132,8 +1126,8 @@
"playRecording": "Reproducir grabación",
"playbackSpeed": "Velocidad de reproducción",
"playing": "Reproduciendo",
"pressKeysForNewBinding": "Presiona teclas para nueva asignación",
"preview": "VISTA PREVIA",
"previousImage": "Imagen anterior",
"profile": "Perfil",
"progressCountOf": "de",
"queued": "En cola",
@@ -1173,7 +1167,6 @@
"resultsCount": "Encontrados {count} resultados",
"running": "En ejecución",
"save": "Guardar",
"saveAnyway": "Guardar de todos modos",
"saving": "Guardando",
"scrollLeft": "Desplazar a la izquierda",
"scrollRight": "Desplazar a la derecha",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "Selecciona elementos para duplicar",
"selectItemsToRename": "Selecciona elementos para renombrar",
"selectedFile": "Archivo seleccionado",
"setAKeybindingForTheFollowing": "Establecer un atajo de teclado para lo siguiente:",
"setAsBackground": "Establecer como fondo",
"settings": "Configuraciones",
"shortcutSuffix": " ({shortcut})",
@@ -1208,8 +1200,6 @@
"stopRecording": "Detener grabación",
"submit": "Enviar",
"success": "Éxito",
"switchToGridView": "Cambiar a vista de cuadrícula",
"switchToSingleView": "Cambiar a vista individual",
"systemInfo": "Información del sistema",
"terminal": "Terminal",
"title": "Título",
@@ -1239,8 +1229,6 @@
"you": "Tú"
},
"graphCanvasMenu": {
"canvasMode": "Modo lienzo",
"canvasToolbar": "Barra de herramientas del lienzo",
"fitView": "Ajustar vista",
"focusMode": "Modo de Enfoque",
"hand": "Mano",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "Arrastra y suelta una imagen",
"emptyWorkflowExplanation": "Tu flujo de trabajo está vacío. Necesitas algunos nodos primero para empezar a construir una aplicación.",
"enterNodeGraph": "Entrar al grafo de nodos",
"error": {
"getHelp": "Para obtener ayuda, consulta nuestro {0}, {1} o {2} con el error copiado.",
"github": "enviar un issue en GitHub",
"goto": "Mostrar errores en el grafo",
"guide": "guía de solución de problemas",
"header": "Esta aplicación encontró un error",
"log": "Registros de errores",
"mobileFixable": "Revisa {0} para ver los errores",
"promptShow": "Mostrar informe de error",
"promptVisitGraph": "Ve el grafo de nodos para ver el error completo.",
"requiresGraph": "Algo salió mal durante la generación. Esto puede deberse a entradas ocultas no válidas, recursos faltantes o problemas de configuración del flujo de trabajo.",
"support": "contacta a nuestro soporte"
},
"giveFeedback": "Enviar comentarios",
"graphMode": "Modo gráfico",
"hasCreditCost": "Requiere créditos adicionales",
"linearMode": "Modo App",
"loadTemplate": "Cargar una plantilla",
"mobileControls": "Editar y ejecutar",
"mobileNoWorkflow": "Este flujo de trabajo no ha sido creado para el modo de aplicación. Prueba con otro.",
"queue": {
"clear": "Limpiar cola",
"clickToClear": "Haz clic para limpiar la cola"
@@ -1504,7 +1478,6 @@
"rerun": "Volver a ejecutar",
"reuseParameters": "Reutilizar parámetros",
"runCount": "Número de ejecuciones:",
"viewGraph": "Ver grafo de nodos",
"viewJob": "Ver tarea",
"welcome": {
"buildApp": "Crear aplicación",
@@ -1713,7 +1686,6 @@
"installedSection": "INSTALADO",
"missingNodes": "Nodos faltantes",
"notInstalled": "No instalado",
"unresolvedNodes": "Nodos no resueltos",
"updatesAvailable": "Actualizaciones disponibles"
},
"nightlyVersion": "Nocturna",
@@ -1760,10 +1732,6 @@
"uninstall": "Desinstalar",
"uninstallSelected": "Desinstalar Seleccionado",
"uninstalling": "Desinstalando",
"unresolvedNodes": {
"message": "Los siguientes nodos no están instalados y no se pudieron encontrar en el registro.",
"title": "Nodos faltantes no resueltos"
},
"update": "Actualizar",
"updateAll": "Actualizar Todos",
"updateSelected": "Actualizar Seleccionados",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2412,33 +2379,6 @@
"inputsNone": "SIN ENTRADAS",
"inputsNoneTooltip": "El nodo no tiene entradas",
"locateNode": "Localizar nodo en el lienzo",
"missingModels": {
"alreadyExistsInCategory": "Este modelo ya existe en \"{category}\"",
"assetLoadTimeout": "El tiempo de detección del modelo se agotó. Intenta recargar el flujo de trabajo.",
"cancelSelection": "Cancelar selección",
"clearUrl": "Borrar URL",
"collapseNodes": "Ocultar nodos de referencia",
"confirmSelection": "Confirmar selección",
"copyModelName": "Copiar nombre del modelo",
"customNodeDownloadDisabled": "El entorno en la nube no admite la importación de modelos para nodos personalizados en esta sección. Por favor, utiliza nodos estándar de carga o sustituye con un modelo de la biblioteca de abajo.",
"expandNodes": "Mostrar nodos de referencia",
"import": "Importar",
"importAnyway": "Importar de todos modos",
"importFailed": "Error al importar",
"importNotSupported": "Importación no soportada",
"imported": "Importado",
"importing": "Importando...",
"locateNode": "Localizar nodo en el lienzo",
"metadataFetchFailed": "No se pudo obtener los metadatos. Por favor, revisa el enlace e inténtalo de nuevo.",
"missingModelsTitle": "Modelos faltantes",
"or": "O",
"typeMismatch": "Este modelo parece ser un \"{detectedType}\". ¿Estás seguro?",
"unknownCategory": "Desconocido",
"unsupportedUrl": "Solo se admiten URLs de Civitai y Hugging Face.",
"urlPlaceholder": "Pega la URL del modelo (Civitai o Hugging Face)",
"useFromLibrary": "Usar de la biblioteca",
"usingFromLibrary": "Usando de la biblioteca"
},
"missingNodePacks": {
"applyChanges": "Aplicar cambios",
"cloudMessage": "Este flujo de trabajo requiere nodos personalizados que aún no están disponibles en Comfy Cloud.",
@@ -3298,7 +3238,6 @@
"interrupted": "La ejecución ha sido interrumpida",
"legacyMaskEditorDeprecated": "El editor de máscaras heredado está obsoleto y se eliminará pronto.",
"migrateToLitegraphReroute": "Los nodos de reroute se eliminarán en futuras versiones. Haz clic para migrar a reroute nativo de litegraph.",
"missingModelVerificationFailed": "No se pudo verificar los modelos faltantes. Algunos modelos pueden no aparecer en la pestaña de Errores.",
"modelLoadedSuccessfully": "Modelo 3D cargado exitosamente",
"no3dScene": "No hay escena 3D para aplicar textura",
"no3dSceneToExport": "No hay escena 3D para exportar",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "Activa la optimización de KV Cache para imágenes de referencia en modelos de la familia Flux.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "modelo",
"tooltip": "El modelo en el que se usará KV Cache."
}
},
"outputs": {
"0": {
"tooltip": "El modelo modificado con KV Cache activado."
}
}
},
"FluxKontextImageScale": {
"description": "Este nodo redimensiona la imagen a una más óptima para flux kontext.",
"display_name": "EscalaImagenFluxKontext",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "Genera imágenes a partir de descripciones de texto usando Reve.",
"display_name": "Reve Crear Imagen",
"inputs": {
"control_after_generate": {
"name": "control después de generar"
},
"model": {
"name": "modelo",
"tooltip": "Versión del modelo a utilizar para la generación."
},
"model_aspect_ratio": {
"name": "relación_de_aspecto"
},
"model_test_time_scaling": {
"name": "escalado_en_tiempo_de_prueba"
},
"prompt": {
"name": "prompt",
"tooltip": "Descripción en texto de la imagen deseada. Máximo 2560 caracteres."
},
"remove_background": {
"name": "eliminar_fondo",
"tooltip": "Eliminar el fondo de la imagen generada. Puede tener un costo adicional."
},
"seed": {
"name": "semilla",
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
},
"upscale": {
"name": "escalar",
"tooltip": "Aumentar la resolución de la imagen generada. Puede tener un costo adicional."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Edita imágenes usando instrucciones en lenguaje natural con Reve.",
"display_name": "Reve Editar Imagen",
"inputs": {
"control_after_generate": {
"name": "control después de generar"
},
"edit_instruction": {
"name": "instrucción_de_edición",
"tooltip": "Descripción en texto de cómo editar la imagen. Máximo 2560 caracteres."
},
"image": {
"name": "imagen",
"tooltip": "La imagen a editar."
},
"model": {
"name": "modelo",
"tooltip": "Versión del modelo a utilizar para la edición."
},
"model_aspect_ratio": {
"name": "relación_de_aspecto"
},
"model_test_time_scaling": {
"name": "escalado_en_tiempo_de_prueba"
},
"remove_background": {
"name": "eliminar_fondo",
"tooltip": "Eliminar el fondo de la imagen generada. Puede tener un costo adicional."
},
"seed": {
"name": "semilla",
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
},
"upscale": {
"name": "escalar",
"tooltip": "Aumentar la resolución de la imagen generada. Puede tener un costo adicional."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "Combina imágenes de referencia con prompts de texto para crear nuevas imágenes usando Reve.",
"display_name": "Reve Mezclar Imagen",
"inputs": {
"control_after_generate": {
"name": "control después de generar"
},
"model": {
"name": "modelo",
"tooltip": "Versión del modelo a utilizar para mezclar."
},
"model_aspect_ratio": {
"name": "relación_de_aspecto"
},
"model_test_time_scaling": {
"name": "escalado_en_tiempo_de_prueba"
},
"prompt": {
"name": "prompt",
"tooltip": "Descripción en texto de la imagen deseada. Puede incluir etiquetas XML img para referenciar imágenes específicas por índice, por ejemplo <img>0</img>, <img>1</img>, etc."
},
"reference_images": {
"name": "imágenes_de_referencia"
},
"remove_background": {
"name": "eliminar_fondo",
"tooltip": "Eliminar el fondo de la imagen generada. Puede tener un costo adicional."
},
"seed": {
"name": "semilla",
"tooltip": "La semilla controla si el nodo debe ejecutarse de nuevo; los resultados son no deterministas independientemente de la semilla."
},
"upscale": {
"name": "escalar",
"tooltip": "Aumentar la resolución de la imagen generada. Puede tener un costo adicional."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Generar activos 3D usando la API de Rodin",
"display_name": "Rodin 3D Generar - Generar Detalle",

View File

@@ -936,8 +936,6 @@
"batchRename": "تغییر نام گروهی",
"beta": "آزمایشی",
"bookmark": "ذخیره در کتابخانه",
"browserReservedKeybinding": "این میانبر توسط برخی مرورگرها رزرو شده و ممکن است نتایج غیرمنتظره‌ای داشته باشد.",
"browserReservedKeybindingTooltip": "این میانبر با میانبرهای رزرو شده مرورگر تداخل دارد",
"calculatingDimensions": "در حال محاسبه ابعاد",
"cancel": "لغو",
"cancelled": "لغو شده",
@@ -972,7 +970,6 @@
"copy": "کپی",
"copyAll": "کپی همه",
"copyJobId": "کپی شناسه وظیفه",
"copySystemInfo": "کپی اطلاعات سیستم",
"copyToClipboard": "کپی در کلیپ‌بورد",
"copyURL": "کپی آدرس",
"core": "هسته",
@@ -1019,7 +1016,6 @@
"enterNewName": "نام جدید را وارد کنید",
"enterNewNamePrompt": "نام جدید را وارد کنید:",
"enterSubgraph": "ورود به زیرگراف",
"enterYourKeybind": "کلید میانبر خود را وارد کنید",
"error": "خطا",
"errorLoadingImage": "خطا در بارگذاری تصویر",
"errorLoadingVideo": "خطا در بارگذاری ویدیو",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "دسترسی میکروفون رد شد",
"migrate": "مهاجرت",
"missing": "ناقص",
"modifyKeybinding": "تغییر کلید میانبر",
"more": "بیشتر",
"moreOptions": "گزینه‌های بیشتر",
"moreWorkflows": "workflowهای بیشتر",
@@ -1098,7 +1093,6 @@
"name": "نام",
"newFolder": "پوشه جدید",
"next": "بعدی",
"nextImage": "تصویر بعدی",
"nightly": "نسخه شبانه",
"no": "خیر",
"noAudioRecorded": "هیچ صدایی ضبط نشد",
@@ -1132,8 +1126,8 @@
"playRecording": "پخش ضبط",
"playbackSpeed": "سرعت پخش",
"playing": "در حال پخش",
"pressKeysForNewBinding": "کلیدهای میانبر جدید را فشار دهید",
"preview": "پیش‌نمایش",
"previousImage": "تصویر قبلی",
"profile": "پروفایل",
"progressCountOf": "از",
"queued": "در صف",
@@ -1173,7 +1167,6 @@
"resultsCount": "{count} نتیجه یافت شد",
"running": "در حال اجرا",
"save": "ذخیره",
"saveAnyway": "ذخیره کن",
"saving": "در حال ذخیره",
"scrollLeft": "اسکرول به چپ",
"scrollRight": "اسکرول به راست",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "مواردی برای تکرار انتخاب کنید",
"selectItemsToRename": "مواردی برای تغییر نام انتخاب کنید",
"selectedFile": "فایل انتخاب‌شده",
"setAKeybindingForTheFollowing": "کلید میانبر را برای مورد زیر تنظیم کنید:",
"setAsBackground": "تنظیم به عنوان پس‌زمینه",
"settings": "تنظیمات",
"shortcutSuffix": " ({shortcut})",
@@ -1208,8 +1200,6 @@
"stopRecording": "پایان ضبط",
"submit": "ارسال",
"success": "موفقیت‌آمیز",
"switchToGridView": "تغییر به نمای شبکه‌ای",
"switchToSingleView": "تغییر به نمای تکی",
"systemInfo": "اطلاعات سیستم",
"terminal": "ترمینال",
"title": "عنوان",
@@ -1239,8 +1229,6 @@
"you": "شما"
},
"graphCanvasMenu": {
"canvasMode": "حالت بوم",
"canvasToolbar": "نوار ابزار بوم",
"fitView": "تطبیق با نما",
"focusMode": "حالت تمرکز",
"hand": "دست",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "تصویر را بکشید و رها کنید",
"emptyWorkflowExplanation": "جریان کاری شما خالی است. ابتدا باید چند node اضافه کنید تا بتوانید یک برنامه بسازید.",
"enterNodeGraph": "ورود به گراف node",
"error": {
"getHelp": "برای دریافت راهنما، {0}، {1} یا {2} را با خطای کپی‌شده مشاهده کنید.",
"github": "ارسال issue در GitHub",
"goto": "نمایش خطاها در گراف",
"guide": "راهنمای رفع اشکال",
"header": "این اپلیکیشن با خطا مواجه شد",
"log": "گزارش‌های خطا",
"mobileFixable": "برای مشاهده خطاها به {0} مراجعه کنید",
"promptShow": "نمایش گزارش خطا",
"promptVisitGraph": "برای مشاهده خطای کامل، گراف نود را ببینید.",
"requiresGraph": "در طول تولید مشکلی پیش آمد. ممکن است به دلیل ورودی‌های مخفی نامعتبر، منابع گمشده یا مشکلات پیکربندی workflow باشد.",
"support": "تماس با پشتیبانی ما"
},
"giveFeedback": "ارسال بازخورد",
"graphMode": "حالت گراف",
"hasCreditCost": "نیازمند اعتبار اضافی",
"linearMode": "حالت برنامه",
"loadTemplate": "بارگذاری قالب",
"mobileControls": "ویرایش و اجرا",
"mobileNoWorkflow": "این workflow برای حالت اپلیکیشن ساخته نشده است. مورد دیگری را امتحان کنید.",
"queue": {
"clear": "پاک‌سازی صف",
"clickToClear": "برای پاک‌سازی صف کلیک کنید"
@@ -1504,7 +1478,6 @@
"rerun": "اجرای مجدد",
"reuseParameters": "استفاده مجدد از پارامترها",
"runCount": "تعداد اجرا: ",
"viewGraph": "مشاهده گراف نود",
"viewJob": "مشاهده وظیفه",
"welcome": {
"buildApp": "ساخت اپلیکیشن",
@@ -1713,7 +1686,6 @@
"installedSection": "نصب شده",
"missingNodes": "Nodeهای مفقود",
"notInstalled": "نصب نشده",
"unresolvedNodes": "نودهای حل‌نشده",
"updatesAvailable": "به‌روزرسانی‌های موجود"
},
"nightlyVersion": "نسخه nightly",
@@ -1760,10 +1732,6 @@
"uninstall": "حذف نصب",
"uninstallSelected": "حذف نصب انتخاب‌شده‌ها",
"uninstalling": "در حال حذف نصب {id}",
"unresolvedNodes": {
"message": "نودهای زیر نصب نشده‌اند و در رجیستری یافت نشدند.",
"title": "نودهای گمشده حل‌نشده"
},
"update": "به‌روزرسانی",
"updateAll": "به‌روزرسانی همه",
"updateSelected": "به‌روزرسانی انتخاب‌شده‌ها",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2412,33 +2379,6 @@
"inputsNone": "بدون ورودی",
"inputsNoneTooltip": "این نود ورودی ندارد",
"locateNode": "یافتن node در canvas",
"missingModels": {
"alreadyExistsInCategory": "این مدل قبلاً در «{category}» وجود دارد",
"assetLoadTimeout": "شناسایی مدل زمان‌بر شد. لطفاً workflow را مجدداً بارگذاری کنید.",
"cancelSelection": "لغو انتخاب",
"clearUrl": "پاک کردن آدرس",
"collapseNodes": "مخفی کردن nodeهای مرتبط",
"confirmSelection": "تأیید انتخاب",
"copyModelName": "کپی نام مدل",
"customNodeDownloadDisabled": "محیط ابری از وارد کردن مدل برای custom nodeها در این بخش پشتیبانی نمی‌کند. لطفاً از nodeهای بارگذار استاندارد استفاده کنید یا مدلی از کتابخانه زیر جایگزین نمایید.",
"expandNodes": "نمایش nodeهای مرتبط",
"import": "وارد کردن",
"importAnyway": "به هر حال وارد کن",
"importFailed": "وارد کردن انجام نشد",
"importNotSupported": "وارد کردن پشتیبانی نمی‌شود",
"imported": "وارد شد",
"importing": "در حال وارد کردن...",
"locateNode": "یافتن node در بوم",
"metadataFetchFailed": "دریافت اطلاعات مدل انجام نشد. لطفاً لینک را بررسی و دوباره تلاش کنید.",
"missingModelsTitle": "مدل‌های مفقود شده",
"or": "یا",
"typeMismatch": "به نظر می‌رسد این مدل از نوع \"{detectedType}\" باشد. مطمئن هستید؟",
"unknownCategory": "نامشخص",
"unsupportedUrl": "فقط آدرس‌های Civitai و Hugging Face پشتیبانی می‌شوند.",
"urlPlaceholder": "آدرس مدل را وارد کنید (Civitai یا Hugging Face)",
"useFromLibrary": "استفاده از کتابخانه",
"usingFromLibrary": "در حال استفاده از کتابخانه"
},
"missingNodePacks": {
"applyChanges": "اعمال تغییرات",
"cloudMessage": "این workflow به nodeهای سفارشی نیاز دارد که هنوز در Comfy Cloud موجود نیستند.",
@@ -3310,7 +3250,6 @@
"interrupted": "اجرا متوقف شد",
"legacyMaskEditorDeprecated": "ویرایشگر mask قدیمی منسوخ شده و به‌زودی حذف خواهد شد.",
"migrateToLitegraphReroute": "nodeهای reroute در نسخه‌های آینده حذف خواهند شد. برای مهاجرت به reroute بومی litegraph کلیک کنید.",
"missingModelVerificationFailed": "تأیید مدل‌های مفقود شده انجام نشد. برخی مدل‌ها ممکن است در برگه خطاها نمایش داده نشوند.",
"modelLoadedSuccessfully": "مدل سه‌بعدی با موفقیت بارگذاری شد",
"no3dScene": "صحنه سه‌بعدی برای اعمال بافت وجود ندارد",
"no3dSceneToExport": "صحنه سه‌بعدی برای صادرات وجود ندارد",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "بهینه‌سازی KV Cache را برای تصاویر مرجع در مدل‌های خانواده Flux فعال می‌کند.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "مدل",
"tooltip": "مدلی که قرار است KV Cache روی آن فعال شود."
}
},
"outputs": {
"0": {
"tooltip": "مدل اصلاح‌شده با KV Cache فعال."
}
}
},
"FluxKontextImageScale": {
"description": "این نود تصویر را به اندازه‌ای تغییر می‌دهد که برای flux kontext بهینه‌تر باشد.",
"display_name": "FluxKontextImageScale",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "تولید تصویر از توضیحات متنی با استفاده از Reve.",
"display_name": "ایجاد تصویر با Reve",
"inputs": {
"control_after_generate": {
"name": "کنترل پس از تولید"
},
"model": {
"name": "مدل",
"tooltip": "نسخه مدل مورد استفاده برای تولید."
},
"model_aspect_ratio": {
"name": "نسبت ابعاد"
},
"model_test_time_scaling": {
"name": "مقیاس‌دهی زمان تست"
},
"prompt": {
"name": "پرامپت",
"tooltip": "توضیح متنی تصویر مورد نظر. حداکثر ۲۵۶۰ کاراکتر."
},
"remove_background": {
"name": "حذف پس‌زمینه",
"tooltip": "حذف پس‌زمینه از تصویر تولید شده. ممکن است هزینه اضافی داشته باشد."
},
"seed": {
"name": "seed",
"tooltip": "seed تعیین می‌کند که node باید دوباره اجرا شود یا خیر؛ نتایج صرف‌نظر از seed غیرقطعی هستند."
},
"upscale": {
"name": "افزایش وضوح",
"tooltip": "افزایش وضوح تصویر تولید شده. ممکن است هزینه اضافی داشته باشد."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "ویرایش تصاویر با استفاده از دستور زبان طبیعی در Reve.",
"display_name": "ویرایش تصویر با Reve",
"inputs": {
"control_after_generate": {
"name": "کنترل پس از تولید"
},
"edit_instruction": {
"name": "دستور ویرایش",
"tooltip": "توضیح متنی نحوه ویرایش تصویر. حداکثر ۲۵۶۰ کاراکتر."
},
"image": {
"name": "تصویر",
"tooltip": "تصویری که باید ویرایش شود."
},
"model": {
"name": "مدل",
"tooltip": "نسخه مدل مورد استفاده برای ویرایش."
},
"model_aspect_ratio": {
"name": "نسبت ابعاد"
},
"model_test_time_scaling": {
"name": "مقیاس‌دهی زمان تست"
},
"remove_background": {
"name": "حذف پس‌زمینه",
"tooltip": "حذف پس‌زمینه از تصویر تولید شده. ممکن است هزینه اضافی داشته باشد."
},
"seed": {
"name": "seed",
"tooltip": "seed تعیین می‌کند که node باید دوباره اجرا شود یا خیر؛ نتایج صرف‌نظر از seed غیرقطعی هستند."
},
"upscale": {
"name": "افزایش وضوح",
"tooltip": "افزایش وضوح تصویر تولید شده. ممکن است هزینه اضافی داشته باشد."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "ترکیب تصاویر مرجع با پرامپت متنی برای ایجاد تصاویر جدید با استفاده از Reve.",
"display_name": "ترکیب تصویر با Reve",
"inputs": {
"control_after_generate": {
"name": "کنترل پس از تولید"
},
"model": {
"name": "مدل",
"tooltip": "نسخه مدل مورد استفاده برای ترکیب."
},
"model_aspect_ratio": {
"name": "نسبت ابعاد"
},
"model_test_time_scaling": {
"name": "مقیاس‌دهی زمان تست"
},
"prompt": {
"name": "پرامپت",
"tooltip": "توضیح متنی تصویر مورد نظر. می‌توانید از تگ XML img برای ارجاع به تصاویر خاص با شماره اندیس استفاده کنید، مانند <img>۰</img>، <img>۱</img> و غیره."
},
"reference_images": {
"name": "تصاویر مرجع"
},
"remove_background": {
"name": "حذف پس‌زمینه",
"tooltip": "حذف پس‌زمینه از تصویر تولید شده. ممکن است هزینه اضافی داشته باشد."
},
"seed": {
"name": "seed",
"tooltip": "seed تعیین می‌کند که node باید دوباره اجرا شود یا خیر؛ نتایج صرف‌نظر از seed غیرقطعی هستند."
},
"upscale": {
"name": "افزایش وضوح",
"tooltip": "افزایش وضوح تصویر تولید شده. ممکن است هزینه اضافی داشته باشد."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "تولید دارایی‌های سه‌بعدی با استفاده از Rodin API",
"display_name": "Rodin 3D Generate - تولید جزئیات",

View File

@@ -936,8 +936,6 @@
"batchRename": "Renommer en lot",
"beta": "BÊTA",
"bookmark": "Enregistrer dans la bibliothèque",
"browserReservedKeybinding": "Ce raccourci est réservé par certains navigateurs et peut entraîner des résultats inattendus.",
"browserReservedKeybindingTooltip": "Ce raccourci est en conflit avec des raccourcis réservés du navigateur",
"calculatingDimensions": "Calcul des dimensions",
"cancel": "Annuler",
"cancelled": "Annulé",
@@ -972,7 +970,6 @@
"copy": "Copier",
"copyAll": "Tout copier",
"copyJobId": "Copier l'ID du travail",
"copySystemInfo": "Copier les informations système",
"copyToClipboard": "Copier dans le presse-papiers",
"copyURL": "Copier lURL",
"core": "Noyau",
@@ -1019,7 +1016,6 @@
"enterNewName": "Entrez le nouveau nom",
"enterNewNamePrompt": "Entrez le nouveau nom :",
"enterSubgraph": "Entrer dans le sous-graphe",
"enterYourKeybind": "Saisissez votre raccourci",
"error": "Erreur",
"errorLoadingImage": "Erreur lors du chargement de l'image",
"errorLoadingVideo": "Erreur lors du chargement de la vidéo",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "Permission du microphone refusée",
"migrate": "Migrer",
"missing": "Manquant",
"modifyKeybinding": "Modifier le raccourci clavier",
"more": "Plus",
"moreOptions": "Plus d'options",
"moreWorkflows": "Plus de workflows",
@@ -1098,7 +1093,6 @@
"name": "Nom",
"newFolder": "Nouveau dossier",
"next": "Suivant",
"nextImage": "Image suivante",
"nightly": "NIGHTLY",
"no": "Non",
"noAudioRecorded": "Aucun audio enregistré",
@@ -1132,8 +1126,8 @@
"playRecording": "Lire l'enregistrement",
"playbackSpeed": "Vitesse de lecture",
"playing": "Lecture en cours",
"pressKeysForNewBinding": "Appuyez sur les touches pour une nouvelle liaison",
"preview": "APERÇU",
"previousImage": "Image précédente",
"profile": "Profil",
"progressCountOf": "sur",
"queued": "En file dattente",
@@ -1173,7 +1167,6 @@
"resultsCount": "{count} Résultats Trouvés",
"running": "En cours",
"save": "Enregistrer",
"saveAnyway": "Enregistrer quand même",
"saving": "Enregistrement",
"scrollLeft": "Faire défiler à gauche",
"scrollRight": "Faire défiler à droite",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "Sélectionnez les éléments à dupliquer",
"selectItemsToRename": "Sélectionnez les éléments à renommer",
"selectedFile": "Fichier sélectionné",
"setAKeybindingForTheFollowing": "Définir un raccourci clavier pour :",
"setAsBackground": "Définir comme arrière-plan",
"settings": "Paramètres",
"shortcutSuffix": " ({shortcut})",
@@ -1208,8 +1200,6 @@
"stopRecording": "Arrêter lenregistrement",
"submit": "Soumettre",
"success": "Succès",
"switchToGridView": "Passer à la vue grille",
"switchToSingleView": "Passer à la vue unique",
"systemInfo": "Informations système",
"terminal": "Terminal",
"title": "Titre",
@@ -1239,8 +1229,6 @@
"you": "Vous"
},
"graphCanvasMenu": {
"canvasMode": "Mode canevas",
"canvasToolbar": "Barre doutils du canevas",
"fitView": "Adapter la vue",
"focusMode": "Mode focus",
"hand": "Main",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "Glissez-déposez une image",
"emptyWorkflowExplanation": "Votre workflow est vide. Vous devez d'abord ajouter des nodes pour commencer à créer une application.",
"enterNodeGraph": "Entrer dans le graphique de nœuds",
"error": {
"getHelp": "Pour obtenir de laide, consultez notre {0}, {1} ou {2} avec lerreur copiée.",
"github": "soumettre un ticket GitHub",
"goto": "Afficher les erreurs dans le graphe",
"guide": "guide de dépannage",
"header": "Une erreur est survenue dans cette application",
"log": "Journaux derreurs",
"mobileFixable": "Vérifiez {0} pour les erreurs",
"promptShow": "Afficher le rapport derreur",
"promptVisitGraph": "Consultez le graphe des nœuds pour voir lerreur complète.",
"requiresGraph": "Une erreur sest produite lors de la génération. Cela peut être dû à des entrées cachées invalides, des ressources manquantes ou des problèmes de configuration du workflow.",
"support": "contacter notre support"
},
"giveFeedback": "Donner un avis",
"graphMode": "Mode graphique",
"hasCreditCost": "Nécessite des crédits supplémentaires",
"linearMode": "Mode App",
"loadTemplate": "Charger un modèle",
"mobileControls": "Éditer & Exécuter",
"mobileNoWorkflow": "Ce workflow na pas été conçu pour le mode application. Essayez-en un autre.",
"queue": {
"clear": "Vider la file d'attente",
"clickToClear": "Cliquez pour vider la file d'attente"
@@ -1504,7 +1478,6 @@
"rerun": "Relancer",
"reuseParameters": "Réutiliser les paramètres",
"runCount": "Nombre dexécutions :",
"viewGraph": "Voir le graphe des nœuds",
"viewJob": "Voir la tâche",
"welcome": {
"buildApp": "Créer une application",
@@ -1713,7 +1686,6 @@
"installedSection": "INSTALLÉ",
"missingNodes": "Nœuds manquants",
"notInstalled": "Non installé",
"unresolvedNodes": "Nœuds non résolus",
"updatesAvailable": "Mises à jour disponibles"
},
"nightlyVersion": "Nocturne",
@@ -1760,10 +1732,6 @@
"uninstall": "Désinstaller",
"uninstallSelected": "Désinstaller sélectionné",
"uninstalling": "Désinstallation",
"unresolvedNodes": {
"message": "Les nœuds suivants ne sont pas installés et n'ont pas pu être trouvés dans le registre.",
"title": "Nœuds manquants non résolus"
},
"update": "Mettre à jour",
"updateAll": "Tout mettre à jour",
"updateSelected": "Mettre à jour la sélection",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2412,33 +2379,6 @@
"inputsNone": "AUCUNE ENTRÉE",
"inputsNoneTooltip": "Le nœud na pas dentrées",
"locateNode": "Localiser le nœud sur le canevas",
"missingModels": {
"alreadyExistsInCategory": "Ce modèle existe déjà dans « {category} »",
"assetLoadTimeout": "Le délai de détection du modèle est dépassé. Essayez de recharger le workflow.",
"cancelSelection": "Annuler la sélection",
"clearUrl": "Effacer lURL",
"collapseNodes": "Masquer les nœuds référencés",
"confirmSelection": "Confirmer la sélection",
"copyModelName": "Copier le nom du modèle",
"customNodeDownloadDisabled": "Lenvironnement cloud ne prend pas en charge limportation de modèles pour les nœuds personnalisés dans cette section. Veuillez utiliser des nœuds de chargement standard ou remplacer par un modèle de la bibliothèque ci-dessous.",
"expandNodes": "Afficher les nœuds référencés",
"import": "Importer",
"importAnyway": "Importer quand même",
"importFailed": "Échec de limportation",
"importNotSupported": "Importation non prise en charge",
"imported": "Importé",
"importing": "Importation...",
"locateNode": "Localiser le nœud sur le canevas",
"metadataFetchFailed": "Échec de la récupération des métadonnées. Veuillez vérifier le lien et réessayer.",
"missingModelsTitle": "Modèles manquants",
"or": "OU",
"typeMismatch": "Ce modèle semble être un(e) « {detectedType} ». Êtes-vous sûr ?",
"unknownCategory": "Inconnu",
"unsupportedUrl": "Seules les URL Civitai et Hugging Face sont prises en charge.",
"urlPlaceholder": "Collez lURL du modèle (Civitai ou Hugging Face)",
"useFromLibrary": "Utiliser depuis la bibliothèque",
"usingFromLibrary": "Utilisation depuis la bibliothèque"
},
"missingNodePacks": {
"applyChanges": "Appliquer les modifications",
"cloudMessage": "Ce workflow nécessite des nœuds personnalisés qui ne sont pas encore disponibles sur Comfy Cloud.",
@@ -3298,7 +3238,6 @@
"interrupted": "L'exécution a été interrompue",
"legacyMaskEditorDeprecated": "Léditeur de masque hérité est obsolète et sera bientôt supprimé.",
"migrateToLitegraphReroute": "Les nœuds de reroute seront supprimés dans les futures versions. Cliquez pour migrer vers le reroute natif de litegraph.",
"missingModelVerificationFailed": "Échec de la vérification des modèles manquants. Certains modèles peuvent ne pas apparaître dans longlet Erreurs.",
"modelLoadedSuccessfully": "Modèle 3D chargé avec succès",
"no3dScene": "Aucune scène 3D pour appliquer la texture",
"no3dSceneToExport": "Aucune scène 3D à exporter",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "Active loptimisation KV Cache pour les images de référence sur les modèles de la famille Flux.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "modèle",
"tooltip": "Le modèle sur lequel activer le KV Cache."
}
},
"outputs": {
"0": {
"tooltip": "Le modèle modifié avec le KV Cache activé."
}
}
},
"FluxKontextImageScale": {
"description": "Ce nœud redimensionne l'image pour une optimisation avec flux kontext.",
"display_name": "Échelle d'image FluxKontext",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "Générez des images à partir de descriptions textuelles avec Reve.",
"display_name": "Reve Création dImage",
"inputs": {
"control_after_generate": {
"name": "contrôle après génération"
},
"model": {
"name": "modèle",
"tooltip": "Version du modèle à utiliser pour la génération."
},
"model_aspect_ratio": {
"name": "rapport daspect"
},
"model_test_time_scaling": {
"name": "mise à léchelle à lexécution"
},
"prompt": {
"name": "prompt",
"tooltip": "Description textuelle de limage souhaitée. Maximum 2560 caractères."
},
"remove_background": {
"name": "supprimer larrière-plan",
"tooltip": "Supprimer larrière-plan de limage générée. Peut entraîner un coût supplémentaire."
},
"seed": {
"name": "graine",
"tooltip": "La graine contrôle si le nœud doit être relancé ; les résultats restent non déterministes quel que soit la graine."
},
"upscale": {
"name": "agrandir",
"tooltip": "Agrandir limage générée. Peut entraîner un coût supplémentaire."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Modifiez des images à laide dinstructions en langage naturel avec Reve.",
"display_name": "Reve Édition dImage",
"inputs": {
"control_after_generate": {
"name": "contrôle après génération"
},
"edit_instruction": {
"name": "instruction dédition",
"tooltip": "Description textuelle de la modification à apporter à limage. Maximum 2560 caractères."
},
"image": {
"name": "image",
"tooltip": "Limage à modifier."
},
"model": {
"name": "modèle",
"tooltip": "Version du modèle à utiliser pour lédition."
},
"model_aspect_ratio": {
"name": "rapport daspect"
},
"model_test_time_scaling": {
"name": "mise à léchelle à lexécution"
},
"remove_background": {
"name": "supprimer larrière-plan",
"tooltip": "Supprimer larrière-plan de limage générée. Peut entraîner un coût supplémentaire."
},
"seed": {
"name": "graine",
"tooltip": "La graine contrôle si le nœud doit être relancé ; les résultats restent non déterministes quel que soit la graine."
},
"upscale": {
"name": "agrandir",
"tooltip": "Agrandir limage générée. Peut entraîner un coût supplémentaire."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "Combinez des images de référence avec des prompts textuels pour créer de nouvelles images avec Reve.",
"display_name": "Reve Remix dImage",
"inputs": {
"control_after_generate": {
"name": "contrôle après génération"
},
"model": {
"name": "modèle",
"tooltip": "Version du modèle à utiliser pour le remix."
},
"model_aspect_ratio": {
"name": "rapport daspect"
},
"model_test_time_scaling": {
"name": "mise à léchelle à lexécution"
},
"prompt": {
"name": "prompt",
"tooltip": "Description textuelle de limage souhaitée. Peut inclure des balises XML img pour référencer des images spécifiques par index, par exemple <img>0</img>, <img>1</img>, etc."
},
"reference_images": {
"name": "images de référence"
},
"remove_background": {
"name": "supprimer larrière-plan",
"tooltip": "Supprimer larrière-plan de limage générée. Peut entraîner un coût supplémentaire."
},
"seed": {
"name": "graine",
"tooltip": "La graine contrôle si le nœud doit être relancé ; les résultats restent non déterministes quel que soit la graine."
},
"upscale": {
"name": "agrandir",
"tooltip": "Agrandir limage générée. Peut entraîner un coût supplémentaire."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Générer des actifs 3D en utilisant l'API Rodin",
"display_name": "Rodin 3D Générer - Générer Détails",

View File

@@ -936,8 +936,6 @@
"batchRename": "一括リネーム",
"beta": "ベータ版",
"bookmark": "ライブラリに保存",
"browserReservedKeybinding": "このショートカットは一部のブラウザで予約されており、予期しない動作を引き起こす可能性があります。",
"browserReservedKeybindingTooltip": "このショートカットはブラウザの予約済みショートカットと競合しています",
"calculatingDimensions": "寸法を計算中",
"cancel": "キャンセル",
"cancelled": "キャンセル済み",
@@ -972,7 +970,6 @@
"copy": "コピー",
"copyAll": "すべてコピー",
"copyJobId": "ジョブIDをコピー",
"copySystemInfo": "システム情報をコピー",
"copyToClipboard": "クリップボードにコピー",
"copyURL": "URLをコピー",
"core": "コア",
@@ -1019,7 +1016,6 @@
"enterNewName": "新しい名前を入力",
"enterNewNamePrompt": "新しい名前を入力してください:",
"enterSubgraph": "サブグラフに入る",
"enterYourKeybind": "キーバインドを入力してください",
"error": "エラー",
"errorLoadingImage": "画像の読み込みエラー",
"errorLoadingVideo": "ビデオの読み込みエラー",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "マイクの許可が拒否されました",
"migrate": "移行する",
"missing": "不足している",
"modifyKeybinding": "キーバインドを変更",
"more": "もっと見る",
"moreOptions": "その他のオプション",
"moreWorkflows": "さらに多くのワークフロー",
@@ -1098,7 +1093,6 @@
"name": "名前",
"newFolder": "新しいフォルダー",
"next": "次へ",
"nextImage": "次の画像",
"nightly": "NIGHTLY",
"no": "いいえ",
"noAudioRecorded": "音声が録音されていません",
@@ -1132,8 +1126,8 @@
"playRecording": "録音を再生",
"playbackSpeed": "再生速度",
"playing": "再生中",
"pressKeysForNewBinding": "新しいバインドのキーを押してください",
"preview": "プレビュー",
"previousImage": "前の画像",
"profile": "プロフィール",
"progressCountOf": "の",
"queued": "キュー中",
@@ -1173,7 +1167,6 @@
"resultsCount": "{count}件の結果が見つかりました",
"running": "実行中",
"save": "保存",
"saveAnyway": "強制的に保存",
"saving": "保存中",
"scrollLeft": "左にスクロール",
"scrollRight": "右にスクロール",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "複製する項目を選択",
"selectItemsToRename": "リネームする項目を選択",
"selectedFile": "選択されたファイル",
"setAKeybindingForTheFollowing": "次の操作のキーバインドを設定:",
"setAsBackground": "背景として設定",
"settings": "設定",
"shortcutSuffix": "{shortcut}",
@@ -1208,8 +1200,6 @@
"stopRecording": "録音停止",
"submit": "送信",
"success": "成功",
"switchToGridView": "グリッドビューに切り替え",
"switchToSingleView": "シングルビューに切り替え",
"systemInfo": "システム情報",
"terminal": "ターミナル",
"title": "タイトル",
@@ -1239,8 +1229,6 @@
"you": "あなた"
},
"graphCanvasMenu": {
"canvasMode": "キャンバスモード",
"canvasToolbar": "キャンバスツールバー",
"fitView": "ビューに合わせる",
"focusMode": "フォーカスモード",
"hand": "パンビュー",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "画像をドラッグ&ドロップ",
"emptyWorkflowExplanation": "ワークフローが空です。アプリを作成するには、まずノードを追加してください。",
"enterNodeGraph": "ノードグラフに入る",
"error": {
"getHelp": "ヘルプが必要な場合は、コピーしたエラーを使って{0}、{1}、または{2}をご覧ください。",
"github": "GitHub イシューを提出",
"goto": "グラフでエラーを表示",
"guide": "トラブルシューティングガイド",
"header": "このアプリでエラーが発生しました",
"log": "エラーログ",
"mobileFixable": "{0}でエラーを確認してください",
"promptShow": "エラーレポートを表示",
"promptVisitGraph": "ノードグラフを表示して、エラーの詳細を確認してください。",
"requiresGraph": "生成中に問題が発生しました。無効な非表示入力、リソースの不足、またはワークフロー設定の問題が原因の可能性があります。",
"support": "サポートに連絡"
},
"giveFeedback": "フィードバックを送る",
"graphMode": "グラフモード",
"hasCreditCost": "追加クレジットが必要です",
"linearMode": "アプリモード",
"loadTemplate": "テンプレートを読み込む",
"mobileControls": "編集と実行",
"mobileNoWorkflow": "このワークフローはアプリモード用に作成されていません。別のものをお試しください。",
"queue": {
"clear": "キューをクリア",
"clickToClear": "クリックしてキューをクリア"
@@ -1504,7 +1478,6 @@
"rerun": "再実行",
"reuseParameters": "パラメータを再利用",
"runCount": "実行回数:",
"viewGraph": "ノードグラフを表示",
"viewJob": "ジョブを表示",
"welcome": {
"buildApp": "アプリを作成",
@@ -1713,7 +1686,6 @@
"installedSection": "インストール済み",
"missingNodes": "不足しているノード",
"notInstalled": "未インストール",
"unresolvedNodes": "未解決ノード",
"updatesAvailable": "アップデートあり"
},
"nightlyVersion": "ナイトリー",
@@ -1760,10 +1732,6 @@
"uninstall": "アンインストール",
"uninstallSelected": "選択したものをアンインストール",
"uninstalling": "アンインストール中",
"unresolvedNodes": {
"message": "以下のノードがインストールされておらず、レジストリで見つかりませんでした。",
"title": "未解決の欠落ノード"
},
"update": "更新",
"updateAll": "すべて更新",
"updateSelected": "選択したものを更新",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2412,33 +2379,6 @@
"inputsNone": "入力なし",
"inputsNoneTooltip": "このノードには入力がありません",
"locateNode": "キャンバス上でノードを探す",
"missingModels": {
"alreadyExistsInCategory": "このモデルはすでに「{category}」に存在します",
"assetLoadTimeout": "モデルの検出がタイムアウトしました。ワークフローを再読み込みしてください。",
"cancelSelection": "選択をキャンセル",
"clearUrl": "URLをクリア",
"collapseNodes": "参照ノードを非表示",
"confirmSelection": "選択を確定",
"copyModelName": "モデル名をコピー",
"customNodeDownloadDisabled": "クラウド環境ではこのセクションのカスタムノード用モデルのインポートはサポートされていません。標準のローダーノードを使用するか、下記のライブラリからモデルを代用してください。",
"expandNodes": "参照ノードを表示",
"import": "インポート",
"importAnyway": "強制的にインポート",
"importFailed": "インポートに失敗しました",
"importNotSupported": "インポートはサポートされていません",
"imported": "インポート完了",
"importing": "インポート中...",
"locateNode": "キャンバス上でノードを表示",
"metadataFetchFailed": "メタデータの取得に失敗しました。リンクを確認して再試行してください。",
"missingModelsTitle": "不足しているモデル",
"or": "または",
"typeMismatch": "このモデルは「{detectedType}」のようです。本当に続行しますか?",
"unknownCategory": "不明",
"unsupportedUrl": "Civitai と Hugging Face のURLのみサポートされています。",
"urlPlaceholder": "モデルのURLを貼り付けCivitai または Hugging Face",
"useFromLibrary": "ライブラリから使用",
"usingFromLibrary": "ライブラリから使用中"
},
"missingNodePacks": {
"applyChanges": "変更を適用",
"cloudMessage": "このワークフローにはComfy Cloudでまだ利用できないカスタムードが必要です。",
@@ -3298,7 +3238,6 @@
"interrupted": "実行が中断されました",
"legacyMaskEditorDeprecated": "従来のマスクエディタは非推奨となり、まもなく削除されます。",
"migrateToLitegraphReroute": "将来のバージョンではRerouteードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。",
"missingModelVerificationFailed": "不足しているモデルの検証に失敗しました。一部のモデルはエラータブに表示されない場合があります。",
"modelLoadedSuccessfully": "3Dモデルが正常に読み込まれました",
"no3dScene": "テクスチャを適用する3Dシーンがありません",
"no3dSceneToExport": "エクスポートする3Dシーンがありません",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "Fluxファミリーモデルの参照画像に対してKVキャッシュ最適化を有効にします。",
"display_name": "Flux KVキャッシュ",
"inputs": {
"model": {
"name": "モデル",
"tooltip": "KVキャッシュを使用するモデル。"
}
},
"outputs": {
"0": {
"tooltip": "KVキャッシュが有効になったパッチ済みモデル。"
}
}
},
"FluxKontextImageScale": {
"description": "このードは、Flux Kontextに最適なサイズに画像をリサイズします。",
"display_name": "FluxKontextImageScale",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "Reveを使用してテキスト説明から画像を生成します。",
"display_name": "Reve画像生成",
"inputs": {
"control_after_generate": {
"name": "生成後のコントロール"
},
"model": {
"name": "モデル",
"tooltip": "生成に使用するモデルバージョン。"
},
"model_aspect_ratio": {
"name": "アスペクト比"
},
"model_test_time_scaling": {
"name": "テスト時スケーリング"
},
"prompt": {
"name": "プロンプト",
"tooltip": "生成したい画像のテキスト説明。最大2560文字。"
},
"remove_background": {
"name": "背景を削除",
"tooltip": "生成された画像から背景を削除します。追加料金が発生する場合があります。"
},
"seed": {
"name": "シード",
"tooltip": "シードはノードの再実行を制御しますが、シードに関わらず結果は非決定的です。"
},
"upscale": {
"name": "アップスケール",
"tooltip": "生成された画像をアップスケールします。追加料金が発生する場合があります。"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Reveを使って自然言語指示で画像を編集します。",
"display_name": "Reve画像編集",
"inputs": {
"control_after_generate": {
"name": "生成後のコントロール"
},
"edit_instruction": {
"name": "編集指示",
"tooltip": "画像の編集方法のテキスト説明。最大2560文字。"
},
"image": {
"name": "画像",
"tooltip": "編集する画像。"
},
"model": {
"name": "モデル",
"tooltip": "編集に使用するモデルバージョン。"
},
"model_aspect_ratio": {
"name": "アスペクト比"
},
"model_test_time_scaling": {
"name": "テスト時スケーリング"
},
"remove_background": {
"name": "背景を削除",
"tooltip": "生成された画像から背景を削除します。追加料金が発生する場合があります。"
},
"seed": {
"name": "シード",
"tooltip": "シードはノードの再実行を制御しますが、シードに関わらず結果は非決定的です。"
},
"upscale": {
"name": "アップスケール",
"tooltip": "生成された画像をアップスケールします。追加料金が発生する場合があります。"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "参照画像とテキストプロンプトを組み合わせてReveで新しい画像を作成します。",
"display_name": "Reve画像リミックス",
"inputs": {
"control_after_generate": {
"name": "生成後のコントロール"
},
"model": {
"name": "モデル",
"tooltip": "リミックスに使用するモデルバージョン。"
},
"model_aspect_ratio": {
"name": "アスペクト比"
},
"model_test_time_scaling": {
"name": "テスト時スケーリング"
},
"prompt": {
"name": "プロンプト",
"tooltip": "生成したい画像のテキスト説明。特定の画像をインデックスで参照するXML imgタグ<img>0</img>, <img>1</img>など)を含めることができます。"
},
"reference_images": {
"name": "参照画像"
},
"remove_background": {
"name": "背景を削除",
"tooltip": "生成された画像から背景を削除します。追加料金が発生する場合があります。"
},
"seed": {
"name": "シード",
"tooltip": "シードはノードの再実行を制御しますが、シードに関わらず結果は非決定的です。"
},
"upscale": {
"name": "アップスケール",
"tooltip": "生成された画像をアップスケールします。追加料金が発生する場合があります。"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Rodin APIを使用して3Dアセットを生成",
"display_name": "Rodin 3D生成 - 詳細生成",

View File

@@ -936,8 +936,6 @@
"batchRename": "일괄 이름 변경",
"beta": "베타",
"bookmark": "라이브러리에 저장",
"browserReservedKeybinding": "이 단축키는 일부 브라우저에서 예약되어 있어 예기치 않은 결과가 발생할 수 있습니다.",
"browserReservedKeybindingTooltip": "이 단축키는 브라우저 예약 단축키와 충돌합니다",
"calculatingDimensions": "크기 계산 중",
"cancel": "취소",
"cancelled": "취소됨",
@@ -972,7 +970,6 @@
"copy": "복사",
"copyAll": "모두 복사",
"copyJobId": "작업 ID 복사",
"copySystemInfo": "시스템 정보 복사",
"copyToClipboard": "클립보드에 복사",
"copyURL": "URL 복사",
"core": "코어",
@@ -1019,7 +1016,6 @@
"enterNewName": "새 이름 입력",
"enterNewNamePrompt": "새 이름을 입력하세요:",
"enterSubgraph": "서브그래프 진입",
"enterYourKeybind": "단축키를 입력하세요",
"error": "오류",
"errorLoadingImage": "이미지 로드 오류",
"errorLoadingVideo": "비디오 로드 오류",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "마이크 권한이 거부되었습니다",
"migrate": "이전(migrate)",
"missing": "누락됨",
"modifyKeybinding": "단축키 수정",
"more": "더보기",
"moreOptions": "추가 옵션",
"moreWorkflows": "더 많은 워크플로",
@@ -1098,7 +1093,6 @@
"name": "이름",
"newFolder": "새 폴더",
"next": "다음",
"nextImage": "다음 이미지",
"nightly": "NIGHTLY",
"no": "아니오",
"noAudioRecorded": "녹음된 오디오가 없습니다",
@@ -1132,8 +1126,8 @@
"playRecording": "녹음 재생",
"playbackSpeed": "재생 속도",
"playing": "재생 중",
"pressKeysForNewBinding": "새 바인딩을 위한 키 입력",
"preview": "미리보기",
"previousImage": "이전 이미지",
"profile": "프로필",
"progressCountOf": "중",
"queued": "대기 중",
@@ -1173,7 +1167,6 @@
"resultsCount": "{count} 개의 결과를 찾았습니다",
"running": "실행 중",
"save": "저장",
"saveAnyway": "그래도 저장",
"saving": "저장 중",
"scrollLeft": "왼쪽으로 스크롤",
"scrollRight": "오른쪽으로 스크롤",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "복제할 항목 선택",
"selectItemsToRename": "이름을 변경할 항목 선택",
"selectedFile": "선택된 파일",
"setAKeybindingForTheFollowing": "다음에 대한 단축키를 설정하세요:",
"setAsBackground": "배경으로 설정",
"settings": "설정",
"shortcutSuffix": " ({shortcut})",
@@ -1208,8 +1200,6 @@
"stopRecording": "녹음 중지",
"submit": "제출",
"success": "성공",
"switchToGridView": "그리드 보기로 전환",
"switchToSingleView": "단일 보기로 전환",
"systemInfo": "시스템 정보",
"terminal": "터미널",
"title": "제목",
@@ -1239,8 +1229,6 @@
"you": "당신"
},
"graphCanvasMenu": {
"canvasMode": "캔버스 모드",
"canvasToolbar": "캔버스 툴바",
"fitView": "보기 맞춤",
"focusMode": "집중 모드",
"hand": "손",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "이미지를 드래그 앤 드롭하세요",
"emptyWorkflowExplanation": "워크플로우가 비어 있습니다. 앱을 만들려면 먼저 노드를 추가해야 합니다.",
"enterNodeGraph": "노드 그래프로 진입",
"error": {
"getHelp": "도움이 필요하다면 복사된 오류와 함께 {0}, {1}, 또는 {2}를 확인하세요.",
"github": "GitHub 이슈 제출",
"goto": "그래프에서 오류 보기",
"guide": "문제 해결 가이드",
"header": "앱에서 오류가 발생했습니다",
"log": "오류 로그",
"mobileFixable": "{0}에서 오류를 확인하세요",
"promptShow": "오류 보고서 보기",
"promptVisitGraph": "전체 오류를 보려면 노드 그래프를 확인하세요.",
"requiresGraph": "생성 중 문제가 발생했습니다. 잘못된 숨겨진 입력, 누락된 리소스 또는 워크플로우 구성 문제일 수 있습니다.",
"support": "고객 지원 문의"
},
"giveFeedback": "피드백 보내기",
"graphMode": "그래프 모드",
"hasCreditCost": "추가 크레딧 필요",
"linearMode": "앱 모드",
"loadTemplate": "템플릿 불러오기",
"mobileControls": "편집 및 실행",
"mobileNoWorkflow": "이 워크플로우는 앱 모드에 맞게 제작되지 않았습니다. 다른 워크플로우를 시도해 보세요.",
"queue": {
"clear": "대기열 비우기",
"clickToClear": "클릭하여 대기열 비우기"
@@ -1504,7 +1478,6 @@
"rerun": "다시 실행",
"reuseParameters": "파라미터 재사용",
"runCount": "실행 횟수:",
"viewGraph": "노드 그래프 보기",
"viewJob": "작업 보기",
"welcome": {
"buildApp": "앱 만들기",
@@ -1713,7 +1686,6 @@
"installedSection": "설치됨",
"missingNodes": "누락된 노드",
"notInstalled": "미설치",
"unresolvedNodes": "해결되지 않은 노드",
"updatesAvailable": "업데이트 가능"
},
"nightlyVersion": "최신 테스트 버전(nightly)",
@@ -1760,10 +1732,6 @@
"uninstall": "제거",
"uninstallSelected": "선택 항목 제거",
"uninstalling": "제거 중",
"unresolvedNodes": {
"message": "다음 노드가 설치되어 있지 않으며 레지스트리에서 찾을 수 없습니다.",
"title": "해결되지 않은 누락된 노드"
},
"update": "업데이트",
"updateAll": "모두 업데이트",
"updateSelected": "선택 항목 업데이트",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2412,33 +2379,6 @@
"inputsNone": "입력 없음",
"inputsNoneTooltip": "노드에 입력이 없습니다",
"locateNode": "캔버스에서 노드 찾기",
"missingModels": {
"alreadyExistsInCategory": "이 모델은 이미 \"{category}\"에 존재합니다",
"assetLoadTimeout": "모델 감지 시간이 초과되었습니다. 워크플로우를 다시 불러와 보세요.",
"cancelSelection": "선택 취소",
"clearUrl": "URL 지우기",
"collapseNodes": "참조 노드 숨기기",
"confirmSelection": "선택 확인",
"copyModelName": "모델 이름 복사",
"customNodeDownloadDisabled": "클라우드 환경에서는 이 섹션의 커스텀 노드 모델 가져오기를 지원하지 않습니다. 표준 로더 노드를 사용하거나 아래 라이브러리의 모델로 대체해 주세요.",
"expandNodes": "참조 노드 보기",
"import": "가져오기",
"importAnyway": "그래도 가져오기",
"importFailed": "가져오기 실패",
"importNotSupported": "가져오기 지원되지 않음",
"imported": "가져오기 완료",
"importing": "가져오는 중...",
"locateNode": "캔버스에서 노드 위치 찾기",
"metadataFetchFailed": "메타데이터를 가져오지 못했습니다. 링크를 확인하고 다시 시도하세요.",
"missingModelsTitle": "누락된 모델",
"or": "또는",
"typeMismatch": "이 모델은 \"{detectedType}\"로 보입니다. 계속하시겠습니까?",
"unknownCategory": "알 수 없음",
"unsupportedUrl": "Civitai와 Hugging Face URL만 지원됩니다.",
"urlPlaceholder": "모델 URL 붙여넣기 (Civitai 또는 Hugging Face)",
"useFromLibrary": "라이브러리에서 사용",
"usingFromLibrary": "라이브러리에서 사용 중"
},
"missingNodePacks": {
"applyChanges": "변경 사항 적용",
"cloudMessage": "이 워크플로우에는 Comfy Cloud에서 아직 사용할 수 없는 커스텀 노드가 필요합니다.",
@@ -3298,7 +3238,6 @@
"interrupted": "실행이 중단되었습니다",
"legacyMaskEditorDeprecated": "레거시 마스크 에디터는 더 이상 지원되지 않으며 곧 제거될 예정입니다.",
"migrateToLitegraphReroute": "향후 버전에서는 Reroute 노드가 제거됩니다. LiteGraph 에서 자체 제공하는 경유점으로 변환하려면 클릭하세요.",
"missingModelVerificationFailed": "누락된 모델 검증에 실패했습니다. 일부 모델이 오류 탭에 표시되지 않을 수 있습니다.",
"modelLoadedSuccessfully": "3D 모델이 성공적으로 로드됨",
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "Flux 계열 모델에서 참조 이미지에 대한 KV 캐시 최적화를 활성화합니다.",
"display_name": "Flux KV 캐시",
"inputs": {
"model": {
"name": "모델",
"tooltip": "KV 캐시를 사용할 모델입니다."
}
},
"outputs": {
"0": {
"tooltip": "KV 캐시가 활성화된 패치된 모델입니다."
}
}
},
"FluxKontextImageScale": {
"description": "이 노드는 이미지를 Flux Kontext에 더 최적화된 크기로 조정합니다.",
"display_name": "Flux Kontext 이미지 스케일",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "Reve를 사용하여 텍스트 설명으로부터 이미지를 생성합니다.",
"display_name": "Reve 이미지 생성",
"inputs": {
"control_after_generate": {
"name": "생성 후 제어"
},
"model": {
"name": "모델",
"tooltip": "생성에 사용할 모델 버전입니다."
},
"model_aspect_ratio": {
"name": "종횡비"
},
"model_test_time_scaling": {
"name": "테스트 타임 스케일링"
},
"prompt": {
"name": "프롬프트",
"tooltip": "원하는 이미지에 대한 텍스트 설명입니다. 최대 2560자까지 입력할 수 있습니다."
},
"remove_background": {
"name": "배경 제거",
"tooltip": "생성된 이미지에서 배경을 제거합니다. 추가 비용이 발생할 수 있습니다."
},
"seed": {
"name": "시드",
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
},
"upscale": {
"name": "업스케일",
"tooltip": "생성된 이미지를 업스케일합니다. 추가 비용이 발생할 수 있습니다."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Reve를 사용하여 자연어 지시로 이미지를 편집합니다.",
"display_name": "Reve 이미지 편집",
"inputs": {
"control_after_generate": {
"name": "생성 후 제어"
},
"edit_instruction": {
"name": "편집 지시",
"tooltip": "이미지를 어떻게 편집할지에 대한 텍스트 설명입니다. 최대 2560자까지 입력할 수 있습니다."
},
"image": {
"name": "이미지",
"tooltip": "편집할 이미지입니다."
},
"model": {
"name": "모델",
"tooltip": "편집에 사용할 모델 버전입니다."
},
"model_aspect_ratio": {
"name": "종횡비"
},
"model_test_time_scaling": {
"name": "테스트 타임 스케일링"
},
"remove_background": {
"name": "배경 제거",
"tooltip": "생성된 이미지에서 배경을 제거합니다. 추가 비용이 발생할 수 있습니다."
},
"seed": {
"name": "시드",
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
},
"upscale": {
"name": "업스케일",
"tooltip": "생성된 이미지를 업스케일합니다. 추가 비용이 발생할 수 있습니다."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "참고 이미지를 텍스트 프롬프트와 결합하여 Reve로 새로운 이미지를 만듭니다.",
"display_name": "Reve 이미지 리믹스",
"inputs": {
"control_after_generate": {
"name": "생성 후 제어"
},
"model": {
"name": "모델",
"tooltip": "리믹스에 사용할 모델 버전입니다."
},
"model_aspect_ratio": {
"name": "종횡비"
},
"model_test_time_scaling": {
"name": "테스트 타임 스케일링"
},
"prompt": {
"name": "프롬프트",
"tooltip": "원하는 이미지에 대한 텍스트 설명입니다. 특정 이미지를 인덱스로 참조하려면 XML img 태그를 사용할 수 있습니다. 예: <img>0</img>, <img>1</img> 등."
},
"reference_images": {
"name": "참고 이미지"
},
"remove_background": {
"name": "배경 제거",
"tooltip": "생성된 이미지에서 배경을 제거합니다. 추가 비용이 발생할 수 있습니다."
},
"seed": {
"name": "시드",
"tooltip": "시드는 노드가 다시 실행될지 여부를 제어합니다. 시드와 관계없이 결과는 비결정적입니다."
},
"upscale": {
"name": "업스케일",
"tooltip": "생성된 이미지를 업스케일합니다. 추가 비용이 발생할 수 있습니다."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Rodin API를 사용하여 3D 에셋 생성",
"display_name": "Rodin 3D 생성 - 디테일 생성",

View File

@@ -936,8 +936,6 @@
"batchRename": "Renomear em lote",
"beta": "BETA",
"bookmark": "Salvar na biblioteca",
"browserReservedKeybinding": "Este atalho é reservado por alguns navegadores e pode ter resultados inesperados.",
"browserReservedKeybindingTooltip": "Este atalho entra em conflito com atalhos reservados do navegador",
"calculatingDimensions": "Calculando dimensões",
"cancel": "Cancelar",
"cancelled": "Cancelado",
@@ -972,7 +970,6 @@
"copy": "Copiar",
"copyAll": "Copiar tudo",
"copyJobId": "Copiar ID da tarefa",
"copySystemInfo": "Copiar informações do sistema",
"copyToClipboard": "Copiar para a área de transferência",
"copyURL": "Copiar URL",
"core": "Núcleo",
@@ -1019,7 +1016,6 @@
"enterNewName": "Digite o novo nome",
"enterNewNamePrompt": "Digite o novo nome:",
"enterSubgraph": "Entrar no Subgrafo",
"enterYourKeybind": "Digite seu atalho",
"error": "Erro",
"errorLoadingImage": "Erro ao carregar imagem",
"errorLoadingVideo": "Erro ao carregar vídeo",
@@ -1089,7 +1085,6 @@
"micPermissionDenied": "Permissão do microfone negada",
"migrate": "Migrar",
"missing": "Faltando",
"modifyKeybinding": "Modificar atalho",
"more": "Mais",
"moreOptions": "Mais opções",
"moreWorkflows": "Mais fluxos de trabalho",
@@ -1098,7 +1093,6 @@
"name": "Nome",
"newFolder": "Nova pasta",
"next": "Próximo",
"nextImage": "Próxima imagem",
"nightly": "NIGHTLY",
"no": "Não",
"noAudioRecorded": "Nenhum áudio gravado",
@@ -1132,8 +1126,8 @@
"playRecording": "Reproduzir gravação",
"playbackSpeed": "Velocidade de reprodução",
"playing": "Reproduzindo",
"pressKeysForNewBinding": "Pressione as teclas para novo atalho",
"preview": "PRÉVIA",
"previousImage": "Imagem anterior",
"profile": "Perfil",
"progressCountOf": "de",
"queued": "Na fila",
@@ -1173,7 +1167,6 @@
"resultsCount": "{count} resultados encontrados",
"running": "Executando",
"save": "Salvar",
"saveAnyway": "Salvar mesmo assim",
"saving": "Salvando",
"scrollLeft": "Rolar para a esquerda",
"scrollRight": "Rolar para a direita",
@@ -1192,7 +1185,6 @@
"selectItemsToDuplicate": "Selecione itens para duplicar",
"selectItemsToRename": "Selecione itens para renomear",
"selectedFile": "Arquivo selecionado",
"setAKeybindingForTheFollowing": "Definir um atalho para o seguinte:",
"setAsBackground": "Definir como plano de fundo",
"settings": "Configurações",
"shortcutSuffix": " ({shortcut})",
@@ -1208,8 +1200,6 @@
"stopRecording": "Parar gravação",
"submit": "Enviar",
"success": "Sucesso",
"switchToGridView": "Alternar para visualização em grade",
"switchToSingleView": "Alternar para visualização única",
"systemInfo": "Informações do sistema",
"terminal": "Terminal",
"title": "Título",
@@ -1239,8 +1229,6 @@
"you": "Você"
},
"graphCanvasMenu": {
"canvasMode": "Modo de tela",
"canvasToolbar": "Barra de ferramentas da tela",
"fitView": "Ajustar à Tela",
"focusMode": "Modo de Foco",
"hand": "Mão",
@@ -1477,26 +1465,12 @@
"dragAndDropImage": "Arraste e solte uma imagem",
"emptyWorkflowExplanation": "Seu fluxo de trabalho está vazio. Você precisa adicionar alguns nós primeiro para começar a construir um app.",
"enterNodeGraph": "Entrar no grafo de nós",
"error": {
"getHelp": "Para obter ajuda, veja nosso {0}, {1} ou {2} com o erro copiado.",
"github": "enviar um problema no GitHub",
"goto": "Mostrar erros no grafo",
"guide": "guia de solução de problemas",
"header": "Este aplicativo encontrou um erro",
"log": "Logs de erro",
"mobileFixable": "Verifique {0} para erros",
"promptShow": "Mostrar relatório de erro",
"promptVisitGraph": "Veja o grafo de nós para ver o erro completo.",
"requiresGraph": "Algo deu errado durante a geração. Isso pode ser devido a entradas ocultas inválidas, recursos ausentes ou problemas de configuração do fluxo de trabalho.",
"support": "contate nosso suporte"
},
"giveFeedback": "Enviar feedback",
"graphMode": "Modo Gráfico",
"hasCreditCost": "Requer créditos adicionais",
"linearMode": "Modo App",
"loadTemplate": "Carregar um modelo",
"mobileControls": "Editar e Executar",
"mobileNoWorkflow": "Este fluxo de trabalho não foi criado para o modo aplicativo. Tente outro.",
"queue": {
"clear": "Limpar fila",
"clickToClear": "Clique para limpar a fila"
@@ -1504,7 +1478,6 @@
"rerun": "Executar novamente",
"reuseParameters": "Reutilizar parâmetros",
"runCount": "Número de execuções:",
"viewGraph": "Ver grafo de nós",
"viewJob": "Ver tarefa",
"welcome": {
"buildApp": "Criar aplicativo",
@@ -1713,7 +1686,6 @@
"installedSection": "INSTALADO",
"missingNodes": "Nós Ausentes",
"notInstalled": "Não Instalado",
"unresolvedNodes": "Nós não resolvidos",
"updatesAvailable": "Atualizações Disponíveis"
},
"nightlyVersion": "Noturna",
@@ -1760,10 +1732,6 @@
"uninstall": "Desinstalar",
"uninstallSelected": "Desinstalar Selecionados",
"uninstalling": "Desinstalando {id}",
"unresolvedNodes": {
"message": "Os seguintes nós não estão instalados e não foram encontrados no registro.",
"title": "Nós ausentes não resolvidos"
},
"update": "Atualizar",
"updateAll": "Atualizar Todos",
"updateSelected": "Atualizar Selecionados",
@@ -2112,7 +2080,6 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2412,33 +2379,6 @@
"inputsNone": "SEM ENTRADAS",
"inputsNoneTooltip": "O nó não possui entradas",
"locateNode": "Localizar nó no canvas",
"missingModels": {
"alreadyExistsInCategory": "Este modelo já existe em \"{category}\"",
"assetLoadTimeout": "Tempo esgotado na detecção do modelo. Tente recarregar o fluxo de trabalho.",
"cancelSelection": "Cancelar seleção",
"clearUrl": "Limpar URL",
"collapseNodes": "Ocultar nós de referência",
"confirmSelection": "Confirmar seleção",
"copyModelName": "Copiar nome do modelo",
"customNodeDownloadDisabled": "O ambiente em nuvem não suporta importação de modelos para nós personalizados nesta seção. Utilize nós de carregamento padrão ou substitua por um modelo da biblioteca abaixo.",
"expandNodes": "Mostrar nós de referência",
"import": "Importar",
"importAnyway": "Importar mesmo assim",
"importFailed": "Falha na importação",
"importNotSupported": "Importação não suportada",
"imported": "Importado",
"importing": "Importando...",
"locateNode": "Localizar nó no canvas",
"metadataFetchFailed": "Falha ao obter metadados. Verifique o link e tente novamente.",
"missingModelsTitle": "Modelos Ausentes",
"or": "OU",
"typeMismatch": "Este modelo parece ser um \"{detectedType}\". Tem certeza?",
"unknownCategory": "Desconhecido",
"unsupportedUrl": "Apenas URLs do Civitai e Hugging Face são suportadas.",
"urlPlaceholder": "Cole a URL do modelo (Civitai ou Hugging Face)",
"useFromLibrary": "Usar da Biblioteca",
"usingFromLibrary": "Usando da Biblioteca"
},
"missingNodePacks": {
"applyChanges": "Aplicar alterações",
"cloudMessage": "Este fluxo de trabalho requer nós personalizados que ainda não estão disponíveis no Comfy Cloud.",
@@ -3310,7 +3250,6 @@
"interrupted": "Execução interrompida",
"legacyMaskEditorDeprecated": "O editor de máscara legado está obsoleto e será removido em breve.",
"migrateToLitegraphReroute": "Nós de redirecionamento serão removidos em versões futuras. Clique para migrar para o redirecionamento nativo do litegraph.",
"missingModelVerificationFailed": "Falha ao verificar modelos ausentes. Alguns modelos podem não aparecer na aba de Erros.",
"modelLoadedSuccessfully": "Modelo 3D carregado com sucesso",
"no3dScene": "Nenhuma cena 3D para aplicar textura",
"no3dSceneToExport": "Nenhuma cena 3D para exportar",

View File

@@ -3207,21 +3207,6 @@
}
}
},
"FluxKVCache": {
"description": "Ativa a otimização KV Cache para imagens de referência em modelos da família Flux.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "modelo",
"tooltip": "O modelo para aplicar o KV Cache."
}
},
"outputs": {
"0": {
"tooltip": "O modelo modificado com KV Cache ativado."
}
}
},
"FluxKontextImageScale": {
"description": "Este nó redimensiona a imagem para um tamanho mais ideal para flux kontext.",
"display_name": "FluxKontextImageScale",
@@ -12870,133 +12855,6 @@
}
}
},
"ReveImageCreateNode": {
"description": "Gere imagens a partir de descrições de texto usando o Reve.",
"display_name": "Reve Image Create",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"model": {
"name": "model",
"tooltip": "Versão do modelo a ser usada para geração."
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"prompt": {
"name": "prompt",
"tooltip": "Descrição em texto da imagem desejada. Máximo de 2560 caracteres."
},
"remove_background": {
"name": "remove_background",
"tooltip": "Remover o fundo da imagem gerada. Pode gerar custo adicional."
},
"seed": {
"name": "seed",
"tooltip": "O seed controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente do seed."
},
"upscale": {
"name": "upscale",
"tooltip": "Aumentar a resolução da imagem gerada. Pode gerar custo adicional."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Edite imagens usando instruções em linguagem natural com o Reve.",
"display_name": "Reve Image Edit",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"edit_instruction": {
"name": "edit_instruction",
"tooltip": "Descrição em texto de como editar a imagem. Máximo de 2560 caracteres."
},
"image": {
"name": "image",
"tooltip": "A imagem a ser editada."
},
"model": {
"name": "model",
"tooltip": "Versão do modelo a ser usada para edição."
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"remove_background": {
"name": "remove_background",
"tooltip": "Remover o fundo da imagem gerada. Pode gerar custo adicional."
},
"seed": {
"name": "seed",
"tooltip": "O seed controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente do seed."
},
"upscale": {
"name": "upscale",
"tooltip": "Aumentar a resolução da imagem gerada. Pode gerar custo adicional."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "Combine imagens de referência com prompts de texto para criar novas imagens usando o Reve.",
"display_name": "Reve Image Remix",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"model": {
"name": "model",
"tooltip": "Versão do modelo a ser usada para remix."
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"prompt": {
"name": "prompt",
"tooltip": "Descrição em texto da imagem desejada. Pode incluir tags XML img para referenciar imagens específicas pelo índice, por exemplo, <img>0</img>, <img>1</img>, etc."
},
"reference_images": {
"name": "reference_images"
},
"remove_background": {
"name": "remove_background",
"tooltip": "Remover o fundo da imagem gerada. Pode gerar custo adicional."
},
"seed": {
"name": "seed",
"tooltip": "O seed controla se o nó deve ser executado novamente; os resultados são não determinísticos independentemente do seed."
},
"upscale": {
"name": "upscale",
"tooltip": "Aumentar a resolução da imagem gerada. Pode gerar custo adicional."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Gerar ativos 3D usando a API Rodin",
"display_name": "Rodin 3D Gerar - Geração Detalhada",

Some files were not shown because too many files have changed in this diff Show More