Add per-run random rolls to action pickers
This commit is contained in:
@@ -106,7 +106,7 @@ angles, POV of either person, over the shoulder, handheld, locked off), 12 light
|
||||
setups, 15 outfits and 9 styles. Aspect ratio stays with the Empty Latent / resolution node. Category filters the action list; every action states the exact body
|
||||
arrangement, hands, legs, face and camera so there is no ambiguity. `source` switches between keeping
|
||||
the person in the connected image (image-to-image) and inventing them from `subject_1` / `subject_2`
|
||||
text (text-to-image). Notes and overrides are appended last. The enhancer has `use_action`; turn it off to ignore the action for a fully custom prompt without disconnecting the node. Output connects to the enhancer's `action` input; while `use_action` is on the brief replaces the enhancer prompt, and the `instructions` field carries custom additions.
|
||||
text (text-to-image). Notes and overrides are appended last. Every picker (category, action, place, camera, lighting, outfit, style) has a `random` option that rolls a concrete value on each run and shows the roll on the node. The enhancer has `use_action`; turn it off to ignore the action for a fully custom prompt without disconnecting the node. Output connects to the enhancer's `action` input; while `use_action` is on the brief replaces the enhancer prompt, and the `instructions` field carries custom additions.
|
||||
|
||||
### PromptForge Enhancer (`PROMPTFORGE_PromptEnhancer`)
|
||||
|
||||
@@ -255,7 +255,7 @@ Makes a long MiniMax H3 video part by part. One node does the generating (**Prom
|
||||
|---|---|
|
||||
| **H3 Modes** | What writes the prompt (`prompt_source`: my idea or action preset; the other node is ignored) and what kind of video: **T2V** (text only, empty latent), **I2V** (first frame encoded and anchored at frame 0), **L2V** (last frame anchored at the end), **FL2V** (both), **REF2V** (pictures, videos and sounds encoded as references). Also one clip or several parts, extend an existing video, second stage on/off, and the prompt source: your idea, the Action node, or the Parts node. Its overview lists the files chosen in the Load nodes with a tick for each; rows the mode does not use are dimmed. Each mode also picks its prompt template (T2VA, I2VA, L2VA, FL2VA, Ref2VA). |
|
||||
| **H3 Idea (my text)** | Your own text with the saved-ideas bar (Save / Save as / Delete, arrows to flip through; stored in `user/promptforge/ideas.json`), negative prompt and dialogue with its language. |
|
||||
| **H3 Action (adult presets)** | A solo act or a position (57, each with an explicit who-is-where definition and a recommended 9:16 or 16:9 frame), a camera angle (13) and who is who: every role has its own picture picker (Woman, Man, Partner) for a reference picture by its input slot, the first frame, none and only a description (`look` text), or `disabled`. Subject 3 defaults to `disabled`; a picture left unattached to any role is passed to the writer as an anatomy/detail reference, not as a third person. The roles of the chosen action are shown under it (for cowgirl: subject 1 on top, subject 2 on the back). Notes carry setting and mood. The prompt writer writes the whole video prompt template as one continuous shot with the act already in progress, for one clip or spread over the parts: lead-in, main action, peak. Works with every mode; where a picture is not available the person is described in words. |
|
||||
| **H3 Action (adult presets)** | A solo act or a position (57, each with an explicit who-is-where definition and a recommended 9:16 or 16:9 frame), a camera angle (13) and who is who: every role has its own picture picker (Woman, Man, Partner) for a reference picture by its input slot, the first frame, none and only a description (`look` text), or `disabled`. Subject 3 defaults to `disabled`; a picture left unattached to any role is passed to the writer as an anatomy/detail reference, not as a third person. The roles of the chosen action are shown under it (for cowgirl: subject 1 on top, subject 2 on the back). Category, act, camera and penetration all have `random`, rolled per run and shown on the node. Notes carry setting and mood. The prompt writer writes the whole video prompt template as one continuous shot with the act already in progress, for one clip or spread over the parts: lead-in, main action, peak. Works with every mode; where a picture is not available the person is described in words. |
|
||||
| **H3 Prompt Writer Settings** | How the writer writes, for the Idea and the Action node alike: use the writer or not, template (one NSFW-capable template per mode; a template you pick wins over the style), style, temperature, top_p, top_k, repetition penalty, max tokens, retries, approval. |
|
||||
| **H3 Parts** (optional) | A long-video node: the same action category/act, penetration, camera and subject pickers as Action, plus one custom note line per part (speed up, pause and talk, pull out, come). Used when Modes says several parts and the prompt source is Parts; the action brief is written per part with that part's note. A box can start with `(10s)` for its own length or `raw:` for a finished prompt. Without it the generator makes one clip. |
|
||||
| **H3 References** | The first and last frame (I2V, L2V, FL2V), up to 9 reference pictures, 3 reference videos (with soundtracks) and 3 reference sounds, each with a description (wire a Load Image / Load Audio DESCRIPTION, or type it). The prompt writer is told what `<Picture N>`, `<Video N>` and `<Audio N>` are, so the Story can name them; the descriptions stay put while you rewrite the Story. |
|
||||
|
||||
+46
-10
@@ -20,7 +20,7 @@ import importlib
|
||||
|
||||
import comfy.samplers
|
||||
|
||||
from . import h3_actions, h3_upscaler, preview_decoder
|
||||
from . import h3_actions, h3_upscaler, preview_decoder, random_picks
|
||||
from . import empty_latent
|
||||
|
||||
CATEGORY = "🔮 PromptForge/Endless Video"
|
||||
@@ -82,6 +82,34 @@ class _Settings:
|
||||
return (values,)
|
||||
|
||||
|
||||
def _roll_action(values, node_id):
|
||||
"""Resolve the action pickers set to random once per run, and show the roll on the node."""
|
||||
if "action" not in values:
|
||||
return values
|
||||
picks = []
|
||||
category = values.get("category")
|
||||
if values["action"] == random_picks.RANDOM:
|
||||
pool = [label for label in h3_actions.ACTIONS
|
||||
if category == random_picks.RANDOM or h3_actions.action_category(label) == category]
|
||||
values["action"] = random_picks.pick(pool)
|
||||
label = values["action"]
|
||||
crafted = h3_actions.action_category(label)
|
||||
if category == random_picks.RANDOM or category != crafted:
|
||||
values["category"] = crafted
|
||||
picks.append(("Category", crafted))
|
||||
picks.append(("Action", label))
|
||||
if values.get("camera_angle") == random_picks.RANDOM:
|
||||
values["camera_angle"] = random_picks.pick(h3_actions.cameras(label) or h3_actions.CAMERAS)
|
||||
picks.append(("Camera", values["camera_angle"]))
|
||||
if values.get("penetration") == random_picks.RANDOM:
|
||||
modes = h3_actions.penetration_modes(label)
|
||||
values["penetration"] = random_picks.pick(modes) if modes else "vaginal"
|
||||
if modes:
|
||||
picks.append(("Penetration", values["penetration"]))
|
||||
random_picks.announce(node_id, picks)
|
||||
return values
|
||||
|
||||
|
||||
def _dialogue_inputs():
|
||||
return {
|
||||
"negative_prompt": ("STRING", {"default": "", "multiline": True, "tooltip": "Things the video must not contain."}),
|
||||
@@ -117,7 +145,7 @@ class PROMPTFORGE_H3Action(_Settings):
|
||||
DESCRIPTION = ("Adult content from a preset instead of your own text: a solo act or a position with clearly defined roles, who is who, and a camera angle. Every role has its own picture picker, "
|
||||
"so the woman and the man each get their own reference picture (Picture 1, 2, 3 ...), the first frame, or none and only a description. The preset also recommends the frame shape: "
|
||||
"9:16 for standing, sitting, on-top and solo action, 16:9 for lying and side arrangements. The prompt writer writes the whole video prompt (the mode's template), for one clip or spread over the parts. "
|
||||
"Works with every mode. The Modes node chooses between this node and the Idea node.")
|
||||
"Every action picker has a random option that rolls a concrete value per run and shows it on the node. Works with every mode. The Modes node chooses between this node and the Idea node.")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -127,18 +155,22 @@ class PROMPTFORGE_H3Action(_Settings):
|
||||
subjects[f"subject_{number}_look"] = ("STRING", {"default": "", "multiline": True, "tooltip": "The person in words: hair, body, face, age. Used when no picture is chosen for this role, and added to the picture when one is."})
|
||||
return {
|
||||
"required": {
|
||||
"category": (list(h3_actions.ACTION_CATEGORIES), {"default": h3_actions.ACTION_CATEGORIES[0], "tooltip": "Filters the action list; the act itself is chosen below."}),
|
||||
"action": (list(h3_actions.ACTIONS), {"default": next(iter(h3_actions.ACTIONS)), "tooltip": "The act or position. The roles of the subjects are shown below it."}),
|
||||
"camera_angle": ([h3_actions.ANY_CAMERA] + list(h3_actions.CAMERAS), {"default": h3_actions.ANY_CAMERA, "tooltip": "How it is filmed. POV (the viewer is B) is what subject 2 sees."}),
|
||||
"category": ([random_picks.RANDOM] + list(h3_actions.ACTION_CATEGORIES), {"default": h3_actions.ACTION_CATEGORIES[0], "tooltip": "Filters the action list; the act itself is chosen below. random rolls a category and an action."}),
|
||||
"action": ([random_picks.RANDOM] + list(h3_actions.ACTIONS), {"default": next(iter(h3_actions.ACTIONS)), "tooltip": "The act or position. The roles of the subjects are shown below it. random rolls one from the category."}),
|
||||
"camera_angle": ([random_picks.RANDOM, h3_actions.ANY_CAMERA] + list(h3_actions.CAMERAS), {"default": h3_actions.ANY_CAMERA, "tooltip": "How it is filmed. POV (the viewer is B) is what subject 2 sees. random rolls a useful camera for the act."}),
|
||||
},
|
||||
"optional": {
|
||||
"penetration": (list(h3_actions.PENETRATION_MODES), {"default": "vaginal", "tooltip": "For penetrative sex: vagina or anus. Forces the named hole for the whole clip so the writer never swaps them."}),
|
||||
"penetration": ([random_picks.RANDOM] + list(h3_actions.PENETRATION_MODES), {"default": "vaginal", "tooltip": "For penetrative sex: vagina or anus. Forces the named hole for the whole clip so the writer never swaps them. random rolls one when the act uses it."}),
|
||||
**subjects,
|
||||
"notes": ("STRING", {"default": "", "multiline": True, "placeholder": "Setting, looks, mood, sounds. Not the act: that is the action above.", "tooltip": "Anything else the video needs: the room, the lighting, the mood, the sounds."}),
|
||||
**_dialogue_inputs(),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
def build(self, unique_id=None, **values):
|
||||
return (_roll_action(values, unique_id),)
|
||||
|
||||
|
||||
class PROMPTFORGE_H3Writer(_Settings):
|
||||
KEY = "writer"
|
||||
@@ -189,18 +221,22 @@ class PROMPTFORGE_H3Parts(_Settings):
|
||||
"parts": ("INT", {"default": 3, "min": 1, "max": 200, "tooltip": "Number of parts. Press + / - on the node."}),
|
||||
"part_prompts": ("STRING", {"default": "", "multiline": True, "tooltip": "The part boxes (kept by the node)."}),
|
||||
"part_notes": ("STRING", {"default": "", "multiline": True, "tooltip": "One line per part with what changes in that part: 1: speed up, 2: pause and talk, 3: pull out and come on her stomach. Lines are matched to parts in order."}),
|
||||
"category": (list(h3_actions.ACTION_CATEGORIES), {"default": h3_actions.ACTION_CATEGORIES[0]}),
|
||||
"action": (list(h3_actions.ACTIONS), {"default": next(iter(h3_actions.ACTIONS))}),
|
||||
"category": ([random_picks.RANDOM] + list(h3_actions.ACTION_CATEGORIES), {"default": h3_actions.ACTION_CATEGORIES[0]}),
|
||||
"action": ([random_picks.RANDOM] + list(h3_actions.ACTIONS), {"default": next(iter(h3_actions.ACTIONS))}),
|
||||
},
|
||||
"optional": {
|
||||
"penetration": (list(h3_actions.PENETRATION_MODES), {"default": "vaginal"}),
|
||||
"camera_angle": ([h3_actions.ANY_CAMERA] + list(h3_actions.CAMERAS), {"default": h3_actions.ANY_CAMERA}),
|
||||
"penetration": ([random_picks.RANDOM] + list(h3_actions.PENETRATION_MODES), {"default": "vaginal"}),
|
||||
"camera_angle": ([random_picks.RANDOM, h3_actions.ANY_CAMERA] + list(h3_actions.CAMERAS), {"default": h3_actions.ANY_CAMERA}),
|
||||
"subject_1_from": (list(SUBJECT_SOURCES), {"default": "none (describe in words)"}), "subject_1_look": ("STRING", {"default": "", "multiline": True}),
|
||||
"subject_2_from": (list(SUBJECT_SOURCES), {"default": "none (describe in words)"}), "subject_2_look": ("STRING", {"default": "", "multiline": True}),
|
||||
"subject_3_from": (list(SUBJECT_SOURCES), {"default": SUBJECT_DISABLED}), "subject_3_look": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
def build(self, unique_id=None, **values):
|
||||
return (_roll_action(values, unique_id),)
|
||||
|
||||
|
||||
class PROMPTFORGE_H3Modes(_Settings):
|
||||
KEY = "modes"
|
||||
|
||||
+40
-11
@@ -1,5 +1,5 @@
|
||||
"""PromptForge Action node: a pose or act plus place, camera, light and outfit for still-image prompts."""
|
||||
from . import image_actions
|
||||
from . import image_actions, random_picks
|
||||
|
||||
|
||||
class PROMPTFORGE_ImageAction:
|
||||
@@ -8,22 +8,22 @@ class PROMPTFORGE_ImageAction:
|
||||
RETURN_NAMES = ("ACTION", "BRIEF")
|
||||
FUNCTION = "build"
|
||||
DESCRIPTION = ("A pose, place and camera as a binding brief for the prompt writer, for any image model. "
|
||||
"Works from an input image or from words, for one person or two.")
|
||||
"Works from an input image or from words, for one person or two. Every picker has a random option rolled per run.")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"category": (list(image_actions.CATEGORIES), {"default": image_actions.CATEGORIES[0]}),
|
||||
"action": ([f"{group} · {label}" for label, (_, group, _) in image_actions.ACTIONS.items()],
|
||||
"category": ([random_picks.RANDOM] + list(image_actions.CATEGORIES), {"default": image_actions.CATEGORIES[0]}),
|
||||
"action": ([random_picks.RANDOM] + [f"{group} · {label}" for label, (_, group, _) in image_actions.ACTIONS.items()],
|
||||
{"default": f"{next(iter(image_actions.ACTIONS.values()))[1]} · {next(iter(image_actions.ACTIONS))}"}),
|
||||
"source": (list(image_actions.SUBJECT_SOURCES), {"default": "input image",
|
||||
"tooltip": "input image: keep the person from the connected image. text description: the writer invents the person from the text below."}),
|
||||
"place": (list(image_actions.PLACES), {"default": "auto"}),
|
||||
"camera": (list(image_actions.CAMERAS), {"default": "auto"}),
|
||||
"lighting": (list(image_actions.LIGHTING), {"default": "auto"}),
|
||||
"outfit": (list(image_actions.OUTFITS), {"default": "as in the image"}),
|
||||
"style": (list(image_actions.STYLES), {"default": "auto"}),
|
||||
"place": ([random_picks.RANDOM] + list(image_actions.PLACES), {"default": "auto"}),
|
||||
"camera": ([random_picks.RANDOM] + list(image_actions.CAMERAS), {"default": "auto"}),
|
||||
"lighting": ([random_picks.RANDOM] + list(image_actions.LIGHTING), {"default": "auto"}),
|
||||
"outfit": ([random_picks.RANDOM] + list(image_actions.OUTFITS), {"default": "as in the image"}),
|
||||
"style": ([random_picks.RANDOM] + list(image_actions.STYLES), {"default": "auto"}),
|
||||
},
|
||||
"optional": {
|
||||
"subject_1": ("STRING", {"default": "", "multiline": True, "placeholder": "Who the first person is. Empty keeps the person from the input image.", "tooltip": "Identity text for the first person. With an image source this adds to the image; without one it is all the writer knows."}),
|
||||
@@ -32,11 +32,40 @@ class PROMPTFORGE_ImageAction:
|
||||
"overrides": ("STRING", {"default": "", "multiline": True, "placeholder": "Hard overrides, for example: red dress, no glasses.", "tooltip": "Passed last and wins over the rest of the brief."}),
|
||||
"instructions": ("STRING", {"default": "", "multiline": True, "placeholder": "Extra instructions for this shot: mood, prop, exact framing. Replaces the enhancer prompt while the action is on.", "tooltip": "Custom instructions appended to the action brief."}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
def build(self, category, action, source, place, camera, lighting, outfit, style,
|
||||
subject_1="", subject_2="", notes="", overrides="", instructions=""):
|
||||
action = action.split(" · ", 1)[-1]
|
||||
subject_1="", subject_2="", notes="", overrides="", instructions="", unique_id=None):
|
||||
picks = []
|
||||
if action == random_picks.RANDOM:
|
||||
pool = [label for label, (_, group, _) in image_actions.ACTIONS.items()
|
||||
if category == random_picks.RANDOM or group == category]
|
||||
action = random_picks.pick(pool)
|
||||
else:
|
||||
action = action.split(" · ", 1)[-1]
|
||||
if category == random_picks.RANDOM:
|
||||
category = image_actions.ACTIONS[action][1]
|
||||
picks.append(("Category", category))
|
||||
picks.append(("Action", action))
|
||||
if place == random_picks.RANDOM:
|
||||
place = random_picks.pick(key for key in image_actions.PLACES if key != "auto")
|
||||
picks.append(("Place", place))
|
||||
if camera == random_picks.RANDOM:
|
||||
count = image_actions.ACTIONS[action][0]
|
||||
camera = random_picks.pick(key for key in image_actions.CAMERAS
|
||||
if key != "auto" and not (count == 1 and key == "side view, full bodies"))
|
||||
picks.append(("Camera", camera))
|
||||
if lighting == random_picks.RANDOM:
|
||||
lighting = random_picks.pick(key for key in image_actions.LIGHTING if key != "auto")
|
||||
picks.append(("Lighting", lighting))
|
||||
if outfit == random_picks.RANDOM:
|
||||
outfit = random_picks.pick(key for key in image_actions.OUTFITS if key != "as in the image")
|
||||
picks.append(("Outfit", outfit))
|
||||
if style == random_picks.RANDOM:
|
||||
style = random_picks.pick(key for key in image_actions.STYLES if key != "auto")
|
||||
picks.append(("Style", style))
|
||||
random_picks.announce(unique_id, picks)
|
||||
data = {"action": action, "source": source, "place": place, "camera": camera, "lighting": lighting,
|
||||
"outfit": outfit, "style": style, "subject_1": subject_1,
|
||||
"subject_2": subject_2, "notes": notes, "overrides": overrides, "instructions": instructions}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI-PromptForge"
|
||||
description = "ComfyUI model workflow suite: model, LoRA and media loaders, prompt enhancement with per-family presets, encoding, sampling, upscaling and saving."
|
||||
version = "1.29.8"
|
||||
version = "1.29.9"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"transformers",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Random choice for the action pickers: one concrete value per run, announced to the node UI."""
|
||||
import random
|
||||
|
||||
RANDOM = "random"
|
||||
|
||||
|
||||
def pick(values):
|
||||
return random.choice(list(values))
|
||||
|
||||
|
||||
def announce(node_id, picks):
|
||||
"""Show what was rolled on the node; `picks` is [(field, value), ...]."""
|
||||
if node_id is None:
|
||||
return
|
||||
from server import PromptServer
|
||||
PromptServer.instance.send_sync("promptforge.random", {"node": str(node_id), "picks": picks}, PromptServer.instance.client_id)
|
||||
@@ -1,9 +1,17 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const NODE = "PROMPTFORGE_ImageAction";
|
||||
const RANDOM = "random";
|
||||
|
||||
app.registerExtension({
|
||||
name: "PromptForge.ImageAction",
|
||||
setup() {
|
||||
api.addEventListener("promptforge.random", (event) => {
|
||||
const node = app.graph?.getNodeById(Number(event.detail.node));
|
||||
node?.promptforgeShowRandom?.(event.detail);
|
||||
});
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== NODE) return;
|
||||
const onCreated = nodeType.prototype.onNodeCreated;
|
||||
@@ -13,21 +21,29 @@ app.registerExtension({
|
||||
const category = widget("category");
|
||||
const action = widget("action");
|
||||
if (!category || !action) return;
|
||||
const all = [...action.options.values];
|
||||
const all = [...action.options.values].filter((value) => value !== RANDOM);
|
||||
const groups = {};
|
||||
for (const label of all) {
|
||||
const prefix = label.split(" · ")[0];
|
||||
(groups[prefix] ||= []).push(label);
|
||||
}
|
||||
const apply = () => {
|
||||
const wanted = (groups[category.value] || []);
|
||||
action.options.values = wanted.length ? wanted : all;
|
||||
if (!wanted.includes(action.value)) action.value = action.options.values[0];
|
||||
const wanted = category.value === RANDOM ? all : (groups[category.value] || []);
|
||||
action.options.values = [RANDOM, ...(wanted.length ? wanted : all)];
|
||||
if (action.value !== RANDOM && !wanted.includes(action.value)) action.value = wanted[0];
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
this.promptforgeApplyActionFilter = apply;
|
||||
const callback = category.callback;
|
||||
category.callback = function () { callback?.apply(this, arguments); apply(); };
|
||||
|
||||
// what the server rolled on the last run; display only, the widgets stay on "random"
|
||||
const frame = document.createElement("div");
|
||||
frame.style.cssText = "padding:2px 6px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;color:var(--input-text,#ddd);background:var(--comfy-input-bg,#333);border:1px solid var(--border-color,#555);border-radius:4px";
|
||||
frame.textContent = "Random: run the prompt to roll the pickers set to random.";
|
||||
const box = this.addDOMWidget("random_overview", "promptforge_random", frame, { serialize: false, getMinHeight: () => 24 });
|
||||
Object.defineProperty(box, "width", { get: () => this.size[0], set: () => {}, configurable: true });
|
||||
this.promptforgeShowRandom = ({ picks }) => { frame.textContent = picks.length ? "Random rolled: " + picks.map(([name, value]) => `${name} = ${value}`).join(", ") : ""; };
|
||||
apply();
|
||||
};
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
|
||||
+41
-19
@@ -13,6 +13,7 @@ const REFERENCES = "PROMPTFORGE_H3References";
|
||||
const LIBRARY = "PROMPTFORGE_PromptLibrary";
|
||||
const SUBJECT_DISABLED = "disabled (not in the video)";
|
||||
const ANY_CAMERA = "(writer's choice)";
|
||||
const RANDOM = "random";
|
||||
// the description box of a slot is shown only while its picture / video / sound is connected and the description is not itself wired
|
||||
const DESCRIPTIONS = { description_: "image_", video_description_: "video_", audio_description_: "audio_" };
|
||||
const SIZE_WIDGETS = { "scale by multiplier": ["scale"], megapixels: ["megapixels"], "target dimensions": ["width", "height"] };
|
||||
@@ -211,6 +212,7 @@ app.registerExtension({
|
||||
const node = (event) => app.graph.getNodeById(Number(event.detail.node));
|
||||
api.addEventListener("promptforge.prompt", (event) => node(event)?.promptforgeShowPrompt?.(event.detail));
|
||||
api.addEventListener("promptforge.request", (event) => node(event)?.promptforgeShowRequest?.(event.detail));
|
||||
api.addEventListener("promptforge.random", (event) => node(event)?.promptforgeShowRandom?.(event.detail));
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name === SECOND_STAGE) {
|
||||
@@ -358,7 +360,9 @@ app.registerExtension({
|
||||
const action = find("action");
|
||||
const category = find("category");
|
||||
const penetration = find("penetration");
|
||||
const allActions = [...(action.options?.values || [])];
|
||||
const cameraWidget = find("camera_angle");
|
||||
const allActions = [...(action.options?.values || [])].filter((value) => value !== RANDOM);
|
||||
const allCameras = [...(cameraWidget?.options?.values || [])].filter((value) => value !== RANDOM && value !== ANY_CAMERA);
|
||||
node.widgets.splice(node.widgets.indexOf(roles), 1);
|
||||
node.widgets.splice(node.widgets.indexOf(action) + 1, 0, roles);
|
||||
const nameFor = (gender, index, list = []) => {
|
||||
@@ -371,29 +375,31 @@ app.registerExtension({
|
||||
let known = null;
|
||||
const refresh = async () => {
|
||||
known = known || await (await api.fetchApi("/promptforge/actions")).json();
|
||||
const values = allActions.filter((value) => known[value]?.category === category.value);
|
||||
action.options = { ...(action.options || {}), values };
|
||||
if (!values.includes(action.value) && values.length) action.value = values[0];
|
||||
const scattered = category.value === RANDOM || action.value === RANDOM;
|
||||
const values = category.value === RANDOM ? allActions : allActions.filter((value) => known[value]?.category === category.value);
|
||||
action.options = { ...(action.options || {}), values: [RANDOM, ...values] };
|
||||
if (action.value !== RANDOM && !values.includes(action.value) && values.length) action.value = values[0];
|
||||
const entry = known[String(action.value)] || { roles: [] };
|
||||
const list = entry.roles || [];
|
||||
const modes = entry.penetration || [];
|
||||
const modes = scattered ? ["vaginal", "anal"] : (entry.penetration || []);
|
||||
if (penetration) {
|
||||
setShown(penetration, modes.length > 0);
|
||||
if (modes.length) {
|
||||
penetration.options = { ...(penetration.options || {}), values: modes };
|
||||
if (!modes.includes(penetration.value)) penetration.value = String(action.value).toLowerCase().includes("anal") ? "anal" : modes[0];
|
||||
penetration.options = { ...(penetration.options || {}), values: scattered ? [RANDOM, ...modes] : modes };
|
||||
if (!penetration.options.values.includes(penetration.value)) penetration.value = String(action.value).toLowerCase().includes("anal") ? "anal" : modes[0];
|
||||
}
|
||||
}
|
||||
const camera = find("camera_angle");
|
||||
if (camera) {
|
||||
const values = [ANY_CAMERA, ...(entry.cameras || [])];
|
||||
camera.options = { ...(camera.options || {}), values };
|
||||
if (!values.includes(camera.value)) camera.value = values[0];
|
||||
if (cameraWidget) {
|
||||
const values = [RANDOM, ANY_CAMERA, ...(scattered ? allCameras : (entry.cameras || []))];
|
||||
cameraWidget.options = { ...(cameraWidget.options || {}), values };
|
||||
if (!values.includes(cameraWidget.value)) cameraWidget.value = ANY_CAMERA;
|
||||
}
|
||||
const shape = entry.aspect === "9:16" ? "portrait" : "landscape";
|
||||
label.innerHTML = list.map((item, index) => `<b>${nameFor(item.gender, index, list)}</b>: ${item.role}`).join("<br>")
|
||||
label.innerHTML = (scattered ? "<b>Random</b>: the act is rolled when the prompt is written<br>" : "")
|
||||
+ list.map((item, index) => `<b>${nameFor(item.gender, index, list)}</b>: ${item.role}`).join("<br>")
|
||||
+ (entry.aspect ? `<br><b>Frame</b>: ${entry.aspect} (${shape})` : "")
|
||||
+ "<br><b>Extra</b>: a person in the video but not in the act (optional)";
|
||||
+ "<br><b>Extra</b>: a person in the video but not in the act (optional)"
|
||||
+ (node.promptforgeRandomText ? `<br><b>${node.promptforgeRandomText}</b>` : "");
|
||||
for (let number = 1; number <= 3; number++) {
|
||||
const used = number <= list.length || number === 3;
|
||||
const gender = list[number - 1]?.gender;
|
||||
@@ -432,6 +438,7 @@ app.registerExtension({
|
||||
const callback = action.callback;
|
||||
action.callback = (...args) => { callback?.apply(action, args); refresh(); };
|
||||
node.promptforgeRefreshAction = refresh;
|
||||
node.promptforgeShowRandom = ({ picks }) => { node.promptforgeRandomText = picks.length ? "Rolled: " + picks.map(([name, value]) => `${name} = ${value}`).join(", ") : ""; refresh(); };
|
||||
refresh();
|
||||
node.promptforgeRestoreActionSize();
|
||||
};
|
||||
@@ -563,13 +570,26 @@ app.registerExtension({
|
||||
const category = find("category");
|
||||
const actionWidget = find("action");
|
||||
const penetration = find("penetration");
|
||||
const cameraWidget = find("camera_angle");
|
||||
const allCameras = [...(cameraWidget?.options?.values || [])].filter((value) => value !== RANDOM && value !== ANY_CAMERA);
|
||||
let known = null;
|
||||
const filter = async () => {
|
||||
if (!known) known = await (await api.fetchApi("/promptforge/actions")).json();
|
||||
const values = Object.keys(known).filter((value) => known[value].category === category.value);
|
||||
actionWidget.options = { ...(actionWidget.options || {}), values };
|
||||
if (!values.includes(actionWidget.value) && values.length) actionWidget.value = values[0];
|
||||
if (penetration) setShown(penetration, (known[actionWidget.value]?.penetration || []).length > 0);
|
||||
const scattered = category.value === RANDOM || actionWidget.value === RANDOM;
|
||||
const values = category.value === RANDOM ? Object.keys(known) : Object.keys(known).filter((value) => known[value].category === category.value);
|
||||
actionWidget.options = { ...(actionWidget.options || {}), values: [RANDOM, ...values] };
|
||||
if (actionWidget.value !== RANDOM && !values.includes(actionWidget.value) && values.length) actionWidget.value = values[0];
|
||||
const modes = scattered ? ["vaginal", "anal"] : (known[actionWidget.value]?.penetration || []);
|
||||
if (penetration) {
|
||||
setShown(penetration, modes.length > 0);
|
||||
if (modes.length) penetration.options = { ...(penetration.options || {}), values: scattered ? [RANDOM, ...modes] : modes };
|
||||
}
|
||||
if (cameraWidget) {
|
||||
const pool = scattered ? allCameras : (known[actionWidget.value]?.cameras || []);
|
||||
const cameras = [RANDOM, ANY_CAMERA, ...pool];
|
||||
cameraWidget.options = { ...(cameraWidget.options || {}), values: cameras };
|
||||
if (!cameras.includes(cameraWidget.value)) cameraWidget.value = ANY_CAMERA;
|
||||
}
|
||||
node.setDirtyCanvas(true, true);
|
||||
};
|
||||
category.callback = ((original) => (...args) => { original?.apply(category, args); filter(); })(category.callback);
|
||||
@@ -583,13 +603,14 @@ app.registerExtension({
|
||||
frame.addEventListener("pointerdown", (event) => event.stopPropagation());
|
||||
root.addEventListener("wheel", (event) => event.stopPropagation());
|
||||
|
||||
const randomLine = el("div", { opacity: ".85", flex: "0 0 auto" });
|
||||
const header = el("div", { display: "flex", alignItems: "center", gap: "6px", flex: "0 0 auto" });
|
||||
const minus = el("button", { cursor: "pointer", width: "24px" }, { textContent: "−" });
|
||||
const plus = el("button", { cursor: "pointer", width: "24px" }, { textContent: "+" });
|
||||
const count = el("input", { width: "52px", textAlign: "center", background: "var(--comfy-input-bg, #333)", color: "var(--input-text, #ddd)", border: "1px solid var(--border-color, #555)", borderRadius: "3px" }, { type: "number", min: 1, max: 200, step: 1 });
|
||||
header.append("Parts", minus, count, plus, el("span", { opacity: ".6", marginLeft: "6px" }, { textContent: "What happens in each part. Start a box with (10s) for a longer part, or raw: to use your own finished prompt." }));
|
||||
const list = el("div", { display: "flex", flexDirection: "column", gap: "4px" });
|
||||
root.append(header, list);
|
||||
root.append(header, randomLine, list);
|
||||
|
||||
node.properties = node.properties || {};
|
||||
const heights = () => (node.properties.partHeights = node.properties.partHeights || []);
|
||||
@@ -628,6 +649,7 @@ app.registerExtension({
|
||||
}));
|
||||
}
|
||||
|
||||
node.promptforgeShowRandom = ({ picks }) => { randomLine.textContent = picks.length ? "Rolled: " + picks.map(([name, value]) => `${name} = ${value}`).join(", ") : ""; };
|
||||
const setCount = (value) => {
|
||||
countWidget.value = Math.min(200, Math.max(1, Math.round(value)));
|
||||
render();
|
||||
|
||||
Reference in New Issue
Block a user