diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index e9cd9ab..529e408 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -144,17 +144,18 @@ Owner today: `build_insta_of_pair`. Keep here: -- soft/hard row creation; - continuity policy; - softcore cast policy; Improve later: -- split pair assembly into small functions by phase: - `build_soft_row`, `build_hard_row`. +- split remaining pair cast/descriptor policy out of `build_insta_of_pair`. Already isolated: +- 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. - 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 diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index e241618..ed61e36 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -65,6 +65,7 @@ Core helper ownership: | Python module | What it owns | | --- | --- | | `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. | +| `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_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 hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, default visible-men clothing, and final root clothing-state assembly. | | `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. | diff --git a/pair_rows.py b/pair_rows.py new file mode 100644 index 0000000..01dd2f9 --- /dev/null +++ b/pair_rows.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +from typing import Any, Callable + + +BuildPrompt = Callable[..., dict[str, Any]] +AxisRng = Callable[[dict[str, int], str, int, int], Any] + + +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], + body_exposure_scene_text: Callable[[Any], str], + pov_prompt_directive: Callable[[list[str]], str], + pov_composition_prompt: Callable[[Any, list[str]], str], +) -> dict[str, Any]: + 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) + + soft_expression_women_count = hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1 + soft_expression_men_count = hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0 + soft_expression_enabled = bool(options["softcore_expression_enabled"]) + soft_expression_intensity = options["softcore_expression_intensity"] + soft_expression_intensity_source = "input" + if soft_expression_enabled: + soft_expression_intensity, soft_expression_intensity_source = cast_expression_intensity_override( + options["softcore_expression_intensity"], + character_slot_map, + soft_expression_women_count, + soft_expression_men_count, + "softcore", + ) + if soft_expression_intensity is None: + soft_expression_enabled = False + else: + soft_expression_intensity_source = "disabled" + + primary_slot = character_slot_map.get("Woman A") + primary_slot_context = None + if primary_slot: + primary_slot_context = context_from_character_slot( + soft_person_rng, + primary_slot, + "woman", + ethnicity, + figure, + no_plus_women, + no_black, + ) + + soft_row = build_prompt( + category=soft_category, + subcategory=soft_subcategory, + row_number=row_number, + start_index=start_index, + seed=seed, + clothing="minimal", + ethnicity=ethnicity, + poses="evocative", + backside_bias=0.0, + figure=figure, + no_plus_women=no_plus_women, + no_black=no_black, + minimal_clothing_ratio=-1, + standard_pose_ratio=-1, + trigger=active_trigger, + prepend_trigger_to_prompt=False, + extra_positive="", + extra_negative="", + seed_config=parsed_seed_config, + women_count=1, + men_count=0, + expression_enabled=soft_expression_enabled, + expression_intensity=soft_expression_intensity, + character_profile="" if primary_slot else character_profile or "", + character_cast="", + location_config=location_config or "", + composition_config=composition_config or "", + ) + soft_row["expression_intensity_source"] = soft_expression_intensity_source + if primary_slot_context: + soft_row = apply_character_context_to_row(soft_row, primary_slot_context) + soft_row["character_slot"] = primary_slot + soft_row["character_slot_status"] = "applied:Woman A" + if not soft_expression_enabled: + soft_row = disable_row_expression(soft_row, soft_expression_intensity_source) + + primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_content_rng) + soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_content_rng, softcore_level_key) + soft_row["pose"] = softcore_pose(soft_content_rng, softcore_level_key) + soft_row["item_label"] = ( + "Insta/OF softcore body exposure" + if softcore_level_key == "explicit_nude" + else "Insta/OF softcore outfit" + ) + soft_row["softcore_item_prompt_label"] = softcore_item_prompt_label(softcore_level_key) + soft_row["custom_item"] = "insta_of_softcore_outfit" + soft_row["softcore_outfit_policy"] = "character_slot:Woman A" if primary_softcore_outfit else "insta_of_safe_softcore" + if softcore_level_key == "explicit_nude": + soft_row["source_scene_text"] = soft_row.get("source_scene_text") or soft_row.get("scene_text", "") + soft_row["scene_text"] = body_exposure_scene_text(soft_row.get("scene_text", "")) + soft_row["pov_character_labels"] = ( + pov_character_labels + if options["softcore_cast"] == "same_as_hardcore" + else [] + ) + soft_row["pov_prompt_directive"] = pov_prompt_directive(soft_row["pov_character_labels"]) + if soft_row["pov_character_labels"]: + soft_row["source_composition"] = soft_row.get("source_composition") or soft_row.get("composition", "") + soft_row["composition"] = pov_composition_prompt( + soft_row["source_composition"], + soft_row["pov_character_labels"], + ) + + hard_row = build_prompt( + category="Hardcore sexual poses", + subcategory=hardcore_random_subcategory, + row_number=row_number, + start_index=start_index, + seed=seed, + clothing="minimal", + ethnicity=ethnicity, + poses="evocative", + backside_bias=0.0, + figure=figure, + no_plus_women=no_plus_women, + no_black=no_black, + minimal_clothing_ratio=-1, + standard_pose_ratio=-1, + trigger=active_trigger, + prepend_trigger_to_prompt=False, + extra_positive="", + extra_negative="", + seed_config=parsed_seed_config, + women_count=hard_women_count, + men_count=hard_men_count, + expression_enabled=options["hardcore_expression_enabled"], + expression_intensity=options["hardcore_expression_intensity"], + character_cast=character_cast or "", + expression_phase="hardcore", + hardcore_position_config=hardcore_position_config or "", + location_config=location_config or "", + composition_config=composition_config or "", + ) + hard_row["hardcore_detail_density"] = options["hardcore_detail_density"] + 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, + } diff --git a/prompt_builder.py b/prompt_builder.py index 2b6eed6..7d07f5d 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -28,6 +28,7 @@ try: from . import pair_clothing from . import pair_camera from . import pair_output + from . import pair_rows from . import scene_camera_adapters from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -60,6 +61,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import pair_clothing import pair_camera import pair_output + import pair_rows import scene_camera_adapters from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -6993,130 +6995,47 @@ 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) - 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) - soft_expression_women_count = hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1 - soft_expression_men_count = hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0 - soft_expression_enabled = bool(options["softcore_expression_enabled"]) - soft_expression_intensity = options["softcore_expression_intensity"] - soft_expression_intensity_source = "input" - if soft_expression_enabled: - soft_expression_intensity, soft_expression_intensity_source = _cast_expression_intensity_override( - options["softcore_expression_intensity"], - character_slot_map, - soft_expression_women_count, - soft_expression_men_count, - "softcore", - ) - if soft_expression_intensity is None: - soft_expression_enabled = False - else: - soft_expression_intensity_source = "disabled" - primary_slot_context = None - primary_slot = character_slot_map.get("Woman A") - if primary_slot: - primary_slot_context = _context_from_character_slot( - soft_person_rng, - primary_slot, - "woman", - ethnicity, - figure, - no_plus_women, - no_black, - ) - - soft_row = build_prompt( - category=soft_category, - subcategory=soft_subcategory, + row_route = pair_rows.build_insta_pair_rows( row_number=row_number, start_index=start_index, seed=seed, - clothing="minimal", + active_trigger=active_trigger, + parsed_seed_config=parsed_seed_config, + options=options, ethnicity=ethnicity, - poses="evocative", - backside_bias=0.0, figure=figure, no_plus_women=no_plus_women, no_black=no_black, - minimal_clothing_ratio=-1, - standard_pose_ratio=-1, - trigger=active_trigger, - prepend_trigger_to_prompt=False, - extra_positive="", - extra_negative="", - seed_config=parsed_seed_config, - women_count=1, - men_count=0, - expression_enabled=soft_expression_enabled, - expression_intensity=soft_expression_intensity, - character_profile="" if primary_slot else character_profile or "", - character_cast="", - location_config=location_config or "", - composition_config=composition_config or "", - ) - soft_row["expression_intensity_source"] = soft_expression_intensity_source - if primary_slot_context: - soft_row = _apply_character_context_to_row(soft_row, primary_slot_context) - soft_row["character_slot"] = primary_slot - soft_row["character_slot_status"] = "applied:Woman A" - if not soft_expression_enabled: - soft_row = _disable_row_expression(soft_row, soft_expression_intensity_source) - primary_softcore_outfit = _slot_softcore_outfit(primary_slot, soft_content_rng) - soft_row["item"] = primary_softcore_outfit or _insta_of_softcore_outfit(soft_content_rng, softcore_level_key) - soft_row["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key) - soft_row["item_label"] = "Insta/OF softcore body exposure" if softcore_level_key == "explicit_nude" else "Insta/OF softcore outfit" - soft_row["softcore_item_prompt_label"] = _insta_of_softcore_item_prompt_label(softcore_level_key) - soft_row["custom_item"] = "insta_of_softcore_outfit" - soft_row["softcore_outfit_policy"] = "character_slot:Woman A" if primary_softcore_outfit else "insta_of_safe_softcore" - if softcore_level_key == "explicit_nude": - soft_row["source_scene_text"] = soft_row.get("source_scene_text") or soft_row.get("scene_text", "") - soft_row["scene_text"] = _body_exposure_scene_text(soft_row.get("scene_text", "")) - soft_row["pov_character_labels"] = ( - pov_character_labels - if options["softcore_cast"] == "same_as_hardcore" - else [] - ) - soft_row["pov_prompt_directive"] = _pov_prompt_directive(soft_row["pov_character_labels"]) - if soft_row["pov_character_labels"]: - soft_row["source_composition"] = soft_row.get("source_composition") or soft_row.get("composition", "") - soft_row["composition"] = _pov_composition_prompt( - soft_row["source_composition"], - soft_row["pov_character_labels"], - ) - hard_row = build_prompt( - category="Hardcore sexual poses", - subcategory=RANDOM_SUBCATEGORY, - row_number=row_number, - start_index=start_index, - seed=seed, - clothing="minimal", - ethnicity=ethnicity, - poses="evocative", - backside_bias=0.0, - figure=figure, - no_plus_women=no_plus_women, - no_black=no_black, - minimal_clothing_ratio=-1, - standard_pose_ratio=-1, - trigger=active_trigger, - prepend_trigger_to_prompt=False, - extra_positive="", - extra_negative="", - seed_config=parsed_seed_config, - women_count=hard_women_count, - men_count=hard_men_count, - expression_enabled=options["hardcore_expression_enabled"], - expression_intensity=options["hardcore_expression_intensity"], + character_profile=character_profile, character_cast=character_cast or "", - expression_phase="hardcore", - hardcore_position_config=hardcore_position_config or "", + 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=RANDOM_SUBCATEGORY, + hardcore_position_config=hardcore_position_config, location_config=location_config or "", composition_config=composition_config or "", + 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=_insta_of_softcore_outfit, + softcore_pose=_insta_of_softcore_pose, + softcore_item_prompt_label=_insta_of_softcore_item_prompt_label, + body_exposure_scene_text=_body_exposure_scene_text, + pov_prompt_directive=_pov_prompt_directive, + pov_composition_prompt=_pov_composition_prompt, ) - hard_row["hardcore_detail_density"] = options["hardcore_detail_density"] - hard_row["pov_character_labels"] = pov_character_labels - hard_row["pov_prompt_directive"] = _pov_prompt_directive(pov_character_labels) + soft_row = row_route["soft_row"] + hard_row = row_route["hard_row"] + hard_content_rng = row_route["hard_content_rng"] descriptor = _insta_of_descriptor(soft_row) cast_descriptors = _insta_of_cast_descriptors(