Add prompt route simulation checks
This commit is contained in:
@@ -980,6 +980,32 @@ pair metadata through the core Python APIs, then verifies:
|
|||||||
Python formatter APIs for metadata-driven pair inputs, so ComfyUI wrappers
|
Python formatter APIs for metadata-driven pair inputs, so ComfyUI wrappers
|
||||||
cannot silently drift from route behavior.
|
cannot silently drift from route behavior.
|
||||||
|
|
||||||
|
## Route Simulation Helper
|
||||||
|
|
||||||
|
For prompt-quality edits, run the simulation helper before and after changing
|
||||||
|
wording or route selection:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/prompt_route_simulation.py --fail-on-issues
|
||||||
|
```
|
||||||
|
|
||||||
|
The script builds representative single-row and Insta/OF pair routes, formats
|
||||||
|
them through Krea2, SDXL, and training-caption paths, and reports structured
|
||||||
|
issues for:
|
||||||
|
|
||||||
|
- formatter routes falling back away from metadata;
|
||||||
|
- raw builder labels leaking into Krea output;
|
||||||
|
- duplicate negative-prompt comma items;
|
||||||
|
- softcore prompt noise;
|
||||||
|
- POV routes emitting third-person camera text or losing first-person wording;
|
||||||
|
- selected hardcore position filters appearing in `position_keys` but not as
|
||||||
|
the primary `position_key`;
|
||||||
|
- pose-axis rerolls changing cast/scene metadata or failing to move pose/action
|
||||||
|
metadata.
|
||||||
|
|
||||||
|
Use `--json --include-prompts` when you need the exact raw and formatted text
|
||||||
|
for debugging a route.
|
||||||
|
|
||||||
## Editing Cheatsheet
|
## Editing Cheatsheet
|
||||||
|
|
||||||
| Symptom | First file/function to inspect |
|
| Symptom | First file/function to inspect |
|
||||||
@@ -1097,8 +1123,9 @@ Before changing prompt behavior:
|
|||||||
3. Decide whether the bug is selection, raw wording, camera adaptation, or
|
3. Decide whether the bug is selection, raw wording, camera adaptation, or
|
||||||
formatter rewrite.
|
formatter rewrite.
|
||||||
4. Edit the smallest owning pool/template/function.
|
4. Edit the smallest owning pool/template/function.
|
||||||
5. Re-run a small simulation with fixed person/scene/category seeds and only the
|
5. Re-run `python tools/prompt_route_simulation.py --fail-on-issues` or a
|
||||||
target axis varied.
|
similarly small simulation with fixed person/scene/category seeds and only
|
||||||
|
the target axis varied.
|
||||||
|
|
||||||
The repo may have unrelated dirty files during interactive prompt work. Always
|
The repo may have unrelated dirty files during interactive prompt work. Always
|
||||||
stage only the intended files for commits.
|
stage only the intended files for commits.
|
||||||
|
|||||||
+22
-1
@@ -50,6 +50,27 @@ def empty_action_position_route() -> dict[str, Any]:
|
|||||||
return empty_action_position_route_result().as_dict()
|
return empty_action_position_route_result().as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
def _primary_position_key(
|
||||||
|
position_keys: list[str],
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
hardcore_position_config: dict[str, Any] | None,
|
||||||
|
) -> str:
|
||||||
|
if not position_keys:
|
||||||
|
return ""
|
||||||
|
configured = []
|
||||||
|
if isinstance(hardcore_position_config, dict):
|
||||||
|
configured = hardcore_position_policy.normalize_hardcore_position_values(
|
||||||
|
hardcore_position_config.get("positions")
|
||||||
|
)
|
||||||
|
for key in configured:
|
||||||
|
if key in position_keys:
|
||||||
|
return key
|
||||||
|
for key in template_policy.template_position_keys(metadata):
|
||||||
|
if key in position_keys:
|
||||||
|
return key
|
||||||
|
return position_keys[0]
|
||||||
|
|
||||||
|
|
||||||
def resolve_action_position_route_result(
|
def resolve_action_position_route_result(
|
||||||
*,
|
*,
|
||||||
is_pose_category: bool,
|
is_pose_category: bool,
|
||||||
@@ -97,7 +118,7 @@ def resolve_action_position_route_result(
|
|||||||
return ActionPositionRoute(
|
return ActionPositionRoute(
|
||||||
position_family=position_family,
|
position_family=position_family,
|
||||||
position_keys=position_keys,
|
position_keys=position_keys,
|
||||||
position_key=position_keys[0] if position_keys else "",
|
position_key=_primary_position_key(position_keys, metadata, hardcore_position_config),
|
||||||
action_family=action_family,
|
action_family=action_family,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run representative prompt-route simulations and report quality issues.
|
||||||
|
|
||||||
|
This is a diagnostic tool, not a golden snapshot test. It builds a small set of
|
||||||
|
metadata rows/pairs, sends them through the Krea2, SDXL, and caption routes, and
|
||||||
|
reports route/noise/seed-control problems in a JSON-friendly structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
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 krea_formatter # noqa: E402
|
||||||
|
import prompt_builder as pb # noqa: E402
|
||||||
|
import sdxl_formatter # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
TRIGGER = "sxcppnl7"
|
||||||
|
SDXL_TRIGGER = "mythp0rt"
|
||||||
|
|
||||||
|
SOFTCORE_NOISE_TERMS = (
|
||||||
|
"the image focuses",
|
||||||
|
"softcore version",
|
||||||
|
"non-explicit teaser setup",
|
||||||
|
"no sex act",
|
||||||
|
"genital contact",
|
||||||
|
"keep the softcore version",
|
||||||
|
"focused on woman a alone",
|
||||||
|
)
|
||||||
|
|
||||||
|
FORMATTER_LABEL_LEAKS = (
|
||||||
|
"role graph:",
|
||||||
|
"sexual scene:",
|
||||||
|
"cast descriptors:",
|
||||||
|
"shared cast descriptors:",
|
||||||
|
)
|
||||||
|
|
||||||
|
HARDCORE_NOISE_TERMS = (
|
||||||
|
"softcore visual reference",
|
||||||
|
"the same visibly adult",
|
||||||
|
"the scene contains",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _json(value: Any) -> str:
|
||||||
|
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_key(value: Any) -> str:
|
||||||
|
return re.sub(r"[^a-z0-9]+", " ", str(value or "").lower()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
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 busty",
|
||||||
|
hair_color="blonde",
|
||||||
|
hair_length="long",
|
||||||
|
hair_style="loose_waves",
|
||||||
|
descriptor_detail="full",
|
||||||
|
expression_intensity=0.55,
|
||||||
|
softcore_expression_intensity=0.35,
|
||||||
|
hardcore_expression_intensity=0.75,
|
||||||
|
)["character_cast"]
|
||||||
|
return pb.build_character_slot_json(
|
||||||
|
subject_type="man",
|
||||||
|
label="A",
|
||||||
|
age="40-year-old adult",
|
||||||
|
ethnicity="western_european",
|
||||||
|
body="average",
|
||||||
|
descriptor_detail="compact",
|
||||||
|
expression_intensity=0.45,
|
||||||
|
softcore_expression_intensity=0.25,
|
||||||
|
hardcore_expression_intensity=0.65,
|
||||||
|
presence_mode="pov" if pov_man else "visible",
|
||||||
|
character_cast=cast,
|
||||||
|
)["character_cast"]
|
||||||
|
|
||||||
|
|
||||||
|
def _coworking_location_config() -> str:
|
||||||
|
return pb.build_location_pool_json(
|
||||||
|
enabled=True,
|
||||||
|
combine_mode="replace",
|
||||||
|
preset="custom_only",
|
||||||
|
custom_locations=(
|
||||||
|
"coworking_sim: coworking lounge with tall windows, warm desks, "
|
||||||
|
"laptop tables, glass partition seams, repeated desk rows, plants, "
|
||||||
|
"and soft shared-office depth"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _orbit_camera(horizontal_angle: int = 45, vertical_angle: int = 0, zoom: float = 6.0) -> 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="action",
|
||||||
|
lens="auto",
|
||||||
|
orientation="auto",
|
||||||
|
phone_visibility="auto",
|
||||||
|
priority="soft_hint",
|
||||||
|
camera_detail="compact",
|
||||||
|
include_degrees=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _position_filter(focus: str, family: str, positions: list[str] | tuple[str, ...] | str) -> str:
|
||||||
|
position_config = pb.build_hardcore_position_pool_json(
|
||||||
|
combine_mode="replace",
|
||||||
|
family=family,
|
||||||
|
selected_positions=positions,
|
||||||
|
)
|
||||||
|
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=position_config,
|
||||||
|
focus=focus,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _insta_options() -> str:
|
||||||
|
return 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",
|
||||||
|
softcore_expression_enabled=True,
|
||||||
|
hardcore_expression_enabled=True,
|
||||||
|
softcore_expression_intensity=0.35,
|
||||||
|
hardcore_expression_intensity=0.75,
|
||||||
|
platform_style="hybrid",
|
||||||
|
continuity="same_creator_same_room",
|
||||||
|
hardcore_clothing_continuity="explicit_nude",
|
||||||
|
softcore_camera_mode="from_camera_config",
|
||||||
|
hardcore_camera_mode="from_camera_config",
|
||||||
|
camera_detail="compact",
|
||||||
|
hardcore_detail_density="balanced",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_metadata(metadata: dict[str, Any], target: str) -> dict[str, Any]:
|
||||||
|
metadata_json = _json(metadata)
|
||||||
|
krea = krea_formatter.format_krea2_prompt(
|
||||||
|
"",
|
||||||
|
metadata_json=metadata_json,
|
||||||
|
input_hint="metadata_json",
|
||||||
|
target=target,
|
||||||
|
detail_level="balanced",
|
||||||
|
style_mode="preserve",
|
||||||
|
)
|
||||||
|
sdxl = sdxl_formatter.format_sdxl_prompt(
|
||||||
|
"",
|
||||||
|
metadata_json=metadata_json,
|
||||||
|
input_hint="metadata_json",
|
||||||
|
target=target,
|
||||||
|
formatter_profile="manual_controls",
|
||||||
|
style_preset="flat_vector_pony",
|
||||||
|
quality_preset="pony_high",
|
||||||
|
trigger=SDXL_TRIGGER,
|
||||||
|
prepend_trigger=True,
|
||||||
|
preserve_trigger=False,
|
||||||
|
nude_weight=1.29,
|
||||||
|
)
|
||||||
|
caption, caption_method, caption_trace = caption_naturalizer.naturalize_caption_with_trace(
|
||||||
|
"",
|
||||||
|
metadata_json=metadata_json,
|
||||||
|
input_hint="metadata_json",
|
||||||
|
target=target,
|
||||||
|
trigger=TRIGGER,
|
||||||
|
include_trigger=True,
|
||||||
|
detail_level="balanced",
|
||||||
|
style_policy="drop_style_tail",
|
||||||
|
caption_profile="training_dense",
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"krea": krea,
|
||||||
|
"sdxl": sdxl,
|
||||||
|
"caption": {
|
||||||
|
"natural_caption": caption,
|
||||||
|
"method": caption_method,
|
||||||
|
"route_trace_json": caption_trace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _duplicate_comma_items(value: Any) -> list[str]:
|
||||||
|
items = [_clean_key(part) for part in str(value or "").split(",")]
|
||||||
|
items = [part for part in items if part]
|
||||||
|
return sorted({part for part in items if items.count(part) > 1})
|
||||||
|
|
||||||
|
|
||||||
|
def _text_issues(label: str, value: Any, *, min_len: int = 8) -> list[str]:
|
||||||
|
text = str(value or "")
|
||||||
|
issues: list[str] = []
|
||||||
|
if len(text.strip()) < min_len:
|
||||||
|
issues.append(f"{label}: empty_or_short")
|
||||||
|
if "None" in text:
|
||||||
|
issues.append(f"{label}: leaked_None")
|
||||||
|
if " " in text:
|
||||||
|
issues.append(f"{label}: repeated_spaces")
|
||||||
|
if " ," in text or " ." in text:
|
||||||
|
issues.append(f"{label}: bad_punctuation_spacing")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def _formatter_issues(name: str, formats: dict[str, Any], *, is_pov: bool = False) -> list[str]:
|
||||||
|
issues: list[str] = []
|
||||||
|
krea = formats["krea"]
|
||||||
|
sdxl = formats["sdxl"]
|
||||||
|
caption = formats["caption"]
|
||||||
|
|
||||||
|
krea_prompt = str(krea.get("krea_prompt") or "")
|
||||||
|
sdxl_prompt = str(sdxl.get("sdxl_prompt") or "")
|
||||||
|
caption_text = str(caption.get("natural_caption") or "")
|
||||||
|
for label, value in (
|
||||||
|
(f"{name}.krea_prompt", krea_prompt),
|
||||||
|
(f"{name}.sdxl_prompt", sdxl_prompt),
|
||||||
|
(f"{name}.caption", caption_text),
|
||||||
|
):
|
||||||
|
issues.extend(_text_issues(label, value, min_len=20))
|
||||||
|
|
||||||
|
for formatter_name, method in (
|
||||||
|
("krea", krea.get("method")),
|
||||||
|
("sdxl", sdxl.get("method")),
|
||||||
|
("caption", caption.get("method")),
|
||||||
|
):
|
||||||
|
if "metadata" not in str(method or ""):
|
||||||
|
issues.append(f"{name}.{formatter_name}: not_metadata_route:{method}")
|
||||||
|
|
||||||
|
for label, value in (
|
||||||
|
(f"{name}.krea_negative", krea.get("negative_prompt")),
|
||||||
|
(f"{name}.sdxl_negative", sdxl.get("negative_prompt")),
|
||||||
|
):
|
||||||
|
duplicates = _duplicate_comma_items(value)
|
||||||
|
if duplicates:
|
||||||
|
issues.append(f"{label}: duplicate_comma_items:{duplicates[:5]}")
|
||||||
|
|
||||||
|
lower_krea = krea_prompt.lower()
|
||||||
|
for leak in FORMATTER_LABEL_LEAKS:
|
||||||
|
if leak in lower_krea:
|
||||||
|
issues.append(f"{name}.krea_prompt: leaked_label:{leak}")
|
||||||
|
for noise in HARDCORE_NOISE_TERMS:
|
||||||
|
if noise in lower_krea:
|
||||||
|
issues.append(f"{name}.krea_prompt: hardcore_noise:{noise}")
|
||||||
|
if is_pov:
|
||||||
|
if "viewer" not in lower_krea or "first-person" not in lower_krea:
|
||||||
|
issues.append(f"{name}.krea_prompt: pov_wording_missing")
|
||||||
|
if "camera:" in krea_prompt:
|
||||||
|
issues.append(f"{name}.krea_prompt: pov_emitted_third_person_camera")
|
||||||
|
return issues
|
||||||
|
|
||||||
|
|
||||||
|
def _softcore_issues(name: str, text: Any) -> list[str]:
|
||||||
|
lower = str(text or "").lower()
|
||||||
|
return [f"{name}: softcore_noise:{term}" for term in SOFTCORE_NOISE_TERMS if term in lower]
|
||||||
|
|
||||||
|
|
||||||
|
def _row_summary(row: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"category": row.get("main_category"),
|
||||||
|
"subcategory": row.get("subcategory"),
|
||||||
|
"scene": row.get("scene"),
|
||||||
|
"scene_profile": row.get("scene_camera_profile_key"),
|
||||||
|
"action_family": row.get("action_family"),
|
||||||
|
"position_family": row.get("position_family"),
|
||||||
|
"position_key": row.get("position_key"),
|
||||||
|
"position_keys": row.get("position_keys") or [],
|
||||||
|
"pov_labels": row.get("pov_character_labels") or [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _route_metadata_issues(name: str, row: dict[str, Any]) -> list[str]:
|
||||||
|
config = row.get("hardcore_position_config") if isinstance(row.get("hardcore_position_config"), dict) else {}
|
||||||
|
configured = [str(value) for value in (config.get("positions") or [])]
|
||||||
|
if not configured:
|
||||||
|
return []
|
||||||
|
available = set(str(value) for value in (row.get("position_keys") or []))
|
||||||
|
selected_available = [value for value in configured if value in available]
|
||||||
|
if selected_available and row.get("position_key") not in selected_available:
|
||||||
|
return [
|
||||||
|
f"{name}: selected_position_not_primary:{row.get('position_key')} not in {selected_available}"
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _case_report(
|
||||||
|
name: str,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
*,
|
||||||
|
target: str,
|
||||||
|
include_prompts: bool,
|
||||||
|
is_pov: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
formats = _format_metadata(metadata, target)
|
||||||
|
issues = _formatter_issues(name, formats, is_pov=is_pov)
|
||||||
|
issues.extend(_route_metadata_issues(name, metadata))
|
||||||
|
if target == "softcore":
|
||||||
|
issues.extend(_softcore_issues(f"{name}.krea_prompt", formats["krea"].get("krea_prompt")))
|
||||||
|
report = {
|
||||||
|
"name": name,
|
||||||
|
"target": target,
|
||||||
|
"summary": _row_summary(metadata),
|
||||||
|
"methods": {
|
||||||
|
"krea": formats["krea"].get("method"),
|
||||||
|
"sdxl": formats["sdxl"].get("method"),
|
||||||
|
"caption": formats["caption"].get("method"),
|
||||||
|
},
|
||||||
|
"issues": issues,
|
||||||
|
}
|
||||||
|
if include_prompts:
|
||||||
|
report["prompts"] = {
|
||||||
|
"raw": metadata.get("prompt", ""),
|
||||||
|
"krea": formats["krea"].get("krea_prompt", ""),
|
||||||
|
"sdxl": formats["sdxl"].get("sdxl_prompt", ""),
|
||||||
|
"caption": formats["caption"].get("natural_caption", ""),
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def _pair_reports(name: str, pair: dict[str, Any], *, include_prompts: bool) -> list[dict[str, Any]]:
|
||||||
|
soft_row = dict(pair.get("softcore_row") or {})
|
||||||
|
hard_row = dict(pair.get("hardcore_row") or {})
|
||||||
|
soft_formats = _format_metadata(pair, "softcore")
|
||||||
|
hard_formats = _format_metadata(pair, "hardcore")
|
||||||
|
soft_issues = _formatter_issues(f"{name}.softcore", soft_formats)
|
||||||
|
soft_issues.extend(_route_metadata_issues(f"{name}.softcore", soft_row))
|
||||||
|
soft_issues.extend(_softcore_issues(f"{name}.softcore.krea_prompt", soft_formats["krea"].get("krea_prompt")))
|
||||||
|
hard_is_pov = bool(hard_row.get("pov_character_labels"))
|
||||||
|
hard_issues = _formatter_issues(f"{name}.hardcore", hard_formats, is_pov=hard_is_pov)
|
||||||
|
hard_issues.extend(_route_metadata_issues(f"{name}.hardcore", hard_row))
|
||||||
|
reports = [
|
||||||
|
{
|
||||||
|
"name": f"{name}.softcore",
|
||||||
|
"target": "softcore",
|
||||||
|
"summary": _row_summary(soft_row),
|
||||||
|
"methods": {
|
||||||
|
"krea": soft_formats["krea"].get("method"),
|
||||||
|
"sdxl": soft_formats["sdxl"].get("method"),
|
||||||
|
"caption": soft_formats["caption"].get("method"),
|
||||||
|
},
|
||||||
|
"issues": soft_issues,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": f"{name}.hardcore",
|
||||||
|
"target": "hardcore",
|
||||||
|
"summary": _row_summary(hard_row),
|
||||||
|
"methods": {
|
||||||
|
"krea": hard_formats["krea"].get("method"),
|
||||||
|
"sdxl": hard_formats["sdxl"].get("method"),
|
||||||
|
"caption": hard_formats["caption"].get("method"),
|
||||||
|
},
|
||||||
|
"issues": hard_issues,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if include_prompts:
|
||||||
|
reports[0]["prompts"] = {
|
||||||
|
"raw": pair.get("softcore_prompt", ""),
|
||||||
|
"krea": soft_formats["krea"].get("krea_softcore_prompt", "") or soft_formats["krea"].get("krea_prompt", ""),
|
||||||
|
"sdxl": soft_formats["sdxl"].get("sdxl_softcore_prompt", "") or soft_formats["sdxl"].get("sdxl_prompt", ""),
|
||||||
|
"caption": soft_formats["caption"].get("natural_caption", ""),
|
||||||
|
}
|
||||||
|
reports[1]["prompts"] = {
|
||||||
|
"raw": pair.get("hardcore_prompt", ""),
|
||||||
|
"krea": hard_formats["krea"].get("krea_hardcore_prompt", "") or hard_formats["krea"].get("krea_prompt", ""),
|
||||||
|
"sdxl": hard_formats["sdxl"].get("sdxl_hardcore_prompt", "") or hard_formats["sdxl"].get("sdxl_prompt", ""),
|
||||||
|
"caption": hard_formats["caption"].get("natural_caption", ""),
|
||||||
|
}
|
||||||
|
return reports
|
||||||
|
|
||||||
|
|
||||||
|
def _regular_single_case(seed: int) -> dict[str, Any]:
|
||||||
|
return pb.build_prompt_from_configs(
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=seed,
|
||||||
|
category_config=pb.build_category_config_json("Casual clothes", "Casual clothes / Streetwear"),
|
||||||
|
cast_config=pb.build_cast_config_json("solo_woman", 1, 0),
|
||||||
|
seed_config=pb.build_seed_lock_config_json(base_seed=seed),
|
||||||
|
camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
||||||
|
character_cast=_character_cast(),
|
||||||
|
location_config=_coworking_location_config(),
|
||||||
|
extra_positive="simulation marker",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _insta_pair_case(seed: int, *, pov: bool, position: str, focus: str, family: str) -> dict[str, Any]:
|
||||||
|
return pb.build_insta_of_pair(
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=seed,
|
||||||
|
ethnicity="any",
|
||||||
|
figure="random",
|
||||||
|
no_plus_women=False,
|
||||||
|
no_black=False,
|
||||||
|
trigger=TRIGGER,
|
||||||
|
prepend_trigger_to_prompt=True,
|
||||||
|
seed_config=pb.build_seed_lock_config_json(base_seed=seed),
|
||||||
|
options_json=_insta_options(),
|
||||||
|
character_cast=_character_cast(pov_man=pov),
|
||||||
|
hardcore_position_config=_position_filter(focus, family, [position]),
|
||||||
|
location_config=_coworking_location_config(),
|
||||||
|
camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=6.0),
|
||||||
|
softcore_camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5),
|
||||||
|
hardcore_camera_config=_orbit_camera(horizontal_angle=68 if pov else 135, vertical_angle=20, zoom=7.5),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_axis_check(seed: int) -> dict[str, Any]:
|
||||||
|
base = pb.build_prompt(
|
||||||
|
category="Hardcore sexual poses",
|
||||||
|
subcategory="Penetrative sex",
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=seed,
|
||||||
|
clothing="random",
|
||||||
|
ethnicity="any",
|
||||||
|
poses="random",
|
||||||
|
backside_bias=0.0,
|
||||||
|
figure="random",
|
||||||
|
no_plus_women=False,
|
||||||
|
no_black=False,
|
||||||
|
minimal_clothing_ratio=-1,
|
||||||
|
standard_pose_ratio=-1,
|
||||||
|
trigger=TRIGGER,
|
||||||
|
prepend_trigger_to_prompt=True,
|
||||||
|
extra_positive="",
|
||||||
|
extra_negative="",
|
||||||
|
seed_config=pb.build_seed_lock_config_json(base_seed=seed),
|
||||||
|
women_count=1,
|
||||||
|
men_count=1,
|
||||||
|
character_cast=_character_cast(),
|
||||||
|
hardcore_position_config=_position_filter("penetration_only", "penetration", ["missionary", "doggy", "cowgirl"]),
|
||||||
|
location_config=_coworking_location_config(),
|
||||||
|
)
|
||||||
|
changed = False
|
||||||
|
mismatches: list[str] = []
|
||||||
|
for reroll_seed in range(seed + 1, seed + 10):
|
||||||
|
rerolled = pb.build_prompt(
|
||||||
|
category="Hardcore sexual poses",
|
||||||
|
subcategory="Penetrative sex",
|
||||||
|
row_number=1,
|
||||||
|
start_index=1,
|
||||||
|
seed=seed,
|
||||||
|
clothing="random",
|
||||||
|
ethnicity="any",
|
||||||
|
poses="random",
|
||||||
|
backside_bias=0.0,
|
||||||
|
figure="random",
|
||||||
|
no_plus_women=False,
|
||||||
|
no_black=False,
|
||||||
|
minimal_clothing_ratio=-1,
|
||||||
|
standard_pose_ratio=-1,
|
||||||
|
trigger=TRIGGER,
|
||||||
|
prepend_trigger_to_prompt=True,
|
||||||
|
extra_positive="",
|
||||||
|
extra_negative="",
|
||||||
|
seed_config=pb.build_seed_lock_config_json(base_seed=seed, reroll_axis="pose", reroll_seed=reroll_seed),
|
||||||
|
women_count=1,
|
||||||
|
men_count=1,
|
||||||
|
character_cast=_character_cast(),
|
||||||
|
hardcore_position_config=_position_filter("penetration_only", "penetration", ["missionary", "doggy", "cowgirl"]),
|
||||||
|
location_config=_coworking_location_config(),
|
||||||
|
)
|
||||||
|
if rerolled.get("cast_descriptor_text") != base.get("cast_descriptor_text"):
|
||||||
|
mismatches.append(f"cast changed on pose reroll {reroll_seed}")
|
||||||
|
if rerolled.get("scene_text") != base.get("scene_text"):
|
||||||
|
mismatches.append(f"scene changed on pose reroll {reroll_seed}")
|
||||||
|
if (
|
||||||
|
rerolled.get("position_key") != base.get("position_key")
|
||||||
|
or rerolled.get("source_role_graph") != base.get("source_role_graph")
|
||||||
|
or rerolled.get("item") != base.get("item")
|
||||||
|
):
|
||||||
|
changed = True
|
||||||
|
break
|
||||||
|
issues = list(mismatches)
|
||||||
|
if not changed:
|
||||||
|
issues.append("pose reroll did not change pose/action metadata within 9 attempts")
|
||||||
|
return {
|
||||||
|
"name": "seed_axis.pose_reroll",
|
||||||
|
"base": _row_summary(base),
|
||||||
|
"changed": changed,
|
||||||
|
"issues": issues,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[str, Any]:
|
||||||
|
cases: list[dict[str, Any]] = []
|
||||||
|
regular = _regular_single_case(seed)
|
||||||
|
cases.append(_case_report("regular.single.casual", regular, target="single", include_prompts=include_prompts))
|
||||||
|
penetration_pair = _insta_pair_case(seed + 1, pov=False, position="doggy", focus="penetration_only", family="penetration")
|
||||||
|
cases.extend(_pair_reports("insta_pair.penetration", penetration_pair, include_prompts=include_prompts))
|
||||||
|
pov_pair = _insta_pair_case(seed + 2, pov=True, position="penis_licking", focus="outercourse_only", family="outercourse")
|
||||||
|
cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_pair, include_prompts=include_prompts))
|
||||||
|
axis_checks = [_seed_axis_check(seed + 3)]
|
||||||
|
issues = [
|
||||||
|
{"case": case["name"], "issue": issue}
|
||||||
|
for case in cases
|
||||||
|
for issue in case.get("issues", [])
|
||||||
|
]
|
||||||
|
issues.extend(
|
||||||
|
{"case": check["name"], "issue": issue}
|
||||||
|
for check in axis_checks
|
||||||
|
for issue in check.get("issues", [])
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"summary": {
|
||||||
|
"seed": seed,
|
||||||
|
"cases": len(cases),
|
||||||
|
"axis_checks": len(axis_checks),
|
||||||
|
"issues": len(issues),
|
||||||
|
},
|
||||||
|
"issues": issues,
|
||||||
|
"cases": cases,
|
||||||
|
"axis_checks": axis_checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _print_text_report(report: dict[str, Any]) -> None:
|
||||||
|
summary = report.get("summary") or {}
|
||||||
|
print(
|
||||||
|
f"Prompt route simulation: seed={summary.get('seed')} "
|
||||||
|
f"cases={summary.get('cases')} axis_checks={summary.get('axis_checks')} issues={summary.get('issues')}"
|
||||||
|
)
|
||||||
|
for case in report.get("cases") or []:
|
||||||
|
summary_text = case.get("summary") or {}
|
||||||
|
route = ", ".join(f"{key}={value}" for key, value in summary_text.items() if value not in (None, "", []))
|
||||||
|
print(f"- {case.get('name')} [{case.get('target')}]: {route}")
|
||||||
|
for issue in case.get("issues") or []:
|
||||||
|
print(f" ISSUE {issue}")
|
||||||
|
for check in report.get("axis_checks") or []:
|
||||||
|
print(f"- {check.get('name')}: changed={check.get('changed')}")
|
||||||
|
for issue in check.get("issues") or []:
|
||||||
|
print(f" ISSUE {issue}")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--seed", type=int, default=3901, help="Base seed for deterministic simulations.")
|
||||||
|
parser.add_argument("--json", action="store_true", help="Print the full JSON report.")
|
||||||
|
parser.add_argument("--include-prompts", action="store_true", help="Include raw and formatted prompt text in the report.")
|
||||||
|
parser.add_argument("--fail-on-issues", action="store_true", help="Exit with code 1 when any issue is reported.")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
report = run_simulation(seed=args.seed, include_prompts=args.include_prompts)
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps(report, ensure_ascii=True, indent=2, sort_keys=True))
|
||||||
|
else:
|
||||||
|
_print_text_report(report)
|
||||||
|
return 1 if args.fail_on_issues and report.get("issues") else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -97,6 +97,7 @@ import sdxl_tag_routes # noqa: E402
|
|||||||
import seed_config # noqa: E402
|
import seed_config # noqa: E402
|
||||||
import krea_pov # noqa: E402
|
import krea_pov # noqa: E402
|
||||||
import subject_context # noqa: E402
|
import subject_context # noqa: E402
|
||||||
|
from tools import prompt_route_simulation # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
Trigger = "sxcppnl7"
|
Trigger = "sxcppnl7"
|
||||||
@@ -6378,6 +6379,7 @@ def smoke_pov_outercourse_position_routes() -> None:
|
|||||||
hard_row = pair.get("hardcore_row") or {}
|
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("action_family") == "outercourse", f"{name} action_family should be outercourse")
|
||||||
_expect(hard_row.get("position_family") == "outercourse", f"{name} position_family should be outercourse")
|
_expect(hard_row.get("position_family") == "outercourse", f"{name} position_family should be outercourse")
|
||||||
|
_expect(hard_row.get("position_key") == position_key, f"{name} selected position should be primary position_key")
|
||||||
_expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
|
_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()
|
role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
|
||||||
for term in role_terms:
|
for term in role_terms:
|
||||||
@@ -7762,6 +7764,25 @@ def smoke_seed_config_policy() -> None:
|
|||||||
_expect(pose_changed, "pose reroll should change pose/action metadata while cast and scene stay locked")
|
_expect(pose_changed, "pose reroll should change pose/action metadata while cast and scene stay locked")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_prompt_route_simulation_policy() -> None:
|
||||||
|
report = prompt_route_simulation.run_simulation(seed=3901, include_prompts=False)
|
||||||
|
summary = report.get("summary") or {}
|
||||||
|
_expect(summary.get("cases") == 5, "Prompt route simulation case count changed unexpectedly")
|
||||||
|
_expect(summary.get("axis_checks") == 1, "Prompt route simulation lost axis check coverage")
|
||||||
|
_expect(summary.get("issues") == 0, f"Prompt route simulation reported issues: {report.get('issues')}")
|
||||||
|
cases = {case.get("name"): case for case in report.get("cases") or []}
|
||||||
|
pov_hard = cases.get("insta_pair.pov_outercourse.hardcore") or {}
|
||||||
|
pov_summary = pov_hard.get("summary") or {}
|
||||||
|
_expect(
|
||||||
|
pov_summary.get("position_key") == "penis_licking",
|
||||||
|
"Prompt route simulation should catch selected outercourse position as primary position_key",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
"penis_licking" in (pov_summary.get("position_keys") or []),
|
||||||
|
"Prompt route simulation lost selected outercourse key from position_keys",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def smoke_node_camera_registration() -> None:
|
def smoke_node_camera_registration() -> None:
|
||||||
required_nodes = [
|
required_nodes = [
|
||||||
"SxCPCameraControl",
|
"SxCPCameraControl",
|
||||||
@@ -8612,6 +8633,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|||||||
("node_utility_registration", smoke_node_utility_registration),
|
("node_utility_registration", smoke_node_utility_registration),
|
||||||
("server_route_payload_policy", smoke_server_route_payload_policy),
|
("server_route_payload_policy", smoke_server_route_payload_policy),
|
||||||
("seed_config_policy", smoke_seed_config_policy),
|
("seed_config_policy", smoke_seed_config_policy),
|
||||||
|
("prompt_route_simulation_policy", smoke_prompt_route_simulation_policy),
|
||||||
("node_camera_registration", smoke_node_camera_registration),
|
("node_camera_registration", smoke_node_camera_registration),
|
||||||
("node_route_config_registration", smoke_node_route_config_registration),
|
("node_route_config_registration", smoke_node_route_config_registration),
|
||||||
("node_character_registration", smoke_node_character_registration),
|
("node_character_registration", smoke_node_character_registration),
|
||||||
|
|||||||
Reference in New Issue
Block a user