Preserve location route metadata

This commit is contained in:
2026-06-27 13:21:51 +02:00
parent 63e8489fb2
commit 75a71a2df6
9 changed files with 215 additions and 24 deletions
+4
View File
@@ -420,6 +420,10 @@ spaces: front/side/back views, zoom, and elevation change which desks, windows,
partitions, bookshelves, reading tables, lamps, or aisles are kept visible. In partitions, bookshelves, reading tables, lamps, or aisles are kept visible. In
male-POV setups this becomes a first-person spatial description and the male-POV setups this becomes a first-person spatial description and the
external camera sentence is suppressed. external camera sentence is suppressed.
Rows keep the selected `scene_entry`, `location_theme`, `scene_theme`,
`composition_entry`, `composition_theme`, and `scene_camera_profile_key` in
`metadata_json` so location/camera behavior can be debugged without guessing
from prompt text alone.
`SxCP SDXL Formatter` rewrites prompt builder output or `metadata_json` into `SxCP SDXL Formatter` rewrites prompt builder output or `metadata_json` into
comma-tag SDXL/Pony-style prompts. Connect `metadata_json` when possible so comma-tag SDXL/Pony-style prompts. Connect `metadata_json` when possible so
+7
View File
@@ -508,6 +508,8 @@ plain prompt text. When debugging, inspect these fields before editing pools.
| `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. | | `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. |
| `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. | | `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. |
| `scene_text` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final location text. | | `scene_text` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final location text. |
| `scene_entry` | `row_prompt_axes.resolve_prompt_axes` / `row_location` | Debug/future route rules | Structured selected scene entry, preserving slug/prompt plus theme metadata when available. |
| `location_theme`, `scene_theme` | `location_config.py`, selected scene entry | Debug/camera route rules | Active theme on the location config and theme of the selected scene. This makes theme-driven behavior inspectable instead of only string-inferred. |
| `source_scene_text` | location/body-exposure/camera adapters | Debug/continuity | Previous scene text before an override. | | `source_scene_text` | location/body-exposure/camera adapters | Debug/continuity | Previous scene text before an override. |
| `location_config` | Location config parser | Debug | Active location pool config, if connected. | | `location_config` | Location config parser | Debug | Active location pool config, if connected. |
| `pose` | `row_prompt_axes.resolve_prompt_axes` | Formatters | Generic pose text. Less important for hardcore action categories than `item`/`role_graph`. | | `pose` | `row_prompt_axes.resolve_prompt_axes` | Formatters | Generic pose text. Less important for hardcore action categories than `item`/`role_graph`. |
@@ -517,11 +519,13 @@ plain prompt text. When debugging, inspect these fields before editing pools.
| `expression_enabled`, `expression_disabled` | Builder/slot override | All formatters | Hard gate for whether expression text should appear. | | `expression_enabled`, `expression_disabled` | Builder/slot override | All formatters | Hard gate for whether expression text should appear. |
| `expression_intensity_source` | Builder/slot override | Debug | Explains whether intensity came from input, random, slot, or disabled state. | | `expression_intensity_source` | Builder/slot override | Debug | Explains whether intensity came from input, random, slot, or disabled state. |
| `composition` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final framing phrase. | | `composition` | `row_prompt_axes.resolve_prompt_axes` | All formatters | Final framing phrase. |
| `composition_entry`, `composition_theme` | `row_prompt_axes.resolve_prompt_axes` / `row_location` | Debug/future route rules | Structured selected composition entry and active composition theme. |
| `source_composition` | `row_prompt_axes.resolve_prompt_axes` | Krea hardcore rewrite | Previous/raw composition, often better for action inference. | | `source_composition` | `row_prompt_axes.resolve_prompt_axes` | Krea hardcore rewrite | Previous/raw composition, often better for action inference. |
| `composition_config` | Composition config parser | Debug | Active composition pool config, if connected. | | `composition_config` | Composition config parser | Debug | Active composition pool config, if connected. |
| `camera_config` | Camera nodes/parser | Krea/SDXL/debug | Structured camera settings. | | `camera_config` | Camera nodes/parser | Krea/SDXL/debug | Structured camera settings. |
| `camera_directive` | `_camera_directive` | Krea/Naturalizer/prompt text | Human camera sentence. Suppressed for POV. | | `camera_directive` | `_camera_directive` | Krea/Naturalizer/prompt text | Human camera sentence. Suppressed for POV. |
| `camera_scene_directive` | scene-camera adapter | Krea/Naturalizer/prompt text | Location-aware camera layout sentence. | | `camera_scene_directive` | scene-camera adapter | Krea/Naturalizer/prompt text | Location-aware camera layout sentence. |
| `scene_camera_profile`, `scene_camera_profile_key` | `row_camera.apply_camera_config` | Debug/camera route rules | Structured camera profile selected for the current scene, e.g. `classical_library` or `coworking_lounge`. |
| `subject_type`, `subject_phrase` | `row_subject_route.resolve_subject_route` | Formatters | Single/couple/group/configured cast route. | | `subject_type`, `subject_phrase` | `row_subject_route.resolve_subject_route` | Formatters | Single/couple/group/configured cast route. |
| `women_count`, `men_count`, `person_count` | `row_subject_route.resolve_subject_route` | Pair/formatters/debug | Effective cast counts. | | `women_count`, `men_count`, `person_count` | `row_subject_route.resolve_subject_route` | Pair/formatters/debug | Effective cast counts. |
| `cast_descriptors`, `cast_descriptor_text` | `row_subject_route.resolve_subject_route` | Krea/SDXL/Naturalizer | Visible cast descriptors. | | `cast_descriptors`, `cast_descriptor_text` | `row_subject_route.resolve_subject_route` | Krea/SDXL/Naturalizer | Visible cast descriptors. |
@@ -651,6 +655,9 @@ Current camera-aware scene adapter:
- Scene profiles live in `scene_camera_adapters.SCENE_CAMERA_PROFILES`. - Scene profiles live in `scene_camera_adapters.SCENE_CAMERA_PROFILES`.
- Coworking/business-cafe/office scenes and classical library/book-stack scenes - Coworking/business-cafe/office scenes and classical library/book-stack scenes
are detected by `scene_camera_profile`. are detected by `scene_camera_profile`.
- Location themes preserve `theme` on configs and selected scene entries, and
rows expose `location_theme`, `scene_theme`, `composition_theme`, and
`scene_camera_profile_key` for debugging and future route rules.
- Direction, distance, and elevation details come from profile-aware helpers - Direction, distance, and elevation details come from profile-aware helpers
such as `scene_direction_detail`, `scene_distance_detail`, and such as `scene_direction_detail`, `scene_distance_detail`, and
`scene_elevation_detail`. `scene_elevation_detail`.
+25 -4
View File
@@ -322,6 +322,7 @@ def build_location_pool_json(
merged_entries = entries merged_entries = entries
active = bool(enabled) and bool(merged_entries) active = bool(enabled) and bool(merged_entries)
theme = str(incoming.get("theme") or "") if combine_mode == "add" and incoming.get("enabled") else ""
summary = ( summary = (
f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}" f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}"
if active if active
@@ -334,6 +335,7 @@ def build_location_pool_json(
"pool_names": merged_pool_names, "pool_names": merged_pool_names,
"scene_entries": merged_entries, "scene_entries": merged_entries,
"summary": summary, "summary": summary,
"theme": theme,
}, },
ensure_ascii=True, ensure_ascii=True,
sort_keys=True, sort_keys=True,
@@ -342,7 +344,7 @@ def build_location_pool_json(
def parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]: def parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]:
if not location_config: if not location_config:
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "scene_entries": []} return {"enabled": False, "apply_mode": "replace", "pool_names": [], "scene_entries": [], "theme": ""}
if isinstance(location_config, dict): if isinstance(location_config, dict):
raw = dict(location_config) raw = dict(location_config)
else: else:
@@ -361,6 +363,7 @@ def parse_location_config(location_config: str | dict[str, Any] | None) -> dict[
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()], "pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
"scene_entries": entries, "scene_entries": entries,
"summary": str(raw.get("summary") or ""), "summary": str(raw.get("summary") or ""),
"theme": str(raw.get("theme") or ""),
} }
@@ -432,6 +435,7 @@ def build_composition_pool_json(
merged_entries = entries merged_entries = entries
active = bool(enabled) and bool(merged_entries) active = bool(enabled) and bool(merged_entries)
theme = str(incoming.get("theme") or "") if combine_mode == "add" and incoming.get("enabled") else ""
summary = ( summary = (
f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}" f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}"
if active if active
@@ -444,6 +448,7 @@ def build_composition_pool_json(
"pool_names": merged_pool_names, "pool_names": merged_pool_names,
"composition_entries": merged_entries, "composition_entries": merged_entries,
"summary": summary, "summary": summary,
"theme": theme,
}, },
ensure_ascii=True, ensure_ascii=True,
sort_keys=True, sort_keys=True,
@@ -452,7 +457,7 @@ def build_composition_pool_json(
def parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]: def parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]:
if not composition_config: if not composition_config:
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "composition_entries": []} return {"enabled": False, "apply_mode": "replace", "pool_names": [], "composition_entries": [], "theme": ""}
if isinstance(composition_config, dict): if isinstance(composition_config, dict):
raw = dict(composition_config) raw = dict(composition_config)
else: else:
@@ -471,6 +476,7 @@ def parse_composition_config(composition_config: str | dict[str, Any] | None) ->
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()], "pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
"composition_entries": entries, "composition_entries": entries,
"summary": str(raw.get("summary") or ""), "summary": str(raw.get("summary") or ""),
"theme": str(raw.get("theme") or ""),
} }
@@ -512,8 +518,23 @@ def build_thematic_location_json(
custom_compositions=composition_lines, custom_compositions=composition_lines,
composition_config=composition_config or "", composition_config=composition_config or "",
) )
location_summary = json.loads(resolved_location_config).get("summary", "") location_payload = json.loads(resolved_location_config)
composition_summary = json.loads(resolved_composition_config).get("summary", "") composition_payload = json.loads(resolved_composition_config)
location_payload["theme"] = str(theme or "")
composition_payload["theme"] = str(theme or "")
themed_scene_entries = []
for entry in location_payload.get("scene_entries") or []:
if isinstance(entry, dict):
themed_entry = dict(entry)
themed_entry.setdefault("theme", str(theme or ""))
themed_scene_entries.append(themed_entry)
else:
themed_scene_entries.append(entry)
location_payload["scene_entries"] = themed_scene_entries
resolved_location_config = json.dumps(location_payload, ensure_ascii=True, sort_keys=True)
resolved_composition_config = json.dumps(composition_payload, ensure_ascii=True, sort_keys=True)
location_summary = location_payload.get("summary", "")
composition_summary = composition_payload.get("summary", "")
summary = f"{theme}; locations={location_summary}; compositions={composition_summary}" summary = f"{theme}; locations={location_summary}; compositions={composition_summary}"
return resolved_location_config, resolved_composition_config, summary return resolved_location_config, resolved_composition_config, summary
+4
View File
@@ -2380,6 +2380,7 @@ def _build_custom_row(
) )
scene_slug = prompt_axes.scene_slug scene_slug = prompt_axes.scene_slug
scene = prompt_axes.scene scene = prompt_axes.scene
scene_entry = dict(prompt_axes.scene_entry)
pose = prompt_axes.pose pose = prompt_axes.pose
expression = prompt_axes.expression expression = prompt_axes.expression
shared_expression = prompt_axes.shared_expression shared_expression = prompt_axes.shared_expression
@@ -2387,6 +2388,7 @@ def _build_custom_row(
character_expression_text = prompt_axes.character_expression_text character_expression_text = prompt_axes.character_expression_text
source_composition = prompt_axes.source_composition source_composition = prompt_axes.source_composition
composition = prompt_axes.composition composition = prompt_axes.composition
composition_entry = dict(prompt_axes.composition_entry)
action_route = _action_position_route( action_route = _action_position_route(
is_pose_category=is_pose_category, is_pose_category=is_pose_category,
subcategory=subcategory, subcategory=subcategory,
@@ -2424,6 +2426,7 @@ def _build_custom_row(
negative_prompt=text_fields.negative_prompt, negative_prompt=text_fields.negative_prompt,
scene_slug=scene_slug, scene_slug=scene_slug,
scene=scene, scene=scene,
scene_entry=scene_entry,
pose=pose, pose=pose,
expression=expression, expression=expression,
shared_expression=shared_expression, shared_expression=shared_expression,
@@ -2434,6 +2437,7 @@ def _build_custom_row(
expression_intensity_source=expression_intensity_source, expression_intensity_source=expression_intensity_source,
composition=composition, composition=composition,
source_composition=source_composition, source_composition=source_composition,
composition_entry=composition_entry,
role_graph=role_graph, role_graph=role_graph,
source_role_graph=source_role_graph, source_role_graph=source_role_graph,
action_family=action_family, action_family=action_family,
+13
View File
@@ -37,6 +37,7 @@ class CustomRowAssemblyRequest:
negative_prompt: str negative_prompt: str
scene_slug: str scene_slug: str
scene: str scene: str
scene_entry: dict[str, Any]
pose: str pose: str
expression: str expression: str
shared_expression: str shared_expression: str
@@ -47,6 +48,7 @@ class CustomRowAssemblyRequest:
expression_intensity_source: str expression_intensity_source: str
composition: str composition: str
source_composition: str source_composition: str
composition_entry: dict[str, Any]
role_graph: str role_graph: str
source_role_graph: str source_role_graph: str
action_family: str action_family: str
@@ -85,6 +87,7 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
"style": r.style, "style": r.style,
"scene": r.scene, "scene": r.scene,
"scene_slug": r.scene_slug, "scene_slug": r.scene_slug,
"scene_entry": r.scene_entry,
"pose": r.pose, "pose": r.pose,
"expression": r.expression, "expression": r.expression,
"shared_expression": r.shared_expression, "shared_expression": r.shared_expression,
@@ -95,6 +98,7 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
"expression_intensity": r.expression_intensity, "expression_intensity": r.expression_intensity,
"expression_intensity_source": r.expression_intensity_source, "expression_intensity_source": r.expression_intensity_source,
"composition": r.composition, "composition": r.composition,
"composition_entry": r.composition_entry,
"source_composition": r.source_composition, "source_composition": r.source_composition,
"composition_prompt": row_camera_policy.composition_prompt(r.composition), "composition_prompt": row_camera_policy.composition_prompt(r.composition),
"composition_config": r.composition_config or {}, "composition_config": r.composition_config or {},
@@ -156,6 +160,13 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
"item_template_metadata": r.item_template_metadata, "item_template_metadata": r.item_template_metadata,
"formatter_hints": r.formatter_hints, "formatter_hints": r.formatter_hints,
"scene_text": r.scene, "scene_text": r.scene,
"scene_entry": r.scene_entry,
"location_theme": (r.location_config or {}).get("theme", ""),
"scene_theme": r.scene_entry.get("theme", "") or (
(r.location_config or {}).get("theme", "")
if (r.location_config or {}).get("apply_mode") == "replace"
else ""
),
"location_config": r.location_config or {}, "location_config": r.location_config or {},
"pose": r.pose, "pose": r.pose,
"seed_config": r.seed_config, "seed_config": r.seed_config,
@@ -168,6 +179,8 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
"position_key": r.position_key, "position_key": r.position_key,
"position_keys": r.position_keys, "position_keys": r.position_keys,
"source_composition": r.source_composition, "source_composition": r.source_composition,
"composition_entry": r.composition_entry,
"composition_theme": (r.composition_config or {}).get("theme", ""),
"pov_character_labels": r.pov_character_labels, "pov_character_labels": r.pov_character_labels,
"pov_prompt_directive": pov_prompt_directive, "pov_prompt_directive": pov_prompt_directive,
"shared_expression": r.shared_expression, "shared_expression": r.shared_expression,
+16
View File
@@ -70,6 +70,18 @@ def apply_contextual_composition(row: dict[str, Any], subject_kind: str) -> dict
return row return row
def scene_camera_profile_metadata(scene_text: Any) -> dict[str, str]:
profile = scene_camera_adapters.scene_camera_profile(scene_text)
if not profile:
return {}
return {
"key": str(profile.get("key") or ""),
"family": str(profile.get("family") or ""),
"layout_label": str(profile.get("layout_label") or ""),
"place": str(profile.get("place") or ""),
}
def camera_scene_directive_for_context( def camera_scene_directive_for_context(
scene_text: Any, scene_text: Any,
composition: Any, composition: Any,
@@ -129,6 +141,10 @@ def apply_camera_config(
pov_labels = row_pov_labels(row, pov_label_resolver) pov_labels = row_pov_labels(row, pov_label_resolver)
subject_kind = row_camera_subject_kind(row) subject_kind = row_camera_subject_kind(row)
row = apply_contextual_composition(row, subject_kind) row = apply_contextual_composition(row, subject_kind)
profile_metadata = scene_camera_profile_metadata(row.get("scene_text") or row.get("source_scene_text") or row.get("scene"))
if profile_metadata:
row["scene_camera_profile"] = profile_metadata
row["scene_camera_profile_key"] = profile_metadata.get("key", "")
scene_directive, parsed = camera_scene_directive_for_context( scene_directive, parsed = camera_scene_directive_for_context(
row.get("scene_text") or row.get("source_scene_text") or row.get("scene"), row.get("scene_text") or row.get("source_scene_text") or row.get("scene"),
row.get("composition") or row.get("source_composition"), row.get("composition") or row.get("source_composition"),
+38 -2
View File
@@ -89,8 +89,31 @@ def _choose_pair(rng: random.Random, items: list[Any]) -> tuple[str, str]:
return _pair_from(_weighted_choice(rng, items)) return _pair_from(_weighted_choice(rng, items))
def _metadata_entry(value: Any, *, slug: str = "", text: str = "") -> dict[str, Any]:
if isinstance(value, dict):
entry = dict(value)
elif isinstance(value, (list, tuple)) and len(value) == 2:
entry = {"slug": str(value[0]), "prompt": str(value[1])}
else:
entry = {"prompt": str(value or "")}
if slug:
entry["slug"] = slug
if text:
if "prompt" in entry:
entry["prompt"] = text
elif "text" in entry:
entry["text"] = text
else:
entry["prompt"] = text
return entry
def _choose_text(rng: random.Random, items: list[Any]) -> str: def _choose_text(rng: random.Random, items: list[Any]) -> str:
item = _weighted_choice(rng, items) item = _weighted_choice(rng, items)
return _text_from_entry(item)
def _text_from_entry(item: Any) -> str:
if isinstance(item, dict): if isinstance(item, dict):
return str( return str(
item.get("template") item.get("template")
@@ -134,13 +157,22 @@ def apply_location_config_to_legacy_row(
else: else:
choices = location_entries choices = location_entries
scene_rng = seed_policy.axis_rng(seed_config, "scene", seed, row_number) scene_rng = seed_policy.axis_rng(seed_config, "scene", seed, row_number)
scene_slug, scene_text = _choose_pair(scene_rng, choices) scene_choice = _weighted_choice(scene_rng, choices)
scene_slug, scene_text = _pair_from(scene_choice)
scene_entry = _metadata_entry(scene_choice, slug=scene_slug, text=scene_text)
old_slug = str(row.get("scene") or "") old_slug = str(row.get("scene") or "")
old_text = legacy_scene_text_for_slug(old_slug) old_text = legacy_scene_text_for_slug(old_slug)
row["source_scene"] = old_slug row["source_scene"] = old_slug
row["source_scene_text"] = old_text row["source_scene_text"] = old_text
row["scene"] = scene_slug row["scene"] = scene_slug
row["scene_text"] = scene_text row["scene_text"] = scene_text
row["scene_entry"] = scene_entry
row["location_theme"] = str(location_config.get("theme") or "")
row["scene_theme"] = scene_entry.get("theme", "") or (
str(location_config.get("theme") or "")
if location_config.get("apply_mode") == "replace"
else ""
)
row["location_config"] = location_config row["location_config"] = location_config
if old_text: if old_text:
row["prompt"] = str(row.get("prompt") or "").replace(f"Scene: {old_text}.", f"Scene: {scene_text}.") row["prompt"] = str(row.get("prompt") or "").replace(f"Scene: {old_text}.", f"Scene: {scene_text}.")
@@ -178,12 +210,16 @@ def apply_composition_config_to_legacy_row(
else: else:
choices = composition_entries choices = composition_entries
composition_rng = seed_policy.axis_rng(seed_config, "composition", seed, row_number) composition_rng = seed_policy.axis_rng(seed_config, "composition", seed, row_number)
new_composition = _choose_text(composition_rng, choices) composition_choice = _weighted_choice(composition_rng, choices)
new_composition = _text_from_entry(composition_choice)
composition_entry = _metadata_entry(composition_choice, text=new_composition)
old_composition = str(row.get("composition") or "") old_composition = str(row.get("composition") or "")
old_prompt_fragment = f"Composition: vertical {old_composition}." old_prompt_fragment = f"Composition: vertical {old_composition}."
new_prompt_fragment = f"Composition: {row_camera.composition_prompt(new_composition)}." new_prompt_fragment = f"Composition: {row_camera.composition_prompt(new_composition)}."
row["source_composition"] = old_composition row["source_composition"] = old_composition
row["composition"] = new_composition row["composition"] = new_composition
row["composition_entry"] = composition_entry
row["composition_theme"] = str(composition_config.get("theme") or "")
row["composition_prompt"] = row_camera.composition_prompt(new_composition) row["composition_prompt"] = row_camera.composition_prompt(new_composition)
row["composition_config"] = composition_config row["composition_config"] = composition_config
if old_composition: if old_composition:
+40 -14
View File
@@ -23,6 +23,7 @@ except ImportError: # Allows local smoke tests from the repository root.
class PromptAxesRoute: class PromptAxesRoute:
scene_slug: str scene_slug: str
scene: str scene: str
scene_entry: dict[str, Any]
pose: str pose: str
expression: str expression: str
shared_expression: str shared_expression: str
@@ -30,11 +31,13 @@ class PromptAxesRoute:
character_expression_text: str character_expression_text: str
source_composition: str source_composition: str
composition: str composition: str
composition_entry: dict[str, Any]
def as_dict(self) -> dict[str, Any]: def as_dict(self) -> dict[str, Any]:
return { return {
"scene_slug": self.scene_slug, "scene_slug": self.scene_slug,
"scene": self.scene, "scene": self.scene,
"scene_entry": dict(self.scene_entry),
"pose": self.pose, "pose": self.pose,
"expression": self.expression, "expression": self.expression,
"shared_expression": self.shared_expression, "shared_expression": self.shared_expression,
@@ -42,9 +45,29 @@ class PromptAxesRoute:
"character_expression_text": self.character_expression_text, "character_expression_text": self.character_expression_text,
"source_composition": self.source_composition, "source_composition": self.source_composition,
"composition": self.composition, "composition": self.composition,
"composition_entry": dict(self.composition_entry),
} }
def _metadata_entry(value: Any, *, slug: str = "", text: str = "") -> dict[str, Any]:
if isinstance(value, dict):
entry = dict(value)
elif isinstance(value, (list, tuple)) and len(value) == 2:
entry = {"slug": str(value[0]), "prompt": str(value[1])}
else:
entry = {"prompt": str(value or "")}
if slug:
entry["slug"] = slug
if text:
if "prompt" in entry:
entry["prompt"] = text
elif "text" in entry:
entry["text"] = text
else:
entry["prompt"] = text
return entry
def resolve_prompt_axes_result( def resolve_prompt_axes_result(
*, *,
category: dict[str, Any], category: dict[str, Any],
@@ -75,14 +98,14 @@ def resolve_prompt_axes_result(
character_slot_map = character_slot_map or {} character_slot_map = character_slot_map or {}
pov_character_labels = pov_character_labels or [] pov_character_labels = pov_character_labels or []
scene_slug, scene = row_item_policy.choose_pair( scene_entries = category_policy.compatible_entries(
scene_rng, row_pool_policy.scene_pool(category, subcategory, item, subject_type, location_config),
category_policy.compatible_entries( women_count,
row_pool_policy.scene_pool(category, subcategory, item, subject_type, location_config), men_count,
women_count,
men_count,
),
) )
scene_choice = row_item_policy.weighted_choice(scene_rng, scene_entries)
scene_slug, scene = row_item_policy.pair_from(scene_choice)
scene_entry = _metadata_entry(scene_choice, slug=scene_slug, text=scene)
pose = str( pose = str(
category_policy.merged_field(category, subcategory, item, "pose", "") category_policy.merged_field(category, subcategory, item, "pose", "")
or context.get("fallback_pose") or context.get("fallback_pose")
@@ -137,21 +160,23 @@ def resolve_prompt_axes_result(
if character_expression_text: if character_expression_text:
expression = character_expression_text expression = character_expression_text
source_composition = row_item_policy.choose_text( composition_entries = category_policy.compatible_entries(
composition_rng, row_pool_policy.composition_pool(category, subcategory, item, subject_type, composition_config),
category_policy.compatible_entries( women_count,
row_pool_policy.composition_pool(category, subcategory, item, subject_type, composition_config), men_count,
women_count,
men_count,
),
) )
composition_choice = row_item_policy.weighted_choice(composition_rng, composition_entries)
source_composition = row_item_policy.item_text(composition_choice)
composition_entry = _metadata_entry(composition_choice, text=source_composition)
if is_pose_category: if is_pose_category:
source_composition = sanitize_hardcore_environment_anchors(source_composition) source_composition = sanitize_hardcore_environment_anchors(source_composition)
composition_entry["prompt"] = source_composition
composition = pov_policy.pov_composition_prompt(source_composition, pov_character_labels) composition = pov_policy.pov_composition_prompt(source_composition, pov_character_labels)
return PromptAxesRoute( return PromptAxesRoute(
scene_slug=scene_slug, scene_slug=scene_slug,
scene=scene, scene=scene,
scene_entry=scene_entry,
pose=pose, pose=pose,
expression=expression, expression=expression,
shared_expression=shared_expression, shared_expression=shared_expression,
@@ -159,6 +184,7 @@ def resolve_prompt_axes_result(
character_expression_text=character_expression_text, character_expression_text=character_expression_text,
source_composition=source_composition, source_composition=source_composition,
composition=composition, composition=composition,
composition_entry=composition_entry,
) )
+68 -4
View File
@@ -611,9 +611,16 @@ def smoke_config_route_location_theme() -> None:
composition = _expect_text("config_route_location_theme.composition", row.get("composition"), 10) composition = _expect_text("config_route_location_theme.composition", row.get("composition"), 10)
camera = _expect_text("config_route_location_theme.camera_directive", row.get("camera_directive"), 20) camera = _expect_text("config_route_location_theme.camera_directive", row.get("camera_directive"), 20)
scene_directive = _expect_text("config_route_location_theme.camera_scene_directive", row.get("camera_scene_directive"), 40) scene_directive = _expect_text("config_route_location_theme.camera_scene_directive", row.get("camera_scene_directive"), 40)
scene_profile = row.get("scene_camera_profile") if isinstance(row.get("scene_camera_profile"), dict) else {}
_expect("library" in scene.lower() or "bookshelves" in scene.lower(), "location theme did not drive scene") _expect("library" in scene.lower() or "bookshelves" in scene.lower(), "location theme did not drive scene")
_expect("books" in composition.lower() or "shelf" in composition.lower() or "library" in composition.lower(), "location theme did not drive composition") _expect("books" in composition.lower() or "shelf" in composition.lower() or "library" in composition.lower(), "location theme did not drive composition")
_expect(row.get("location_theme") == "classical_library", "location theme did not survive into row metadata")
_expect(row.get("scene_theme") == "classical_library", "selected scene theme did not survive into row metadata")
_expect(row.get("composition_theme") == "classical_library", "composition theme did not survive into row metadata")
_expect(row.get("scene_entry", {}).get("theme") == "classical_library", "selected scene entry lost theme metadata")
_expect("Library camera layout" in scene_directive, "location theme did not drive library camera-scene adapter") _expect("Library camera layout" in scene_directive, "location theme did not drive library camera-scene adapter")
_expect(row.get("scene_camera_profile_key") == "classical_library", "row lost scene camera profile key")
_expect(scene_profile.get("family") == "library", "row lost scene camera profile family")
_expect("front-left quarter view" in scene_directive, "library camera-scene adapter missed orbit direction") _expect("front-left quarter view" in scene_directive, "library camera-scene adapter missed orbit direction")
_expect("bag" not in composition.lower() and "shoes" not in composition.lower(), "location theme composition leaked outfit-check props") _expect("bag" not in composition.lower() and "shoes" not in composition.lower(), "location theme composition leaked outfit-check props")
_expect("315-degree front-left quarter view" in camera, "config route did not preserve orbit camera directive") _expect("315-degree front-left quarter view" in camera, "config route did not preserve orbit camera directive")
@@ -964,8 +971,44 @@ def smoke_location_config_policy() -> None:
theme="classical_library", theme="classical_library",
) )
_expect("classical_library" in theme_summary, "Themed location summary lost theme name") _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") themed_location_payload = json.loads(themed_location)
_expect(json.loads(themed_composition).get("composition_entries"), "Themed location did not output compositions") themed_composition_payload = json.loads(themed_composition)
_expect(themed_location_payload.get("scene_entries"), "Themed location did not output locations")
_expect(themed_location_payload.get("theme") == "classical_library", "Themed location config lost theme metadata")
_expect(
all(
not isinstance(entry, dict) or entry.get("theme") == "classical_library"
for entry in themed_location_payload.get("scene_entries") or []
),
"Themed location entries lost theme metadata",
)
_expect(themed_composition_payload.get("composition_entries"), "Themed location did not output compositions")
_expect(themed_composition_payload.get("theme") == "classical_library", "Themed composition config lost theme metadata")
parsed_themed = pb._parse_location_config(themed_location_payload)
_expect(parsed_themed.get("theme") == "classical_library", "Location parser lost theme metadata")
replaced_after_theme = json.loads(
location_config.build_location_pool_json(
enabled=True,
combine_mode="replace",
preset="custom_only",
custom_locations="plain_room: plain room after theme",
location_config=themed_location_payload,
)
)
_expect(replaced_after_theme.get("theme") == "", "Location replace mode should not inherit upstream theme metadata")
replaced_composition_after_theme = json.loads(
location_config.build_composition_pool_json(
enabled=True,
combine_mode="replace",
preset="custom_only",
custom_compositions="plain composition after theme",
composition_config=themed_composition_payload,
)
)
_expect(
replaced_composition_after_theme.get("theme") == "",
"Composition replace mode should not inherit upstream theme metadata",
)
def smoke_row_location_policy() -> None: def smoke_row_location_policy() -> None:
@@ -997,11 +1040,16 @@ def smoke_row_location_policy() -> None:
"Row location policy did not apply forced custom scene text", "Row location policy did not apply forced custom scene text",
) )
_expect(updated.get("source_scene") == "unknown_old_scene", "Row location policy lost source scene slug") _expect(updated.get("source_scene") == "unknown_old_scene", "Row location policy lost source scene slug")
_expect(updated.get("scene_entry", {}).get("slug") == "archive_corner", "Row location policy lost selected scene entry")
_expect( _expect(
"Scene: hidden archive corner with repeated shelves and warm table lamps. Pose:" in updated.get("prompt", ""), "Scene: hidden archive corner with repeated shelves and warm table lamps. Pose:" in updated.get("prompt", ""),
"Row location policy did not rewrite prompt scene", "Row location policy did not rewrite prompt scene",
) )
_expect(updated.get("composition") == "long archive aisle composition", "Row location policy did not apply forced composition") _expect(updated.get("composition") == "long archive aisle composition", "Row location policy did not apply forced composition")
_expect(
updated.get("composition_entry", {}).get("prompt") == "long archive aisle composition",
"Row location policy lost selected composition entry",
)
_expect( _expect(
updated.get("composition_prompt") == "vertical long archive aisle composition", updated.get("composition_prompt") == "vertical long archive aisle composition",
"Row location policy did not compute composition prompt", "Row location policy did not compute composition prompt",
@@ -1239,10 +1287,19 @@ def smoke_row_prompt_axes_policy() -> None:
_expect(route_result.scene_slug == "studio", "Typed prompt axes route lost selected scene slug") _expect(route_result.scene_slug == "studio", "Typed prompt axes route lost selected scene slug")
_expect(route["scene_slug"] == "studio", "Prompt axes route lost selected scene slug") _expect(route["scene_slug"] == "studio", "Prompt axes route lost selected scene slug")
_expect(route["scene"] == "quiet studio with repeatable anchors", "Prompt axes route lost selected scene text") _expect(route["scene"] == "quiet studio with repeatable anchors", "Prompt axes route lost selected scene text")
_expect(route["scene_entry"].get("slug") == "studio", "Prompt axes route lost selected scene entry slug")
_expect(
route["scene_entry"].get("prompt") == "quiet studio with repeatable anchors",
"Prompt axes route lost selected scene entry prompt",
)
_expect(route["pose"] == "standing fallback pose", "Prompt axes route lost selected fallback pose") _expect(route["pose"] == "standing fallback pose", "Prompt axes route lost selected fallback pose")
_expect(route["expression"] == "", "Prompt axes route should omit expression when disabled") _expect(route["expression"] == "", "Prompt axes route should omit expression when disabled")
_expect(route["shared_expression"] == "", "Prompt axes route should omit shared expression when disabled") _expect(route["shared_expression"] == "", "Prompt axes route should omit shared expression when disabled")
_expect(route["source_composition"] == "all participants visible centered frame", "Prompt axes route lost source composition") _expect(route["source_composition"] == "all participants visible centered frame", "Prompt axes route lost source composition")
_expect(
route["composition_entry"].get("prompt") == "all participants visible centered frame",
"Prompt axes route lost selected composition entry",
)
pov_route = row_prompt_axes.resolve_prompt_axes( pov_route = row_prompt_axes.resolve_prompt_axes(
**{**base_kwargs, "expression_disabled": True}, **{**base_kwargs, "expression_disabled": True},
@@ -2348,6 +2405,7 @@ def smoke_row_assembly_policy() -> None:
"negative_prompt": "bad anatomy", "negative_prompt": "bad anatomy",
"scene_slug": "test_room", "scene_slug": "test_room",
"scene": "warm test room", "scene": "warm test room",
"scene_entry": {"slug": "test_room", "prompt": "warm test room", "theme": "fixture_theme"},
"pose": "standing close", "pose": "standing close",
"expression": "focused look", "expression": "focused look",
"shared_expression": "focused look", "shared_expression": "focused look",
@@ -2358,6 +2416,7 @@ def smoke_row_assembly_policy() -> None:
"expression_intensity_source": "disabled", "expression_intensity_source": "disabled",
"composition": "centered frame", "composition": "centered frame",
"source_composition": "centered frame", "source_composition": "centered frame",
"composition_entry": {"prompt": "centered frame"},
"role_graph": "the visible partner stays centered", "role_graph": "the visible partner stays centered",
"source_role_graph": "Man A stays centered", "source_role_graph": "Man A stays centered",
"action_family": "test_action", "action_family": "test_action",
@@ -2369,8 +2428,8 @@ def smoke_row_assembly_policy() -> None:
"cast_descriptor_text": "Woman A: adult woman; Man A: adult man", "cast_descriptor_text": "Woman A: adult woman; Man A: adult man",
"seed_config": {"content_seed": 123}, "seed_config": {"content_seed": 123},
"hardcore_position_config": {"family": "standing"}, "hardcore_position_config": {"family": "standing"},
"location_config": {"location": "test_room"}, "location_config": {"location": "test_room", "theme": "fixture_theme", "apply_mode": "replace"},
"composition_config": {"composition": "centered"}, "composition_config": {"composition": "centered", "theme": "fixture_theme"},
"content_seed_axis": "pose", "content_seed_axis": "pose",
"count_adjustment": count_adjustment, "count_adjustment": count_adjustment,
"applied_profile": {"name": "profile_a"}, "applied_profile": {"name": "profile_a"},
@@ -2390,6 +2449,11 @@ def smoke_row_assembly_policy() -> None:
_expect(row["source"] == "json_category", "Row assembly lost source marker") _expect(row["source"] == "json_category", "Row assembly lost source marker")
_expect(row["figure"] == "balanced cast", "Row assembly lost figure metadata") _expect(row["figure"] == "balanced cast", "Row assembly lost figure metadata")
_expect(row["formatter_hints"] == {"krea": ["test_hint"]}, "Row assembly lost formatter hints") _expect(row["formatter_hints"] == {"krea": ["test_hint"]}, "Row assembly lost formatter hints")
_expect(row["scene_entry"].get("slug") == "test_room", "Row assembly lost selected scene entry")
_expect(row["location_theme"] == "fixture_theme", "Row assembly lost location theme")
_expect(row["scene_theme"] == "fixture_theme", "Row assembly lost selected scene theme")
_expect(row["composition_entry"].get("prompt") == "centered frame", "Row assembly lost selected composition entry")
_expect(row["composition_theme"] == "fixture_theme", "Row assembly lost composition theme")
_expect(row["cast_count_adjustment"] == count_adjustment, "Row assembly lost configured-cast count adjustment") _expect(row["cast_count_adjustment"] == count_adjustment, "Row assembly lost configured-cast count adjustment")
_expect(row["content_seed_axis"] == "pose", "Row assembly lost content seed axis") _expect(row["content_seed_axis"] == "pose", "Row assembly lost content seed axis")
_expect("POV participant: Man A" in row["prompt"], "Row assembly lost POV prompt directive") _expect("POV participant: Man A" in row["prompt"], "Row assembly lost POV prompt directive")