Merge branch 'develop'
This commit is contained in:
+22
@@ -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
|
||||
@@ -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
@@ -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` は、フラットな候補選択用としてそのまま残しています
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user