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): 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) { // 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); } }, });