File size: 5,128 Bytes
5bf61fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
"""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  # noqa: E402

from app.services.face_pipeline.landmark_detector import (  # noqa: E402
    detect_landmarks_result,
    draw_landmark_overlay,
    landmark_backend,
)
from app.services.gemini_client import normalize_image_orientation_bytes  # noqa: E402


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))

    # original cropped to the face crop box
    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())