# AGENTS.md ## Best-Effort Product Principle - Treat best-effort processing as the minimum product guarantee: when the input is available and processing can run, return the best result the current system can produce. - A result with visible residuals or minor imperfections is preferable to returning the unchanged input or no result. - Low confidence, incomplete catalog coverage, or a quality warning should not by themselves block output. Use the most plausible validated/fallback strategy, produce the result, and surface the uncertainty as non-blocking metadata or UI copy. - Do not tell users to refresh or retry when repeating the same action is not expected to change the outcome. Retry guidance is allowed only when there is a concrete, recoverable state change and the user can take an actionable step. - Reserve hard failure for cases where processing truly cannot run or no usable input/output can be produced; do not use failure as a substitute for a best-effort result. ## Debug Workflow ### Supported Sample Scope - Quality fixes target original Gemini outputs. Inputs with repeated watermarking, screenshots, or subsequent image edits are outside the required support scope; do not add special processing solely to accommodate them. - Classify sample provenance separately from processing quality. Keep confirmed edited exports as observations, with the evidence for exclusion; retain unknown provenance as unknown. Neither unusual dimensions, missing metadata, nor poor removal quality establishes editing history. - An unknown-source upload can motivate an investigation, but does not by itself establish a defect in supported original outputs. Reproduce the suspected issue on a verified original or a controlled fixture before promoting a fix. Do not silently relabel unresolved samples as passing or edited to satisfy a release gate. - This scope is an investigation and acceptance rule, not a new runtime rejection rule. Preserve existing best-effort behavior and content-damage regression controls. ### Allenk Upstream Reference - The local fork of allenk/GeminiWatermarkTool is at `${GWR_ALLENK_ROOT}`. - Local-only path variables are configured in `.env`; use `.env.example` as the public template. - When learning or comparing upstream watermark catalog specs, alpha maps, video rules, FDnCNN behavior, or CLI behavior, prefer this local fork over temporary clones or remote README-only assumptions. - Treat upstream specs as candidate priors until they are verified against this repo's sample scoring, crop sheets, and output residual gates. ### Data-Driven Watermark Investigation - When user-provided samples show obvious watermarks being skipped or poorly removed, treat the first task as pattern discovery, not threshold tuning. - Derive the watermark geometry and rendering rules from the samples before changing removal heuristics: - exact anchor position (`x/y`, right/bottom margins) - watermark size and aspect - subpixel offset / scale drift - alpha map shape and alpha strength - background-dependent compositing behavior - Build batch reports and visual artifacts from the sample set: - full image list with dimensions and scores - bottom-right crop sheets - candidate-position overlays when debugging detection - before/after crops for every changed strategy - Prefer improving candidate localization and alpha estimation over loosening safety/protection gates. - Safety gates are a final fallback. If a visible watermark is skipped, first ask whether the selected position/size/alpha candidate is wrong or incomplete. - Do not generalize from a single image when the user supplied a sample set. Cluster samples by size, anchor, background, and residual behavior, then make the smallest algorithm change supported by that cluster. - When residual artifacts remain after mathematically valid inverse alpha removal, do not assume stronger inpaint, preview-only priors, or subpixel sweeps are safe production fixes. First verify whether the real alpha edge profile / antialiasing model differs from the current template, and keep any visual cleanup evidence-gated. - Do not treat a restoration score measured only with the candidate's own selected alpha map and geometry as independent cleanliness or damage evidence. A wrong candidate can erase the pattern it scores against while leaving an obvious hole, shifted star, or larger residual outside that ROI. Use those self-consistent scores for candidate ranking only; promotion evidence should come from frozen cross-profile/decoy signals and blind visual review. - Preserve signed source polarity when a rescue assumes a white alpha template. A localized gradient or edge witness can be strong even when the spatial template polarity is inverted; the fresh exact-48 R96 case 77 passed the decoy/edge gates but inverse removal turned it into a dark star. Keep the exact-48 R96 white source-witness gate constrained to `spatialSignedTarget > 0`; do not replace this with an absolute/max score or generalize it to other polarity models without independent validation. - Issue #123 independently confirmed the same collision class at exact-96 R192: a near-zero negative spatial witness with highly localized unsigned edges can be structured dark content, and applying the white `20260520` rescue creates a visible black hole. Require positive signed spatial evidence for the normal white rescue. Only retain the established structured-content exception when a drifted dark-polarity selection independently exists and the exact anchor has sufficiently strong, localized spatial evidence; a gradient/edge witness alone is not enough. - Treat `texture.hardReject`, `visibleDarkHole`, and selection-stage damage warnings as noisy review signals, not independent cleanliness truth. Confirmed clean exact-48 rescues can trigger them, while visible dark residuals can still retain a top-level `clean` status. Do not directly map these signals to `qualityStatus`, candidate ranking, blocking, or retry behavior without frozen independent validation. - Background-endpoint consistency is not by itself a safe gate for row/patch inpainting. A discovery-only endpoint threshold improved one dark-hole sample but later triggered on a clean fresh exact-48 rescue and introduced visible horizontal banding. Keep such repair offline until a preregistered temporal holdout demonstrates both useful recall and no clean-output damage. ## Deployment Note - The active local/debugging build surface is the generated `dist/` directory. - Keep deployment assumptions aligned with the current repo contents. ### Website SDK Release Sync - `https://geminiwatermarkremover.io/` is deployed from the separate local `geminiwatermarkremover.io` repository; resolve its checkout from local configuration instead of committing a machine-specific absolute path. - Publishing this repository to npm, GitHub Releases, or the Chrome Web Store does **not** update the website's image-processing Worker. The website pins `@pilio/gemini-watermark-remover` in both `package.json` and `pnpm-lock.yaml` and must be upgraded, rebuilt, tested, and deployed separately. - After every core package release, compare the website's pinned SDK version with npm `latest` before treating the website as current. A current website userscript or extension download does not prove that the website Worker uses the current SDK. - Verify the deployed Worker itself with a real sample and inspect its selected candidate / strategy metadata or other version-specific behavior. Do not rely only on CDN asset timestamps, package release status, userscript `@version`, or successful HTTP responses. - Known failure mode from issue #115: the public site still bundled SDK `1.0.31` after `1.0.33` was published. Upgrading exposed a second path mismatch: the browser engine loaded the real `36-v2` template while the sync image-data SDK interpolated size 36, allowing local validation to select `48x48` while the deployed Worker skipped. Keep runtime alpha-map catalogs and candidate orchestration aligned across SDK entry points. - Treat the website as a separate release surface. If its main worktree contains unrelated user changes, prepare and validate the dependency upgrade in an isolated worktree so those changes are not overwritten or accidentally deployed. ### Gemini Image Size / Watermark Catalog Use this catalog when checking watermark size and anchor regressions against Gemini image outputs. Current code expectation: - `0.5K` preview outputs use a `48x48` watermark with `32px` right/bottom margins. - `1K`, `2K`, and `4K` preview/full outputs use a `96x96` watermark with `64px` right/bottom margins. - New reported anchor type: exact `1K` / `2K` / `4K` catalog sizes also consider a secondary `96x96` candidate with `192px` right/bottom margins. Keep the canonical `64px` anchor first; the `192px` anchor must remain evidence-gated by candidate validation. - Known confirmed local exception: `2816x1536` uses `96x96` with `192px` right/bottom margins (`2k-new-margin` in `src/core/geminiSizeCatalog.js`). - The local catalog also keeps a confirmed `1408x768` 16:9 `1K` variant. #### Gemini 3.1 Flash Image Preview | Aspect ratio | 512 resolution | 0.5K tokens | 1K resolution | 1K tokens | 2K resolution | 2K tokens | 4K resolution | 4K tokens | |---|---|---:|---|---:|---|---:|---|---:| | **1:1** | 512x512 | 747 | 1024x1024 | 1120 | 2048x2048 | 1680 | 4096x4096 | 2520 | | **1:4** | 256x1024 | 747 | 512x2048 | 1120 | 1024x4096 | 1680 | 2048x8192 | 2520 | | **1:8** | 192x1536 | 747 | 384x3072 | 1120 | 768x6144 | 1680 | 1536x12288 | 2520 | | **2:3** | 424x632 | 747 | 848x1264 | 1120 | 1696x2528 | 1680 | 3392x5056 | 2520 | | **3:2** | 632x424 | 747 | 1264x848 | 1120 | 2528x1696 | 1680 | 5056x3392 | 2520 | | **3:4** | 448x600 | 747 | 896x1200 | 1120 | 1792x2400 | 1680 | 3584x4800 | 2520 | | **4:1** | 1024x256 | 747 | 2048x512 | 1120 | 4096x1024 | 1680 | 8192x2048 | 2520 | | **4:3** | 600x448 | 747 | 1200x896 | 1120 | 2400x1792 | 1680 | 4800x3584 | 2520 | | **4:5** | 464x576 | 747 | 928x1152 | 1120 | 1856x2304 | 1680 | 3712x4608 | 2520 | | **5:4** | 576x464 | 747 | 1152x928 | 1120 | 2304x1856 | 1680 | 4608x3712 | 2520 | | **8:1** | 1536x192 | 747 | 3072x384 | 1120 | 6144x768 | 1680 | 12288x1536 | 2520 | | **9:16** | 384x688 | 747 | 768x1376 | 1120 | 1536x2752 | 1680 | 3072x5504 | 2520 | | **16:9** | 688x384 | 747 | 1376x768 | 1120 | 2752x1536 | 1680 | 5504x3072 | 2520 | | **21:9** | 792x168 | 747 | 1584x672 | 1120 | 3168x1344 | 1680 | 6336x2688 | 2520 | #### Gemini 3 Pro Image Preview | Aspect ratio | 1K resolution | 1K tokens | 2K resolution | 2K tokens | 4K resolution | 4K tokens | |---|---|---:|---|---:|---|---:| | **1:1** | 1024x1024 | 1120 | 2048x2048 | 1120 | 4096x4096 | 2000 | | **2:3** | 848x1264 | 1120 | 1696x2528 | 1120 | 3392x5056 | 2000 | | **3:2** | 1264x848 | 1120 | 2528x1696 | 1120 | 5056x3392 | 2000 | | **3:4** | 896x1200 | 1120 | 1792x2400 | 1120 | 3584x4800 | 2000 | | **4:3** | 1200x896 | 1120 | 2400x1792 | 1120 | 4800x3584 | 2000 | | **4:5** | 928x1152 | 1120 | 1856x2304 | 1120 | 3712x4608 | 2000 | | **5:4** | 1152x928 | 1120 | 2304x1856 | 1120 | 4608x3712 | 2000 | | **9:16** | 768x1376 | 1120 | 1536x2752 | 1120 | 3072x5504 | 2000 | | **16:9** | 1376x768 | 1120 | 2752x1536 | 1120 | 5504x3072 | 2000 | | **21:9** | 1584x672 | 1120 | 3168x1344 | 1120 | 6336x2688 | 2000 | #### Gemini 2.5 Flash Image | Aspect ratio | Resolution | Tokens | |---|---|---:| | 1:1 | 1024x1024 | 1290 | | 2:3 | 832x1248 | 1290 | | 3:2 | 1248x832 | 1290 | | 3:4 | 864x1184 | 1290 | | 4:3 | 1184x864 | 1290 | | 4:5 | 896x1152 | 1290 | | 5:4 | 1152x896 | 1290 | | 9:16 | 768x1344 | 1290 | | 16:9 | 1344x768 | 1290 | | 21:9 | 1536x672 | 1290 | ### Fixed Tampermonkey / Gemini Environment - Fixed Chrome profile: `.chrome-debug/tampermonkey-profile` - Fixed CDP port: `9226` - Default proxy: `http://127.0.0.1:7890` - Production userscript artifact: `dist/userscript/gemini-watermark-remover.user.js` Platform notes: - macOS launcher uses `/usr/bin/open -na "Google Chrome.app" --args ...` - macOS default Chrome app lookup: - `/Applications/Google Chrome.app` - `~/Applications/Google Chrome.app` - Override Chrome location on any platform with `GWR_DEBUG_EXECUTABLE_PATH` ### Open the Fixed Profile - Shell launcher: `./scripts/open-fixed-chrome-profile.sh` - Node launcher: `node scripts/open-tampermonkey-profile.js --cdp-port 9226` - CMD launcher: `.\scripts\open-fixed-chrome-profile.cmd` - PowerShell launcher: `.\scripts\open-fixed-chrome-profile.ps1` Default behavior: - Reuse the fixed Chrome profile - Open remote debugging on port `9226` - Use the local proxy - The plain Node launcher opens the Tampermonkey Chrome Web Store page by default unless `--url` is passed - The shell wrapper opens the local probe page on `http://127.0.0.1:4173/tampermonkey-worker-probe.html` by default and still forwards extra args - The current CMD/PowerShell wrappers are fixed shortcuts to the same local probe page; they do not forward additional CLI args macOS quick path: 1. Run `pnpm build` 2. Start the local artifact server, for example `pnpm dev` 3. Open the fixed profile with `./scripts/open-fixed-chrome-profile.sh --url https://gemini.google.com/app` 4. If you do not want the repo's default proxy, pass `--proxy off` ### One-Time Manual Setup Do this only once in the fixed profile: 1. Install Tampermonkey. 2. Enable `Allow User Scripts` in Chrome extension details. 3. Keep Developer Mode enabled. 4. Install `public/tampermonkey-worker-probe.user.js` when local probe validation is needed. 5. Install or reinstall the production userscript from the current local build server when validating the latest build. - Use the active local build server URL printed by `pnpm dev` - `pnpm dev` starts probing from `http://127.0.0.1:4173/` and will auto-increment if that port is occupied - A previously confirmed request-layer debugging session used `http://127.0.0.1:4317/userscript/gemini-watermark-remover.user.js`, but do not assume that is still current ### Local Build and Services - Production build: `pnpm build` - Local dist server: `pnpm dev`, `pnpm serve`, or the active local build server for this worktree - Default `pnpm dev` start port: `http://127.0.0.1:4173/` - Actual active dev server port may be higher; always trust the current `pnpm dev` console output - Probe smoke test: `pnpm probe:tm` - Installed userscript freshness check: `pnpm probe:tm:freshness` - Open fixed profile: `pnpm probe:tm:profile` Current `pnpm probe:tm` behavior: - in `run` mode it now attempts a Tampermonkey userscript freshness preflight first - if freshness returns `stale`, `probe:tm` must fail before running the worker/bridge smoke page - if the freshness preflight context itself is unavailable, for example: - fixed `9226` profile is not open - the CDP endpoint is unavailable - the editor is mid-navigation then the preflight is recorded as `skipped` and the smoke flow continues - this keeps stale installs fail-fast without making `probe:tm` hard-depend on a manually opened editor page ### Installed Userscript Freshness Check When real-page behavior does not match the current worktree, verify the installed userscript body, not just the script name or `@version`. - Same `@version` does not guarantee the fixed profile is running the latest build. - A stale Tampermonkey script can still show: - `[Gemini Watermark Remover] Initializing...` - `[Gemini Watermark Remover] Ready` - while silently missing newer request-layer fixes such as the download sticky intent window - Preferred check: 1. Open the Tampermonkey editor for `Gemini NanoBanana 图片水印移除` 2. Run `pnpm probe:tm:freshness` 3. Read `.artifacts/tampermonkey-freshness/latest.json` - If you just reinstalled the userscript and the check still reports `stale`, refresh the already-open Tampermonkey editor page once, then run `pnpm probe:tm:freshness` again. - Current command behavior: - exits `0` when the installed userscript exactly matches the local `dist/userscript/gemini-watermark-remover.user.js` - exits `1` when the installed userscript is stale or mismatched - compares full normalized source hashes, not just `@version` - also reports whether expected markers are missing - Current report path: - `.artifacts/tampermonkey-freshness/latest.json` - Manual fallback if needed: 1. Compare the installed source against `dist/userscript/gemini-watermark-remover.user.js` 2. Confirm the installed source contains the expected newer markers before continuing real-page debugging - `DEFAULT_DOWNLOAD_STICKY_WINDOW_MS` - `downloadStickyUntil` - `getActionContextFromIntentGate(intentGate = null, candidate = null)` 3. Refresh the real Gemini page after the fixed profile is updated ### Real Gemini Page Validation Target page: - `https://gemini.google.com/app` Minimum validation flow: 1. Run `pnpm build` 2. Reinstall the latest userscript in the fixed profile 3. Open the real Gemini page 4. Check that the console shows: - `[Gemini Watermark Remover] Initializing...` - `[Gemini Watermark Remover] Ready` 5. If bridge validation is needed, trigger from page side: - `gwr:userscript-process-request` - Expect `gwr:userscript-process-response` Current confirmed request-layer behavior on the fixed profile: - `copy` can populate the strict original binding path through real `rd-gg` asset fetches and then place a processed `image/png` onto the clipboard - `download` stays on Gemini's native `c8o8Fe -> gg-dl -> rd-gg-dl` export flow; the userscript does not cancel the click - the userscript keeps explicit download intent alive for Gemini download asset URLs long enough to catch late `rd-gg-dl` requests on the native chain - If the original URL binding is unavailable when the required download/original request arrives, the action must fail closed with: - `无法获取原图,请刷新页面后重试` - A successful real-page full-size download currently produces: - a browser `download` event - a blob-backed saved file such as `Gemini_Generated_Image_vusbaevusbaevusb.png` - local detector result `skipReason=no-watermark-detected` ### Real-Page Pixel Verification - Single image compare: `pnpm probe:real-page:compare` - All ready images on the current Gemini page: `pnpm probe:real-page:compare --all` - Latest batch summary: - `.artifacts/real-page-pixel-compare/latest-summary.json` - Complex-figure validation checklist: - `docs/complex-figure-verification-checklist.md` Use this when page-level screenshots are not enough and you need original blob pixel metrics for `before/after`. Do not rely on hardcoded sample counts here. Treat `.artifacts/real-page-pixel-compare/latest-summary.json` as the source of truth for the current worktree. Current checked local summary in this worktree: - artifact timestamp: `2026-04-06T07:25:13.120Z` - `total = 1` - the recorded ready image landed at: - `afterSpatial ~= -0.2707` - `afterGradient ~= 0.1075` Historical multi-image baselines from earlier fixed-profile sessions should be treated as dated reference points, not as the current expected batch shape. ### Current Preview Display Path Current production expectation for real Gemini preview display is: - keep request-layer preview interception enabled as an early processing source and observability point - do not assume Gemini's final displayed `blob:` image is fully controlled by the page `fetch` hook chain - keep `src/shared/pageImageReplacement.js` as the production display path for preview replacement - treat request-layer preview handling as supportive, but rely on page-level replacement to guarantee the user-visible preview is actually de-watermarked Current real-page success signal for preview display: - the displayed image reaches: - `data-gwr-page-image-state=ready` - `data-gwr-watermark-object-url=blob:...` - detector on that processed overlay blob reports: - `skipReason=no-watermark-detected` ### Confirmed Performance Pitfalls When the user reports "this version became much slower", check these first before touching the core algorithm: 1. Page runtime / page bridge did not actually install into the real Gemini page. - Symptom: - Real page silently falls back to the userscript sandbox / slow main-thread path. - Earlier bad runs showed `removeWatermarkMs` on the order of `11s ~ 13s` for a single preview image. - Verify: - Reinstall the latest userscript from the current active build server - use the actual userscript URL from the active `pnpm dev` server - the server starts probing from `http://127.0.0.1:4173/` and may auto-increment - Refresh the real page - Confirm console reaches `Initializing...` and `Ready` - Confirm preview images continue to `page image process success` 2. Preview queue blocked by a `blob:` image that is not renderable yet. - Symptom: - One image gets stuck at `state=processing` - The element often has `complete=false`, `naturalWidth=0`, `naturalHeight=0` - Later images stop progressing because the serial queue is effectively wedged - Current fix: - `src/shared/pageImageReplacement.js` now waits for renderability and retries instead of processing immediately - If this regresses, inspect the waiting / retry path before changing watermark math 3. Preview-anchor cleanup accidentally doing expensive work that is not adopted. - Symptom: - Main thread is busy, but output source does not include a successful `+subpixel` - Earlier bad runs showed `subpixelRefinementMs ~= 80ms ~ 115ms` on strong preview samples with no accepted subpixel shift - Current fix: - preview-anchor cleanup no longer runs the expensive subpixel refinement path - It relies on cheaper preview edge cleanup instead - Rule: - Do not re-enable preview-anchor subpixel search unless you have a real fixture that proves the accepted result is both safer and materially better ### Confirmed Quality / Performance Tradeoff For strong real-page preview samples, the current strategy is: - Skip expensive preview-anchor subpixel refinement - Use stronger preview edge cleanup only when: - the image is a preview-anchor style match - spatial residual is already low enough to be safe - gradient residual is still strong enough to justify cleanup Why this exists: - It lowers strong-sample real-page residual gradient from roughly `0.53` to roughly `0.30` - It keeps preview-anchor cleanup latency low by avoiding no-op subpixel sweeps - It accepts some spatial drift to stay within a safe residual envelope rather than overfitting and risking content damage ### Confirmed Download / Copy Integration Constraint Do not re-enable the old active direct-download click hook in production. Confirmed real-page failure mode on `https://gemini.google.com/u/1/app/d3cd7d14852ecd3b?pageId=none`: - When the userscript intercepts `下载完整尺寸的图片` at capture time and calls `preventDefault()/stopImmediatePropagation()`, Gemini's own download flow is blocked before it can issue its native `c8o8Fe` / `rd-gg-dl` chain. - In that state, the userscript only has the earlier history bootstrap bindings from `hNvQHb`. - Current real `hNvQHb` bindings are mostly preview-style `gg/...=s0` URLs, not the final native download URL. - Falling back to those preview bindings makes the userscript attempt its own fetch path too early, which previously surfaced as: - `Original image is unavailable for download processing` - or `Failed to fetch image: 403` - followed by the user-facing retry alert There is a second real-page failure mode to keep in mind even after removing the active click hook: - Gemini's native full-size download chain can be much slower than the base intent window. - On the 2026-04-04 fixed-profile trace: - `c8o8Fe` request started about `+50ms` after click - `c8o8Fe` response returned about `+22.4s` - final `rd-gg-dl ... image/png` arrived about `+23.9s` - A plain `5000ms` intent window expires far too early, so the passive request hook stops processing before the final full-size image request appears. Current correct production shape: - keep the intent gate for copy / download gestures - keep Gemini RPC discovery hooks (`hNvQHb`, `c8o8Fe`, related batchexecute responses) - keep generated-asset fetch interception for the native request flow - let Gemini continue its own click handling - do not block the button just to start a parallel userscript-only download path - keep a download-specific sticky intent window for Gemini download asset URLs - current default: `30000ms` - release it after terminal success/failure so it does not leak across actions Current confirmed real-page result with the passive native chain plus sticky download intent: - `下载完整尺寸的图片` produced a browser `download` event - the resulting download used a blob URL generated from the page flow - the saved file was `3136 x 1344`, about `5.4MB`, sha256 `4e945813779b58a5eda0f01f7973c924210477a84ae1d3826138f57b60eb691f` - local detector on that downloaded file reported: - `skipReason = no-watermark-detected` - `originalSpatialScore ~= -0.4096` - `originalGradientScore ~= 0.0826` - no `无法获取原图,请刷新页面后重试` alert appeared - `复制图片` wrote an `image/png` item to the clipboard without a failure alert - the request-layer verification report path is session-specific; do not assume an older dated artifact still exists in the current worktree ### Worker Debug Flow For reproduction only. This is not the default production path. 1. In the real page DevTools, run: - `localStorage.setItem('__gwr_force_inline_worker__', '1')` 2. Refresh `https://gemini.google.com/app/...` 3. Inspect console logs Current confirmed result: - The real Gemini page can attempt to start the inline worker. - The worker crashes during startup because of CSP / runtime restrictions. - Production must stay on the main-thread path by default. - The force flag is for debugging only. ### Worker Success / Failure Criteria Do not treat `new Worker(blobUrl)` returning without an immediate throw as proof that the worker is usable. Current correct criteria: - If `[Gemini Watermark Remover] Worker acceleration enabled` appears, that only means startup was attempted. - The worker is only considered usable if the startup handshake succeeds. - If `[Gemini Watermark Remover] Worker initialization failed, using main thread: ...` appears, safe fallback has happened. - After fallback, the page should still continue with: - `page image process start` - `page image process strategy` - `page image process success` ### Known Constraints - Direct `new Worker(blobUrl)` from Tampermonkey DOM sandbox is not reliable in the current environment. - The real Gemini page has CSP restrictions, so worker assumptions must not be based on probe-page success. - Runtime flags must be read across `unsafeWindow`; reading only the userscript sandbox `globalThis/localStorage` is insufficient.