diff --git a/README.md b/README.md index de5487e..7e758ef 100644 --- a/README.md +++ b/README.md @@ -462,7 +462,7 @@ cleanup. When connected to `SxCP Insta/OF Prompt Pair` metadata, the naturalizer emits a single combined natural caption with the shared descriptor plus separate -softcore and hardcore version descriptions. It uses the final selected +softcore and hardcore side descriptions. It uses the final selected expression and composition from the generated rows, including any expression pool and intensity settings. Set `target=softcore` or `target=hardcore` to emit only one side of the pair for diff --git a/caption_metadata_routes.py b/caption_metadata_routes.py index 1fca3cd..95d7617 100644 --- a/caption_metadata_routes.py +++ b/caption_metadata_routes.py @@ -54,6 +54,7 @@ class CaptionMetadataRouteDependencies: natural_cast_descriptor_text: Callable[[str], str] cast_labels: Callable[[str], list[str]] natural_label_text: Callable[[Any, list[str]], str] + softcore_caption_setup_phrase: Callable[..., str] metadata_to_prose: Callable[..., tuple[str, str]] @@ -352,10 +353,12 @@ def insta_of_pair_from_row_result( if cast_descriptor_text and not same_soft_cast: parts.append(deps.natural_cast_descriptor_text(cast_descriptor_text)) if same_soft_cast and include_soft: - if target == "auto": - parts.append("The softcore version keeps the same adult cast present together in a non-explicit teaser setup") - else: - parts.append("The same adult cast is present together in a non-explicit teaser setup") + parts.append( + deps.softcore_caption_setup_phrase( + same_cast=True, + target_auto=target == "auto", + ) + ) partner_styling = row.get("softcore_partner_styling") if isinstance(partner_styling, dict): outfits = partner_styling.get("outfits") @@ -368,9 +371,9 @@ def insta_of_pair_from_row_result( if pose: parts.append(f"The shared softcore cast pose is {pose}") if soft_text: - parts.append(f"Softcore version: {soft_text}" if target == "auto" else soft_text) + parts.append(f"Softcore side: {soft_text}" if target == "auto" else soft_text) if hard_text: - parts.append(f"Hardcore version: {hard_text}" if target == "auto" else hard_text) + parts.append(f"Hardcore side: {hard_text}" if target == "auto" else hard_text) if not parts: return None return CaptionMetadataRoute(deps.join_sentences(parts), "metadata(insta_of_pair)") diff --git a/caption_text_policy.py b/caption_text_policy.py index c4cfd0a..ca6cd39 100644 --- a/caption_text_policy.py +++ b/caption_text_policy.py @@ -9,12 +9,14 @@ try: from . import formatter_input as input_policy from . import krea_cast as cast_policy from . import route_metadata as route_metadata_policy + from . import softcore_text_policy except ImportError: # Allows local smoke tests with `python -c`. import caption_metadata_routes import caption_policy import formatter_input as input_policy import krea_cast as cast_policy import route_metadata as route_metadata_policy + import softcore_text_policy OLD_TRIGGER = caption_policy.OLD_TRIGGER @@ -300,5 +302,6 @@ def metadata_route_dependencies( natural_cast_descriptor_text=natural_cast_descriptor_text, cast_labels=cast_labels, natural_label_text=natural_label_text, + softcore_caption_setup_phrase=softcore_text_policy.softcore_caption_setup_phrase, metadata_to_prose=metadata_to_prose, ) diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 0294f5e..99d3f15 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -336,6 +336,9 @@ Already isolated: prose, descriptor-entry assembly, shared descriptors, cast-label cleanup, same-cast softcore descriptor text, partner styling, platform and level labels, softcore cast presence text, and hard cast summary text. +- shared softcore pair prose for solo/same-cast/POV presence, caption side + wording, and creator-shot teaser directives lives in + `softcore_text_policy.py`; pair, Krea, and caption routes delegate to it. - pair-level camera routing lives in `pair_camera.py` behind `InstaPairCameraRoute`, including soft/hard camera config selection, same-as-softcore mode, camera-detail override, same-room hard scene diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index f9e1ddb..9e06e00 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -111,6 +111,7 @@ Core helper ownership: | `pair_builder.py` | Insta/OF pair route sequencing behind `InstaPairBuildRequest` and `InstaPairBuildDependencies`, including option/filter/seed/cast parsing handoff, soft/hard row, cast, camera, clothing, and final output adapter orchestration. | | `pair_rows.py` | Insta/OF soft/hard row creation behind `InstaPairRowsRoute`, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, POV row fields, and legacy dict compatibility. | | `pair_cast.py` | Insta/OF descriptor prose, descriptor-entry assembly, shared descriptors, cast-label cleanup, same-cast softcore descriptor text, partner styling selection, cast-summary wording, platform/level labels, softcore cast presence text, and hard cast summary text. | +| `softcore_text_policy.py` | Shared softcore pair prose for solo/same-cast/POV cast presence, caption side setup wording, and creator-shot teaser style directives. | | `pair_camera.py` | Insta/OF soft/hard camera route resolution behind `InstaPairCameraRoute`, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, synchronized row/root camera metadata, and legacy dict compatibility. | | `pair_clothing.py` | Insta/OF clothing sentence formatting and hardcore clothing continuity behind `HardcorePairClothingRoute`, body-exposure scene cleanup, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, final root clothing-state assembly, and legacy dict compatibility. | | `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. | diff --git a/krea_formatter.py b/krea_formatter.py index fcd84ed..d0ed8f3 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -7,6 +7,7 @@ try: from . import formatter_input as input_policy from . import krea_format_route from . import route_metadata as route_metadata_policy + from . import softcore_text_policy from .krea_action_context import ( is_close_foreplay_text as _is_close_foreplay_text, is_outercourse_text as _is_outercourse_text, @@ -43,6 +44,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import formatter_input as input_policy import krea_format_route import route_metadata as route_metadata_policy + import softcore_text_policy from krea_action_context import ( is_close_foreplay_text as _is_close_foreplay_text, is_outercourse_text as _is_outercourse_text, @@ -570,6 +572,7 @@ def _krea_pair_format_dependencies() -> krea_pair_formatter.KreaPairFormatDepend pov_camera_phrase=lambda labels: _pov_camera_phrase(labels), pov_soft_camera_phrase=lambda labels: _pov_camera_phrase(labels, softcore=True), pov_composition_text=_pov_composition_text, + softcore_cast_presence_phrase=softcore_text_policy.softcore_cast_presence_phrase, natural_clothing_state=_natural_clothing_state, composition_phrase=_composition_phrase, paragraph=_paragraph, diff --git a/krea_pair_formatter.py b/krea_pair_formatter.py index f9c3f81..a156922 100644 --- a/krea_pair_formatter.py +++ b/krea_pair_formatter.py @@ -47,6 +47,7 @@ class KreaPairFormatDependencies: pov_camera_phrase: Callable[[list[str]], str] pov_soft_camera_phrase: Callable[[list[str]], str] pov_composition_text: Callable[[Any, list[str]], str] + softcore_cast_presence_phrase: Callable[..., str] natural_clothing_state: Callable[[Any, str], str] composition_phrase: Callable[..., str] paragraph: Callable[[list[str]], str] @@ -134,17 +135,12 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma hard_output_composition = deps.pov_composition_text(hard_composition, pov_labels) same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" soft_output_composition = deps.pov_composition_text(soft.get("composition"), pov_labels if same_soft_cast else []) - if same_soft_cast and pov_labels: - soft_cast_presence = ( - "the woman is framed from the POV participant's first-person camera in a soft creator-teaser pose, " - "with the POV participant kept off-camera as the viewpoint and implied by camera position or foreground cues" - ) - else: - soft_cast_presence = ( - f"{deps.label_join(soft_labels)} share the frame in a soft creator-teaser pose" - if same_soft_cast - else "The image focuses on the woman alone" - ) + soft_cast_presence = deps.softcore_cast_presence_phrase( + same_cast=same_soft_cast, + pov_labels=pov_labels if same_soft_cast else [], + cast_label=deps.label_join(soft_labels), + woman_label="the woman", + ) partner_styling = row.get("softcore_partner_styling") if isinstance(partner_styling, dict): outfits = partner_styling.get("outfits") diff --git a/pair_cast.py b/pair_cast.py index 8ee9c3c..73607da 100644 --- a/pair_cast.py +++ b/pair_cast.py @@ -7,11 +7,13 @@ try: from . import character_profile as character_profile_policy from . import pair_clothing from . import pair_options + from . import softcore_text_policy except ImportError: # Allows local smoke tests with top-level imports. import cast_context as cast_context_policy import character_profile as character_profile_policy import pair_clothing import pair_options + import softcore_text_policy AxisRng = Callable[[dict[str, int], str, int, int], Any] @@ -264,16 +266,13 @@ def resolve_insta_pair_cast_context( else f"soft creator-teaser setup with {cast_summary_phrase(hard_women_count, hard_men_count)}" ) soft_cast_presence = ( - ( - "Frame Woman A from the POV participant's first-person camera in a soft creator-teaser setup; " - "keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. " - ) - if same_softcore_cast and pov_character_labels - else ( - "Place Woman A and the listed partners together in a soft creator-teaser pose. " - if same_softcore_cast - else "Keep the softcore version focused on Woman A alone. " + softcore_text_policy.softcore_cast_presence_phrase( + same_cast=same_softcore_cast, + pov_labels=pov_character_labels, + cast_label="Woman A and the listed partners", + woman_label="Woman A", ) + + ". " ) soft_cast_styling_sentence = ( f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. " diff --git a/pair_output.py b/pair_output.py index 34facb3..306b50d 100644 --- a/pair_output.py +++ b/pair_output.py @@ -4,8 +4,10 @@ from typing import Any, Callable try: from . import row_normalization as row_policy + from . import softcore_text_policy except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`. import row_normalization as row_policy + import softcore_text_policy def _labeled_expression_sentence(label: str, expression: Any) -> str: @@ -84,7 +86,7 @@ def assemble_insta_pair_metadata( f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}" f"Composition: {soft_row['composition']}. " f"{soft_camera_sentence}" - "Keep the softcore version seductive, creator-shot, and styled as a soft teaser. " + f"{softcore_text_policy.softcore_style_directive()} " f"{soft_row['positive_suffix']}." ) hard_prompt = ( diff --git a/softcore_text_policy.py b/softcore_text_policy.py new file mode 100644 index 0000000..9d2c615 --- /dev/null +++ b/softcore_text_policy.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import Any + + +def _clean(value: Any) -> str: + return " ".join(str(value or "").strip().split()) + + +def softcore_cast_presence_phrase( + *, + same_cast: bool, + pov_labels: list[str] | tuple[str, ...] | None = None, + cast_label: str = "", + woman_label: str = "the woman", +) -> str: + pov_labels = [str(label) for label in (pov_labels or []) if str(label).strip()] + cast_label = _clean(cast_label) + woman_label = _clean(woman_label) or "the woman" + if same_cast and pov_labels: + return ( + f"{woman_label} is framed from the POV participant's first-person creator camera, " + "with the POV participant implied by camera position or foreground body cues" + ) + if same_cast: + visible_cast = cast_label or "the named cast" + verb = "share" if " and " in visible_cast or "," in visible_cast else "shares" + return f"{visible_cast} {verb} a styled creator-teaser frame" + return f"solo creator frame with {woman_label} as the only visible subject" + + +def softcore_caption_setup_phrase(*, same_cast: bool, target_auto: bool = False) -> str: + if same_cast: + return ( + "The softcore side keeps the same adult cast together in a styled creator setup" + if target_auto + else "The same adult cast shares a styled creator setup" + ) + return ( + "The softcore side is a solo styled creator setup" + if target_auto + else "Solo styled creator setup" + ) + + +def softcore_style_directive() -> str: + return "Use seductive creator-shot teaser styling." diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 61003f3..7164262 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -145,6 +145,21 @@ def _expect_no_duplicate_comma_items(name: str, value: Any) -> None: _expect(not duplicates, f"{name} has duplicate comma items: {duplicates[:5]}") +def _expect_no_softcore_noise(name: str, value: Any) -> None: + text = str(value or "").lower() + noisy = ( + "the image focuses", + "softcore version", + "non-explicit teaser setup", + "no sex act", + "genital contact", + "keep the softcore version", + "focused on woman a alone", + ) + found = [phrase for phrase in noisy if phrase in text] + _expect(not found, f"{name} has softcore prompt noise: {found}") + + def _trigger_count(text: str, trigger: str) -> int: return len(re.findall(rf"(? None: _expect(hard_route is not None, "Caption pair hardcore target did not match") assert soft_route is not None assert hard_route is not None - _expect("Softcore version:" not in soft_route.prose, "Caption softcore target should not keep combined pair labels") - _expect("Hardcore version:" not in soft_route.prose, "Caption softcore target should not include hard label") - _expect("Softcore version:" not in hard_route.prose, "Caption hardcore target should not include soft label") - _expect("Hardcore version:" not in hard_route.prose, "Caption hardcore target should not keep combined pair labels") + _expect("Softcore side:" not in soft_route.prose, "Caption softcore target should not keep combined pair labels") + _expect("Hardcore side:" not in soft_route.prose, "Caption softcore target should not include hard label") + _expect("Softcore side:" not in hard_route.prose, "Caption hardcore target should not include soft label") + _expect("Hardcore side:" not in hard_route.prose, "Caption hardcore target should not keep combined pair labels") _expect(soft_route.prose != hard_route.prose, "Caption pair soft/hard targets should produce distinct prose") public_hard, public_hard_method = caption_naturalizer.naturalize_caption( "", @@ -5368,6 +5383,12 @@ def smoke_insta_pair() -> None: ) _expect_pair(pair, "insta_pair_same_cast") _expect(pair["softcore_row"].get("scene_text") == pair["hardcore_row"].get("scene_text"), "pair scene continuity broke") + _expect_no_softcore_noise("insta_pair_same_cast.softcore_prompt", pair.get("softcore_prompt")) + _expect("styled creator-teaser frame" in str(pair.get("softcore_prompt", "")).lower(), "pair softcore prompt lost clean cast-presence wording") + krea_soft = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="softcore") + krea_soft_prompt = _expect_text("insta_pair_same_cast.krea_soft_prompt", krea_soft.get("krea_prompt"), 40) + _expect_no_softcore_noise("insta_pair_same_cast.krea_soft_prompt", krea_soft_prompt) + _expect("styled creator-teaser frame" in krea_soft_prompt.lower(), "Krea softcore prompt lost clean same-cast wording") clothing_state = _clean_key(pair.get("hardcore_clothing_state")) _expect("body is fully exposed" in clothing_state, "explicit nude pair should keep body exposure state") _expect("teaser outfit detail" not in clothing_state, "explicit nude pair should not repeat softcore outfit detail") @@ -6546,6 +6567,8 @@ def smoke_formatter_metadata_fixtures() -> None: krea_hard = _expect_text("fixture_external_pair.krea_hard", krea_pair.get("krea_hardcore_prompt"), 40).lower() _expect(krea_pair.get("method") == "metadata_json:krea2(insta_of_pair)", "External pair Krea route changed method") _expect("black buttoned shirt" in krea_soft, "External pair Krea soft route lost embedded partner styling") + _expect_no_softcore_noise("fixture_external_pair.krea_soft", krea_pair.get("krea_softcore_prompt")) + _expect("styled creator-teaser frame" in krea_soft, "External pair Krea soft route lost clean cast-presence wording") _expect("red satin lingerie set" in krea_hard, "External pair Krea hard route lost embedded clothing state") _expect("row hard right-side view" in krea_hard, "External pair Krea hard route lost embedded camera directive") _expect_no_duplicate_comma_items("fixture_external_pair.krea_negative", krea_pair.get("negative_prompt")) @@ -6578,8 +6601,9 @@ def smoke_formatter_metadata_fixtures() -> None: _expect(method == "metadata_json:metadata(insta_of_pair)", "External pair caption route changed method") _expect_trigger_once("fixture_external_pair.caption", caption, Trigger) _expect("black buttoned shirt" in caption_text, "External pair caption route lost embedded partner styling") - _expect("softcore version" in caption_text, "External pair caption route lost softcore side") - _expect("hardcore version" in caption_text, "External pair caption route lost hardcore side") + _expect("softcore side" in caption_text, "External pair caption route lost softcore side") + _expect("hardcore side" in caption_text, "External pair caption route lost hardcore side") + _expect("softcore version" not in caption_text and "hardcore version" not in caption_text, "External pair caption kept version labels") generated = _prompt_row( name="fixture_generated_formatter_invariants",