Add analysis profiles with distance/proximity-aware axes

A discrete verdict collapses magnitude and a generic axis can hide what you're
calibrating (a blowjob where the head is 20cm away still reads sexual_act=oral ->
MATCH). New 'profile' input selects an act-specialized axis set (general / oral /
penetration / handjob / solo) whose act-critical axes capture distance explicitly
(mouth_genital_distance: touching/<5cm/10-20cm/>20cm, oral_depth, insertion_depth,
stroke_position, ...). axes now overrides the profile when set. agent_bridge gains
--profile; workflows + docs updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 00:48:46 +02:00
parent 06992506d7
commit 887dfc0bbb
6 changed files with 95 additions and 7 deletions
+63 -4
View File
@@ -78,8 +78,63 @@ AXIS_DEFS = {
"scene": "location, furniture, props, background",
"lighting_color": "lighting quality and color palette / grade",
"art_style": "rendering style and realism (photoreal, anime, illustration, 3D)",
# --- DISTANCE-AWARE act-specific axes (used by analysis profiles) ---
# Oral: capture proximity/depth explicitly so 'mouth touching' vs 'head far away'
# is a measurable difference, not hidden behind a coarse 'oral' label.
"mouth_genital_contact": "is the mouth in contact with the penis/genitals? options: lips on tip / tip in mouth / shaft in mouth / licking shaft / kissing / NOT in contact",
"mouth_genital_distance": "if NOT in contact, how far is the mouth from the genitals: touching (~0), very close (<5cm), near (~10-20cm), far (>20cm). If in contact, say 'contact'.",
"oral_depth": "how much of the penis is inside the mouth: none / tip only / about half / deep (throat)",
"tongue": "tongue visible and where: not visible / on tip / on shaft / flat / extended",
"hand_on_shaft": "hands on the penis/shaft: none / one hand (base/mid/tip) / two hands",
"gaze_up": "is the giver looking up at the partner, at the camera, down, or eyes closed",
# Penetration depth/angle as measurable values
"insertion_depth": "how deep is penetration: tip only / shallow / half / full/hilt / pulling out",
"insertion_angle": "angle of penetration relative to the body: vertical / horizontal / oblique",
# Handjob
"grip_style": "how the penis is held: loose / firm / two-handed / fingertips / not held",
"stroke_position": "where the hand is along the shaft: base / mid / tip / gliding full length",
# Solo
"self_touch_location": "where the subject is touching themselves: clitoris / labia / breasts / penetration / none",
"toy_use": "any toy/object in use and where: none / dildo / vibrator / other (location)",
}
DEFAULT_AXES = ", ".join(AXIS_DEFS)
# Shared identity/body/wardrobe/affect/camera/render axes (act-independent).
_BASE_AXES = [
"subject_count", "gender_mix", "age_appearance", "ethnicity_skin", "body_type",
"breast_size", "distinctive_features", "hair", "clothing_state",
"facial_expression", "gaze", "framing", "camera_angle", "scene",
"lighting_color", "art_style",
]
# Analysis profiles: act-specialized axis sets. The act-critical axes are made
# distance/proximity-aware so magnitude (e.g. mouth-to-penis distance) is captured.
# Pick on the node via `profile`; leave `axes` empty to use the profile, or set
# `axes` to override entirely.
PROFILES = {
"general": _BASE_AXES + [
"sexual_act", "body_orientation", "limb_arrangement", "penetration",
"contact_points", "genital_visibility", "pose",
],
"oral": _BASE_AXES + [
"body_orientation", "mouth_genital_contact", "mouth_genital_distance",
"oral_depth", "tongue", "hand_on_shaft", "gaze_up", "genital_visibility", "pose",
],
"penetration": _BASE_AXES + [
"sexual_act", "body_orientation", "limb_arrangement", "insertion_depth",
"insertion_angle", "penetration", "contact_points", "genital_visibility", "pose",
],
"handjob": _BASE_AXES + [
"body_orientation", "hand_on_shaft", "grip_style", "stroke_position",
"mouth_genital_contact", "gaze_up", "genital_visibility",
],
"solo": _BASE_AXES + [
"self_touch_location", "toy_use", "insertion_depth", "limb_arrangement",
"genital_visibility", "pose",
],
}
DEFAULT_AXES = ", ".join(PROFILES["general"])
# Cache loaded (model, processor) keyed by (path, precision) so the loop does not
# reload weights every iteration.
@@ -607,9 +662,12 @@ class QwenVLImageJudge:
# describe = reference only -> target description (first pass, seeds the
# initial prompt). compare = ref vs generated -> per-axis scoring.
"mode": (["compare", "describe"], {"default": "compare"}),
# Analysis profile: act-specialized axis set (distance-aware where it
# matters). `axes` below overrides it when non-empty.
"profile": (list(PROFILES.keys()), {"default": "general"}),
"model_path": ("STRING", {"default": DEFAULT_MODEL_PATH}),
"precision": (["bf16", "fp16", "fp8", "nf4"], {"default": "bf16"}),
"axes": ("STRING", {"default": DEFAULT_AXES, "multiline": True}),
"axes": ("STRING", {"default": "", "multiline": True}),
"max_new_tokens": ("INT", {"default": 1024, "min": 64, "max": 4096}),
"temperature": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.5, "step": 0.05}),
"swap_eval": ("BOOLEAN", {"default": True}),
@@ -629,12 +687,13 @@ class QwenVLImageJudge:
}
def judge(self, reference_image, mode, model_path, precision, axes,
max_new_tokens, temperature, swap_eval, generated_image=None,
max_new_tokens, temperature, swap_eval, profile="general", generated_image=None,
keep_loaded=True, auto_download=True,
report_dir="", run_tag="", prompt_used="", reference_description=""):
# `axes` overrides the profile when provided; otherwise use the profile's axis set.
axis_list = [a.strip() for a in re.split(r"[,\n]", axes) if a.strip()]
if not axis_list:
axis_list = [a.strip() for a in DEFAULT_AXES.split(",")]
axis_list = list(PROFILES.get(profile, PROFILES["general"]))
try:
resolved_path = _resolve_model_source(model_path, auto_download)