@pilio/gemini-watermark-remover 1.0.37 → 1.0.39

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.37",
4
+ "version": "1.0.39",
5
5
  "author": "GargantuaX",
6
6
  "sideEffects": false,
7
7
  "files": [
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] [--video-page <url-or-file>]\n');
5
+ io.stdout.write('Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json] [--video-page <url-or-file>] [--video-timeout-ms <ms>] [--video-bitrate-mbps <Mbps>]\n');
6
6
  return 0;
7
7
  }
8
8
 
@@ -10,9 +10,9 @@ import {
10
10
  } from '../sdk/node.js';
11
11
 
12
12
  const REMOVE_USAGE =
13
- 'Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json] [--video-page <url-or-file>]';
13
+ 'Usage: gwr remove <input> [--output <file> | --out-dir <dir>] [--overwrite] [--json] [--video-page <url-or-file>] [--video-timeout-ms <ms>] [--video-bitrate-mbps <Mbps>]';
14
14
 
15
- export async function runRemoveCommand(argv, io) {
15
+ export async function runRemoveCommand(argv, io, dependencies = {}) {
16
16
  if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
17
17
  io.stdout.write(`${REMOVE_USAGE}\n`);
18
18
  return 0;
@@ -24,7 +24,12 @@ export async function runRemoveCommand(argv, io) {
24
24
  return 2;
25
25
  }
26
26
 
27
- const commandOptions = { ...options, imageCodecPromise: null };
27
+ const commandOptions = {
28
+ ...options,
29
+ imageCodecPromise: null,
30
+ onVideoProgress: createVideoProgressReporter(io.stderr),
31
+ removeVideoFile: dependencies.removeVideoWatermarkFromFile || removeVideoWatermarkFromFile
32
+ };
28
33
  const inputStats = await stat(commandOptions.input).catch(() => null);
29
34
  if (!inputStats) {
30
35
  io.stderr.write(`Input not found: ${commandOptions.input}\n`);
@@ -66,6 +71,7 @@ function parseRemoveArgs(argv) {
66
71
  videoPage: null,
67
72
  videoDenoiseBackend: null,
68
73
  videoTimeoutMs: null,
74
+ videoBitrateMbps: null,
69
75
  allowLowConfidence: false
70
76
  };
71
77
 
@@ -142,6 +148,14 @@ function parseRemoveArgs(argv) {
142
148
  continue;
143
149
  }
144
150
 
151
+ if (token === '--video-bitrate-mbps') {
152
+ const parsed = parseOptionValue(argv, index, '--video-bitrate-mbps');
153
+ if (!parsed.ok) return parsed;
154
+ options.videoBitrateMbps = Number(parsed.value);
155
+ index = parsed.index;
156
+ continue;
157
+ }
158
+
145
159
  if (token === '--overwrite') {
146
160
  options.overwrite = true;
147
161
  continue;
@@ -180,6 +194,10 @@ function parseRemoveArgs(argv) {
180
194
  return { ok: false, error: '--video-timeout-ms must be a positive number.' };
181
195
  }
182
196
 
197
+ if (options.videoBitrateMbps !== null && (!Number.isFinite(options.videoBitrateMbps) || options.videoBitrateMbps <= 0)) {
198
+ return { ok: false, error: '--video-bitrate-mbps must be a positive number.' };
199
+ }
200
+
183
201
  return options;
184
202
  }
185
203
 
@@ -204,6 +222,30 @@ function parseOptionValue(argv, optionIndex, optionName) {
204
222
  return { ok: true, value: nextToken, index: optionIndex + 1 };
205
223
  }
206
224
 
225
+ function createVideoProgressReporter(stderr) {
226
+ let lastPercent = -1;
227
+ let lastPhase = null;
228
+
229
+ return (progress = {}) => {
230
+ const percent = Number.isFinite(progress.progress)
231
+ ? Math.max(0, Math.min(100, Math.round(progress.progress * 100)))
232
+ : null;
233
+ const phaseChanged = progress.phase && progress.phase !== lastPhase;
234
+ const percentChanged = percent !== null && (percent === 100 || percent >= lastPercent + 5);
235
+ if (!phaseChanged && !percentChanged) return;
236
+
237
+ const frameLabel = Number.isFinite(progress.processedFrames)
238
+ ? ` ${progress.processedFrames}${Number.isFinite(progress.frameEstimate) ? `/${progress.frameEstimate}` : ''} frames`
239
+ : '';
240
+ const inferenceLabel = Number.isFinite(progress.aiDenoiseFrames) || Number.isFinite(progress.aiReuseFrames)
241
+ ? ` (AI ${progress.aiDenoiseFrames || 0}, reused ${progress.aiReuseFrames || 0})`
242
+ : '';
243
+ stderr.write(`[video] ${percent ?? '?'}%${frameLabel}${inferenceLabel}\n`);
244
+ if (percent !== null) lastPercent = percent;
245
+ lastPhase = progress.phase || lastPhase;
246
+ };
247
+ }
248
+
207
249
  async function resolveCodecOptions(options) {
208
250
  if (!options.decoder && !options.encoder) {
209
251
  return createSharpCodec();
@@ -337,7 +379,7 @@ async function processOneFile(inputPath, outputPath, options) {
337
379
 
338
380
  await mkdir(path.dirname(outputPath), { recursive: true });
339
381
  if (isVideoPath(inputPath) || isVideoPath(outputPath)) {
340
- const result = await removeVideoWatermarkFromFile(inputPath, {
382
+ const result = await options.removeVideoFile(inputPath, {
341
383
  outputPath,
342
384
  mimeType: inferVideoMimeTypeFromPath(outputPath || inputPath),
343
385
  pagePath: options.videoPage || undefined,
@@ -345,6 +387,10 @@ async function processOneFile(inputPath, outputPath, options) {
345
387
  timeoutMs: Number.isFinite(options.videoTimeoutMs) && options.videoTimeoutMs > 0
346
388
  ? options.videoTimeoutMs
347
389
  : undefined,
390
+ videoBitrate: Number.isFinite(options.videoBitrateMbps) && options.videoBitrateMbps > 0
391
+ ? options.videoBitrateMbps * 1_000_000
392
+ : undefined,
393
+ onProgress: options.onVideoProgress,
348
394
  allowLowConfidence: options.allowLowConfidence
349
395
  });
350
396
 
@@ -383,3 +429,5 @@ async function getImageCodec(options) {
383
429
  }
384
430
  return options.imageCodecPromise;
385
431
  }
432
+
433
+ export { parseRemoveArgs };
package/src/sdk/node.d.ts CHANGED
@@ -4,6 +4,7 @@ export type {
4
4
  VideoBufferRemovalResult,
5
5
  VideoFileRemovalOptions,
6
6
  VideoFileRemovalResult,
7
+ VideoProcessingProgress,
7
8
  VideoRemovalMeta
8
9
  } from './video.js';
9
10
 
@@ -1,11 +1,24 @@
1
- export interface VideoRemovalMeta {
1
+ export interface VideoRemovalMeta {
2
2
  status?: string;
3
3
  denoiseBackend?: string;
4
4
  actualDenoiseBackend?: string;
5
5
  actualControls?: Record<string, unknown>;
6
6
  pagePath?: string;
7
7
  [key: string]: unknown;
8
- }
8
+ }
9
+
10
+ export interface VideoProcessingProgress {
11
+ tone: string;
12
+ status: string;
13
+ progressText: string;
14
+ progress: number | null;
15
+ phase: string | null;
16
+ processedFrames: number | null;
17
+ frameEstimate: number | null;
18
+ aiDenoiseFrames: number | null;
19
+ aiReuseFrames: number | null;
20
+ elapsedMs: number;
21
+ }
9
22
 
10
23
  export interface VideoFileProcessorContext {
11
24
  outputPath?: string | null;
@@ -19,9 +32,10 @@ export interface VideoFileProcessorContext {
19
32
  residualCleanupStrength?: number;
20
33
  videoBitrate?: number;
21
34
  adaptiveAlpha?: boolean;
22
- alphaGain?: number;
23
- alphaProfile?: string;
24
- }
35
+ alphaGain?: number;
36
+ alphaProfile?: string;
37
+ onProgress?: (progress: VideoProcessingProgress) => void;
38
+ }
25
39
 
26
40
  export interface VideoBufferProcessorContext extends Omit<VideoFileProcessorContext, 'filePath'> {
27
41
  filePath?: string;
@@ -43,9 +57,10 @@ export interface VideoFileRemovalOptions {
43
57
  residualCleanupStrength?: number;
44
58
  videoBitrate?: number;
45
59
  adaptiveAlpha?: boolean;
46
- alphaGain?: number;
47
- alphaProfile?: string;
48
- processVideoFile?: (
60
+ alphaGain?: number;
61
+ alphaProfile?: string;
62
+ onProgress?: (progress: VideoProcessingProgress) => void;
63
+ processVideoFile?: (
49
64
  inputPath: string,
50
65
  context: VideoFileProcessorContext
51
66
  ) => Promise<VideoProcessorResult> | VideoProcessorResult;
package/src/sdk/video.js CHANGED
@@ -2,10 +2,14 @@ import path from 'node:path';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { createServer } from 'node:http';
4
4
  import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const DEFAULT_VIDEO_DENOISE_BACKEND = 'allenk-fdncnn-browser-spike';
8
- const DEFAULT_VIDEO_TIMEOUT_MS = 6 * 60 * 1000;
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ DEFAULT_VIDEO_TIMEOUT_MS,
8
+ configureVideoPageTimeouts,
9
+ waitForVideoProcessing
10
+ } from './videoProgress.js';
11
+
12
+ const DEFAULT_VIDEO_DENOISE_BACKEND = 'allenk-fdncnn-browser-spike';
9
13
 
10
14
  function normalizeBufferLike(value) {
11
15
  if (Buffer.isBuffer(value)) return value;
@@ -200,10 +204,11 @@ async function processVideoWithPreviewPage(inputPath, options = {}) {
200
204
  edgeDenoiseStrength,
201
205
  residualCleanupStrength,
202
206
  videoBitrate,
203
- adaptiveAlpha = false,
204
- alphaGain,
205
- alphaProfile
206
- } = options;
207
+ adaptiveAlpha = false,
208
+ alphaGain,
209
+ alphaProfile,
210
+ onProgress
211
+ } = options;
207
212
 
208
213
  if (!isHttpUrl(pagePath)) {
209
214
  await assertReadableFile(pagePath, 'Video preview page');
@@ -215,8 +220,8 @@ async function processVideoWithPreviewPage(inputPath, options = {}) {
215
220
  const browser = await chromium.launch({ headless: true });
216
221
  try {
217
222
  return await withLocalVideoPreviewPage(pagePath, async (pageUrl) => {
218
- const page = await browser.newPage();
219
- page.setDefaultTimeout(timeoutMs);
223
+ const page = await browser.newPage();
224
+ configureVideoPageTimeouts(page);
220
225
  await page.goto(pageUrl);
221
226
  await page.locator('#fileInput').setInputFiles(inputPath);
222
227
  await page.evaluate((value) => {
@@ -266,13 +271,10 @@ async function processVideoWithPreviewPage(inputPath, options = {}) {
266
271
  }
267
272
 
268
273
  await page.locator('#processBtn').click();
269
- await page.waitForFunction(() => {
270
- const status = document.getElementById('status');
271
- return status?.dataset?.tone === 'success' || status?.dataset?.tone === 'error';
272
- }, null, { timeout: timeoutMs });
273
-
274
- const status = await page.locator('#status').textContent();
275
- const tone = await page.locator('#status').getAttribute('data-tone');
274
+ const { status, tone } = await waitForVideoProcessing(page, {
275
+ timeoutMs,
276
+ onProgress
277
+ });
276
278
  if (tone !== 'success') {
277
279
  throw new Error(status || 'Video export failed');
278
280
  }
@@ -0,0 +1,108 @@
1
+ const DEFAULT_VIDEO_TIMEOUT_MS = 6 * 60 * 1000;
2
+ const DEFAULT_VIDEO_PROGRESS_POLL_INTERVAL_MS = 1000;
3
+ const DEFAULT_VIDEO_PAGE_SETUP_TIMEOUT_MS = 30_000;
4
+
5
+ function delay(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ async function readVideoProgressSnapshot(page) {
10
+ return page.evaluate(() => {
11
+ const status = document.getElementById('status');
12
+ const progressBar = document.getElementById('progressBar');
13
+ const progressText = document.getElementById('progressText');
14
+ const rawProgress = Number.parseFloat(progressBar?.style?.width || '');
15
+ const runtimeProgress = globalThis.__gwrVideoCliProgress;
16
+
17
+ return {
18
+ tone: status?.dataset?.tone || '',
19
+ status: status?.textContent || '',
20
+ progressText: progressText?.textContent || '',
21
+ progress: Number.isFinite(runtimeProgress?.progress)
22
+ ? Math.max(0, Math.min(1, runtimeProgress.progress))
23
+ : Number.isFinite(rawProgress)
24
+ ? Math.max(0, Math.min(1, rawProgress / 100))
25
+ : null,
26
+ phase: runtimeProgress?.phase || null,
27
+ processedFrames: Number.isFinite(runtimeProgress?.processedFrames)
28
+ ? runtimeProgress.processedFrames
29
+ : null,
30
+ frameEstimate: Number.isFinite(runtimeProgress?.frameEstimate)
31
+ ? runtimeProgress.frameEstimate
32
+ : null,
33
+ aiDenoiseFrames: Number.isFinite(runtimeProgress?.aiDenoiseFrames)
34
+ ? runtimeProgress.aiDenoiseFrames
35
+ : null,
36
+ aiReuseFrames: Number.isFinite(runtimeProgress?.aiReuseFrames)
37
+ ? runtimeProgress.aiReuseFrames
38
+ : null
39
+ };
40
+ });
41
+ }
42
+
43
+ function describeProgress(snapshot) {
44
+ return [snapshot?.progressText, snapshot?.status]
45
+ .map((value) => String(value || '').trim())
46
+ .filter(Boolean)
47
+ .filter((value, index, values) => values.indexOf(value) === index)
48
+ .join(' - ');
49
+ }
50
+
51
+ function configureVideoPageTimeouts(page) {
52
+ page.setDefaultTimeout(DEFAULT_VIDEO_PAGE_SETUP_TIMEOUT_MS);
53
+ page.setDefaultNavigationTimeout(DEFAULT_VIDEO_PAGE_SETUP_TIMEOUT_MS);
54
+ }
55
+
56
+ async function waitForVideoProcessing(page, options = {}) {
57
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
58
+ ? options.timeoutMs
59
+ : DEFAULT_VIDEO_TIMEOUT_MS;
60
+ const pollIntervalMs = Number.isFinite(options.pollIntervalMs) && options.pollIntervalMs >= 0
61
+ ? options.pollIntervalMs
62
+ : DEFAULT_VIDEO_PROGRESS_POLL_INTERVAL_MS;
63
+ const now = typeof options.now === 'function' ? options.now : Date.now;
64
+ const sleep = typeof options.sleep === 'function' ? options.sleep : delay;
65
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
66
+ const startedAt = now();
67
+ let lastActivityAt = startedAt;
68
+ let lastSnapshotKey = null;
69
+
70
+ while (true) {
71
+ const snapshot = await readVideoProgressSnapshot(page);
72
+ const elapsedMs = Math.max(0, now() - startedAt);
73
+ const result = { ...snapshot, elapsedMs };
74
+ const snapshotKey = JSON.stringify(snapshot);
75
+ if (snapshotKey !== lastSnapshotKey) {
76
+ lastActivityAt = now();
77
+ lastSnapshotKey = snapshotKey;
78
+ onProgress(result);
79
+ }
80
+
81
+ if (snapshot.tone === 'success' || snapshot.tone === 'error') {
82
+ return result;
83
+ }
84
+
85
+ const inactiveMs = Math.max(0, now() - lastActivityAt);
86
+ if (inactiveMs >= timeoutMs) {
87
+ const detail = describeProgress(snapshot);
88
+ const timeoutLabel = timeoutMs < 1000
89
+ ? `${Math.round(timeoutMs)} ms`
90
+ : `${Math.round(timeoutMs / 1000)} seconds`;
91
+ throw new Error(
92
+ `Video processing made no progress for ${timeoutLabel}${detail ? ` at ${detail}` : ''}. ` +
93
+ 'Increase --video-timeout-ms for unusually long files.'
94
+ );
95
+ }
96
+
97
+ await sleep(Math.min(pollIntervalMs, Math.max(0, timeoutMs - inactiveMs)));
98
+ }
99
+ }
100
+
101
+ export {
102
+ DEFAULT_VIDEO_PAGE_SETUP_TIMEOUT_MS,
103
+ DEFAULT_VIDEO_PROGRESS_POLL_INTERVAL_MS,
104
+ DEFAULT_VIDEO_TIMEOUT_MS,
105
+ configureVideoPageTimeouts,
106
+ describeProgress,
107
+ waitForVideoProcessing
108
+ };