Delete every knob the fixed profiles cannot honor

The CLI still advertised --model, --steps, --guidance-scale, --device and a
deprecated --auto. Each pinned a value the two surviving profiles fix -- the
model stack, the per-stage distilled schedule, CFG 1.0, CUDA -- so the only
outcome any of them had was an error raised several frames below the caller,
under a message naming an internal profile. A flag whose sole result is a
refusal is worse than no flag: it advertises a capability that does not exist,
and it lets a wrapper thread a value that will silently do nothing. They are
gone from the parser, from InvisibleEngine, and from WatermarkRemover, so the
failure is now a TypeError or a Click "No such option" at the point the caller
can act on.

The install hint was wrong in the same way. is_available() checked torch and
diffusers, then told the user to install [diffusion] -- which contains neither
DiffSynth nor the Z-Image face stage both profiles run. Following the advice
produced a second, different failure. The module list and the extra name now
live once in watermark_profiles (REMOVAL_MODULES, INVISIBLE_EXTRA) and are read
by both the CLI gate and the remover's precondition, which cannot drift apart
because they are the same tuple.

The adaptive-polish default moved out of the argument parser. It was resolved by
reading Click's parameter source, which put per-profile data in the CLI layer,
left the engine declaring the opposite default (False vs True) so a library
caller and a CLI caller on one profile got different output, and lost the polish
entirely for anything that supplies the flag non-interactively. The flag is now
tri-state (default=None) and resolve_adaptive_polish owns the per-profile
answer. The seed follows the same rule: the CLI stopped pre-resolving it.

Dead code removed with it: six scan_*_video wrappers and the _scan_video helper
none of them had a caller for, PNG_METADATA_KEYS, feather_region_composite and
the remover region path that was only reachable from a no-caller convenience
wrapper, remove_watermark_batch on both layers, try_empty_device_cache, the
_generate/_run_qwen_zimage pass-through pair, self.model_id, and the _internal
PEP 562 shim that no caller ever went through. get_device now answers cuda or
cpu only: mps and xpu travelled one frame to the same CUDA-only refusal while
costing a device probe each, and that refusal now names the resolved device, so
device=None on a CUDA-less host says 'cpu' rather than 'None'. The XPU wheel
index went with them.

Docs: README, cli, installation, python-api, supported-signals,
known-limitations and module-internals all still described the removed profiles,
the CPU/MPS/XPU ladder, a `default`->`sdxl` alias, and the wrong extra.
known-limitations still listed the retired SDXL strength ladder as current.
scripts/smoke_matrix.py and real_examples_e2e.py drove --device mps.

Next release is 0.25.0, not a patch: this removes public parameters and
narrows a published extra on top of the released 0.24.0.

