import html import queue import threading import uuid from dataclasses import dataclass from functools import wraps from typing import Any, Callable import gradio as gr from shared import magic_mask MAGIC_WAND_LABEL = "\U0001FA84" _ABORT_EVENTS: dict[str, threading.Event] = {} _ORIGINAL_IMAGE_EDITOR = None class MagicMaskAbort(Exception): pass def magic_mask_button_updates(image_mode, video_prompt_type): mask_visible = "V" in video_prompt_type and "A" in video_prompt_type and "U" not in video_prompt_type image_outputs = image_mode > 0 return gr.update(visible=mask_visible and image_outputs), gr.update(visible=mask_visible and not image_outputs) def _status_html(message, tone="info"): if not message: return "" tone_class = "is-error" if tone == "error" else "" return f"
{html.escape(str(message))}
" def _progress_html(message, percent=0): if not message: return "" percent = max(0, min(100, int(percent))) return ( "
" f"
{html.escape(str(message))}
" "
" f"
" "
" ) def _abort_event(token): token = str(token or "") if len(token) == 0: token = "default" if token not in _ABORT_EVENTS: _ABORT_EVENTS[token] = threading.Event() return _ABORT_EVENTS[token] def _open_panel(): return gr.update(visible=True), "", "", gr.update(visible=True, interactive=True), gr.update(visible=False, interactive=False), None, None def _close_panel(): return ( gr.update(visible=False), "", "", gr.update(visible=False, interactive=False), None, None, ) def _abort_magic_mask(abort_token): _abort_event(abort_token).set() return _status_html("Aborting Magic Mask..."), gr.update(visible=True, interactive=False) def _exit_button_running(): return gr.update(visible=False, interactive=False) def _exit_button_idle(): return gr.update(visible=True, interactive=True) def _abort_button_running(): return gr.update(visible=True, interactive=True) def _abort_button_idle(): return gr.update(visible=False, interactive=False) def _raise_if_aborted(abort_event): if abort_event.is_set(): raise MagicMaskAbort() def _image_source(image_mask_guide, image_guide): if isinstance(image_mask_guide, dict) and image_mask_guide.get("background") is not None: return image_mask_guide["background"] return image_guide def _keywords_processed_html(processed, total): return _status_html(f"Masks generated: {processed}/{total}") def _mask_progress_html(keyword, done, total): percent = int(done * 100 / max(int(total), 1)) return _progress_html(f'Generating Mask "{keyword}"', percent) def _current_keyword_progress(keywords, done, total): keyword_count = max(len(keywords), 1) total_steps = max(float(total), 1.0) done = max(0.0, min(float(done), total_steps)) current_index = min(keyword_count - 1, int(done * keyword_count / total_steps)) keyword_steps = total_steps / keyword_count keyword_done = max(0.0, min(keyword_steps, done - current_index * keyword_steps)) return keywords[current_index], keyword_done, keyword_steps def _run_keyword_mask(video, keywords, abort_event): progress_events = queue.Queue() def progress_callback(done, total): if abort_event.is_set(): raise MagicMaskAbort() progress_events.put(("progress", int(done), int(total))) def worker(): try: if abort_event.is_set(): raise MagicMaskAbort() progress_events.put(("done", magic_mask.generate_keyword_masks(video, keywords, progress_callback=progress_callback))) except MagicMaskAbort as exc: progress_events.put(("abort", exc)) except Exception as exc: progress_events.put(("error", exc)) thread = threading.Thread(target=worker, daemon=True) thread.start() pending_event = None while True: event = pending_event or progress_events.get() pending_event = None if event[0] == "progress": latest_event = event while True: try: next_event = progress_events.get_nowait() except queue.Empty: break if next_event[0] == "progress": latest_event = next_event else: pending_event = next_event break event = latest_event yield event[1], event[2] elif event[0] == "done": thread.join() return event[1] elif event[0] == "abort": thread.join() raise event[1] else: thread.join() raise event[1] def _generate_magic_mask( state, keywords_text, negative_mask, image_mode, video_guide, image_mask_guide, image_guide, abort_token, *, download_assets: Callable[[dict[str, Any]], Any], acquire_gpu: Callable[[Any, str, str], Any], release_gpu: Callable[[Any, str], Any], get_model_settings: Callable[[Any], dict], ): source_image = None if image_mode > 0: source_image = _image_source(image_mask_guide, image_guide) if source_image is None: yield gr.update(), gr.update(), gr.update(), _status_html("Magic Mask needs a control image.", "error"), gr.update(visible=True), "", _exit_button_idle(), _abort_button_idle(), None, None return elif video_guide is None: yield gr.update(), gr.update(), gr.update(), _status_html("Magic Mask needs a control video.", "error"), gr.update(visible=True), "", _exit_button_idle(), _abort_button_idle(), None, None return keywords = magic_mask.parse_keywords(keywords_text) if len(keywords) == 0: yield gr.update(), gr.update(), gr.update(), _status_html("Enter at least one keyword.", "error"), gr.update(visible=True), "", _exit_button_idle(), _abort_button_idle(), None, None return keywords_label = ", ".join(keywords) abort_event = _abort_event(abort_token) abort_event.clear() acquired = False try: yield gr.update(), gr.update(), gr.update(), _status_html("Initializing Magic Mask"), gr.update(visible=True), _progress_html("Preparing files", 0), _exit_button_running(), _abort_button_running(), None, None download_assets(magic_mask.query_download_def()) _raise_if_aborted(abort_event) yield gr.update(), gr.update(), gr.update(), _status_html("Initializing Magic Mask"), gr.update(visible=True), _progress_html("Initializing", 0), _exit_button_running(), _abort_button_running(), None, None acquire_gpu(state, magic_mask.PROCESS_ID, magic_mask.PROCESS_NAME) _raise_if_aborted(abort_event) acquired = True ui_settings = get_model_settings(state) if image_mode > 0: _raise_if_aborted(abort_event) background, video = magic_mask.prepare_image_mask_input(source_image) total = len(keywords) mask_generator = _run_keyword_mask(video, keywords, abort_event) progress_started = False try: while True: done, frame_total = next(mask_generator) if done <= 0: continue progress_started = True processed = min(total, int(done * total / max(frame_total, 1))) current_keyword, keyword_done, keyword_total = _current_keyword_progress(keywords, done, frame_total) yield gr.update(), gr.update(), gr.update(), _keywords_processed_html(processed, total), gr.update(visible=True), _mask_progress_html(current_keyword, keyword_done, keyword_total), _exit_button_running(), _abort_button_running(), None, None except StopIteration as stop: merged_mask = stop.value[0] if not progress_started: yield gr.update(), gr.update(), gr.update(), _keywords_processed_html(0, total), gr.update(visible=True), _mask_progress_html(keywords[0], 1, 1), _exit_button_running(), _abort_button_running(), None, None yield gr.update(), gr.update(), gr.update(), _keywords_processed_html(total, total), gr.update(visible=True), _mask_progress_html(keywords[-1], 1, 1), _exit_button_running(), _abort_button_running(), None, None yield gr.update(), gr.update(), gr.update(), _status_html("Saving Image Mask..."), gr.update(visible=True), "", _exit_button_running(), _abort_button_running(), None, None _raise_if_aborted(abort_event) mask_image = magic_mask.mask_to_image(magic_mask.finalize_masks(merged_mask, negative_mask=negative_mask)) image_mask_guide_value = magic_mask.build_image_editor_value(background, mask_image) if isinstance(ui_settings, dict): ui_settings["image_guide"] = background ui_settings["image_mask"] = mask_image gr.Info(f"Magic Mask generated {'a negative ' if negative_mask else 'an '}image mask for: {keywords_label}.") yield gr.update(value=image_mask_guide_value), gr.update(value=mask_image), gr.update(), "", gr.update(visible=False), "", _exit_button_idle(), _abort_button_idle(), None, None return _raise_if_aborted(abort_event) video_path, video, fps = magic_mask.prepare_video_mask_input(video_guide) total = len(keywords) mask_generator = _run_keyword_mask(video, keywords, abort_event) progress_started = False try: while True: done, frame_total = next(mask_generator) if done <= 0: continue progress_started = True processed = min(total, int(done * total / max(frame_total, 1))) current_keyword, keyword_done, keyword_total = _current_keyword_progress(keywords, done, frame_total) yield gr.update(), gr.update(), gr.update(), _keywords_processed_html(processed, total), gr.update(visible=True), _mask_progress_html(current_keyword, keyword_done, keyword_total), _exit_button_running(), _abort_button_running(), None, None except StopIteration as stop: merged_mask = stop.value if not progress_started: yield gr.update(), gr.update(), gr.update(), _keywords_processed_html(0, total), gr.update(visible=True), _mask_progress_html(keywords[0], 1, 1), _exit_button_running(), _abort_button_running(), None, None yield gr.update(), gr.update(), gr.update(), _keywords_processed_html(total, total), gr.update(visible=True), _mask_progress_html(keywords[-1], 1, 1), _exit_button_running(), _abort_button_running(), None, None yield gr.update(), gr.update(), gr.update(), _status_html("Saving Video Mask..."), gr.update(visible=True), "", _exit_button_running(), _abort_button_running(), None, None mask_path = magic_mask.save_mask_video(video_path, magic_mask.finalize_masks(merged_mask, negative_mask=negative_mask), fps, keywords, abort_callback=lambda: _raise_if_aborted(abort_event)) if isinstance(ui_settings, dict): ui_settings["video_mask"] = mask_path gr.Info(f"Magic Mask generated {'a negative ' if negative_mask else 'a '}video mask for: {keywords_label}.") yield gr.update(), gr.update(), gr.update(value=mask_path), "", gr.update(visible=False), "", _exit_button_idle(), _abort_button_idle(), None, None except MagicMaskAbort: yield gr.update(), gr.update(), gr.update(), _status_html("Magic Mask aborted."), gr.update(visible=True), "", _exit_button_idle(), _abort_button_idle(), None, None except Exception as exc: yield gr.update(), gr.update(), gr.update(), _status_html(exc, "error"), gr.update(visible=True), "", _exit_button_idle(), _abort_button_idle(), None, None finally: if acquired: release_gpu(state, magic_mask.PROCESS_ID) @dataclass class MagicMaskUI: trigger: gr.Button | None = None panel: gr.Group | None = None keywords: gr.Textbox | None = None negative_mask: gr.Checkbox | None = None status: gr.HTML | None = None progress_html: gr.HTML | None = None cancel_btn: gr.Button | None = None abort_btn: gr.Button | None = None generate_btn: gr.Button | None = None abort_token: gr.State | None = None pending_image_mask_guide: gr.State | None = None pending_image_mask: gr.State | None = None @staticmethod def hidden_trigger(): return gr.Button(MAGIC_WAND_LABEL, size="sm", min_width=1, visible=False, elem_classes=["wangp-magic-mask-trigger", "wangp-magic-mask-trigger--hidden"]) @staticmethod def button_updates(image_mode, video_prompt_type): return magic_mask_button_updates(image_mode, video_prompt_type) @staticmethod def patch_image_editor(): global _ORIGINAL_IMAGE_EDITOR if _ORIGINAL_IMAGE_EDITOR is not None: return True original = gr.ImageEditor original_init = original.__init__ if getattr(original_init, "__wangp_magic_mask_patch__", False): _ORIGINAL_IMAGE_EDITOR = original_init return True @wraps(original_init) def patched_init(self, *args, **kwargs): original_init(self, *args, **kwargs) self._wangp_magic_mask_patch_enabled = True patched_init.__wangp_magic_mask_patch__ = True _ORIGINAL_IMAGE_EDITOR = original_init original.__init__ = patched_init return True @staticmethod def get_css(): return r""" .wangp-magic-mask-anchor { position: relative; gap: 0 !important; padding: 0 !important; margin: 0 !important; } .wangp-magic-mask-anchor--image-editor { position: relative; } .wangp-magic-mask-anchor > .form, .wangp-magic-mask-anchor > .styler { gap: 0 !important; padding: 0 !important; margin: 0 !important; } .wangp-magic-mask-anchor:not(:has(> .block:not(.hide):not(.hidden), > button:not(.hide):not(.hidden), > .gr-group:not(.hide):not(.hidden))) { display: none !important; } .wangp-magic-mask-trigger, .wangp-magic-mask-trigger button { width: 34px !important; min-width: 34px !important; max-width: 34px !important; height: 34px; min-height: 34px; padding: 0 !important; border: 1px solid rgba(17, 84, 118, 0.14); border-radius: 12px; background: linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, rgba(236, 244, 249, 0.99) 100%); color: #155574; box-shadow: 0 10px 18px rgba(11, 44, 63, 0.08); font-weight: 700; line-height: 1; } .wangp-magic-mask-trigger--overlay { position: absolute !important; top: 28px; right: 8px; z-index: 35; } .wangp-magic-mask-trigger--editor { display: none !important; } .wangp-magic-mask-toolbar-button { display: flex !important; align-items: center !important; justify-content: center !important; flex: 0 0 auto !important; margin: var(--spacing-xxs, 2px) !important; width: 28px !important; min-width: 28px !important; max-width: 28px !important; height: 28px !important; min-height: 28px !important; padding: 0 !important; border: 1px solid transparent !important; border-radius: var(--radius-xs, 4px) !important; background: transparent !important; color: var(--block-label-text-color) !important; box-shadow: none !important; font-size: 16px !important; line-height: 1 !important; } .wangp-magic-mask-toolbar-button:hover { cursor: pointer !important; background: var(--background-fill-secondary) !important; color: var(--color-accent) !important; transform: none !important; } .wangp-magic-mask-toolbar-button.wangp-magic-mask-unavailable { cursor: not-allowed !important; opacity: 0.52; filter: grayscale(1); } .wangp-magic-mask-toolbar-button.wangp-magic-mask-unavailable:hover { cursor: not-allowed !important; color: var(--body-text-color-subdued) !important; } .wangp-magic-mask-toolbar-button[hidden] { display: none !important; } .wangp-magic-mask-synthetic-toolbar { position: absolute !important; top: var(--block-label-margin, 8px) !important; right: var(--block-label-margin, 8px) !important; z-index: var(--layer-3, 1000) !important; } .wangp-magic-mask-trigger:hover, .wangp-magic-mask-trigger button:hover { transform: translateY(-1px); box-shadow: 0 14px 24px rgba(11, 44, 63, 0.12); } .wangp-magic-mask-trigger:hover::after { content: "Magic Mask"; position: absolute; top: 40px; right: 0; width: max-content; max-width: 160px; padding: 5px 7px; border-radius: 4px; background: rgba(0, 0, 0, 0.82); color: #ffffff; font-size: calc(12px * var(--wangp-ui-scale)); font-weight: 600; line-height: 1.2; pointer-events: none; } .wangp-magic-mask-panel.hide { display: none !important; } .wangp-magic-mask-panel:not(.hide) { position: absolute !important; inset: 0; z-index: 40; display: flex !important; align-items: stretch; justify-content: stretch; margin: 0 !important; padding: 0 !important; border: 0 !important; background: #ffffff !important; box-shadow: none !important; overflow: hidden !important; pointer-events: auto; box-sizing: border-box; } .wangp-magic-mask-anchor--image-editor .wangp-magic-mask-panel:not(.hide) { padding-left: 20px !important; padding-right: 20px !important; } .wangp-magic-mask-panel:not(.hide), .wangp-magic-mask-panel:not(.hide) *, .wangp-magic-mask-card, .wangp-magic-mask-card * { box-sizing: border-box; scrollbar-width: none; } .wangp-magic-mask-panel:not(.hide)::-webkit-scrollbar, .wangp-magic-mask-panel:not(.hide) *::-webkit-scrollbar { width: 0 !important; height: 0 !important; display: none !important; } .wangp-magic-mask-panel > .form, .wangp-magic-mask-panel > .styler { width: 100% !important; height: 100% !important; display: flex !important; align-items: stretch !important; justify-content: stretch !important; padding: 0 !important; border: 0 !important; background: transparent !important; box-shadow: none !important; overflow: hidden !important; } .wangp-magic-mask-card { display: flex !important; flex-direction: column !important; width: 100% !important; min-width: 0 !important; height: 100% !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; gap: 0 !important; border: 0 !important; border-radius: 0 !important; background: #ffffff !important; box-shadow: none !important; overflow: hidden !important; } .wangp-magic-mask-card > .form { display: flex !important; flex: 1 1 auto !important; flex-direction: column !important; height: 100% !important; min-height: 0 !important; padding: 0 !important; border: 0 !important; background: transparent !important; box-shadow: none !important; overflow: hidden !important; } .wangp-magic-mask-card .block, .wangp-magic-mask-card .html-container, .wangp-magic-mask-card .prose { max-width: 100% !important; overflow: hidden !important; } .wangp-magic-mask-titlebar { padding: 10px 16px 9px; background: linear-gradient(180deg, rgba(16, 86, 121, 0.98) 0%, rgba(10, 59, 84, 0.98) 100%); color: #f3fbff; } .wangp-magic-mask-heading { font-size: calc(0.95rem * var(--wangp-ui-scale)); font-weight: 800; letter-spacing: 0; color: #f3fbff !important; } .wangp-magic-mask-body { flex: 0 0 auto !important; padding: 14px 18px 0; overflow: hidden !important; } .wangp-magic-mask-body .block, .wangp-magic-mask-body .form, .wangp-magic-mask-body .wrap { margin: 0 !important; overflow: hidden !important; } .wangp-magic-mask-intro { margin: 0 0 12px; color: #164f70; font-size: calc(0.88rem * var(--wangp-ui-scale)); line-height: 1.45; } .wangp-magic-mask-keyword-row { align-items: center; } .wangp-magic-mask-keyword-row > .form { align-items: center; } .wangp-magic-mask-keywords textarea { min-height: 38px !important; height: 38px !important; overflow-y: hidden !important; resize: none !important; } .wangp-magic-mask-negative { flex: 0 0 150px !important; min-width: 150px !important; padding: 0 !important; border: 0 !important; } .wangp-magic-mask-negative, .wangp-magic-mask-negative > .form, .wangp-magic-mask-negative > .styler, .wangp-magic-mask-negative .block, .wangp-magic-mask-negative .wrap, .wangp-magic-mask-negative .checkbox-wrap, .wangp-magic-mask-negative-checkbox, .wangp-magic-mask-negative-checkbox label, .wangp-magic-mask-negative label { background: transparent !important; background-color: transparent !important; box-shadow: none !important; border-color: transparent !important; padding: 0 !important; } .wangp-magic-mask-negative label { white-space: nowrap !important; } .wangp-magic-mask-message { flex: 0 0 auto !important; margin: 12px 18px 0; color: #164f70; font-size: calc(0.9rem * var(--wangp-ui-scale)); line-height: 1.5; font-weight: 600; } .wangp-magic-mask-message.is-error { color: #b33434; } .wangp-magic-mask-progress { flex: 0 0 auto !important; margin: 12px 18px 0; } .wangp-magic-mask-progress-label { margin-bottom: 6px; color: #164f70; font-size: calc(0.82rem * var(--wangp-ui-scale)); font-weight: 700; } .wangp-magic-mask-progress-track { width: 100%; height: 8px; overflow: hidden; border-radius: 999px; background: rgba(19, 91, 126, 0.14); } .wangp-magic-mask-progress-bar { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #2d89b7 0%, #56b18e 100%); transition: width 0.22s ease; } .wangp-magic-mask-spacer { flex: 1 1 auto !important; min-height: 0 !important; height: auto !important; margin: 0 !important; padding: 0 !important; border: 0 !important; overflow: hidden !important; } .wangp-magic-mask-actions { flex: 0 0 auto !important; justify-content: flex-end; align-items: flex-end; gap: 10px; margin-top: auto !important; padding: 18px; overflow: hidden !important; } .wangp-magic-mask-actions > .form { justify-content: flex-end !important; align-items: flex-end !important; overflow: hidden !important; } .wangp-magic-mask-btn, .wangp-magic-mask-btn button { min-width: 92px; height: 40px; min-height: 40px; border-radius: 14px; border: 1px solid rgba(17, 84, 118, 0.14); background: linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, rgba(237, 245, 250, 0.99) 100%); color: #155574; box-shadow: 0 10px 18px rgba(11, 44, 63, 0.08); font-weight: 700; } .wangp-magic-mask-btn--primary, .wangp-magic-mask-btn--primary button { color: #f4fbff; border-color: rgba(10, 59, 84, 0.12); background: linear-gradient(180deg, rgba(16, 86, 121, 0.98) 0%, rgba(10, 59, 84, 0.98) 100%); } .wangp-magic-mask-btn--danger, .wangp-magic-mask-btn--danger button { color: #ffffff; border-color: rgba(142, 45, 45, 0.16); background: linear-gradient(180deg, rgba(188, 67, 67, 0.98) 0%, rgba(132, 41, 41, 0.98) 100%); } .wangp-magic-mask-btn:disabled, .wangp-magic-mask-btn button:disabled { cursor: not-allowed !important; filter: grayscale(0.9); opacity: 0.48; } @media (prefers-color-scheme: dark) { .wangp-magic-mask-trigger, .wangp-magic-mask-trigger button, .wangp-magic-mask-btn, .wangp-magic-mask-btn button { color: #ecf4f9; border-color: rgba(103, 132, 151, 0.22); background: linear-gradient(180deg, rgba(10, 10, 10, 0.99) 0%, rgba(21, 21, 21, 0.99) 100%); box-shadow: 0 10px 18px rgba(0, 0, 0, 0.22); } .wangp-magic-mask-panel:not(.hide), .wangp-magic-mask-card { background: #000000 !important; } .wangp-magic-mask-intro, .wangp-magic-mask-message, .wangp-magic-mask-progress-label { color: #ecf4f9; } .wangp-magic-mask-message.is-error { color: #ff9e9e; } .wangp-magic-mask-progress-track { background: rgba(236, 244, 249, 0.18); } } """ @staticmethod def get_javascript(): return r""" window.__wangpMagicMaskNS = window.__wangpMagicMaskNS || {}; const WMM = window.__wangpMagicMaskNS; WMM.init = WMM.init || false; WMM.observer = WMM.observer || null; WMM.raf = WMM.raf || null; WMM.interval = WMM.interval || null; WMM.isVisible = function (element) { if (!element) return false; const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return rect.width > 2 && rect.height > 2 && style.display !== 'none' && style.visibility !== 'hidden'; }; WMM.findImageEditorToolbar = function (editor) { const synthetic = editor.querySelector('.wangp-magic-mask-synthetic-toolbar'); const topControls = editor.querySelector('.icon-button-wrapper.top-panel:not(.wangp-magic-mask-synthetic-toolbar), .icon-button-wrapper:not(.wangp-magic-mask-synthetic-toolbar)'); if (topControls) { if (synthetic) synthetic.remove(); return topControls; } const imageToolbars = Array.from(editor.querySelectorAll('.toolbar-wrap')); const primaryToolbar = imageToolbars.find((toolbar) => !toolbar.closest('.toolbar-wrap-wrap')) || imageToolbars[0]; if (primaryToolbar) { if (synthetic) synthetic.remove(); return primaryToolbar; } const explicit = editor.querySelector('[role="toolbar"], [class*="toolbar"], [class*="Toolbar"], [class*="tools"], [class*="Tools"]'); if (explicit && !explicit.classList.contains('wangp-magic-mask-synthetic-toolbar')) { if (synthetic) synthetic.remove(); return explicit; } const editorRect = editor.getBoundingClientRect(); let best = null; let bestScore = -Infinity; const candidates = new Map(); editor.querySelectorAll('button').forEach((button) => { let node = button.parentElement; for (let depth = 0; node && node !== editor && depth < 5; depth += 1, node = node.parentElement) { candidates.set(node, node.querySelectorAll('button').length); } }); candidates.forEach((buttonCount, node) => { const rect = node.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0 || rect.top > editorRect.top + 100) return; const topDistance = Math.abs(rect.top - editorRect.top); const rightBias = rect.left > editorRect.left + editorRect.width * 0.45 ? 16 : 0; const heightPenalty = Math.max(0, rect.height - 52) * 2; const score = buttonCount * 24 + rightBias - topDistance - heightPenalty; if (score > bestScore) { best = node; bestScore = score; } }); if (best) { if (synthetic) synthetic.remove(); return best; } let fallback = synthetic; if (!fallback) { fallback = document.createElement('div'); fallback.className = 'icon-button-wrapper top-panel wangp-magic-mask-synthetic-toolbar'; fallback.setAttribute('role', 'toolbar'); fallback.setAttribute('aria-label', 'ImageEditor tools'); if (getComputedStyle(editor).position === 'static') editor.style.position = 'relative'; editor.appendChild(fallback); } return fallback; }; WMM.visibleImageEditors = function () { return Array.from(document.querySelectorAll('#img_editor, [data-testid="image-editor"], .imageeditor, .image-editor')) .map((candidate) => candidate.closest('.block') || candidate.closest('[id^="component-"]') || candidate) .filter((editor, index, editors) => WMM.isVisible(editor) && editors.indexOf(editor) === index); }; WMM.focusImageEditor = function (editor, clickBrush) { if (!editor || !WMM.isVisible(editor)) return false; const focusTarget = editor.querySelector('.pixi-target canvas, canvas, .pixi-target, [data-testid="image"]') || editor; if (!focusTarget.hasAttribute('tabindex')) focusTarget.setAttribute('tabindex', '-1'); try { focusTarget.focus({ preventScroll: true }); } catch (_) { focusTarget.focus(); } if (clickBrush) { const brushButton = editor.querySelector('button[aria-label="Brush"]') || document.querySelector('button[aria-label="Brush"]'); if (brushButton && !brushButton.disabled) brushButton.click(); } return true; }; WMM.focusVisibleImageEditor = function (clickBrush) { const editor = WMM.visibleImageEditors()[0]; return WMM.focusImageEditor(editor, clickBrush); }; WMM.refocusImageEditorAfterMagicMask = function () { const openPanel = Array.from(document.querySelectorAll('.wangp-magic-mask-panel:not(.hide)')).some((panel) => WMM.isVisible(panel)); if (openPanel) return; setTimeout(() => WMM.focusVisibleImageEditor(true), 150); setTimeout(() => WMM.focusVisibleImageEditor(true), 700); }; WMM.installImageEditorFocusPatch = function () { if (WMM.imageEditorFocusPatchInstalled) return; WMM.imageEditorFocusPatchInstalled = true; document.addEventListener('pointerdown', (event) => { const editor = event.target?.closest?.('#img_editor, [data-testid="image-editor"], .imageeditor, .image-editor')?.closest?.('.block') || event.target?.closest?.('#img_editor, [data-testid="image-editor"], .imageeditor, .image-editor'); if (!editor || !WMM.isVisible(editor)) return; if (event.target?.closest?.('button, input, textarea, select, [role="button"]')) return; WMM.focusImageEditor(editor, false); }, true); }; WMM.openMagicMaskPanelInAnchor = function (anchor) { const panel = anchor?.querySelector?.('.wangp-magic-mask-panel'); if (!panel) return false; panel.hidden = false; panel.classList.remove('hide'); panel.style.display = ''; return true; }; WMM.installOverlayTriggerPatch = function () { document.querySelectorAll('.wangp-magic-mask-trigger--overlay').forEach((trigger) => { if (trigger.dataset.wangpMagicMaskOverlayBound === '1') return; trigger.dataset.wangpMagicMaskOverlayBound = '1'; trigger.addEventListener('click', () => { WMM.openMagicMaskPanelInAnchor(trigger.closest('.wangp-magic-mask-anchor')); }, true); }); }; WMM.findImageEditorForTrigger = function (trigger) { const roots = [ trigger.closest('.wangp-magic-mask-anchor--image-editor'), trigger.parentElement, trigger.closest('.column'), trigger.closest('[id^="component-"]')?.parentElement, ].filter(Boolean); for (const root of roots) { const editor = Array.from(root.querySelectorAll('#img_editor, [data-testid="image-editor"], .imageeditor, .image-editor')) .find((candidate) => { const block = candidate.closest('.block') || candidate.closest('[id^="component-"]') || candidate; return WMM.isVisible(block); }); if (editor) return editor.closest('.block') || editor.closest('[id^="component-"]') || editor; } const previousEditors = Array.from(document.querySelectorAll('#img_editor, [data-testid="image-editor"], .imageeditor, .image-editor')) .map((candidate) => candidate.closest('.block') || candidate.closest('[id^="component-"]') || candidate) .filter((editor) => WMM.isVisible(editor) && editor.compareDocumentPosition(trigger) & Node.DOCUMENT_POSITION_FOLLOWING); return previousEditors.pop() || null; }; WMM.mountImageEditorTriggers = function () { WMM.installOverlayTriggerPatch(); document.querySelectorAll('.wangp-magic-mask-trigger--editor').forEach((trigger) => { const anchor = trigger.closest('.wangp-magic-mask-anchor--image-editor') || trigger.parentElement || document.body; if (trigger.classList.contains('hidden') || !!trigger.closest('.hidden') || !!trigger.closest('.hide') || !WMM.isVisible(anchor)) return; const editor = WMM.findImageEditorForTrigger(trigger); if (!editor || !trigger) return; const toolbar = WMM.findImageEditorToolbar(editor); if (!toolbar) { return; } anchor.querySelectorAll('.wangp-magic-mask-toolbar-button').forEach((button) => { if (button.parentElement !== toolbar) button.remove(); }); let toolbarButton = toolbar.querySelector('.wangp-magic-mask-toolbar-button'); if (!toolbarButton) { toolbarButton = document.createElement('button'); toolbarButton.type = 'button'; toolbarButton.className = 'wangp-magic-mask-toolbar-button'; toolbarButton.setAttribute('aria-label', 'Magic Mask'); toolbarButton.setAttribute('title', 'Magic Mask'); toolbarButton.textContent = '\u{1FA84}'; toolbar.appendChild(toolbarButton); } toolbar.classList.add('wangp-magic-mask-toolbar'); toolbarButton.hidden = trigger.classList.contains('hidden') || !!trigger.closest('.hidden'); toolbarButton.disabled = trigger.disabled; const needsImage = /Upload an image/i.test(editor.innerText || '') && /select the draw tool to start/i.test(editor.innerText || ''); toolbarButton.classList.toggle('wangp-magic-mask-unavailable', needsImage); toolbarButton.title = needsImage ? 'Magic Mask needs a control image' : 'Magic Mask'; toolbarButton.onclick = (event) => { event.preventDefault(); event.stopPropagation(); if (needsImage) return; trigger.click(); }; }); }; WMM.scheduleMount = function () { if (WMM.raf) cancelAnimationFrame(WMM.raf); WMM.raf = requestAnimationFrame(WMM.mountImageEditorTriggers); }; if (!WMM.init) { WMM.init = true; WMM.observer = new MutationObserver(WMM.scheduleMount); const root = document.querySelector('gradio-app') || document.body; if (root) WMM.observer.observe(root, { childList: true, subtree: true }); window.addEventListener('resize', WMM.scheduleMount); window.addEventListener('load', WMM.scheduleMount); WMM.interval = window.setInterval(WMM.scheduleMount, 500); } WMM.installImageEditorFocusPatch(); WMM.scheduleMount(); """ @staticmethod def focus_image_editor_javascript(): return "() => { window.__wangpMagicMaskNS?.refocusImageEditorAfterMagicMask?.(); }" def render(self, visible=False, trigger_mode="overlay"): self.abort_token = gr.State(str(uuid.uuid4())) self.pending_image_mask_guide = gr.State(None) self.pending_image_mask = gr.State(None) self.trigger = gr.Button(MAGIC_WAND_LABEL, size="sm", min_width=1, visible=visible, elem_classes=["wangp-magic-mask-trigger", f"wangp-magic-mask-trigger--{trigger_mode}"]) with gr.Group(visible=False, elem_classes=["wangp-magic-mask-panel"]) as self.panel: with gr.Column(elem_classes=["wangp-magic-mask-card"]): gr.HTML("
Magic Mask
") with gr.Column(elem_classes=["wangp-magic-mask-body"]): gr.HTML("
Enter the list of Object or Persons to track and that will be used to build the Mask. Each object / person should be separated by a \",\". For example: \"blue car, woman to the right\"
") with gr.Row(elem_classes=["wangp-magic-mask-keyword-row"]): self.keywords = gr.Textbox(show_label=False, placeholder="person, car, sky", lines=1, scale=4, elem_classes=["wangp-magic-mask-keywords"]) with gr.Group(elem_classes=["wangp-magic-mask-negative"]): self.negative_mask = gr.Checkbox(label="Negative Mask", value=False, container=False, min_width=1, elem_classes=["wangp-magic-mask-negative-checkbox"]) self.status = gr.HTML("") self.progress_html = gr.HTML("") gr.HTML("", elem_classes=["wangp-magic-mask-spacer"], padding=False) with gr.Row(elem_classes=["wangp-magic-mask-actions"]): self.cancel_btn = gr.Button("Exit", size="sm", elem_classes=["wangp-magic-mask-btn"]) self.abort_btn = gr.Button("Abort", size="sm", visible=False, elem_classes=["wangp-magic-mask-btn", "wangp-magic-mask-btn--danger"]) self.generate_btn = gr.Button("Generate", size="sm", elem_classes=["wangp-magic-mask-btn", "wangp-magic-mask-btn--primary"]) return self def mount( self, *, state, image_mode, video_guide, image_mask_guide, image_guide, image_mask, video_mask, download_assets: Callable[[dict[str, Any]], Any], acquire_gpu: Callable[[Any, str, str], Any], release_gpu: Callable[[Any, str], Any], get_model_settings: Callable[[Any], dict], ): self.trigger.click(fn=_open_panel, inputs=[], outputs=[self.panel, self.status, self.progress_html, self.cancel_btn, self.abort_btn, self.pending_image_mask_guide, self.pending_image_mask], show_progress="hidden") self.cancel_btn.click( fn=_close_panel, inputs=[], outputs=[self.panel, self.status, self.progress_html, self.abort_btn, self.pending_image_mask_guide, self.pending_image_mask], show_progress="hidden", ) self.abort_btn.click(fn=_abort_magic_mask, inputs=[self.abort_token], outputs=[self.status, self.abort_btn], show_progress="hidden") def generate(state_value, keywords_text, negative_mask_value, image_mode_value, video_guide_value, image_mask_guide_value, image_guide_value, abort_token_value): yield from _generate_magic_mask( state_value, keywords_text, negative_mask_value, image_mode_value, video_guide_value, image_mask_guide_value, image_guide_value, abort_token_value, download_assets=download_assets, acquire_gpu=acquire_gpu, release_gpu=release_gpu, get_model_settings=get_model_settings, ) generate_event = self.generate_btn.click( fn=generate, inputs=[state, self.keywords, self.negative_mask, image_mode, video_guide, image_mask_guide, image_guide, self.abort_token], outputs=[image_mask_guide, image_mask, video_mask, self.status, self.panel, self.progress_html, self.cancel_btn, self.abort_btn, self.pending_image_mask_guide, self.pending_image_mask], show_progress="hidden", ) generate_event.then(fn=None, inputs=[], outputs=[], js=MagicMaskUI.focus_image_editor_javascript())