// ==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/onnxruntime-web@1.24.3/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 // @downloadURL https://update.greasyfork.icu/scripts/559640/Google%20banana%20%E5%8E%BB%E9%99%A4%E5%8F%B3%E4%B8%8B%E8%A7%92%E6%B0%B4%E5%8D%B0.user.js // @updateURL https://update.greasyfork.icu/scripts/559640/Google%20banana%20%E5%8E%BB%E9%99%A4%E5%8F%B3%E4%B8%8B%E8%A7%92%E6%B0%B4%E5%8D%B0.meta.js // ==/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); }; } } } })();