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 Control`
|
||||||
- `prompt_builder / SxCP Seed Locker`
|
- `prompt_builder / SxCP Seed Locker`
|
||||||
- `prompt_builder / SxCP Camera Control`
|
- `prompt_builder / SxCP Camera Control`
|
||||||
|
- `prompt_builder / SxCP Camera Orbit Control`
|
||||||
- `prompt_builder / SxCP Category Preset`
|
- `prompt_builder / SxCP Category Preset`
|
||||||
- `prompt_builder / SxCP Cast Control`
|
- `prompt_builder / SxCP Cast Control`
|
||||||
- `prompt_builder / SxCP Generation Profile`
|
- `prompt_builder / SxCP Generation Profile`
|
||||||
@@ -54,8 +55,8 @@ node. For cleaner workflows, use the split nodes:
|
|||||||
The practical compact workflow is:
|
The practical compact workflow is:
|
||||||
|
|
||||||
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
`Category Preset` + `Cast Control` + `Generation Profile` + optional
|
||||||
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control`,
|
`Advanced Filters`, `Seed Locker` or `Seed Control`, `Camera Control` or
|
||||||
`Woman Slot` / `Man Slot`, and `Character Profile`
|
`Camera Orbit Control`, `Woman Slot` / `Man Slot`, and `Character Profile`
|
||||||
into `Prompt Builder From Configs`.
|
into `Prompt Builder From Configs`.
|
||||||
|
|
||||||
An importable default workflow is included at
|
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
|
axes stay frozen to `base_seed`; the rerolled axis follows `reroll_seed`, or the
|
||||||
main prompt seed when `reroll_seed=-1`.
|
main prompt seed when `reroll_seed=-1`.
|
||||||
|
|
||||||
`SxCP Camera Control` outputs `camera_config`, which can be connected to the
|
`SxCP Camera Control` and `SxCP Camera Orbit Control` output `camera_config`,
|
||||||
prompt builder or the Insta/OF pair node. It makes camera/framing first-class
|
which can be connected to the prompt builder or the Insta/OF pair node. They
|
||||||
instead of relying on a weak phrase inside the prompt.
|
make camera/framing first-class instead of relying on a weak phrase inside the
|
||||||
|
prompt.
|
||||||
|
|
||||||
Camera controls:
|
Camera controls:
|
||||||
|
|
||||||
@@ -201,6 +203,24 @@ Camera controls:
|
|||||||
- `camera_detail`: `off` emits no camera sentence, `compact` emits one short
|
- `camera_detail`: `off` emits no camera sentence, `compact` emits one short
|
||||||
camera sentence, and `full` emits the full detailed camera constraint.
|
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
|
`SxCP Caption Naturalizer` rewrites tag-like captions or labeled prompts into
|
||||||
more natural language. Connect the prompt builder's `metadata_json` output to
|
more natural language. Connect the prompt builder's `metadata_json` output to
|
||||||
`source_text` for the cleanest result. You can also connect `caption` or
|
`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;
|
prompt can include partners. Softcore defaults to handheld selfie framing;
|
||||||
hardcore defaults to `from_camera_config`, which emits no camera sentence unless
|
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.
|
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
|
For stronger camera control, connect `SxCP Camera Control` or
|
||||||
optional `camera_config` input.
|
`SxCP Camera Orbit Control` to the pair node's optional `camera_config` input.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
@@ -342,11 +362,12 @@ Options:
|
|||||||
references the softcore outfit, uses it displaced/removed, or makes Woman A
|
references the softcore outfit, uses it displaced/removed, or makes Woman A
|
||||||
explicitly nude. It is a fallback for Woman A; `hardcore_clothing` on
|
explicitly nude. It is a fallback for Woman A; `hardcore_clothing` on
|
||||||
`SxCP Woman Slot` or `SxCP Man Slot` takes priority for that character.
|
`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
|
- `hardcore_camera_mode`: `from_camera_config`, `same_as_softcore`, or a
|
||||||
separate base camera mode for the hardcore output. `from_camera_config` is
|
separate base camera mode for the hardcore output. `from_camera_config` is
|
||||||
neutral with no connected camera config, and uses `SxCP Camera Control` when
|
neutral with no connected camera config, and uses `SxCP Camera Control` or
|
||||||
one is connected.
|
`SxCP Camera Orbit Control` when one is connected.
|
||||||
- `camera_detail`: `off`, `compact`, or `full` for the pair prompt camera text.
|
- `camera_detail`: `off`, `compact`, or `full` for the pair prompt camera text.
|
||||||
- `hardcore_detail_density`: `compact` keeps the Krea hardcore rewrite mostly
|
- `hardcore_detail_density`: `compact` keeps the Krea hardcore rewrite mostly
|
||||||
to the position/action sentence, `balanced` keeps one useful non-duplicated
|
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`.
|
- `figure`: `curvy`, `balanced`, `bombshell`.
|
||||||
- In split workflows, use `SxCP Advanced Filters` checkboxes instead of negative
|
- In split workflows, use `SxCP Advanced Filters` checkboxes instead of negative
|
||||||
toggles. Black/African and plus-size are positive include choices there.
|
toggles. Black/African and plus-size are positive include choices there.
|
||||||
- Optional `camera_config`: connect `SxCP Camera Control` to force selfie,
|
- Optional `camera_config`: connect `SxCP Camera Control` or
|
||||||
phone, lens, angle, distance, crop, and camera-priority behavior. This applies
|
`SxCP Camera Orbit Control` to force selfie, phone, lens, angle, numeric
|
||||||
to custom categories too, including `Hardcore sexual poses`.
|
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
|
`auto_weighted` uses the original batch mix: mostly women, then men, couples, and
|
||||||
group/layout rows. Direct categories generate only that selected category.
|
group/layout rows. Direct categories generate only that selected category.
|
||||||
|
|||||||
+71
-1
@@ -6,6 +6,7 @@ import random
|
|||||||
try:
|
try:
|
||||||
from .prompt_builder import (
|
from .prompt_builder import (
|
||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
|
build_camera_orbit_config_json,
|
||||||
build_cast_config_json,
|
build_cast_config_json,
|
||||||
build_category_config_json,
|
build_category_config_json,
|
||||||
build_character_slot_json,
|
build_character_slot_json,
|
||||||
@@ -23,6 +24,8 @@ try:
|
|||||||
camera_distance_choices,
|
camera_distance_choices,
|
||||||
camera_lens_choices,
|
camera_lens_choices,
|
||||||
camera_mode_choices,
|
camera_mode_choices,
|
||||||
|
camera_orbit_focus_choices,
|
||||||
|
camera_orbit_framing_choices,
|
||||||
camera_orientation_choices,
|
camera_orientation_choices,
|
||||||
camera_phone_choices,
|
camera_phone_choices,
|
||||||
camera_priority_choices,
|
camera_priority_choices,
|
||||||
@@ -52,6 +55,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from prompt_builder import (
|
from prompt_builder import (
|
||||||
build_camera_config_json,
|
build_camera_config_json,
|
||||||
|
build_camera_orbit_config_json,
|
||||||
build_cast_config_json,
|
build_cast_config_json,
|
||||||
build_category_config_json,
|
build_category_config_json,
|
||||||
build_character_slot_json,
|
build_character_slot_json,
|
||||||
@@ -69,6 +73,8 @@ except ImportError:
|
|||||||
camera_distance_choices,
|
camera_distance_choices,
|
||||||
camera_lens_choices,
|
camera_lens_choices,
|
||||||
camera_mode_choices,
|
camera_mode_choices,
|
||||||
|
camera_orbit_focus_choices,
|
||||||
|
camera_orbit_framing_choices,
|
||||||
camera_orientation_choices,
|
camera_orientation_choices,
|
||||||
camera_phone_choices,
|
camera_phone_choices,
|
||||||
camera_priority_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:
|
class SxCPCategoryPreset:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -1083,7 +1151,7 @@ class SxCPInstaOFOptions:
|
|||||||
"platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}),
|
"platform_style": (["hybrid", "instagram", "onlyfans"], {"default": "hybrid"}),
|
||||||
"continuity": (["same_creator_same_room", "same_creator_new_scene"], {"default": "same_creator_same_room"}),
|
"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"}),
|
"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"}),
|
"hardcore_camera_mode": (["from_camera_config", "same_as_softcore"] + camera_mode_choices(), {"default": "from_camera_config"}),
|
||||||
"camera_detail": (camera_detail_choices(), {"default": "compact"}),
|
"camera_detail": (camera_detail_choices(), {"default": "compact"}),
|
||||||
"hardcore_detail_density": (hardcore_detail_density_choices(), {"default": "balanced"}),
|
"hardcore_detail_density": (hardcore_detail_density_choices(), {"default": "balanced"}),
|
||||||
@@ -1233,6 +1301,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"SxCPSeedControl": SxCPSeedControl,
|
"SxCPSeedControl": SxCPSeedControl,
|
||||||
"SxCPSeedLocker": SxCPSeedLocker,
|
"SxCPSeedLocker": SxCPSeedLocker,
|
||||||
"SxCPCameraControl": SxCPCameraControl,
|
"SxCPCameraControl": SxCPCameraControl,
|
||||||
|
"SxCPCameraOrbitControl": SxCPCameraOrbitControl,
|
||||||
"SxCPCategoryPreset": SxCPCategoryPreset,
|
"SxCPCategoryPreset": SxCPCategoryPreset,
|
||||||
"SxCPCastControl": SxCPCastControl,
|
"SxCPCastControl": SxCPCastControl,
|
||||||
"SxCPGenerationProfile": SxCPGenerationProfile,
|
"SxCPGenerationProfile": SxCPGenerationProfile,
|
||||||
@@ -1254,6 +1323,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"SxCPSeedControl": "SxCP Seed Control",
|
"SxCPSeedControl": "SxCP Seed Control",
|
||||||
"SxCPSeedLocker": "SxCP Seed Locker",
|
"SxCPSeedLocker": "SxCP Seed Locker",
|
||||||
"SxCPCameraControl": "SxCP Camera Control",
|
"SxCPCameraControl": "SxCP Camera Control",
|
||||||
|
"SxCPCameraOrbitControl": "SxCP Camera Orbit Control",
|
||||||
"SxCPCategoryPreset": "SxCP Category Preset",
|
"SxCPCategoryPreset": "SxCP Category Preset",
|
||||||
"SxCPCastControl": "SxCP Cast Control",
|
"SxCPCastControl": "SxCP Cast Control",
|
||||||
"SxCPGenerationProfile": "SxCP Generation Profile",
|
"SxCPGenerationProfile": "SxCP Generation Profile",
|
||||||
|
|||||||
@@ -1320,6 +1320,11 @@ def _camera_phrase(row: dict[str, Any]) -> str:
|
|||||||
detail = _clean(config.get("camera_detail"))
|
detail = _clean(config.get("camera_detail"))
|
||||||
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
||||||
return ""
|
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("_", " ")
|
mode = _clean(config.get("camera_mode")).replace("_", " ")
|
||||||
shot = _clean(config.get("shot_size")).replace("_", " ")
|
shot = _clean(config.get("shot_size")).replace("_", " ")
|
||||||
angle = _clean(config.get("angle")).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"))
|
detail = _clean(config.get("camera_detail"))
|
||||||
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
||||||
return ""
|
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 = [
|
values = [
|
||||||
_clean(config.get("camera_mode")).replace("_", " "),
|
_clean(config.get("camera_mode")).replace("_", " "),
|
||||||
_clean(config.get("shot_size")).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"]
|
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
||||||
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
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 = (
|
GENERIC_POSITIVE_SUFFIX = (
|
||||||
"Use crisp clean comic linework, detailed hatching, soft blended shading, "
|
"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)
|
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]:
|
def camera_shot_choices() -> list[str]:
|
||||||
return list(CAMERA_SHOT_PROMPTS)
|
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:
|
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
|
||||||
value = str(value or default)
|
value = str(value or default)
|
||||||
return value if value in choices else 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 = {
|
defaults = {
|
||||||
"camera_mode": "standard",
|
"camera_mode": "standard",
|
||||||
"shot_size": "auto",
|
"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):
|
if not isinstance(raw, dict):
|
||||||
raise ValueError("camera_config must be a JSON object")
|
raise ValueError("camera_config must be a JSON object")
|
||||||
parsed = {**defaults, **raw}
|
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"]),
|
"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"]),
|
"shot_size": _choice(parsed.get("shot_size"), CAMERA_SHOT_PROMPTS, defaults["shot_size"]),
|
||||||
"angle": _choice(parsed.get("angle"), CAMERA_ANGLE_PROMPTS, defaults["angle"]),
|
"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
|
if str(parsed.get("camera_detail") or defaults["camera_detail"]) in CAMERA_DETAIL_CHOICES
|
||||||
else defaults["camera_detail"],
|
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)
|
parsed = _parse_camera_config(camera_config)
|
||||||
if camera_mode and camera_mode != "from_camera_config":
|
if camera_mode and camera_mode != "from_camera_config":
|
||||||
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
|
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
|
||||||
return parsed
|
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)
|
parsed = _parse_camera_config(camera_config)
|
||||||
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
|
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
|
||||||
return "", parsed
|
return "", parsed
|
||||||
|
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
||||||
if parsed["camera_detail"] == "compact":
|
if parsed["camera_detail"] == "compact":
|
||||||
values = [
|
values = [
|
||||||
parsed["camera_mode"],
|
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 = [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"]
|
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:
|
if not labels:
|
||||||
return "", parsed
|
return "", parsed
|
||||||
directive = "Camera: " + ", ".join(labels) + "."
|
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_ORIENTATION_PROMPTS[parsed["orientation"]],
|
||||||
CAMERA_PHONE_PROMPTS[parsed["phone_visibility"]],
|
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]
|
parts = [part for part in parts if part]
|
||||||
if not parts:
|
if not parts:
|
||||||
return "", parsed
|
return "", parsed
|
||||||
@@ -1603,6 +1787,16 @@ def _insert_positive_directive(prompt: str, directive: str) -> str:
|
|||||||
return f"{prompt.rstrip()} {directive}"
|
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]:
|
def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||||
directive, parsed = _camera_directive(camera_config)
|
directive, parsed = _camera_directive(camera_config)
|
||||||
row["camera_config"] = parsed
|
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:
|
if not directive:
|
||||||
return row
|
return row
|
||||||
row["prompt"] = _insert_positive_directive(row["prompt"], directive)
|
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
|
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
|
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
|
||||||
else defaults["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 (
|
if (
|
||||||
parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS
|
parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS
|
||||||
and parsed["hardcore_camera_mode"] not in ("from_camera_config", "same_as_softcore")
|
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_partner_styling["pose"],
|
||||||
soft_row["scene_text"],
|
soft_row["scene_text"],
|
||||||
soft_row["composition"],
|
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())
|
soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip())
|
||||||
hard_caption_parts = [
|
hard_caption_parts = [
|
||||||
@@ -4629,7 +4829,7 @@ def build_insta_of_pair(
|
|||||||
hard_row["item"],
|
hard_row["item"],
|
||||||
hard_scene,
|
hard_scene,
|
||||||
hard_composition,
|
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())
|
hard_caption = ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip())
|
||||||
metadata = {
|
metadata = {
|
||||||
|
|||||||
Reference in New Issue
Block a user