diff --git a/README.md b/README.md index c7f0806..59babc1 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The node is registered as: - `prompt_builder / SxCP Seed Control` - `prompt_builder / SxCP Camera Control` - `prompt_builder / SxCP Caption Naturalizer` +- `prompt_builder / SxCP Krea2 Formatter` - `prompt_builder / SxCP Insta/OF Options` - `prompt_builder / SxCP Insta/OF Prompt Pair` @@ -65,6 +66,35 @@ It outputs: - `natural_caption` - `method` +`SxCP Krea2 Formatter` rewrites an existing prompt or `metadata_json` into a +Krea2-oriented natural-language paragraph. It is a formatter, not a safety or +content downgrade pass: hardcore items, role graphs, sexual pose wording, and +camera controls are preserved. Negative prompts stay separate. + +Important behavior: + +- Age wording is preserved deliberately. Phrases like `21-year-old adult`, + `late 20s adult woman`, and `all participants 21+ and visibly adult` are kept + because they help avoid unwanted young-looking outputs. +- Trigger words are removed by default because Krea2 prompting generally reads + better as natural language. Enable `preserve_trigger` if you still need a LoRA + trigger in the positive prompt. +- `style_mode`: `preserve` keeps the current generated style text, + `photographic` converts the style tail toward creator-photo language, and + `minimal` omits most style text. +- For Insta/OF paired metadata, the node returns both `krea_softcore_prompt` and + `krea_hardcore_prompt`, with separate softcore and hardcore negatives. + +It outputs: + +- `krea_prompt` +- `negative_prompt` +- `krea_softcore_prompt` +- `krea_hardcore_prompt` +- `softcore_negative_prompt` +- `hardcore_negative_prompt` +- `method` + `SxCP Insta/OF Prompt Pair` is a special paired-output mode. It creates one shared primary creator descriptor, then returns both a softcore prompt and a hardcore prompt from that same descriptor. This is useful when you want the same @@ -180,6 +210,46 @@ provided, the node uses a generic composer that selects subject appearance, scene, pose, expression, composition, and a random item from the selected subcategory. +Reusable location banks can be defined with top-level `scene_pools` in any +`categories/*.json` file. Categories, subcategories, and items can reference +them with `scene_pools`; referenced pools are merged with any local `scenes`. +This keeps location expansion scalable without duplicating the same bedroom, +selfie, mirror, creator, or group-sex locations across every subcategory. + +Set `"inherit_scenes": false` on a subcategory or item when it should use only +its own `scenes` and `scene_pools` instead of also inheriting parent category +locations. This is useful for narrow subcategories such as group scenes, vehicle +sets, outdoor-only sets, or any category where a generic parent room would be a +bad match. + +Example: + +```json +{ + "scene_pools": { + "creator_selfie_rooms": [ + { + "slug": "bedroom_phone_tripod", + "prompt": "private creator bedroom with a phone tripod, rumpled bedding, and warm lamps" + } + ] + }, + "categories": [ + { + "name": "Example", + "subcategories": [ + { + "name": "Selfie set", + "inherit_scenes": false, + "scene_pools": ["creator_selfie_rooms"], + "items": ["simple outfit prompt"] + } + ] + } + ] +} +``` + For large categories, prefer `item_templates` plus `item_axes` instead of writing every final item by hand: diff --git a/__init__.py b/__init__.py index 714a032..b291062 100644 --- a/__init__.py +++ b/__init__.py @@ -21,6 +21,7 @@ try: subcategory_choices, ) from .caption_naturalizer import naturalize_caption + from .krea_formatter import format_krea2_prompt except ImportError: from prompt_builder import ( build_camera_config_json, @@ -40,6 +41,7 @@ except ImportError: subcategory_choices, ) from caption_naturalizer import naturalize_caption + from krea_formatter import format_krea2_prompt class SxCPPromptBuilder: @@ -277,6 +279,75 @@ class SxCPCaptionNaturalizer: ) +class SxCPKrea2Formatter: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "source_text": ("STRING", {"default": "", "multiline": True}), + "input_hint": (["auto", "metadata_json", "prompt"], {"default": "auto"}), + "target": (["auto", "single", "softcore", "hardcore"], {"default": "auto"}), + "detail_level": (["balanced", "concise", "dense"], {"default": "balanced"}), + "style_mode": (["preserve", "photographic", "minimal"], {"default": "preserve"}), + "preserve_trigger": ("BOOLEAN", {"default": False}), + }, + "optional": { + "metadata_json": ("STRING", {"default": "", "multiline": True}), + "negative_prompt": ("STRING", {"default": "", "multiline": True}), + "extra_positive": ("STRING", {"default": "", "multiline": True}), + "extra_negative": ("STRING", {"default": "", "multiline": True}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ( + "krea_prompt", + "negative_prompt", + "krea_softcore_prompt", + "krea_hardcore_prompt", + "softcore_negative_prompt", + "hardcore_negative_prompt", + "method", + ) + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + source_text, + input_hint, + target, + detail_level, + style_mode, + preserve_trigger, + metadata_json="", + negative_prompt="", + extra_positive="", + extra_negative="", + ): + row = format_krea2_prompt( + source_text=source_text or "", + metadata_json=metadata_json or "", + negative_prompt=negative_prompt or "", + input_hint=input_hint, + target=target, + detail_level=detail_level, + style_mode=style_mode, + preserve_trigger=preserve_trigger, + extra_positive=extra_positive or "", + extra_negative=extra_negative or "", + ) + return ( + row["krea_prompt"], + row["negative_prompt"], + row["krea_softcore_prompt"], + row["krea_hardcore_prompt"], + row["softcore_negative_prompt"], + row["hardcore_negative_prompt"], + row["method"], + ) + + class SxCPInstaOFOptions: @classmethod def INPUT_TYPES(cls): @@ -417,6 +488,7 @@ NODE_CLASS_MAPPINGS = { "SxCPSeedControl": SxCPSeedControl, "SxCPCameraControl": SxCPCameraControl, "SxCPCaptionNaturalizer": SxCPCaptionNaturalizer, + "SxCPKrea2Formatter": SxCPKrea2Formatter, "SxCPInstaOFOptions": SxCPInstaOFOptions, "SxCPInstaOFPromptPair": SxCPInstaOFPromptPair, } @@ -426,6 +498,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "SxCPSeedControl": "SxCP Seed Control", "SxCPCameraControl": "SxCP Camera Control", "SxCPCaptionNaturalizer": "SxCP Caption Naturalizer", + "SxCPKrea2Formatter": "SxCP Krea2 Formatter", "SxCPInstaOFOptions": "SxCP Insta/OF Options", "SxCPInstaOFPromptPair": "SxCP Insta/OF Prompt Pair", } diff --git a/categories/default_categories.json b/categories/default_categories.json index be2b816..b8c236a 100644 --- a/categories/default_categories.json +++ b/categories/default_categories.json @@ -14,13 +14,136 @@ "name": "Streetwear", "slug": "streetwear", "weight": 1.0, - "items": [ - "oversized hoodie with slim jeans and clean sneakers", - "cropped bomber jacket with cargo pants and chunky trainers", - "graphic tee under an open flannel with fitted denim", - "sleek track jacket with joggers and minimal sneakers", - "denim-on-denim outfit with a fitted white tee" + "scene_pools": ["casual_urban_scenes"], + "items": ["streetwear outfit generator"], + "item_templates": [ + "{outerwear} layered over {top}, with {bottom}, {footwear}, and {accessory}", + "{top} with {bottom}, {outerwear}, {footwear}, and {detail}", + "{set_piece} styled with {footwear}, {accessory}, and {color_mood}", + "{outerwear} with {bottom}, {bag}, {footwear}, and {texture_detail}", + "{top} tucked into {bottom}, paired with {footwear}, {jewelry}, and {detail}", + "{outerwear} half-zipped over {top}, with {bottom}, {bag}, and {color_mood}", + "{set_piece} with {outerwear}, {footwear}, {jewelry}, and {texture_detail}", + "{top} under {outerwear}, with {bottom}, {accessory}, {bag}, and {footwear}" ], + "item_axes": { + "accessory": [ + "a ribbed beanie", + "thin sunglasses", + "a baseball cap", + "wire-frame glasses", + "a silk neck scarf", + "fingerless gloves", + "a chunky belt", + "a low-profile headphones detail" + ], + "bag": [ + "a small crossbody bag", + "a canvas tote", + "a compact shoulder bag", + "a mini backpack", + "a waist pack worn crossbody", + "a structured leather satchel", + "a nylon sling bag", + "a glossy phone pouch" + ], + "bottom": [ + "relaxed cargo pants", + "straight-leg jeans", + "wide-leg denim", + "pleated utility trousers", + "fitted black jeans", + "soft joggers", + "a denim mini skirt with tights", + "tailored track pants", + "high-waist cropped trousers", + "washed grey jeans" + ], + "color_mood": [ + "muted city neutrals", + "black and cream contrast", + "soft grey-blue tones", + "washed denim colors", + "olive and charcoal accents", + "clean monochrome styling", + "warm brick-red accents", + "navy, white, and graphite layers" + ], + "detail": [ + "rolled cuffs", + "visible layered hems", + "a sharp cropped silhouette", + "clean oversized proportions", + "a relaxed off-duty fit", + "asymmetric tucked fabric", + "subtle graphic print details", + "a neat high-waist line" + ], + "footwear": [ + "clean white sneakers", + "chunky trainers", + "platform sneakers", + "minimal leather sneakers", + "black ankle boots", + "retro running shoes", + "canvas high-tops", + "sleek street boots" + ], + "jewelry": [ + "small hoop earrings", + "layered chain necklaces", + "a simple watch", + "stacked rings", + "a thin choker", + "minimal silver earrings", + "a pendant necklace", + "a narrow bracelet" + ], + "outerwear": [ + "oversized hoodie", + "cropped bomber jacket", + "open flannel shirt", + "sleek track jacket", + "denim jacket", + "boxy leather jacket", + "lightweight varsity jacket", + "oversized zip hoodie", + "short trench jacket", + "quilted street jacket" + ], + "set_piece": [ + "denim-on-denim outfit", + "matching track set", + "monochrome hoodie-and-jogger set", + "oversized shirt-and-cargo outfit", + "cropped jacket and wide denim outfit", + "clean athleisure set", + "utility vest and trouser outfit", + "layered tee and open-shirt outfit" + ], + "texture_detail": [ + "washed cotton texture", + "soft fleece texture", + "matte nylon fabric", + "structured denim seams", + "subtle ribbed knit trim", + "smooth leather accents", + "canvas strap details", + "creased everyday fabric" + ], + "top": [ + "fitted white tee", + "cropped tank top", + "graphic tee", + "ribbed long-sleeve top", + "simple black tee", + "slim turtleneck", + "soft henley shirt", + "cropped sweatshirt", + "mesh-layered tee", + "clean sleeveless top" + ] + }, "scenes": [ { "slug": "city_crosswalk", @@ -34,20 +157,166 @@ "poses": [ "standing with one hand in a pocket", "leaning against a wall with relaxed confidence", - "walking forward with a casual runway stride" + "walking forward with a casual runway stride", + "looking over one shoulder while stepping off a curb", + "sitting on a low concrete wall with one knee raised", + "adjusting sunglasses with a relaxed half-smile", + "hands in jacket pockets with a slight hip shift", + "turning mid-step as fabric layers move naturally" ] }, { "name": "Summer casual", "slug": "summer_casual", "weight": 1.0, - "items": [ - "light cotton sundress with simple sandals", - "linen shorts with a tucked-in sleeveless blouse", - "soft camp-collar shirt with relaxed trousers", - "ribbed tank top with a flowing midi skirt", - "breathable linen two-piece in pale summer colors" + "scene_pools": ["casual_summer_scenes"], + "items": ["summer casual outfit generator"], + "item_templates": [ + "{dress} with {footwear}, {accessory}, and {fabric_detail}", + "{top} with {bottom}, {footwear}, {bag}, and {color_mood}", + "{layer} over {top}, with {bottom}, {accessory}, and {texture_detail}", + "{two_piece} with {footwear}, {jewelry}, and {fabric_detail}", + "{top} tucked into {bottom}, paired with {bag}, {footwear}, and {detail}", + "{dress} under {layer}, with {accessory}, {jewelry}, and {color_mood}", + "{two_piece} styled with {bag}, {footwear}, {texture_detail}, and {detail}", + "{top} with {bottom}, {layer}, {accessory}, and {fabric_detail}" ], + "item_axes": { + "accessory": [ + "wide sunglasses", + "a straw sunhat", + "a silk hair scarf", + "a woven belt", + "thin oval sunglasses", + "a cotton bandana", + "a simple sun visor", + "a lightweight shawl" + ], + "bag": [ + "a woven tote", + "a small raffia bag", + "a canvas beach bag", + "a soft shoulder bag", + "a compact basket purse", + "a linen drawstring bag", + "a minimal crossbody pouch", + "a straw clutch" + ], + "bottom": [ + "linen shorts", + "relaxed cropped trousers", + "a flowing midi skirt", + "high-waist cotton shorts", + "a breezy wrap skirt", + "wide-leg linen pants", + "a soft pleated skirt", + "light denim shorts", + "paperbag-waist trousers", + "a calf-length summer skirt" + ], + "color_mood": [ + "pale summer colors", + "sun-washed white and blue", + "coral and cream accents", + "sage green and ivory tones", + "soft yellow and denim blue", + "warm terracotta details", + "fresh white and citrus accents", + "muted sea-glass colors" + ], + "detail": [ + "rolled hems", + "a loose tucked waist", + "wind-lifted fabric edges", + "open collar styling", + "a relaxed vacation fit", + "thin shoulder straps", + "a clean high-waist line", + "softly draped fabric" + ], + "dress": [ + "light cotton sundress", + "button-front linen dress", + "soft wrap dress", + "ribbed tank dress", + "flowing midi sundress", + "simple slip dress", + "tiered cotton dress", + "halter summer dress" + ], + "fabric_detail": [ + "breathable linen texture", + "soft cotton folds", + "light gauze fabric", + "fine ribbed cotton", + "sunlit woven texture", + "thin summer knit texture", + "crisp poplin folds", + "airy voile fabric" + ], + "footwear": [ + "simple sandals", + "flat leather sandals", + "white canvas sneakers", + "espadrilles", + "strappy sandals", + "woven slides", + "minimal mule sandals", + "low platform sandals" + ], + "jewelry": [ + "small gold hoops", + "a shell necklace", + "a delicate anklet", + "thin stacked bracelets", + "a small pendant necklace", + "pearl stud earrings", + "a beaded bracelet", + "a fine waist chain over fabric" + ], + "layer": [ + "soft camp-collar shirt", + "open linen shirt", + "light cotton overshirt", + "cropped denim jacket", + "sheer beach shirt", + "loose short-sleeve cardigan", + "thin knit wrap", + "unbuttoned chambray shirt" + ], + "texture_detail": [ + "natural linen slubs", + "matte cotton finish", + "subtle woven stripes", + "soft wrinkled fabric", + "delicate eyelet texture", + "smooth summer poplin", + "sunlit thread texture", + "lightweight ribbing" + ], + "top": [ + "tucked-in sleeveless blouse", + "ribbed tank top", + "cropped cotton tee", + "linen camisole", + "halter knit top", + "soft bandeau under a shirt", + "square-neck tank", + "thin-strapped summer top", + "buttoned linen vest", + "loose cotton blouse" + ], + "two_piece": [ + "breathable linen two-piece", + "matching crop top and skirt set", + "soft resort shirt-and-short set", + "ribbed tank and midi skirt set", + "cotton co-ord set", + "linen vest and trouser set", + "open shirt and wrap-skirt set", + "minimal summer knit set" + ] + }, "scenes": [ { "slug": "sunny_market", @@ -61,20 +330,148 @@ "poses": [ "turning slightly with fabric moving in the breeze", "standing in warm sunlight with relaxed shoulders", - "sitting on a low wall with one knee bent" + "sitting on a low wall with one knee bent", + "walking with one hand holding a sunhat", + "leaning on a promenade railing with a soft smile", + "lifting sunglasses while looking toward the light", + "standing barefoot near warm paving stones", + "mid-step with loose fabric trailing behind" ] }, { "name": "Cozy lounge", "slug": "cozy_lounge", "weight": 1.0, - "items": [ - "soft knit cardigan with a fitted tee and lounge trousers", - "oversized sweater with leggings and wool socks", - "relaxed sweatshirt with drawstring joggers", - "ribbed lounge set with a long open cardigan", - "simple cotton tee with loose pajama-style trousers" + "scene_pools": ["casual_lounge_scenes"], + "items": ["cozy lounge outfit generator"], + "item_templates": [ + "{layer} over {top}, with {bottom}, {footwear}, and {texture_detail}", + "{set_piece} with {layer}, {accessory}, and {color_mood}", + "{top} with {bottom}, {footwear}, {prop}, and {detail}", + "{layer} wrapped around {top}, with {bottom}, {jewelry}, and {texture_detail}", + "{set_piece} styled with {footwear}, {prop}, and {detail}", + "{top} tucked loosely into {bottom}, with {layer}, {accessory}, and {color_mood}", + "{layer} slipping off one shoulder over {top}, with {bottom}, {footwear}, and {texture_detail}", + "{set_piece} with {accessory}, {jewelry}, {prop}, and {detail}" ], + "item_axes": { + "accessory": [ + "a soft hair clip", + "thin reading glasses", + "a scrunchie at the wrist", + "a loose blanket scarf", + "a simple headband", + "a warm knit beanie", + "a delicate sleep mask pushed up", + "a cotton bandana" + ], + "bottom": [ + "lounge trousers", + "soft leggings", + "drawstring joggers", + "loose pajama-style trousers", + "ribbed lounge pants", + "wide knit pants", + "cotton sleep shorts", + "thermal leggings", + "slouchy sweatpants", + "soft waffle-knit pants" + ], + "color_mood": [ + "warm oatmeal and grey tones", + "soft rose and cream colors", + "muted sage and ivory", + "heather grey and white", + "warm mocha neutrals", + "powder blue and cream", + "charcoal and blush accents", + "soft lavender and warm beige" + ], + "detail": [ + "relaxed draped proportions", + "rolled sleeves", + "softly rumpled fabric", + "a loose off-duty silhouette", + "visible cozy layering", + "a slightly oversized fit", + "bare ankle styling", + "a gentle high-waist line" + ], + "footwear": [ + "wool socks", + "soft house slippers", + "bare feet", + "fuzzy socks", + "knit ankle socks", + "shearling slippers", + "simple cotton socks", + "ribbed knee socks" + ], + "jewelry": [ + "tiny hoop earrings", + "a simple pendant necklace", + "a thin bracelet", + "small stud earrings", + "a delicate ring", + "a minimal chain necklace", + "a soft fabric wristband", + "a subtle ankle chain" + ], + "layer": [ + "soft knit cardigan", + "long open cardigan", + "oversized sweater", + "relaxed sweatshirt", + "chunky cable-knit cardigan", + "brushed fleece hoodie", + "thin robe cardigan", + "cropped lounge hoodie", + "slouchy wrap sweater", + "soft waffle-knit cardigan" + ], + "prop": [ + "a casual magazine nearby", + "a warm mug in one hand", + "a paperback book nearby", + "a folded blanket at the hip", + "a phone resting on the sofa", + "a small tray on a side table", + "a pillow tucked against the waist", + "a half-open notebook nearby" + ], + "set_piece": [ + "ribbed lounge set", + "matching sweatshirt and jogger set", + "soft knit co-ord set", + "cotton tee and pajama trouser set", + "waffle-knit lounge set", + "brushed fleece lounge set", + "cardigan and knit pant set", + "simple sleepwear-inspired set" + ], + "texture_detail": [ + "soft knit texture", + "brushed fleece texture", + "ribbed cotton texture", + "waffle-knit fabric", + "warm wool fibers", + "smooth jersey folds", + "plush cardigan texture", + "softly worn cotton" + ], + "top": [ + "fitted tee", + "simple cotton tee", + "ribbed tank top", + "soft long-sleeve top", + "thin camisole", + "slouchy henley shirt", + "cropped lounge tee", + "thermal knit top", + "light sleep tank", + "soft V-neck tee" + ] + }, "scenes": [ { "slug": "sunny_apartment", @@ -88,7 +485,12 @@ "poses": [ "curled comfortably on a sofa", "standing barefoot near a window with a relaxed smile", - "sitting cross-legged with a casual magazine nearby" + "sitting cross-legged with a casual magazine nearby", + "leaning into a couch corner with one knee raised", + "stretching lazily with sleeves falling over the hands", + "kneeling on a rug while reaching for a warm mug", + "standing in profile near soft curtains", + "reclining with one arm draped over a pillow" ] } ] diff --git a/categories/erotic_clothes.json b/categories/erotic_clothes.json index 3f0ff87..829a62d 100644 --- a/categories/erotic_clothes.json +++ b/categories/erotic_clothes.json @@ -10,6 +10,7 @@ "style": "explicit adult erotic fashion illustration, sensual pin-up coloured-pencil comic style, adults only", "positive_suffix": "Use crisp clean comic linework, detailed hatching, soft skin shading, tactile fabric texture, warm intimate lighting, and textured paper.", "negative_prompt": "minors, childlike appearance, schoolgirl, childlike costume, non-consensual, coercion, violence, injury, watermark", + "scene_pools": ["softcore_creator_scenes", "mirror_scenes"], "expressions": [ "heavy-lidded seductive gaze", "direct erotic stare", @@ -47,6 +48,7 @@ "name": "Provocative lingerie", "slug": "provocative_lingerie", "weight": 1.0, + "scene_pools": ["boudoir_bedroom_scenes"], "item_templates": [ "{color} {fabric} {bra_style} with {bottom_style}, {garter_detail}, {stocking_style}, and {shoe_style}", "{color} {bodywear} with {neckline}, {hip_cut}, {back_detail}, and {trim_detail}", @@ -306,6 +308,7 @@ "name": "Sheer exposed", "slug": "sheer_exposed", "weight": 1.0, + "scene_pools": ["softcore_creator_scenes", "mirror_scenes"], "item_templates": [ "{color} {sheer_fabric} {sheer_garment} with {exposed_breast_detail}, {hip_detail}, and {shoe_style}", "{color} open sheer robe exposing {exposed_breast_detail}, {lower_exposure}, and {jewelry}", @@ -549,6 +552,7 @@ "name": "Fetish inspired", "slug": "fetish_inspired", "weight": 1.0, + "scene_pools": ["fetish_studio_scenes"], "item_templates": [ "{color} {material} {latex_piece} with {zipper_detail}, {boot_style}, and {glove_style}", "{color} {harness_style} over {breast_exposure} with {bottom_detail}, {hardware}, and {shoe_style}", @@ -808,6 +812,7 @@ "name": "Nude accessories", "slug": "nude_accessories", "weight": 1.0, + "scene_pools": ["boudoir_bedroom_scenes", "mirror_scenes"], "item_templates": [ "fully nude body styled only with {stocking_style}, {shoe_style}, and {jewelry}", "bare breasts and bare hips framed by {accessory_set}, {stocking_style}, and {shoe_style}", @@ -971,6 +976,7 @@ "name": "Microwear and body tape", "slug": "microwear_body_tape", "weight": 1.0, + "scene_pools": ["softcore_creator_scenes", "fetish_studio_scenes"], "item_templates": [ "{color} micro bikini with {top_detail}, {bottom_detail}, {body_tape}, and {shoe_style}", "{color} body tape design covering only {tape_coverage}, with {bottom_detail}, {jewelry}, and {shoe_style}", @@ -1160,6 +1166,7 @@ "name": "Erotic costumes", "slug": "erotic_costumes", "weight": 1.0, + "scene_pools": ["costume_backstage_scenes", "softcore_creator_scenes"], "item_templates": [ "{color} adult {costume_role} look with {top_detail}, {bottom_detail}, {stocking_style}, and {shoe_style}", "{color} fantasy {costume_role} costume with {corset_detail}, {exposure_detail}, {accessory}, and {shoe_style}", diff --git a/categories/location_pools.json b/categories/location_pools.json new file mode 100644 index 0000000..92dfea9 --- /dev/null +++ b/categories/location_pools.json @@ -0,0 +1,181 @@ +{ + "version": 1, + "scene_pools": { + "casual_urban_scenes": [ + {"slug": "city_crosswalk_phone_snap", "prompt": "sunlit city crosswalk with storefront reflections and a casual phone-snapshot angle"}, + {"slug": "subway_tile_selfie_corner", "prompt": "clean subway platform with tiled walls, overhead lights, and a quiet selfie corner"}, + {"slug": "parking_rooftop_streetwear", "prompt": "parking garage rooftop with painted lines, city haze, and late-afternoon light"}, + {"slug": "record_shop_mirror_wall", "prompt": "record shop aisle with warm neon, poster walls, and a small mirror behind the counter"}, + {"slug": "coffee_window_counter", "prompt": "coffee shop window counter with street reflections and soft morning light"}, + {"slug": "brick_alley_fire_escape", "prompt": "brick alley beside a fire escape with clean pavement and reflected city light"}, + {"slug": "boutique_changing_mirror", "prompt": "boutique changing-room mirror with clothing hooks, soft bulbs, and a candid outfit-check angle"}, + {"slug": "streetwear_elevator_lobby", "prompt": "modern apartment elevator lobby with brushed metal doors and a full-length mirror"} + ], + "casual_summer_scenes": [ + {"slug": "seaside_prom_phone_photo", "prompt": "seaside promenade with bright sky, warm paving stones, and casual phone-photo framing"}, + {"slug": "weekend_market_canvas_awning", "prompt": "open-air weekend market with fruit stands, canvas awnings, and sunlit walkway space"}, + {"slug": "rooftop_day_terrace", "prompt": "daytime rooftop terrace with potted plants, white chairs, and clear summer light"}, + {"slug": "beach_cafe_table", "prompt": "beach cafe table with woven chairs, linen shade, and ocean light in the background"}, + {"slug": "poolside_daybed_casual", "prompt": "poolside daybed with white towels, glass railings, and soft resort sunlight"}, + {"slug": "garden_wall_summer", "prompt": "garden wall with climbing flowers, pale stone, and warm afternoon shadows"}, + {"slug": "lake_boardwalk_summer", "prompt": "wooden lakeside boardwalk with reeds, clean sky, and relaxed vacation light"}, + {"slug": "sunny_balcony_outfit_check", "prompt": "sunny apartment balcony with plants, city rooftops, and a casual outfit-check composition"} + ], + "casual_lounge_scenes": [ + {"slug": "sunny_apartment_phone_tripod", "prompt": "sunny apartment corner with bookshelves, a warm rug, and a phone on a small tripod"}, + {"slug": "window_seat_soft_curtains", "prompt": "comfortable window seat with soft curtains, pillows, and afternoon light"}, + {"slug": "bedroom_floor_lounge", "prompt": "cozy bedroom floor with folded blankets, low shelves, and soft window light"}, + {"slug": "sofa_ring_light_corner", "prompt": "living-room sofa corner with a ring light stand, plants, and warm lamps"}, + {"slug": "loft_kitchen_lounge", "prompt": "open loft kitchen with a breakfast counter, wood floor, and relaxed home light"}, + {"slug": "reading_nook_phone_snap", "prompt": "reading nook with stacked books, a small mirror, and candid phone-snapshot framing"}, + {"slug": "hotel_morning_lounge", "prompt": "hotel room lounge chair with rumpled travel bags, curtains, and soft morning sun"}, + {"slug": "studio_apartment_mirror", "prompt": "small studio apartment with a full-length mirror, neutral bedding, and warm lamp light"} + ], + "softcore_creator_scenes": [ + {"slug": "creator_bedroom_ring_light", "prompt": "private creator bedroom with a ring light, phone tripod, rumpled bedding, and warm lamps"}, + {"slug": "onlyfans_mirror_bedroom", "prompt": "bedroom mirror selfie setup with a visible phone, messy sheets, and soft amber light"}, + {"slug": "walk_in_closet_tryon", "prompt": "mirrored walk-in closet with open drawers, hanging outfits, and warm boutique bulbs"}, + {"slug": "hotel_bed_phone_tripod", "prompt": "hotel bed content setup with a phone on a mini tripod, city lights, and satin bedding"}, + {"slug": "bathroom_counter_selfie", "prompt": "private bathroom counter with mirror haze, phone reflection, towels, and warm vanity lights"}, + {"slug": "vanity_ring_light_close", "prompt": "vanity corner with makeup lights, perfume bottles, silk fabric, and close creator-shot framing"}, + {"slug": "apartment_floor_content", "prompt": "apartment floor content setup with a low mirror, pillows, and soft practical lamps"}, + {"slug": "balcony_phone_selfie", "prompt": "private balcony with city lights, glass railing, and handheld phone-selfie perspective"}, + {"slug": "car_interior_creator_selfie", "prompt": "parked car interior with seat reflections, dashboard glow, and close phone-selfie framing"}, + {"slug": "shower_steam_phone_reflection", "prompt": "steamy private shower room with wet tile, glass reflections, and implied phone-shot framing"}, + {"slug": "studio_bedroom_backdrop", "prompt": "small creator studio with a bed, seamless backdrop, ring light, and visible phone stand"}, + {"slug": "couch_lamp_creator_clip", "prompt": "private couch corner with scattered clothes, warm lamp glow, and vertical creator-video framing"} + ], + "mirror_scenes": [ + {"slug": "large_bedroom_mirror_selfie", "prompt": "large bedroom mirror with the phone visible, bed behind the subject, and warm side lamps"}, + {"slug": "antique_mirror_boudoir", "prompt": "antique mirror corner with dark wood, perfume bottles, and golden lamplight"}, + {"slug": "bathroom_mirror_haze", "prompt": "bathroom mirror selfie setup with steam haze, marble tile, and warm vanity bulbs"}, + {"slug": "closet_full_length_mirror", "prompt": "full-length closet mirror with outfit racks, shoe shelves, and soft boutique lighting"}, + {"slug": "hotel_mirror_city_view", "prompt": "hotel suite mirror reflecting city lights, satin bedding, and a phone in hand"}, + {"slug": "neon_mirror_wall", "prompt": "neon mirror wall with glossy floor reflections and saturated magenta-blue edge light"}, + {"slug": "gold_vanity_mirror", "prompt": "gold-framed vanity mirror with makeup lights, silk fabric, and close reflected framing"}, + {"slug": "black_lacquer_mirror_room", "prompt": "black lacquer mirror room with glossy furniture, velvet curtains, and sharp rim light"} + ], + "hardcore_mirror_scenes": [ + {"slug": "hardcore_bedroom_mirror_pair", "prompt": "large bedroom mirror placed beside a rumpled bed, reflecting explicit adult body contact and the phone-camera angle", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_hotel_mirror_pair", "prompt": "hotel suite mirror facing a messy bed, city lights behind it, and enough space for explicit adult positioning", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_floor_mirror_set", "prompt": "floor mattress beside a full-length mirror with reflected bodies, warm lamps, and close creator-shot framing", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_shower_mirror_pair", "prompt": "private shower room with a fogged mirror wall, wet tile, and reflected explicit adult contact", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_vanity_mirror_floor", "prompt": "vanity mirror floor setup with bulbs, scattered clothing, low phone-camera angle, and reflected adult bodies", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_lacquer_mirror_bed", "prompt": "black lacquer mirror bedroom with glossy reflections, a low bed, and warm rim light around explicit adult contact", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_tripod_mirror_bed", "prompt": "creator bedroom mirror setup with a phone on tripod, bed sheets, reflected bodies, and clear adult-only framing", "min_people": 2, "max_people": 3}, + {"slug": "hardcore_threesome_mirror_suite", "prompt": "wide mirror-wall suite arranged for three adults, with a bed, floor cushions, and all bodies visible in direct and reflected views", "min_people": 3, "max_people": 3} + ], + "boudoir_bedroom_scenes": [ + {"slug": "warm_boudoir_canopy_bed", "prompt": "warm boudoir bedroom with satin sheets, canopy curtains, low lamplight, and bedside phone framing"}, + {"slug": "silk_bed_close_creator", "prompt": "silk-sheet bed with pillows, a phone on the nightstand, and intimate creator-shot light"}, + {"slug": "velvet_headboard_bedroom", "prompt": "velvet headboard bedroom with gold lamps, rumpled bedding, and close sensual framing"}, + {"slug": "four_poster_lingerie_room", "prompt": "four-poster bed with sheer drapes, warm lamps, and soft floor shadows"}, + {"slug": "hotel_satin_bedroom", "prompt": "luxury hotel bedroom with satin bedding, city glow, and a visible mirror near the bed"}, + {"slug": "rose_lamp_bedroom", "prompt": "intimate bedroom with rose-colored lamps, silk sheets, and soft shadows"} + ], + "fetish_studio_scenes": [ + {"slug": "black_latex_studio_floor", "prompt": "dark private studio with glossy black floor reflections, rim light, and a phone tripod"}, + {"slug": "red_velvet_lacquer_room", "prompt": "red velvet room with black lacquer furniture, low spotlights, and reflective surfaces"}, + {"slug": "industrial_loft_private_set", "prompt": "private industrial loft with brick walls, metal beams, chains, and dramatic light"}, + {"slug": "neon_lacquer_private_room", "prompt": "private neon-lit lacquer room with magenta highlights, mirrors, and glossy floor"}, + {"slug": "harness_wall_studio", "prompt": "private harness-wall studio with leather props, matte black backdrop, and controlled rim light"}, + {"slug": "chrome_fetish_set", "prompt": "chrome studio set with reflective panels, black curtains, and hard-edged erotic lighting"} + ], + "costume_backstage_scenes": [ + {"slug": "costume_dressing_room_phone", "prompt": "private costume dressing room with racks, warm mirror bulbs, velvet curtains, and a phone selfie angle"}, + {"slug": "burlesque_stage_close", "prompt": "small private burlesque stage with red curtains, warm spotlights, and close audience-level framing"}, + {"slug": "cabaret_backstage_vanity", "prompt": "cabaret backstage corner with feather props, vanity mirrors, and dark wood"}, + {"slug": "after_dark_private_office", "prompt": "stylized private office after dark with blinds, desk lamp, and city glow"}, + {"slug": "fantasy_parlor_content_set", "prompt": "dark fantasy parlor with velvet chair, candlelight, antique wallpaper, and creator-shot framing"}, + {"slug": "cosplay_hotel_mirror", "prompt": "hotel mirror cosplay try-on setup with costume pieces on chairs and warm vanity bulbs"} + ], + "hardcore_private_scenes": [ + {"slug": "hardcore_bedroom_phone_tripod", "prompt": "private bedroom sex-content setup with a phone tripod, rumpled sheets, pillows, and warm lamps"}, + {"slug": "hardcore_hotel_bed_city", "prompt": "luxury hotel suite with city lights, messy satin bedding, floor clothes, and low amber light"}, + {"slug": "hardcore_mirror_bedroom", "prompt": "bedroom with a large mirror reflecting the explicit adult scene and a visible phone angle"}, + {"slug": "hardcore_low_mattress_studio", "prompt": "private photo studio with a low mattress, fabric drapes, phone tripod, and controlled warm spotlighting"}, + {"slug": "hardcore_velvet_room", "prompt": "private velvet room with dark curtains, soft cushions, scattered clothes, and warm red light"}, + {"slug": "hardcore_shower_room", "prompt": "large private shower room with steam, wet tile, glass reflections, and warm reflected light"}, + {"slug": "hardcore_lounge_couch", "prompt": "dim private lounge with a wide couch, scattered clothing, side lamps, and soft golden shadows"}, + {"slug": "hardcore_floor_cushion_room", "prompt": "intimate room with floor cushions, silk sheets, low candles, and close vertical camera framing"}, + {"slug": "hardcore_ring_light_bed", "prompt": "creator bedroom with ring light reflection, phone on tripod, bed sheets, and visible content setup"}, + {"slug": "hardcore_bathroom_counter", "prompt": "private bathroom with counter mirror, wet towels, steam, and explicit creator-shot framing"}, + {"slug": "hardcore_walk_in_closet_floor", "prompt": "mirrored walk-in closet floor with clothes scattered, warm bulbs, and a phone reflection"}, + {"slug": "hardcore_car_backseat", "prompt": "parked private car backseat with tinted windows, dashboard glow, and tight phone-camera framing"} + ], + "hardcore_bed_scenes": [ + {"slug": "bed_edge_close_contact", "prompt": "edge of a rumpled bed with pillows pushed aside, sheets pulled tight, and close phone-camera framing"}, + {"slug": "low_bed_mirror_angle", "prompt": "low bed beside a full-length mirror with visible reflected bodies and warm lamps"}, + {"slug": "hotel_bed_overhead", "prompt": "hotel bed arranged for an overhead phone shot with satin sheets and city light"}, + {"slug": "floor_mattress_creator_set", "prompt": "floor mattress content setup with fabric drapes, ring light shadow, and scattered clothes"}, + {"slug": "canopy_bed_explicit_set", "prompt": "canopy bed with sheer curtains, warm bedside light, and intimate full-body framing"}, + {"slug": "velvet_bedroom_wide", "prompt": "velvet bedroom with wide bed, dark curtains, soft cushions, and warm red side light"} + ], + "hardcore_penetrative_scenes": [ + {"slug": "penetration_mirror_bedroom", "prompt": "mirror-facing bedroom setup designed to show explicit body contact and the phone angle"}, + {"slug": "penetration_edge_of_bed", "prompt": "edge-of-bed setup with hips near the camera, rumpled sheets, and warm lamp light"}, + {"slug": "penetration_low_mattress", "prompt": "low mattress studio with side lighting, fabric drapes, and all bodies visible"}, + {"slug": "penetration_couch_lounge", "prompt": "wide private couch with pillows pushed aside, scattered clothes, and golden shadows"}, + {"slug": "penetration_shower_bench", "prompt": "wet shower bench with steam, tile reflections, and close explicit framing"}, + {"slug": "penetration_floor_cushions", "prompt": "floor cushions and silk sheets arranged for full-body explicit contact"} + ], + "hardcore_oral_scenes": [ + {"slug": "oral_bed_kneeling_close", "prompt": "bedside kneeling setup with pillows, warm lamps, and close mouth-level camera framing"}, + {"slug": "oral_mirror_floor", "prompt": "mirror-facing floor setup with bodies reflected and phone held low"}, + {"slug": "oral_couch_front_view", "prompt": "private couch arranged for front-view oral contact with soft side lamps"}, + {"slug": "oral_shower_steam", "prompt": "steamy shower room with wet tile, glass reflections, and close explicit framing"}, + {"slug": "oral_vanity_floor", "prompt": "vanity-room floor with mirror bulbs, scattered lingerie, and low phone-camera angle"}, + {"slug": "oral_hotel_bed_close", "prompt": "hotel bed close-up setup with satin sheets, city light, and tight vertical framing"} + ], + "hardcore_anal_scenes": [ + {"slug": "anal_rear_mirror_bed", "prompt": "bedroom mirror setup emphasizing rear-view body alignment and explicit contact"}, + {"slug": "anal_bent_over_couch", "prompt": "wide private couch arranged for bent-over explicit contact and low camera angle"}, + {"slug": "anal_edge_bed_low_angle", "prompt": "edge-of-bed low-angle setup with sheets pulled aside and hips near the lens"}, + {"slug": "anal_shower_wall", "prompt": "wet shower wall with steam, tile reflections, and hard side lighting"}, + {"slug": "anal_velvet_bench", "prompt": "private velvet bench with dark curtains, red light, and clear full-body framing"}, + {"slug": "anal_floor_mattress_mirror", "prompt": "floor mattress beside a full-length mirror with both direct and reflected views"} + ], + "hardcore_threesome_scenes": [ + {"slug": "threesome_wide_bedroom", "prompt": "wide bedroom setup with a large bed, mirror wall, phone tripod, and all three bodies visible"}, + {"slug": "threesome_hotel_suite", "prompt": "hotel suite bed and couch area arranged for three adults and city-window light"}, + {"slug": "threesome_floor_cushions", "prompt": "floor cushion room with silk sheets, low lamps, and enough space for three bodies"}, + {"slug": "threesome_studio_mattress", "prompt": "private studio mattress with fabric drapes, ring light shadow, and three-person framing"}, + {"slug": "threesome_shower_room", "prompt": "large shower room with wet tile, steam, glass reflections, and three-person spacing"}, + {"slug": "threesome_velvet_lounge", "prompt": "private velvet lounge with wide couch, cushions, and warm red-gold light"} + ], + "hardcore_group_scenes": [ + {"slug": "group_suite_wide_bed", "prompt": "large private suite with a wide bed, couch area, scattered clothes, and all participants visible"}, + {"slug": "group_studio_mattress_room", "prompt": "private studio with multiple low mattresses, fabric drapes, ring lights, and full group visibility"}, + {"slug": "group_velvet_orgy_room", "prompt": "private velvet orgy room with dark curtains, floor cushions, soft lamps, and open floor space"}, + {"slug": "group_lounge_couches", "prompt": "dim private lounge with multiple couches, pillows, warm side lamps, and wide group framing"}, + {"slug": "group_floor_pillow_room", "prompt": "large floor-pillow room with silk sheets, low candles, and overhead camera possibility"}, + {"slug": "group_shower_spa_room", "prompt": "large private shower spa with wet tile, steam, benches, and space for multiple adults"}, + {"slug": "group_rooftop_private_party", "prompt": "private rooftop after-party suite with glass railings, couches, and city lights"}, + {"slug": "group_hotel_party_bedroom", "prompt": "messy hotel party bedroom with a king bed, side couch, bottles, and warm practical lights"}, + {"slug": "group_backstage_private_room", "prompt": "private backstage green room with costume racks, couches, mirror bulbs, and scattered clothes"}, + {"slug": "group_neon_loft_room", "prompt": "private neon loft with mattresses, glossy floor reflections, and wide explicit group composition"}, + {"slug": "group_mirror_wall_suite", "prompt": "large mirror-wall suite with a wide bed, floor cushions, phone tripod, and enough space to reflect a full adult group"}, + {"slug": "group_lacquer_mirror_lounge", "prompt": "wide black-lacquer mirror lounge with multiple couches, glossy reflections, warm rim light, and all participants visible"} + ], + "hardcore_climax_scenes": [ + {"slug": "climax_bed_close_flash", "prompt": "rumpled bed close-up setup with direct phone flash, sheets, and visible fluid detail"}, + {"slug": "climax_mirror_counter", "prompt": "mirror-facing vanity counter setup with phone reflection and explicit aftermath framing"}, + {"slug": "climax_floor_sheets", "prompt": "floor sheets with pillows pushed aside, low lamp light, and close body-detail framing"}, + {"slug": "climax_hotel_bed_flash", "prompt": "hotel bed with phone-flash lighting, messy satin sheets, and city light behind"}, + {"slug": "climax_shower_tile", "prompt": "wet shower tile scene with steam, reflected light, and visible fluid detail"}, + {"slug": "climax_velvet_couch", "prompt": "velvet couch with dark curtains, warm red light, and close explicit aftermath framing"} + ] + }, + "pool_extensions": { + "group_scenes": [ + {"slug": "private_suite_group_party", "prompt": "large private suite with a wide bed, couch area, scattered clothes, and warm party lighting"}, + {"slug": "private_studio_group_mattresses", "prompt": "private studio with multiple low mattresses, fabric drapes, and wide group visibility"}, + {"slug": "velvet_lounge_group_couches", "prompt": "private velvet lounge with multiple couches, pillows, and low red-gold light"}, + {"slug": "rooftop_afterparty_suite", "prompt": "private rooftop after-party suite with glass railings, couches, and city lights"}, + {"slug": "hotel_party_bedroom_group", "prompt": "messy hotel party bedroom with a king bed, side couch, bottles, and warm practical lights"}, + {"slug": "neon_loft_group_room", "prompt": "private neon loft with glossy floor reflections, mattresses, and wide group composition"}, + {"slug": "backstage_green_room_group", "prompt": "private backstage green room with costume racks, mirror bulbs, couches, and scattered clothes"}, + {"slug": "shower_spa_group_room", "prompt": "large private shower spa room with benches, steam, wet tile, and multiple adults"} + ] + } +} diff --git a/categories/sexual_poses.json b/categories/sexual_poses.json index c94673a..928d8ef 100644 --- a/categories/sexual_poses.json +++ b/categories/sexual_poses.json @@ -10,6 +10,7 @@ "style": "explicit consensual adult hardcore sex illustration, anatomically clear erotic comic pin-up style, adults only", "positive_suffix": "Use clear adult anatomy, visible sexual contact, intense body language, crisp comic linework, detailed hatching, warm erotic lighting, and tactile textured paper.", "negative_prompt": "minors, childlike appearance, teen, schoolgirl, incest, bestiality, non-consensual, coercion, rape, violence, injury, blood, gore, watermark", + "scene_pools": ["hardcore_private_scenes"], "prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Sexual pose: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit, hardcore, and anatomically clear, with visible genital contact and adult bodies only. {positive_suffix} Avoid: {negative_prompt}.", "caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, {item}, {scene}, {composition}, explicit consensual adult hardcore sex illustration", "expressions": [ @@ -84,6 +85,7 @@ "slug": "penetrative_sex", "min_people": 2, "weight": 1.0, + "scene_pools": ["hardcore_penetrative_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"], "item_templates": [ "{penetration_act} in {position}, with {body_contact}, {intensity}, and {visibility}", "{position} while {penetration_act}, {hand_detail}, {mouth_detail}, and {visibility}", @@ -248,6 +250,7 @@ "slug": "oral_sex", "min_people": 2, "weight": 1.0, + "scene_pools": ["hardcore_oral_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"], "item_templates": [ "{oral_act} in {position}, with {hand_detail}, {expression_detail}, and {visibility}", "{position} featuring {oral_act}, {body_contact}, {saliva_detail}, and {climax_hint}", @@ -380,6 +383,7 @@ "slug": "anal_double_penetration", "min_people": 2, "weight": 1.0, + "scene_pools": ["hardcore_anal_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"], "item_templates": [ "{anal_act} in {position}, with {leg_detail}, {hand_detail}, and {visibility}", "{double_act} with {body_arrangement}, {intensity}, {mouth_detail}, and {visibility}", @@ -584,6 +588,7 @@ "slug": "threesomes", "min_people": 3, "weight": 1.0, + "scene_pools": ["hardcore_threesome_scenes", "hardcore_group_scenes", "hardcore_mirror_scenes"], "item_templates": [ "{threesome_act} with {body_arrangement}, {oral_detail}, {penetration_detail}, and {visibility}", "{body_arrangement} while {threesome_act}, with {hand_detail}, {mouth_detail}, and {climax_hint}", @@ -760,7 +765,9 @@ "name": "Group sex and orgy", "slug": "group_sex_orgy", "min_people": 4, + "inherit_scenes": false, "weight": 1.0, + "scene_pools": ["hardcore_group_scenes"], "item_templates": [ "{group_act} with {arrangement}, {contact_detail}, {fluid_detail}, and {visibility}", "{arrangement} featuring {group_act}, {oral_detail}, {penetration_detail}, and {intensity}", @@ -928,6 +935,7 @@ "slug": "cumshot_climax", "min_people": 1, "weight": 1.0, + "scene_pools": ["hardcore_climax_scenes", "hardcore_bed_scenes", "hardcore_mirror_scenes"], "item_templates": [ "{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", "{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}", diff --git a/generate_prompt_batches.py b/generate_prompt_batches.py index e425d74..5817853 100755 --- a/generate_prompt_batches.py +++ b/generate_prompt_batches.py @@ -1331,9 +1331,10 @@ def _expand_scenes() -> list[tuple[str, str]]: additions: list[tuple[str, str]] = [] for base_slug, base_description in bases: for mood_slug, mood_description in moods: + separator = ", " if mood_description.startswith("with ") else " " additions.append(( _scene_slug(f"{base_slug}_{mood_slug}"), - f"{base_description} {mood_description}", + f"{base_description}{separator}{mood_description}", )) additions.extend( [ diff --git a/krea_formatter.py b/krea_formatter.py new file mode 100644 index 0000000..d81a3fc --- /dev/null +++ b/krea_formatter.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +import json +import re +from typing import Any + + +TRIGGER_CANDIDATES = ( + "sxcpinup_coloredpencil", + "sxcppnl7", +) + +PROMPT_FIELD_LABELS = ( + "Ages", + "Body types", + "Cast", + "Scene", + "Setting", + "Pose", + "Sexual pose", + "Facial expression", + "Facial expressions", + "Clothing", + "Erotic outfit", + "Prop/detail", + "Composition", + "Role graph", + "Use", + "Avoid", +) + + +def _clean(value: Any) -> str: + text = "" if value is None else str(value) + text = text.replace("\n", " ") + text = re.sub(r"\s+", " ", text).strip() + text = re.sub(r"\s+([,.;:])", r"\1", text) + return text + + +def _sentence(text: str) -> str: + text = _clean(text).strip(" ,;") + if not text: + return "" + text = text[:1].upper() + text[1:] + if text[-1] not in ".!?": + text += "." + return text + + +def _paragraph(parts: list[str]) -> str: + return " ".join(part for part in (_sentence(part) for part in parts) if part) + + +def _maybe_json(text: str) -> dict[str, Any] | None: + text = _clean(text) + if not text.startswith("{"): + return None + try: + value = json.loads(text) + except json.JSONDecodeError: + return None + return value if isinstance(value, dict) else None + + +def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]: + candidates: list[tuple[str, str]] = [] + if input_hint in ("auto", "metadata_json"): + candidates.append((metadata_json, "metadata_json")) + candidates.append((source_text, "source_json")) + for text, method in candidates: + row = _maybe_json(text) + if row is not None: + return row, method + return None, "text" + + +def _strip_trigger(text: str, preserve_trigger: bool) -> str: + text = _clean(text) + if preserve_trigger: + return text + for trigger in TRIGGER_CANDIDATES: + if text.lower().startswith(trigger.lower() + ","): + return text[len(trigger) + 1 :].strip(" ,") + if text.lower().startswith(trigger.lower() + "."): + return text[len(trigger) + 1 :].strip(" ,") + return text + + +def _split_avoid(text: str) -> tuple[str, str]: + match = re.search(r"\bAvoid:\s*(.*)$", text) + if not match: + return text, "" + return text[: match.start()].strip(" ."), match.group(1).strip(" .") + + +def _prompt_field(text: str, label: str) -> str: + text = _clean(text) + if not text: + return "" + labels = "|".join(re.escape(name) for name in PROMPT_FIELD_LABELS) + pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)" + match = re.search(pattern, text) + if not match: + return "" + return _clean(match.group(1)).rstrip(".") + + +def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str: + value = _clean(row.get(key, "")) + if value: + return value + prompt = _clean(row.get("prompt", "")) + for label in labels: + value = _prompt_field(prompt, label) + if value: + return value + return "" + + +def _single_caption_front(row: dict[str, Any]) -> dict[str, str]: + caption = _strip_trigger(_clean(row.get("caption")), False) + if not caption: + return {} + subject = _clean(row.get("primary_subject")) + age = _clean(row.get("age_band") or row.get("age")) + body = _clean(row.get("body_phrase")) + if not body: + body_type = _clean(row.get("body_type") or row.get("body")) + figure = _clean(row.get("figure")) + body = f"{body_type} figure with {figure}" if body_type and figure else f"{body_type} figure".strip() + front = f"{subject}, {age}, {body}, " + if subject in ("woman", "man") and age and body and caption.startswith(front): + try: + skin, hair, eyes, _rest = caption[len(front) :].split(", ", 3) + except ValueError: + return {} + return {"body_phrase": body, "skin": skin, "hair": hair, "eyes": eyes} + return {} + + +def _combine_negative(*parts: str) -> str: + cleaned = [_clean(part).strip(" ,.") for part in parts if _clean(part).strip(" ,.")] + return ", ".join(cleaned) + + +def _clean_age(age: Any) -> str: + return _clean(age) + + +def _age_subject(row: dict[str, Any], fallback_subject: str = "adult person") -> str: + subject = _clean(row.get("subject_phrase") or row.get("primary_subject") or row.get("subject") or fallback_subject) + age = _clean_age(row.get("age_band") or row.get("age")) + if row.get("subject_type") == "configured_cast": + return _clean(row.get("subject_phrase") or subject) + if subject in ("woman", "man"): + if age: + return f"{age} {subject}" if "adult" in age.lower() else f"{age} adult {subject}" + return f"adult {subject}" + if age and "adult" not in subject.lower(): + return f"{age} {subject}" + return subject or fallback_subject + + +def _appearance_phrase(row: dict[str, Any]) -> str: + front = _single_caption_front(row) + parts = [ + _row_value(row, "body_phrase") or front.get("body_phrase"), + _row_value(row, "skin") or front.get("skin"), + _row_value(row, "hair") or front.get("hair"), + _row_value(row, "eyes") or front.get("eyes"), + ] + return ", ".join(_clean(part) for part in parts if _clean(part)) + + +def _camera_phrase(row: dict[str, Any]) -> str: + directive = _clean(row.get("camera_directive")) + if directive: + return directive + config = row.get("camera_config") + if isinstance(config, dict): + mode = _clean(config.get("camera_mode")).replace("_", " ") + shot = _clean(config.get("shot_size")).replace("_", " ") + angle = _clean(config.get("angle")).replace("_", " ") + pieces = [piece for piece in (mode, shot, angle) if piece and piece != "auto" and piece != "standard"] + if pieces: + return "Camera framing uses " + ", ".join(pieces) + return "" + + +def _style_phrase(row: dict[str, Any], style_mode: str) -> str: + if style_mode == "minimal": + return "" + if style_mode == "photographic": + return "realistic creator-shot photography with natural lighting, tactile skin and fabric detail, and clean social-media composition" + style = _clean(row.get("style")) + suffix = _clean(row.get("positive_suffix")) or _prompt_field(_clean(row.get("prompt")), "Use") + if style and suffix: + return f"{style}; {suffix}" + return style or suffix + + +def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str]: + subject_type = _clean(row.get("subject_type")) + primary = _clean(row.get("primary_subject")) + item = _row_value(row, "item", ("Sexual pose", "Erotic outfit", "Clothing")) or _clean(row.get("custom_item")) + item = re.sub(r",?\s*(fashion editorial|resort) styling$", "", item, flags=re.IGNORECASE) + scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene")) + pose = _row_value(row, "pose", ("Sexual pose", "Pose")) + expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) + composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE) + camera = _camera_phrase(row) + style = _style_phrase(row, style_mode) + + if subject_type == "configured_cast" or _clean(row.get("cast_summary")): + subject = _clean(row.get("subject_phrase") or primary or "adult sexual scene") + cast = _clean(row.get("cast_summary")) + role_graph = _clean(row.get("role_graph")) + parts = [ + f"A consensual explicit adult scene with {subject}, all participants 21+ and visibly adult", + f"The cast includes {cast}" if cast else "", + role_graph, + f"The sexual action is {item}" if item else "", + f"The setting is {scene}" if scene else "", + f"Facial expressions are {expression}" if expression else "", + f"The image is framed as {composition}" if composition else "", + camera, + style if detail_level != "concise" else "", + ] + return _paragraph(parts), "metadata(configured_cast)" + + if primary in ("woman", "man") or subject_type in ("woman", "man", "single_any"): + subject = _age_subject(row, "adult woman") + appearance = _appearance_phrase(row) + parts = [ + f"A {subject}" if not subject.lower().startswith(("a ", "an ")) else subject, + f"with {appearance}" if appearance else "", + f"wearing {item}" if item else "", + f"{pose}" if pose else "", + f"with {expression}" if expression else "", + f"in {scene}" if scene else "", + f"framed as {composition}" if composition else "", + camera, + style if detail_level != "concise" else "", + ] + return _paragraph([", ".join(part for part in parts[:6] if part), *parts[6:]]), "metadata(single)" + + subject = _age_subject(row, primary or "adult scene") + parts = [ + f"{subject}", + f"featuring {item}" if item else "", + f"in {scene}" if scene else "", + f"with {expression}" if expression else "", + f"framed as {composition}" if composition else "", + camera, + style if detail_level != "concise" else "", + ] + return _paragraph(parts), "metadata(generic)" + + +def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str, str, str]: + descriptor = _clean(row.get("shared_descriptor")) + soft = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {} + hard = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {} + soft_camera = _clean(row.get("softcore_camera_directive")) or _camera_phrase(soft) + hard_camera = _clean(row.get("hardcore_camera_directive")) or _camera_phrase(hard) + soft_style = _style_phrase(soft, style_mode) + hard_style = _style_phrase(hard, style_mode) + options = row.get("options") if isinstance(row.get("options"), dict) else {} + soft_level = _clean(options.get("softcore_level")).replace("_", " ") + hard_level = _clean(options.get("hardcore_level")).replace("_", " ") + hard_cast = _clean(row.get("hardcore_women_count")) + hard_men = _clean(row.get("hardcore_men_count")) + hard_cast_text = _clean(hard.get("cast_summary")) or ( + f"{hard_cast} adult women and {hard_men} adult men" if hard_cast or hard_men else "" + ) + + soft_parts = [ + f"A visibly adult creator, {descriptor}", + f"shown in a {soft_level or 'softcore'} Insta/OF creator image", + f"wearing {soft.get('item')}" if soft.get("item") else "", + f"{soft.get('pose')}" if soft.get("pose") else "", + f"with {soft.get('expression')}" if soft.get("expression") else "", + f"in {soft.get('scene_text')}" if soft.get("scene_text") else "", + f"framed as {soft.get('composition')}" if soft.get("composition") else "", + soft_camera, + soft_style if detail_level != "concise" else "", + ] + hard_parts = [ + f"The same visibly adult creator, {descriptor}, is the visually central woman in a consensual explicit adult {hard_level or 'hardcore'} scene", + f"all participants are 21+ and visibly adult; the cast includes {hard_cast_text}" if hard_cast_text else "all participants are 21+ and visibly adult", + _clean(hard.get("role_graph")), + f"The sexual action is {hard.get('item')}" if hard.get("item") else "", + f"set in {row.get('hardcore_row', {}).get('scene_text') or hard.get('scene_text')}" if hard.get("scene_text") else "", + f"with {hard.get('expression')}" if hard.get("expression") else "", + f"framed as {hard.get('composition')}" if hard.get("composition") else "", + hard_camera, + hard_style if detail_level != "concise" else "", + ] + return ( + _paragraph(soft_parts), + _combine_negative(row.get("softcore_negative_prompt")), + _paragraph(hard_parts), + _combine_negative(row.get("hardcore_negative_prompt")), + ) + + +def _fallback_text_to_krea( + source_text: str, + preserve_trigger: bool, + detail_level: str, + style_mode: str, +) -> tuple[str, str, str]: + positive, negative = _split_avoid(_strip_trigger(source_text, preserve_trigger)) + positive = re.sub(r"\b(?:Scene|Setting):", "The setting is", positive) + positive = re.sub(r"\b(?:Pose|Sexual pose):", "The pose is", positive) + positive = re.sub(r"\bFacial expressions?:", "The facial expression is", positive) + positive = re.sub(r"\bComposition:", "The composition is", positive) + positive = re.sub(r"\bRole graph:", "The role choreography is", positive) + positive = re.sub(r"\bUse\b", "Use", positive) + positive = _clean(positive) + return _paragraph([positive]), negative, "text(fallback)" + + +def format_krea2_prompt( + source_text: str, + metadata_json: str = "", + negative_prompt: str = "", + input_hint: str = "auto", + target: str = "auto", + detail_level: str = "balanced", + style_mode: str = "preserve", + preserve_trigger: bool = False, + extra_positive: str = "", + extra_negative: str = "", +) -> dict[str, str]: + detail_level = detail_level if detail_level in ("concise", "balanced", "dense") else "balanced" + style_mode = style_mode if style_mode in ("preserve", "photographic", "minimal") else "preserve" + target = target if target in ("auto", "single", "softcore", "hardcore") else "auto" + row, method = _row_from_inputs(source_text, metadata_json, input_hint) + extracted_negative = "" + + if row and row.get("mode") == "Insta/OF": + soft_prompt, soft_negative, hard_prompt, hard_negative = _insta_pair_to_krea(row, detail_level, style_mode) + selected = hard_prompt if target == "hardcore" else soft_prompt if target == "softcore" else soft_prompt + selected_negative = hard_negative if target == "hardcore" else soft_negative + if extra_positive.strip(): + selected = f"{selected.rstrip()} {extra_positive.strip()}" + soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}" + hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}" + negative = _combine_negative(selected_negative, negative_prompt, extra_negative) + return { + "krea_prompt": selected, + "negative_prompt": negative, + "krea_softcore_prompt": soft_prompt, + "krea_hardcore_prompt": hard_prompt, + "softcore_negative_prompt": _combine_negative(soft_negative, extra_negative), + "hardcore_negative_prompt": _combine_negative(hard_negative, extra_negative), + "method": f"{method}:krea2(insta_of_pair)", + } + + if row: + prompt, kind = _normal_row_to_krea(row, detail_level, style_mode) + extracted_negative = _clean(row.get("negative_prompt")) + method = f"{method}:krea2({kind})" + else: + prompt, extracted_negative, method = _fallback_text_to_krea(source_text, preserve_trigger, detail_level, style_mode) + + if extra_positive.strip(): + prompt = f"{prompt.rstrip()} {extra_positive.strip()}" + negative = _combine_negative(extracted_negative, negative_prompt, extra_negative) + return { + "krea_prompt": prompt, + "negative_prompt": negative, + "krea_softcore_prompt": "", + "krea_hardcore_prompt": "", + "softcore_negative_prompt": "", + "hardcore_negative_prompt": "", + "method": method, + } diff --git a/prompt_builder.py b/prompt_builder.py index ce9e172..80c11c9 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -202,6 +202,14 @@ def _list_from(value: Any) -> list[Any]: return [value] +def _is_false(value: Any) -> bool: + if isinstance(value, bool): + return value is False + if isinstance(value, str): + return value.strip().lower() in ("false", "0", "no", "off") + return False + + def _unique_extend(target: list[Any], additions: list[Any]) -> None: seen = set() for item in target: @@ -554,6 +562,24 @@ def load_category_library() -> list[dict[str, Any]]: return categories +def load_scene_pool_library() -> dict[str, list[Any]]: + pools: dict[str, list[Any]] = {} + for path in _json_files(): + data = _read_json(path) + raw_pools = data.get("scene_pools", {}) + if not raw_pools: + continue + if not isinstance(raw_pools, dict): + raise ValueError(f"scene_pools in {path} must be an object") + for name, entries in raw_pools.items(): + pool_name = str(name).strip() + if not pool_name: + continue + pools.setdefault(pool_name, []) + _unique_extend(pools[pool_name], _list_from(entries)) + return pools + + def _extension_targets() -> dict[str, tuple[list[Any], bool]]: return { "women_clothes": (g.WOMEN_CLOTHES, False), @@ -1335,7 +1361,27 @@ def _subject_context( def _scene_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str) -> list[Any]: fallback = g.GROUP_SCENES if subject_type in ("group", "configured_cast") else g.SCENES - return _list_from(_merged_field(category, subcategory, item, "scenes", fallback)) + scene_entries: list[Any] = [] + scene_pools = load_scene_pool_library() + item_source = item if isinstance(item, dict) else None + if item_source is not None and _is_false(item_source.get("inherit_scenes")): + sources = (item_source,) + elif _is_false(subcategory.get("inherit_scenes")): + sources = (subcategory, item_source) + else: + sources = (category, subcategory, item_source) + for source in sources: + if not isinstance(source, dict): + continue + if "scenes" in source: + _unique_extend(scene_entries, _list_from(source["scenes"])) + refs = _list_from(source.get("scene_pool")) + _list_from(source.get("scene_pools")) + for ref in refs: + ref_name = str(ref).strip() + if ref_name not in scene_pools: + raise ValueError(f"Unknown scene pool '{ref_name}'") + _unique_extend(scene_entries, scene_pools[ref_name]) + return scene_entries or fallback def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str, poses: str) -> list[Any]: