Support item template route metadata
This commit is contained in:
@@ -954,7 +954,11 @@
|
||||
"expression_pools": ["hardcore_penetration_expressions"],
|
||||
"composition_pools": ["penetration_compositions"],
|
||||
"item_templates": [
|
||||
"{penetration_act} in {position}, with {body_contact}, {intensity}, and {visibility}",
|
||||
{
|
||||
"template": "{penetration_act} in {position}, with {body_contact}, {intensity}, and {visibility}",
|
||||
"action_family": "penetration",
|
||||
"position_family": "penetrative"
|
||||
},
|
||||
"{position} while {penetration_act}, {hand_detail}, {mouth_detail}, and {visibility}",
|
||||
"{penetration_act} from {angle}, with {leg_detail}, {body_contact}, and {intensity}",
|
||||
"hardcore {position} featuring {penetration_act}, {thrust_detail}, {hand_detail}, and {visibility}",
|
||||
@@ -1123,7 +1127,11 @@
|
||||
"expression_pools": ["hardcore_oral_expressions"],
|
||||
"composition_pools": ["oral_compositions"],
|
||||
"item_templates": [
|
||||
"{oral_act} in {position}, with {hand_detail}, {expression_detail}, and {visibility}",
|
||||
{
|
||||
"template": "{oral_act} in {position}, with {hand_detail}, {expression_detail}, and {visibility}",
|
||||
"action_family": "oral",
|
||||
"position_family": "oral"
|
||||
},
|
||||
"{position} featuring {oral_act}, {body_contact}, {saliva_detail}, and {climax_hint}",
|
||||
"{oral_act} from {angle}, with {mouth_detail}, {hand_detail}, and {visibility}",
|
||||
"hardcore oral scene with {oral_act}, {body_contact}, {saliva_detail}, and {expression_detail}",
|
||||
@@ -1271,7 +1279,11 @@
|
||||
{"text": "close candid creator-shot frame centered on non-penetrative genital contact", "min_people": 2, "max_people": 3}
|
||||
],
|
||||
"item_templates": [
|
||||
"{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}",
|
||||
{
|
||||
"template": "{outer_act} in {position}, with {contact_detail}, {hand_detail}, and {visibility}",
|
||||
"action_family": "outercourse",
|
||||
"position_family": "outercourse"
|
||||
},
|
||||
"{position} featuring {outer_act}, {body_contact}, {texture_detail}, seen from a {angle} view",
|
||||
"{angle} view of {outer_act}, with {visibility}, {contact_detail}, and {expression_detail}",
|
||||
"explicit non-penetrative sex pose: {outer_act}, {position}, {contact_detail}, and {visibility}",
|
||||
@@ -1986,7 +1998,11 @@
|
||||
"expression_pools": ["hardcore_climax_expressions"],
|
||||
"composition_pools": ["climax_compositions"],
|
||||
"item_templates": [
|
||||
"{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
|
||||
{
|
||||
"template": "{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
|
||||
"action_family": "climax",
|
||||
"position_family": "climax"
|
||||
},
|
||||
"{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}",
|
||||
"{angle} aftermath view with {body_position}, {body_contact}, and {visibility}",
|
||||
"hardcore post-ejaculation scene with {fluid_location}, {body_position}, {expression_detail}, and {visibility}",
|
||||
|
||||
@@ -332,14 +332,17 @@ Keep here:
|
||||
- named scene/expression/composition pools;
|
||||
- item templates and axes;
|
||||
- direct category-specific wording.
|
||||
- optional object-style item templates with route metadata such as
|
||||
`action_family`, `action_type`, `position_family`, `family`, `position_key`,
|
||||
and `position_keys`; string templates remain valid and fall back to Python
|
||||
inference.
|
||||
|
||||
Improve later:
|
||||
|
||||
- introduce optional `family` and `action_type` fields on item templates so
|
||||
Python filters do less keyword guessing;
|
||||
- add `formatter_hint` fields only where needed, not globally;
|
||||
- keep `tools/prompt_map_audit.py` passing; it now checks referenced
|
||||
expression/composition/scene pools and item-template axes.
|
||||
expression/composition/scene pools and item-template axes for both string and
|
||||
object templates.
|
||||
|
||||
### Node / UI Path
|
||||
|
||||
|
||||
@@ -216,6 +216,9 @@ Important JSON keys:
|
||||
- `subcategories`: selectable subcategories inside a category.
|
||||
- `items`: item/action entries selected by the content or pose axis.
|
||||
- `item_templates`: templates with axis placeholders.
|
||||
- `item_templates` entries may be strings or objects with `template` plus
|
||||
optional route metadata such as `action_family`, `action_type`,
|
||||
`position_family`, `family`, `position_key`, and `position_keys`.
|
||||
- `axes`: values used to fill `item_templates`.
|
||||
- `scene_pool` / `scene_pools` or direct `scenes`: location road.
|
||||
- `expression_pool` / `expression_pools` or direct `expressions`: expression road.
|
||||
@@ -444,9 +447,10 @@ plain prompt text. When debugging, inspect these fields before editing pools.
|
||||
| `content_seed_axis` | `_build_custom_row` | Debug | Shows whether the item/action was driven by `content` or `pose`. Critical for hardcore pose categories. |
|
||||
| `item` | `_compose_item` or Insta override | Krea/SDXL/Naturalizer | Clothing item, category item, or sexual scene/action text. |
|
||||
| `item_axis_values` | `_compose_item` | Krea hardcore rewrite, SDXL tags | Filled template axes such as position/action/detail values. |
|
||||
| `action_family` | `hardcore_action_metadata.source_hardcore_action_family` | Krea hardcore rewrite, SDXL tags, natural captions, debug | Source-aware formatter semantic family such as `foreplay`, `outercourse`, `oral`, `penetration`, `toy_double`, or `climax`. |
|
||||
| `position_family` | `_hardcore_source_position_family` | Debug/filtering | Source/UI hardcore family selected by subcategory, such as `manual`, `interaction`, `oral`, `anal`, or `climax`. |
|
||||
| `position_key`, `position_keys` | `_hardcore_position_keys` | Debug/future filters | Concrete position tokens inferred from axes and role text, such as `kneeling`, `doggy`, `boobjob`, or `open_thighs`. |
|
||||
| `item_template_metadata` | `_compose_item` | Debug, Krea/SDXL/Naturalizer route metadata | Optional metadata from object-style item templates; currently used to prefer explicit action/position families and keys before inference. |
|
||||
| `action_family` | `item_template_metadata` or `hardcore_action_metadata.source_hardcore_action_family` | Krea hardcore rewrite, SDXL tags, natural captions, debug | Source-aware formatter semantic family such as `foreplay`, `outercourse`, `oral`, `penetration`, `toy_double`, or `climax`. |
|
||||
| `position_family` | `item_template_metadata` or `_hardcore_source_position_family` | Debug/filtering | Source/UI hardcore family selected by template metadata or subcategory, such as `manual`, `interaction`, `oral`, `anal`, or `climax`. |
|
||||
| `position_key`, `position_keys` | `item_template_metadata` plus `_hardcore_position_keys` | Debug/future filters | Concrete position tokens from object-template metadata and inferred axes/role text, such as `kneeling`, `doggy`, `boobjob`, or `open_thighs`. |
|
||||
| `custom_item`, `item_label` | Category/pair route | Formatters and debug | Label/name for item route. |
|
||||
| `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. |
|
||||
| `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. |
|
||||
|
||||
+72
-9
@@ -45,7 +45,7 @@ try:
|
||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||
)
|
||||
from .hardcore_action_metadata import source_hardcore_action_family
|
||||
from .hardcore_action_metadata import normalize_hardcore_action_family, source_hardcore_action_family
|
||||
from .hardcore_role_graphs import build_hardcore_role_graph
|
||||
except ImportError: # Allows local smoke tests with `python -c`.
|
||||
from category_library import (
|
||||
@@ -85,7 +85,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||
)
|
||||
from hardcore_action_metadata import source_hardcore_action_family
|
||||
from hardcore_action_metadata import normalize_hardcore_action_family, source_hardcore_action_family
|
||||
from hardcore_role_graphs import build_hardcore_role_graph
|
||||
|
||||
|
||||
@@ -300,6 +300,53 @@ def _item_name(item: Any) -> str:
|
||||
return _item_text(item)
|
||||
|
||||
|
||||
def _template_metadata(item: Any) -> dict[str, Any]:
|
||||
if not isinstance(item, dict):
|
||||
return {}
|
||||
metadata: dict[str, Any] = {}
|
||||
for key in (
|
||||
"action_family",
|
||||
"action_type",
|
||||
"family",
|
||||
"position_family",
|
||||
"position_key",
|
||||
"position_keys",
|
||||
"formatter_hint",
|
||||
):
|
||||
if key in item:
|
||||
metadata[key] = item[key]
|
||||
return metadata
|
||||
|
||||
|
||||
def _template_position_family(metadata: dict[str, Any]) -> str:
|
||||
return _normalize_hardcore_position_family(
|
||||
metadata.get("position_family") or metadata.get("family"),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
def _template_position_keys(metadata: dict[str, Any]) -> list[str]:
|
||||
keys: list[Any] = []
|
||||
if metadata.get("position_keys") is not None:
|
||||
raw_keys = metadata.get("position_keys")
|
||||
keys.extend(raw_keys if isinstance(raw_keys, list) else [raw_keys])
|
||||
if metadata.get("position_key") is not None:
|
||||
keys.append(metadata.get("position_key"))
|
||||
return _normalize_hardcore_position_values(keys)
|
||||
|
||||
|
||||
def _template_action_family(metadata: dict[str, Any]) -> str:
|
||||
return normalize_hardcore_action_family(metadata.get("action_family") or metadata.get("action_type"), "")
|
||||
|
||||
|
||||
def _merge_position_keys(primary: list[str], fallback: list[str]) -> list[str]:
|
||||
merged: list[str] = []
|
||||
for key in [*primary, *fallback]:
|
||||
if key and key not in merged:
|
||||
merged.append(key)
|
||||
return merged
|
||||
|
||||
|
||||
def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
|
||||
position_text = str(position or "").lower()
|
||||
if not position_text:
|
||||
@@ -503,11 +550,12 @@ def _compose_item(
|
||||
item: Any,
|
||||
women_count: int = 1,
|
||||
men_count: int = 1,
|
||||
) -> tuple[str, str, dict[str, str]]:
|
||||
) -> tuple[str, str, dict[str, str], dict[str, Any]]:
|
||||
templates = _template_list(category, subcategory, item, "item_templates")
|
||||
axes = _merged_axes(category, subcategory, item)
|
||||
if templates and axes:
|
||||
template = _entry_text(_weighted_choice(rng, _compatible_entries(templates, women_count, men_count)))
|
||||
template_entry = _weighted_choice(rng, _compatible_entries(templates, women_count, men_count))
|
||||
template = _entry_text(template_entry)
|
||||
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
|
||||
unique_fields = list(dict.fromkeys(fields))
|
||||
axis_values: dict[str, str] = {}
|
||||
@@ -535,8 +583,8 @@ def _compose_item(
|
||||
axis_values[name] = _entry_text(_weighted_choice(rng, values))
|
||||
item_text = _format(template, axis_values).strip()
|
||||
item_name = _item_name(item) or subcategory["name"]
|
||||
return item_text, item_name, axis_values
|
||||
return _item_text(item), _item_name(item), {}
|
||||
return item_text, item_name, axis_values, _template_metadata(template_entry)
|
||||
return _item_text(item), _item_name(item), {}, _template_metadata(item)
|
||||
|
||||
|
||||
def _choose_text(rng: random.Random, items: list[Any]) -> str:
|
||||
@@ -3722,7 +3770,14 @@ def _build_custom_row(
|
||||
content_rng = _axis_rng(seed_config, content_axis, seed, row_number)
|
||||
items = _list_from(subcategory.get("items", [subcategory["name"]]))
|
||||
item = _weighted_choice(content_rng, items)
|
||||
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
|
||||
item_text, item_name, item_axis_values, item_template_metadata = _compose_item(
|
||||
content_rng,
|
||||
category,
|
||||
subcategory,
|
||||
item,
|
||||
women_count,
|
||||
men_count,
|
||||
)
|
||||
is_pose_category = _is_pose_content_category(category, subcategory)
|
||||
if is_pose_category:
|
||||
item_text = _sanitize_hardcore_environment_anchors(item_text)
|
||||
@@ -3868,15 +3923,22 @@ def _build_custom_row(
|
||||
position_key = ""
|
||||
action_family = ""
|
||||
if is_pose_category:
|
||||
position_family = _hardcore_source_position_family(subcategory, parsed_hardcore_position_config)
|
||||
position_keys = _hardcore_position_keys(
|
||||
template_position_family = _template_position_family(item_template_metadata)
|
||||
position_family = template_position_family or _hardcore_source_position_family(
|
||||
subcategory,
|
||||
parsed_hardcore_position_config,
|
||||
)
|
||||
inferred_position_keys = _hardcore_position_keys(
|
||||
item_text,
|
||||
source_role_graph,
|
||||
source_composition,
|
||||
pose,
|
||||
axis_values=item_axis_values,
|
||||
)
|
||||
position_keys = _merge_position_keys(_template_position_keys(item_template_metadata), inferred_position_keys)
|
||||
position_key = position_keys[0] if position_keys else ""
|
||||
action_family = _template_action_family(item_template_metadata)
|
||||
if not action_family:
|
||||
action_family = source_hardcore_action_family(
|
||||
position_family,
|
||||
source_role_graph,
|
||||
@@ -3991,6 +4053,7 @@ def _build_custom_row(
|
||||
"positive_suffix": positive_suffix,
|
||||
"custom_item": item_name,
|
||||
"item_axis_values": item_axis_values,
|
||||
"item_template_metadata": item_template_metadata,
|
||||
"scene_text": scene,
|
||||
"location_config": parsed_location_config if _location_config_active(parsed_location_config) else {},
|
||||
"pose": pose,
|
||||
|
||||
@@ -177,15 +177,29 @@ def _template_axis_errors(path: str, node: dict[str, Any]) -> list[tuple[str, st
|
||||
axis_names = set(axes) if isinstance(axes, dict) else set()
|
||||
errors: list[tuple[str, str]] = []
|
||||
for index, template in enumerate(templates):
|
||||
if not isinstance(template, str):
|
||||
errors.append((f"{path}.item_templates[{index}]", "template is not a string"))
|
||||
template_path = f"{path}.item_templates[{index}]"
|
||||
if isinstance(template, dict):
|
||||
template_text = str(
|
||||
template.get("template")
|
||||
or template.get("prompt")
|
||||
or template.get("text")
|
||||
or template.get("description")
|
||||
or template.get("name")
|
||||
or ""
|
||||
).strip()
|
||||
elif isinstance(template, str):
|
||||
template_text = template
|
||||
else:
|
||||
template_text = ""
|
||||
if not template_text:
|
||||
errors.append((template_path, "template must be a string or object with template/text"))
|
||||
continue
|
||||
tokens = set(TEMPLATE_TOKEN_RE.findall(template))
|
||||
tokens = set(TEMPLATE_TOKEN_RE.findall(template_text))
|
||||
missing = sorted(token for token in tokens if token not in axis_names)
|
||||
if missing:
|
||||
errors.append(
|
||||
(
|
||||
f"{path}.item_templates[{index}]",
|
||||
template_path,
|
||||
"missing item_axes for placeholders: " + ", ".join(missing),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1183,6 +1183,35 @@ def smoke_hardcore_position_config_policy() -> None:
|
||||
_expect(keys == ["doggy"], "Hardcore position key detection changed")
|
||||
source_family = hardcore_position_config.hardcore_source_position_family({"slug": "manual_stimulation"}, filtered)
|
||||
_expect(source_family == "manual", "Hardcore source family lookup changed")
|
||||
item_text, item_name, axis_values, template_metadata = pb._compose_item(
|
||||
random.Random(42),
|
||||
{},
|
||||
{
|
||||
"name": "Template metadata route",
|
||||
"item_templates": [
|
||||
{
|
||||
"template": "{act} in {position}",
|
||||
"action_family": "oral",
|
||||
"position_family": "oral",
|
||||
"position_keys": ["kneeling", "open_thighs"],
|
||||
}
|
||||
],
|
||||
"item_axes": {
|
||||
"act": ["mouth contact"],
|
||||
"position": ["kneeling oral position"],
|
||||
},
|
||||
},
|
||||
"Template metadata route",
|
||||
women_count=1,
|
||||
men_count=1,
|
||||
)
|
||||
_expect(item_text == "mouth contact in kneeling oral position", "Template metadata route changed composed item text")
|
||||
_expect(item_name == "Template metadata route", "Template metadata route changed item name")
|
||||
_expect(axis_values == {"act": "mouth contact", "position": "kneeling oral position"}, "Template metadata route lost axis values")
|
||||
_expect(template_metadata.get("action_family") == "oral", "Template metadata route lost action family")
|
||||
_expect(pb._template_position_family(template_metadata) == "oral", "Template metadata route lost position family")
|
||||
_expect(pb._template_position_keys(template_metadata) == ["kneeling", "open_thighs"], "Template metadata route lost position keys")
|
||||
_expect(pb._template_action_family(template_metadata) == "oral", "Template metadata route lost normalized action family")
|
||||
|
||||
|
||||
def smoke_category_library_route() -> None:
|
||||
@@ -1266,6 +1295,26 @@ def smoke_hardcore_category_routes() -> None:
|
||||
_expect(sdxl_tag in (sdxl.get("sdxl_prompt") or "").lower(), f"{name} SDXL prompt did not include family tag {sdxl_tag!r}")
|
||||
caption, _method = caption_naturalizer.naturalize_caption("", metadata_json=_json(row), trigger=Trigger, include_trigger=True)
|
||||
_expect(caption_label in caption.lower(), f"{name} caption did not include family label {caption_label!r}")
|
||||
annotated_row = None
|
||||
for seed in range(1801, 1841):
|
||||
row = _prompt_row(
|
||||
name="hardcore_annotated_template",
|
||||
category="Hardcore sexual poses",
|
||||
subcategory="Oral sex",
|
||||
seed=seed,
|
||||
character_cast=cast,
|
||||
women_count=1,
|
||||
men_count=1,
|
||||
hardcore_position_config=_action_filter("oral_only"),
|
||||
)
|
||||
if row.get("item_template_metadata"):
|
||||
annotated_row = row
|
||||
break
|
||||
_expect(annotated_row is not None, "No annotated item template reached generated row in deterministic seed window")
|
||||
if annotated_row is not None:
|
||||
_expect(annotated_row.get("action_family") == "oral", "Annotated item template action_family did not reach row")
|
||||
_expect(annotated_row.get("position_family") == "oral", "Annotated item template position_family did not reach row")
|
||||
_expect(annotated_row.get("item_template_metadata", {}).get("action_family") == "oral", "Annotated item metadata missing in row")
|
||||
|
||||
|
||||
def smoke_krea_close_foreplay_route() -> None:
|
||||
|
||||
Reference in New Issue
Block a user