| | import base64 |
| | import io |
| | import json |
| | import subprocess |
| | import time |
| | from pathlib import Path |
| |
|
| | import ffmpeg |
| | import gradio as gr |
| | import numpy as np |
| | from PIL import Image, ImageOps |
| |
|
| | from shared.utils.plugins import WAN2GPPlugin |
| |
|
| |
|
| | class MotionDesignerPlugin(WAN2GPPlugin): |
| | def __init__(self): |
| | super().__init__() |
| | self.name = "Motion Designer" |
| | self.version = "1.0.0" |
| | self.description = ( |
| | "Cut objects, design their motion paths, preview the animation, and send the mask directly into WanGP." |
| | ) |
| | self._iframe_html_cache: str | None = None |
| | self._iframe_cache_signature: tuple[int, int, int] | None = None |
| |
|
| | def setup_ui(self): |
| | self.request_global("update_video_prompt_type") |
| | self.request_global("get_model_def") |
| | self.request_component("state") |
| | self.request_component("main_tabs") |
| | self.request_component("refresh_form_trigger") |
| | self.request_global("get_current_model_settings") |
| | self.add_custom_js(self._js_bridge()) |
| | self.add_tab( |
| | tab_id="motion_designer", |
| | label="Motion Designer", |
| | component_constructor=self._build_ui, |
| | ) |
| |
|
| | def _build_ui(self): |
| | iframe_html = self._get_iframe_markup() |
| | iframe_wrapper_style = """ |
| | <style> |
| | #motion_designer_iframe_container, |
| | #motion_designer_iframe_container > div { |
| | padding: 0 !important; |
| | margin: 0 !important; |
| | } |
| | #motion_designer_iframe_container iframe { |
| | display: block; |
| | } |
| | </style> |
| | """ |
| | with gr.Column(elem_id="motion_designer_plugin"): |
| | gr.HTML( |
| | value=iframe_wrapper_style + iframe_html, |
| | elem_id="motion_designer_iframe_container", |
| | min_height=None, |
| | ) |
| | mask_payload = gr.Textbox( |
| | label="Mask Payload", |
| | visible=False, |
| | elem_id="motion_designer_mask_payload", |
| | ) |
| | metadata_payload = gr.Textbox( |
| | label="Mask Metadata", |
| | visible=False, |
| | elem_id="motion_designer_meta_payload", |
| | ) |
| | background_payload = gr.Textbox( |
| | label="Background Payload", |
| | visible=False, |
| | elem_id="motion_designer_background_payload", |
| | ) |
| | guide_payload = gr.Textbox( |
| | label="Guide Payload", |
| | visible=False, |
| | elem_id="motion_designer_guide_payload", |
| | ) |
| | guide_metadata_payload = gr.Textbox( |
| | label="Guide Metadata", |
| | visible=False, |
| | elem_id="motion_designer_guide_meta_payload", |
| | ) |
| | mode_sync = gr.Textbox( |
| | label="Mode Sync", |
| | value="cut_drag", |
| | visible=False, |
| | elem_id="motion_designer_mode_sync", |
| | ) |
| | trajectory_payload = gr.Textbox( |
| | label="Trajectory Payload", |
| | visible=False, |
| | elem_id="motion_designer_trajectory_payload", |
| | ) |
| | trajectory_metadata = gr.Textbox( |
| | label="Trajectory Metadata", |
| | visible=False, |
| | elem_id="motion_designer_trajectory_meta", |
| | ) |
| | trajectory_background = gr.Textbox( |
| | label="Trajectory Background", |
| | visible=False, |
| | elem_id="motion_designer_trajectory_background", |
| | ) |
| | trigger = gr.Button( |
| | "Apply Motion Designer data", |
| | visible=False, |
| | elem_id="motion_designer_apply_trigger", |
| | ) |
| | trajectory_trigger = gr.Button( |
| | "Apply Trajectory data", |
| | visible=False, |
| | elem_id="motion_designer_trajectory_trigger", |
| | ) |
| |
|
| | trajectory_trigger.click( |
| | fn=self._apply_trajectory, |
| | inputs=[ |
| | self.state, |
| | trajectory_payload, |
| | trajectory_metadata, |
| | trajectory_background, |
| | ], |
| | outputs=[self.refresh_form_trigger], |
| | show_progress="hidden", |
| | ).then( |
| | fn=self.goto_video_tab, |
| | inputs=[self.state], |
| | outputs=[self.main_tabs], |
| | ) |
| |
|
| | trigger.click( |
| | fn=self._apply_mask, |
| | inputs=[ |
| | self.state, |
| | mask_payload, |
| | metadata_payload, |
| | background_payload, |
| | guide_payload, |
| | guide_metadata_payload, |
| | ], |
| | outputs=[self.refresh_form_trigger], |
| | show_progress="hidden", |
| | ).then( |
| | fn=self.goto_video_tab, |
| | inputs=[self.state], |
| | outputs=[self.main_tabs], |
| | ) |
| |
|
| | mode_sync.change( |
| | fn=lambda _: None, |
| | inputs=[mode_sync], |
| | outputs=[], |
| | show_progress="hidden", |
| | queue=False, |
| | js=""" |
| | (mode) => { |
| | const raw = (mode || "").toString(); |
| | const normalized = raw.split("|", 1)[0]?.trim().toLowerCase(); |
| | if (!normalized) { |
| | return; |
| | } |
| | if (window.motionDesignerSetRenderMode) { |
| | window.motionDesignerSetRenderMode(normalized); |
| | } else { |
| | console.warn("[MotionDesignerPlugin] motionDesignerSetRenderMode not ready yet."); |
| | } |
| | } |
| | """, |
| | ) |
| | self.on_tab_outputs = [mode_sync] |
| |
|
| | def on_tab_select(self, state: dict) -> str: |
| | model_def = self.get_model_def(state["model_type"]) |
| | mode = "cut_drag" |
| | if model_def.get("i2v_v2v", False): |
| | mode = "cut_drag" |
| | elif model_def.get("vace_class", False): |
| | mode = "classic" |
| | elif model_def.get("i2v_trajectory", False): |
| | mode = "trajectory" |
| | else: |
| | return gr.update() |
| | return f"{mode}|{time.time():.6f}" |
| |
|
| | def _apply_mask( |
| | self, |
| | state, |
| | encoded_video: str | None, |
| | metadata_json: str | None, |
| | background_image_data: str | None, |
| | guide_video_data: str | None, |
| | guide_metadata_json: str | None, |
| | ): |
| | if not encoded_video: |
| | raise gr.Error("No mask video received from Motion Designer.") |
| |
|
| | encoded_video = encoded_video.strip() |
| | try: |
| | video_bytes = base64.b64decode(encoded_video) |
| | except Exception as exc: |
| | raise gr.Error("Unable to decode the mask video payload.") from exc |
| |
|
| | metadata: dict[str, object] = {} |
| | if metadata_json: |
| | try: |
| | metadata = json.loads(metadata_json) |
| | if not isinstance(metadata, dict): |
| | metadata = {} |
| | except json.JSONDecodeError: |
| | metadata = {} |
| |
|
| | guide_metadata: dict[str, object] = {} |
| | if guide_metadata_json: |
| | try: |
| | guide_metadata = json.loads(guide_metadata_json) |
| | if not isinstance(guide_metadata, dict): |
| | guide_metadata = {} |
| | except json.JSONDecodeError: |
| | guide_metadata = {} |
| |
|
| | background_image = self._decode_background_image(background_image_data) |
| |
|
| | output_dir = Path("mask_outputs") |
| | output_dir.mkdir(parents=True, exist_ok=True) |
| | timestamp = time.strftime("%Y%m%d_%H%M%S") |
| | file_path = output_dir / f"motion_designer_mask_{timestamp}.webm" |
| | file_path.write_bytes(video_bytes) |
| |
|
| | guide_path: Path | None = None |
| | if guide_video_data: |
| | try: |
| | guide_bytes = base64.b64decode(guide_video_data.strip()) |
| | guide_path = output_dir / f"motion_designer_guide_{timestamp}.webm" |
| | guide_path.write_bytes(guide_bytes) |
| | except Exception as exc: |
| | print(f"[MotionDesignerPlugin] Failed to decode guide video payload: {exc}") |
| | guide_path = None |
| |
|
| | fps_hint = None |
| | render_mode = "" |
| | if isinstance(metadata, dict): |
| | fps_hint = metadata.get("fps") |
| | render_mode = str(metadata.get("renderMode") or "").lower() |
| | if fps_hint is None and isinstance(guide_metadata, dict): |
| | fps_hint = guide_metadata.get("fps") |
| | if render_mode not in ("classic", "cut_drag") and isinstance(guide_metadata, dict): |
| | render_mode = str(guide_metadata.get("renderMode") or "").lower() |
| | if render_mode not in ("classic", "cut_drag"): |
| | render_mode = "cut_drag" |
| |
|
| | sanitized_mask_path = self._transcode_video(file_path, fps_hint) |
| | sanitized_guide_path = self._transcode_video(guide_path, fps_hint) if guide_path else None |
| | self._log_frame_check("mask", sanitized_mask_path, metadata) |
| | if sanitized_guide_path: |
| | self._log_frame_check("guide", sanitized_guide_path, guide_metadata or metadata) |
| |
|
| | |
| | |
| |
|
| | ui_settings = self.get_current_model_settings(state) |
| | if render_mode == "classic": |
| | ui_settings["video_guide"] = str(sanitized_mask_path) |
| | ui_settings.pop("video_mask", None) |
| | ui_settings.pop("video_mask_meta", None) |
| | if metadata: |
| | ui_settings["video_guide_meta"] = metadata |
| | else: |
| | ui_settings.pop("video_guide_meta", None) |
| | else: |
| | ui_settings["video_mask"] = str(sanitized_mask_path) |
| | if metadata: |
| | ui_settings["video_mask_meta"] = metadata |
| | else: |
| | ui_settings.pop("video_mask_meta", None) |
| |
|
| | guide_video_path = sanitized_guide_path or sanitized_mask_path |
| | ui_settings["video_guide"] = str(guide_video_path) |
| | if guide_metadata: |
| | ui_settings["video_guide_meta"] = guide_metadata |
| | elif metadata: |
| | ui_settings["video_guide_meta"] = metadata |
| | else: |
| | ui_settings.pop("video_guide_meta", None) |
| |
|
| | if background_image is not None: |
| | if render_mode == "classic": |
| | existing_refs = ui_settings.get("image_refs") |
| | if isinstance(existing_refs, list) and existing_refs: |
| | new_refs = list(existing_refs) |
| | new_refs[0] = background_image |
| | ui_settings["image_refs"] = new_refs |
| | else: |
| | ui_settings["image_refs"] = [background_image] |
| | else: |
| | ui_settings["image_start"] = [background_image] |
| | if render_mode == "classic": |
| | self.update_video_prompt_type(state, any_video_guide = True, any_background_image_ref = True, process_type = "") |
| | else: |
| | self.update_video_prompt_type(state, any_video_guide = True, any_video_mask = True, default_update="G") |
| |
|
| | gr.Info("Motion Designer data transferred to the Video Generator.") |
| | return time.time() |
| |
|
| | def _apply_trajectory( |
| | self, |
| | state, |
| | trajectory_json: str | None, |
| | metadata_json: str | None, |
| | background_data_url: str | None, |
| | ): |
| | if not trajectory_json: |
| | raise gr.Error("No trajectory data received from Motion Designer.") |
| |
|
| | try: |
| | trajectories = json.loads(trajectory_json) |
| | if not isinstance(trajectories, list) or len(trajectories) == 0: |
| | raise gr.Error("Invalid trajectory data: expected non-empty array.") |
| | except json.JSONDecodeError as exc: |
| | raise gr.Error("Unable to parse trajectory data.") from exc |
| |
|
| | metadata: dict[str, object] = {} |
| | if metadata_json: |
| | try: |
| | metadata = json.loads(metadata_json) |
| | if not isinstance(metadata, dict): |
| | metadata = {} |
| | except json.JSONDecodeError: |
| | metadata = {} |
| |
|
| | |
| | |
| | trajectory_array = np.array(trajectories, dtype=np.float32) |
| |
|
| | |
| | if len(trajectory_array.shape) != 3 or trajectory_array.shape[2] != 2: |
| | raise gr.Error(f"Invalid trajectory shape: expected [T, N, 2], got {trajectory_array.shape}") |
| |
|
| | |
| | output_dir = Path("mask_outputs") |
| | output_dir.mkdir(parents=True, exist_ok=True) |
| | timestamp = time.strftime("%Y%m%d_%H%M%S") |
| | file_path = output_dir / f"motion_designer_trajectory_{timestamp}.npy" |
| | np.save(file_path, trajectory_array) |
| |
|
| | print(f"[MotionDesignerPlugin] Trajectory saved: {file_path} (shape: {trajectory_array.shape})") |
| |
|
| | |
| | ui_settings = self.get_current_model_settings(state) |
| | ui_settings["custom_guide"] = str(file_path.absolute()) |
| |
|
| | |
| | background_image = self._decode_background_image(background_data_url) |
| | if background_image is not None: |
| | ui_settings["image_start"] = [background_image] |
| |
|
| | gr.Info(f"Trajectory data saved ({trajectory_array.shape[0]} frames, {trajectory_array.shape[1]} trajectories).") |
| | return time.time() |
| |
|
| | def _decode_background_image(self, data_url: str | None): |
| | if not data_url: |
| | return None |
| | payload = data_url |
| | if isinstance(payload, str) and "," in payload: |
| | _, payload = payload.split(",", 1) |
| | try: |
| | image_bytes = base64.b64decode(payload) |
| | with Image.open(io.BytesIO(image_bytes)) as img: |
| | return ImageOps.exif_transpose(img.convert("RGB")) |
| | except Exception as exc: |
| | print(f"[MotionDesignerPlugin] Failed to decode background image: {exc}") |
| | return None |
| |
|
| | def _transcode_video(self, source_path: Path, fps: int | float | None) -> Path: |
| | frame_rate = max(int(fps), 1) if isinstance(fps, (int, float)) and fps else 16 |
| | temp_path = source_path.with_suffix(".clean.webm") |
| | try: |
| | |
| | ( |
| | ffmpeg |
| | .input(str(source_path)) |
| | .output( |
| | str(temp_path), |
| | c="copy", |
| | r=frame_rate, |
| | **{ |
| | "vsync": "cfr", |
| | "fps_mode": "cfr", |
| | "fflags": "+genpts", |
| | "copyts": None, |
| | }, |
| | ) |
| | .overwrite_output() |
| | .run(quiet=True) |
| | ) |
| | if source_path.exists(): |
| | source_path.unlink() |
| | temp_path.replace(source_path) |
| | except ffmpeg.Error as err: |
| | stderr = getattr(err, "stderr", b"") |
| | decoded = stderr.decode("utf-8", errors="ignore") if isinstance(stderr, (bytes, bytearray)) else str(stderr) |
| | print(f"[MotionDesignerPlugin] FFmpeg failed to sanitize mask video: {decoded.strip()}") |
| | except Exception as exc: |
| | print(f"[MotionDesignerPlugin] Unexpected error while sanitizing mask video: {exc}") |
| | return source_path |
| |
|
| | def _probe_frames_fps(self, video_path: Path) -> tuple[int | None, float | None]: |
| | if not video_path or not video_path.exists(): |
| | return (None, None) |
| | ffprobe_path = Path("ffprobe.exe") |
| | if not ffprobe_path.exists(): |
| | ffprobe_path = Path("ffprobe") |
| | cmd = [ |
| | str(ffprobe_path), |
| | "-v", |
| | "error", |
| | "-select_streams", |
| | "v:0", |
| | "-count_frames", |
| | "-show_entries", |
| | "stream=nb_read_frames,nb_frames,avg_frame_rate,r_frame_rate", |
| | "-of", |
| | "default=noprint_wrappers=1:nokey=1", |
| | str(video_path), |
| | ] |
| | try: |
| | result = subprocess.run(cmd, capture_output=True, text=True, check=False) |
| | output = (result.stdout or "").strip().splitlines() |
| | |
| | frame_count = None |
| | fps_val = None |
| | for line in output: |
| | if line.strip().isdigit(): |
| | |
| | val = int(line.strip()) |
| | frame_count = val |
| | elif "/" in line: |
| | num, _, denom = line.partition("/") |
| | try: |
| | n = float(num) |
| | d = float(denom) |
| | if d != 0: |
| | fps_val = n / d |
| | except (ValueError, ZeroDivisionError): |
| | continue |
| | return (frame_count, fps_val) |
| | except Exception: |
| | return (None, None) |
| |
|
| | def _log_frame_check(self, label: str, video_path: Path, metadata: dict[str, object] | None): |
| | expected_frames = None |
| | if isinstance(metadata, dict): |
| | exp = metadata.get("expectedFrames") |
| | if isinstance(exp, (int, float)): |
| | expected_frames = int(exp) |
| | actual_frames, fps = self._probe_frames_fps(video_path) |
| | if expected_frames is None or actual_frames is None: |
| | return |
| | if expected_frames != actual_frames: |
| | print( |
| | f"[MotionDesignerPlugin] Frame count mismatch for {label}: " |
| | f"expected {expected_frames}, got {actual_frames} (fps probed: {fps or 'n/a'})" |
| | ) |
| |
|
| | def _get_iframe_markup(self) -> str: |
| | assets_dir = Path(__file__).parent / "assets" |
| | template_path = assets_dir / "motion_designer_iframe_template.html" |
| | script_path = assets_dir / "app.js" |
| | style_path = assets_dir / "style.css" |
| |
|
| | cache_signature: tuple[int, int, int] | None = None |
| | try: |
| | cache_signature = ( |
| | template_path.stat().st_mtime_ns, |
| | script_path.stat().st_mtime_ns, |
| | style_path.stat().st_mtime_ns, |
| | ) |
| | except FileNotFoundError: |
| | cache_signature = None |
| | if ( |
| | self._iframe_html_cache |
| | and cache_signature |
| | and cache_signature == self._iframe_cache_signature |
| | ): |
| | return self._iframe_html_cache |
| |
|
| | template_html = template_path.read_text(encoding="utf-8") |
| | script_js = script_path.read_text(encoding="utf-8") |
| | style_css = style_path.read_text(encoding="utf-8") |
| |
|
| | iframe_html = template_html.replace("<!-- MOTION_DESIGNER_STYLE_INLINE -->", f"<style>{style_css}</style>") |
| | iframe_html = iframe_html.replace("<!-- MOTION_DESIGNER_SCRIPT_INLINE -->", f"<script>{script_js}</script>") |
| |
|
| | encoded = base64.b64encode(iframe_html.encode("utf-8")).decode("ascii") |
| | self._iframe_html_cache = ( |
| | "<iframe id='motion-designer-iframe' " |
| | "title='Motion Designer' " |
| | "sandbox='allow-scripts allow-same-origin allow-pointer-lock allow-downloads' " |
| | "style='width:100%;border:none;border-radius:12px;display:block;' " |
| | f"src='data:text/html;base64,{encoded}'></iframe>" |
| | ) |
| | self._iframe_cache_signature = cache_signature |
| | return self._iframe_html_cache |
| |
|
| | def _js_bridge(self) -> str: |
| | return r""" |
| | const MOTION_DESIGNER_EVENT_TYPE = "WAN2GP_MOTION_DESIGNER"; |
| | const MOTION_DESIGNER_CONTROL_MESSAGE_TYPE = "WAN2GP_MOTION_DESIGNER_CONTROL"; |
| | const MOTION_DESIGNER_MODE_INPUT_SELECTOR = "#motion_designer_mode_sync textarea, #motion_designer_mode_sync input"; |
| | const MOTION_DESIGNER_IFRAME_SELECTOR = "#motion-designer-iframe"; |
| | const MOTION_DESIGNER_MODAL_LOCK = "WAN2GP_MOTION_DESIGNER_MODAL_LOCK"; |
| | const MODAL_PLACEHOLDER_ID = "motion-designer-iframe-placeholder"; |
| | let modalLockState = { |
| | locked: false, |
| | scrollX: 0, |
| | scrollY: 0, |
| | placeholder: null, |
| | prevStyles: {}, |
| | unlockTimeout: null, |
| | }; |
| | console.log("[MotionDesignerPlugin] Bridge script injected"); |
| | |
| | function motionDesignerRoot() { |
| | if (window.gradioApp) { |
| | return window.gradioApp(); |
| | } |
| | const app = document.querySelector("gradio-app"); |
| | return app ? (app.shadowRoot || app) : document; |
| | } |
| | |
| | function motionDesignerDispatchInput(element, value) { |
| | if (!element) { |
| | return; |
| | } |
| | element.value = value; |
| | element.dispatchEvent(new Event("input", { bubbles: true })); |
| | } |
| | |
| | function motionDesignerTriggerButton(appRoot) { |
| | return appRoot.querySelector("#motion_designer_apply_trigger button, #motion_designer_apply_trigger"); |
| | } |
| | |
| | function motionDesignerGetIframe() { |
| | return document.querySelector(MOTION_DESIGNER_IFRAME_SELECTOR); |
| | } |
| | |
| | function motionDesignerSendControlMessage(action, value) { |
| | const iframe = motionDesignerGetIframe(); |
| | if (!iframe || !iframe.contentWindow) { |
| | console.warn("[MotionDesignerPlugin] Unable to locate Motion Designer iframe for", action); |
| | return; |
| | } |
| | console.debug("[MotionDesignerPlugin] Posting control message", action, value); |
| | iframe.contentWindow.postMessage( |
| | { type: MOTION_DESIGNER_CONTROL_MESSAGE_TYPE, action, value }, |
| | "*", |
| | ); |
| | } |
| | |
| | function motionDesignerExtractMode(value) { |
| | if (!value) { |
| | return ""; |
| | } |
| | return value.split("|", 1)[0]?.trim().toLowerCase() || ""; |
| | } |
| | |
| | window.motionDesignerSetRenderMode = (mode) => { |
| | const normalized = motionDesignerExtractMode(mode); |
| | if (!normalized) { |
| | return; |
| | } |
| | let target; |
| | if (normalized === "classic") { |
| | target = "classic"; |
| | } else if (normalized === "trajectory") { |
| | target = "trajectory"; |
| | } else { |
| | target = "cut_drag"; |
| | } |
| | console.log("[MotionDesignerPlugin] Mode sync triggered:", target); |
| | motionDesignerSendControlMessage("setMode", target); |
| | }; |
| | |
| | window.addEventListener("message", (event) => { |
| | if (event?.data?.type === "WAN2GP_MOTION_DESIGNER_RESIZE") { |
| | if (typeof event.data.height === "number") { |
| | const iframe = document.querySelector("#motion-designer-iframe"); |
| | if (iframe) { |
| | iframe.style.height = `${Math.max(event.data.height, 400)}px`; |
| | } |
| | } |
| | return; |
| | } |
| | if (event?.data?.type === MOTION_DESIGNER_MODAL_LOCK) { |
| | const iframe = document.querySelector(MOTION_DESIGNER_IFRAME_SELECTOR); |
| | if (!iframe) { |
| | return; |
| | } |
| | const lock = Boolean(event.data.open); |
| | const clearUnlockTimeout = () => { |
| | if (modalLockState.unlockTimeout) { |
| | clearTimeout(modalLockState.unlockTimeout); |
| | modalLockState.unlockTimeout = null; |
| | } |
| | }; |
| | if (lock) { |
| | clearUnlockTimeout(); |
| | if (modalLockState.locked) { |
| | return; |
| | } |
| | modalLockState.locked = true; |
| | modalLockState.scrollX = window.scrollX; |
| | modalLockState.scrollY = window.scrollY; |
| | const rect = iframe.getBoundingClientRect(); |
| | const placeholder = document.createElement("div"); |
| | placeholder.id = MODAL_PLACEHOLDER_ID; |
| | placeholder.style.width = `${rect.width}px`; |
| | placeholder.style.height = `${iframe.offsetHeight}px`; |
| | placeholder.style.pointerEvents = "none"; |
| | placeholder.style.flex = iframe.style.flex || "0 0 auto"; |
| | iframe.insertAdjacentElement("afterend", placeholder); |
| | modalLockState.placeholder = placeholder; |
| | modalLockState.prevStyles = { |
| | position: iframe.style.position, |
| | top: iframe.style.top, |
| | left: iframe.style.left, |
| | right: iframe.style.right, |
| | bottom: iframe.style.bottom, |
| | width: iframe.style.width, |
| | height: iframe.style.height, |
| | maxHeight: iframe.style.maxHeight, |
| | zIndex: iframe.style.zIndex, |
| | }; |
| | iframe.style.position = "fixed"; |
| | iframe.style.top = "0"; |
| | iframe.style.left = `${rect.left}px`; |
| | iframe.style.right = "auto"; |
| | iframe.style.bottom = "auto"; |
| | iframe.style.width = `${rect.width}px`; |
| | iframe.style.height = "100vh"; |
| | iframe.style.maxHeight = "100vh"; |
| | iframe.style.zIndex = "2147483647"; |
| | document.documentElement.classList.add("modal-open"); |
| | document.body.classList.add("modal-open"); |
| | } else { |
| | clearUnlockTimeout(); |
| | modalLockState.unlockTimeout = setTimeout(() => { |
| | if (!modalLockState.locked) { |
| | return; |
| | } |
| | modalLockState.locked = false; |
| | iframe.style.position = modalLockState.prevStyles.position || ""; |
| | iframe.style.top = modalLockState.prevStyles.top || ""; |
| | iframe.style.left = modalLockState.prevStyles.left || ""; |
| | iframe.style.right = modalLockState.prevStyles.right || ""; |
| | iframe.style.bottom = modalLockState.prevStyles.bottom || ""; |
| | iframe.style.width = modalLockState.prevStyles.width || ""; |
| | iframe.style.height = modalLockState.prevStyles.height || ""; |
| | iframe.style.maxHeight = modalLockState.prevStyles.maxHeight || ""; |
| | iframe.style.zIndex = modalLockState.prevStyles.zIndex || ""; |
| | if (modalLockState.placeholder?.parentNode) { |
| | modalLockState.placeholder.parentNode.removeChild(modalLockState.placeholder); |
| | } |
| | modalLockState.placeholder = null; |
| | modalLockState.prevStyles = {}; |
| | document.documentElement.classList.remove("modal-open"); |
| | document.body.classList.remove("modal-open"); |
| | window.scrollTo(modalLockState.scrollX, modalLockState.scrollY); |
| | }, 120); |
| | } |
| | return; |
| | } |
| | if (!event?.data || event.data.type !== MOTION_DESIGNER_EVENT_TYPE) { |
| | return; |
| | } |
| | console.debug("[MotionDesignerPlugin] Received iframe payload", event.data); |
| | const appRoot = motionDesignerRoot(); |
| | |
| | // Handle trajectory export separately |
| | if (event.data.isTrajectoryExport) { |
| | const trajectoryInput = appRoot.querySelector("#motion_designer_trajectory_payload textarea, #motion_designer_trajectory_payload input"); |
| | const trajectoryMetaInput = appRoot.querySelector("#motion_designer_trajectory_meta textarea, #motion_designer_trajectory_meta input"); |
| | const trajectoryBgInput = appRoot.querySelector("#motion_designer_trajectory_background textarea, #motion_designer_trajectory_background input"); |
| | const trajectoryButton = appRoot.querySelector("#motion_designer_trajectory_trigger button, #motion_designer_trajectory_trigger"); |
| | if (!trajectoryInput || !trajectoryMetaInput || !trajectoryBgInput || !trajectoryButton) { |
| | console.warn("[MotionDesignerPlugin] Trajectory bridge components missing in Gradio DOM."); |
| | return; |
| | } |
| | const trajectoryData = event.data.trajectoryData || []; |
| | const trajectoryMetadata = event.data.metadata || {}; |
| | const backgroundImage = event.data.backgroundImage || ""; |
| | motionDesignerDispatchInput(trajectoryInput, JSON.stringify(trajectoryData)); |
| | motionDesignerDispatchInput(trajectoryMetaInput, JSON.stringify(trajectoryMetadata)); |
| | motionDesignerDispatchInput(trajectoryBgInput, backgroundImage); |
| | trajectoryButton.click(); |
| | return; |
| | } |
| | |
| | const maskInput = appRoot.querySelector("#motion_designer_mask_payload textarea, #motion_designer_mask_payload input"); |
| | const metaInput = appRoot.querySelector("#motion_designer_meta_payload textarea, #motion_designer_meta_payload input"); |
| | const bgInput = appRoot.querySelector("#motion_designer_background_payload textarea, #motion_designer_background_payload input"); |
| | const guideInput = appRoot.querySelector("#motion_designer_guide_payload textarea, #motion_designer_guide_payload input"); |
| | const guideMetaInput = appRoot.querySelector("#motion_designer_guide_meta_payload textarea, #motion_designer_guide_meta_payload input"); |
| | const button = motionDesignerTriggerButton(appRoot); |
| | if (!maskInput || !metaInput || !bgInput || !guideInput || !guideMetaInput || !button) { |
| | console.warn("[MotionDesignerPlugin] Bridge components missing in Gradio DOM."); |
| | return; |
| | } |
| | |
| | const payload = event.data.payload || ""; |
| | const metadata = event.data.metadata || {}; |
| | const backgroundImage = event.data.backgroundImage || ""; |
| | const guidePayload = event.data.guidePayload || ""; |
| | const guideMetadata = event.data.guideMetadata || {}; |
| | |
| | motionDesignerDispatchInput(maskInput, payload); |
| | motionDesignerDispatchInput(metaInput, JSON.stringify(metadata)); |
| | motionDesignerDispatchInput(bgInput, backgroundImage); |
| | motionDesignerDispatchInput(guideInput, guidePayload); |
| | motionDesignerDispatchInput(guideMetaInput, JSON.stringify(guideMetadata)); |
| | button.click(); |
| | }); |
| | |
| | function motionDesignerBindModeInput() { |
| | const appRoot = motionDesignerRoot(); |
| | if (!appRoot) { |
| | setTimeout(motionDesignerBindModeInput, 300); |
| | return; |
| | } |
| | const input = appRoot.querySelector(MOTION_DESIGNER_MODE_INPUT_SELECTOR); |
| | if (!input) { |
| | setTimeout(motionDesignerBindModeInput, 300); |
| | return; |
| | } |
| | if (input.dataset.motionDesignerModeBound === "1") { |
| | return; |
| | } |
| | input.dataset.motionDesignerModeBound = "1"; |
| | let lastValue = ""; |
| | const handleChange = () => { |
| | const rawValue = input.value || ""; |
| | const extracted = motionDesignerExtractMode(rawValue); |
| | if (!extracted || extracted === lastValue) { |
| | return; |
| | } |
| | lastValue = extracted; |
| | window.motionDesignerSetRenderMode(extracted); |
| | }; |
| | const observer = new MutationObserver(handleChange); |
| | observer.observe(input, { attributes: true, attributeFilter: ["value"] }); |
| | input.addEventListener("input", handleChange); |
| | handleChange(); |
| | } |
| | |
| | motionDesignerBindModeInput(); |
| | console.log("[MotionDesignerPlugin] Bridge initialization complete"); |
| | """ |
| |
|