@pilio/gemini-watermark-remover 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -127,11 +127,17 @@ node bin/gwr.mjs remove <input> --output <file>
127
127
  gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json]
128
128
  ```
129
129
 
130
- If you do not have `gwr` installed globally, use:
131
-
132
- ```bash
133
- pnpm dlx @pilio/gemini-watermark-remover remove <input> --output <file>
134
- ```
130
+ If you do not have `gwr` installed globally, use:
131
+
132
+ ```bash
133
+ pnpm dlx @pilio/gemini-watermark-remover remove <input> --output <file>
134
+ ```
135
+
136
+ The default CLI file decoder/encoder uses `sharp`. SDK consumers that only use browser or `ImageData` APIs do not need it. If you use the CLI file path in your own project, install `sharp` alongside this package:
137
+
138
+ ```bash
139
+ pnpm add sharp
140
+ ```
135
141
 
136
142
  ### Can't Remove Your Watermark?
137
143
 
@@ -255,11 +261,12 @@ const result = await removeWatermarkFromBuffer(inputBuffer, {
255
261
  - TypedArray (`Float32Array`, `Uint8ClampedArray`)
256
262
  - for the website copy button: `navigator.clipboard.write(...)` and `ClipboardItem`
257
263
 
258
- ### CLI And Skill
259
-
260
- - a local Node.js runtime capable of running this package and its dependencies
261
- - filesystem access for local input/output paths
262
- - for repo-local usage:
264
+ ### CLI And Skill
265
+
266
+ - a local Node.js runtime capable of running this package
267
+ - `sharp` installed when using the default CLI file decoder/encoder
268
+ - filesystem access for local input/output paths
269
+ - for repo-local usage:
263
270
 
264
271
  ```bash
265
272
  node bin/gwr.mjs remove <input> --output <file>
package/README_zh.md CHANGED
@@ -133,6 +133,12 @@ gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json]
133
133
  pnpm dlx @pilio/gemini-watermark-remover remove <input> --output <file>
