Compare commits

..

37 Commits

Author SHA1 Message Date
bymyself
e04225dd49 feat: remove client-side cache-busting query parameters
Remove getRandParam() and urlWithTimestamp that added cache-busting
query params (&rand=... and &t=...) to output URLs.

This is now handled by the backend generating unique filenames
with timestamps (ComfyUI PR pending).

Updated files:
- app.ts: removed getRandParam() method
- queueStore.ts: removed urlWithTimestamp getter
- imagePreviewStore.ts: removed unused imports
- useCompletionSummary.ts: use .url instead of .urlWithTimestamp
- imageCompare.ts, useMaskEditorLoader.ts, audioUtils.ts,
  Load3dUtils.ts: removed getRandParam() usage

Amp-Thread-ID: https://ampcode.com/threads/T-019c17e5-1c0a-736f-970d-e411aae222fc
2026-03-12 13:11:47 -07:00
AustinMroz
ffda940e5a Make the vue toggle ring surround toggle (#9071)
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/3d414c6e-972e-4c87-8765-c30dc8288ddb"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/a3ec3eb4-f61f-42ac-bcf3-bc4c766040d7"/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9071-Make-the-vue-toggle-ring-surround-toggle-30f6d73d365081e0a427cf270ef2763a)
by [Unito](https://www.unito.io)
2026-03-12 12:00:02 -07:00
AustinMroz
84f77e7675 Support search filtering to dynamic input types (#9388)
Previously, MatchType and Autogrow inputs would not be considered would
filtering searchbox entires. For example, "Batch Images" would not show
as a suggestion would dragging a noodle from a "Load Image" node.

This is resolved by adding a step during nodeDef registration to
precalculate a list of all input types. This may have performance
implications.
- Search filtering should be more performant
- Initial node registration will be slower
- There's additional memory cost to store this information on every
node.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9388-Support-search-filtering-to-dynamic-input-types-3196d73d365081d9939eff5e167a7e83)
by [Unito](https://www.unito.io)
2026-03-12 09:14:11 -07:00
Alexander Brown
adf81fcd73 refactor: centralize display_name || name into getAssetDisplayName (#9641)
## Summary

Centralize the inline `display_name || name` pattern into
`getAssetDisplayName`, adding `display_name` to the existing metadata
fallback chain.

## Changes

- **What**: Update `getAssetDisplayName` fallback chain to
`user_metadata.name → metadata.name → display_name → name`. Replace all
6 inline `asset.display_name || asset.name` call sites with the shared
utility. Remove duplicate local function in `AssetsSidebarListView.vue`.

## Review Focus

The fallback order preserves user_metadata overrides while incorporating
the `display_name` field added in #9626. All callers now go through a
single code path.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9641-refactor-centralize-display_name-name-into-getAssetDisplayName-31e6d73d365081e09e5de85486583443)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-12 09:13:20 -07:00
Comfy Org PR Bot
c85a15547b 1.42.3 (#9685)
Patch version increment to 1.42.3

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-03-12 09:08:21 -07:00
Christian Byrne
c602dce375 feat: integrate nightly survey system into app (#8480)
## Summary

Wires the nightly survey system into the app by adding a controller
component and a convenience composable for feature-site usage tracking.

## Changes

- **What**: NightlySurveyController iterates enabled surveys from the
registry and renders a NightlySurveyPopover for each.
useSurveyFeatureTracking wraps useFeatureUsageTracker with a
config-enabled guard for use at feature call sites.
- **Tree-shaking**: Controller is loaded via defineAsyncComponent behind
a compile-time isNightly/isCloud/isDesktop guard in SideToolbar.vue, so
the entire survey module subtree is eliminated from cloud/desktop/stable
builds.

## Review Focus

- DCE pattern: controller imported conditionally via
defineAsyncComponent + distribution guard (same pattern as
ComfyRunButton/index.ts)
- useSurveyFeatureTracking short-circuits early when config is
absent/disabled (avoids initializing tracker storage)
- No user-facing behavior change: FEATURE_SURVEYS registry is still
empty

## Part of Nightly Survey System

This is part 5 of a stacked PR chain:
1. feat/feature-usage-tracker - useFeatureUsageTracker (merged in #8189)
2. feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3. feat/survey-config - surveyRegistry.ts (#8355, merged)
4. feat/survey-popover - NightlySurveyPopover.vue (#9083, merged)
5. **feat/survey-integration** - NightlySurveyController.vue (this PR)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-12 09:06:26 -07:00
Kelly Yang
34b1799b21 feat/mask-editor-brush-step-size (#9730)
## Summary

Fix #9727
FYI #9534

## GIMP Source

Accroding to GIMP( like open-source Adobe
Photoshop)`/composables/maskeditor/useBrushDrawing.ts`,
```
const brushStepSizeSliderValue = computed({
  get: () => {
    if (rawStepSizeSliderValue.value !== null) {
      const cachedSize = Math.round(Math.pow(100, rawStepSizeSliderValue.value))
      if (cachedSize === brushStepSize.value) {
        return rawStepSizeSliderValue.value
      }
    }
    return Math.log(brushStepSize.value) / Math.log(100)
  },
  set: (value: number) => {
    rawStepSizeSliderValue.value = value
    const size = Math.round(Math.pow(100, value))
    store.setBrushStepSize(size)
  }
})
```

## Screenshot


https://github.com/user-attachments/assets/971e7bd9-9690-475f-b214-33d06939d2ef

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9730-feat-mask-editor-brush-step-size-3206d73d36508198b934e0fb374eb5a9)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-03-12 09:01:44 -07:00
Robin Huang
bacb5570c8 feat: add server-side PostHog config overrides (#9758)
Add posthog_config to RemoteConfig so any PostHog init parameter can be
overridden via /api/features. Client-side defaults are applied first,
then server config is spread on top.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9758-feat-add-server-side-PostHog-config-overrides-3206d73d36508123ad82c31187e69e49)
by [Unito](https://www.unito.io)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:53:02 -07:00
Christian Byrne
8b53d5c807 fix: preserve input asset previews across execution updates (#9123)
## Summary

Fix input asset previews (images/videos) disappearing from
LoadImage/LoadVideo nodes after execution and a browser tab switch.

## Changes

- **What**: Guard `setOutputsByLocatorId` in `imagePreviewStore` to
preserve existing input-type preview images (`type: 'input'`) when the
incoming execution output has no images. Execution outputs with actual
images still overwrite as expected.

## Review Focus

- The guard only applies when existing output is an input preview (`type
=== 'input'` for all images) AND incoming output has no images — this is
the exact scenario where execution clobbers upload widget previews.
- Root cause: execution results from the backend overwrite the upload
widget's synthetic preview for LoadImage/LoadVideo nodes (which produce
no output images). Combined with the deferred resize-observer
re-observation from PR #8805, returning to a hidden tab reads the
now-empty store entry.
2026-03-12 08:50:14 -07:00
Dante
39ce4a23cc fix: skip node metadata paste when media node is selected (#9773)
## Summary

- When a media node (LoadImage/LoadAudio/LoadVideo) is selected and the
clipboard contains stale node metadata from a prior Ctrl+C, pasting
skips the node-metadata deserialization so that the paste falls through
to litegraph's default handler instead of incorrectly pasting the old
copied node.
- Fixes Comfy-Org/ComfyUI#12896

## Root Cause

The paste handler in `usePaste.ts` checks clipboard `text/html` for
`data-metadata` (serialized node data) **before** falling through to
litegraph's default paste. When a user copies a node, then copies a web
image, the browser clipboard may retain the old `data-metadata` in
`text/html` while the image data is not available as a
`DataTransferItem` file. This causes the stale node to be pasted instead
of the image.

## Fix

Skip `pasteClipboardItems()` when a media node is selected, allowing the
paste to fall through to litegraph's default handler which can handle
the clipboard content appropriately.

## Test plan

- [x] Added unit test verifying node metadata paste is skipped when
media node is selected
- [x] Manual: Copy a node → copy a web image → select LoadImage node →
Ctrl+V → verify image is pasted, not the node


## AS IS 


https://github.com/user-attachments/assets/210d77d3-5c49-4e38-91b7-b9d9ea0e7ca0



## TO BE



https://github.com/user-attachments/assets/b68e4582-0c57-48b8-9ed9-0b3243bb1554




🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9773-fix-skip-node-metadata-paste-when-media-node-is-selected-3216d73d3650814d92dadcd0c0ec79c7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-12 08:44:11 -07:00
Johnpaul Chiwetelu
ef477d0381 feat: Warn when binding browser-reserved shortcuts (#9406)
## Summary

Show a non-blocking warning in the keybinding edit dialog when users try
to bind shortcuts that browsers intercept (e.g. Ctrl+T, Ctrl+W, F12).

## Changes

- **What**: Add `RESERVED_BY_BROWSER` set of known browser-intercepted
shortcuts, `isBrowserReserved` getter on `KeyComboImpl`, and a warning
`<Message>` in the keybinding edit dialog. Users can still save the
binding.

## Review Focus

Whether the list of browser-reserved shortcuts is comprehensive enough,
and whether a non-blocking warning (vs blocking) is the right UX choice.

## Before


https://github.com/user-attachments/assets/5abfc062-5ed1-4fcd-b394-ff98221d82a8

## After



https://github.com/user-attachments/assets/12a49e24-051f-4579-894a-164dbf1cb7b7


Fixes #1087

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9406-feat-Warn-when-binding-browser-reserved-shortcuts-31a6d73d36508162a021e88ab76914f6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Kelly Yang <124ykl@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: Dante <bunggl@naver.com>
Co-authored-by: Deep Mehta <42841935+deepme987@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Yourz <crazilou@vip.qq.com>
Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
2026-03-12 16:31:09 +01:00
Christian Byrne
37c6ddfcd9 chore: add backport-management agent skill (#9619)
Adds a reusable agent skill for managing cherry-pick backports across
stable release branches.

## What
Agent skill at `.claude/skills/backport-management/` with routing-table
SKILL.md + 4 reference files (discovery, analysis, execution, logging).

## Why
Codifies lessons from backporting 57 PRs across cloud/1.41, core/1.41,
and core/1.40. Makes future backport sessions faster and less
error-prone.

## Key learnings baked in
- Cloud-only PRs must not be backported to `core/*` branches (wasted
effort)
- Wave verification (`pnpm typecheck`) between batches to catch breakage
early
- Human review required for non-trivial conflict resolutions before
admin-merge
- MUST vs SHOULD decision guide with clear criteria
- Continuous backporting preference over bulk sessions
- Mermaid diagram as final session deliverable
- Conflict triage table (never skip based on file count alone)
- `gh api` for labels instead of `gh pr edit` (Projects Classic
deprecation)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9619-chore-add-backport-management-agent-skill-31d6d73d3650815b9808c3916b8e3343)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-12 04:55:01 -07:00
jaeone94
55c42ee484 [bugfix] Asset widget search matches display label (#9774)
## Summary
Asset widget dropdown search only matched against `item.name`
(filename), but users see `item.label` (display name). Now searches both
fields so filtering matches what is visually displayed.

## Changes
- **What**: `defaultSearcher` in `FormDropdown` now matches against both
`name` and `label` fields
- Added 3 unit tests covering label-based search scenarios

## Review Focus
- The change only affects cloud asset mode where `name` (filename) and
`label` (display name) differ. In local mode, `label` is either
`undefined` or identical to `name`, so behavior is unchanged.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9774-bugfix-Asset-widget-search-matches-display-label-3216d73d365081ca8befdf7260c66a26)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-12 20:40:44 +09:00
Dante
b04db536a1 fix: restore correct workflow on page reload (#9318)
## Summary

- Fixes incorrect workflow loading after page refresh when the active
workflow is saved and unmodified
- Adds saved-workflow fallback in `loadPreviousWorkflowFromStorage()`
before falling back to latest draft
- Calls `openWorkflow()` in `restoreWorkflowTabsState()` to activate the
correct tab after restoration

- Fixes #9317

## Test plan

- [x] Unit tests for saved-workflow fallback (draft missing, saved
workflow exists)
- [x] Unit tests for correct tab activation after restoration
- [x] Unit tests for existing behavior preservation (draft preferred
over saved, no-session-path fallback)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All 117 persistence tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9318-fix-restore-correct-workflow-on-page-reload-3166d73d365081ba9139f7c23c917aa4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-03-12 20:25:10 +09:00
pythongosssss
975d6a360d fix: switching tabs in app mode clearing outputs (#9745)
## Summary

- remove reset on exiting app mode and instead cleanup at specific
stages instead of a reset all
- more job<->workflow specificity updates
- ensure pending data is cleared up and doesnt leak over time

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9745-fix-switching-tabs-in-app-mode-clearing-outputs-3206d73d365081038cb0c83f0d953e71)
by [Unito](https://www.unito.io)
2026-03-12 03:27:52 -07:00
Christian Byrne
7e137d880b docs: add change tracker architecture documentation (#9767)
Documents the ChangeTracker undo/redo system: how checkState() works,
all automatic triggers, when manual calls are needed, transaction
guards, and key invariants.

Companion to #9623 which fixed missing checkState() calls in Vue
widgets.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9767-docs-add-change-tracker-architecture-documentation-3216d73d365081268c27c54e0d5824e6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-12 03:03:07 -07:00
Dante
8db6fb7733 feat: replace PrimeVue Galleria/Skeleton with custom DisplayCarousel and ImagePreview (#9712)
## Summary
- Replace `primevue/galleria` with custom `DisplayCarousel` component
featuring Single (carousel) and Grid display modes
- Hover action buttons (mask, download, remove) appear on image
hover/focus
- Thumbnail strip with prev/next navigation; arrows at edges, thumbnails
centered
- Grid mode uses fixed 56px image tiles matching Figma spec
- Replace `primevue/skeleton` and `useToast()` in `ImagePreview` with
`Skeleton.vue` and `useToastStore()`
- Rename `WidgetGalleria` → `DisplayCarousel` across registry, stories,
and tests
- Add Storybook stories for both `DisplayCarousel` and `ImagePreview`
- Retain `WidgetGalleriaOriginal` with its own story for side-by-side
comparison

## Test plan
- [x] Unit tests pass (30 DisplayCarousel + 21 ImagePreview)
- [x] `pnpm typecheck` clean
- [x] `pnpm lint` clean
- [x] `pnpm knip` clean
- [x] Visual verification via Storybook: hover controls, nav, grid mode,
single/grid toggle
- [x] Manual Storybook check: Components/Display/DisplayCarousel,
Components/Display/ImagePreview


## screenshot
<img width="604" height="642" alt="스크린샷 2026-03-12 오후 2 01 51"
src="https://github.com/user-attachments/assets/94df3070-9910-470b-a8f5-5507433ef6e6"
/>
<img width="609" height="651" alt="스크린샷 2026-03-12 오후 2 04 47"
src="https://github.com/user-attachments/assets/3d9884b4-f1bd-4ef5-957a-c7cf7fdc04d8"
/>
<img width="729" height="681" alt="스크린샷 2026-03-12 오후 2 04 49"
src="https://github.com/user-attachments/assets/715f9367-17a3-4d7b-b81f-a7cd6bd446bf"
/>
<img width="534" height="460" alt="스크린샷 2026-03-12 오후 2 05 39"
src="https://github.com/user-attachments/assets/b810eee2-55cb-4dbd-aaca-6331527d13ca"
/>


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:10:42 +09:00
Deep Mehta
7c2c59b9fb fix: center ComfyUI logo in sidebar menu button with chevron (#9722)
## Summary

Center the ComfyUI logo in the sidebar menu button after chevron icon
was added, and add padding to floating sidebar item groups.

## Changes

Before / after:
<img width="127" height="417" alt="image"
src="https://github.com/user-attachments/assets/aa327fd9-5550-49aa-b28d-a90435e9d1bc"
/>


- **What**: Use negative margin on chevron icon so it doesn't affect
logo centering. Add `p-0.5` padding to floating sidebar item groups.

## Review Focus

Visual alignment of the logo in both floating and connected sidebar
modes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9722-fix-center-ComfyUI-logo-in-sidebar-menu-button-with-chevron-31f6d73d3650811a9392cda3070310f9)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-03-12 01:10:26 -07:00
jaeone94
2f7f3c4e56 [feat] Surface missing models in Errors tab (Cloud) (#9743)
## Summary
When a workflow is loaded with missing models, users currently have no
way to identify or resolve them from within the UI. This PR adds a full
missing-model detection and resolution pipeline that surfaces missing
models in the Errors tab, allowing users to install or import them
without leaving the editor.

## Changes

### Missing Model Detection
- Scan all COMBO widgets across root graph and subgraphs for model-like
filenames during workflow load
- Enrich candidates with embedded workflow metadata (url, hash,
directory) when available
- Verify asset-supported candidates against the asset store
asynchronously to confirm installation status
- Propagate missing model state to `executionErrorStore` alongside
existing node/prompt errors

### Errors Tab UI — Model Resolution
- Group missing models by directory (e.g. `checkpoints`, `loras`, `vae`)
with collapsible category cards
- Each model row displays:
  - Model name with copy-to-clipboard button
  - Expandable list of referencing nodes with locate-on-canvas button
- **Library selector**: Pick an alternative from the user's existing
models to substitute the missing model with one click
- **URL import**: Paste a Civitai or HuggingFace URL to import a model
directly; debounced metadata fetch shows filename and file size before
confirming; type-mismatch warnings (e.g. importing a LoRA into
checkpoints directory) are surfaced with an "Import Anyway" option
- **Upgrade prompt**: In cloud environment, free-tier subscribers are
shown an upgrade modal when attempting URL import
- Separate "Import Not Supported" section for custom-node models that
cannot be auto-resolved
- Status card with live download progress, completion, failure, and
category-mismatch states

### Canvas Integration
- Highlight nodes and widgets that reference missing models with error
indicators
- Propagate missing-model badges through subgraph containers so issues
are visible at every graph level

### Code Cleanup
- Simplify `surfacePendingWarnings` in workflowService, remove stale
widget-detected model merging logic
- Add `flattenWorkflowNodes` utility to workflowSchema for traversing
nested subgraph structures
- Extract `MissingModelUrlInput`, `MissingModelLibrarySelect`,
`MissingModelStatusCard` as focused single-responsibility components

## Testing
- Unit tests for scan pipeline (`missingModelScan.test.ts`): enrichment,
skip-installed, subgraph flattening
- Unit tests for store (`missingModelStore.test.ts`): state management,
removal helpers
- Unit tests for interactions (`useMissingModelInteractions.test.ts`):
combo select, URL input, import flow, library confirm
- Component tests for `MissingModelCard` and error grouping
(`useErrorGroups.test.ts`)
- Updated `workflowService.test.ts` and `workflowSchema.test.ts` for new
logic

## Review Focus
- Missing model scan + enrichment pipeline in `missingModelScan.ts`
- Interaction composable `useMissingModelInteractions.ts` — URL metadata
fetch, library install, upload fallback
- Store integration and canvas-level error propagation

## Screenshots 


https://github.com/user-attachments/assets/339a6d5b-93a3-43cd-98dd-0fb00681b66f



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9743-feat-Surface-missing-models-in-Errors-tab-Cloud-3206d73d365081678326d3a16c2165d8)
by [Unito](https://www.unito.io)
2026-03-12 16:21:54 +09:00
Christian Byrne
4c00d39ade fix: app mode widgets disappear after hard refresh (#9621)
## Summary

Fix all app mode widgets (including seed) disappearing after hard
refresh due to a race condition in `pruneLinearData` and a missing
reactivity dependency in `mappedSelections`.

## Changes

- **What**: Guard `pruneLinearData` with `!ChangeTracker.isLoadingGraph`
so inputs are preserved while `rootGraph.configure()` hasn't populated
nodes yet. Add `graphNodes` dependency to `mappedSelections` computed in
`LinearControls.vue` so it re-evaluates when the graph finishes
configuring.

## Review Focus

The core fix is a one-line guard change: `app.rootGraph &&
!ChangeTracker.isLoadingGraph` instead of just `app.rootGraph`. The
previous guard failed because `rootGraph` exists as an empty graph
during loading — `resolveNode()` returns `undefined` for all nodes and
everything gets filtered out.

Fixes COM-16193

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9621-fix-app-mode-widgets-disappear-after-hard-refresh-31d6d73d3650811193f5e1bc8f3c15c8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-11 23:54:27 -07:00
Dante
f1fc5fa9b3 feat: add DropZone Storybook coverage for file upload states (#9690)
## Summary
- align the linear-mode `DropZone` upload indicator with the Figma file
upload states
- add a co-located Storybook story for the default and hover variants
- add a `forceHovered` preview prop so Storybook can render the hover
state deterministically

## Validation
- `pnpm typecheck` (run in the original workspace with dependencies
installed)
- `pnpm lint` (passes with one pre-existing warning in
`src/lib/litegraph/src/ContextMenu.ts`)
- Storybook smoke check is currently blocked by an existing workspace
issue: `vite-plugin-inspect` fails with `Can not found environment
context for client`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9690-feat-add-DropZone-Storybook-coverage-for-file-upload-states-31f6d73d365081ae9eabdde6b5915f26)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-11 23:39:08 -07:00
Benjamin Lu
c111fb7758 fix: rename docked queue panel setting (#9620)
## Summary

Rename the `Comfy.Queue.QPOV2` settings label to `Docked job
history/queue panel` to improve searchability/discoverability in the
settings UI.

## Changes

- **What**: Updated the visible setting name in the core settings
definition and the English locale string.

## Review Focus

The change is intentionally limited to the display label. The persisted
setting key remains `Comfy.Queue.QPOV2`, so existing user configuration
is preserved.

## Screenshots (if applicable)

Not applicable.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9620-fix-rename-docked-queue-panel-setting-31d6d73d36508189a2d1d3a621739a22)
by [Unito](https://www.unito.io)
2026-03-11 23:35:16 -07:00
Kelly Yang
65655ba35f fix: update WidgetLayoutField border styling (#9456)
## Summary

This makes the focus ring only appear on keyboard navigation (Tab), not
on mouse click for widgets like toggle switches, while text inputs still
show the ring on click since browsers apply ` :focus-visible` to them.

## Mozilla Standard

Legacy `focus-within` triggers a highlight ring on every mouse click,
creating unnecessary visual noise during canvas navigation. Following
[MDN
standards](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible),
`:focus-visible` only triggers the highlight when the browser determines
a visual cue is needed (e.g., keyboard navigation).

Using the `:has()` relational selector allows the container to react to
the state of its children natively in CSS. Removes the need for Vue
event listeners or complex state bubbling to highlight the field border.
This reduces JavaScript overhead and simplifies component logic. FYI
[MDN
:has()](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has).

Reordered Tailwind classes to move `transition-all` to the end,
following official best practices. Groups layout/shape first, followed
by interaction states, and finally animations. This improves code
readability and maintainability.


## Screenshots 

before

<img width="558" height="252" alt="12efd5721fb792a7e2dab7e022c2bed6"
src="https://github.com/user-attachments/assets/f881fe13-9f4f-40fd-a8cc-f438b1ba4bde"
/>
 
after
<img width="538" height="235" alt="e5ffec0a34d3b237c4fca9818ec598dd"
src="https://github.com/user-attachments/assets/5ada4112-64bd-48a4-9e9c-b59de6984370"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9456-fix-update-WidgetLayoutField-border-styling-31b6d73d36508193a31ed02bfdef414f)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-03-11 23:34:40 -07:00
Terry Jia
852d77159e fix: prevent WebGLRenderer leak in app mode 3D preview (#9766)
## Summary
Reuse the Load3d instance when switching between 3D results in app mode
instead of creating a new WebGLRenderer each time. Add onUnmounted
cleanup to Preview3d to release WebGL resources when the component is
removed.

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/c9818d10-941f-4994-9b48-2710c88454e7



after


https://github.com/user-attachments/assets/36361763-6800-4bc8-8089-14d64b7fcd16

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9766-fix-prevent-WebGLRenderer-leak-in-app-mode-3D-preview-3216d73d365081e19305d7255b71bc49)
by [Unito](https://www.unito.io)
2026-03-12 01:12:20 -04:00
Jin Yi
b61029b9da fix: show correct empty state on Missing tab instead of misleading registry error (#9640)
## Summary

Show tab-specific empty state messages instead of a misleading registry
connection error on tabs that don't depend on the Manager API.

## Changes

- **What**: Scoped `comfyManagerStore.error` to only affect tabs that
actually use the Manager API (AllInstalled, UpdateAvailable,
Conflicting). Missing and Workflow tabs use the registry directly, so
the Manager API error is irrelevant to them. Previously, any prior
Manager API failure would cause `"Error connecting to the Comfy Node
Registry"` to appear on the Missing tab even when there are simply no
missing nodes.

## Review Focus

The `isManagerErrorRelevant` computed only shows the error for Manager
API-dependent tabs. Verify that AllInstalled/UpdateAvailable/Conflicting
still correctly show the error when Manager API fails.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9640-fix-show-correct-empty-state-on-Missing-tab-instead-of-misleading-registry-error-31e6d73d365081868da1d226a9bb5fdc)
by [Unito](https://www.unito.io)
2026-03-12 13:40:29 +09:00
Christian Byrne
0a62ea0b2c fix: call checkState after image input changes for proper undo tracking (#9623)
## Summary

Image input changes (dropdown selection and file upload) in app/linear
mode did not create their own undo entries, causing undo to skip or
bundle image changes with subsequent actions.

## Changes

- **What**: Add explicit `checkState()` calls in
`WidgetSelectDropdown.vue` after `modelValue` is set in
`updateSelectedItems()` (dropdown selection) and `handleFilesUpdate()`
(file upload), ensuring each image change gets its own undo entry.

## Review Focus

The fix is intentionally scoped to `WidgetSelectDropdown` rather than
the generic `updateHandler` in `NodeWidgets.vue`, which would create
excessive undo entries for text inputs. The pattern follows existing
usage in `useSelectedNodeActions.ts` and other composables.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9623-fix-call-checkState-after-image-input-changes-for-proper-undo-tracking-31d6d73d3650814781dbca5db459ab6d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-11 19:44:16 -07:00
Dante
cccc0944a0 fix: restore widget.inputEl backward compatibility for custom nodes (#9759)
## Summary

Restores `widget.inputEl` assignment on STRING multiline widgets that
was removed in commit a7c211516 (PR #8594) when it was renamed to
`widget.element`. Custom nodes (e.g. comfyui-custom-scripts) rely on
`widget.inputEl` to call `addEventListener` or set `readOnly`.

- Fixes Comfy-Org/ComfyUI#12893

## Test plan

- Verify custom nodes that access `widget.inputEl` on STRING widgets
work correctly
- Verify `widget.element` still works as before

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:48:22 +00:00
Kelly Yang
aadec87bff fix(minimap): minimap re-render/perf issue (#9741)
## Summary

Fix #9732

To clarify how preventing the 60 FPS object assignment solves the
`vue-i18n` (intlify) issue, here is the complete chain reaction leading
to the performance loop:

1. The Root Cause: In `useMinimapViewport.ts`, `useRafFn` acts as a
timer bound to the browser's **refresh rate** (60 FPS). In the original
code, it unconditionally executed the `viewportTransform.value = { ... }
`assignment 60 times a second.

2. Vue's Reactivity Interception: Because `viewportTransform` is a
reactive variable (`ref`), updating it causes its corresponding
**computed** property (`viewportStyles`) to register a data dependency
update.

3. Forced Re-rendering: The `<template> ` in `MiniMap.vue` is bound to
`:style="viewportStyles"`. Since the dependent value changed, Vue's
Virtual DOM decides: "I must re-render the entire `MiniMap.vue`
interface **60 times** per second to ensure the element positions are
up-to-date!"

4. The Victim Emerges: Inside the template of `MiniMap.vue`, there are
several internationalization translation functions: `<button
:aria-label="$t('g.settings')" ...> <button :aria-label="$t('g.close')"
...> `In Vue, whenever a component re-renders, all functions within its
template (including `$t()`) must be re-evaluated. Because the component
was being forced to re-render **60 times** per second, and there are
approximately **6 calls** to `$t()` within this UI, it multiplied into
60 × 6 = **360** intlify compilation and evaluate events per second.


## Solution
Only assemble objects and hand them over to Vue for rendering when the
mouse is actually dragging the canvas.

By extracting the math into **stack-allocated** primitive variables `(x,
y, w, h) `and strictly comparing them, it completely halts the CPU burn
at the source with minimal runtime overhead.

## Screenshot

before
<img width="1820" height="908" alt="image"
src="https://github.com/user-attachments/assets/b48d1e76-6498-47c0-af41-e0594d4e7e2f"
/>

after
<img width="1566" height="486" alt="image"
src="https://github.com/user-attachments/assets/5848b7b7-c57c-494f-a99e-4f7c92889ed0"
/>
2026-03-11 20:20:51 -04:00
Robin Huang
f5088d6cfb feat: add remote config support for PostHog debug mode (#9755)
Allow PostHog debug mode to be toggled at runtime via the
`/api/features` endpoint (`posthog_debug`), falling back to
`VITE_POSTHOG_DEBUG` env var.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9755-feat-add-remote-config-support-for-PostHog-debug-mode-3206d73d365081fca3afd4cfef117eb1)
by [Unito](https://www.unito.io)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:11:07 -07:00
Robin Huang
fd7ce3a852 feat: add telemetry for workflow save and default view (#9734)
Add two new telemetry events: `app:workflow_saved` (with `is_app` and
`is_new` metadata) and `app:default_view_set` for App Builder (with the
chosen view mode). Instrumented in workflowService and
useAppSetDefaultView.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9734-feat-add-telemetry-for-workflow-save-and-default-view-3206d73d3650814e8678f83c8419625f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:31:21 -07:00
Dante
08a2f8ae15 ci: deploy Storybook to fixed production URL on main merge (#9373)
## Summary
- Add `deploy-production` job to Storybook CI workflow
- Triggers on push to `main` branch
- Deploys to fixed Cloudflare Pages URL:
`https://comfy-storybook.pages.dev`
- PR preview deployments remain unchanged

## Linked Issues
- Notion:
[COM-15826](https://www.notion.so/Deploy-Storybook-to-fixed-production-URL-on-main-branch-merge-3196d73d36508159a875d42694a619d1)

## Test Plan
- [ ] Merge to main and verify `deploy-production` job runs in GitHub
Actions
- [ ] Confirm `https://comfy-storybook.pages.dev` serves latest
Storybook
- [ ] Verify existing PR preview deployments still work
- [ ] Verify other jobs (comment, chromatic) are not affected by the new
trigger

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9373-ci-deploy-Storybook-to-fixed-production-URL-on-main-merge-3196d73d3650816795ead1a5b839a571)
by [Unito](https://www.unito.io)
2026-03-11 16:38:29 +01:00
Johnpaul Chiwetelu
7b9f24f515 fix: Rename keybindingService.forwarding.test.ts to reflect canvas keybinding tests (#9498) (#9658) 2026-03-11 13:50:54 +01:00
AustinMroz
faed80e99a Support tooltips on DynamicCombos (#9717)
Tooltips are normally resolved through the node definition. Since
DynamicCombo added widgets are nested in the spec definition, this
lookup fails to find them. This PR makes it so that when a widget is
dynamically added using `litegraphService:addNodeInput`, any 'tooltiptip
defined in the provided inputSpec is applied on the widget.

The tooltip system does not current support tooltips for dynamiclly
added inputs. That can be considered for a followup PR.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9717-Support-tooltips-on-DynamicCombos-31f6d73d365081dc93f9eadd98572b3c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-11 00:17:37 -07:00
Robin Huang
b129d64c5d fix: update PostHog api_host fallback domain (#9733)
Update the PostHog `api_host` fallback from `ph.comfy.org` to
`t.comfy.org`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9733-fix-update-PostHog-api_host-fallback-domain-3206d73d36508107a5d1e1fdfd3ccaec)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:44:26 -07:00
Jin Yi
4c9b83a224 fix: resolve extraneous attrs warning in TreeExplorerV2Node (#9735)
## Summary

Fix Vue warning about extraneous non-props attributes (`data-index`,
`style`) in `TreeExplorerV2Node`, which caused the Node Library sidebar
to freeze.

## Changes

- **What**: Added `defineOptions({ inheritAttrs: false })` and
`v-bind="$attrs"` on both node/folder `<div>` elements so the
virtualizer's positioning attributes are properly applied to the
rendered DOM.

## Review Focus

`TreeExplorerV2Node` has a fragment root (`<TreeItem as-child>` +
`<Teleport>`), so Vue cannot auto-inherit attrs. The virtualizer's
`style` (positioning) merges cleanly with `rowStyle` (`paddingLeft`
only).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9735-fix-resolve-extraneous-attrs-warning-in-TreeExplorerV2Node-3206d73d3650817c8619e2145e98813d)
by [Unito](https://www.unito.io)
2026-03-11 13:34:44 +09:00
Dante
e973efb44a fix: improve canvas menu keyboard navigation and ARIA accessibility (#9526)
## Summary

The canvas mode selector popover (Select/Hand mode) uses plain `div`
elements for its menu items, making them completely inaccessible to
keyboard-only users and screen readers. This PR replaces them with
proper semantic HTML and ARIA attributes.

## Problem (AS-IS)

As reported in #9519, the canvas mode selector popover has the following
accessibility issues:

1. **Menu items are `div` elements** — they cannot receive keyboard
focus, so users cannot Tab into the popover or activate items with
Enter/Space. Keyboard-only users are completely locked out of switching
between Select and Hand (pan) mode via the popover.

2. **No ARIA roles** — screen readers announce the popover content as
generic text rather than an interactive menu. Users relying on assistive
technology have no way to understand that these are selectable options.

3. **No keyboard navigation within the popover** — even if a user
somehow focuses an item, there are no ArrowUp/ArrowDown handlers to move
between options, which is the standard interaction pattern for
`role="menu"` widgets (WAI-ARIA Menu Pattern).

4. **Decorative icons are not hidden from assistive technology** — icon
elements (`<i>` tags) are exposed to screen readers, adding noise to the
announcement.

5. **The bottom toolbar (`GraphCanvasMenu`) lacks semantic grouping** —
the `ButtonGroup` container has no `role="toolbar"` or `aria-label`, so
screen readers cannot convey that these buttons form a related control
group.

> Note: Pan mode itself already has keyboard shortcuts (`H` for
Hand/Lock, `V` for Select/Unlock), but the popover UI that surfaces
these options is not keyboard-accessible.

## Solution (TO-BE)

1. **Replace `div` → `button`** for menu items in `CanvasModeSelector` —
buttons are natively focusable and activatable via Enter/Space without
extra work.

2. **Add `role="menu"` on the container and `role="menuitem"` on each
option** — screen readers now announce "Canvas Mode menu" with two menu
items.

3. **Add ArrowUp/ArrowDown keyboard navigation** with wrap-around —
pressing ArrowDown on "Select" moves focus to "Hand", and vice versa.
This follows the WAI-ARIA Menu Pattern.

4. **Add `aria-label` to each menu item and `aria-hidden="true"` to
decorative icons** — screen readers announce "Select" / "Hand" clearly
without icon noise.

5. **Add `role="toolbar"` with `aria-label="Canvas Toolbar"` to the
`ButtonGroup`** — screen readers identify the bottom control bar as a
coherent toolbar.

## Changes

- **What**: Accessibility improvements to `CanvasModeSelector` popover
and `GraphCanvasMenu` toolbar
- **Dependencies**: None — only HTML/ARIA attribute changes and two new
i18n keys (`canvasMode`, `canvasToolbar`)

## Review Focus

- Verify the `button` elements render visually identical to the previous
`div` elements (same padding, hover styles, cursor)
- Confirm ArrowUp/ArrowDown navigation works correctly in the popover
- Check that screen readers announce the menu and toolbar correctly

Fixes #9519

> Note: The issue also requests Space-bar hold-to-pan, Tab through node
ports, and link creation mode keyboard shortcuts. These are significant
new features beyond the scope of this accessibility fix and should be
tracked separately.

## Test plan

- [x] Unit tests for ARIA roles, button elements, aria-labels,
aria-hidden, and arrow key navigation (7 tests)
- [ ] Manual: open canvas mode selector popover → Tab focuses first item
→ ArrowDown/ArrowUp navigates → Enter/Space selects
- [ ] Screen reader: verify "Canvas Mode menu" with "Select" and "Hand"
menu items are announced

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9526-fix-improve-canvas-menu-keyboard-navigation-and-ARIA-accessibility-31c6d73d3650814c9487ecf748cf6a99)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:15:17 +09:00
Luke Mino-Altherr
9ecb100d11 fix: make zPreviewOutput accept text-only job outputs (#9724)
## Summary

Fixes Zod validation crash when the jobs batch contains text-only
preview outputs (e.g. from LLM nodes), which caused the entire Assets
sidebar to show nothing.

## Changes

- **What**: Made `filename`, `subfolder`, and `type` optional in
`zPreviewOutput` and added `.passthrough()` for extra fields like
`content`. Text-only jobs are safely filtered out downstream by
`supportsPreview`.
- Added tests in `fetchJobs.test.ts` verifying a mixed batch (image +
text-only + no-preview) parses successfully.
- Added test in `assetsStore.test.ts` verifying text-only jobs are
skipped without breaking sibling image jobs. Improved `TaskItemImpl`
mock to realistically handle media types.

## Review Focus

- The `zPreviewOutput` schema now uses `.passthrough()` to allow extra
fields from new preview types (like `content` on text previews). This is
consistent with `zRawJobListItem` and `zExecutionError` which also use
`.passthrough()`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9724-fix-make-zPreviewOutput-accept-text-only-job-outputs-31f6d73d36508119a7aef99f9b765ecd)
by [Unito](https://www.unito.io)
2026-03-10 18:18:54 -07:00
181 changed files with 11165 additions and 657 deletions

View File

@@ -0,0 +1,150 @@
---
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

@@ -0,0 +1,68 @@
# 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

@@ -0,0 +1,42 @@
# 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

@@ -0,0 +1,150 @@
# 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

@@ -0,0 +1,96 @@
# 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

@@ -4,6 +4,8 @@ name: 'CI: Tests Storybook'
on:
workflow_dispatch: # Allow manual triggering
pull_request:
push:
branches: [main]
jobs:
# Post starting comment for non-forked PRs
@@ -138,6 +140,29 @@ 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]

1
.gitignore vendored
View File

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

View File

@@ -0,0 +1,47 @@
{
"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('Press keys for new binding')
const input = comfyPage.page.getByPlaceholder('Enter your keybind')
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
@@ -345,7 +345,7 @@ test.describe('Settings', () => {
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('New Blank Workflow')
.getByLabel('Modify keybinding')
.getByText('Save')
await saveButton.click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -0,0 +1,58 @@
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: 65 KiB

After

Width:  |  Height:  |  Size: 64 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

@@ -0,0 +1,91 @@
# 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,6 +35,7 @@ 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

View File

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

View File

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

View File

@@ -43,6 +43,9 @@ export function useAppSetDefaultView() {
const extra = (app.rootGraph.extra ??= {})
extra.linearMode = openAsApp
workflow.changeTracker?.checkState()
useTelemetry()?.trackDefaultViewSet({
default_view: openAsApp ? 'app' : 'graph'
})
closeDialog()
showAppliedDialog(openAsApp)
}

View File

@@ -8,6 +8,7 @@
<!-- Node -->
<div
v-if="item.value.type === 'node'"
v-bind="$attrs"
:class="cn(ROW_CLASS, isSelected && 'bg-comfy-input')"
:style="rowStyle"
draggable="true"
@@ -48,6 +49,7 @@
<!-- Folder -->
<div
v-else
v-bind="$attrs"
:class="cn(ROW_CLASS, isSelected && 'bg-comfy-input')"
:style="rowStyle"
@click.stop="handleClick($event, handleToggle, handleSelect)"
@@ -98,6 +100,10 @@ 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,7 +59,15 @@
:pt="{ bodyCell: 'p-1 min-h-8' }"
>
<template #body="slotProps">
<div class="truncate" :title="slotProps.data.id">
<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"
/>
{{ slotProps.data.label }}
</div>
</template>
@@ -93,44 +101,6 @@
</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"
@@ -147,18 +117,14 @@
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, watchEffect } from 'vue'
import { computed, ref } 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 { KeyComboImpl } from '@/platform/keybindings/keyCombo'
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useEditKeybindingDialog } from '@/composables/useEditKeybindingDialog'
import type { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useCommandStore } from '@/stores/commandStore'
@@ -195,50 +161,16 @@ const commandsData = computed<ICommandData[]>(() => {
})
const selectedCommandData = ref<ICommandData | null>(null)
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)
})
const editKeybindingDialog = useEditKeybindingDialog()
function editKeybinding(commandData: ICommandData) {
currentEditingCommand.value = commandData
newBindingKeyCombo.value = commandData.keybinding
? commandData.keybinding.combo
: null
editDialogVisible.value = true
editKeybindingDialog.show({
commandId: commandData.id,
commandLabel: commandData.label,
currentCombo: commandData.keybinding?.combo ?? null
})
}
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)
@@ -246,40 +178,6 @@ 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

@@ -0,0 +1,56 @@
<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

@@ -0,0 +1,72 @@
<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

@@ -0,0 +1,5 @@
<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

@@ -0,0 +1,153 @@
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,15 +4,21 @@
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" />
<i :class="currentModeIcon" class="block size-4" aria-hidden="true" />
</div>
<i class="icon-[lucide--chevron-down] block size-4 pr-1.5" />
<i
class="icon-[lucide--chevron-down] block size-4 pr-1.5"
aria-hidden="true"
/>
</div>
</Button>
@@ -24,31 +30,54 @@
:close-on-escape="true"
unstyled
:pt="popoverPt"
@show="onPopoverShow"
@hide="onPopoverHide"
>
<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"
<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')"
@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" />
<i class="icon-[lucide--mouse-pointer-2] size-4" aria-hidden="true" />
<span>{{ $t('graphCanvasMenu.select') }}</span>
</div>
<span class="text-[9px] text-text-primary">{{
unlockCommandText
}}</span>
</div>
</button>
<div
class="flex cursor-pointer items-center justify-between rounded-sm px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
<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')"
@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" />
<i class="icon-[lucide--hand] size-4" aria-hidden="true" />
<span>{{ $t('graphCanvasMenu.hand') }}</span>
</div>
<span class="text-[9px] text-text-primary">{{ lockCommandText }}</span>
</div>
</button>
</div>
</Popover>
</template>
@@ -56,7 +85,7 @@
<script setup lang="ts">
import Popover from 'primevue/popover'
import type { ComponentPublicInstance } from 'vue'
import { computed, ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -69,6 +98,8 @@ 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()
@@ -106,6 +137,43 @@ 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

@@ -538,7 +538,7 @@ onMounted(async () => {
// Restore saved workflow and workflow tabs state
await workflowPersistence.initializeWorkflow()
workflowPersistence.restoreWorkflowTabsState()
await workflowPersistence.restoreWorkflowTabsState()
const sharedWorkflowLoadStatus =
await workflowPersistence.loadSharedWorkflowFromUrlIfPresent()

View File

@@ -10,6 +10,8 @@
></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
@@ -30,7 +32,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" />
<i class="icon-[lucide--focus] size-4" aria-hidden="true" />
</Button>
<Button
@@ -44,7 +46,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" />
<i class="icon-[lucide--chevron-down] size-4" aria-hidden="true" />
</span>
</Button>
@@ -59,7 +61,7 @@
:class="minimapButtonClass"
@click="onMinimapToggleClick"
>
<i class="icon-[lucide--map] size-4" />
<i class="icon-[lucide--map] size-4" aria-hidden="true" />
</Button>
<Button
@@ -78,7 +80,7 @@
:style="stringifiedMinimapStyles.buttonStyles"
@click="onLinkVisibilityToggleClick"
>
<i class="icon-[lucide--route-off] size-4" />
<i class="icon-[lucide--route-off] size-4" aria-hidden="true" />
</Button>
</ButtonGroup>
</div>

View File

@@ -13,6 +13,7 @@ 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'
@@ -36,6 +37,7 @@ import TabErrors from './errors/TabErrors.vue'
const canvasStore = useCanvasStore()
const executionErrorStore = useExecutionErrorStore()
const missingModelStore = useMissingModelStore()
const rightSidePanelStore = useRightSidePanelStore()
const settingStore = useSettingStore()
const { t } = useI18n()
@@ -43,6 +45,8 @@ const { t } = useI18n()
const { hasAnyError, allErrorExecutionIds, activeMissingNodeGraphIds } =
storeToRefs(executionErrorStore)
const { activeMissingModelGraphIds } = storeToRefs(missingModelStore)
const { findParentGroup } = useGraphHierarchy()
const { selectedItems: directlySelectedItems } = storeToRefs(canvasStore)
@@ -118,12 +122,21 @@ 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
hasMissingNodeSelected.value ||
hasMissingModelSelected.value
)
})
@@ -314,7 +327,11 @@ function handleTitleCancel() {
:value="tab.value"
>
{{ tab.label() }}
<i v-if="tab.icon" :class="cn(tab.icon, 'size-4')" />
<i
v-if="tab.icon"
aria-hidden="true"
: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">
<div class="min-w-0 flex-1 overflow-y-auto" aria-live="polite">
<TransitionGroup tag="div" name="list-scale" class="relative">
<div
v-if="filteredGroups.length === 0"
@@ -32,11 +32,7 @@
:key="group.title"
:collapse="isSectionCollapsed(group.title) && !isSearching"
class="border-b border-interface-stroke"
:size="
group.type === 'missing_node' || group.type === 'swap_nodes'
? 'lg'
: 'default'
"
:size="getGroupSize(group)"
@update:collapse="setSectionCollapsed(group.title, $event)"
>
<template #label>
@@ -130,6 +126,14 @@
@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>
@@ -187,12 +191,14 @@ 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()
@@ -211,6 +217,15 @@ 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) !==
@@ -226,6 +241,7 @@ const {
errorNodeCache,
missingNodeCache,
missingPackGroups,
missingModelGroups,
swapNodeGroups
} = useErrorGroups(searchQuery, t)
@@ -283,6 +299,10 @@ 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,3 +23,4 @@ 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,6 +47,13 @@ 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'
@@ -520,4 +527,115 @@ 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,6 +3,7 @@ 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'
@@ -23,6 +24,11 @@ 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
@@ -39,6 +45,9 @@ 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[]
@@ -231,6 +240,7 @@ 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>>({})
@@ -559,6 +569,60 @@ 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>()
@@ -566,7 +630,11 @@ export function useErrorGroups(
processNodeErrors(groupsMap)
processExecutionError(groupsMap)
return [...buildMissingNodeGroups(), ...toSortedGroups(groupsMap)]
return [
...buildMissingNodeGroups(),
...buildMissingModelGroups(),
...toSortedGroups(groupsMap)
]
})
const tabErrorGroups = computed<ErrorGroup[]>(() => {
@@ -580,7 +648,11 @@ export function useErrorGroups(
? toSortedGroups(regroupByErrorMessage(groupsMap))
: toSortedGroups(groupsMap)
return [...buildMissingNodeGroups(), ...executionGroups]
return [
...buildMissingNodeGroups(),
...buildMissingModelGroups(),
...executionGroups
]
})
const filteredGroups = computed<ErrorGroup[]>(() => {
@@ -615,6 +687,7 @@ export function useErrorGroups(
missingNodeCache,
groupedErrorMessages,
missingPackGroups,
missingModelGroups,
swapNodeGroups
}
}

View File

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

View File

@@ -48,13 +48,24 @@
</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, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
computed,
defineAsyncComponent,
nextTick,
onBeforeUnmount,
onMounted,
ref,
watch
} from 'vue'
import { useI18n } from 'vue-i18n'
import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
@@ -62,7 +73,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 } from '@/platform/distribution/types'
import { isCloud, isDesktop, isNightly } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -78,6 +89,13 @@ 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,6 +90,7 @@ 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 {
@@ -135,10 +136,6 @@ 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,6 +236,7 @@ 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'
@@ -569,7 +570,7 @@ const handleZoomClick = (asset: AssetItem) => {
const dialogStore = useDialogStore()
dialogStore.showDialog({
key: 'asset-3d-viewer',
title: asset.display_name || asset.name,
title: getAssetDisplayName(asset),
component: Load3dViewerContent,
props: {
modelUrl: asset.preview_url || ''

View File

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

View File

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

View File

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

View File

@@ -59,10 +59,7 @@ function mkFileUrl(props: { ref: ImageRef; preview?: boolean }): string {
}
const pathPlusQueryParams = api.apiURL(
'/view?' +
params.toString() +
app.getPreviewFormatParam() +
app.getRandParam()
'/view?' + params.toString() + app.getPreviewFormatParam()
)
const imageElement = new Image()
imageElement.crossOrigin = 'anonymous'

View File

@@ -17,7 +17,7 @@ type MockTask = {
executionEndTimestamp?: number
previewOutput?: {
isImage: boolean
urlWithTimestamp: string
url: string
}
}
@@ -94,7 +94,7 @@ describe(useQueueNotificationBanners, () => {
if (previewUrl) {
task.previewOutput = {
isImage,
urlWithTimestamp: previewUrl
url: previewUrl
}
}

View File

@@ -231,7 +231,7 @@ export const useQueueNotificationBanners = () => {
completedCount++
const preview = task.previewOutput
if (preview?.isImage) {
imagePreviews.push(preview.urlWithTimestamp)
imagePreviews.push(preview.url)
}
} else if (state === 'failed') {
failedCount++

View File

@@ -0,0 +1,60 @@
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,7 +357,8 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
}
/**
* Initialize viewer in standalone mode (for asset preview)
* Initialize viewer in standalone mode (for asset preview).
* Creates the Load3d instance once; subsequent calls reuse it.
*/
const initializeStandaloneViewer = async (
containerRef: HTMLElement,
@@ -366,6 +367,11 @@ export const useLoad3dViewer = (node?: LGraphNode) => {
if (!containerRef) return
try {
if (load3d) {
await loadStandaloneModel(modelUrl)
return
}
isStandaloneMode.value = true
load3d = new Load3d(containerRef, {
@@ -392,6 +398,23 @@ 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,6 +595,34 @@ 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,7 +229,10 @@ export const usePaste = () => {
return
}
}
if (pasteClipboardItems(data)) return
const isMediaNodeSelected =
isImageNodeSelected || isVideoNodeSelected || isAudioNodeSelected
if (!isMediaNodeSelected && pasteClipboardItems(data)) return
// No image found. Look for node data
data = data.getData('text/plain')

View File

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

View File

@@ -1,7 +1,6 @@
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { NodeOutputWith } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
type ImageCompareOutput = NodeOutputWith<{
@@ -24,11 +23,10 @@ useExtensionService().registerExtension({
onExecuted?.call(this, output)
const { a_images: aImages, b_images: bImages } = output
const rand = app.getRandParam()
const toUrl = (record: Record<string, string>) => {
const params = new URLSearchParams(record)
return api.apiURL(`/view?${params}${rand}`)
return api.apiURL(`/view?${params}`)
}
const beforeImages =

View File

@@ -2,7 +2,6 @@ import type Load3d from '@/extensions/core/load3d/Load3d'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
class Load3dUtils {
static async generateThumbnailIfNeeded(
@@ -133,8 +132,7 @@ class Load3dUtils {
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder,
app.getRandParam().substring(1)
'subfolder=' + subfolder
].join('&')
return `/view?${params}`

View File

@@ -936,6 +936,8 @@
"batchRename": "إعادة تسمية جماعية",
"beta": "نسخة تجريبية",
"bookmark": "حفظ في المكتبة",
"browserReservedKeybinding": "هذا الاختصار محجوز من قبل بعض المتصفحات وقد يؤدي إلى نتائج غير متوقعة.",
"browserReservedKeybindingTooltip": "هذا الاختصار يتعارض مع اختصارات المتصفح المحجوزة",
"calculatingDimensions": "جارٍ حساب الأبعاد",
"cancel": "إلغاء",
"cancelled": "أُلغي",
@@ -970,6 +972,7 @@
"copy": "نسخ",
"copyAll": "نسخ الكل",
"copyJobId": "نسخ معرف المهمة",
"copySystemInfo": "نسخ معلومات النظام",
"copyToClipboard": "نسخ إلى الحافظة",
"copyURL": "نسخ الرابط",
"core": "النواة",
@@ -1016,6 +1019,7 @@
"enterNewName": "أدخل الاسم الجديد",
"enterNewNamePrompt": "أدخل اسمًا جديدًا:",
"enterSubgraph": "دخول الرسم البياني الفرعي",
"enterYourKeybind": "أدخل اختصارك",
"error": "خطأ",
"errorLoadingImage": "حدث خطأ أثناء تحميل الصورة",
"errorLoadingVideo": "حدث خطأ أثناء تحميل الفيديو",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "تم رفض إذن الميكروفون",
"migrate": "ترحيل",
"missing": "مفقود",
"modifyKeybinding": "تعديل اختصار لوحة المفاتيح",
"more": "المزيد",
"moreOptions": "خيارات إضافية",
"moreWorkflows": "المزيد من سير العمل",
@@ -1093,6 +1098,7 @@
"name": "الاسم",
"newFolder": "مجلد جديد",
"next": "التالي",
"nextImage": "الصورة التالية",
"nightly": "NIGHTLY",
"no": "لا",
"noAudioRecorded": "لم يتم تسجيل أي صوت",
@@ -1126,8 +1132,8 @@
"playRecording": "تشغيل التسجيل",
"playbackSpeed": "سرعة التشغيل",
"playing": "جاري التشغيل",
"pressKeysForNewBinding": "اضغط على المفاتيح لربط جديد",
"preview": "معاينة",
"previousImage": "الصورة السابقة",
"profile": "الملف الشخصي",
"progressCountOf": "من",
"queued": "في قائمة الانتظار",
@@ -1167,6 +1173,7 @@
"resultsCount": "تم العثور على {count} نتيجة",
"running": "يعمل",
"save": "حفظ",
"saveAnyway": "احفظ على أي حال",
"saving": "جارٍ الحفظ",
"scrollLeft": "التمرير لليسار",
"scrollRight": "التمرير لليمين",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "حدد العناصر لعمل نسخة",
"selectItemsToRename": "حدد العناصر لإعادة التسمية",
"selectedFile": "الملف المحدد",
"setAKeybindingForTheFollowing": "تعيين اختصار لوحة المفاتيح لما يلي:",
"setAsBackground": "تعيين كخلفية",
"settings": "الإعدادات",
"shortcutSuffix": " ({shortcut})",
@@ -1200,6 +1208,8 @@
"stopRecording": "إيقاف التسجيل",
"submit": "إرسال",
"success": "نجاح",
"switchToGridView": "التبديل إلى عرض الشبكة",
"switchToSingleView": "التبديل إلى العرض الفردي",
"systemInfo": "معلومات النظام",
"terminal": "الطرفية",
"title": "العنوان",
@@ -1229,6 +1239,8 @@
"you": "أنت"
},
"graphCanvasMenu": {
"canvasMode": "وضع اللوحة",
"canvasToolbar": "شريط أدوات اللوحة",
"fitView": "ملائمة العرض",
"focusMode": "وضع التركيز",
"hand": "يد",
@@ -1465,12 +1477,26 @@
"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": "انقر لمسح قائمة الانتظار"
@@ -1478,6 +1504,7 @@
"rerun": "تشغيل مجدد",
"reuseParameters": "إعادة استخدام المعلمات",
"runCount": "عدد مرات التشغيل:",
"viewGraph": "عرض مخطط العُقد",
"viewJob": "عرض المهمة",
"welcome": {
"buildApp": "إنشاء تطبيق",
@@ -1686,6 +1713,7 @@
"installedSection": "المثبتة",
"missingNodes": "عقد مفقودة",
"notInstalled": "غير مثبت",
"unresolvedNodes": "العُقد غير المحلولة",
"updatesAvailable": "تحديثات متوفرة"
},
"nightlyVersion": "ليلي",
@@ -1732,6 +1760,10 @@
"uninstall": "إلغاء التثبيت",
"uninstallSelected": "إلغاء تثبيت المحدد",
"uninstalling": "جاري إلغاء التثبيت",
"unresolvedNodes": {
"message": "العُقد التالية غير مثبتة ولم يتم العثور عليها في السجل.",
"title": "العُقد المفقودة غير المحلولة"
},
"update": "تحديث",
"updateAll": "تحديث الكل",
"updateSelected": "تحديث المحدد",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "رودان",
"Runway": "رن واي",
"Sora": "سورا",
@@ -2379,6 +2412,33 @@
"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.",
@@ -3238,6 +3298,7 @@
"interrupted": "تم إيقاف التنفيذ",
"legacyMaskEditorDeprecated": "محرر القناع القديم لم يعد مدعوماً وسيتم إزالته قريباً.",
"migrateToLitegraphReroute": "سيتم إزالة عقد إعادة التوجيه في الإصدارات المستقبلية. انقر للترحيل إلى إعادة التوجيه الأصلية في Litegraph.",
"missingModelVerificationFailed": "فشل التحقق من النماذج المفقودة. قد لا تظهر بعض النماذج في علامة تبويب الأخطاء.",
"modelLoadedSuccessfully": "تم تحميل النموذج ثلاثي الأبعاد بنجاح",
"no3dScene": "لا يوجد مشهد ثلاثي الأبعاد لتطبيق الخامة",
"no3dSceneToExport": "لا يوجد مشهد ثلاثي الأبعاد للتصدير",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"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",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +35,10 @@
"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",
@@ -106,6 +110,7 @@
"delete": "Delete",
"rename": "Rename",
"save": "Save",
"saveAnyway": "Save Anyway",
"saving": "Saving",
"no": "No",
"cancel": "Cancel",
@@ -116,7 +121,6 @@
"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",
@@ -261,6 +265,11 @@
"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",
@@ -1052,6 +1061,8 @@
"logoProviderSeparator": " & "
},
"graphCanvasMenu": {
"canvasMode": "Canvas Mode",
"canvasToolbar": "Canvas Toolbar",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"resetView": "Reset View",
@@ -1621,6 +1632,7 @@
"unet": "unet",
"sigmas": "sigmas",
"BFL": "BFL",
"": "",
"Gemini": "Gemini",
"video_models": "video_models",
"gligen": "gligen",
@@ -1655,6 +1667,7 @@
"primitive": "primitive",
"Recraft": "Recraft",
"edit_models": "edit_models",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"animation": "animation",
@@ -1668,7 +1681,6 @@
"style_model": "style_model",
"Tencent": "Tencent",
"textgen": "textgen",
"": "",
"Topaz": "Topaz",
"Tripo": "Tripo",
"Veo": "Veo",
@@ -1955,6 +1967,7 @@
"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",
@@ -3386,6 +3399,33 @@
"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,6 +3319,21 @@
}
}
},
"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.",
@@ -12855,6 +12870,133 @@
}
}
},
"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": "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."
"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."
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Batch count limit",

View File

@@ -936,6 +936,8 @@
"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",
@@ -970,6 +972,7 @@
"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",
@@ -1016,6 +1019,7 @@
"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",
@@ -1085,6 +1089,7 @@
"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",
@@ -1093,6 +1098,7 @@
"name": "Nombre",
"newFolder": "Nueva carpeta",
"next": "Siguiente",
"nextImage": "Imagen siguiente",
"nightly": "NIGHTLY",
"no": "No",
"noAudioRecorded": "No se grabó audio",
@@ -1126,8 +1132,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",
@@ -1167,6 +1173,7 @@
"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",
@@ -1185,6 +1192,7 @@
"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})",
@@ -1200,6 +1208,8 @@
"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",
@@ -1229,6 +1239,8 @@
"you": "Tú"
},
"graphCanvasMenu": {
"canvasMode": "Modo lienzo",
"canvasToolbar": "Barra de herramientas del lienzo",
"fitView": "Ajustar vista",
"focusMode": "Modo de Enfoque",
"hand": "Mano",
@@ -1465,12 +1477,26 @@
"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"
@@ -1478,6 +1504,7 @@
"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",
@@ -1686,6 +1713,7 @@
"installedSection": "INSTALADO",
"missingNodes": "Nodos faltantes",
"notInstalled": "No instalado",
"unresolvedNodes": "Nodos no resueltos",
"updatesAvailable": "Actualizaciones disponibles"
},
"nightlyVersion": "Nocturna",
@@ -1732,6 +1760,10 @@
"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",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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.",
@@ -3238,6 +3298,7 @@
"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,6 +3207,21 @@
}
}
},
"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",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +936,8 @@
"batchRename": "تغییر نام گروهی",
"beta": "آزمایشی",
"bookmark": "ذخیره در کتابخانه",
"browserReservedKeybinding": "این میانبر توسط برخی مرورگرها رزرو شده و ممکن است نتایج غیرمنتظره‌ای داشته باشد.",
"browserReservedKeybindingTooltip": "این میانبر با میانبرهای رزرو شده مرورگر تداخل دارد",
"calculatingDimensions": "در حال محاسبه ابعاد",
"cancel": "لغو",
"cancelled": "لغو شده",
@@ -970,6 +972,7 @@
"copy": "کپی",
"copyAll": "کپی همه",
"copyJobId": "کپی شناسه وظیفه",
"copySystemInfo": "کپی اطلاعات سیستم",
"copyToClipboard": "کپی در کلیپ‌بورد",
"copyURL": "کپی آدرس",
"core": "هسته",
@@ -1016,6 +1019,7 @@
"enterNewName": "نام جدید را وارد کنید",
"enterNewNamePrompt": "نام جدید را وارد کنید:",
"enterSubgraph": "ورود به زیرگراف",
"enterYourKeybind": "کلید میانبر خود را وارد کنید",
"error": "خطا",
"errorLoadingImage": "خطا در بارگذاری تصویر",
"errorLoadingVideo": "خطا در بارگذاری ویدیو",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "دسترسی میکروفون رد شد",
"migrate": "مهاجرت",
"missing": "ناقص",
"modifyKeybinding": "تغییر کلید میانبر",
"more": "بیشتر",
"moreOptions": "گزینه‌های بیشتر",
"moreWorkflows": "workflowهای بیشتر",
@@ -1093,6 +1098,7 @@
"name": "نام",
"newFolder": "پوشه جدید",
"next": "بعدی",
"nextImage": "تصویر بعدی",
"nightly": "نسخه شبانه",
"no": "خیر",
"noAudioRecorded": "هیچ صدایی ضبط نشد",
@@ -1126,8 +1132,8 @@
"playRecording": "پخش ضبط",
"playbackSpeed": "سرعت پخش",
"playing": "در حال پخش",
"pressKeysForNewBinding": "کلیدهای میانبر جدید را فشار دهید",
"preview": "پیش‌نمایش",
"previousImage": "تصویر قبلی",
"profile": "پروفایل",
"progressCountOf": "از",
"queued": "در صف",
@@ -1167,6 +1173,7 @@
"resultsCount": "{count} نتیجه یافت شد",
"running": "در حال اجرا",
"save": "ذخیره",
"saveAnyway": "ذخیره کن",
"saving": "در حال ذخیره",
"scrollLeft": "اسکرول به چپ",
"scrollRight": "اسکرول به راست",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "مواردی برای تکرار انتخاب کنید",
"selectItemsToRename": "مواردی برای تغییر نام انتخاب کنید",
"selectedFile": "فایل انتخاب‌شده",
"setAKeybindingForTheFollowing": "کلید میانبر را برای مورد زیر تنظیم کنید:",
"setAsBackground": "تنظیم به عنوان پس‌زمینه",
"settings": "تنظیمات",
"shortcutSuffix": " ({shortcut})",
@@ -1200,6 +1208,8 @@
"stopRecording": "پایان ضبط",
"submit": "ارسال",
"success": "موفقیت‌آمیز",
"switchToGridView": "تغییر به نمای شبکه‌ای",
"switchToSingleView": "تغییر به نمای تکی",
"systemInfo": "اطلاعات سیستم",
"terminal": "ترمینال",
"title": "عنوان",
@@ -1229,6 +1239,8 @@
"you": "شما"
},
"graphCanvasMenu": {
"canvasMode": "حالت بوم",
"canvasToolbar": "نوار ابزار بوم",
"fitView": "تطبیق با نما",
"focusMode": "حالت تمرکز",
"hand": "دست",
@@ -1465,12 +1477,26 @@
"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": "برای پاک‌سازی صف کلیک کنید"
@@ -1478,6 +1504,7 @@
"rerun": "اجرای مجدد",
"reuseParameters": "استفاده مجدد از پارامترها",
"runCount": "تعداد اجرا: ",
"viewGraph": "مشاهده گراف نود",
"viewJob": "مشاهده وظیفه",
"welcome": {
"buildApp": "ساخت اپلیکیشن",
@@ -1686,6 +1713,7 @@
"installedSection": "نصب شده",
"missingNodes": "Nodeهای مفقود",
"notInstalled": "نصب نشده",
"unresolvedNodes": "نودهای حل‌نشده",
"updatesAvailable": "به‌روزرسانی‌های موجود"
},
"nightlyVersion": "نسخه nightly",
@@ -1732,6 +1760,10 @@
"uninstall": "حذف نصب",
"uninstallSelected": "حذف نصب انتخاب‌شده‌ها",
"uninstalling": "در حال حذف نصب {id}",
"unresolvedNodes": {
"message": "نودهای زیر نصب نشده‌اند و در رجیستری یافت نشدند.",
"title": "نودهای گمشده حل‌نشده"
},
"update": "به‌روزرسانی",
"updateAll": "به‌روزرسانی همه",
"updateSelected": "به‌روزرسانی انتخاب‌شده‌ها",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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 موجود نیستند.",
@@ -3250,6 +3310,7 @@
"interrupted": "اجرا متوقف شد",
"legacyMaskEditorDeprecated": "ویرایشگر mask قدیمی منسوخ شده و به‌زودی حذف خواهد شد.",
"migrateToLitegraphReroute": "nodeهای reroute در نسخه‌های آینده حذف خواهند شد. برای مهاجرت به reroute بومی litegraph کلیک کنید.",
"missingModelVerificationFailed": "تأیید مدل‌های مفقود شده انجام نشد. برخی مدل‌ها ممکن است در برگه خطاها نمایش داده نشوند.",
"modelLoadedSuccessfully": "مدل سه‌بعدی با موفقیت بارگذاری شد",
"no3dScene": "صحنه سه‌بعدی برای اعمال بافت وجود ندارد",
"no3dSceneToExport": "صحنه سه‌بعدی برای صادرات وجود ندارد",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"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",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +936,8 @@
"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é",
@@ -970,6 +972,7 @@
"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",
@@ -1016,6 +1019,7 @@
"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",
@@ -1085,6 +1089,7 @@
"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",
@@ -1093,6 +1098,7 @@
"name": "Nom",
"newFolder": "Nouveau dossier",
"next": "Suivant",
"nextImage": "Image suivante",
"nightly": "NIGHTLY",
"no": "Non",
"noAudioRecorded": "Aucun audio enregistré",
@@ -1126,8 +1132,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",
@@ -1167,6 +1173,7 @@
"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",
@@ -1185,6 +1192,7 @@
"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})",
@@ -1200,6 +1208,8 @@
"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",
@@ -1229,6 +1239,8 @@
"you": "Vous"
},
"graphCanvasMenu": {
"canvasMode": "Mode canevas",
"canvasToolbar": "Barre doutils du canevas",
"fitView": "Adapter la vue",
"focusMode": "Mode focus",
"hand": "Main",
@@ -1465,12 +1477,26 @@
"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"
@@ -1478,6 +1504,7 @@
"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",
@@ -1686,6 +1713,7 @@
"installedSection": "INSTALLÉ",
"missingNodes": "Nœuds manquants",
"notInstalled": "Non installé",
"unresolvedNodes": "Nœuds non résolus",
"updatesAvailable": "Mises à jour disponibles"
},
"nightlyVersion": "Nocturne",
@@ -1732,6 +1760,10 @@
"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",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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.",
@@ -3238,6 +3298,7 @@
"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,6 +3207,21 @@
}
}
},
"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",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +936,8 @@
"batchRename": "一括リネーム",
"beta": "ベータ版",
"bookmark": "ライブラリに保存",
"browserReservedKeybinding": "このショートカットは一部のブラウザで予約されており、予期しない動作を引き起こす可能性があります。",
"browserReservedKeybindingTooltip": "このショートカットはブラウザの予約済みショートカットと競合しています",
"calculatingDimensions": "寸法を計算中",
"cancel": "キャンセル",
"cancelled": "キャンセル済み",
@@ -970,6 +972,7 @@
"copy": "コピー",
"copyAll": "すべてコピー",
"copyJobId": "ジョブIDをコピー",
"copySystemInfo": "システム情報をコピー",
"copyToClipboard": "クリップボードにコピー",
"copyURL": "URLをコピー",
"core": "コア",
@@ -1016,6 +1019,7 @@
"enterNewName": "新しい名前を入力",
"enterNewNamePrompt": "新しい名前を入力してください:",
"enterSubgraph": "サブグラフに入る",
"enterYourKeybind": "キーバインドを入力してください",
"error": "エラー",
"errorLoadingImage": "画像の読み込みエラー",
"errorLoadingVideo": "ビデオの読み込みエラー",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "マイクの許可が拒否されました",
"migrate": "移行する",
"missing": "不足している",
"modifyKeybinding": "キーバインドを変更",
"more": "もっと見る",
"moreOptions": "その他のオプション",
"moreWorkflows": "さらに多くのワークフロー",
@@ -1093,6 +1098,7 @@
"name": "名前",
"newFolder": "新しいフォルダー",
"next": "次へ",
"nextImage": "次の画像",
"nightly": "NIGHTLY",
"no": "いいえ",
"noAudioRecorded": "音声が録音されていません",
@@ -1126,8 +1132,8 @@
"playRecording": "録音を再生",
"playbackSpeed": "再生速度",
"playing": "再生中",
"pressKeysForNewBinding": "新しいバインドのキーを押してください",
"preview": "プレビュー",
"previousImage": "前の画像",
"profile": "プロフィール",
"progressCountOf": "の",
"queued": "キュー中",
@@ -1167,6 +1173,7 @@
"resultsCount": "{count}件の結果が見つかりました",
"running": "実行中",
"save": "保存",
"saveAnyway": "強制的に保存",
"saving": "保存中",
"scrollLeft": "左にスクロール",
"scrollRight": "右にスクロール",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "複製する項目を選択",
"selectItemsToRename": "リネームする項目を選択",
"selectedFile": "選択されたファイル",
"setAKeybindingForTheFollowing": "次の操作のキーバインドを設定:",
"setAsBackground": "背景として設定",
"settings": "設定",
"shortcutSuffix": "{shortcut}",
@@ -1200,6 +1208,8 @@
"stopRecording": "録音停止",
"submit": "送信",
"success": "成功",
"switchToGridView": "グリッドビューに切り替え",
"switchToSingleView": "シングルビューに切り替え",
"systemInfo": "システム情報",
"terminal": "ターミナル",
"title": "タイトル",
@@ -1229,6 +1239,8 @@
"you": "あなた"
},
"graphCanvasMenu": {
"canvasMode": "キャンバスモード",
"canvasToolbar": "キャンバスツールバー",
"fitView": "ビューに合わせる",
"focusMode": "フォーカスモード",
"hand": "パンビュー",
@@ -1465,12 +1477,26 @@
"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": "クリックしてキューをクリア"
@@ -1478,6 +1504,7 @@
"rerun": "再実行",
"reuseParameters": "パラメータを再利用",
"runCount": "実行回数:",
"viewGraph": "ノードグラフを表示",
"viewJob": "ジョブを表示",
"welcome": {
"buildApp": "アプリを作成",
@@ -1686,6 +1713,7 @@
"installedSection": "インストール済み",
"missingNodes": "不足しているノード",
"notInstalled": "未インストール",
"unresolvedNodes": "未解決ノード",
"updatesAvailable": "アップデートあり"
},
"nightlyVersion": "ナイトリー",
@@ -1732,6 +1760,10 @@
"uninstall": "アンインストール",
"uninstallSelected": "選択したものをアンインストール",
"uninstalling": "アンインストール中",
"unresolvedNodes": {
"message": "以下のノードがインストールされておらず、レジストリで見つかりませんでした。",
"title": "未解決の欠落ノード"
},
"update": "更新",
"updateAll": "すべて更新",
"updateSelected": "選択したものを更新",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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でまだ利用できないカスタムードが必要です。",
@@ -3238,6 +3298,7 @@
"interrupted": "実行が中断されました",
"legacyMaskEditorDeprecated": "従来のマスクエディタは非推奨となり、まもなく削除されます。",
"migrateToLitegraphReroute": "将来のバージョンではRerouteードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。",
"missingModelVerificationFailed": "不足しているモデルの検証に失敗しました。一部のモデルはエラータブに表示されない場合があります。",
"modelLoadedSuccessfully": "3Dモデルが正常に読み込まれました",
"no3dScene": "テクスチャを適用する3Dシーンがありません",
"no3dSceneToExport": "エクスポートする3Dシーンがありません",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"FluxKVCache": {
"description": "Fluxファミリーモデルの参照画像に対してKVキャッシュ最適化を有効にします。",
"display_name": "Flux KVキャッシュ",
"inputs": {
"model": {
"name": "モデル",
"tooltip": "KVキャッシュを使用するモデル。"
}
},
"outputs": {
"0": {
"tooltip": "KVキャッシュが有効になったパッチ済みモデル。"
}
}
},
"FluxKontextImageScale": {
"description": "このードは、Flux Kontextに最適なサイズに画像をリサイズします。",
"display_name": "FluxKontextImageScale",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +936,8 @@
"batchRename": "일괄 이름 변경",
"beta": "베타",
"bookmark": "라이브러리에 저장",
"browserReservedKeybinding": "이 단축키는 일부 브라우저에서 예약되어 있어 예기치 않은 결과가 발생할 수 있습니다.",
"browserReservedKeybindingTooltip": "이 단축키는 브라우저 예약 단축키와 충돌합니다",
"calculatingDimensions": "크기 계산 중",
"cancel": "취소",
"cancelled": "취소됨",
@@ -970,6 +972,7 @@
"copy": "복사",
"copyAll": "모두 복사",
"copyJobId": "작업 ID 복사",
"copySystemInfo": "시스템 정보 복사",
"copyToClipboard": "클립보드에 복사",
"copyURL": "URL 복사",
"core": "코어",
@@ -1016,6 +1019,7 @@
"enterNewName": "새 이름 입력",
"enterNewNamePrompt": "새 이름을 입력하세요:",
"enterSubgraph": "서브그래프 진입",
"enterYourKeybind": "단축키를 입력하세요",
"error": "오류",
"errorLoadingImage": "이미지 로드 오류",
"errorLoadingVideo": "비디오 로드 오류",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "마이크 권한이 거부되었습니다",
"migrate": "이전(migrate)",
"missing": "누락됨",
"modifyKeybinding": "단축키 수정",
"more": "더보기",
"moreOptions": "추가 옵션",
"moreWorkflows": "더 많은 워크플로",
@@ -1093,6 +1098,7 @@
"name": "이름",
"newFolder": "새 폴더",
"next": "다음",
"nextImage": "다음 이미지",
"nightly": "NIGHTLY",
"no": "아니오",
"noAudioRecorded": "녹음된 오디오가 없습니다",
@@ -1126,8 +1132,8 @@
"playRecording": "녹음 재생",
"playbackSpeed": "재생 속도",
"playing": "재생 중",
"pressKeysForNewBinding": "새 바인딩을 위한 키 입력",
"preview": "미리보기",
"previousImage": "이전 이미지",
"profile": "프로필",
"progressCountOf": "중",
"queued": "대기 중",
@@ -1167,6 +1173,7 @@
"resultsCount": "{count} 개의 결과를 찾았습니다",
"running": "실행 중",
"save": "저장",
"saveAnyway": "그래도 저장",
"saving": "저장 중",
"scrollLeft": "왼쪽으로 스크롤",
"scrollRight": "오른쪽으로 스크롤",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "복제할 항목 선택",
"selectItemsToRename": "이름을 변경할 항목 선택",
"selectedFile": "선택된 파일",
"setAKeybindingForTheFollowing": "다음에 대한 단축키를 설정하세요:",
"setAsBackground": "배경으로 설정",
"settings": "설정",
"shortcutSuffix": " ({shortcut})",
@@ -1200,6 +1208,8 @@
"stopRecording": "녹음 중지",
"submit": "제출",
"success": "성공",
"switchToGridView": "그리드 보기로 전환",
"switchToSingleView": "단일 보기로 전환",
"systemInfo": "시스템 정보",
"terminal": "터미널",
"title": "제목",
@@ -1229,6 +1239,8 @@
"you": "당신"
},
"graphCanvasMenu": {
"canvasMode": "캔버스 모드",
"canvasToolbar": "캔버스 툴바",
"fitView": "보기 맞춤",
"focusMode": "집중 모드",
"hand": "손",
@@ -1465,12 +1477,26 @@
"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": "클릭하여 대기열 비우기"
@@ -1478,6 +1504,7 @@
"rerun": "다시 실행",
"reuseParameters": "파라미터 재사용",
"runCount": "실행 횟수:",
"viewGraph": "노드 그래프 보기",
"viewJob": "작업 보기",
"welcome": {
"buildApp": "앱 만들기",
@@ -1686,6 +1713,7 @@
"installedSection": "설치됨",
"missingNodes": "누락된 노드",
"notInstalled": "미설치",
"unresolvedNodes": "해결되지 않은 노드",
"updatesAvailable": "업데이트 가능"
},
"nightlyVersion": "최신 테스트 버전(nightly)",
@@ -1732,6 +1760,10 @@
"uninstall": "제거",
"uninstallSelected": "선택 항목 제거",
"uninstalling": "제거 중",
"unresolvedNodes": {
"message": "다음 노드가 설치되어 있지 않으며 레지스트리에서 찾을 수 없습니다.",
"title": "해결되지 않은 누락된 노드"
},
"update": "업데이트",
"updateAll": "모두 업데이트",
"updateSelected": "선택 항목 업데이트",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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에서 아직 사용할 수 없는 커스텀 노드가 필요합니다.",
@@ -3238,6 +3298,7 @@
"interrupted": "실행이 중단되었습니다",
"legacyMaskEditorDeprecated": "레거시 마스크 에디터는 더 이상 지원되지 않으며 곧 제거될 예정입니다.",
"migrateToLitegraphReroute": "향후 버전에서는 Reroute 노드가 제거됩니다. LiteGraph 에서 자체 제공하는 경유점으로 변환하려면 클릭하세요.",
"missingModelVerificationFailed": "누락된 모델 검증에 실패했습니다. 일부 모델이 오류 탭에 표시되지 않을 수 있습니다.",
"modelLoadedSuccessfully": "3D 모델이 성공적으로 로드됨",
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"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 이미지 스케일",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +936,8 @@
"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",
@@ -970,6 +972,7 @@
"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",
@@ -1016,6 +1019,7 @@
"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",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "Permissão do microfone negada",
"migrate": "Migrar",
"missing": "Faltando",
"modifyKeybinding": "Modificar atalho",
"more": "Mais",
"moreOptions": "Mais opções",
"moreWorkflows": "Mais fluxos de trabalho",
@@ -1093,6 +1098,7 @@
"name": "Nome",
"newFolder": "Nova pasta",
"next": "Próximo",
"nextImage": "Próxima imagem",
"nightly": "NIGHTLY",
"no": "Não",
"noAudioRecorded": "Nenhum áudio gravado",
@@ -1126,8 +1132,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",
@@ -1167,6 +1173,7 @@
"resultsCount": "{count} resultados encontrados",
"running": "Executando",
"save": "Salvar",
"saveAnyway": "Salvar mesmo assim",
"saving": "Salvando",
"scrollLeft": "Rolar para a esquerda",
"scrollRight": "Rolar para a direita",
@@ -1185,6 +1192,7 @@
"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})",
@@ -1200,6 +1208,8 @@
"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",
@@ -1229,6 +1239,8 @@
"you": "Você"
},
"graphCanvasMenu": {
"canvasMode": "Modo de tela",
"canvasToolbar": "Barra de ferramentas da tela",
"fitView": "Ajustar à Tela",
"focusMode": "Modo de Foco",
"hand": "Mão",
@@ -1465,12 +1477,26 @@
"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"
@@ -1478,6 +1504,7 @@
"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",
@@ -1686,6 +1713,7 @@
"installedSection": "INSTALADO",
"missingNodes": "Nós Ausentes",
"notInstalled": "Não Instalado",
"unresolvedNodes": "Nós não resolvidos",
"updatesAvailable": "Atualizações Disponíveis"
},
"nightlyVersion": "Noturna",
@@ -1732,6 +1760,10 @@
"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",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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.",
@@ -3250,6 +3310,7 @@
"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,6 +3207,21 @@
}
}
},
"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",
@@ -12855,6 +12870,133 @@
}
}
},
"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",

View File

@@ -936,6 +936,8 @@
"batchRename": "Пакетное переименование",
"beta": "БЕТА",
"bookmark": "Сохранить в библиотеку",
"browserReservedKeybinding": "Это сочетание клавиш зарезервировано некоторыми браузерами и может привести к неожиданным результатам.",
"browserReservedKeybindingTooltip": "Это сочетание конфликтует с зарезервированными сочетаниями браузера",
"calculatingDimensions": "Расчёт размеров",
"cancel": "Отмена",
"cancelled": "Отменено",
@@ -970,6 +972,7 @@
"copy": "Копировать",
"copyAll": "Скопировать всё",
"copyJobId": "Копировать ID задания",
"copySystemInfo": "Скопировать информацию о системе",
"copyToClipboard": "Скопировать в буфер обмена",
"copyURL": "Скопировать URL",
"core": "Ядро",
@@ -1016,6 +1019,7 @@
"enterNewName": "Введите новое имя",
"enterNewNamePrompt": "Введите новое имя:",
"enterSubgraph": "Войти в подграф",
"enterYourKeybind": "Введите ваше сочетание клавиш",
"error": "Ошибка",
"errorLoadingImage": "Ошибка загрузки изображения",
"errorLoadingVideo": "Ошибка загрузки видео",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "Доступ к микрофону запрещён",
"migrate": "Мигрировать",
"missing": "Отсутствует",
"modifyKeybinding": "Изменить сочетание клавиш",
"more": "Больше",
"moreOptions": "Больше опций",
"moreWorkflows": "Больше рабочих процессов",
@@ -1093,6 +1098,7 @@
"name": "Имя",
"newFolder": "Новая папка",
"next": "Далее",
"nextImage": "Следующее изображение",
"nightly": "NIGHTLY",
"no": "Нет",
"noAudioRecorded": "Аудио не записано",
@@ -1126,8 +1132,8 @@
"playRecording": "Воспроизвести запись",
"playbackSpeed": "Скорость воспроизведения",
"playing": "Воспроизводится",
"pressKeysForNewBinding": "Нажмите клавиши для новой привязки",
"preview": "ПРЕДПРОСМОТР",
"previousImage": "Предыдущее изображение",
"profile": "Профиль",
"progressCountOf": "из",
"queued": "В очереди",
@@ -1167,6 +1173,7 @@
"resultsCount": "Найдено {count} результатов",
"running": "Выполняется",
"save": "Сохранить",
"saveAnyway": "Сохранить в любом случае",
"saving": "Сохранение",
"scrollLeft": "Прокрутить влево",
"scrollRight": "Прокрутить вправо",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "Выберите элементы для дублирования",
"selectItemsToRename": "Выберите элементы для переименования",
"selectedFile": "Выбранный файл",
"setAKeybindingForTheFollowing": "Установите сочетание клавиш для следующего:",
"setAsBackground": "Установить как фон",
"settings": "Настройки",
"shortcutSuffix": " ({shortcut})",
@@ -1200,6 +1208,8 @@
"stopRecording": "Остановить запись",
"submit": "Отправить",
"success": "Успех",
"switchToGridView": "Переключиться на вид сетки",
"switchToSingleView": "Переключиться на одиночный вид",
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"title": "Заголовок",
@@ -1229,6 +1239,8 @@
"you": "Вы"
},
"graphCanvasMenu": {
"canvasMode": "Режим холста",
"canvasToolbar": "Панель инструментов холста",
"fitView": "Подгонять под выделенные",
"focusMode": "Режим фокуса",
"hand": "Рука",
@@ -1465,12 +1477,26 @@
"dragAndDropImage": "Перетащите изображение",
"emptyWorkflowExplanation": "Ваш рабочий процесс пуст. Сначала добавьте несколько узлов, чтобы начать создавать приложение.",
"enterNodeGraph": "Войти в граф узлов",
"error": {
"getHelp": "Для получения помощи ознакомьтесь с нашим {0}, {1} или {2} с копией ошибки.",
"github": "отправить issue на GitHub",
"goto": "Показать ошибки в графе",
"guide": "руководство по устранению неполадок",
"header": "В приложении произошла ошибка",
"log": "Журналы ошибок",
"mobileFixable": "Проверьте {0} на наличие ошибок",
"promptShow": "Показать отчёт об ошибке",
"promptVisitGraph": "Просмотрите граф узлов, чтобы увидеть полную ошибку.",
"requiresGraph": "Что-то пошло не так во время генерации. Возможные причины: неверные скрытые входные данные, отсутствующие ресурсы или ошибки конфигурации рабочего процесса.",
"support": "связаться с поддержкой"
},
"giveFeedback": "Оставить отзыв",
"graphMode": "Графовый режим",
"hasCreditCost": "Требуются дополнительные кредиты",
"linearMode": "Режим приложения",
"loadTemplate": "Загрузить шаблон",
"mobileControls": "Редактировать и запустить",
"mobileNoWorkflow": "Для этого режима приложения рабочий процесс не создан. Попробуйте другой.",
"queue": {
"clear": "Очистить очередь",
"clickToClear": "Нажмите, чтобы очистить очередь"
@@ -1478,6 +1504,7 @@
"rerun": "Перезапустить",
"reuseParameters": "Повторно использовать параметры",
"runCount": "Количество запусков:",
"viewGraph": "Просмотреть граф узлов",
"viewJob": "Просмотреть задачу",
"welcome": {
"buildApp": "Создать приложение",
@@ -1686,6 +1713,7 @@
"installedSection": "УСТАНОВЛЕНО",
"missingNodes": "Отсутствующие узлы",
"notInstalled": "Не установлено",
"unresolvedNodes": "Неразрешённые узлы",
"updatesAvailable": "Доступны обновления"
},
"nightlyVersion": "Ночная",
@@ -1732,6 +1760,10 @@
"uninstall": "Удалить",
"uninstallSelected": "Удалить выбранное",
"uninstalling": "Удаление",
"unresolvedNodes": {
"message": "Следующие узлы не установлены и не найдены в реестре.",
"title": "Неразрешённые отсутствующие узлы"
},
"update": "Обновить",
"updateAll": "Обновить всё",
"updateSelected": "Обновить выбранное",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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": "Поддерживаются только URL Civitai и Hugging Face.",
"urlPlaceholder": "Вставьте URL модели (Civitai или Hugging Face)",
"useFromLibrary": "Использовать из библиотеки",
"usingFromLibrary": "Используется из библиотеки"
},
"missingNodePacks": {
"applyChanges": "Применить изменения",
"cloudMessage": "Для этого рабочего процесса требуются пользовательские узлы, которые ещё недоступны в Comfy Cloud.",
@@ -3238,6 +3298,7 @@
"interrupted": "Выполнение было прервано",
"legacyMaskEditorDeprecated": "Устаревший редактор масок будет скоро удалён.",
"migrateToLitegraphReroute": "Узлы перенаправления будут удалены в будущих версиях. Нажмите, чтобы перейти на litegraph-native reroute.",
"missingModelVerificationFailed": "Не удалось проверить отсутствующие модели. Некоторые модели могут не отображаться на вкладке Ошибки.",
"modelLoadedSuccessfully": "3D-модель успешно загружена",
"no3dScene": "Нет 3D сцены для применения текстуры",
"no3dSceneToExport": "Нет 3D сцены для экспорта",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"FluxKVCache": {
"description": "Включает оптимизацию KV Cache для эталонных изображений в моделях семейства Flux.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "model",
"tooltip": "Модель, для которой будет использоваться KV Cache."
}
},
"outputs": {
"0": {
"tooltip": "Патченная модель с включённым KV Cache."
}
}
},
"FluxKontextImageScale": {
"description": "Этот узел изменяет размер изображения до более оптимального для flux kontext.",
"display_name": "FluxKontextImageScale",
@@ -12855,6 +12870,133 @@
}
}
},
"ReveImageCreateNode": {
"description": "Генерируйте изображения по текстовым описаниям с помощью Reve.",
"display_name": "Reve Создание изображения",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"model": {
"name": "model",
"tooltip": "Версия модели для генерации."
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"prompt": {
"name": "prompt",
"tooltip": "Текстовое описание желаемого изображения. Максимум 2560 символов."
},
"remove_background": {
"name": "remove_background",
"tooltip": "Удалить фон с сгенерированного изображения. Может потребоваться дополнительная оплата."
},
"seed": {
"name": "seed",
"tooltip": "Seed определяет, должен ли узел запускаться повторно; результаты не являются детерминированными независимо от seed."
},
"upscale": {
"name": "upscale",
"tooltip": "Увеличить разрешение сгенерированного изображения. Может потребоваться дополнительная оплата."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Редактируйте изображения с помощью естественно-языковых инструкций в Reve.",
"display_name": "Reve Редактирование изображения",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"edit_instruction": {
"name": "edit_instruction",
"tooltip": "Текстовое описание того, как редактировать изображение. Максимум 2560 символов."
},
"image": {
"name": "image",
"tooltip": "Изображение для редактирования."
},
"model": {
"name": "model",
"tooltip": "Версия модели для редактирования."
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"remove_background": {
"name": "remove_background",
"tooltip": "Удалить фон с сгенерированного изображения. Может потребоваться дополнительная оплата."
},
"seed": {
"name": "seed",
"tooltip": "Seed определяет, должен ли узел запускаться повторно; результаты не являются детерминированными независимо от seed."
},
"upscale": {
"name": "upscale",
"tooltip": "Увеличить разрешение сгенерированного изображения. Может потребоваться дополнительная оплата."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "Комбинируйте референсные изображения с текстовыми подсказками для создания новых изображений с помощью Reve.",
"display_name": "Reve Ремикс изображения",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"model": {
"name": "model",
"tooltip": "Версия модели для ремикса."
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"prompt": {
"name": "prompt",
"tooltip": "Текстовое описание желаемого изображения. Можно использовать XML-теги img для ссылки на определённые изображения по индексу, например <img>0</img>, <img>1</img> и т.д."
},
"reference_images": {
"name": "reference_images"
},
"remove_background": {
"name": "remove_background",
"tooltip": "Удалить фон с сгенерированного изображения. Может потребоваться дополнительная оплата."
},
"seed": {
"name": "seed",
"tooltip": "Seed определяет, должен ли узел запускаться повторно; результаты не являются детерминированными независимо от seed."
},
"upscale": {
"name": "upscale",
"tooltip": "Увеличить разрешение сгенерированного изображения. Может потребоваться дополнительная оплата."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Создание 3D-объектов с помощью Rodin API",
"display_name": "Rodin 3D Generate - Детальная генерация",

View File

@@ -936,6 +936,8 @@
"batchRename": "Toplu Yeniden Adlandır",
"beta": "BETA",
"bookmark": "Kütüphaneye Kaydet",
"browserReservedKeybinding": "Bu kısayol bazı tarayıcılar tarafından ayrılmıştır ve beklenmeyen sonuçlara yol açabilir.",
"browserReservedKeybindingTooltip": "Bu kısayol, tarayıcıya ayrılmış kısayollarla çakışıyor",
"calculatingDimensions": "Boyutlar hesaplanıyor",
"cancel": "İptal",
"cancelled": "İptal Edildi",
@@ -970,6 +972,7 @@
"copy": "Kopyala",
"copyAll": "Tümünü Kopyala",
"copyJobId": "İş Kimliğini Kopyala",
"copySystemInfo": "Sistem Bilgilerini Kopyala",
"copyToClipboard": "Panoya Kopyala",
"copyURL": "URL'yi Kopyala",
"core": "Çekirdek",
@@ -1016,6 +1019,7 @@
"enterNewName": "Yeni adı girin",
"enterNewNamePrompt": "Yeni adı girin:",
"enterSubgraph": "Alt Grafiğe Gir",
"enterYourKeybind": "Kısayol tuşunuzu girin",
"error": "Hata",
"errorLoadingImage": "Görüntü yüklenirken hata",
"errorLoadingVideo": "Video yüklenirken hata",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "Mikrofon izni reddedildi",
"migrate": "Taşı",
"missing": "Eksik",
"modifyKeybinding": "Kısayol tuşunu değiştir",
"more": "Daha Fazla",
"moreOptions": "Daha Fazla Seçenek",
"moreWorkflows": "Daha fazla iş akışı",
@@ -1093,6 +1098,7 @@
"name": "Ad",
"newFolder": "Yeni Klasör",
"next": "İleri",
"nextImage": "Sonraki görsel",
"nightly": "NIGHTLY",
"no": "Hayır",
"noAudioRecorded": "Ses kaydedilmedi",
@@ -1126,8 +1132,8 @@
"playRecording": "Kaydı Oynat",
"playbackSpeed": "Oynatma Hızı",
"playing": "Oynatılıyor",
"pressKeysForNewBinding": "Yeni bağlama için tuşlara basın",
"preview": "ÖNİZLEME",
"previousImage": "Önceki görsel",
"profile": "Profil",
"progressCountOf": "/",
"queued": "Kuyrukta",
@@ -1167,6 +1173,7 @@
"resultsCount": "{count} Sonuç Bulundu",
"running": "Çalışıyor",
"save": "Kaydet",
"saveAnyway": "Yine de Kaydet",
"saving": "Kaydediliyor",
"scrollLeft": "Sola Kaydır",
"scrollRight": "Sağa Kaydır",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "Çoğaltılacak öğeleri seçin",
"selectItemsToRename": "Yeniden adlandırılacak öğeleri seçin",
"selectedFile": "Seçilen dosya",
"setAKeybindingForTheFollowing": "Aşağıdakiler için bir kısayol tuşu ayarla:",
"setAsBackground": "Arka Plan Olarak Ayarla",
"settings": "Ayarlar",
"shortcutSuffix": " ({shortcut})",
@@ -1200,6 +1208,8 @@
"stopRecording": "Kaydı Durdur",
"submit": "Gönder",
"success": "Başarılı",
"switchToGridView": "Izgara görünümüne geç",
"switchToSingleView": "Tekli görünüme geç",
"systemInfo": "Sistem Bilgisi",
"terminal": "Terminal",
"title": "Başlık",
@@ -1229,6 +1239,8 @@
"you": "Sen"
},
"graphCanvasMenu": {
"canvasMode": "Tuval Modu",
"canvasToolbar": "Tuval Araç Çubuğu",
"fitView": "Görünüme Sığdır",
"focusMode": "Odak Modu",
"hand": "El",
@@ -1465,12 +1477,26 @@
"dragAndDropImage": "Bir görseli sürükleyip bırakın",
"emptyWorkflowExplanation": "Çalışma akışınız boş. Bir uygulama oluşturmaya başlamak için önce bazı düğümler eklemelisiniz.",
"enterNodeGraph": "Düğüm grafiğine gir",
"error": {
"getHelp": "Yardım için, kopyalanan hatayla birlikte {0}, {1} veya {2} adresini ziyaret edin.",
"github": "GitHub sorunu gönder",
"goto": "Grafikte hataları göster",
"guide": "sorun giderme rehberi",
"header": "Uygulamada bir hata oluştu",
"log": "Hata Kayıtları",
"mobileFixable": "Hatalar için {0} kontrol edin",
"promptShow": "Hata raporunu göster",
"promptVisitGraph": "Tüm hatayı görmek için düğüm grafiğini görüntüleyin.",
"requiresGraph": "Oluşturma sırasında bir şeyler ters gitti. Bu, geçersiz gizli girdiler, eksik kaynaklar veya iş akışı yapılandırma sorunlarından kaynaklanıyor olabilir.",
"support": "destek ekibimizle iletişime geçin"
},
"giveFeedback": "Geri bildirim ver",
"graphMode": "Grafik Modu",
"hasCreditCost": "Ekstra kredi gerektirir",
"linearMode": "Uygulama Modu",
"loadTemplate": "Şablon yükle",
"mobileControls": "Düzenle ve Çalıştır",
"mobileNoWorkflow": "Bu iş akışı uygulama modu için oluşturulmamış. Farklı bir tane deneyin.",
"queue": {
"clear": "Kuyruğu temizle",
"clickToClear": "Kuyruğu temizlemek için tıklayın"
@@ -1478,6 +1504,7 @@
"rerun": "Tekrar Çalıştır",
"reuseParameters": "Parametreleri Yeniden Kullan",
"runCount": "Çalıştırma sayısı:",
"viewGraph": "Düğüm grafiğini görüntüle",
"viewJob": "İşi Görüntüle",
"welcome": {
"buildApp": "Uygulama oluştur",
@@ -1686,6 +1713,7 @@
"installedSection": "YÜKLÜ",
"missingNodes": "Eksik Düğümler",
"notInstalled": "Yüklü Değil",
"unresolvedNodes": "Çözülemeyen Düğümler",
"updatesAvailable": "Güncellemeler Mevcut"
},
"nightlyVersion": "Gecelik",
@@ -1732,6 +1760,10 @@
"uninstall": "Kaldır",
"uninstallSelected": "Seçilenleri Kaldır",
"uninstalling": "{id} kaldırılıyor",
"unresolvedNodes": {
"message": "Aşağıdaki düğümler yüklü değil ve kayıt defterinde bulunamadı.",
"title": "Çözülemeyen Eksik Düğümler"
},
"update": "Güncelle",
"updateAll": "Tümünü Güncelle",
"updateSelected": "Seçilenleri Güncelle",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"inputsNone": "GİRİŞ YOK",
"inputsNoneTooltip": "Düğümün girişi yok",
"locateNode": "Düğümü tuvalde bul",
"missingModels": {
"alreadyExistsInCategory": "Bu model zaten \"{category}\" içinde mevcut",
"assetLoadTimeout": "Model algılama zaman aşımına uğradı. Lütfen iş akışını yeniden yüklemeyi deneyin.",
"cancelSelection": "Seçimi iptal et",
"clearUrl": "URL'yi temizle",
"collapseNodes": "Referans veren node'ları gizle",
"confirmSelection": "Seçimi onayla",
"copyModelName": "Model adını kopyala",
"customNodeDownloadDisabled": "Bulut ortamı, bu bölümde özel node'lar için model içe aktarmayı desteklemiyor. Lütfen standart yükleyici node'larını kullanın veya aşağıdaki kütüphaneden bir model ile değiştirin.",
"expandNodes": "Referans veren node'ları göster",
"import": "İçe aktar",
"importAnyway": "Yine de içe aktar",
"importFailed": "İçe aktarılamadı",
"importNotSupported": "İçe Aktarma Desteklenmiyor",
"imported": "İçe aktarıldı",
"importing": "İçe aktarılıyor...",
"locateNode": "Node'u tuvalde bul",
"metadataFetchFailed": "Meta veriler alınamadı. Lütfen bağlantıyı kontrol edip tekrar deneyin.",
"missingModelsTitle": "Eksik Modeller",
"or": "VEYA",
"typeMismatch": "Bu model \"{detectedType}\" gibi görünüyor. Emin misiniz?",
"unknownCategory": "Bilinmeyen",
"unsupportedUrl": "Sadece Civitai ve Hugging Face URL'leri desteklenmektedir.",
"urlPlaceholder": "Model URL'sini yapıştırın (Civitai veya Hugging Face)",
"useFromLibrary": "Kütüphaneden kullan",
"usingFromLibrary": "Kütüphaneden kullanılıyor"
},
"missingNodePacks": {
"applyChanges": "Değişiklikleri Uygula",
"cloudMessage": "Bu iş akışı, Comfy Cloud'da henüz mevcut olmayan özel düğümler gerektiriyor.",
@@ -3238,6 +3298,7 @@
"interrupted": "Yürütme kesintiye uğradı",
"legacyMaskEditorDeprecated": "Klasik maske düzenleyici kullanımdan kaldırıldı ve yakında kaldırılacak.",
"migrateToLitegraphReroute": "Yeniden yönlendirme düğümleri gelecekteki sürümlerde kaldırılacaktır. Litegraph yerel yeniden yönlendirmeye geçmek için tıklayın.",
"missingModelVerificationFailed": "Eksik modeller doğrulanamadı. Bazı modeller Hatalar sekmesinde gösterilmeyebilir.",
"modelLoadedSuccessfully": "3B model başarıyla yüklendi",
"no3dScene": "Doku uygulanacak 3D sahne yok",
"no3dSceneToExport": "Dışa aktarılacak 3D sahne yok",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"FluxKVCache": {
"description": "Flux ailesi modellerinde referans görselleri için KV Cache optimizasyonunu etkinleştirir.",
"display_name": "Flux KV Cache",
"inputs": {
"model": {
"name": "model",
"tooltip": "KV Cache uygulanacak model."
}
},
"outputs": {
"0": {
"tooltip": "KV Cache etkinleştirilmiş yamalı model."
}
}
},
"FluxKontextImageScale": {
"description": "Bu düğüm, görüntüyü flux kontext için daha uygun bir boyuta yeniden boyutlandırır.",
"display_name": "FluxKontext Görüntü Ölçeği",
@@ -12855,6 +12870,133 @@
}
}
},
"ReveImageCreateNode": {
"description": "Reve kullanarak metin açıklamalarından görseller oluşturun.",
"display_name": "Reve Görsel Oluştur",
"inputs": {
"control_after_generate": {
"name": "oluşturduktan sonra kontrol et"
},
"model": {
"name": "model",
"tooltip": "Oluşturma için kullanılacak model sürümü."
},
"model_aspect_ratio": {
"name": "en_boy_oranı"
},
"model_test_time_scaling": {
"name": "test_zamanı_ölçekleme"
},
"prompt": {
"name": "prompt",
"tooltip": "İstenen görselin metin açıklaması. En fazla 2560 karakter."
},
"remove_background": {
"name": "remove_background",
"tooltip": "Oluşturulan görselin arka planını kaldır. Ek maliyet oluşturabilir."
},
"seed": {
"name": "seed",
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
},
"upscale": {
"name": "upscale",
"tooltip": "Oluşturulan görseli büyüt. Ek maliyet oluşturabilir."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "Reve ile doğal dil talimatları kullanarak görselleri düzenleyin.",
"display_name": "Reve Görsel Düzenle",
"inputs": {
"control_after_generate": {
"name": "oluşturduktan sonra kontrol et"
},
"edit_instruction": {
"name": "düzenleme_talimatı",
"tooltip": "Görselin nasıl düzenleneceğine dair metin açıklaması. En fazla 2560 karakter."
},
"image": {
"name": "görsel",
"tooltip": "Düzenlenecek görsel."
},
"model": {
"name": "model",
"tooltip": "Düzenleme için kullanılacak model sürümü."
},
"model_aspect_ratio": {
"name": "en_boy_oranı"
},
"model_test_time_scaling": {
"name": "test_zamanı_ölçekleme"
},
"remove_background": {
"name": "remove_background",
"tooltip": "Oluşturulan görselin arka planını kaldır. Ek maliyet oluşturabilir."
},
"seed": {
"name": "seed",
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
},
"upscale": {
"name": "upscale",
"tooltip": "Oluşturulan görseli büyüt. Ek maliyet oluşturabilir."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "Referans görselleri ve metin istemlerini birleştirerek Reve ile yeni görseller oluşturun.",
"display_name": "Reve Görsel Remix",
"inputs": {
"control_after_generate": {
"name": "oluşturduktan sonra kontrol et"
},
"model": {
"name": "model",
"tooltip": "Remix için kullanılacak model sürümü."
},
"model_aspect_ratio": {
"name": "en_boy_oranı"
},
"model_test_time_scaling": {
"name": "test_zamanı_ölçekleme"
},
"prompt": {
"name": "prompt",
"tooltip": "İstenen görselin metin açıklaması. Belirli görselleri indeks ile referans göstermek için XML img etiketleri ekleyebilirsiniz, örn. <img>0</img>, <img>1</img> vb."
},
"reference_images": {
"name": "referans_görseller"
},
"remove_background": {
"name": "remove_background",
"tooltip": "Oluşturulan görselin arka planını kaldır. Ek maliyet oluşturabilir."
},
"seed": {
"name": "seed",
"tooltip": "Seed, düğümün tekrar çalıştırılıp çalıştırılmayacağını kontrol eder; seed ne olursa olsun sonuçlar deterministik değildir."
},
"upscale": {
"name": "upscale",
"tooltip": "Oluşturulan görseli büyüt. Ek maliyet oluşturabilir."
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "Rodin API kullanarak 3D Varlıklar Oluştur",
"display_name": "Rodin 3D Oluştur - Detay Oluştur",

View File

@@ -936,6 +936,8 @@
"batchRename": "批次重新命名",
"beta": "測試版",
"bookmark": "儲存至程式庫",
"browserReservedKeybinding": "此快捷鍵已被部分瀏覽器保留,可能會產生非預期結果。",
"browserReservedKeybindingTooltip": "此快捷鍵與瀏覽器保留的快捷鍵衝突",
"calculatingDimensions": "計算尺寸中",
"cancel": "取消",
"cancelled": "已取消",
@@ -970,6 +972,7 @@
"copy": "複製",
"copyAll": "全部複製",
"copyJobId": "複製工作 ID",
"copySystemInfo": "複製系統資訊",
"copyToClipboard": "複製到剪貼簿",
"copyURL": "複製網址",
"core": "核心",
@@ -1016,6 +1019,7 @@
"enterNewName": "輸入新名稱",
"enterNewNamePrompt": "輸入新名稱:",
"enterSubgraph": "進入子圖",
"enterYourKeybind": "輸入您的快捷鍵",
"error": "錯誤",
"errorLoadingImage": "載入圖片時發生錯誤",
"errorLoadingVideo": "載入影片時發生錯誤",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "麥克風權限被拒絕",
"migrate": "遷移",
"missing": "缺少",
"modifyKeybinding": "修改快捷鍵",
"more": "更多",
"moreOptions": "更多選項",
"moreWorkflows": "更多工作流程",
@@ -1093,6 +1098,7 @@
"name": "名稱",
"newFolder": "新資料夾",
"next": "下一步",
"nextImage": "下一張圖片",
"nightly": "NIGHTLY",
"no": "否",
"noAudioRecorded": "沒有錄製到音訊",
@@ -1126,8 +1132,8 @@
"playRecording": "播放錄製",
"playbackSpeed": "播放速度",
"playing": "播放中",
"pressKeysForNewBinding": "按下按鍵設定新綁定",
"preview": "預覽",
"previousImage": "上一張圖片",
"profile": "個人檔案",
"progressCountOf": "共",
"queued": "已排隊",
@@ -1167,6 +1173,7 @@
"resultsCount": "找到 {count} 筆結果",
"running": "執行中",
"save": "儲存",
"saveAnyway": "仍要儲存",
"saving": "儲存中",
"scrollLeft": "向左捲動",
"scrollRight": "向右捲動",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "請選擇要複製的項目",
"selectItemsToRename": "請選擇要重新命名的項目",
"selectedFile": "已選取的檔案",
"setAKeybindingForTheFollowing": "為以下項目設定快捷鍵:",
"setAsBackground": "設為背景",
"settings": "設定",
"shortcutSuffix": "{shortcut}",
@@ -1200,6 +1208,8 @@
"stopRecording": "停止錄音",
"submit": "提交",
"success": "成功",
"switchToGridView": "切換至網格檢視",
"switchToSingleView": "切換至單一檢視",
"systemInfo": "系統資訊",
"terminal": "終端機",
"title": "標題",
@@ -1229,6 +1239,8 @@
"you": "你"
},
"graphCanvasMenu": {
"canvasMode": "畫布模式",
"canvasToolbar": "畫布工具列",
"fitView": "適合視窗",
"focusMode": "專注模式",
"hand": "手形",
@@ -1465,12 +1477,26 @@
"dragAndDropImage": "拖曳圖片到此",
"emptyWorkflowExplanation": "您的工作流程目前是空的。您需要先新增一些節點,才能開始建立應用程式。",
"enterNodeGraph": "進入節點圖",
"error": {
"getHelp": "如需協助,請參閱我們的 {0}、{1} 或 {2} 並附上複製的錯誤資訊。",
"github": "提交 GitHub 問題",
"goto": "在圖中顯示錯誤",
"guide": "疑難排解指南",
"header": "此應用程式發生錯誤",
"log": "錯誤日誌",
"mobileFixable": "請檢查 {0} 以獲取錯誤資訊",
"promptShow": "顯示錯誤報告",
"promptVisitGraph": "檢視節點圖以查看完整錯誤。",
"requiresGraph": "產生過程中發生錯誤。可能原因包括無效的隱藏輸入、缺少資源或工作流程設定問題。",
"support": "聯絡我們的支援"
},
"giveFeedback": "提供回饋",
"graphMode": "圖形模式",
"hasCreditCost": "需要額外點數",
"linearMode": "App 模式",
"loadTemplate": "載入範本",
"mobileControls": "編輯與執行",
"mobileNoWorkflow": "此工作流程尚未為應用程式模式建立。請嘗試其他工作流程。",
"queue": {
"clear": "清除佇列",
"clickToClear": "點擊以清除佇列"
@@ -1478,6 +1504,7 @@
"rerun": "重新執行",
"reuseParameters": "重用參數",
"runCount": "執行次數:",
"viewGraph": "檢視節點圖",
"viewJob": "檢視任務",
"welcome": {
"buildApp": "建立應用",
@@ -1686,6 +1713,7 @@
"installedSection": "已安裝",
"missingNodes": "缺少節點",
"notInstalled": "未安裝",
"unresolvedNodes": "未解決的節點",
"updatesAvailable": "有可用更新"
},
"nightlyVersion": "每夜建置版",
@@ -1732,6 +1760,10 @@
"uninstall": "解除安裝",
"uninstallSelected": "解除安裝所選項目",
"uninstalling": "正在解除安裝",
"unresolvedNodes": {
"message": "以下節點尚未安裝,且在註冊表中找不到。",
"title": "未解決的缺失節點"
},
"update": "更新",
"updateAll": "全部更新",
"updateSelected": "更新所選項目",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "羅丹",
"Runway": "跑道",
"Sora": "蒼穹",
@@ -2379,6 +2412,33 @@
"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 尚未提供的自訂節點。",
@@ -3238,6 +3298,7 @@
"interrupted": "執行已被中斷",
"legacyMaskEditorDeprecated": "舊版遮罩編輯器即將淘汰並很快移除。",
"migrateToLitegraphReroute": "重導節點將於未來版本移除。點擊以遷移至 litegraph 原生重導。",
"missingModelVerificationFailed": "驗證缺少的模型失敗。部分模型可能不會顯示在錯誤標籤頁中。",
"modelLoadedSuccessfully": "3D 模型載入成功",
"no3dScene": "沒有 3D 場景可套用材質",
"no3dSceneToExport": "沒有 3D 場景可匯出",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"FluxKVCache": {
"description": "為 Flux 系列模型的參考圖像啟用 KV 快取最佳化。",
"display_name": "Flux KV 快取",
"inputs": {
"model": {
"name": "模型",
"tooltip": "要啟用 KV 快取的模型。"
}
},
"outputs": {
"0": {
"tooltip": "已啟用 KV 快取的修補模型。"
}
}
},
"FluxKontextImageScale": {
"description": "此節點將圖像調整為更適合flux kontext的尺寸。",
"display_name": "FluxKontextImageScale",
@@ -12855,6 +12870,133 @@
}
}
},
"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,6 +936,8 @@
"batchRename": "批量重命名",
"beta": "测试版",
"bookmark": "保存到库",
"browserReservedKeybinding": "此快捷键被部分浏览器保留,可能导致意外结果。",
"browserReservedKeybindingTooltip": "此快捷键与浏览器保留的快捷键冲突",
"calculatingDimensions": "正在计算尺寸",
"cancel": "取消",
"cancelled": "已取消",
@@ -970,6 +972,7 @@
"copy": "复制",
"copyAll": "全部复制",
"copyJobId": "复制队列 ID",
"copySystemInfo": "复制系统信息",
"copyToClipboard": "复制到剪贴板",
"copyURL": "复制链接",
"core": "核心",
@@ -1016,6 +1019,7 @@
"enterNewName": "输入新名称",
"enterNewNamePrompt": "输入新名称:",
"enterSubgraph": "进入子图",
"enterYourKeybind": "输入你的快捷键",
"error": "错误",
"errorLoadingImage": "图片加载出错",
"errorLoadingVideo": "视频加载出错",
@@ -1085,6 +1089,7 @@
"micPermissionDenied": "麦克风权限被拒绝",
"migrate": "迁移",
"missing": "缺失",
"modifyKeybinding": "修改快捷键",
"more": "更多",
"moreOptions": "更多选项",
"moreWorkflows": "更多工作流",
@@ -1093,6 +1098,7 @@
"name": "名称",
"newFolder": "新文件夹",
"next": "下一个",
"nextImage": "下一张图像",
"nightly": "NIGHTLY",
"no": "否",
"noAudioRecorded": "未录制音频",
@@ -1126,8 +1132,8 @@
"playRecording": "播放录音",
"playbackSpeed": "播放速度",
"playing": "播放中",
"pressKeysForNewBinding": "按下按键设置新绑定",
"preview": "预览",
"previousImage": "上一张图像",
"profile": "档案",
"progressCountOf": "共",
"queued": "已执行",
@@ -1167,6 +1173,7 @@
"resultsCount": "找到 {count} 个结果",
"running": "正在运行",
"save": "保存",
"saveAnyway": "仍然保存",
"saving": "正在保存",
"scrollLeft": "向左滚动",
"scrollRight": "向右滚动",
@@ -1185,6 +1192,7 @@
"selectItemsToDuplicate": "选择复制",
"selectItemsToRename": "选择重命名",
"selectedFile": "已选文件",
"setAKeybindingForTheFollowing": "为以下操作设置快捷键:",
"setAsBackground": "设为背景",
"settings": "设置",
"shortcutSuffix": "{shortcut}",
@@ -1200,6 +1208,8 @@
"stopRecording": "停止录音",
"submit": "提交",
"success": "成功",
"switchToGridView": "切换到网格视图",
"switchToSingleView": "切换到单图视图",
"systemInfo": "系统信息",
"terminal": "终端",
"title": "标题",
@@ -1229,6 +1239,8 @@
"you": "你"
},
"graphCanvasMenu": {
"canvasMode": "画布模式",
"canvasToolbar": "画布工具栏",
"fitView": "适应视图",
"focusMode": "专注模式",
"hand": "拖拽",
@@ -1465,12 +1477,26 @@
"dragAndDropImage": "拖拽图片到此处",
"emptyWorkflowExplanation": "你的工作流为空。你需要先添加一些节点,才能开始构建应用。",
"enterNodeGraph": "进入节点图",
"error": {
"getHelp": "如需帮助,请查看我们的 {0}、{1} 或 {2},并附上复制的错误信息。",
"github": "提交 GitHub 问题",
"goto": "在图中显示错误",
"guide": "故障排查指南",
"header": "应用遇到错误",
"log": "错误日志",
"mobileFixable": "请检查 {0} 以获取错误信息",
"promptShow": "显示错误报告",
"promptVisitGraph": "查看节点图以获取完整错误信息。",
"requiresGraph": "生成过程中出现问题。可能由于无效的隐藏输入、缺失资源或工作流配置问题导致。",
"support": "联系我们的支持团队"
},
"giveFeedback": "提供反馈",
"graphMode": "图形模式",
"hasCreditCost": "需要额外积分",
"linearMode": "App 模式",
"loadTemplate": "加载模板",
"mobileControls": "编辑与运行",
"mobileNoWorkflow": "此工作流未为应用模式构建。请尝试其他工作流。",
"queue": {
"clear": "清空队列",
"clickToClear": "点击清空队列"
@@ -1478,6 +1504,7 @@
"rerun": "重新运行",
"reuseParameters": "复用参数",
"runCount": "运行次数:",
"viewGraph": "查看节点图",
"viewJob": "查看任务",
"welcome": {
"buildApp": "构建应用",
@@ -1686,6 +1713,7 @@
"installedSection": "已安装",
"missingNodes": "缺失节点",
"notInstalled": "未安装",
"unresolvedNodes": "未解析节点",
"updatesAvailable": "有可用更新"
},
"nightlyVersion": "每夜",
@@ -1732,6 +1760,10 @@
"uninstall": "卸载",
"uninstallSelected": "卸载所选",
"uninstalling": "正在卸载",
"unresolvedNodes": {
"message": "以下节点未安装,且在注册表中未找到。",
"title": "未解析的缺失节点"
},
"update": "更新",
"updateAll": "全部更新",
"updateSelected": "更新所选",
@@ -2080,6 +2112,7 @@
"OpenAI": "OpenAI",
"PixVerse": "PixVerse",
"Recraft": "Recraft",
"Reve": "Reve",
"Rodin": "Rodin",
"Runway": "Runway",
"Sora": "Sora",
@@ -2379,6 +2412,33 @@
"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 上尚未提供的自定义节点。",
@@ -3250,6 +3310,7 @@
"interrupted": "执行已被中断",
"legacyMaskEditorDeprecated": "旧版遮罩编辑器已弃用,即将删除。",
"migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。",
"missingModelVerificationFailed": "验证缺失模型失败。部分模型可能不会在错误标签页中显示。",
"modelLoadedSuccessfully": "3D模型加载成功",
"no3dScene": "没有3D场景可以应用纹理",
"no3dSceneToExport": "没有3D场景可以导出",

View File

@@ -3207,6 +3207,21 @@
}
}
},
"FluxKVCache": {
"description": "为 Flux 系列模型的参考图像启用 KV 缓存优化。",
"display_name": "Flux KV 缓存",
"inputs": {
"model": {
"name": "模型",
"tooltip": "要在其上启用 KV 缓存的模型。"
}
},
"outputs": {
"0": {
"tooltip": "已启用 KV 缓存的模型。"
}
}
},
"FluxKontextImageScale": {
"description": "将图像调整为更适合 Flux Kontext 的尺寸。",
"display_name": "图像缩放为FluxKontext",
@@ -12855,6 +12870,133 @@
}
}
},
"ReveImageCreateNode": {
"description": "使用 Reve 根据文本描述生成图像。",
"display_name": "Reve 图像生成",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"model": {
"name": "model",
"tooltip": "用于生成的模型版本。"
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"prompt": {
"name": "prompt",
"tooltip": "期望图像的文本描述。最多 2560 个字符。"
},
"remove_background": {
"name": "remove_background",
"tooltip": "移除生成图像的背景。可能会产生额外费用。"
},
"seed": {
"name": "seed",
"tooltip": "seed 控制节点是否重新运行;无论 seed 如何,结果都是非确定性的。"
},
"upscale": {
"name": "upscale",
"tooltip": "对生成的图像进行放大。可能会产生额外费用。"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageEditNode": {
"description": "使用 Reve 通过自然语言指令编辑图像。",
"display_name": "Reve 图像编辑",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"edit_instruction": {
"name": "edit_instruction",
"tooltip": "关于如何编辑图像的文本描述。最多 2560 个字符。"
},
"image": {
"name": "image",
"tooltip": "要编辑的图像。"
},
"model": {
"name": "model",
"tooltip": "用于编辑的模型版本。"
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"remove_background": {
"name": "remove_background",
"tooltip": "移除生成图像的背景。可能会产生额外费用。"
},
"seed": {
"name": "seed",
"tooltip": "seed 控制节点是否重新运行;无论 seed 如何,结果都是非确定性的。"
},
"upscale": {
"name": "upscale",
"tooltip": "对生成的图像进行放大。可能会产生额外费用。"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"ReveImageRemixNode": {
"description": "结合参考图像和文本提示,使用 Reve 创建新图像。",
"display_name": "Reve 图像混合",
"inputs": {
"control_after_generate": {
"name": "control after generate"
},
"model": {
"name": "model",
"tooltip": "用于混合的模型版本。"
},
"model_aspect_ratio": {
"name": "aspect_ratio"
},
"model_test_time_scaling": {
"name": "test_time_scaling"
},
"prompt": {
"name": "prompt",
"tooltip": "期望图像的文本描述。可包含 XML img 标签,通过索引引用特定图像,例如 <img>0</img>、<img>1</img> 等。"
},
"reference_images": {
"name": "reference_images"
},
"remove_background": {
"name": "remove_background",
"tooltip": "移除生成图像的背景。可能会产生额外费用。"
},
"seed": {
"name": "seed",
"tooltip": "seed 控制节点是否重新运行;无论 seed 如何,结果都是非确定性的。"
},
"upscale": {
"name": "upscale",
"tooltip": "对生成的图像进行放大。可能会产生额外费用。"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"Rodin3D_Detail": {
"description": "使用Rodin API生成3D资源",
"display_name": "Rodin 3D生成 - 细节生成",

View File

@@ -186,7 +186,7 @@ const tooltipDelay = computed<number>(() =>
const { isLoading, error } = useImage({
src: asset.preview_url ?? '',
alt: asset.display_name || asset.name
alt: displayName.value
})
function handleSelect() {

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