156 Commits
Author SHA1 Message Date
mus-taches 4b7c68fef9 push: update workflows files 2025-12-18 01:25:14 +00:00
mus-taches 33653511e9 bump: v0.3.0 2025-12-17 23:49:48 +00:00
mus-taches 59a7a3c878 doc: detail breaking changes 2025-12-17 23:49:32 +00:00
mus-taches 08d3a9d6f3 Merge branch 'feat-improve-clothes' 2025-12-05 23:39:44 +00:00
mus-taches 9cfab48523 fix: typos and detailed for default config 2025-12-05 22:43:32 +00:00
mus-taches 76e0782ade fix: comma missing for jinx clothes 2025-11-26 02:31:06 +00:00
mus-taches 8c7126c22a fix: remove deprecated dpath package 2025-11-26 02:09:54 +00:00
--show-origin 9dae44569a minor changes in tag distribution and generation 2025-05-28 09:29:56 +02:00
--show-origin 6528e52b37 fix hide_input 2025-05-25 17:06:09 +02:00
--show-origin 1186b5a97d fix dress style to overwrite style 2025-05-25 13:57:49 +02:00
--show-origin 5f1d7b628c fixes inputs that didn't show 2025-05-25 13:55:56 +02:00
--show-origin 9c35fc88dd improve personas, add accessories 2025-05-25 13:23:45 +02:00
--show-origin 9a671b8634 add personas 2025-05-24 22:52:02 +02:00
--show-origin 2e60d72233 split clothes in different nodes 2025-05-24 18:48:17 +02:00
--show-origin 01ffe39508 tag process hotfixes 2025-05-24 18:48:05 +02:00
--show-origin 39bc22e213 fix: username in publish routine 2025-05-21 15:07:09 +02:00
--show-origin d172fdb606 improve giant male 2025-05-21 08:36:38 +02:00
--show-origin 5f8c7a1db3 improve sweat tags 2025-05-21 08:31:45 +02:00
--show-origin 230401c4cd improve disembodied 2025-05-21 08:31:35 +02:00
--show-origin 168d3de31d bump v0.2.0 2025-05-19 21:01:48 +02:00
--show-origin 8af24d3d91 limit widget hider to custom nodes 2025-05-19 21:01:15 +02:00
--show-origin 2f32e9321f fix cleavage apparition 2025-05-15 12:20:42 +02:00
--show-origin de65d32ea6 Merge remote-tracking branch 'refs/remotes/origin/main' 2025-05-15 09:14:45 +02:00
Taches 6fd3705cfe Merge pull request #7 from ComfyNodePRs/update-publish-yaml
Update Github Action for Publishing to Comfy Registry
2025-05-14 22:30:58 +02:00
mus-taches 540f3231af bump v0.1.3 version 2025-05-14 22:26:14 +02:00
mus-taches 1159509805 small adaptation for dynamic generation 2025-05-14 17:25:45 +02:00
mus-taches 53c3afa928 fix attributes as variables 2025-05-14 15:00:01 +02:00
mus-taches f3aee5cda2 minor tag fix and adjustements 2025-05-14 13:48:41 +02:00
mus-taches ade26d5a14 fix wrong color theme variable 2025-05-14 13:38:28 +02:00
mus-taches 280431c8da fix wrong variable name 2025-05-14 13:30:58 +02:00
mus-taches 510d350ddc fix and update tag generation 2025-05-14 13:23:54 +02:00
mus-taches faaf2a6f24 fix hide input for custom values 2025-05-14 12:51:46 +02:00
mus-taches 382989df85 change to forceInput 2025-05-14 10:54:43 +02:00
mus-taches aba472b8f8 fix output conditions 2025-05-14 10:40:40 +02:00
mus-taches dcada8fd6b update tags 2025-05-14 10:10:14 +02:00
mus-taches e65f26c8a3 change how output config file is handled 2025-05-14 10:10:02 +02:00
mus-taches 82edd5da30 fix output and rules logic 2025-05-10 11:01:04 +02:00
mus-taches cae62792c3 flexibility for main testing function node flag 2025-05-10 09:01:36 +02:00
mus-taches ea058bae66 fix: all custom_output can be hidden 2025-05-09 20:34:37 +02:00
mus-taches 5f46308950 improve tag generation 2025-05-09 16:42:27 +02:00
mus-taches 97c6f63e84 add AND logic for rules 2025-05-09 16:42:27 +02:00
mus-taches ce6bdd64b1 fix rule value so it can be a dict 2025-05-09 13:30:10 +02:00
mus-taches 29b1d0fe4a default value for custom_output 2025-05-09 12:29:09 +02:00
mus-taches dfab9c5d76 improve replacements rules 2025-05-09 12:20:36 +02:00
mus-taches d27ca8e838 update file for testing purpose 2025-05-08 23:20:14 +02:00
mus-taches ac99f78071 add replace feat rules 2025-05-08 23:18:17 +02:00
mus-taches ef9dbf47e1 feat: add custom_output and presets 2025-05-08 18:40:19 +02:00
mus-taches 6006441dc6 add custom output mechanic 2025-05-08 14:42:42 +02:00
mus-taches ab259c6802 fix 1boy protagonist 2025-05-04 19:17:41 +02:00
mus-taches b51e8b5e65 add some cosmetic tag fix 2025-05-04 19:01:10 +02:00
mus-taches f76e1ba38a remove toml 2025-05-04 18:19:12 +02:00
mus-taches 07d2dd32e8 fix: handle local variable replacement 2025-05-04 17:41:53 +02:00
mus-taches 87b010a60e update color variable 2025-05-03 23:28:34 +02:00
mus-taches 1b80dacc6a spread variables 2025-05-03 23:28:28 +02:00
mus-taches b6edbafc3b better environments 2025-05-03 23:01:28 +02:00
mus-taches caeb4a1df1 add sound effects 2025-05-03 18:56:09 +02:00
mus-taches 7f73d326e0 fix some tags generation 2025-05-03 18:17:08 +02:00
mus-taches 55797b831b add dynamic lines 2025-05-03 18:16:58 +02:00
mus-taches 9baa95869d attribute edit 2025-05-03 16:10:26 +02:00
mus-taches 06a0de2930 add output to style 2025-05-03 16:10:13 +02:00
mus-taches f5a95349f1 add comma to certain tags 2025-04-23 11:11:18 +02:00
mus-taches 4ea9efcda3 fix: variable replacement 2025-04-23 10:22:15 +02:00
mus-taches 73c75a84e7 fix: better variable rendering 2025-04-23 08:12:39 +02:00
mus-taches 9f73492858 fix some tag generation 2025-04-21 18:39:45 +02:00
mus-taches ebbf562e5f fix: utf-8 encoding at config file loading 2025-04-21 17:02:55 +02:00
mus-taches 02cc14f3f3 add teasing action 2025-04-21 11:31:40 +02:00
mus-taches e575779220 change expression emplacment 2025-04-21 11:18:56 +02:00
mus-taches c84ccff415 add render style 2025-04-21 11:18:44 +02:00
mus-taches 977963684c feat: add attributes 2025-04-21 10:55:46 +02:00
mus-taches 4f5e6d6e1f add attributes 2025-04-15 11:42:38 +02:00
mus-taches c05ebbb95e add some rules 2025-04-15 10:50:26 +02:00
mus-taches 0aaf2403cc feat: rule replacement 2025-04-15 10:34:41 +02:00
mus-taches f5afd208c0 add more clothes 2025-04-15 10:05:31 +02:00
mus-taches bfc1e6df3f fix eyes 2025-04-15 09:46:17 +02:00
mus-taches 65db4f8721 fix: after action number 2025-04-15 09:35:53 +02:00
mus-taches 28b1112b2a fix: bad skin variable name 2025-04-15 09:32:25 +02:00
mus-taches f353917b89 add eye effect 2025-04-15 09:25:36 +02:00
mus-taches c68dc7d630 add freckles 2025-04-15 09:21:28 +02:00
mus-taches 81c3191721 fix: hide variables in input 2025-04-15 09:19:19 +02:00
mus-taches 52e790dfc8 fix: hide variables in inputs 2025-04-15 09:17:50 +02:00
mus-taches aef8d13d1a feat: add tags 2025-04-15 09:10:01 +02:00
mus-taches 0a08f2f761 feat: add more modularity to action scenes 2025-04-14 16:52:35 +02:00
mus-taches 06e02ac981 fix: rollback to chain-variables paradigm instead of blackboard 2025-04-14 10:38:45 +02:00
mus-taches 0d805e3748 fix: add textfield to output node 2025-04-13 16:05:14 +02:00
mus-taches 87beed51fe feat: add output node 2025-04-13 16:02:55 +02:00
mus-taches 34b1622ced feat: change node organisation, add some tags 2025-04-13 15:51:00 +02:00
mus-taches 8a880096a5 rework variable mechanism 2025-04-13 14:28:58 +02:00
mus-taches c3adb31102 feat: consistency in variables without connecting them 2025-04-09 16:05:00 +02:00
mus-taches 44ec838788 feat: add custom node name 2025-04-04 18:32:04 +02:00
mus-taches 88b7b945ee feat: centralized character properties 2025-04-04 17:22:26 +02:00
mus-taches 4846cc796d fix: datastructure of cleanup prompt 2025-04-04 15:30:31 +02:00
mus-taches e7ac1f1f3d fix: better management of rules and variables 2025-04-04 15:28:06 +02:00
mus-taches 6f65534b3a fix: adapt config files 2025-04-04 13:21:49 +02:00
mus-taches ff1fddf88f ci: add config and pull script to gitignore 2025-04-04 13:21:33 +02:00
mus-taches 5b3dcddb9d Merge remote-tracking branch 'upstream/main' into v0.1.3 2025-04-04 13:20:08 +02:00
snomiao 3c4433a2fa chore(publish): update workflow permissions and action version
- Added write permissions for issues in the workflow
- Updated condition to check repository owner instead of fork status
- Changed publish-node-action from main to v1 for stability and version control
2025-01-25 07:51:54 +00:00
mus-taches c29a1074ea refactor: add component system in node classes 2024-11-17 21:49:08 +01:00
mus-taches e18954b69d push: v0.1.2 2024-11-15 12:08:24 +01:00
mus-taches f59ca83964 refactor: use Node static methods to handle widget values 2024-11-15 09:37:43 +01:00
mus-taches efa81d5b6b refactor: clean cozy_spoke dynamic widget system 2024-11-15 09:35:29 +01:00
mus-taches 902b1aa856 fix: hide act condition 2024-11-14 15:03:29 +01:00
mus-taches a4309aeb4f fix: hide act when action is sfw 2024-11-13 15:55:45 +01:00
mus-taches 2737049c10 update requirements.txt 2024-11-13 14:07:53 +01:00
mus-taches b29ad95829 refactor: test data handling with dict 2024-11-10 17:32:39 +01:00
mus-taches d58994d532 doc: add comments 2024-11-10 17:32:14 +01:00
mus-taches 9750d398fb fix: correctly hide act_type and act 2024-11-10 17:07:48 +01:00
mus-taches c0d59b04a9 fix: node mapping 2024-11-10 16:19:01 +01:00
mus-taches 7f5e177a1a add: widgets list change dynamically according to other internal widgets 2024-11-10 16:13:24 +01:00
taches-ai dbcfa19980 add: civitAI social links 2024-11-09 17:02:08 +01:00
taches-ai 54f062e22e fix: finisher-related action appears according to boolean widget value 2024-11-06 17:49:30 +01:00
taches-ai f1c6c0670b fix: build input list handle "tags" keyword in config 2024-11-06 17:47:47 +01:00
taches-ai eef4061b43 add: basic finisher action 2024-11-06 17:07:35 +01:00
taches-ai 8d041fe8f6 fix: remove unusued ears accessories 2024-11-06 16:51:30 +01:00
taches-ai 5bb9d212a7 add: makeup and lipstick 2024-11-06 16:21:51 +01:00
taches-ai afc36842a2 fix: looking condition 2024-11-06 16:21:40 +01:00
taches-ai baf20b12be add: accessories 2024-11-06 16:06:18 +01:00
taches-ai 71d6ec9653 add: glaze and looking direction 2024-11-06 12:25:16 +01:00
taches-ai c4b7730284 fix: parted lips condition 2024-11-06 12:17:26 +01:00
taches-ai 6ce5000ad2 change: improve eyewear generation 2024-11-06 12:11:42 +01:00
taches-ai 43ba7388a0 fix: typo in camera framings 2024-11-06 12:11:16 +01:00
taches-ai bd40040991 change: adapt "bored" attitude 2024-11-06 12:11:06 +01:00
taches-ai 56109734e6 change: better randomization of the camera 2024-11-06 10:47:20 +01:00
taches-ai f580d85540 fix: mouth/eyes expression 2024-11-06 10:46:38 +01:00
taches-ai e57e80deb5 fix: goggles with body position 2024-11-06 10:46:05 +01:00
taches-ai ee9d7af958 refactor: change utils to constant, now that no utils are inside anymore 2024-11-06 10:12:49 +01:00
taches-ai 8c821853b7 add: migrate expression from character to actions, since its not supposed to be static 2024-11-06 10:12:11 +01:00
taches-ai b74146dc2f fix: improve tags clean up 2024-11-05 21:49:11 +01:00
taches-ai 0b66d19de3 add: improve character and environment generation 2024-11-05 20:41:10 +01:00
taches-ai cb44861374 refactor: change method names 2024-11-05 20:40:29 +01:00
taches-ai 8abeea9e3d add: character attitudes and expressions 2024-10-31 14:42:39 +01:00
taches-ai 116c98888f change: distribution of gestures 2024-10-31 14:42:27 +01:00
taches-ai 62a9e6926a fix: character use rng to spread seeding across its sub-components 2024-10-31 12:38:09 +01:00
taches-ai e18616dc44 change: default clothes state is now clothed 2024-10-31 12:37:02 +01:00
taches-ai 5e402746dc fix. state and type are no longer overwriting class attributes 2024-10-31 12:36:33 +01:00
taches-ai a8e18356e8 fix: remove unused seed in composition 2024-10-31 11:58:32 +01:00
taches-ai d9b1fac873 fix: adapt build_prompt seeding for comfyui 2024-10-31 11:17:12 +01:00
taches-ai 9414b59cdf add: improve environment 2024-10-31 11:07:51 +01:00
taches-ai b43aca1727 fix: RNG don't reset during tag selection 2024-10-30 21:41:58 +01:00
taches-ai f00ddf95a5 Push version number to Comfy registry 2024-10-30 13:54:36 +01:00
taches-ai 3e3d328524 fix: inputs_list build method don't overwrite lists anymore (stupid extra indent) 2024-10-30 12:13:16 +01:00
taches-ai 13dc35745d fix: alter action seed to add more randomness 2024-10-27 18:49:26 +01:00
taches-ai b49f59d861 fix: change old probability character 2024-10-27 17:44:47 +01:00
taches-ai 6abed1f6be fix: change '?' with '[]' to define distribution in config files (avoid comfyui bad behavior) 2024-10-27 17:38:58 +01:00
taches-ai af458973ac fix: correctly update scene's seed 2024-10-27 17:06:42 +01:00
taches-ai 6505c2c98d docs: move wiki-related explanations and update LICENSE 2024-10-27 15:17:08 +01:00
taches-ai b13dc22f6b Add workflows 2024-10-27 13:36:00 +01:00
taches-ai e9bcbf1a07 change: set nsfw default value to false 2024-10-27 13:24:40 +01:00
taches-ai 8f4401066d fix: clothes use select_tag to handle random attribution 2024-10-27 13:11:41 +01:00
taches-ai 7474335343 Remove main.py file for testing purpose 2024-10-27 12:57:01 +01:00
taches-ai e684eecd47 fix: remove prefix replacement in act list to let the back-end target it 2024-10-27 12:53:30 +01:00
taches-ai 7bb938dccd refactor: rename "src" to "py" 2024-10-26 17:48:33 +02:00
Taches f3b30b182a Merge pull request #3 from ComfyNodePRs/pyproject
Add pyproject.toml for Custom Node Registry
2024-10-26 16:54:42 +02:00
Taches f6aa8a7403 Merge branch 'main' into pyproject 2024-10-26 16:54:30 +02:00
taches-ai ba29770958 Update pyproject infos 2024-10-26 16:52:55 +02:00
snomiao 92570ae6e4 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-10-26 16:52:55 +02:00
Taches f8d9578ecf Merge pull request #4 from ComfyNodePRs/publish
Add Github Action for Publishing to Comfy Registry
2024-10-26 16:49:28 +02:00
70 changed files with 33622 additions and 1199 deletions
+5 -3
View File
@@ -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 }}
+5 -1
View File
@@ -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/
@@ -83,6 +86,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
test.py
# Translations
*.mo
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 taches-ai
Copyright (c) 2024 Taches
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+30 -89
View File
@@ -1,108 +1,49 @@
>[!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.
![Scene Composer workflow](workflow.png)
## Installation
Clone this repo in `custom_nodes`, in your ComfyUI folder.
### Via ComfyUI
If you have [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) installed, you can simply import [`workflow.json`](workflow.json) or [`workflow.png`](workflow.png) in ComfyUI and click on "Install Missing Nodes".
Alternatively, you can install it via the [Comfy Registry](https://registry.comfy.org/publishers/taches/nodes/comfyui-scene-composer) using the [CLI](https://docs.comfy.org/comfy-cli/getting-started#install-cli).
```shell
$ comfy node registry-install comfyui-scene-composer
```
### Manually
Clone this repo in `custom_nodes` situated in your ComfyUI install folder.
```shell
$ git clone https://github.com/taches/comfyui-scene-composer
$ pip install -r requirements.txt
```
## Nodes
## Usage
### Scene
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.
The Scene node generates a procedural random prompt as a string. The prompt follows the following structure:
You can check out the full NSFW workflow here: [civitai.com/models/579396](https://civitai.com/models/579396)
| Component | Description |
| --------------- | ------------------------------------------------------------- |
| **C**omposition | The layout of the scene, cameras, number of protagonists, etc |
| **A**ctions | Activites done by the character/protagonist(s) |
| **C**haracter | Main character, focusing on body type and faces, etc |
| **C**lothes | Include casual clothes, uniforms, swimsuits, underwear, etc |
| **E**nvironment | The setting, including background, weather, time of day, etc |
## About
You can define some aspects of the scene directly in the node. If you want to have more control over what's generated, you can override these components by passing a string as input. This work particularly well with the node components, which are explained just below.
### CivitAI links
### Components
- 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)
Node components also generate a procedural random prompt as a string. It can be used to have better control over the generation of each concept. You can then pass it to the Scene node or even us it as standalone.
Don't hesitate to share your creations or text if you have questions and ideas of improvement.
### Seed
### License
Each node has its own seed. This way, you can alter the generation of each component independently without changing the whole scene. The scene seed is just there as a fallback if a component is not set in the optional input.
## Config files
You can configure components and other related concepts using the TOML files situated in the `config` folder.
Strings will output the chain of tags as it is. Lists will randomly take one tag
```toml
tags = "foo, bar" # -> foo, bar
tags = ["foo", "bar"] # -> bar
```
### Weighted lists
You can change the distribution by assigning a `?weight` next to the tag. By default, every tag has a weight of 1.
```toml
tags = ["foo?2", "bar", "baz"]
```
In this example, `foo` has a 50% chance to be chosen over `bar` or `baz`. This is because its weight is 2x the sum of all weights:
$\cfrac{\text{foo}}{\text{foo+bar+baz}} = \cfrac{2}{2+1+1}=0.5$
You can of course add fractions to the weights to make it _less_ likely to be chosen.
```toml
tags = ["foo?0.5", "bar", "baz"]
```
In this example, `foo` has a 20% chance to be chosen over `bar` or `baz`.
### Probabilities and repetitions
Some notations inside config files allow to alter the generation:
- `probability` define how likely the group of tags will be retained
- `repeat` define how many tags should be taken from a list with the key
You can do so by moving the string/list into a dictionary with the `tags` key.
```toml
# Before
[group]
items = ["foo", "bar", "baz"]
# After
[group.items]
probability = 0.75
repeat = 2
tags = ["foo", "bar", "baz"]
```
In this example, there is a 25% chance to ignore the tags. If it's not the case, 2 tags will be chosen from the list.
> [!NOTE]
> You can also determine a range of tags to take from the list.<br>
> For example: `repeat = [2,4]` will take between 2 and 4 tags
### Nested lists
You can define a `list` that includes the name of nested elements. This way, you can set what concept is randomly chosen and how likely.
```toml
[group]
list = ["foo:2", "bar"]
foo = ["tag1", "tag2", "tag3"]
bar = ["tag4", "tag5", "tag6"]
baz = ["tag7", "tag8", "tag9"]
```
In this example, `foo` tags are more likely to be chosen than `bar` tags. `baz` is not included in the `list` and will never be chosen.
This project is licensed under the [MIT LICENSE](LICENSE)
+32 -16
View File
@@ -1,19 +1,35 @@
from .src.scene import Scene
from .src.components.composition import Composition
from .src.components.action import Action
from .src.components.character.character import Character
from .src.components.clothes import Clothes
from .src.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"
]
+53
View File
@@ -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
}
+42
View File
@@ -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"]
}
}
}
}
}
}
+19
View File
@@ -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"]
}
}
}
}
}
+58
View File
@@ -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"
]
}
}
}
+45
View File
@@ -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"] }
}
}
}
+55
View File
@@ -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"
}
}
}
}
+62
View File
@@ -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"]
}
}
}
+37
View File
@@ -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"
}
}
}
+45
View File
@@ -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}"
}
+261
View File
@@ -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}"
}
+4
View File
@@ -0,0 +1,4 @@
{
"name": "🚫 Negative prompt",
"output": ""
}
+4
View File
@@ -0,0 +1,4 @@
{
"name": "✅ Positive prompt",
"output": "{character},{clothes}"
}
+11
View File
@@ -0,0 +1,11 @@
[
{
"triggers": ["*rainy*"],
"actions": [
{
"type": "add",
"value": "wet, wet hair"
}
]
}
]
+21
View File
@@ -0,0 +1,21 @@
[
{
"triggers": ["disembodied penis"],
"actions": [
{
"type": "remove",
"value": ["*male*"]
}
]
},
{
"triggers": ["giant male"],
"actions": [
{
"type": "replace",
"value": "small penis",
"new_value": "penis"
}
]
}
]
+214
View File
@@ -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"
}
]
}
]
+95
View File
@@ -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"]
}
}
]
}
]
+18
View File
@@ -0,0 +1,18 @@
{
"color": {
"fixed": false,
"tags": [
"aqua",
"black",
"blue",
"brown",
"gray",
"green",
"pink",
"purple",
"red",
"white",
"yellow"
]
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"head_grab": [
"hand on another's head",
"hands on another's head",
"head grab",
"grabbing another's hair"
]
}
-31
View File
@@ -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",
]
-88
View File
@@ -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",
]
-42
View File
@@ -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"]
-14
View File
@@ -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",
]
-18
View File
@@ -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"]
-153
View File
@@ -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")
.map(act => act.replace(`${actTypeWidget.value}_`, ""));
// 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);
},
});
}
},
});
+95 -21
View File
@@ -1,38 +1,112 @@
import argparse
import random
from src.scene import Scene
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, nsfw):
def main(seed, verbose, with_inputs, with_rules, node_output):
if not seed:
seed = random.randint(0, 0xffffffffffffffff)
rules = ApplyRules()
scene = Scene()
scene.components["action"]["default"].INPUT_TYPES()
output = scene.build_prompt(seed)
# Dislpay seed
if 0 != seed:
print(f"SEED: {seed}")
print("---")
print(f"SEED: {seed}")
print(f"TAGS: {output}")
# 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="Simulate a scene")
parser.add_argument(
"--seed", "-s",
type=int,
default=0,
help="Seed for the node"
parser = argparse.ArgumentParser(
description="Generate a prompt from a node."
)
parser.add_argument(
"--nsfw", "-n",
type=bool,
"-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="Whether the scene is NSFW"
help="Name of the node to output"
)
args = parser.parse_args()
main(args.seed, args.nsfw)
main(
seed=args.seed,
verbose=args.verbose,
with_inputs=args.inputs,
with_rules=args.rules,
node_output=args.node
)
+25
View File
@@ -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")
+189
View File
@@ -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
+74
View File
@@ -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
+32
View File
@@ -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,)
+149
View File
@@ -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"]
+227
View File
@@ -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
+115
View File
@@ -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
+57
View File
@@ -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
+70
View File
@@ -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
View File
@@ -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.0"
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
View File
@@ -1,3 +1,3 @@
numpy==2.1.2
PyYAML==6.0.2
toml==0.10.2
dpath
numpy
toml
-174
View File
@@ -1,174 +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
)
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
)
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
-23
View File
@@ -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,)
-20
View File
@@ -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,)
-34
View File
@@ -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
-18
View File
@@ -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,)
-92
View File
@@ -1,92 +0,0 @@
from ..node import Node
class Clothes(Node):
def __init__(self, seed=0, state="random", type="random"):
self.state = state
self.type = type
super().__init__(seed, data_file="clothes.toml")
def build_prompt(self, seed):
self.seed = seed
prompt = ""
if self.state == "random":
self.state = self.select_tags(self.data["states"])
if self.type == "random":
self.type = self.select_tags(self.data["types"])
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()
-72
View File
@@ -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,)
-34
View File
@@ -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
-143
View File
@@ -1,143 +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):
"""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"""
rng = np.random.default_rng(self.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:
tag_name, weight = tag.split('?')
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"""
data = [tag.split('?')[0] for tag in data]
inputs_list = ["random"] + data
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]
-95
View File
@@ -1,95 +0,0 @@
from .node import Node
from .components.composition import Composition
# from .components.action.action import Action
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": True,
"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):
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
View File
@@ -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
+81
View File
@@ -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) }
}
}
},
});
+89
View File
@@ -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;
},
})
+30567
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB