From fb6d99ac20bc7b9a734f3d67c1c4ebe026ff9bd2 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Wed, 24 Jun 2026 17:42:11 +0200 Subject: [PATCH] Tune hardcore detail density --- README.md | 4 + __init__.py | 5 + categories/expression_composition_pools.json | 6 +- categories/sexual_poses.json | 8 +- examples/default_task_lanes_workflow.json | 4 +- krea_formatter.py | 173 +++++++++++++++++-- prompt_builder.py | 25 +++ 7 files changed, 206 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 0bce9d7..fe43c22 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,10 @@ Options: neutral with no connected camera config, and uses `SxCP Camera Control` when one is connected. - `camera_detail`: `off`, `compact`, or `full` for the pair prompt camera text. +- `hardcore_detail_density`: `compact` keeps the Krea hardcore rewrite mostly + to the position/action sentence, `balanced` keeps one useful non-duplicated + motion or aftermath detail, and `dense` keeps more detail after dedupe. This + is separate from expression intensity. ## Built-In Categories diff --git a/__init__.py b/__init__.py index 903e18d..684d392 100644 --- a/__init__.py +++ b/__init__.py @@ -41,6 +41,7 @@ try: character_woman_body_choices, ethnicity_choices, generation_profile_choices, + hardcore_detail_density_choices, load_character_profile_json, seed_mode_choices, subcategory_choices, @@ -85,6 +86,7 @@ except ImportError: character_woman_body_choices, ethnicity_choices, generation_profile_choices, + hardcore_detail_density_choices, load_character_profile_json, seed_mode_choices, subcategory_choices, @@ -1040,6 +1042,7 @@ class SxCPInstaOFOptions: "softcore_camera_mode": (camera_mode_choices(), {"default": "handheld_selfie"}), "hardcore_camera_mode": (["from_camera_config", "same_as_softcore"] + camera_mode_choices(), {"default": "from_camera_config"}), "camera_detail": (camera_detail_choices(), {"default": "compact"}), + "hardcore_detail_density": (hardcore_detail_density_choices(), {"default": "balanced"}), } } @@ -1066,6 +1069,7 @@ class SxCPInstaOFOptions: softcore_camera_mode, hardcore_camera_mode, camera_detail, + hardcore_detail_density, ): return ( build_insta_of_options_json( @@ -1085,6 +1089,7 @@ class SxCPInstaOFOptions: softcore_camera_mode=softcore_camera_mode, hardcore_camera_mode=hardcore_camera_mode, camera_detail=camera_detail, + hardcore_detail_density=hardcore_detail_density, ), ) diff --git a/categories/expression_composition_pools.json b/categories/expression_composition_pools.json index 458e4b6..267c9d1 100644 --- a/categories/expression_composition_pools.json +++ b/categories/expression_composition_pools.json @@ -516,15 +516,15 @@ {"text": "full-room group-sex composition with clear adult spacing", "min_people": 4} ], "climax_compositions": [ - {"text": "tight post-ejaculation crop on face, body, hands, and visible fluids", "min_people": 1}, - {"text": "direct-flash close-up of orgasm aftermath", "min_people": 1}, + {"text": "tight post-ejaculation crop with body position, hands, and visible fluids readable", "min_people": 1}, + {"text": "direct-flash close-up of aftermath with the body position still readable", "min_people": 1}, {"text": "bed-level ejaculation frame with thighs, face, and fluid detail visible", "min_people": 1}, {"text": "mirror-reflected post-ejaculation composition", "min_people": 1}, {"text": "front-facing cumshot composition with body and expression centered", "min_people": 1}, {"text": "overhead post-orgasm frame on rumpled sheets", "min_people": 1}, {"text": "kneeling ejaculation frame with open mouth and body visible", "min_people": 1}, {"text": "wide aftermath composition with all adult bodies visible", "min_people": 2}, - {"text": "tight subscriber-view crop of orgasm expression and fluid detail", "min_people": 1}, + {"text": "tight subscriber-view crop with body position and fluid detail readable", "min_people": 1}, {"text": "side-profile post-ejaculation body-line composition", "min_people": 1} ] } diff --git a/categories/sexual_poses.json b/categories/sexual_poses.json index 251e2b7..d6ec824 100644 --- a/categories/sexual_poses.json +++ b/categories/sexual_poses.json @@ -967,10 +967,10 @@ "item_templates": [ "{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", "{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}", - "{angle} ejaculation view featuring {climax_act}, {body_position}, {body_contact}, {fluid_detail}, and {visibility}", + "{angle} aftermath view with {body_position}, {body_contact}, and {visibility}", "hardcore post-ejaculation scene with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", "{climax_act} on {surface}, with {body_position}, {body_contact}, {hand_detail}, and {fluid_detail}", - "{angle} view of {fluid_location}, {body_position}, {climax_act}, and {visibility}", + "{angle} aftermath view of {body_position}, with {fluid_location} and {visibility}", "explicit orgasm scene: {climax_act}, {body_position}, {fluid_detail}, {expression_detail}, and {body_contact}", "{body_position} with {fluid_location}, {hand_detail}, {visibility}, and {climax_act}" ], @@ -1137,8 +1137,8 @@ "post-ejaculation fluids anatomically clear", "face and body covered in visible cum", "open thighs and wetness visible", - "explicit semen aftermath visible", - "hardcore ejaculation detail visible", + "aftermath detail visible", + "ejaculation detail visible", "sexual fluids and body contact visible" ] } diff --git a/examples/default_task_lanes_workflow.json b/examples/default_task_lanes_workflow.json index 3cecd84..9a61859 100644 --- a/examples/default_task_lanes_workflow.json +++ b/examples/default_task_lanes_workflow.json @@ -210,7 +210,7 @@ "id": 12, "type": "SxCPInstaOFOptions", "pos": [-1220, 290], - "size": [360, 366], + "size": [360, 390], "flags": {}, "order": 11, "mode": 0, @@ -219,7 +219,7 @@ {"name": "options_json", "type": "STRING", "links": [10], "slot_index": 0} ], "properties": {"Node name for S&R": "SxCPInstaOFOptions"}, - "widgets_values": ["same_as_hardcore", "couple", 1, 1, "lingerie_tease", "hardcore", true, true, 0.45, 0.85, "hybrid", "same_creator_same_room", "partially_removed", "handheld_selfie", "from_camera_config", "compact"] + "widgets_values": ["same_as_hardcore", "couple", 1, 1, "lingerie_tease", "hardcore", true, true, 0.45, 0.85, "hybrid", "same_creator_same_room", "partially_removed", "handheld_selfie", "from_camera_config", "compact", "balanced"] }, { "id": 13, diff --git a/krea_formatter.py b/krea_formatter.py index 8c98606..3a4a143 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -9,6 +9,7 @@ TRIGGER_CANDIDATES = ( "sxcpinup_coloredpencil", "sxcppnl7", ) +HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"} PROMPT_FIELD_LABELS = ( "Ages", @@ -52,6 +53,11 @@ def _expression_disabled(row: dict[str, Any]) -> bool: return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True)) +def _normalize_hardcore_detail_density(value: Any) -> str: + text = _clean(value).lower() + return text if text in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced" + + def _sentence(text: str) -> str: text = _clean(text).strip(" ,;") if not text: @@ -747,6 +753,112 @@ def _dedupe_hardcore_detail(detail: str, anchor: str) -> str: return _clean(detail).strip(" ,;") +def _detail_clauses(detail: str) -> list[str]: + return [part.strip(" ,;") for part in re.split(r",\s*(?:and\s+)?", _clean(detail)) if part.strip(" ,;")] + + +def _join_detail_clauses(clauses: list[str]) -> str: + cleaned: list[str] = [] + seen: set[str] = set() + for clause in clauses: + clause = _clean(clause).strip(" ,;") + key = clause.lower() + if clause and key not in seen: + cleaned.append(clause) + seen.add(key) + return ", ".join(cleaned) + + +def _action_position_phrase(action: str) -> str: + action = _clean(action).lower() + if "face-down" in action and "ass raised" in action: + return "face-down raised-hip position" + if "on all fours" in action: + return "all-fours raised-hip position" + if "bends forward" in action or "bent forward" in action: + return "bent-over raised-hip position" + if "lies on her back" in action and ("thighs open" in action or "legs open" in action): + return "open-thigh reclined position" + if "lies at the bed edge" in action or "bed edge" in action: + return "bed-edge position" + if "lies on her side" in action: + return "side-lying position" + if "kneels in front" in action: + return "kneeling-at-hip-height position" + if "straddles" in action or "squats over" in action: + return "straddling position" + if "sits in the man's lap" in action: + return "lap-straddling position" + if "stands braced" in action: + return "standing braced position" + if "held between" in action or "front-and-back" in action: + return "front-and-back position" + if "lies between" in action: + return "between-partners position" + return "" + + +def _normalize_climax_view_clause(clause: str, role_graph: str) -> str: + lower = clause.lower() + if "view" not in lower and "frame" not in lower: + return clause + angle_match = re.search( + r"\b(front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\b", + lower, + ) + if not angle_match: + return clause + angle = angle_match.group(1) + if angle == "wide": + angle = "wide full-body" + position = _action_position_phrase(role_graph) + if position: + return f"{angle} aftermath view with the {position} readable" + return f"{angle} aftermath view" + + +def _climax_clause_duplicates_role(clause: str, role_graph: str) -> bool: + clause_lower = clause.lower() + role_lower = role_graph.lower() + role_has_ejaculation = any(token in role_lower for token in ("ejaculates semen", "visible semen", "semen lands")) + if role_has_ejaculation and re.search( + r"\b(?:cum clearly visible|explicit semen aftermath visible|hardcore ejaculation detail visible|" + r"post-ejaculation fluids anatomically clear|sexual fluids and body contact visible|" + r"visible external ejaculation|hardcore ejaculation scene|visible orgasm aftermath)\b", + clause_lower, + ): + return True + duplicate_pairs = ( + (("lower back", "ass"), ("lower back", "ass")), + (("ass",), ("ass",)), + (("pussy", "thigh"), ("pussy", "thigh")), + (("face", "lips"), ("face", "lips")), + (("tongue", "chin"), ("face", "lips", "mouth", "tongue")), + (("breast",), ("breast", "chest")), + (("belly",), ("belly", "torso")), + (("body",), ("body",)), + ) + if any(token in clause_lower for token in ("cum", "semen", "fluid")): + for clause_tokens, role_tokens in duplicate_pairs: + if any(token in clause_lower for token in clause_tokens) and any(token in role_lower for token in role_tokens): + return True + return False + + +def _limit_detail_for_density(detail: str, density: str, is_climax: bool) -> str: + density = _normalize_hardcore_detail_density(density) + if density == "compact": + return "" + clauses = _detail_clauses(detail) + if not clauses: + return "" + if density == "balanced": + limit = 1 if is_climax else 2 + else: + limit = 3 if is_climax else 4 + return _join_detail_clauses(clauses[:limit]) + + def _is_climax_text(*parts: str) -> bool: text = " ".join(_clean(part).lower() for part in parts if _clean(part)) return any( @@ -790,7 +902,7 @@ def _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs" if "on all fours with hips raised" in text: return "the woman is on all fours with hips raised while the man is positioned behind her and ejaculates semen across her ass, thighs, and lower back" - if "face-down ass-up" in text: + if "face-down ass-up" in text or "lies face-down" in text or "face down" in text: return "the woman lies face-down with ass raised while the man is positioned behind her and ejaculates semen across her lower back and ass" if "bent over with ass raised" in text or "bent over" in text: return "the woman bends forward with hips raised while the man stands behind her with visible semen across her lower back, ass, and thighs" @@ -823,7 +935,7 @@ def _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her body" -def _dedupe_climax_detail(detail: str, role_graph: str) -> str: +def _dedupe_climax_detail(detail: str, role_graph: str, density: str = "balanced") -> str: detail = _clean(detail) lower = role_graph.lower() patterns: list[str] = [] @@ -870,10 +982,25 @@ def _dedupe_climax_detail(detail: str, role_graph: str) -> str: detail = re.sub(r"\bwith\s*,\s*", "with ", detail, flags=re.IGNORECASE) detail = re.sub(r"^with\s+", "", detail, flags=re.IGNORECASE) detail = re.sub(r"^and\s+", "", detail, flags=re.IGNORECASE) - return _clean(detail).strip(" ,;") + clauses: list[str] = [] + for clause in _detail_clauses(detail): + normalized = _normalize_climax_view_clause(clause, role_graph) + if _climax_clause_duplicates_role(normalized, role_graph): + continue + if density != "dense" and normalized.lower() in ("orgasm during penetration", "post-orgasm visible release"): + continue + clauses.append(normalized) + return _limit_detail_for_density(_join_detail_clauses(clauses), density, True) -def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str: +def _hardcore_action_sentence( + role_graph: str, + hard_item: str, + composition: str = "", + axis_values: Any = None, + detail_density: str = "balanced", +) -> str: + detail_density = _normalize_hardcore_detail_density(detail_density) role_graph = _clean(role_graph).rstrip(".") hard_item = _clean(hard_item).rstrip(".") role_graph = re.sub( @@ -938,9 +1065,10 @@ def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str ) if is_climax: anchor = "" - detail = _dedupe_climax_detail(detail, role_graph) + detail = _dedupe_climax_detail(detail, role_graph, detail_density) else: detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail + detail = _limit_detail_for_density(detail, detail_density, False) arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values) anchor_phrase = _with_indefinite_article(anchor) if anchor else "" if arrangement and anchor_phrase: @@ -960,12 +1088,18 @@ def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str return sentence -def _composition_phrase(composition: Any, action: str = "", prefix: str = "framed as") -> str: +def _composition_phrase( + composition: Any, + action: str = "", + prefix: str = "framed as", + detail_density: str = "balanced", +) -> str: composition = _clean(composition) if not composition: return "" action_lower = _clean(action).lower() composition_lower = composition.lower() + detail_density = _normalize_hardcore_detail_density(detail_density) oral_pose_tokens = ( "kneeling oral", "side-lying oral", @@ -983,6 +1117,15 @@ def _composition_phrase(composition: Any, action: str = "", prefix: str = "frame if composition_oral_tokens and not any(token in action_lower for token in composition_oral_tokens): match = re.search(r"\bwith\s+(.+)$", composition, flags=re.IGNORECASE) return f"framed with {match.group(1)}" if match else "" + position = _action_position_phrase(action) + close_or_aftermath = any( + token in composition_lower + for token in ("close-up", "close crop", "tight", "direct-flash", "subscriber-view", "post-ejaculation", "aftermath") + ) + if position and close_or_aftermath: + if detail_density == "compact": + return f"{prefix} {composition}, with the {position} still readable" + return f"{prefix} {composition}, keeping the {position} and action geography readable" return f"{prefix} {composition}" @@ -1140,7 +1283,8 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) role_graph = _natural_label_text(role_graph, cast_labels) item = _natural_label_text(item, cast_labels) axis_values = row.get("item_axis_values") if isinstance(row.get("item_axis_values"), dict) else {} - action = _hardcore_action_sentence(role_graph, item, composition, axis_values) + detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density")) + action = _hardcore_action_sentence(role_graph, item, composition, axis_values, detail_density) parts = [ action, cast_prose, @@ -1148,7 +1292,7 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", f"The setting is {scene}" if scene else "", _expression_phrase(expression), - _composition_phrase(composition, action, "The image is framed as"), + _composition_phrase(composition, action, "The image is framed as", detail_density), camera, style if detail_level != "concise" else "", ] @@ -1235,7 +1379,16 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) hard_item = _natural_label_text(hard_item, hard_labels) hard_role_graph = _natural_label_text(hard_role_graph, hard_labels) hard_axis_values = hard.get("item_axis_values") if isinstance(hard.get("item_axis_values"), dict) else {} - hard_action = _hardcore_action_sentence(hard_role_graph, hard_item, hard_composition, hard_axis_values) + hard_detail_density = _normalize_hardcore_detail_density( + hard.get("hardcore_detail_density") or row.get("hardcore_detail_density") or options.get("hardcore_detail_density") + ) + hard_action = _hardcore_action_sentence( + hard_role_graph, + hard_item, + hard_composition, + hard_axis_values, + hard_detail_density, + ) same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" soft_cast_presence = ( f"{_label_join(soft_labels)} are together in a non-explicit teaser pose, with no sex act or genital contact" @@ -1284,7 +1437,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) hard_cast_prose, f"set in {hard_scene}" if hard_scene else "", _expression_phrase(hard_expression), - _composition_phrase(hard_composition, hard_action), + _composition_phrase(hard_composition, hard_action, detail_density=hard_detail_density), hard_camera, hard_style if detail_level != "concise" else "", ] diff --git a/prompt_builder.py b/prompt_builder.py index fcb6511..a6911eb 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -178,6 +178,7 @@ CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "min CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"} CAMERA_DETAIL_CHOICES = ["off", "compact", "full"] +HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"] GENERIC_POSITIVE_SUFFIX = ( "Use crisp clean comic linework, detailed hatching, soft blended shading, " @@ -1440,6 +1441,10 @@ def camera_detail_choices() -> list[str]: return list(CAMERA_DETAIL_CHOICES) +def hardcore_detail_density_choices() -> list[str]: + return list(HARDCORE_DETAIL_DENSITY_CHOICES) + + def camera_shot_choices() -> list[str]: return list(CAMERA_SHOT_PROMPTS) @@ -3785,7 +3790,11 @@ def build_insta_of_options_json( hardcore_expression_intensity: float = 0.85, softcore_expression_enabled: bool = True, hardcore_expression_enabled: bool = True, + hardcore_detail_density: str = "balanced", ) -> str: + hardcore_detail_density = ( + hardcore_detail_density if hardcore_detail_density in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced" + ) return json.dumps( { "softcore_cast": softcore_cast, @@ -3804,6 +3813,7 @@ def build_insta_of_options_json( "hardcore_expression_enabled": not _is_false(hardcore_expression_enabled), "softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45), "hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85), + "hardcore_detail_density": hardcore_detail_density, }, ensure_ascii=True, sort_keys=True, @@ -3828,6 +3838,7 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s "hardcore_expression_enabled": True, "softcore_expression_intensity": 0.45, "hardcore_expression_intensity": 0.85, + "hardcore_detail_density": "balanced", } if not options_json: return defaults @@ -3869,6 +3880,11 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s parsed.get("hardcore_expression_intensity"), defaults["hardcore_expression_intensity"], ) + parsed["hardcore_detail_density"] = ( + parsed["hardcore_detail_density"] + if parsed.get("hardcore_detail_density") in HARDCORE_DETAIL_DENSITY_CHOICES + else defaults["hardcore_detail_density"] + ) for key in ("hardcore_women_count", "hardcore_men_count"): try: parsed[key] = max(0, min(12, int(parsed[key]))) @@ -4153,6 +4169,7 @@ def build_insta_of_pair( expression_intensity=options["hardcore_expression_intensity"], character_cast=character_cast or "", ) + hard_row["hardcore_detail_density"] = options["hardcore_detail_density"] descriptor = _insta_of_descriptor(soft_row) cast_descriptors = _insta_of_cast_descriptors( @@ -4220,6 +4237,12 @@ def build_insta_of_pair( options["hardcore_clothing_continuity"], soft_row["item"], ) + hard_detail_density = options["hardcore_detail_density"] + hard_detail_directive = { + "compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ", + "balanced": "", + "dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ", + }[hard_detail_density] soft_descriptor_sentence = ( f"Cast descriptors: {soft_cast_descriptor_text}. " if options["softcore_cast"] == "same_as_hardcore" @@ -4249,6 +4272,7 @@ def build_insta_of_pair( f"Setting: {hard_scene}. " f"{_labeled_expression_sentence('Facial expressions', hard_row.get('expression'))}" f"Composition: {hard_composition}. " + f"{hard_detail_directive}" f"{hard_camera_sentence}" f"{hard_row['positive_suffix']}." ) @@ -4294,6 +4318,7 @@ def build_insta_of_pair( "shared_cast_descriptors": cast_descriptors, "softcore_partner_styling": soft_partner_styling, "hardcore_clothing_state": hard_clothing_state, + "hardcore_detail_density": hard_detail_density, "softcore_prompt": soft_prompt, "hardcore_prompt": hard_prompt, "softcore_negative_prompt": soft_negative,