diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 1aca917..e150345 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -147,7 +147,6 @@ Keep here: - soft/hard row creation; - continuity policy; - softcore cast policy; -- pair-level camera routing; - pair metadata shape. Improve later: @@ -155,8 +154,15 @@ 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`, `resolve_pair_camera`, - `resolve_pair_clothing`, `assemble_pair_metadata`. + `build_soft_row`, `build_hard_row`, `resolve_pair_clothing`, + `assemble_pair_metadata`. + +Already isolated: + +- 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. ### Krea2 Formatter Path diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index fbe29ce..c7dd49d 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_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. | | `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. | diff --git a/pair_camera.py b/pair_camera.py new file mode 100644 index 0000000..bfdbef5 --- /dev/null +++ b/pair_camera.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from typing import Any, Callable + + +CameraConfigWithMode = Callable[[str | dict[str, Any] | None, str], dict[str, Any]] +CameraDirective = Callable[[str | dict[str, Any] | None], tuple[str, dict[str, Any]]] +ApplyComposition = Callable[[dict[str, Any], str], dict[str, Any]] +CompositionPrompt = Callable[[Any, Any, str], str] +CameraSceneDirective = Callable[ + [Any, Any, str | dict[str, Any] | None, list[str] | None, str], + tuple[str, dict[str, Any]], +] + + +def camera_config_with_detail( + camera_config: dict[str, Any], + camera_detail: str, + camera_detail_choices: list[str] | tuple[str, ...], +) -> dict[str, Any]: + if camera_detail in camera_detail_choices: + camera_config["camera_detail"] = camera_detail + return camera_config + + +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]: + 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 + if hard_camera_mode == "same_as_softcore": + hard_camera_mode = str(options["softcore_camera_mode"]) + hard_camera_source = soft_camera_source + + soft_camera_config_dict = camera_config_with_mode(soft_camera_source, str(options["softcore_camera_mode"])) + hard_camera_config_dict = camera_config_with_mode(hard_camera_source, hard_camera_mode) + soft_camera_config_dict = camera_config_with_detail( + soft_camera_config_dict, + str(options["camera_detail"]), + camera_detail_choices, + ) + hard_camera_config_dict = camera_config_with_detail( + hard_camera_config_dict, + str(options["camera_detail"]), + camera_detail_choices, + ) + soft_camera_directive, soft_camera_config_dict = camera_directive(soft_camera_config_dict) + hard_camera_directive, hard_camera_config_dict = camera_directive(hard_camera_config_dict) + + soft_subject_kind = "woman" if options["softcore_cast"] == "solo" else "subjects" + hard_subject_kind = "couple" if hard_women_count + hard_men_count == 2 else "subjects" + soft_row = apply_contextual_composition(soft_row, soft_subject_kind) + hard_row = apply_contextual_composition(hard_row, hard_subject_kind) + + hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"] + if hard_scene != hard_row.get("scene_text"): + hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "") + hard_row["scene_text"] = hard_scene + + hard_composition = contextual_composition_prompt(hard_scene, hard_row["composition"], hard_subject_kind) + if hard_composition != hard_row["composition"]: + hard_row["source_composition"] = hard_row.get("source_composition") or hard_row["composition"] + hard_row["composition"] = hard_composition + hard_row["composition_prompt"] = composition_prompt(hard_composition) + + soft_pov_camera_labels = pov_character_labels if options["softcore_cast"] == "same_as_hardcore" else [] + soft_camera_scene_directive, soft_camera_config_dict = camera_scene_directive_for_context( + soft_row.get("scene_text"), + soft_row.get("composition"), + soft_camera_config_dict, + soft_pov_camera_labels, + soft_subject_kind, + ) + hard_camera_scene_directive, hard_camera_config_dict = camera_scene_directive_for_context( + hard_scene, + hard_composition, + hard_camera_config_dict, + pov_character_labels, + hard_subject_kind, + ) + + if soft_pov_camera_labels: + soft_camera_directive = "" + if pov_character_labels: + hard_camera_directive = "" + + soft_row["camera_config"] = soft_camera_config_dict + soft_row["camera_directive"] = soft_camera_directive + soft_row["camera_scene_directive"] = soft_camera_scene_directive + hard_row["camera_config"] = hard_camera_config_dict + 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 "", + } diff --git a/prompt_builder.py b/prompt_builder.py index 14df261..251da4d 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -25,6 +25,7 @@ try: template_list as _template_list, ) from . import generate_prompt_batches as g + from . import pair_camera from . import scene_camera_adapters from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -54,6 +55,7 @@ except ImportError: # Allows local smoke tests with `python -c`. template_list as _template_list, ) import generate_prompt_batches as g + import pair_camera import scene_camera_adapters from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, @@ -6804,12 +6806,6 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s return parsed -def _insta_camera_config_with_detail(camera_config: dict[str, Any], camera_detail: str) -> dict[str, Any]: - if camera_detail in CAMERA_DETAIL_CHOICES: - camera_config["camera_detail"] = camera_detail - return camera_config - - def _insta_of_hardcore_counts(options: dict[str, Any]) -> tuple[int, int]: policy = str(options.get("hardcore_cast", "use_counts")) if policy == "couple": @@ -7448,64 +7444,38 @@ def build_insta_of_pair( platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]] soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]] hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]] - hard_camera_mode = options["hardcore_camera_mode"] - soft_camera_source = softcore_camera_config or camera_config - hard_camera_source = hardcore_camera_config or camera_config - if hard_camera_mode == "same_as_softcore": - hard_camera_mode = options["softcore_camera_mode"] - hard_camera_source = soft_camera_source - soft_camera_config = _camera_config_with_mode(soft_camera_source, options["softcore_camera_mode"]) - hard_camera_config = _camera_config_with_mode(hard_camera_source, hard_camera_mode) - soft_camera_config = _insta_camera_config_with_detail(soft_camera_config, options["camera_detail"]) - hard_camera_config = _insta_camera_config_with_detail(hard_camera_config, options["camera_detail"]) - soft_camera_directive, soft_camera_config = _camera_directive(soft_camera_config) - hard_camera_directive, hard_camera_config = _camera_directive(hard_camera_config) - soft_subject_kind = "woman" if options["softcore_cast"] == "solo" else "subjects" - hard_subject_kind = "couple" if hard_women_count + hard_men_count == 2 else "subjects" - soft_row = _apply_coworking_composition(soft_row, soft_subject_kind) - hard_row = _apply_coworking_composition(hard_row, hard_subject_kind) - hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"] - if hard_scene != hard_row.get("scene_text"): - hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "") - hard_row["scene_text"] = hard_scene - hard_composition = _coworking_composition_prompt(hard_scene, hard_row["composition"], hard_subject_kind) - if hard_composition != hard_row["composition"]: - hard_row["source_composition"] = hard_row.get("source_composition") or hard_row["composition"] - hard_row["composition"] = hard_composition - hard_row["composition_prompt"] = _composition_prompt(hard_composition) - soft_pov_camera_labels = ( - pov_character_labels - if options["softcore_cast"] == "same_as_hardcore" - else [] + camera_route = pair_camera.resolve_insta_pair_camera( + 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_coworking_composition, + contextual_composition_prompt=_coworking_composition_prompt, + composition_prompt=_composition_prompt, + camera_scene_directive_for_context=_camera_scene_directive_for_context, ) - soft_camera_scene_directive, soft_camera_config = _camera_scene_directive_for_context( - soft_row.get("scene_text"), - soft_row.get("composition"), - soft_camera_config, - soft_pov_camera_labels, - soft_subject_kind, - ) - hard_camera_scene_directive, hard_camera_config = _camera_scene_directive_for_context( - hard_scene, - hard_composition, - hard_camera_config, - pov_character_labels, - hard_subject_kind, - ) - if soft_pov_camera_labels: - soft_camera_directive = "" - if pov_character_labels: - hard_camera_directive = "" - soft_row["camera_config"] = soft_camera_config - soft_row["camera_directive"] = soft_camera_directive - soft_row["camera_scene_directive"] = soft_camera_scene_directive - hard_row["camera_config"] = hard_camera_config - hard_row["camera_directive"] = hard_camera_directive - hard_row["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 "" + 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 = ( "solo creator setup with Woman A alone" if options["softcore_cast"] == "solo" diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 00f2bbd..aefec4d 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -755,6 +755,14 @@ def smoke_insta_pair_camera_split() -> None: _expect("elevated shot" in hard_scene, "hard camera scene missed hard elevation") _expect("front-right quarter view" in str(pair.get("softcore_camera_directive")), "soft pair camera directive was not preserved") _expect("back-right quarter view" in str(pair.get("hardcore_camera_directive")), "hard pair camera directive was not preserved") + soft_row = pair.get("softcore_row") or {} + hard_row = pair.get("hardcore_row") or {} + _expect(pair.get("softcore_camera_config") == soft_row.get("camera_config"), "soft pair camera config drifted from soft row") + _expect(pair.get("hardcore_camera_config") == hard_row.get("camera_config"), "hard pair camera config drifted from hard row") + _expect(pair.get("softcore_camera_directive") == soft_row.get("camera_directive"), "soft pair camera directive drifted from soft row") + _expect(pair.get("hardcore_camera_directive") == hard_row.get("camera_directive"), "hard pair camera directive drifted from hard row") + _expect(pair.get("softcore_camera_scene_directive") == soft_row.get("camera_scene_directive"), "soft pair camera scene drifted from soft row") + _expect(pair.get("hardcore_camera_scene_directive") == hard_row.get("camera_scene_directive"), "hard pair camera scene drifted from hard row") krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="auto") _expect("front-right quarter view" in (krea.get("krea_softcore_prompt") or ""), "Krea soft pair lost soft camera geometry") _expect("back-right quarter view" in (krea.get("krea_hardcore_prompt") or ""), "Krea hard pair lost hard camera geometry")