5 Commits
Author SHA1 Message Date
Taches 489c526404 Merge pull request #3 from ComfyNodePRs/pyproject
Add pyproject.toml for Custom Node Registry
2024-10-26 16:54:42 +02:00
Taches c51e71aaf4 Merge branch 'main' into pyproject 2024-10-26 16:54:30 +02:00
taches-ai bd2cc15447 Update pyproject infos 2024-10-26 16:52:55 +02:00
snomiao d71cb04f88 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-10-26 16:52:55 +02:00
Taches 035b2c3003 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 1195 additions and 33618 deletions
+3 -5
View File
@@ -8,19 +8,17 @@ on:
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'mus-taches' }}
# if this is a forked repository. Skipping the workflow.
if: github.event.repository.fork == false
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
uses: Comfy-Org/publish-node-action@main
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+1 -5
View File
@@ -1,10 +1,6 @@
# 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
@@ -51,6 +47,7 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -86,7 +83,6 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
test.py
# Translations
*.mo
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Taches
Copyright (c) 2024 taches-ai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+89 -30
View File
@@ -1,49 +1,108 @@
>[!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 explicit NSFW scenes with random procedural generation.
![Scene Composer workflow](workflow.png)
A collection of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) nodes to create scenes with random procedural generation.
## Installation
### 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.
Clone this repo in `custom_nodes`, in your ComfyUI folder.
```shell
$ git clone https://github.com/taches/comfyui-scene-composer
$ pip install -r requirements.txt
```
## Usage
## Nodes
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.
### Scene
You can check out the full NSFW workflow here: [civitai.com/models/579396](https://civitai.com/models/579396)
The Scene node generates a procedural random prompt as a string. The prompt follows the following structure:
## About
| 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 |
### CivitAI links
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.
- 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)
### Components
Don't hesitate to share your creations or text if you have questions and ideas of improvement.
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.
### License
### Seed
This project is licensed under the [MIT LICENSE](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.
+16 -32
View File
@@ -1,35 +1,19 @@
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
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
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./web"
NODE_CLASS_MAPPINGS = {
"🎞️ Scene": Scene,
"📹 Composition": Composition,
"🎬 Action": Action,
"👤 Character": Character,
"👕 Clothes": Clothes,
"⛅️ Environment": Environment,
}
WEB_DIRECTORY = "js"
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"
]
__all__ = ['NODE_CLASS_MAPPINGS', "WEB_DIRECTORY"]
-53
View File
@@ -1,53 +0,0 @@
{
"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
}
@@ -1,67 +0,0 @@
{
"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}"
}
}
}
@@ -1,48 +0,0 @@
{
"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}"
}
@@ -1,49 +0,0 @@
{
"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
@@ -1,42 +0,0 @@
{
"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
@@ -1,19 +0,0 @@
{
"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}"
}
}
}
}
}
}
@@ -1,49 +0,0 @@
{
"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
@@ -1,58 +0,0 @@
{
"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"] }
}
}
}
}
@@ -1,33 +0,0 @@
{
"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}"
}
@@ -1,44 +0,0 @@
{
"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
@@ -1,45 +0,0 @@
{
"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
@@ -1,55 +0,0 @@
{
"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"
]
}
}
}
@@ -1,72 +0,0 @@
{
"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
@@ -1,62 +0,0 @@
{
"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
@@ -1,37 +0,0 @@
{
"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
@@ -1,45 +0,0 @@
{
"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"]
}
}
}
@@ -1,16 +0,0 @@
{
"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"
]
}
}
@@ -1,60 +0,0 @@
{
"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
@@ -1,261 +0,0 @@
{
"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"
}
@@ -1,41 +0,0 @@
{
"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"
]
}
}
}
@@ -1,43 +0,0 @@
{
"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}"
}
@@ -1,73 +0,0 @@
{
"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}"
}
@@ -1,46 +0,0 @@
{
"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"
}
}
}
@@ -1,79 +0,0 @@
{
"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
@@ -1,4 +0,0 @@
{
"name": "🚫 Negative prompt",
"output": ""
}
-4
View File
@@ -1,4 +0,0 @@
{
"name": "✅ Positive prompt",
"output": "{character},{clothes}"
}
-11
View File
@@ -1,11 +0,0 @@
[
{
"triggers": ["*rainy*"],
"actions": [
{
"type": "add",
"value": "wet, wet hair"
}
]
}
]
-21
View File
@@ -1,21 +0,0 @@
[
{
"triggers": ["disembodied penis"],
"actions": [
{
"type": "remove",
"value": ["*male*"]
}
]
},
{
"triggers": ["giant male"],
"actions": [
{
"type": "replace",
"value": "small penis",
"new_value": "penis"
}
]
}
]
-214
View File
@@ -1,214 +0,0 @@
[
{
"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
@@ -1,95 +0,0 @@
[
{
"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
@@ -1,18 +0,0 @@
{
"color": {
"fixed": false,
"tags": [
"aqua",
"black",
"blue",
"brown",
"gray",
"green",
"pink",
"purple",
"red",
"white",
"yellow"
]
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"head_grab": [
"hand on another's head",
"hands on another's head",
"head grab",
"grabbing another's hair"
]
}
+31
View File
@@ -0,0 +1,31 @@
# 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
@@ -0,0 +1,88 @@
# 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
@@ -0,0 +1,42 @@
# 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
@@ -0,0 +1,14 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,153 @@
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);
},
});
}
},
});
+17 -91
View File
@@ -1,112 +1,38 @@
import argparse
from py.nodes.node_factory import NodeFactory
from py.utils.config import load_config
from py.nodes.apply_rules import ApplyRules
config = load_config()
import random
from src.scene import Scene
def main(seed, verbose, with_inputs, with_rules, node_output):
def main(seed, nsfw):
rules = ApplyRules()
if not seed:
seed = random.randint(0, 0xffffffffffffffff)
# Dislpay seed
if 0 != seed:
print(f"SEED: {seed}")
print("---")
scene = Scene()
scene.components["action"]["default"].INPUT_TYPES()
output = scene.build_prompt(seed)
# 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)
print(f"SEED: {seed}")
print(f"TAGS: {output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate a prompt from a node."
)
parser = argparse.ArgumentParser(description="Simulate a scene")
parser.add_argument(
"-s", "--seed",
"--seed", "-s",
type=int,
default=0,
help="Seed value for random number generator"
help="Seed for the node"
)
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,
"--nsfw", "-n",
type=bool,
default=False,
help="Name of the node to output"
help="Whether the scene is NSFW"
)
args = parser.parse_args()
main(
seed=args.seed,
verbose=args.verbose,
with_inputs=args.inputs,
with_rules=args.rules,
node_output=args.node
)
main(args.seed, args.nsfw)
-25
View File
@@ -1,25 +0,0 @@
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
@@ -1,189 +0,0 @@
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
@@ -1,74 +0,0 @@
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
@@ -1,32 +0,0 @@
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
@@ -1,149 +0,0 @@
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
@@ -1,227 +0,0 @@
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
@@ -1,115 +0,0 @@
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
@@ -1,57 +0,0 @@
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
@@ -1,70 +0,0 @@
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 explicit NSFW scenes in ComfyUI."
version = "0.3.0"
description = "A collection of nodes to facilitate the creation of scenes in ComfyUI."
version = "0.1.0"
license = { file = "LICENSE" }
dependencies = ["dpath", "numpy", "toml"]
dependencies = ["numpy==2.1.2", "PyYAML==6.0.2", "toml==0.10.2"]
[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 @@
dpath
numpy
toml
numpy==2.1.2
PyYAML==6.0.2
toml==0.10.2
+174
View File
@@ -0,0 +1,174 @@
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
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,92 @@
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
@@ -0,0 +1,72 @@
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
@@ -0,0 +1,34 @@
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
@@ -0,0 +1,143 @@
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
@@ -0,0 +1,95 @@
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
@@ -0,0 +1,10 @@
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
@@ -1,81 +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 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
@@ -1,89 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 1.5 MiB