feat(face-restore): add InstantID as the default non-commercial restore path

Per the 2026-06-08 deep-research synthesis (docs/synthid-robust-identity-
research-2026-06-08.md), the entire ArcFace-class identity-adapter ecosystem
for SDXL is blocked from commercial use by InsightFace's non-commercial model
packs (antelopev2 / buffalo_l). No commercial-safe ArcFace-grade identity
stack exists today. The user explicitly opted into shipping a non-commercial
restore path (research / personal use; raiw.cc must NOT install the extra).

Architectural choice: InstantID over PhotoMaker-V2 as the default.
- PhotoMaker-V2 (CLIP+ArcFace dual encoder, txt2img only): documented upstream
  identity drift on Asian male faces, visually confirmed in our cert sweep
  (tatsunari rendered as a generic woman; group photo collapsed into a
  patchwork).
- InstantID (ArcFace cross-attention + landmark ControlNet): semantic
  identity branch + spatial weak landmark control, decoupled. Per InstantID
  paper (arXiv:2401.07519) and the research report, stronger identity fidelity
  on single portraits. Critically: NO original face pixels enter the diffusion
  (ArcFace embedding is semantic, landmark stick figure is pure geometry), so
  SynthID is not transported.

Implementation:
- New `src/remove_ai_watermarks/instantid_restore.py` mirrors the
  `photomaker_restore.py` shape (lazy singletons for pipeline + FaceAnalysis,
  per-face crop + _composite_faces from photomaker_restore). Loads the
  InstantID community pipeline via `DiffusionPipeline.from_pretrained(
  custom_pipeline="pipeline_stable_diffusion_xl_instantid")` -- no upstream
  Python package needed; diffusers fetches the file from its community
  examples.
- New `instantid` extra in pyproject (insightface + onnxruntime +
  huggingface-hub). NON-COMMERCIAL block in the comment explains why.
- CLI: `--restore-faces-method [instantid|photomaker]`, default `instantid`.
  Both methods explicitly labeled NON-COMMERCIAL in the help text.
- Engine: dispatch on `restore_faces_method` to either
  `_restore_faces_instantid` or `_restore_faces_photomaker`.
- 9 control-flow tests for InstantID without model download (mirror the
  photomaker_restore.py test pattern + draw_kps helper checks). 587/587 pass.

Diffusers-0.38 compat verified by upstream code inspection: the InstantID
pipeline inherits from `StableDiffusionXLControlNetPipeline`, uses only
public diffusers APIs (`encode_prompt`, `prepare_image`, `prepare_latents`,
`get_guidance_scale_embedding`), uses legacy attention processor API which
diffusers preserves for backward compat. No PhotoMaker-V1-style internal
text_encoder access. End-to-end execution will be validated by the Modal
cert sweep in the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-06-08 19:44:17 -07:00
co-authored by Claude Opus 4.8
parent c486badaa8
commit 70e8b3a517
8 changed files with 752 additions and 18 deletions
+31 -10
View File
@@ -236,21 +236,34 @@ def _warn_if_esrgan_unavailable(upscaler: str) -> None:
def _restore_faces_options(f: Any) -> Any:
"""Attach the face-restoration flag to an invisible-pipeline command.
"""Attach the face-restoration flags to an invisible-pipeline command.
The post-pass uses PhotoMaker-V2 to regenerate each face from a CLIP+ArcFace
embedding. **NON-COMMERCIAL** -- PhotoMaker-V2 pulls InsightFace antelopev2/
buffalo_l model packs at runtime, which are research-only. A paid service
(raiw.cc, any monetized SaaS) MUST NOT use this flag.
Two methods. ``instantid`` (default; the `instantid` extra) regenerates each
face from an ArcFace embedding + landmark ControlNet -- semantic identity
plus weak spatial control, no original pixels. ``photomaker`` (the
`photomaker` extra) uses PhotoMaker-V2's CLIP+ArcFace dual encoder.
**BOTH ARE NON-COMMERCIAL**: they pull InsightFace antelopev2 / buffalo_l
model packs at runtime, which are research-only. A paid service (raiw.cc,
any monetized SaaS) MUST NOT use this flag.
"""
method = click.option(
"--restore-faces-method",
type=click.Choice(["instantid", "photomaker"]),
default="instantid",
help="Face-restore mechanism. 'instantid' (default) uses InstantID's ArcFace + "
"landmark ControlNet for stronger identity fidelity on single portraits. "
"'photomaker' uses PhotoMaker-V2's CLIP+ArcFace dual encoder. **BOTH are "
"NON-COMMERCIAL** (InsightFace antelopev2 / buffalo_l model packs are "
"research-only). Pick whichever extra you've installed; for personal / research "
"use only. Do NOT use in a paid service.",
)(f)
return click.option(
"--restore-faces/--no-restore-faces",
default=False,
help="EXPERIMENTAL, opt-in, **NON-COMMERCIAL** -- needs the 'photomaker' extra "
"which pulls non-commercial InsightFace model packs. Restores face identity via "
"PhotoMaker-V2 (CLIP+ArcFace embedding -> fresh face); off by default, auto-skips "
"when no face is detected or the extra is absent.",
)(f)
help="EXPERIMENTAL, opt-in, **NON-COMMERCIAL**. Restore face identity via the "
"chosen --restore-faces-method (default: instantid); off by default, auto-skips "
"when no face is detected or the chosen extra is absent.",
)(method)
def _watermark_region(det: DetectionResult, width: int, height: int) -> tuple[int, int, int, int]:
@@ -601,6 +614,7 @@ def cmd_invisible(
min_resolution: int,
controlnet_scale: float,
restore_faces: bool,
restore_faces_method: str,
upscaler: str,
auto: bool,
adaptive_polish: bool,
@@ -663,6 +677,7 @@ def cmd_invisible(
upscaler=upscaler,
vendor=vendor,
restore_faces=restore_faces,
restore_faces_method=restore_faces_method,
)
elapsed = time.monotonic() - t0
@@ -864,6 +879,7 @@ def cmd_all(
min_resolution: int,
controlnet_scale: float,
restore_faces: bool,
restore_faces_method: str,
upscaler: str,
auto: bool,
adaptive_polish: bool,
@@ -972,6 +988,7 @@ def cmd_all(
upscaler=upscaler,
vendor=vendor,
restore_faces=restore_faces,
restore_faces_method=restore_faces_method,
)
console.print(" Invisible watermark removed")
@@ -1027,6 +1044,7 @@ def _process_batch_image(
max_resolution: int = 0,
min_resolution: int = 1024,
restore_faces: bool = False,
restore_faces_method: str = "instantid",
controlnet_scale: float = 1.0,
upscaler: str = "lanczos",
auto: bool = False,
@@ -1105,6 +1123,7 @@ def _process_batch_image(
min_resolution=min_resolution,
upscaler=upscaler,
restore_faces=restore_faces,
restore_faces_method=restore_faces_method,
# Detect the vendor from the pristine original (`img_path`), not the
# visible-processed `out_path` whose C2PA is already gone.
vendor=vendor_for_strength(img_path),
@@ -1187,6 +1206,7 @@ def cmd_batch(
max_resolution: int,
min_resolution: int,
restore_faces: bool,
restore_faces_method: str,
controlnet_scale: float,
upscaler: str,
auto: bool,
@@ -1246,6 +1266,7 @@ def cmd_batch(
max_resolution=max_resolution,
min_resolution=min_resolution,
restore_faces=restore_faces,
restore_faces_method=restore_faces_method,
controlnet_scale=controlnet_scale,
upscaler=upscaler,
auto=auto,