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