Compare commits

..

25 Commits

Author SHA1 Message Date
Comfy Org PR Bot
4702cd18ce 1.15.3 (#3246)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-26 16:33:13 -04:00
Chenlei Hu
00d281c7fa [Test] Add playwright tests (#3245)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-26 16:32:42 -04:00
Chenlei Hu
3e25e08b10 [Bug] Fix broken output link id during reroute migration (#3244) 2025-03-26 16:00:54 -04:00
Comfy Org PR Bot
1d66d6d7d3 [chore] Update litegraph to 0.11.3 (#3242)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-26 15:25:17 -04:00
dependabot[bot]
cae5bbe86f Bump vite from 5.4.14 to 5.4.15 (#3243)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 15:25:08 -04:00
Christian Byrne
4d35d937cf [Manager] Fix build env vars (#3238) 2025-03-26 11:03:03 -04:00
Comfy Org PR Bot
60afa5cf6c [chore] Update Comfy Registry API types from comfy-api@d7be0da (#3239)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-03-26 02:48:38 -07:00
Comfy Org PR Bot
a1a33c8c9b 1.15.2 (#3237)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-25 22:07:08 -04:00
Chenlei Hu
9988fb8f1e [Bug] Fix selection toolbox select+drag (#3235)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-25 15:27:25 -04:00
Chenlei Hu
0518b170d3 [i18n] Add spanish translation (#3233)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-25 14:34:44 -04:00
Chenlei Hu
562cd7ea70 [TS] Fix type errors in app.ts (#3232) 2025-03-25 13:02:55 -04:00
Chenlei Hu
d3c64d404b [Bug] Fix selection toolbox position on pasted node (#3231) 2025-03-25 10:51:04 -04:00
Chenlei Hu
24dcaa7f72 [i18n] Translate toast messages (#3228)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 22:22:17 -04:00
Comfy Org PR Bot
6c18781663 1.15.1 (#3227)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-24 22:01:15 -04:00
Chenlei Hu
b6988e8d5c Report file load error via toast (#3226)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 22:01:07 -04:00
Chenlei Hu
ae64721555 Implement load workflow error dialog in Vue (#3225)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 21:00:50 -04:00
Terry Jia
abe65e58a0 [3d] add support to upload texture (#3224)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 16:58:45 -04:00
Comfy Org PR Bot
a1cfb68116 [chore] Update litegraph to 0.11.2 (#3223)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-03-24 16:09:40 -04:00
MohammadAboulEla
5bee36a73e Enable/Disable the drawing of image size (#3200) 2025-03-24 13:47:55 -04:00
Chenlei Hu
a4b0f5ab5e [Bug] Fix widget placeholder rendering (#3215)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-23 23:02:35 -04:00
filtered
f8a2c90138 Fix drag new link from reroute to widget (#3214) 2025-03-23 22:29:51 -04:00
Chenlei Hu
27c252f74a Render placeholder rect for DOM widgets when zoomed out (#3213)
Co-authored-by: github-actions <github-actions@github.com>
2025-03-23 21:35:58 -04:00
Comfy Org PR Bot
7e26cffb26 [chore] Update litegraph to 0.11.1 (#3212)
Co-authored-by: huchenlei <20929282+huchenlei@users.noreply.github.com>
2025-03-23 21:02:31 -04:00
niboshi
cb8354bfce Show error message on drop error (#3189) 2025-03-23 20:52:55 -04:00
Comfy Org PR Bot
d5ebd7b7cb [chore] Update litegraph to 0.11.0 (#3211)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-03-24 11:36:24 +11:00
67 changed files with 8779 additions and 359 deletions

View File

@@ -9,7 +9,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr'],
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, controlnet, lora.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

View File

@@ -0,0 +1,104 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 304.3653259277344,
"1": 42.15586471557617
},
"size": [
315,
262
],
"flags": {},
"order": 0,
"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,
"shape": 3
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 1,
"type": "PrimitiveInt",
"pos": {
"0": 14,
"1": 43
},
"size": [
203.1999969482422,
40.368401303242536
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "value",
"type": "INT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Int"
},
"widgets_values": [10]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -589,11 +589,20 @@ export class ComfyPage {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
async connectEdge() {
await this.dragAndDrop(
this.loadCheckpointNodeClipOutputSlot,
this.clipTextEncodeNode1InputSlot
)
async connectEdge(
options: {
reverse?: boolean
} = {}
) {
const { reverse = false } = options
const start = reverse
? this.clipTextEncodeNode1InputSlot
: this.loadCheckpointNodeClipOutputSlot
const end = reverse
? this.loadCheckpointNodeClipOutputSlot
: this.clipTextEncodeNode1InputSlot
await this.dragAndDrop(start, end)
}
async adjustWidgetValue() {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -309,3 +309,21 @@ test.describe('Feedback dialog', () => {
await expect(feedbackHeader).not.toBeVisible()
})
})
test.describe('Error dialog', () => {
test('Should display an error dialog when graph configure fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
const graph = window['graph']
graph.configure = () => {
throw new Error('Error on configure!')
}
})
await comfyPage.loadWorkflow('default')
const errorDialog = comfyPage.page.locator('.error-dialog-content')
await expect(errorDialog).toBeVisible()
})
})

View File

@@ -91,15 +91,20 @@ test.describe('Node Interaction', () => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
})
test('Can disconnect/connect edge', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge()
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
// Test both directions of edge connection.
;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => {
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge({ reverse })
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -5,14 +5,14 @@ import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Primitive Node', () => {
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive_node')
await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
})
// When link is dropped on widget, it should automatically convert the widget
// to input.
test('Can connect to widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive_node_unconnected')
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
// Connect the output of the primitive node to the input of first widget of the ksampler node
@@ -23,7 +23,9 @@ test.describe('Primitive Node', () => {
})
test('Can connect to dom widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive_node_unconnected_dom_widget')
await comfyPage.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget'
)
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2)
await primitiveNode.connectWidget(0, clipEncoderNode, 0)
@@ -31,4 +33,14 @@ test.describe('Primitive Node', () => {
'primitive_node_connected_dom_widget.png'
)
})
test('Can connect to static primitive node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'static_primitive_connected.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -30,6 +30,46 @@ test.describe('Selection Toolbox', () => {
).toBeVisible()
})
test('shows at correct position when node is pasted', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.selectNodes(['KSampler'])
await comfyPage.ctrlC()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV()
const overlayContainer = comfyPage.page.locator(
'.selection-overlay-container'
)
await expect(overlayContainer).toBeVisible()
// Verify the absolute position
const boundingBox = await overlayContainer.boundingBox()
expect(boundingBox).not.toBeNull()
// 10px offset for the pasted node
expect(Math.round(boundingBox!.x)).toBeCloseTo(90, -1) // Allow ~10px tolerance
// 30px offset of node title height
expect(Math.round(boundingBox!.y)).toBeCloseTo(60, -1)
})
test('hide when select and drag happen at the same time', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodePos = await node.getPosition()
// Drag on the title of the node
await comfyPage.page.mouse.move(nodePos.x + 100, nodePos.y - 15)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('.selection-overlay-container')
).not.toBeVisible()
})
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])

2
global.d.ts vendored
View File

@@ -1,6 +1,8 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
interface Navigator {
/**

20
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.15.0",
"version": "1.15.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.15.0",
"version": "1.15.3",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.11.0-2",
"@comfyorg/litegraph": "^0.11.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",
@@ -77,7 +77,7 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.14",
"vite": "^5.4.15",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
@@ -478,9 +478,9 @@
"license": "GPL-3.0-only"
},
"node_modules/@comfyorg/litegraph": {
"version": "0.11.0-2",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.11.0-2.tgz",
"integrity": "sha512-Hy1kamsE1w/kgaDecRWMzyL4i+Qmg6ffFvvEkYd+3pXYKsU9znR3juTTw8AHvbtg5WZRPEe+cnXieTuDtSyRHA==",
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.11.3.tgz",
"integrity": "sha512-OEZJMFbC4WwsIW+w42MA8cwMY/Uu+OL5tQfvdhrlIj51G/TZv1yvU7tNPeu0gZM24Vm5rzF9IxXF8aajD9BIlw==",
"license": "MIT"
},
"node_modules/@cspotcode/source-map-support": {
@@ -12559,9 +12559,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"version": "5.4.15",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
"integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.15.0",
"version": "1.15.3",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -60,7 +60,7 @@
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.19.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.14",
"vite": "^5.4.15",
"vite-plugin-dts": "^4.3.0",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
@@ -71,7 +71,7 @@
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.31",
"@comfyorg/litegraph": "^0.11.0-2",
"@comfyorg/litegraph": "^0.11.3",
"@primevue/forms": "^4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/vue": "^8.48.0",

View File

@@ -0,0 +1,80 @@
<template>
<div class="error-dialog-content flex flex-col gap-4">
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="title"
:message="errorMessage"
/>
<pre
class="stack-trace p-5 text-neutral-400 text-xs max-h-[50vh] overflow-auto bg-black/20"
>
{{ stackTrace }}
</pre>
<template v-if="extensionFile">
<span>{{ t('errorDialog.extensionFileHint') }}:</span>
<br />
<span class="font-bold">{{ extensionFile }}</span>
</template>
<Button
v-show="!sendReportOpen"
text
fluid
:label="$t('issueReport.helpFix')"
@click="showSendReport"
/>
<ReportIssuePanel
v-if="sendReportOpen"
:error-type="errorType"
:extra-fields="[
{
label: t('issueReport.stackTrace'),
value: 'StackTrace',
optIn: true,
getData: () => stackTrace
}
]"
:tags="{
exceptionMessage: errorMessage,
extensionFile: extensionFile ?? 'UNKNOWN'
}"
:title="t('issueReport.submitErrorReport')"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import ReportIssuePanel from './error/ReportIssuePanel.vue'
const { t } = useI18n()
const {
title: _title,
errorMessage,
stackTrace: _stackTrace,
extensionFile,
errorType = 'frontendError'
} = defineProps<{
title?: string
errorMessage: string
stackTrace?: string
extensionFile?: string
errorType?: string
}>()
const title = computed(() => _title ?? t('errorDialog.defaultTitle'))
const stackTrace = computed(() => _stackTrace ?? t('errorDialog.noStackTrace'))
const sendReportOpen = ref(false)
function showSendReport() {
sendReportOpen.value = true
}
</script>

View File

@@ -30,7 +30,6 @@ const widgets = computed(() =>
)
)
const DEFAULT_MARGIN = 10
const updateWidgets = () => {
const lgCanvas = canvasStore.canvas
if (!lgCanvas) return
@@ -49,7 +48,7 @@ const updateWidgets = () => {
widgetState.visible = visible
if (visible) {
const margin = widget.options.margin ?? DEFAULT_MARGIN
const margin = widget.margin
widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]
widgetState.size = [
(widget.width ?? node.width) - margin * 2,

View File

@@ -52,8 +52,11 @@ watch(
(canvas: LGraphCanvas | null) => {
if (!canvas) return
canvas.onSelectionChange = useChainCallback(canvas.onSelectionChange, () =>
positionSelectionOverlay(canvas)
canvas.onSelectionChange = useChainCallback(
canvas.onSelectionChange,
// Wait for next frame as sometimes the selected items haven't been
// rendered yet, so the boundingRect is not available on them.
() => requestAnimationFrame(() => positionSelectionOverlay(canvas))
)
},
{ immediate: true }
@@ -88,7 +91,12 @@ watch(
positionSelectionOverlay(canvasStore.canvas as LGraphCanvas)
}, 100)
} else {
visible.value = false
// Selection change update to visible state is delayed by a frame. Here
// we also delay a frame so that the order of events is correct when
// the initial selection and dragging happens at the same time.
requestAnimationFrame(() => {
visible.value = false
})
}
}
)

View File

@@ -54,6 +54,7 @@
@updateUpDirection="handleUpdateUpDirection"
@updateMaterialMode="handleUpdateMaterialMode"
@updateEdgeThreshold="handleUpdateEdgeThreshold"
@uploadTexture="handleUploadTexture"
@exportModel="handleExportModel"
/>
</div>
@@ -61,6 +62,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Load3DControls from '@/components/load3d/Load3DControls.vue'
import Load3DScene from '@/components/load3d/Load3DScene.vue'
@@ -75,6 +77,7 @@ import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComponentWidget } from '@/scripts/domWidget'
import { useToastStore } from '@/stores/toastStore'
const { t } = useI18n()
const { widget } = defineProps<{
widget: ComponentWidget<string[]>
}>()
@@ -155,6 +158,23 @@ const handleBackgroundImageUpdate = async (file: File | null) => {
node.properties['Background Image'] = backgroundImage.value
}
const handleUploadTexture = async (file: File) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert(t('toastMessages.no3dScene'))
return
}
try {
const texturePath = await Load3dUtils.uploadFile(file)
await load3DSceneRef.value.load3d.applyTexture(texturePath)
node.properties['Texture'] = texturePath
} catch (error) {
console.error('Error applying texture:', error)
useToastStore().addAlert(t('toastMessages.failedToApplyTexture'))
}
}
const handleUpdateFOV = (value: number) => {
fov.value = value
@@ -187,7 +207,7 @@ const handleUpdateMaterialMode = (value: MaterialMode) => {
const handleExportModel = async (format: string) => {
if (!load3DSceneRef.value?.load3d) {
useToastStore().addAlert('No 3D scene to export')
useToastStore().addAlert(t('toastMessages.no3dSceneToExport'))
return
}
@@ -196,7 +216,9 @@ const handleExportModel = async (format: string) => {
} catch (error) {
console.error('Error exporting model:', error)
useToastStore().addAlert(
`Failed to export model as ${format.toUpperCase()}`
t('toastMessages.failedToExportModel', {
format: format.toUpperCase()
})
)
}
}

View File

@@ -50,6 +50,7 @@
@updateUpDirection="handleUpdateUpDirection"
@updateMaterialMode="handleUpdateMaterialMode"
@updateEdgeThreshold="handleUpdateEdgeThreshold"
@uploadTexture="handleUploadTexture"
ref="modelControlsRef"
/>
@@ -182,6 +183,7 @@ const emit = defineEmits<{
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'exportModel', format: string): void
(e: 'uploadTexture', file: File): void
}>()
const backgroundColor = ref(props.backgroundColor)
@@ -230,6 +232,10 @@ const handleUpdateEdgeThreshold = (value: number) => {
emit('updateEdgeThreshold', value)
}
const handleUploadTexture = (file: File) => {
emit('uploadTexture', file)
}
const handleUpdateLightIntensity = (value: number) => {
emit('updateLightIntensity', value)
}

View File

@@ -65,7 +65,10 @@ const eventConfig = {
},
exportLoadingEnd: () => {
loadingOverlayRef.value?.endLoading()
}
},
textureLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.applyingTexture')),
textureLoadingEnd: () => loadingOverlayRef.value?.endLoading()
} as const
watchEffect(() => {

View File

@@ -58,6 +58,33 @@
</div>
</div>
</div>
<div
v-if="
materialMode === 'original' &&
!props.inputSpec.isAnimation &&
!props.inputSpec.isPreview
"
class="relative show-texture-upload"
>
<Button class="p-button-rounded p-button-text" @click="openTextureUpload">
<i
class="pi pi-image text-white text-lg"
v-tooltip.right="{
value: t('load3d.uploadTexture'),
showDelay: 300
}"
></i>
<input
type="file"
ref="texturePickerRef"
accept="image/*"
@change="uploadTexture"
class="absolute opacity-0 w-0 h-0 p-0 m-0 pointer-events-none"
/>
</Button>
</div>
<div v-if="materialMode === 'lineart'" class="relative show-edge-threshold">
<Button
class="p-button-rounded p-button-text"
@@ -115,6 +142,7 @@ const emit = defineEmits<{
(e: 'updateUpDirection', direction: UpDirection): void
(e: 'updateMaterialMode', mode: MaterialMode): void
(e: 'updateEdgeThreshold', value: number): void
(e: 'uploadTexture', file: File): void
}>()
const upDirection = ref(props.upDirection || 'original')
@@ -123,6 +151,7 @@ const edgeThreshold = ref(props.edgeThreshold || 85)
const showUpDirection = ref(false)
const showMaterialMode = ref(false)
const showEdgeThreshold = ref(false)
const texturePickerRef = ref<HTMLInputElement | null>(null)
const upDirections: UpDirection[] = [
'original',
@@ -218,6 +247,18 @@ const updateEdgeThreshold = () => {
emit('updateEdgeThreshold', edgeThreshold.value)
}
const openTextureUpload = () => {
texturePickerRef.value?.click()
}
const uploadTexture = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files && input.files[0]) {
emit('uploadTexture', input.files[0])
}
}
const closeSceneSlider = (e: MouseEvent) => {
const target = e.target as HTMLElement

View File

@@ -184,8 +184,8 @@ export function useCoreCommands(): ComfyCommand[] {
await api.interrupt()
useToastStore().add({
severity: 'info',
summary: 'Interrupted',
detail: 'Execution has been interrupted',
summary: t('g.interrupted'),
detail: t('toastMessages.interrupted'),
life: 1000
})
}
@@ -198,8 +198,8 @@ export function useCoreCommands(): ComfyCommand[] {
await useQueueStore().clear(['queue'])
useToastStore().add({
severity: 'info',
summary: 'Confirmed',
detail: 'Pending tasks deleted',
summary: t('g.confirmed'),
detail: t('toastMessages.pendingTasksDeleted'),
life: 3000
})
}
@@ -246,7 +246,7 @@ export function useCoreCommands(): ComfyCommand[] {
if (app.canvas.empty) {
useToastStore().add({
severity: 'error',
summary: 'Empty canvas',
summary: t('toastMessages.emptyCanvas'),
life: 3000
})
return
@@ -325,9 +325,8 @@ export function useCoreCommands(): ComfyCommand[] {
if (!canvas.selectedItems?.size) {
useToastStore().add({
severity: 'error',
summary: 'Nothing to group',
detail:
'Please select the nodes (or other groups) to create a group for',
summary: t('toastMessages.nothingToGroup'),
detail: t('toastMessages.pleaseSelectNodesToGroup'),
life: 3000
})
return

View File

@@ -8,6 +8,7 @@ import type {
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
import { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { useSettingStore } from '@/stores/settingStore'
import { is_all_same_aspect_ratio } from '@/utils/imageUtil'
const renderPreview = (
@@ -36,7 +37,9 @@ const renderPreview = (
node.imageIndex = imageIndex = 0
}
const IMAGE_TEXT_SIZE_TEXT_HEIGHT = 15
const settingStore = useSettingStore()
const allowImageSizeDraw = settingStore.get('Comfy.Node.AllowImageSizeDraw')
const IMAGE_TEXT_SIZE_TEXT_HEIGHT = allowImageSizeDraw ? 15 : 0
const dw = node.size[0]
const dh = node.size[1] - shiftY - IMAGE_TEXT_SIZE_TEXT_HEIGHT
@@ -165,12 +168,14 @@ const renderPreview = (
ctx.drawImage(img, x, y, w, h)
// Draw image size text below the image
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR
ctx.textAlign = 'center'
ctx.font = '10px sans-serif'
const sizeText = `${Math.round(img.naturalWidth)} × ${Math.round(img.naturalHeight)}`
const textY = y + h + 10
ctx.fillText(sizeText, x + w / 2, textY)
if (allowImageSizeDraw) {
ctx.fillStyle = LiteGraph.NODE_TEXT_COLOR
ctx.textAlign = 'center'
ctx.font = '10px sans-serif'
const sizeText = `${Math.round(img.naturalWidth)} × ${Math.round(img.naturalHeight)}`
const textY = y + h + 10
ctx.fillText(sizeText, x + w / 2, textY)
}
const drawButton = (
x: number,

View File

@@ -219,6 +219,13 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'boolean',
defaultValue: true
},
{
id: 'Comfy.Node.AllowImageSizeDraw',
category: ['LiteGraph', 'Node Widget', 'AllowImageSizeDraw'],
name: 'Show width × height below the image preview',
type: 'boolean',
defaultValue: true
},
{
id: 'Comfy.Group.DoubleClickTitleToEdit',
category: ['LiteGraph', 'Group', 'DoubleClickTitleToEdit'],
@@ -272,7 +279,8 @@ export const CORE_SETTINGS: SettingParams[] = [
{ value: 'ru', text: 'Русский' },
{ value: 'ja', text: '日本語' },
{ value: 'ko', text: '한국어' },
{ value: 'fr', text: 'Français' }
{ value: 'fr', text: 'Français' },
{ value: 'es', text: 'Español' }
],
defaultValue: () => navigator.language.split('-')[0] || 'en'
},

View File

@@ -7,6 +7,7 @@ import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { t } from '@/i18n'
import { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { api } from '@/scripts/api'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
@@ -32,11 +33,13 @@ useExtensionService().registerExtension({
(w: IWidget) => w.name === 'model_file'
) as IStringWidget
node.properties['Texture'] = undefined
const uploadPath = await Load3dUtils.uploadFile(
fileInput.files[0]
).catch((error) => {
console.error('File upload failed:', error)
useToastStore().addAlert('File upload failed')
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
})
const modelUrl = api.apiURL(
@@ -70,6 +73,8 @@ useExtensionService().registerExtension({
)
if (modelWidget) {
modelWidget.value = ''
node.properties['Texture'] = undefined
}
})
@@ -176,7 +181,7 @@ useExtensionService().registerExtension({
fileInput.files[0]
).catch((error) => {
console.error('File upload failed:', error)
useToastStore().addAlert('File upload failed')
useToastStore().addAlert(t('toastMessages.fileUploadFailed'))
})
const modelUrl = api.apiURL(
@@ -348,10 +353,8 @@ useExtensionService().registerExtension({
let filePath = message.model_file[0]
if (!filePath) {
const msg = 'unable to get model file path.'
const msg = t('toastMessages.unableToGetModelFilePath')
console.error(msg)
useToastStore().addAlert(msg)
}
@@ -425,10 +428,8 @@ useExtensionService().registerExtension({
let filePath = message.model_file[0]
if (!filePath) {
const msg = 'unable to get model file path.'
const msg = t('toastMessages.unableToGetModelFilePath')
console.error(msg)
useToastStore().addAlert(msg)
}

View File

@@ -61,7 +61,12 @@ class Load3DConfiguration {
if (modelWidget.value) {
onModelWidgetUpdate(modelWidget.value)
}
modelWidget.callback = onModelWidgetUpdate
modelWidget.callback = (value: string | number | boolean | object) => {
this.load3d.node.properties['Texture'] = undefined
onModelWidgetUpdate(value)
}
}
private setupDefaultProperties() {
@@ -136,6 +141,12 @@ class Load3DConfiguration {
this.load3d.setEdgeThreshold(edgeThreshold)
const texturePath = this.load3d.loadNodeProperty('Texture', null)
if (texturePath) {
await this.load3d.applyTexture(texturePath)
}
if (isFirstLoad && cameraState && typeof cameraState === 'object') {
try {
this.load3d.setCameraState(cameraState)

View File

@@ -26,7 +26,7 @@ class Load3d {
renderer: THREE.WebGLRenderer
protected clock: THREE.Clock
protected animationFrameId: number | null = null
protected node: LGraphNode
node: LGraphNode
protected eventManager: EventManager
protected nodeStorage: NodeStorage
@@ -267,6 +267,23 @@ class Load3d {
}
}
async applyTexture(texturePath: string): Promise<void> {
if (!this.modelManager.currentModel) {
throw new Error('No model to apply texture to')
}
this.eventManager.emitEvent('textureLoadingStart', null)
try {
await this.modelManager.applyTexture(texturePath)
} catch (error) {
console.error('Error applying texture:', error)
throw error
} finally {
this.eventManager.emitEvent('textureLoadingEnd', null)
}
}
setBackgroundColor(color: string): void {
this.sceneManager.setBackgroundColor(color)
this.forceRender()

View File

@@ -1,3 +1,4 @@
import { t } from '@/i18n'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useToastStore } from '@/stores/toastStore'
@@ -55,7 +56,9 @@ class Load3dUtils {
} catch (error) {
console.error('Upload error:', error)
useToastStore().addAlert(
error instanceof Error ? error.message : 'Upload failed'
error instanceof Error
? error.message
: t('toastMessages.fileUploadFailed')
)
}

View File

@@ -5,6 +5,7 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { t } from '@/i18n'
import { useToastStore } from '@/stores/toastStore'
import {
@@ -67,7 +68,7 @@ export class LoaderManager implements LoaderManagerInterface {
}
if (!fileExtension) {
useToastStore().addAlert('Could not determine file type')
useToastStore().addAlert(t('toastMessages.couldNotDetermineFileType'))
return
}
@@ -81,7 +82,7 @@ export class LoaderManager implements LoaderManagerInterface {
} catch (error) {
this.eventManager.emitEvent('modelLoadingEnd', null)
console.error('Error loading model:', error)
useToastStore().addAlert('Error loading model')
useToastStore().addAlert(t('toastMessages.errorLoadingModel'))
}
}

View File

@@ -3,6 +3,7 @@ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { t } from '@/i18n'
import { useToastStore } from '@/stores/toastStore'
export class ModelExporter {
@@ -46,7 +47,7 @@ export class ModelExporter {
URL.revokeObjectURL(link.href)
} catch (error) {
console.error('Error downloading from URL:', error)
useToastStore().addAlert('Failed to download file from URL')
useToastStore().addAlert(t('toastMessages.failedToDownloadFile'))
throw error
}
}
@@ -84,7 +85,9 @@ export class ModelExporter {
ModelExporter.saveArrayBuffer(result, filename)
} catch (error) {
console.error('Error exporting GLB:', error)
useToastStore().addAlert('Failed to export model as GLB')
useToastStore().addAlert(
t('toastMessages.failedToExportModel', { format: 'GLB' })
)
throw error
}
}
@@ -111,7 +114,9 @@ export class ModelExporter {
ModelExporter.saveString(result, filename)
} catch (error) {
console.error('Error exporting OBJ:', error)
useToastStore().addAlert('Failed to export model as OBJ')
useToastStore().addAlert(
t('toastMessages.failedToExportModel', { format: 'OBJ' })
)
throw error
}
}
@@ -138,7 +143,9 @@ export class ModelExporter {
ModelExporter.saveString(result, filename)
} catch (error) {
console.error('Error exporting STL:', error)
useToastStore().addAlert('Failed to export model as STL')
useToastStore().addAlert(
t('toastMessages.failedToExportModel', { format: 'STL' })
)
throw error
}
}

View File

@@ -5,6 +5,7 @@ import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeome
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
import Load3dUtils from './Load3dUtils'
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
import { ConditionalEdgesGeometry } from './conditional-lines/ConditionalEdgesGeometry'
import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShader.js'
@@ -37,6 +38,8 @@ export class ModelManager implements ModelManagerInterface {
depthMaterial: THREE.MeshDepthMaterial
originalFileName: string | null = null
originalURL: string | null = null
appliedTexture: THREE.Texture | null = null
textureLoader: THREE.TextureLoader
private scene: THREE.Scene
private renderer: THREE.WebGLRenderer
@@ -68,6 +71,7 @@ export class ModelManager implements ModelManagerInterface {
this.eventManager = eventManager
this.activeCamera = getActiveCamera()
this.setupCamera = setupCamera
this.textureLoader = new THREE.TextureLoader()
if (
options &&
@@ -113,6 +117,11 @@ export class ModelManager implements ModelManagerInterface {
this.wireframeMaterial.dispose()
this.depthMaterial.dispose()
if (this.appliedTexture) {
this.appliedTexture.dispose()
this.appliedTexture = null
}
this.disposeLineartModel()
}
@@ -126,6 +135,66 @@ export class ModelManager implements ModelManagerInterface {
})
}
async applyTexture(texturePath: string): Promise<void> {
if (!this.currentModel) {
throw new Error('No model available to apply texture to')
}
if (this.appliedTexture) {
this.appliedTexture.dispose()
}
try {
let imageUrl = Load3dUtils.getResourceURL(
...Load3dUtils.splitFilePath(texturePath)
)
if (!imageUrl.startsWith('/api')) {
imageUrl = '/api' + imageUrl
}
this.appliedTexture = await new Promise<THREE.Texture>(
(resolve, reject) => {
this.textureLoader.load(
imageUrl,
(texture) => {
texture.colorSpace = THREE.SRGBColorSpace
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
resolve(texture)
},
undefined,
(error) => reject(error)
)
}
)
if (this.materialMode === 'original') {
this.currentModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
const material = new THREE.MeshStandardMaterial({
map: this.appliedTexture,
metalness: 0.1,
roughness: 0.8,
side: THREE.DoubleSide
})
if (!this.originalMaterials.has(child)) {
this.originalMaterials.set(child, child.material)
}
child.material = material
}
})
}
return Promise.resolve()
} catch (error) {
console.error('Error applying texture:', error)
return Promise.reject(error)
}
}
disposeLineartModel(): void {
this.disposeEdgesModel()
this.disposeShadowModel()
@@ -576,7 +645,16 @@ export class ModelManager implements ModelManagerInterface {
if (originalMaterial) {
child.material = originalMaterial
} else {
child.material = this.standardMaterial
if (this.appliedTexture) {
child.material = new THREE.MeshStandardMaterial({
map: this.appliedTexture,
metalness: 0.1,
roughness: 0.8,
side: THREE.DoubleSide
})
} else {
child.material = this.standardMaterial
}
}
break
}
@@ -638,6 +716,11 @@ export class ModelManager implements ModelManagerInterface {
this.originalFileName = null
this.originalURL = null
if (this.appliedTexture) {
this.appliedTexture.dispose()
this.appliedTexture = null
}
this.originalMaterials = new WeakMap()
}

View File

@@ -140,7 +140,7 @@ class ManageTemplates extends ComfyDialog {
exportAll() {
if (this.templates.length == 0) {
useToastStore().addAlert('No templates to export.')
useToastStore().addAlert(t('toastMessages.noTemplatesToExport'))
return
}

View File

@@ -532,7 +532,7 @@ export function convertToInput(
node: LGraphNode,
widget: IWidget,
config: InputSpec
) {
): INodeInputSlot {
hideWidget(node, widget)
const { type } = getWidgetType(config)
@@ -690,11 +690,7 @@ app.registerExtension({
if (!inputSpec) return
const input = convertToInput(node, widget, inputSpec)
if (!input) return
const originNode = link.node
originNode.connectSlots(link.fromSlot, node, input, undefined)
link.node.connectSlots(link.fromSlot, node, input, link.fromReroute?.id)
}
)
},

View File

@@ -4,6 +4,10 @@ import enCommands from './locales/en/commands.json'
import en from './locales/en/main.json'
import enNodes from './locales/en/nodeDefs.json'
import enSettings from './locales/en/settings.json'
import esCommands from './locales/es/commands.json'
import es from './locales/es/main.json'
import esNodes from './locales/es/nodeDefs.json'
import esSettings from './locales/es/settings.json'
import frCommands from './locales/fr/commands.json'
import fr from './locales/fr/main.json'
import frNodes from './locales/fr/nodeDefs.json'
@@ -40,7 +44,8 @@ const messages = {
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings)
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings)
}
export const i18n = createI18n({
@@ -50,7 +55,7 @@ export const i18n = createI18n({
fallbackLocale: 'en',
messages,
// Ignore warnings for locale options as each option is in its own language.
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français"
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"
missingWarn: /^(?!settings\.Comfy_Locale\.options\.).+/,
fallbackWarn: /^(?!settings\.Comfy_Locale\.options\.).+/
})

View File

@@ -10,6 +10,7 @@ Our project supports multiple languages using `vue-i18n`. This allows users arou
- ja (日本語)
- ko (한국어)
- fr (Français)
- es (Español)
## How to Add a New Language
@@ -28,7 +29,7 @@ module.exports = defineConfig({
entry: 'src/locales/en.json', // Base language file
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr'], // Add the new language(s) here
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'], // Add the new language(s) here
});
```
@@ -66,7 +67,8 @@ Add the newly added language to the following item in `src/constants/coreSetting
{ value: 'ru', text: 'Русский' },
{ value: 'ja', text: '日本語' },
{ value: 'ko', text: '한국어' },
{ value: 'fr', text: 'Français' }
{ value: 'fr', text: 'Français' },
{ value: 'es', text: 'Español' }
],
defaultValue: navigator.language.split('-')[0] || 'en'
},

View File

@@ -20,6 +20,7 @@
"about": "About",
"add": "Add",
"confirm": "Confirm",
"confirmed": "Confirmed",
"reset": "Reset",
"resetKeybindingsTooltip": "Reset keybindings to default",
"customizeFolder": "Customize Folder",
@@ -84,6 +85,7 @@
"nodes": "Nodes",
"community": "Community",
"all": "All",
"update": "Update",
"updated": "Updated",
"resultsCount": "Found {count} Results",
"status": "Status",
@@ -98,7 +100,8 @@
"restart": "Restart",
"missing": "Missing",
"inProgress": "In progress",
"completed": "Completed"
"completed": "Completed",
"interrupted": "Interrupted"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -951,6 +954,12 @@
"missingModels": "Missing Models",
"missingModelsMessage": "When loading the graph, the following models were not found"
},
"errorDialog": {
"defaultTitle": "An error occurred",
"loadWorkflowTitle": "Loading aborted due to error reloading workflow data",
"noStackTrace": "No stacktrace available",
"extensionFileHint": "This may be due to the following script"
},
"desktopUpdate": {
"title": "Updating ComfyUI Desktop",
"description": "ComfyUI Desktop is installing new dependencies. This may take a few minutes.",
@@ -981,6 +990,31 @@
"edgeThreshold": "Edge Threshold",
"export": "Export",
"exportModel": "Export Model",
"exportingModel": "Exporting model..."
"exportingModel": "Exporting model...",
"uploadTexture": "Upload Texture",
"applyingTexture": "Applying Texture..."
},
"toastMessages": {
"no3dScene": "No 3D scene to apply texture",
"failedToApplyTexture": "Failed to apply texture",
"no3dSceneToExport": "No 3D scene to export",
"failedToExportModel": "Failed to export model as {format}",
"fileLoadError": "Unable to find workflow in {fileName}",
"dropFileError": "Unable to process dropped item: {error}",
"interrupted": "Execution has been interrupted",
"pendingTasksDeleted": "Pending tasks deleted",
"nothingToGroup": "Nothing to group",
"pleaseSelectNodesToGroup": "Please select the nodes (or other groups) to create a group for",
"emptyCanvas": "Empty canvas",
"fileUploadFailed": "File upload failed",
"unableToGetModelFilePath": "Unable to get model file path",
"couldNotDetermineFileType": "Could not determine file type",
"errorLoadingModel": "Error loading model",
"failedToDownloadFile": "Failed to download file",
"updateRequested": "Update requested",
"nodeDefinitionsUpdated": "Node definitions updated",
"errorSaveSetting": "Error saving setting {id}: {err}",
"errorCopyImage": "Error copying image: {error}",
"noTemplatesToExport": "No templates to export"
}
}

View File

@@ -135,6 +135,9 @@
"title": "title"
}
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Show width × height below the image preview"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Auto snap link to node slot",
"tooltip": "When dragging a link over a node, the link automatically snap to a viable input slot on the node"

View File

@@ -0,0 +1,210 @@
{
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "Abrir carpeta de nodos personalizados"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "Abrir carpeta de entradas"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "Abrir carpeta de registros"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "Abrir extra_model_paths.yaml"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "Abrir carpeta de modelos"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": "Abrir carpeta de salidas"
},
"Comfy-Desktop_OpenDevTools": {
"label": "Abrir herramientas de desarrollo"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "Guía de usuario de escritorio"
},
"Comfy-Desktop_Quit": {
"label": "Salir"
},
"Comfy-Desktop_Reinstall": {
"label": "Reinstalar"
},
"Comfy-Desktop_Restart": {
"label": "Reiniciar"
},
"Comfy_BrowseTemplates": {
"label": "Explorar plantillas"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "Eliminar elementos seleccionados"
},
"Comfy_Canvas_FitView": {
"label": "Ajustar vista a los nodos seleccionados"
},
"Comfy_Canvas_ResetView": {
"label": "Restablecer vista"
},
"Comfy_Canvas_ToggleLinkVisibility": {
"label": "Alternar visibilidad de enlace en lienzo"
},
"Comfy_Canvas_ToggleLock": {
"label": "Alternar bloqueo en lienzo"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "Omitir/No omitir nodos seleccionados"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "Colapsar/Expandir nodos seleccionados"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "Silenciar/Activar sonido de nodos seleccionados"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "Anclar/Desanclar nodos seleccionados"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "Anclar/Desanclar elementos seleccionados"
},
"Comfy_Canvas_ZoomIn": {
"label": "Acercar"
},
"Comfy_Canvas_ZoomOut": {
"label": "Alejar"
},
"Comfy_ClearPendingTasks": {
"label": "Borrar tareas pendientes"
},
"Comfy_ClearWorkflow": {
"label": "Borrar flujo de trabajo"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicar flujo de trabajo actual"
},
"Comfy_ExportWorkflow": {
"label": "Exportar flujo de trabajo"
},
"Comfy_ExportWorkflowAPI": {
"label": "Exportar flujo de trabajo (formato API)"
},
"Comfy_Feedback": {
"label": "Dar retroalimentación"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Ajustar grupo al contenido"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "Agrupar nodos seleccionados"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "Convertir nodos seleccionados en nodo de grupo"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "Administrar nodos de grupo"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "Desagrupar nodos de grupo seleccionados"
},
"Comfy_Help_AboutComfyUI": {
"label": "Abrir Acerca de ComfyUI"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "Abrir Comfy-Org Discord"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "Abrir documentación de ComfyUI"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "Abrir foro de ComfyUI"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "Abrir problemas de ComfyUI"
},
"Comfy_Interrupt": {
"label": "Interrumpir"
},
"Comfy_LoadDefaultWorkflow": {
"label": "Cargar flujo de trabajo predeterminado"
},
"Comfy_Manager_CustomNodesManager": {
"label": "Administrador de nodos personalizados"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "Alternar diálogo de progreso del administrador"
},
"Comfy_NewBlankWorkflow": {
"label": "Nuevo flujo de trabajo en blanco"
},
"Comfy_OpenClipspace": {
"label": "Abrir espacio de clips"
},
"Comfy_OpenWorkflow": {
"label": "Abrir Flujo de Trabajo"
},
"Comfy_QueuePrompt": {
"label": "Prompt de Cola"
},
"Comfy_QueuePromptFront": {
"label": "Prompt de Cola (Frente)"
},
"Comfy_Redo": {
"label": "Rehacer"
},
"Comfy_RefreshNodeDefinitions": {
"label": "Actualizar Definiciones de Nodo"
},
"Comfy_SaveWorkflow": {
"label": "Guardar Flujo de Trabajo"
},
"Comfy_SaveWorkflowAs": {
"label": "Guardar Flujo de Trabajo Como"
},
"Comfy_ShowSettingsDialog": {
"label": "Mostrar Diálogo de Configuraciones"
},
"Comfy_ToggleTheme": {
"label": "Cambiar Tema (Oscuro/Claro)"
},
"Comfy_Undo": {
"label": "Deshacer"
},
"Workspace_CloseWorkflow": {
"label": "Cerrar Flujo de Trabajo Actual"
},
"Workspace_NextOpenedWorkflow": {
"label": "Siguiente Flujo de Trabajo Abierto"
},
"Workspace_PreviousOpenedWorkflow": {
"label": "Flujo de Trabajo Abierto Anterior"
},
"Workspace_SearchBox_Toggle": {
"label": "Alternar Caja de Búsqueda"
},
"Workspace_ToggleBottomPanel": {
"label": "Alternar Panel Inferior"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Alternar Panel Inferior de Terminal"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Alternar Panel Inferior de Registros"
},
"Workspace_ToggleFocusMode": {
"label": "Alternar Modo de Enfoque"
},
"Workspace_ToggleSidebarTab_model-library": {
"label": "Alternar Barra Lateral de Biblioteca de Modelos",
"tooltip": "Biblioteca de Modelos"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Alternar Barra Lateral de Biblioteca de Nodos",
"tooltip": "Biblioteca de Nodos"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Alternar Barra Lateral de Cola",
"tooltip": "Cola"
},
"Workspace_ToggleSidebarTab_workflows": {
"label": "Alternar Barra Lateral de Flujos de Trabajo",
"tooltip": "Flujos de Trabajo"
}
}

1020
src/locales/es/main.json Normal file

File diff suppressed because it is too large Load Diff

6046
src/locales/es/nodeDefs.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,339 @@
{
"Comfy-Desktop_AutoUpdate": {
"name": "Verificar actualizaciones automáticamente"
},
"Comfy-Desktop_SendStatistics": {
"name": "Enviar métricas de uso anónimas"
},
"Comfy-Desktop_UV_PypiInstallMirror": {
"name": "Espejo de instalación Pypi",
"tooltip": "Espejo de instalación pip por defecto"
},
"Comfy-Desktop_UV_PythonInstallMirror": {
"name": "Espejo de instalación Python",
"tooltip": "Las instalaciones de Python gestionadas se descargan del proyecto python-build-standalone de Astral. Esta variable puede establecerse en una URL de espejo para usar una fuente diferente para las instalaciones de Python. La URL proporcionada reemplazará https://github.com/astral-sh/python-build-standalone/releases/download en, por ejemplo, https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. Las distribuciones pueden leerse desde un directorio local utilizando el esquema de URL file://."
},
"Comfy-Desktop_UV_TorchInstallMirror": {
"name": "Espejo de instalación Torch",
"tooltip": "Espejo de instalación pip para pytorch"
},
"Comfy-Desktop_WindowStyle": {
"name": "Estilo de ventana",
"options": {
"custom": "personalizado",
"default": "predeterminado"
},
"tooltip": "Personalizado: Reemplace la barra de título del sistema con el menú superior de ComfyUI"
},
"Comfy_Canvas_SelectionToolbox": {
"name": "Mostrar caja de herramientas de selección"
},
"Comfy_ConfirmClear": {
"name": "Requerir confirmación al borrar el flujo de trabajo"
},
"Comfy_DOMClippingEnabled": {
"name": "Habilitar el recorte de elementos DOM (la habilitación puede reducir el rendimiento)"
},
"Comfy_DevMode": {
"name": "Habilitar opciones de modo de desarrollo (guardar API, etc.)"
},
"Comfy_DisableFloatRounding": {
"name": "Desactivar el redondeo predeterminado del widget flotante.",
"tooltip": "(requiere recargar la página) No se puede desactivar el redondeo cuando el redondeo está establecido por el nodo en el backend."
},
"Comfy_DisableSliders": {
"name": "Desactivar los deslizadores del widget del nodo"
},
"Comfy_EditAttention_Delta": {
"name": "Precisión Ctrl+arriba/abajo"
},
"Comfy_EnableTooltips": {
"name": "Habilitar Tooltips"
},
"Comfy_EnableWorkflowViewRestore": {
"name": "Guardar y restaurar la posición del lienzo y el nivel de zoom en los flujos de trabajo"
},
"Comfy_FloatRoundingPrecision": {
"name": "Decimales de redondeo del widget flotante [0 = automático].",
"tooltip": "(requiere recargar la página)"
},
"Comfy_Graph_CanvasInfo": {
"name": "Mostrar información del lienzo en la esquina inferior izquierda (fps, etc.)"
},
"Comfy_Graph_CanvasMenu": {
"name": "Mostrar menú del lienzo del gráfico"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "Habilitar atajo de zoom rápido (Ctrl + Shift + Arrastrar)"
},
"Comfy_Graph_LinkMarkers": {
"name": "Marcadores de punto medio de enlace",
"options": {
"Arrow": "Flecha",
"Circle": "Círculo",
"None": "Ninguno"
}
},
"Comfy_Graph_ZoomSpeed": {
"name": "Velocidad de zoom del lienzo"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "Relleno de nodos seleccionados en grupo"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "Haga doble clic en el título del grupo para editar"
},
"Comfy_LinkRelease_Action": {
"name": "Acción al soltar el enlace (Sin modificador)",
"options": {
"context menu": "menú contextual",
"no action": "sin acción",
"search box": "caja de búsqueda"
}
},
"Comfy_LinkRelease_ActionShift": {
"name": "Acción al soltar el enlace (Shift)",
"options": {
"context menu": "menú contextual",
"no action": "sin acción",
"search box": "caja de búsqueda"
}
},
"Comfy_LinkRenderMode": {
"name": "Modo de renderizado de enlace",
"options": {
"Hidden": "Oculto",
"Linear": "Lineal",
"Spline": "Spline",
"Straight": "Recto"
}
},
"Comfy_Locale": {
"name": "Idioma"
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "Multiplicador de velocidad de ajuste del pincel",
"tooltip": "Controla la rapidez con la que cambian el tamaño y la dureza del pincel al ajustar. Valores más altos significan cambios más rápidos."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "Bloquear ajuste del pincel al eje dominante",
"tooltip": "Cuando está habilitado, los ajustes del pincel solo afectarán el tamaño O la dureza según la dirección en la que te muevas más"
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "Usar nuevo editor de máscara",
"tooltip": "Cambiar a la nueva interfaz del editor de máscaras"
},
"Comfy_ModelLibrary_AutoLoadAll": {
"name": "Cargar automáticamente todas las carpetas de modelos",
"tooltip": "Si es verdadero, todas las carpetas se cargarán tan pronto como abras la biblioteca de modelos (esto puede causar retrasos mientras se carga). Si es falso, las carpetas de modelos de nivel raíz solo se cargarán una vez que hagas clic en ellas."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "Qué nombre mostrar en la vista de árbol de la biblioteca de modelos",
"options": {
"filename": "nombre de archivo",
"title": "título"
},
"tooltip": "Selecciona \"nombre de archivo\" para renderizar una vista simplificada del nombre de archivo bruto (sin directorio o extensión \".safetensors\") en la lista de modelos. Selecciona \"título\" para mostrar el título de metadatos del modelo configurable."
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "Modo de insignia de ID de nodo",
"options": {
"None": "Ninguno",
"Show all": "Mostrar todo"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "Modo de insignia de ciclo de vida del nodo",
"options": {
"None": "Ninguno",
"Show all": "Mostrar todo"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "Modo de insignia de fuente de nodo",
"options": {
"Hide built-in": "Ocultar incorporado",
"None": "Ninguno",
"Show all": "Mostrar todo"
}
},
"Comfy_NodeInputConversionSubmenus": {
"name": "En el menú contextual del nodo, coloque las entradas que convierten entre entrada/widget en submenús."
},
"Comfy_NodeSearchBoxImpl": {
"name": "Implementación de la caja de búsqueda de nodos",
"options": {
"default": "predeterminado",
"litegraph (legacy)": "litegraph (legado)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "Vista previa del nodo",
"tooltip": "Solo se aplica a la implementación predeterminada"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "Mostrar categoría de nodo en los resultados de búsqueda",
"tooltip": "Solo se aplica a la implementación predeterminada"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "Mostrar nombre de id de nodo en los resultados de búsqueda",
"tooltip": "Solo se aplica a la implementación predeterminada"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "Mostrar frecuencia de nodos en los resultados de búsqueda",
"tooltip": "Solo se aplica a la implementación predeterminada"
},
"Comfy_NodeSuggestions_number": {
"name": "Número de sugerencias de nodos",
"tooltip": "Solo para el cuadro de búsqueda/contexto del menú de litegraph"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Mostrar ancho × altura debajo de la vista previa de la imagen"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Enlace de ajuste automático a la ranura del nodo",
"tooltip": "Al arrastrar un enlace sobre un nodo, el enlace se ajustará automáticamente a una ranura de entrada viable en el nodo"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "Mantener todos los enlaces al eliminar nodos",
"tooltip": "Al eliminar un nodo, intenta reconectar todos sus enlaces de entrada y salida (eludiendo el nodo eliminado)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "Haz doble clic en el título del nodo para editarlo"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "El clic medio crea un nuevo nodo de reenrutamiento"
},
"Comfy_Node_Opacity": {
"name": "Opacidad del nodo"
},
"Comfy_Node_ShowDeprecated": {
"name": "Mostrar nodos obsoletos en la búsqueda",
"tooltip": "Los nodos obsoletos están ocultos por defecto en la interfaz de usuario, pero siguen siendo funcionales en los flujos de trabajo existentes que los utilizan."
},
"Comfy_Node_ShowExperimental": {
"name": "Mostrar nodos experimentales en la búsqueda",
"tooltip": "Los nodos experimentales están marcados como tales en la interfaz de usuario y pueden estar sujetos a cambios significativos o eliminación en futuras versiones. Úselos con precaución en flujos de trabajo de producción"
},
"Comfy_Node_SnapHighlightsNode": {
"name": "Destacar nodo de ajuste",
"tooltip": "Al arrastrar un enlace sobre un nodo con ranura de entrada viable, resalta el nodo"
},
"Comfy_Pointer_ClickBufferTime": {
"name": "Retraso de deriva del clic del puntero",
"tooltip": "Después de presionar un botón del puntero, este es el tiempo máximo (en milisegundos) que se puede ignorar el movimiento del puntero.\n\nAyuda a prevenir que los objetos sean movidos involuntariamente si el puntero se mueve al hacer clic."
},
"Comfy_Pointer_ClickDrift": {
"name": "Deriva del clic del puntero (distancia máxima)",
"tooltip": "Si el puntero se mueve más de esta distancia mientras se mantiene presionado un botón, se considera arrastrar (en lugar de hacer clic).\n\nAyuda a prevenir que los objetos sean movidos involuntariamente si el puntero se mueve al hacer clic."
},
"Comfy_Pointer_DoubleClickTime": {
"name": "Intervalo de doble clic (máximo)",
"tooltip": "El tiempo máximo en milisegundos entre los dos clics de un doble clic. Aumentar este valor puede ayudar si a veces no se registran los doble clics."
},
"Comfy_PreviewFormat": {
"name": "Formato de imagen de vista previa",
"tooltip": "Al mostrar una vista previa en el widget de imagen, conviértala en una imagen ligera, por ejemplo, webp, jpeg, webp;50, etc."
},
"Comfy_PromptFilename": {
"name": "Solicitar nombre de archivo al guardar el flujo de trabajo"
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "Límite de conteo de lotes",
"tooltip": "El número máximo de tareas añadidas a la cola con un solo clic de botón"
},
"Comfy_Queue_MaxHistoryItems": {
"name": "Tamaño del historial de la cola",
"tooltip": "El número máximo de tareas que se muestran en el historial de la cola."
},
"Comfy_Sidebar_Location": {
"name": "Ubicación de la barra lateral",
"options": {
"left": "izquierda",
"right": "derecha"
}
},
"Comfy_Sidebar_Size": {
"name": "Tamaño de la barra lateral",
"options": {
"normal": "normal",
"small": "pequeña"
}
},
"Comfy_SnapToGrid_GridSize": {
"name": "Tamaño de la cuadrícula para ajustar",
"tooltip": "Al arrastrar y redimensionar nodos mientras se mantiene presionada la tecla shift, se alinearán a la cuadrícula, esto controla el tamaño de esa cuadrícula."
},
"Comfy_TextareaWidget_FontSize": {
"name": "Tamaño de fuente del widget de área de texto"
},
"Comfy_TextareaWidget_Spellcheck": {
"name": "Corrector ortográfico del widget de área de texto"
},
"Comfy_TreeExplorer_ItemPadding": {
"name": "Relleno del elemento del explorador de árboles"
},
"Comfy_UseNewMenu": {
"name": "Usar nuevo menú",
"options": {
"Bottom": "Abajo",
"Disabled": "Deshabilitado",
"Top": "Arriba"
}
},
"Comfy_Validation_NodeDefs": {
"name": "Validar definiciones de nodos (lento)",
"tooltip": "Recomendado para desarrolladores de nodos. Esto validará todas las definiciones de nodos al iniciar."
},
"Comfy_Validation_Workflows": {
"name": "Validar flujos de trabajo"
},
"Comfy_WidgetControlMode": {
"name": "Modo de control del widget",
"options": {
"after": "después",
"before": "antes"
},
"tooltip": "Controla cuándo se actualizan los valores del widget (aleatorizar/incrementar/decrementar), ya sea antes de que se encole el aviso o después."
},
"Comfy_Window_UnloadConfirmation": {
"name": "Mostrar confirmación al cerrar la ventana"
},
"Comfy_Workflow_ConfirmDelete": {
"name": "Mostrar confirmación al eliminar flujos de trabajo"
},
"Comfy_Workflow_ShowMissingModelsWarning": {
"name": "Mostrar advertencia de modelos faltantes"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "Mostrar advertencia de nodos faltantes"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "Ordenar IDs de nodos al guardar el flujo de trabajo"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "Posición de los flujos de trabajo abiertos",
"options": {
"Sidebar": "Barra lateral",
"Topbar": "Barra superior",
"Topbar (2nd-row)": "Barra superior (2ª fila)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "Umbral de renderizado de baja calidad al hacer zoom",
"tooltip": "Renderiza formas de baja calidad cuando se aleja"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "FPS máximo",
"tooltip": "La cantidad máxima de cuadros por segundo que se permite renderizar en el lienzo. Limita el uso de la GPU a costa de la suavidad. Si es 0, se utiliza la tasa de refresco de la pantalla. Predeterminado: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "Escala los menús de widgets combinados de nodos (listas) al acercar"
},
"LiteGraph_Node_TooltipDelay": {
"name": "Retraso de la información sobre herramientas"
},
"pysssss_SnapToGrid": {
"name": "Siempre ajustar a la cuadrícula"
}
}

View File

@@ -115,6 +115,12 @@
"paused": "En pause",
"resume": "Reprendre le téléchargement"
},
"errorDialog": {
"defaultTitle": "Une erreur est survenue",
"extensionFileHint": "Cela peut être dû au script suivant",
"loadWorkflowTitle": "Chargement interrompu en raison d'une erreur de rechargement des données de workflow",
"noStackTrace": "Aucune trace de pile disponible"
},
"g": {
"about": "À propos",
"add": "Ajouter",
@@ -132,6 +138,7 @@
"community": "Communauté",
"completed": "Terminé",
"confirm": "Confirmer",
"confirmed": "Confirmé",
"continue": "Continuer",
"control_after_generate": "contrôle après génération",
"control_before_generate": "contrôle avant génération",
@@ -165,6 +172,7 @@
"install": "Installer",
"installed": "Installé",
"installing": "Installation",
"interrupted": "Interrompu",
"keybinding": "Raccourci clavier",
"loadAllFolders": "Charger tous les dossiers",
"loadWorkflow": "Charger le flux de travail",
@@ -211,6 +219,7 @@
"success": "Succès",
"systemInfo": "Informations système",
"terminal": "Terminal",
"update": "Mettre à jour",
"updated": "Mis à jour",
"upload": "Téléverser",
"videoFailedToLoad": "Échec du chargement de la vidéo",
@@ -346,6 +355,7 @@
}
},
"load3d": {
"applyingTexture": "Application de la texture...",
"backgroundColor": "Couleur de fond",
"camera": "Caméra",
"edgeThreshold": "Seuil de Bordure",
@@ -365,7 +375,8 @@
"switchCamera": "Changer de caméra",
"switchingMaterialMode": "Changement de mode de matériau...",
"upDirection": "Direction Haut",
"uploadBackgroundImage": "Télécharger l'image de fond"
"uploadBackgroundImage": "Télécharger l'image de fond",
"uploadTexture": "Télécharger Texture"
},
"maintenance": {
"None": "Aucun",
@@ -967,6 +978,29 @@
},
"title": "Commencez avec un modèle"
},
"toastMessages": {
"couldNotDetermineFileType": "Impossible de déterminer le type de fichier",
"dropFileError": "Impossible de traiter l'élément déposé : {error}",
"emptyCanvas": "Toile vide",
"errorCopyImage": "Erreur lors de la copie de l'image: {error}",
"errorLoadingModel": "Erreur lors du chargement du modèle",
"errorSaveSetting": "Erreur lors de l'enregistrement du paramètre {id}: {err}",
"failedToApplyTexture": "Échec de l'application de la texture",
"failedToDownloadFile": "Échec du téléchargement du fichier",
"failedToExportModel": "Échec de l'exportation du modèle en {format}",
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
"fileUploadFailed": "Échec du téléchargement du fichier",
"interrupted": "L'exécution a été interrompue",
"no3dScene": "Aucune scène 3D pour appliquer la texture",
"no3dSceneToExport": "Aucune scène 3D à exporter",
"noTemplatesToExport": "Aucun modèle à exporter",
"nodeDefinitionsUpdated": "Définitions de nœuds mises à jour",
"nothingToGroup": "Rien à regrouper",
"pendingTasksDeleted": "Tâches en attente supprimées",
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
"updateRequested": "Mise à jour demandée"
},
"userSelect": {
"enterUsername": "Entrez un nom d'utilisateur",
"existingUser": "Utilisateur existant",

View File

@@ -187,6 +187,9 @@
"name": "Nombre de suggestions de nœuds",
"tooltip": "Uniquement pour la boîte de recherche/contexte du menu litegraph"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Afficher la largeur × la hauteur sous l'aperçu de l'image"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Lien d'ancrage automatique à l'emplacement du nœud",
"tooltip": "Lorsque vous faites glisser un lien sur un nœud, le lien se fixe automatiquement à une fente d'entrée viable sur le nœud"

View File

@@ -115,6 +115,12 @@
"paused": "一時停止",
"resume": "ダウンロードを再開"
},
"errorDialog": {
"defaultTitle": "エラーが発生しました",
"extensionFileHint": "これは次のスクリプトが原因かもしれません",
"loadWorkflowTitle": "ワークフローデータの再読み込みエラーにより、読み込みが中止されました",
"noStackTrace": "スタックトレースは利用できません"
},
"g": {
"about": "情報",
"add": "追加",
@@ -132,6 +138,7 @@
"community": "コミュニティ",
"completed": "完了",
"confirm": "確認",
"confirmed": "確認済み",
"continue": "続ける",
"control_after_generate": "生成後の制御",
"control_before_generate": "生成前の制御",
@@ -165,6 +172,7 @@
"install": "インストール",
"installed": "インストール済み",
"installing": "インストール中",
"interrupted": "中断されました",
"keybinding": "キーバインディング",
"loadAllFolders": "すべてのフォルダーを読み込む",
"loadWorkflow": "ワークフローを読み込む",
@@ -211,6 +219,7 @@
"success": "成功",
"systemInfo": "システム情報",
"terminal": "ターミナル",
"update": "更新",
"updated": "更新済み",
"upload": "アップロード",
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
@@ -346,6 +355,7 @@
}
},
"load3d": {
"applyingTexture": "テクスチャを適用中...",
"backgroundColor": "背景色",
"camera": "カメラ",
"edgeThreshold": "エッジ閾値",
@@ -365,7 +375,8 @@
"switchCamera": "カメラを切り替える",
"switchingMaterialMode": "マテリアルモードの切り替え中...",
"upDirection": "上方向",
"uploadBackgroundImage": "背景画像をアップロード"
"uploadBackgroundImage": "背景画像をアップロード",
"uploadTexture": "テクスチャをアップロード"
},
"maintenance": {
"None": "なし",
@@ -967,6 +978,29 @@
},
"title": "テンプレートを利用して開始"
},
"toastMessages": {
"couldNotDetermineFileType": "ファイルタイプを判断できませんでした",
"dropFileError": "ドロップされたアイテムを処理できません: {error}",
"emptyCanvas": "キャンバスが空です",
"errorCopyImage": "画像のコピーにエラーが発生しました: {error}",
"errorLoadingModel": "モデルの読み込みエラー",
"errorSaveSetting": "設定{id}の保存エラー: {err}",
"failedToApplyTexture": "テクスチャの適用に失敗しました",
"failedToDownloadFile": "ファイルのダウンロードに失敗しました",
"failedToExportModel": "{format}としてモデルのエクスポートに失敗しました",
"fileLoadError": "{fileName}でワークフローが見つかりません",
"fileUploadFailed": "ファイルのアップロードに失敗しました",
"interrupted": "実行が中断されました",
"no3dScene": "テクスチャを適用する3Dシーンがありません",
"no3dSceneToExport": "エクスポートする3Dシーンがありません",
"noTemplatesToExport": "エクスポートするテンプレートがありません",
"nodeDefinitionsUpdated": "ノード定義が更新されました",
"nothingToGroup": "グループ化するものがありません",
"pendingTasksDeleted": "保留中のタスクが削除されました",
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
"updateRequested": "更新が要求されました"
},
"userSelect": {
"enterUsername": "ユーザー名を入力してください",
"existingUser": "既存のユーザー",

View File

@@ -187,6 +187,9 @@
"name": "ノード提案の数",
"tooltip": "ライトグラフの検索ボックス/コンテキストメニュー専用"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "画像プレビューの下に幅×高さを表示する"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "ノードスロットにリンクを自動スナップ",
"tooltip": "ノードの上にリンクをドラッグすると、リンクがノードの有効な入力スロットに自動的にスナップします"

View File

@@ -115,6 +115,12 @@
"paused": "일시 중지됨",
"resume": "다운로드 재개"
},
"errorDialog": {
"defaultTitle": "오류가 발생했습니다",
"extensionFileHint": "다음 스크립트 때문일 수 있습니다",
"loadWorkflowTitle": "워크플로우 데이터를 다시 로드하는 중 오류로 인해 로드가 중단되었습니다",
"noStackTrace": "스택 추적이 사용할 수 없습니다"
},
"g": {
"about": "정보",
"add": "추가",
@@ -132,6 +138,7 @@
"community": "커뮤니티",
"completed": "완료됨",
"confirm": "확인",
"confirmed": "확인됨",
"continue": "계속",
"control_after_generate": "생성 후 제어",
"control_before_generate": "생성 전 제어",
@@ -165,6 +172,7 @@
"install": "설치",
"installed": "설치됨",
"installing": "설치 중",
"interrupted": "중단됨",
"keybinding": "키 바인딩",
"loadAllFolders": "모든 폴더 로드",
"loadWorkflow": "워크플로 로드",
@@ -211,6 +219,7 @@
"success": "성공",
"systemInfo": "시스템 정보",
"terminal": "터미널",
"update": "업데이트",
"updated": "업데이트 됨",
"upload": "업로드",
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
@@ -346,6 +355,7 @@
}
},
"load3d": {
"applyingTexture": "텍스처 적용 중...",
"backgroundColor": "배경색",
"camera": "카메라",
"edgeThreshold": "엣지 임계값",
@@ -365,7 +375,8 @@
"switchCamera": "카메라 전환",
"switchingMaterialMode": "재질 모드 전환 중...",
"upDirection": "위 방향",
"uploadBackgroundImage": "배경 이미지 업로드"
"uploadBackgroundImage": "배경 이미지 업로드",
"uploadTexture": "텍스처 업로드"
},
"maintenance": {
"None": "없음",
@@ -967,6 +978,29 @@
},
"title": "템플릿으로 시작하기"
},
"toastMessages": {
"couldNotDetermineFileType": "파일 유형을 결정할 수 없습니다",
"dropFileError": "드롭된 항목을 처리할 수 없습니다: {error}",
"emptyCanvas": "빈 캔버스",
"errorCopyImage": "이미지 복사 오류: {error}",
"errorLoadingModel": "모델 로딩 오류",
"errorSaveSetting": "설정 {id} 저장 오류: {err}",
"failedToApplyTexture": "텍스처 적용에 실패했습니다",
"failedToDownloadFile": "파일 다운로드에 실패했습니다",
"failedToExportModel": "{format} 형식으로 모델 내보내기에 실패했습니다",
"fileLoadError": "{fileName}에서 워크플로우를 찾을 수 없습니다",
"fileUploadFailed": "파일 업로드에 실패했습니다",
"interrupted": "실행이 중단되었습니다",
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",
"noTemplatesToExport": "내보낼 템플릿이 없습니다",
"nodeDefinitionsUpdated": "노드 정의가 업데이트되었습니다",
"nothingToGroup": "그룹화할 항목이 없습니다",
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
"updateRequested": "업데이트 요청됨"
},
"userSelect": {
"enterUsername": "사용자 이름 입력",
"existingUser": "기존 사용자",

View File

@@ -187,6 +187,9 @@
"name": "노드 제안 수",
"tooltip": "라이트그래프 검색 상자/컨텍스트 메뉴 전용"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "이미지 미리보기 아래에 너비 × 높이 표시"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "링크를 노드 슬롯에 자동 스냅",
"tooltip": "링크를 노드 위로 드래그할 때 링크가 노드의 유효한 입력 슬롯에 자동으로 스냅됩니다."

View File

@@ -115,6 +115,12 @@
"paused": "Приостановлено",
"resume": "Возобновить загрузку"
},
"errorDialog": {
"defaultTitle": "Произошла ошибка",
"extensionFileHint": "Это может быть связано со следующим скриптом",
"loadWorkflowTitle": "Загрузка прервана из-за ошибки при перезагрузке данных рабочего процесса",
"noStackTrace": "Стек вызовов недоступен"
},
"g": {
"about": "О программе",
"add": "Добавить",
@@ -132,6 +138,7 @@
"community": "Сообщество",
"completed": "Завершено",
"confirm": "Подтвердить",
"confirmed": "Подтверждено",
"continue": "Продолжить",
"control_after_generate": "управление после генерации",
"control_before_generate": "управление до генерации",
@@ -165,6 +172,7 @@
"install": "Установить",
"installed": "Установлено",
"installing": "Установка",
"interrupted": "Прервано",
"keybinding": "Привязка клавиш",
"loadAllFolders": "Загрузить все папки",
"loadWorkflow": "Загрузить рабочий процесс",
@@ -211,6 +219,7 @@
"success": "Успех",
"systemInfo": "Информация о системе",
"terminal": "Терминал",
"update": "Обновить",
"updated": "Обновлено",
"upload": "Загрузить",
"videoFailedToLoad": "Не удалось загрузить видео",
@@ -346,6 +355,7 @@
}
},
"load3d": {
"applyingTexture": "Применение текстуры...",
"backgroundColor": "Цвет фона",
"camera": "Камера",
"edgeThreshold": "Пороговое значение края",
@@ -365,7 +375,8 @@
"switchCamera": "Переключить камеру",
"switchingMaterialMode": "Переключение режима материала...",
"upDirection": "Направление Вверх",
"uploadBackgroundImage": "Загрузить фоновое изображение"
"uploadBackgroundImage": "Загрузить фоновое изображение",
"uploadTexture": "Загрузить текстуру"
},
"maintenance": {
"None": "Нет",
@@ -967,6 +978,29 @@
},
"title": "Начните с шаблона"
},
"toastMessages": {
"couldNotDetermineFileType": "Не удалось определить тип файла",
"dropFileError": "Не удалось обработать перетаскиваемый элемент: {error}",
"emptyCanvas": "Пустой холст",
"errorCopyImage": "Ошибка копирования изображения: {error}",
"errorLoadingModel": "Ошибка загрузки модели",
"errorSaveSetting": "Ошибка сохранения настройки {id}: {err}",
"failedToApplyTexture": "Не удалось применить текстуру",
"failedToDownloadFile": "Не удалось скачать файл",
"failedToExportModel": "Не удалось экспортировать модель как {format}",
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
"fileUploadFailed": "Не удалось загрузить файл",
"interrupted": "Выполнение было прервано",
"no3dScene": "Нет 3D сцены для применения текстуры",
"no3dSceneToExport": "Нет 3D сцены для экспорта",
"noTemplatesToExport": "Нет шаблонов для экспорта",
"nodeDefinitionsUpdated": "Определения узлов обновлены",
"nothingToGroup": "Нечего группировать",
"pendingTasksDeleted": "Ожидающие задачи удалены",
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
"updateRequested": "Запрошено обновление"
},
"userSelect": {
"enterUsername": "Введите имя пользователя",
"existingUser": "Существующий пользователь",

View File

@@ -187,6 +187,9 @@
"name": "Количество предложенных нод",
"tooltip": "Только для поля поиска litegraph/контекстного меню"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "Показывать ширину × высоту под предварительным просмотром изображения"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "Автоматически привязывать ссылку к слоту ноды",
"tooltip": "При перетаскивании ссылки над нодой ссылка автоматически привязывается к подходящему входному слоту ноды"

View File

@@ -115,6 +115,12 @@
"paused": "已暂停",
"resume": "恢复下载"
},
"errorDialog": {
"defaultTitle": "发生错误",
"extensionFileHint": "这可能是由于以下脚本",
"loadWorkflowTitle": "由于重新加载工作流数据出错,加载被中止",
"noStackTrace": "无可用堆栈跟踪"
},
"g": {
"about": "关于",
"add": "添加",
@@ -132,6 +138,7 @@
"community": "社区",
"completed": "已完成",
"confirm": "确认",
"confirmed": "已确认",
"continue": "继续",
"control_after_generate": "生成后控制",
"control_before_generate": "生成前控制",
@@ -165,6 +172,7 @@
"install": "安装",
"installed": "已安装",
"installing": "正在安装",
"interrupted": "已中断",
"keybinding": "按键绑定",
"loadAllFolders": "加载所有文件夹",
"loadWorkflow": "加载工作流",
@@ -211,6 +219,7 @@
"success": "成功",
"systemInfo": "系统信息",
"terminal": "终端",
"update": "更新",
"updated": "已更新",
"upload": "上传",
"videoFailedToLoad": "视频加载失败",
@@ -346,6 +355,7 @@
}
},
"load3d": {
"applyingTexture": "应用纹理中...",
"backgroundColor": "背景颜色",
"camera": "相机",
"edgeThreshold": "边缘阈值",
@@ -365,7 +375,8 @@
"switchCamera": "切换摄像头",
"switchingMaterialMode": "切换材质模式中...",
"upDirection": "向上方向",
"uploadBackgroundImage": "上传背景图片"
"uploadBackgroundImage": "上传背景图片",
"uploadTexture": "上传纹理"
},
"maintenance": {
"None": "无",
@@ -967,6 +978,29 @@
},
"title": "从模板开始"
},
"toastMessages": {
"couldNotDetermineFileType": "无法确定文件类型",
"dropFileError": "无法处理掉落的项目:{error}",
"emptyCanvas": "画布为空",
"errorCopyImage": "复制图片出错:{error}",
"errorLoadingModel": "加载模型出错",
"errorSaveSetting": "保存设置 {id} 出错:{err}",
"failedToApplyTexture": "应用纹理失败",
"failedToDownloadFile": "文件下载失败",
"failedToExportModel": "无法将模型导出为 {format}",
"fileLoadError": "无法在 {fileName} 中找到工作流",
"fileUploadFailed": "文件上传失败",
"interrupted": "执行已被中断",
"no3dScene": "没有3D场景可以应用纹理",
"no3dSceneToExport": "没有3D场景可以导出",
"noTemplatesToExport": "没有模板可以导出",
"nodeDefinitionsUpdated": "节点定义已更新",
"nothingToGroup": "没有可分组的内容",
"pendingTasksDeleted": "待处理任务已删除",
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
"unableToGetModelFilePath": "无法获取模型文件路径",
"updateRequested": "已请求更新"
},
"userSelect": {
"enterUsername": "输入用户名",
"existingUser": "用户已存在",

View File

@@ -187,6 +187,9 @@
"name": "节点建议数量",
"tooltip": "仅适用于 litegraph"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "在图像预览下方显示宽度×高度"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "连线自动吸附到节点接口",
"tooltip": "在节点上拖动连线时,连线会自动吸附到节点的可用输入接口。"

View File

@@ -11,15 +11,15 @@ import _ from 'lodash'
import type { ToastMessageOptions } from 'primevue/toast'
import { reactive } from 'vue'
import { st } from '@/i18n'
import { st, t } from '@/i18n'
import type { ResultItem } from '@/schemas/apiSchema'
import {
ComfyApiWorkflow,
type ComfyWorkflowJSON,
type ModelFile,
type NodeId,
validateComfyWorkflow
} from '@/schemas/comfyWorkflowSchema'
import { type ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import { getFromWebmFile } from '@/scripts/metadata/ebml'
import { getGltfBinaryMetadata } from '@/scripts/metadata/gltf'
@@ -63,8 +63,7 @@ import { type ComfyWidgetConstructor, ComfyWidgets } from './widgets'
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
// @ts-expect-error fixme ts strict error
function sanitizeNodeName(string) {
function sanitizeNodeName(string: string) {
let entityMap = {
'&': '',
'<': '',
@@ -75,8 +74,7 @@ function sanitizeNodeName(string) {
'=': ''
}
return String(string).replace(/[&<>"'`=]/g, function fromEntityMap(s) {
// @ts-expect-error fixme ts strict error
return entityMap[s]
return entityMap[s as keyof typeof entityMap]
})
}
@@ -92,14 +90,12 @@ type Clipspace = {
export class ComfyApp {
/**
* List of entries to queue
* @type {{number: number, batchCount: number}[]}
*/
#queueItems = []
#queueItems: { number: number; batchCount: number }[] = []
/**
* If the queue is currently being processed
* @type {boolean}
*/
#processingQueue = false
#processingQueue: boolean = false
/**
* Content Clipboard
@@ -122,17 +118,13 @@ export class ComfyApp {
graph: LGraph
// @ts-expect-error fixme ts strict error
canvas: LGraphCanvas
// @ts-expect-error fixme ts strict error
dragOverNode: LGraphNode | null
dragOverNode: LGraphNode | null = null
// @ts-expect-error fixme ts strict error
canvasEl: HTMLCanvasElement
// @ts-expect-error fixme ts strict error
lastNodeErrors: any[] | null
lastNodeErrors: any[] | null = null
/** @type {ExecutionErrorWsMessage} */
// @ts-expect-error fixme ts strict error
lastExecutionError: { node_id?: NodeId } | null
// @ts-expect-error fixme ts strict error
configuringGraph: boolean
lastExecutionError: { node_id?: NodeId } | null = null
configuringGraph: boolean = false
// @ts-expect-error fixme ts strict error
ctx: CanvasRenderingContext2D
bodyTop: HTMLElement
@@ -271,11 +263,9 @@ export class ComfyApp {
ComfyApp.clipspace_return_node = null
}
// @ts-expect-error fixme ts strict error
static copyToClipspace(node) {
static copyToClipspace(node: LGraphNode) {
var widgets = null
if (node.widgets) {
// @ts-expect-error fixme ts strict error
widgets = node.widgets.map(({ type, name, value }) => ({
type,
name,
@@ -410,9 +400,8 @@ export class ComfyApp {
#addRestoreWorkflowView() {
const serialize = LGraph.prototype.serialize
const self = this
LGraph.prototype.serialize = function () {
// @ts-expect-error fixme ts strict error
const workflow = serialize.apply(this, arguments)
LGraph.prototype.serialize = function (...args) {
const workflow = serialize.apply(this, args)
// Store the drag & scale info in the serialized workflow if the setting is enabled
if (useSettingStore().get('Comfy.EnableWorkflowViewRestore')) {
@@ -438,39 +427,43 @@ export class ComfyApp {
#addDropHandler() {
// Get prompt from dropped PNG or json
document.addEventListener('drop', async (event) => {
event.preventDefault()
event.stopPropagation()
try {
event.preventDefault()
event.stopPropagation()
const n = this.dragOverNode
this.dragOverNode = null
// Node handles file drop, we dont use the built in onDropFile handler as its buggy
// If you drag multiple files it will call it multiple times with the same file
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
return
}
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
if (
// @ts-expect-error fixme ts strict error
event.dataTransfer.files.length &&
// @ts-expect-error fixme ts strict error
event.dataTransfer.files[0].type !== 'image/bmp'
) {
// @ts-expect-error fixme ts strict error
await this.handleFile(event.dataTransfer.files[0])
} else {
// Try loading the first URI in the transfer list
const validTypes = ['text/uri-list', 'text/x-moz-url']
// @ts-expect-error fixme ts strict error
const match = [...event.dataTransfer.types].find((t) =>
validTypes.find((v) => t === v)
)
if (match) {
// @ts-expect-error fixme ts strict error
const uri = event.dataTransfer.getData(match)?.split('\n')?.[0]
if (uri) {
await this.handleFile(await (await fetch(uri)).blob())
const n = this.dragOverNode
this.dragOverNode = null
// Node handles file drop, we dont use the built in onDropFile handler as its buggy
// If you drag multiple files it will call it multiple times with the same file
if (n && n.onDragDrop && (await n.onDragDrop(event))) {
return
}
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
if (!event.dataTransfer) return
if (
event.dataTransfer.files.length &&
event.dataTransfer.files[0].type !== 'image/bmp'
) {
await this.handleFile(event.dataTransfer.files[0])
} else {
// Try loading the first URI in the transfer list
const validTypes = ['text/uri-list', 'text/x-moz-url']
const match = [...event.dataTransfer.types].find((t) =>
validTypes.find((v) => t === v)
)
if (match) {
const uri = event.dataTransfer.getData(match)?.split('\n')?.[0]
if (uri) {
await this.handleFile(
new File([await (await fetch(uri)).blob()], uri)
)
}
}
}
} catch (err: any) {
useToastStore().addAlert(
t('toastMessages.dropFileError', { error: err })
)
}
})
@@ -784,14 +777,13 @@ export class ComfyApp {
#addAfterConfigureHandler() {
const app = this
const onConfigure = app.graph.onConfigure
app.graph.onConfigure = function () {
app.graph.onConfigure = function (...args) {
// Fire callbacks before the onConfigure, this is used by widget inputs to setup the config
for (const node of app.graph.nodes) {
node.onGraphConfigured?.()
}
// @ts-expect-error fixme ts strict error
const r = onConfigure?.apply(this, arguments)
const r = onConfigure?.apply(this, args)
// Fire after onConfigure, used by primitives to generate widget using input nodes config
for (const node of app.graph.nodes) {
@@ -876,12 +868,9 @@ export class ComfyApp {
this.canvas?.draw(true, true)
}
private updateVueAppNodeDefs(
defs: Record<string, ComfyNodeDefV1 & ComfyNodeDefV2>
) {
private updateVueAppNodeDefs(defs: Record<string, ComfyNodeDefV1>) {
// Frontend only nodes registered by custom nodes.
// Example: https://github.com/rgthree/rgthree-comfy/blob/dd534e5384be8cf0c0fa35865afe2126ba75ac55/src_web/comfyui/fast_groups_bypasser.ts#L10
// @ts-expect-error fixme ts strict error
const rawDefs: Record<string, ComfyNodeDefV1> = Object.fromEntries(
Object.entries(LiteGraph.registered_node_types).map(([name, node]) => [
name,
@@ -893,9 +882,10 @@ export class ComfyApp {
output: [],
output_name: [],
output_is_list: [],
output_node: false,
python_module: 'custom_nodes.frontend_only',
description: `Frontend only node for ${name}`
}
} as ComfyNodeDefV1
])
)
@@ -922,10 +912,9 @@ export class ComfyApp {
`nodeDefs.${def.name}.display_name`,
def.display_name ?? def.name
),
// @ts-expect-error fixme ts strict error
description: def.description
? st(`nodeDefs.${def.name}.description`, def.description)
: undefined,
: '',
category: def.category
.split('/')
.map((category: string) => st(`nodeCategories.${category}`, category))
@@ -949,7 +938,6 @@ export class ComfyApp {
await this.registerNodesFromDefs(defs)
await useExtensionService().invokeExtensionsAsync('registerCustomNodes')
if (this.vueAppReady) {
// @ts-expect-error fixme ts strict error
this.updateVueAppNodeDefs(defs)
}
}
@@ -1139,56 +1127,10 @@ export class ComfyApp {
this.canvas.ds.scale = graphData.extra.ds.scale
}
} catch (error) {
let errorHint = []
// Try extracting filename to see if it was caused by an extension script
const filename =
// @ts-expect-error fixme ts strict error
error.fileName ||
// @ts-expect-error fixme ts strict error
(error.stack || '').match(/(\/extensions\/.*\.js)/)?.[1]
const pos = (filename || '').indexOf('/extensions/')
if (pos > -1) {
errorHint.push(
$el('span', {
textContent: 'This may be due to the following script:'
}),
$el('br'),
$el('span', {
style: {
fontWeight: 'bold'
},
textContent: filename.substring(pos)
})
)
}
// Show dialog to let the user know something went wrong loading the data
this.ui.dialog.show(
$el('div', [
$el('p', {
textContent: 'Loading aborted due to error reloading workflow data'
}),
$el('pre', {
style: { padding: '5px', backgroundColor: 'rgba(255,0,0,0.2)' },
// @ts-expect-error fixme ts strict error
textContent: error.toString()
}),
$el('pre', {
style: {
padding: '5px',
color: '#ccc',
fontSize: '10px',
maxHeight: '50vh',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.2)'
},
// @ts-expect-error fixme ts strict error
textContent: error.stack || 'No stacktrace available'
}),
...errorHint
]).outerHTML
)
useDialogService().showErrorDialog(error, {
title: t('errorDialog.loadWorkflowTitle'),
errorType: 'loadWorkflowError'
})
return
}
for (const node of this.graph.nodes) {
@@ -1308,13 +1250,11 @@ export class ComfyApp {
}
async queuePrompt(number: number, batchCount: number = 1): Promise<boolean> {
// @ts-expect-error fixme ts strict error
this.#queueItems.push({ number, batchCount })
// Only have one action process the items so each one gets a unique seed correctly
if (this.#processingQueue) {
// @ts-expect-error fixme ts strict error
return
return false
}
this.#processingQueue = true
@@ -1322,8 +1262,7 @@ export class ComfyApp {
try {
while (this.#queueItems.length) {
// @ts-expect-error fixme ts strict error
;({ number, batchCount } = this.#queueItems.pop())
const { number, batchCount } = this.#queueItems.pop()!
for (let i = 0; i < batchCount; i++) {
// Allow widgets to run callbacks before a prompt has been queued
@@ -1333,20 +1272,19 @@ export class ComfyApp {
const p = await this.graphToPrompt()
try {
const res = await api.queuePrompt(number, p)
// @ts-expect-error fixme ts strict error
this.lastNodeErrors = res.node_errors
// @ts-expect-error fixme ts strict error
if (this.lastNodeErrors.length > 0) {
this.lastNodeErrors = res.node_errors ?? null
if (this.lastNodeErrors?.length) {
this.canvas.draw(true, true)
} else {
try {
useExecutionStore().storePrompt({
// @ts-expect-error fixme ts strict error
id: res.prompt_id,
nodes: Object.keys(p.output),
workflow: useWorkspaceStore().workflow
.activeWorkflow as ComfyWorkflow
})
if (res.prompt_id) {
useExecutionStore().storePrompt({
id: res.prompt_id,
nodes: Object.keys(p.output),
workflow: useWorkspaceStore().workflow
.activeWorkflow as ComfyWorkflow
})
}
} catch (error) {}
}
} catch (error) {
@@ -1364,8 +1302,9 @@ export class ComfyApp {
// Allow widgets to run callbacks after a prompt has been queued
// e.g. random seed after every gen
executeWidgetsCallback(
// @ts-expect-error fixme ts strict error
p.workflow.nodes.map((n) => this.graph.getNodeById(n.id)),
p.workflow.nodes
.map((n) => this.graph.getNodeById(n.id))
.filter((n) => !!n) as LGraphNode[],
'afterQueued'
)
this.canvas.draw(true, true)
@@ -1379,12 +1318,9 @@ export class ComfyApp {
return !this.lastNodeErrors
}
// @ts-expect-error fixme ts strict error
showErrorOnFileLoad(file) {
this.ui.dialog.show(
$el('div', [
$el('p', { textContent: `Unable to find workflow in ${file.name}` })
]).outerHTML
showErrorOnFileLoad(file: File) {
useToastStore().addAlert(
t('toastMessages.fileLoadError', { fileName: file.name })
)
}
@@ -1392,10 +1328,8 @@ export class ComfyApp {
* Loads workflow data from the specified file
* @param {File} file
*/
// @ts-expect-error fixme ts strict error
async handleFile(file) {
// @ts-expect-error fixme ts strict error
const removeExt = (f) => {
async handleFile(file: File) {
const removeExt = (f: string) => {
if (!f) return f
const p = f.lastIndexOf('.')
if (p === -1) return f
@@ -1518,25 +1452,18 @@ export class ComfyApp {
}
}
// @ts-expect-error fixme ts strict error
isApiJson(data) {
// @ts-expect-error
return Object.values(data).every((v) => v.class_type)
isApiJson(data: unknown) {
return _.isObject(data) && Object.values(data).every((v) => v.class_type)
}
// @ts-expect-error fixme ts strict error
loadApiJson(apiData, fileName: string) {
loadApiJson(apiData: ComfyApiWorkflow, fileName: string) {
useWorkflowService().beforeLoadNewGraph()
const missingNodeTypes = Object.values(apiData).filter(
// @ts-expect-error
(n) => !LiteGraph.registered_node_types[n.class_type]
)
if (missingNodeTypes.length) {
this.#showMissingNodesError(
// @ts-expect-error
missingNodeTypes.map((t) => t.class_type)
)
this.#showMissingNodesError(missingNodeTypes.map((t) => t.class_type))
return
}
@@ -1545,11 +1472,9 @@ export class ComfyApp {
for (const id of ids) {
const data = apiData[id]
const node = LiteGraph.createNode(data.class_type)
// @ts-expect-error fixme ts strict error
if (!node) continue
node.id = isNaN(+id) ? id : +id
// @ts-expect-error fixme ts strict error
node.title = data._meta?.title ?? node.title
// @ts-expect-error fixme ts strict error
app.graph.add(node)
}
@@ -1650,8 +1575,8 @@ export class ComfyApp {
async refreshComboInNodes() {
const requestToastMessage: ToastMessageOptions = {
severity: 'info',
summary: 'Update',
detail: 'Update requested'
summary: t('g.update'),
detail: t('toastMessages.updateRequested')
}
if (this.vueAppReady) {
useToastStore().add(requestToastMessage)
@@ -1688,13 +1613,12 @@ export class ComfyApp {
)
if (this.vueAppReady) {
// @ts-expect-error fixme ts strict error
this.updateVueAppNodeDefs(defs)
useToastStore().remove(requestToastMessage)
useToastStore().add({
severity: 'success',
summary: 'Updated',
detail: 'Node definitions updated',
summary: t('g.updated'),
detail: t('toastMessages.nodeDefinitionsUpdated'),
life: 1000
})
}

View File

@@ -1,4 +1,4 @@
import { LGraphNode } from '@comfyorg/litegraph'
import { LGraphNode, LiteGraph } from '@comfyorg/litegraph'
import type {
ICustomWidget,
IWidget,
@@ -27,6 +27,8 @@ export interface BaseDOMWidget<V extends object | string>
readonly node: LGraphNode
/** Whether the widget is visible. */
isVisible(): boolean
/** The margin of the widget. */
margin: number
}
/**
@@ -86,6 +88,7 @@ export const isComponentWidget = <V extends object | string>(
abstract class BaseDOMWidgetImpl<V extends object | string>
implements BaseDOMWidget<V>
{
static readonly DEFAULT_MARGIN = 10
readonly type: 'custom'
readonly name: string
readonly options: DOMWidgetOptions<V>
@@ -121,6 +124,10 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
this.callback?.(this.value)
}
get margin(): number {
return this.options.margin ?? BaseDOMWidgetImpl.DEFAULT_MARGIN
}
isVisible(): boolean {
return (
!_.isNil(this.computedHeight) &&
@@ -130,7 +137,28 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
)
}
draw(): void {
draw(
ctx: CanvasRenderingContext2D,
_node: LGraphNode,
widget_width: number,
y: number,
widget_height: number,
lowQuality?: boolean
): void {
if (this.options.hideOnZoom && lowQuality) {
// Draw a placeholder rectangle
const originalFillStyle = ctx.fillStyle
ctx.beginPath()
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR
ctx.rect(
this.margin,
y + this.margin,
widget_width - this.margin * 2,
(this.computedHeight ?? widget_height) - 2 * this.margin
)
ctx.fill()
ctx.fillStyle = originalFillStyle
}
this.options.onDraw?.(this)
}

View File

@@ -1,3 +1,4 @@
import { t } from '@/i18n'
import type { Settings } from '@/schemas/apiSchema'
import type { ComfyApp } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
@@ -85,7 +86,9 @@ export class ComfySettingsDialog extends ComfyDialog<HTMLDialogElement> {
useSettingStore()
.set(id, value)
.catch((err) => {
useToastStore().addAlert(`Error saving setting '${id}': ${err}`)
useToastStore().addAlert(
t('toastMessages.errorSaveSetting', { id, err })
)
})
}

View File

@@ -8,9 +8,6 @@ import { omit } from 'lodash'
import { components } from '@/types/comfyRegistryTypes'
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
type SafeNestedProperty<
T,
K1 extends keyof T,

View File

@@ -1,4 +1,5 @@
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
@@ -147,6 +148,52 @@ export const useDialogService = () => {
})
}
function parseError(error: Error) {
const filename =
'fileName' in error
? (error.fileName as string)
: error.stack?.match(/(\/extensions\/.*\.js)/)?.[1]
const extensionFile = filename
? filename.substring(filename.indexOf('/extensions/'))
: undefined
return {
errorMessage: error.toString(),
stackTrace: error.stack,
extensionFile
}
}
/**
* Show a error dialog to the user when an error occurs.
* @param error The error to show
* @param options The options for the dialog
*/
function showErrorDialog(
error: unknown,
options: {
title?: string
errorType?: string
} = {}
) {
const props =
error instanceof Error
? parseError(error)
: {
errorMessage: String(error)
}
dialogStore.showDialog({
key: 'global-error',
component: ErrorDialogContent,
props: {
...props,
...options
}
})
}
async function prompt({
title,
message,
@@ -230,6 +277,7 @@ export const useDialogService = () => {
showIssueReportDialog,
showManagerDialog,
showManagerProgressDialog,
showErrorDialog,
prompt,
confirm
}

View File

@@ -11,7 +11,7 @@ import { Vector2 } from '@comfyorg/litegraph'
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
import { st } from '@/i18n'
import { st, t } from '@/i18n'
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -264,8 +264,10 @@ export const useLitegraphService = () => {
}
} catch (error) {
toastStore.addAlert(
// @ts-expect-error fixme ts strict error
'Error copying image: ' + (error.message ?? error)
t('toastMessages.errorCopyImage', {
// @ts-expect-error fixme ts strict error
error: error.message ?? error
})
)
}
}

View File

@@ -832,10 +832,14 @@ export interface paths {
parameters: {
query?: never
header?: never
path?: never
path: {
nodeId: string
version: string
}
cookie?: never
}
get?: never
/** list comfy-nodes for certain node */
get: operations['ListComfyNodes']
put?: never
/** create comfy-nodes for certain node */
post: operations['CreateComfyNodes']
@@ -2464,6 +2468,8 @@ export interface operations {
latest?: boolean
/** @description Database column to use as ascending ordering. Add `;desc` as suffix on each column for descending sort */
sort?: string[]
/** @description node_id to use as filter */
node_id?: string[]
/** @description The platform requesting the nodes */
form_factor?: string
}
@@ -3084,6 +3090,71 @@ export interface operations {
}
}
}
ListComfyNodes: {
parameters: {
query?: {
/** @description The page number to retrieve. */
page?: number
/** @description The number of items to include per page. */
limit?: number
}
header?: never
path: {
nodeId: string
version: string
}
cookie?: never
}
requestBody?: never
responses: {
/** @description Comy Nodes obtained successfully */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': {
comfy_nodes?: components['schemas']['ComfyNode'][]
totalNumberOfPages?: number
}
}
}
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Forbidden */
403: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['ErrorResponse']
}
}
/** @description Version not found */
404: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['ErrorResponse']
}
}
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['ErrorResponse']
}
}
}
}
CreateComfyNodes: {
parameters: {
query?: never

View File

@@ -1,3 +1,5 @@
import _ from 'lodash'
import type {
ComfyLink,
ComfyNode,
@@ -67,10 +69,15 @@ export function createNewLinks(
workflow: WorkflowJSON04,
rerouteMap: Map<NodeId, RerouteEntry>
): {
nodes: ComfyNode[]
reroutes: Reroute[]
links: ComfyLink[]
linkExtensions: LinkExtension[]
} {
const nodeById = _.keyBy(
workflow.nodes.filter((node) => node.type !== 'Reroute').map(_.cloneDeep),
'id'
)
const links: ComfyLink[] = []
const linkExtensions: LinkExtension[] = []
@@ -153,6 +160,23 @@ export function createNewLinks(
validLinkExtensions.push(linkExtension)
chainedReroutes.forEach((reroute) => validReroutes.add(reroute))
// Update source node's output slot's link ids to point to the new link.
const sourceNode = nodeById[sourceNodeId]
if (!sourceNode) {
throw new Error(
`Corrupted workflow: Source node ${sourceNodeId} not found`
)
}
const outputSlot = sourceNode.outputs?.[sourceSlot]
if (!outputSlot) {
throw new Error(
`Corrupted workflow: Output slot ${sourceSlot} not found`
)
}
outputSlot.links = outputSlot.links?.map((l) =>
l === rerouteInputLink[0] ? linkId : l
)
}
entry = undefined
}
@@ -160,6 +184,7 @@ export function createNewLinks(
}
return {
nodes: Object.values(nodeById),
links,
linkExtensions: validLinkExtensions,
reroutes: Array.from(validReroutes)
@@ -192,16 +217,14 @@ export const migrateLegacyRerouteNodes = (
const rerouteMap = createReroutePoints(legacyRerouteNodes)
// Create new links and link extensions
const { links, linkExtensions, reroutes } = createNewLinks(
const { nodes, links, linkExtensions, reroutes } = createNewLinks(
workflow,
rerouteMap
)
// Update the workflow
newWorkflow.links = links
newWorkflow.nodes = newWorkflow.nodes.filter(
(node) => node.type !== 'Reroute'
)
newWorkflow.nodes = nodes
newWorkflow.extra.reroutes = reroutes
newWorkflow.extra.linkExtensions = linkExtensions

View File

@@ -2,6 +2,51 @@
"last_node_id": 27,
"last_link_id": 34,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
47.948699951171875,
239.2628173828125
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": []
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": []
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
13,
21
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.safetensors"
]
},
{
"id": 12,
"type": "VAEDecode",
@@ -40,51 +85,6 @@
},
"widgets_values": []
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
47.948699951171875,
239.2628173828125
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": []
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": []
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
13,
31
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.safetensors"
]
},
{
"id": 26,
"type": "VAEDecode",

View File

@@ -2,6 +2,50 @@
"last_node_id": 24,
"last_link_id": 30,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
160,
240
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": []
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": []
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
21
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.safetensors"
]
},
{
"id": 12,
"type": "VAEDecode",
@@ -39,50 +83,6 @@
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
160,
240
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": []
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": []
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
13
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.safetensors"
]
}
],
"links": [

View File

@@ -183,8 +183,8 @@ export default defineConfig({
!(process.env.NODE_ENV === 'development' || !process.env.SENTRY_DSN)
),
__SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || ''),
__ALGOLIA_APP_ID__: JSON.stringify(process.env.VITE_ALGOLIA_APP_ID || ''),
__ALGOLIA_API_KEY__: JSON.stringify(process.env.VITE_ALGOLIA_API_KEY || '')
__ALGOLIA_APP_ID__: JSON.stringify(process.env.ALGOLIA_APP_ID || ''),
__ALGOLIA_API_KEY__: JSON.stringify(process.env.ALGOLIA_API_KEY || '')
},
resolve: {