Add split workflow nodes and profile controls
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from pathlib import Path
|
||||
from string import Formatter
|
||||
from typing import Any
|
||||
@@ -14,6 +15,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
CATEGORY_DIR = ROOT_DIR / "categories"
|
||||
PROFILE_DIR = ROOT_DIR / "profiles"
|
||||
|
||||
BUILTIN_CATEGORIES = [
|
||||
"auto_weighted",
|
||||
@@ -688,6 +690,267 @@ def subcategory_choices() -> list[str]:
|
||||
return choices
|
||||
|
||||
|
||||
CATEGORY_PRESETS = {
|
||||
"auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY),
|
||||
"women_casual": ("Casual clothes", RANDOM_SUBCATEGORY),
|
||||
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
|
||||
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
|
||||
"provocative_erotic": ("Provocative erotic clothes", RANDOM_SUBCATEGORY),
|
||||
"hardcore_pose": ("Hardcore sexual poses", RANDOM_SUBCATEGORY),
|
||||
"custom_random": ("custom_random", RANDOM_SUBCATEGORY),
|
||||
}
|
||||
|
||||
CAST_PRESETS = {
|
||||
"solo_woman": (1, 0),
|
||||
"solo_man": (0, 1),
|
||||
"mixed_couple": (1, 1),
|
||||
"two_women": (2, 0),
|
||||
"two_men": (0, 2),
|
||||
"threesome_2w1m": (2, 1),
|
||||
"small_group_3w2m": (3, 2),
|
||||
}
|
||||
|
||||
GENERATION_PROFILE_PRESETS = {
|
||||
"balanced": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_intensity": 0.5,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"casual_clean": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_intensity": 0.35,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"evocative_softcore": {
|
||||
"clothing": "minimal",
|
||||
"poses": "evocative",
|
||||
"expression_intensity": 0.65,
|
||||
"backside_bias": 0.2,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"hardcore_intense": {
|
||||
"clothing": "minimal",
|
||||
"poses": "evocative",
|
||||
"expression_intensity": 0.9,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
"krea2_friendly": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_intensity": 0.55,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": False,
|
||||
},
|
||||
"flux_original": {
|
||||
"clothing": "full",
|
||||
"poses": "standard",
|
||||
"expression_intensity": 0.5,
|
||||
"backside_bias": 0.0,
|
||||
"minimal_clothing_ratio": -1.0,
|
||||
"standard_pose_ratio": -1.0,
|
||||
"trigger": "sxcpinup_coloredpencil",
|
||||
"prepend_trigger_to_prompt": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def category_preset_choices() -> list[str]:
|
||||
return list(CATEGORY_PRESETS)
|
||||
|
||||
|
||||
def cast_preset_choices() -> list[str]:
|
||||
return list(CAST_PRESETS) + ["custom_counts"]
|
||||
|
||||
|
||||
def generation_profile_choices() -> list[str]:
|
||||
return list(GENERATION_PROFILE_PRESETS)
|
||||
|
||||
|
||||
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
|
||||
category, default_subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
||||
chosen_subcategory = subcategory if subcategory and subcategory != RANDOM_SUBCATEGORY else default_subcategory
|
||||
return json.dumps(
|
||||
{
|
||||
"preset": preset if preset in CATEGORY_PRESETS else "auto_weighted",
|
||||
"category": category,
|
||||
"subcategory": chosen_subcategory,
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _parse_category_config(category_config: str | dict[str, Any] | None) -> tuple[str, str]:
|
||||
if not category_config:
|
||||
return CATEGORY_PRESETS["auto_weighted"]
|
||||
if isinstance(category_config, dict):
|
||||
raw = category_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(category_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid category_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("category_config must be a JSON object")
|
||||
preset = str(raw.get("preset") or "auto_weighted")
|
||||
category, subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
||||
category = str(raw.get("category") or category)
|
||||
subcategory = str(raw.get("subcategory") or subcategory or RANDOM_SUBCATEGORY)
|
||||
return category, subcategory
|
||||
|
||||
|
||||
def build_cast_config_json(cast_mode: str = "mixed_couple", women_count: int = 1, men_count: int = 1) -> str:
|
||||
if cast_mode in CAST_PRESETS:
|
||||
women_count, men_count = CAST_PRESETS[cast_mode]
|
||||
else:
|
||||
women_count = max(0, min(12, int(women_count)))
|
||||
men_count = max(0, min(12, int(men_count)))
|
||||
if women_count + men_count == 0:
|
||||
women_count = 1
|
||||
cast_mode = "custom_counts"
|
||||
return json.dumps(
|
||||
{
|
||||
"cast_mode": cast_mode,
|
||||
"women_count": int(women_count),
|
||||
"men_count": int(men_count),
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _parse_cast_config(cast_config: str | dict[str, Any] | None) -> dict[str, int | str]:
|
||||
if not cast_config:
|
||||
return {"cast_mode": "mixed_couple", "women_count": 1, "men_count": 1}
|
||||
if isinstance(cast_config, dict):
|
||||
raw = cast_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(cast_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid cast_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("cast_config must be a JSON object")
|
||||
return json.loads(build_cast_config_json(str(raw.get("cast_mode") or "custom_counts"), raw.get("women_count", 1), raw.get("men_count", 1)))
|
||||
|
||||
|
||||
def build_generation_profile_json(
|
||||
profile: str = "balanced",
|
||||
clothing_override: str = "profile_default",
|
||||
poses_override: str = "profile_default",
|
||||
expression_intensity: float = -1.0,
|
||||
backside_bias: float = -1.0,
|
||||
minimal_clothing_ratio: float = -1.0,
|
||||
standard_pose_ratio: float = -1.0,
|
||||
trigger_policy: str = "profile_default",
|
||||
) -> str:
|
||||
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
|
||||
config = dict(GENERATION_PROFILE_PRESETS[profile])
|
||||
if clothing_override in ("full", "minimal"):
|
||||
config["clothing"] = clothing_override
|
||||
if poses_override in ("standard", "evocative"):
|
||||
config["poses"] = poses_override
|
||||
if float(expression_intensity) >= 0:
|
||||
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
|
||||
if float(backside_bias) >= 0:
|
||||
config["backside_bias"] = _clamped_float(backside_bias, config["backside_bias"])
|
||||
if float(minimal_clothing_ratio) >= 0:
|
||||
config["minimal_clothing_ratio"] = _clamped_float(minimal_clothing_ratio, config["minimal_clothing_ratio"])
|
||||
if float(standard_pose_ratio) >= 0:
|
||||
config["standard_pose_ratio"] = _clamped_float(standard_pose_ratio, config["standard_pose_ratio"])
|
||||
if trigger_policy == "prepend_trigger":
|
||||
config["prepend_trigger_to_prompt"] = True
|
||||
elif trigger_policy == "do_not_prepend":
|
||||
config["prepend_trigger_to_prompt"] = False
|
||||
config["profile"] = profile
|
||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||
|
||||
|
||||
def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not profile_config:
|
||||
return dict(GENERATION_PROFILE_PRESETS["balanced"])
|
||||
if isinstance(profile_config, dict):
|
||||
raw = profile_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(profile_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid generation_profile JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("generation_profile must be a JSON object")
|
||||
profile = str(raw.get("profile") or "balanced")
|
||||
parsed = dict(GENERATION_PROFILE_PRESETS.get(profile, GENERATION_PROFILE_PRESETS["balanced"]))
|
||||
parsed.update(raw)
|
||||
parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal") else "full"
|
||||
parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative") else "standard"
|
||||
parsed["expression_intensity"] = _clamped_float(parsed.get("expression_intensity"), 0.5)
|
||||
parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0)
|
||||
parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0)
|
||||
parsed["standard_pose_ratio"] = _clamped_float(parsed.get("standard_pose_ratio"), -1.0, -1.0, 1.0)
|
||||
parsed["trigger"] = str(parsed.get("trigger") or "sxcpinup_coloredpencil")
|
||||
parsed["prepend_trigger_to_prompt"] = bool(parsed.get("prepend_trigger_to_prompt"))
|
||||
return parsed
|
||||
|
||||
|
||||
def build_filter_config_json(
|
||||
ethnicity: str = "any",
|
||||
figure: str = "curvy",
|
||||
no_plus_women: bool = False,
|
||||
no_black: bool = False,
|
||||
) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"ethnicity": ethnicity if ethnicity in ("any", "asian", "white_asian") else "any",
|
||||
"figure": figure if figure in ("curvy", "balanced", "bombshell") else "curvy",
|
||||
"no_plus_women": bool(no_plus_women),
|
||||
"no_black": bool(no_black),
|
||||
},
|
||||
ensure_ascii=True,
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
defaults = {"ethnicity": "any", "figure": "curvy", "no_plus_women": False, "no_black": False}
|
||||
if not filter_config:
|
||||
return defaults
|
||||
if isinstance(filter_config, dict):
|
||||
raw = filter_config
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(filter_config))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError("filter_config must be a JSON object")
|
||||
parsed = {**defaults, **raw}
|
||||
parsed["ethnicity"] = parsed["ethnicity"] if parsed.get("ethnicity") in ("any", "asian", "white_asian") else "any"
|
||||
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell") else "curvy"
|
||||
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
|
||||
parsed["no_black"] = bool(parsed.get("no_black"))
|
||||
return parsed
|
||||
|
||||
|
||||
def _ratio_or_none(value: float) -> float | None:
|
||||
try:
|
||||
ratio = float(value)
|
||||
@@ -1162,6 +1425,238 @@ def _body_phrase(body: Any, figure_note: Any = "") -> str:
|
||||
return f"{body} figure with {figure_note}"
|
||||
|
||||
|
||||
def _safe_profile_name(profile_name: str) -> str:
|
||||
profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
|
||||
return profile_name[:64] or "profile"
|
||||
|
||||
|
||||
def _profile_path(profile_name: str) -> Path:
|
||||
return PROFILE_DIR / f"{_safe_profile_name(profile_name)}.json"
|
||||
|
||||
|
||||
def character_profile_choices() -> list[str]:
|
||||
if not PROFILE_DIR.exists():
|
||||
return ["manual"]
|
||||
names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
|
||||
return ["manual"] + names
|
||||
|
||||
|
||||
def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
|
||||
if not value:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid {label} JSON: {exc}") from exc
|
||||
if not isinstance(raw, dict):
|
||||
raise ValueError(f"{label} must be a JSON object")
|
||||
return raw
|
||||
|
||||
|
||||
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
row = _load_json_object(metadata_json, "metadata_json")
|
||||
if isinstance(row.get("softcore_row"), dict):
|
||||
return row["softcore_row"]
|
||||
return row
|
||||
|
||||
|
||||
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
|
||||
age = str(profile.get("age") or "").strip()
|
||||
age = age.removesuffix(" adults").removesuffix(" adult").strip()
|
||||
subject_phrase = f"{age} adult {subject}".strip() if age else f"adult {subject}"
|
||||
pieces = [
|
||||
subject_phrase,
|
||||
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
|
||||
profile.get("skin"),
|
||||
profile.get("hair"),
|
||||
profile.get("eyes"),
|
||||
]
|
||||
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
||||
|
||||
|
||||
def _normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
|
||||
subject_type = str(profile.get("subject_type") or profile.get("primary_subject") or profile.get("subject") or "").strip()
|
||||
if subject_type not in ("woman", "man"):
|
||||
subject_type = "woman"
|
||||
body = str(profile.get("body") or profile.get("body_type") or "").strip()
|
||||
figure = str(profile.get("figure") or "").strip()
|
||||
body_phrase = str(profile.get("body_phrase") or "").strip() or _body_phrase(body, figure)
|
||||
normalized = {
|
||||
"profile_type": "character",
|
||||
"profile_name": _safe_profile_name(profile_name or str(profile.get("profile_name") or "")),
|
||||
"subject_type": subject_type,
|
||||
"subject": subject_type,
|
||||
"subject_phrase": subject_type,
|
||||
"age": str(profile.get("age") or profile.get("age_band") or "").strip(),
|
||||
"body": body,
|
||||
"body_phrase": body_phrase,
|
||||
"skin": str(profile.get("skin") or "").strip(),
|
||||
"hair": str(profile.get("hair") or "").strip(),
|
||||
"eyes": str(profile.get("eyes") or "").strip(),
|
||||
"figure": figure,
|
||||
}
|
||||
normalized["descriptor"] = _character_profile_descriptor(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def build_character_profile_json(
|
||||
profile_name: str = "",
|
||||
source: str = "metadata_json",
|
||||
metadata_json: str | dict[str, Any] | None = "",
|
||||
subject_type: str = "woman",
|
||||
age: str = "",
|
||||
body: str = "",
|
||||
body_phrase: str = "",
|
||||
skin: str = "",
|
||||
hair: str = "",
|
||||
eyes: str = "",
|
||||
figure: str = "",
|
||||
save_now: bool = False,
|
||||
) -> dict[str, str]:
|
||||
if source == "metadata_json":
|
||||
row = _row_from_profile_metadata(metadata_json)
|
||||
raw_profile = {
|
||||
"profile_name": profile_name,
|
||||
"subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
|
||||
"age": row.get("age") or row.get("age_band") or age,
|
||||
"body": row.get("body") or row.get("body_type") or body,
|
||||
"body_phrase": row.get("body_phrase") or body_phrase,
|
||||
"skin": row.get("skin") or skin,
|
||||
"hair": row.get("hair") or hair,
|
||||
"eyes": row.get("eyes") or eyes,
|
||||
"figure": row.get("figure") or figure,
|
||||
}
|
||||
else:
|
||||
raw_profile = {
|
||||
"profile_name": profile_name,
|
||||
"subject_type": subject_type,
|
||||
"age": age,
|
||||
"body": body,
|
||||
"body_phrase": body_phrase,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"figure": figure,
|
||||
}
|
||||
profile = _normalize_character_profile(raw_profile, profile_name)
|
||||
saved_path = ""
|
||||
status = "not_saved"
|
||||
if save_now:
|
||||
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = _profile_path(profile["profile_name"])
|
||||
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
saved_path = str(path)
|
||||
status = "saved"
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": saved_path,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def _empty_profile_result(status: str = "empty") -> dict[str, str]:
|
||||
return {
|
||||
"profile_json": "",
|
||||
"profile_name": "",
|
||||
"descriptor": "",
|
||||
"saved_path": "",
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def load_character_profile_json(
|
||||
profile_name: str = "",
|
||||
fallback_profile_json: str | dict[str, Any] | None = "",
|
||||
enabled: bool = True,
|
||||
delete_now: bool = False,
|
||||
rename_now: bool = False,
|
||||
rename_to: str = "",
|
||||
) -> dict[str, str]:
|
||||
if not enabled:
|
||||
return _empty_profile_result("disabled")
|
||||
if delete_now and rename_now:
|
||||
return _empty_profile_result("choose_delete_or_rename")
|
||||
|
||||
raw_profile = _load_json_object(fallback_profile_json, "fallback_profile_json")
|
||||
saved_path = ""
|
||||
if profile_name and profile_name != "manual":
|
||||
path = _profile_path(profile_name)
|
||||
if delete_now:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
return _empty_profile_result(f"deleted:{path.stem}")
|
||||
return _empty_profile_result(f"delete_missing:{_safe_profile_name(profile_name)}")
|
||||
if rename_now:
|
||||
new_name = _safe_profile_name(rename_to)
|
||||
if not rename_to.strip():
|
||||
return _empty_profile_result("rename_missing_name")
|
||||
if not path.exists():
|
||||
return _empty_profile_result(f"rename_missing:{_safe_profile_name(profile_name)}")
|
||||
target = _profile_path(new_name)
|
||||
if target.exists() and target != path:
|
||||
return _empty_profile_result(f"rename_target_exists:{target.stem}")
|
||||
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
||||
profile = _normalize_character_profile(raw_profile, new_name)
|
||||
target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
if target != path:
|
||||
path.unlink()
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": str(target),
|
||||
"status": f"renamed:{path.stem}->{target.stem}",
|
||||
}
|
||||
if path.exists():
|
||||
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
||||
saved_path = str(path)
|
||||
if not raw_profile:
|
||||
return _empty_profile_result("empty")
|
||||
profile = _normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": saved_path,
|
||||
"status": "loaded" if saved_path else "fallback",
|
||||
}
|
||||
|
||||
|
||||
def _parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
raw = _load_json_object(character_profile, "character_profile")
|
||||
if not raw:
|
||||
return {}
|
||||
if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
|
||||
return _normalize_character_profile(raw, str(raw.get("profile_name") or ""))
|
||||
return {}
|
||||
|
||||
|
||||
def _apply_character_profile_to_context(
|
||||
context: dict[str, Any],
|
||||
character_profile: str | dict[str, Any] | None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
||||
profile = _parse_character_profile(character_profile)
|
||||
if not profile:
|
||||
return context, {}, "none"
|
||||
if context.get("subject_type") not in ("woman", "man"):
|
||||
return context, profile, "skipped_non_single_subject"
|
||||
if profile["subject_type"] != context.get("subject_type"):
|
||||
return context, profile, "skipped_subject_mismatch"
|
||||
updated = dict(context)
|
||||
for key in ("subject_type", "subject", "subject_phrase", "age", "body", "body_phrase", "skin", "hair", "eyes", "figure"):
|
||||
value = profile.get(key)
|
||||
if value:
|
||||
updated[key] = value
|
||||
updated["subject"] = profile["subject_type"]
|
||||
updated["subject_phrase"] = profile["subject_type"]
|
||||
return updated, profile, "applied"
|
||||
|
||||
|
||||
def _composition_prompt(composition: str) -> str:
|
||||
composition = str(composition or "").strip()
|
||||
if not composition:
|
||||
@@ -1760,6 +2255,7 @@ def _build_custom_row(
|
||||
seed: int,
|
||||
seed_config: dict[str, int],
|
||||
expression_intensity: float,
|
||||
character_profile: str | dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
categories = load_category_library()
|
||||
category_rng = _axis_rng(seed_config, "category", seed, row_number)
|
||||
@@ -1797,6 +2293,7 @@ def _build_custom_row(
|
||||
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
|
||||
subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any"))
|
||||
context = _subject_context(person_rng, subject_type, ethnicity, figure, no_plus_women, no_black, women_count, men_count)
|
||||
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
||||
subject_type = context["subject_type"]
|
||||
role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
|
||||
|
||||
@@ -1914,6 +2411,8 @@ def _build_custom_row(
|
||||
"men_count": context.get("men_count", ""),
|
||||
"person_count": context.get("person_count", ""),
|
||||
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {},
|
||||
"character_profile": applied_profile,
|
||||
"character_profile_status": profile_status,
|
||||
"source": "json_category",
|
||||
}
|
||||
)
|
||||
@@ -1946,6 +2445,7 @@ def build_prompt(
|
||||
men_count: int = 1,
|
||||
camera_config: str | dict[str, Any] | None = None,
|
||||
expression_intensity: float = 0.5,
|
||||
character_profile: str | dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
apply_pool_extensions()
|
||||
row_number = max(1, int(row_number))
|
||||
@@ -2009,6 +2509,7 @@ def build_prompt(
|
||||
seed,
|
||||
parsed_seed_config,
|
||||
expression_intensity,
|
||||
character_profile,
|
||||
)
|
||||
|
||||
if extra_positive.strip():
|
||||
@@ -2022,6 +2523,52 @@ def build_prompt(
|
||||
return row
|
||||
|
||||
|
||||
def build_prompt_from_configs(
|
||||
row_number: int,
|
||||
start_index: int,
|
||||
seed: int,
|
||||
category_config: str | dict[str, Any] | None = "",
|
||||
cast_config: str | dict[str, Any] | None = "",
|
||||
generation_profile: str | dict[str, Any] | None = "",
|
||||
filter_config: str | dict[str, Any] | None = "",
|
||||
seed_config: str | dict[str, Any] | None = "",
|
||||
camera_config: str | dict[str, Any] | None = "",
|
||||
character_profile: str | dict[str, Any] | None = "",
|
||||
extra_positive: str = "",
|
||||
extra_negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
category, subcategory = _parse_category_config(category_config)
|
||||
cast = _parse_cast_config(cast_config)
|
||||
profile = _parse_generation_profile(generation_profile)
|
||||
filters = _parse_filter_config(filter_config)
|
||||
return build_prompt(
|
||||
category=category,
|
||||
subcategory=subcategory,
|
||||
row_number=row_number,
|
||||
start_index=start_index,
|
||||
seed=seed,
|
||||
clothing=profile["clothing"],
|
||||
ethnicity=filters["ethnicity"],
|
||||
poses=profile["poses"],
|
||||
expression_intensity=profile["expression_intensity"],
|
||||
backside_bias=profile["backside_bias"],
|
||||
figure=filters["figure"],
|
||||
no_plus_women=filters["no_plus_women"],
|
||||
no_black=filters["no_black"],
|
||||
women_count=int(cast["women_count"]),
|
||||
men_count=int(cast["men_count"]),
|
||||
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
|
||||
standard_pose_ratio=profile["standard_pose_ratio"],
|
||||
trigger=profile["trigger"],
|
||||
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
|
||||
extra_positive=extra_positive or "",
|
||||
extra_negative=extra_negative or "",
|
||||
seed_config=seed_config or "",
|
||||
camera_config=camera_config or "",
|
||||
character_profile=character_profile or "",
|
||||
)
|
||||
|
||||
|
||||
INSTA_OF_SOFT_LEVELS = {
|
||||
"social_tease": "Instagram-style thirst-trap post, suggestive but non-explicit, polished social feed energy",
|
||||
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate but without explicit sex",
|
||||
@@ -2264,6 +2811,7 @@ def build_insta_of_pair(
|
||||
seed_config: str | dict[str, Any] | None = None,
|
||||
options_json: str | dict[str, Any] | None = None,
|
||||
camera_config: str | dict[str, Any] | None = None,
|
||||
character_profile: str | dict[str, Any] | None = "",
|
||||
extra_positive: str = "",
|
||||
extra_negative: str = "",
|
||||
) -> dict[str, Any]:
|
||||
@@ -2295,6 +2843,7 @@ def build_insta_of_pair(
|
||||
women_count=1,
|
||||
men_count=0,
|
||||
expression_intensity=options["softcore_expression_intensity"],
|
||||
character_profile=character_profile or "",
|
||||
)
|
||||
hard_row = build_prompt(
|
||||
category="Hardcore sexual poses",
|
||||
|
||||
Reference in New Issue
Block a user