This commit is contained in:
Hossein 2025-12-01 15:17:48 +03:30
parent 0f00b8ffbd
commit 60f86db416

285
main.py
View File

@ -80,6 +80,93 @@ def find_first_image_part(resp):
return None return None
async def generate_tryon_image(user_part, outfit_part, prompt, previous_image_part=None):
"""
Helper function to generate try-on image from Gemini.
Returns the raw image bytes.
"""
contents = [prompt, user_part, outfit_part]
if previous_image_part:
contents.append(previous_image_part)
contents.append("Third image = previous incorrect attempt. Fix it by keeping the original face exactly.")
else:
contents.append("First image = user selfie. Second image = outfit reference.")
print(f"[DEBUG] Generating try-on image with model: {IMAGE_MODEL}")
if previous_image_part:
print("[DEBUG] Correction mode: regenerating with face-lock instructions")
try:
response = client.models.generate_content(
model=IMAGE_MODEL,
contents=contents,
)
except Exception as e:
error_msg = str(e)
if "API key" in error_msg or "INVALID_ARGUMENT" in error_msg or "API_KEY" in error_msg:
raise HTTPException(
status_code=401,
detail=(
f"Gemini API key error: {error_msg}. "
"Please verify your GEMINI_API_KEY in the .env file is correct."
),
)
import traceback
print(f"[ERROR] Gemini API error: {error_msg}")
print(traceback.format_exc())
raise HTTPException(status_code=502, detail=f"Gemini error: {error_msg}")
# Extract output image
img_part = find_first_image_part(response)
if not img_part or not getattr(img_part, "data", None):
message = "No image returned from Gemini."
if getattr(response, "candidates", None) and len(response.candidates) > 0:
cand = response.candidates[0]
finish_reason = getattr(cand, "finish_reason", "unknown")
if hasattr(cand, "content") and getattr(cand.content, "parts", None):
for part in cand.content.parts:
txt = getattr(part, "text", None)
if txt:
message += f" Gemini says: {txt[:500]}"
break
if hasattr(cand, "safety_ratings"):
safety_ratings = cand.safety_ratings
if safety_ratings:
blocked = [
f"{getattr(r, 'category', 'unknown')}"
for r in safety_ratings
if hasattr(r, "blocked") and getattr(r, "blocked", False)
]
if blocked:
message += f" Safety blocked: {', '.join(blocked)}"
raise HTTPException(
status_code=502,
detail=f"{message} Finish reason: {finish_reason}",
)
else:
raise HTTPException(
status_code=502,
detail="No image returned from Gemini. No candidates in response.",
)
raw = img_part.data
# Ensure valid image bytes
try:
Image.open(BytesIO(raw))
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Returned image is invalid: {e}. Image size: {len(raw)} bytes",
)
return raw
def faces_match_strict(original_bytes: bytes, edited_bytes: bytes) -> bool: def faces_match_strict(original_bytes: bytes, edited_bytes: bytes) -> bool:
""" """
Second Gemini call: compare ORIGINAL vs EDITED and decide Second Gemini call: compare ORIGINAL vs EDITED and decide
@ -281,6 +368,11 @@ These identity rules are STRONGER than all other instructions. If you cannot fol
ABSOLUTE FACE LOCK (READ-ONLY REGION): ABSOLUTE FACE LOCK (READ-ONLY REGION):
- Treat the entire face, head, ears, and visible neck area of the FIRST image as a **READ-ONLY REGION**. - Treat the entire face, head, ears, and visible neck area of the FIRST image as a **READ-ONLY REGION**.
- DO NOT TOUCH the face at all.
- DO NOT TOUCH identity.
- Only modify clothing regions.
- Keep head/face exactly as the original.
- Do not redraw the face at all.
- You MUST NOT re-render, redraw, regenerate, beautify, smooth, reshape, de-age, re-gender, - You MUST NOT re-render, redraw, regenerate, beautify, smooth, reshape, de-age, re-gender,
or otherwise alter the face in any way. or otherwise alter the face in any way.
- The face region in the final image must be **indistinguishable** from the original FIRST image, - The face region in the final image must be **indistinguishable** from the original FIRST image,
@ -384,133 +476,104 @@ SECOND image = outfit_photo (outfit reference).
mime_type=outfit_photo.content_type or "image/jpeg", mime_type=outfit_photo.content_type or "image/jpeg",
) )
# ---- Call Gemini (image model) ---- print(f"\n[DEBUG] Metadata received: {metadata[:200] if metadata else '(empty)'}")
print(f"\n[DEBUG] Using image model: {IMAGE_MODEL}")
print(f"[DEBUG] Metadata received: {metadata[:200] if metadata else '(empty)'}") # ============================================================
# STEP A: Generate initial try-on image (clothing only)
# ============================================================
print("\n[STEP A] Generating initial try-on image...")
try: try:
response = client.models.generate_content( raw_v1 = await generate_tryon_image(user_part, outfit_part, prompt)
model=IMAGE_MODEL, print("[STEP A] ✅ Initial try-on image generated")
contents=[ except HTTPException:
prompt, raise
user_part,
outfit_part,
"First image = user selfie. Second image = outfit reference.",
],
)
except Exception as e: except Exception as e:
error_msg = str(e)
if "API key" in error_msg or "INVALID_ARGUMENT" in error_msg or "API_KEY" in error_msg:
raise HTTPException(
status_code=401,
detail=(
f"Gemini API key error: {error_msg}. "
"Please verify your GEMINI_API_KEY in the .env file is correct."
),
)
import traceback import traceback
print(f"[ERROR] Gemini API error: {error_msg}") print(f"[ERROR] Step A failed: {e}")
print(traceback.format_exc()) print(traceback.format_exc())
raise HTTPException(status_code=502, detail=f"Gemini error: {error_msg}") raise HTTPException(status_code=502, detail=f"Failed to generate initial try-on: {str(e)}")
# ============================================================
# STEP B: Identity validation check
# ============================================================
print("\n[STEP B] Running identity consistency check on initial result...")
same_identity = faces_match_strict(user_bytes, raw_v1)
print(f"[STEP B] Identity check result: same_identity={same_identity}")
if same_identity:
# ✅ Identity preserved - return the image
print("[STEP B] ✅ Identity validated - returning initial result")
headers = {}
if request_id:
headers["X-TryOn-Request-Id"] = str(request_id)
return Response(content=raw_v1, media_type="image/png", headers=headers)
# ============================================================
# STEP C: Identity changed - regenerate with HARD face lock
# ============================================================
print("\n[STEP C] ❌ Identity changed detected - regenerating with face-lock correction...")
correction_prompt = f"""
You changed the user's identity in the previous image generation attempt.
THIS IS ABSOLUTELY FORBIDDEN.
# ---- Debug: Inspect response ---- 🚫 CRITICAL CORRECTION INSTRUCTIONS:
print("\n[DEBUG] === Gemini Response Analysis ===")
if not getattr(response, "candidates", None):
print("[DEBUG] No candidates at all. Full response:")
pprint(response)
else:
cand = response.candidates[0]
finish_reason = getattr(cand, "finish_reason", None)
print(f"[DEBUG] Finish reason: {finish_reason}")
print(f"[DEBUG] Safety ratings: {getattr(cand, 'safety_ratings', None)}")
if hasattr(cand, "content") and getattr(cand.content, "parts", None): DO NOT TOUCH THE USER'S FACE AT ALL
print(f"[DEBUG] Parts in first candidate ({len(cand.content.parts)} total):") DO NOT REDRAW THE FACE
for i, part in enumerate(cand.content.parts): DO NOT CHANGE IDENTITY, HAIRLINE, BEARD, OR ANY FACIAL FEATURES
part_type = type(part).__name__ DO NOT CHANGE HEAD SHAPE OR NECK
print(f" Part {i}: type={part_type}") DO NOT BEAUTIFY, SMOOTH, OR RESHAPE THE FACE
DO NOT MODIFY SKIN TONE ON THE FACE
text = getattr(part, "text", None) YOU MUST KEEP THE FIRST IMAGE FACE EXACTLY, PIXEL BY PIXEL IF POSSIBLE.
if text:
print(f" TEXT: {text[:300]}")
if hasattr(part, "inline_data"): ONLY modify clothing areas using the outfit reference from the SECOND image.
inline = part.inline_data Keep the background, lighting, pose, and identity EXACTLY as in the original user photo (FIRST image).
if inline:
data_size = len(getattr(inline, "data", b""))
mime = getattr(inline, "mime_type", "unknown")
print(f" INLINE_DATA: mime={mime}, size={data_size} bytes")
if hasattr(part, "blob"): The face, head, ears, and neck region from the FIRST image are READ-ONLY.
blob = part.blob Only clothing below the neck and on the body should be changed.
if blob:
data_size = len(getattr(blob, "data", b""))
mime = getattr(blob, "mime_type", "unknown")
print(f" BLOB: mime={mime}, size={data_size} bytes")
print("[DEBUG] === End Response Analysis ===\n")
# ---- Extract output image ---- {metadata_text}
img_part = find_first_image_part(response)
if not img_part or not getattr(img_part, "data", None):
message = "No image returned from Gemini."
if getattr(response, "candidates", None) and len(response.candidates) > 0: {extra_rules}
cand = response.candidates[0] """
finish_reason = getattr(cand, "finish_reason", "unknown")
# Create a Part from the previous incorrect image for reference
if hasattr(cand, "content") and getattr(cand.content, "parts", None): previous_image_part = Part.from_bytes(
for part in cand.content.parts: data=raw_v1,
txt = getattr(part, "text", None) mime_type="image/png",
if txt: )
message += f" Gemini says: {txt[:500]}"
break
if hasattr(cand, "safety_ratings"):
safety_ratings = cand.safety_ratings
if safety_ratings:
blocked = [
f"{getattr(r, 'category', 'unknown')}"
for r in safety_ratings
if hasattr(r, "blocked") and getattr(r, "blocked", False)
]
if blocked:
message += f" Safety blocked: {', '.join(blocked)}"
raise HTTPException(
status_code=502,
detail=f"{message} Finish reason: {finish_reason}",
)
else:
raise HTTPException(
status_code=502,
detail="No image returned from Gemini. No candidates in response.",
)
raw = img_part.data
# Ensure valid image bytes
try: try:
Image.open(BytesIO(raw)) raw_v2 = await generate_tryon_image(user_part, outfit_part, correction_prompt, previous_image_part)
print("[STEP C] ✅ Correction image generated")
except HTTPException:
raise
except Exception as e: except Exception as e:
raise HTTPException( import traceback
status_code=500, print(f"[ERROR] Step C failed: {e}")
detail=f"Returned image is invalid: {e}. Image size: {len(raw)} bytes", print(traceback.format_exc())
)
# ---- SECOND CALL: IDENTITY VALIDATION ----
print("[DEBUG] Running identity consistency check...")
same_identity = faces_match_strict(user_bytes, raw)
print(f"[DEBUG] Identity check result: same_identity={same_identity}")
if not same_identity:
# Hard fail: we do NOT return the image if identity changed
raise HTTPException( raise HTTPException(
status_code=502, status_code=502,
detail="Model changed user identity; try-on result rejected by identity guard.", detail="Failed to generate corrected try-on after identity mismatch.",
) )
# ============================================================
# STEP D: Re-validate corrected version (optional check, always return)
# ============================================================
print("\n[STEP D] Running identity consistency check on corrected result...")
same_identity_v2 = faces_match_strict(user_bytes, raw_v2)
print(f"[STEP D] Identity check result: same_identity={same_identity_v2}")
if same_identity_v2:
print("[STEP D] ✅ Identity validated after correction")
else:
print("[STEP D] ⚠️ Identity check failed, but returning corrected image anyway")
# Always return the corrected image, regardless of identity check result
headers = {} headers = {}
if request_id: if request_id:
headers["X-TryOn-Request-Id"] = str(request_id) headers["X-TryOn-Request-Id"] = str(request_id)
return Response(content=raw, media_type="image/png", headers=headers) return Response(content=raw_v2, media_type="image/png", headers=headers)