From 2425e32d510207e79db930e6a584c42f268fa00c Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Thu, 8 May 2025 11:44:26 -0400 Subject: [PATCH] Partial execute to selected output nodes (#3818) Co-authored-by: github-actions --- .../assets/execution/partial_execution.json | 85 +++++++++++++++++++ browser_tests/tests/execution.spec.ts | 24 ++++++ src/components/graph/SelectionToolbox.vue | 2 + .../graph/selectionToolbox/ExecuteButton.vue | 71 ++++++++++++++++ src/composables/useCoreCommands.ts | 22 +++++ src/locales/en/commands.json | 3 + src/locales/en/main.json | 9 ++ src/locales/es/commands.json | 3 + src/locales/es/main.json | 9 ++ src/locales/fr/commands.json | 3 + src/locales/fr/main.json | 9 ++ src/locales/ja/commands.json | 3 + src/locales/ja/main.json | 9 ++ src/locales/ko/commands.json | 3 + src/locales/ko/main.json | 9 ++ src/locales/ru/commands.json | 3 + src/locales/ru/main.json | 9 ++ src/locales/zh/commands.json | 3 + src/locales/zh/main.json | 9 ++ src/scripts/app.ts | 26 ++++-- src/utils/executionUtil.ts | 49 +++++++++-- 21 files changed, 351 insertions(+), 12 deletions(-) create mode 100644 browser_tests/assets/execution/partial_execution.json create mode 100644 src/components/graph/selectionToolbox/ExecuteButton.vue diff --git a/browser_tests/assets/execution/partial_execution.json b/browser_tests/assets/execution/partial_execution.json new file mode 100644 index 000000000..5685ab814 --- /dev/null +++ b/browser_tests/assets/execution/partial_execution.json @@ -0,0 +1,85 @@ +{ + "id": "1a95532f-c8aa-4c9d-a7f6-f928ba2d4862", + "revision": 0, + "last_node_id": 4, + "last_link_id": 3, + "nodes": [ + { + "id": 4, + "type": "PreviewAny", + "pos": [946.2566528320312, 598.4373168945312], + "size": [140, 76], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 3 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 1, + "type": "PreviewAny", + "pos": [951.0236206054688, 421.3861083984375], + "size": [140, 76], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 2 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 3, + "type": "PrimitiveString", + "pos": [575.1760864257812, 504.5214538574219], + "size": [270, 58], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [2, 3] + } + ], + "properties": { + "Node name for S&R": "PrimitiveString" + }, + "widgets_values": ["foo"] + } + ], + "links": [ + [2, 3, 0, 1, 0, "*"], + [3, 3, 0, 4, 0, "*"] + ], + "groups": [], + "config": {}, + "extra": { + "frontendVersion": "1.19.1", + "ds": { + "offset": [400, 400], + "scale": 1 + } + }, + "version": 0.4 +} diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index 1352d792f..4adab98b6 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -18,3 +18,27 @@ test.describe('Execution', () => { ) }) }) + +test.describe('Execute to selected output nodes', () => { + test('Execute to selected output nodes', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('execution/partial_execution') + const input = await comfyPage.getNodeRefById(3) + const output1 = await comfyPage.getNodeRefById(1) + const output2 = await comfyPage.getNodeRefById(4) + expect(await (await input.getWidget(0)).getValue()).toBe('foo') + expect(await (await output1.getWidget(0)).getValue()).toBe('') + expect(await (await output2.getWidget(0)).getValue()).toBe('') + + await output1.click('title') + + await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') + // @note: Wait for the execution to finish. We might want to move to a more + // reliable way to wait for the execution to finish. Workflow in this test + // is simple enough that this is fine for now. + await comfyPage.page.waitForTimeout(200) + + expect(await (await input.getWidget(0)).getValue()).toBe('foo') + expect(await (await output1.getWidget(0)).getValue()).toBe('foo') + expect(await (await output2.getWidget(0)).getValue()).toBe('') + }) +}) diff --git a/src/components/graph/SelectionToolbox.vue b/src/components/graph/SelectionToolbox.vue index 2c36a6c2a..122d270c8 100644 --- a/src/components/graph/SelectionToolbox.vue +++ b/src/components/graph/SelectionToolbox.vue @@ -6,6 +6,7 @@ content: 'p-0 flex flex-row' }" > + + + + diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts index 080f62cf5..9e1a31d52 100644 --- a/src/composables/useCoreCommands.ts +++ b/src/composables/useCoreCommands.ts @@ -312,6 +312,28 @@ export function useCoreCommands(): ComfyCommand[] { await app.queuePrompt(-1, batchCount) } }, + { + id: 'Comfy.QueueSelectedOutputNodes', + icon: 'pi pi-play', + label: 'Queue Selected Output Nodes', + versionAdded: '1.19.6', + function: async () => { + const batchCount = useQueueSettingsStore().batchCount + const queueNodeIds = getSelectedNodes() + .filter((node) => node.constructor.nodeData.output_node) + .map((node) => node.id) + if (queueNodeIds.length === 0) { + toastStore.add({ + severity: 'error', + summary: t('toastMessages.nothingToQueue'), + detail: t('toastMessages.pleaseSelectOutputNodes'), + life: 3000 + }) + return + } + await app.queuePrompt(0, batchCount, queueNodeIds) + } + }, { id: 'Comfy.ShowSettingsDialog', icon: 'pi pi-cog', diff --git a/src/locales/en/commands.json b/src/locales/en/commands.json index 3afc8fedb..e707fb93f 100644 --- a/src/locales/en/commands.json +++ b/src/locales/en/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "Queue Prompt (Front)" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "Queue Selected Output Nodes" + }, "Comfy_Redo": { "label": "Redo" }, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 6ee05ac3e..bcd671c23 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -803,6 +803,7 @@ "Open": "Open", "Queue Prompt": "Queue Prompt", "Queue Prompt (Front)": "Queue Prompt (Front)", + "Queue Selected Output Nodes": "Queue Selected Output Nodes", "Redo": "Redo", "Refresh Node Definitions": "Refresh Node Definitions", "Save": "Save", @@ -1197,6 +1198,8 @@ "resizeNodeMatchOutput": "Resize Node to match output" }, "toastMessages": { + "nothingToQueue": "Nothing to queue", + "pleaseSelectOutputNodes": "Please select output nodes", "no3dScene": "No 3D scene to apply texture", "failedToApplyTexture": "Failed to apply texture", "no3dSceneToExport": "No 3D scene to export", @@ -1343,5 +1346,11 @@ "title": "Run!" } } + }, + "selectionToolbox": { + "executeButton": { + "tooltip": "Execute to selected output nodes (Highlighted with orange border)", + "disabledTooltip": "No output nodes selected" + } } } \ No newline at end of file diff --git a/src/locales/es/commands.json b/src/locales/es/commands.json index 271755c59..1294be59f 100644 --- a/src/locales/es/commands.json +++ b/src/locales/es/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "Prompt de Cola (Frente)" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "Encolar nodos de salida seleccionados" + }, "Comfy_Redo": { "label": "Rehacer" }, diff --git a/src/locales/es/main.json b/src/locales/es/main.json index 85280b170..eb85678f7 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -695,6 +695,7 @@ "Previous Opened Workflow": "Flujo de trabajo abierto anterior", "Queue Prompt": "Indicador de cola", "Queue Prompt (Front)": "Indicador de cola (Frente)", + "Queue Selected Output Nodes": "Encolar nodos de salida seleccionados", "Quit": "Salir", "Redo": "Rehacer", "Refresh Node Definitions": "Actualizar definiciones de nodo", @@ -801,6 +802,12 @@ }, "title": "Tu dispositivo no es compatible" }, + "selectionToolbox": { + "executeButton": { + "disabledTooltip": "No hay nodos de salida seleccionados", + "tooltip": "Ejecutar en los nodos de salida seleccionados (resaltados con borde naranja)" + } + }, "serverConfig": { "modifiedConfigs": "Has modificado las siguientes configuraciones del servidor. Reinicia para aplicar los cambios.", "restart": "Reiniciar", @@ -1297,8 +1304,10 @@ "noTemplatesToExport": "No hay plantillas para exportar", "nodeDefinitionsUpdated": "Definiciones de nodos actualizadas", "nothingToGroup": "Nada para agrupar", + "nothingToQueue": "Nada para poner en cola", "pendingTasksDeleted": "Tareas pendientes eliminadas", "pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para", + "pleaseSelectOutputNodes": "Por favor, selecciona los nodos de salida", "unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo", "unauthorizedDomain": "Tu dominio {domain} no está autorizado para usar este servicio. Por favor, contacta a {email} para agregar tu dominio a la lista blanca.", "updateRequested": "Actualización solicitada", diff --git a/src/locales/fr/commands.json b/src/locales/fr/commands.json index bd18cd8ac..a38391210 100644 --- a/src/locales/fr/commands.json +++ b/src/locales/fr/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "Invite de file d'attente (avant)" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "Mettre en file d’attente les nœuds de sortie sélectionnés" + }, "Comfy_Redo": { "label": "Refaire" }, diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index bb76e7546..187b15672 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -695,6 +695,7 @@ "Previous Opened Workflow": "Flux de travail ouvert précédent", "Queue Prompt": "Invite de file d'attente", "Queue Prompt (Front)": "Invite de file d'attente (Front)", + "Queue Selected Output Nodes": "Mettre en file d’attente les nœuds de sortie sélectionnés", "Quit": "Quitter", "Redo": "Refaire", "Refresh Node Definitions": "Actualiser les définitions de nœud", @@ -801,6 +802,12 @@ }, "title": "Votre appareil n'est pas pris en charge" }, + "selectionToolbox": { + "executeButton": { + "disabledTooltip": "Aucun nœud de sortie sélectionné", + "tooltip": "Exécuter vers les nœuds de sortie sélectionnés (surlignés avec une bordure orange)" + } + }, "serverConfig": { "modifiedConfigs": "Vous avez modifié les configurations suivantes du serveur. Redémarrez pour appliquer les modifications.", "restart": "Redémarrer", @@ -1297,8 +1304,10 @@ "noTemplatesToExport": "Aucun modèle à exporter", "nodeDefinitionsUpdated": "Définitions de nœuds mises à jour", "nothingToGroup": "Rien à regrouper", + "nothingToQueue": "Rien à ajouter à la file d’attente", "pendingTasksDeleted": "Tâches en attente supprimées", "pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour", + "pleaseSelectOutputNodes": "Veuillez sélectionner les nœuds de sortie", "unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle", "unauthorizedDomain": "Votre domaine {domain} n'est pas autorisé à utiliser ce service. Veuillez contacter {email} pour ajouter votre domaine à la liste blanche.", "updateRequested": "Mise à jour demandée", diff --git a/src/locales/ja/commands.json b/src/locales/ja/commands.json index 22a14488a..75dfd93e3 100644 --- a/src/locales/ja/commands.json +++ b/src/locales/ja/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "キュープロンプト(フロント)" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "選択した出力ノードをキューに追加" + }, "Comfy_Redo": { "label": "やり直す" }, diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index e1405a659..a403909f8 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -695,6 +695,7 @@ "Previous Opened Workflow": "前に開いたワークフロー", "Queue Prompt": "キューのプロンプト", "Queue Prompt (Front)": "キューのプロンプト (前面)", + "Queue Selected Output Nodes": "選択した出力ノードをキューに追加", "Quit": "終了", "Redo": "やり直す", "Refresh Node Definitions": "ノード定義を更新", @@ -801,6 +802,12 @@ }, "title": "お使いのデバイスはサポートされていません" }, + "selectionToolbox": { + "executeButton": { + "disabledTooltip": "出力ノードが選択されていません", + "tooltip": "選択した出力ノードに実行(オレンジ色の枠でハイライト表示)" + } + }, "serverConfig": { "modifiedConfigs": "以下のサーバー設定を変更しました。変更を適用するには再起動してください。", "restart": "再起動", @@ -1297,8 +1304,10 @@ "noTemplatesToExport": "エクスポートするテンプレートがありません", "nodeDefinitionsUpdated": "ノード定義が更新されました", "nothingToGroup": "グループ化するものがありません", + "nothingToQueue": "キューに追加する項目がありません", "pendingTasksDeleted": "保留中のタスクが削除されました", "pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください", + "pleaseSelectOutputNodes": "出力ノードを選択してください", "unableToGetModelFilePath": "モデルファイルのパスを取得できません", "unauthorizedDomain": "あなたのドメイン {domain} はこのサービスを利用する権限がありません。ご利用のドメインをホワイトリストに追加するには、{email} までご連絡ください。", "updateRequested": "更新が要求されました", diff --git a/src/locales/ko/commands.json b/src/locales/ko/commands.json index 001f8a3ac..624f0db1b 100644 --- a/src/locales/ko/commands.json +++ b/src/locales/ko/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "실행 큐 맨 앞에 프롬프트 추가" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "선택한 출력 노드 대기열에 추가" + }, "Comfy_Redo": { "label": "다시 실행" }, diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index fe81ef7af..f68c00c6d 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -695,6 +695,7 @@ "Previous Opened Workflow": "이전 열린 워크플로", "Queue Prompt": "실행 큐에 프롬프트 추가", "Queue Prompt (Front)": "실행 큐 맨 앞에 프롬프트 추가", + "Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가", "Quit": "종료", "Redo": "다시 실행", "Refresh Node Definitions": "노드 정의 새로 고침", @@ -801,6 +802,12 @@ }, "title": "이 장치는 지원되지 않습니다." }, + "selectionToolbox": { + "executeButton": { + "disabledTooltip": "선택된 출력 노드가 없습니다", + "tooltip": "선택한 출력 노드에 실행 (주황색 테두리로 강조 표시됨)" + } + }, "serverConfig": { "modifiedConfigs": "다음 서버 구성을 수정했습니다. 변경 사항을 적용하려면 다시 시작하세오.", "restart": "다시 시작", @@ -1297,8 +1304,10 @@ "noTemplatesToExport": "내보낼 템플릿이 없습니다", "nodeDefinitionsUpdated": "노드 정의가 업데이트되었습니다", "nothingToGroup": "그룹화할 항목이 없습니다", + "nothingToQueue": "대기열에 추가할 항목이 없습니다", "pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다", "pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요", + "pleaseSelectOutputNodes": "출력 노드를 선택해 주세요", "unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다", "unauthorizedDomain": "귀하의 도메인 {domain}은(는) 이 서비스를 사용할 수 있는 권한이 없습니다. 도메인을 허용 목록에 추가하려면 {email}로 문의해 주세요.", "updateRequested": "업데이트 요청됨", diff --git a/src/locales/ru/commands.json b/src/locales/ru/commands.json index 90d95985f..e692760fc 100644 --- a/src/locales/ru/commands.json +++ b/src/locales/ru/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "Очередь запросов (передняя)" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "Добавить выбранные выходные узлы в очередь" + }, "Comfy_Redo": { "label": "Повторить" }, diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index 1d1eac625..065a5ce9a 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -695,6 +695,7 @@ "Previous Opened Workflow": "Предыдущий открытый рабочий процесс", "Queue Prompt": "Запрос в очереди", "Queue Prompt (Front)": "Запрос в очереди (спереди)", + "Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь", "Quit": "Выйти", "Redo": "Повторить", "Refresh Node Definitions": "Обновить определения нод", @@ -801,6 +802,12 @@ }, "title": "Ваше устройство не поддерживается" }, + "selectionToolbox": { + "executeButton": { + "disabledTooltip": "Выходные узлы не выбраны", + "tooltip": "Выполнить для выбранных выходных узлов (выделены оранжевой рамкой)" + } + }, "serverConfig": { "modifiedConfigs": "Вы изменили следующие конфигурации сервера. Перезапустите, чтобы применить изменения.", "restart": "Перезапустить", @@ -1297,8 +1304,10 @@ "noTemplatesToExport": "Нет шаблонов для экспорта", "nodeDefinitionsUpdated": "Определения узлов обновлены", "nothingToGroup": "Нечего группировать", + "nothingToQueue": "Нет заданий в очереди", "pendingTasksDeleted": "Ожидающие задачи удалены", "pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы", + "pleaseSelectOutputNodes": "Пожалуйста, выберите выходные узлы", "unableToGetModelFilePath": "Не удалось получить путь к файлу модели", "unauthorizedDomain": "Ваш домен {domain} не авторизован для использования этого сервиса. Пожалуйста, свяжитесь с {email}, чтобы добавить ваш домен в белый список.", "updateRequested": "Запрошено обновление", diff --git a/src/locales/zh/commands.json b/src/locales/zh/commands.json index afd044bb6..29fef86d1 100644 --- a/src/locales/zh/commands.json +++ b/src/locales/zh/commands.json @@ -155,6 +155,9 @@ "Comfy_QueuePromptFront": { "label": "执行提示词(前端)" }, + "Comfy_QueueSelectedOutputNodes": { + "label": "队列所选输出节点" + }, "Comfy_Redo": { "label": "重做" }, diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index e79455afc..9999ee729 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -695,6 +695,7 @@ "Previous Opened Workflow": "上一个打开的工作流", "Queue Prompt": "执行提示词", "Queue Prompt (Front)": "执行提示词 (优先执行)", + "Queue Selected Output Nodes": "将所选输出节点加入队列", "Quit": "退出", "Redo": "重做", "Refresh Node Definitions": "刷新节点定义", @@ -801,6 +802,12 @@ }, "title": "您的设备不受支持" }, + "selectionToolbox": { + "executeButton": { + "disabledTooltip": "未选择输出节点", + "tooltip": "执行到选定的输出节点(用橙色边框高亮显示)" + } + }, "serverConfig": { "modifiedConfigs": "您已修改以下服务器配置。重启以应用更改。", "restart": "重启", @@ -1297,8 +1304,10 @@ "noTemplatesToExport": "没有模板可以导出", "nodeDefinitionsUpdated": "节点定义已更新", "nothingToGroup": "没有可分组的内容", + "nothingToQueue": "没有可加入队列的内容", "pendingTasksDeleted": "待处理任务已删除", "pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组", + "pleaseSelectOutputNodes": "请选择输出节点", "unableToGetModelFilePath": "无法获取模型文件路径", "unauthorizedDomain": "您的域名 {domain} 未被授权使用此服务。请联系 {email} 将您的域名添加到白名单。", "updateRequested": "已请求更新", diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 38dfd8c48..f588c7892 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -107,7 +107,11 @@ export class ComfyApp { /** * List of entries to queue */ - #queueItems: { number: number; batchCount: number }[] = [] + #queueItems: { + number: number + batchCount: number + queueNodeIds?: NodeId[] + }[] = [] /** * If the queue is currently being processed */ @@ -1144,14 +1148,22 @@ export class ComfyApp { }) } - async graphToPrompt(graph = this.graph) { + async graphToPrompt( + graph = this.graph, + options: { queueNodeIds?: NodeId[] } = {} + ) { return graphToPrompt(graph, { - sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave') + sortNodes: useSettingStore().get('Comfy.Workflow.SortNodeIdOnSave'), + queueNodeIds: options.queueNodeIds }) } - async queuePrompt(number: number, batchCount: number = 1): Promise { - this.#queueItems.push({ number, batchCount }) + async queuePrompt( + number: number, + batchCount: number = 1, + queueNodeIds?: NodeId[] + ): Promise { + this.#queueItems.push({ number, batchCount, queueNodeIds }) // Only have one action process the items so each one gets a unique seed correctly if (this.#processingQueue) { @@ -1167,14 +1179,14 @@ export class ComfyApp { try { while (this.#queueItems.length) { - const { number, batchCount } = this.#queueItems.pop()! + const { number, batchCount, queueNodeIds } = this.#queueItems.pop()! for (let i = 0; i < batchCount; i++) { // Allow widgets to run callbacks before a prompt has been queued // e.g. random seed before every gen executeWidgetsCallback(this.graph.nodes, 'beforeQueued') - const p = await this.graphToPrompt() + const p = await this.graphToPrompt(this.graph, { queueNodeIds }) try { api.authToken = comfyOrgAuthToken const res = await api.queuePrompt(number, p) diff --git a/src/utils/executionUtil.ts b/src/utils/executionUtil.ts index b630be539..eeb56c2dc 100644 --- a/src/utils/executionUtil.ts +++ b/src/utils/executionUtil.ts @@ -1,4 +1,4 @@ -import type { LGraph } from '@comfyorg/litegraph' +import type { LGraph, NodeId } from '@comfyorg/litegraph' import { LGraphEventMode } from '@comfyorg/litegraph' import type { @@ -8,16 +8,46 @@ import type { import { compressWidgetInputSlots } from './litegraphUtil' +/** + * Recursively target node's parent nodes to the new output. + * @param nodeId The node id to add. + * @param oldOutput The old output. + * @param newOutput The new output. + * @returns The new output. + */ +function recursiveAddNodes( + nodeId: NodeId, + oldOutput: ComfyApiWorkflow, + newOutput: ComfyApiWorkflow +) { + const currentId = String(nodeId) + const currentNode = oldOutput[currentId]! + if (newOutput[currentId] == null) { + newOutput[currentId] = currentNode + for (const inputValue of Object.values(currentNode.inputs || [])) { + if (Array.isArray(inputValue)) { + recursiveAddNodes(inputValue[0], oldOutput, newOutput) + } + } + } + return newOutput +} + /** * Converts the current graph workflow for sending to the API. - * Note: Node widgets are updated before serialization to prepare queueing. + * @note Node widgets are updated before serialization to prepare queueing. + * + * @param graph The graph to convert. + * @param options The options for the conversion. + * - `sortNodes`: Whether to sort the nodes by execution order. + * - `queueNodeIds`: The output nodes to execute. Execute all output nodes if not provided. * @returns The workflow and node links */ export const graphToPrompt = async ( graph: LGraph, - options: { sortNodes?: boolean } = {} + options: { sortNodes?: boolean; queueNodeIds?: NodeId[] } = {} ): Promise<{ workflow: ComfyWorkflowJSON; output: ComfyApiWorkflow }> => { - const { sortNodes = false } = options + const { sortNodes = false, queueNodeIds } = options for (const node of graph.computeExecutionOrder(false)) { const innerNodes = node.getInnerNodes ? node.getInnerNodes() : [node] @@ -44,7 +74,7 @@ export const graphToPrompt = async ( workflow.extra ??= {} workflow.extra.frontendVersion = __COMFYUI_FRONTEND_VERSION__ - const output: ComfyApiWorkflow = {} + let output: ComfyApiWorkflow = {} // Process nodes in order of execution for (const outerNode of graph.computeExecutionOrder(false)) { const skipNode = @@ -163,6 +193,15 @@ export const graphToPrompt = async ( } } + // Partial execution + if (queueNodeIds?.length) { + const newOutput = {} + for (const queueNodeId of queueNodeIds) { + recursiveAddNodes(queueNodeId, output, newOutput) + } + output = newOutput + } + // @ts-expect-error Convert ISerializedGraph to ComfyWorkflowJSON return { workflow: workflow as ComfyWorkflowJSON, output } }