Merge branch 'develop'

This commit is contained in:
るみなー先生
2026-05-21 21:12:01 +09:00
5 changed files with 1092 additions and 101 deletions
+22
View File
@@ -0,0 +1,22 @@
# --- Python関連 ---
# これがJavaの.classに近いものじゃ。実行時に自動生成される。
__pycache__/
*.py[cod]
*$py.class
# 仮想環境(もしプロジェクト内に作っておるなら)
venv/
.venv/
env/
# --- エディタ・OS関連 ---
# VS Codeの設定ファイル(個人設定が含まれることがあるので除外)
.vscode/
# macOSが勝手に作るゴミ
.DS_Store
# Windowsのサムネイルキャッシュ
Thumbs.db
# --- ComfyUI/カスタムノード特有 ---
# もし一時ファイルやログを書き出す仕組みを作ったなら、それも追加じゃ
*.log
+688 -15
View File
@@ -9,6 +9,22 @@
フロー内に複数個置いても、それぞれ独立して動くぞ。
## v0.4.0
`Prompt Random Choice Ex` を追加したのじゃ。
- `Prompt Random Choice`
- フラットな候補リストから1つ選ぶノード
- `Prompt Random Choice Ex`
- フラット候補に加えて、`{}` による入れ子の候補展開に対応したノード
- 先にすべての葉候補へ展開し、最後に1回だけランダム選択します
- 展開済み候補リストは `options_text` が変わるまでキャッシュします
どちらもノードとしての出力は同じじゃ。
- `selected_text`
- `selected_text_safe`
## 特徴
- **実行ごとにランダム選択**
@@ -38,6 +54,34 @@
- **明示的な空候補 `()` に対応**
- `()` が選ばれた場合、プロンプト向け出力は空文字に、ファイル名向け出力は `empty` になるのじゃ。
## Prompt Random Choice Ex の追加要素
- **フラット候補に加えて、`{}` による入れ子の候補展開に対応**
- しかも、ComfyUIの標準記法と異なり、`{}` 内を入れ子にできるのじゃ。
- `{}` 内の要素は内側から解釈され、選択項目がカンマで前後に接続される。
- `|` と改行の両方を区切りとして扱うので、混在していても動くぞ。
- 区切り文字が連続して出現した場合は、候補から取り除かれることに注意じゃ。
- 明示的に空白文字列を候補として返却させたい場合は、明示的に `()` と指定するのじゃ。
ここは実例を見てもらった方が話が早そうじゃ。
```text
town|zoo{animals{birds|penguins}|aquarium,{fish|jellyfish}}
```
これは、内部的には次の候補へ展開され、キャッシュされるのじゃ。
```text
town
zoo, animals, birds
zoo, animals, penguins
zoo, aquarium, fish
zoo, aquarium, jellyfish
```
この5候補から1つが選ばれるのじゃ。<br/>
展開後の最終候補が、すべて等しい確率で選ばれるようにしてるのじゃな。
<br/>
## 導入方法
ComfyUIの `custom_nodes` ディレクトリで、以下のコマンドを打ち込むのじゃ!
@@ -58,7 +102,7 @@ git clone https://github.com/ruminar/ComfyUI-PromptRandomChoice.git
<img width="544" height="526" alt="image" src="https://github.com/user-attachments/assets/d230659e-f008-4232-955d-1fa6fdf299fa" /><br/><br/>
<img width="594" height="555" alt="image" src="https://github.com/user-attachments/assets/7966c50e-15c7-41cf-b167-06a54054acec" /><br/>
※ 設定後は折りたたんで使うのもおすすめです
※ 設定後は折りたたんで使うのもおすすめなのじゃ。
## 入力例
@@ -119,6 +163,8 @@ day|day|day|sunset|night
昼を多めに出したい、たまに夕方や夜も混ぜたい、という時に便利じゃ。
<br/>
## 仕様
- `|` または実際の改行で分割
@@ -130,6 +176,83 @@ day|day|day|sunset|night
- `change_every` が 2 以上なら、その回数ぶん同じ候補を維持する
- 実行時にタイトルへ `Choice: lake` や `Choice: (empty) (2/3)` のように表示する
## Prompt Random Choice
候補リストから1つ選びます。
```text
town|park|lake|coffee shop
```
`()` は明示的な空候補です。
```text
()|(full body:0.9)
```
## Prompt Random Choice Ex
`Prompt Random Choice` と同じフラットな候補リストをそのまま使えます。
```text
town|park|lake|coffee shop
```
さらに、候補の中に `{}` を書くことで、選ばれた候補にだけ追加候補をぶら下げられます。
```text
town|zoo{animals{birds|penguins}|aquarium,{fish|jellyfish}}
```
この入力は、内部的には次の候補へ展開されます。
```text
town
zoo, animals, birds
zoo, animals, penguins
zoo, aquarium, fish
zoo, aquarium, jellyfish
```
この5候補から1つが選ばれます。
### 複数行の Ex 例
```text
zoo{
animals{
birds
penguins
}
aquarium{
fish
jellyfish
}
}
```
出力例:
```text
zoo, animals, birds
zoo, animals, penguins
zoo, aquarium, fish
zoo, aquarium, jellyfish
```
### Ex のルール
- 選択候補の区切り文字は、実際の改行 または `|`
- 空候補は無視
- `()` は明示的な空候補
- `{}` の内部も、実際の改行 または `|` で候補分割
- `{}` は最内側からすべての葉候補へ展開
- 展開結果は親要素へ `, ` で接続
- 最終候補リストから最後に1回だけランダム選択
- 展開済み候補リストは `options_text` が変わるまでキャッシュ
- 展開候補数と展開回数には安全上限があります
- リテラルの `{` / `}` をプロンプト文字として使う用途は非対応
## 出力
- `selected_text`
@@ -140,6 +263,14 @@ day|day|day|sunset|night
ファイル名向けに安全化した出力です。
`selected_text` が空なら `empty` を返します。
## 推奨構成
`selected_text` を kjnodes の `Join String Multi` などへ接続し、区切り文字は結合ノード側で管理するのがおすすめじゃ。
複数の `Prompt Random Choice` / `Prompt Random Choice Ex` を並べることで、背景・時間帯・天気・構図などを別々にランダム化するのじゃ。
ただし、Ex はすべての要素を1つにまとめるためのノードではなく、`zoo` の時だけ動物候補を追加するような、親子関係のある候補を扱うためのノードとして使うのがおすすめじゃ。
<br/>
## 標準選択記法 `{day|night|morning}` との違いについて
@@ -176,92 +307,634 @@ GPL-3.0(ComfyUI本体の掟に従っておるぞ!)
<img width="1055" height="1491" alt="PromptRandomChoice説明画像" src="https://github.com/user-attachments/assets/7a4f1b5f-c77b-4e47-90af-cbd0330c85fe" />
EX版
<img width="1122" height="1402" alt="PromptRandomChoiceEx説明画像" src="https://github.com/user-attachments/assets/f45a44b7-5692-4d98-854a-7736677e1f5a" />
## 付録
## コピペ用おすすめ候補リスト
背景候補
### 背景
```text
Indoor,
girl's room,
bedroom,
living room,
kitchen,
dining room,
bathroom,
attic,
basement,
coffee shop,
cafe terrace,
library,
private library,
classroom,
music room,
science room,
computer room,
school infirmary,
gymnasium,
office,
meeting room,
conference room,
laboratory,
medical room,
art studio,
art gallery,
museum,
aquarium,
planetarium,
observatory interior,
bookstore,
bakery,
flower shop,
convenience store,
supermarket,
restaurant,
bar,
diner,
karaoke room,
arcade,
game center,
cinema,
concert hall,
theater,
dance studio,
school hallway,
locker room,
stairwell,
elevator hall,
greenhouse,
observatory,
train interior,
subway interior,
airport terminal,
shopping mall,
hotel room,
lobby,
chapel interior,
Outdoor,
City,
city,
town,
downtown,
residential area,
park,
playground,
plaza,
rooftop,
balcony,
terrace,
train station,
bus stop,
airport runway,
shopping street,
courtyard,
bridge,
crosswalk,
intersection,
riverside,
canal,
harbor,
port,
boardwalk,
marketplace,
festival street,
food stall area,
alley,
back alley,
village,
suburban street,
schoolyard,
campus,
parking lot,
construction site,
amusement park,
theme park,
zoo,
stadium exterior,
cemetery,
clock tower,
lighthouse,
windmill,
waterfront,
Nature,
lake,
pond,
waterfall,
river,
stream,
flower garden,
rose garden,
forest,
bamboo forest,
pine forest,
rainforest,
grasslands,
meadow,
savanna,
sea,
ocean,
coral reef,
mountain,
mountain path,
hilltop,
valley,
cliff,
flower field,
sunflower field,
lavender field,
beach,
shore,
island,
tropical island,
cave,
crystal cave,
botanical garden,
jungle,
swamp,
marsh,
desert,
oasis,
snowfield,
glacier,
ice cave,
volcanic area,
hot spring,
starry sky,
night sky,
aurora,
Traditional,
Fantasy-ish,
Japanese garden,
shrine,
temple,
tea house,
tatami room,
engawa,
onsen,
ryokan,
festival grounds,
torii gate,
bamboo grove path,
castle,
fortress,
palace,
ruins,
old town,
stone pavement,
pagoda,
dojo,
samurai residence,
courtyard garden,
```
時刻
### 時刻
```text
day|day|day|morning|sunset|night
()|day|day|day|morning|sunset|night
```
天候
### 天候
```text
()|(clear sky:0.9)|(clear sky:0.9)|(clear sky:0.9)|(cloudy sky:0.9)|rain|snow
()|Strong sunshine|(clear sky:0.9)|(clear sky:0.9)|(clear sky:0.9)|(cloudy sky:0.9)|rain|snow|Rainbow after Rain|storm, thunder
```
光
### 光
```text
()|soft lighting|warm lighting|natural lighting|(backlighting:0.8)|(dramatic lighting:0.8)|(cinematic lighting:0.8)
```
姿勢、視線、動作
### 姿勢、視線、動作
```text
()|standing|sitting|walking|looking at viewer|waving|hands on hips|jumping|running|skipping|looking up
()|standing|sitting|walking|looking at viewer|waving|hands on hips|jumping high|running|skipping|looking up
```
表情
### 表情
```text
()|smiling|gentle smile|serious expression|surprised expression|slightly surprised|shy expression|happy expression|smiling, open mouth|slightly open mouth|closed-mouth smile
```
構図
### 構図
```text
()|(face close-up:0.9)|upper body|upper body|full body|full body|full body|full body|full body|wide shot|(from side:0.8)|(from above:0.8)|(low angle:0.8)|(from behind, looking back:0.8)
```
### 背景 Ex 版
背景は、項目ごとにまとめてあるから、全部くっつけて1つの背景ノードにするのも、<br/>
それぞれ別ノードにして組み合わせるのも、おぬしの好きな方を選べるようにしたぞ!<br/>
同じ項目を繰り返したり、不要な項目を削除したりして、おぬし好みのプロンプトに育てておくれなのじゃ。
#### Ex 標準背景
```text
indoor{
girl's room,
bedroom,
living room,
kitchen,
dining room,
bathroom,
attic,
basement,
coffee shop,
cafe terrace,
library,
private library,
classroom,
music room,
science room,
computer room,
school infirmary,
gymnasium,
office,
meeting room,
conference room,
laboratory,
medical room,
art studio,
art gallery,
museum,
aquarium,
planetarium,
observatory interior,
bookstore,
bakery,
flower shop,
convenience store,
supermarket,
restaurant,
bar,
diner,
karaoke room,
arcade,
game center,
cinema,
concert hall,
theater,
dance studio,
school hallway,
locker room,
stairwell,
elevator hall,
greenhouse,
train interior,
subway interior,
airport terminal,
shopping mall,
hotel room,
lobby,
chapel interior,
}
Outdoor{
city,
town,
downtown,
residential area,
park,
playground,
plaza,
rooftop,
balcony,
terrace,
train station,
bus stop,
airport runway,
shopping street,
courtyard,
bridge,
crosswalk,
intersection,
riverside,
canal,
harbor,
port,
boardwalk,
marketplace,
festival street,
food stall area,
alley,
back alley,
village,
suburban street,
schoolyard,
campus,
parking lot,
construction site,
amusement park,
theme park,
zoo,
stadium exterior,
cemetery,
clock tower,
lighthouse,
windmill,
waterfront,
}
Nature{
lake,
pond,
waterfall,
river,
stream,
flower garden,
rose garden,
forest,
bamboo forest,
pine forest,
rainforest,
grasslands,
meadow,
savanna,
sea,
ocean,
coral reef,
mountain,
mountain path,
hilltop,
valley,
cliff,
flower field,
sunflower field,
lavender field,
beach,
shore,
island,
tropical island,
cave,
crystal cave,
botanical garden,
jungle,
swamp,
marsh,
desert,
oasis,
snowfield,
glacier,
ice cave,
volcanic area,
hot spring,
starry sky,
night sky,
aurora,
}
traditional{
Japanese garden,
shrine,
temple,
tea house,
tatami room,
engawa,
onsen,
ryokan,
festival grounds,
torii gate,
bamboo grove path,
castle,
fortress,
palace,
ruins,
old town,
stone pavement,
pagoda,
dojo,
samurai residence,
courtyard garden,
}
```
#### Ex ファンタジー特盛背景
```text
Fantasy{
(),
magic library,
alchemy workshop,
wizard tower,
enchanted forest,
fairy garden,
floating island,
sky castle,
crystal palace,
ancient ruins,
mystic cave,
dragon's lair,
underground city,
sacred spring,
giant tree,
mirror lake,
celestial garden,
forgotten temple,
phantom town,
clockwork city,
throne room,
dungeon,
cathedral,
portal site,
magic academy,
sorcerer's tower,
witch's cottage,
fairy village,
elven forest,
dwarf mine,
crystal cave,
ancient altar,
holy sanctuary,
forbidden library,
sky temple,
floating garden,
moonlit lake,
starlight forest,
enchanted castle,
royal palace,
hidden village,
ancient labyrinth,
monster arena,
summoning chamber,
}
Japanese-style Fantasy{
(),
moonlit shrine,
mystic shrine,
ancient shrine,
forgotten shrine,
mountain shrine,
forest shrine,
torii gate,
spirit forest,
youkai village,
oni castle,
kitsune shrine,
tanuki forest,
sacred mountain,
hidden onsen,
samurai castle,
ninja village,
abandoned temple,
bamboo spirit path,
misty bamboo forest,
sakura spirit realm,
red torii path,
shrine festival night,
haunted Japanese mansion,
old samurai residence,
floating lantern river,
dragon god shrine,
celestial fox shrine,
underworld gate,
}
Chinese-style Fantasy{
(),
ancient Chinese palace,
imperial palace,
jade palace,
celestial palace,
xianxia sect,
martial arts sect,
mountain cultivation temple,
immortal mountain,
cloud sea,
bamboo mountain path,
lotus pond,
moon gate garden,
Chinese courtyard,
ancient Chinese city,
lantern street,
water town,
stone bridge town,
dragon palace,
phoenix palace,
taoist temple,
misty peak,
sword cultivation arena,
heavenly staircase,
jade pavilion,
floating pagoda,
immortal cave,
spirit spring,
celestial river,
}
Fantasy-ish{
(),
gothic castle,
vampire mansion,
haunted mansion,
dark cathedral,
graveyard,
crypt,
necromancer's lair,
witch market,
night carnival,
dream world,
mirror world,
toy kingdom,
candy kingdom,
steampunk city,
airship dock,
mechanical tower,
clock tower interior,
abandoned laboratory,
magical observatory,
starship temple,
ancient machine room,
lost civilization,
desert ruins,
sunken city,
underwater palace,
ice palace,
volcanic fortress,
shadow realm,
celestial battlefield,
}
```
#### Ex 季節/イベント背景
```text
Spring{
(),
cherry blossoms,
sakura avenue,
hanami,
spring festival,
graduation ceremony,
entrance ceremony,
easter,
flower viewing picnic,
rainy season,
hydrangea garden,
children's day,
doll festival,
easter egg hunt,
}
Summer{
(),
summer festival,
festival night,
food stalls,
lantern festival,
bon festival,
fireworks,
fireworks festival,
poolside,
water park,
beach party,
tropical vacation,
campground,
tanabata,
tanabata festival,
star festival,
}
Autumn{
(),
autumn leaves,
maple forest,
harvest festival,
moon viewing,
halloween,
halloween party,
halloween street,
pumpkin patch,
haunted house,
masquerade party,
autumn festival,
thanksgiving,
}
Winter{
(),
snowy town,
snow festival,
ice skating rink,
christmas,
christmas market,
christmas tree,
christmas party,
illuminations,
winter holiday,
new year,
new year's shrine visit,
first sunrise,
snowy shrine,
winter illuminations,
new year's festival,
new year's eve party,
winter market,
christmas dinner,
holiday shopping street,
valentine's day,
}
(){
white day,
wedding ceremony,
birthday party,
anniversary,
school festival,
cultural festival,
sports festival,
idol concert,
live event,
tea party,
garden party,
picnic,
parade,
carnival,
temple fair,
}
```
+46 -32
View File
@@ -1,42 +1,56 @@
## v0.3.0
## v0.4.0
- 出力を整理
- `selected_text` : プロンプト向け出力
- `selected_text_safe` : ファイル名向け出力
- 明示的な空候補 `()` に対応
- `()` が選ばれた場合、プロンプト向け出力は空文字になり、ファイル名向け出力は `empty` になります
`Prompt Random Choice Ex` を追加しました。
`ComfyUI-PromptRandomChoice` は、プロンプト候補リストから実行のたびにランダムで1項目を選び、ポジティブプロンプトなどへ追加しやすい形で出力する ComfyUI カスタムノードです。
## 追加
## 主な機能
- `Prompt Random Choice Ex`
- `Prompt Random Choice` と同じフラット候補リストに対応
- `{}` による入れ子の候補展開に対応
- すべての葉候補へ展開してから、最後に1回だけランダム選択
- 展開結果を親要素へ `, ` で接続
- 展開済み候補リストを `options_text` 単位でキャッシュ
- `selected_text` / `selected_text_safe` を出力
- `(){...}` のような空親グループに対応
- 親要素 `()` は出力されません
- 子要素だけが候補になります
- プロンプト候補から実行ごとにランダムで1項目を選択
- 候補は `|` または実際の改行で区切り
- 候補の前後にある空白や `,` を自動で trim
- 選ばれた候補を `selected_text` として通常の文字列で出力
- ファイル名向けに安全化した `selected_text_safe` を出力
- `change_every` に対応
- `1` なら毎回選び直し
- `3` なら同じ候補を3回返してから次を選択
- 実行中の選択結果をノードタイトルに表示
- 例: `Choice: coffee shop`
- 例: `Choice: lake (2/3)`
- 複数ノード設置に対応
- 背景用、時間帯用、天気用などを別々にランダム化可能
- 同じ候補を複数回書くことで、選ばれやすさを調整可能
- 例: `day|day|day|sunset|night`
- 明示的な空候補 `()` に対応
- `()` が選ばれた場合、プロンプト向け出力は空文字になり、ファイル名向け出力は `empty` になります
## 例
## 注意
```text
town|zoo{animals{birds|penguins}|aquarium,{fish|jellyfish}}
```
これは、内部的には次の候補へ展開されます。
区切り文字は **実際の改行** または **`|`** です。
```text
town
zoo, animals, birds
zoo, animals, penguins
zoo, aquarium, fish
zoo, aquarium, jellyfish
```
この5候補から1つが選ばれます。
<br/>
`,` は区切り文字ではありません。
ただし、各候補の前後に付いている `,` は自動で取り除きます。
詳しい使い方は README.md を参照してください。
親要素 `()` の例
```text
(){
white day
wedding ceremony
birthday party
}
```
## ライセンス
内部的には次の候補へ展開されます。
GPL-3.0
```text
white day
wedding ceremony
birthday party
## 備考
- `Prompt Random Choice Ex` は、すべての葉候補へ展開してから最後に1回だけランダム選択します
- 展開済み候補リストは `options_text` 単位でキャッシュします
- 通常の `Prompt Random Choice` は、フラットな候補選択用としてそのまま残しています
+326 -50
View File
@@ -18,10 +18,56 @@ flower field,
starry sky,
coffee shop,"""
DEFAULT_OPTIONS_TEXT_EX = """town
zoo{animals{birds|penguins}|aquarium,{fish|jellyfish}}
coffee shop{cake|coffee cup}
amusement park{ferris wheel|carousel|balloons}
(){
white day
wedding ceremony
birthday party
}"""
MAX_EXPAND_STEPS = 64
MAX_EXPANDED_OPTIONS = 4096
def _split_options(options_text: str):
text = str(options_text or "")
parts = re.split(r"\||\r?\n", text)
"""
Split candidates by "|" or actual newline only at brace depth 0.
Empty candidates are ignored.
"()" is treated as an explicit empty candidate and becomes "".
This function is shared by PromptRandomChoice and PromptRandomChoiceEx.
For Ex, nested blocks such as zoo{animals|birds} stay together when
splitting the top-level candidate list.
"""
text = str(options_text or "").replace("\r\n", "\n").replace("\r", "\n")
parts = []
current = []
depth = 0
for char in text:
if char == "{":
depth += 1
current.append(char)
continue
if char == "}":
depth = max(0, depth - 1)
current.append(char)
continue
if depth == 0 and char in ("|", "\n"):
parts.append("".join(current))
current = []
continue
current.append(char)
parts.append("".join(current))
options = []
for part in parts:
@@ -42,6 +88,43 @@ def _split_options(options_text: str):
return options
def _normalize_prompt_fragment(text: str):
"""
Normalize comma-connected prompt fragments after nested expansion.
Standalone "()" fragments are removed here, which enables:
(){white day|wedding ceremony}
to become:
white day
wedding ceremony
This also keeps the syntax forgiving:
zoo{aquarium,{fish|jellyfish}}
becomes:
zoo, aquarium, fish
"""
value = str(text or "")
# Normalize line breaks/tabs to spaces.
value = re.sub(r"[\t\r\n]+", " ", value)
# Normalize spaces around commas.
value = re.sub(r"\s*,\s*", ",", value)
# Split comma-connected fragments, remove empty fragments and explicit
# empty marker fragments, then join them in a prompt-friendly form.
parts = []
for part in value.split(","):
item = part.strip()
if not item:
continue
if item == "()":
continue
parts.append(item)
return ", ".join(parts)
def _safe_text(value: str):
text = str(value or "")
@@ -52,19 +135,184 @@ def _safe_text(value: str):
text = re.sub(r'[<>:"|?*\x00-\x1f]', "_", text)
text = re.sub(r"[\s,]+", "_", text)
text = text.replace("(", "_").replace(")", "_")
text = text.replace("{", "_").replace("}", "_")
text = re.sub(r"_+", "_", text)
text = text.strip().strip("._-")
return text or "empty"
class PromptRandomChoice:
def _find_first_block(text: str):
"""
Return (start, end) for the first balanced {...} block.
If braces are unmatched, return None and leave the text as literal.
"""
value = str(text or "")
start = value.find("{")
if start < 0:
return None
depth = 0
for index in range(start, len(value)):
char = value[index]
if char == "{":
depth += 1
elif char == "}":
depth -= 1
if depth == 0:
return start, index
return None
def _expand_all_nested_choices(option_text: str):
"""
Expand one top-level candidate into all final leaf candidates.
Ex uses leaf-uniform probability:
1. expand all nested {...} branches first
2. choose one item from the expanded leaf list
Example:
town|zoo{animals{birds|penguins}|aquarium,{fish|jellyfish}}
becomes:
town
zoo, animals, birds
zoo, animals, penguins
zoo, aquarium, fish
zoo, aquarium, jellyfish
Empty parent groups are supported:
(){white day|wedding ceremony}
becomes:
white day
wedding ceremony
"""
def expand_recursive(value: str, depth: int):
if depth > MAX_EXPAND_STEPS:
raise ValueError(
"PromptRandomChoiceEx expansion exceeded the maximum expansion steps. "
"Please check nested braces."
)
block = _find_first_block(value)
if block is None:
return [_normalize_prompt_fragment(value)]
start, end = block
prefix = value[:start]
body = value[start + 1:end]
suffix = value[end + 1:]
inner_options = _split_options(body)
if not inner_options:
inner_options = [""]
results = []
for inner in inner_options:
inner_expanded_options = expand_recursive(inner, depth + 1)
for inner_expanded in inner_expanded_options:
replacement = f", {inner_expanded}" if inner_expanded else ""
candidate = prefix + replacement + suffix
expanded_candidates = expand_recursive(candidate, depth + 1)
for expanded in expanded_candidates:
results.append(expanded)
if len(results) > MAX_EXPANDED_OPTIONS:
raise ValueError(
"PromptRandomChoiceEx expanded too many options "
f"({len(results)} > {MAX_EXPANDED_OPTIONS})."
)
return results
initial = str(option_text or "").strip(" \t\r\n,")
return expand_recursive(initial, 0)
def _build_expanded_options(options_text: str):
top_options = _split_options(options_text)
expanded_options = []
for option in top_options:
for expanded in _expand_all_nested_choices(option):
# Keep empty results because they can come from explicit ().
expanded_options.append(expanded)
if len(expanded_options) > MAX_EXPANDED_OPTIONS:
raise ValueError(
"PromptRandomChoiceEx expanded too many options "
f"({len(expanded_options)} > {MAX_EXPANDED_OPTIONS})."
)
return tuple(expanded_options)
class _RandomChoiceStateMixin:
def __init__(self):
self._current_choice = None
self._repeat_index = 0
self._last_options_key = None
self._last_change_every = None
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time_ns()
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 _run_choice(self, options, change_every):
change_every = max(1, int(change_every))
options = tuple(options)
options_key = options
if not options:
self._reset_state()
return "", "empty", "(empty)", 0, change_every
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_text = str(self._current_choice or "")
selected_text_safe = _safe_text(selected_text)
selected_text_title = selected_text if selected_text else "(empty)"
return (
selected_text,
selected_text_safe,
selected_text_title,
self._repeat_index,
change_every,
)
class PromptRandomChoice(_RandomChoiceStateMixin):
@classmethod
def INPUT_TYPES(cls):
return {
@@ -93,60 +341,86 @@ class PromptRandomChoice:
FUNCTION = "pick"
CATEGORY = "utils/prompt"
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time_ns()
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_title": ["(empty)"],
"repeat_index": [0],
"change_every": [change_every],
},
"result": ("", "empty"),
}
state_changed = (
self._last_options_key != options_key
or self._last_change_every != change_every
or self._current_choice not in options
selected_text, selected_text_safe, selected_text_title, repeat_index, change_every = self._run_choice(
options,
change_every,
)
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_text = str(self._current_choice or "")
selected_text_safe = _safe_text(selected_text)
selected_text_title = selected_text if selected_text else "(empty)"
return {
"ui": {
"selected_text_title": [selected_text_title],
"repeat_index": [self._repeat_index],
"repeat_index": [repeat_index],
"change_every": [change_every],
},
"result": (selected_text, selected_text_safe),
}
class PromptRandomChoiceEx(_RandomChoiceStateMixin):
def __init__(self):
super().__init__()
self._expanded_options_cache_key = None
self._expanded_options_cache = tuple()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"options_text": (
"STRING",
{
"multiline": True,
"default": DEFAULT_OPTIONS_TEXT_EX,
},
),
"change_every": (
"INT",
{
"default": 1,
"min": 1,
"max": 999999,
"step": 1,
},
),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("selected_text", "selected_text_safe")
FUNCTION = "pick"
CATEGORY = "utils/prompt"
def _reset_state(self):
super()._reset_state()
# Keep the expansion cache; it depends only on options_text.
# Selection state and expansion cache have different lifecycles.
def _get_expanded_options_cached(self, options_text):
cache_key = str(options_text or "")
if self._expanded_options_cache_key == cache_key:
return self._expanded_options_cache
expanded_options = _build_expanded_options(cache_key)
self._expanded_options_cache_key = cache_key
self._expanded_options_cache = expanded_options
return expanded_options
def pick(self, options_text, change_every):
options = self._get_expanded_options_cached(options_text)
selected_text, selected_text_safe, selected_text_title, repeat_index, change_every = self._run_choice(
options,
change_every,
)
return {
"ui": {
"selected_text_title": [selected_text_title],
"repeat_index": [repeat_index],
"change_every": [change_every],
},
"result": (selected_text, selected_text_safe),
@@ -155,8 +429,10 @@ class PromptRandomChoice:
NODE_CLASS_MAPPINGS = {
"PromptRandomChoice": PromptRandomChoice,
"PromptRandomChoiceEx": PromptRandomChoiceEx,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptRandomChoice": "Prompt Random Choice",
"PromptRandomChoiceEx": "Prompt Random Choice Ex",
}
+10 -4
View File
@@ -1,7 +1,10 @@
import { app } from "../../scripts/app.js";
const EXTENSION_NAME = "ruminar.PromptRandomChoice";
const NODE_NAME = "PromptRandomChoice";
const NODE_NAMES = new Set([
"PromptRandomChoice",
"PromptRandomChoiceEx",
]);
function shorten(text, maxLength = 40) {
const value = String(text ?? "");
@@ -15,10 +18,13 @@ app.registerExtension({
name: EXTENSION_NAME,
beforeRegisterNodeDef(nodeType, nodeData) {
if (String(nodeData?.name ?? "") !== NODE_NAME) {
if (!NODE_NAMES.has(String(nodeData?.name ?? ""))) {
return;
}
const nodeName = String(nodeData?.name ?? "");
const prefix = nodeName === "PromptRandomChoiceEx" ? "Ex" : "Ch";
const maxTitleLength = nodeName === "PromptRandomChoiceEx" ? 36 : 40;
const originalOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
@@ -29,9 +35,9 @@ app.registerExtension({
const changeEvery = Number(message?.change_every?.[0] ?? 1);
if (changeEvery <= 1) {
this.title = `Choice: ${shorten(selectedTitle)}`;
this.title = `${prefix}: ${shorten(selectedTitle)}`;
} else {
this.title = `Choice: ${shorten(selectedTitle)} (${repeatIndex}/${changeEvery})`;
this.title = `${prefix}: ${shorten(selectedTitle)} (${repeatIndex}/${changeEvery})`;
}
this.setDirtyCanvas?.(true, true);