From 00ac8be640206e21073628f4a8ab63f8b714f484 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 24 Jun 2026 09:46:51 +0200 Subject: [PATCH] Add camera control node --- README.md | 32 +++++- __init__.py | 78 ++++++++++++++ prompt_builder.py | 264 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 371 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 82192db..c7f0806 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/__init__.py b/__init__.py index d17cb45..714a032 100644 --- a/__init__.py +++ b/__init__.py @@ -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", diff --git a/prompt_builder.py b/prompt_builder.py index 960a4c5..ce9e172 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -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