Add prompt control and filter options

This commit is contained in:
2026-06-24 14:32:54 +02:00
parent 2b64499687
commit cb35e1881f
5 changed files with 592 additions and 99 deletions
+46 -22
View File
@@ -7,6 +7,7 @@ The node is registered as:
- `prompt_builder / SxCP Prompt Builder`
- `prompt_builder / SxCP Seed Control`
- `prompt_builder / SxCP Seed Locker`
- `prompt_builder / SxCP Camera Control`
- `prompt_builder / SxCP Category Preset`
- `prompt_builder / SxCP Cast Control`
@@ -41,8 +42,8 @@ node. For cleaner workflows, use the split nodes:
- `SxCP Generation Profile` outputs `generation_profile` for common behavior
presets such as casual-clean, evocative-softcore, hardcore-intense,
Krea2-friendly, or Flux-original.
- `SxCP Advanced Filters` outputs `filter_config` for ethnicity, figure, and
exclusion filters.
- `SxCP Advanced Filters` outputs `filter_config` for appearance include
checkboxes, figure, and plus-size inclusion.
- `SxCP Prompt Builder From Configs` consumes those config outputs and produces
the same prompt, negative, caption, metadata, category, and subcategory
outputs as the full builder.
@@ -50,7 +51,7 @@ node. For cleaner workflows, use the split nodes:
The practical compact workflow is:
`Category Preset` + `Cast Control` + `Generation Profile` + optional
`Advanced Filters`, `Seed Control`, `Camera Control`, and `Character Profile`
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control`, and `Character Profile`
into `Prompt Builder From Configs`.
An importable default workflow is included at
@@ -99,13 +100,18 @@ appearance remains stable.
`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt
builder's optional `seed_config` input.
`SxCP Seed Locker` is the fast version for iteration. Set `base_seed` to a seed
you like, choose one `reroll_axis`, and connect its `seed_config`. All other
axes stay frozen to `base_seed`; the rerolled axis follows `reroll_seed`, or the
main prompt seed when `reroll_seed=-1`.
`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`,
- `camera_mode`: `disabled`, `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`,
@@ -119,6 +125,8 @@ Camera controls:
- `phone_visibility`: `auto`, `phone_visible`, `phone_hidden`,
`screen_reflection`, or `ring_light_visible`.
- `priority`: `soft_hint`, `strong`, or `locked`.
- `camera_detail`: `off` emits no camera sentence, `compact` emits one short
camera sentence, and `full` emits the full detailed camera constraint.
`SxCP Caption Naturalizer` rewrites tag-like captions or labeled prompts into
more natural language. Connect the prompt builder's `metadata_json` output to
@@ -177,18 +185,17 @@ It outputs:
- `method`
`SxCP Insta/OF Prompt Pair` is a special paired-output mode. It creates one
shared primary creator descriptor, then returns both a softcore prompt and a
hardcore prompt from that same descriptor. This is useful when you want the same
primary creator descriptor internally, then returns both a softcore prompt and a
hardcore prompt from that descriptor. This is useful when you want matching
person/look/scene continuity but need two different prompt strengths.
When the hardcore cast includes partners, pair mode also creates deterministic
shared cast descriptors such as `woman A / primary creator` and `man A`. Use
cast descriptors such as `Woman A` and `Man A`. Use
`softcore_cast=same_as_hardcore`, `hardcore_cast=couple`, and
`continuity=same_creator_same_room` when you want a soft prompt with the same
woman, man, and location as the hardcore prompt. In that setup, the softcore
prompt keeps the same listed adult cast physically present together in a
non-explicit teaser pose, with deterministic non-explicit partner outfits and a
shared cast pose.
`continuity=same_creator_same_room` when you want both outputs to reuse the cast
and location. The generated positive prompts are still standalone: each output
lists the relevant cast descriptors directly and does not depend on the image
model carrying context from another prompt.
It outputs:
@@ -217,10 +224,11 @@ Options:
- `hardcore_women_count` and `hardcore_men_count`: used when `hardcore_cast` is
`use_counts`. The pair mode always keeps at least one adult woman as the
primary creator so the shared descriptor remains valid.
- `softcore_level`: `social_tease`, `lingerie_tease`, `implied_nude`, or
`explicit_tease`. Insta/OF softcore uses dedicated outfit pools so teaser
prompts do not randomly pull hardcore-adjacent harness, nude accessory,
microwear, or shirtless partner styling.
- `softcore_level`: `social_tease`, `lingerie_tease`, `implied_nude`,
`explicit_tease`, or `explicit_nude`. Insta/OF softcore uses dedicated outfit
pools so teaser prompts do not randomly pull hardcore-adjacent harness,
microwear, or shirtless partner styling. `explicit_nude` is available when
you want visible nude creator-shot framing without a sex act.
- `hardcore_level`: `explicit` or `hardcore`.
- `softcore_expression_intensity`: `0.0` is mild/controlled, `0.5` is sensual,
`1.0` strongly favors more heated softcore faces.
@@ -228,12 +236,17 @@ Options:
hardcore, `1.0` strongly favors ahegao-style, drooling, fucked-out, climax,
and messy orgasm expressions.
- `platform_style`: `hybrid`, `instagram`, or `onlyfans`.
- `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.
- `continuity`: `same_creator_same_room` keeps the scene aligned while each
output keeps its own pose/composition; `same_creator_new_scene` keeps the same
creator descriptor but lets the hardcore scene use its own setting.
- `hardcore_clothing_continuity`: `none`, `same_outfit`, `partially_removed`,
`implied_nude`, or `explicit_nude`. This controls whether the hardcore prompt
references the softcore outfit, uses it displaced/removed, or makes Woman A
explicitly nude.
- `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.
- `camera_detail`: `off`, `compact`, or `full` for the pair prompt camera text.
## Built-In Categories
@@ -242,7 +255,10 @@ The node keeps the original generator controls:
- `category`: `auto_weighted`, `woman`, `man`, `couple`, `group_or_layout`, or a custom JSON category.
- `clothing`: `full` or `minimal`.
- `minimal_clothing_ratio`: `-1` disables mixing; `0.0` to `1.0` mixes minimal/full clothing.
- `ethnicity`: `any`, `asian`, `white_asian`.
- `ethnicity`: `any`, `european`, `mediterranean_mena`, `latina`,
`east_asian`, `southeast_asian`, `south_asian`, `black_african`,
`indigenous`, `mixed`, `asian`, or `white_asian`. Combined filter strings
such as `latina+south_asian` are also accepted in config JSON.
- `poses`: `standard` or `evocative`.
- `expression_intensity`: `0.0` favors mild, neutral, controlled expressions;
`0.5` favors balanced category expressions; `1.0` strongly favors the most
@@ -252,8 +268,8 @@ The node keeps the original generator controls:
- `standard_pose_ratio`: `-1` disables mixing; `0.0` to `1.0` mixes standard/evocative poses.
- `backside_bias`: `0.0` to `1.0`, applies to evocative single-subject poses.
- `figure`: `curvy`, `balanced`, `bombshell`.
- `no_plus_women`: excludes plus-size women.
- `no_black`: excludes Black/African-coded women from women-focused pools.
- In split workflows, use `SxCP Advanced Filters` checkboxes instead of negative
toggles. Black/African and plus-size are positive include choices there.
- 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`.
@@ -475,6 +491,14 @@ alone.
The main `seed` input is still the default master seed. Connect `SxCP Seed
Control` to `seed_config` when you want to lock or vary specific axes.
For normal prompt iteration, `SxCP Seed Locker` is usually simpler:
- `base_seed`: the seed whose character/location/etc. you want to keep.
- `reroll_axis`: `none`, `content`, `person`, `scene`, `pose`, `role`,
`expression`, `composition`, `content_pose`, or `scene_pose`.
- `reroll_seed`: `-1` makes the selected axis follow the main prompt seed;
`0` or higher pins that selected axis to a specific seed.
Seed values:
- `-1`: follow the main seed.
+101 -18
View File
@@ -15,7 +15,9 @@ try:
build_prompt,
build_prompt_from_configs,
build_seed_config_json,
build_seed_lock_config_json,
camera_angle_choices,
camera_detail_choices,
camera_distance_choices,
camera_lens_choices,
camera_mode_choices,
@@ -27,6 +29,7 @@ try:
category_preset_choices,
category_choices,
character_profile_choices,
ethnicity_choices,
generation_profile_choices,
load_character_profile_json,
subcategory_choices,
@@ -46,7 +49,9 @@ except ImportError:
build_prompt,
build_prompt_from_configs,
build_seed_config_json,
build_seed_lock_config_json,
camera_angle_choices,
camera_detail_choices,
camera_distance_choices,
camera_lens_choices,
camera_mode_choices,
@@ -58,6 +63,7 @@ except ImportError:
category_preset_choices,
category_choices,
character_profile_choices,
ethnicity_choices,
generation_profile_choices,
load_character_profile_json,
subcategory_choices,
@@ -77,13 +83,11 @@ class SxCPPromptBuilder:
"start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}),
"seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}),
"clothing": (["full", "minimal"], {"default": "full"}),
"ethnicity": (["any", "asian", "white_asian"], {"default": "any"}),
"ethnicity": (ethnicity_choices(), {"default": "any"}),
"poses": (["standard", "evocative"], {"default": "standard"}),
"expression_intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"backside_bias": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}),
"no_plus_women": ("BOOLEAN", {"default": False}),
"no_black": ("BOOLEAN", {"default": False}),
"women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
"men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
"minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}),
@@ -118,8 +122,6 @@ class SxCPPromptBuilder:
expression_intensity,
backside_bias,
figure,
no_plus_women,
no_black,
women_count,
men_count,
minimal_clothing_ratio,
@@ -131,6 +133,8 @@ class SxCPPromptBuilder:
character_profile="",
extra_positive="",
extra_negative="",
no_plus_women=False,
no_black=False,
):
row = build_prompt(
category=category,
@@ -218,6 +222,46 @@ class SxCPSeedControl:
)
class SxCPSeedLocker:
@classmethod
def INPUT_TYPES(cls):
seed_spec = {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}
reroll_seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}
return {
"required": {
"base_seed": ("INT", seed_spec),
"reroll_axis": (
[
"none",
"category",
"subcategory",
"content",
"person",
"scene",
"pose",
"role",
"expression",
"composition",
"content_pose",
"scene_pose",
],
{"default": "none"},
),
"reroll_seed": ("INT", reroll_seed_spec),
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("seed_config", "summary")
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(self, base_seed, reroll_axis, reroll_seed):
config = build_seed_lock_config_json(base_seed=base_seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed)
summary = f"base {base_seed}; reroll {reroll_axis} with {'main seed' if int(reroll_seed) < 0 else reroll_seed}"
return config, summary
class SxCPCameraControl:
@classmethod
def INPUT_TYPES(cls):
@@ -231,6 +275,7 @@ class SxCPCameraControl:
"orientation": (camera_orientation_choices(), {"default": "vertical_story"}),
"phone_visibility": (camera_phone_choices(), {"default": "phone_visible"}),
"priority": (camera_priority_choices(), {"default": "locked"}),
"camera_detail": (camera_detail_choices(), {"default": "compact"}),
}
}
@@ -249,6 +294,7 @@ class SxCPCameraControl:
orientation,
phone_visibility,
priority,
camera_detail,
):
return (
build_camera_config_json(
@@ -260,6 +306,7 @@ class SxCPCameraControl:
orientation=orientation,
phone_visibility=phone_visibility,
priority=priority,
camera_detail=camera_detail,
),
)
@@ -360,10 +407,17 @@ class SxCPAdvancedFilters:
def INPUT_TYPES(cls):
return {
"required": {
"ethnicity": (["any", "asian", "white_asian"], {"default": "any"}),
"include_european": ("BOOLEAN", {"default": True}),
"include_mediterranean_mena": ("BOOLEAN", {"default": True}),
"include_latina": ("BOOLEAN", {"default": True}),
"include_east_asian": ("BOOLEAN", {"default": True}),
"include_southeast_asian": ("BOOLEAN", {"default": True}),
"include_south_asian": ("BOOLEAN", {"default": True}),
"include_black_african": ("BOOLEAN", {"default": True}),
"include_indigenous": ("BOOLEAN", {"default": True}),
"include_mixed": ("BOOLEAN", {"default": True}),
"include_plus_size": ("BOOLEAN", {"default": True}),
"figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}),
"no_plus_women": ("BOOLEAN", {"default": False}),
"no_black": ("BOOLEAN", {"default": False}),
}
}
@@ -372,13 +426,33 @@ class SxCPAdvancedFilters:
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(self, ethnicity, figure, no_plus_women, no_black):
def build(
self,
include_european,
include_mediterranean_mena,
include_latina,
include_east_asian,
include_southeast_asian,
include_south_asian,
include_black_african,
include_indigenous,
include_mixed,
include_plus_size,
figure,
):
return (
build_filter_config_json(
ethnicity=ethnicity,
figure=figure,
no_plus_women=no_plus_women,
no_black=no_black,
include_european=include_european,
include_mediterranean_mena=include_mediterranean_mena,
include_latina=include_latina,
include_east_asian=include_east_asian,
include_southeast_asian=include_southeast_asian,
include_south_asian=include_south_asian,
include_black_african=include_black_african,
include_indigenous=include_indigenous,
include_mixed=include_mixed,
include_plus_size=include_plus_size,
),
)
@@ -673,14 +747,16 @@ class SxCPInstaOFOptions:
"hardcore_cast": (["use_counts", "couple", "threesome", "group"], {"default": "use_counts"}),
"hardcore_women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
"hardcore_men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}),
"softcore_level": (["social_tease", "lingerie_tease", "implied_nude", "explicit_tease"], {"default": "lingerie_tease"}),
"softcore_level": (["social_tease", "lingerie_tease", "implied_nude", "explicit_tease", "explicit_nude"], {"default": "lingerie_tease"}),
"hardcore_level": (["explicit", "hardcore"], {"default": "hardcore"}),
"softcore_expression_intensity": ("FLOAT", {"default": 0.45, "min": 0.0, "max": 1.0, "step": 0.01}),
"hardcore_expression_intensity": ("FLOAT", {"default": 0.85, "min": 0.0, "max": 1.0, "step": 0.01}),
"platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}),
"continuity": (["same_creator_same_room", "same_creator_new_scene"], {"default": "same_creator_same_room"}),
"hardcore_clothing_continuity": (["none", "same_outfit", "partially_removed", "implied_nude", "explicit_nude"], {"default": "partially_removed"}),
"softcore_camera_mode": (camera_mode_choices(), {"default": "handheld_selfie"}),
"hardcore_camera_mode": (["same_as_softcore"] + camera_mode_choices(), {"default": "same_as_softcore"}),
"camera_detail": (camera_detail_choices(), {"default": "compact"}),
}
}
@@ -701,8 +777,10 @@ class SxCPInstaOFOptions:
hardcore_expression_intensity,
platform_style,
continuity,
hardcore_clothing_continuity,
softcore_camera_mode,
hardcore_camera_mode,
camera_detail,
):
return (
build_insta_of_options_json(
@@ -716,8 +794,10 @@ class SxCPInstaOFOptions:
hardcore_expression_intensity=hardcore_expression_intensity,
platform_style=platform_style,
continuity=continuity,
hardcore_clothing_continuity=hardcore_clothing_continuity,
softcore_camera_mode=softcore_camera_mode,
hardcore_camera_mode=hardcore_camera_mode,
camera_detail=camera_detail,
),
)
@@ -730,16 +810,15 @@ class SxCPInstaOFPromptPair:
"row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}),
"start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}),
"seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}),
"ethnicity": (["any", "asian", "white_asian"], {"default": "any"}),
"ethnicity": (ethnicity_choices(), {"default": "any"}),
"figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}),
"no_plus_women": ("BOOLEAN", {"default": False}),
"no_black": ("BOOLEAN", {"default": False}),
"trigger": ("STRING", {"default": "sxcpinup_coloredpencil"}),
"prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}),
},
"optional": {
"seed_config": ("STRING", {"default": "", "multiline": True}),
"options_json": ("STRING", {"default": "", "multiline": True}),
"filter_config": ("STRING", {"default": "", "multiline": True}),
"camera_config": ("STRING", {"default": "", "multiline": True}),
"character_profile": ("STRING", {"default": "", "multiline": True}),
"extra_positive": ("STRING", {"default": "", "multiline": True}),
@@ -768,16 +847,17 @@ class SxCPInstaOFPromptPair:
seed,
ethnicity,
figure,
no_plus_women,
no_black,
trigger,
prepend_trigger_to_prompt,
seed_config="",
options_json="",
filter_config="",
camera_config="",
character_profile="",
extra_positive="",
extra_negative="",
no_plus_women=False,
no_black=False,
):
row = build_insta_of_pair(
row_number=row_number,
@@ -791,6 +871,7 @@ class SxCPInstaOFPromptPair:
prepend_trigger_to_prompt=prepend_trigger_to_prompt,
seed_config=seed_config or "",
options_json=options_json or "",
filter_config=filter_config or "",
camera_config=camera_config or "",
character_profile=character_profile or "",
extra_positive=extra_positive or "",
@@ -811,6 +892,7 @@ class SxCPInstaOFPromptPair:
NODE_CLASS_MAPPINGS = {
"SxCPPromptBuilder": SxCPPromptBuilder,
"SxCPSeedControl": SxCPSeedControl,
"SxCPSeedLocker": SxCPSeedLocker,
"SxCPCameraControl": SxCPCameraControl,
"SxCPCategoryPreset": SxCPCategoryPreset,
"SxCPCastControl": SxCPCastControl,
@@ -828,6 +910,7 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"SxCPPromptBuilder": "SxCP Prompt Builder",
"SxCPSeedControl": "SxCP Seed Control",
"SxCPSeedLocker": "SxCP Seed Locker",
"SxCPCameraControl": "SxCP Camera Control",
"SxCPCategoryPreset": "SxCP Category Preset",
"SxCPCastControl": "SxCP Cast Control",
+111 -14
View File
@@ -2782,21 +2782,122 @@ WHITE_KEYWORDS = (
"french",
"mediterranean",
)
EAST_ASIAN_KEYWORDS = (
"east asian",
"japanese",
"korean",
"chinese",
"taiwanese",
"mongolian",
"tibetan",
"manchu",
"okinawan",
)
SOUTHEAST_ASIAN_KEYWORDS = (
"southeast asian",
"vietnamese",
"thai",
"filipina",
"filipino",
"indonesian",
"malay",
"cambodian",
"lao",
"burmese",
"singaporean",
"hmong",
"balinese",
)
SOUTH_ASIAN_KEYWORDS = (
"south asian",
"indian",
"punjabi",
"tamil",
"bengali",
"sri lankan",
"nepali",
"pakistani",
"gujarati",
"bangladeshi",
"malayali",
"kashmiri",
)
MEDITERRANEAN_MENA_KEYWORDS = (
"mediterranean",
"greek",
"italian",
"spanish",
"portuguese",
"turkish",
"persian",
"levantine",
"maghrebi",
"egyptian",
"moroccan",
"amazigh",
"kurdish",
"middle-eastern",
"middle eastern",
"mena",
"olive",
)
LATINA_KEYWORDS = (
"latina",
"latino",
"mexican",
"chicana",
"colombian",
"brazilian",
"puerto rican",
"cuban",
"dominican",
"venezuelan",
"peruvian",
"chilean",
"argentine",
"uruguayan",
"ecuadorian",
)
BLACK_AFRICAN_KEYWORDS = ("african", "african-diaspora", "cape verdean")
INDIGENOUS_KEYWORDS = ("indigenous", "amazigh")
MIXED_KEYWORDS = ("mixed",)
ETHNICITY_KEYWORD_GROUPS = {
"asian": ASIAN_KEYWORDS,
"white_asian": WHITE_KEYWORDS + ASIAN_KEYWORDS,
"european": WHITE_KEYWORDS,
"mediterranean_mena": MEDITERRANEAN_MENA_KEYWORDS,
"latina": LATINA_KEYWORDS,
"east_asian": EAST_ASIAN_KEYWORDS,
"southeast_asian": SOUTHEAST_ASIAN_KEYWORDS,
"south_asian": SOUTH_ASIAN_KEYWORDS,
"black_african": BLACK_AFRICAN_KEYWORDS,
"indigenous": INDIGENOUS_KEYWORDS,
"mixed": MIXED_KEYWORDS,
}
def by_ethnicity(pool: list, ethnicity: str) -> list:
"""Filter an appearance pool by heritage keywords found in the skin field.
'asian' = East/Southeast/South/Central Asian; 'white_asian' = white/European + Asian;
'any' returns the full pool."""
ethnicity = str(ethnicity or "any").strip().lower()
if ethnicity == "any":
return pool
if ethnicity == "asian":
kws = ASIAN_KEYWORDS
elif ethnicity == "white_asian":
kws = WHITE_KEYWORDS + ASIAN_KEYWORDS
else:
kws = (ethnicity,)
tokens = [token.strip() for token in re.split(r"[,+|;/\s]+", ethnicity) if token.strip()]
kws: list[str] = []
exclude_kws: list[str] = []
for token in tokens:
if token.startswith("exclude_"):
exclude_key = token.removeprefix("exclude_")
exclude_kws.extend(ETHNICITY_KEYWORD_GROUPS.get(exclude_key, (exclude_key,)))
continue
if token in ("not_mixed", "no_mixed"):
exclude_kws.extend(MIXED_KEYWORDS)
continue
kws.extend(ETHNICITY_KEYWORD_GROUPS.get(token, (token,)))
filtered = [e for e in pool if any(k in e[3].lower() for k in kws)]
if exclude_kws:
filtered = [e for e in filtered if not any(k in e[3].lower() for k in exclude_kws)]
return filtered or pool
@@ -2894,10 +2995,7 @@ def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_de
clothes = choose(rng, WOMEN_CLOTHES_MINIMAL if minimal else WOMEN_CLOTHES)
figure_note = choose(rng, figure_pool(figure))
else:
# The ethnicity bias targets women; men stay any-heritage unless the
# batch is a fully-themed 'asian' batch.
men_eth = ethnicity if ethnicity == "asian" else "any"
men_pool = by_ethnicity(MEN, men_eth)
men_pool = by_ethnicity(MEN, ethnicity)
subject, age, body, skin, hair, eyes = choose(rng, men_pool)
clothes = choose(rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES)
figure_note = ""
@@ -3247,11 +3345,10 @@ def main() -> None:
)
parser.add_argument(
"--ethnicity",
choices=["any", "asian", "white_asian"],
choices=["any", *ETHNICITY_KEYWORD_GROUPS.keys()],
default="any",
help="'any' = balanced heritage mix (default); "
"'asian' = restrict all subjects to Asian (East/Southeast/South/Central Asian); "
"'white_asian' = bias women to white/European + Asian (men stay any heritage).",
help="'any' = balanced heritage mix (default). Other values restrict the appearance pool "
"by heritage keywords, e.g. east_asian, latina, black_african, mixed, asian, or white_asian.",
)
parser.add_argument(
"--poses",
+52 -18
View File
@@ -164,6 +164,10 @@ def _combine_negative(*parts: str) -> str:
return ", ".join(cleaned)
def _prompt_cast_descriptors(text: str) -> str:
return _clean(text).replace("Woman A / primary creator:", "Woman A:")
def _clean_age(age: Any) -> str:
return _clean(age)
@@ -205,15 +209,50 @@ def _camera_phrase(row: dict[str, Any]) -> str:
return directive
config = row.get("camera_config")
if isinstance(config, dict):
detail = _clean(config.get("camera_detail"))
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
return ""
mode = _clean(config.get("camera_mode")).replace("_", " ")
shot = _clean(config.get("shot_size")).replace("_", " ")
angle = _clean(config.get("angle")).replace("_", " ")
pieces = [piece for piece in (mode, shot, angle) if piece and piece != "auto" and piece != "standard"]
if pieces:
return "Camera framing uses " + ", ".join(pieces)
return "Camera: " + ", ".join(pieces)
return ""
def _camera_phrase_from_config(config: Any) -> str:
if not isinstance(config, dict):
return ""
detail = _clean(config.get("camera_detail"))
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
return ""
values = [
_clean(config.get("camera_mode")).replace("_", " "),
_clean(config.get("shot_size")).replace("_", " "),
_clean(config.get("angle")).replace("_", " "),
_clean(config.get("lens")).replace("_", " "),
_clean(config.get("distance")).replace("_", " "),
_clean(config.get("orientation")).replace("_", " "),
_clean(config.get("phone_visibility")).replace("_", " "),
]
pieces = [value for value in values if value and value not in ("auto", "standard")]
if not pieces:
return ""
return "Camera: " + ", ".join(pieces)
def _pair_camera_phrase(directive: Any, config: Any, row: dict[str, Any]) -> str:
directive_text = _clean(directive)
if directive_text:
return directive_text
if isinstance(config, dict) and (
_clean(config.get("camera_detail")) == "off" or _clean(config.get("camera_mode")) == "disabled"
):
return ""
return _camera_phrase_from_config(config) or _camera_phrase(row)
def _style_phrase(row: dict[str, Any], style_mode: str) -> str:
if style_mode == "minimal":
return ""
@@ -323,31 +362,27 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
cast_descriptor_text = "; ".join(_clean(item) for item in cast_descriptors if _clean(item))
else:
cast_descriptor_text = _clean(cast_descriptors)
cast_descriptor_text = _prompt_cast_descriptors(cast_descriptor_text)
soft = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
hard = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
soft_camera = _clean(row.get("softcore_camera_directive")) or _camera_phrase(soft)
hard_camera = _clean(row.get("hardcore_camera_directive")) or _camera_phrase(hard)
soft_camera = _pair_camera_phrase(row.get("softcore_camera_directive"), row.get("softcore_camera_config"), soft)
hard_camera = _pair_camera_phrase(row.get("hardcore_camera_directive"), row.get("hardcore_camera_config"), hard)
soft_style = _style_phrase(soft, style_mode)
hard_style = _style_phrase(hard, style_mode)
options = row.get("options") if isinstance(row.get("options"), dict) else {}
soft_level = _clean(options.get("softcore_level")).replace("_", " ")
hard_level = _clean(options.get("hardcore_level")).replace("_", " ")
hard_cast = _clean(row.get("hardcore_women_count"))
hard_men = _clean(row.get("hardcore_men_count"))
hard_cast_text = _clean(hard.get("cast_summary")) or (
f"{hard_cast} adult women and {hard_men} adult men" if hard_cast or hard_men else ""
)
same_room = options.get("continuity") == "same_creator_same_room"
hard_scene = soft.get("scene_text") if same_room and soft.get("scene_text") else hard.get("scene_text")
hard_composition = soft.get("composition") if same_room and soft.get("composition") else hard.get("composition")
hard_composition = hard.get("composition")
soft_cast_descriptor_text = (
cast_descriptor_text
if options.get("softcore_cast") == "same_as_hardcore"
else f"Woman A / primary creator: {descriptor}"
else f"Woman A: {descriptor}"
)
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
soft_cast_presence = (
"The same cast is present together in a non-explicit teaser pose, with no sex act or genital contact"
"Woman A and the listed partners are present together in a non-explicit teaser pose, with no sex act or genital contact"
if same_soft_cast
else "The softcore version focuses on Woman A alone"
)
@@ -361,12 +396,11 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
partner_pose = ""
soft_parts = [
descriptor,
f"Shared cast descriptors: {soft_cast_descriptor_text}" if same_soft_cast and soft_cast_descriptor_text else "",
f"Softcore primary creator descriptor: {soft_cast_descriptor_text}" if not same_soft_cast and soft_cast_descriptor_text else "",
f"Cast descriptors: {soft_cast_descriptor_text}" if same_soft_cast and soft_cast_descriptor_text else "",
soft_cast_descriptor_text if not same_soft_cast and soft_cast_descriptor_text else "",
soft_cast_presence,
f"Partner softcore styling: {partner_outfit_text}" if partner_outfit_text else "",
f"The shared softcore cast pose is {partner_pose}" if partner_pose else "",
f"Cast pose: {partner_pose}" if partner_pose else "",
f"shown in a {soft_level or 'softcore'} Insta/OF creator image",
f"wearing {soft.get('item')}" if soft.get("item") else "",
f"{soft.get('pose')}" if soft.get("pose") else "",
@@ -377,9 +411,9 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str)
soft_style if detail_level != "concise" else "",
]
hard_parts = [
f"The primary creator remains {descriptor}, visually central in a {hard_level or 'hardcore'} scene",
f"{'Shared' if same_soft_cast else 'Hardcore'} cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "",
f"The cast includes {hard_cast_text}" if hard_cast_text else "",
f"{hard_level or 'hardcore'} scene with Woman A visually central",
f"Cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "",
_clean(row.get("hardcore_clothing_state")),
_clean(hard.get("role_graph")),
f"The sexual action is {hard.get('item')}" if hard.get("item") else "",
f"set in {hard_scene}" if hard_scene else "",
+282 -27
View File
@@ -49,6 +49,35 @@ SEED_AXIS_ALIASES = {
"composition": ("composition_seed", "camera_seed", "composition"),
}
SEED_LOCK_AXES = (
"category",
"subcategory",
"content",
"person",
"scene",
"pose",
"role",
"expression",
"composition",
)
ETHNICITY_FILTER_CHOICES = [
"any",
"european",
"mediterranean_mena",
"latina",
"east_asian",
"southeast_asian",
"south_asian",
"black_african",
"indigenous",
"mixed",
"asian",
"white_asian",
]
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
GENERIC_POSITIVE_SUFFIX = (
"Use crisp clean comic linework, detailed hatching, soft blended shading, "
"pastel skin tones, muted blues and pinks, warm sensual lighting, and tactile textured paper."
@@ -79,6 +108,7 @@ LAYOUT_TEMPLATE = (
)
CAMERA_MODE_PROMPTS = {
"disabled": "",
"standard": "",
"handheld_selfie": (
"Camera mode: handheld smartphone selfie, close arm-length framing, visible creator-shot perspective, "
@@ -109,6 +139,47 @@ CAMERA_MODE_PROMPTS = {
),
}
CAMERA_COMPACT_LABELS = {
"disabled": "",
"standard": "",
"handheld_selfie": "handheld smartphone selfie",
"mirror_selfie": "mirror selfie",
"phone_tripod": "phone tripod / ring-light setup",
"creator_pov": "creator-held POV",
"bed_selfie": "bed selfie",
"bathroom_mirror": "bathroom mirror selfie",
"phone_flash": "phone-flash selfie",
"action_cam": "handheld action-camera view",
"full_body": "full body",
"three_quarter": "three-quarter body",
"waist_up": "waist-up",
"close_up": "close-up",
"extreme_close_up": "extreme close-up",
"eye_level": "eye-level",
"high_angle": "high-angle",
"low_angle": "low-angle",
"overhead": "overhead",
"side_profile": "side-profile",
"rear_view": "rear-view",
"mirror_reflection": "mirror reflection",
"smartphone_wide": "smartphone wide-angle",
"ultra_wide": "ultra-wide",
"portrait_lens": "phone portrait lens",
"telephoto": "telephoto-style",
"macro_detail": "macro detail",
"arm_length": "arm-length",
"near_body": "near-body",
"bedside": "bedside phone",
"room_corner": "room-corner phone",
"vertical_story": "vertical 9:16",
"square_feed": "square feed",
"horizontal": "horizontal",
"phone_visible": "phone visible",
"phone_hidden": "phone hidden",
"screen_reflection": "screen reflection",
"ring_light_visible": "ring light visible",
}
CAMERA_SHOT_PROMPTS = {
"auto": "",
"full_body": "Shot size: full body visible, head-to-toe framing, no important body parts cropped out.",
@@ -917,13 +988,46 @@ def build_filter_config_json(
figure: str = "curvy",
no_plus_women: bool = False,
no_black: bool = False,
include_european: bool = True,
include_mediterranean_mena: bool = True,
include_latina: bool = True,
include_east_asian: bool = True,
include_southeast_asian: bool = True,
include_south_asian: bool = True,
include_black_african: bool = True,
include_indigenous: bool = True,
include_mixed: bool = True,
include_plus_size: bool = True,
) -> str:
include_flags = {
"european": include_european,
"mediterranean_mena": include_mediterranean_mena,
"latina": include_latina,
"east_asian": include_east_asian,
"southeast_asian": include_southeast_asian,
"south_asian": include_south_asian,
"black_african": include_black_african,
"indigenous": include_indigenous,
"mixed": include_mixed,
}
selected_ethnicities = [key for key, enabled in include_flags.items() if enabled]
disabled_ethnicities = [key for key, enabled in include_flags.items() if not enabled]
enabled_ethnicities = list(selected_ethnicities)
if enabled_ethnicities:
enabled_ethnicities.extend(f"exclude_{key}" for key in disabled_ethnicities)
if 0 < len(selected_ethnicities) < len(include_flags):
ethnicity = "+".join(enabled_ethnicities)
elif ethnicity not in ETHNICITY_FILTER_CHOICES:
ethnicity = "any"
return json.dumps(
{
"ethnicity": ethnicity if ethnicity in ("any", "asian", "white_asian") else "any",
"ethnicity": ethnicity,
"ethnicity_includes": selected_ethnicities,
"figure": figure if figure in ("curvy", "balanced", "bombshell") else "curvy",
"no_plus_women": bool(no_plus_women),
"no_black": bool(no_black),
"include_plus_size": bool(include_plus_size),
"include_black_african": bool(include_black_african),
"no_plus_women": not bool(include_plus_size) or bool(no_plus_women),
"no_black": not bool(include_black_african) or bool(no_black),
},
ensure_ascii=True,
sort_keys=True,
@@ -931,7 +1035,14 @@ def build_filter_config_json(
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
defaults = {"ethnicity": "any", "figure": "curvy", "no_plus_women": False, "no_black": False}
defaults = {
"ethnicity": "any",
"figure": "curvy",
"no_plus_women": False,
"no_black": False,
"include_plus_size": True,
"include_black_african": True,
}
if not filter_config:
return defaults
if isinstance(filter_config, dict):
@@ -944,8 +1055,11 @@ def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str
if not isinstance(raw, dict):
raise ValueError("filter_config must be a JSON object")
parsed = {**defaults, **raw}
parsed["ethnicity"] = parsed["ethnicity"] if parsed.get("ethnicity") in ("any", "asian", "white_asian") else "any"
ethnicity = str(parsed.get("ethnicity") or "any")
parsed["ethnicity"] = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in ethnicity else "any"
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell") else "curvy"
parsed["include_plus_size"] = bool(parsed.get("include_plus_size"))
parsed["include_black_african"] = bool(parsed.get("include_black_african"))
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
parsed["no_black"] = bool(parsed.get("no_black"))
return parsed
@@ -997,6 +1111,34 @@ def build_seed_config_json(
)
def build_seed_lock_config_json(
base_seed: int = 20260614,
reroll_axis: str = "none",
reroll_seed: int = -1,
) -> str:
base_seed = int(base_seed)
reroll_seed = int(reroll_seed)
reroll_groups = {
"none": (),
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
"role": ("role",),
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
config: dict[str, int] = {}
for axis in SEED_LOCK_AXES:
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
if not seed_config:
return {}
@@ -1075,6 +1217,14 @@ def camera_mode_choices() -> list[str]:
return list(CAMERA_MODE_PROMPTS)
def ethnicity_choices() -> list[str]:
return list(ETHNICITY_FILTER_CHOICES)
def camera_detail_choices() -> list[str]:
return list(CAMERA_DETAIL_CHOICES)
def camera_shot_choices() -> list[str]:
return list(CAMERA_SHOT_PROMPTS)
@@ -1112,6 +1262,7 @@ def build_camera_config_json(
orientation: str = "auto",
phone_visibility: str = "auto",
priority: str = "strong",
camera_detail: str = "compact",
) -> str:
return json.dumps(
{
@@ -1123,6 +1274,7 @@ def build_camera_config_json(
"orientation": orientation,
"phone_visibility": phone_visibility,
"priority": priority,
"camera_detail": camera_detail,
},
ensure_ascii=True,
sort_keys=True,
@@ -1144,6 +1296,7 @@ def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str
"orientation": "auto",
"phone_visibility": "auto",
"priority": "strong",
"camera_detail": "compact",
}
if not camera_config:
return defaults
@@ -1166,6 +1319,9 @@ def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str
"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"]),
"camera_detail": str(parsed.get("camera_detail") or defaults["camera_detail"])
if str(parsed.get("camera_detail") or defaults["camera_detail"]) in CAMERA_DETAIL_CHOICES
else defaults["camera_detail"],
}
@@ -1178,6 +1334,26 @@ def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, str]]:
parsed = _parse_camera_config(camera_config)
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
return "", parsed
if parsed["camera_detail"] == "compact":
values = [
parsed["camera_mode"],
parsed["shot_size"],
parsed["angle"],
parsed["lens"],
parsed["distance"],
parsed["orientation"],
parsed["phone_visibility"],
]
labels = [CAMERA_COMPACT_LABELS.get(value, value.replace("_", " ")) for value in values]
labels = [label for value, label in zip(values, labels) if label and value != "auto"]
if not labels:
return "", parsed
directive = "Camera: " + ", ".join(labels) + "."
if parsed["priority"] == "locked":
directive += " Keep this camera framing."
return directive, parsed
parts = [
CAMERA_MODE_PROMPTS[parsed["camera_mode"]],
CAMERA_SHOT_PROMPTS[parsed["shot_size"]],
@@ -1679,7 +1855,7 @@ def _appearance_for_subject(
subject_type = "woman" if rng.random() < 0.82 else "man"
if subject_type == "man":
men_ethnicity = ethnicity if ethnicity == "asian" else "any"
men_ethnicity = ethnicity if ethnicity else "any"
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
return {
"subject_type": "man",
@@ -2452,7 +2628,7 @@ def build_prompt(
start_index = max(1, int(start_index))
seed = int(seed)
clothing = clothing if clothing in ("full", "minimal") else "full"
ethnicity = ethnicity if ethnicity in ("any", "asian", "white_asian") else "any"
ethnicity = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in str(ethnicity) else "any"
poses = poses if poses in ("standard", "evocative") else "standard"
figure = figure if figure in ("curvy", "balanced", "bombshell") else "curvy"
minimal_ratio = _ratio_or_none(minimal_clothing_ratio)
@@ -2574,6 +2750,7 @@ INSTA_OF_SOFT_LEVELS = {
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate",
"implied_nude": "implied nude creator set, strategically covered body and intimate teaser framing",
"explicit_tease": "stronger adult teaser set with bolder nude-adjacent styling and solo-tease framing",
"explicit_nude": "explicit nude creator set with fully nude solo-tease framing",
}
INSTA_OF_HARDCORE_LEVELS = {
@@ -2587,6 +2764,14 @@ INSTA_OF_PLATFORM_STYLES = {
"onlyfans": "OnlyFans-inspired creator shoot, intimate subscriber-view camera and candid premium-content framing",
}
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = {
"none": "",
"same_outfit": "Woman A keeps the softcore outfit in the hardcore scene",
"partially_removed": "Woman A's softcore outfit is partially removed or pushed aside for the hardcore scene",
"implied_nude": "Woman A is nude-adjacent in the hardcore scene, with the softcore outfit slipping off or covering only part of the body",
"explicit_nude": "Woman A is fully nude in the hardcore scene, with the removed softcore outfit visible nearby",
}
INSTA_OF_NEGATIVE = (
"minors, childlike appearance, teen, underage, schoolgirl, non-consensual, coercion, rape, "
"violence, injury, blood, gore, incest, bestiality, watermark, logo, readable username, social media UI"
@@ -2603,6 +2788,7 @@ INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = {
"lingerie_tease": "Provocative erotic clothes / Provocative lingerie",
"implied_nude": "Provocative erotic clothes / Provocative lingerie",
"explicit_tease": "Provocative erotic clothes / Sheer exposed",
"explicit_nude": "Provocative erotic clothes / Nude accessories",
}
INSTA_OF_SOFTCORE_OUTFITS = {
@@ -2642,6 +2828,14 @@ INSTA_OF_SOFTCORE_OUTFITS = {
"bare-shoulder robe opened around covered lingerie, explicit adult tease without partnered contact",
"strappy lingerie set with covered cups and high-waisted bottoms, styled as a stronger solo teaser",
],
"explicit_nude": [
"fully nude creator styling with jewelry, heels, and direct adult selfie confidence",
"fully nude mirror-selfie styling with jewelry only and bold creator-shot framing",
"nude-on-sheets creator pose with lingerie discarded nearby and direct eye contact",
"fully nude vanity-mirror pose with heels, necklace, and premium adult teaser styling",
"nude shower-afterglow creator pose with wet hair, skin highlights, and phone-shot framing",
"fully nude bedroom creator pose with one hand holding the phone and lingerie visible nearby",
],
}
INSTA_OF_SOFTCORE_POSES = {
@@ -2677,6 +2871,14 @@ INSTA_OF_SOFTCORE_POSES = {
"sitting at the vanity in a bolder covered lingerie pose with direct eye contact",
"arching subtly in a solo adult tease while the styling keeps explicit anatomy obscured",
],
"explicit_nude": [
"taking a bold nude mirror selfie with direct eye contact and the body clearly framed",
"posing fully nude on the bed with jewelry and heels as the only styling",
"standing at the vanity fully nude in a premium creator-shot pose",
"reclining fully nude on soft sheets with the phone held close",
"turning slightly in a nude mirror pose with the body framed head-to-thigh",
"kneeling fully nude in a controlled adult teaser pose with direct phone-camera awareness",
],
}
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = [
@@ -2711,8 +2913,10 @@ def build_insta_of_options_json(
hardcore_level: str = "hardcore",
platform_style: str = "hybrid",
continuity: str = "same_creator_same_room",
hardcore_clothing_continuity: str = "partially_removed",
softcore_camera_mode: str = "handheld_selfie",
hardcore_camera_mode: str = "same_as_softcore",
camera_detail: str = "compact",
softcore_expression_intensity: float = 0.45,
hardcore_expression_intensity: float = 0.85,
) -> str:
@@ -2726,8 +2930,10 @@ def build_insta_of_options_json(
"hardcore_level": hardcore_level,
"platform_style": platform_style,
"continuity": continuity,
"hardcore_clothing_continuity": hardcore_clothing_continuity,
"softcore_camera_mode": softcore_camera_mode,
"hardcore_camera_mode": hardcore_camera_mode,
"camera_detail": camera_detail,
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
},
@@ -2746,8 +2952,10 @@ 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",
"hardcore_clothing_continuity": "partially_removed",
"softcore_camera_mode": "handheld_selfie",
"hardcore_camera_mode": "same_as_softcore",
"camera_detail": "compact",
"softcore_expression_intensity": 0.45,
"hardcore_expression_intensity": 0.85,
}
@@ -2769,9 +2977,15 @@ 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["hardcore_clothing_continuity"] = (
parsed["hardcore_clothing_continuity"]
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
else defaults["hardcore_clothing_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"]
parsed["camera_detail"] = parsed["camera_detail"] if parsed["camera_detail"] in CAMERA_DETAIL_CHOICES else defaults["camera_detail"]
parsed["softcore_expression_intensity"] = _clamped_float(
parsed.get("softcore_expression_intensity"),
defaults["softcore_expression_intensity"],
@@ -2865,13 +3079,17 @@ def _insta_of_cast_phrase(women_count: int, men_count: int) -> str:
return context["cast_summary"]
def _insta_of_prompt_cast_descriptors(text: str) -> str:
return str(text or "").replace("Woman A / primary creator:", "Woman A:")
SOFTCORE_CAST_POSES = [
"standing together for a mirror selfie with bodies close but no sexual contact",
"posing shoulder-to-shoulder in a creator-shot group teaser",
"leaning together on the bed in a non-explicit subscriber preview",
"sitting close together with hands kept above clothing",
"arranged around Woman A in a flirtatious non-explicit teaser pose",
"posing in the same room as a coordinated adult creator set",
"posing together as a coordinated adult creator set",
"standing near the phone tripod with relaxed teasing body language",
"framed together in a softcore cast reveal with no sex act",
]
@@ -2896,6 +3114,15 @@ def _insta_of_softcore_pose(rng: random.Random, level: str) -> str:
return g.choose(rng, pool)
def _insta_of_hardcore_clothing_state(mode: str, softcore_outfit: str) -> str:
mode = mode if mode in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY else "none"
outfit = str(softcore_outfit or "").strip()
if mode == "none" or not outfit:
return ""
base = INSTA_OF_HARDCORE_CLOTHING_CONTINUITY[mode]
return f"Clothing state: {base}; softcore visual reference: {outfit}."
def _insta_of_partner_styling(
seed_config: dict[str, int],
seed: int,
@@ -2934,12 +3161,19 @@ 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,
filter_config: str | dict[str, Any] | None = None,
camera_config: str | dict[str, Any] | None = None,
character_profile: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
options = _parse_insta_of_options(options_json)
if filter_config:
filters = _parse_filter_config(filter_config)
ethnicity = filters["ethnicity"]
figure = filters["figure"]
no_plus_women = filters["no_plus_women"]
no_black = filters["no_black"]
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
active_trigger = trigger.strip() or g.TRIGGER
parsed_seed_config = _parse_seed_config(seed_config)
@@ -3015,11 +3249,11 @@ def build_insta_of_pair(
hard_women_count,
hard_men_count,
)
cast_descriptor_text = "; ".join(cast_descriptors)
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
soft_cast_descriptor_text = (
cast_descriptor_text
if options["softcore_cast"] == "same_as_hardcore"
else f"Woman A / primary creator: {descriptor}"
else f"Woman A: {descriptor}"
)
soft_partner_styling = _insta_of_partner_styling(
parsed_seed_config,
@@ -3039,33 +3273,44 @@ def build_insta_of_pair(
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_config["camera_detail"] = options["camera_detail"]
hard_camera_config["camera_detail"] = options["camera_detail"]
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"]
hard_composition = hard_row["composition"]
soft_cast = (
"solo creator setup; the primary creator is alone in the softcore version"
"solo creator setup with Woman A alone"
if options["softcore_cast"] == "solo"
else f"non-explicit teaser setup with the same adult cast as the hardcore version: {_insta_of_cast_phrase(hard_women_count, hard_men_count)}"
else f"non-explicit teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}"
)
soft_cast_presence = (
"Keep the same cast together in the softcore version in a non-explicit teaser pose with no sex act or genital contact. "
"Place Woman A and the listed partners together in a non-explicit teaser pose with no sex act or genital contact. "
if options["softcore_cast"] == "same_as_hardcore"
else "Keep the softcore version focused on Woman A alone. "
)
soft_cast_styling_sentence = (
f"Partner softcore styling: {soft_partner_outfit_text}. Shared softcore cast pose: {soft_partner_styling['pose']}. "
f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. "
if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text
else ""
)
hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count)
hard_clothing_state = _insta_of_hardcore_clothing_state(
options["hardcore_clothing_continuity"],
soft_row["item"],
)
soft_descriptor_sentence = (
f"Cast descriptors: {soft_cast_descriptor_text}. "
if options["softcore_cast"] == "same_as_hardcore"
else f"Woman A: {descriptor}. "
)
soft_prompt = (
f"Insta/OF softcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. "
f"Softcore setup: {soft_level}. Cast continuity: {soft_cast}. "
f"Shared cast descriptors: {soft_cast_descriptor_text}. "
f"Insta/OF softcore mode: {platform_style}. "
f"{soft_descriptor_sentence}"
f"Softcore setup: {soft_level}. Cast: {soft_cast}. "
f"{soft_cast_presence}"
f"{soft_cast_styling_sentence}"
f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
@@ -3075,10 +3320,11 @@ def build_insta_of_pair(
f"{soft_row['positive_suffix']}."
)
hard_prompt = (
f"Insta/OF hardcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. "
f"Insta/OF hardcore mode: {platform_style}. "
f"Hardcore setup: {hard_level}. Cast: {hard_cast}. "
f"Shared cast descriptors: {cast_descriptor_text}. "
"Apply the shared descriptor to the most visually central woman, keeping her continuous with the softcore version. "
f"Cast descriptors: {cast_descriptor_text}. "
"Keep Woman A visually central. "
f"{hard_clothing_state} "
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}"
@@ -3103,20 +3349,29 @@ def build_insta_of_pair(
soft_partner_styling["pose"],
soft_row["scene_text"],
soft_row["composition"],
f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera",
f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera" if soft_camera_directive else "",
]
soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip())
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_camera_config['camera_mode'].replace('_', ' ')} camera"
)
hard_caption_parts = [
active_trigger,
"Insta/OF hardcore mode",
"Woman A",
descriptor,
hard_cast,
hard_row["role_graph"],
hard_row["item"],
hard_scene,
hard_composition,
f"{hard_camera_config['camera_mode'].replace('_', ' ')} camera" if hard_camera_directive else "",
]
hard_caption = ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip())
metadata = {
"mode": "Insta/OF",
"options": options,
"shared_descriptor": descriptor,
"shared_cast_descriptors": cast_descriptors,
"softcore_partner_styling": soft_partner_styling,
"hardcore_clothing_state": hard_clothing_state,
"softcore_prompt": soft_prompt,
"hardcore_prompt": hard_prompt,
"softcore_negative_prompt": soft_negative,