134
134
  ```
135
135
 
136
+ 默认 CLI 文件解码/编码路径使用 `sharp`。只使用浏览器或 `ImageData` SDK 的消费者不需要它。如果你在自己的项目里使用 CLI 文件处理,请同时安装 `sharp`:
137
+
138
+ ```bash
139
+ pnpm add sharp
140
+ ```
141
+
136
142
  ### 去不掉的水印?
137
143
 
138
144
  本工具专门针对 Gemini 可见水印(右下角半透明 Logo)。如果你的图片水印不属于 Gemini 已知格式,或需要去除其他类型的图片水印,可以试试通用 AI 去水印工具:
@@ -257,7 +263,8 @@ const result = await removeWatermarkFromBuffer(inputBuffer, {
257
263
 
258
264
  ### CLI 与 Skill
259
265
 
260
- - 能运行本包及其依赖的本地 Node.js 环境
266
+ - 能运行本包的本地 Node.js 环境
267
+ - 使用默认 CLI 文件解码/编码路径时需要安装 `sharp`
261
268
  - 可读写本地输入/输出路径的文件系统环境
262
269
  - 在仓库内可直接使用:
263
270
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pilio/gemini-watermark-remover",
3
3
  "description": "Automatically removes watermarks from Gemini AI generated images",
4
- "version": "1.0.19",
4
+ "version": "1.0.21",
5
5
  "author": "GargantuaX",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -45,6 +45,10 @@
45
45
  "types": "./src/sdk/node.d.ts",
46
46
  "import": "./src/sdk/node.js"
47
47
  },
48
+ "./video": {
49
+ "types": "./src/sdk/video.d.ts",
50
+ "import": "./src/sdk/video.js"
51
+ },
48
52
  "./runtime-browser": {
49
53
  "types": "./src/runtime/browser.d.ts",
50
54
  "import": "./src/runtime/browser.js"
@@ -54,15 +58,25 @@
54
58
  "import": "./src/runtime/userscript.js"
55
59
  }
56
60
  },
57
- "dependencies": {
61
+ "peerDependencies": {
58
62
  "sharp": "^0.34.5"
59
63
  },
64
+ "peerDependenciesMeta": {
65
+ "sharp": {
66
+ "optional": true
67
+ }
68
+ },
60
69
  "devDependencies": {
61
70
  "@types/node": "^24.6.0",
62
71
  "esbuild": "^0.24.0",
72
+ "onnxruntime-web": "^1.26.0",
63
73
  "playwright": "^1.58.2",
74
+ "sharp": "^0.34.5",
64
75
  "typescript": "^5.9.3"
65
76
  },
77
+ "dependencies": {
78
+ "mediabunny": "^1.46.0"
79
+ },
66
80
  "scripts": {
67
81
  "dev": "node build.js",
68
82
  "build": "node build.js --prod",
@@ -78,12 +92,69 @@
78
92
  "probe:real-page:compare": "node scripts/real-page-pixel-compare.js",
79
93
  "benchmark:samples": "node scripts/sample-benchmark.js",
80
94
  "benchmark:userscript": "node scripts/userscript-benchmark.js",
95
+ "compare:allenk-v2": "node scripts/create-allenk-v2-comparison-report.js",
96
+ "release:readiness": "node scripts/create-release-readiness-report.js",
97
+ "release:quality-gate": "pnpm compare:allenk-v2 -- --fail-on-incomplete && pnpm release:readiness -- --fail-on-not-ready",
98
+ "release:goal-audit": "node scripts/create-release-goal-audit-report.js",
99
+ "release:preflight": "pnpm test && pnpm build && pnpm package:extension && pnpm release:quality-gate && pnpm release:goal-audit -- --fail-on-incomplete",
100
+ "visible-residual:loop": "node scripts/run-visible-residual-loop.js",
101
+ "visible-residual:verify": "node scripts/verify-visible-residual-loop.js",
102
+ "visible-residual:validate-human-review": "node scripts/validate-visible-residual-human-review.js",
103
+ "visible-residual:create-gold-manifest": "node scripts/create-visible-residual-gold-manifest.js",
104
+ "visible-residual:review-status": "node scripts/report-visible-residual-review-progress.js",
105
+ "visible-residual:apply-focused-batch": "node scripts/apply-visible-residual-focused-review-batch.js",
106
+ "visible-residual:review-worksheet": "node scripts/create-visible-residual-review-worksheet.js",
107
+ "visible-residual:admission-report": "node scripts/create-visible-residual-admission-report.js",
108
+ "visible-residual:goal-audit": "node scripts/create-visible-residual-goal-audit-report.js",
109
+ "visible-residual:cluster-report": "node scripts/create-visible-residual-cluster-report.js",
110
+ "visible-residual:geometry-audit": "node scripts/audit-visible-residual-geometry.js",
81
111
  "scan:sample-watermarks": "node scripts/scan-sample-watermarks.js",
82
112
  "render:skipped-crops": "node scripts/render-skipped-candidate-crops.js",
113
+ "render:video-crops": "node scripts/render-video-crop-sheet.js",
114
+ "analyze:video-residual": "node scripts/analyze-video-residual.js",
115
+ "benchmark:video-crops": "node scripts/video-crop-benchmark.js",
116
+ "report:video-crops": "node scripts/report-video-crop-benchmark.js",
117
+ "diagnose:video-lowbody": "node scripts/diagnose-video-lowbody-regression.js",
118
+ "lab:video-frames": "node scripts/run-video-frame-backend-lab.js",
119
+ "lab:allenk-fdncnn-onnx-frames": "node scripts/run-allenk-fdncnn-onnx-frame-lab.js",
120
+ "lab:video-alpha-profile": "node scripts/run-video-alpha-profile-lab.js",
121
+ "lab:video-temporal-residual": "node scripts/run-video-temporal-residual-lab.js",
122
+ "fit:video-alpha-shape": "node scripts/fit-video-alpha-shape.js",
123
+ "gate:video-alpha-shape": "node scripts/gate-video-alpha-shape-candidates.js",
124
+ "gate:video-denoise": "node scripts/gate-video-denoise-candidates.js",
125
+ "gate:video-delivery": "node scripts/run-video-delivery-gate.js",
126
+ "report:video-alpha-policy-evidence": "node scripts/create-video-alpha-policy-evidence-report.js",
127
+ "report:video-frame-lab": "node scripts/report-video-frame-lab-sweep.js",
128
+ "report:video-review-pack": "node scripts/create-video-review-pack.js",
129
+ "report:video-review-index": "node scripts/create-video-review-index.js",
130
+ "report:video-review-screenshot": "node scripts/create-video-review-screenshot.js",
131
+ "report:video-review-thumbnail-sheet": "node scripts/create-video-review-thumbnail-sheet.js",
132
+ "report:video-dashboard-screenshot": "node scripts/create-video-review-screenshot.js --html .artifacts/video-delivery-dashboard/latest-video-dashboard.html --output .artifacts/video-delivery-dashboard/latest-video-dashboard.png --report .artifacts/video-delivery-dashboard/latest-video-dashboard-screenshot.json",
133
+ "report:video-review-decision": "node scripts/create-video-review-decision-report.js",
134
+ "report:video-acceptance-quickstart": "node scripts/create-video-acceptance-quickstart.js",
135
+ "report:video-pending-review-decision": "node scripts/create-video-pending-review-decision.js",
136
+ "report:video-alpha-policy-review-pack": "node scripts/create-video-alpha-policy-review-pack.js",
137
+ "report:video-light-polish-review-pack": "node scripts/create-video-light-polish-review-pack.js",
138
+ "report:video-polish-sweep-review-pack": "node scripts/create-video-polish-sweep-review-pack.js",
139
+ "report:video-delivery-dashboard": "node scripts/create-video-delivery-dashboard.js",
140
+ "report:video-goal-status": "node scripts/create-video-goal-status-report.js",
141
+ "report:video-delivery-bundle": "node scripts/create-video-delivery-bundle.js",
142
+ "verify:video-delivery-bundle": "node scripts/verify-video-delivery-bundle.js",
143
+ "export:video-backend": "node scripts/export-video-backend-variant.js",
144
+ "export:allenk-fdncnn-onnx-video": "node scripts/export-allenk-fdncnn-onnx-video.js",
145
+ "export:allenk-fdncnn-onnx-frame-video": "node scripts/export-allenk-fdncnn-onnx-frame-video.js",
146
+ "report:allenk-fdncnn-onnx-video-evidence": "node scripts/create-allenk-fdncnn-onnx-video-evidence.js",
147
+ "export:video-ui-preset": "node scripts/export-video-ui-preset.js",
148
+ "render:video-comparison-grid": "node scripts/render-video-comparison-grid.js",
83
149
  "package:extension": "node scripts/package-extension-release.js",
84
150
  "cli:smoke": "node bin/gwr.mjs --help",
85
151
  "serve": "pnpm dlx serve dist",
86
152
  "export:samples": "node scripts/export-fixed-samples.js",
87
- "calibrate:preview-alpha": "node scripts/calibrate-preview-alpha.js"
153
+ "calibrate:preview-alpha": "node scripts/calibrate-preview-alpha.js",
154
+ "extract:allenk-fdncnn": "node scripts/extract-allenk-fdncnn-model.js",
155
+ "export:allenk-fdncnn-onnx": "node scripts/export-allenk-fdncnn-onnx.js",
156
+ "smoke:allenk-fdncnn-onnx-runtime": "node scripts/smoke-allenk-fdncnn-onnx-runtime.js",
157
+ "report:allenk-fdncnn-browser-spike": "node scripts/create-allenk-fdncnn-browser-spike-report.js",
158
+ "report:allenk-fdncnn-runtime-seam": "node scripts/create-allenk-fdncnn-runtime-seam-report.js"
88
159
  }
89
160
  }
package/src/cli/gwrCli.js CHANGED
@@ -2,7 +2,7 @@ import { runRemoveCommand } from './gwrRemoveCommand.js';
2
2
 
3
3
  export async function main(argv, io) {
4
4
  if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
5
- io.stdout.write('Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json]\n');
5
+ io.stdout.write('Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json] [--video-page <url-or-file>]\n');
6
6
  return 0;
7
7
  }
8
8
 
@@ -1,10 +1,16 @@
1
1
  import path from 'node:path';
2
2
  import { mkdir, readdir, stat } from 'node:fs/promises';
3
3
 
4
- import { inferMimeTypeFromPath, removeWatermarkFromFile } from '../sdk/node.js';
4
+ import {
5
+ inferMimeTypeFromPath,
6
+ inferVideoMimeTypeFromPath,
7
+ isVideoMimeType,
8
+ removeVideoWatermarkFromFile,
9
+ removeWatermarkFromFile
10
+ } from '../sdk/node.js';
5
11
 
6
12
  const REMOVE_USAGE =
7
- 'Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json]';
13
+ 'Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json] [--video-page <url-or-file>]';
8
14
 
9
15
  export async function runRemoveCommand(argv, io) {
10
16
  if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
@@ -18,15 +24,7 @@ export async function runRemoveCommand(argv, io) {
18
24
  return 2;
19
25
  }
20
26
 
21
- const codec = await resolveCodecOptions(options).catch((error) => {
22
- io.stderr.write(`${error.message}\n`);
23
- return null;
24
- });
25
- if (!codec) {
26
- return 2;
27
- }
28
-
29
- const commandOptions = { ...options, codec };
27
+ const commandOptions = { ...options, imageCodecPromise: null };
30
28
  const inputStats = await stat(commandOptions.input).catch(() => null);
31
29
  if (!inputStats) {
32
30
  io.stderr.write(`Input not found: ${commandOptions.input}\n`);
@@ -64,7 +62,11 @@ function parseRemoveArgs(argv) {
64
62
  overwrite: false,
65
63
  json: false,
66
64
  decoder: null,
67
- encoder: null
65
+ encoder: null,
66
+ videoPage: null,
67
+ videoDenoiseBackend: null,
68
+ videoTimeoutMs: null,
69
+ allowLowConfidence: false
68
70
  };
69
71
 
70
72
  let parseOptions = true;
@@ -116,6 +118,30 @@ function parseRemoveArgs(argv) {
116
118
  continue;
117
119
  }
118
120
 
121
+ if (token === '--video-page') {
122
+ const parsed = parseOptionValue(argv, index, '--video-page');
123
+ if (!parsed.ok) return parsed;
124
+ options.videoPage = parsed.value;
125
+ index = parsed.index;
126
+ continue;
127
+ }
128
+
129
+ if (token === '--video-denoise-backend') {
130
+ const parsed = parseOptionValue(argv, index, '--video-denoise-backend');
131
+ if (!parsed.ok) return parsed;
132
+ options.videoDenoiseBackend = parsed.value;
133
+ index = parsed.index;
134
+ continue;
135
+ }
136
+
137
+ if (token === '--video-timeout-ms') {
138
+ const parsed = parseOptionValue(argv, index, '--video-timeout-ms');
139
+ if (!parsed.ok) return parsed;
140
+ options.videoTimeoutMs = Number(parsed.value);
141
+ index = parsed.index;
142
+ continue;
143
+ }
144
+
119
145
  if (token === '--overwrite') {
120
146
  options.overwrite = true;
121
147
  continue;
@@ -126,6 +152,11 @@ function parseRemoveArgs(argv) {
126
152
  continue;
127
153
  }
128
154
 
155
+ if (token === '--allow-low-confidence') {
156
+ options.allowLowConfidence = true;
157
+ continue;
158
+ }
159
+
129
160
  return { ok: false, error: `Unknown option: ${token}` };
130
161
  }
131
162
 
@@ -145,6 +176,10 @@ function parseRemoveArgs(argv) {
145
176
  return { ok: false, error: '--decoder and --encoder must be used together.' };
146
177
  }
147
178
 
179
+ if (options.videoTimeoutMs !== null && (!Number.isFinite(options.videoTimeoutMs) || options.videoTimeoutMs <= 0)) {
180
+ return { ok: false, error: '--video-timeout-ms must be a positive number.' };
181
+ }
182
+
148
183
  return options;
149
184
  }
150
185
 
@@ -301,17 +336,50 @@ async function processOneFile(inputPath, outputPath, options) {
301
336
  }
302
337
 
303
338
  await mkdir(path.dirname(outputPath), { recursive: true });
339
+ if (isVideoPath(inputPath) || isVideoPath(outputPath)) {
340
+ const result = await removeVideoWatermarkFromFile(inputPath, {
341
+ outputPath,
342
+ mimeType: inferVideoMimeTypeFromPath(outputPath || inputPath),
343
+ pagePath: options.videoPage || undefined,
344
+ denoiseBackend: options.videoDenoiseBackend || undefined,
345
+ timeoutMs: Number.isFinite(options.videoTimeoutMs) && options.videoTimeoutMs > 0
346
+ ? options.videoTimeoutMs
347
+ : undefined,
348
+ allowLowConfidence: options.allowLowConfidence
349
+ });
350
+
351
+ return {
352
+ input: inputPath,
353
+ output: outputPath,
354
+ kind: 'video',
355
+ meta: result.meta
356
+ };
357
+ }
358
+
359
+ const codec = await getImageCodec(options);
304
360
  const mimeType = inferMimeTypeFromPath(outputPath);
305
361
  const result = await removeWatermarkFromFile(inputPath, {
306
362
  outputPath,
307
363
  mimeType,
308
- decodeImageData: options.codec.decodeImageData,
309
- encodeImageData: options.codec.encodeImageData
364
+ decodeImageData: codec.decodeImageData,
365
+ encodeImageData: codec.encodeImageData
310
366
  });
311
367
 
312
368
  return {
313
369
  input: inputPath,
314
370
  output: outputPath,
371
+ kind: 'image',
315
372
  meta: result.meta
316
373
  };
317
374
  }
375
+
376
+ function isVideoPath(filePath) {
377
+ return isVideoMimeType(inferVideoMimeTypeFromPath(filePath));
378
+ }
379
+
380
+ async function getImageCodec(options) {
381
+ if (!options.imageCodecPromise) {
382
+ options.imageCodecPromise = resolveCodecOptions(options);
383
+ }
384
+ return options.imageCodecPromise;
385
+ }
@@ -7,6 +7,8 @@ import { resolveGeminiWatermarkSearchConfigs } from './geminiSizeCatalog.js';
7
7
 
8
8
  const DEFAULT_THRESHOLD = 0.35;
9
9
  const EPSILON = 1e-8;
10
+ const REFERENCE_WATERMARK_SIZE = 96;
11
+ const MIN_COARSE_ADJUSTED_SCORE = 0.08;
10
12
 
11
13
  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
12
14
 
@@ -172,6 +174,23 @@ function createScaleList(minSize, maxSize) {
172
174
  return [...set].sort((a, b) => a - b);
173
175
  }
174
176
 
177
+ export function computeSizeAdjustedConfidence(confidence, size, referenceSize = REFERENCE_WATERMARK_SIZE) {
178
+ if (
179
+ !Number.isFinite(confidence) ||
180
+ !Number.isFinite(size) ||
181
+ !Number.isFinite(referenceSize) ||
182
+ size <= 0 ||
183
+ referenceSize <= 0
184
+ ) {
185
+ return 0;
186
+ }
187
+
188
+ // NCC tends to favor tiny templates. A cube-root penalty keeps that bias in
189
+ // check without crushing legitimate preview-tier watermarks.
190
+ const sizeWeight = Math.min(1, Math.cbrt(size / referenceSize));
191
+ return confidence * sizeWeight;
192
+ }
193
+
175
194
  function buildSeedConfigs(width, height, defaultConfig) {
176
195
  // Start adaptive search from both the coarse default anchor and any
177
196
  // catalog-projected anchors for official or near-official Gemini sizes.
@@ -407,7 +426,7 @@ export function detectAdaptiveWatermarkRegion({
407
426
  size: seedCandidate.size,
408
427
  x: seedCandidate.x,
409
428
  y: seedCandidate.y,
410
- adjustedScore: seedCandidate.confidence * Math.min(1, Math.sqrt(seedCandidate.size / 96))
429
+ adjustedScore: computeSizeAdjustedConfidence(seedCandidate.confidence, seedCandidate.size)
411
430
  });
412
431
  }
413
432
 
@@ -424,8 +443,8 @@ export function detectAdaptiveWatermarkRegion({
424
443
  if (!score) continue;
425
444
 
426
445
  // Prefer sizes close to known watermark scales to avoid tiny-template bias.
427
- const adjustedScore = score.confidence * Math.min(1, Math.sqrt(size / 96));
428
- if (adjustedScore < 0.08) continue;
446
+ const adjustedScore = computeSizeAdjustedConfidence(score.confidence, size);
447
+ if (adjustedScore < MIN_COARSE_ADJUSTED_SCORE) continue;
429
448
 
430
449
  pushTopK({
431
450
  size,