3646 lines
170 KiB
Python
3646 lines
170 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_policy # noqa: E402
|
|
import category_template_metadata # noqa: E402
|
|
import character_config # noqa: E402
|
|
import character_profile # noqa: E402
|
|
import category_cast_config # noqa: E402
|
|
import category_library # noqa: E402
|
|
import filter_config # noqa: E402
|
|
import formatter_input # noqa: E402
|
|
import hardcore_position_config # noqa: E402
|
|
import __init__ as sxcp_nodes # noqa: E402
|
|
import generation_profile_config # noqa: E402
|
|
import index_switch_policy # noqa: E402
|
|
import krea_cast # noqa: E402
|
|
import krea_formatter # noqa: E402
|
|
import location_config # noqa: E402
|
|
import loop_nodes # noqa: E402
|
|
import prompt_builder as pb # noqa: E402
|
|
import pov_policy # noqa: E402
|
|
import row_normalization # noqa: E402
|
|
import route_metadata # noqa: E402
|
|
import row_camera # noqa: E402
|
|
import row_location # noqa: E402
|
|
import server_routes # noqa: E402
|
|
import sdxl_formatter # noqa: E402
|
|
import sdxl_presets # noqa: E402
|
|
import seed_config # noqa: E402
|
|
import krea_pov # 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 _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,
|
|
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 _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 _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 _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]:
|
|
location_config, composition_config, _summary = pb.build_thematic_location_json(
|
|
enabled=True,
|
|
combine_mode="replace",
|
|
theme="classical_library",
|
|
)
|
|
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,
|
|
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="",
|
|
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_trigger_once("builtin_single_woman.prompt", row.get("prompt"), Trigger)
|
|
_expect_formatter_outputs(row, "builtin_single_woman", target="single")
|
|
|
|
|
|
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")
|
|
|
|
|
|
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",
|
|
)
|
|
|
|
|
|
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)
|
|
_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("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")
|
|
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("315-degree front-left quarter view" in prompt, "Krea config route lost camera directive")
|
|
_expect_formatter_outputs(row, "config_route_location_theme", target="single")
|
|
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
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",
|
|
)
|
|
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")
|
|
_expect(json.loads(themed_location).get("scene_entries"), "Themed location did not output locations")
|
|
_expect(json.loads(themed_composition).get("composition_entries"), "Themed location did not output compositions")
|
|
|
|
|
|
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(
|
|
"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_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_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("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")
|
|
|
|
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")
|
|
|
|
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_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("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")
|
|
|
|
|
|
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"))
|
|
|
|
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_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded soft.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded soft caption.",
|
|
"negative_prompt": "bad anatomy, bad anatomy",
|
|
},
|
|
"hardcore_row": {
|
|
"prompt": f"{Trigger}, {Trigger}, embedded hard.",
|
|
"caption": f"{Trigger}, {Trigger}, embedded hard caption.",
|
|
"negative_prompt": "low quality, bad anatomy, low quality",
|
|
},
|
|
},
|
|
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_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"))
|
|
|
|
|
|
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)
|
|
|
|
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")
|
|
_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)
|
|
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_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.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.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_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_style_preset("bad") == sdxl_presets.DEFAULT_STYLE_PRESET, "SDXL invalid style fallback changed")
|
|
_expect(sdxl_presets.normalize_quality_preset("bad") == sdxl_presets.DEFAULT_QUALITY_PRESET, "SDXL invalid quality fallback changed")
|
|
_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_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")
|
|
|
|
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")
|
|
position_filtered = hardcore_position_config.apply_hardcore_position_config_to_subcategory(
|
|
{
|
|
"slug": "oral_sex",
|
|
"item_templates": [
|
|
{"template": "oral contact in {position}"},
|
|
{"template": "oral sex without a position axis"},
|
|
{"template": "unsupported static template"},
|
|
],
|
|
"item_axes": {
|
|
"position": ["standing oral position", "kneeling oral position"],
|
|
"oral_act": ["blowjob", "cunnilingus"],
|
|
},
|
|
},
|
|
base,
|
|
)
|
|
_expect(
|
|
position_filtered["item_templates"] == [{"template": "oral contact in {position}"}],
|
|
"Hardcore position policy did not filter templates by selected position requirements",
|
|
)
|
|
_expect(
|
|
position_filtered["item_axes"]["position"] == ["standing oral position"],
|
|
"Hardcore position policy did not filter position axes by selected keys",
|
|
)
|
|
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")
|
|
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")
|
|
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",
|
|
)
|
|
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")
|
|
_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_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")
|
|
|
|
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")
|
|
|
|
|
|
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", {"foreplay", "outercourse"}, "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",
|
|
)
|
|
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 _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")
|
|
_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")
|
|
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:
|
|
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, "krea_pair_clothing_state")
|
|
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")
|
|
|
|
|
|
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("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")
|
|
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("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 tightly around", "glans sits just below"),
|
|
("press her breasts tightly around", "glans just below", "penis shaft"),
|
|
),
|
|
(
|
|
"pov_outercourse_testicle",
|
|
"testicle_sucking",
|
|
("mouth and tongue on the pov viewer's balls", "penis points upward"),
|
|
("mouth and tongue licking", "balls", "penis points upward"),
|
|
),
|
|
(
|
|
"pov_outercourse_penis_licking",
|
|
"penis_licking",
|
|
("head low under the pov viewer's penis", "tongue running along"),
|
|
("tongue runs along", "penis shaft", "glans"),
|
|
),
|
|
(
|
|
"pov_outercourse_handjob",
|
|
"handjob",
|
|
("one hand wrapped around the pov viewer's penis", "strokes toward the glans"),
|
|
("one hand wraps around", "penis shaft", "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(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_pov_oral_position_routes() -> None:
|
|
cases = [
|
|
(
|
|
"pov_oral_kneeling",
|
|
"kneeling",
|
|
("viewer's penis", "takes the viewer's penis in her mouth"),
|
|
("takes the viewer's penis in her mouth", "viewer stands over her"),
|
|
),
|
|
(
|
|
"pov_oral_face_sitting",
|
|
"face_sitting",
|
|
("straddling the viewer's face", "pussy directly over the viewer's mouth"),
|
|
("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"),
|
|
("head-to-hips", "viewer's mouth on the woman's pussy"),
|
|
),
|
|
(
|
|
"pov_oral_edge_supported",
|
|
"edge_supported",
|
|
("raised edge with thighs open", "viewer kneels between her legs"),
|
|
("raised edge with thighs open", "viewer kneels between her legs"),
|
|
),
|
|
(
|
|
"pov_oral_side_lying",
|
|
"side_lying",
|
|
("woman a lies on her side", "viewer lies beside her hips"),
|
|
("woman lies on her side", "viewer lies beside her hips"),
|
|
),
|
|
(
|
|
"pov_oral_chair",
|
|
"chair_oral",
|
|
("viewer sits in a chair", "kneels between his thighs"),
|
|
("viewer sits in a chair", "kneels between the viewer's thighs"),
|
|
),
|
|
]
|
|
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")
|
|
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)
|
|
_expect(row.get("action_family") == "foreplay", f"{name} action_family should stay formatter foreplay")
|
|
_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="foreplay",
|
|
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")
|
|
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
seed, seed_config, summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"]().build(12345)
|
|
parsed_seed = json.loads(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 = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPSeedLocker"]().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")
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
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")
|
|
|
|
|
|
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")
|
|
|
|
|
|
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_inputs = woman_slot_node.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")
|
|
|
|
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")
|
|
|
|
caption, caption_method = 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_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("tooltip" in caption_inputs["caption_profile"][1], "Caption profile tooltip injection missing")
|
|
|
|
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")
|
|
|
|
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_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("tooltip" in sdxl_inputs["formatter_profile"][1], "SDXL formatter_profile tooltip injection missing")
|
|
|
|
|
|
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")
|
|
_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")
|
|
_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),
|
|
("camera_scene_single", smoke_camera_scene_single),
|
|
("row_camera_policy", smoke_row_camera_policy),
|
|
("config_route_location_theme", smoke_config_route_location_theme),
|
|
("location_config_policy", smoke_location_config_policy),
|
|
("row_location_policy", smoke_row_location_policy),
|
|
("category_cast_config_policy", smoke_category_cast_config_policy),
|
|
("generation_profile_config_policy", smoke_generation_profile_config_policy),
|
|
("filter_config_policy", smoke_filter_config_policy),
|
|
("character_config_policy", smoke_character_config_policy),
|
|
("character_profile_policy", smoke_character_profile_policy),
|
|
("row_normalization_policy", smoke_row_normalization_policy),
|
|
("formatter_input_policy", smoke_formatter_input_policy),
|
|
("formatter_cast_policy", smoke_formatter_cast_policy),
|
|
("caption_policy", smoke_caption_policy),
|
|
("sdxl_presets_policy", smoke_sdxl_presets_policy),
|
|
("hardcore_position_config_policy", smoke_hardcore_position_config_policy),
|
|
("category_library_route", smoke_category_library_route),
|
|
("hardcore_category_routes", smoke_hardcore_category_routes),
|
|
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
|
|
("pair_options_policy", smoke_pair_options_policy),
|
|
("insta_pair_same_cast", smoke_insta_pair),
|
|
("krea_pair_clothing_state", smoke_krea_pair_clothing_state),
|
|
("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_utility_registration", smoke_node_utility_registration),
|
|
("server_route_payload_policy", smoke_server_route_payload_policy),
|
|
("seed_config_policy", smoke_seed_config_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())
|