pre-commit: 1) maintain.sh - exit 0 (1091 tests, Pyright 0 errors, no
vulnerabilities); 2) /simplify - 4 agents, 11 findings applied, 2 skipped
(dropping the `device` parameter entirely, which raiw-app pins; folding
diffsynth into the `diffusion` extra, which video-only callers do not need);
3) docs sync - grepped every removed identifier across README, docs/, scripts/,
.claude/; updated 9 docs; 4) CLAUDE.md - added the no-error-only-knobs rule to
.claude/rules/development.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-08-03 15:38:40 -07:00
co-authored by Claude Opus 5
parent bf4bfc1ab7
commit 52b2c115e8
28 changed files with 543 additions and 955 deletions
+45 -17
View File
@@ -53,9 +53,19 @@ The decorators for diffusion options are shared by `invisible`, `all`, and
`batch`. The runtime help generated by Click is the source of truth for option
names and defaults.
The deprecated `--auto` option does not select a pipeline or change adaptive
polishing. [`_resolve_auto_polish`](../src/remove_ai_watermarks/cli.py) emits a
warning and returns the explicit polish value unchanged.
`--adaptive-polish` is tri-state: it declares `default=None`, so "the user did not
choose" is a value the CLI passes through rather than a default it has to invent.
`resolve_adaptive_polish` in `watermark_profiles.py` turns that `None` into the
profile's answer (off for `qwen-zimage`, whose output already matches the input's
detail level; on for `sdxl-zimage`). The same call runs inside
`InvisibleEngine.remove_watermark`, so a library caller and a CLI caller on one
profile get the same output.
It used to read Click's parameter source in the CLI instead. That put per-profile
data in the argument-parsing layer, left the engine declaring the opposite default,
and silently lost the polish for anything supplying the flag non-interactively (an
envvar default or a wrapper calling `main()` with a defaulted list is classified
`DEFAULT`). The seed follows the same rule: the CLI does not pre-resolve it either.
Regression coverage:
@@ -477,25 +487,42 @@ Regression coverage:
[`_internal/watermark_profiles.py`](../src/remove_ai_watermarks/_internal/watermark_profiles.py)
is the source of truth for:
- profile aliases;
- default model identifiers;
- default steps and seeds;
- vendor-adaptive strength resolution;
- the minimum viable step calculation.
- profile names and their underscore spellings;
- the fixed seed;
- the SDXL global-stage checkpoint id (`SDXL_MODEL_ID`) and the Canny ControlNet id;
- strength resolution for both profiles.
The current profiles are `qwen-zimage` (the default) and `sdxl-zimage`, and both
are CUDA-only. `controlnet`, `sdxl`, `qwen` and `default` were removed rather than
kept as a CPU path, and are rejected rather than aliased onward. There is no
content-dependent automatic router.
The current profiles are `controlnet`, `sdxl`, `qwen`, and `qwen-zimage`.
For serverless cold starts, `InvisibleEngine.preload(global_only=True)` loads the
mandatory Qwen stage and YuNet while leaving the optional Z-Image and SAM face
mandatory global stage and YuNet while leaving the optional Z-Image and SAM face
stack lazy until a face is detected. The default `preload()` still loads every
stage.
`default` is a legacy alias for `sdxl`. There is no content-dependent automatic
router.
**What is deliberately not a parameter.** Model id, step count, CFG and any
non-CUDA device are fixed by the profile, so none of them appears in
`WatermarkRemover.__init__`, `remove_watermark`, `InvisibleEngine`, or the CLI.
They used to be accepted and then rejected several frames down; a signature that
refuses the argument outright fails where the caller can act on it, and stops a
wrapper from threading a value that would silently do nothing. The step count and
CFG live with the stage that runs them (`GLOBAL_STEPS`, `FACE_STEPS`, `GLOBAL_CFG`,
`FACE_CFG` in `qwen_zimage_pipeline.py`). The dtype is likewise profile-owned: see
"Face-stage dtype" for what an override cost the last time one existed.
[`invisible_engine.py`](../src/remove_ai_watermarks/invisible_engine.py) handles
image sizing, postprocessing, and the public engine
interface. It delegates model execution to
[`_internal/watermark_remover.py`](../src/remove_ai_watermarks/_internal/watermark_remover.py).
`get_device` in that module answers only `cuda` or `cpu`. An `mps` or `xpu` answer
would travel one frame to the same CUDA-only refusal while costing a device probe,
and reporting it implied an Apple-silicon or Intel-GPU path that does not exist.
The refusal names the *resolved* device, so `device=None` on a CUDA-less host says
`'cpu'` rather than `'None'`.
The Python engine and CLI do not have identical defaults for every optional
postprocessing argument. Integrations that require reproducibility should pass
the relevant values explicitly.
@@ -509,9 +536,8 @@ unit-test pass. Exact prompt and edge-map regression guards live in
Regression coverage:
- [`test_watermark_profiles.py`](../tests/test_watermark_profiles.py)
- [`test_invisible_engine.py`](../tests/test_invisible_engine.py)
- [`test_img2img_runner.py`](../tests/test_img2img_runner.py)
- [`test_qwen_zimage_pipeline.py`](../tests/test_qwen_zimage_pipeline.py)
- [`test_platform.py`](../tests/test_platform.py)
### CPU offload
@@ -712,14 +738,16 @@ Regression coverage:
### Tiling
[`_internal/tiling.py`](../src/remove_ai_watermarks/_internal/tiling.py) contains pure
tile planning, feather weights, tile orchestration, and region compositing.
tile planning, feather weights, and tile orchestration.
Tiling engages only when requested and the long side exceeds the tile size.
It avoids an explicit full-image downscale but does not make diffusion
pixel-preserving. Each tile is still regenerated.
`feather_region_composite` changes only the requested box and leaves pixels
outside it unchanged.
It also held a `feather_region_composite` for AI-*enhanced* composites, where only
the edited region should change. Nothing ever reached it: the `erase` command
inpaints through `region_eraser`, and the remover's `region` argument was only
reachable from a module-level convenience wrapper with no callers. Both went.
Regression coverage: