| """Debug face-landmark detection (no Gemini call, no key). |
| |
| Runs the OPTIONAL landmark backend on three inputs and records, separately, why |
| detection succeeds/fails: |
| 1. original full image |
| 2. original cropped to the face crop box |
| 3. generated raw face crop |
| |
| For each it writes a landmark overlay (bbox + eye/nose/mouth + contour on success; |
| size/crop/backend/error annotation on failure) and a QA markdown. No model |
| weights are committed. EXPERIMENTAL. Pilot Ready: NOT CONFIRMED. |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import sys |
| from io import BytesIO |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) |
|
|
| from PIL import Image |
|
|
| from app.services.face_pipeline.landmark_detector import ( |
| detect_landmarks_result, |
| draw_landmark_overlay, |
| landmark_backend, |
| ) |
| from app.services.gemini_client import normalize_image_orientation_bytes |
|
|
|
|
| def _parse_norm(value: str): |
| parts = [float(p) for p in value.split(",")] |
| if len(parts) != 4: |
| raise ValueError("crop-box-norm must be 'x1,y1,x2,y2'") |
| return tuple(parts) |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| parser = argparse.ArgumentParser(description="Debug face landmark detection (no API)") |
| parser.add_argument("--original", required=True) |
| parser.add_argument("--generated-crop", required=True) |
| parser.add_argument("--crop-box-norm", default="0.33,0.39,0.69,0.78") |
| parser.add_argument("--evidence-root", default="runtime/gemini-smoke-evidence") |
| parser.add_argument("--tag", default="salon-crop-09-landmark-debug") |
| parser.add_argument("--min-confidence", type=float, default=0.5) |
| args = parser.parse_args(argv) |
|
|
| original_path = Path(args.original) |
| crop_path = Path(args.generated_crop) |
| if not original_path.exists(): |
| print(f"REFUSED: original not found: {original_path}") |
| return 2 |
| if not crop_path.exists(): |
| print(f"REFUSED: generated crop not found: {crop_path}") |
| return 2 |
|
|
| smoke = Path(args.evidence_root) / "gemini-smoke" |
| lm_dir = smoke / "landmarks" |
| qa_md = smoke / "qa" / f"landmark-debug-{args.tag}.md" |
| for p in (lm_dir / "x", qa_md): |
| p.parent.mkdir(parents=True, exist_ok=True) |
|
|
| backend = landmark_backend() |
|
|
| original_bytes = normalize_image_orientation_bytes(original_path.read_bytes()) |
| crop_bytes = crop_path.read_bytes() |
|
|
| base = Image.open(BytesIO(original_bytes)).convert("RGB") |
| w, h = base.size |
| nx1, ny1, nx2, ny2 = _parse_norm(args.crop_box_norm) |
| crop_box = (int(nx1 * w), int(ny1 * h), int(nx2 * w), int(ny2 * h)) |
|
|
| |
| orig_crop = base.crop(crop_box) |
| orig_crop_buf = BytesIO() |
| orig_crop.save(orig_crop_buf, format="PNG") |
| orig_crop_bytes = orig_crop_buf.getvalue() |
|
|
| targets = [ |
| ("original", original_bytes, crop_box), |
| ("original-crop", orig_crop_bytes, None), |
| ("generated-crop", crop_bytes, None), |
| ] |
|
|
| rows = [] |
| any_success = False |
| for name, img_bytes, box in targets: |
| res = detect_landmarks_result(img_bytes, min_detection_confidence=args.min_confidence) |
| any_success = any_success or res.success |
| overlay = draw_landmark_overlay(img_bytes, res, crop_box=box, label=name) |
| out_path = lm_dir / f"landmark-debug-{args.tag}-{name}.png" |
| out_path.write_bytes(overlay) |
| rows.append((name, res, out_path.name)) |
| print(f"[{name}] success={res.success} count={res.detection_count} " |
| f"size={res.image_size} backend={res.backend} " |
| f"error={res.error_type or '-'} hint={res.hint()}") |
|
|
| real_eval = "YES" if any_success else "NO" |
| lines = [f"# Landmark detection debug - {args.tag}\n", |
| f"- landmark_backend: {backend or 'none'}", |
| f"- crop_box: {crop_box}", |
| f"- real_landmark_eval: {real_eval}", |
| f"- synthetic_fallback_used: {'NO' if any_success else 'YES'}", |
| f"- business_quality_eval_valid: {'PENDING (human QA)' if any_success else 'NO'}", |
| f"- reason: {'' if any_success else 'real landmarks were not detected on any input'}", |
| "- pilot_ready: NOT CONFIRMED\n", |
| "## Per-image detection\n"] |
| for name, res, fname in rows: |
| lines += [ |
| f"### {name}", |
| f"- overlay: {fname}", |
| f"- success: {'YES' if res.success else 'NO'}", |
| f"- detection_count: {res.detection_count}", |
| f"- image_size: {res.image_size}", |
| f"- error_type: {res.error_type or '-'}", |
| f"- error_message: {(res.error_message or '-')[:200]}", |
| f"- attempts: {res.attempts}", |
| f"- hint: {res.hint()}\n", |
| ] |
| qa_md.write_text("\n".join(lines), encoding="utf-8") |
|
|
| print(f"landmark_debug: DONE backend={backend or 'none'} real_landmark_eval={real_eval}") |
| print(f"overlays in {lm_dir} ; qa={qa_md.name}") |
| print("EXPERIMENTAL. Human QA required. Pilot Ready: NOT CONFIRMED.") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|