Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7c68fef9 | ||
|
|
33653511e9 | ||
|
|
59a7a3c878 | ||
|
|
08d3a9d6f3 | ||
|
|
9cfab48523 | ||
|
|
76e0782ade | ||
|
|
8c7126c22a | ||
|
|
9dae44569a | ||
|
|
6528e52b37 | ||
|
|
1186b5a97d | ||
|
|
5f1d7b628c | ||
|
|
9c35fc88dd | ||
|
|
9a671b8634 | ||
|
|
2e60d72233 | ||
|
|
01ffe39508 | ||
|
|
39bc22e213 | ||
|
|
d172fdb606 | ||
|
|
5f8c7a1db3 | ||
|
|
230401c4cd | ||
|
|
168d3de31d | ||
|
|
8af24d3d91 | ||
|
|
2f32e9321f | ||
|
|
de65d32ea6 | ||
|
|
6fd3705cfe | ||
|
|
540f3231af | ||
|
|
1159509805 | ||
|
|
53c3afa928 | ||
|
|
f3aee5cda2 | ||
|
|
ade26d5a14 | ||
|
|
280431c8da | ||
|
|
510d350ddc | ||
|
|
faaf2a6f24 | ||
|
|
382989df85 | ||
|
|
aba472b8f8 | ||
|
|
dcada8fd6b | ||
|
|
e65f26c8a3 | ||
|
|
82edd5da30 | ||
|
|
cae62792c3 | ||
|
|
ea058bae66 | ||
|
|
5f46308950 | ||
|
|
97c6f63e84 | ||
|
|
ce6bdd64b1 | ||
|
|
29b1d0fe4a | ||
|
|
dfab9c5d76 | ||
|
|
d27ca8e838 | ||
|
|
ac99f78071 | ||
|
|
ef9dbf47e1 | ||
|
|
6006441dc6 | ||
|
|
ab259c6802 | ||
|
|
b51e8b5e65 | ||
|
|
f76e1ba38a | ||
|
|
07d2dd32e8 | ||
|
|
87b010a60e | ||
|
|
1b80dacc6a | ||
|
|
b6edbafc3b | ||
|
|
caeb4a1df1 | ||
|
|
7f73d326e0 | ||
|
|
55797b831b | ||
|
|
9baa95869d | ||
|
|
06a0de2930 | ||
|
|
f5a95349f1 | ||
|
|
4ea9efcda3 | ||
|
|
73c75a84e7 | ||
|
|
9f73492858 | ||
|
|
ebbf562e5f | ||
|
|
02cc14f3f3 | ||
|
|
e575779220 | ||
|
|
c84ccff415 | ||
|
|
977963684c | ||
|
|
4f5e6d6e1f | ||
|
|
c05ebbb95e | ||
|
|
0aaf2403cc | ||
|
|
f5afd208c0 | ||
|
|
bfc1e6df3f | ||
|
|
65db4f8721 | ||
|
|
28b1112b2a | ||
|
|
f353917b89 | ||
|
|
c68dc7d630 | ||
|
|
81c3191721 | ||
|
|
52e790dfc8 | ||
|
|
aef8d13d1a | ||
|
|
0a08f2f761 | ||
|
|
06e02ac981 | ||
|
|
0d805e3748 | ||
|
|
87beed51fe | ||
|
|
34b1622ced | ||
|
|
8a880096a5 | ||
|
|
c3adb31102 | ||
|
|
44ec838788 | ||
|
|
88b7b945ee | ||
|
|
4846cc796d | ||
|
|
e7ac1f1f3d | ||
|
|
6f65534b3a | ||
|
|
ff1fddf88f | ||
|
|
5b3dcddb9d | ||
|
|
3c4433a2fa | ||
|
|
c29a1074ea | ||
|
|
e18954b69d | ||
|
|
f59ca83964 | ||
|
|
efa81d5b6b | ||
|
|
902b1aa856 | ||
|
|
a4309aeb4f | ||
|
|
2737049c10 | ||
|
|
b29ad95829 | ||
|
|
d58994d532 | ||
|
|
9750d398fb | ||
|
|
c0d59b04a9 | ||
|
|
7f5e177a1a | ||
|
|
dbcfa19980 | ||
|
|
54f062e22e | ||
|
|
f1c6c0670b | ||
|
|
eef4061b43 | ||
|
|
8d041fe8f6 | ||
|
|
5bb9d212a7 | ||
|
|
afc36842a2 | ||
|
|
baf20b12be | ||
|
|
71d6ec9653 | ||
|
|
c4b7730284 | ||
|
|
6ce5000ad2 | ||
|
|
43ba7388a0 | ||
|
|
bd40040991 | ||
|
|
56109734e6 | ||
|
|
f580d85540 | ||
|
|
e57e80deb5 | ||
|
|
ee9d7af958 | ||
|
|
8c821853b7 | ||
|
|
b74146dc2f | ||
|
|
0b66d19de3 | ||
|
|
cb44861374 | ||
|
|
8abeea9e3d | ||
|
|
116c98888f | ||
|
|
62a9e6926a | ||
|
|
e18616dc44 | ||
|
|
5e402746dc | ||
|
|
a8e18356e8 | ||
|
|
d9b1fac873 | ||
|
|
9414b59cdf | ||
|
|
b43aca1727 | ||
|
|
f00ddf95a5 | ||
|
|
3e3d328524 | ||
|
|
13dc35745d | ||
|
|
b49f59d861 | ||
|
|
6abed1f6be | ||
|
|
af458973ac | ||
|
|
6505c2c98d | ||
|
|
b13dc22f6b | ||
|
|
e9bcbf1a07 | ||
|
|
8f4401066d | ||
|
|
7474335343 | ||
|
|
e684eecd47 | ||
|
|
7bb938dccd | ||
|
|
f3b30b182a | ||
|
|
f6aa8a7403 | ||
|
|
ba29770958 | ||
|
|
92570ae6e4 | ||
|
|
f8d9578ecf |
@@ -8,17 +8,19 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
# if this is a forked repository. Skipping the workflow.
|
||||
if: github.event.repository.fork == false
|
||||
if: ${{ github.repository_owner == 'mus-taches' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
+4
-1
@@ -1,6 +1,10 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/osx,python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=osx,python
|
||||
|
||||
## Project specific
|
||||
config
|
||||
pull-upstream.sh
|
||||
|
||||
### OSX ###
|
||||
# General
|
||||
.DS_Store
|
||||
@@ -47,7 +51,6 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
>[!WARNING]
|
||||
>`v0.3.x` use the new sub-graph system to simplify the workflow and avoid too much custom node dependencies. Old workflows are expected to break. Please, use the repo's [workflow.json](workflow.json) as a basis. Some features are not there anymore for now (e.g. CivitAI metadatas parser); they will be added over time.
|
||||
|
||||
# ComfyUI Scene Composer
|
||||
A collection of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) nodes to create scenes with random procedural generation.
|
||||
|
||||
A collection of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) nodes to create explicit NSFW scenes with random procedural generation.
|
||||
|
||||

