61 Commits
Author SHA1 Message Date
mus-taches f75d90808c push: v0.1.2 2024-11-15 12:08:24 +01:00
mus-taches 7a6085dc71 refactor: use Node static methods to handle widget values 2024-11-15 09:37:43 +01:00
mus-taches 6d01bc9ab6 refactor: clean cozy_spoke dynamic widget system 2024-11-15 09:35:29 +01:00
mus-taches d027d9f965 fix: hide act condition 2024-11-14 15:03:29 +01:00
mus-taches 0abfd24eb4 fix: hide act when action is sfw 2024-11-13 15:55:45 +01:00
mus-taches f752e4b3a9 update requirements.txt 2024-11-13 14:07:53 +01:00
mus-taches 5bd2b45fa5 refactor: test data handling with dict 2024-11-10 17:32:39 +01:00
mus-taches ae8be8cd46 doc: add comments 2024-11-10 17:32:14 +01:00
mus-taches cdec42e57d fix: correctly hide act_type and act 2024-11-10 17:07:48 +01:00
mus-taches b82192408a fix: node mapping 2024-11-10 16:19:01 +01:00
mus-taches ace5fcc34d add: widgets list change dynamically according to other internal widgets 2024-11-10 16:13:24 +01:00
taches-ai dcf5128ede add: civitAI social links 2024-11-09 17:02:08 +01:00
taches-ai 252fa25d35 fix: finisher-related action appears according to boolean widget value 2024-11-06 17:49:30 +01:00
taches-ai c6f010c653 fix: build input list handle "tags" keyword in config 2024-11-06 17:47:47 +01:00
taches-ai 3690bdb2c4 add: basic finisher action 2024-11-06 17:07:35 +01:00
taches-ai 90af275b71 fix: remove unusued ears accessories 2024-11-06 16:51:30 +01:00
taches-ai 8253cc14b9 add: makeup and lipstick 2024-11-06 16:21:51 +01:00
taches-ai 721a38ed98 fix: looking condition 2024-11-06 16:21:40 +01:00
taches-ai 0d1f0824ca add: accessories 2024-11-06 16:06:18 +01:00
taches-ai cd7485fe98 add: glaze and looking direction 2024-11-06 12:25:16 +01:00
taches-ai cac90e3e64 fix: parted lips condition 2024-11-06 12:17:26 +01:00
taches-ai 13fdd9e447 change: improve eyewear generation 2024-11-06 12:11:42 +01:00
taches-ai aa77f3ee26 fix: typo in camera framings 2024-11-06 12:11:16 +01:00
taches-ai 361e86adc5 change: adapt "bored" attitude 2024-11-06 12:11:06 +01:00
taches-ai dc13b09eb7 change: better randomization of the camera 2024-11-06 10:47:20 +01:00
taches-ai f3dfcb93ca fix: mouth/eyes expression 2024-11-06 10:46:38 +01:00
taches-ai 0131ceba4e fix: goggles with body position 2024-11-06 10:46:05 +01:00
taches-ai 7c3739508d refactor: change utils to constant, now that no utils are inside anymore 2024-11-06 10:12:49 +01:00
taches-ai 12d66e065d add: migrate expression from character to actions, since its not supposed to be static 2024-11-06 10:12:11 +01:00
taches-ai aa86a9f20e fix: improve tags clean up 2024-11-05 21:49:11 +01:00
taches-ai 8100f659f1 add: improve character and environment generation 2024-11-05 20:41:10 +01:00
taches-ai 97c2fce17a refactor: change method names 2024-11-05 20:40:29 +01:00
taches-ai 8a4274f9d6 add: character attitudes and expressions 2024-10-31 14:42:39 +01:00
taches-ai c013ee0888 change: distribution of gestures 2024-10-31 14:42:27 +01:00
taches-ai 1f8b9072b5 fix: character use rng to spread seeding across its sub-components 2024-10-31 12:38:09 +01:00
taches-ai c1d18991d8 change: default clothes state is now clothed 2024-10-31 12:37:02 +01:00
taches-ai 1d6dc0e971 fix. state and type are no longer overwriting class attributes 2024-10-31 12:36:33 +01:00
taches-ai edd287d548 fix: remove unused seed in composition 2024-10-31 11:58:32 +01:00
taches-ai 4c2454bebb fix: adapt build_prompt seeding for comfyui 2024-10-31 11:17:12 +01:00
taches-ai 685273eecd add: improve environment 2024-10-31 11:07:51 +01:00
taches-ai 33ab93010c fix: RNG don't reset during tag selection 2024-10-30 21:41:58 +01:00
taches-ai d1b774d7ce Push version number to Comfy registry 2024-10-30 13:54:36 +01:00
taches-ai b4c965b1a5 fix: inputs_list build method don't overwrite lists anymore (stupid extra indent) 2024-10-30 12:13:16 +01:00
taches-ai b94ba237a1 fix: alter action seed to add more randomness 2024-10-27 18:49:26 +01:00
taches-ai a7d498a081 fix: change old probability character 2024-10-27 17:44:47 +01:00
taches-ai 8fe532f2f7 fix: change '?' with '[]' to define distribution in config files (avoid comfyui bad behavior) 2024-10-27 17:38:58 +01:00
taches-ai 4d67a6b4cc fix: correctly update scene's seed 2024-10-27 17:06:42 +01:00
taches-ai 1ac972f912 docs: move wiki-related explanations and update LICENSE 2024-10-27 15:17:08 +01:00
taches-ai aa26f4aae2 Add workflows 2024-10-27 13:36:00 +01:00
taches-ai b78501d4e2 change: set nsfw default value to false 2024-10-27 13:24:40 +01:00
taches-ai acdcb0032b fix: clothes use select_tag to handle random attribution 2024-10-27 13:11:41 +01:00
taches-ai ed31d7dd82 Remove main.py file for testing purpose 2024-10-27 12:57:01 +01:00
taches-ai 4ab36b4470 fix: remove prefix replacement in act list to let the back-end target it 2024-10-27 12:53:30 +01:00
taches-ai a22a2e9023 refactor: rename "src" to "py" 2024-10-26 17:48:33 +02:00
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
snomiao dc9bb0ad48 chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-10-19 13:59:45 +00:00
snomiao 5feda0fbab chore(publish): Add Github Action for Publishing to Comfy Registry 2024-10-19 13:59:45 +00:00
34 changed files with 1596 additions and 401 deletions
+24
View File
@@ -0,0 +1,24 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
- master
paths:
- "pyproject.toml"
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
# if this is a forked repository. Skipping the workflow.
if: github.event.repository.fork == false
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
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
View File
@@ -83,6 +83,7 @@ coverage.xml
.hypothesis/
.pytest_cache/
cover/
test.py
# Translations
*.mo
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 taches-ai
Copyright (c) 2024 Taches
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+36 -90
View File
@@ -2,107 +2,53 @@
A collection of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) nodes to create scenes with random procedural generation.
![Scene Composer workflow](workflow.png)
## Installation
Clone this repo in `custom_nodes`, in your ComfyUI folder.
### Via ComfyUI
If you have [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) installed, you can simply import [`workflow.json`](workflow.json) or [`workflow.png`](workflow.png) in ComfyUI and click on "Install Missing Nodes".
Alternatively, you can install it via the [Comfy Registry](https://registry.comfy.org/publishers/taches/nodes/comfyui-scene-composer) using the [CLI](https://docs.comfy.org/comfy-cli/getting-started#install-cli).
```shell
$ comfy node registry-install comfyui-scene-composer
```
### Manually
Clone this repo in `custom_nodes` situated in your ComfyUI install folder.
```shell
$ git clone https://github.com/taches/comfyui-scene-composer
$ pip install -r requirements.txt
```
## Nodes
### Scene
The Scene node generates a procedural random prompt as a string. The prompt follows the following structure:
| 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 |
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.
### Components
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.
### Seed
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.
## Usage
> [!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
> This is a basic guide to get you started. For more detailed information, please refer to the [wiki](https://github.com/mousse-taches/comfyui-scene-composer/wiki)
### Nested lists
Add the `🎞️ Scene` node to your workflow. The output will be a prompt formatted as a string.
You can directly pass it to a conditioning node and connect it to your sampler.
You can define a `list` that includes the name of nested elements. This way, you can set what concept is randomly chosen and how likely.
The other nodes are scene components. You can use it as an optional input for the `🎞️ Scene` node, or directly as standalone.
```toml
[group]
list = ["foo:2", "bar"]
foo = ["tag1", "tag2", "tag3"]
bar = ["tag4", "tag5", "tag6"]
baz = ["tag7", "tag8", "tag9"]
```
| Component nodes | Description |
| ----------------- | ------------------------------------------------------------- |
| `📹 Composition` | The layout of the scene, cameras, number of protagonists, etc |
| `🎬 Action` | Activities done by the character/protagonist(s) |
| `👤 Character` | Main character, focusing on body type and faces, etc |
| `👕 Clothes` | Include casual clothes, uniforms, swimsuits, underwear, etc |
| `⛅️ Environment` | The setting, including background, weather, time of day, etc |
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.
## About
### Social
You can find the Scene Composer [custom nodes](https://civitai.com/models/902456/scene-composer-custom-nodes) and a complete [workflow](https://civitai.com/models/579396/scene-composer-workflow) on my [CivitAI profile](https://civitai.com/user/taches). Don't hesitate to share your creations!
### License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+27 -13
View File
@@ -1,19 +1,33 @@
from .src.scene import Scene
from .src.components.composition import Composition
from .src.components.action import Action
from .src.components.character.character import Character
from .src.components.clothes import Clothes
from .src.components.environment import Environment
from .py.scene import Scene
from .py.components.composition import Composition
from .py.components.action import Action
from .py.components.character.character import Character
from .py.components.clothes import Clothes
from .py.components.environment import Environment
from .py.cozy_spoke import initialize_cozy_spoke
initialize_cozy_spoke()
# Mappings
NODE_CLASS_MAPPINGS = {
"🎞️ Scene": Scene,
"📹 Composition": Composition,
"🎬 Action": Action,
"👤 Character": Character,
"👕 Clothes": Clothes,
"⛅️ Environment": Environment,
"Scene": Scene,
"Composition": Composition,
"Action": Action,
"Character": Character,
"Clothes": Clothes,
"Environment": Environment,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Scene": "🎞️ Scene",
"Composition": "📹 Composition",
"Action": "🎬 Action",
"Character": "👤 Character",
"Clothes": "👕 Clothes",
"Environment": "⛅️ Environment",
}
WEB_DIRECTORY = "js"
__all__ = ['NODE_CLASS_MAPPINGS', "WEB_DIRECTORY"]
__all__ = ['NODE_CLASS_MAPPINGS',
"NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+36 -11
View File
@@ -1,23 +1,45 @@
# SFW
[sfw]
positions = ["standing", "sitting", "walking", "running", "lying"]
gestures = ["waving", "adjusting hair", "holding hand", "drinking", "reading"]
### Expressions ###
[expressions.eyes]
probability = 0.5
tags = ["open", "half-closed", "wink", "closed[0.4]"]
# NSFW
[expressions.looking]
probability = 0.5
tags = ["at viewer", "away", "up", "down", "back"]
[expressions.mouth]
probability = 0.5
tags = ["open", "half-closed", "closed"]
[expressions.tongue]
probability = 0.2
tags = ["tongue out"]
### SFW ###
[sfw.positions]
probability = 0.7
tags = ["standing", "sitting", "lying", "walking[0.5]", "running[0.3]"]
[sfw.gestures]
probability = 0.7
tags = ["adjusting hair", "waving", "hand on hip", "crossed arms"]
### NSFW ###
# Settings
[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"]
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
# Actions
[nsfw.acts]
teasing = ["flashing"]
preliminaries = ["fingering", "handjob", "fellatio", "licking penis", "paizuri"]
@@ -29,3 +51,6 @@ intercourses = [
"suspended congress",
"full nelson",
]
[nsfw.cum]
location = ["inside", "outside"]
+44 -10
View File
@@ -1,8 +1,8 @@
# BODY
[body.colors]
probability = 0.4
list = ["standard_colors", "special_colors?0.5"]
standard_colors = ["dark?1.5", "pale"]
list = ["standard_colors", "special_colors[0.5]"]
standard_colors = ["dark[1.5]", "pale"]
special_colors = ["black", "red", "green", "blue", "purple", "pink"]
[body.types]
@@ -15,8 +15,8 @@ tags = [
"muscular",
"curvy",
"plump",
"chubby?0.2",
"fat?0.1",
"chubby[0.2]",
"fat[0.1]",
]
[body.breasts]
@@ -25,18 +25,18 @@ sizes = [
"small breasts",
"medium breasts",
"large breasts",
"huge breasts?0.5",
"huge breasts[0.5]",
]
[body.breasts.nipples]
probability = 0.5
repeat = [1, 2]
repeat = [1, 3]
tags = ["dark", "puffy"]
[body.extras]
probability = 0.75
repeat = [1, 3]
tags = ["freckles?2", "tattoos", "piercings", "scars?0.2"]
tags = ["freckles[2]", "mole", "tattoos", "piercings", "scars[0.4]"]
# HAIR
[hair]
@@ -49,7 +49,7 @@ styles = ["ponytail", "bun", "braid", "pigtails"]
appearances = ["curly", "straight", "wavy"]
[hair.colors]
list = ["standard_colors", "special_colors?0.5"]
list = ["standard_colors", "special_colors[0.7]", "gradient[0.4]"]
standard_colors = ["black", "brown", "blonde", "ginger"]
special_colors = [
"red",
@@ -62,16 +62,32 @@ special_colors = [
"pink",
"purple",
]
gradient = ["gradient"]
# EYES & EYEWEAR
[eyes.colors]
list = ["standard_colors", "special_colors?0.8"]
list = ["standard_colors", "special_colors[0.8]"]
standard_colors = ["black", "brown", "blue", "green"]
special_colors = ["red", "yellow", "orange", "pink", "purple", "grey", "white"]
[eyes.makeup]
probability = 0.6
tags = ["eyeliner", "eyeshadow"]
colors = [
"black[1.1]",
"blue[1.1]",
"cyan[1.1]",
"red",
"green",
"pink",
"white[0.6]",
"yellow[0.6]",
"orange[0.6]",
]
[eyes.eyewears]
probability = 0.4
types = ["glasses", "sunglasses", "round glasses", "googles"]
types = ["glasses", "sunglasses", "round glasses", "goggles[0.3]"]
colors = [
"aqua",
"black",
@@ -86,3 +102,21 @@ colors = [
"white",
"yellow",
]
# MOUTH
[mouth.lipstick]
probability = 0.6
tags = ["lipstick"]
colors = ["red[1.2]", "black", "blue[0.8]"]
# ATTITUDE
[attitudes]
tags = [
"happy[1.2]",
"smirk[1.2]",
"bored",
"upset[0.9]",
"confused[0.7]",
"surprised[0.6]",
"crazy[0.4]",
]
+27 -1
View File
@@ -1,5 +1,5 @@
# State: how clothed the character is
states = ["clothed", "underwear?0.3", "nude?0.1"]
states = ["clothed", "underwear[0.3]", "nude[0.1]"]
# Types: if clothed, what type of clothing
types = ["casual", "dress", "swimsuit", "uniform"]
@@ -40,3 +40,29 @@ length = ["short", "long"]
[dress.style]
probability = 0.5
tags = ["off-shoulder", "backless"]
# Accessories
[accessories]
probability = 0.6
repeat = [2, 3]
list = ["head", "neck", "hand"]
[accessories.head]
list = ["hair", "hats"]
hair = ["hairbow", "hair ribbon", "hair tie", "hairband"]
hats = [
"cap",
"beanie",
"bucket hat",
"sun hat",
"straw hat[0.5]",
"rice hat[0.5]",
"witch hat[0.5]",
"crown[0.5]",
]
[accessories.neck]
tags = ["chain necklace", "tooth necklace", "choker", "pendant"]
[accessories.hand]
tags = ["bracelet", "watch", "ring"]
+10 -6
View File
@@ -2,13 +2,17 @@ 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 = [
[camera.framings]
probability = 0.7
tags = ["close-up", "portrait", "upper body", "full body[0.4]"]
[camera.angles]
probability = 0.7
tags = [
"straight on",
"pov",
"from above",
"from below",
"from side",
"from behind",
"straight on",
"pov",
"from behind[0.4]",
]
+21 -5
View File
@@ -1,16 +1,32 @@
time = ["day", "night", "dawn", "dusk", "sunset", "sunrise"]
weather = ["sunny", "cloudy", "rainy", "snowy"]
time = ["day[1.5]", "night[0.5]", "dawn", "dusk", "sunset", "sunrise"]
weather = ["sun", "cloud", "rain[0.5]", "snow[0.3]"]
[locations]
list = ["indoors", "outdoors"]
[locations.indoors]
list = ["house", "public"]
house = ["bedroom", "living room", "kitchen", "bathroom"]
public = ["office", "library"]
house = ["bedroom[1.5]", "living room[1.2]", "kitchen", "bathroom"]
public = [
"office",
"library",
"changing room",
"storage room",
"classroom",
"infirmary",
]
[locations.outdoors]
tags = ["forest", "mountain", "beach"]
tags = [
"forest",
"mountain",
"beach",
"garden",
"lake",
"river",
"park",
"city",
]
[effects]
probability = 0.5
+93
View File
@@ -0,0 +1,93 @@
// https://github.com/cozy-comfyui/cozy_spoke
import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
const _id = "Action"
const _root = "cozy_spoke"
const EVENT_COZY_UPDATE = "cozy-event-combo-update";
const nodeWidgetHandlers = [
{
"parent_widget": "act_type",
"child_widget": "act",
}
]
async function apiRoute(route, data = null, id = null) {
const full_route = `/${_root}/${route}`;
var blob = {
method: "GET",
headers: { "Content-Type": "application/json" }
}
// if we are passing data, or need a specific node by ID, we must use a POST
if (data != null || id != null) {
blob['method'] = "POST",
blob['body'] = JSON.stringify({
id: id,
data: data
});
}
try {
const response = await api.fetchApi(`${full_route}`, blob);
if (!response.ok) {
throw new Error(`Error: ${response.status} - ${response.statusText}`);
}
return response;
} catch (error) {
console.error("API call to Jovimetrix failed:", error);
throw error; // or return { success: false, message: error.message }
}
}
app.registerExtension({
name: 'cozy.spoke.' + _id,
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== _id) {
return
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const me = onNodeCreated?.apply(this);
// Update all children widgets according to the parent widgets value.
// in respect of what's defined in the config files
nodeWidgetHandlers.forEach((handler) => {
const parentWidget = this.widgets.find(w => w.name == handler.parent_widget);
const childWidget = this.widgets.find(w => w.name == handler.child_widget);
parentWidget.callback = async () => {
try {
const response = await apiRoute("node", parentWidget.value);
const responseData = await response.json();
childWidget.options.values = responseData.data;
childWidget.value = responseData.data[0] || "random";
this.setDirtyCanvas(true, true);
} catch (error) {
console.error('Error in parent widget callback:', error);
}
}
});
// process the message sent from python.
// this could be during node execution or outside of it.
async function python_update(data) {
// Assuming you will do something with the data from Python here
console.info('Received update from Python for child widget:', data);
}
// catch the event(s) from python.
api.addEventListener(EVENT_COZY_UPDATE, python_update);
// clean up your mess when leaving.
this.onDestroy = () => {
api.removeEventListener(EVENT_COZY_UPDATE, python_update);
};
return me;
}
}
})
+14 -29
View File
@@ -39,8 +39,9 @@ function toggleWidget(node, widget, show = false, suffix = "") {
// Create a map of node titles to their respective widget handlers
const nodeWidgetHandlers = {
"🎬 Action": {
"Action": {
nsfw: handleSceneNsfw,
act_type: handleActType,
},
};
@@ -56,13 +57,16 @@ function widgetLogic(node, widget) {
// Show SFW/NSFW widgets according to the value of the "nsfw" widget
function handleSceneNsfw(node, widget) {
const isNsfw = widget.value;
const actTypeWidget = findWidgetByName(node, "act_type")
const actWidget = findWidgetByName(node, "act");
const sfwWidgets = [
findWidgetByName(node, `position`),
findWidgetByName(node, `gesture`),
];
const nsfwWidgets = [
findWidgetByName(node, `act_type`),
findWidgetByName(node, `act`),
actTypeWidget, actWidget,
findWidgetByName(node, `cum`),
];
const allWidgets = [...sfwWidgets, ...nsfwWidgets];
@@ -77,37 +81,18 @@ function handleSceneNsfw(node, widget) {
toggleWidget(node, w, true);
}
// Hide the act widget if the act type is "random"
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);
toggleWidget(node, actWidget, actTypeWidget.value !== "random");
}
}
function filterInputList(node, actWidget, actTypeWidget) {
// Restore original value
if (origProps[actWidget.name].originalValues) {
actWidget.options.values = origProps[actWidget.name].originalValues;
}
function handleActType(node, widget) {
const actType = widget.value;
const actWidget = findWidgetByName(node, "act");
// 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];
// Hide the act widget if the act type is "random"
toggleWidget(node, actWidget, actType !== "random");
}
app.registerExtension({
-38
View File
@@ -1,38 +0,0 @@
import argparse
import random
from src.scene import Scene
def main(seed, nsfw):
if not seed:
seed = random.randint(0, 0xffffffffffffffff)
scene = Scene()
scene.components["action"]["default"].INPUT_TYPES()
output = scene.build_prompt(seed)
print(f"SEED: {seed}")
print(f"TAGS: {output}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Simulate a scene")
parser.add_argument(
"--seed", "-s",
type=int,
default=0,
help="Seed for the node"
)
parser.add_argument(
"--nsfw", "-n",
type=bool,
default=False,
help="Whether the scene is NSFW"
)
args = parser.parse_args()
main(args.seed, args.nsfw)
@@ -1,5 +1,4 @@
from ..node import Node
from ..utils import is_true
class Action(Node):
@@ -16,9 +15,6 @@ class Action(Node):
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":
@@ -31,35 +27,71 @@ class Action(Node):
recursive=False
)
for act in data["nsfw"]["acts"][act_type]:
acts.extend([f"{act_type}_{act}"])
acts.extend([f"{act}"])
acts = cls().build_inputs_list(acts)
# Update the required inputs
inputs["required"] = {
"nsfw": ("BOOLEAN", {"default": False}),
# SFW inputs
"position": (positions,),
"gesture": (gestures,),
# NSFW inputs
"act_type": (act_types,),
"act": (acts,),
"cum": ("BOOLEAN", {"default": False}),
"seed": seed
}
return inputs
def build_prompt(self, seed, nsfw, position, gesture, act_type, act):
def build_prompt(self, seed, ident, nsfw, position, gesture,
act_type, act, cum):
super().build_prompt(seed)
self.seed = seed
prompt = ""
self.comfy_message("cozy-event-combo-update", ident, {
"id": ident,
"a": act_type,
"b": act
})
action = ""
expression = self.build_expression()
if nsfw:
prompt = self.build_nsfw_action(act_type, act)
action = self.build_nsfw_action(act_type, act, cum)
else:
prompt = self.build_sfw_action(position, gesture)
action = self.build_sfw_action(position, gesture)
prompt = f"{action}, {expression}"
return (prompt,)
def build_expression(self):
eyes = self.select_tags(self.data["expressions"]["eyes"])
if eyes:
eyes += " eyes"
if "wink" in eyes:
eyes = "one eye closed"
looking = self.select_tags(self.data["expressions"]["looking"])
if looking:
looking = f"looking {looking}"
mouth = self.select_tags(self.data["expressions"]["mouth"])
if mouth:
mouth += " mouth"
if "half-closed" in mouth:
mouth = "parted lips"
tongue = self.select_tags(self.data["expressions"]["tongue"])
return f"{eyes}, {looking}, {mouth}, {tongue}"
def build_sfw_action(self, position, gesture):
"""Build a safe-for-work action prompt"""
data = self.data["sfw"]
position = self.select_tags(
@@ -69,14 +101,13 @@ class Action(Node):
gesture = self.select_tags(
tags=data["gestures"],
selected=gesture
selected=gesture,
)
prompt = f"{position}, {gesture}"
return prompt
def build_nsfw_action(self, act_type, act):
"""Build a not-safe-for-work action prompt"""
def build_nsfw_action(self, act_type, act, cum):
data = self.data["nsfw"]
act_type = self.select_tags(
@@ -91,19 +122,32 @@ class Action(Node):
)
settings = self.apply_settings(data["settings"])
prompt = self.enhance_prompt(data["acts"], act, settings)
cum_prompt = ""
if cum:
cum_prompt = "cum, orgasm"
cum_location = self.select_tags(data["cum"]["location"])
match cum_location:
case "inside":
cum_prompt += ", cum inside"
case "outside":
cum_prompt = ", ejaculation, cum on body, facial"
prompt = self.enhance_nsfw_prompt(data["acts"], act, settings)
prompt = f"{prompt}, {cum_prompt}"
return prompt
def apply_settings(self, data):
"""Apply NSFW settings"""
settings = {}
for key, value in data.items():
for key in data.keys():
settings[key] = self.select_tags(data[key])
return settings
def enhance_prompt(self, data, act_type, settings):
def enhance_nsfw_prompt(self, data, act_type, settings):
"""Enhance the NSFW action.
Add or remove tags depending on the action type"""
@@ -123,7 +167,7 @@ class Action(Node):
if act_type in data["preliminaries"]:
if "fingering" in act_type:
penis = ""
if is_true(self.seed, 0.5):
if self.rng.random() < 0.5:
act += ", fingering from behind"
else:
pussy = ""
@@ -150,7 +194,7 @@ class Action(Node):
# Testicles
testicles = ""
if is_true(self.seed, 0.3):
if self.rng.random() < 0.3:
testicles = f"{settings['testicles_sizes']} testicles"
# Pussy
+26
View File
@@ -0,0 +1,26 @@
from ...node import Node
class Attitude(Node):
def __init__(self, seed, ident, rng):
super().__init__(seed, data_file="character.toml")
self.rng = rng
def build_prompt(self):
attitude = self.select_tags(self.data["attitudes"])
prompt = attitude
match attitude:
case "happy":
prompt += ", smile"
case "smirk":
prompt += ", smile, smug"
case "bored":
extra_tags = ["sleepy", "tired", "expressionless"]
tags = self.select_tags(extra_tags, n=[1, 3])
prompt += f", {tags}"
case "upset":
prompt += ", sad, angry, frown"
case "crazy":
prompt += ", crazy eyes, crazy smile, constricted pupils"
return (prompt,)
+53
View File
@@ -0,0 +1,53 @@
from ...node import Node
class Body(Node):
def __init__(self, seed, ident, rng):
super().__init__(seed, data_file="character.toml")
self.rng = rng
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 = data["breasts"]
breasts_size = self.select_tags(breasts_data["sizes"])
breasts_prompt = f"{breasts_size}"
lipstick = self.select_tags(self.data["mouth"]["lipstick"])
if lipstick:
lipstick_color = self.select_tags(
self.data["mouth"]["lipstick"]["colors"])
lipstick = f"{lipstick_color} lipstick"
extras = self.select_tags(data["extras"])
extras = self.enhance_extras(extras)
components = [type, color, breasts_prompt, lipstick, extras]
prompt = self.stringify_tags(components)
return (prompt,)
def enhance_extras(self, extras):
prompt = ""
match extras:
case "mole":
location = ["under eye", "under mouth", "on neck"]
location = self.select_tags(location)
prompt = f"mole {location}"
case "tatoos":
location = ["arm", "leg", "back", "neck", "full-body"]
location = self.select_tags(location)
prompt = f"{location} tatoo"
case "piercings":
location = ["ear", "nose", "navel", "lip", "eyelid"]
location = self.select_tags(location)
prompt = f"{location} piercing"
case "scar":
location = ["on neck", "on face", "on arm", "on forhead",
"on cheek", "accross eye", "on nose"]
location = self.select_tags(location, n=[1, 2])
prompt = f"scar {location}"
return prompt
+23
View File
@@ -0,0 +1,23 @@
from ...node import Node
from .body import Body
from .hair import Hair
from .eyes import Eyes
from .attitude import Attitude
class Character(Node):
def build_prompt(self, seed, ident):
super().build_prompt(seed)
components = {
'body': Body(self.seed, ident, self.rng),
'hair': Hair(self.seed, ident, self.rng),
'eyes': Eyes(self.seed, ident, self.rng),
'attitude': Attitude(self.seed, ident, self.rng)
}
prompt = ", ".join(str(component) for component in components.values())
prompt = str(prompt)
return (prompt,)
+52
View File
@@ -0,0 +1,52 @@
from ...node import Node
class Eyes(Node):
def __init__(self, seed, ident, rng):
super().__init__(seed, data_file="character.toml")
self.rng = rng
def build_prompt(self):
data = self.data["eyes"]
suffix = "eyes"
color = self.select_tags(data["colors"])
makeup = self.build_makeup_prompt(data["makeup"])
eyewear = self.build_eyewear_prompt(data["eyewears"])
prompt = f"{color} {suffix}, {makeup}, {eyewear}"
return (prompt,)
def build_makeup_prompt(self, data):
makeup = ""
makeup_type = self.select_tags(data)
if makeup_type:
makeup_color = self.select_tags(data["colors"])
makeup = f"{makeup_color} {makeup_type}"
return makeup
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 = "framed"
if "goggles" in type:
colored_element = "tinted"
position = self.select_tags(["on head", "around neck"])
type = f"{type}, {type} {position}"
if "sunglasses" in type:
colored_element = "tinted"
tags = [
f"{type}",
f"{color}-{colored_element} {suffix}",
]
eyewear_prompt = self.stringify_tags(
tags) if self.rng.random() < p else ""
return eyewear_prompt
@@ -3,16 +3,27 @@ from ...node import Node
class Hair(Node):
def __init__(self, seed):
def __init__(self, seed, ident, rng):
super().__init__(seed, data_file="character.toml")
self.rng = rng
def build_prompt(self):
data = self.data["hair"]
suffix = "hair"
color = self.select_tags(data["colors"])
if color == "gradient":
color = self.enhance_gradient()
length = self.select_tags(data["lengths"])
style = self.select_tags(data["styles"])
prompt = f"{color} {suffix}, {length} {suffix}, {style}"
return (prompt,)
def enhance_gradient(self):
color_1 = self.select_tags(
self.data["hair"]["colors"]["standard_colors"])
color_2 = self.select_tags(
self.data["hair"]["colors"]["special_colors"])
return f"gradient hair, {color_1} hair, {color_2}"
@@ -3,38 +3,42 @@ from ..node import Node
class Clothes(Node):
def __init__(self, seed=0, state="random", type="random"):
def __init__(self, seed=0, state="clothed", type="random"):
super().__init__(seed, data_file="clothes.toml")
self.state = state
self.type = type
super().__init__(seed, data_file="clothes.toml")
def build_prompt(self, seed):
self.seed = seed
def build_prompt(self, seed, ident):
super().build_prompt(seed)
prompt = ""
if self.state == "random":
self.state = self.select_tags(self.data["states"])
state = self.select_tags(self.data["states"], selected=self.state)
type = self.select_tags(self.data["types"], selected=self.type)
if self.type == "random":
self.type = self.select_tags(self.data["types"])
match self.state:
match state:
case "clothed":
prompt = self.build_clothes()
cloth = self.build_clothes(type)
case "underwear":
prompt = self.build_underwear()
cloth = self.build_underwear()
case "nude":
prompt = "nude"
cloth = "nude"
accessories = self.build_accessory()
prompt = f"{cloth}, {accessories}"
return (prompt,)
def build_clothes(self):
def build_clothes(self, type):
prompt = ""
match self.type:
match 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")
colors = self.data["colors"]
items = self.data["casual"]
vest = self.build_piece(colors, items["vest"])
top = self.build_piece(colors, items["top"])
bottom = self.build_piece(colors, items["bottom"])
prompt = f"{vest}, {top}, {bottom}"
case "dress":
@@ -67,26 +71,20 @@ class Clothes(Node):
prompt = f"{color} bra, {color} panties"
return prompt
def build_piece(self, colors, items):
class Piece(Node):
"""Return a colored piece of clothing"""
color = self.select_tags(colors)
item = self.select_tags(items)
piece = f"{color} {item}"
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 == "":
if item == "":
piece = ""
prompt = f"{piece}"
return prompt
return piece
def __str__(self):
return self.build_prompt()
def build_accessory(self):
accessories = self.select_tags(self.data["accessories"])
colors = self.data["colors"]
accessory = self.build_piece(colors, accessories.split(","))
return accessory
@@ -9,8 +9,7 @@ class Composition(Node):
@classmethod
def INPUT_TYPES(cls):
inputs = super().INPUT_TYPES()
seed = inputs["required"]["seed"]
data = cls(seed).data
data = cls().data
prefix = data["prefix"]
protagonists = cls().build_inputs_list(data["protagonists"])
@@ -24,12 +23,13 @@ class Composition(Node):
"camera_angle": (camera_angles,),
"seed": inputs["required"]["seed"]
}
return inputs
def build_prompt(self, seed, prefix, protagonists,
def build_prompt(self, seed, ident, prefix, protagonists,
camera_framing, camera_angle):
self.seed = seed
super().build_prompt(seed)
components = {
"prefix": {
@@ -6,16 +6,16 @@ class Environment(Node):
def __init__(self, seed=0):
super().__init__(seed, data_file="environment.toml")
def build_prompt(self, seed):
self.seed = seed
def build_prompt(self, seed, ident):
super().build_prompt(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)
weather = self.build_weather(time, location)
components = [time, effects, weather, location]
components = [time, effects, weather, location_type, location]
if location_type == "indoors":
components.remove(weather)
@@ -23,12 +23,17 @@ class Environment(Node):
prompt = ", ".join(components)
return (prompt,)
def build_weather(self, location):
def build_weather(self, time, location):
weather = self.data["weather"]
remove_from_list = []
if time == "night":
remove_from_list += ["sun"]
if location == "beach":
remove_from_list = ["snow", "rain"]
weather = [w for w in weather if w not in remove_from_list]
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
+3
View File
@@ -0,0 +1,3 @@
from pathlib import Path
ROOT_DIR = Path(__file__).parent.parent
+84
View File
@@ -0,0 +1,84 @@
# CozySpoke – Widget manipulation with API
# https://github.com/cozy-comfyui/cozy_spoke
import time
# import toml
# from pathlib import Path
from typing import Any
from aiohttp import web # , ClientSession
from server import PromptServer
from .node import Node
EVENT_COZY_UPDATE = "cozy-event-combo-update"
class TimedOutException(Exception):
pass
class ComfyAPIMessage:
"""
This is to collect messages from JS for nodes that are parsing/looking
for messages.
"""
MESSAGE = {}
@classmethod
def poll(cls, ident, period=0.01, timeout=3) -> Any:
_t = time.monotonic()
if isinstance(ident, (set, list, tuple, )):
ident = ident[0]
sid = str(ident)
while not (sid in cls.MESSAGE) and time.monotonic() - _t < timeout:
time.sleep(period)
if not (sid in cls.MESSAGE):
raise TimedOutException
dat = cls.MESSAGE.pop(sid)
return dat
def initialize_cozy_spoke():
data = Node.load_data("actions.toml")
@PromptServer.instance.routes.get("/cozy_spoke")
async def route_cozy_spoke(request) -> Any:
"""A simple lookup for the data provided.
The function names need to be unique for each 'route'.
Returns all messages stored in the Message Bus.
"""
return web.json_response(ComfyAPIMessage.MESSAGE)
@PromptServer.instance.routes.post("/cozy_spoke")
async def route_cozy_spoke_combo(request) -> Any:
"""A catch all route to pass messages for specific node messages needed
during node execution. The node itself will search the message bucket
and can process during its run function call."""
json_data = await request.json()
if (did := json_data.get("id")) is not None:
# stores the data-call for the instanced node (by id)
# to check on execute
ComfyAPIMessage.MESSAGE[str(did)] = json_data
return web.json_response(json_data)
return web.json_response({})
@PromptServer.instance.routes.post("/cozy_spoke/node")
async def route_cozy_spoke_update(request) -> Any:
"""A specific route for a specific node
to process outside of an execution run.
Here we manipulate the values output based on the selection in ComboA
"""
# Show/Hide widgets according to data dict
json_data = await request.json()
result = {}
if (response := json_data.get("data")) is not None:
acts = data["nsfw"]["acts"]
selected_acts = acts.get(response.lower(), [])
inputs = Node.build_inputs_list(selected_acts)
result = {'data': inputs}
return web.json_response(result)
+63 -26
View File
@@ -1,7 +1,8 @@
import numpy as np
import toml
from server import PromptServer
from .utils import ROOT_DIR
from .constants import ROOT_DIR
class Node:
@@ -11,22 +12,22 @@ class Node:
def __init__(self, seed=0, data_file=""):
self.seed = seed
self.rng = np.random.default_rng(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
"required": {
"seed": ("INT", {
"default": 0,
"min": 0,
"max": 0xffffffffffffffff
})
},
"optional": {},
"hidden": {"ident": "UNIQUE_ID"}
}
return inputs
@@ -34,7 +35,8 @@ class Node:
FUNCTION = "build_prompt"
CATEGORY = "🎞️ Scene Composer"
def build_prompt(self):
def build_prompt(self, seed):
self.rng = np.random.default_rng(seed)
pass
def select_tags(self, tags, p=1, n=1, selected="random", recursive=True):
@@ -43,7 +45,6 @@ class Node:
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
@@ -53,19 +54,24 @@ class Node:
p = tags.get("probability", p)
n = tags.get("repeat", n)
if rng.random() > p:
if self.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))
n = self.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]
selected_from_list = selected_from_list.split(", ")
tag_list = []
for item in selected_from_list:
tag_list.append(self.select_tags(tags[item]))
tags = tag_list
elif "tags" in tags:
tags = tags.get("tags", [])
@@ -73,7 +79,7 @@ class Node:
# Handle recursive tags
else:
first_level_keys = list(tags.keys())
chosen_key = rng.choice(first_level_keys)
chosen_key = self.rng.choice(first_level_keys)
chosen_value = tags[chosen_key]
if isinstance(chosen_value, (list, dict)):
@@ -85,7 +91,13 @@ class Node:
if isinstance(tags, list):
tag_names, weights = self.parse_tag_distribution(tags)
selected_tags = rng.choice(
# 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 = self.rng.integers(int(min_n), int(max_n))
selected_tags = self.rng.choice(
tag_names,
size=n,
replace=False,
@@ -99,12 +111,14 @@ class Node:
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"""
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('?')
if '[' in tag and ']' in tag:
tag_name, weight = tag.split('[')
weight = weight.rstrip(']')
tag_names.append(tag_name)
weights.append(float(weight))
else:
@@ -127,17 +141,40 @@ class Node:
@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
inputs_list = ["random"]
if "tags" in data:
data = data["tags"]
for tag in data:
if '[' in tag and ']' in tag:
tag = tag.split('[')[0]
inputs_list.append(tag)
return inputs_list
@staticmethod
def stringify_tags(tags):
"""Return a string from a list of tags"""
tags = ", ".join(map(str, tags))
tags = tags.replace(", ,", ",")
"""Return a string from a list of tags
Remove extra commas and spaces"""
# Convert numpy array to list if necessary
if isinstance(tags, np.ndarray):
tags = tags.tolist()
# Convert all tags to strings and join them with commas
tags = ', '.join(map(str, tags))
# Remove extra commas and spaces
tags = ', '.join(filter(None, map(str.strip, tags.split(','))))
return tags
def __str__(self, **kwargs):
return self.build_prompt(**kwargs)[0]
# Generic function to send messages to Javascript
@staticmethod
def comfy_message(route: str, ident: str, data: dict) -> None:
data['id'] = ident
PromptServer.instance.send_sync(route, data)
+26 -16
View File
@@ -1,6 +1,5 @@
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
@@ -14,28 +13,29 @@ class Scene(Node):
self.components = {
"composition": {
"default": Composition(),
"default": None,
"prefix": '',
"protagonists": '1girl, solo',
"camera_framing": 'random',
"camera_angle": 'random'
"camera_angle": 'random',
},
"action": {
"default": Action(),
"nsfw": True,
"default": None,
"nsfw": False,
"position": "random",
"gesture": "random",
"act_type": "random",
"act": "random"
"act": "random",
"cum": False
},
"character": {
"default": Character(),
"default": None,
},
"clothes": {
"default": Clothes(),
"default": None,
},
"environment": {
"default": Environment(),
"default": None,
}
}
@@ -46,9 +46,7 @@ class Scene(Node):
components = cls().components
inputs["required"] = {
"seed": seed
}
inputs["required"] = {"seed": seed}
# Add the components as optional inputs
optional_components = {
@@ -58,13 +56,25 @@ class Scene(Node):
})
for key in components.keys()
}
inputs["optional"] = optional_components
inputs["hidden"] = {"ident": "UNIQUE_ID"}
return inputs
def build_prompt(self, seed, **kwargs):
components = self.components
def build_prompt(self, seed, ident, **kwargs):
prompt = []
components = self.components
default_components = {
"composition": Composition(seed),
"action": Action(seed),
"character": Character(seed),
"clothes": Clothes(seed),
"environment": Environment(seed)
}
# Build each component's prompt
# If the component is in the kwargs, use it
@@ -74,10 +84,10 @@ class Scene(Node):
tags = ""
if "default" in component_data:
default = component_data["default"]
default = default_components[component_name]
args = component_data.copy()
args.pop("default")
tags = default.build_prompt(seed, **args)[0]
tags = default.build_prompt(seed, ident, **args)[0]
if component_name in kwargs.keys():
tags = kwargs[component_name] + ", "
+28
View File
@@ -0,0 +1,28 @@
[project]
name = "comfyui-scene-composer"
description = "A collection of nodes to facilitate the creation of scenes in ComfyUI."
version = "0.1.2"
license = { file = "LICENSE" }
dependencies = [
"aiohappyeyeballs==2.4.3",
"aiohttp==3.10.10",
"aiosignal==1.3.1",
"attrs==24.2.0",
"frozenlist==1.5.0",
"idna==3.10",
"multidict==6.1.0",
"numpy==2.1.2",
"propcache==0.2.0",
"PyYAML==6.0.2",
"toml==0.10.2",
"yarl==1.17.1",
]
[project.urls]
Repository = "https://github.com/taches-ai/comfyui-scene-composer"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "taches"
DisplayName = "Scene Composer"
Icon = ""
+9
View File
@@ -1,3 +1,12 @@
aiohappyeyeballs==2.4.3
aiohttp==3.10.10
aiosignal==1.3.1
attrs==24.2.0
frozenlist==1.5.0
idna==3.10
multidict==6.1.0
numpy==2.1.2
propcache==0.2.0
PyYAML==6.0.2
toml==0.10.2
yarl==1.17.1
-23
View File
@@ -1,23 +0,0 @@
from ...node import Node
class Body(Node):
def __init__(self, seed):
super().__init__(seed, data_file="character.toml")
def build_prompt(self):
data = self.data["body"]
type = self.select_tags(data["types"])
colors = self.select_tags(data["colors"])
color = f"{colors} skin" if colors else ""
breasts_data = self.data["body"]["breasts"]
breasts_size = self.select_tags(breasts_data["sizes"])
breasts_prompt = f"{breasts_size}"
extras = self.select_tags(data["extras"])
components = [type, color, breasts_prompt, extras]
prompt = self.stringify_tags(components)
return (prompt,)
-20
View File
@@ -1,20 +0,0 @@
from ...node import Node
from .body import Body
from .hair import Hair
from .eyes import Eyes
class Character(Node):
def build_prompt(self, seed):
components = {
'body': Body(seed),
'hair': Hair(seed),
'eyes': Eyes(seed)
}
prompt = ", ".join(str(component) for component in components.values())
prompt = str(prompt)
return (prompt,)
-34
View File
@@ -1,34 +0,0 @@
from ...node import Node
from ...utils import is_true
class Eyes(Node):
def __init__(self, seed):
super().__init__(seed, data_file="character.toml")
def build_prompt(self):
data = self.data["eyes"]
suffix = "eyes"
color = self.select_tags(data["colors"])
eyewear = self.build_eyewear_prompt(data["eyewears"])
prompt = f"{color} {suffix}, {eyewear}"
return (prompt,)
def build_eyewear_prompt(self, eyewear):
p = eyewear["probability"]
suffix = "eyewear"
type = self.select_tags(eyewear["types"], p)
color = self.select_tags(eyewear["colors"], p)
colored_element = self.select_tags(["framed", "tinted"])
tags = [
f"{type}",
f"{color}-{colored_element} {suffix}",
]
eyewear_prompt = self.stringify_tags(
tags) if is_true(self.seed, p) else ""
return eyewear_prompt
-10
View File
@@ -1,10 +0,0 @@
from pathlib import Path
import numpy as np
ROOT_DIR = Path(__file__).parent.parent
def is_true(seed, p=0.5):
"""Return True with probability p"""
rng = np.random.default_rng(seed)
return rng.random() < p
+769
View File
@@ -0,0 +1,769 @@
{
"last_node_id": 27,
"last_link_id": 24,
"nodes": [
{
"id": 20,
"type": "Note",
"pos": {
"0": 16,
"1": 53
},
"size": {
"0": 366,
"1": 205
},
"flags": {
"collapsed": true
},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "📖 README",
"properties": {},
"widgets_values": [
"Here's a simple workflow to test the Scene Composer custom nodes.\n\nJust queue a prompt: the 🎞️ Scene will be able to randomly generate an image for you!\n\nHere, optional components are connected to the 🎞️ Scene. That let you have more control over the generation of the scene.\n\nFor example, just change the seed of the 👤 Character. The whole scene stay the same, but the character has changed."
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 21,
"type": "Note",
"pos": {
"0": 167,
"1": 54
},
"size": {
"0": 458,
"1": 208
},
"flags": {
"collapsed": true
},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "🔗 Links",
"properties": {},
"widgets_values": [
"For more info, check out these links:\n\nSource code: https://github.com/mus-taches/comfyui-scene-composer\nWiki: https://github.com/mus-taches/comfyui-scene-composer/wiki\nRoadmap: https://github.com/users/mousse-taches/projects/2/"
],
"color": "#222",
"bgcolor": "#000"
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": {
"0": 738,
"1": 527
},
"size": {
"0": 210,
"1": 106
},
"flags": {
"collapsed": false
},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1024,
1024,
1
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": {
"0": 750,
"1": 304
},
"size": {
"0": 210,
"1": 54
},
"flags": {
"collapsed": true
},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
},
{
"name": "text",
"type": "STRING",
"link": 19,
"widget": {
"name": "text"
}
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"title": "Positive",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 3,
"type": "KSampler",
"pos": {
"0": 985,
"1": 273
},
"size": {
"0": 267,
"1": 262
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
25,
7,
"euler",
"normal",
0.9500000000000001
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 9,
"type": "SaveImage",
"pos": {
"0": 1280,
"1": 240
},
"size": {
"0": 327,
"1": 302.48333740234375
},
"flags": {},
"order": 14,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 8,
"type": "VAEDecode",
"pos": {
"0": 1271,
"1": 175
},
"size": {
"0": 210,
"1": 46
},
"flags": {
"collapsed": true
},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": [],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": {
"0": 386,
"1": 108
},
"size": {
"0": 312,
"1": 98
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"tponyai.safetensors"
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 22,
"type": "Scene",
"pos": {
"0": 383,
"1": 434
},
"size": {
"0": 315,
"1": 202
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "composition",
"type": "STRING",
"link": 20,
"widget": {
"name": "composition"
},
"shape": 7
},
{
"name": "action",
"type": "STRING",
"link": 21,
"widget": {
"name": "action"
},
"shape": 7
},
{
"name": "character",
"type": "STRING",
"link": 22,
"widget": {
"name": "character"
},
"shape": 7
},
{
"name": "clothes",
"type": "STRING",
"link": 23,
"widget": {
"name": "clothes"
},
"shape": 7
},
{
"name": "environment",
"type": "STRING",
"link": 24,
"widget": {
"name": "environment"
},
"shape": 7
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
19
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Scene"
},
"widgets_values": [
310131604611505,
"fixed",
"",
"",
"",
"",
""
]
},
{
"id": 23,
"type": "Composition",
"pos": {
"0": 15,
"1": 111
},
"size": {
"0": 326.8691101074219,
"1": 196.00003051757812
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
20
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Composition"
},
"widgets_values": [
"score_9, score_8_up, score_7_up, source_anime",
"1girl",
"random",
"random",
676629682859978,
"fixed"
]
},
{
"id": 24,
"type": "Action",
"pos": {
"0": 22,
"1": 367
},
"size": {
"0": 315,
"1": 154
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
21
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Action"
},
"widgets_values": [
false,
"random",
"random",
"random",
"random",
false,
357446330605007,
"fixed"
]
},
{
"id": 27,
"type": "Environment",
"pos": {
"0": 20,
"1": 844
},
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
24
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Environment"
},
"widgets_values": [
19523959859772,
"fixed"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": {
"0": 738,
"1": 352
},
"size": {
"0": 210,
"1": 119.52225494384766
},
"flags": {
"collapsed": false
},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"title": "Negative",
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"score_6, score_5, score_4\nchild"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 25,
"type": "Character",
"pos": {
"0": 25,
"1": 574
},
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
22
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Character"
},
"widgets_values": [
615775202884440,
"fixed"
]
},
{
"id": 26,
"type": "Clothes",
"pos": {
"0": 21,
"1": 709
},
"size": {
"0": 315,
"1": 82
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [
23
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Clothes"
},
"widgets_values": [
1032014846174790,
"fixed"
]
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
19,
22,
0,
6,
1,
"STRING"
],
[
20,
23,
0,
22,
0,
"STRING"
],
[
21,
24,
0,
22,
1,
"STRING"
],
[
22,
25,
0,
22,
2,
"STRING"
],
[
23,
26,
0,
22,
3,
"STRING"
],
[
24,
27,
0,
22,
4,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.6830134553650705,
"offset": [
337.62860000000046,
42.49950000000007
]
}
},
"version": 0.4
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB