A Chrome extension for working with Google Gemini images at scale, entirely on your own machine:
- Generate in bulk — hand it a CSV of prompts and it drives Gemini for you.
- Strip watermarks in bulk — remove Gemini's corner watermark with the LaMa inpainting model, running locally in WebAssembly.
The two compose: generated images are cleaned as they arrive, so one CSV gives you a zip of finished images. No server, no API key, no account, and no image ever leaves your computer.
A fork of dinoBOLT/Gemini-Watermark-Remover, substantially extended. See Changes from upstream.
Drop in a CSV of prompts and the extension drives the Gemini web UI in your own browser — new chat, type, send, collect the image — then optionally runs each result straight through the watermark cleaner.
prompt,name
"A friendly cartoon lion reading a book, soft pastel colours",lion-reading
"A paper-craft origami mosque at sunrise, warm light",origami-mosqueOne prompt per line also works, with no header at all. The optional name column
sets the output filename; otherwise names are derived from the prompt. A ready-made
example lives at examples/prompts-example.csv.
Warning
This automates gemini.google.com, which Google's terms prohibit. The account you run it under can be rate-limited or suspended. It also breaks whenever Google changes their page. For anything you depend on, use the official API instead — it is supported, stable, and its images carry no visible watermark at all, so the cleaning half becomes unnecessary. It costs roughly $0.02–0.039/image with a free tier of ~500/day.
The macro fails stop, not stealthy: on any rate limit, refusal, or human check it halts and tells you. There is deliberately no fingerprint spoofing, CAPTCHA solving, or account rotation.
Gemini stamps a visible watermark into the bottom-right corner of generated images. This tool masks that corner and asks the LaMa (Large Mask Inpainting) model to reconstruct what belongs there, then composites the repaired patch back onto the original at full resolution. Everything outside the watermark corner is left byte-for-byte untouched.
- Bulk by default — queue any number of images; each is processed in turn with a live per-image status, and one failure never aborts the run
- Download one or all — click any finished card to save that image, or take
the whole run as a
.zip. Single images also get a before/after slider - Original filenames preserved —
sunset.pngbecomessunset-clean.png - Off the main thread — inference runs in a Web Worker, so the tab stays responsive during a long batch
- Local and private — verifiable in DevTools: no network requests are made after the page loads
It removes the visible watermark only. Gemini images also carry SynthID, an invisible watermark encoded into the pixels themselves. Inpainting a corner does not touch it, and these images remain identifiable as AI-generated. Do not treat the output as untraceable.
It is slow: expect a few seconds per image. A 168-image batch takes tens of minutes. It runs unattended, but it is not fast — see Why it is slow for the specifics.
The watermark must be in the bottom-right. The masked region is fixed
(CONFIG.WATERMARK in src/js/config.js). Watermarks
elsewhere are not detected.
Keep the tab open. All work happens in that tab and results live in memory until you download them. Closing it cancels the batch and discards anything you have not saved.
Requires Google Chrome 90+ (or any Chromium browser) and about 250MB of disk.
1. Get the code
git clone https://github.com/athm793/Gemini-Bulk-Studio.git2. Download the AI model — the extension will not work without it
The model is ~198MB, which is over GitHub's 100MB file limit, and this project does not use Git LFS. So it is a manual download:
Put it in src/assets/:
Gemini-Bulk-Studio/
├── src/assets/lama_fp32.onnx <-- here (~198MB)
├── manifest.json
└── ...
3. Load it in Chrome
- Go to
chrome://extensions/ - Turn on Developer mode (top-right)
- Click Load unpacked and select the folder containing
manifest.json
- Click the extension icon — it opens the app in a new tab
- Drop in one image or hundreds
- Wait. Progress and a per-image log are shown as it goes
- Download all as .zip, or Download Image for a single result
Files land in your Chrome download folder (usually ~/Downloads).
The model runs single-threaded on the CPU, and that is a hard constraint rather than an oversight. Both escape routes are closed:
Multi-threading is impossible. ONNX Runtime spawns its WASM threads as
workers loaded from blob: URLs. Manifest V3 pins extension pages to
script-src 'self' 'wasm-unsafe-eval' and
forbids relaxing it,
so those workers are blocked outright:
Loading the script 'blob:...' violates the following Content Security Policy
directive: "script-src 'self' 'wasm-unsafe-eval'"
This holds even though the page is cross-origin isolated and
SharedArrayBuffer is available (see the COOP/COEP keys in
manifest.json) — the blocker is the CSP on blob: scripts,
which SAB availability does not change.
WebGPU does not work with this model. It was tried on ONNX Runtime 1.17 and 1.27 and fails identically at run time, on every image:
[WebGPU] Kernel "[Add] /generator/model/model.5/conv1/ffc/convg2g/Add" failed.
Error: Can't perform binary op on the given tensors
The WebGPU EP cannot broadcast the Add inside LaMa's Fourier convolutions. The
session builds successfully and only fails once inference runs, which makes it
look supported when it is not. The GPU path was removed rather than left in as
dead weight.
If you want to revisit this, the blocker is that one kernel — the question is whether a newer ONNX Runtime can broadcast it, not whether WebGPU is "supported".
The realistic remaining lever is an int8-quantized model, worth roughly 2–4x
on CPU. The current lama_fp32.onnx is full fp32.
index.html App shell and UI
styles.css Styles
manifest.json MV3 manifest (+ COOP/COEP, gemini.google.com host permission)
background.js Opens the app when the toolbar icon is clicked
BRAIN.html Full architecture, build log, dead ends, risks
examples/ Example prompt CSV
src/
lib/ ONNX Runtime 1.17.1 + its WASM binaries
assets/ lama_fp32.onnx (you download this)
js/
app.js Orchestration: both queues, tabs, downloads
config.js All tunables and constants, with the reasoning
ui-manager.js DOM, batch grid, progress, comparison slider
model-manager.js Owns the inference worker and talks to it
inference-worker.js Runs ONNX Runtime off the main thread
image-processor.js Pre/post-processing and final composition
macro.js Prompt queue; talks to the content script
gemini-content.js Content script: drives the Gemini page
csv.js RFC 4180 parser + prompt extraction
zip.js Minimal store-only ZIP writer
utils.js Validation, logging, helpers
BRAIN.html is the deep reference — architecture, build log, the
dead ends below in full, and the risk register. Open it in a browser.
The pipeline per image: decode → resize to 512×512 → build an RGB tensor plus a mask over the watermark corner → run LaMa in the worker → convert the output back to pixels → draw only the repaired corner onto the untouched original → encode to PNG.
Images are processed sequentially. They share one InferenceSession, so
concurrent runs would contend for the same WASM heap rather than overlap.
I forked dinoBOLT/Gemini-Watermark-Remover at v3.0.0. At that point it was a capable but strictly single-image watermark remover:
- Drop one image → it masks the corner, inpaints with LaMa, shows a before/after slider, and downloads the result.
- Selecting multiple files silently kept only the first (
files[0]) and discarded the rest. - Inference ran inline on the main thread, so a large image froze the tab.
- No way to generate images — you made them by hand in Gemini first.
- Six source modules (
app,config,image-processor,model-manager,ui-manager,utils) totalling ~356 lines of the parts I later changed.
It worked, and the core inpainting approach is still upstream's. Everything below is what I added or changed on top of it.
Roughly +3,300 lines across 19 files, including six new modules. The headline is that it went from a one-at-a-time cleaner to a bulk studio — generate from a CSV and clean at scale — hence the rename to Gemini Bulk Studio.
New: generate images from a CSV (entirely mine — no upstream equivalent)
- A second mode that drives the Gemini web UI from a list of prompts and collects the images, cleaning each one as it arrives.
csv.js— a proper RFC 4180 parser (prompts are prose full of commas and quotes), with 13 unit tests including Excel BOM/CRLF and non-Latin scripts.macro.js— the sequential prompt queue, fail-stop on any rate limit or refusal, configurable pacing.gemini-content.js— the content script that types, sends, and extracts the rendered image (the extraction had to work around Gemini revoking its ownblob:URLs — see How it works).- In-app help explaining exactly how to build the CSV, plus a bundled example.
New: bulk cleaning (upstream was single-image only)
- Multi-file select and drag-drop, a sequential queue, and a live results grid with per-image status. One failure no longer aborts the run.
zip.js— a hand-rolled store-only ZIP writer to download a whole batch at once (CSP blocks CDN libraries; PNGs are already compressed).- Click any finished card to download that one image.
- Output keeps the source filename (
x.png→x-clean.png) instead ofgemini-clean-<timestamp>.png.
Security and correctness fixes (bugs I found in the upstream code)
- Fixed an XSS hole: the log and error toast rendered messages via
innerHTML, and messages embed user-supplied filenames — so a file named<img src=x onerror=...>.pngwould execute inside a privileged extension page. Everything builds DOM nodes now. - Fixed an unbounded DOM leak:
LOG_MAX_LINEStrimmed the log array but not the log element, so the on-page log grew without limit. - Fixed the progress bar jumping backwards from 50% → 0% when a batch followed a model load.
Performance and memory (rearchitected upstream's approach)
- Moved inference off the main thread into a Web Worker (
inference-worker.js). Upstream ran it inline, which triggered Chrome's "Page unresponsive" dialog on long runs. - Results are
Blobs behind object URLs instead of base64 data URLs (~33% smaller, revocable); decoded bitmaps areclose()d per image, so peak memory stays flat across a batch instead of growing linearly. - Tensor buffers are transferred to the worker rather than copied.
- Raised
graphOptimizationLevelfrombasictoall.
Documentation
- Added
BRAIN.html— full architecture, a build log, the dead ends below, and a risk register. - Investigated and rejected two speed routes, documented in
src/js/config.jsso they are not rediscovered: WASM multi-threading (blocked by MV3's CSP onblob:workers) and WebGPU (a broken kernel on this model, on both ORT 1.17 and 1.27). See Why it is slow.
What I kept from upstream: the LaMa inpainting model and the core mask-and-composite approach, the Apache-2.0 license, and the general UI shape of the cleaning flow. Credit for those is upstream's.
Everything runs locally. Image processing makes no network requests at all — the model is read from disk and inpainting happens in WebAssembly. You can verify this in DevTools → Network while cleaning.
The only host permission is gemini.google.com, used solely by the generate
feature to drive the page you are already signed into. Nothing is sent anywhere
else, and the cleaning side needs no network access whatsoever.
- LaMa — Resolution-robust Large Mask Inpainting with Fourier Convolutions
- ONNX Runtime Web — onnxruntime.ai
- Original project — dinoBOLT/Gemini-Watermark-Remover
Apache License 2.0 — see LICENSE. Modifications from the original are listed under Changes from upstream.