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