Extract row expression policy
This commit is contained in:
@@ -169,6 +169,10 @@ Already isolated:
|
||||
runtime location/composition pool overrides, and generator fallback pool
|
||||
selection live in `row_pools.py`; `prompt_builder.py` keeps public delegate
|
||||
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,
|
||||
config JSON builders/parsers, focus-policy toggles, subcategory allow-list
|
||||
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. |
|
||||
| `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_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. |
|
||||
| `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. |
|
||||
@@ -299,8 +300,8 @@ Edit targets:
|
||||
|
||||
### Expression
|
||||
|
||||
Expression text is selected by `_expression_pool`, then filtered by
|
||||
`_expression_entries_for_intensity`.
|
||||
Expression text is selected by `_expression_pool`, then filtered through
|
||||
`row_expression.expression_entries_for_intensity`.
|
||||
|
||||
Resolution order:
|
||||
|
||||
@@ -315,7 +316,7 @@ Edit targets:
|
||||
- General expression pools: `categories/expression_composition_pools.json`.
|
||||
- Hardcore-specific expressions: usually `categories/sexual_poses.json` or named
|
||||
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`.
|
||||
|
||||
### 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`. |
|
||||
| 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. |
|
||||
| Wrong expression intensity | Character slot expression settings, `_expression_entries_for_intensity`, expression pools. |
|
||||
| Expression appears when disabled | `_disable_row_expression`, formatter expression extraction. |
|
||||
| Wrong expression intensity | Character slot expression settings, `row_expression.py`, expression pools. |
|
||||
| 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`. |
|
||||
| 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. |
|
||||
|
||||
+20
-214
@@ -41,6 +41,7 @@ try:
|
||||
from . import pov_policy
|
||||
from . import row_normalization as row_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_pools as row_pool_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 row_normalization as row_policy
|
||||
import row_camera as row_camera_policy
|
||||
import row_expression as row_expression_policy
|
||||
import row_location as row_location_policy
|
||||
import row_pools as row_pool_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:
|
||||
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()
|
||||
return row_expression_policy.clean_prompt_punctuation(text)
|
||||
|
||||
|
||||
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)
|
||||
return row_expression_policy.strip_expression_text(text, expression)
|
||||
|
||||
|
||||
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
|
||||
return row_expression_policy.disable_row_expression(row, source)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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]],
|
||||
@@ -1848,35 +1817,13 @@ def _cast_expression_intensity_override(
|
||||
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 _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"
|
||||
return row_expression_policy.cast_expression_intensity_override(
|
||||
fallback,
|
||||
label_map,
|
||||
women_count,
|
||||
men_count,
|
||||
expression_phase,
|
||||
)
|
||||
|
||||
|
||||
def _character_expression_entries(
|
||||
@@ -1888,41 +1835,15 @@ def _character_expression_entries(
|
||||
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 _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),
|
||||
return row_expression_policy.character_expression_entries(
|
||||
rng,
|
||||
expression_pool,
|
||||
fallback_intensity,
|
||||
label_map,
|
||||
women_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(
|
||||
@@ -2521,126 +2442,11 @@ def _expression_pool(category: dict[str, Any], subcategory: dict[str, Any], item
|
||||
|
||||
|
||||
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
|
||||
return row_expression_policy.expression_intensity_hint(entry)
|
||||
|
||||
|
||||
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
|
||||
return row_expression_policy.expression_entries_for_intensity(entries, expression_intensity)
|
||||
|
||||
|
||||
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 route_metadata # noqa: E402
|
||||
import row_camera # noqa: E402
|
||||
import row_expression # noqa: E402
|
||||
import row_location # noqa: E402
|
||||
import row_pools # 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")
|
||||
|
||||
|
||||
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:
|
||||
_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")
|
||||
@@ -3856,6 +3930,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
||||
("config_route_location_theme", smoke_config_route_location_theme),
|
||||
("location_config_policy", smoke_location_config_policy),
|
||||
("row_location_policy", smoke_row_location_policy),
|
||||
("row_expression_policy", smoke_row_expression_policy),
|
||||
("category_cast_config_policy", smoke_category_cast_config_policy),
|
||||
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
||||
("filter_config_policy", smoke_filter_config_policy),
|
||||
|
||||
Reference in New Issue
Block a user