Add camera control node

This commit is contained in:
2026-06-24 09:46:51 +02:00
parent 084702f351
commit 00ac8be640
3 changed files with 371 additions and 3 deletions
+31 -1
View File
@@ -7,6 +7,7 @@ The node is registered as:
- `prompt_builder / SxCP Prompt Builder`
- `prompt_builder / SxCP Seed Control`
- `prompt_builder / SxCP Camera Control`
- `prompt_builder / SxCP Caption Naturalizer`
- `prompt_builder / SxCP Insta/OF Options`
- `prompt_builder / SxCP Insta/OF Prompt Pair`
@@ -23,6 +24,27 @@ It outputs:
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
builder's optional `seed_config` input.
`SxCP Camera Control` outputs `camera_config`, which can be connected to the
prompt builder or the Insta/OF pair node. It makes camera/framing first-class
instead of relying on a weak phrase inside the prompt.
Camera controls:
- `camera_mode`: `standard`, `handheld_selfie`, `mirror_selfie`,
`phone_tripod`, `creator_pov`, `bed_selfie`, `bathroom_mirror`,
`phone_flash`, or `action_cam`.
- `shot_size`: `auto`, `full_body`, `three_quarter`, `waist_up`, `close_up`,
or `extreme_close_up`.
- `angle`: `auto`, `eye_level`, `high_angle`, `low_angle`, `overhead`,
`side_profile`, `rear_view`, or `mirror_reflection`.
- `lens`: `auto`, `smartphone_wide`, `ultra_wide`, `portrait_lens`,
`telephoto`, or `macro_detail`.
- `distance`: `auto`, `arm_length`, `near_body`, `bedside`, or `room_corner`.
- `orientation`: `auto`, `vertical_story`, `square_feed`, or `horizontal`.
- `phone_visibility`: `auto`, `phone_visible`, `phone_hidden`,
`screen_reflection`, or `ring_light_visible`.
- `priority`: `soft_hint`, `strong`, or `locked`.
`SxCP Caption Naturalizer` rewrites tag-like captions or labeled prompts into
more natural language. Connect the prompt builder's `metadata_json` output to
`source_text` for the cleanest result. You can also connect `caption` or
@@ -61,7 +83,9 @@ It outputs:
`SxCP Insta/OF Options` outputs `options_json`, which can be connected to the
pair node. Defaults are set so the softcore prompt is solo while the hardcore
prompt can include partners.
prompt can include partners. It also defaults the camera to handheld selfie
framing. For stronger camera control, connect `SxCP Camera Control` to the pair
node's optional `camera_config` input.
Options:
@@ -77,6 +101,9 @@ Options:
- `continuity`: `same_creator_same_room` keeps the scene/composition aligned;
`same_creator_new_scene` keeps the same creator descriptor but lets the
hardcore scene use its own setting.
- `softcore_camera_mode`: base camera mode for the softcore output.
- `hardcore_camera_mode`: `same_as_softcore` or a separate base camera mode for
the hardcore output.
## Built-In Categories
@@ -92,6 +119,9 @@ The node keeps the original generator controls:
- `figure`: `curvy`, `balanced`, `bombshell`.
- `no_plus_women`: excludes plus-size women.
- `no_black`: excludes Black/African-coded women from women-focused pools.
- Optional `camera_config`: connect `SxCP Camera Control` to force selfie,
phone, lens, angle, distance, crop, and camera-priority behavior. This applies
to custom categories too, including `Hardcore sexual poses`.
`auto_weighted` uses the original batch mix: mostly women, then men, couples, and
group/layout rows. Direct categories generate only that selected category.
+78
View File
@@ -4,20 +4,38 @@ import json
try:
from .prompt_builder import (
build_camera_config_json,
build_insta_of_options_json,
build_insta_of_pair,
build_prompt,
build_seed_config_json,
camera_angle_choices,
camera_distance_choices,
camera_lens_choices,
camera_mode_choices,
camera_orientation_choices,
camera_phone_choices,
camera_priority_choices,
camera_shot_choices,
category_choices,
subcategory_choices,
)
from .caption_naturalizer import naturalize_caption
except ImportError:
from prompt_builder import (
build_camera_config_json,
build_insta_of_options_json,
build_insta_of_pair,
build_prompt,
build_seed_config_json,
camera_angle_choices,
camera_distance_choices,
camera_lens_choices,
camera_mode_choices,
camera_orientation_choices,
camera_phone_choices,
camera_priority_choices,
camera_shot_choices,
category_choices,
subcategory_choices,
)
@@ -50,6 +68,7 @@ class SxCPPromptBuilder:
},
"optional": {
"seed_config": ("STRING", {"default": "", "multiline": True}),
"camera_config": ("STRING", {"default": "", "multiline": True}),
"extra_positive": ("STRING", {"default": "", "multiline": True}),
"extra_negative": ("STRING", {"default": "", "multiline": True}),
},
@@ -81,6 +100,7 @@ class SxCPPromptBuilder:
trigger,
prepend_trigger_to_prompt,
seed_config="",
camera_config="",
extra_positive="",
extra_negative="",
):
@@ -106,6 +126,7 @@ class SxCPPromptBuilder:
extra_positive=extra_positive or "",
extra_negative=extra_negative or "",
seed_config=seed_config or "",
camera_config=camera_config or "",
)
return (
row["prompt"],
@@ -167,6 +188,52 @@ class SxCPSeedControl:
)
class SxCPCameraControl:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"camera_mode": (camera_mode_choices(), {"default": "handheld_selfie"}),
"shot_size": (camera_shot_choices(), {"default": "auto"}),
"angle": (camera_angle_choices(), {"default": "auto"}),
"lens": (camera_lens_choices(), {"default": "smartphone_wide"}),
"distance": (camera_distance_choices(), {"default": "arm_length"}),
"orientation": (camera_orientation_choices(), {"default": "vertical_story"}),
"phone_visibility": (camera_phone_choices(), {"default": "phone_visible"}),
"priority": (camera_priority_choices(), {"default": "locked"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("camera_config",)
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(
self,
camera_mode,
shot_size,
angle,
lens,
distance,
orientation,
phone_visibility,
priority,
):
return (
build_camera_config_json(
camera_mode=camera_mode,
shot_size=shot_size,
angle=angle,
lens=lens,
distance=distance,
orientation=orientation,
phone_visibility=phone_visibility,
priority=priority,
),
)
class SxCPCaptionNaturalizer:
@classmethod
def INPUT_TYPES(cls):
@@ -223,6 +290,8 @@ class SxCPInstaOFOptions:
"hardcore_level": (["explicit", "hardcore"], {"default": "hardcore"}),
"platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}),
"continuity": (["same_creator_same_room", "same_creator_new_scene"], {"default": "same_creator_same_room"}),
"softcore_camera_mode": (camera_mode_choices(), {"default": "handheld_selfie"}),
"hardcore_camera_mode": (["same_as_softcore"] + camera_mode_choices(), {"default": "same_as_softcore"}),
}
}
@@ -241,6 +310,8 @@ class SxCPInstaOFOptions:
hardcore_level,
platform_style,
continuity,
softcore_camera_mode,
hardcore_camera_mode,
):
return (
build_insta_of_options_json(
@@ -252,6 +323,8 @@ class SxCPInstaOFOptions:
hardcore_level=hardcore_level,
platform_style=platform_style,
continuity=continuity,
softcore_camera_mode=softcore_camera_mode,
hardcore_camera_mode=hardcore_camera_mode,
),
)
@@ -274,6 +347,7 @@ class SxCPInstaOFPromptPair:
"optional": {
"seed_config": ("STRING", {"default": "", "multiline": True}),
"options_json": ("STRING", {"default": "", "multiline": True}),
"camera_config": ("STRING", {"default": "", "multiline": True}),
"extra_positive": ("STRING", {"default": "", "multiline": True}),
"extra_negative": ("STRING", {"default": "", "multiline": True}),
},
@@ -306,6 +380,7 @@ class SxCPInstaOFPromptPair:
prepend_trigger_to_prompt,
seed_config="",
options_json="",
camera_config="",
extra_positive="",
extra_negative="",
):
@@ -321,6 +396,7 @@ class SxCPInstaOFPromptPair:
prepend_trigger_to_prompt=prepend_trigger_to_prompt,
seed_config=seed_config or "",
options_json=options_json or "",
camera_config=camera_config or "",
extra_positive=extra_positive or "",
extra_negative=extra_negative or "",
)
@@ -339,6 +415,7 @@ class SxCPInstaOFPromptPair:
NODE_CLASS_MAPPINGS = {
"SxCPPromptBuilder": SxCPPromptBuilder,
"SxCPSeedControl": SxCPSeedControl,
"SxCPCameraControl": SxCPCameraControl,
"SxCPCaptionNaturalizer": SxCPCaptionNaturalizer,
"SxCPInstaOFOptions": SxCPInstaOFOptions,
"SxCPInstaOFPromptPair": SxCPInstaOFPromptPair,
@@ -347,6 +424,7 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"SxCPPromptBuilder": "SxCP Prompt Builder",
"SxCPSeedControl": "SxCP Seed Control",
"SxCPCameraControl": "SxCP Camera Control",
"SxCPCaptionNaturalizer": "SxCP Caption Naturalizer",
"SxCPInstaOFOptions": "SxCP Insta/OF Options",
"SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair",
+262 -2
View File
@@ -76,6 +76,95 @@ LAYOUT_TEMPLATE = (
"Avoid: {negative_prompt}. Use no readable text unless the layout naturally needs small decorative placeholder marks."
)
CAMERA_MODE_PROMPTS = {
"standard": "",
"handheld_selfie": (
"Camera mode: handheld smartphone selfie, close arm-length framing, visible creator-shot perspective, "
"slight wide-angle intimacy, direct eye contact, natural phone-camera composition."
),
"mirror_selfie": (
"Camera mode: mirror selfie with the phone visible in one hand, reflective framing, creator looking at the screen, "
"body and environment visible through the mirror."
),
"phone_tripod": (
"Camera mode: phone on tripod or ring-light stand, creator-facing social-video framing, stable vertical composition, "
"hands-free self-recorded setup."
),
"creator_pov": (
"Camera mode: creator-held POV, intimate subscriber-view angle, the creator controls the camera, close foreground body framing."
),
"bed_selfie": (
"Camera mode: bed selfie shot from a phone held above or beside the body, intimate close framing, sheets visible around the subject."
),
"bathroom_mirror": (
"Camera mode: bathroom mirror selfie, phone visible, tiled private room, close vertical framing, candid creator-shot energy."
),
"phone_flash": (
"Camera mode: direct phone-flash selfie, crisp flash highlights, candid night-post feeling, hard-edged smartphone shadows."
),
"action_cam": (
"Camera mode: body-mounted or handheld action-camera intimacy, very close wide-angle perspective, dynamic creator-shot framing."
),
}
CAMERA_SHOT_PROMPTS = {
"auto": "",
"full_body": "Shot size: full body visible, head-to-toe framing, no important body parts cropped out.",
"three_quarter": "Shot size: three-quarter body framing, face, torso, hips, and thighs clearly visible.",
"waist_up": "Shot size: waist-up creator framing with face and upper body as the focus.",
"close_up": "Shot size: close-up framing with face, expression, hands, and body contact emphasized.",
"extreme_close_up": "Shot size: extreme close-up detail shot, tightly framed and intimate.",
}
CAMERA_ANGLE_PROMPTS = {
"auto": "",
"eye_level": "Angle: eye-level camera angle with direct creator eye contact.",
"high_angle": "Angle: high-angle selfie looking down toward the body.",
"low_angle": "Angle: low-angle phone camera looking upward from near the body.",
"overhead": "Angle: overhead phone shot looking down at the full pose.",
"side_profile": "Angle: side-profile camera view emphasizing body silhouette and contact points.",
"rear_view": "Angle: rear-view camera framing with the body turned away from the lens.",
"mirror_reflection": "Angle: mirror-reflection composition with the phone and reflected body placement readable.",
}
CAMERA_LENS_PROMPTS = {
"auto": "",
"smartphone_wide": "Lens: smartphone wide-angle lens with slight edge distortion and close personal scale.",
"ultra_wide": "Lens: ultra-wide phone lens, exaggerated near-camera perspective, environmental context visible.",
"portrait_lens": "Lens: phone portrait mode, shallow depth of field, crisp subject separation.",
"telephoto": "Lens: compressed telephoto-style framing, flatter proportions, less distortion.",
"macro_detail": "Lens: macro-detail phone shot focused on texture, skin, fabric, and contact detail.",
}
CAMERA_DISTANCE_PROMPTS = {
"auto": "",
"arm_length": "Camera distance: arm-length selfie distance, close enough to feel handheld.",
"near_body": "Camera distance: near-body camera placement with intimate foreground framing.",
"bedside": "Camera distance: phone placed beside the body on the bed or floor.",
"room_corner": "Camera distance: phone set across the room, self-recorded but wider and more observational.",
}
CAMERA_ORIENTATION_PROMPTS = {
"auto": "",
"vertical_story": "Orientation: vertical 9:16 story/reel framing.",
"square_feed": "Orientation: square social-feed crop.",
"horizontal": "Orientation: horizontal phone-video crop.",
}
CAMERA_PHONE_PROMPTS = {
"auto": "",
"phone_visible": "Phone visibility: phone visible in hand or mirror, clearly creator-shot.",
"phone_hidden": "Phone visibility: phone is implied but not visible, preserving the selfie/creator-shot perspective.",
"screen_reflection": "Phone visibility: screen glow or reflection visible in the scene.",
"ring_light_visible": "Phone visibility: ring light or tripod visible enough to read as self-recorded content.",
}
CAMERA_PRIORITY_PROMPTS = {
"soft_hint": "Camera priority: treat the camera notes as style guidance.",
"strong": "Camera priority: strongly preserve the selected camera, lens, angle, crop, and phone-shot perspective.",
"locked": "Camera priority: locked camera constraint; do not replace this with a studio, third-person, cinematic, or unrelated camera view.",
}
_EXTENSIONS_APPLIED = False
@@ -641,6 +730,148 @@ def _combined_negative(base: str, extra: str) -> str:
return ", ".join(parts)
def camera_mode_choices() -> list[str]:
return list(CAMERA_MODE_PROMPTS)
def camera_shot_choices() -> list[str]:
return list(CAMERA_SHOT_PROMPTS)
def camera_angle_choices() -> list[str]:
return list(CAMERA_ANGLE_PROMPTS)
def camera_lens_choices() -> list[str]:
return list(CAMERA_LENS_PROMPTS)
def camera_distance_choices() -> list[str]:
return list(CAMERA_DISTANCE_PROMPTS)
def camera_orientation_choices() -> list[str]:
return list(CAMERA_ORIENTATION_PROMPTS)
def camera_phone_choices() -> list[str]:
return list(CAMERA_PHONE_PROMPTS)
def camera_priority_choices() -> list[str]:
return list(CAMERA_PRIORITY_PROMPTS)
def build_camera_config_json(
camera_mode: str = "standard",
shot_size: str = "auto",
angle: str = "auto",
lens: str = "auto",
distance: str = "auto",
orientation: str = "auto",
phone_visibility: str = "auto",
priority: str = "strong",
) -> str:
return json.dumps(
{
"camera_mode": camera_mode,
"shot_size": shot_size,
"angle": angle,
"lens": lens,
"distance": distance,
"orientation": orientation,
"phone_visibility": phone_visibility,
"priority": priority,
},
ensure_ascii=True,
sort_keys=True,
)
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
value = str(value or default)
return value if value in choices else default
def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, str]:
defaults = {
"camera_mode": "standard",
"shot_size": "auto",
"angle": "auto",
"lens": "auto",
"distance": "auto",
"orientation": "auto",
"phone_visibility": "auto",
"priority": "strong",
}
if not camera_config:
return defaults
if isinstance(camera_config, dict):
raw = camera_config
else:
try:
raw = json.loads(str(camera_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid camera_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("camera_config must be a JSON object")
parsed = {**defaults, **raw}
return {
"camera_mode": _choice(parsed.get("camera_mode"), CAMERA_MODE_PROMPTS, defaults["camera_mode"]),
"shot_size": _choice(parsed.get("shot_size"), CAMERA_SHOT_PROMPTS, defaults["shot_size"]),
"angle": _choice(parsed.get("angle"), CAMERA_ANGLE_PROMPTS, defaults["angle"]),
"lens": _choice(parsed.get("lens"), CAMERA_LENS_PROMPTS, defaults["lens"]),
"distance": _choice(parsed.get("distance"), CAMERA_DISTANCE_PROMPTS, defaults["distance"]),
"orientation": _choice(parsed.get("orientation"), CAMERA_ORIENTATION_PROMPTS, defaults["orientation"]),
"phone_visibility": _choice(parsed.get("phone_visibility"), CAMERA_PHONE_PROMPTS, defaults["phone_visibility"]),
"priority": _choice(parsed.get("priority"), CAMERA_PRIORITY_PROMPTS, defaults["priority"]),
}
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, str]:
parsed = _parse_camera_config(camera_config)
if camera_mode and camera_mode != "from_camera_config":
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
return parsed
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, str]]:
parsed = _parse_camera_config(camera_config)
parts = [
CAMERA_MODE_PROMPTS[parsed["camera_mode"]],
CAMERA_SHOT_PROMPTS[parsed["shot_size"]],
CAMERA_ANGLE_PROMPTS[parsed["angle"]],
CAMERA_LENS_PROMPTS[parsed["lens"]],
CAMERA_DISTANCE_PROMPTS[parsed["distance"]],
CAMERA_ORIENTATION_PROMPTS[parsed["orientation"]],
CAMERA_PHONE_PROMPTS[parsed["phone_visibility"]],
]
parts = [part for part in parts if part]
if not parts:
return "", parsed
parts.append(CAMERA_PRIORITY_PROMPTS[parsed["priority"]])
return " ".join(parts), parsed
def _insert_positive_directive(prompt: str, directive: str) -> str:
marker = " Avoid:"
if marker in prompt:
before, after = prompt.split(marker, 1)
return f"{before.rstrip()} {directive}{marker}{after}"
return f"{prompt.rstrip()} {directive}"
def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
directive, parsed = _camera_directive(camera_config)
row["camera_config"] = parsed
row["camera_directive"] = directive
if not directive:
return row
row["prompt"] = _insert_positive_directive(row["prompt"], directive)
row["caption"] = f"{row.get('caption', '').rstrip()}, {parsed['camera_mode'].replace('_', ' ')} camera framing"
return row
def _row_seed(seed: int, row_number: int, salt: int = 0) -> int:
return int(seed) + int(row_number) * 1009 + salt * 9176
@@ -1310,6 +1541,7 @@ def build_prompt(
seed_config: str | dict[str, Any] | None = None,
women_count: int = 1,
men_count: int = 1,
camera_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
apply_pool_extensions()
row_number = max(1, int(row_number))
@@ -1375,6 +1607,7 @@ def build_prompt(
if extra_positive.strip():
row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}"
row = _apply_camera_config(row, camera_config)
active_trigger = trigger.strip() or g.TRIGGER
row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt))
row["negative_prompt"] = _combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
@@ -1419,6 +1652,8 @@ def build_insta_of_options_json(
hardcore_level: str = "hardcore",
platform_style: str = "hybrid",
continuity: str = "same_creator_same_room",
softcore_camera_mode: str = "handheld_selfie",
hardcore_camera_mode: str = "same_as_softcore",
) -> str:
return json.dumps(
{
@@ -1430,6 +1665,8 @@ def build_insta_of_options_json(
"hardcore_level": hardcore_level,
"platform_style": platform_style,
"continuity": continuity,
"softcore_camera_mode": softcore_camera_mode,
"hardcore_camera_mode": hardcore_camera_mode,
},
ensure_ascii=True,
sort_keys=True,
@@ -1446,6 +1683,8 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s
"hardcore_level": "hardcore",
"platform_style": "hybrid",
"continuity": "same_creator_same_room",
"softcore_camera_mode": "handheld_selfie",
"hardcore_camera_mode": "same_as_softcore",
}
if not options_json:
return defaults
@@ -1465,6 +1704,9 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s
parsed["hardcore_level"] = parsed["hardcore_level"] if parsed["hardcore_level"] in INSTA_OF_HARDCORE_LEVELS else defaults["hardcore_level"]
parsed["platform_style"] = parsed["platform_style"] if parsed["platform_style"] in INSTA_OF_PLATFORM_STYLES else defaults["platform_style"]
parsed["continuity"] = parsed["continuity"] if parsed["continuity"] in ("same_creator_same_room", "same_creator_new_scene") else defaults["continuity"]
parsed["softcore_camera_mode"] = parsed["softcore_camera_mode"] if parsed["softcore_camera_mode"] in CAMERA_MODE_PROMPTS else defaults["softcore_camera_mode"]
if parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS and parsed["hardcore_camera_mode"] != "same_as_softcore":
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
for key in ("hardcore_women_count", "hardcore_men_count"):
try:
parsed[key] = max(0, min(12, int(parsed[key])))
@@ -1526,6 +1768,7 @@ def build_insta_of_pair(
prepend_trigger_to_prompt: bool,
seed_config: str | dict[str, Any] | None = None,
options_json: str | dict[str, Any] | None = None,
camera_config: str | dict[str, Any] | None = None,
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
@@ -1585,6 +1828,15 @@ def build_insta_of_pair(
platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]]
soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]]
hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]]
hard_camera_mode = options["hardcore_camera_mode"]
if hard_camera_mode == "same_as_softcore":
hard_camera_mode = options["softcore_camera_mode"]
soft_camera_config = _camera_config_with_mode(camera_config, options["softcore_camera_mode"])
hard_camera_config = _camera_config_with_mode(camera_config, hard_camera_mode)
soft_camera_directive, soft_camera_config = _camera_directive(soft_camera_config)
hard_camera_directive, hard_camera_config = _camera_directive(hard_camera_config)
soft_camera_sentence = f"Camera control: {soft_camera_directive} " if soft_camera_directive else ""
hard_camera_sentence = f"Camera control: {hard_camera_directive} " if hard_camera_directive else ""
hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"]
hard_composition = soft_row["composition"] if options["continuity"] == "same_creator_same_room" else hard_row["composition"]
soft_cast = (
@@ -1599,6 +1851,7 @@ def build_insta_of_pair(
f"Softcore setup: {soft_level}. Cast continuity: {soft_cast}. "
f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
f"Facial expression: {soft_row['expression']}. Composition: {soft_row['composition']}. "
f"{soft_camera_sentence}"
"Keep the softcore version adult-only, consensual, seductive, creator-shot, and non-explicit. "
f"{soft_row['positive_suffix']} Avoid: {INSTA_OF_SOFT_NEGATIVE}."
)
@@ -1608,6 +1861,7 @@ def build_insta_of_pair(
"Apply the shared descriptor to the most visually central woman, keeping her continuous with the softcore version. "
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
f"Setting: {hard_scene}. Facial expressions: {hard_row['expression']}. Composition: {hard_composition}. "
f"{hard_camera_sentence}"
"All participants are consenting adults 21+. "
f"{hard_row['positive_suffix']} Avoid: {INSTA_OF_NEGATIVE}."
)
@@ -1621,11 +1875,13 @@ def build_insta_of_pair(
hard_negative = _combined_negative(INSTA_OF_NEGATIVE, extra_negative)
soft_caption = (
f"{active_trigger}, Insta/OF softcore mode, {descriptor}, {soft_level}, "
f"{soft_row['item']}, {soft_row['pose']}, {soft_row['scene_text']}, {soft_row['composition']}"
f"{soft_row['item']}, {soft_row['pose']}, {soft_row['scene_text']}, {soft_row['composition']}, "
f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera"
)
hard_caption = (
f"{active_trigger}, Insta/OF hardcore mode, same primary creator descriptor, {descriptor}, "
f"{hard_cast}, {hard_row['role_graph']}, {hard_row['item']}, {hard_scene}, {hard_composition}"
f"{hard_cast}, {hard_row['role_graph']}, {hard_row['item']}, {hard_scene}, {hard_composition}, "
f"{hard_camera_config['camera_mode'].replace('_', ' ')} camera"
)
metadata = {
"mode": "Insta/OF",
@@ -1641,5 +1897,9 @@ def build_insta_of_pair(
"hardcore_row": hard_row,
"hardcore_women_count": hard_women_count,
"hardcore_men_count": hard_men_count,
"softcore_camera_config": soft_camera_config,
"hardcore_camera_config": hard_camera_config,
"softcore_camera_directive": soft_camera_directive,
"hardcore_camera_directive": hard_camera_directive,
}
return metadata