Move action expression sanitizer

This commit is contained in:
2026-06-27 09:20:59 +02:00
parent 3c1f6784c1
commit b46b709e8a
5 changed files with 123 additions and 90 deletions
+2 -2
View File
@@ -179,8 +179,8 @@ Already isolated:
wrappers. wrappers.
- row expression text cleanup, expression intensity weighting, - row expression text cleanup, expression intensity weighting,
character-slot/cast expression override resolution, and per-character character-slot/cast expression override resolution, and per-character
expression picking live in `row_expression.py`; `prompt_builder.py` keeps expression picking plus action-aware character-expression sanitizing live in
public delegate wrappers. `row_expression.py`; `prompt_builder.py` keeps public delegate wrappers.
- hardcore position/action-filter choices, selected-position normalization, - hardcore position/action-filter choices, selected-position normalization,
config JSON builders/parsers, focus-policy toggles, subcategory allow-list config JSON builders/parsers, focus-policy toggles, subcategory allow-list
policy, position-key detection, category filtering, and item-template/axis policy, position-key detection, category filtering, and item-template/axis
+2 -2
View File
@@ -85,7 +85,7 @@ Core helper ownership:
| `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. | | `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. | | `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. | | `row_location.py` | Built-in row location/composition config application, deterministic scene/composition choice, source metadata, and legacy prompt/caption rewrites. |
| `row_expression.py` | Row expression cleanup, expression intensity weighting, character-slot/cast expression override resolution, and per-character expression selection. | | `row_expression.py` | Row expression cleanup, expression intensity weighting, character-slot/cast expression override resolution, per-character expression selection, and action-aware character-expression sanitizing. |
| `row_pools.py` | Row scene/expression/pose/composition pool routing, category inheritance handling, runtime location/composition pool overrides, and generator fallback pools. | | `row_pools.py` | Row scene/expression/pose/composition pool routing, category inheritance handling, runtime location/composition pool overrides, and generator fallback pools. |
| `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. | | `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. |
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, hardcore cast count policy, and hardcore detail-density directives. | | `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, hardcore cast count policy, and hardcore detail-density directives. |
@@ -846,7 +846,7 @@ pair metadata through the core Python APIs, then verifies:
| Wrong location | `categories/location_pools.json`, category `scene_pool`, `_scene_pool`. | | Wrong location | `categories/location_pools.json`, category `scene_pool`, `_scene_pool`. |
| Location good but camera/location layout wrong | `_camera_scene_directive_for_context`, coworking adapter functions. | | Location good but camera/location layout wrong | `_camera_scene_directive_for_context`, coworking adapter functions. |
| Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. | | Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. |
| Wrong expression intensity | Character slot expression settings, `row_expression.py`, expression pools. | | Wrong expression intensity or action-incompatible character expression | Character slot expression settings, `row_expression.py`, expression pools. |
| Expression appears when disabled | `row_expression.disable_row_expression`, formatter expression extraction. | | Expression appears when disabled | `row_expression.disable_row_expression`, formatter expression extraction. |
| Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `hardcore_position_config.apply_hardcore_position_config_to_subcategory`. | | Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `hardcore_position_config.apply_hardcore_position_config_to_subcategory`. |
| Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and `krea_action_context.is_foreplay_text` / `krea_action_positions.hardcore_pose_anchor`. | | Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and `krea_action_context.is_foreplay_text` / `krea_action_positions.hardcore_pose_anchor`. |
+3 -84
View File
@@ -1454,93 +1454,12 @@ def _sanitize_character_expression_text_for_action(
item: Any, item: Any,
axis_values: Any = None, axis_values: Any = None,
) -> str: ) -> str:
text = str(expression_text or "").strip() return row_expression_policy.sanitize_character_expression_text_for_action(
if not text: expression_text,
return ""
context = " ".join(
str(part or "").lower()
for part in (
role_graph, role_graph,
item, item,
*((axis_values or {}).values() if isinstance(axis_values, dict) else ()), axis_values,
) )
)
woman_active_outercourse = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"boobjob",
"titjob",
"breast sex",
"breasts tightly",
"testicle",
"balls-licking",
"balls licking",
"penis-licking",
"penis licking",
"handjob",
"hand job",
"footjob",
)
)
)
woman_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"takes man",
"penis in her mouth",
"mouth at penis level",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"lips wrapped",
)
)
)
man_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"mouth on her pussy",
"mouth on woman",
"mouth pressed to her pussy",
"cunnilingus",
"pussy licking",
"tongue on pussy",
)
)
)
mouth_expression_terms = ("mouth", "oral", "tongue", "lips", "gagging", "saliva")
clauses = [clause.strip() for clause in text.split(";") if clause.strip()]
if woman_active_outercourse:
clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)]
if woman_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Man [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
if man_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Woman [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
return "; ".join(clauses)
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str: def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
+95
View File
@@ -302,3 +302,98 @@ def character_expression_entries(
used.add(choice) used.add(choice)
expressions.append(f"{label} has {choice}") expressions.append(f"{label} has {choice}")
return expressions return expressions
def sanitize_character_expression_text_for_action(
expression_text: str,
role_graph: Any,
item: Any,
axis_values: Any = None,
) -> str:
text = str(expression_text or "").strip()
if not text:
return ""
context = " ".join(
str(part or "").lower()
for part in (
role_graph,
item,
*((axis_values or {}).values() if isinstance(axis_values, dict) else ()),
)
)
woman_active_outercourse = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"boobjob",
"titjob",
"breast sex",
"breasts tightly",
"testicle",
"balls-licking",
"balls licking",
"penis-licking",
"penis licking",
"handjob",
"hand job",
"footjob",
)
)
)
woman_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"takes man",
"penis in her mouth",
"mouth at penis level",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"lips wrapped",
)
)
)
man_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"mouth on her pussy",
"mouth on woman",
"mouth pressed to her pussy",
"cunnilingus",
"pussy licking",
"tongue on pussy",
)
)
)
mouth_expression_terms = ("mouth", "oral", "tongue", "lips", "gagging", "saliva")
clauses = [clause.strip() for clause in text.split(";") if clause.strip()]
if woman_active_outercourse:
clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)]
if woman_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Man [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
if man_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Woman [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
return "; ".join(clauses)
+19
View File
@@ -733,6 +733,25 @@ def smoke_row_expression_policy() -> None:
== (None, "character_slots:disabled"), == (None, "character_slots:disabled"),
"Row expression cast override did not honor all-slot expression disable", "Row expression cast override did not honor all-slot expression disable",
) )
expression_text = "Woman A has steady focus; Man A has parted lips with saliva"
context_role = "Woman A performs a handjob while Man A stands close"
_expect(
pb._sanitize_character_expression_text_for_action(expression_text, context_role, "", {})
== row_expression.sanitize_character_expression_text_for_action(expression_text, context_role, "", {})
== "Woman A has steady focus",
"Row expression action sanitizer did not remove incompatible partner expression",
)
reverse_context = "Man A has mouth pressed to her pussy while Woman A lies back"
_expect(
row_expression.sanitize_character_expression_text_for_action(
"Woman A has tongue out; Man A has focused mouth contact",
reverse_context,
"",
{},
)
== "Man A has focused mouth contact",
"Row expression action sanitizer did not remove incompatible visible-subject expression",
)
def smoke_row_item_policy() -> None: def smoke_row_item_policy() -> None: