Add typed pair route contracts
This commit is contained in:
@@ -290,22 +290,25 @@ Already isolated:
|
||||
policy, plus hardcore detail-density directive text, live in
|
||||
`pair_options.py`; `prompt_builder.py` keeps public delegate wrappers for
|
||||
existing nodes and tests.
|
||||
- soft/hard row creation lives in `pair_rows.py`, including softcore expression
|
||||
override resolution, Woman A slot context application, soft outfit/pose
|
||||
overrides, POV row fields, and hardcore row creation.
|
||||
- soft/hard row creation lives in `pair_rows.py` behind `InstaPairRowsRoute`,
|
||||
including softcore expression override resolution, Woman A slot context
|
||||
application, soft outfit/pose overrides, POV row fields, hardcore row
|
||||
creation, and legacy dict compatibility.
|
||||
- pair-level cast/display context lives in `pair_cast.py`, including descriptor
|
||||
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.
|
||||
- pair-level camera routing lives in `pair_camera.py`, including soft/hard
|
||||
camera config selection, same-as-softcore mode, camera-detail override,
|
||||
same-room hard scene continuity, camera-aware composition mutation, POV camera
|
||||
suppression, and row/root camera metadata synchronization.
|
||||
- pair-level clothing policy lives in `pair_clothing.py`, including clothing
|
||||
sentence formatting, body-exposure scene cleanup, action-aware body-access
|
||||
flags, conflicting outfit-piece cleanup, default visible-men clothing,
|
||||
character-clothing override handling, hardcore clothing continuity, and final
|
||||
root clothing-state assembly.
|
||||
- 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
|
||||
continuity, camera-aware composition mutation, POV camera suppression,
|
||||
row/root camera metadata synchronization, and legacy dict compatibility.
|
||||
- pair-level clothing policy lives in `pair_clothing.py` behind
|
||||
`HardcorePairClothingRoute`, including clothing sentence formatting,
|
||||
body-exposure scene cleanup, action-aware body-access flags, conflicting
|
||||
outfit-piece cleanup, default visible-men clothing, character-clothing
|
||||
override handling, hardcore clothing continuity, final root clothing-state
|
||||
assembly, and legacy dict compatibility.
|
||||
- final pair output assembly lives in `pair_output.py`, including soft/hard
|
||||
prompt strings, trigger preservation, negatives, captions, and root metadata
|
||||
shape; the final cleanup step is delegated to `row_normalization.py`.
|
||||
|
||||
@@ -96,10 +96,10 @@ Core helper ownership:
|
||||
| `row_prompt_axes.py` | Row scene/pose/expression/composition axis selection behind `PromptAxesRoute`, compatible-entry filtering, expression-disabled handling, per-character expression promotion, legacy dict compatibility, POV composition adaptation, and pose-category environment sanitizing. |
|
||||
| `hardcore_position_config.py` | Hardcore position/action-filter choices, selected-position normalization, config JSON builders/parsers, focus-policy toggles, subcategory allow-list policy, position-key detection, and category/template/axis filtering. |
|
||||
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, hardcore cast count policy, and hardcore detail-density directives. |
|
||||
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
|
||||
| `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. |
|
||||
| `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. |
|
||||
| `pair_clothing.py` | Insta/OF clothing sentence formatting, body-exposure scene cleanup, hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, and final root clothing-state assembly. |
|
||||
| `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. |
|
||||
| `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry, called through `row_role_graph.py` for row generation. |
|
||||
| `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. |
|
||||
|
||||
+94
-18
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
@@ -23,7 +24,43 @@ def camera_config_with_detail(
|
||||
return camera_config
|
||||
|
||||
|
||||
def resolve_insta_pair_camera(
|
||||
@dataclass(frozen=True)
|
||||
class InstaPairCameraRoute:
|
||||
soft_row: dict[str, Any]
|
||||
hard_row: dict[str, Any]
|
||||
hard_scene: str
|
||||
hard_composition: str
|
||||
soft_camera_config: dict[str, Any]
|
||||
hard_camera_config: dict[str, Any]
|
||||
soft_camera_directive: str
|
||||
hard_camera_directive: str
|
||||
soft_camera_scene_directive: str
|
||||
hard_camera_scene_directive: str
|
||||
soft_camera_scene_sentence: str
|
||||
hard_camera_scene_sentence: str
|
||||
soft_camera_sentence: str
|
||||
hard_camera_sentence: str
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"soft_row": self.soft_row,
|
||||
"hard_row": self.hard_row,
|
||||
"hard_scene": self.hard_scene,
|
||||
"hard_composition": self.hard_composition,
|
||||
"soft_camera_config": dict(self.soft_camera_config),
|
||||
"hard_camera_config": dict(self.hard_camera_config),
|
||||
"soft_camera_directive": self.soft_camera_directive,
|
||||
"hard_camera_directive": self.hard_camera_directive,
|
||||
"soft_camera_scene_directive": self.soft_camera_scene_directive,
|
||||
"hard_camera_scene_directive": self.hard_camera_scene_directive,
|
||||
"soft_camera_scene_sentence": self.soft_camera_scene_sentence,
|
||||
"hard_camera_scene_sentence": self.hard_camera_scene_sentence,
|
||||
"soft_camera_sentence": self.soft_camera_sentence,
|
||||
"hard_camera_sentence": self.hard_camera_sentence,
|
||||
}
|
||||
|
||||
|
||||
def resolve_insta_pair_camera_result(
|
||||
*,
|
||||
soft_row: dict[str, Any],
|
||||
hard_row: dict[str, Any],
|
||||
@@ -41,7 +78,7 @@ def resolve_insta_pair_camera(
|
||||
contextual_composition_prompt: CompositionPrompt,
|
||||
composition_prompt: Callable[[Any], str],
|
||||
camera_scene_directive_for_context: CameraSceneDirective,
|
||||
) -> dict[str, Any]:
|
||||
) -> InstaPairCameraRoute:
|
||||
hard_camera_mode = str(options["hardcore_camera_mode"])
|
||||
soft_camera_source = softcore_camera_config or camera_config
|
||||
hard_camera_source = hardcore_camera_config or camera_config
|
||||
@@ -108,19 +145,58 @@ def resolve_insta_pair_camera(
|
||||
hard_row["camera_directive"] = hard_camera_directive
|
||||
hard_row["camera_scene_directive"] = hard_camera_scene_directive
|
||||
|
||||
return {
|
||||
"soft_row": soft_row,
|
||||
"hard_row": hard_row,
|
||||
"hard_scene": hard_scene,
|
||||
"hard_composition": hard_composition,
|
||||
"soft_camera_config": soft_camera_config_dict,
|
||||
"hard_camera_config": hard_camera_config_dict,
|
||||
"soft_camera_directive": soft_camera_directive,
|
||||
"hard_camera_directive": hard_camera_directive,
|
||||
"soft_camera_scene_directive": soft_camera_scene_directive,
|
||||
"hard_camera_scene_directive": hard_camera_scene_directive,
|
||||
"soft_camera_scene_sentence": f"{soft_camera_scene_directive} " if soft_camera_scene_directive else "",
|
||||
"hard_camera_scene_sentence": f"{hard_camera_scene_directive} " if hard_camera_scene_directive else "",
|
||||
"soft_camera_sentence": f"Camera control: {soft_camera_directive} " if soft_camera_directive else "",
|
||||
"hard_camera_sentence": f"Camera control: {hard_camera_directive} " if hard_camera_directive else "",
|
||||
}
|
||||
return InstaPairCameraRoute(
|
||||
soft_row=soft_row,
|
||||
hard_row=hard_row,
|
||||
hard_scene=hard_scene,
|
||||
hard_composition=hard_composition,
|
||||
soft_camera_config=soft_camera_config_dict,
|
||||
hard_camera_config=hard_camera_config_dict,
|
||||
soft_camera_directive=soft_camera_directive,
|
||||
hard_camera_directive=hard_camera_directive,
|
||||
soft_camera_scene_directive=soft_camera_scene_directive,
|
||||
hard_camera_scene_directive=hard_camera_scene_directive,
|
||||
soft_camera_scene_sentence=f"{soft_camera_scene_directive} " if soft_camera_scene_directive else "",
|
||||
hard_camera_scene_sentence=f"{hard_camera_scene_directive} " if hard_camera_scene_directive else "",
|
||||
soft_camera_sentence=f"Camera control: {soft_camera_directive} " if soft_camera_directive else "",
|
||||
hard_camera_sentence=f"Camera control: {hard_camera_directive} " if hard_camera_directive else "",
|
||||
)
|
||||
|
||||
|
||||
def resolve_insta_pair_camera(
|
||||
*,
|
||||
soft_row: dict[str, Any],
|
||||
hard_row: dict[str, Any],
|
||||
options: dict[str, Any],
|
||||
camera_config: str | dict[str, Any] | None,
|
||||
softcore_camera_config: str | dict[str, Any] | None,
|
||||
hardcore_camera_config: str | dict[str, Any] | None,
|
||||
hard_women_count: int,
|
||||
hard_men_count: int,
|
||||
pov_character_labels: list[str],
|
||||
camera_detail_choices: list[str] | tuple[str, ...],
|
||||
camera_config_with_mode: CameraConfigWithMode,
|
||||
camera_directive: CameraDirective,
|
||||
apply_contextual_composition: ApplyComposition,
|
||||
contextual_composition_prompt: CompositionPrompt,
|
||||
composition_prompt: Callable[[Any], str],
|
||||
camera_scene_directive_for_context: CameraSceneDirective,
|
||||
) -> dict[str, Any]:
|
||||
return resolve_insta_pair_camera_result(
|
||||
soft_row=soft_row,
|
||||
hard_row=hard_row,
|
||||
options=options,
|
||||
camera_config=camera_config,
|
||||
softcore_camera_config=softcore_camera_config,
|
||||
hardcore_camera_config=hardcore_camera_config,
|
||||
hard_women_count=hard_women_count,
|
||||
hard_men_count=hard_men_count,
|
||||
pov_character_labels=pov_character_labels,
|
||||
camera_detail_choices=camera_detail_choices,
|
||||
camera_config_with_mode=camera_config_with_mode,
|
||||
camera_directive=camera_directive,
|
||||
apply_contextual_composition=apply_contextual_composition,
|
||||
contextual_composition_prompt=contextual_composition_prompt,
|
||||
composition_prompt=composition_prompt,
|
||||
camera_scene_directive_for_context=camera_scene_directive_for_context,
|
||||
).as_dict()
|
||||
|
||||
+56
-10
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
from typing import Any, Callable
|
||||
|
||||
@@ -373,7 +374,27 @@ def default_man_hardcore_clothing_entries(
|
||||
return entries
|
||||
|
||||
|
||||
def resolve_hardcore_pair_clothing(
|
||||
@dataclass(frozen=True)
|
||||
class HardcorePairClothingRoute:
|
||||
access_flags: dict[str, bool]
|
||||
woman_access: str
|
||||
default_man_hardcore_clothing: list[str]
|
||||
hardcore_clothing_state: str
|
||||
hardcore_clothing_sentence: str
|
||||
requires_body_exposure_scene: bool
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"access_flags": dict(self.access_flags),
|
||||
"woman_access": self.woman_access,
|
||||
"default_man_hardcore_clothing": list(self.default_man_hardcore_clothing),
|
||||
"hardcore_clothing_state": self.hardcore_clothing_state,
|
||||
"hardcore_clothing_sentence": self.hardcore_clothing_sentence,
|
||||
"requires_body_exposure_scene": self.requires_body_exposure_scene,
|
||||
}
|
||||
|
||||
|
||||
def resolve_hardcore_pair_clothing_result(
|
||||
*,
|
||||
hard_row: dict[str, Any],
|
||||
mode: str,
|
||||
@@ -384,7 +405,7 @@ def resolve_hardcore_pair_clothing(
|
||||
rng: Any,
|
||||
continuity_map: dict[str, str],
|
||||
choose: Callable[[Any, list[str]], str],
|
||||
) -> dict[str, Any]:
|
||||
) -> HardcorePairClothingRoute:
|
||||
access_flags = hardcore_row_access_flags(hard_row)
|
||||
woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else ""
|
||||
default_man_entries = default_man_hardcore_clothing_entries(
|
||||
@@ -412,14 +433,39 @@ def resolve_hardcore_pair_clothing(
|
||||
if str(part or "").strip()
|
||||
]
|
||||
hard_clothing_state = "; ".join(hard_clothing_parts)
|
||||
return {
|
||||
"access_flags": access_flags,
|
||||
"woman_access": woman_access,
|
||||
"default_man_hardcore_clothing": default_man_entries,
|
||||
"hardcore_clothing_state": hard_clothing_state,
|
||||
"hardcore_clothing_sentence": f"{hard_clothing_state}. " if hard_clothing_state else "",
|
||||
"requires_body_exposure_scene": (
|
||||
return HardcorePairClothingRoute(
|
||||
access_flags=access_flags,
|
||||
woman_access=woman_access,
|
||||
default_man_hardcore_clothing=default_man_entries,
|
||||
hardcore_clothing_state=hard_clothing_state,
|
||||
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
|
||||
requires_body_exposure_scene=(
|
||||
"body is fully exposed" in hard_clothing_state.lower()
|
||||
or "bare skin unobstructed" in hard_clothing_state.lower()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def resolve_hardcore_pair_clothing(
|
||||
*,
|
||||
hard_row: dict[str, Any],
|
||||
mode: str,
|
||||
softcore_outfit: str,
|
||||
character_hardcore_clothing_entries: list[str],
|
||||
men_count: int,
|
||||
pov_labels: list[str] | None,
|
||||
rng: Any,
|
||||
continuity_map: dict[str, str],
|
||||
choose: Callable[[Any, list[str]], str],
|
||||
) -> dict[str, Any]:
|
||||
return resolve_hardcore_pair_clothing_result(
|
||||
hard_row=hard_row,
|
||||
mode=mode,
|
||||
softcore_outfit=softcore_outfit,
|
||||
character_hardcore_clothing_entries=character_hardcore_clothing_entries,
|
||||
men_count=men_count,
|
||||
pov_labels=pov_labels,
|
||||
rng=rng,
|
||||
continuity_map=continuity_map,
|
||||
choose=choose,
|
||||
).as_dict()
|
||||
|
||||
+102
-7
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
try:
|
||||
@@ -12,7 +13,21 @@ BuildPrompt = Callable[..., dict[str, Any]]
|
||||
AxisRng = Callable[[dict[str, int], str, int, int], Any]
|
||||
|
||||
|
||||
def build_insta_pair_rows(
|
||||
@dataclass(frozen=True)
|
||||
class InstaPairRowsRoute:
|
||||
soft_row: dict[str, Any]
|
||||
hard_row: dict[str, Any]
|
||||
hard_content_rng: Any
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"soft_row": self.soft_row,
|
||||
"hard_row": self.hard_row,
|
||||
"hard_content_rng": self.hard_content_rng,
|
||||
}
|
||||
|
||||
|
||||
def build_insta_pair_rows_result(
|
||||
*,
|
||||
row_number: int,
|
||||
start_index: int,
|
||||
@@ -52,7 +67,7 @@ def build_insta_pair_rows(
|
||||
softcore_item_prompt_label: Callable[[str], str],
|
||||
pov_prompt_directive: Callable[[list[str]], str],
|
||||
pov_composition_prompt: Callable[[Any, list[str]], str],
|
||||
) -> dict[str, Any]:
|
||||
) -> InstaPairRowsRoute:
|
||||
soft_content_rng = axis_rng(parsed_seed_config, "content", seed, row_number + 311)
|
||||
hard_content_rng = axis_rng(parsed_seed_config, "content", seed, row_number + 317)
|
||||
soft_person_rng = axis_rng(parsed_seed_config, "person", seed, row_number)
|
||||
@@ -186,8 +201,88 @@ def build_insta_pair_rows(
|
||||
hard_row["pov_character_labels"] = pov_character_labels
|
||||
hard_row["pov_prompt_directive"] = pov_prompt_directive(pov_character_labels)
|
||||
|
||||
return {
|
||||
"soft_row": soft_row,
|
||||
"hard_row": hard_row,
|
||||
"hard_content_rng": hard_content_rng,
|
||||
}
|
||||
return InstaPairRowsRoute(
|
||||
soft_row=soft_row,
|
||||
hard_row=hard_row,
|
||||
hard_content_rng=hard_content_rng,
|
||||
)
|
||||
|
||||
|
||||
def build_insta_pair_rows(
|
||||
*,
|
||||
row_number: int,
|
||||
start_index: int,
|
||||
seed: int,
|
||||
active_trigger: str,
|
||||
parsed_seed_config: dict[str, int],
|
||||
options: dict[str, Any],
|
||||
ethnicity: str,
|
||||
figure: str,
|
||||
no_plus_women: bool,
|
||||
no_black: bool,
|
||||
character_profile: str | dict[str, Any] | None,
|
||||
character_cast: str | dict[str, Any] | list[Any] | None,
|
||||
character_slot_map: dict[str, dict[str, Any]],
|
||||
pov_character_labels: list[str],
|
||||
hard_women_count: int,
|
||||
hard_men_count: int,
|
||||
soft_category: str,
|
||||
soft_subcategory: str,
|
||||
softcore_level_key: str,
|
||||
hardcore_random_subcategory: str,
|
||||
hardcore_position_config: str | dict[str, Any] | None,
|
||||
location_config: str | dict[str, Any] | None,
|
||||
composition_config: str | dict[str, Any] | None,
|
||||
build_prompt: BuildPrompt,
|
||||
axis_rng: AxisRng,
|
||||
cast_expression_intensity_override: Callable[
|
||||
[float, dict[str, dict[str, Any]], int, int, str],
|
||||
tuple[float | None, str],
|
||||
],
|
||||
context_from_character_slot: Callable[[Any, dict[str, Any], str, str, str, bool, bool], dict[str, Any]],
|
||||
apply_character_context_to_row: Callable[[dict[str, Any], dict[str, Any]], dict[str, Any]],
|
||||
disable_row_expression: Callable[[dict[str, Any], str], dict[str, Any]],
|
||||
slot_softcore_outfit: Callable[[dict[str, Any] | None, Any], str],
|
||||
softcore_outfit: Callable[[Any, str], str],
|
||||
softcore_pose: Callable[[Any, str], str],
|
||||
softcore_item_prompt_label: Callable[[str], str],
|
||||
pov_prompt_directive: Callable[[list[str]], str],
|
||||
pov_composition_prompt: Callable[[Any, list[str]], str],
|
||||
) -> dict[str, Any]:
|
||||
return build_insta_pair_rows_result(
|
||||
row_number=row_number,
|
||||
start_index=start_index,
|
||||
seed=seed,
|
||||
active_trigger=active_trigger,
|
||||
parsed_seed_config=parsed_seed_config,
|
||||
options=options,
|
||||
ethnicity=ethnicity,
|
||||
figure=figure,
|
||||
no_plus_women=no_plus_women,
|
||||
no_black=no_black,
|
||||
character_profile=character_profile,
|
||||
character_cast=character_cast,
|
||||
character_slot_map=character_slot_map,
|
||||
pov_character_labels=pov_character_labels,
|
||||
hard_women_count=hard_women_count,
|
||||
hard_men_count=hard_men_count,
|
||||
soft_category=soft_category,
|
||||
soft_subcategory=soft_subcategory,
|
||||
softcore_level_key=softcore_level_key,
|
||||
hardcore_random_subcategory=hardcore_random_subcategory,
|
||||
hardcore_position_config=hardcore_position_config,
|
||||
location_config=location_config,
|
||||
composition_config=composition_config,
|
||||
build_prompt=build_prompt,
|
||||
axis_rng=axis_rng,
|
||||
cast_expression_intensity_override=cast_expression_intensity_override,
|
||||
context_from_character_slot=context_from_character_slot,
|
||||
apply_character_context_to_row=apply_character_context_to_row,
|
||||
disable_row_expression=disable_row_expression,
|
||||
slot_softcore_outfit=slot_softcore_outfit,
|
||||
softcore_outfit=softcore_outfit,
|
||||
softcore_pose=softcore_pose,
|
||||
softcore_item_prompt_label=softcore_item_prompt_label,
|
||||
pov_prompt_directive=pov_prompt_directive,
|
||||
pov_composition_prompt=pov_composition_prompt,
|
||||
).as_dict()
|
||||
|
||||
+24
-24
@@ -2812,7 +2812,7 @@ def build_insta_of_pair(
|
||||
pov_character_labels = _pov_character_labels(character_slot_map, hard_men_count)
|
||||
softcore_level_key = str(options["softcore_level"])
|
||||
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
|
||||
row_route = pair_rows.build_insta_pair_rows(
|
||||
row_route = pair_rows.build_insta_pair_rows_result(
|
||||
row_number=row_number,
|
||||
start_index=start_index,
|
||||
seed=seed,
|
||||
@@ -2849,9 +2849,9 @@ def build_insta_of_pair(
|
||||
pov_prompt_directive=_pov_prompt_directive,
|
||||
pov_composition_prompt=_pov_composition_prompt,
|
||||
)
|
||||
soft_row = row_route["soft_row"]
|
||||
hard_row = row_route["hard_row"]
|
||||
hard_content_rng = row_route["hard_content_rng"]
|
||||
soft_row = row_route.soft_row
|
||||
hard_row = row_route.hard_row
|
||||
hard_content_rng = row_route.hard_content_rng
|
||||
|
||||
cast_context = pair_cast.resolve_insta_pair_cast_context(
|
||||
soft_row=soft_row,
|
||||
@@ -2885,7 +2885,7 @@ def build_insta_of_pair(
|
||||
platform_style = cast_context["platform_style"]
|
||||
soft_level = cast_context["soft_level"]
|
||||
hard_level = cast_context["hard_level"]
|
||||
camera_route = pair_camera.resolve_insta_pair_camera(
|
||||
camera_route = pair_camera.resolve_insta_pair_camera_result(
|
||||
soft_row=soft_row,
|
||||
hard_row=hard_row,
|
||||
options=options,
|
||||
@@ -2903,20 +2903,20 @@ def build_insta_of_pair(
|
||||
composition_prompt=_composition_prompt,
|
||||
camera_scene_directive_for_context=_camera_scene_directive_for_context,
|
||||
)
|
||||
soft_row = camera_route["soft_row"]
|
||||
hard_row = camera_route["hard_row"]
|
||||
hard_scene = camera_route["hard_scene"]
|
||||
hard_composition = camera_route["hard_composition"]
|
||||
soft_camera_config = camera_route["soft_camera_config"]
|
||||
hard_camera_config = camera_route["hard_camera_config"]
|
||||
soft_camera_directive = camera_route["soft_camera_directive"]
|
||||
hard_camera_directive = camera_route["hard_camera_directive"]
|
||||
soft_camera_scene_directive = camera_route["soft_camera_scene_directive"]
|
||||
hard_camera_scene_directive = camera_route["hard_camera_scene_directive"]
|
||||
soft_camera_scene_sentence = camera_route["soft_camera_scene_sentence"]
|
||||
hard_camera_scene_sentence = camera_route["hard_camera_scene_sentence"]
|
||||
soft_camera_sentence = camera_route["soft_camera_sentence"]
|
||||
hard_camera_sentence = camera_route["hard_camera_sentence"]
|
||||
soft_row = camera_route.soft_row
|
||||
hard_row = camera_route.hard_row
|
||||
hard_scene = camera_route.hard_scene
|
||||
hard_composition = camera_route.hard_composition
|
||||
soft_camera_config = camera_route.soft_camera_config
|
||||
hard_camera_config = camera_route.hard_camera_config
|
||||
soft_camera_directive = camera_route.soft_camera_directive
|
||||
hard_camera_directive = camera_route.hard_camera_directive
|
||||
soft_camera_scene_directive = camera_route.soft_camera_scene_directive
|
||||
hard_camera_scene_directive = camera_route.hard_camera_scene_directive
|
||||
soft_camera_scene_sentence = camera_route.soft_camera_scene_sentence
|
||||
hard_camera_scene_sentence = camera_route.hard_camera_scene_sentence
|
||||
soft_camera_sentence = camera_route.soft_camera_sentence
|
||||
hard_camera_sentence = camera_route.hard_camera_sentence
|
||||
soft_cast = cast_context["soft_cast"]
|
||||
soft_cast_presence = cast_context["soft_cast_presence"]
|
||||
soft_cast_styling_sentence = cast_context["soft_cast_styling_sentence"]
|
||||
@@ -2929,7 +2929,7 @@ def build_insta_of_pair(
|
||||
hard_content_rng,
|
||||
_slot_hardcore_clothing,
|
||||
)
|
||||
clothing_route = pair_clothing.resolve_hardcore_pair_clothing(
|
||||
clothing_route = pair_clothing.resolve_hardcore_pair_clothing_result(
|
||||
hard_row=hard_row,
|
||||
mode=options["hardcore_clothing_continuity"],
|
||||
softcore_outfit=soft_row["item"],
|
||||
@@ -2940,10 +2940,10 @@ def build_insta_of_pair(
|
||||
continuity_map=INSTA_OF_HARDCORE_CLOTHING_CONTINUITY,
|
||||
choose=g.choose,
|
||||
)
|
||||
default_man_hardcore_clothing_entries = clothing_route["default_man_hardcore_clothing"]
|
||||
hard_clothing_state = clothing_route["hardcore_clothing_state"]
|
||||
hard_clothing_sentence = clothing_route["hardcore_clothing_sentence"]
|
||||
if clothing_route["requires_body_exposure_scene"]:
|
||||
default_man_hardcore_clothing_entries = clothing_route.default_man_hardcore_clothing
|
||||
hard_clothing_state = clothing_route.hardcore_clothing_state
|
||||
hard_clothing_sentence = clothing_route.hardcore_clothing_sentence
|
||||
if clothing_route.requires_body_exposure_scene:
|
||||
hard_scene = pair_clothing.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["scene_text"] = hard_scene
|
||||
|
||||
@@ -45,8 +45,10 @@ import krea_cast # noqa: E402
|
||||
import krea_formatter # noqa: E402
|
||||
import location_config # noqa: E402
|
||||
import loop_nodes # noqa: E402
|
||||
import pair_camera # noqa: E402
|
||||
import pair_cast # noqa: E402
|
||||
import pair_clothing # noqa: E402
|
||||
import pair_rows # noqa: E402
|
||||
import prompt_builder as pb # noqa: E402
|
||||
import pov_policy # noqa: E402
|
||||
import row_normalization # noqa: E402
|
||||
@@ -2922,6 +2924,148 @@ def smoke_pair_options_policy() -> None:
|
||||
_expect(pb._insta_of_softcore_category("social_tease") == ("Casual clothes", "Casual clothes / Smart casual"), "softcore category mapping changed")
|
||||
|
||||
|
||||
def smoke_pair_route_policy() -> None:
|
||||
def _fake_build_prompt(**kwargs: Any) -> dict[str, Any]:
|
||||
is_hard = kwargs.get("category") == "Hardcore sexual poses"
|
||||
return {
|
||||
"category": kwargs.get("category"),
|
||||
"subcategory": kwargs.get("subcategory"),
|
||||
"scene_text": "shared test room",
|
||||
"source_scene_text": "",
|
||||
"composition": "centered test composition",
|
||||
"source_composition": "",
|
||||
"expression": "steady look",
|
||||
"role_graph": "test role graph",
|
||||
"item": "test item",
|
||||
"positive_suffix": "test positive suffix",
|
||||
"negative_prompt": "test negative",
|
||||
"prompt": "test hard prompt" if is_hard else "test soft prompt",
|
||||
"caption": "test hard caption" if is_hard else "test soft caption",
|
||||
}
|
||||
|
||||
pair_options_data = {
|
||||
"softcore_cast": "solo",
|
||||
"softcore_expression_enabled": True,
|
||||
"softcore_expression_intensity": 0.45,
|
||||
"hardcore_expression_enabled": True,
|
||||
"hardcore_expression_intensity": 0.85,
|
||||
"hardcore_detail_density": "balanced",
|
||||
}
|
||||
pair_rows_kwargs: dict[str, Any] = {
|
||||
"row_number": 1,
|
||||
"start_index": 1,
|
||||
"seed": 10,
|
||||
"active_trigger": Trigger,
|
||||
"parsed_seed_config": {},
|
||||
"options": pair_options_data,
|
||||
"ethnicity": "any",
|
||||
"figure": "random",
|
||||
"no_plus_women": False,
|
||||
"no_black": False,
|
||||
"character_profile": "",
|
||||
"character_cast": "",
|
||||
"character_slot_map": {},
|
||||
"pov_character_labels": [],
|
||||
"hard_women_count": 1,
|
||||
"hard_men_count": 1,
|
||||
"soft_category": "Casual clothes",
|
||||
"soft_subcategory": "Casual clothes / Smart casual",
|
||||
"softcore_level_key": "social_tease",
|
||||
"hardcore_random_subcategory": pb.RANDOM_SUBCATEGORY,
|
||||
"hardcore_position_config": "",
|
||||
"location_config": "",
|
||||
"composition_config": "",
|
||||
"build_prompt": _fake_build_prompt,
|
||||
"axis_rng": lambda _config, _axis, seed_value, row_value: random.Random(seed_value + row_value),
|
||||
"cast_expression_intensity_override": lambda value, _slots, _women, _men, _phase: (value, "input"),
|
||||
"context_from_character_slot": lambda *_args, **_kwargs: {},
|
||||
"apply_character_context_to_row": lambda row, context: {**row, **context},
|
||||
"disable_row_expression": lambda row, source: {**row, "expression_disabled": True, "expression_intensity_source": source},
|
||||
"slot_softcore_outfit": lambda _slot, _rng: "",
|
||||
"softcore_outfit": lambda _rng, _level: "test soft outfit",
|
||||
"softcore_pose": lambda _rng, _level: "test soft pose",
|
||||
"softcore_item_prompt_label": lambda _level: "Softcore test outfit",
|
||||
"pov_prompt_directive": lambda labels: "POV directive" if labels else "",
|
||||
"pov_composition_prompt": lambda composition, labels: f"{composition} for {','.join(labels)}" if labels else str(composition),
|
||||
}
|
||||
rows_route = pair_rows.build_insta_pair_rows_result(**pair_rows_kwargs)
|
||||
rows_legacy = pair_rows.build_insta_pair_rows(**pair_rows_kwargs)
|
||||
_expect(rows_route.soft_row == rows_legacy["soft_row"], "Typed pair row route should match legacy soft row")
|
||||
_expect(rows_route.hard_row == rows_legacy["hard_row"], "Typed pair row route should match legacy hard row")
|
||||
_expect(
|
||||
rows_route.hard_content_rng.getstate() == rows_legacy["hard_content_rng"].getstate(),
|
||||
"Typed pair row route should match legacy hard content RNG state",
|
||||
)
|
||||
_expect(rows_route.soft_row["item"] == "test soft outfit", "Typed pair row route lost soft outfit override")
|
||||
_expect(rows_route.hard_row["hardcore_detail_density"] == "balanced", "Typed pair row route lost hard density")
|
||||
|
||||
camera_options = {
|
||||
"hardcore_camera_mode": "same_as_softcore",
|
||||
"softcore_camera_mode": "standard",
|
||||
"camera_detail": "compact",
|
||||
"softcore_cast": "same_as_hardcore",
|
||||
"continuity": "same_creator_same_room",
|
||||
}
|
||||
|
||||
def _camera_config_with_mode(source: Any, mode: str) -> dict[str, Any]:
|
||||
parsed = dict(source or {})
|
||||
parsed["camera_mode"] = mode
|
||||
return parsed
|
||||
|
||||
def _camera_directive(config: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
||||
return f"{config.get('camera_mode')} camera", config
|
||||
|
||||
def _camera_rows() -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
return (
|
||||
{"scene_text": "soft room", "composition": "soft composition"},
|
||||
{"scene_text": "hard room", "composition": "hard composition"},
|
||||
)
|
||||
|
||||
camera_common = {
|
||||
"options": camera_options,
|
||||
"camera_config": {"base": "camera"},
|
||||
"softcore_camera_config": None,
|
||||
"hardcore_camera_config": None,
|
||||
"hard_women_count": 1,
|
||||
"hard_men_count": 1,
|
||||
"pov_character_labels": [],
|
||||
"camera_detail_choices": ("compact",),
|
||||
"camera_config_with_mode": _camera_config_with_mode,
|
||||
"camera_directive": _camera_directive,
|
||||
"apply_contextual_composition": lambda row, subject_kind: {**row, "subject_kind": subject_kind},
|
||||
"contextual_composition_prompt": lambda scene, composition, subject_kind: f"{subject_kind}: {scene}: {composition}",
|
||||
"composition_prompt": lambda composition: f"Framed as {composition}",
|
||||
"camera_scene_directive_for_context": lambda _scene, _composition, config, _pov, subject: (f"{subject} scene directive", config),
|
||||
}
|
||||
soft_row, hard_row = _camera_rows()
|
||||
camera_route = pair_camera.resolve_insta_pair_camera_result(soft_row=soft_row, hard_row=hard_row, **camera_common)
|
||||
soft_row, hard_row = _camera_rows()
|
||||
camera_legacy = pair_camera.resolve_insta_pair_camera(soft_row=soft_row, hard_row=hard_row, **camera_common)
|
||||
_expect(camera_route.as_dict() == camera_legacy, "Typed pair camera route should match legacy dict route")
|
||||
_expect(camera_route.hard_scene == "soft room", "Typed pair camera route lost same-room continuity")
|
||||
_expect(camera_route.hard_camera_sentence.startswith("Camera control:"), "Typed pair camera route lost hard camera sentence")
|
||||
|
||||
clothing_common = {
|
||||
"hard_row": {
|
||||
"role_graph": "the man thrusts his penis into the woman",
|
||||
"item": "penetrative test item",
|
||||
},
|
||||
"mode": "explicit_nude",
|
||||
"softcore_outfit": "test lingerie",
|
||||
"character_hardcore_clothing_entries": [],
|
||||
"men_count": 0,
|
||||
"pov_labels": [],
|
||||
"rng": random.Random(1),
|
||||
"continuity_map": pb.INSTA_OF_HARDCORE_CLOTHING_CONTINUITY,
|
||||
"choose": lambda _rng, pool: pool[0],
|
||||
}
|
||||
clothing_route = pair_clothing.resolve_hardcore_pair_clothing_result(**clothing_common)
|
||||
clothing_legacy = pair_clothing.resolve_hardcore_pair_clothing(**clothing_common)
|
||||
_expect(clothing_route.as_dict() == clothing_legacy, "Typed pair clothing route should match legacy dict route")
|
||||
_expect(clothing_route.woman_access == "lower", "Typed pair clothing route lost lower-access detection")
|
||||
_expect(clothing_route.requires_body_exposure_scene is True, "Typed pair clothing route lost exposure-scene flag")
|
||||
|
||||
|
||||
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")
|
||||
@@ -4823,6 +4967,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||
("hardcore_category_routes", smoke_hardcore_category_routes),
|
||||
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
|
||||
("pair_options_policy", smoke_pair_options_policy),
|
||||
("pair_route_policy", smoke_pair_route_policy),
|
||||
("insta_pair_same_cast", smoke_insta_pair),
|
||||
("krea_pair_clothing_state", smoke_krea_pair_clothing_state),
|
||||
("insta_pair_pov_man", smoke_insta_pair_pov),
|
||||
|
||||
Reference in New Issue
Block a user