Add camera orbit control
This commit is contained in:
@@ -9,6 +9,7 @@ The node is registered as:
|
||||
- `prompt_builder / SxCP Seed Control`
|
||||
- `prompt_builder / SxCP Seed Locker`
|
||||
- `prompt_builder / SxCP Camera Control`
|
||||
- `prompt_builder / SxCP Camera Orbit Control`
|
||||
- `prompt_builder / SxCP Category Preset`
|
||||
- `prompt_builder / SxCP Cast Control`
|
||||
- `prompt_builder / SxCP Generation Profile`
|
||||
@@ -54,8 +55,8 @@ node. For cleaner workflows, use the split nodes:
|
||||
The practical compact workflow is:
|
||||
|
||||
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
||||
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control`,
|
||||
`Woman Slot` / `Man Slot`, and `Character Profile`
|
||||
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control` or
|
||||
`Camera Orbit Control`, `Woman Slot` / `Man Slot`, and `Character Profile`
|
||||
into `Prompt Builder From Configs`.
|
||||
|
||||
An importable default workflow is included at
|
||||
@@ -178,9 +179,10 @@ you like, choose one `reroll_axis`, and connect its `seed_config`. All other
|
||||
axes stay frozen to `base_seed`; the rerolled axis follows `reroll_seed`, or the
|
||||
main prompt seed when `reroll_seed=-1`.
|
||||
|
||||
`SxCP Camera Control` outputs `camera_config`, which can be connected to the
|
||||
prompt builder or the Insta/OF pair node. It makes camera/framing first-class
|
||||
instead of relying on a weak phrase inside the prompt.
|
||||
`SxCP Camera Control` and `SxCP Camera Orbit Control` output `camera_config`,
|
||||
which can be connected to the prompt builder or the Insta/OF pair node. They
|
||||
make camera/framing first-class instead of relying on a weak phrase inside the
|
||||
prompt.
|
||||
|
||||
Camera controls:
|
||||
|
||||
@@ -201,6 +203,24 @@ Camera controls:
|
||||
- `camera_detail`: `off` emits no camera sentence, `compact` emits one short
|
||||
camera sentence, and `full` emits the full detailed camera constraint.
|
||||
|
||||
`SxCP Camera Orbit Control` is the numeric/directable version inspired by
|
||||
multi-angle camera nodes. It maps `horizontal_angle`, `vertical_angle`, and
|
||||
`zoom` into a stable prompt such as `135-degree back-right quarter view,
|
||||
elevated shot, medium shot`. Use it when the model needs an exact front, side,
|
||||
back-quarter, low, high, or overhead style camera anchor. Its first output is
|
||||
the same `camera_config` type, so it can replace `SxCP Camera Control` anywhere.
|
||||
|
||||
Orbit controls:
|
||||
|
||||
- `horizontal_angle`: `0` front, `90` right side, `180` back, `270` left side,
|
||||
with quarter views between them.
|
||||
- `vertical_angle`: negative values are low-angle, `0` is eye-level, positive
|
||||
values move toward elevated/high-angle.
|
||||
- `zoom`: `0-2` wide, `2-6` medium, `6-10` close unless `framing` overrides it.
|
||||
- `subject_focus`: optionally centers face, torso, hips, full body, main action,
|
||||
contact points, or the environment.
|
||||
- `include_degrees`: keeps the numeric angle in the emitted camera phrase.
|
||||
|
||||
`SxCP Caption Naturalizer` rewrites tag-like captions or labeled prompts into
|
||||
more natural language. Connect the prompt builder's `metadata_json` output to
|
||||
`source_text` for the cleanest result. You can also connect `caption` or
|
||||
@@ -306,8 +326,8 @@ pair node. Defaults are set so the softcore prompt is solo while the hardcore
|
||||
prompt can include partners. Softcore defaults to handheld selfie framing;
|
||||
hardcore defaults to `from_camera_config`, which emits no camera sentence unless
|
||||
a camera config is connected or you select an explicit hardcore camera mode.
|
||||
For stronger camera control, connect `SxCP Camera Control` to the pair node's
|
||||
optional `camera_config` input.
|
||||
For stronger camera control, connect `SxCP Camera Control` or
|
||||
`SxCP Camera Orbit Control` to the pair node's optional `camera_config` input.
|
||||
|
||||
Options:
|
||||
|
||||
@@ -342,11 +362,12 @@ Options:
|
||||
references the softcore outfit, uses it displaced/removed, or makes Woman A
|
||||
explicitly nude. It is a fallback for Woman A; `hardcore_clothing` on
|
||||
`SxCP Woman Slot` or `SxCP Man Slot` takes priority for that character.
|
||||
- `softcore_camera_mode`: base camera mode for the softcore output.
|
||||
- `softcore_camera_mode`: `from_camera_config` or a base camera mode for the
|
||||
softcore output. The default is still `handheld_selfie`.
|
||||
- `hardcore_camera_mode`: `from_camera_config`, `same_as_softcore`, or a
|
||||
separate base camera mode for the hardcore output. `from_camera_config` is
|
||||
neutral with no connected camera config, and uses `SxCP Camera Control` when
|
||||
one is connected.
|
||||
neutral with no connected camera config, and uses `SxCP Camera Control` or
|
||||
`SxCP Camera Orbit Control` when one is connected.
|
||||
- `camera_detail`: `off`, `compact`, or `full` for the pair prompt camera text.
|
||||
- `hardcore_detail_density`: `compact` keeps the Krea hardcore rewrite mostly
|
||||
to the position/action sentence, `balanced` keeps one useful non-duplicated
|
||||
@@ -376,9 +397,10 @@ The node keeps the original generator controls:
|
||||
- `figure`: `curvy`, `balanced`, `bombshell`.
|
||||
- In split workflows, use `SxCP Advanced Filters` checkboxes instead of negative
|
||||
toggles. Black/African and plus-size are positive include choices there.
|
||||
- Optional `camera_config`: connect `SxCP Camera Control` to force selfie,
|
||||
phone, lens, angle, distance, crop, and camera-priority behavior. This applies
|
||||
to custom categories too, including `Hardcore sexual poses`.
|
||||
- Optional `camera_config`: connect `SxCP Camera Control` or
|
||||
`SxCP Camera Orbit Control` to force selfie, phone, lens, angle, numeric
|
||||
orbit, crop, and camera-priority behavior. This applies to custom categories
|
||||
too, including `Hardcore sexual poses`.
|
||||
|
||||
`auto_weighted` uses the original batch mix: mostly women, then men, couples, and
|
||||
group/layout rows. Direct categories generate only that selected category.
|
||||
|
||||
+71
-1
@@ -6,6 +6,7 @@ import random
|
||||
try:
|
||||
from .prompt_builder import (
|
||||
build_camera_config_json,
|
||||
build_camera_orbit_config_json,
|
||||
build_cast_config_json,
|
||||
build_category_config_json,
|
||||
build_character_slot_json,
|
||||
@@ -23,6 +24,8 @@ try:
|
||||
camera_distance_choices,
|
||||
camera_lens_choices,
|
||||
camera_mode_choices,
|
||||
camera_orbit_focus_choices,
|
||||
camera_orbit_framing_choices,
|
||||
camera_orientation_choices,
|
||||
camera_phone_choices,
|
||||
camera_priority_choices,
|
||||
@@ -52,6 +55,7 @@ try:
|
||||
except ImportError:
|
||||
from prompt_builder import (
|
||||
build_camera_config_json,
|
||||
build_camera_orbit_config_json,
|
||||
build_cast_config_json,
|
||||
build_category_config_json,
|
||||
build_character_slot_json,
|
||||
@@ -69,6 +73,8 @@ except ImportError:
|
||||
camera_distance_choices,
|
||||
camera_lens_choices,
|
||||
camera_mode_choices,
|
||||
camera_orbit_focus_choices,
|
||||
camera_orbit_framing_choices,
|
||||
camera_orientation_choices,
|
||||
camera_phone_choices,
|
||||
camera_priority_choices,
|
||||
@@ -373,6 +379,68 @@ class SxCPCameraControl:
|
||||
)
|
||||
|
||||
|
||||
class SxCPCameraOrbitControl:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"enabled": ("BOOLEAN", {"default": True}),
|
||||
"camera_mode": (camera_mode_choices(), {"default": "standard"}),
|
||||
"horizontal_angle": ("INT", {"default": 0, "min": 0, "max": 359, "step": 1}),
|
||||
"vertical_angle": ("INT", {"default": 0, "min": -90, "max": 90, "step": 1}),
|
||||
"zoom": ("FLOAT", {"default": 5.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
||||
"framing": (camera_orbit_framing_choices(), {"default": "from_zoom"}),
|
||||
"subject_focus": (camera_orbit_focus_choices(), {"default": "auto"}),
|
||||
"lens": (camera_lens_choices(), {"default": "auto"}),
|
||||
"orientation": (camera_orientation_choices(), {"default": "auto"}),
|
||||
"phone_visibility": (camera_phone_choices(), {"default": "auto"}),
|
||||
"priority": (camera_priority_choices(), {"default": "locked"}),
|
||||
"camera_detail": (camera_detail_choices(), {"default": "compact"}),
|
||||
"include_degrees": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("camera_config", "camera_prompt", "camera_info_json")
|
||||
FUNCTION = "build"
|
||||
CATEGORY = "prompt_builder"
|
||||
|
||||
def build(
|
||||
self,
|
||||
enabled,
|
||||
camera_mode,
|
||||
horizontal_angle,
|
||||
vertical_angle,
|
||||
zoom,
|
||||
framing,
|
||||
subject_focus,
|
||||
lens,
|
||||
orientation,
|
||||
phone_visibility,
|
||||
priority,
|
||||
camera_detail,
|
||||
include_degrees,
|
||||
):
|
||||
config = build_camera_orbit_config_json(
|
||||
enabled=enabled,
|
||||
camera_mode=camera_mode,
|
||||
horizontal_angle=horizontal_angle,
|
||||
vertical_angle=vertical_angle,
|
||||
zoom=zoom,
|
||||
framing=framing,
|
||||
subject_focus=subject_focus,
|
||||
lens=lens,
|
||||
orientation=orientation,
|
||||
phone_visibility=phone_visibility,
|
||||
priority=priority,
|
||||
camera_detail=camera_detail,
|
||||
include_degrees=include_degrees,
|
||||
)
|
||||
parsed = json.loads(config)
|
||||
camera_prompt = parsed.get("custom_camera_prompt", "")
|
||||
return config, camera_prompt, json.dumps(parsed, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
class SxCPCategoryPreset:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -1083,7 +1151,7 @@ class SxCPInstaOFOptions:
|
||||
"platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}),
|
||||
"continuity": (["same_creator_same_room", "same_creator_new_scene"], {"default": "same_creator_same_room"}),
|
||||
"hardcore_clothing_continuity": (["none", "same_outfit", "partially_removed", "implied_nude", "explicit_nude"], {"default": "partially_removed"}),
|
||||
"softcore_camera_mode": (camera_mode_choices(), {"default": "handheld_selfie"}),
|
||||
"softcore_camera_mode": (["from_camera_config"] + camera_mode_choices(), {"default": "handheld_selfie"}),
|
||||
"hardcore_camera_mode": (["from_camera_config", "same_as_softcore"] + camera_mode_choices(), {"default": "from_camera_config"}),
|
||||
"camera_detail": (camera_detail_choices(), {"default": "compact"}),
|
||||
"hardcore_detail_density": (hardcore_detail_density_choices(), {"default": "balanced"}),
|
||||
@@ -1233,6 +1301,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"SxCPSeedControl": SxCPSeedControl,
|
||||
"SxCPSeedLocker": SxCPSeedLocker,
|
||||
"SxCPCameraControl": SxCPCameraControl,
|
||||
"SxCPCameraOrbitControl": SxCPCameraOrbitControl,
|
||||
"SxCPCategoryPreset": SxCPCategoryPreset,
|
||||
"SxCPCastControl": SxCPCastControl,
|
||||
"SxCPGenerationProfile": SxCPGenerationProfile,
|
||||
@@ -1254,6 +1323,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SxCPSeedControl": "SxCP Seed Control",
|
||||
"SxCPSeedLocker": "SxCP Seed Locker",
|
||||
"SxCPCameraControl": "SxCP Camera Control",
|
||||
"SxCPCameraOrbitControl": "SxCP Camera Orbit Control",
|
||||
"SxCPCategoryPreset": "SxCP Category Preset",
|
||||
"SxCPCastControl": "SxCP Cast Control",
|
||||
"SxCPGenerationProfile": "SxCP Generation Profile",
|
||||
|
||||
@@ -1320,6 +1320,11 @@ def _camera_phrase(row: dict[str, Any]) -> str:
|
||||
detail = _clean(config.get("camera_detail"))
|
||||
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
||||
return ""
|
||||
custom = _clean(config.get("custom_camera_prompt"))
|
||||
if custom:
|
||||
base = _clean(config.get("camera_mode")).replace("_", " ")
|
||||
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
||||
return "Camera: " + ", ".join(pieces)
|
||||
mode = _clean(config.get("camera_mode")).replace("_", " ")
|
||||
shot = _clean(config.get("shot_size")).replace("_", " ")
|
||||
angle = _clean(config.get("angle")).replace("_", " ")
|
||||
@@ -1335,6 +1340,11 @@ def _camera_phrase_from_config(config: Any) -> str:
|
||||
detail = _clean(config.get("camera_detail"))
|
||||
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
||||
return ""
|
||||
custom = _clean(config.get("custom_camera_prompt"))
|
||||
if custom:
|
||||
base = _clean(config.get("camera_mode")).replace("_", " ")
|
||||
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
||||
return "Camera: " + ", ".join(pieces)
|
||||
values = [
|
||||
_clean(config.get("camera_mode")).replace("_", " "),
|
||||
_clean(config.get("shot_size")).replace("_", " "),
|
||||
|
||||
+208
-8
@@ -180,6 +180,25 @@ CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "defau
|
||||
|
||||
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
||||
CAMERA_ORBIT_FRAMING_CHOICES = [
|
||||
"from_zoom",
|
||||
"wide",
|
||||
"medium",
|
||||
"full_body",
|
||||
"three_quarter",
|
||||
"close_up",
|
||||
"extreme_close_up",
|
||||
]
|
||||
CAMERA_ORBIT_FOCUS_CHOICES = [
|
||||
"auto",
|
||||
"face",
|
||||
"torso",
|
||||
"hips",
|
||||
"full_body",
|
||||
"action",
|
||||
"contact_points",
|
||||
"environment",
|
||||
]
|
||||
|
||||
GENERIC_POSITIVE_SUFFIX = (
|
||||
"Use crisp clean comic linework, detailed hatching, soft blended shading, "
|
||||
@@ -1450,6 +1469,14 @@ def hardcore_detail_density_choices() -> list[str]:
|
||||
return list(HARDCORE_DETAIL_DENSITY_CHOICES)
|
||||
|
||||
|
||||
def camera_orbit_framing_choices() -> list[str]:
|
||||
return list(CAMERA_ORBIT_FRAMING_CHOICES)
|
||||
|
||||
|
||||
def camera_orbit_focus_choices() -> list[str]:
|
||||
return list(CAMERA_ORBIT_FOCUS_CHOICES)
|
||||
|
||||
|
||||
def camera_shot_choices() -> list[str]:
|
||||
return list(CAMERA_SHOT_PROMPTS)
|
||||
|
||||
@@ -1506,12 +1533,145 @@ def build_camera_config_json(
|
||||
)
|
||||
|
||||
|
||||
def _camera_orbit_direction(horizontal_angle: Any) -> str:
|
||||
h_angle = int(float(horizontal_angle or 0)) % 360
|
||||
if h_angle < 22.5 or h_angle >= 337.5:
|
||||
return "front view"
|
||||
if h_angle < 67.5:
|
||||
return "front-right quarter view"
|
||||
if h_angle < 112.5:
|
||||
return "right side view"
|
||||
if h_angle < 157.5:
|
||||
return "back-right quarter view"
|
||||
if h_angle < 202.5:
|
||||
return "back view"
|
||||
if h_angle < 247.5:
|
||||
return "back-left quarter view"
|
||||
if h_angle < 292.5:
|
||||
return "left side view"
|
||||
return "front-left quarter view"
|
||||
|
||||
|
||||
def _camera_orbit_elevation(vertical_angle: Any) -> str:
|
||||
vertical = int(float(vertical_angle or 0))
|
||||
if vertical < -15:
|
||||
return "low-angle shot"
|
||||
if vertical < 15:
|
||||
return "eye-level shot"
|
||||
if vertical < 45:
|
||||
return "elevated shot"
|
||||
return "high-angle shot"
|
||||
|
||||
|
||||
def _camera_orbit_distance(zoom: Any, framing: str = "from_zoom") -> str:
|
||||
framing = framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom"
|
||||
framing_labels = {
|
||||
"wide": "wide shot",
|
||||
"medium": "medium shot",
|
||||
"full_body": "full-body shot",
|
||||
"three_quarter": "three-quarter body shot",
|
||||
"close_up": "close-up",
|
||||
"extreme_close_up": "extreme close-up",
|
||||
}
|
||||
if framing != "from_zoom":
|
||||
return framing_labels[framing]
|
||||
zoom_value = float(zoom or 0.0)
|
||||
if zoom_value < 2:
|
||||
return "wide shot"
|
||||
if zoom_value < 6:
|
||||
return "medium shot"
|
||||
return "close-up"
|
||||
|
||||
|
||||
def _camera_orbit_focus(subject_focus: str) -> str:
|
||||
return {
|
||||
"face": "face and expression centered",
|
||||
"torso": "torso and hands centered",
|
||||
"hips": "hips and lower body centered",
|
||||
"full_body": "full body centered",
|
||||
"action": "main action centered",
|
||||
"contact_points": "body contact points centered",
|
||||
"environment": "subject and room both readable",
|
||||
}.get(str(subject_focus or "auto"), "")
|
||||
|
||||
|
||||
def _camera_orbit_prompt(
|
||||
horizontal_angle: Any,
|
||||
vertical_angle: Any,
|
||||
zoom: Any,
|
||||
framing: str = "from_zoom",
|
||||
subject_focus: str = "auto",
|
||||
include_degrees: bool = True,
|
||||
) -> tuple[str, dict[str, Any]]:
|
||||
azimuth = max(0, min(359, int(float(horizontal_angle or 0))))
|
||||
elevation = max(-90, min(90, int(float(vertical_angle or 0))))
|
||||
zoom_value = max(0.0, min(10.0, float(zoom or 0.0)))
|
||||
direction = _camera_orbit_direction(azimuth)
|
||||
elevation_label = _camera_orbit_elevation(elevation)
|
||||
distance_label = _camera_orbit_distance(zoom_value, framing)
|
||||
focus_label = _camera_orbit_focus(subject_focus)
|
||||
pieces = [direction, elevation_label, distance_label, focus_label]
|
||||
prompt = ", ".join(piece for piece in pieces if piece)
|
||||
if include_degrees:
|
||||
prompt = f"{azimuth}-degree {prompt}"
|
||||
return prompt, {
|
||||
"orbit_azimuth": azimuth,
|
||||
"orbit_elevation": elevation,
|
||||
"orbit_zoom": zoom_value,
|
||||
"orbit_direction": direction,
|
||||
"orbit_elevation_label": elevation_label,
|
||||
"orbit_distance_label": distance_label,
|
||||
"orbit_framing": framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom",
|
||||
"orbit_focus": subject_focus if subject_focus in CAMERA_ORBIT_FOCUS_CHOICES else "auto",
|
||||
}
|
||||
|
||||
|
||||
def build_camera_orbit_config_json(
|
||||
enabled: bool = True,
|
||||
camera_mode: str = "standard",
|
||||
horizontal_angle: int = 0,
|
||||
vertical_angle: int = 0,
|
||||
zoom: float = 5.0,
|
||||
framing: str = "from_zoom",
|
||||
subject_focus: str = "auto",
|
||||
lens: str = "auto",
|
||||
orientation: str = "auto",
|
||||
phone_visibility: str = "auto",
|
||||
priority: str = "locked",
|
||||
camera_detail: str = "compact",
|
||||
include_degrees: bool = True,
|
||||
) -> str:
|
||||
orbit_prompt, orbit_metadata = _camera_orbit_prompt(
|
||||
horizontal_angle,
|
||||
vertical_angle,
|
||||
zoom,
|
||||
framing=framing,
|
||||
subject_focus=subject_focus,
|
||||
include_degrees=include_degrees,
|
||||
)
|
||||
config = {
|
||||
"camera_mode": "disabled" if _is_false(enabled) else _choice(camera_mode, CAMERA_MODE_PROMPTS, "standard"),
|
||||
"shot_size": "auto",
|
||||
"angle": "auto",
|
||||
"lens": _choice(lens, CAMERA_LENS_PROMPTS, "auto"),
|
||||
"distance": "auto",
|
||||
"orientation": _choice(orientation, CAMERA_ORIENTATION_PROMPTS, "auto"),
|
||||
"phone_visibility": _choice(phone_visibility, CAMERA_PHONE_PROMPTS, "auto"),
|
||||
"priority": _choice(priority, CAMERA_PRIORITY_PROMPTS, "locked"),
|
||||
"camera_detail": camera_detail if camera_detail in CAMERA_DETAIL_CHOICES else "compact",
|
||||
"camera_source": "orbit",
|
||||
"custom_camera_prompt": orbit_prompt if not _is_false(enabled) else "",
|
||||
**orbit_metadata,
|
||||
}
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
|
||||
value = str(value or default)
|
||||
return value if value in choices else default
|
||||
|
||||
|
||||
def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, str]:
|
||||
def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
defaults = {
|
||||
"camera_mode": "standard",
|
||||
"shot_size": "auto",
|
||||
@@ -1535,7 +1695,9 @@ def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("camera_config must be a JSON object")
|
||||
parsed = {**defaults, **raw}
|
||||
return {
|
||||
custom_camera_prompt = _clean_prompt_punctuation(parsed.get("custom_camera_prompt", "")).rstrip(".")
|
||||
camera_source = str(parsed.get("camera_source") or "")
|
||||
normalized = {
|
||||
"camera_mode": _choice(parsed.get("camera_mode"), CAMERA_MODE_PROMPTS, defaults["camera_mode"]),
|
||||
"shot_size": _choice(parsed.get("shot_size"), CAMERA_SHOT_PROMPTS, defaults["shot_size"]),
|
||||
"angle": _choice(parsed.get("angle"), CAMERA_ANGLE_PROMPTS, defaults["angle"]),
|
||||
@@ -1548,19 +1710,37 @@ def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str
|
||||
if str(parsed.get("camera_detail") or defaults["camera_detail"]) in CAMERA_DETAIL_CHOICES
|
||||
else defaults["camera_detail"],
|
||||
}
|
||||
if custom_camera_prompt:
|
||||
normalized["custom_camera_prompt"] = custom_camera_prompt
|
||||
if camera_source:
|
||||
normalized["camera_source"] = camera_source
|
||||
for key in (
|
||||
"orbit_azimuth",
|
||||
"orbit_elevation",
|
||||
"orbit_zoom",
|
||||
"orbit_direction",
|
||||
"orbit_elevation_label",
|
||||
"orbit_distance_label",
|
||||
"orbit_framing",
|
||||
"orbit_focus",
|
||||
):
|
||||
if key in parsed:
|
||||
normalized[key] = parsed[key]
|
||||
return normalized
|
||||
|
||||
|
||||
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, str]:
|
||||
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, Any]:
|
||||
parsed = _parse_camera_config(camera_config)
|
||||
if camera_mode and camera_mode != "from_camera_config":
|
||||
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
|
||||
return parsed
|
||||
|
||||
|
||||
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, str]]:
|
||||
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, Any]]:
|
||||
parsed = _parse_camera_config(camera_config)
|
||||
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
|
||||
return "", parsed
|
||||
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
||||
if parsed["camera_detail"] == "compact":
|
||||
values = [
|
||||
parsed["camera_mode"],
|
||||
@@ -1573,6 +1753,8 @@ def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str,
|
||||
]
|
||||
labels = [CAMERA_COMPACT_LABELS.get(value, value.replace("_", " ")) for value in values]
|
||||
labels = [label for value, label in zip(values, labels) if label and value != "auto"]
|
||||
if custom_camera_prompt:
|
||||
labels.append(custom_camera_prompt)
|
||||
if not labels:
|
||||
return "", parsed
|
||||
directive = "Camera: " + ", ".join(labels) + "."
|
||||
@@ -1588,6 +1770,8 @@ def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str,
|
||||
CAMERA_ORIENTATION_PROMPTS[parsed["orientation"]],
|
||||
CAMERA_PHONE_PROMPTS[parsed["phone_visibility"]],
|
||||
]
|
||||
if custom_camera_prompt:
|
||||
parts.append(f"Camera orbit: {custom_camera_prompt}.")
|
||||
parts = [part for part in parts if part]
|
||||
if not parts:
|
||||
return "", parsed
|
||||
@@ -1603,6 +1787,16 @@ def _insert_positive_directive(prompt: str, directive: str) -> str:
|
||||
return f"{prompt.rstrip()} {directive}"
|
||||
|
||||
|
||||
def _camera_caption_text(parsed: dict[str, Any]) -> str:
|
||||
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
||||
if custom_camera_prompt:
|
||||
return custom_camera_prompt
|
||||
camera_mode = str(parsed.get("camera_mode") or "").replace("_", " ").strip()
|
||||
if not camera_mode or camera_mode == "standard":
|
||||
return ""
|
||||
return f"{camera_mode} camera framing"
|
||||
|
||||
|
||||
def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
directive, parsed = _camera_directive(camera_config)
|
||||
row["camera_config"] = parsed
|
||||
@@ -1610,7 +1804,9 @@ def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any
|
||||
if not directive:
|
||||
return row
|
||||
row["prompt"] = _insert_positive_directive(row["prompt"], directive)
|
||||
row["caption"] = f"{row.get('caption', '').rstrip()}, {parsed['camera_mode'].replace('_', ' ')} camera framing"
|
||||
camera_caption = _camera_caption_text(parsed)
|
||||
if camera_caption:
|
||||
row["caption"] = f"{row.get('caption', '').rstrip()}, {camera_caption}"
|
||||
return row
|
||||
|
||||
|
||||
@@ -4117,7 +4313,11 @@ def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[s
|
||||
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
|
||||
else defaults["hardcore_clothing_continuity"]
|
||||
)
|
||||
parsed["softcore_camera_mode"] = parsed["softcore_camera_mode"] if parsed["softcore_camera_mode"] in CAMERA_MODE_PROMPTS else defaults["softcore_camera_mode"]
|
||||
parsed["softcore_camera_mode"] = (
|
||||
parsed["softcore_camera_mode"]
|
||||
if parsed["softcore_camera_mode"] in CAMERA_MODE_PROMPTS or parsed["softcore_camera_mode"] == "from_camera_config"
|
||||
else defaults["softcore_camera_mode"]
|
||||
)
|
||||
if (
|
||||
parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS
|
||||
and parsed["hardcore_camera_mode"] not in ("from_camera_config", "same_as_softcore")
|
||||
@@ -4616,7 +4816,7 @@ def build_insta_of_pair(
|
||||
soft_partner_styling["pose"],
|
||||
soft_row["scene_text"],
|
||||
soft_row["composition"],
|
||||
f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera" if soft_camera_directive else "",
|
||||
_camera_caption_text(soft_camera_config) if soft_camera_directive else "",
|
||||
]
|
||||
soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip())
|
||||
hard_caption_parts = [
|
||||
@@ -4629,7 +4829,7 @@ def build_insta_of_pair(
|
||||
hard_row["item"],
|
||||
hard_scene,
|
||||
hard_composition,
|
||||
f"{hard_camera_config['camera_mode'].replace('_', ' ')} camera" if hard_camera_directive else "",
|
||||
_camera_caption_text(hard_camera_config) if hard_camera_directive else "",
|
||||
]
|
||||
hard_caption = ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip())
|
||||
metadata = {
|
||||
|
||||
Reference in New Issue
Block a user