updates UI
This commit is contained in:
@@ -799,5 +799,308 @@ class TestAPIRegression(unittest.TestCase):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_14_static_serialization_of_deep_metadata(self):
|
||||
import edit_api
|
||||
import database
|
||||
import json
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
# Create dummy image and person record
|
||||
fn = "test_deep_meta_serialization_123.png"
|
||||
fpath = os.path.join(self.output_dir, fn)
|
||||
Image.new("RGB", (10, 10)).save(fpath)
|
||||
|
||||
conn = database.get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO person (filename, filepath, group_id, people_count, anatomical_completeness, facial_direction, objects)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""", (fn, fpath, "test_group", 2, False, "front", json.dumps([{"tag": "hat", "score": 0.9}])))
|
||||
conn.commit()
|
||||
finally:
|
||||
cur.close()
|
||||
database._put_db_connection(conn)
|
||||
|
||||
try:
|
||||
# Trigger write static images.json
|
||||
edit_api._write_all_static()
|
||||
|
||||
# Read static file and check that the deep metadata properties are serialized correctly
|
||||
static_file = os.path.join(self.output_dir, "_data", "images.json")
|
||||
self.assertTrue(os.path.exists(static_file))
|
||||
with open(static_file, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
imgs = data.get("images", [])
|
||||
matched = [x for x in imgs if x["filename"] == fn]
|
||||
self.assertEqual(len(matched), 1)
|
||||
item = matched[0]
|
||||
|
||||
# Assert that the serialized dictionary has all of the deep metadata fields
|
||||
self.assertEqual(item.get("people_count"), 2)
|
||||
self.assertEqual(item.get("anatomical_completeness"), False)
|
||||
self.assertEqual(item.get("facial_direction"), "front")
|
||||
self.assertEqual(item.get("objects"), [{"tag": "hat", "score": 0.9}])
|
||||
|
||||
finally:
|
||||
# Clean up
|
||||
if os.path.exists(fpath):
|
||||
os.remove(fpath)
|
||||
conn = database.get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("DELETE FROM person WHERE filename = %s", (fn,))
|
||||
conn.commit()
|
||||
finally:
|
||||
cur.close()
|
||||
database._put_db_connection(conn)
|
||||
# Re-generate static to clean up the dummy entry
|
||||
edit_api._write_all_static()
|
||||
|
||||
def test_15_anatomical_completeness_refinement(self):
|
||||
import edit_api
|
||||
|
||||
# 17 keypoints matching COCO-17 format: [x, y, score]
|
||||
# Nose (0), shoulders (5,6), elbows (7,8), wrists (9,10), hips (11,12), knees (13,14), ankles (15,16)
|
||||
# All of them are visible with score >= 0.3, and well within bounds [0.1, 0.9]
|
||||
complete_kpts = [
|
||||
[50, 10, 0.9], # Nose (0)
|
||||
[48, 11, 0.9], # L_Eye (1)
|
||||
[52, 11, 0.9], # R_Eye (2)
|
||||
[46, 12, 0.9], # L_Ear (3)
|
||||
[54, 12, 0.9], # R_Ear (4)
|
||||
[40, 20, 0.9], # L_Shoulder (5)
|
||||
[60, 20, 0.9], # R_Shoulder (6)
|
||||
[35, 35, 0.9], # L_Elbow (7)
|
||||
[65, 35, 0.9], # R_Elbow (8)
|
||||
[30, 50, 0.9], # L_Wrist (9)
|
||||
[70, 50, 0.9], # R_Wrist (10)
|
||||
[42, 55, 0.9], # L_Hip (11)
|
||||
[58, 55, 0.9], # R_Hip (12)
|
||||
[40, 75, 0.9], # L_Knee (13)
|
||||
[60, 75, 0.9], # R_Knee (14)
|
||||
[40, 90, 0.9], # L_Ankle (15)
|
||||
[60, 90, 0.9], # R_Ankle (16)
|
||||
]
|
||||
|
||||
# Test complete pose
|
||||
res1 = edit_api._detect_anatomical_completeness(complete_kpts, 100, 100)
|
||||
self.assertTrue(res1)
|
||||
|
||||
# Test partial pose: missing knees and ankles
|
||||
partial_kpts = [list(kp) for kp in complete_kpts]
|
||||
for idx in [13, 14, 15, 16]:
|
||||
partial_kpts[idx][2] = 0.1 # Hide them
|
||||
res2 = edit_api._detect_anatomical_completeness(partial_kpts, 100, 100)
|
||||
self.assertFalse(res2)
|
||||
|
||||
# Test cropped pose: ankles too close to bottom edge (y=99 in height=100)
|
||||
cropped_bottom_kpts = [list(kp) for kp in complete_kpts]
|
||||
cropped_bottom_kpts[15][1] = 99
|
||||
cropped_bottom_kpts[16][1] = 99
|
||||
res3 = edit_api._detect_anatomical_completeness(cropped_bottom_kpts, 100, 100)
|
||||
self.assertFalse(res3)
|
||||
|
||||
def test_16_object_bbox_estimation(self):
|
||||
import edit_api
|
||||
|
||||
# Define some realistic keypoints
|
||||
kpts = [
|
||||
[50, 10, 0.9], # Nose (0)
|
||||
[48, 11, 0.9], # L_Eye (1)
|
||||
[52, 11, 0.9], # R_Eye (2)
|
||||
[46, 12, 0.9], # L_Ear (3)
|
||||
[54, 12, 0.9], # R_Ear (4)
|
||||
[40, 20, 0.9], # L_Shoulder (5)
|
||||
[60, 20, 0.9], # R_Shoulder (6)
|
||||
[35, 35, 0.9], # L_Elbow (7)
|
||||
[65, 35, 0.9], # R_Elbow (8)
|
||||
[30, 50, 0.9], # L_Wrist (9)
|
||||
[70, 50, 0.9], # R_Wrist (10)
|
||||
[42, 55, 0.9], # L_Hip (11)
|
||||
[58, 55, 0.9], # R_Hip (12)
|
||||
[40, 75, 0.9], # L_Knee (13)
|
||||
[60, 75, 0.9], # R_Knee (14)
|
||||
[40, 90, 0.9], # L_Ankle (15)
|
||||
[60, 90, 0.9], # R_Ankle (16)
|
||||
]
|
||||
|
||||
# Test head / hair tag bbox estimation
|
||||
bbox_hair = edit_api._estimate_bbox_for_tag("long_hair", kpts, 100, 100)
|
||||
self.assertIsNotNone(bbox_hair)
|
||||
self.assertEqual(len(bbox_hair), 4)
|
||||
# Bounding box should surround the head/face region, which is y-centered around 10-12.
|
||||
# So upper y (bbox_hair[1]) should be relatively small (close to 0/top)
|
||||
self.assertLess(bbox_hair[1], 15)
|
||||
|
||||
# Test chest tag bbox estimation
|
||||
bbox_breasts = edit_api._estimate_bbox_for_tag("breasts", kpts, 100, 100)
|
||||
self.assertIsNotNone(bbox_breasts)
|
||||
self.assertEqual(len(bbox_breasts), 4)
|
||||
# Should be below shoulders (y=20) and above hips (y=55). Center around 30.
|
||||
self.assertGreater(bbox_breasts[1], 15)
|
||||
self.assertLess(bbox_breasts[3], 55)
|
||||
|
||||
# Test stomach tag bbox estimation
|
||||
bbox_navel = edit_api._estimate_bbox_for_tag("navel", kpts, 100, 100)
|
||||
self.assertIsNotNone(bbox_navel)
|
||||
self.assertEqual(len(bbox_navel), 4)
|
||||
# Should be around the stomach area, i.e. between 35 and 55.
|
||||
self.assertGreater(bbox_navel[1], 25)
|
||||
|
||||
# Test arms tag bbox estimation
|
||||
bbox_arms = edit_api._estimate_bbox_for_tag("sleeves", kpts, 100, 100)
|
||||
self.assertIsNotNone(bbox_arms)
|
||||
self.assertEqual(len(bbox_arms), 4)
|
||||
|
||||
# Test legs tag bbox estimation
|
||||
bbox_legs = edit_api._estimate_bbox_for_tag("thighs", kpts, 100, 100)
|
||||
self.assertIsNotNone(bbox_legs)
|
||||
self.assertEqual(len(bbox_legs), 4)
|
||||
|
||||
def test_17_gaze_and_pose_description_enhancements(self):
|
||||
import edit_api
|
||||
|
||||
# Test 1: Standard upright front facing pose with direct gaze
|
||||
standing_kpts = [
|
||||
[50, 12.5, 0.9], # Nose (0)
|
||||
[52, 11, 0.9], # L_Eye (1)
|
||||
[48, 11, 0.9], # R_Eye (2)
|
||||
[54, 12, 0.9], # L_Ear (3)
|
||||
[46, 12, 0.9], # R_Ear (4)
|
||||
[60, 20, 0.9], # L_Shoulder (5)
|
||||
[40, 20, 0.9], # R_Shoulder (6)
|
||||
[65, 35, 0.9], # L_Elbow (7)
|
||||
[35, 35, 0.9], # R_Elbow (8)
|
||||
[70, 50, 0.9], # L_Wrist (9)
|
||||
[30, 50, 0.9], # R_Wrist (10)
|
||||
[58, 55, 0.9], # L_Hip (11)
|
||||
[42, 55, 0.9], # R_Hip (12)
|
||||
[60, 75, 0.9], # L_Knee (13)
|
||||
[40, 75, 0.9], # R_Knee (14)
|
||||
[60, 90, 0.9], # L_Ankle (15)
|
||||
[40, 90, 0.9], # R_Ankle (16)
|
||||
]
|
||||
|
||||
pose_desc = edit_api._describe_pose(standing_kpts)
|
||||
self.assertIn("standing", pose_desc)
|
||||
self.assertIn("facing forward", pose_desc)
|
||||
|
||||
gaze = edit_api._detect_facial_direction(standing_kpts)
|
||||
self.assertEqual(gaze, "looking forward")
|
||||
|
||||
# Test 2: Gaze look left and up
|
||||
left_up_kpts = [list(kp) for kp in standing_kpts]
|
||||
# To look left: nose (0) moves to the left (smaller X than ear midpoint 50)
|
||||
left_up_kpts[0][0] = 45 # Nose X
|
||||
# To look up: nose (0) moves up (y-level close to eye level y=11)
|
||||
left_up_kpts[0][1] = 11.2 # Nose Y
|
||||
|
||||
gaze_left_up = edit_api._detect_facial_direction(left_up_kpts)
|
||||
self.assertEqual(gaze_left_up, "looking left and up")
|
||||
|
||||
# Test 3: Lying down / reclining pose
|
||||
lying_kpts = [list(kp) for kp in standing_kpts]
|
||||
# In lying down, nose is at y=10, hips are at y=12 (very horizontal)
|
||||
# Head X = 10, Hip X = 80
|
||||
lying_kpts[0][0] = 10 # Head X
|
||||
lying_kpts[0][1] = 10 # Head Y
|
||||
lying_kpts[11][0] = 80 # Hip X
|
||||
lying_kpts[11][1] = 12 # Hip Y
|
||||
lying_kpts[12][0] = 80
|
||||
lying_kpts[12][1] = 12
|
||||
|
||||
pose_desc_lying = edit_api._describe_pose(lying_kpts)
|
||||
self.assertIn("lying down", pose_desc_lying)
|
||||
|
||||
def test_18_designer_enhancements(self):
|
||||
import unittest.mock as mock
|
||||
import edit_api
|
||||
|
||||
# 1. Test Deduplication of Pose Names
|
||||
# We will mock the external requests.post LLM call to return some mock poses,
|
||||
# one of which collides with an existing pose name in poses.md (e.g., "The Clasp" or "standing")
|
||||
mock_raw_response = (
|
||||
"# standing\n"
|
||||
"This is a standing pose description.\n"
|
||||
"Perfect anatomy, photorealistic.\n\n"
|
||||
"# Custom Pose\n"
|
||||
"This is a custom pose description.\n"
|
||||
"Anatomically precise, photorealistic.\n"
|
||||
)
|
||||
|
||||
mock_response = mock.Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": mock_raw_response
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with mock.patch("requests.post", return_value=mock_response) as mock_post:
|
||||
# Let's override _load_poses to return {"standing": "existing text"} to force a name collision
|
||||
with mock.patch("edit_api._load_poses", return_value={"standing": {"text": "existing text", "beta": False}}):
|
||||
payload = {
|
||||
"n": 2,
|
||||
"context": "standing and custom pose instructions",
|
||||
"filename": None,
|
||||
"beta": False,
|
||||
"messages": None
|
||||
}
|
||||
response = self.client.post("/designer/generate", json=payload)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data["status"], "success")
|
||||
poses = data["poses"]
|
||||
# The duplicate "standing" name should be renamed to "standing 2" instead of skipped!
|
||||
self.assertIn("standing 2", poses)
|
||||
self.assertIn("Custom Pose", poses)
|
||||
self.assertEqual(poses["standing 2"], "This is a standing pose description. Perfect anatomy, photorealistic.")
|
||||
self.assertEqual(poses["Custom Pose"], "This is a custom pose description. Anatomically precise, photorealistic.")
|
||||
|
||||
# 2. Test Multi-Turn Conversation/History Payload Building
|
||||
with mock.patch("requests.post", return_value=mock_response) as mock_post:
|
||||
with mock.patch("edit_api._load_poses", return_value={}):
|
||||
# Send history messages in the request
|
||||
history_messages = [
|
||||
{"role": "user", "content": "Initial prompt"},
|
||||
{"role": "assistant", "content": "# Some Pose\nbody text"}
|
||||
]
|
||||
payload_with_history = {
|
||||
"n": 1,
|
||||
"context": "make them sit down",
|
||||
"filename": None,
|
||||
"beta": False,
|
||||
"messages": history_messages
|
||||
}
|
||||
response = self.client.post("/designer/generate", json=payload_with_history)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that requests.post was called with the correct messages payload
|
||||
self.assertTrue(mock_post.called)
|
||||
call_args = mock_post.call_args
|
||||
called_json = call_args[1]["json"]
|
||||
called_messages = called_json["messages"]
|
||||
|
||||
# The payload should contain:
|
||||
# 1. System message (DESIGNER_SYSTEM)
|
||||
# 2. Initial user prompt
|
||||
# 3. Assistant response
|
||||
# 4. New user follow-up prompt incorporating "make them sit down"
|
||||
self.assertEqual(len(called_messages), 4)
|
||||
self.assertEqual(called_messages[0]["role"], "system")
|
||||
self.assertEqual(called_messages[1]["role"], "user")
|
||||
self.assertEqual(called_messages[1]["content"], "Initial prompt")
|
||||
self.assertEqual(called_messages[2]["role"], "assistant")
|
||||
self.assertEqual(called_messages[2]["content"], "# Some Pose\nbody text")
|
||||
self.assertEqual(called_messages[3]["role"], "user")
|
||||
self.assertIn("make them sit down", called_messages[3]["content"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user