Add themed location and composition controls

This commit is contained in:
2026-06-25 22:37:49 +02:00
parent 38ab587e8e
commit 9434070877
3 changed files with 522 additions and 4 deletions
+413 -2
View File
@@ -1348,6 +1348,188 @@ def load_composition_pool_library() -> dict[str, list[Any]]:
return _load_named_pool_library("composition_pools")
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),
@@ -1824,6 +2006,156 @@ 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
def _ethnicity_text_from_value(value: Any) -> str:
if isinstance(value, dict):
return str(value.get("ethnicity") or "").strip()
@@ -5857,6 +6189,50 @@ def _apply_location_config_to_legacy_row(
return row
def _legacy_composition_entries_for_row(row: dict[str, Any]) -> list[Any]:
subject = str(row.get("primary_subject") or "").lower()
if "group" in subject or "layout" in subject:
return list(g.GROUP_COMPOSITIONS)
return list(g.COMPOSITIONS)
def _apply_composition_config_to_legacy_row(
row: dict[str, Any],
composition_config: dict[str, Any],
seed_config: dict[str, int],
seed: int,
row_number: int,
) -> dict[str, Any]:
if not _composition_config_active(composition_config):
return row
composition_entries = _list_from(composition_config.get("composition_entries"))
if composition_config.get("apply_mode") == "add":
choices = _legacy_composition_entries_for_row(row)
_unique_extend(choices, composition_entries)
else:
choices = composition_entries
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
new_composition = _choose_text(composition_rng, choices)
old_composition = str(row.get("composition") or "")
old_prompt_fragment = f"Composition: vertical {old_composition}."
new_prompt_fragment = f"Composition: {_composition_prompt(new_composition)}."
row["source_composition"] = old_composition
row["composition"] = new_composition
row["composition_prompt"] = _composition_prompt(new_composition)
row["composition_config"] = composition_config
if old_composition:
row["prompt"] = str(row.get("prompt") or "").replace(old_prompt_fragment, new_prompt_fragment)
row["caption"] = str(row.get("caption") or "").replace(f", {old_composition},", f", {new_composition},")
else:
row["prompt"] = re.sub(
r"Composition:\s*.*?\.\s*Use",
f"{new_prompt_fragment} Use",
str(row.get("prompt") or ""),
count=1,
)
return row
def _sources_with_inheritance(
category: dict[str, Any],
subcategory: dict[str, Any],
@@ -6042,7 +6418,17 @@ def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any,
return g.EVOCATIVE_ALL if poses == "evocative" else g.POSES
def _composition_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str) -> list[Any]:
def _composition_pool(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
subject_type: str,
composition_config: dict[str, Any] | None = None,
) -> list[Any]:
composition_config = composition_config or {}
composition_entries = _list_from(composition_config.get("composition_entries"))
if _composition_config_active(composition_config) and composition_config.get("apply_mode") == "replace":
return composition_entries
configured = _configured_pool(
category,
subcategory,
@@ -6052,6 +6438,9 @@ def _composition_pool(category: dict[str, Any], subcategory: dict[str, Any], ite
load_composition_pool_library(),
"inherit_compositions",
)
if _composition_config_active(composition_config) and composition_config.get("apply_mode") == "add":
configured = list(configured or [])
_unique_extend(configured, composition_entries)
if configured:
return configured
if subject_type in ("group", "configured_cast"):
@@ -6083,6 +6472,7 @@ def _build_custom_row(
expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
categories = load_category_library()
category_rng = _axis_rng(seed_config, "category", seed, row_number)
@@ -6095,6 +6485,7 @@ def _build_custom_row(
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
parsed_hardcore_position_config = _parse_hardcore_position_config(hardcore_position_config)
parsed_location_config = _parse_location_config(location_config)
parsed_composition_config = _parse_composition_config(composition_config)
requested_women_count = women_count
requested_men_count = men_count
@@ -6259,7 +6650,11 @@ def _build_custom_row(
expression = character_expression_text
source_composition = _choose_text(
composition_rng,
_compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count),
_compatible_entries(
_composition_pool(category, subcategory, item, subject_type, parsed_composition_config),
women_count,
men_count,
),
)
if is_pose_category:
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
@@ -6302,6 +6697,7 @@ def _build_custom_row(
"composition": composition,
"source_composition": source_composition,
"composition_prompt": _composition_prompt(composition),
"composition_config": parsed_composition_config if _composition_config_active(parsed_composition_config) else {},
"role_graph": role_graph,
"source_role_graph": source_role_graph,
"pov_character_labels": pov_character_labels,
@@ -6441,6 +6837,7 @@ def build_prompt(
expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
apply_pool_extensions()
row_number = max(1, int(row_number))
@@ -6452,6 +6849,7 @@ def build_prompt(
pose_ratio = _ratio_or_none(standard_pose_ratio)
parsed_seed_config = _parse_seed_config(seed_config)
parsed_location_config = _parse_location_config(location_config)
parsed_composition_config = _parse_composition_config(composition_config)
content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number)
pose_axis_rng = _axis_rng(parsed_seed_config, "pose", seed, row_number)
person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
@@ -6525,6 +6923,7 @@ def build_prompt(
expression_phase,
hardcore_position_config,
parsed_location_config,
parsed_composition_config,
)
if row.get("source") == "built_in_generator":
@@ -6535,6 +6934,13 @@ def build_prompt(
seed,
row_number,
)
row = _apply_composition_config_to_legacy_row(
row,
parsed_composition_config,
parsed_seed_config,
seed,
row_number,
)
if not expression_enabled:
row = _disable_row_expression(row, "disabled")
if extra_positive.strip():
@@ -6563,6 +6969,7 @@ def build_prompt_from_configs(
character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
@@ -6599,6 +7006,7 @@ def build_prompt_from_configs(
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
)
@@ -7382,6 +7790,7 @@ def build_insta_of_pair(
character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
@@ -7460,6 +7869,7 @@ def build_insta_of_pair(
character_profile="" if primary_slot else character_profile or "",
character_cast="",
location_config=location_config or "",
composition_config=composition_config or "",
)
soft_row["expression_intensity_source"] = soft_expression_intensity_source
if primary_slot_context:
@@ -7518,6 +7928,7 @@ def build_insta_of_pair(
expression_phase="hardcore",
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
)
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
hard_row["pov_character_labels"] = pov_character_labels