@pilio/gemini-watermark-remover 1.0.22 → 1.0.24

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/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.22",
4
+ "version": "1.0.24",
5
5
  "author": "GargantuaX",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -112,6 +112,9 @@
112
112
  "render:skipped-crops": "node scripts/render-skipped-candidate-crops.js",
113
113
  "render:video-crops": "node scripts/render-video-crop-sheet.js",
114
114
  "analyze:video-residual": "node scripts/analyze-video-residual.js",
115
+ "score:video-candidates": "node scripts/score-video-watermark-candidates.js",
116
+ "sweep:veo-text-cleanup": "node scripts/sweep-veo-text-cleanup.js",
117
+ "score:video-output-residual": "node scripts/score-video-output-residual.js",
115
118
  "benchmark:video-crops": "node scripts/video-crop-benchmark.js",
116
119
  "report:video-crops": "node scripts/report-video-crop-benchmark.js",
117
120
  "diagnose:video-lowbody": "node scripts/diagnose-video-lowbody-regression.js",
@@ -144,7 +147,12 @@
144
147
  "export:allenk-fdncnn-onnx-video": "node scripts/export-allenk-fdncnn-onnx-video.js",
145
148
  "export:allenk-fdncnn-onnx-frame-video": "node scripts/export-allenk-fdncnn-onnx-frame-video.js",
146
149
  "report:allenk-fdncnn-onnx-video-evidence": "node scripts/create-allenk-fdncnn-onnx-video-evidence.js",
150
+ "report:allenk-catalog-audit": "node scripts/create-allenk-catalog-audit.js",
151
+ "report:allenk-video-binary-analysis": "node scripts/create-allenk-video-binary-analysis.js",
147
152
  "export:video-ui-preset": "node scripts/export-video-ui-preset.js",
153
+ "verify:video-ui-preset-output": "node scripts/verify-video-ui-preset-output.js",
154
+ "verify:video-ui-preset-batch": "node scripts/verify-video-ui-preset-batch.js",
155
+ "verify:video-regression-samples": "pnpm verify:video-ui-preset-batch -- --manifest tests/fixtures/video-regression-samples/gemini-video-regression-samples.json --output-dir .artifacts/video-regression-samples --no-screenshots --fail-on-residual",
148
156
  "render:video-comparison-grid": "node scripts/render-video-comparison-grid.js",
149
157
  "package:extension": "node scripts/package-extension-release.js",
150
158
  "cli:smoke": "node bin/gwr.mjs --help",
