Extract location config policy

This commit is contained in:
2026-06-27 00:12:21 +02:00
parent 6abcccbae1
commit fef2bf6d81
6 changed files with 716 additions and 8 deletions
@@ -104,6 +104,10 @@ Already isolated:
- JSON category loading, subcategory normalization, named scene/expression/
composition pool loading, cast compatibility filtering, exact subcategory
lookup, and inheritance-based pool merging live in `category_library.py`.
- location/composition config presets, themed location packs, custom
location/composition entry parsing, merge behavior, and config parsing live
in `location_config.py`; `prompt_builder.py` still applies selected configs
to rows.
- 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.
+3 -2
View File
@@ -70,6 +70,7 @@ Core helper ownership:
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
| `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. |
| `location_config.py` | Location/composition preset schemas, themed location packs, custom location/composition parsing, pool merge behavior, and location/composition config parsing. |
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, and hardcore cast count policy. |
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
| `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. |
@@ -264,7 +265,7 @@ Edit targets:
- Add reusable named locations: `categories/location_pools.json`.
- Add category-specific locations: the category JSON file.
- Add quick workflow-only locations: `SxCP Location Pool` custom locations.
- Add themed location packs: `THEMATIC_LOCATION_PRESETS` in `prompt_builder.py`.
- Add themed location packs: `THEMATIC_LOCATION_PRESETS` in `location_config.py`.
### Expression
@@ -885,7 +886,7 @@ Use these traces to narrow a problem in one pass.
location pools.
3. If a scene-camera adapter rewrote composition, inspect
`scene_camera_adapters.py`.
4. If the issue comes from `Location Theme`, edit `THEMATIC_LOCATION_PRESETS`.
4. If the issue comes from `Location Theme`, edit `location_config.py` / `THEMATIC_LOCATION_PRESETS`.
### Trigger missing after formatting
+530
View File
@@ -0,0 +1,530 @@
from __future__ import annotations
import json
import re
from typing import Any
try:
from .category_library import load_composition_pool_library, load_scene_pool_library
except ImportError: # Allows local smoke tests from the repository root.
from category_library import load_composition_pool_library, load_scene_pool_library
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",),
}
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",
],
}
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 _slug(value: str) -> str:
text = str(value or "").lower()
text = re.sub(r"[^a-z0-9]+", "_", text)
return text.strip("_")[:48] or "custom"
def _list_from(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def _unique_extend(target: list[Any], additions: list[Any]) -> None:
seen = set()
for item in target:
try:
seen.add(json.dumps(item, sort_keys=True))
except TypeError:
seen.add(repr(item))
for item in additions:
try:
marker = json.dumps(item, sort_keys=True)
except TypeError:
marker = repr(item)
if marker not in seen:
target.append(item)
seen.add(marker)
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
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
def location_theme_choices() -> list[str]:
return list(THEMATIC_LOCATION_PRESETS)
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_names_for_preset = location_pool_names_for_preset
_custom_location_entries = custom_location_entries
_scene_entries_for_pool_names = scene_entries_for_pool_names
_parse_location_config = parse_location_config
_location_config_active = location_config_active
_composition_pool_names_for_preset = composition_pool_names_for_preset
_custom_composition_entries = custom_composition_entries
_composition_entries_for_pool_names = composition_entries_for_pool_names
_parse_composition_config = parse_composition_config
_composition_config_active = composition_config_active
+10 -6
View File
@@ -7,29 +7,33 @@ try:
from .prompt_builder import (
build_cast_config_json,
build_category_config_json,
cast_preset_choices,
category_preset_choices,
subcategory_choices,
)
from .location_config import (
build_composition_pool_json,
build_location_pool_json,
build_thematic_location_json,
cast_preset_choices,
category_preset_choices,
composition_pool_preset_choices,
location_pool_preset_choices,
location_theme_choices,
subcategory_choices,
)
except ImportError: # Allows local smoke tests from the repository root.
from prompt_builder import (
build_cast_config_json,
build_category_config_json,
cast_preset_choices,
category_preset_choices,
subcategory_choices,
)
from location_config import (
build_composition_pool_json,
build_location_pool_json,
build_thematic_location_json,
cast_preset_choices,
category_preset_choices,
composition_pool_preset_choices,
location_pool_preset_choices,
location_theme_choices,
subcategory_choices,
)
+112
View File
@@ -25,6 +25,7 @@ try:
)
from . import camera_config as camera_policy
from . import generate_prompt_batches as g
from . import location_config as location_policy
from . import pair_clothing
from . import pair_camera
from . import pair_cast
@@ -62,6 +63,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
)
import camera_config as camera_policy
import generate_prompt_batches as g
import location_config as location_policy
import pair_clothing
import pair_camera
import pair_cast
@@ -1798,6 +1800,116 @@ def build_thematic_location_json(
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
THEMATIC_LOCATION_PRESETS = location_policy.THEMATIC_LOCATION_PRESETS
def location_pool_preset_choices() -> list[str]:
return location_policy.location_pool_preset_choices()
def composition_pool_preset_choices() -> list[str]:
return location_policy.composition_pool_preset_choices()
def location_theme_choices() -> list[str]:
return location_policy.location_theme_choices()
def _location_pool_names_for_preset(preset: str) -> list[str]:
return location_policy.location_pool_names_for_preset(preset)
def _custom_location_entries(custom_locations: str) -> list[dict[str, str]]:
return location_policy.custom_location_entries(custom_locations)
def _scene_entries_for_pool_names(pool_names: list[str]) -> list[Any]:
return location_policy.scene_entries_for_pool_names(pool_names)
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:
return location_policy.build_location_pool_json(
enabled=enabled,
combine_mode=combine_mode,
preset=preset,
custom_locations=custom_locations,
location_config=location_config,
)
def _parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]:
return location_policy.parse_location_config(location_config)
def _location_config_active(location_config: dict[str, Any]) -> bool:
return location_policy.location_config_active(location_config)
def _composition_pool_names_for_preset(preset: str) -> list[str]:
return location_policy.composition_pool_names_for_preset(preset)
def _custom_composition_entries(custom_compositions: str) -> list[str]:
return location_policy.custom_composition_entries(custom_compositions)
def _composition_entries_for_pool_names(pool_names: list[str]) -> list[Any]:
return location_policy.composition_entries_for_pool_names(pool_names)
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:
return location_policy.build_composition_pool_json(
enabled=enabled,
combine_mode=combine_mode,
preset=preset,
custom_compositions=custom_compositions,
composition_config=composition_config,
)
def _parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]:
return location_policy.parse_composition_config(composition_config)
def _composition_config_active(composition_config: dict[str, Any]) -> bool:
return location_policy.composition_config_active(composition_config)
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]:
return location_policy.build_thematic_location_json(
enabled=enabled,
combine_mode=combine_mode,
theme=theme,
custom_locations=custom_locations,
custom_compositions=custom_compositions,
location_config=location_config,
composition_config=composition_config,
)
def _ethnicity_text_from_value(value: Any) -> str:
if isinstance(value, dict):
return str(value.get("ethnicity") or "").strip()
+57
View File
@@ -27,6 +27,7 @@ import caption_naturalizer # noqa: E402
import category_library # noqa: E402
import __init__ as sxcp_nodes # noqa: E402
import krea_formatter # noqa: E402
import location_config # noqa: E402
import prompt_builder as pb # noqa: E402
import sdxl_formatter # noqa: E402
import seed_config # noqa: E402
@@ -498,6 +499,61 @@ def smoke_config_route_location_theme() -> None:
_expect_formatter_outputs(row, "config_route_location_theme", target="single")
def smoke_location_config_policy() -> None:
_expect(pb.LOCATION_POOL_PRESETS is location_config.LOCATION_POOL_PRESETS, "Prompt builder location presets are not delegated")
_expect(pb.COMPOSITION_POOL_PRESETS is location_config.COMPOSITION_POOL_PRESETS, "Prompt builder composition presets are not delegated")
_expect("classical_library" in location_config.location_theme_choices(), "Location themes lost classical_library")
custom = json.loads(
pb.build_location_pool_json(
enabled=True,
combine_mode="replace",
preset="custom_only",
custom_locations="custom_room: a quiet room with warm lamps",
)
)
_expect(custom.get("enabled") is True, "Custom location config should be active")
_expect(custom.get("apply_mode") == "replace", "Custom location config lost replace mode")
_expect(custom.get("scene_entries", [{}])[0].get("slug") == "custom_room", "Custom location slug parser changed")
added = json.loads(
location_config.build_location_pool_json(
enabled=True,
combine_mode="add",
preset="custom_only",
custom_locations="second_room: another quiet room",
location_config=custom,
)
)
_expect(added.get("apply_mode") == "replace", "Location add merge should preserve incoming apply_mode")
_expect(len(added.get("scene_entries") or []) == 2, "Location add merge did not keep both custom locations")
composition = json.loads(
pb.build_composition_pool_json(
enabled=True,
combine_mode="replace",
preset="no_outfit_check",
custom_compositions="manual frame through foreground bookshelves",
)
)
_expect(composition.get("enabled") is True, "Composition config should be active")
_expect(
any("outfit-check" in str(entry) for entry in composition.get("composition_entries") or []),
"Composition inline preset no_outfit_check was not applied",
)
parsed = pb._parse_location_config({"enabled": True, "pool_names": [], "scene_entries": custom["scene_entries"]})
_expect(pb._location_config_active(parsed), "Prompt builder location parser wrapper is inactive")
themed_location, themed_composition, theme_summary = pb.build_thematic_location_json(
enabled=True,
combine_mode="replace",
theme="classical_library",
)
_expect("classical_library" in theme_summary, "Themed location summary lost theme name")
_expect(json.loads(themed_location).get("scene_entries"), "Themed location did not output locations")
_expect(json.loads(themed_composition).get("composition_entries"), "Themed location did not output compositions")
def smoke_category_library_route() -> None:
categories = category_library.load_category_library()
_expect(len(categories) >= 3, "category library should load JSON categories")
@@ -2404,6 +2460,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("builtin_single_woman", smoke_builtin_single),
("camera_scene_single", smoke_camera_scene_single),
("config_route_location_theme", smoke_config_route_location_theme),
("location_config_policy", smoke_location_config_policy),
("category_library_route", smoke_category_library_route),
("hardcore_category_routes", smoke_hardcore_category_routes),
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),