Centralize softcore pair wording

This commit is contained in:
2026-06-27 16:32:32 +02:00
parent c69274d2ee
commit 9cd1f03bfe
11 changed files with 115 additions and 34 deletions
+1 -1
View File
@@ -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
+9 -6
View File
@@ -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)")
+3
View File
@@ -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,
)
@@ -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
+1
View File
@@ -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. |
+3
View File
@@ -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,
+7 -11
View File
@@ -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")
+8 -9
View File
@@ -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']}. "
+3 -1
View File
@@ -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 = (
+47
View File
@@ -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."
+30 -6
View File
@@ -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"(?<![a-z0-9_]){re.escape(trigger)}(?![a-z0-9_])", text, flags=re.IGNORECASE))
@@ -3850,10 +3865,10 @@ def smoke_caption_metadata_routes() -> 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",