diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 40bf6e4..d546609 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -275,13 +275,13 @@ Already isolated: ### Pair / Adapter Layer -Owner today: `build_insta_of_pair`. +Owner today: `pair_builder.py`; `prompt_builder.build_insta_of_pair` is the +public wrapper used by the node layer. Keep here: -- pair route sequencing; -- top-level continuity option handoff between row, camera, clothing, and output - adapters. +- the public wrapper signature and dependency bridge needed by existing nodes + and tests. Already isolated: @@ -290,6 +290,10 @@ 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. +- pair route sequencing now lives in `pair_builder.py` behind + `InstaPairBuildRequest` and `InstaPairBuildDependencies`, covering + option/filter/seed/cast parsing handoff, soft/hard row orchestration, cast + context, camera route, clothing route, and final output assembly delegation. - 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 diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index cc5f828..a6251c4 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -96,6 +96,7 @@ 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_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. | | `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. | @@ -420,13 +421,14 @@ Edit targets: ```mermaid flowchart TD - O[SxCP Insta/OF Options] --> P[build_insta_of_pair] + O[SxCP Insta/OF Options] --> P[prompt_builder wrapper] C[character_cast] --> P S[seed_config] --> P L[location_config] --> P M[composition_config] --> P H[hardcore_position_config] --> P - P --> R[pair_rows.py] + P --> Q[pair_builder.py] + Q --> R[pair_rows.py] R --> A[soft_row via build_prompt] R --> B[hard_row via build_prompt] A --> D[pair_cast.py] diff --git a/pair_builder.py b/pair_builder.py new file mode 100644 index 0000000..4743148 --- /dev/null +++ b/pair_builder.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable + +try: + from . import pair_camera + from . import pair_cast + from . import pair_clothing + from . import pair_output + from . import pair_rows +except ImportError: # Allows local smoke tests with top-level imports. + import pair_camera + import pair_cast + import pair_clothing + import pair_output + import pair_rows + + +BuildPrompt = Callable[..., dict[str, Any]] +AxisRng = Callable[[dict[str, int], str, int, int], Any] +Choose = Callable[[Any, list[str]], str] + + +@dataclass(frozen=True) +class InstaPairBuildRequest: + row_number: int + start_index: int + seed: int + ethnicity: str + figure: str + no_plus_women: bool + no_black: bool + trigger: str + prepend_trigger_to_prompt: bool + seed_config: str | dict[str, Any] | None = None + options_json: str | dict[str, Any] | None = None + filter_config: str | dict[str, Any] | None = None + camera_config: str | dict[str, Any] | None = None + softcore_camera_config: str | dict[str, Any] | None = None + hardcore_camera_config: str | dict[str, Any] | None = None + character_profile: str | dict[str, Any] | None = "" + character_cast: str | dict[str, Any] | list[Any] | None = "" + hardcore_position_config: str | dict[str, Any] | None = "" + location_config: str | dict[str, Any] | None = "" + composition_config: str | dict[str, Any] | None = "" + extra_positive: str = "" + extra_negative: str = "" + + +@dataclass(frozen=True) +class InstaPairBuildDependencies: + default_trigger: str + random_subcategory: str + soft_negative_base: str + hard_negative_base: str + camera_detail_choices: list[str] | tuple[str, ...] + hardcore_clothing_continuity: dict[str, str] + platform_styles: dict[str, str] + soft_levels: dict[str, str] + hardcore_levels: dict[str, str] + parse_options: Callable[[str | dict[str, Any] | None], dict[str, Any]] + parse_filter_config: Callable[[str | dict[str, Any] | None], dict[str, Any]] + parse_seed_config: Callable[[str | dict[str, Any] | None], dict[str, int]] + parse_character_cast: Callable[[str | dict[str, Any] | list[Any] | None], list[dict[str, Any]]] + character_slot_label_map: Callable[[list[dict[str, Any]]], dict[str, dict[str, Any]]] + pov_character_labels: Callable[[dict[str, dict[str, Any]], int], list[str]] + softcore_category: Callable[[str], tuple[str, str]] + 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] + hardcore_counts: Callable[[dict[str, Any]], tuple[int, int]] + character_context_for_label: Callable[ + [str, dict[str, dict[str, Any]], Any, str, str, bool, bool], + tuple[dict[str, Any], dict[str, Any] | None], + ] + slot_is_pov: Callable[[dict[str, Any] | None], bool] + choose: Choose + camera_config_with_mode: Callable[[str | dict[str, Any] | None, str], dict[str, Any]] + camera_directive: Callable[[str | dict[str, Any] | None], tuple[str, dict[str, Any]]] + apply_contextual_composition: Callable[[dict[str, Any], str], dict[str, Any]] + contextual_composition_prompt: Callable[[Any, Any, str], str] + composition_prompt: Callable[[Any], str] + camera_scene_directive_for_context: Callable[ + [Any, Any, str | dict[str, Any] | None, list[str] | None, str], + tuple[str, dict[str, Any]], + ] + slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] + hardcore_detail_directive: Callable[[Any], str] + camera_caption_text: Callable[[dict[str, Any]], str] + + +def build_insta_of_pair(request: InstaPairBuildRequest, deps: InstaPairBuildDependencies) -> dict[str, Any]: + options = deps.parse_options(request.options_json) + ethnicity = request.ethnicity + figure = request.figure + no_plus_women = request.no_plus_women + no_black = request.no_black + if request.filter_config: + filters = deps.parse_filter_config(request.filter_config) + ethnicity = filters["ethnicity"] + figure = filters["figure"] + no_plus_women = filters["no_plus_women"] + no_black = filters["no_black"] + + hard_women_count, hard_men_count = deps.hardcore_counts(options) + active_trigger = request.trigger.strip() or deps.default_trigger + parsed_seed_config = deps.parse_seed_config(request.seed_config) + character_slots = deps.parse_character_cast(request.character_cast) + character_slot_map = deps.character_slot_label_map(character_slots) + pov_character_labels = deps.pov_character_labels(character_slot_map, hard_men_count) + softcore_level_key = str(options["softcore_level"]) + soft_category, soft_subcategory = deps.softcore_category(softcore_level_key) + + row_route = pair_rows.build_insta_pair_rows_result( + row_number=request.row_number, + start_index=request.start_index, + seed=request.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=request.character_profile, + character_cast=request.character_cast 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=deps.random_subcategory, + hardcore_position_config=request.hardcore_position_config, + location_config=request.location_config or "", + composition_config=request.composition_config or "", + build_prompt=deps.build_prompt, + axis_rng=deps.axis_rng, + cast_expression_intensity_override=deps.cast_expression_intensity_override, + context_from_character_slot=deps.context_from_character_slot, + apply_character_context_to_row=deps.apply_character_context_to_row, + disable_row_expression=deps.disable_row_expression, + slot_softcore_outfit=deps.slot_softcore_outfit, + softcore_outfit=deps.softcore_outfit, + softcore_pose=deps.softcore_pose, + softcore_item_prompt_label=deps.softcore_item_prompt_label, + pov_prompt_directive=deps.pov_prompt_directive, + pov_composition_prompt=deps.pov_composition_prompt, + ) + 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, + options=options, + parsed_seed_config=parsed_seed_config, + seed=request.seed, + row_number=request.row_number, + ethnicity=ethnicity, + figure=figure, + no_plus_women=no_plus_women, + no_black=no_black, + hard_women_count=hard_women_count, + hard_men_count=hard_men_count, + character_slots=character_slots, + character_slot_map=character_slot_map, + pov_character_labels=pov_character_labels, + platform_styles=deps.platform_styles, + soft_levels=deps.soft_levels, + hardcore_levels=deps.hardcore_levels, + axis_rng=deps.axis_rng, + character_context_for_label=deps.character_context_for_label, + slot_is_pov=deps.slot_is_pov, + choose=deps.choose, + slot_softcore_outfit=deps.slot_softcore_outfit, + ) + + camera_route = pair_camera.resolve_insta_pair_camera_result( + soft_row=soft_row, + hard_row=hard_row, + options=options, + camera_config=request.camera_config, + softcore_camera_config=request.softcore_camera_config, + hardcore_camera_config=request.hardcore_camera_config, + hard_women_count=hard_women_count, + hard_men_count=hard_men_count, + pov_character_labels=pov_character_labels, + camera_detail_choices=deps.camera_detail_choices, + camera_config_with_mode=deps.camera_config_with_mode, + camera_directive=deps.camera_directive, + apply_contextual_composition=deps.apply_contextual_composition, + contextual_composition_prompt=deps.contextual_composition_prompt, + composition_prompt=deps.composition_prompt, + camera_scene_directive_for_context=deps.camera_scene_directive_for_context, + ) + soft_row = camera_route.soft_row + hard_row = camera_route.hard_row + hard_scene = camera_route.hard_scene + + character_hardcore_clothing_entries = pair_clothing.character_hardcore_clothing_entries( + character_slot_map, + hard_women_count, + hard_men_count, + pov_character_labels, + hard_content_rng, + deps.slot_hardcore_clothing, + ) + clothing_route = pair_clothing.resolve_hardcore_pair_clothing_result( + hard_row=hard_row, + mode=options["hardcore_clothing_continuity"], + softcore_outfit=soft_row["item"], + character_hardcore_clothing_entries=character_hardcore_clothing_entries, + men_count=hard_men_count, + pov_labels=pov_character_labels, + rng=hard_content_rng, + continuity_map=deps.hardcore_clothing_continuity, + choose=deps.choose, + ) + 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 + + hard_detail_density = options["hardcore_detail_density"] + return pair_output.assemble_insta_pair_metadata( + active_trigger=active_trigger, + prepend_trigger_to_prompt=bool(request.prepend_trigger_to_prompt), + extra_positive=request.extra_positive, + extra_negative=request.extra_negative, + soft_negative_base=deps.soft_negative_base, + hard_negative_base=deps.hard_negative_base, + options=options, + platform_style=cast_context["platform_style"], + soft_descriptor_sentence=cast_context["soft_descriptor_sentence"], + soft_level=cast_context["soft_level"], + soft_cast=cast_context["soft_cast"], + soft_cast_presence=cast_context["soft_cast_presence"], + soft_cast_styling_sentence=cast_context["soft_cast_styling_sentence"], + soft_row=soft_row, + soft_camera_scene_sentence=camera_route.soft_camera_scene_sentence, + soft_camera_sentence=camera_route.soft_camera_sentence, + hard_level=cast_context["hard_level"], + hard_cast=cast_context["hard_cast"], + cast_descriptor_text=cast_context["cast_descriptor_text"], + pov_directive=deps.pov_prompt_directive(pov_character_labels), + pov_character_labels=pov_character_labels, + hard_clothing_sentence=clothing_route.hardcore_clothing_sentence, + hard_row=hard_row, + hard_scene=hard_scene, + hard_camera_scene_sentence=camera_route.hard_camera_scene_sentence, + hard_composition=camera_route.hard_composition, + hard_detail_directive=deps.hardcore_detail_directive(hard_detail_density), + hard_camera_sentence=camera_route.hard_camera_sentence, + descriptor=cast_context["descriptor"], + soft_partner_outfit_text=cast_context["soft_partner_outfit_text"], + soft_partner_styling=cast_context["soft_partner_styling"], + soft_camera_scene_directive=camera_route.soft_camera_scene_directive, + soft_camera_config=camera_route.soft_camera_config, + soft_camera_directive=camera_route.soft_camera_directive, + hard_camera_scene_directive=camera_route.hard_camera_scene_directive, + hard_camera_config=camera_route.hard_camera_config, + hard_camera_directive=camera_route.hard_camera_directive, + camera_caption_text=deps.camera_caption_text, + cast_descriptors=cast_context["cast_descriptors"], + character_hardcore_clothing_entries=character_hardcore_clothing_entries, + default_man_hardcore_clothing_entries=clothing_route.default_man_hardcore_clothing, + hard_clothing_state=clothing_route.hardcore_clothing_state, + hard_detail_density=hard_detail_density, + hard_women_count=hard_women_count, + hard_men_count=hard_men_count, + character_slots=character_slots, + character_slot_map=character_slot_map, + ) diff --git a/prompt_builder.py b/prompt_builder.py index 7e7e2aa..9344c06 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -26,11 +26,8 @@ try: from . import generation_profile_config as generation_profile_policy from . import hardcore_position_config as hardcore_position_policy from . import location_config as location_policy - from . import pair_clothing - from . import pair_camera + from . import pair_builder from . import pair_cast - from . import pair_output - from . import pair_rows from . import pair_options from . import pov_policy from . import row_normalization as row_policy @@ -75,11 +72,8 @@ except ImportError: # Allows local smoke tests with `python -c`. import generation_profile_config as generation_profile_policy import hardcore_position_config as hardcore_position_policy import location_config as location_policy - import pair_clothing - import pair_camera + import pair_builder import pair_cast - import pair_output - import pair_rows import pair_options import pov_policy import row_normalization as row_policy @@ -2773,6 +2767,52 @@ def _insta_of_softcore_pose(rng: random.Random, level: str) -> str: return g.choose(rng, pair_options.softcore_pose_pool(level)) +def _insta_pair_build_dependencies() -> pair_builder.InstaPairBuildDependencies: + return pair_builder.InstaPairBuildDependencies( + default_trigger=g.TRIGGER, + random_subcategory=RANDOM_SUBCATEGORY, + soft_negative_base=INSTA_OF_SOFT_NEGATIVE, + hard_negative_base=INSTA_OF_NEGATIVE, + camera_detail_choices=CAMERA_DETAIL_CHOICES, + hardcore_clothing_continuity=INSTA_OF_HARDCORE_CLOTHING_CONTINUITY, + platform_styles=INSTA_OF_PLATFORM_STYLES, + soft_levels=INSTA_OF_SOFT_LEVELS, + hardcore_levels=INSTA_OF_HARDCORE_LEVELS, + parse_options=_parse_insta_of_options, + parse_filter_config=_parse_filter_config, + parse_seed_config=_parse_seed_config, + parse_character_cast=_parse_character_cast, + character_slot_label_map=_character_slot_label_map, + pov_character_labels=_pov_character_labels, + softcore_category=_insta_of_softcore_category, + 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, + pov_prompt_directive=_pov_prompt_directive, + pov_composition_prompt=_pov_composition_prompt, + hardcore_counts=_insta_of_hardcore_counts, + character_context_for_label=_character_context_for_label, + slot_is_pov=_slot_is_pov, + choose=g.choose, + camera_config_with_mode=_camera_config_with_mode, + camera_directive=_camera_directive, + apply_contextual_composition=_apply_coworking_composition, + contextual_composition_prompt=_coworking_composition_prompt, + composition_prompt=_composition_prompt, + camera_scene_directive_for_context=_camera_scene_directive_for_context, + slot_hardcore_clothing=_slot_hardcore_clothing, + hardcore_detail_directive=pair_options.hardcore_detail_directive, + camera_caption_text=_camera_caption_text, + ) + + def build_insta_of_pair( row_number: int, start_index: int, @@ -2797,207 +2837,28 @@ def build_insta_of_pair( extra_positive: str = "", extra_negative: str = "", ) -> dict[str, Any]: - options = _parse_insta_of_options(options_json) - if filter_config: - filters = _parse_filter_config(filter_config) - ethnicity = filters["ethnicity"] - figure = filters["figure"] - no_plus_women = filters["no_plus_women"] - no_black = filters["no_black"] - hard_women_count, hard_men_count = _insta_of_hardcore_counts(options) - active_trigger = trigger.strip() or g.TRIGGER - parsed_seed_config = _parse_seed_config(seed_config) - character_slots = _parse_character_cast(character_cast) - character_slot_map = _character_slot_label_map(character_slots) - 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_result( + request = pair_builder.InstaPairBuildRequest( 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 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, - 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 - - cast_context = pair_cast.resolve_insta_pair_cast_context( - soft_row=soft_row, - options=options, - parsed_seed_config=parsed_seed_config, - seed=seed, - row_number=row_number, - ethnicity=ethnicity, - figure=figure, - no_plus_women=no_plus_women, - no_black=no_black, - hard_women_count=hard_women_count, - hard_men_count=hard_men_count, - character_slots=character_slots, - character_slot_map=character_slot_map, - pov_character_labels=pov_character_labels, - platform_styles=INSTA_OF_PLATFORM_STYLES, - soft_levels=INSTA_OF_SOFT_LEVELS, - hardcore_levels=INSTA_OF_HARDCORE_LEVELS, - axis_rng=_axis_rng, - character_context_for_label=_character_context_for_label, - slot_is_pov=_slot_is_pov, - choose=g.choose, - slot_softcore_outfit=_slot_softcore_outfit, - ) - descriptor = cast_context["descriptor"] - cast_descriptors = cast_context["cast_descriptors"] - cast_descriptor_text = cast_context["cast_descriptor_text"] - soft_partner_styling = cast_context["soft_partner_styling"] - soft_partner_outfit_text = cast_context["soft_partner_outfit_text"] - 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_result( - soft_row=soft_row, - hard_row=hard_row, - options=options, + trigger=trigger, + prepend_trigger_to_prompt=prepend_trigger_to_prompt, + seed_config=seed_config, + options_json=options_json, + filter_config=filter_config, 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_coworking_composition, - contextual_composition_prompt=_coworking_composition_prompt, - 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_cast = cast_context["soft_cast"] - soft_cast_presence = cast_context["soft_cast_presence"] - soft_cast_styling_sentence = cast_context["soft_cast_styling_sentence"] - hard_cast = cast_context["hard_cast"] - character_hardcore_clothing_entries = pair_clothing.character_hardcore_clothing_entries( - character_slot_map, - hard_women_count, - hard_men_count, - pov_character_labels, - hard_content_rng, - _slot_hardcore_clothing, - ) - clothing_route = pair_clothing.resolve_hardcore_pair_clothing_result( - hard_row=hard_row, - mode=options["hardcore_clothing_continuity"], - softcore_outfit=soft_row["item"], - character_hardcore_clothing_entries=character_hardcore_clothing_entries, - men_count=hard_men_count, - pov_labels=pov_character_labels, - rng=hard_content_rng, - 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: - 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 - hard_detail_density = options["hardcore_detail_density"] - hard_detail_directive = pair_options.hardcore_detail_directive(hard_detail_density) - pov_directive = _pov_prompt_directive(pov_character_labels) - soft_descriptor_sentence = cast_context["soft_descriptor_sentence"] - - return pair_output.assemble_insta_pair_metadata( - active_trigger=active_trigger, - prepend_trigger_to_prompt=bool(prepend_trigger_to_prompt), + character_profile=character_profile, + character_cast=character_cast, + hardcore_position_config=hardcore_position_config, + location_config=location_config, + composition_config=composition_config, extra_positive=extra_positive, extra_negative=extra_negative, - soft_negative_base=INSTA_OF_SOFT_NEGATIVE, - hard_negative_base=INSTA_OF_NEGATIVE, - options=options, - platform_style=platform_style, - soft_descriptor_sentence=soft_descriptor_sentence, - soft_level=soft_level, - soft_cast=soft_cast, - soft_cast_presence=soft_cast_presence, - soft_cast_styling_sentence=soft_cast_styling_sentence, - soft_row=soft_row, - soft_camera_scene_sentence=soft_camera_scene_sentence, - soft_camera_sentence=soft_camera_sentence, - hard_level=hard_level, - hard_cast=hard_cast, - cast_descriptor_text=cast_descriptor_text, - pov_directive=pov_directive, - pov_character_labels=pov_character_labels, - hard_clothing_sentence=hard_clothing_sentence, - hard_row=hard_row, - hard_scene=hard_scene, - hard_camera_scene_sentence=hard_camera_scene_sentence, - hard_composition=hard_composition, - hard_detail_directive=hard_detail_directive, - hard_camera_sentence=hard_camera_sentence, - descriptor=descriptor, - soft_partner_outfit_text=soft_partner_outfit_text, - soft_partner_styling=soft_partner_styling, - soft_camera_scene_directive=soft_camera_scene_directive, - soft_camera_config=soft_camera_config, - soft_camera_directive=soft_camera_directive, - hard_camera_scene_directive=hard_camera_scene_directive, - hard_camera_config=hard_camera_config, - hard_camera_directive=hard_camera_directive, - camera_caption_text=_camera_caption_text, - cast_descriptors=cast_descriptors, - character_hardcore_clothing_entries=character_hardcore_clothing_entries, - default_man_hardcore_clothing_entries=default_man_hardcore_clothing_entries, - hard_clothing_state=hard_clothing_state, - hard_detail_density=hard_detail_density, - hard_women_count=hard_women_count, - hard_men_count=hard_men_count, - character_slots=character_slots, - character_slot_map=character_slot_map, ) + return pair_builder.build_insta_of_pair(request, _insta_pair_build_dependencies()) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 5424dc7..7569753 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -45,6 +45,7 @@ import krea_cast # noqa: E402 import krea_formatter # noqa: E402 import location_config # noqa: E402 import loop_nodes # noqa: E402 +import pair_builder # noqa: E402 import pair_camera # noqa: E402 import pair_cast # noqa: E402 import pair_clothing # noqa: E402 @@ -3066,6 +3067,50 @@ def smoke_pair_route_policy() -> None: _expect(clothing_route.requires_body_exposure_scene is True, "Typed pair clothing route lost exposure-scene flag") +def smoke_pair_builder_policy() -> None: + request = pair_builder.InstaPairBuildRequest( + 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"), + ) + built = pair_builder.build_insta_of_pair(request, pb._insta_pair_build_dependencies()) + delegated = pb.build_insta_of_pair( + row_number=request.row_number, + start_index=request.start_index, + seed=request.seed, + ethnicity=request.ethnicity, + figure=request.figure, + no_plus_women=request.no_plus_women, + no_black=request.no_black, + trigger=request.trigger, + prepend_trigger_to_prompt=request.prepend_trigger_to_prompt, + seed_config=request.seed_config, + options_json=request.options_json, + filter_config=request.filter_config, + camera_config=request.camera_config, + softcore_camera_config=request.softcore_camera_config, + hardcore_camera_config=request.hardcore_camera_config, + character_profile=request.character_profile, + character_cast=request.character_cast, + hardcore_position_config=request.hardcore_position_config, + location_config=request.location_config, + composition_config=request.composition_config, + extra_positive=request.extra_positive, + extra_negative=request.extra_negative, + ) + _expect(built == delegated, "Prompt builder Insta/OF wrapper should delegate to pair_builder without output drift") + _expect_pair(built, "pair_builder_policy") + + 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") @@ -4968,6 +5013,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("krea_close_foreplay_route", smoke_krea_close_foreplay_route), ("pair_options_policy", smoke_pair_options_policy), ("pair_route_policy", smoke_pair_route_policy), + ("pair_builder_policy", smoke_pair_builder_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),