@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/README.md +15 -4
- package/dist/video-app.js +17 -17
- package/package.json +1 -1
- package/src/cli/gwrCli.js +1 -1
- package/src/cli/gwrRemoveCommand.js +52 -4
- package/src/sdk/node.d.ts +1 -0
- package/src/sdk/video.d.ts +23 -8
- package/src/sdk/video.js +19 -17
- package/src/sdk/videoProgress.js +108 -0
package/package.json
CHANGED
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 = {
|
|
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
|
|
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
package/src/sdk/video.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
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
|
+
};
|