Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f75d90808c | ||
|
|
7a6085dc71 | ||
|
|
6d01bc9ab6 | ||
|
|
d027d9f965 | ||
|
|
0abfd24eb4 | ||
|
|
f752e4b3a9 | ||
|
|
5bd2b45fa5 | ||
|
|
ae8be8cd46 | ||
|
|
cdec42e57d | ||
|
|
b82192408a | ||
|
|
ace5fcc34d | ||
|
|
dcf5128ede | ||
|
|
252fa25d35 | ||
|
|
c6f010c653 | ||
|
|
3690bdb2c4 | ||
|
|
90af275b71 | ||
|
|
8253cc14b9 | ||
|
|
721a38ed98 | ||
|
|
0d1f0824ca | ||
|
|
cd7485fe98 | ||
|
|
cac90e3e64 | ||
|
|
13fdd9e447 | ||
|
|
aa77f3ee26 | ||
|
|
361e86adc5 | ||
|
|
dc13b09eb7 | ||
|
|
f3dfcb93ca | ||
|
|
0131ceba4e | ||
|
|
7c3739508d | ||
|
|
12d66e065d | ||
|
|
aa86a9f20e | ||
|
|
8100f659f1 | ||
|
|
97c2fce17a | ||
|
|
8a4274f9d6 | ||
|
|
c013ee0888 | ||
|
|
1f8b9072b5 | ||
|
|
c1d18991d8 | ||
|
|
1d6dc0e971 | ||
|
|
edd287d548 | ||
|
|
4c2454bebb | ||
|
|
685273eecd | ||
|
|
33ab93010c | ||
|
|
d1b774d7ce | ||
|
|
b4c965b1a5 | ||
|
|
b94ba237a1 | ||
|
|
a7d498a081 | ||
|
|
8fe532f2f7 | ||
|
|
4d67a6b4cc | ||
|
|
1ac972f912 | ||
|
|
aa26f4aae2 | ||
|
|
b78501d4e2 | ||
|
|
acdcb0032b | ||
|
|
ed31d7dd82 | ||
|
|
4ab36b4470 | ||
|
|
a22a2e9023 | ||
|
|
489c526404 | ||
|
|
c51e71aaf4 | ||
|
|
bd2cc15447 | ||
|
|
d71cb04f88 | ||
|
|
035b2c3003 | ||
|
|
dc9bb0ad48 | ||
|
|
5feda0fbab |
@@ -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 }}
|
||||
@@ -83,6 +83,7 @@ coverage.xml
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
test.py
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,107 +2,53 @@
|
||||
|
||||
A collection of [ComfyUI](https://github.com/comfyanonymous/ComfyUI) nodes to create scenes with random procedural generation.
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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
|
||||
@@ -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,)
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT_DIR = Path(__file__).parent.parent
|
||||
@@ -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
@@ -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
@@ -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] + ", "
|
||||
@@ -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 = ""
|
||||
@@ -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
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from ...node import Node
|
||||
|
||||
|
||||
class Body(Node):
|
||||
|
||||
def __init__(self, seed):
|
||||
super().__init__(seed, data_file="character.toml")
|
||||
|
||||
def build_prompt(self):
|
||||
data = self.data["body"]
|
||||
type = self.select_tags(data["types"])
|
||||
colors = self.select_tags(data["colors"])
|
||||
color = f"{colors} skin" if colors else ""
|
||||
|
||||
breasts_data = self.data["body"]["breasts"]
|
||||
breasts_size = self.select_tags(breasts_data["sizes"])
|
||||
breasts_prompt = f"{breasts_size}"
|
||||
|
||||
extras = self.select_tags(data["extras"])
|
||||
|
||||
components = [type, color, breasts_prompt, extras]
|
||||
prompt = self.stringify_tags(components)
|
||||
return (prompt,)
|
||||
@@ -1,20 +0,0 @@
|
||||
from ...node import Node
|
||||
|
||||
from .body import Body
|
||||
from .hair import Hair
|
||||
from .eyes import Eyes
|
||||
|
||||
|
||||
class Character(Node):
|
||||
|
||||
def build_prompt(self, seed):
|
||||
|
||||
components = {
|
||||
'body': Body(seed),
|
||||
'hair': Hair(seed),
|
||||
'eyes': Eyes(seed)
|
||||
}
|
||||
|
||||
prompt = ", ".join(str(component) for component in components.values())
|
||||
prompt = str(prompt)
|
||||
return (prompt,)
|
||||
@@ -1,34 +0,0 @@
|
||||
from ...node import Node
|
||||
from ...utils import is_true
|
||||
|
||||
|
||||
class Eyes(Node):
|
||||
|
||||
def __init__(self, seed):
|
||||
super().__init__(seed, data_file="character.toml")
|
||||
|
||||
def build_prompt(self):
|
||||
data = self.data["eyes"]
|
||||
suffix = "eyes"
|
||||
color = self.select_tags(data["colors"])
|
||||
eyewear = self.build_eyewear_prompt(data["eyewears"])
|
||||
|
||||
prompt = f"{color} {suffix}, {eyewear}"
|
||||
return (prompt,)
|
||||
|
||||
def build_eyewear_prompt(self, eyewear):
|
||||
p = eyewear["probability"]
|
||||
|
||||
suffix = "eyewear"
|
||||
type = self.select_tags(eyewear["types"], p)
|
||||
color = self.select_tags(eyewear["colors"], p)
|
||||
colored_element = self.select_tags(["framed", "tinted"])
|
||||
|
||||
tags = [
|
||||
f"{type}",
|
||||
f"{color}-{colored_element} {suffix}",
|
||||
]
|
||||
|
||||
eyewear_prompt = self.stringify_tags(
|
||||
tags) if is_true(self.seed, p) else ""
|
||||
return eyewear_prompt
|
||||
@@ -1,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
@@ -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
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user