diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index f46ba9d..e9cd9ab 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -147,14 +147,11 @@ Keep here: - soft/hard row creation; - continuity policy; - softcore cast policy; -- pair metadata shape. Improve later: -- make a single pair metadata sanitizer that normalizes `softcore_row`, - `hardcore_row`, pair prompts, negatives, captions, and camera fields; - split pair assembly into small functions by phase: - `build_soft_row`, `build_hard_row`, `assemble_pair_metadata`. + `build_soft_row`, `build_hard_row`. Already isolated: @@ -166,6 +163,9 @@ Already isolated: including action-aware body-access flags, conflicting outfit-piece cleanup, default visible-men clothing, character-clothing override handling, and final root clothing-state assembly. +- final pair output assembly lives in `pair_output.py`, including soft/hard + prompt strings, trigger preservation, negatives, captions, and root metadata + shape. ### Krea2 Formatter Path diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index f0f5e4e..e241618 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -67,6 +67,7 @@ Core helper ownership: | `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_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. | | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | | `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. | | `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. | @@ -456,8 +457,8 @@ plain prompt text. When debugging, inspect these fields before editing pools. | `shared_descriptor` | Soft row descriptor | Pair formatters | Primary creator descriptor. | | `shared_cast_descriptors` | Cast descriptor builder | Pair formatters | Full cast descriptor list. | | `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side. | -| `softcore_prompt`, `hardcore_prompt` | Pair assembly | Direct output/fallback | Raw pair prompts before formatter rewrite. | -| `softcore_negative_prompt`, `hardcore_negative_prompt` | Pair assembly | Formatter negatives | Separate negatives for each side. | +| `softcore_prompt`, `hardcore_prompt` | `pair_output.py` | Direct output/fallback | Raw pair prompts before formatter rewrite. | +| `softcore_negative_prompt`, `hardcore_negative_prompt` | `pair_output.py` | Formatter negatives | Separate negatives for each side. | | `softcore_partner_styling` | `_insta_of_partner_styling` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. | | `character_hardcore_clothing` | Character slots | Krea pair branch | Explicit per-character hardcore clothing state. | | `default_man_hardcore_clothing` | Pair fallback | Krea pair branch | Auto clothing for visible men without configured clothing. | @@ -851,8 +852,8 @@ Use these traces to narrow a problem in one pass. 1. Check whether the prompt came from pair softcore or normal category builder. 2. In pair softcore, inspect `softcore_partner_styling`, `softcore_row.item`, `softcore_row.pose`, and options `softcore_cast`. -3. If the raw soft prompt contains awkward defensive clauses, fix - `build_insta_of_pair` soft prompt assembly. +3. If the raw soft prompt contains awkward defensive clauses, inspect + `pair_output.py`. 4. If Krea adds the awkwardness, inspect `_insta_pair_to_krea`. ### Location composition mentions irrelevant props diff --git a/pair_output.py b/pair_output.py new file mode 100644 index 0000000..24cdea1 --- /dev/null +++ b/pair_output.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +from typing import Any, Callable + +try: + from .prompt_hygiene import sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text +except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`. + from prompt_hygiene import sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text + + +def _labeled_expression_sentence(label: str, expression: Any) -> str: + expression = str(expression or "").strip() + if not expression: + return "" + return f"{label}: {expression}. " + + +def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str: + trigger = trigger.strip() + if not enabled or not trigger: + return prompt + if prompt.lower().startswith(trigger.lower()): + return prompt + return f"{trigger}, {prompt}" + + +def _combined_negative(base: str, extra: str) -> str: + parts = [part.strip() for part in (base, extra) if part and part.strip()] + return ", ".join(parts) + + +def assemble_insta_pair_metadata( + *, + active_trigger: str, + prepend_trigger_to_prompt: bool, + extra_positive: str, + extra_negative: str, + soft_negative_base: str, + hard_negative_base: str, + options: dict[str, Any], + platform_style: str, + soft_descriptor_sentence: str, + soft_level: str, + soft_cast: str, + soft_cast_presence: str, + soft_cast_styling_sentence: str, + soft_row: dict[str, Any], + soft_camera_scene_sentence: str, + soft_camera_sentence: str, + hard_level: str, + hard_cast: str, + cast_descriptor_text: str, + pov_directive: str, + pov_character_labels: list[str], + hard_clothing_sentence: str, + hard_row: dict[str, Any], + hard_scene: str, + hard_camera_scene_sentence: str, + hard_composition: str, + hard_detail_directive: str, + hard_camera_sentence: str, + descriptor: str, + soft_partner_outfit_text: str, + soft_partner_styling: dict[str, Any], + soft_camera_scene_directive: str, + soft_camera_config: dict[str, Any], + soft_camera_directive: str, + hard_camera_scene_directive: str, + hard_camera_config: dict[str, Any], + hard_camera_directive: str, + camera_caption_text: Callable[[dict[str, Any]], str], + cast_descriptors: list[str], + character_hardcore_clothing_entries: list[str], + default_man_hardcore_clothing_entries: list[str], + hard_clothing_state: str, + hard_detail_density: str, + hard_women_count: int, + hard_men_count: int, + character_slots: list[dict[str, Any]], + character_slot_map: dict[str, dict[str, Any]], +) -> dict[str, Any]: + soft_prompt = ( + f"Insta/OF softcore mode: {platform_style}. " + f"{soft_descriptor_sentence}" + f"Softcore setup: {soft_level}. Cast: {soft_cast}. " + f"{soft_cast_presence}" + f"{soft_cast_styling_sentence}" + f"{soft_row['softcore_item_prompt_label']}: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " + f"{soft_camera_scene_sentence}" + 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"{soft_row['positive_suffix']}." + ) + hard_prompt = ( + f"Insta/OF hardcore mode: {platform_style}. " + f"Hardcore setup: {hard_level}. Cast: {hard_cast}. " + f"Cast descriptors: {cast_descriptor_text}. " + f"{pov_directive + ' ' if pov_directive else ''}" + f"{'Keep Woman A visually central from the POV camera. ' if pov_character_labels else 'Keep Woman A visually central. '}" + f"{hard_clothing_sentence}" + f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. " + f"Setting: {hard_scene}. " + f"{hard_camera_scene_sentence}" + f"{_labeled_expression_sentence('Facial expressions', hard_row.get('expression'))}" + f"Composition: {hard_composition}. " + f"{hard_detail_directive}" + f"{hard_camera_sentence}" + f"{hard_row['positive_suffix']}." + ) + if extra_positive.strip(): + soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}" + hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}" + + soft_prompt = _prepend_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt)) + hard_prompt = _prepend_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt)) + soft_prompt = sanitize_prompt_text(soft_prompt, triggers=(active_trigger,)) + hard_prompt = sanitize_prompt_text(hard_prompt, triggers=(active_trigger,)) + soft_negative = sanitize_negative_text(_combined_negative(soft_negative_base, extra_negative)) + hard_negative = sanitize_negative_text(_combined_negative(hard_negative_base, extra_negative)) + + soft_caption_parts = [ + active_trigger, + "Insta/OF softcore mode", + descriptor, + soft_level, + soft_row["item"], + soft_row["pose"], + soft_partner_outfit_text, + soft_partner_styling["pose"], + soft_row["scene_text"], + soft_camera_scene_directive, + soft_row["composition"], + camera_caption_text(soft_camera_config) if soft_camera_directive else "", + ] + soft_caption = sanitize_caption_text( + ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip()), + triggers=(active_trigger,), + ) + hard_caption_parts = [ + active_trigger, + "Insta/OF hardcore mode", + "Woman A", + descriptor, + hard_cast, + hard_row["role_graph"], + hard_row["item"], + hard_scene, + hard_camera_scene_directive, + hard_composition, + camera_caption_text(hard_camera_config) if hard_camera_directive else "", + ] + hard_caption = sanitize_caption_text( + ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip()), + triggers=(active_trigger,), + ) + + return { + "mode": "Insta/OF", + "options": options, + "shared_descriptor": descriptor, + "shared_cast_descriptors": cast_descriptors, + "pov_character_labels": pov_character_labels, + "pov_prompt_directive": pov_directive, + "softcore_partner_styling": soft_partner_styling, + "character_hardcore_clothing": character_hardcore_clothing_entries, + "default_man_hardcore_clothing": default_man_hardcore_clothing_entries, + "hardcore_clothing_state": hard_clothing_state, + "hardcore_detail_density": hard_detail_density, + "hardcore_position_config": hard_row.get("hardcore_position_config", {}), + "softcore_prompt": soft_prompt, + "hardcore_prompt": hard_prompt, + "softcore_negative_prompt": soft_negative, + "hardcore_negative_prompt": hard_negative, + "softcore_caption": soft_caption, + "hardcore_caption": hard_caption, + "softcore_row": soft_row, + "hardcore_row": hard_row, + "hardcore_women_count": hard_women_count, + "hardcore_men_count": hard_men_count, + "character_cast_slots": character_slots, + "character_slot_labels": sorted(character_slot_map), + "softcore_camera_config": soft_camera_config, + "hardcore_camera_config": hard_camera_config, + "softcore_camera_directive": soft_camera_directive, + "hardcore_camera_directive": hard_camera_directive, + "softcore_camera_scene_directive": soft_camera_scene_directive, + "hardcore_camera_scene_directive": hard_camera_scene_directive, + } diff --git a/prompt_builder.py b/prompt_builder.py index 9089a12..2b6eed6 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -27,6 +27,7 @@ try: from . import generate_prompt_batches as g from . import pair_clothing from . import pair_camera + from . import pair_output from . import scene_camera_adapters from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -58,6 +59,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import generate_prompt_batches as g import pair_clothing import pair_camera + import pair_output import scene_camera_adapters from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -2797,13 +2799,6 @@ def _disable_row_expression(row: dict[str, Any], source: str = "disabled") -> di return row -def _labeled_expression_sentence(label: str, expression: Any) -> str: - expression = str(expression or "").strip() - if not expression: - return "" - return f"{label}: {expression}. " - - def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str: trigger = trigger.strip() if not enabled or not trigger: @@ -6959,10 +6954,6 @@ def _insta_of_partner_styling( } -def _insta_of_active_trigger(prompt: str, trigger: str, enabled: bool) -> str: - return _prepend_trigger(prompt, trigger, enabled) - - def build_insta_of_pair( row_number: int, start_index: int, @@ -7256,111 +7247,52 @@ def build_insta_of_pair( else f"Woman A: {descriptor}. " ) - soft_prompt = ( - f"Insta/OF softcore mode: {platform_style}. " - f"{soft_descriptor_sentence}" - f"Softcore setup: {soft_level}. Cast: {soft_cast}. " - f"{soft_cast_presence}" - f"{soft_cast_styling_sentence}" - f"{soft_row['softcore_item_prompt_label']}: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " - f"{soft_camera_scene_sentence}" - 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"{soft_row['positive_suffix']}." + return pair_output.assemble_insta_pair_metadata( + active_trigger=active_trigger, + prepend_trigger_to_prompt=bool(prepend_trigger_to_prompt), + 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, ) - hard_prompt = ( - f"Insta/OF hardcore mode: {platform_style}. " - f"Hardcore setup: {hard_level}. Cast: {hard_cast}. " - f"Cast descriptors: {cast_descriptor_text}. " - f"{pov_directive + ' ' if pov_directive else ''}" - f"{'Keep Woman A visually central from the POV camera. ' if pov_character_labels else 'Keep Woman A visually central. '}" - f"{hard_clothing_sentence}" - f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. " - f"Setting: {hard_scene}. " - f"{hard_camera_scene_sentence}" - f"{_labeled_expression_sentence('Facial expressions', hard_row.get('expression'))}" - f"Composition: {hard_composition}. " - f"{hard_detail_directive}" - f"{hard_camera_sentence}" - f"{hard_row['positive_suffix']}." - ) - if extra_positive.strip(): - soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}" - hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}" - - soft_prompt = _insta_of_active_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt)) - hard_prompt = _insta_of_active_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt)) - soft_prompt = sanitize_prompt_text(soft_prompt, triggers=(active_trigger,)) - hard_prompt = sanitize_prompt_text(hard_prompt, triggers=(active_trigger,)) - soft_negative = sanitize_negative_text(_combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative)) - hard_negative = sanitize_negative_text(_combined_negative(INSTA_OF_NEGATIVE, extra_negative)) - soft_caption_parts = [ - active_trigger, - "Insta/OF softcore mode", - descriptor, - soft_level, - soft_row["item"], - soft_row["pose"], - soft_partner_outfit_text, - soft_partner_styling["pose"], - soft_row["scene_text"], - soft_camera_scene_directive, - soft_row["composition"], - _camera_caption_text(soft_camera_config) if soft_camera_directive else "", - ] - soft_caption = sanitize_caption_text( - ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip()), - triggers=(active_trigger,), - ) - hard_caption_parts = [ - active_trigger, - "Insta/OF hardcore mode", - "Woman A", - descriptor, - hard_cast, - hard_row["role_graph"], - hard_row["item"], - hard_scene, - hard_camera_scene_directive, - hard_composition, - _camera_caption_text(hard_camera_config) if hard_camera_directive else "", - ] - hard_caption = sanitize_caption_text( - ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip()), - triggers=(active_trigger,), - ) - metadata = { - "mode": "Insta/OF", - "options": options, - "shared_descriptor": descriptor, - "shared_cast_descriptors": cast_descriptors, - "pov_character_labels": pov_character_labels, - "pov_prompt_directive": pov_directive, - "softcore_partner_styling": soft_partner_styling, - "character_hardcore_clothing": character_hardcore_clothing_entries, - "default_man_hardcore_clothing": default_man_hardcore_clothing_entries, - "hardcore_clothing_state": hard_clothing_state, - "hardcore_detail_density": hard_detail_density, - "hardcore_position_config": hard_row.get("hardcore_position_config", {}), - "softcore_prompt": soft_prompt, - "hardcore_prompt": hard_prompt, - "softcore_negative_prompt": soft_negative, - "hardcore_negative_prompt": hard_negative, - "softcore_caption": soft_caption, - "hardcore_caption": hard_caption, - "softcore_row": soft_row, - "hardcore_row": hard_row, - "hardcore_women_count": hard_women_count, - "hardcore_men_count": hard_men_count, - "character_cast_slots": character_slots, - "character_slot_labels": sorted(character_slot_map), - "softcore_camera_config": soft_camera_config, - "hardcore_camera_config": hard_camera_config, - "softcore_camera_directive": soft_camera_directive, - "hardcore_camera_directive": hard_camera_directive, - "softcore_camera_scene_directive": soft_camera_scene_directive, - "hardcore_camera_scene_directive": hard_camera_scene_directive, - } - return metadata diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index f0525a0..2642351 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -632,6 +632,10 @@ def _expect_pair(pair: dict[str, Any], name: str) -> None: _expect_custom_row(pair.get("hardcore_row") or {}, f"{name}.hardcore_row") _expect_text(f"{name}.softcore_prompt", pair.get("softcore_prompt"), 20) _expect_text(f"{name}.hardcore_prompt", pair.get("hardcore_prompt"), 20) + _expect_trigger_once(f"{name}.softcore_prompt", pair.get("softcore_prompt"), Trigger) + _expect_trigger_once(f"{name}.hardcore_prompt", pair.get("hardcore_prompt"), Trigger) + _expect_trigger_once(f"{name}.softcore_caption", pair.get("softcore_caption"), Trigger) + _expect_trigger_once(f"{name}.hardcore_caption", pair.get("hardcore_caption"), Trigger) _expect_no_duplicate_comma_items(f"{name}.softcore_negative", pair.get("softcore_negative_prompt")) _expect_no_duplicate_comma_items(f"{name}.hardcore_negative", pair.get("hardcore_negative_prompt")) _expect_formatter_outputs(pair, name, target="softcore")