Spaces:
Paused
Paused
Umut Kocasari Claude Opus 4.8 commited on
Commit ·
1f2eb37
1
Parent(s): 715dbba
Viewer: preload all frames in a client-side three.js player (smooth playback)
Browse filesReplace gradio Model3D + Timer with a custom three.js point-cloud player
embedded via gr.HTML(head=..., js_on_load=...). All per-frame .glb point
clouds are loaded once into the scene; playback animates by toggling frame
visibility, so the points stay on screen the whole time (no per-frame
network/parse → no white flashes) and the full, un-subsampled cloud is kept.
Play/Pause, Speed (fps) and a scrub slider run entirely client-side; a hidden
file list bridges the served .glb URLs to the viewer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -526,29 +526,21 @@ def run(
|
|
| 526 |
|
| 527 |
_say(1.0, "Done.")
|
| 528 |
|
| 529 |
-
model3d = view_glbs[0] if view_glbs else None
|
| 530 |
if not has_canon:
|
| 531 |
log.append("WARNING: model produced no canonical output — canonical "
|
| 532 |
"video and point tracks were skipped (check the checkpoint).")
|
| 533 |
|
| 534 |
status = "\n".join(f"• {m}" for m in log)
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
visible=bool(view_glbs) and N > 1,
|
| 539 |
-
interactive=N > 1,
|
| 540 |
-
)
|
| 541 |
-
play_row_update = gr.update(visible=bool(view_glbs) and N > 1)
|
| 542 |
return (
|
| 543 |
-
|
| 544 |
canonical_vid,
|
| 545 |
depth_vid,
|
| 546 |
normals_vid,
|
| 547 |
tracks2d_vid,
|
| 548 |
tracks_zip,
|
| 549 |
-
view_glbs,
|
| 550 |
-
slider_update,
|
| 551 |
-
play_row_update,
|
| 552 |
status,
|
| 553 |
)
|
| 554 |
except gr.Error:
|
|
@@ -558,15 +550,6 @@ def run(
|
|
| 558 |
raise gr.Error(f"Inference failed: {e}\n\n{tb[-1500:]}")
|
| 559 |
|
| 560 |
|
| 561 |
-
def pick_frame(idx, view_glbs):
|
| 562 |
-
"""Swap the 3D viewer to a different frame's track point cloud."""
|
| 563 |
-
if not view_glbs:
|
| 564 |
-
return None
|
| 565 |
-
idx = int(idx)
|
| 566 |
-
idx = max(0, min(idx, len(view_glbs) - 1))
|
| 567 |
-
return view_glbs[idx]
|
| 568 |
-
|
| 569 |
-
|
| 570 |
# --------------------------------------------------------------------------- #
|
| 571 |
# UI
|
| 572 |
# --------------------------------------------------------------------------- #
|
|
@@ -583,11 +566,220 @@ temporally-consistent **3D point tracks**.
|
|
| 583 |
[Code](https://github.com/kocasariumut/FaceAnything)
|
| 584 |
"""
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
|
| 587 |
def build_demo():
|
| 588 |
with gr.Blocks(title="Face Anything") as demo:
|
| 589 |
gr.Markdown(DESCRIPTION)
|
| 590 |
-
view_state = gr.State([])
|
| 591 |
|
| 592 |
with gr.Row():
|
| 593 |
# ---------------- inputs ----------------
|
|
@@ -645,24 +837,18 @@ def build_demo():
|
|
| 645 |
|
| 646 |
# ---------------- outputs ----------------
|
| 647 |
with gr.Column(scale=1):
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
)
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
# play / pause / speed — animate the point cloud over time
|
| 657 |
-
with gr.Row(visible=False) as play_row:
|
| 658 |
-
play_btn = gr.Button("▶ Play", size="sm")
|
| 659 |
-
pause_btn = gr.Button("⏸ Pause", size="sm")
|
| 660 |
-
play_speed = gr.Slider(
|
| 661 |
-
1, 20, value=8, step=1, label="Speed (fps)",
|
| 662 |
-
info="Frames/second. Smoothness depends on point-cloud "
|
| 663 |
-
"size (each frame is reloaded); pause to orbit freely.")
|
| 664 |
-
play_timer = gr.Timer(1.0 / 8, active=False)
|
| 665 |
-
playing = gr.State(False)
|
| 666 |
tracks_zip = gr.File(
|
| 667 |
label="Download point clouds (.zip: tracks/ + points/)")
|
| 668 |
with gr.Tab("Canonical (2D)"):
|
|
@@ -683,34 +869,12 @@ def build_demo():
|
|
| 683 |
inputs=[files, video, mode, process_res, remove_bg,
|
| 684 |
conf_percentile, n_tracks, track_k, track_threshold,
|
| 685 |
fps, max_frames],
|
| 686 |
-
outputs=[
|
| 687 |
-
|
| 688 |
concurrency_limit=1,
|
| 689 |
)
|
| 690 |
-
#
|
| 691 |
-
|
| 692 |
-
frame_slider.release(pick_frame, inputs=[frame_slider, view_state],
|
| 693 |
-
outputs=model3d)
|
| 694 |
-
|
| 695 |
-
# ---------------- playback (dynamic point cloud over time) ----------------
|
| 696 |
-
def _advance(idx, view_glbs):
|
| 697 |
-
"""Timer tick: hop to the next frame's point cloud, looping at the end."""
|
| 698 |
-
if not view_glbs:
|
| 699 |
-
return gr.update(), gr.update()
|
| 700 |
-
nxt = (int(idx) + 1) % len(view_glbs)
|
| 701 |
-
return view_glbs[nxt], nxt
|
| 702 |
-
|
| 703 |
-
play_timer.tick(_advance, inputs=[frame_slider, view_state],
|
| 704 |
-
outputs=[model3d, frame_slider], show_progress="hidden")
|
| 705 |
-
play_btn.click(
|
| 706 |
-
lambda f: (gr.Timer(value=1.0 / float(f), active=True), True),
|
| 707 |
-
inputs=[play_speed], outputs=[play_timer, playing])
|
| 708 |
-
pause_btn.click(
|
| 709 |
-
lambda: (gr.Timer(active=False), False),
|
| 710 |
-
outputs=[play_timer, playing])
|
| 711 |
-
play_speed.change(
|
| 712 |
-
lambda f, p: gr.Timer(value=1.0 / float(f), active=bool(p)),
|
| 713 |
-
inputs=[play_speed, playing], outputs=[play_timer])
|
| 714 |
|
| 715 |
# ---------------- examples (thumbnail shown, click to load + run) ----------------
|
| 716 |
ex40 = sorted(glob.glob(os.path.join(APP_DIR, "examples", "seq40", "*.png")))[:MAX_IMAGES]
|
|
@@ -718,9 +882,8 @@ def build_demo():
|
|
| 718 |
run_inputs = [files, video, mode, process_res, remove_bg,
|
| 719 |
conf_percentile, n_tracks, track_k, track_threshold,
|
| 720 |
fps, max_frames]
|
| 721 |
-
run_outputs = [
|
| 722 |
-
tracks2d_vid, tracks_zip,
|
| 723 |
-
play_row, status]
|
| 724 |
|
| 725 |
def _thumb():
|
| 726 |
return gr.Image(value=ex40[0], height=150, show_label=False,
|
|
|
|
| 526 |
|
| 527 |
_say(1.0, "Done.")
|
| 528 |
|
|
|
|
| 529 |
if not has_canon:
|
| 530 |
log.append("WARNING: model produced no canonical output — canonical "
|
| 531 |
"video and point tracks were skipped (check the checkpoint).")
|
| 532 |
|
| 533 |
status = "\n".join(f"• {m}" for m in log)
|
| 534 |
+
# view_glbs is the per-frame track point cloud (.glb), glTF-aligned and
|
| 535 |
+
# track-colored. They go to the hidden file list, whose URLs the
|
| 536 |
+
# client-side three.js player preloads and animates (see VIEWER_JS).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
return (
|
| 538 |
+
view_glbs or None,
|
| 539 |
canonical_vid,
|
| 540 |
depth_vid,
|
| 541 |
normals_vid,
|
| 542 |
tracks2d_vid,
|
| 543 |
tracks_zip,
|
|
|
|
|
|
|
|
|
|
| 544 |
status,
|
| 545 |
)
|
| 546 |
except gr.Error:
|
|
|
|
| 550 |
raise gr.Error(f"Inference failed: {e}\n\n{tb[-1500:]}")
|
| 551 |
|
| 552 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
# --------------------------------------------------------------------------- #
|
| 554 |
# UI
|
| 555 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 566 |
[Code](https://github.com/kocasariumut/FaceAnything)
|
| 567 |
"""
|
| 568 |
|
| 569 |
+
# --------------------------------------------------------------------------- #
|
| 570 |
+
# Custom 3D viewer (client-side three.js).
|
| 571 |
+
#
|
| 572 |
+
# gradio's Model3D re-fetches and re-parses a .glb from the server on every
|
| 573 |
+
# frame, so animating it flashes white (the next cloud isn't on the client yet).
|
| 574 |
+
# Instead we load *every* frame's .glb once into a three.js scene and animate by
|
| 575 |
+
# toggling which frame is visible — no per-frame network/parse, the points stay
|
| 576 |
+
# on screen the whole time, and the full (un-subsampled) cloud is kept.
|
| 577 |
+
# --------------------------------------------------------------------------- #
|
| 578 |
+
THREE_HEAD = """
|
| 579 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.137.0/build/three.min.js"></script>
|
| 580 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/loaders/GLTFLoader.js"></script>
|
| 581 |
+
<script src="https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/controls/OrbitControls.js"></script>
|
| 582 |
+
"""
|
| 583 |
+
|
| 584 |
+
VIEWER_MARKUP = """
|
| 585 |
+
<div class="fa-viewer-root" style="width:100%;">
|
| 586 |
+
<div class="fa-canvas-wrap" style="position:relative;width:100%;height:420px;background:#ffffff;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;">
|
| 587 |
+
<div class="fa-overlay" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#6b7280;font-family:sans-serif;font-size:14px;text-align:center;padding:0 16px;">Run a reconstruction to view the 3D point tracks here.</div>
|
| 588 |
+
</div>
|
| 589 |
+
<div class="fa-controls" style="display:none;gap:10px;align-items:center;padding:8px 4px 2px;font-family:sans-serif;font-size:13px;flex-wrap:wrap;">
|
| 590 |
+
<button class="fa-play" type="button" style="cursor:pointer;padding:4px 12px;border:1px solid #d1d5db;border-radius:6px;background:#f9fafb;">▶ Play</button>
|
| 591 |
+
<span style="display:inline-flex;align-items:center;gap:6px;">Speed <input class="fa-speed" type="range" min="1" max="30" step="1" value="12" style="width:90px;vertical-align:middle;"><span class="fa-fps">12 fps</span></span>
|
| 592 |
+
<input class="fa-scrub" type="range" min="0" max="0" step="1" value="0" style="flex:1;min-width:120px;vertical-align:middle;">
|
| 593 |
+
<span class="fa-frame" style="min-width:64px;text-align:right;color:#374151;">– / –</span>
|
| 594 |
+
</div>
|
| 595 |
+
</div>
|
| 596 |
+
"""
|
| 597 |
+
|
| 598 |
+
# Runs once when the HTML component mounts; sets up the three.js scene and
|
| 599 |
+
# exposes window.faViewer.load(urls) for the file bridge below to call.
|
| 600 |
+
VIEWER_JS = """
|
| 601 |
+
(function(){
|
| 602 |
+
if (element.__faInit) return;
|
| 603 |
+
element.__faInit = true;
|
| 604 |
+
var wrap = element.querySelector('.fa-canvas-wrap');
|
| 605 |
+
var overlay = element.querySelector('.fa-overlay');
|
| 606 |
+
var controls = element.querySelector('.fa-controls');
|
| 607 |
+
var playBtn = element.querySelector('.fa-play');
|
| 608 |
+
var speedEl = element.querySelector('.fa-speed');
|
| 609 |
+
var fpsEl = element.querySelector('.fa-fps');
|
| 610 |
+
var scrubEl = element.querySelector('.fa-scrub');
|
| 611 |
+
var frameEl = element.querySelector('.fa-frame');
|
| 612 |
+
if (!wrap) return;
|
| 613 |
+
var renderer, scene, camera, orbit, group;
|
| 614 |
+
var frames = [], cur = 0, playing = false, fps = 12, acc = 0, last = 0, loadToken = 0;
|
| 615 |
+
|
| 616 |
+
function waitThree(cb, tries){
|
| 617 |
+
tries = tries || 0;
|
| 618 |
+
if (window.THREE && THREE.GLTFLoader && THREE.OrbitControls) { cb(); }
|
| 619 |
+
else if (tries > 200) { setOverlay('Could not load the 3D viewer (three.js) \\u2014 check your network / ad-blocker.'); }
|
| 620 |
+
else { setTimeout(function(){ waitThree(cb, tries + 1); }, 60); }
|
| 621 |
+
}
|
| 622 |
+
function setOverlay(msg){
|
| 623 |
+
if (!overlay) return;
|
| 624 |
+
if (msg) { overlay.textContent = msg; overlay.style.display = 'flex'; }
|
| 625 |
+
else { overlay.style.display = 'none'; }
|
| 626 |
+
}
|
| 627 |
+
function resize(){
|
| 628 |
+
if (!renderer) return;
|
| 629 |
+
var w = wrap.clientWidth || 1, h = wrap.clientHeight || 1;
|
| 630 |
+
renderer.setSize(w, h, false);
|
| 631 |
+
camera.aspect = w / h; camera.updateProjectionMatrix();
|
| 632 |
+
}
|
| 633 |
+
function applyStyle(root){
|
| 634 |
+
root.traverse(function(o){
|
| 635 |
+
if (o.isPoints && o.material){
|
| 636 |
+
o.material.size = 2.5;
|
| 637 |
+
o.material.sizeAttenuation = false;
|
| 638 |
+
o.material.vertexColors = true;
|
| 639 |
+
o.material.needsUpdate = true;
|
| 640 |
+
}
|
| 641 |
+
});
|
| 642 |
+
}
|
| 643 |
+
function firstPoints(root){
|
| 644 |
+
var found = null;
|
| 645 |
+
root.traverse(function(o){ if (!found && o.isPoints) found = o; });
|
| 646 |
+
return found;
|
| 647 |
+
}
|
| 648 |
+
function showFrame(i){
|
| 649 |
+
if (!frames.length) return;
|
| 650 |
+
if (i < 0) i = 0;
|
| 651 |
+
if (i > frames.length - 1) i = frames.length - 1;
|
| 652 |
+
for (var k = 0; k < frames.length; k++){ frames[k].visible = (k === i); }
|
| 653 |
+
cur = i;
|
| 654 |
+
if (scrubEl) scrubEl.value = String(i);
|
| 655 |
+
if (frameEl) frameEl.textContent = (i + 1) + ' / ' + frames.length;
|
| 656 |
+
}
|
| 657 |
+
function fitCamera(){
|
| 658 |
+
if (!frames.length) return;
|
| 659 |
+
var box = new THREE.Box3().setFromObject(frames[0]);
|
| 660 |
+
if (box.isEmpty()) return;
|
| 661 |
+
var c = box.getCenter(new THREE.Vector3());
|
| 662 |
+
var s = box.getSize(new THREE.Vector3());
|
| 663 |
+
var r = Math.max(s.x, s.y, s.z) * 0.5 || 0.5;
|
| 664 |
+
var d = (r / Math.tan(camera.fov * Math.PI / 360)) * 1.7;
|
| 665 |
+
orbit.target.copy(c);
|
| 666 |
+
camera.near = Math.max(d / 200, 0.0005);
|
| 667 |
+
camera.far = d * 50 + r * 20;
|
| 668 |
+
camera.position.set(c.x, c.y, c.z + d);
|
| 669 |
+
camera.updateProjectionMatrix();
|
| 670 |
+
orbit.update();
|
| 671 |
+
}
|
| 672 |
+
function clearFrames(){
|
| 673 |
+
for (var k = 0; k < frames.length; k++){
|
| 674 |
+
group.remove(frames[k]);
|
| 675 |
+
if (frames[k].geometry) frames[k].geometry.dispose();
|
| 676 |
+
if (frames[k].material) frames[k].material.dispose();
|
| 677 |
+
}
|
| 678 |
+
frames = []; cur = 0;
|
| 679 |
+
}
|
| 680 |
+
function play(){ if (frames.length < 2) return; playing = true; last = 0; acc = 0; if (playBtn) playBtn.innerHTML = '\\u23F8 Pause'; }
|
| 681 |
+
function pause(){ playing = false; if (playBtn) playBtn.innerHTML = '\\u25B6 Play'; }
|
| 682 |
+
function toggle(){ if (playing) pause(); else play(); }
|
| 683 |
+
function animate(ts){
|
| 684 |
+
requestAnimationFrame(animate);
|
| 685 |
+
if (orbit) orbit.update();
|
| 686 |
+
if (playing && frames.length > 1){
|
| 687 |
+
if (!last) last = ts;
|
| 688 |
+
acc += (ts - last); last = ts;
|
| 689 |
+
var interval = 1000 / Math.max(1, fps);
|
| 690 |
+
if (acc >= interval){
|
| 691 |
+
var steps = Math.floor(acc / interval);
|
| 692 |
+
acc -= steps * interval;
|
| 693 |
+
showFrame((cur + steps) % frames.length);
|
| 694 |
+
}
|
| 695 |
+
} else { last = ts; }
|
| 696 |
+
if (renderer && scene && camera) renderer.render(scene, camera);
|
| 697 |
+
}
|
| 698 |
+
function initThree(){
|
| 699 |
+
if (renderer) return;
|
| 700 |
+
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
| 701 |
+
renderer.setPixelRatio(1);
|
| 702 |
+
renderer.setClearColor(0xffffff, 1);
|
| 703 |
+
if (THREE.sRGBEncoding) renderer.outputEncoding = THREE.sRGBEncoding;
|
| 704 |
+
renderer.domElement.style.display = 'block';
|
| 705 |
+
renderer.domElement.style.width = '100%';
|
| 706 |
+
renderer.domElement.style.height = '100%';
|
| 707 |
+
wrap.appendChild(renderer.domElement);
|
| 708 |
+
scene = new THREE.Scene();
|
| 709 |
+
scene.background = new THREE.Color(0xffffff);
|
| 710 |
+
camera = new THREE.PerspectiveCamera(50, 1, 0.001, 1000);
|
| 711 |
+
camera.position.set(0, 0, 2);
|
| 712 |
+
orbit = new THREE.OrbitControls(camera, renderer.domElement);
|
| 713 |
+
orbit.enableDamping = true; orbit.dampingFactor = 0.1;
|
| 714 |
+
group = new THREE.Group();
|
| 715 |
+
scene.add(group);
|
| 716 |
+
resize();
|
| 717 |
+
if (window.ResizeObserver) { new ResizeObserver(resize).observe(wrap); }
|
| 718 |
+
else { window.addEventListener('resize', resize); }
|
| 719 |
+
requestAnimationFrame(animate);
|
| 720 |
+
}
|
| 721 |
+
function finishLoad(token, loaded){
|
| 722 |
+
if (token !== loadToken) return;
|
| 723 |
+
frames = loaded.filter(function(p){ return p; });
|
| 724 |
+
if (!frames.length){ setOverlay('Failed to load the 3D point clouds.'); return; }
|
| 725 |
+
for (var k = 0; k < frames.length; k++){ frames[k].visible = false; group.add(frames[k]); }
|
| 726 |
+
setOverlay('');
|
| 727 |
+
if (controls) controls.style.display = 'flex';
|
| 728 |
+
if (scrubEl){ scrubEl.min = '0'; scrubEl.max = String(frames.length - 1); scrubEl.value = '0'; }
|
| 729 |
+
fitCamera();
|
| 730 |
+
showFrame(0);
|
| 731 |
+
if (frames.length > 1) play();
|
| 732 |
+
}
|
| 733 |
+
function load(urls){
|
| 734 |
+
waitThree(function(){
|
| 735 |
+
initThree();
|
| 736 |
+
var token = ++loadToken;
|
| 737 |
+
pause();
|
| 738 |
+
clearFrames();
|
| 739 |
+
if (controls) controls.style.display = 'none';
|
| 740 |
+
if (!urls || !urls.length){ setOverlay('No 3D point tracks for this run.'); return; }
|
| 741 |
+
setOverlay('Loading 3D sequence\\u2026 0 / ' + urls.length);
|
| 742 |
+
var loader = new THREE.GLTFLoader();
|
| 743 |
+
var loaded = new Array(urls.length);
|
| 744 |
+
var done = 0;
|
| 745 |
+
function tick(){
|
| 746 |
+
done++;
|
| 747 |
+
setOverlay('Loading 3D sequence\\u2026 ' + done + ' / ' + urls.length);
|
| 748 |
+
if (done === urls.length) finishLoad(token, loaded);
|
| 749 |
+
}
|
| 750 |
+
urls.forEach(function(url, idx){
|
| 751 |
+
loader.load(url, function(gltf){
|
| 752 |
+
if (token !== loadToken) return;
|
| 753 |
+
var pts = firstPoints(gltf.scene);
|
| 754 |
+
if (pts){ applyStyle(pts); loaded[idx] = pts; }
|
| 755 |
+
tick();
|
| 756 |
+
}, undefined, function(){ if (token === loadToken){ loaded[idx] = null; tick(); } });
|
| 757 |
+
});
|
| 758 |
+
});
|
| 759 |
+
}
|
| 760 |
+
if (playBtn) playBtn.addEventListener('click', toggle);
|
| 761 |
+
if (speedEl) speedEl.addEventListener('input', function(){ fps = parseInt(speedEl.value, 10) || 12; if (fpsEl) fpsEl.textContent = fps + ' fps'; });
|
| 762 |
+
if (scrubEl) scrubEl.addEventListener('input', function(){ pause(); showFrame(parseInt(scrubEl.value, 10) || 0); });
|
| 763 |
+
window.faViewer = { load: load, play: play, pause: pause, setFrame: showFrame };
|
| 764 |
+
waitThree(function(){ initThree(); });
|
| 765 |
+
})();
|
| 766 |
+
"""
|
| 767 |
+
|
| 768 |
+
# Bridge: when the hidden file list (served .glb URLs) changes, hand the URLs to
|
| 769 |
+
# the three.js viewer. Runs purely client-side (no server round-trip).
|
| 770 |
+
ANIM_BRIDGE_JS = """
|
| 771 |
+
(files) => {
|
| 772 |
+
try {
|
| 773 |
+
var list = (files || []).map(function(f){ return f && (f.url || f.path); }).filter(Boolean);
|
| 774 |
+
if (window.faViewer) { window.faViewer.load(list); }
|
| 775 |
+
} catch (e) { console.error('faViewer load error', e); }
|
| 776 |
+
}
|
| 777 |
+
"""
|
| 778 |
+
|
| 779 |
|
| 780 |
def build_demo():
|
| 781 |
with gr.Blocks(title="Face Anything") as demo:
|
| 782 |
gr.Markdown(DESCRIPTION)
|
|
|
|
| 783 |
|
| 784 |
with gr.Row():
|
| 785 |
# ---------------- inputs ----------------
|
|
|
|
| 837 |
|
| 838 |
# ---------------- outputs ----------------
|
| 839 |
with gr.Column(scale=1):
|
| 840 |
+
gr.Markdown("**3D point cloud with colorful tracks** · "
|
| 841 |
+
"loads the whole sequence, then plays smoothly")
|
| 842 |
+
# client-side three.js player: all frames preloaded once, then
|
| 843 |
+
# animated by visibility toggle (no per-frame reload → no white
|
| 844 |
+
# flashes; full, un-subsampled cloud). See VIEWER_JS above.
|
| 845 |
+
viewer = gr.HTML(
|
| 846 |
+
value=VIEWER_MARKUP, head=THREE_HEAD, js_on_load=VIEWER_JS,
|
| 847 |
+
show_label=False,
|
| 848 |
)
|
| 849 |
+
# hidden: run() puts the per-frame .glb files here so gradio
|
| 850 |
+
# serves them; ANIM_BRIDGE_JS hands their URLs to the viewer.
|
| 851 |
+
anim_files = gr.File(file_count="multiple", visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
tracks_zip = gr.File(
|
| 853 |
label="Download point clouds (.zip: tracks/ + points/)")
|
| 854 |
with gr.Tab("Canonical (2D)"):
|
|
|
|
| 869 |
inputs=[files, video, mode, process_res, remove_bg,
|
| 870 |
conf_percentile, n_tracks, track_k, track_threshold,
|
| 871 |
fps, max_frames],
|
| 872 |
+
outputs=[anim_files, canonical_vid, depth_vid, normals_vid,
|
| 873 |
+
tracks2d_vid, tracks_zip, status],
|
| 874 |
concurrency_limit=1,
|
| 875 |
)
|
| 876 |
+
# when the served .glb list changes, push the URLs to the three.js player
|
| 877 |
+
anim_files.change(None, inputs=anim_files, outputs=None, js=ANIM_BRIDGE_JS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
# ---------------- examples (thumbnail shown, click to load + run) ----------------
|
| 880 |
ex40 = sorted(glob.glob(os.path.join(APP_DIR, "examples", "seq40", "*.png")))[:MAX_IMAGES]
|
|
|
|
| 882 |
run_inputs = [files, video, mode, process_res, remove_bg,
|
| 883 |
conf_percentile, n_tracks, track_k, track_threshold,
|
| 884 |
fps, max_frames]
|
| 885 |
+
run_outputs = [anim_files, canonical_vid, depth_vid, normals_vid,
|
| 886 |
+
tracks2d_vid, tracks_zip, status]
|
|
|
|
| 887 |
|
| 888 |
def _thumb():
|
| 889 |
return gr.Image(value=ex40[0], height=150, show_label=False,
|