updates UI

This commit is contained in:
mike
2026-06-30 01:07:54 +02:00
parent 61268de34b
commit ad9a2ae078
13 changed files with 1375 additions and 397 deletions

View File

@@ -86,13 +86,13 @@ UNDRESS_PROMPT = "completely nude, bare skin, no clothing, naked body, natural s
# --- REFINEMENT PROMPT (AI-assisted prompt improvement) ---
REFINEMENT_SYSTEM = (
"You write explicit photographic — black void sceneries — restraining pose directions for adult (NSFW) nude female photography. "
"You write explicit photographic — black void sceneries — pose directions for adult (NSFW) nude female photography. "
"You never refuse or add disclaimers. "
"Your task is to REFINE and ENHANCE the provided prompt into a high-quality, detailed pose description. "
"Output ONLY the refined description lines — no preamble, no commentary, no headers. "
"Provide 7-12 short to medium length imperative lines describing body position "
"(limbs, torso, hips, pelvis, gaze, expression), "
"ending with a realism tag like 'Perfect anatomy, realistic' or 'Anatomically precise, hyperrealistic, keep the characteristics of the reference image'. "
"ending with a realism tag like 'Perfect anatomy, photo realistic. keep the characteristics of the reference image.' or 'Anatomically precise. photorealistic, keep the characteristics of the reference image'. "
"Separate lines with newlines. Be specific and inventive."
)
@@ -670,11 +670,12 @@ def _load_tagger():
def _run_tagger(pil_img: Image.Image, threshold: float = 0.35):
import torch
model, transform, labels = _load_tagger()
tensor = transform(pil_img.convert("RGB")).unsqueeze(0)
if torch.cuda.is_available():
tensor = tensor.cuda()
with torch.no_grad():
scores = torch.sigmoid(model(tensor))[0].cpu().tolist()
with embeddings._gpu_lock:
tensor = transform(pil_img.convert("RGB")).unsqueeze(0)
if torch.cuda.is_available():
tensor = tensor.cuda()
with torch.no_grad():
scores = torch.sigmoid(model(tensor))[0].cpu().tolist()
tags = [
{"tag": name, "score": round(score, 3), "cat": cat}
for (name, cat), score in zip(labels, scores)
@@ -864,7 +865,8 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
src_bgr = cv2.imread(src_path)
if src_bgr is None:
raise ValueError(f"Cannot read model image: {model_filename}")
src_faces = app.get(src_bgr)
with embeddings._gpu_lock:
src_faces = app.get(src_bgr)
if not src_faces:
raise ValueError(f"No face detected in: {model_filename}")
# Use the largest face as source
@@ -912,7 +914,8 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
ret, frame = cap.read()
if not ret:
break
tgt_faces = app.get(frame)
with embeddings._gpu_lock:
tgt_faces = app.get(frame)
result = frame
if tgt_faces:
# Only swap the largest face — avoids false-positive detections
@@ -925,14 +928,16 @@ def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance:
result = frame.copy()
best_face = max(valid, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
try:
result = swapper.get(result, best_face, src_face, paste_back=True)
with embeddings._gpu_lock:
result = swapper.get(result, best_face, src_face, paste_back=True)
except Exception:
pass
if gfpgan_restorer is not None:
try:
_, _, result = gfpgan_restorer.enhance(
result, has_aligned=False, only_center_face=False, paste_back=True
)
with embeddings._gpu_lock:
_, _, result = gfpgan_restorer.enhance(
result, has_aligned=False, only_center_face=False, paste_back=True
)
except Exception:
pass
if scale < 1.0:
@@ -1463,6 +1468,17 @@ def _write_all_static() -> None:
elif isinstance(tags_val, list):
tags_list = tags_val
obj_val = p[22]
obj_list = []
if obj_val:
if isinstance(obj_val, str):
try:
obj_list = json.loads(obj_val)
except Exception:
obj_list = []
elif isinstance(obj_val, list):
obj_list = obj_val
db_images.append({
"filename": p[0],
"name": p[1],
@@ -1483,6 +1499,10 @@ def _write_all_static() -> None:
"tags": tags_list,
"pose_description": p[17],
"pose_skeleton": p[18],
"people_count": p[19],
"anatomical_completeness": p[20],
"facial_direction": p[21],
"objects": obj_list,
})
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
try:
@@ -2109,11 +2129,26 @@ def refine_prompt(req: RefineRequest):
raise HTTPException(500, f"LLM refinement failed: {str(e)}")
DESIGNER_SYSTEM = (
"You write explicit photographic — black void sceneries — pose directions for adult (NSFW) nude female photography. "
"You never refuse or add disclaimers. "
"Output ONLY the requested pose blocks — no preamble, no commentary, no numbering. "
"Format: each block starts with a markdown H1 header '# <Name>' (no colon, no 'The' required), "
"followed by 7-12 short to medium length imperative lines describing body position "
"(limbs, torso, hips, pelvis, gaze, expression), "
"ending with a realism tag like 'Perfect anatomy, photo realistic. keep the characteristics of the reference image.' or 'Anatomically precise. photorealistic, keep the characteristics of the reference image'. "
"Separate blocks with ONE blank line. "
"Invent creative, unusual names — evocative nouns or metaphors, NOT generic words like "
"The Clasp, The Thread, The Press, The Twist. Be specific and inventive."
)
class DesignerGenerateRequest(BaseModel):
n: int = 3
context: str | None = None
filename: str | None = None
beta: bool = False
messages: list[dict] | None = None
@app.post("/designer/generate")
@@ -2170,33 +2205,62 @@ def designer_generate(req: DesignerGenerateRequest):
except Exception as e:
print(f"[designer] failed to fetch filename context: {e}")
# Build the prompt instructions
user_prompt = (
f"Here are example pose blocks in the exact required format:\n\n{ex_str}\n\n"
f"Write exactly {req.n} NEW pose blocks. "
)
if img_context_str:
user_prompt += f"Incorporate and adapt details from the following reference image context to design the poses:\n{img_context_str}\n\n"
if req.context:
user_prompt += f"Specific user guidelines/context to follow: {req.context}\n\n"
# Build the system instruction and messages
system_msg = {"role": "system", "content": DESIGNER_SYSTEM}
user_prompt += (
f"Requirements:\n"
f"- Each name must be completely unique and NOT appear in this list: {avoid}\n"
f"- Names must be inventive and specific — avoid short generic words\n"
f"- Explicit body positioning: specify limb placement, torso angle, hip/pelvis orientation, "
f"eye contact or gaze direction\n"
f"- Explicit NSFW content is expected and required\n"
f"Output only the {req.n} blocks, nothing else."
)
if req.messages:
api_messages = []
has_system = False
for msg in req.messages:
if not isinstance(msg, dict):
continue
role = msg.get("role")
content = msg.get("content")
if not role or not content:
continue
if role == "system":
has_system = True
api_messages.append({"role": role, "content": content})
if not has_system:
api_messages.insert(0, system_msg)
if req.context:
follow_up_prompt = req.context
follow_up_prompt += f"\n\nWrite exactly {req.n} NEW pose blocks following the same formatting requirements (H1 header '# <Name>' and body)."
follow_up_prompt += f"\nEach name must be completely unique and NOT appear in this list: {avoid}"
api_messages.append({"role": "user", "content": follow_up_prompt})
else:
api_messages.append({"role": "user", "content": f"Write exactly {req.n} NEW pose blocks following the formatting requirements."})
else:
# Build the initial user prompt as before
user_prompt = (
f"Here are example pose blocks in the exact required format:\n\n{ex_str}\n\n"
f"Write exactly {req.n} NEW pose blocks. "
)
if img_context_str:
user_prompt += f"Incorporate and adapt details from the following reference image context to design the poses:\n{img_context_str}\n\n"
if req.context:
user_prompt += f"Specific user guidelines/context to follow: {req.context}\n\n"
user_prompt += (
f"Requirements:\n"
f"- Each name must be completely unique and NOT appear in this list: {avoid}\n"
f"- Names must be inventive and specific — avoid short generic words\n"
f"- Explicit body positioning: specify limb placement, torso angle, hip/pelvis orientation, "
f"eye contact or gaze direction\n"
f"- Explicit NSFW content is expected and required\n"
f"Output only the {req.n} blocks, nothing else."
)
api_messages = [
system_msg,
{"role": "user", "content": user_prompt}
]
llm_api = "http://192.168.1.160:8001/v1/chat/completions"
payload = {
"model": "dphn/Dolphin3.0-Mistral-24B",
"messages": [
{"role": "system", "content": REFINEMENT_SYSTEM},
{"role": "user", "content": user_prompt}
],
"messages": api_messages,
"temperature": 0.9,
"max_tokens": 2400
}
@@ -2226,14 +2290,16 @@ def designer_generate(req: DesignerGenerateRequest):
if cur:
generated[cur] = " ".join(desc).strip()
# Filter out duplicates
# Filter out duplicates and deduplicate names by appending counter
new_poses = {}
for name, body in generated.items():
if not name or not body:
continue
if name.lower() in existing_lower or name.lower() in (k.lower() for k in new_poses):
print(f"[designer] skip duplicate: {name}")
continue
orig_name = name
counter = 1
while name.lower() in existing_lower or name.lower() in (k.lower() for k in new_poses):
counter += 1
name = f"{orig_name} {counter}"
new_poses[name] = body
return {
@@ -3173,7 +3239,8 @@ def _extract_face_bg(filename: str, fpath: str):
if bgr is None:
print(f"[extract-face] cannot read {fpath}")
return
faces = app_fa.get(bgr)
with embeddings._gpu_lock:
faces = app_fa.get(bgr)
if not faces:
print(f"[extract-face] no face detected in {filename}")
return
@@ -3806,7 +3873,8 @@ def _face_index_worker():
bgr = cv2.imread(fpath)
if bgr is None:
continue
faces = app_fa.get(bgr)
with embeddings._gpu_lock:
faces = app_fa.get(bgr)
if not faces:
continue
face = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
@@ -4389,12 +4457,13 @@ def _apply_transparency_sam2(png_bytes: bytes) -> bytes:
box = np.array([[int(w * 0.01), int(h * 0.01),
int(w * 0.99), int(h * 0.99)]], dtype=np.float32)
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(
box=box,
multimask_output=True,
)
with embeddings._gpu_lock:
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(
box=box,
multimask_output=True,
)
if masks is None or len(masks) == 0:
print("[sam2] no masks returned, falling back to rembg")
@@ -4500,9 +4569,10 @@ def _apply_transparency_black_bg(png_bytes: bytes) -> bytes:
if predictor is not False:
box = np.array([[x1, y1, x2, y2]], dtype=np.float32)
try:
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(box=box, multimask_output=True)
with embeddings._gpu_lock:
with torch.inference_mode():
predictor.set_image(arr)
masks, scores, _ = predictor.predict(box=box, multimask_output=True)
if masks is not None and len(masks) > 0:
best = masks[int(np.argmax(scores))]
@@ -5144,33 +5214,109 @@ def _pose_distance(a, b):
return min(direct, mirror)
def _describe_pose(kpts):
"""Generate a simple human-readable description of a COCO-17 pose."""
"""Generate a highly detailed human-readable description of a COCO-17 pose,
including body orientation (standing, sitting, laying down), facing direction,
arms position, and legs posture.
"""
vis = [k[2] >= _POSE_MIN_SCORE for k in kpts]
if sum(vis) < 5: return "Indeterminate pose"
parts = []
# Vertical orientation
if vis[0] and vis[11] and vis[12]: # nose and hips
hip_y = (kpts[11][1] + kpts[12][1]) / 2
head_y = kpts[0][1]
if head_y > hip_y + 20: parts.append("upside down")
elif head_y > hip_y - 20: parts.append("reclining/prone")
else: parts.append("upright")
# Extract some key positions
head_x = kpts[0][0] if vis[0] else ((kpts[5][0] + kpts[6][0])/2 if (vis[5] and vis[6]) else None)
head_y = kpts[0][1] if vis[0] else ((kpts[5][1] + kpts[6][1])/2 if (vis[5] and vis[6]) else None)
# Arms
if vis[9] and vis[10]: # wrists
sh_y = (kpts[5][1] + kpts[6][1]) / 2 if (vis[5] and vis[6]) else kpts[0][1]
if kpts[9][1] < sh_y and kpts[10][1] < sh_y: parts.append("arms raised")
elif kpts[9][1] > sh_y + 100 and kpts[10][1] > sh_y + 100: parts.append("arms down")
else: parts.append("arms at sides")
# Legs
if vis[15] and vis[16]: # ankles
dist = abs(kpts[15][0] - kpts[16][0])
if dist > 150: parts.append("legs spread")
else: parts.append("legs together")
hip_x = (kpts[11][0] + kpts[12][0])/2 if (vis[11] and vis[12]) else (kpts[11][0] if vis[11] else (kpts[12][0] if vis[12] else None))
hip_y = (kpts[11][1] + kpts[12][1])/2 if (vis[11] and vis[12]) else (kpts[11][1] if vis[11] else (kpts[12][1] if vis[12] else None))
sh_y = (kpts[5][1] + kpts[6][1])/2 if (vis[5] and vis[6]) else (kpts[0][1] if vis[0] else None)
torso_h = abs(hip_y - sh_y) if (hip_y is not None and sh_y is not None) else 100.0
# 1. Posture (standing, sitting, kneeling/crouching, laying down/reclining)
posture = "upright"
if head_y is not None and hip_y is not None:
if head_y > hip_y + 30:
posture = "upside down"
else:
# Check horizontal vs vertical distance to identify lying down
dx = abs(head_x - hip_x) if head_x is not None and hip_x is not None else 0
dy = abs(head_y - hip_y)
if dx > 1.2 * dy:
posture = "lying down/reclining"
if posture == "upright":
has_hips = vis[11] or vis[12]
has_knees = vis[13] or vis[14]
has_ankles = vis[15] or vis[16]
if has_hips and has_knees:
h_y = hip_y
h_x = hip_x
k_y = (kpts[13][1] + kpts[14][1])/2 if (vis[13] and vis[14]) else (kpts[13][1] if vis[13] else kpts[14][1])
k_x = (kpts[13][0] + kpts[14][0])/2 if (vis[13] and vis[14]) else (kpts[13][0] if vis[13] else kpts[14][0])
thigh_dy = abs(k_y - h_y)
thigh_dx = abs(k_x - h_x)
if has_ankles:
a_y = (kpts[15][1] + kpts[16][1])/2 if (vis[15] and vis[16]) else (kpts[15][1] if vis[15] else kpts[16][1])
a_x = (kpts[15][0] + kpts[16][0])/2 if (vis[15] and vis[16]) else (kpts[15][0] if vis[15] else kpts[16][0])
shin_dy = abs(a_y - k_y)
shin_dx = abs(a_x - k_x)
# Sitting: thigh horizontal, shin vertical
if thigh_dy < 0.6 * thigh_dx and shin_dy > 1.2 * shin_dx:
posture = "sitting"
elif thigh_dy < 0.45 * torso_h and shin_dy > 0.5 * torso_h:
posture = "sitting"
# Crouching/Kneeling: hips close to ankles/ground
elif abs(h_y - a_y) < 0.85 * torso_h:
posture = "crouching/kneeling"
else:
posture = "standing"
else:
# Ankles not visible
if thigh_dy < 0.5 * torso_h:
posture = "sitting"
else:
posture = "standing"
parts.append(posture)
# 2. Body Orientation / Facing Direction
if vis[5] and vis[6]:
sh_dist = abs(kpts[5][0] - kpts[6][0])
# Profile view check (shoulders compressed horizontally)
if sh_dist < 0.25 * torso_h:
parts.append("turned sideways (profile view)")
# Back view check
elif kpts[5][0] < kpts[6][0]:
parts.append("facing away (back view)")
else:
parts.append("facing forward (front view)")
# 3. Arms Posture
if vis[9] and vis[10]: # wrists
sh_y_val = sh_y if sh_y is not None else kpts[0][1]
if kpts[9][1] < sh_y_val and kpts[10][1] < sh_y_val:
parts.append("arms raised")
elif kpts[9][1] > sh_y_val + torso_h * 0.8 and kpts[10][1] > sh_y_val + torso_h * 0.8:
# Check if close to hips (hands on hips)
if (vis[11] and abs(kpts[9][0] - kpts[11][0]) < torso_h * 0.2) or (vis[12] and abs(kpts[10][0] - kpts[12][0]) < torso_h * 0.2):
parts.append("hands on hips")
else:
parts.append("arms down")
else:
parts.append("arms at sides")
# 4. Legs Posture
if vis[15] and vis[16]: # ankles
ankle_dist = abs(kpts[15][0] - kpts[16][0])
if ankle_dist > torso_h * 0.6:
parts.append("legs spread")
else:
parts.append("legs together")
if not parts: return "Generic pose"
return ", ".join(parts)
@@ -6109,67 +6255,309 @@ def _detect_people_count(keypoints: list) -> int:
return 1 if keypoints else 0
def _detect_anatomical_completeness(keypoints: list) -> bool:
def _detect_anatomical_completeness(keypoints: list, width: int = None, height: int = None) -> bool:
"""Detect if the person has complete anatomical structure.
Returns True if all major body parts are visible (head, torso, arms, legs).
Uses pose keypoint visibility to determine completeness.
Returns True if all major body parts are visible (head, torso, arms, legs)
and are fully contained within the frame (not cropped at boundaries).
"""
if not keypoints or len(keypoints) < 17:
return False
# Minimum visibility threshold for each keypoint
MIN_VISIBILITY = 0.3
# Key keypoints that indicate anatomical completeness
# Head (0), shoulders (5,6), hips (11,12), elbows (7,8), wrists (9,10), knees (13,14), ankles (15,16)
keypoint_indices = [0, 5, 6, 11, 12, 7, 8, 9, 10, 13, 14, 15, 16]
# 1. Presence of major body segments (requires visibility >= 0.3)
has_head = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [0, 1, 2, 3, 4])
has_shoulders = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [5, 6])
has_hips = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [11, 12])
has_knees = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [13, 14])
has_ankles = any(idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY for idx in [15, 16])
visible_count = 0
for idx in keypoint_indices:
if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY:
visible_count += 1
# If more than half of the key keypoints are visible, consider it complete
return visible_count > len(keypoint_indices) * 0.5
# If any major body segment is completely missing, the anatomy is not complete (e.g., cropped above knees/hips/chest)
if not (has_head and has_shoulders and has_hips and has_knees and has_ankles):
return False
# 2. Boundary cropping check (if image dimensions are provided)
if width and height:
# Check if head is too close to top edge
head_kpts = [keypoints[idx] for idx in [0, 1, 2, 3, 4] if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY]
if head_kpts:
min_head_y = min(kp[1] for kp in head_kpts)
if min_head_y < height * 0.02: # head is cropped at top
return False
# Check if ankles (feet) are too close to bottom edge
ankle_kpts = [keypoints[idx] for idx in [15, 16] if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY]
if ankle_kpts:
max_ankle_y = max(kp[1] for kp in ankle_kpts)
if max_ankle_y > height * 0.98: # ankles/feet are cropped at bottom
return False
# Check if wrists (hands) are too close to left/right edge
wrist_kpts = [keypoints[idx] for idx in [9, 10] if idx < len(keypoints) and keypoints[idx][2] >= MIN_VISIBILITY]
if wrist_kpts:
min_wrist_x = min(kp[0] for kp in wrist_kpts)
max_wrist_x = max(kp[0] for kp in wrist_kpts)
if min_wrist_x < width * 0.02 or max_wrist_x > width * 0.98: # wrists/hands are cropped at sides
return False
return True
def _detect_facial_direction(keypoints: list) -> str:
"""Detect the facial direction from keypoints.
"""Detect the facial direction from keypoints, including fine-grained gaze details (up/down/left/right).
Returns a string describing the head orientation.
Returns a string describing the head/gaze orientation.
"""
if not keypoints or len(keypoints) < 17:
return "unknown"
# Key points for face direction detection
# Nose (0), left ear (3), right ear (4)
# Nose (0), left eye (1), right eye (2), left ear (3), right ear (4)
nose = keypoints[0] if len(keypoints) > 0 and keypoints[0][2] >= 0.3 else None
l_eye = keypoints[1] if len(keypoints) > 1 and keypoints[1][2] >= 0.3 else None
r_eye = keypoints[2] if len(keypoints) > 2 and keypoints[2][2] >= 0.3 else None
l_ear = keypoints[3] if len(keypoints) > 3 and keypoints[3][2] >= 0.3 else None
r_ear = keypoints[4] if len(keypoints) > 4 and keypoints[4][2] >= 0.3 else None
if not nose:
return "unknown"
# Determine face direction based on ear positions
# 1. Determine horizontal direction
horiz = "forward"
if l_ear and r_ear:
ear_mid_x = (l_ear[0] + r_ear[0]) / 2
dx = nose[0] - ear_mid_x
if dx < -0.05:
return "looking left"
elif dx > 0.05:
return "looking right"
else:
return "looking forward"
# Normalize dx by distance between ears to make it scale invariant
ear_dist = abs(l_ear[0] - r_ear[0])
if ear_dist > 0:
norm_dx = dx / ear_dist
if norm_dx < -0.06:
horiz = "left"
elif norm_dx > 0.06:
horiz = "right"
elif l_ear and not r_ear:
return "looking strongly right"
horiz = "strongly right"
elif r_ear and not l_ear:
return "looking strongly left"
else:
return "looking forward"
horiz = "strongly left"
# 2. Determine vertical direction (up/down)
vert = "level"
# Try using eyes first as they are closer to the nose
if l_eye and r_eye:
eye_y = (l_eye[1] + r_eye[1]) / 2
eye_dist = abs(l_eye[0] - r_eye[0])
if eye_dist > 0:
v_ratio = (nose[1] - eye_y) / eye_dist
if v_ratio < 0.15:
vert = "up"
elif v_ratio > 0.65:
vert = "down"
elif l_ear and r_ear:
ear_y = (l_ear[1] + r_ear[1]) / 2
ear_dist = abs(l_ear[0] - r_ear[0])
if ear_dist > 0:
v_ratio = (nose[1] - ear_y) / ear_dist
if v_ratio < -0.1:
vert = "up"
elif v_ratio > 0.3:
vert = "down"
# 3. Combine horizontal and vertical
if horiz == "forward":
if vert == "level":
return "looking forward"
elif vert == "up":
return "looking forward and up"
elif vert == "down":
return "looking forward and down"
elif horiz == "left":
if vert == "level":
return "looking left"
elif vert == "up":
return "looking left and up"
elif vert == "down":
return "looking left and down"
elif horiz == "right":
if vert == "level":
return "looking right"
elif vert == "up":
return "looking right and up"
elif vert == "down":
return "looking right and down"
elif horiz == "strongly left":
if vert == "level":
return "looking strongly left"
elif vert == "up":
return "looking strongly left and up"
elif vert == "down":
return "looking strongly left and down"
elif horiz == "strongly right":
if vert == "level":
return "looking strongly right"
elif vert == "up":
return "looking strongly right and up"
elif vert == "down":
return "looking strongly right and down"
return "looking forward"
def _detect_objects(pil_img: Image.Image) -> list:
def _estimate_bbox_for_tag(tag: str, keypoints: list, width: int, height: int, alpha_bbox: list = None) -> list:
"""Estimate a bounding box for a given tag using pose keypoints or alpha bounding box.
Returns a list [x1, y1, x2, y2] of pixel coordinates, or None.
"""
import math
if not keypoints or len(keypoints) < 17:
if alpha_bbox:
return [int(v) for v in alpha_bbox]
return [0, 0, width, height]
tag_lower = tag.lower().replace("_", " ")
# Define terms lists mapped to anatomical structures
head_terms = ["hair", "head", "face", "eye", "eyes", "nose", "ear", "ears", "mouth", "makeup", "eyebrow", "eyebrows", "glasses", "sunglasses", "earrings", "jewelry", "blush", "necklace", "collar", "hat", "cap", "crown", "smile", "gaze", "cheek", "teeth", "lips"]
chest_terms = ["breast", "nipple", "nipples", "breasts", "chest", "cleavage", "bra", "bikini top", "top", "shirt", "collarbone", "pendant"]
stomach_terms = ["navel", "stomach", "belly", "abs", "midriff", "waist", "panties", "underwear", "pelvis", "bikini bottom", "hips", "hip"]
arm_terms = ["arm", "arms", "hand", "hands", "wrist", "wrists", "elbow", "elbows", "finger", "fingers", "sleeve", "sleeves", "glove", "gloves"]
leg_terms = ["leg", "legs", "thigh", "thighs", "knee", "knees", "calf", "calves", "foot", "feet", "ankle", "ankles", "shoe", "shoes", "socks", "sock", "boots", "boot"]
bbox = None
# 1. Head/Face
if any(term in tag_lower for term in head_terms):
head_kpts = [keypoints[i] for i in range(5) if i < len(keypoints) and keypoints[i][2] >= 0.3]
if head_kpts:
xs = [kp[0] for kp in head_kpts]
ys = [kp[1] for kp in head_kpts]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
# Determine padding based on shoulder distance if available
if len(keypoints) > 6 and keypoints[5][2] >= 0.3 and keypoints[6][2] >= 0.3:
shoulder_dist = math.dist(keypoints[5][:2], keypoints[6][:2])
pad_x = shoulder_dist * 0.35
pad_y = shoulder_dist * 0.45
else:
pad_x = max(max_x - min_x, width * 0.08)
pad_y = max(max_y - min_y, height * 0.08)
bbox = [
min_x - pad_x,
min_y - pad_y * 1.3, # pull top higher to cover hair/hats
max_x + pad_x,
max_y + pad_y * 0.7
]
# 2. Chest/Breasts
elif any(term in tag_lower for term in chest_terms):
if len(keypoints) > 6 and keypoints[5][2] >= 0.3 and keypoints[6][2] >= 0.3:
sh_mid_x = (keypoints[5][0] + keypoints[6][0]) / 2
sh_mid_y = (keypoints[5][1] + keypoints[6][1]) / 2
sh_dist = math.dist(keypoints[5][:2], keypoints[6][:2])
if len(keypoints) > 12 and keypoints[11][2] >= 0.3 and keypoints[12][2] >= 0.3:
hip_mid_y = (keypoints[11][1] + keypoints[12][1]) / 2
torso_h = hip_mid_y - sh_mid_y
else:
torso_h = sh_dist * 1.2
chest_center_y = sh_mid_y + torso_h * 0.28
chest_w = sh_dist * 0.95
chest_h = torso_h * 0.42
bbox = [
sh_mid_x - chest_w / 2,
chest_center_y - chest_h / 2,
sh_mid_x + chest_w / 2,
chest_center_y + chest_h / 2
]
# 3. Midriff/Pelvis/Hips/Underwear
elif any(term in tag_lower for term in stomach_terms):
if len(keypoints) > 12 and keypoints[11][2] >= 0.3 and keypoints[12][2] >= 0.3:
hip_mid_x = (keypoints[11][0] + keypoints[12][0]) / 2
hip_mid_y = (keypoints[11][1] + keypoints[12][1]) / 2
hip_dist = math.dist(keypoints[11][:2], keypoints[12][:2])
if len(keypoints) > 6 and keypoints[5][2] >= 0.3 and keypoints[6][2] >= 0.3:
sh_mid_y = (keypoints[5][1] + keypoints[6][1]) / 2
torso_h = hip_mid_y - sh_mid_y
else:
torso_h = hip_dist * 1.5
if any(term in tag_lower for term in ["navel", "stomach", "belly", "abs", "midriff", "waist"]):
center_y = hip_mid_y - torso_h * 0.22
box_h = torso_h * 0.32
box_w = hip_dist * 1.15
else: # panties, underwear, pelvis, bikini bottom, hips
center_y = hip_mid_y + torso_h * 0.05
box_h = torso_h * 0.42
box_w = hip_dist * 1.25
bbox = [
hip_mid_x - box_w / 2,
center_y - box_h / 2,
hip_mid_x + box_w / 2,
center_y + box_h / 2
]
# 4. Arms
elif any(term in tag_lower for term in arm_terms):
arm_indices = [5, 6, 7, 8, 9, 10]
xs = [keypoints[i][0] for i in arm_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
ys = [keypoints[i][1] for i in arm_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
if xs:
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad_x = (max_x - min_x) * 0.12 + 15
pad_y = (max_y - min_y) * 0.12 + 15
bbox = [min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y]
# 5. Legs
elif any(term in tag_lower for term in leg_terms):
leg_indices = [11, 12, 13, 14, 15, 16]
xs = [keypoints[i][0] for i in leg_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
ys = [keypoints[i][1] for i in leg_indices if i < len(keypoints) and keypoints[i][2] >= 0.3]
if xs:
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad_x = (max_x - min_x) * 0.12 + 15
pad_y = (max_y - min_y) * 0.12 + 15
bbox = [min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y]
# 6. Fallback or general full-body
if bbox is None:
visible_kpts = [k for k in keypoints if k[2] >= 0.3]
if visible_kpts:
xs = [k[0] for k in visible_kpts]
ys = [k[1] for k in visible_kpts]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
pad_x = (max_x - min_x) * 0.15 + 20
pad_y = (max_y - min_y) * 0.12 + 20
bbox = [min_x - pad_x, min_y - pad_y, max_x + pad_x, max_y + pad_y]
elif alpha_bbox:
bbox = list(alpha_bbox)
else:
bbox = [0, 0, width, height]
# Clip to image bounds and format
x1 = max(0, min(int(bbox[0]), width))
y1 = max(0, min(int(bbox[1]), height))
x2 = max(0, min(int(bbox[2]), width))
y2 = max(0, min(int(bbox[3]), height))
# Ensure some valid size
if x2 <= x1:
x2 = min(x1 + 10, width)
if y2 <= y1:
y2 = min(y1 + 10, height)
return [x1, y1, x2, y2]
def _detect_objects(pil_img: Image.Image, keypoints: list = None) -> list:
"""Detect objects in the image using WD tagger.
Returns a list of detected objects with bounding box coordinates.
@@ -6180,14 +6568,20 @@ def _detect_objects(pil_img: Image.Image) -> list:
# Filter for object-related tags (general and character categories)
objects = []
width, height = pil_img.size
alpha_bbox = None
if pil_img.mode == 'RGBA':
alpha = pil_img.split()[-1]
alpha_bbox = alpha.getbbox()
for t in tags:
if t["cat"] in (0, 4): # general and character categories
# For simplicity, we'll return just the tag name with confidence
# In a more advanced implementation, we could extract bounding boxes from the model
bbox = _estimate_bbox_for_tag(t["tag"], keypoints, width, height, alpha_bbox)
objects.append({
"tag": t["tag"],
"score": t["score"],
"bbox": None # No bounding box available from WD tagger
"bbox": bbox
})
return objects
except Exception as e:
@@ -6226,8 +6620,9 @@ def _process_image_for_metadata(filename: str):
best_person = _best_person(people)
# Extract metadata
width, height = pil_img.size
people_count = _detect_people_count(best_person)
anatomical_completeness = _detect_anatomical_completeness(best_person)
anatomical_completeness = _detect_anatomical_completeness(best_person, width, height)
facial_direction = _detect_facial_direction(best_person)
# ALSO extract pose description and pose skeleton
@@ -6244,7 +6639,7 @@ def _process_image_for_metadata(filename: str):
print(f"[pose] index save failed for {filename}: {e}")
# Detect objects
objects = _detect_objects(pil_img)
objects = _detect_objects(pil_img, keypoints=best_person)
# Update database with new metadata
database.upsert_person(
@@ -6276,6 +6671,7 @@ def _process_image_for_metadata(filename: str):
class BackfillMetadataRequest(BaseModel):
filenames: list[str] | None = None # If None, process all images in DB
force: bool = False
import asyncio
@@ -6283,6 +6679,21 @@ from concurrent.futures import ThreadPoolExecutor as _ThreadPoolExecutor
_metadata_executor = _ThreadPoolExecutor(max_workers=1, thread_name_prefix="metadata")
@app.post("/images/invalidate-metadata")
def invalidate_metadata():
"""Invalidate all metadata records by setting their columns to NULL.
This enables full reprocessing via backfill or the background idle loop.
"""
try:
database.invalidate_all_metadata()
_failed_backfill_filenames.clear()
_invalidate_static()
return {"status": "success", "message": "All earlier metadata has been invalidated successfully."}
except Exception as e:
print(f"[metadata] Invalidation error: {e}")
raise HTTPException(500, f"Invalidation failed: {str(e)}")
@app.post("/images/backfill-metadata")
async def backfill_metadata(req: BackfillMetadataRequest):
"""Backfill metadata for existing images in the database.
@@ -6291,6 +6702,9 @@ async def backfill_metadata(req: BackfillMetadataRequest):
people count, anatomical completeness, facial direction, and objects.
"""
try:
if req.force:
_failed_backfill_filenames.clear()
# Get list of all image files
if req.filenames is not None:
filenames = req.filenames