mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-06 22:18:36 +02:00
Finish CPU offload support on current main
This commit is contained in:
@@ -37,16 +37,20 @@ that cannot afford LaMa's ~4.7 GB peak pins `--backend migan` explicitly.
|
||||
|
||||
`invisible` pipeline processes at **native resolution for inputs whose long side is >= 1024px**, and **auto-upscales smaller inputs UP to a 1024px floor** (`min_resolution=1024`, the default; `--min-resolution 0` disables) before diffusion -- SDXL img2img distorts badly on a tiny latent (a 381x512 portrait wrecks at native, the #36 follow-up), and the output is restored to the original input size so the floor is a transparent quality boost (it adds time/memory on small inputs). The floor upscale uses Lanczos by default; **`--upscaler esrgan`** (opt-in, the `esrgan` extra) runs Real-ESRGAN first for better detail before the Lanczos resize to the exact target (`upscaler.py` / `InvisibleEngine._esrgan_upscale`, falls back to Lanczos if the extra is absent). `max_resolution=0` (default) means no downscale cap, matching the hosted raiw.cc backend (fal fast-sdxl, no pre-downscale). The old forced downscale-to-1024 -> upscale-back round-trip for LARGE images was the main quality loss (issue #10) and is gone; at strength ~0.05 SDXL img2img does not need a downscale.
|
||||
|
||||
**CUDA model CPU offload (`--cpu-offload`):** Diffusers normally places the complete SDXL, ControlNet, or base Qwen pipeline on the selected CUDA device. The opt-in offload mode instead uses Diffusers model-level CPU offload, keeping one model component on the GPU at a time and moving it back to CPU before the next component runs. This reduces peak VRAM use but adds transfer overhead. The custom `qwen-zimage` runtime already manages its global model placement; the same flag forces its face stack to stay on the offload path instead of becoming resident on a high-memory GPU. CPU and MPS behavior is unchanged.
|
||||
|
||||
**Final `--unsharp` post-filter (`humanizer.unsharp_mask`, opt-in, default 0):** applied LAST (after the face-restore pass, else it would be smoothed over) to counter the soft/over-smoothed look diffusion + restoration leave (an AI tell); ~0.5-0.8 safe, higher risks halos. Pairs with `--humanize` (grain adds sensor-noise texture, unsharp adds crispness). `--max-resolution N` re-introduces an opt-in long-side cap purely to bound GPU/MPS memory on very large inputs (it reintroduces the lossy round-trip). For huge images that OOM at native, **`--tile` is the lossless alternative** -- see the tiled-diffusion subsection below.
|
||||
|
||||
### Tiled diffusion for large inputs (`--tile`, issue #10)
|
||||
|
||||
`--tile` (OFF by default; `--tile-size` default 1024, `--tile-overlap` default 128) processes the diffusion pass in overlapping sliding-window tiles instead of one forward pass, so a large image is regenerated at **native resolution** without the OOM and without the lossy `--max-resolution` downscale round-trip. It engages only when the long side exceeds `--tile-size`; a sub-tile image runs a single pass unchanged. `WatermarkRemover.remove_watermark` refactors the single-image `_generate` into a per-tile `_generate_one` (the ControlNet canny edge map is rebuilt per tile, so structure preservation works tile-local) and routes it through `noai.tiling.run_tiled` when tiling is active. The geometry and blend math are pure helpers, unit-tested without the model (`tests/test_tiling.py`):
|
||||
`--tile` (OFF by default; `--tile-size` default 1024, `--tile-overlap` default 128) processes the diffusion pass in overlapping sliding-window tiles instead of one forward pass, so a large image is regenerated at **native resolution** without the OOM and without the lossy `--max-resolution` downscale round-trip. It engages only when the long side exceeds `--tile-size`; a sub-tile image runs a single pass unchanged. The SDXL, ControlNet, and base Qwen paths refactor the single-image `_generate` into a per-tile `_generate_one` (the ControlNet canny edge map is rebuilt per tile, so structure preservation works tile-local) and route it through `noai.tiling.run_tiled` when tiling is active. `qwen-zimage` instead tiles only its global Qwen pass, feather-blends that result, and then runs YuNet, SAM, and Z-Image once against the full original/result pair. The geometry and blend math are pure helpers, unit-tested without the model (`tests/test_tiling.py`):
|
||||
|
||||
- `plan_tiles(w, h, tile_size, overlap)` lays out a row-major grid where every tile is exactly `tile_size` (the last tile on each axis is pulled back flush to the far edge, simply overlapping its predecessor more). Uniform tile size keeps each diffusion pass at SDXL's preferred dimension.
|
||||
- `feather_weights(w, h, overlap)` is a separable linear taper, ~1 in the interior and ramping toward each edge, kept **strictly positive** so the normalized accumulate-and-divide blend (`accum / weight_sum`) is a partition of unity: a region covered by one feathered edge (an image corner) still divides cleanly. Identical (unchanged) tiles therefore reconstruct the input exactly -- the seam-free guarantee, asserted in `test_identity_generate_reconstructs_image`.
|
||||
|
||||
CAVEAT: each tile is an **independent** low-strength regeneration. At the certified removal strengths (0.20-0.30) the per-tile drift is small and the feather blend hides the seams, but tiling is a memory workaround, not a quality upgrade over a single native pass -- a 32 GB MPS box that clears the native UNet peak should prefer no tiling. The MPS->CPU fallback still applies per tile; if the first tile falls back to CPU, the device stays CPU for the rest of the image.
|
||||
CAVEAT: each tile is an **independent** low-strength regeneration. At the current SDXL/ControlNet defaults (0.10-0.15) the per-tile drift is small and the feather blend hides the seams, but tiling is a memory workaround, not a quality upgrade over a single native pass -- a 32 GB MPS box that clears the native UNet peak should prefer no tiling. The MPS->CPU fallback still applies per tile; if the first tile falls back to CPU, the device stays CPU for the rest of the image.
|
||||
|
||||
For `qwen-zimage`, the global denoise is still computed from the full-frame megapixel count and the same resolved seed is reused for every tile. The profile defaults to seed 0, matching the release-candidate oracle run; an explicit seed overrides it. Running the face stage only after blending avoids duplicate regeneration and boundary-local face misses. A real H100 smoke on 2026-07-25 exercised the shipped branch on a 4096x3072 input (20 tiles at 1024 with 128 px overlap, seed 0, strength 0.154): it completed in 653.367 seconds after 43.741 seconds of setup, preserved the exact dimensions, and peaked at 22.732 GiB allocated / 23.861 GiB reserved CUDA memory. Visual inspection found no tile seams. The worst tile-boundary gradient-change line was at the 98.563 percentile of all image lines (2.522 standard deviations), below the preselected 99th-percentile outlier threshold; overview fidelity was MAE 3.332%, PSNR 26.564 dB, and global SSIM 0.988627. This no-face input validates the global tiled execution and blend, not the post-blend face path. The July 25 seed-0 oracle result still certifies exact non-tiled candidate bytes only; tiled SynthID efficacy requires a separate provider-oracle check.
|
||||
|
||||
**Concrete MPS data points (the OOM is memory-tier-dependent, NOT a hard MPS limit):** on a ~24 GB unified-memory machine (verified 2026-05-25, 1254x1254 gpt-image SDXL, fp32) native res OOMs at the *UNet* step (peak ~17 GiB), not only the VAE decode, and the auto-fallback in `img2img_runner` reloads on CPU and finishes (slow, ~13 min) -- the output is still weight-identical and defeats SynthID, so "looks hung/crashed" on Mac is usually this CPU fallback, not a pipeline error. On a **32 GB** unified-memory machine the same default SDXL pass runs entirely on MPS with **no CPU fallback** (verified 2026-05-31, 1122x1402 gpt-image, `all`/default, ~155 s end-to-end), so 32 GB clears the native-res UNet peak that 24 GB could not. Adding `enable_vae_tiling()` alone does NOT prevent the 24 GB OOM (the peak is the UNet, not the VAE). The fast Mac workarounds for memory-constrained machines are fp16 on MPS (roughly halves memory) or `--max-resolution` to cap the long side; neither is wired as the default. The `controlnet` pipeline adds the canny ControlNet weights on top of SDXL, so its peak is a bit higher than the plain `default` pass; the same MPS->CPU fallback covers an OOM. The native-vs-cap-vs-floor decision lives in the pure helper `invisible_engine._target_size(w, h, max_resolution, min_resolution)` (returns `None` for native, a target tuple for a downscale cap OR an upscale floor; cap takes precedence, the floor is skipped on a min>max misconfig) so it is unit-tested (`tests/test_invisible_engine.py::TestTargetSize`, the #10/#15/#36 regression guard) without loading the model -- keep that logic in the helper, don't re-inline it.
|
||||
|
||||
@@ -128,7 +132,7 @@ CAVEAT: Google's 0.15 was validated only on `--max-resolution 1536`; native larg
|
||||
|
||||
Default strength is vendor-adaptive (see the bullet above); `docs/synthid.md` §2.2 is authoritative for the numbers.
|
||||
|
||||
**Oracle scope (load-bearing):** the Gemini app "Verify with SynthID" is the ONLY valid SynthID oracle (detects Google's mark on any image); `openai.com/verify` is scoped to OpenAI provenance (its own C2PA), NOT a SynthID oracle -- a negative there is meaningless for SynthID. There is no local SynthID detector, so the tool cannot self-check; if the oracle still reads SynthID, raise `--strength` to the lowest value that verifies clean. Only the `sdxl` (plain SDXL img2img; `default` is a back-compat alias) and `controlnet` (SDXL + canny ControlNet) profiles exist; the local `invisible` default is weight-for-weight identical to raiw.cc prod (`fal-ai/fast-sdxl` = `stabilityai/stable-diffusion-xl-base-1.0`, runtime-downloaded, not bundled).
|
||||
**Oracle scope (load-bearing):** the Gemini app "Verify with SynthID" is the ONLY valid SynthID oracle (detects Google's mark on any image); `openai.com/verify` is scoped to OpenAI provenance (its own C2PA), NOT a SynthID oracle -- a negative there is meaningless for SynthID. There is no local SynthID detector, so the tool cannot self-check; if the oracle still reads SynthID, raise `--strength` to the lowest value that verifies clean. The profiles are `sdxl` (plain SDXL img2img; `default` is a back-compat alias), `controlnet` (SDXL + canny ControlNet), `qwen` (Qwen-Image img2img), and the experimental `qwen-zimage` two-stage stack.
|
||||
|
||||
**Forensic-stealth caveat** (arXiv:2605.09203): defeating the SynthID verifier is NOT forensic invisibility -- independent detectors flag *removal-processed* images vs genuinely-clean ones at >98% TPR@1%FPR, so do not over-claim "indistinguishable from a real photo".
|
||||
|
||||
@@ -148,9 +152,9 @@ The robust fix is a HIGHER strength, oracle-revalidated per content type (contro
|
||||
|
||||
**Follow-up same day: re-running the two photoreal survivors through controlnet at an explicit `--strength 0.15` cleared BOTH on the oracle -- BUT one of them (the bracelet) had SURVIVED the SAME 0.15 controlnet config in the first pass (only the random, unset seed differed). So removal near the threshold is SEED-NON-DETERMINISTIC: the same image+pipeline+strength+resolution can pass or fail run-to-run (img2img uses `seed=None`/random unless `--seed` is passed, and there is no local SynthID detector to self-verify). 0.15 is the borderline, NOT a robust floor -- pick a strength with MARGIN (controlnet ~>= 0.20) rather than exactly on it; the content×pipeline table's 0.15 data point is near-threshold noise. A confirming run at `--strength 0.20` controlnet cleared BOTH photoreal survivors on the oracle (ladder: 0.10 grid detected → 0.15 borderline/non-deterministic → 0.20 both clean), so **0.20 is the recommended robust controlnet floor for OpenAI photoreal** (one margin run, not an N-run repeatability proof -- a service should add margin or verify repeatability since there is no local SynthID detector to self-check).
|
||||
|
||||
**Engineering follow-up DONE 2026-06-09 (three coupled changes):** (1) **strength raised + unified** -- `resolve_strength(strength, vendor)` now applies ONE vendor-adaptive ladder (the certified controlnet floors 0.20/0.30/0.30) to BOTH pipelines; see the DEFAULT STRENGTH bullet above for why one ladder covers `sdxl`. (2) **`controlnet` is now the DEFAULT pipeline** (CLI `--pipeline` default = `controlnet` + both engine ctors). Rationale: with the certified higher ladder it clears BOTH content classes that flipped in the content-x-pipeline table (photoreal AND flat graphic), whereas plain SDXL left SynthID on flat graphics -- so controlnet is the more removal-robust default. Cost: every non-`--auto` run now downloads the canny ControlNet weights + a higher memory peak (MPS->CPU fallback covers OOM). (3) **the plain-SDXL profile was renamed `default` -> `sdxl`** (`watermark_profiles.SDXL_PROFILE`/`normalize_profile`); `default` stays as a back-compat CLI/ctor alias (the `--pipeline` Choice accepts `sdxl`/`controlnet`/`default`, a click callback `_normalize_pipeline` maps `default`->`sdxl` AND warns that `default` is deprecated). (4) **the content-detection layer + `--auto` planner were removed and `--auto` was retired to a deprecated alias for `--adaptive-polish`** -- see the dedicated `auto_config.py`-removal bullet above (controlnet is the default pipeline and the polish self-gates, so detection changed nothing). raiw.cc still needs its own per-vendor/content calibration on the GPU worker for native resolution. The Gemini-native resolution caveat stands: controlnet 0.30 is certified only <=1536.** **CERTIFIED 2026-06-04 via the isolated `raiw-controlnet-cert` Modal app (`raiw-app/modal_cert.py`), restore OFF, ≤1536, each vendor on its own oracle: controlnet floors are OpenAI 0.20 (2 photoreal × 3 seeds = 6/6 clean; the 0.15-flipper is seed-robust at 0.20) and Gemini 0.30 (0.20 detected → 0.30 clean on 2/2 seeds). OpenAI 0.20 transfers to prod (resolution-independent); Gemini 0.30 holds only ≤1536 — Gemini is resolution-sensitive and raiw.cc runs NATIVE (`max_resolution=0`), so cap Gemini ≤1536 + use 0.30, or native-calibrate (~0.35+). Prod recipe: controlnet + per-vendor floor in `resolve_strength` (not the default ladder) + FIXED seed (kills the non-determinism).
|
||||
**Engineering follow-up DONE 2026-06-09 (three coupled changes):** (1) **strength raised + unified** -- `resolve_strength(strength, vendor)` now applies ONE vendor-adaptive ladder (the certified controlnet floors 0.20/0.30/0.30) to BOTH pipelines; see the DEFAULT STRENGTH bullet above for why one ladder covers `sdxl`. (2) **`controlnet` is now the DEFAULT pipeline** (CLI `--pipeline` default = `controlnet` + both engine ctors). Rationale: with the certified higher ladder it clears BOTH content classes that flipped in the content-x-pipeline table (photoreal AND flat graphic), whereas plain SDXL left SynthID on flat graphics -- so controlnet is the more removal-robust default. Cost: every non-`--auto` run now downloads the canny ControlNet weights + a higher memory peak (MPS->CPU fallback covers OOM). (3) **the plain-SDXL profile was renamed `default` -> `sdxl`** (`watermark_profiles.SDXL_PROFILE`/`normalize_profile`); `default` stays as a back-compat CLI/ctor alias (the `--pipeline` Choice accepts `sdxl`/`controlnet`/`default`, a click callback `_normalize_pipeline` maps `default`->`sdxl` AND warns that `default` is deprecated). (4) **the content-detection layer + `--auto` planner were removed and `--auto` was retired to a deprecated alias for `--adaptive-polish`** -- see the dedicated `auto_config.py`-removal bullet above (controlnet is the default pipeline and the polish self-gates, so detection changed nothing). A production caller still needs its own per-vendor/content calibration at its deployed native resolution. The Gemini-native resolution caveat stands: controlnet 0.30 is certified only <=1536.** **CERTIFIED 2026-06-04 via an isolated Modal certification harness, restore OFF, ≤1536, each vendor on its own oracle: controlnet floors are OpenAI 0.20 (2 photoreal × 3 seeds = 6/6 clean; the 0.15-flipper is seed-robust at 0.20) and Gemini 0.30 (0.20 detected → 0.30 clean on 2/2 seeds). OpenAI 0.20 transfers to production (resolution-independent); Gemini 0.30 holds only ≤1536 — Gemini is resolution-sensitive, so a native-resolution caller should cap Gemini to ≤1536 at 0.30 or calibrate its native path (~0.35+). Production recipe: controlnet + per-vendor floor in `resolve_strength` (not the default ladder) + FIXED seed (kills the non-determinism).
|
||||
|
||||
**No face-restore in the library:** every approach evaluated (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned, 2026-06-04 - 2026-06-08 cert sweeps) regenerated the face via SDXL diffusion -- the output face inherited SDXL "clean skin" gloss and lost original identity precision, looking MORE AI-generated than the cleaned image, not less. The drifted face from controlnet 0.20 is the least-AI state we can reach; for a paid service that's the prod output. See `docs/synthid-robust-identity-research-2026-06-08.md` "Empirical follow-up".**
|
||||
**No face-restore runs in the default controlnet profile:** every earlier approach evaluated there (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned, 2026-06-04 - 2026-06-08 cert sweeps) regenerated the face via SDXL diffusion -- the output face inherited SDXL "clean skin" gloss and lost original identity precision, looking MORE AI-generated than the cleaned image, not less. The separate experimental `qwen-zimage` profile now tests a different architecture, Z-Image regeneration from the original SAM-masked face crop. Its first ArcFace/LPIPS run is recorded below, but it still needs its own oracle and multi-image face/text matrix.**
|
||||
|
||||
See `docs/synthid.md` §5.5 + `docs/controlnet-removal-pipeline-research.md` (certified floors table).** **Lesson: visual-quality + face-recovery validation does NOT prove watermark removal -- only the SynthID oracle does, across MULTIPLE content types; never infer removal from sharpness/identity, and never conclude from a partial result (the photoreal-only data first read as "controlnet shields, default removes" -- the flat-graphic result reversed it).**
|
||||
|
||||
@@ -172,6 +176,37 @@ The scrub still comes from the img2img `strength` (same lever as SDXL); the call
|
||||
|
||||
**Conclusion: Qwen wins TEXT only for clean body text on a plain background with NO faces; controlnet wins faces AND display/decorative text in a scene. So `qwen` is a MANUAL `--pipeline qwen` opt-in, not a routed lane.** A content `--pipeline auto` router + a faces+text mixed dual-pass were prototyped and DROPPED (2026-06-20): on the canonical faces+text case (the abba poster, faces + display text) controlnet won EVERY metric incl. text (CER 0.114 vs qwen 0.379), so grafting qwen text only hurts; and "text→qwen" is undecidable cheaply (body-vs-display text is what matters). Caveat: `resolve_strength(..., pipeline="qwen")` carries the Qwen ladder (`_QWEN_VENDOR_STRENGTH`, Gemini 0.25), so `--pipeline qwen` gets the 0.25 Gemini floor automatically — the old manual `--strength 0.25` workaround is retired. `_build_qwen_kwargs` now passes an explicit height/width (qwen squished non-square inputs to 1024² without it). Flat-graphic content was not in the sample.
|
||||
|
||||
**Improving Qwen (ship vs improve):** the cited research lives in `docs/qwen-improvement-research.md` -- read it before extending the `qwen` pipeline. Verdict: shippable as an opt-in text lane. **The "add a Qwen-Image ControlNet to fix face smoothing" lead was built, measured, and CLOSED (2026-06-20):** a DiffSynth-Studio Qwen + Apache-2.0 blockwise-canny ControlNet at the Gemini floor 0.25 did NOT restore face skin texture (face Laplacian-variance retention flat 0.40 -> 0.40, 13/16 faces within +-0.02; the SDXL+canny target 0.62 was not approached), because canny carries edges not skin grain and Qwen's higher Gemini floor (0.25 vs SDXL+canny 0.15) forces more smoothing -- and a deep-research sweep confirmed NO permissively-licensed Qwen tile/detail/realism/skin ControlNet exists anywhere (every Qwen conditioning is geometry). So **faces stay on SDXL+controlnet; Qwen is the text lane, not a face fix.** The strongest remaining lead is **Z-Image-Turbo** (6B, Apache-2.0, `ZImageImg2ImgPipeline`, scrub mechanism preserved) -- its own SynthID floor and face/text fidelity are UNMEASURED; that is the next experiment. Non-regenerative high-frequency detail re-injection is NOT safe by assumption (the "clean-output high frequencies do not carry the watermark" claim was refuted) -- it must be oracle-gated. Always validate any improvement at the certified floors with `scripts/fidelity_metrics.py` first.
|
||||
**Improving Qwen (ship vs improve):** the cited research lives in `docs/qwen-improvement-research.md` -- read it before extending the `qwen` pipeline. Verdict: shippable as an opt-in text lane. **The "add a Qwen-Image ControlNet to fix face smoothing" lead was built, measured, and CLOSED (2026-06-20):** a DiffSynth-Studio Qwen + Apache-2.0 blockwise-canny ControlNet at the Gemini floor 0.25 did NOT restore face skin texture (face Laplacian-variance retention flat 0.40 -> 0.40, 13/16 faces within +-0.02; the SDXL+canny target 0.62 was not approached), because canny carries edges not skin grain and Qwen's higher Gemini floor (0.25 vs SDXL+canny 0.15) forces more smoothing -- and a deep-research sweep confirmed NO permissively-licensed Qwen tile/detail/realism/skin ControlNet exists anywhere (every Qwen conditioning is geometry). So **base Qwen stays the text lane, not a face fix.** The distinct Z-Image face-crop lead is now implemented as `qwen-zimage`; direct face comparisons are below, and its exact current six-output candidate is negative in the corresponding provider oracles. Broad seeded removal and text behavior remain unmeasured. Non-regenerative high-frequency detail re-injection is NOT safe by assumption (the "clean-output high frequencies do not carry the watermark" claim was refuted) -- it must be oracle-gated.
|
||||
|
||||
**Seed as a quality lever (measured, openai_1 at 0.10, seeds 0-4):** the seed barely moves whole-image fidelity (img LPIPS 0.062-0.065, SSIM 0.855-0.857, PSNR 28.5-28.7 — flat) but does shift TEXT legibility (OCR CER 0.241-0.290, ~17% spread) -- the seed changes WHICH details get regenerated, not the overall level. So a per-image best-of-N-seed selection is a WEAK, text-only lever (pick the lowest-CER seed that still scrubs; fidelity selection needs no oracle). Not worth the N× cost for general use -- pin one decent seed in prod; reserve best-of-N for text-heavy premium cases.
|
||||
|
||||
## `qwen-zimage` pipeline
|
||||
|
||||
`--pipeline qwen-zimage` is the recommended high-quality SynthID removal mode when CUDA capacity is available and fidelity matters more than latency or cost. It remains a manual opt-in so the broadly compatible, much cheaper ControlNet path can stay the default. The profile ports the upstream two-stage workflow: an input-resolution Qwen-Image-2512 Lightning Canny pass regenerates the frame, then original face crops are segmented and regenerated with Z-Image Turbo before a feathered paste. DiffSynth requires both pixel inputs and the requested dimensions to use the same /16 latent grid, so each stage makes that small alignment resize internally and restores the global result to the original dimensions. The profile defaults to deterministic seed 0 because the release-candidate oracle evidence was produced at that seed; explicit callers can still override it.
|
||||
|
||||
The port is architectural, not bit-identical. The active graph was traced from upstream commit `3007d0351596ae0a78b7074dae7ad179710b1e48`, including its linked Impact Pack implementation. It confirms that the active face path is YOLO + SAM; the MediaPipe node visible on the canvas is unconnected. The port keeps the two adaptive-denoise formulas, four-step Qwen Lightning stage, Canny thresholds and scale, AuraFlow shift 3 equivalent, original-image face source, SAM center + box prompts, IoU-0.93 proposal union with highest-score fallback, detector-box intersection, crop factor 2.5, 768 face guide, 1024 crop cap, eight-step face stage, and paste feather 10.
|
||||
|
||||
Four runtime differences remain. This package uses full safetensors instead of the source graph's quantized GGUF models, YuNet instead of Ultralytics YOLO to avoid an AGPL runtime, DiffSynth FlowMatch samplers instead of ComfyUI's DPM++ 2M / SGM Uniform and `res_2s` / `bong_tangent` pairs, and no latent-space 20 px detailer noise-mask feather. The face crop is regenerated in full, then only the feathered SAM pixels are composited back, so generated pixels outside that mask are discarded. These differences prevent an exact-output claim even though the architecture and active decision path match.
|
||||
|
||||
The default full-frame denoise is resolution-adaptive, not vendor-adaptive. The face denoise is separate and scales from the largest detected face. `--strength` overrides only the global Qwen stage. The profile fixes the global step count at four because its Lightning LoRA is distilled for that schedule; the face stage uses its own eight-step schedule. `--model` is unsupported. `--tile` follows the global-only route described above, with one full-frame face stage after blending.
|
||||
|
||||
Direct comparison now covers two official upstream before/after pairs plus the existing crowded `gemini_3` fixture. The published upstream examples were scored against their own original inputs, with the upstream output resized back only for metric alignment where necessary:
|
||||
|
||||
| Case | Result | ArcFace identity | Face LPIPS | Texture retention | Image LPIPS | SSIM |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| Upstream example 10 | published upstream | 0.976 | 0.172 | 0.166 | 0.259 | 0.627 |
|
||||
| Upstream example 10 | local `qwen-zimage` | 0.950 | 0.045 | 0.570 | 0.167 | 0.765 |
|
||||
| Upstream example 10 | current polished ControlNet | 0.701 | 0.105 | 0.941 | 0.094 | 0.781 |
|
||||
| Upstream example 12, matched size | published upstream | 0.976 | 0.014 | 0.873 | 0.111 | 0.777 |
|
||||
| Upstream example 12, matched size | local `qwen-zimage` | 0.947 | 0.015 | 0.708 | 0.085 | 0.896 |
|
||||
| Upstream example 12, matched size | current polished ControlNet | 0.548 | 0.061 | 0.961 | 0.105 | 0.887 |
|
||||
|
||||
The result reproduces the upstream architecture's main advantage: identity retention is far stronger than the current ControlNet path. On the group example, local face LPIPS nearly matches the published upstream output and whole-image fidelity is better; upstream still leads slightly on ArcFace identity and texture retention. ControlNet preserves more global detail and, on example 10, lower provisional OCR CER, but its faces drift to different identities. The OCR reference for example 10 came from the original image's OCR rather than hand transcription, so it is supporting evidence, not a text certification. The published upstream outputs are also downscaled relative to their originals, which penalizes their detail metrics but is the actual result the repository presents.
|
||||
|
||||
The comparison exposed a real implementation defect on a non-/16 input: the requested DiffSynth dimensions were floored while the PIL image remained at its original size, so the VAE latent and noise grid disagreed. Regression tests were written to fail on that mismatch, then both global and face inputs were changed to use the exact same aligned grid as their `height` and `width`.
|
||||
|
||||
**Final candidate oracle result (2026-07-25):** the user checked every image in the provider-separated `full-clean-final-candidate-2026-07-25-by-oracle` bundle with the corresponding provider oracle and confirmed that none of the six outputs retained SynthID or the provider generation signal. These are the current seed-0 bytes after the complete `visible -> qwen-zimage -> metadata` route, including the calibrated YuNet 0.5 gate and the prompt-cache/model-residency optimizations. This supersedes the earlier first-port batch check as the release-candidate result. It certifies these exact outputs, not every seed, resolution, or content class.
|
||||
|
||||
YuNet's score threshold is 0.5, not the upstream graph's YOLO threshold of 0.2: detector scores are not interchangeable. The copied 0.2 threshold admitted false/duplicate boxes and multiplied serial Z-Image calls. The calibrated gate retained every visible face in the public and upstream fixtures while reducing `gemini_3` from 36 boxes to 18 and the poster from 30 to 10. Serial face regeneration still scales with the retained detector count. Visual QA also found that the smallest multilingual text degraded on the typography sheet even though the larger headings survived. Keep `controlnet` as the compatibility and cost default, but recommend `qwen-zimage` when the user prioritizes output fidelity, especially face identity. The final exact-output oracle check covers the current YuNet threshold and runtime optimizations; do not call the profile broadly certified until a wider seeded face/text matrix is complete.
|
||||
|
||||
**Modal runtime measurement (2026-07-24 through 2026-07-25, seed 0, GPU stage only):** the exact paired A100-40GB run measured ControlNet at 3.342-12.543 seconds per image. `qwen-zimage` took 133.556-188.493 seconds on the three zero-face images and 1212.496 seconds on the 18-face group. The same group initially took 262.072 seconds on an exact H100, including 181.764 seconds in serial face regeneration. On H100 the three zero-face cases took 45.029-65.071 seconds. The shipped fast-load resident placement reduced the group to 133.543 seconds total and 38.272 seconds for face regeneration while producing a pixel-identical output; peak CUDA allocation rose from 24.364 to 43.477 GiB. Setup increased from 32.282 to 43.960 seconds, so even a cold one-request total fell from 294.354 to 177.503 seconds. Reusing the fixed prompt embeddings reduced a warm 18-face request further to 78.474 seconds after an earlier request populated the Qwen embedding; the cached and uncached outputs were pixel-identical, and peak VRAM was unchanged. The Qwen cache helps from the second request in one container, while the Z-Image cache helps after the first face in a multi-face request. Residency is automatic at 64 GiB VRAM or above; smaller cards retain offload. H100 remains both faster and cheaper at the live Modal rates for this workload. Pricing is intentionally not copied here; calculate from the current Modal rate and the recorded GPU seconds. Model setup must be added to an un-warmed single call or amortized over a warm batch.
|
||||
|
||||
Reference in New Issue
Block a user