| """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 |
|
|
| from app.services.face_pipeline.audit import run_audit |
| from app.services.face_pipeline.landmark_detector import ( |
| detect_face_landmarks, |
| landmark_backend, |
| synthetic_landmarks, |
| ) |
| 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 _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()) |
|
|