8776 lines
431 KiB
Python
8776 lines
431 KiB
Python
#!/usr/bin/env python3
|
|
"""Smoke-test core prompt routes without importing ComfyUI.
|
|
|
|
The checks here are intentionally lightweight invariants, not golden prompt
|
|
snapshots. They prove that representative rows still carry structured metadata
|
|
and that the Krea2, SDXL, and caption formatter paths consume metadata instead
|
|
of silently falling back to raw text parsing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import random
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
import caption_naturalizer # noqa: E402
|
|
import caption_format_route # noqa: E402
|
|
import caption_metadata_routes # noqa: E402
|
|
import caption_policy # noqa: E402
|
|
import caption_text_policy # noqa: E402
|
|
import builder_config_route # noqa: E402
|
|
import builder_prompt_route # noqa: E402
|
|
import cast_context # noqa: E402
|
|
import category_extensions # noqa: E402
|
|
import category_template_metadata # noqa: E402
|
|
import character_appearance # noqa: E402
|
|
import character_config # noqa: E402
|
|
import character_profile # noqa: E402
|
|
import character_slot # noqa: E402
|
|
import category_cast_config # noqa: E402
|
|
import category_library # noqa: E402
|
|
import filter_config # noqa: E402
|
|
import formatter_detail # noqa: E402
|
|
import formatter_input # noqa: E402
|
|
import formatter_target # noqa: E402
|
|
import hardcore_action_metadata # noqa: E402
|
|
import hardcore_position_config # noqa: E402
|
|
import __init__ as sxcp_nodes # noqa: E402
|
|
import generation_profile_config # noqa: E402
|
|
import hardcore_role_outercourse # noqa: E402
|
|
import index_switch_policy # noqa: E402
|
|
import item_axis_policy # noqa: E402
|
|
import node_tooltips # noqa: E402
|
|
import krea_cast # noqa: E402
|
|
import krea_action_details # noqa: E402
|
|
import krea_action_context # noqa: E402
|
|
import krea_configured_cast_formatter # noqa: E402
|
|
import krea_format_route # noqa: E402
|
|
import krea_formatter # noqa: E402
|
|
import krea_normal_formatter # noqa: E402
|
|
import krea_pair_formatter # noqa: E402
|
|
import krea_row_fields # noqa: E402
|
|
import location_config # noqa: E402
|
|
import loop_nodes # noqa: E402
|
|
import pair_builder # noqa: E402
|
|
import pair_camera # noqa: E402
|
|
import pair_cast # noqa: E402
|
|
import pair_clothing # noqa: E402
|
|
import pair_rows # noqa: E402
|
|
import prompt_hygiene # noqa: E402
|
|
import prompt_builder as pb # noqa: E402
|
|
import outercourse_action_policy # noqa: E402
|
|
import pov_policy # noqa: E402
|
|
import row_normalization # noqa: E402
|
|
import row_assembly # noqa: E402
|
|
import route_metadata # noqa: E402
|
|
import row_camera # noqa: E402
|
|
import row_category_route # noqa: E402
|
|
import row_expression # noqa: E402
|
|
import row_generation # noqa: E402
|
|
import row_item # noqa: E402
|
|
import row_location # noqa: E402
|
|
import row_pools # noqa: E402
|
|
import row_prompt_axes # noqa: E402
|
|
import row_rendering # noqa: E402
|
|
import row_role_graph # noqa: E402
|
|
import row_route_metadata # noqa: E402
|
|
import row_subject_route # noqa: E402
|
|
import scene_camera_adapters # noqa: E402
|
|
import server_routes # noqa: E402
|
|
import sdxl_formatter # noqa: E402
|
|
import sdxl_format_route # noqa: E402
|
|
import sdxl_presets # noqa: E402
|
|
import sdxl_tag_policy # noqa: E402
|
|
import sdxl_tag_routes # noqa: E402
|
|
import seed_config # noqa: E402
|
|
import krea_pov # noqa: E402
|
|
import subject_context # noqa: E402
|
|
from tools import prompt_route_simulation # noqa: E402
|
|
|
|
|
|
Trigger = "sxcppnl7"
|
|
SdxlTrigger = "mythp0rt"
|
|
|
|
|
|
@dataclass
|
|
class SmokeReport:
|
|
passed: list[str] = field(default_factory=list)
|
|
failed: list[str] = field(default_factory=list)
|
|
|
|
def ok(self, name: str) -> None:
|
|
self.passed.append(name)
|
|
print(f"PASS {name}")
|
|
|
|
def fail(self, name: str, message: str) -> None:
|
|
detail = f"{name}: {message}"
|
|
self.failed.append(detail)
|
|
print(f"FAIL {detail}")
|
|
|
|
|
|
def _clean_key(value: str) -> str:
|
|
return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip()
|
|
|
|
|
|
def _json(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
|
|
|
|
def _expect(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def _expect_text(name: str, value: Any, min_len: int = 8) -> str:
|
|
text = str(value or "").strip()
|
|
_expect(len(text) >= min_len, f"{name} is empty or too short")
|
|
_expect("None" not in text, f"{name} leaked None")
|
|
_expect(" " not in text, f"{name} has repeated spaces")
|
|
_expect(" ," not in text and " ." not in text, f"{name} has bad punctuation spacing")
|
|
return text
|
|
|
|
|
|
def _expect_no_duplicate_comma_items(name: str, value: Any) -> None:
|
|
items = [_clean_key(part) for part in str(value or "").split(",")]
|
|
items = [part for part in items if part]
|
|
duplicates = sorted({part for part in items if items.count(part) > 1})
|
|
_expect(not duplicates, f"{name} has duplicate comma items: {duplicates[:5]}")
|
|
|
|
|
|
def _expect_no_softcore_noise(name: str, value: Any) -> None:
|
|
text = str(value or "").lower()
|
|
noisy = (
|
|
"the image focuses",
|
|
"softcore version",
|
|
"non-explicit teaser setup",
|
|
"no sex act",
|
|
"genital contact",
|
|
"keep the softcore version",
|
|
"focused on woman a alone",
|
|
)
|
|
found = [phrase for phrase in noisy if phrase in text]
|
|
_expect(not found, f"{name} has softcore prompt noise: {found}")
|
|
|
|
|
|
def _trigger_count(text: str, trigger: str) -> int:
|
|
return len(re.findall(rf"(?<![a-z0-9_]){re.escape(trigger)}(?![a-z0-9_])", text, flags=re.IGNORECASE))
|
|
|
|
|
|
def _expect_trigger_once(name: str, value: Any, trigger: str) -> None:
|
|
text = str(value or "")
|
|
count = _trigger_count(text, trigger)
|
|
_expect(count == 1, f"{name} should contain trigger {trigger!r} exactly once, got {count}")
|
|
|
|
|
|
def _expect_row_base(row: dict[str, Any], name: str) -> None:
|
|
_expect(isinstance(row, dict), f"{name} did not return a metadata row")
|
|
_expect_text(f"{name}.prompt", row.get("prompt"), 20)
|
|
_expect_text(f"{name}.negative_prompt", row.get("negative_prompt"), 8)
|
|
_expect_no_duplicate_comma_items(f"{name}.negative_prompt", row.get("negative_prompt"))
|
|
_expect(json.loads(_json(row)) == row, f"{name} is not JSON-stable")
|
|
|
|
|
|
def _expect_custom_row(row: dict[str, Any], name: str) -> None:
|
|
_expect_row_base(row, name)
|
|
_expect(row.get("source") == "json_category", f"{name}.source should be json_category")
|
|
_expect_text(f"{name}.item", row.get("item"), 8)
|
|
_expect_text(f"{name}.scene_text", row.get("scene_text"), 8)
|
|
_expect_text(f"{name}.composition", row.get("composition"), 8)
|
|
_expect_text(f"{name}.role_graph", row.get("source_role_graph") or row.get("role_graph"), 8)
|
|
_expect(isinstance(row.get("item_axis_values"), dict), f"{name}.item_axis_values missing")
|
|
_expect(isinstance(row.get("formatter_hints"), dict), f"{name}.formatter_hints missing")
|
|
|
|
|
|
def _expect_formatter_outputs(row: dict[str, Any], name: str, *, target: str = "auto") -> None:
|
|
metadata = _json(row)
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=metadata, target=target)
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata: {krea.get('method')}")
|
|
_expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 20)
|
|
_expect_no_duplicate_comma_items(f"{name}.krea_negative", krea.get("negative_prompt"))
|
|
|
|
sdxl = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=metadata,
|
|
target=target,
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
_expect("metadata" in sdxl.get("method", ""), f"{name}.sdxl did not use metadata: {sdxl.get('method')}")
|
|
_expect_text(f"{name}.sdxl_prompt", sdxl.get("sdxl_prompt"), 20)
|
|
_expect_trigger_once(f"{name}.sdxl_prompt", sdxl.get("sdxl_prompt"), SdxlTrigger)
|
|
_expect_no_duplicate_comma_items(f"{name}.sdxl_negative", sdxl.get("negative_prompt"))
|
|
|
|
caption, method = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=metadata,
|
|
target=target,
|
|
trigger=Trigger,
|
|
include_trigger=True,
|
|
)
|
|
_expect("metadata" in method, f"{name}.caption did not use metadata: {method}")
|
|
_expect_text(f"{name}.caption", caption, 20)
|
|
_expect_trigger_once(f"{name}.caption", caption, Trigger)
|
|
|
|
|
|
def _expect_krea_normal_route_parity(row: dict[str, Any], name: str, method: str) -> None:
|
|
typed_route = krea_normal_formatter.format_normal_row_result(
|
|
krea_formatter._krea_normal_row_request_from_row(row, "balanced", "preserve"),
|
|
krea_formatter._krea_normal_row_dependencies(),
|
|
)
|
|
legacy_route = krea_formatter._normal_row_to_krea(row, "balanced", "preserve")
|
|
_expect(
|
|
typed_route.as_tuple() == legacy_route,
|
|
f"{name} typed Krea normal formatter route should match legacy wrapper output",
|
|
)
|
|
_expect(typed_route.method == method, f"{name} typed Krea normal formatter method changed")
|
|
_expect_text(f"{name}.typed_krea_prompt", typed_route.prompt, 20)
|
|
|
|
|
|
def _character_cast(*, pov_man: bool = False) -> str:
|
|
cast = pb.build_character_slot_json(
|
|
subject_type="woman",
|
|
label="A",
|
|
age="25-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="slim",
|
|
descriptor_detail="full",
|
|
expression_intensity=0.65,
|
|
softcore_expression_intensity=0.45,
|
|
hardcore_expression_intensity=0.85,
|
|
)["character_cast"]
|
|
return pb.build_character_slot_json(
|
|
subject_type="man",
|
|
label="A",
|
|
age="40-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="average",
|
|
descriptor_detail="compact",
|
|
expression_intensity=0.55,
|
|
softcore_expression_intensity=0.35,
|
|
hardcore_expression_intensity=0.75,
|
|
presence_mode="pov" if pov_man else "visible",
|
|
character_cast=cast,
|
|
)["character_cast"]
|
|
|
|
|
|
def _character_cast_two_men(*, pov_first_man: bool = False) -> str:
|
|
cast = _character_cast(pov_man=pov_first_man)
|
|
return pb.build_character_slot_json(
|
|
subject_type="man",
|
|
label="B",
|
|
age="41-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="average",
|
|
descriptor_detail="compact",
|
|
expression_intensity=0.55,
|
|
softcore_expression_intensity=0.35,
|
|
hardcore_expression_intensity=0.75,
|
|
character_cast=cast,
|
|
)["character_cast"]
|
|
|
|
|
|
def _character_cast_subjects(subjects: list[str] | tuple[str, ...]) -> str:
|
|
cast = ""
|
|
counts = {"woman": 0, "man": 0}
|
|
for subject in subjects:
|
|
subject = str(subject)
|
|
counts[subject] += 1
|
|
label = chr(ord("A") + counts[subject] - 1)
|
|
cast = pb.build_character_slot_json(
|
|
subject_type=subject,
|
|
label=label,
|
|
age="25-year-old adult" if subject == "woman" else "40-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="slim" if subject == "woman" else "average",
|
|
descriptor_detail="compact",
|
|
expression_intensity=0.55,
|
|
softcore_expression_intensity=0.35,
|
|
hardcore_expression_intensity=0.75,
|
|
character_cast=cast,
|
|
)["character_cast"]
|
|
return cast
|
|
|
|
|
|
def _exact_subcategory_selector(category: dict[str, Any], subcategory: dict[str, Any]) -> str:
|
|
return category_library.exact_subcategory_selector(category, subcategory)
|
|
|
|
|
|
def _matrix_cast_for_route(category: dict[str, Any], subcategory: dict[str, Any]) -> tuple[int, int, str]:
|
|
subject_type = str(subcategory.get("subject_type") or category.get("subject_type") or "woman")
|
|
if subject_type == "woman":
|
|
return 1, 0, ""
|
|
if subject_type == "man":
|
|
return 0, 1, ""
|
|
if subject_type == "couple":
|
|
return 1, 1, ""
|
|
|
|
min_people = int(subcategory.get("min_people") or 0)
|
|
women_count = int(subcategory.get("min_women") or 0)
|
|
men_count = int(subcategory.get("min_men") or 0)
|
|
if min_people <= 1 and not women_count and not men_count:
|
|
women_count = 1
|
|
elif min_people >= 4:
|
|
women_count = max(women_count, 2)
|
|
men_count = max(men_count, min_people - women_count)
|
|
elif min_people >= 3:
|
|
women_count = max(women_count, 1)
|
|
men_count = max(men_count, min_people - women_count)
|
|
elif min_people >= 2:
|
|
women_count = max(women_count, 1)
|
|
men_count = max(men_count, min_people - women_count)
|
|
required_total = max(1, min_people)
|
|
if women_count + men_count < required_total:
|
|
men_count += required_total - (women_count + men_count)
|
|
if women_count == 0 and men_count == 0:
|
|
women_count = 1
|
|
|
|
cast = _character_cast_subjects(["woman"] * women_count + ["man"] * men_count)
|
|
return women_count, men_count, cast
|
|
|
|
|
|
def _action_filter(focus: str, hardcore_position_config: str | dict[str, Any] | None = "") -> str:
|
|
kwargs = {
|
|
"allow_toys": False,
|
|
"allow_double": False,
|
|
"allow_penetration": focus in ("penetration_only", "keep_pool"),
|
|
"allow_foreplay": focus in ("foreplay_only", "keep_pool"),
|
|
"allow_interaction": focus in ("interaction_only", "keep_pool"),
|
|
"allow_manual": focus in ("manual_only", "keep_pool"),
|
|
"allow_oral": focus in ("oral_only", "keep_pool"),
|
|
"allow_outercourse": focus in ("outercourse_only", "keep_pool"),
|
|
"allow_anal": focus in ("anal_only", "keep_pool"),
|
|
"allow_climax": focus in ("climax_only", "keep_pool"),
|
|
}
|
|
return pb.build_hardcore_action_filter_json(
|
|
hardcore_position_config=hardcore_position_config,
|
|
focus=focus,
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
def _broad_hardcore_filter() -> str:
|
|
return pb.build_hardcore_action_filter_json(
|
|
focus="keep_pool",
|
|
allow_toys=True,
|
|
allow_double=True,
|
|
allow_penetration=True,
|
|
allow_foreplay=True,
|
|
allow_interaction=True,
|
|
allow_manual=True,
|
|
allow_oral=True,
|
|
allow_outercourse=True,
|
|
allow_anal=True,
|
|
allow_climax=True,
|
|
)
|
|
|
|
|
|
def _position_filter(focus: str, family: str, positions: list[str] | tuple[str, ...] | str) -> str:
|
|
position_config = pb.build_hardcore_position_pool_json(
|
|
combine_mode="replace",
|
|
family=family,
|
|
selected_positions=positions,
|
|
)
|
|
return _action_filter(focus, position_config)
|
|
|
|
|
|
def _anal_double_filter(positions: list[str] | tuple[str, ...] | str) -> str:
|
|
position_config = pb.build_hardcore_position_pool_json(
|
|
combine_mode="replace",
|
|
family="anal",
|
|
selected_positions=positions,
|
|
)
|
|
return pb.build_hardcore_action_filter_json(
|
|
hardcore_position_config=position_config,
|
|
focus="anal_only",
|
|
allow_toys=True,
|
|
allow_double=True,
|
|
allow_penetration=True,
|
|
allow_foreplay=False,
|
|
allow_interaction=False,
|
|
allow_manual=False,
|
|
allow_oral=False,
|
|
allow_outercourse=False,
|
|
allow_anal=True,
|
|
allow_climax=False,
|
|
)
|
|
|
|
|
|
def _coworking_location_config() -> str:
|
|
return pb.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations=(
|
|
"coworking_smoke: coworking lounge with tall windows, warm desks, "
|
|
"laptop tables, glass partition seams, repeated desk rows, plants, "
|
|
"and soft shared-office depth"
|
|
),
|
|
)
|
|
|
|
|
|
def _classical_library_theme_configs() -> tuple[str, str]:
|
|
return _thematic_location_configs("classical_library")
|
|
|
|
|
|
def _thematic_location_configs(theme: str) -> tuple[str, str]:
|
|
location_config, composition_config, _summary = pb.build_thematic_location_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
theme=theme,
|
|
)
|
|
return location_config, composition_config
|
|
|
|
|
|
def _orbit_camera(
|
|
*,
|
|
horizontal_angle: int,
|
|
vertical_angle: int,
|
|
zoom: float,
|
|
subject_focus: str = "auto",
|
|
camera_detail: str = "compact",
|
|
) -> str:
|
|
return pb.build_camera_orbit_config_json(
|
|
enabled=True,
|
|
camera_mode="standard",
|
|
horizontal_angle=horizontal_angle,
|
|
vertical_angle=vertical_angle,
|
|
zoom=zoom,
|
|
framing="from_zoom",
|
|
subject_focus=subject_focus,
|
|
lens="auto",
|
|
orientation="auto",
|
|
phone_visibility="auto",
|
|
priority="strong",
|
|
camera_detail=camera_detail,
|
|
include_degrees=True,
|
|
)
|
|
|
|
|
|
def _prompt_row(
|
|
*,
|
|
name: str,
|
|
category: str,
|
|
subcategory: str,
|
|
seed: int,
|
|
seed_config: str | dict[str, Any] | None = None,
|
|
character_cast: str = "",
|
|
women_count: int = 1,
|
|
men_count: int = 1,
|
|
hardcore_position_config: str = "",
|
|
camera_config: str | dict[str, Any] | None = "",
|
|
location_config: str | dict[str, Any] | None = "",
|
|
composition_config: str | dict[str, Any] | None = "",
|
|
) -> dict[str, Any]:
|
|
row = pb.build_prompt(
|
|
category=category,
|
|
subcategory=subcategory,
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=seed,
|
|
clothing="random",
|
|
ethnicity="any",
|
|
poses="random",
|
|
backside_bias=0.35,
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=0.5,
|
|
standard_pose_ratio=0.5,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="",
|
|
extra_negative="",
|
|
seed_config=seed_config,
|
|
character_cast=character_cast,
|
|
women_count=women_count,
|
|
men_count=men_count,
|
|
expression_enabled=True,
|
|
expression_intensity=0.6,
|
|
hardcore_position_config=hardcore_position_config,
|
|
camera_config=camera_config,
|
|
location_config=location_config,
|
|
composition_config=composition_config,
|
|
)
|
|
_expect_row_base(row, name)
|
|
return row
|
|
|
|
|
|
def _fixture_hardcore_row(**overrides: Any) -> dict[str, Any]:
|
|
row: dict[str, Any] = {
|
|
"source": "json_category",
|
|
"prompt": "Fixture explicit adult prompt for metadata route.",
|
|
"caption": "fixture caption",
|
|
"negative_prompt": "low quality, bad anatomy",
|
|
"main_category": "Hardcore sexual poses",
|
|
"subcategory": "Penetrative sex",
|
|
"category_slug": "hardcore_sexual_poses",
|
|
"subcategory_slug": "penetrative_sex",
|
|
"subject_type": "configured_cast",
|
|
"subject_phrase": "1 adult woman and 1 adult man",
|
|
"cast_summary": "1 woman, 1 man",
|
|
"cast_descriptor_text": (
|
|
"Woman A: 25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes; "
|
|
"Man A: 40-year-old adult man, average figure, tan skin, dark hair"
|
|
),
|
|
"cast_descriptors": [
|
|
"Woman A: 25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes",
|
|
"Man A: 40-year-old adult man, average figure, tan skin, dark hair",
|
|
],
|
|
"women_count": 1,
|
|
"men_count": 1,
|
|
"person_count": 2,
|
|
"item": (
|
|
"missionary position while full-body penetrative sex, hands gripping the ass, "
|
|
"mouth close to the ear, and explicit genital contact visible"
|
|
),
|
|
"custom_item": "Penetrative sex",
|
|
"item_label": "Sexual pose",
|
|
"item_axis_values": {
|
|
"position": "missionary position",
|
|
"penetration_act": "full-body penetrative sex",
|
|
"mouth_detail": "mouth close to the ear",
|
|
},
|
|
"item_template_metadata": {},
|
|
"formatter_hints": {},
|
|
"scene_text": "private studio room with warm light",
|
|
"scene_kind": "explicit adult sex scene",
|
|
"pose": "configured explicit pose",
|
|
"composition": "front-facing full-body frame",
|
|
"source_composition": "front-facing full-body frame",
|
|
"role_graph": (
|
|
"Woman A lies on her back with legs open around Man A's hips while Man A is above her between her thighs; "
|
|
"Man A's hips press close and Man A's penis thrusts into her pussy."
|
|
),
|
|
"source_role_graph": (
|
|
"Woman A lies on her back with legs open around Man A's hips while Man A is above her between her thighs; "
|
|
"Man A's hips press close and Man A's penis thrusts into her pussy."
|
|
),
|
|
"expression": "focused adult expression",
|
|
"action_family": "penetration",
|
|
"position_family": "penetrative",
|
|
"position_key": "missionary",
|
|
"position_keys": ["missionary"],
|
|
}
|
|
row.update(overrides)
|
|
return row
|
|
|
|
|
|
def smoke_builtin_single() -> None:
|
|
row = _prompt_row(name="builtin_single_woman", category="woman", subcategory="random", seed=1001, men_count=0)
|
|
_expect(row.get("source") == "built_in_generator", "builtin row should come from built-in generator")
|
|
_expect(row.get("subject_type") == "woman", "builtin single row lost normalized subject_type")
|
|
_expect(row.get("subject_phrase") == "woman", "builtin single row lost normalized subject_phrase")
|
|
_expect(row.get("women_count") == 1 and row.get("men_count") == 0, "builtin single row lost normalized cast counts")
|
|
_expect(row.get("person_count") == 1, "builtin single row lost normalized person count")
|
|
_expect_text("builtin_single_woman.scene_text", row.get("scene_text"), 12)
|
|
_expect(row.get("scene_slug") == row.get("scene"), "builtin single row lost legacy scene slug metadata")
|
|
_expect(row.get("scene_entry", {}).get("slug") == row.get("scene"), "builtin single row lost scene_entry slug")
|
|
item = _expect_text("builtin_single_woman.item", row.get("item"), 8)
|
|
pose = _expect_text("builtin_single_woman.pose", row.get("pose"), 8)
|
|
body_phrase = _expect_text("builtin_single_woman.body_phrase", row.get("body_phrase"), 8)
|
|
skin = _expect_text("builtin_single_woman.skin", row.get("skin"), 8)
|
|
hair = _expect_text("builtin_single_woman.hair", row.get("hair"), 5)
|
|
eyes = _expect_text("builtin_single_woman.eyes", row.get("eyes"), 4)
|
|
_expect(row.get("item_label") == "Clothing", "builtin single row lost item label")
|
|
_expect(row.get("clothing") == item, "builtin single row did not mirror clothing into item metadata")
|
|
_expect("fashion editorial styling" not in item.lower(), "builtin single item kept generic styling suffix")
|
|
_expect("cast_summary" not in row, "builtin single row should not masquerade as configured cast")
|
|
_expect_trigger_once("builtin_single_woman.prompt", row.get("prompt"), Trigger)
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
_expect(row.get("scene_text") in str(krea.get("krea_prompt", "")), "builtin single Krea route used scene slug instead of scene text")
|
|
sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=_json(row), target="single", trigger=SdxlTrigger, prepend_trigger=True)
|
|
_expect("1woman" in str(sdxl.get("sdxl_prompt", "")).lower(), "builtin single SDXL route lost normalized woman count")
|
|
sdxl_scene_text = str(sdxl.get("sdxl_prompt", "")).lower()
|
|
scene_words = re.findall(r"[a-z0-9]+", str(row.get("scene_text", "")).split(",", 1)[0].lower())
|
|
scene_anchor = " ".join(scene_words[:2])
|
|
_expect(scene_anchor in sdxl_scene_text, "builtin single SDXL route lost readable scene text")
|
|
_expect(str(row.get("scene_slug", "")).lower() not in sdxl_scene_text, "builtin single SDXL route leaked raw scene slug")
|
|
caption, caption_method = caption_naturalizer.naturalize_caption("", metadata_json=_json(row), target="single", trigger=Trigger, include_trigger=True)
|
|
_expect(caption_method.endswith("metadata(single)"), "builtin single caption route did not use single metadata branch")
|
|
_expect("woman" in caption.lower(), "builtin single caption route lost normalized subject")
|
|
_expect(row.get("scene_text") in caption, "builtin single caption route used scene slug instead of scene text")
|
|
|
|
metadata_only = dict(row)
|
|
metadata_only["prompt"] = ""
|
|
metadata_only["caption"] = ""
|
|
krea_metadata = krea_formatter.format_krea2_prompt("", metadata_json=_json(metadata_only), target="single")
|
|
sdxl_metadata = sdxl_formatter.format_sdxl_prompt("", metadata_json=_json(metadata_only), target="single", trigger=SdxlTrigger, prepend_trigger=True)
|
|
caption_metadata, caption_metadata_method = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=_json(metadata_only),
|
|
target="single",
|
|
trigger=Trigger,
|
|
include_trigger=True,
|
|
)
|
|
_expect(item in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost explicit item")
|
|
_expect(pose in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost explicit pose")
|
|
_expect(body_phrase in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost body phrase")
|
|
_expect(skin in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost skin")
|
|
_expect(hair in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost hair")
|
|
_expect(eyes in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost eyes")
|
|
item_anchor = " ".join(re.findall(r"[a-z0-9]+", item.lower())[:3])
|
|
pose_anchor = " ".join(re.findall(r"[a-z0-9]+", pose.lower())[:4])
|
|
sdxl_metadata_prompt = str(sdxl_metadata.get("sdxl_prompt", "")).lower()
|
|
_expect(item_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit item")
|
|
_expect(pose_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit pose")
|
|
for body_tag in sdxl_tag_policy.split_tag_text(body_phrase):
|
|
_expect(body_tag.lower() in sdxl_metadata_prompt, f"SDXL metadata-only built-in route lost body tag: {body_tag}")
|
|
_expect(skin.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost skin")
|
|
_expect(hair.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost hair")
|
|
_expect(eyes.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost eyes")
|
|
_expect(caption_metadata_method.endswith("metadata(single)"), "Caption metadata-only built-in route did not use single metadata branch")
|
|
_expect(item in caption_metadata and pose in caption_metadata, "Caption metadata-only built-in route lost explicit item or pose")
|
|
_expect(body_phrase in caption_metadata and skin in caption_metadata, "Caption metadata-only built-in route lost appearance")
|
|
_expect(hair in caption_metadata and eyes in caption_metadata, "Caption metadata-only built-in route lost hair or eyes")
|
|
_expect_formatter_outputs(row, "builtin_single_woman", target="single")
|
|
|
|
|
|
def smoke_builtin_couple_metadata() -> None:
|
|
row = _prompt_row(name="builtin_couple", category="couple", subcategory="random", seed=1003, men_count=0)
|
|
_expect(row.get("source") == "built_in_generator", "builtin couple row should come from built-in generator")
|
|
_expect(row.get("subject_type") == "couple", "builtin couple row lost normalized subject_type")
|
|
_expect(row.get("women_count") + row.get("men_count") == row.get("person_count"), "builtin couple row lost count consistency")
|
|
_expect(row.get("person_count") == 2, "builtin couple row lost normalized person count")
|
|
subject = _expect_text("builtin_couple.subject_phrase", row.get("subject_phrase"), 5)
|
|
age = _expect_text("builtin_couple.age_band", row.get("age_band"), 8)
|
|
body = _expect_text("builtin_couple.body_type", row.get("body_type"), 5)
|
|
scene = _expect_text("builtin_couple.scene_text", row.get("scene_text"), 12)
|
|
item = _expect_text("builtin_couple.item", row.get("item"), 8)
|
|
pose = _expect_text("builtin_couple.pose", row.get("pose"), 8)
|
|
expression = _expect_text("builtin_couple.expression", row.get("expression"), 8)
|
|
composition = _expect_text("builtin_couple.composition", row.get("composition"), 8)
|
|
|
|
metadata_only = dict(row)
|
|
metadata_only["prompt"] = ""
|
|
metadata_only["caption"] = ""
|
|
metadata = _json(metadata_only)
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=metadata, target="auto")
|
|
sdxl = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=metadata,
|
|
target="auto",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
caption, caption_method = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=metadata,
|
|
target="auto",
|
|
trigger=Trigger,
|
|
include_trigger=True,
|
|
)
|
|
|
|
clean_age = caption_text_policy.clean_age_phrase(age)
|
|
krea_prompt = str(krea.get("krea_prompt", ""))
|
|
sdxl_prompt = str(sdxl.get("sdxl_prompt", "")).lower()
|
|
_expect("metadata(couple)" in krea.get("method", ""), "Krea built-in couple route did not use metadata")
|
|
_expect(subject.capitalize() in krea_prompt and clean_age in krea_prompt, "Krea built-in couple route lost subject or age")
|
|
_expect("all visibly adult" not in krea_prompt.lower(), "Krea built-in couple route kept redundant adult noise")
|
|
_expect("Age detail:" not in krea_prompt, "Krea built-in couple route kept age helper label")
|
|
for value in (body, scene, item, pose, expression, composition):
|
|
_expect(value in krea_prompt, f"Krea built-in couple route lost metadata value: {value}")
|
|
|
|
_expect("2women" in sdxl_prompt or "1woman, 1man" in sdxl_prompt or "2men" in sdxl_prompt, "SDXL built-in couple route lost count tags")
|
|
for value in (body, scene, item, pose, expression, composition):
|
|
for tag in sdxl_tag_policy.split_tag_text(value):
|
|
_expect(tag.lower() in sdxl_prompt, f"SDXL built-in couple route lost metadata tag: {tag}")
|
|
|
|
_expect(caption_method.endswith("metadata(couple)"), "Caption built-in couple route did not use metadata")
|
|
_expect(clean_age in caption and scene in caption, "Caption built-in couple route lost age or scene")
|
|
_expect("The age detail is" not in caption, "Caption built-in couple route kept age helper sentence")
|
|
for value in (body, item, pose, expression, composition):
|
|
_expect(value in caption, f"Caption built-in couple route lost metadata value: {value}")
|
|
|
|
|
|
def smoke_camera_scene_single() -> None:
|
|
row = _prompt_row(
|
|
name="camera_scene_single",
|
|
category="woman",
|
|
subcategory="random",
|
|
seed=1051,
|
|
men_count=0,
|
|
camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=-30,
|
|
zoom=5.0,
|
|
subject_focus="environment",
|
|
),
|
|
location_config=_coworking_location_config(),
|
|
)
|
|
scene_directive = _expect_text("camera_scene_single.camera_scene_directive", row.get("camera_scene_directive"), 40)
|
|
camera_directive = _expect_text("camera_scene_single.camera_directive", row.get("camera_directive"), 20)
|
|
_expect("Coworking camera layout" in scene_directive, "single camera-scene adapter did not identify coworking layout")
|
|
_expect("front-right quarter view" in scene_directive, "single camera scene missed orbit direction")
|
|
_expect("low-angle shot" in scene_directive, "single camera scene missed orbit elevation")
|
|
_expect("45-degree front-right quarter view" in camera_directive, "single camera directive missed custom orbit prompt")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
prompt = krea.get("krea_prompt") or ""
|
|
_expect("Coworking camera layout" in prompt, "Krea single prompt lost camera-scene directive")
|
|
_expect("45-degree front-right quarter view" in prompt, "Krea single prompt lost camera directive")
|
|
_expect_formatter_outputs(row, "camera_scene_single", target="single")
|
|
|
|
custom_location = pb.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations=json.dumps(
|
|
{
|
|
"slug": "greenhouse_suite",
|
|
"prompt": "private room with soft daylight",
|
|
"camera_profile": {
|
|
"key": "glass_conservatory",
|
|
"family": "greenhouse",
|
|
"layout_label": "Glass conservatory camera layout",
|
|
"place": "glass conservatory",
|
|
"foreground": "plant shelf edge, fern leaves, and iron table corner",
|
|
"midground": "glass panes, iron ribs, and potted palms",
|
|
"background": "hanging vines, greenhouse windows, and layered plant depth",
|
|
"detail_label": "conservatory details",
|
|
"composition": {
|
|
"woman": "glass conservatory frame with the woman beside fern leaves and greenhouse depth behind her",
|
|
"default": "glass conservatory frame with the subjects beside fern leaves and greenhouse depth behind them",
|
|
},
|
|
},
|
|
},
|
|
sort_keys=True,
|
|
),
|
|
)
|
|
custom_composition = pb.build_composition_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_compositions=json.dumps({"prompt": "polished mirror view with bag and shoes visible"}, sort_keys=True),
|
|
)
|
|
custom_row = _prompt_row(
|
|
name="camera_scene_custom_inline_profile",
|
|
category="woman",
|
|
subcategory="random",
|
|
seed=1061,
|
|
men_count=0,
|
|
camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=30,
|
|
zoom=5.0,
|
|
subject_focus="environment",
|
|
),
|
|
location_config=custom_location,
|
|
composition_config=custom_composition,
|
|
)
|
|
custom_scene = _expect_text(
|
|
"camera_scene_custom_inline_profile.camera_scene_directive",
|
|
custom_row.get("camera_scene_directive"),
|
|
40,
|
|
)
|
|
custom_composition_text = _expect_text(
|
|
"camera_scene_custom_inline_profile.composition",
|
|
custom_row.get("composition"),
|
|
20,
|
|
)
|
|
_expect("Glass conservatory camera layout" in custom_scene, "custom Location Pool JSON camera profile did not drive scene layout")
|
|
_expect(custom_row.get("scene_camera_profile_key") == "glass_conservatory", "custom Location Pool JSON profile key was not exposed")
|
|
_expect(custom_row.get("scene_entry", {}).get("camera_profile", {}).get("family") == "greenhouse", "custom Location Pool JSON profile metadata was not preserved")
|
|
_expect("glass conservatory" in custom_composition_text.lower(), "custom Location Pool JSON profile did not clean composition")
|
|
_expect("bag" not in custom_composition_text.lower() and "shoes" not in custom_composition_text.lower(), "custom inline profile composition leaked unrelated props")
|
|
|
|
|
|
def smoke_scene_camera_adapter_pov_profile_policy() -> None:
|
|
parsed_camera = {
|
|
"camera_mode": "standard",
|
|
"camera_detail": "compact",
|
|
"orbit_direction": "front-right quarter view",
|
|
"orbit_elevation_label": "elevated shot",
|
|
"orbit_distance_label": "medium shot",
|
|
"custom_camera_prompt": "front-right quarter view, elevated shot, medium shot",
|
|
}
|
|
for profile in scene_camera_adapters.SCENE_CAMERA_PROFILES:
|
|
key = str(profile.get("key") or "")
|
|
foreground = str(profile.get("foreground") or "")
|
|
non_pov = _expect_text(
|
|
f"scene_camera_adapter_pov_profile_policy.{key}.non_pov",
|
|
scene_camera_adapters.scene_camera_directive(
|
|
"",
|
|
parsed_camera,
|
|
pov_labels=[],
|
|
subject_kind="couple",
|
|
profile_key=key,
|
|
),
|
|
40,
|
|
)
|
|
pov = _expect_text(
|
|
f"scene_camera_adapter_pov_profile_policy.{key}.pov",
|
|
scene_camera_adapters.scene_camera_directive(
|
|
"",
|
|
parsed_camera,
|
|
pov_labels=["Man A"],
|
|
subject_kind="couple",
|
|
profile_key=key,
|
|
),
|
|
40,
|
|
)
|
|
_expect(foreground and foreground in non_pov, f"{key} non-POV scene directive lost profile foreground anchor")
|
|
_expect(foreground not in pov, f"{key} POV scene directive reused profile foreground anchor as viewer-side text")
|
|
_expect("from POV" in pov, f"{key} POV scene directive lost POV marker")
|
|
_expect(
|
|
"lower foreground is reserved for POV body or hand cues" in pov,
|
|
f"{key} POV scene directive lost lower-foreground body-cue reservation",
|
|
)
|
|
|
|
|
|
def smoke_row_camera_policy() -> None:
|
|
row = {
|
|
"prompt": "A generated adult prompt. Composition: vertical office-lobby walking composition. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, office-lobby walking composition, illustration",
|
|
"scene_text": "coworking lounge with tall windows, warm desks, and a polished outfit-check angle",
|
|
"composition": "office-lobby walking composition",
|
|
"subject_type": "configured_cast",
|
|
"women_count": 1,
|
|
"men_count": 1,
|
|
"pov_character_labels": ["Man A"],
|
|
}
|
|
updated = row_camera.apply_camera_config(
|
|
row,
|
|
_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
|
compact_labels=pb.CAMERA_COMPACT_LABELS,
|
|
)
|
|
_expect(updated.get("camera_directive") == "", "POV row camera policy should suppress normal camera directive")
|
|
scene_directive = _expect_text("row_camera_policy.camera_scene_directive", updated.get("camera_scene_directive"), 40)
|
|
_expect("Coworking camera layout from POV" in scene_directive, "row camera policy missed POV coworking layout")
|
|
_expect("first-person spatial geometry" in scene_directive, "row camera policy lost POV geometry instruction")
|
|
_expect("Camera:" not in updated.get("prompt", ""), "row camera policy should not add normal Camera label")
|
|
_expect("45-degree front-right quarter view" not in updated.get("caption", ""), "POV row camera policy should not append camera caption")
|
|
_expect(
|
|
"coworking lounge frame with the couple near a desk edge" in updated.get("composition", ""),
|
|
"row camera policy did not adapt coworking composition for couple rows",
|
|
)
|
|
library_row = {
|
|
"prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration",
|
|
"scene_text": "grand classical library hall with towering dark-wood bookshelves, carved columns, rolling ladders, marble floor, and warm brass lamps",
|
|
"composition": "polished mirror view with bag and shoes visible",
|
|
"subject_type": "woman",
|
|
"women_count": 1,
|
|
"men_count": 0,
|
|
}
|
|
updated_library = row_camera.apply_camera_config(
|
|
library_row,
|
|
_orbit_camera(horizontal_angle=315, vertical_angle=0, zoom=5.0),
|
|
compact_labels=pb.CAMERA_COMPACT_LABELS,
|
|
)
|
|
library_scene = _expect_text("row_camera_policy.library_scene", updated_library.get("camera_scene_directive"), 40)
|
|
library_composition = _expect_text("row_camera_policy.library_composition", updated_library.get("composition"), 20)
|
|
_expect("Library camera layout" in library_scene, "row camera policy missed library layout")
|
|
_expect("front-left quarter view" in library_scene, "row camera library layout missed orbit direction")
|
|
_expect("bookshelf" in library_scene.lower() or "bookshelves" in library_scene.lower(), "row camera library layout missed shelf anchors")
|
|
_expect("bag" not in library_composition.lower(), "row camera library composition leaked bag wording")
|
|
_expect("shoes" not in library_composition.lower(), "row camera library composition leaked shoes wording")
|
|
_expect("library" in library_composition.lower(), "row camera library composition did not become location-aware")
|
|
semi_public_row = {
|
|
"prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration",
|
|
"scene_text": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove",
|
|
"scene_entry": {
|
|
"slug": "hotel_corridor_affair",
|
|
"prompt": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove",
|
|
"theme": "semi_public_affair",
|
|
},
|
|
"scene_theme": "semi_public_affair",
|
|
"composition": "polished mirror view with bag and shoes visible",
|
|
"subject_type": "configured_cast",
|
|
"women_count": 1,
|
|
"men_count": 1,
|
|
"pov_character_labels": ["Man A"],
|
|
}
|
|
updated_semi_public = row_camera.apply_camera_config(
|
|
semi_public_row,
|
|
_orbit_camera(horizontal_angle=180, vertical_angle=30, zoom=7.5),
|
|
compact_labels=pb.CAMERA_COMPACT_LABELS,
|
|
)
|
|
semi_public_scene = _expect_text("row_camera_policy.semi_public_scene", updated_semi_public.get("camera_scene_directive"), 40)
|
|
semi_public_composition = _expect_text(
|
|
"row_camera_policy.semi_public_composition",
|
|
updated_semi_public.get("composition"),
|
|
20,
|
|
)
|
|
_expect("Hotel corridor camera layout from POV" in semi_public_scene, "row camera semi-public scene did not use hotel corridor profile")
|
|
_expect("back view" in semi_public_scene, "row camera semi-public scene missed orbit direction")
|
|
_expect("first-person spatial geometry" in semi_public_scene, "row camera semi-public POV scene lost first-person geometry")
|
|
_expect(updated_semi_public.get("scene_camera_profile_key") == "hotel_corridor", "row camera semi-public scene did not expose text-matched profile key")
|
|
_expect("hotel corridor" in semi_public_composition.lower(), "row camera semi-public composition did not become location-aware")
|
|
_expect("bag" not in semi_public_composition.lower() and "shoes" not in semi_public_composition.lower(), "row camera semi-public composition leaked outfit-check props")
|
|
metadata_profile_row = {
|
|
"prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration",
|
|
"scene_text": "private themed room with neutral walls and warm lamps",
|
|
"scene_entry": {
|
|
"slug": "library_by_metadata",
|
|
"prompt": "private themed room with neutral walls and warm lamps",
|
|
"theme": "classical_library",
|
|
},
|
|
"scene_theme": "classical_library",
|
|
"composition": "polished mirror view with bag and shoes visible",
|
|
"subject_type": "woman",
|
|
"women_count": 1,
|
|
"men_count": 0,
|
|
}
|
|
updated_metadata_profile = row_camera.apply_camera_config(
|
|
metadata_profile_row,
|
|
_orbit_camera(horizontal_angle=315, vertical_angle=0, zoom=5.0),
|
|
compact_labels=pb.CAMERA_COMPACT_LABELS,
|
|
)
|
|
metadata_scene = _expect_text(
|
|
"row_camera_policy.metadata_scene",
|
|
updated_metadata_profile.get("camera_scene_directive"),
|
|
40,
|
|
)
|
|
_expect("Library camera layout" in metadata_scene, "row camera should prefer scene theme metadata over generic scene text")
|
|
_expect(
|
|
updated_metadata_profile.get("scene_camera_profile_key") == "classical_library",
|
|
"row camera should expose metadata-selected profile key",
|
|
)
|
|
_expect(
|
|
"library" in str(updated_metadata_profile.get("composition", "")).lower(),
|
|
"row camera metadata-selected profile did not clean composition",
|
|
)
|
|
explicit_profile_row = {
|
|
"prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration",
|
|
"scene_text": "coworking lounge with tall windows, warm desks, and glass partitions",
|
|
"scene_camera_profile_key": "classical_library",
|
|
"composition": "polished mirror view with bag and shoes visible",
|
|
"subject_type": "woman",
|
|
"women_count": 1,
|
|
"men_count": 0,
|
|
}
|
|
updated_explicit_profile = row_camera.apply_camera_config(
|
|
explicit_profile_row,
|
|
_orbit_camera(horizontal_angle=315, vertical_angle=0, zoom=5.0),
|
|
compact_labels=pb.CAMERA_COMPACT_LABELS,
|
|
)
|
|
_expect(
|
|
"Library camera layout" in str(updated_explicit_profile.get("camera_scene_directive", "")),
|
|
"explicit scene_camera_profile_key should override text-matched scene profile",
|
|
)
|
|
inline_profile_row = {
|
|
"prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration",
|
|
"scene_text": "private room with soft daylight",
|
|
"scene_entry": {
|
|
"slug": "greenhouse_room",
|
|
"prompt": "private room with soft daylight",
|
|
"camera_profile": {
|
|
"key": "glass_conservatory",
|
|
"family": "greenhouse",
|
|
"layout_label": "Glass conservatory camera layout",
|
|
"place": "glass conservatory",
|
|
"foreground": "plant shelf edge, fern leaves, and iron table corner",
|
|
"midground": "glass panes, iron ribs, and potted palms",
|
|
"background": "hanging vines, greenhouse windows, and layered plant depth",
|
|
"detail_label": "conservatory details",
|
|
"composition": {
|
|
"woman": "glass conservatory frame with the woman beside fern leaves and greenhouse depth behind her",
|
|
"default": "glass conservatory frame with the subjects beside fern leaves and greenhouse depth behind them",
|
|
},
|
|
},
|
|
},
|
|
"composition": "polished mirror view with bag and shoes visible",
|
|
"subject_type": "woman",
|
|
"women_count": 1,
|
|
"men_count": 0,
|
|
}
|
|
updated_inline_profile = row_camera.apply_camera_config(
|
|
inline_profile_row,
|
|
_orbit_camera(horizontal_angle=45, vertical_angle=30, zoom=5.0),
|
|
compact_labels=pb.CAMERA_COMPACT_LABELS,
|
|
)
|
|
inline_scene = _expect_text(
|
|
"row_camera_policy.inline_profile_scene",
|
|
updated_inline_profile.get("camera_scene_directive"),
|
|
40,
|
|
)
|
|
inline_composition = _expect_text(
|
|
"row_camera_policy.inline_profile_composition",
|
|
updated_inline_profile.get("composition"),
|
|
20,
|
|
)
|
|
inline_profile = updated_inline_profile.get("scene_camera_profile") if isinstance(updated_inline_profile.get("scene_camera_profile"), dict) else {}
|
|
_expect("Glass conservatory camera layout" in inline_scene, "inline scene camera profile did not drive camera layout")
|
|
_expect(updated_inline_profile.get("scene_camera_profile_key") == "glass_conservatory", "inline profile key was not exposed")
|
|
_expect(inline_profile.get("family") == "greenhouse", "inline profile family was not exposed")
|
|
_expect("glass conservatory" in inline_composition.lower(), "inline profile did not drive composition cleanup")
|
|
_expect("bag" not in inline_composition.lower() and "shoes" not in inline_composition.lower(), "inline profile composition leaked unrelated props")
|
|
|
|
beach_profile = scene_camera_adapters.scene_camera_profile(
|
|
"beach cafe table with woven chairs, linen shade, and ocean light in the background",
|
|
scene_entry={"slug": "beach_cafe_table"},
|
|
)
|
|
subway_profile = scene_camera_adapters.scene_camera_profile(
|
|
"clean subway platform with tiled walls, overhead lights, and a quiet selfie corner",
|
|
scene_entry={"slug": "subway_tile_selfie_corner"},
|
|
)
|
|
apartment_profile = scene_camera_adapters.scene_camera_profile(
|
|
"sunny apartment corner with bookshelves, a warm rug, and a phone on a small tripod",
|
|
scene_entry={"slug": "sunny_apartment_phone_tripod"},
|
|
)
|
|
_expect(not beach_profile, "scene camera resolver should not classify beach cafe as business cafe")
|
|
_expect(not subway_profile, "scene camera resolver should not classify subway tile as station lockers")
|
|
_expect(not apartment_profile, "scene camera resolver should not classify apartment bookshelves as classical library")
|
|
|
|
mirror_profile = scene_camera_adapters.scene_camera_profile(
|
|
"large bedroom mirror with the phone visible, bed behind the subject, and warm side lamps",
|
|
scene_entry={"slug": "large_bedroom_mirror_selfie"},
|
|
)
|
|
studio_profile = scene_camera_adapters.scene_camera_profile(
|
|
"dark private studio with glossy black floor reflections, rim light, and a phone tripod",
|
|
scene_entry={"slug": "black_latex_studio_floor"},
|
|
)
|
|
_expect(mirror_profile.get("key") == "mirror_room", "scene slug resolver missed mirror-room profile")
|
|
_expect(studio_profile.get("key") == "private_studio", "scene slug resolver missed private-studio profile")
|
|
|
|
|
|
def smoke_config_route_location_theme() -> None:
|
|
location_config, composition_config = _classical_library_theme_configs()
|
|
row = pb.build_prompt_from_configs(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3301,
|
|
category_config=pb.build_category_config_json("hardcore_pose", "Foreplay and teasing"),
|
|
cast_config=pb.build_cast_config_json("mixed_couple"),
|
|
generation_profile=pb.build_generation_profile_json(
|
|
profile="hardcore_intense",
|
|
trigger_policy="prepend_trigger",
|
|
),
|
|
filter_config=pb.build_ethnicity_list_json(
|
|
include_french_european=True,
|
|
strict_excludes=True,
|
|
)["filter_config"],
|
|
seed_config=pb.build_seed_lock_config_json(
|
|
base_seed=3301,
|
|
reroll_axis="pose",
|
|
reroll_seed=3302,
|
|
),
|
|
camera_config=_orbit_camera(
|
|
horizontal_angle=315,
|
|
vertical_angle=0,
|
|
zoom=5.0,
|
|
subject_focus="action",
|
|
),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("foreplay_only"),
|
|
location_config=location_config,
|
|
composition_config=composition_config,
|
|
)
|
|
_expect_custom_row(row, "config_route_location_theme")
|
|
_expect(row.get("subcategory") == "Foreplay and teasing", "config route did not preserve requested subcategory")
|
|
_expect(row.get("subject_type") == "configured_cast", "config route did not apply character cast")
|
|
scene = _expect_text("config_route_location_theme.scene_text", row.get("scene_text"), 20)
|
|
composition = _expect_text("config_route_location_theme.composition", row.get("composition"), 10)
|
|
camera = _expect_text("config_route_location_theme.camera_directive", row.get("camera_directive"), 20)
|
|
scene_directive = _expect_text("config_route_location_theme.camera_scene_directive", row.get("camera_scene_directive"), 40)
|
|
scene_profile = row.get("scene_camera_profile") if isinstance(row.get("scene_camera_profile"), dict) else {}
|
|
_expect("library" in scene.lower() or "bookshelves" in scene.lower(), "location theme did not drive scene")
|
|
_expect("books" in composition.lower() or "shelf" in composition.lower() or "library" in composition.lower(), "location theme did not drive composition")
|
|
_expect(row.get("location_theme") == "classical_library", "location theme did not survive into row metadata")
|
|
_expect(row.get("scene_theme") == "classical_library", "selected scene theme did not survive into row metadata")
|
|
_expect(row.get("composition_theme") == "classical_library", "composition theme did not survive into row metadata")
|
|
_expect(row.get("scene_entry", {}).get("theme") == "classical_library", "selected scene entry lost theme metadata")
|
|
_expect("Library camera layout" in scene_directive, "location theme did not drive library camera-scene adapter")
|
|
_expect(row.get("scene_camera_profile_key") == "classical_library", "row lost scene camera profile key")
|
|
_expect(scene_profile.get("family") == "library", "row lost scene camera profile family")
|
|
_expect("front-left quarter view" in scene_directive, "library camera-scene adapter missed orbit direction")
|
|
_expect("bag" not in composition.lower() and "shoes" not in composition.lower(), "location theme composition leaked outfit-check props")
|
|
_expect("315-degree front-left quarter view" in camera, "config route did not preserve orbit camera directive")
|
|
seed_config = row.get("seed_config") if isinstance(row.get("seed_config"), dict) else {}
|
|
_expect(seed_config.get("pose_seed") == 3302, "seed lock did not reroll pose axis")
|
|
_expect(seed_config.get("role_seed") == 3302, "seed lock did not reroll role axis")
|
|
_expect(row.get("trigger") == "sxcpinup_coloredpencil", "generation profile trigger did not apply")
|
|
_expect_trigger_once("config_route_location_theme.prompt", row.get("prompt"), "sxcpinup_coloredpencil")
|
|
typed_route = krea_configured_cast_formatter.format_configured_cast_result(
|
|
krea_formatter._krea_configured_cast_request_from_row(row, "balanced", "preserve"),
|
|
krea_formatter._krea_configured_cast_dependencies(),
|
|
)
|
|
legacy_route = krea_formatter._normal_row_to_krea(row, "balanced", "preserve")
|
|
_expect(
|
|
typed_route.as_tuple() == legacy_route,
|
|
"Typed Krea configured-cast formatter route should match legacy wrapper output",
|
|
)
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
prompt = krea.get("krea_prompt") or ""
|
|
_expect("library" in prompt.lower() or "bookshelves" in prompt.lower(), "Krea config route lost theme scene")
|
|
_expect("Library camera layout" in prompt, "Krea config route lost library camera-scene directive")
|
|
_expect("315-degree front-left quarter view" in prompt, "Krea config route lost camera directive")
|
|
_expect_formatter_outputs(row, "config_route_location_theme", target="single")
|
|
|
|
parking_location_config, parking_composition_config = _thematic_location_configs("parking_garage")
|
|
parking_row = pb.build_prompt_from_configs(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3311,
|
|
category_config=pb.build_category_config_json("woman", "random"),
|
|
cast_config=pb.build_cast_config_json("solo_woman", 1, 0),
|
|
generation_profile=pb.build_generation_profile_json(profile="balanced"),
|
|
camera_config=_orbit_camera(
|
|
horizontal_angle=135,
|
|
vertical_angle=-30,
|
|
zoom=4.0,
|
|
subject_focus="environment",
|
|
),
|
|
location_config=parking_location_config,
|
|
composition_config=parking_composition_config,
|
|
)
|
|
_expect_row_base(parking_row, "config_route_location_theme.parking")
|
|
parking_scene = _expect_text("config_route_location_theme.parking_scene", parking_row.get("scene_text"), 20)
|
|
parking_composition = _expect_text("config_route_location_theme.parking_composition", parking_row.get("composition"), 10)
|
|
parking_directive = _expect_text(
|
|
"config_route_location_theme.parking_camera_scene_directive",
|
|
parking_row.get("camera_scene_directive"),
|
|
40,
|
|
)
|
|
parking_profile = parking_row.get("scene_camera_profile") if isinstance(parking_row.get("scene_camera_profile"), dict) else {}
|
|
_expect("parking" in parking_scene.lower() or "garage" in parking_scene.lower(), "parking theme did not drive scene")
|
|
_expect("parking" in parking_composition.lower() or "garage" in parking_composition.lower() or "pillar" in parking_composition.lower(), "parking theme did not drive composition")
|
|
_expect(parking_row.get("location_theme") == "parking_garage", "parking location theme did not survive")
|
|
_expect(parking_row.get("scene_theme") == "parking_garage", "parking scene theme did not survive")
|
|
_expect(parking_row.get("main_category") == "woman", "parking built-in woman preset fell back to another category")
|
|
_expect(parking_row.get("primary_subject") == "woman", "parking built-in woman preset did not generate a woman")
|
|
_expect(parking_row.get("scene_camera_profile_key") == "parking_garage", "parking theme did not expose camera profile key")
|
|
_expect(parking_profile.get("family") == "semi_public", "parking camera profile family should be semi_public")
|
|
_expect("Parking garage camera layout" in parking_directive, "parking theme did not drive camera-scene adapter")
|
|
_expect("back-right quarter view" in parking_directive, "parking camera-scene adapter missed orbit direction")
|
|
_expect("low-angle shot" in parking_directive, "parking camera-scene adapter missed elevation")
|
|
|
|
creator_location_config, creator_composition_config = _thematic_location_configs("creator_bedroom")
|
|
creator_row = pb.build_prompt_from_configs(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3321,
|
|
category_config=pb.build_category_config_json("woman", "random"),
|
|
cast_config=pb.build_cast_config_json("solo_woman", 1, 0),
|
|
generation_profile=pb.build_generation_profile_json(profile="balanced"),
|
|
camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=5.5,
|
|
subject_focus="environment",
|
|
),
|
|
location_config=creator_location_config,
|
|
composition_config=creator_composition_config,
|
|
)
|
|
_expect_row_base(creator_row, "config_route_location_theme.creator")
|
|
creator_scene = _expect_text("config_route_location_theme.creator_scene", creator_row.get("scene_text"), 20)
|
|
creator_composition = _expect_text("config_route_location_theme.creator_composition", creator_row.get("composition"), 10)
|
|
creator_directive = _expect_text(
|
|
"config_route_location_theme.creator_camera_scene_directive",
|
|
creator_row.get("camera_scene_directive"),
|
|
40,
|
|
)
|
|
creator_profile = creator_row.get("scene_camera_profile") if isinstance(creator_row.get("scene_camera_profile"), dict) else {}
|
|
_expect("creator" in creator_scene.lower() or "phone" in creator_scene.lower(), "creator theme did not drive scene")
|
|
_expect(
|
|
any(token in creator_composition.lower() for token in ("creator", "tripod", "phone", "bed")),
|
|
"creator theme did not drive composition",
|
|
)
|
|
_expect(creator_row.get("location_theme") == "creator_bedroom", "creator location theme did not survive")
|
|
_expect(creator_row.get("scene_theme") == "creator_bedroom", "creator scene theme did not survive")
|
|
_expect(creator_row.get("main_category") == "woman", "creator built-in woman preset fell back to another category")
|
|
_expect(creator_row.get("primary_subject") == "woman", "creator built-in woman preset did not generate a woman")
|
|
_expect(creator_row.get("scene_camera_profile_key") == "creator_bedroom", "creator theme did not expose camera profile key")
|
|
_expect(creator_profile.get("family") == "private_creator", "creator camera profile family should be private_creator")
|
|
_expect("Creator room camera layout" in creator_directive, "creator theme did not drive camera-scene adapter")
|
|
_expect("front-right quarter view" in creator_directive, "creator camera-scene adapter missed orbit direction")
|
|
creator_krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(creator_row), target="single")
|
|
creator_prompt = creator_krea.get("krea_prompt") or ""
|
|
_expect("Creator room camera layout" in creator_prompt, "Krea config route lost creator camera-scene directive")
|
|
|
|
|
|
def smoke_builder_prompt_route_policy() -> None:
|
|
def legacy_from_request(request: builder_prompt_route.PromptBuildRequest) -> dict[str, Any]:
|
|
return pb.build_prompt(
|
|
category=request.category,
|
|
subcategory=request.subcategory,
|
|
row_number=request.row_number,
|
|
start_index=request.start_index,
|
|
seed=request.seed,
|
|
clothing=request.clothing,
|
|
ethnicity=request.ethnicity,
|
|
poses=request.poses,
|
|
backside_bias=request.backside_bias,
|
|
figure=request.figure,
|
|
no_plus_women=request.no_plus_women,
|
|
no_black=request.no_black,
|
|
minimal_clothing_ratio=request.minimal_clothing_ratio,
|
|
standard_pose_ratio=request.standard_pose_ratio,
|
|
trigger=request.trigger,
|
|
prepend_trigger_to_prompt=request.prepend_trigger_to_prompt,
|
|
extra_positive=request.extra_positive,
|
|
extra_negative=request.extra_negative,
|
|
seed_config=request.seed_config,
|
|
women_count=request.women_count,
|
|
men_count=request.men_count,
|
|
camera_config=request.camera_config,
|
|
expression_intensity=request.expression_intensity,
|
|
character_profile=request.character_profile,
|
|
character_cast=request.character_cast,
|
|
expression_enabled=request.expression_enabled,
|
|
expression_phase=request.expression_phase,
|
|
hardcore_position_config=request.hardcore_position_config,
|
|
location_config=request.location_config,
|
|
composition_config=request.composition_config,
|
|
)
|
|
|
|
seed_config_json = pb.build_seed_lock_config_json(base_seed=3501, reroll_axis="content", reroll_seed=3502)
|
|
request = builder_prompt_route.PromptBuildRequest(
|
|
category="Casual clothes",
|
|
subcategory="Casual clothes / Smart casual",
|
|
row_number=3,
|
|
start_index=8,
|
|
seed=3501,
|
|
clothing="random",
|
|
ethnicity="french_european",
|
|
poses="random",
|
|
backside_bias=0.2,
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=0.3,
|
|
standard_pose_ratio=0.4,
|
|
trigger="sxcpinup_coloredpencil",
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="typed builder route marker",
|
|
extra_negative="typed builder negative marker",
|
|
seed_config=seed_config_json,
|
|
women_count=1,
|
|
men_count=0,
|
|
camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
|
expression_intensity=0.6,
|
|
expression_enabled=True,
|
|
)
|
|
typed_route = builder_prompt_route.build_prompt_result(request, pb._prompt_build_dependencies())
|
|
legacy_row = legacy_from_request(request)
|
|
_expect(typed_route.row == legacy_row, "Typed builder prompt route should match public wrapper output")
|
|
_expect(typed_route.category == "Casual clothes", "Builder prompt route changed category")
|
|
_expect(typed_route.subcategory == "Casual clothes / Smart casual", "Builder prompt route changed subcategory")
|
|
_expect(typed_route.branch == "custom", "Builder prompt route should use custom branch for category JSON route")
|
|
_expect(typed_route.parsed_seed_config.get("content_seed") == 3502, "Builder prompt route lost seed config")
|
|
custom_trace = typed_route.row.get("generation_trace")
|
|
_expect(isinstance(custom_trace, dict), "Builder custom route lost generation_trace")
|
|
_expect(custom_trace.get("branch") == "custom", "Builder custom generation_trace lost branch")
|
|
_expect(custom_trace.get("source") == "json_category", "Builder custom generation_trace lost source")
|
|
_expect(custom_trace.get("category_slug") == "casual_clothes", "Builder custom generation_trace lost category slug")
|
|
_expect(custom_trace.get("content_seed_axis") == "content", "Builder custom generation_trace lost content axis")
|
|
_expect(custom_trace.get("seed_axes", {}).get("content", {}).get("source") == "configured", "Builder custom generation_trace lost configured content seed")
|
|
_expect(custom_trace.get("seed_axes", {}).get("content", {}).get("seed") == 3502, "Builder custom generation_trace lost content seed value")
|
|
_expect("typed builder route marker" in typed_route.row.get("prompt", ""), "Builder prompt route lost extra positive")
|
|
_expect("typed builder negative marker" in typed_route.row.get("negative_prompt", ""), "Builder prompt route lost extra negative")
|
|
_expect(
|
|
"45-degree front-right quarter view" in typed_route.row.get("camera_directive", ""),
|
|
"Builder prompt route lost camera config",
|
|
)
|
|
_expect_trigger_once("builder_prompt_route_policy.prompt", typed_route.row.get("prompt"), "sxcpinup_coloredpencil")
|
|
|
|
built_in_request = builder_prompt_route.PromptBuildRequest(
|
|
category="woman",
|
|
subcategory="random",
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3503,
|
|
clothing="full",
|
|
ethnicity="any",
|
|
poses="standard",
|
|
backside_bias=0.0,
|
|
figure="curvy",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=0.0,
|
|
standard_pose_ratio=1.0,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="built-in route marker",
|
|
extra_negative="built-in route negative",
|
|
expression_intensity=0.5,
|
|
expression_enabled=False,
|
|
)
|
|
built_in_route = builder_prompt_route.build_prompt_result(built_in_request, pb._prompt_build_dependencies())
|
|
_expect(built_in_route.row == legacy_from_request(built_in_request), "Builder built-in route should match public wrapper")
|
|
_expect(built_in_route.branch == "built_in", "Builder prompt route lost built-in branch")
|
|
_expect(built_in_route.row.get("source") == "built_in_generator", "Builder built-in branch changed source")
|
|
built_in_trace = built_in_route.row.get("generation_trace")
|
|
_expect(isinstance(built_in_trace, dict), "Builder built-in route lost generation_trace")
|
|
_expect(built_in_trace.get("branch") == "built_in", "Builder built-in generation_trace lost branch")
|
|
_expect(built_in_trace.get("source") == "built_in_generator", "Builder built-in generation_trace lost source")
|
|
_expect(built_in_trace.get("seed_axes", {}).get("person", {}).get("source") == "main", "Builder built-in generation_trace should follow main seed")
|
|
_expect(built_in_route.row.get("expression_disabled") is True, "Builder built-in branch lost expression disable")
|
|
_expect("built-in route marker" in built_in_route.row.get("prompt", ""), "Builder built-in branch lost extra positive")
|
|
|
|
auto_weighted_request = builder_prompt_route.PromptBuildRequest(
|
|
category="auto_weighted",
|
|
subcategory="random",
|
|
row_number=2,
|
|
start_index=10,
|
|
seed=3504,
|
|
clothing="random",
|
|
ethnicity="any",
|
|
poses="random",
|
|
backside_bias=0.35,
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=0.4,
|
|
standard_pose_ratio=0.6,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="auto route marker",
|
|
extra_negative="auto route negative",
|
|
seed_config=pb.build_seed_lock_config_json(base_seed=3504, reroll_axis="person", reroll_seed=3505),
|
|
expression_intensity=0.7,
|
|
expression_enabled=True,
|
|
)
|
|
auto_route = builder_prompt_route.build_prompt_result(auto_weighted_request, pb._prompt_build_dependencies())
|
|
_expect(auto_route.row == legacy_from_request(auto_weighted_request), "Builder auto-weighted route should match public wrapper")
|
|
_expect(auto_route.branch == "auto_weighted", "Builder prompt route lost auto-weighted branch")
|
|
_expect(auto_route.parsed_seed_config.get("person_seed") == 3505, "Builder auto-weighted branch lost person seed lock")
|
|
auto_trace = auto_route.row.get("generation_trace")
|
|
_expect(isinstance(auto_trace, dict), "Builder auto-weighted route lost generation_trace")
|
|
_expect(auto_trace.get("branch") == "auto_weighted", "Builder auto-weighted generation_trace lost branch")
|
|
_expect(auto_trace.get("seed_axes", {}).get("person", {}).get("source") == "configured", "Builder auto-weighted trace lost configured person seed")
|
|
_expect(auto_trace.get("seed_axes", {}).get("person", {}).get("seed") == 3505, "Builder auto-weighted trace lost person seed")
|
|
_expect("auto route marker" in auto_route.row.get("prompt", ""), "Builder auto-weighted branch lost extra positive")
|
|
|
|
|
|
def smoke_builder_config_route_policy() -> None:
|
|
category_config = pb.build_category_config_json("women_casual", "Casual clothes / Smart casual")
|
|
cast_config = pb.build_cast_config_json("solo_woman")
|
|
generation_profile = pb.build_generation_profile_json(
|
|
profile="casual_clean",
|
|
trigger_policy="prepend_trigger",
|
|
)
|
|
filter_config = pb.build_filter_config_json(
|
|
ethnicity="french_european",
|
|
figure="balanced",
|
|
)
|
|
seed_config_json = pb.build_seed_lock_config_json(base_seed=3401, reroll_axis="scene", reroll_seed=3402)
|
|
request = builder_config_route.PromptFromConfigsRequest(
|
|
row_number=2,
|
|
start_index=5,
|
|
seed=3401,
|
|
category_config=category_config,
|
|
cast_config=cast_config,
|
|
generation_profile=generation_profile,
|
|
filter_config=filter_config,
|
|
seed_config=seed_config_json,
|
|
extra_positive="clean route marker",
|
|
extra_negative="bad route marker",
|
|
)
|
|
typed_route = builder_config_route.build_prompt_from_configs_result(
|
|
request,
|
|
pb._prompt_from_configs_dependencies(),
|
|
)
|
|
legacy_row = pb.build_prompt_from_configs(
|
|
row_number=request.row_number,
|
|
start_index=request.start_index,
|
|
seed=request.seed,
|
|
category_config=category_config,
|
|
cast_config=cast_config,
|
|
generation_profile=generation_profile,
|
|
filter_config=filter_config,
|
|
seed_config=seed_config_json,
|
|
extra_positive=request.extra_positive,
|
|
extra_negative=request.extra_negative,
|
|
)
|
|
_expect(typed_route.row == legacy_row, "Prompt Builder From Configs route should match public wrapper output")
|
|
_expect(typed_route.category == "Casual clothes", "Config route lost category preset")
|
|
_expect(typed_route.subcategory == "Casual clothes / Smart casual", "Config route lost requested subcategory")
|
|
_expect(typed_route.cast["women_count"] == 1 and typed_route.cast["men_count"] == 0, "Config route lost cast preset")
|
|
_expect(typed_route.profile["trigger"] == "sxcpinup_coloredpencil", "Config route lost generation profile trigger")
|
|
_expect(typed_route.filters["ethnicity"] == "french_european", "Config route lost filter ethnicity")
|
|
config_trace = typed_route.row.get("generation_trace")
|
|
_expect(isinstance(config_trace, dict), "Config route row lost generation_trace")
|
|
_expect(config_trace.get("branch") == "custom", "Config route generation_trace lost builder branch")
|
|
_expect(config_trace.get("seed_axes", {}).get("scene", {}).get("source") == "configured", "Config route generation_trace lost scene seed lock")
|
|
_expect(config_trace.get("seed_axes", {}).get("scene", {}).get("seed") == 3402, "Config route generation_trace lost scene reroll seed")
|
|
kwargs = typed_route.build_kwargs
|
|
_expect(kwargs["category"] == typed_route.category, "Config route build kwargs category drifted")
|
|
_expect(kwargs["subcategory"] == typed_route.subcategory, "Config route build kwargs subcategory drifted")
|
|
_expect(kwargs["women_count"] == 1 and kwargs["men_count"] == 0, "Config route build kwargs cast counts drifted")
|
|
_expect(kwargs["seed_config"] == seed_config_json, "Config route build kwargs seed config drifted")
|
|
_expect(kwargs["extra_positive"] == "clean route marker", "Config route build kwargs extra positive drifted")
|
|
_expect("clean route marker" in typed_route.row.get("prompt", ""), "Config route row lost extra positive")
|
|
_expect("bad route marker" in typed_route.row.get("negative_prompt", ""), "Config route row lost extra negative")
|
|
|
|
|
|
def smoke_krea_normal_row_routes() -> None:
|
|
single = {
|
|
"subject_type": "woman",
|
|
"primary_subject": "woman",
|
|
"age_band": "25-year-old adult",
|
|
"body_phrase": "slim figure",
|
|
"skin": "fair skin",
|
|
"hair": "long blonde hair",
|
|
"eyes": "blue eyes",
|
|
"item": "silk dress",
|
|
"pose": "standing beside a window",
|
|
"scene_text": "quiet studio with warm daylight",
|
|
"expression": "soft smile",
|
|
"composition": "vertical centered portrait",
|
|
"camera_directive": "Camera: eye-level medium shot",
|
|
"style": "realistic creator-shot photography",
|
|
}
|
|
_expect_krea_normal_route_parity(single, "krea_normal_single", "metadata(single)")
|
|
style_metadata = dict(
|
|
single,
|
|
style="metadata style phrase",
|
|
positive_suffix="metadata suffix phrase",
|
|
prompt="Use: stale prompt suffix phrase.",
|
|
)
|
|
style_prompt, style_method = krea_formatter._normal_row_to_krea(style_metadata, "balanced", "preserve")
|
|
_expect(style_method == "metadata(single)", "Krea style metadata route changed method")
|
|
style_prompt_lower = style_prompt.lower()
|
|
_expect("metadata style phrase" in style_prompt_lower, "Krea metadata route lost structured style")
|
|
_expect("metadata suffix phrase" in style_prompt_lower, "Krea metadata route lost structured positive suffix")
|
|
_expect("stale prompt suffix" not in style_prompt_lower, "Krea metadata route parsed stale Use prompt text")
|
|
|
|
couple = {
|
|
"subject_type": "couple",
|
|
"primary_subject": "a woman and a man",
|
|
"subject_phrase": "woman and man",
|
|
"age": "25-year-old adult and 40-year-old adult",
|
|
"body": "slim and average builds",
|
|
"item": "Partner A wears black dress; Partner B wears dark shirt",
|
|
"pose": "standing close together",
|
|
"scene_text": "private lounge with soft lamps",
|
|
"expression": "shared confident gaze",
|
|
"composition": "two-person editorial frame",
|
|
"camera_directive": "Camera: front view, medium shot",
|
|
"style": "realistic social photo",
|
|
}
|
|
_expect_krea_normal_route_parity(couple, "krea_normal_couple", "metadata(couple)")
|
|
|
|
generic = {
|
|
"subject_type": "location",
|
|
"primary_subject": "adult editorial scene",
|
|
"item": "polished lounge styling",
|
|
"scene_text": "hotel hallway with warm wall sconces",
|
|
"expression": "quiet atmosphere",
|
|
"composition": "wide establishing frame",
|
|
"camera_directive": "Camera: wide shot",
|
|
"style": "clean photographic realism",
|
|
}
|
|
_expect_krea_normal_route_parity(generic, "krea_normal_generic", "metadata(generic)")
|
|
|
|
configured_with_descriptor = _fixture_hardcore_row(
|
|
prompt="Characters: stale prompt subject, stale body, stale skin, stale hair, stale eyes.",
|
|
cast_descriptor_text=(
|
|
"Woman A: 30-year-old adult woman, toned figure, fair skin, red hair, gray eyes; "
|
|
"Man A: 45-year-old adult man, average figure, tan skin, dark hair"
|
|
),
|
|
)
|
|
descriptor_prompt, descriptor_method = krea_formatter._normal_row_to_krea(
|
|
configured_with_descriptor,
|
|
"balanced",
|
|
"preserve",
|
|
)
|
|
_expect(descriptor_method == "metadata(configured_cast)", "Krea configured-cast route changed method")
|
|
_expect("30-year-old adult woman" in descriptor_prompt, "Krea configured-cast route lost descriptor metadata")
|
|
_expect("toned figure" in descriptor_prompt, "Krea configured-cast route lost descriptor body metadata")
|
|
_expect("stale" not in descriptor_prompt, "Krea configured-cast route parsed stale prompt character labels")
|
|
|
|
configured_without_descriptor = _fixture_hardcore_row(
|
|
prompt="Characters: stale prompt subject, stale body, stale skin, stale hair, stale eyes.",
|
|
cast_descriptor_text="",
|
|
cast_descriptors=[],
|
|
)
|
|
no_descriptor_prompt, no_descriptor_method = krea_formatter._normal_row_to_krea(
|
|
configured_without_descriptor,
|
|
"balanced",
|
|
"preserve",
|
|
)
|
|
_expect(no_descriptor_method == "metadata(configured_cast)", "Krea configured-cast no-descriptor route changed method")
|
|
_expect("stale" not in no_descriptor_prompt, "Krea configured-cast route should not parse prompt labels without metadata")
|
|
_expect("private studio room with warm light" in no_descriptor_prompt, "Krea configured-cast route lost structured scene")
|
|
|
|
|
|
def smoke_krea_action_details_policy() -> None:
|
|
_expect(
|
|
krea_action_details.strip_redundant_position_detail(
|
|
"kneeling penis-licking position while slow tongue licking on the underside of the penis"
|
|
)
|
|
== "slow tongue licking on the underside of the penis",
|
|
"Krea action detail cleanup should remove leading position-while scaffolding",
|
|
)
|
|
_expect(
|
|
krea_action_details.strip_redundant_position_detail(
|
|
"raised edge fingering position featuring mutual masturbation with both bodies touching themselves"
|
|
)
|
|
== "mutual masturbation with both bodies touching themselves",
|
|
"Krea action detail cleanup should remove leading position-featuring scaffolding",
|
|
)
|
|
_expect(
|
|
krea_action_details.strip_redundant_position_detail(
|
|
"footjob with toes curled around the penis shaft in seated footjob position"
|
|
)
|
|
== "footjob with toes curled around the penis shaft",
|
|
"Krea action detail cleanup should remove trailing in-position scaffolding",
|
|
)
|
|
_expect(
|
|
"position while"
|
|
not in krea_action_details.dedupe_outercourse_detail(
|
|
"kneeling penis-licking position while slow tongue licking on the underside of the penis",
|
|
"the woman bends forward between the man's open thighs",
|
|
"penis licking",
|
|
{"position": "kneeling penis-licking position"},
|
|
).lower(),
|
|
"Krea outercourse detail cleanup leaked position-while scaffolding",
|
|
)
|
|
_expect(
|
|
"position featuring"
|
|
not in krea_action_details.sanitize_foreplay_detail(
|
|
"raised edge fingering position featuring mutual masturbation with both bodies touching themselves",
|
|
"the woman and man sit close facing each other",
|
|
).lower(),
|
|
"Krea foreplay/manual detail cleanup leaked position-featuring scaffolding",
|
|
)
|
|
_expect(
|
|
krea_action_details.dedupe_anchor_detail(
|
|
"side-lying anal position, one leg lifted high",
|
|
"side-lying rear-entry anal pose",
|
|
)
|
|
== "one leg lifted high",
|
|
"Krea anchored detail cleanup should remove repeated anal position prefix",
|
|
)
|
|
|
|
|
|
def smoke_outercourse_action_policy() -> None:
|
|
_expect(
|
|
outercourse_action_policy.infer_outercourse_action_kind("kneeling boobjob position")
|
|
== outercourse_action_policy.OUTERCOURSE_BOOBJOB,
|
|
"Outercourse classifier lost boobjob position detection",
|
|
)
|
|
_expect(
|
|
outercourse_action_policy.infer_outercourse_action_kind("bent-over balls-licking position")
|
|
== outercourse_action_policy.OUTERCOURSE_TESTICLE,
|
|
"Outercourse classifier lost testicle/balls position detection",
|
|
)
|
|
_expect(
|
|
outercourse_action_policy.infer_outercourse_action_kind("tongue runs along the underside of the penis")
|
|
== outercourse_action_policy.OUTERCOURSE_PENIS_LICKING,
|
|
"Outercourse classifier lost penis-licking detail detection",
|
|
)
|
|
_expect(
|
|
outercourse_action_policy.infer_outercourse_action_kind("two-handed handjob with thumb and fingers around the penis")
|
|
== outercourse_action_policy.OUTERCOURSE_HANDJOB,
|
|
"Outercourse classifier lost handjob detection",
|
|
)
|
|
_expect(
|
|
outercourse_action_policy.infer_outercourse_action_kind("footjob with toes curled around the penis")
|
|
== outercourse_action_policy.OUTERCOURSE_FOOTJOB,
|
|
"Outercourse classifier lost footjob detection",
|
|
)
|
|
axis_text = krea_action_context.axis_values_text(
|
|
{
|
|
"outer_act": "handjob with one hand wrapped around the penis",
|
|
"contact_detail": "hand wrapped around the penis shaft with the glans visible",
|
|
"visibility": "hand and penis centered in frame",
|
|
}
|
|
)
|
|
_expect("handjob" in axis_text and "hand and penis" in axis_text, "Krea action context lost outercourse axes")
|
|
role_graph = hardcore_role_outercourse.build_outercourse_role_graph(
|
|
"Woman A",
|
|
"Man A",
|
|
"testicle sucking with lips around the balls",
|
|
{"position": "bent-over testicle-sucking position"},
|
|
["Man A"],
|
|
)
|
|
lower_role = role_graph.lower()
|
|
_expect("face below the pov viewer's penis at testicle height" in lower_role, "POV testicle role graph lost low-head geometry")
|
|
_expect("penis points upward" in lower_role, "POV testicle role graph lost foreground penis geometry")
|
|
deduped = krea_action_details.dedupe_outercourse_detail(
|
|
"testicle sucking with lips around the balls, balls and mouth contact visible, wet lips and tongue contact",
|
|
role_graph,
|
|
"testicle sucking with lips around the balls",
|
|
{"position": "bent-over testicle-sucking position"},
|
|
).lower()
|
|
_expect("testicle" not in deduped and "balls and mouth" not in deduped, "Krea outercourse dedupe kept redundant testicle clauses")
|
|
_expect("wet lips" in deduped, "Krea outercourse dedupe removed useful texture clause")
|
|
|
|
|
|
def smoke_item_axis_policy() -> None:
|
|
axis_values = {
|
|
"ignored": "random",
|
|
"position": "kneeling oral position",
|
|
"contact_detail": {"text": "mouth contact at hip height"},
|
|
"nested": {"unused": "fallback body detail"},
|
|
"list_detail": ["hands on hips", "auto"],
|
|
"unprioritized_detail": "extra unprioritized cue",
|
|
}
|
|
texts = item_axis_policy.axis_value_texts(axis_values)
|
|
_expect("kneeling oral position" in texts, "Item axis policy lost position value")
|
|
_expect("mouth contact at hip height" in texts, "Item axis policy lost preferred dict text")
|
|
_expect("fallback body detail" in texts, "Item axis policy lost nested fallback text")
|
|
_expect("hands on hips" in texts, "Item axis policy lost list text")
|
|
_expect("random" not in texts and "auto" not in texts, "Item axis policy leaked placeholder values")
|
|
_expect(
|
|
item_axis_policy.axis_value_texts(axis_values, existing_text="kneeling oral position already present")[0]
|
|
== "mouth contact at hip height",
|
|
"Item axis policy should skip details already present in existing text",
|
|
)
|
|
context_text = item_axis_policy.action_context_text(axis_values)
|
|
_expect("kneeling oral position" in context_text, "Item axis policy context lost priority position")
|
|
_expect("mouth contact at hip height" in context_text, "Item axis policy context lost priority contact")
|
|
_expect("extra unprioritized cue" not in context_text, "Item axis policy context should ignore unprioritized values")
|
|
_expect(
|
|
krea_action_context.axis_values_text(axis_values) == context_text,
|
|
"Krea action context should delegate to shared item axis policy",
|
|
)
|
|
|
|
|
|
def smoke_krea_row_fields_policy() -> None:
|
|
row = {
|
|
"subject_type": "configured_cast",
|
|
"primary_subject": "woman",
|
|
"cast_summary": "1 woman, 1 man",
|
|
"item": "lace bodysuit fashion editorial styling",
|
|
"pose": "standing close together",
|
|
"scene_text": "private room with warm lamps",
|
|
"expression": "soft smile",
|
|
"expression_enabled": False,
|
|
"composition": "vertical tight two-person frame",
|
|
"source_composition": "vertical source action frame",
|
|
"camera_directive": "Camera: eye-level close-up",
|
|
"camera_scene_directive": "Camera-aware scene layout.",
|
|
"style": "realistic social photo",
|
|
}
|
|
fields = krea_row_fields.extract_krea_row_fields(
|
|
row,
|
|
"preserve",
|
|
krea_formatter._krea_row_field_dependencies(),
|
|
)
|
|
normal_request = krea_formatter._krea_normal_row_request_from_row(row, "balanced", "preserve")
|
|
cast_request = krea_formatter._krea_configured_cast_request_from_row(row, "balanced", "preserve")
|
|
_expect(fields.item == "lace bodysuit", "Krea row fields did not strip generic styling suffix")
|
|
_expect(fields.expression == "", "Krea row fields ignored expression disabled flag")
|
|
_expect(fields.composition == "tight two-person frame", "Krea row fields did not normalize composition prefix")
|
|
_expect(fields.source_composition == "source action frame", "Krea row fields did not normalize source composition")
|
|
_expect(normal_request.item == fields.item, "Normal Krea route item extraction drifted")
|
|
_expect(cast_request.item == fields.item, "Configured-cast Krea route item extraction drifted")
|
|
_expect(normal_request.expression == cast_request.expression == fields.expression, "Krea route expression extraction drifted")
|
|
_expect(normal_request.camera == cast_request.camera == fields.camera, "Krea route camera extraction drifted")
|
|
_expect(cast_request.source_composition == fields.source_composition, "Configured-cast source composition drifted")
|
|
|
|
|
|
def smoke_location_config_policy() -> None:
|
|
_expect(pb.LOCATION_POOL_PRESETS is location_config.LOCATION_POOL_PRESETS, "Prompt builder location presets are not delegated")
|
|
_expect(pb.COMPOSITION_POOL_PRESETS is location_config.COMPOSITION_POOL_PRESETS, "Prompt builder composition presets are not delegated")
|
|
_expect("classical_library" in location_config.location_theme_choices(), "Location themes lost classical_library")
|
|
_expect("creator_bedroom" in location_config.location_theme_choices(), "Location themes lost creator_bedroom")
|
|
_expect("mirror_room" in location_config.location_theme_choices(), "Location themes lost mirror_room")
|
|
_expect("fetish_studio" in location_config.location_theme_choices(), "Location themes lost fetish_studio")
|
|
|
|
custom = json.loads(
|
|
pb.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations="custom_room: a quiet room with warm lamps",
|
|
)
|
|
)
|
|
_expect(custom.get("enabled") is True, "Custom location config should be active")
|
|
_expect(custom.get("apply_mode") == "replace", "Custom location config lost replace mode")
|
|
_expect(custom.get("scene_entries", [{}])[0].get("slug") == "custom_room", "Custom location slug parser changed")
|
|
structured_custom = json.loads(
|
|
pb.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations=json.dumps(
|
|
{
|
|
"slug": "structured_room",
|
|
"text": "structured room with preserved metadata",
|
|
"camera_profile": {
|
|
"key": "structured_camera_profile",
|
|
"foreground": "foreground test anchor",
|
|
"midground": "middle test anchor",
|
|
"background": "background test anchor",
|
|
},
|
|
},
|
|
sort_keys=True,
|
|
),
|
|
)
|
|
)
|
|
structured_entry = structured_custom.get("scene_entries", [{}])[0]
|
|
_expect(structured_entry.get("slug") == "structured_room", "Structured custom location lost slug")
|
|
_expect(structured_entry.get("prompt") == "structured room with preserved metadata", "Structured custom location did not normalize prompt text")
|
|
_expect(structured_entry.get("camera_profile", {}).get("key") == "structured_camera_profile", "Structured custom location lost camera profile metadata")
|
|
|
|
added = json.loads(
|
|
location_config.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="add",
|
|
preset="custom_only",
|
|
custom_locations="second_room: another quiet room",
|
|
location_config=custom,
|
|
)
|
|
)
|
|
_expect(added.get("apply_mode") == "replace", "Location add merge should preserve incoming apply_mode")
|
|
_expect(len(added.get("scene_entries") or []) == 2, "Location add merge did not keep both custom locations")
|
|
|
|
composition = json.loads(
|
|
pb.build_composition_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="no_outfit_check",
|
|
custom_compositions="manual frame through foreground bookshelves",
|
|
)
|
|
)
|
|
_expect(composition.get("enabled") is True, "Composition config should be active")
|
|
_expect(
|
|
any("outfit-check" in str(entry) for entry in composition.get("composition_entries") or []),
|
|
"Composition inline preset no_outfit_check was not applied",
|
|
)
|
|
structured_composition = json.loads(
|
|
pb.build_composition_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_compositions=json.dumps({"text": "structured composition frame", "source": "json_line"}, sort_keys=True),
|
|
)
|
|
)
|
|
structured_composition_entry = structured_composition.get("composition_entries", [{}])[0]
|
|
_expect(structured_composition_entry.get("prompt") == "structured composition frame", "Structured custom composition did not normalize prompt text")
|
|
_expect(structured_composition_entry.get("source") == "json_line", "Structured custom composition lost metadata")
|
|
parsed = pb._parse_location_config({"enabled": True, "pool_names": [], "scene_entries": custom["scene_entries"]})
|
|
_expect(pb._location_config_active(parsed), "Prompt builder location parser wrapper is inactive")
|
|
|
|
themed_location, themed_composition, theme_summary = pb.build_thematic_location_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
theme="classical_library",
|
|
)
|
|
_expect("classical_library" in theme_summary, "Themed location summary lost theme name")
|
|
themed_location_payload = json.loads(themed_location)
|
|
themed_composition_payload = json.loads(themed_composition)
|
|
_expect(themed_location_payload.get("scene_entries"), "Themed location did not output locations")
|
|
_expect(themed_location_payload.get("theme") == "classical_library", "Themed location config lost theme metadata")
|
|
_expect(
|
|
all(
|
|
not isinstance(entry, dict) or entry.get("theme") == "classical_library"
|
|
for entry in themed_location_payload.get("scene_entries") or []
|
|
),
|
|
"Themed location entries lost theme metadata",
|
|
)
|
|
_expect(themed_composition_payload.get("composition_entries"), "Themed location did not output compositions")
|
|
_expect(themed_composition_payload.get("theme") == "classical_library", "Themed composition config lost theme metadata")
|
|
parsed_themed = pb._parse_location_config(themed_location_payload)
|
|
_expect(parsed_themed.get("theme") == "classical_library", "Location parser lost theme metadata")
|
|
replaced_after_theme = json.loads(
|
|
location_config.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations="plain_room: plain room after theme",
|
|
location_config=themed_location_payload,
|
|
)
|
|
)
|
|
_expect(replaced_after_theme.get("theme") == "", "Location replace mode should not inherit upstream theme metadata")
|
|
replaced_composition_after_theme = json.loads(
|
|
location_config.build_composition_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_compositions="plain composition after theme",
|
|
composition_config=themed_composition_payload,
|
|
)
|
|
)
|
|
_expect(
|
|
replaced_composition_after_theme.get("theme") == "",
|
|
"Composition replace mode should not inherit upstream theme metadata",
|
|
)
|
|
|
|
|
|
def smoke_row_location_policy() -> None:
|
|
location = json.loads(
|
|
location_config.build_location_pool_json(
|
|
combine_mode="replace",
|
|
custom_locations="archive_corner: hidden archive corner with repeated shelves and warm table lamps",
|
|
)
|
|
)
|
|
composition = json.loads(
|
|
location_config.build_composition_pool_json(
|
|
combine_mode="replace",
|
|
custom_compositions="long archive aisle composition",
|
|
)
|
|
)
|
|
row = {
|
|
"source": "built_in_generator",
|
|
"primary_subject": "adult woman",
|
|
"scene": "unknown_old_scene",
|
|
"composition": "old frame",
|
|
"prompt": "A generated adult prompt. Scene: old room. Pose: standing. Composition: vertical old frame. Avoid: low quality.",
|
|
"caption": "sxcppnl7, generated adult prompt, old room, old frame, illustration",
|
|
}
|
|
updated = row_location.apply_location_config_to_legacy_row(dict(row), location, {}, 123, 1)
|
|
updated = row_location.apply_composition_config_to_legacy_row(updated, composition, {}, 123, 1)
|
|
_expect(updated.get("scene") == "archive_corner", "Row location policy did not select forced custom scene slug")
|
|
_expect(
|
|
updated.get("scene_text") == "hidden archive corner with repeated shelves and warm table lamps",
|
|
"Row location policy did not apply forced custom scene text",
|
|
)
|
|
_expect(updated.get("source_scene") == "unknown_old_scene", "Row location policy lost source scene slug")
|
|
_expect(updated.get("scene_entry", {}).get("slug") == "archive_corner", "Row location policy lost selected scene entry")
|
|
_expect(
|
|
"Scene: hidden archive corner with repeated shelves and warm table lamps. Pose:" in updated.get("prompt", ""),
|
|
"Row location policy did not rewrite prompt scene",
|
|
)
|
|
_expect(updated.get("composition") == "long archive aisle composition", "Row location policy did not apply forced composition")
|
|
_expect(
|
|
updated.get("composition_entry", {}).get("prompt") == "long archive aisle composition",
|
|
"Row location policy lost selected composition entry",
|
|
)
|
|
_expect(
|
|
updated.get("composition_prompt") == "vertical long archive aisle composition",
|
|
"Row location policy did not compute composition prompt",
|
|
)
|
|
_expect(
|
|
"Composition: vertical long archive aisle composition." in updated.get("prompt", ""),
|
|
"Row location policy did not rewrite prompt 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._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(
|
|
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",
|
|
)
|
|
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"
|
|
context_role = "Woman A performs a handjob while Man A stands close"
|
|
_expect(
|
|
pb._sanitize_character_expression_text_for_action(expression_text, context_role, "", {})
|
|
== row_expression.sanitize_character_expression_text_for_action(expression_text, context_role, "", {})
|
|
== "Woman A has steady focus",
|
|
"Row expression action sanitizer did not remove incompatible partner expression",
|
|
)
|
|
reverse_context = "Man A has mouth pressed to her pussy while Woman A lies back"
|
|
_expect(
|
|
row_expression.sanitize_character_expression_text_for_action(
|
|
"Woman A has tongue out; Man A has focused mouth contact",
|
|
reverse_context,
|
|
"",
|
|
{},
|
|
)
|
|
== "Man A has focused mouth contact",
|
|
"Row expression action sanitizer did not remove incompatible visible-subject expression",
|
|
)
|
|
|
|
|
|
def smoke_row_prompt_axes_policy() -> None:
|
|
category = {
|
|
"name": "Axis Test",
|
|
"slug": "axis_test",
|
|
"scenes": [{"slug": "studio", "prompt": "quiet studio with repeatable anchors"}],
|
|
"poses": ["standing fallback pose"],
|
|
"expressions": ["quiet focus", "heated smirk"],
|
|
"compositions": ["all participants visible centered frame"],
|
|
}
|
|
subcategory = {"name": "Axis Sub", "slug": "axis_sub", "items": ["axis item"]}
|
|
item = "axis item"
|
|
base_kwargs = {
|
|
"category": category,
|
|
"subcategory": subcategory,
|
|
"item": item,
|
|
"subject_type": "woman",
|
|
"context": {"fallback_pose": ""},
|
|
"poses": "standard",
|
|
"women_count": 1,
|
|
"men_count": 0,
|
|
"expression_disabled": True,
|
|
"expression_intensity": 0.5,
|
|
"is_pose_category": False,
|
|
"location_config": {},
|
|
"composition_config": {},
|
|
}
|
|
route = row_prompt_axes.resolve_prompt_axes(
|
|
**base_kwargs,
|
|
scene_rng=random.Random(1),
|
|
pose_rng=random.Random(2),
|
|
expression_rng=random.Random(3),
|
|
composition_rng=random.Random(4),
|
|
)
|
|
route_result = row_prompt_axes.resolve_prompt_axes_result(
|
|
**base_kwargs,
|
|
scene_rng=random.Random(1),
|
|
pose_rng=random.Random(2),
|
|
expression_rng=random.Random(3),
|
|
composition_rng=random.Random(4),
|
|
)
|
|
delegated = pb._prompt_axes_route(
|
|
**base_kwargs,
|
|
scene_rng=random.Random(1),
|
|
pose_rng=random.Random(2),
|
|
expression_rng=random.Random(3),
|
|
composition_rng=random.Random(4),
|
|
)
|
|
typed_delegated = pb._prompt_axes_route_result(
|
|
**base_kwargs,
|
|
scene_rng=random.Random(1),
|
|
pose_rng=random.Random(2),
|
|
expression_rng=random.Random(3),
|
|
composition_rng=random.Random(4),
|
|
)
|
|
_expect(delegated == route, "Prompt builder prompt-axes route should delegate to row_prompt_axes")
|
|
_expect(route_result.as_dict() == route, "Typed prompt axes route should match legacy dict route")
|
|
_expect(typed_delegated == route_result, "Prompt builder typed prompt-axes route should delegate")
|
|
_expect(route_result.scene_slug == "studio", "Typed prompt axes route lost selected scene slug")
|
|
_expect(route["scene_slug"] == "studio", "Prompt axes route lost selected scene slug")
|
|
_expect(route["scene"] == "quiet studio with repeatable anchors", "Prompt axes route lost selected scene text")
|
|
_expect(route["scene_entry"].get("slug") == "studio", "Prompt axes route lost selected scene entry slug")
|
|
_expect(
|
|
route["scene_entry"].get("prompt") == "quiet studio with repeatable anchors",
|
|
"Prompt axes route lost selected scene entry prompt",
|
|
)
|
|
_expect(route["pose"] == "standing fallback pose", "Prompt axes route lost selected fallback pose")
|
|
_expect(route["expression"] == "", "Prompt axes route should omit expression when disabled")
|
|
_expect(route["shared_expression"] == "", "Prompt axes route should omit shared expression when disabled")
|
|
_expect(route["source_composition"] == "all participants visible centered frame", "Prompt axes route lost source composition")
|
|
_expect(
|
|
route["composition_entry"].get("prompt") == "all participants visible centered frame",
|
|
"Prompt axes route lost selected composition entry",
|
|
)
|
|
|
|
pov_route = row_prompt_axes.resolve_prompt_axes(
|
|
**{**base_kwargs, "expression_disabled": True},
|
|
scene_rng=random.Random(1),
|
|
pose_rng=random.Random(2),
|
|
expression_rng=random.Random(3),
|
|
composition_rng=random.Random(4),
|
|
pov_character_labels=["Man A"],
|
|
)
|
|
_expect("first-person POV" in pov_route["composition"], "Prompt axes route did not adapt composition for POV")
|
|
|
|
woman_slot = character_slot.normalize_character_slot(
|
|
{"subject_type": "woman", "label": "A", "expression_intensity": 0.25}
|
|
)
|
|
configured_route = row_prompt_axes.resolve_prompt_axes(
|
|
**{
|
|
**base_kwargs,
|
|
"subject_type": "configured_cast",
|
|
"context": {"subject_type": "configured_cast"},
|
|
"expression_disabled": False,
|
|
"character_slots": [woman_slot],
|
|
"character_slot_map": {"Woman A": woman_slot},
|
|
},
|
|
scene_rng=random.Random(1),
|
|
pose_rng=random.Random(2),
|
|
expression_rng=random.Random(3),
|
|
composition_rng=random.Random(4),
|
|
)
|
|
_expect(
|
|
configured_route["character_expression_text"].startswith("Woman A has "),
|
|
"Prompt axes route lost configured-cast character expression",
|
|
)
|
|
_expect(
|
|
configured_route["expression"] == configured_route["character_expression_text"],
|
|
"Prompt axes route did not promote character expression text to row expression",
|
|
)
|
|
|
|
|
|
def smoke_row_item_policy() -> None:
|
|
weighted_entries = [
|
|
{"text": "first", "weight": 0.0},
|
|
{"text": "second", "weight": 4.0},
|
|
{"text": "third", "weight": 1.0},
|
|
]
|
|
_expect(
|
|
pb._weighted_choice(random.Random(44), weighted_entries) == row_item.weighted_choice(random.Random(44), weighted_entries),
|
|
"Prompt builder weighted item choice should delegate to row_item",
|
|
)
|
|
_expect(
|
|
pb._pair_from({"name": "Library Corner", "description": "carved shelves and warm lamps"})
|
|
== row_item.pair_from({"name": "Library Corner", "description": "carved shelves and warm lamps"}),
|
|
"Prompt builder pair parser should delegate to row_item",
|
|
)
|
|
|
|
oral_values = [
|
|
"fellatio with penis in mouth",
|
|
"cunnilingus with tongue on pussy",
|
|
"sixty-nine mutual oral",
|
|
]
|
|
_expect(
|
|
pb._oral_acts_for_position(oral_values, "kneeling oral position")
|
|
== row_item.oral_acts_for_position(oral_values, "kneeling oral position")
|
|
== ["fellatio with penis in mouth"],
|
|
"Row item oral act filtering changed for kneeling oral",
|
|
)
|
|
outer_values = [
|
|
"titjob with compressed breasts",
|
|
"handjob with palm around shaft",
|
|
"footjob with soles and toes",
|
|
]
|
|
_expect(
|
|
pb._outercourse_acts_for_position(outer_values, "footjob position")
|
|
== row_item.outercourse_acts_for_position(outer_values, "footjob position")
|
|
== ["footjob with soles and toes"],
|
|
"Row item outercourse act filtering changed for footjob",
|
|
)
|
|
texture_values = [
|
|
"fingers pressing around shaft",
|
|
"soles pressing around shaft",
|
|
"tongue wet against shaft",
|
|
]
|
|
_expect(
|
|
row_item.outercourse_axis_values_for_position(texture_values, "footjob position", "texture_detail")
|
|
== ["soles pressing around shaft"],
|
|
"Row item outercourse texture axis should prefer footjob-compatible details",
|
|
)
|
|
anal_leg_values = [
|
|
"standing with legs braced",
|
|
"one leg lifted high",
|
|
"kneeling with thighs apart",
|
|
"knees pressed to chest",
|
|
]
|
|
_expect(
|
|
pb._anal_axis_values_for_position(anal_leg_values, "side-lying anal position", "leg_detail")
|
|
== row_item.anal_axis_values_for_position(anal_leg_values, "side-lying anal position", "leg_detail")
|
|
== ["one leg lifted high"],
|
|
"Row item anal leg-detail filtering changed for side-lying anal",
|
|
)
|
|
|
|
category = {}
|
|
subcategory = {
|
|
"name": "Oral sex",
|
|
"slug": "oral_sex",
|
|
"item_templates": [
|
|
{
|
|
"template": "{oral_act}; {hand_detail}",
|
|
"action_family": "oral",
|
|
"position_family": "oral",
|
|
}
|
|
],
|
|
"item_axes": {
|
|
"position": ["kneeling oral position"],
|
|
"oral_act": oral_values,
|
|
"hand_detail": ["hands on hips", "spreading thighs"],
|
|
},
|
|
}
|
|
item = "Oral sex"
|
|
builder_result = pb._compose_item(random.Random(7), category, subcategory, item, women_count=1, men_count=1)
|
|
policy_result = row_item.compose_item(random.Random(7), category, subcategory, item, women_count=1, men_count=1)
|
|
_expect(builder_result == policy_result, "Prompt builder compose item should delegate to row_item")
|
|
item_text, item_name, axis_values, metadata = policy_result
|
|
_expect(item_name == "Oral sex", "Row item compose lost item name")
|
|
_expect("fellatio" in item_text, "Row item compose did not apply oral act compatibility filter")
|
|
_expect(axis_values.get("hand_detail") == "hands on hips", "Row item compose did not apply oral detail filter")
|
|
_expect(metadata.get("action_family") == "oral", "Row item compose lost template metadata")
|
|
|
|
anal_subcategory = {
|
|
"name": "Anal and double penetration",
|
|
"slug": "anal_double_penetration",
|
|
"item_templates": [
|
|
{
|
|
"template": "{anal_act} in {position}, with {leg_detail}",
|
|
"action_family": "default",
|
|
"position_family": "anal",
|
|
}
|
|
],
|
|
"item_axes": {
|
|
"position": ["side-lying anal position"],
|
|
"anal_act": ["penis entering ass"],
|
|
"leg_detail": anal_leg_values,
|
|
},
|
|
}
|
|
anal_text, _anal_name, anal_axis_values, _anal_metadata = row_item.compose_item(
|
|
random.Random(5),
|
|
{},
|
|
anal_subcategory,
|
|
"Anal and double penetration",
|
|
women_count=1,
|
|
men_count=1,
|
|
)
|
|
_expect("standing with legs braced" not in anal_text, "Row item compose leaked standing legs into side-lying anal")
|
|
_expect(anal_axis_values.get("leg_detail") == "one leg lifted high", "Row item compose did not apply anal leg-detail filter")
|
|
|
|
|
|
def smoke_row_category_route_policy() -> None:
|
|
hard_config = hardcore_position_config.parse_hardcore_position_config(_position_filter("oral_only", "oral", ["kneeling"]))
|
|
seed_cfg = seed_config.parse_seed_config({})
|
|
route = row_category_route.select_category_item_route(
|
|
category_choice="custom_random",
|
|
subcategory_choice="Hardcore sexual poses / Oral sex",
|
|
seed_config=seed_cfg,
|
|
seed=2301,
|
|
row_number=1,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=hard_config,
|
|
)
|
|
route_result = row_category_route.select_category_item_route_result(
|
|
category_choice="custom_random",
|
|
subcategory_choice="Hardcore sexual poses / Oral sex",
|
|
seed_config=seed_cfg,
|
|
seed=2301,
|
|
row_number=1,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=hard_config,
|
|
)
|
|
delegated = pb._select_category_item_route(
|
|
category_choice="custom_random",
|
|
subcategory_choice="Hardcore sexual poses / Oral sex",
|
|
seed_config=seed_cfg,
|
|
seed=2301,
|
|
row_number=1,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=hard_config,
|
|
)
|
|
typed_delegated = pb._select_category_item_route_result(
|
|
category_choice="custom_random",
|
|
subcategory_choice="Hardcore sexual poses / Oral sex",
|
|
seed_config=seed_cfg,
|
|
seed=2301,
|
|
row_number=1,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=hard_config,
|
|
)
|
|
_expect(delegated == route, "Prompt builder category/item route should delegate to row_category_route")
|
|
_expect(route_result.as_dict() == route, "Typed category/item route should match legacy dict route")
|
|
_expect(typed_delegated == route_result, "Prompt builder typed category/item route should delegate")
|
|
_expect(route_result.content_axis == "pose", "Typed category/item route lost content seed axis")
|
|
_expect(route["category"]["slug"] == "hardcore_sexual_poses", "Row category route selected wrong hardcore category")
|
|
_expect(route["subcategory"]["slug"] == "oral_sex", "Row category route selected wrong hardcore subcategory")
|
|
_expect(route["content_axis"] == "pose", "Hardcore pose category should use pose seed axis")
|
|
_expect(route["is_pose_category"] is True, "Hardcore pose category should be marked as pose content")
|
|
_expect(isinstance(route["item_axis_values"], dict), "Row category route lost item axis metadata")
|
|
_expect(isinstance(route["formatter_hints"], dict), "Row category route lost formatter hint metadata")
|
|
_expect(
|
|
pb._is_pose_content_category(route["category"], route["subcategory"])
|
|
== row_category_route.is_pose_content_category(route["category"], route["subcategory"]),
|
|
"Prompt builder pose-content wrapper should delegate",
|
|
)
|
|
|
|
casual_route = row_category_route.select_category_item_route(
|
|
category_choice="custom_random",
|
|
subcategory_choice="Casual clothes / Streetwear",
|
|
seed_config=seed_cfg,
|
|
seed=2301,
|
|
row_number=1,
|
|
women_count=1,
|
|
men_count=0,
|
|
hardcore_position_config={},
|
|
)
|
|
_expect(casual_route["category"]["slug"] == "casual_clothes", "Row category route selected wrong casual category")
|
|
_expect(casual_route["content_axis"] == "content", "Non-pose category should use content seed axis")
|
|
_expect(casual_route["is_pose_category"] is False, "Non-pose category should not be marked as pose content")
|
|
|
|
exposed_route = row_category_route.select_category_item_route(
|
|
category_choice="custom_random",
|
|
subcategory_choice="Provocative erotic clothes / Sheer exposed",
|
|
seed_config=seed_cfg,
|
|
seed=2302,
|
|
row_number=1,
|
|
women_count=1,
|
|
men_count=0,
|
|
hardcore_position_config={},
|
|
)
|
|
_expect(exposed_route["subcategory"]["slug"] == "sheer_exposed", "Row category route selected wrong exposed category")
|
|
_expect(exposed_route["content_axis"] == "content", "Exposed clothing slug should not be treated as pose content")
|
|
_expect(exposed_route["is_pose_category"] is False, "Exposed clothing slug should not be marked as pose content")
|
|
|
|
|
|
def smoke_row_generation_policy() -> None:
|
|
_expect(pb._ratio_or_none(-1) is None, "Prompt builder ratio helper should treat negative as unset")
|
|
_expect(pb._ratio_or_none(1.5) == row_generation.ratio_or_none(1.5) == 1.0, "Row generation ratio clamp changed")
|
|
_expect(pb._clamped_float("bad", 0.4) == row_generation.clamped_float("bad", 0.4) == 0.4, "Row generation float default changed")
|
|
|
|
_expect(
|
|
pb._pick_clothing_mode(random.Random(1), "random", None)
|
|
== row_generation.pick_clothing_mode(random.Random(1), "random", None),
|
|
"Prompt builder clothing mode picker should delegate to row_generation",
|
|
)
|
|
_expect(
|
|
row_generation.pick_pose_mode(random.Random(2), "evocative", 1.0) == "standard",
|
|
"Row generation standard pose ratio override changed",
|
|
)
|
|
_expect(
|
|
pb._pick_figure_bias(random.Random(3), "random") == row_generation.pick_figure_bias(random.Random(3), "random"),
|
|
"Prompt builder figure picker should delegate to row_generation",
|
|
)
|
|
_expect(
|
|
row_generation.pick_expression_intensity(random.Random(4), -1) == (0.24, "random"),
|
|
"Row generation random expression intensity changed",
|
|
)
|
|
_expect(
|
|
pb._pick_expression_intensity(random.Random(4), 2.0) == row_generation.pick_expression_intensity(random.Random(4), 2.0) == (1.0, "input"),
|
|
"Prompt builder expression intensity picker should delegate to row_generation",
|
|
)
|
|
|
|
direct_args = dict(
|
|
category="woman",
|
|
row_number=3,
|
|
start_index=41,
|
|
clothing="full",
|
|
ethnicity="any",
|
|
poses="standard",
|
|
backside_bias=0.25,
|
|
figure="curvy",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=None,
|
|
standard_pose_ratio=None,
|
|
seed=5050,
|
|
)
|
|
_expect(
|
|
pb._build_direct_builtin_row(**direct_args) == row_generation.build_direct_builtin_row(**direct_args),
|
|
"Prompt builder direct built-in row should delegate to row_generation",
|
|
)
|
|
auto_args = dict(
|
|
row_number=2,
|
|
start_index=41,
|
|
clothing="minimal",
|
|
ethnicity="any",
|
|
poses="evocative",
|
|
backside_bias=0.0,
|
|
figure="balanced",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=None,
|
|
standard_pose_ratio=None,
|
|
seed=6060,
|
|
)
|
|
auto_row = row_generation.build_auto_weighted_row(**auto_args)
|
|
_expect(pb._build_auto_weighted_row(**auto_args) == auto_row, "Prompt builder auto-weighted row should delegate to row_generation")
|
|
_expect(auto_row.get("source") == "built_in_generator", "Row generation auto-weighted row lost source metadata")
|
|
|
|
seed_cfg = seed_config.parse_seed_config({"category_seed": 123})
|
|
_expect(
|
|
pb._auto_full_choice(seed_cfg, 7070, 1) == row_generation.auto_full_choice(seed_cfg, 7070, 1),
|
|
"Prompt builder auto-full choice should delegate to row_generation",
|
|
)
|
|
|
|
|
|
def smoke_category_extensions_policy() -> None:
|
|
_expect(pb.BUILTIN_CATEGORIES is category_extensions.BUILTIN_CATEGORIES, "Prompt builder built-in categories should come from category_extensions")
|
|
targets = category_extensions.extension_targets()
|
|
_expect(
|
|
pb._extension_targets().keys() == targets.keys(),
|
|
"Prompt builder extension targets should delegate to category_extensions",
|
|
)
|
|
_expect(targets.get("group_scenes", (None, None))[1] is True, "Group scene extension target should expect scene pairs")
|
|
sample = [{"slug": "a", "prompt": "one"}]
|
|
category_extensions.unique_extend(sample, [{"slug": "a", "prompt": "one"}, {"slug": "b", "prompt": "two"}])
|
|
_expect(len(sample) == 2 and sample[1]["slug"] == "b", "Category extension unique_extend changed")
|
|
|
|
pb.apply_pool_extensions()
|
|
category_extensions.apply_pool_extensions()
|
|
_expect(pb.category_choices() == category_extensions.category_choices(), "Prompt builder category choices should delegate")
|
|
_expect(pb.subcategory_choices() == category_extensions.subcategory_choices(), "Prompt builder subcategory choices should delegate")
|
|
_expect("Hardcore sexual poses" in pb.category_choices(), "Category choices lost hardcore JSON category")
|
|
_expect(
|
|
any(slug == "private_suite_group_party" for slug, _prompt in targets["group_scenes"][0]),
|
|
"JSON pool_extensions did not reach legacy group scenes",
|
|
)
|
|
|
|
previous_category_dir = category_library.CATEGORY_DIR
|
|
previous_extensions_applied = category_extensions._EXTENSIONS_APPLIED
|
|
try:
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
category_library.CATEGORY_DIR = Path(temp_dir)
|
|
category_extensions._EXTENSIONS_APPLIED = False
|
|
Path(temp_dir, "custom_category.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"categories": {
|
|
"Dev Test Wear": {
|
|
"prompt_template": (
|
|
"{subject_phrase}: clean test style. {item_label}: {item}. "
|
|
"Scene: {scene}. Pose: {pose}. Facial expression: {expression}. "
|
|
"Composition: {composition}."
|
|
),
|
|
"caption_template": "{subject_phrase}, {item}, {scene}, {composition}",
|
|
"subcategories": {
|
|
"Layered Office": {
|
|
"items": ["structured test blazer over dark trousers"],
|
|
"scenes": ["temporary showroom with modular shelves"],
|
|
"poses": ["standing by a rail"],
|
|
"expressions": ["focused calm look"],
|
|
"compositions": ["centered catalog frame"],
|
|
}
|
|
},
|
|
},
|
|
"Dev / Test Wear": {
|
|
"prompt_template": (
|
|
"{subject_phrase}: slash-name test style. {item_label}: {item}. "
|
|
"Scene: {scene}. Pose: {pose}. Facial expression: {expression}. "
|
|
"Composition: {composition}."
|
|
),
|
|
"caption_template": "{subject_phrase}, {item}, {scene}, {composition}",
|
|
"subcategories": {
|
|
"Layered / Office": {
|
|
"items": ["slash-safe structured blazer over tailored trousers"],
|
|
"scenes": ["slash-safe showroom with glass shelving"],
|
|
"poses": ["standing beside a mirrored divider"],
|
|
"expressions": ["focused exact-route look"],
|
|
"compositions": ["slash-safe centered catalog frame"],
|
|
}
|
|
},
|
|
},
|
|
}
|
|
},
|
|
ensure_ascii=True,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
_expect("Dev Test Wear" in pb.category_choices(), "User-added JSON category did not reach category choices")
|
|
_expect(
|
|
"Dev Test Wear / Layered Office" in pb.subcategory_choices(),
|
|
"User-added JSON subcategory did not reach subcategory choices",
|
|
)
|
|
slash_selector = "Dev / Test Wear / Layered / Office"
|
|
_expect("Dev / Test Wear" in pb.category_choices(), "Slash-bearing user category did not reach category choices")
|
|
_expect(
|
|
slash_selector in pb.subcategory_choices(),
|
|
"Slash-bearing user subcategory did not reach subcategory choices",
|
|
)
|
|
row = pb.build_prompt(
|
|
category="Dev Test Wear",
|
|
subcategory="Dev Test Wear / Layered Office",
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=4109,
|
|
clothing="random",
|
|
ethnicity="any",
|
|
poses="random",
|
|
backside_bias=0.0,
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=0.0,
|
|
standard_pose_ratio=1.0,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="",
|
|
extra_negative="",
|
|
women_count=1,
|
|
men_count=0,
|
|
)
|
|
_expect_row_base(row, "category_extensions_user_added_json")
|
|
_expect(row.get("source") == "json_category", "User-added JSON category did not use custom row route")
|
|
_expect(row.get("main_category") == "Dev Test Wear", "User-added JSON category name drifted")
|
|
_expect(row.get("subcategory") == "Layered Office", "User-added JSON subcategory name drifted")
|
|
_expect(row.get("item") == "structured test blazer over dark trousers", "User-added JSON item did not generate")
|
|
_expect(row.get("scene_text") == "temporary showroom with modular shelves", "User-added JSON scene did not generate")
|
|
_expect(row.get("pose") == "standing by a rail", "User-added JSON pose did not generate")
|
|
_expect(row.get("expression") == "focused calm look", "User-added JSON expression did not generate")
|
|
_expect(row.get("composition") == "centered catalog frame", "User-added JSON composition did not generate")
|
|
_expect_formatter_outputs(row, "category_extensions_user_added_json", target="single")
|
|
slash_row = pb.build_prompt(
|
|
category="Dev / Test Wear",
|
|
subcategory=slash_selector,
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=4110,
|
|
clothing="random",
|
|
ethnicity="any",
|
|
poses="random",
|
|
backside_bias=0.0,
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
minimal_clothing_ratio=0.0,
|
|
standard_pose_ratio=1.0,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="",
|
|
extra_negative="",
|
|
women_count=1,
|
|
men_count=0,
|
|
)
|
|
_expect_row_base(slash_row, "category_extensions_slash_named_json")
|
|
_expect(slash_row.get("main_category") == "Dev / Test Wear", "Slash-bearing JSON category name drifted")
|
|
_expect(slash_row.get("subcategory") == "Layered / Office", "Slash-bearing JSON subcategory name drifted")
|
|
_expect(
|
|
slash_row.get("item") == "slash-safe structured blazer over tailored trousers",
|
|
"Slash-bearing JSON exact subcategory did not generate the intended item",
|
|
)
|
|
_expect_formatter_outputs(slash_row, "category_extensions_slash_named_json", target="single")
|
|
finally:
|
|
category_library.CATEGORY_DIR = previous_category_dir
|
|
category_extensions._EXTENSIONS_APPLIED = previous_extensions_applied
|
|
|
|
|
|
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")
|
|
_expect("woman" in category_cast_config.category_preset_choices(), "Category preset choices lost built-in woman")
|
|
_expect("man" in category_cast_config.category_preset_choices(), "Category preset choices lost built-in man")
|
|
_expect("hardcore_pose" in category_cast_config.category_preset_choices(), "Category preset choices lost hardcore_pose")
|
|
_expect("custom_counts" in category_cast_config.cast_preset_choices(), "Cast preset choices lost custom_counts")
|
|
_expect(
|
|
pb._count_phrase(2, "adult woman", "adult women") == cast_context.count_phrase(2, "adult woman", "adult women"),
|
|
"Prompt builder count phrase should delegate to cast_context",
|
|
)
|
|
configured = cast_context.configured_cast_context(1, 2)
|
|
_expect(configured.get("subject_phrase") == "one adult woman and two adult men", "Configured cast subject phrase changed")
|
|
_expect(configured.get("cast_summary") == "1 woman, 2 men, 3 total adults", "Configured cast summary changed")
|
|
_expect(configured.get("scene_kind") == "adult threesome sex scene", "Configured cast scene kind changed")
|
|
_expect(
|
|
pb._configured_cast_context(1, 2) == configured,
|
|
"Prompt builder configured cast context should delegate to cast_context",
|
|
)
|
|
_expect(
|
|
cast_context.couple_type_from_counts(
|
|
random.Random(1),
|
|
2,
|
|
0,
|
|
choose=lambda _rng, pool: pool[0],
|
|
couple_types=[("woman and man", "a woman and a man", "fallback pose")],
|
|
)
|
|
== ("two women", "two women", "close affectionate couple pose", 2, 0),
|
|
"Couple type count override for two women changed",
|
|
)
|
|
_expect(
|
|
pb._subject_context(random.Random(5), "couple", "any", "curvy", False, False, 1, 1)
|
|
== subject_context.subject_context(random.Random(5), "couple", "any", "curvy", False, False, 1, 1),
|
|
"Prompt builder subject context should delegate to subject_context",
|
|
)
|
|
_expect(
|
|
subject_context.subject_context(random.Random(1), "configured_cast", "any", "curvy", False, False, 2, 1).get("cast_summary")
|
|
== "2 women, 1 man, 3 total adults",
|
|
"Configured cast subject context changed",
|
|
)
|
|
group = subject_context.subject_context(random.Random(2), "group", "asian", "curvy", False, False)
|
|
_expect(group.get("subject") == "mixed Asian adult group", "Group subject ethnicity wording changed")
|
|
_expect(
|
|
subject_context.subject_context(random.Random(3), "layout", "any", "curvy", False, False).get("subject")
|
|
== "layout scene",
|
|
"Layout subject context fallback changed",
|
|
)
|
|
label_slots = [
|
|
{"subject_type": "woman", "label": "auto_chain", "name": "older auto"},
|
|
{"subject_type": "woman", "label": "auto_chain", "name": "newer auto"},
|
|
{"subject_type": "man", "label": "B", "name": "explicit man"},
|
|
]
|
|
label_map = cast_context.character_slot_label_map(label_slots)
|
|
_expect(
|
|
label_map.get("Woman A", {}).get("name") == "newer auto"
|
|
and label_map.get("Woman B", {}).get("name") == "older auto",
|
|
"Character slot auto-chain label order changed",
|
|
)
|
|
_expect(
|
|
label_map.get("Man B", {}).get("name") == "explicit man",
|
|
"Character slot explicit label mapping changed",
|
|
)
|
|
_expect(
|
|
pb._character_slot_label_map(label_slots) == label_map,
|
|
"Prompt builder character slot label map should delegate to cast_context",
|
|
)
|
|
|
|
category_config = json.loads(pb.build_category_config_json("hardcore_pose", "Foreplay and teasing"))
|
|
_expect(category_config.get("category") == "Hardcore sexual poses", "Category config lost hardcore category mapping")
|
|
_expect(category_config.get("subcategory") == "Foreplay and teasing", "Category config lost explicit subcategory")
|
|
_expect(pb._parse_category_config(category_config) == ("Hardcore sexual poses", "Foreplay and teasing"), "Category parser wrapper drifted")
|
|
builtin_woman_config = json.loads(pb.build_category_config_json("woman", "random"))
|
|
_expect(builtin_woman_config.get("preset") == "woman", "Built-in woman category preset fell back")
|
|
_expect(builtin_woman_config.get("category") == "woman", "Built-in woman category config lost direct category")
|
|
_expect(pb._parse_category_config(builtin_woman_config) == ("woman", "random"), "Built-in woman category parser drifted")
|
|
|
|
fallback_config = json.loads(category_cast_config.build_category_config_json("unknown", "random"))
|
|
_expect(fallback_config.get("preset") == "auto_weighted", "Unknown category preset did not fall back")
|
|
_expect(pb._parse_category_config({"preset": "unknown"}) == ("auto_weighted", "random"), "Unknown category parser fallback changed")
|
|
|
|
cast_config = json.loads(pb.build_cast_config_json("mixed_couple", 9, 9))
|
|
_expect((cast_config.get("women_count"), cast_config.get("men_count")) == (1, 1), "Cast preset did not override manual counts")
|
|
custom_cast = json.loads(category_cast_config.build_cast_config_json("custom_counts", -5, 99))
|
|
_expect((custom_cast.get("women_count"), custom_cast.get("men_count")) == (0, 12), "Custom cast counts were not clamped")
|
|
empty_cast = pb._parse_cast_config({"cast_mode": "custom_counts", "women_count": 0, "men_count": 0})
|
|
_expect((empty_cast.get("women_count"), empty_cast.get("men_count")) == (1, 0), "Empty custom cast was not corrected")
|
|
|
|
|
|
def smoke_row_subject_route_policy() -> None:
|
|
seed_cfg = seed_config.parse_seed_config({})
|
|
slot_cast = pb.build_character_slot_json(
|
|
subject_type="woman",
|
|
label="A",
|
|
age="32-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="slim",
|
|
hair="short silver bob",
|
|
eyes="gray eyes",
|
|
descriptor_detail="full",
|
|
)["character_cast"]
|
|
profile = {
|
|
"profile_type": "character",
|
|
"subject_type": "woman",
|
|
"age": "45-year-old adult",
|
|
"body": "average",
|
|
"body_phrase": "average figure",
|
|
"skin": "profile skin",
|
|
"hair": "profile hair",
|
|
"eyes": "profile eyes",
|
|
}
|
|
route = row_subject_route.resolve_subject_route(
|
|
subject_type="woman",
|
|
seed_config=seed_cfg,
|
|
seed=501,
|
|
row_number=1,
|
|
ethnicity="any",
|
|
figure="balanced",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
women_count=1,
|
|
men_count=0,
|
|
character_profile=profile,
|
|
character_cast=slot_cast,
|
|
)
|
|
delegated = pb._subject_route(
|
|
subject_type="woman",
|
|
seed_config=seed_cfg,
|
|
seed=501,
|
|
row_number=1,
|
|
ethnicity="any",
|
|
figure="balanced",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
women_count=1,
|
|
men_count=0,
|
|
character_profile=profile,
|
|
character_cast=slot_cast,
|
|
)
|
|
_expect(delegated == route, "Prompt builder subject route should delegate to row_subject_route")
|
|
_expect(route["subject_type"] == "woman", "Subject route changed single-woman subject type")
|
|
_expect(route["character_slot_status"] == "applied:Woman A", "Subject route did not apply matching Woman A slot")
|
|
_expect(route["character_profile_status"] == "skipped_character_slot", "Subject route should skip profile when slot applies")
|
|
_expect(route["context"].get("age") == "32-year-old adult", "Subject route lost slot age override")
|
|
_expect(route["context"].get("hair") == "short silver bob", "Subject route lost slot hair override")
|
|
_expect(route["applied_profile"] == {}, "Subject route should not apply profile over matching slot")
|
|
|
|
cast_route = row_subject_route.resolve_subject_route(
|
|
subject_type="configured_cast",
|
|
seed_config=seed_cfg,
|
|
seed=502,
|
|
row_number=1,
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
women_count=1,
|
|
men_count=1,
|
|
character_cast=_character_cast(pov_man=True),
|
|
)
|
|
_expect(cast_route["subject_type"] == "configured_cast", "Configured-cast subject route changed subject type")
|
|
_expect(cast_route["pov_character_labels"] == ["Man A"], "Subject route lost configured-cast POV man label")
|
|
_expect("Woman A:" in cast_route["cast_descriptor_text"], "Subject route lost visible woman descriptor")
|
|
_expect("Man A:" not in cast_route["cast_descriptor_text"], "Subject route should not describe POV man as visible cast")
|
|
|
|
|
|
def smoke_generation_profile_config_policy() -> None:
|
|
_expect(
|
|
pb.GENERATION_PROFILE_PRESETS is generation_profile_config.GENERATION_PROFILE_PRESETS,
|
|
"Prompt builder generation profile presets are not delegated",
|
|
)
|
|
_expect("krea2_friendly" in generation_profile_config.generation_profile_choices(), "Generation profile choices lost krea2_friendly")
|
|
|
|
profile = json.loads(
|
|
pb.build_generation_profile_json(
|
|
profile="krea2_friendly",
|
|
clothing_override="minimal",
|
|
poses_override="random",
|
|
expression_enabled=False,
|
|
expression_intensity_mode="random",
|
|
expression_intensity=0.8,
|
|
backside_bias=2,
|
|
minimal_clothing_ratio=0.25,
|
|
standard_pose_ratio=0.75,
|
|
trigger_policy="prepend_trigger",
|
|
)
|
|
)
|
|
_expect(profile.get("profile") == "krea2_friendly", "Generation profile output lost selected profile")
|
|
_expect(profile.get("clothing") == "minimal", "Generation profile clothing override failed")
|
|
_expect(profile.get("poses") == "random", "Generation profile poses override failed")
|
|
_expect(profile.get("expression_enabled") is False, "Generation profile expression disable failed")
|
|
_expect(profile.get("expression_intensity") == -1.0, "Generation profile random expression marker changed")
|
|
_expect(profile.get("backside_bias") == 1.0, "Generation profile backside bias clamp changed")
|
|
_expect(profile.get("prepend_trigger_to_prompt") is True, "Generation profile trigger override failed")
|
|
|
|
parsed = pb._parse_generation_profile(profile)
|
|
_expect(parsed.get("clothing") == "minimal", "Generation profile parser wrapper lost clothing")
|
|
_expect(parsed.get("expression_enabled") is False, "Generation profile parser wrapper lost expression disable")
|
|
_expect(parsed.get("minimal_clothing_ratio") == 0.25, "Generation profile parser wrapper lost minimal clothing ratio")
|
|
|
|
fallback = generation_profile_config.parse_generation_profile({"profile": "unknown", "clothing": "bad", "poses": "bad"})
|
|
_expect(fallback.get("profile") == "unknown", "Generation profile parser should preserve raw profile label")
|
|
_expect(fallback.get("clothing") == "full", "Generation profile parser did not normalize invalid clothing")
|
|
_expect(fallback.get("poses") == "standard", "Generation profile parser did not normalize invalid poses")
|
|
_expect(fallback.get("trigger") == "sxcpinup_coloredpencil", "Generation profile parser lost default trigger")
|
|
|
|
|
|
def smoke_filter_config_policy() -> None:
|
|
_expect(pb.ETHNICITY_FILTER_CHOICES is filter_config.ETHNICITY_FILTER_CHOICES, "Prompt builder ethnicity choices are not delegated")
|
|
_expect("french_european" in filter_config.ETHNICITY_LIST_KEYS, "Ethnicity list keys lost regional choices")
|
|
|
|
advanced = json.loads(
|
|
pb.build_filter_config_json(
|
|
include_european=True,
|
|
include_mediterranean_mena=False,
|
|
include_latina=False,
|
|
include_east_asian=False,
|
|
include_southeast_asian=False,
|
|
include_south_asian=False,
|
|
include_black_african=True,
|
|
include_indigenous=False,
|
|
include_mixed=False,
|
|
include_plus_size=False,
|
|
figure="bad",
|
|
)
|
|
)
|
|
_expect(advanced.get("ethnicity_includes") == ["european", "black_african"], "Advanced filter selected ethnicity list changed")
|
|
_expect("exclude_latina" in advanced.get("ethnicity", ""), "Advanced filter ethnicity excludes changed")
|
|
_expect(advanced.get("figure") == "curvy", "Advanced filter invalid figure fallback changed")
|
|
_expect(advanced.get("no_plus_women") is True, "Advanced filter plus-size exclusion changed")
|
|
|
|
ethnicity_list = pb.build_ethnicity_list_json(include_french_european=True, include_asian=True, strict_excludes=True)
|
|
_expect("french_european" in ethnicity_list["ethnicity"], "Ethnicity list lost regional include")
|
|
_expect("asian" in ethnicity_list["ethnicity"], "Ethnicity list lost umbrella Asian include")
|
|
_expect("exclude_european" not in ethnicity_list["ethnicity"], "Ethnicity list should protect European when regional Europe is selected")
|
|
_expect("exclude_east_asian" not in ethnicity_list["ethnicity"], "Ethnicity list should protect East Asian when Asian is selected")
|
|
_expect("filter_config" in ethnicity_list, "Ethnicity list lost filter_config output")
|
|
|
|
parsed_text = pb._parse_filter_config("french_european")
|
|
_expect(parsed_text.get("ethnicity") == "french_european", "Filter parser text shortcut changed")
|
|
parsed_bad = filter_config.parse_filter_config({"ethnicity": "bad", "figure": "bad"})
|
|
_expect(parsed_bad.get("ethnicity") == "any", "Filter parser invalid ethnicity fallback changed")
|
|
_expect(parsed_bad.get("figure") == "curvy", "Filter parser invalid figure fallback changed")
|
|
_expect(pb.normalize_ethnicity_filter("random", "any", allow_random=True) == "random", "Ethnicity random normalization changed")
|
|
_expect(pb.normalize_ethnicity_filter("random", "any", allow_random=False) == "any", "Ethnicity default normalization changed")
|
|
|
|
|
|
def smoke_character_config_policy() -> None:
|
|
_expect(pb.CHARACTER_LABEL_CHOICES is character_config.CHARACTER_LABEL_CHOICES, "Prompt builder character choices are not delegated")
|
|
_expect("21-year-old adult" in character_config.character_age_choices(), "Character age choices lost adult ages")
|
|
_expect("fat" in character_config.character_man_body_choices(), "Man body pool lost fat option")
|
|
_expect(pb.character_figure_choices() == character_config.character_figure_choices(), "Character figure choices should delegate")
|
|
_expect("platinum_blonde" in character_config.character_hair_color_choices(), "Hair color choices lost platinum blonde")
|
|
|
|
traits = json.loads(
|
|
pb.build_characteristics_config_json(
|
|
axis="bodies",
|
|
selected_values=["slim", "bad value", "slim", "fat"],
|
|
combine_mode="replace_axis",
|
|
)
|
|
)
|
|
_expect(traits.get("bodies") == ["slim", "fat"], "Character body trait normalization changed")
|
|
merged_traits = json.loads(
|
|
character_config.build_characteristics_config_json(
|
|
characteristics=traits,
|
|
axis="eyes",
|
|
selected_values=["blue", "gray-brown", "blue"],
|
|
combine_mode="add_to_axis",
|
|
)
|
|
)
|
|
_expect(merged_traits.get("bodies") == ["slim", "fat"], "Character trait merge lost existing axis")
|
|
_expect(merged_traits.get("eyes") == ["blue", "gray_brown"], "Character eye trait normalization changed")
|
|
_expect(pb._characteristic_choice({"ages": ["21-year-old adult"]}, "ages", random.Random(1)) == "21-year-old adult", "Trait choice changed")
|
|
|
|
hair = json.loads(
|
|
pb.build_hair_config_json(
|
|
axis="color",
|
|
selected_values=["platinum blonde", "bad", "dark-brown"],
|
|
combine_mode="replace_axis",
|
|
)
|
|
)
|
|
_expect(hair.get("colors") == ["platinum_blonde", "dark_brown"], "Hair color normalization changed")
|
|
hair = json.loads(
|
|
character_config.build_hair_config_json(
|
|
hair_config=hair,
|
|
axis="style",
|
|
selected_values=["messy bun", "straight"],
|
|
combine_mode="add_to_axis",
|
|
)
|
|
)
|
|
_expect(hair.get("styles") == ["messy_bun", "straight"], "Hair style config merge changed")
|
|
_expect(pb._hair_phrase_from_parts("platinum_blonde", "long", "messy_bun") == "long platinum-blonde hair in a messy bun", "Hair phrase helper changed")
|
|
_expect(character_config.normalize_presence_mode("pov", "woman") == "visible", "POV presence should stay man-only")
|
|
pov_slot = {"subject_type": "man", "presence_mode": "pov"}
|
|
visible_slot = {"subject_type": "man", "presence_mode": "visible"}
|
|
_expect(pb._slot_is_pov(pov_slot) is True, "Prompt builder POV slot helper should delegate to POV policy")
|
|
_expect(pov_policy.slot_is_pov(visible_slot) is False, "Visible man slot should not be POV")
|
|
_expect(
|
|
pb._pov_character_labels({"Man A": pov_slot, "Man B": visible_slot}, 2) == ["Man A"],
|
|
"POV label selection should keep only POV men in count order",
|
|
)
|
|
_expect(
|
|
pb._pov_role_graph_prompt("Man A is positioned behind Woman A", ["Man A"])
|
|
== "First-person POV from Man A; the POV camera is positioned behind Woman A",
|
|
"Builder POV role graph prompt should use shared viewer replacement",
|
|
)
|
|
_expect(
|
|
pb._pov_composition_prompt("wide group-sex composition with all bodies visible", ["Man A"])
|
|
== "first-person group-sex POV composition with visible partners readable",
|
|
"Builder POV composition prompt should use shared POV composition replacements",
|
|
)
|
|
_expect(
|
|
krea_pov.pov_composition_text(
|
|
"wide group-sex composition with all bodies visible, adapted for first-person POV with the POV participant kept off-camera",
|
|
["Man A"],
|
|
)
|
|
== "first-person group-sex POV composition with visible partners readable",
|
|
"Krea POV composition cleanup should delegate shared replacements and strip builder annotation",
|
|
)
|
|
_expect(character_config.normalize_slot_seed(0xFFFFFFFF + 99) == 0xFFFFFFFF, "Slot seed clamp changed")
|
|
slot_result = character_slot.build_character_slot_json(
|
|
subject_type="man",
|
|
label="Man B",
|
|
slot_seed=123,
|
|
age="manual",
|
|
manual_age="44-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="manual",
|
|
manual_body="stocky",
|
|
descriptor_detail="compact",
|
|
expression_intensity=1.5,
|
|
softcore_expression_intensity=0.25,
|
|
hardcore_expression_intensity=-1,
|
|
presence_mode="pov",
|
|
hair_color="dark brown",
|
|
hair_length="short",
|
|
hair_style="straight",
|
|
softcore_outfit="buttoned shirt",
|
|
hardcore_clothing="shirt pushed open",
|
|
)
|
|
_expect(
|
|
pb.build_character_slot_json(
|
|
subject_type="man",
|
|
label="Man B",
|
|
slot_seed=123,
|
|
age="manual",
|
|
manual_age="44-year-old adult",
|
|
ethnicity="western_european",
|
|
figure="balanced",
|
|
body="manual",
|
|
manual_body="stocky",
|
|
descriptor_detail="compact",
|
|
expression_intensity=1.5,
|
|
softcore_expression_intensity=0.25,
|
|
hardcore_expression_intensity=-1,
|
|
presence_mode="pov",
|
|
hair_color="dark brown",
|
|
hair_length="short",
|
|
hair_style="straight",
|
|
softcore_outfit="buttoned shirt",
|
|
hardcore_clothing="shirt pushed open",
|
|
)
|
|
== slot_result,
|
|
"Prompt builder character slot JSON should delegate to character_slot",
|
|
)
|
|
slot = json.loads(slot_result["character_slot"])
|
|
_expect(slot.get("age") == "44-year-old adult", "Character slot manual age normalization changed")
|
|
_expect(slot.get("body") == "stocky", "Character slot manual body normalization changed")
|
|
_expect(slot.get("presence_mode") == "pov", "Character slot POV presence normalization changed")
|
|
_expect(slot.get("expression_intensity") == 1.0, "Character slot expression intensity clamp changed")
|
|
_expect(
|
|
character_slot.slot_expression_intensity_for_phase(slot, "softcore") == 0.25
|
|
and character_slot.slot_expression_intensity_for_phase(slot, "hardcore") == 1.0,
|
|
"Character slot phase expression fallback changed",
|
|
)
|
|
_expect(
|
|
pb._slot_effective_figure({"slot_seed": 123, "figure": "random"}, "woman", "curvy")
|
|
== character_slot.slot_effective_figure({"slot_seed": 123, "figure": "random"}, "woman", "curvy"),
|
|
"Prompt builder seeded slot figure should delegate to character_slot",
|
|
)
|
|
_expect(
|
|
pb._appearance_for_subject(random.Random(9), "woman", "western_european", "balanced", False, False)
|
|
== character_appearance.appearance_for_subject(random.Random(9), "woman", "western_european", "balanced", False, False),
|
|
"Prompt builder appearance selection should delegate to character_appearance",
|
|
)
|
|
_expect(
|
|
pb._context_from_character_slot(random.Random(11), slot, "man", "any", "curvy", False, False)
|
|
== character_appearance.context_from_character_slot(random.Random(11), slot, "man", "any", "curvy", False, False),
|
|
"Prompt builder slot context should delegate to character_appearance",
|
|
)
|
|
_expect(
|
|
pb._row_from_character_slot(slot_result["character_slot"])
|
|
== character_appearance.row_from_character_slot(slot_result["character_slot"]),
|
|
"Prompt builder slot row conversion should delegate to character_appearance",
|
|
)
|
|
row = character_appearance.apply_character_context_to_row({}, {"age": "44-year-old adult", "body": "stocky"})
|
|
_expect(row.get("age_band") == "44-year-old adult" and row.get("body") == "stocky", "Character context row application changed")
|
|
|
|
|
|
def smoke_character_profile_policy() -> None:
|
|
_expect(pb.CHARACTER_MANUAL_FIELDS is character_profile.CHARACTER_MANUAL_FIELDS, "Prompt builder manual fields are not delegated")
|
|
_expect(pb.PROFILE_DIR == character_profile.PROFILE_DIR, "Prompt builder profile dir is not delegated")
|
|
_expect(pb._body_phrase("curvy", "hourglass figure") == "curvy build and hourglass figure", "Body phrase helper changed")
|
|
_expect(pb._safe_profile_name("bad name!*") == "bad_name", "Profile name sanitizer changed")
|
|
|
|
manual = json.loads(
|
|
pb.build_character_manual_config_json(
|
|
combine_mode="merge_nonempty",
|
|
manual_age="31-year-old adult",
|
|
body_phrase="custom body",
|
|
skin="warm skin",
|
|
softcore_outfit="red dress",
|
|
)
|
|
)
|
|
_expect(manual.get("manual_age") == "31-year-old adult", "Manual config lost age")
|
|
_expect(manual.get("softcore_outfit") == "red dress", "Manual config lost outfit")
|
|
_expect("manual_age=31-year-old adult" in manual.get("summary", ""), "Manual config summary changed")
|
|
|
|
metadata_row = {
|
|
"subject_type": "woman",
|
|
"age": "28-year-old adult",
|
|
"body": "curvy",
|
|
"body_phrase": "curvy figure with full hips",
|
|
"skin": "warm skin",
|
|
"hair": "long black hair",
|
|
"eyes": "brown eyes",
|
|
"figure": "balanced",
|
|
"descriptor_detail": "medium",
|
|
}
|
|
profile_result = character_profile.build_character_profile_json(
|
|
profile_name="smoke profile",
|
|
source="metadata_json",
|
|
metadata_json=metadata_row,
|
|
)
|
|
profile = json.loads(profile_result["profile_json"])
|
|
_expect(profile.get("profile_name") == "smoke_profile", "Profile name normalization changed")
|
|
_expect(profile.get("age") == "28-year-old adult", "Profile metadata extraction lost age")
|
|
_expect("long black hair" in profile_result["descriptor"], "Profile descriptor lost hair at medium detail")
|
|
|
|
loaded = pb.load_character_profile_json(
|
|
profile_name="manual",
|
|
fallback_profile_json=profile_result["profile_json"],
|
|
override_age="35-year-old adult",
|
|
override_descriptor_detail="compact",
|
|
)
|
|
loaded_profile = json.loads(loaded["profile_json"])
|
|
_expect(loaded.get("status") == "fallback", "Profile fallback load status changed")
|
|
_expect(loaded_profile.get("age") == "35-year-old adult", "Profile override age did not apply")
|
|
_expect(loaded_profile.get("descriptor_detail") == "compact", "Profile override descriptor detail did not apply")
|
|
|
|
context = {"subject_type": "woman", "subject": "woman", "subject_phrase": "woman", "age": "21-year-old adult"}
|
|
applied_context, applied_profile, status = pb._apply_character_profile_to_context(context, loaded_profile)
|
|
_expect(status == "applied", "Profile context application changed")
|
|
_expect(applied_context.get("age") == "35-year-old adult", "Profile context application lost age")
|
|
_expect(applied_profile.get("profile_type") == "character", "Profile context returned wrong profile")
|
|
|
|
|
|
def smoke_row_normalization_policy() -> None:
|
|
_expect(
|
|
pb._prepend_trigger("base prompt", Trigger, True) == row_normalization.prepend_trigger("base prompt", Trigger, True),
|
|
"Prompt builder trigger helper should delegate to row normalization policy",
|
|
)
|
|
_expect(
|
|
pb._combined_negative("bad anatomy", "low quality") == row_normalization.combined_negative("bad anatomy", "low quality"),
|
|
"Prompt builder negative helper should delegate to row normalization policy",
|
|
)
|
|
|
|
row = row_normalization.normalize_prompt_row(
|
|
{
|
|
"prompt": f"{Trigger}, {Trigger}, base prompt.",
|
|
"caption": f"{Trigger}, {Trigger}, base caption.",
|
|
"negative_prompt": "bad anatomy, bad anatomy",
|
|
},
|
|
active_trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="extra detail",
|
|
extra_negative="low quality, bad anatomy",
|
|
default_negative="bad anatomy",
|
|
)
|
|
_expect_trigger_once("row_normalization.prompt", row.get("prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.caption", row.get("caption"), Trigger)
|
|
_expect("extra detail" in row.get("prompt", ""), "Row normalization lost extra positive text")
|
|
_expect(row.get("trigger") == Trigger, "Row normalization lost active trigger")
|
|
_expect_no_duplicate_comma_items("row_normalization.negative", row.get("negative_prompt"))
|
|
|
|
legacy_couple = row_normalization.normalize_prompt_row(
|
|
{
|
|
"source": "built_in_generator",
|
|
"primary_subject": "two women",
|
|
"scene": "office",
|
|
"prompt": (
|
|
"Two adults in a clean legacy prompt. Scene: old room. "
|
|
"Pose: standing close, affectionate and flirtatious but non-explicit. "
|
|
"Facial expressions: one with a calm smile, the other with a side glance. "
|
|
"Clothing: coordinated satin outfits, resort styling. Composition: vertical old frame."
|
|
),
|
|
"caption": "legacy couple caption",
|
|
"negative_prompt": "bad anatomy",
|
|
},
|
|
active_trigger=Trigger,
|
|
prepend_trigger_to_prompt=False,
|
|
)
|
|
_expect(legacy_couple.get("subject_type") == "couple", "Legacy couple row lost normalized subject_type")
|
|
_expect(legacy_couple.get("women_count") == 2 and legacy_couple.get("men_count") == 0, "Legacy couple row lost normalized counts")
|
|
_expect(legacy_couple.get("person_count") == 2, "Legacy couple row lost normalized person count")
|
|
_expect(legacy_couple.get("scene_slug") == "office", "Legacy couple row lost scene slug")
|
|
_expect("cozy office desk" in str(legacy_couple.get("scene_text", "")), "Legacy couple row lost readable scene text")
|
|
_expect(legacy_couple.get("scene_entry", {}).get("slug") == "office", "Legacy couple row lost scene entry")
|
|
_expect(legacy_couple.get("pose") == "standing close", "Legacy couple row did not clean pose suffix")
|
|
_expect(legacy_couple.get("item") == "coordinated satin outfits", "Legacy couple row did not clean clothing suffix")
|
|
_expect(legacy_couple.get("item_label") == "Clothing", "Legacy couple row lost item label")
|
|
_expect("calm smile" in str(legacy_couple.get("expression", "")), "Legacy couple row lost expression metadata")
|
|
_expect("cast_summary" not in legacy_couple, "Legacy couple row should not gain configured-cast summary")
|
|
|
|
legacy_single = row_normalization.normalize_prompt_row(
|
|
{
|
|
"source": "built_in_generator",
|
|
"primary_subject": "woman",
|
|
"scene": "studio",
|
|
"age_band": "25-year-old adult",
|
|
"body_type": "curvy",
|
|
"figure": "soft curves, defined waist",
|
|
"caption": (
|
|
f"{Trigger}, woman, 25-year-old adult, curvy figure with soft curves, defined waist, "
|
|
"warm skin, short blonde hair, blue eyes, pose, expression, clothing, scene, composition"
|
|
),
|
|
"prompt": (
|
|
"A woman. Scene: old studio. Pose: standing calmly. "
|
|
"Facial expression: direct look. Clothing: fitted dress, fashion editorial styling. "
|
|
"Composition: vertical portrait."
|
|
),
|
|
"negative_prompt": "bad anatomy",
|
|
},
|
|
active_trigger=Trigger,
|
|
prepend_trigger_to_prompt=False,
|
|
)
|
|
_expect(legacy_single.get("body_phrase") == "curvy figure with soft curves, defined waist", "Legacy single row lost body phrase")
|
|
_expect(legacy_single.get("skin") == "warm skin", "Legacy single row lost skin metadata")
|
|
_expect(legacy_single.get("hair") == "short blonde hair", "Legacy single row lost hair metadata")
|
|
_expect(legacy_single.get("eyes") == "blue eyes", "Legacy single row lost eye metadata")
|
|
|
|
legacy_group = row_normalization.normalize_prompt_row(
|
|
{
|
|
"source": "built_in_generator",
|
|
"primary_subject": "mixed adult group",
|
|
"prompt": "Group legacy prompt.",
|
|
"caption": "legacy group caption",
|
|
"negative_prompt": "bad anatomy",
|
|
},
|
|
active_trigger=Trigger,
|
|
prepend_trigger_to_prompt=False,
|
|
)
|
|
_expect(legacy_group.get("subject_type") == "group", "Legacy group row lost normalized subject_type")
|
|
_expect(legacy_group.get("women_count") == 2 and legacy_group.get("men_count") == 2, "Legacy group row lost fallback counts")
|
|
_expect(legacy_group.get("person_count") == 4, "Legacy group row lost normalized person count")
|
|
|
|
legacy_layout = row_normalization.normalize_prompt_row(
|
|
{
|
|
"source": "built_in_generator",
|
|
"primary_subject": "layout scene",
|
|
"prompt": "Layout legacy prompt.",
|
|
"caption": "legacy layout caption",
|
|
"negative_prompt": "bad anatomy",
|
|
},
|
|
active_trigger=Trigger,
|
|
prepend_trigger_to_prompt=False,
|
|
)
|
|
_expect(legacy_layout.get("subject_type") == "layout", "Legacy layout row lost normalized subject_type")
|
|
_expect("women_count" not in legacy_layout and "men_count" not in legacy_layout, "Legacy layout row should not invent cast counts")
|
|
|
|
outputs = row_normalization.normalize_pair_text_outputs(
|
|
active_trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
extra_positive="pair extra",
|
|
extra_negative="low quality, bad anatomy",
|
|
soft_prompt="soft prompt.",
|
|
hard_prompt="hard prompt.",
|
|
soft_negative_base="bad anatomy, bad anatomy",
|
|
hard_negative_base="bad anatomy, low quality",
|
|
soft_caption_parts=[Trigger, "soft caption"],
|
|
hard_caption_parts=[Trigger, "hard caption"],
|
|
)
|
|
_expect_trigger_once("row_normalization.soft_prompt", outputs.get("soft_prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.hard_prompt", outputs.get("hard_prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.soft_caption", outputs.get("soft_caption"), Trigger)
|
|
_expect_trigger_once("row_normalization.hard_caption", outputs.get("hard_caption"), Trigger)
|
|
_expect_no_duplicate_comma_items("row_normalization.soft_negative", outputs.get("soft_negative"))
|
|
_expect_no_duplicate_comma_items("row_normalization.hard_negative", outputs.get("hard_negative"))
|
|
|
|
pair = row_normalization.normalize_pair_metadata(
|
|
{
|
|
"softcore_prompt": f"{Trigger}, {Trigger}, soft pair.",
|
|
"hardcore_prompt": f"{Trigger}, {Trigger}, hard pair.",
|
|
"softcore_caption": f"{Trigger}, {Trigger}, soft caption.",
|
|
"hardcore_caption": f"{Trigger}, {Trigger}, hard caption.",
|
|
"softcore_negative_prompt": "bad anatomy, bad anatomy",
|
|
"hardcore_negative_prompt": "bad anatomy, low quality, bad anatomy",
|
|
"softcore_partner_styling": {"outfits": ["partner outfit"], "pose": "partner pose"},
|
|
"hardcore_clothing_state": "structured hard clothing state",
|
|
"character_hardcore_clothing": ["Woman A custom hard clothing"],
|
|
"default_man_hardcore_clothing": ["Man A default hard clothing"],
|
|
"hardcore_detail_density": "dense",
|
|
"hardcore_position_config": {"family": "oral"},
|
|
"softcore_camera_config": {"camera_mode": "standard", "camera_detail": "compact"},
|
|
"hardcore_camera_config": {"camera_mode": "pov", "camera_detail": "dense"},
|
|
"softcore_camera_directive": "Camera: soft front view.",
|
|
"hardcore_camera_directive": "Camera: hard side view.",
|
|
"softcore_camera_scene_directive": "Soft camera-aware scene layout.",
|
|
"hardcore_camera_scene_directive": "Hard camera-aware scene layout.",
|
|
"softcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded soft.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded soft caption.",
|
|
"negative_prompt": "bad anatomy, bad anatomy",
|
|
"camera_directive": "stale soft camera",
|
|
},
|
|
"hardcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded hard.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded hard caption.",
|
|
"negative_prompt": "low quality, bad anatomy, low quality",
|
|
"camera_scene_directive": "stale hard camera scene",
|
|
},
|
|
},
|
|
active_trigger=Trigger,
|
|
)
|
|
_expect_trigger_once("row_normalization.pair.softcore_prompt", pair.get("softcore_prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.pair.hardcore_prompt", pair.get("hardcore_prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.pair.softcore_row.prompt", pair["softcore_row"].get("prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.pair.hardcore_row.caption", pair["hardcore_row"].get("caption"), Trigger)
|
|
_expect(
|
|
pair["softcore_row"].get("prompt") == pair.get("softcore_prompt"),
|
|
"Pair normalization left stale soft row prompt text",
|
|
)
|
|
_expect(
|
|
pair["hardcore_row"].get("caption") == pair.get("hardcore_caption"),
|
|
"Pair normalization left stale hard row caption text",
|
|
)
|
|
_expect(
|
|
pair["softcore_row"].get("softcore_partner_styling") == pair.get("softcore_partner_styling"),
|
|
"Pair normalization left stale soft side metadata",
|
|
)
|
|
_expect(
|
|
pair["hardcore_row"].get("hardcore_clothing_state") == pair.get("hardcore_clothing_state"),
|
|
"Pair normalization left stale hard clothing metadata",
|
|
)
|
|
_expect(
|
|
pair["hardcore_row"].get("default_man_hardcore_clothing") == pair.get("default_man_hardcore_clothing"),
|
|
"Pair normalization left stale hard default clothing metadata",
|
|
)
|
|
_expect(
|
|
pair["softcore_row"].get("camera_config") == pair.get("softcore_camera_config"),
|
|
"Pair normalization left stale soft camera config",
|
|
)
|
|
_expect(
|
|
pair["hardcore_row"].get("camera_config") == pair.get("hardcore_camera_config"),
|
|
"Pair normalization left stale hard camera config",
|
|
)
|
|
_expect(
|
|
pair["softcore_row"].get("camera_directive") == pair.get("softcore_camera_directive"),
|
|
"Pair normalization left stale soft camera directive",
|
|
)
|
|
_expect(
|
|
pair["hardcore_row"].get("camera_scene_directive") == pair.get("hardcore_camera_scene_directive"),
|
|
"Pair normalization left stale hard camera scene directive",
|
|
)
|
|
_expect_no_duplicate_comma_items("row_normalization.pair.soft_negative", pair.get("softcore_negative_prompt"))
|
|
_expect_no_duplicate_comma_items("row_normalization.pair.hard_row_negative", pair["hardcore_row"].get("negative_prompt"))
|
|
|
|
reverse_pair = row_normalization.normalize_pair_metadata(
|
|
{
|
|
"softcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded-only soft.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded-only soft caption.",
|
|
"negative_prompt": "bad anatomy, bad anatomy",
|
|
"softcore_partner_styling": {"outfits": ["row partner outfit"], "pose": "row partner pose"},
|
|
"camera_config": {"camera_mode": "standard"},
|
|
"camera_directive": "Camera: row soft front view.",
|
|
"camera_scene_directive": "Row soft scene camera layout.",
|
|
},
|
|
"hardcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded-only hard.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded-only hard caption.",
|
|
"negative_prompt": "low quality, low quality",
|
|
"hardcore_clothing_state": "row hard clothing state",
|
|
"character_hardcore_clothing": ["Woman A row hard clothing"],
|
|
"default_man_hardcore_clothing": ["Man A row default hard clothing"],
|
|
"hardcore_detail_density": "concise",
|
|
"hardcore_position_config": {"family": "outercourse"},
|
|
"camera_config": {"camera_mode": "pov"},
|
|
"camera_directive": "Camera: row hard side view.",
|
|
"camera_scene_directive": "Row hard scene camera layout.",
|
|
},
|
|
},
|
|
active_trigger=Trigger,
|
|
)
|
|
_expect_trigger_once("row_normalization.reverse.soft_prompt", reverse_pair.get("softcore_prompt"), Trigger)
|
|
_expect_trigger_once("row_normalization.reverse.hard_caption", reverse_pair.get("hardcore_caption"), Trigger)
|
|
_expect(
|
|
reverse_pair.get("softcore_partner_styling") == reverse_pair["softcore_row"].get("softcore_partner_styling"),
|
|
"Pair normalization did not lift soft side metadata from embedded row",
|
|
)
|
|
_expect(
|
|
reverse_pair.get("hardcore_clothing_state") == reverse_pair["hardcore_row"].get("hardcore_clothing_state"),
|
|
"Pair normalization did not lift hard side metadata from embedded row",
|
|
)
|
|
_expect(
|
|
reverse_pair.get("softcore_camera_config") == reverse_pair["softcore_row"].get("camera_config"),
|
|
"Pair normalization did not lift soft camera config from embedded row",
|
|
)
|
|
_expect(
|
|
reverse_pair.get("hardcore_camera_scene_directive") == reverse_pair["hardcore_row"].get("camera_scene_directive"),
|
|
"Pair normalization did not lift hard camera scene from embedded row",
|
|
)
|
|
_expect_no_duplicate_comma_items("row_normalization.reverse.hard_negative", reverse_pair.get("hardcore_negative_prompt"))
|
|
|
|
|
|
def smoke_prompt_hygiene_policy() -> None:
|
|
merged = prompt_hygiene.combine_negative_text(
|
|
"bad anatomy, bad anatomy",
|
|
"low quality",
|
|
"bad anatomy",
|
|
"",
|
|
)
|
|
_expect(merged == "bad anatomy, low quality", "Prompt hygiene negative merge/dedupe changed")
|
|
_expect(
|
|
row_normalization.combined_negative("bad anatomy, bad anatomy", "low quality, bad anatomy") == merged,
|
|
"Row normalization negative merge should delegate to prompt hygiene",
|
|
)
|
|
_expect(
|
|
krea_formatter._combine_negative("bad anatomy, bad anatomy", "low quality", "bad anatomy") == merged,
|
|
"Krea negative merge should delegate to prompt hygiene",
|
|
)
|
|
_expect(
|
|
prompt_hygiene.sanitize_prose_text("Scene: . A sentence. A sentence.") == "A sentence.",
|
|
"Prompt hygiene prose cleanup changed",
|
|
)
|
|
|
|
|
|
def smoke_row_rendering_policy() -> None:
|
|
_expect(pb.SINGLE_TEMPLATE == row_rendering.SINGLE_TEMPLATE, "Prompt builder single template should delegate to row_rendering")
|
|
_expect(
|
|
pb._format("Known {known}, missing {missing}", {"known": 7})
|
|
== row_rendering.format_template("Known {known}, missing {missing}", {"known": 7}),
|
|
"Prompt builder safe formatter should delegate to row_rendering",
|
|
)
|
|
_expect(
|
|
row_rendering.format_template("Known {known}, missing {missing}", {"known": 7}) == "Known 7, missing {missing}",
|
|
"Row rendering changed missing-field preservation",
|
|
)
|
|
_expect(
|
|
row_rendering.prompt_template_for({}, {}, {}, "woman") == row_rendering.SINGLE_TEMPLATE,
|
|
"Row rendering default woman template changed",
|
|
)
|
|
_expect(
|
|
row_rendering.prompt_template_for({}, {}, {}, "group") == row_rendering.GROUP_TEMPLATE,
|
|
"Row rendering default group template changed",
|
|
)
|
|
category_text = {
|
|
"name": "Category Label",
|
|
"negative_prompt": "category negative",
|
|
"positive_suffix": "category suffix",
|
|
"style": "category style",
|
|
"item_label": "Category Item",
|
|
}
|
|
subcategory_text = {
|
|
"negative_prompt": "subcategory negative",
|
|
"positive_suffix": "subcategory suffix",
|
|
"style": "subcategory style",
|
|
}
|
|
item_text = {
|
|
"negative_prompt": "item negative",
|
|
"style": "item style",
|
|
}
|
|
_expect(
|
|
pb._row_text_fields(category_text, subcategory_text, item_text)
|
|
== row_rendering.resolve_row_text_fields(category_text, subcategory_text, item_text),
|
|
"Prompt builder row text field wrapper should delegate to row_rendering",
|
|
)
|
|
text_fields = row_rendering.resolve_row_text_fields(category_text, subcategory_text, item_text)
|
|
_expect(text_fields.negative_prompt == "item negative", "Row text fields did not prefer item negative prompt")
|
|
_expect(text_fields.positive_suffix == "subcategory suffix", "Row text fields did not prefer subcategory suffix")
|
|
_expect(text_fields.style == "item style", "Row text fields did not prefer item style")
|
|
_expect(text_fields.item_label == "Category Item", "Row text fields did not fall back to category item label")
|
|
default_text_fields = row_rendering.resolve_row_text_fields({"name": "Default Category"}, {}, {})
|
|
_expect(default_text_fields.negative_prompt, "Row text fields lost default negative prompt")
|
|
_expect(default_text_fields.positive_suffix == row_rendering.GENERIC_POSITIVE_SUFFIX, "Row text fields lost suffix default")
|
|
_expect(default_text_fields.style == row_rendering.DEFAULT_STYLE, "Row text fields lost style default")
|
|
_expect(default_text_fields.item_label == "Default Category", "Row text fields lost category-name label default")
|
|
|
|
context = {
|
|
"trigger": Trigger,
|
|
"subject": "configured cast",
|
|
"subject_phrase": "configured adult cast",
|
|
"age": "adult",
|
|
"body": "varied",
|
|
"body_phrase": "varied",
|
|
"skin": "",
|
|
"hair": "",
|
|
"eyes": "",
|
|
"item_label": "Scene",
|
|
"item": "shared action",
|
|
"scene": "warm room",
|
|
"pose": "standing close",
|
|
"expression": "focused look",
|
|
"composition": "centered frame",
|
|
"composition_prompt": "vertical centered frame",
|
|
"positive_suffix": "clear readable bodies.",
|
|
"negative_prompt": "bad anatomy",
|
|
"cast_descriptors": "Woman A: adult woman; Man A: adult man",
|
|
}
|
|
rendered = row_rendering.render_prompt_caption(
|
|
item={},
|
|
subcategory={
|
|
"prompt_template": "Scene: {item}. Composition: {composition_prompt}. Avoid: {negative_prompt}.",
|
|
"caption_template": "{trigger}, {item}, {scene}",
|
|
},
|
|
category={},
|
|
subject_type="configured_cast",
|
|
context=context,
|
|
cast_descriptor_text="Woman A: adult woman; Man A: adult man",
|
|
pov_prompt_directive="First-person POV from Man A.",
|
|
)
|
|
prompt = rendered["prompt"]
|
|
caption = rendered["caption"]
|
|
_expect("Characters: Woman A: adult woman; Man A: adult man." in prompt, "Row rendering lost configured-cast descriptors")
|
|
_expect("First-person POV from Man A." in prompt, "Row rendering lost configured-cast POV directive")
|
|
_expect(
|
|
prompt.index("Characters:") < prompt.index("First-person POV") < prompt.index("Avoid:"),
|
|
"Row rendering did not insert configured-cast directives before negative prompt",
|
|
)
|
|
_expect(
|
|
caption.endswith("Woman A: adult woman; Man A: adult man"),
|
|
"Row rendering did not append descriptors to captions without descriptor placeholders",
|
|
)
|
|
|
|
|
|
def smoke_row_role_graph_policy() -> None:
|
|
empty_route = row_role_graph.resolve_role_graph_route(
|
|
rng=random.Random(51),
|
|
subcategory={"slug": "penetration"},
|
|
context={"subject_type": "woman"},
|
|
item_axis_values={"position": "missionary"},
|
|
pov_character_labels=[],
|
|
is_pose_category=True,
|
|
)
|
|
_expect(empty_route == row_role_graph.RoleGraphRoute("", ""), "Role graph route should stay empty outside configured cast")
|
|
|
|
context = {
|
|
"subject_type": "configured_cast",
|
|
"women_count": "1",
|
|
"men_count": "1",
|
|
}
|
|
subcategory = {"slug": "cumshot_climax", "name": "Cumshot and climax"}
|
|
axis_values = {"position": "lying at the bed edge with thighs open"}
|
|
route = row_role_graph.resolve_role_graph_route(
|
|
rng=random.Random(52),
|
|
subcategory=subcategory,
|
|
context=context,
|
|
item_axis_values=axis_values,
|
|
pov_character_labels=[],
|
|
is_pose_category=True,
|
|
)
|
|
delegated = pb._role_graph_route(
|
|
rng=random.Random(52),
|
|
subcategory=subcategory,
|
|
context=context,
|
|
item_axis_values=axis_values,
|
|
pov_character_labels=[],
|
|
is_pose_category=True,
|
|
)
|
|
_expect(route == delegated, "Prompt builder role graph route wrapper should delegate to row_role_graph")
|
|
_expect("raised edge" in route.source_role_graph, "Role graph route did not sanitize bed-edge environment anchor")
|
|
_expect("bed edge" not in route.source_role_graph.lower(), "Role graph route leaked bed-edge environment anchor")
|
|
_expect(route.role_graph == route.source_role_graph, "Role graph route changed non-POV role graph text")
|
|
|
|
pov_route = row_role_graph.resolve_role_graph_route(
|
|
rng=random.Random(53),
|
|
subcategory={"slug": "oral", "name": "Oral"},
|
|
context=context,
|
|
item_axis_values={"position": "standing oral", "act": "blowjob"},
|
|
pov_character_labels=["Man A"],
|
|
is_pose_category=False,
|
|
)
|
|
_expect(pov_route.source_role_graph, "Role graph route lost POV source role graph")
|
|
_expect(
|
|
pov_route.role_graph.startswith("First-person POV from Man A;"),
|
|
"Role graph route did not prepend POV role graph directive",
|
|
)
|
|
structured_axis_route = row_role_graph.resolve_role_graph_route(
|
|
rng=random.Random(54),
|
|
subcategory={"slug": "oral", "name": "Oral"},
|
|
context=context,
|
|
item_axis_values={
|
|
"position": {"text": "standing oral position"},
|
|
"oral_act": ["blowjob", "auto"],
|
|
"contact_detail": {"unused": "mouth contact at hip height"},
|
|
},
|
|
pov_character_labels=[],
|
|
is_pose_category=False,
|
|
)
|
|
_expect(
|
|
"kneels in front" in structured_axis_route.source_role_graph,
|
|
"Role graph route should read structured/list axis values through item_axis_policy",
|
|
)
|
|
|
|
|
|
def smoke_row_assembly_policy() -> None:
|
|
context = {
|
|
"subject": "configured cast",
|
|
"subject_phrase": "configured adult cast",
|
|
"age": "21+ adults",
|
|
"body": "varied adult builds",
|
|
"body_phrase": "varied adult builds",
|
|
"figure": "balanced cast",
|
|
"skin": "warm skin tones",
|
|
"hair": "dark hair",
|
|
"eyes": "brown eyes",
|
|
"cast_summary": "one woman and one man",
|
|
"scene_kind": "configured_cast",
|
|
"women_count": "1",
|
|
"men_count": "1",
|
|
"person_count": "2",
|
|
}
|
|
count_adjustment = {"requested_women_count": 1, "requested_men_count": 1}
|
|
kwargs = {
|
|
"row_number": 2,
|
|
"start_index": 10,
|
|
"category": {"name": "Axis Test", "slug": "axis_test"},
|
|
"subcategory": {
|
|
"name": "Custom Scene",
|
|
"slug": "custom_scene",
|
|
"prompt_template": "Scene: {item}. Composition: {composition_prompt}. Avoid: {negative_prompt}.",
|
|
"caption_template": "{trigger}, {item}, {scene}",
|
|
},
|
|
"item": {"text": "shared structured action"},
|
|
"context": context,
|
|
"subject_type": "configured_cast",
|
|
"item_text": "shared structured action",
|
|
"item_name": "shared_action",
|
|
"item_axis_values": {"action_family": "test_action"},
|
|
"item_template_metadata": {"position_key": "test_position"},
|
|
"formatter_hints": {"krea": ["test_hint"]},
|
|
"item_label": "Scene",
|
|
"style": "clean test style",
|
|
"positive_suffix": "clear readable composition.",
|
|
"negative_prompt": "bad anatomy",
|
|
"scene_slug": "test_room",
|
|
"scene": "warm test room",
|
|
"scene_entry": {"slug": "test_room", "prompt": "warm test room", "theme": "fixture_theme"},
|
|
"pose": "standing close",
|
|
"expression": "focused look",
|
|
"shared_expression": "focused look",
|
|
"character_expressions": ["Woman A has focused look"],
|
|
"character_expression_text": "Woman A has focused look",
|
|
"expression_disabled": True,
|
|
"expression_intensity": 0.7,
|
|
"expression_intensity_source": "disabled",
|
|
"composition": "centered frame",
|
|
"source_composition": "centered frame",
|
|
"composition_entry": {"prompt": "centered frame"},
|
|
"role_graph": "the visible partner stays centered",
|
|
"source_role_graph": "Man A stays centered",
|
|
"action_family": "test_action",
|
|
"position_family": "standing",
|
|
"position_key": "test_position",
|
|
"position_keys": ["test_position"],
|
|
"pov_character_labels": ["Man A"],
|
|
"cast_descriptors": ["Woman A: adult woman", "Man A: adult man"],
|
|
"cast_descriptor_text": "Woman A: adult woman; Man A: adult man",
|
|
"seed_config": {"content_seed": 123},
|
|
"hardcore_position_config": {"family": "standing"},
|
|
"location_config": {"location": "test_room", "theme": "fixture_theme", "apply_mode": "replace"},
|
|
"composition_config": {"composition": "centered", "theme": "fixture_theme"},
|
|
"content_seed_axis": "pose",
|
|
"count_adjustment": count_adjustment,
|
|
"applied_profile": {"name": "profile_a"},
|
|
"profile_status": "applied",
|
|
"applied_slot": {"label": "Woman A"},
|
|
"slot_status": "applied",
|
|
"character_slots": [{"label": "Woman A"}, {"label": "Man A"}],
|
|
}
|
|
request = row_assembly.CustomRowAssemblyRequest(**kwargs)
|
|
row = row_assembly.assemble_custom_row(request)
|
|
delegated = pb._assemble_custom_row(request)
|
|
|
|
_expect(row == delegated, "Prompt builder row assembly wrapper should delegate without changing output")
|
|
_expect(request.content_seed_axis == "pose", "Row assembly request lost seed-axis metadata")
|
|
_expect(row["id"] == "sxcp_0011", "Row assembly changed row indexing")
|
|
_expect(row["batch"] == "batch_001", "Row assembly changed batch calculation")
|
|
_expect(row["source"] == "json_category", "Row assembly lost source marker")
|
|
_expect(row["figure"] == "balanced cast", "Row assembly lost figure metadata")
|
|
_expect(row["formatter_hints"] == {"krea": ["test_hint"]}, "Row assembly lost formatter hints")
|
|
_expect(row["scene_entry"].get("slug") == "test_room", "Row assembly lost selected scene entry")
|
|
_expect(row["location_theme"] == "fixture_theme", "Row assembly lost location theme")
|
|
_expect(row["scene_theme"] == "fixture_theme", "Row assembly lost selected scene theme")
|
|
_expect(row["composition_entry"].get("prompt") == "centered frame", "Row assembly lost selected composition entry")
|
|
_expect(row["composition_theme"] == "fixture_theme", "Row assembly lost composition theme")
|
|
_expect(row["cast_count_adjustment"] == count_adjustment, "Row assembly lost configured-cast count adjustment")
|
|
_expect(row["content_seed_axis"] == "pose", "Row assembly lost content seed axis")
|
|
_expect("POV participant: Man A" in row["prompt"], "Row assembly lost POV prompt directive")
|
|
_expect("Characters: Woman A: adult woman; Man A: adult man." in row["prompt"], "Row assembly lost cast descriptor insertion")
|
|
_expect(row["caption"].endswith("Woman A: adult woman; Man A: adult man"), "Row assembly lost caption descriptor append")
|
|
_expect(row["expression"] == "", "Disabled expression should clear row expression")
|
|
_expect(row["expression_enabled"] is False, "Disabled expression should mark expression disabled")
|
|
_expect(row["expression_intensity"] is None, "Disabled expression should clear intensity")
|
|
_expect(row["expression_intensity_source"] == "disabled", "Disabled expression should preserve disabled source")
|
|
|
|
|
|
def smoke_formatter_input_policy() -> None:
|
|
source_row = {
|
|
"prompt": "A simple adult portrait. Setting: quiet studio. Pose: standing calmly. Avoid: low quality.",
|
|
"caption": "adult portrait, quiet studio",
|
|
"negative_prompt": "low quality",
|
|
"subject_type": "woman",
|
|
"primary_subject": "woman",
|
|
"age": "25-year-old adult",
|
|
"body_phrase": "average figure",
|
|
"skin": "warm skin",
|
|
"hair": "dark hair",
|
|
"eyes": "brown eyes",
|
|
"item": "black dress",
|
|
"scene_text": "quiet studio",
|
|
"pose": "standing calmly",
|
|
"composition": "centered portrait",
|
|
"trigger": Trigger,
|
|
}
|
|
source_json = _json(source_row)
|
|
|
|
_expect(
|
|
formatter_input.input_hint_choices(text_hint=formatter_input.INPUT_HINT_PROMPT) == ["auto", "metadata_json", "prompt"],
|
|
"Formatter prompt input-hint choices changed",
|
|
)
|
|
_expect(
|
|
formatter_input.input_hint_choices(text_hint=formatter_input.INPUT_HINT_CAPTION_OR_PROMPT)
|
|
== ["auto", "metadata_json", "caption_or_prompt"],
|
|
"Formatter caption input-hint choices changed",
|
|
)
|
|
_expect(
|
|
formatter_input.normalize_input_hint("bad_hint") == "auto",
|
|
"Formatter input-hint policy should normalize invalid values to auto",
|
|
)
|
|
_expect(
|
|
formatter_input.normalize_input_hint("caption", text_hint=formatter_input.INPUT_HINT_CAPTION_OR_PROMPT)
|
|
== "caption_or_prompt",
|
|
"Formatter input-hint policy lost caption alias",
|
|
)
|
|
_expect(
|
|
formatter_input.normalize_input_hint("caption_or_prompt", text_hint=formatter_input.INPUT_HINT_PROMPT) == "prompt",
|
|
"Formatter input-hint policy should map text hints to the route's text mode",
|
|
)
|
|
|
|
row, method = formatter_input.row_from_inputs(source_json, "", "auto")
|
|
_expect(method == "source_json", "Formatter input parser should read source JSON when metadata is empty")
|
|
_expect(row == source_row, "Formatter input parser changed parsed JSON row")
|
|
row, method = formatter_input.row_from_inputs(source_json, "", "bad_hint")
|
|
_expect(method == "source_json" and row == source_row, "Formatter input parser should treat invalid hints as auto")
|
|
row, method = formatter_input.row_from_inputs(source_json, "", "prompt")
|
|
_expect(row is None and method == "text", "Formatter input parser should not parse source JSON in explicit prompt mode")
|
|
pair_metadata = {
|
|
"trigger": Trigger,
|
|
"softcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded-only soft.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded-only soft caption.",
|
|
"negative_prompt": "bad anatomy, bad anatomy",
|
|
"softcore_partner_styling": {"outfits": ["row partner outfit"], "pose": "row partner pose"},
|
|
"camera_config": {"camera_mode": "standard"},
|
|
"camera_directive": "Camera: row soft front view.",
|
|
"camera_scene_directive": "Row soft scene camera layout.",
|
|
},
|
|
"hardcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded-only hard.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded-only hard caption.",
|
|
"negative_prompt": "low quality, low quality",
|
|
"hardcore_clothing_state": "row hard clothing state",
|
|
"camera_config": {"camera_mode": "pov"},
|
|
"camera_scene_directive": "Row hard scene camera layout.",
|
|
},
|
|
}
|
|
_expect(formatter_input.is_pair_metadata(pair_metadata), "Formatter input policy should detect structural pair metadata")
|
|
parsed_pair, pair_method = formatter_input.row_from_inputs("", _json(pair_metadata), "metadata_json")
|
|
_expect(pair_method == "metadata_json", "Formatter input parser should read pair metadata JSON")
|
|
_expect(formatter_input.is_pair_metadata(parsed_pair), "Formatter input parser should preserve structural pair metadata")
|
|
_expect_trigger_once("formatter_input.pair.soft_prompt", parsed_pair.get("softcore_prompt"), Trigger)
|
|
_expect(
|
|
parsed_pair.get("softcore_partner_styling") == parsed_pair["softcore_row"].get("softcore_partner_styling"),
|
|
"Formatter input parser did not normalize pair soft side metadata",
|
|
)
|
|
_expect(
|
|
parsed_pair.get("hardcore_clothing_state") == parsed_pair["hardcore_row"].get("hardcore_clothing_state"),
|
|
"Formatter input parser did not normalize pair hard side metadata",
|
|
)
|
|
_expect(
|
|
parsed_pair.get("softcore_camera_config") == parsed_pair["softcore_row"].get("camera_config"),
|
|
"Formatter input parser did not normalize pair camera metadata",
|
|
)
|
|
_expect_no_duplicate_comma_items("formatter_input.pair.hard_negative", parsed_pair.get("hardcore_negative_prompt"))
|
|
_expect(formatter_input.split_avoid("Prompt body. Avoid: blur, watermark") == ("Prompt body", "blur, watermark"), "Avoid split changed")
|
|
_expect(
|
|
formatter_input.prompt_field(source_row["prompt"], "Setting") == "quiet studio",
|
|
"Prompt field extraction changed",
|
|
)
|
|
_expect(
|
|
formatter_input.row_value({"prompt": source_row["prompt"]}, "scene_text", ("Setting",)) == "quiet studio",
|
|
"Row value prompt fallback changed",
|
|
)
|
|
_expect(
|
|
krea_formatter.PROMPT_FIELD_LABELS is formatter_input.DEFAULT_PROMPT_FIELD_LABELS,
|
|
"Krea formatter field-label inventory should delegate to formatter_input",
|
|
)
|
|
_expect(
|
|
sdxl_formatter.PROMPT_FIELD_LABELS is formatter_input.DEFAULT_PROMPT_FIELD_LABELS,
|
|
"SDXL formatter field-label inventory should delegate to formatter_input",
|
|
)
|
|
_expect(
|
|
caption_naturalizer.PROMPT_FIELD_LABELS is formatter_input.DEFAULT_PROMPT_FIELD_LABELS,
|
|
"Caption formatter field-label inventory should delegate to formatter_input",
|
|
)
|
|
labeled_prompt = "Sexual scene: close-contact action. Camera control: side view. Composition: centered frame."
|
|
_expect(
|
|
formatter_input.prompt_field(labeled_prompt, "Sexual scene") == "close-contact action",
|
|
"Shared formatter field-label inventory lost Sexual scene parsing",
|
|
)
|
|
_expect(
|
|
formatter_input.prompt_field(labeled_prompt, "Camera control") == "side view",
|
|
"Shared formatter field-label inventory lost Camera control parsing",
|
|
)
|
|
stripped_labels = formatter_input.strip_prompt_field_labels(
|
|
"Characters: woman. Erotic outfit: sheer dress. Camera: side view."
|
|
)
|
|
_expect("Characters:" not in stripped_labels, "Shared label stripper did not remove Characters label")
|
|
_expect("Erotic outfit:" not in stripped_labels, "Shared label stripper did not remove Erotic outfit label")
|
|
_expect("Camera:" not in stripped_labels, "Shared label stripper did not remove Camera label")
|
|
|
|
_expect(krea_formatter._clean("a b , c") == formatter_input.clean_text("a b , c"), "Krea clean helper is not delegated")
|
|
_expect(sdxl_formatter._clean("a b , c") == formatter_input.clean_text("a b , c"), "SDXL clean helper is not delegated")
|
|
_expect(
|
|
sdxl_formatter._strip_prompt_field_labels("Characters: woman. Camera: side view.") == "woman. side view.",
|
|
"SDXL label stripper should delegate to formatter_input",
|
|
)
|
|
_expect(caption_naturalizer._clean_text("a b , c") == formatter_input.clean_text("a b , c"), "Caption clean helper is not delegated")
|
|
_expect(krea_formatter._strip_trigger(f"{Trigger}, prompt text", False) == "prompt text", "Krea trigger stripping changed")
|
|
_expect(sdxl_formatter._strip_trigger(f"{SdxlTrigger}, prompt text", False) == "prompt text", "SDXL trigger stripping changed")
|
|
_expect(caption_naturalizer._remove_trigger(Trigger, Trigger) == "", "Caption exact-trigger removal changed")
|
|
|
|
krea = krea_formatter.format_krea2_prompt(source_json, input_hint="auto")
|
|
sdxl = sdxl_formatter.format_sdxl_prompt(source_json, input_hint="auto", trigger=SdxlTrigger, prepend_trigger=True)
|
|
caption, caption_method = caption_naturalizer.naturalize_caption(source_json, input_hint="auto", trigger=Trigger)
|
|
_expect(krea.get("method", "").startswith("source_json:krea2("), "Krea formatter did not use shared source JSON parsing")
|
|
_expect(sdxl.get("method", "").startswith("source_json:sdxl("), "SDXL formatter did not use shared source JSON parsing")
|
|
_expect(caption_method.startswith("source_json:metadata("), "Caption naturalizer did not use shared source JSON parsing")
|
|
_expect_text("formatter_input.krea_prompt", krea.get("krea_prompt"), 20)
|
|
_expect_text("formatter_input.sdxl_prompt", sdxl.get("sdxl_prompt"), 20)
|
|
_expect_text("formatter_input.caption", caption, 20)
|
|
|
|
bad_hint_krea = krea_formatter.format_krea2_prompt(source_json, input_hint="bad_hint")
|
|
bad_hint_sdxl = sdxl_formatter.format_sdxl_prompt(
|
|
source_json,
|
|
input_hint="bad_hint",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
bad_hint_caption, bad_hint_caption_method = caption_naturalizer.naturalize_caption(
|
|
source_json,
|
|
input_hint="bad_hint",
|
|
trigger=Trigger,
|
|
)
|
|
_expect(
|
|
bad_hint_krea.get("method", "").startswith("source_json:krea2("),
|
|
"Krea formatter did not normalize bad input hint to auto",
|
|
)
|
|
_expect(
|
|
bad_hint_sdxl.get("method", "").startswith("source_json:sdxl("),
|
|
"SDXL formatter did not normalize bad input hint to auto",
|
|
)
|
|
_expect(
|
|
bad_hint_caption_method.startswith("source_json:metadata("),
|
|
"Caption formatter did not normalize bad input hint to auto",
|
|
)
|
|
|
|
fallback_sdxl = sdxl_formatter.format_sdxl_prompt(
|
|
"Characters: woman. Erotic outfit: sheer dress. Camera: side view. Avoid: blur",
|
|
input_hint="prompt",
|
|
style_preset="none",
|
|
quality_preset="none",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=False,
|
|
)
|
|
fallback_prompt = fallback_sdxl.get("sdxl_prompt", "")
|
|
_expect("Characters:" not in fallback_prompt, "SDXL fallback leaked Characters label")
|
|
_expect("Erotic outfit:" not in fallback_prompt, "SDXL fallback leaked Erotic outfit label")
|
|
_expect("Camera:" not in fallback_prompt, "SDXL fallback leaked Camera label")
|
|
_expect("blur" in fallback_sdxl.get("negative_prompt", ""), "SDXL fallback lost Avoid negative text")
|
|
|
|
|
|
def smoke_formatter_target_policy() -> None:
|
|
_expect(
|
|
formatter_target.target_choices() == ["auto", "single", "softcore", "hardcore"],
|
|
"Formatter target choices changed",
|
|
)
|
|
_expect(formatter_target.normalize_target("single") == "single", "Formatter target lost single")
|
|
_expect(formatter_target.normalize_target("Hard-Core") == "hardcore", "Formatter target alias lost hardcore")
|
|
_expect(formatter_target.normalize_target("soft") == "softcore", "Formatter target alias lost softcore")
|
|
_expect(formatter_target.normalize_target("bad target") == "auto", "Formatter target should normalize invalid values")
|
|
|
|
auto_pair = formatter_target.pair_policy("auto")
|
|
_expect(auto_pair.target == "auto", "Pair target policy lost normalized auto target")
|
|
_expect(auto_pair.pair_target == "auto", "Pair target policy lost auto pair target")
|
|
_expect(auto_pair.selected_side == "softcore", "Pair auto should select softcore side for single-output formatters")
|
|
_expect(auto_pair.include_softcore and auto_pair.include_hardcore, "Pair auto should include both sides for combined captions")
|
|
|
|
single_pair = formatter_target.pair_policy("single")
|
|
_expect(single_pair.target == "single", "Pair target policy lost normalized single target")
|
|
_expect(single_pair.pair_target == "auto", "Pair single should map to auto for pair inclusion")
|
|
_expect(single_pair.selected_side == "softcore", "Pair single should select softcore side by default")
|
|
_expect(single_pair.include_softcore and single_pair.include_hardcore, "Pair single should include both sides when treated as auto")
|
|
|
|
hard_pair = formatter_target.pair_policy("hard")
|
|
_expect(hard_pair.target == "hardcore", "Pair target policy lost hard alias")
|
|
_expect(hard_pair.pair_target == "hardcore", "Pair hard alias should become hardcore pair target")
|
|
_expect(hard_pair.selected_side == "hardcore", "Pair hardcore should select hardcore side")
|
|
_expect(not hard_pair.include_softcore and hard_pair.include_hardcore, "Pair hardcore should include only hard side")
|
|
|
|
|
|
def smoke_formatter_detail_policy() -> None:
|
|
_expect(
|
|
formatter_detail.detail_level_choices() == ["balanced", "concise", "dense"],
|
|
"Formatter detail choices changed",
|
|
)
|
|
_expect(formatter_detail.normalize_detail_level("dense") == "dense", "Formatter detail lost dense")
|
|
_expect(formatter_detail.normalize_detail_level("BAD") == "balanced", "Formatter detail should normalize invalid values")
|
|
_expect(formatter_detail.detail_allows("concise") is False, "Formatter detail concise gate changed")
|
|
_expect(formatter_detail.detail_allows("dense", dense_only=True) is True, "Formatter detail dense-only gate changed")
|
|
_expect(caption_policy.DETAIL_LEVELS is formatter_detail.DETAIL_LEVELS, "Caption detail levels should delegate")
|
|
_expect(
|
|
caption_policy.normalize_detail_level("bad") == formatter_detail.normalize_detail_level("bad"),
|
|
"Caption detail normalization should delegate",
|
|
)
|
|
_expect(
|
|
caption_policy.detail_allows("dense", dense_only=True) == formatter_detail.detail_allows("dense", dense_only=True),
|
|
"Caption detail gate should delegate",
|
|
)
|
|
|
|
|
|
def smoke_krea_format_route_policy() -> None:
|
|
row = _prompt_row(
|
|
name="krea_format_route_single",
|
|
category="woman",
|
|
subcategory="random",
|
|
seed=3601,
|
|
men_count=0,
|
|
camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
|
)
|
|
single_request = krea_format_route.KreaFormatRequest(
|
|
source_text="",
|
|
metadata_json=_json(row),
|
|
target="single",
|
|
detail_level="dense",
|
|
style_mode="photographic",
|
|
extra_positive="krea route marker",
|
|
extra_negative="krea route negative",
|
|
)
|
|
typed_single = krea_format_route.format_krea2_prompt_result(
|
|
single_request,
|
|
krea_formatter._krea_format_dependencies(),
|
|
)
|
|
public_single = krea_formatter.format_krea2_prompt(
|
|
"",
|
|
metadata_json=single_request.metadata_json,
|
|
target=single_request.target,
|
|
detail_level=single_request.detail_level,
|
|
style_mode=single_request.style_mode,
|
|
extra_positive=single_request.extra_positive,
|
|
extra_negative=single_request.extra_negative,
|
|
)
|
|
_expect(typed_single.output == public_single, "Typed Krea format route should match public single formatter output")
|
|
_expect(typed_single.branch == "metadata(single)", "Typed Krea format route changed single branch")
|
|
_expect(typed_single.target == "single", "Typed Krea format route lost target normalization")
|
|
single_trace = json.loads(_expect_text("krea_format_route_policy.single_trace", typed_single.output.get("route_trace_json"), 20))
|
|
_expect(single_trace.get("formatter") == "krea2", "Typed Krea single trace lost formatter")
|
|
_expect(single_trace.get("branch") == "metadata(single)", "Typed Krea single trace lost branch")
|
|
_expect(single_trace.get("target") == "single", "Typed Krea single trace lost target")
|
|
_expect("krea route marker" in typed_single.output.get("krea_prompt", ""), "Typed Krea route lost extra positive")
|
|
_expect("krea route negative" in typed_single.output.get("negative_prompt", ""), "Typed Krea route lost extra negative")
|
|
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3602,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
pair_request = krea_format_route.KreaFormatRequest(
|
|
source_text="",
|
|
metadata_json=_json(pair),
|
|
target="hardcore",
|
|
detail_level="balanced",
|
|
style_mode="preserve",
|
|
extra_positive="pair route marker",
|
|
extra_negative="pair route negative",
|
|
)
|
|
typed_pair = krea_format_route.format_krea2_prompt_result(
|
|
pair_request,
|
|
krea_formatter._krea_format_dependencies(),
|
|
)
|
|
public_pair = krea_formatter.format_krea2_prompt(
|
|
"",
|
|
metadata_json=pair_request.metadata_json,
|
|
target=pair_request.target,
|
|
detail_level=pair_request.detail_level,
|
|
style_mode=pair_request.style_mode,
|
|
extra_positive=pair_request.extra_positive,
|
|
extra_negative=pair_request.extra_negative,
|
|
)
|
|
_expect(typed_pair.output == public_pair, "Typed Krea format route should match public pair formatter output")
|
|
_expect(typed_pair.branch == "insta_of_pair", "Typed Krea format route changed pair branch")
|
|
pair_trace = json.loads(_expect_text("krea_format_route_policy.pair_trace", typed_pair.output.get("route_trace_json"), 20))
|
|
_expect(pair_trace.get("branch") == "insta_of_pair", "Typed Krea pair trace lost branch")
|
|
_expect(pair_trace.get("selected_side") == "hardcore", "Typed Krea pair trace lost selected side")
|
|
_expect_text("krea_format_route_policy.hard_prompt", typed_pair.output.get("krea_hardcore_prompt"), 40)
|
|
_expect("pair route marker" in typed_pair.output.get("krea_prompt", ""), "Typed Krea pair route lost extra positive")
|
|
|
|
fallback_request = krea_format_route.KreaFormatRequest(
|
|
source_text="Scene: quiet studio. Pose: seated portrait. Avoid: blur",
|
|
input_hint="prompt",
|
|
target="weird",
|
|
detail_level="verbose",
|
|
style_mode="invalid",
|
|
preserve_trigger=False,
|
|
)
|
|
typed_fallback = krea_format_route.format_krea2_prompt_result(
|
|
fallback_request,
|
|
krea_formatter._krea_format_dependencies(),
|
|
)
|
|
public_fallback = krea_formatter.format_krea2_prompt(
|
|
fallback_request.source_text,
|
|
input_hint=fallback_request.input_hint,
|
|
target=fallback_request.target,
|
|
detail_level=fallback_request.detail_level,
|
|
style_mode=fallback_request.style_mode,
|
|
preserve_trigger=fallback_request.preserve_trigger,
|
|
)
|
|
_expect(typed_fallback.output == public_fallback, "Typed Krea format route should match public fallback output")
|
|
_expect(typed_fallback.branch == "fallback", "Typed Krea format route changed fallback branch")
|
|
_expect(typed_fallback.target == "auto", "Typed Krea format route should normalize invalid target")
|
|
fallback_trace = json.loads(_expect_text("krea_format_route_policy.fallback_trace", typed_fallback.output.get("route_trace_json"), 20))
|
|
_expect(fallback_trace.get("branch") == "fallback", "Typed Krea fallback trace lost branch")
|
|
_expect(fallback_trace.get("input_hint") == "prompt", "Typed Krea fallback trace lost input hint")
|
|
_expect(typed_fallback.detail_level == "balanced", "Typed Krea format route should normalize invalid detail level")
|
|
_expect(typed_fallback.style_mode == "preserve", "Typed Krea format route should normalize invalid style mode")
|
|
_expect(krea_format_route.style_mode_choices() == ["preserve", "photographic", "minimal"], "Krea style mode choices changed")
|
|
_expect(krea_format_route.normalize_style_mode("photographic") == "photographic", "Krea style mode lost photographic")
|
|
_expect(krea_format_route.normalize_style_mode("bad") == "preserve", "Krea style mode invalid fallback changed")
|
|
_expect("blur" in typed_fallback.output.get("negative_prompt", ""), "Typed Krea fallback route lost Avoid negative")
|
|
|
|
|
|
def smoke_formatter_cast_policy() -> None:
|
|
descriptor = (
|
|
"Woman A / primary creator: 25-year-old adult woman, average figure, warm skin, dark hair; "
|
|
"Man A: 40-year-old adult man, average figure, tan skin, short dark hair"
|
|
)
|
|
entries = [
|
|
("Woman A", "25-year-old adult woman, average figure, warm skin, dark hair"),
|
|
("Man A", "40-year-old adult man, average figure, tan skin, short dark hair"),
|
|
]
|
|
_expect(krea_cast.cast_entries(descriptor) == entries, "Shared cast entry parser changed")
|
|
_expect(caption_naturalizer._cast_entries(descriptor) == entries, "Caption cast parser should delegate to shared cast policy")
|
|
_expect(krea_cast.cast_labels(descriptor) == ["Woman A", "Man A"], "Shared cast label parser changed")
|
|
_expect(
|
|
caption_naturalizer._cast_labels(descriptor) == krea_cast.cast_labels(descriptor),
|
|
"Caption cast labels should delegate to shared cast policy",
|
|
)
|
|
natural = krea_cast.natural_cast_descriptor_text(descriptor)
|
|
_expect(natural.startswith("A 25-year-old adult woman"), "Shared natural cast descriptor text changed")
|
|
_expect(caption_naturalizer._natural_cast_descriptor_text(descriptor) == natural, "Caption cast descriptor text should delegate")
|
|
_expect(
|
|
krea_cast.natural_label_text("Woman A faces Man A.", ["Woman A", "Man A"]) == "The woman faces the man.",
|
|
"Krea natural label text should keep sentence capitalization",
|
|
)
|
|
_expect(
|
|
caption_naturalizer._natural_label_text("Woman A faces Man A.", ["Woman A", "Man A"]) == "the woman faces the man.",
|
|
"Caption natural label text should preserve previous lowercase inline behavior",
|
|
)
|
|
|
|
|
|
def smoke_caption_policy() -> None:
|
|
_expect(
|
|
caption_naturalizer.STYLE_TAILS is caption_policy.STYLE_TAILS,
|
|
"Caption naturalizer style tails should delegate to caption_policy",
|
|
)
|
|
_expect(
|
|
caption_naturalizer.ITEM_LABELS is caption_policy.ITEM_LABELS,
|
|
"Caption naturalizer item labels should delegate to caption_policy",
|
|
)
|
|
_expect(
|
|
caption_naturalizer.ACTION_FAMILY_CAPTION_LABELS is caption_policy.ACTION_FAMILY_CAPTION_LABELS,
|
|
"Caption naturalizer action labels should delegate to caption_policy",
|
|
)
|
|
_expect(caption_policy.normalize_detail_level("bad") == "balanced", "Caption invalid detail fallback changed")
|
|
_expect(caption_policy.normalize_style_policy("bad") == "drop_style_tail", "Caption invalid style fallback changed")
|
|
_expect(
|
|
caption_policy.normalize_style_policy("Keep Style Terms") == "keep_style_terms",
|
|
"Caption style policy should normalize spaces/case",
|
|
)
|
|
_expect(caption_policy.style_policy_choices() == ["drop_style_tail", "keep_style_terms"], "Caption style policy choices changed")
|
|
_expect(caption_policy.keep_style_terms("keep_style_terms") is True, "Caption style policy keep flag changed")
|
|
_expect(caption_policy.detail_allows("concise") is False, "Caption concise detail gate changed")
|
|
_expect(caption_policy.detail_allows("dense", dense_only=True) is True, "Caption dense-only gate changed")
|
|
_expect("training_concise" in caption_policy.caption_profile_choices(), "Caption profile choices lost training_concise")
|
|
_expect(
|
|
caption_policy.normalize_caption_profile("bad") == caption_policy.CAPTION_PROFILE_DEFAULT,
|
|
"Caption invalid profile fallback changed",
|
|
)
|
|
_expect(
|
|
caption_policy.normalize_caption_profile("training-dense") == "training_dense",
|
|
"Caption profile should normalize hyphen spelling",
|
|
)
|
|
_expect(
|
|
caption_policy.apply_caption_profile(
|
|
"training_dense",
|
|
detail_level="concise",
|
|
style_policy="keep_style_terms",
|
|
include_trigger=False,
|
|
)
|
|
== ("dense", "drop_style_tail", True),
|
|
"Caption training_dense profile overrides changed",
|
|
)
|
|
_expect(
|
|
caption_policy.apply_caption_profile(
|
|
"manual_controls",
|
|
detail_level="concise",
|
|
style_policy="keep_style_terms",
|
|
include_trigger=False,
|
|
)
|
|
== ("concise", "keep_style_terms", False),
|
|
"Caption manual profile should preserve explicit controls",
|
|
)
|
|
|
|
style_tail = caption_policy.STYLE_TAILS[0]
|
|
_expect(
|
|
caption_policy.strip_style_tail(f"caption body{style_tail}") == "caption body",
|
|
"Caption style-tail stripping changed",
|
|
)
|
|
_expect(
|
|
caption_naturalizer._strip_style_tail(f"caption body{style_tail}") == "caption body",
|
|
"Caption naturalizer style-tail wrapper should delegate",
|
|
)
|
|
_expect(
|
|
caption_policy.normalize_composition("vertical centered body frame") == "centered body frame",
|
|
"Caption composition normalization changed",
|
|
)
|
|
_expect(
|
|
caption_policy.clean_clothing("silk dress, fashion editorial styling") == "silk dress",
|
|
"Caption clothing cleanup changed",
|
|
)
|
|
row = {"action_family": "oral", "position_family": ""}
|
|
_expect(caption_policy.metadata_action_label(row) == "oral action", "Caption action-family label changed")
|
|
row = {"action_family": "oral", "position_family": "Anal"}
|
|
_expect(caption_naturalizer._metadata_action_label(row) == "anal action", "Caption position-family label priority changed")
|
|
browsing_caption, browsing_method = caption_naturalizer.naturalize_caption(
|
|
"woman, red dress, studio",
|
|
caption_profile="browsing",
|
|
include_trigger=True,
|
|
)
|
|
_expect(not browsing_caption.startswith(Trigger), "Caption browsing profile should disable trigger by default")
|
|
_expect(browsing_method == "text(fallback)", "Caption browsing profile changed fallback method")
|
|
|
|
|
|
def smoke_caption_format_route_policy() -> None:
|
|
row = _prompt_row(
|
|
name="caption_format_route_single",
|
|
category="woman",
|
|
subcategory="random",
|
|
seed=3801,
|
|
men_count=0,
|
|
)
|
|
metadata_request = caption_format_route.CaptionFormatRequest(
|
|
source_text="",
|
|
metadata_json=_json(row),
|
|
input_hint="metadata_json",
|
|
target="single",
|
|
trigger=Trigger,
|
|
include_trigger=False,
|
|
detail_level="concise",
|
|
style_policy="keep_style_terms",
|
|
caption_profile="training_dense",
|
|
)
|
|
typed_metadata = caption_format_route.naturalize_caption_result(
|
|
metadata_request,
|
|
caption_naturalizer._caption_format_dependencies(),
|
|
)
|
|
public_metadata = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=metadata_request.metadata_json,
|
|
input_hint=metadata_request.input_hint,
|
|
target=metadata_request.target,
|
|
trigger=metadata_request.trigger,
|
|
include_trigger=metadata_request.include_trigger,
|
|
detail_level=metadata_request.detail_level,
|
|
style_policy=metadata_request.style_policy,
|
|
caption_profile=metadata_request.caption_profile,
|
|
)
|
|
_expect(typed_metadata.as_tuple() == public_metadata, "Typed caption format route should match public metadata output")
|
|
_expect(typed_metadata.branch == "metadata", "Typed caption format route changed metadata branch")
|
|
_expect(typed_metadata.input_hint == "metadata_json", "Typed caption route lost input hint")
|
|
_expect(typed_metadata.target == "single", "Typed caption route lost target normalization")
|
|
_expect(typed_metadata.detail_level == "dense", "Typed caption route lost training_dense detail override")
|
|
_expect(typed_metadata.style_policy == "drop_style_tail", "Typed caption route lost training_dense style override")
|
|
_expect(typed_metadata.include_trigger is True, "Typed caption route lost training_dense trigger override")
|
|
metadata_trace = json.loads(_expect_text("caption_format_route_policy.metadata_trace", typed_metadata.route_trace_json, 20))
|
|
_expect(metadata_trace.get("formatter") == "caption", "Typed caption metadata trace lost formatter")
|
|
_expect(metadata_trace.get("branch") == "metadata", "Typed caption metadata trace lost branch")
|
|
_expect(metadata_trace.get("target") == "single", "Typed caption metadata trace lost target")
|
|
traced_public_metadata = caption_naturalizer.naturalize_caption_with_trace(
|
|
"",
|
|
metadata_json=metadata_request.metadata_json,
|
|
input_hint=metadata_request.input_hint,
|
|
target=metadata_request.target,
|
|
trigger=metadata_request.trigger,
|
|
include_trigger=metadata_request.include_trigger,
|
|
detail_level=metadata_request.detail_level,
|
|
style_policy=metadata_request.style_policy,
|
|
caption_profile=metadata_request.caption_profile,
|
|
)
|
|
_expect(traced_public_metadata == typed_metadata.as_trace_tuple(), "Caption trace wrapper drifted from typed route")
|
|
_expect(typed_metadata.caption.startswith(Trigger), "Typed caption metadata route should prepend training trigger")
|
|
|
|
fallback_request = caption_format_route.CaptionFormatRequest(
|
|
source_text="woman, red dress, studio, coloured pencil comic illustration",
|
|
input_hint="bad_hint",
|
|
target="weird",
|
|
trigger=Trigger,
|
|
include_trigger=True,
|
|
detail_level="dense",
|
|
style_policy="drop_style_tail",
|
|
caption_profile="browsing",
|
|
)
|
|
typed_fallback = caption_format_route.naturalize_caption_result(
|
|
fallback_request,
|
|
caption_naturalizer._caption_format_dependencies(),
|
|
)
|
|
public_fallback = caption_naturalizer.naturalize_caption(
|
|
fallback_request.source_text,
|
|
input_hint=fallback_request.input_hint,
|
|
target=fallback_request.target,
|
|
trigger=fallback_request.trigger,
|
|
include_trigger=fallback_request.include_trigger,
|
|
detail_level=fallback_request.detail_level,
|
|
style_policy=fallback_request.style_policy,
|
|
caption_profile=fallback_request.caption_profile,
|
|
)
|
|
_expect(typed_fallback.as_tuple() == public_fallback, "Typed caption format route should match public fallback output")
|
|
_expect(typed_fallback.branch == "text", "Typed caption format route changed fallback branch")
|
|
_expect(typed_fallback.input_hint == "auto", "Typed caption route should normalize invalid input hint")
|
|
_expect(typed_fallback.target == "auto", "Typed caption route should normalize invalid target")
|
|
_expect(typed_fallback.include_trigger is False, "Typed caption browsing profile should disable trigger")
|
|
fallback_trace = json.loads(_expect_text("caption_format_route_policy.fallback_trace", typed_fallback.route_trace_json, 20))
|
|
_expect(fallback_trace.get("branch") == "text", "Typed caption fallback trace lost branch")
|
|
_expect(fallback_trace.get("input_hint") == "auto", "Typed caption fallback trace lost input hint")
|
|
_expect(typed_fallback.keep_style is True, "Typed caption browsing profile should keep style terms")
|
|
_expect(not typed_fallback.caption.startswith(Trigger), "Typed caption fallback route should not prepend browsing trigger")
|
|
_expect(typed_fallback.method == "text(fallback)", "Typed caption fallback method changed")
|
|
|
|
|
|
def smoke_caption_text_policy() -> None:
|
|
row = {
|
|
"primary_subject": "woman",
|
|
"age_band": "25-year-old adult",
|
|
"body_phrase": "slim figure",
|
|
"caption": f"{Trigger}, woman, 25-year-old adult, slim figure, fair skin, blonde hair, blue eyes, studio",
|
|
"formatter_hints": {"caption": ["caption policy hint"]},
|
|
}
|
|
_expect(
|
|
caption_naturalizer._body_phrase("slim", "balanced figure") == caption_text_policy.body_phrase("slim", "balanced figure"),
|
|
"Caption body phrase wrapper should delegate to caption_text_policy",
|
|
)
|
|
_expect(
|
|
caption_naturalizer._single_caption_front(row) == caption_text_policy.single_caption_front(row),
|
|
"Caption front parser wrapper should delegate to caption_text_policy",
|
|
)
|
|
_expect(
|
|
caption_naturalizer._formatter_hint_parts(row) == caption_text_policy.formatter_hint_parts(row),
|
|
"Caption formatter hint wrapper should delegate to caption_text_policy",
|
|
)
|
|
_expect(
|
|
caption_naturalizer._append_formatter_hints("Base sentence.", row)
|
|
== caption_text_policy.append_formatter_hints("Base sentence.", row),
|
|
"Caption formatter hint append wrapper should delegate to caption_text_policy",
|
|
)
|
|
_expect(
|
|
caption_naturalizer._with_trigger("A caption body", Trigger, True)
|
|
== caption_text_policy.with_trigger("A caption body", Trigger, True),
|
|
"Caption trigger wrapper should delegate to caption_text_policy",
|
|
)
|
|
axis_detail_row = {
|
|
"item_axis_values": {
|
|
"position": "standing oral position",
|
|
"contact_detail": "mouth contact at hip height",
|
|
"duplicate": "standing oral position",
|
|
"ignored": "random",
|
|
}
|
|
}
|
|
_expect(
|
|
caption_text_policy.item_axis_detail_text(axis_detail_row, "generic action")
|
|
== "standing oral position and mouth contact at hip height",
|
|
"Caption axis detail text should flatten selected item axes",
|
|
)
|
|
_expect(
|
|
caption_text_policy.item_axis_detail_text(axis_detail_row, "standing oral position already appears")
|
|
== "mouth contact at hip height",
|
|
"Caption axis detail text should skip details already present in item prose",
|
|
)
|
|
deps = caption_naturalizer._caption_metadata_route_dependencies()
|
|
_expect(deps.clean_text is caption_text_policy.clean_text, "Caption route deps lost clean text policy")
|
|
_expect(deps.field_row_value is caption_text_policy.field_row_value, "Caption route deps lost field row-value policy")
|
|
_expect(deps.expression_disabled is caption_text_policy.expression_disabled, "Caption route deps lost expression policy")
|
|
_expect(deps.single_caption_front is caption_text_policy.single_caption_front, "Caption route deps lost front parser")
|
|
_expect(deps.item_axis_detail_text is caption_text_policy.item_axis_detail_text, "Caption route deps lost item-axis detail policy")
|
|
_expect(deps.metadata_to_prose is caption_naturalizer._metadata_to_prose, "Caption route deps lost metadata recursion callback")
|
|
|
|
|
|
def _expect_caption_route_parity(
|
|
name: str,
|
|
row: dict[str, Any],
|
|
route_builder: Callable[
|
|
[caption_metadata_routes.CaptionMetadataRouteRequest, caption_metadata_routes.CaptionMetadataRouteDependencies],
|
|
caption_metadata_routes.CaptionMetadataRoute | None,
|
|
],
|
|
wrapper: Callable[[dict[str, Any], str, bool], tuple[str, str] | None],
|
|
expected_method: str,
|
|
) -> None:
|
|
request = caption_naturalizer._caption_metadata_route_request(row, "balanced", False)
|
|
deps = caption_naturalizer._caption_metadata_route_dependencies()
|
|
typed_route = route_builder(request, deps)
|
|
legacy_route = wrapper(row, "balanced", False)
|
|
_expect(typed_route is not None, f"{name} typed caption metadata route did not match")
|
|
assert typed_route is not None
|
|
_expect(
|
|
typed_route.as_tuple() == legacy_route,
|
|
f"{name} typed caption metadata route should match legacy wrapper output",
|
|
)
|
|
_expect(typed_route.method == expected_method, f"{name} caption route method changed")
|
|
_expect_text(f"{name}.caption_route", typed_route.prose, 20)
|
|
|
|
|
|
def smoke_caption_metadata_routes() -> None:
|
|
single = {
|
|
"primary_subject": "woman",
|
|
"age_band": "25-year-old adult",
|
|
"body_phrase": "slim figure",
|
|
"skin": "fair skin",
|
|
"hair": "long blonde hair",
|
|
"eyes": "blue eyes",
|
|
"item": "silk dress",
|
|
"pose": "standing beside a window",
|
|
"scene_text": "quiet studio with warm daylight",
|
|
"expression": "soft smile",
|
|
"composition": "vertical centered portrait",
|
|
}
|
|
_expect_caption_route_parity(
|
|
"caption_route_single",
|
|
single,
|
|
caption_metadata_routes.single_from_row_result,
|
|
caption_naturalizer._single_from_row,
|
|
"metadata(single)",
|
|
)
|
|
|
|
couple = {
|
|
"primary_subject": "a woman and a man",
|
|
"subject_phrase": "woman and man",
|
|
"age": "25-year-old adult and 40-year-old adult",
|
|
"body": "slim and average builds",
|
|
"item": "Partner A wears black dress; Partner B wears dark shirt",
|
|
"pose": "standing close together",
|
|
"scene_text": "private lounge with soft lamps",
|
|
"expression": "shared confident gaze",
|
|
"composition": "two-person editorial frame",
|
|
}
|
|
_expect_caption_route_parity(
|
|
"caption_route_couple",
|
|
couple,
|
|
caption_metadata_routes.couple_from_row_result,
|
|
caption_naturalizer._couple_from_row,
|
|
"metadata(couple)",
|
|
)
|
|
|
|
configured = _fixture_hardcore_row()
|
|
_expect_caption_route_parity(
|
|
"caption_route_configured_cast",
|
|
configured,
|
|
caption_metadata_routes.configured_cast_from_row_result,
|
|
caption_naturalizer._configured_cast_from_row,
|
|
"metadata(configured_cast)",
|
|
)
|
|
configured_axis_only = _fixture_hardcore_row(
|
|
item="generic configured adult action",
|
|
role_graph="",
|
|
source_role_graph="",
|
|
item_axis_values={
|
|
"position": "standing oral position",
|
|
"contact_detail": "mouth contact at hip height, hands on hips",
|
|
},
|
|
action_family="oral",
|
|
position_family="oral",
|
|
position_key="standing",
|
|
position_keys=["standing"],
|
|
)
|
|
axis_route = caption_metadata_routes.configured_cast_from_row_result(
|
|
caption_naturalizer._caption_metadata_route_request(configured_axis_only, "balanced", False),
|
|
caption_naturalizer._caption_metadata_route_dependencies(),
|
|
)
|
|
_expect(axis_route is not None, "Caption configured-cast axis-only row did not match")
|
|
assert axis_route is not None
|
|
_expect("Selected action details include" in axis_route.prose, "Caption route did not emit selected axis details")
|
|
_expect("standing oral position" in axis_route.prose, "Caption route lost item-axis position detail")
|
|
_expect("mouth contact at hip height" in axis_route.prose, "Caption route lost item-axis contact detail")
|
|
_expect("hands on hips" in axis_route.prose, "Caption route lost item-axis split detail")
|
|
|
|
group = {
|
|
"primary_subject": "group scene",
|
|
"subject_phrase": "three adult friends",
|
|
"age": "late 20s adults",
|
|
"item": "coordinated evening outfits",
|
|
"scene_text": "rooftop lounge with city lights",
|
|
"expression": "relaxed shared smiles",
|
|
"composition": "wide group frame",
|
|
}
|
|
_expect_caption_route_parity(
|
|
"caption_route_group",
|
|
group,
|
|
caption_metadata_routes.group_or_layout_from_row_result,
|
|
caption_naturalizer._group_or_layout_from_row,
|
|
"metadata(group_layout)",
|
|
)
|
|
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3511,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(hardcore_clothing_continuity="partially_removed"),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
_expect_pair(pair, "caption_route_pair")
|
|
_expect_caption_route_parity(
|
|
"caption_route_insta_pair",
|
|
pair,
|
|
caption_metadata_routes.insta_of_pair_from_row_result,
|
|
caption_naturalizer._insta_of_pair_from_row,
|
|
"metadata(insta_of_pair)",
|
|
)
|
|
deps = caption_naturalizer._caption_metadata_route_dependencies()
|
|
soft_route = caption_metadata_routes.insta_of_pair_from_row_result(
|
|
caption_naturalizer._caption_metadata_route_request(pair, "balanced", False, target="softcore"),
|
|
deps,
|
|
)
|
|
hard_route = caption_metadata_routes.insta_of_pair_from_row_result(
|
|
caption_naturalizer._caption_metadata_route_request(pair, "balanced", False, target="hardcore"),
|
|
deps,
|
|
)
|
|
_expect(soft_route is not None, "Caption pair softcore target did not match")
|
|
_expect(hard_route is not None, "Caption pair hardcore target did not match")
|
|
assert soft_route is not None
|
|
assert hard_route is not None
|
|
_expect("Softcore side:" not in soft_route.prose, "Caption softcore target should not keep combined pair labels")
|
|
_expect("Hardcore side:" not in soft_route.prose, "Caption softcore target should not include hard label")
|
|
_expect("Softcore side:" not in hard_route.prose, "Caption hardcore target should not include soft label")
|
|
_expect("Hardcore side:" not in hard_route.prose, "Caption hardcore target should not keep combined pair labels")
|
|
_expect(soft_route.prose != hard_route.prose, "Caption pair soft/hard targets should produce distinct prose")
|
|
public_hard, public_hard_method = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=_json(pair),
|
|
input_hint="metadata_json",
|
|
target="hardcore",
|
|
trigger=Trigger,
|
|
include_trigger=False,
|
|
)
|
|
_expect(public_hard == hard_route.prose, "Public caption hardcore target drifted from typed route")
|
|
_expect("metadata(insta_of_pair)" in public_hard_method, "Public caption hardcore target lost pair method")
|
|
|
|
|
|
def smoke_sdxl_presets_policy() -> None:
|
|
_expect(
|
|
sdxl_formatter.SDXL_STYLE_PRESETS is sdxl_presets.SDXL_STYLE_PRESETS,
|
|
"SDXL formatter style presets should delegate to sdxl_presets",
|
|
)
|
|
_expect(
|
|
sdxl_formatter.SDXL_QUALITY_PRESETS is sdxl_presets.SDXL_QUALITY_PRESETS,
|
|
"SDXL formatter quality presets should delegate to sdxl_presets",
|
|
)
|
|
_expect(
|
|
sdxl_formatter.SDXL_FORMATTER_PROFILES is sdxl_presets.SDXL_FORMATTER_PROFILES,
|
|
"SDXL formatter profiles should delegate to sdxl_presets",
|
|
)
|
|
_expect(
|
|
sdxl_formatter.SDXL_ACTION_FAMILY_TAGS is sdxl_presets.SDXL_ACTION_FAMILY_TAGS,
|
|
"SDXL formatter action-family tags should delegate to sdxl_presets",
|
|
)
|
|
_expect("sdxl_photo" in sdxl_presets.sdxl_formatter_profile_choices(), "SDXL profile choices lost sdxl_photo")
|
|
_expect("flat_vector_pony" in sdxl_presets.sdxl_style_preset_choices(), "SDXL style preset choices lost default")
|
|
_expect("pony_high" in sdxl_presets.sdxl_quality_preset_choices(), "SDXL quality preset choices lost default")
|
|
_expect(
|
|
sdxl_presets.normalize_formatter_profile("bad") == sdxl_presets.DEFAULT_FORMATTER_PROFILE,
|
|
"SDXL invalid profile fallback changed",
|
|
)
|
|
_expect(sdxl_presets.normalize_formatter_profile("SDXL Photo") == "sdxl_photo", "SDXL profile should normalize spaces/case")
|
|
_expect(sdxl_presets.normalize_style_preset("bad") == sdxl_presets.DEFAULT_STYLE_PRESET, "SDXL invalid style fallback changed")
|
|
_expect(sdxl_presets.normalize_style_preset("flat-vector-pony") == "flat_vector_pony", "SDXL style should normalize hyphens")
|
|
_expect(sdxl_presets.normalize_quality_preset("bad") == sdxl_presets.DEFAULT_QUALITY_PRESET, "SDXL invalid quality fallback changed")
|
|
_expect(sdxl_presets.normalize_quality_preset("Pony High") == "pony_high", "SDXL quality should normalize spaces/case")
|
|
_expect(
|
|
sdxl_presets.apply_formatter_profile(
|
|
"sdxl_photo",
|
|
style_preset="flat_vector_pony",
|
|
quality_preset="pony_high",
|
|
)
|
|
== ("photographic", "sdxl_high"),
|
|
"SDXL photo profile overrides changed",
|
|
)
|
|
_expect(
|
|
sdxl_presets.apply_formatter_profile(
|
|
"manual_controls",
|
|
style_preset="flat_vector",
|
|
quality_preset="none",
|
|
)
|
|
== ("flat_vector", "none"),
|
|
"SDXL manual profile should preserve explicit controls",
|
|
)
|
|
|
|
row = _fixture_hardcore_row(
|
|
action_family="oral",
|
|
position_family="oral",
|
|
position_key="kneeling_oral",
|
|
position_keys=["kneeling_oral"],
|
|
)
|
|
tags = sdxl_formatter._metadata_family_tags(row)
|
|
_expect("oral sex" in tags, "SDXL metadata family tags lost oral family tag")
|
|
_expect("kneeling oral" in tags, "SDXL metadata family tags lost position key tag")
|
|
formatted = sdxl_formatter.format_sdxl_prompt(
|
|
_json(row),
|
|
input_hint="auto",
|
|
style_preset="bad",
|
|
quality_preset="bad",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
_expect_trigger_once("sdxl_presets.formatted_prompt", formatted.get("sdxl_prompt"), SdxlTrigger)
|
|
_expect("Flat vector" in formatted.get("sdxl_prompt", ""), "SDXL invalid style did not fall back to default preset")
|
|
_expect("score_9" in formatted.get("sdxl_prompt", ""), "SDXL invalid quality did not fall back to default preset")
|
|
profiled = sdxl_formatter.format_sdxl_prompt(
|
|
_json(row),
|
|
input_hint="auto",
|
|
formatter_profile="sdxl_photo",
|
|
style_preset="flat_vector_pony",
|
|
quality_preset="pony_high",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
profiled_prompt = profiled.get("sdxl_prompt", "")
|
|
_expect("realistic photo" in profiled_prompt, "SDXL photo profile did not apply photographic style")
|
|
_expect("score_9" not in profiled_prompt, "SDXL photo profile should switch away from Pony score quality tail")
|
|
|
|
|
|
def smoke_sdxl_format_route_policy() -> None:
|
|
row = _prompt_row(
|
|
name="sdxl_format_route_single",
|
|
category="woman",
|
|
subcategory="random",
|
|
seed=3701,
|
|
men_count=0,
|
|
camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
|
)
|
|
single_request = sdxl_format_route.SDXLFormatRequest(
|
|
source_text="",
|
|
metadata_json=_json(row),
|
|
target="single",
|
|
style_preset="flat_vector_pony",
|
|
quality_preset="pony_high",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
nude_weight=9.0,
|
|
extra_positive="sdxl route marker",
|
|
extra_negative="sdxl route negative",
|
|
formatter_profile="sdxl_photo",
|
|
)
|
|
typed_single = sdxl_format_route.format_sdxl_prompt_result(
|
|
single_request,
|
|
sdxl_formatter._sdxl_format_dependencies(),
|
|
)
|
|
public_single = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=single_request.metadata_json,
|
|
target=single_request.target,
|
|
style_preset=single_request.style_preset,
|
|
quality_preset=single_request.quality_preset,
|
|
trigger=single_request.trigger,
|
|
prepend_trigger=single_request.prepend_trigger,
|
|
nude_weight=single_request.nude_weight,
|
|
extra_positive=single_request.extra_positive,
|
|
extra_negative=single_request.extra_negative,
|
|
formatter_profile=single_request.formatter_profile,
|
|
)
|
|
_expect(typed_single.output == public_single, "Typed SDXL format route should match public single formatter output")
|
|
_expect(typed_single.branch == "metadata", "Typed SDXL format route changed single branch")
|
|
_expect(typed_single.target == "single", "Typed SDXL format route lost target normalization")
|
|
_expect(typed_single.nude_weight == 3.0, "Typed SDXL format route should clamp high nude weight")
|
|
_expect(typed_single.style_preset == "photographic", "Typed SDXL format route lost profile style override")
|
|
single_trace = json.loads(_expect_text("sdxl_format_route_policy.single_trace", typed_single.output.get("route_trace_json"), 20))
|
|
_expect(single_trace.get("formatter") == "sdxl", "Typed SDXL single trace lost formatter")
|
|
_expect(single_trace.get("branch") == "metadata", "Typed SDXL single trace lost branch")
|
|
_expect(single_trace.get("target") == "single", "Typed SDXL single trace lost target")
|
|
_expect(single_trace.get("nude_weight") == 3.0, "Typed SDXL single trace lost clamped nude weight")
|
|
_expect("sdxl route marker" in typed_single.output.get("sdxl_prompt", ""), "Typed SDXL route lost extra positive")
|
|
_expect("sdxl route negative" in typed_single.output.get("negative_prompt", ""), "Typed SDXL route lost extra negative")
|
|
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3702,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
pair_request = sdxl_format_route.SDXLFormatRequest(
|
|
source_text="",
|
|
metadata_json=_json(pair),
|
|
target="hardcore",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
extra_positive="pair sdxl route marker",
|
|
extra_negative="pair sdxl route negative",
|
|
)
|
|
typed_pair = sdxl_format_route.format_sdxl_prompt_result(
|
|
pair_request,
|
|
sdxl_formatter._sdxl_format_dependencies(),
|
|
)
|
|
public_pair = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=pair_request.metadata_json,
|
|
target=pair_request.target,
|
|
trigger=pair_request.trigger,
|
|
prepend_trigger=pair_request.prepend_trigger,
|
|
extra_positive=pair_request.extra_positive,
|
|
extra_negative=pair_request.extra_negative,
|
|
)
|
|
_expect(typed_pair.output == public_pair, "Typed SDXL format route should match public pair formatter output")
|
|
_expect(typed_pair.branch == "insta_of_pair", "Typed SDXL format route changed pair branch")
|
|
pair_trace = json.loads(_expect_text("sdxl_format_route_policy.pair_trace", typed_pair.output.get("route_trace_json"), 20))
|
|
_expect(pair_trace.get("branch") == "insta_of_pair", "Typed SDXL pair trace lost branch")
|
|
_expect(pair_trace.get("selected_side") == "hardcore", "Typed SDXL pair trace lost selected side")
|
|
_expect_text("sdxl_format_route_policy.hard_prompt", typed_pair.output.get("sdxl_hardcore_prompt"), 40)
|
|
_expect("pair sdxl route marker" in typed_pair.output.get("sdxl_prompt", ""), "Typed SDXL pair route lost extra positive")
|
|
|
|
fallback_request = sdxl_format_route.SDXLFormatRequest(
|
|
source_text="Characters: woman. Erotic outfit: sheer dress. Camera: side view. Avoid: blur",
|
|
input_hint="prompt",
|
|
target="weird",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=False,
|
|
nude_weight=0.01,
|
|
style_preset="none",
|
|
quality_preset="none",
|
|
)
|
|
typed_fallback = sdxl_format_route.format_sdxl_prompt_result(
|
|
fallback_request,
|
|
sdxl_formatter._sdxl_format_dependencies(),
|
|
)
|
|
public_fallback = sdxl_formatter.format_sdxl_prompt(
|
|
fallback_request.source_text,
|
|
input_hint=fallback_request.input_hint,
|
|
target=fallback_request.target,
|
|
trigger=fallback_request.trigger,
|
|
prepend_trigger=fallback_request.prepend_trigger,
|
|
nude_weight=fallback_request.nude_weight,
|
|
style_preset=fallback_request.style_preset,
|
|
quality_preset=fallback_request.quality_preset,
|
|
)
|
|
_expect(typed_fallback.output == public_fallback, "Typed SDXL format route should match public fallback output")
|
|
_expect(typed_fallback.branch == "fallback", "Typed SDXL format route changed fallback branch")
|
|
_expect(typed_fallback.target == "auto", "Typed SDXL format route should normalize invalid target")
|
|
_expect(typed_fallback.nude_weight == 0.1, "Typed SDXL format route should clamp low nude weight")
|
|
fallback_trace = json.loads(_expect_text("sdxl_format_route_policy.fallback_trace", typed_fallback.output.get("route_trace_json"), 20))
|
|
_expect(fallback_trace.get("branch") == "fallback", "Typed SDXL fallback trace lost branch")
|
|
_expect(fallback_trace.get("input_hint") == "prompt", "Typed SDXL fallback trace lost input hint")
|
|
_expect("Characters:" not in typed_fallback.output.get("sdxl_prompt", ""), "Typed SDXL fallback leaked Characters label")
|
|
_expect("blur" in typed_fallback.output.get("negative_prompt", ""), "Typed SDXL fallback route lost Avoid negative")
|
|
|
|
|
|
def smoke_sdxl_tag_policy() -> None:
|
|
row = _fixture_hardcore_row(
|
|
action_family="oral",
|
|
position_family="oral",
|
|
position_key="kneeling_oral",
|
|
position_keys=["kneeling_oral"],
|
|
formatter_hints={"sdxl": ["policy route tag"]},
|
|
)
|
|
_expect(
|
|
sdxl_formatter._split_tag_text("Woman A with camera, Man A")
|
|
== sdxl_tag_policy.split_tag_text("Woman A with camera, Man A"),
|
|
"SDXL formatter split helper should delegate to sdxl_tag_policy",
|
|
)
|
|
_expect(
|
|
sdxl_formatter._metadata_family_tags(row) == sdxl_tag_policy.metadata_family_tags(row),
|
|
"SDXL formatter metadata-family helper should delegate to sdxl_tag_policy",
|
|
)
|
|
axis_row = {
|
|
"item_axis_values": {
|
|
"position": "edge-supported kneeling pose",
|
|
"contact_detail": "hands braced on thighs, close body alignment",
|
|
"ignored": "random",
|
|
}
|
|
}
|
|
axis_tags = sdxl_tag_policy.axis_value_tags(axis_row)
|
|
_expect("edge-supported kneeling pose" in axis_tags, "SDXL axis tags lost selected position axis")
|
|
_expect("hands braced on thighs" in axis_tags, "SDXL axis tags lost selected detail axis")
|
|
_expect("close body alignment" in axis_tags, "SDXL axis tags lost split detail axis")
|
|
_expect("random" not in axis_tags, "SDXL axis tags should ignore random placeholders")
|
|
_expect(
|
|
sdxl_formatter._camera_tags(row) == sdxl_tag_policy.camera_tags(row),
|
|
"SDXL formatter camera helper should delegate to sdxl_tag_policy",
|
|
)
|
|
_expect(
|
|
sdxl_formatter._combine_tags("a, b", "a", "c")
|
|
== sdxl_tag_policy.combine_tags("a, b", "a", "c")
|
|
== "a, b, c",
|
|
"SDXL tag combining changed",
|
|
)
|
|
deps = sdxl_formatter._sdxl_tag_route_dependencies()
|
|
_expect(deps.tag_key is sdxl_tag_policy.tag_key, "SDXL route deps lost policy tag_key")
|
|
_expect(deps.normal_character_tags is sdxl_tag_policy.normal_character_tags, "SDXL route deps lost character tag policy")
|
|
_expect(deps.metadata_family_tags is sdxl_tag_policy.metadata_family_tags, "SDXL route deps lost metadata family policy")
|
|
_expect(deps.axis_value_tags is sdxl_tag_policy.axis_value_tags, "SDXL route deps lost axis-value tag policy")
|
|
_expect(deps.camera_tags is sdxl_tag_policy.camera_tags, "SDXL route deps lost camera tag policy")
|
|
_expect(deps.explicit_tags is sdxl_tag_policy.explicit_tags, "SDXL route deps lost explicit tag policy")
|
|
_expect(
|
|
deps.filter_incompatible_route_tags is sdxl_tag_policy.filter_incompatible_route_tags,
|
|
"SDXL route deps lost route-family tag filter",
|
|
)
|
|
_expect(deps.softcore_pair_tags is sdxl_tag_policy.softcore_pair_tags, "SDXL route deps lost softcore pair tag policy")
|
|
mouth_nearby_tags = sdxl_tag_policy.explicit_tags(
|
|
"missionary penetration with mouth close to the ear",
|
|
1.29,
|
|
)
|
|
_expect("penetration" in mouth_nearby_tags, "SDXL explicit tags lost penetration signal")
|
|
_expect("oral sex" not in mouth_nearby_tags, "SDXL explicit tags should not treat nearby mouth wording as oral")
|
|
outercourse_filtered_tags = sdxl_tag_policy.filter_incompatible_route_tags(
|
|
["outercourse", "penis licking", "oral sex", "penetration"],
|
|
_fixture_hardcore_row(
|
|
action_family="outercourse",
|
|
position_family="outercourse",
|
|
position_key="penis_licking",
|
|
position_keys=["penis_licking"],
|
|
),
|
|
)
|
|
_expect("outercourse" in outercourse_filtered_tags, "SDXL route filter removed matching outercourse tag")
|
|
_expect("penis licking" in outercourse_filtered_tags, "SDXL route filter removed specific outercourse key")
|
|
_expect("oral sex" not in outercourse_filtered_tags, "SDXL route filter kept incompatible oral tag")
|
|
_expect("penetration" not in outercourse_filtered_tags, "SDXL route filter kept incompatible penetration tag")
|
|
|
|
stale_character_row = {
|
|
"prompt": "Characters: 99-year-old adult man, stale body, stale skin, stale hair, stale eyes.",
|
|
"age_band": "27-year-old adult",
|
|
"subject_phrase": "woman",
|
|
"body_phrase": "athletic figure",
|
|
"skin": "warm olive skin",
|
|
"hair": "short black hair",
|
|
"eyes": "green eyes",
|
|
}
|
|
stale_character_tags = sdxl_tag_policy.normal_character_tags(stale_character_row)
|
|
_expect("27-year-old adult" in stale_character_tags, "SDXL character tags lost structured age")
|
|
_expect("warm olive skin" in stale_character_tags, "SDXL character tags lost structured skin")
|
|
_expect(
|
|
all("stale" not in tag for tag in stale_character_tags),
|
|
"SDXL character tags should not parse stale raw prompt character labels",
|
|
)
|
|
descriptor_tags = sdxl_tag_policy.normal_character_tags(
|
|
{
|
|
"prompt": "Characters: stale prompt descriptor.",
|
|
"cast_descriptor_text": "Woman A: 30-year-old adult woman, toned figure, fair skin, red hair, gray eyes",
|
|
}
|
|
)
|
|
_expect("30-year-old adult woman" in descriptor_tags, "SDXL character tags lost cast descriptor metadata")
|
|
_expect("toned build" in descriptor_tags, "SDXL character tags did not normalize descriptor figure tag")
|
|
_expect(all("stale" not in tag for tag in descriptor_tags), "SDXL cast descriptor metadata should beat stale prompt labels")
|
|
|
|
|
|
def smoke_sdxl_tag_routes() -> None:
|
|
row = _fixture_hardcore_row(
|
|
formatter_hints={
|
|
"all": ["shared route anchor"],
|
|
"sdxl": ["sdxl route tag"],
|
|
}
|
|
)
|
|
deps = sdxl_formatter._sdxl_tag_route_dependencies()
|
|
typed_row = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(row, 1.29),
|
|
deps,
|
|
)
|
|
_expect(
|
|
typed_row.tags == sdxl_formatter._row_core_tags(row, 1.29),
|
|
"Typed SDXL row tag route should match legacy wrapper output",
|
|
)
|
|
_expect("sdxl route tag" in typed_row.as_text(), "Typed SDXL row tag route lost route-specific formatter hint")
|
|
axis_only_row = _fixture_hardcore_row(
|
|
item="generic configured adult action",
|
|
pose="configured explicit pose",
|
|
role_graph="",
|
|
source_role_graph="",
|
|
item_axis_values={
|
|
"position": "standing oral position",
|
|
"contact_detail": "mouth contact at hip height, hands on hips",
|
|
},
|
|
action_family="oral",
|
|
position_family="oral",
|
|
position_key="standing",
|
|
position_keys=["standing"],
|
|
)
|
|
axis_only_tags = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(axis_only_row, 1.29),
|
|
deps,
|
|
).as_text()
|
|
_expect("standing oral position" in axis_only_tags, "SDXL row route lost item axis position tag")
|
|
_expect("mouth contact at hip height" in axis_only_tags, "SDXL row route lost item axis contact tag")
|
|
_expect("hands on hips" in axis_only_tags, "SDXL row route lost split item axis detail tag")
|
|
stale_character_route_row = _fixture_hardcore_row(
|
|
prompt="Characters: stale prompt subject, stale body, stale skin, stale hair, stale eyes.",
|
|
cast_descriptor_text="",
|
|
cast_descriptors=[],
|
|
subject_type="woman",
|
|
subject_phrase="woman",
|
|
primary_subject="woman",
|
|
age_band="27-year-old adult",
|
|
body_phrase="athletic figure",
|
|
skin="warm olive skin",
|
|
hair="short black hair",
|
|
eyes="green eyes",
|
|
women_count=1,
|
|
men_count=0,
|
|
person_count=1,
|
|
)
|
|
stale_character_route_tags = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(stale_character_route_row, 1.29),
|
|
deps,
|
|
).as_text()
|
|
_expect("27-year-old adult" in stale_character_route_tags, "SDXL route lost structured character age")
|
|
_expect("warm olive skin" in stale_character_route_tags, "SDXL route lost structured character skin")
|
|
_expect("stale" not in stale_character_route_tags, "SDXL route should not parse stale prompt character labels")
|
|
stale_prompt_row = _fixture_hardcore_row(
|
|
prompt="stale raw prompt mentions fully nude naked pussy penis oral anal semen penetration",
|
|
item="standing portrait setup",
|
|
pose="standing pose",
|
|
role_graph="",
|
|
source_role_graph="",
|
|
expression="neutral expression",
|
|
action_family="",
|
|
position_family="",
|
|
position_key="",
|
|
position_keys=[],
|
|
)
|
|
stale_tags = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(stale_prompt_row, 1.29),
|
|
deps,
|
|
).as_text()
|
|
for forbidden in ("(naked:", "pussy", "penis", "oral sex", "anal sex", "semen", "penetration"):
|
|
_expect(forbidden not in stale_tags, f"SDXL row tags should not infer explicit tag from stale prompt: {forbidden}")
|
|
|
|
metadata_explicit_row = _fixture_hardcore_row(
|
|
prompt="stale raw prompt without explicit tag anchors",
|
|
item="kneeling pose with visible penis and pussy contact",
|
|
pose="penetration pose",
|
|
role_graph="penis thrusts into pussy",
|
|
source_role_graph="penis thrusts into pussy",
|
|
hardcore_clothing_state="fully nude body, bare skin unobstructed",
|
|
action_family="penetration",
|
|
position_family="penetrative",
|
|
position_key="kneeling",
|
|
position_keys=["kneeling"],
|
|
)
|
|
metadata_tags = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(metadata_explicit_row, 1.29),
|
|
deps,
|
|
).as_text()
|
|
for required in ("(naked:1.29)", "pussy", "penis", "penetration"):
|
|
_expect(required in metadata_tags, f"SDXL row tags lost structured explicit metadata tag: {required}")
|
|
outercourse_noise_row = _fixture_hardcore_row(
|
|
item="penis-licking outercourse position with tongue along the penis shaft",
|
|
pose="configured outercourse pose",
|
|
role_graph="Woman A bends low while her tongue runs along Man A's penis shaft.",
|
|
source_role_graph="Woman A bends low while her tongue runs along Man A's penis shaft.",
|
|
item_axis_values={
|
|
"position": "penis-licking outercourse position",
|
|
"outer_act": "tongue along the penis shaft",
|
|
},
|
|
action_family="outercourse",
|
|
position_family="outercourse",
|
|
position_key="penis_licking",
|
|
position_keys=["penis_licking"],
|
|
)
|
|
outercourse_noise_tags = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(outercourse_noise_row, 1.29),
|
|
deps,
|
|
).as_text()
|
|
_expect("outercourse" in outercourse_noise_tags, "SDXL outercourse row lost matching family tag")
|
|
_expect("penis licking" in outercourse_noise_tags, "SDXL outercourse row lost specific position key")
|
|
_expect("oral sex" not in outercourse_noise_tags, "SDXL outercourse row kept incompatible oral tag")
|
|
_expect("penetration" not in outercourse_noise_tags, "SDXL outercourse row kept incompatible penetration tag")
|
|
stale_hardcore_pose_row = _fixture_hardcore_row(
|
|
item="oral contact with mouth on the visible genitals in side-lying oral position",
|
|
pose="kneeling and balancing a cucumber upright on an open palm held overhead",
|
|
role_graph="Woman A lies on her side while Man A's mouth is pressed to her pussy.",
|
|
source_role_graph="Woman A lies on her side while Man A's mouth is pressed to her pussy.",
|
|
item_axis_values={
|
|
"position": "side-lying oral position",
|
|
"oral_act": "oral contact with mouth on the visible genitals",
|
|
},
|
|
action_family="oral",
|
|
position_family="oral",
|
|
position_key="side_lying",
|
|
position_keys=["side_lying"],
|
|
)
|
|
stale_hardcore_pose_tags = sdxl_tag_routes.row_core_tags_result(
|
|
sdxl_tag_routes.SDXLRowTagRequest(stale_hardcore_pose_row, 1.29),
|
|
deps,
|
|
).as_text()
|
|
_expect("oral sex" in stale_hardcore_pose_tags, "SDXL hardcore route lost oral family tag")
|
|
_expect("side lying" in stale_hardcore_pose_tags, "SDXL hardcore route lost structured position key")
|
|
_expect("cucumber" not in stale_hardcore_pose_tags, "SDXL hardcore route leaked generic stale pose text")
|
|
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3511,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(hardcore_clothing_continuity="partially_removed"),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
_expect_pair(pair, "sdxl_tag_routes_pair")
|
|
soft_row = pair.get("softcore_row") if isinstance(pair.get("softcore_row"), dict) else {}
|
|
hard_row = pair.get("hardcore_row") if isinstance(pair.get("hardcore_row"), dict) else {}
|
|
typed_soft = sdxl_tag_routes.soft_tags_result(
|
|
sdxl_tag_routes.SDXLPairTagRequest(soft_row, pair, 1.29),
|
|
deps,
|
|
)
|
|
typed_hard = sdxl_tag_routes.hard_tags_result(
|
|
sdxl_tag_routes.SDXLPairTagRequest(hard_row, pair, 1.29),
|
|
deps,
|
|
)
|
|
_expect(
|
|
typed_soft.as_text() == sdxl_formatter._soft_tags(soft_row, pair, 1.29),
|
|
"Typed SDXL pair soft tag route should match legacy wrapper output",
|
|
)
|
|
_expect(
|
|
typed_hard.as_text() == sdxl_formatter._hard_tags(hard_row, pair, 1.29),
|
|
"Typed SDXL pair hard tag route should match legacy wrapper output",
|
|
)
|
|
pair_stale_hard_row = _fixture_hardcore_row(
|
|
prompt="",
|
|
item="standing portrait setup",
|
|
pose="standing pose",
|
|
role_graph="",
|
|
source_role_graph="",
|
|
expression="neutral expression",
|
|
composition="standing portrait frame",
|
|
action_family="",
|
|
position_family="",
|
|
position_key="",
|
|
position_keys=[],
|
|
)
|
|
pair_stale_root = {
|
|
"hardcore_prompt": "stale pair prompt says fully nude naked pussy penis oral anal semen penetration",
|
|
"hardcore_women_count": 1,
|
|
"hardcore_men_count": 1,
|
|
}
|
|
pair_stale_tags = sdxl_tag_routes.hard_tags_result(
|
|
sdxl_tag_routes.SDXLPairTagRequest(pair_stale_hard_row, pair_stale_root, 1.29),
|
|
deps,
|
|
).as_text()
|
|
for forbidden in ("(naked:", "pussy", "penis", "oral sex", "anal sex", "semen", "penetration"):
|
|
_expect(forbidden not in pair_stale_tags, f"SDXL pair tags should not infer explicit tag from stale prompt: {forbidden}")
|
|
|
|
pair_metadata_root = {
|
|
"hardcore_prompt": "stale pair prompt without explicit tag anchors",
|
|
"hardcore_women_count": 1,
|
|
"hardcore_men_count": 1,
|
|
"hardcore_clothing_state": "fully nude body, bare skin unobstructed",
|
|
}
|
|
pair_metadata_hard_row = _fixture_hardcore_row(
|
|
prompt="",
|
|
item="kneeling pose with visible penis and pussy contact",
|
|
role_graph="penis thrusts into pussy",
|
|
source_role_graph="penis thrusts into pussy",
|
|
action_family="penetration",
|
|
position_family="penetrative",
|
|
position_key="kneeling",
|
|
position_keys=["kneeling"],
|
|
)
|
|
pair_metadata_tags = sdxl_tag_routes.hard_tags_result(
|
|
sdxl_tag_routes.SDXLPairTagRequest(pair_metadata_hard_row, pair_metadata_root, 1.29),
|
|
deps,
|
|
).as_text()
|
|
for required in ("(naked:1.29)", "pussy", "penis", "penetration"):
|
|
_expect(required in pair_metadata_tags, f"SDXL pair tags lost structured explicit metadata tag: {required}")
|
|
pair_axis_tags = sdxl_tag_routes.hard_tags_result(
|
|
sdxl_tag_routes.SDXLPairTagRequest(axis_only_row, pair_metadata_root, 1.29),
|
|
deps,
|
|
).as_text()
|
|
_expect("standing oral position" in pair_axis_tags, "SDXL pair hard route lost item axis position tag")
|
|
_expect("mouth contact at hip height" in pair_axis_tags, "SDXL pair hard route lost item axis contact tag")
|
|
formatted = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=_json(pair),
|
|
target="hardcore",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
_expect("sdxl(insta_of_pair)" in formatted.get("method", ""), "SDXL pair formatter route changed method")
|
|
|
|
|
|
def smoke_hardcore_position_config_policy() -> None:
|
|
_expect(
|
|
pb.HARDCORE_POSITION_FAMILY_CHOICES is hardcore_position_config.HARDCORE_POSITION_FAMILY_CHOICES,
|
|
"Prompt builder hardcore position family choices are not delegated",
|
|
)
|
|
_expect("outercourse_only" in hardcore_position_config.hardcore_position_focus_choices(), "Hardcore focus choices lost outercourse_only")
|
|
_expect("boobjob" in hardcore_position_config.hardcore_position_key_choices(), "Hardcore position keys lost boobjob")
|
|
_expect(
|
|
category_template_metadata.template_action_family({"action_family": "toy double"}) == "toy_double",
|
|
"Template action-family normalizer should accept spaced aliases",
|
|
)
|
|
_expect(
|
|
category_template_metadata.template_action_family({"action_family": "manual stimulation"}) == "manual",
|
|
"Template action-family normalizer should accept subcategory-style aliases",
|
|
)
|
|
_expect(
|
|
category_template_metadata.template_position_family({"position_family": "penetration"}) == "penetrative",
|
|
"Template position-family normalizer should accept action-style aliases",
|
|
)
|
|
_expect(
|
|
category_template_metadata.template_position_family({"position_family": "outer-course"}) == "outercourse",
|
|
"Template position-family normalizer should accept hyphenated aliases",
|
|
)
|
|
|
|
base = json.loads(
|
|
pb.build_hardcore_position_pool_json(
|
|
combine_mode="replace",
|
|
family="oral",
|
|
selected_positions=["standing", "bad value", "standing"],
|
|
)
|
|
)
|
|
_expect(base.get("enabled") is True, "Hardcore position pool should enable config")
|
|
_expect(base.get("family") == "oral", "Hardcore position pool lost family")
|
|
_expect(base.get("positions") == ["standing"], "Hardcore position normalization changed")
|
|
_expect(base.get("require_position") is True, "Hardcore position pool should require selected position")
|
|
|
|
added = json.loads(
|
|
hardcore_position_config.build_hardcore_position_pool_json(
|
|
hardcore_position_config=base,
|
|
combine_mode="add",
|
|
family="any",
|
|
selected_positions=["kneeling", "standing"],
|
|
)
|
|
)
|
|
_expect(added.get("positions") == ["standing", "kneeling"], "Hardcore position add merge changed")
|
|
|
|
filtered = json.loads(
|
|
pb.build_hardcore_action_filter_json(
|
|
hardcore_position_config=added,
|
|
focus="outercourse_only",
|
|
allow_toys=False,
|
|
allow_double=False,
|
|
allow_penetration=True,
|
|
allow_foreplay=True,
|
|
allow_interaction=True,
|
|
allow_manual=True,
|
|
allow_oral=True,
|
|
allow_outercourse=True,
|
|
allow_anal=True,
|
|
allow_climax=True,
|
|
)
|
|
)
|
|
_expect(filtered.get("family") == "outercourse", "Hardcore action focus did not set outercourse family")
|
|
_expect(filtered.get("allow_oral") is False, "Hardcore outercourse focus should disable oral")
|
|
_expect(filtered.get("allow_penetration") is False, "Hardcore outercourse focus should disable penetration")
|
|
_expect("outercourse_sex" in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories lost outercourse")
|
|
_expect("oral_sex" not in hardcore_position_config.hardcore_allowed_subcategory_slugs(filtered), "Allowed subcategories should exclude oral")
|
|
action_only = json.loads(
|
|
hardcore_position_config.build_hardcore_action_filter_json(
|
|
focus="outercourse_only",
|
|
allow_toys=False,
|
|
allow_double=False,
|
|
allow_penetration=True,
|
|
allow_foreplay=True,
|
|
allow_interaction=True,
|
|
allow_manual=True,
|
|
allow_oral=True,
|
|
allow_outercourse=True,
|
|
allow_anal=True,
|
|
allow_climax=True,
|
|
)
|
|
)
|
|
action_axis = hardcore_position_config.filter_hardcore_axis(
|
|
"outer_act",
|
|
["boobjob body contact", "blowjob oral sex", "vaginal penetration"],
|
|
action_only,
|
|
)
|
|
_expect(action_axis == ["boobjob body contact"], "Hardcore action filter policy did not block disabled oral/penetration text")
|
|
action_axis_metadata = hardcore_position_config.filter_hardcore_axis(
|
|
"outer_act",
|
|
[
|
|
{"text": "generic contact route", "action_family": "outercourse", "position_family": "outercourse"},
|
|
{"text": "generic contact route", "action_family": "oral", "position_family": "oral"},
|
|
{"text": "generic contact route", "action_family": "penetration", "position_family": "penetrative"},
|
|
],
|
|
action_only,
|
|
)
|
|
_expect(
|
|
action_axis_metadata == [{"text": "generic contact route", "action_family": "outercourse", "position_family": "outercourse"}],
|
|
"Hardcore action filter policy did not honor structured action metadata",
|
|
)
|
|
position_filtered = hardcore_position_config.apply_hardcore_position_config_to_subcategory(
|
|
{
|
|
"slug": "oral_sex",
|
|
"item_templates": [
|
|
{"template": "oral contact in {position}"},
|
|
{"template": "metadata-specific oral contact", "position_key": "standing", "action_family": "oral"},
|
|
{"template": "oral sex without a position axis"},
|
|
{"template": "unsupported static template"},
|
|
],
|
|
"item_axes": {
|
|
"position": [
|
|
"standing oral position",
|
|
"kneeling oral position",
|
|
{"text": "generic standing pose", "position_key": "standing"},
|
|
{"text": "generic kneeling pose", "position_key": "kneeling"},
|
|
],
|
|
"oral_act": ["blowjob", "cunnilingus"],
|
|
},
|
|
},
|
|
base,
|
|
)
|
|
_expect(
|
|
position_filtered["item_templates"]
|
|
== [
|
|
{"template": "oral contact in {position}"},
|
|
{"template": "metadata-specific oral contact", "position_key": "standing", "action_family": "oral"},
|
|
],
|
|
"Hardcore position policy did not filter templates by selected position requirements or metadata",
|
|
)
|
|
_expect(
|
|
position_filtered["item_axes"]["position"] == ["standing oral position", {"text": "generic standing pose", "position_key": "standing"}],
|
|
"Hardcore position policy did not filter position axes by selected keys or metadata",
|
|
)
|
|
filtered_categories = hardcore_position_config.filter_hardcore_categories_for_position(
|
|
[
|
|
{
|
|
"name": "Hardcore sexual poses",
|
|
"slug": "hardcore_sexual_poses",
|
|
"subcategories": [{"slug": "oral_sex"}, {"slug": "outercourse_sex"}],
|
|
},
|
|
{"name": "Casual clothes", "slug": "casual_clothes", "subcategories": [{"slug": "tops"}]},
|
|
],
|
|
filtered,
|
|
1,
|
|
1,
|
|
lambda _entry, _women, _men: True,
|
|
)
|
|
_expect(
|
|
[entry["slug"] for entry in filtered_categories[0]["subcategories"]] == ["outercourse_sex"],
|
|
"Hardcore category filter policy did not remove disallowed subcategories",
|
|
)
|
|
_expect(filtered_categories[1]["slug"] == "casual_clothes", "Hardcore category filter should preserve non-hardcore categories")
|
|
|
|
keys = pb._hardcore_position_keys("woman on all fours from behind", axis_values={"position": "doggy"})
|
|
_expect(keys == ["doggy"], "Hardcore position key detection changed")
|
|
source_family = hardcore_position_config.hardcore_source_position_family({"slug": "manual_stimulation"}, filtered)
|
|
_expect(source_family == "manual", "Hardcore source family lookup changed")
|
|
source_action_family = hardcore_action_metadata.source_hardcore_action_family(
|
|
"outer-course",
|
|
"",
|
|
"generic contact",
|
|
)
|
|
_expect(source_action_family == "outercourse", "Source action-family fallback should accept hyphenated source aliases")
|
|
default_action_route = row_route_metadata.resolve_action_position_route(
|
|
is_pose_category=True,
|
|
subcategory={"slug": "anal_double_penetration"},
|
|
hardcore_position_config=None,
|
|
item_template_metadata={"action_family": "default", "position_family": "anal"},
|
|
item_text="toy-assisted double penetration with front-and-back contact",
|
|
source_role_graph="one partner between two bodies",
|
|
source_composition="",
|
|
pose="",
|
|
item_axis_values={"double_act": "toy-assisted double penetration"},
|
|
)
|
|
_expect(default_action_route.get("position_family") == "anal", "Default-action metadata should preserve position family")
|
|
_expect(default_action_route.get("action_family") == "toy_double", "Default-action metadata should still allow action inference")
|
|
item_text, item_name, axis_values, template_metadata = pb._compose_item(
|
|
random.Random(42),
|
|
{},
|
|
{
|
|
"name": "Template metadata route",
|
|
"item_templates": [
|
|
{
|
|
"template": "{act} in {position}",
|
|
"action_family": "oral",
|
|
"position_family": "oral",
|
|
"position_keys": ["kneeling", "open_thighs"],
|
|
"formatter_hint": {
|
|
"krea2": "keep mouth contact readable",
|
|
"sdxl": ["oral contact", "kneeling oral"],
|
|
"training_caption": "oral contact caption detail",
|
|
},
|
|
}
|
|
],
|
|
"item_axes": {
|
|
"act": ["mouth contact"],
|
|
"position": ["kneeling oral position"],
|
|
},
|
|
},
|
|
"Template metadata route",
|
|
women_count=1,
|
|
men_count=1,
|
|
)
|
|
_expect(item_text == "mouth contact in kneeling oral position", "Template metadata route changed composed item text")
|
|
_expect(item_name == "Template metadata route", "Template metadata route changed item name")
|
|
_expect(axis_values == {"act": "mouth contact", "position": "kneeling oral position"}, "Template metadata route lost axis values")
|
|
_expect(template_metadata.get("action_family") == "oral", "Template metadata route lost action family")
|
|
_expect(pb._template_position_family(template_metadata) == "oral", "Template metadata route lost position family")
|
|
_expect(pb._template_position_keys(template_metadata) == ["kneeling", "open_thighs"], "Template metadata route lost position keys")
|
|
_expect(pb._template_action_family(template_metadata) == "oral", "Template metadata route lost normalized action family")
|
|
formatter_hints = pb._template_formatter_hints(template_metadata)
|
|
_expect(formatter_hints.get("krea") == ["keep mouth contact readable"], "Template metadata route lost Krea formatter hint")
|
|
_expect(formatter_hints.get("sdxl") == ["oral contact", "kneeling oral"], "Template metadata route lost SDXL formatter hints")
|
|
_expect(formatter_hints.get("caption") == ["oral contact caption detail"], "Template metadata route lost caption formatter hint")
|
|
inherited_text, _inherited_name, inherited_axis_values, inherited_metadata = pb._compose_item(
|
|
random.Random(42),
|
|
{},
|
|
{
|
|
"name": "Inherited metadata route",
|
|
"item_template_metadata": {
|
|
"action_family": "manual",
|
|
"position_family": "manual",
|
|
"position_keys": ["kneeling"],
|
|
"formatter_hint": {"caption": "inherited caption cue"},
|
|
},
|
|
"item_templates": ["{act} in {position}"],
|
|
"item_axes": {
|
|
"act": ["hand stimulation"],
|
|
"position": ["kneeling manual position"],
|
|
},
|
|
},
|
|
"Inherited metadata route",
|
|
women_count=1,
|
|
men_count=1,
|
|
)
|
|
_expect(inherited_text == "hand stimulation in kneeling manual position", "Inherited template metadata changed item text")
|
|
_expect(inherited_axis_values == {"act": "hand stimulation", "position": "kneeling manual position"}, "Inherited template metadata lost axis values")
|
|
_expect(inherited_metadata.get("action_family") == "manual", "String template did not inherit action family")
|
|
_expect(inherited_metadata.get("position_family") == "manual", "String template did not inherit position family")
|
|
_expect(pb._template_position_keys(inherited_metadata) == ["kneeling"], "String template did not inherit position keys")
|
|
_expect(
|
|
route_metadata.row_formatter_hints({"item_template_metadata": inherited_metadata}, "caption") == ["inherited caption cue"],
|
|
"String template did not inherit formatter hints",
|
|
)
|
|
override_text, _override_name, _override_axis_values, override_metadata = pb._compose_item(
|
|
random.Random(42),
|
|
{},
|
|
{
|
|
"name": "Override metadata route",
|
|
"item_template_metadata": {
|
|
"action_family": "manual",
|
|
"position_family": "manual",
|
|
"formatter_hint": {"all": "inherited shared cue"},
|
|
},
|
|
"item_templates": [
|
|
{
|
|
"template": "{act} in {position}",
|
|
"action_family": "oral",
|
|
"formatter_hint": {"krea2": "override krea cue"},
|
|
}
|
|
],
|
|
"item_axes": {
|
|
"act": ["mouth contact"],
|
|
"position": ["kneeling oral position"],
|
|
},
|
|
},
|
|
"Override metadata route",
|
|
women_count=1,
|
|
men_count=1,
|
|
)
|
|
_expect(override_text == "mouth contact in kneeling oral position", "Override template metadata changed item text")
|
|
_expect(override_metadata.get("action_family") == "oral", "Template object did not override inherited action family")
|
|
_expect(override_metadata.get("position_family") == "manual", "Template object should keep inherited position family when absent")
|
|
_expect(
|
|
route_metadata.row_formatter_hints({"item_template_metadata": override_metadata}, "krea")
|
|
== ["inherited shared cue", "override krea cue"],
|
|
"Template metadata did not merge inherited and template formatter hints",
|
|
)
|
|
route_row = {
|
|
"action_family": "penetrative",
|
|
"position_family": "Oral",
|
|
"position_keys": ["spread leg oral", "bad key"],
|
|
"position_key": "open thighs",
|
|
"formatter_hints": {"all": ["shared formatter cue"], "training_caption": ["caption formatter cue"]},
|
|
}
|
|
_expect(route_metadata.row_action_family(route_row) == "penetration", "Route metadata action normalization changed")
|
|
_expect(route_metadata.row_position_family(route_row) == "oral", "Route metadata position-family normalization changed")
|
|
_expect(
|
|
route_metadata.row_position_keys(route_row) == ["spread_leg_oral", "open_thighs"],
|
|
"Route metadata position-key normalization changed",
|
|
)
|
|
_expect(
|
|
route_metadata.row_position_keys({"position_keys": ["kneeling_oral"]}, include_unknown=True) == ["kneeling_oral"],
|
|
"Route metadata legacy position-key passthrough changed",
|
|
)
|
|
_expect(
|
|
route_metadata.row_formatter_hints(route_row, "caption") == ["shared formatter cue", "caption formatter cue"],
|
|
"Route metadata formatter hint routing changed",
|
|
)
|
|
nested_route_row = {
|
|
"item_template_metadata": {
|
|
"action_family": "oral",
|
|
"position_family": "oral",
|
|
"position_keys": ["kneeling", "open_thighs"],
|
|
"formatter_hint": {"krea2": "nested krea cue", "sdxl": "nested sdxl cue", "training_caption": "nested caption cue"},
|
|
}
|
|
}
|
|
_expect(
|
|
route_metadata.row_action_family(nested_route_row) == "oral",
|
|
"Route metadata should fall back to nested template action family",
|
|
)
|
|
_expect(
|
|
route_metadata.row_position_family(nested_route_row) == "oral",
|
|
"Route metadata should fall back to nested template position family",
|
|
)
|
|
_expect(
|
|
route_metadata.row_position_keys(nested_route_row) == ["kneeling", "open_thighs"],
|
|
"Route metadata should fall back to nested template position keys",
|
|
)
|
|
_expect(
|
|
route_metadata.row_formatter_hints(nested_route_row, "krea") == ["nested krea cue"],
|
|
"Route metadata should fall back to nested Krea formatter hints",
|
|
)
|
|
merged_route_row = {
|
|
"position_key": "standing",
|
|
"formatter_hints": {"all": ["shared cue"]},
|
|
"item_template_metadata": {
|
|
"position_keys": ["kneeling"],
|
|
"formatter_hint": {"caption": "nested caption cue"},
|
|
},
|
|
}
|
|
_expect(
|
|
route_metadata.row_position_keys(merged_route_row) == ["standing", "kneeling"],
|
|
"Route metadata should merge top-level and nested position keys",
|
|
)
|
|
_expect(
|
|
route_metadata.row_formatter_hints(merged_route_row, "caption") == ["shared cue", "nested caption cue"],
|
|
"Route metadata should merge top-level and nested formatter hints",
|
|
)
|
|
route_hints = category_template_metadata.formatter_hints_for_route(
|
|
{"formatter_hints": {"all": ["shared formatter cue"], "krea2": ["krea formatter cue"]}},
|
|
"krea2",
|
|
)
|
|
_expect(route_hints == ["shared formatter cue", "krea formatter cue"], "Formatter hint route resolver changed")
|
|
raw_route_hints = category_template_metadata.formatter_hints_for_route(
|
|
{"all": ["raw shared cue"], "caption": ["raw caption cue"]},
|
|
"caption",
|
|
)
|
|
_expect(raw_route_hints == ["raw shared cue", "raw caption cue"], "Raw formatter route-map resolver changed")
|
|
_expect(
|
|
category_template_metadata.formatter_hints_for_route({"caption": "row caption should not be a hint", "prompt": "row prompt"}, "caption")
|
|
== [],
|
|
"Formatter hint route resolver treated an arbitrary metadata row as route hints",
|
|
)
|
|
_expect(
|
|
pb._template_action_family(template_metadata) == category_template_metadata.template_action_family(template_metadata),
|
|
"Prompt builder template action policy should delegate",
|
|
)
|
|
_expect(
|
|
category_template_metadata.template_metadata_errors(template_metadata) == [],
|
|
"Valid template metadata should not report audit errors",
|
|
)
|
|
invalid_metadata = {
|
|
"action_family": "bad_action",
|
|
"position_family": "bad_family",
|
|
"position_keys": ["kneeling", "bad_position"],
|
|
"formatter_hint": {"bad_route": 9, "sdxl": ["ok", ""]},
|
|
}
|
|
invalid_errors = category_template_metadata.template_metadata_errors(invalid_metadata)
|
|
_expect(any("bad_action" in error for error in invalid_errors), "Template metadata validation missed bad action")
|
|
_expect(any("bad_family" in error for error in invalid_errors), "Template metadata validation missed bad family")
|
|
_expect(any("bad_position" in error for error in invalid_errors), "Template metadata validation missed bad position key")
|
|
_expect(any("bad_route" in error for error in invalid_errors), "Template metadata validation missed bad formatter route")
|
|
_expect(any("invalid formatter_hint" in error for error in invalid_errors), "Template metadata validation missed bad formatter hint value")
|
|
|
|
|
|
def smoke_row_route_metadata_policy() -> None:
|
|
template_metadata = {
|
|
"action_family": "oral",
|
|
"position_family": "oral",
|
|
"position_keys": ["kneeling", "open_thighs"],
|
|
}
|
|
route = row_route_metadata.resolve_action_position_route(
|
|
is_pose_category=True,
|
|
subcategory={"slug": "oral_sex"},
|
|
hardcore_position_config={},
|
|
item_template_metadata=template_metadata,
|
|
item_text="mouth contact in kneeling oral position",
|
|
source_role_graph="the woman kneels in front of the man",
|
|
source_composition="close kneeling oral composition",
|
|
pose="kneeling pose",
|
|
item_axis_values={"position": "kneeling oral position"},
|
|
)
|
|
_expect(route["action_family"] == "oral", "Route policy lost template action family")
|
|
_expect(route["position_family"] == "oral", "Route policy lost template position family")
|
|
_expect(route["position_key"] == "kneeling", "Route policy did not preserve first template position key")
|
|
_expect(route["position_keys"] == ["kneeling", "open_thighs"], "Route policy changed template position-key precedence")
|
|
route_result = row_route_metadata.resolve_action_position_route_result(
|
|
is_pose_category=True,
|
|
subcategory={"slug": "oral_sex"},
|
|
hardcore_position_config={},
|
|
item_template_metadata=template_metadata,
|
|
item_text="mouth contact in kneeling oral position",
|
|
source_role_graph="the woman kneels in front of the man",
|
|
source_composition="close kneeling oral composition",
|
|
pose="kneeling pose",
|
|
item_axis_values={"position": "kneeling oral position"},
|
|
)
|
|
_expect(route_result.as_dict() == route, "Typed action/position route should match legacy dict route")
|
|
_expect(route_result.position_key == "kneeling", "Typed action/position route lost first position key")
|
|
|
|
delegated = pb._action_position_route_metadata(
|
|
is_pose_category=True,
|
|
subcategory={"slug": "oral_sex"},
|
|
hardcore_position_config={},
|
|
item_template_metadata=template_metadata,
|
|
item_text="mouth contact in kneeling oral position",
|
|
source_role_graph="the woman kneels in front of the man",
|
|
source_composition="close kneeling oral composition",
|
|
pose="kneeling pose",
|
|
item_axis_values={"position": "kneeling oral position"},
|
|
)
|
|
_expect(delegated == route, "Prompt builder route wrapper should delegate to row_route_metadata")
|
|
typed_delegated = pb._action_position_route(
|
|
is_pose_category=True,
|
|
subcategory={"slug": "oral_sex"},
|
|
hardcore_position_config={},
|
|
item_template_metadata=template_metadata,
|
|
item_text="mouth contact in kneeling oral position",
|
|
source_role_graph="the woman kneels in front of the man",
|
|
source_composition="close kneeling oral composition",
|
|
pose="kneeling pose",
|
|
item_axis_values={"position": "kneeling oral position"},
|
|
)
|
|
_expect(typed_delegated == route_result, "Prompt builder typed route wrapper should delegate to row_route_metadata")
|
|
|
|
fallback = row_route_metadata.resolve_action_position_route(
|
|
is_pose_category=True,
|
|
subcategory={"slug": "manual_stimulation"},
|
|
hardcore_position_config={},
|
|
item_template_metadata={},
|
|
item_text="manual stimulation while kneeling",
|
|
source_role_graph="the woman kneels close and uses her hand",
|
|
source_composition="kneeling manual composition",
|
|
pose="kneeling pose",
|
|
item_axis_values={"position": "kneeling manual position"},
|
|
)
|
|
_expect(fallback["position_family"] == "manual", "Route policy lost source position-family fallback")
|
|
_expect(fallback["action_family"] == "manual", "Route policy lost source action-family fallback")
|
|
_expect("kneeling" in fallback["position_keys"], "Route policy lost inferred position key")
|
|
|
|
empty = row_route_metadata.resolve_action_position_route(
|
|
is_pose_category=False,
|
|
subcategory={"slug": "casual_clothes"},
|
|
hardcore_position_config={},
|
|
item_template_metadata=template_metadata,
|
|
item_text="casual outfit",
|
|
source_role_graph="",
|
|
source_composition="",
|
|
pose="standing pose",
|
|
)
|
|
_expect(empty == row_route_metadata.empty_action_position_route(), "Non-pose route should return empty route metadata")
|
|
_expect(
|
|
row_route_metadata.empty_action_position_route_result().as_dict() == empty,
|
|
"Typed empty action/position route should match legacy dict route",
|
|
)
|
|
|
|
|
|
def smoke_category_library_route() -> None:
|
|
categories = category_library.load_category_library()
|
|
_expect(len(categories) >= 3, "category library should load JSON categories")
|
|
category, subcategory, women_count, men_count = category_library.find_subcategory(
|
|
categories,
|
|
"custom_random",
|
|
"Hardcore sexual poses / Oral sex",
|
|
random.Random(101),
|
|
random.Random(102),
|
|
women_count=1,
|
|
men_count=1,
|
|
)
|
|
_expect(category.get("slug") == "hardcore_sexual_poses", "exact category lookup selected wrong category")
|
|
_expect(subcategory.get("slug") == "oral_sex", "exact subcategory lookup selected wrong subcategory")
|
|
_expect((women_count, men_count) == (1, 1), "exact subcategory lookup changed compatible cast counts")
|
|
|
|
slash_categories = [
|
|
{"name": "Dev", "slug": "dev", "subcategories": [{"name": "Wrong Route", "slug": "wrong_route", "items": ["wrong item"]}]},
|
|
{
|
|
"name": "Dev / Test Wear",
|
|
"slug": "dev_test_wear",
|
|
"subcategories": [{"name": "Layered / Office", "slug": "layered_office", "items": ["structured test item"]}],
|
|
},
|
|
]
|
|
slash_selector = category_library.exact_subcategory_selector(
|
|
slash_categories[1],
|
|
slash_categories[1]["subcategories"][0],
|
|
)
|
|
slash_choice = category_library.split_exact_subcategory_choice(slash_categories, slash_selector)
|
|
_expect(slash_choice is not None, "Exact selector parser did not accept slash-bearing category/subcategory names")
|
|
if slash_choice is not None:
|
|
_expect(slash_choice[0].get("slug") == "dev_test_wear", "Exact selector parser did not prefer longest category prefix")
|
|
_expect(slash_choice[1] == "Layered / Office", "Exact selector parser trimmed slash-bearing subcategory incorrectly")
|
|
slash_category, slash_subcategory, _slash_women, _slash_men = category_library.find_subcategory(
|
|
slash_categories,
|
|
"custom_random",
|
|
slash_selector,
|
|
random.Random(201),
|
|
random.Random(202),
|
|
women_count=1,
|
|
men_count=0,
|
|
)
|
|
_expect(slash_category.get("slug") == "dev_test_wear", "Exact subcategory lookup failed slash-bearing category")
|
|
_expect(slash_subcategory.get("slug") == "layered_office", "Exact subcategory lookup failed slash-bearing subcategory")
|
|
|
|
item = category_library.compatible_entries(list(subcategory.get("items") or []), women_count, men_count)[0]
|
|
scenes = category_library.configured_pool(
|
|
category,
|
|
subcategory,
|
|
item,
|
|
"scenes",
|
|
"scene_pools",
|
|
category_library.load_scene_pool_library(),
|
|
"inherit_scenes",
|
|
)
|
|
expressions = category_library.configured_pool(
|
|
category,
|
|
subcategory,
|
|
item,
|
|
"expressions",
|
|
"expression_pools",
|
|
category_library.load_expression_pool_library(),
|
|
"inherit_expressions",
|
|
)
|
|
compositions = category_library.configured_pool(
|
|
category,
|
|
subcategory,
|
|
item,
|
|
"compositions",
|
|
"composition_pools",
|
|
category_library.load_composition_pool_library(),
|
|
"inherit_compositions",
|
|
)
|
|
_expect(scenes, "category inheritance did not resolve scenes")
|
|
_expect(expressions, "category inheritance did not resolve expressions")
|
|
_expect(compositions, "category inheritance did not resolve compositions")
|
|
_expect(any("oral" in _clean_key(entry.get("prompt") if isinstance(entry, dict) else entry) for entry in scenes), "oral scene pool did not contribute")
|
|
location_override = {"enabled": True, "apply_mode": "replace", "scene_entries": ["custom scene"]}
|
|
composition_override = {"enabled": True, "apply_mode": "replace", "composition_entries": ["custom composition"]}
|
|
_expect(
|
|
pb._scene_pool(category, subcategory, item, "configured_cast", location_override)
|
|
== row_pools.scene_pool(category, subcategory, item, "configured_cast", location_override),
|
|
"Prompt builder scene pool should delegate to row_pools",
|
|
)
|
|
_expect(
|
|
pb._expression_pool(category, subcategory, item) == row_pools.expression_pool(category, subcategory, item),
|
|
"Prompt builder expression pool should delegate to row_pools",
|
|
)
|
|
_expect(
|
|
pb._pose_pool(category, subcategory, item, "couple", "standard") == row_pools.pose_pool(category, subcategory, item, "couple", "standard"),
|
|
"Prompt builder pose pool should delegate to row_pools",
|
|
)
|
|
_expect(
|
|
pb._composition_pool(category, subcategory, item, "configured_cast", composition_override)
|
|
== row_pools.composition_pool(category, subcategory, item, "configured_cast", composition_override),
|
|
"Prompt builder composition pool should delegate to row_pools",
|
|
)
|
|
|
|
|
|
def smoke_category_subcategory_matrix() -> None:
|
|
categories = category_library.load_category_library()
|
|
cases: list[tuple[dict[str, Any], dict[str, Any]]] = []
|
|
for category in categories:
|
|
for subcategory in category.get("subcategories") or []:
|
|
if subcategory.get("item_templates") or subcategory.get("items"):
|
|
cases.append((category, subcategory))
|
|
_expect(len(cases) >= 30, "category matrix should cover all configured JSON subcategories")
|
|
|
|
hardcore_filter = _broad_hardcore_filter()
|
|
for index, (category, subcategory) in enumerate(cases, start=6101):
|
|
category_slug = str(category.get("slug") or "")
|
|
subcategory_slug = str(subcategory.get("slug") or "")
|
|
name = f"category_matrix.{category_slug}.{subcategory_slug}"
|
|
women_count, men_count, cast = _matrix_cast_for_route(category, subcategory)
|
|
row = _prompt_row(
|
|
name=name,
|
|
category=str(category.get("name") or ""),
|
|
subcategory=_exact_subcategory_selector(category, subcategory),
|
|
seed=index,
|
|
character_cast=cast,
|
|
women_count=women_count,
|
|
men_count=men_count,
|
|
hardcore_position_config=hardcore_filter if category_slug == "hardcore_sexual_poses" else "",
|
|
)
|
|
|
|
_expect(row.get("source") == "json_category", f"{name}.source should be json_category")
|
|
_expect(row.get("category_slug") == category_slug, f"{name}.category_slug drifted to {row.get('category_slug')}")
|
|
_expect(
|
|
row.get("subcategory_slug") == subcategory_slug,
|
|
f"{name}.subcategory_slug drifted to {row.get('subcategory_slug')}",
|
|
)
|
|
_expect_text(f"{name}.item", row.get("item"), 8)
|
|
_expect_text(f"{name}.scene_text", row.get("scene_text"), 8)
|
|
_expect_text(f"{name}.composition", row.get("composition"), 8)
|
|
_expect(isinstance(row.get("item_axis_values"), dict), f"{name}.item_axis_values missing")
|
|
_expect(isinstance(row.get("formatter_hints"), dict), f"{name}.formatter_hints missing")
|
|
|
|
if category_slug == "hardcore_sexual_poses":
|
|
_expect(row.get("content_seed_axis") == "pose", f"{name}.content_seed_axis should be pose")
|
|
_expect_text(f"{name}.source_role_graph", row.get("source_role_graph") or row.get("role_graph"), 20)
|
|
_expect_text(f"{name}.action_family", row.get("action_family"), 3)
|
|
_expect_text(f"{name}.position_family", row.get("position_family"), 3)
|
|
_expect(isinstance(row.get("position_keys"), list), f"{name}.position_keys missing")
|
|
_expect(isinstance(row.get("item_template_metadata"), dict), f"{name}.item_template_metadata missing")
|
|
_expect(row.get("item_template_metadata"), f"{name}.item_template_metadata should not be empty")
|
|
else:
|
|
_expect(row.get("content_seed_axis") == "content", f"{name}.content_seed_axis should be content")
|
|
|
|
_expect_formatter_outputs(row, name, target="single")
|
|
|
|
|
|
def smoke_hardcore_category_routes() -> None:
|
|
cast = _character_cast()
|
|
cases = [
|
|
("hardcore_penetration", "Penetrative sex", "penetration_only", "penetrative", {"penetration", "default"}, "penetrative sex", "penetrative action"),
|
|
("hardcore_oral", "Oral sex", "oral_only", "oral", {"oral"}, "oral sex", "oral action"),
|
|
("hardcore_manual", "Manual stimulation", "manual_only", "manual", {"manual"}, "manual stimulation", "manual action"),
|
|
("hardcore_outercourse", "Outercourse and genital teasing", "outercourse_only", "outercourse", {"outercourse"}, "outercourse", "non-penetrative action"),
|
|
("hardcore_foreplay", "Foreplay and teasing", "foreplay_only", "foreplay", {"foreplay"}, "foreplay", "foreplay action"),
|
|
("hardcore_aftercare", "Aftercare and cleanup", "interaction_only", "interaction", {"foreplay"}, "interaction", "interaction beat"),
|
|
]
|
|
for index, (name, subcategory, focus, position_family, action_families, sdxl_tag, caption_label) in enumerate(cases, start=1101):
|
|
row = _prompt_row(
|
|
name=name,
|
|
category="Hardcore sexual poses",
|
|
subcategory=subcategory,
|
|
seed=index,
|
|
character_cast=cast,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=_action_filter(focus),
|
|
)
|
|
_expect_custom_row(row, name)
|
|
_expect(row.get("subject_type") == "configured_cast", f"{name} should use configured cast")
|
|
_expect(row.get("position_family") == position_family, f"{name} position_family mismatch: {row.get('position_family')}")
|
|
_expect(row.get("action_family") in action_families, f"{name} action_family mismatch: {row.get('action_family')}")
|
|
_expect(isinstance(row.get("position_keys"), list), f"{name} position_keys missing")
|
|
_expect_formatter_outputs(row, name, target="single")
|
|
sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=_json(row), target="single", trigger=SdxlTrigger, prepend_trigger=True)
|
|
_expect(sdxl_tag in (sdxl.get("sdxl_prompt") or "").lower(), f"{name} SDXL prompt did not include family tag {sdxl_tag!r}")
|
|
caption, _method = caption_naturalizer.naturalize_caption("", metadata_json=_json(row), trigger=Trigger, include_trigger=True)
|
|
_expect(caption_label in caption.lower(), f"{name} caption did not include family label {caption_label!r}")
|
|
annotated_row = None
|
|
for seed in range(1801, 1841):
|
|
row = _prompt_row(
|
|
name="hardcore_annotated_template",
|
|
category="Hardcore sexual poses",
|
|
subcategory="Oral sex",
|
|
seed=seed,
|
|
character_cast=cast,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=_action_filter("oral_only"),
|
|
)
|
|
if row.get("item_template_metadata"):
|
|
annotated_row = row
|
|
break
|
|
_expect(annotated_row is not None, "No annotated item template reached generated row in deterministic seed window")
|
|
if annotated_row is not None:
|
|
_expect(annotated_row.get("action_family") == "oral", "Annotated item template action_family did not reach row")
|
|
_expect(annotated_row.get("position_family") == "oral", "Annotated item template position_family did not reach row")
|
|
_expect(annotated_row.get("item_template_metadata", {}).get("action_family") == "oral", "Annotated item metadata missing in row")
|
|
|
|
|
|
def smoke_krea_close_foreplay_route() -> None:
|
|
row = _prompt_row(
|
|
name="krea_close_foreplay_route",
|
|
category="Hardcore sexual poses",
|
|
subcategory="Foreplay and teasing",
|
|
seed=3401,
|
|
character_cast=_character_cast(),
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=_action_filter("foreplay_only"),
|
|
)
|
|
_expect_custom_row(row, "krea_close_foreplay_route")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
prompt = _expect_text("krea_close_foreplay_route.krea_prompt", krea.get("krea_prompt"), 40)
|
|
lower = prompt.lower()
|
|
_expect("metadata" in krea.get("method", ""), "close foreplay route did not use metadata")
|
|
_expect("role graph:" not in lower, "close foreplay leaked raw role label")
|
|
_expect("foreplay action:" not in lower, "close foreplay leaked raw item label")
|
|
_expect("on against" not in lower, "close foreplay kept invalid surface grammar")
|
|
_expect(
|
|
any(term in lower for term in ("clothing", "hands", "kiss", "bodies press", "body contact")),
|
|
"close foreplay lost close-contact action wording",
|
|
)
|
|
_expect_formatter_outputs(row, "krea_close_foreplay_route", target="single")
|
|
|
|
|
|
def _insta_options(**overrides: Any) -> str:
|
|
options = pb.build_insta_of_options_json(
|
|
softcore_cast="same_as_hardcore",
|
|
hardcore_cast="couple",
|
|
hardcore_women_count=1,
|
|
hardcore_men_count=1,
|
|
softcore_level="lingerie_tease",
|
|
hardcore_level="hardcore",
|
|
platform_style="hybrid",
|
|
continuity="same_creator_same_room",
|
|
hardcore_clothing_continuity="explicit_nude",
|
|
softcore_camera_mode="standard",
|
|
hardcore_camera_mode="standard",
|
|
camera_detail="compact",
|
|
hardcore_detail_density="balanced",
|
|
)
|
|
data = json.loads(options)
|
|
data.update(overrides)
|
|
return _json(data)
|
|
|
|
|
|
def smoke_pair_options_policy() -> None:
|
|
_expect(
|
|
pb.INSTA_OF_SOFTCORE_OUTFITS is pb.pair_options.INSTA_OF_SOFTCORE_OUTFITS,
|
|
"prompt_builder should delegate Insta/OF softcore outfit policy to pair_options",
|
|
)
|
|
_expect(
|
|
pb.HARDCORE_DETAIL_DENSITY_CHOICES is pb.pair_options.HARDCORE_DETAIL_DENSITY_CHOICES,
|
|
"prompt_builder should delegate hardcore detail density choices to pair_options",
|
|
)
|
|
_expect(
|
|
pb.pair_options.hardcore_detail_directive("compact").startswith("Use one compact"),
|
|
"compact hardcore detail density should have a compact directive",
|
|
)
|
|
_expect(
|
|
pb.pair_options.hardcore_detail_directive("dense").startswith("Use dense"),
|
|
"dense hardcore detail density should have a dense directive",
|
|
)
|
|
_expect(
|
|
pb.pair_options.hardcore_detail_directive("balanced") == "",
|
|
"balanced hardcore detail density should not add a directive",
|
|
)
|
|
_expect(
|
|
pb.pair_options.hardcore_detail_directive("bad") == "",
|
|
"invalid hardcore detail density directive should be empty",
|
|
)
|
|
_expect(
|
|
"scattered clothes" not in pair_clothing.body_exposure_scene_text(
|
|
"mirror corner, scattered clothes, outfit-check framing"
|
|
),
|
|
"Pair clothing body exposure scene cleanup should remove clothing clutter",
|
|
)
|
|
_expect(
|
|
"creator-shot" in pair_clothing.body_exposure_scene_text("outfit-check framing"),
|
|
"Pair clothing body exposure scene cleanup should replace outfit-check wording",
|
|
)
|
|
sanitized_closet_scene = pair_clothing.body_exposure_scene_text(
|
|
"full-length closet mirror with outfit racks, shoe shelves, and soft boutique lighting"
|
|
).lower()
|
|
_expect("outfit racks" not in sanitized_closet_scene, "Pair clothing body exposure scene cleanup should remove outfit racks")
|
|
_expect("shoe shelves" not in sanitized_closet_scene, "Pair clothing body exposure scene cleanup should remove shoe shelves")
|
|
_expect("mirror shelves" in sanitized_closet_scene, "Pair clothing body exposure scene cleanup should keep neutral mirror detail")
|
|
_expect(
|
|
pair_clothing.softcore_outfit_sentence("Man A", "wears hoodie and joggers")
|
|
== "Man A wears hoodie and joggers",
|
|
"Pair clothing softcore outfit sentence formatting changed",
|
|
)
|
|
_expect(
|
|
pair_clothing.hardcore_clothing_sentence("Woman A", "fully nude")
|
|
== "Woman A's body is fully exposed, bare skin unobstructed",
|
|
"Pair clothing hardcore fully nude sentence formatting changed",
|
|
)
|
|
_expect(
|
|
pair_clothing.character_hardcore_clothing_entries(
|
|
{
|
|
"Woman A": {"hardcore_clothing": "fully nude"},
|
|
"Man A": {"hardcore_clothing": "wears jeans"},
|
|
},
|
|
1,
|
|
1,
|
|
["Man A"],
|
|
random.Random(1),
|
|
lambda slot, _rng: str((slot or {}).get("hardcore_clothing") or ""),
|
|
)
|
|
== ["Woman A's body is fully exposed, bare skin unobstructed"],
|
|
"Pair clothing character entries should skip POV labels",
|
|
)
|
|
_expect(
|
|
pair_cast.cast_summary_phrase(2, 1) == "2 women, 1 man, 3 total adults",
|
|
"Pair cast summary phrase should live in pair_cast",
|
|
)
|
|
descriptor_row = {
|
|
"age": "25-year-old adult",
|
|
"body_phrase": "curvy build",
|
|
"skin": "warm skin",
|
|
"hair": "dark hair",
|
|
"eyes": "brown eyes",
|
|
}
|
|
_expect(
|
|
pb._insta_of_descriptor(descriptor_row) == pair_cast.insta_descriptor_from_row(descriptor_row),
|
|
"Prompt builder Insta descriptor should delegate to pair_cast",
|
|
)
|
|
_expect(
|
|
pair_cast.insta_descriptor_from_context(
|
|
{"subject_type": "man", "age": "40-year-old adult", "body_phrase": "stocky figure"}
|
|
)
|
|
== "40-year-old adult man, stocky figure",
|
|
"Pair cast context descriptor formatting changed",
|
|
)
|
|
_expect(
|
|
pair_cast.prompt_cast_descriptors("Woman A / primary creator: descriptor") == "Woman A: descriptor",
|
|
"Pair cast prompt descriptor label cleanup changed",
|
|
)
|
|
|
|
def _fake_character_context(
|
|
label: str,
|
|
label_map: dict[str, dict[str, Any]],
|
|
_rng: random.Random,
|
|
_ethnicity: str,
|
|
_figure: str,
|
|
_no_plus_women: bool,
|
|
_no_black: bool,
|
|
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
|
subject = "man" if label.startswith("Man ") else "woman"
|
|
age = "40-year-old adult" if subject == "man" else "30-year-old adult"
|
|
return {"subject_type": subject, "age": age, "body_phrase": f"{label} body"}, label_map.get(label)
|
|
|
|
descriptor_entries, descriptor_slots = pair_cast.cast_descriptor_entries_from_slots(
|
|
seed_config={},
|
|
seed=1,
|
|
row_number=1,
|
|
ethnicity="any",
|
|
figure="any",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
women_count=2,
|
|
men_count=1,
|
|
character_slots=[{"subject_type": "man", "presence_mode": "pov"}],
|
|
character_slot_map={"Man A": {"subject_type": "man", "presence_mode": "pov"}},
|
|
primary_descriptor="primary descriptor",
|
|
axis_rng=lambda _config, _axis, seed_value, row_value: random.Random(seed_value + row_value),
|
|
character_context_for_label=_fake_character_context,
|
|
slot_is_pov=lambda slot: bool(slot and slot.get("presence_mode") == "pov"),
|
|
)
|
|
_expect(
|
|
descriptor_entries
|
|
== [
|
|
"Woman A / primary creator: primary descriptor",
|
|
"Woman B: 30-year-old adult woman, Woman B body",
|
|
],
|
|
"Pair cast descriptor entries should keep primary label and skip POV men",
|
|
)
|
|
_expect(
|
|
descriptor_slots == [{"subject_type": "man", "presence_mode": "pov"}],
|
|
"Pair cast descriptor entries should return the source slots",
|
|
)
|
|
partner_styling = pair_cast.softcore_partner_styling(
|
|
seed_config={},
|
|
seed=1,
|
|
row_number=1,
|
|
women_count=2,
|
|
men_count=1,
|
|
pov_labels=["Man A"],
|
|
label_map={"Woman B": {"softcore_outfit": "custom satin dress"}, "Man A": {"softcore_outfit": "hidden"}},
|
|
axis_rng=lambda _config, _axis, seed_value, row_value: random.Random(seed_value + row_value),
|
|
choose=lambda _rng, pool: pool[0],
|
|
slot_softcore_outfit=lambda slot, _rng: str((slot or {}).get("softcore_outfit") or ""),
|
|
)
|
|
_expect(
|
|
partner_styling["outfits"] == ["Woman B wears custom satin dress"],
|
|
"Pair cast partner styling should use configured partner outfit and skip POV men",
|
|
)
|
|
_expect_text("pair_cast.partner_pose", partner_styling.get("pose"), 12)
|
|
options = json.loads(
|
|
pb.build_insta_of_options_json(
|
|
softcore_expression_enabled="false",
|
|
hardcore_expression_enabled="0",
|
|
softcore_expression_intensity=1.4,
|
|
hardcore_expression_intensity=-0.4,
|
|
hardcore_detail_density="invalid",
|
|
)
|
|
)
|
|
_expect(options["softcore_expression_enabled"] is False, "softcore expression enabled should normalize false strings")
|
|
_expect(options["hardcore_expression_enabled"] is False, "hardcore expression enabled should normalize false strings")
|
|
_expect(options["softcore_expression_intensity"] == 1.0, "softcore expression intensity should clamp high values")
|
|
_expect(options["hardcore_expression_intensity"] == 0.0, "hardcore expression intensity should clamp low values")
|
|
_expect(options["hardcore_detail_density"] == "balanced", "invalid hardcore detail density should fallback")
|
|
|
|
parsed = pb._parse_insta_of_options(
|
|
{
|
|
"softcore_cast": "bad",
|
|
"hardcore_cast": "bad",
|
|
"softcore_camera_mode": "bad",
|
|
"hardcore_camera_mode": "bad",
|
|
"camera_detail": "bad",
|
|
"hardcore_detail_density": "bad",
|
|
"hardcore_women_count": "20",
|
|
"hardcore_men_count": "-3",
|
|
}
|
|
)
|
|
_expect(parsed["softcore_cast"] == "solo", "invalid softcore cast should fallback")
|
|
_expect(parsed["hardcore_cast"] == "use_counts", "invalid hardcore cast should fallback")
|
|
_expect(parsed["softcore_camera_mode"] == "handheld_selfie", "invalid softcore camera should fallback")
|
|
_expect(parsed["hardcore_camera_mode"] == "from_camera_config", "invalid hardcore camera should fallback")
|
|
_expect(parsed["camera_detail"] == "from_camera_config", "invalid camera detail should fallback")
|
|
_expect(parsed["hardcore_detail_density"] == "balanced", "invalid hardcore density should fallback on parse")
|
|
_expect(parsed["hardcore_women_count"] == 12, "women count should clamp to max")
|
|
_expect(parsed["hardcore_men_count"] == 0, "men count should clamp to min")
|
|
|
|
_expect(pb.character_softcore_outfit_values("partner_man"), "partner man softcore outfit pool should not be empty")
|
|
_expect(
|
|
pb.character_softcore_outfit_values("custom", "one; two\nthree") == ["one", "two", "three"],
|
|
"custom softcore outfits should split stable free-text lists",
|
|
)
|
|
_expect("fully nude" in pb.character_hardcore_clothing_values("fully_nude"), "fully nude clothing state should be exposed")
|
|
_expect(
|
|
pb.character_hardcore_clothing_values("custom", "bare; outfit pushed aside") == ["bare", "outfit pushed aside"],
|
|
"custom hardcore clothing should split stable free-text lists",
|
|
)
|
|
_expect(pb._insta_of_hardcore_counts({"hardcore_cast": "threesome"}) == (2, 1), "threesome count policy changed")
|
|
_expect(pb._insta_of_softcore_category("social_tease") == ("Casual clothes", "Casual clothes / Smart casual"), "softcore category mapping changed")
|
|
|
|
|
|
def smoke_pair_route_policy() -> None:
|
|
def _fake_build_prompt(**kwargs: Any) -> dict[str, Any]:
|
|
is_hard = kwargs.get("category") == "Hardcore sexual poses"
|
|
return {
|
|
"category": kwargs.get("category"),
|
|
"subcategory": kwargs.get("subcategory"),
|
|
"scene_text": "shared test room",
|
|
"source_scene_text": "",
|
|
"composition": "centered test composition",
|
|
"source_composition": "",
|
|
"expression": "steady look",
|
|
"role_graph": "test role graph",
|
|
"item": "test item",
|
|
"positive_suffix": "test positive suffix",
|
|
"negative_prompt": "test negative",
|
|
"prompt": "test hard prompt" if is_hard else "test soft prompt",
|
|
"caption": "test hard caption" if is_hard else "test soft caption",
|
|
}
|
|
|
|
pair_options_data = {
|
|
"softcore_cast": "solo",
|
|
"softcore_expression_enabled": True,
|
|
"softcore_expression_intensity": 0.45,
|
|
"hardcore_expression_enabled": True,
|
|
"hardcore_expression_intensity": 0.85,
|
|
"hardcore_detail_density": "balanced",
|
|
}
|
|
pair_rows_kwargs: dict[str, Any] = {
|
|
"row_number": 1,
|
|
"start_index": 1,
|
|
"seed": 10,
|
|
"active_trigger": Trigger,
|
|
"parsed_seed_config": {},
|
|
"options": pair_options_data,
|
|
"ethnicity": "any",
|
|
"figure": "random",
|
|
"no_plus_women": False,
|
|
"no_black": False,
|
|
"character_profile": "",
|
|
"character_cast": "",
|
|
"character_slot_map": {},
|
|
"pov_character_labels": [],
|
|
"hard_women_count": 1,
|
|
"hard_men_count": 1,
|
|
"soft_category": "Casual clothes",
|
|
"soft_subcategory": "Casual clothes / Smart casual",
|
|
"softcore_level_key": "social_tease",
|
|
"hardcore_random_subcategory": pb.RANDOM_SUBCATEGORY,
|
|
"hardcore_position_config": "",
|
|
"location_config": "",
|
|
"composition_config": "",
|
|
"build_prompt": _fake_build_prompt,
|
|
"axis_rng": lambda _config, _axis, seed_value, row_value: random.Random(seed_value + row_value),
|
|
"cast_expression_intensity_override": lambda value, _slots, _women, _men, _phase: (value, "input"),
|
|
"context_from_character_slot": lambda *_args, **_kwargs: {},
|
|
"apply_character_context_to_row": lambda row, context: {**row, **context},
|
|
"disable_row_expression": lambda row, source: {**row, "expression_disabled": True, "expression_intensity_source": source},
|
|
"slot_softcore_outfit": lambda _slot, _rng: "",
|
|
"softcore_outfit": lambda _rng, _level: "test soft outfit",
|
|
"softcore_pose": lambda _rng, _level: "test soft pose",
|
|
"softcore_item_prompt_label": lambda _level: "Softcore test outfit",
|
|
"pov_prompt_directive": lambda labels: "POV directive" if labels else "",
|
|
"pov_composition_prompt": lambda composition, labels: f"{composition} for {','.join(labels)}" if labels else str(composition),
|
|
}
|
|
rows_route = pair_rows.build_insta_pair_rows_result(**pair_rows_kwargs)
|
|
rows_legacy = pair_rows.build_insta_pair_rows(**pair_rows_kwargs)
|
|
_expect(rows_route.soft_row == rows_legacy["soft_row"], "Typed pair row route should match legacy soft row")
|
|
_expect(rows_route.hard_row == rows_legacy["hard_row"], "Typed pair row route should match legacy hard row")
|
|
_expect(
|
|
rows_route.hard_content_rng.getstate() == rows_legacy["hard_content_rng"].getstate(),
|
|
"Typed pair row route should match legacy hard content RNG state",
|
|
)
|
|
_expect(rows_route.soft_row["item"] == "test soft outfit", "Typed pair row route lost soft outfit override")
|
|
_expect(rows_route.hard_row["hardcore_detail_density"] == "balanced", "Typed pair row route lost hard density")
|
|
|
|
camera_options = {
|
|
"hardcore_camera_mode": "same_as_softcore",
|
|
"softcore_camera_mode": "standard",
|
|
"camera_detail": "compact",
|
|
"softcore_cast": "same_as_hardcore",
|
|
"continuity": "same_creator_same_room",
|
|
}
|
|
|
|
def _camera_config_with_mode(source: Any, mode: str) -> dict[str, Any]:
|
|
parsed = dict(source or {})
|
|
parsed["camera_mode"] = mode
|
|
return parsed
|
|
|
|
def _camera_directive(config: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
return f"{config.get('camera_mode')} camera", config
|
|
|
|
def _camera_rows() -> tuple[dict[str, Any], dict[str, Any]]:
|
|
return (
|
|
{"scene_text": "soft room", "composition": "soft composition"},
|
|
{"scene_text": "hard room", "composition": "hard composition"},
|
|
)
|
|
|
|
camera_common = {
|
|
"options": camera_options,
|
|
"camera_config": {"base": "camera"},
|
|
"softcore_camera_config": None,
|
|
"hardcore_camera_config": None,
|
|
"hard_women_count": 1,
|
|
"hard_men_count": 1,
|
|
"pov_character_labels": [],
|
|
"camera_detail_choices": ("compact",),
|
|
"camera_config_with_mode": _camera_config_with_mode,
|
|
"camera_directive": _camera_directive,
|
|
"apply_contextual_composition": lambda row, subject_kind: {**row, "subject_kind": subject_kind},
|
|
"contextual_composition_prompt": lambda scene, composition, subject_kind: f"{subject_kind}: {scene}: {composition}",
|
|
"composition_prompt": lambda composition: f"Framed as {composition}",
|
|
"camera_scene_directive_for_context": lambda _scene, _composition, config, _pov, subject: (f"{subject} scene directive", config),
|
|
}
|
|
soft_row, hard_row = _camera_rows()
|
|
camera_route = pair_camera.resolve_insta_pair_camera_result(soft_row=soft_row, hard_row=hard_row, **camera_common)
|
|
soft_row, hard_row = _camera_rows()
|
|
camera_legacy = pair_camera.resolve_insta_pair_camera(soft_row=soft_row, hard_row=hard_row, **camera_common)
|
|
_expect(camera_route.as_dict() == camera_legacy, "Typed pair camera route should match legacy dict route")
|
|
_expect(camera_route.hard_scene == "soft room", "Typed pair camera route lost same-room continuity")
|
|
_expect(camera_route.hard_camera_sentence.startswith("Camera control:"), "Typed pair camera route lost hard camera sentence")
|
|
|
|
clothing_common = {
|
|
"hard_row": {
|
|
"role_graph": "the man thrusts his penis into the woman",
|
|
"item": "penetrative test item",
|
|
},
|
|
"mode": "explicit_nude",
|
|
"softcore_outfit": "test lingerie",
|
|
"character_hardcore_clothing_entries": [],
|
|
"men_count": 0,
|
|
"pov_labels": [],
|
|
"rng": random.Random(1),
|
|
"continuity_map": pb.INSTA_OF_HARDCORE_CLOTHING_CONTINUITY,
|
|
"choose": lambda _rng, pool: pool[0],
|
|
}
|
|
clothing_route = pair_clothing.resolve_hardcore_pair_clothing_result(**clothing_common)
|
|
clothing_legacy = pair_clothing.resolve_hardcore_pair_clothing(**clothing_common)
|
|
_expect(clothing_route.as_dict() == clothing_legacy, "Typed pair clothing route should match legacy dict route")
|
|
_expect(clothing_route.woman_access == "lower", "Typed pair clothing route lost lower-access detection")
|
|
_expect(clothing_route.requires_body_exposure_scene is True, "Typed pair clothing route lost exposure-scene flag")
|
|
partial_common = {**clothing_common, "mode": "partially_removed"}
|
|
partial_route = pair_clothing.resolve_hardcore_pair_clothing_result(**partial_common)
|
|
_expect(partial_route.requires_body_exposure_scene is True, "Partial lower-access clothing should request scene cleanup")
|
|
structured_axis_clothing = pair_clothing.resolve_hardcore_pair_clothing_result(
|
|
**{
|
|
**clothing_common,
|
|
"hard_row": {
|
|
"role_graph": "generic adult action",
|
|
"item": "generic configured action",
|
|
"item_axis_values": {
|
|
"position": {"text": "edge-supported penetration"},
|
|
"leg_detail": ["thighs open", "auto"],
|
|
},
|
|
},
|
|
"mode": "partially_removed",
|
|
}
|
|
)
|
|
_expect(
|
|
structured_axis_clothing.woman_access == "lower",
|
|
"Pair clothing should read structured/list axis values for lower-access detection",
|
|
)
|
|
oral_common = {
|
|
**clothing_common,
|
|
"hard_row": {
|
|
"role_graph": "the woman takes the man's penis in her mouth",
|
|
"item": "standing oral test item",
|
|
},
|
|
"mode": "partially_removed",
|
|
}
|
|
oral_route = pair_clothing.resolve_hardcore_pair_clothing_result(**oral_common)
|
|
_expect(oral_route.requires_body_exposure_scene is True, "Man lower-access clothing should request scene cleanup")
|
|
|
|
|
|
def smoke_pair_builder_policy() -> None:
|
|
request = pair_builder.InstaPairBuildRequest(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=2101,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
built = pair_builder.build_insta_of_pair(request, pb._insta_pair_build_dependencies())
|
|
delegated = pb.build_insta_of_pair(
|
|
row_number=request.row_number,
|
|
start_index=request.start_index,
|
|
seed=request.seed,
|
|
ethnicity=request.ethnicity,
|
|
figure=request.figure,
|
|
no_plus_women=request.no_plus_women,
|
|
no_black=request.no_black,
|
|
trigger=request.trigger,
|
|
prepend_trigger_to_prompt=request.prepend_trigger_to_prompt,
|
|
seed_config=request.seed_config,
|
|
options_json=request.options_json,
|
|
filter_config=request.filter_config,
|
|
camera_config=request.camera_config,
|
|
softcore_camera_config=request.softcore_camera_config,
|
|
hardcore_camera_config=request.hardcore_camera_config,
|
|
character_profile=request.character_profile,
|
|
character_cast=request.character_cast,
|
|
hardcore_position_config=request.hardcore_position_config,
|
|
location_config=request.location_config,
|
|
composition_config=request.composition_config,
|
|
extra_positive=request.extra_positive,
|
|
extra_negative=request.extra_negative,
|
|
)
|
|
_expect(built == delegated, "Prompt builder Insta/OF wrapper should delegate to pair_builder without output drift")
|
|
_expect_pair(built, "pair_builder_policy")
|
|
|
|
|
|
def _expect_pair(pair: dict[str, Any], name: str) -> None:
|
|
_expect(pair.get("mode") == "Insta/OF", f"{name}.mode should be Insta/OF")
|
|
_expect_row_base(pair.get("softcore_row") or {}, f"{name}.softcore_row")
|
|
_expect_custom_row(pair.get("hardcore_row") or {}, f"{name}.hardcore_row")
|
|
_expect_text(f"{name}.shared_descriptor", pair.get("shared_descriptor"), 12)
|
|
_expect(pair.get("shared_cast_descriptors"), f"{name}.shared_cast_descriptors should not be empty")
|
|
shared_cast_text = "; ".join(str(item).strip() for item in pair.get("shared_cast_descriptors") or [] if str(item).strip())
|
|
_expect(
|
|
pair["hardcore_row"].get("cast_descriptor_text") == shared_cast_text,
|
|
f"{name}.hardcore_row cast descriptors drifted from pair root",
|
|
)
|
|
if pair.get("options", {}).get("softcore_cast") == "same_as_hardcore":
|
|
_expect(
|
|
pair["softcore_row"].get("cast_descriptor_text") == shared_cast_text,
|
|
f"{name}.softcore_row cast descriptors drifted from same-cast pair root",
|
|
)
|
|
_expect_text(f"{name}.softcore_prompt", pair.get("softcore_prompt"), 20)
|
|
_expect_text(f"{name}.hardcore_prompt", pair.get("hardcore_prompt"), 20)
|
|
_expect_trigger_once(f"{name}.softcore_prompt", pair.get("softcore_prompt"), Trigger)
|
|
_expect_trigger_once(f"{name}.hardcore_prompt", pair.get("hardcore_prompt"), Trigger)
|
|
_expect_trigger_once(f"{name}.softcore_caption", pair.get("softcore_caption"), Trigger)
|
|
_expect_trigger_once(f"{name}.hardcore_caption", pair.get("hardcore_caption"), Trigger)
|
|
_expect(pair["softcore_row"].get("prompt") == pair.get("softcore_prompt"), f"{name}.softcore_row prompt drifted from pair prompt")
|
|
_expect(pair["hardcore_row"].get("prompt") == pair.get("hardcore_prompt"), f"{name}.hardcore_row prompt drifted from pair prompt")
|
|
_expect(pair["softcore_row"].get("caption") == pair.get("softcore_caption"), f"{name}.softcore_row caption drifted from pair caption")
|
|
_expect(pair["hardcore_row"].get("caption") == pair.get("hardcore_caption"), f"{name}.hardcore_row caption drifted from pair caption")
|
|
_expect(
|
|
pair["softcore_row"].get("negative_prompt") == pair.get("softcore_negative_prompt"),
|
|
f"{name}.softcore_row negative drifted from pair negative",
|
|
)
|
|
_expect(
|
|
pair["hardcore_row"].get("negative_prompt") == pair.get("hardcore_negative_prompt"),
|
|
f"{name}.hardcore_row negative drifted from pair negative",
|
|
)
|
|
if "softcore_partner_styling" in pair:
|
|
_expect(
|
|
pair["softcore_row"].get("softcore_partner_styling") == pair.get("softcore_partner_styling"),
|
|
f"{name}.softcore_row partner styling drifted from pair root",
|
|
)
|
|
for key in (
|
|
"hardcore_clothing_state",
|
|
"character_hardcore_clothing",
|
|
"default_man_hardcore_clothing",
|
|
"hardcore_detail_density",
|
|
"hardcore_position_config",
|
|
):
|
|
if key in pair:
|
|
_expect(pair["hardcore_row"].get(key) == pair.get(key), f"{name}.hardcore_row {key} drifted from pair root")
|
|
_expect_no_duplicate_comma_items(f"{name}.softcore_negative", pair.get("softcore_negative_prompt"))
|
|
_expect_no_duplicate_comma_items(f"{name}.hardcore_negative", pair.get("hardcore_negative_prompt"))
|
|
_expect_formatter_outputs(pair, name, target="softcore")
|
|
_expect_formatter_outputs(pair, f"{name}.hardcore", target="hardcore")
|
|
|
|
|
|
def smoke_insta_pair() -> None:
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=2101,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
_expect_pair(pair, "insta_pair_same_cast")
|
|
_expect(pair["softcore_row"].get("scene_text") == pair["hardcore_row"].get("scene_text"), "pair scene continuity broke")
|
|
_expect_no_softcore_noise("insta_pair_same_cast.softcore_prompt", pair.get("softcore_prompt"))
|
|
_expect("styled creator-teaser frame" in str(pair.get("softcore_prompt", "")).lower(), "pair softcore prompt lost clean cast-presence wording")
|
|
krea_soft = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="softcore")
|
|
krea_soft_prompt = _expect_text("insta_pair_same_cast.krea_soft_prompt", krea_soft.get("krea_prompt"), 40)
|
|
_expect_no_softcore_noise("insta_pair_same_cast.krea_soft_prompt", krea_soft_prompt)
|
|
_expect("styled creator-teaser frame" in krea_soft_prompt.lower(), "Krea softcore prompt lost clean same-cast wording")
|
|
clothing_state = _clean_key(pair.get("hardcore_clothing_state"))
|
|
_expect("body is fully exposed" in clothing_state, "explicit nude pair should keep body exposure state")
|
|
_expect("teaser outfit detail" not in clothing_state, "explicit nude pair should not repeat softcore outfit detail")
|
|
partner_styling = pair.get("softcore_partner_styling") or {}
|
|
_expect(partner_styling.get("outfits"), "same-cast pair should keep partner softcore outfit styling")
|
|
_expect_text("insta_pair_same_cast.partner_pose", partner_styling.get("pose"), 12)
|
|
|
|
|
|
def smoke_krea_pair_clothing_state() -> None:
|
|
forced_location = pb.build_location_pool_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
preset="custom_only",
|
|
custom_locations="closet_scene: full-length closet mirror with outfit racks, shoe shelves, and soft boutique lighting",
|
|
)
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3511,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(hardcore_clothing_continuity="partially_removed"),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
location_config=forced_location,
|
|
)
|
|
_expect_pair(pair, "krea_pair_clothing_state")
|
|
typed_route = krea_pair_formatter.format_insta_pair_result(
|
|
krea_pair_formatter.KreaPairFormatRequest(pair, "balanced", "preserve"),
|
|
krea_formatter._krea_pair_format_dependencies(),
|
|
)
|
|
legacy_route = krea_formatter._insta_pair_to_krea(pair, "balanced", "preserve")
|
|
_expect(
|
|
typed_route.as_tuple() == legacy_route,
|
|
"Typed Krea pair formatter route should match legacy wrapper output",
|
|
)
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text("krea_pair_clothing_state.krea_prompt", krea.get("krea_prompt"), 60)
|
|
lower = prompt.lower()
|
|
root_clothing = _clean_key(pair.get("hardcore_clothing_state"))
|
|
_expect("lower body is clear" in root_clothing, "pair root clothing state lost lower-body access wording")
|
|
_expect(pair.get("default_man_hardcore_clothing"), "pair root default man hardcore clothing is missing")
|
|
_expect("metadata" in krea.get("method", ""), "pair clothing route did not use metadata")
|
|
_expect("clothing state:" not in lower, "Krea clothing route leaked raw clothing label")
|
|
_expect("visual clothing state" not in lower, "Krea clothing route fell back to visual clothing state label")
|
|
_expect("softcore outfit" not in lower and "teaser outfit" not in lower, "Krea clothing route leaked softcore outfit label")
|
|
_expect("lower body is clear" in lower, "Krea clothing route lost generated clothing continuity")
|
|
_expect("the man keeps" in lower, "Krea clothing route lost partner clothing continuity")
|
|
_expect("outfit racks" not in lower and "shoe shelves" not in lower, "Krea pair formatter leaked unsanitized hard scene")
|
|
hard_scene = _clean_key(pair["hardcore_row"].get("scene_text"))
|
|
_expect("outfit racks" not in hard_scene and "shoe shelves" not in hard_scene, "Pair builder leaked outfit-check scene clutter into hardcore row")
|
|
_expect("mirror shelves" in hard_scene, "Pair builder lost neutralized scene anchor")
|
|
caption_text, _caption_method = caption_naturalizer.naturalize_caption("", metadata_json=_json(pair), target="hardcore")
|
|
caption_lower = caption_text.lower()
|
|
_expect("outfit racks" not in caption_lower and "shoe shelves" not in caption_lower, "Caption pair formatter leaked unsanitized hard scene")
|
|
sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
sdxl_lower = sdxl.get("sdxl_prompt", "").lower()
|
|
_expect("outfit racks" not in sdxl_lower and "shoe shelves" not in sdxl_lower, "SDXL pair formatter leaked unsanitized hard scene")
|
|
|
|
|
|
def smoke_krea_anal_axis_compatibility() -> None:
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=6252,
|
|
ethnicity="french_european",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(hardcore_clothing_continuity="partially_removed", camera_detail="off"),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter(
|
|
"anal_only",
|
|
pb.build_hardcore_position_pool_json(family="anal"),
|
|
),
|
|
seed_config=pb.build_seed_lock_config_json(base_seed=6252, reroll_axis="pose", reroll_seed=6352),
|
|
)
|
|
hard_row = pair["hardcore_row"]
|
|
axis_values = hard_row.get("item_axis_values") or {}
|
|
position = str(axis_values.get("position") or "").lower()
|
|
leg_detail = str(axis_values.get("leg_detail") or "").lower()
|
|
if "side-lying" in position:
|
|
_expect("standing" not in leg_detail, "Generated side-lying anal row leaked standing leg detail")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text("krea_anal_axis_compatibility.krea_prompt", krea.get("krea_prompt"), 60)
|
|
lower = prompt.lower()
|
|
_expect(
|
|
"side-lying rear-entry anal pose" not in lower or "stands braced" not in lower,
|
|
"Krea anal formatter mixed side-lying anchor with standing role graph",
|
|
)
|
|
_expect("side-lying anal position, standing with legs braced" not in lower, "Krea anal formatter leaked contradictory axis detail")
|
|
|
|
|
|
def smoke_insta_pair_pov() -> None:
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=2201,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_action_filter("oral_only"),
|
|
)
|
|
_expect_pair(pair, "insta_pair_pov_man")
|
|
pov_labels = pair.get("pov_character_labels") or []
|
|
_expect("Man A" in pov_labels, "pair POV labels should include Man A")
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect("Man A" in (hard_row.get("pov_character_labels") or []), "hard row POV labels should include Man A")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = krea.get("krea_prompt") or ""
|
|
_expect("viewer" in prompt.lower(), "POV Krea prompt should mention viewer perspective")
|
|
|
|
|
|
def smoke_insta_pair_camera_split() -> None:
|
|
soft_camera = _orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=-30,
|
|
zoom=5.0,
|
|
subject_focus="environment",
|
|
)
|
|
hard_camera = _orbit_camera(
|
|
horizontal_angle=135,
|
|
vertical_angle=30,
|
|
zoom=8.0,
|
|
subject_focus="action",
|
|
)
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=2251,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
location_config=_coworking_location_config(),
|
|
softcore_camera_config=soft_camera,
|
|
hardcore_camera_config=hard_camera,
|
|
)
|
|
_expect_pair(pair, "insta_pair_camera_split")
|
|
soft_scene = _expect_text("insta_pair_camera_split.soft_camera_scene", pair.get("softcore_camera_scene_directive"), 40)
|
|
hard_scene = _expect_text("insta_pair_camera_split.hard_camera_scene", pair.get("hardcore_camera_scene_directive"), 40)
|
|
_expect("front-right quarter view" in soft_scene, "soft camera scene missed soft orbit direction")
|
|
_expect("back-right quarter view" in hard_scene, "hard camera scene missed hard orbit direction")
|
|
_expect("low-angle shot" in soft_scene, "soft camera scene missed soft elevation")
|
|
_expect("elevated shot" in hard_scene, "hard camera scene missed hard elevation")
|
|
_expect("near desk edge" in soft_scene, "non-POV camera scene lost coworking foreground anchor")
|
|
_expect("laptop corner" in soft_scene, "non-POV camera scene lost coworking foreground detail")
|
|
_expect("front-right quarter view" in str(pair.get("softcore_camera_directive")), "soft pair camera directive was not preserved")
|
|
_expect("back-right quarter view" in str(pair.get("hardcore_camera_directive")), "hard pair camera directive was not preserved")
|
|
soft_row = pair.get("softcore_row") or {}
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(pair.get("softcore_camera_config") == soft_row.get("camera_config"), "soft pair camera config drifted from soft row")
|
|
_expect(pair.get("hardcore_camera_config") == hard_row.get("camera_config"), "hard pair camera config drifted from hard row")
|
|
_expect(pair.get("softcore_camera_directive") == soft_row.get("camera_directive"), "soft pair camera directive drifted from soft row")
|
|
_expect(pair.get("hardcore_camera_directive") == hard_row.get("camera_directive"), "hard pair camera directive drifted from hard row")
|
|
_expect(pair.get("softcore_camera_scene_directive") == soft_row.get("camera_scene_directive"), "soft pair camera scene drifted from soft row")
|
|
_expect(pair.get("hardcore_camera_scene_directive") == hard_row.get("camera_scene_directive"), "hard pair camera scene drifted from hard row")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="auto")
|
|
_expect("front-right quarter view" in (krea.get("krea_softcore_prompt") or ""), "Krea soft pair lost soft camera geometry")
|
|
_expect("back-right quarter view" in (krea.get("krea_hardcore_prompt") or ""), "Krea hard pair lost hard camera geometry")
|
|
|
|
|
|
def smoke_pov_camera_scene() -> None:
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=2261,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_action_filter("oral_only"),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=135,
|
|
vertical_angle=30,
|
|
zoom=8.0,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, "pov_camera_scene")
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(not hard_row.get("camera_directive"), "POV hard row should suppress normal camera directive")
|
|
scene_directive = _expect_text("pov_camera_scene.hard_camera_scene", hard_row.get("camera_scene_directive"), 40)
|
|
_expect("from POV" in scene_directive, "POV camera scene should be marked as first-person")
|
|
_expect("not in the lower foreground" in scene_directive, "POV camera scene should keep location anchors out of lower foreground")
|
|
_expect(
|
|
"near desk edge" not in scene_directive and "laptop corner" not in scene_directive and "chair back" not in scene_directive,
|
|
"POV camera scene should not reuse coworking foreground anchors as viewer-side objects",
|
|
)
|
|
_expect(
|
|
"lower foreground is reserved for POV body or hand cues" in scene_directive,
|
|
"POV camera scene should reserve the lower foreground for first-person body cues",
|
|
)
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = krea.get("krea_prompt") or ""
|
|
_expect("from POV" in prompt, "Krea POV prompt lost camera-scene directive")
|
|
_expect("laptop corner" not in prompt, "Krea POV prompt leaked foreground desk anchor into camera scene")
|
|
_expect("lower foreground is reserved for POV body or hand cues" in prompt, "Krea POV prompt lost POV foreground reservation")
|
|
_expect("Camera:" not in prompt, "Krea POV prompt should not emit normal third-person camera directive")
|
|
|
|
|
|
def smoke_krea_pov_penetration_route() -> None:
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3411,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=5.5,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, "krea_pov_penetration_route")
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect("Man A" in (hard_row.get("pov_character_labels") or []), "POV penetration hard row lost Man A POV label")
|
|
_expect(not hard_row.get("camera_directive"), "POV penetration should suppress normal camera directive")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text("krea_pov_penetration_route.krea_prompt", krea.get("krea_prompt"), 60)
|
|
lower = prompt.lower()
|
|
_expect("metadata" in krea.get("method", ""), "POV penetration route did not use metadata")
|
|
_expect("viewer" in lower and "first-person" in lower, "POV penetration lost first-person wording")
|
|
_expect("penetrates" in lower or "penetration" in lower, "POV penetration lost penetration action wording")
|
|
_expect("woman" in lower and "thigh" in lower, "POV penetration lost body-position anchors")
|
|
_expect("camera:" not in prompt, "POV penetration emitted normal third-person camera directive")
|
|
_expect("role graph:" not in lower and "sexual scene:" not in lower, "POV penetration leaked raw prompt labels")
|
|
_expect("composition. explicit" in lower, "POV penetration composition sentence should keep punctuation before style suffix")
|
|
|
|
|
|
def smoke_pov_outercourse_position_routes() -> None:
|
|
cases = [
|
|
(
|
|
"pov_outercourse_boobjob",
|
|
"boobjob",
|
|
("breasts inward around", "directly above the glans"),
|
|
("push her breasts inward around", "directly above the glans", "held between her breasts"),
|
|
),
|
|
(
|
|
"pov_outercourse_testicle",
|
|
"testicle_sucking",
|
|
("face below the pov viewer's penis at testicle height", "penis points upward"),
|
|
("face is below the viewer's penis at testicle height", "mouth and tongue licking", "penis points upward"),
|
|
),
|
|
(
|
|
"pov_outercourse_penis_licking",
|
|
"penis_licking",
|
|
("head low under the pov viewer's penis", "tongue touches the underside"),
|
|
("tongue touches the underside", "glans at the tip", "viewer"),
|
|
),
|
|
(
|
|
"pov_outercourse_handjob",
|
|
"handjob",
|
|
("one hand grips and strokes the pov viewer's penis", "strokes toward the glans"),
|
|
("one hand grips and strokes the viewer's penis", "thumb and fingers", "strokes toward the glans"),
|
|
),
|
|
(
|
|
"pov_outercourse_footjob",
|
|
"footjob",
|
|
("both soles wrapped around the pov viewer's penis", "lower foreground"),
|
|
("soles wrap around", "penis shaft", "lower foreground"),
|
|
),
|
|
]
|
|
for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3601):
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=offset,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_position_filter("outercourse_only", "outercourse", [position_key]),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=7.5,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, name)
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(hard_row.get("action_family") == "outercourse", f"{name} action_family should be outercourse")
|
|
_expect(hard_row.get("position_family") == "outercourse", f"{name} position_family should be outercourse")
|
|
_expect(hard_row.get("position_key") == position_key, f"{name} selected position should be primary position_key")
|
|
_expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("viewer" in prompt and "first-person" in prompt, f"{name} Krea prompt lost POV wording")
|
|
_expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
if position_key == "handjob":
|
|
_expect("behind the penis shaft" not in prompt, f"{name} Krea prompt kept vague shaft-only wording: {prompt}")
|
|
if position_key == "testicle_sucking":
|
|
_expect("head is tucked under the penis shaft" not in prompt, f"{name} Krea prompt kept high-head testicle wording: {prompt}")
|
|
|
|
|
|
def smoke_pov_oral_position_routes() -> None:
|
|
cases = [
|
|
(
|
|
"pov_oral_kneeling",
|
|
"kneeling",
|
|
("viewer's penis", "takes the viewer's penis in her mouth"),
|
|
("pov kneeling oral position", "viewer stands over her", "head is at penis height", "thighs framing"),
|
|
),
|
|
(
|
|
"pov_oral_face_sitting",
|
|
"face_sitting",
|
|
("straddling the viewer's face", "pussy directly over the viewer's mouth"),
|
|
("close first-person underview", "straddling the viewer's face", "tongue contact visible"),
|
|
),
|
|
(
|
|
"pov_oral_sixty_nine",
|
|
"sixty_nine",
|
|
("head-to-hips", "viewer's mouth on woman a's pussy"),
|
|
("pov sixty-nine oral position", "head-to-hips", "viewer's mouth on the woman's pussy", "lower-foreground body cues aligned"),
|
|
),
|
|
(
|
|
"pov_oral_edge_supported",
|
|
"edge_supported",
|
|
("raised edge with thighs open", "viewer kneels between her legs"),
|
|
("pov raised-edge cunnilingus position", "raised edge with thighs open", "face at pussy height"),
|
|
),
|
|
(
|
|
"pov_oral_side_lying",
|
|
"side_lying",
|
|
("woman a lies on her side", "viewer lies beside her hips"),
|
|
("pov side-lying cunnilingus position", "woman lies on her side", "top thigh lifted", "viewer lies beside her hips"),
|
|
),
|
|
(
|
|
"pov_oral_chair",
|
|
"chair_oral",
|
|
("viewer sits in a chair", "kneels between his thighs"),
|
|
("pov chair oral position", "viewer sits in a chair", "kneels between the viewer's thighs", "head is low at his pelvis"),
|
|
),
|
|
]
|
|
for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3701):
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=offset,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_position_filter("oral_only", "oral", [position_key]),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=7.5,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, name)
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(hard_row.get("action_family") == "oral", f"{name} action_family should be oral")
|
|
_expect(hard_row.get("position_family") == "oral", f"{name} position_family should be oral")
|
|
_expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("viewer" in prompt and "first-person" in prompt, f"{name} Krea prompt lost POV wording")
|
|
_expect("viewer lies on the viewer" not in prompt, f"{name} Krea prompt kept recursive POV wording: {prompt}")
|
|
_expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive")
|
|
_expect("as he looks down" not in prompt, f"{name} Krea prompt kept unresolved male pronoun: {prompt}")
|
|
_expect(
|
|
"the woman takes the viewer's penis in her mouth with" not in prompt,
|
|
f"{name} Krea prompt repeated oral contact detail after POV rewrite: {prompt}",
|
|
)
|
|
_expect("edge-supported oral position;" not in prompt, f"{name} Krea prompt kept source oral-position scaffold: {prompt}")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
|
|
|
|
def smoke_pov_penetration_position_routes() -> None:
|
|
cases = [
|
|
(
|
|
"pov_penetration_missionary",
|
|
"missionary",
|
|
("woman a lies on her back", "man a is above her between her thighs"),
|
|
("pov missionary position", "viewer is above her", "penetrates her pussy"),
|
|
),
|
|
(
|
|
"pov_penetration_cowgirl",
|
|
"cowgirl",
|
|
("woman a straddles man a's hips facing him", "man a lies under her"),
|
|
("pov cowgirl position", "viewer lies on his back", "woman straddles his hips"),
|
|
),
|
|
(
|
|
"pov_penetration_reverse_cowgirl",
|
|
"reverse_cowgirl",
|
|
("woman a straddles man a's hips facing away", "man a lies under her"),
|
|
("pov reverse cowgirl position", "facing away", "viewer lies on his back"),
|
|
),
|
|
(
|
|
"pov_penetration_doggy",
|
|
"doggy",
|
|
("woman a is on all fours", "man a is positioned behind her"),
|
|
("ass raised toward the pov viewer", "on all fours", "penetrates her pussy"),
|
|
),
|
|
(
|
|
"pov_penetration_edge_supported",
|
|
"edge_supported",
|
|
("raised edge", "man a kneels between her thighs"),
|
|
("pov raised-edge penetration position", "viewer kneels between her legs", "penetrates her pussy"),
|
|
),
|
|
(
|
|
"pov_penetration_lotus",
|
|
"lotus_lap",
|
|
("woman a sits in man a's lap", "legs around his hips"),
|
|
("pov lotus position", "woman sits in his lap", "penetrates her pussy"),
|
|
),
|
|
]
|
|
for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3801):
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=offset,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_position_filter("penetration_only", "penetrative", [position_key]),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=7.5,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, name)
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(hard_row.get("action_family") == "penetration", f"{name} action_family should be penetration")
|
|
_expect(hard_row.get("position_family") == "penetrative", f"{name} position_family should be penetrative")
|
|
_expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("viewer" in prompt and "pov" in prompt, f"{name} Krea prompt lost POV wording")
|
|
_expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
|
|
|
|
def smoke_pov_anal_position_routes() -> None:
|
|
cases = [
|
|
(
|
|
"pov_anal_doggy",
|
|
"doggy",
|
|
("on all fours", "positioned behind her"),
|
|
("on all fours directly in front", "penetrates her ass"),
|
|
),
|
|
(
|
|
"pov_anal_bent_over",
|
|
"bent_over",
|
|
("bent forward", "stands behind her"),
|
|
("bent forward at the waist", "penetrates her ass"),
|
|
),
|
|
(
|
|
"pov_anal_face_down",
|
|
"face_down_ass_up",
|
|
("lies face-down", "ass raised"),
|
|
("lying face-down", "penetrates her ass"),
|
|
),
|
|
(
|
|
"pov_anal_standing",
|
|
"standing",
|
|
("stands braced", "stands behind her"),
|
|
("pov standing rear-entry position", "viewer stands behind her"),
|
|
),
|
|
(
|
|
"pov_anal_side_lying",
|
|
"side_lying",
|
|
("lies on her side", "presses behind her"),
|
|
("pov side-lying sex position", "viewer is behind her"),
|
|
),
|
|
(
|
|
"pov_anal_edge_supported",
|
|
"edge_supported",
|
|
("raised edge", "kneels behind her"),
|
|
("pov raised-edge penetration position", "viewer kneels between her legs"),
|
|
),
|
|
(
|
|
"pov_anal_kneeling",
|
|
"kneeling",
|
|
("kneels forward", "kneels behind her"),
|
|
("pov kneeling rear-entry position", "viewer kneels behind her"),
|
|
),
|
|
]
|
|
for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3901):
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=offset,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast(pov_man=True),
|
|
hardcore_position_config=_position_filter("anal_only", "anal", [position_key]),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=7.5,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, name)
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(hard_row.get("position_family") == "anal", f"{name} position_family should be anal")
|
|
_expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("viewer" in prompt and "first-person" in prompt, f"{name} Krea prompt lost POV wording")
|
|
_expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
|
|
|
|
def smoke_double_front_back_route() -> None:
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=3911,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(
|
|
hardcore_cast="mixed_group",
|
|
hardcore_men_count=2,
|
|
softcore_camera_mode="from_camera_config",
|
|
hardcore_camera_mode="from_camera_config",
|
|
camera_detail="compact",
|
|
),
|
|
character_cast=_character_cast_two_men(),
|
|
hardcore_position_config=_anal_double_filter(["front_back"]),
|
|
location_config=_coworking_location_config(),
|
|
hardcore_camera_config=_orbit_camera(
|
|
horizontal_angle=45,
|
|
vertical_angle=0,
|
|
zoom=7.5,
|
|
subject_focus="action",
|
|
),
|
|
)
|
|
_expect_pair(pair, "double_front_back_route")
|
|
hard_row = pair.get("hardcore_row") or {}
|
|
_expect(hard_row.get("position_family") == "anal", "double route position_family should be anal")
|
|
_expect("front_back" in (hard_row.get("position_keys") or []), "double route lost front_back key")
|
|
role_graph = _expect_text("double_front_back_route.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
|
|
_expect("second penetration point from the front" in role_graph, f"double route role graph lost front/back placement: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
|
|
prompt = _expect_text("double_front_back_route.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), "double route Krea did not use metadata")
|
|
_expect("front-and-back" in prompt, "double route Krea lost front/back position wording")
|
|
_expect("second penetration point" in prompt, "double route Krea lost second-contact wording")
|
|
_expect("role graph:" not in prompt and "sexual scene:" not in prompt, "double route Krea leaked raw labels")
|
|
|
|
|
|
def smoke_climax_position_routes() -> None:
|
|
cases = [
|
|
(
|
|
"climax_face_down",
|
|
"face_down_ass_up",
|
|
4001,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("lies face-down", "lower back and ass"),
|
|
("face-down", "lower back and ass"),
|
|
),
|
|
(
|
|
"climax_side_lying",
|
|
"side_lying",
|
|
4042,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("lies on her side", "thighs and pussy"),
|
|
("lies on her side", "thighs and pussy"),
|
|
),
|
|
(
|
|
"climax_lotus_lap",
|
|
"lotus_lap",
|
|
4001,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("sits in man a's lap", "legs wrapped"),
|
|
("sits in the man's lap", "legs wrapped"),
|
|
),
|
|
(
|
|
"climax_open_thighs",
|
|
"open_thighs",
|
|
4001,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("lies on her back", "thighs open"),
|
|
("lies on her back", "thighs open"),
|
|
),
|
|
(
|
|
"climax_front_back",
|
|
"front_back",
|
|
4090,
|
|
_character_cast_two_men(),
|
|
1,
|
|
2,
|
|
("lies between man a and man b", "man a under her hips"),
|
|
("lies between man a and man b", "visible semen lands"),
|
|
),
|
|
]
|
|
for name, position_key, seed, cast, women_count, men_count, role_terms, krea_terms in cases:
|
|
row = _prompt_row(
|
|
name=name,
|
|
category="Hardcore sexual poses",
|
|
subcategory="Cumshot and climax",
|
|
seed=seed,
|
|
character_cast=cast,
|
|
women_count=women_count,
|
|
men_count=men_count,
|
|
hardcore_position_config=_position_filter("climax_only", "climax", [position_key]),
|
|
)
|
|
_expect_custom_row(row, name)
|
|
_expect(row.get("action_family") == "climax", f"{name} action_family should be climax")
|
|
_expect(row.get("position_family") == "climax", f"{name} position_family should be climax")
|
|
_expect(position_key in (row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", row.get("source_role_graph"), 40).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("role graph:" not in prompt and "sexual scene:" not in prompt, f"{name} Krea leaked raw labels")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
if position_key == "side_lying":
|
|
_expect("lower back and ass" not in prompt, f"{name} Krea kept conflicting rear-entry fluid location: {prompt}")
|
|
_expect_formatter_outputs(row, name, target="single")
|
|
|
|
|
|
def smoke_interaction_role_graph_routes() -> None:
|
|
cases = [
|
|
(
|
|
"interaction_manual",
|
|
"Manual stimulation",
|
|
"manual_only",
|
|
"manual",
|
|
"fingering",
|
|
4301,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("reclines with thighs open", "fingers visibly stimulating"),
|
|
("fingers visibly stimulating", "between her legs"),
|
|
),
|
|
(
|
|
"interaction_clothing_transition",
|
|
"Clothing and position transitions",
|
|
"interaction_only",
|
|
"interaction",
|
|
"position_transition",
|
|
4302,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("mid-transition", "moving clothing aside"),
|
|
("mid-transition", "guiding the woman's hips"),
|
|
),
|
|
(
|
|
"interaction_body_worship",
|
|
"Body worship and touching",
|
|
"interaction_only",
|
|
"interaction",
|
|
"body_worship",
|
|
4301,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("kisses down her body", "hands tracing"),
|
|
("kisses down her body", "hands tracing"),
|
|
),
|
|
(
|
|
"interaction_camera_performance",
|
|
"Camera performance",
|
|
"interaction_only",
|
|
"interaction",
|
|
"camera_showing",
|
|
4301,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("faces the camera", "creator-shot reveal"),
|
|
("faces the camera", "creator-shot reveal"),
|
|
),
|
|
(
|
|
"interaction_aftercare",
|
|
"Aftercare and cleanup",
|
|
"interaction_only",
|
|
"interaction",
|
|
"aftercare",
|
|
4301,
|
|
_character_cast(),
|
|
1,
|
|
1,
|
|
("lie close together after sex", "post-sex cuddle"),
|
|
("lie close together after sex", "post-sex cuddle"),
|
|
),
|
|
(
|
|
"interaction_group_coordination",
|
|
"Group coordination",
|
|
"interaction_only",
|
|
"interaction",
|
|
"watching",
|
|
4301,
|
|
_character_cast_two_men(),
|
|
1,
|
|
2,
|
|
("is centered", "hold and present"),
|
|
("is centered", "each role clearly visible"),
|
|
),
|
|
]
|
|
for name, subcategory, focus, family, position_key, seed, cast, women_count, men_count, role_terms, krea_terms in cases:
|
|
row = _prompt_row(
|
|
name=name,
|
|
category="Hardcore sexual poses",
|
|
subcategory=subcategory,
|
|
seed=seed,
|
|
character_cast=cast,
|
|
women_count=women_count,
|
|
men_count=men_count,
|
|
hardcore_position_config=_position_filter(focus, family, [position_key]),
|
|
)
|
|
_expect_custom_row(row, name)
|
|
expected_action_family = "manual" if family == "manual" else "foreplay"
|
|
_expect(
|
|
row.get("action_family") == expected_action_family,
|
|
f"{name} action_family mismatch: {row.get('action_family')} != {expected_action_family}",
|
|
)
|
|
_expect(row.get("position_family") == family, f"{name} position_family mismatch: {row.get('position_family')}")
|
|
_expect(position_key in (row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", row.get("source_role_graph"), 40).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("role graph:" not in prompt and "sexual scene:" not in prompt, f"{name} Krea leaked raw labels")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
_expect_formatter_outputs(row, name, target="single")
|
|
|
|
|
|
def smoke_fallback_role_graph_routes() -> None:
|
|
cases = [
|
|
(
|
|
"fallback_solo_woman_manual",
|
|
("woman",),
|
|
"Manual stimulation",
|
|
"manual_only",
|
|
"manual",
|
|
"fingering",
|
|
4401,
|
|
1,
|
|
0,
|
|
("reclines with thighs open", "one hand between her legs"),
|
|
("one hand between her legs", "fingers visibly stimulating"),
|
|
),
|
|
(
|
|
"fallback_solo_man_climax",
|
|
("man",),
|
|
"Cumshot and climax",
|
|
"climax_only",
|
|
"climax",
|
|
"open_thighs",
|
|
4401,
|
|
0,
|
|
1,
|
|
("solo visible ejaculation pose", "semen visible"),
|
|
("solo visible ejaculation pose", "visible semen on skin"),
|
|
),
|
|
(
|
|
"fallback_women_only_oral",
|
|
("woman", "woman"),
|
|
"Oral sex",
|
|
"oral_only",
|
|
"oral",
|
|
"reclining_oral",
|
|
4401,
|
|
2,
|
|
0,
|
|
("woman a kneels between woman b", "uses tongue and fingers"),
|
|
("woman a kneels between woman b", "uses tongue and fingers"),
|
|
),
|
|
(
|
|
"fallback_women_only_threesome",
|
|
("woman", "woman", "woman"),
|
|
"Threesomes",
|
|
"threesome_only",
|
|
"threesome",
|
|
"front_back",
|
|
4401,
|
|
3,
|
|
0,
|
|
("uses a strap-on", "gives oral contact"),
|
|
("uses a strap-on", "gives oral contact"),
|
|
),
|
|
(
|
|
"fallback_men_only_oral",
|
|
("man", "man"),
|
|
"Oral sex",
|
|
"oral_only",
|
|
"oral",
|
|
"kneeling",
|
|
4401,
|
|
0,
|
|
2,
|
|
("man a kneels", "takes man b's penis"),
|
|
("man a kneels", "takes man b's penis"),
|
|
),
|
|
(
|
|
"fallback_men_only_threesome",
|
|
("man", "man", "man"),
|
|
"Threesomes",
|
|
"threesome_only",
|
|
"threesome",
|
|
"front_back",
|
|
4401,
|
|
0,
|
|
3,
|
|
("penetrates man b anally", "gives oral contact"),
|
|
("penetrates man b anally", "gives oral contact"),
|
|
),
|
|
(
|
|
"fallback_mixed_threesome",
|
|
("woman", "man", "man"),
|
|
"Threesomes",
|
|
"threesome_only",
|
|
"threesome",
|
|
"front_back",
|
|
4401,
|
|
1,
|
|
2,
|
|
("thrusts his penis into woman a", "uses mouth and hands"),
|
|
("thrusts his penis into woman a", "uses mouth and hands"),
|
|
),
|
|
]
|
|
for name, subjects, subcategory, focus, family, position_key, seed, women_count, men_count, role_terms, krea_terms in cases:
|
|
row = _prompt_row(
|
|
name=name,
|
|
category="Hardcore sexual poses",
|
|
subcategory=subcategory,
|
|
seed=seed,
|
|
character_cast=_character_cast_subjects(subjects),
|
|
women_count=women_count,
|
|
men_count=men_count,
|
|
hardcore_position_config=_position_filter(focus, family, [position_key]),
|
|
)
|
|
_expect_custom_row(row, name)
|
|
_expect(row.get("position_family") == family, f"{name} position_family mismatch: {row.get('position_family')}")
|
|
_expect(position_key in (row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
|
role_graph = _expect_text(f"{name}.source_role_graph", row.get("source_role_graph"), 30).lower()
|
|
for term in role_terms:
|
|
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(row), target="single")
|
|
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("role graph:" not in prompt and "sexual scene:" not in prompt, f"{name} Krea leaked raw labels")
|
|
for term in krea_terms:
|
|
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
|
|
if name == "fallback_solo_man_climax":
|
|
_expect("lower back and ass" not in prompt, f"{name} Krea kept conflicting solo male climax detail: {prompt}")
|
|
_expect_formatter_outputs(row, name, target="single")
|
|
|
|
|
|
def smoke_no_expression_fallback() -> None:
|
|
cast = pb.build_character_slot_json(
|
|
subject_type="woman",
|
|
label="A",
|
|
age="25-year-old adult",
|
|
ethnicity="western_european",
|
|
body="slim",
|
|
descriptor_detail="full",
|
|
expression_enabled=False,
|
|
)["character_cast"]
|
|
row = _prompt_row(
|
|
name="hardcore_expression_disabled",
|
|
category="Hardcore sexual poses",
|
|
subcategory="Penetrative sex",
|
|
seed=2301,
|
|
character_cast=cast,
|
|
women_count=1,
|
|
men_count=1,
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
_expect_custom_row(row, "hardcore_expression_disabled")
|
|
_expect(not row.get("expression"), "expression should stay disabled without fallback")
|
|
_expect("Facial expressions:" not in row.get("prompt", ""), "disabled expression leaked into prompt")
|
|
_expect_formatter_outputs(row, "hardcore_expression_disabled", target="single")
|
|
|
|
|
|
def smoke_formatter_metadata_fixtures() -> None:
|
|
cases = [
|
|
{
|
|
"name": "fixture_penetration_text_noise",
|
|
"row": _fixture_hardcore_row(),
|
|
"krea_terms": ("penis thrusts",),
|
|
"sdxl_terms": ("penetrative sex", "missionary"),
|
|
"caption_terms": ("penetrative action",),
|
|
},
|
|
{
|
|
"name": "fixture_manual_source_family",
|
|
"row": _fixture_hardcore_row(
|
|
subcategory="Manual stimulation",
|
|
subcategory_slug="manual_stimulation",
|
|
item="wet fingers moving between the thighs, one hand braced on the hip, wet shine on fingers and inner thighs",
|
|
custom_item="Manual stimulation",
|
|
item_axis_values={
|
|
"position": "kneeling hand-between-thighs position",
|
|
"manual_act": "wet fingers moving between the thighs",
|
|
},
|
|
composition="close crop on hands and face",
|
|
source_composition="close crop on hands and face",
|
|
role_graph=(
|
|
"Woman A reclines with thighs open while Man A's hand is between her legs, "
|
|
"fingers visibly stimulating her pussy."
|
|
),
|
|
source_role_graph=(
|
|
"Woman A reclines with thighs open while Man A's hand is between her legs, "
|
|
"fingers visibly stimulating her pussy."
|
|
),
|
|
action_family="manual",
|
|
position_family="manual",
|
|
position_key="fingering",
|
|
position_keys=["fingering", "open_thighs"],
|
|
),
|
|
"krea_terms": ("fingers visibly stimulating",),
|
|
"sdxl_terms": ("manual stimulation", "fingering"),
|
|
"caption_terms": ("manual action",),
|
|
},
|
|
{
|
|
"name": "fixture_climax_family",
|
|
"row": _fixture_hardcore_row(
|
|
subcategory="Cumshot and climax",
|
|
subcategory_slug="cumshot_climax",
|
|
item="external cumshot with cum on lower back and ass, explicit semen aftermath visible",
|
|
custom_item="Cumshot and climax",
|
|
item_axis_values={"position": "on all fours with hips raised"},
|
|
composition="low-angle post-orgasm frame",
|
|
source_composition="low-angle post-orgasm frame",
|
|
role_graph=(
|
|
"Woman A is on all fours with hips raised while Man A is positioned behind her "
|
|
"and ejaculates semen across her ass, thighs, and lower back."
|
|
),
|
|
source_role_graph=(
|
|
"Woman A is on all fours with hips raised while Man A is positioned behind her "
|
|
"and ejaculates semen across her ass, thighs, and lower back."
|
|
),
|
|
action_family="climax",
|
|
position_family="climax",
|
|
position_key="doggy",
|
|
position_keys=["doggy"],
|
|
),
|
|
"krea_terms": ("ejaculates semen",),
|
|
"sdxl_terms": ("climax", "semen"),
|
|
"caption_terms": ("climax action",),
|
|
},
|
|
]
|
|
for case in cases:
|
|
name = case["name"]
|
|
row = case["row"]
|
|
_expect_custom_row(row, name)
|
|
metadata = _json(row)
|
|
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=metadata, target="single")
|
|
krea_prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 40).lower()
|
|
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
|
|
_expect("role graph:" not in krea_prompt and "sexual pose:" not in krea_prompt, f"{name}.krea leaked raw labels")
|
|
for term in case["krea_terms"]:
|
|
_expect(term in krea_prompt, f"{name}.krea missing {term!r}")
|
|
|
|
sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=metadata, target="single", trigger=SdxlTrigger, prepend_trigger=True)
|
|
sdxl_prompt = _expect_text(f"{name}.sdxl_prompt", sdxl.get("sdxl_prompt"), 40).lower()
|
|
_expect("metadata" in sdxl.get("method", ""), f"{name}.sdxl did not use metadata")
|
|
for term in case["sdxl_terms"]:
|
|
_expect(term in sdxl_prompt, f"{name}.sdxl missing {term!r}")
|
|
|
|
caption, method = caption_naturalizer.naturalize_caption("", metadata_json=metadata, trigger=Trigger, include_trigger=True)
|
|
caption_text = _expect_text(f"{name}.caption", caption, 40).lower()
|
|
_expect("metadata" in method, f"{name}.caption did not use metadata")
|
|
for term in case["caption_terms"]:
|
|
_expect(term in caption_text, f"{name}.caption missing {term!r}")
|
|
|
|
route_row = _fixture_hardcore_row(
|
|
formatter_hints={
|
|
"all": ["shared route anchor"],
|
|
"krea": ["krea readable anchor"],
|
|
"sdxl": ["sdxl route tag"],
|
|
"caption": ["caption route phrase"],
|
|
}
|
|
)
|
|
_expect_custom_row(route_row, "fixture_formatter_hints")
|
|
metadata = _json(route_row)
|
|
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=metadata, target="single")
|
|
krea_prompt = _expect_text("fixture_formatter_hints.krea_prompt", krea.get("krea_prompt"), 40).lower()
|
|
_expect("shared route anchor" in krea_prompt, "Krea formatter missed shared formatter hint")
|
|
_expect("krea readable anchor" in krea_prompt, "Krea formatter missed Krea formatter hint")
|
|
_expect("sdxl route tag" not in krea_prompt, "Krea formatter leaked SDXL formatter hint")
|
|
_expect("caption route phrase" not in krea_prompt, "Krea formatter leaked caption formatter hint")
|
|
|
|
sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=metadata, target="single", trigger=SdxlTrigger, prepend_trigger=True)
|
|
sdxl_prompt = _expect_text("fixture_formatter_hints.sdxl_prompt", sdxl.get("sdxl_prompt"), 40).lower()
|
|
_expect("shared route anchor" in sdxl_prompt, "SDXL formatter missed shared formatter hint")
|
|
_expect("sdxl route tag" in sdxl_prompt, "SDXL formatter missed SDXL formatter hint")
|
|
_expect("krea readable anchor" not in sdxl_prompt, "SDXL formatter leaked Krea formatter hint")
|
|
_expect("caption route phrase" not in sdxl_prompt, "SDXL formatter leaked caption formatter hint")
|
|
|
|
caption, method = caption_naturalizer.naturalize_caption("", metadata_json=metadata, trigger=Trigger, include_trigger=True)
|
|
caption_text = _expect_text("fixture_formatter_hints.caption", caption, 40).lower()
|
|
_expect("metadata" in method, "Caption formatter hints fixture did not use metadata")
|
|
_expect("shared route anchor" in caption_text, "Caption naturalizer missed shared formatter hint")
|
|
_expect("caption route phrase" in caption_text, "Caption naturalizer missed caption formatter hint")
|
|
_expect("krea readable anchor" not in caption_text, "Caption naturalizer leaked Krea formatter hint")
|
|
_expect("sdxl route tag" not in caption_text, "Caption naturalizer leaked SDXL formatter hint")
|
|
|
|
external_pair = {
|
|
"trigger": Trigger,
|
|
"shared_descriptor": "25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes",
|
|
"shared_cast_descriptors": [
|
|
"Woman A: 25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes",
|
|
"Man A: 40-year-old adult man, average figure, tan skin, dark hair",
|
|
],
|
|
"options": {
|
|
"softcore_cast": "same_as_hardcore",
|
|
"continuity": "same_creator_same_room",
|
|
"softcore_level": "lingerie_tease",
|
|
"hardcore_level": "hardcore",
|
|
},
|
|
"softcore_row": {
|
|
"prompt": f"{Trigger}, embedded soft pair prompt.",
|
|
"caption": f"{Trigger}, embedded soft pair caption.",
|
|
"negative_prompt": "bad hands, bad hands",
|
|
"subject_type": "configured_cast",
|
|
"primary_subject": "woman",
|
|
"item": "red satin lingerie set",
|
|
"softcore_item_prompt_label": "Erotic outfit",
|
|
"pose": "standing together beside a mirror",
|
|
"scene_text": "private studio room with warm light",
|
|
"composition": "mirror teaser composition",
|
|
"expression": "confident soft smile",
|
|
"softcore_partner_styling": {
|
|
"outfits": ["Man A wears a black buttoned shirt and dark trousers"],
|
|
"pose": "standing close with one hand at the waist",
|
|
},
|
|
"camera_config": {
|
|
"camera_mode": "standard",
|
|
"custom_camera_prompt": "front-right quarter view, eye-level shot, medium shot",
|
|
},
|
|
"camera_directive": "Camera: row soft front-right view.",
|
|
"camera_scene_directive": "Row soft mirror camera layout.",
|
|
},
|
|
"hardcore_row": {
|
|
"prompt": f"{Trigger}, embedded hard pair prompt.",
|
|
"caption": f"{Trigger}, embedded hard pair caption.",
|
|
"negative_prompt": "low quality, low quality",
|
|
"subject_type": "configured_cast",
|
|
"primary_subject": "configured_cast",
|
|
"item": "missionary position while full-body penetrative sex",
|
|
"custom_item": "Penetrative sex",
|
|
"item_label": "Sexual pose",
|
|
"item_axis_values": {
|
|
"position": "missionary position",
|
|
"penetration_act": "full-body penetrative sex",
|
|
},
|
|
"scene_text": "private studio room with warm light",
|
|
"composition": "front-facing full-body frame",
|
|
"source_composition": "front-facing full-body frame",
|
|
"role_graph": (
|
|
"Woman A lies on her back with legs open around Man A's hips while Man A is above her "
|
|
"between her thighs; Man A's penis thrusts into her pussy."
|
|
),
|
|
"source_role_graph": (
|
|
"Woman A lies on her back with legs open around Man A's hips while Man A is above her "
|
|
"between her thighs; Man A's penis thrusts into her pussy."
|
|
),
|
|
"expression": "focused adult expression",
|
|
"action_family": "penetration",
|
|
"position_family": "penetrative",
|
|
"position_key": "missionary",
|
|
"position_keys": ["missionary"],
|
|
"hardcore_clothing_state": "Woman A's red satin lingerie set is pushed aside at the hips",
|
|
"camera_config": {
|
|
"camera_mode": "standard",
|
|
"custom_camera_prompt": "right side view, eye-level shot, medium shot",
|
|
},
|
|
"camera_directive": "Camera: row hard right-side view.",
|
|
"camera_scene_directive": "Row hard body-aligned camera layout.",
|
|
},
|
|
}
|
|
external_metadata = _json(external_pair)
|
|
|
|
krea_pair = krea_formatter.format_krea2_prompt("", metadata_json=external_metadata, target="auto")
|
|
krea_soft = _expect_text("fixture_external_pair.krea_soft", krea_pair.get("krea_softcore_prompt"), 40).lower()
|
|
krea_hard = _expect_text("fixture_external_pair.krea_hard", krea_pair.get("krea_hardcore_prompt"), 40).lower()
|
|
_expect(krea_pair.get("method") == "metadata_json:krea2(insta_of_pair)", "External pair Krea route changed method")
|
|
_expect("black buttoned shirt" in krea_soft, "External pair Krea soft route lost embedded partner styling")
|
|
_expect_no_softcore_noise("fixture_external_pair.krea_soft", krea_pair.get("krea_softcore_prompt"))
|
|
_expect("styled creator-teaser frame" in krea_soft, "External pair Krea soft route lost clean cast-presence wording")
|
|
_expect("red satin lingerie set" in krea_hard, "External pair Krea hard route lost embedded clothing state")
|
|
_expect("row hard right-side view" in krea_hard, "External pair Krea hard route lost embedded camera directive")
|
|
_expect_no_duplicate_comma_items("fixture_external_pair.krea_negative", krea_pair.get("negative_prompt"))
|
|
|
|
sdxl_pair = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=external_metadata,
|
|
target="auto",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
sdxl_soft = _expect_text("fixture_external_pair.sdxl_soft", sdxl_pair.get("sdxl_softcore_prompt"), 40).lower()
|
|
sdxl_hard = _expect_text("fixture_external_pair.sdxl_hard", sdxl_pair.get("sdxl_hardcore_prompt"), 40).lower()
|
|
_expect(sdxl_pair.get("method") == "metadata_json:sdxl(insta_of_pair)", "External pair SDXL route changed method")
|
|
_expect_trigger_once("fixture_external_pair.sdxl_soft", sdxl_pair.get("sdxl_softcore_prompt"), SdxlTrigger)
|
|
_expect_trigger_once("fixture_external_pair.sdxl_hard", sdxl_pair.get("sdxl_hardcore_prompt"), SdxlTrigger)
|
|
_expect("black buttoned shirt" in sdxl_soft, "External pair SDXL soft route lost embedded partner styling")
|
|
_expect("1man" in sdxl_soft, "External pair SDXL soft route lost partner count tag")
|
|
_expect("40-year-old adult man" in sdxl_soft, "External pair SDXL soft route lost partner descriptor")
|
|
_expect("softcore teaser" in sdxl_soft, "External pair SDXL soft route lost softcore teaser tag")
|
|
_expect_no_softcore_noise("fixture_external_pair.sdxl_soft", sdxl_pair.get("sdxl_softcore_prompt"))
|
|
_expect("red satin lingerie set" in sdxl_hard, "External pair SDXL hard route lost embedded clothing state")
|
|
_expect("row hard right-side view" in sdxl_hard, "External pair SDXL hard route lost embedded camera directive")
|
|
_expect_no_duplicate_comma_items("fixture_external_pair.sdxl_negative", sdxl_pair.get("negative_prompt"))
|
|
|
|
caption, method = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=external_metadata,
|
|
target="auto",
|
|
trigger=Trigger,
|
|
include_trigger=True,
|
|
)
|
|
caption_text = _expect_text("fixture_external_pair.caption", caption, 40).lower()
|
|
_expect(method == "metadata_json:metadata(insta_of_pair)", "External pair caption route changed method")
|
|
_expect_trigger_once("fixture_external_pair.caption", caption, Trigger)
|
|
_expect("black buttoned shirt" in caption_text, "External pair caption route lost embedded partner styling")
|
|
_expect("softcore side" in caption_text, "External pair caption route lost softcore side")
|
|
_expect("hardcore side" in caption_text, "External pair caption route lost hardcore side")
|
|
_expect("softcore version" not in caption_text and "hardcore version" not in caption_text, "External pair caption kept version labels")
|
|
|
|
generated = _prompt_row(
|
|
name="fixture_generated_formatter_invariants",
|
|
category="Casual clothes",
|
|
subcategory="Casual clothes / Smart casual",
|
|
seed=4101,
|
|
men_count=0,
|
|
character_cast=_character_cast_subjects(["woman"]),
|
|
camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
|
location_config=_coworking_location_config(),
|
|
)
|
|
_expect_row_base(generated, "fixture_generated_formatter_invariants")
|
|
_expect(generated.get("source") == "json_category", "Generated formatter invariant should use category JSON route")
|
|
generated_metadata = _json(generated)
|
|
generated_item = _expect_text("fixture_generated_formatter_invariants.item", generated.get("item"), 8)
|
|
item_anchor = " ".join(generated_item.lower().split()[:3])
|
|
|
|
krea = krea_formatter.format_krea2_prompt("", metadata_json=generated_metadata, target="single")
|
|
krea_prompt = _expect_text("fixture_generated_formatter_invariants.krea_prompt", krea.get("krea_prompt"), 40).lower()
|
|
_expect("metadata" in krea.get("method", ""), "Generated Krea invariant did not use metadata")
|
|
_expect(item_anchor in krea_prompt, "Generated Krea prompt lost clothing/item metadata")
|
|
_expect("coworking lounge" in krea_prompt, "Generated Krea prompt lost scene metadata")
|
|
_expect("front-right quarter view" in krea_prompt, "Generated Krea prompt lost camera metadata")
|
|
|
|
sdxl = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=generated_metadata,
|
|
target="single",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
)
|
|
sdxl_prompt = _expect_text("fixture_generated_formatter_invariants.sdxl_prompt", sdxl.get("sdxl_prompt"), 40).lower()
|
|
_expect("metadata" in sdxl.get("method", ""), "Generated SDXL invariant did not use metadata")
|
|
_expect_trigger_once("fixture_generated_formatter_invariants.sdxl_prompt", sdxl.get("sdxl_prompt"), SdxlTrigger)
|
|
_expect(item_anchor in sdxl_prompt, "Generated SDXL prompt lost clothing/item metadata")
|
|
_expect("coworking lounge" in sdxl_prompt, "Generated SDXL prompt lost scene metadata")
|
|
_expect("front-right quarter view" in sdxl_prompt, "Generated SDXL prompt lost camera metadata")
|
|
for leaked_label in ("camera:", "composition:", "scene:", "setting:", "pose:", "clothing:"):
|
|
_expect(leaked_label not in sdxl_prompt, f"Generated SDXL prompt leaked raw label {leaked_label!r}")
|
|
|
|
caption, method = caption_naturalizer.naturalize_caption(
|
|
"",
|
|
metadata_json=generated_metadata,
|
|
trigger=Trigger,
|
|
include_trigger=False,
|
|
caption_profile="training_dense",
|
|
)
|
|
caption_text = _expect_text("fixture_generated_formatter_invariants.caption", caption, 40).lower()
|
|
_expect("metadata" in method, "Generated caption invariant did not use metadata")
|
|
_expect_trigger_once("fixture_generated_formatter_invariants.caption", caption, Trigger)
|
|
_expect(item_anchor in caption_text, "Generated training caption lost clothing/item metadata")
|
|
_expect("coworking lounge" in caption_text, "Generated training caption lost scene metadata")
|
|
|
|
|
|
def _expect_comfy_input_spec(node_name: str, group: str, input_name: str, spec: Any) -> None:
|
|
_expect(isinstance(input_name, str) and input_name, f"{node_name}.{group} has invalid input name {input_name!r}")
|
|
if group == "hidden":
|
|
_expect(
|
|
isinstance(spec, (str, tuple)),
|
|
f"{node_name}.{input_name} hidden input should be a ComfyUI hidden token or tuple",
|
|
)
|
|
return
|
|
|
|
_expect(isinstance(spec, tuple) and spec, f"{node_name}.{input_name} visible input has invalid spec")
|
|
type_spec = spec[0]
|
|
_expect(
|
|
isinstance(type_spec, str) or isinstance(type_spec, list),
|
|
f"{node_name}.{input_name} input type should be a socket type or choices list",
|
|
)
|
|
if isinstance(type_spec, list):
|
|
_expect(type_spec, f"{node_name}.{input_name} choice list should not be empty")
|
|
_expect(len(spec) >= 2 and isinstance(spec[1], dict), f"{node_name}.{input_name} visible input options should be a dict")
|
|
_expect("tooltip" in spec[1], f"{node_name}.{input_name} visible input is missing tooltip")
|
|
|
|
|
|
def smoke_node_runtime_contracts() -> None:
|
|
node_names = sorted(sxcp_nodes.NODE_CLASS_MAPPINGS)
|
|
_expect(node_names, "Node registry is empty")
|
|
_expect(
|
|
set(sxcp_nodes.NODE_CLASS_MAPPINGS) == set(sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS),
|
|
"Node class and display registries are out of sync",
|
|
)
|
|
_expect(len(node_names) >= 50, "Node registry unexpectedly small")
|
|
|
|
for node_name in node_names:
|
|
node_class = sxcp_nodes.NODE_CLASS_MAPPINGS[node_name]
|
|
display_name = sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS.get(node_name)
|
|
_expect(isinstance(display_name, str) and display_name, f"{node_name} has no display name")
|
|
|
|
input_types = node_class.INPUT_TYPES()
|
|
_expect(isinstance(input_types, dict), f"{node_name}.INPUT_TYPES should return a dict")
|
|
_expect("required" in input_types, f"{node_name}.INPUT_TYPES lost required group")
|
|
for group, inputs in input_types.items():
|
|
_expect(group in {"required", "optional", "hidden"}, f"{node_name}.INPUT_TYPES has unknown group {group!r}")
|
|
_expect(isinstance(inputs, dict), f"{node_name}.{group} inputs should be a dict")
|
|
for input_name, spec in inputs.items():
|
|
_expect_comfy_input_spec(node_name, group, input_name, spec)
|
|
|
|
return_types = getattr(node_class, "RETURN_TYPES", None)
|
|
_expect(isinstance(return_types, tuple) and return_types, f"{node_name}.RETURN_TYPES should be a non-empty tuple")
|
|
for output_type in return_types:
|
|
_expect(isinstance(output_type, str) and output_type, f"{node_name}.RETURN_TYPES contains an invalid output type")
|
|
|
|
return_names = getattr(node_class, "RETURN_NAMES", None)
|
|
if return_names is not None:
|
|
_expect(isinstance(return_names, tuple), f"{node_name}.RETURN_NAMES should be a tuple when present")
|
|
_expect(len(return_names) == len(return_types), f"{node_name}.RETURN_NAMES length does not match RETURN_TYPES")
|
|
for output_name in return_names:
|
|
_expect(isinstance(output_name, str) and output_name, f"{node_name}.RETURN_NAMES contains an invalid name")
|
|
|
|
function_name = getattr(node_class, "FUNCTION", None)
|
|
_expect(isinstance(function_name, str) and function_name, f"{node_name}.FUNCTION should name the executor method")
|
|
_expect(callable(getattr(node_class(), function_name, None)), f"{node_name}.FUNCTION target is not callable")
|
|
category = getattr(node_class, "CATEGORY", None)
|
|
_expect(isinstance(category, str) and category, f"{node_name}.CATEGORY should be a non-empty string")
|
|
|
|
|
|
def smoke_node_utility_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPGlobalSeed",
|
|
"SxCPSeedControl",
|
|
"SxCPSeedLocker",
|
|
"SxCPSDXLBucketSize",
|
|
"SxCPKrea2ResolutionSelector",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
global_seed_inputs = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"].INPUT_TYPES().get("required") or {}
|
|
global_seed_tooltip = global_seed_inputs.get("global_seed", (None, {}))[1].get("tooltip", "")
|
|
_expect("Connect seed to generator seed" in global_seed_tooltip, "Global Seed tooltip should explain generator wiring")
|
|
|
|
seed_control = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedControl"]
|
|
seed_inputs = seed_control.INPUT_TYPES().get("required") or {}
|
|
_expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input")
|
|
_expect("tooltip" in seed_inputs["category_seed_mode"][1], "Seed Control tooltip injection missing")
|
|
_expect(seed_control.RETURN_NAMES == ("seed_config", "summary"), "Seed Control lost visible summary output")
|
|
category_seed_tooltip = node_tooltips._tooltip_for_input("SxCPSeedControl", "category_seed_mode")
|
|
_expect(
|
|
"random rerolls this axis at queue time" in category_seed_tooltip
|
|
and "field value stays unchanged" in category_seed_tooltip,
|
|
"Node tooltip policy lost Seed Control override",
|
|
)
|
|
_expect(
|
|
"Autoscaling switch input" in node_tooltips._tooltip_for_input("SxCPIndexSwitch", "input_12"),
|
|
"Node tooltip policy lost autoscaling input fallback",
|
|
)
|
|
seed_control_config, seed_control_summary = seed_control().build(
|
|
"fixed",
|
|
-1,
|
|
"follow_main",
|
|
-1,
|
|
"random",
|
|
-1,
|
|
"auto",
|
|
123,
|
|
"auto",
|
|
-1,
|
|
"fixed",
|
|
777,
|
|
"follow_main",
|
|
888,
|
|
"auto",
|
|
-1,
|
|
"fixed",
|
|
999,
|
|
)
|
|
parsed_seed_control = json.loads(seed_control_config)
|
|
_expect(parsed_seed_control.get("category_seed") == 0, "Seed Control fixed mode did not clamp negative seed")
|
|
_expect(parsed_seed_control.get("subcategory_seed") == -1, "Seed Control follow_main mode should emit -1")
|
|
_expect(int(parsed_seed_control.get("content_seed", -1)) >= 0, "Seed Control random mode did not emit resolved seed")
|
|
_expect(parsed_seed_control.get("person_seed") == 123, "Seed Control auto mode did not preserve explicit value")
|
|
_expect(parsed_seed_control.get("pose_seed") == 777, "Seed Control fixed mode did not preserve positive seed")
|
|
_expect(parsed_seed_control.get("role_seed") == -1, "Seed Control follow_main role did not emit -1")
|
|
_expect(parsed_seed_control.get("composition_seed") == 999, "Seed Control fixed composition seed changed")
|
|
_expect("category=0" in seed_control_summary, "Seed Control summary lost fixed resolved seed")
|
|
_expect("subcategory=follow_main" in seed_control_summary, "Seed Control summary lost follow_main marker")
|
|
_expect(
|
|
f"content={parsed_seed_control['content_seed']}" in seed_control_summary,
|
|
"Seed Control summary lost random resolved seed value",
|
|
)
|
|
|
|
seed_locker = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedLocker"]
|
|
locker_inputs = seed_locker.INPUT_TYPES().get("required") or {}
|
|
_expect(
|
|
list(locker_inputs["reroll_axis"][0]) == seed_config.seed_reroll_axis_choices(),
|
|
"Seed Locker reroll choices drifted from seed_config",
|
|
)
|
|
|
|
seed, global_seed_config, summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"]().build(12345)
|
|
parsed_seed = json.loads(global_seed_config)
|
|
_expect(seed == 12345, "Global Seed did not return the clamped seed")
|
|
_expect(parsed_seed, "Global Seed config should not be empty")
|
|
_expect(all(int(value) == 12345 for value in parsed_seed.values()), "Global Seed config did not lock every axis")
|
|
_expect("all axes locked" in summary, "Global Seed summary changed unexpectedly")
|
|
|
|
locker_config, locker_summary = seed_locker().build(12345, "pose", 999)
|
|
parsed_locker = json.loads(locker_config)
|
|
_expect(parsed_locker.get("pose_seed") == 999, "Seed Locker did not apply pose reroll seed")
|
|
_expect("reroll pose" in locker_summary, "Seed Locker summary lost reroll axis")
|
|
|
|
bucket_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSDXLBucketSize"]()
|
|
bucket_a = bucket_node.build("portrait", 77, 3, 0)
|
|
bucket_b = bucket_node.build("portrait", 77, 3, 0)
|
|
_expect(bucket_a == bucket_b, "SDXL bucket should be deterministic for fixed seed and row")
|
|
_expect(bucket_a[3] == "portrait", "SDXL bucket ignored orientation filter")
|
|
bucket_seed_config = json.dumps({"camera_seed": 9001})
|
|
bucket_seeded_a = bucket_node.build("landscape", -1, 4, 0, bucket_seed_config)
|
|
bucket_seeded_b = bucket_node.build("landscape", -1, 4, 0, bucket_seed_config)
|
|
_expect(bucket_seeded_a == bucket_seeded_b, "SDXL bucket should honor seed_config aliases deterministically")
|
|
_expect(
|
|
bucket_node._configured_bucket_seed({"camera_seed": "42"}) == 42,
|
|
"SDXL bucket seed should delegate through composition/camera seed aliases",
|
|
)
|
|
|
|
krea_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2ResolutionSelector"]()
|
|
krea_width, krea_height, _resolution, aspect_ratio, api_aspect, _api_resolution, *_rest = krea_node.select("1.0MP", "9:16")
|
|
krea_config = json.loads(_rest[-1])
|
|
_expect(krea_height > krea_width, "Krea2 9:16 selector should return portrait dimensions")
|
|
_expect(aspect_ratio == "9:16", "Krea2 selector lost requested aspect ratio")
|
|
_expect(api_aspect == "9:16", "Krea2 selector lost API aspect mapping")
|
|
_expect(krea_config.get("width") == krea_width and krea_config.get("height") == krea_height, "Krea2 config_json dimensions mismatch")
|
|
|
|
|
|
def smoke_server_route_payload_policy() -> None:
|
|
requested, selected, available = index_switch_policy.input_selection(
|
|
0,
|
|
"zero_based",
|
|
"fallback",
|
|
{"input_1": "first"},
|
|
)
|
|
_expect((requested, selected, available) == (1, 1, [1]), "Index switch policy zero-based selection changed")
|
|
_expect(
|
|
index_switch_policy.route_selection(65, "one_based", "wrap") == (65, 1),
|
|
"Index switch policy wrap routing changed",
|
|
)
|
|
_expect(
|
|
index_switch_policy.lazy_inputs(2, "pick_input", "one_based", "fallback", {"input_2": "second"}) == ["input_2"],
|
|
"Index switch policy lazy input selection changed",
|
|
)
|
|
|
|
switch = loop_nodes.SxCPIndexSwitch()
|
|
picked = switch.switch(
|
|
2,
|
|
"pick_input",
|
|
"one_based",
|
|
"fallback",
|
|
input_1="first",
|
|
input_2="second",
|
|
fallback="fallback",
|
|
)
|
|
_expect(picked[0] == "second", "Index Switch pick_input did not select the requested input")
|
|
_expect(picked[1] == 2, "Index Switch pick_input selected_index changed")
|
|
_expect("selected=input_2" in picked[2], "Index Switch pick_input status lost selected input")
|
|
|
|
routed = switch.switch(3, "route_output", "one_based", "fallback", route_value="routed")
|
|
_expect(routed[0] == "routed", "Index Switch route_output primary value changed")
|
|
_expect(routed[1] == 3, "Index Switch route_output selected_index changed")
|
|
_expect(routed[5] == "routed", "Index Switch route_output did not route value to output_3")
|
|
|
|
key = "smoke_route_payload"
|
|
loop_nodes._ACCUMULATOR_STORES[key] = [
|
|
{"id": "first", "value": "alpha", "_sxcp_preview_key": "first-key"},
|
|
{"id": "second", "value": "beta", "_sxcp_preview_key": "second-key"},
|
|
]
|
|
try:
|
|
listed = server_routes.accumulator_list_payload({"store_key": key, "preview_limit": "0"})
|
|
_expect(listed.get("count") == 2, "Accumulator list payload lost stored entries")
|
|
_expect(listed["entries"][0].get("value") == "alpha", "Accumulator list payload lost value summary")
|
|
|
|
moved = server_routes.accumulator_move_payload({"store_key": key, "entry_id": "second", "target_index": "1"})
|
|
_expect(moved.get("moved") is True, "Accumulator move payload did not report movement")
|
|
_expect(moved.get("from_index") == 2 and moved.get("to_index") == 1, "Accumulator move payload changed indices")
|
|
_expect(moved["entries"][0].get("id") == "second", "Accumulator move payload did not reorder entries")
|
|
|
|
deleted = server_routes.accumulator_delete_payload({"store_key": key, "preview_key": "first-key"})
|
|
_expect(deleted.get("removed") == 1, "Accumulator delete payload did not remove by preview key")
|
|
_expect(deleted.get("count") == 1, "Accumulator delete payload count changed")
|
|
|
|
cleared = server_routes.accumulator_delete_payload({"store_key": key, "clear": True})
|
|
_expect(cleared.get("removed") == 1 and cleared.get("count") == 0, "Accumulator clear payload changed")
|
|
finally:
|
|
loop_nodes._ACCUMULATOR_STORES.pop(key, None)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
previous_profile_dir = character_profile.PROFILE_DIR
|
|
character_profile.PROFILE_DIR = Path(tmpdir)
|
|
try:
|
|
profile = character_profile.build_character_profile_json(
|
|
profile_name="route source",
|
|
source="manual",
|
|
subject_type="woman",
|
|
age="28-year-old adult",
|
|
body="slim",
|
|
hair="long black hair",
|
|
save_now=False,
|
|
)
|
|
saved = server_routes.profile_save_cached_payload(
|
|
{"profile_name": "Route Save!*", "profile_json": profile["profile_json"]}
|
|
)
|
|
saved_path = Path(saved.get("saved_path") or "")
|
|
_expect(saved.get("status") == "saved", "Profile save payload did not save")
|
|
_expect(saved.get("profile_name") == "Route_Save", "Profile save payload did not sanitize requested name")
|
|
_expect(saved_path.exists(), "Profile save payload did not write profile file")
|
|
finally:
|
|
character_profile.PROFILE_DIR = previous_profile_dir
|
|
|
|
|
|
def smoke_seed_config_policy() -> None:
|
|
_expect(pb.SEED_AXIS_SALTS is seed_config.SEED_AXIS_SALTS, "prompt_builder seed salts should delegate to seed_config")
|
|
_expect(pb.seed_mode_choices() == seed_config.seed_mode_choices(), "seed mode choices drifted from seed_config")
|
|
_expect(
|
|
pb.seed_reroll_axis_choices() == seed_config.seed_reroll_axis_choices(),
|
|
"seed reroll axis choices drifted from seed_config",
|
|
)
|
|
_expect(pb.normalize_seed_mode("follow main") == "follow_main", "seed mode normalizer should accept spaced labels")
|
|
_expect(pb.normalize_seed_mode("FOLLOW-MAIN") == "follow_main", "seed mode normalizer should accept hyphenated labels")
|
|
_expect(pb.normalize_reroll_axis("content pose") == "content_pose", "reroll axis normalizer should accept spaced labels")
|
|
|
|
fixed_config = json.loads(
|
|
pb.build_seed_config_json(
|
|
category_seed=-1,
|
|
content_seed=123,
|
|
pose_seed=456,
|
|
role_seed=789,
|
|
category_seed_mode="fixed",
|
|
content_seed_mode="fixed",
|
|
pose_seed_mode="follow-main",
|
|
role_seed_mode="auto",
|
|
)
|
|
)
|
|
_expect(fixed_config["category_seed"] == 0, "fixed seed mode should clamp negative seeds to zero")
|
|
_expect(fixed_config["content_seed"] == 123, "fixed seed mode should preserve positive seed")
|
|
_expect(fixed_config["pose_seed"] == -1, "follow_main seed mode should emit unlocked axis")
|
|
_expect(fixed_config["role_seed"] == 789, "auto seed mode should preserve numeric seed")
|
|
|
|
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "bad": "nope"})
|
|
_expect(parsed == {"item_seed": 44, "pose_seed": 55}, "seed parser should keep integer-like values only")
|
|
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
|
|
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
|
|
_expect(
|
|
seed_config.configured_seed_from_axes({"camera_seed": "88", "content_seed": "99"}, ("composition", "content")) == 88,
|
|
"seed helper should honor axis aliases in precedence order",
|
|
)
|
|
_expect(
|
|
seed_config.configured_seed_from_axes('{"bad": "json"', ("content",)) is None,
|
|
"seed helper should return no seed for invalid config JSON",
|
|
)
|
|
|
|
locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content pose", reroll_seed=999))
|
|
_expect(locked["content_seed"] == 999, "content_pose reroll should alter content seed")
|
|
_expect(locked["pose_seed"] == 999 and locked["role_seed"] == 999, "content_pose reroll should alter pose and role seeds")
|
|
_expect(locked["scene_seed"] == 100, "content_pose reroll should leave scene locked")
|
|
axis_trace = seed_config.axis_seed_trace({"content_seed": 44}, 99, 3, axes=("content", "scene"))
|
|
_expect(axis_trace["content"]["source"] == "configured", "Seed axis trace lost configured source")
|
|
_expect(axis_trace["content"]["seed"] == 44, "Seed axis trace lost configured seed")
|
|
_expect(axis_trace["scene"]["source"] == "main", "Seed axis trace lost main source")
|
|
_expect(axis_trace["scene"]["seed"] == 99, "Seed axis trace lost main seed")
|
|
_expect(
|
|
axis_trace["content"]["rng_seed"] == seed_config.row_seed(44, 3, seed_config.SEED_AXIS_SALTS["content"]),
|
|
"Seed axis trace lost content RNG seed",
|
|
)
|
|
|
|
rng_a = pb._axis_rng({"content_seed": 123}, "content", 999, 7)
|
|
rng_b = seed_config.axis_rng({"content_seed": 123}, "content", 999, 7)
|
|
_expect(rng_a.random() == rng_b.random(), "prompt_builder axis RNG should delegate to seed_config")
|
|
_expect(pb._row_seed(123, 7, 41) == seed_config.row_seed(123, 7, 41), "row seed wrapper drifted from seed_config")
|
|
|
|
deterministic_seed = 3901
|
|
deterministic_config = pb.build_seed_lock_config_json(base_seed=deterministic_seed)
|
|
deterministic_kwargs = {
|
|
"name": "seed_config_policy_deterministic",
|
|
"category": "Hardcore sexual poses",
|
|
"subcategory": "Penetrative sex",
|
|
"seed": deterministic_seed,
|
|
"seed_config": deterministic_config,
|
|
"character_cast": _character_cast(),
|
|
"hardcore_position_config": _action_filter("penetration_only"),
|
|
"location_config": _coworking_location_config(),
|
|
}
|
|
deterministic_a = _prompt_row(**deterministic_kwargs)
|
|
deterministic_b = _prompt_row(**deterministic_kwargs)
|
|
_expect(deterministic_a == deterministic_b, "locked seed config should reproduce identical prompt row output")
|
|
|
|
slot_descriptor_keys = ("age_band", "body_phrase", "skin", "hair", "eyes", "figure")
|
|
|
|
def slot_seed_cast(slot_seed: int) -> str:
|
|
return pb.build_character_slot_json(
|
|
subject_type="woman",
|
|
label="A",
|
|
slot_seed=slot_seed,
|
|
age="random",
|
|
ethnicity="random",
|
|
figure="random",
|
|
body="random",
|
|
hair_color="random",
|
|
hair_length="random",
|
|
hair_style="random",
|
|
descriptor_detail="full",
|
|
)["character_cast"]
|
|
|
|
def slot_seed_row(character_cast: str, reroll_seed: int) -> dict[str, Any]:
|
|
return _prompt_row(
|
|
name=f"seed_config_policy_slot_seed_{reroll_seed}",
|
|
category="Casual clothes",
|
|
subcategory="Casual clothes / Streetwear",
|
|
seed=41001,
|
|
seed_config=pb.build_seed_lock_config_json(
|
|
base_seed=41001,
|
|
reroll_axis="person",
|
|
reroll_seed=reroll_seed,
|
|
),
|
|
character_cast=character_cast,
|
|
women_count=1,
|
|
men_count=0,
|
|
)
|
|
|
|
seeded_slot_cast = slot_seed_cast(50123)
|
|
seeded_slot_a = slot_seed_row(seeded_slot_cast, 60001)
|
|
seeded_slot_b = slot_seed_row(seeded_slot_cast, 60002)
|
|
_expect(
|
|
tuple(seeded_slot_a.get(key) for key in slot_descriptor_keys)
|
|
== tuple(seeded_slot_b.get(key) for key in slot_descriptor_keys),
|
|
"slot_seed should keep random character-slot descriptors stable across person-axis rerolls",
|
|
)
|
|
seeded_trace_a = seeded_slot_a.get("generation_trace") if isinstance(seeded_slot_a.get("generation_trace"), dict) else {}
|
|
seeded_trace_b = seeded_slot_b.get("generation_trace") if isinstance(seeded_slot_b.get("generation_trace"), dict) else {}
|
|
_expect(
|
|
seeded_trace_a.get("seed_axes", {}).get("person", {}).get("seed")
|
|
!= seeded_trace_b.get("seed_axes", {}).get("person", {}).get("seed"),
|
|
"slot_seed regression check should actually reroll the person axis",
|
|
)
|
|
seeded_krea_a = krea_formatter.format_krea2_prompt("", metadata_json=_json(seeded_slot_a), target="single")
|
|
seeded_krea_b = krea_formatter.format_krea2_prompt("", metadata_json=_json(seeded_slot_b), target="single")
|
|
_expect(
|
|
seeded_krea_a.get("krea_prompt") == seeded_krea_b.get("krea_prompt"),
|
|
"slot_seed should keep final Krea prompt stable across person-axis rerolls",
|
|
)
|
|
|
|
unseeded_slot_cast = slot_seed_cast(-1)
|
|
unseeded_slot_a = slot_seed_row(unseeded_slot_cast, 60001)
|
|
unseeded_slot_b = slot_seed_row(unseeded_slot_cast, 60002)
|
|
_expect(
|
|
tuple(unseeded_slot_a.get(key) for key in slot_descriptor_keys)
|
|
!= tuple(unseeded_slot_b.get(key) for key in slot_descriptor_keys),
|
|
"unseeded random character slot should still follow person-axis rerolls",
|
|
)
|
|
|
|
pose_changed = False
|
|
for reroll_seed in range(deterministic_seed + 1, deterministic_seed + 10):
|
|
pose_reroll = _prompt_row(
|
|
name="seed_config_policy_pose_reroll",
|
|
category="Hardcore sexual poses",
|
|
subcategory="Penetrative sex",
|
|
seed=deterministic_seed,
|
|
seed_config=pb.build_seed_lock_config_json(
|
|
base_seed=deterministic_seed,
|
|
reroll_axis="pose",
|
|
reroll_seed=reroll_seed,
|
|
),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
location_config=_coworking_location_config(),
|
|
)
|
|
_expect(
|
|
pose_reroll.get("cast_descriptor_text") == deterministic_a.get("cast_descriptor_text"),
|
|
"pose reroll should keep locked cast descriptors stable",
|
|
)
|
|
_expect(
|
|
pose_reroll.get("scene_text") == deterministic_a.get("scene_text"),
|
|
"pose reroll should keep locked scene stable",
|
|
)
|
|
if (
|
|
pose_reroll.get("position_key") != deterministic_a.get("position_key")
|
|
or pose_reroll.get("source_role_graph") != deterministic_a.get("source_role_graph")
|
|
or pose_reroll.get("item") != deterministic_a.get("item")
|
|
):
|
|
pose_changed = True
|
|
break
|
|
_expect(pose_changed, "pose reroll should change pose/action metadata while cast and scene stay locked")
|
|
|
|
|
|
def smoke_prompt_route_simulation_policy() -> None:
|
|
report = prompt_route_simulation.run_simulation(seed=3901, include_prompts=False)
|
|
summary = report.get("summary") or {}
|
|
_expect(summary.get("cases") == 11, "Prompt route simulation case count changed unexpectedly")
|
|
_expect(summary.get("axis_checks") == 6, "Prompt route simulation lost axis check coverage")
|
|
_expect(summary.get("issues") == 0, f"Prompt route simulation reported issues: {report.get('issues')}")
|
|
cases = {case.get("name"): case for case in report.get("cases") or []}
|
|
for route_name in (
|
|
"hardcore.single.oral",
|
|
"hardcore.single.manual",
|
|
"hardcore.single.outercourse",
|
|
"hardcore.single.foreplay",
|
|
"hardcore.single.anal",
|
|
"hardcore.single.climax",
|
|
):
|
|
_expect(route_name in cases, f"Prompt route simulation lost route family case {route_name}")
|
|
pov_hard = cases.get("insta_pair.pov_outercourse.hardcore") or {}
|
|
pov_summary = pov_hard.get("summary") or {}
|
|
_expect(
|
|
pov_summary.get("position_key") == "penis_licking",
|
|
"Prompt route simulation should catch selected outercourse position as primary position_key",
|
|
)
|
|
_expect(
|
|
"penis_licking" in (pov_summary.get("position_keys") or []),
|
|
"Prompt route simulation lost selected outercourse key from position_keys",
|
|
)
|
|
axis_checks = {check.get("name"): check for check in report.get("axis_checks") or []}
|
|
for check_name in (
|
|
"seed_axis.locked_determinism",
|
|
"seed_axis.person_reroll",
|
|
"seed_axis.scene_reroll",
|
|
"seed_axis.pose_reroll",
|
|
"seed_axis.expression_reroll",
|
|
"seed_axis.composition_reroll",
|
|
):
|
|
check = axis_checks.get(check_name) or {}
|
|
_expect(check, f"Prompt route simulation lost seed-axis check {check_name}")
|
|
_expect(not check.get("issues"), f"Prompt route simulation seed-axis check reported issues: {check_name}")
|
|
_expect(axis_checks["seed_axis.locked_determinism"].get("changed") is False, "Locked determinism check should not be a reroll")
|
|
for check_name in (
|
|
"seed_axis.person_reroll",
|
|
"seed_axis.scene_reroll",
|
|
"seed_axis.pose_reroll",
|
|
"seed_axis.expression_reroll",
|
|
"seed_axis.composition_reroll",
|
|
):
|
|
_expect(axis_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can reroll")
|
|
|
|
|
|
def smoke_node_camera_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPCameraControl",
|
|
"SxCPCameraOrbitControl",
|
|
"SxCPQwenCameraTranslator",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
camera_control = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCameraControl"]
|
|
camera_inputs = camera_control.INPUT_TYPES().get("required") or {}
|
|
_expect("camera_mode" in camera_inputs, "Camera Control lost camera_mode input")
|
|
_expect("tooltip" in camera_inputs["camera_mode"][1], "Camera Control tooltip injection missing")
|
|
camera_config = camera_control().build(
|
|
"handheld_selfie",
|
|
"three_quarter_body",
|
|
"high_angle",
|
|
"smartphone_wide",
|
|
"arm_length",
|
|
"vertical_story",
|
|
"phone_visible",
|
|
"locked",
|
|
"compact",
|
|
)[0]
|
|
parsed_camera = json.loads(camera_config)
|
|
_expect(parsed_camera.get("camera_mode") == "handheld_selfie", "Camera Control lost camera_mode")
|
|
_expect(parsed_camera.get("phone_visibility") == "phone_visible", "Camera Control lost phone visibility")
|
|
|
|
orbit_config, orbit_prompt, orbit_info = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCameraOrbitControl"]().build(
|
|
True,
|
|
"standard",
|
|
45,
|
|
0,
|
|
5.5,
|
|
"from_zoom",
|
|
"auto",
|
|
"auto",
|
|
"auto",
|
|
"auto",
|
|
"soft_hint",
|
|
"compact",
|
|
True,
|
|
)
|
|
parsed_orbit = json.loads(orbit_config)
|
|
_expect(parsed_orbit.get("camera_source") == "orbit", "Orbit camera lost source metadata")
|
|
_expect("front-right quarter view" in orbit_prompt, "Orbit camera prompt lost direction")
|
|
_expect(json.loads(orbit_info).get("orbit_azimuth") == 45, "Orbit info JSON lost azimuth")
|
|
|
|
qwen_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPQwenCameraTranslator"]
|
|
qwen_inputs = qwen_node.INPUT_TYPES()
|
|
_expect("camera_info" in (qwen_inputs.get("optional") or {}), "Qwen translator lost camera_info optional input")
|
|
qwen_config, qwen_prompt, qwen_info = qwen_node().build(
|
|
"<sks> front-right quarter view eye-level shot medium shot",
|
|
True,
|
|
"standard",
|
|
"auto",
|
|
"auto",
|
|
"auto",
|
|
"auto",
|
|
"soft_hint",
|
|
"compact",
|
|
False,
|
|
True,
|
|
)
|
|
parsed_qwen = json.loads(qwen_config)
|
|
_expect(parsed_qwen.get("camera_source") == "qwen_multiangle_prompt", "Qwen translator lost source metadata")
|
|
_expect(parsed_qwen.get("phone_visibility") == "auto", "Qwen translator should suppress phone visibility by default")
|
|
_expect("front-right quarter view" in qwen_prompt, "Qwen camera prompt lost direction")
|
|
_expect(json.loads(qwen_info).get("qwen_prompt", "").startswith("<sks>"), "Qwen info JSON lost original prompt")
|
|
|
|
|
|
def smoke_node_route_config_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPCategoryPreset",
|
|
"SxCPLocationPool",
|
|
"SxCPCompositionPool",
|
|
"SxCPLocationTheme",
|
|
"SxCPCastControl",
|
|
"SxCPCastBias",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
category_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCategoryPreset"]
|
|
category_inputs = category_node.INPUT_TYPES().get("required") or {}
|
|
_expect("preset" in category_inputs, "Category Preset lost preset input")
|
|
_expect("tooltip" in category_inputs["preset"][1], "Category Preset tooltip injection missing")
|
|
category_config, category, subcategory = category_node().build("auto_weighted", "random")
|
|
parsed_category = json.loads(category_config)
|
|
_expect(category == parsed_category.get("category") == "auto_weighted", "Category Preset output category mismatch")
|
|
_expect(subcategory == "random", "Category Preset output subcategory mismatch")
|
|
|
|
location_config, location_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPLocationPool"]().build(
|
|
True,
|
|
"replace",
|
|
"custom_only",
|
|
"classical library stacks with brass lamps",
|
|
)
|
|
parsed_location = json.loads(location_config)
|
|
_expect(parsed_location.get("scene_entries"), "Location Pool did not keep custom location")
|
|
_expect("locations=1" in location_summary, "Location Pool summary lost custom count")
|
|
|
|
composition_config, composition_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCompositionPool"]().build(
|
|
True,
|
|
"replace",
|
|
"no_outfit_check",
|
|
"long aisle composition with shelves repeating behind the subject",
|
|
)
|
|
parsed_composition = json.loads(composition_config)
|
|
_expect(parsed_composition.get("composition_entries"), "Composition Pool did not keep composition entries")
|
|
_expect("compositions=" in composition_summary, "Composition Pool summary lost composition count")
|
|
|
|
theme_location, theme_composition, theme_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPLocationTheme"]().build(
|
|
True,
|
|
"replace",
|
|
"semi_public_affair",
|
|
"",
|
|
"",
|
|
)
|
|
_expect(json.loads(theme_location).get("scene_entries"), "Location Theme did not output locations")
|
|
_expect(json.loads(theme_composition).get("composition_entries"), "Location Theme did not output compositions")
|
|
_expect("semi_public_affair" in theme_summary, "Location Theme summary lost theme name")
|
|
|
|
cast_config, women_count, men_count, cast_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCastControl"]().build(
|
|
"mixed_couple",
|
|
1,
|
|
1,
|
|
)
|
|
parsed_cast = json.loads(cast_config)
|
|
_expect((women_count, men_count) == (parsed_cast.get("women_count"), parsed_cast.get("men_count")), "Cast Control count outputs mismatch")
|
|
_expect("1 women, 1 men" in cast_summary, "Cast Control summary changed unexpectedly")
|
|
|
|
cast_bias = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCastBias"]()
|
|
bias_a = cast_bias.build(123, 2, "0.7,0.3", 1, "0.4,0.6", 0, "force_one_woman")
|
|
bias_b = cast_bias.build(123, 2, "0.7,0.3", 1, "0.4,0.6", 0, "force_one_woman")
|
|
_expect(bias_a == bias_b, "Cast Bias should be deterministic for fixed seed and row")
|
|
_expect(bias_a[1] + bias_a[2] >= 1, "Cast Bias empty behavior allowed empty cast")
|
|
_expect("weighted cast:" in bias_a[3], "Cast Bias summary lost weighted cast label")
|
|
bias_seed_config = json.dumps({"category": 702})
|
|
seeded_bias_a = cast_bias.build(-1, 2, "0.7,0.3", 1, "0.4,0.6", 0, "force_one_woman", bias_seed_config)
|
|
seeded_bias_b = cast_bias.build(-1, 2, "0.7,0.3", 1, "0.4,0.6", 0, "force_one_woman", bias_seed_config)
|
|
_expect(seeded_bias_a == seeded_bias_b, "Cast Bias should honor seed_config aliases deterministically")
|
|
_expect(cast_bias._configured_cast_seed({"category": "702"}) == 702, "Cast Bias seed should delegate through category aliases")
|
|
|
|
|
|
def smoke_node_character_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPHairLength",
|
|
"SxCPHairColor",
|
|
"SxCPHairStyle",
|
|
"SxCPCharacterAgeRange",
|
|
"SxCPCharacterBodyPool",
|
|
"SxCPWomanBodyPool",
|
|
"SxCPManBodyPool",
|
|
"SxCPEyeColorPool",
|
|
"SxCPCharacterClothing",
|
|
"SxCPCharacterManualDetails",
|
|
"SxCPWomanSlot",
|
|
"SxCPManSlot",
|
|
"SxCPCharacterSlot",
|
|
"SxCPCharacterProfileSave",
|
|
"SxCPCharacterProfileLoad",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
woman_slot_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPWomanSlot"]
|
|
woman_slot_input_types = woman_slot_node.INPUT_TYPES()
|
|
woman_slot_inputs = woman_slot_input_types.get("required") or {}
|
|
_expect("slot_seed" in woman_slot_inputs, "Woman Slot lost slot_seed input")
|
|
_expect("tooltip" in woman_slot_inputs["slot_seed"][1], "Woman Slot tooltip injection missing")
|
|
woman_slot_optional = woman_slot_input_types.get("optional") or {}
|
|
cast_tooltip = woman_slot_optional.get("character_cast", (None, {}))[1].get("tooltip", "")
|
|
_expect("incoming cast from the previous slot" in cast_tooltip, "Woman Slot character_cast tooltip should explain chain order")
|
|
man_slot_required = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPManSlot"].INPUT_TYPES().get("required") or {}
|
|
presence_tooltip = man_slot_required.get("presence_mode", (None, {}))[1].get("tooltip", "")
|
|
_expect("first-person viewer" in presence_tooltip, "Man Slot presence tooltip should explain POV behavior")
|
|
|
|
hair_config, hair_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPHairColor"]().build(
|
|
"replace_axis",
|
|
"",
|
|
include_blonde=True,
|
|
)
|
|
parsed_hair = json.loads(hair_config)
|
|
_expect(parsed_hair.get("colors") == ["blonde"], "Hair Color did not output selected blonde pool")
|
|
_expect("colors=blonde" in hair_summary, "Hair Color summary changed unexpectedly")
|
|
|
|
age_config, age_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterAgeRange"]().build(
|
|
"replace_axis",
|
|
25,
|
|
27,
|
|
)
|
|
parsed_age = json.loads(age_config)
|
|
_expect(parsed_age.get("ages") == ["25-year-old adult", "26-year-old adult", "27-year-old adult"], "Age Range output changed")
|
|
_expect("25-year-old adult" in age_summary, "Age Range summary lost selected ages")
|
|
|
|
body_config, _body_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPWomanBodyPool"]().build(
|
|
"replace_axis",
|
|
"",
|
|
include_curvy=True,
|
|
)
|
|
_expect(json.loads(body_config).get("bodies") == ["curvy"], "Woman Body Pool did not output selected body")
|
|
|
|
eye_config, _eye_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPEyeColorPool"]().build(
|
|
"replace_axis",
|
|
"",
|
|
include_blue=True,
|
|
)
|
|
_expect(json.loads(eye_config).get("eyes") == ["blue"], "Eye Color Pool did not output selected eye color")
|
|
|
|
clothing_config, clothing_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterClothing"]().build(
|
|
"replace_axis",
|
|
"lingerie_tease",
|
|
"fully_nude",
|
|
"",
|
|
"",
|
|
)
|
|
parsed_clothing = json.loads(clothing_config)
|
|
_expect(parsed_clothing.get("softcore_outfits"), "Character Clothing lost softcore outfit pool")
|
|
_expect(parsed_clothing.get("hardcore_clothing") == ["fully nude"], "Character Clothing lost hardcore clothing state")
|
|
_expect("soft_outfits=" in clothing_summary, "Character Clothing summary lost outfit count")
|
|
|
|
manual_config, manual_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterManualDetails"]().build(
|
|
"merge_nonempty",
|
|
"31-year-old adult",
|
|
"curvy",
|
|
"custom body phrase",
|
|
"warm skin",
|
|
"short blonde hair",
|
|
"blue eyes",
|
|
"red dress",
|
|
"fully nude",
|
|
)
|
|
parsed_manual = json.loads(manual_config)
|
|
_expect(parsed_manual.get("manual_age") == "31-year-old adult", "Manual Details lost manual_age")
|
|
_expect(parsed_manual.get("softcore_outfit") == "red dress", "Manual Details lost softcore outfit")
|
|
_expect("manual_age=31-year-old adult" in manual_summary, "Manual Details summary changed unexpectedly")
|
|
|
|
cast, slot, slot_summary, slot_status = woman_slot_node().build(
|
|
True,
|
|
"A",
|
|
123,
|
|
"25-year-old adult",
|
|
"western_european",
|
|
"balanced",
|
|
"curvy",
|
|
"full",
|
|
True,
|
|
0.5,
|
|
0.4,
|
|
0.8,
|
|
"",
|
|
"",
|
|
"",
|
|
hair_config,
|
|
"",
|
|
)
|
|
parsed_slot = json.loads(slot)
|
|
_expect(parsed_slot.get("subject_type") == "woman", "Woman Slot output lost subject type")
|
|
_expect(parsed_slot.get("slot_seed") == 123, "Woman Slot output lost slot seed")
|
|
_expect("Woman A" in slot_summary, "Woman Slot summary lost label")
|
|
_expect("1 slot(s)" in slot_status, "Woman Slot status lost cast count")
|
|
_expect(json.loads(cast).get("slots"), "Woman Slot did not output chained cast JSON")
|
|
|
|
man_cast, man_slot, _man_summary, _man_status = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPManSlot"]().build(
|
|
True,
|
|
"A",
|
|
124,
|
|
"40-year-old adult",
|
|
"western_european",
|
|
"average",
|
|
"compact",
|
|
True,
|
|
0.3,
|
|
"pov",
|
|
0.2,
|
|
0.7,
|
|
cast,
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
)
|
|
_expect(json.loads(man_slot).get("presence_mode") == "pov", "Man Slot output lost POV presence mode")
|
|
_expect(len(json.loads(man_cast).get("slots") or []) == 2, "Man Slot did not append to incoming cast")
|
|
|
|
save_result = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterProfileSave"]().build(
|
|
"smoke_profile",
|
|
"character_slot",
|
|
"woman",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
False,
|
|
character_slot=slot,
|
|
)
|
|
saved_profile = save_result["result"][0]
|
|
_expect(save_result["result"][2] == "smoke_profile", "Profile Save lost profile name")
|
|
_expect(save_result["result"][4] == "not_saved", "Profile Save should not write when save_now is false")
|
|
loaded_profile = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCharacterProfileLoad"]().build(
|
|
True,
|
|
"manual",
|
|
"",
|
|
False,
|
|
False,
|
|
fallback_profile_json=saved_profile,
|
|
)
|
|
_expect(loaded_profile[4] == "fallback", "Profile Load should consume fallback profile JSON")
|
|
_expect(json.loads(loaded_profile[0]).get("profile_type") == "character", "Profile Load returned wrong profile type")
|
|
|
|
|
|
def smoke_node_hardcore_position_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPHardcorePositionPool",
|
|
"SxCPHardcoreActionFilter",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
position_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPHardcorePositionPool"]
|
|
position_inputs = position_node.INPUT_TYPES().get("required") or {}
|
|
_expect("family" in position_inputs, "Hardcore Position Pool lost family input")
|
|
_expect("tooltip" in position_inputs["family"][1], "Hardcore Position Pool tooltip injection missing")
|
|
pool_config, pool_summary = position_node().build(
|
|
"replace",
|
|
"oral",
|
|
"",
|
|
include_boobjob=True,
|
|
include_handjob=True,
|
|
)
|
|
parsed_pool = json.loads(pool_config)
|
|
_expect(parsed_pool.get("family") == "oral", "Hardcore Position Pool lost selected family")
|
|
_expect(parsed_pool.get("positions") == ["boobjob", "handjob"], "Hardcore Position Pool lost selected positions")
|
|
_expect("positions=boobjob,handjob" in pool_summary, "Hardcore Position Pool summary changed unexpectedly")
|
|
|
|
filter_config, filter_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPHardcoreActionFilter"]().build(
|
|
"outercourse_only",
|
|
False,
|
|
False,
|
|
False,
|
|
True,
|
|
True,
|
|
True,
|
|
False,
|
|
True,
|
|
False,
|
|
False,
|
|
pool_config,
|
|
)
|
|
parsed_filter = json.loads(filter_config)
|
|
_expect(parsed_filter.get("family") == "outercourse", "Hardcore Action Filter did not apply focus family")
|
|
_expect(parsed_filter.get("positions") == ["boobjob", "handjob"], "Hardcore Action Filter lost incoming positions")
|
|
_expect(parsed_filter.get("allow_penetration") is False, "Hardcore Action Filter did not block penetration")
|
|
_expect(parsed_filter.get("allow_outercourse") is True, "Hardcore Action Filter should allow outercourse")
|
|
_expect("blocked=" in filter_summary, "Hardcore Action Filter summary lost blocked-gate details")
|
|
|
|
|
|
def smoke_node_formatter_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPCaptionNaturalizer",
|
|
"SxCPKrea2Formatter",
|
|
"SxCPSDXLFormatter",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
krea_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2Formatter"]
|
|
krea_inputs = krea_node.INPUT_TYPES().get("required") or {}
|
|
_expect("source_text" in krea_inputs, "Krea2 Formatter lost source_text input")
|
|
_expect("tooltip" in krea_inputs["source_text"][1], "Krea2 Formatter tooltip injection missing")
|
|
krea_optional = krea_node.INPUT_TYPES().get("optional") or {}
|
|
_expect("source_text_input" in krea_optional, "Krea2 Formatter lost connectable source_text_input")
|
|
_expect("metadata_json" in krea_optional, "Krea2 Formatter lost metadata_json input")
|
|
_expect(krea_optional["metadata_json"][1].get("forceInput") is True, "Krea2 Formatter metadata_json should be connectable")
|
|
_expect(krea_optional["negative_prompt"][1].get("forceInput") is True, "Krea2 Formatter negative_prompt should be connectable")
|
|
|
|
caption, caption_method, caption_trace_json = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCaptionNaturalizer"]().build(
|
|
"A woman standing by a window, best quality",
|
|
"caption_or_prompt",
|
|
"manual_controls",
|
|
"concise",
|
|
"drop_style_tail",
|
|
"sxcppnl7",
|
|
True,
|
|
)
|
|
_expect_text("node_formatter.caption", caption, 20)
|
|
_expect(caption.startswith("sxcppnl7"), "Caption Naturalizer did not prepend trigger")
|
|
_expect("text(" in caption_method, "Caption Naturalizer method changed unexpectedly")
|
|
caption_trace = json.loads(_expect_text("node_formatter.caption_trace", caption_trace_json, 20))
|
|
_expect(caption_trace.get("formatter") == "caption", "Caption Naturalizer trace lost formatter")
|
|
_expect(caption_trace.get("branch") == "text", "Caption Naturalizer trace lost text branch")
|
|
caption_inputs = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCaptionNaturalizer"].INPUT_TYPES().get("required") or {}
|
|
_expect("caption_profile" in caption_inputs, "Caption Naturalizer lost caption_profile input")
|
|
_expect("target" in caption_inputs, "Caption Naturalizer lost target input")
|
|
_expect(caption_inputs["target"][0] == formatter_target.target_choices(), "Caption Naturalizer target choices drifted")
|
|
_expect(caption_inputs["detail_level"][0] == formatter_detail.detail_level_choices(), "Caption Naturalizer detail choices drifted")
|
|
_expect(caption_inputs["style_policy"][0] == caption_policy.style_policy_choices(), "Caption Naturalizer style choices drifted")
|
|
_expect("tooltip" in caption_inputs["caption_profile"][1], "Caption profile tooltip injection missing")
|
|
_expect(krea_inputs["target"][0] == formatter_target.target_choices(), "Krea2 Formatter target choices drifted")
|
|
_expect(krea_inputs["detail_level"][0] == formatter_detail.detail_level_choices(), "Krea2 Formatter detail choices drifted")
|
|
_expect(krea_inputs["style_mode"][0] == krea_format_route.style_mode_choices(), "Krea2 Formatter style choices drifted")
|
|
|
|
krea_output = krea_node().build(
|
|
"sxcppnl7 A woman standing by a window",
|
|
"prompt",
|
|
"single",
|
|
"concise",
|
|
"preserve",
|
|
True,
|
|
)
|
|
_expect_text("node_formatter.krea_prompt", krea_output[0], 20)
|
|
_expect("sxcppnl7" in krea_output[0], "Krea2 Formatter did not preserve trigger when enabled")
|
|
_expect(krea_output[6].startswith("text("), "Krea2 Formatter method changed unexpectedly")
|
|
krea_trace = json.loads(_expect_text("node_formatter.krea_trace", krea_output[7], 20))
|
|
_expect(krea_trace.get("formatter") == "krea2", "Krea2 Formatter trace lost formatter")
|
|
_expect(krea_trace.get("branch") == "fallback", "Krea2 Formatter trace lost fallback branch")
|
|
|
|
sdxl_output = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSDXLFormatter"]().build(
|
|
"A woman standing by a window",
|
|
"prompt",
|
|
"single",
|
|
"manual_controls",
|
|
"flat_vector_pony",
|
|
"pony_high",
|
|
"mythp0rt",
|
|
True,
|
|
False,
|
|
1.29,
|
|
)
|
|
_expect_text("node_formatter.sdxl_prompt", sdxl_output[0], 40)
|
|
_expect_trigger_once("node_formatter.sdxl_prompt", sdxl_output[0], "mythp0rt")
|
|
_expect_text("node_formatter.sdxl_negative", sdxl_output[1], 20)
|
|
_expect(sdxl_output[6].startswith("text("), "SDXL Formatter method changed unexpectedly")
|
|
sdxl_trace = json.loads(_expect_text("node_formatter.sdxl_trace", sdxl_output[7], 20))
|
|
_expect(sdxl_trace.get("formatter") == "sdxl", "SDXL Formatter trace lost formatter")
|
|
_expect(sdxl_trace.get("branch") == "fallback", "SDXL Formatter trace lost fallback branch")
|
|
sdxl_inputs = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSDXLFormatter"].INPUT_TYPES().get("required") or {}
|
|
_expect("formatter_profile" in sdxl_inputs, "SDXL Formatter lost formatter_profile input")
|
|
_expect(sdxl_inputs["target"][0] == formatter_target.target_choices(), "SDXL Formatter target choices drifted")
|
|
_expect("tooltip" in sdxl_inputs["formatter_profile"][1], "SDXL formatter_profile tooltip injection missing")
|
|
|
|
pair = pb.build_insta_of_pair(
|
|
row_number=1,
|
|
start_index=1,
|
|
seed=4311,
|
|
ethnicity="any",
|
|
figure="random",
|
|
no_plus_women=False,
|
|
no_black=False,
|
|
trigger=Trigger,
|
|
prepend_trigger_to_prompt=True,
|
|
options_json=_insta_options(),
|
|
character_cast=_character_cast(),
|
|
hardcore_position_config=_action_filter("penetration_only"),
|
|
)
|
|
pair_metadata = _json(pair)
|
|
|
|
expected_krea_pair = krea_formatter.format_krea2_prompt(
|
|
"",
|
|
metadata_json=pair_metadata,
|
|
input_hint="metadata_json",
|
|
target="hardcore",
|
|
detail_level="balanced",
|
|
style_mode="preserve",
|
|
extra_positive="node krea pair marker",
|
|
extra_negative="node krea pair negative",
|
|
)
|
|
node_krea_pair = krea_node().build(
|
|
"",
|
|
"metadata_json",
|
|
"hardcore",
|
|
"balanced",
|
|
"preserve",
|
|
False,
|
|
metadata_json=pair_metadata,
|
|
extra_positive="node krea pair marker",
|
|
extra_negative="node krea pair negative",
|
|
)
|
|
_expect(
|
|
node_krea_pair
|
|
== (
|
|
expected_krea_pair["krea_prompt"],
|
|
expected_krea_pair["negative_prompt"],
|
|
expected_krea_pair["krea_softcore_prompt"],
|
|
expected_krea_pair["krea_hardcore_prompt"],
|
|
expected_krea_pair["softcore_negative_prompt"],
|
|
expected_krea_pair["hardcore_negative_prompt"],
|
|
expected_krea_pair["method"],
|
|
expected_krea_pair["route_trace_json"],
|
|
),
|
|
"Krea2 Formatter node output drifted from public metadata formatter",
|
|
)
|
|
|
|
expected_sdxl_pair = sdxl_formatter.format_sdxl_prompt(
|
|
"",
|
|
metadata_json=pair_metadata,
|
|
input_hint="metadata_json",
|
|
target="hardcore",
|
|
formatter_profile="manual_controls",
|
|
style_preset="flat_vector_pony",
|
|
quality_preset="pony_high",
|
|
trigger=SdxlTrigger,
|
|
prepend_trigger=True,
|
|
preserve_trigger=False,
|
|
nude_weight=1.29,
|
|
extra_positive="node sdxl pair marker",
|
|
extra_negative="node sdxl pair negative",
|
|
)
|
|
node_sdxl_pair = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSDXLFormatter"]().build(
|
|
"",
|
|
"metadata_json",
|
|
"hardcore",
|
|
"manual_controls",
|
|
"flat_vector_pony",
|
|
"pony_high",
|
|
SdxlTrigger,
|
|
True,
|
|
False,
|
|
1.29,
|
|
metadata_json=pair_metadata,
|
|
extra_positive="node sdxl pair marker",
|
|
extra_negative="node sdxl pair negative",
|
|
)
|
|
_expect(
|
|
node_sdxl_pair
|
|
== (
|
|
expected_sdxl_pair["sdxl_prompt"],
|
|
expected_sdxl_pair["negative_prompt"],
|
|
expected_sdxl_pair["sdxl_softcore_prompt"],
|
|
expected_sdxl_pair["sdxl_hardcore_prompt"],
|
|
expected_sdxl_pair["softcore_negative_prompt"],
|
|
expected_sdxl_pair["hardcore_negative_prompt"],
|
|
expected_sdxl_pair["method"],
|
|
expected_sdxl_pair["route_trace_json"],
|
|
),
|
|
"SDXL Formatter node output drifted from public metadata formatter",
|
|
)
|
|
|
|
expected_caption_pair = caption_naturalizer.naturalize_caption_with_trace(
|
|
"",
|
|
metadata_json=pair_metadata,
|
|
input_hint="metadata_json",
|
|
target="hardcore",
|
|
trigger=Trigger,
|
|
include_trigger=True,
|
|
detail_level="balanced",
|
|
style_policy="drop_style_tail",
|
|
caption_profile="training_dense",
|
|
)
|
|
node_caption_pair = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPCaptionNaturalizer"]().build(
|
|
"",
|
|
"metadata_json",
|
|
"training_dense",
|
|
"balanced",
|
|
"drop_style_tail",
|
|
Trigger,
|
|
True,
|
|
"hardcore",
|
|
metadata_json=pair_metadata,
|
|
)
|
|
_expect(
|
|
node_caption_pair == expected_caption_pair,
|
|
"Caption Naturalizer node output drifted from public metadata formatter",
|
|
)
|
|
|
|
|
|
def smoke_node_insta_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPInstaOFOptions",
|
|
"SxCPInstaOFPromptPair",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
options_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPInstaOFOptions"]
|
|
options_inputs = options_node.INPUT_TYPES().get("required") or {}
|
|
_expect("hardcore_detail_density" in options_inputs, "Insta/OF Options lost hardcore_detail_density input")
|
|
_expect("tooltip" in options_inputs["hardcore_detail_density"][1], "Insta/OF Options tooltip injection missing")
|
|
options_json = options_node().build(
|
|
"same_as_hardcore",
|
|
"couple",
|
|
1,
|
|
1,
|
|
"lingerie_tease",
|
|
"hardcore",
|
|
True,
|
|
True,
|
|
0.45,
|
|
0.85,
|
|
"hybrid",
|
|
"same_creator_same_room",
|
|
"explicit_nude",
|
|
"standard",
|
|
"standard",
|
|
"compact",
|
|
"balanced",
|
|
)[0]
|
|
parsed_options = json.loads(options_json)
|
|
_expect(parsed_options.get("softcore_cast") == "same_as_hardcore", "Insta/OF Options lost softcore cast")
|
|
_expect(parsed_options.get("hardcore_cast") == "couple", "Insta/OF Options lost hardcore cast")
|
|
_expect(parsed_options.get("hardcore_clothing_continuity") == "explicit_nude", "Insta/OF Options lost clothing continuity")
|
|
|
|
pair_output = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPInstaOFPromptPair"]().build(
|
|
1,
|
|
41,
|
|
123,
|
|
"any",
|
|
"random",
|
|
Trigger,
|
|
True,
|
|
options_json=options_json,
|
|
)
|
|
_expect_text("node_insta.softcore_prompt", pair_output[0], 20)
|
|
_expect_text("node_insta.hardcore_prompt", pair_output[1], 20)
|
|
pair = json.loads(pair_output[7])
|
|
_expect_pair(pair, "node_insta_pair")
|
|
_expect(pair.get("options", {}).get("hardcore_cast") == "couple", "Insta/OF Prompt Pair lost options metadata")
|
|
|
|
|
|
def smoke_node_builder_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPPromptBuilder",
|
|
"SxCPPromptBuilderFromConfigs",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
builder_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPPromptBuilder"]
|
|
builder_inputs = builder_node.INPUT_TYPES().get("required") or {}
|
|
_expect("category" in builder_inputs, "Prompt Builder lost category input")
|
|
_expect("tooltip" in builder_inputs["category"][1], "Prompt Builder tooltip injection missing")
|
|
direct_output = builder_node().build(
|
|
"woman",
|
|
"random",
|
|
1,
|
|
41,
|
|
123,
|
|
"full",
|
|
"any",
|
|
"standard",
|
|
True,
|
|
0.5,
|
|
0.0,
|
|
"random",
|
|
1,
|
|
0,
|
|
-1,
|
|
-1,
|
|
Trigger,
|
|
True,
|
|
)
|
|
direct_row = json.loads(direct_output[3])
|
|
_expect_row_base(direct_row, "node_builder.direct_row")
|
|
_expect(direct_output[0] == direct_row.get("prompt"), "Prompt Builder prompt output drifted from metadata")
|
|
_expect(direct_output[4] == direct_row.get("main_category"), "Prompt Builder category output drifted from metadata")
|
|
direct_trace = direct_row.get("generation_trace")
|
|
_expect(isinstance(direct_trace, dict), "Prompt Builder metadata lost generation_trace")
|
|
_expect(direct_trace.get("branch") == "built_in", "Prompt Builder metadata generation_trace lost branch")
|
|
_expect(direct_trace.get("seed_axes", {}).get("content", {}).get("source") == "main", "Prompt Builder metadata trace lost content seed source")
|
|
_expect_trigger_once("node_builder.direct_prompt", direct_output[0], Trigger)
|
|
|
|
config_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPPromptBuilderFromConfigs"]
|
|
config_inputs = config_node.INPUT_TYPES()
|
|
_expect("category_config" in (config_inputs.get("optional") or {}), "Prompt Builder From Configs lost category_config input")
|
|
config_output = config_node().build(
|
|
1,
|
|
41,
|
|
123,
|
|
category_config=pb.build_category_config_json("woman", "random"),
|
|
cast_config=pb.build_cast_config_json("solo_woman", 1, 0),
|
|
generation_profile=pb.build_generation_profile_json(profile="balanced"),
|
|
)
|
|
config_row = json.loads(config_output[3])
|
|
_expect_row_base(config_row, "node_builder.config_row")
|
|
_expect(config_output[0] == config_row.get("prompt"), "Prompt Builder From Configs prompt output drifted from metadata")
|
|
_expect(config_output[4] == config_row.get("main_category"), "Prompt Builder From Configs category output drifted from metadata")
|
|
config_trace = config_row.get("generation_trace")
|
|
_expect(isinstance(config_trace, dict), "Prompt Builder From Configs metadata lost generation_trace")
|
|
_expect(config_trace.get("builder") == "prompt_builder", "Prompt Builder From Configs trace lost builder label")
|
|
_expect_text("node_builder.config_caption", config_output[2], 20)
|
|
|
|
|
|
def smoke_node_profile_filter_registration() -> None:
|
|
required_nodes = [
|
|
"SxCPGenerationProfile",
|
|
"SxCPAdvancedFilters",
|
|
"SxCPEthnicityList",
|
|
]
|
|
for node_name in required_nodes:
|
|
_expect(node_name in sxcp_nodes.NODE_CLASS_MAPPINGS, f"{node_name} missing from node registry")
|
|
_expect(node_name in sxcp_nodes.NODE_DISPLAY_NAME_MAPPINGS, f"{node_name} missing from display registry")
|
|
|
|
profile_node = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGenerationProfile"]
|
|
profile_inputs = profile_node.INPUT_TYPES().get("required") or {}
|
|
_expect("profile" in profile_inputs, "Generation Profile lost profile input")
|
|
_expect("tooltip" in profile_inputs["profile"][1], "Generation Profile tooltip injection missing")
|
|
profile_config, profile_summary = profile_node().build(
|
|
profile="balanced",
|
|
clothing_override="profile_default",
|
|
poses_override="profile_default",
|
|
expression_enabled=True,
|
|
expression_intensity_mode="fixed",
|
|
expression_intensity=0.5,
|
|
backside_bias=-1,
|
|
minimal_clothing_ratio=-1,
|
|
standard_pose_ratio=-1,
|
|
trigger_policy="profile_default",
|
|
)
|
|
parsed_profile = json.loads(profile_config)
|
|
_expect(parsed_profile.get("profile") == "balanced", "Generation Profile output lost profile")
|
|
_expect(parsed_profile.get("expression_intensity") == 0.5, "Generation Profile output lost fixed expression intensity")
|
|
_expect("balanced:" in profile_summary, "Generation Profile summary changed unexpectedly")
|
|
|
|
filter_config = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPAdvancedFilters"]().build(
|
|
include_european=True,
|
|
include_mediterranean_mena=False,
|
|
include_latina=False,
|
|
include_east_asian=False,
|
|
include_southeast_asian=False,
|
|
include_south_asian=False,
|
|
include_black_african=True,
|
|
include_indigenous=False,
|
|
include_mixed=False,
|
|
include_plus_size=False,
|
|
figure="curvy",
|
|
)[0]
|
|
parsed_filter = json.loads(filter_config)
|
|
_expect(parsed_filter.get("figure") == "curvy", "Advanced Filters lost figure")
|
|
_expect(parsed_filter.get("ethnicity_includes") == ["european", "black_african"], "Advanced Filters ethnicity includes changed")
|
|
_expect(parsed_filter.get("no_plus_women") is True, "Advanced Filters should set no_plus_women when plus size is excluded")
|
|
|
|
ethnicity, ethnicity_filter_config, ethnicity_summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPEthnicityList"]().build(
|
|
include_european=False,
|
|
include_mediterranean_mena=False,
|
|
include_latina=False,
|
|
include_east_asian=False,
|
|
include_southeast_asian=False,
|
|
include_south_asian=False,
|
|
include_black_african=False,
|
|
include_indigenous=False,
|
|
include_mixed=False,
|
|
include_asian=False,
|
|
include_white_asian=False,
|
|
include_western_european=False,
|
|
include_french_european=True,
|
|
include_germanic_european=False,
|
|
include_nordic_european=False,
|
|
include_celtic_european=False,
|
|
include_slavic_european=False,
|
|
include_baltic_european=False,
|
|
include_alpine_european=False,
|
|
include_balkan_european=False,
|
|
include_greek_mediterranean=False,
|
|
include_italian_mediterranean=False,
|
|
include_iberian_mediterranean=False,
|
|
strict_excludes=True,
|
|
)
|
|
parsed_ethnicity_filter = json.loads(ethnicity_filter_config)
|
|
_expect("french_european" in ethnicity, "Ethnicity List output lost selected regional ethnicity")
|
|
_expect(parsed_ethnicity_filter.get("ethnicity_includes") == ["french_european"], "Ethnicity List filter output changed")
|
|
_expect("ethnicity list:" in ethnicity_summary, "Ethnicity List summary changed unexpectedly")
|
|
|
|
|
|
SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|
("builtin_single_woman", smoke_builtin_single),
|
|
("builtin_couple_metadata", smoke_builtin_couple_metadata),
|
|
("camera_scene_single", smoke_camera_scene_single),
|
|
("scene_camera_adapter_pov_profile_policy", smoke_scene_camera_adapter_pov_profile_policy),
|
|
("row_camera_policy", smoke_row_camera_policy),
|
|
("config_route_location_theme", smoke_config_route_location_theme),
|
|
("builder_prompt_route_policy", smoke_builder_prompt_route_policy),
|
|
("builder_config_route_policy", smoke_builder_config_route_policy),
|
|
("krea_normal_row_routes", smoke_krea_normal_row_routes),
|
|
("krea_action_details_policy", smoke_krea_action_details_policy),
|
|
("outercourse_action_policy", smoke_outercourse_action_policy),
|
|
("item_axis_policy", smoke_item_axis_policy),
|
|
("krea_row_fields_policy", smoke_krea_row_fields_policy),
|
|
("location_config_policy", smoke_location_config_policy),
|
|
("row_location_policy", smoke_row_location_policy),
|
|
("row_expression_policy", smoke_row_expression_policy),
|
|
("row_prompt_axes_policy", smoke_row_prompt_axes_policy),
|
|
("row_item_policy", smoke_row_item_policy),
|
|
("row_category_route_policy", smoke_row_category_route_policy),
|
|
("row_generation_policy", smoke_row_generation_policy),
|
|
("category_extensions_policy", smoke_category_extensions_policy),
|
|
("category_cast_config_policy", smoke_category_cast_config_policy),
|
|
("row_subject_route_policy", smoke_row_subject_route_policy),
|
|
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
|
("filter_config_policy", smoke_filter_config_policy),
|
|
("character_config_policy", smoke_character_config_policy),
|
|
("character_profile_policy", smoke_character_profile_policy),
|
|
("row_normalization_policy", smoke_row_normalization_policy),
|
|
("prompt_hygiene_policy", smoke_prompt_hygiene_policy),
|
|
("row_rendering_policy", smoke_row_rendering_policy),
|
|
("row_role_graph_policy", smoke_row_role_graph_policy),
|
|
("row_assembly_policy", smoke_row_assembly_policy),
|
|
("formatter_input_policy", smoke_formatter_input_policy),
|
|
("formatter_target_policy", smoke_formatter_target_policy),
|
|
("formatter_detail_policy", smoke_formatter_detail_policy),
|
|
("krea_format_route_policy", smoke_krea_format_route_policy),
|
|
("formatter_cast_policy", smoke_formatter_cast_policy),
|
|
("caption_policy", smoke_caption_policy),
|
|
("caption_format_route_policy", smoke_caption_format_route_policy),
|
|
("caption_text_policy", smoke_caption_text_policy),
|
|
("caption_metadata_routes", smoke_caption_metadata_routes),
|
|
("sdxl_presets_policy", smoke_sdxl_presets_policy),
|
|
("sdxl_format_route_policy", smoke_sdxl_format_route_policy),
|
|
("sdxl_tag_policy", smoke_sdxl_tag_policy),
|
|
("sdxl_tag_routes", smoke_sdxl_tag_routes),
|
|
("hardcore_position_config_policy", smoke_hardcore_position_config_policy),
|
|
("row_route_metadata_policy", smoke_row_route_metadata_policy),
|
|
("category_library_route", smoke_category_library_route),
|
|
("category_subcategory_matrix", smoke_category_subcategory_matrix),
|
|
("hardcore_category_routes", smoke_hardcore_category_routes),
|
|
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
|
|
("pair_options_policy", smoke_pair_options_policy),
|
|
("pair_route_policy", smoke_pair_route_policy),
|
|
("pair_builder_policy", smoke_pair_builder_policy),
|
|
("insta_pair_same_cast", smoke_insta_pair),
|
|
("krea_pair_clothing_state", smoke_krea_pair_clothing_state),
|
|
("krea_anal_axis_compatibility", smoke_krea_anal_axis_compatibility),
|
|
("insta_pair_pov_man", smoke_insta_pair_pov),
|
|
("insta_pair_camera_split", smoke_insta_pair_camera_split),
|
|
("pov_camera_scene", smoke_pov_camera_scene),
|
|
("krea_pov_penetration_route", smoke_krea_pov_penetration_route),
|
|
("pov_outercourse_position_routes", smoke_pov_outercourse_position_routes),
|
|
("pov_oral_position_routes", smoke_pov_oral_position_routes),
|
|
("pov_penetration_position_routes", smoke_pov_penetration_position_routes),
|
|
("pov_anal_position_routes", smoke_pov_anal_position_routes),
|
|
("double_front_back_route", smoke_double_front_back_route),
|
|
("climax_position_routes", smoke_climax_position_routes),
|
|
("interaction_role_graph_routes", smoke_interaction_role_graph_routes),
|
|
("fallback_role_graph_routes", smoke_fallback_role_graph_routes),
|
|
("expression_disabled", smoke_no_expression_fallback),
|
|
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
|
("node_runtime_contracts", smoke_node_runtime_contracts),
|
|
("node_utility_registration", smoke_node_utility_registration),
|
|
("server_route_payload_policy", smoke_server_route_payload_policy),
|
|
("seed_config_policy", smoke_seed_config_policy),
|
|
("prompt_route_simulation_policy", smoke_prompt_route_simulation_policy),
|
|
("node_camera_registration", smoke_node_camera_registration),
|
|
("node_route_config_registration", smoke_node_route_config_registration),
|
|
("node_character_registration", smoke_node_character_registration),
|
|
("node_hardcore_position_registration", smoke_node_hardcore_position_registration),
|
|
("node_formatter_registration", smoke_node_formatter_registration),
|
|
("node_insta_registration", smoke_node_insta_registration),
|
|
("node_builder_registration", smoke_node_builder_registration),
|
|
("node_profile_filter_registration", smoke_node_profile_filter_registration),
|
|
]
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--case",
|
|
choices=[name for name, _func in SMOKE_CASES],
|
|
action="append",
|
|
help="Run only the named smoke case. Can be passed multiple times.",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
selected = set(args.case or [])
|
|
report = SmokeReport()
|
|
for name, func in SMOKE_CASES:
|
|
if selected and name not in selected:
|
|
continue
|
|
try:
|
|
func()
|
|
except Exception as exc: # noqa: BLE001 - report all smoke failures uniformly.
|
|
report.fail(name, str(exc))
|
|
else:
|
|
report.ok(name)
|
|
print(f"\nSummary: {len(report.passed)} passed, {len(report.failed)} failed")
|
|
if report.failed:
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|