diff --git a/prompt_builder.py b/prompt_builder.py index 84972bd..f582e55 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -962,218 +962,6 @@ def _choose_pair(rng: random.Random, items: list[Any]) -> tuple[str, str]: return _pair_from(_weighted_choice(rng, items)) -LOCATION_POOL_PRESETS = { - "custom_only": (), - "all_json_locations": ("*",), - "casual_all": ("casual_",), - "casual_urban": ("casual_urban_scenes",), - "casual_summer": ("casual_summer_scenes",), - "casual_home": ("casual_lounge_scenes",), - "casual_smart": ("casual_smart_scenes",), - "creator_softcore": ("softcore_creator_scenes", "mirror_scenes", "boudoir_bedroom_scenes"), - "mirror_rooms": ("mirror_scenes", "hardcore_mirror_scenes"), - "boudoir_bedroom": ("boudoir_bedroom_scenes", "hardcore_bed_scenes"), - "fetish_studio": ("fetish_studio_scenes",), - "costume_backstage": ("costume_backstage_scenes",), - "hardcore_all": ("hardcore_",), - "hardcore_private": ("hardcore_private_scenes",), - "hardcore_bed": ("hardcore_bed_scenes",), - "hardcore_penetrative": ("hardcore_penetrative_scenes",), - "hardcore_oral": ("hardcore_oral_scenes",), - "hardcore_anal": ("hardcore_anal_scenes",), - "hardcore_threesome": ("hardcore_threesome_scenes",), - "hardcore_group": ("hardcore_group_scenes",), - "hardcore_climax": ("hardcore_climax_scenes",), -} - - -def location_pool_preset_choices() -> list[str]: - pool_choices = [f"pool:{key}" for key in sorted(load_scene_pool_library())] - return list(LOCATION_POOL_PRESETS) + pool_choices - - -COMPOSITION_POOL_PRESETS = { - "custom_only": (), - "all_json_compositions": ("*",), - "casual_all": ("casual_", "streetwear_", "summer_", "cozy_home_", "smart_casual_", "athleisure_"), - "creator_softcore": ("softcore_creator_compositions", "boudoir_body_compositions"), - "hardcore_all": ("hardcore_",), - "hardcore_explicit": ("hardcore_explicit_compositions",), - "no_outfit_check": (), -} - - -COMPOSITION_INLINE_PRESETS = { - "no_outfit_check": [ - "environment-led frame with no outfit-check wording", - "mid-distance scene composition with the room context readable", - "partly occluded candid frame through foreground architecture", - "long perspective frame using repeating background structure", - "waist-up or three-quarter frame without bag, shoes, or footwear emphasis", - ], -} - - -def composition_pool_preset_choices() -> list[str]: - pool_choices = [f"pool:{key}" for key in sorted(load_composition_pool_library())] - return list(COMPOSITION_POOL_PRESETS) + pool_choices - - -THEMATIC_LOCATION_PRESETS = { - "classical_library": { - "locations": [ - {"slug": "classical_large_library", "prompt": "grand classical library hall with towering dark-wood bookshelves, carved columns, rolling ladders, marble floor, warm brass lamps, arched windows, and deep quiet academic atmosphere"}, - {"slug": "old_world_reading_room", "prompt": "large old-world reading room with floor-to-ceiling bookshelves, heavy wooden tables, green banker lamps, leather chairs, tall arched windows, and warm amber evening light"}, - {"slug": "hidden_library_stacks", "prompt": "quiet library stacks with endless tall bookshelves, narrow aisles, rolling ladders, brass lamps, and hidden sightlines between shelves"}, - ], - "compositions": [ - "narrow aisle frame between towering bookshelves", - "over-the-shoulder view through foreground books", - "warm lamp-lit reading-table composition", - "long vanishing-point frame down repeated library stacks", - "partly hidden frame behind carved columns and shelf edges", - ], - }, - "semi_public_affair": { - "locations": [ - {"slug": "hotel_corridor_affair", "prompt": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove"}, - {"slug": "hotel_service_hall", "prompt": "luxury hotel service corridor with repeating linen carts, beige doors, utility shelves, wall sconces, and a private turn away from the main hallway"}, - {"slug": "parking_garage_hidden", "prompt": "empty multi-level parking garage with repeating concrete pillars, parked cars, painted floor lines, low fluorescent light, and shadowed blind spots"}, - {"slug": "office_afterhours_affair", "prompt": "empty corporate office after hours with rows of glass partitions, repeating desks, blinds, copier alcove, muted city light, and no visible coworkers"}, - {"slug": "library_stacks_secret", "prompt": "classical library stacks with endless tall bookshelves, narrow aisles, rolling ladders, carved columns, warm brass lamps, and hidden sightlines between shelves"}, - ], - "compositions": [ - "partly concealed frame from behind a doorway edge", - "long corridor vanishing-point composition with repeated doors", - "hidden alcove frame with foreground obstruction", - "surveillance-like candid angle from across the empty space", - "tight frame using pillars, shelves, or walls to block side visibility", - ], - }, - "hotel_corridor": { - "locations": [ - {"slug": "upscale_hotel_corridor", "prompt": "upscale hotel corridor with repeating doors, patterned carpet, brass wall lamps, quiet service alcoves, and warm late-night light"}, - {"slug": "hotel_service_alcove", "prompt": "hotel service alcove with linen carts, beige utility doors, folded towels, soft wall sconces, and a secluded turn off the main corridor"}, - {"slug": "boutique_hotel_stair_landing", "prompt": "boutique hotel stair landing with repeating railings, framed wall panels, low amber lamps, and a quiet corner between floors"}, - ], - "compositions": [ - "long hallway frame with repeated doors receding behind the body", - "corner-alcove composition partly hidden by a wall edge", - "low corridor angle with patterned carpet leading lines", - "over-the-shoulder frame toward a closed hotel-room door", - ], - }, - "parking_garage": { - "locations": [ - {"slug": "empty_parking_garage", "prompt": "empty multi-level parking garage with repeating concrete pillars, parked cars, painted bay lines, low fluorescent light, and deep shadowed corners"}, - {"slug": "underground_garage_corner", "prompt": "underground parking garage corner with numbered pillars, glossy concrete floor, parked cars, and blue-green fluorescent light"}, - {"slug": "rooftop_parking_deck_night", "prompt": "rooftop parking deck at night with repeated concrete barriers, distant city lights, painted lines, and open wind"}, - ], - "compositions": [ - "pillar-framed composition with repeated concrete columns", - "low angle across painted parking lines", - "hidden corner frame between parked cars", - "wide empty garage frame with strong fluorescent perspective", - ], - }, - "theater_backstage": { - "locations": [ - {"slug": "old_theater_backstage", "prompt": "old theater backstage with repeated velvet curtains, prop racks, costume rails, bulb mirrors, dark wings, and narrow hidden passages"}, - {"slug": "cabaret_backstage_wings", "prompt": "cabaret backstage wings with red curtains, costume racks, vanity bulbs, stage ropes, and warm theatrical shadows"}, - {"slug": "prop_storage_corridor", "prompt": "theater prop storage corridor with stacked trunks, repeated scenery flats, rolling racks, and dim practical lamps"}, - ], - "compositions": [ - "frame between layered velvet curtains", - "backstage mirror-bulb composition with costume racks behind", - "hidden wing angle looking toward the stage light spill", - "narrow prop-aisle frame with repeated vertical flats", - ], - }, - "wine_cellar": { - "locations": [ - {"slug": "private_wine_cellar", "prompt": "private wine cellar with repeating bottle racks, arched brick walls, narrow aisles, dim amber lamps, and secluded corners between shelves"}, - {"slug": "restaurant_wine_storage", "prompt": "restaurant wine storage room with stacked bottle shelves, crate rows, stone floor, soft utility light, and hidden service-door access"}, - {"slug": "arched_cellar_corridor", "prompt": "arched cellar corridor with repeated brick niches, wine racks, low golden lamps, and cool shadowed depth"}, - ], - "compositions": [ - "narrow aisle frame between repeated bottle racks", - "arched brick corridor composition with warm lamps", - "foreground bottle-rack occlusion framing the body", - "low cellar angle with shelves receding behind", - ], - }, - "museum_archive": { - "locations": [ - {"slug": "museum_archive_room", "prompt": "museum archive room with repeating storage shelves, labeled boxes, rolling ladders, long work tables, soft overhead lights, and hidden aisles"}, - {"slug": "gallery_storage_backroom", "prompt": "gallery storage backroom with stacked frames, rolling racks, crate labels, clean concrete floor, and muted work lights"}, - {"slug": "rare_books_archive", "prompt": "rare-books archive with compact shelving, catalog drawers, reading lamps, archival boxes, and narrow private aisles"}, - ], - "compositions": [ - "hidden archive-aisle frame between storage shelves", - "table-edge composition with labeled boxes in the background", - "foreground crate or shelf occlusion", - "long compact-shelving perspective with repeated rows", - ], - }, - "laundromat_late_night": { - "locations": [ - {"slug": "late_night_laundromat", "prompt": "late-night laundromat with repeating washing machines, chrome reflections, tiled floor, fluorescent lights, empty aisles, and a secluded back corner"}, - {"slug": "coin_laundry_back_row", "prompt": "coin laundry back row with stacked dryers, plastic folding tables, detergent shelves, buzzing fluorescent light, and no other customers"}, - {"slug": "laundromat_mirror_windows", "prompt": "quiet laundromat with mirrored machine doors, repeated round windows, tile floor, and cool blue night light through front glass"}, - ], - "compositions": [ - "repeating washer-door perspective behind the body", - "folding-table edge frame with chrome reflections", - "low tiled-floor angle down an empty machine row", - "back-corner composition partly hidden by laundry machines", - ], - }, - "train_station_lockers": { - "locations": [ - {"slug": "train_station_locker_corridor", "prompt": "quiet train-station locker corridor with repeating metal lockers, tiled walls, vending machines, fluorescent light, and a hidden side alcove"}, - {"slug": "empty_platform_underpass", "prompt": "empty station underpass with tiled walls, repeated poster frames, stair railings, fluorescent lights, and late-night quiet"}, - {"slug": "station_service_passage", "prompt": "station service passage with repeating utility doors, metal lockers, warning stripes, and cool overhead light"}, - ], - "compositions": [ - "locker-row vanishing-point composition", - "side-alcove frame partly blocked by metal lockers", - "fluorescent underpass frame with repeated tile lines", - "candid angle from behind a vending machine edge", - ], - }, - "nightclub_back_hall": { - "locations": [ - {"slug": "nightclub_back_hall", "prompt": "nightclub back hallway with black doors, repeated neon strips, coat-check racks, textured walls, and distant colored dance-floor light"}, - {"slug": "club_vip_corridor", "prompt": "VIP club corridor with velvet ropes, mirrored wall panels, low red light, repeated booths, and a private bend in the hallway"}, - {"slug": "music_venue_greenroom_hall", "prompt": "music venue greenroom corridor with stickered doors, cable cases, dim practical lamps, and repeated black curtains"}, - ], - "compositions": [ - "neon hallway frame with repeated dark doors", - "partly hidden VIP-booth angle", - "mirror-panel composition with colored light streaks", - "tight backstage corridor frame with curtains at the edges", - ], - }, - "restaurant_private_booth": { - "locations": [ - {"slug": "restaurant_private_booth", "prompt": "dim restaurant private booth with high banquettes, repeating table lamps, dark wood partitions, folded napkins, and secluded sightlines"}, - {"slug": "empty_bistro_back_corner", "prompt": "empty bistro back corner with tiled floor, small round tables, brass lamps, mirrored walls, and a hidden booth"}, - {"slug": "afterhours_dining_room", "prompt": "after-hours dining room with stacked chairs, repeated tables, low amber sconces, and a quiet service doorway"}, - ], - "compositions": [ - "booth-partition frame with high seat backs blocking the sides", - "table-edge composition with lamps repeating behind", - "mirror-wall restaurant angle with dark wood partitions", - "after-hours dining-room perspective through empty tables", - ], - }, -} - - -def location_theme_choices() -> list[str]: - return list(THEMATIC_LOCATION_PRESETS) - - def _extension_targets() -> dict[str, tuple[list[Any], bool]]: return { "women_clothes": (g.WOMEN_CLOTHES, False), @@ -1532,274 +1320,6 @@ def build_filter_config_json( ) -def _location_pool_names_for_preset(preset: str) -> list[str]: - scene_pools = load_scene_pool_library() - preset = str(preset or "custom_only") - if preset.startswith("pool:"): - pool_name = preset.split(":", 1)[1].strip() - return [pool_name] if pool_name in scene_pools else [] - selectors = LOCATION_POOL_PRESETS.get(preset, ()) - names: list[str] = [] - for selector in selectors: - if selector == "*": - _unique_extend(names, sorted(scene_pools)) - elif selector.endswith("_"): - _unique_extend(names, sorted(name for name in scene_pools if name.startswith(selector))) - elif selector in scene_pools: - _unique_extend(names, [selector]) - return names - - -def _custom_location_entries(custom_locations: str) -> list[dict[str, str]]: - entries: list[dict[str, str]] = [] - for raw_line in str(custom_locations or "").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - slug = "" - prompt = line - if ":" in line: - maybe_slug, maybe_prompt = line.split(":", 1) - if maybe_slug.strip() and maybe_prompt.strip(): - slug = _slug(maybe_slug) - prompt = maybe_prompt.strip() - prompt = prompt.strip() - if prompt: - entries.append({"slug": slug or _slug(prompt), "prompt": prompt}) - return entries - - -def _scene_entries_for_pool_names(pool_names: list[str]) -> list[Any]: - scene_pools = load_scene_pool_library() - entries: list[Any] = [] - for pool_name in pool_names: - if pool_name not in scene_pools: - continue - _unique_extend(entries, scene_pools[pool_name]) - return entries - - -def build_location_pool_json( - enabled: bool = True, - combine_mode: str = "replace", - preset: str = "custom_only", - custom_locations: str = "", - location_config: str | dict[str, Any] | None = "", -) -> str: - incoming = _parse_location_config(location_config) - combine_mode = combine_mode if combine_mode in ("replace", "add") else "replace" - pool_names = _location_pool_names_for_preset(preset) - entries = _scene_entries_for_pool_names(pool_names) - _unique_extend(entries, _custom_location_entries(custom_locations)) - - if combine_mode == "add" and incoming.get("enabled"): - apply_mode = str(incoming.get("apply_mode") or "replace") - merged_pool_names = _list_from(incoming.get("pool_names")) - _unique_extend(merged_pool_names, pool_names) - merged_entries = _list_from(incoming.get("scene_entries")) - _unique_extend(merged_entries, entries) - else: - apply_mode = "replace" if combine_mode == "replace" else "add" - merged_pool_names = pool_names - merged_entries = entries - - active = bool(enabled) and bool(merged_entries) - summary = ( - f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}" - if active - else "disabled or empty" - ) - return json.dumps( - { - "enabled": active, - "apply_mode": apply_mode, - "pool_names": merged_pool_names, - "scene_entries": merged_entries, - "summary": summary, - }, - ensure_ascii=True, - sort_keys=True, - ) - - -def _parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]: - if not location_config: - return {"enabled": False, "apply_mode": "replace", "pool_names": [], "scene_entries": []} - if isinstance(location_config, dict): - raw = dict(location_config) - else: - try: - raw = json.loads(str(location_config)) - except json.JSONDecodeError as exc: - raise ValueError(f"Invalid location_config JSON: {exc}") from exc - if not isinstance(raw, dict): - raise ValueError("location_config must be a JSON object") - entries = _list_from(raw.get("scene_entries")) - if not entries and raw.get("pool_names"): - entries = _scene_entries_for_pool_names([str(name) for name in _list_from(raw.get("pool_names"))]) - return { - "enabled": bool(raw.get("enabled")) and bool(entries), - "apply_mode": str(raw.get("apply_mode") or "replace") if str(raw.get("apply_mode") or "replace") in ("replace", "add") else "replace", - "pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()], - "scene_entries": entries, - "summary": str(raw.get("summary") or ""), - } - - -def _location_config_active(location_config: dict[str, Any]) -> bool: - return bool(location_config.get("enabled")) and bool(location_config.get("scene_entries")) - - -def _composition_pool_names_for_preset(preset: str) -> list[str]: - composition_pools = load_composition_pool_library() - preset = str(preset or "custom_only") - if preset.startswith("pool:"): - pool_name = preset.split(":", 1)[1].strip() - return [pool_name] if pool_name in composition_pools else [] - selectors = COMPOSITION_POOL_PRESETS.get(preset, ()) - names: list[str] = [] - for selector in selectors: - if selector == "*": - _unique_extend(names, sorted(composition_pools)) - elif selector.endswith("_"): - _unique_extend(names, sorted(name for name in composition_pools if name.startswith(selector))) - elif selector in composition_pools: - _unique_extend(names, [selector]) - return names - - -def _custom_composition_entries(custom_compositions: str) -> list[str]: - entries: list[str] = [] - for raw_line in str(custom_compositions or "").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - entries.append(line) - return entries - - -def _composition_entries_for_pool_names(pool_names: list[str]) -> list[Any]: - composition_pools = load_composition_pool_library() - entries: list[Any] = [] - for pool_name in pool_names: - if pool_name not in composition_pools: - continue - _unique_extend(entries, composition_pools[pool_name]) - return entries - - -def build_composition_pool_json( - enabled: bool = True, - combine_mode: str = "replace", - preset: str = "custom_only", - custom_compositions: str = "", - composition_config: str | dict[str, Any] | None = "", -) -> str: - incoming = _parse_composition_config(composition_config) - combine_mode = combine_mode if combine_mode in ("replace", "add") else "replace" - pool_names = _composition_pool_names_for_preset(preset) - entries = _composition_entries_for_pool_names(pool_names) - _unique_extend(entries, COMPOSITION_INLINE_PRESETS.get(str(preset or ""), [])) - _unique_extend(entries, _custom_composition_entries(custom_compositions)) - - if combine_mode == "add" and incoming.get("enabled"): - apply_mode = str(incoming.get("apply_mode") or "replace") - merged_pool_names = _list_from(incoming.get("pool_names")) - _unique_extend(merged_pool_names, pool_names) - merged_entries = _list_from(incoming.get("composition_entries")) - _unique_extend(merged_entries, entries) - else: - apply_mode = "replace" if combine_mode == "replace" else "add" - merged_pool_names = pool_names - merged_entries = entries - - active = bool(enabled) and bool(merged_entries) - summary = ( - f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}" - if active - else "disabled or empty" - ) - return json.dumps( - { - "enabled": active, - "apply_mode": apply_mode, - "pool_names": merged_pool_names, - "composition_entries": merged_entries, - "summary": summary, - }, - ensure_ascii=True, - sort_keys=True, - ) - - -def _parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]: - if not composition_config: - return {"enabled": False, "apply_mode": "replace", "pool_names": [], "composition_entries": []} - if isinstance(composition_config, dict): - raw = dict(composition_config) - else: - try: - raw = json.loads(str(composition_config)) - except json.JSONDecodeError as exc: - raise ValueError(f"Invalid composition_config JSON: {exc}") from exc - if not isinstance(raw, dict): - raise ValueError("composition_config must be a JSON object") - entries = _list_from(raw.get("composition_entries")) - if not entries and raw.get("pool_names"): - entries = _composition_entries_for_pool_names([str(name) for name in _list_from(raw.get("pool_names"))]) - return { - "enabled": bool(raw.get("enabled")) and bool(entries), - "apply_mode": str(raw.get("apply_mode") or "replace") if str(raw.get("apply_mode") or "replace") in ("replace", "add") else "replace", - "pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()], - "composition_entries": entries, - "summary": str(raw.get("summary") or ""), - } - - -def _composition_config_active(composition_config: dict[str, Any]) -> bool: - return bool(composition_config.get("enabled")) and bool(composition_config.get("composition_entries")) - - -def build_thematic_location_json( - enabled: bool = True, - combine_mode: str = "replace", - theme: str = "semi_public_affair", - custom_locations: str = "", - custom_compositions: str = "", - location_config: str | dict[str, Any] | None = "", - composition_config: str | dict[str, Any] | None = "", -) -> tuple[str, str, str]: - theme_data = THEMATIC_LOCATION_PRESETS.get(str(theme or ""), THEMATIC_LOCATION_PRESETS["semi_public_affair"]) - location_lines = "\n".join( - f"{entry['slug']}: {entry['prompt']}" - for entry in theme_data.get("locations", []) - if isinstance(entry, dict) and entry.get("slug") and entry.get("prompt") - ) - if custom_locations.strip(): - location_lines = "\n".join(part for part in (location_lines, custom_locations.strip()) if part) - composition_lines = "\n".join(str(entry) for entry in theme_data.get("compositions", []) if str(entry).strip()) - if custom_compositions.strip(): - composition_lines = "\n".join(part for part in (composition_lines, custom_compositions.strip()) if part) - resolved_location_config = build_location_pool_json( - enabled=enabled, - combine_mode=combine_mode, - preset="custom_only", - custom_locations=location_lines, - location_config=location_config or "", - ) - resolved_composition_config = build_composition_pool_json( - enabled=enabled, - combine_mode=combine_mode, - preset="custom_only", - custom_compositions=composition_lines, - composition_config=composition_config or "", - ) - location_summary = json.loads(resolved_location_config).get("summary", "") - composition_summary = json.loads(resolved_composition_config).get("summary", "") - summary = f"{theme}; locations={location_summary}; compositions={composition_summary}" - return resolved_location_config, resolved_composition_config, summary - - LOCATION_POOL_PRESETS = location_policy.LOCATION_POOL_PRESETS COMPOSITION_POOL_PRESETS = location_policy.COMPOSITION_POOL_PRESETS COMPOSITION_INLINE_PRESETS = location_policy.COMPOSITION_INLINE_PRESETS