Extract character profile policy
This commit is contained in:
+75
-316
@@ -25,6 +25,7 @@ try:
|
||||
)
|
||||
from . import camera_config as camera_policy
|
||||
from . import character_config as character_policy
|
||||
from . import character_profile as character_profile_policy
|
||||
from . import category_cast_config as category_cast_policy
|
||||
from . import filter_config as filter_policy
|
||||
from . import generate_prompt_batches as g
|
||||
@@ -68,6 +69,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
)
|
||||
import camera_config as camera_policy
|
||||
import character_config as character_policy
|
||||
import character_profile as character_profile_policy
|
||||
import category_cast_config as category_cast_policy
|
||||
import filter_config as filter_policy
|
||||
import generate_prompt_batches as g
|
||||
@@ -96,7 +98,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parent
|
||||
PROFILE_DIR = ROOT_DIR / "profiles"
|
||||
PROFILE_DIR = character_profile_policy.PROFILE_DIR
|
||||
|
||||
BUILTIN_CATEGORIES = [
|
||||
"auto_weighted",
|
||||
@@ -1933,81 +1935,34 @@ def _auto_full_choice(seed_config: dict[str, int], seed: int, row_number: int) -
|
||||
|
||||
|
||||
def _body_phrase(body: Any, figure_note: Any = "") -> str:
|
||||
body = str(body or "").strip()
|
||||
figure_note = str(figure_note or "").strip()
|
||||
if not body:
|
||||
return figure_note
|
||||
if not figure_note:
|
||||
return f"{body} figure"
|
||||
if "figure" in figure_note.lower():
|
||||
return f"{body} build and {figure_note}"
|
||||
return f"{body} figure with {figure_note}"
|
||||
return character_profile_policy.body_phrase(body, 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"
|
||||
return character_profile_policy.safe_profile_name(profile_name)
|
||||
|
||||
|
||||
def _profile_path(profile_name: str) -> Path:
|
||||
return PROFILE_DIR / f"{_safe_profile_name(profile_name)}.json"
|
||||
return character_profile_policy.profile_path(profile_name)
|
||||
|
||||
|
||||
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
|
||||
return character_profile_policy.character_profile_choices()
|
||||
|
||||
|
||||
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
|
||||
return character_profile_policy.load_json_object(value, label)
|
||||
|
||||
|
||||
CHARACTER_MANUAL_FIELDS = (
|
||||
"manual_age",
|
||||
"manual_body",
|
||||
"body_phrase",
|
||||
"skin",
|
||||
"hair",
|
||||
"eyes",
|
||||
"softcore_outfit",
|
||||
"hardcore_clothing",
|
||||
)
|
||||
CHARACTER_MANUAL_FIELDS = character_profile_policy.CHARACTER_MANUAL_FIELDS
|
||||
|
||||
|
||||
def _parse_character_manual_config(value: str | dict[str, Any] | None) -> dict[str, str]:
|
||||
if not value:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
raw = value
|
||||
else:
|
||||
try:
|
||||
raw = json.loads(str(value))
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
if not isinstance(raw, dict):
|
||||
return {}
|
||||
return {
|
||||
key: str(raw.get(key) or "").strip()
|
||||
for key in CHARACTER_MANUAL_FIELDS
|
||||
if str(raw.get(key) or "").strip()
|
||||
}
|
||||
return character_profile_policy.parse_character_manual_config(value)
|
||||
|
||||
|
||||
def _character_manual_summary(config: dict[str, str]) -> str:
|
||||
parts = [f"{key}={value}" for key, value in config.items() if value]
|
||||
return "; ".join(parts) if parts else "manual unrestricted"
|
||||
return character_profile_policy.character_manual_summary(config)
|
||||
|
||||
|
||||
def build_character_manual_config_json(
|
||||
@@ -2022,24 +1977,18 @@ def build_character_manual_config_json(
|
||||
softcore_outfit: str = "",
|
||||
hardcore_clothing: str = "",
|
||||
) -> str:
|
||||
base = {} if combine_mode == "replace_all" else _parse_character_manual_config(manual)
|
||||
updates = {
|
||||
"manual_age": manual_age,
|
||||
"manual_body": manual_body,
|
||||
"body_phrase": body_phrase,
|
||||
"skin": skin,
|
||||
"hair": hair,
|
||||
"eyes": eyes,
|
||||
"softcore_outfit": softcore_outfit,
|
||||
"hardcore_clothing": hardcore_clothing,
|
||||
}
|
||||
for key, value in updates.items():
|
||||
value = str(value or "").strip()
|
||||
if value:
|
||||
base[key] = value
|
||||
result = {"config_type": "character_manual", **base}
|
||||
result["summary"] = _character_manual_summary(base)
|
||||
return json.dumps(result, ensure_ascii=True, sort_keys=True)
|
||||
return character_profile_policy.build_character_manual_config_json(
|
||||
manual=manual,
|
||||
combine_mode=combine_mode,
|
||||
manual_age=manual_age,
|
||||
manual_body=manual_body,
|
||||
body_phrase=body_phrase,
|
||||
skin=skin,
|
||||
hair=hair,
|
||||
eyes=eyes,
|
||||
softcore_outfit=softcore_outfit,
|
||||
hardcore_clothing=hardcore_clothing,
|
||||
)
|
||||
|
||||
|
||||
def _slot_value(value: Any) -> str:
|
||||
@@ -2363,10 +2312,7 @@ def _sanitize_character_expression_text_for_action(
|
||||
|
||||
|
||||
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
|
||||
detail = _normalize_descriptor_detail(descriptor_detail)
|
||||
if detail != "auto":
|
||||
return detail
|
||||
return "compact" if str(subject or "").strip().lower() == "man" else "full"
|
||||
return character_profile_policy.descriptor_detail_for_subject(subject, descriptor_detail)
|
||||
|
||||
|
||||
def _descriptor_from_parts(
|
||||
@@ -2378,21 +2324,15 @@ def _descriptor_from_parts(
|
||||
eyes: Any,
|
||||
descriptor_detail: Any = "auto",
|
||||
) -> str:
|
||||
subject = str(subject or "person").strip() or "person"
|
||||
age_text = " ".join(str(age or "").strip().split())
|
||||
age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
|
||||
if age_text in ("adult", "adults"):
|
||||
age_text = ""
|
||||
subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
|
||||
detail = _descriptor_detail_for_subject(subject, descriptor_detail)
|
||||
detail_map = {
|
||||
"minimal": (body_phrase,),
|
||||
"compact": (body_phrase, skin),
|
||||
"medium": (body_phrase, skin, hair),
|
||||
"full": (body_phrase, skin, hair, eyes),
|
||||
}
|
||||
pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
|
||||
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
||||
return character_profile_policy.descriptor_from_parts(
|
||||
subject,
|
||||
age,
|
||||
body_phrase,
|
||||
skin,
|
||||
hair,
|
||||
eyes,
|
||||
descriptor_detail,
|
||||
)
|
||||
|
||||
|
||||
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
||||
@@ -3070,10 +3010,7 @@ def _cast_descriptor_entries(
|
||||
|
||||
|
||||
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
|
||||
return character_profile_policy.row_from_profile_metadata(metadata_json)
|
||||
|
||||
|
||||
def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
|
||||
@@ -3096,42 +3033,11 @@ def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dic
|
||||
|
||||
|
||||
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
|
||||
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
|
||||
return _descriptor_from_parts(
|
||||
subject,
|
||||
profile.get("age"),
|
||||
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
|
||||
profile.get("skin"),
|
||||
profile.get("hair"),
|
||||
profile.get("eyes"),
|
||||
profile.get("descriptor_detail"),
|
||||
)
|
||||
return character_profile_policy.character_profile_descriptor(profile)
|
||||
|
||||
|
||||
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,
|
||||
"descriptor_detail": _normalize_descriptor_detail(profile.get("descriptor_detail")),
|
||||
}
|
||||
normalized["descriptor"] = _character_profile_descriptor(normalized)
|
||||
return normalized
|
||||
return character_profile_policy.normalize_character_profile(profile, profile_name)
|
||||
|
||||
|
||||
def build_character_profile_json(
|
||||
@@ -3149,90 +3055,30 @@ def build_character_profile_json(
|
||||
figure: str = "",
|
||||
save_now: bool = False,
|
||||
) -> dict[str, str]:
|
||||
if source == "character_slot":
|
||||
row = _row_from_character_slot(character_slot or metadata_json)
|
||||
raw_profile = {
|
||||
"profile_name": profile_name,
|
||||
"subject_type": row.get("subject_type") or subject_type,
|
||||
"age": row.get("age") or age,
|
||||
"body": row.get("body") 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,
|
||||
"descriptor_detail": row.get("descriptor_detail") or "auto",
|
||||
}
|
||||
elif 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,
|
||||
"descriptor_detail": row.get("descriptor_detail") or "auto",
|
||||
}
|
||||
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,
|
||||
"descriptor_detail": "auto",
|
||||
}
|
||||
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,
|
||||
}
|
||||
character_slot_row = _row_from_character_slot(character_slot or metadata_json) if source == "character_slot" else None
|
||||
return character_profile_policy.build_character_profile_json(
|
||||
profile_name=profile_name,
|
||||
source=source,
|
||||
metadata_json=metadata_json,
|
||||
character_slot_row=character_slot_row,
|
||||
subject_type=subject_type,
|
||||
age=age,
|
||||
body=body,
|
||||
body_phrase_value=body_phrase,
|
||||
skin=skin,
|
||||
hair=hair,
|
||||
eyes=eyes,
|
||||
figure=figure,
|
||||
save_now=save_now,
|
||||
)
|
||||
|
||||
|
||||
def save_character_profile_payload(profile_name: str = "", profile_json: str | dict[str, Any] | None = "") -> dict[str, str]:
|
||||
raw_profile = _load_json_object(profile_json, "profile_json")
|
||||
if not raw_profile:
|
||||
raise ValueError("No cached character profile is available to save.")
|
||||
profile = _normalize_character_profile(raw_profile, profile_name or str(raw_profile.get("profile_name") or ""))
|
||||
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")
|
||||
return {
|
||||
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
||||
"profile_name": profile["profile_name"],
|
||||
"descriptor": profile["descriptor"],
|
||||
"saved_path": str(path),
|
||||
"status": "saved",
|
||||
}
|
||||
return character_profile_policy.save_character_profile_payload(profile_name, profile_json)
|
||||
|
||||
|
||||
def _empty_profile_result(status: str = "empty") -> dict[str, str]:
|
||||
return {
|
||||
"profile_json": "",
|
||||
"profile_name": "",
|
||||
"descriptor": "",
|
||||
"saved_path": "",
|
||||
"status": status,
|
||||
}
|
||||
return character_profile_policy.empty_profile_result(status)
|
||||
|
||||
|
||||
def _apply_character_profile_overrides(
|
||||
@@ -3247,31 +3093,18 @@ def _apply_character_profile_overrides(
|
||||
override_figure: str = "",
|
||||
override_descriptor_detail: str = "",
|
||||
) -> dict[str, Any]:
|
||||
updated = dict(profile)
|
||||
subject_type = str(override_subject_type or "").strip()
|
||||
if subject_type in ("woman", "man"):
|
||||
updated["subject_type"] = subject_type
|
||||
updated["subject"] = subject_type
|
||||
updated["subject_phrase"] = subject_type
|
||||
for key, value in (
|
||||
("age", override_age),
|
||||
("body", override_body),
|
||||
("body_phrase", override_body_phrase),
|
||||
("skin", override_skin),
|
||||
("hair", override_hair),
|
||||
("eyes", override_eyes),
|
||||
("figure", override_figure),
|
||||
):
|
||||
text = str(value or "").strip()
|
||||
if text:
|
||||
updated[key] = text
|
||||
descriptor_detail = str(override_descriptor_detail or "").strip()
|
||||
if descriptor_detail and descriptor_detail != "keep_profile":
|
||||
updated["descriptor_detail"] = _normalize_descriptor_detail(descriptor_detail)
|
||||
if not str(updated.get("body_phrase") or "").strip():
|
||||
updated["body_phrase"] = _body_phrase(updated.get("body"), updated.get("figure"))
|
||||
updated["descriptor"] = _character_profile_descriptor(updated)
|
||||
return updated
|
||||
return character_profile_policy.apply_character_profile_overrides(
|
||||
profile,
|
||||
override_subject_type=override_subject_type,
|
||||
override_age=override_age,
|
||||
override_body=override_body,
|
||||
override_body_phrase=override_body_phrase,
|
||||
override_skin=override_skin,
|
||||
override_hair=override_hair,
|
||||
override_eyes=override_eyes,
|
||||
override_figure=override_figure,
|
||||
override_descriptor_detail=override_descriptor_detail,
|
||||
)
|
||||
|
||||
|
||||
def load_character_profile_json(
|
||||
@@ -3291,49 +3124,13 @@ def load_character_profile_json(
|
||||
override_figure: str = "",
|
||||
override_descriptor_detail: 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", ""))
|
||||
profile = _apply_character_profile_overrides(
|
||||
profile,
|
||||
return character_profile_policy.load_character_profile_json(
|
||||
profile_name=profile_name,
|
||||
fallback_profile_json=fallback_profile_json,
|
||||
enabled=enabled,
|
||||
delete_now=delete_now,
|
||||
rename_now=rename_now,
|
||||
rename_to=rename_to,
|
||||
override_subject_type=override_subject_type,
|
||||
override_age=override_age,
|
||||
override_body=override_body,
|
||||
@@ -3344,55 +3141,17 @@ def load_character_profile_json(
|
||||
override_figure=override_figure,
|
||||
override_descriptor_detail=override_descriptor_detail,
|
||||
)
|
||||
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 {}
|
||||
return character_profile_policy.parse_character_profile(character_profile)
|
||||
|
||||
|
||||
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",
|
||||
"descriptor_detail",
|
||||
):
|
||||
value = profile.get(key)
|
||||
if value:
|
||||
updated[key] = value
|
||||
updated["subject"] = profile["subject_type"]
|
||||
updated["subject_phrase"] = profile["subject_type"]
|
||||
return updated, profile, "applied"
|
||||
return character_profile_policy.apply_character_profile_to_context(context, character_profile)
|
||||
|
||||
|
||||
def _composition_prompt(composition: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user