Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
ComfyUI
Commits
4ef4cf91
Commit
4ef4cf91
authored
Mar 03, 2023
by
pythongosssss
Browse files
Adding built in extensions + example
parent
2e52d01c
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
243 additions
and
57 deletions
+243
-57
nodes.py
nodes.py
+1
-1
web/extensions/core/dynamicPrompts.js
web/extensions/core/dynamicPrompts.js
+40
-0
web/extensions/core/rerouteNode.js
web/extensions/core/rerouteNode.js
+92
-0
web/extensions/logging.js.example
web/extensions/logging.js.example
+54
-0
web/index.html
web/index.html
+2
-0
web/scripts/app.js
web/scripts/app.js
+51
-40
web/scripts/extensions.js
web/scripts/extensions.js
+0
-9
web/scripts/widgets.js
web/scripts/widgets.js
+3
-7
No files found.
nodes.py
View file @
4ef4cf91
...
...
@@ -51,7 +51,7 @@ def interrupt_processing(value=True):
class
CLIPTextEncode
:
@
classmethod
def
INPUT_TYPES
(
s
):
return
{
"required"
:
{
"text"
:
(
"STRING"
,
{
"multiline"
:
True
,
"dynamic_prompt"
:
True
}),
"clip"
:
(
"CLIP"
,
)}}
return
{
"required"
:
{
"text"
:
(
"STRING"
,
{
"multiline"
:
True
}),
"clip"
:
(
"CLIP"
,
)}}
RETURN_TYPES
=
(
"CONDITIONING"
,)
FUNCTION
=
"encode"
...
...
web/extensions/core/dynamicPrompts.js
0 → 100644
View file @
4ef4cf91
import
{
app
}
from
"
../../scripts/app.js
"
;
// Allows for simple dynamic prompt replacement
// Inputs in the format {a|b} will have a random value of a or b chosen when the prompt is queued.
app
.
registerExtension
({
name
:
"
Comfy.DynamicPrompts
"
,
nodeCreated
(
node
)
{
// TODO: Change this to replace the value and restore it after posting
if
(
node
.
widgets
)
{
// Locate dynamic prompt text widgets
// Include any widgets with dynamicPrompts set to true, and customtext
const
widgets
=
node
.
widgets
.
filter
(
(
n
)
=>
(
n
.
type
===
"
customtext
"
&&
n
.
dynamicPrompts
!==
false
)
||
n
.
dynamicPrompts
);
for
(
const
widget
of
widgets
)
{
// Override the serialization of the value to resolve dynamic prompts for all widgets supporting it in this node
widget
.
serializeValue
=
()
=>
{
let
prompt
=
widget
.
value
;
while
(
prompt
.
replace
(
"
\\
{
"
,
""
).
includes
(
"
{
"
)
&&
prompt
.
replace
(
"
\\
}
"
,
""
).
includes
(
"
}
"
))
{
const
startIndex
=
prompt
.
replace
(
"
\\
{
"
,
"
00
"
).
indexOf
(
"
{
"
);
const
endIndex
=
prompt
.
replace
(
"
\\
}
"
,
"
00
"
).
indexOf
(
"
}
"
);
const
optionsString
=
prompt
.
substring
(
startIndex
+
1
,
endIndex
);
const
options
=
optionsString
.
split
(
"
|
"
);
const
randomIndex
=
Math
.
floor
(
Math
.
random
()
*
options
.
length
);
const
randomOption
=
options
[
randomIndex
];
prompt
=
prompt
.
substring
(
0
,
startIndex
)
+
randomOption
+
prompt
.
substring
(
endIndex
+
1
);
}
return
prompt
;
};
}
}
},
});
web/extensions/core/rerouteNode.js
0 → 100644
View file @
4ef4cf91
import
{
app
}
from
"
../../scripts/app.js
"
;
// Node that allows you to redirect connections for cleaner graphs
app
.
registerExtension
({
name
:
"
Comfy.RerouteNode
"
,
registerCustomNodes
()
{
class
RerouteNode
{
constructor
()
{
if
(
!
this
.
properties
)
{
this
.
properties
=
{};
}
this
.
properties
.
showOutputText
=
RerouteNode
.
defaultVisibility
;
this
.
addInput
(
""
,
"
*
"
);
this
.
addOutput
(
this
.
properties
.
showOutputText
?
"
*
"
:
""
,
"
*
"
);
this
.
onConnectInput
=
function
(
_
,
type
)
{
if
(
type
!==
this
.
outputs
[
0
].
type
)
{
this
.
removeOutput
(
0
);
this
.
addOutput
(
this
.
properties
.
showOutputText
?
type
:
""
,
type
);
this
.
size
=
this
.
computeSize
();
}
};
this
.
clone
=
function
()
{
const
cloned
=
RerouteNode
.
prototype
.
clone
.
apply
(
this
);
cloned
.
removeOutput
(
0
);
cloned
.
addOutput
(
this
.
properties
.
showOutputText
?
"
*
"
:
""
,
"
*
"
);
cloned
.
size
=
cloned
.
computeSize
();
return
cloned
;
};
// This node is purely frontend and does not impact the resulting prompt so should not be serialized
this
.
isVirtualNode
=
true
;
}
getExtraMenuOptions
(
_
,
options
)
{
options
.
unshift
(
{
content
:
(
this
.
properties
.
showOutputText
?
"
Hide
"
:
"
Show
"
)
+
"
Type
"
,
callback
:
()
=>
{
this
.
properties
.
showOutputText
=
!
this
.
properties
.
showOutputText
;
if
(
this
.
properties
.
showOutputText
)
{
this
.
outputs
[
0
].
name
=
this
.
outputs
[
0
].
type
;
}
else
{
this
.
outputs
[
0
].
name
=
""
;
}
this
.
size
=
this
.
computeSize
();
},
},
{
content
:
(
RerouteNode
.
defaultVisibility
?
"
Hide
"
:
"
Show
"
)
+
"
Type By Default
"
,
callback
:
()
=>
{
RerouteNode
.
setDefaultTextVisibility
(
!
RerouteNode
.
defaultVisibility
);
},
}
);
}
computeSize
()
{
return
[
this
.
properties
.
showOutputText
&&
this
.
outputs
&&
this
.
outputs
.
length
?
Math
.
max
(
55
,
LiteGraph
.
NODE_TEXT_SIZE
*
this
.
outputs
[
0
].
name
.
length
*
0.6
+
40
)
:
55
,
26
,
];
}
static
setDefaultTextVisibility
(
visible
)
{
RerouteNode
.
defaultVisibility
=
visible
;
if
(
visible
)
{
localStorage
[
"
Comfy.RerouteNode.DefaultVisibility
"
]
=
"
true
"
;
}
else
{
delete
localStorage
[
"
Comfy.RerouteNode.DefaultVisibility
"
];
}
}
}
// Load default visibility
RerouteNode
.
setDefaultTextVisibility
(
!!
localStorage
[
"
Comfy.RerouteNode.DefaultVisibility
"
]);
LiteGraph
.
registerNodeType
(
"
Reroute
"
,
Object
.
assign
(
RerouteNode
,
{
title_mode
:
LiteGraph
.
NO_TITLE
,
title
:
"
Reroute
"
,
})
);
RerouteNode
.
category
=
"
utils
"
;
},
});
web/extensions/logging.js.example
0 → 100644
View file @
4ef4cf91
import { app } from "../scripts/app.js";
const ext = {
name: "Example.LoggingExtension",
async init(app) {
// Any initial setup to run as soon as the page loads
console.log("[logging]", "extension init");
},
async setup(app) {
// Any setup to run after the app is created
console.log("[logging]", "extension setup");
},
async addCustomNodeDefs(defs, app) {
// Add custom node definitions
// These definitions will be configured and registered automatically
// defs is a lookup core nodes, add yours into this
console.log("[logging]", "add custom node definitions", "current nodes:", Object.keys(defs));
},
async getCustomWidgets(app) {
// Return custom widget types
// See ComfyWidgets for widget examples
console.log("[logging]", "provide custom widgets");
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Run custom logic before a node definition is registered with the graph
console.log("[logging]", "before register node: ", nodeType, nodeData);
// This fires for every node definition so only log once
delete ext.beforeRegisterNodeDef;
},
async registerCustomNodes(app) {
// Register any custom node implementations here allowing for more flexability than a custom node def
console.log("[logging]", "register custom nodes");
},
loadedGraphNode(node, app) {
// Fires for each node when loading/dragging/etc a workflow json or png
// If you break something in the backend and want to patch workflows in the frontend
// This is the place to do this
console.log("[logging]", "loaded graph node: ", node);
// This fires for every node on each load so only log once
delete ext.loadedGraphNode;
},
nodeCreated(node, app) {
// Fires every time a node is constructed
// You can modify widgets/add handlers/etc here
console.log("[logging]", "node created: ", node);
// This fires for every node so only log once
delete ext.nodeCreated;
}
};
app.registerExtension(ext);
web/index.html
View file @
4ef4cf91
...
...
@@ -6,6 +6,8 @@
<script
type=
"text/javascript"
src=
"lib/litegraph.core.js"
></script>
<script
type=
"module"
>
import
"
/extensions/core/dynamicPrompts.js
"
;
import
{
app
}
from
"
/scripts/app.js
"
;
await
app
.
setup
();
window
.
app
=
app
;
...
...
web/scripts/app.js
View file @
4ef4cf91
...
...
@@ -7,34 +7,8 @@ import { getPngMetadata } from "./pnginfo.js";
class
ComfyApp
{
constructor
()
{
this
.
ui
=
new
ComfyUI
(
this
);
this
.
extensions
=
[];
this
.
nodeOutputs
=
{};
this
.
extensions
=
[
{
name
:
"
TestExtension
"
,
init
(
app
)
{
console
.
log
(
"
[ext:init]
"
,
app
);
},
setup
(
app
)
{
console
.
log
(
"
[ext:setup]
"
,
app
);
},
addCustomNodeDefs
(
defs
,
app
)
{
console
.
log
(
"
[ext:addCustomNodeDefs]
"
,
defs
,
app
);
},
loadedGraphNode
(
node
,
app
)
{
// console.log("[ext:loadedGraphNode]", node, app);
},
getCustomWidgets
(
app
)
{
console
.
log
(
"
[ext:getCustomWidgets]
"
,
app
);
return
{};
},
beforeRegisterNode
(
nodeType
,
nodeData
,
app
)
{
// console.log("[ext:beforeRegisterNode]", nodeType, nodeData, app);
},
registerCustomNodes
(
app
)
{
console
.
log
(
"
[ext:registerCustomNodes]
"
,
app
);
},
},
];
}
#
log
(
message
,
...
other
)
{
...
...
@@ -49,7 +23,7 @@ class ComfyApp {
* Invoke an extension callback
* @param {string} method The extension callback to execute
* @param {...any} args Any arguments to pass to the callback
* @returns
* @returns
*/
#
invokeExtensions
(
method
,
...
args
)
{
let
results
=
[];
...
...
@@ -75,7 +49,7 @@ class ComfyApp {
* Each callback will be invoked concurrently
* @param {string} method The extension callback to execute
* @param {...any} args Any arguments to pass to the callback
* @returns
* @returns
*/
async
#
invokeExtensionsAsync
(
method
,
...
args
)
{
return
await
Promise
.
all
(
...
...
@@ -540,6 +514,8 @@ class ComfyApp {
s
[
1
]
=
Math
.
max
(
config
.
minHeight
,
s
[
1
]);
this
.
size
=
s
;
this
.
serialize_widgets
=
true
;
app
.
#
invokeExtensionsAsync
(
"
nodeCreated
"
,
this
);
},
{
title
:
nodeData
.
name
,
...
...
@@ -551,7 +527,7 @@ class ComfyApp {
this
.
#
addNodeContextMenuHandler
(
node
);
this
.
#
addDrawBackgroundHandler
(
node
,
app
);
await
this
.
#
invokeExtensionsAsync
(
"
beforeRegisterNode
"
,
node
,
nodeData
);
await
this
.
#
invokeExtensionsAsync
(
"
beforeRegisterNode
Def
"
,
node
,
nodeData
);
LiteGraph
.
registerNodeType
(
nodeId
,
node
);
node
.
category
=
nodeData
.
category
;
}
...
...
@@ -598,28 +574,55 @@ class ComfyApp {
* @returns The workflow and node links
*/
graphToPrompt
()
{
// TODO: Implement dynamic prompts
const
workflow
=
this
.
graph
.
serialize
();
const
output
=
{};
for
(
const
n
of
workflow
.
nodes
)
{
const
inputs
=
{};
const
node
=
this
.
graph
.
getNodeById
(
n
.
id
);
if
(
node
.
isVirtualNode
)
{
// Don't serialize frontend only nodes
continue
;
}
const
inputs
=
{};
const
widgets
=
node
.
widgets
;
// Store all widget values
if
(
widgets
)
{
for
(
const
widget
of
widgets
)
{
if
(
widget
.
options
.
serialize
!==
false
)
{
inputs
[
widget
.
name
]
=
widget
.
value
;
if
(
!
widget
.
options
||
widget
.
options
.
serialize
!==
false
)
{
inputs
[
widget
.
name
]
=
widget
.
serializeValue
?
widget
.
serializeValue
()
:
widget
.
value
;
}
}
}
// Store all node links
for
(
let
i
in
node
.
inputs
)
{
const
link
=
node
.
getInputLink
(
i
);
if
(
link
)
{
inputs
[
node
.
inputs
[
i
].
name
]
=
[
String
(
link
.
origin_id
),
parseInt
(
link
.
origin_slot
)];
let
parent
=
node
.
getInputNode
(
i
);
if
(
parent
)
{
let
link
;
if
(
parent
.
isVirtualNode
)
{
// Follow the path of virtual nodes until we reach the first real one
while
(
parent
!=
null
)
{
link
=
parent
.
getInputLink
(
0
);
if
(
link
)
{
const
from
=
graph
.
getNodeById
(
link
.
origin_id
);
if
(
from
.
isVirtualNode
)
{
parent
=
from
;
}
else
{
parent
=
null
;
}
}
else
{
parent
=
null
;
}
}
}
else
{
link
=
node
.
getInputLink
(
i
);
}
if
(
link
)
{
inputs
[
node
.
inputs
[
i
].
name
]
=
[
String
(
link
.
origin_id
),
parseInt
(
link
.
origin_slot
)];
}
}
}
...
...
@@ -655,15 +658,13 @@ class ComfyApp {
}
}
// TODO: check dynamic prompts here
this
.
canvas
.
draw
(
true
,
true
);
await
this
.
ui
.
queue
.
update
();
}
/**
* Loads workflow data from the specified file
* @param {File} file
* @param {File} file
*/
async
handleFile
(
file
)
{
if
(
file
.
type
===
"
image/png
"
)
{
...
...
@@ -679,6 +680,16 @@ class ComfyApp {
reader
.
readAsText
(
file
);
}
}
registerExtension
(
extension
)
{
if
(
!
extension
.
name
)
{
throw
new
Error
(
"
Extensions must have a 'name' property.
"
);
}
if
(
this
.
extensions
.
find
((
ext
)
=>
ext
.
name
===
extension
.
name
))
{
throw
new
Error
(
`Extension named '
${
extension
.
name
}
' already registered.`
);
}
this
.
extensions
.
push
(
extension
);
}
}
export
const
app
=
new
ComfyApp
();
web/scripts/extensions.js
deleted
100644 → 0
View file @
2e52d01c
export
class
ComfyExtension
{
init
(
app
)
{}
setup
(
app
)
{}
loadedGraphNode
(
node
,
app
)
{}
addCustomNodeDefs
(
defs
,
app
)
{}
getCustomWidgets
(
app
)
{}
beforeRegisterNode
(
nodeType
,
nodeData
,
app
)
{}
registerCustomNodes
(
app
)
{}
}
\ No newline at end of file
web/scripts/widgets.js
View file @
4ef4cf91
...
...
@@ -27,7 +27,7 @@ function seedWidget(node, inputName, inputData) {
return
{
widget
:
seed
,
randomize
};
}
function
addMultilineWidget
(
node
,
name
,
defaultVal
,
dynamicPrompt
,
app
)
{
function
addMultilineWidget
(
node
,
name
,
defaultVal
,
app
)
{
const
widget
=
{
type
:
"
customtext
"
,
name
,
...
...
@@ -37,9 +37,6 @@ function addMultilineWidget(node, name, defaultVal, dynamicPrompt, app) {
set
value
(
x
)
{
this
.
inputEl
.
value
=
x
;
},
options
:
{
dynamicPrompt
,
},
draw
:
function
(
ctx
,
_
,
widgetWidth
,
y
,
widgetHeight
)
{
const
visible
=
app
.
canvas
.
ds
.
scale
>
0.5
;
const
t
=
ctx
.
getTransform
();
...
...
@@ -106,12 +103,11 @@ export const ComfyWidgets = {
STRING
(
node
,
inputName
,
inputData
,
app
)
{
const
defaultVal
=
inputData
[
1
].
default
||
""
;
const
multiline
=
!!
inputData
[
1
].
multiline
;
const
dynamicPrompt
=
!!
inputData
[
1
].
dynamic_prompt
;
if
(
multiline
)
{
return
addMultilineWidget
(
node
,
inputName
,
defaultVal
,
dynamicPrompt
,
app
);
return
addMultilineWidget
(
node
,
inputName
,
defaultVal
,
app
);
}
else
{
return
{
widget
:
node
.
addWidget
(
"
text
"
,
inputName
,
defaultVal
,
()
=>
{},
{
dynamicPrompt
})
};
return
{
widget
:
node
.
addWidget
(
"
text
"
,
inputName
,
defaultVal
,
()
=>
{},
{})
};
}
},
};
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment