Spaces:
Running
on
Zero
Running
on
Zero
import { app } from "scripts/app.js"; | |
import { ComfyWidgets } from "scripts/widgets.js"; | |
import type { | |
ContextMenuItem, | |
IContextMenuOptions, | |
ContextMenu, | |
LGraphNode as TLGraphNode, | |
IWidget, | |
LGraphCanvas, | |
SerializedLGraphNode, | |
} from "typings/litegraph.js"; | |
import type { | |
ComfyObjectInfo, | |
ComfyWidget, | |
ComfyNodeConstructor, | |
ComfyApiPrompt, | |
} from "typings/comfy.js"; | |
import { RgthreeBaseServerNode } from "./base_node.js"; | |
import { rgthree } from "./rgthree.js"; | |
import { addConnectionLayoutSupport } from "./utils.js"; | |
import { NodeTypesString } from "./constants.js"; | |
import { SerializedNode } from "typings/index.js"; | |
const LAST_SEED_BUTTON_LABEL = "♻️ (Use Last Queued Seed)"; | |
const SPECIAL_SEED_RANDOM = -1; | |
const SPECIAL_SEED_INCREMENT = -2; | |
const SPECIAL_SEED_DECREMENT = -3; | |
const SPECIAL_SEEDS = [SPECIAL_SEED_RANDOM, SPECIAL_SEED_INCREMENT, SPECIAL_SEED_DECREMENT]; | |
interface SeedSerializedCtx { | |
inputSeed?: number; | |
seedUsed?: number; | |
} | |
class RgthreeSeed extends RgthreeBaseServerNode { | |
static override title = NodeTypesString.SEED; | |
static override type = NodeTypesString.SEED; | |
static comfyClass = NodeTypesString.SEED; | |
override serialize_widgets = true; | |
private logger = rgthree.newLogSession(`[Seed]`); | |
static override exposedActions = ["Randomize Each Time", "Use Last Queued Seed"]; | |
lastSeed?: number = undefined; | |
serializedCtx: SeedSerializedCtx = {}; | |
seedWidget!: IWidget; | |
lastSeedButton!: IWidget; | |
lastSeedValue: ComfyWidget | null = null; | |
randMax = 1125899906842624; | |
// We can have a full range of seeds, including negative. But, for the randomRange we'll | |
// only generate positives, since that's what folks assume. | |
// const min = Math.max(-1125899906842624, this.seedWidget.options.min); | |
randMin = 0; | |
randomRange = 1125899906842624; | |
private handleApiHijackingBound = this.handleApiHijacking.bind(this); | |
constructor(title = RgthreeSeed.title) { | |
super(title); | |
rgthree.addEventListener( | |
"comfy-api-queue-prompt-before", | |
this.handleApiHijackingBound as EventListener, | |
); | |
} | |
override onRemoved() { | |
rgthree.addEventListener( | |
"comfy-api-queue-prompt-before", | |
this.handleApiHijackingBound as EventListener, | |
); | |
} | |
override configure(info: SerializedLGraphNode<TLGraphNode>): void { | |
super.configure(info); | |
if (this.properties?.["showLastSeed"]) { | |
this.addLastSeedValue(); | |
} | |
} | |
override async handleAction(action: string) { | |
if (action === "Randomize Each Time") { | |
this.seedWidget.value = SPECIAL_SEED_RANDOM; | |
} else if (action === "Use Last Queued Seed") { | |
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; | |
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; | |
this.lastSeedButton.disabled = true; | |
} | |
} | |
override onNodeCreated() { | |
super.onNodeCreated?.(); | |
// Grab the already available widgets, and remove the built-in control_after_generate | |
for (const [i, w] of this.widgets.entries()) { | |
if (w.name === "seed") { | |
this.seedWidget = w; // as ComfyWidget; | |
this.seedWidget.value = SPECIAL_SEED_RANDOM; | |
} else if (w.name === "control_after_generate") { | |
this.widgets.splice(i, 1); | |
} | |
} | |
// Update random values in case seed comes down with different options. | |
let step = this.seedWidget.options.step || 1; | |
this.randMax = Math.min(1125899906842624, this.seedWidget.options.max); | |
// We can have a full range of seeds, including negative. But, for the randomRange we'll | |
// only generate positives, since that's what folks assume. | |
this.randMin = Math.max(0, this.seedWidget.options.min); | |
this.randomRange = (this.randMax - Math.max(0, this.randMin)) / (step / 10); | |
this.addWidget( | |
"button", | |
"🎲 Randomize Each Time", | |
null, | |
() => { | |
this.seedWidget.value = SPECIAL_SEED_RANDOM; | |
}, | |
{ serialize: false }, | |
) as ComfyWidget; | |
this.addWidget( | |
"button", | |
"🎲 New Fixed Random", | |
null, | |
() => { | |
this.seedWidget.value = | |
Math.floor(Math.random() * this.randomRange) * (step / 10) + this.randMin; | |
}, | |
{ serialize: false }, | |
); | |
this.lastSeedButton = this.addWidget( | |
"button", | |
LAST_SEED_BUTTON_LABEL, | |
null, | |
() => { | |
this.seedWidget.value = this.lastSeed != null ? this.lastSeed : this.seedWidget.value; | |
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; | |
this.lastSeedButton.disabled = true; | |
}, | |
{ width: 50, serialize: false }, | |
); | |
this.lastSeedButton.disabled = true; | |
} | |
override getExtraMenuOptions(canvas: LGraphCanvas, options: ContextMenuItem[]): void { | |
super.getExtraMenuOptions?.apply(this, [...arguments] as any); | |
options.splice(options.length - 1, 0, { | |
content: "Show/Hide Last Seed Value", | |
callback: ( | |
_value: ContextMenuItem, | |
_options: IContextMenuOptions, | |
_event: MouseEvent, | |
_parentMenu: ContextMenu | undefined, | |
_node: TLGraphNode, | |
) => { | |
this.properties["showLastSeed"] = !this.properties["showLastSeed"]; | |
if (this.properties["showLastSeed"]) { | |
this.addLastSeedValue(); | |
} else { | |
this.removeLastSeedValue(); | |
} | |
}, | |
}); | |
} | |
addLastSeedValue() { | |
if (this.lastSeedValue) return; | |
this.lastSeedValue = ComfyWidgets["STRING"]( | |
this, | |
"last_seed", | |
["STRING", { multiline: true }], | |
app, | |
).widget; | |
this.lastSeedValue!.inputEl!.readOnly = true; | |
this.lastSeedValue!.inputEl!.style.fontSize = "0.75rem"; | |
this.lastSeedValue!.inputEl!.style.textAlign = "center"; | |
this.computeSize(); | |
} | |
removeLastSeedValue() { | |
if (!this.lastSeedValue) return; | |
this.lastSeedValue!.inputEl!.remove(); | |
this.widgets.splice(this.widgets.indexOf(this.lastSeedValue as IWidget), 1); | |
this.lastSeedValue = null; | |
this.computeSize(); | |
} | |
/** | |
* Intercepts the prompt right before ComfyUI sends it to the server (as fired from rgthree) so we | |
* can inspect the prompt and workflow data and change swap in the seeds. | |
* | |
* Note, the original implementation tried to change the widget value itself when the graph was | |
* queued (and the relied on ComfyUI serializing the data changed data) and then changing it back. | |
* This worked well until other extensions kept calling graphToPrompt during asynchronous | |
* operations within, causing the widget to get confused without a reliable state to reflect upon. | |
*/ | |
handleApiHijacking(e: CustomEvent<ComfyApiPrompt>) { | |
// Don't do any work if we're muted/bypassed. | |
if (this.mode === LiteGraph.NEVER || this.mode === 4) { | |
return; | |
} | |
const workflow = e.detail.workflow; | |
const output = e.detail.output; | |
let workflowNode = workflow?.nodes?.find((n: SerializedNode) => n.id === this.id) ?? null; | |
let outputInputs = output?.[this.id]?.inputs; | |
if ( | |
!workflowNode || | |
!outputInputs || | |
outputInputs[this.seedWidget.name || "seed"] === undefined | |
) { | |
const [n, v] = this.logger.warnParts( | |
`Node ${this.id} not found in prompt data sent to server. This may be fine if only ` + | |
`queuing part of the workflow. If not, then this could be a bug.`, | |
); | |
console[n]?.(...v); | |
return; | |
} | |
const seedToUse = this.getSeedToUse(); | |
const seedWidgetndex = this.widgets.indexOf(this.seedWidget); | |
workflowNode.widgets_values![seedWidgetndex] = seedToUse; | |
outputInputs[this.seedWidget.name || "seed"] = seedToUse; | |
this.lastSeed = seedToUse; | |
if (seedToUse != this.seedWidget.value) { | |
this.lastSeedButton.name = `♻️ ${this.lastSeed}`; | |
this.lastSeedButton.disabled = false; | |
} else { | |
this.lastSeedButton.name = LAST_SEED_BUTTON_LABEL; | |
this.lastSeedButton.disabled = true; | |
} | |
if (this.lastSeedValue) { | |
this.lastSeedValue.value = `Last Seed: ${this.lastSeed}`; | |
} | |
} | |
/** | |
* Determines a seed to use depending on the seed widget's current value and the last used seed. | |
* There are no sideffects to calling this method. | |
*/ | |
private getSeedToUse() { | |
const inputSeed: number = this.seedWidget.value; | |
let seedToUse: number | null = null; | |
// If our input seed was a special seed, then handle it. | |
if (SPECIAL_SEEDS.includes(inputSeed)) { | |
// If the last seed was not a special seed and we have increment/decrement, then do that on | |
// the last seed. | |
if (typeof this.lastSeed === "number" && !SPECIAL_SEEDS.includes(this.lastSeed)) { | |
if (inputSeed === SPECIAL_SEED_INCREMENT) { | |
seedToUse = this.lastSeed + 1; | |
} else if (inputSeed === SPECIAL_SEED_DECREMENT) { | |
seedToUse = this.lastSeed - 1; | |
} | |
} | |
// If we don't have a seed to use, or it's special seed (like we incremented into one), then | |
// we randomize. | |
if (seedToUse == null || SPECIAL_SEEDS.includes(seedToUse)) { | |
seedToUse = | |
Math.floor(Math.random() * this.randomRange) * | |
((this.seedWidget.options.step || 1) / 10) + | |
this.randMin; | |
} | |
} | |
return seedToUse ?? inputSeed; | |
} | |
static override setUp(comfyClass: ComfyNodeConstructor, nodeData: ComfyObjectInfo) { | |
RgthreeBaseServerNode.registerForOverride(comfyClass, nodeData, RgthreeSeed); | |
} | |
static override onRegisteredForOverride(comfyClass: any, ctxClass: any) { | |
addConnectionLayoutSupport(RgthreeSeed, app, [ | |
["Left", "Right"], | |
["Right", "Left"], | |
]); | |
setTimeout(() => { | |
RgthreeSeed.category = comfyClass.category; | |
}); | |
} | |
} | |
app.registerExtension({ | |
name: "rgthree.Seed", | |
async beforeRegisterNodeDef(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo) { | |
if (nodeData.name === RgthreeSeed.type) { | |
RgthreeSeed.setUp(nodeType, nodeData); | |
} | |
}, | |
}); | |