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
male-POV setups this becomes a first-person spatial description and the
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
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. |
| `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_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. |
| `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`. |
@@ -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_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_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. |
| `composition_config` | Composition config parser | Debug | Active composition pool config, if connected. |
| `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_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. |
| `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. |
@@ -651,6 +655,9 @@ Current camera-aware scene adapter:
- Scene profiles live in `scene_camera_adapters.SCENE_CAMERA_PROFILES`.
- Coworking/business-cafe/office scenes and classical library/book-stack scenes
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
such as `scene_direction_detail`, `scene_distance_detail`, and
`scene_elevation_detail`.
+25 -4
View File
@@ -322,6 +322,7 @@ def build_location_pool_json(
merged_entries = entries
active = bool(enabled) and bool(merged_entries)
theme = str(incoming.get("theme") or "") if combine_mode == "add" and incoming.get("enabled") else ""
summary = (
f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}"
if active
@@ -334,6 +335,7 @@ def build_location_pool_json(
"pool_names": merged_pool_names,
"scene_entries": merged_entries,
"summary": summary,
"theme": theme,
},
ensure_ascii=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]:
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):
raw = dict(location_config)
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()],
"scene_entries": entries,
"summary": str(raw.get("summary") or ""),
"theme": str(raw.get("theme") or ""),
}
@@ -432,6 +435,7 @@ def build_composition_pool_json(
merged_entries = entries
active = bool(enabled) and bool(merged_entries)
theme = str(incoming.get("theme") or "") if combine_mode == "add" and incoming.get("enabled") else ""
summary = (
f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}"
if active
@@ -444,6 +448,7 @@ def build_composition_pool_json(
"pool_names": merged_pool_names,
"composition_entries": merged_entries,
"summary": summary,
"theme": theme,
},
ensure_ascii=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]:
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):
raw = dict(composition_config)
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()],
"composition_entries": entries,
"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,
composition_config=composition_config or "",
)
location_summary = json.loads(resolved_location_config).get("summary", "")
composition_summary = json.loads(resolved_composition_config).get("summary", "")
location_payload = json.loads(resolved_location_config)
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}"
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 = prompt_axes.scene
scene_entry = dict(prompt_axes.scene_entry)
pose = prompt_axes.pose
expression = prompt_axes.expression
shared_expression = prompt_axes.shared_expression
@@ -2387,6 +2388,7 @@ def _build_custom_row(
character_expression_text = prompt_axes.character_expression_text
source_composition = prompt_axes.source_composition
composition = prompt_axes.composition
composition_entry = dict(prompt_axes.composition_entry)
action_route = _action_position_route(
is_pose_category=is_pose_category,
subcategory=subcategory,
@@ -2424,6 +2426,7 @@ def _build_custom_row(
negative_prompt=text_fields.negative_prompt,
scene_slug=scene_slug,
scene=scene,
scene_entry=scene_entry,
pose=pose,
expression=expression,
shared_expression=shared_expression,
@@ -2434,6 +2437,7 @@ def _build_custom_row(
expression_intensity_source=expression_intensity_source,
composition=composition,
source_composition=source_composition,
composition_entry=composition_entry,
role_graph=role_graph,
source_role_graph=source_role_graph,
action_family=action_family,
+13
View File
@@ -37,6 +37,7 @@ class CustomRowAssemblyRequest:
negative_prompt: str
scene_slug: str
scene: str
scene_entry: dict[str, Any]
pose: str
expression: str
shared_expression: str
@@ -47,6 +48,7 @@ class CustomRowAssemblyRequest:
expression_intensity_source: str
composition: str
source_composition: str
composition_entry: dict[str, Any]
role_graph: str
source_role_graph: str
action_family: str
@@ -85,6 +87,7 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
"style": r.style,
"scene": r.scene,
"scene_slug": r.scene_slug,
"scene_entry": r.scene_entry,
"pose": r.pose,
"expression": r.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_source": r.expression_intensity_source,
"composition": r.composition,
"composition_entry": r.composition_entry,
"source_composition": r.source_composition,
"composition_prompt": row_camera_policy.composition_prompt(r.composition),
"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,
"formatter_hints": r.formatter_hints,
"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 {},
"pose": r.pose,
"seed_config": r.seed_config,
@@ -168,6 +179,8 @@ def assemble_custom_row(request: CustomRowAssemblyRequest) -> dict[str, Any]:
"position_key": r.position_key,
"position_keys": r.position_keys,
"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_prompt_directive": pov_prompt_directive,
"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
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(
scene_text: Any,
composition: Any,
@@ -129,6 +141,10 @@ def apply_camera_config(
pov_labels = row_pov_labels(row, pov_label_resolver)
subject_kind = row_camera_subject_kind(row)
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(
row.get("scene_text") or row.get("source_scene_text") or row.get("scene"),
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))
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:
item = _weighted_choice(rng, items)
return _text_from_entry(item)
def _text_from_entry(item: Any) -> str:
if isinstance(item, dict):
return str(
item.get("template")
@@ -134,13 +157,22 @@ def apply_location_config_to_legacy_row(
else:
choices = location_entries
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_text = legacy_scene_text_for_slug(old_slug)
row["source_scene"] = old_slug
row["source_scene_text"] = old_text
row["scene"] = scene_slug
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
if old_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:
choices = composition_entries
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_prompt_fragment = f"Composition: vertical {old_composition}."
new_prompt_fragment = f"Composition: {row_camera.composition_prompt(new_composition)}."
row["source_composition"] = old_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_config"] = composition_config
if old_composition:
+34 -8
View File
@@ -23,6 +23,7 @@ except ImportError: # Allows local smoke tests from the repository root.
class PromptAxesRoute:
scene_slug: str
scene: str
scene_entry: dict[str, Any]
pose: str
expression: str
shared_expression: str
@@ -30,11 +31,13 @@ class PromptAxesRoute:
character_expression_text: str
source_composition: str
composition: str
composition_entry: dict[str, Any]
def as_dict(self) -> dict[str, Any]:
return {
"scene_slug": self.scene_slug,
"scene": self.scene,
"scene_entry": dict(self.scene_entry),
"pose": self.pose,
"expression": self.expression,
"shared_expression": self.shared_expression,
@@ -42,9 +45,29 @@ class PromptAxesRoute:
"character_expression_text": self.character_expression_text,
"source_composition": self.source_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(
*,
category: dict[str, Any],
@@ -75,14 +98,14 @@ def resolve_prompt_axes_result(
character_slot_map = character_slot_map or {}
pov_character_labels = pov_character_labels or []
scene_slug, scene = row_item_policy.choose_pair(
scene_rng,
category_policy.compatible_entries(
scene_entries = category_policy.compatible_entries(
row_pool_policy.scene_pool(category, subcategory, item, subject_type, location_config),
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(
category_policy.merged_field(category, subcategory, item, "pose", "")
or context.get("fallback_pose")
@@ -137,21 +160,23 @@ def resolve_prompt_axes_result(
if character_expression_text:
expression = character_expression_text
source_composition = row_item_policy.choose_text(
composition_rng,
category_policy.compatible_entries(
composition_entries = category_policy.compatible_entries(
row_pool_policy.composition_pool(category, subcategory, item, subject_type, composition_config),
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:
source_composition = sanitize_hardcore_environment_anchors(source_composition)
composition_entry["prompt"] = source_composition
composition = pov_policy.pov_composition_prompt(source_composition, pov_character_labels)
return PromptAxesRoute(
scene_slug=scene_slug,
scene=scene,
scene_entry=scene_entry,
pose=pose,
expression=expression,
shared_expression=shared_expression,
@@ -159,6 +184,7 @@ def resolve_prompt_axes_result(
character_expression_text=character_expression_text,
source_composition=source_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)
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_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("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(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("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")
@@ -964,8 +971,44 @@ def smoke_location_config_policy() -> None:
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")
themed_location_payload = json.loads(themed_location)
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:
@@ -997,11 +1040,16 @@ def smoke_row_location_policy() -> None:
"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("scene_entry", {}).get("slug") == "archive_corner", "Row location policy lost selected scene entry")
_expect(
"Scene: hidden archive corner with repeated shelves and warm table lamps. Pose:" in updated.get("prompt", ""),
"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_entry", {}).get("prompt") == "long archive aisle composition",
"Row location policy lost selected composition entry",
)
_expect(
updated.get("composition_prompt") == "vertical long archive aisle composition",
"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["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_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["expression"] == "", "Prompt axes route should omit 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["composition_entry"].get("prompt") == "all participants visible centered frame",
"Prompt axes route lost selected composition entry",
)
pov_route = row_prompt_axes.resolve_prompt_axes(
**{**base_kwargs, "expression_disabled": True},
@@ -2348,6 +2405,7 @@ def smoke_row_assembly_policy() -> None:
"negative_prompt": "bad anatomy",
"scene_slug": "test_room",
"scene": "warm test room",
"scene_entry": {"slug": "test_room", "prompt": "warm test room", "theme": "fixture_theme"},
"pose": "standing close",
"expression": "focused look",
"shared_expression": "focused look",
@@ -2358,6 +2416,7 @@ def smoke_row_assembly_policy() -> None:
"expression_intensity_source": "disabled",
"composition": "centered frame",
"source_composition": "centered frame",
"composition_entry": {"prompt": "centered frame"},
"role_graph": "the visible partner stays centered",
"source_role_graph": "Man A stays centered",
"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",
"seed_config": {"content_seed": 123},
"hardcore_position_config": {"family": "standing"},
"location_config": {"location": "test_room"},
"composition_config": {"composition": "centered"},
"location_config": {"location": "test_room", "theme": "fixture_theme", "apply_mode": "replace"},
"composition_config": {"composition": "centered", "theme": "fixture_theme"},
"content_seed_axis": "pose",
"count_adjustment": count_adjustment,
"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["figure"] == "balanced cast", "Row assembly lost figure metadata")
_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["content_seed_axis"] == "pose", "Row assembly lost content seed axis")
_expect("POV participant: Man A" in row["prompt"], "Row assembly lost POV prompt directive")