Allow inline scene camera profiles

This commit is contained in:
2026-06-27 13:30:59 +02:00
parent f811c02641
commit 17c6d34784
4 changed files with 146 additions and 0 deletions
+4
View File
@@ -426,6 +426,10 @@ Rows keep the selected `scene_entry`, `location_theme`, `scene_theme`,
from prompt text alone.
When camera-aware profile routing runs, explicit `scene_camera_profile_key` and
theme metadata are used before fallback text matching.
Advanced scene entries may also include an inline `camera_profile` /
`scene_camera_profile` object with `layout_label`, `foreground`, `midground`,
`background`, and optional composition text, so custom location packs can define
their own camera behavior.
`SxCP SDXL Formatter` rewrites prompt builder output or `metadata_json` into
comma-tag SDXL/Pony-style prompts. Connect `metadata_json` when possible so
+4
View File
@@ -656,6 +656,10 @@ Current camera-aware scene adapter:
- Profile resolution is metadata-first: explicit `scene_camera_profile_key`,
selected `scene_entry` profile keys, and theme metadata are preferred before
text matching.
- Selected scene entries can provide inline `camera_profile` or
`scene_camera_profile` objects with `key`, `family`, `layout_label`, `place`,
`foreground`, `midground`, `background`, `detail_label`, and optional
per-subject `composition` text.
- 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
+90
View File
@@ -118,6 +118,17 @@ THEME_PROFILE_KEYS = {
"classical_library": "classical_library",
}
PROFILE_TEXT_FIELDS = (
"key",
"family",
"layout_label",
"place",
"foreground",
"midground",
"background",
"detail_label",
)
MISMATCHED_COMPOSITION_TERMS = (
"outfit-check",
"outfit check",
@@ -129,6 +140,10 @@ MISMATCHED_COMPOSITION_TERMS = (
)
def _clean_text(value: Any) -> str:
return " ".join(str(value or "").strip().split())
def _profile_by_key(value: Any) -> dict[str, Any]:
key = str(value or "").strip()
if not key:
@@ -141,6 +156,62 @@ def _profile_by_key(value: Any) -> dict[str, Any]:
return {}
def _profile_title(value: str) -> str:
text = _clean_text(value).replace("_", " ").replace("-", " ")
if not text:
return "Scene"
return " ".join(part[:1].upper() + part[1:] for part in text.split())
def _default_composition(profile: dict[str, Any]) -> dict[str, str]:
place = _clean_text(profile.get("place")) or "scene"
foreground = _clean_text(profile.get("foreground")) or "foreground anchor"
background = _clean_text(profile.get("background")) or "environment depth"
return {
"woman": f"{place} frame with the woman near {foreground} and {background} behind her",
"man": f"{place} frame with the man near {foreground} and {background} behind him",
"default": f"{place} frame with the subjects near {foreground} and {background} behind them",
}
def normalize_scene_camera_profile(value: Any) -> dict[str, Any]:
if not isinstance(value, dict):
return {}
base = _profile_by_key(value.get("base_profile_key") or value.get("extends"))
merged = dict(base)
for key, raw_value in value.items():
if key in ("base_profile_key", "extends"):
continue
merged[key] = raw_value
has_profile_fields = any(_clean_text(merged.get(key)) for key in ("layout_label", "place", "foreground", "midground", "background"))
if not has_profile_fields:
return {}
key = _clean_text(merged.get("key") or merged.get("slug") or merged.get("name") or base.get("key") or "custom_scene")
place = _clean_text(merged.get("place") or merged.get("name") or key.replace("_", " "))
profile = {field: _clean_text(merged.get(field)) for field in PROFILE_TEXT_FIELDS}
profile["key"] = key
profile["family"] = profile["family"] or "custom"
profile["place"] = place
profile["layout_label"] = profile["layout_label"] or f"{_profile_title(place)} camera layout"
profile["foreground"] = profile["foreground"] or base.get("foreground", "foreground anchor")
profile["midground"] = profile["midground"] or base.get("midground", "midground environment anchors")
profile["background"] = profile["background"] or base.get("background", "background depth")
profile["detail_label"] = profile["detail_label"] or f"{place} details"
composition = merged.get("composition")
if isinstance(composition, dict):
profile["composition"] = {
str(key): _clean_text(text)
for key, text in composition.items()
if _clean_text(text)
}
else:
base_composition = base.get("composition") if isinstance(base.get("composition"), dict) else {}
profile["composition"] = dict(base_composition) if base_composition else _default_composition(profile)
if not profile["composition"]:
profile["composition"] = _default_composition(profile)
return profile
def _scene_entry_text(scene_entry: Any) -> str:
if not isinstance(scene_entry, dict):
return ""
@@ -165,6 +236,19 @@ def _scene_entry_profile_key(scene_entry: Any) -> str:
).strip()
def _scene_entry_profile(scene_entry: Any) -> dict[str, Any]:
if not isinstance(scene_entry, dict):
return {}
for key in ("scene_camera_profile", "camera_profile"):
profile = normalize_scene_camera_profile(scene_entry.get(key))
if profile:
return profile
profile = normalize_scene_camera_profile(scene_entry.get("profile"))
if profile:
return profile
return normalize_scene_camera_profile(scene_entry)
def scene_camera_profile(
scene_text: Any = "",
*,
@@ -172,9 +256,15 @@ def scene_camera_profile(
theme: Any = "",
profile_key: Any = "",
) -> dict[str, Any]:
inline_explicit_profile = normalize_scene_camera_profile(profile_key)
if inline_explicit_profile:
return inline_explicit_profile
explicit_profile = _profile_by_key(profile_key)
if explicit_profile:
return explicit_profile
inline_entry_profile = _scene_entry_profile(scene_entry)
if inline_entry_profile:
return inline_entry_profile
entry_profile = _profile_by_key(_scene_entry_profile_key(scene_entry))
if entry_profile:
return entry_profile
+48
View File
@@ -623,6 +623,54 @@ def smoke_row_camera_policy() -> None:
"Library camera layout" in str(updated_explicit_profile.get("camera_scene_directive", "")),
"explicit scene_camera_profile_key should override text-matched scene profile",
)
inline_profile_row = {
"prompt": "A generated adult prompt. Composition: vertical polished mirror view with bag and shoes visible. Avoid: low quality.",
"caption": "sxcppnl7, generated adult prompt, polished mirror view with bag and shoes visible, illustration",
"scene_text": "private room with soft daylight",
"scene_entry": {
"slug": "greenhouse_room",
"prompt": "private room with soft daylight",
"camera_profile": {
"key": "glass_conservatory",
"family": "greenhouse",
"layout_label": "Glass conservatory camera layout",
"place": "glass conservatory",
"foreground": "plant shelf edge, fern leaves, and iron table corner",
"midground": "glass panes, iron ribs, and potted palms",
"background": "hanging vines, greenhouse windows, and layered plant depth",
"detail_label": "conservatory details",
"composition": {
"woman": "glass conservatory frame with the woman beside fern leaves and greenhouse depth behind her",
"default": "glass conservatory frame with the subjects beside fern leaves and greenhouse depth behind them",
},
},
},
"composition": "polished mirror view with bag and shoes visible",
"subject_type": "woman",
"women_count": 1,
"men_count": 0,
}
updated_inline_profile = row_camera.apply_camera_config(
inline_profile_row,
_orbit_camera(horizontal_angle=45, vertical_angle=30, zoom=5.0),
compact_labels=pb.CAMERA_COMPACT_LABELS,
)
inline_scene = _expect_text(
"row_camera_policy.inline_profile_scene",
updated_inline_profile.get("camera_scene_directive"),
40,
)
inline_composition = _expect_text(
"row_camera_policy.inline_profile_composition",
updated_inline_profile.get("composition"),
20,
)
inline_profile = updated_inline_profile.get("scene_camera_profile") if isinstance(updated_inline_profile.get("scene_camera_profile"), dict) else {}
_expect("Glass conservatory camera layout" in inline_scene, "inline scene camera profile did not drive camera layout")
_expect(updated_inline_profile.get("scene_camera_profile_key") == "glass_conservatory", "inline profile key was not exposed")
_expect(inline_profile.get("family") == "greenhouse", "inline profile family was not exposed")
_expect("glass conservatory" in inline_composition.lower(), "inline profile did not drive composition cleanup")
_expect("bag" not in inline_composition.lower() and "shoes" not in inline_composition.lower(), "inline profile composition leaked unrelated props")
def smoke_config_route_location_theme() -> None: