diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..95784f0 --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,35 @@ +# v0.2.0 + +change_everyを追加したのじゃ! + +`ComfyUI-PromptRandomChoice` は、プロンプト候補リストから実行のたびにランダムで1項目を選び、ポジティブプロンプトなどへ追加しやすい形で出力する ComfyUI カスタムノードです。 + +## 主な機能 + +- プロンプト候補から実行ごとにランダムで1項目を選択 +- 候補は `|` または実際の改行で区切り +- 候補の前後にある空白や `,` を自動で trim +- 選ばれた候補を `,coffee shop,` のように前後カンマ付きで出力 +- `change_every` に対応 + - `1` なら毎回選び直し + - `3` なら同じ候補を3回返してから次を選択 +- 実行中の選択結果をノードタイトルに表示 + - 例: `Choice: coffee shop` + - 例: `Choice: lake (2/3)` +- 複数ノード設置に対応 + - 背景用、時間帯用、天気用などを別々にランダム化可能 +- 同じ候補を複数回書くことで、選ばれやすさを調整可能 + - 例: `day|day|day|sunset|night` + +## 注意 + +区切り文字は **実際の改行** または **`|`** です。 + +`,` は区切り文字ではありません。 +ただし、各候補の前後に付いている `,` は自動で取り除きます。 + +詳しい使い方は README.md を参照してください。 + +## ライセンス + +GPL-3.0 \ No newline at end of file diff --git a/nodes.py b/nodes.py index 38c873e..7bcb734 100644 --- a/nodes.py +++ b/nodes.py @@ -1,4 +1,3 @@ -import random import re import secrets import time @@ -22,9 +21,6 @@ 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 = [] @@ -37,6 +33,12 @@ def _split_options(options_text: str): class PromptRandomChoice: + def __init__(self): + self._current_choice = None + self._repeat_index = 0 + self._last_options_key = None + self._last_change_every = None + @classmethod def INPUT_TYPES(cls): return { @@ -48,6 +50,15 @@ class PromptRandomChoice: "default": DEFAULT_OPTIONS_TEXT, }, ), + "change_every": ( + "INT", + { + "default": 1, + "min": 1, + "max": 999999, + "step": 1, + }, + ), }, } @@ -58,30 +69,58 @@ class PromptRandomChoice: @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): + def _reset_state(self): + self._current_choice = None + self._repeat_index = 0 + self._last_options_key = None + self._last_change_every = None + + def _select_new_choice(self, options): + self._current_choice = secrets.choice(options) + self._repeat_index = 1 + + def pick(self, options_text, change_every): options = _split_options(options_text) + change_every = max(1, int(change_every)) + options_key = tuple(options) if not options: + self._reset_state() return { "ui": { "selected_text": [""], + "repeat_index": [0], + "change_every": [change_every], }, "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) + state_changed = ( + self._last_options_key != options_key + or self._last_change_every != change_every + or self._current_choice not in options + ) + + if state_changed or self._current_choice is None: + self._select_new_choice(options) + elif self._repeat_index < change_every: + self._repeat_index += 1 + else: + self._select_new_choice(options) + + self._last_options_key = options_key + self._last_change_every = change_every + + selected = self._current_choice prompt_text = f",{selected}," return { "ui": { "selected_text": [selected], + "repeat_index": [self._repeat_index], + "change_every": [change_every], }, "result": (prompt_text,), } diff --git a/web/prompt_random_choice.js b/web/prompt_random_choice.js index b54adfa..2723619 100644 --- a/web/prompt_random_choice.js +++ b/web/prompt_random_choice.js @@ -25,10 +25,15 @@ app.registerExtension({ originalOnExecuted?.apply(this, arguments); const selected = message?.selected_text?.[0] ?? ""; - if (selected) { + const repeatIndex = Number(message?.repeat_index?.[0] ?? 0); + const changeEvery = Number(message?.change_every?.[0] ?? 1); + + if (!selected) { + this.title = "Prompt Random Choice"; + } else if (changeEvery <= 1) { this.title = `Choice: ${shorten(selected)}`; } else { - this.title = "Prompt Random Choice"; + this.title = `Choice: ${shorten(selected)} (${repeatIndex}/${changeEvery})`; } this.setDirtyCanvas?.(true, true);