Allow inline scene camera profiles
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user