| |
| |
| """Stripped-down kimodo viewer: type a prompt, see a 5-second SMPL-X animation. |
| |
| Usage (inside the demo container, with the text-encoder service running): |
| python -m kimodo.scripts.run_simple_app |
| |
| Set SERVER_PORT to override the default 7860. Reads TEXT_ENCODER_URL from env |
| the same way kimodo's full demo does. |
| """ |
|
|
| import os |
| import threading |
| import time |
| import traceback |
|
|
| import torch |
| import viser |
|
|
| from kimodo.model.load_model import load_model |
| from kimodo.viz.playback import CharacterMotion |
| from kimodo.viz.scene import Character |
|
|
|
|
| MODEL_NAME = "kimodo-smplx-rp" |
| DEFAULT_PROMPT = "A person walks forward." |
| CLIP_SECONDS = 5.0 |
| NUM_DENOISING_STEPS = 20 |
|
|
|
|
| def _passthrough(iterable, *args, **kwargs): |
| return iterable |
|
|
|
|
| def main() -> None: |
| device = "cuda:0" if torch.cuda.is_available() else "cpu" |
| print(f"Loading {MODEL_NAME} on {device}...") |
| model = load_model(MODEL_NAME, device=device) |
| skeleton = model.motion_rep.skeleton.to(device) |
| fps = float(model.motion_rep.fps) |
| num_frames = int(round(CLIP_SECONDS * fps)) |
| print(f"Model loaded. fps={fps}, clip={num_frames} frames") |
|
|
| port = int(os.environ.get("SERVER_PORT", 7860)) |
| server = viser.ViserServer(host="0.0.0.0", port=port, label="Kimodo Simple") |
| server.scene.set_up_direction("+y") |
| server.scene.world_axes.visible = False |
|
|
| state_lock = threading.Lock() |
| client_state: dict[int, dict] = {} |
|
|
| @server.on_client_connect |
| def _on_connect(client: viser.ClientHandle) -> None: |
| cid = client.client_id |
| character = Character( |
| name=f"character_{cid}", |
| server=client, |
| skeleton=skeleton, |
| mesh_mode="smplx_skin", |
| create_skeleton_mesh=False, |
| create_skinned_mesh=True, |
| visible_skinned_mesh=True, |
| ) |
|
|
| |
| rest_pos, rest_rot = character.get_pose() |
| rest_motion = CharacterMotion( |
| character, |
| rest_pos[None].repeat(num_frames, 1, 1), |
| rest_rot[None].repeat(num_frames, 1, 1, 1), |
| ) |
| rest_motion.set_frame(0) |
|
|
| with state_lock: |
| client_state[cid] = { |
| "character": character, |
| "motion": rest_motion, |
| "frame": 0, |
| "playing": False, |
| } |
|
|
| with client.gui.add_folder("Prompt"): |
| prompt_box = client.gui.add_text("Text", initial_value=DEFAULT_PROMPT) |
| generate_btn = client.gui.add_button("Generate (5s)") |
| status = client.gui.add_markdown("Ready.") |
|
|
| @generate_btn.on_click |
| def _on_generate(_) -> None: |
| generate_btn.disabled = True |
| status.content = "Generating..." |
| try: |
| with torch.no_grad(): |
| out = model( |
| [prompt_box.value], |
| num_frames, |
| NUM_DENOISING_STEPS, |
| progress_bar=_passthrough, |
| ) |
| joints_pos = out["posed_joints"][0] |
| joints_rot = out["global_rot_mats"][0] |
| foot_contacts = out.get("foot_contacts") |
| if foot_contacts is not None: |
| foot_contacts = foot_contacts[0] |
|
|
| with state_lock: |
| s = client_state.get(cid) |
| if s is None: |
| return |
| s["motion"] = CharacterMotion(s["character"], joints_pos, joints_rot, foot_contacts) |
| s["frame"] = 0 |
| s["playing"] = True |
| status.content = "Playing (looped)." |
| except Exception as e: |
| status.content = f"Error: {type(e).__name__}: {e}" |
| print(f"Generation failed for client {cid}:") |
| traceback.print_exc() |
| finally: |
| generate_btn.disabled = False |
|
|
| @server.on_client_disconnect |
| def _on_disconnect(client: viser.ClientHandle) -> None: |
| with state_lock: |
| client_state.pop(client.client_id, None) |
|
|
| def _playback_loop() -> None: |
| period = 1.0 / fps |
| next_tick = time.time() |
| while True: |
| with state_lock: |
| snapshot = [(cid, s) for cid, s in client_state.items() if s["playing"] and s["motion"] is not None] |
| for cid, s in snapshot: |
| try: |
| s["motion"].set_frame(s["frame"]) |
| except Exception as e: |
| print(f"Playback error for client {cid}: {e}") |
| continue |
| with state_lock: |
| st = client_state.get(cid) |
| if st is not None and st["motion"] is s["motion"]: |
| st["frame"] = (st["frame"] + 1) % st["motion"].length |
|
|
| next_tick += period |
| sleep_for = next_tick - time.time() |
| if sleep_for > 0: |
| time.sleep(sleep_for) |
| else: |
| |
| next_tick = time.time() |
|
|
| threading.Thread(target=_playback_loop, daemon=True).start() |
| print(f"Simple app running at http://localhost:{port}") |
| while True: |
| time.sleep(3600) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|