leechard / scripts /audit_face_recompose.py
nenae18's picture
Deploy LeeChard
5d3c2a9 verified
Raw
History Blame Contribute Delete
5.18 kB
"""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())