diff --git a/README.md b/README.md
index 26c027d..9859239 100644
--- a/README.md
+++ b/README.md
@@ -53,12 +53,12 @@ cfg 2, `euler`/`normal`. The DMD LoRAs (`lodestones-8-steps`, `sft-4-steps`) run
## MiniMax H3
-Required extras: the H3 model, its VAE and an audio VAE. The `PromptForge_H3_AllInOne_Modes_v5.json` workflow
-covers all modes; switch them in the Modes node.
+Required extras: the H3 model, its VAE and an audio VAE. The `PromptForge_H3_AllInOne_Modes_v6.json` workflow
+covers all modes; switch them in the Modes node. Everything changed often (mode, action, idea, writer, format, sampling, LoRA, generate) sits in one row next to each other. The v5 file is left untouched.
### Action node and roles
-57 acts, each with an explicit who-is-where description and a recommended 9:16 or 16:9 frame. Every role has
+85 acts, each with an explicit who-is-where description, the orifice for sex acts (vagina or anus, never swapped), and a 9:16 or 16:9 frame. The camera list is filtered per act and puts POV of one named person first; a POV line is enforced as that person's eyes for the whole clip, their face never shown, the other person always in frame. Every role has
its own picture picker (Woman, Man, Partner): choose the reference slot, the first frame, none and only a
description, or `disabled`. Subject 3 defaults to `disabled`; pictures left unassigned become anatomy/detail
references, not extra people. See the [H3 Action reference](#promptforge-h3-action-adult-presets).
diff --git a/h3_actions.py b/h3_actions.py
index be6f9ca..34aa252 100644
--- a/h3_actions.py
+++ b/h3_actions.py
@@ -5,12 +5,12 @@ prompt writer's NSFW template turns it into a full video prompt, for one clip or
RULES = """# Coherence rules for this video (mandatory, they override the template where they conflict)
1. One continuous shot from the fixed camera. Write exactly one [Shot 1], no other shot labels, no cuts, and no timestamps at all inside the shot: use "then", "after a moment", "as", "meanwhile" instead of 00:0X.
-2. Camera: the framing from the camera line, chosen so every subject is fully in frame and every face is visible. At most one slow, small camera move in the whole clip (a slight drift or pan). Never zoom, push in, pull out, rack focus, whip, or cut to a detail. Never end on a close-up or on an empty part of the room.
+2. Camera: the framing from the camera line, chosen so every subject is fully in frame and every face is visible, except a POV line: a POV is strict, it is the named person's eyes for the whole clip, only their hands, forearms, chest or thighs enter the frame, their face never appears (except in a mirror), and the other person stays in view. At most one slow, small camera move in the whole clip (a slight drift or pan). Never zoom, push in, pull out, rack focus, whip, or cut to a detail. Never end on a close-up or on an empty part of the room.
3. The act is already under way in the first sentence: the bodies are already in the arrangement, the genitals, hands or mouths are already in contact, and the movement starts immediately. No walking in, no undressing, no arranging limbs, except for a striptease.
4. Layout: state where each subject is in the frame (left, right, foreground, background), which way each faces, and which body parts point to the camera. Keep that layout for the whole clip: nobody turns around, swaps places, changes pose or leaves unless the action says so.
5. Motion and contact: name the contact and the rhythm ("his hips thrust into her at a steady pace, about one stroke per second", "her mouth slides up and down the shaft"), then vary the speed, depth and weight. Describe the motion once, do not repeat it every sentence, and keep it running to the final second. Fill the clip with at least eight changes of movement, reaction, breathing and sound; the detailed description is 500-800 words.
6. Bodies: each person has one head, two arms and two legs. Say where each hand and each leg is (for example "her hands rest on his chest", "his hands hold her hips"). Describe only what the camera can see; parts hidden by the pose are not described. Bodies never pass through each other or through furniture.
-7. Contact must be physically possible for the arrangement. Follow the arrangement and subject roles literally; never invent another position or swap the roles.
+7. Contact must be physically possible for the arrangement. Follow the arrangement and subject roles literally; never invent another position or swap the roles. The action names the orifice (vagina or anus): keep exactly that one for the whole clip, never swap vaginal for anal or the reverse, and show the shaft entering and leaving it, the grip of the opening, and the wetness or lube.
8. The penis is erect from the first frame, whatever the reference picture shows.
9. Expressions and sounds follow the action and what is on screen.
10. No garment from a reference picture is worn in the act: everyone is naked from the first frame. A striptease is the only exception and starts dressed.
@@ -97,6 +97,20 @@ ACTIONS = {
"Solo - male, lying, jerking off": ((("male", "the man"),), "{A} lies on his back on the bed, fully naked, one hand stroking the erect penis in long strokes, the other behind his head, the hips pushing up into the fist.", "16:9"),
"Solo - male, standing in the shower": ((("male", "the man"),), "{A} stands under running water, one hand on the tiles, the other stroking the erect penis, water running over his chest and stomach.", "9:16"),
"Solo - male, toy, on all fours": ((("male", "the man"),), "{A} is on all fours, fully naked, reaching back to push a toy into himself, the erect penis hanging, the head turned to the camera.", "16:9"),
+ "Position - standing anal, from behind": ((("female", "the woman bent forward, hands on a wall"), ("male", "the man stands behind")), "{A} stands bent forward with both hands on a wall, the legs apart; {B} stands behind, spreads her rear with one hand and pushes the erect penis into her anus, the other hand on her hip, short slow strokes into the anus.", "16:9"),
+ "Position - prone bone anal": ((("female", "the woman face down, hips raised"), ("male", "the man on top behind")), "{A} lies face down with a pillow under the hips, the rear raised; {B} lies over her from behind and thrusts the erect penis into her anus in short deep strokes, his chest on her back.", "16:9"),
+ "Position - lotus anal": ((("female", "the woman in his lap, facing"), ("male", "the man sits")), "{B} sits cross-legged; {A} sits on his lap facing him with her arms around his neck, the erect penis in her anus, both grinding slowly, her mouth at his ear.", "9:16"),
+ "Position - reverse cowgirl anal, lean forward": ((("female", "the woman on top, facing away, leans forward"), ("male", "the man lies on his back")), "{B} lies on his back; {A} straddles him facing away, lowers onto the penis anally and leans forward with her hands on his shins, pushing her hips back onto him in small strokes, the rear toward the camera.", "9:16"),
+ "Position - spooning, top leg raised": ((("female", "the woman on her side, top leg raised"), ("male", "the man behind")), "{A} lies on her side with the top leg pulled up toward her chest; {B} lies along her back, one arm under her leg holding it up, and thrusts into her vagina slowly, both bodies pressed together.", "16:9"),
+ "Position - mission, slow and deep": ((("female", "the woman on her back, hips lifted"), ("male", "the man above, chest to chest")), "{A} lies on her back with the hips lifted and the legs wrapped around {B}; {B} lies chest to chest and rolls his hips into her vagina in slow deep strokes, foreheads almost touching, her hands in his hair.", "16:9"),
+ "Position - table, bent over": ((("female", "the woman bent over a table"), ("male", "the man stands behind")), "{A} bends over the edge of a table, forearms and chest on the surface, the legs apart; {B} stands behind, pulls her hips back onto his erect penis and thrusts into her vagina, her feet lifted off the floor.", "16:9"),
+ "Position - squatting cowgirl": ((("female", "the woman squats over him"), ("male", "the man lies on his back")), "{B} lies on his back; {A} squats over him with her feet flat, lowers herself onto his erect penis and bounces in a deep squat, hands on his chest, thighs working.", "9:16"),
+ "Position - sitting on his lap, grinding": ((("female", "the woman in his lap"), ("male", "the man sits")), "{B} sits on a chair; {A} sits astride his lap facing him, the penis inside her vagina, both hands on his shoulders, grinding her hips in slow circles instead of bouncing, faces close.", "9:16"),
+ "Position - standing, her back arched": ((("female", "the woman stands, back arched against him"), ("male", "the man behind, holds her")), "{A} stands with her back arched against {B}'s chest, one arm up around his neck; {B} holds her waist and one breast and thrusts up into her vagina, both facing the camera.", "9:16"),
+ "Position - edge of the bed, legs up": ((("female", "the woman on her back at the edge, legs up"), ("male", "the man stands between her legs")), "{A} lies at the edge of the bed with both legs raised and held open; {B} stands between her legs and thrusts down into her vagina at a deep angle, her hands gripping the sheets.", "16:9"),
+ "Oral - blowjob, from below": ((("female", "the woman below, gives oral"), ("male", "the man above, receives")), "{B} stands over {A}; {A} lies on her back with the head tipped back over the edge of the bed and takes his erect penis into her mouth from below, his hands on her throat and hair.", "9:16"),
+ "Oral - cunnilingus, from behind": ((("female", "the woman on all fours, receives oral"), ("male", "the man kneels behind, gives oral")), "{A} is on hands and knees, the rear toward {B}; {B} kneels behind, grips her hips and licks from the clitoris to the anus, tongue flat then pointed, her back arching.", "16:9"),
+ "Hands - fingering, standing": ((("female", "the woman stands, receives"), ("male", "the man behind, gives")), "{A} stands with her back to {B}, one hand on a wall, legs apart; {B} presses against her from behind, one hand on her breast and the other between her legs, two fingers inside her vagina, the thumb on the clitoris.", "9:16"),
}
ACTION_CATEGORIES = ("Solo female", "Solo male", "Oral", "Hands", "Positions", "Anal", "Other")
@@ -109,7 +123,7 @@ def action_category(label):
return "Oral"
if label.startswith("Hands"):
return "Hands"
- if label.startswith("Position - anal"):
+ if "anal" in label.lower():
return "Anal"
if label.startswith("Position"):
return "Positions"
@@ -117,8 +131,8 @@ def action_category(label):
CAMERAS = {
- "POV (the viewer is B)": "Point of view of {B}: the camera sees {A} and the action from {B}'s eyes, {B}'s hands and body entering the frame.",
- "POV (the viewer is A)": "Point of view of {A}: the camera sees {B} and the action from {A}'s eyes, {A}'s hands and body entering the frame.",
+ "POV (the viewer is B)": "STRICT POV of {B}: the camera IS {B}'s eyes. {A} and the action fill the frame; only {B}'s hands, forearms, chest or thighs enter from the bottom edge. Never show {B}'s face (only in a mirror), never cut away from this angle.",
+ "POV (the viewer is A)": "STRICT POV of {A}: the camera IS {A}'s eyes. {B} and the action fill the frame; only {A}'s hands, forearms, chest or thighs enter from the bottom edge. Never show {A}'s face (only in a mirror), never cut away from this angle.",
"side view, full bodies": "A still camera at chest height, side-on to the people, framing both bodies fully with both faces visible.",
"from behind": "The camera is behind the people, looking at the action from behind.",
"front view": "A still camera at chest height facing the person or people from the front, the whole body in frame, the face visible.",
@@ -189,9 +203,39 @@ def _role_name(genders, index):
return "the person"
+POV_B = "POV (the viewer is B)"
+POV_A = "POV (the viewer is A)"
+CLOSE = "close-up on the action"
+SIDE = "side view, full bodies"
+FRONT = "front view"
+LOW = "low angle, looking up"
+HIGH = "high angle, looking down"
+BEHIND = "from behind"
+OVER = "over the shoulder"
+
+
+def cameras(label):
+ """Useful camera picks for this arrangement, strongest first; POV only where a face is not needed."""
+ if label.startswith("Solo"):
+ return [FRONT, SIDE, LOW, HIGH, CLOSE]
+ if label.startswith("Oral"):
+ return [POV_A, POV_B, CLOSE, SIDE]
+ if "anal" in label.lower():
+ return [POV_B, BEHIND, CLOSE, SIDE, HIGH]
+ if label.startswith("Hands"):
+ return [POV_B, CLOSE, SIDE, FRONT]
+ if "from behind" in label.lower() or "doggy" in label.lower() or "spooning" in label.lower():
+ return [POV_B, BEHIND, CLOSE, SIDE]
+ if "on top" in label.lower() or "cowgirl" in label.lower():
+ return [POV_A, POV_B, LOW, SIDE, FRONT]
+ return [POV_A, POV_B, SIDE, FRONT, CLOSE]
+
+
def default_camera(label):
"""The framing used when the camera is left to the writer: both bodies and faces in one still frame."""
- return "front view" if stages_for(label) == 1 else "side view, full bodies"
+ if stages_for(label) == 1:
+ return FRONT
+ return cameras(label)[0]
def brief(label, camera, subjects, notes=""):
@@ -228,6 +272,8 @@ def brief(label, camera, subjects, notes=""):
lines.append(subjects[2])
else:
lines.append(f"Subject 3 is also in the video, not part of the action: {subjects[2]}")
+ if "anal" in label.lower():
+ lines.append("This is ANAL sex: the penis is in the anus, not the vagina, for the whole clip.")
lines.append("Use the pictures for identity and anatomy only: ignore their clothing, pose, lighting and background.")
if "nudity" in notes.lower() or "naked" in notes.lower():
if "striptease" in label.lower():
diff --git a/h3_generate.py b/h3_generate.py
index 5ab0bb1..4c359c6 100644
--- a/h3_generate.py
+++ b/h3_generate.py
@@ -386,6 +386,7 @@ class PROMPTFORGE_H3Generate:
subjects[2] = (subjects[2] + " " if subjects[2] else "") + (
"The other attached pictures are anatomy and detail references, not extra people: " + ", ".join(anatomy) + ".")
block = h3_actions.brief(action_name, story["camera_angle"], subjects, story["notes"])
+ block += f"\nVideo frame: {fmt['aspect_ratio']}, {'portrait' if fmt['orientation'] == 'portrait' else 'landscape'}, {width} x {height} px. Compose for this frame: a portrait frame favours vertical bodies, a landscape frame favours lying or side arrangements; keep the named bodies inside the frame."
if several:
brief = f"{brief}\n\n{block}".strip()
for index, part in enumerate(plan):
diff --git a/h3_ideas.py b/h3_ideas.py
index f74c37f..d5e5dfb 100644
--- a/h3_ideas.py
+++ b/h3_ideas.py
@@ -58,5 +58,5 @@ async def _delete(request):
@PromptServer.instance.routes.get("/promptforge/actions")
async def _actions(request):
return web.json_response({label: {"roles": h3_actions.role_info(label), "aspect": h3_actions.aspect(label),
- "category": h3_actions.action_category(label)}
+ "category": h3_actions.action_category(label), "cameras": h3_actions.cameras(label)}
for label in h3_actions.ACTIONS})
diff --git a/pyproject.toml b/pyproject.toml
index 5baf88a..9d2428d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI-PromptForge"
description = "ComfyUI prompt enhancement suite: model/config node with GPU-aware auto settings, prompt enhancer with a preset catalog, and a standalone config finder."
-version = "1.28.21"
+version = "1.28.22"
license = { file = "LICENSE" }
dependencies = [
"transformers",
diff --git a/web/promptforge_h3.js b/web/promptforge_h3.js
index 17873cd..7b0d598 100644
--- a/web/promptforge_h3.js
+++ b/web/promptforge_h3.js
@@ -11,6 +11,7 @@ const SECOND_STAGE = "PROMPTFORGE_H3SecondStage";
const GENERATE = "PROMPTFORGE_H3Generate";
const REFERENCES = "PROMPTFORGE_H3References";
const SUBJECT_DISABLED = "disabled (not in the video)";
+const ANY_CAMERA = "(writer's choice)";
// 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"] };
@@ -340,6 +341,12 @@ app.registerExtension({
if (!values.includes(action.value) && values.length) action.value = values[0];
const entry = known[String(action.value)] || { roles: [] };
const list = entry.roles || [];
+ 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];
+ }
const shape = entry.aspect === "9:16" ? "portrait" : "landscape";
label.innerHTML = list.map((item, index) => `${nameFor(item.gender, index, list)}: ${item.role}`).join("
")
+ (entry.aspect ? `
Frame: ${entry.aspect} (${shape})` : "")