Extract row expression policy
This commit is contained in:
@@ -169,6 +169,10 @@ Already isolated:
|
|||||||
runtime location/composition pool overrides, and generator fallback pool
|
runtime location/composition pool overrides, and generator fallback pool
|
||||||
selection live in `row_pools.py`; `prompt_builder.py` keeps public delegate
|
selection live in `row_pools.py`; `prompt_builder.py` keeps public delegate
|
||||||
wrappers.
|
wrappers.
|
||||||
|
- row expression text cleanup, expression intensity weighting,
|
||||||
|
character-slot/cast expression override resolution, and per-character
|
||||||
|
expression picking 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
|
||||||
|
|||||||
@@ -82,6 +82,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_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. |
|
||||||
@@ -299,8 +300,8 @@ Edit targets:
|
|||||||
|
|
||||||
### Expression
|
### Expression
|
||||||
|
|
||||||
Expression text is selected by `_expression_pool`, then filtered by
|
Expression text is selected by `_expression_pool`, then filtered through
|
||||||
`_expression_entries_for_intensity`.
|
`row_expression.expression_entries_for_intensity`.
|
||||||
|
|
||||||
Resolution order:
|
Resolution order:
|
||||||
|
|
||||||
@@ -315,7 +316,7 @@ Edit targets:
|
|||||||
- General expression pools: `categories/expression_composition_pools.json`.
|
- General expression pools: `categories/expression_composition_pools.json`.
|
||||||
- Hardcore-specific expressions: usually `categories/sexual_poses.json` or named
|
- Hardcore-specific expressions: usually `categories/sexual_poses.json` or named
|
||||||
hardcore expression pools.
|
hardcore expression pools.
|
||||||
- Character-level expression settings: slot config and `_cast_expression_intensity_override`.
|
- Character-level expression settings: slot config and `row_expression.py`.
|
||||||
- Formatter expression wording: `krea_formatter.py` or `caption_naturalizer.py`.
|
- Formatter expression wording: `krea_formatter.py` or `caption_naturalizer.py`.
|
||||||
|
|
||||||
### Pose / Action
|
### Pose / Action
|
||||||
@@ -842,8 +843,8 @@ 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, `_expression_entries_for_intensity`, expression pools. |
|
| Wrong expression intensity | Character slot expression settings, `row_expression.py`, expression pools. |
|
||||||
| Expression appears when disabled | `_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`. |
|
||||||
| Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. |
|
| Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. |
|
||||||
|
|||||||
+20
-214
@@ -41,6 +41,7 @@ try:
|
|||||||
from . import pov_policy
|
from . import pov_policy
|
||||||
from . import row_normalization as row_policy
|
from . import row_normalization as row_policy
|
||||||
from . import row_camera as row_camera_policy
|
from . import row_camera as row_camera_policy
|
||||||
|
from . import row_expression as row_expression_policy
|
||||||
from . import row_location as row_location_policy
|
from . import row_location as row_location_policy
|
||||||
from . import row_pools as row_pool_policy
|
from . import row_pools as row_pool_policy
|
||||||
from . import seed_config as seed_policy
|
from . import seed_config as seed_policy
|
||||||
@@ -85,6 +86,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
import pov_policy
|
import pov_policy
|
||||||
import row_normalization as row_policy
|
import row_normalization as row_policy
|
||||||
import row_camera as row_camera_policy
|
import row_camera as row_camera_policy
|
||||||
|
import row_expression as row_expression_policy
|
||||||
import row_location as row_location_policy
|
import row_location as row_location_policy
|
||||||
import row_pools as row_pool_policy
|
import row_pools as row_pool_policy
|
||||||
import seed_config as seed_policy
|
import seed_config as seed_policy
|
||||||
@@ -1135,44 +1137,15 @@ def _format(template: str, context: dict[str, Any]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _clean_prompt_punctuation(text: str) -> str:
|
def _clean_prompt_punctuation(text: str) -> str:
|
||||||
text = re.sub(r"\s+", " ", str(text or "")).strip()
|
return row_expression_policy.clean_prompt_punctuation(text)
|
||||||
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
|
||||||
text = re.sub(r"(?:,\s*){2,}", ", ", text)
|
|
||||||
text = re.sub(r"\.\s*\.", ".", text)
|
|
||||||
text = re.sub(r":\s*\.", ".", text)
|
|
||||||
return text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_expression_text(text: str, expression: Any = "") -> str:
|
def _strip_expression_text(text: str, expression: Any = "") -> str:
|
||||||
text = str(text or "")
|
return row_expression_policy.strip_expression_text(text, expression)
|
||||||
if not text:
|
|
||||||
return ""
|
|
||||||
text = re.sub(r"\s*Facial expressions?:\s*[^.]*\.\s*", " ", text, flags=re.IGNORECASE)
|
|
||||||
text = re.sub(r",\s*one with [^,]+ and the other with [^,]+(?=,)", "", text, flags=re.IGNORECASE)
|
|
||||||
text = re.sub(r",\s*a lively mix of expressions from [^,]+(?=,)", "", text, flags=re.IGNORECASE)
|
|
||||||
text = re.sub(r"\s+with\s+(?:an?|the)\s+[^,]*expression(?=,)", "", text, flags=re.IGNORECASE)
|
|
||||||
expression_text = str(expression or "").strip()
|
|
||||||
if expression_text:
|
|
||||||
for part in [piece.strip() for piece in expression_text.split(";") if piece.strip()]:
|
|
||||||
escaped = re.escape(part)
|
|
||||||
text = re.sub(rf",\s*{escaped}(?=,)", "", text, flags=re.IGNORECASE)
|
|
||||||
text = re.sub(rf"\s+with\s+(?:an?|the)?\s*{escaped}", "", text, flags=re.IGNORECASE)
|
|
||||||
return _clean_prompt_punctuation(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dict[str, Any]:
|
def _disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dict[str, Any]:
|
||||||
previous_expression = row.get("expression", "")
|
return row_expression_policy.disable_row_expression(row, source)
|
||||||
row["prompt"] = _strip_expression_text(row.get("prompt", ""), previous_expression)
|
|
||||||
row["caption"] = _strip_expression_text(row.get("caption", ""), previous_expression)
|
|
||||||
row["expression"] = ""
|
|
||||||
row["shared_expression"] = ""
|
|
||||||
row["character_expressions"] = []
|
|
||||||
row["character_expression_text"] = ""
|
|
||||||
row["expression_enabled"] = False
|
|
||||||
row["expression_disabled"] = True
|
|
||||||
row["expression_intensity"] = None
|
|
||||||
row["expression_intensity_source"] = source
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str:
|
def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str:
|
||||||
@@ -1837,10 +1810,6 @@ def _slot_effective_figure(
|
|||||||
return character_slot_policy.slot_effective_figure(slot, subject_type, fallback_figure)
|
return character_slot_policy.slot_effective_figure(slot, subject_type, fallback_figure)
|
||||||
|
|
||||||
|
|
||||||
def _mean(values: list[float]) -> float:
|
|
||||||
return sum(values) / len(values)
|
|
||||||
|
|
||||||
|
|
||||||
def _cast_expression_intensity_override(
|
def _cast_expression_intensity_override(
|
||||||
fallback: float,
|
fallback: float,
|
||||||
label_map: dict[str, dict[str, Any]],
|
label_map: dict[str, dict[str, Any]],
|
||||||
@@ -1848,35 +1817,13 @@ def _cast_expression_intensity_override(
|
|||||||
men_count: int,
|
men_count: int,
|
||||||
expression_phase: str = "",
|
expression_phase: str = "",
|
||||||
) -> tuple[float | None, str]:
|
) -> tuple[float | None, str]:
|
||||||
groups: list[tuple[str, list[str]]] = [
|
return row_expression_policy.cast_expression_intensity_override(
|
||||||
("women", [f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))]),
|
fallback,
|
||||||
("men", [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]),
|
label_map,
|
||||||
]
|
women_count,
|
||||||
all_values: list[float] = []
|
men_count,
|
||||||
matching_slots: list[dict[str, Any]] = []
|
expression_phase,
|
||||||
for group_name, labels in groups:
|
)
|
||||||
values: list[float] = []
|
|
||||||
value_labels: list[str] = []
|
|
||||||
for label in labels:
|
|
||||||
slot = label_map.get(label)
|
|
||||||
if _slot_is_pov(slot):
|
|
||||||
continue
|
|
||||||
if slot:
|
|
||||||
matching_slots.append(slot)
|
|
||||||
value = _slot_expression_intensity_for_phase(slot, expression_phase)
|
|
||||||
if value is not None:
|
|
||||||
values.append(value)
|
|
||||||
value_labels.append(label)
|
|
||||||
all_values.append(value)
|
|
||||||
if values:
|
|
||||||
if len(values) == 1:
|
|
||||||
return values[0], f"character_slot:{value_labels[0]}"
|
|
||||||
return _mean(values), f"character_slots:{group_name}"
|
|
||||||
if all_values:
|
|
||||||
return _mean(all_values), "character_slots:cast"
|
|
||||||
if matching_slots and all(not _slot_expression_enabled(slot) for slot in matching_slots):
|
|
||||||
return None, "character_slots:disabled"
|
|
||||||
return fallback, "input"
|
|
||||||
|
|
||||||
|
|
||||||
def _character_expression_entries(
|
def _character_expression_entries(
|
||||||
@@ -1888,41 +1835,15 @@ def _character_expression_entries(
|
|||||||
men_count: int,
|
men_count: int,
|
||||||
expression_phase: str = "",
|
expression_phase: str = "",
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
labels = [
|
return row_expression_policy.character_expression_entries(
|
||||||
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
|
rng,
|
||||||
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
|
expression_pool,
|
||||||
]
|
fallback_intensity,
|
||||||
expressions: list[str] = []
|
label_map,
|
||||||
used: set[str] = set()
|
|
||||||
for label in labels:
|
|
||||||
slot = label_map.get(label)
|
|
||||||
if not slot:
|
|
||||||
continue
|
|
||||||
if _slot_is_pov(slot):
|
|
||||||
continue
|
|
||||||
if not _slot_expression_enabled(slot):
|
|
||||||
continue
|
|
||||||
intensity = _slot_expression_intensity_for_phase(slot, expression_phase)
|
|
||||||
if intensity is None:
|
|
||||||
intensity = fallback_intensity
|
|
||||||
entries = _compatible_entries(
|
|
||||||
_expression_entries_for_intensity(expression_pool, intensity),
|
|
||||||
women_count,
|
women_count,
|
||||||
men_count,
|
men_count,
|
||||||
|
expression_phase,
|
||||||
)
|
)
|
||||||
if not entries:
|
|
||||||
continue
|
|
||||||
choice = ""
|
|
||||||
for _attempt in range(5):
|
|
||||||
candidate = _choose_text(rng, entries)
|
|
||||||
if candidate not in used:
|
|
||||||
choice = candidate
|
|
||||||
break
|
|
||||||
if not choice:
|
|
||||||
choice = _choose_text(rng, entries)
|
|
||||||
used.add(choice)
|
|
||||||
expressions.append(f"{label} has {choice}")
|
|
||||||
return expressions
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_character_expression_text_for_action(
|
def _sanitize_character_expression_text_for_action(
|
||||||
@@ -2521,126 +2442,11 @@ def _expression_pool(category: dict[str, Any], subcategory: dict[str, Any], item
|
|||||||
|
|
||||||
|
|
||||||
def _expression_intensity_hint(entry: Any) -> float:
|
def _expression_intensity_hint(entry: Any) -> float:
|
||||||
if isinstance(entry, dict):
|
return row_expression_policy.expression_intensity_hint(entry)
|
||||||
for key in ("expression_intensity", "intensity"):
|
|
||||||
if key in entry:
|
|
||||||
return _clamped_float(entry[key], 0.5)
|
|
||||||
|
|
||||||
text = _entry_text(entry).lower()
|
|
||||||
high_terms = (
|
|
||||||
"ahegao",
|
|
||||||
"orgasm",
|
|
||||||
"climax",
|
|
||||||
"drool",
|
|
||||||
"drooling",
|
|
||||||
"tongue out",
|
|
||||||
"eyes rolled",
|
|
||||||
"fucked-out",
|
|
||||||
"cum-smeared",
|
|
||||||
"saliva",
|
|
||||||
"gagging",
|
|
||||||
"slack jaw",
|
|
||||||
"jaw slack",
|
|
||||||
"slack-jawed",
|
|
||||||
"sex-drunk",
|
|
||||||
"overwhelmed",
|
|
||||||
"strained",
|
|
||||||
"messy",
|
|
||||||
"panting",
|
|
||||||
"trembling",
|
|
||||||
"shaking",
|
|
||||||
"wide open mouth",
|
|
||||||
"raw ",
|
|
||||||
"wild ",
|
|
||||||
"dazed",
|
|
||||||
"spent",
|
|
||||||
)
|
|
||||||
if any(term in text for term in high_terms):
|
|
||||||
return 0.9
|
|
||||||
|
|
||||||
medium_terms = (
|
|
||||||
"seductive",
|
|
||||||
"teasing",
|
|
||||||
"lustful",
|
|
||||||
"aroused",
|
|
||||||
"bedroom",
|
|
||||||
"dominant",
|
|
||||||
"predatory",
|
|
||||||
"control",
|
|
||||||
"stern",
|
|
||||||
"strict",
|
|
||||||
"smirk",
|
|
||||||
"parted lips",
|
|
||||||
"open-mouthed",
|
|
||||||
"heated",
|
|
||||||
"hungry",
|
|
||||||
"inviting",
|
|
||||||
"sensual",
|
|
||||||
"fetish",
|
|
||||||
"commanding",
|
|
||||||
"flushed",
|
|
||||||
"moan",
|
|
||||||
)
|
|
||||||
if any(term in text for term in medium_terms):
|
|
||||||
return 0.62
|
|
||||||
|
|
||||||
low_terms = (
|
|
||||||
"neutral",
|
|
||||||
"quiet",
|
|
||||||
"calm",
|
|
||||||
"reserved",
|
|
||||||
"relaxed",
|
|
||||||
"candid",
|
|
||||||
"closed-mouth",
|
|
||||||
"thoughtful",
|
|
||||||
"controlled",
|
|
||||||
"focused",
|
|
||||||
"steady",
|
|
||||||
"bitten-lip",
|
|
||||||
"braced",
|
|
||||||
"held breath",
|
|
||||||
"concentrated",
|
|
||||||
"aloof",
|
|
||||||
"bored",
|
|
||||||
"tired",
|
|
||||||
"unfocused",
|
|
||||||
"contented",
|
|
||||||
"fashion",
|
|
||||||
"soft",
|
|
||||||
"sleepy",
|
|
||||||
"fresh-faced",
|
|
||||||
)
|
|
||||||
if any(term in text for term in low_terms):
|
|
||||||
return 0.25
|
|
||||||
return 0.5
|
|
||||||
|
|
||||||
|
|
||||||
def _expression_entries_for_intensity(entries: list[Any], expression_intensity: float) -> list[Any]:
|
def _expression_entries_for_intensity(entries: list[Any], expression_intensity: float) -> list[Any]:
|
||||||
target = _clamped_float(expression_intensity, 0.5)
|
return row_expression_policy.expression_entries_for_intensity(entries, expression_intensity)
|
||||||
weighted: list[Any] = []
|
|
||||||
for entry in entries:
|
|
||||||
entry_intensity = _expression_intensity_hint(entry)
|
|
||||||
distance = abs(target - entry_intensity)
|
|
||||||
if distance <= 0.18:
|
|
||||||
intensity_weight = 4.0
|
|
||||||
elif distance <= 0.35:
|
|
||||||
intensity_weight = 1.4
|
|
||||||
elif distance <= 0.55:
|
|
||||||
intensity_weight = 0.35
|
|
||||||
else:
|
|
||||||
intensity_weight = 0.05
|
|
||||||
|
|
||||||
if isinstance(entry, dict):
|
|
||||||
adjusted = dict(entry)
|
|
||||||
try:
|
|
||||||
base_weight = float(adjusted.get("weight", 1.0))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
base_weight = 1.0
|
|
||||||
adjusted["weight"] = max(0.0, base_weight) * intensity_weight
|
|
||||||
weighted.append(adjusted)
|
|
||||||
else:
|
|
||||||
weighted.append({"text": _entry_text(entry), "weight": intensity_weight})
|
|
||||||
return weighted or entries
|
|
||||||
|
|
||||||
|
|
||||||
def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str, poses: str) -> list[Any]:
|
def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str, poses: str) -> list[Any]:
|
||||||
|
|||||||
@@ -0,0 +1,304 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import category_library as category_policy
|
||||||
|
from . import character_slot as character_slot_policy
|
||||||
|
from . import pov_policy
|
||||||
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
|
import category_library as category_policy
|
||||||
|
import character_slot as character_slot_policy
|
||||||
|
import pov_policy
|
||||||
|
|
||||||
|
|
||||||
|
def clean_prompt_punctuation(text: str) -> str:
|
||||||
|
text = re.sub(r"\s+", " ", str(text or "")).strip()
|
||||||
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
||||||
|
text = re.sub(r"(?:,\s*){2,}", ", ", text)
|
||||||
|
text = re.sub(r"\.\s*\.", ".", text)
|
||||||
|
text = re.sub(r":\s*\.", ".", text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def strip_expression_text(text: str, expression: Any = "") -> str:
|
||||||
|
text = str(text or "")
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
text = re.sub(r"\s*Facial expressions?:\s*[^.]*\.\s*", " ", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r",\s*one with [^,]+ and the other with [^,]+(?=,)", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r",\s*a lively mix of expressions from [^,]+(?=,)", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(r"\s+with\s+(?:an?|the)\s+[^,]*expression(?=,)", "", text, flags=re.IGNORECASE)
|
||||||
|
expression_text = str(expression or "").strip()
|
||||||
|
if expression_text:
|
||||||
|
for part in [piece.strip() for piece in expression_text.split(";") if piece.strip()]:
|
||||||
|
escaped = re.escape(part)
|
||||||
|
text = re.sub(rf",\s*{escaped}(?=,)", "", text, flags=re.IGNORECASE)
|
||||||
|
text = re.sub(rf"\s+with\s+(?:an?|the)?\s*{escaped}", "", text, flags=re.IGNORECASE)
|
||||||
|
return clean_prompt_punctuation(text)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dict[str, Any]:
|
||||||
|
previous_expression = row.get("expression", "")
|
||||||
|
row["prompt"] = strip_expression_text(row.get("prompt", ""), previous_expression)
|
||||||
|
row["caption"] = strip_expression_text(row.get("caption", ""), previous_expression)
|
||||||
|
row["expression"] = ""
|
||||||
|
row["shared_expression"] = ""
|
||||||
|
row["character_expressions"] = []
|
||||||
|
row["character_expression_text"] = ""
|
||||||
|
row["expression_enabled"] = False
|
||||||
|
row["expression_disabled"] = True
|
||||||
|
row["expression_intensity"] = None
|
||||||
|
row["expression_intensity_source"] = source
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
|
||||||
|
try:
|
||||||
|
number = float(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
return max(min_value, min(max_value, number))
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_text(entry: Any) -> str:
|
||||||
|
return category_policy._entry_text(entry)
|
||||||
|
|
||||||
|
|
||||||
|
def expression_intensity_hint(entry: Any) -> float:
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
for key in ("expression_intensity", "intensity"):
|
||||||
|
if key in entry:
|
||||||
|
return _clamped_float(entry[key], 0.5)
|
||||||
|
|
||||||
|
text = _entry_text(entry).lower()
|
||||||
|
high_terms = (
|
||||||
|
"ahegao",
|
||||||
|
"orgasm",
|
||||||
|
"climax",
|
||||||
|
"drool",
|
||||||
|
"drooling",
|
||||||
|
"tongue out",
|
||||||
|
"eyes rolled",
|
||||||
|
"fucked-out",
|
||||||
|
"cum-smeared",
|
||||||
|
"saliva",
|
||||||
|
"gagging",
|
||||||
|
"slack jaw",
|
||||||
|
"jaw slack",
|
||||||
|
"slack-jawed",
|
||||||
|
"sex-drunk",
|
||||||
|
"overwhelmed",
|
||||||
|
"strained",
|
||||||
|
"messy",
|
||||||
|
"panting",
|
||||||
|
"trembling",
|
||||||
|
"shaking",
|
||||||
|
"wide open mouth",
|
||||||
|
"raw ",
|
||||||
|
"wild ",
|
||||||
|
"dazed",
|
||||||
|
"spent",
|
||||||
|
)
|
||||||
|
if any(term in text for term in high_terms):
|
||||||
|
return 0.9
|
||||||
|
|
||||||
|
medium_terms = (
|
||||||
|
"seductive",
|
||||||
|
"teasing",
|
||||||
|
"lustful",
|
||||||
|
"aroused",
|
||||||
|
"bedroom",
|
||||||
|
"dominant",
|
||||||
|
"predatory",
|
||||||
|
"control",
|
||||||
|
"stern",
|
||||||
|
"strict",
|
||||||
|
"smirk",
|
||||||
|
"parted lips",
|
||||||
|
"open-mouthed",
|
||||||
|
"heated",
|
||||||
|
"hungry",
|
||||||
|
"inviting",
|
||||||
|
"sensual",
|
||||||
|
"fetish",
|
||||||
|
"commanding",
|
||||||
|
"flushed",
|
||||||
|
"moan",
|
||||||
|
)
|
||||||
|
if any(term in text for term in medium_terms):
|
||||||
|
return 0.62
|
||||||
|
|
||||||
|
low_terms = (
|
||||||
|
"neutral",
|
||||||
|
"quiet",
|
||||||
|
"calm",
|
||||||
|
"reserved",
|
||||||
|
"relaxed",
|
||||||
|
"candid",
|
||||||
|
"closed-mouth",
|
||||||
|
"thoughtful",
|
||||||
|
"controlled",
|
||||||
|
"focused",
|
||||||
|
"steady",
|
||||||
|
"bitten-lip",
|
||||||
|
"braced",
|
||||||
|
"held breath",
|
||||||
|
"concentrated",
|
||||||
|
"aloof",
|
||||||
|
"bored",
|
||||||
|
"tired",
|
||||||
|
"unfocused",
|
||||||
|
"contented",
|
||||||
|
"fashion",
|
||||||
|
"soft",
|
||||||
|
"sleepy",
|
||||||
|
"fresh-faced",
|
||||||
|
)
|
||||||
|
if any(term in text for term in low_terms):
|
||||||
|
return 0.25
|
||||||
|
return 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def expression_entries_for_intensity(entries: list[Any], expression_intensity: float) -> list[Any]:
|
||||||
|
target = _clamped_float(expression_intensity, 0.5)
|
||||||
|
weighted: list[Any] = []
|
||||||
|
for entry in entries:
|
||||||
|
entry_intensity = expression_intensity_hint(entry)
|
||||||
|
distance = abs(target - entry_intensity)
|
||||||
|
if distance <= 0.18:
|
||||||
|
intensity_weight = 4.0
|
||||||
|
elif distance <= 0.35:
|
||||||
|
intensity_weight = 1.4
|
||||||
|
elif distance <= 0.55:
|
||||||
|
intensity_weight = 0.35
|
||||||
|
else:
|
||||||
|
intensity_weight = 0.05
|
||||||
|
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
adjusted = dict(entry)
|
||||||
|
try:
|
||||||
|
base_weight = float(adjusted.get("weight", 1.0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
base_weight = 1.0
|
||||||
|
adjusted["weight"] = max(0.0, base_weight) * intensity_weight
|
||||||
|
weighted.append(adjusted)
|
||||||
|
else:
|
||||||
|
weighted.append({"text": _entry_text(entry), "weight": intensity_weight})
|
||||||
|
return weighted or entries
|
||||||
|
|
||||||
|
|
||||||
|
def _mean(values: list[float]) -> float:
|
||||||
|
return sum(values) / len(values)
|
||||||
|
|
||||||
|
|
||||||
|
def cast_expression_intensity_override(
|
||||||
|
fallback: float,
|
||||||
|
label_map: dict[str, dict[str, Any]],
|
||||||
|
women_count: int,
|
||||||
|
men_count: int,
|
||||||
|
expression_phase: str = "",
|
||||||
|
) -> tuple[float | None, str]:
|
||||||
|
groups: list[tuple[str, list[str]]] = [
|
||||||
|
("women", [f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))]),
|
||||||
|
("men", [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]),
|
||||||
|
]
|
||||||
|
all_values: list[float] = []
|
||||||
|
matching_slots: list[dict[str, Any]] = []
|
||||||
|
for group_name, labels in groups:
|
||||||
|
values: list[float] = []
|
||||||
|
value_labels: list[str] = []
|
||||||
|
for label in labels:
|
||||||
|
slot = label_map.get(label)
|
||||||
|
if pov_policy.slot_is_pov(slot):
|
||||||
|
continue
|
||||||
|
if slot:
|
||||||
|
matching_slots.append(slot)
|
||||||
|
value = character_slot_policy.slot_expression_intensity_for_phase(slot, expression_phase)
|
||||||
|
if value is not None:
|
||||||
|
values.append(value)
|
||||||
|
value_labels.append(label)
|
||||||
|
all_values.append(value)
|
||||||
|
if values:
|
||||||
|
if len(values) == 1:
|
||||||
|
return values[0], f"character_slot:{value_labels[0]}"
|
||||||
|
return _mean(values), f"character_slots:{group_name}"
|
||||||
|
if all_values:
|
||||||
|
return _mean(all_values), "character_slots:cast"
|
||||||
|
if matching_slots and all(not character_slot_policy.slot_expression_enabled(slot) for slot in matching_slots):
|
||||||
|
return None, "character_slots:disabled"
|
||||||
|
return fallback, "input"
|
||||||
|
|
||||||
|
|
||||||
|
def _weighted_choice(rng: random.Random, items: list[Any]) -> Any:
|
||||||
|
if not items:
|
||||||
|
raise ValueError("Cannot choose from an empty list")
|
||||||
|
weights: list[float] = []
|
||||||
|
for item in items:
|
||||||
|
weight = item.get("weight", 1.0) if isinstance(item, dict) else 1.0
|
||||||
|
try:
|
||||||
|
weights.append(max(0.0, float(weight)))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
weights.append(1.0)
|
||||||
|
total = sum(weights)
|
||||||
|
if total <= 0:
|
||||||
|
return items[rng.randrange(len(items))]
|
||||||
|
pick = rng.random() * total
|
||||||
|
running = 0.0
|
||||||
|
for item, weight in zip(items, weights):
|
||||||
|
running += weight
|
||||||
|
if pick <= running:
|
||||||
|
return item
|
||||||
|
return items[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_text(rng: random.Random, items: list[Any]) -> str:
|
||||||
|
return _entry_text(_weighted_choice(rng, items))
|
||||||
|
|
||||||
|
|
||||||
|
def character_expression_entries(
|
||||||
|
rng: random.Random,
|
||||||
|
expression_pool: list[Any],
|
||||||
|
fallback_intensity: float,
|
||||||
|
label_map: dict[str, dict[str, Any]],
|
||||||
|
women_count: int,
|
||||||
|
men_count: int,
|
||||||
|
expression_phase: str = "",
|
||||||
|
) -> list[str]:
|
||||||
|
labels = [
|
||||||
|
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
|
||||||
|
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
|
||||||
|
]
|
||||||
|
expressions: list[str] = []
|
||||||
|
used: set[str] = set()
|
||||||
|
for label in labels:
|
||||||
|
slot = label_map.get(label)
|
||||||
|
if not slot:
|
||||||
|
continue
|
||||||
|
if pov_policy.slot_is_pov(slot):
|
||||||
|
continue
|
||||||
|
if not character_slot_policy.slot_expression_enabled(slot):
|
||||||
|
continue
|
||||||
|
intensity = character_slot_policy.slot_expression_intensity_for_phase(slot, expression_phase)
|
||||||
|
if intensity is None:
|
||||||
|
intensity = fallback_intensity
|
||||||
|
entries = category_policy.compatible_entries(
|
||||||
|
expression_entries_for_intensity(expression_pool, intensity),
|
||||||
|
women_count,
|
||||||
|
men_count,
|
||||||
|
)
|
||||||
|
if not entries:
|
||||||
|
continue
|
||||||
|
choice = ""
|
||||||
|
for _attempt in range(5):
|
||||||
|
candidate = _choose_text(rng, entries)
|
||||||
|
if candidate not in used:
|
||||||
|
choice = candidate
|
||||||
|
break
|
||||||
|
if not choice:
|
||||||
|
choice = _choose_text(rng, entries)
|
||||||
|
used.add(choice)
|
||||||
|
expressions.append(f"{label} has {choice}")
|
||||||
|
return expressions
|
||||||
@@ -51,6 +51,7 @@ import pov_policy # noqa: E402
|
|||||||
import row_normalization # noqa: E402
|
import row_normalization # noqa: E402
|
||||||
import route_metadata # noqa: E402
|
import route_metadata # noqa: E402
|
||||||
import row_camera # noqa: E402
|
import row_camera # noqa: E402
|
||||||
|
import row_expression # noqa: E402
|
||||||
import row_location # noqa: E402
|
import row_location # noqa: E402
|
||||||
import row_pools # noqa: E402
|
import row_pools # noqa: E402
|
||||||
import server_routes # noqa: E402
|
import server_routes # noqa: E402
|
||||||
@@ -658,6 +659,79 @@ def smoke_row_location_policy() -> None:
|
|||||||
_expect(", long archive aisle composition," in updated.get("caption", ""), "Row location policy did not rewrite caption composition")
|
_expect(", long archive aisle composition," in updated.get("caption", ""), "Row location policy did not rewrite caption composition")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_row_expression_policy() -> None:
|
||||||
|
entries = [
|
||||||
|
{"text": "quiet calm focus", "weight": 2.0},
|
||||||
|
{"text": "heated smirk", "weight": 3.0},
|
||||||
|
{"text": "dazed overwhelmed look", "weight": 1.0},
|
||||||
|
]
|
||||||
|
_expect(
|
||||||
|
pb._expression_entries_for_intensity(entries, 0.25) == row_expression.expression_entries_for_intensity(entries, 0.25),
|
||||||
|
"Prompt builder expression intensity wrapper should delegate to row_expression",
|
||||||
|
)
|
||||||
|
weighted = row_expression.expression_entries_for_intensity(entries, 0.25)
|
||||||
|
_expect(weighted[0]["weight"] == 8.0, "Row expression low-intensity weighting changed")
|
||||||
|
_expect(weighted[2]["weight"] == 0.05, "Row expression distant-intensity weighting changed")
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"prompt": "Person with quiet focus, in room. Facial expression: quiet focus. Keep this.",
|
||||||
|
"caption": "sxcppnl7, person with quiet focus, in room",
|
||||||
|
"expression": "quiet focus",
|
||||||
|
"shared_expression": "quiet focus",
|
||||||
|
"character_expressions": ["Woman A has quiet focus"],
|
||||||
|
"character_expression_text": "Woman A has quiet focus",
|
||||||
|
}
|
||||||
|
_expect(
|
||||||
|
pb._disable_row_expression(dict(row), "test") == row_expression.disable_row_expression(dict(row), "test"),
|
||||||
|
"Prompt builder expression disable wrapper should delegate to row_expression",
|
||||||
|
)
|
||||||
|
disabled = row_expression.disable_row_expression(dict(row), "test")
|
||||||
|
_expect(disabled.get("expression_disabled") is True, "Row expression disable did not set disabled metadata")
|
||||||
|
_expect("quiet focus" not in disabled.get("prompt", ""), "Row expression disable did not strip prompt expression text")
|
||||||
|
|
||||||
|
woman_slot = character_slot.normalize_character_slot(
|
||||||
|
{
|
||||||
|
"subject_type": "woman",
|
||||||
|
"label": "A",
|
||||||
|
"expression_intensity": 0.8,
|
||||||
|
"softcore_expression_intensity": 0.2,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pov_man_slot = character_slot.normalize_character_slot(
|
||||||
|
{
|
||||||
|
"subject_type": "man",
|
||||||
|
"label": "A",
|
||||||
|
"presence_mode": "pov",
|
||||||
|
"expression_intensity": 1.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
label_map = {"Woman A": woman_slot, "Man A": pov_man_slot}
|
||||||
|
_expect(
|
||||||
|
pb._cast_expression_intensity_override(0.5, label_map, 1, 1, "softcore")
|
||||||
|
== row_expression.cast_expression_intensity_override(0.5, label_map, 1, 1, "softcore"),
|
||||||
|
"Prompt builder cast expression override wrapper should delegate to row_expression",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
row_expression.cast_expression_intensity_override(0.5, label_map, 1, 1, "softcore")
|
||||||
|
== (0.2, "character_slot:Woman A"),
|
||||||
|
"Row expression cast override did not prefer visible slot phase intensity",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
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"),
|
||||||
|
"Prompt builder character expression wrapper should delegate to row_expression",
|
||||||
|
)
|
||||||
|
|
||||||
|
disabled_slot = character_slot.normalize_character_slot(
|
||||||
|
{"subject_type": "woman", "label": "A", "expression_enabled": False}
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
row_expression.cast_expression_intensity_override(0.5, {"Woman A": disabled_slot}, 1, 0, "hardcore")
|
||||||
|
== (None, "character_slots:disabled"),
|
||||||
|
"Row expression cast override did not honor all-slot expression disable",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def smoke_category_cast_config_policy() -> None:
|
def smoke_category_cast_config_policy() -> None:
|
||||||
_expect(pb.CATEGORY_PRESETS is category_cast_config.CATEGORY_PRESETS, "Prompt builder category presets are not delegated")
|
_expect(pb.CATEGORY_PRESETS is category_cast_config.CATEGORY_PRESETS, "Prompt builder category presets are not delegated")
|
||||||
_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")
|
||||||
@@ -3856,6 +3930,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|||||||
("config_route_location_theme", smoke_config_route_location_theme),
|
("config_route_location_theme", smoke_config_route_location_theme),
|
||||||
("location_config_policy", smoke_location_config_policy),
|
("location_config_policy", smoke_location_config_policy),
|
||||||
("row_location_policy", smoke_row_location_policy),
|
("row_location_policy", smoke_row_location_policy),
|
||||||
|
("row_expression_policy", smoke_row_expression_policy),
|
||||||
("category_cast_config_policy", smoke_category_cast_config_policy),
|
("category_cast_config_policy", smoke_category_cast_config_policy),
|
||||||
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
||||||
("filter_config_policy", smoke_filter_config_policy),
|
("filter_config_policy", smoke_filter_config_policy),
|
||||||
|
|||||||
Reference in New Issue
Block a user