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.
|
from prompt text alone.
|
||||||
When camera-aware profile routing runs, explicit `scene_camera_profile_key` and
|
When camera-aware profile routing runs, explicit `scene_camera_profile_key` and
|
||||||
theme metadata are used before fallback text matching.
|
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
|
`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
|
||||||
|
|||||||
@@ -656,6 +656,10 @@ Current camera-aware scene adapter:
|
|||||||
- Profile resolution is metadata-first: explicit `scene_camera_profile_key`,
|
- Profile resolution is metadata-first: explicit `scene_camera_profile_key`,
|
||||||
selected `scene_entry` profile keys, and theme metadata are preferred before
|
selected `scene_entry` profile keys, and theme metadata are preferred before
|
||||||
text matching.
|
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
|
- 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
|
- Location themes preserve `theme` on configs and selected scene entries, and
|
||||||
|
|||||||
@@ -118,6 +118,17 @@ THEME_PROFILE_KEYS = {
|
|||||||
"classical_library": "classical_library",
|
"classical_library": "classical_library",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PROFILE_TEXT_FIELDS = (
|
||||||
|
"key",
|
||||||
|
"family",
|
||||||
|
"layout_label",
|
||||||
|
"place",
|
||||||
|
"foreground",
|
||||||
|
"midground",
|
||||||
|
"background",
|
||||||
|
"detail_label",
|
||||||
|
)
|
||||||
|
|
||||||
MISMATCHED_COMPOSITION_TERMS = (
|
MISMATCHED_COMPOSITION_TERMS = (
|
||||||
"outfit-check",
|
"outfit-check",
|
||||||
"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]:
|
def _profile_by_key(value: Any) -> dict[str, Any]:
|
||||||
key = str(value or "").strip()
|
key = str(value or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
@@ -141,6 +156,62 @@ def _profile_by_key(value: Any) -> dict[str, Any]:
|
|||||||
return {}
|
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:
|
def _scene_entry_text(scene_entry: Any) -> str:
|
||||||
if not isinstance(scene_entry, dict):
|
if not isinstance(scene_entry, dict):
|
||||||
return ""
|
return ""
|
||||||
@@ -165,6 +236,19 @@ def _scene_entry_profile_key(scene_entry: Any) -> str:
|
|||||||
).strip()
|
).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(
|
def scene_camera_profile(
|
||||||
scene_text: Any = "",
|
scene_text: Any = "",
|
||||||
*,
|
*,
|
||||||
@@ -172,9 +256,15 @@ def scene_camera_profile(
|
|||||||
theme: Any = "",
|
theme: Any = "",
|
||||||
profile_key: Any = "",
|
profile_key: Any = "",
|
||||||
) -> dict[str, 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)
|
explicit_profile = _profile_by_key(profile_key)
|
||||||
if explicit_profile:
|
if explicit_profile:
|
||||||
return 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))
|
entry_profile = _profile_by_key(_scene_entry_profile_key(scene_entry))
|
||||||
if entry_profile:
|
if entry_profile:
|
||||||
return 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", "")),
|
"Library camera layout" in str(updated_explicit_profile.get("camera_scene_directive", "")),
|
||||||
"explicit scene_camera_profile_key should override text-matched scene profile",
|
"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:
|
def smoke_config_route_location_theme() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user