commit d110477e9de26dcea8fcc5585fd2f69ae3add4ad Author: るみなー先生 Date: Tue May 12 21:25:19 2026 +0900 新規作成 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e5c55c --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# ComfyUI-PromptRandomChoice + +実行のたびに「プロンプトガチャ」を爆速で回す、超軽量なランダム選択ノードじゃ! + +「背景や天気を適当に変えて、一晩中回しておきたいのう……」 +そんなおぬしの願いを、実行時間 0.010s(注:作者環境で速いとき)という瞬速の魔法で叶えて進ぜよう! + +背景・時間帯・天気などを別々にランダム化したい時に便利じゃ。 +フロー内に複数個置いても、それぞれ独立して動くぞ。 + + +## 特徴 + +- **実行ごとにランダム選択** + - キューを雑に100件積んでも、毎回候補を選び直すぞ。 + +- **区切りは `|` または実際の改行** + - `|` と改行の両方を区切りとして扱うので、混在していても動くぞ。 + +- **候補の前後の `,` と空白を自動で整理** + - `town,` + - `coffee shop,` + - `castle, fortress,` + のような書き方でも大丈夫じゃ。 + +- **プロンプト結合向けの返り値** + - 選ばれた候補の前後に `,` を付けて返すぞ。 + - 例: `coffee shop` → `,coffee shop,` + +- **現在の選択結果をタイトル表示** + - 実行時にノードタイトルが `Choice: coffee shop` のように更新されるぞ。 + - KSamplerのプレビューと見比べやすいのじゃ。 + +- **複数設置に対応** + - 背景用、時間帯用、天気用など、複数ノードを置いてそれぞれ別々に使えるぞ。 + +## 導入方法 + +ComfyUIの `custom_nodes` ディレクトリで、以下のコマンドを打ち込むのじゃ! + +```bash +git clone https://github.com/ruminar/ComfyUI-PromptRandomChoice.git +``` + +## 使い方 + +1. `Prompt Random Choice` ノードを置く +2. `options_text` に候補を入れる +3. `prompt_text` を文字列結合ノードなどでポジティブプロンプトへ足す +4. キューを好きなだけ積む + +## 入力例 + +### 改行区切り (改行前の`,`は任意です) + +```text +town, +girl's room, +park, +lake, +flower garden, +castle, fortress, +forest, +grasslands, +sea, +snowy landscape, +mountain, +flower field, +starry sky, +coffee shop, +``` + +### `|` 区切り + +```text +town| +girl's room| +park| +lake| +flower garden| +castle, fortress| +forest| +grasslands| +sea| +snowy landscape| +mountain| +flower field| +starry sky| +coffee shop| +``` + +### 選ばれやすさを調整したい場合 + +同じ候補を複数回書けば、そのぶん選ばれやすくなります。 + +```text +day|day|day|sunset|night +``` + +昼を多めに出したい、たまに夕方や夜も混ぜたい、という時に便利じゃ。 + +## 仕様 + +- `|` または実際の改行で分割 +- `\n` という文字列は区切りとして扱わない +- 各候補の前後の空白と `,` を trim +- 空候補は無視 +- 候補が空なら空文字 `""` を返す +- 候補があれば `,selected,` を返す + +## 出力 + +- `prompt_text` + - 例: `,coffee shop,` + +## ライセンス + +GPL-3.0(ComfyUI本体の掟に従っておるぞ!) + +## 宣伝画像 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..fc7085e --- /dev/null +++ b/__init__.py @@ -0,0 +1,9 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +WEB_DIRECTORY = "./web" + +__all__ = [ + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", + "WEB_DIRECTORY", +] diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..38c873e --- /dev/null +++ b/nodes.py @@ -0,0 +1,96 @@ +import random +import re +import secrets +import time + + +DEFAULT_OPTIONS_TEXT = """town, +girl's room, +park, +lake, +flower garden, +castle, fortress, +forest, +grasslands, +sea, +snowy landscape, +mountain, +flower field, +starry sky, +coffee shop,""" + + +def _split_options(options_text: str): + text = str(options_text or "") + + # Split by "|" or actual newlines only. + # Do not treat literal "\\n" text as a delimiter. + parts = re.split(r"\||\r?\n", text) + + options = [] + for part in parts: + item = str(part).strip(" \t\r\n,") + if item: + options.append(item) + + return options + + +class PromptRandomChoice: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "options_text": ( + "STRING", + { + "multiline": True, + "default": DEFAULT_OPTIONS_TEXT, + }, + ), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("prompt_text",) + FUNCTION = "pick" + CATEGORY = "utils/prompt" + + @classmethod + def IS_CHANGED(cls, **kwargs): + # Force execution for every queued run so the random choice changes. + return time.time_ns() + + def pick(self, options_text): + options = _split_options(options_text) + + if not options: + return { + "ui": { + "selected_text": [""], + }, + "result": ("",), + } + + # Do not use random.choice here. + # Some ComfyUI workflows/extensions may reset Python's global random state + # from the generation seed, which can make random.choice repeat the same + # index every queued run. secrets.choice uses OS entropy instead. + selected = secrets.choice(options) + prompt_text = f",{selected}," + + return { + "ui": { + "selected_text": [selected], + }, + "result": (prompt_text,), + } + + +NODE_CLASS_MAPPINGS = { + "PromptRandomChoice": PromptRandomChoice, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PromptRandomChoice": "Prompt Random Choice", +} diff --git a/web/prompt_random_choice.js b/web/prompt_random_choice.js new file mode 100644 index 0000000..b54adfa --- /dev/null +++ b/web/prompt_random_choice.js @@ -0,0 +1,38 @@ +import { app } from "../../scripts/app.js"; + +const EXTENSION_NAME = "ruminar.PromptRandomChoice"; +const NODE_NAME = "PromptRandomChoice"; + +function shorten(text, maxLength = 40) { + const value = String(text ?? ""); + if (value.length <= maxLength) { + return value; + } + return value.slice(0, maxLength - 3) + "..."; +} + +app.registerExtension({ + name: EXTENSION_NAME, + + beforeRegisterNodeDef(nodeType, nodeData) { + if (String(nodeData?.name ?? "") !== NODE_NAME) { + return; + } + + const originalOnExecuted = nodeType.prototype.onExecuted; + + nodeType.prototype.onExecuted = function (message) { + originalOnExecuted?.apply(this, arguments); + + const selected = message?.selected_text?.[0] ?? ""; + if (selected) { + this.title = `Choice: ${shorten(selected)}`; + } else { + this.title = "Prompt Random Choice"; + } + + this.setDirtyCanvas?.(true, true); + app.graph?.setDirtyCanvas?.(true, true); + }; + }, +});