Skip to content

Commit a108c52

Browse files
custom_node provided blueprints (#6172)
Frontend implementation for a system to allow custom_nodes to provide a set of subgraph blueprints. Requires comfyanonymous/ComfyUI#10438, but handles gracefully in unavailable. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6172-Early-POC-custom_node-provided-blueprints-2926d73d3650814982ecd43f12abd873) by [Unito](https://www.unito.io) --------- Co-authored-by: Christian Byrne <[email protected]>
1 parent 8ed9be2 commit a108c52

File tree

4 files changed

+77
-23
lines changed

4 files changed

+77
-23
lines changed

src/platform/workflow/management/stores/workflowStore.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ export class ComfyWorkflow extends UserFile {
8383
* @param force Whether to force loading the content even if it is already loaded.
8484
* @returns this
8585
*/
86-
override async load({
87-
force = false
88-
}: { force?: boolean } = {}): Promise<LoadedComfyWorkflow> {
86+
override async load({ force = false }: { force?: boolean } = {}): Promise<
87+
this & LoadedComfyWorkflow
88+
> {
8989
await super.load({ force })
90-
if (!force && this.isLoaded) return this as LoadedComfyWorkflow
90+
if (!force && this.isLoaded) return this as this & LoadedComfyWorkflow
9191

9292
if (!this.originalContent) {
9393
throw new Error('[ASSERT] Workflow content should be loaded')
@@ -100,7 +100,7 @@ export class ComfyWorkflow extends UserFile {
100100
/* initialState= */ JSON.parse(this.originalContent)
101101
)
102102
)
103-
return this as LoadedComfyWorkflow
103+
return this as this & LoadedComfyWorkflow
104104
}
105105

106106
override unload(): void {

src/scripts/api.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ type SimpleApiEvents = keyof PickNevers<ApiEventTypes>
204204
/** Keys (names) of API events that pass a {@link CustomEvent} `detail` object. */
205205
type ComplexApiEvents = keyof NeverNever<ApiEventTypes>
206206

207+
export type GlobalSubgraphData = {
208+
name: string
209+
info: { node_pack: string }
210+
data: string | Promise<string>
211+
}
212+
207213
function addHeaderEntry(headers: HeadersInit, key: string, value: string) {
208214
if (Array.isArray(headers)) {
209215
headers.push([key, value])
@@ -1118,6 +1124,22 @@ export class ComfyApi extends EventTarget {
11181124
return resp.json()
11191125
}
11201126

1127+
async getGlobalSubgraphData(id: string): Promise<string> {
1128+
const resp = await api.fetchApi('/global_subgraphs/' + id)
1129+
if (resp.status !== 200) return ''
1130+
const subgraph: GlobalSubgraphData = await resp.json()
1131+
return subgraph?.data ?? ''
1132+
}
1133+
async getGlobalSubgraphs(): Promise<Record<string, GlobalSubgraphData>> {
1134+
const resp = await api.fetchApi('/global_subgraphs')
1135+
if (resp.status !== 200) return {}
1136+
const subgraphs: Record<string, GlobalSubgraphData> = await resp.json()
1137+
for (const [k, v] of Object.entries(subgraphs)) {
1138+
if (!v.data) v.data = this.getGlobalSubgraphData(k)
1139+
}
1140+
return subgraphs
1141+
}
1142+
11211143
async getLogs(): Promise<string> {
11221144
return (await axios.get(this.internalURL('/logs'))).data
11231145
}

src/scripts/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,8 @@ export class ComfyApp {
775775
this.canvasElRef.value = canvasEl
776776

777777
await useWorkspaceStore().workflow.syncWorkflows()
778-
await useSubgraphStore().fetchSubgraphs()
778+
//Doesn't need to block. Blueprints will load async
779+
void useSubgraphStore().fetchSubgraphs()
779780
await useExtensionService().loadExtensions()
780781

781782
this.addProcessKeyHandler()

src/stores/subgraphStore.ts

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
InputSpec
2424
} from '@/schemas/nodeDefSchema'
2525
import { api } from '@/scripts/api'
26+
import type { GlobalSubgraphData } from '@/scripts/api'
2627
import { useDialogService } from '@/services/dialogService'
2728
import { useExecutionStore } from '@/stores/executionStore'
2829
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
@@ -106,9 +107,9 @@ export const useSubgraphStore = defineStore('subgraph', () => {
106107
useSubgraphStore().updateDef(await this.load())
107108
return ret
108109
}
109-
override async load({
110-
force = false
111-
}: { force?: boolean } = {}): Promise<LoadedComfyWorkflow> {
110+
override async load({ force = false }: { force?: boolean } = {}): Promise<
111+
this & LoadedComfyWorkflow
112+
> {
112113
if (!force && this.isLoaded) return await super.load({ force })
113114
const loaded = await super.load({ force })
114115
const st = loaded.activeState
@@ -147,20 +148,46 @@ export const useSubgraphStore = defineStore('subgraph', () => {
147148
modified: number
148149
size: number
149150
}): Promise<void> {
150-
const name = options.path.slice(0, -'.json'.length)
151151
options.path = SubgraphBlueprint.basePath + options.path
152152
const bp = await new SubgraphBlueprint(options, true).load()
153153
useWorkflowStore().attachWorkflow(bp)
154-
const nodeDef = convertToNodeDef(bp)
155-
156-
subgraphDefCache.value.set(name, nodeDef)
157-
subgraphCache[name] = bp
154+
registerNodeDef(bp)
155+
}
156+
async function loadInstalledBlueprints() {
157+
async function loadGlobalBlueprint([k, v]: [string, GlobalSubgraphData]) {
158+
const path = SubgraphBlueprint.basePath + v.name + '.json'
159+
const blueprint = new SubgraphBlueprint({
160+
path,
161+
modified: Date.now(),
162+
size: -1
163+
})
164+
blueprint.originalContent = blueprint.content = await v.data
165+
blueprint.filename = v.name
166+
useWorkflowStore().attachWorkflow(blueprint)
167+
const loaded = await blueprint.load()
168+
registerNodeDef(
169+
loaded,
170+
{
171+
python_module: v.info.node_pack,
172+
display_name: v.name
173+
},
174+
k
175+
)
176+
}
177+
const subgraphs = await api.getGlobalSubgraphs()
178+
await Promise.allSettled(
179+
Object.entries(subgraphs).map(loadGlobalBlueprint)
180+
)
158181
}
159182

160-
const res = (
183+
const userSubs = (
161184
await api.listUserDataFullInfo(SubgraphBlueprint.basePath)
162185
).filter((f) => f.path.endsWith('.json'))
163-
const settled = await Promise.allSettled(res.map(loadBlueprint))
186+
const settled = await Promise.allSettled([
187+
...userSubs.map(loadBlueprint),
188+
loadInstalledBlueprints()
189+
])
190+
164191
const errors = settled.filter((i) => 'reason' in i).map((i) => i.reason)
165192
errors.forEach((e) => console.error('Failed to load subgraph blueprint', e))
166193
if (errors.length > 0) {
@@ -172,8 +199,11 @@ export const useSubgraphStore = defineStore('subgraph', () => {
172199
})
173200
}
174201
}
175-
function convertToNodeDef(workflow: LoadedComfyWorkflow): ComfyNodeDefImpl {
176-
const name = workflow.filename
202+
function registerNodeDef(
203+
workflow: LoadedComfyWorkflow,
204+
overrides: Partial<ComfyNodeDefV1> = {},
205+
name: string = workflow.filename
206+
) {
177207
const subgraphNode = workflow.changeTracker.initialState.nodes[0]
178208
if (!subgraphNode) throw new Error('Invalid Subgraph Blueprint')
179209
subgraphNode.inputs ??= []
@@ -197,10 +227,12 @@ export const useSubgraphStore = defineStore('subgraph', () => {
197227
description,
198228
category: 'Subgraph Blueprints',
199229
output_node: false,
200-
python_module: 'blueprint'
230+
python_module: 'blueprint',
231+
...overrides
201232
}
202233
const nodeDefImpl = new ComfyNodeDefImpl(nodedefv1)
203-
return nodeDefImpl
234+
subgraphDefCache.value.set(name, nodeDefImpl)
235+
subgraphCache[name] = workflow
204236
}
205237
async function publishSubgraph() {
206238
const canvas = canvasStore.getCanvas()
@@ -252,8 +284,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
252284
await workflow.save()
253285
//add to files list?
254286
useWorkflowStore().attachWorkflow(loadedWorkflow)
255-
subgraphDefCache.value.set(name, convertToNodeDef(loadedWorkflow))
256-
subgraphCache[name] = loadedWorkflow
287+
registerNodeDef(loadedWorkflow)
257288
useToastStore().add({
258289
severity: 'success',
259290
summary: t('subgraphStore.publishSuccess'),
@@ -262,7 +293,7 @@ export const useSubgraphStore = defineStore('subgraph', () => {
262293
})
263294
}
264295
function updateDef(blueprint: LoadedComfyWorkflow) {
265-
subgraphDefCache.value.set(blueprint.filename, convertToNodeDef(blueprint))
296+
registerNodeDef(blueprint)
266297
}
267298
async function editBlueprint(nodeType: string) {
268299
const name = nodeType.slice(typePrefix.length)

0 commit comments

Comments
 (0)