"""A2-4 audit runner: measure where the face transformation is lost (no API). Recomposes (original + already-generated raw crop) and writes per-stage delta heatmaps + a numeric summary so a human can see whether the generator, the warp, the alpha mask, or the color/blend is killing the transformation. No Gemini call, no key, no model weights committed. Outputs under runtime/ (gitignored). Pilot Ready: NOT CONFIRMED. """ from __future__ import annotations import argparse import json 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.audit import run_audit # noqa: E402 from app.services.face_pipeline.landmark_detector import ( # noqa: E402 detect_face_landmarks, landmark_backend, synthetic_landmarks, ) 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 _f(value): return None if value is None or value < 0 else value def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Face recompose alpha/delta audit (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-17-audit-piecewise-strong") parser.add_argument("--warp-mode", default="piecewise", choices=["affine", "piecewise"]) parser.add_argument("--blend-mode", default="strong", choices=["safe", "medium", "strong"]) parser.add_argument("--color-match-strength", type=float, default=None) parser.add_argument("--feature-strength", type=float, default=None) parser.add_argument("--inner-core-alpha", type=int, default=None) 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 out_dir = Path(args.evidence_root) / "gemini-smoke" / "audit" / args.tag out_dir.mkdir(parents=True, exist_ok=True) 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)) backend = landmark_backend() target = detect_face_landmarks(original_bytes) source = detect_face_landmarks(crop_bytes) if target is not None and source is not None: landmark_source = backend or "unknown" else: print(f"NOTE: landmark backend unavailable ({backend or 'none'}); synthetic fallback.") face_xywh = (crop_box[0], crop_box[1], crop_box[2] - crop_box[0], crop_box[3] - crop_box[1]) target = synthetic_landmarks(base.size, face_xywh) crop_img = Image.open(BytesIO(crop_bytes)).convert("RGB") source = synthetic_landmarks(crop_img.size, (0, 0, *crop_img.size)) landmark_source = "synthetic" result = run_audit( original_bytes, crop_bytes, crop_box, target_landmarks=target, source_landmarks=source, landmark_source=landmark_source, warp_mode=args.warp_mode, blend_mode=args.blend_mode, color_match_strength=_f(args.color_match_strength), feature_strength=_f(args.feature_strength), inner_core_alpha=_f(args.inner_core_alpha), ) for name, img in result["images"].items(): img.save(out_dir / f"{name}.png") metrics = result["metrics"] metrics["landmark_backend"] = backend or "none" metrics["crop_box"] = list(crop_box) (out_dir / "audit-summary.json").write_text( json.dumps(metrics, indent=2, ensure_ascii=False), encoding="utf-8" ) md = ["# Face recompose audit - " + args.tag, ""] md += [f"- {k}: {v}" for k, v in metrics.items()] md += ["", "> EXPERIMENTAL / NOT_PRODUCTION_READY. Human QA required.", "> Pilot Ready: NOT CONFIRMED."] (out_dir / "audit-summary.md").write_text("\n".join(md), encoding="utf-8") print(f"audit: DONE tag={args.tag} backend={backend or 'none'} verdict={metrics['verdict']}") for k in ("raw_delta_mean", "warped_delta_mean", "final_delta_mean", "feature_core_alpha_mean", "feature_core_alpha_max", "final_to_raw_transfer_ratio", "eyes_delta", "nose_delta", "mouth_delta", "cheeks_delta"): print(f"{k}={metrics[k]}") print(f"out={out_dir}") print("Pilot Ready: NOT CONFIRMED.") return 0 if __name__ == "__main__": sys.exit(main())