Skip to content

feat([issue-912]): regenerate gallery images to defeat SynthID watermarks#958

Merged
atomantic merged 6 commits into
mainfrom
claim/issue-912
Jun 5, 2026
Merged

feat([issue-912]): regenerate gallery images to defeat SynthID watermarks#958
atomantic merged 6 commits into
mainfrom
claim/issue-912

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Adds a post-hoc, history-only Regenerate action to the image gallery lightbox (next to Clean) that defeats SynthID watermarks the only honest way: by round-tripping the image through a local FLUX model.

The existing Clean only strips the C2PA caBX chunk + median/sharpen, which SynthID (Google's invisible per-pixel watermark) survives by design. Regenerate runs the source image through the already-present local-FLUX img2img path at low denoise (~0.4 default, 0.2–0.6), so composition holds but the per-pixel signal is overwritten by fresh sampling. It reuses the source image's own prompt and dimensions.

  • Hardware-gated. The lightbox button only appears when a local FLUX runner is installed (FLUX.2 venv healthy, or legacy mflux on macOS). GET /api/image-gen/regen/availability drives the gate; POST /api/image-gen/:filename/regenerate 400s with an actionable message when no runner exists. Confirmed available on the 128GB machine (Flux2KleinPipeline imports cleanly).
  • History-only, never auto-applied. Each run enqueues a normal local image job through the existing GPU lane (a separate regen lane would contend for the same MLX runtime and OOM — reusing the GPU lane is the correct shape) and lands as a new variant beside the original (kept). The lightbox variant switch toggles Original ↔ Regenerated.
  • Honest lineage. The output sidecar records regenerated: true, regenSteps, regenStrength, regenModelId (the issue's contract) plus cleanedFrom so the variant grouping picks it up.

Implementation reuses the proven generateImage/mediaJobQueue img2img pipeline — the only new render-path code is the regenOf lineage stamp threaded through buildSidecarMeta. New server/services/imageGen/regen.js owns the backend-availability gate, candidate ordering (source model first, then fast distilled), and pure param assembly.

Closes #912

Test plan

  • server/services/imageGen/regen.test.js — candidate ordering, regen capability/availability resolution (venv healthy / not installed / source-model preference), pure param assembly (prompt + dims fallbacks, regenOf).
  • server/services/imageGen/local.test.jsbuildSidecarMeta stamps regen lineage when regenOf is set and omits it otherwise.
  • server/routes/imageGen.test.js — availability endpoint, out-of-range-strength 400, missing-source 404.
  • client/src/components/media/variants.test.js — regen variants label as "Regenerated" and coexist with cleaned variants under one source.
  • All touched suites green (104 server tests in the regen/local/route files); client lint clean.

atomantic added 6 commits June 5, 2026 07:47
… watermarks

The lossless cleaner only strips the C2PA caBX chunk + median/sharpen, which
SynthID survives by design. Add a post-hoc, history-only 'Regenerate' action
beside 'Clean' in the gallery lightbox that round-trips the image through the
existing local-FLUX img2img path at low denoise (~0.4), overwriting the
per-pixel watermark with fresh sampling while preserving composition.

- regen.js: backend-availability gate (FLUX.2 venv health / mflux binary),
  candidate ordering (source model first, then fast distilled), and pure
  param assembly reusing the source's own prompt + dimensions.
- generateImage/buildSidecarMeta: thread regenOf to stamp regenerated/
  regenSteps/regenStrength/regenModelId + cleanedFrom (so the lightbox variant
  group toggles it as 'Regenerated').
- Route: GET /regen/availability (UI gate) + POST /:filename/regenerate
  (enqueues a normal local image job through the GPU lane — a separate lane
  would contend for the same MLX runtime and OOM, so reusing it is correct).
- UI: Regenerate button shown only when the local FLUX backend is installed.

Refs #912
…tton handlers

/simplify cleanups: run the source sidecar + dimension reads concurrently in
the regenerate route (no data dependency), and extract the shared
guard/try-finally/close pattern behind the Clean and Regenerate buttons into
one runBusyAction helper instead of copy-pasting it.
…-lineage guards

Address review (codex + claude):
- PR-blocker (codex): normalizeImage dropped the new regenerated/regen* sidecar
  fields, so the gallery's normalized items never carried the discriminator and
  regen variants rendered as 'Cleaned' in the real UI. Thread the fields through
  normalizeImage and add a 'Regenerated from …' branch to the lightbox lineage
  describer. Added a composed normalize→computeImageVariantGroup regression test
  (the original test used raw objects, missing the normalization layer).
- buildSidecarMeta only stamps regenerated/cleanedFrom when the init image
  actually resolved — a source that failed to resolve degrades to txt2img and
  must not falsely claim 'regenerated: true'.
- regen availability now re-checks when the Settings drawer closes (via
  reloadBackends), so installing the FLUX venv mid-session reveals the action
  without a hard reload.
…iant grouping at root

Address codex iteration-2 review (both PR-blocking):
- Restrict regen capability to FLUX.2 + legacy-mflux-on-macOS. The broader
  diffusers family (Z-Image/ERNIE/HiDream/Qwen) is excluded because
  z_image_turbo.py explicitly falls back to txt2img + ignores the init image
  when a model has no i2i sibling — the JS can't know the round-trip happened,
  so stamping regenerated:true there would be a false lineage claim.
- Anchor the regen's grouping lineage (cleanedFrom/regenOf) at the ROOT
  original (sourceMeta.cleanedFrom || filename) so regenerating a cleaned or
  already-regenerated variant produces a sibling that groups under the family,
  not an orphan; pixels still come from the clicked image.
@atomantic atomantic merged commit 91187bb into main Jun 5, 2026
1 of 2 checks passed
@atomantic atomantic deleted the claim/issue-912 branch June 5, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generative regen path for SynthID defeat (image cleaner) — research, hardware-gated

1 participant