| """A2 real-landmark + segmentation recompose spike (no Gemini call). |
| |
| Detects landmarks on the ORIGINAL and the already-generated raw face crop using |
| an OPTIONAL backend (MediaPipe / InsightFace). If no backend is installed it |
| falls back to synthetic landmarks (clearly recorded as landmark_detected=NO) so |
| the structure still runs for inspection. Aligns via eyes/nose/mouth, protects |
| everything outside the face-contour landmark polygon, color-matches, and blends |
| onto the original. No API key, no model weights committed. |
| |
| Outputs under runtime/ (gitignored). EXPERIMENTAL / NOT_PRODUCTION_READY. |
| Human QA required. 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, ImageDraw |
|
|
| from app.services.face_pipeline.geometry_align import recompose_a2 |
| from app.services.face_pipeline.landmark_detector import ( |
| detect_face_landmarks, |
| landmark_backend, |
| synthetic_landmarks, |
| ) |
| from app.services.face_pipeline.segmentation_mask import ( |
| segmentation_backend_available, |
| ) |
| from app.services.gemini_client import normalize_image_orientation_bytes |
|
|
|
|
| def _parse_norm(value: str) -> tuple[float, float, float, float]: |
| parts = [float(p) for p in value.split(",")] |
| if len(parts) != 4: |
| raise ValueError("crop-box-norm must be 'x1,y1,x2,y2'") |
| return parts[0], parts[1], parts[2], parts[3] |
|
|
|
|
| def main(argv: list[str] | None = None) -> int: |
| parser = argparse.ArgumentParser(description="A2 real-landmark recompose spike (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-08-a2-real-landmark") |
| parser.add_argument("--warp-mode", default="affine", choices=["affine", "piecewise"]) |
| parser.add_argument("--blend-mode", default="safe", 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) |
| parser.add_argument("--boundary-feather", type=int, default=None) |
| parser.add_argument("--visual-mode", default=None, choices=["natural", "balanced", "strong"]) |
| parser.add_argument("--eye-protect-strength", type=float, default=None) |
| parser.add_argument("--max-eye-alpha", type=int, default=None) |
| parser.add_argument("--max-forehead-alpha", type=int, default=None) |
| parser.add_argument("--max-boundary-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 |
|
|
| smoke = Path(args.evidence_root) / "gemini-smoke" |
| out_png = smoke / "output" / f"gemini-output-{args.tag}.png" |
| mask_png = smoke / "masks" / f"gemini-mask-{args.tag}.png" |
| compare_png = smoke / "comparison" / f"gemini-compare-{args.tag}.png" |
| qa_md = smoke / "qa" / f"gemini-a2-qa-{args.tag}.md" |
| for p in (out_png, mask_png, compare_png, qa_md): |
| p.parent.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") |
| width_img, height_img = base.size |
| nx1, ny1, nx2, ny2 = _parse_norm(args.crop_box_norm) |
| crop_box = (int(nx1 * width_img), int(ny1 * height_img), |
| int(nx2 * width_img), int(ny2 * height_img)) |
|
|
| backend = landmark_backend() |
| crop_img = Image.open(BytesIO(crop_bytes)).convert("RGB") |
|
|
| target_landmarks = detect_face_landmarks(original_bytes) |
| source_landmarks = detect_face_landmarks(crop_bytes) |
| if target_landmarks is not None and source_landmarks is not None: |
| landmark_source = backend or "unknown" |
| else: |
| |
| print(f"NOTE: landmark backend unavailable ({backend or 'none'}); " |
| "using synthetic landmarks (landmark_detected=NO).") |
| face_xywh = (crop_box[0], crop_box[1], |
| crop_box[2] - crop_box[0], crop_box[3] - crop_box[1]) |
| target_landmarks = synthetic_landmarks(base.size, face_xywh) |
| source_landmarks = synthetic_landmarks(crop_img.size, (0, 0, *crop_img.size)) |
| landmark_source = "synthetic" |
|
|
| final_bytes, qa, editable, _warped = recompose_a2( |
| original_bytes, crop_bytes, |
| target_landmarks=target_landmarks, source_landmarks=source_landmarks, |
| landmark_source=landmark_source, |
| segmentation_used=segmentation_backend_available(), |
| warp_mode=args.warp_mode, |
| blend_mode=args.blend_mode, |
| color_match_strength=args.color_match_strength, |
| feature_strength=args.feature_strength, |
| inner_core_alpha=args.inner_core_alpha, |
| boundary_feather=args.boundary_feather, |
| visual_mode=args.visual_mode, |
| eye_protect_strength=args.eye_protect_strength, |
| max_eye_alpha=args.max_eye_alpha, |
| max_forehead_alpha=args.max_forehead_alpha, |
| max_boundary_alpha=args.max_boundary_alpha, |
| return_debug=True, |
| ) |
|
|
| out_png.write_bytes(final_bytes) |
| editable.save(mask_png) |
|
|
| raw_crop = crop_img |
| final_img = Image.open(BytesIO(final_bytes)).convert("RGB") |
| tint = Image.new("RGB", base.size, (255, 40, 40)) |
| seg_panel = Image.composite(Image.blend(base, tint, 0.45), base, editable) |
| panels = [("original", base), ("raw crop", raw_crop), |
| ("segmentation mask", seg_panel), ("final composite", final_img)] |
| h = 360 |
| thumbs = [(lbl, im.resize((max(1, int(im.width * h / im.height)), h))) for lbl, im in panels] |
| pad, label_h = 12, 26 |
| total_w = sum(t.width for _, t in thumbs) + pad * (len(thumbs) + 1) |
| canvas = Image.new("RGB", (total_w, h + label_h + pad * 2), (245, 245, 245)) |
| cd = ImageDraw.Draw(canvas) |
| x = pad |
| for lbl, t in thumbs: |
| canvas.paste(t, (x, pad + label_h)) |
| cd.text((x, pad + 6), lbl, fill=(20, 20, 20)) |
| x += t.width + pad |
| canvas.save(compare_png) |
|
|
| qa_lines = "\n".join(f"- {k}: {v}" for k, v in qa.items()) |
| qa_md.write_text( |
| f"# A2 real-landmark recompose QA - {args.tag}\n\n" |
| f"- landmark_backend: {backend or 'none (synthetic fallback)'}\n" |
| f"- crop_box: {crop_box}\n" |
| f"{qa_lines}\n\n" |
| "> EXPERIMENTAL / NOT_PRODUCTION_READY. Human QA required.\n" |
| "> Pilot Ready: NOT CONFIRMED.\n", |
| encoding="utf-8", |
| ) |
|
|
| print("recompose_a2_spike: DONE (no API call, no key used)") |
| print(f"landmark_backend={backend or 'none'} landmark_source={landmark_source}") |
| for k, v in qa.items(): |
| print(f"{k}={v}") |
| print(f"output={out_png.name} mask={mask_png.name} compare={compare_png.name}") |
| print("Human QA required. Pilot Ready: NOT CONFIRMED.") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| sys.exit(main()) |
|
|