| 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"); |
| """ |
|
|