Extract cast context policy
This commit is contained in:
+100
@@ -0,0 +1,100 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
|
||||||
|
Choose = Callable[[Any, list[tuple[str, str, str]]], tuple[str, str, str]]
|
||||||
|
|
||||||
|
|
||||||
|
def count_phrase(count: int, singular: str, plural: str) -> str:
|
||||||
|
words = {
|
||||||
|
0: "no",
|
||||||
|
1: "one",
|
||||||
|
2: "two",
|
||||||
|
3: "three",
|
||||||
|
4: "four",
|
||||||
|
5: "five",
|
||||||
|
6: "six",
|
||||||
|
7: "seven",
|
||||||
|
8: "eight",
|
||||||
|
9: "nine",
|
||||||
|
10: "ten",
|
||||||
|
11: "eleven",
|
||||||
|
12: "twelve",
|
||||||
|
}
|
||||||
|
label = singular if count == 1 else plural
|
||||||
|
return f"{words.get(count, str(count))} {label}"
|
||||||
|
|
||||||
|
|
||||||
|
def cast_summary_phrase(women_count: int, men_count: int) -> str:
|
||||||
|
women_count = max(0, int(women_count))
|
||||||
|
men_count = max(0, int(men_count))
|
||||||
|
if women_count + men_count == 0:
|
||||||
|
women_count = 1
|
||||||
|
person_count = women_count + men_count
|
||||||
|
women_label = "woman" if women_count == 1 else "women"
|
||||||
|
men_label = "man" if men_count == 1 else "men"
|
||||||
|
return f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
|
||||||
|
|
||||||
|
|
||||||
|
def configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
|
||||||
|
women_count = max(0, int(women_count))
|
||||||
|
men_count = max(0, int(men_count))
|
||||||
|
if women_count + men_count == 0:
|
||||||
|
women_count = 1
|
||||||
|
parts = []
|
||||||
|
if women_count:
|
||||||
|
parts.append(count_phrase(women_count, "adult woman", "adult women"))
|
||||||
|
if men_count:
|
||||||
|
parts.append(count_phrase(men_count, "adult man", "adult men"))
|
||||||
|
subject_phrase = parts[0] if len(parts) == 1 else f"{parts[0]} and {parts[1]}"
|
||||||
|
person_count = women_count + men_count
|
||||||
|
if person_count == 1:
|
||||||
|
scene_kind = "solo adult sexual pose"
|
||||||
|
elif person_count == 2:
|
||||||
|
scene_kind = "adult couple sex scene"
|
||||||
|
elif person_count == 3:
|
||||||
|
scene_kind = "adult threesome sex scene"
|
||||||
|
else:
|
||||||
|
scene_kind = "adult group sex scene"
|
||||||
|
return {
|
||||||
|
"subject_type": "configured_cast",
|
||||||
|
"subject": f"{women_count}w_{men_count}m_sex_scene",
|
||||||
|
"subject_phrase": subject_phrase,
|
||||||
|
"age": "21+ adults",
|
||||||
|
"body": "varied",
|
||||||
|
"skin": "",
|
||||||
|
"hair": "",
|
||||||
|
"eyes": "",
|
||||||
|
"body_phrase": "varied adult bodies",
|
||||||
|
"women_count": str(women_count),
|
||||||
|
"men_count": str(men_count),
|
||||||
|
"person_count": str(person_count),
|
||||||
|
"cast_summary": cast_summary_phrase(women_count, men_count),
|
||||||
|
"scene_kind": scene_kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def couple_type_from_counts(
|
||||||
|
rng: Any,
|
||||||
|
women_count: int,
|
||||||
|
men_count: int,
|
||||||
|
*,
|
||||||
|
choose: Choose,
|
||||||
|
couple_types: list[tuple[str, str, str]],
|
||||||
|
) -> tuple[str, str, str, int, int]:
|
||||||
|
women_count = max(0, int(women_count))
|
||||||
|
men_count = max(0, int(men_count))
|
||||||
|
if women_count >= 2 and men_count == 0:
|
||||||
|
return "two women", "two women", "close affectionate couple pose", 2, 0
|
||||||
|
if men_count >= 2 and women_count == 0:
|
||||||
|
return "two men", "two men", "relaxed romantic couple pose", 0, 2
|
||||||
|
if women_count >= 1 and men_count >= 1:
|
||||||
|
return "woman and man", "a woman and a man", "playful date-night pose", 1, 1
|
||||||
|
|
||||||
|
primary_subject, subject_phrase, pose = choose(rng, couple_types)
|
||||||
|
if primary_subject == "two women":
|
||||||
|
return primary_subject, subject_phrase, pose, 2, 0
|
||||||
|
if primary_subject == "two men":
|
||||||
|
return primary_subject, subject_phrase, pose, 0, 2
|
||||||
|
return primary_subject, subject_phrase, pose, 1, 1
|
||||||
@@ -129,6 +129,10 @@ Already isolated:
|
|||||||
- category/cast route preset schemas, config JSON builders, choice lists, and
|
- category/cast route preset schemas, config JSON builders, choice lists, and
|
||||||
parsers live in `category_cast_config.py`; `prompt_builder.py` keeps public
|
parsers live in `category_cast_config.py`; `prompt_builder.py` keeps public
|
||||||
delegate wrappers for existing nodes and tests.
|
delegate wrappers for existing nodes and tests.
|
||||||
|
- generation-time cast count phrases, configured-cast context metadata,
|
||||||
|
scene-kind labels, cast-summary wording, and couple count normalization live
|
||||||
|
in `cast_context.py`; `prompt_builder.py` keeps delegate wrappers where
|
||||||
|
existing generation paths still call the old helper names.
|
||||||
- ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter
|
- ethnicity/filter choices, advanced filter JSON, ethnicity-list JSON, filter
|
||||||
parsing, and ethnicity normalization live in `filter_config.py`; character
|
parsing, and ethnicity normalization live in `filter_config.py`; character
|
||||||
routes and builder filters use `prompt_builder.py` delegate wrappers.
|
routes and builder filters use `prompt_builder.py` delegate wrappers.
|
||||||
|
|||||||
@@ -70,6 +70,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. |
|
| `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. |
|
||||||
| `category_template_metadata.py` | Object-style item-template metadata extraction, action/position family normalization, position-key normalization, key merging, and audit validation errors. |
|
| `category_template_metadata.py` | Object-style item-template metadata extraction, action/position family normalization, position-key normalization, key merging, and audit validation errors. |
|
||||||
| `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. |
|
| `category_cast_config.py` | Category preset and cast preset schemas, category/cast config JSON builders, choice lists, and config parsers used by route nodes. |
|
||||||
|
| `cast_context.py` | Generation-time cast count phrases, configured-cast context metadata, cast-summary wording, scene-kind labels, and couple count normalization. |
|
||||||
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
|
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
|
||||||
| `character_config.py` | Character choice lists, descriptor detail/presence/slot-seed normalization, characteristic-list JSON builders/parsers, eye labels, hair config builders/parsers, and hair phrase helpers. |
|
| `character_config.py` | Character choice lists, descriptor detail/presence/slot-seed normalization, characteristic-list JSON builders/parsers, eye labels, hair config builders/parsers, and hair phrase helpers. |
|
||||||
| `character_profile.py` | Character manual-detail config, profile name/path policy, profile JSON normalization, descriptor assembly, save/load/rename/delete operations, fallback profile loading, and context override application. |
|
| `character_profile.py` | Character manual-detail config, profile name/path policy, profile JSON normalization, descriptor assembly, save/load/rename/delete operations, fallback profile loading, and context override application. |
|
||||||
|
|||||||
+3
-8
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import cast_context as cast_context_policy
|
||||||
from . import pair_clothing
|
from . import pair_clothing
|
||||||
from . import pair_options
|
from . import pair_options
|
||||||
except ImportError: # Allows local smoke tests with top-level imports.
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import cast_context as cast_context_policy
|
||||||
import pair_clothing
|
import pair_clothing
|
||||||
import pair_options
|
import pair_options
|
||||||
|
|
||||||
@@ -18,14 +20,7 @@ SlotSoftcoreOutfit = Callable[[dict[str, Any] | None, Any], str]
|
|||||||
|
|
||||||
|
|
||||||
def cast_summary_phrase(women_count: int, men_count: int) -> str:
|
def cast_summary_phrase(women_count: int, men_count: int) -> str:
|
||||||
women_count = max(0, int(women_count))
|
return cast_context_policy.cast_summary_phrase(women_count, men_count)
|
||||||
men_count = max(0, int(men_count))
|
|
||||||
if women_count + men_count == 0:
|
|
||||||
women_count = 1
|
|
||||||
person_count = women_count + men_count
|
|
||||||
women_label = "woman" if women_count == 1 else "women"
|
|
||||||
men_label = "man" if men_count == 1 else "men"
|
|
||||||
return f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
|
|
||||||
|
|
||||||
|
|
||||||
def softcore_partner_styling(
|
def softcore_partner_styling(
|
||||||
|
|||||||
+11
-73
@@ -24,6 +24,7 @@ try:
|
|||||||
template_list as _template_list,
|
template_list as _template_list,
|
||||||
)
|
)
|
||||||
from . import camera_config as camera_policy
|
from . import camera_config as camera_policy
|
||||||
|
from . import cast_context as cast_context_policy
|
||||||
from . import category_template_metadata as item_template_policy
|
from . import category_template_metadata as item_template_policy
|
||||||
from . import character_config as character_policy
|
from . import character_config as character_policy
|
||||||
from . import character_profile as character_profile_policy
|
from . import character_profile as character_profile_policy
|
||||||
@@ -67,6 +68,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
template_list as _template_list,
|
template_list as _template_list,
|
||||||
)
|
)
|
||||||
import camera_config as camera_policy
|
import camera_config as camera_policy
|
||||||
|
import cast_context as cast_context_policy
|
||||||
import category_template_metadata as item_template_policy
|
import category_template_metadata as item_template_policy
|
||||||
import character_config as character_policy
|
import character_config as character_policy
|
||||||
import character_profile as character_profile_policy
|
import character_profile as character_profile_policy
|
||||||
@@ -2831,67 +2833,11 @@ def _appearance_for_subject(
|
|||||||
|
|
||||||
|
|
||||||
def _count_phrase(count: int, singular: str, plural: str) -> str:
|
def _count_phrase(count: int, singular: str, plural: str) -> str:
|
||||||
words = {
|
return cast_context_policy.count_phrase(count, singular, plural)
|
||||||
0: "no",
|
|
||||||
1: "one",
|
|
||||||
2: "two",
|
|
||||||
3: "three",
|
|
||||||
4: "four",
|
|
||||||
5: "five",
|
|
||||||
6: "six",
|
|
||||||
7: "seven",
|
|
||||||
8: "eight",
|
|
||||||
9: "nine",
|
|
||||||
10: "ten",
|
|
||||||
11: "eleven",
|
|
||||||
12: "twelve",
|
|
||||||
}
|
|
||||||
label = singular if count == 1 else plural
|
|
||||||
return f"{words.get(count, str(count))} {label}"
|
|
||||||
|
|
||||||
|
|
||||||
def _configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
|
def _configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
|
||||||
women_count = max(0, int(women_count))
|
return cast_context_policy.configured_cast_context(women_count, men_count)
|
||||||
men_count = max(0, int(men_count))
|
|
||||||
if women_count + men_count == 0:
|
|
||||||
women_count = 1
|
|
||||||
parts = []
|
|
||||||
if women_count:
|
|
||||||
parts.append(_count_phrase(women_count, "adult woman", "adult women"))
|
|
||||||
if men_count:
|
|
||||||
parts.append(_count_phrase(men_count, "adult man", "adult men"))
|
|
||||||
if len(parts) == 1:
|
|
||||||
subject_phrase = parts[0]
|
|
||||||
else:
|
|
||||||
subject_phrase = f"{parts[0]} and {parts[1]}"
|
|
||||||
person_count = women_count + men_count
|
|
||||||
if person_count == 1:
|
|
||||||
scene_kind = "solo adult sexual pose"
|
|
||||||
elif person_count == 2:
|
|
||||||
scene_kind = "adult couple sex scene"
|
|
||||||
elif person_count == 3:
|
|
||||||
scene_kind = "adult threesome sex scene"
|
|
||||||
else:
|
|
||||||
scene_kind = "adult group sex scene"
|
|
||||||
women_label = "woman" if women_count == 1 else "women"
|
|
||||||
men_label = "man" if men_count == 1 else "men"
|
|
||||||
cast_summary = f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
|
|
||||||
return {
|
|
||||||
"subject_type": "configured_cast",
|
|
||||||
"subject": f"{women_count}w_{men_count}m_sex_scene",
|
|
||||||
"subject_phrase": subject_phrase,
|
|
||||||
"age": "21+ adults",
|
|
||||||
"body": "varied",
|
|
||||||
"skin": "",
|
|
||||||
"hair": "",
|
|
||||||
"eyes": "",
|
|
||||||
"body_phrase": "varied adult bodies",
|
|
||||||
"women_count": str(women_count),
|
|
||||||
"men_count": str(men_count),
|
|
||||||
"person_count": str(person_count),
|
|
||||||
"cast_summary": cast_summary,
|
|
||||||
"scene_kind": scene_kind,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _couple_type_from_counts(
|
def _couple_type_from_counts(
|
||||||
@@ -2899,21 +2845,13 @@ def _couple_type_from_counts(
|
|||||||
women_count: int,
|
women_count: int,
|
||||||
men_count: int,
|
men_count: int,
|
||||||
) -> tuple[str, str, str, int, int]:
|
) -> tuple[str, str, str, int, int]:
|
||||||
women_count = max(0, int(women_count))
|
return cast_context_policy.couple_type_from_counts(
|
||||||
men_count = max(0, int(men_count))
|
rng,
|
||||||
if women_count >= 2 and men_count == 0:
|
women_count,
|
||||||
return "two women", "two women", "close affectionate couple pose", 2, 0
|
men_count,
|
||||||
if men_count >= 2 and women_count == 0:
|
choose=g.choose,
|
||||||
return "two men", "two men", "relaxed romantic couple pose", 0, 2
|
couple_types=g.COUPLE_TYPES,
|
||||||
if women_count >= 1 and men_count >= 1:
|
)
|
||||||
return "woman and man", "a woman and a man", "playful date-night pose", 1, 1
|
|
||||||
|
|
||||||
primary_subject, subject_phrase, pose = g.choose(rng, g.COUPLE_TYPES)
|
|
||||||
if primary_subject == "two women":
|
|
||||||
return primary_subject, subject_phrase, pose, 2, 0
|
|
||||||
if primary_subject == "two men":
|
|
||||||
return primary_subject, subject_phrase, pose, 0, 2
|
|
||||||
return primary_subject, subject_phrase, pose, 1, 1
|
|
||||||
|
|
||||||
|
|
||||||
def _subject_context(
|
def _subject_context(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ if str(ROOT) not in sys.path:
|
|||||||
|
|
||||||
import caption_naturalizer # noqa: E402
|
import caption_naturalizer # noqa: E402
|
||||||
import caption_policy # noqa: E402
|
import caption_policy # noqa: E402
|
||||||
|
import cast_context # noqa: E402
|
||||||
import category_template_metadata # noqa: E402
|
import category_template_metadata # noqa: E402
|
||||||
import character_config # noqa: E402
|
import character_config # noqa: E402
|
||||||
import character_profile # noqa: E402
|
import character_profile # noqa: E402
|
||||||
@@ -658,6 +659,29 @@ def smoke_category_cast_config_policy() -> None:
|
|||||||
_expect(pb.CAST_PRESETS is category_cast_config.CAST_PRESETS, "Prompt builder cast presets are not delegated")
|
_expect(pb.CAST_PRESETS is category_cast_config.CAST_PRESETS, "Prompt builder cast presets are not delegated")
|
||||||
_expect("hardcore_pose" in category_cast_config.category_preset_choices(), "Category preset choices lost hardcore_pose")
|
_expect("hardcore_pose" in category_cast_config.category_preset_choices(), "Category preset choices lost hardcore_pose")
|
||||||
_expect("custom_counts" in category_cast_config.cast_preset_choices(), "Cast preset choices lost custom_counts")
|
_expect("custom_counts" in category_cast_config.cast_preset_choices(), "Cast preset choices lost custom_counts")
|
||||||
|
_expect(
|
||||||
|
pb._count_phrase(2, "adult woman", "adult women") == cast_context.count_phrase(2, "adult woman", "adult women"),
|
||||||
|
"Prompt builder count phrase should delegate to cast_context",
|
||||||
|
)
|
||||||
|
configured = cast_context.configured_cast_context(1, 2)
|
||||||
|
_expect(configured.get("subject_phrase") == "one adult woman and two adult men", "Configured cast subject phrase changed")
|
||||||
|
_expect(configured.get("cast_summary") == "1 woman, 2 men, 3 total adults", "Configured cast summary changed")
|
||||||
|
_expect(configured.get("scene_kind") == "adult threesome sex scene", "Configured cast scene kind changed")
|
||||||
|
_expect(
|
||||||
|
pb._configured_cast_context(1, 2) == configured,
|
||||||
|
"Prompt builder configured cast context should delegate to cast_context",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
cast_context.couple_type_from_counts(
|
||||||
|
random.Random(1),
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
choose=lambda _rng, pool: pool[0],
|
||||||
|
couple_types=[("woman and man", "a woman and a man", "fallback pose")],
|
||||||
|
)
|
||||||
|
== ("two women", "two women", "close affectionate couple pose", 2, 0),
|
||||||
|
"Couple type count override for two women changed",
|
||||||
|
)
|
||||||
|
|
||||||
category_config = json.loads(pb.build_category_config_json("hardcore_pose", "Foreplay and teasing"))
|
category_config = json.loads(pb.build_category_config_json("hardcore_pose", "Foreplay and teasing"))
|
||||||
_expect(category_config.get("category") == "Hardcore sexual poses", "Category config lost hardcore category mapping")
|
_expect(category_config.get("category") == "Hardcore sexual poses", "Category config lost hardcore category mapping")
|
||||||
|
|||||||
Reference in New Issue
Block a user