|
||||
|
||||
@@ -26,26 +30,20 @@ $ pip install -r requirements.txt
|
||||
|
||||
## Usage
|
||||
|
||||
> [!NOTE]
|
||||
> This is a basic guide to get you started. For more detailed information, please refer to the [wiki](https://github.com/mousse-taches/comfyui-scene-composer/wiki)
|
||||
The best way to getting started is to use the provided [`workflow.json`](workflow.json), which you can import in ComfyUI. You can play a bit with the parameters to see how they affect the generation.
|
||||
|
||||
Add the `🎞️ Scene` node to your workflow. The output will be a prompt formatted as a string.
|
||||
You can directly pass it to a conditioning node and connect it to your sampler.
|
||||
You can check out the full NSFW workflow here: [civitai.com/models/579396](https://civitai.com/models/579396)
|
||||
|
||||
The other nodes are scene components. You can use it as an optional input for the `🎞️ Scene` node, or directly as standalone.
|
||||
## About
|
||||
|
||||
| Component nodes | Description |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| 📹 Composition | The layout of the scene, cameras, number of protagonists, etc |
|
||||
| 🎬 Action | Activities done by the character/protagonist(s) |
|
||||
| 👤 Character | Main character, focusing on body type and faces, etc |
|
||||
| 👕 Clothes | Include casual clothes, uniforms, swimsuits, underwear, etc |
|
||||
| ⛅️ Environment | The setting, including background, weather, time of day, etc |
|
||||
### CivitAI links
|
||||
|
||||
### Config files
|
||||
- Author's profile: [civitai.com/user/taches](https://civitai.com/user/taches)
|
||||
- Custom nodes page: [civitai.com/models/902456](https://civitai.com/models/902456/)
|
||||
- Workflow page (NSFW): [civitai.com/models/579396](https://civitai.com/models/579396)
|
||||
|
||||
You can customize the prompt generation for components by editing the corresponding TOML file in the `config` folder. Keep it mind that the config will be replace by the default when there's an update, so you might want to keep a backup.
|
||||
Don't hesitate to share your creations or text if you have questions and ideas of improvement.
|
||||
|
||||
## License
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
This project is licensed under the [MIT LICENSE](LICENSE)
|
||||
|
||||
+32
-16
@@ -1,19 +1,35 @@
|
||||
from .py.scene import Scene
|
||||
from .py.components.composition import Composition
|
||||
from .py.components.action import Action
|
||||
from .py.components.character.character import Character
|
||||
from .py.components.clothes import Clothes
|
||||
from .py.components.environment import Environment
|
||||
from .py.utils.config import load_config
|
||||
from .py.nodes.node_factory import NodeFactory
|
||||
from .py.nodes.merge_strings import MergeStrings
|
||||
from .py.nodes.cleanup_prompt import CleanupPrompt
|
||||
from .py.nodes.apply_rules import ApplyRules
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"🎞️ Scene": Scene,
|
||||
"📹 Composition": Composition,
|
||||
"🎬 Action": Action,
|
||||
"👤 Character": Character,
|
||||
"👕 Clothes": Clothes,
|
||||
"⛅️ Environment": Environment,
|
||||
}
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
WEB_DIRECTORY = "js"
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', "WEB_DIRECTORY"]
|
||||
def register_node(node_id, node_name, node_class):
|
||||
NODE_CLASS_MAPPINGS[node_id] = node_class
|
||||
NODE_DISPLAY_NAME_MAPPINGS[node_id] = node_name
|
||||
|
||||
|
||||
# Create each node according to the config folder
|
||||
config = load_config()
|
||||
|
||||
for key, value in config.items():
|
||||
node_id = key
|
||||
node_name = value.get("name", key)
|
||||
ClassNode = NodeFactory.create_node(node_id, node_name)
|
||||
register_node(node_id, node_name, ClassNode)
|
||||
|
||||
# Add additional nodes
|
||||
register_node("MergeStrings", "🪡 Merge Strings", MergeStrings)
|
||||
register_node("CleanupPrompt", "🧹 CleanUp Prompt", CleanupPrompt)
|
||||
register_node("ApplyRules", "📋 Apply Rules", ApplyRules)
|
||||
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
"WEB_DIRECTORY"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "💦 Fluids",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"cum": ["cum", "excessive cum"],
|
||||
"after_cum": {
|
||||
"show": false,
|
||||
"tags": ["cum bubble", "cum string"]
|
||||
},
|
||||
"sweat": {
|
||||
"number": [1, 4],
|
||||
"tags": [
|
||||
"sweat",
|
||||
"very sweaty",
|
||||
"sparkling sweat",
|
||||
"sweating profusely",
|
||||
"wet",
|
||||
"hot",
|
||||
"shiny skin"
|
||||
]
|
||||
},
|
||||
"pussy_juice": {
|
||||
"number": [1, 3],
|
||||
"distribution": [0.7, 0.3],
|
||||
"tags": [
|
||||
"pussy juice",
|
||||
"excessive pussy juice",
|
||||
"pussy juice trail",
|
||||
"pussy juice puddle"
|
||||
]
|
||||
},
|
||||
"saliva": {
|
||||
"number": [1, 3],
|
||||
"distribution": [0.7, 0.3],
|
||||
"tags": [
|
||||
"saliva",
|
||||
"excessive saliva",
|
||||
"saliva trail",
|
||||
"drooling",
|
||||
"saliva drip"
|
||||
]
|
||||
},
|
||||
"precum": {
|
||||
"prefix": "precum, ",
|
||||
"number": [1, 2],
|
||||
"distribution": [0.45, 0.45, 0.1],
|
||||
"tags": ["precum trail", "precum drip", "precum bubble"]
|
||||
}
|
||||
},
|
||||
"output": false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "🔥 Intercourse",
|
||||
"variables": {
|
||||
"penetration_type": {
|
||||
"show": true,
|
||||
"distribution": [0.75, 0.25],
|
||||
"tags": ["vaginal", "anal"]
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"position": [
|
||||
"doggystyle",
|
||||
"top-down bottom-up",
|
||||
"cowgirl",
|
||||
"reverse cowgirl",
|
||||
"full nelson",
|
||||
"mating press"
|
||||
],
|
||||
"penetration": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"imminent": "imminent {penetration_type}, {pussy}, {penis}, penis head",
|
||||
"ongoing": "deep penetration, {penetration_type} penetration, {pussy}, {penis}",
|
||||
"climax": {
|
||||
"number": 2,
|
||||
"tags": {
|
||||
"penetration": "deep penetration, {penetration_type} penetration, {pussy}, {pussy_juice}, {penis}, orgasm, climax",
|
||||
"cum": {
|
||||
"prefix": "{cum},",
|
||||
"number": [1, 2],
|
||||
"distribution": [0.8],
|
||||
"tags": ["internal cumshot", "x-ray", "cross-section"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"after": {
|
||||
"number": 5,
|
||||
"tags": {
|
||||
"after_action": "after {penetration_type}, {pussy}",
|
||||
"after_penis": "{penis},penis head,",
|
||||
"after_penis_state": {
|
||||
"number": [0, 2],
|
||||
"tags": ["flaccid", "twitching penis", "after ejaculation"]
|
||||
},
|
||||
"after_cum": {
|
||||
"prefix": "{cum},{after_cum},",
|
||||
"number": [1, 3],
|
||||
"tags": [
|
||||
"cum on leg",
|
||||
"cum on body",
|
||||
"cum on breast",
|
||||
"cum on penis",
|
||||
"cum on stomach"
|
||||
]
|
||||
},
|
||||
"after_body_state": ["twitching", "shaking"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"breasts_out": {
|
||||
"show": false,
|
||||
"probability": 0.6,
|
||||
"tags": "breasts out,{nipples}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "🌶️ Preliminaries",
|
||||
"variables": {
|
||||
"penis_grab": {
|
||||
"probability": 0.5,
|
||||
"tags": "penis grab"
|
||||
},
|
||||
"prelim_head_grab": {
|
||||
"probability": 0.2,
|
||||
"fixed": true,
|
||||
"tags": "{head_grab}"
|
||||
},
|
||||
"breasts_out": {
|
||||
"probability": 0.5,
|
||||
"tags": "breasts out,{nipples}"
|
||||
},
|
||||
"prelim_climax": {
|
||||
"tags": "orgasm, climax, {cum},"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"action": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"fellatio": "fellatio, saliva on penis, {saliva}, {prelim_head_grab}",
|
||||
"handjob": "handjob, {precum}",
|
||||
"paizuri": {
|
||||
"tags": [
|
||||
"paizuri",
|
||||
"straddling paizuri",
|
||||
"paizuri on lap",
|
||||
"reverse paizuri"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"imminent": "imminent {action}, penis head, {precum}",
|
||||
"ongoing": "{action},{precum}",
|
||||
"climax": "{action},{prelim_climax}",
|
||||
"after": "after {action}, penis head, flaccid, {cum}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": "{state},{penis},{penis_grab},{breasts_out}"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "🍑 Sexual Attributes",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"nipple_color": {
|
||||
"probability": 0.2,
|
||||
"distribution": [0.95, 0.05],
|
||||
"tags": ["dark", "{color}"],
|
||||
"suffix": " nipples"
|
||||
},
|
||||
"nipple_style": {
|
||||
"probability": 0.4,
|
||||
"tags": [
|
||||
"puffy nipples",
|
||||
"huge nipples",
|
||||
"inverted nipples",
|
||||
"long nipples",
|
||||
"small nipples",
|
||||
"large areolae"
|
||||
]
|
||||
},
|
||||
"nipple_piercing": {
|
||||
"probability": 0.1,
|
||||
"prefix": "nipple piercing,",
|
||||
"tags": ["nipple bar", "nipple chain", "nipple rings"]
|
||||
},
|
||||
"pussy_hair": {
|
||||
"probability": 0.2,
|
||||
"tags": [
|
||||
"pubic hair",
|
||||
"pubic stubble",
|
||||
"sparse pubic hair",
|
||||
"excessive pubic hair",
|
||||
"heart-shaped pubic hair"
|
||||
]
|
||||
},
|
||||
"pussy": {
|
||||
"show": false,
|
||||
"tags": "pussy, {pussy_hair}"
|
||||
},
|
||||
"nipples": {
|
||||
"show": false,
|
||||
"tags": "{nipple_color},{nipple_style},{nipple_piercing}"
|
||||
}
|
||||
},
|
||||
"output": false
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "🪽 SFW",
|
||||
"tags": {
|
||||
"position": {
|
||||
"probability": 0.5,
|
||||
"distribution": [0.3, 0.3, 0.1, 0.1],
|
||||
"tags": ["standing", "sitting", "lying", "kneeling", "squatting"]
|
||||
},
|
||||
"action": {
|
||||
"group_labels": true,
|
||||
"distribution": [0.7],
|
||||
"tags": {
|
||||
"simple gesture": [
|
||||
"crossed arms",
|
||||
"hand on own hip",
|
||||
"hand in own hair, adjusting hair",
|
||||
"hand on own face",
|
||||
"hand on own chest",
|
||||
"v sign",
|
||||
"finger to face"
|
||||
],
|
||||
"on phone": "holding cellphone",
|
||||
"gaming": {
|
||||
"prefix": "holding ",
|
||||
"tags": ["game controller", "handlheld console", "desktop"]
|
||||
},
|
||||
"reading": {
|
||||
"number": [1, 2],
|
||||
"tags": ["holding book", "reading book"]
|
||||
},
|
||||
"drinking": {
|
||||
"prefix": "holding drink,",
|
||||
"number": 2,
|
||||
"tags": {
|
||||
"drink_action": { "probability": 0.6, "tags": "drinking" },
|
||||
"drink_type": ["beer mug", "sake bottle", "coffee mug", "soda can"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "👅 Teasing",
|
||||
"tags": {
|
||||
"action": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"fingering": {
|
||||
"tags": {
|
||||
"fingering_action": {
|
||||
"number": [0, 3],
|
||||
"tags": ["fingering", "anal fingering", "fingering from behind"]
|
||||
},
|
||||
"fingering_extra": "{pussy}, {pussy_juice}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "💭 Attitude",
|
||||
"tags": {
|
||||
"attitude": {
|
||||
"distribution": [0.3, 0.3, 0.2],
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"happy": {
|
||||
"number": [1, 2],
|
||||
"tags": ["happy", "smiling", "determined", "seductive smile", ":3"]
|
||||
},
|
||||
"smirk": {
|
||||
"number": [1, 4],
|
||||
"tags": [
|
||||
"smirk",
|
||||
"smiling",
|
||||
"evil eyes",
|
||||
"evil smile",
|
||||
"seductive smile",
|
||||
"naughty face",
|
||||
"come hither",
|
||||
"determined",
|
||||
"smug",
|
||||
"doyago"
|
||||
]
|
||||
},
|
||||
"emotionless": {
|
||||
"number": [1, 3],
|
||||
"tags": ["emotionless", "bored", "tired", "neutral", "serious"]
|
||||
},
|
||||
"upset": {
|
||||
"number": [1, 2],
|
||||
"tags": [
|
||||
"upset",
|
||||
"annoyed",
|
||||
"frustrated",
|
||||
"angry",
|
||||
"disgusted",
|
||||
"serious"
|
||||
]
|
||||
},
|
||||
"confused": {
|
||||
"number": [1, 2],
|
||||
"tags": ["confused", "pensive", "thinking", "wince", "surprised"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "🧍♀️ Body",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"standard_skin_color": [
|
||||
"tan skin, tanlines",
|
||||
"dark skin",
|
||||
"very dark skin",
|
||||
"pale skin"
|
||||
],
|
||||
"special_skin_color": "{color}"
|
||||
},
|
||||
"tags": {
|
||||
"skin_color": {
|
||||
"probability": 0.3,
|
||||
"distribution": [0.95, 0.05],
|
||||
"tags": ["{standard_skin_color}", "{special_skin_color} skin"]
|
||||
},
|
||||
"body_type": {
|
||||
"probability": 0.7,
|
||||
"distribution": [0.2, 0.2, 0.1, 0.1, 0.1],
|
||||
"tags": [
|
||||
"petite",
|
||||
"slim",
|
||||
"fit",
|
||||
"skinny",
|
||||
"curvy",
|
||||
"muscular",
|
||||
"plump",
|
||||
"chubby",
|
||||
"fat"
|
||||
]
|
||||
},
|
||||
"breasts": {
|
||||
"distribution": [0.05, 0.3, 0.3, 0.3, 0.05],
|
||||
"tags": [
|
||||
"flat chest",
|
||||
"small breasts",
|
||||
"medium breasts",
|
||||
"large breasts",
|
||||
"huge breasts"
|
||||
]
|
||||
},
|
||||
"bottom": {
|
||||
"probability": 0.1,
|
||||
"number": [1, 2],
|
||||
"tags": ["thick thighs", "wide hips", "narrow waist"]
|
||||
},
|
||||
"freckles": {
|
||||
"probability": 0.2,
|
||||
"tags": {
|
||||
"freckles": { "tags": ["freckles", "body freckles"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "👤 Character",
|
||||
"tags": {
|
||||
"character_type": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"generic": "{body},{hair},{eyes}",
|
||||
"2b": "2b, nier automata, small breasts, short hair, white hair, black hairband, black blindfold, mole under mouth",
|
||||
"android_18": "android 18, small breasts, short hair, blonde hair, blue eyes, stud earrings",
|
||||
"bowsette": "bowsette, large breast, blonde hair, ponytail, horns, blue eyes, crown, spiked choker, spiked bracelet, turtle shell, spiked tail, earrings",
|
||||
"chel": "chel, el dorado, medium breasts, dark-skinned female, brown eyes, long hair, black hair, blunt bangs, blunt sidelocks, earrings, bracelet",
|
||||
"chun-li": "chun-li, medium breasts, brown hair, double bun, brown eyes, spiked bracelets, earrings",
|
||||
"hinata": "hinata hyuga, medium breasts, long hair, dark blue hair, white eyes, no pupils, headband around neck",
|
||||
"jinx": "jinx, flat chest, blue hair, pink eyes, very long hair, twin braids, armwear, fingerless gloves, cloud tattoo, shoulder tattoo, choker",
|
||||
"kim_possible": "kim possible, small breasts, ginger, long hair, green eyes",
|
||||
"lara_croft": "lara croft, large breasts, long hair, brown hair, braided ponytail, parted bangs, brown eyes",
|
||||
"princess_daisy": "princess daisy, medium breasts, brown hair, blue eyes, crown, flower earrings",
|
||||
"princess_peach": "princess peach, medium breasts, blonde hair, blue eyes",
|
||||
"princess_zelda": "princess zelda, medium breasts, blonde hair, long hair, blue eyes, pointy ears",
|
||||
"raven": "raven, dc comics, pale skin, medium breasts, short hair, purple hair, purple eyes, forehead jewlery, red gem",
|
||||
"sakura": "sakura haruno, medium breasts, pink hair, medium hair, green eyes, red headband",
|
||||
"samus": "samus aran, medium breasts, long hair, blonde hair, ponytail, blue eyes",
|
||||
"tsunade": "tsunade, large breasts, blonde hair, low ponytail, long hair, yellow eyes, forhead mark, purple mark",
|
||||
"velma": "velma, large breasts, brown hair, short hair, freckles, glasses"
|
||||
}
|
||||
},
|
||||
"costume": [
|
||||
"official costume",
|
||||
"{clothes}"
|
||||
]
|
||||
},
|
||||
"output": "{character_type},{attitude},{costume}"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "🗯️ Expression",
|
||||
"tags": {
|
||||
"eyes": {
|
||||
"probability": 0.3,
|
||||
"distribution": [0.3, 0.3, 0.3, 0.1],
|
||||
"tags": [
|
||||
"half-closed eyes",
|
||||
"one eye half-closed",
|
||||
"one eye closed",
|
||||
"closed eyes",
|
||||
"wide open eyes"
|
||||
]
|
||||
},
|
||||
"gaze": {
|
||||
"probability": 0.3,
|
||||
"prefix": "looking ",
|
||||
"tags": ["at viewer", "up", "down", "to the side", "back"]
|
||||
},
|
||||
"facing": {
|
||||
"probability": 0.3,
|
||||
"prefix": "facing ",
|
||||
"tags": ["viewer", "up", "down", "to the side"]
|
||||
},
|
||||
"eyebrow": {
|
||||
"probability": 0.3,
|
||||
"tags": ["raised eyebrow", "raised eyebrows", "frown", "v eyebrows"]
|
||||
},
|
||||
"mouth": {
|
||||
"probability": 0.3,
|
||||
"tags": ["closed mouth", "parted lips", "open mouth"]
|
||||
},
|
||||
"symbols": {
|
||||
"probability": 0.3,
|
||||
"tags": [
|
||||
"^^^",
|
||||
"notice lines",
|
||||
"shout lines",
|
||||
"puff of air",
|
||||
"spoken heart"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "👁️ Eyes",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"makeup_type": ["eyeliner", "eyeshadow", "mascara"],
|
||||
"makeup_color": ["{color}"],
|
||||
"eyewear_color": {
|
||||
"tags": ["{color}"],
|
||||
"suffix": " eyewear"
|
||||
},
|
||||
"eyewear_shape": {
|
||||
"probability": 0.1,
|
||||
"tags": ["rectangular", "round"],
|
||||
"suffix": " eyewear"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"eye_color": {
|
||||
"tags": ["{color}"],
|
||||
"suffix": " eyes"
|
||||
},
|
||||
"makeup": {
|
||||
"probability": 0.2,
|
||||
"tags": { "makeup": "{makeup_color} {makeup_type}" }
|
||||
},
|
||||
"eyewear": {
|
||||
"group_labels": true,
|
||||
"probability": 0.2,
|
||||
"distribution": [0.5, 0.3],
|
||||
"tags": {
|
||||
"regular": "glasses, {eyewear_color}, {eyewear_shape}",
|
||||
"sunglasses": "sunglasses, {eyewear_color}, {eyewear_shape}",
|
||||
"tinted_glasses": "glasses, tinted eyewear, {eyewear_color}, {eyewear_shape}",
|
||||
"eyepatch": "eyepatch, {eyewear_color}",
|
||||
"googles": ["googles on head", "googles around neck"]
|
||||
}
|
||||
},
|
||||
"eye_effect": {
|
||||
"probability": 0.1,
|
||||
"tags": { "eye_effect": ["glowing eyes", "no pupils", "sparkling eyes"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "🪮 Hair",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"hair_main_color": {
|
||||
"output": true,
|
||||
"tags": ["blonde", "ginger", "{color}"],
|
||||
"suffix": " hair"
|
||||
},
|
||||
"hair_secondary_color": {
|
||||
"tags": ["blonde", "ginger", "{color}"],
|
||||
"suffix": " hair"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"hair_color": { "show": false, "tags": "{hair_main_color}" },
|
||||
"multicolored_hair": {
|
||||
"probability": 0.1,
|
||||
"prefix": "{hair_secondary_color} hair, multicolored hair,",
|
||||
"tags": { "multicolored": ["colored tips", "gradient hair"] }
|
||||
},
|
||||
"hair_length": {
|
||||
"distribution": [0.3, 0.3, 0.3, 0.1],
|
||||
"tags": ["short", "medium", "long", "very long", "big"],
|
||||
"suffix": " hair"
|
||||
},
|
||||
"hair_style": {
|
||||
"probability": 0.4,
|
||||
"tags": [
|
||||
"afro",
|
||||
"bangs",
|
||||
"single braid",
|
||||
"twin braids",
|
||||
"crown braid",
|
||||
"curly hair",
|
||||
"double bun",
|
||||
"drill hair",
|
||||
"hair bun",
|
||||
"messy hair",
|
||||
"mohawk",
|
||||
"ponytail",
|
||||
"side ponytail",
|
||||
"sidecut",
|
||||
"sidelocks",
|
||||
"spiked hair",
|
||||
"twintails",
|
||||
"wavy hair",
|
||||
"hair slicked back",
|
||||
"hair pulled back"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "🧢 Accessories",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"headwear_color": ["{color}"],
|
||||
"hat": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"backwards cap": "baseball cap, backwards hat, {headwear_color}",
|
||||
"bandana": "bandana, {headwear_color}",
|
||||
"beanie": "beanie, {headwear_color}",
|
||||
"bucket hat": "bucket hat, {headwear_color}",
|
||||
"cap": "baseball cap, {headwear_color}",
|
||||
"cowboy": "cowboy hat",
|
||||
"diadem": {
|
||||
"tags": ["diadem", "tiara"],
|
||||
"suffix": ", {headwear_color}"
|
||||
},
|
||||
"fedora": "fedora, {headwear_color}",
|
||||
"headphones (on)": "headphones, {headwear_color}",
|
||||
"headphones (around neck)": "headphones around neck, {headwear_color}",
|
||||
"newsboy": "newsboy cap, {headwear_color}",
|
||||
"rice hat": "rice hat",
|
||||
"straw hat": "straw hat"
|
||||
}
|
||||
},
|
||||
"hairband": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"hairband": "{headwear_color} hairband",
|
||||
"headband": "{headwear_color} headband",
|
||||
"hair_bow": "hair bow, {headwear_color} bow",
|
||||
"hair_ribbon": "hair ribbon, {headwear_color} ribbon",
|
||||
"hair_stick": "hair stick",
|
||||
"hairpin": ["hairpin", "hairclip"]
|
||||
}
|
||||
},
|
||||
"earrings": [
|
||||
"floating earring",
|
||||
"hoop earring",
|
||||
"stud earring",
|
||||
"earpiercing"
|
||||
],
|
||||
"neckwear_color": ["{color}"],
|
||||
"neckwear_type": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"choker": "{neckwear_color} choker",
|
||||
"dog tags": "dog tags, {neckwear_color} neckwear",
|
||||
"lace-trimmed": "lace-trimmed collar, {neckwear_color} neckwear",
|
||||
"necklace (chain)": "chain necklace",
|
||||
"necklace (tooth)": "tooth necklace",
|
||||
"necklace": "necklace, {neckwear_color} neckwear",
|
||||
"o-ring": "o-ring collar, {neckwear_color} neckwear",
|
||||
"ribbon": "neck ribbon, {neckwear_color} ribbon",
|
||||
"spiked": "spiked collar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"headwear_type": {
|
||||
"group_labels": true,
|
||||
"probability": 0.2,
|
||||
"tags": {
|
||||
"hairband": "{hairband}",
|
||||
"hat": "{headwear} headwear"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "👕 Casual clothes",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"color_main": { "tags": ["{color}"] },
|
||||
"color_secondary": { "tags": ["{color}"] },
|
||||
"bottom_high": {
|
||||
"probability": 0.5,
|
||||
"tags": ["lowleg", "high-waist"]
|
||||
},
|
||||
"skirt_length": { "tags": ["microskirt", "short", "medium", "long"] },
|
||||
"cleavage": {
|
||||
"probability": 0.5,
|
||||
"tags": { "cleavage": "cleavage" }
|
||||
},
|
||||
"no_bra": {
|
||||
"probability": 0.2,
|
||||
"number": [1, 2],
|
||||
"tags": ["no bra", "covered nipples"]
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"top": {
|
||||
"prefix": "{color_main} ",
|
||||
"tags": [
|
||||
"shirt,{cleavage}",
|
||||
"crew shirt",
|
||||
"t-shirt",
|
||||
"polo shirt",
|
||||
"hawaiian shirt",
|
||||
"oversized shirt",
|
||||
"dress shirt,{cleavage}",
|
||||
"blouse, {cleavage}",
|
||||
"tank top, {cleavage}",
|
||||
"tube top, {cleavage}",
|
||||
"turtleneck",
|
||||
"crop top, {cleavage}, ",
|
||||
"hoodie",
|
||||
"sweater"
|
||||
],
|
||||
"suffix": ",{no_bra}"
|
||||
},
|
||||
"bottom": {
|
||||
"prefix": "{color_secondary} ",
|
||||
"tags": [
|
||||
"{bottom_high} pants",
|
||||
"{bottom_high} shorts",
|
||||
"jeans",
|
||||
"{length_clothes} skirt",
|
||||
"leggings",
|
||||
"loincloth"
|
||||
]
|
||||
},
|
||||
"vest": {
|
||||
"probability": 0.1,
|
||||
"prefix": "{color_secondary} ",
|
||||
"tags": ["jacket", "suite jacket", "leather jacket", "blazer", "coat"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "👔 Clothes",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"clothes_type": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"casual": "{casual}",
|
||||
"dress": "{dress}",
|
||||
"swimsuit": "{swimsuit}",
|
||||
"uniform": "{uniform}",
|
||||
"underwear": "{underwear}"
|
||||
}
|
||||
},
|
||||
"cleavage": {
|
||||
"probability": 0.5,
|
||||
"distribution": [0.5],
|
||||
"tags": [
|
||||
"cleavage",
|
||||
"see-through cleavage",
|
||||
"cleavage cutout",
|
||||
"underboob",
|
||||
"sideboob"
|
||||
]
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"group_labels": true,
|
||||
"distribution": [0.4, 0.6],
|
||||
"tags": {
|
||||
"generic": "{clothes_type}",
|
||||
"persona": "official costume"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "👗 Dress",
|
||||
"tags": {
|
||||
"dress_color": { "tags": ["{color}"], "suffix": " dress" },
|
||||
"dress_length": ["microdress", "short dress", "medium dress", "long dress"],
|
||||
"dress_style": [
|
||||
"pleated dress",
|
||||
"latex dress",
|
||||
"lace-trimmed dress",
|
||||
"layered dress",
|
||||
"tight dress",
|
||||
"taut dress",
|
||||
"chinese dress"
|
||||
],
|
||||
"sleeves": {
|
||||
"probability": 0.3,
|
||||
"tags": ["sleeveless dress", "long sleeve", "short sleeve"]
|
||||
},
|
||||
"cut": {
|
||||
"probability": 0.5,
|
||||
"tags": [
|
||||
"off-shoulder dress",
|
||||
"strapless dress",
|
||||
"plunging neckline",
|
||||
"backless",
|
||||
"side slit"
|
||||
]
|
||||
},
|
||||
"cleavage": {
|
||||
"probability": 0.5,
|
||||
"distribution": [0.5],
|
||||
"tags": [
|
||||
"cleavage",
|
||||
"see-through cleavage",
|
||||
"cleavage cutout",
|
||||
"sideboob"
|
||||
]
|
||||
},
|
||||
"no_bra": {
|
||||
"probability": 0.2,
|
||||
"number": [1, 2],
|
||||
"tags": ["no bra", "covered nipples"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "👙 Swimsuit",
|
||||
"tags": {
|
||||
"swimsuit_color": { "tags": ["{color}"], "suffix": " swimsuit" },
|
||||
"bikini_type": [
|
||||
"bikini",
|
||||
"one-piece swimsuit",
|
||||
"highleg bikini",
|
||||
"highleg one-piece swimsuit",
|
||||
"lowleg bikini",
|
||||
"micro bikini",
|
||||
"thong bikini",
|
||||
"slingshot swimsuit"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "🩲 Underwear",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"color_main": ["{color}"],
|
||||
"cleavage": {
|
||||
"probability": 0.75,
|
||||
"tags": { "cleavage": "cleavage" }
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"lingerie": {
|
||||
"probability": 0.5,
|
||||
"tags": { "lace": "lingerie,lace" }
|
||||
},
|
||||
"crotchless": {
|
||||
"probability": 0.3,
|
||||
"tags": { "crotchless": "crotchless,{pussy}" }
|
||||
},
|
||||
"bra": {
|
||||
"group_labels": true,
|
||||
"prefix": "{color_main} ",
|
||||
"tags": {
|
||||
"bra": "bra, {cleavage}",
|
||||
"bustier": "bustier, {cleavage}",
|
||||
"corset": "corset, {cleavage}"
|
||||
}
|
||||
},
|
||||
"panties": {
|
||||
"group_labels": true,
|
||||
"prefix": "{color_main} ",
|
||||
"tags": ["panties", "thong", "g-string"]
|
||||
},
|
||||
"leotard": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"colored": "{color_main} leotard",
|
||||
"fishnet": "fishnet leotard"
|
||||
},
|
||||
"suffix": ", no neckwear, {cleavage}"
|
||||
},
|
||||
"legwear": {
|
||||
"tags": ["lace-trimmed", "lace-up", "ribbon-trimmed"],
|
||||
"suffix": " thighhighs"
|
||||
},
|
||||
"accessories": ["garter belt", "garter straps"]
|
||||
},
|
||||
"output": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"standard": "{panties}, {bra}",
|
||||
"standard_with_legwear": "{panties}, {bra}, {legwear}, {accessories}",
|
||||
"one-piece": "{leotard}",
|
||||
"one-piece_with_legwear": "{leotard}, {legwear}, {accessories}"
|
||||
}
|
||||
},
|
||||
"custom_output": "{panties}, {bra}, {legwear}, {accessories}"
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"name": "🥋 Uniform",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"color_main": ["{color}"],
|
||||
"color_secondary": ["{color}"],
|
||||
"skirt_length": ["short", "medium", "long"],
|
||||
"cleavage": {
|
||||
"probability": 0.5,
|
||||
"tags": { "cleavage": "cleavage" }
|
||||
},
|
||||
"vest": {
|
||||
"show": false,
|
||||
"tags": ["vest", "blazer", "waistcoat"]
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"school_uniform_type": {
|
||||
"group_labels": true,
|
||||
"prefix": "school uniform,",
|
||||
"tags": {
|
||||
"sailor": "serafuku, {color_main} blouse, {color_secondary} skirt, {skirt_length} skirt, pleated skirt, sailor-style collar, {color_secondary} neckerchief",
|
||||
"vest": "{color_main} {vest}, {color_secondary} skirt, {skirt_length} skirt, pleated skirt, white blouse, {color_secondary} necktie",
|
||||
"sweater": {
|
||||
"prefix": "{color_main} ",
|
||||
"tags": ["sweater", "sweater vest"],
|
||||
"suffix": ",{color_secondary} skirt, {skirt_length} skirt, pleated skirt, collared shirt"
|
||||
}
|
||||
},
|
||||
"suffix": ",{cleavage}"
|
||||
},
|
||||
"gym_uniform_type": {
|
||||
"show": false,
|
||||
"number": 3,
|
||||
"tags": {
|
||||
"top": {
|
||||
"prefix": "gym uniform,{color_main} ",
|
||||
"tags": ["tank top,{cleavage}", "shirt, gym shirt"]
|
||||
},
|
||||
"bottom": {
|
||||
"prefix": "{color_secondary} ",
|
||||
"tags": [
|
||||
"shorts, gym shorts",
|
||||
"shorts, bururma",
|
||||
"shorts, bike shorts",
|
||||
"shorts, dolphin shorts",
|
||||
"leggings",
|
||||
"pants, yoga pants",
|
||||
"pants, track pants",
|
||||
"skirt, short skirt"
|
||||
]
|
||||
},
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": ["{color_secondary} headband", "{color_secondary} wristband"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"military_uniform_type": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"camo": {
|
||||
"number": 7,
|
||||
"tags": {
|
||||
"prefix": "military fatigues,",
|
||||
"camo_type": ["woodland", "desert", "urban", "snow"],
|
||||
"camo_top": ["military jacket", "shirt"],
|
||||
"camo_bottom": ["camouflage pants", "cargo shorts"],
|
||||
"camo_shoes": {
|
||||
"probability": 0.2,
|
||||
"tags": "combat boots"
|
||||
},
|
||||
"camo_hat": {
|
||||
"probability": 0.2,
|
||||
"tags": ["field cap", "beret", "boonie hat", "aviator cap"]
|
||||
},
|
||||
"camo_accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 3],
|
||||
"tags": ["dog tags", "military belt", "gloves"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"formal": {
|
||||
"number": 6,
|
||||
"tags": {
|
||||
"prefix": "military dress uniform, {color_main} uniform,",
|
||||
"formal_top": ["military jacket", "military coat"],
|
||||
"format_shirt": ["dress shirt", "collared shirt"],
|
||||
"formal_bottom": [
|
||||
"military pants",
|
||||
"military skirt, {skirt_length} skirt"
|
||||
],
|
||||
"formal_hat": {
|
||||
"probability": 0.2,
|
||||
"suffix": "{color_secondary} ",
|
||||
"tags": ["beret", "peaked cap"]
|
||||
},
|
||||
"formal_accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 3],
|
||||
"tags": [
|
||||
"medal",
|
||||
"ribbon bar",
|
||||
"epaulettes",
|
||||
"{color_secondary} necktie"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"police_uniform_type": {
|
||||
"show": false,
|
||||
"number": 5,
|
||||
"tags": {
|
||||
"prefix": "police uniform,",
|
||||
"top": ["collared shirt", "jacket", "utility vest"],
|
||||
"bottom": ["pants", "skirt"],
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 3],
|
||||
"tags": [
|
||||
"{color_secondary} armband",
|
||||
"holster",
|
||||
"police badge",
|
||||
"{color_secondary} necktie",
|
||||
"utility belt"
|
||||
]
|
||||
},
|
||||
"hat": { "probability": 0.3, "tags": ["police hat", "peaked cap"] }
|
||||
}
|
||||
},
|
||||
"nurse_uniform_type": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"nurse": {
|
||||
"uniform": "nurse, {color_main} uniform, {cleavage}",
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": [
|
||||
"nurse cap, red cross",
|
||||
"nurse apron",
|
||||
"armband, red cross",
|
||||
"{color_secondary} stethoscope",
|
||||
"surgical mask"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lab_coat": {
|
||||
"prefix": "white coat, lab coat,",
|
||||
"tags": {
|
||||
"under_lab": [
|
||||
"{casual}",
|
||||
"{dress}",
|
||||
"{underwear}",
|
||||
"naked lab coat,{nipples}"
|
||||
]
|
||||
},
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": ["{color_secondary} stethoscope", "surgical mask"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"maid_uniform_type": {
|
||||
"group_labels": true,
|
||||
"prefix": "maid,",
|
||||
"tags": {
|
||||
"standard": {
|
||||
"number": 5,
|
||||
"tags": {
|
||||
"color": {
|
||||
"distribution": [0.4],
|
||||
"tags": ["black", "{color_main}"],
|
||||
"suffix": " uniform"
|
||||
},
|
||||
"dress": "{skirt_length} dress,",
|
||||
"sleeves": ["long sleeves", "short sleeves"],
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": ["maid apron", "maid headdress"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lingerie": {
|
||||
"number": 4,
|
||||
"tags": {
|
||||
"color": {
|
||||
"distribution": [0.4],
|
||||
"tags": ["black", "{color_main}"],
|
||||
"suffix": " uniform"
|
||||
},
|
||||
"dress": "short dress, {cleavage},",
|
||||
"bottom": "{panties},{legwear},{crotchless},{lingerie}",
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": ["maid apron", "maid headdress"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"animal_uniform_type": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"playboy": {
|
||||
"prefix": "playboy bunny, strapless leotard, {color_main} leotard, bunny ears, bunny tail,",
|
||||
"tags": {
|
||||
"bottom": ["fishnet pantyhose", "thighhighs"],
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": [
|
||||
"{color_secondary} bowtie",
|
||||
"{color_secondary} wrist cuffs",
|
||||
"{color_secondary} colar, detached collar"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"kittysuit": {
|
||||
"prefix": "kittysuit, strapless leotard, {color_main} leotard, cat ears, cat tail,",
|
||||
"tags": {
|
||||
"bottom": ["fishnet pantyhose", "thighhighs"],
|
||||
"accessories": {
|
||||
"probability": 0.3,
|
||||
"number": [1, 2],
|
||||
"tags": [
|
||||
"{color_secondary} bowtie",
|
||||
"{color_secondary} wrist cuffs",
|
||||
"{color_secondary} colar, detached collar"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"school": "{school_uniform_type}",
|
||||
"gym": "{gym_uniform_type}",
|
||||
"military": "{military_uniform_type}",
|
||||
"police": "{police_uniform_type}",
|
||||
"nurse": "{nurse_uniform_type}",
|
||||
"maid": "{maid_uniform_type}",
|
||||
"animal": "{animal_uniform_type}"
|
||||
}
|
||||
},
|
||||
"custom_output": "serafuku, white blouse, blue skirt, medium skirt, pleated skirt, sailor-style collar, red neckerchief"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "📷 Camera",
|
||||
"tags": {
|
||||
"multiple_views": {
|
||||
"probability": 0.05,
|
||||
"number": [1, 2],
|
||||
"tags": ["multiple views", "comic", "borderless panels"]
|
||||
},
|
||||
"perspective": {
|
||||
"probability": 0.5,
|
||||
"number": [1, 2],
|
||||
"tags": ["perspective", "vanishing point", "foreshortening"]
|
||||
},
|
||||
"framings": {
|
||||
"probability": 0.3,
|
||||
"distribution": [0.3, 0.2, 0.2, 0.2, 0.1],
|
||||
"tags": [
|
||||
"pov",
|
||||
"portrait",
|
||||
"upper body",
|
||||
"close-up",
|
||||
"full body",
|
||||
"wide shot",
|
||||
"very wide shot"
|
||||
]
|
||||
},
|
||||
"angles": {
|
||||
"probability": 0.3,
|
||||
"distribution": [0.4, 0.3],
|
||||
"tags": [
|
||||
"straight on",
|
||||
"three quarter view",
|
||||
"from above",
|
||||
"from below",
|
||||
"from side",
|
||||
"from behind",
|
||||
"upside-down"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "💥 Dynamic",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"motion": {
|
||||
"probability": 0.9,
|
||||
"number": [1, 2],
|
||||
"tags": ["motion lines", "speed lines"]
|
||||
},
|
||||
"emphasis": {
|
||||
"probability": 0.7,
|
||||
"number": [1, 2],
|
||||
"tags": ["emphasis lines", "^^^"]
|
||||
},
|
||||
"afterimage": {
|
||||
"probability": 0.2,
|
||||
"number": [1, 2],
|
||||
"tags": ["afterimage", "motion blur"]
|
||||
},
|
||||
"sound": {
|
||||
"group_labels": true,
|
||||
"probability": 0.5,
|
||||
"number": 2,
|
||||
"tags": {
|
||||
"sound_effects": [
|
||||
"sound effects",
|
||||
"speech bubble",
|
||||
"spoken heart",
|
||||
"spoken sound effect"
|
||||
],
|
||||
"language": {
|
||||
"probability": 0.5,
|
||||
"distribution": [0.7, 0.3],
|
||||
"tags": ["japanese", "english"],
|
||||
"suffix": " text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": "{motion}, {emphasis}, {afterimage}, {sound}"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "⛅️ Environment",
|
||||
"variables": {
|
||||
"furniture": ["desk", "table"]
|
||||
},
|
||||
"tags": {
|
||||
"indoors_location": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"bedroom": "bedroom, bed",
|
||||
"living room": {
|
||||
"prefix": "living room,",
|
||||
"tags": ["coffee table", "couch"]
|
||||
},
|
||||
"kitchen": "kitchen, table",
|
||||
"office": "office, {furniture}",
|
||||
"library": "room, library, {furniture}",
|
||||
"classroom": "classroom, {furniture}"
|
||||
}
|
||||
},
|
||||
"outdoors_location": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"park": {
|
||||
"prefix": "nature, park,",
|
||||
"number": [0, 3],
|
||||
"tags": ["bench", "grass", "trees", "tree shade", "garden"]
|
||||
},
|
||||
"beach": {
|
||||
"prefix": "nature, beach,",
|
||||
"number": [0, 3],
|
||||
"tags": [
|
||||
"sand",
|
||||
"shore",
|
||||
"palm trees",
|
||||
"lounge chair",
|
||||
"parasol",
|
||||
"beach towel",
|
||||
"island"
|
||||
]
|
||||
},
|
||||
"forest": {
|
||||
"prefix": "nature, forest,",
|
||||
"number": [0, 3],
|
||||
"tags": ["grass", "trees", "tree shade", "foliage"]
|
||||
},
|
||||
"rooftop": "rooftop, on rooftop, ground"
|
||||
}
|
||||
},
|
||||
"weather": {
|
||||
"distribution": [0.65, 0.2, 0.1, 0.05],
|
||||
"tags": ["sunny", "rainy", "cloudy", "snowy"]
|
||||
},
|
||||
"background_pattern": {
|
||||
"tags": ["simple", "halftone"],
|
||||
"suffix": " background"
|
||||
},
|
||||
"background_color": {
|
||||
"tags": "{color}",
|
||||
"suffix": " background"
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"group_labels": true,
|
||||
"distribution": [0.45, 0.45, 0.1],
|
||||
"tags": {
|
||||
"indoors": "indoors,{indoors_location}",
|
||||
"outdoors": "outdoors,{outdoors_location},{weather}",
|
||||
"solid_background": "{background_pattern},{background_color}"
|
||||
}
|
||||
},
|
||||
"custom_output": "{outdoors_location},{weather}"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "👥 Protagonists",
|
||||
"settings": {
|
||||
"show_variables": true
|
||||
},
|
||||
"variables": {
|
||||
"solo_focus": {
|
||||
"probability": 0.8,
|
||||
"tags": { "solo_focus": "solo focus" }
|
||||
},
|
||||
"disembodied_penis": {
|
||||
"probability": 0.2,
|
||||
"tags": { "disembodied penis": "disembodied penis" }
|
||||
},
|
||||
"penis_size": {
|
||||
"probability": 0.7,
|
||||
"distribution": [0.1, 0.6, 0.2, 0.1],
|
||||
"tags": ["small", "large", "huge", "gigantic"]
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"male_type": {
|
||||
"group_labels": true,
|
||||
"distribution": [0.5, 0.4],
|
||||
"prefix": "1boy,{solo_focus},",
|
||||
"tags": {
|
||||
"standard": "",
|
||||
"muscular": {
|
||||
"prefix": "muscular male,",
|
||||
"number": [0, 2],
|
||||
"tags": ["thick arms", "large hands"]
|
||||
},
|
||||
"giant": {
|
||||
"prefix": "giant male, size difference,",
|
||||
"number": [0, 2],
|
||||
"tags": ["thick arms", "large hands", "muscular male"]
|
||||
},
|
||||
"fat": "fat male"
|
||||
}
|
||||
},
|
||||
"penis": {
|
||||
"show": false,
|
||||
"tags": "{disembodied_penis}, {penis_size} penis"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "🎨 Style",
|
||||
"tags": {
|
||||
"quality": {
|
||||
"group_labels": true,
|
||||
"tags": {
|
||||
"masterpiece": "masterpiece, best quality, absurdres",
|
||||
"masterpiece+": "masterpiece, best quality, absurdres, amazing quality, very aesthetic, newest, volumetric lighting,",
|
||||
"score_up": "score_9, score_8_up, score_7_up"
|
||||
}
|
||||
},
|
||||
"lineart": {
|
||||
"probability": 0.5,
|
||||
"number": [1, 2],
|
||||
"tags": [
|
||||
"lineart",
|
||||
"thick outlines",
|
||||
"color trace",
|
||||
"colored lineart",
|
||||
"continuous line"
|
||||
]
|
||||
},
|
||||
"color_palette": {
|
||||
"probability": 0.2,
|
||||
"tags": [
|
||||
"monochrome",
|
||||
"flat colors",
|
||||
"partially colored",
|
||||
"pastel colors",
|
||||
"dark colors",
|
||||
"muted colors",
|
||||
"pale colors",
|
||||
"sepia",
|
||||
"saturated colors"
|
||||
]
|
||||
},
|
||||
"color_theme": {
|
||||
"probability": 0.05,
|
||||
"tags": "{color}",
|
||||
"suffix": " theme"
|
||||
},
|
||||
"light": {
|
||||
"probability": 0.5,
|
||||
"distribution": [0.2, 0.2, 0.2, 0.2],
|
||||
"tags": [
|
||||
"dim lighting",
|
||||
"sidelighting",
|
||||
"backlighting",
|
||||
"underlighting",
|
||||
"overexposure",
|
||||
"high contrast"
|
||||
]
|
||||
},
|
||||
"light_effect": {
|
||||
"number": [1, 3],
|
||||
"probability": 0.4,
|
||||
"tags": ["light particles", "lens flare", "bloom", "drop shadow"]
|
||||
},
|
||||
"depth of field": {
|
||||
"probability": 0.4,
|
||||
"tags": {
|
||||
"depth of field": {
|
||||
"number": [1, 2],
|
||||
"tags": ["depth of field", "bokeh", "blurry background", "bloom"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"group_labels": true,
|
||||
"prefix": "{quality},",
|
||||
"tags": {
|
||||
"lineart": "lineart,continuous line",
|
||||
"doujinshi": "{lineart},monochrome,grayscale,splash page",
|
||||
"unicolor": "monochrome,{color_theme}"
|
||||
}
|
||||
},
|
||||
"custom_output": "{quality},{lineart},{color_palette},{color_theme},{light},{light_effect}"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "🚫 Negative prompt",
|
||||
"output": ""
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "✅ Positive prompt",
|
||||
"output": "{character},{clothes}"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"triggers": ["*rainy*"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "add",
|
||||
"value": "wet, wet hair"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"triggers": ["disembodied penis"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "remove",
|
||||
"value": ["*male*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["giant male"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "small penis",
|
||||
"new_value": "penis"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,214 @@
|
||||
[
|
||||
{
|
||||
"triggers": ["original", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "{clothes_type}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["original"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "remove",
|
||||
"value": "original"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["2b", "nier automata", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "black dress, black thighhighs, high heel, thigh boots"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["android 18", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "blue jacket, denim jacket, black tank top, white sleeves, stripped sleeves, cleavage, blue jeans"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["bowsette", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "black dress, sleevless dress, off-shoulder dress, long dress"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["chel", "el dorado", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "pink tube top, loin cloth, pelvic curtain"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["chun-li", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "blue dress, china dress, puffy short sleeves, side slit, brown pantyhose"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["hinata hyuga", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "purple jacket, white sleeves, dark blue pants"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["jinx", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "brown crop top, purple pants, striped pants"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["kim possible", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "black crop top, long sleeve, gray gloves, green cargo pants"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["lara croft", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "cyan tank top, brown shorts, short shorts, brown belt, thigh straps, holster"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["princess daisy", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "yellow dress, white gloves"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["princess zelda", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": {
|
||||
"tags": [
|
||||
"blue shirt, white collar, golden belt, black pants",
|
||||
"pink dress, white dress, long dress, bracelet"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["raven", "dc comics", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "purple cloak, hooded cloak, black leotard, long sleeves"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["sakura haruno", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "red dress, sleevless dress, short dress, black shorts, black gloves"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["samus aran", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "zero suit, blue bodysuit, long sleeve,"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["tsunade", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "green coat, gray kimono, cleavage, blue belt, blue shorts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["velma", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "orange sweater, orange turtleneck, red skirt, short skirt, orange socks, kneehighs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["princess peach", "official costume"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "official costume",
|
||||
"new_value": "pink dress, white gloves, crown"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,95 @@
|
||||
[
|
||||
{
|
||||
"triggers": ["*anal*"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "add",
|
||||
"value": "anus"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["*fellatio*"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "remove",
|
||||
"value": ["*mouth*", "*smile*"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["imminent fellatio"],
|
||||
"actions": [
|
||||
{
|
||||
"type": "add",
|
||||
"value": {
|
||||
"distribution": [0.7, 0.3],
|
||||
"number": [1, 2],
|
||||
"tags": ["licking penis", "tongue out"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["*fellatio*", "climax"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "replace",
|
||||
"value": "fellatio",
|
||||
"new_value": {
|
||||
"tags": {
|
||||
"fellatio_state": ["fellatio", "irrumatio", "deepthroat"],
|
||||
"head_gesture": {
|
||||
"probability": 0.5,
|
||||
"tags": "{prelim_head_grab}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "add",
|
||||
"value": {
|
||||
"number": 1,
|
||||
"tags": {
|
||||
"cum_inside": {
|
||||
"prefix": "cum in mouth,",
|
||||
"probability": 0.3,
|
||||
"tags": "cum in nose"
|
||||
},
|
||||
"cum_outside": {
|
||||
"prefix": "penis head",
|
||||
"number": 3,
|
||||
"tags": {
|
||||
"handjob": "handjob,penis grab",
|
||||
"ejaculation": {
|
||||
"number": [1, 2],
|
||||
"tags": ["ejaculation", "projectile cum"]
|
||||
},
|
||||
"facial": {
|
||||
"number": [1, 3],
|
||||
"distribution": [0.5, 0.25, 0.25],
|
||||
"tags": ["facial", "cum on hair", "cum on tongue"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"triggers": ["*paizuri*", "climax"],
|
||||
"logic": "AND",
|
||||
"actions": [
|
||||
{
|
||||
"type": "add",
|
||||
"value": {
|
||||
"number": [1, 3],
|
||||
"distribution": [0.5, 0.25, 0.25],
|
||||
"tags": ["cum on breasts", "cum on body", "cum on stomach"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"color": {
|
||||
"fixed": false,
|
||||
"tags": [
|
||||
"aqua",
|
||||
"black",
|
||||
"blue",
|
||||
"brown",
|
||||
"gray",
|
||||
"green",
|
||||
"pink",
|
||||
"purple",
|
||||
"red",
|
||||
"white",
|
||||
"yellow"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"head_grab": [
|
||||
"hand on another's head",
|
||||
"hands on another's head",
|
||||
"head grab",
|
||||
"grabbing another's hair"
|
||||
]
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
# SFW
|
||||
[sfw]
|
||||
positions = ["standing", "sitting", "walking", "running", "lying"]
|
||||
gestures = ["waving", "adjusting hair", "holding hand", "drinking", "reading"]
|
||||
|
||||
# NSFW
|
||||
[nsfw.settings]
|
||||
insertions = ["vaginal", "anal"]
|
||||
|
||||
# Male
|
||||
hide_male = false
|
||||
penis_sizes = ["small[0.5]", "", "large", "huge[0.5]"]
|
||||
testicles_sizes = ["small[0.5]", "", "large", "huge[0.5]"]
|
||||
|
||||
# Intensity (float number)
|
||||
sweat = 1.0
|
||||
cum = 1.0
|
||||
pussy_juice = 1.0
|
||||
|
||||
# NSFW Actions
|
||||
[nsfw.acts]
|
||||
teasing = ["flashing"]
|
||||
preliminaries = ["fingering", "handjob", "fellatio", "licking penis", "paizuri"]
|
||||
intercourses = [
|
||||
"doggystyle",
|
||||
"cowgirl",
|
||||
"reversed cowgirl",
|
||||
"piledriver",
|
||||
"suspended congress",
|
||||
"full nelson",
|
||||
]
|
||||
@@ -1,88 +0,0 @@
|
||||
# BODY
|
||||
[body.colors]
|
||||
probability = 0.4
|
||||
list = ["standard_colors", "special_colors[0.5]"]
|
||||
standard_colors = ["dark[1.5]", "pale"]
|
||||
special_colors = ["black", "red", "green", "blue", "purple", "pink"]
|
||||
|
||||
[body.types]
|
||||
probability = 0.5
|
||||
tags = [
|
||||
"petite",
|
||||
"slim",
|
||||
"skinny",
|
||||
"athletic",
|
||||
"muscular",
|
||||
"curvy",
|
||||
"plump",
|
||||
"chubby[0.2]",
|
||||
"fat[0.1]",
|
||||
]
|
||||
|
||||
[body.breasts]
|
||||
sizes = [
|
||||
"flat chest",
|
||||
"small breasts",
|
||||
"medium breasts",
|
||||
"large breasts",
|
||||
"huge breasts[0.5]",
|
||||
]
|
||||
|
||||
[body.breasts.nipples]
|
||||
probability = 0.5
|
||||
repeat = [1, 2]
|
||||
tags = ["dark", "puffy"]
|
||||
|
||||
[body.extras]
|
||||
probability = 0.75
|
||||
repeat = [1, 3]
|
||||
tags = ["freckles[2]", "tattoos", "piercings", "scars[0.2]"]
|
||||
|
||||
# HAIR
|
||||
[hair]
|
||||
lengths = ["short", "medium", "long", "very long"]
|
||||
|
||||
[hair.styles]
|
||||
probability = 0.8
|
||||
list = ["styles", "appearances"]
|
||||
styles = ["ponytail", "bun", "braid", "pigtails"]
|
||||
appearances = ["curly", "straight", "wavy"]
|
||||
|
||||
[hair.colors]
|
||||
list = ["standard_colors", "special_colors[0.5]"]
|
||||
standard_colors = ["black", "brown", "blonde", "ginger"]
|
||||
special_colors = [
|
||||
"red",
|
||||
"blue",
|
||||
"aqua",
|
||||
"green",
|
||||
"grey",
|
||||
"white",
|
||||
"silver",
|
||||
"pink",
|
||||
"purple",
|
||||
]
|
||||
|
||||
# EYES & EYEWEAR
|
||||
[eyes.colors]
|
||||
list = ["standard_colors", "special_colors[0.8]"]
|
||||
standard_colors = ["black", "brown", "blue", "green"]
|
||||
special_colors = ["red", "yellow", "orange", "pink", "purple", "grey", "white"]
|
||||
|
||||
[eyes.eyewears]
|
||||
probability = 0.4
|
||||
types = ["glasses", "sunglasses", "round glasses", "googles"]
|
||||
colors = [
|
||||
"aqua",
|
||||
"black",
|
||||
"blue",
|
||||
"brown",
|
||||
"green",
|
||||
"grey",
|
||||
"orange",
|
||||
"pink",
|
||||
"purple",
|
||||
"red",
|
||||
"white",
|
||||
"yellow",
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
# State: how clothed the character is
|
||||
states = ["clothed", "underwear[0.3]", "nude[0.1]"]
|
||||
|
||||
# Types: if clothed, what type of clothing
|
||||
types = ["casual", "dress", "swimsuit", "uniform"]
|
||||
|
||||
# Colors apply for all piece of clothing
|
||||
colors = [
|
||||
"white",
|
||||
"grey",
|
||||
"black",
|
||||
"brown",
|
||||
"red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"blue",
|
||||
"aqua",
|
||||
"purple",
|
||||
"pink",
|
||||
]
|
||||
|
||||
# Swimsuit & Uniforms
|
||||
swimsuit = ["bikini", "one-piece swimsuit"]
|
||||
uniform = ["school", "sport", "military", "maid"]
|
||||
|
||||
# Casual clothing
|
||||
[casual]
|
||||
top = ["shirt", "blouse", "sweater", "hoodie"]
|
||||
bottom = ["jeans", "pants", "shorts", "skirt"]
|
||||
|
||||
[casual.vest]
|
||||
probability = 0.3
|
||||
tags = ["vest", "coat", "trenchoat", "jacket"]
|
||||
|
||||
# Dress
|
||||
[dress]
|
||||
length = ["short", "long"]
|
||||
|
||||
[dress.style]
|
||||
probability = 0.5
|
||||
tags = ["off-shoulder", "backless"]
|
||||
@@ -1,14 +0,0 @@
|
||||
prefix = "score_9, score_8_up, score_7_up, source_anime"
|
||||
#negative = "score_6, score_5, score_4"
|
||||
protagonists = ["1girl", "1girl, 1boy"]
|
||||
|
||||
[camera]
|
||||
framings = ["close-up", "portrait", "upper body", "full body"]
|
||||
angles = [
|
||||
"from above",
|
||||
"from below",
|
||||
"from side",
|
||||
"from behind",
|
||||
"straight on",
|
||||
"pov",
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
time = ["day", "night", "dawn", "dusk", "sunset", "sunrise"]
|
||||
weather = ["sunny", "cloudy", "rainy", "snowy"]
|
||||
|
||||
[locations]
|
||||
list = ["indoors", "outdoors"]
|
||||
|
||||
[locations.indoors]
|
||||
list = ["house", "public"]
|
||||
house = ["bedroom", "living room", "kitchen", "bathroom"]
|
||||
public = ["office", "library"]
|
||||
|
||||
[locations.outdoors]
|
||||
tags = ["forest", "mountain", "beach"]
|
||||
|
||||
[effects]
|
||||
probability = 0.5
|
||||
repeat = [1, 2]
|
||||
tags = ["light particles", "light rays"]
|
||||
@@ -1,153 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
let origProps = {};
|
||||
|
||||
const findWidgetByName = (node, name) => {
|
||||
return node.widgets ? node.widgets.find(w => w.name === name) : null;
|
||||
};
|
||||
|
||||
const doesInputWithNameExist = (node, name) => {
|
||||
return node.inputs ? node.inputs.some(input => input.name === name) : false;
|
||||
};
|
||||
|
||||
const HIDDEN_TAG = "cschide";
|
||||
|
||||
// Toggle Widget + change size
|
||||
function toggleWidget(node, widget, show = false, suffix = "") {
|
||||
if (!widget || doesInputWithNameExist(node, widget.name)) return;
|
||||
|
||||
// Store the original properties of the widget if not already stored
|
||||
if (!origProps[widget.name]) {
|
||||
origProps[widget.name] = {
|
||||
origType: widget.type,
|
||||
origComputeSize: widget.computeSize,
|
||||
};
|
||||
}
|
||||
|
||||
const origSize = node.size;
|
||||
|
||||
// Set the widget type and computeSize based on the show flag
|
||||
widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix;
|
||||
widget.computeSize = show
|
||||
? origProps[widget.name].origComputeSize
|
||||
: () => [0, -4];
|
||||
|
||||
// Calculate the new height for the node based on its computeSize method
|
||||
const newHeight = node.computeSize()[1];
|
||||
node.setSize([node.size[0], newHeight]);
|
||||
}
|
||||
|
||||
// Create a map of node titles to their respective widget handlers
|
||||
const nodeWidgetHandlers = {
|
||||
"🎬 Action": {
|
||||
nsfw: handleSceneNsfw,
|
||||
},
|
||||
};
|
||||
|
||||
// In the main function where widgetLogic is called
|
||||
function widgetLogic(node, widget) {
|
||||
// Retrieve the handler for the current node title and widget name
|
||||
const handler = nodeWidgetHandlers[node.comfyClass]?.[widget.name];
|
||||
if (handler) {
|
||||
handler(node, widget);
|
||||
}
|
||||
}
|
||||
|
||||
// Show SFW/NSFW widgets according to the value of the "nsfw" widget
|
||||
function handleSceneNsfw(node, widget) {
|
||||
const isNsfw = widget.value;
|
||||
const sfwWidgets = [
|
||||
findWidgetByName(node, `position`),
|
||||
findWidgetByName(node, `gesture`),
|
||||
];
|
||||
const nsfwWidgets = [
|
||||
findWidgetByName(node, `act_type`),
|
||||
findWidgetByName(node, `act`),
|
||||
];
|
||||
const allWidgets = [...sfwWidgets, ...nsfwWidgets];
|
||||
|
||||
// Hide all widgets
|
||||
for (const w of allWidgets) {
|
||||
toggleWidget(node, w, false);
|
||||
}
|
||||
|
||||
// Show the appropriate SFW/NSFW widgets
|
||||
const widgetsToToggle = isNsfw ? nsfwWidgets : sfwWidgets;
|
||||
for (const w of widgetsToToggle) {
|
||||
toggleWidget(node, w, true);
|
||||
}
|
||||
|
||||
if (isNsfw) {
|
||||
// Dynamic act list based on the selected act_type
|
||||
const actWidget = findWidgetByName(node, `act`);
|
||||
const actTypeWidget = findWidgetByName(node, `act_type`);
|
||||
filterInputList(node, actWidget, actTypeWidget);
|
||||
actTypeWidget.callback = () =>
|
||||
filterInputList(node, actWidget, actTypeWidget);
|
||||
}
|
||||
}
|
||||
|
||||
function filterInputList(node, actWidget, actTypeWidget) {
|
||||
// Restore original value
|
||||
if (origProps[actWidget.name].originalValues) {
|
||||
actWidget.options.values = origProps[actWidget.name].originalValues;
|
||||
}
|
||||
|
||||
// Store original values before filtering
|
||||
if (origProps[actWidget.name]) {
|
||||
origProps[actWidget.name].originalValues = [...actWidget.options.values];
|
||||
}
|
||||
|
||||
// Hide act widget if act_type is random
|
||||
toggleWidget(node, actWidget, actTypeWidget.value !== "random");
|
||||
|
||||
// Filter the act list based on the selected act_type
|
||||
actWidget.options.values = actWidget.options.values.filter(
|
||||
act => act.startsWith(`${actTypeWidget.value}_`) || act == "random"
|
||||
);
|
||||
|
||||
// Set the value to the first act in the list
|
||||
actWidget.value = actWidget.options.values[0];
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "scenecomposer.widgethider",
|
||||
nodeCreated(node) {
|
||||
for (const w of node.widgets || []) {
|
||||
let widgetValue = w.value;
|
||||
|
||||
// Store the original descriptor if it exists
|
||||
let originalDescriptor = Object.getOwnPropertyDescriptor(w, "value");
|
||||
if (!originalDescriptor) {
|
||||
originalDescriptor = Object.getOwnPropertyDescriptor(
|
||||
w.constructor.prototype,
|
||||
"value"
|
||||
);
|
||||
}
|
||||
|
||||
widgetLogic(node, w);
|
||||
|
||||
Object.defineProperty(w, "value", {
|
||||
get() {
|
||||
// If there's an original getter, use it. Otherwise, return widgetValue.
|
||||
let valueToReturn =
|
||||
originalDescriptor && originalDescriptor.get
|
||||
? originalDescriptor.get.call(w)
|
||||
: widgetValue;
|
||||
|
||||
return valueToReturn;
|
||||
},
|
||||
set(newVal) {
|
||||
// If there's an original setter, use it. Otherwise, set widgetValue.
|
||||
if (originalDescriptor && originalDescriptor.set) {
|
||||
originalDescriptor.set.call(w, newVal);
|
||||
} else {
|
||||
widgetValue = newVal;
|
||||
}
|
||||
|
||||
widgetLogic(node, w);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import argparse
|
||||
|
||||
from py.nodes.node_factory import NodeFactory
|
||||
from py.utils.config import load_config
|
||||
|
||||
from py.nodes.apply_rules import ApplyRules
|
||||
|
||||
config = load_config()
|
||||
|
||||
|
||||
def main(seed, verbose, with_inputs, with_rules, node_output):
|
||||
|
||||
rules = ApplyRules()
|
||||
|
||||
# Dislpay seed
|
||||
if 0 != seed:
|
||||
print(f"SEED: {seed}")
|
||||
print("---")
|
||||
|
||||
# Create prompt for each node
|
||||
prompts = []
|
||||
nodes_variables = {}
|
||||
for key, value in config.items():
|
||||
ClassNode = NodeFactory.create_node(key)
|
||||
node = ClassNode()
|
||||
|
||||
node_name = value.get("name", key)
|
||||
inputs = node.INPUT_TYPES()["required"]
|
||||
if "seed" in inputs:
|
||||
del inputs["seed"]
|
||||
prompt = node.build_prompt(seed=seed)
|
||||
|
||||
if verbose:
|
||||
print(f"{node_name:<20} {prompt[0]}")
|
||||
|
||||
# Optionally display inputs
|
||||
if with_inputs:
|
||||
for sub_key, sub_value in inputs.items():
|
||||
if isinstance(sub_value[0], list):
|
||||
sub_value = sub_value[0]
|
||||
elif isinstance(sub_value[0], str):
|
||||
sub_value = sub_value[1]["default"]
|
||||
if sub_value is True:
|
||||
sub_value = "Boolean"
|
||||
print(f"> {sub_key:<20} {sub_value}")
|
||||
print("---")
|
||||
nodes_variables.update(prompt[1])
|
||||
nodes_variables.update({node.__class__.__name__: prompt[0]})
|
||||
prompts.append(prompt[0])
|
||||
|
||||
# Final prompt output
|
||||
if node_output is not False:
|
||||
OutputNode = NodeFactory.create_node(node_output)
|
||||
output_node = OutputNode()
|
||||
prompt = output_node.build_prompt(
|
||||
seed=seed, variables=nodes_variables)[0]
|
||||
else:
|
||||
prompt = ",".join(prompts)
|
||||
|
||||
if with_rules:
|
||||
prompt = rules.apply_rules(prompt, "all", seed)
|
||||
|
||||
print(prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate a prompt from a node."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-s", "--seed",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Seed value for random number generator"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="Split each node's prompt"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-i", "--inputs",
|
||||
action="store_true",
|
||||
help="Display inputs for each node"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-r", "--rules",
|
||||
action="store_true",
|
||||
help="Apply rules"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-n", "--node",
|
||||
type=str,
|
||||
default=False,
|
||||
help="Name of the node to output"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main(
|
||||
seed=args.seed,
|
||||
verbose=args.verbose,
|
||||
with_inputs=args.inputs,
|
||||
with_rules=args.rules,
|
||||
node_output=args.node
|
||||
)
|
||||
@@ -1,176 +0,0 @@
|
||||
from ..node import Node
|
||||
from ..utils import is_true
|
||||
|
||||
|
||||
class Action(Node):
|
||||
|
||||
def __init__(self, seed=0):
|
||||
super().__init__(seed, data_file="actions.toml")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
inputs = super().INPUT_TYPES()
|
||||
seed = inputs["required"]["seed"]
|
||||
data = cls().data
|
||||
|
||||
positions = cls().build_inputs_list(data["sfw"]["positions"])
|
||||
gestures = cls().build_inputs_list(data["sfw"]["gestures"])
|
||||
act_types = cls().build_inputs_list(data["nsfw"]["acts"].keys())
|
||||
|
||||
# Add act_type as prefix to each act in the list
|
||||
# (This will be filtered by the front-end, according to the act_type)
|
||||
acts = []
|
||||
for act_type in act_types:
|
||||
if act_type == "random":
|
||||
continue
|
||||
# If act_type is not in the list, select a random one
|
||||
if act_type not in data["nsfw"]["acts"]:
|
||||
act_type = cls().select_tags(
|
||||
tags=data["nsfw"]["acts"],
|
||||
selected=act_type,
|
||||
recursive=False
|
||||
)
|
||||
for act in data["nsfw"]["acts"][act_type]:
|
||||
acts.extend([f"{act_type}_{act}"])
|
||||
acts = cls().build_inputs_list(acts)
|
||||
|
||||
# Update the required inputs
|
||||
inputs["required"] = {
|
||||
"nsfw": ("BOOLEAN", {"default": False}),
|
||||
"position": (positions,),
|
||||
"gesture": (gestures,),
|
||||
"act_type": (act_types,),
|
||||
"act": (acts,),
|
||||
"seed": seed
|
||||
}
|
||||
|
||||
return inputs
|
||||
|
||||
def build_prompt(self, seed, nsfw, position, gesture, act_type, act):
|
||||
|
||||
self.seed = seed
|
||||
prompt = ""
|
||||
|
||||
if nsfw:
|
||||
prompt = self.build_nsfw_action(act_type, act)
|
||||
else:
|
||||
prompt = self.build_sfw_action(position, gesture)
|
||||
|
||||
return (prompt,)
|
||||
|
||||
def build_sfw_action(self, position, gesture):
|
||||
"""Build a safe-for-work action prompt"""
|
||||
data = self.data["sfw"]
|
||||
|
||||
position = self.select_tags(
|
||||
tags=data["positions"],
|
||||
selected=position
|
||||
)
|
||||
|
||||
gesture = self.select_tags(
|
||||
tags=data["gestures"],
|
||||
selected=gesture,
|
||||
seed=self.seed+1
|
||||
)
|
||||
|
||||
prompt = f"{position}, {gesture}"
|
||||
return prompt
|
||||
|
||||
def build_nsfw_action(self, act_type, act):
|
||||
"""Build a not-safe-for-work action prompt"""
|
||||
data = self.data["nsfw"]
|
||||
|
||||
act_type = self.select_tags(
|
||||
tags=data["acts"],
|
||||
selected=act_type,
|
||||
recursive=False
|
||||
)
|
||||
|
||||
act = self.select_tags(
|
||||
tags=data["acts"][act_type],
|
||||
selected=act,
|
||||
seed=self.seed+1
|
||||
)
|
||||
|
||||
settings = self.apply_settings(data["settings"])
|
||||
prompt = self.enhance_prompt(data["acts"], act, settings)
|
||||
return prompt
|
||||
|
||||
def apply_settings(self, data):
|
||||
"""Apply NSFW settings"""
|
||||
settings = {}
|
||||
|
||||
for key, value in data.items():
|
||||
settings[key] = self.select_tags(data[key])
|
||||
|
||||
return settings
|
||||
|
||||
def enhance_prompt(self, data, act_type, settings):
|
||||
"""Enhance the NSFW action.
|
||||
Add or remove tags depending on the action type"""
|
||||
|
||||
act = act_type
|
||||
if act_type in data:
|
||||
act = self.select_tags(data[act_type])
|
||||
|
||||
penis, pussy = self.build_sex_parts(settings)
|
||||
sweat = "sweat"
|
||||
|
||||
# Enhance act
|
||||
if act_type in data["intercourses"]:
|
||||
insertion = self.select_tags(settings["insertions"])
|
||||
act += f", {insertion}"
|
||||
|
||||
# Enhance preliminary
|
||||
if act_type in data["preliminaries"]:
|
||||
if "fingering" in act_type:
|
||||
penis = ""
|
||||
if is_true(self.seed, 0.5):
|
||||
act += ", fingering from behind"
|
||||
else:
|
||||
pussy = ""
|
||||
|
||||
# Enhance teasing
|
||||
if "flashing" in act_type:
|
||||
# TODO: Add random body part flashing
|
||||
penis = ""
|
||||
pussy = ""
|
||||
act += ", nipples, clothes lift"
|
||||
|
||||
components = [act, penis, pussy, sweat]
|
||||
act = self.stringify_tags(components)
|
||||
return act
|
||||
|
||||
def build_sex_parts(self, settings):
|
||||
"""Build sex parts, including penis, testicles, pussy, etc."""
|
||||
|
||||
# Penis
|
||||
penis = f"{settings['penis_sizes']} penis"
|
||||
|
||||
if settings["hide_male"]:
|
||||
penis += ", disembodied penis"
|
||||
|
||||
# Testicles
|
||||
testicles = ""
|
||||
if is_true(self.seed, 0.3):
|
||||
testicles = f"{settings['testicles_sizes']} testicles"
|
||||
|
||||
# Pussy
|
||||
pussy = "pussy"
|
||||
pussy_juice = "pussy juice"
|
||||
pussy_juice_weight = settings["pussy_juice"]
|
||||
|
||||
if pussy_juice_weight >= 1:
|
||||
additional_tags = {
|
||||
"repeat": [1, 2],
|
||||
"tags": ["pussy juice trail", "pussy juice puddle"]
|
||||
}
|
||||
pussy_juice += ", " + self.select_tags(additional_tags)
|
||||
|
||||
if pussy_juice_weight >= 1.1:
|
||||
pussy_juice = f"excessive {pussy_juice}"
|
||||
|
||||
# Merge
|
||||
penis = self.stringify_tags([penis, testicles])
|
||||
pussy = self.stringify_tags([pussy, pussy_juice])
|
||||
return penis, pussy
|
||||
@@ -0,0 +1,25 @@
|
||||
from ..utils.config import load_config
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
"""
|
||||
Ensure that only one instance of the class exists.
|
||||
"""
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *arg, **kw):
|
||||
if cls not in cls._instances:
|
||||
instance = super().__call__(*arg, **kw)
|
||||
cls._instances[cls] = instance
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class Blackboard(metaclass=Singleton):
|
||||
"""
|
||||
Store data so all nodes can modify and access it.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.variables = load_config("variables", with_filename=False)
|
||||
self.config = load_config("nodes")
|
||||
self.rules = load_config("rules")
|
||||
@@ -1,23 +0,0 @@
|
||||
from ...node import Node
|
||||
|
||||
|
||||
class Body(Node):
|
||||
|
||||
def __init__(self, seed):
|
||||
super().__init__(seed, data_file="character.toml")
|
||||
|
||||
def build_prompt(self):
|
||||
data = self.data["body"]
|
||||
type = self.select_tags(data["types"])
|
||||
colors = self.select_tags(data["colors"])
|
||||
color = f"{colors} skin" if colors else ""
|
||||
|
||||
breasts_data = self.data["body"]["breasts"]
|
||||
breasts_size = self.select_tags(breasts_data["sizes"])
|
||||
breasts_prompt = f"{breasts_size}"
|
||||
|
||||
extras = self.select_tags(data["extras"])
|
||||
|
||||
components = [type, color, breasts_prompt, extras]
|
||||
prompt = self.stringify_tags(components)
|
||||
return (prompt,)
|
||||
@@ -1,20 +0,0 @@
|
||||
from ...node import Node
|
||||
|
||||
from .body import Body
|
||||
from .hair import Hair
|
||||
from .eyes import Eyes
|
||||
|
||||
|
||||
class Character(Node):
|
||||
|
||||
def build_prompt(self, seed):
|
||||
|
||||
components = {
|
||||
'body': Body(seed),
|
||||
'hair': Hair(seed),
|
||||
'eyes': Eyes(seed)
|
||||
}
|
||||
|
||||
prompt = ", ".join(str(component) for component in components.values())
|
||||
prompt = str(prompt)
|
||||
return (prompt,)
|
||||
@@ -1,34 +0,0 @@
|
||||
from ...node import Node
|
||||
from ...utils import is_true
|
||||
|
||||
|
||||
class Eyes(Node):
|
||||
|
||||
def __init__(self, seed):
|
||||
super().__init__(seed, data_file="character.toml")
|
||||
|
||||
def build_prompt(self):
|
||||
data = self.data["eyes"]
|
||||
suffix = "eyes"
|
||||
color = self.select_tags(data["colors"])
|
||||
eyewear = self.build_eyewear_prompt(data["eyewears"])
|
||||
|
||||
prompt = f"{color} {suffix}, {eyewear}"
|
||||
return (prompt,)
|
||||
|
||||
def build_eyewear_prompt(self, eyewear):
|
||||
p = eyewear["probability"]
|
||||
|
||||
suffix = "eyewear"
|
||||
type = self.select_tags(eyewear["types"], p)
|
||||
color = self.select_tags(eyewear["colors"], p)
|
||||
colored_element = self.select_tags(["framed", "tinted"])
|
||||
|
||||
tags = [
|
||||
f"{type}",
|
||||
f"{color}-{colored_element} {suffix}",
|
||||
]
|
||||
|
||||
eyewear_prompt = self.stringify_tags(
|
||||
tags) if is_true(self.seed, p) else ""
|
||||
return eyewear_prompt
|
||||
@@ -1,18 +0,0 @@
|
||||
from ...node import Node
|
||||
|
||||
|
||||
class Hair(Node):
|
||||
|
||||
def __init__(self, seed):
|
||||
super().__init__(seed, data_file="character.toml")
|
||||
|
||||
def build_prompt(self):
|
||||
data = self.data["hair"]
|
||||
suffix = "hair"
|
||||
|
||||
color = self.select_tags(data["colors"])
|
||||
length = self.select_tags(data["lengths"])
|
||||
style = self.select_tags(data["styles"])
|
||||
|
||||
prompt = f"{color} {suffix}, {length} {suffix}, {style}"
|
||||
return (prompt,)
|
||||
@@ -1,89 +0,0 @@
|
||||
from ..node import Node
|
||||
|
||||
|
||||
class Clothes(Node):
|
||||
|
||||
def __init__(self, seed=0, state="random", type="random"):
|
||||
super().__init__(seed, data_file="clothes.toml")
|
||||
self.state = state
|
||||
self.type = type
|
||||
|
||||
def build_prompt(self, seed):
|
||||
self.seed = seed
|
||||
prompt = ""
|
||||
|
||||
self.state = self.select_tags(self.data["states"], selected=self.state)
|
||||
self.type = self.select_tags(self.data["types"], selected=self.type)
|
||||
|
||||
match self.state:
|
||||
case "clothed":
|
||||
prompt = self.build_clothes()
|
||||
case "underwear":
|
||||
prompt = self.build_underwear()
|
||||
case "nude":
|
||||
prompt = "nude"
|
||||
|
||||
return (prompt,)
|
||||
|
||||
def build_clothes(self):
|
||||
prompt = ""
|
||||
match self.type:
|
||||
case "casual":
|
||||
vest = Piece(self.data, self.seed+1, "vest")
|
||||
top = Piece(self.data, self.seed+2, "top")
|
||||
bottom = Piece(self.data, self.seed+3, "bottom")
|
||||
prompt = f"{vest}, {top}, {bottom}"
|
||||
|
||||
case "dress":
|
||||
suffix = "dress"
|
||||
color = self.select_tags(self.data["colors"])
|
||||
length = self.select_tags(self.data["dress"]["length"])
|
||||
style = self.select_tags(self.data["dress"]["style"])
|
||||
prompt = (f"{color} {suffix}, "
|
||||
f"{length} {suffix}, "
|
||||
f"{style} {suffix}")
|
||||
|
||||
case "swimsuit":
|
||||
color = self.select_tags(self.data["colors"])
|
||||
type = self.select_tags(self.data["swimsuit"])
|
||||
prompt = f"{color} swimsuit, {type}"
|
||||
|
||||
case "uniform":
|
||||
color = self.select_tags(self.data["colors"])
|
||||
type = self.select_tags(self.data["uniform"])
|
||||
prompt = f"{color} uniform, {type} uniform"
|
||||
|
||||
case _:
|
||||
color = self.select_tags(self.data["colors"])
|
||||
type = self.select_tags(self.data[self.type])
|
||||
prompt = f"{color} {type}"
|
||||
return prompt
|
||||
|
||||
def build_underwear(self):
|
||||
color = self.select_tags(self.data["colors"])
|
||||
prompt = f"{color} bra, {color} panties"
|
||||
return prompt
|
||||
|
||||
|
||||
class Piece(Node):
|
||||
"""Return a colored piece of clothing"""
|
||||
|
||||
def __init__(self, data, seed, type):
|
||||
super().__init__(seed)
|
||||
self.data = data
|
||||
self.seed = seed
|
||||
self.type = type
|
||||
|
||||
def build_prompt(self):
|
||||
color = self.select_tags(self.data["colors"])
|
||||
type = self.select_tags(self.data["casual"][self.type])
|
||||
piece = f"{color} {type}"
|
||||
|
||||
if type == "":
|
||||
piece = ""
|
||||
|
||||
prompt = f"{piece}"
|
||||
return prompt
|
||||
|
||||
def __str__(self):
|
||||
return self.build_prompt()
|
||||
@@ -1,72 +0,0 @@
|
||||
from ..node import Node
|
||||
|
||||
|
||||
class Composition(Node):
|
||||
|
||||
def __init__(self, seed=0):
|
||||
super().__init__(seed, data_file="composition.toml")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
inputs = super().INPUT_TYPES()
|
||||
seed = inputs["required"]["seed"]
|
||||
data = cls(seed).data
|
||||
|
||||
prefix = data["prefix"]
|
||||
protagonists = cls().build_inputs_list(data["protagonists"])
|
||||
camera_framings = cls().build_inputs_list(data["camera"]["framings"])
|
||||
camera_angles = cls().build_inputs_list(data["camera"]["angles"])
|
||||
|
||||
inputs["required"] = {
|
||||
"prefix": ("STRING", {"default": prefix, "multiline": True}),
|
||||
"protagonists": (protagonists,),
|
||||
"camera_framing": (camera_framings,),
|
||||
"camera_angle": (camera_angles,),
|
||||
"seed": inputs["required"]["seed"]
|
||||
}
|
||||
return inputs
|
||||
|
||||
def build_prompt(self, seed, prefix, protagonists,
|
||||
camera_framing, camera_angle):
|
||||
|
||||
self.seed = seed
|
||||
|
||||
components = {
|
||||
"prefix": {
|
||||
"tags": self.data["prefix"],
|
||||
"selected": prefix
|
||||
},
|
||||
"protagonists": {
|
||||
"tags": self.data["protagonists"],
|
||||
"selected": protagonists
|
||||
},
|
||||
"camera_framing": {
|
||||
"tags": self.data["camera"]["framings"],
|
||||
"selected": camera_framing
|
||||
},
|
||||
"camera_angle": {
|
||||
"tags": self.data["camera"]["angles"],
|
||||
"selected": camera_angle
|
||||
}
|
||||
}
|
||||
|
||||
prompts = {}
|
||||
|
||||
# Select tags for each component
|
||||
for name, params in components.items():
|
||||
prompt = self.select_tags(
|
||||
tags=params["tags"],
|
||||
selected=params["selected"]
|
||||
)
|
||||
prompts[name] = prompt
|
||||
|
||||
# Protagonists enhancement
|
||||
match prompts["protagonists"]:
|
||||
case "1girl":
|
||||
prompts["protagonists"] = "1girl, solo"
|
||||
case "1girl, 1boy":
|
||||
prompts["protagonists"] = "1girl, 1boy, solo focus"
|
||||
|
||||
# Build the prompt
|
||||
prompt = ", ".join(prompts.values())
|
||||
return (prompt,)
|
||||
@@ -1,34 +0,0 @@
|
||||
from ..node import Node
|
||||
|
||||
|
||||
class Environment(Node):
|
||||
|
||||
def __init__(self, seed=0):
|
||||
super().__init__(seed, data_file="environment.toml")
|
||||
|
||||
def build_prompt(self, seed):
|
||||
self.seed = seed
|
||||
|
||||
time = self.select_tags(self.data["time"])
|
||||
location_type = self.select_tags(self.data["locations"]["list"])
|
||||
location = self.select_tags(self.data["locations"][location_type])
|
||||
effects = self.select_tags(self.data["effects"])
|
||||
weather = self.build_weather(location)
|
||||
|
||||
components = [time, effects, weather, location]
|
||||
|
||||
if location_type == "indoors":
|
||||
components.remove(weather)
|
||||
|
||||
prompt = ", ".join(components)
|
||||
return (prompt,)
|
||||
|
||||
def build_weather(self, location):
|
||||
weather = self.data["weather"]
|
||||
|
||||
if location == "beach":
|
||||
remove_from_list = ["snow", "rain"]
|
||||
weather = [w for w in weather if w not in remove_from_list]
|
||||
|
||||
prompt = self.select_tags(weather)
|
||||
return prompt
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
import numpy as np
|
||||
import toml
|
||||
|
||||
from .utils import ROOT_DIR
|
||||
|
||||
|
||||
class Node:
|
||||
"""ComfyUI Node parent class
|
||||
Will build and return a prompt according to other sub-components.
|
||||
"""
|
||||
|
||||
def __init__(self, seed=0, data_file=""):
|
||||
self.seed = seed
|
||||
self.data = self.load_data(data_file)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""ComfyUI node's inputs"""
|
||||
required = {
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xffffffffffffffff
|
||||
})
|
||||
}
|
||||
optional = {}
|
||||
inputs = {
|
||||
"required": required,
|
||||
"optional": optional
|
||||
}
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "build_prompt"
|
||||
CATEGORY = "🎞️ Scene Composer"
|
||||
|
||||
def build_prompt(self):
|
||||
pass
|
||||
|
||||
def select_tags(self, tags, p=1, n=1,
|
||||
selected="random", recursive=True, seed=None):
|
||||
"""Return n tags from a string, list or dict
|
||||
p: probability of selecting the tags
|
||||
n: number of tags to select
|
||||
selected: fallback tag to select if not random
|
||||
recursive: go deeper randomly to select last children tags"""
|
||||
if seed is None:
|
||||
seed = self.seed
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
if selected != "random":
|
||||
return selected
|
||||
|
||||
if isinstance(tags, dict):
|
||||
|
||||
p = tags.get("probability", p)
|
||||
n = tags.get("repeat", n)
|
||||
|
||||
if rng.random() > p:
|
||||
return ""
|
||||
|
||||
# If n is a list, choose a random number between the 2 first values
|
||||
if isinstance(n, list):
|
||||
min_n = n[0]
|
||||
max_n = min(n[1], len(tags))
|
||||
n = rng.integers(int(min_n), int(max_n))
|
||||
|
||||
# Handle "list" and "tags" special keys
|
||||
if "list" in tags:
|
||||
selected_from_list = self.select_tags(tags["list"], p, n)
|
||||
tags = tags[selected_from_list]
|
||||
|
||||
elif "tags" in tags:
|
||||
tags = tags.get("tags", [])
|
||||
|
||||
# Handle recursive tags
|
||||
else:
|
||||
first_level_keys = list(tags.keys())
|
||||
chosen_key = rng.choice(first_level_keys)
|
||||
chosen_value = tags[chosen_key]
|
||||
|
||||
if isinstance(chosen_value, (list, dict)):
|
||||
tags = self.select_tags(chosen_value, p, n)
|
||||
|
||||
if not recursive:
|
||||
tags = first_level_keys
|
||||
|
||||
if isinstance(tags, list):
|
||||
tag_names, weights = self.parse_tag_distribution(tags)
|
||||
|
||||
selected_tags = rng.choice(
|
||||
tag_names,
|
||||
size=n,
|
||||
replace=False,
|
||||
p=weights
|
||||
)
|
||||
tags = self.stringify_tags(selected_tags)
|
||||
|
||||
return tags
|
||||
|
||||
@staticmethod
|
||||
def parse_tag_distribution(tags):
|
||||
"""Choose tags randomly according to defined weights
|
||||
If none are defined, tag weight is 1
|
||||
Example: "foo[1.5], bar[0.5]" -> 75% chance of foo, 25% chance of bar
|
||||
"""
|
||||
tag_names = []
|
||||
weights = []
|
||||
for tag in tags:
|
||||
if '[' in tag and ']' in tag:
|
||||
tag_name, weight = tag.split('[')
|
||||
weight = weight.rstrip(']')
|
||||
tag_names.append(tag_name)
|
||||
weights.append(float(weight))
|
||||
else:
|
||||
tag_names.append(tag)
|
||||
weights.append(1)
|
||||
probabilities = np.array(weights) / sum(weights)
|
||||
return tag_names, probabilities
|
||||
|
||||
@staticmethod
|
||||
def load_data(filename):
|
||||
"""Load data from a TOML file"""
|
||||
path = f"{ROOT_DIR}/config/{filename}"
|
||||
try:
|
||||
with open(path) as file:
|
||||
data = toml.load(file)
|
||||
except Exception:
|
||||
data = {}
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def build_inputs_list(data):
|
||||
"""Build a list with a random option"""
|
||||
|
||||
inputs_list = ["random"]
|
||||
|
||||
for tag in data:
|
||||
if '[' in tag and ']' in tag:
|
||||
tag = tag.split('[')[0]
|
||||
inputs_list.append(tag)
|
||||
|
||||
return inputs_list
|
||||
|
||||
@staticmethod
|
||||
def stringify_tags(tags):
|
||||
"""Return a string from a list of tags"""
|
||||
tags = ", ".join(map(str, tags))
|
||||
tags = tags.replace(", ,", ",")
|
||||
return tags
|
||||
|
||||
def __str__(self, **kwargs):
|
||||
return self.build_prompt(**kwargs)[0]
|
||||
@@ -0,0 +1,189 @@
|
||||
import fnmatch
|
||||
import dpath
|
||||
import numpy as np
|
||||
|
||||
from ..utils.config import get_project_name
|
||||
from .node_factory._tags import choose_random_tags, stringify_tags
|
||||
from .node_factory._variables import apply_variables
|
||||
from ..components.blackboard import Blackboard
|
||||
|
||||
|
||||
class ApplyRules:
|
||||
"""
|
||||
A ComfyUI node to apply rules to a string.
|
||||
Input: string
|
||||
Output: filtered string
|
||||
"""
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
FUNCTION = "apply_rules"
|
||||
PROJECT_NAME = get_project_name()
|
||||
CATEGORY = f"{PROJECT_NAME}/🛠️ Utils"
|
||||
|
||||
def __init__(self):
|
||||
self.config = Blackboard().config.copy()
|
||||
self.rules = Blackboard().rules.copy()
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
instance = cls()
|
||||
rules = list(instance.rules.keys())
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"ruleset": (["all", *rules],),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xffffffffffffffff
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"variables": ("STRING", {"forceInput": True})
|
||||
}
|
||||
}
|
||||
|
||||
def apply_rules(self, prompt, ruleset, seed, variables=None):
|
||||
"""
|
||||
Apply the rules defined in config file.
|
||||
Add and remove tags in the prompt
|
||||
according to others tags in the same prompt
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
# Put prompt in a tag list
|
||||
tags = [tag.strip() for tag in prompt.replace(
|
||||
"\n", ", ").split(",") if tag.strip()]
|
||||
|
||||
# Merge rule config files
|
||||
if ruleset == "all":
|
||||
rules = [rule for rule_set in self.rules.values()
|
||||
for rule in rule_set]
|
||||
else:
|
||||
rules = self.rules.get(ruleset, [])
|
||||
|
||||
# Apply the rules
|
||||
for rule in rules:
|
||||
triggers = rule["triggers"]
|
||||
logic = rule.get("logic", "OR")
|
||||
actions = rule["actions"]
|
||||
p = rule.get("probability", 1)
|
||||
|
||||
if rng.random() > p:
|
||||
continue
|
||||
|
||||
# If any triggering tag occurs in the prompt,
|
||||
# run all the defined actions
|
||||
if logic == "OR":
|
||||
for trigger in triggers:
|
||||
if any(fnmatch.fnmatch(tag, trigger) for tag in tags):
|
||||
tags = run_actions(rng, actions, tags, self.config)
|
||||
continue
|
||||
|
||||
elif logic == "AND":
|
||||
if all(any(fnmatch.fnmatch(tag, trigger) for tag in tags) for trigger in triggers):
|
||||
tags = run_actions(rng, actions, tags, self.config)
|
||||
|
||||
if variables:
|
||||
tags = apply_variables(rng, tags, variables)
|
||||
|
||||
tags = stringify_tags(tags, separator=", ")
|
||||
return (tags,)
|
||||
|
||||
|
||||
def run_actions(rng, actions, tags, config):
|
||||
"""
|
||||
Run actions from the rule.
|
||||
Add or remove tags with filtering tags
|
||||
"""
|
||||
|
||||
process_tags = tags.copy()
|
||||
|
||||
for action in actions:
|
||||
|
||||
type = action.get("type", "add")
|
||||
value = action.get("value", [])
|
||||
p = action.get("probability", 1)
|
||||
|
||||
if rng.random() > p:
|
||||
continue
|
||||
|
||||
filtering_tags = process_action_value(value, config)
|
||||
|
||||
match type:
|
||||
|
||||
# Add tags at the end of the prompt
|
||||
case "add":
|
||||
value = choose_random_tags(rng, filtering_tags)
|
||||
process_tags.append(value)
|
||||
|
||||
# Remove any tag that matches the value
|
||||
case "remove":
|
||||
process_tags = [
|
||||
tag for tag in process_tags
|
||||
if not any(fnmatch.fnmatch(tag, v)
|
||||
for v in filtering_tags)]
|
||||
|
||||
# In any tag that matches the filter, replace the value with the new one
|
||||
case "replace":
|
||||
filter = process_action_value(
|
||||
action.get("filter", ["*"]), config)
|
||||
|
||||
new_value = action.get("new_value", [])
|
||||
new_value = choose_random_tags(rng, new_value)
|
||||
|
||||
# Handle string value
|
||||
if isinstance(value, str) and value != "*":
|
||||
value = [value]
|
||||
|
||||
# Handle list value
|
||||
if isinstance(value, list) and len(value) > 0:
|
||||
for v in value:
|
||||
process_tags = [
|
||||
tag.replace(v, new_value)
|
||||
if any(fnmatch.fnmatch(tag, pattern) for pattern in filter)
|
||||
else tag
|
||||
for tag in process_tags
|
||||
]
|
||||
|
||||
# For any other case, replace the whole tag
|
||||
else:
|
||||
process_tags = [new_value if any(fnmatch.fnmatch(
|
||||
tag, pattern) for pattern in filter) else tag for tag in process_tags]
|
||||
|
||||
return process_tags
|
||||
|
||||
|
||||
def process_action_value(value, config):
|
||||
"""
|
||||
Process the action's value.
|
||||
Return selected tags randomly or according to a given path
|
||||
"""
|
||||
|
||||
tags = []
|
||||
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
|
||||
if isinstance(value, str):
|
||||
value = [value]
|
||||
|
||||
# If the value is a path,
|
||||
# take all the tags from this path
|
||||
for v in value:
|
||||
if is_path(v):
|
||||
for k, i in dpath.search(config, v,
|
||||
yielded=True, dirs=False):
|
||||
if isinstance(i, str):
|
||||
tags.append(i)
|
||||
else:
|
||||
tags.append(v)
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def is_path(string):
|
||||
"Check if the value is a glob path"
|
||||
is_path = isinstance(string, str) and ("/" in string)
|
||||
return is_path
|
||||
@@ -0,0 +1,74 @@
|
||||
import numpy as np
|
||||
import fnmatch
|
||||
|
||||
from ..utils.config import get_project_name
|
||||
from .node_factory._tags import stringify_tags
|
||||
|
||||
|
||||
class CleanupPrompt:
|
||||
"""
|
||||
A ComfyUI node to clean up your prompt.
|
||||
Sort tags and remove duplications
|
||||
Inputs: string
|
||||
Output: string
|
||||
"""
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
FUNCTION = "cleanup_prompt"
|
||||
PROJECT_NAME = get_project_name()
|
||||
CATEGORY = f"{PROJECT_NAME}/🛠️ Utils"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"cleanup": ("BOOLEAN", {"default": True}),
|
||||
"sort": (["none", "asc", "desc", "random"],),
|
||||
"custom_sort": ("STRING", {"default": "", "multiline": True}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xffffffffffffffff
|
||||
})
|
||||
},
|
||||
"optional": {}
|
||||
}
|
||||
|
||||
def cleanup_prompt(self, prompt, cleanup, sort, custom_sort, seed):
|
||||
|
||||
tags = prompt.split(", ")
|
||||
|
||||
if cleanup is True:
|
||||
tags = self.remove_duplicates(tags)
|
||||
|
||||
match sort:
|
||||
|
||||
case "asc": tags = sorted(tags)
|
||||
case "desc": tags = sorted(tags, reverse=True)
|
||||
case "random":
|
||||
np.random.seed(seed)
|
||||
np.random.shuffle(tags)
|
||||
|
||||
if custom_sort != "":
|
||||
custom_sort = custom_sort.split(", ")
|
||||
custom_order_dict = {}
|
||||
for i, pattern in enumerate(custom_sort):
|
||||
for tag in tags:
|
||||
if fnmatch.fnmatch(tag, pattern):
|
||||
custom_order_dict[tag] = i
|
||||
|
||||
tags = sorted(tags, key=lambda x:
|
||||
custom_order_dict.get(x, len(custom_order_dict)))
|
||||
|
||||
prompt = stringify_tags(tags, ", ")
|
||||
return (prompt,)
|
||||
|
||||
def remove_duplicates(self, tags):
|
||||
seen = set()
|
||||
unique_tags = []
|
||||
for tag in tags:
|
||||
if tag not in seen:
|
||||
unique_tags.append(tag)
|
||||
seen.add(tag)
|
||||
return unique_tags
|
||||
@@ -0,0 +1,32 @@
|
||||
from ..utils.config import get_project_name
|
||||
|
||||
|
||||
class MergeStrings:
|
||||
"""
|
||||
A ComfyUI node to merge string.
|
||||
Inputs: strings (stacked)
|
||||
Output: merged string
|
||||
Check merge_strings.js for the frontend implementation.
|
||||
"""
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("string",)
|
||||
FUNCTION = "merge_strings"
|
||||
|
||||
PROJECT_NAME = get_project_name()
|
||||
CATEGORY = f"{PROJECT_NAME}/🛠️ Utils"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"separator": ("STRING", {"default": ", "})
|
||||
},
|
||||
"optional": {}
|
||||
}
|
||||
|
||||
def merge_strings(self, separator, **kwargs):
|
||||
value = separator.join(
|
||||
v for v in kwargs.values()
|
||||
if isinstance(v, str)
|
||||
)
|
||||
return (value,)
|
||||
@@ -0,0 +1,149 @@
|
||||
import numpy as np
|
||||
import json
|
||||
|
||||
from ...components.blackboard import Blackboard
|
||||
|
||||
from ...utils.config import get_project_name
|
||||
from ._inputs import build_inputs, apply_input_values
|
||||
from ._variables import apply_variables
|
||||
from ._tags import choose_random_tags, stringify_tags
|
||||
|
||||
|
||||
class NodeFactory:
|
||||
"""
|
||||
Create nodes dynamically according to config files.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.__class__.__name__.lower()
|
||||
self.data = Blackboard().config[self.name]
|
||||
self.variables = {}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
instance = cls()
|
||||
inputs = build_inputs(instance)
|
||||
inputs["required"]["seed"] = ("INT", {
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0xffffffffffffffff
|
||||
})
|
||||
inputs["optional"]["variables"] = ("STRING", {"forceInput": True})
|
||||
return inputs
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("prompt", "variables",)
|
||||
FUNCTION = "build_prompt"
|
||||
PROJECT_NAME = get_project_name()
|
||||
CATEGORY = f"{PROJECT_NAME}/⭐️ My Nodes"
|
||||
|
||||
def build_prompt(self, **args):
|
||||
"""
|
||||
Build the prompt according to the node inputs the user selected.
|
||||
Concatenate the tags and return the prompt.
|
||||
"""
|
||||
rng = np.random.default_rng(args["seed"])
|
||||
settings = self.data.get("settings", {})
|
||||
|
||||
# Apply node's input values
|
||||
tags_from_data = self.data.get("tags", {})
|
||||
tags_from_inputs = apply_input_values(data=tags_from_data, inputs=args)
|
||||
|
||||
# Process variables
|
||||
global_variables = Blackboard().variables.copy()
|
||||
received_variables = args.get("variables", {})
|
||||
|
||||
if isinstance(received_variables, str):
|
||||
if received_variables == "":
|
||||
received_variables = {}
|
||||
else:
|
||||
received_variables = json.loads(received_variables)
|
||||
|
||||
local_variables = self.data.get("variables", {})
|
||||
local_variables = apply_input_values(data=local_variables, inputs=args)
|
||||
|
||||
# Replace local {variables} with global variables
|
||||
for key, value in local_variables.items():
|
||||
local_variables[key] = apply_variables(
|
||||
rng, value, global_variables)
|
||||
|
||||
self.variables = {
|
||||
**global_variables,
|
||||
**received_variables,
|
||||
**local_variables
|
||||
}
|
||||
|
||||
share_variables = settings.get("share_variables", True)
|
||||
output_variables = settings.get("output_variables", False)
|
||||
tags = {}
|
||||
|
||||
for key, value in self.variables.items():
|
||||
|
||||
# Share this variable with next nodes
|
||||
share_variable = share_variables
|
||||
fixed_variable = True
|
||||
if isinstance(value, dict):
|
||||
share_variable = value.get("share", share_variables)
|
||||
fixed_variable = value.get("fixed", fixed_variable)
|
||||
|
||||
if fixed_variable:
|
||||
value = choose_random_tags(rng, value)
|
||||
if share_variable:
|
||||
self.variables.update({key: value})
|
||||
|
||||
# Add this variable to the prompt
|
||||
output_variable = output_variables
|
||||
if isinstance(value, dict):
|
||||
output_variable = value.get("output", output_variables)
|
||||
if output_variable:
|
||||
tags[key] = choose_random_tags(rng, value)
|
||||
|
||||
# Select tags randomly and update the variables
|
||||
rng_state = rng.bit_generator.state
|
||||
for key, value in tags_from_inputs.items():
|
||||
tags[key] = choose_random_tags(rng, value)
|
||||
apply_variables(rng, tags[key], self.variables)
|
||||
self.variables.update({key: tags[key]})
|
||||
rng.bit_generator.state = rng_state
|
||||
|
||||
# Apply variables to the tags
|
||||
tags = apply_variables(rng, tags, self.variables)
|
||||
prompt = stringify_tags(tags.values(), ", ")
|
||||
|
||||
# Handle output
|
||||
output_from_data = self.data.get("output", True)
|
||||
|
||||
match output_from_data:
|
||||
case True:
|
||||
output = prompt
|
||||
case False:
|
||||
output = ""
|
||||
case _:
|
||||
output_arg = args.get("output", "random")
|
||||
|
||||
if output_arg == "custom":
|
||||
output = args.get("custom_output", "")
|
||||
elif output_arg == "random":
|
||||
output = choose_random_tags(rng, output_from_data)
|
||||
else:
|
||||
output = output_arg
|
||||
if isinstance(output_from_data, dict):
|
||||
output = output_from_data["tags"].get(
|
||||
output_arg, "")
|
||||
output = choose_random_tags(rng, output)
|
||||
|
||||
output = apply_variables(rng, output, self.variables)
|
||||
output = stringify_tags(output, ", ")
|
||||
|
||||
self.variables.update({self.name: output})
|
||||
return (output, self.variables,)
|
||||
|
||||
@classmethod
|
||||
def create_node(cls, node_id, node_name=None):
|
||||
"Create a new node with ID and name"
|
||||
return type(node_id, (cls,), {
|
||||
"id": node_id,
|
||||
"name": node_name or node_id.capitalize()
|
||||
})
|
||||
|
||||
__all__ = ["NodeFactory"]
|
||||
@@ -0,0 +1,227 @@
|
||||
import dpath
|
||||
from ._variables import spread_variables
|
||||
|
||||
|
||||
def build_inputs(self):
|
||||
"""
|
||||
Build node's inputs according to the data
|
||||
"""
|
||||
inputs = {"required": {}, "optional": {}}
|
||||
show_variables = self.data.get("settings", {}).get("show_variables", False)
|
||||
|
||||
variables = self.data.get("variables", {})
|
||||
variables_inputs = process_inputs(variables.items(), show_variables)
|
||||
|
||||
tags = dpath.search(self.data.get("tags", {}), '*', yielded=True)
|
||||
tags_inputs = process_inputs(tags)
|
||||
|
||||
inputs["required"] = {**variables_inputs, **dict(tags_inputs)}
|
||||
|
||||
# Handle outputs
|
||||
concatenated_keys = ", ".join(
|
||||
f"{{{key.rstrip('?')}}}" for key in tags_inputs.keys())
|
||||
|
||||
output = self.data.get("output", True)
|
||||
|
||||
if output is False:
|
||||
return inputs
|
||||
|
||||
output = format_value("output", output)
|
||||
|
||||
if output is True:
|
||||
output = concatenated_keys
|
||||
|
||||
if isinstance(output[0], list):
|
||||
if output[0][-1] == "none":
|
||||
output[0][-1] = "custom"
|
||||
|
||||
inputs["required"].update({"output": output})
|
||||
|
||||
# Handle custom output
|
||||
if isinstance(output[0], list):
|
||||
custom_output = format_value("custom_output", self.data.get(
|
||||
"custom_output", concatenated_keys))
|
||||
inputs["required"].update({"custom_output": custom_output})
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
def process_inputs(data, show_inputs=True):
|
||||
"""
|
||||
Process the inputs to display or hide them in the node.
|
||||
Add a "?" to the key to indicate that the input is a boolean.
|
||||
"""
|
||||
|
||||
inputs = {}
|
||||
|
||||
for key, value in data:
|
||||
show_input = show_inputs
|
||||
|
||||
if isinstance(value, dict):
|
||||
show_input = value.get("show", show_input)
|
||||
|
||||
if show_input is False:
|
||||
continue
|
||||
|
||||
inputs[key] = format_value(key, value)
|
||||
|
||||
if inputs[key] is None:
|
||||
inputs.pop(key)
|
||||
continue
|
||||
|
||||
input_type = inputs[key][0]
|
||||
if input_type in ("FLOAT", "BOOLEAN"):
|
||||
inputs[f"{key}?"] = inputs.pop(key)
|
||||
|
||||
return inputs
|
||||
|
||||
|
||||
def format_value(key, value):
|
||||
"Format the value to respect ComfyUI's convention, depending on the type"
|
||||
|
||||
input = None
|
||||
|
||||
match value:
|
||||
|
||||
case bool():
|
||||
input = ("BOOLEAN", {
|
||||
"default": value
|
||||
})
|
||||
|
||||
case float():
|
||||
input = ("FLOAT", {
|
||||
"default": value,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.05
|
||||
})
|
||||
|
||||
case str():
|
||||
input = ("STRING", {
|
||||
"default": value,
|
||||
"multiline": True
|
||||
})
|
||||
|
||||
text_has_changed, value = spread_variables(value)
|
||||
if text_has_changed:
|
||||
input = format_value(key, value)
|
||||
|
||||
case list():
|
||||
|
||||
# If a {variable} is found in the list,
|
||||
# spread its value inside it
|
||||
updated_value = []
|
||||
for item in value:
|
||||
text_has_changed, processed_item = spread_variables(item)
|
||||
|
||||
if isinstance(processed_item, dict) and "tags" in processed_item:
|
||||
processed_item = processed_item["tags"]
|
||||
|
||||
if isinstance(processed_item, list):
|
||||
updated_value.extend(processed_item)
|
||||
else:
|
||||
updated_value.append(processed_item)
|
||||
|
||||
value = ["random", *updated_value, "none"]
|
||||
|
||||
input = (value, {
|
||||
"default": value[0] if value else ""
|
||||
})
|
||||
|
||||
case dict():
|
||||
|
||||
conditions = [
|
||||
value.get("show", True) is False,
|
||||
"tags" not in value
|
||||
]
|
||||
|
||||
if any(conditions):
|
||||
return
|
||||
|
||||
match value["tags"]:
|
||||
|
||||
case list() | str():
|
||||
input = format_value(key, value["tags"])
|
||||
|
||||
case dict():
|
||||
input = format_subtags(key, value)
|
||||
|
||||
return input
|
||||
|
||||
|
||||
def format_subtags(key, value):
|
||||
"""
|
||||
Handle how subtags/group of tags are displayed as input in the node,
|
||||
depending on the config file
|
||||
"""
|
||||
|
||||
display_group_labels = value.get("group_labels", False)
|
||||
probability_key = value.get("probability", 1)
|
||||
|
||||
# Display true/false checkbox as input
|
||||
input = format_value(key, True)
|
||||
|
||||
# Display a custom probability is set, show a float as input
|
||||
if probability_key != 1:
|
||||
input = format_value(key, probability_key)
|
||||
|
||||
# Display subtags labels as input
|
||||
if display_group_labels:
|
||||
tags_keys = list(value["tags"].keys())
|
||||
input = format_value(key, tags_keys)
|
||||
|
||||
return input
|
||||
|
||||
|
||||
def apply_input_values(data, inputs):
|
||||
"""
|
||||
Apply what has been selected in the node inputs, which can be "random", "none", or a selected value
|
||||
"""
|
||||
applied_values = {}
|
||||
|
||||
# Remove "?" from keys in inputs
|
||||
inputs = {key.rstrip('?'): value for key, value in inputs.items()}
|
||||
|
||||
def traverse(data):
|
||||
|
||||
for key, value in data.items():
|
||||
|
||||
selected = inputs.get(key, "random")
|
||||
match selected:
|
||||
|
||||
# If "none" or false, just ignore the tag
|
||||
case "none" | False:
|
||||
applied_values[key] = ""
|
||||
|
||||
# If a number, use "probability"
|
||||
case int() | float():
|
||||
if isinstance(value, dict) and "probability" in value:
|
||||
value["probability"] = selected
|
||||
applied_values[key] = value
|
||||
|
||||
# If "random", return the string, list or dict
|
||||
# according to the JSON config file
|
||||
case "random":
|
||||
applied_values[key] = value
|
||||
|
||||
if isinstance(value, dict) and not value.get("tags"):
|
||||
traverse(value)
|
||||
|
||||
# If a value is selected, just return it
|
||||
case _:
|
||||
applied_values[key] = selected
|
||||
|
||||
if selected is True:
|
||||
applied_values[key] = value
|
||||
continue
|
||||
|
||||
if isinstance(value, dict):
|
||||
prefix = value.get("prefix", "")
|
||||
suffix = value.get("suffix", "")
|
||||
applied_values[key] = f"{prefix}{selected}{suffix}"
|
||||
|
||||
if isinstance(value.get("tags"), dict):
|
||||
applied_values[key] = value["tags"][selected]
|
||||
|
||||
traverse(data)
|
||||
return applied_values
|
||||
@@ -0,0 +1,115 @@
|
||||
import numpy as np
|
||||
from ...utils.config import RESERVED_KEYS
|
||||
|
||||
|
||||
def choose_random_tags(rng, data, p=None):
|
||||
"""
|
||||
Select tags randomly
|
||||
String: simply return the tags
|
||||
List/dict: choose tags randomly according to the parameters
|
||||
(e.g. probability, distribution, etc)
|
||||
"""
|
||||
|
||||
selected_tags = []
|
||||
|
||||
if isinstance(data, (str, list)):
|
||||
data = {"tags": data}
|
||||
|
||||
# Probability
|
||||
if p is None:
|
||||
p = data.get("probability", 1)
|
||||
|
||||
if rng.random() > p:
|
||||
return ""
|
||||
|
||||
# Tag selection
|
||||
tags = data.get("tags", [])
|
||||
prefix = data.get("prefix", "")
|
||||
suffix = data.get("suffix", "")
|
||||
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
|
||||
if isinstance(tags, dict):
|
||||
if tags.get("tags"):
|
||||
tags = choose_random_tags(rng, tags)
|
||||
else:
|
||||
subtags = []
|
||||
for subtag in tags.values():
|
||||
subtags.append(choose_random_tags(rng, subtag))
|
||||
tags = subtags
|
||||
|
||||
# Number of tags to select
|
||||
n = data.get("number", 1)
|
||||
if isinstance(n, int) and n > len(tags):
|
||||
n = len(tags)
|
||||
|
||||
# If n is a list, choose a
|
||||
# random number between the 2 first values
|
||||
if isinstance(n, list):
|
||||
min_n = n[0]
|
||||
max_n = min(n[1], len(tags))
|
||||
n = rng.integers(int(min_n), int(max_n))
|
||||
|
||||
# Distribution: how likely each tag is to be selected
|
||||
# Normalize the distribution so the sum = 1
|
||||
# Add missing values if necessary
|
||||
d = data.get("distribution", np.ones(len(tags)))
|
||||
if np.sum(d) > 1:
|
||||
d = d / np.sum(d)
|
||||
d = np.append(d, np.zeros(len(tags) - len(d)))
|
||||
elif len(d) < len(tags):
|
||||
n_remaining_tags = len(tags) - len(d)
|
||||
remaining_d = 1 - np.sum(d)
|
||||
d = np.append(
|
||||
d, np.full(
|
||||
n_remaining_tags,
|
||||
remaining_d / n_remaining_tags
|
||||
)
|
||||
)
|
||||
|
||||
# Chose between the tags
|
||||
if tags:
|
||||
selected_tags = rng.choice(
|
||||
tags,
|
||||
size=n,
|
||||
p=d,
|
||||
replace=False,
|
||||
)
|
||||
selected_tags = sorted(
|
||||
selected_tags, key=lambda x: tags.index(x))
|
||||
|
||||
# Recursive choice (grouped tag)
|
||||
else:
|
||||
for key, value in data.items():
|
||||
if key not in RESERVED_KEYS:
|
||||
selected_tags.append(choose_random_tags(rng, value))
|
||||
|
||||
# Add prefix and suffix
|
||||
selected_tags = [
|
||||
f"{prefix}{tag}{suffix}" for tag in selected_tags
|
||||
]
|
||||
|
||||
# Clean up
|
||||
separator = data.get("separator", ",")
|
||||
selected_tags = stringify_tags(selected_tags, separator)
|
||||
|
||||
return selected_tags
|
||||
|
||||
|
||||
def stringify_tags(tags, separator=""):
|
||||
"""
|
||||
Return a string from a list of tags
|
||||
Remove extra commas and spaces
|
||||
"""
|
||||
if isinstance(tags, np.ndarray):
|
||||
tags = tags.tolist()
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
tags = separator.join(map(str, tags))
|
||||
|
||||
# Remove extra comma and spaces
|
||||
tags = tags.replace(", ", ",").replace(
|
||||
",,", ",").replace(",", ", ").strip(", ")
|
||||
|
||||
return tags
|
||||
@@ -0,0 +1,57 @@
|
||||
from ._tags import choose_random_tags
|
||||
from ...components.blackboard import Blackboard
|
||||
|
||||
|
||||
def apply_variables(rng, tags, variables):
|
||||
"""
|
||||
Each time a {variable} placeholder is found in `tags`, it will be replaced with a value in `variables` which has the same key.
|
||||
"""
|
||||
|
||||
def replace_placeholders(text, variables):
|
||||
|
||||
if isinstance(text, dict):
|
||||
text = choose_random_tags(rng, text)
|
||||
|
||||
for key, value in variables.items():
|
||||
placeholder = f"{{{key}}}"
|
||||
while placeholder in text:
|
||||
replacement = choose_random_tags(rng, value)
|
||||
text = text.replace(placeholder, str(replacement), 1)
|
||||
|
||||
return text
|
||||
|
||||
def process_placeholders(item):
|
||||
while any("{" + placeholder + "}" in item for placeholder in variables.keys()):
|
||||
item = replace_placeholders(item, variables)
|
||||
return item
|
||||
|
||||
replaced_tags = {}
|
||||
|
||||
if isinstance(tags, str):
|
||||
return [process_placeholders(tags)]
|
||||
|
||||
if isinstance(tags, list):
|
||||
for tag in tags:
|
||||
replaced_tags[tag] = process_placeholders(tag)
|
||||
return list(replaced_tags.values())
|
||||
|
||||
for key, value in tags.items():
|
||||
if isinstance(value, str):
|
||||
replaced_tags[key] = process_placeholders(value)
|
||||
else:
|
||||
replaced_tags[key] = value
|
||||
|
||||
return replaced_tags
|
||||
|
||||
|
||||
def spread_variables(text):
|
||||
variables = Blackboard().variables
|
||||
text_has_changed = False
|
||||
|
||||
if text.startswith("{") and text.endswith("}"):
|
||||
placeholder = text[1:-1]
|
||||
if placeholder in variables:
|
||||
text_has_changed = True
|
||||
text = variables[placeholder]
|
||||
|
||||
return text_has_changed, text
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
from .node import Node
|
||||
from .components.composition import Composition
|
||||
from .components.action import Action
|
||||
from .components.character.character import Character
|
||||
from .components.clothes import Clothes
|
||||
from .components.environment import Environment
|
||||
|
||||
|
||||
class Scene(Node):
|
||||
|
||||
def __init__(self, seed=0, data_file=""):
|
||||
super().__init__(seed)
|
||||
|
||||
self.components = {
|
||||
"composition": {
|
||||
"default": Composition(),
|
||||
"prefix": '',
|
||||
"protagonists": '1girl, solo',
|
||||
"camera_framing": 'random',
|
||||
"camera_angle": 'random'
|
||||
},
|
||||
"action": {
|
||||
"default": Action(),
|
||||
"nsfw": False,
|
||||
"position": "random",
|
||||
"gesture": "random",
|
||||
"act_type": "random",
|
||||
"act": "random"
|
||||
},
|
||||
"character": {
|
||||
"default": Character(),
|
||||
},
|
||||
"clothes": {
|
||||
"default": Clothes(),
|
||||
},
|
||||
"environment": {
|
||||
"default": Environment(),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
inputs = super().INPUT_TYPES()
|
||||
seed = inputs["required"]["seed"]
|
||||
|
||||
components = cls().components
|
||||
|
||||
inputs["required"] = {
|
||||
"seed": seed
|
||||
}
|
||||
|
||||
# Add the components as optional inputs
|
||||
optional_components = {
|
||||
key: ("STRING", {
|
||||
"default": "",
|
||||
"forceInput": True
|
||||
})
|
||||
for key in components.keys()
|
||||
}
|
||||
inputs["optional"] = optional_components
|
||||
|
||||
return inputs
|
||||
|
||||
def build_prompt(self, seed, **kwargs):
|
||||
self.seed = seed
|
||||
components = self.components
|
||||
prompt = []
|
||||
|
||||
# Build each component's prompt
|
||||
# If the component is in the kwargs, use it
|
||||
# Otherwise, use a default component
|
||||
for component_name, component_data in components.items():
|
||||
|
||||
tags = ""
|
||||
|
||||
if "default" in component_data:
|
||||
default = component_data["default"]
|
||||
args = component_data.copy()
|
||||
args.pop("default")
|
||||
tags = default.build_prompt(seed, **args)[0]
|
||||
|
||||
if component_name in kwargs.keys():
|
||||
tags = kwargs[component_name] + ", "
|
||||
|
||||
prompt.append(tags)
|
||||
|
||||
# Clean up prompt
|
||||
# Seperate tags with commas and remove trailing commas
|
||||
prompt = self.stringify_tags(prompt)
|
||||
if prompt.startswith(", "):
|
||||
prompt = prompt[2:]
|
||||
if prompt.endswith(", "):
|
||||
prompt = prompt[:-2]
|
||||
|
||||
return (prompt,)
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
ROOT_DIR = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def is_true(seed, p=0.5):
|
||||
"""Return True with probability p"""
|
||||
rng = np.random.default_rng(seed)
|
||||
return rng.random() < p
|
||||
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
import json
|
||||
from glob import glob
|
||||
import toml
|
||||
|
||||
ROOT_PATH = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
DEFAULT_PATH = "config.default"
|
||||
CUSTOM_PATH = "config"
|
||||
VARIABLE_FILE = "variables.json"
|
||||
|
||||
RESERVED_KEYS = [
|
||||
"prefix",
|
||||
"suffix",
|
||||
"separator",
|
||||
"probability",
|
||||
"distribution",
|
||||
"number",
|
||||
"show",
|
||||
"group_labels",
|
||||
"fixed"
|
||||
]
|
||||
|
||||
|
||||
def get_project_name():
|
||||
"""Get the project name from the pyproject.toml file"""
|
||||
pyproject_path = os.path.join(ROOT_PATH, "pyproject.toml")
|
||||
|
||||
if not os.path.exists(pyproject_path):
|
||||
return "⚙️ Prompt Factory"
|
||||
|
||||
with open(pyproject_path, 'r', encoding='utf-8') as pyproject_file:
|
||||
pyproject_data = toml.load(pyproject_file)
|
||||
project_name = pyproject_data.get("tool", {}).get(
|
||||
"comfy", {}).get("DisplayName", "Project")
|
||||
return project_name
|
||||
|
||||
|
||||
def load_config(config_path="nodes", with_filename=True):
|
||||
"Load and merge config files"
|
||||
config_path = chose_config(config_path)
|
||||
config_files = glob(os.path.join(
|
||||
config_path, "**", "*.json"), recursive=True)
|
||||
|
||||
config = {}
|
||||
for path in config_files:
|
||||
with open(path, 'r', encoding='utf-8') as config_file:
|
||||
config_data = json.load(config_file)
|
||||
|
||||
if with_filename:
|
||||
node_id = os.path.splitext(os.path.basename(path))[0]
|
||||
config[node_id] = config_data
|
||||
else:
|
||||
config.update(config_data)
|
||||
return config
|
||||
|
||||
|
||||
def chose_config(path=""):
|
||||
"Chose between default and custom config"
|
||||
custom_config_path = os.path.join(ROOT_PATH, CUSTOM_PATH)
|
||||
|
||||
if os.path.exists(custom_config_path):
|
||||
folder_path = CUSTOM_PATH
|
||||
else:
|
||||
folder_path = DEFAULT_PATH
|
||||
|
||||
absolute_config_path = os.path.join(ROOT_PATH, folder_path, path)
|
||||
|
||||
return absolute_config_path
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
[project]
|
||||
name = "comfyui-scene-composer"
|
||||
description = "A collection of nodes to facilitate the creation of scenes in ComfyUI."
|
||||
version = "0.1.1"
|
||||
description = "A collection of nodes to facilitate the creation of explicit NSFW scenes in ComfyUI."
|
||||
version = "0.3.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["numpy==2.1.2", "PyYAML==6.0.2", "toml==0.10.2"]
|
||||
dependencies = ["dpath", "numpy", "toml"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/taches-ai/comfyui-scene-composer"
|
||||
@@ -11,5 +11,5 @@ Repository = "https://github.com/taches-ai/comfyui-scene-composer"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "taches"
|
||||
DisplayName = "Scene Composer"
|
||||
DisplayName = "🎞️ Scene Composer"
|
||||
Icon = ""
|
||||
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
numpy==2.1.2
|
||||
PyYAML==6.0.2
|
||||
toml==0.10.2
|
||||
dpath
|
||||
numpy
|
||||
toml
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
let origProps = {};
|
||||
|
||||
const findWidgetByName = (node, name) => {
|
||||
return node.widgets ? node.widgets.find(w => w.name === name) : null;
|
||||
};
|
||||
|
||||
const doesInputWithNameExist = (node, name) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const HIDDEN_TAG = "schide";
|
||||
|
||||
// Toggle Widget + change size
|
||||
function toggleWidget(node, widget, show = false, suffix = "") {
|
||||
if (!widget || doesInputWithNameExist(node, widget.name)) return;
|
||||
|
||||
// Store the original properties of the widget if not already stored
|
||||
if (!origProps[widget.name]) {
|
||||
origProps[widget.name] = {
|
||||
origType: widget.type,
|
||||
origComputeSize: widget.computeSize,
|
||||
};
|
||||
}
|
||||
|
||||
const origSize = node.size;
|
||||
|
||||
// Set the widget type and computeSize based on the show flag
|
||||
widget.type = show ? origProps[widget.name].origType : HIDDEN_TAG + suffix;
|
||||
|
||||
widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4];
|
||||
|
||||
// A stupid fix to force textarea to hide
|
||||
widget.element.style.display = show ? "block" : "none";
|
||||
|
||||
// Calculate the new height for the node based on its computeSize method
|
||||
const newHeight = node.computeSize()[1];
|
||||
node.setSize([node.size[0], newHeight]);
|
||||
}
|
||||
|
||||
// Create a map of node titles to their respective widget handlers
|
||||
const nodeWidgetHandlers = {
|
||||
"output": {
|
||||
output: toggleCustomOutput,
|
||||
},
|
||||
};
|
||||
|
||||
// In the main function where widgetLogic is called
|
||||
function widgetLogic(node, widget) {
|
||||
// Retrieve the handler function for the current node title and widget name
|
||||
// const handler = nodeWidgetHandlers[node.title]?.[widget.name];
|
||||
|
||||
// Trigger toggleCustomOutput for each "output" widget
|
||||
if (widget.name === "output") {
|
||||
toggleCustomOutput(node, widget);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCustomOutput(node, widget) {
|
||||
|
||||
const custom_output = findWidgetByName(node, "custom_output");
|
||||
const output_value = widget.value;
|
||||
|
||||
// Hide custom_output if the output is not "custom"
|
||||
toggleWidget(node, custom_output, output_value == "custom");
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "scenecomposer.widgethider",
|
||||
async nodeCreated(node) {
|
||||
console.log(node)
|
||||
const customnodes_package_name = node.properties.aux_id
|
||||
console.log(customnodes_package_name)
|
||||
if (customnodes_package_name == "mus-taches/comfyui-scene-composer") {
|
||||
for (const widget of node.widgets || []) {
|
||||
widget.callback = function () { widgetLogic(node, widget) }
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { app } from "../../../scripts/app.js"
|
||||
|
||||
const TypeSlot = {
|
||||
Input: 1,
|
||||
Output: 2,
|
||||
};
|
||||
|
||||
const TypeSlotEvent = {
|
||||
Connect: true,
|
||||
Disconnect: false,
|
||||
};
|
||||
|
||||
const _ID = "MergeStrings";
|
||||
const _PREFIX = "string";
|
||||
const _TYPE = "STRING";
|
||||
|
||||
app.registerExtension({
|
||||
name: 'cozy_ex.' + _ID,
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
// skip the node if it is not the one we want
|
||||
if (nodeData.name !== _ID) {
|
||||
return
|
||||
}
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = async function () {
|
||||
const me = onNodeCreated?.apply(this);
|
||||
// start with a new dynamic input
|
||||
this.addInput(_PREFIX, _TYPE);
|
||||
return me;
|
||||
}
|
||||
|
||||
const onConnectionsChange = nodeType.prototype.onConnectionsChange
|
||||
nodeType.prototype.onConnectionsChange = function (slotType, slot_idx, event, link_info, node_slot) {
|
||||
const me = onConnectionsChange?.apply(this, arguments);
|
||||
|
||||
if (slotType === TypeSlot.Input) {
|
||||
if (link_info && event === TypeSlotEvent.Connect) {
|
||||
// get the parent (left side node) from the link
|
||||
const fromNode = this.graph._nodes.find(
|
||||
(otherNode) => otherNode.id == link_info.origin_id
|
||||
)
|
||||
|
||||
if (fromNode) {
|
||||
// make sure there is a parent for the link
|
||||
const parent_link = fromNode.outputs[link_info.origin_slot];
|
||||
if (parent_link) {
|
||||
node_slot.type = parent_link.type;
|
||||
node_slot.name = `${_PREFIX}_`;
|
||||
}
|
||||
}
|
||||
} else if (event === TypeSlotEvent.Disconnect) {
|
||||
this.removeInput(slot_idx);
|
||||
}
|
||||
|
||||
// Track each slot name so we can index the uniques
|
||||
let idx = 0;
|
||||
let slot_tracker = {};
|
||||
for (const slot of this.inputs) {
|
||||
if (slot.link === null) {
|
||||
this.removeInput(idx);
|
||||
continue;
|
||||
}
|
||||
idx += 1;
|
||||
const name = slot.name.split('_')[0];
|
||||
|
||||
// Correctly increment the count in slot_tracker
|
||||
let count = (slot_tracker[name] || 0) + 1;
|
||||
slot_tracker[name] = count;
|
||||
|
||||
// Update the slot name with the count if greater than 1
|
||||
slot.name = `${name}_${count}`;
|
||||
}
|
||||
|
||||
// check that the last slot is a dynamic entry....
|
||||
let last = this.inputs[this.inputs.length - 1];
|
||||
if (last === undefined || (last.name != _PREFIX || last.type != _TYPE)) {
|
||||
this.addInput(_PREFIX, _TYPE);
|
||||
}
|
||||
|
||||
// force the node to resize itself for the new/deleted connections
|
||||
this?.graph?.setDirtyCanvas(true);
|
||||
return me;
|
||||
}
|
||||
}
|
||||
return nodeType;
|
||||
},
|
||||
|
||||
})
|
||||
+30466
-657
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 1.5 MiB |
Reference in New Issue
Block a user