@@ -1,5 +1,3 @@
1
- import { createAlphaGradientMask } from './alphaGradientMask.js';
2
-
3
1
  const ALLENK_FDNCNN_MODEL = Object.freeze({
4
2
  name: 'FDnCNN Color FP16',
5
3
  upstream: 'allenk/GeminiWatermarkTool',
@@ -20,6 +18,15 @@ function clamp(value, min, max) {
20
18
  return Math.max(min, Math.min(max, value));
21
19
  }
22
20
 
21
+ function reflect101(index, length) {
22
+ if (length <= 1) return 0;
23
+ let value = Math.round(index);
24
+ while (value < 0 || value >= length) {
25
+ value = value < 0 ? -value : (length * 2 - value - 2);
26
+ }
27
+ return value;
28
+ }
29
+
23
30
  function createGaussianKernel(sigma, radius = Math.ceil(sigma * 3)) {
24
31
  const safeSigma = Math.max(0.01, sigma);
25
32
  const safeRadius = Math.max(1, Math.round(radius));
@@ -52,7 +59,7 @@ function gaussianBlurFloatMap(source, width, height, sigma, radius = Math.ceil(s
52
59
  for (let x = 0; x < width; x++) {
53
60
  let sum = 0;
54
61
  for (let dx = -r; dx <= r; dx++) {
55
- const xx = clamp(x + dx, 0, width - 1);
62
+ const xx = reflect101(x + dx, width);
56
63
  sum += source[y * width + xx] * kernel[dx + r];
57
64
  }
58
65
  temp[y * width + x] = sum;
@@ -63,7 +70,7 @@ function gaussianBlurFloatMap(source, width, height, sigma, radius = Math.ceil(s
63
70
  for (let x = 0; x < width; x++) {
64
71
  let sum = 0;
65
72
  for (let dy = -r; dy <= r; dy++) {
66
- const yy = clamp(y + dy, 0, height - 1);
73
+ const yy = reflect101(y + dy, height);
67
74
  sum += temp[yy * width + x] * kernel[dy + r];
68
75
  }
69
76
  output[y * width + x] = sum;
@@ -123,6 +130,139 @@ function resizeSquareAlphaMapArea(sourceAlpha, sourceSize, targetWidth, targetHe
123
130
  return output;
124
131
  }
125
132
 
133
+ function resizeSquareAlphaMapLinear(sourceAlpha, sourceSize, targetWidth, targetHeight = targetWidth) {
134
+ if (!sourceAlpha || sourceSize <= 0 || targetWidth <= 0 || targetHeight <= 0) {
135
+ return new Float32Array(0);
136
+ }
137
+ if (sourceSize === targetWidth && sourceSize === targetHeight) {
138
+ return new Float32Array(sourceAlpha);
139
+ }
140
+
141
+ const output = new Float32Array(targetWidth * targetHeight);
142
+ const scaleX = sourceSize / targetWidth;
143
+ const scaleY = sourceSize / targetHeight;
144
+
145
+ for (let y = 0; y < targetHeight; y++) {
146
+ const sourceY = (y + 0.5) * scaleY - 0.5;
147
+ const y0 = Math.floor(sourceY);
148
+ const y1 = y0 + 1;
149
+ const wy = sourceY - y0;
150
+
151
+ for (let x = 0; x < targetWidth; x++) {
152
+ const sourceX = (x + 0.5) * scaleX - 0.5;
153
+ const x0 = Math.floor(sourceX);
154
+ const x1 = x0 + 1;
155
+ const wx = sourceX - x0;
156
+
157
+ const topLeft = sourceAlpha[clamp(y0, 0, sourceSize - 1) * sourceSize + clamp(x0, 0, sourceSize - 1)] || 0;
158
+ const topRight = sourceAlpha[clamp(y0, 0, sourceSize - 1) * sourceSize + clamp(x1, 0, sourceSize - 1)] || 0;
159
+ const bottomLeft = sourceAlpha[clamp(y1, 0, sourceSize - 1) * sourceSize + clamp(x0, 0, sourceSize - 1)] || 0;
160
+ const bottomRight = sourceAlpha[clamp(y1, 0, sourceSize - 1) * sourceSize + clamp(x1, 0, sourceSize - 1)] || 0;
161
+ const top = topLeft * (1 - wx) + topRight * wx;
162
+ const bottom = bottomLeft * (1 - wx) + bottomRight * wx;
163
+
164
+ output[y * targetWidth + x] = top * (1 - wy) + bottom * wy;
165
+ }
166
+ }
167
+
168
+ return output;
169
+ }
170
+
171
+ function resizeSquareAlphaMapOpenCv(sourceAlpha, sourceSize, targetWidth, targetHeight = targetWidth) {
172
+ return targetWidth > sourceSize
173
+ ? resizeSquareAlphaMapLinear(sourceAlpha, sourceSize, targetWidth, targetHeight)
174
+ : resizeSquareAlphaMapArea(sourceAlpha, sourceSize, targetWidth, targetHeight);
175
+ }
176
+
177
+ const ALLENK_ELLIPSE_5X5 = Object.freeze([
178
+ [0, 0, 1, 0, 0],
179
+ [1, 1, 1, 1, 1],
180
+ [1, 1, 1, 1, 1],
181
+ [1, 1, 1, 1, 1],
182
+ [0, 0, 1, 0, 0]
183
+ ]);
184
+
185
+ function dilateAllenkEllipse5x5(values, width, height) {
186
+ const output = new Float32Array(values.length);
187
+
188
+ for (let y = 0; y < height; y++) {
189
+ for (let x = 0; x < width; x++) {
190
+ let maxValue = 0;
191
+ for (let ky = 0; ky < 5; ky++) {
192
+ for (let kx = 0; kx < 5; kx++) {
193
+ if (!ALLENK_ELLIPSE_5X5[ky][kx]) continue;
194
+ const sx = x + kx - 2;
195
+ const sy = y + ky - 2;
196
+ if (sx < 0 || sy < 0 || sx >= width || sy >= height) continue;
197
+ maxValue = Math.max(maxValue, values[sy * width + sx] || 0);
198
+ }
199
+ }
200
+ output[y * width + x] = maxValue;
201
+ }
202
+ }
203
+
204
+ return output;
205
+ }
206
+
207
+ function createAllenkOpenCvGradientMask({ alphaMap, width, height, strength }) {
208
+ if (!alphaMap || width <= 0 || height <= 0 || alphaMap.length < width * height) {
209
+ return new Float32Array(0);
210
+ }
211
+
212
+ const gradient = new Float32Array(width * height);
213
+ let minGradient = Number.POSITIVE_INFINITY;
214
+ let maxGradient = Number.NEGATIVE_INFINITY;
215
+
216
+ for (let y = 0; y < height; y++) {
217
+ for (let x = 0; x < width; x++) {
218
+ const top = reflect101(y - 1, height);
219
+ const bottom = reflect101(y + 1, height);
220
+ const left = reflect101(x - 1, width);
221
+ const right = reflect101(x + 1, width);
222
+ const gx = (
223
+ -alphaMap[top * width + left] -
224
+ 2 * alphaMap[y * width + left] -
225
+ alphaMap[bottom * width + left] +
226
+ alphaMap[top * width + right] +
227
+ 2 * alphaMap[y * width + right] +
228
+ alphaMap[bottom * width + right]
229
+ );
230
+ const gy = (
231
+ -alphaMap[top * width + left] -
232
+ 2 * alphaMap[top * width + x] -
233
+ alphaMap[top * width + right] +
234
+ alphaMap[bottom * width + left] +
235
+ 2 * alphaMap[bottom * width + x] +
236
+ alphaMap[bottom * width + right]
237
+ );
238
+ const value = Math.sqrt(gx * gx + gy * gy);
239
+ gradient[y * width + x] = value;
240
+ minGradient = Math.min(minGradient, value);
241
+ maxGradient = Math.max(maxGradient, value);
242
+ }
243
+ }
244
+
245
+ if (!Number.isFinite(minGradient) || maxGradient <= minGradient) {
246
+ return new Float32Array(width * height);
247
+ }
248
+
249
+ const normalized = new Float32Array(width * height);
250
+ const scale = 1 / (maxGradient - minGradient);
251
+ for (let i = 0; i < normalized.length; i++) {
252
+ normalized[i] = Math.sqrt(clamp((gradient[i] - minGradient) * scale, 0, 1));
253
+ }
254
+
255
+ const expanded = dilateAllenkEllipse5x5(normalized, width, height);
256
+ const blurred = gaussianBlurFloatMap(expanded, width, height, 2);
257
+ const safeStrength = Number.isFinite(strength) ? Math.max(0, strength) : 1;
258
+
259
+ for (let i = 0; i < blurred.length; i++) {
260
+ blurred[i] = clamp(blurred[i] * safeStrength, 0, 1);
261
+ }
262
+
263
+ return blurred;
264
+ }
265
+
126
266
  function normalizeAllenkFdncnnOptions(options = {}) {
127
267
  const sigma = Number.isFinite(options.sigma)
128
268
  ? clamp(options.sigma, 0, ALLENK_FDNCNN_MODEL.maxSigma)
@@ -145,17 +285,14 @@ function createAllenkGradientMask({
145
285
  } = {}) {
146
286
  const sourceSize = inferSquareSize(alphaMap);
147
287
  const resizedAlphaMap = sourceSize > 0
148
- ? resizeSquareAlphaMapArea(alphaMap, sourceSize, width, height)
288
+ ? resizeSquareAlphaMapOpenCv(alphaMap, sourceSize, width, height)
149
289
  : alphaMap;
150
290
 
151
- return createAlphaGradientMask({
291
+ return createAllenkOpenCvGradientMask({
152
292
  alphaMap: resizedAlphaMap,
153
293
  width,
154
294
  height,
155
- strength,
156
- gamma: 0.5,
157
- dilateRadius: 2,
158
- blurSigma: 2
295
+ strength
159
296
  });
160
297
  }
161
298
 
@@ -188,6 +325,154 @@ function calculateAllenkPaddedRoi({ imageWidth, imageHeight, region, padding = A
188
325
  };
189
326
  }
190
327
 
328
+ function calculateAllenkVirtualPaddedRoi({
329
+ imageWidth,
330
+ imageHeight,
331
+ region,
332
+ padding = ALLENK_FDNCNN_MODEL.defaultPadding,
333
+ targetWidth = null,
334
+ targetHeight = null
335
+ } = {}) {
336
+ if (!region || imageWidth <= 0 || imageHeight <= 0 || region.width <= 0 || region.height <= 0) {
337
+ return null;
338
+ }
339
+
340
+ const safePadding = Math.max(0, Math.round(padding));
341
+ const x = Math.round(region.x - safePadding);
342
+ const y = Math.round(region.y - safePadding);
343
+ const defaultWidth = Math.round(region.width + safePadding * 2);
344
+ const defaultHeight = Math.round(region.height + safePadding * 2);
345
+ const width = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : defaultWidth;
346
+ const height = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : defaultHeight;
347
+
348
+ if (width < 4 || height < 4) return null;
349
+
350
+ const visibleX = clamp(x, 0, imageWidth);
351
+ const visibleY = clamp(y, 0, imageHeight);
352
+ const visibleRight = clamp(x + width, 0, imageWidth);
353
+ const visibleBottom = clamp(y + height, 0, imageHeight);
354
+ const visibleWidth = visibleRight - visibleX;
355
+ const visibleHeight = visibleBottom - visibleY;
356
+ if (visibleWidth <= 0 || visibleHeight <= 0) return null;
357
+
358
+ return {
359
+ x,
360
+ y,
361
+ width,
362
+ height,
363
+ inner: {
364
+ x: clamp(Math.round(region.x - x), 0, width),
365
+ y: clamp(Math.round(region.y - y), 0, height),
366
+ width: clamp(Math.round(region.width), 0, width),
367
+ height: clamp(Math.round(region.height), 0, height)
368
+ },
369
+ visible: {
370
+ x: visibleX,
371
+ y: visibleY,
372
+ width: visibleWidth,
373
+ height: visibleHeight,
374
+ offsetX: visibleX - x,
375
+ offsetY: visibleY - y
376
+ }
377
+ };
378
+ }
379
+
380
+ function calculateAllenkRuntimeRoi({
381
+ imageWidth,
382
+ imageHeight,
383
+ region,
384
+ padding = ALLENK_FDNCNN_MODEL.defaultPadding,
385
+ targetWidth = null,
386
+ targetHeight = null
387
+ } = {}) {
388
+ const padded = calculateAllenkPaddedRoi({ imageWidth, imageHeight, region, padding });
389
+ if (!padded) return null;
390
+
391
+ const safeTargetWidth = Number.isInteger(targetWidth) && targetWidth > 0 ? targetWidth : null;
392
+ const safeTargetHeight = Number.isInteger(targetHeight) && targetHeight > 0 ? targetHeight : null;
393
+ if (
394
+ !safeTargetWidth ||
395
+ !safeTargetHeight ||
396
+ imageWidth < safeTargetWidth ||
397
+ imageHeight < safeTargetHeight ||
398
+ padded.width > safeTargetWidth ||
399
+ padded.height > safeTargetHeight
400
+ ) {
401
+ return padded;
402
+ }
403
+
404
+ const centerX = region.x + region.width / 2;
405
+ const centerY = region.y + region.height / 2;
406
+ const x = clamp(Math.round(centerX - safeTargetWidth / 2), 0, imageWidth - safeTargetWidth);
407
+ const y = clamp(Math.round(centerY - safeTargetHeight / 2), 0, imageHeight - safeTargetHeight);
408
+
409
+ return {
410
+ x,
411
+ y,
412
+ width: safeTargetWidth,
413
+ height: safeTargetHeight,
414
+ inner: {
415
+ x: clamp(Math.round(region.x - x), 0, safeTargetWidth),
416
+ y: clamp(Math.round(region.y - y), 0, safeTargetHeight),
417
+ width: clamp(Math.round(region.width), 0, safeTargetWidth),
418
+ height: clamp(Math.round(region.height), 0, safeTargetHeight)
419
+ }
420
+ };
421
+ }
422
+
423
+ function extractAllenkVirtualImageData({
424
+ imageData,
425
+ roi,
426
+ imageX = 0,
427
+ imageY = 0,
428
+ canvasWidth = imageData?.width || 0,
429
+ canvasHeight = imageData?.height || 0
430
+ } = {}) {
431
+ if (
432
+ !imageData?.data ||
433
+ !roi ||
434
+ imageData.width <= 0 ||
435
+ imageData.height <= 0 ||
436
+ roi.width <= 0 ||
437
+ roi.height <= 0 ||
438
+ canvasWidth <= 0 ||
439
+ canvasHeight <= 0
440
+ ) {
441
+ return { width: 0, height: 0, data: new Uint8ClampedArray(0) };
442
+ }
443
+
444
+ const width = Math.max(1, Math.round(roi.width));
445
+ const height = Math.max(1, Math.round(roi.height));
446
+ const output = new Uint8ClampedArray(width * height * 4);
447
+ const sourceStride = imageData.data.length >= imageData.width * imageData.height * 4 ? 4 : 3;
448
+
449
+ for (let y = 0; y < height; y++) {
450
+ const globalY = clamp(Math.round(roi.y + y), 0, canvasHeight - 1);
451
+ const localY = clamp(globalY - Math.round(imageY), 0, imageData.height - 1);
452
+ for (let x = 0; x < width; x++) {
453
+ const globalX = clamp(Math.round(roi.x + x), 0, canvasWidth - 1);
454
+ const localX = clamp(globalX - Math.round(imageX), 0, imageData.width - 1);
455
+ const src = (localY * imageData.width + localX) * sourceStride;
456
+ const dst = (y * width + x) * 4;
457
+ output[dst] = imageData.data[src] || 0;
458
+ output[dst + 1] = imageData.data[src + 1] || 0;
459
+ output[dst + 2] = imageData.data[src + 2] || 0;
460
+ output[dst + 3] = sourceStride >= 4 ? (imageData.data[src + 3] ?? 255) : 255;
461
+ }
462
+ }
463
+
464
+ const ImageDataCtor = typeof imageData.constructor === 'function' && imageData.constructor !== Object
465
+ ? imageData.constructor
466
+ : typeof ImageData === 'function'
467
+ ? ImageData
468
+ : null;
469
+ if (ImageDataCtor) {
470
+ return new ImageDataCtor(output, width, height);
471
+ }
472
+
473
+ return { width, height, data: output };
474
+ }
475
+
191
476
  function embedAllenkRoiWeights({ roiWeights, roiWidth, roiHeight, paddedRoi, blurSigma = 1 } = {}) {
192
477
  if (!roiWeights || !paddedRoi?.inner || roiWidth <= 0 || roiHeight <= 0) {
193
478
  return new Float32Array(0);
@@ -319,8 +604,11 @@ export {
319
604
  blendAllenkDenoisedRoi,
320
605
  buildAllenkFdncnnInput,
321
606
  calculateAllenkPaddedRoi,
607
+ calculateAllenkRuntimeRoi,
608
+ calculateAllenkVirtualPaddedRoi,
322
609
  convertAllenkFdncnnOutputToRgba,
323
610
  createAllenkGradientMask,
324
611
  embedAllenkRoiWeights,
612
+ extractAllenkVirtualImageData,
325
613
  normalizeAllenkFdncnnOptions
326
614
  };
@@ -231,12 +231,13 @@ function createGraphProto({
231
231
  segments,
232
232
  inputName,
233
233
  outputName,
234
- roiSize
234
+ roiWidth,
235
+ roiHeight
235
236
  }) {
236
237
  const initializers = createInitializers({ bin, segments });
237
238
  const nodes = createNodes({ segments, inputName, outputName });
238
- const inputShape = [1, segments[0].inputChannels, roiSize, roiSize];
239
- const outputShape = [1, segments[segments.length - 1].outputChannels, roiSize, roiSize];
239
+ const inputShape = [1, segments[0].inputChannels, roiHeight, roiWidth];
240
+ const outputShape = [1, segments[segments.length - 1].outputChannels, roiHeight, roiWidth];
240
241
 
241
242
  return {
242
243
  nodeCount: nodes.length,
@@ -275,6 +276,8 @@ function exportAllenkFdncnnOnnx({
275
276
  bin,
276
277
  weightLayout,
277
278
  roiSize = 72,
279
+ roiWidth = roiSize,
280
+ roiHeight = roiSize,
278
281
  inputName = 'fdncnn_input',
279
282
  outputName = 'fdncnn_output',
280
283
  graphName = 'allenk_fdncnn_color',
@@ -285,8 +288,11 @@ function exportAllenkFdncnnOnnx({
285
288
  if (!segments.length) {
286
289
  throw new Error('allenk FDnCNN ONNX export requires weightLayout.segments');
287
290
  }
288
- if (!Number.isInteger(roiSize) || roiSize <= 0) {
289
- throw new Error(`Invalid ONNX ROI size: ${roiSize}`);
291
+ if (!Number.isInteger(roiWidth) || roiWidth <= 0) {
292
+ throw new Error(`Invalid ONNX ROI width: ${roiWidth}`);
293
+ }
294
+ if (!Number.isInteger(roiHeight) || roiHeight <= 0) {
295
+ throw new Error(`Invalid ONNX ROI height: ${roiHeight}`);
290
296
  }
291
297
 
292
298
  const graph = createGraphProto({
@@ -295,7 +301,8 @@ function exportAllenkFdncnnOnnx({
295
301
  segments,
296
302
  inputName,
297
303
  outputName,
298
- roiSize
304
+ roiWidth,
305
+ roiHeight
299
306
  });
300
307
  const model = createModelProto({
301
308
  graph: graph.bytes,
@@ -313,13 +320,15 @@ function exportAllenkFdncnnOnnx({
313
320
  graphName,
314
321
  inputName,
315
322
  outputName,
316
- inputShape: [1, segments[0].inputChannels, roiSize, roiSize],
317
- outputShape: [1, segments[segments.length - 1].outputChannels, roiSize, roiSize],
323
+ inputShape: [1, segments[0].inputChannels, roiHeight, roiWidth],
324
+ outputShape: [1, segments[segments.length - 1].outputChannels, roiHeight, roiWidth],
318
325
  nodeCount: graph.nodeCount,
319
326
  convolutionNodeCount: segments.length,
320
327
  reluNodeCount: segments.filter((segment, index) => index < segments.length - 1 && segment.activationType === 1).length,
321
328
  initializerCount: graph.initializerCount,
322
- roiSize
329
+ roiSize: roiWidth === roiHeight ? roiWidth : null,
330
+ roiWidth,
331
+ roiHeight
323
332
  }
324
333
  };
325
334
  }