Extract expression route resolution

This commit is contained in:
2026-06-27 10:13:55 +02:00
parent 58abbaa347
commit a5b648eb98
5 changed files with 178 additions and 29 deletions
+5 -4
View File
@@ -195,10 +195,11 @@ Already isolated:
filtering, expression-disabled handling, per-character expression promotion, filtering, expression-disabled handling, per-character expression promotion,
POV composition adaptation, and pose-category environment sanitizing live in POV composition adaptation, and pose-category environment sanitizing live in
`row_prompt_axes.py`; `prompt_builder.py` keeps a public delegate wrapper. `row_prompt_axes.py`; `prompt_builder.py` keeps a public delegate wrapper.
- row expression text cleanup, expression intensity weighting, - row expression text cleanup, expression route resolution, expression
character-slot/cast expression override resolution, and per-character intensity weighting, character-slot/cast expression override resolution, and
expression picking plus action-aware character-expression sanitizing live in per-character expression picking plus action-aware character-expression
`row_expression.py`; `prompt_builder.py` keeps public delegate wrappers. sanitizing live in `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
+1 -1
View File
@@ -90,7 +90,7 @@ Core helper ownership:
| `row_subject_route.py` | Row subject route orchestration, character slot/profile precedence, configured-cast POV labels, visible cast descriptor collection, and descriptor prompt cleanup. | | `row_subject_route.py` | Row subject route orchestration, character slot/profile precedence, configured-cast POV labels, visible cast descriptor collection, and descriptor prompt cleanup. |
| `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, per-character expression selection, and action-aware character-expression sanitizing. | | `row_expression.py` | Row expression cleanup, expression route resolution, 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. |
| `row_prompt_axes.py` | Row scene/pose/expression/composition axis selection, compatible-entry filtering, expression-disabled handling, per-character expression promotion, POV composition adaptation, and pose-category environment sanitizing. | | `row_prompt_axes.py` | Row scene/pose/expression/composition axis selection, compatible-entry filtering, expression-disabled handling, per-character expression promotion, POV composition adaptation, and pose-category environment sanitizing. |
| `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. |
+42 -24
View File
@@ -1446,6 +1446,33 @@ def _cast_expression_intensity_override(
) )
def _resolve_expression_route(
*,
expression_enabled: bool,
expression_intensity: float,
expression_intensity_source: str,
subject_type: str,
applied_slot: dict[str, Any] | None = None,
character_slots: list[dict[str, Any]] | None = None,
character_slot_map: dict[str, dict[str, Any]] | None = None,
women_count: int = 1,
men_count: int = 1,
expression_phase: str = "",
) -> row_expression_policy.ExpressionRoute:
return row_expression_policy.resolve_expression_route(
expression_enabled=expression_enabled,
expression_intensity=expression_intensity,
expression_intensity_source=expression_intensity_source,
subject_type=subject_type,
applied_slot=applied_slot,
character_slots=character_slots,
character_slot_map=character_slot_map,
women_count=women_count,
men_count=men_count,
expression_phase=expression_phase,
)
def _character_expression_entries( def _character_expression_entries(
rng: random.Random, rng: random.Random,
expression_pool: list[Any], expression_pool: list[Any],
@@ -2176,30 +2203,21 @@ def _build_custom_row(
if is_pose_category: if is_pose_category:
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph) source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph)
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels) role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
expression_intensity_source = expression_intensity_source or "input" expression_route = _resolve_expression_route(
expression_disabled = not bool(expression_enabled) expression_enabled=expression_enabled,
if expression_disabled: expression_intensity=expression_intensity,
expression_intensity_source = "disabled" expression_intensity_source=expression_intensity_source,
elif subject_type in ("woman", "man") and applied_slot: subject_type=subject_type,
slot_label = "Woman A" if subject_type == "woman" else "Man A" applied_slot=applied_slot,
if not _slot_expression_enabled(applied_slot): character_slots=character_slots,
expression_disabled = True character_slot_map=character_slot_map,
expression_intensity_source = f"character_slot:{slot_label}:disabled" women_count=women_count,
else: men_count=men_count,
slot_expression_intensity = _slot_expression_intensity_for_phase(applied_slot, expression_phase) expression_phase=expression_phase,
if slot_expression_intensity is not None: )
expression_intensity = slot_expression_intensity expression_disabled = expression_route.expression_disabled
expression_intensity_source = f"character_slot:{slot_label}" expression_intensity = expression_route.expression_intensity
elif subject_type == "configured_cast" and character_slots: expression_intensity_source = expression_route.expression_intensity_source
expression_intensity, expression_intensity_source = _cast_expression_intensity_override(
expression_intensity,
character_slot_map,
women_count,
men_count,
expression_phase,
)
if expression_intensity is None:
expression_disabled = True
prompt_axes = _prompt_axes_route( prompt_axes = _prompt_axes_route(
category=category, category=category,
+56
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import random import random
import re import re
from typing import Any from typing import Any
@@ -55,6 +56,61 @@ def disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dic
return row return row
@dataclass(frozen=True)
class ExpressionRoute:
expression_disabled: bool
expression_intensity: float | None
expression_intensity_source: str
def resolve_expression_route(
*,
expression_enabled: bool,
expression_intensity: float,
expression_intensity_source: str,
subject_type: str,
applied_slot: dict[str, Any] | None = None,
character_slots: list[dict[str, Any]] | None = None,
character_slot_map: dict[str, dict[str, Any]] | None = None,
women_count: int = 1,
men_count: int = 1,
expression_phase: str = "",
) -> ExpressionRoute:
source = expression_intensity_source or "input"
disabled = not bool(expression_enabled)
intensity: float | None = expression_intensity
if disabled:
source = "disabled"
elif subject_type in ("woman", "man") and applied_slot:
slot_label = "Woman A" if subject_type == "woman" else "Man A"
if not character_slot_policy.slot_expression_enabled(applied_slot):
disabled = True
source = f"character_slot:{slot_label}:disabled"
else:
slot_expression_intensity = character_slot_policy.slot_expression_intensity_for_phase(
applied_slot,
expression_phase,
)
if slot_expression_intensity is not None:
intensity = slot_expression_intensity
source = f"character_slot:{slot_label}"
elif subject_type == "configured_cast" and character_slots:
intensity, source = cast_expression_intensity_override(
expression_intensity,
character_slot_map or {},
women_count,
men_count,
expression_phase,
)
if intensity is None:
disabled = True
return ExpressionRoute(
expression_disabled=disabled,
expression_intensity=intensity,
expression_intensity_source=source,
)
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float: def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
try: try:
number = float(value) number = float(value)
+74
View File
@@ -725,6 +725,41 @@ def smoke_row_expression_policy() -> None:
== (0.2, "character_slot:Woman A"), == (0.2, "character_slot:Woman A"),
"Row expression cast override did not prefer visible slot phase intensity", "Row expression cast override did not prefer visible slot phase intensity",
) )
_expect(
pb._resolve_expression_route(
expression_enabled=True,
expression_intensity=0.5,
expression_intensity_source="input",
subject_type="woman",
applied_slot=woman_slot,
women_count=1,
men_count=0,
expression_phase="softcore",
)
== row_expression.resolve_expression_route(
expression_enabled=True,
expression_intensity=0.5,
expression_intensity_source="input",
subject_type="woman",
applied_slot=woman_slot,
women_count=1,
men_count=0,
expression_phase="softcore",
),
"Prompt builder expression route wrapper should delegate to row_expression",
)
route = row_expression.resolve_expression_route(
expression_enabled=True,
expression_intensity=0.5,
expression_intensity_source="input",
subject_type="woman",
applied_slot=woman_slot,
women_count=1,
men_count=0,
expression_phase="softcore",
)
_expect(route.expression_intensity == 0.2, "Expression route did not apply phase-specific slot intensity")
_expect(route.expression_intensity_source == "character_slot:Woman A", "Expression route lost slot source")
_expect( _expect(
pb._character_expression_entries(random.Random(22), entries, 0.5, label_map, 1, 1, "softcore") pb._character_expression_entries(random.Random(22), entries, 0.5, label_map, 1, 1, "softcore")
== row_expression.character_expression_entries(random.Random(22), entries, 0.5, label_map, 1, 1, "softcore"), == row_expression.character_expression_entries(random.Random(22), entries, 0.5, label_map, 1, 1, "softcore"),
@@ -739,6 +774,45 @@ 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",
) )
global_disabled = row_expression.resolve_expression_route(
expression_enabled=False,
expression_intensity=0.8,
expression_intensity_source="input",
subject_type="woman",
applied_slot=woman_slot,
)
_expect(global_disabled.expression_disabled is True, "Expression route did not honor global disabled state")
_expect(global_disabled.expression_intensity == 0.8, "Expression route changed disabled fallback intensity too early")
_expect(global_disabled.expression_intensity_source == "disabled", "Expression route did not mark global disabled source")
slot_disabled = row_expression.resolve_expression_route(
expression_enabled=True,
expression_intensity=0.5,
expression_intensity_source="input",
subject_type="woman",
applied_slot=disabled_slot,
)
_expect(slot_disabled.expression_disabled is True, "Expression route did not honor single-slot disable")
_expect(
slot_disabled.expression_intensity_source == "character_slot:Woman A:disabled",
"Expression route lost single-slot disabled source",
)
cast_disabled = row_expression.resolve_expression_route(
expression_enabled=True,
expression_intensity=0.5,
expression_intensity_source="input",
subject_type="configured_cast",
character_slots=[disabled_slot],
character_slot_map={"Woman A": disabled_slot},
women_count=1,
men_count=0,
expression_phase="hardcore",
)
_expect(cast_disabled.expression_disabled is True, "Expression route did not honor all-slot cast disable")
_expect(cast_disabled.expression_intensity is None, "Expression route did not clear all-slot disabled intensity")
_expect(
cast_disabled.expression_intensity_source == "character_slots:disabled",
"Expression route lost all-slot disabled source",
)
expression_text = "Woman A has steady focus; Man A has parted lips with saliva" 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" context_role = "Woman A performs a handjob while Man A stands close"
_expect( _expect(