Compare commits
4 Commits
4f0203fc3d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cff1a248ec | |||
| b3cd8d77a1 | |||
| c768b37399 | |||
| a392ab9779 |
@@ -349,15 +349,13 @@ resolutions and outputs `width`, `height`, and a `resolution` string. Use
|
|||||||
position inside the filtered pool. Connect `SxCP Global Seed` or Seed Locker's
|
position inside the filtered pool. Connect `SxCP Global Seed` or Seed Locker's
|
||||||
`seed_config` when the bucket choice must be reproducible.
|
`seed_config` when the bucket choice must be reproducible.
|
||||||
|
|
||||||
`SxCP Krea2 Resolution Selector` outputs model-friendly `width`, `height`,
|
`SxCP Krea2 Resolution Selector` is a simple size picker: choose `megapixels`
|
||||||
`resolution`, aspect ratio, megapixel, and hosted API fields for Krea2. Use
|
and `aspect_ratio`, then use the output `width` and `height`. It assumes local
|
||||||
`profile=turbo_local_2k` for local Turbo generation up to a 2K long edge, or
|
Krea2 Turbo 2K limits, searches for the best multiple-of-16 size, and clamps the
|
||||||
`profile=raw_local_1k` for RAW/local 1K limits. `profile=api_hosted_1k` keeps
|
selected megapixel target when that aspect ratio cannot fit it. `max_for_aspect`
|
||||||
the official API fields visible: `api_aspect_ratio` and `api_resolution=1K`.
|
returns the largest valid size for the chosen ratio. The official Krea API
|
||||||
The node searches for the best multiple-of-16 size for the selected aspect ratio
|
aspect ratios are listed first, with a few local helper ratios such as `8:9`
|
||||||
and megapixel preset, then clamps to the selected profile's limit. Use
|
after them.
|
||||||
`max_for_aspect` to get the highest valid size for that aspect ratio, or
|
|
||||||
`random_1_to_limit` with a seed config for reproducible random sizes.
|
|
||||||
|
|
||||||
`SxCP Camera Control` and `SxCP Camera Orbit Control` output `camera_config`,
|
`SxCP Camera Control` and `SxCP Camera Orbit Control` output `camera_config`,
|
||||||
which can be connected to the prompt builder or the Insta/OF pair node. They
|
which can be connected to the prompt builder or the Insta/OF pair node. They
|
||||||
|
|||||||
+22
-98
@@ -45,10 +45,8 @@ SDXL_BUCKET_RESOLUTIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
KREA2_API_ASPECT_RATIOS = ["1:1", "4:3", "3:2", "16:9", "2.35:1", "4:5", "2:3", "9:16"]
|
KREA2_API_ASPECT_RATIOS = ["1:1", "4:3", "3:2", "16:9", "2.35:1", "4:5", "2:3", "9:16"]
|
||||||
KREA2_ASPECT_RATIOS = KREA2_API_ASPECT_RATIOS + ["21:9", "9:21", "3:1", "1:3", "custom", "random_api"]
|
KREA2_ASPECT_RATIOS = KREA2_API_ASPECT_RATIOS + ["8:9", "21:9", "9:21", "3:1", "1:3"]
|
||||||
KREA2_MEGAPIXEL_PRESETS = [
|
KREA2_MEGAPIXEL_PRESETS = [
|
||||||
"max_for_aspect",
|
|
||||||
"random_1_to_limit",
|
|
||||||
"1.0MP",
|
"1.0MP",
|
||||||
"1.25MP",
|
"1.25MP",
|
||||||
"1.5MP",
|
"1.5MP",
|
||||||
@@ -62,7 +60,7 @@ KREA2_MEGAPIXEL_PRESETS = [
|
|||||||
"3.5MP",
|
"3.5MP",
|
||||||
"3.75MP",
|
"3.75MP",
|
||||||
"4.0MP",
|
"4.0MP",
|
||||||
"custom",
|
"max_for_aspect",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -259,15 +257,8 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"seed_config": "Optional seed config. The composition seed controls bucket choice, so Seed Locker can keep sizes fixed while rerolling pose/person.",
|
"seed_config": "Optional seed config. The composition seed controls bucket choice, so Seed Locker can keep sizes fixed while rerolling pose/person.",
|
||||||
},
|
},
|
||||||
"SxCPKrea2ResolutionSelector": {
|
"SxCPKrea2ResolutionSelector": {
|
||||||
"profile": "api_hosted_1k returns official API fields; raw_local_1k and turbo_local_2k return explicit ComfyUI width/height under known local limits.",
|
"megapixels": "Target megapixel preset. If it cannot fit the aspect ratio under the 2K Krea2 Turbo limit, the node clamps to the maximum valid size.",
|
||||||
"aspect_ratio": "Official Krea API ratios are listed first. custom uses custom_aspect_width/custom_aspect_height; random_api chooses from official ratios.",
|
"aspect_ratio": "Krea API ratios are listed first; local-only helper ratios like 8:9 are included after them.",
|
||||||
"megapixel_preset": "Pick a target megapixel count, max_for_aspect, random_1_to_limit, or custom_megapixels.",
|
|
||||||
"round_to": "Krea2 local inference pads to multiples of 16; 32/64 are stricter bucket-friendly choices.",
|
|
||||||
"custom_max_long_edge": "Only used by custom_limit. Local Turbo documentation uses 2048 as the 2K upper edge.",
|
|
||||||
"custom_max_megapixels": "Only used by custom_limit. The selector still respects custom_max_long_edge.",
|
|
||||||
"seed": "Used only for random_api or random_1_to_limit. Connect Seed Config for deterministic workflow-wide randomness.",
|
|
||||||
"row_number": "Deterministic row offset for random ratio/megapixel choices.",
|
|
||||||
"seed_config": "Optional seed config. The composition seed controls resolution choices when connected.",
|
|
||||||
},
|
},
|
||||||
"SxCPCameraControl": {
|
"SxCPCameraControl": {
|
||||||
"camera_mode": "Camera style preset. Use from_camera_config in Insta/OF options to consume this.",
|
"camera_mode": "Camera style preset. Use from_camera_config in Insta/OF options to consume this.",
|
||||||
@@ -299,6 +290,8 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"allow_double": "Allow double-penetration or second-contact wording.",
|
"allow_double": "Allow double-penetration or second-contact wording.",
|
||||||
"allow_penetration": "Allow vaginal/penetrative sex subcategories.",
|
"allow_penetration": "Allow vaginal/penetrative sex subcategories.",
|
||||||
"allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.",
|
"allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.",
|
||||||
|
"allow_interaction": "Allow non-act interaction pools such as body worship, clothing transitions, guidance, camera presentation, watching, and aftercare.",
|
||||||
|
"allow_manual": "Allow manual stimulation pools such as fingering, clit rubbing, and mutual masturbation.",
|
||||||
"allow_oral": "Allow oral sex subcategories.",
|
"allow_oral": "Allow oral sex subcategories.",
|
||||||
"allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.",
|
"allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.",
|
||||||
"allow_anal": "Allow anal subcategories.",
|
"allow_anal": "Allow anal subcategories.",
|
||||||
@@ -1039,23 +1032,8 @@ class SxCPKrea2ResolutionSelector:
|
|||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"profile": (
|
"megapixels": (KREA2_MEGAPIXEL_PRESETS, {"default": "1.0MP"}),
|
||||||
["turbo_local_2k", "raw_local_1k", "api_hosted_1k", "custom_limit"],
|
|
||||||
{"default": "turbo_local_2k"},
|
|
||||||
),
|
|
||||||
"aspect_ratio": (KREA2_ASPECT_RATIOS, {"default": "1:1"}),
|
"aspect_ratio": (KREA2_ASPECT_RATIOS, {"default": "1:1"}),
|
||||||
"megapixel_preset": (KREA2_MEGAPIXEL_PRESETS, {"default": "1.0MP"}),
|
|
||||||
"custom_megapixels": ("FLOAT", {"default": 1.0, "min": 0.10, "max": 16.0, "step": 0.05}),
|
|
||||||
"round_to": (["16", "32", "64"], {"default": "16"}),
|
|
||||||
"custom_aspect_width": ("FLOAT", {"default": 1.0, "min": 0.10, "max": 100.0, "step": 0.05}),
|
|
||||||
"custom_aspect_height": ("FLOAT", {"default": 1.0, "min": 0.10, "max": 100.0, "step": 0.05}),
|
|
||||||
"custom_max_long_edge": ("INT", {"default": 2048, "min": 256, "max": 8192, "step": 16}),
|
|
||||||
"custom_max_megapixels": ("FLOAT", {"default": 4.20, "min": 0.10, "max": 64.0, "step": 0.05}),
|
|
||||||
"seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}),
|
|
||||||
"row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}),
|
|
||||||
},
|
|
||||||
"optional": {
|
|
||||||
"seed_config": (SXCP_SEED_CONFIG,),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,10 +1054,6 @@ class SxCPKrea2ResolutionSelector:
|
|||||||
FUNCTION = "select"
|
FUNCTION = "select"
|
||||||
CATEGORY = "prompt_builder/util"
|
CATEGORY = "prompt_builder/util"
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _configured_seed(seed_config):
|
|
||||||
return SxCPSDXLBucketSize._configured_bucket_seed(seed_config)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _aspect_value(aspect_ratio, custom_aspect_width, custom_aspect_height, rng):
|
def _aspect_value(aspect_ratio, custom_aspect_width, custom_aspect_height, rng):
|
||||||
selected = str(aspect_ratio or "1:1").strip()
|
selected = str(aspect_ratio or "1:1").strip()
|
||||||
@@ -1189,56 +1163,11 @@ class SxCPKrea2ResolutionSelector:
|
|||||||
return 1.0
|
return 1.0
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
def select(self, megapixels, aspect_ratio):
|
||||||
def IS_CHANGED(cls, *args, **kwargs):
|
multiple = 16
|
||||||
aspect_ratio = kwargs.get("aspect_ratio")
|
profile = "turbo_local_2k"
|
||||||
if aspect_ratio is None and len(args) > 1:
|
max_long_edge, max_profile_mp, _profile_label = self._profile_limits(profile, 2048, 4.20)
|
||||||
aspect_ratio = args[1]
|
resolved_aspect, ratio = self._aspect_value(aspect_ratio, 1.0, 1.0, random.Random("krea2_resolution"))
|
||||||
megapixel_preset = kwargs.get("megapixel_preset")
|
|
||||||
if megapixel_preset is None and len(args) > 2:
|
|
||||||
megapixel_preset = args[2]
|
|
||||||
seed_value = kwargs.get("seed")
|
|
||||||
if seed_value is None and len(args) > 9:
|
|
||||||
seed_value = args[9]
|
|
||||||
seed_config = kwargs.get("seed_config", "")
|
|
||||||
if not seed_config and len(args) > 11:
|
|
||||||
seed_config = args[11]
|
|
||||||
try:
|
|
||||||
seed = int(seed_value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
seed = -1
|
|
||||||
uses_random = str(aspect_ratio) == "random_api" or str(megapixel_preset) == "random_1_to_limit"
|
|
||||||
if uses_random and seed < 0 and cls._configured_seed(seed_config) is None:
|
|
||||||
return random.random()
|
|
||||||
return tuple(args), tuple(sorted(kwargs.items()))
|
|
||||||
|
|
||||||
def select(
|
|
||||||
self,
|
|
||||||
profile,
|
|
||||||
aspect_ratio,
|
|
||||||
megapixel_preset,
|
|
||||||
custom_megapixels,
|
|
||||||
round_to,
|
|
||||||
custom_aspect_width,
|
|
||||||
custom_aspect_height,
|
|
||||||
custom_max_long_edge,
|
|
||||||
custom_max_megapixels,
|
|
||||||
seed,
|
|
||||||
row_number,
|
|
||||||
seed_config="",
|
|
||||||
):
|
|
||||||
configured_seed = self._configured_seed(seed_config)
|
|
||||||
if configured_seed is None and int(seed) < 0:
|
|
||||||
rng = random.Random(random.getrandbits(64))
|
|
||||||
seed_label = "fresh"
|
|
||||||
else:
|
|
||||||
selected_seed = configured_seed if configured_seed is not None else int(seed)
|
|
||||||
rng = random.Random(f"krea2_resolution:{selected_seed}:{int(row_number)}")
|
|
||||||
seed_label = str(selected_seed)
|
|
||||||
|
|
||||||
multiple = int(round_to) if str(round_to).isdigit() else 16
|
|
||||||
max_long_edge, max_profile_mp, profile_label = self._profile_limits(profile, custom_max_long_edge, custom_max_megapixels)
|
|
||||||
resolved_aspect, ratio = self._aspect_value(aspect_ratio, custom_aspect_width, custom_aspect_height, rng)
|
|
||||||
api_aspect_ratio = resolved_aspect if resolved_aspect in KREA2_API_ASPECT_RATIOS else self._closest_api_aspect(ratio)
|
api_aspect_ratio = resolved_aspect if resolved_aspect in KREA2_API_ASPECT_RATIOS else self._closest_api_aspect(ratio)
|
||||||
|
|
||||||
continuous_max_mp = self._continuous_limit_mp(ratio, max_long_edge, max_profile_mp)
|
continuous_max_mp = self._continuous_limit_mp(ratio, max_long_edge, max_profile_mp)
|
||||||
@@ -1246,19 +1175,10 @@ class SxCPKrea2ResolutionSelector:
|
|||||||
ratio, continuous_max_mp, max_long_edge, max_profile_mp, multiple
|
ratio, continuous_max_mp, max_long_edge, max_profile_mp, multiple
|
||||||
)
|
)
|
||||||
|
|
||||||
preset = str(megapixel_preset or "1.0MP").strip()
|
preset = str(megapixels or "1.0MP").strip()
|
||||||
target_mp = self._preset_megapixels(preset)
|
target_mp = self._preset_megapixels(preset)
|
||||||
if preset == "custom":
|
if preset == "max_for_aspect":
|
||||||
target_mp = max(0.10, float(custom_megapixels))
|
|
||||||
elif preset == "max_for_aspect":
|
|
||||||
target_mp = max_actual_mp
|
target_mp = max_actual_mp
|
||||||
elif preset == "random_1_to_limit":
|
|
||||||
available = [self._preset_megapixels(value) for value in KREA2_MEGAPIXEL_PRESETS if value.endswith("MP")]
|
|
||||||
available = [value for value in available if value is not None and 1.0 <= value <= max_actual_mp + 0.001]
|
|
||||||
if not available:
|
|
||||||
target_mp = max_actual_mp
|
|
||||||
else:
|
|
||||||
target_mp = rng.choice(available)
|
|
||||||
if target_mp is None:
|
if target_mp is None:
|
||||||
target_mp = 1.0
|
target_mp = 1.0
|
||||||
|
|
||||||
@@ -1276,17 +1196,15 @@ class SxCPKrea2ResolutionSelector:
|
|||||||
resolution = f"{width}x{height}"
|
resolution = f"{width}x{height}"
|
||||||
api_resolution = "1K"
|
api_resolution = "1K"
|
||||||
summary_parts = [
|
summary_parts = [
|
||||||
f"{profile_label}",
|
|
||||||
f"{resolution}",
|
f"{resolution}",
|
||||||
f"{actual_mp:.2f} MP",
|
f"{actual_mp:.2f} MP",
|
||||||
f"aspect {resolved_aspect} ({actual_ratio:.3f})",
|
f"aspect {resolved_aspect} ({actual_ratio:.3f})",
|
||||||
f"max for aspect {max_width}x{max_height} / {max_actual_mp:.2f} MP",
|
f"max for aspect {max_width}x{max_height} / {max_actual_mp:.2f} MP",
|
||||||
f"API {api_aspect_ratio} {api_resolution}",
|
f"Krea2 Turbo 2K",
|
||||||
|
f"API equivalent {api_aspect_ratio} {api_resolution}",
|
||||||
]
|
]
|
||||||
if clamped:
|
if clamped:
|
||||||
summary_parts.append(f"target {target_mp:.2f} MP clamped to aspect/profile limit")
|
summary_parts.append(f"target {target_mp:.2f} MP clamped to aspect/profile limit")
|
||||||
if preset.startswith("random") or resolved_aspect == "random_api":
|
|
||||||
summary_parts.append(f"seed {seed_label}, row {int(row_number)}")
|
|
||||||
summary = "; ".join(summary_parts)
|
summary = "; ".join(summary_parts)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
@@ -2262,6 +2180,8 @@ class SxCPHardcoreActionFilter:
|
|||||||
"allow_double": ("BOOLEAN", {"default": False}),
|
"allow_double": ("BOOLEAN", {"default": False}),
|
||||||
"allow_penetration": ("BOOLEAN", {"default": True}),
|
"allow_penetration": ("BOOLEAN", {"default": True}),
|
||||||
"allow_foreplay": ("BOOLEAN", {"default": True}),
|
"allow_foreplay": ("BOOLEAN", {"default": True}),
|
||||||
|
"allow_interaction": ("BOOLEAN", {"default": True}),
|
||||||
|
"allow_manual": ("BOOLEAN", {"default": True}),
|
||||||
"allow_oral": ("BOOLEAN", {"default": True}),
|
"allow_oral": ("BOOLEAN", {"default": True}),
|
||||||
"allow_outercourse": ("BOOLEAN", {"default": True}),
|
"allow_outercourse": ("BOOLEAN", {"default": True}),
|
||||||
"allow_anal": ("BOOLEAN", {"default": True}),
|
"allow_anal": ("BOOLEAN", {"default": True}),
|
||||||
@@ -2284,6 +2204,8 @@ class SxCPHardcoreActionFilter:
|
|||||||
allow_double,
|
allow_double,
|
||||||
allow_penetration,
|
allow_penetration,
|
||||||
allow_foreplay,
|
allow_foreplay,
|
||||||
|
allow_interaction,
|
||||||
|
allow_manual,
|
||||||
allow_oral,
|
allow_oral,
|
||||||
allow_outercourse,
|
allow_outercourse,
|
||||||
allow_anal,
|
allow_anal,
|
||||||
@@ -2297,6 +2219,8 @@ class SxCPHardcoreActionFilter:
|
|||||||
allow_double=allow_double,
|
allow_double=allow_double,
|
||||||
allow_penetration=allow_penetration,
|
allow_penetration=allow_penetration,
|
||||||
allow_foreplay=allow_foreplay,
|
allow_foreplay=allow_foreplay,
|
||||||
|
allow_interaction=allow_interaction,
|
||||||
|
allow_manual=allow_manual,
|
||||||
allow_oral=allow_oral,
|
allow_oral=allow_oral,
|
||||||
allow_outercourse=allow_outercourse,
|
allow_outercourse=allow_outercourse,
|
||||||
allow_anal=allow_anal,
|
allow_anal=allow_anal,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .prompt_hygiene import sanitize_prose_text
|
||||||
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
from prompt_hygiene import sanitize_prose_text
|
||||||
|
|
||||||
|
|
||||||
OLD_TRIGGER = "sxcpinup_coloredpencil"
|
OLD_TRIGGER = "sxcpinup_coloredpencil"
|
||||||
DEFAULT_TRIGGER = "sxcppnl7"
|
DEFAULT_TRIGGER = "sxcppnl7"
|
||||||
@@ -724,6 +729,8 @@ def naturalize_caption(
|
|||||||
row, row_method = _row_from_inputs(source_text, metadata_json, input_hint)
|
row, row_method = _row_from_inputs(source_text, metadata_json, input_hint)
|
||||||
if row is not None:
|
if row is not None:
|
||||||
prose, method = _metadata_to_prose(row, detail_level, keep_style)
|
prose, method = _metadata_to_prose(row, detail_level, keep_style)
|
||||||
return _with_trigger(prose, trigger, include_trigger), f"{row_method}:{method}"
|
caption = sanitize_prose_text(_with_trigger(prose, trigger, include_trigger), triggers=(trigger,))
|
||||||
|
return caption, f"{row_method}:{method}"
|
||||||
prose, method = _text_to_prose(source_text, detail_level, keep_style)
|
prose, method = _text_to_prose(source_text, detail_level, keep_style)
|
||||||
return _with_trigger(prose, trigger, include_trigger), method
|
caption = sanitize_prose_text(_with_trigger(prose, trigger, include_trigger), triggers=(trigger,))
|
||||||
|
return caption, method
|
||||||
|
|||||||
@@ -211,6 +211,50 @@
|
|||||||
"focused stare while clothing is pulled aside",
|
"focused stare while clothing is pulled aside",
|
||||||
"close-range lustful eye contact"
|
"close-range lustful eye contact"
|
||||||
],
|
],
|
||||||
|
"hardcore_interaction_expressions": [
|
||||||
|
"close-range lustful eye contact",
|
||||||
|
"breathless body-contact expression",
|
||||||
|
"focused stare while hands guide the pose",
|
||||||
|
"flushed reaction to teasing touch",
|
||||||
|
"heavy-lidded look during body worship",
|
||||||
|
"mouth open during hands-on guidance",
|
||||||
|
"controlled dominant stare",
|
||||||
|
"submissive bitten-lip expression",
|
||||||
|
"shameless camera-aware expression",
|
||||||
|
"soft moan while being held",
|
||||||
|
"hungry stare during clothing movement",
|
||||||
|
"wide-eyed aroused reaction",
|
||||||
|
"teasing grin while being watched",
|
||||||
|
"steady eye contact during the transition"
|
||||||
|
],
|
||||||
|
"hardcore_manual_expressions": [
|
||||||
|
"focused pleasure face during manual stimulation",
|
||||||
|
"bitten-lip reaction to fingers",
|
||||||
|
"eyes half-closed while being touched",
|
||||||
|
"breathless open-mouth manual-stimulation expression",
|
||||||
|
"direct eye contact while fingers work",
|
||||||
|
"flushed face with hips lifting",
|
||||||
|
"controlled clit-rubbing reaction",
|
||||||
|
"messy aroused stare during fingering",
|
||||||
|
"shameless mutual-masturbation expression",
|
||||||
|
"glossy eyes and parted lips while hands move",
|
||||||
|
"strained moan from hand stimulation",
|
||||||
|
"soft dazed pleasure face"
|
||||||
|
],
|
||||||
|
"hardcore_aftercare_expressions": [
|
||||||
|
"spent satisfied expression",
|
||||||
|
"soft post-sex eye contact",
|
||||||
|
"breathless relaxed face",
|
||||||
|
"dazed afterglow stare",
|
||||||
|
"small exhausted smile",
|
||||||
|
"heavy-lidded calm expression",
|
||||||
|
"flushed skin and relaxed mouth",
|
||||||
|
"tender direct gaze",
|
||||||
|
"sleepy post-orgasm look",
|
||||||
|
"messy but satisfied stare",
|
||||||
|
"quiet intimate expression",
|
||||||
|
"soft cleanup-focused gaze"
|
||||||
|
],
|
||||||
"hardcore_penetration_expressions": [
|
"hardcore_penetration_expressions": [
|
||||||
"controlled eye contact during penetration",
|
"controlled eye contact during penetration",
|
||||||
"focused adult pleasure face during thrusting",
|
"focused adult pleasure face during thrusting",
|
||||||
@@ -483,6 +527,56 @@
|
|||||||
{"text": "close candid creator-shot frame centered on undressing and body contact", "min_people": 2, "max_people": 3},
|
{"text": "close candid creator-shot frame centered on undressing and body contact", "min_people": 2, "max_people": 3},
|
||||||
{"text": "full-body pre-sex foreplay frame with faces, hands, and clothes readable", "min_people": 2, "max_people": 3}
|
{"text": "full-body pre-sex foreplay frame with faces, hands, and clothes readable", "min_people": 2, "max_people": 3}
|
||||||
],
|
],
|
||||||
|
"manual_stimulation_compositions": [
|
||||||
|
{"text": "close crop on hands, open thighs, and manual contact", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "bed-edge manual-stimulation frame with fingers and face readable", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "mirror-view manual contact composition with hands centered", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "side-profile hand-between-thighs composition", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "overhead manual-stimulation frame with body geometry clear", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "tight creator-shot crop centered on fingers, thighs, and reaction", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "seated lap manual contact frame", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "full-body manual-stimulation composition with hands and expression visible", "min_people": 1, "max_people": 3}
|
||||||
|
],
|
||||||
|
"interaction_compositions": [
|
||||||
|
{"text": "close body-interaction frame with hands, faces, and exposed skin readable", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "standing hands-on-body composition with clear body contact", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "bed-edge transition frame with clothing movement and hand placement visible", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "mirror-view interaction composition with bodies and gestures readable", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "tight crop on guidance hands, face, waist, and hips", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "wall-pressed body-control frame with hands and reaction visible", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "couch body-worship composition with mouth, hands, and skin centered", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "full-body transition composition showing the next position clearly", "min_people": 2, "max_people": 3}
|
||||||
|
],
|
||||||
|
"camera_performance_compositions": [
|
||||||
|
{"text": "direct-to-camera body presentation with hands and exposed skin centered", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "mirror creator-performance frame with the body presented to the viewer", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "low phone-angle reveal with hands framing the body", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "bed-edge camera presentation with direct eye contact", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "partner-held presentation frame with the body opened toward the camera", "min_people": 2, "max_people": 3},
|
||||||
|
{"text": "tight subscriber-view crop centered on presentation gesture", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "vertical creator-shot frame with body display and face readable", "min_people": 1, "max_people": 3},
|
||||||
|
{"text": "wide creator-performance frame with hands, hips, and camera awareness visible", "min_people": 1, "max_people": 3}
|
||||||
|
],
|
||||||
|
"group_coordination_compositions": [
|
||||||
|
{"text": "wide group coordination frame with watching and touching roles readable", "min_people": 3},
|
||||||
|
{"text": "center-body group frame with partners arranged around the subject", "min_people": 3},
|
||||||
|
{"text": "mirror-view group interaction showing observer and touching roles", "min_people": 3},
|
||||||
|
{"text": "bed-level group coordination composition with hands and faces visible", "min_people": 3},
|
||||||
|
{"text": "three-person close frame with one watching and one touching", "min_people": 3, "max_people": 3},
|
||||||
|
{"text": "full-room group interaction frame with clear role spacing", "min_people": 3},
|
||||||
|
{"text": "side-profile group presentation frame", "min_people": 3},
|
||||||
|
{"text": "overhead group coordination frame with bodies separated clearly", "min_people": 3}
|
||||||
|
],
|
||||||
|
"aftercare_compositions": [
|
||||||
|
{"text": "post-sex close body-contact frame with hands and faces readable", "min_people": 2},
|
||||||
|
{"text": "bed-edge cleanup composition with towel, skin, and aftermath detail visible", "min_people": 2},
|
||||||
|
{"text": "side-lying aftercare frame with bodies held close", "min_people": 2},
|
||||||
|
{"text": "mirror-view aftermath composition with relaxed bodies and cleanup detail", "min_people": 2},
|
||||||
|
{"text": "tight crop on towel, hands, thighs, and spent faces", "min_people": 2},
|
||||||
|
{"text": "wide aftermath frame with rumpled sheets and discarded clothing", "min_people": 2},
|
||||||
|
{"text": "couch post-sex cuddle composition", "min_people": 2},
|
||||||
|
{"text": "shower-bench cleanup frame with hands and wet skin visible", "min_people": 2}
|
||||||
|
],
|
||||||
"hardcore_explicit_compositions": [
|
"hardcore_explicit_compositions": [
|
||||||
{"text": "full-body explicit sex frame with all adult bodies visible", "min_people": 2},
|
{"text": "full-body explicit sex frame with all adult bodies visible", "min_people": 2},
|
||||||
{"text": "bed-level camera angle focused on genital contact and faces", "min_people": 2, "max_people": 3},
|
{"text": "bed-level camera angle focused on genital contact and faces", "min_people": 2, "max_people": 3},
|
||||||
|
|||||||
@@ -203,6 +203,746 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Manual stimulation",
|
||||||
|
"slug": "manual_stimulation",
|
||||||
|
"min_people": 1,
|
||||||
|
"min_women": 1,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.85,
|
||||||
|
"item_label": "Manual action",
|
||||||
|
"positive_suffix": "Use clear adult manual contact, readable hands, explicit body positioning, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Manual action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult manual stimulation, visible hands, exposed skin, clear body positioning, and readable reaction. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, manual stimulation action: {item}, {scene}, {composition}, explicit consensual adult manual stimulation illustration",
|
||||||
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
|
"expression_pools": ["hardcore_manual_expressions"],
|
||||||
|
"composition_pools": ["manual_stimulation_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{manual_act} in {position}, with {manual_detail}, {body_contact}, and {visibility}",
|
||||||
|
"{position} featuring {manual_act}, {hand_detail}, {reaction_detail}, and {visibility}",
|
||||||
|
"{manual_act} on {surface}, with {manual_detail}, {hand_detail}, and {reaction_detail}",
|
||||||
|
"manual stimulation setup: {position}, {manual_act}, {body_contact}, and {visibility}",
|
||||||
|
"{position} while {manual_act}, with {hand_detail}, {manual_detail}, and {reaction_detail}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"position": [
|
||||||
|
"reclining open-thigh manual position",
|
||||||
|
"bed-edge fingering position",
|
||||||
|
"seated lap manual stimulation position",
|
||||||
|
"standing wall-braced manual position",
|
||||||
|
"side-lying clit-rubbing position",
|
||||||
|
"mirror-facing mutual masturbation position",
|
||||||
|
"kneeling hand-between-thighs position",
|
||||||
|
"couch manual stimulation position"
|
||||||
|
],
|
||||||
|
"manual_act": [
|
||||||
|
"fingering with fingers working inside the pussy",
|
||||||
|
"two fingers sliding into the pussy while the other hand holds the thigh open",
|
||||||
|
"clit rubbing with fingers moving over the clit",
|
||||||
|
"slow clit stimulation while thighs stay open",
|
||||||
|
"hand on pussy with fingers spreading and rubbing",
|
||||||
|
"mutual masturbation with both bodies touching themselves",
|
||||||
|
"one partner guiding the other's hand between the thighs",
|
||||||
|
"partner-held vibrator pressed to the clit",
|
||||||
|
"toy-assisted manual stimulation with one hand controlling the toy",
|
||||||
|
"wet fingers moving between the thighs"
|
||||||
|
],
|
||||||
|
"manual_detail": [
|
||||||
|
"fingers visibly pressing into wet skin",
|
||||||
|
"hand placement centered between the open thighs",
|
||||||
|
"one hand holding the thigh open",
|
||||||
|
"thumb circling the clit",
|
||||||
|
"fingers sliding against the pussy",
|
||||||
|
"wet shine on fingers and inner thighs",
|
||||||
|
"hand guided firmly into place",
|
||||||
|
"toy and fingers both visible at the contact point"
|
||||||
|
],
|
||||||
|
"hand_detail": [
|
||||||
|
"one hand braced on the hip",
|
||||||
|
"one hand gripping the sheets",
|
||||||
|
"hands spreading thighs apart",
|
||||||
|
"fingers hooked at the inner thigh",
|
||||||
|
"one hand holding the wrist in place",
|
||||||
|
"both hands visible and active",
|
||||||
|
"fingers pressing into soft skin",
|
||||||
|
"hand resting on the lower belly"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"thighs held open",
|
||||||
|
"hips tilted toward the hand",
|
||||||
|
"knees spread with the hand centered",
|
||||||
|
"body arched into the touch",
|
||||||
|
"legs parted around the active hand",
|
||||||
|
"torso leaning back with hips exposed",
|
||||||
|
"bodies pressed close while one hand works",
|
||||||
|
"one partner leaning over the other"
|
||||||
|
],
|
||||||
|
"reaction_detail": [
|
||||||
|
"breathless open mouth",
|
||||||
|
"focused direct eye contact",
|
||||||
|
"eyes half-closed from the touch",
|
||||||
|
"bitten-lip reaction",
|
||||||
|
"hips lifting toward the hand",
|
||||||
|
"strained moan with flushed cheeks",
|
||||||
|
"messy aroused expression",
|
||||||
|
"controlled pleasure stare"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"hands and contact point clearly readable",
|
||||||
|
"manual stimulation visible between the thighs",
|
||||||
|
"open thighs and fingers centered",
|
||||||
|
"hands, pussy, and reaction visible",
|
||||||
|
"toy or fingers clearly visible",
|
||||||
|
"body position and hand action readable"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a wide couch",
|
||||||
|
"a hotel bed",
|
||||||
|
"floor cushions",
|
||||||
|
"a low mattress",
|
||||||
|
"a mirror-side bench",
|
||||||
|
"a soft rug",
|
||||||
|
"a private shower bench"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Body worship and touching",
|
||||||
|
"slug": "body_worship_touching",
|
||||||
|
"min_people": 2,
|
||||||
|
"min_women": 1,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.7,
|
||||||
|
"item_label": "Body interaction",
|
||||||
|
"positive_suffix": "Use readable adult body contact, hands and mouth on skin, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Body interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult body worship, close skin contact, mouth and hand placement, exposed skin, and readable body positioning. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, body worship action: {item}, {scene}, {composition}, explicit consensual adult body-contact illustration",
|
||||||
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
|
"expression_pools": ["hardcore_interaction_expressions"],
|
||||||
|
"composition_pools": ["interaction_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{worship_act} in {position}, with {touch_detail}, {face_detail}, and {visibility}",
|
||||||
|
"{position} featuring {worship_act}, {body_contact}, {touch_detail}, and {reaction_detail}",
|
||||||
|
"{worship_act} on {surface}, with {body_contact}, {face_detail}, and {visibility}",
|
||||||
|
"body worship setup: {position}, {worship_act}, {touch_detail}, and {reaction_detail}",
|
||||||
|
"{position} while {worship_act}, with {body_contact}, {face_detail}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"position": [
|
||||||
|
"reclining body-worship position",
|
||||||
|
"kneeling nipple-play position",
|
||||||
|
"bed-edge thigh-kissing body-worship position",
|
||||||
|
"mirror-facing ass-grab body-worship position",
|
||||||
|
"standing breast-touch position",
|
||||||
|
"bed-edge thigh-kissing position",
|
||||||
|
"side-lying caress position",
|
||||||
|
"mirror-facing ass-grab position",
|
||||||
|
"lap-sitting body-contact position",
|
||||||
|
"couch body-worship position"
|
||||||
|
],
|
||||||
|
"worship_act": [
|
||||||
|
"mouth kissing down the chest and stomach",
|
||||||
|
"nipple licking with one hand cupping the breast",
|
||||||
|
"nipple sucking while the other hand grips the waist",
|
||||||
|
"thigh kissing with mouth on inner thighs",
|
||||||
|
"mouth on inner thighs while hands hold the legs open",
|
||||||
|
"kissing inner thighs with hands holding the legs open",
|
||||||
|
"ass grabbing with fingers pressing into soft skin",
|
||||||
|
"mouth on the stomach while hands roam over hips",
|
||||||
|
"breast kissing while the body arches upward",
|
||||||
|
"slow body worship with hands tracing breasts, waist, and thighs",
|
||||||
|
"kissing across the hips and lower belly",
|
||||||
|
"face pressed against the body while hands hold the ass"
|
||||||
|
],
|
||||||
|
"touch_detail": [
|
||||||
|
"hands cupping breasts",
|
||||||
|
"fingers squeezing the ass",
|
||||||
|
"thumbs brushing nipples",
|
||||||
|
"hands sliding across the lower belly",
|
||||||
|
"fingers tracing the inner thighs",
|
||||||
|
"hands gripping the waist",
|
||||||
|
"one hand holding the chin in place",
|
||||||
|
"hands spreading the thighs gently"
|
||||||
|
],
|
||||||
|
"face_detail": [
|
||||||
|
"mouth pressed to skin",
|
||||||
|
"lips around a nipple",
|
||||||
|
"face close to the chest",
|
||||||
|
"cheek pressed against the stomach",
|
||||||
|
"mouth near the inner thigh",
|
||||||
|
"heavy eye contact while kissing the body",
|
||||||
|
"breath against exposed skin",
|
||||||
|
"tongue visible on skin"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"chest arched toward the mouth",
|
||||||
|
"thighs open around the kissing partner",
|
||||||
|
"hips tilted into the touch",
|
||||||
|
"body pressed against the wall",
|
||||||
|
"one body reclined while the other kneels close",
|
||||||
|
"torso bent toward the touch",
|
||||||
|
"bodies close enough for skin-on-skin pressure",
|
||||||
|
"legs draped around the partner"
|
||||||
|
],
|
||||||
|
"reaction_detail": [
|
||||||
|
"breathless parted lips",
|
||||||
|
"eyes half-closed from the touch",
|
||||||
|
"hands gripping sheets",
|
||||||
|
"flushed skin and direct eye contact",
|
||||||
|
"soft moan while being kissed",
|
||||||
|
"body arching into the mouth",
|
||||||
|
"head tilted back",
|
||||||
|
"controlled aroused stare"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"mouth, hands, and exposed skin clearly readable",
|
||||||
|
"breasts, thighs, hands, and faces visible",
|
||||||
|
"body worship contact centered",
|
||||||
|
"skin contact and reaction visible",
|
||||||
|
"hands and mouth placement clear",
|
||||||
|
"full body-contact geography readable"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a hotel bed",
|
||||||
|
"a wide couch",
|
||||||
|
"floor cushions",
|
||||||
|
"a soft rug",
|
||||||
|
"a mirror-side bench",
|
||||||
|
"a low mattress",
|
||||||
|
"a velvet chair"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Clothing and position transitions",
|
||||||
|
"slug": "clothing_position_transitions",
|
||||||
|
"min_people": 2,
|
||||||
|
"min_women": 1,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.65,
|
||||||
|
"item_label": "Transition action",
|
||||||
|
"positive_suffix": "Use readable adult movement, clothing being moved, hands guiding bodies, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Transition action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult undressing, position changes, visible hands, exposed skin, and clear movement from one sexual beat to the next. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, clothing and position transition: {item}, {scene}, {composition}, explicit consensual adult transition illustration",
|
||||||
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
|
"expression_pools": ["hardcore_interaction_expressions"],
|
||||||
|
"composition_pools": ["interaction_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{transition_act} in {position}, with {clothing_detail}, {hand_detail}, and {visibility}",
|
||||||
|
"{position} featuring {transition_act}, {body_contact}, {clothing_detail}, and {movement_detail}",
|
||||||
|
"{transition_act} on {surface}, with {hand_detail}, {body_contact}, and {visibility}",
|
||||||
|
"position transition: {position}, {transition_act}, {movement_detail}, and {clothing_detail}",
|
||||||
|
"{position} while {transition_act}, with {hand_detail}, {body_contact}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"position": [
|
||||||
|
"bed-edge clothing-removal position",
|
||||||
|
"standing turn-around transition",
|
||||||
|
"pulling-onto-the-bed transition",
|
||||||
|
"lifting-legs transition position",
|
||||||
|
"kneeling-to-standing transition",
|
||||||
|
"mirror undressing transition",
|
||||||
|
"couch position-change setup",
|
||||||
|
"floor-to-bed transition"
|
||||||
|
],
|
||||||
|
"transition_act": [
|
||||||
|
"pulling clothing aside before the next act",
|
||||||
|
"lower garments being pulled down below the hips",
|
||||||
|
"shirt being opened while bodies press together",
|
||||||
|
"one partner turning the other around by the hips",
|
||||||
|
"one partner pulling the other onto the bed",
|
||||||
|
"legs being lifted and spread into position",
|
||||||
|
"hips being guided backward into position",
|
||||||
|
"body being turned from kissing into a rear-facing pose",
|
||||||
|
"kneeling partner being guided upward by the hands",
|
||||||
|
"clothes being discarded beside the bodies"
|
||||||
|
],
|
||||||
|
"clothing_detail": [
|
||||||
|
"fabric bunched at the waist",
|
||||||
|
"straps sliding off shoulders",
|
||||||
|
"panties or lower garments pulled aside",
|
||||||
|
"shirt open with bare chest visible",
|
||||||
|
"skirt lifted high around the hips",
|
||||||
|
"trousers or underwear lowered below the hips",
|
||||||
|
"garment half removed and hanging from one leg",
|
||||||
|
"discarded clothing visible on the floor"
|
||||||
|
],
|
||||||
|
"hand_detail": [
|
||||||
|
"hands on hips guiding the turn",
|
||||||
|
"one hand lifting a thigh",
|
||||||
|
"hands pulling fabric aside",
|
||||||
|
"one hand on the lower back",
|
||||||
|
"hands spreading thighs into position",
|
||||||
|
"fingers hooked into the waistband",
|
||||||
|
"hands pulling the body closer",
|
||||||
|
"one hand braced on the bed"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"bodies pressed together during the position change",
|
||||||
|
"hips angled toward the next pose",
|
||||||
|
"legs lifted into the frame",
|
||||||
|
"knees moving apart",
|
||||||
|
"torso bent forward by the guiding hands",
|
||||||
|
"body rotated toward the camera",
|
||||||
|
"one body reclined while the other stands close",
|
||||||
|
"skin revealed as clothing moves"
|
||||||
|
],
|
||||||
|
"movement_detail": [
|
||||||
|
"movement frozen mid-transition",
|
||||||
|
"clear before-the-next-act body geometry",
|
||||||
|
"hands actively changing the pose",
|
||||||
|
"clothing motion visible",
|
||||||
|
"body weight shifting toward the bed",
|
||||||
|
"hips and legs visibly being repositioned",
|
||||||
|
"urgent consensual movement",
|
||||||
|
"slow teasing repositioning"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"clothing movement and hands clearly readable",
|
||||||
|
"transition body geometry visible",
|
||||||
|
"fabric, exposed skin, and hand placement centered",
|
||||||
|
"pose change visible in one frame",
|
||||||
|
"the next position readable from the body angle",
|
||||||
|
"hands, hips, and clothing clearly visible"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a hotel bed",
|
||||||
|
"a wide couch",
|
||||||
|
"a low mattress",
|
||||||
|
"floor cushions",
|
||||||
|
"a mirror-side bench",
|
||||||
|
"a soft rug",
|
||||||
|
"a dressing-room chair"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dominant guidance and restraint",
|
||||||
|
"slug": "dominant_guidance",
|
||||||
|
"min_people": 2,
|
||||||
|
"min_women": 1,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.55,
|
||||||
|
"item_label": "Guidance action",
|
||||||
|
"positive_suffix": "Use consensual adult control, readable hand placement, clear body positioning, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Guidance action: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through consensual adult guidance, hair or wrist control, body positioning, visible hands, exposed skin, and clear power dynamic. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, dominant guidance action: {item}, {scene}, {composition}, explicit consensual adult power-dynamic illustration",
|
||||||
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
|
"expression_pools": ["hardcore_interaction_expressions"],
|
||||||
|
"composition_pools": ["interaction_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{control_act} in {position}, with {hand_detail}, {body_contact}, and {visibility}",
|
||||||
|
"{position} featuring {control_act}, {power_detail}, {hand_detail}, and {reaction_detail}",
|
||||||
|
"{control_act} on {surface}, with {body_contact}, {power_detail}, and {visibility}",
|
||||||
|
"consensual control setup: {position}, {control_act}, {hand_detail}, and {reaction_detail}",
|
||||||
|
"{position} while {control_act}, with {power_detail}, {body_contact}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"position": [
|
||||||
|
"standing hair-holding position",
|
||||||
|
"bed-edge wrist-pinning position",
|
||||||
|
"kneeling guided-position position",
|
||||||
|
"wall-pinned body-control position",
|
||||||
|
"reclining wrists-overhead position",
|
||||||
|
"mirror-facing dominant guidance position",
|
||||||
|
"couch hands-on-hips position",
|
||||||
|
"close dirty-talk guidance position",
|
||||||
|
"floor-level guided pose"
|
||||||
|
],
|
||||||
|
"control_act": [
|
||||||
|
"hair held back while the body is guided closer",
|
||||||
|
"wrists pinned above the head on the bed",
|
||||||
|
"one hand holding the jaw while the other guides the hips",
|
||||||
|
"hands on hips turning the body into position",
|
||||||
|
"one partner spreading thighs with both hands",
|
||||||
|
"hair pulled back gently to expose the face",
|
||||||
|
"wrist held down while the body arches",
|
||||||
|
"dirty talk whispered close to the ear while one hand holds the waist",
|
||||||
|
"mouth near the ear giving verbal teasing while the body is held close",
|
||||||
|
"hands pressing the body into a wall-braced pose",
|
||||||
|
"chin lifted by fingers during a dominant stare",
|
||||||
|
"hips pulled back by both hands"
|
||||||
|
],
|
||||||
|
"hand_detail": [
|
||||||
|
"fingers tangled in hair",
|
||||||
|
"one hand wrapped around both wrists",
|
||||||
|
"hands gripping hips",
|
||||||
|
"one hand under the chin",
|
||||||
|
"hands spreading thighs",
|
||||||
|
"palm pressing between the shoulder blades",
|
||||||
|
"one hand braced near the head",
|
||||||
|
"one hand holding the body close during whispering",
|
||||||
|
"fingers pressing into the waist"
|
||||||
|
],
|
||||||
|
"power_detail": [
|
||||||
|
"consensual rough guidance",
|
||||||
|
"controlled dominant pacing",
|
||||||
|
"clear guided body position",
|
||||||
|
"submissive presenting posture",
|
||||||
|
"dominant hand placement visible",
|
||||||
|
"steady eye contact during control",
|
||||||
|
"body held open for the camera",
|
||||||
|
"dirty talk and verbal teasing visible through mouth-near-ear body language",
|
||||||
|
"commanding close-range tension"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"body arched under the guiding hands",
|
||||||
|
"hips pulled toward the partner",
|
||||||
|
"wrists held against the sheets",
|
||||||
|
"thighs spread by both hands",
|
||||||
|
"torso pressed toward the wall",
|
||||||
|
"body angled by the grip on the hips",
|
||||||
|
"knees separated while hands hold position",
|
||||||
|
"head tilted back by the hair hold"
|
||||||
|
],
|
||||||
|
"reaction_detail": [
|
||||||
|
"wide-eyed aroused stare",
|
||||||
|
"bitten-lip submission",
|
||||||
|
"steady dominant eye contact",
|
||||||
|
"breathless parted lips",
|
||||||
|
"flushed controlled expression",
|
||||||
|
"shameless direct gaze",
|
||||||
|
"eyes half-closed under control",
|
||||||
|
"strained moan with consent visible"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"hands, wrists, hair, and body position clearly readable",
|
||||||
|
"control gesture visible without confusion",
|
||||||
|
"dominant hand placement centered",
|
||||||
|
"face, hands, and hips visible",
|
||||||
|
"the guided body position readable",
|
||||||
|
"consensual power dynamic clear in the pose"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a hotel bed",
|
||||||
|
"against a wall",
|
||||||
|
"a wide couch",
|
||||||
|
"floor cushions",
|
||||||
|
"a low mattress",
|
||||||
|
"a mirror-side bench",
|
||||||
|
"a soft rug"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Camera performance and presentation",
|
||||||
|
"slug": "camera_performance",
|
||||||
|
"min_people": 1,
|
||||||
|
"min_women": 1,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.6,
|
||||||
|
"item_label": "Camera performance",
|
||||||
|
"positive_suffix": "Use creator-shot adult presentation, readable camera-facing pose, exposed skin, clear hand placement, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Camera performance: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through camera-aware adult presentation, body opened or displayed to the viewer, visible hands, exposed skin, and clean creator-shot framing. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, camera performance action: {item}, {scene}, {composition}, explicit consensual adult creator-performance illustration",
|
||||||
|
"scene_pools": ["hardcore_private_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"],
|
||||||
|
"expression_pools": ["hardcore_interaction_expressions"],
|
||||||
|
"composition_pools": ["camera_performance_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{performance_act} in {position}, with {presentation_detail}, {hand_detail}, and {visibility}",
|
||||||
|
"{position} featuring {performance_act}, {camera_detail}, {presentation_detail}, and {reaction_detail}",
|
||||||
|
"{performance_act} on {surface}, with {hand_detail}, {camera_detail}, and {visibility}",
|
||||||
|
"creator-performance setup: {position}, {performance_act}, {presentation_detail}, and {reaction_detail}",
|
||||||
|
"{position} while {performance_act}, with {camera_detail}, {hand_detail}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"position": [
|
||||||
|
"reclining camera-presentation position",
|
||||||
|
"mirror spread-open presentation",
|
||||||
|
"standing show-to-camera position",
|
||||||
|
"bed-edge creator presentation",
|
||||||
|
"kneeling close-camera reveal",
|
||||||
|
"side-lying camera-facing display",
|
||||||
|
"partner-held presentation position",
|
||||||
|
"couch camera-performance position"
|
||||||
|
],
|
||||||
|
"performance_act": [
|
||||||
|
"body presented directly to the camera",
|
||||||
|
"spreading open for the camera with hands visible",
|
||||||
|
"partner holding the body open for the camera",
|
||||||
|
"looking into the camera while exposing the body",
|
||||||
|
"mirror-facing creator reveal with phone-angle intimacy",
|
||||||
|
"one hand pointing or guiding attention to the body",
|
||||||
|
"teasing the camera while a partner watches",
|
||||||
|
"showing the next action to the camera",
|
||||||
|
"hands framing breasts, hips, and thighs for the viewer",
|
||||||
|
"body angled to keep the erotic focus visible"
|
||||||
|
],
|
||||||
|
"presentation_detail": [
|
||||||
|
"direct eye contact with the lens",
|
||||||
|
"hands framing the exposed body",
|
||||||
|
"body opened toward the viewer",
|
||||||
|
"hips tilted toward the camera",
|
||||||
|
"legs placed to keep the body readable",
|
||||||
|
"partner's hands presenting the body",
|
||||||
|
"mirror reflection doubling the reveal",
|
||||||
|
"phone-camera intimacy implied by the framing"
|
||||||
|
],
|
||||||
|
"camera_detail": [
|
||||||
|
"creator-shot framing",
|
||||||
|
"close subscriber-view angle",
|
||||||
|
"mirror-camera presentation",
|
||||||
|
"low phone angle",
|
||||||
|
"tight handheld crop",
|
||||||
|
"vertical feed-style framing",
|
||||||
|
"direct-to-camera performance",
|
||||||
|
"camera-aware body display"
|
||||||
|
],
|
||||||
|
"hand_detail": [
|
||||||
|
"hands holding thighs open",
|
||||||
|
"one hand on the camera-facing hip",
|
||||||
|
"hands framing breasts",
|
||||||
|
"partner's hands on the waist",
|
||||||
|
"one hand pulling fabric aside",
|
||||||
|
"fingers spread over the lower belly",
|
||||||
|
"hands braced beside the body",
|
||||||
|
"one hand touching the face while looking at the camera"
|
||||||
|
],
|
||||||
|
"reaction_detail": [
|
||||||
|
"confident direct stare",
|
||||||
|
"shameless camera-aware grin",
|
||||||
|
"breathless close-camera expression",
|
||||||
|
"heavy-lidded creator gaze",
|
||||||
|
"teasing open mouth",
|
||||||
|
"controlled performance stare",
|
||||||
|
"flushed direct eye contact",
|
||||||
|
"knowing subscriber-camera expression"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"camera-facing action clearly readable",
|
||||||
|
"hands, exposed skin, and body opening centered",
|
||||||
|
"presentation gesture visible",
|
||||||
|
"body display and camera awareness clear",
|
||||||
|
"partner hands and creator gaze visible",
|
||||||
|
"mirror or lens-directed pose readable"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a hotel bed",
|
||||||
|
"a mirror-side bench",
|
||||||
|
"a wide couch",
|
||||||
|
"floor cushions",
|
||||||
|
"a low mattress",
|
||||||
|
"a soft rug",
|
||||||
|
"a private shower bench"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Group coordination and watching",
|
||||||
|
"slug": "group_coordination",
|
||||||
|
"min_people": 3,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.55,
|
||||||
|
"item_label": "Group interaction",
|
||||||
|
"positive_suffix": "Use readable adult group coordination, clear body spacing, visible watching/touching roles, exposed skin, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Group interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult group coordination, watching, guiding hands, body presentation, exposed skin, and clear role spacing. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, group coordination action: {item}, {scene}, {composition}, explicit consensual adult group-interaction illustration",
|
||||||
|
"scene_pools": ["hardcore_group_scenes", "hardcore_private_scenes"],
|
||||||
|
"expression_pools": ["hardcore_group_expressions"],
|
||||||
|
"composition_pools": ["group_coordination_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{coordination_act} with {arrangement}, {touch_detail}, {reaction_detail}, and {visibility}",
|
||||||
|
"{arrangement} featuring {coordination_act}, {body_contact}, {watching_detail}, and {visibility}",
|
||||||
|
"{coordination_act} on {surface}, with {touch_detail}, {watching_detail}, and {body_contact}",
|
||||||
|
"group coordination setup: {arrangement}, {coordination_act}, {watching_detail}, and {visibility}",
|
||||||
|
"{arrangement} while {coordination_act}, with {touch_detail}, {reaction_detail}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"arrangement": [
|
||||||
|
"one central body with partners arranged around them",
|
||||||
|
"one partner watching while another touches the central body",
|
||||||
|
"two partners guiding the central body into position",
|
||||||
|
"one partner behind and one partner in front without penetration",
|
||||||
|
"a waiting-turn arrangement beside the bed",
|
||||||
|
"partners framing the body from both sides",
|
||||||
|
"one partner kneeling while another stands close",
|
||||||
|
"mirror-view group coordination arrangement"
|
||||||
|
],
|
||||||
|
"coordination_act": [
|
||||||
|
"one partner watches while another caresses the body",
|
||||||
|
"two partners hold the body open for the camera",
|
||||||
|
"one partner kisses while another touches breasts and hips",
|
||||||
|
"partners take turns touching and presenting the central body",
|
||||||
|
"one partner guides the face while another guides the hips",
|
||||||
|
"watching partner keeps a hand on the shoulder",
|
||||||
|
"two partners coordinate hands across breasts, waist, and thighs",
|
||||||
|
"one partner waits close while the other controls the pose"
|
||||||
|
],
|
||||||
|
"touch_detail": [
|
||||||
|
"hands on breasts, hips, and thighs",
|
||||||
|
"one hand holding the chin",
|
||||||
|
"hands spreading thighs",
|
||||||
|
"one partner gripping the waist",
|
||||||
|
"hands framing the exposed body",
|
||||||
|
"one partner touching from behind",
|
||||||
|
"fingers tangled in hair",
|
||||||
|
"one hand braced on the bed"
|
||||||
|
],
|
||||||
|
"watching_detail": [
|
||||||
|
"watching partner keeps eye contact",
|
||||||
|
"one partner looks down at the action",
|
||||||
|
"waiting partner stays close in frame",
|
||||||
|
"side partner watches with hands on skin",
|
||||||
|
"partners exchange direct looks",
|
||||||
|
"one partner watches from beside the bed",
|
||||||
|
"mirror reflection shows the observer",
|
||||||
|
"all faces remain readable"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"central body held open",
|
||||||
|
"bodies pressed around the center",
|
||||||
|
"partners close enough to touch from both sides",
|
||||||
|
"central hips angled toward the camera",
|
||||||
|
"knees spread by guiding hands",
|
||||||
|
"skin-on-skin contact from multiple sides",
|
||||||
|
"body framed by surrounding hands",
|
||||||
|
"clear spacing between each role"
|
||||||
|
],
|
||||||
|
"reaction_detail": [
|
||||||
|
"mixed hungry stares",
|
||||||
|
"central body flushed and breathless",
|
||||||
|
"watching partner focused and close",
|
||||||
|
"several direct camera-aware looks",
|
||||||
|
"one partner smiling while another concentrates",
|
||||||
|
"shared aroused expressions",
|
||||||
|
"heavy-lidded group tension",
|
||||||
|
"focused adult anticipation"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"all roles readable in one frame",
|
||||||
|
"watching, touching, and presenting roles clear",
|
||||||
|
"hands and faces visible across the group",
|
||||||
|
"central body and surrounding partners visible",
|
||||||
|
"group spacing clear without action confusion",
|
||||||
|
"body presentation and observer reaction visible"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a hotel bed",
|
||||||
|
"floor cushions",
|
||||||
|
"a wide couch",
|
||||||
|
"a low mattress",
|
||||||
|
"a velvet lounge",
|
||||||
|
"a soft rug",
|
||||||
|
"a private suite floor"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Aftercare and cleanup",
|
||||||
|
"slug": "aftercare_cleanup",
|
||||||
|
"min_people": 2,
|
||||||
|
"inherit_expressions": false,
|
||||||
|
"inherit_compositions": false,
|
||||||
|
"weight": 0.35,
|
||||||
|
"item_label": "Aftermath interaction",
|
||||||
|
"positive_suffix": "Use adult post-sex intimacy, readable bodies and hands, visible aftermath details, warm erotic lighting, crisp comic linework, detailed hatching, and tactile textured paper.",
|
||||||
|
"prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Aftermath interaction: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit through adult post-sex closeness, cleanup, visible skin, relaxed body contact, aftermath details, and readable hands and faces. {positive_suffix} Avoid: {negative_prompt}.",
|
||||||
|
"caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, aftercare and cleanup action: {item}, {scene}, {composition}, explicit consensual adult post-sex aftermath illustration",
|
||||||
|
"scene_pools": ["hardcore_climax_scenes", "hardcore_bed_scenes", "hardcore_private_scenes"],
|
||||||
|
"expression_pools": ["hardcore_aftercare_expressions"],
|
||||||
|
"composition_pools": ["aftercare_compositions"],
|
||||||
|
"item_templates": [
|
||||||
|
"{aftercare_act} in {position}, with {cleanup_detail}, {body_contact}, and {visibility}",
|
||||||
|
"{position} featuring {aftercare_act}, {touch_detail}, {cleanup_detail}, and {reaction_detail}",
|
||||||
|
"{aftercare_act} on {surface}, with {body_contact}, {touch_detail}, and {visibility}",
|
||||||
|
"post-sex aftermath setup: {position}, {aftercare_act}, {cleanup_detail}, and {reaction_detail}",
|
||||||
|
"{position} while {aftercare_act}, with {touch_detail}, {body_contact}, and {visibility}"
|
||||||
|
],
|
||||||
|
"item_axes": {
|
||||||
|
"position": [
|
||||||
|
"reclining post-sex cuddle position",
|
||||||
|
"bed-edge cleanup position",
|
||||||
|
"side-lying aftercare position",
|
||||||
|
"kneeling towel-cleanup position",
|
||||||
|
"mirror aftermath position",
|
||||||
|
"couch post-sex closeness position",
|
||||||
|
"lap-held aftercare position",
|
||||||
|
"shower-bench cleanup position"
|
||||||
|
],
|
||||||
|
"aftercare_act": [
|
||||||
|
"kissing after sex with bodies still close",
|
||||||
|
"one partner wiping skin with a towel",
|
||||||
|
"post-sex cuddle with hands on the body",
|
||||||
|
"one partner checking the other's face while holding them close",
|
||||||
|
"cleanup with a wet cloth across the thighs",
|
||||||
|
"exhausted bodies resting together after orgasm",
|
||||||
|
"one partner kissing cum or sweat from skin",
|
||||||
|
"gentle caressing after the explicit act",
|
||||||
|
"bodies still tangled in the aftermath",
|
||||||
|
"one partner holding the other against the chest"
|
||||||
|
],
|
||||||
|
"cleanup_detail": [
|
||||||
|
"towel visible in one hand",
|
||||||
|
"wet cloth wiping skin",
|
||||||
|
"aftermath fluids still visible on skin",
|
||||||
|
"rumpled sheets and discarded clothing nearby",
|
||||||
|
"hands wiping thighs and belly",
|
||||||
|
"skin shiny with sweat and aftermath",
|
||||||
|
"one hand smoothing hair away from the face",
|
||||||
|
"messy bedding and relaxed limbs"
|
||||||
|
],
|
||||||
|
"touch_detail": [
|
||||||
|
"hands resting on the lower back",
|
||||||
|
"one hand cupping the face",
|
||||||
|
"fingers brushing hair aside",
|
||||||
|
"hands wrapped around the waist",
|
||||||
|
"one hand on the thigh",
|
||||||
|
"bodies held chest to chest",
|
||||||
|
"gentle hand on the stomach",
|
||||||
|
"arms wrapped around shoulders"
|
||||||
|
],
|
||||||
|
"body_contact": [
|
||||||
|
"bodies lying close together",
|
||||||
|
"legs tangled on the sheets",
|
||||||
|
"one body curled against the other",
|
||||||
|
"skin pressed together in relaxed contact",
|
||||||
|
"one partner kneeling close with a towel",
|
||||||
|
"thighs relaxed and open after the scene",
|
||||||
|
"body resting against a partner's chest",
|
||||||
|
"both bodies exhausted but close"
|
||||||
|
],
|
||||||
|
"reaction_detail": [
|
||||||
|
"spent satisfied expression",
|
||||||
|
"soft post-sex eye contact",
|
||||||
|
"breathless relaxed face",
|
||||||
|
"dazed afterglow stare",
|
||||||
|
"small exhausted smile",
|
||||||
|
"heavy-lidded calm expression",
|
||||||
|
"flushed skin and relaxed mouth",
|
||||||
|
"tender direct gaze"
|
||||||
|
],
|
||||||
|
"visibility": [
|
||||||
|
"aftercare gesture clearly visible",
|
||||||
|
"cleanup object and body contact readable",
|
||||||
|
"hands, faces, and aftermath details visible",
|
||||||
|
"post-sex closeness centered",
|
||||||
|
"towel or cloth action clear",
|
||||||
|
"relaxed bodies and visible skin readable"
|
||||||
|
],
|
||||||
|
"surface": [
|
||||||
|
"rumpled bed sheets",
|
||||||
|
"a hotel bed",
|
||||||
|
"a wide couch",
|
||||||
|
"floor cushions",
|
||||||
|
"a low mattress",
|
||||||
|
"a shower bench",
|
||||||
|
"a soft rug",
|
||||||
|
"a velvet chaise"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Penetrative sex",
|
"name": "Penetrative sex",
|
||||||
"slug": "penetrative_sex",
|
"slug": "penetrative_sex",
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# Prompt Architecture Improvement Plan
|
||||||
|
|
||||||
|
This is a working research note for organizing the prompt builder around the
|
||||||
|
routing map in `docs/prompt-pool-routing-map.md`.
|
||||||
|
|
||||||
|
## Current Branch Additions
|
||||||
|
|
||||||
|
The current branch adds two major surfaces:
|
||||||
|
|
||||||
|
- `SxCP Krea2 Resolution Selector` in `__init__.py`, with README notes.
|
||||||
|
- Expanded hardcore interaction/manual/action pools in
|
||||||
|
`categories/sexual_poses.json`,
|
||||||
|
`categories/expression_composition_pools.json`, `prompt_builder.py`, and
|
||||||
|
`krea_formatter.py`.
|
||||||
|
|
||||||
|
The map audit currently sees:
|
||||||
|
|
||||||
|
- 15 sexual pose subcategories.
|
||||||
|
- 94 sexual pose item templates.
|
||||||
|
- 23 expression pools.
|
||||||
|
- 24 composition pools.
|
||||||
|
- A new Krea2 resolution node with width/height/API aspect outputs.
|
||||||
|
|
||||||
|
## Architectural Finding
|
||||||
|
|
||||||
|
The project has a good functional map, but ownership is still mixed inside large
|
||||||
|
files:
|
||||||
|
|
||||||
|
- `prompt_builder.py` owns selection, character resolution, role graph logic,
|
||||||
|
camera adaptation, pair assembly, and some final string cleanup.
|
||||||
|
- `krea_formatter.py` owns metadata parsing, cast naturalization, sexual action
|
||||||
|
rewriting, POV rewriting, clothing cleanup, camera preservation, fallback
|
||||||
|
parsing, and final prose assembly.
|
||||||
|
- `sdxl_formatter.py` owns tag assembly and style/quality presets.
|
||||||
|
- `caption_naturalizer.py` owns training-caption prose.
|
||||||
|
- Category JSON files own scalable pool content, but Python still owns several
|
||||||
|
compatibility and role-graph decisions.
|
||||||
|
|
||||||
|
The biggest maintainability risk is not the number of pools. The risk is that
|
||||||
|
selection, semantic rewriting, and final text hygiene are too interleaved. When a
|
||||||
|
prompt has wrong text, it is easy to patch the wrong layer.
|
||||||
|
|
||||||
|
## First Refactor Boundary
|
||||||
|
|
||||||
|
Generic text hygiene now has one home:
|
||||||
|
|
||||||
|
- `prompt_hygiene.py`
|
||||||
|
|
||||||
|
It should only handle route-agnostic cleanup:
|
||||||
|
|
||||||
|
- whitespace and punctuation normalization;
|
||||||
|
- empty field-label removal;
|
||||||
|
- repeated trigger prefix cleanup;
|
||||||
|
- duplicate comma-list item removal;
|
||||||
|
- adjacent duplicate sentence cleanup;
|
||||||
|
- simple dangling connector cleanup.
|
||||||
|
|
||||||
|
It must not make semantic decisions such as sexual action positioning, POV
|
||||||
|
geometry, clothing state, or model-specific tag weighting. Those stay in the
|
||||||
|
route-specific owner.
|
||||||
|
|
||||||
|
Current integration points:
|
||||||
|
|
||||||
|
- `prompt_builder.build_prompt`
|
||||||
|
- `prompt_builder.build_insta_of_pair`
|
||||||
|
- `krea_formatter.format_krea2_prompt`
|
||||||
|
- `sdxl_formatter.format_sdxl_prompt`
|
||||||
|
- `caption_naturalizer.naturalize_caption`
|
||||||
|
|
||||||
|
## Target Organization
|
||||||
|
|
||||||
|
### Generation Layer
|
||||||
|
|
||||||
|
Owner: `prompt_builder.py` plus `categories/*.json`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- category/subcategory/item selection;
|
||||||
|
- seed axis routing;
|
||||||
|
- character slot/profile resolution;
|
||||||
|
- scene/expression/composition pool selection;
|
||||||
|
- role graph creation from structured category axes;
|
||||||
|
- metadata row construction.
|
||||||
|
|
||||||
|
Move or isolate later:
|
||||||
|
|
||||||
|
- role graph generation for hardcore interaction categories into a dedicated
|
||||||
|
module, for example `hardcore_role_graphs.py`;
|
||||||
|
- camera-scene adapters into `scene_camera_adapters.py`;
|
||||||
|
- category-library loading and inheritance helpers into `category_library.py`.
|
||||||
|
|
||||||
|
### Pair / Adapter Layer
|
||||||
|
|
||||||
|
Owner today: `build_insta_of_pair`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- soft/hard row creation;
|
||||||
|
- continuity policy;
|
||||||
|
- softcore cast policy;
|
||||||
|
- pair-level camera routing;
|
||||||
|
- pair metadata shape.
|
||||||
|
|
||||||
|
Improve later:
|
||||||
|
|
||||||
|
- make a single pair metadata sanitizer that normalizes `softcore_row`,
|
||||||
|
`hardcore_row`, pair prompts, negatives, captions, and camera fields;
|
||||||
|
- split pair assembly into small functions by phase:
|
||||||
|
`build_soft_row`, `build_hard_row`, `resolve_pair_camera`,
|
||||||
|
`resolve_pair_clothing`, `assemble_pair_metadata`.
|
||||||
|
|
||||||
|
### Krea2 Formatter Path
|
||||||
|
|
||||||
|
Owner: `krea_formatter.py`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- Krea prose style;
|
||||||
|
- cast prose;
|
||||||
|
- hardcore action sentence rewriting;
|
||||||
|
- POV sentence rewriting;
|
||||||
|
- clothing naturalization;
|
||||||
|
- camera-scene preservation;
|
||||||
|
- fallback text parsing.
|
||||||
|
|
||||||
|
Improve later:
|
||||||
|
|
||||||
|
- split semantic blocks into modules:
|
||||||
|
`krea_cast.py`, `krea_actions.py`, `krea_pov.py`, `krea_clothing.py`;
|
||||||
|
- add route-level smoke fixtures for representative metadata rows;
|
||||||
|
- make `_hardcore_action_sentence` dispatch by action family instead of long
|
||||||
|
conditional chains.
|
||||||
|
|
||||||
|
### SDXL Formatter Path
|
||||||
|
|
||||||
|
Owner: `sdxl_formatter.py`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- trigger behavior;
|
||||||
|
- style and quality presets;
|
||||||
|
- tag ordering;
|
||||||
|
- weighted explicit tags;
|
||||||
|
- negative-prompt assembly.
|
||||||
|
|
||||||
|
Improve later:
|
||||||
|
|
||||||
|
- move presets into data dictionaries or JSON so adding styles does not require
|
||||||
|
editing formatter logic;
|
||||||
|
- add formatter profiles for Pony, SDXL photo, and flat vector;
|
||||||
|
- make fallback cleanup use the shared field-label inventory.
|
||||||
|
|
||||||
|
### Naturalizer Path
|
||||||
|
|
||||||
|
Owner: `caption_naturalizer.py`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- natural sentence caption assembly;
|
||||||
|
- training-caption trigger behavior;
|
||||||
|
- style-tail policy.
|
||||||
|
|
||||||
|
Improve later:
|
||||||
|
|
||||||
|
- share more metadata readers with Krea without sharing Krea prose;
|
||||||
|
- add a `caption_profile` option for concise/dense LoRA caption styles.
|
||||||
|
|
||||||
|
### Category JSON Path
|
||||||
|
|
||||||
|
Owner: `categories/*.json`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- scalable prompt pool content;
|
||||||
|
- named scene/expression/composition pools;
|
||||||
|
- item templates and axes;
|
||||||
|
- direct category-specific wording.
|
||||||
|
|
||||||
|
Improve later:
|
||||||
|
|
||||||
|
- introduce optional `family` and `action_type` fields on item templates so
|
||||||
|
Python filters do less keyword guessing;
|
||||||
|
- add `formatter_hint` fields only where needed, not globally;
|
||||||
|
- add a JSON audit that checks every referenced expression/composition/scene pool
|
||||||
|
exists.
|
||||||
|
|
||||||
|
### Node / UI Path
|
||||||
|
|
||||||
|
Owner: `__init__.py`, `loop_nodes.py`, `web/*.js`.
|
||||||
|
|
||||||
|
Keep here:
|
||||||
|
|
||||||
|
- ComfyUI node input/output declarations;
|
||||||
|
- widget behavior;
|
||||||
|
- button actions;
|
||||||
|
- dynamic input slots.
|
||||||
|
|
||||||
|
Improve later:
|
||||||
|
|
||||||
|
- split large node classes into files by family;
|
||||||
|
- keep node display names, return names, and docs in sync through the audit
|
||||||
|
helper;
|
||||||
|
- add small endpoint tests for profile/accumulator/index-switch routes.
|
||||||
|
|
||||||
|
## Path-Specific Improvements
|
||||||
|
|
||||||
|
### Prompt Builder
|
||||||
|
|
||||||
|
Near-term:
|
||||||
|
|
||||||
|
- Add final row hygiene already done through `prompt_hygiene.py`.
|
||||||
|
- Add a metadata smoke checker for representative rows through
|
||||||
|
`tools/prompt_smoke.py`.
|
||||||
|
- Normalize every row with one function before JSON serialization.
|
||||||
|
|
||||||
|
Medium-term:
|
||||||
|
|
||||||
|
- Extract category loading and role graph logic.
|
||||||
|
- Convert keyword-heavy interaction filtering to template metadata.
|
||||||
|
|
||||||
|
### Insta/OF Pair
|
||||||
|
|
||||||
|
Near-term:
|
||||||
|
|
||||||
|
- Normalize pair metadata with one helper.
|
||||||
|
- Confirm pair prompts, captions, and soft/hard rows carry the same sanitized
|
||||||
|
scene/camera/clothing fields.
|
||||||
|
- Keep same-room pair continuity synchronized in both assembled prompt text and
|
||||||
|
`hardcore_row.scene_text`; `tools/prompt_smoke.py` covers this drift case.
|
||||||
|
|
||||||
|
Medium-term:
|
||||||
|
|
||||||
|
- Make pair camera and clothing phases explicit subfunctions.
|
||||||
|
- Add smoke fixtures for same-cast, POV man, explicit nude, and different-camera
|
||||||
|
modes.
|
||||||
|
|
||||||
|
### Krea2
|
||||||
|
|
||||||
|
Near-term:
|
||||||
|
|
||||||
|
- Add final prose hygiene already done through `prompt_hygiene.py`.
|
||||||
|
- Add smoke coverage through `tools/prompt_smoke.py` for metadata-driven Krea2
|
||||||
|
formatting across built-in rows, hardcore rows, same-cast pairs, and POV
|
||||||
|
pairs. Expand it next for close foreplay, POV penetration, and camera-scene
|
||||||
|
preservation.
|
||||||
|
|
||||||
|
Medium-term:
|
||||||
|
|
||||||
|
- Dispatch action rewriting by action family.
|
||||||
|
- Split Krea semantic helpers into smaller modules.
|
||||||
|
|
||||||
|
### SDXL
|
||||||
|
|
||||||
|
Near-term:
|
||||||
|
|
||||||
|
- Add final tag hygiene already done through `prompt_hygiene.py`.
|
||||||
|
- Add smoke tests for trigger preservation and duplicate tag removal through
|
||||||
|
`tools/prompt_smoke.py`.
|
||||||
|
|
||||||
|
Medium-term:
|
||||||
|
|
||||||
|
- Make style/quality presets data-driven.
|
||||||
|
|
||||||
|
### Naturalizer
|
||||||
|
|
||||||
|
Near-term:
|
||||||
|
|
||||||
|
- Add final prose hygiene already done through `prompt_hygiene.py`.
|
||||||
|
- Verify training captions keep trigger exactly once through
|
||||||
|
`tools/prompt_smoke.py`.
|
||||||
|
|
||||||
|
Medium-term:
|
||||||
|
|
||||||
|
- Add caption profiles for training and browsing use cases.
|
||||||
|
|
||||||
|
### Camera / Scene
|
||||||
|
|
||||||
|
Near-term:
|
||||||
|
|
||||||
|
- Keep Qwen/orbit as camera source.
|
||||||
|
- Keep scene-camera adapters scoped by location family.
|
||||||
|
- Use the memory note in
|
||||||
|
`/home/ethanfel/.codex/memories/scene-camera-system.md` when editing POV.
|
||||||
|
|
||||||
|
Medium-term:
|
||||||
|
|
||||||
|
- Move coworking adapter into a scene-camera adapter module.
|
||||||
|
- Build new adapters one location family at a time.
|
||||||
|
|
||||||
|
## Invariants To Preserve
|
||||||
|
|
||||||
|
- Metadata is the preferred formatter input.
|
||||||
|
- Prompt Builder should output structured rows even if raw prompt text is rough.
|
||||||
|
- Krea should fix prose and semantic action readability, not category selection.
|
||||||
|
- SDXL should produce tag-style output and preserve model triggers as requested.
|
||||||
|
- Naturalizer should output training-friendly captions without changing the
|
||||||
|
selected content.
|
||||||
|
- Generic cleanup belongs in `prompt_hygiene.py`; semantic cleanup belongs in
|
||||||
|
the owning route.
|
||||||
|
|
||||||
|
## Recommended Next Passes
|
||||||
|
|
||||||
|
1. Expand `tools/prompt_smoke.py` with camera-scene, explicit nude, and
|
||||||
|
different-camera pair fixtures.
|
||||||
|
2. Split Krea action/POV/clothing helpers into separate modules.
|
||||||
|
3. Add category JSON pool reference validation to `tools/prompt_map_audit.py`.
|
||||||
|
4. Extract scene-camera adapters from `prompt_builder.py`.
|
||||||
|
5. Split `__init__.py` node classes by family after behavior is covered by smoke
|
||||||
|
checks.
|
||||||
@@ -85,6 +85,7 @@ These recipes identify the intended road before editing prompt text.
|
|||||||
| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, Krea `_hardcore_action_sentence` |
|
| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, Krea `_hardcore_action_sentence` |
|
||||||
| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `_hardcore_action_sentence` |
|
| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `_hardcore_action_sentence` |
|
||||||
| Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `_pov_hardcore_pose_sentence`, `_pov_action_phrase`, `_cast_prose` omit-label handling |
|
| Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `_pov_hardcore_pose_sentence`, `_pov_action_phrase`, `_cast_prose` omit-label handling |
|
||||||
|
| Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, Krea `_is_foreplay_text` / `_hardcore_action_sentence` |
|
||||||
| Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields |
|
| Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields |
|
||||||
| Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch |
|
| Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch |
|
||||||
| Change only outfit/clothing | Character clothing or category content route | Keep `person_seed`, `scene_seed`, `pose_seed`; change `content_seed`; slot `softcore_outfit` overrides Insta/OF outfit | `SxCP Character Clothing`, `INSTA_OF_SOFTCORE_OUTFITS`, category item templates |
|
| Change only outfit/clothing | Character clothing or category content route | Keep `person_seed`, `scene_seed`, `pose_seed`; change `content_seed`; slot `softcore_outfit` overrides Insta/OF outfit | `SxCP Character Clothing`, `INSTA_OF_SOFTCORE_OUTFITS`, category item templates |
|
||||||
@@ -205,7 +206,7 @@ This table is the first stop when the selected content is wrong.
|
|||||||
| `default_categories.json` men casual subcategories | Male casual outfit/items and men-specific casual pools | Same as above | Medium if men are part of a mixed cast and clothing detail is too strong |
|
| `default_categories.json` men casual subcategories | Male casual outfit/items and men-specific casual pools | Same as above | Medium if men are part of a mixed cast and clothing detail is too strong |
|
||||||
| `default_categories.json` couple casual subcategories | Couple outfit/action-ish soft poses and couple pools | Same as above | Medium because labels and partner styling can duplicate pair mode |
|
| `default_categories.json` couple casual subcategories | Couple outfit/action-ish soft poses and couple pools | Same as above | Medium because labels and partner styling can duplicate pair mode |
|
||||||
| `erotic_clothes.json` | Provocative/erotic clothing categories and softcore creator scenes | `content`, `scene`, `expression`, `composition` | Medium because nude/implied-nude wording can conflict with clothes |
|
| `erotic_clothes.json` | Provocative/erotic clothing categories and softcore creator scenes | `content`, `scene`, `expression`, `composition` | Medium because nude/implied-nude wording can conflict with clothes |
|
||||||
| `sexual_poses.json` foreplay/oral/outercourse/penetration/etc. | Hardcore action item templates, role graphs, axis values, hardcore pool references | `pose` for pose-content route, also `role`; sometimes `content` aliases matter | High because Krea2 rewrites action and POV position text |
|
| `sexual_poses.json` foreplay/interaction/manual/oral/outercourse/penetration/etc. | Hardcore action and porn-scene interaction templates, role graphs, axis values, hardcore pool references | `pose` for pose-content route, also `role`; sometimes `content` aliases matter | High because Krea2 rewrites action and POV position text |
|
||||||
| `location_pools.json` | Reusable scene pools and legacy scene extensions | `scene` | Medium when a camera-aware adapter changes scene/composition wording |
|
| `location_pools.json` | Reusable scene pools and legacy scene extensions | `scene` | Medium when a camera-aware adapter changes scene/composition wording |
|
||||||
| `expression_composition_pools.json` | Reusable expressions and framing/composition pools | `expression`, `composition` | Medium because formatter may label or suppress expressions |
|
| `expression_composition_pools.json` | Reusable expressions and framing/composition pools | `expression`, `composition` | Medium because formatter may label or suppress expressions |
|
||||||
| `generate_prompt_batches.py` legacy pools | Built-in generator clothing, pose, expression, scene, composition lists | Main row seed plus axis config through legacy adapter | Medium because legacy prompt format is field-label heavy |
|
| `generate_prompt_batches.py` legacy pools | Built-in generator clothing, pose, expression, scene, composition lists | Main row seed plus axis config through legacy adapter | Medium because legacy prompt format is field-label heavy |
|
||||||
@@ -268,6 +269,10 @@ Edit targets:
|
|||||||
|
|
||||||
- Normal pose pools: legacy `g.POSES`, `g.EVOCATIVE_POSES`, or JSON `poses`.
|
- Normal pose pools: legacy `g.POSES`, `g.EVOCATIVE_POSES`, or JSON `poses`.
|
||||||
- Hardcore positions/actions: `categories/sexual_poses.json`.
|
- Hardcore positions/actions: `categories/sexual_poses.json`.
|
||||||
|
- Hardcore interaction beats: `categories/sexual_poses.json` subcategories
|
||||||
|
`foreplay_teasing`, `manual_stimulation`, `body_worship_touching`,
|
||||||
|
`clothing_position_transitions`, `dominant_guidance`,
|
||||||
|
`camera_performance`, `group_coordination`, and `aftercare_cleanup`.
|
||||||
- Position filtering UI: `build_hardcore_position_pool_json`,
|
- Position filtering UI: `build_hardcore_position_pool_json`,
|
||||||
`build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`.
|
`build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`.
|
||||||
- Krea2 action rewrite, POV position rewrite, cleanup: `krea_formatter.py`.
|
- Krea2 action rewrite, POV position rewrite, cleanup: `krea_formatter.py`.
|
||||||
@@ -358,8 +363,8 @@ Hardcore row:
|
|||||||
|
|
||||||
- Category is always `Hardcore sexual poses`.
|
- Category is always `Hardcore sexual poses`.
|
||||||
- Cast count comes from `SxCP Insta/OF Options`.
|
- Cast count comes from `SxCP Insta/OF Options`.
|
||||||
- Position/action can be constrained by `SxCP Hardcore Position Pool` and
|
- Position/action/interaction can be constrained by `SxCP Hardcore Position
|
||||||
`SxCP Hardcore Action Filter`.
|
Pool` and `SxCP Hardcore Action Filter`.
|
||||||
- Clothing comes from character slot hardcore clothing first, then fallback
|
- Clothing comes from character slot hardcore clothing first, then fallback
|
||||||
`hardcore_clothing_continuity`.
|
`hardcore_clothing_continuity`.
|
||||||
- Men receive default hardcore clothing if visible and not configured.
|
- Men receive default hardcore clothing if visible and not configured.
|
||||||
@@ -410,7 +415,7 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
|||||||
| `character_cast_slots` | Character slot chain | POV/camera/formatters | Raw configured slots. |
|
| `character_cast_slots` | Character slot chain | POV/camera/formatters | Raw configured slots. |
|
||||||
| `character_slot_status`, `character_profile_status` | Character/profile application | Debug | Explains whether slot/profile was applied or skipped. |
|
| `character_slot_status`, `character_profile_status` | Character/profile application | Debug | Explains whether slot/profile was applied or skipped. |
|
||||||
| `pov_character_labels` | Character slot presence mode | Krea/prompt/camera | Labels omitted from visible cast and rewritten as first-person POV. |
|
| `pov_character_labels` | Character slot presence mode | Krea/prompt/camera | Labels omitted from visible cast and rewritten as first-person POV. |
|
||||||
| `hardcore_position_config` | Hardcore position/filter nodes | Debug | Active hardcore family/position/action constraints. |
|
| `hardcore_position_config` | Hardcore position/filter nodes | Debug | Active hardcore family/position/action/interaction constraints, including `interaction_only` and `manual_only`. |
|
||||||
| `negative_prompt` | Category/pair/default negative route | Formatter output | Base negative text before formatter extras. |
|
| `negative_prompt` | Category/pair/default negative route | Formatter output | Base negative text before formatter extras. |
|
||||||
| `trigger` | Builder input | Formatter/fallback/debug | Active trigger after fallback to default. |
|
| `trigger` | Builder input | Formatter/fallback/debug | Active trigger after fallback to default. |
|
||||||
|
|
||||||
@@ -453,11 +458,40 @@ flowchart TD
|
|||||||
What each part owns:
|
What each part owns:
|
||||||
|
|
||||||
- `sexual_poses.json`: available positions, families, action templates, role
|
- `sexual_poses.json`: available positions, families, action templates, role
|
||||||
graph templates, and action-specific pool references.
|
graph templates, interaction templates, and action-specific pool references.
|
||||||
- `prompt_builder.py`: filters which templates/axes remain available.
|
- `prompt_builder.py`: filters which templates/axes remain available.
|
||||||
- `krea_formatter.py`: rewrites the selected action into model-readable prose,
|
- `krea_formatter.py`: rewrites the selected action into model-readable prose,
|
||||||
including POV variants and cleanup.
|
including POV variants and cleanup.
|
||||||
|
|
||||||
|
Current broad hardcore families:
|
||||||
|
|
||||||
|
| Family / focus | Subcategories |
|
||||||
|
| --- | --- |
|
||||||
|
| `penetrative` / `penetration_only` | `penetrative_sex` |
|
||||||
|
| `foreplay` / `foreplay_only` | `foreplay_teasing` |
|
||||||
|
| `interaction` / `interaction_only` | `foreplay_teasing`, `body_worship_touching`, `clothing_position_transitions`, `dominant_guidance`, `camera_performance`, `group_coordination`, `aftercare_cleanup` |
|
||||||
|
| `manual` / `manual_only` | `manual_stimulation` |
|
||||||
|
| `oral` / `oral_only` | `oral_sex` |
|
||||||
|
| `outercourse` / `outercourse_only` | `outercourse_sex`, `manual_stimulation` |
|
||||||
|
| `anal` / `anal_only` | `anal_double_penetration` |
|
||||||
|
| `climax` / `climax_only` | `cumshot_climax` |
|
||||||
|
| `threesome` / `threesome_only` | `threesomes` |
|
||||||
|
| `group` / `group_only` | `group_sex_orgy` |
|
||||||
|
|
||||||
|
The action filter also has independent gates for toys, double-contact,
|
||||||
|
penetration, foreplay, interaction, manual stimulation, oral, outercourse, anal,
|
||||||
|
and climax. Keep `allow_interaction=true` when using the broader interaction
|
||||||
|
family; keep `allow_manual=true` when manual stimulation should remain possible.
|
||||||
|
`allow_anal=false` blocks anal-sex wording, not ordinary ass-touching interaction
|
||||||
|
phrases such as ass grabbing or body worship.
|
||||||
|
|
||||||
|
Interaction selector keys include kissing/caressing/undressing, body worship,
|
||||||
|
nipple play, ass grabbing, thigh kissing, hair holding, wrist pinning, dirty
|
||||||
|
talk, position transitions, guided positioning, camera presentation, watching,
|
||||||
|
aftercare, cleanup, fingering, clit rubbing, and mutual masturbation. These keys
|
||||||
|
are still routed through `hardcore_position_config.positions`, so they are
|
||||||
|
controlled by the same `pose`/`role` debug path as other hardcore action keys.
|
||||||
|
|
||||||
If one action keeps recurring, inspect:
|
If one action keeps recurring, inspect:
|
||||||
|
|
||||||
1. The enabled position family/action flags.
|
1. The enabled position family/action flags.
|
||||||
@@ -571,6 +605,25 @@ Naturalizer field consumption:
|
|||||||
| Insta/OF pair | `softcore_row`, `hardcore_row`, pair options and continuity | `_insta_pair_from_row` |
|
| Insta/OF pair | `softcore_row`, `hardcore_row`, pair options and continuity | `_insta_pair_from_row` |
|
||||||
| Text fallback | `caption` or `prompt` text | `_text_to_prose` |
|
| Text fallback | `caption` or `prompt` text | `_text_to_prose` |
|
||||||
|
|
||||||
|
### Final Text Hygiene
|
||||||
|
|
||||||
|
`prompt_hygiene.py` owns route-agnostic final cleanup. It is intentionally
|
||||||
|
small: whitespace, punctuation, empty field labels, adjacent duplicate
|
||||||
|
sentences, repeated trigger prefixes, duplicate comma-list items, and dangling
|
||||||
|
connectors.
|
||||||
|
|
||||||
|
It is called from:
|
||||||
|
|
||||||
|
- `prompt_builder.build_prompt`
|
||||||
|
- `prompt_builder.build_insta_of_pair`
|
||||||
|
- `krea_formatter.format_krea2_prompt`
|
||||||
|
- `sdxl_formatter.format_sdxl_prompt`
|
||||||
|
- `caption_naturalizer.naturalize_caption`
|
||||||
|
|
||||||
|
Do not put semantic fixes in `prompt_hygiene.py`. Sexual action readability,
|
||||||
|
POV geometry, clothing state, Krea prose, SDXL weighting, and training-caption
|
||||||
|
policy still belong to their route-specific owner.
|
||||||
|
|
||||||
## Utility / Workflow Nodes
|
## Utility / Workflow Nodes
|
||||||
|
|
||||||
These do not own prompt pool wording, but they affect execution and review:
|
These do not own prompt pool wording, but they affect execution and review:
|
||||||
@@ -582,6 +635,7 @@ These do not own prompt pool wording, but they affect execution and review:
|
|||||||
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
||||||
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
||||||
| SDXL bucket size | `SxCPSDXLBucketSize` in `__init__.py` | Random/fixed SDXL bucket width and height selection. |
|
| SDXL bucket size | `SxCPSDXLBucketSize` in `__init__.py` | Random/fixed SDXL bucket width and height selection. |
|
||||||
|
| Krea2 resolution selector | `SxCPKrea2ResolutionSelector` in `__init__.py` | Krea-compatible width/height and API aspect/resolution helper. |
|
||||||
|
|
||||||
## Drift Audit Helper
|
## Drift Audit Helper
|
||||||
|
|
||||||
@@ -601,6 +655,28 @@ The script does not import ComfyUI. It parses the repo and prints:
|
|||||||
Use its output to spot doc drift after adding a new node or pool. If a new node
|
Use its output to spot doc drift after adding a new node or pool. If a new node
|
||||||
or pool appears there but not in this map, update the relevant route table.
|
or pool appears there but not in this map, update the relevant route table.
|
||||||
|
|
||||||
|
## Behavioral Smoke Helper
|
||||||
|
|
||||||
|
Route behavior should be checked when changing prompt generation, pair assembly,
|
||||||
|
formatter metadata parsing, trigger handling, expression disabling, or scene
|
||||||
|
continuity. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/prompt_smoke.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The script does not import ComfyUI. It builds representative metadata rows and
|
||||||
|
pair metadata through the core Python APIs, then verifies:
|
||||||
|
|
||||||
|
- generated rows keep prompt, negative prompt, scene, composition, action item,
|
||||||
|
and role graph metadata populated;
|
||||||
|
- Krea2, SDXL, and natural caption routes use metadata instead of text fallback;
|
||||||
|
- SDXL and caption trigger handling keeps one trigger;
|
||||||
|
- negative prompts do not duplicate comma-list items;
|
||||||
|
- same-room Insta/OF continuity keeps prompt text and `hardcore_row.scene_text`
|
||||||
|
synchronized;
|
||||||
|
- expression-disabled rows do not fall back to generated expression text.
|
||||||
|
|
||||||
## Editing Cheatsheet
|
## Editing Cheatsheet
|
||||||
|
|
||||||
| Symptom | First file/function to inspect |
|
| Symptom | First file/function to inspect |
|
||||||
@@ -614,12 +690,14 @@ or pool appears there but not in this map, update the relevant route table.
|
|||||||
| Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. |
|
| Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. |
|
||||||
| Expression appears when disabled | `_disable_row_expression`, formatter expression extraction. |
|
| Expression appears when disabled | `_disable_row_expression`, formatter expression extraction. |
|
||||||
| Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `_apply_hardcore_position_config_to_subcategory`. |
|
| Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `_apply_hardcore_position_config_to_subcategory`. |
|
||||||
|
| Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and Krea `_is_foreplay_text` / `_hardcore_pose_anchor`. |
|
||||||
| Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. |
|
| Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. |
|
||||||
| Krea2 hardcore prompt position is vague | `_hardcore_action_sentence` or `_pov_hardcore_pose_sentence`. |
|
| Krea2 hardcore prompt position is vague | `_hardcore_action_sentence` or `_pov_hardcore_pose_sentence`. |
|
||||||
| Man appears described in POV | POV labels, `_cast_prose` omit labels, `_pov_action_phrase`. |
|
| Man appears described in POV | POV labels, `_cast_prose` omit labels, `_pov_action_phrase`. |
|
||||||
| Camera prompt missing from Krea2 | Row `camera_directive` / `camera_scene_directive`, then Krea `_camera_phrase`. |
|
| Camera prompt missing from Krea2 | Row `camera_directive` / `camera_scene_directive`, then Krea `_camera_phrase`. |
|
||||||
| Trigger missing in Krea2 fallback | `format_krea2_prompt` preserve-trigger fallback behavior. |
|
| Trigger missing in Krea2 fallback | `format_krea2_prompt` preserve-trigger fallback behavior. |
|
||||||
| SDXL tags too weak/wrong style | `sdxl_formatter.py` presets and `_row_core_tags` / `_soft_tags` / `_hard_tags`. |
|
| SDXL tags too weak/wrong style | `sdxl_formatter.py` presets and `_row_core_tags` / `_soft_tags` / `_hard_tags`. |
|
||||||
|
| Duplicate punctuation, empty labels, repeated trigger, repeated tag item | `prompt_hygiene.py`, then the route-specific formatter if the repeated content is semantic. |
|
||||||
| Saved profile does not match liked character | Profile save/load path and whether the saved input is row metadata or regenerated slot config. |
|
| Saved profile does not match liked character | Profile save/load path and whether the saved input is row metadata or regenerated slot config. |
|
||||||
| Accumulator preview behavior wrong | `loop_nodes.py` accumulator methods and `web/accumulator_preview.js`. |
|
| Accumulator preview behavior wrong | `loop_nodes.py` accumulator methods and `web/accumulator_preview.js`. |
|
||||||
|
|
||||||
|
|||||||
+70
-7
@@ -4,6 +4,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text
|
||||||
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
from prompt_hygiene import sanitize_negative_text, sanitize_prose_text
|
||||||
|
|
||||||
|
|
||||||
TRIGGER_CANDIDATES = (
|
TRIGGER_CANDIDATES = (
|
||||||
"sxcpinup_coloredpencil",
|
"sxcpinup_coloredpencil",
|
||||||
@@ -839,6 +844,17 @@ def _is_outercourse_text(*parts: Any) -> bool:
|
|||||||
"hand stroking",
|
"hand stroking",
|
||||||
"hand wraps around",
|
"hand wraps around",
|
||||||
"manual stimulation",
|
"manual stimulation",
|
||||||
|
"fingering",
|
||||||
|
"fingers inside",
|
||||||
|
"fingers in pussy",
|
||||||
|
"hand on pussy",
|
||||||
|
"fingers on pussy",
|
||||||
|
"fingers sliding against the pussy",
|
||||||
|
"open-thigh manual",
|
||||||
|
"clit rubbing",
|
||||||
|
"clit",
|
||||||
|
"clitoris",
|
||||||
|
"mutual masturbation",
|
||||||
"footjob",
|
"footjob",
|
||||||
"soles wrap around",
|
"soles wrap around",
|
||||||
"soles",
|
"soles",
|
||||||
@@ -904,6 +920,51 @@ def _is_foreplay_text(*parts: Any) -> bool:
|
|||||||
"pulling clothing",
|
"pulling clothing",
|
||||||
"sliding straps",
|
"sliding straps",
|
||||||
"unbuttoning",
|
"unbuttoning",
|
||||||
|
"body worship",
|
||||||
|
"nipple",
|
||||||
|
"mouth on skin",
|
||||||
|
"kissing down",
|
||||||
|
"ass grabbing",
|
||||||
|
"gripping the ass",
|
||||||
|
"thigh kissing",
|
||||||
|
"inner thighs",
|
||||||
|
"hair held",
|
||||||
|
"holding hair",
|
||||||
|
"hair pulled back",
|
||||||
|
"wrist",
|
||||||
|
"wrists",
|
||||||
|
"pinning",
|
||||||
|
"guided",
|
||||||
|
"guiding",
|
||||||
|
"turning the body",
|
||||||
|
"position transition",
|
||||||
|
"pulling onto the bed",
|
||||||
|
"lifting and spreading",
|
||||||
|
"spreading thighs",
|
||||||
|
"dirty talk",
|
||||||
|
"whispering",
|
||||||
|
"camera performance",
|
||||||
|
"presented directly to the camera",
|
||||||
|
"present her body",
|
||||||
|
"showing to camera",
|
||||||
|
"spread open for the camera",
|
||||||
|
"watching partner",
|
||||||
|
"waiting turn",
|
||||||
|
"group coordination",
|
||||||
|
"aftercare",
|
||||||
|
"cleanup",
|
||||||
|
"wiping",
|
||||||
|
"towel",
|
||||||
|
"post-sex",
|
||||||
|
"fingering",
|
||||||
|
"fingers inside",
|
||||||
|
"hand on pussy",
|
||||||
|
"fingers on pussy",
|
||||||
|
"clit rubbing",
|
||||||
|
"clit",
|
||||||
|
"clitoris",
|
||||||
|
"manual stimulation",
|
||||||
|
"mutual masturbation",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2622,20 +2683,21 @@ def format_krea2_prompt(
|
|||||||
|
|
||||||
if row and row.get("mode") == "Insta/OF":
|
if row and row.get("mode") == "Insta/OF":
|
||||||
soft_prompt, soft_negative, hard_prompt, hard_negative = _insta_pair_to_krea(row, detail_level, style_mode)
|
soft_prompt, soft_negative, hard_prompt, hard_negative = _insta_pair_to_krea(row, detail_level, style_mode)
|
||||||
selected = hard_prompt if target == "hardcore" else soft_prompt if target == "softcore" else soft_prompt
|
|
||||||
selected_negative = hard_negative if target == "hardcore" else soft_negative
|
|
||||||
if extra_positive.strip():
|
if extra_positive.strip():
|
||||||
selected = f"{selected.rstrip()} {extra_positive.strip()}"
|
|
||||||
soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}"
|
soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}"
|
||||||
hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}"
|
hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}"
|
||||||
negative = _combine_negative(selected_negative, negative_prompt, extra_negative)
|
soft_prompt = sanitize_prose_text(soft_prompt, triggers=TRIGGER_CANDIDATES)
|
||||||
|
hard_prompt = sanitize_prose_text(hard_prompt, triggers=TRIGGER_CANDIDATES)
|
||||||
|
selected = hard_prompt if target == "hardcore" else soft_prompt if target == "softcore" else soft_prompt
|
||||||
|
selected_negative = hard_negative if target == "hardcore" else soft_negative
|
||||||
|
negative = sanitize_negative_text(_combine_negative(selected_negative, negative_prompt, extra_negative))
|
||||||
return {
|
return {
|
||||||
"krea_prompt": selected,
|
"krea_prompt": selected,
|
||||||
"negative_prompt": negative,
|
"negative_prompt": negative,
|
||||||
"krea_softcore_prompt": soft_prompt,
|
"krea_softcore_prompt": soft_prompt,
|
||||||
"krea_hardcore_prompt": hard_prompt,
|
"krea_hardcore_prompt": hard_prompt,
|
||||||
"softcore_negative_prompt": _combine_negative(soft_negative, extra_negative),
|
"softcore_negative_prompt": sanitize_negative_text(_combine_negative(soft_negative, extra_negative)),
|
||||||
"hardcore_negative_prompt": _combine_negative(hard_negative, extra_negative),
|
"hardcore_negative_prompt": sanitize_negative_text(_combine_negative(hard_negative, extra_negative)),
|
||||||
"method": f"{method}:krea2(insta_of_pair)",
|
"method": f"{method}:krea2(insta_of_pair)",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2648,7 +2710,8 @@ def format_krea2_prompt(
|
|||||||
|
|
||||||
if extra_positive.strip():
|
if extra_positive.strip():
|
||||||
prompt = f"{prompt.rstrip()} {extra_positive.strip()}"
|
prompt = f"{prompt.rstrip()} {extra_positive.strip()}"
|
||||||
negative = _combine_negative(extracted_negative, negative_prompt, extra_negative)
|
prompt = sanitize_prose_text(prompt, triggers=TRIGGER_CANDIDATES)
|
||||||
|
negative = sanitize_negative_text(_combine_negative(extracted_negative, negative_prompt, extra_negative))
|
||||||
return {
|
return {
|
||||||
"krea_prompt": prompt,
|
"krea_prompt": prompt,
|
||||||
"negative_prompt": negative,
|
"negative_prompt": negative,
|
||||||
|
|||||||
+296
-12
@@ -10,8 +10,18 @@ from typing import Any, Callable
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
from . import generate_prompt_batches as g
|
from . import generate_prompt_batches as g
|
||||||
|
from .prompt_hygiene import (
|
||||||
|
sanitize_caption_text,
|
||||||
|
sanitize_negative_text,
|
||||||
|
sanitize_prompt_text,
|
||||||
|
)
|
||||||
except ImportError: # Allows local smoke tests with `python -c`.
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
import generate_prompt_batches as g
|
import generate_prompt_batches as g
|
||||||
|
from prompt_hygiene import (
|
||||||
|
sanitize_caption_text,
|
||||||
|
sanitize_negative_text,
|
||||||
|
sanitize_prompt_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
ROOT_DIR = Path(__file__).resolve().parent
|
ROOT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -300,6 +310,8 @@ HARDCORE_POSITION_FAMILY_CHOICES = [
|
|||||||
"any",
|
"any",
|
||||||
"penetrative",
|
"penetrative",
|
||||||
"foreplay",
|
"foreplay",
|
||||||
|
"interaction",
|
||||||
|
"manual",
|
||||||
"oral",
|
"oral",
|
||||||
"outercourse",
|
"outercourse",
|
||||||
"anal",
|
"anal",
|
||||||
@@ -311,6 +323,8 @@ HARDCORE_POSITION_FOCUS_CHOICES = [
|
|||||||
"keep_pool",
|
"keep_pool",
|
||||||
"penetration_only",
|
"penetration_only",
|
||||||
"foreplay_only",
|
"foreplay_only",
|
||||||
|
"interaction_only",
|
||||||
|
"manual_only",
|
||||||
"oral_only",
|
"oral_only",
|
||||||
"outercourse_only",
|
"outercourse_only",
|
||||||
"anal_only",
|
"anal_only",
|
||||||
@@ -341,6 +355,22 @@ HARDCORE_POSITION_KEY_CHOICES = [
|
|||||||
"breast_touch",
|
"breast_touch",
|
||||||
"face_touch",
|
"face_touch",
|
||||||
"undressing",
|
"undressing",
|
||||||
|
"body_worship",
|
||||||
|
"nipple_play",
|
||||||
|
"ass_grab",
|
||||||
|
"thigh_kissing",
|
||||||
|
"hair_holding",
|
||||||
|
"wrist_pinning",
|
||||||
|
"dirty_talk",
|
||||||
|
"position_transition",
|
||||||
|
"guided_positioning",
|
||||||
|
"camera_showing",
|
||||||
|
"watching",
|
||||||
|
"aftercare",
|
||||||
|
"cleanup",
|
||||||
|
"fingering",
|
||||||
|
"clit_rubbing",
|
||||||
|
"mutual_masturbation",
|
||||||
"boobjob",
|
"boobjob",
|
||||||
"testicle_sucking",
|
"testicle_sucking",
|
||||||
"penis_licking",
|
"penis_licking",
|
||||||
@@ -353,17 +383,34 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
|||||||
"any": [
|
"any": [
|
||||||
"penetrative_sex",
|
"penetrative_sex",
|
||||||
"foreplay_teasing",
|
"foreplay_teasing",
|
||||||
|
"body_worship_touching",
|
||||||
|
"clothing_position_transitions",
|
||||||
|
"dominant_guidance",
|
||||||
|
"camera_performance",
|
||||||
|
"manual_stimulation",
|
||||||
"oral_sex",
|
"oral_sex",
|
||||||
"outercourse_sex",
|
"outercourse_sex",
|
||||||
"anal_double_penetration",
|
"anal_double_penetration",
|
||||||
"threesomes",
|
"threesomes",
|
||||||
|
"group_coordination",
|
||||||
"group_sex_orgy",
|
"group_sex_orgy",
|
||||||
"cumshot_climax",
|
"cumshot_climax",
|
||||||
|
"aftercare_cleanup",
|
||||||
],
|
],
|
||||||
"penetrative": ["penetrative_sex"],
|
"penetrative": ["penetrative_sex"],
|
||||||
"foreplay": ["foreplay_teasing"],
|
"foreplay": ["foreplay_teasing"],
|
||||||
|
"interaction": [
|
||||||
|
"foreplay_teasing",
|
||||||
|
"body_worship_touching",
|
||||||
|
"clothing_position_transitions",
|
||||||
|
"dominant_guidance",
|
||||||
|
"camera_performance",
|
||||||
|
"group_coordination",
|
||||||
|
"aftercare_cleanup",
|
||||||
|
],
|
||||||
|
"manual": ["manual_stimulation"],
|
||||||
"oral": ["oral_sex"],
|
"oral": ["oral_sex"],
|
||||||
"outercourse": ["outercourse_sex"],
|
"outercourse": ["outercourse_sex", "manual_stimulation"],
|
||||||
"anal": ["anal_double_penetration"],
|
"anal": ["anal_double_penetration"],
|
||||||
"climax": ["cumshot_climax"],
|
"climax": ["cumshot_climax"],
|
||||||
"threesome": ["threesomes"],
|
"threesome": ["threesomes"],
|
||||||
@@ -392,6 +439,22 @@ HARDCORE_POSITION_KEY_MATCHES = {
|
|||||||
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
|
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
|
||||||
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
|
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
|
||||||
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
|
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
|
||||||
|
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
|
||||||
|
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
|
||||||
|
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
|
||||||
|
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
|
||||||
|
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
|
||||||
|
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
|
||||||
|
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
|
||||||
|
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
|
||||||
|
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
|
||||||
|
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
|
||||||
|
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
|
||||||
|
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
|
||||||
|
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
|
||||||
|
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
|
||||||
|
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
|
||||||
|
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
|
||||||
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
|
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
|
||||||
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
|
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
|
||||||
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
|
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
|
||||||
@@ -400,7 +463,23 @@ HARDCORE_POSITION_KEY_MATCHES = {
|
|||||||
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
|
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
|
||||||
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
|
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
|
||||||
}
|
}
|
||||||
HARDCORE_POSITION_AXIS_KEYS = {"position", "body_position", "body_arrangement", "arrangement"}
|
HARDCORE_POSITION_AXIS_KEYS = {
|
||||||
|
"position",
|
||||||
|
"body_position",
|
||||||
|
"body_arrangement",
|
||||||
|
"arrangement",
|
||||||
|
"tease_act",
|
||||||
|
"touch_detail",
|
||||||
|
"manual_act",
|
||||||
|
"manual_detail",
|
||||||
|
"worship_act",
|
||||||
|
"transition_act",
|
||||||
|
"control_act",
|
||||||
|
"performance_act",
|
||||||
|
"coordination_act",
|
||||||
|
"aftercare_act",
|
||||||
|
"cleanup_detail",
|
||||||
|
}
|
||||||
CAMERA_ORBIT_FRAMING_CHOICES = [
|
CAMERA_ORBIT_FRAMING_CHOICES = [
|
||||||
"from_zoom",
|
"from_zoom",
|
||||||
"wide",
|
"wide",
|
||||||
@@ -2347,6 +2426,8 @@ def _empty_hardcore_position_config() -> dict[str, Any]:
|
|||||||
"allow_double": True,
|
"allow_double": True,
|
||||||
"allow_penetration": True,
|
"allow_penetration": True,
|
||||||
"allow_foreplay": True,
|
"allow_foreplay": True,
|
||||||
|
"allow_interaction": True,
|
||||||
|
"allow_manual": True,
|
||||||
"allow_oral": True,
|
"allow_oral": True,
|
||||||
"allow_outercourse": True,
|
"allow_outercourse": True,
|
||||||
"allow_anal": True,
|
"allow_anal": True,
|
||||||
@@ -2376,6 +2457,8 @@ def _parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[
|
|||||||
"allow_double",
|
"allow_double",
|
||||||
"allow_penetration",
|
"allow_penetration",
|
||||||
"allow_foreplay",
|
"allow_foreplay",
|
||||||
|
"allow_interaction",
|
||||||
|
"allow_manual",
|
||||||
"allow_oral",
|
"allow_oral",
|
||||||
"allow_outercourse",
|
"allow_outercourse",
|
||||||
"allow_anal",
|
"allow_anal",
|
||||||
@@ -2401,6 +2484,8 @@ def _hardcore_position_summary(config: dict[str, Any]) -> str:
|
|||||||
("allow_double", "double"),
|
("allow_double", "double"),
|
||||||
("allow_penetration", "penetration"),
|
("allow_penetration", "penetration"),
|
||||||
("allow_foreplay", "foreplay"),
|
("allow_foreplay", "foreplay"),
|
||||||
|
("allow_interaction", "interaction"),
|
||||||
|
("allow_manual", "manual"),
|
||||||
("allow_oral", "oral"),
|
("allow_oral", "oral"),
|
||||||
("allow_outercourse", "outercourse"),
|
("allow_outercourse", "outercourse"),
|
||||||
("allow_anal", "anal"),
|
("allow_anal", "anal"),
|
||||||
@@ -2446,6 +2531,8 @@ def build_hardcore_action_filter_json(
|
|||||||
allow_double: bool = False,
|
allow_double: bool = False,
|
||||||
allow_penetration: bool = True,
|
allow_penetration: bool = True,
|
||||||
allow_foreplay: bool = True,
|
allow_foreplay: bool = True,
|
||||||
|
allow_interaction: bool = True,
|
||||||
|
allow_manual: bool = True,
|
||||||
allow_oral: bool = True,
|
allow_oral: bool = True,
|
||||||
allow_outercourse: bool = True,
|
allow_outercourse: bool = True,
|
||||||
allow_anal: bool = True,
|
allow_anal: bool = True,
|
||||||
@@ -2457,6 +2544,8 @@ def build_hardcore_action_filter_json(
|
|||||||
focus_family = {
|
focus_family = {
|
||||||
"penetration_only": "penetrative",
|
"penetration_only": "penetrative",
|
||||||
"foreplay_only": "foreplay",
|
"foreplay_only": "foreplay",
|
||||||
|
"interaction_only": "interaction",
|
||||||
|
"manual_only": "manual",
|
||||||
"oral_only": "oral",
|
"oral_only": "oral",
|
||||||
"outercourse_only": "outercourse",
|
"outercourse_only": "outercourse",
|
||||||
"anal_only": "anal",
|
"anal_only": "anal",
|
||||||
@@ -2470,6 +2559,8 @@ def build_hardcore_action_filter_json(
|
|||||||
config["allow_double"] = bool(allow_double)
|
config["allow_double"] = bool(allow_double)
|
||||||
config["allow_penetration"] = bool(allow_penetration)
|
config["allow_penetration"] = bool(allow_penetration)
|
||||||
config["allow_foreplay"] = bool(allow_foreplay)
|
config["allow_foreplay"] = bool(allow_foreplay)
|
||||||
|
config["allow_interaction"] = bool(allow_interaction)
|
||||||
|
config["allow_manual"] = bool(allow_manual)
|
||||||
config["allow_oral"] = bool(allow_oral)
|
config["allow_oral"] = bool(allow_oral)
|
||||||
config["allow_outercourse"] = bool(allow_outercourse)
|
config["allow_outercourse"] = bool(allow_outercourse)
|
||||||
config["allow_anal"] = bool(allow_anal)
|
config["allow_anal"] = bool(allow_anal)
|
||||||
@@ -2481,6 +2572,8 @@ def build_hardcore_action_filter_json(
|
|||||||
for enabled, family in (
|
for enabled, family in (
|
||||||
(config["allow_penetration"], "penetrative"),
|
(config["allow_penetration"], "penetrative"),
|
||||||
(config["allow_foreplay"], "foreplay"),
|
(config["allow_foreplay"], "foreplay"),
|
||||||
|
(config["allow_interaction"], "interaction"),
|
||||||
|
(config["allow_manual"], "manual"),
|
||||||
(config["allow_oral"], "oral"),
|
(config["allow_oral"], "oral"),
|
||||||
(config["allow_outercourse"], "outercourse"),
|
(config["allow_outercourse"], "outercourse"),
|
||||||
(config["allow_anal"], "anal"),
|
(config["allow_anal"], "anal"),
|
||||||
@@ -2493,6 +2586,12 @@ def build_hardcore_action_filter_json(
|
|||||||
|
|
||||||
if focus == "foreplay_only":
|
if focus == "foreplay_only":
|
||||||
config["allow_foreplay"] = True
|
config["allow_foreplay"] = True
|
||||||
|
config["allow_interaction"] = True
|
||||||
|
elif focus == "interaction_only":
|
||||||
|
config["allow_interaction"] = True
|
||||||
|
config["allow_foreplay"] = True
|
||||||
|
elif focus == "manual_only":
|
||||||
|
config["allow_manual"] = True
|
||||||
elif focus == "oral_only":
|
elif focus == "oral_only":
|
||||||
config["allow_oral"] = True
|
config["allow_oral"] = True
|
||||||
config["allow_penetration"] = False
|
config["allow_penetration"] = False
|
||||||
@@ -2530,6 +2629,20 @@ def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
|
|||||||
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
|
||||||
if not config.get("allow_foreplay", True):
|
if not config.get("allow_foreplay", True):
|
||||||
allowed.discard("foreplay_teasing")
|
allowed.discard("foreplay_teasing")
|
||||||
|
if not config.get("allow_interaction", True):
|
||||||
|
allowed.difference_update(
|
||||||
|
{
|
||||||
|
"foreplay_teasing",
|
||||||
|
"body_worship_touching",
|
||||||
|
"clothing_position_transitions",
|
||||||
|
"dominant_guidance",
|
||||||
|
"camera_performance",
|
||||||
|
"group_coordination",
|
||||||
|
"aftercare_cleanup",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if not config.get("allow_manual", True):
|
||||||
|
allowed.discard("manual_stimulation")
|
||||||
if not config.get("allow_oral", True):
|
if not config.get("allow_oral", True):
|
||||||
allowed.discard("oral_sex")
|
allowed.discard("oral_sex")
|
||||||
if not config.get("allow_outercourse", True):
|
if not config.get("allow_outercourse", True):
|
||||||
@@ -2582,7 +2695,7 @@ def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str
|
|||||||
return True
|
return True
|
||||||
if not config.get("allow_anal", True) and (
|
if not config.get("allow_anal", True) and (
|
||||||
axis_name == "anal_act"
|
axis_name == "anal_act"
|
||||||
or any(term in text for term in (" anal", "ass", "rear-entry anal"))
|
or any(term in text for term in (" anal", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
if not config.get("allow_oral", True) and (
|
if not config.get("allow_oral", True) and (
|
||||||
@@ -2626,6 +2739,68 @@ def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str
|
|||||||
)
|
)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
if not config.get("allow_interaction", True) and (
|
||||||
|
axis_name
|
||||||
|
in (
|
||||||
|
"tease_act",
|
||||||
|
"touch_detail",
|
||||||
|
"clothing_detail",
|
||||||
|
"foreplay_detail",
|
||||||
|
"face_detail",
|
||||||
|
"body_contact",
|
||||||
|
"mood_detail",
|
||||||
|
"worship_act",
|
||||||
|
"transition_act",
|
||||||
|
"control_act",
|
||||||
|
"performance_act",
|
||||||
|
"coordination_act",
|
||||||
|
"aftercare_act",
|
||||||
|
"cleanup_detail",
|
||||||
|
)
|
||||||
|
or any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"kiss",
|
||||||
|
"kissing",
|
||||||
|
"caress",
|
||||||
|
"body worship",
|
||||||
|
"nipple",
|
||||||
|
"ass grab",
|
||||||
|
"thigh",
|
||||||
|
"hair holding",
|
||||||
|
"wrists",
|
||||||
|
"dirty talk",
|
||||||
|
"whispering",
|
||||||
|
"undressing",
|
||||||
|
"position transition",
|
||||||
|
"guided",
|
||||||
|
"camera",
|
||||||
|
"watching",
|
||||||
|
"aftercare",
|
||||||
|
"cleanup",
|
||||||
|
"wiping",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
if not config.get("allow_manual", True) and (
|
||||||
|
axis_name in ("manual_act", "manual_detail")
|
||||||
|
or any(
|
||||||
|
term in text
|
||||||
|
for term in (
|
||||||
|
"fingering",
|
||||||
|
"fingers inside",
|
||||||
|
"clit",
|
||||||
|
"clitoris",
|
||||||
|
"manual stimulation",
|
||||||
|
"mutual masturbation",
|
||||||
|
"masturbating together",
|
||||||
|
"fingers on pussy",
|
||||||
|
"fingers on clit",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return True
|
||||||
if not config.get("allow_climax", True) and (
|
if not config.get("allow_climax", True) and (
|
||||||
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
|
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
|
||||||
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
|
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
|
||||||
@@ -6067,6 +6242,74 @@ def _role_graph(
|
|||||||
f"hands caressing skin while clothing is pulled aside."
|
f"hands caressing skin while clothing is pulled aside."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def interaction_text() -> str:
|
||||||
|
return " ".join(
|
||||||
|
str(part or "").lower()
|
||||||
|
for part in (
|
||||||
|
item_text,
|
||||||
|
*((item_axis_values or {}).values()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def manual_position_graph(primary: str, partner: str = "") -> str:
|
||||||
|
text = interaction_text()
|
||||||
|
if not partner:
|
||||||
|
if "mutual" in text:
|
||||||
|
return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing."
|
||||||
|
return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy."
|
||||||
|
if "mutual" in text:
|
||||||
|
return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible."
|
||||||
|
if "clit" in text or "clitoris" in text:
|
||||||
|
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers rubbing her clit as her hips tilt toward the touch."
|
||||||
|
if "toy" in text or "vibrator" in text:
|
||||||
|
return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open."
|
||||||
|
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy."
|
||||||
|
|
||||||
|
def interaction_position_graph(primary: str, partner: str, third: str = "") -> str:
|
||||||
|
text = interaction_text()
|
||||||
|
if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")):
|
||||||
|
if "cleanup" in text or "wiping" in text or "towel" in text:
|
||||||
|
return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible."
|
||||||
|
return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle."
|
||||||
|
if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")):
|
||||||
|
if third:
|
||||||
|
return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer."
|
||||||
|
return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal."
|
||||||
|
if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")):
|
||||||
|
if "ass" in text:
|
||||||
|
return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin."
|
||||||
|
if "thigh" in text:
|
||||||
|
return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place."
|
||||||
|
if "nipple" in text or "breast" in text:
|
||||||
|
return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast."
|
||||||
|
return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs."
|
||||||
|
if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")):
|
||||||
|
if "turn" in text or "rear-facing" in text:
|
||||||
|
return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose."
|
||||||
|
if "legs" in text or "thigh" in text:
|
||||||
|
return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible."
|
||||||
|
return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose."
|
||||||
|
if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")):
|
||||||
|
if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text:
|
||||||
|
return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close."
|
||||||
|
if "wrist" in text or "wrists" in text:
|
||||||
|
return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible."
|
||||||
|
if "hair" in text:
|
||||||
|
return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible."
|
||||||
|
if "thigh" in text or "spread" in text:
|
||||||
|
return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera."
|
||||||
|
return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable."
|
||||||
|
return foreplay_position_graph(primary, partner)
|
||||||
|
|
||||||
|
def group_coordination_graph(primary: str, partner: str, third: str) -> str:
|
||||||
|
observer = third or any_person({primary, partner})
|
||||||
|
text = interaction_text()
|
||||||
|
if "camera" in text or "hold" in text or "present" in text:
|
||||||
|
return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible."
|
||||||
|
if "watch" in text or "waiting" in text:
|
||||||
|
return f"{primary} is centered while {partner} touches her body and {observer} watches close beside them, hands and faces readable."
|
||||||
|
return f"{primary} is centered while {partner} touches her body and {observer} stays close as the watching or guiding partner."
|
||||||
|
|
||||||
def mentions_ass(text: str) -> bool:
|
def mentions_ass(text: str) -> bool:
|
||||||
return bool(
|
return bool(
|
||||||
re.search(
|
re.search(
|
||||||
@@ -6346,6 +6589,10 @@ def _role_graph(
|
|||||||
if people_count == 1:
|
if people_count == 1:
|
||||||
solo = people[0]
|
solo = people[0]
|
||||||
if women_count == 1:
|
if women_count == 1:
|
||||||
|
if "manual_stimulation" in slug:
|
||||||
|
return manual_position_graph(solo)
|
||||||
|
if "camera_performance" in slug:
|
||||||
|
return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose."
|
||||||
if "cumshot" in slug or "climax" in slug:
|
if "cumshot" in slug or "climax" in slug:
|
||||||
return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets."
|
return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets."
|
||||||
return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness."
|
return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness."
|
||||||
@@ -6357,7 +6604,16 @@ def _role_graph(
|
|||||||
a, b = _pick_distinct(rng, women, 2)
|
a, b = _pick_distinct(rng, women, 2)
|
||||||
c = any_woman({a, b}) if len(women) >= 3 else ""
|
c = any_woman({a, b}) if len(women) >= 3 else ""
|
||||||
used = {a, b}
|
used = {a, b}
|
||||||
if "foreplay" in slug:
|
if "manual_stimulation" in slug:
|
||||||
|
graph = manual_position_graph(a, b)
|
||||||
|
elif "group_coordination" in slug and c:
|
||||||
|
graph = group_coordination_graph(a, b, c)
|
||||||
|
used.add(c)
|
||||||
|
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
|
||||||
|
graph = interaction_position_graph(a, b, c)
|
||||||
|
if c and "camera_performance" in slug:
|
||||||
|
used.add(c)
|
||||||
|
elif "foreplay" in slug:
|
||||||
graph = foreplay_position_graph(a, b)
|
graph = foreplay_position_graph(a, b)
|
||||||
elif "outercourse" in slug:
|
elif "outercourse" in slug:
|
||||||
graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact."
|
graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact."
|
||||||
@@ -6379,7 +6635,14 @@ def _role_graph(
|
|||||||
a, b = _pick_distinct(rng, men, 2)
|
a, b = _pick_distinct(rng, men, 2)
|
||||||
c = any_man({a, b}) if len(men) >= 3 else ""
|
c = any_man({a, b}) if len(men) >= 3 else ""
|
||||||
used = {a, b}
|
used = {a, b}
|
||||||
if "foreplay" in slug:
|
if "manual_stimulation" in slug:
|
||||||
|
graph = f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup."
|
||||||
|
elif "group_coordination" in slug and c:
|
||||||
|
graph = group_coordination_graph(a, b, c)
|
||||||
|
used.add(c)
|
||||||
|
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
|
||||||
|
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
|
||||||
|
elif "foreplay" in slug:
|
||||||
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
|
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
|
||||||
elif "outercourse" in slug:
|
elif "outercourse" in slug:
|
||||||
graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet."
|
graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet."
|
||||||
@@ -6401,7 +6664,13 @@ def _role_graph(
|
|||||||
woman = any_woman()
|
woman = any_woman()
|
||||||
man = any_man()
|
man = any_man()
|
||||||
third = any_person({woman, man}) if people_count >= 3 else ""
|
third = any_person({woman, man}) if people_count >= 3 else ""
|
||||||
if "foreplay" in slug:
|
if "manual_stimulation" in slug:
|
||||||
|
graph = manual_position_graph(woman, man)
|
||||||
|
elif "group_coordination" in slug:
|
||||||
|
graph = group_coordination_graph(woman, man, third)
|
||||||
|
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
|
||||||
|
graph = interaction_position_graph(woman, man, third)
|
||||||
|
elif "foreplay" in slug:
|
||||||
graph = foreplay_position_graph(woman, man)
|
graph = foreplay_position_graph(woman, man)
|
||||||
elif "outercourse" in slug:
|
elif "outercourse" in slug:
|
||||||
graph = outercourse_position_graph(woman, man)
|
graph = outercourse_position_graph(woman, man)
|
||||||
@@ -7350,7 +7619,11 @@ def build_prompt(
|
|||||||
row = _apply_camera_config(row, camera_config)
|
row = _apply_camera_config(row, camera_config)
|
||||||
active_trigger = trigger.strip() or g.TRIGGER
|
active_trigger = trigger.strip() or g.TRIGGER
|
||||||
row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt))
|
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)
|
row["prompt"] = sanitize_prompt_text(row["prompt"], triggers=(active_trigger,))
|
||||||
|
row["caption"] = sanitize_caption_text(row.get("caption", ""), triggers=(active_trigger,))
|
||||||
|
row["negative_prompt"] = sanitize_negative_text(
|
||||||
|
_combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
|
||||||
|
)
|
||||||
row["trigger"] = active_trigger
|
row["trigger"] = active_trigger
|
||||||
row.setdefault("expression_intensity", expression_intensity)
|
row.setdefault("expression_intensity", expression_intensity)
|
||||||
row.setdefault("expression_intensity_source", expression_intensity_source)
|
row.setdefault("expression_intensity_source", expression_intensity_source)
|
||||||
@@ -8388,6 +8661,9 @@ def build_insta_of_pair(
|
|||||||
soft_row = _apply_coworking_composition(soft_row, soft_subject_kind)
|
soft_row = _apply_coworking_composition(soft_row, soft_subject_kind)
|
||||||
hard_row = _apply_coworking_composition(hard_row, hard_subject_kind)
|
hard_row = _apply_coworking_composition(hard_row, hard_subject_kind)
|
||||||
hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"]
|
hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"]
|
||||||
|
if hard_scene != hard_row.get("scene_text"):
|
||||||
|
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
|
||||||
|
hard_row["scene_text"] = hard_scene
|
||||||
hard_composition = _coworking_composition_prompt(hard_scene, hard_row["composition"], hard_subject_kind)
|
hard_composition = _coworking_composition_prompt(hard_scene, hard_row["composition"], hard_subject_kind)
|
||||||
if hard_composition != hard_row["composition"]:
|
if hard_composition != hard_row["composition"]:
|
||||||
hard_row["source_composition"] = hard_row.get("source_composition") or hard_row["composition"]
|
hard_row["source_composition"] = hard_row.get("source_composition") or hard_row["composition"]
|
||||||
@@ -8485,7 +8761,7 @@ def build_insta_of_pair(
|
|||||||
if "body is fully exposed" in hard_clothing_state.lower() or "bare skin unobstructed" in hard_clothing_state.lower():
|
if "body is fully exposed" in hard_clothing_state.lower() or "bare skin unobstructed" in hard_clothing_state.lower():
|
||||||
hard_scene = _body_exposure_scene_text(hard_scene)
|
hard_scene = _body_exposure_scene_text(hard_scene)
|
||||||
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
|
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
|
||||||
hard_row["scene_text"] = _body_exposure_scene_text(hard_row.get("scene_text", ""))
|
hard_row["scene_text"] = hard_scene
|
||||||
hard_detail_density = options["hardcore_detail_density"]
|
hard_detail_density = options["hardcore_detail_density"]
|
||||||
hard_detail_directive = {
|
hard_detail_directive = {
|
||||||
"compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ",
|
"compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ",
|
||||||
@@ -8535,8 +8811,10 @@ def build_insta_of_pair(
|
|||||||
|
|
||||||
soft_prompt = _insta_of_active_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt))
|
soft_prompt = _insta_of_active_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt))
|
||||||
hard_prompt = _insta_of_active_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt))
|
hard_prompt = _insta_of_active_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt))
|
||||||
soft_negative = _combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative)
|
soft_prompt = sanitize_prompt_text(soft_prompt, triggers=(active_trigger,))
|
||||||
hard_negative = _combined_negative(INSTA_OF_NEGATIVE, extra_negative)
|
hard_prompt = sanitize_prompt_text(hard_prompt, triggers=(active_trigger,))
|
||||||
|
soft_negative = sanitize_negative_text(_combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative))
|
||||||
|
hard_negative = sanitize_negative_text(_combined_negative(INSTA_OF_NEGATIVE, extra_negative))
|
||||||
soft_caption_parts = [
|
soft_caption_parts = [
|
||||||
active_trigger,
|
active_trigger,
|
||||||
"Insta/OF softcore mode",
|
"Insta/OF softcore mode",
|
||||||
@@ -8551,7 +8829,10 @@ def build_insta_of_pair(
|
|||||||
soft_row["composition"],
|
soft_row["composition"],
|
||||||
_camera_caption_text(soft_camera_config) if soft_camera_directive else "",
|
_camera_caption_text(soft_camera_config) if soft_camera_directive else "",
|
||||||
]
|
]
|
||||||
soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip())
|
soft_caption = sanitize_caption_text(
|
||||||
|
", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip()),
|
||||||
|
triggers=(active_trigger,),
|
||||||
|
)
|
||||||
hard_caption_parts = [
|
hard_caption_parts = [
|
||||||
active_trigger,
|
active_trigger,
|
||||||
"Insta/OF hardcore mode",
|
"Insta/OF hardcore mode",
|
||||||
@@ -8565,7 +8846,10 @@ def build_insta_of_pair(
|
|||||||
hard_composition,
|
hard_composition,
|
||||||
_camera_caption_text(hard_camera_config) if hard_camera_directive else "",
|
_camera_caption_text(hard_camera_config) if hard_camera_directive else "",
|
||||||
]
|
]
|
||||||
hard_caption = ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip())
|
hard_caption = sanitize_caption_text(
|
||||||
|
", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip()),
|
||||||
|
triggers=(active_trigger,),
|
||||||
|
)
|
||||||
metadata = {
|
metadata = {
|
||||||
"mode": "Insta/OF",
|
"mode": "Insta/OF",
|
||||||
"options": options,
|
"options": options,
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_FIELD_LABELS = (
|
||||||
|
"Ages",
|
||||||
|
"Body types",
|
||||||
|
"Cast",
|
||||||
|
"Cast descriptors",
|
||||||
|
"Characters",
|
||||||
|
"Scene",
|
||||||
|
"Setting",
|
||||||
|
"Pose",
|
||||||
|
"Sexual pose",
|
||||||
|
"Sexual scene",
|
||||||
|
"Facial expression",
|
||||||
|
"Facial expressions",
|
||||||
|
"Clothing",
|
||||||
|
"Erotic outfit",
|
||||||
|
"Prop/detail",
|
||||||
|
"Composition",
|
||||||
|
"Role graph",
|
||||||
|
"Camera",
|
||||||
|
"Camera control",
|
||||||
|
"Camera priority",
|
||||||
|
"Use",
|
||||||
|
"Avoid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_spacing(value: Any) -> str:
|
||||||
|
text = "" if value is None else str(value)
|
||||||
|
text = text.replace("\n", " ")
|
||||||
|
text = re.sub(r"\s+", " ", text).strip()
|
||||||
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||||
|
text = re.sub(r"([,;:]){2,}", r"\1", text)
|
||||||
|
text = re.sub(r"\.\s*\.", ".", text)
|
||||||
|
text = re.sub(r",\s*\.", ".", text)
|
||||||
|
text = re.sub(r":\s*\.", ".", text)
|
||||||
|
text = re.sub(r";\s*\.", ".", text)
|
||||||
|
text = re.sub(r"\(\s+", "(", text)
|
||||||
|
text = re.sub(r"\s+\)", ")", text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_empty_fields(text: str) -> str:
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
labels = "|".join(re.escape(label) for label in EMPTY_FIELD_LABELS)
|
||||||
|
text = re.sub(rf"\b(?:{labels})\s*:\s*[.,;]", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(rf"\b(?:{labels}):\s*(?=\.|,|;|$)", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(rf"\b(?:{labels})\.(?=\s|$)", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(rf"\b(?:{labels}):\s*(?:none|null|n/a)\b[.,;]?", "", text, flags=re.IGNORECASE)
|
||||||
|
return clean_spacing(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_dangling_connectors(text: str) -> str:
|
||||||
|
text = re.sub(r"\b(?:with|and|or|while|featuring)\s*([,.;])", r"\1", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"([,.;])\s*(?:with|and|or|while|featuring)\s*([,.;])", r"\1", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\bwith\s*,", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r",\s*and\s*\.", ".", text, flags=re.IGNORECASE)
|
||||||
|
return clean_spacing(text)
|
||||||
|
|
||||||
|
|
||||||
|
def _sentence_key(text: str, triggers: Iterable[str] = ()) -> str:
|
||||||
|
key_text = text
|
||||||
|
for trigger in triggers:
|
||||||
|
trigger = str(trigger or "").strip()
|
||||||
|
if trigger:
|
||||||
|
key_text = re.sub(rf"^{re.escape(trigger)}\s*[,.;]\s*", "", key_text, flags=re.IGNORECASE)
|
||||||
|
return re.sub(r"\W+", " ", key_text.lower()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_adjacent_sentences(text: str, triggers: Iterable[str] = ()) -> str:
|
||||||
|
parts = [part.strip() for part in re.split(r"(?<=[.!?])\s+", text) if part.strip()]
|
||||||
|
deduped: list[str] = []
|
||||||
|
previous = ""
|
||||||
|
for part in parts:
|
||||||
|
key = _sentence_key(part, triggers)
|
||||||
|
if key and key != previous:
|
||||||
|
deduped.append(part)
|
||||||
|
previous = key
|
||||||
|
return " ".join(deduped)
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_labeled_sentences(text: str) -> str:
|
||||||
|
parts = [part.strip() for part in re.split(r"(?<=[.!?])\s+", text) if part.strip()]
|
||||||
|
seen: set[tuple[str, str]] = set()
|
||||||
|
deduped: list[str] = []
|
||||||
|
for part in parts:
|
||||||
|
match = re.match(r"^([A-Za-z][A-Za-z /_-]{1,40}):\s*(.+)$", part)
|
||||||
|
if not match:
|
||||||
|
deduped.append(part)
|
||||||
|
continue
|
||||||
|
key = (match.group(1).strip().lower(), re.sub(r"\W+", " ", match.group(2).lower()).strip())
|
||||||
|
if key not in seen:
|
||||||
|
deduped.append(part)
|
||||||
|
seen.add(key)
|
||||||
|
return " ".join(deduped)
|
||||||
|
|
||||||
|
|
||||||
|
def _trigger_prefix_key(text: str, triggers: Iterable[str]) -> str:
|
||||||
|
lowered = text.lower().strip()
|
||||||
|
for trigger in triggers:
|
||||||
|
trigger = str(trigger or "").strip()
|
||||||
|
if trigger and lowered.startswith(trigger.lower()):
|
||||||
|
return trigger
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_trigger_prefix(text: str, triggers: Iterable[str]) -> str:
|
||||||
|
text = clean_spacing(text)
|
||||||
|
trigger = _trigger_prefix_key(text, triggers)
|
||||||
|
if not trigger:
|
||||||
|
return text
|
||||||
|
pattern = rf"^(?:{re.escape(trigger)}\s*[,.;]\s*)+"
|
||||||
|
return f"{trigger}, {re.sub(pattern, '', text, flags=re.IGNORECASE).strip(' ,.;')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _split_comma_items(text: str) -> list[str]:
|
||||||
|
return [part.strip(" ,.;") for part in re.split(r"\s*[,;]\s*", clean_spacing(text)) if part.strip(" ,.;")]
|
||||||
|
|
||||||
|
|
||||||
|
def dedupe_comma_list(text: Any) -> str:
|
||||||
|
items: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for item in _split_comma_items(str(text or "")):
|
||||||
|
key = re.sub(r"\W+", " ", item.lower()).strip()
|
||||||
|
if key and key not in seen:
|
||||||
|
items.append(item)
|
||||||
|
seen.add(key)
|
||||||
|
return ", ".join(items)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_prose_text(value: Any, triggers: Iterable[str] = ()) -> str:
|
||||||
|
text = clean_spacing(value)
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
text = _strip_empty_fields(text)
|
||||||
|
text = _drop_dangling_connectors(text)
|
||||||
|
text = _dedupe_labeled_sentences(text)
|
||||||
|
text = _dedupe_trigger_prefix(text, triggers)
|
||||||
|
text = _dedupe_adjacent_sentences(text, triggers)
|
||||||
|
return clean_spacing(text).strip(" ,;")
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_prompt_text(value: Any, triggers: Iterable[str] = ()) -> str:
|
||||||
|
return sanitize_prose_text(value, triggers=triggers)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_caption_text(value: Any, triggers: Iterable[str] = ()) -> str:
|
||||||
|
return sanitize_prose_text(value, triggers=triggers)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_tag_prompt(value: Any, triggers: Iterable[str] = ()) -> str:
|
||||||
|
text = clean_spacing(value)
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
trigger = _trigger_prefix_key(text, triggers)
|
||||||
|
if trigger:
|
||||||
|
text = re.sub(rf"^(?:{re.escape(trigger)}\s*[,;]\s*)+", "", text, flags=re.IGNORECASE).strip(" ,;")
|
||||||
|
return f"{trigger}, {dedupe_comma_list(text)}" if text else trigger
|
||||||
|
return dedupe_comma_list(text)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_negative_text(value: Any) -> str:
|
||||||
|
return dedupe_comma_list(value)
|
||||||
+28
-10
@@ -4,6 +4,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt
|
||||||
|
except ImportError: # Allows local smoke tests with `python -c`.
|
||||||
|
from prompt_hygiene import sanitize_negative_text, sanitize_tag_prompt
|
||||||
|
|
||||||
|
|
||||||
TRIGGER_CANDIDATES = (
|
TRIGGER_CANDIDATES = (
|
||||||
"sxcpinup_coloredpencil",
|
"sxcpinup_coloredpencil",
|
||||||
@@ -432,11 +437,14 @@ def _assemble_prompt(
|
|||||||
custom_quality: str,
|
custom_quality: str,
|
||||||
extra_positive: str,
|
extra_positive: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
return _combine_tags(
|
return sanitize_tag_prompt(
|
||||||
_style_prefix(style_preset, trigger, prepend_trigger, custom_style),
|
_combine_tags(
|
||||||
body_tags,
|
_style_prefix(style_preset, trigger, prepend_trigger, custom_style),
|
||||||
_quality_tail(quality_preset, custom_quality),
|
body_tags,
|
||||||
extra_positive,
|
_quality_tail(quality_preset, custom_quality),
|
||||||
|
extra_positive,
|
||||||
|
),
|
||||||
|
triggers=(trigger,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -504,14 +512,22 @@ def format_sdxl_prompt(
|
|||||||
extra_positive,
|
extra_positive,
|
||||||
)
|
)
|
||||||
selected = hard_prompt if target == "hardcore" else soft_prompt
|
selected = hard_prompt if target == "hardcore" else soft_prompt
|
||||||
selected_negative = row.get("hardcore_negative_prompt") if target == "hardcore" else row.get("softcore_negative_prompt")
|
selected_negative = (
|
||||||
|
row.get("hardcore_negative_prompt") if target == "hardcore" else row.get("softcore_negative_prompt")
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"sdxl_prompt": selected,
|
"sdxl_prompt": selected,
|
||||||
"negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, selected_negative, negative_prompt, extra_negative),
|
"negative_prompt": sanitize_negative_text(
|
||||||
|
_combine_negative(SDXL_DEFAULT_NEGATIVE, selected_negative, negative_prompt, extra_negative)
|
||||||
|
),
|
||||||
"sdxl_softcore_prompt": soft_prompt,
|
"sdxl_softcore_prompt": soft_prompt,
|
||||||
"sdxl_hardcore_prompt": hard_prompt,
|
"sdxl_hardcore_prompt": hard_prompt,
|
||||||
"softcore_negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("softcore_negative_prompt"), extra_negative),
|
"softcore_negative_prompt": sanitize_negative_text(
|
||||||
"hardcore_negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("hardcore_negative_prompt"), extra_negative),
|
_combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("softcore_negative_prompt"), extra_negative)
|
||||||
|
),
|
||||||
|
"hardcore_negative_prompt": sanitize_negative_text(
|
||||||
|
_combine_negative(SDXL_DEFAULT_NEGATIVE, row.get("hardcore_negative_prompt"), extra_negative)
|
||||||
|
),
|
||||||
"method": f"{method}:sdxl(insta_of_pair)",
|
"method": f"{method}:sdxl(insta_of_pair)",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +550,9 @@ def format_sdxl_prompt(
|
|||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"sdxl_prompt": prompt,
|
"sdxl_prompt": prompt,
|
||||||
"negative_prompt": _combine_negative(SDXL_DEFAULT_NEGATIVE, extracted_negative, negative_prompt, extra_negative),
|
"negative_prompt": sanitize_negative_text(
|
||||||
|
_combine_negative(SDXL_DEFAULT_NEGATIVE, extracted_negative, negative_prompt, extra_negative)
|
||||||
|
),
|
||||||
"sdxl_softcore_prompt": "",
|
"sdxl_softcore_prompt": "",
|
||||||
"sdxl_hardcore_prompt": "",
|
"sdxl_hardcore_prompt": "",
|
||||||
"softcore_negative_prompt": "",
|
"softcore_negative_prompt": "",
|
||||||
|
|||||||
@@ -0,0 +1,395 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Smoke-test core prompt routes without importing ComfyUI.
|
||||||
|
|
||||||
|
The checks here are intentionally lightweight invariants, not golden prompt
|
||||||
|
snapshots. They prove that representative rows still carry structured metadata
|
||||||
|
and that the Krea2, SDXL, and caption formatter paths consume metadata instead
|
||||||
|
of silently falling back to raw text parsing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import caption_naturalizer # noqa: E402
|
||||||
|
import krea_formatter # noqa: E402
|
||||||
|
import prompt_builder as pb # noqa: E402
|
||||||
|
import sdxl_formatter # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
Trigger = "sxcppnl7"
|
||||||
|
SdxlTrigger = "mythp0rt"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SmokeReport:
|
||||||
|
passed: list[str] = field(default_factory=list)
|
||||||
|
failed: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
def ok(self, name: str) -> None:
|
||||||
|
self.passed.append(name)
|
||||||
|
print(f"PASS {name}")
|
||||||
|
|
||||||
|
def fail(self, name: str, message: str) -> None:
|
||||||
|
detail = f"{name}: {message}"
|
||||||
|
self.failed.append(detail)
|
||||||
|
print(f"FAIL {detail}")
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_key(value: str) -> str:
|
||||||
|
return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _json(value: Any) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _expect(condition: bool, message: str) -> None:
|
||||||
|
if not condition:
|
||||||
|
raise AssertionError(message)
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_text(name: str, value: Any, min_len: int = 8) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
_expect(len(text) >= min_len, f"{name} is empty or too short")
|
||||||
|
_expect("None" not in text, f"{name} leaked None")
|
||||||
|
_expect(" " not in text, f"{name} has repeated spaces")
|
||||||
|
_expect(" ," not in text and " ." not in text, f"{name} has bad punctuation spacing")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_no_duplicate_comma_items(name: str, value: Any) -> None:
|
||||||
|
items = [_clean_key(part) for part in str(value or "").split(",")]
|
||||||
|
items = [part for part in items if part]
|
||||||
|
duplicates = sorted({part for part in items if items.count(part) > 1})
|
||||||
|
_expect(not duplicates, f"{name} has duplicate comma items: {duplicates[:5]}")
|
||||||
|
|
||||||
|
|
||||||
|
def _trigger_count(text: str, trigger: str) -> int:
|
||||||
|
return len(re.findall(rf"(?<![a-z0-9_]){re.escape(trigger)}(?![a-z0-9_])", text, flags=re.IGNORECASE))
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_trigger_once(name: str, value: Any, trigger: str) -> None:
|
||||||
|
text = str(value or "")
|
||||||
|
count = _trigger_count(text, trigger)
|
||||||
|
_expect(count == 1, f"{name} should contain trigger {trigger!r} exactly once, got {count}")
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_row_base(row: dict[str, Any], name: str) -> None:
|
||||||
|
_expect(isinstance(row, dict), f"{name} did not return a metadata row")
|
||||||
|
_expect_text(f"{name}.prompt", row.get("prompt"), 20)
|
||||||
|
_expect_text(f"{name}.negative_prompt", row.get("negative_prompt"), 8)
|
||||||
|
_expect_no_duplicate_comma_items(f"{name}.negative_prompt", row.get("negative_prompt"))
|
||||||
|
_expect(json.loads(_json(row)) == row, f"{name} is not JSON-stable")
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_custom_row(row: dict[str, Any], name: str) -> None:
|
||||||
|
_expect_row_base(row, name)
|
||||||
|
_expect(row.get("source") == "json_category", f"{name}.source should be json_category")
|
||||||
|
_expect_text(f"{name}.item", row.get("item"), 8)
|
||||||
|
_expect_text(f"{name}.scene_text", row.get("scene_text"), 8)
|
||||||
|
_expect_text(f"{name}.composition", row.get("composition"), 8)
|
||||||
|
_expect_text(f"{name}.role_graph", row.get("source_role_graph") or row.get("role_graph"), 8)
|
||||||
|
_expect(isinstance(row.get("item_axis_values"), dict), f"{name}.item_axis_values missing")
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_formatter_outputs(row: dict[str, Any], name: str, *, target: str = "auto") -> None:
|
||||||
|
metadata = _json(row)
|
||||||
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=metadata, target=target)
|
||||||
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata: {krea.get('method')}")
|
||||||
|
_expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 20)
|
||||||
|
_expect_no_duplicate_comma_items(f"{name}.krea_negative", krea.get("negative_prompt"))
|
||||||
|
|
||||||
|
sdxl = sdxl_formatter.format_sdxl_prompt(
|
||||||
|
"",
|
||||||
|
metadata_json=metadata,
|
||||||
|
target=target,
|
||||||
|
trigger=SdxlTrigger,
|
||||||
|
prepend_trigger=True,
|
||||||
|
)
|
||||||
|
_expect("metadata" in sdxl.get("method", ""), f"{name}.sdxl did not use metadata: {sdxl.get('method')}")
|
||||||
|
_expect_text(f"{name}.sdxl_prompt", sdxl.get("sdxl_prompt"), 20)
|
||||||
|
_expect_trigger_once(f"{name}.sdxl_prompt", sdxl.get("sdxl_prompt"), SdxlTrigger)
|
||||||
|
_expect_no_duplicate_comma_items(f"{name}.sdxl_negative", sdxl.get("negative_prompt"))
|
||||||
|
|
||||||
|
caption, method = caption_naturalizer.naturalize_caption(
|
||||||
|
"",
|
||||||
|
metadata_json=metadata,
|
||||||
|
trigger=Trigger,
|
||||||
|
include_trigger=True,
|
||||||
|
)
|
||||||
|
_expect("metadata" in method, f"{name}.caption did not use metadata: {method}")
|
||||||
|
_expect_text(f"{name}.caption", caption, 20)
|
||||||
|
_expect_trigger_once(f"{name}.caption", caption, Trigger)
|
||||||
|
|
||||||
|
|
||||||
|
def _character_cast(*, pov_man: bool = False) -> str:
|
||||||
|
cast = pb.build_character_slot_json(
|
||||||
|
subject_type="woman",
|
||||||
|
label="A",
|
||||||
|
age="25-year-old adult",
|
||||||
|
ethnicity="western_european",
|
||||||
|
figure="balanced",
|
||||||
|
body="slim",
|
||||||
|
descriptor_detail="full",
|
||||||
|
expression_intensity=0.65,
|
||||||
|
softcore_expression_intensity=0.45,
|
||||||
|
hardcore_expression_intensity=0.85,
|
||||||
|
)["character_cast"]
|
||||||
|
return pb.build_character_slot_json(
|
||||||
|
subject_type="man",
|
||||||
|
label="A",
|
||||||
|
age="40-year-old adult",
|
||||||
|
ethnicity="western_european",
|
||||||
|
figure="balanced",
|
||||||
|
body="average",
|
||||||
|
descriptor_detail="compact",
|
||||||
|
expression_intensity=0.55,
|
||||||
|
softcore_expression_intensity=0.35,
|
||||||
|
hardcore_expression_intensity=0.75,
|
||||||
|
presence_mode="pov" if pov_man else "visible",
|
||||||
|
character_cast=cast,
|
||||||
|
)["character_cast"]
|
||||||
|
|
||||||
|
|
||||||
|
def _action_filter(focus: str) -> str:
|
||||||
|
kwargs = {
|
||||||
|
"allow_toys": False,
|
||||||
|
"allow_double": False,
|
||||||
|
"allow_penetration": focus in ("penetration_only", "keep_pool"),
|
||||||
|
"allow_foreplay": focus in ("foreplay_only", "keep_pool"),
|
||||||
|
"allow_interaction": focus in ("interaction_only", "keep_pool"),
|
||||||
|
"allow_manual": focus in ("manual_only", "keep_pool"),
|
||||||
|
"allow_oral": focus in ("oral_only", "keep_pool"),
|
||||||
|
"allow_outercourse": focus in ("outercourse_only", "keep_pool"),
|
||||||
|
"allow_anal": focus in ("anal_only", "keep_pool"),
|
||||||
|
"allow_climax": focus in ("climax_only", "keep_pool"),
|
||||||
|
}
|
||||||
|
return pb.build_hardcore_action_filter_json(focus=focus, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt_row(
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
category: str,
|
||||||
|
subcategory: str,
|
||||||
|
seed: int,
|
||||||
|
character_cast: str = "",
|
||||||
|
women_count: int = 1,
|
||||||
|
men_count: int = 1,
|
||||||
|
hardcore_position_config: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
row = pb.build_prompt(
|
||||||
|
category=category,
|
||||||
|
subcategory=subcategory,
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=seed,
|
||||||
|
clothing="random",
|
||||||
|
ethnicity="any",
|
||||||
|
poses="random",
|
||||||
|
backside_bias=0.35,
|
||||||
|
figure="random",
|
||||||
|
no_plus_women=False,
|
||||||
|
no_black=False,
|
||||||
|
minimal_clothing_ratio=0.5,
|
||||||
|
standard_pose_ratio=0.5,
|
||||||
|
trigger=Trigger,
|
||||||
|
prepend_trigger_to_prompt=True,
|
||||||
|
extra_positive="",
|
||||||
|
extra_negative="",
|
||||||
|
character_cast=character_cast,
|
||||||
|
women_count=women_count,
|
||||||
|
men_count=men_count,
|
||||||
|
expression_enabled=True,
|
||||||
|
expression_intensity=0.6,
|
||||||
|
hardcore_position_config=hardcore_position_config,
|
||||||
|
)
|
||||||
|
_expect_row_base(row, name)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_builtin_single() -> None:
|
||||||
|
row = _prompt_row(name="builtin_single_woman", category="woman", subcategory="random", seed=1001, men_count=0)
|
||||||
|
_expect(row.get("source") == "built_in_generator", "builtin row should come from built-in generator")
|
||||||
|
_expect_trigger_once("builtin_single_woman.prompt", row.get("prompt"), Trigger)
|
||||||
|
_expect_formatter_outputs(row, "builtin_single_woman", target="single")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_hardcore_category_routes() -> None:
|
||||||
|
cast = _character_cast()
|
||||||
|
cases = [
|
||||||
|
("hardcore_penetration", "Penetrative sex", "penetration_only"),
|
||||||
|
("hardcore_oral", "Oral sex", "oral_only"),
|
||||||
|
("hardcore_manual", "Manual stimulation", "manual_only"),
|
||||||
|
("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only"),
|
||||||
|
("hardcore_foreplay", "Foreplay and teasing", "foreplay_only"),
|
||||||
|
("hardcore_aftercare", "Aftercare and cleanup", "interaction_only"),
|
||||||
|
]
|
||||||
|
for index, (name, subcategory, focus) in enumerate(cases, start=1101):
|
||||||
|
row = _prompt_row(
|
||||||
|
name=name,
|
||||||
|
category="Hardcore sexual poses",
|
||||||
|
subcategory=subcategory,
|
||||||
|
seed=index,
|
||||||
|
character_cast=cast,
|
||||||
|
women_count=1,
|
||||||
|
men_count=1,
|
||||||
|
hardcore_position_config=_action_filter(focus),
|
||||||
|
)
|
||||||
|
_expect_custom_row(row, name)
|
||||||
|
_expect(row.get("subject_type") == "configured_cast", f"{name} should use configured cast")
|
||||||
|
_expect_formatter_outputs(row, name, target="single")
|
||||||
|
|
||||||
|
|
||||||
|
def _insta_options(**overrides: Any) -> str:
|
||||||
|
options = pb.build_insta_of_options_json(
|
||||||
|
softcore_cast="same_as_hardcore",
|
||||||
|
hardcore_cast="couple",
|
||||||
|
hardcore_women_count=1,
|
||||||
|
hardcore_men_count=1,
|
||||||
|
softcore_level="lingerie_tease",
|
||||||
|
hardcore_level="hardcore",
|
||||||
|
platform_style="hybrid",
|
||||||
|
continuity="same_creator_same_room",
|
||||||
|
hardcore_clothing_continuity="explicit_nude",
|
||||||
|
softcore_camera_mode="standard",
|
||||||
|
hardcore_camera_mode="standard",
|
||||||
|
camera_detail="compact",
|
||||||
|
hardcore_detail_density="balanced",
|
||||||
|
)
|
||||||
|
data = json.loads(options)
|
||||||
|
data.update(overrides)
|
||||||
|
return _json(data)
|
||||||
|
|
||||||
|
|
||||||
|
def _expect_pair(pair: dict[str, Any], name: str) -> None:
|
||||||
|
_expect(pair.get("mode") == "Insta/OF", f"{name}.mode should be Insta/OF")
|
||||||
|
_expect_row_base(pair.get("softcore_row") or {}, f"{name}.softcore_row")
|
||||||
|
_expect_custom_row(pair.get("hardcore_row") or {}, f"{name}.hardcore_row")
|
||||||
|
_expect_text(f"{name}.softcore_prompt", pair.get("softcore_prompt"), 20)
|
||||||
|
_expect_text(f"{name}.hardcore_prompt", pair.get("hardcore_prompt"), 20)
|
||||||
|
_expect_no_duplicate_comma_items(f"{name}.softcore_negative", pair.get("softcore_negative_prompt"))
|
||||||
|
_expect_no_duplicate_comma_items(f"{name}.hardcore_negative", pair.get("hardcore_negative_prompt"))
|
||||||
|
_expect_formatter_outputs(pair, name, target="softcore")
|
||||||
|
_expect_formatter_outputs(pair, f"{name}.hardcore", target="hardcore")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_insta_pair() -> None:
|
||||||
|
pair = pb.build_insta_of_pair(
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=2101,
|
||||||
|
ethnicity="any",
|
||||||
|
figure="random",
|
||||||
|
no_plus_women=False,
|
||||||
|
no_black=False,
|
||||||
|
trigger=Trigger,
|
||||||
|
prepend_trigger_to_prompt=True,
|
||||||
|
options_json=_insta_options(),
|
||||||
|
character_cast=_character_cast(),
|
||||||
|
hardcore_position_config=_action_filter("penetration_only"),
|
||||||
|
)
|
||||||
|
_expect_pair(pair, "insta_pair_same_cast")
|
||||||
|
_expect(pair["softcore_row"].get("scene_text") == pair["hardcore_row"].get("scene_text"), "pair scene continuity broke")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_insta_pair_pov() -> None:
|
||||||
|
pair = pb.build_insta_of_pair(
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=2201,
|
||||||
|
ethnicity="any",
|
||||||
|
figure="random",
|
||||||
|
no_plus_women=False,
|
||||||
|
no_black=False,
|
||||||
|
trigger=Trigger,
|
||||||
|
prepend_trigger_to_prompt=True,
|
||||||
|
options_json=_insta_options(),
|
||||||
|
character_cast=_character_cast(pov_man=True),
|
||||||
|
hardcore_position_config=_action_filter("oral_only"),
|
||||||
|
)
|
||||||
|
_expect_pair(pair, "insta_pair_pov_man")
|
||||||
|
pov_labels = pair.get("pov_character_labels") or []
|
||||||
|
_expect("Man A" in pov_labels, "pair POV labels should include Man A")
|
||||||
|
hard_row = pair.get("hardcore_row") or {}
|
||||||
|
_expect("Man A" in (hard_row.get("pov_character_labels") or []), "hard row POV labels should include Man A")
|
||||||
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
||||||
|
prompt = krea.get("krea_prompt") or ""
|
||||||
|
_expect("viewer" in prompt.lower(), "POV Krea prompt should mention viewer perspective")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_no_expression_fallback() -> None:
|
||||||
|
cast = pb.build_character_slot_json(
|
||||||
|
subject_type="woman",
|
||||||
|
label="A",
|
||||||
|
age="25-year-old adult",
|
||||||
|
ethnicity="western_european",
|
||||||
|
body="slim",
|
||||||
|
descriptor_detail="full",
|
||||||
|
expression_enabled=False,
|
||||||
|
)["character_cast"]
|
||||||
|
row = _prompt_row(
|
||||||
|
name="hardcore_expression_disabled",
|
||||||
|
category="Hardcore sexual poses",
|
||||||
|
subcategory="Penetrative sex",
|
||||||
|
seed=2301,
|
||||||
|
character_cast=cast,
|
||||||
|
women_count=1,
|
||||||
|
men_count=1,
|
||||||
|
hardcore_position_config=_action_filter("penetration_only"),
|
||||||
|
)
|
||||||
|
_expect_custom_row(row, "hardcore_expression_disabled")
|
||||||
|
_expect(not row.get("expression"), "expression should stay disabled without fallback")
|
||||||
|
_expect("Facial expressions:" not in row.get("prompt", ""), "disabled expression leaked into prompt")
|
||||||
|
_expect_formatter_outputs(row, "hardcore_expression_disabled", target="single")
|
||||||
|
|
||||||
|
|
||||||
|
SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||||
|
("builtin_single_woman", smoke_builtin_single),
|
||||||
|
("hardcore_category_routes", smoke_hardcore_category_routes),
|
||||||
|
("insta_pair_same_cast", smoke_insta_pair),
|
||||||
|
("insta_pair_pov_man", smoke_insta_pair_pov),
|
||||||
|
("expression_disabled", smoke_no_expression_fallback),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--case",
|
||||||
|
choices=[name for name, _func in SMOKE_CASES],
|
||||||
|
action="append",
|
||||||
|
help="Run only the named smoke case. Can be passed multiple times.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
selected = set(args.case or [])
|
||||||
|
report = SmokeReport()
|
||||||
|
for name, func in SMOKE_CASES:
|
||||||
|
if selected and name not in selected:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as exc: # noqa: BLE001 - report all smoke failures uniformly.
|
||||||
|
report.fail(name, str(exc))
|
||||||
|
else:
|
||||||
|
report.ok(name)
|
||||||
|
print(f"\nSummary: {len(report.passed)} passed, {len(report.failed)} failed")
|
||||||
|
if report.failed:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user