diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 03a4507..7375417 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -102,6 +102,9 @@ Already isolated: - hardcore configured-cast role graph generation lives in `hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and then asks that module for the source role graph. +- fallback role graph wording lives in `hardcore_role_fallback.py`, covering + solo rows, women-only rows, men-only rows, mixed group fallbacks, and support + partner sentences. - interaction-style role graph wording lives in `hardcore_role_interaction.py`, covering foreplay, manual stimulation, body worship, clothing transitions, dominant guidance, camera performance, aftercare, and group coordination. @@ -323,6 +326,8 @@ Near-term: so source aftermath placement and formatter details cannot drift apart. - Cover generated interaction routes through Krea, SDXL, and natural caption outputs so source contact/guidance/presentation wording stays metadata-driven. +- Cover generated fallback role routes through Krea, SDXL, and natural caption + outputs so solo and same-sex paths do not remain untested edge behavior. Medium-term: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 9c2cfad..dc810f8 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -65,6 +65,7 @@ Core helper ownership: | Python module | What it owns | | --- | --- | | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | +| `hardcore_role_fallback.py` | Solo, same-sex, mixed group fallback, and support-partner role graph wording for configured casts. | | `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. | | `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. | | `hardcore_role_outercourse.py` | Outercourse role graph wording for boobjob, testicle-sucking, penis-licking, handjob, and footjob geometry. | @@ -756,6 +757,8 @@ pair metadata through the core Python APIs, then verifies: - interaction routes keep source role graphs, Krea prose, SDXL tags, and training captions synchronized for manual, clothing transition, body worship, camera-performance, aftercare, and group-coordination rows; +- fallback role routes keep solo, women-only, men-only, and mixed-threesome + source role graphs synchronized with Krea, SDXL, and training-caption outputs; - expression-disabled rows do not fall back to generated expression text. - static formatter metadata fixtures keep source-provided action families stable across Krea2 prose, SDXL tags, and natural captions even when raw item diff --git a/hardcore_role_fallback.py b/hardcore_role_fallback.py new file mode 100644 index 0000000..edd2140 --- /dev/null +++ b/hardcore_role_fallback.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +import random +from typing import Any + +try: + from .hardcore_role_interaction import build_group_coordination_role_graph, build_manual_role_graph +except ImportError: # Allows local smoke tests with `python -c`. + from hardcore_role_interaction import build_group_coordination_role_graph, build_manual_role_graph + + +def build_support_sentence(rng: random.Random, people: list[str], exclude: set[str]) -> str: + extras = [person for person in people if person not in exclude] + if not extras: + return "" + extra = rng.choice(extras) + actions = [ + "kisses and grips the nearest body", + "holds hips open for the camera", + "touches breasts, thighs, and stomach", + "keeps one hand on a partner's ass", + "watches close and joins the body contact", + "presses in from the side with hands on skin", + ] + return f" {extra} {rng.choice(actions)}." + + +def build_solo_role_graph( + solo: str, + women_count: int, + slug: str, + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> str: + if women_count == 1: + if "manual_stimulation" in slug: + return build_manual_role_graph(solo, item_text=item_text, item_axis_values=item_axis_values) + if "camera_performance" in slug: + return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose." + if "cumshot" in slug or "climax" in slug: + return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets." + return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness." + if "cumshot" in slug or "climax" in slug: + return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible." + return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing." + + +def build_women_only_role_graph( + slug: str, + a: str, + b: str, + c: str = "", + fallback_helper: str = "", + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> tuple[str, set[str]]: + used = {a, b} + if "manual_stimulation" in slug: + return build_manual_role_graph(a, b, item_text, item_axis_values), used + if "group_coordination" in slug and c: + used.add(c) + return build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values), used + if "outercourse" in slug: + return f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact.", used + if "oral" in slug: + return f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy.", used + if "anal" in slug or "double" in slug: + return f"{a} uses a strap-on on {b} while keeping her hips held open.", used + if "threesome" in slug or "group" in slug or "orgy" in slug: + helper = c or fallback_helper or b + used.add(helper) + return f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies.", used + if "cumshot" in slug or "climax" in slug: + return f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets.", used + return f"{a} uses a strap-on on {b} while their bodies stay pressed together.", used + + +def build_men_only_role_graph( + slug: str, + a: str, + b: str, + c: str = "", + fallback_helper: str = "", + item_text: str = "", + item_axis_values: dict[str, Any] | None = None, +) -> tuple[str, set[str]]: + used = {a, b} + if "manual_stimulation" in slug: + return f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup.", used + if "group_coordination" in slug and c: + used.add(c) + return build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values), used + if any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): + return f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside.", used + if "outercourse" in slug: + return f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet.", used + if "oral" in slug: + return f"{a} kneels and takes {b}'s penis in his mouth while holding his hips.", used + if "anal" in slug or "double" in slug or "penetrative" in slug: + return f"{a} penetrates {b} anally while {b}'s hips are held open.", used + if "threesome" in slug or "group" in slug or "orgy" in slug: + helper = c or fallback_helper or b + used.add(helper) + return f"{a} penetrates {b} anally while {helper} gives oral contact from the front.", used + if "cumshot" in slug or "climax" in slug: + return f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis.", used + return f"{a} and {b} keep explicit penis and anal contact visible.", used + + +def build_mixed_group_fallback_role_graph( + woman: str, + man: str, + third: str, + helper: str, + slug: str, +) -> str: + if "threesome" in slug: + return f"{man} thrusts his penis into {woman} while {third or helper} uses mouth and hands on the exposed body." + if "group" in slug or "orgy" in slug: + return f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs." + return "" diff --git a/hardcore_role_graphs.py b/hardcore_role_graphs.py index 3c2614b..ef00c0c 100644 --- a/hardcore_role_graphs.py +++ b/hardcore_role_graphs.py @@ -6,6 +6,13 @@ from typing import Any try: from .hardcore_role_anal import build_anal_or_double_role_graph from .hardcore_role_climax import build_climax_role_graph + from .hardcore_role_fallback import ( + build_men_only_role_graph, + build_mixed_group_fallback_role_graph, + build_solo_role_graph, + build_support_sentence, + build_women_only_role_graph, + ) from .hardcore_role_interaction import ( build_foreplay_role_graph, build_group_coordination_role_graph, @@ -18,6 +25,13 @@ try: except ImportError: # Allows local smoke tests with `python -c`. from hardcore_role_anal import build_anal_or_double_role_graph from hardcore_role_climax import build_climax_role_graph + from hardcore_role_fallback import ( + build_men_only_role_graph, + build_mixed_group_fallback_role_graph, + build_solo_role_graph, + build_support_sentence, + build_women_only_role_graph, + ) from hardcore_role_interaction import ( build_foreplay_role_graph, build_group_coordination_role_graph, @@ -88,94 +102,44 @@ def build_hardcore_role_graph( pool = [person for person in men if person not in exclude] or [person for person in people if person not in exclude] or people return rng.choice(pool) - def support_sentence(exclude: set[str]) -> str: - extras = [person for person in people if person not in exclude] - if not extras: - return "" - extra = rng.choice(extras) - actions = [ - "kisses and grips the nearest body", - "holds hips open for the camera", - "touches breasts, thighs, and stomach", - "keeps one hand on a partner's ass", - "watches close and joins the body contact", - "presses in from the side with hands on skin", - ] - return f" {extra} {rng.choice(actions)}." - if people_count == 1: - solo = people[0] - if women_count == 1: - if "manual_stimulation" in slug: - return build_manual_role_graph(solo, item_text=item_text, item_axis_values=item_axis_values) - if "camera_performance" in slug: - return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose." - if "cumshot" in slug or "climax" in slug: - return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets." - return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness." - if "cumshot" in slug or "climax" in slug: - return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible." - return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing." + return build_solo_role_graph(people[0], women_count, slug, item_text, item_axis_values) if women_count > 0 and men_count == 0: a, b = _pick_distinct(rng, women, 2) c = any_woman({a, b}) if len(women) >= 3 else "" used = {a, b} - if "manual_stimulation" in slug: - graph = build_manual_role_graph(a, b, item_text, item_axis_values) - elif "group_coordination" in slug and c: - graph = build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values) - used.add(c) - elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): + if any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): graph = build_interaction_role_graph(a, b, c, slug, item_text, item_axis_values) if c and "camera_performance" in slug: used.add(c) elif "foreplay" in slug: graph = build_foreplay_role_graph(a, b, item_text, item_axis_values) - elif "outercourse" in slug: - graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact." - elif "oral" in slug: - graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy." - elif "anal" in slug or "double" in slug: - graph = f"{a} uses a strap-on on {b} while keeping her hips held open." - elif "threesome" in slug or "group" in slug or "orgy" in slug: - helper = c or any_woman({a}) - graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies." - used.add(helper) - elif "cumshot" in slug or "climax" in slug: - graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets." else: - graph = f"{a} uses a strap-on on {b} while their bodies stay pressed together." - return graph + support_sentence(used) + graph, used = build_women_only_role_graph( + slug, + a, + b, + c, + c or any_woman({a}), + item_text, + item_axis_values, + ) + return graph + build_support_sentence(rng, people, used) if men_count > 0 and women_count == 0: a, b = _pick_distinct(rng, men, 2) c = any_man({a, b}) if len(men) >= 3 else "" - used = {a, b} - if "manual_stimulation" in slug: - graph = f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup." - elif "group_coordination" in slug and c: - graph = build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values) - used.add(c) - elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): - graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside." - elif "foreplay" in slug: - graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside." - elif "outercourse" in slug: - graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet." - elif "oral" in slug: - graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips." - elif "anal" in slug or "double" in slug or "penetrative" in slug: - graph = f"{a} penetrates {b} anally while {b}'s hips are held open." - elif "threesome" in slug or "group" in slug or "orgy" in slug: - helper = c or any_man({a}) - graph = f"{a} penetrates {b} anally while {helper} gives oral contact from the front." - used.add(helper) - elif "cumshot" in slug or "climax" in slug: - graph = f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis." - else: - graph = f"{a} and {b} keep explicit penis and anal contact visible." - return graph + support_sentence(used) + graph, used = build_men_only_role_graph( + slug, + a, + b, + c, + c or any_man({a}), + item_text, + item_axis_values, + ) + return graph + build_support_sentence(rng, people, used) woman = any_woman() man = any_man() @@ -201,12 +165,10 @@ def build_hardcore_role_graph( graph = build_oral_role_graph(woman, man, item_text, item_axis_values, pov_labels) elif "anal" in slug or "double" in slug: graph = build_anal_or_double_role_graph(woman, man, third, people_count, item_text, item_axis_values) - elif "threesome" in slug: - graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body." - elif "group" in slug or "orgy" in slug: - graph = f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs." + elif "threesome" in slug or "group" in slug or "orgy" in slug: + graph = build_mixed_group_fallback_role_graph(woman, man, third, any_person({woman, man}), slug) elif "cumshot" in slug or "climax" in slug: graph = build_climax_role_graph(woman, man, third, item_text, item_axis_values) else: graph = build_penetration_role_graph(woman, man, item_text, item_axis_values) - return graph + support_sentence({woman, man, third} if third else {woman, man}) + return graph + build_support_sentence(rng, people, {woman, man, third} if third else {woman, man}) diff --git a/krea_action_climax.py b/krea_action_climax.py index d4cb52f..4680d01 100644 --- a/krea_action_climax.py +++ b/krea_action_climax.py @@ -126,6 +126,9 @@ def dedupe_climax_detail(detail: str, role_graph: str, density: str = "balanced" detail = _clean(detail) lower = role_graph.lower() patterns: list[str] = [] + if "solo visible ejaculation" in lower or "one hand on his penis" in lower: + detail = re.sub(r"\bcum on lower back and ass\b", "visible semen on skin", detail, flags=re.IGNORECASE) + detail = re.sub(r"\bcum (?:on|dripping from) ass\b", "visible semen on skin", detail, flags=re.IGNORECASE) if "lies on her back" in lower: patterns.extend((r"lying on the back with legs spread and hips lifted", r"reclining with thighs open", r"lying on the back with legs spread")) detail = re.sub(r"\bcum on lower back and ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 90ad946..6ace139 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -180,6 +180,29 @@ def _character_cast_two_men(*, pov_first_man: bool = False) -> str: )["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, @@ -1325,6 +1348,128 @@ def smoke_interaction_role_graph_routes() -> None: _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", @@ -1463,6 +1608,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("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), ]