Make InvisibleOptions engine knobs only and pin the forwarding

`InvisibleOptions` promises in its docstring that every default mirrors
`InvisibleEngine`. Two fields made that promise cost something to keep: `force` is
not an engine parameter at all, and `controlnet_scale` was a third spelling of the
engine's `controlnet_conditioning_scale`. The mirror test carried an exception
table for each. This removes both, so the comparison needs no exceptions -- a field
that needs one is a field that belongs somewhere else.

`force` decides WHETHER the engine runs, which is settled before it is built, so it
joins `backend` and `sensitivity` as a parameter of `remove_all` and `remove_batch`
and is threaded to `_run_invisible` as its own argument. `controlnet_scale` takes
the engine's own name; the click option stays `--controlnet-scale` and is now
translated exactly once instead of at three forwarding sites.

Safe to do today: both symbols landed after 0.25.0 and have never been published.

The forwarding turned out to be the weaker half. A defaults comparison cannot see a
hardcoded literal at the seam, and `_run_invisible` passed the entire suite with
`controlnet_conditioning_scale` pinned to a constant. Each of the two knobs also
reaches the engine through TWO paths -- `remove_all` versus `remove_batch(mode="all")`
for `force`, `_run_invisible` versus `_batch_engine` for the scale -- and guarding one
left the other free to hardcode with a green suite. So:

  * `test_every_field_arrives_at_the_engine_with_the_caller_s_value` drives the real
    seam with all 13 fields set off their defaults; mutating any one of them to its
    default fails it.
  * `test_force_reaches_the_scrub_gate_in_every_scrubbing_mode` and
    `test_batch_controlnet_scale_flows_to_the_cached_engine` are parametrized over
    both modes, so neither path can be pinned alone.

Also fixes an order-dependent test surfaced by the added tests reshuffling the xdist
shards. `test_visible_path_decodes_file_once` counted every `image_io.imread` in the
process, but the Gemini engine loads its own bundled capture assets on first
construction, so the count was 3 on a cold engine and 1 on a warm one and the test
passed only when an earlier test happened to build the engine first. It now counts
decodes of the SOURCE, which is the invariant it exists for, and still fails when the
shared decode is broken. The production path was never wrong: the source bitmap is
decoded exactly once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-08-04 19:33:54 -07:00
co-authored by Claude Opus 5
parent 13095fb45c
commit 4a896cd4b5
8 changed files with 227 additions and 39 deletions
+15 -7
View File
@@ -87,13 +87,21 @@ over:
- `remove_all`, returning a `RemoveAllResult` after the visible, invisible, and
metadata stages
- `remove_batch`, returning a `BatchSummary` for one directory and one mode
- `InvisibleOptions`, the invisible stage's knobs as one immutable value. Every
default mirrors `InvisibleEngine`, so a bare `InvisibleOptions()` behaves
exactly like calling the engine with no arguments. Two silently stopped:
`max_resolution=None` reached `_target_size`'s `max_resolution > 0` and raised
`TypeError` on every library call, and `cpu_offload=True` made a library run
slower than the identical CLI run. `TestInvisibleOptionsMirrorTheEngine`
compares the two signatures field by field
- `InvisibleOptions`, the invisible stage's knobs as one immutable value. Engine
knobs only, under the engine's own names and defaults, so a bare
`InvisibleOptions()` behaves exactly like calling the engine with no arguments.
The engine takes them across two callables, `__init__` for what shapes the
loaded stack and `remove_watermark` for the per-image ones, so `_run_invisible`
forwards each field to the right one rather than splatting the whole bag. Two
defaults silently stopped
mirroring: `max_resolution=None` reached `_target_size`'s `max_resolution > 0`
and raised `TypeError` on every library call, and `cpu_offload=True` made a
library run slower than the identical CLI run.
`TestInvisibleOptionsMirrorTheEngine` compares the two signatures field by
field, and deliberately keeps no exception table: a field needing one is a
field that belongs elsewhere. `force` was such a field, and it decides whether
the engine runs rather than how, so it is a parameter of `remove_all` and
`remove_batch` next to `backend` and `sensitivity`
- `MetadataStripIncomplete`, raised before any write when AI metadata survives
`remove_all` reports progress as `(stage, detail)` pairs of stable tokens, not
+7 -1
View File
@@ -106,11 +106,17 @@ from remove_ai_watermarks import InvisibleOptions
raiw.remove_all(
"input.png",
"clean.png",
invisible=InvisibleOptions(strength=0.35, force=True),
invisible=InvisibleOptions(strength=0.35),
force=True,
progress=print,
)
```
`InvisibleOptions` carries only what `InvisibleEngine` itself takes, and uses the
engine's own parameter names and defaults. `force`, which decides whether the
engine runs at all, is a parameter of `remove_all` and `remove_batch` alongside
`backend` and `sensitivity`.
If AI metadata survives the strip, `remove_all` raises `MetadataStripIncomplete`
**before** writing anything: an AI-readable output is worse than no output.