From d9275f5f0c7d72de50ec9e1dbd97873ccff5d2d9 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 08:41:13 +0200 Subject: [PATCH] Extract subject context policy --- docs/prompt-architecture-improvement-plan.md | 3 + docs/prompt-pool-routing-map.md | 7 +- prompt_builder.py | 65 +++--------- subject_context.py | 103 +++++++++++++++++++ tools/prompt_smoke.py | 18 ++++ 5 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 subject_context.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 9391740..0e3aa70 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -134,6 +134,9 @@ Already isolated: couple count normalization live in `cast_context.py`; `prompt_builder.py` keeps delegate wrappers where existing generation paths still call the old helper names. +- row subject-context routing for single, couple, configured-cast, group, and + layout subjects lives in `subject_context.py`; it combines appearance policy, + cast metadata, and generator subject pools behind one row-facing entry point. - ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter parsing, and ethnicity normalization live in `filter_config.py`; character routes and builder filters use `prompt_builder.py` delegate wrappers. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 02c4176..086ffbf 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -79,6 +79,7 @@ Core helper ownership: | `filter_config.py` | Ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter parsing, and ethnicity normalization used by builder and character routes. | | `generation_profile_config.py` | Generation profile presets, profile option overrides, trigger policy, expression/pose/clothing config normalization, and profile config parsing. | | `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. | +| `subject_context.py` | Row subject-context routing for single, couple, configured-cast, group, and layout subjects, combining appearance policy, cast metadata, and generator subject pools. | | `location_config.py` | Location/composition preset schemas, themed location packs, custom location/composition parsing, pool merge behavior, and location/composition config parsing. | | `row_location.py` | Built-in row location/composition config application, deterministic scene/composition choice, source metadata, and legacy prompt/caption rewrites. | | `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. | @@ -391,9 +392,9 @@ Important behavior: Edit targets: -- Character slot JSON/parsing/summary: `character_slot.py`; generation-time - appearance field resolution: `character_appearance.py`; character-slot label - assignment: +- Subject routing: `subject_context.py`; character slot JSON/parsing/summary: + `character_slot.py`; generation-time appearance field resolution: + `character_appearance.py`; character-slot label assignment: `cast_context.character_slot_label_map`; pair cast descriptor entry assembly: `pair_cast.cast_descriptor_entries`. - Profile save/load: `SxCPCharacterProfileSave`, diff --git a/prompt_builder.py b/prompt_builder.py index 11090c2..5f31e3c 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -47,6 +47,7 @@ try: from . import row_camera as row_camera_policy from . import row_location as row_location_policy from . import seed_config as seed_policy + from . import subject_context as subject_context_policy from .hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, @@ -93,6 +94,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import row_camera as row_camera_policy import row_location as row_location_policy import seed_config as seed_policy + import subject_context as subject_context_policy from hardcore_text_cleanup import ( sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values, sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors, @@ -2498,59 +2500,16 @@ def _subject_context( women_count: int = 1, men_count: int = 1, ) -> dict[str, str]: - if subject_type in ("woman", "man", "single_any"): - return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black) - - if subject_type == "configured_cast": - return _configured_cast_context(women_count, men_count) - - if subject_type == "couple": - primary_subject, subject_phrase, pose, effective_women_count, effective_men_count = _couple_type_from_counts( - rng, - women_count, - men_count, - ) - return { - "subject_type": "couple", - "subject": primary_subject, - "subject_phrase": subject_phrase, - "age": g.choose(rng, g.COUPLE_AGES), - "body": g.choose(rng, ["slim and average", "curvy and broad", "stocky and curvy", "average and athletic"]), - "skin": "", - "hair": "", - "eyes": "", - "body_phrase": "", - "fallback_pose": pose, - "women_count": str(effective_women_count), - "men_count": str(effective_men_count), - "person_count": "2", - } - - if subject_type == "group": - eth = "Asian " if ethnicity == "asian" else "" - return { - "subject_type": "group", - "subject": f"mixed {eth}adult group", - "subject_phrase": f"A mixed {eth}adult group of women and men", - "age": g.choose(rng, g.GROUP_AGES), - "body": "diverse", - "skin": "", - "hair": "", - "eyes": "", - "body_phrase": "diverse adult body types", - } - - return { - "subject_type": subject_type, - "subject": "layout scene", - "subject_phrase": "Adult layout scene", - "age": "adult", - "body": "varied", - "skin": "", - "hair": "", - "eyes": "", - "body_phrase": "varied adult figures", - } + return subject_context_policy.subject_context( + rng, + subject_type, + ethnicity, + figure, + no_plus_women, + no_black, + women_count, + men_count, + ) def _scene_pool( diff --git a/subject_context.py b/subject_context.py new file mode 100644 index 0000000..989f2a2 --- /dev/null +++ b/subject_context.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import random +from typing import Any + +try: + from . import cast_context as cast_context_policy + from . import character_appearance as character_appearance_policy + from . import generate_prompt_batches as g +except ImportError: # Allows local smoke tests with top-level imports. + import cast_context as cast_context_policy + import character_appearance as character_appearance_policy + import generate_prompt_batches as g + + +def couple_context( + rng: random.Random, + women_count: int, + men_count: int, +) -> dict[str, str]: + primary_subject, subject_phrase, pose, effective_women_count, effective_men_count = cast_context_policy.couple_type_from_counts( + rng, + women_count, + men_count, + choose=g.choose, + couple_types=g.COUPLE_TYPES, + ) + return { + "subject_type": "couple", + "subject": primary_subject, + "subject_phrase": subject_phrase, + "age": g.choose(rng, g.COUPLE_AGES), + "body": g.choose(rng, ["slim and average", "curvy and broad", "stocky and curvy", "average and athletic"]), + "skin": "", + "hair": "", + "eyes": "", + "body_phrase": "", + "fallback_pose": pose, + "women_count": str(effective_women_count), + "men_count": str(effective_men_count), + "person_count": "2", + } + + +def group_context(rng: random.Random, ethnicity: str) -> dict[str, str]: + eth = "Asian " if ethnicity == "asian" else "" + return { + "subject_type": "group", + "subject": f"mixed {eth}adult group", + "subject_phrase": f"A mixed {eth}adult group of women and men", + "age": g.choose(rng, g.GROUP_AGES), + "body": "diverse", + "skin": "", + "hair": "", + "eyes": "", + "body_phrase": "diverse adult body types", + } + + +def layout_context(subject_type: str) -> dict[str, str]: + return { + "subject_type": subject_type, + "subject": "layout scene", + "subject_phrase": "Adult layout scene", + "age": "adult", + "body": "varied", + "skin": "", + "hair": "", + "eyes": "", + "body_phrase": "varied adult figures", + } + + +def subject_context( + rng: random.Random, + subject_type: str, + ethnicity: str, + figure: str, + no_plus_women: bool, + no_black: bool, + women_count: int = 1, + men_count: int = 1, +) -> dict[str, str]: + if subject_type in ("woman", "man", "single_any"): + return character_appearance_policy.appearance_for_subject( + rng, + subject_type, + ethnicity, + figure, + no_plus_women, + no_black, + ) + + if subject_type == "configured_cast": + return cast_context_policy.configured_cast_context(women_count, men_count) + + if subject_type == "couple": + return couple_context(rng, women_count, men_count) + + if subject_type == "group": + return group_context(rng, ethnicity) + + return layout_context(subject_type) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index e41f2a7..e9526cb 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -57,6 +57,7 @@ import sdxl_formatter # noqa: E402 import sdxl_presets # noqa: E402 import seed_config # noqa: E402 import krea_pov # noqa: E402 +import subject_context # noqa: E402 Trigger = "sxcppnl7" @@ -684,6 +685,23 @@ def smoke_category_cast_config_policy() -> None: == ("two women", "two women", "close affectionate couple pose", 2, 0), "Couple type count override for two women changed", ) + _expect( + pb._subject_context(random.Random(5), "couple", "any", "curvy", False, False, 1, 1) + == subject_context.subject_context(random.Random(5), "couple", "any", "curvy", False, False, 1, 1), + "Prompt builder subject context should delegate to subject_context", + ) + _expect( + subject_context.subject_context(random.Random(1), "configured_cast", "any", "curvy", False, False, 2, 1).get("cast_summary") + == "2 women, 1 man, 3 total adults", + "Configured cast subject context changed", + ) + group = subject_context.subject_context(random.Random(2), "group", "asian", "curvy", False, False) + _expect(group.get("subject") == "mixed Asian adult group", "Group subject ethnicity wording changed") + _expect( + subject_context.subject_context(random.Random(3), "layout", "any", "curvy", False, False).get("subject") + == "layout scene", + "Layout subject context fallback changed", + ) label_slots = [ {"subject_type": "woman", "label": "auto_chain", "name": "older auto"}, {"subject_type": "woman", "label": "auto_chain", "name": "newer auto"},