Greasy Fork

Greasy Fork is available in English.

Google banana 去除右下角水印

去除 Gemini/AI Studio 生成图片的水印。注意:处理时页面会卡UI,请耐心等待。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Google banana 去除右下角水印
// @description  去除 Gemini/AI Studio 生成图片的水印。注意:处理时页面会卡UI,请耐心等待。
// @version      1.2.0
// @author       会飞的蛋蛋面
// @license      All Rights Reserved
// @namespace    http://tampermonkey.net/
// @match        https://aistudio.google.com/*
// @match        https://ai.google.dev/*
// @match        https://gemini.google.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.webgpu.min.js
// @connect      hf-mirror.com
// @connect      cas-bridge.xethub.hf.co
// @connect      cdn.jsdelivr.net.cn
// @connect      lh3.googleusercontent.com
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function() {
        "use strict";
        const isTop = window.top === window.self;
        if (!isTop) return;
        const rootWin = unsafeWindow.top || unsafeWindow;
        if (rootWin.__wmWatermarkAssistantLoaded) return;
        rootWin.__wmWatermarkAssistantLoaded = true;
        const host = location.hostname;
        if (host.includes("aistudio") || host.includes("ai.google.dev")) runAiStudioInterceptor(); else if (host.includes("gemini")) runGeminiWatermarkRemover();
        function runAiStudioInterceptor() {
            const KEY = "watermark";
            const isBad = u => typeof u === "string" && u.includes(KEY);
            const origFetch = unsafeWindow.fetch;
            unsafeWindow.fetch = (...args) => isBad(args[0]?.url || args[0]) ? Promise.reject() : origFetch.apply(this, args);
            const XHR = unsafeWindow.XMLHttpRequest.prototype;
            const origOpen = XHR.open;
            const origSend = XHR.send;
            XHR.open = function(m, u, ...a) {
                if (isBad(u)) this._block = true; else origOpen.apply(this, [ m, u, ...a ]);
            };
            XHR.send = function(...a) {
                if (!this._block) origSend.apply(this, a);
            };
        }
        function runGeminiWatermarkRemover() {
            const DB_NAME = "GeminiWatermarkDB";
            const STORE_NAME = "models";
            const MODEL_KEY = "lama_fp32_carve_20240515";
            const MODEL_URL = "https://hf-mirror.com/Carve/LaMa-ONNX/resolve/main/lama_fp32.onnx";
            const REQUESTED_ORT_VERSION = "1.24.3";
            const ORT_VERSION = getLoadedOrtVersion() || REQUESTED_ORT_VERSION;
            const ORT_CDN_BASE = `https://cdn.jsdelivr.net.cn/npm/onnxruntime-web@${ORT_VERSION}/dist/`;
            const MODEL_DOWNLOAD_TIMEOUT_MS = 90 * 1e3;
            const WASM_DOWNLOAD_TIMEOUT_MS = 30 * 1e3;
            const SESSION_CREATE_TIMEOUT_MS = 60 * 1e3;
            const INFERENCE_TIMEOUT_MS = 60 * 1e3;
            const COPY_TIMEOUT_MS = 30 * 1e3;
            const IMAGE_FETCH_TIMEOUT_MS = 15 * 1e3;
            const PREWARM_DELAY_MS = 1500;
            const PREWARM_TIMEOUT_MS = 15 * 1e3;
            const IMAGE_BLOB_CACHE_TTL_MS = 5 * 60 * 1e3;
            const COPY_TOAST_SUPPRESS_MS = 2500;
            const MAX_WASM_THREADS = 4;
            const LOG_PREFIX = "[水印助手]";
            const MODEL_SIZE = 512;
            const WM_BASE = 1024;
            const WM_BOX_W_AT_BASE = 96;
            const WM_BOX_H_AT_BASE = 96;
            const WM_BOX_RIGHT_MARGIN_AT_BASE = 16;
            const WM_BOX_BOTTOM_MARGIN_AT_BASE = 16;
            const WM_BOX_PAD_AT_BASE = 8;
            const WM_CROP_SIZE_AT_BASE = 320;
            let capturedImageBlob;
            let capturedImageBlobAt = 0;
            let ignoreCreateObjectUrlCapture = false;
            let ortSession;
            let ortSessionPromise;
            let progressBar;
            let progressRafId = 0;
            let progressValue = 0;
            let progressStartAt = 0;
            let geminiInitAt = 0;
            let suppressClipboardToastUntil = 0;
            let ortPrewarmPromise = null;
            let ortPrewarmState = null;
            let ortSessionMeta = null;
            const imageBlobCache = new Map;
            const runtimeCaps = detectRuntimeCapabilities();
            const webgpuCompatState = installWebGPUCompatibilityShim();
            const ortWasmArtifacts = getOrtWasmArtifacts();
            const preferredWasmFile = ortWasmArtifacts.preferredWasmFile;
            geminiInitAt = performance.now();
            let wasmBufferCache = {};
            function getWasmBuffer(filename) {
                if (!wasmBufferCache[filename]) {
                    const wasmUrl = ORT_CDN_BASE + filename;
                    wasmBufferCache[filename] = gmGetArrayBuffer(wasmUrl, WASM_DOWNLOAD_TIMEOUT_MS, "WASM下载").then(({buffer: buffer}) => buffer).catch(error => {
                        delete wasmBufferCache[filename];
                        throw error;
                    });
                }
                return wasmBufferCache[filename];
            }
            const originalFetch = window.fetch;
            window.fetch = async function(input, init) {
                try {
                    let url = "";
                    if (typeof input === "string") url = input; else if (input && typeof input === "object") url = input.url || "";
                    if (url && typeof url === "string" && url.includes("ort-wasm")) {
                        const match = url.match(/ort-wasm[^/]*\.wasm$/);
                        if (match) {
                            const filename = match[0];
                            const buffer = await getWasmBuffer(filename);
                            return new Response(buffer, {
                                status: 200,
                                headers: {
                                    "Content-Type": "application/wasm"
                                }
                            });
                        }
                    }
                } catch (e) {}
                return originalFetch.apply(this, arguments);
            };
            getWasmBuffer(preferredWasmFile).catch(() => {});
            getOrtSession().catch(error => {
                logWarn("模型预加载失败,可点击时重试", {
                    message: simplifyError(error)
                });
            });
            hookImageBlobCapture();
            scheduleAfterLoad(initGeminiUi);
            function initGeminiUi() {
                if (rootWin.__wmGeminiUiInitialized) return;
                rootWin.__wmGeminiUiInitialized = true;
                installClipboardToastSuppressor();
                observeImages(getOrtSession);
            }
            function ensureProgressBar() {
                if (progressBar) return progressBar;
                const bar = document.createElement("div");
                bar.className = "wm-progress";
                Object.assign(bar.style, {
                    position: "fixed",
                    top: "0",
                    left: "0",
                    width: "100%",
                    height: "3px",
                    background: "#1a73e8",
                    transformOrigin: "0 0",
                    transform: "scaleX(0)",
                    opacity: "0",
                    transition: "opacity 120ms ease",
                    zIndex: 2147483647,
                    pointerEvents: "none"
                });
                document.body.appendChild(bar);
                progressBar = bar;
                return bar;
            }
            function startInferenceProgress() {
                const bar = ensureProgressBar();
                if (progressRafId) cancelAnimationFrame(progressRafId);
                progressValue = 0;
                progressStartAt = performance.now();
                bar.style.opacity = "1";
                bar.style.transform = "scaleX(0.02)";
                const step = now => {
                    const elapsed = now - progressStartAt;
                    const target = .9;
                    const duration = 3500;
                    const next = Math.min(target, elapsed / duration * target);
                    if (next > progressValue) progressValue = next;
                    bar.style.transform = `scaleX(${progressValue})`;
                    if (progressValue < target) progressRafId = requestAnimationFrame(step); else progressRafId = 0;
                };
                progressRafId = requestAnimationFrame(step);
            }
            function finishInferenceProgress() {
                if (!progressBar) return;
                if (progressRafId) cancelAnimationFrame(progressRafId);
                progressRafId = 0;
                progressValue = 1;
                progressBar.style.transform = "scaleX(1)";
                setTimeout(() => {
                    progressBar.style.opacity = "0";
                    progressBar.style.transform = "scaleX(0)";
                }, 200);
            }
            function showInferenceOverlay() {
                const overlay = document.createElement("div");
                overlay.className = "wm-inference-overlay";
                Object.assign(overlay.style, {
                    position: "fixed",
                    top: "0",
                    left: "0",
                    width: "100%",
                    height: "100%",
                    background: "rgba(0,0,0,0.5)",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    zIndex: 2147483646,
                    pointerEvents: "none"
                });
                const box = document.createElement("div");
                Object.assign(box.style, {
                    background: "#fff",
                    padding: "24px 32px",
                    borderRadius: "12px",
                    boxShadow: "0 4px 24px rgba(0,0,0,0.2)",
                    textAlign: "center",
                    fontFamily: "system-ui, sans-serif"
                });
                const title = document.createElement("div");
                Object.assign(title.style, {
                    fontSize: "18px",
                    fontWeight: "500",
                    marginBottom: "8px"
                });
                title.textContent = "正在去除水印...";
                const subtitle = document.createElement("div");
                Object.assign(subtitle.style, {
                    fontSize: "14px",
                    color: "#666"
                });
                subtitle.textContent = "请稍候,约需 30 秒";
                box.appendChild(title);
                box.appendChild(subtitle);
                overlay.appendChild(box);
                document.body.appendChild(overlay);
                return overlay;
            }
            function hideInferenceOverlay(overlay) {
                if (overlay && overlay.parentNode) overlay.remove();
            }
            function scheduleAfterLoad(fn) {
                if (document.readyState === "complete") {
                    fn();
                    return;
                }
                window.addEventListener("load", fn, {
                    once: true
                });
            }
            function openDb() {
                return new Promise((resolve, reject) => {
                    const req = indexedDB.open(DB_NAME, 1);
                    req.onupgradeneeded = e => e.target.result.createObjectStore(STORE_NAME);
                    req.onsuccess = e => resolve(e.target.result);
                    req.onerror = reject;
                });
            }
            function resetOrtSession() {
                ortSession = null;
                ortSessionPromise = null;
                ortSessionMeta = null;
                ortPrewarmPromise = null;
                ortPrewarmState = null;
            }
            function isWebGpuBackend() {
                return ortSessionMeta?.backend === "webgpu";
            }
            function waitForIdleSlot(delayMs = PREWARM_DELAY_MS) {
                return new Promise(resolve => {
                    setTimeout(() => {
                        if (typeof requestIdleCallback === "function") {
                            requestIdleCallback(() => resolve(), {
                                timeout: PREWARM_DELAY_MS
                            });
                            return;
                        }
                        setTimeout(resolve, 0);
                    }, delayMs);
                });
            }
            function withTimeout(promise, timeoutMs, label) {
                let timeoutId = 0;
                return Promise.race([ promise, new Promise((_, reject) => {
                    timeoutId = setTimeout(() => reject(new Error(`${label}超时`)), timeoutMs);
                }) ]).finally(() => clearTimeout(timeoutId));
            }
            function getTimeoutErrorMessage(message) {
                if (!message || !message.includes("超时")) return "";
                if (message.includes("模型下载") || message.includes("WASM下载") || message.includes("模型初始化")) return "模型加载超时";
                if (message.includes("图片获取") || message.includes("复制图片")) return "图片获取超时";
                if (message.includes("推理")) return "推理超时";
                if (message.includes("模型预热")) return "模型加载超时";
                return "请求超时";
            }
            function getUserFacingErrorMessage(error) {
                const message = error?.message || "";
                if (!message) return "处理失败";
                const timeoutMessage = getTimeoutErrorMessage(message);
                if (timeoutMessage) return timeoutMessage;
                if (message.includes("Request failed") || message.includes("Request timed out")) return "网络请求失败";
                if (message.length > 18) return message.slice(0, 18) + "...";
                return message;
            }
            function detectRuntimeCapabilities() {
                const supportsWasmThreads = self.crossOriginIsolated && typeof SharedArrayBuffer !== "undefined";
                return {
                    preferWebGPU: !!navigator.gpu,
                    supportsWasmThreads: supportsWasmThreads,
                    wasmThreads: supportsWasmThreads ? Math.min(MAX_WASM_THREADS, Math.max(2, Math.floor((navigator.hardwareConcurrency || 4) / 2))) : 1
                };
            }
            function getLoadedOrtVersion() {
                if (typeof ort !== "undefined" && typeof ort?.version === "string" && ort.version) return ort.version;
                return "";
            }
            function installWebGPUCompatibilityShim() {
                if (!navigator.gpu || typeof navigator.gpu.requestAdapter !== "function") return {
                    enabled: false,
                    hits: 0
                };
                if (navigator.gpu.__wmRequestAdapterInfoShimState) return navigator.gpu.__wmRequestAdapterInfoShimState;
                const state = {
                    enabled: true,
                    hits: 0
                };
                const originalRequestAdapter = navigator.gpu.requestAdapter.bind(navigator.gpu);
                navigator.gpu.requestAdapter = async function(...args) {
                    const adapter = await originalRequestAdapter(...args);
                    if (adapter && typeof adapter.requestAdapterInfo !== "function") {
                        const fallbackInfo = adapter.info || {
                            vendor: "unknown",
                            architecture: "unknown",
                            device: "unknown",
                            description: "unknown"
                        };
                        try {
                            Object.defineProperty(adapter, "requestAdapterInfo", {
                                configurable: true,
                                value: async () => adapter.info || fallbackInfo
                            });
                        } catch (_) {
                            adapter.requestAdapterInfo = async () => adapter.info || fallbackInfo;
                        }
                        state.hits += 1;
                    }
                    return adapter;
                };
                navigator.gpu.__wmRequestAdapterInfoShimState = state;
                return state;
            }
            function isModernOrtLayout() {
                const [major, minor] = ORT_VERSION.split(".").map(value => Number.parseInt(value, 10) || 0);
                return major > 1 || major === 1 && minor >= 20;
            }
            function getOrtWasmArtifacts() {
                if (isModernOrtLayout()) {
                    if (runtimeCaps.preferWebGPU) return {
                        preferredWasmFile: "ort-wasm-simd-threaded.asyncify.wasm",
                        wasmPaths: {
                            mjs: `${ORT_CDN_BASE}ort-wasm-simd-threaded.asyncify.mjs`,
                            wasm: `${ORT_CDN_BASE}ort-wasm-simd-threaded.asyncify.wasm`
                        }
                    };
                    return {
                        preferredWasmFile: "ort-wasm-simd-threaded.wasm",
                        wasmPaths: {
                            mjs: `${ORT_CDN_BASE}ort-wasm-simd-threaded.mjs`,
                            wasm: `${ORT_CDN_BASE}ort-wasm-simd-threaded.wasm`
                        }
                    };
                }
                const requestedName = runtimeCaps.supportsWasmThreads ? "ort-wasm-simd-threaded.wasm" : "ort-wasm-simd.wasm";
                const actualName = runtimeCaps.supportsWasmThreads ? "ort-wasm-simd-threaded.jsep.wasm" : "ort-wasm-simd.jsep.wasm";
                return {
                    preferredWasmFile: actualName,
                    wasmPaths: {
                        [requestedName]: `${ORT_CDN_BASE}${actualName}`
                    }
                };
            }
            function getOrtWasmPaths() {
                return ortWasmArtifacts.wasmPaths;
            }
            function configureOrtEnv() {
                ort.env.logLevel = "error";
                ort.env.wasm.simd = true;
                ort.env.wasm.numThreads = runtimeCaps.wasmThreads;
                ort.env.wasm.wasmPaths = getOrtWasmPaths();
                if (ort.env.webgpu) ort.env.webgpu.powerPreference = "high-performance";
            }
            function getSessionAttempts() {
                const attempts = [];
                if (runtimeCaps.preferWebGPU) attempts.push({
                    label: "webgpu",
                    options: {
                        executionProviders: [ "webgpu", "wasm" ]
                    }
                });
                attempts.push({
                    label: "wasm",
                    options: {
                        executionProviders: [ "wasm" ]
                    }
                });
                return attempts;
            }
            async function createOrtSession(modelBuffer) {
                let lastError;
                for (const attempt of getSessionAttempts()) try {
                    const createStartAt = performance.now();
                    const session = await withTimeout(ort.InferenceSession.create(modelBuffer, attempt.options), SESSION_CREATE_TIMEOUT_MS, "模型初始化");
                    const sessionCreateMs = Math.round(performance.now() - createStartAt);
                    const backend = attempt.label.startsWith("webgpu") && ort.env.webgpu?.device ? "webgpu" : "wasm";
                    return {
                        session: session,
                        sessionCreateMs: sessionCreateMs,
                        backend: backend
                    };
                } catch (error) {
                    lastError = error;
                    if (attempt.label === "wasm") throw error;
                }
                throw lastError || new Error("模型初始化失败");
            }
            function createDummyInpaintFeeds() {
                const size = MODEL_SIZE * MODEL_SIZE;
                const dims = [ 1, 3, MODEL_SIZE, MODEL_SIZE ];
                const imageData = new Float32Array(3 * size);
                const maskData = new Float32Array(size);
                for (let y = MODEL_SIZE - 96; y < MODEL_SIZE; y++) {
                    const row = y * MODEL_SIZE;
                    for (let x = MODEL_SIZE - 96; x < MODEL_SIZE; x++) maskData[row + x] = 1;
                }
                return {
                    image: new ort.Tensor("float32", imageData, dims),
                    mask: new ort.Tensor("float32", maskData, [ 1, 1, MODEL_SIZE, MODEL_SIZE ])
                };
            }
            function scheduleSessionPrewarm(session) {
                if (!isWebGpuBackend() || ortPrewarmState?.session === session) return;
                ortPrewarmState = {
                    session: session,
                    status: "scheduled",
                    cancelled: false,
                    durationMs: 0
                };
                ortPrewarmPromise = (async () => {
                    await waitForIdleSlot();
                    if (!ortPrewarmState || ortPrewarmState.session !== session || ortPrewarmState.cancelled) return;
                    ortPrewarmState.status = "running";
                    const feeds = createDummyInpaintFeeds();
                    const startedAt = performance.now();
                    try {
                        await withTimeout(session.run(feeds), PREWARM_TIMEOUT_MS, "模型预热");
                        if (ortPrewarmState?.session === session) {
                            ortPrewarmState.status = "done";
                            ortPrewarmState.durationMs = Math.round(performance.now() - startedAt);
                        }
                    } catch (error) {
                        if (ortPrewarmState?.session === session) ortPrewarmState.status = "failed";
                        logWarn("模型预热失败,继续按需运行", {
                            backend: ortSessionMeta?.backend || "unknown",
                            message: simplifyError(error)
                        });
                    }
                })();
            }
            async function ensureSessionReadyForInference(session) {
                if (!ortPrewarmState || ortPrewarmState.session !== session) return;
                if (ortPrewarmState.status === "scheduled") {
                    ortPrewarmState.cancelled = true;
                    return;
                }
                if (ortPrewarmState.status === "running" && ortPrewarmPromise) await ortPrewarmPromise.catch(() => {});
            }
            async function removeWatermarkWithRetry(img, controls, session) {
                const job = await createRemovalJob(img, controls);
                try {
                    await ensureSessionReadyForInference(session);
                    return await executeRemovalJob(job, session);
                } finally {
                    cleanupRemovalJob(job);
                }
            }
            function logInfo(message, details) {
                if (details && Object.keys(details).length > 0) return;
            }
            function logWarn(message, details) {
                if (details && Object.keys(details).length > 0) {
                    console.warn(`${LOG_PREFIX} ${message}`, details);
                    return;
                }
                console.warn(`${LOG_PREFIX} ${message}`);
            }
            function logError(message, error, details) {
                if (details && Object.keys(details).length > 0) {
                    console.error(`${LOG_PREFIX} ${message}`, details, error);
                    return;
                }
                console.error(`${LOG_PREFIX} ${message}`, error);
            }
            function simplifyError(error) {
                return error?.message || String(error);
            }
            async function getOrtSession() {
                if (ortSession) return ortSession;
                if (ortSessionPromise) return await ortSessionPromise;
                ortSessionPromise = (async () => {
                    await sleep(0);
                    const modelMeta = await getModel();
                    if (!modelMeta?.buffer) throw new Error("模型加载失败");
                    await sleep(0);
                    configureOrtEnv();
                    await sleep(0);
                    const {session: session, sessionCreateMs: sessionCreateMs, backend: backend} = await createOrtSession(modelMeta.buffer);
                    ortSession = session;
                    ortSessionMeta = {
                        backend: backend,
                        modelSource: modelMeta.fromCache ? "cache" : "network",
                        modelDownloadMs: modelMeta.downloadMs,
                        sessionCreateMs: sessionCreateMs,
                        totalInitMs: Math.round(performance.now() - geminiInitAt),
                        wasmThreads: runtimeCaps.supportsWasmThreads ? `auto<=${MAX_WASM_THREADS}` : "1"
                    };
                    logInfo("模型已就绪", {
                        backend: ortSessionMeta.backend,
                        ort: ORT_VERSION,
                        model: ortSessionMeta.modelSource === "cache" ? "cache" : `${ortSessionMeta.modelDownloadMs}ms`,
                        session: `${ortSessionMeta.sessionCreateMs}ms`,
                        total: `${ortSessionMeta.totalInitMs}ms`,
                        threads: ortSessionMeta.wasmThreads,
                        shim: webgpuCompatState.enabled ? `adapterInfo(${webgpuCompatState.hits})` : "none"
                    });
                    scheduleSessionPrewarm(session);
                    return session;
                })().catch(error => {
                    resetOrtSession();
                    logError("模型加载失败", error, {
                        message: simplifyError(error)
                    });
                    throw error;
                });
                return await ortSessionPromise;
            }
            function sleep(ms) {
                return new Promise(resolve => setTimeout(resolve, ms));
            }
            async function getModel() {
                const db = await openDb();
                const tx = db.transaction(STORE_NAME, "readonly");
                const cached = await new Promise(r => {
                    const req = tx.objectStore(STORE_NAME).get(MODEL_KEY);
                    req.onsuccess = () => r(req.result);
                });
                if (cached) return {
                    buffer: cached,
                    fromCache: true,
                    downloadMs: 0
                };
                if (typeof GM_xmlhttpRequest !== "function") throw new Error("GM_xmlhttpRequest 未授权");
                const downloadStartAt = performance.now();
                const {buffer: buffer} = await gmGetArrayBuffer(MODEL_URL, MODEL_DOWNLOAD_TIMEOUT_MS, "模型下载");
                const data = buffer;
                const wTx = db.transaction(STORE_NAME, "readwrite");
                wTx.objectStore(STORE_NAME).put(data, MODEL_KEY);
                return {
                    buffer: data,
                    fromCache: false,
                    downloadMs: Math.round(performance.now() - downloadStartAt)
                };
            }
            function observeImages(getSession) {
                document.addEventListener("mouseover", e => {
                    const target = e.target;
                    if (target.tagName === "IMG" && target.width > 200 && !target.dataset.wmHandled) showButton(target, getSession);
                });
            }
            function showButton(img, getSession) {
                const controls = findControlsContainer(img);
                if (!controls) return;
                if (controls.querySelector(".wm-btn")) return;
                const btn = document.createElement("button");
                btn.className = "wm-btn";
                btn.innerText = "去水印下载";
                Object.assign(btn.style, {
                    background: "#4285f4",
                    color: "#fff",
                    border: "none",
                    padding: "5px 10px",
                    borderRadius: "4px",
                    cursor: "pointer",
                    fontSize: "12px",
                    alignSelf: "flex-start",
                    marginBottom: "6px",
                    display: "none"
                });
                controls.insertBefore(btn, controls.firstChild);
                img.dataset.wmHandled = "1";
                bindHoverVisibility(img, btn);
                btn.onclick = async e => {
                    if (btn.disabled) return;
                    e.preventDefault();
                    e.stopPropagation();
                    btn.disabled = true;
                    btn.style.opacity = "0.7";
                    const runId = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
                    btn.dataset.wmRunId = runId;
                    let resetDelay = 1200;
                    try {
                        btn.innerText = ortSession ? "处理中..." : "加载模型...";
                        const session = await getSession();
                        btn.innerText = "处理中...";
                        const metrics = await removeWatermarkWithRetry(img, controls, session);
                        btn.innerText = "已保存";
                        logInfo("处理完成", {
                            backend: ortSessionMeta?.backend || "wasm",
                            source: metrics.source,
                            acquire: `${metrics.acquireMs}ms`,
                            infer: `${metrics.inferMs}ms`,
                            total: `${metrics.totalMs}ms`
                        });
                    } catch (error) {
                        resetDelay = 3e3;
                        btn.innerText = getUserFacingErrorMessage(error);
                        logError("去水印失败", error, {
                            backend: ortSessionMeta?.backend || "unknown",
                            message: simplifyError(error)
                        });
                    } finally {
                        btn.disabled = false;
                        btn.style.opacity = "";
                        setTimeout(() => {
                            if (btn.dataset.wmRunId === runId && !btn.disabled) btn.innerText = "去水印下载";
                        }, resetDelay);
                    }
                };
            }
            function findControlsContainer(imgElement) {
                let node = imgElement;
                for (let i = 0; i < 10 && node; i++) {
                    const controls = node.querySelector?.(".generated-image-controls");
                    if (controls) return controls;
                    node = node.parentElement;
                }
                return null;
            }
            function bindHoverVisibility(imgElement, btn) {
                const container = imgElement.closest(".image-container");
                if (!container) return;
                container.addEventListener("mouseenter", () => {
                    btn.style.display = "inline-flex";
                });
                container.addEventListener("mouseleave", () => {
                    btn.style.display = "none";
                });
            }
            function findCopyButton(controls) {
                const copyBtn = controls.querySelector("copy-button");
                if (!copyBtn) return null;
                return copyBtn.querySelector("button") || copyBtn;
            }
            function triggerCopyAndWait(controls) {
                const copyBtn = findCopyButton(controls);
                if (!copyBtn) throw new Error("未找到复制按钮");
                const startAt = capturedImageBlobAt;
                suppressClipboardToastOnce();
                const evt = new unsafeWindow.MouseEvent("click", {
                    bubbles: true,
                    cancelable: true,
                    view: unsafeWindow
                });
                copyBtn.dispatchEvent(evt);
                return waitForCapturedImageBlob(startAt, COPY_TIMEOUT_MS);
            }
            function suppressClipboardToastOnce() {
                suppressClipboardToastUntil = Date.now() + COPY_TOAST_SUPPRESS_MS;
            }
            function installClipboardToastSuppressor() {
                if (rootWin.__wmClipboardToastSuppressorInstalled) return;
                rootWin.__wmClipboardToastSuppressorInstalled = true;
                const observer = new MutationObserver(mutations => {
                    if (Date.now() > suppressClipboardToastUntil) return;
                    for (const mutation of mutations) for (const node of mutation.addedNodes) {
                        const toastNode = findClipboardToastNode(node);
                        if (toastNode) dismissClipboardToast(toastNode);
                    }
                });
                observer.observe(document.body || document.documentElement, {
                    childList: true,
                    subtree: true
                });
            }
            function findClipboardToastNode(node) {
                if (!(node instanceof Element)) return null;
                if (isClipboardToastNode(node)) return node;
                const candidates = node.querySelectorAll?.("mat-snack-bar-container, bard-simple-snack-bar, simple-snack-bar, .mdc-snackbar, .mdc-snackbar__surface, .mat-mdc-snack-bar-label, .mdc-snackbar__label");
                if (!candidates) return null;
                for (const candidate of candidates) if (isClipboardToastNode(candidate)) return candidate;
                return null;
            }
            function isClipboardToastNode(node) {
                if (!(node instanceof Element)) return false;
                const text = getNormalizedText(node);
                if (text !== "已复制到剪贴板" && text !== "Copied to clipboard") return false;
                return node.matches("mat-snack-bar-container, bard-simple-snack-bar, simple-snack-bar, .mdc-snackbar, .mdc-snackbar__surface, .mat-mdc-snack-bar-label, .mdc-snackbar__label");
            }
            function dismissClipboardToast(node) {
                const wrapper = node.closest(".cdk-global-overlay-wrapper") || node.closest(".cdk-overlay-pane") || node.closest("mat-snack-bar-container");
                if (wrapper) wrapper.remove();
                const announcers = document.querySelectorAll(".cdk-live-announcer-element");
                for (const announcer of announcers) {
                    const text = getNormalizedText(announcer);
                    if (text === "已复制到剪贴板" || text === "Copied to clipboard") announcer.textContent = "";
                }
            }
            function getNormalizedText(node) {
                return String(node?.innerText || node?.textContent || "").replace(/\s+/g, " ").trim();
            }
            function waitForCapturedImageBlob(startAt, timeoutMs) {
                const deadline = Date.now() + timeoutMs;
                return new Promise((resolve, reject) => {
                    const tick = () => {
                        const blob = capturedImageBlobAt > startAt ? getRecentCapturedImageBlob() : null;
                        if (blob) return resolve(blob);
                        if (Date.now() >= deadline) return reject(new Error("图片获取超时"));
                        setTimeout(tick, 50);
                    };
                    tick();
                });
            }
            function rememberCapturedImageBlob(blob) {
                if (!blob) return;
                capturedImageBlob = blob;
                capturedImageBlobAt = Date.now();
            }
            function cacheImageBlobForSource(source, blob) {
                if (!source || !blob) return;
                imageBlobCache.set(source, {
                    blob: blob,
                    at: Date.now()
                });
                if (imageBlobCache.size <= 12) return;
                const oldestKey = imageBlobCache.keys().next().value;
                if (oldestKey) imageBlobCache.delete(oldestKey);
            }
            function getCachedImageBlobForSource(source) {
                const cached = imageBlobCache.get(source);
                if (!cached) return null;
                if (Date.now() - cached.at > IMAGE_BLOB_CACHE_TTL_MS) {
                    imageBlobCache.delete(source);
                    return null;
                }
                return cached.blob;
            }
            function guessImageMimeType(url) {
                try {
                    const pathname = new URL(url, location.href).pathname.toLowerCase();
                    if (pathname.endsWith(".png")) return "image/png";
                    if (pathname.endsWith(".webp")) return "image/webp";
                    if (pathname.endsWith(".gif")) return "image/gif";
                } catch (error) {}
                return "image/jpeg";
            }
            async function fetchCrossOriginImageBlob(source) {
                const {buffer: buffer, contentType: contentType} = await gmGetArrayBuffer(source, IMAGE_FETCH_TIMEOUT_MS, "图片获取");
                return new Blob([ buffer ], {
                    type: contentType || guessImageMimeType(source)
                });
            }
            async function acquireImageBitmap(imgElement, controls) {
                const source = imgElement.currentSrc || imgElement.src;
                if (!source) throw new Error("Image src empty");
                const startedAt = performance.now();
                const resolved = new URL(source, location.href);
                if (resolved.origin === location.origin || resolved.protocol === "data:" || resolved.protocol === "blob:") {
                    const bitmap = await createImageBitmap(await loadImageElement(source));
                    return {
                        bitmap: bitmap,
                        source: source,
                        sourceKind: "dom",
                        acquireMs: Math.round(performance.now() - startedAt)
                    };
                }
                const cachedBlob = getCachedImageBlobForSource(source);
                if (cachedBlob) return {
                    bitmap: await createImageBitmap(cachedBlob),
                    source: source,
                    sourceKind: "cache",
                    acquireMs: Math.round(performance.now() - startedAt)
                };
                try {
                    const blob = await fetchCrossOriginImageBlob(source);
                    rememberCapturedImageBlob(blob);
                    cacheImageBlobForSource(source, blob);
                    return {
                        bitmap: await createImageBitmap(blob),
                        source: source,
                        sourceKind: "fetch",
                        acquireMs: Math.round(performance.now() - startedAt)
                    };
                } catch (fetchError) {
                    const copiedBlob = await triggerCopyAndWait(controls);
                    rememberCapturedImageBlob(copiedBlob);
                    cacheImageBlobForSource(source, copiedBlob);
                    return {
                        bitmap: await createImageBitmap(copiedBlob),
                        source: source,
                        sourceKind: "copy",
                        acquireMs: Math.round(performance.now() - startedAt)
                    };
                }
            }
            function createPatchCanvas(bitmap, region) {
                const patchCanvas = document.createElement("canvas");
                patchCanvas.width = MODEL_SIZE;
                patchCanvas.height = MODEL_SIZE;
                const patchCtx = patchCanvas.getContext("2d", {
                    willReadFrequently: true
                });
                patchCtx.imageSmoothingEnabled = true;
                patchCtx.imageSmoothingQuality = "high";
                patchCtx.drawImage(bitmap, region.cropX, region.cropY, region.cropSize, region.cropSize, 0, 0, MODEL_SIZE, MODEL_SIZE);
                return {
                    patchCanvas: patchCanvas,
                    patchCtx: patchCtx,
                    imageData: patchCtx.getImageData(0, 0, MODEL_SIZE, MODEL_SIZE)
                };
            }
            function createInpaintFeeds(imageData, region) {
                const dims = [ 1, 3, MODEL_SIZE, MODEL_SIZE ];
                const size = MODEL_SIZE * MODEL_SIZE;
                const {data: data} = imageData;
                const floatData = new Float32Array(3 * size);
                for (let i = 0; i < size; i++) {
                    floatData[i] = data[i * 4] / 255;
                    floatData[size + i] = data[i * 4 + 1] / 255;
                    floatData[size * 2 + i] = data[i * 4 + 2] / 255;
                }
                const maskData = new Float32Array(size);
                for (let y = region.maskY0; y < region.maskY1; y++) {
                    const row = y * MODEL_SIZE;
                    for (let x = region.maskX0; x < region.maskX1; x++) maskData[row + x] = 1;
                }
                return {
                    image: new ort.Tensor("float32", floatData, dims),
                    mask: new ort.Tensor("float32", maskData, [ 1, 1, MODEL_SIZE, MODEL_SIZE ])
                };
            }
            async function createRemovalJob(imgElement, controls) {
                const startedAt = performance.now();
                let bitmap;
                try {
                    const sourceAsset = await acquireImageBitmap(imgElement, controls);
                    bitmap = sourceAsset.bitmap;
                    const w = bitmap.width || imgElement.naturalWidth || imgElement.width;
                    const h = bitmap.height || imgElement.naturalHeight || imgElement.height;
                    const region = getWatermarkRegion(w, h);
                    const patch = createPatchCanvas(bitmap, region);
                    return {
                        startedAt: startedAt,
                        bitmap: bitmap,
                        width: w,
                        height: h,
                        region: region,
                        source: sourceAsset.sourceKind,
                        acquireMs: sourceAsset.acquireMs,
                        ...patch,
                        feeds: createInpaintFeeds(patch.imageData, region)
                    };
                } catch (error) {
                    if (bitmap) bitmap.close();
                    throw error;
                }
            }
            async function runInpaintInference(session, feeds) {
                let inferenceOverlay = null;
                let inferenceStarted = false;
                try {
                    inferenceOverlay = showInferenceOverlay();
                    startInferenceProgress();
                    inferenceStarted = true;
                    await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
                    const startedAt = performance.now();
                    const results = await withTimeout(session.run(feeds), INFERENCE_TIMEOUT_MS, "推理");
                    return {
                        output: pickOutputTensor(results),
                        inferMs: Math.round(performance.now() - startedAt)
                    };
                } finally {
                    if (inferenceStarted) finishInferenceProgress();
                    hideInferenceOverlay(inferenceOverlay);
                }
            }
            function renderOutputToPatchCanvas(patchCtx, output) {
                const outData = output?.data;
                if (!outData) throw new Error("模型输出为空");
                const size = MODEL_SIZE * MODEL_SIZE;
                const {scale: scale, offset: offset} = getOutputScale(outData);
                const outImgData = patchCtx.createImageData(MODEL_SIZE, MODEL_SIZE);
                const layout = inferOutputLayout(output);
                if (layout === "NHWC") for (let i = 0; i < size; i++) {
                    const base = i * 3;
                    outImgData.data[i * 4] = outData[base] * scale + offset;
                    outImgData.data[i * 4 + 1] = outData[base + 1] * scale + offset;
                    outImgData.data[i * 4 + 2] = outData[base + 2] * scale + offset;
                    outImgData.data[i * 4 + 3] = 255;
                } else for (let i = 0; i < size; i++) {
                    outImgData.data[i * 4] = outData[i] * scale + offset;
                    outImgData.data[i * 4 + 1] = outData[size + i] * scale + offset;
                    outImgData.data[i * 4 + 2] = outData[size * 2 + i] * scale + offset;
                    outImgData.data[i * 4 + 3] = 255;
                }
                patchCtx.putImageData(outImgData, 0, 0);
            }
            function composeFinalCanvas(job) {
                const finalCanvas = document.createElement("canvas");
                finalCanvas.width = job.width;
                finalCanvas.height = job.height;
                const finalCtx = finalCanvas.getContext("2d");
                finalCtx.imageSmoothingEnabled = true;
                finalCtx.imageSmoothingQuality = "high";
                finalCtx.drawImage(job.bitmap, 0, 0, job.width, job.height);
                finalCtx.save();
                finalCtx.beginPath();
                finalCtx.rect(job.region.outX, job.region.outY, job.region.outW, job.region.outH);
                finalCtx.clip();
                finalCtx.drawImage(job.patchCanvas, 0, 0, MODEL_SIZE, MODEL_SIZE, job.region.cropX, job.region.cropY, job.region.cropSize, job.region.cropSize);
                finalCtx.restore();
                return finalCanvas;
            }
            async function executeRemovalJob(job, session) {
                const {output: output, inferMs: inferMs} = await runInpaintInference(session, job.feeds);
                renderOutputToPatchCanvas(job.patchCtx, output);
                const finalCanvas = composeFinalCanvas(job);
                await downloadCanvasImage(finalCanvas, `gemini_inpaint_${Date.now()}.png`);
                return {
                    source: job.source,
                    acquireMs: job.acquireMs,
                    inferMs: inferMs,
                    totalMs: Math.round(performance.now() - job.startedAt)
                };
            }
            function cleanupRemovalJob(job) {
                if (job?.bitmap) job.bitmap.close();
            }
            function getWatermarkRegion(w, h) {
                const base = Math.min(w, h);
                const k = base / WM_BASE;
                const boxW = Math.max(32, Math.round(WM_BOX_W_AT_BASE * k));
                const boxH = Math.max(32, Math.round(WM_BOX_H_AT_BASE * k));
                const rightMargin = Math.max(0, Math.round(WM_BOX_RIGHT_MARGIN_AT_BASE * k));
                const bottomMargin = Math.max(0, Math.round(WM_BOX_BOTTOM_MARGIN_AT_BASE * k));
                const pad = Math.max(0, Math.round(WM_BOX_PAD_AT_BASE * k));
                const boxX = w - rightMargin - boxW;
                const boxY = h - bottomMargin - boxH;
                let outX = boxX - pad;
                let outY = boxY - pad;
                let outW = boxW + pad * 2;
                let outH = boxH + pad * 2;
                if (outX < 0) {
                    outW += outX;
                    outX = 0;
                }
                if (outY < 0) {
                    outH += outY;
                    outY = 0;
                }
                outW = Math.min(outW, w - outX);
                outH = Math.min(outH, h - outY);
                const cropMin = Math.max(boxW, boxH) + pad * 2;
                let cropSize = Math.round(WM_CROP_SIZE_AT_BASE * k);
                cropSize = Math.max(cropSize, cropMin);
                cropSize = Math.min(cropSize, w, h);
                let cropX = w - cropSize;
                let cropY = h - cropSize;
                cropX = Math.min(cropX, outX);
                cropY = Math.min(cropY, outY);
                cropX = Math.max(0, Math.min(cropX, w - cropSize));
                cropY = Math.max(0, Math.min(cropY, h - cropSize));
                const sx = MODEL_SIZE / cropSize;
                const relX0 = outX - cropX;
                const relY0 = outY - cropY;
                const relX1 = relX0 + outW;
                const relY1 = relY0 + outH;
                const maskX0 = clampInt(Math.floor(relX0 * sx), 0, MODEL_SIZE);
                const maskY0 = clampInt(Math.floor(relY0 * sx), 0, MODEL_SIZE);
                const maskX1 = clampInt(Math.ceil(relX1 * sx), 0, MODEL_SIZE);
                const maskY1 = clampInt(Math.ceil(relY1 * sx), 0, MODEL_SIZE);
                return {
                    cropX: cropX,
                    cropY: cropY,
                    cropSize: cropSize,
                    outX: outX,
                    outY: outY,
                    outW: outW,
                    outH: outH,
                    maskX0: maskX0,
                    maskY0: maskY0,
                    maskX1: maskX1,
                    maskY1: maskY1
                };
            }
            function clampInt(v, min, max) {
                if (v < min) return min;
                if (v > max) return max;
                return v | 0;
            }
            function pickOutputTensor(results) {
                if (results?.output && isImageTensor(results.output)) return results.output;
                const tensors = Object.values(results || {}).filter(Boolean);
                const imageTensor = tensors.find(isImageTensor);
                return imageTensor || tensors[0];
            }
            function isImageTensor(t) {
                const d = t?.dims;
                if (!Array.isArray(d) || d.length !== 4) return false;
                const isNchw = d[1] === 3 && d[2] === 512 && d[3] === 512;
                const isNhwc = d[3] === 3 && d[1] === 512 && d[2] === 512;
                return isNchw || isNhwc;
            }
            function inferOutputLayout(t) {
                const d = t?.dims;
                if (Array.isArray(d) && d.length === 4 && d[3] === 3) return "NHWC";
                return "NCHW";
            }
            function getOutputScale(outData) {
                let min = 1 / 0;
                let max = -1 / 0;
                const step = Math.max(1, Math.floor(outData.length / 2e4));
                for (let i = 0; i < outData.length; i += step) {
                    const v = outData[i];
                    if (v < min) min = v;
                    if (v > max) max = v;
                }
                if (!Number.isFinite(min)) min = 0;
                if (!Number.isFinite(max)) max = 0;
                if (min >= -1.2 && max <= 1.2) {
                    if (min < 0) return {
                        scale: 127.5,
                        offset: 127.5
                    };
                    return {
                        scale: 255,
                        offset: 0
                    };
                }
                return {
                    scale: 1,
                    offset: 0
                };
            }
            function downloadCanvasImage(canvas, filename) {
                return new Promise((resolve, reject) => {
                    canvas.toBlob(blob => {
                        if (!blob) return reject(new Error("导出失败"));
                        downloadBlob(blob, filename);
                        resolve();
                    }, "image/png");
                });
            }
            function downloadBlob(blob, filename) {
                ignoreCreateObjectUrlCapture = true;
                const url = URL.createObjectURL(blob);
                ignoreCreateObjectUrlCapture = false;
                downloadUrl(url, filename);
                setTimeout(() => URL.revokeObjectURL(url), 6e4);
            }
            function downloadUrl(url, filename) {
                const a = document.createElement("a");
                a.href = url;
                a.download = filename;
                a.style.display = "none";
                document.body.appendChild(a);
                a.click();
                a.remove();
            }
            function loadImageElement(url) {
                return new Promise((resolve, reject) => {
                    const img = new Image;
                    img.onload = () => resolve(img);
                    img.onerror = () => reject(new Error("Image load failed"));
                    img.src = url;
                });
            }
            function gmGetArrayBuffer(url, timeoutMs, timeoutLabel = "网络请求") {
                return new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: url,
                        responseType: "arraybuffer",
                        timeout: timeoutMs,
                        onload: res => {
                            if (res.status === 200) {
                                const contentType = /content-type:\s*([^\n;]+)/i.exec(res.responseHeaders || "")?.[1]?.trim() || "";
                                resolve({
                                    buffer: res.response,
                                    contentType: contentType
                                });
                            } else reject(new Error("Request failed: " + res.status));
                        },
                        onerror: () => reject(new Error("Request failed")),
                        ontimeout: () => reject(new Error(`${timeoutLabel}超时`))
                    });
                });
            }
            function getRecentCapturedImageBlob() {
                if (!capturedImageBlob) return null;
                if (Date.now() - capturedImageBlobAt > 6e4) return null;
                return capturedImageBlob;
            }
            function hookImageBlobCapture() {
                if (unsafeWindow.__wmBlobCaptureHooked) return;
                unsafeWindow.__wmBlobCaptureHooked = true;
                const urlApi = unsafeWindow.URL;
                const rawCreate = urlApi.createObjectURL.bind(urlApi);
                urlApi.createObjectURL = obj => {
                    const url = rawCreate(obj);
                    if (ignoreCreateObjectUrlCapture) return url;
                    const isBlob = obj instanceof unsafeWindow.Blob;
                    const type = isBlob ? obj.type : "";
                    if (isBlob && type.startsWith("image/")) rememberCapturedImageBlob(obj);
                    return url;
                };
                const clip = unsafeWindow.navigator.clipboard;
                if (!clip.__wmWriteHooked) {
                    clip.__wmWriteHooked = true;
                    const rawWrite = clip.write.bind(clip);
                    clip.write = async items => {
                        for (const item of items) {
                            const types = item.types;
                            for (const type of types) {
                                if (!type.startsWith("image/")) continue;
                                const blob = await item.getType(type);
                                rememberCapturedImageBlob(blob);
                                break;
                            }
                        }
                        return rawWrite(items);
                    };
                }
            }
        }
})();