Extract expression route resolution
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user