@pilio/gemini-watermark-remover 1.0.20 → 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.
@@ -45,6 +45,31 @@ export interface WatermarkConfig {
45
45
  logoSize: number;
46
46
  marginRight: number;
47
47
  marginBottom: number;
48
+ alphaVariant?: string;
49
+ }
50
+
51
+ export interface WatermarkHaloMeta {
52
+ bandCount: number;
53
+ outerCount: number;
54
+ bandMeanLum: number;
55
+ outerMeanLum: number;
56
+ bandStdLum: number;
57
+ outerStdLum: number;
58
+ deltaLum: number;
59
+ positiveDeltaLum: number;
60
+ visibility: number;
61
+ }
62
+
63
+ export interface WatermarkResidualVisibilityMeta {
64
+ visible: boolean;
65
+ positiveHaloLum: number;
66
+ haloVisibility: number;
67
+ spatialResidual: number;
68
+ gradientResidual: number;
69
+ visiblePositiveHalo: boolean;
70
+ visibleGradientResidual: boolean;
71
+ visibleSpatialResidual: boolean;
72
+ halo?: WatermarkHaloMeta;
48
73
  }
49
74
 
50
75
  export interface WatermarkDetectionMeta {
@@ -54,6 +79,7 @@ export interface WatermarkDetectionMeta {
54
79
  processedSpatialScore: number | null;
55
80
  processedGradientScore: number | null;
56
81
  suppressionGain: number | null;
82
+ residualVisibility?: WatermarkResidualVisibilityMeta | null;
57
83
  }
58
84
 
59
85
  export interface WatermarkSelectionDebug {
@@ -91,10 +117,12 @@ export interface WatermarkMeta {
91
117
 
92
118
  export interface RemoveOptions {
93
119
  adaptiveMode?: 'auto' | 'always' | 'never' | 'off';
120
+ aggressiveLocatedFallback?: boolean;
121
+ locatedAggressiveRemoval?: boolean;
94
122
  engine?: WatermarkEngine;
95
123
  alpha48?: Float32Array;
96
124
  alpha96?: Float32Array;
97
- getAlphaMap?: (size: number) => Float32Array;
125
+ getAlphaMap?: (size: number | string) => Float32Array;
98
126
  }
99
127
 
100
128
  export interface ImageDataRemovalResult {
package/src/sdk/node.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  import type { ImageDataRemovalResult, RemoveOptions, WatermarkMeta } from './index.js';
2
+ export type {
3
+ VideoBufferRemovalOptions,
4
+ VideoBufferRemovalResult,
5
+ VideoFileRemovalOptions,
6
+ VideoFileRemovalResult,
7
+ VideoRemovalMeta
8
+ } from './video.js';
2
9
 
3
10
  export interface NodeCodecContext {
4
11
  mimeType: string;
@@ -40,3 +47,9 @@ export function removeWatermarkFromFile(
40
47
  inputPath: string,
41
48
  options: NodeFileRemovalOptions
42
49
  ): Promise<NodeFileRemovalResult>;
50
+ export {
51
+ inferVideoMimeTypeFromPath,
52
+ isVideoMimeType,
53
+ removeVideoWatermarkFromBuffer,
54
+ removeVideoWatermarkFromFile
55
+ } from './video.js';
package/src/sdk/node.js CHANGED
@@ -75,3 +75,10 @@ export async function removeWatermarkFromFile(inputPath, options = {}) {
75
75
  outputPath
76
76
  };
77
77
  }
78
+
79
+ export {
80
+ inferVideoMimeTypeFromPath,
81
+ isVideoMimeType,
82
+ removeVideoWatermarkFromBuffer,
83
+ removeVideoWatermarkFromFile
84
+ } from './video.js';
@@ -0,0 +1,84 @@
1
+ export interface VideoRemovalMeta {
2
+ status?: string;
3
+ denoiseBackend?: string;
4
+ actualDenoiseBackend?: string;
5
+ actualControls?: Record<string, unknown>;
6
+ pagePath?: string;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ export interface VideoFileProcessorContext {
11
+ outputPath?: string | null;
12
+ mimeType: string;
13
+ filePath: string;
14
+ pagePath?: string;
15
+ denoiseBackend?: string;
16
+ allowLowConfidence?: boolean;
17
+ timeoutMs?: number;
18
+ edgeDenoiseStrength?: number;
19
+ residualCleanupStrength?: number;
20
+ videoBitrate?: number;
21
+ adaptiveAlpha?: boolean;
22
+ alphaGain?: number;
23
+ alphaProfile?: string;
24
+ }
25
+
26
+ export interface VideoBufferProcessorContext extends Omit<VideoFileProcessorContext, 'filePath'> {
27
+ filePath?: string;
28
+ }
29
+
30
+ export interface VideoProcessorResult {
31
+ buffer?: Buffer | Uint8Array | ArrayBuffer;
32
+ meta?: VideoRemovalMeta | null;
33
+ }
34
+
35
+ export interface VideoFileRemovalOptions {
36
+ outputPath?: string | null;
37
+ mimeType?: string;
38
+ pagePath?: string;
39
+ denoiseBackend?: string;
40
+ allowLowConfidence?: boolean;
41
+ timeoutMs?: number;
42
+ edgeDenoiseStrength?: number;
43
+ residualCleanupStrength?: number;
44
+ videoBitrate?: number;
45
+ adaptiveAlpha?: boolean;
46
+ alphaGain?: number;
47
+ alphaProfile?: string;
48
+ processVideoFile?: (
49
+ inputPath: string,
50
+ context: VideoFileProcessorContext
51
+ ) => Promise<VideoProcessorResult> | VideoProcessorResult;
52
+ }
53
+
54
+ export interface VideoBufferRemovalOptions extends VideoBufferProcessorContext {
55
+ processVideoBuffer: (
56
+ inputBuffer: Buffer,
57
+ context: VideoBufferProcessorContext
58
+ ) => Promise<VideoProcessorResult> | VideoProcessorResult;
59
+ }
60
+
61
+ export interface VideoFileRemovalResult {
62
+ buffer: Buffer;
63
+ outputPath: string | null;
64
+ mimeType: string;
65
+ meta: VideoRemovalMeta | null;
66
+ }
67
+
68
+ export interface VideoBufferRemovalResult {
69
+ buffer: Buffer;
70
+ mimeType: string;
71
+ meta: VideoRemovalMeta | null;
72
+ }
73
+
74
+ export function inferVideoMimeTypeFromPath(filePath: string): string;
75
+ export function isVideoMimeType(mimeType: string): boolean;
76
+ export function removeVideoWatermarkFromFile(
77
+ inputPath: string,
78
+ options?: VideoFileRemovalOptions
79
+ ): Promise<VideoFileRemovalResult>;
80
+ export function removeVideoWatermarkFromBuffer(
81
+ inputBuffer: Buffer | Uint8Array | ArrayBuffer,
82
+ options: VideoBufferRemovalOptions
83
+ ): Promise<VideoBufferRemovalResult>;
84
+
@@ -0,0 +1,263 @@
1
+ import path from 'node:path';
2
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ const DEFAULT_VIDEO_DENOISE_BACKEND = 'allenk-fdncnn-browser-spike';
6
+ const DEFAULT_VIDEO_TIMEOUT_MS = 6 * 60 * 1000;
7
+
8
+ function normalizeBufferLike(value) {
9
+ if (Buffer.isBuffer(value)) return value;
10
+ if (value instanceof Uint8Array) return Buffer.from(value);
11
+ if (value instanceof ArrayBuffer) return Buffer.from(value);
12
+ throw new TypeError('Expected Buffer, Uint8Array, or ArrayBuffer');
13
+ }
14
+
15
+ function assertFunction(value, name) {
16
+ if (typeof value !== 'function') {
17
+ throw new TypeError(`${name} must be a function`);
18
+ }
19
+ }
20
+
21
+ function isHttpUrl(value) {
22
+ return /^https?:\/\//i.test(String(value || ''));
23
+ }
24
+
25
+ function resolveDefaultVideoPreviewPage() {
26
+ return path.resolve(process.cwd(), 'dist', 'video-preview.html');
27
+ }
28
+
29
+ async function assertReadableFile(filePath, label) {
30
+ try {
31
+ await access(filePath);
32
+ } catch (error) {
33
+ throw new Error(`${label} is unavailable: ${filePath}`, { cause: error });
34
+ }
35
+ }
36
+
37
+ export function inferVideoMimeTypeFromPath(filePath) {
38
+ const ext = path.extname(filePath || '').toLowerCase();
39
+ if (ext === '.mp4' || ext === '.m4v') return 'video/mp4';
40
+ if (ext === '.webm') return 'video/webm';
41
+ if (ext === '.mov') return 'video/quicktime';
42
+ return 'application/octet-stream';
43
+ }
44
+
45
+ export function isVideoMimeType(mimeType) {
46
+ return String(mimeType || '').toLowerCase().startsWith('video/');
47
+ }
48
+
49
+ async function setControlValue(page, selector, value) {
50
+ await page.evaluate(({ selector: targetSelector, value: targetValue }) => {
51
+ const control = document.querySelector(targetSelector);
52
+ if (!control) throw new Error(`Cannot find control: ${targetSelector}`);
53
+ control.value = String(targetValue);
54
+ control.dispatchEvent(new Event('input', { bubbles: true }));
55
+ control.dispatchEvent(new Event('change', { bubbles: true }));
56
+ }, { selector, value });
57
+ }
58
+
59
+ async function setCheckboxValue(page, selector, checked) {
60
+ await page.evaluate(({ selector: targetSelector, checked: targetChecked }) => {
61
+ const checkbox = document.querySelector(targetSelector);
62
+ if (!checkbox) throw new Error(`Cannot find control: ${targetSelector}`);
63
+ checkbox.checked = Boolean(targetChecked);
64
+ checkbox.dispatchEvent(new Event('input', { bubbles: true }));
65
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }));
66
+ }, { selector, checked });
67
+ }
68
+
69
+ async function setNumericInputValue(page, selector, value) {
70
+ await page.evaluate(({ selector: targetSelector, value: targetValue }) => {
71
+ const input = document.querySelector(targetSelector);
72
+ if (!input) throw new Error(`Cannot find control: ${targetSelector}`);
73
+ if (input.hasAttribute('max') && Number(targetValue) > Number(input.getAttribute('max'))) {
74
+ input.setAttribute('max', String(targetValue));
75
+ }
76
+ input.value = String(targetValue);
77
+ input.dispatchEvent(new Event('input', { bubbles: true }));
78
+ input.dispatchEvent(new Event('change', { bubbles: true }));
79
+ }, { selector, value });
80
+ }
81
+
82
+ async function readDownloadBufferFromPage(page) {
83
+ const base64 = await page.evaluate(async () => {
84
+ const link = document.getElementById('downloadBtn');
85
+ if (!link?.href || link.getAttribute('aria-disabled') === 'true') {
86
+ throw new Error('Video export has no downloadable result');
87
+ }
88
+ const blob = await fetch(link.href).then((response) => response.blob());
89
+ const reader = new FileReader();
90
+ return await new Promise((resolve, reject) => {
91
+ reader.onerror = () => reject(reader.error);
92
+ reader.onload = () => {
93
+ const result = String(reader.result || '');
94
+ resolve(result.includes(',') ? result.split(',')[1] : result);
95
+ };
96
+ reader.readAsDataURL(blob);
97
+ });
98
+ });
99
+ return Buffer.from(base64, 'base64');
100
+ }
101
+
102
+ async function collectVideoControls(page) {
103
+ return page.evaluate(() => ({
104
+ denoiseBackend: document.getElementById('denoiseBackend')?.value || '',
105
+ edgeDenoiseStrength: Number(document.getElementById('edgeDenoiseStrength')?.value),
106
+ residualCleanupStrength: Number(document.getElementById('residualCleanup')?.value),
107
+ videoBitrateMbps: Number(document.getElementById('videoBitrateMbps')?.value),
108
+ allowLowConfidence: Boolean(document.getElementById('allowLowConfidence')?.checked)
109
+ }));
110
+ }
111
+
112
+ async function processVideoWithPreviewPage(inputPath, options = {}) {
113
+ const {
114
+ pagePath = resolveDefaultVideoPreviewPage(),
115
+ denoiseBackend = DEFAULT_VIDEO_DENOISE_BACKEND,
116
+ allowLowConfidence = false,
117
+ timeoutMs = DEFAULT_VIDEO_TIMEOUT_MS,
118
+ edgeDenoiseStrength,
119
+ residualCleanupStrength,
120
+ videoBitrate,
121
+ adaptiveAlpha = false,
122
+ alphaGain,
123
+ alphaProfile
124
+ } = options;
125
+
126
+ if (!isHttpUrl(pagePath)) {
127
+ await assertReadableFile(pagePath, 'Video preview page');
128
+ }
129
+
130
+ const { chromium } = await import('playwright').catch((error) => {
131
+ throw new Error('Video processing requires the optional "playwright" dependency', { cause: error });
132
+ });
133
+ const browser = await chromium.launch({ headless: true });
134
+ try {
135
+ const page = await browser.newPage();
136
+ page.setDefaultTimeout(timeoutMs);
137
+ await page.goto(isHttpUrl(pagePath) ? pagePath : pathToFileURL(pagePath).href);
138
+ await page.locator('#fileInput').setInputFiles(inputPath);
139
+ await setControlValue(page, '#denoiseBackend', denoiseBackend);
140
+
141
+ if (adaptiveAlpha) {
142
+ await setCheckboxValue(page, '#adaptiveAlpha', true);
143
+ }
144
+ if (allowLowConfidence) {
145
+ await page.evaluate(() => {
146
+ window.__gwrVideoOverrideAllowLowConfidence = true;
147
+ });
148
+ await setCheckboxValue(page, '#allowLowConfidence', true);
149
+ }
150
+ if (Number.isFinite(alphaGain) && alphaGain > 0) {
151
+ await setNumericInputValue(page, '#alphaGain', Math.max(0.25, Math.min(1.35, alphaGain)));
152
+ }
153
+ if (typeof alphaProfile === 'string' && alphaProfile) {
154
+ await page.evaluate((value) => {
155
+ window.__gwrVideoAlphaProfile = value;
156
+ }, alphaProfile);
157
+ }
158
+ if (Number.isFinite(edgeDenoiseStrength)) {
159
+ const value = Math.max(0, Math.min(3, edgeDenoiseStrength));
160
+ await page.evaluate((nextValue) => {
161
+ window.__gwrVideoOverrideEdgeDenoiseStrength = nextValue;
162
+ }, value);
163
+ await setNumericInputValue(page, '#edgeDenoiseStrength', value);
164
+ }
165
+ if (Number.isFinite(residualCleanupStrength)) {
166
+ const value = Math.max(0, Math.min(1.8, residualCleanupStrength));
167
+ await page.evaluate((nextValue) => {
168
+ window.__gwrVideoOverrideResidualCleanupStrength = nextValue;
169
+ }, value);
170
+ await setNumericInputValue(page, '#residualCleanup', value);
171
+ }
172
+ if (Number.isFinite(videoBitrate) && videoBitrate > 0) {
173
+ await setNumericInputValue(page, '#videoBitrateMbps', videoBitrate / 1000 / 1000);
174
+ }
175
+
176
+ await page.locator('#processBtn').click();
177
+ await page.waitForFunction(() => {
178
+ const status = document.getElementById('status');
179
+ return status?.dataset?.tone === 'success' || status?.dataset?.tone === 'error';
180
+ }, null, { timeout: timeoutMs });
181
+
182
+ const status = await page.locator('#status').textContent();
183
+ const tone = await page.locator('#status').getAttribute('data-tone');
184
+ if (tone !== 'success') {
185
+ throw new Error(status || 'Video export failed');
186
+ }
187
+
188
+ const controls = await collectVideoControls(page);
189
+ const buffer = await readDownloadBufferFromPage(page);
190
+ return {
191
+ buffer,
192
+ meta: {
193
+ status,
194
+ denoiseBackend,
195
+ actualDenoiseBackend: controls.denoiseBackend,
196
+ actualControls: controls,
197
+ pagePath
198
+ }
199
+ };
200
+ } finally {
201
+ await browser.close();
202
+ }
203
+ }
204
+
205
+ export async function removeVideoWatermarkFromFile(inputPath, options = {}) {
206
+ const {
207
+ outputPath = null,
208
+ mimeType = inferVideoMimeTypeFromPath(inputPath),
209
+ processVideoFile = null,
210
+ ...videoOptions
211
+ } = options;
212
+
213
+ if (processVideoFile !== null) {
214
+ assertFunction(processVideoFile, 'processVideoFile');
215
+ const customResult = await processVideoFile(inputPath, {
216
+ ...videoOptions,
217
+ outputPath,
218
+ mimeType,
219
+ filePath: inputPath
220
+ });
221
+ const buffer = customResult?.buffer
222
+ ? normalizeBufferLike(customResult.buffer)
223
+ : outputPath
224
+ ? await readFile(outputPath)
225
+ : null;
226
+ if (!buffer) {
227
+ throw new Error('processVideoFile must return a buffer or write outputPath');
228
+ }
229
+ if (outputPath && customResult?.buffer) {
230
+ await mkdir(path.dirname(outputPath), { recursive: true });
231
+ await writeFile(outputPath, buffer);
232
+ }
233
+ return {
234
+ buffer,
235
+ outputPath,
236
+ mimeType,
237
+ meta: customResult?.meta ?? null
238
+ };
239
+ }
240
+
241
+ const result = await processVideoWithPreviewPage(inputPath, videoOptions);
242
+ if (outputPath) {
243
+ await mkdir(path.dirname(outputPath), { recursive: true });
244
+ await writeFile(outputPath, result.buffer);
245
+ }
246
+ return {
247
+ buffer: result.buffer,
248
+ outputPath,
249
+ mimeType,
250
+ meta: result.meta
251
+ };
252
+ }
253
+
254
+ export async function removeVideoWatermarkFromBuffer(inputBuffer, options = {}) {
255
+ const { processVideoBuffer = null, ...videoOptions } = options;
256
+ assertFunction(processVideoBuffer, 'processVideoBuffer');
257
+ const result = await processVideoBuffer(normalizeBufferLike(inputBuffer), videoOptions);
258
+ return {
259
+ buffer: normalizeBufferLike(result?.buffer),
260
+ mimeType: videoOptions.mimeType || 'video/mp4',
261
+ meta: result?.meta ?? null
262
+ };
263
+ }
@@ -0,0 +1,109 @@
1
+ const DB_NAME = 'gwr-debug-file-handoff';
2
+ const DB_VERSION = 1;
3
+ const STORE_NAME = 'files';
4
+ const LATEST_KEY = 'latest';
5
+
6
+ const IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
7
+ const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
8
+ const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.m4v']);
9
+
10
+ function getFileExtension(file) {
11
+ const name = typeof file?.name === 'string' ? file.name.toLowerCase() : '';
12
+ const dotIndex = name.lastIndexOf('.');
13
+ return dotIndex >= 0 ? name.slice(dotIndex) : '';
14
+ }
15
+
16
+ export function getDebugFileKind(file) {
17
+ if (!file) return null;
18
+
19
+ const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
20
+ const extension = getFileExtension(file);
21
+
22
+ if (type.startsWith('video/') || VIDEO_EXTENSIONS.has(extension)) {
23
+ return 'video';
24
+ }
25
+ if (IMAGE_TYPES.has(type) || IMAGE_EXTENSIONS.has(extension)) {
26
+ return 'image';
27
+ }
28
+ return null;
29
+ }
30
+
31
+ export function pickDebugUploadFile(files) {
32
+ const list = Array.from(files || []).filter(Boolean);
33
+ return (
34
+ list.find((file) => getDebugFileKind(file) === 'video')
35
+ || list.find((file) => getDebugFileKind(file) === 'image')
36
+ || null
37
+ );
38
+ }
39
+
40
+ function openHandoffDb() {
41
+ return new Promise((resolve, reject) => {
42
+ const indexedDb = globalThis.indexedDB;
43
+ if (!indexedDb) {
44
+ reject(new Error('当前浏览器不支持本地文件暂存,请直接打开目标调试页后重新选择文件。'));
45
+ return;
46
+ }
47
+
48
+ const request = indexedDb.open(DB_NAME, DB_VERSION);
49
+ request.onupgradeneeded = () => {
50
+ const db = request.result;
51
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
52
+ db.createObjectStore(STORE_NAME, { keyPath: 'id' });
53
+ }
54
+ };
55
+ request.onerror = () => reject(request.error || new Error('无法打开本地文件暂存。'));
56
+ request.onsuccess = () => resolve(request.result);
57
+ });
58
+ }
59
+
60
+ export async function saveDebugFileHandoff(file, targetKind = getDebugFileKind(file)) {
61
+ if (!file || !targetKind) {
62
+ throw new Error('不支持的文件类型。');
63
+ }
64
+
65
+ const record = {
66
+ id: LATEST_KEY,
67
+ kind: targetKind,
68
+ file,
69
+ name: file.name || '',
70
+ type: file.type || '',
71
+ size: Number.isFinite(file.size) ? file.size : 0,
72
+ updatedAt: Date.now()
73
+ };
74
+
75
+ const db = await openHandoffDb();
76
+ await new Promise((resolve, reject) => {
77
+ const transaction = db.transaction(STORE_NAME, 'readwrite');
78
+ transaction.objectStore(STORE_NAME).put(record);
79
+ transaction.oncomplete = () => resolve();
80
+ transaction.onerror = () => reject(transaction.error || new Error('本地文件暂存失败。'));
81
+ transaction.onabort = () => reject(transaction.error || new Error('本地文件暂存已取消。'));
82
+ }).finally(() => db.close());
83
+ return record;
84
+ }
85
+
86
+ export async function consumeDebugFileHandoff(expectedKind = null) {
87
+ const db = await openHandoffDb();
88
+ let matchedRecord = null;
89
+
90
+ await new Promise((resolve, reject) => {
91
+ const transaction = db.transaction(STORE_NAME, 'readwrite');
92
+ const store = transaction.objectStore(STORE_NAME);
93
+ const request = store.get(LATEST_KEY);
94
+
95
+ request.onsuccess = () => {
96
+ const record = request.result || null;
97
+ if (record && (!expectedKind || record.kind === expectedKind)) {
98
+ matchedRecord = record;
99
+ store.delete(LATEST_KEY);
100
+ }
101
+ };
102
+ request.onerror = () => reject(request.error || new Error('读取本地文件暂存失败。'));
103
+ transaction.oncomplete = () => resolve();
104
+ transaction.onerror = () => reject(transaction.error || new Error('读取本地文件暂存失败。'));
105
+ transaction.onabort = () => reject(transaction.error || new Error('读取本地文件暂存已取消。'));
106
+ }).finally(() => db.close());
107
+
108
+ return matchedRecord;
109
+ }