caiyiyi1998's picture
Initial commit
9a25493
Raw History Blame Contribute Delete
8.48 kB
"""Persistent streaming mesh and skeleton viewer for Gradio 6.28.0."""
from pathlib import Path
import gradio as gr
HERE = Path(__file__).resolve().parent
HTML = """
<div class="motion-viewer">
<div class="viewer-heading">
<span class="viewer-action">Motion</span>
<span class="viewer-status" role="status">Ready</span>
</div>
<div class="viewer-stage">
<div class="viewer-focus-hint">Click the view to use WASD</div>
<div class="viewer-body-notice"><span class="viewer-body-status" role="status"></span><button class="viewer-body-retry" type="button" hidden>Retry body loading</button></div>
</div>
<div class="viewer-controls">
<div class="viewer-key-legend"><span><kbd data-key="KeyW">W</kbd><kbd data-key="KeyS">S</kbd> Camera forward / back</span><span><kbd data-key="KeyA">A</kbd><kbd data-key="KeyD">D</kbd> Left / right</span><span><kbd data-key="Shift">Shift</kbd> Walk → run</span></div>
<div class="viewer-slots" aria-label="Prompt selection"><span data-slot="0">1</span><span data-slot="1">2</span><span data-slot="2">3</span><span data-slot="3">4</span><span class="viewer-slot-label">Prompt 1</span></div>
<span class="viewer-lead">Lead 0.00 s</span>
</div>
<div class="viewer-camera-hint">Drag to orbit · Scroll to zoom · Double-click to reset view</div>
<div class="viewer-path-legend"><span class="path-reached">Reached</span><span class="path-ahead">Ahead</span><span class="path-target">◇ Target</span><span class="path-spring">○ Input</span></div>
<div class="viewer-stats">
<div><span>Runtime</span><strong data-stat="runtime">0.0 s</strong></div>
<div><span>Generation</span><strong data-stat="generation">— fps</strong></div>
<div><span>Playback</span><strong data-stat="playback">0 fps</strong></div>
<div><span>Buffer</span><strong data-stat="buffer">0.00 s</strong></div>
</div>
</div>
"""
CSS = """
.motion-viewer { overflow: hidden; border: 1px solid #dce3e8; border-radius: 16px; background: #f8fafb; color: #233340; font-family: inherit; }
.viewer-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 20px 0; }
.viewer-action { font-size: 16px; font-weight: 600; }
.viewer-status { color: #667782; font-size: 12px; }
.viewer-stage { position: relative; width: 100%; height: clamp(300px, 48vw, 490px); overflow: hidden; }
.viewer-stage canvas { display: block; width: 100%; height: 100%; outline: none; cursor: grab; touch-action: none; }
.viewer-stage canvas:active { cursor: grabbing; }
.viewer-stage:focus-within { box-shadow: inset 0 0 0 2px #397ad5; }
.viewer-focus-hint { position: absolute; bottom: 10px; left: 0; right: 0; z-index: 1; text-align: center; pointer-events: none; font-size: 12px; color: #647582; }
.viewer-stage:focus-within .viewer-focus-hint { opacity: 0; }
.viewer-body-notice { position: absolute; top: 12px; right: 18px; z-index: 1; color: #647582; font-size: 11px; display: flex; align-items: center; gap: 8px; }
.viewer-body-retry { border: 1px solid #ced8df; border-radius: 5px; padding: 4px 7px; background: white; color: #445b6a; cursor: pointer; font: inherit; }
.viewer-body-retry[hidden] { display: none; }
.viewer-controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 20px; padding: 12px 18px 5px; font-size: 12px; }
.viewer-key-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 14px; }
.viewer-key-legend > span { white-space: nowrap; }
kbd { display: inline-block; min-width: 22px; margin-right: 3px; padding: 2px 5px; border: 1px solid #ced8df; border-radius: 4px; color: #445b6a; background: white; font: inherit; text-align: center; }
kbd.is-down { color: white; background: #397ad5; border-color: #397ad5; }
kbd[data-key="Shift"].is-down { background: #8b5cdd; border-color: #8b5cdd; }
.viewer-slots { display: flex; align-items: center; gap: 5px; }
.viewer-slots [data-slot] { width: 22px; height: 22px; line-height: 20px; text-align: center; border: 1px solid currentColor; border-radius: 50%; opacity: .6; }
.viewer-slots [data-slot="0"] { color: #397ad5; }
.viewer-slots [data-slot="1"] { color: #8b5cdd; }
.viewer-slots [data-slot="2"] { color: #2d9e72; }
.viewer-slots [data-slot="3"] { color: #d55a9a; }
.viewer-slots [data-slot].is-selected { opacity: 1; border-width: 2px; font-weight: 700; line-height: 18px; }
.viewer-slot-label { margin-left: 4px; }
.viewer-lead { margin-left: auto; color: #667782; font-variant-numeric: tabular-nums; }
.viewer-camera-hint { padding: 4px 18px 8px; color: #667782; font-size: 11px; }
.viewer-path-legend { display: flex; gap: 16px; padding: 4px 18px 12px; color: #667782; font-size: 11px; }
.viewer-path-legend span::before { content: ""; display: inline-block; width: 18px; margin-right: 5px; border-top: 2px solid #397ad5; vertical-align: middle; }
.viewer-path-legend .path-reached::before { opacity: .55; }
.viewer-path-legend .path-ahead::before { border-top-style: dashed; }
.viewer-path-legend .path-target::before, .viewer-path-legend .path-spring::before { display: none; }
.viewer-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid #e4eaee; background: #fff; }
.viewer-stats > div { padding: 14px 16px; min-width: 0; }
.viewer-stats span { display: block; font-size: 11px; color: #6c7b86; margin-bottom: 5px; }
.viewer-stats strong { display: block; font-size: 17px; font-weight: 550; font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 500px) { .viewer-heading { padding: 14px 14px 0; } .viewer-stats > div { padding: 12px 8px; } .viewer-stats strong { font-size: 14px; } .viewer-stats span { font-size: 10px; } }
"""
def create_viewer(*, require_body_ready: bool = False) -> gr.HTML:
"""Return one HTML output; yield native dictionaries to update its value.
Each packet contains session_id, sequence, start_frame, frames [T,22,3],
fps, generated_frames, elapsed_seconds, inference_fps, action and status.
Frames use world XYZ, with Y up. A new session_id resets playback.
mesh_pose contains rotations (base64 float32 LE [T,22,3,3], row-major local
matrices with the world root first), origins (world pelvis [T,3]), and
frame_count=T. The fixed neutral body is loaded once and deformed on each
displayed frame using all skinning weights and the full body pose corrective.
With require_body_ready=True, bind viewer.body_ready to enable Start only
after the browser has loaded the body; loading assets consumes no GPU time.
Optional mesh_topology contains faces (base64 uint16 LE), vertex_count=6890
and face_count=13776, sent once per session. Optional mesh contains vertices
(base64 float16 LE, C-order [T,6890,3], relative to each supplied root),
origins [T,3] in world XYZ, frame_count=T and vertex_count=6890. Mesh, joints
and frame_meta share start_frame and one playback queue. Canvas fallback
and packets without mesh continue to display the skeleton.
Optional control_points and frame_meta retain backend coordinates unchanged.
With control_points_delta=True, points are merged by frame (up to 2000);
otherwise control_points is a complete replacement. New sessions reset it.
Bind ``viewer.control(fn, inputs=[], outputs=[], queue=False,
trigger_mode="multiple")``; an annotated gr.EventData receives the control
dictionary through ``evt._data``. This event never writes ``value``.
"""
vendor = (HERE / "three.min.js").read_text(encoding="utf-8")
body_script = (HERE / "body.js").read_text(encoding="utf-8")
script = (HERE / "viewer.js").read_text(encoding="utf-8")
body_manifest = HERE / "assets" / "body.json"
body_data = HERE / "assets" / "body.bin"
gr.set_static_paths(paths=[body_manifest, body_data])
# The local UMD build is loaded once; no CDN or file-serving setup is needed.
js = "if (!globalThis.THREE) {\n" + vendor + "\n}\n" + body_script + "\n" + script
return gr.HTML(
value={"session_id": "", "sequence": -1, "frames": [], "status": "idle"},
html_template=HTML,
css_template=CSS,
js_on_load=js,
apply_default_css=False,
container=False,
padding=False,
elem_id="live-motion-viewer",
body_manifest_url="/gradio_api/file=" + body_manifest.resolve().as_posix(),
body_data_url="/gradio_api/file=" + body_data.resolve().as_posix(),
require_body_ready=require_body_ready,
)